Skip to content

Commit 70d2ab4

Browse files
logaretmclaude
andcommitted
feat(feedback): allow error messages to be customized
Adds five new text options (errorEmptyMessageText, errorNoClientText, errorTimeoutText, errorForbiddenText, errorGenericText) to FeedbackTextConfiguration so consumers can translate or reword the widget's error messages. sendFeedback now throws/rejects with stable codes (typed via FeedbackErrorCode) and the widget maps those codes to the configured text. Closes #14687 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 5ee2da4 commit 70d2ab4

7 files changed

Lines changed: 87 additions & 21 deletions

File tree

packages/core/src/types-hoist/feedback/config.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,31 @@ export interface FeedbackTextConfiguration {
191191
* The label for the button that removed a highlight/hidden section of the screenshot.
192192
*/
193193
removeHighlightText: string;
194+
195+
/**
196+
* Error text shown when feedback submission is attempted with an empty message
197+
*/
198+
errorEmptyMessageText: string;
199+
200+
/**
201+
* Error text shown when the Sentry client is not set up
202+
*/
203+
errorNoClientText: string;
204+
205+
/**
206+
* Error text shown when the feedback submission times out (after 30s)
207+
*/
208+
errorTimeoutText: string;
209+
210+
/**
211+
* Error text shown when the feedback submission is blocked because the domain is not allowed (HTTP 403)
212+
*/
213+
errorForbiddenText: string;
214+
215+
/**
216+
* Error text shown when the feedback submission fails for any other reason (e.g. network error, ad-blocker)
217+
*/
218+
errorGenericText: string;
194219
}
195220

196221
/**

packages/feedback/src/constants/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,14 @@ export const HIGHLIGHT_TOOL_TEXT = 'Highlight';
2626
export const HIDE_TOOL_TEXT = 'Hide';
2727
export const REMOVE_HIGHLIGHT_TEXT = 'Remove';
2828

29+
export const ERROR_EMPTY_MESSAGE_TEXT = 'Unable to submit feedback with empty message';
30+
export const ERROR_NO_CLIENT_TEXT = 'No client setup, cannot send feedback.';
31+
export const ERROR_TIMEOUT_TEXT = 'Unable to determine if Feedback was correctly sent.';
32+
export const ERROR_FORBIDDEN_TEXT =
33+
'Unable to send feedback. This could be because this domain is not in your list of allowed domains.';
34+
export const ERROR_GENERIC_TEXT =
35+
'Unable to send feedback. This could be because of network issues, or because you are using an ad-blocker.';
36+
2937
export const FEEDBACK_WIDGET_SOURCE = 'widget';
3038
export const FEEDBACK_API_SOURCE = 'api';
3139

packages/feedback/src/core/integration.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
/* eslint-disable max-lines */
2+
/* eslint-disable complexity */
23

