From a71d6ab470784af661f4df003198a572edb3a334 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Fri, 18 Sep 2026 19:24:59 -0700 Subject: [PATCH 1/3] feat(website): report visible engaged time so readers stop counting as bounces posthog-js fires $pageleave only from its pagehide/unload handler, never on visibilitychange, and PostHog derives session_duration from the span of a session's events. A visitor who lands, reads, clicks nothing and leaves the tab open therefore emits one $pageview, records zero seconds, and is scored as a bounce. Measured over 30 days: 13.6% of desktop and 17.8% of mobile homepage sessions. The tab-close lands in a rotated session - 67 orphan pageleave-only sessions, median gap 221 minutes - which is what rules out an ad blocker. The gap concentrated where nothing passive could fire. marketing:stage_progress is gated to 1024x720 and fired on 0.0% of mobile sessions; 91% of mobile sessions emitted nothing but the pageview, so mobile engagement was not measurable at all. marketing:engaged_time has no viewport gate. It counts visible time only, pausing while the tab is hidden, so it corrects the recorded duration rather than inflating it. That distinction is the whole design: the hidden-time tests are load-bearing, not incidental. 10s matches PostHog's own bounce cutoff so a genuine ten-second read stops being recorded as zero. All 14 assertions mutation-verified: hidden time counted, threshold dedupe removed, visible-run restarted, cleanup dropped, listener unregistered, and interval never installed each turn exactly the expected tests red. Co-Authored-By: Claude Opus 5 --- apps/website/src/app/layout.tsx | 2 + .../shared/EngagedTimeSignal.spec.tsx | 88 ++++++++++++ .../components/shared/EngagedTimeSignal.tsx | 57 ++++++++ apps/website/src/lib/analytics/client.ts | 8 ++ .../src/lib/analytics/engaged-time.spec.ts | 125 ++++++++++++++++++ .../website/src/lib/analytics/engaged-time.ts | 93 +++++++++++++ apps/website/src/lib/analytics/events.ts | 7 + docs/gtm/taxonomy.md | 1 + 8 files changed, 381 insertions(+) create mode 100644 apps/website/src/components/shared/EngagedTimeSignal.spec.tsx create mode 100644 apps/website/src/components/shared/EngagedTimeSignal.tsx create mode 100644 apps/website/src/lib/analytics/engaged-time.spec.ts create mode 100644 apps/website/src/lib/analytics/engaged-time.ts diff --git a/apps/website/src/app/layout.tsx b/apps/website/src/app/layout.tsx index ff38d2d5a..9e7bb6b3d 100644 --- a/apps/website/src/app/layout.tsx +++ b/apps/website/src/app/layout.tsx @@ -20,6 +20,7 @@ import { } from '../lib/site-metadata'; import { getFormPolicy } from '../lib/growth/form-policy'; import { WebsiteSignals } from '../components/shared/WebsiteSignals'; +import { EngagedTimeSignal } from '../components/shared/EngagedTimeSignal'; import { websiteContentCatalog } from '../lib/growth/website-content'; const display = Archivo_Black({ @@ -97,6 +98,7 @@ export default function RootLayout({ */} +