<commit_before>import * as Sentry from '@sentry/browser';
import { TransportOptions } from '@sentry/types';

import { ProtonConfig } from '../interfaces';
import { VPN_HOSTNAME } from '../constants';
import { getUIDHeaders } from '../fetch/headers';

const isLocalhost = (host: string) => host.startsWith('localhost');

const isProduction = (host: string) =>
    host.endsWith('.protonmail.com') || host.endsWith('.proton.me') || host === VPN_HOSTNAME;

let authHeaders: { [key: string]: string } = {};

export const setUID = (uid: string | undefined) => {
    if (!uid) {
        authHeaders = {};
        return;
    }
    authHeaders = {
        ...getUIDHeaders(uid),
    };
};

const getContentTypeHeaders = (input: RequestInfo): HeadersInit => {
    const url = input.toString();
    /**
     * The sentry library does not append the content-type header to requests. The documentation states
     * what routes accept what content-type. Those content-type headers are also expected through our sentry tunnel.
     */
    if (url.includes('/envelope/')) {
        return {
            'content-type': 'application/x-sentry-envelope',
        };
    }
    if (url.includes('/store/')) {
        return {
            'content-type': 'application/json',
        };
    }
    return {};
};

const sentryFetch = (input: RequestInfo, init?: RequestInit) => {
    return window.fetch(input, {
        ...init,
        headers: {
            ...init?.headers,
            ...getContentTypeHeaders(input),
            ...authHeaders,
        },
    });
};

class Transport extends Sentry.Transports.FetchTransport {
    constructor(options: TransportOptions) {
        super(options, sentryFetch);
    }
}

interface Arguments {
    sessionTracking?: boolean;
    config: Pick<ProtonConfig, 'SENTRY_DSN' | 'COMMIT' | 'APP_VERSION'>;
    uid?: string;
}

