From a60f4f75106e491cfc8e4b27c29690486d41e1e1 Mon Sep 17 00:00:00 2001 From: dschom Date: Wed, 25 Feb 2026 09:22:09 -0800 Subject: [PATCH] fix(settings): enrich Sentry errors in fetchLegalTerms and fetchConfig with timing and cancellation info Adds fetchDuration (ms), cancelled (AbortError detection), and errorName to the Sentry captureException extra context so 'Load failed' errors are easier to triage by cause and duration. Co-Authored-By: Claude Sonnet 4.6 --- .../fxa-settings/src/models/hooks.test.ts | 8 ++- packages/fxa-settings/src/models/hooks.ts | 66 ++++++++++++++----- 2 files changed, 58 insertions(+), 16 deletions(-) diff --git a/packages/fxa-settings/src/models/hooks.test.ts b/packages/fxa-settings/src/models/hooks.test.ts index 71acd147c4c..8c572dba501 100644 --- a/packages/fxa-settings/src/models/hooks.test.ts +++ b/packages/fxa-settings/src/models/hooks.test.ts @@ -321,7 +321,13 @@ describe('useCmsInfoState', () => { // why we changed the hook to start with. expect(Sentry.captureException).toHaveBeenCalledWith(fetchError, { tags: { area: 'useCmsInfoState' }, - extra: { clientId: '1234567890abcdef', entrypoint: 'preferences' }, + extra: { + clientId: '1234567890abcdef', + entrypoint: 'preferences', + cancelled: false, + errorName: 'Error', + fetchDuration: expect.any(Number), + }, }); }); diff --git a/packages/fxa-settings/src/models/hooks.ts b/packages/fxa-settings/src/models/hooks.ts index 77716addab6..6cf7f94e941 100644 --- a/packages/fxa-settings/src/models/hooks.ts +++ b/packages/fxa-settings/src/models/hooks.ts @@ -17,7 +17,10 @@ import { } from '../lib/integrations'; import { ReachRouterWindow } from '../lib/window'; import { StorageData, UrlHashData, UrlQueryData } from '../lib/model-data'; -import { MetricsData, SignedInAccountStatus } from '../components/App/interfaces'; +import { + MetricsData, + SignedInAccountStatus, +} from '../components/App/interfaces'; import { RelierClientInfo, RelierSubscriptionInfo, @@ -28,7 +31,10 @@ import * as Sentry from '@sentry/browser'; import { useDynamicLocalization } from '../contexts/DynamicLocalizationContext'; import { sessionToken } from '../lib/cache'; import { useLocalStorageSync } from '../lib/hooks/useLocalStorageSync'; -import { getFullAccountData, isSignedIn as checkIsSignedIn } from '../lib/account-storage'; +import { + getFullAccountData, + isSignedIn as checkIsSignedIn, +} from '../lib/account-storage'; const DEFAULT_CMS_ENTRYPOINT = 'default'; @@ -197,15 +203,21 @@ export function useInitialMetricsQueryState() { throw new Error('AuthClient not available'); } - const [accountResult, totpResult, recoveryKeyResult] = await Promise.allSettled([ - authClient.account(token), - authClient.checkTotpTokenExists(token), - authClient.recoveryKeyExists(token, undefined), - ]); - - const accountData = accountResult.status === 'fulfilled' ? accountResult.value : null; - const totpData = totpResult.status === 'fulfilled' ? totpResult.value : null; - const recoveryKeyData = recoveryKeyResult.status === 'fulfilled' ? recoveryKeyResult.value : null; + const [accountResult, totpResult, recoveryKeyResult] = + await Promise.allSettled([ + authClient.account(token), + authClient.checkTotpTokenExists(token), + authClient.recoveryKeyExists(token, undefined), + ]); + + const accountData = + accountResult.status === 'fulfilled' ? accountResult.value : null; + const totpData = + totpResult.status === 'fulfilled' ? totpResult.value : null; + const recoveryKeyData = + recoveryKeyResult.status === 'fulfilled' + ? recoveryKeyResult.value + : null; if (mounted && accountData) { const emails = accountData.emails || []; @@ -215,10 +227,16 @@ export function useInitialMetricsQueryState() { account: { uid: accountData.uid, recoveryKey: recoveryKeyData - ? { exists: recoveryKeyData.exists, estimatedSyncDeviceCount: recoveryKeyData.estimatedSyncDeviceCount } + ? { + exists: recoveryKeyData.exists, + estimatedSyncDeviceCount: + recoveryKeyData.estimatedSyncDeviceCount, + } : null, metricsEnabled: accountData.metricsEnabled ?? true, - primaryEmail: emails.find((e: { isPrimary?: boolean }) => e.isPrimary) || null, + primaryEmail: + emails.find((e: { isPrimary?: boolean }) => e.isPrimary) || + null, emails, totp: totpData || null, }, @@ -388,6 +406,7 @@ export function useCmsInfoState() { setState((prev) => ({ ...prev, loading: true })); const fetchConfig = async () => { + const fetchStart = performance.now(); try { const url = new URL(`${authUrl}/v1/cms/config`); url.searchParams.append('clientId', clientId); @@ -420,9 +439,17 @@ export function useCmsInfoState() { }); } } catch (error) { + const fetchDuration = Math.round(performance.now() - fetchStart); + const cancelled = error instanceof Error && error.name === 'AbortError'; Sentry.captureException(error, { tags: { area: 'useCmsInfoState' }, - extra: { clientId, entrypoint }, + extra: { + clientId, + entrypoint, + fetchDuration, + cancelled, + errorName: error instanceof Error ? error.name : typeof error, + }, }); if (mounted) { @@ -579,6 +606,7 @@ export function useLegalTermsState() { setState((prev) => ({ ...prev, loading: true })); const fetchLegalTerms = async () => { + const fetchStart = performance.now(); try { const url = new URL(`${authUrl}/v1/cms/legal-terms`); url.searchParams.append(queryParam, queryValue); @@ -622,9 +650,17 @@ export function useLegalTermsState() { }); } } catch (error) { + const fetchDuration = Math.round(performance.now() - fetchStart); + const cancelled = error instanceof Error && error.name === 'AbortError'; Sentry.captureException(error, { tags: { area: 'useLegalTermsState' }, - extra: { queryParam, queryValue }, + extra: { + queryParam, + queryValue, + fetchDuration, + cancelled, + errorName: error instanceof Error ? error.name : typeof error, + }, }); if (mounted) {