From 5ca970efcef4dca516001e1bb5633e9f74797984 Mon Sep 17 00:00:00 2001 From: Matt Godbolt Date: Tue, 7 Oct 2025 15:44:15 -0500 Subject: [PATCH] Extract diagnostic info from Safari CustomEvent rejections (#8173) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #8172 Fixes [COMPILER-EXPLORER-DXM](https://compiler-explorer.sentry.io/issues/COMPILER-EXPLORER-DXM) ## Problem Sentry issue COMPILER-EXPLORER-DXM has accumulated **7,018+ occurrences** with essentially no diagnostic information: - Error: `CustomEvent: Event 'CustomEvent' (type=unhandledrejection) captured as promise rejection` - No stacktrace - Safari-specific (Safari 26.0 on macOS) - No actionable information to debug the issue ## Root Cause Safari sometimes rejects promises with `CustomEvent`/`Event` objects instead of Error objects. Our `unhandledrejection` handler in `static/sentry.ts:110-122` converts non-Error rejections to Error objects using: ```typescript const errorMessage = typeof reason === 'string' ? reason : `Non-Error rejection: ${JSON.stringify(reason)}`; ``` When `JSON.stringify()` is called on Event objects, it returns `{}` because Event properties are **non-enumerable**. This loses all valuable diagnostic information like: - `event.type` - the event type - `event.target` - what triggered the event - `event.detail` - custom data for CustomEvents ## Evidence - [Sentry JavaScript SDK Issue #2210](https://github.com/getsentry/sentry-javascript/issues/2210) - Documents the exact same problem - [WebKit Bug #150358](https://bugs.webkit.org/show_bug.cgi?id=150358) - Promise rejection event handling - Multiple Safari 16.3+ reports of CustomEvent rejections ## Solution Extract meaningful properties from Event/CustomEvent objects **before** stringifying: 1. **Add type guard**: `isEventLike()` to detect Event/CustomEvent objects 2. **Extract properties**: `formatEventRejection()` to get `type`, `target`, `detail` 3. **Cleaner code**: Refactor with ternary chain and modern TypeScript patterns 4. **Better diagnostics**: Use `Object.assign()` for type-safe property addition ### Before ``` Non-Error rejection: {} ``` ### After ``` Event rejection: type="unhandledrejection", target="Window", detail={...} ``` ## Impact This will allow us to: 1. ✅ Identify the actual source of these Safari rejections 2. ✅ Determine if they're from CE code, third-party libraries, or browser extensions 3. ✅ Decide if they need fixing or should be filtered out 4. ✅ Get actionable diagnostic information instead of empty objects ## Test Plan - ✅ TypeScript type checking passes - ✅ Linter passes with auto-formatting - ✅ Related tests pass - ✅ Pre-commit hooks pass - Manual testing: Wait for Safari users to trigger these errors and verify we now get useful diagnostic info in Sentry ## Code Review Notes The fix operates at the **right layer** - transforming Events into meaningful Error messages at the point of rejection, before Sentry sees them. This is different from Sentry's `ExtraErrorData` integration which operates on already-serialized errors. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude --- static/sentry.ts | 40 +++++++++++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 7 deletions(-) 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'); }); }