From 3ac1315f22f8a3aeda34903ca11578410d340fac Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Mon, 1 Jul 2024 10:54:04 +0200 Subject: [PATCH 01/14] wip: Add Sentry Babel Transformer --- package.json | 1 + samples/react-native/babel.config.js | 4 +- samples/react-native/metro.config.js | 4 + samples/react-native/sentryTransformer.js | 18 ++++ .../src/Screens/TrackerScreen.tsx | 10 +- src/js/tools/metroconfig.ts | 13 +++ src/js/tools/sentryBabelTransformer.ts | 97 +++++++++++++++++++ src/js/touchevents.tsx | 30 +++--- yarn.lock | 5 + 9 files changed, 160 insertions(+), 22 deletions(-) create mode 100644 samples/react-native/sentryTransformer.js create mode 100644 src/js/tools/sentryBabelTransformer.ts diff --git a/package.json b/package.json index 3224e5fd1e..caaf93ffd0 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "react-native": ">=0.65.0" }, "dependencies": { + "@sentry/babel-plugin-component-annotate": "2.20.1", "@sentry/browser": "7.117.0", "@sentry/cli": "2.31.2", "@sentry/core": "7.117.0", diff --git a/samples/react-native/babel.config.js b/samples/react-native/babel.config.js index 06dc90ec75..d547caed40 100644 --- a/samples/react-native/babel.config.js +++ b/samples/react-native/babel.config.js @@ -1,4 +1,4 @@ -const componentAnnotatePlugin = require('@sentry/babel-plugin-component-annotate'); +//const componentAnnotatePlugin = require('@sentry/babel-plugin-component-annotate'); module.exports = { presets: ['module:@react-native/babel-preset'], @@ -12,6 +12,6 @@ module.exports = { }, ], 'react-native-reanimated/plugin', - componentAnnotatePlugin, + //componentAnnotatePlugin, ], }; diff --git a/samples/react-native/metro.config.js b/samples/react-native/metro.config.js index 10d30549bc..57957f72af 100644 --- a/samples/react-native/metro.config.js +++ b/samples/react-native/metro.config.js @@ -57,7 +57,11 @@ const config = { }, ), }, + transformer: { + babelTransformerPath: require.resolve('./sentryTransformer.js'), + }, }; const m = mergeConfig(getDefaultConfig(__dirname), config); +console.log(m.transformer.babelTransformerPath); module.exports = withSentryConfig(m); diff --git a/samples/react-native/sentryTransformer.js b/samples/react-native/sentryTransformer.js new file mode 100644 index 0000000000..8ca69ce73c --- /dev/null +++ b/samples/react-native/sentryTransformer.js @@ -0,0 +1,18 @@ +const componentAnnotatePlugin = require('@sentry/babel-plugin-component-annotate'); +const defaultTransformer = require('@react-native/metro-babel-transformer'); +const fs = require('fs'); + +const transform = (...args) => { + if (!args[0].filename.includes('node_modules')) { + args[0].plugins.push(componentAnnotatePlugin); + } + + return defaultTransformer.transform(...args); +}; + +const babelTransformer = { + ...defaultTransformer, + transform, +}; + +module.exports = babelTransformer; diff --git a/samples/react-native/src/Screens/TrackerScreen.tsx b/samples/react-native/src/Screens/TrackerScreen.tsx index 19f3a6783d..7be1635df7 100644 --- a/samples/react-native/src/Screens/TrackerScreen.tsx +++ b/samples/react-native/src/Screens/TrackerScreen.tsx @@ -75,9 +75,7 @@ const TrackerScreen = () => { return ( - - Global COVID19 Cases - + {cases ? ( <> @@ -113,6 +111,12 @@ const TrackerScreen = () => { ); }; +const TrackerTitle = () => ( + + Global COVID19 Cases + +); + export default Sentry.withProfiler(TrackerScreen); const Statistic = (props: { diff --git a/src/js/tools/metroconfig.ts b/src/js/tools/metroconfig.ts index 8f40922016..d43ddabbcb 100644 --- a/src/js/tools/metroconfig.ts +++ b/src/js/tools/metroconfig.ts @@ -19,6 +19,7 @@ export function withSentryConfig(config: MetroConfig): MetroConfig { newConfig = withSentryDebugId(newConfig); newConfig = withSentryFramesCollapsed(newConfig); + newConfig = withSentryBabelTransformer(newConfig); return newConfig; } @@ -64,6 +65,18 @@ function loadExpoMetroConfigModule(): { } } +function withSentryBabelTransformer(config: MetroConfig): MetroConfig { + // TODO: check if custom babel transformer set, if so wrap it + + return { + ...config, + transformer: { + ...config.transformer, + babelTransformerPath: require.resolve('./sentryBabelTransformer'), + }, + }; +} + type MetroCustomSerializer = Required['serializer']>['customSerializer'] | undefined; function withSentryDebugId(config: MetroConfig): MetroConfig { diff --git a/src/js/tools/sentryBabelTransformer.ts b/src/js/tools/sentryBabelTransformer.ts new file mode 100644 index 0000000000..679f8491ee --- /dev/null +++ b/src/js/tools/sentryBabelTransformer.ts @@ -0,0 +1,97 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @oncall react_native + */ + +interface CustomTransformOptions { + [key: string]: unknown; +} + +type TransformProfile = 'default' | 'hermes-stable' | 'hermes-canary'; + +interface BabelTransformerOptions { + readonly customTransformOptions?: CustomTransformOptions; + readonly dev: boolean; + readonly enableBabelRCLookup?: boolean; + readonly enableBabelRuntime: boolean | string; + readonly extendsBabelConfigPath?: string; + readonly experimentalImportSupport?: boolean; + readonly hermesParser?: boolean; + readonly hot: boolean; + readonly minify: boolean; + readonly unstable_disableES6Transforms?: boolean; + readonly platform: string | null; + readonly projectRoot: string; + readonly publicPath: string; + readonly unstable_transformProfile?: TransformProfile; + readonly globalPrefix: string; +} + +interface BabelTransformerArgs { + readonly filename: string; + readonly options: BabelTransformerOptions; + readonly plugins?: unknown; + readonly src: string; +} + +interface BabelTransformer { + transform: (args: BabelTransformerArgs) => { + ast: unknown; + metadata: unknown; + }; + getCacheKey?: () => string; +} +// TODO: Add above to the vendor dir + +import componentAnnotatePlugin from '@sentry/babel-plugin-component-annotate'; + +/** + * + */ +function createSentryBabelTransformer(): BabelTransformer { + // TODO: Read default from withSentry options + const defaultTransformer = loadDefaultBabelTransformer(); + + // Using spread operator to avoid any conflicts with the default transformer + const transform: BabelTransformer['transform'] = (...args) => { + updateArgs(args[0]); + return defaultTransformer.transform(...args); + }; + + return { + ...defaultTransformer, + transform, + }; +} + +function updateArgs(args: BabelTransformerArgs | undefined): void { + if (!args || typeof args.filename !== 'string' || !Array.isArray(args.plugins)) { + return undefined; + } + + if (!args.filename.includes('node_modules')) { + args.plugins.push(componentAnnotatePlugin); + } +}; + +/** + * + */ +function loadDefaultBabelTransformer(): BabelTransformer { + // TODO: Add to dev dependencies + // TODO: Recreate node module resolution logic to avoid picking the hoisted package + // eslint-disable-next-line @typescript-eslint/no-var-requires, import/no-extraneous-dependencies + const defaultTransformer = require('@react-native/metro-babel-transformer'); + return defaultTransformer; +} + +const sentryBabelTransformer = createSentryBabelTransformer(); + +// With TS set to `commonjs` this will be translated to `module.exports = sentryBabelTransformer;` +// which will be correctly picked up by Metro +export = sentryBabelTransformer; diff --git a/src/js/touchevents.tsx b/src/js/touchevents.tsx index 785dc2977c..6c89d6766e 100644 --- a/src/js/touchevents.tsx +++ b/src/js/touchevents.tsx @@ -130,7 +130,10 @@ class TouchEventBoundary extends React.Component { const crumb = { category: this.props.breadcrumbCategory, - data: { path: touchPath }, + data: { + path: touchPath, + target: detail, + }, level: level, message: `Touch event within element: ${detail}`, type: this.props.breadcrumbType, @@ -193,25 +196,13 @@ class TouchEventBoundary extends React.Component { const info: TouchedComponentInfo = {}; // provided by @sentry/babel-plugin-component-annotate - if ( - typeof props[SENTRY_COMPONENT_PROP_KEY] === 'string' && - props[SENTRY_COMPONENT_PROP_KEY].length > 0 && - props[SENTRY_COMPONENT_PROP_KEY] !== 'unknown' - ) { + if (typeof props[SENTRY_COMPONENT_PROP_KEY] === 'string' && props[SENTRY_COMPONENT_PROP_KEY].length > 0 && props[SENTRY_COMPONENT_PROP_KEY] !== 'unknown') { info.name = props[SENTRY_COMPONENT_PROP_KEY]; } - if ( - typeof props[SENTRY_ELEMENT_PROP_KEY] === 'string' && - props[SENTRY_ELEMENT_PROP_KEY].length > 0 && - props[SENTRY_ELEMENT_PROP_KEY] !== 'unknown' - ) { + if (typeof props[SENTRY_ELEMENT_PROP_KEY] === 'string' && props[SENTRY_ELEMENT_PROP_KEY].length > 0 && props[SENTRY_ELEMENT_PROP_KEY] !== 'unknown') { info.element = props[SENTRY_ELEMENT_PROP_KEY]; } - if ( - typeof props[SENTRY_FILE_PROP_KEY] === 'string' && - props[SENTRY_FILE_PROP_KEY].length > 0 && - props[SENTRY_FILE_PROP_KEY] !== 'unknown' - ) { + if (typeof props[SENTRY_FILE_PROP_KEY] === 'string' && props[SENTRY_FILE_PROP_KEY].length > 0 && props[SENTRY_FILE_PROP_KEY] !== 'unknown') { info.file = props[SENTRY_FILE_PROP_KEY]; } @@ -233,7 +224,12 @@ class TouchEventBoundary extends React.Component { info.name = currentInst.elementType?.displayName; } - this._pushIfNotIgnored(touchPath, info); + // TODO: let's push all, but in highlight only the annotated or label one in the breadcrumb.message + if ( + (typeof props[SENTRY_COMPONENT_PROP_KEY] === 'string' && props[SENTRY_COMPONENT_PROP_KEY].length > 0 && props[SENTRY_COMPONENT_PROP_KEY] !== 'unknown') + || labelValue) { + this._pushIfNotIgnored(touchPath, info); + } currentInst = currentInst.return; } diff --git a/yarn.lock b/yarn.lock index d36f2dd66b..92775e219d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3804,6 +3804,11 @@ resolved "https://registry.yarnpkg.com/@sentry-internal/typescript/-/typescript-7.117.0.tgz#bd43fc07a222e98861e6ab8a85ddd60e7399cd47" integrity sha512-SylReCEo1FiTuir6XiZuV+sWBOBERDL0C3YmdHhczOh0aeu50FUja7uJfoXMx0LTEwaUAXq62dWUvb9WetluOQ== +"@sentry/babel-plugin-component-annotate@2.20.1": + version "2.20.1" + resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-2.20.1.tgz#204c63ed006a048f48f633876e1b8bacf87a9722" + integrity sha512-4mhEwYTK00bIb5Y9UWIELVUfru587Vaeg0DQGswv4aIRHIiMKLyNqCEejaaybQ/fNChIZOKmvyqXk430YVd7Qg== + "@sentry/browser@7.117.0": version "7.117.0" resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.117.0.tgz#3030073f360974dadcf5a5f2e1542497b3be2482" From d558312bd0c15bfa9e846e7bd8785068116d23e0 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Mon, 1 Jul 2024 10:54:04 +0200 Subject: [PATCH 02/14] add annotate react as opt in feature --- samples/react-native/metro.config.js | 8 +- samples/react-native/sentryTransformer.js | 18 --- src/js/tools/enableLogger.ts | 10 ++ src/js/tools/metroconfig.ts | 42 ++++++- src/js/tools/sentryBabelTransformer.ts | 82 +++----------- src/js/tools/sentryBabelTransformerUtils.ts | 105 ++++++++++++++++++ .../vendor/metro/metroBabelTransformer.ts | 64 +++++++++++ 7 files changed, 237 insertions(+), 92 deletions(-) delete mode 100644 samples/react-native/sentryTransformer.js create mode 100644 src/js/tools/enableLogger.ts create mode 100644 src/js/tools/sentryBabelTransformerUtils.ts create mode 100644 src/js/tools/vendor/metro/metroBabelTransformer.ts diff --git a/samples/react-native/metro.config.js b/samples/react-native/metro.config.js index 57957f72af..9e1e8c7f14 100644 --- a/samples/react-native/metro.config.js +++ b/samples/react-native/metro.config.js @@ -57,11 +57,9 @@ const config = { }, ), }, - transformer: { - babelTransformerPath: require.resolve('./sentryTransformer.js'), - }, }; const m = mergeConfig(getDefaultConfig(__dirname), config); -console.log(m.transformer.babelTransformerPath); -module.exports = withSentryConfig(m); +module.exports = withSentryConfig(m, { + annotateReactComponents: true, +}); diff --git a/samples/react-native/sentryTransformer.js b/samples/react-native/sentryTransformer.js deleted file mode 100644 index 8ca69ce73c..0000000000 --- a/samples/react-native/sentryTransformer.js +++ /dev/null @@ -1,18 +0,0 @@ -const componentAnnotatePlugin = require('@sentry/babel-plugin-component-annotate'); -const defaultTransformer = require('@react-native/metro-babel-transformer'); -const fs = require('fs'); - -const transform = (...args) => { - if (!args[0].filename.includes('node_modules')) { - args[0].plugins.push(componentAnnotatePlugin); - } - - return defaultTransformer.transform(...args); -}; - -const babelTransformer = { - ...defaultTransformer, - transform, -}; - -module.exports = babelTransformer; diff --git a/src/js/tools/enableLogger.ts b/src/js/tools/enableLogger.ts new file mode 100644 index 0000000000..a5d36ade2a --- /dev/null +++ b/src/js/tools/enableLogger.ts @@ -0,0 +1,10 @@ +import { logger } from '@sentry/utils'; + +/** + * Enables debug logger when SENTRY_LOG_LEVEL=debug. + */ +export function enableLogger(): void { + if (process.env.SENTRY_LOG_LEVEL === 'debug') { + logger.enable(); + } +} diff --git a/src/js/tools/metroconfig.ts b/src/js/tools/metroconfig.ts index d43ddabbcb..4785d7f93e 100644 --- a/src/js/tools/metroconfig.ts +++ b/src/js/tools/metroconfig.ts @@ -1,25 +1,46 @@ +import { logger } from '@sentry/utils'; import type { MetroConfig, MixedOutput, Module, ReadOnlyGraph } from 'metro'; +import * as process from 'process'; import { env } from 'process'; +import { enableLogger } from './enableLogger'; +import { canUseSentryBabelTransformer, cleanDefaultBabelTransformerPath, saveDefaultBabelTransformerPath } from './sentryBabelTransformerUtils'; import { createSentryMetroSerializer, unstable_beforeAssetSerializationPlugin } from './sentryMetroSerializer'; import type { DefaultConfigOptions } from './vendor/expo/expoconfig'; export * from './sentryMetroSerializer'; +enableLogger(); + +export interface SentryMetroConfigOptions { + /** + * Annotates React components with Sentry data. + * @default false + */ + annotateReactComponents?: boolean; +} + /** * Adds Sentry to the Metro config. * * Adds Debug ID to the output bundle and source maps. * Collapses Sentry frames from the stack trace view in LogBox. */ -export function withSentryConfig(config: MetroConfig): MetroConfig { +export function withSentryConfig( + config: MetroConfig, + { + annotateReactComponents = false, + }: SentryMetroConfigOptions = {}, +): MetroConfig { setSentryMetroDevServerEnvFlag(); let newConfig = config; newConfig = withSentryDebugId(newConfig); newConfig = withSentryFramesCollapsed(newConfig); - newConfig = withSentryBabelTransformer(newConfig); + if (annotateReactComponents) { + newConfig = withSentryBabelTransformer(newConfig); + } return newConfig; } @@ -42,6 +63,7 @@ export function getSentryExpoConfig( ], }); + // TODO: newConfig = withSentryBabelTransformer(newConfig); return withSentryFramesCollapsed(config); } @@ -66,7 +88,21 @@ function loadExpoMetroConfigModule(): { } function withSentryBabelTransformer(config: MetroConfig): MetroConfig { - // TODO: check if custom babel transformer set, if so wrap it + const defaultBabelTransformerPath = config.transformer && config.transformer.babelTransformerPath; + logger.debug('Default Babel transformer path from `config.transformer`:', defaultBabelTransformerPath); + + if (!defaultBabelTransformerPath && !canUseSentryBabelTransformer(config.projectRoot)) { + // eslint-disable-next-line no-console + console.warn('Sentry Babel transformer cannot be used. Not adding it ...'); + return config; + } + + if (defaultBabelTransformerPath) { + saveDefaultBabelTransformerPath(config.projectRoot || '.', defaultBabelTransformerPath); + process.on('exit', () => { + cleanDefaultBabelTransformerPath(config.projectRoot || '.'); + }); + } return { ...config, diff --git a/src/js/tools/sentryBabelTransformer.ts b/src/js/tools/sentryBabelTransformer.ts index 679f8491ee..793cbfb307 100644 --- a/src/js/tools/sentryBabelTransformer.ts +++ b/src/js/tools/sentryBabelTransformer.ts @@ -1,65 +1,27 @@ -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @format - * @oncall react_native - */ - -interface CustomTransformOptions { - [key: string]: unknown; -} - -type TransformProfile = 'default' | 'hermes-stable' | 'hermes-canary'; - -interface BabelTransformerOptions { - readonly customTransformOptions?: CustomTransformOptions; - readonly dev: boolean; - readonly enableBabelRCLookup?: boolean; - readonly enableBabelRuntime: boolean | string; - readonly extendsBabelConfigPath?: string; - readonly experimentalImportSupport?: boolean; - readonly hermesParser?: boolean; - readonly hot: boolean; - readonly minify: boolean; - readonly unstable_disableES6Transforms?: boolean; - readonly platform: string | null; - readonly projectRoot: string; - readonly publicPath: string; - readonly unstable_transformProfile?: TransformProfile; - readonly globalPrefix: string; -} - -interface BabelTransformerArgs { - readonly filename: string; - readonly options: BabelTransformerOptions; - readonly plugins?: unknown; - readonly src: string; -} +import componentAnnotatePlugin from '@sentry/babel-plugin-component-annotate'; -interface BabelTransformer { - transform: (args: BabelTransformerArgs) => { - ast: unknown; - metadata: unknown; - }; - getCacheKey?: () => string; -} -// TODO: Add above to the vendor dir +import { enableLogger } from './enableLogger'; +import { loadDefaultBabelTransformer } from './sentryBabelTransformerUtils'; +import type { BabelTransformer, BabelTransformerArgs } from './vendor/metro/metroBabelTransformer'; -import componentAnnotatePlugin from '@sentry/babel-plugin-component-annotate'; +enableLogger(); /** - * + * Creates a Babel transformer with Sentry component annotation plugin. */ function createSentryBabelTransformer(): BabelTransformer { - // TODO: Read default from withSentry options - const defaultTransformer = loadDefaultBabelTransformer(); + let defaultTransformer: BabelTransformer | undefined; // Using spread operator to avoid any conflicts with the default transformer const transform: BabelTransformer['transform'] = (...args) => { - updateArgs(args[0]); + const transformerArgs = args[0]; + const projectRoot = transformerArgs.options.projectRoot; + + if (!defaultTransformer) { + defaultTransformer = loadDefaultBabelTransformer(projectRoot); + } + + addSentryComponentAnnotatePlugin(transformerArgs); return defaultTransformer.transform(...args); }; @@ -69,7 +31,7 @@ function createSentryBabelTransformer(): BabelTransformer { }; } -function updateArgs(args: BabelTransformerArgs | undefined): void { +function addSentryComponentAnnotatePlugin(args: BabelTransformerArgs | undefined): void { if (!args || typeof args.filename !== 'string' || !Array.isArray(args.plugins)) { return undefined; } @@ -79,19 +41,7 @@ function updateArgs(args: BabelTransformerArgs | undefined): void { } }; -/** - * - */ -function loadDefaultBabelTransformer(): BabelTransformer { - // TODO: Add to dev dependencies - // TODO: Recreate node module resolution logic to avoid picking the hoisted package - // eslint-disable-next-line @typescript-eslint/no-var-requires, import/no-extraneous-dependencies - const defaultTransformer = require('@react-native/metro-babel-transformer'); - return defaultTransformer; -} - const sentryBabelTransformer = createSentryBabelTransformer(); - // With TS set to `commonjs` this will be translated to `module.exports = sentryBabelTransformer;` // which will be correctly picked up by Metro export = sentryBabelTransformer; diff --git a/src/js/tools/sentryBabelTransformerUtils.ts b/src/js/tools/sentryBabelTransformerUtils.ts new file mode 100644 index 0000000000..e2ae478017 --- /dev/null +++ b/src/js/tools/sentryBabelTransformerUtils.ts @@ -0,0 +1,105 @@ +import { logger } from '@sentry/utils'; +import * as fs from 'fs'; +import * as path from 'path'; + +import type { BabelTransformer } from './vendor/metro/metroBabelTransformer'; + +/** + * Saves default Babel transformer path to the project root. + */ +export function saveDefaultBabelTransformerPath(projectRoot: string, defaultBabelTransformerPath: string): void { + try { + fs.mkdirSync(path.join(projectRoot, '.sentry'), { recursive: true }); + fs.writeFileSync(getDefaultBabelTransformerPath(projectRoot), defaultBabelTransformerPath); + logger.debug('Saved default Babel transformer path'); + } catch (e) { + // eslint-disable-next-line no-console + console.error('[Sentry] Failed to save default Babel transformer path:', e); + } +} + +/** + * Reads default Babel transformer path from the project root. + */ +export function readDefaultBabelTransformerPath(projectRoot: string): string | undefined { + try { + if (fs.existsSync(getDefaultBabelTransformerPath(projectRoot))) { + return fs.readFileSync(getDefaultBabelTransformerPath(projectRoot)).toString(); + } + } catch (e) { + // eslint-disable-next-line no-console + console.error('[Sentry] Failed to read default Babel transformer path:', e); + } + return undefined; +} + +/** + * Cleans default Babel transformer path from the project root. + */ +export function cleanDefaultBabelTransformerPath(projectRoot: string): void { + try { + if (fs.existsSync(getDefaultBabelTransformerPath(projectRoot))) { + fs.unlinkSync(getDefaultBabelTransformerPath(projectRoot)); + } + logger.debug('Cleaned default Babel transformer path'); + } catch (e) { + // eslint-disable-next-line no-console + console.error('[Sentry] Failed to clean default Babel transformer path:', e); + } +} + +function getDefaultBabelTransformerPath(from: string): string { + return path.join(from, '.sentry/.defaultBabelTransformerPath') +} + +/** + * Loads default Babel transformer from `@react-native/metro-config` -> `@react-native/metro-babel-transformer`. + */ +export function loadDefaultBabelTransformer(projectRoot: string): BabelTransformer { + const defaultBabelTransformerPath = readDefaultBabelTransformerPath(projectRoot); + if (defaultBabelTransformerPath) { + logger.debug(`Loading default Babel transformer from ${defaultBabelTransformerPath}`); + // eslint-disable-next-line @typescript-eslint/no-var-requires + return require(defaultBabelTransformerPath); + } + + const reactNativeMetroConfigPath = resolveReactNativeMetroConfigPath(projectRoot); + if (!reactNativeMetroConfigPath) { + throw new Error('Cannot resolve `@react-native/metro-config` to find `@react-native/metro-babel-transformer`.'); + } + + let defaultTransformerPath: string; + try { + defaultTransformerPath = require.resolve('@react-native/metro-babel-transformer', { paths: [reactNativeMetroConfigPath] }); + logger.debug(`Resolved @react-native/metro-babel-transformer to ${defaultTransformerPath}`); + } catch (e) { + throw new Error('Cannot load `@react-native/metro-babel-transformer` from `${reactNativeMetroConfig}`.'); + } + + // eslint-disable-next-line @typescript-eslint/no-var-requires + const defaultTransformer = require(defaultTransformerPath); + return defaultTransformer; +} + +/** + * Checks current environment and installed dependencies to determine if Sentry Babel transformer can be used. + */ +export function canUseSentryBabelTransformer(projectRoot?: string): boolean { + return !!resolveReactNativeMetroConfigPath(projectRoot); +} + +/** + * Resolves path to the installed `@react-native/metro-config` package. + * Available since React Native 0.72 + */ +function resolveReactNativeMetroConfigPath(projectRoot?: string): string | undefined { + try { + const p = require.resolve('@react-native/metro-config', projectRoot ? { paths: [projectRoot] } : undefined); + logger.debug(`Resolved @react-native/metro-config to ${p}`); + return p; + } catch (e) { + // return undefined; + } + logger.debug('Failed to resolve @react-native/metro-config'); + return undefined; +} diff --git a/src/js/tools/vendor/metro/metroBabelTransformer.ts b/src/js/tools/vendor/metro/metroBabelTransformer.ts new file mode 100644 index 0000000000..62b5616943 --- /dev/null +++ b/src/js/tools/vendor/metro/metroBabelTransformer.ts @@ -0,0 +1,64 @@ +// Vendored / modified from @facebook/metro + +// https://github.com/facebook/metro/blob/9b295e5f7ecd9cb6332a199bf9cdc1bd8fddf6d9/packages/metro-babel-transformer/types/index.d.ts + +// MIT License + +// Copyright (c) Meta Platforms, Inc. and affiliates. + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +export interface CustomTransformOptions { + [key: string]: unknown; +} + +export type TransformProfile = 'default' | 'hermes-stable' | 'hermes-canary'; + +export interface BabelTransformerOptions { + readonly customTransformOptions?: CustomTransformOptions; + readonly dev: boolean; + readonly enableBabelRCLookup?: boolean; + readonly enableBabelRuntime: boolean | string; + readonly extendsBabelConfigPath?: string; + readonly experimentalImportSupport?: boolean; + readonly hermesParser?: boolean; + readonly hot: boolean; + readonly minify: boolean; + readonly unstable_disableES6Transforms?: boolean; + readonly platform: string | null; + readonly projectRoot: string; + readonly publicPath: string; + readonly unstable_transformProfile?: TransformProfile; + readonly globalPrefix: string; +} + +export interface BabelTransformerArgs { + readonly filename: string; + readonly options: BabelTransformerOptions; + readonly plugins?: unknown; + readonly src: string; +} + +export interface BabelTransformer { + transform: (args: BabelTransformerArgs) => { + ast: unknown; + metadata: unknown; + }; + getCacheKey?: () => string; +} From 670bfc6d62473630a329f3329e7f62f4b2f80cfb Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Mon, 1 Jul 2024 10:54:04 +0200 Subject: [PATCH 03/14] fix lint --- src/js/tools/metroconfig.ts | 10 ++++++---- src/js/tools/sentryBabelTransformer.ts | 2 +- src/js/tools/sentryBabelTransformerUtils.ts | 6 ++++-- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/js/tools/metroconfig.ts b/src/js/tools/metroconfig.ts index 4785d7f93e..24baf67400 100644 --- a/src/js/tools/metroconfig.ts +++ b/src/js/tools/metroconfig.ts @@ -4,7 +4,11 @@ import * as process from 'process'; import { env } from 'process'; import { enableLogger } from './enableLogger'; -import { canUseSentryBabelTransformer, cleanDefaultBabelTransformerPath, saveDefaultBabelTransformerPath } from './sentryBabelTransformerUtils'; +import { + canUseSentryBabelTransformer, + cleanDefaultBabelTransformerPath, + saveDefaultBabelTransformerPath, +} from './sentryBabelTransformerUtils'; import { createSentryMetroSerializer, unstable_beforeAssetSerializationPlugin } from './sentryMetroSerializer'; import type { DefaultConfigOptions } from './vendor/expo/expoconfig'; @@ -28,9 +32,7 @@ export interface SentryMetroConfigOptions { */ export function withSentryConfig( config: MetroConfig, - { - annotateReactComponents = false, - }: SentryMetroConfigOptions = {}, + { annotateReactComponents = false }: SentryMetroConfigOptions = {}, ): MetroConfig { setSentryMetroDevServerEnvFlag(); diff --git a/src/js/tools/sentryBabelTransformer.ts b/src/js/tools/sentryBabelTransformer.ts index 793cbfb307..1dc4f83ef7 100644 --- a/src/js/tools/sentryBabelTransformer.ts +++ b/src/js/tools/sentryBabelTransformer.ts @@ -39,7 +39,7 @@ function addSentryComponentAnnotatePlugin(args: BabelTransformerArgs | undefined if (!args.filename.includes('node_modules')) { args.plugins.push(componentAnnotatePlugin); } -}; +} const sentryBabelTransformer = createSentryBabelTransformer(); // With TS set to `commonjs` this will be translated to `module.exports = sentryBabelTransformer;` diff --git a/src/js/tools/sentryBabelTransformerUtils.ts b/src/js/tools/sentryBabelTransformerUtils.ts index e2ae478017..7e5c756b79 100644 --- a/src/js/tools/sentryBabelTransformerUtils.ts +++ b/src/js/tools/sentryBabelTransformerUtils.ts @@ -49,7 +49,7 @@ export function cleanDefaultBabelTransformerPath(projectRoot: string): void { } function getDefaultBabelTransformerPath(from: string): string { - return path.join(from, '.sentry/.defaultBabelTransformerPath') + return path.join(from, '.sentry/.defaultBabelTransformerPath'); } /** @@ -70,7 +70,9 @@ export function loadDefaultBabelTransformer(projectRoot: string): BabelTransform let defaultTransformerPath: string; try { - defaultTransformerPath = require.resolve('@react-native/metro-babel-transformer', { paths: [reactNativeMetroConfigPath] }); + defaultTransformerPath = require.resolve('@react-native/metro-babel-transformer', { + paths: [reactNativeMetroConfigPath], + }); logger.debug(`Resolved @react-native/metro-babel-transformer to ${defaultTransformerPath}`); } catch (e) { throw new Error('Cannot load `@react-native/metro-babel-transformer` from `${reactNativeMetroConfig}`.'); From ea413b71a5ed166304871888511957b372977bdd Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Mon, 1 Jul 2024 10:54:04 +0200 Subject: [PATCH 04/14] revert: touch events changes --- src/js/touchevents.tsx | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/src/js/touchevents.tsx b/src/js/touchevents.tsx index 6c89d6766e..785dc2977c 100644 --- a/src/js/touchevents.tsx +++ b/src/js/touchevents.tsx @@ -130,10 +130,7 @@ class TouchEventBoundary extends React.Component { const crumb = { category: this.props.breadcrumbCategory, - data: { - path: touchPath, - target: detail, - }, + data: { path: touchPath }, level: level, message: `Touch event within element: ${detail}`, type: this.props.breadcrumbType, @@ -196,13 +193,25 @@ class TouchEventBoundary extends React.Component { const info: TouchedComponentInfo = {}; // provided by @sentry/babel-plugin-component-annotate - if (typeof props[SENTRY_COMPONENT_PROP_KEY] === 'string' && props[SENTRY_COMPONENT_PROP_KEY].length > 0 && props[SENTRY_COMPONENT_PROP_KEY] !== 'unknown') { + if ( + typeof props[SENTRY_COMPONENT_PROP_KEY] === 'string' && + props[SENTRY_COMPONENT_PROP_KEY].length > 0 && + props[SENTRY_COMPONENT_PROP_KEY] !== 'unknown' + ) { info.name = props[SENTRY_COMPONENT_PROP_KEY]; } - if (typeof props[SENTRY_ELEMENT_PROP_KEY] === 'string' && props[SENTRY_ELEMENT_PROP_KEY].length > 0 && props[SENTRY_ELEMENT_PROP_KEY] !== 'unknown') { + if ( + typeof props[SENTRY_ELEMENT_PROP_KEY] === 'string' && + props[SENTRY_ELEMENT_PROP_KEY].length > 0 && + props[SENTRY_ELEMENT_PROP_KEY] !== 'unknown' + ) { info.element = props[SENTRY_ELEMENT_PROP_KEY]; } - if (typeof props[SENTRY_FILE_PROP_KEY] === 'string' && props[SENTRY_FILE_PROP_KEY].length > 0 && props[SENTRY_FILE_PROP_KEY] !== 'unknown') { + if ( + typeof props[SENTRY_FILE_PROP_KEY] === 'string' && + props[SENTRY_FILE_PROP_KEY].length > 0 && + props[SENTRY_FILE_PROP_KEY] !== 'unknown' + ) { info.file = props[SENTRY_FILE_PROP_KEY]; } @@ -224,12 +233,7 @@ class TouchEventBoundary extends React.Component { info.name = currentInst.elementType?.displayName; } - // TODO: let's push all, but in highlight only the annotated or label one in the breadcrumb.message - if ( - (typeof props[SENTRY_COMPONENT_PROP_KEY] === 'string' && props[SENTRY_COMPONENT_PROP_KEY].length > 0 && props[SENTRY_COMPONENT_PROP_KEY] !== 'unknown') - || labelValue) { - this._pushIfNotIgnored(touchPath, info); - } + this._pushIfNotIgnored(touchPath, info); currentInst = currentInst.return; } From 50005b38404e2251f54b63f7a2823dcaeab8a28d Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Mon, 1 Jul 2024 10:54:04 +0200 Subject: [PATCH 05/14] Add expo support for react annotate --- .gitignore | 3 +++ samples/expo/metro.config.js | 1 + src/js/tools/metroconfig.ts | 17 ++++++++++++++--- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 818f97beb2..87a34b65ec 100644 --- a/.gitignore +++ b/.gitignore @@ -74,3 +74,6 @@ yalc.lock # E2E tests test/react-native/versions + +# Created by Sentry Metro Plugin +.sentry/ diff --git a/samples/expo/metro.config.js b/samples/expo/metro.config.js index b79a919dd8..d8f5aa467a 100644 --- a/samples/expo/metro.config.js +++ b/samples/expo/metro.config.js @@ -9,6 +9,7 @@ const config = getSentryExpoConfig(__dirname, { // [Web-only]: Enables CSS support in Metro. isCSSEnabled: true, getDefaultConfig, + annotateReactComponents: true, }); config.watchFolders.push(path.resolve(__dirname, '../../node_modules/@sentry')); diff --git a/src/js/tools/metroconfig.ts b/src/js/tools/metroconfig.ts index 24baf67400..9a1192df26 100644 --- a/src/js/tools/metroconfig.ts +++ b/src/js/tools/metroconfig.ts @@ -24,6 +24,13 @@ export interface SentryMetroConfigOptions { annotateReactComponents?: boolean; } +export interface SentryExpoConfigOptions { + /** + * Pass a custom `getDefaultConfig` function to override the default Expo configuration getter. + */ + getDefaultConfig?: typeof getSentryExpoConfig +} + /** * Adds Sentry to the Metro config. * @@ -52,7 +59,7 @@ export function withSentryConfig( */ export function getSentryExpoConfig( projectRoot: string, - options: DefaultConfigOptions & { getDefaultConfig?: typeof getSentryExpoConfig } = {}, + options: DefaultConfigOptions & SentryExpoConfigOptions & SentryMetroConfigOptions = {}, ): MetroConfig { setSentryMetroDevServerEnvFlag(); @@ -65,8 +72,12 @@ export function getSentryExpoConfig( ], }); - // TODO: newConfig = withSentryBabelTransformer(newConfig); - return withSentryFramesCollapsed(config); + let newConfig = withSentryFramesCollapsed(config); + if (options.annotateReactComponents) { + newConfig = withSentryBabelTransformer(newConfig); + } + + return newConfig; } function loadExpoMetroConfigModule(): { From b030816d8ad4bebd4bba5ef44265d410b5ac7bf3 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Mon, 1 Jul 2024 10:54:04 +0200 Subject: [PATCH 06/14] remove babel config, add tests --- samples/expo/app/_layout.tsx | 3 + samples/expo/babel.config.js | 3 - samples/react-native/babel.config.js | 3 - src/js/tools/metroconfig.ts | 19 ++-- src/js/tools/sentryBabelTransformer.ts | 8 +- src/js/tools/sentryBabelTransformerUtils.ts | 75 +++---------- test/tools/fixtures/mockBabelTransformer.js | 4 + test/tools/metroconfig.test.ts | 116 +++++++++++++++++--- test/tools/sentryBabelTransformer.test.ts | 55 ++++++++++ 9 files changed, 190 insertions(+), 96 deletions(-) create mode 100644 test/tools/fixtures/mockBabelTransformer.js create mode 100644 test/tools/sentryBabelTransformer.test.ts diff --git a/samples/expo/app/_layout.tsx b/samples/expo/app/_layout.tsx index 810f56797b..094ea7cba8 100644 --- a/samples/expo/app/_layout.tsx +++ b/samples/expo/app/_layout.tsx @@ -9,12 +9,15 @@ import { HttpClient } from '@sentry/integrations'; import { SENTRY_INTERNAL_DSN } from '../utils/dsn'; import * as Sentry from '@sentry/react-native'; import { isExpoGo } from '../utils/isExpoGo'; +import { LogBox } from 'react-native'; export { // Catch any errors thrown by the Layout component. ErrorBoundary, } from 'expo-router'; +LogBox.ignoreAllLogs(); + // Prevent the splash screen from auto-hiding before asset loading is complete. SplashScreen.preventAutoHideAsync(); diff --git a/samples/expo/babel.config.js b/samples/expo/babel.config.js index 7a13872315..3a495f8f7c 100644 --- a/samples/expo/babel.config.js +++ b/samples/expo/babel.config.js @@ -1,5 +1,3 @@ -const componentAnnotatePlugin = require('@sentry/babel-plugin-component-annotate'); - module.exports = function (api) { api.cache(false); return { @@ -13,7 +11,6 @@ module.exports = function (api) { }, }, ], - componentAnnotatePlugin, ], }; }; diff --git a/samples/react-native/babel.config.js b/samples/react-native/babel.config.js index d547caed40..8c8fb9c1a6 100644 --- a/samples/react-native/babel.config.js +++ b/samples/react-native/babel.config.js @@ -1,5 +1,3 @@ -//const componentAnnotatePlugin = require('@sentry/babel-plugin-component-annotate'); - module.exports = { presets: ['module:@react-native/babel-preset'], plugins: [ @@ -12,6 +10,5 @@ module.exports = { }, ], 'react-native-reanimated/plugin', - //componentAnnotatePlugin, ], }; diff --git a/src/js/tools/metroconfig.ts b/src/js/tools/metroconfig.ts index 9a1192df26..34a0c932af 100644 --- a/src/js/tools/metroconfig.ts +++ b/src/js/tools/metroconfig.ts @@ -4,11 +4,7 @@ import * as process from 'process'; import { env } from 'process'; import { enableLogger } from './enableLogger'; -import { - canUseSentryBabelTransformer, - cleanDefaultBabelTransformerPath, - saveDefaultBabelTransformerPath, -} from './sentryBabelTransformerUtils'; +import { cleanDefaultBabelTransformerPath, saveDefaultBabelTransformerPath } from './sentryBabelTransformerUtils'; import { createSentryMetroSerializer, unstable_beforeAssetSerializationPlugin } from './sentryMetroSerializer'; import type { DefaultConfigOptions } from './vendor/expo/expoconfig'; @@ -28,7 +24,7 @@ export interface SentryExpoConfigOptions { /** * Pass a custom `getDefaultConfig` function to override the default Expo configuration getter. */ - getDefaultConfig?: typeof getSentryExpoConfig + getDefaultConfig?: typeof getSentryExpoConfig; } /** @@ -100,20 +96,23 @@ function loadExpoMetroConfigModule(): { } } -function withSentryBabelTransformer(config: MetroConfig): MetroConfig { +/** + * Adds Sentry Babel transformer to the Metro config. + */ +export function withSentryBabelTransformer(config: MetroConfig): MetroConfig { const defaultBabelTransformerPath = config.transformer && config.transformer.babelTransformerPath; logger.debug('Default Babel transformer path from `config.transformer`:', defaultBabelTransformerPath); - if (!defaultBabelTransformerPath && !canUseSentryBabelTransformer(config.projectRoot)) { + if (!defaultBabelTransformerPath) { // eslint-disable-next-line no-console console.warn('Sentry Babel transformer cannot be used. Not adding it ...'); return config; } if (defaultBabelTransformerPath) { - saveDefaultBabelTransformerPath(config.projectRoot || '.', defaultBabelTransformerPath); + saveDefaultBabelTransformerPath(defaultBabelTransformerPath); process.on('exit', () => { - cleanDefaultBabelTransformerPath(config.projectRoot || '.'); + cleanDefaultBabelTransformerPath(); }); } diff --git a/src/js/tools/sentryBabelTransformer.ts b/src/js/tools/sentryBabelTransformer.ts index 1dc4f83ef7..e1833fab72 100644 --- a/src/js/tools/sentryBabelTransformer.ts +++ b/src/js/tools/sentryBabelTransformer.ts @@ -10,18 +10,14 @@ enableLogger(); * Creates a Babel transformer with Sentry component annotation plugin. */ function createSentryBabelTransformer(): BabelTransformer { - let defaultTransformer: BabelTransformer | undefined; + const defaultTransformer = loadDefaultBabelTransformer(); // Using spread operator to avoid any conflicts with the default transformer const transform: BabelTransformer['transform'] = (...args) => { const transformerArgs = args[0]; - const projectRoot = transformerArgs.options.projectRoot; - - if (!defaultTransformer) { - defaultTransformer = loadDefaultBabelTransformer(projectRoot); - } addSentryComponentAnnotatePlugin(transformerArgs); + return defaultTransformer.transform(...args); }; diff --git a/src/js/tools/sentryBabelTransformerUtils.ts b/src/js/tools/sentryBabelTransformerUtils.ts index 7e5c756b79..e9a6071414 100644 --- a/src/js/tools/sentryBabelTransformerUtils.ts +++ b/src/js/tools/sentryBabelTransformerUtils.ts @@ -1,16 +1,17 @@ import { logger } from '@sentry/utils'; import * as fs from 'fs'; import * as path from 'path'; +import * as process from 'process'; import type { BabelTransformer } from './vendor/metro/metroBabelTransformer'; /** * Saves default Babel transformer path to the project root. */ -export function saveDefaultBabelTransformerPath(projectRoot: string, defaultBabelTransformerPath: string): void { +export function saveDefaultBabelTransformerPath(defaultBabelTransformerPath: string): void { try { - fs.mkdirSync(path.join(projectRoot, '.sentry'), { recursive: true }); - fs.writeFileSync(getDefaultBabelTransformerPath(projectRoot), defaultBabelTransformerPath); + fs.mkdirSync(path.join(process.cwd(), '.sentry'), { recursive: true }); + fs.writeFileSync(getDefaultBabelTransformerPath(), defaultBabelTransformerPath); logger.debug('Saved default Babel transformer path'); } catch (e) { // eslint-disable-next-line no-console @@ -21,10 +22,10 @@ export function saveDefaultBabelTransformerPath(projectRoot: string, defaultBabe /** * Reads default Babel transformer path from the project root. */ -export function readDefaultBabelTransformerPath(projectRoot: string): string | undefined { +export function readDefaultBabelTransformerPath(): string | undefined { try { - if (fs.existsSync(getDefaultBabelTransformerPath(projectRoot))) { - return fs.readFileSync(getDefaultBabelTransformerPath(projectRoot)).toString(); + if (fs.existsSync(getDefaultBabelTransformerPath())) { + return fs.readFileSync(getDefaultBabelTransformerPath()).toString(); } } catch (e) { // eslint-disable-next-line no-console @@ -36,10 +37,10 @@ export function readDefaultBabelTransformerPath(projectRoot: string): string | u /** * Cleans default Babel transformer path from the project root. */ -export function cleanDefaultBabelTransformerPath(projectRoot: string): void { +export function cleanDefaultBabelTransformerPath(): void { try { - if (fs.existsSync(getDefaultBabelTransformerPath(projectRoot))) { - fs.unlinkSync(getDefaultBabelTransformerPath(projectRoot)); + if (fs.existsSync(getDefaultBabelTransformerPath())) { + fs.unlinkSync(getDefaultBabelTransformerPath()); } logger.debug('Cleaned default Babel transformer path'); } catch (e) { @@ -48,60 +49,20 @@ export function cleanDefaultBabelTransformerPath(projectRoot: string): void { } } -function getDefaultBabelTransformerPath(from: string): string { - return path.join(from, '.sentry/.defaultBabelTransformerPath'); +function getDefaultBabelTransformerPath(): string { + return path.join(process.cwd(), '.sentry/.defaultBabelTransformerPath'); } /** * Loads default Babel transformer from `@react-native/metro-config` -> `@react-native/metro-babel-transformer`. */ -export function loadDefaultBabelTransformer(projectRoot: string): BabelTransformer { - const defaultBabelTransformerPath = readDefaultBabelTransformerPath(projectRoot); - if (defaultBabelTransformerPath) { - logger.debug(`Loading default Babel transformer from ${defaultBabelTransformerPath}`); - // eslint-disable-next-line @typescript-eslint/no-var-requires - return require(defaultBabelTransformerPath); - } - - const reactNativeMetroConfigPath = resolveReactNativeMetroConfigPath(projectRoot); - if (!reactNativeMetroConfigPath) { - throw new Error('Cannot resolve `@react-native/metro-config` to find `@react-native/metro-babel-transformer`.'); - } - - let defaultTransformerPath: string; - try { - defaultTransformerPath = require.resolve('@react-native/metro-babel-transformer', { - paths: [reactNativeMetroConfigPath], - }); - logger.debug(`Resolved @react-native/metro-babel-transformer to ${defaultTransformerPath}`); - } catch (e) { - throw new Error('Cannot load `@react-native/metro-babel-transformer` from `${reactNativeMetroConfig}`.'); +export function loadDefaultBabelTransformer(): BabelTransformer { + const defaultBabelTransformerPath = readDefaultBabelTransformerPath(); + if (!defaultBabelTransformerPath) { + throw new Error('Default Babel transformer path not found'); } + logger.debug(`Loading default Babel transformer from ${defaultBabelTransformerPath}`); // eslint-disable-next-line @typescript-eslint/no-var-requires - const defaultTransformer = require(defaultTransformerPath); - return defaultTransformer; -} - -/** - * Checks current environment and installed dependencies to determine if Sentry Babel transformer can be used. - */ -export function canUseSentryBabelTransformer(projectRoot?: string): boolean { - return !!resolveReactNativeMetroConfigPath(projectRoot); -} - -/** - * Resolves path to the installed `@react-native/metro-config` package. - * Available since React Native 0.72 - */ -function resolveReactNativeMetroConfigPath(projectRoot?: string): string | undefined { - try { - const p = require.resolve('@react-native/metro-config', projectRoot ? { paths: [projectRoot] } : undefined); - logger.debug(`Resolved @react-native/metro-config to ${p}`); - return p; - } catch (e) { - // return undefined; - } - logger.debug('Failed to resolve @react-native/metro-config'); - return undefined; + return require(defaultBabelTransformerPath); } diff --git a/test/tools/fixtures/mockBabelTransformer.js b/test/tools/fixtures/mockBabelTransformer.js new file mode 100644 index 0000000000..17628495a5 --- /dev/null +++ b/test/tools/fixtures/mockBabelTransformer.js @@ -0,0 +1,4 @@ +module.exports = { + transform: jest.fn(), + getCacheKey: jest.fn(), +}; diff --git a/test/tools/metroconfig.test.ts b/test/tools/metroconfig.test.ts index 63312c8816..ef31de6886 100644 --- a/test/tools/metroconfig.test.ts +++ b/test/tools/metroconfig.test.ts @@ -1,33 +1,115 @@ +jest.mock('fs', () => { + return { + existsSync: jest.fn(), + mkdirSync: jest.fn(), + writeFileSync: jest.fn(), + unlinkSync: jest.fn(), + }; +}); + +import * as fs from 'fs'; import type { MetroConfig } from 'metro'; +import * as path from 'path'; +import * as process from 'process'; -import { withSentryFramesCollapsed } from '../../src/js/tools/metroconfig'; +import { withSentryBabelTransformer, withSentryFramesCollapsed } from '../../src/js/tools/metroconfig'; type MetroFrame = Parameters['symbolicator']>['customizeFrame']>[0]; -describe('withSentryFramesCollapsed', () => { - test('adds customizeFrames if undefined ', () => { - const config = withSentryFramesCollapsed({}); - expect(config.symbolicator?.customizeFrame).toBeDefined(); +describe('metroconfig', () => { + beforeEach(() => { + jest.clearAllMocks(); }); - test('wraps existing customizeFrames', async () => { - const originalCustomizeFrame = jest.fn(); - const config = withSentryFramesCollapsed({ symbolicator: { customizeFrame: originalCustomizeFrame } }); + describe('withSentryFramesCollapsed', () => { + test('adds customizeFrames if undefined ', () => { + const config = withSentryFramesCollapsed({}); + expect(config.symbolicator?.customizeFrame).toBeDefined(); + }); + + test('wraps existing customizeFrames', async () => { + const originalCustomizeFrame = jest.fn(); + const config = withSentryFramesCollapsed({ symbolicator: { customizeFrame: originalCustomizeFrame } }); + + const customizeFrame = config.symbolicator?.customizeFrame; + await customizeFrame?.(createMockSentryInstrumentMetroFrame()); - const customizeFrame = config.symbolicator?.customizeFrame; - await customizeFrame?.(createMockSentryInstrumentMetroFrame()); + expect(config.symbolicator?.customizeFrame).not.toBe(originalCustomizeFrame); + expect(originalCustomizeFrame).toHaveBeenCalledTimes(1); + }); - expect(config.symbolicator?.customizeFrame).not.toBe(originalCustomizeFrame); - expect(originalCustomizeFrame).toHaveBeenCalledTimes(1); + test('collapses sentry instrument frames', async () => { + const config = withSentryFramesCollapsed({}); + + const customizeFrame = config.symbolicator?.customizeFrame; + const customizedFrame = await customizeFrame?.(createMockSentryInstrumentMetroFrame()); + + expect(customizedFrame?.collapse).toBe(true); + }); }); - test('collapses sentry instrument frames', async () => { - const config = withSentryFramesCollapsed({}); + describe('withSentryBabelTransformer', () => { + test.each([[{}], [{ transformer: {} }], [{ transformer: { hermesParser: true } }]])( + "does not add babel transformer none is set in the config object '%o'", + input => { + expect(withSentryBabelTransformer(JSON.parse(JSON.stringify(input)))).toEqual(input); + }, + ); + + test.each([ + [{ transformer: { babelTransformerPath: 'babelTransformerPath' }, projectRoot: 'project/root' }], + [{ transformer: { babelTransformerPath: 'babelTransformerPath' } }], + ])('save default babel transformer path to a file', () => { + const defaultBabelTransformerPath = '/default/babel/transformer'; + + withSentryBabelTransformer({ + transformer: { + babelTransformerPath: defaultBabelTransformerPath, + }, + projectRoot: 'project/root', + }); + + expect(fs.mkdirSync).toHaveBeenCalledWith(path.join(process.cwd(), '.sentry'), { recursive: true }); + expect(fs.writeFileSync).toHaveBeenCalledWith( + path.join(process.cwd(), '.sentry/.defaultBabelTransformerPath'), + defaultBabelTransformerPath, + ); + }); + + test('clean default babel transformer path file on exit', () => { + (fs.existsSync as jest.Mock).mockReturnValue(true); + const processOnSpy: jest.SpyInstance = jest.spyOn(process, 'on'); + + const defaultBabelTransformerPath = 'defaultBabelTransformerPath'; + + withSentryBabelTransformer({ + transformer: { + babelTransformerPath: defaultBabelTransformerPath, + }, + projectRoot: 'project/root', + }); + + const actualExitHandler: () => void | undefined = processOnSpy.mock.calls[0][1]; + actualExitHandler?.(); + + expect(processOnSpy).toHaveBeenCalledWith('exit', expect.any(Function)); + expect(fs.existsSync).toHaveBeenCalledWith(path.join(process.cwd(), '.sentry/.defaultBabelTransformerPath')); + expect(fs.unlinkSync).toHaveBeenCalledWith(path.join(process.cwd(), '.sentry/.defaultBabelTransformerPath')); + }); + + test('return config with sentry babel transformer path', () => { + const defaultBabelTransformerPath = 'defaultBabelTransformerPath'; - const customizeFrame = config.symbolicator?.customizeFrame; - const customizedFrame = await customizeFrame?.(createMockSentryInstrumentMetroFrame()); + const config = withSentryBabelTransformer({ + transformer: { + babelTransformerPath: defaultBabelTransformerPath, + }, + }); - expect(customizedFrame?.collapse).toBe(true); + expect(config.transformer?.babelTransformerPath).toBe( + require.resolve('../../src/js/tools/sentryBabelTransformer'), + ); + }); }); }); diff --git a/test/tools/sentryBabelTransformer.test.ts b/test/tools/sentryBabelTransformer.test.ts new file mode 100644 index 0000000000..89ae8385f1 --- /dev/null +++ b/test/tools/sentryBabelTransformer.test.ts @@ -0,0 +1,55 @@ +jest.mock('fs', () => { + return { + existsSync: jest.fn(), + readFileSync: jest.fn(), + }; +}); + +import * as fs from 'fs'; +import * as path from 'path'; + +// needs to be defined before sentryBabelTransformer is imported +// the transformer is created on import (side effect) +(fs.existsSync as jest.Mock).mockReturnValue(true); +(fs.readFileSync as jest.Mock).mockReturnValue(require.resolve('./fixtures/mockBabelTransformer.js')); +import * as SentryBabelTransformer from '../../src/js/tools/sentryBabelTransformer'; +import type { BabelTransformerArgs } from '../../src/js/tools/vendor/metro/metroBabelTransformer'; + +const MockDefaultBabelTransformer: { + transform: jest.Mock; + getCacheKey: jest.Mock; +} = require('./fixtures/mockBabelTransformer'); + +describe('SentryBabelTransformer', () => { + // WARN: since the mocked fs is called during import we can't clear mock before each test + + test('getCacheKey calls the original transformer', () => { + SentryBabelTransformer.getCacheKey?.(); + + expect(SentryBabelTransformer.getCacheKey).toBeDefined(); + expect(MockDefaultBabelTransformer.getCacheKey).toHaveBeenCalledTimes(1); + }); + + test('transform calls the original transformer', () => { + SentryBabelTransformer.transform?.({ + filename: 'filename', + options: { + projectRoot: 'project/root', + }, + plugins: [], + } as BabelTransformerArgs); + + expect(fs.readFileSync).toHaveBeenCalledWith(path.join(process.cwd(), '.sentry/.defaultBabelTransformerPath')); + expect(MockDefaultBabelTransformer.transform).toHaveBeenCalledTimes(1); + expect(MockDefaultBabelTransformer.transform).toHaveBeenCalledWith({ + filename: 'filename', + options: { + projectRoot: 'project/root', + }, + plugins: [expect.any(Function)], + }); + expect(MockDefaultBabelTransformer.transform.mock.calls[0][0]['plugins'][0].name).toEqual( + 'componentNameAnnotatePlugin', + ); + }); +}); From 0a9d0e10d52ad7e7c25ef804102114ef69f13412 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Mon, 1 Jul 2024 10:54:04 +0200 Subject: [PATCH 07/14] add more tests --- test/tools/sentryBabelTransformer.test.ts | 52 +++++++++++++++++++---- 1 file changed, 43 insertions(+), 9 deletions(-) diff --git a/test/tools/sentryBabelTransformer.test.ts b/test/tools/sentryBabelTransformer.test.ts index 89ae8385f1..e5d5ecb7e2 100644 --- a/test/tools/sentryBabelTransformer.test.ts +++ b/test/tools/sentryBabelTransformer.test.ts @@ -6,12 +6,12 @@ jest.mock('fs', () => { }); import * as fs from 'fs'; -import * as path from 'path'; // needs to be defined before sentryBabelTransformer is imported // the transformer is created on import (side effect) (fs.existsSync as jest.Mock).mockReturnValue(true); (fs.readFileSync as jest.Mock).mockReturnValue(require.resolve('./fixtures/mockBabelTransformer.js')); + import * as SentryBabelTransformer from '../../src/js/tools/sentryBabelTransformer'; import type { BabelTransformerArgs } from '../../src/js/tools/vendor/metro/metroBabelTransformer'; @@ -21,7 +21,9 @@ const MockDefaultBabelTransformer: { } = require('./fixtures/mockBabelTransformer'); describe('SentryBabelTransformer', () => { - // WARN: since the mocked fs is called during import we can't clear mock before each test + beforeEach(() => { + jest.clearAllMocks(); + }); test('getCacheKey calls the original transformer', () => { SentryBabelTransformer.getCacheKey?.(); @@ -30,26 +32,58 @@ describe('SentryBabelTransformer', () => { expect(MockDefaultBabelTransformer.getCacheKey).toHaveBeenCalledTimes(1); }); - test('transform calls the original transformer', () => { + test('transform calls the original transformer with the annotation plugin', () => { SentryBabelTransformer.transform?.({ - filename: 'filename', + filename: '/project/file', options: { projectRoot: 'project/root', }, - plugins: [], + plugins: [jest.fn()], } as BabelTransformerArgs); - expect(fs.readFileSync).toHaveBeenCalledWith(path.join(process.cwd(), '.sentry/.defaultBabelTransformerPath')); expect(MockDefaultBabelTransformer.transform).toHaveBeenCalledTimes(1); expect(MockDefaultBabelTransformer.transform).toHaveBeenCalledWith({ - filename: 'filename', + filename: '/project/file', options: { projectRoot: 'project/root', }, - plugins: [expect.any(Function)], + plugins: [expect.any(Function), expect.any(Function)], }); - expect(MockDefaultBabelTransformer.transform.mock.calls[0][0]['plugins'][0].name).toEqual( + expect(MockDefaultBabelTransformer.transform.mock.calls[0][0]['plugins'][1].name).toEqual( 'componentNameAnnotatePlugin', ); }); + + test('transform adds plugin', () => { + SentryBabelTransformer.transform?.({ + filename: '/project/file', + options: { + projectRoot: 'project/root', + }, + plugins: [], + } as BabelTransformerArgs); + }); + + test.each([ + [ + { + filename: 'node_modules/file', + plugins: [jest.fn()], + } as BabelTransformerArgs, + ], + [ + { + filename: 'project/node_modules/file', + plugins: [jest.fn()], + } as BabelTransformerArgs, + ], + ])('transform does not add plugin if filename includes node_modules', input => { + SentryBabelTransformer.transform?.(input); + + expect(MockDefaultBabelTransformer.transform).toHaveBeenCalledTimes(1); + expect(MockDefaultBabelTransformer.transform).toHaveBeenCalledWith({ + filename: input.filename, + plugins: expect.not.arrayContaining([expect.objectContaining({ name: 'componentNameAnnotatePlugin' })]), + }); + }); }); From 4ac1605e99454ebbcc38c70d5e5bdc5d3d2a258d Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Mon, 1 Jul 2024 10:54:04 +0200 Subject: [PATCH 08/14] fix lint --- samples/react-native/src/Screens/TrackerScreen.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/react-native/src/Screens/TrackerScreen.tsx b/samples/react-native/src/Screens/TrackerScreen.tsx index 7be1635df7..5562274dfc 100644 --- a/samples/react-native/src/Screens/TrackerScreen.tsx +++ b/samples/react-native/src/Screens/TrackerScreen.tsx @@ -113,8 +113,8 @@ const TrackerScreen = () => { const TrackerTitle = () => ( - Global COVID19 Cases - + Global COVID19 Cases + ); export default Sentry.withProfiler(TrackerScreen); From f5d0d6e636b8ac7d2c3e68567c2558ee5fd61779 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Mon, 1 Jul 2024 10:54:04 +0200 Subject: [PATCH 09/14] add changelog --- CHANGELOG.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 808d4feb65..bbb5fa640c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,18 @@ - Improve touch event component info if annotated with [`@sentry/babel-plugin-component-annotate`](https://www.npmjs.com/package/@sentry/babel-plugin-component-annotate) ([#3899](https://github.com/getsentry/sentry-react-native/pull/3899)) - Add replay breadcrumbs for touch & navigation events ([#3846](https://github.com/getsentry/sentry-react-native/pull/3846)) - Add network data to Session Replays ([#3912](https://github.com/getsentry/sentry-react-native/pull/3912)) +- Add `annotateReactComponents` option to `@sentry/react-native/metro` ([#3916](https://github.com/getsentry/sentry-react-native/pull/3916)) + + ```js + // For Expo + const { getSentryExpoConfig } = require("@sentry/react-native/metro"); + const config = getSentryExpoConfig(__dirname, { annotateReactComponents: true }); + + // For RN + const { getDefaultConfig } = require('@react-native/metro-config'); + const { withSentryConfig } = require('@sentry/react-native/metro'); + module.exports = withSentryConfig(getDefaultConfig(__dirname), { annotateReactComponents: true }); + ``` ### Dependencies @@ -473,7 +485,7 @@ see [the Expo guide](https://docs.sentry.io/platforms/react-native/manual-setup/ const { getSentryExpoConfig } = require("@sentry/react-native/metro"); // const config = getDefaultConfig(__dirname); - const config = getSentryExpoConfig(config, {}); + const config = getSentryExpoConfig(__dirname); ``` - New `npx sentry-expo-upload-sourcemaps` for simple EAS Update (`npx expo export`) source maps upload ([#3491](https://github.com/getsentry/sentry-react-native/pull/3491), [#3510](https://github.com/getsentry/sentry-react-native/pull/3510), [#3515](https://github.com/getsentry/sentry-react-native/pull/3515), [#3507](https://github.com/getsentry/sentry-react-native/pull/3507)) From 35a0038aca55fd3bc191fd1eb9f13d79a8664b7f Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Mon, 1 Jul 2024 10:54:05 +0200 Subject: [PATCH 10/14] add react annotate to e2e tests --- test/react-native/rn.patch.metro.config.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/test/react-native/rn.patch.metro.config.js b/test/react-native/rn.patch.metro.config.js index 05cb2b4fe1..f4354c2153 100755 --- a/test/react-native/rn.patch.metro.config.js +++ b/test/react-native/rn.patch.metro.config.js @@ -20,6 +20,8 @@ const importSerializer = "const { withSentryConfig } = require('@sentry/react-na let config = fs.readFileSync(configFilePath, 'utf8').split('\n'); +const sentryOptions = '{ annotateReactComponents: true }'; + const isPatched = config.includes(importSerializer); if (!isPatched) { config = [importSerializer, ...config]; @@ -35,11 +37,18 @@ if (!isPatched) { lineParsed[1] = lineParsed[1].slice(0, -1); } - lineParsed[1] = `= withSentryConfig(${lineParsed[1]}${endsWithSemicolon ? ');' : ''}`; + lineParsed[1] = `= withSentryConfig(${lineParsed[1]}${endsWithSemicolon ? `, ${sentryOptions});` : ''}`; config[moduleExportsLineIndex] = lineParsed.join(''); if (endOfModuleExportsIndex !== -1) { - config[endOfModuleExportsIndex] = '});'; + config[endOfModuleExportsIndex] = `}, ${sentryOptions});`; + } + + // RN Before 0.72 does not include default config in the metro.config.js + // We have to specify babelTransformerPath manually + const transformerIndex = config.findIndex(line => line.includes('transformer: {')); + if (transformerIndex !== -1) { + config[transformerIndex] = `transformer: { babelTransformerPath: require.resolve('metro-babel-transformer'),`; } fs.writeFileSync(configFilePath, config.join('\n'), 'utf8'); From 4174f983224f26f8e664b348fa15d02927e3551b Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Mon, 1 Jul 2024 10:54:05 +0200 Subject: [PATCH 11/14] add reason why annotate plugin can't be used in the warning --- src/js/tools/metroconfig.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/js/tools/metroconfig.ts b/src/js/tools/metroconfig.ts index 34a0c932af..9226cd6475 100644 --- a/src/js/tools/metroconfig.ts +++ b/src/js/tools/metroconfig.ts @@ -105,7 +105,9 @@ export function withSentryBabelTransformer(config: MetroConfig): MetroConfig { if (!defaultBabelTransformerPath) { // eslint-disable-next-line no-console - console.warn('Sentry Babel transformer cannot be used. Not adding it ...'); + console.warn('`transformer.babelTransformerPath` is undefined.'); + // eslint-disable-next-line no-console + console.warn('Sentry Babel transformer cannot be used. Not adding it...'); return config; } From d31895ecf30656f58929cdd3d74752e6687bc8b5 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Mon, 1 Jul 2024 10:54:05 +0200 Subject: [PATCH 12/14] update debug logs --- src/js/tools/metroconfig.ts | 1 + src/js/tools/sentryBabelTransformerUtils.ts | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/js/tools/metroconfig.ts b/src/js/tools/metroconfig.ts index 9226cd6475..6e58544757 100644 --- a/src/js/tools/metroconfig.ts +++ b/src/js/tools/metroconfig.ts @@ -104,6 +104,7 @@ export function withSentryBabelTransformer(config: MetroConfig): MetroConfig { logger.debug('Default Babel transformer path from `config.transformer`:', defaultBabelTransformerPath); if (!defaultBabelTransformerPath) { + // This has to be console.warn because the options is enabled but won't be used // eslint-disable-next-line no-console console.warn('`transformer.babelTransformerPath` is undefined.'); // eslint-disable-next-line no-console diff --git a/src/js/tools/sentryBabelTransformerUtils.ts b/src/js/tools/sentryBabelTransformerUtils.ts index e9a6071414..db34e3428c 100644 --- a/src/js/tools/sentryBabelTransformerUtils.ts +++ b/src/js/tools/sentryBabelTransformerUtils.ts @@ -44,6 +44,7 @@ export function cleanDefaultBabelTransformerPath(): void { } logger.debug('Cleaned default Babel transformer path'); } catch (e) { + // We don't want to fail the build if we can't clean the file // eslint-disable-next-line no-console console.error('[Sentry] Failed to clean default Babel transformer path:', e); } @@ -59,7 +60,7 @@ function getDefaultBabelTransformerPath(): string { export function loadDefaultBabelTransformer(): BabelTransformer { const defaultBabelTransformerPath = readDefaultBabelTransformerPath(); if (!defaultBabelTransformerPath) { - throw new Error('Default Babel transformer path not found'); + throw new Error('Default Babel Transformer Path not found in `.sentry` directory.'); } logger.debug(`Loading default Babel transformer from ${defaultBabelTransformerPath}`); From c0fbda258412ac434b734caa293082cd3d9fdf44 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Wed, 3 Jul 2024 15:11:35 +0200 Subject: [PATCH 13/14] fix fs review comments --- src/js/tools/sentryBabelTransformerUtils.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/js/tools/sentryBabelTransformerUtils.ts b/src/js/tools/sentryBabelTransformerUtils.ts index db34e3428c..dd04d2f67b 100644 --- a/src/js/tools/sentryBabelTransformerUtils.ts +++ b/src/js/tools/sentryBabelTransformerUtils.ts @@ -24,9 +24,7 @@ export function saveDefaultBabelTransformerPath(defaultBabelTransformerPath: str */ export function readDefaultBabelTransformerPath(): string | undefined { try { - if (fs.existsSync(getDefaultBabelTransformerPath())) { - return fs.readFileSync(getDefaultBabelTransformerPath()).toString(); - } + return fs.readFileSync(getDefaultBabelTransformerPath()).toString(); } catch (e) { // eslint-disable-next-line no-console console.error('[Sentry] Failed to read default Babel transformer path:', e); @@ -39,9 +37,7 @@ export function readDefaultBabelTransformerPath(): string | undefined { */ export function cleanDefaultBabelTransformerPath(): void { try { - if (fs.existsSync(getDefaultBabelTransformerPath())) { - fs.unlinkSync(getDefaultBabelTransformerPath()); - } + fs.unlinkSync(getDefaultBabelTransformerPath()); logger.debug('Cleaned default Babel transformer path'); } catch (e) { // We don't want to fail the build if we can't clean the file From 49e403af1be004cf13b6849398c0dfb8b95a0e00 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Wed, 3 Jul 2024 17:25:06 +0200 Subject: [PATCH 14/14] fix tests --- test/tools/metroconfig.test.ts | 3 --- test/tools/sentryBabelTransformer.test.ts | 2 -- 2 files changed, 5 deletions(-) diff --git a/test/tools/metroconfig.test.ts b/test/tools/metroconfig.test.ts index ef31de6886..a0ee9533ff 100644 --- a/test/tools/metroconfig.test.ts +++ b/test/tools/metroconfig.test.ts @@ -1,6 +1,5 @@ jest.mock('fs', () => { return { - existsSync: jest.fn(), mkdirSync: jest.fn(), writeFileSync: jest.fn(), unlinkSync: jest.fn(), @@ -77,7 +76,6 @@ describe('metroconfig', () => { }); test('clean default babel transformer path file on exit', () => { - (fs.existsSync as jest.Mock).mockReturnValue(true); const processOnSpy: jest.SpyInstance = jest.spyOn(process, 'on'); const defaultBabelTransformerPath = 'defaultBabelTransformerPath'; @@ -93,7 +91,6 @@ describe('metroconfig', () => { actualExitHandler?.(); expect(processOnSpy).toHaveBeenCalledWith('exit', expect.any(Function)); - expect(fs.existsSync).toHaveBeenCalledWith(path.join(process.cwd(), '.sentry/.defaultBabelTransformerPath')); expect(fs.unlinkSync).toHaveBeenCalledWith(path.join(process.cwd(), '.sentry/.defaultBabelTransformerPath')); }); diff --git a/test/tools/sentryBabelTransformer.test.ts b/test/tools/sentryBabelTransformer.test.ts index e5d5ecb7e2..3c888d1195 100644 --- a/test/tools/sentryBabelTransformer.test.ts +++ b/test/tools/sentryBabelTransformer.test.ts @@ -1,6 +1,5 @@ jest.mock('fs', () => { return { - existsSync: jest.fn(), readFileSync: jest.fn(), }; }); @@ -9,7 +8,6 @@ import * as fs from 'fs'; // needs to be defined before sentryBabelTransformer is imported // the transformer is created on import (side effect) -(fs.existsSync as jest.Mock).mockReturnValue(true); (fs.readFileSync as jest.Mock).mockReturnValue(require.resolve('./fixtures/mockBabelTransformer.js')); import * as SentryBabelTransformer from '../../src/js/tools/sentryBabelTransformer';