function main({ config: { SENTRY_DSN, COMMIT, APP_VERSION }, uid, sessionTracking = false }: Arguments) {
    const { host } = window.location;

    // No need to configure it if we don't load the DSN
    if (!SENTRY_DSN || isLocalhost(host)) {
        return;
    }

    setUID(uid);

    // Assumes SENTRY_DSN is: https://111b3eeaaec34cae8e812df705690a36@sentry/11
    // To get https://111b3eeaaec34cae8e812df705690a36@protonmail.com/api/core/v4/reports/sentry/11
    const dsn = SENTRY_DSN.replace('sentry', `${host}/api/core/v4/reports/sentry`);

    Sentry.init({
        dsn,
        release: isProduction(host) ? APP_VERSION : COMMIT,
        environment: host.split('.').splice(1).join('.'),
        normalizeDepth: 5,
        transport: Transport,
        autoSessionTracking: sessionTracking,
        beforeSend(event, hint) {
            const error = hint?.originalException;
            const stack = typeof error === 'string' ? error : error?.stack;
            // Filter out broken ferdi errors
            if (stack && stack.match(/ferdi|franz/i)) {
                return null;
            }

            // Remove the hash from the request URL and navigation breadcrumbs to avoid
            // leaking the search parameters of encrypted searches
            if (event.request && event.request.url) {
                [event.request.url] = event.request.url.split('#');
            }
            if (event.breadcrumbs) {
                event.breadcrumbs = event.breadcrumbs.map((breadcrumb) => {
                    if (breadcrumb.category === 'navigation' && breadcrumb.data) {
                        [breadcrumb.data.from] = breadcrumb.data.from.split('#');
                        [breadcrumb.data.to] = breadcrumb.data.to.split('#');
                    }
                    return breadcrumb;<commit_msg>Don't send uncaught API errors to Sentry<commit_after>import * as Sentry from '@sentry/browser';
import { TransportOptions } from '@sentry/types';

import { ProtonConfig } from '../interfaces';
import { VPN_HOSTNAME } from '../constants';
import { getUIDHeaders } from '../fetch/headers';
import { ApiError } from '../fetch/ApiError';

const isLocalhost = (host: string) => host.startsWith('localhost');

const isProduction = (host: string) =>
    host.endsWith('.protonmail.com') || host.endsWith('.proton.me') || host === VPN_HOSTNAME;

let authHeaders: { [key: string]: string } = {};

export const setUID = (uid: string | undefined) => {
    if (!uid) {
        authHeaders = {};
        return;
    }
    authHeaders = {
        ...getUIDHeaders(uid),
    };
};

const getContentTypeHeaders = (input: RequestInfo): HeadersInit => {
    const url = input.toString();
    /**
     * The sentry library does not append the content-type header to requests. The documentation states
     * what routes accept what content-type. Those content-type headers are also expected through our sentry tunnel.
     */
    if (url.includes('/envelope/')) {
        return {
            'content-type': 'application/x-sentry-envelope',
        };
    }
    if (url.includes('/store/')) {
        return {
            'content-type': 'application/json',
        };
    }
    return {};
};

const sentryFetch = (input: RequestInfo, init?: RequestInit) => {
    return window.fetch(input, {
        ...init,
        headers: {
            ...init?.headers,
            ...getContentTypeHeaders(input),
            ...authHeaders,
        },
    });
};

class Transport extends Sentry.Transports.FetchTransport {
    constructor(options: TransportOptions) {
        super(options, sentryFetch);
    }
}

interface Arguments {
    sessionTracking?: boolean;
    config: Pick<ProtonConfig, 'SENTRY_DSN' | 'COMMIT' | 'APP_VERSION'>;
    uid?: string;
}

function main({ config: { SENTRY_DSN, COMMIT, APP_VERSION }, uid, sessionTracking = false }: Arguments) {
    const { host } = window.location;

    // No need to configure it if we don't load the DSN
    if (!SENTRY_DSN || isLocalhost(host)) {
        return;
    }

    setUID(uid);

    // Assumes SENTRY_DSN is: https://111b3eeaaec34cae8e812df705690a36@sentry/11
    // To get https://<EMAIL>/api/core/v4/reports/sentry/11
    const dsn = SENTRY_DSN.replace('sentry', `${host}/api/core/v4/reports/sentry`);

    Sentry.init({
        dsn,
        release: isProduction(host) ? APP_VERSION : COMMIT,
        environment: host.split('.').splice(1).join('.'),
        normalizeDepth: 5,
        transport: Transport,
        autoSessionTracking: sessionTracking,
        beforeSend(event, hint) {
            const error = hint?.originalException;
            const stack = typeof error === 'string' ? error : error?.stack;
            // Filter out broken ferdi errors
            if (stack && stack.match(/ferdi|franz/i)) {
                return null;
            }

            // Not interested in uncaught API errors
            if (error instanceof ApiError) {
                return null;
            }

            // Remove the hash from the request URL and navigation breadcrumbs to avoid
            // leaking the search parameters of encrypted searches
            if (event.request && event.request.url) {
                [event.request.url] = event.request.url.split('#');
            }
            if (event.breadcrumbs) {
                event.breadcrumbs = event.breadcrumbs.map((breadcrumb) => {
                    if (breadcrumb.category === 'navigation' && breadcrumb.data) {
                        [breadcrumb.data.from] = breadcrumb.data.from.split('#');
                        [breadcrumb.data.to] = breadcrumb.data.to.split('#');
                    }
                    return breadcrumb;
{'fraction_non_alphanumeric': 0.1349701393497014, 'fraction_numerical': 0.0092899800928998, 'mean_word_length': 2.2736750651607296, 'pattern_counts': {'":': 0, '<': 6, '<?xml version=': 0, '>': 18, 'https://': 4, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 1, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '233673', 'n_tokens_mistral': 2230, 'n_tokens_neox': 2155, 'n_words': 511}