From 4482787affb4d2ca3e263e41c08d151a5ca24e0b Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Thu, 23 Apr 2026 22:42:35 -0400 Subject: [PATCH 1/6] 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) --- .../core/src/types-hoist/feedback/config.ts | 25 +++++++++++++++++++ packages/feedback/src/constants/index.ts | 8 ++++++ packages/feedback/src/core/integration.ts | 16 ++++++++++++ packages/feedback/src/core/sendFeedback.ts | 16 ++++++------ .../feedback/src/modal/components/Form.tsx | 19 ++++++++++++-- .../feedback/src/util/createFeedbackError.ts | 10 ++++++++ .../feedback/test/core/sendFeedback.test.ts | 14 +++-------- 7 files changed, 87 insertions(+), 21 deletions(-) create mode 100644 packages/feedback/src/util/createFeedbackError.ts diff --git a/packages/core/src/types-hoist/feedback/config.ts b/packages/core/src/types-hoist/feedback/config.ts index f6a90c7c5b73..5d7db7636996 100644 --- a/packages/core/src/types-hoist/feedback/config.ts +++ b/packages/core/src/types-hoist/feedback/config.ts @@ -191,6 +191,31 @@ export interface FeedbackTextConfiguration { * The label for the button that removed a highlight/hidden section of the screenshot. */ removeHighlightText: string; + + /** + * Error text shown when feedback submission is attempted with an empty message + */ + errorEmptyMessageText: string; + + /** + * Error text shown when the Sentry client is not set up + */ + errorNoClientText: string; + + /** + * Error text shown when the feedback submission times out (after 30s) + */ + errorTimeoutText: string; + + /** + * Error text shown when the feedback submission is blocked because the domain is not allowed (HTTP 403) + */ + errorForbiddenText: string; + + /** + * Error text shown when the feedback submission fails for any other reason (e.g. network error, ad-blocker) + */ + errorGenericText: string; } /** diff --git a/packages/feedback/src/constants/index.ts b/packages/feedback/src/constants/index.ts index d18392258417..309bba7490ab 100644 --- a/packages/feedback/src/constants/index.ts +++ b/packages/feedback/src/constants/index.ts @@ -26,6 +26,14 @@ export const HIGHLIGHT_TOOL_TEXT = 'Highlight'; export const HIDE_TOOL_TEXT = 'Hide'; export const REMOVE_HIGHLIGHT_TEXT = 'Remove'; +export const ERROR_EMPTY_MESSAGE_TEXT = 'Unable to submit feedback with empty message'; +export const ERROR_NO_CLIENT_TEXT = 'No client setup, cannot send feedback.'; +export const ERROR_TIMEOUT_TEXT = 'Unable to determine if Feedback was correctly sent.'; +export const ERROR_FORBIDDEN_TEXT = + 'Unable to send feedback. This could be because this domain is not in your list of allowed domains.'; +export const ERROR_GENERIC_TEXT = + 'Unable to send feedback. This could be because of network issues, or because you are using an ad-blocker.'; + export const FEEDBACK_WIDGET_SOURCE = 'widget'; export const FEEDBACK_API_SOURCE = 'api'; diff --git a/packages/feedback/src/core/integration.ts b/packages/feedback/src/core/integration.ts index 1dc418ed131f..a57be211af69 100644 --- a/packages/feedback/src/core/integration.ts +++ b/packages/feedback/src/core/integration.ts @@ -1,4 +1,5 @@ /* eslint-disable max-lines */ +/* eslint-disable complexity */ import type { FeedbackInternalOptions, @@ -15,6 +16,11 @@ import { DOCUMENT, EMAIL_LABEL, EMAIL_PLACEHOLDER, + ERROR_EMPTY_MESSAGE_TEXT, + ERROR_FORBIDDEN_TEXT, + ERROR_GENERIC_TEXT, + ERROR_NO_CLIENT_TEXT, + ERROR_TIMEOUT_TEXT, FORM_TITLE, HIDE_TOOL_TEXT, HIGHLIGHT_TOOL_TEXT, @@ -119,6 +125,11 @@ export const buildFeedbackIntegration = ({ highlightToolText = HIGHLIGHT_TOOL_TEXT, hideToolText = HIDE_TOOL_TEXT, removeHighlightText = REMOVE_HIGHLIGHT_TEXT, + errorEmptyMessageText = ERROR_EMPTY_MESSAGE_TEXT, + errorNoClientText = ERROR_NO_CLIENT_TEXT, + errorTimeoutText = ERROR_TIMEOUT_TEXT, + errorForbiddenText = ERROR_FORBIDDEN_TEXT, + errorGenericText = ERROR_GENERIC_TEXT, // FeedbackCallbacks onFormOpen, @@ -164,6 +175,11 @@ export const buildFeedbackIntegration = ({ highlightToolText, hideToolText, removeHighlightText, + errorEmptyMessageText, + errorNoClientText, + errorTimeoutText, + errorForbiddenText, + errorGenericText, onFormClose, onFormOpen, diff --git a/packages/feedback/src/core/sendFeedback.ts b/packages/feedback/src/core/sendFeedback.ts index 712da5c269bf..d33748e237ae 100644 --- a/packages/feedback/src/core/sendFeedback.ts +++ b/packages/feedback/src/core/sendFeedback.ts @@ -1,6 +1,8 @@ import type { Event, EventHint, SendFeedback, SendFeedbackParams, TransportMakeRequestResponse } from '@sentry/core'; import { captureFeedback, getClient, getCurrentScope, getLocationHref } from '@sentry/core'; import { FEEDBACK_API_SOURCE } from '../constants'; +import type { FeedbackErrorCode } from '../util/createFeedbackError'; +import { createFeedbackError } from '../util/createFeedbackError'; /** * Public API to send a Feedback item to Sentry @@ -10,14 +12,14 @@ export const sendFeedback: SendFeedback = ( hint: EventHint & { includeReplay?: boolean } = { includeReplay: true }, ): Promise => { if (!params.message) { - throw new Error('Unable to submit feedback with empty message'); + throw createFeedbackError('ERROR_EMPTY_MESSAGE'); } // We want to wait for the feedback to be sent (or not) const client = getClient(); if (!client) { - throw new Error('No client setup, cannot send feedback.'); + throw createFeedbackError('ERROR_NO_CLIENT'); } if (params.tags && Object.keys(params.tags).length) { @@ -35,7 +37,7 @@ export const sendFeedback: SendFeedback = ( // We want to wait for the feedback to be sent (or not) return new Promise((resolve, reject) => { // After 30s, we want to clear anyhow - const timeout = setTimeout(() => reject('Unable to determine if Feedback was correctly sent.'), 30_000); + const timeout = setTimeout(() => reject('ERROR_TIMEOUT' satisfies FeedbackErrorCode), 30_000); const cleanup = client.on('afterSendEvent', (event: Event, response: TransportMakeRequestResponse) => { if (event.event_id !== eventId) { @@ -51,14 +53,10 @@ export const sendFeedback: SendFeedback = ( } if (response?.statusCode === 403) { - return reject( - 'Unable to send feedback. This could be because this domain is not in your list of allowed domains.', - ); + return reject('ERROR_FORBIDDEN' satisfies FeedbackErrorCode); } - return reject( - 'Unable to send feedback. This could be because of network issues, or because you are using an ad-blocker.', - ); + return reject('ERROR_GENERIC' satisfies FeedbackErrorCode); }); }); }; diff --git a/packages/feedback/src/modal/components/Form.tsx b/packages/feedback/src/modal/components/Form.tsx index 0f09e969fee5..011d19dca4ed 100644 --- a/packages/feedback/src/modal/components/Form.tsx +++ b/packages/feedback/src/modal/components/Form.tsx @@ -9,6 +9,7 @@ import type { JSX, VNode } from 'preact'; import { h } from 'preact'; // eslint-disable-line @typescript-eslint/no-unused-vars import { useCallback, useState } from 'preact/hooks'; import { FEEDBACK_WIDGET_SOURCE } from '../../constants'; +import type { FeedbackErrorCode } from '../../util/createFeedbackError'; import { DEBUG_BUILD } from '../../util/debug-build'; import { getMissingFields } from '../../util/validate'; @@ -59,7 +60,20 @@ export function Form({ namePlaceholder, submitButtonLabel, isRequiredLabel, + errorEmptyMessageText, + errorNoClientText, + errorTimeoutText, + errorForbiddenText, + errorGenericText, } = options; + + const errorTextByCode: Record = { + ERROR_EMPTY_MESSAGE: errorEmptyMessageText, + ERROR_NO_CLIENT: errorNoClientText, + ERROR_TIMEOUT: errorTimeoutText, + ERROR_FORBIDDEN: errorForbiddenText, + ERROR_GENERIC: errorGenericText, + }; const [isSubmitting, setIsSubmitting] = useState(false); // TODO: set a ref on the form, and whenever an input changes call processForm() and setError() const [error, setError] = useState(null); @@ -131,8 +145,9 @@ export function Form({ onSubmitSuccess(data, eventId); } catch (error) { DEBUG_BUILD && debug.error(error); - setError(error as string); - onSubmitError(error as Error); + const err = error instanceof Error ? error : new Error(String(error)); + setError(errorTextByCode[err.message as FeedbackErrorCode] || errorGenericText); + onSubmitError(err); } } finally { setIsSubmitting(false); diff --git a/packages/feedback/src/util/createFeedbackError.ts b/packages/feedback/src/util/createFeedbackError.ts new file mode 100644 index 000000000000..51024126a822 --- /dev/null +++ b/packages/feedback/src/util/createFeedbackError.ts @@ -0,0 +1,10 @@ +export type FeedbackErrorCode = + | 'ERROR_EMPTY_MESSAGE' + | 'ERROR_NO_CLIENT' + | 'ERROR_TIMEOUT' + | 'ERROR_FORBIDDEN' + | 'ERROR_GENERIC'; + +export function createFeedbackError(reason: FeedbackErrorCode): Error { + return new Error(reason); +} diff --git a/packages/feedback/test/core/sendFeedback.test.ts b/packages/feedback/test/core/sendFeedback.test.ts index a0cbb084da59..b94d787e8eaf 100644 --- a/packages/feedback/test/core/sendFeedback.test.ts +++ b/packages/feedback/test/core/sendFeedback.test.ts @@ -279,9 +279,7 @@ describe('sendFeedback', () => { email: 're@example.org', message: 'mi', }), - ).rejects.toMatch( - 'Unable to send feedback. This could be because of network issues, or because you are using an ad-blocker.', - ); + ).rejects.toMatch('ERROR_GENERIC'); }); it('handles 0 transport error', async () => { @@ -296,9 +294,7 @@ describe('sendFeedback', () => { email: 're@example.org', message: 'mi', }), - ).rejects.toMatch( - 'Unable to send feedback. This could be because of network issues, or because you are using an ad-blocker.', - ); + ).rejects.toMatch('ERROR_GENERIC'); }); it('handles 403 transport error', async () => { @@ -313,9 +309,7 @@ describe('sendFeedback', () => { email: 're@example.org', message: 'mi', }), - ).rejects.toMatch( - 'Unable to send feedback. This could be because this domain is not in your list of allowed domains.', - ); + ).rejects.toMatch('ERROR_FORBIDDEN'); }); it('handles 200 transport response', async () => { @@ -349,7 +343,7 @@ describe('sendFeedback', () => { vi.advanceTimersByTime(30_000); - await expect(promise).rejects.toMatch('Unable to determine if Feedback was correctly sent.'); + await expect(promise).rejects.toMatch('ERROR_TIMEOUT'); vi.useRealTimers(); }); From d7b27f60a6785b89d36ba7b422e2fd036d8f472f Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Thu, 23 Apr 2026 23:13:26 -0400 Subject: [PATCH 2/6] address PR review feedback - Form: use ?? so empty-string text overrides are respected (Copilot) - constants: grammar fix "with an empty message" (Copilot) - sendFeedback: clean up afterSendEvent listener on timeout to avoid leak (Copilot) - tests: cover empty-message and no-client early-return paths (Copilot) Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/feedback/src/constants/index.ts | 2 +- packages/feedback/src/core/sendFeedback.ts | 5 ++++- packages/feedback/src/modal/components/Form.tsx | 2 +- packages/feedback/test/core/sendFeedback.test.ts | 15 +++++++++++++++ 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/packages/feedback/src/constants/index.ts b/packages/feedback/src/constants/index.ts index 309bba7490ab..cbfafc4f36a6 100644 --- a/packages/feedback/src/constants/index.ts +++ b/packages/feedback/src/constants/index.ts @@ -26,7 +26,7 @@ export const HIGHLIGHT_TOOL_TEXT = 'Highlight'; export const HIDE_TOOL_TEXT = 'Hide'; export const REMOVE_HIGHLIGHT_TEXT = 'Remove'; -export const ERROR_EMPTY_MESSAGE_TEXT = 'Unable to submit feedback with empty message'; +export const ERROR_EMPTY_MESSAGE_TEXT = 'Unable to submit feedback with an empty message'; export const ERROR_NO_CLIENT_TEXT = 'No client setup, cannot send feedback.'; export const ERROR_TIMEOUT_TEXT = 'Unable to determine if Feedback was correctly sent.'; export const ERROR_FORBIDDEN_TEXT = diff --git a/packages/feedback/src/core/sendFeedback.ts b/packages/feedback/src/core/sendFeedback.ts index d33748e237ae..03f32ad651fe 100644 --- a/packages/feedback/src/core/sendFeedback.ts +++ b/packages/feedback/src/core/sendFeedback.ts @@ -37,7 +37,10 @@ export const sendFeedback: SendFeedback = ( // We want to wait for the feedback to be sent (or not) return new Promise((resolve, reject) => { // After 30s, we want to clear anyhow - const timeout = setTimeout(() => reject('ERROR_TIMEOUT' satisfies FeedbackErrorCode), 30_000); + const timeout = setTimeout(() => { + cleanup(); + reject('ERROR_TIMEOUT' satisfies FeedbackErrorCode); + }, 30_000); const cleanup = client.on('afterSendEvent', (event: Event, response: TransportMakeRequestResponse) => { if (event.event_id !== eventId) { diff --git a/packages/feedback/src/modal/components/Form.tsx b/packages/feedback/src/modal/components/Form.tsx index 011d19dca4ed..7627164c3cea 100644 --- a/packages/feedback/src/modal/components/Form.tsx +++ b/packages/feedback/src/modal/components/Form.tsx @@ -146,7 +146,7 @@ export function Form({ } catch (error) { DEBUG_BUILD && debug.error(error); const err = error instanceof Error ? error : new Error(String(error)); - setError(errorTextByCode[err.message as FeedbackErrorCode] || errorGenericText); + setError(errorTextByCode[err.message as FeedbackErrorCode] ?? errorGenericText); onSubmitError(err); } } finally { diff --git a/packages/feedback/test/core/sendFeedback.test.ts b/packages/feedback/test/core/sendFeedback.test.ts index b94d787e8eaf..be7d686ab422 100644 --- a/packages/feedback/test/core/sendFeedback.test.ts +++ b/packages/feedback/test/core/sendFeedback.test.ts @@ -267,6 +267,21 @@ describe('sendFeedback', () => { ]); }); + it('throws when message is empty', () => { + mockSdk(); + expect(() => sendFeedback({ message: '' })).toThrow('ERROR_EMPTY_MESSAGE'); + }); + + it('throws when no client is set up', async () => { + // Isolate in its own scope so the client set up by other tests doesn't bleed in. + // `getClient` reads from the current scope; resetting it here leaves no client. + const { getGlobalScope } = await import('@sentry/core'); + getGlobalScope().setClient(undefined); + getCurrentScope().setClient(undefined); + getIsolationScope().setClient(undefined); + expect(() => sendFeedback({ message: 'mi' })).toThrow('ERROR_NO_CLIENT'); + }); + it('handles 400 transport error', async () => { mockSdk(); vi.spyOn(getClient()!.getTransport()!, 'send').mockImplementation(() => { From 688409edb20e984f20c0d946e2eddc167755094f Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Thu, 23 Apr 2026 23:20:54 -0400 Subject: [PATCH 3/6] refactor: inject error texts via sendFeedback hint, preserve default messages Addresses review feedback that sendFeedback (a public API) was rejecting with internal codes instead of human-readable strings, changing observable behavior for non-widget consumers. sendFeedback now accepts optional errorMessages overrides via its hint argument, defaulting to the original English strings. The widget wraps sendFeedback to inject its configured text options, so standalone consumers of sendFeedback see the same strings as before. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/core/src/index.ts | 2 ++ .../core/src/types-hoist/feedback/index.ts | 11 +++++-- .../src/types-hoist/feedback/sendFeedback.ts | 11 ++++++- packages/feedback/src/core/integration.ts | 14 ++++++++- packages/feedback/src/core/sendFeedback.ts | 26 ++++++++++------ .../feedback/src/modal/components/Form.tsx | 16 +--------- .../feedback/src/util/createFeedbackError.ts | 30 ++++++++++++++----- .../feedback/test/core/sendFeedback.test.ts | 29 ++++++++++++++---- 8 files changed, 97 insertions(+), 42 deletions(-) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 7f112bd4c95b..72fb619ca4f2 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -453,6 +453,8 @@ export type { ReplayStopReason, } from './types-hoist/replay'; export type { + FeedbackErrorCode, + FeedbackErrorMessages, FeedbackEvent, FeedbackFormData, FeedbackInternalOptions, diff --git a/packages/core/src/types-hoist/feedback/index.ts b/packages/core/src/types-hoist/feedback/index.ts index 239a44d82543..fd44615e878c 100644 --- a/packages/core/src/types-hoist/feedback/index.ts +++ b/packages/core/src/types-hoist/feedback/index.ts @@ -6,10 +6,17 @@ import type { FeedbackTextConfiguration, FeedbackThemeConfiguration, } from './config'; -import type { FeedbackEvent, SendFeedback, SendFeedbackParams, UserFeedback } from './sendFeedback'; +import type { + FeedbackErrorCode, + FeedbackErrorMessages, + FeedbackEvent, + SendFeedback, + SendFeedbackParams, + UserFeedback, +} from './sendFeedback'; export type { FeedbackFormData } from './form'; -export type { FeedbackEvent, UserFeedback, SendFeedback, SendFeedbackParams }; +export type { FeedbackErrorCode, FeedbackErrorMessages, FeedbackEvent, SendFeedback, SendFeedbackParams, UserFeedback }; /** * The integration's internal `options` member where every value should be set diff --git a/packages/core/src/types-hoist/feedback/sendFeedback.ts b/packages/core/src/types-hoist/feedback/sendFeedback.ts index 63d63b402b50..f1d0fd5d0f3e 100644 --- a/packages/core/src/types-hoist/feedback/sendFeedback.ts +++ b/packages/core/src/types-hoist/feedback/sendFeedback.ts @@ -47,7 +47,16 @@ export interface SendFeedbackParams { tags?: { [key: string]: Primitive }; } +export type FeedbackErrorCode = + | 'ERROR_EMPTY_MESSAGE' + | 'ERROR_NO_CLIENT' + | 'ERROR_TIMEOUT' + | 'ERROR_FORBIDDEN' + | 'ERROR_GENERIC'; + +export type FeedbackErrorMessages = Partial>; + export type SendFeedback = ( params: SendFeedbackParams, - hint?: EventHint & { includeReplay?: boolean }, + hint?: EventHint & { includeReplay?: boolean; errorMessages?: FeedbackErrorMessages }, ) => Promise; diff --git a/packages/feedback/src/core/integration.ts b/packages/feedback/src/core/integration.ts index a57be211af69..a2f94a078976 100644 --- a/packages/feedback/src/core/integration.ts +++ b/packages/feedback/src/core/integration.ts @@ -2,11 +2,13 @@ /* eslint-disable complexity */ import type { + FeedbackErrorMessages, FeedbackInternalOptions, FeedbackModalIntegration, FeedbackScreenshotIntegration, Integration, IntegrationFn, + SendFeedback, } from '@sentry/core'; import { addIntegration, debug, isBrowser } from '@sentry/core'; import { @@ -246,6 +248,16 @@ export const buildFeedbackIntegration = ({ debug.error('[Feedback] Missing feedback screenshot integration. Proceeding without screenshots.'); } + const errorMessages: FeedbackErrorMessages = { + ERROR_EMPTY_MESSAGE: options.errorEmptyMessageText, + ERROR_NO_CLIENT: options.errorNoClientText, + ERROR_TIMEOUT: options.errorTimeoutText, + ERROR_FORBIDDEN: options.errorForbiddenText, + ERROR_GENERIC: options.errorGenericText, + }; + const wrappedSendFeedback: SendFeedback = (params, hint) => + sendFeedback(params, { includeReplay: true, ...hint, errorMessages }); + const dialog = modalIntegration.createDialog({ options: { ...options, @@ -259,7 +271,7 @@ export const buildFeedbackIntegration = ({ }, }, screenshotIntegration, - sendFeedback, + sendFeedback: wrappedSendFeedback, shadow: _createShadow(options), }); diff --git a/packages/feedback/src/core/sendFeedback.ts b/packages/feedback/src/core/sendFeedback.ts index 03f32ad651fe..204cf1cfa6c7 100644 --- a/packages/feedback/src/core/sendFeedback.ts +++ b/packages/feedback/src/core/sendFeedback.ts @@ -1,25 +1,33 @@ -import type { Event, EventHint, SendFeedback, SendFeedbackParams, TransportMakeRequestResponse } from '@sentry/core'; +import type { + Event, + EventHint, + FeedbackErrorMessages, + SendFeedback, + SendFeedbackParams, + TransportMakeRequestResponse, +} from '@sentry/core'; import { captureFeedback, getClient, getCurrentScope, getLocationHref } from '@sentry/core'; import { FEEDBACK_API_SOURCE } from '../constants'; -import type { FeedbackErrorCode } from '../util/createFeedbackError'; -import { createFeedbackError } from '../util/createFeedbackError'; +import { createFeedbackError, resolveFeedbackErrorMessage } from '../util/createFeedbackError'; /** * Public API to send a Feedback item to Sentry */ export const sendFeedback: SendFeedback = ( params: SendFeedbackParams, - hint: EventHint & { includeReplay?: boolean } = { includeReplay: true }, + hint: EventHint & { includeReplay?: boolean; errorMessages?: FeedbackErrorMessages } = { includeReplay: true }, ): Promise => { + const errorMessages = hint.errorMessages; + if (!params.message) { - throw createFeedbackError('ERROR_EMPTY_MESSAGE'); + throw createFeedbackError('ERROR_EMPTY_MESSAGE', errorMessages); } // We want to wait for the feedback to be sent (or not) const client = getClient(); if (!client) { - throw createFeedbackError('ERROR_NO_CLIENT'); + throw createFeedbackError('ERROR_NO_CLIENT', errorMessages); } if (params.tags && Object.keys(params.tags).length) { @@ -39,7 +47,7 @@ export const sendFeedback: SendFeedback = ( // After 30s, we want to clear anyhow const timeout = setTimeout(() => { cleanup(); - reject('ERROR_TIMEOUT' satisfies FeedbackErrorCode); + reject(resolveFeedbackErrorMessage('ERROR_TIMEOUT', errorMessages)); }, 30_000); const cleanup = client.on('afterSendEvent', (event: Event, response: TransportMakeRequestResponse) => { @@ -56,10 +64,10 @@ export const sendFeedback: SendFeedback = ( } if (response?.statusCode === 403) { - return reject('ERROR_FORBIDDEN' satisfies FeedbackErrorCode); + return reject(resolveFeedbackErrorMessage('ERROR_FORBIDDEN', errorMessages)); } - return reject('ERROR_GENERIC' satisfies FeedbackErrorCode); + return reject(resolveFeedbackErrorMessage('ERROR_GENERIC', errorMessages)); }); }); }; diff --git a/packages/feedback/src/modal/components/Form.tsx b/packages/feedback/src/modal/components/Form.tsx index 7627164c3cea..ee4b06b25d88 100644 --- a/packages/feedback/src/modal/components/Form.tsx +++ b/packages/feedback/src/modal/components/Form.tsx @@ -9,7 +9,6 @@ import type { JSX, VNode } from 'preact'; import { h } from 'preact'; // eslint-disable-line @typescript-eslint/no-unused-vars import { useCallback, useState } from 'preact/hooks'; import { FEEDBACK_WIDGET_SOURCE } from '../../constants'; -import type { FeedbackErrorCode } from '../../util/createFeedbackError'; import { DEBUG_BUILD } from '../../util/debug-build'; import { getMissingFields } from '../../util/validate'; @@ -60,20 +59,7 @@ export function Form({ namePlaceholder, submitButtonLabel, isRequiredLabel, - errorEmptyMessageText, - errorNoClientText, - errorTimeoutText, - errorForbiddenText, - errorGenericText, } = options; - - const errorTextByCode: Record = { - ERROR_EMPTY_MESSAGE: errorEmptyMessageText, - ERROR_NO_CLIENT: errorNoClientText, - ERROR_TIMEOUT: errorTimeoutText, - ERROR_FORBIDDEN: errorForbiddenText, - ERROR_GENERIC: errorGenericText, - }; const [isSubmitting, setIsSubmitting] = useState(false); // TODO: set a ref on the form, and whenever an input changes call processForm() and setError() const [error, setError] = useState(null); @@ -146,7 +132,7 @@ export function Form({ } catch (error) { DEBUG_BUILD && debug.error(error); const err = error instanceof Error ? error : new Error(String(error)); - setError(errorTextByCode[err.message as FeedbackErrorCode] ?? errorGenericText); + setError(err.message); onSubmitError(err); } } finally { diff --git a/packages/feedback/src/util/createFeedbackError.ts b/packages/feedback/src/util/createFeedbackError.ts index 51024126a822..d7c2c4100f3e 100644 --- a/packages/feedback/src/util/createFeedbackError.ts +++ b/packages/feedback/src/util/createFeedbackError.ts @@ -1,10 +1,24 @@ -export type FeedbackErrorCode = - | 'ERROR_EMPTY_MESSAGE' - | 'ERROR_NO_CLIENT' - | 'ERROR_TIMEOUT' - | 'ERROR_FORBIDDEN' - | 'ERROR_GENERIC'; +import type { FeedbackErrorCode, FeedbackErrorMessages } from '@sentry/core'; +import { + ERROR_EMPTY_MESSAGE_TEXT, + ERROR_FORBIDDEN_TEXT, + ERROR_GENERIC_TEXT, + ERROR_NO_CLIENT_TEXT, + ERROR_TIMEOUT_TEXT, +} from '../constants'; -export function createFeedbackError(reason: FeedbackErrorCode): Error { - return new Error(reason); +const DEFAULT_MESSAGES: Record = { + ERROR_EMPTY_MESSAGE: ERROR_EMPTY_MESSAGE_TEXT, + ERROR_NO_CLIENT: ERROR_NO_CLIENT_TEXT, + ERROR_TIMEOUT: ERROR_TIMEOUT_TEXT, + ERROR_FORBIDDEN: ERROR_FORBIDDEN_TEXT, + ERROR_GENERIC: ERROR_GENERIC_TEXT, +}; + +export function resolveFeedbackErrorMessage(code: FeedbackErrorCode, messages?: FeedbackErrorMessages): string { + return messages?.[code] ?? DEFAULT_MESSAGES[code]; +} + +export function createFeedbackError(code: FeedbackErrorCode, messages?: FeedbackErrorMessages): Error { + return new Error(resolveFeedbackErrorMessage(code, messages)); } diff --git a/packages/feedback/test/core/sendFeedback.test.ts b/packages/feedback/test/core/sendFeedback.test.ts index be7d686ab422..15dcf37914d0 100644 --- a/packages/feedback/test/core/sendFeedback.test.ts +++ b/packages/feedback/test/core/sendFeedback.test.ts @@ -269,7 +269,7 @@ describe('sendFeedback', () => { it('throws when message is empty', () => { mockSdk(); - expect(() => sendFeedback({ message: '' })).toThrow('ERROR_EMPTY_MESSAGE'); + expect(() => sendFeedback({ message: '' })).toThrow('Unable to submit feedback with an empty message'); }); it('throws when no client is set up', async () => { @@ -279,7 +279,18 @@ describe('sendFeedback', () => { getGlobalScope().setClient(undefined); getCurrentScope().setClient(undefined); getIsolationScope().setClient(undefined); - expect(() => sendFeedback({ message: 'mi' })).toThrow('ERROR_NO_CLIENT'); + expect(() => sendFeedback({ message: 'mi' })).toThrow('No client setup, cannot send feedback.'); + }); + + it('uses provided errorMessages overrides', async () => { + mockSdk(); + vi.spyOn(getClient()!.getTransport()!, 'send').mockImplementation(() => { + return Promise.resolve({ statusCode: 403 }); + }); + + await expect( + sendFeedback({ message: 'mi' }, { errorMessages: { ERROR_FORBIDDEN: 'custom forbidden text' } }), + ).rejects.toMatch('custom forbidden text'); }); it('handles 400 transport error', async () => { @@ -294,7 +305,9 @@ describe('sendFeedback', () => { email: 're@example.org', message: 'mi', }), - ).rejects.toMatch('ERROR_GENERIC'); + ).rejects.toMatch( + 'Unable to send feedback. This could be because of network issues, or because you are using an ad-blocker.', + ); }); it('handles 0 transport error', async () => { @@ -309,7 +322,9 @@ describe('sendFeedback', () => { email: 're@example.org', message: 'mi', }), - ).rejects.toMatch('ERROR_GENERIC'); + ).rejects.toMatch( + 'Unable to send feedback. This could be because of network issues, or because you are using an ad-blocker.', + ); }); it('handles 403 transport error', async () => { @@ -324,7 +339,9 @@ describe('sendFeedback', () => { email: 're@example.org', message: 'mi', }), - ).rejects.toMatch('ERROR_FORBIDDEN'); + ).rejects.toMatch( + 'Unable to send feedback. This could be because this domain is not in your list of allowed domains.', + ); }); it('handles 200 transport response', async () => { @@ -358,7 +375,7 @@ describe('sendFeedback', () => { vi.advanceTimersByTime(30_000); - await expect(promise).rejects.toMatch('ERROR_TIMEOUT'); + await expect(promise).rejects.toMatch('Unable to determine if Feedback was correctly sent.'); vi.useRealTimers(); }); From 32e030fde8fdd98645d01e284cf84ac31f3b95a7 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Fri, 24 Apr 2026 00:07:56 -0400 Subject: [PATCH 4/6] bump size limits for feedback error-message overrides Co-Authored-By: Claude Opus 4.7 (1M context) --- .size-limit.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.size-limit.js b/.size-limit.js index e9e760d91526..6075311aaa01 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -103,7 +103,7 @@ module.exports = [ path: 'packages/browser/build/npm/esm/prod/index.js', import: createImport('init', 'feedbackIntegration'), gzip: true, - limit: '43 KB', + limit: '44 KB', }, { name: '@sentry/browser (incl. sendFeedback)', @@ -233,7 +233,7 @@ module.exports = [ name: 'CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics)', path: createCDNPath('bundle.tracing.replay.feedback.logs.metrics.min.js'), gzip: true, - limit: '90 KB', + limit: '91 KB', }, // browser CDN bundles (non-gzipped) { @@ -297,7 +297,7 @@ module.exports = [ path: createCDNPath('bundle.tracing.replay.feedback.logs.metrics.min.js'), gzip: false, brotli: false, - limit: '272 KB', + limit: '273 KB', }, // Next.js SDK (ESM) { From 4333b9237463565a2ffa691bb4e33e857b42be5f Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Fri, 24 Apr 2026 02:18:24 -0400 Subject: [PATCH 5/6] test: add default-fallback test for partial errorMessages override Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/feedback/test/core/sendFeedback.test.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/feedback/test/core/sendFeedback.test.ts b/packages/feedback/test/core/sendFeedback.test.ts index 15dcf37914d0..e40f65d6a29c 100644 --- a/packages/feedback/test/core/sendFeedback.test.ts +++ b/packages/feedback/test/core/sendFeedback.test.ts @@ -293,6 +293,20 @@ describe('sendFeedback', () => { ).rejects.toMatch('custom forbidden text'); }); + it('falls back to default messages for codes not in errorMessages', async () => { + mockSdk(); + vi.spyOn(getClient()!.getTransport()!, 'send').mockImplementation(() => { + return Promise.resolve({ statusCode: 400 }); + }); + + // Only override ERROR_FORBIDDEN — a 400 should still use the default generic message. + await expect( + sendFeedback({ message: 'mi' }, { errorMessages: { ERROR_FORBIDDEN: 'custom forbidden text' } }), + ).rejects.toMatch( + 'Unable to send feedback. This could be because of network issues, or because you are using an ad-blocker.', + ); + }); + it('handles 400 transport error', async () => { mockSdk(); vi.spyOn(getClient()!.getTransport()!, 'send').mockImplementation(() => { From 029c5351f473bc7783fd647ea5555a3e0672e21e Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Fri, 24 Apr 2026 09:19:16 -0400 Subject: [PATCH 6/6] fix: restore original ERROR_EMPTY_MESSAGE_TEXT wording Keeps the default error string identical to the pre-PR version so standalone sendFeedback consumers matching on the exact message string see no behavior change. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/feedback/src/constants/index.ts | 2 +- packages/feedback/test/core/sendFeedback.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/feedback/src/constants/index.ts b/packages/feedback/src/constants/index.ts index cbfafc4f36a6..309bba7490ab 100644 --- a/packages/feedback/src/constants/index.ts +++ b/packages/feedback/src/constants/index.ts @@ -26,7 +26,7 @@ export const HIGHLIGHT_TOOL_TEXT = 'Highlight'; export const HIDE_TOOL_TEXT = 'Hide'; export const REMOVE_HIGHLIGHT_TEXT = 'Remove'; -export const ERROR_EMPTY_MESSAGE_TEXT = 'Unable to submit feedback with an empty message'; +export const ERROR_EMPTY_MESSAGE_TEXT = 'Unable to submit feedback with empty message'; export const ERROR_NO_CLIENT_TEXT = 'No client setup, cannot send feedback.'; export const ERROR_TIMEOUT_TEXT = 'Unable to determine if Feedback was correctly sent.'; export const ERROR_FORBIDDEN_TEXT = diff --git a/packages/feedback/test/core/sendFeedback.test.ts b/packages/feedback/test/core/sendFeedback.test.ts index e40f65d6a29c..56938d1ddd6a 100644 --- a/packages/feedback/test/core/sendFeedback.test.ts +++ b/packages/feedback/test/core/sendFeedback.test.ts @@ -269,7 +269,7 @@ describe('sendFeedback', () => { it('throws when message is empty', () => { mockSdk(); - expect(() => sendFeedback({ message: '' })).toThrow('Unable to submit feedback with an empty message'); + expect(() => sendFeedback({ message: '' })).toThrow('Unable to submit feedback with empty message'); }); it('throws when no client is set up', async () => {