mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-12 18:06:11 +02:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - Its browser error boundaries recover from failed renders and report the exception when error monitoring is enabled. > - The boundaries already receive the React component stack, but discard it before reporting the error. > - A minified DOM insertion error can therefore lack enough context to identify the affected component. > - Browser translation can replace text nodes that React still uses as insertion anchors. > - This pull request preserves bounded component names and browser state for the next failure. > - Maintainers can locate the failed component without collecting page content or customer URLs. ## Linked Issues or Issue Description Related: #13719 attributes browser errors to the loaded release. #13784 supplies the browser environment. This change adds context to error-boundary reports. **What happened?** A browser error boundary reports a DOM `NotFoundError` with only a minified JavaScript stack. The React component trace is logged to the console, which production builds remove. Browser translation is a plausible cause, but the report cannot identify the component or confirm the translation marker. **Expected behavior** An error-boundary report includes a bounded component trace and limited browser state. It excludes component props, text, HTML, element IDs, arbitrary CSS classes, page URLs, and query strings. **Steps to reproduce** 1. Render a component with conditional content before a text node. 2. Replace that text node with a translation element outside React. 3. Enable the preceding conditional content. React tries to insert before the detached text node and throws `NotFoundError`. 4. The boundary shows its recovery UI, but the old report loses the component trace. **Paperclip version or commit** Based on `6681c71b40`. The regression test reproduces the DOM mutation with the installed React version. **Deployment mode** Built browser UI with optional Sentry monitoring enabled for the signed-in session. ## What Changed - Pass the component stack and boundary kind from both error boundaries. - Keep at most 40 component names from at most 16 KiB of stack input. Drop locations and unrecognized lines. - Snapshot document readiness, visibility, and the browser translation root-class marker before the asynchronous reporting queue runs. - Attach diagnostics to that event only. Preserve the existing monitoring gate, sign-out behavior, and original exception if diagnostics fail. - Preserve function names in production bundles. Test the actual Vite production pipeline. - Document the fields, privacy limits, and translation-marker limitations. ## Verification - Focused diagnostics, boundary, real Sentry SDK, and production-build tests: 49 passed. - The translation DOM-mutation test reproduces `NotFoundError`, verifies the failed component trace, and keeps the recovery UI usable. - The real SDK test checks emitted events, private fixture exclusion, event isolation, and no capture after sign-out. Its transport stays in-process. - `pnpm check:token-gates`: passed. - [Greptile review](https://github.com/paperclipai/paperclip/pull/13904#issuecomment-5804220318): 5/5 on `92a2a23d9c`, with no review threads. - `pnpm -r typecheck` and `pnpm build`: passed. - Full CI unit and integration test matrix: passed, including all server, chat, workspace, runner, and serialized suites. - Local `pnpm test:run` was started, then stopped after the equivalent full CI matrix passed. The unsharded local run was not completed and is not claimed as a pass. - `pnpm exec playwright test --config tests/e2e/playwright.config.ts tests/e2e/project-repositories.spec.ts --repeat-each=3 --trace=on --reporter=line`: six tests passed against the production build. - Initial CI browser shard 8 timed out after the repository form replaced an enabled Save button with a disabled one before the click. The retained page snapshot shows the original repository selection. No error-boundary fallback appeared. [CI attempt 2](https://github.com/paperclipai/paperclip/actions/runs/35929990870/attempts/2) passed the failed jobs on the same commit. The full PR check matrix is green. This confirms an intermittent failure, but does not establish the cause of the first failure. - Full browser builds with and without name preservation passed. The initial JavaScript chunk grows from 1,571,919 to 1,668,453 gzip bytes (+6.1%). Total JavaScript across all chunks grows by 219,610 gzip bytes (+5.3%). ## Risks - This adds diagnostics. It reproduces a translation failure mode but does not identify or repair the specific application component from a past report. - The root-class marker is a hint. Other translation tools may omit it, and its presence does not prove causation. - Name preservation increases bundle size as measured above. No source maps are published by this change. - The error is still reported. DOM operations, browser translation, and recovery behavior are unchanged. ## Model Used OpenAI GPT-6 through Codex, with reasoning, code editing, terminal tools, and test execution. The exact serving model identifier and context-window size are not exposed in this session. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge Co-authored-by: Paperclip <noreply@paperclip.ing>
76 lines
2.7 KiB
TypeScript
76 lines
2.7 KiB
TypeScript
import { Component, type ErrorInfo, type ReactNode } from "react";
|
|
import { useLocation, useNavigate } from "@/lib/router";
|
|
import { Button } from "@/components/ui/button";
|
|
import { captureBrowserException } from "@/lib/sentry";
|
|
|
|
type RouteErrorBoundaryInnerProps = {
|
|
resetKey: string;
|
|
onReset: () => void;
|
|
children: ReactNode;
|
|
};
|
|
|
|
type RouteErrorBoundaryState = {
|
|
error: Error | null;
|
|
};
|
|
|
|
class RouteErrorBoundaryInner extends Component<RouteErrorBoundaryInnerProps, RouteErrorBoundaryState> {
|
|
override state: RouteErrorBoundaryState = { error: null };
|
|
|
|
static getDerivedStateFromError(error: unknown): RouteErrorBoundaryState {
|
|
return { error: error instanceof Error ? error : new Error(String(error)) };
|
|
}
|
|
|
|
override componentDidCatch(error: unknown, info: ErrorInfo): void {
|
|
console.error("Page render failed", { error, componentStack: info.componentStack });
|
|
captureBrowserException(error, { boundary: "route", componentStack: info.componentStack });
|
|
}
|
|
|
|
override componentDidUpdate(prevProps: RouteErrorBoundaryInnerProps): void {
|
|
// A render throw with no boundary unmounts the whole app, so navigating
|
|
// away (back button included) can't recover without a hard refresh. We sit
|
|
// above the routed <Outlet />, so reset whenever the route changes.
|
|
if (this.state.error && prevProps.resetKey !== this.props.resetKey) {
|
|
this.setState({ error: null });
|
|
}
|
|
}
|
|
|
|
override render() {
|
|
const { error } = this.state;
|
|
if (!error) return this.props.children;
|
|
|
|
return (
|
|
<div className="mx-auto max-w-2xl space-y-4 px-4 py-10">
|
|
<div>
|
|
<h1 className="text-lg font-semibold">This page hit an error</h1>
|
|
<p className="mt-1 text-sm text-muted-foreground">
|
|
Something went wrong while rendering this page. You can go back and try again, or reload.
|
|
</p>
|
|
</div>
|
|
<pre className="overflow-auto rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-xs text-destructive whitespace-pre-wrap">
|
|
{error.message}
|
|
</pre>
|
|
<div className="flex items-center gap-2">
|
|
<Button variant="outline" size="sm" onClick={this.props.onReset}>
|
|
Go back
|
|
</Button>
|
|
<Button size="sm" onClick={() => window.location.reload()}>
|
|
Reload page
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
}
|
|
|
|
export function RouteErrorBoundary({ children }: { children: ReactNode }) {
|
|
const location = useLocation();
|
|
const navigate = useNavigate();
|
|
const resetKey = `${location.pathname}${location.search}`;
|
|
|
|
return (
|
|
<RouteErrorBoundaryInner resetKey={resetKey} onReset={() => navigate(-1)}>
|
|
{children}
|
|
</RouteErrorBoundaryInner>
|
|
);
|
|
}
|