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.
| Developer mode: a page that fails can say why. Under Settings, Account, a Developer mode switch (kept per browser in a g1t_dev cookie, like Appearance) makes the error page show the error itself, its message and stack, the address, the time, whether it came from a page the server drew or a navigation in the browser, the routes it was drawn from, the site's build (its stylesheet's hashed name), the browser and the desktop app's version, with Copy details to paste it all into a report; with it off, the page says where to turn it on. A signed-in request with developer mode on is answered by React Router in its development mode (workers/app.ts, a second handler made the first time it is wanted), so an error on the server comes through in full instead of "Unexpected Server Error"; that mode differs from production only in that and in hooks only Vite's dev server sets, and only ever covers the person's own requests. Checked in a production build: a failing repository page shows "counts failed with status 503" and its stack with developer mode on, and the usual page with it off. The workspaces guide covers it under Developer mode. | 1 | import { useSyncExternalStore } from "react"; |
| 2 | ||
| 3 | /** | |
| 4 | * Developer mode: what a person turns on (Settings, Account) so that when | |
| 5 | * something breaks they can say what. It is kept per browser in a cookie | |
| 6 | * (`g1t_dev`), as Appearance is, so the server knows it from the first | |
| 7 | * byte. While it is on: | |
| 8 | * | |
| 9 | * - a page that fails shows the error itself, its stack, where it happened | |
| 10 | * and the build, with a button that copies it all to paste into a report | |
| 11 | * (`debugReport`); | |
| 12 | * - a signed-in request is answered by React Router in its development | |
| 13 | * mode (workers/app.ts), which sends the server's own error instead of | |
| 14 | * "Unexpected Server Error". That mode differs from production only in | |
| 15 | * that, and in hooks only the Vite dev server sets. It is for the person's | |
| 16 | * own requests alone, and g1t's code is open, so the error tells them | |
| 17 | * nothing of anyone else's. | |
| 18 | */ | |
| 19 | ||
| 20 | export const DEVELOPER_COOKIE = "g1t_dev"; | |
| 21 | ||
| 22 | /** Whether the cookie's value turns developer mode on. */ | |
| 23 | export function readDeveloperMode(value: string | null | undefined): boolean { | |
| 24 | return value === "1"; | |
| 25 | } | |
| 26 | ||
| 27 | /** The Set-Cookie value that turns it on for a year, or off. */ | |
| 28 | export function developerCookie(on: boolean, secure: boolean): string { | |
| 29 | const lasts = on ? "Max-Age=31536000" : "Max-Age=0"; | |
| 30 | return `${DEVELOPER_COOKIE}=${on ? "1" : ""}; Path=/; ${lasts}; SameSite=Lax${secure ? "; Secure" : ""}`; | |
| 31 | } | |
| 32 | ||
| 33 | /** | |
| 34 | * Whether a request is answered with the server's own errors: developer | |
| 35 | * mode on, and signed in (a session cookie, or a token, which the site | |
| 36 | * checks before anything is read). | |
| 37 | */ | |
| 38 | export function wantsServerErrors(request: Request): boolean { | |
| 39 | const cookies = request.headers.get("cookie") ?? ""; | |
| 40 | const on = new RegExp(`(?:^|;\\s*)${DEVELOPER_COOKIE}=1(?:;|$)`).test(cookies); | |
| 41 | return on && (/(?:^|;\s*)g1t_session=/.test(cookies) || request.headers.has("authorization")); | |
| 42 | } | |
| 43 | ||
| 44 | // Switched in this tab since the page loaded; null until it is. | |
| 45 | let chosen: boolean | null = null; | |
| 46 | const listeners = new Set<() => void>(); | |
| 47 | ||
| 48 | function subscribe(listener: () => void) { | |
| 49 | listeners.add(listener); | |
| 50 | return () => listeners.delete(listener); | |
| 51 | } | |
| 52 | ||
| 53 | function fromCookie(): boolean { | |
| 54 | const match = document.cookie.match(new RegExp(`(?:^|;\\s*)${DEVELOPER_COOKIE}=([^;]*)`)); | |
| 55 | return readDeveloperMode(match?.[1]); | |
| 56 | } | |
| 57 | ||
| 58 | /** | |
| 59 | * Whether developer mode is on: as switched in this tab, else as this | |
| 60 | * browser's cookie says. `drawn` is what the server drew the page with (the | |
| 61 | * root loader's), used while the page hydrates; an error page whose root | |
| 62 | * data is gone draws without it, then shows the details once in the browser. | |
| 63 | */ | |
| 64 | export function useDeveloperMode(drawn?: boolean): boolean { | |
| 65 | return useSyncExternalStore( | |
| 66 | subscribe, | |
| 67 | () => chosen ?? fromCookie(), | |
| 68 | () => drawn ?? false, | |
| 69 | ); | |
| 70 | } | |
| 71 | ||
| 72 | /** Turn developer mode on or off in this browser, now and for the pages the server draws next. */ | |
| 73 | export function setDeveloperMode(on: boolean) { | |
| 74 | chosen = on; | |
| 75 | if (typeof document !== "undefined") document.cookie = developerCookie(on, window.location.protocol === "https:"); | |
| 76 | for (const listener of listeners) listener(); | |
| 77 | } | |
| 78 | ||
| 79 | /** What a failed page knows about itself, for `debugReport`. */ | |
| 80 | export type DebugFacts = { | |
| 81 | url: string; | |
| 82 | /** When it was seen in the browser; null while the server draws the page. */ | |
| 83 | at: Date | null; | |
| 84 | error: unknown; | |
| 85 | /** The routes the page was drawn from, outermost first. */ | |
| 86 | routes: string[]; | |
| 87 | /** Which build of the site: its stylesheet's hashed name. */ | |
| 88 | build: string; | |
| 89 | userAgent: string; | |
| 90 | /** Inside the desktop app: its version. */ | |
| 91 | desktop?: string | null; | |
| 92 | /** Whether the error came from a document the server drew, or a navigation in the browser. */ | |
| 93 | navigation: "document" | "client"; | |
| 94 | }; | |
| 95 | ||
| 96 | /** The error's parts, whatever was thrown. */ | |
| 97 | export function describeError(error: unknown): { kind: string; status?: number; message: string; stack?: string; data?: string } { | |
| 98 | if (error && typeof error === "object" && "status" in error && "data" in error && typeof (error as { status: unknown }).status === "number") { | |
| 99 | const response = error as { status: number; statusText?: string; data: unknown; error?: unknown }; | |
| 100 | const inner = response.error instanceof Error ? response.error : null; | |
| 101 | return { | |
| 102 | kind: "Response", | |
| 103 | status: response.status, | |
| 104 | message: [response.status, response.statusText].filter(Boolean).join(" "), | |
| 105 | data: typeof response.data === "string" ? response.data : safeJson(response.data), | |
| 106 | stack: inner?.stack, | |
| 107 | }; | |
| 108 | } | |
| 109 | if (error instanceof Error) { | |
| 110 | return { kind: error.name || "Error", message: error.message, stack: error.stack }; | |
| 111 | } | |
| 112 | return { kind: typeof error, message: typeof error === "string" ? error : safeJson(error) }; | |
| 113 | } | |
| 114 | ||
| 115 | function safeJson(value: unknown): string { | |
| 116 | try { | |
| 117 | return JSON.stringify(value, null, 2) ?? String(value); | |
| 118 | } catch { | |
| 119 | return String(value); | |
| 120 | } | |
| 121 | } | |
| 122 | ||
| 123 | /** | |
| 124 | * Everything about a failed page, as text to paste into a report: what | |
| 125 | * failed, where, when and on which build. Never anything of the person's | |
| 126 | * beyond the address they were at. | |
| 127 | */ | |
| 128 | export function debugReport(facts: DebugFacts): string { | |
| 129 | const error = describeError(facts.error); | |
| 130 | const lines = [ | |
| 131 | `g1t error report`, | |
| 132 | ``, | |
| 133 | `URL: ${facts.url}`, | |
| 134 | `Time: ${facts.at?.toISOString() ?? "(not yet in the browser)"}`, | |
| 135 | `Loaded: ${facts.navigation === "document" ? "a page drawn by the server" : "a navigation in the browser"}`, | |
| 136 | `Build: ${facts.build}`, | |
| 137 | `Routes: ${facts.routes.join(" > ") || "(none)"}`, | |
| 138 | `Browser: ${facts.userAgent}`, | |
| 139 | ]; | |
| 140 | if (facts.desktop) lines.push(`Desktop app: ${facts.desktop}`); | |
| 141 | lines.push(``, `${error.kind}${error.status ? ` ${error.status}` : ""}: ${error.message}`); | |
| 142 | if (error.data) lines.push(``, `Data:`, error.data); | |
| 143 | if (error.stack) lines.push(``, `Stack:`, error.stack); | |
| 144 | return lines.join("\n"); | |
| 145 | } |