Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
cc038b9
fix(analytics): forward $raw_user_agent and $host for cookieless inge…
biggest-littlest Aug 15, 2026
985bd2a
chore(git): reconcile main before cookieless promotion
scttbnsn Aug 15, 2026
3b6d9cd
chore(config): remove the stale Cursor rules (#54)
scttbnsn Aug 16, 2026
30cdd8e
docs(readme): describe what this repo is and how it deploys (#55)
scttbnsn Aug 16, 2026
89bad5b
chore(git): reconcile main before docs promotion
scttbnsn Aug 16, 2026
d8aec70
chore(gitignore): ignore the root .vercel link and history-backup bun…
scttbnsn Aug 26, 2026
f50fadb
feat(analytics): capture $pageleave and send $pathname (#60)
scttbnsn Aug 26, 2026
1a40882
fix(seo): repair JSON-LD logo 404 and double-slash base URLs
scttbnsn Aug 28, 2026
e1f7a85
build(deps): bump next to ^16.2.11 to clear all 35 Dependabot alerts
scttbnsn Aug 28, 2026
ac19fbb
build(deps): regenerate next-env.d.ts for next 16.3
scttbnsn Aug 28, 2026
faea446
docs(roadmap): track web-analytics table coverage follow-ups (ops X37)
scttbnsn Aug 28, 2026
25e49cd
chore: ignore .claude/ with a tracked line (#59)
scttbnsn Aug 28, 2026
9025b2f
docs(roadmap): point acquisition-data item at the ops analytics standard
scttbnsn Aug 28, 2026
f4add91
chore(git): reconcile main before analytics and hardening promotion
scttbnsn Aug 28, 2026
5aade3b
docs(roadmap): pageleave ratio is structural; note the bot-detection …
scttbnsn Aug 28, 2026
b34f329
chore(config): add .planning/ to .gitignore (#62)
scttbnsn Sep 7, 2026
b620263
chore(git): reconcile main before the .planning gitignore promotion
scttbnsn Sep 7, 2026
2d3ccab
fix(analytics): preserve validated referring hostnames (#64)
scttbnsn Sep 13, 2026
16c153c
chore(git): reconcile main before the referrer reporting promotion
scttbnsn Sep 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion frontend/instrumentation-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
15 changes: 15 additions & 0 deletions frontend/lib/posthog-privacy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,19 @@ function getRawPath(properties: Record<string, unknown>): 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<string, unknown>) {
const token = properties.token;
// PostHog's cookieless server-hash ingestion step computes the anonymous
Expand Down Expand Up @@ -113,6 +126,8 @@ function createCommonProperties(properties: Record<string, unknown>) {
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;
}

Expand Down
1 change: 1 addition & 0 deletions frontend/test/posthog-source.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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, "\\$&")));
Expand Down
48 changes: 48 additions & 0 deletions frontend/test/posthog.test.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
Loading