Skip to content

Commit 93c3c6a

Browse files
Merge pull request #28 from spatie/feature/react-improvements
Feature/react improvements
2 parents 7e1aa5f + d5cc60a commit 93c3c6a

18 files changed

+714
-216
lines changed

.claude/CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
- We're equals.
88
- Try to be neutral and objective.
99
- Do not use emojis.
10+
- Do not use -- when writing comments or explaining something.
1011
- For more information regarding:
1112
- The research: take a look at .claude/docs/research
1213
- Repo cleanup: take a look at .claude/docs/repo-cleanup

.claude/docs/projects - react-improvements.md renamed to .claude/docs/projects-react-improvements.md

Lines changed: 66 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@
55
- [x] FlareErrorBoundary supports `fallback` property
66
- [x] FlareErrorBoundary supports fallback with a reset method for resetting the Error Boundary
77
- [x] FlareErrorBoundary fallback passes `componentStack`
8-
- [x] FlareErrorBoundary supports `onError` callback
9-
- [x] FlareErrorBoundary supports `beforeCapture` callback
8+
- [x] FlareErrorBoundary supports `beforeEvaluate` callback
9+
- [x] FlareErrorBoundary supports `beforeSubmit` callback
10+
- [x] FlareErrorBoundary supports `afterSubmit` callback
1011
- [x] FlareErrorBoundary supports `onReset` property
1112
- [x] FlareErrorBoundary `onReset` passes previous error
1213
- [x] FlareErrorBoundary supports `resetKeys` property
13-
- [x] Add `flareReactErrorHandler`
14+
- [x] Add `flareReactErrorHandler` with `beforeEvaluate`, `beforeSubmit`, and `afterSubmit` callbacks
1415
- [x] Structured component stack parsing with sourcemap-ready frames
1516

