mirror of
https://github.com/compiler-explorer/compiler-explorer.git
synced 2025-12-27 09:23:52 -05:00
Extract diagnostic info from Safari CustomEvent rejections (#8173)
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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');
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user