Skip to content

Commit 106d828

Browse files
authored
fix(feedback)!: always reject sendFeedback with an Error (#20475)
`sendFeedback` currently rejects with an `Error` in some paths and a raw string in others. This normalizes all paths to reject with an `Error`, which is the expected shape and easier to handle for consumers. Breaking: intended for the next major.
1 parent 7226a4a commit 106d828

5 files changed

Lines changed: 44 additions & 19 deletions

File tree

‎MIGRATION.md‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -613,6 +613,22 @@ on casing, or use `tracePropagationTargets` in combination with a more specific
613613
As part of this, the `g` and `y` flags are ignored on `tracePropagationTargets` regular expressions. These flags made
614614
matching stateful via `lastIndex`, so a target like `/myApi\.com/g` previously matched only every other request.
615615

616+
### `sendFeedback` rejects with an `Error`
617+
618+
Affected SDKs: All SDKs running in the browser.
619+
620+
`Sentry.sendFeedback()` now rejects with an `Error` in all cases. Previously it rejected with a plain string when the request timed out, was rejected with a 403, or otherwise failed to send, while the synchronous validation paths (empty message, no client configured) already threw an `Error`. The message text itself is unchanged, and is still customizable through the `errorMessages` hint, so read it off `error.message`:
621+
622+
```js
623+
try {
624+
await Sentry.sendFeedback({ message: 'Hello' });
625+
} catch (error) {
626+
// v10: a string on send failures, an Error on validation failures
627+
// v11: always an Error
628+
console.log(error.message);
629+
}
630+
```
631+
616632
### Span attribute changes
617633

618634
Affected SDKs: All SDKs.

‎packages/feedback/src/core/sendFeedback.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import type {
88
} from '@sentry/core';
99
import { captureFeedback, getClient, getCurrentScope, getLocationHref } from '@sentry/core';
1010
import { FEEDBACK_API_SOURCE } from '../constants';
11-
import { createFeedbackError, resolveFeedbackErrorMessage } from '../util/createFeedbackError';
11+
import { createFeedbackError } from '../util/createFeedbackError';
1212

1313
/**
1414
* Public API to send a Feedback item to Sentry
@@ -47,7 +47,7 @@ export const sendFeedback: SendFeedback = (
4747
// After 30s, we want to clear anyhow
4848
const timeout = setTimeout(() => {
4949
cleanup();
50-
reject(resolveFeedbackErrorMessage('ERROR_TIMEOUT', errorMessages));
50+
reject(createFeedbackError('ERROR_TIMEOUT', errorMessages));
5151
}, 30_000);
5252

5353
const cleanup = client.on('afterSendEvent', (event: Event, response: TransportMakeRequestResponse) => {
@@ -64,10 +64,10 @@ export const sendFeedback: SendFeedback = (
6464
}
6565

6666
if (response?.statusCode === 403) {
67-
return reject(resolveFeedbackErrorMessage('ERROR_FORBIDDEN', errorMessages));
67+
return reject(createFeedbackError('ERROR_FORBIDDEN', errorMessages));
6868
}
6969

70-
return reject(resolveFeedbackErrorMessage('ERROR_GENERIC', errorMessages));
70+
return reject(createFeedbackError('ERROR_GENERIC', errorMessages));
7171
});
7272
});
7373
};

‎packages/feedback/src/modal/components/Form.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ export function Form({
131131
onSubmitSuccess(data, eventId);
132132
} catch (error) {
133133
DEBUG_BUILD && debug.error(error);
134-
const err = error instanceof Error ? error : new Error(String(error));
134+
const err = error as Error;
135135
setError(err.message);
136136
onSubmitError(err);
137137
}

‎packages/feedback/src/util/createFeedbackError.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const DEFAULT_MESSAGES: Record<FeedbackErrorCode, string> = {
1515
ERROR_GENERIC: ERROR_GENERIC_TEXT,
1616
};
1717

18-
export function resolveFeedbackErrorMessage(code: FeedbackErrorCode, messages?: FeedbackErrorMessages): string {
18+
function resolveFeedbackErrorMessage(code: FeedbackErrorCode, messages?: FeedbackErrorMessages): string {
1919
return messages?.[code] ?? DEFAULT_MESSAGES[code];
2020
}
2121

‎packages/feedback/test/core/sendFeedback.test.ts‎

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,18 @@ describe('sendFeedback', () => {
3333
patchedDecoder && delete global.window.TextDecoder;
3434
});
3535

36+
// `sendFeedback` always signals failure with an `Error`, never a bare string. A `toThrow(text)`
37+
// assertion alone also passes for a thrown/rejected string, so assert the shape explicitly too.
38+
async function expectRejectsWithError(promise: Promise<unknown>, message: string): Promise<void> {
39+
await expect(promise).rejects.toBeInstanceOf(Error);
40+
await expect(promise).rejects.toThrow(message);
41+
}
42+
43+
function expectThrowsWithError(fn: () => unknown, message: string): void {
44+
expect(fn).toThrow(Error);
45+
expect(fn).toThrow(message);
46+
}
47+
3648
it('sends feedback with minimal options', async () => {
3749
mockSdk();
3850
const mockTransport = vi.spyOn(getClient()!.getTransport()!, 'send');
@@ -269,7 +281,7 @@ describe('sendFeedback', () => {
269281

270282
it('throws when message is empty', () => {
271283
mockSdk();
272-
expect(() => sendFeedback({ message: '' })).toThrow('Unable to submit feedback with empty message');
284+
expectThrowsWithError(() => sendFeedback({ message: '' }), 'Unable to submit feedback with empty message');
273285
});
274286

275287
it('throws when no client is set up', async () => {
@@ -279,7 +291,7 @@ describe('sendFeedback', () => {
279291
getGlobalScope().setClient(undefined);
280292
getCurrentScope().setClient(undefined);
281293
getIsolationScope().setClient(undefined);
282-
expect(() => sendFeedback({ message: 'mi' })).toThrow('No client setup, cannot send feedback.');
294+
expectThrowsWithError(() => sendFeedback({ message: 'mi' }), 'No client setup, cannot send feedback.');
283295
});
284296

285297
it('uses provided errorMessages overrides', async () => {
@@ -288,9 +300,10 @@ describe('sendFeedback', () => {
288300
return Promise.resolve({ statusCode: 403 });
289301
});
290302

291-
await expect(
303+
await expectRejectsWithError(
292304
sendFeedback({ message: 'mi' }, { errorMessages: { ERROR_FORBIDDEN: 'custom forbidden text' } }),
293-
).rejects.toMatch('custom forbidden text');
305+
'custom forbidden text',
306+
);
294307
});
295308

296309
it('falls back to default messages for codes not in errorMessages', async () => {
@@ -300,9 +313,8 @@ describe('sendFeedback', () => {
300313
});
301314

302315
// Only override ERROR_FORBIDDEN — a 400 should still use the default generic message.
303-
await expect(
316+
await expectRejectsWithError(
304317
sendFeedback({ message: 'mi' }, { errorMessages: { ERROR_FORBIDDEN: 'custom forbidden text' } }),
305-
).rejects.toMatch(
306318
'Unable to send feedback. This could be because of network issues, or because you are using an ad-blocker.',
307319
);
308320
});
@@ -313,13 +325,12 @@ describe('sendFeedback', () => {
313325
return Promise.resolve({ statusCode: 400 });
314326
});
315327

316-
await expect(
328+
await expectRejectsWithError(
317329
sendFeedback({
318330
name: 'doe',
319331
email: 're@example.org',
320332
message: 'mi',
321333
}),
322-
).rejects.toMatch(
323334
'Unable to send feedback. This could be because of network issues, or because you are using an ad-blocker.',
324335
);
325336
});
@@ -330,13 +341,12 @@ describe('sendFeedback', () => {
330341
return Promise.resolve({ statusCode: 0 });
331342
});
332343

333-
await expect(
344+
await expectRejectsWithError(
334345
sendFeedback({
335346
name: 'doe',
336347
email: 're@example.org',
337348
message: 'mi',
338349
}),
339-
).rejects.toMatch(
340350
'Unable to send feedback. This could be because of network issues, or because you are using an ad-blocker.',
341351
);
342352
});
@@ -347,13 +357,12 @@ describe('sendFeedback', () => {
347357
return Promise.resolve({ statusCode: 403 });
348358
});
349359

350-
await expect(
360+
await expectRejectsWithError(
351361
sendFeedback({
352362
name: 'doe',
353363
email: 're@example.org',
354364
message: 'mi',
355365
}),
356-
).rejects.toMatch(
357366
'Unable to send feedback. This could be because this domain is not in your list of allowed domains.',
358367
);
359368
});
@@ -389,7 +398,7 @@ describe('sendFeedback', () => {
389398

390399
vi.advanceTimersByTime(30_000);
391400

392-
await expect(promise).rejects.toMatch('Unable to determine if Feedback was correctly sent.');
401+
await expectRejectsWithError(promise, 'Unable to determine if Feedback was correctly sent.');
393402

394403
vi.useRealTimers();
395404
});

0 commit comments

Comments
 (0)