xref: /expo/docs/common/sentry-utilities.ts (revision 49009baa)
1import { Event } from '@sentry/types';
2/*
3 * Error logging filtering - prevent users from submitting errors we do not care about,
4 * eg: specific error messages that are caused by extensions or other scripts
5 * out of our control, or the same error being reported many times.
6 */
7
8// These exact error messages may be different depending on the browser!
9const ERRORS_TO_DISCARD = [
10  // Filter out errors from extensions
11  'chrome-extension://',
12  'moz-extension://',
13  'safari-extension://',
14  // This error only appears in Safari
15  "undefined is not an object (evaluating 'window.__pad.performLoop')",
16  // This error appears in Firefox related to local storage and flooded our Sentry bandwidth
17  'SecurityError: The operation is insecure.',
18];
19
20const REPORTED_ERRORS_KEY = 'sentry:reportedErrors';
21const TIMESTAMP_KEY = 'sentry:errorReportingInit';
22const ONE_DAY_MS = 24 * 60 * 60 * 1000;
23const HALF_HOUR_MS = 0.5 * 60 * 60 * 1000;
24
25export function preprocessSentryError(event: Event) {
26  const message = getMessage(event);
27
28  // Check if it's rate limited to avoid sending the same error over and over
29  if (isRateLimited(message || 'empty')) {
30    return null;
31  }
32
33  // If we don't know about this particular type of event then just pass it along
34  if (!message) {
35    return event;
36  }
37
38  // Discard any errors that we know we do not care about
39  if (ERRORS_TO_DISCARD.includes(message)) {
40    return null;
41  }
42
43  // Only attempt to check against cached reported messages if we have localStorage
44  try {
45    if (isLocalStorageAvailable()) {
46      // Clear the saved error messages every day
47      maybeResetReportedErrorsCache();
48
49      // Bail out if we have reported the error already
50      if (userHasReportedErrorMessage(message)) {
51        return null;
52      }
53
54      saveReportedErrorMessage(message);
55    }
56  } catch {
57    // Ignore the local storage exceptions
58    return event;
59  }
60
61  return event;
62}
63
64// https://gist.github.com/paulirish/5558557
65function isLocalStorageAvailable(): boolean {
66  try {
67    if (!window.localStorage || localStorage === null || typeof localStorage === 'undefined') {
68      return false;
69    }
70
71    localStorage.setItem('localStorage:test', 'value');
72    if (localStorage.getItem('localStorage:test') !== 'value') {
73      return false;
74    }
75    localStorage.removeItem('localStorage:test');
76    return true;
77  } catch {
78    return false;
79  }
80}
81
82// https://github.com/getsentry/sentry-javascript/issues/435
83const rateLimiter: Record<string, number> = {};
84function isRateLimited(message: string) {
85  if (rateLimiter[message] && rateLimiter[message] > Date.now()) {
86    return true;
87  }
88
89  rateLimiter[message] = Date.now() + HALF_HOUR_MS;
90  return false;
91}
92
93// Extract a stable event error message out of the Sentry event object
94function getMessage(event: Event) {
95  if (event.message) {
96    return event.message;
97  }
98
99  if (event.exception && event.exception.values) {
100    const value = event.exception.values[0].value;
101    if (value) {
102      return value;
103    }
104  }
105
106  return null;
107}
108
109function maybeResetReportedErrorsCache() {
110  const timestamp = parseInt(localStorage.getItem(TIMESTAMP_KEY) || '', 10);
111  const now = new Date().getTime();
112
113  if (!timestamp) {
114    localStorage.setItem(TIMESTAMP_KEY, new Date().getTime().toString());
115  } else if (now - timestamp >= ONE_DAY_MS) {
116    localStorage.removeItem(REPORTED_ERRORS_KEY);
117    localStorage.removeItem(TIMESTAMP_KEY);
118  }
119}
120
121function userHasReportedErrorMessage(message: string) {
122  const messages = getReportedErrorMessages();
123  if (messages.includes(message)) {
124    return true;
125  } else {
126    return false;
127  }
128}
129
130function saveReportedErrorMessage(message: string) {
131  const messages = getReportedErrorMessages();
132  localStorage.setItem(REPORTED_ERRORS_KEY, JSON.stringify([...messages, message]));
133}
134
135function getReportedErrorMessages(): string[] {
136  const messages = localStorage.getItem(REPORTED_ERRORS_KEY);
137  if (!messages) {
138    return [];
139  }
140
141  return JSON.parse(messages);
142}
143