mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-31 22:24:16 +01: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>
58 lines
2.4 KiB
TypeScript
58 lines
2.4 KiB
TypeScript
export interface BrowserErrorDetails {
|
|
boundary: "app" | "route";
|
|
componentStack?: string | null;
|
|
}
|
|
|
|
const MAX_STACK_LENGTH = 16_384;
|
|
const MAX_COMPONENT_FRAMES = 40;
|
|
|
|
/** Keep component names only: React stack locations can contain tenant URLs. */
|
|
export function sanitizeComponentStack(stack: string | null | undefined): string | undefined {
|
|
if (!stack) return undefined;
|
|
const frames: string[] = [];
|
|
for (const line of stack.slice(0, MAX_STACK_LENGTH).split("\n").slice(0, MAX_COMPONENT_FRAMES + 1)) {
|
|
// V8/React and Firefox/Safari component-frame formats. Never retain the
|
|
// location, arguments, or an unrecognized line as a fallback.
|
|
const name = /^\s*(?:at|in) ([A-Za-z_$][\w$.-]{0,99})(?:\s|$)/.exec(line)?.[1]
|
|
?? /^([A-Za-z_$][\w$.-]{0,99})@/.exec(line)?.[1];
|
|
if (name) frames.push(` at ${name}`);
|
|
if (frames.length === MAX_COMPONENT_FRAMES) break;
|
|
}
|
|
return frames.length ? `\n${frames.join("\n")}` : undefined;
|
|
}
|
|
|
|
/** A bounded snapshot, taken at the error rather than when the capture queue runs. */
|
|
export function buildBrowserErrorContext(details: BrowserErrorDetails) {
|
|
const componentStack = sanitizeComponentStack(details.componentStack);
|
|
const browserState: {
|
|
ready_state?: "loading" | "interactive" | "complete";
|
|
visibility_state?: "visible" | "hidden";
|
|
translation_marker?: boolean;
|
|
} = {};
|
|
if (typeof document !== "undefined") {
|
|
// Each read is optional. A DOM wrapper or extension must not prevent the
|
|
// original exception and component trace from reaching the error monitor.
|
|
try {
|
|
const state = document.readyState;
|
|
if (state === "loading" || state === "interactive" || state === "complete") {
|
|
browserState.ready_state = state;
|
|
}
|
|
} catch { /* diagnostic unavailable */ }
|
|
try {
|
|
const state = document.visibilityState;
|
|
if (state === "visible" || state === "hidden") browserState.visibility_state = state;
|
|
} catch { /* diagnostic unavailable */ }
|
|
try {
|
|
const classes = document.documentElement.classList;
|
|
browserState.translation_marker = classes.contains("translated-ltr") || classes.contains("translated-rtl");
|
|
} catch { /* diagnostic unavailable */ }
|
|
}
|
|
return {
|
|
tags: { react_error_boundary: details.boundary },
|
|
contexts: {
|
|
react: componentStack ? { componentStack } : {},
|
|
browser_state: browserState,
|
|
},
|
|
};
|
|
}
|