diff --git a/static/app/actionCreators/dashboards.tsx b/static/app/actionCreators/dashboards.tsx index 7f50ea08fc6531..940c96c6a14f81 100644 --- a/static/app/actionCreators/dashboards.tsx +++ b/static/app/actionCreators/dashboards.tsx @@ -12,6 +12,7 @@ import {defined} from 'sentry/utils'; import {getApiUrl} from 'sentry/utils/api/getApiUrl'; import {TOP_N} from 'sentry/utils/discover/types'; import {fetchMutation} from 'sentry/utils/queryClient'; +import {RequestError} from 'sentry/utils/requestError/requestError'; import {getStarredDashboardsQueryKey} from 'sentry/views/dashboards/hooks/useGetStarredDashboards'; import { DashboardFilter, @@ -38,7 +39,8 @@ export function fetchDashboards( ); promise.catch(response => { - const errorResponse = response?.responseJSON ?? null; + const errorResponse = + response instanceof RequestError ? response?.responseJSON : null; if (errorResponse) { const errors = flattenErrors(errorResponse, {}); @@ -82,7 +84,8 @@ export function createDashboard( ); promise.catch(response => { - const errorResponse = response?.responseJSON ?? null; + const errorResponse = + response instanceof RequestError ? response?.responseJSON : null; if (errorResponse) { const errors = flattenErrors(errorResponse, {}); @@ -166,8 +169,9 @@ export async function updateDashboardFavorite( queryKey: getStarredDashboardsQueryKey(organization), }); addSuccessMessage(isFavorited ? t('Added as favorite') : t('Removed as favorite')); - } catch (response: any) { - const errorResponse = response?.responseJSON ?? null; + } catch (response) { + const errorResponse = + response instanceof RequestError ? response?.responseJSON : null; if (errorResponse) { const errors = flattenErrors(errorResponse, {}); addErrorMessage(errors[Object.keys(errors)[0]!]! as string); @@ -193,7 +197,8 @@ export function fetchDashboard( ); promise.catch(response => { - const errorResponse = response?.responseJSON ?? null; + const errorResponse = + response instanceof RequestError ? response?.responseJSON : null; if (errorResponse) { const errors = flattenErrors(errorResponse, {}); @@ -248,7 +253,8 @@ export function updateDashboard( // that it can be more specific than just "Dashboard updated," but do the // error-handling here, since it doesn't depend on the caller's context promise.catch(response => { - const errorResponse = response?.responseJSON ?? null; + const errorResponse = + response instanceof RequestError ? response?.responseJSON : null; if (errorResponse) { const errors = flattenErrors(errorResponse, {}); @@ -281,7 +287,8 @@ export function deleteDashboard( }); promise.catch(response => { - const errorResponse = response?.responseJSON ?? null; + const errorResponse = + response instanceof RequestError ? response?.responseJSON : null; if (errorResponse) { const errors = flattenErrors(errorResponse, {}); @@ -335,7 +342,8 @@ export function updateDashboardPermissions( ); promise.catch(response => { - const errorResponse = response?.responseJSON ?? null; + const errorResponse = + response instanceof RequestError ? response?.responseJSON : null; if (errorResponse) { const errors = flattenErrors(errorResponse, {}); diff --git a/static/app/actionCreators/integrations.tsx b/static/app/actionCreators/integrations.tsx index c608c0c0365a63..f43b4ad8b02eba 100644 --- a/static/app/actionCreators/integrations.tsx +++ b/static/app/actionCreators/integrations.tsx @@ -66,7 +66,7 @@ function applyRepositoryAddComplete(promise: Promise) { }); addSuccessMessage(message); }, - errorData => { + (errorData: any) => { const text = errorData.responseJSON.errors ? errorData.responseJSON.errors.__all__ : t('Unable to add repository.'); diff --git a/static/app/actionCreators/performance.tsx b/static/app/actionCreators/performance.tsx index 2e57ab42cfff0d..72591a959c6359 100644 --- a/static/app/actionCreators/performance.tsx +++ b/static/app/actionCreators/performance.tsx @@ -6,6 +6,7 @@ import { import type {Client} from 'sentry/api'; import {t} from 'sentry/locale'; import {parseLinkHeader} from 'sentry/utils/parseLinkHeader'; +import {RequestError} from 'sentry/utils/requestError/requestError'; type KeyTransaction = { project_id: string; @@ -96,13 +97,17 @@ export function toggleKeyTransaction( promise.then(clearIndicators); promise.catch(response => { - const responseJSON = response?.responseJSON; - const errorDetails = responseJSON?.detail ?? responseJSON?.non_field_errors; - - if (Array.isArray(errorDetails) && errorDetails.length && errorDetails[0]) { - addErrorMessage(errorDetails[0]); - } else { - addErrorMessage(errorDetails ?? t('Unable to update key transaction')); + if (response instanceof RequestError) { + const responseJSON = response?.responseJSON; + const errorDetails = responseJSON?.detail ?? responseJSON?.non_field_errors; + + if (Array.isArray(errorDetails) && errorDetails.length && errorDetails[0]) { + addErrorMessage(errorDetails[0]); + } else if (typeof errorDetails === 'string') { + addErrorMessage(errorDetails); + } else { + addErrorMessage(t('Unable to update key transaction')); + } } }); diff --git a/static/app/actionCreators/projects.tsx b/static/app/actionCreators/projects.tsx index 8fa48a37edcb69..d410d0900aac33 100644 --- a/static/app/actionCreators/projects.tsx +++ b/static/app/actionCreators/projects.tsx @@ -15,6 +15,7 @@ import {ProjectsStore} from 'sentry/stores/projectsStore'; import type {Team} from 'sentry/types/organization'; import type {Project} from 'sentry/types/project'; import {apiOptions} from 'sentry/utils/api/apiOptions'; +import type {RequestError} from 'sentry/utils/requestError/requestError'; import {useApi} from 'sentry/utils/useApi'; type UpdateParams = { @@ -38,7 +39,7 @@ export function update(api: Client, params: UpdateParams) { ProjectsStore.onUpdateSuccess(data); return data; }, - err => { + (err: Error) => { ProjectsStatsStore.onUpdateError(err, params.projectId); throw err; } @@ -138,11 +139,11 @@ export function transferProject( }) ); }, - err => { + (err: RequestError) => { let message = ''; // Handle errors with known failures - if (err.status >= 400 && err.status < 500 && err.responseJSON) { - message = err.responseJSON?.detail; + if (err.status && err.status >= 400 && err.status < 500 && err.responseJSON) { + message = err.responseJSON.detail as string; } if (message) { diff --git a/static/app/actionCreators/release.tsx b/static/app/actionCreators/release.tsx index 4a073dd85cfb06..ec8faa8f283c5a 100644 --- a/static/app/actionCreators/release.tsx +++ b/static/app/actionCreators/release.tsx @@ -6,6 +6,7 @@ import { import type {Client} from 'sentry/api'; import {t} from 'sentry/locale'; import {ReleaseStatus} from 'sentry/types/release'; +import {RequestError} from 'sentry/utils/requestError/requestError'; type ParamsGet = { orgSlug: string; @@ -31,7 +32,14 @@ export function archiveRelease(api: Client, params: ParamsGet) { addSuccessMessage(t('Release was successfully archived.')); }) .catch(error => { - addErrorMessage(error.responseJSON?.detail ?? t('Release could not be archived.')); + if ( + error instanceof RequestError && + typeof error.responseJSON?.detail === 'string' + ) { + addErrorMessage(error.responseJSON?.detail); + } else { + addErrorMessage(t('Release could not be archived.')); + } throw error; }); } @@ -54,7 +62,15 @@ export function restoreRelease(api: Client, params: ParamsGet) { addSuccessMessage(t('Release was successfully restored.')); }) .catch(error => { - addErrorMessage(error.responseJSON?.detail ?? t('Release could not be restored.')); + if ( + error instanceof RequestError && + typeof error.responseJSON?.detail === 'string' + ) { + addErrorMessage(error.responseJSON?.detail); + } else { + addErrorMessage(t('Release could not be restored.')); + } + throw error; }); } diff --git a/static/app/api.tsx b/static/app/api.tsx index 8af02977da610a..b8c03c01c3383f 100644 --- a/static/app/api.tsx +++ b/static/app/api.tsx @@ -648,7 +648,7 @@ export class Client { // Not related to errors in responses } ) - .catch(error => { + .catch((error: Error) => { // eslint-disable-next-line no-console console.error(error); diff --git a/static/app/components/core/select/async.tsx b/static/app/components/core/select/async.tsx index 2bbce6f31f1c2e..371d77963ba7ad 100644 --- a/static/app/components/core/select/async.tsx +++ b/static/app/components/core/select/async.tsx @@ -65,7 +65,7 @@ class SelectAsyncControl extends Component< api: Client | null; cache: Record; - doQuery = debounce((cb: (...args: [RequestError] | [null, TData]) => void) => { + doQuery = debounce((cb: (...args: [Error] | [null, TData]) => void) => { const {url, onQuery} = this.props; const {query} = this.state; @@ -79,7 +79,7 @@ class SelectAsyncControl extends Component< }) .then( data => cb(null, data), - err => cb(err) + (err: Error) => cb(err) ); }, 250); diff --git a/static/app/components/exports/useDataExport.ts b/static/app/components/exports/useDataExport.ts index 627157379f3b73..9e9deb43ba3657 100644 --- a/static/app/components/exports/useDataExport.ts +++ b/static/app/components/exports/useDataExport.ts @@ -5,6 +5,7 @@ import type {ResponseMeta} from 'sentry/api'; import {t} from 'sentry/locale'; import {getApiUrl} from 'sentry/utils/api/getApiUrl'; import {downloadFromHref} from 'sentry/utils/downloadFromHref'; +import {RequestError} from 'sentry/utils/requestError/requestError'; import {useApi} from 'sentry/utils/useApi'; import {useOrganization} from 'sentry/utils/useOrganization'; import {createLogDownloadFilename} from 'sentry/views/explore/logs/createLogDownloadFilename'; @@ -112,13 +113,18 @@ export function useDataExport({ if (unmountedRef?.current) { return; } - const message = - error?.responseJSON?.detail ?? - t( - "We tried our hardest, but we couldn't export your data. Give it another go." + if ( + error instanceof RequestError && + typeof error.responseJSON?.detail === 'string' + ) { + addErrorMessage(error.responseJSON.detail); + } else { + addErrorMessage( + t( + "We tried our hardest, but we couldn't export your data. Give it another go." + ) ); - - addErrorMessage(message); + } inProgressCallback?.(false); }); diff --git a/static/app/components/forms/model.tsx b/static/app/components/forms/model.tsx index 80bf4a600d8537..8d709146fc7c77 100644 --- a/static/app/components/forms/model.tsx +++ b/static/app/components/forms/model.tsx @@ -532,7 +532,7 @@ export class FormModel { this.options.onSubmitSuccess(resp, this); } }) - .catch(resp => { + .catch((resp: any) => { // should we revert field value to last known state? saveSnapshot = null; if (this.options.resetOnError) { @@ -648,7 +648,7 @@ export class FormModel { return data; }) - .catch(resp => { + .catch((resp: any) => { // should we revert field value to last known state? saveSnapshot = null; diff --git a/static/app/components/selectMembers/index.tsx b/static/app/components/selectMembers/index.tsx index 3efc16645cac22..57afbc6e73d868 100644 --- a/static/app/components/selectMembers/index.tsx +++ b/static/app/components/selectMembers/index.tsx @@ -141,7 +141,7 @@ class SelectMembers extends Component { }) .then( data => cb(null, data), - err => cb(err) + err => cb(err as Error) ); }, 250 diff --git a/static/app/types/promise.d.ts b/static/app/types/promise.d.ts new file mode 100644 index 00000000000000..b04150c2382e44 --- /dev/null +++ b/static/app/types/promise.d.ts @@ -0,0 +1,10 @@ +interface Promise { + then( + onfulfilled?: ((value: T) => TFul | PromiseLike) | null, + onrejected?: ((reason: unknown) => TRej | PromiseLike) | null + ): Promise; + + catch( + onrejected?: ((reason: unknown) => TRej | PromiseLike) | null + ): Promise; +} diff --git a/static/app/views/alerts/rules/issue/index.tsx b/static/app/views/alerts/rules/issue/index.tsx index e9f0249e5421a6..5746a62005298a 100644 --- a/static/app/views/alerts/rules/issue/index.tsx +++ b/static/app/views/alerts/rules/issue/index.tsx @@ -436,7 +436,7 @@ class IssueRuleEditor extends DeprecatedAsyncComponent { success: true, }); }) - .catch(error => { + .catch((error: any) => { addErrorMessage(tn('Notification failed', 'Notifications failed', actions)); this.setState({detailedError: error.responseJSON || null}); trackAnalytics('edit_alert_rule.notification_test', { diff --git a/static/app/views/dashboards/detail.tsx b/static/app/views/dashboards/detail.tsx index cb8abf5b77ece5..120a0af276696e 100644 --- a/static/app/views/dashboards/detail.tsx +++ b/static/app/views/dashboards/detail.tsx @@ -894,7 +894,7 @@ class DashboardDetail extends Component { Sentry.captureMessage('Generated dashboard failed to save', { extra: { dashboard_title: newModifiedDashboard.title, - error_message: error?.message, + error_message: error instanceof Error ? error.message : undefined, }, }); }); diff --git a/static/app/views/dashboards/utils.tsx b/static/app/views/dashboards/utils.tsx index 77a9bbe7275016..5ac09f54b1c9f8 100644 --- a/static/app/views/dashboards/utils.tsx +++ b/static/app/views/dashboards/utils.tsx @@ -327,7 +327,7 @@ export function getWidgetReleasesUrl( } export function flattenErrors( - data: ValidationError | string, + data: Record | string, update: FlatValidationError ): FlatValidationError { if (typeof data === 'string') { diff --git a/static/app/views/discover/table/index.tsx b/static/app/views/discover/table/index.tsx index 13fa70cafedf8d..91d25606734ae6 100644 --- a/static/app/views/discover/table/index.tsx +++ b/static/app/views/discover/table/index.tsx @@ -254,7 +254,7 @@ class Table extends PureComponent { setSplitDecision?.(splitDecision); } }) - .catch(err => { + .catch((err: any) => { metric.measure({ name: 'app.api.discover-query', start: `discover-events-start-${apiPayload.query}`, diff --git a/static/app/views/explore/profiling/profilesProvider.tsx b/static/app/views/explore/profiling/profilesProvider.tsx index 2e0b53cf74dee5..3177ae9fce42c2 100644 --- a/static/app/views/explore/profiling/profilesProvider.tsx +++ b/static/app/views/explore/profiling/profilesProvider.tsx @@ -147,7 +147,7 @@ export function TransactionProfileProvider({ setProfile({type: 'resolved', data: p}); }) .catch(err => { - const message = err.toString(); + const message = String(err); setProfile({type: 'errored', error: message}); Sentry.captureException(err); diff --git a/static/app/views/performance/transactionSummary/transactionThresholdButton.tsx b/static/app/views/performance/transactionSummary/transactionThresholdButton.tsx index 6718cb0bb18346..03f1de3866da62 100644 --- a/static/app/views/performance/transactionSummary/transactionThresholdButton.tsx +++ b/static/app/views/performance/transactionSummary/transactionThresholdButton.tsx @@ -11,6 +11,7 @@ import type {Organization} from 'sentry/types/organization'; import type {Project} from 'sentry/types/project'; import {defined} from 'sentry/utils'; import type {EventView} from 'sentry/utils/discover/eventView'; +import {RequestError} from 'sentry/utils/requestError/requestError'; import {withApi} from 'sentry/utils/withApi'; import {withProjects} from 'sentry/utils/withProjects'; @@ -81,8 +82,9 @@ function TransactionThresholdButton({ }) .catch(err => { setLoadingThreshold(false); - const errorMessage = err.responseJSON?.threshold ?? null; - addErrorMessage(errorMessage); + const errorMessage = + err instanceof RequestError ? err.responseJSON?.threshold : null; + addErrorMessage(errorMessage as string); }); }); }, [api, project, organization.slug, transactionName]); diff --git a/static/app/views/performance/transactionSummary/transactionThresholdModal.tsx b/static/app/views/performance/transactionSummary/transactionThresholdModal.tsx index 3b7382ff5a7a36..d6ea927152f798 100644 --- a/static/app/views/performance/transactionSummary/transactionThresholdModal.tsx +++ b/static/app/views/performance/transactionSummary/transactionThresholdModal.tsx @@ -17,6 +17,7 @@ import type {Organization} from 'sentry/types/organization'; import type {Project} from 'sentry/types/project'; import {defined} from 'sentry/utils'; import type {EventView} from 'sentry/utils/discover/eventView'; +import {RequestError} from 'sentry/utils/requestError/requestError'; import {withApi} from 'sentry/utils/withApi'; import {withProjects} from 'sentry/utils/withProjects'; @@ -91,11 +92,13 @@ function TransactionThresholdModal({ }) .catch(err => { let errorMessage = - err.responseJSON?.threshold ?? err.responseJSON?.non_field_errors ?? null; + err instanceof RequestError + ? (err.responseJSON?.threshold ?? err.responseJSON?.non_field_errors) + : null; if (Array.isArray(errorMessage)) { errorMessage = errorMessage[0]; } - addErrorMessage(errorMessage); + addErrorMessage(errorMessage as string); }); }; @@ -139,8 +142,9 @@ function TransactionThresholdModal({ }); }) .catch(err => { - const errorMessage = err.responseJSON?.threshold ?? null; - addErrorMessage(errorMessage); + const errorMessage = + err instanceof RequestError ? err.responseJSON?.threshold : null; + addErrorMessage(errorMessage as string); }); }; diff --git a/static/app/views/settings/account/accountEmails.tsx b/static/app/views/settings/account/accountEmails.tsx index 5082eaff601746..abd844747ef927 100644 --- a/static/app/views/settings/account/accountEmails.tsx +++ b/static/app/views/settings/account/accountEmails.tsx @@ -158,7 +158,7 @@ export function EmailAddresses() { api .requestPromise(endpoint, requestParams) .catch(err => { - if (err?.responseJSON?.email) { + if (err instanceof RequestError && typeof err?.responseJSON?.email === 'string') { addErrorMessage(err.responseJSON.email); } }) diff --git a/static/app/views/settings/project/projectOwnership/ownerInput.tsx b/static/app/views/settings/project/projectOwnership/ownerInput.tsx index 889eb3e98cc793..61c3a84d34cbc2 100644 --- a/static/app/views/settings/project/projectOwnership/ownerInput.tsx +++ b/static/app/views/settings/project/projectOwnership/ownerInput.tsx @@ -17,6 +17,7 @@ import type {Organization} from 'sentry/types/organization'; import type {Project} from 'sentry/types/project'; import {defined} from 'sentry/utils'; import {trackIntegrationAnalytics} from 'sentry/utils/integrationUtil'; +import {RequestError} from 'sentry/utils/requestError/requestError'; type Props = { dateUpdated: string | null; @@ -87,25 +88,25 @@ export function OwnerInput({ }); }) .catch(caught => { - setError(caught.responseJSON); - if (caught.status === 403) { - addErrorMessage( - t( - "You don't have permission to modify issue ownership rules for this project" - ) - ); - } else if ( - caught.status === 400 && - caught.responseJSON.raw?.[0].startsWith('Invalid rule owners:') - ) { - addErrorMessage( - t( - 'Unable to save issue ownership rule changes: %s', - caught.responseJSON.raw[0] - ) - ); - } else { - addErrorMessage(t('Unable to save issue ownership rule changes')); + if (caught instanceof RequestError) { + const inputError = caught.responseJSON as InputError; + setError(inputError); + if (caught.status === 403) { + addErrorMessage( + t( + "You don't have permission to modify issue ownership rules for this project" + ) + ); + } else if ( + caught.status === 400 && + inputError.raw?.[0]?.startsWith('Invalid rule owners:') + ) { + addErrorMessage( + t('Unable to save issue ownership rule changes: %s', inputError.raw[0]) + ); + } else { + addErrorMessage(t('Unable to save issue ownership rule changes')); + } } }); diff --git a/static/gsAdmin/components/migrateLegacySeerAction.tsx b/static/gsAdmin/components/migrateLegacySeerAction.tsx index 28e1af5d20fac1..acf5fe5f49c6dc 100644 --- a/static/gsAdmin/components/migrateLegacySeerAction.tsx +++ b/static/gsAdmin/components/migrateLegacySeerAction.tsx @@ -7,6 +7,7 @@ import { addSuccessMessage, } from 'sentry/actionCreators/indicator'; import type {Client} from 'sentry/api'; +import {RequestError} from 'sentry/utils/requestError/requestError'; import {withApi} from 'sentry/utils/withApi'; import type { @@ -54,9 +55,13 @@ class MigrateLegacySeerAction extends Component { addSuccessMessage('Legacy Seer migration complete.'); }) .catch(res => { - addErrorMessage( - res.responseJSON?.detail ?? res.responseText ?? 'Migration failed.' - ); + if (res instanceof RequestError && typeof res.responseJSON?.detail === 'string') { + addErrorMessage(res.responseJSON.detail); + } else if (res instanceof RequestError && res.responseText) { + addErrorMessage(res.responseText); + } else { + addErrorMessage('Migration failed.'); + } }); }; diff --git a/static/gsAdmin/components/sendWeeklyEmailAction.tsx b/static/gsAdmin/components/sendWeeklyEmailAction.tsx index 4148bd52346999..c9dcd94c42b5d1 100644 --- a/static/gsAdmin/components/sendWeeklyEmailAction.tsx +++ b/static/gsAdmin/components/sendWeeklyEmailAction.tsx @@ -8,6 +8,7 @@ import { import type {Client} from 'sentry/api'; import {BooleanField} from 'sentry/components/forms/fields/booleanField'; import {TextField} from 'sentry/components/forms/fields/textField'; +import {RequestError} from 'sentry/utils/requestError/requestError'; import {withApi} from 'sentry/utils/withApi'; import type { @@ -50,10 +51,12 @@ class SendWeeklyEmailAction extends Component { addSuccessMessage('Email queued'); }) .catch(res => { - if (res.status === 404) { - addErrorMessage('User is not a member of the organization!'); - } else { - addErrorMessage(res.responseText || res.name); + if (res instanceof RequestError) { + if (res.status === 404) { + addErrorMessage('User is not a member of the organization!'); + } else { + addErrorMessage(res.responseText || res.name); + } } }); }; diff --git a/static/gsAdmin/views/relocationCreate.tsx b/static/gsAdmin/views/relocationCreate.tsx index 0f3632d15b57ff..59e7b12f81b132 100644 --- a/static/gsAdmin/views/relocationCreate.tsx +++ b/static/gsAdmin/views/relocationCreate.tsx @@ -9,6 +9,7 @@ import {OverlayTrigger} from '@sentry/scraps/overlayTrigger'; import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator'; import {Client} from 'sentry/api'; import {ConfigStore} from 'sentry/stores/configStore'; +import {RequestError} from 'sentry/utils/requestError/requestError'; import {useApi} from 'sentry/utils/useApi'; import {useNavigate} from 'sentry/utils/useNavigate'; @@ -50,7 +51,7 @@ function RelocationForm() { host: region.url, }) .catch(error => { - if (error.status === 403) { + if (error instanceof RequestError && error.status === 403) { addErrorMessage(PROMO_CODE_ERROR_MSG); // Ensure that the wrapping `catch` block doesn't try to re-print this error message.