Skip to content

Commit 67e3c84

Browse files
authored
fix: error boundary attachment override for react-native (#21)
1 parent d8f9f70 commit 67e3c84

10 files changed

Lines changed: 224 additions & 175 deletions

File tree

.github/workflows/cd.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,12 @@ jobs:
1111
runs-on: ubuntu-latest
1212
steps:
1313
- name: Checkout
14-
uses: actions/checkout@v3
14+
uses: actions/checkout@v4
1515

1616
- name: Setup Node
17-
uses: actions/setup-node@v3
17+
uses: actions/setup-node@v4
1818
with:
19-
node-version: lts/*
19+
node-version: 24
2020
cache: 'npm'
2121

2222
- name: Install Dependencies

.github/workflows/ci.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@ jobs:
1515
runs-on: ubuntu-latest
1616
strategy:
1717
matrix:
18-
node-version: [18]
18+
node-version: [24]
1919
steps:
2020
- name: Checkout
21-
uses: actions/checkout@v3
21+
uses: actions/checkout@v4
2222

2323
- name: Setup Node v${{ matrix.node-version }}
24-
uses: actions/setup-node@v3
24+
uses: actions/setup-node@v4
2525
with:
2626
node-version: ${{ matrix.node-version }}
2727
cache: 'npm'

.nvmrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
18
1+
24

package-lock.json

Lines changed: 135 additions & 142 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
"author": "zmrl",
3737
"license": "MIT",
3838
"dependencies": {
39-
"bugsplat": "^9.0.0"
39+
"bugsplat": "^9.1.0"
4040
},
4141
"devDependencies": {
4242
"@bugsplat/js-api-client": "^2.0.0",

spec/ErrorBoundary.spec.tsx

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ describe('<ErrorBoundary />', () => {
8787

8888
describe('when BugSplat has been initialized', () => {
8989
let bugSplat: BugSplat;
90-
let scope: Pick<Scope, 'getClient'>;
90+
let scope: Scope;
9191

9292
beforeEach(() => {
9393
bugSplat = {
@@ -96,7 +96,7 @@ describe('<ErrorBoundary />', () => {
9696
version: '',
9797
post: mockPost,
9898
} as unknown as BugSplat;
99-
scope = { getClient: () => bugSplat };
99+
scope = new Scope(bugSplat);
100100
});
101101

102102
it('should call onError', async () => {
@@ -152,6 +152,41 @@ describe('<ErrorBoundary />', () => {
152152
await waitFor(() => expect(mockPost).toHaveBeenCalledTimes(1));
153153
});
154154

155+
it('should attach componentStack as a text/plain Blob by default', async () => {
156+
render(
157+
<ErrorBoundary scope={scope} fallback={BasicFallback}>
158+
<BlowUp />
159+
</ErrorBoundary>
160+
);
161+
162+
await waitFor(() => expect(mockPost).toHaveBeenCalledTimes(1));
163+
164+
const [, options] = mockPost.mock.calls[0];
165+
expect(options.attachments).toHaveLength(1);
166+
const [attachment] = options.attachments;
167+
expect(attachment.filename).toBe('componentStack.txt');
168+
expect(attachment.data).toBeInstanceOf(Blob);
169+
expect(attachment.data.type).toBe('text/plain');
170+
});
171+
172+
it('honors scope.getCreateComponentStackAttachment() when provided', async () => {
173+
const customAttachment = { filename: 'componentStack.txt', data: 'CUSTOM' };
174+
const customBuilder = jest.fn(() => customAttachment);
175+
const scopeWithBuilder = new Scope(bugSplat, customBuilder);
176+
177+
render(
178+
<ErrorBoundary scope={scopeWithBuilder} fallback={BasicFallback}>
179+
<BlowUp />
180+
</ErrorBoundary>
181+
);
182+
183+
await waitFor(() => expect(mockPost).toHaveBeenCalledTimes(1));
184+
185+
expect(customBuilder).toHaveBeenCalledWith(expect.stringContaining('BlowUp'));
186+
const [, options] = mockPost.mock.calls[0];
187+
expect(options.attachments).toEqual([customAttachment]);
188+
});
189+
155190
it('should call beforePost', async () => {
156191
render(
157192
<ErrorBoundary

src/ErrorBoundary.tsx

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ import {
1111
type ReactElement,
1212
type ReactNode,
1313
} from 'react';
14-
import { getBugSplat } from './appScope';
14+
import { appScope } from './appScope';
15+
import type { Scope } from './scope';
1516

1617
/**
1718
* Shallowly compare two arrays to determine if they are different.
@@ -28,18 +29,6 @@ function isArrayDiff(a: unknown[] = [], b: unknown[] = []) {
2829
return a.some((item, index) => !Object.is(item, b[index]));
2930
}
3031

31-
/**
32-
* Pack a component stack trace string into an attachment
33-
*/
34-
function createComponentStackAttachment(
35-
componentStack: string
36-
): BugSplatAttachment {
37-
return {
38-
filename: 'componentStack.txt',
39-
data: new Blob([componentStack]),
40-
};
41-
}
42-
4332
export interface FallbackProps {
4433
/**
4534
* Error that caused crash
@@ -150,7 +139,7 @@ interface InternalErrorBoundaryProps {
150139
* to pass their own scope that will inject the client for use by
151140
* ErrorBoundary.
152141
*/
153-
scope: { getClient(): BugSplat | null };
142+
scope: Pick<Scope, 'getClient' | 'getCreateComponentStackAttachment'>;
154143
}
155144

156145
export type ErrorBoundaryProps = JSX.LibraryManagedAttributes<
@@ -210,7 +199,7 @@ export class ErrorBoundary extends Component<
210199
onResetKeysChange: noop,
211200
onUnmount: noop,
212201
disablePost: false,
213-
scope: { getClient: getBugSplat },
202+
scope: appScope,
214203
};
215204

