diff --git a/src/__tests__/funnel.test.ts b/src/__tests__/funnel.test.ts index f372836..54de707 100644 --- a/src/__tests__/funnel.test.ts +++ b/src/__tests__/funnel.test.ts @@ -20,7 +20,7 @@ beforeEach(() => { beaconMock.mockClear(); vi.unstubAllEnvs(); vi.stubEnv("NEXT_PUBLIC_VERCEL_ENV", "production"); - vi.stubGlobal("navigator", { sendBeacon: beaconMock }); + vi.stubGlobal("navigator", { sendBeacon: beaconMock, webdriver: false }); vi.stubGlobal("Blob", class { constructor(public parts: string[]) {} text() { return this.parts.join(""); } }); }); @@ -79,6 +79,25 @@ describe("the counts reach our own store, not only the analytics vendor", () => }); }); +describe("a browser being driven by a script is not a visitor", () => { + it("counts nothing when navigator.webdriver is set", () => { + // Our own headless checks opened the editor 12 times in one afternoon, and every one + // of them counted. The first numbers anybody read were mostly us. + vi.stubGlobal("navigator", { sendBeacon: beaconMock, webdriver: true }); + countEditorOpened("kept"); + countFirstEdit(); + countExportStarted(); + countExportFinished(); + expect(trackMock).not.toHaveBeenCalled(); + expect(beaconMock).not.toHaveBeenCalled(); + }); + + it("still counts an ordinary browser", () => { + countEditorOpened("kept"); + expect(beaconMock).toHaveBeenCalled(); + }); +}); + describe("it cannot carry a person's work, even by accident", () => { it("sends a template slug only when it looks like one", () => { countEditorOpened("Our Clinic's Private Draft, 2026"); diff --git a/src/lib/funnel.ts b/src/lib/funnel.ts index 301d8fe..c879be5 100644 --- a/src/lib/funnel.ts +++ b/src/lib/funnel.ts @@ -22,8 +22,21 @@ const MAX_EXPORT_MS = 600_000; */ let exportStartedAt: number | null = null; +/** + * A browser being driven by a script. + * + * Set by every automation framework and by Chrome's own remote debugging. Our headless + * checks opened the editor a dozen times in one afternoon and all of it counted, so the + * first real numbers were mostly us. A visitor who finishes a site is the whole point of + * these counts, and a robot never does. + */ +function driven(): boolean { + return typeof navigator !== "undefined" && navigator.webdriver === true; +} + function send(name: FunnelEvent, properties?: Record): void { if (!process.env.NEXT_PUBLIC_VERCEL_ENV) return; + if (driven()) return; try { track(name, properties); } catch {