Skip to content

Commit 153ad0c

Browse files
Merge pull request #18 from spatie/feature/react-flare-error-boundary-on-error-callback
Setup alias for playground development and add onError callback
2 parents 4f61dce + 30aea8a commit 153ad0c

File tree

9 files changed

+149
-32
lines changed

9 files changed

+149
-32
lines changed

.claude/docs/react-improvements.md

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# Research: Improving `@flareapp/react` Context Collection
2+
3+
## Current State
4+
5+
The `FlareErrorBoundary` currently captures:
6+
7+
- The error object
8+
- `componentStack` string from React's `ErrorInfo` (formatted)
9+
- Standard browser context via `@flareapp/js` (URL, user agent, cookies, query params)
10+
11+
That's about it. There's room to grow.
12+
13+
---
14+
15+
## Industry Landscape
16+
17+
| Feature | Industry standard | Flare |
18+
|--------------------------------|-------------------|---------|
19+
| Component stack | Yes | Yes |
20+
| Component names in breadcrumbs | Some (via plugins)| No |
21+
| Component props/state | **No** | **No** |
22+
| Redux/store state | Some | No |
23+
| React Router integration | Some | No |
24+
| React 19 root error handlers | Emerging | No |
25+
| Component interaction trail | Rare | No |
26+
| `beforeCapture` scope hook | Rare | No |
27+
| User feedback dialog | Rare | No |
28+
| Component profiling | Rare | No |
29+
30+
**Key finding: Nobody automatically captures component props or state.** Despite the theoretical possibility via React
31+
fiber internals, every tool either ignores these or provides manual hooks. Reasons: serialization complexity (circular
32+
refs, functions, React elements), performance cost of fiber tree traversal, and privacy risks (leaking PII/tokens).
33+
34+
---
35+
36+
## What's Worth Implementing (Prioritized)
37+
38+
### Tier 1 -- Baseline improvements (low effort, high value)
39+
40+
1. **Better component stack formatting and sourcemap-decoded display** -- currently just splitting by newline. Could
41+
parse into structured data (component name, file, line) for better Flare dashboard rendering.
42+
43+
2. **`beforeCapture` callback on ErrorBoundary** -- a `beforeCapture(scope, error, componentStack)` hook. Lets
44+
users attach custom tags, component props, or state to the error before it's sent. This is the pragmatic answer to "
45+
how do we capture state" -- let the developer decide what to include.
46+
47+
3. **React 19 `createRoot` error handlers** -- provide a `flareReactErrorHandler()` function for the new
48+
`onCaughtError`/`onUncaughtError`/`onRecoverableError` hooks. Few tools support this today. Gets you
49+
error capture *without* requiring an ErrorBoundary wrapper.
50+
51+
### Tier 2 -- Differentiating features (moderate effort)
52+
53+
4. **Component name annotation via build plugin** -- inject component name and source file attributes onto DOM nodes at
54+
build time (e.g. `data-flare-component`, `data-flare-source-file`). These survive minification. Since Flare already
55+
has `@flareapp/vite`, this could be a Vite plugin addition or a separate Babel plugin. Component names would then
56+
appear in breadcrumbs/glows.
57+
58+
5. **React Router integration** -- capture parameterized route names (`/users/:id` instead of `/users/12345`) as
59+
context. Add navigation breadcrumbs on route changes. Support React Router v6/v7 and TanStack Router.
60+
61+
6. **State management integration** -- provide a Redux middleware and/or Zustand middleware that attaches store state
62+
snapshots to error reports. Use a `stateTransformer` for sanitization.
63+
This is where "capture state" actually becomes practical -- at the store level, not the component level.
64+
65+
### Tier 3 -- Nice-to-have (higher effort)
66+
67+
7. **Component interaction breadcrumbs** -- track which components the user interacted with before the error. This gives
68+
a "user journey through components" trail.
69+
70+
8. **Component profiling** -- `withProfiler()` HOC / `useProfiler()` hook that tracks mount/render/update timing. Useful
71+
for correlating errors with performance, but requires a tracing infrastructure.
72+
73+
---
74+
75+
## What's NOT Worth Pursuing
76+
77+
- **Automatic props/state via fiber internals** -- no tool does this reliably. Internal API instability across React
78+
versions, serialization nightmares, privacy risks, performance cost. The fiber tree (`memoizedProps`, `memoizedState`)
79+
is unstable and undocumented.
80+
- **`captureOwnerStack()`** -- React 19 API that returns `null` in production. Development-only, useless for error
81+
tracking.
82+
- **Session replay** -- massive scope, framework-agnostic, not a React package concern.
83+
84+
---
85+
86+
## Recommended Implementation Order
87+
88+
For the `@flareapp/react` package specifically:
89+
90+
1. `beforeCapture` callback on `FlareErrorBoundary` -- quick win, gives users control
91+
2. React 19 `createRoot` error handler utility -- forward-looking, few tools support this yet
92+
3. Structured component stack parsing -- better data for the Flare dashboard
93+
4. Vite/Babel plugin for component name annotations -- builds on existing `@flareapp/vite`
94+
5. React Router integration (v6/v7)
95+
6. Redux/Zustand middleware (could be separate packages)
96+
97+
## Tasks
98+
99+
- [x] FlareErrorBoundary supports onError callback

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"build": "npm run build --workspaces --if-present",
1212
"test": "npm run test --workspaces --if-present",
1313
"typescript": "npm run typescript --workspaces --if-present",
14-
"playground": "npm run build && npm run dev --workspace=playground",
14+
"playground": "npm run dev --workspace=playground",
1515
"format": "prettier --write \"**/*.{js,json,vue,ts,tsx}\"",
1616
"prepare": "husky"
1717
},

