/** * Route templates for metrics: a path reduced to its shape (`/:owner/:repo/issues/:n`), never carrying account, * repository, branch or file names. Only words from a fixed list of route segments survive; anything else * becomes a parameter, so a template is safe to send to analytics whatever the path held. */ /** First path segments that are app pages, not account names. */ export const APP_ROOTS = new Set([ "", "abuse", "admin", "explore", "ingest", "features", "forgot-password", "login", "new", "notifications", "orgs", "pricing", "projects", "reset-password", "search", "self-host", "settings", "signup", "verify-email", "api", "auth", ]); const STATIC_SEGMENTS = new Set( `abuse accept acceptable-use access actions admin advisories agents ai ai-fix alerts api apply approve artifacts attempts audit auto_merge billing blame blob branch-protections branches bypass caches cancel check code-owner-review code-scanning codeowners collaborators comments commit commits compare conflicts containers contents cookies credential decline deliveries dependencies deploy-tokens deployments discussions dismissals dispatches dmca domain drafts edit editor-sessions environments explore features files find forgot-password forks hooks import insights integrations invitations issues labels lfs lfs-quota lfs-usage linked-issues login mcp-usage members merge merge_queue milestones new notification-settings notifications org organization_invitations orgs permissions pings planning policy privacy projects protections pull pull-template pulls push-blocks push-protection registration-token releases replies repos requested_reviewers requested-reviewers rerequest rerun reset-password review reviews rollback runners runs scratch search secret-scanning secrets security security-advisories self-host settings signatures signing-keys signup site site-usage sites state status statuses suggestions support tags terms threads tokens transfer tree two-factor unblock update-branch usage user v1 variables verify-email webhooks workflows info refs git-upload-pack git-receive-pack objects batch locks raw archive download attachments og camo avatars mcp ingest jobs runner sse ws listen oauth authorize token register revoke callback github _serverFn graph day years diff patch` .split(/\s+/) .filter(Boolean), ); function segmentTemplate(segment: string): string { if (segment === "") return ""; if (STATIC_SEGMENTS.has(segment)) return segment; if (/^\d+$/.test(segment)) return ":n"; if (/\.(diff|patch)$/.test(segment)) return `:range.${segment.split(".").pop()}`; return ":param"; } export function routeTemplate(pathname: string): string { const parts = pathname.split("/").slice(1); if (parts.length === 0) return "/"; const [first = "", second, ...rest] = parts; // Server function ids are code identifiers (vite.config.ts serverFnId), never user data. if (first === "_serverFn") return `/_serverFn/${second && /^[A-Za-z0-9._]{1,120}$/.test(second) ? second : ":fn"}`; if (APP_ROOTS.has(first.toLowerCase())) { const tail = second === undefined ? [] : [second, ...rest]; return `/${[first, ...tail.map(segmentTemplate)].join("/")}`; } if (second === undefined) return "/:owner"; const repo = second.endsWith(".git") ? ":repo.git" : ":repo"; return `/${[":owner", repo, ...rest.map(segmentTemplate)].join("/")}`; } const PLACEHOLDER = /^:(owner|repo|repo\.git|n|param|fn|range\.(diff|patch))$/; const SERVER_FN = /^[A-Za-z0-9._]{1,120}$/; /** Whether `value` is already a template this module produced (only route words and placeholders). */ export function isRouteTemplate(value: string): boolean { if (value === "/") return true; if (!value.startsWith("/")) return false; const parts = value.slice(1).split("/"); if (parts[0] === "_serverFn") return parts.length === 2 && (parts[1] === ":fn" || SERVER_FN.test(parts[1] ?? "")); return parts.every( (part, index) => PLACEHOLDER.test(part) || STATIC_SEGMENTS.has(part) || (index === 0 && APP_ROOTS.has(part)), ); }