instrumentation-client.ts 12 KB

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