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) =>
92 host.endsWith('.proton.me') || host.endsWith('.protonvpn.com') || host === VPN_HOSTNAME;
94 const getDefaultSentryConfig = ({ APP_VERSION, COMMIT }: ProtonConfig): SentryConfig => {
95 const { host } = window.location;
98 release: isProduction(host) ? APP_VERSION : COMMIT,
99 environment: host.split('.').splice(1).join('.'),
103 const getDefaultDenyUrls = (): SentryDenyUrls => {
107 // Facebook flakiness
108 /graph\.facebook\.com/i,
110 /connect\.facebook\.net\/en_US\/all\.js/i,
112 /eatdifferent\.com\.woopra-ns\.com/i,
113 /static\.woopra\.com\/js\/woopra\.js/i,
117 /chrome-extension:\/\//i,
118 /moz-extension:\/\//i,
119 /webkit-masked-url:\/\//i,
121 /127\.0\.0\.1:4001\/isrunning/i, // Cacaoweb
122 /webappstoolbarba\.texthelp\.com\//i,
123 /metrics\.itunes\.apple\.com\.edgesuite\.net\//i,
127 const getDefaultIgnoreErrors = (): SentryIgnoreErrors => {
129 // Ignore random plugins/extensions
131 'canvas.contentDocument',
132 'MyApp_RemoveAllHighlights',
134 // See http://toolbar.conduit.com/Developer/HtmlAndGadget/Methods/JSInjection.aspx
136 // https://bugzilla.mozilla.org/show_bug.cgi?id=1678243
137 'XDR encoding failure',
139 'No network connection',
142 'NetworkError when attempting to fetch resource.',
143 'webkitExitFullScreen', // Bug in Firefox for iOS.
145 'InvalidStateError', // Ignore Pale Moon throwing InvalidStateError trying to use idb
146 'UnhandledRejection', // Happens too often in extensions and we have lints for that, so should be safe to ignore.
149 'TransferCancel', // User action to interrupt upload or download in Drive.
150 'UploadConflictError', // User uploading the same file again in Drive.
151 'UploadUserError', // Upload error on user's side in Drive.
152 'ValidationError', // Validation error on user's side in Drive.
153 'ChunkLoadError', // WebPack loading source code.
154 /ResizeObserver loop/, // Chromium bug https://stackoverflow.com/questions/49384120/resizeobserver-loop-limit-exceeded
155 // See: http://blog.errorception.com/2012/03/tale-of-unfindable-js-error.html
156 'originalCreateNotification',
157 'http://tt.epicplay.com',
158 "Can't find variable: ZiteReader",
159 'jigsaw is not defined',
160 'ComboSearch is not defined',
161 'http://loading.retry.widdit.com/',
164 // ISP "optimizing" proxy - `Cache-Control: no-transform` seems to reduce this. (thanks @acdha)
165 // See http://stackoverflow.com/questions/4113268/how-to-stop-javascript-injection-from-vodafone-proxy
167 'EBCallBackMessageReceived',
168 // Avast extension error
179 sessionTracking = false,
180 sentryConfig = getDefaultSentryConfig(config),
181 ignore = ({ host }) => isLocalhost(host),
182 denyUrls = getDefaultDenyUrls(),
183 ignoreErrors = getDefaultIgnoreErrors(),
185 const { SENTRY_DSN, SENTRY_DESKTOP_DSN, APP_VERSION } = config;
186 const sentryDSN = isElectronApp ? SENTRY_DESKTOP_DSN || SENTRY_DSN : SENTRY_DSN;
187 const { host, release, environment } = sentryConfig;
189 // No need to configure it if we don't load the DSN
190 if (!sentryDSN || ignore(sentryConfig)) {
196 // Assumes sentryDSN is: https://111b3eeaaec34cae8e812df705690a36@sentry/11
197 // To get https://111b3eeaaec34cae8e812df705690a36@protonmail.com/api/core/v4/reports/sentry/11
198 const dsn = sentryDSN.replace('sentry', `${host}/api/core/v4/reports/sentry`);
205 transport: makeProtonFetchTransport,
206 autoSessionTracking: sessionTracking,
207 // do not log calls to console.log, console.error, etc.
209 new SentryIntegrations.Breadcrumbs({
213 // Disable client reports. Client reports are used by sentry to retry events that failed to send on visibility change.
214 // Unfortunately Sentry does not use the custom transport for those, and thus fails to add the headers the API requires.
215 sendClientReports: false,
216 beforeSend(event, hint) {
217 const error = hint?.originalException as any;
218 const stack = typeof error === 'string' ? error : error?.stack;
219 // Filter out broken ferdi errors
220 if (stack && stack.match(/ferdi|franz/i)) {
224 // Not interested in uncaught API errors, or known errors
225 if (error instanceof ApiError || error?.trace === false) {
229 if (!context.enabled) {
233 // Remove the hash from the request URL and navigation breadcrumbs to avoid
234 // leaking the search parameters of encrypted searches
235 if (event.request && event.request.url) {
236 [event.request.url] = event.request.url.split('#');
238 // keys/all endpoint accepts Email as parameter which is PII.
239 if (event.request && event.request.url) {
240 [event.request.url] = event.request.url.toLowerCase().split('email');
242 if (event.breadcrumbs) {
243 event.breadcrumbs = event.breadcrumbs.map((breadcrumb) => {
244 if (breadcrumb.category === 'navigation' && breadcrumb.data) {
245 [breadcrumb.data.from] = breadcrumb.data.from.split('#');
246 [breadcrumb.data.to] = breadcrumb.data.to.split('#');
249 // Button titles may contain accidental PII
251 breadcrumb.category === 'ui.click' &&
252 breadcrumb.message &&
253 breadcrumb.message.startsWith('button')
255 breadcrumb.message = breadcrumb.message.replace(/\[title=".+?"\]/g, '[title="(Filtered)"]');
264 // Some ignoreErrors and denyUrls are taken from this gist: https://gist.github.com/Chocksy/e9b2cdd4afc2aadc7989762c4b8b495a
265 // This gist is suggested in the Sentry documentation: https://docs.sentry.io/clients/javascript/tips/#decluttering-sentry
270 configureScope((scope) => {
271 scope.setTag('appVersion', APP_VERSION);
275 export const traceError = (...args: Parameters<typeof captureException>) => {
276 if (!isLocalhost(window.location.host)) {
277 captureException(...args);
278 Availability.mark(AvailabilityTypes.SENTRY);
282 export const captureMessage = (...args: Parameters<typeof sentryCaptureMessage>) => {
283 if (!isLocalhost(window.location.host)) {
284 sentryCaptureMessage(...args);
288 type MailInitiative = 'drawer-security-center' | 'composer' | 'assistant' | 'mail-onboarding';
289 type CommonInitiatives = 'post-subscription';
290 export type SentryInitiative = MailInitiative;
291 type CaptureExceptionArgs = Parameters<typeof captureException>;
294 * Capture error with an additional initiative tag
298 export const traceInitiativeError = (
299 initiative: MailInitiative | CommonInitiatives,
300 error: CaptureExceptionArgs[0]
302 if (!isLocalhost(window.location.host)) {
303 captureException(error, {
312 * Capture message with an additional initiative tag
316 export const captureInitiativeMessage: (initiative: SentryInitiative, message: string) => void = (
320 captureMessage(message, {