| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314 |
- // This file configures the initialization of Sentry on the client.
- // The config you add here will be used whenever a user loads a page in their browser.
- // https://docs.sentry.io/platforms/javascript/guides/nextjs/
- import * as Sentry from '@sentry/nextjs'
- import { hasConsented } from 'common'
- import { IS_PLATFORM } from 'common/constants/environment'
- import { MIRRORED_BREADCRUMBS } from '@/lib/breadcrumbs'
- import { sanitizeArrayOfObjects, sanitizeUrlHashParams } from '@/lib/sanitize'
- const DEFAULT_ERROR_SAMPLE_RATE = 1.0
- const LOW_PRIORITY_ERROR_SAMPLE_RATE = 0.01
- const CHUNK_LOAD_ERROR_PATTERNS = [
- /ChunkLoadError/i,
- /Loading chunk [\d]+ failed/i,
- /Loading CSS chunk [\d]+ failed/i,
- ]
- // This is a workaround to ignore hCaptcha related errors.
- function isHCaptchaRelatedError(event: Sentry.Event): boolean {
- const errors = event.exception?.values ?? []
- for (const error of errors) {
- if (
- error.value?.includes('is not a function') &&
- error.stacktrace?.frames?.some((f) => f.filename === 'api.js')
- ) {
- return true
- }
- }
- return false
- }
- // Filter browser wallet extension errors (e.g., Gate.io wallet)
- // These errors come from injected wallet scripts and are not actionable
- // Examples: BRIVEN-APP-AFC, BRIVEN-APP-92A
- export function isBrowserWalletExtensionError(event: Sentry.Event): boolean {
- const frames = event.exception?.values?.flatMap((e) => e.stacktrace?.frames || []) || []
- return frames.some((frame) => {
- const filename = frame.filename || frame.abs_path || ''
- return filename.includes('gt-window-provider') || filename.includes('wallet-provider')
- })
- }
- // Filter user-aborted operations (intentional cancellations)
- // These are expected when users cancel requests or navigate away
- // Examples: BRIVEN-APP-BG6, BRIVEN-APP-BG7
- export function isUserAbortedOperation(error: unknown, event: Sentry.Event): boolean {
- const errorMessage = error instanceof Error ? error.message : ''
- const eventMessage = event.message || ''
- const message = errorMessage || eventMessage
- return (
- message.includes('operation was aborted') ||
- message.includes('signal is aborted') ||
- message.includes('manually canceled') ||
- message.includes('AbortError')
- )
- }
- // Filter cancellation promise rejections (e.g., from query cancellation)
- // These occur when operations are intentionally cancelled by the user
- // Example: BRIVEN-APP-353 (~466k events)
- export function isCancellationRejection(event: Sentry.Event): boolean {
- const serialized = event.extra?.__serialized__ as Record<string, unknown> | undefined
- return serialized?.type === 'cancelation'
- }
- // Filter challenge/captcha expired errors (user timeout)
- // These happen when users don't complete captcha in time - expected behavior
- // Example: BRIVEN-APP-ACC
- export function isChallengeExpiredError(error: unknown, event: Sentry.Event): boolean {
- const errorMessage = error instanceof Error ? error.message : ''
- const eventMessage = event.message || ''
- const message = errorMessage || eventMessage
- return message.includes('challenge-expired')
- }
- function isChunkLoadError(error: unknown, event: Sentry.Event): boolean {
- const errorMessage = error instanceof Error ? error.message : ''
- const eventMessage = event.message || ''
- const exceptionMessages = event.exception?.values?.map((ex) => ex.value ?? '') ?? []
- const combinedMessages = [errorMessage, eventMessage, ...exceptionMessages].filter(Boolean)
- return CHUNK_LOAD_ERROR_PATTERNS.some((pattern) =>
- combinedMessages.some((message) => pattern.test(message))
- )
- }
- Sentry.init({
- dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
- ...(process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT && {
- environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT,
- }),
- // Setting this option to true will print useful information to the console while you're setting up Sentry.
- debug: false,
- // Enable performance monitoring
- tracesSampleRate: 0.02,
- integrations: (() => {
- const thirdPartyErrorFilterIntegration = (Sentry as any).thirdPartyErrorFilterIntegration
- if (!thirdPartyErrorFilterIntegration) return []
- // Tag errors whose stack trace only contains third-party frames (browser extensions,
- // injected scripts, etc.). This uses build-time code annotation via the applicationKey
- // in next.config.ts to reliably distinguish our code from third-party code.
- // We use 'apply-tag' instead of 'drop' so that beforeSend can exempt error boundary
- // crashes — these may originate in third-party code but are caused by first-party bugs.
- return [
- thirdPartyErrorFilterIntegration({
- filterKeys: ['briven-studio'],
- behaviour: 'apply-tag-if-exclusively-contains-third-party-frames',
- }),
- ]
- })(),
- // Only capture errors originating from our own code.
- // This is a whitelist on the source URL in stack frames — it drops errors from
- // browser extensions, injected scripts, third-party widgets, etc. (FE-2094)
- allowUrls: [
- /https?:\/\/(.*\.)?briven\.(com|co|green|io)/,
- /app:\/\//, // Next.js rewrites source URLs to app:// with source maps
- ],
- beforeBreadcrumb(breadcrumb, _hint) {
- const cleanedBreadcrumb = { ...breadcrumb }
- if (cleanedBreadcrumb.category === 'navigation') {
- if (typeof cleanedBreadcrumb.data?.from === 'string') {
- cleanedBreadcrumb.data.from = sanitizeUrlHashParams(cleanedBreadcrumb.data.from)
- }
- if (typeof cleanedBreadcrumb.data?.to === 'string') {
- cleanedBreadcrumb.data.to = sanitizeUrlHashParams(cleanedBreadcrumb.data.to)
- }
- }
- MIRRORED_BREADCRUMBS.pushBack(cleanedBreadcrumb)
- return cleanedBreadcrumb
- },
- beforeSend(event, hint) {
- const consent = hasConsented()
- if (!consent) {
- return null
- }
- if (!IS_PLATFORM) {
- return null
- }
- const isErrorBoundaryCrash =
- event.tags?.globalErrorBoundary === true || event.tags?.globalErrorBoundary === 'true'
- const isThirdPartyOnly =
- event.tags?.third_party_code === true || event.tags?.third_party_code === 'true'
- // Drop third-party-only errors UNLESS they crashed the page via the global error boundary.
- // This preserves noise reduction for browser extensions and injected scripts,
- // while ensuring page-crashing errors from third-party libs (caused by first-party bugs)
- // are always reported.
- if (isThirdPartyOnly && !isErrorBoundaryCrash) {
- return null
- }
- // Downsample only known high-noise classes; keep all other errors at full rate.
- const isInvalidUrlEvent = (hint.originalException as any)?.message?.includes(
- `Failed to construct 'URL': Invalid URL`
- )
- const isSessionTimeoutEvent = (hint.originalException as any)?.message?.includes(
- 'Session error detected'
- )
- const isChunkLoadFailure = isChunkLoadError(hint.originalException, event)
- const codeSampleRate =
- isInvalidUrlEvent || isSessionTimeoutEvent || isChunkLoadFailure
- ? LOW_PRIORITY_ERROR_SAMPLE_RATE
- : DEFAULT_ERROR_SAMPLE_RATE
- if (Math.random() > codeSampleRate) {
- return null
- }
- event.tags = {
- ...event.tags,
- codeSampleRate: codeSampleRate.toString(),
- }
- if (isHCaptchaRelatedError(event)) {
- return null
- }
- // Drop events where every exception has no stack trace — these are not debuggable.
- // Exempt error boundary crashes: even without stack frames, a page crash is always worth reporting.
- const exceptions = event.exception?.values ?? []
- if (
- !isErrorBoundaryCrash &&
- exceptions.length > 0 &&
- exceptions.every((ex) => !ex.stacktrace?.frames?.length)
- ) {
- return null
- }
- // Filter out errors like 'e._5BLbSXV[t] is not a function' or anything matching '[t] is not a function'
- if (
- hint.originalException instanceof Error &&
- hint.originalException.message.includes('[t] is not a function')
- ) {
- return null
- }
- if (isBrowserWalletExtensionError(event)) {
- return null
- }
- if (isUserAbortedOperation(hint.originalException, event)) {
- return null
- }
- if (isCancellationRejection(event)) {
- return null
- }
- if (isChallengeExpiredError(hint.originalException, event)) {
- return null
- }
- if (event.breadcrumbs) {
- event.breadcrumbs = sanitizeArrayOfObjects(event.breadcrumbs) as Sentry.Breadcrumb[]
- }
- return event
- },
- ignoreErrors: [
- // === Monaco Editor ===
- 'ResizeObserver',
- 's.getModifierState is not a function',
- /^Uncaught NetworkError: Failed to execute 'importScripts' on 'WorkerGlobalScope'/,
- // === Browser extension errors ===
- // Gate.io wallet
- 'shouldSetTallyForCurrentProvider is not a function',
- // SAP browser extensions (SAP GUI, SAP Companion)
- 'sap is not defined',
- // Non-Error objects thrown as exceptions (e.g., Event objects)
- '[object Event]',
- // === Third-party SDK errors ===
- // stripe-js: https://github.com/stripe/stripe-js/issues/26
- 'Failed to load Stripe.js',
- // hCaptcha
- "undefined is not an object (evaluating 'n.chat.setReady')",
- "undefined is not an object (evaluating 'i.chat.setReady')",
- // === Next.js internals ===
- // Ref: https://github.com/briven/briven/pull/9729
- /The provided `href` \(\/org\/\[slug\]\/.*\) value is missing query values/,
- // Next.js throws these during navigation, not actual errors
- 'NEXT_NOT_FOUND',
- 'NEXT_REDIRECT',
- // === User input errors (not bugs) ===
- // sql-formatter lexer on invalid SQL input
- /^Parse error: Unexpected ".+" at line \d+ column \d+$/,
- // === Network / infrastructure (not actionable on FE) ===
- /504 Gateway Time-out/,
- 'Network request failed',
- 'Failed to fetch',
- 'Load failed',
- 'AbortError',
- 'TypeError: cancelled',
- 'TypeError: Cancelled',
- // === Browser extensions & Google Translate DOM manipulation ===
- 'Node.insertBefore: Child to insert before is not a child of this node',
- 'Node.removeChild: The node to be removed is not a child of this node',
- "NotFoundError: Failed to execute 'removeChild' on 'Node'",
- "NotFoundError: Failed to execute 'insertBefore' on 'Node'",
- 'NotFoundError: The object can not be found here.',
- "Cannot read properties of null (reading 'parentNode')",
- "Cannot read properties of null (reading 'removeChild')",
- "TypeError: can't access dead object",
- /^NS_ERROR_/,
- // === Non-Error throws (extensions, third-party libs throwing strings/objects) ===
- 'Non-Error exception captured',
- 'Non-Error promise rejection captured',
- /^Object captured as exception with keys:/,
- // === Cross-origin script errors (no useful info) ===
- 'Script error.',
- 'Script error',
- // === React hydration mismatches caused by extensions modifying DOM ===
- // Note: we only suppress the generic browser messages, NOT "Hydration failed because..."
- // which can indicate real SSR/client mismatches in our own code.
- /text content does not match/i,
- /There was an error while hydrating/i,
- // === Web crawler / bot errors ===
- 'instantSearchSDKJSBridgeClearHighlight',
- // === Third-party library race conditions ===
- // cmdk: useSyncExternalStore subscribe called before store context is available
- "Cannot read properties of undefined (reading 'subscribe')",
- "undefined is not an object (evaluating 't.subscribe')",
- // === Misc known noise ===
- 'r.default.setDefaultLevel is not a function',
- // Clipboard permission denied
- 'The request is not allowed by the user agent or the platform in the current context, possibly because the user denied permission.',
- // Facebook pixel
- 'fb_xd_fragment',
- ],
- })
- // This export will instrument router navigations, and is only relevant if you enable tracing.
- export const onRouterTransitionStart = Sentry.captureRouterTransitionStart
|