Skip to content

Commit 689f605

Browse files
bobbyg603claude
andcommitted
fix: wire ErrorBoundary attachment builder for React Native
@bugsplat/react@2.1 moved the componentStack attachment shape behind a Scope-injected builder; the default produces a browser Blob, which RN's FormData polyfill can't serialize. @bugsplat/expo installs an RN-safe builder (base64 data URI inside the { uri, type } file-ref shape RN streams as a real multipart file part) on appScope at the top of init(), synchronously before any awaits so an ErrorBoundary catching during startup doesn't race the default. Also updates the jest mocks in the native and Expo Go suites to expose the appScope.setCreateComponentStackAttachment setter, plus a new test that round-trips the installed builder through init() to verify the shape. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 443efd6 commit 689f605

4 files changed

Lines changed: 69 additions & 9 deletions

File tree

package-lock.json

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

src/BugsplatExpo.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { type BugSplat, init as initReact } from '@bugsplat/react';
1+
import { appScope, type BugSplat, init as initReact } from '@bugsplat/react';
2+
import type { BugSplatAttachment } from 'bugsplat';
23

34
import type {
45
BugSplatFeedbackOptions,
@@ -14,6 +15,36 @@ export const nativeAvailable = BugsplatExpoModule != null;
1415
let jsClient: BugSplat | null = null;
1516
const jsAttributes: Record<string, string> = {};
1617

18+
function utf8ToBase64(text: string): string {
19+
const NodeBuffer = (
20+
globalThis as {
21+
Buffer?: { from(s: string, enc: string): { toString(enc: string): string } };
22+
}
23+
).Buffer;
24+
if (NodeBuffer) {
25+
return NodeBuffer.from(text, 'utf-8').toString('base64');
26+
}
27+
return btoa(unescape(encodeURIComponent(text)));
28+
}
29+
30+
/**
31+
* React Native-compatible componentStack attachment builder. RN's FormData
32+
* polyfill can't serialize browser `Blob`s, so we hand it a `data:` URI inside
33+
* the `{ uri, type }` file-ref shape that RN's fetch uploads as a real file
34+
* part.
35+
*/
36+
function rnCreateComponentStackAttachment(
37+
componentStack: string
38+
): BugSplatAttachment {
39+
return {
40+
filename: 'componentStack.txt',
41+
data: {
42+
uri: `data:text/plain;base64,${utf8ToBase64(componentStack)}`,
43+
type: 'text/plain',
44+
},
45+
};
46+
}
47+
1748
function applyDefaults(client: BugSplat, options?: BugSplatInitOptions): void {
1849
if (options?.appKey) client.setDefaultAppKey(options.appKey);
1950
if (options?.userName) client.setDefaultUser(options.userName);
@@ -42,6 +73,11 @@ export async function init(
4273
version: string,
4374
options?: BugSplatInitOptions
4475
): Promise<void> {
76+
// Tell bugsplat-react's ErrorBoundary how to build its componentStack
77+
// attachment on React Native. Set synchronously before any awaits so an
78+
// ErrorBoundary that catches during startup doesn't race the default.
79+
appScope.setCreateComponentStackAttachment(rnCreateComponentStackAttachment);
80+
4581
if (nativeAvailable) {
4682
await BugsplatExpoModule!.init(database, application, version, options as Record<string, unknown>);
4783
initReact({ database, application, version })((client) => {

src/__tests__/BugsplatExpo.expoGo.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,13 @@ const mockInitReact = jest.fn().mockReturnValue(
2222
}
2323
);
2424

25+
const mockSetCreateComponentStackAttachment = jest.fn();
26+
2527
jest.mock('@bugsplat/react', () => ({
2628
init: mockInitReact,
29+
appScope: {
30+
setCreateComponentStackAttachment: mockSetCreateComponentStackAttachment,
31+
},
2732
}));
2833

2934
import {

src/__tests__/BugsplatExpo.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,13 @@ const mockInitReact = jest.fn().mockReturnValue(
3131
}
3232
);
3333

34+
const mockSetCreateComponentStackAttachment = jest.fn();
35+
3436
jest.mock('@bugsplat/react', () => ({
3537
init: mockInitReact,
38+
appScope: {
39+
setCreateComponentStackAttachment: mockSetCreateComponentStackAttachment,
40+
},
3641
}));
3742

3843
import {
@@ -70,6 +75,20 @@ describe('BugsplatExpo (native)', () => {
7075
});
7176
});
7277

78+
it('installs an RN-compatible componentStack attachment builder on appScope', async () => {
79+
await init('test-db', 'MyApp', '1.0.0');
80+
expect(mockSetCreateComponentStackAttachment).toHaveBeenCalledTimes(1);
81+
const [builder] = mockSetCreateComponentStackAttachment.mock.calls[0];
82+
const attachment = builder('at BuggyComponent\n at ErrorBoundary');
83+
expect(attachment).toEqual({
84+
filename: 'componentStack.txt',
85+
data: {
86+
uri: expect.stringMatching(/^data:text\/plain;base64,/),
87+
type: 'text/plain',
88+
},
89+
});
90+
});
91+
7392
it('passes options to native init', async () => {
7493
const options = {
7594
appKey: 'key123',

0 commit comments

Comments
 (0)