216205
state = INITIAL_STATE;
@@ -257,9 +246,9 @@ export class ErrorBoundary extends Component<
257246
await beforePost(client, error, componentStack);
258247

259248
return client.post(error, {
260-
attachments: [
261-
createComponentStackAttachment(componentStack),
262-
],
249+
attachments: componentStack
250+
? [scope.getCreateComponentStackAttachment()(componentStack)]
251+
: [],
263252
});
264253
}
265254

src/appScope.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ export interface BugSplatInit {
2121
}
2222

2323
/**
24-
* Container for managing shared `BugSplat` instance
24+
* Container for the shared `BugSplat` instance and scope-level overrides.
2525
*/
26-
const appScope: Scope = new Scope();
26+
export const appScope: Scope = new Scope();
2727

2828
/**
2929
* Initialize a new BugSplat instance and store the reference in scope

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export type {
77

88
export * from './appScope';
99
export * from './ErrorBoundary';
10+
export * from './scope';
1011
export * from './useErrorHandler';
1112
export * from './useFeedback';
1213
export * from './withErrorBoundary';

src/scope.ts

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,30 @@
1-
import type { BugSplat } from 'bugsplat';
1+
import type { BugSplat, BugSplatAttachment } from 'bugsplat';
22

33
/**
4-
* Encapsulate BugSplat client instance
4+
* Builds the componentStack attachment for ErrorBoundary posts.
5+
*/
6+
export type CreateComponentStackAttachment = (
7+
componentStack: string
8+
) => BugSplatAttachment;
9+
10+
/**
11+
* Default builder — wraps the stack in a `text/plain` `Blob`.
12+
*/
13+
export const defaultCreateComponentStackAttachment: CreateComponentStackAttachment = (
14+
componentStack
15+
) => ({
16+
filename: 'componentStack.txt',
17+
data: new Blob([componentStack], { type: 'text/plain' }),
18+
});
19+
20+
/**
21+
* Encapsulate BugSplat client instance and scope-level overrides.
522
*/
623
export class Scope {
7-
constructor(private client: BugSplat | null = null) {}
24+
constructor(
25+
private client: BugSplat | null = null,
26+
private createComponentStackAttachment: CreateComponentStackAttachment = defaultCreateComponentStackAttachment
27+
) {}
828

929
/**
1030
* @returns BugSplat client instance or null if unset
@@ -16,4 +36,15 @@ export class Scope {
1636
setClient(client: BugSplat) {
1737
this.client = client;
1838
}
39+
40+
/**
41+
* @returns the current componentStack attachment builder
42+
*/
43+
getCreateComponentStackAttachment() {
44+
return this.createComponentStackAttachment;
45+
}
46+
47+
setCreateComponentStackAttachment(fn: CreateComponentStackAttachment) {
48+
this.createComponentStackAttachment = fn;
49+
}
1950
}

0 commit comments

Comments
 (0)