Skip to content

fix(analytics): stop Plausible double-counting; distinct permalink event; honest privacy copy - #172

Merged
obvious-autobuild-staging[bot] merged 1 commit into
masterfrom
fix/analytics-followup
Sep 1, 2026
Merged

fix(analytics): stop Plausible double-counting; distinct permalink event; honest privacy copy#172
obvious-autobuild-staging[bot] merged 1 commit into
masterfrom
fix/analytics-followup

Conversation

@obvious-autobuild-staging

Copy link
Copy Markdown
Contributor

Why

Code review on #171 (the merged analytics restore) found a launch-blocking measurement bug: Plausible's standard script.js fires 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 dropping gtag from the CI remnant ban had also removed the only source-level tripwire for the dead UA-46942708 ID.

What

  • Plausible manual mode: head script is now https://plausible.io/js/script.manual.js (data-domain unchanged). Plausible fires nothing automatically; the app's trackPageview() (mount, main.tsx) is its only pageview source. verify-seo.sh now asserts script.manual.js ships and fails if the auto-tracking script.js" reappears; unit tests pin manual mode in the built dist.
  • Permalink path measures a distinct event, not a second visit: the /:tool/:id hydration effect now fires gtag('event','permalink_view') + window.plausible('Permalink View') instead of a duplicate pageview (the mount pageview already carried the permalink URL). New trackPermalinkView() in app/src/analytics/analytics.ts with tests asserting it never sends page_view or a Plausible pageview.
  • Honest FAQ copy: "The site's analytics never see your data: Plausible counts visits cookieless, and the Google tag is a standard conversion-measurement tag that uses the page URL and referrer to measure ad performance." Conversion-privacy claims unchanged and still true.
  • CI remnant gate: dead UA-46942708 re-added to remnant_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 from config.ts's comment and assembled from fragments in the test (with comments explaining why). GA4's send_page_view: false is untouched.
  • Docs: criterion-8 record in docs/verification-report.md updated to match.

How to Review

  • app/index.html — the manual-mode Plausible script and comments.
  • app/src/analytics/analytics.tstrackPermalinkView; 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.
  • Excluded on purpose: no dashboard verification (requires the live Plausible property David registers); the review's "verify 1 pageview per visit in the dashboard" step is a post-deploy manual check for David.

Verification

  • 109/109 tests (new: manual-mode script asserted in built HTML; trackPermalinkView sends the event and provably no page_view/pageview), lint + typecheck clean, production build + prerender green, dist committed fresh, verify-seo.sh passing including the new manual-mode assertion and auto-script negative, and the full CI remnant grep simulated locally over all targets with the new UA-46942708 term: clean.

🔗 Obvious Project · 🧵 Obvious Thread

…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.

@obvious-autobuild-staging obvious-autobuild-staging Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:95 ships script.manual.js (manual mode: no auto pageview); the app's single trackPageview() at main.tsx:24 is now Plausible's only pageview source. Verified the deferred Plausible script precedes the module bundle in the built dist/index.html (offsets 4173 < 4266), so window.plausible is always defined at mount — the fix neither double-counts nor silently undercounts. Negative gate (verify-seo.sh) fails loudly if script.js" ever ships again.
  • FAQ privacy copy (Medium security, #171) — resolved. faqContent.ts:26 no 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_re in ci.yml:96; simulated locally over all gate targets, clean. docs/ is outside the gate targets, so the verbatim literals in verification-report.md cannot trip it; the test asserts against the ID via fragments, avoiding a self-trip.
  • Permalink event identity (Suggestion, #171) — resolved. trackPermalinkView() fires permalink_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 · hygieneapp/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.


View full review in Obvious

@obvious-autobuild-staging

Copy link
Copy Markdown
Contributor Author

Replying to this comment

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.

@obvious-autobuild-staging
obvious-autobuild-staging Bot merged commit 528c5b0 into master Sep 1, 2026
3 checks passed
obvious-autobuild-staging Bot added a commit that referenced this pull request Sep 1, 2026
* 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>
@obvious-autobuild-staging
obvious-autobuild-staging Bot deleted the fix/analytics-followup branch September 1, 2026 18:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant