From a933c10dd80363d1396cd874b26fc18ab15ec570 Mon Sep 17 00:00:00 2001 From: appdevelopsk Date: Fri, 6 Nov 2026 19:35:14 +0900 Subject: [PATCH 1/3] fix(analytics): report finalized catalog searches, not initial keystrokes --- apps.html | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/apps.html b/apps.html index 00eba4c..ea463a3 100644 --- a/apps.html +++ b/apps.html @@ -1424,7 +1424,7 @@

Chaos - Stranger Missions

ai: ['debate-me','voice-memo','future-me','date-pilot','global-news','pantry-ai','name-maker','idea-market','life-trace'] }; const clean = value => value.normalize('NFKC').toLocaleLowerCase().trim(); - let goal = 'all', loggedSearch = false; + let goal = 'all', lastReportedSearch = ''; const slug = card => { const src = card.querySelector('img')?.getAttribute('src') || ''; return src.split('/').pop()?.replace(/\.png$/, '') || ''; @@ -1448,19 +1448,25 @@

Chaos - Stranger Missions

empty.hidden = shown !== 0; return shown; } - input.addEventListener('input', () => { - const shown = render(); - // Search strings might include personal details. Never transmit them. - if (input.value.trim() && !loggedSearch) { - fire('catalog_search_used', {results: shown, search_length_band: input.value.length < 5 ? 'short' : 'long'}); - loggedSearch = true; - } - if (!input.value.trim()) loggedSearch = false; + // Do not report the unfinished first keystroke as a completed search. + // Keep query text local; GA4 receives only a coarse length bucket and result count. + const reportSearch = () => { + const query = clean(input.value); + if (!query || query === lastReportedSearch) return; + fire('catalog_search_used', { + results: render(), search_length_band: query.length < 5 ? 'short' : 'long' + }); + lastReportedSearch = query; + }; + input.addEventListener('input', render); + input.addEventListener('blur', reportSearch); + input.addEventListener('keydown', event => { + if (event.key === 'Enter') reportSearch(); }); reset.addEventListener('click', () => { input.value = ''; goal = 'all'; - loggedSearch = false; + lastReportedSearch = ''; buttons.forEach(button => button.setAttribute('aria-pressed', String(button.dataset.goal === 'all'))); render(); input.focus(); From ab8d75e26ad80e28859700cb7f22a2a99da35fdd Mon Sep 17 00:00:00 2001 From: appdevelopsk Date: Fri, 6 Nov 2026 19:35:29 +0900 Subject: [PATCH 2/3] test(growth): assert search analytics only fires on committed input --- scripts/check_growth_live.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/scripts/check_growth_live.py b/scripts/check_growth_live.py index 48f3e25..71a9d29 100644 --- a/scripts/check_growth_live.py +++ b/scripts/check_growth_live.py @@ -74,6 +74,13 @@ def check(path, key, valid): check("/apps", "no_stale_live_store_claim", "92 free apps on Google Play" not in catalog) check("/apps", "store_click_tracking", "catalog_store_click" in catalog and "download_click" in catalog) check("/apps", "detail_click_tracking", "catalog_app_detail_click" in catalog) + check("/apps", "finalized_search_analytics", + "input.addEventListener('input', render)" in catalog and + "input.addEventListener('blur', reportSearch)" in catalog and + "if (event.key === 'Enter') reportSearch()" in catalog and + "query === lastReportedSearch" in catalog and + "lastReportedSearch = query" in catalog and + "loggedSearch" not in catalog) check("/apps", "search_includes_app_slug", "const app = slug(card), content = clean((card.textContent || '') + ' ' + app" in catalog and "app.replace(/-/g, ' ')" in catalog) From e3187ac5f422ff33f955665374ec80f64573ccf9 Mon Sep 17 00:00:00 2001 From: appdevelopsk Date: Fri, 6 Nov 2026 19:35:45 +0900 Subject: [PATCH 3/3] test(growth): catch premature, missing and duplicate search event tracking --- scripts/test_growth_live.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/scripts/test_growth_live.py b/scripts/test_growth_live.py index 3eedecc..89205a7 100644 --- a/scripts/test_growth_live.py +++ b/scripts/test_growth_live.py @@ -26,6 +26,9 @@ def valid_pages(): pages["/apps"] = ( '' + cards + " catalog_store_click download_click catalog_app_detail_click " + + "input.addEventListener('input', render) input.addEventListener('blur', reportSearch) " + + "if (event.key === 'Enter') reportSearch() query === lastReportedSearch " + + "lastReportedSearch = query " + "const app = slug(card), content = clean((card.textContent || '') + ' ' + app; app.replace(/-/g, ' ')" + 'id="finder-reset" reset.addEventListener(\'click\' input.value = \'\' goal = \'all\' render(); catalog_search_reset ' + " ".join(f'' for slug in SLUGS) @@ -187,6 +190,24 @@ def test_store_schema_package_mismatch_fails(self): "id=jp.appdevelopsk.testapp1", "id=jp.appdevelopsk.unlisted", 1) self.assertFalse(inspect(data)["passed"]) + def test_first_keystroke_analytics_rejected(self): + data = valid_pages() + data["/apps"] = data["/apps"].replace( + "input.addEventListener('input', render)", + "input.addEventListener('input', reportSearch)") + self.assertFalse(inspect(data)["passed"]) + + def test_missing_blur_search_reporting_rejected(self): + data = valid_pages() + data["/apps"] = data["/apps"].replace( + "input.addEventListener('blur', reportSearch)", "") + self.assertFalse(inspect(data)["passed"]) + + def test_repeated_search_reporting_rejected(self): + data = valid_pages() + data["/apps"] = data["/apps"].replace("query === lastReportedSearch", "") + self.assertFalse(inspect(data)["passed"]) + def test_missing_slug_search_fails(self): data = valid_pages() data["/apps"] = data["/apps"].replace(