Skip to content

Commit 2fe689f

Browse files
bobbyg603claude
andauthored
feat: user feedback reports (#74)
* Add postFeedback API for user feedback submission Adds postFeedback method using the 3-step presigned URL upload flow (getCrashUploadUrl -> PUT to S3 -> commitS3CrashUpload) with crashTypeId=36. Creates feedback.json with title and description, zips it using a minimal inline ZIP implementation with CRC-32, and uploads via the presigned URL flow. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Replace inline ZIP implementation with jszip library Use jszip instead of hand-rolled ZIP/CRC-32 code for creating the feedback.json zip archive in postFeedback. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Replace jszip with fflate for lighter zip creation fflate is tree-shakeable (~3KB gzipped) vs jszip (~45KB). Only zipSync and strToU8 are imported. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add attachments support to postFeedback (e.g. screenshots) Adds BugSplatAttachment type and attachments option to BugSplatFeedbackOptions. Attachments are included in the zip alongside feedback.json. Named BugSplatAttachment (not BugSplatFeedbackAttachment) so it can be reused for other report types in the future. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add postFeedback and attachments documentation to README Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix no-explicit-any lint error in postFeedback by using validateResponseBody type guard Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * BREAKING: Unify crash and feedback uploads via presigned URL flow Migrate post() from direct /post/js/ to the presigned URL upload flow, add attributes and attachments as first-class options, remove additionalFormDataParams and FormDataParam, and unify response types to match the commitS3CrashUpload endpoint shape. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix no-explicit-any lint error in test file Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Scope type suppression to private field mock only Use @ts-expect-error on the single _formData assignment instead of typing the whole bugsplat variable as any. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Update validateResponseBody tests for new response shape Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Use javascriptCallStack.txt instead of crash.json in zip Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Make infoUrl optional in response validation The server doesn't always return infoUrl in the commit response, causing validateResponseBody to incorrectly reject valid responses. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix unused variable lint error in post method The description variable was already resolved inside _upload, making the one in post redundant. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent bbaabc9 commit 2fe689f

11 files changed

Lines changed: 501 additions & 413 deletions

README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,31 @@ async bugsplat.post(error, options); // Posts an arbitrary Error object to BugSp
106106
// Returns a promise that resolves with properties: error (if there was an error posting to BugSplat), response (the response from the BugSplat crash post API), and original (the error passed by bugsplat.post)
107107
```
108108

109+
### User Feedback
110+
111+
You can also submit non-crashing user feedback (e.g. bug reports, feature requests) using `postFeedback`. Feedback reports appear in BugSplat with the "User Feedback" crash type, grouped by `title`.
112+
113+
```ts
114+
const response = await bugsplat.postFeedback('Login button does not respond', {
115+
description: 'Tapping login on iPhone does nothing.',
116+
user: 'jane@example.com',
117+
email: 'jane@example.com',
118+
});
119+
```
120+
121+
You can attach files such as screenshots:
122+
123+
```ts
124+
const screenshot = document.querySelector('input[type="file"]').files[0];
125+
126+
await bugsplat.postFeedback('UI rendering issue', {
127+
description: 'The sidebar overlaps the main content.',
128+
attachments: [
129+
{ filename: 'screenshot.png', data: screenshot },
130+
],
131+
});
132+
```
133+
109134
## 📢 Upgrading
110135

111136
If you are developing a Node.js application and were using bugsplat-js <= 5.0.0 please upgrade to [bugsplat-node](https://www.npmjs.com/package/bugsplat-node). BugSplat-node has the same consumer APIs as bugsplat-js <= 5.0.0. Additionally, support for file attachments and exiting the Node process in the error handler have been moved to [bugsplat-node](https://www.npmjs.com/package/bugsplat-node) so that bugsplat-js can be run in browsers as well as Node.js environments.

package-lock.json

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

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,5 +55,8 @@
5555
"typescript": "^5.9.3",
5656
"typescript-eslint": "^8.33.1",
5757
"vitest": "^4.0.16"
58+
},
59+
"dependencies": {
60+
"fflate": "^0.8.2"
5861
}
5962
}

spec/bugsplat-response.spec.ts

Lines changed: 36 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,30 @@ describe('validateResponseBody', () => {
66
const responseBodies = [
77
{
88
status: 'success',
9-
current_server_time: 12,
10-
message: 'message 1',
11-
url: 'osaiujdhfihfju',
12-
crash_id: 9,
9+
crashId: 9,
10+
stackKeyId: 1,
11+
messageId: 1,
12+
infoUrl: 'https://app.bugsplat.com/browse/crashInfo.php',
1313
},
1414
{
1515
status: 'fail',
16-
current_server_time: 24,
17-
message: 'message 2',
18-
url: 'http://example.com',
19-
crash_id: 100,
16+
crashId: 100,
17+
stackKeyId: 55,
18+
messageId: 200,
19+
infoUrl: 'https://app.bugsplat.com/browse/crashInfo.php?id=100',
2020
},
2121
{
2222
status: 'success',
23-
current_server_time: -100,
24-
message: 'message 3',
25-
url: 'http://bugsplat.com/rocks',
26-
crash_id: 333,
23+
crashId: 333,
24+
stackKeyId: 0,
25+
messageId: 0,
26+
infoUrl: 'https://app.bugsplat.com/browse/crashInfo.php?id=333',
27+
},
28+
{
29+
status: 'success',
30+
crashId: 21417,
31+
stackKeyId: -1,
32+
messageId: -1,
2733
},
2834
];
2935

@@ -41,41 +47,35 @@ describe('validateResponseBody', () => {
4147
{},
4248
{
4349
status: 'succes',
44-
current_server_time: 12,
45-
message: 'message 1',
46-
url: 'osaiujdhfihfju',
47-
crash_id: 9,
50+
crashId: 9,
51+
stackKeyId: 1,
52+
messageId: 1,
53+
infoUrl: 'https://app.bugsplat.com',
4854
},
4955
{
50-
status: 'fail',
51-
current_server_time: 24,
52-
message: 'message 2',
53-
url: new Date(),
54-
crash_id: 100,
55-
},
56-
{
57-
current_server_time: -100,
58-
message: 'message 3',
59-
url: 'http://bugsplat.com/rocks',
60-
crash_id: 333,
56+
status: 'success',
57+
stackKeyId: 1,
58+
messageId: 1,
59+
infoUrl: 'https://app.bugsplat.com',
6160
},
6261
{
6362
status: 'success',
64-
message: 'message 3',
65-
url: 'http://bugsplat.com/rocks',
66-
crash_id: 333,
63+
crashId: 9,
64+
messageId: 1,
65+
infoUrl: 'https://app.bugsplat.com',
6766
},
6867
{
6968
status: 'success',
70-
current_server_time: -100,
71-
url: 'http://bugsplat.com/rocks',
72-
crash_id: 333,
69+
crashId: 9,
70+
stackKeyId: 1,
71+
infoUrl: 'https://app.bugsplat.com',
7372
},
7473
{
7574
status: 'success',
76-
current_server_time: -100,
77-
message: 'message 3',
78-
url: 'http://bugsplat.com/rocks',
75+
crashId: 9,
76+
stackKeyId: 1,
77+
messageId: 1,
78+
infoUrl: 42,
7979
},
8080
];
8181

spec/bugsplat.e2e.spec.ts

Lines changed: 51 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ describe('BugSplat', () => {
1717
throw new Error('Please set FRED_PASSWORD environment variable');
1818
}
1919

20-
const api = await BugSplatApiClient.createAuthenticatedClientForNode(email, password);
20+
const api = await BugSplatApiClient.createAuthenticatedClientForNode(
21+
email,
22+
password,
23+
);
2124
client = new CrashApiClient(api);
2225

2326
// Posting too frequently results in 400s from the web server
@@ -36,26 +39,62 @@ describe('BugSplat', () => {
3639
const additionalFile = './spec/files/additionalFile.txt';
3740
const fileName = path.basename(additionalFile);
3841
const fileContents = await readFile(additionalFile);
39-
const additionalFormDataParams = [
40-
{
41-
key: fileName,
42-
value: new Blob([new Uint8Array(fileContents)]),
43-
filename: fileName,
44-
},
45-
{ key: 'attributes', value: '{"foo": "bar"}' },
46-
];
4742
const bugsplat = new BugSplat(database, appName, appVersion);
4843
bugsplat.setDefaultAppKey(appKey);
4944
bugsplat.setDefaultUser(user);
5045
bugsplat.setDefaultEmail(email);
5146
bugsplat.setDefaultDescription(description);
5247

53-
const result = await bugsplat.post(error, { additionalFormDataParams });
48+
const result = await bugsplat.post(error, {
49+
attachments: [
50+
{ data: new Uint8Array(fileContents), filename: fileName },
51+
],
52+
attributes: { foo: 'bar' },
53+
});
5454
if (result.error) {
5555
throw new Error(result.error.message);
5656
}
5757

58-
const expectedCrashId = result.response.crash_id;
58+
const expectedCrashId = result.response.crashId;
59+
const crashData = await client.getCrashById(database, expectedCrashId);
60+
expect(crashData.appName).toEqual(appName);
61+
expect(crashData.appVersion).toEqual(appVersion);
62+
expect(crashData.appKey).toEqual(appKey);
63+
expect(crashData.description).toEqual(description);
64+
expect(crashData.user).toBeTruthy(); // Fred has PII obfuscated so the best we can do here is to check if truthy
65+
expect(crashData.email).toBeTruthy(); // Fred has PII obfuscated so the best we can do here is to check if truthy
66+
}, 30000);
67+
68+
it('should post a feedback report with all provided information', async () => {
69+
const database = 'fred';
70+
const appName = 'my-node-crasher';
71+
const appVersion = '1.2.3.4';
72+
const appKey = 'Key!';
73+
const user = 'User!';
74+
const email = 'fred@bedrock.com';
75+
const title = 'Symbol Store Size Limit';
76+
const description =
77+
'I hit the 8 GB symbol size limit and can no longer upload symbols';
78+
const additionalFile = './spec/files/additionalFile.txt';
79+
const fileName = path.basename(additionalFile);
80+
const fileContents = await readFile(additionalFile);
81+
const bugsplat = new BugSplat(database, appName, appVersion);
82+
bugsplat.setDefaultAppKey(appKey);
83+
bugsplat.setDefaultUser(user);
84+
bugsplat.setDefaultEmail(email);
85+
bugsplat.setDefaultDescription(description);
86+
87+
const result = await bugsplat.postFeedback(title, {
88+
attachments: [
89+
{ data: new Uint8Array(fileContents), filename: fileName },
90+
],
91+
attributes: { foo: 'bar' },
92+
});
93+
if (result.error) {
94+
throw new Error(result.error.message);
95+
}
96+
97+
const expectedCrashId = result.response.crashId;
5998
const crashData = await client.getCrashById(database, expectedCrashId);
6099
expect(crashData.appName).toEqual(appName);
61100
expect(crashData.appVersion).toEqual(appVersion);
@@ -76,7 +115,7 @@ describe('BugSplat', () => {
76115
if (result.error) {
77116
throw new Error(result.error.message);
78117
}
79-
const expectedCrashId = result.response.crash_id;
118+
const expectedCrashId = result.response.crashId;
80119
const crashData = await client.getCrashById(database, expectedCrashId);
81120

82121
expect(crashData.appName).toEqual(appName);

0 commit comments

Comments
 (0)