// Copyright (c) 2023, Compiler Explorer Authors // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are met: // // * Redistributions of source code must retain the above copyright notice, // this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above copyright // notice, this list of conditions and the following disclaimer in the // documentation and/or other materials provided with the distribution. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE // POSSIBILITY OF SUCH DAMAGE. import * as Sentry from '@sentry/browser'; import GoldenLayout from 'golden-layout'; import {parse} from '../shared/stacktrace.js'; import {serialiseState} from '../shared/url-serialization.js'; import {options} from './options.js'; import {SiteSettings} from './settings.js'; let layout: GoldenLayout; let allowSendCode: boolean; export function setSentryLayout(l: GoldenLayout) { layout = l; layout.eventHub.on('settingsChange', (newSettings: SiteSettings) => { allowSendCode = newSettings.allowStoreCodeDebug; }); Sentry.addEventProcessor(event => { if (!allowSendCode) { return event; } try { const config = layout.toConfig(); if (event.extra === undefined) { event.extra = {}; } event.extra['full_url'] = window.location.origin + window.httpRoot + '#' + serialiseState(config); } catch (e) { console.log('Error adding full_url to Sentry event', e); } return event; }); } 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({ dsn: options.sentryDsn, release: options.release, environment: options.sentryEnvironment, ignoreErrors: [ // NOTE: Monaco Editor patterns may not work reliably due to code minification // Source mapping happens AFTER beforeSend/ignoreErrors processing /this.error\(new CancellationError\(\)/, /new StandardMouseEvent\(monaco-editor/, // CEFSharp bot errors - these come from automated scanners, particularly Microsoft Outlook's // SafeLink feature that scans URLs in emails. The error format "Object Not Found Matching Id:X, // MethodName:Y, ParamCount:Z" is specific to CEFSharp (.NET Chromium wrapper). // Analysis of 76,000+ events shows 100% come from Windows + Chrome (CEFSharp's signature). // This pattern was previously seen with Id:2 (PR #7103). // See: https://github.com/DataDog/browser-sdk/issues/2715 /Object Not Found Matching Id:\d+/, /Illegal value for lineNumber/, 'SlowRequest', // Monaco Editor clipboard cancellation errors 'Canceled', ], beforeSend(event, hint) { // Filter Monaco Editor errors // // IMPORTANT: Frame-based filtering doesn't work reliably! // In beforeSend hooks, frame.filename contains minified bundle paths like: // "https://static.ce-cdn.net/vendor.v59.be68c0bf31258854d1b2.js" // // Source mapping happens AFTER beforeSend processing, which is why the // final Sentry UI shows readable paths like: // "./node_modules/monaco-editor/esm/vs/platform/clipboard/browser/clipboardService.js" // // For reliable filtering, use: // 1. ignoreErrors patterns (processed before beforeSend) // 2. Error message content (event.exception.values[0].value) // 3. Error type (event.exception.values[0].type) // // DO NOT rely on frame.filename, frame.module, or frame.function for Monaco errors! if (event.exception?.values?.[0]) { // Filter hit testing errors // See: https://github.com/microsoft/monaco-editor/issues/4527 // Uses error message content since frame data is minified if (event.exception.values[0].value?.includes('_doHitTestWithCaretPositionFromPoint')) { return null; // Don't send to Sentry } } return event; }, }); window.addEventListener('unhandledrejection', event => { let reason = 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'); }); } } export function SentryCapture(value: unknown, context?: string) { if (value instanceof Error) { if (context) { value.message += `\nSentryCapture Context: ${context}`; } Sentry.captureException(value); } else { const e = new Error(); const trace = parse(e); Sentry.captureMessage( 'Non-Error capture:\n' + (context ? `Context: ${context}\n` : '') + `Data:\n${JSON.stringify(value)}\n` + 'Trace:\n' + trace .map(frame => `${frame.functionName} ${frame.fileName}:${frame.lineNumber}:${frame.columnNumber}`) .join('\n'), ); } }