Skip to content

Commit

Permalink
fix(sentry): fix sentry unhandled rejection (#4270)
Browse files Browse the repository at this point in the history
* fix: do not throw handled error

* refactor: extract breadcrumbs checking to an external fn

* refactor: move cow-react/sentry.ts to cow-react/sentry/index.ts

* refactor: move beforeSend to its own file

* feat: ignore metamask unhandled rejection error

* refactor: rename fn

* fix: ignore errors based on error code

* fix: add one more error code

* chore: remove debug statements
alfetopito authored May 3, 2024
1 parent 55f02b2 commit 7477bc0
Showing 4 changed files with 143 additions and 93 deletions.
Original file line number Diff line number Diff line change
@@ -54,7 +54,7 @@ export function useTradeApproveCallback(amountToApprove?: CurrencyAmount<Currenc
updateTradeApproveState({ error: typeof error === 'string' ? error : error.message || error.toString() })
}

throw error
return undefined
})
},
[symbol, approveCallback, updateTradeApproveState, currency]
92 changes: 0 additions & 92 deletions apps/cowswap-frontend/src/cow-react/sentry.ts

This file was deleted.

114 changes: 114 additions & 0 deletions apps/cowswap-frontend/src/cow-react/sentry/beforeSend.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import * as Sentry from '@sentry/react'
import { ErrorEvent as SentryErrorEvent } from '@sentry/types'

export function beforeSend(event: SentryErrorEvent, _hint: Sentry.EventHint) {
if (shouldIgnoreErrorBasedOnExtra(event)) {
return null
} else if (shouldIgnoreErrorBasedOnBreadcrumbs(event)) {
return null
} else {
return event
}
}

/**
* Ignore sentry errors that are unhandled exceptions and have extra serialized that similar to:
*
* {
* code: -32000,
* message: header not found
* }
*/
function shouldIgnoreErrorBasedOnExtra(error: SentryErrorEvent): boolean {
const serialized = error.extra?.__serialized__

return Boolean(
serialized &&
typeof serialized === 'object' &&
'code' in serialized &&
serialized.code &&
typeof serialized.code === 'number' &&
EXTRA_ERROR_CODES_TO_IGNORE.has(serialized.code)
)
}

const EXTRA_ERROR_CODES_TO_IGNORE = new Set([-32000, 4001, -32603])

/**
* Detects whether given error is a load failed error
*
* Adapted from https://gist.github.com/jeengbe/4bc86f05a41a1831e6abf2369579cc7a
*/
function shouldIgnoreErrorBasedOnBreadcrumbs(error: SentryErrorEvent): boolean {
const exception = error.exception?.values?.[0]
const breadcrumbs = error.breadcrumbs

if (
!exception?.type ||
!breadcrumbs ||
!isTypeError(exception.type, exception?.value) ||
!isUnhandledRejectionError(exception.type)
) {
return false
}

return searchBreadcrumbs(breadcrumbs, [isFetchError, isMetamaskRpcError])
}

function isTypeError(type: string, value: string | undefined): boolean {
return !!value && type === 'TypeError' && TYPE_ERROR_FETCH_FAILED_VALUES.has(value)
}

function isUnhandledRejectionError(type: string): boolean {
return type === 'UnhandledRejection'
}

function searchBreadcrumbs(breadcrumbs: Sentry.Breadcrumb[], checkBreadcrumbs: CheckBreadcrumb[]) {
const now = Date.now()

// We go from the back since the last breadcrumb is most likely the erroneous one
for (let i = breadcrumbs.length - 1; i >= 0; i--) {
const breadcrumb = breadcrumbs[i]
if (!breadcrumb) continue

// We only need to check the last 3s of breadcrumbs as any earlier breadcrumbs are definitely unrelated
if (breadcrumb.timestamp && now - breadcrumb.timestamp * 1000 > 3000) {
break
}

if (checkBreadcrumbs.some((fn) => fn(breadcrumb))) {
return true
}
}

return false
}

type CheckBreadcrumb = (breadcrumb: Sentry.Breadcrumb) => boolean

const TYPE_ERROR_FETCH_FAILED_VALUES = new Set([
'Failed to fetch',
'NetworkError when attempting to fetch resource.',
'Load failed',
])

function isFetchError(breadcrumb: Sentry.Breadcrumb): boolean {
if (breadcrumb.level !== 'error' || (breadcrumb.category !== 'xhr' && breadcrumb.category !== 'fetch')) {
return false
}

const url = breadcrumb.data?.url as string | undefined
if (!url) return false

return URLS_TO_IGNORE_FETCH_ERRORS.test(url)
}

const URLS_TO_IGNORE_FETCH_ERRORS =
/(twnodes\.com)|(assets\/cow-no-connection)|(api\.blocknative\.com)|(api\.country\.is)|(nodereal\.io)|(wallet\.coinbase\.com)|(cowprotocol\/cowswap-banner)/i

function isMetamaskRpcError(breadcrumb: Sentry.Breadcrumb): boolean {
if (breadcrumb.level !== 'error' || !breadcrumb.message) {
return false
}
return /MetaMask.*RPC Error/i.test(breadcrumb.message)
}
28 changes: 28 additions & 0 deletions apps/cowswap-frontend/src/cow-react/sentry/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { environmentName } from '@cowprotocol/common-utils'

import * as Sentry from '@sentry/react'

import { SENTRY_IGNORED_GP_QUOTE_ERRORS } from 'api/gnosisProtocol/errors/QuoteError'

import { beforeSend } from './beforeSend'

// eslint-disable-next-line @nx/enforce-module-boundaries
import pkg from '../../../package.json'

const SENTRY_DSN = process.env.REACT_APP_SENTRY_DSN
const SENTRY_TRACES_SAMPLE_RATE = process.env.REACT_APP_SENTRY_TRACES_SAMPLE_RATE

if (SENTRY_DSN) {
Sentry.init({
dsn: process.env.REACT_APP_SENTRY_DSN,
integrations: [new Sentry.BrowserTracing()],
release: 'CowSwap@v' + pkg.version,
environment: environmentName,
ignoreErrors: [...SENTRY_IGNORED_GP_QUOTE_ERRORS, `Can't find variable: bytecode`],
beforeSend,
// Set tracesSampleRate to 1.0 to capture 100%
// of transactions for performance monitoring.
// We recommend adjusting this value in production
tracesSampleRate: SENTRY_TRACES_SAMPLE_RATE ? Number(SENTRY_TRACES_SAMPLE_RATE) : 1.0,
})
}

0 comments on commit 7477bc0

Please sign in to comment.