fix(analytics): stop Plausible double-counting; distinct permalink event; honest privacy copy - #172
Conversation
…st copy Follow-up to the merged analytics restore (#171), from Obvious code review. - HIGH: Plausible auto-tracked a pageview on load while the app fired its own on mount (plus a third on permalink hydration). Switch to manual mode (script.manual.js): Plausible fires nothing on its own; the app sends exactly one pageview per mount, and the /:tool/:id hydration path now fires a distinct gtag('event','permalink_view') + window.plausible('Permalink View') instead of a duplicate pageview. verify-seo.sh fails if the auto-tracking script.js ever ships. - MEDIUM: FAQ privacy copy now characterizes each tag honestly — Plausible counts visits cookieless; the Google tag is a standard conversion- measurement tag using page URL and referrer. "Never see your data" kept (still true for both). - MEDIUM: dead UA-46942708 re-added to the CI remnant ban so the never-ship invariant is enforced at source level, not only in built HTML; config.ts and test literals rephrased/assembled so the gate stays false-positive-free. - GA4 send_page_view:false behavior (already correct) unchanged.
There was a problem hiding this comment.
Obvious Code Review — Pass 1
Verdict: COMMENT (advisory)
- Blocker: 0 · High: 0 · Medium: 0 · Suggestion: 1
PR #171 follow-up — all findings verified resolved
- Plausible double-count (High, #171) — resolved.
app/index.html:95shipsscript.manual.js(manual mode: no auto pageview); the app's singletrackPageview()atmain.tsx:24is now Plausible's only pageview source. Verified the deferred Plausible script precedes the module bundle in the builtdist/index.html(offsets 4173 < 4266), sowindow.plausibleis always defined at mount — the fix neither double-counts nor silently undercounts. Negative gate (verify-seo.sh) fails loudly ifscript.js"ever ships again. - FAQ privacy copy (Medium security, #171) — resolved.
faqContent.ts:26no longer lumps the Google tag into "counts only that a visit happened"; each tag is now characterized separately and truthfully. - UA-46942708 source-level ban (Medium reliability, #171) — resolved. Added to
remnant_reinci.yml:96; simulated locally over all gate targets, clean.docs/is outside the gate targets, so the verbatim literals inverification-report.mdcannot trip it; the test asserts against the ID via fragments, avoiding a self-trip. - Permalink event identity (Suggestion, #171) — resolved.
trackPermalinkView()firespermalink_view(gtag) /Permalink View(Plausible), with a covering double-count guard test.
Verified: 109/109 vitest at head d133e11; committed dist/ rebuilt and consistent (new bundle index-DzMikUmS.js referenced, stale bundle removed, manual-mode assertions target the built HTML).
Suggestion (1)
Suggestion · hygiene — app/src/seo/faqContent.ts:26: the new copy says the Google tag "uses the page URL and referrer to measure ad performance." A standard Google Ads conversion tag also sets first-party conversion cookies (_gcl_au via the conversion linker) on the page. The sentence is accurate as far as it goes (and no longer overclaims), but since this FAQ entry exists to be candid about what analytics touches, one clause — e.g. "…and a first-party conversion cookie to measure ad performance" — would make the enumeration complete. Author's discretion.
|
Thanks — glad all findings verified resolved. On the one suggestion (mentioning the first-party conversion cookie in the FAQ clause): fair point, and Im adopting it in the follow-up analytics-events PR rather than pushing here, so this PR merges on its current green CI. Tracking it for that PR. |
* feat(analytics): conversion and export event instrumentation
Extend the analytics helper with trackEvent(name, props), fanning each
event from one call site to both surfaces: window.plausible under its
capitalized goal name with { props }, and gtag as a lowercase
machine-named event (Google Ads today; GA4 automatically once
VITE_GA4_MEASUREMENT_ID is configured).
- conversion: direction + input method (paste/file/drag/permalink) +
input byte bucket. Fires once per first stable output after the input
settles ~2s (only on a valid result), keyed by a direction+method+length
signature; refires only when the input actually changes; capped at one
event per 2s window. Picker/drop/permalink successes fire immediately
under the same gates.
- export: via (copy|download) + resolved format, on every intentional
copy/download click, no debounce.
- Ads seam documented: conversion events carry no send_to yet; the
conversion action's label lands in one line once David creates the
action in the Ads console.
- FAQ privacy clause now mentions the first-party conversion cookie
(adopting the deferred advisory from the #172 review).
- docs/verification-report.md: "Analytics events" subsection covering
event names, props, fire points, and dashboard expectations
(Plausible goals conversion/export for David to add; GA4 receives the
same events once VITE_GA4_MEASUREMENT_ID is set).
- Tests: 12 new unit tests (debounce, fire-once-per-signature, 2s window,
exact gtag/Plausible payloads, no-op safety) plus App-level integration
for settle semantics and export clicks. Full suite: 123 passing.
- dist rebuilt and committed fresh (dist-freshness gate); verify-seo.sh
passing.
* fix(analytics): fingerprint conversion signature; reuse memoized validity
Address the two Medium findings from the Obvious review:
- Fold an FNV-1a content fingerprint into the conversion signature so
genuinely new inputs of the same byte length count, and cancel()
forgets the last signature so clear + re-paste of identical content
is counted again. Both undercounts previously suppressed real
conversions on the launch dashboard.
- Settle-path validity now reads the memoized result via
converterStateRef instead of re-running the full conversion; the full
parse+serialize is no longer paid twice on the main thread. Discrete
fires (upload/drop/permalink) still convert directly when React has
not rendered the new input, and the permalink hydration refreshes the
ref with hydrated direction/options before its immediate fire.
Adds regression tests for same-length refire and clear-then-re-paste.
---------
Co-authored-by: Obvious <obvious@obvious.ai>
Why
Code review on #171 (the merged analytics restore) found a launch-blocking measurement bug: Plausible's standard
script.jsfires a pageview on load and the app fires another on mount (a third on permalink hydration), so every visit was counted 2–3x from day one. GA4 already had auto-pageview suppressed (send_page_view: false); Plausible didn't. Two medium findings rode along: the FAQ privacy copy mischaracterized the Google Ads gtag as "counts only that a visit happened", and droppinggtagfrom the CI remnant ban had also removed the only source-level tripwire for the deadUA-46942708ID.What
https://plausible.io/js/script.manual.js(data-domain unchanged). Plausible fires nothing automatically; the app'strackPageview()(mount, main.tsx) is its only pageview source.verify-seo.shnow assertsscript.manual.jsships and fails if the auto-trackingscript.js"reappears; unit tests pin manual mode in the built dist./:tool/:idhydration effect now firesgtag('event','permalink_view')+window.plausible('Permalink View')instead of a duplicate pageview (the mount pageview already carried the permalink URL). NewtrackPermalinkView()inapp/src/analytics/analytics.tswith tests asserting it never sendspage_viewor a Plausiblepageview.UA-46942708re-added toremnant_re(banned everywhere in the scanned tree — it collects nothing, so any reintroduction is a bug). To keep the gate false-positive-free, the literal is gone fromconfig.ts's comment and assembled from fragments in the test (with comments explaining why). GA4'ssend_page_view: falseis untouched.docs/verification-report.mdupdated to match.How to Review
app/index.html— the manual-mode Plausible script and comments.app/src/analytics/analytics.ts—trackPermalinkView; docs on the single-source pageview invariant.app/src/App.tsx/app/src/main.tsx— the two call sites (mount = one pageview; hydration = event only)..github/workflows/ci.yml,.github/scripts/verify-seo.sh— gate changes.Verification
trackPermalinkViewsends the event and provably nopage_view/pageview), lint + typecheck clean, production build + prerender green, dist committed fresh,verify-seo.shpassing including the new manual-mode assertion and auto-script negative, and the full CI remnant grep simulated locally over all targets with the newUA-46942708term: clean.🔗 Obvious Project · 🧵 Obvious Thread