Skip to content
122 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Merge site-audit: SEO routes, analytics with consent first in Europe, security page, founder and X account, docs header; the tour opens on the pull request and leads with Code, on the real shell, with Agents and Docs working today1/**
2 * What product analytics may learn from a page, decided before an event
3 * leaves the browser (lib/analytics.client.ts).
4 *
5 * The public front door (the home page, pricing, Explore, signing up and
6 * in, support, security and the policies) is sent as it is. Everywhere else, names are replaced
7 * with placeholders: `/acme/web/pull/12` is sent as
8 * `/:name/:name/pull/:n`, the page title as "g1t", and a clicked element
9 * without its text, link or attributes. Query strings keep only `utm_*`.
10 * Session recordings and error reports are never sent, and click heatmaps
11 * only from the front door. No Workers or browser imports, so it is tested
12 * under Node.
13 */
14
15/** Paths sent as they are. */
16const PUBLIC = new Set([
17 "/",
18 "/pricing",
19 "/explore",
20 "/register",
21 "/login",
22 "/security",
23 "/support",
24 "/status",
25 "/policies",
26]);
27
28/**
29 * Words of g1t's own addresses, kept in a scrubbed path so it still says
30 * which kind of page it was. Anything else is a name, and is replaced.
31 */
32const ROUTE_WORDS = new Set([
33 "-", "about.json", "account", "actions", "activity", "agents", "applications", "archive", "audit", "billing",
34 "blob", "branches", "browse", "bypass-requests", "chat", "checks", "code", "code-access", "commit", "commits",
35 "compare", "confirm-email", "context", "deployments", "dm", "docs", "emails", "emoji", "entries", "explore", "files", "forgot", "gateway",
g1t is described as the product it is in alpha, and its shell takes the new shape: the README, the docs home and docs/PLAN.md label every feature Live, Preview or Coming; a floating dock with the g1t mark, Today, Chat, Notifications, Agents, Code, Artifacts, your pinned apps and the Apps launcher, with People, Workspace and the account menu (now holding help) at its foot; the workspace's logo and switcher heading a flat sidebar that folds away with Ctrl B, or leading the header's breadcrumbs where there is none; the page in a rounded panel with a full-width header; Today as the front page, with first-time acceptance of agents' pull requests, what needs you and what's at stake, spend today and one item to start with, every number from a service; Notifications in place of the Inbox at /notifications; a bottom bar and More sheet on phones; and sign-in, sign-up, two-factor, reset, invites and choosing a workspace on standalone pages with no app around them.36 "github", "guardrails", "home", "inbox", "apps", "insights", "integrations", "invitations", "invite", "invites", "issues",
Merge site-audit: SEO routes, analytics with consent first in Europe, security page, founder and X account, docs header; the tour opens on the pull request and leads with Code, on the real shell, with Agents and Docs working today37 "jobs", "keys", "labels", "login", "members", "memory", "merge-queue", "milestones", "new", "notifications", "overview",
38 "packages", "patterns", "people", "personal-access-tokens", "pins", "policies", "profile", "projects", "pull",
39 "pulls", "queue", "releases", "repositories", "reset", "rules", "runners", "runs", "search", "secrets", "security",
g1t is described as the product it is in alpha, and its shell takes the new shape: the README, the docs home and docs/PLAN.md label every feature Live, Preview or Coming; a floating dock with the g1t mark, Today, Chat, Notifications, Agents, Code, Artifacts, your pinned apps and the Apps launcher, with People, Workspace and the account menu (now holding help) at its foot; the workspace's logo and switcher heading a flat sidebar that folds away with Ctrl B, or leading the header's breadcrumbs where there is none; the page in a rounded panel with a full-width header; Today as the front page, with first-time acceptance of agents' pull requests, what needs you and what's at stake, spend today and one item to start with, every number from a service; Notifications in place of the Inbox at /notifications; a bottom bar and More sheet on phones; and sign-in, sign-up, two-factor, reset, invites and choosing a workspace on standalone pages with no app around them.40 "security-log", "settings", "soon", "spend", "tags", "teams", "today", "tokens", "tree", "two-factor", "u", "usage",
Merge site-audit: SEO routes, analytics with consent first in Europe, security page, founder and X account, docs header; the tour opens on the pull request and leads with Code, on the real shell, with Agents and Docs working today41 "verify", "webhooks", "workspace", "workspaces",
42]);
43
44export function isPublicPath(pathname: string): boolean {
45 const path = pathname.replace(/\/+$/, "") || "/";
46 return PUBLIC.has(path) || path.startsWith("/policies/");
47}
48
49/** A path as analytics may see it. */
50export function scrubPath(pathname: string): string {
51 if (isPublicPath(pathname)) return pathname;
52 return pathname
53 .split("/")
54 .map((segment) => {
55 if (segment === "" || ROUTE_WORDS.has(segment)) return segment;
56 return /^\d+$/.test(segment) ? ":n" : ":name";
57 })
58 .join("/");
59}
60
61/** Only campaign parameters survive; everything else in a query can name something. */
62function scrubSearch(search: URLSearchParams): string {
63 const kept = new URLSearchParams();
64 for (const [key, value] of search) if (key.startsWith("utm_")) kept.append(key, value);
65 const query = kept.toString();
66 return query ? `?${query}` : "";
67}
68
69/** A URL on this site as analytics may see it; another site's is left alone. */
70export function scrubUrl(value: string, origin: string): string {
71 let url: URL;
72 try {
73 url = new URL(value);
74 } catch {
75 return value;
76 }
77 if (url.origin !== origin) return value;
78 return `${url.origin}${scrubPath(url.pathname)}${scrubSearch(url.searchParams)}`;
79}
80
81/** Element text, links and attributes in autocapture's chain string. */
82const CHAIN_DETAIL = /(?:text|href|attr__[\w-]+)="(?:[^"\\]|\\.)*"/g;
83
84function scrubValue(key: string, value: unknown, origin: string, publicPage: boolean): unknown {
85 if (typeof value === "string") {
86 if (!publicPage && (key === "$title" || key === "title")) return "g1t";
87 if (!publicPage && key === "$el_text") return undefined;
88 if (!publicPage && key === "$elements_chain") return value.replace(CHAIN_DETAIL, "");
89 if (/pathname$/i.test(key) && value.startsWith("/")) return scrubPath(value);
90 return value.startsWith(origin) ? scrubUrl(value, origin) : value;
91 }
92 if (Array.isArray(value)) return value.map((item) => scrubValue(key, item, origin, publicPage));
93 if (value && typeof value === "object") {
94 const out: Record<string, unknown> = {};
95 for (const [inner, innerValue] of Object.entries(value)) {
96 if (!publicPage && (inner === "$el_text" || inner === "href" || inner.startsWith("attr__"))) continue;
97 const scrubbed = scrubValue(inner, innerValue, origin, publicPage);
98 // Some objects are keyed by address, such as a heatmap's pages.
99 if (scrubbed !== undefined) out[inner.startsWith(origin) ? scrubUrl(inner, origin) : inner] = scrubbed;
100 }
101 return out;
102 }
103 return value;
104}
105
106/** Events that are never sent: session recordings and error reports, which carry page content. */
107const NEVER = new Set(["$snapshot", "$snapshot_items", "$exception"]);
108
109export type CapturedEvent = { event: string; properties: Record<string, unknown>; [key: string]: unknown };
110
111/**
112 * The event as analytics may see it, or null to drop it. `pathname` is the
113 * page the event happened on; `origin` is this site's.
114 */
115export function scrubEvent<T extends CapturedEvent>(event: T | null, pathname: string, origin: string): T | null {
116 if (!event || NEVER.has(event.event)) return null;
117 const publicPage = isPublicPath(pathname);
118 // A heatmap is keyed by the page's address, and holds where on it people clicked.
119 if (event.event === "$$heatmap" && !publicPage) return null;
120 const properties = scrubValue("", event.properties ?? {}, origin, publicPage) as Record<string, unknown>;
121 return { ...event, properties };
122}

This file's history is long; its oldest lines are credited to the oldest commit read.