From 17b935d04371850cb8eb20eb2b5aa2894f5d3eaf Mon Sep 17 00:00:00 2001 From: scttbnsn <80784472+scttbnsn@users.noreply.github.com> Date: Sun, 13 Sep 2026 10:54:04 -0400 Subject: [PATCH] fix(analytics): preserve validated referring hostnames --- frontend/instrumentation-client.ts | 2 +- frontend/lib/posthog-privacy.ts | 15 +++++++++ frontend/test/posthog-source.test.mjs | 1 + frontend/test/posthog.test.ts | 48 +++++++++++++++++++++++++++ 4 files changed, 65 insertions(+), 1 deletion(-) diff --git a/frontend/instrumentation-client.ts b/frontend/instrumentation-client.ts index ee29658..6a8604f 100644 --- a/frontend/instrumentation-client.ts +++ b/frontend/instrumentation-client.ts @@ -41,7 +41,7 @@ if (typeof window !== "undefined" && posthogConfig) { persistence: "memory", disable_persistence: true, respect_dnt: true, - save_referrer: false, + save_referrer: true, save_campaign_params: false, disable_capture_url_hashes: true, disable_scroll_properties: true, diff --git a/frontend/lib/posthog-privacy.ts b/frontend/lib/posthog-privacy.ts index c241757..c2f8040 100644 --- a/frontend/lib/posthog-privacy.ts +++ b/frontend/lib/posthog-privacy.ts @@ -68,6 +68,19 @@ function getRawPath(properties: Record): unknown { } } +function sanitizeReferringDomain(value: unknown): string | undefined { + if (typeof value !== "string") return undefined; + if (value === "$direct") return value; + if ( + value.length > 253 || + value.trim() !== value || + !/^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z](?:[a-z0-9-]{0,61}[a-z0-9])?$/iu.test(value) + ) { + return undefined; + } + return value.toLowerCase(); +} + function createCommonProperties(properties: Record) { const token = properties.token; // PostHog's cookieless server-hash ingestion step computes the anonymous @@ -113,6 +126,8 @@ function createCommonProperties(properties: Record) { if (properties.distinct_id === "$posthog_cookieless") { common.distinct_id = "$posthog_cookieless"; } + const referringDomain = sanitizeReferringDomain(properties.$referring_domain); + if (referringDomain !== undefined) common.$referring_domain = referringDomain; return common; } diff --git a/frontend/test/posthog-source.test.mjs b/frontend/test/posthog-source.test.mjs index ea1f6ce..cdcbcbe 100644 --- a/frontend/test/posthog-source.test.mjs +++ b/frontend/test/posthog-source.test.mjs @@ -34,6 +34,7 @@ test("privacy posture disables persistence, recording, autocapture, and automati 'cookieless_mode: "always"', "advanced_disable_flags: true", "disable_persistence: true", + "save_referrer: true", "capture_performance:", ]) { assert.match(instrumentation, new RegExp(option.replace(/[.*+?^${}()|[\\]\\]/g, "\\$&"))); diff --git a/frontend/test/posthog.test.ts b/frontend/test/posthog.test.ts index de2c021..4c4d7a8 100644 --- a/frontend/test/posthog.test.ts +++ b/frontend/test/posthog.test.ts @@ -1,4 +1,52 @@ import assert from "node:assert/strict"; + +test("referring hostnames survive the envelope without URLs or campaign data", () => { + const accepted = ["github.com", "news.ycombinator.com", "$direct", "SEARCH.Example"]; + const rejected = [ + undefined, + null, + 42, + "", + "https://search.example/private?email=me@example.com", + "//search.example", + "search.example/path", + "search.example?x=1", + "me@example.com", + "search.example:443", + "search.example\n", + "-bad.example", + "bad-.example", + `${"a".repeat(64)}.example`, + ]; + for (const event of ["$pageview", "$pageleave", "$web_vitals"]) { + for (const value of [...accepted, ...rejected]) { + const result = sanitizeEvent({ + event, + properties: { + token: "phc_referrer_fixture", + $cookieless_mode: true, + $process_person_profile: false, + $raw_user_agent: "Mozilla/5.0 (Referrer Fixture)", + $host: "codeswhat.com", + path: "/", + $web_vitals_LCP_value: 100, + $referring_domain: value, + $referrer: "https://search.example/private?email=me@example.com", + utm_source: "me@example.com", + }, + }); + assert.ok(result); + assert.equal( + result.properties.$referring_domain, + accepted.includes(value as string) ? (value as string).toLowerCase() : undefined, + `${event}: ${JSON.stringify(value)}`, + ); + assert.equal("$referrer" in result.properties, false); + assert.equal("utm_source" in result.properties, false); + } + } +}); + import { createRequire } from "node:module"; import { test } from "node:test"; import {