1617
## FlareErrorBoundary: `fallback` property
@@ -47,37 +48,64 @@ The `fallback` prop accepts either a static `ReactNode` or a render function. Th
4748
</FlareErrorBoundary>
4849
```
4950

50-
## FlareErrorBoundary: `onError` callback
51+
## FlareErrorBoundary: `beforeEvaluate` callback
5152

5253
### Why
5354

54-
Developers need a hook to perform side effects when an error is caught -- logging to a secondary service,
55-
showing a toast, updating app state, etc. This fires *after* the error has been reported to Flare.
55+
Fires *before* the component stack context is built, giving developers a chance to attach custom context, tags, or
56+
user information to the Flare report. This is the pragmatic answer to "how do we capture component props/state" -- let
57+
the developer decide what to include rather than trying to automatically serialize React internals.
5658

5759
```tsx
5860
<FlareErrorBoundary
59-
onError={({ error, errorInfo }) => {
60-
console.error('Caught by FlareErrorBoundary:', error);
61-
console.error('Component stack:', errorInfo.componentStack);
61+
beforeEvaluate={({ error, errorInfo }) => {
62+
flare.addContext('user', { id: currentUser.id });
63+
flare.addContext('feature-flags', getActiveFlags());
6264
}}
6365
>
6466
<App />
6567
</FlareErrorBoundary>
6668
```
6769

68-
## FlareErrorBoundary: `beforeCapture` callback
70+
## FlareErrorBoundary: `beforeSubmit` callback
6971

7072
### Why
7173

72-
Fires *before* the error is reported to Flare, giving developers a chance to attach custom context, tags, or
73-
user information to the Flare report. This is the pragmatic answer to "how do we capture component props/state" -- let
74-
the developer decide what to include rather than trying to automatically serialize React internals.
74+
Fires after the component stack context is built but *before* the error is reported to Flare. The callback receives
75+
the `context` and must return a (possibly modified) context object. Use this to filter or enrich the report context.
7576

7677
```tsx
7778
<FlareErrorBoundary
78-
beforeCapture={({ error, errorInfo }) => {
79-
flare.addContext('user', { id: currentUser.id });
80-
flare.addContext('feature-flags', getActiveFlags());
79+
beforeSubmit={({ error, errorInfo, context }) => {
80+
return {
81+
...context,
82+
react: {
83+
...context.react,
84+
componentStack: context.react.componentStack.filter(
85+
(line) => !line.includes('ThirdPartyWrapper'),
86+
),
87+
},
88+
};
89+
}}
90+
>
91+
<App />
92+
</FlareErrorBoundary>
93+
```
94+
95+
## FlareErrorBoundary: `afterSubmit` callback
96+
97+
### Why
98+
99+
Developers need a hook to perform side effects when an error is caught -- logging to a secondary service,
100+
showing a toast, updating app state, etc. This fires *after* the error has been reported to Flare. The callback
101+
receives the final context that was submitted.
102+
103+
```tsx
104+
<FlareErrorBoundary
105+
afterSubmit={({ error, errorInfo, context }) => {
106+
console.error('Caught by FlareErrorBoundary:', error);
107+
console.error('Component stack:', errorInfo.componentStack);
108+
console.error('Reported context:', context);
81109
}}
82110
>
83111
<App />
@@ -142,22 +170,37 @@ function App() {
142170
React 19 introduced `onCaughtError`, `onUncaughtError`, and `onRecoverableError` callbacks on `createRoot`.
143171
These are root-level error handlers that catch errors *without* requiring an ErrorBoundary wrapper.
144172

145-
`flareReactErrorHandler` is a wrapper function that accepts an optional callback. It also handles non-Error values
146-
(strings, objects) by converting them to proper Error instances via `convertToError()`, making it more resilient to
147-
edge cases.
173+
`flareReactErrorHandler` is a wrapper function that accepts an optional options object with `beforeEvaluate`,
174+
`beforeSubmit`, and `afterSubmit` callbacks -- the same callback pattern used by `FlareErrorBoundary`. It also handles
175+
non-Error values (strings, objects) by converting them to proper Error instances via `convertToError()`, making it more
176+
resilient to edge cases.
177+
178+
### Callback lifecycle
179+
180+
1. **`beforeEvaluate`** -- called after the error is converted to an `Error`, before building the component stack context. Use this to attach custom context to Flare (e.g. user info, feature flags).
181+
2. **`beforeSubmit`** -- called with the built context, must return a (possibly modified) context object. Use this to filter or enrich the report context before it is sent.
182+
3. **`flare.report()`** -- the error is reported to Flare.
183+
4. **`afterSubmit`** -- called after the report is sent. Use this for side effects like logging or showing a toast.
148184

149185
```tsx
150186
import { flareReactErrorHandler } from '@flareapp/react';
151187

152188
const root = createRoot(document.getElementById('root')!, {
153189
// Errors caught by an Error Boundary
154-
onCaughtError: flareReactErrorHandler((error, errorInfo) => {
155-
console.warn('Caught error:', error);
190+
onCaughtError: flareReactErrorHandler({
191+
afterSubmit: ({ error, errorInfo }) => {
192+
console.warn('Caught error:', error);
193+
},
156194
}),
157195

158196
// Errors NOT caught by any Error Boundary
159-
onUncaughtError: flareReactErrorHandler((error, errorInfo) => {
160-
console.error('Uncaught error:', error);
197+
onUncaughtError: flareReactErrorHandler({
198+
beforeEvaluate: ({ error }) => {
199+
flare.addContext('user', { id: currentUser.id });
200+
},
201+
afterSubmit: ({ error }) => {
202+
console.error('Uncaught error:', error);
203+
},
161204
}),
162205

163206
// Errors React recovers from automatically (e.g. hydration mismatches)

.claude/docs/research.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ handling (strings, numbers silently dropped).
6363
`sendBeacon()` for unload, no request timeout.
6464

6565
**React** (`@flareapp/react`) — captures component stack string only. Missing: fallback UI (`getDerivedStateFromError`),
66-
component props, component name, onError/onReset callbacks, React Router integration, state management integration.
66+
component props, component name, afterSubmit/onReset callbacks, React Router integration, state management integration.
6767

6868
**Vue** (`@flareapp/vue`) — captures component name + info string. Missing: component props, Vue Router context,
6969
Pinia/Vuex state, component tree.
@@ -123,7 +123,7 @@ Sprint target: fallback UI, callbacks, and component name. Defer React Router in
123123
- [ ] Fallback UI: implement `getDerivedStateFromError` so the boundary can render a fallback component
124124
- [ ] Configurable fallback: `<FlareErrorBoundary fallback={<ErrorPage />}>` or render prop
125125
`fallback={(error, reset) => ...}`
126-
- [ ] `onError` callback prop: let developers hook into error events
126+
- [ ] `afterSubmit` callback prop: let developers hook into error events
127127
- [ ] `onReset` callback prop: for error recovery flows
128128
- [ ] Capture component props from the error boundary's child tree
129129
- [ ] Capture the erroring component's name (not just the stack)

packages/react/src/FlareErrorBoundary.ts

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { flare } from '@flareapp/js';
22
import { Component, ErrorInfo, type PropsWithChildren, type ReactNode } from 'react';
33

4-
import { formatComponentStack } from './format-component-stack';
5-
import { parseComponentStack } from './parse-component-stack';
4+
import { formatComponentStack } from './formatComponentStack';
5+
import { parseComponentStack } from './parseComponentStack';
66
import { FlareReactContext } from './types';
77

88
export type FlareErrorBoundaryFallbackProps = {
@@ -14,8 +14,9 @@ export type FlareErrorBoundaryFallbackProps = {
1414
export type FlareErrorBoundaryProps = PropsWithChildren<{
1515
fallback?: ReactNode | ((props: FlareErrorBoundaryFallbackProps) => ReactNode);
1616
resetKeys?: unknown[];
17-
beforeCapture?: (params: { error: Error; errorInfo: ErrorInfo }) => void;
18-
onError?: (params: { error: Error; errorInfo: ErrorInfo }) => void;
17+
beforeEvaluate?: (params: { error: Error; errorInfo: ErrorInfo }) => void;
18+
beforeSubmit?: (params: { error: Error; errorInfo: ErrorInfo; context: FlareReactContext }) => FlareReactContext;
19+
afterSubmit?: (params: { error: Error; errorInfo: ErrorInfo; context: FlareReactContext }) => void;
1920
onReset?: (error: Error | null) => void;
2021
}>;
2122

@@ -32,7 +33,7 @@ export class FlareErrorBoundary extends Component<FlareErrorBoundaryProps, Flare
3233
}
3334

3435
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
35-
this.props.beforeCapture?.({
36+
this.props.beforeEvaluate?.({
3637
error,
3738
errorInfo,
3839
});
@@ -46,13 +47,21 @@ export class FlareErrorBoundary extends Component<FlareErrorBoundaryProps, Flare
4647
},
4748
};
4849

49-
this.setState({ componentStack: context.react.componentStack });
50+
const finalContext =
51+
this.props.beforeSubmit?.({
52+
error,
53+
errorInfo,
54+
context,
55+
}) ?? context;
5056

51-
flare.report(error, context, { react: { errorInfo } });
57+
this.setState({ componentStack: finalContext.react.componentStack });
5258

53-
this.props.onError?.({
59+
flare.report(error, finalContext, { react: { errorInfo } });
60+
61+
this.props.afterSubmit?.({
5462
error,
5563
errorInfo,
64+
context: finalContext,
5665
});
5766
}
5867

packages/react/src/flare-react-error-handler.ts

Lines changed: 0 additions & 27 deletions
This file was deleted.
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { flare } from '@flareapp/js';
2+
3+
import { convertToError } from './convertToError';
4+
import { formatComponentStack } from './formatComponentStack';
5+
import { parseComponentStack } from './parseComponentStack';
6+
import { FlareReactContext } from './types';
7+
8+
export type FlareReactErrorHandlerCallback = (error: unknown, errorInfo: { componentStack?: string }) => void;
9+
10+
export type FlareReactErrorHandlerOptions = {
11+
beforeEvaluate?: (params: { error: Error; errorInfo: { componentStack?: string } }) => void;
12+
beforeSubmit?: (params: {
13+
error: Error;
14+
errorInfo: { componentStack?: string };
15+
context: FlareReactContext;
16+
}) => FlareReactContext;
17+
afterSubmit?: (params: {
18+
error: Error;
19+
errorInfo: { componentStack?: string };
20+
context: FlareReactContext;
21+
}) => void;
22+
};
23+
24+
export function flareReactErrorHandler(options?: FlareReactErrorHandlerOptions): FlareReactErrorHandlerCallback {
25+
return (error: unknown, errorInfo: { componentStack?: string }) => {
26+
const errorObject = convertToError(error);
27+
28+
options?.beforeEvaluate?.({ error: errorObject, errorInfo });
29+
30+
const rawStack = errorInfo.componentStack ?? '';
31+
32+
const context: FlareReactContext = {
33+
react: {
34+
componentStack: formatComponentStack(rawStack),
35+
componentStackFrames: parseComponentStack(rawStack),
36+
},
37+
};
38+
39+
const finalContext =
40+
options?.beforeSubmit?.({
41+
error: errorObject,
42+
errorInfo,
43+
context,
44+
}) ?? context;
45+
46+
flare.report(errorObject, finalContext, { react: { errorInfo } });
47+
48+
options?.afterSubmit?.({ error: errorObject, errorInfo, context: finalContext });
49+
};
50+
}
File renamed without changes.

packages/react/src/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,8 @@ export {
55
type FlareErrorBoundaryState,
66
} from './FlareErrorBoundary';
77

8-
export { flareReactErrorHandler, type FlareReactErrorHandlerCallback } from './flare-react-error-handler';
8+
export {
9+
flareReactErrorHandler,
10+
type FlareReactErrorHandlerCallback,
11+
type FlareReactErrorHandlerOptions,
12+
} from './flareReactErrorHandler';
File renamed without changes.

0 commit comments

Comments
 (0)