instrumentation-client.ts314 lines · main
1// This file configures the initialization of Sentry on the client.
2// The config you add here will be used whenever a user loads a page in their browser.
3// https://docs.sentry.io/platforms/javascript/guides/nextjs/
4
5import * as Sentry from '@sentry/nextjs'
6import { hasConsented } from 'common'
7import { IS_PLATFORM } from 'common/constants/environment'
8
9import { MIRRORED_BREADCRUMBS } from '@/lib/breadcrumbs'
10import { sanitizeArrayOfObjects, sanitizeUrlHashParams } from '@/lib/sanitize'
11
12const DEFAULT_ERROR_SAMPLE_RATE = 1.0
13const LOW_PRIORITY_ERROR_SAMPLE_RATE = 0.01
14const CHUNK_LOAD_ERROR_PATTERNS = [
15 /ChunkLoadError/i,
16 /Loading chunk [\d]+ failed/i,
17 /Loading CSS chunk [\d]+ failed/i,
18]
19
20// This is a workaround to ignore hCaptcha related errors.
21function isHCaptchaRelatedError(event: Sentry.Event): boolean {
22 const errors = event.exception?.values ?? []
23 for (const error of errors) {
24 if (
25 error.value?.includes('is not a function') &&
26 error.stacktrace?.frames?.some((f) => f.filename === 'api.js')
27 ) {
28 return true
29 }
30 }
31 return false
32}
33
34// Filter browser wallet extension errors (e.g., Gate.io wallet)
35// These errors come from injected wallet scripts and are not actionable
36// Examples: BRIVEN-APP-AFC, BRIVEN-APP-92A
37export function isBrowserWalletExtensionError(event: Sentry.Event): boolean {
38 const frames = event.exception?.values?.flatMap((e) => e.stacktrace?.frames || []) || []
39 return frames.some((frame) => {
40 const filename = frame.filename || frame.abs_path || ''
41 return filename.includes('gt-window-provider') || filename.includes('wallet-provider')
42 })
43}
44
45// Filter user-aborted operations (intentional cancellations)
46// These are expected when users cancel requests or navigate away
47// Examples: BRIVEN-APP-BG6, BRIVEN-APP-BG7
48export function isUserAbortedOperation(error: unknown, event: Sentry.Event): boolean {
49 const errorMessage = error instanceof Error ? error.message : ''
50 const eventMessage = event.message || ''
51 const message = errorMessage || eventMessage
52
53 return (
54 message.includes('operation was aborted') ||
55 message.includes('signal is aborted') ||
56 message.includes('manually canceled') ||
57 message.includes('AbortError')
58 )
59}
60
61// Filter cancellation promise rejections (e.g., from query cancellation)
62// These occur when operations are intentionally cancelled by the user
63// Example: BRIVEN-APP-353 (~466k events)
64export function isCancellationRejection(event: Sentry.Event): boolean {
65 const serialized = event.extra?.__serialized__ as Record<string, unknown> | undefined
66 return serialized?.type === 'cancelation'
67}
68
69// Filter challenge/captcha expired errors (user timeout)
70// These happen when users don't complete captcha in time - expected behavior
71// Example: BRIVEN-APP-ACC
72export function isChallengeExpiredError(error: unknown, event: Sentry.Event): boolean {
73 const errorMessage = error instanceof Error ? error.message : ''
74 const eventMessage = event.message || ''
75 const message = errorMessage || eventMessage
76
77 return message.includes('challenge-expired')
78}
79
80function isChunkLoadError(error: unknown, event: Sentry.Event): boolean {
81 const errorMessage = error instanceof Error ? error.message : ''
82 const eventMessage = event.message || ''
83 const exceptionMessages = event.exception?.values?.map((ex) => ex.value ?? '') ?? []
84 const combinedMessages = [errorMessage, eventMessage, ...exceptionMessages].filter(Boolean)
85
86 return CHUNK_LOAD_ERROR_PATTERNS.some((pattern) =>
87 combinedMessages.some((message) => pattern.test(message))
88 )
89}
90
91Sentry.init({
92 dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
93 ...(process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT && {
94 environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT,
95 }),
96 // Setting this option to true will print useful information to the console while you're setting up Sentry.
97 debug: false,
98
99 // Enable performance monitoring
100 tracesSampleRate: 0.02,
101
102 integrations: (() => {
103 const thirdPartyErrorFilterIntegration = (Sentry as any).thirdPartyErrorFilterIntegration
104 if (!thirdPartyErrorFilterIntegration) return []
105
106 // Tag errors whose stack trace only contains third-party frames (browser extensions,
107 // injected scripts, etc.). This uses build-time code annotation via the applicationKey
108 // in next.config.ts to reliably distinguish our code from third-party code.
109 // We use 'apply-tag' instead of 'drop' so that beforeSend can exempt error boundary
110 // crashes — these may originate in third-party code but are caused by first-party bugs.
111 return [
112 thirdPartyErrorFilterIntegration({
113 filterKeys: ['briven-studio'],
114 behaviour: 'apply-tag-if-exclusively-contains-third-party-frames',
115 }),
116 ]
117 })(),
118
119 // Only capture errors originating from our own code.
120 // This is a whitelist on the source URL in stack frames — it drops errors from
121 // browser extensions, injected scripts, third-party widgets, etc. (FE-2094)
122 allowUrls: [
123 /https?:\/\/(.*\.)?briven\.(com|co|green|io)/,
124 /app:\/\//, // Next.js rewrites source URLs to app:// with source maps
125 ],
126 beforeBreadcrumb(breadcrumb, _hint) {
127 const cleanedBreadcrumb = { ...breadcrumb }
128
129 if (cleanedBreadcrumb.category === 'navigation') {
130 if (typeof cleanedBreadcrumb.data?.from === 'string') {
131 cleanedBreadcrumb.data.from = sanitizeUrlHashParams(cleanedBreadcrumb.data.from)
132 }
133 if (typeof cleanedBreadcrumb.data?.to === 'string') {
134 cleanedBreadcrumb.data.to = sanitizeUrlHashParams(cleanedBreadcrumb.data.to)
135 }
136 }
137
138 MIRRORED_BREADCRUMBS.pushBack(cleanedBreadcrumb)
139 return cleanedBreadcrumb
140 },
141 beforeSend(event, hint) {
142 const consent = hasConsented()
143
144 if (!consent) {
145 return null
146 }
147
148 if (!IS_PLATFORM) {
149 return null
150 }
151
152 const isErrorBoundaryCrash =
153 event.tags?.globalErrorBoundary === true || event.tags?.globalErrorBoundary === 'true'
154 const isThirdPartyOnly =
155 event.tags?.third_party_code === true || event.tags?.third_party_code === 'true'
156
157 // Drop third-party-only errors UNLESS they crashed the page via the global error boundary.
158 // This preserves noise reduction for browser extensions and injected scripts,
159 // while ensuring page-crashing errors from third-party libs (caused by first-party bugs)
160 // are always reported.
161 if (isThirdPartyOnly && !isErrorBoundaryCrash) {
162 return null
163 }
164
165 // Downsample only known high-noise classes; keep all other errors at full rate.
166 const isInvalidUrlEvent = (hint.originalException as any)?.message?.includes(
167 `Failed to construct 'URL': Invalid URL`
168 )
169 const isSessionTimeoutEvent = (hint.originalException as any)?.message?.includes(
170 'Session error detected'
171 )
172 const isChunkLoadFailure = isChunkLoadError(hint.originalException, event)
173
174 const codeSampleRate =
175 isInvalidUrlEvent || isSessionTimeoutEvent || isChunkLoadFailure
176 ? LOW_PRIORITY_ERROR_SAMPLE_RATE
177 : DEFAULT_ERROR_SAMPLE_RATE
178
179 if (Math.random() > codeSampleRate) {
180 return null
181 }
182
183 event.tags = {
184 ...event.tags,
185 codeSampleRate: codeSampleRate.toString(),
186 }
187
188 if (isHCaptchaRelatedError(event)) {
189 return null
190 }
191
192 // Drop events where every exception has no stack trace — these are not debuggable.
193 // Exempt error boundary crashes: even without stack frames, a page crash is always worth reporting.
194 const exceptions = event.exception?.values ?? []
195 if (
196 !isErrorBoundaryCrash &&
197 exceptions.length > 0 &&
198 exceptions.every((ex) => !ex.stacktrace?.frames?.length)
199 ) {
200 return null
201 }
202
203 // Filter out errors like 'e._5BLbSXV[t] is not a function' or anything matching '[t] is not a function'
204 if (
205 hint.originalException instanceof Error &&
206 hint.originalException.message.includes('[t] is not a function')
207 ) {
208 return null
209 }
210
211 if (isBrowserWalletExtensionError(event)) {
212 return null
213 }
214 if (isUserAbortedOperation(hint.originalException, event)) {
215 return null
216 }
217 if (isCancellationRejection(event)) {
218 return null
219 }
220 if (isChallengeExpiredError(hint.originalException, event)) {
221 return null
222 }
223
224 if (event.breadcrumbs) {
225 event.breadcrumbs = sanitizeArrayOfObjects(event.breadcrumbs) as Sentry.Breadcrumb[]
226 }
227 return event
228 },
229 ignoreErrors: [
230 // === Monaco Editor ===
231 'ResizeObserver',
232 's.getModifierState is not a function',
233 /^Uncaught NetworkError: Failed to execute 'importScripts' on 'WorkerGlobalScope'/,
234
235 // === Browser extension errors ===
236 // Gate.io wallet
237 'shouldSetTallyForCurrentProvider is not a function',
238 // SAP browser extensions (SAP GUI, SAP Companion)
239 'sap is not defined',
240 // Non-Error objects thrown as exceptions (e.g., Event objects)
241 '[object Event]',
242
243 // === Third-party SDK errors ===
244 // stripe-js: https://github.com/stripe/stripe-js/issues/26
245 'Failed to load Stripe.js',
246 // hCaptcha
247 "undefined is not an object (evaluating 'n.chat.setReady')",
248 "undefined is not an object (evaluating 'i.chat.setReady')",
249
250 // === Next.js internals ===
251 // Ref: https://github.com/briven/briven/pull/9729
252 /The provided `href` \(\/org\/\[slug\]\/.*\) value is missing query values/,
253 // Next.js throws these during navigation, not actual errors
254 'NEXT_NOT_FOUND',
255 'NEXT_REDIRECT',
256
257 // === User input errors (not bugs) ===
258 // sql-formatter lexer on invalid SQL input
259 /^Parse error: Unexpected ".+" at line \d+ column \d+$/,
260
261 // === Network / infrastructure (not actionable on FE) ===
262 /504 Gateway Time-out/,
263 'Network request failed',
264 'Failed to fetch',
265 'Load failed',
266 'AbortError',
267 'TypeError: cancelled',
268 'TypeError: Cancelled',
269
270 // === Browser extensions & Google Translate DOM manipulation ===
271 'Node.insertBefore: Child to insert before is not a child of this node',
272 'Node.removeChild: The node to be removed is not a child of this node',
273 "NotFoundError: Failed to execute 'removeChild' on 'Node'",
274 "NotFoundError: Failed to execute 'insertBefore' on 'Node'",
275 'NotFoundError: The object can not be found here.',
276 "Cannot read properties of null (reading 'parentNode')",
277 "Cannot read properties of null (reading 'removeChild')",
278 "TypeError: can't access dead object",
279 /^NS_ERROR_/,
280
281 // === Non-Error throws (extensions, third-party libs throwing strings/objects) ===
282 'Non-Error exception captured',
283 'Non-Error promise rejection captured',
284 /^Object captured as exception with keys:/,
285
286 // === Cross-origin script errors (no useful info) ===
287 'Script error.',
288 'Script error',
289
290 // === React hydration mismatches caused by extensions modifying DOM ===
291 // Note: we only suppress the generic browser messages, NOT "Hydration failed because..."
292 // which can indicate real SSR/client mismatches in our own code.
293 /text content does not match/i,
294 /There was an error while hydrating/i,
295
296 // === Web crawler / bot errors ===
297 'instantSearchSDKJSBridgeClearHighlight',
298
299 // === Third-party library race conditions ===
300 // cmdk: useSyncExternalStore subscribe called before store context is available
301 "Cannot read properties of undefined (reading 'subscribe')",
302 "undefined is not an object (evaluating 't.subscribe')",
303
304 // === Misc known noise ===
305 'r.default.setDefaultLevel is not a function',
306 // Clipboard permission denied
307 'The request is not allowed by the user agent or the platform in the current context, possibly because the user denied permission.',
308 // Facebook pixel
309 'fb_xd_fragment',
310 ],
311})
312
313// This export will instrument router navigations, and is only relevant if you enable tracing.
314export const onRouterTransitionStart = Sentry.captureRouterTransitionStart