| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| type SentryNs = typeof import('@sentry/browser'); |
| type SentryCall = (s: SentryNs) => void; |
| type SentryEvent = Parameters<SentryNs['captureEvent']>[0]; |
| type SentryLoader = () => Promise<SentryNs>; |
|
|
| let sentryNs: SentryNs | null = null; |
| let initPromise: Promise<void> | null = null; |
| let scheduled = false; |
| let queueInstalled = false; |
| |
| |
| |
| |
| let loadFailed = false; |
| const pendingCalls: SentryCall[] = []; |
| const pendingErrors: ErrorEvent[] = []; |
| const pendingRejections: PromiseRejectionEvent[] = []; |
|
|
| |
| |
| |
| const MAX_QUEUE = 50; |
| const SENTRY_AUDIT_WINDOW_DELAY_MS = 10_000; |
| const SENTRY_POST_DELAY_IDLE_TIMEOUT_MS = 2_000; |
| const SENTRY_ONERROR_MECHANISM = 'auto.browser.global_handlers.onerror'; |
| const SENTRY_ONUNHANDLEDREJECTION_MECHANISM = 'auto.browser.global_handlers.onunhandledrejection'; |
| const UNKNOWN_FUNCTION = '?'; |
|
|
| async function defaultSentryLoader(): Promise<SentryNs> { |
| const { loadAndInitSentry } = await import('./sentry-init'); |
| return loadAndInitSentry(); |
| } |
|
|
| let sentryLoader: SentryLoader = defaultSentryLoader; |
|
|
| function enqueueBounded<T>(queue: T[], item: T): void { |
| if (queue.length >= MAX_QUEUE) queue.shift(); |
| queue.push(item); |
| } |
|
|
| function onError(e: ErrorEvent): void { |
| enqueueBounded(pendingErrors, e); |
| } |
|
|
| function onUnhandledRejection(e: PromiseRejectionEvent): void { |
| enqueueBounded(pendingRejections, e); |
| } |
|
|
| function isErrorLike(value: unknown): value is Error { |
| switch (Object.prototype.toString.call(value)) { |
| case '[object Error]': |
| case '[object Exception]': |
| case '[object DOMException]': |
| case '[object WebAssembly.Exception]': |
| return true; |
| default: |
| return value instanceof Error; |
| } |
| } |
|
|
| function isPrimitive(value: unknown): boolean { |
| return value === null || (typeof value !== 'object' && typeof value !== 'function'); |
| } |
|
|
| function isPlainObject(value: unknown): value is Record<string, unknown> { |
| return Object.prototype.toString.call(value) === '[object Object]'; |
| } |
|
|
| function isEventObject(value: unknown): value is Event { |
| return typeof Event !== 'undefined' && value instanceof Event; |
| } |
|
|
| function getObjectClassName(value: object): string | undefined { |
| try { |
| return Object.getPrototypeOf(value)?.constructor?.name; |
| } catch { |
| return undefined; |
| } |
| } |
|
|
| function extractExceptionKeysForMessage(exception: Record<string, unknown>): string { |
| const keys = Object.keys(exception); |
| keys.sort(); |
| return !keys[0] ? '[object has no keys]' : keys.join(', '); |
| } |
|
|
| function getCurrentHref(): string { |
| try { |
| return typeof location !== 'undefined' ? location.href : ''; |
| } catch { |
| return ''; |
| } |
| } |
|
|
| type ErrorEventSnapshot = Pick<ErrorEvent, 'message' | 'filename' | 'lineno' | 'colno' | 'error'>; |
|
|
| function buildQueuedErrorEvent(ev: ErrorEventSnapshot): SentryEvent { |
| const message = ev.message || 'Unknown error'; |
| return { |
| message, |
| level: 'error', |
| exception: { |
| values: [{ |
| type: 'Error', |
| value: message, |
| stacktrace: { |
| frames: [{ |
| colno: ev.colno || undefined, |
| filename: ev.filename || getCurrentHref(), |
| function: UNKNOWN_FUNCTION, |
| in_app: true, |
| lineno: ev.lineno || undefined, |
| }], |
| }, |
| }], |
| }, |
| }; |
| } |
|
|
| function buildQueuedUnhandledRejectionEvent(reason: unknown): SentryEvent | null { |
| if (isErrorLike(reason)) return null; |
|
|
| if (isPrimitive(reason)) { |
| return { |
| level: 'error', |
| exception: { |
| values: [{ |
| type: 'UnhandledRejection', |
| value: `Non-Error promise rejection captured with value: ${String(reason)}`, |
| }], |
| }, |
| }; |
| } |
|
|
| if (isEventObject(reason)) { |
| const className = getObjectClassName(reason) ?? 'Event'; |
| return { |
| level: 'error', |
| exception: { |
| values: [{ |
| type: className, |
| value: `Event \`${className}\` (type=${reason.type}) captured as promise rejection`, |
| }], |
| }, |
| extra: { |
| __serialized__: { type: reason.type }, |
| }, |
| }; |
| } |
|
|
| if (isPlainObject(reason)) { |
| return { |
| level: 'error', |
| exception: { |
| values: [{ |
| type: 'UnhandledRejection', |
| value: `Object captured as promise rejection with keys: ${extractExceptionKeysForMessage(reason)}`, |
| }], |
| }, |
| extra: { |
| __serialized__: reason, |
| }, |
| }; |
| } |
|
|
| return null; |
| } |
|
|
| |
| |
| |
| |
| |
| export function installPreInitErrorQueue(): void { |
| if (queueInstalled || typeof window === 'undefined') return; |
| queueInstalled = true; |
| window.addEventListener('error', onError); |
| window.addEventListener('unhandledrejection', onUnhandledRejection); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export function enqueueSentryCall(fn: SentryCall): void { |
| if (sentryNs) { |
| try { fn(sentryNs); } catch { } |
| return; |
| } |
| if (loadFailed) return; |
| enqueueBounded(pendingCalls, fn); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| function teardownPreInitState(): void { |
| window.removeEventListener('error', onError); |
| window.removeEventListener('unhandledrejection', onUnhandledRejection); |
| pendingCalls.length = 0; |
| pendingErrors.length = 0; |
| pendingRejections.length = 0; |
| } |
|
|
| function drainQueuedSentryState(ns: SentryNs): void { |
| sentryNs = ns; |
|
|
| |
| |
| const calls = pendingCalls.splice(0, pendingCalls.length); |
| for (const fn of calls) { |
| try { fn(ns); } catch { } |
| } |
|
|
| |
| |
| |
| |
| const errs = pendingErrors.splice(0, pendingErrors.length); |
| for (const ev of errs) { |
| const hint = { |
| originalException: ev.error ?? ev.message, |
| mechanism: { type: SENTRY_ONERROR_MECHANISM, handled: false }, |
| }; |
| if (isErrorLike(ev.error)) { |
| ns.captureException(ev.error, hint); |
| } else { |
| ns.captureEvent(buildQueuedErrorEvent(ev), hint); |
| } |
| } |
|
|
| |
| |
| |
| |
| const rejs = pendingRejections.splice(0, pendingRejections.length); |
| for (const ev of rejs) { |
| const hint = { |
| originalException: ev.reason, |
| mechanism: { type: SENTRY_ONUNHANDLEDREJECTION_MECHANISM, handled: false }, |
| }; |
| const event = buildQueuedUnhandledRejectionEvent(ev.reason); |
| if (event) { |
| ns.captureEvent(event, hint); |
| } else { |
| ns.captureException(ev.reason, hint); |
| } |
| } |
|
|
| |
| |
| teardownPreInitState(); |
| } |
|
|
| async function loadAndInit(): Promise<void> { |
| const ns = await sentryLoader(); |
| drainQueuedSentryState(ns); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export function scheduleSentryInit(): Promise<void> { |
| if (initPromise) return initPromise; |
| if (typeof window === 'undefined') return Promise.resolve(); |
| if (scheduled) return Promise.resolve(); |
| scheduled = true; |
|
|
| initPromise = new Promise<void>((resolve) => { |
| const start = (): void => { |
| void loadAndInit() |
| .catch((err) => { |
| console.warn('[sentry] deferred init failed', err); |
| |
| |
| |
| |
| |
| |
| loadFailed = true; |
| teardownPreInitState(); |
| }) |
| .finally(() => resolve()); |
| }; |
| setTimeout(() => { |
| const ric = (window as unknown as { requestIdleCallback?: (cb: () => void, opts?: { timeout: number }) => number }).requestIdleCallback; |
| if (typeof ric === 'function') { |
| ric(start, { timeout: SENTRY_POST_DELAY_IDLE_TIMEOUT_MS }); |
| return; |
| } |
| start(); |
| }, SENTRY_AUDIT_WINDOW_DELAY_MS); |
| }); |
| return initPromise; |
| } |
|
|
| |
| export function _buildQueuedErrorEventForTests(ev: ErrorEventSnapshot): SentryEvent { |
| return buildQueuedErrorEvent(ev); |
| } |
|
|
| |
| export function _buildQueuedUnhandledRejectionEventForTests(reason: unknown): SentryEvent | null { |
| return buildQueuedUnhandledRejectionEvent(reason); |
| } |
|
|
| |
| export function _setSentryLoaderForTests(loader: SentryLoader): void { |
| sentryLoader = loader; |
| } |
|
|
| |
| export function _resetSentryDeferStateForTests(): void { |
| sentryNs = null; |
| initPromise = null; |
| scheduled = false; |
| queueInstalled = false; |
| loadFailed = false; |
| pendingCalls.length = 0; |
| pendingErrors.length = 0; |
| pendingRejections.length = 0; |
| sentryLoader = defaultSentryLoader; |
| } |
|
|