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    // Ignore the log forwarder used in the expo package.
28    '/expo/build/logs/RemoteConsole.js$',
29    // Improve errors thrown by invariant (ex: `Invariant Violation: "main" has not been registered`).
30    'node_modules/invariant/.+\\.js$',
31    // Remove babel runtime additions
32    'node_modules/regenerator-runtime/.+\\.js$',
33    // Remove react native setImmediate ponyfill
34    'node_modules/promise/setimmediate/.+\\.js$',
35    // Babel helpers that implement language features
36    'node_modules/@babel/runtime/.+\\.js$',
37    // Hide Hermes internal bytecode
38    '/InternalBytecode/InternalBytecode\\.js$',
39    // Block native code invocations
40    `\\[native code\\]`,
41    // Hide react-dom (web)
42    'node_modules/react-dom/.+\\.js$',
43  ].join('|')
44);
45
46function isUrl(value: string): boolean {
47  try {
48    // eslint-disable-next-line no-new
49    new URL(value);
50    return true;
51  } catch {
52    return false;
53  }
54}
55
56/**
57 * The default frame processor. This is used to modify the stack traces.
58 * This method attempts to collapse all frames that aren't relevant to
59 * the user by default.
60 */
61export function getDefaultCustomizeFrame(): CustomizeFrameFunc {
62  return (frame: Parameters<CustomizeFrameFunc>[0]) => {
63    if (frame.file && isUrl(frame.file)) {
64      return {
65        ...frame,
66        // HACK: This prevents Metro from attempting to read the invalid file URL it sent us.
67        lineNumber: null,
68        column: null,
69        // This prevents the invalid frame from being shown by default.
70        collapse: true,
71      };
72    }
73    let collapse = Boolean(frame.file && INTERNAL_CALLSITES_REGEX.test(frame.file));
74
75    if (!collapse) {
76      // This represents the first frame of the stacktrace.
77      // Often this looks like: `__r(0);`.
78      // The URL will also be unactionable in the app and therefore not very useful to the developer.
79      if (
80        frame.column === 3 &&
81        frame.methodName === 'global code' &&
82        frame.file?.match(/^https?:\/\//g)
83      ) {
84        collapse = true;
85      }
86    }
87
88    return { ...(frame || {}), collapse };
89  };
90}
91