authorgravatar for 71498452+StiensWout@users.noreply.github.comWout Stiens <71498452+StiensWout@users.noreply.github.com> 2026-09-02 07:12:36+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-02 01:12:36-04:00
log133db22fae34cfb2e62a806e99d22674b0376f35
tree7b813c3e9dc378e1385623a0baa0b38a4479f8c6
parentb520120cf169ce63a5606447a292451e97622c9a
signature Signed by PGP key B5690EEEBB952194

feat(web): copy the full error report from the error page (#9166)

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

1 files changed, 48 insertions(+), 11 deletions(-)

apps/web/src/routes/__root.tsx+48-11
......@@ -8,9 +8,10 @@ import {
88 useLocation,
99 useNavigate,
1010} from "@tanstack/react-router";
11import { useEffect, useEffectEvent, useRef, useState } from "react";
11import { CheckIcon, CopyIcon } from "lucide-react";
12import { useEffect, useEffectEvent, useMemo, useRef, useState } from "react";
1213
13import { APP_BASE_NAME, APP_DISPLAY_NAME, APP_STAGE_LABEL } from "../branding";
14import { APP_BASE_NAME, APP_DISPLAY_NAME, APP_STAGE_LABEL, APP_VERSION } from "../branding";
1415import { resolveServerBackedAppDisplayName } from "../branding.logic";
1516import { AppSidebarLayout } from "../components/AppSidebarLayout";
1617import { CommandPalette } from "../components/CommandPalette";
......@@ -22,6 +23,7 @@ import { DesktopAppActivationCoordinator } from "../components/desktop/DesktopAp
2223import { ProviderUpdateLaunchNotification } from "../components/ProviderUpdateLaunchNotification";
2324import { SlowRpcRequestToastCoordinator } from "../components/SlowRpcRequestToastCoordinator";
2425import { ThemeEditorHost } from "../components/settings/ThemeEditorHost";
26import { useCopyToClipboard } from "../hooks/useCopyToClipboard";
2527import { useDefaultThemeAdoption } from "../hooks/useDefaultTheme";
2628import { useEnvironmentThemeSync } from "../hooks/useEnvironmentTheme";
2729import { Button } from "../components/ui/button";
......@@ -267,7 +269,9 @@ function HostedStaticEnvironmentBootstrap() {
267269
268270function RootRouteErrorView({ error, reset }: ErrorComponentProps) {
269271 const message = errorMessage(error);
270 const details = errorDetails(error);
272 // Router pathname rather than window.location: desktop uses hash history, where the window path is always "/".
273 const pathname = useLocation({ select: (location) => location.pathname });
274 const report = useMemo(() => errorReport(error, pathname), [error, pathname]);
271275
272276 return (
273277 <div className="relative flex min-h-screen items-center justify-center overflow-hidden bg-background px-4 py-10 text-foreground sm:px-6">
......@@ -292,22 +296,32 @@ function RootRouteErrorView({ error, reset }: ErrorComponentProps) {
292296 <Button size="sm" variant="outline" onClick={() => window.location.reload()}>
293297 Reload app
294298 </Button>
299 <CopyErrorButton report={report} />
295300 </div>
296301
297 <details className="group mt-5 overflow-hidden rounded-lg border border-border/70 bg-background/55">
298 <summary className="cursor-pointer list-none px-3 py-2 text-xs font-medium text-muted-foreground">
299 <span className="group-open:hidden">Show error details</span>
300 <span className="hidden group-open:inline">Hide error details</span>
301 </summary>
302 <pre className="max-h-56 overflow-auto border-t border-border/70 bg-background/80 px-3 py-2 text-xs text-foreground/85">
303 {details}
302 <div className="mt-5 overflow-hidden rounded-lg border border-border/70 bg-background/55">
303 <p className="px-3 py-1.5 text-xs font-medium text-muted-foreground">Error report</p>
304 <pre className="max-h-64 overflow-auto border-t border-border/70 bg-background/80 px-3 py-2 text-xs whitespace-pre-wrap text-foreground/85">
305 {report}
304306 </pre>
305 </details>
307 </div>
306308 </section>
307309 </div>
308310 );
309311}
310312
313/** Copies the full error report and swaps to a check mark for a moment as confirmation. */
314function CopyErrorButton({ report }: { report: string }) {
315 const { copyToClipboard, isCopied } = useCopyToClipboard({ target: "error-report" });
316
317 return (
318 <Button size="sm" variant="outline" onClick={() => copyToClipboard(report)}>
319 {isCopied ? <CheckIcon className="text-success" /> : <CopyIcon />}
320 {isCopied ? "Copied" : "Copy error"}
321 </Button>
322 );
323}
324
311325function errorMessage(error: unknown): string {
312326 if (error instanceof Error && error.message.trim().length > 0) {
313327 return error.message;
......@@ -336,6 +350,29 @@ function errorDetails(error: unknown): string {
336350 }
337351}
338352
353const MAX_ERROR_CAUSE_DEPTH = 5;
354
355/**
356 * Full error text for bug reports: app build, page path, time, then the stack
357 * and any cause chain. Takes the pathname only so tokens in the query never
358 * land on the clipboard.
359 */
360function errorReport(error: unknown, pathname: string): string {
361 const lines = [
362 `${APP_DISPLAY_NAME} ${APP_VERSION}`,
363 `Path: ${pathname}`,
364 `Time: ${new Date().toISOString()}`,
365 "",
366 errorDetails(error),
367 ];
368 let cause = error instanceof Error ? error.cause : undefined;
369 for (let depth = 0; cause !== undefined && depth < MAX_ERROR_CAUSE_DEPTH; depth += 1) {
370 lines.push("", "Caused by:", errorDetails(cause));
371 cause = cause instanceof Error ? cause.cause : undefined;
372 }
373 return lines.join("\n");
374}
375
339376function AuthenticatedTracingBootstrap() {
340377 useEffect(() => {
341378 void configureClientTracing();