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 today | 1 | /** |
| 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. */ | |
| 16 | const 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 | */ | |
| 32 | const 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", |
| Memory is gone: people's own harnesses keep their own memory (CLAUDE.md, AGENTS.md, auto-memory), so g1t's project and workspace notes that agents recalled and remembered only duplicated them. The recall and remember operations, their /repos/{owner}/{name}/memory routes, the memory MCP tool and its lines in the MCP instructions, the Memory section of the API reference and OpenAPI document (and the CLI's table generated from it) are removed, as are the work service's memory methods, the Memory pages of a project and of a workspace with their sidebar links, Home's Decisions card (which read decisions out of workspace memory), the Memory guide (its address and the old agent-memory, agents-and-memory and context-hub ones now lead to Connect a coding agent) and every mention in the docs, README, llms.txt, the landing page, the privacy policy and the Ask bench. Decision records, g1t why and g1t work stay as they were. memory:read and memory:write are retired as agents:run and the models scopes were: tokens and OAuth grants that hold them still parse and keep working for their other scopes, but no preset (the Agent preset and the OAuth default included), token form, mobile sign-in or hosted session hands them out and nothing needs them; looking up a ticket (search ticket, get_context), which needed memory:read, needs issues:read. The memories table stays in work's D1 untouched and is no longer read or written, except that purging a repository or deleting a workspace still deletes its old rows. Checked: cargo test for g1t-contracts (308), g1t-api (201), g1t (102 and 41), g1t-work (103) and g1t-integrations (65), cargo check of the workspace, the OpenAPI document and ops table regenerated, every workspace's typecheck, the site's 901 tests and a production build, the contracts' 48 tests, the phone app's 11, the deploy tool's 92, and the docs' tests and build. | 37 | "jobs", "keys", "labels", "login", "members", "merge-queue", "milestones", "new", "notifications", "overview", |
| 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 today | 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 branch 'worktree-agent-aa68cf6ecc03dffaf' | 41 | "verify", "webhooks", "work", "workspace", "workspaces", |
| 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 today | 42 | ]); |
| 43 | ||
| 44 | export 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. */ | |
| 50 | export 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. */ | |
| 62 | function 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. */ | |
| 70 | export 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. */ | |
| 82 | const CHAIN_DETAIL = /(?:text|href|attr__[\w-]+)="(?:[^"\\]|\\.)*"/g; | |
| 83 | ||
| 84 | function 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. */ | |
| 107 | const NEVER = new Set(["$snapshot", "$snapshot_items", "$exception"]); | |
| 108 | ||
| 109 | export 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 | */ | |
| 115 | export 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.