error-reporter.ts 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. // Conditionally enable Sentry based on whether the DSN token is set
  2. import getMeta from '../utils/meta'
  3. import OError from '@overleaf/o-error'
  4. import { debugConsole } from '@/utils/debugging'
  5. import type { ErrorEvent } from '@sentry/types/types/event'
  6. const {
  7. sentryAllowedOriginRegex,
  8. sentryDsn,
  9. sentryEnvironment,
  10. sentryRelease,
  11. } = getMeta('ol-ExposedSettings')
  12. const reporterPromise = sentryDsn ? sentryReporter() : nullReporter()
  13. const isPropensityNetworkError = (err: ErrorEvent) => {
  14. const errorBreadcrumbs = err.breadcrumbs?.filter(b => b.level === 'error')
  15. if (!errorBreadcrumbs || errorBreadcrumbs.length !== 1) {
  16. // don't ignore Propensity if there are more errors to report
  17. return false
  18. }
  19. const breadcrumbUrl = errorBreadcrumbs[0]?.data?.url
  20. return Boolean(
  21. breadcrumbUrl &&
  22. [
  23. 'https://analytics.propensity.com/',
  24. 'https://analytics.propensity-abm.com/',
  25. ].some(url => breadcrumbUrl.startsWith(url))
  26. )
  27. }
  28. function sentryReporter() {
  29. return (
  30. import(/* webpackMode: "eager" */ '@sentry/browser')
  31. .then(Sentry => {
  32. let eventCount = 0
  33. Sentry.init({
  34. dsn: sentryDsn,
  35. environment: sentryEnvironment,
  36. release: sentryRelease,
  37. autoSessionTracking: false,
  38. // Ignore errors unless they come from our origins
  39. // Adapted from: https://docs.sentry.io/platforms/javascript/configuration/filtering/#decluttering-sentry
  40. allowUrls: [new RegExp(sentryAllowedOriginRegex)],
  41. ignoreErrors: [
  42. // Ignore very noisy error
  43. 'SecurityError: Permission denied to access property "pathname" on cross-origin object',
  44. // Ignore unhandled error that is "expected" - see https://github.com/overleaf/issues/issues/3321
  45. /^Missing PDF/,
  46. // Ignore "expected" error from aborted fetch - see https://github.com/overleaf/issues/issues/3321
  47. /^AbortError/,
  48. // Ignore spurious error from Ace internals - see https://github.com/overleaf/issues/issues/3321
  49. 'ResizeObserver loop limit exceeded',
  50. 'ResizeObserver loop completed with undelivered notifications.',
  51. // Microsoft Outlook SafeLink crawler
  52. // https://forum.sentry.io/t/unhandledrejection-non-error-promise-rejection-captured-with-value/14062
  53. /Non-Error promise rejection captured with value: Object Not Found Matching Id/,
  54. // Ignore CM6 error until upgraded
  55. "Cannot read properties of undefined (reading 'length')",
  56. // Ignore Angular digest iteration limit - see https://github.com/overleaf/internal/issues/15750
  57. '10 $digest() iterations reached',
  58. // Ignore a frequent unhandled promise rejection
  59. /Non-Error promise rejection captured with keys: currentTarget, detail, isTrusted, target/,
  60. /Non-Error promise rejection captured with keys: message, status/,
  61. ],
  62. denyUrls: [
  63. // Chrome extensions
  64. /extensions\//i,
  65. /^chrome:\/\//i,
  66. ],
  67. beforeSend(event) {
  68. // Limit number of events sent to Sentry to 100 events "per page load",
  69. // (i.e. the cap will be reset if the page is reloaded). This prevent
  70. // hitting their server-side event cap.
  71. eventCount++
  72. if (eventCount > 100) {
  73. return null // Block the event from sending
  74. }
  75. // Do not send events related to third party code (extensions)
  76. if (
  77. (event.extra?.arguments as { type: string }[] | undefined)?.[0]
  78. ?.type === 'UNSTABLE_editor:extensions'
  79. ) {
  80. return null // Block the event from sending
  81. }
  82. // Do not send link-sharing token to Sentry
  83. if (event.request?.headers?.Referer) {
  84. const refererUrl = new URL(event.request.headers.Referer)
  85. if (
  86. refererUrl.hostname === window.location.hostname &&
  87. refererUrl.pathname.startsWith('/read/')
  88. ) {
  89. refererUrl.pathname = '/read/'
  90. event.request.headers.Referer = refererUrl.toString()
  91. }
  92. }
  93. if (isPropensityNetworkError(event)) {
  94. return null
  95. }
  96. return event
  97. },
  98. })
  99. Sentry.setUser({ id: getMeta('ol-user_id') })
  100. const splitTestAssignments = getMeta('ol-splitTestVariants')
  101. if (splitTestAssignments) {
  102. for (const [name, value] of Object.entries(splitTestAssignments)) {
  103. // Ensure Sentry tag name is within the 32-character limit
  104. Sentry.setTag(`ol.${name}`.slice(0, 32), value.toString())
  105. }
  106. }
  107. return Sentry
  108. })
  109. // If Sentry fails to load, use the null reporter instead
  110. .catch(error => {
  111. debugConsole.error(error)
  112. return nullReporter()
  113. })
  114. )
  115. }
  116. function nullReporter() {
  117. return Promise.resolve({
  118. captureException: debugConsole.error,
  119. captureMessage: debugConsole.error,
  120. })
  121. }
  122. // https://develop.sentry.dev/sdk/data-model/event-payloads/contexts/
  123. // https://docs.sentry.io/platforms/javascript/enriching-events/context/#passing-context-directly
  124. type Options = {
  125. tags?: Record<string, any>
  126. extra?: Record<string, any>
  127. }
  128. export function captureException(err: Error, options?: Options) {
  129. options = options || {}
  130. const extra = Object.assign(OError.getFullInfo(err), options.extra || {})
  131. const fullStack = OError.getFullStack(err)
  132. if (err.stack !== fullStack) {
  133. // Attach tracebacks from OError.tag() and OError.cause.
  134. extra.fullStack = fullStack
  135. }
  136. reporterPromise.then(reporter =>
  137. reporter.captureException(err, {
  138. ...options,
  139. extra,
  140. })
  141. )
  142. }