1 import type { BrowserOptions } from '@sentry/browser';
3 Integrations as SentryIntegrations,
8 captureMessage as sentryCaptureMessage,
9 } from '@sentry/browser';
10 import type { BrowserTransportOptions } from '@sentry/browser/types/transports/types';
12 import { Availability, AvailabilityTypes } from '@proton/utils/availability';
14 import { VPN_HOSTNAME } from '../constants';
15 import { ApiError } from '../fetch/ApiError';
16 import { getUIDHeaders } from '../fetch/headers';
17 import type { ProtonConfig } from '../interfaces';
18 import { isElectronApp } from './desktop';
20 type SentryContext = {
21 authHeaders: { [key: string]: string };
31 type SentryDenyUrls = BrowserOptions['denyUrls'];
32 type SentryIgnoreErrors = BrowserOptions['ignoreErrors'];
34 type SentryOptions = {
35 sessionTracking?: boolean;
38 sentryConfig?: SentryConfig;
39 ignore?: (config: SentryConfig) => boolean;
40 denyUrls?: SentryDenyUrls;
41 ignoreErrors?: SentryIgnoreErrors;
44 const context: SentryContext = {
49 export const setUID = (uid: string | undefined) => {
50 context.authHeaders = uid ? getUIDHeaders(uid) : {};
53 export const setSentryEnabled = (enabled: boolean) => {
54 context.enabled = enabled;
57 type FirstFetchParameter = Parameters<typeof fetch>[0];
58 export const getContentTypeHeaders = (input: FirstFetchParameter): HeadersInit => {
59 const url = input.toString();
61 * The sentry library does not append the content-type header to requests. The documentation states
62 * what routes accept what content-type. Those content-type headers are also expected through our sentry tunnel.
64 if (url.includes('/envelope/')) {
65 return { 'content-type': 'application/x-sentry-envelope' };
68 if (url.includes('/store/')) {
69 return { 'content-type': 'application/json' };
75 const sentryFetch: typeof fetch = (input, init?) => {
76 return globalThis.fetch(input, {
80 ...getContentTypeHeaders(input),
81 ...context.authHeaders,
86 const makeProtonFetchTransport = (options: BrowserTransportOptions) => {
87 return makeFetchTransport(options, sentryFetch);
90 const isLocalhost = (host: string) => host.startsWith('localhost');
91 export const isProduction = (host: string) => host.endsWith('.proton.me') || host === VPN_HOSTNAME;
93 const getDefaultSentryConfig = ({ APP_VERSION, COMMIT }: ProtonConfig): SentryConfig => {
94 const { host } = window.location;
97 release: isProduction(host) ? APP_VERSION : COMMIT,
98 environment: host.split('.').splice(1).join('.'),
102 const getDefaultDenyUrls = (): SentryDenyUrls => {
106 // Facebook flakiness
107 /graph\.facebook\.com/i,
109 /connect\.facebook\.net\/en_US\/all\.js/i,
111 /eatdifferent\.com\.woopra-ns\.com/i,
112 /static\.woopra\.com\/js\/woopra\.js/i,
116 /chrome-extension:\/\//i,
117 /moz-extension:\/\//i,
118 /webkit-masked-url:\/\//i,
120 /127\.0\.0\.1:4001\/isrunning/i, // Cacaoweb
121 /webappstoolbarba\.texthelp\.com\//i,
122 /metrics\.itunes\.apple\.com\.edgesuite\.net\//i,
126 const getDefaultIgnoreErrors = (): SentryIgnoreErrors => {
128 // Ignore random plugins/extensions
130 'canvas.contentDocument',
131 'MyApp_RemoveAllHighlights',
133 // See http://toolbar.conduit.com/Developer/HtmlAndGadget/Methods/JSInjection.aspx
135 // https://bugzilla.mozilla.org/show_bug.cgi?id=1678243
136 'XDR encoding failure',
138 'No network connection',
141 'NetworkError when attempting to fetch resource.',
142 'webkitExitFullScreen', // Bug in Firefox for iOS.
144 'InvalidStateError', // Ignore Pale Moon throwing InvalidStateError trying to use idb
145 'UnhandledRejection', // Happens too often in extensions and we have lints for that, so should be safe to ignore.
148 'TransferCancel', // User action to interrupt upload or download in Drive.
149 'UploadConflictError', // User uploading the same file again in Drive.
150 'UploadUserError', // Upload error on user's side in Drive.
151 'ValidationError', // Validation error on user's side in Drive.
152 'ChunkLoadError', // WebPack loading source code.
153 /ResizeObserver loop/, // Chromium bug https://stackoverflow.com/questions/49384120/resizeobserver-loop-limit-exceeded
154 // See: http://blog.errorception.com/2012/03/tale-of-unfindable-js-error.html
155 'originalCreateNotification',
156 'http://tt.epicplay.com',
157 "Can't find variable: ZiteReader",
158 'jigsaw is not defined',
159 'ComboSearch is not defined',
160 'http://loading.retry.widdit.com/',
163 // ISP "optimizing" proxy - `Cache-Control: no-transform` seems to reduce this. (thanks @acdha)
164 // See http://stackoverflow.com/questions/4113268/how-to-stop-javascript-injection-from-vodafone-proxy
166 'EBCallBackMessageReceived',
167 // Avast extension error
178 sessionTracking = false,
179 sentryConfig = getDefaultSentryConfig(config),
180 ignore = ({ host }) => isLocalhost(host),
181 denyUrls = getDefaultDenyUrls(),
182 ignoreErrors = getDefaultIgnoreErrors(),
184 const { SENTRY_DSN, SENTRY_DESKTOP_DSN, APP_VERSION } = config;
185 const sentryDSN = isElectronApp ? SENTRY_DESKTOP_DSN || SENTRY_DSN : SENTRY_DSN;
186 const { host, release, environment } = sentryConfig;
188 // No need to configure it if we don't load the DSN
189 if (!sentryDSN || ignore(sentryConfig)) {
195 // Assumes sentryDSN is: https://111b3eeaaec34cae8e812df705690a36@sentry/11
196 // To get https://111b3eeaaec34cae8e812df705690a36@protonmail.com/api/core/v4/reports/sentry/11
197 const dsn = sentryDSN.replace('sentry', `${host}/api/core/v4/reports/sentry`);
204 transport: makeProtonFetchTransport,
205 autoSessionTracking: sessionTracking,
206 // do not log calls to console.log, console.error, etc.
208 new SentryIntegrations.Breadcrumbs({
212 // Disable client reports. Client reports are used by sentry to retry events that failed to send on visibility change.
213 // Unfortunately Sentry does not use the custom transport for those, and thus fails to add the headers the API requires.
214 sendClientReports: false,
215 beforeSend(event, hint) {
216 const error = hint?.originalException as any;
217 const stack = typeof error === 'string' ? error : error?.stack;
218 // Filter out broken ferdi errors
219 if (stack && stack.match(/ferdi|franz/i)) {
223 // Not interested in uncaught API errors, or known errors
224 if (error instanceof ApiError || error?.trace === false) {
228 if (!context.enabled) {
232 // Remove the hash from the request URL and navigation breadcrumbs to avoid
233 // leaking the search parameters of encrypted searches
234 if (event.request && event.request.url) {
235 [event.request.url] = event.request.url.split('#');
237 // keys/all endpoint accepts Email as parameter which is PII.
238 if (event.request && event.request.url) {
239 [event.request.url] = event.request.url.toLowerCase().split('email');
241 if (event.breadcrumbs) {
242 event.breadcrumbs = event.breadcrumbs.map((breadcrumb) => {
243 if (breadcrumb.category === 'navigation' && breadcrumb.data) {
244 [breadcrumb.data.from] = breadcrumb.data.from.split('#');
245 [breadcrumb.data.to] = breadcrumb.data.to.split('#');
248 // Button titles may contain accidental PII
250 breadcrumb.category === 'ui.click' &&
251 breadcrumb.message &&
252 breadcrumb.message.startsWith('button')
254 breadcrumb.message = breadcrumb.message.replace(/\[title=".+?"\]/g, '[title="(Filtered)"]');
263 // Some ignoreErrors and denyUrls are taken from this gist: https://gist.github.com/Chocksy/e9b2cdd4afc2aadc7989762c4b8b495a
264 // This gist is suggested in the Sentry documentation: https://docs.sentry.io/clients/javascript/tips/#decluttering-sentry
269 configureScope((scope) => {
270 scope.setTag('appVersion', APP_VERSION);
274 export const traceError = (...args: Parameters<typeof captureException>) => {
275 if (!isLocalhost(window.location.host)) {
276 captureException(...args);
277 Availability.mark(AvailabilityTypes.SENTRY);
281 export const captureMessage = (...args: Parameters<typeof sentryCaptureMessage>) => {
282 if (!isLocalhost(window.location.host)) {
283 sentryCaptureMessage(...args);
287 type MailInitiative = 'drawer-security-center' | 'composer' | 'assistant';
288 export type SentryInitiative = MailInitiative;
289 type CaptureExceptionArgs = Parameters<typeof captureException>;
292 * Capture error with an additional initiative tag
296 export const traceInitiativeError = (initiative: MailInitiative, error: CaptureExceptionArgs[0]) => {
297 if (!isLocalhost(window.location.host)) {
298 captureException(error, {
307 * Capture message with an additional initiative tag
311 export const captureInitiativeMessage: (initiative: SentryInitiative, message: string) => void = (
315 captureMessage(message, {