@@ -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