diff --git a/static/sentry.ts b/static/sentry.ts index 3f5fdc808..e7bd06777 100644 --- a/static/sentry.ts +++ b/static/sentry.ts @@ -55,6 +55,25 @@ export function setSentryLayout(l: GoldenLayout) { }); } +function isEventLike(value: unknown): value is Event { + return value instanceof Event || value?.constructor?.name === 'Event' || value?.constructor?.name === 'CustomEvent'; +} + +function formatEventRejection(evt: Event): string { + const targetName = evt.target?.constructor?.name ?? 'unknown'; + let message = `Event rejection: type="${evt.type}", target="${targetName}"`; + + if ('detail' in evt && evt.detail !== undefined) { + try { + message += `, detail=${JSON.stringify(evt.detail)}`; + } catch { + message += ', detail=[Unserializable]'; + } + } + + return message; +} + export function SetupSentry() { if (options.statusTrackingEnabled && options.sentryDsn) { Sentry.init({ @@ -108,16 +127,23 @@ export function SetupSentry() { }, }); window.addEventListener('unhandledrejection', event => { - // Convert non-Error rejection reasons to Error objects let reason = event.reason; - if (!(reason instanceof Error)) { - const errorMessage = - typeof reason === 'string' ? reason : `Non-Error rejection: ${JSON.stringify(reason)}`; - reason = new Error(errorMessage); - // Preserve original reason for debugging - (reason as any).originalReason = event.reason; + if (!(reason instanceof Error)) { + // Safari sometimes rejects promises with CustomEvent/Event objects. + // Extract useful properties instead of stringifying to empty object. + // See: https://github.com/compiler-explorer/compiler-explorer/issues/8172 + // Related: https://github.com/getsentry/sentry-javascript/issues/2210 + const errorMessage = + typeof reason === 'string' + ? reason + : isEventLike(reason) + ? formatEventRejection(reason) + : `Non-Error rejection: ${JSON.stringify(reason)}`; + + reason = Object.assign(new Error(errorMessage), {originalReason: event.reason}); } + SentryCapture(reason, 'Unhandled Promise Rejection'); }); }