packages/js/src/env/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ declare const FLARE_SOURCEMAP_VERSION: string | undefined;
33

44
// Injected during build
55
export const CLIENT_VERSION =
6-
typeof process.env.FLARE_JS_CLIENT_VERSION === 'undefined' ? '?' : process.env.FLARE_JS_CLIENT_VERSION;
6+
typeof process !== 'undefined' && typeof process.env?.FLARE_JS_CLIENT_VERSION !== 'undefined'
7+
? process.env.FLARE_JS_CLIENT_VERSION
8+
: '?';
79

810
// Injected by flare-vite-plugin-sourcemap-uploader (optional)
911
export const KEY = typeof FLARE_JS_KEY === 'undefined' ? '' : FLARE_JS_KEY;
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { flare } from '@flareapp/js';
2+
import { Component, ErrorInfo, type PropsWithChildren, type ReactNode } from 'react';
3+
4+
import { formatComponentStack } from './format-component-stack';
5+
6+
type Props = PropsWithChildren<{
7+
onError?: (error: Error, errorInfo: ErrorInfo) => void;
8+
}>;
9+
10+
type State = {};
11+
12+
export class FlareErrorBoundary extends Component<Props, State> {
13+
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
14+
const context = {
15+
react: {
16+
componentStack: formatComponentStack(errorInfo.componentStack ?? ''),
17+
},
18+
};
19+
20+
flare.report(error, context, { react: { errorInfo } });
21+
22+
this.props.onError?.(error, errorInfo);
23+
}
24+
25+
render(): ReactNode {
26+
return this.props.children;
27+
}
28+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function formatComponentStack(stack: string): string[] {
2+
return stack.split(/\s*\n\s*/g).filter((line) => line.length > 0);
3+
}

packages/react/src/index.ts

Lines changed: 1 addition & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1 @@
1-
import { flare } from '@flareapp/js';
2-
import { Component, PropsWithChildren } from 'react';
3-
4-
interface Context {
5-
react: {
6-
componentStack: Array<String>;
7-
};
8-
}
9-
10-
export class FlareErrorBoundary extends Component<PropsWithChildren> {
11-
componentDidCatch(error: Error, reactErrorInfo: React.ErrorInfo) {
12-
const context: Context = {
13-
react: {
14-
componentStack: formatReactComponentStack(reactErrorInfo?.componentStack ?? ''),
15-
},
16-
};
17-
18-
flare.report(error, context, { react: { errorInfo: reactErrorInfo } });
19-
}
20-
21-
render() {
22-
return this.props.children;
23-
}
24-
}
25-
26-
function formatReactComponentStack(stack: string) {
27-
return stack.split(/\s*\n\s*/g).filter((line) => line.length > 0);
28-
}
1+
export { FlareErrorBoundary } from './FlareErrorBoundary';

playground/react/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export function App() {
2828
Reset render error
2929
</Button>
3030
{showBuggy && (
31-
<FlareErrorBoundary>
31+
<FlareErrorBoundary onError={() => console.log('FlareErrorBoundary onError callback')}>
3232
<BuggyComponent />
3333
</FlareErrorBoundary>
3434
)}

playground/tsconfig.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
{
22
"extends": "../tsconfig.json",
33
"compilerOptions": {
4-
"jsx": "react-jsx"
4+
"jsx": "react-jsx",
5+
"paths": {
6+
"@flareapp/js": ["../packages/js/src/index.ts"],
7+
"@flareapp/react": ["../packages/react/src/index.ts"],
8+
"@flareapp/vue": ["../packages/vue/src/index.ts"]
9+
}
510
},
611
"include": ["**/*.ts", "**/*.tsx", "**/*.vue"]
712
}

playground/vite.config.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@ export default defineConfig(({ mode }) => {
99
const env = loadEnv(mode, process.cwd());
1010

1111
return {
12+
resolve: {
13+
alias: {
14+
'@flareapp/js': resolve(__dirname, '../packages/js/src/index.ts'),
15+
'@flareapp/react': resolve(__dirname, '../packages/react/src/index.ts'),
16+
'@flareapp/vue': resolve(__dirname, '../packages/vue/src/index.ts'),
17+
},
18+
},
1219
plugins: [
1320
tailwindcss(),
1421
react(),

0 commit comments

Comments
 (0)