From 10ee160114e66c2c664ca7a4e412b56cafb17bb3 Mon Sep 17 00:00:00 2001 From: innolove-dev Date: Sat, 18 Jul 2026 08:47:27 +0100 Subject: [PATCH 1/2] fix(sentry): stop double-counting fetch failures, add missing DrawerTitle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit captureConsoleIntegration promotes every console.error/warn into a Sentry event, and fetchWithSentry both captures the underlying failure AND re-throws a ServiceUnavailableError wrapper that gets captured again by global handlers. One API timeout could produce three Sentry events. - sentry.utils: ignore ServiceUnavailableError (always a wrapper whose cause was already captured at the fetch site) — PEANUT-UI-QDJ - fetchWithSentry: console.info instead of console.error for the raw fetch rejection; the explicit captures below carry the full context - useTokenPrice: fallback log at console.info — the fallback is graceful and the failure is already reported — PEANUT-UI-MH5 - BadgeStatusDrawer: badge name becomes the DrawerTitle, fixing the Radix 'DialogContent requires a DialogTitle' a11y warning — PEANUT-UI-MJS --- sentry.utils.ts | 6 ++++++ src/components/Badges/BadgeStatusDrawer.tsx | 6 ++++-- src/hooks/useTokenPrice.ts | 8 ++++++-- src/utils/sentry.utils.ts | 5 ++++- 4 files changed, 20 insertions(+), 5 deletions(-) diff --git a/sentry.utils.ts b/sentry.utils.ts index 2a72d7990c..ce5099033e 100644 --- a/sentry.utils.ts +++ b/sentry.utils.ts @@ -38,6 +38,12 @@ 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). + 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`) From ac181ff0fe2203d8e7836c902fc35c01ba5adba4 Mon Sep 17 00:00:00 2001 From: innolove-dev Date: Tue, 21 Jul 2026 02:32:26 +0100 Subject: [PATCH 2/2] test(sentry): cover the ServiceUnavailableError ignore entry + note substring safety Adds root-level sentry.utils.test.ts for the SDK-config filter's alreadyReported entry (distinct from src/utils/sentry.utils.ts, the fetchWithSentry wrapper), and documents that substring-matching ServiceUnavailableError is safe only because it is our internal wrapper name. --- sentry.utils.test.ts | 19 +++++++++++++++++++ sentry.utils.ts | 3 +++ 2 files changed, 22 insertions(+) create mode 100644 sentry.utils.test.ts 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 ce5099033e..87f27bbc3d 100644 --- a/sentry.utils.ts +++ b/sentry.utils.ts @@ -42,6 +42,9 @@ const IGNORED_ERRORS = { // 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)