1// Copyright 2023-present 650 Industries (Expo). All rights reserved.
2import { SymbolicatorConfigT } from 'metro-config';
3import { URL } from 'url';
4
5type CustomizeFrameFunc = SymbolicatorConfigT['customizeFrame'];
6
7// Import only the types here, the values will be imported from the project, at runtime.
8export const INTERNAL_CALLSITES_REGEX = new RegExp(
9  [
10    '/Libraries/Renderer/implementations/.+\\.js$',
11    '/Libraries/BatchedBridge/MessageQueue\\.js$',
12    '/Libraries/YellowBox/.+\\.js$',
13    '/Libraries/LogBox/.+\\.js$',
14    '/Libraries/Core/Timers/.+\\.js$',
15    'node_modules/react-devtools-core/.+\\.js$',
16    'node_modules/react-refresh/.+\\.js$',
17    'node_modules/scheduler/.+\\.js$',
18    // Metro replaces `require()` with a different method,
19    // we want to omit this method from the stack trace.
20    // This is akin to most React tooling.
21    '/metro/.*/polyfills/require.js$',
22    // Hide frames related to a fast refresh.
23    '/metro/.*/lib/bundle-modules/.+\\.js$',
24    'node_modules/react-native/Libraries/Utilities/HMRClient.js$',
25    'node_modules/eventemitter3/index.js',
26    'node_modules/event-target-shim/dist/.+\\.js$',
27    // Improve errors thrown by invariant (ex: `Invariant Violation: "main" has not been registered`).
28    'node_modules/invariant/.+\\.js$',
29    // Remove babel runtime additions
30    'node_modules/regenerator-runtime/.+\\.js$',
31    // Remove react native setImmediate ponyfill
32    'node_modules/promise/setimmediate/.+\\.js$',
33    // Babel helpers that implement language features
34    'node_modules/@babel/runtime/.+\\.js$',
35    // Hide Hermes internal bytecode
36    '/InternalBytecode/InternalBytecode\\.js$',
37    // Block native code invocations
38    `\\[native code\\]`,
39    // Hide react-dom (web)
40    'node_modules/react-dom/.+\\.js$',
41    // Block expo's metro-runtime
42    '@expo/metro-runtime/build/.+\\.js$',
43    // Block upstream metro-runtime
44    '/metro-runtime/.+\\.js$',
45  ].join('|')
46);
47
48function isUrl(value: string): boolean {
49  try {
50    // eslint-disable-next-line no-new
51    new URL(value);
52    return true;
53  } catch {
54    return false;
55  }
56}
57
58/**
59 * The default frame processor. This is used to modify the stack traces.
60 * This method attempts to collapse all frames that aren't relevant to
61 * the user by default.
62 */
63export function getDefaultCustomizeFrame(): CustomizeFrameFunc {
64  return (frame: Parameters<CustomizeFrameFunc>[0]) => {
65    if (frame.file && isUrl(frame.file)) {
66      return {
67        ...frame,
68        // HACK: This prevents Metro from attempting to read the invalid file URL it sent us.
69        lineNumber: null,
70        column: null,
71        // This prevents the invalid frame from being shown by default.
72        collapse: true,
73      };
74    }
75    let collapse = Boolean(frame.file && INTERNAL_CALLSITES_REGEX.test(frame.file));
76
77    if (!collapse) {
78      // This represents the first frame of the stacktrace.
79      // Often this looks like: `__r(0);`.
80      // The URL will also be unactionable in the app and therefore not very useful to the developer.
81      if (
82        frame.column === 3 &&
83        frame.methodName === 'global code' &&
84        frame.file?.match(/^https?:\/\//g)
85      ) {
86        collapse = true;
87      }
88    }
89
90    return { ...(frame || {}), collapse };
91  };
92}
93