34
import type {
45
FeedbackInternalOptions,
@@ -15,6 +16,11 @@ import {
1516
DOCUMENT,
1617
EMAIL_LABEL,
1718
EMAIL_PLACEHOLDER,
19+
ERROR_EMPTY_MESSAGE_TEXT,
20+
ERROR_FORBIDDEN_TEXT,
21+
ERROR_GENERIC_TEXT,
22+
ERROR_NO_CLIENT_TEXT,
23+
ERROR_TIMEOUT_TEXT,
1824
FORM_TITLE,
1925
HIDE_TOOL_TEXT,
2026
HIGHLIGHT_TOOL_TEXT,
@@ -119,6 +125,11 @@ export const buildFeedbackIntegration = ({
119125
highlightToolText = HIGHLIGHT_TOOL_TEXT,
120126
hideToolText = HIDE_TOOL_TEXT,
121127
removeHighlightText = REMOVE_HIGHLIGHT_TEXT,
128+
errorEmptyMessageText = ERROR_EMPTY_MESSAGE_TEXT,
129+
errorNoClientText = ERROR_NO_CLIENT_TEXT,
130+
errorTimeoutText = ERROR_TIMEOUT_TEXT,
131+
errorForbiddenText = ERROR_FORBIDDEN_TEXT,
132+
errorGenericText = ERROR_GENERIC_TEXT,
122133

123134
// FeedbackCallbacks
124135
onFormOpen,
@@ -164,6 +175,11 @@ export const buildFeedbackIntegration = ({
164175
highlightToolText,
165176
hideToolText,
166177
removeHighlightText,
178+
errorEmptyMessageText,
179+
errorNoClientText,
180+
errorTimeoutText,
181+
errorForbiddenText,
182+
errorGenericText,
167183

168184
onFormClose,
169185
onFormOpen,

packages/feedback/src/core/sendFeedback.ts

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import type { Event, EventHint, SendFeedback, SendFeedbackParams, TransportMakeRequestResponse } from '@sentry/core';
22
import { captureFeedback, getClient, getCurrentScope, getLocationHref } from '@sentry/core';
33
import { FEEDBACK_API_SOURCE } from '../constants';
4+
import type { FeedbackErrorCode } from '../util/createFeedbackError';
5+
import { createFeedbackError } from '../util/createFeedbackError';
46

57
/**
68
* Public API to send a Feedback item to Sentry
@@ -10,14 +12,14 @@ export const sendFeedback: SendFeedback = (
1012
hint: EventHint & { includeReplay?: boolean } = { includeReplay: true },
1113
): Promise<string> => {
1214
if (!params.message) {
13-
throw new Error('Unable to submit feedback with empty message');
15+
throw createFeedbackError('ERROR_EMPTY_MESSAGE');
1416
}
1517

1618
// We want to wait for the feedback to be sent (or not)
1719
const client = getClient();
1820

1921
if (!client) {
20-
throw new Error('No client setup, cannot send feedback.');
22+
throw createFeedbackError('ERROR_NO_CLIENT');
2123
}
2224

2325
if (params.tags && Object.keys(params.tags).length) {
@@ -35,7 +37,7 @@ export const sendFeedback: SendFeedback = (
3537
// We want to wait for the feedback to be sent (or not)
3638
return new Promise<string>((resolve, reject) => {
3739
// After 30s, we want to clear anyhow
38-
const timeout = setTimeout(() => reject('Unable to determine if Feedback was correctly sent.'), 30_000);
40+
const timeout = setTimeout(() => reject('ERROR_TIMEOUT' satisfies FeedbackErrorCode), 30_000);
3941

4042
const cleanup = client.on('afterSendEvent', (event: Event, response: TransportMakeRequestResponse) => {
4143
if (event.event_id !== eventId) {
@@ -51,14 +53,10 @@ export const sendFeedback: SendFeedback = (
5153
}
5254

5355
if (response?.statusCode === 403) {
54-
return reject(
55-
'Unable to send feedback. This could be because this domain is not in your list of allowed domains.',
56-
);
56+
return reject('ERROR_FORBIDDEN' satisfies FeedbackErrorCode);
5757
}
5858

59-
return reject(
60-
'Unable to send feedback. This could be because of network issues, or because you are using an ad-blocker.',
61-
);
59+
return reject('ERROR_GENERIC' satisfies FeedbackErrorCode);
6260
});
6361
});
6462
};

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

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type { JSX, VNode } from 'preact';
99
import { h } from 'preact'; // eslint-disable-line @typescript-eslint/no-unused-vars
1010
import { useCallback, useState } from 'preact/hooks';
1111
import { FEEDBACK_WIDGET_SOURCE } from '../../constants';
12+
import type { FeedbackErrorCode } from '../../util/createFeedbackError';
1213
import { DEBUG_BUILD } from '../../util/debug-build';
1314
import { getMissingFields } from '../../util/validate';
1415

@@ -59,7 +60,20 @@ export function Form({
5960
namePlaceholder,
6061
submitButtonLabel,
6162
isRequiredLabel,
63+
errorEmptyMessageText,
64+
errorNoClientText,
65+
errorTimeoutText,
66+
errorForbiddenText,
67+
errorGenericText,
6268
} = options;
69+
70+
const errorTextByCode: Record<FeedbackErrorCode, string> = {
71+
ERROR_EMPTY_MESSAGE: errorEmptyMessageText,
72+
ERROR_NO_CLIENT: errorNoClientText,
73+
ERROR_TIMEOUT: errorTimeoutText,
74+
ERROR_FORBIDDEN: errorForbiddenText,
75+
ERROR_GENERIC: errorGenericText,
76+
};
6377
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
6478
// TODO: set a ref on the form, and whenever an input changes call processForm() and setError()
6579
const [error, setError] = useState<null | string>(null);
@@ -131,8 +145,9 @@ export function Form({
131145
onSubmitSuccess(data, eventId);
132146
} catch (error) {
133147
DEBUG_BUILD && debug.error(error);
134-
setError(error as string);
135-
onSubmitError(error as Error);
148+
const err = error instanceof Error ? error : new Error(String(error));
149+
setError(errorTextByCode[err.message as FeedbackErrorCode] || errorGenericText);
150+
onSubmitError(err);
136151
}
137152
} finally {
138153
setIsSubmitting(false);
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export type FeedbackErrorCode =
2+
| 'ERROR_EMPTY_MESSAGE'
3+
| 'ERROR_NO_CLIENT'
4+
| 'ERROR_TIMEOUT'
5+
| 'ERROR_FORBIDDEN'
6+
| 'ERROR_GENERIC';
7+
8+
export function createFeedbackError(reason: FeedbackErrorCode): Error {
9+
return new Error(reason);
10+
}

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

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -279,9 +279,7 @@ describe('sendFeedback', () => {
279279
email: 're@example.org',
280280
message: 'mi',
281281
}),
282-
).rejects.toMatch(
283-
'Unable to send feedback. This could be because of network issues, or because you are using an ad-blocker.',
284-
);
282+
).rejects.toMatch('ERROR_GENERIC');
285283
});
286284

287285
it('handles 0 transport error', async () => {
@@ -296,9 +294,7 @@ describe('sendFeedback', () => {
296294
email: 're@example.org',
297295
message: 'mi',
298296
}),
299-
).rejects.toMatch(
300-
'Unable to send feedback. This could be because of network issues, or because you are using an ad-blocker.',
301-
);
297+
).rejects.toMatch('ERROR_GENERIC');
302298
});
303299

304300
it('handles 403 transport error', async () => {
@@ -313,9 +309,7 @@ describe('sendFeedback', () => {
313309
email: 're@example.org',
314310
message: 'mi',
315311
}),
316-
).rejects.toMatch(
317-
'Unable to send feedback. This could be because this domain is not in your list of allowed domains.',
318-
);
312+
).rejects.toMatch('ERROR_FORBIDDEN');
319313
});
320314

321315
it('handles 200 transport response', async () => {
@@ -349,7 +343,7 @@ describe('sendFeedback', () => {
349343

350344
vi.advanceTimersByTime(30_000);
351345

352-
await expect(promise).rejects.toMatch('Unable to determine if Feedback was correctly sent.');
346+
await expect(promise).rejects.toMatch('ERROR_TIMEOUT');
353347

354348
vi.useRealTimers();
355349
});

0 commit comments

Comments
 (0)