Skip to content

Commit 384b9fc

Browse files
docs: record GA4 search analytics setup, fix CLAUDE.md filename case
Adds a Search analytics section covering the search and search_result_click events, why Enhanced Measurement site search and browser-history pageviews are deliberately off, the three registered custom dimensions and their parameter-keying caveat, and the constraints on changing the click tracking. Also renames claude.md to CLAUDE.md so it resolves on case-sensitive filesystems, where Claude Code would otherwise not find it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 1ac67cd commit 384b9fc

1 file changed

Lines changed: 70 additions & 0 deletions

File tree

claude.md renamed to CLAUDE.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,3 +148,73 @@ response headers, so `Accept: text/markdown` content negotiation and
148148
- The commands layout is `layouts/commands/single.html`, NOT
149149
`layouts/_default/single.html` — Hugo picks it up automatically because
150150
content is in `content/commands/`.
151+
152+
## Search analytics (GA4)
153+
154+
GA4 property `G-WZ9LF10DRD`, loaded in `_default/baseof.html`. Two search
155+
surfaces report to it, and both are hand-written — **GA4 Enhanced Measurement
156+
does not track search on this site and must not be re-enabled.**
157+
158+
Both `static/js/site-search.js` (modal, Ctrl+K) and
159+
`static/js/advanced-search.js` (`/search/` page) send:
160+
161+
- `search``{ search_term }`, from `reportSearch()`. Fires 1200ms after
162+
typing stops, not on the 150ms render debounce, so the report holds
163+
questions rather than keystrokes. Deduped against `lastReported`, including
164+
backspacing over a term already sent.
165+
- `search_result_click``{ search_term, link_url, result_position }`, from
166+
`reportResultClick()`. `search_term` is `lastQuery` (the query that produced
167+
the visible results, not what is in the box at click time);
168+
`result_position` is 1-based, read from the `data-search-position` attribute
169+
written in `displayResults()`.
170+
171+
`search_result_click` is the whole point of the pair: `search` says what people
172+
asked, `search_result_click` says whether they found it. Terms with `search`
173+
events and no matching `search_result_click` are the content gaps.
174+
175+
### Why Enhanced Measurement is off (turned off 2026-07-31)
176+
177+
- **Site search** — it cannot see the Ctrl+K modal, which has no URL. Leaving
178+
it on meant two event names for one user action across two surfaces, plus
179+
double-counting on `/search/`, where `updateUrl()` writes `?q=` and `q` is a
180+
default GA4 site-search parameter.
181+
- **Page views → "Page changes based on browser history events"** — the site
182+
has no `pushState` anywhere and only two `replaceState` callers
183+
(`advanced-search.js` `updateUrl()`, `commands-page.js` filter state).
184+
Neither is a navigation, so this only manufactured fake pageviews on the two
185+
busiest interactive pages.
186+
187+
Both changes produce a discontinuity dated 2026-07-31: `view_search_results`
188+
stops accruing, and `/search/` + `/commands` pageviews step down.
189+
190+
### Registered custom dimensions
191+
192+
All Event scope. GA4 keys custom dimensions by **parameter name across all
193+
events**, so every report must break down by Event name or these blend.
194+
195+
| Dimension | Parameter | Also carried by |
196+
| --- | --- | --- |
197+
| Site search term | `search_term` | `search` and `search_result_click` |
198+
| Clicked link URL | `link_url` | Enhanced Measurement outbound clicks (still on) |
199+
| Search result position | `result_position` | nothing else |
200+
201+
Registration is not retroactive and takes 24–48h to populate. `result_position`
202+
is a dimension, not a custom metric.
203+
204+
### Rules for changing this code
205+
206+
- Never intercept the click. No `preventDefault`, no waiting on an
207+
`event_callback`. gtag.js transports with `navigator.sendBeacon` and flushes
208+
on `pagehide` (verified against the live bundle), so the hit survives unload
209+
on its own. A blocked tag must never break navigation.
210+
- Every reporting path is guarded by `typeof gtag !== 'function'`.
211+
- Result clicks use **one delegated listener on the container**, bound in
212+
`setupEventListeners()`. `displayResults()` replaces `innerHTML` on every
213+
keystroke, so per-anchor listeners are destroyed immediately.
214+
- Bind **both `click` and `auxclick`**. Middle-click opens a new tab but fires
215+
`auxclick`, not `click`; filter `auxclick` on `button === 1` so right-click
216+
context menus are not counted.
217+
- `checkUrlQuery()` calls `reportSearch()` so shared and bookmarked `?q=`
218+
links still count. Enhanced Measurement used to cover that case.
219+
- Renaming a parameter means burning a custom dimension slot — they can be
220+
archived but never deleted, and there are 50.

0 commit comments

Comments
 (0)