Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
145 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.

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.1import { 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
20export const DEVELOPER_COOKIE = "g1t_dev";
21
22/** Whether the cookie's value turns developer mode on. */
23export 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. */
28export 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 */
38export 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.
45let chosen: boolean | null = null;
46const listeners = new Set<() => void>();
47
48function subscribe(listener: () => void) {
49 listeners.add(listener);
50 return () => listeners.delete(listener);
51}
52
53function 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 */
64export 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. */
73export 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`. */
80export 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. */
97export 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
115function 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 */
128export 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}