All files / app/common/error SentryErrorLogger.ts

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 15581x 81x     81x   81x 81x   81x               81x       13x         13x 13x 13x   13x 13x   13x                                       81x     7x     6x   6x 6x   6x 6x     6x   6x       6x   6x       81x 6x   1x     2x             3x                       81x         4x 1x     3x 3x 1x     3x           13x 4x 4x 3x     1x           13x 2x       2x       2x 1x       2x   81x  
import { captureException, init, withScope, BrowserOptions, Event, Integrations, Severity, StackFrame } from '@sentry/browser';
import { RewriteFrames } from '@sentry/integrations';
import { EventHint, Exception } from '@sentry/types';
 
import computeErrorCode from './computeErrorCode';
import ConsoleErrorLogger from './ConsoleErrorLogger';
import ErrorLogger, { ErrorLevelType, ErrorMeta, ErrorTags } from './ErrorLogger';
import NoopErrorLogger from './NoopErrorLogger';
 
const FILENAME_PREFIX = 'app://';
 
export interface SentryErrorLoggerOptions {
    consoleLogger?: ConsoleErrorLogger;
    errorTypes?: string[];
    publicPath?: string;
}
 
export default class SentryErrorLogger implements ErrorLogger {
    private consoleLogger: ErrorLogger;
    private publicPath: string;
 
    constructor(
        config: BrowserOptions,
        options?: SentryErrorLoggerOptions
    ) {
        const {
            consoleLogger = new NoopErrorLogger(),
            publicPath = '',
        } = options || {};
 
        this.consoleLogger = consoleLogger;
        this.publicPath = publicPath;
 
        init({
            beforeSend: this.handleBeforeSend,
            blacklistUrls: [
                ...(config.blacklistUrls || []),
                'polyfill~checkout',
                'sentry~checkout',
            ],
            integrations: [
                new Integrations.GlobalHandlers({
                    onerror: false,
                    onunhandledrejection: true,
                }),
                new RewriteFrames({
                    iteratee: this.handleRewriteFrame,
                }),
            ],
            ...config,
        });
    }
 
    log(
        error: Error,
        tags?: ErrorTags,
        level: ErrorLevelType = ErrorLevelType.Error,
        payload?: ErrorMeta
    ): void {
        this.consoleLogger.log(error, tags, level);
 
        withScope(scope => {
            const { errorCode = computeErrorCode(error) } = tags || {};
 
            Eif (errorCode) {
                scope.setTags({ errorCode });
            }
 
            scope.setLevel(this.mapToSentryLevel(level));
 
            Iif (payload) {
                scope.setExtras(payload);
            }
 
            scope.setFingerprint(['{{ default }}']);
 
            captureException(error);
        });
    }
 
    private mapToSentryLevel(level: ErrorLevelType): Severity {
        switch (level) {
        case ErrorLevelType.Info:
            return Severity.Info;
 
        case ErrorLevelType.Warning:
            return Severity.Warning;
 
        case ErrorLevelType.Debug:
            return Severity.Debug;
 
        case ErrorLevelType.Error:
        default:
            return Severity.Error;
        }
    }
 
    /**
     * Ignore exceptions that don't have a stacktrace at all, or have a stacktrace that references files external to
     * this app. For example, if the exception is caused by a piece of third party code, one of the frames in the
     * stacktrace will reference a file that is not a part of the app. This behaviour is different to the whitelist
     * config provided by Sentry, as the latter only examines the topmost frame in the stacktrace. The config is not
     * sufficient for us because some stores have customisation code built on top of our code, resulting in a stacktrace
     * whose topmost frame is ours but frames below it are not.
     */
    private shouldReportExceptions(exceptions: Exception[], originalException: Error | string | null): boolean {
        // Ignore exceptions that are not an instance of Error because they are most likely not thrown by our own code,
        // as we have a lint rule that prevents us from doing so. Although these exceptions don't actually have a
        // stacktrace, meaning that the condition below should theoretically cover the scenario, but we still need this
        // condition because Sentry client creates a "synthentic" stacktrace for them using the information it has.
        if (!exceptions?.length || !(originalException instanceof Error)) {
            return false;
        }
 
        return exceptions.every(exception => {
            if (!exception.stacktrace?.frames?.length) {
                return false;
            }
 
            return exception.stacktrace?.frames?.every(frame =>
                frame.filename?.startsWith(FILENAME_PREFIX)
            );
        });
    }
 
    private handleBeforeSend: (event: Event, hint?: EventHint) => Event | null = (event, hint) => {
        Eif (event.exception) {
            if (!this.shouldReportExceptions(event.exception.values ?? [], hint?.originalException ?? null)) {
                return null;
            }
 
            return event;
        }
 
        return event;
    };
 
    private handleRewriteFrame: (frame: StackFrame) => StackFrame = frame => {
        Eif (this.publicPath && frame.filename) {
            // We want to remove the base path of the filename, otherwise we
            // will need to specify it when we upload the sourcemaps so that the
            // filenames can match up.
            const filename = frame.filename.replace(new RegExp(`^${this.publicPath}\/?`), '');
 
            // `frame` needs to be modified in-place (based on the example in
            // their documentation).
            if (filename !== frame.filename) {
                frame.filename = `${FILENAME_PREFIX}/${filename}`;
            }
        }
 
        return frame;
    };
}