Skip to content

Commit 70abd39

Browse files
bobbyg603claude
andcommitted
feat: forward all post options through to bugsplat-js
Re-export upstream BugSplatOptions as BugSplatPostOptions/BugSplatFeedbackOptions and pass the whole options bag through to bs.post()/bs.postFeedback(), so consumers can use attachments and attributes (e.g. to bundle a componentStack with user-entered context from an ErrorBoundary fallback). Adds a README recipe showing the disablePost + manual-post pattern. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 43f1ac3 commit 70abd39

8 files changed

Lines changed: 150 additions & 83 deletions

README.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,57 @@ The fallback prop accepts a React node or a render function:
177177
</ErrorBoundary>
178178
```
179179

180+
### Collecting user input before posting
181+
182+
By default, `<ErrorBoundary>` posts to BugSplat the moment it catches an error. If you'd rather give the user a chance to describe what they were doing first — and bundle that into a single report instead of two — set `disablePost` on the boundary and post manually from your fallback:
183+
184+
```tsx
185+
import { useState } from 'react';
186+
import { ErrorBoundary, post } from '@bugsplat/expo';
187+
import { Text, TextInput, Button, View } from 'react-native';
188+
189+
<ErrorBoundary
190+
disablePost
191+
fallback={({ error, componentStack, resetErrorBoundary }) => {
192+
const [description, setDescription] = useState('');
193+
const [posted, setPosted] = useState(false);
194+
195+
const submit = async () => {
196+
if (posted) return;
197+
setPosted(true);
198+
await post(error, {
199+
description,
200+
attributes: { route: 'tasks/123' },
201+
attachments: componentStack
202+
? [{
203+
filename: 'componentStack.txt',
204+
data: new TextEncoder().encode(componentStack),
205+
}]
206+
: undefined,
207+
});
208+
};
209+
210+
return (
211+
<View>
212+
<Text>Something went wrong: {error.message}</Text>
213+
<TextInput value={description} onChangeText={setDescription} />
214+
<Button title="Submit" onPress={submit} />
215+
<Button title="Dismiss" onPress={() => { submit(); resetErrorBoundary(); }} />
216+
</View>
217+
);
218+
}}
219+
>
220+
<App />
221+
</ErrorBoundary>
222+
```
223+
224+
A few notes on this pattern:
225+
226+
- `post()` is **not** idempotent. The `posted` flag in the example is the consumer's responsibility — without it, "Submit then Dismiss" would fire two reports.
227+
- `componentStack` is wrapped in `Uint8Array` via `TextEncoder` (works on both web and native via Hermes). If you only target web, `new Blob([componentStack], { type: 'text/plain' })` reads more naturally.
228+
- `attributes` becomes a queryable column in the BugSplat dashboard — useful for filtering crashes by route, feature flag, build channel, etc.
229+
- If posting fails and you want retry, catch errors from `post()` and reset `posted` accordingly. The recipe doesn't show this to keep it minimal.
230+
180231
### User Feedback
181232

182233
Submit user feedback tied to your BugSplat database. Works on iOS, Android, and Web.

src/BugsplatExpo.ts

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -112,12 +112,7 @@ export async function post(
112112

113113
const err = error instanceof Error ? error : new Error(error);
114114
try {
115-
const result = await jsClient.post(err, {
116-
appKey: options?.appKey,
117-
user: options?.user,
118-
email: options?.email,
119-
description: options?.description,
120-
});
115+
const result = await jsClient.post(err, options);
121116
if (result.error) {
122117
return { success: false, error: result.error.message };
123118
}
@@ -144,12 +139,7 @@ export async function postFeedback(
144139
}
145140

146141
try {
147-
const result = await jsClient.postFeedback(title, {
148-
appKey: options?.appKey,
149-
user: options?.user,
150-
email: options?.email,
151-
description: options?.description,
152-
});
142+
const result = await jsClient.postFeedback(title, options);
153143
if (result.error) {
154144
return { success: false, error: result.error.message };
155145
}

src/BugsplatExpo.types.ts

Lines changed: 7 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
export type {
2+
BugSplatAttachment,
3+
BugSplatOptions,
4+
BugSplatOptions as BugSplatPostOptions,
5+
BugSplatOptions as BugSplatFeedbackOptions,
6+
} from 'bugsplat';
7+
18
/**
29
* Options for initializing BugSplat.
310
*/
@@ -18,20 +25,6 @@ export interface BugSplatInitOptions {
1825
description?: string;
1926
}
2027

21-
/**
22-
* Options for manually posting an error.
23-
*/
24-
export interface BugSplatPostOptions {
25-
/** Override default app key */
26-
appKey?: string;
27-
/** Override default user */
28-
user?: string;
29-
/** Override default email */
30-
email?: string;
31-
/** Description of the error context */
32-
description?: string;
33-
}
34-
3528
/**
3629
* Result from posting an error report.
3730
*/
@@ -42,20 +35,6 @@ export interface BugSplatPostResult {
4235
error?: string;
4336
}
4437

45-
/**
46-
* Options for submitting user feedback.
47-
*/
48-
export interface BugSplatFeedbackOptions {
49-
/** Override default app key */
50-
appKey?: string;
51-
/** Override default user name */
52-
user?: string;
53-
/** Override default user email */
54-
email?: string;
55-
/** Longer description of the feedback (title is the short summary) */
56-
description?: string;
57-
}
58-
5938
/**
6039
* Result from submitting user feedback.
6140
*/

src/BugsplatExpo.web.ts

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,7 @@ export async function post(
5959
const err = error instanceof Error ? error : new Error(error);
6060

6161
try {
62-
const result = await bs.post(err, {
63-
appKey: options?.appKey,
64-
user: options?.user,
65-
email: options?.email,
66-
description: options?.description,
67-
});
62+
const result = await bs.post(err, options);
6863

6964
if (result.error) {
7065
return { success: false, error: result.error.message };
@@ -90,12 +85,7 @@ export async function postFeedback(
9085
): Promise<BugSplatFeedbackResult> {
9186
const bs = getInstance();
9287
try {
93-
const result = await bs.postFeedback(title, {
94-
appKey: options?.appKey,
95-
user: options?.user,
96-
email: options?.email,
97-
description: options?.description,
98-
});
88+
const result = await bs.postFeedback(title, options);
9989
if (result.error) {
10090
return { success: false, error: result.error.message };
10191
}

src/__tests__/BugsplatExpo.expoGo.test.ts

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -114,20 +114,37 @@ describe('BugsplatExpo (Expo Go / JS fallback)', () => {
114114
const error = new Error('test error');
115115
const result = await post(error);
116116
expect(result).toEqual({ success: true });
117-
expect(mockBugSplatInstance.post).toHaveBeenCalledWith(error, {
118-
appKey: undefined,
119-
user: undefined,
120-
email: undefined,
121-
description: undefined,
122-
});
117+
expect(mockBugSplatInstance.post).toHaveBeenCalledWith(error, undefined);
118+
});
119+
120+
it('forwards attachments to JS client post', async () => {
121+
await init('test-db', 'MyApp', '1.0.0');
122+
const attachments = [
123+
{ filename: 'componentStack.txt', data: new Uint8Array([1, 2, 3]) },
124+
];
125+
await post(new Error('x'), { attachments });
126+
expect(mockBugSplatInstance.post).toHaveBeenCalledWith(
127+
expect.any(Error),
128+
expect.objectContaining({ attachments })
129+
);
130+
});
131+
132+
it('forwards attributes to JS client post', async () => {
133+
await init('test-db', 'MyApp', '1.0.0');
134+
const attributes = { route: 'home', channel: 'beta' };
135+
await post(new Error('x'), { attributes });
136+
expect(mockBugSplatInstance.post).toHaveBeenCalledWith(
137+
expect.any(Error),
138+
expect.objectContaining({ attributes })
139+
);
123140
});
124141

125142
it('posts string error by wrapping in Error', async () => {
126143
await init('test-db', 'MyApp', '1.0.0');
127144
await post('string error');
128145
expect(mockBugSplatInstance.post).toHaveBeenCalledWith(
129146
expect.objectContaining({ message: 'string error' }),
130-
expect.any(Object)
147+
undefined
131148
);
132149
});
133150

@@ -219,12 +236,18 @@ describe('BugsplatExpo (Expo Go / JS fallback)', () => {
219236
expect(result).toEqual({ success: true, crashId: 99 });
220237
expect(mockBugSplatInstance.postFeedback).toHaveBeenCalledWith(
221238
'Login button broken',
222-
{
223-
appKey: undefined,
224-
user: undefined,
225-
email: undefined,
226-
description: 'Nothing happens when I tap it',
227-
}
239+
{ description: 'Nothing happens when I tap it' }
240+
);
241+
});
242+
243+
it('forwards attachments and attributes to JS client postFeedback', async () => {
244+
await init('test-db', 'MyApp', '1.0.0');
245+
const attachments = [{ filename: 'log.txt', data: new Uint8Array([1]) }];
246+
const attributes = { route: 'settings' };
247+
await postFeedback('subject', { attachments, attributes });
248+
expect(mockBugSplatInstance.postFeedback).toHaveBeenCalledWith(
249+
'subject',
250+
expect.objectContaining({ attachments, attributes })
228251
);
229252
});
230253

src/__tests__/BugsplatExpo.test.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -193,12 +193,7 @@ describe('BugsplatExpo (native)', () => {
193193
expect(result).toEqual({ success: true, crashId: 42 });
194194
expect(mockBugSplatInstance.postFeedback).toHaveBeenCalledWith(
195195
'Login button broken',
196-
{
197-
appKey: undefined,
198-
user: undefined,
199-
email: undefined,
200-
description: 'Nothing happens when I tap it',
201-
}
196+
{ description: 'Nothing happens when I tap it' }
202197
);
203198
});
204199

src/__tests__/BugsplatExpo.web.test.ts

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -70,21 +70,49 @@ describe('BugsplatExpo (web)', () => {
7070
const error = new Error('test error');
7171
const result = await post(error);
7272
expect(result).toEqual({ success: true });
73-
expect(mockPost).toHaveBeenCalledWith(error, {
74-
appKey: undefined,
75-
user: undefined,
76-
email: undefined,
77-
description: undefined,
73+
expect(mockPost).toHaveBeenCalledWith(error, undefined);
74+
});
75+
76+
it('forwards attachments to bs.post', async () => {
77+
const attachments = [
78+
{
79+
filename: 'componentStack.txt',
80+
data: new Uint8Array([1, 2, 3]),
81+
},
82+
];
83+
await post(new Error('x'), { attachments });
84+
expect(mockPost).toHaveBeenCalledWith(
85+
expect.any(Error),
86+
expect.objectContaining({ attachments })
87+
);
88+
});
89+
90+
it('forwards attributes to bs.post', async () => {
91+
const attributes = { route: 'tasks/123', feature: 'beta' };
92+
await post(new Error('x'), { attributes });
93+
expect(mockPost).toHaveBeenCalledWith(
94+
expect.any(Error),
95+
expect.objectContaining({ attributes })
96+
);
97+
});
98+
99+
it('forwards existing fields (description, user, email, appKey)', async () => {
100+
await post(new Error('x'), {
101+
appKey: 'k',
102+
user: 'u',
103+
email: 'e',
104+
description: 'd',
78105
});
106+
expect(mockPost).toHaveBeenCalledWith(
107+
expect.any(Error),
108+
{ appKey: 'k', user: 'u', email: 'e', description: 'd' }
109+
);
79110
});
80111

81112
it('posts a string error by wrapping it in Error', async () => {
82113
const result = await post('string error');
83114
expect(result).toEqual({ success: true });
84-
expect(mockPost).toHaveBeenCalledWith(
85-
expect.any(Error),
86-
expect.any(Object)
87-
);
115+
expect(mockPost).toHaveBeenCalledWith(expect.any(Error), undefined);
88116
});
89117

90118
it('returns failure when post throws', async () => {
@@ -155,13 +183,22 @@ describe('BugsplatExpo (web)', () => {
155183
});
156184
expect(result).toEqual({ success: true, crashId: 7 });
157185
expect(mockPostFeedback).toHaveBeenCalledWith('Login button broken', {
158-
appKey: undefined,
159-
user: undefined,
160-
email: undefined,
161186
description: 'Nothing happens when I tap it',
162187
});
163188
});
164189

190+
it('forwards attachments and attributes to bs.postFeedback', async () => {
191+
const attachments = [
192+
{ filename: 'log.txt', data: new Uint8Array([1]) },
193+
];
194+
const attributes = { route: 'settings' };
195+
await postFeedback('subject', { attachments, attributes });
196+
expect(mockPostFeedback).toHaveBeenCalledWith(
197+
'subject',
198+
expect.objectContaining({ attachments, attributes })
199+
);
200+
});
201+
165202
it('returns failure when postFeedback throws', async () => {
166203
mockPostFeedback.mockRejectedValueOnce(new Error('network error'));
167204
const result = await postFeedback('subject');

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
export type {
2+
BugSplatAttachment,
3+
BugSplatOptions,
24
BugSplatInitOptions,
35
BugSplatPostOptions,
46
BugSplatPostResult,

0 commit comments

Comments
 (0)