diff --git a/sentry.utils.test.ts b/sentry.utils.test.ts new file mode 100644 index 0000000000..0700ab1ee5 --- /dev/null +++ b/sentry.utils.test.ts @@ -0,0 +1,19 @@ +import type { ErrorEvent } from '@sentry/nextjs' +import { shouldIgnoreError } from './sentry.utils' + +function eventWith(partial: { message?: string; type?: string; value?: string }): ErrorEvent { + return { + message: partial.message, + exception: { values: [{ type: partial.type, value: partial.value }] }, + } as unknown as ErrorEvent +} + +describe('shouldIgnoreError — alreadyReported (fetchWithSentry wrapper)', () => { + it('ignores a re-thrown ServiceUnavailableError (already captured at the fetch site)', () => { + expect(shouldIgnoreError(eventWith({ type: 'ServiceUnavailableError', value: 'upstream 503' }))).toBe(true) + }) + + it('does not ignore an unrelated application error', () => { + expect(shouldIgnoreError(eventWith({ type: 'TypeError', value: 'x is not a function' }))).toBe(false) + }) +}) diff --git a/sentry.utils.ts b/sentry.utils.ts index 2a72d7990c..87f27bbc3d 100644 --- a/sentry.utils.ts +++ b/sentry.utils.ts @@ -38,6 +38,15 @@ const IGNORED_ERRORS = { // Third-party scripts we don't control thirdParty: ['googletagmanager', 'gtag', 'analytics', 'hotjar', 'clarity', 'intercom', 'crisp'], + // fetchWithSentry wrapper errors: the underlying timeout/network/HTTP + // failure is already captured at the fetch site with full context, so the + // re-thrown ServiceUnavailableError bubbling to global handlers (or being + // console.error'd by a consumer) would only double-count it (PEANUT-UI-QDJ). + // Substring-matching this pattern is safe only because ServiceUnavailableError + // is our own internal fetchWithSentry wrapper name, not a generic string that + // could appear in an unrelated third-party error message. + alreadyReported: ['ServiceUnavailableError'], + // Third-party SDK internal errors (not actionable) thirdPartySdkErrors: [ 'IndexedDB:Set:InternalError', // Vercel Analytics storage - fails in private browsing, not actionable diff --git a/src/components/Badges/BadgeStatusDrawer.tsx b/src/components/Badges/BadgeStatusDrawer.tsx index 41b5073c10..0ce57b3425 100644 --- a/src/components/Badges/BadgeStatusDrawer.tsx +++ b/src/components/Badges/BadgeStatusDrawer.tsx @@ -1,4 +1,4 @@ -import { Drawer, DrawerContent } from '@/components/Global/Drawer' +import { Drawer, DrawerContent, DrawerTitle } from '@/components/Global/Drawer' import Image from 'next/image' import { useState } from 'react' import { formatDate } from '@/utils/general.utils' @@ -64,7 +64,9 @@ export const BadgeStatusDrawer = ({ isOpen, onClose, badge }: BadgeStatusDrawerP

Badge unlocked!

-

{displayName}

+ + {displayName} + diff --git a/src/hooks/useTokenPrice.ts b/src/hooks/useTokenPrice.ts index 11ecf74e9e..5aab942c7e 100644 --- a/src/hooks/useTokenPrice.ts +++ b/src/hooks/useTokenPrice.ts @@ -95,9 +95,13 @@ export const useTokenPrice = ({ return null } catch (error) { - // Preserve Sentry error reporting from original implementation + // fetchWithSentry already reported the underlying failure; this + // capture only surfaces non-fetch errors (beforeSend drops the + // ServiceUnavailableError wrapper). console.info stays out of + // captureConsoleIntegration (PEANUT-UI-MH5) — the fallback is + // graceful, not an error. Sentry.captureException(error) - console.error('error fetching tokenPrice, falling back to tokenDenomination') + console.info('error fetching tokenPrice, falling back to tokenDenomination') return null } }, diff --git a/src/utils/sentry.utils.ts b/src/utils/sentry.utils.ts index 198428986d..44a95987ac 100644 --- a/src/utils/sentry.utils.ts +++ b/src/utils/sentry.utils.ts @@ -409,7 +409,10 @@ export const fetchWithSentry = async ( // fetch rejected (timeout / DNS / connection refused) — the request never // reached the backend, so flag a connectivity failure. reportNetworkError() - console.error(error) + // console.info, not error: captureConsoleIntegration would turn an + // error-level log into a second Sentry event on top of the explicit + // captures below. + console.info(error) if (error instanceof Error && error.name === 'AbortError') { const timeoutError = new Error(`Request to ${url} timed out after ${timeoutMs}ms`)