Skip to content

Commit d79517e

Browse files
committed
Attach React component stacks to error data
1 parent cd3dd96 commit d79517e

6 files changed

Lines changed: 46 additions & 2 deletions

File tree

packages/browser/src/plugins/BrowserErrorPlugin.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ import {
1212

1313
import { fromError, StackFrame } from "stacktrace-js";
1414

15+
const ReactComponentStackContextKey = "@@_ComponentStack";
16+
const ReactComponentStackDataKey = "@component_stack";
17+
1518
export class BrowserErrorPlugin implements IEventPlugin {
1619
public priority = 30;
1720
public name = "BrowserErrorPlugin";
@@ -36,6 +39,12 @@ export class BrowserErrorPlugin implements IEventPlugin {
3639
result.data["@ext"] = JSON.parse(additionalData);
3740
}
3841

42+
const componentStack = context.eventContext[ReactComponentStackContextKey];
43+
if (typeof componentStack === "string" && componentStack) {
44+
result.data = result.data ?? {};
45+
result.data[ReactComponentStackDataKey] = componentStack;
46+
}
47+
3948
context.event.data[KnownEventDataKeys.Error] = result;
4049
}
4150
}

packages/browser/test/plugins/BrowserErrorPlugin.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,16 @@ describe("BrowserErrorPlugin", () => {
111111
const additionalData = getAdditionalData(context.event);
112112
expect(additionalData).toBeUndefined();
113113
});
114+
115+
test("should attach React component stack to error data", async () => {
116+
const componentStack = "\n at CrashyComponent (http://localhost:3000/index.js:10:20)";
117+
context.eventContext["@@_ComponentStack"] = componentStack;
118+
119+
await processError(new Error("Component crashed"));
120+
121+
expect(getError(context.event)?.data?.["@component_stack"]).toBe(componentStack);
122+
expect(context.event.data?.componentStack).toBeUndefined();
123+
});
114124
});
115125
});
116126

packages/react-native/src/ExceptionlessErrorBoundary.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { Component, type ErrorInfo as ReactErrorInfo, type PropsWithChildren, ty
22

33
import { Exceptionless } from "./client.js";
44

5+
const ReactComponentStackContextKey = "@@_ComponentStack";
6+
57
interface ErrorBoundaryProps {
68
fallback?: ReactNode;
79
}
@@ -23,7 +25,7 @@ export class ExceptionlessErrorBoundary extends Component<PropsWithChildren<Erro
2325
async componentDidCatch(error: Error, errorInfo: ReactErrorInfo): Promise<void> {
2426
const builder = Exceptionless.createException(error);
2527
if (errorInfo.componentStack) {
26-
builder.setProperty("componentStack", errorInfo.componentStack);
28+
builder.setContextProperty(ReactComponentStackContextKey, errorInfo.componentStack);
2729
}
2830

2931
await builder.submit();

packages/react-native/src/plugins/ReactNativeErrorPlugin.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ interface ParsedLocation {
1616
isNative: boolean;
1717
}
1818

19+
const ReactComponentStackContextKey = "@@_ComponentStack";
20+
const ReactComponentStackDataKey = "@component_stack";
21+
1922
export class ReactNativeErrorPlugin implements IEventPlugin {
2023
public priority = 30;
2124
public name = "ReactNativeErrorPlugin";
@@ -36,6 +39,12 @@ export class ReactNativeErrorPlugin implements IEventPlugin {
3639
result.data["@ext"] = JSON.parse(additionalData);
3740
}
3841

42+
const componentStack = context.eventContext[ReactComponentStackContextKey];
43+
if (typeof componentStack === "string" && componentStack) {
44+
result.data = result.data ?? {};
45+
result.data[ReactComponentStackDataKey] = componentStack;
46+
}
47+
3948
context.event.data[KnownEventDataKeys.Error] = result;
4049
}
4150
}

packages/react-native/test/plugins/ReactNativeErrorPlugin.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,18 @@ describe("ReactNativeErrorPlugin", () => {
162162
})
163163
]);
164164
});
165+
166+
test("should attach React component stack to error data", async () => {
167+
const error = new Error("Component crashed inside error boundary!");
168+
const componentStack = "\n at CrashyComponent (http://localhost:8083/index.bundle:10:20)";
169+
context.eventContext.setException(error);
170+
context.eventContext["@@_ComponentStack"] = componentStack;
171+
172+
await plugin.run(context);
173+
174+
expect(getError(context.event)?.data?.["@component_stack"]).toBe(componentStack);
175+
expect(context.event.data?.componentStack).toBeUndefined();
176+
});
165177
});
166178

167179
function getError(event: Event): ErrorInfo | undefined {

packages/react/src/ExceptionlessErrorBoundary.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { Component, type ErrorInfo as ReactErrorInfo, type PropsWithChildren } from "react";
22
import { Exceptionless } from "@exceptionless/browser";
33

4+
const ReactComponentStackContextKey = "@@_ComponentStack";
5+
46
type ErrorState = {
57
hasError: boolean;
68
};
@@ -13,7 +15,7 @@ export class ExceptionlessErrorBoundary extends Component<PropsWithChildren, Err
1315
async componentDidCatch(error: Error, errorInfo: ReactErrorInfo) {
1416
const builder = Exceptionless.createException(error);
1517
if (errorInfo.componentStack) {
16-
builder.setProperty("componentStack", errorInfo.componentStack);
18+
builder.setContextProperty(ReactComponentStackContextKey, errorInfo.componentStack);
1719
}
1820

1921
await builder.submit();

0 commit comments

Comments
 (0)