Skip to content

Commit fd77856

Browse files
Merge ab136a5 into ac72abc
2 parents ac72abc + ab136a5 commit fd77856

File tree

12 files changed

+315
-9
lines changed

12 files changed

+315
-9
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,6 @@ yalc.lock
7474

7575
# E2E tests
7676
test/react-native/versions
77+
78+
# Created by Sentry Metro Plugin
79+
.sentry/

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
"react-native": ">=0.65.0"
6868
},
6969
"dependencies": {
70+
"@sentry/babel-plugin-component-annotate": "2.20.1",
7071
"@sentry/browser": "7.117.0",
7172
"@sentry/cli": "2.31.2",
7273
"@sentry/core": "7.117.0",

samples/expo/metro.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const config = getSentryExpoConfig(__dirname, {
99
// [Web-only]: Enables CSS support in Metro.
1010
isCSSEnabled: true,
1111
getDefaultConfig,
12+
annotateReactComponents: true,
1213
});
1314

1415
config.watchFolders.push(path.resolve(__dirname, '../../node_modules/@sentry'));

samples/react-native/babel.config.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const componentAnnotatePlugin = require('@sentry/babel-plugin-component-annotate');
1+
//const componentAnnotatePlugin = require('@sentry/babel-plugin-component-annotate');
22

33
module.exports = {
44
presets: ['module:@react-native/babel-preset'],
@@ -12,6 +12,6 @@ module.exports = {
1212
},
1313
],
1414
'react-native-reanimated/plugin',
15-
componentAnnotatePlugin,
15+
//componentAnnotatePlugin,
1616
],
1717
};

samples/react-native/metro.config.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,4 +60,6 @@ const config = {
6060
};
6161

6262
const m = mergeConfig(getDefaultConfig(__dirname), config);
63-
module.exports = withSentryConfig(m);
63+
module.exports = withSentryConfig(m, {
64+
annotateReactComponents: true,
65+
});

samples/react-native/src/Screens/TrackerScreen.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,7 @@ const TrackerScreen = () => {
7575
return (
7676
<View style={styles.screen}>
7777
<Sentry.TimeToInitialDisplay record />
78-
<View style={styles.titleContainer}>
79-
<Text style={styles.title}>Global COVID19 Cases</Text>
80-
</View>
78+
<TrackerTitle />
8179
<View style={styles.card}>
8280
{cases ? (
8381
<>
@@ -113,6 +111,12 @@ const TrackerScreen = () => {
113111
);
114112
};
115113

114+
const TrackerTitle = () => (
115+
<View style={styles.titleContainer}>
116+
<Text style={styles.title}>Global COVID19 Cases</Text>
117+
</View>
118+
);
119+
116120
export default Sentry.withProfiler(TrackerScreen);
117121

118122
const Statistic = (props: {

src/js/tools/enableLogger.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { logger } from '@sentry/utils';
2+
3+
/**
4+
* Enables debug logger when SENTRY_LOG_LEVEL=debug.
5+
*/
6+
export function enableLogger(): void {
7+
if (process.env.SENTRY_LOG_LEVEL === 'debug') {
8+
logger.enable();
9+
}
10+
}

src/js/tools/metroconfig.ts

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,55 @@
1+
import { logger } from '@sentry/utils';
12
import type { MetroConfig, MixedOutput, Module, ReadOnlyGraph } from 'metro';
3+
import * as process from 'process';
24
import { env } from 'process';
35

6+
import { enableLogger } from './enableLogger';
7+
import {
8+
canUseSentryBabelTransformer,
9+
cleanDefaultBabelTransformerPath,
10+
saveDefaultBabelTransformerPath,
11+
} from './sentryBabelTransformerUtils';
412
import { createSentryMetroSerializer, unstable_beforeAssetSerializationPlugin } from './sentryMetroSerializer';
513
import type { DefaultConfigOptions } from './vendor/expo/expoconfig';
614

715
export * from './sentryMetroSerializer';
816

17+
enableLogger();
18+
19+
export interface SentryMetroConfigOptions {
20+
/**
21+
* Annotates React components with Sentry data.
22+
* @default false
23+
*/
24+
annotateReactComponents?: boolean;
25+
}
26+
27+
export interface SentryExpoConfigOptions {
28+
/**
29+
* Pass a custom `getDefaultConfig` function to override the default Expo configuration getter.
30+
*/
31+
getDefaultConfig?: typeof getSentryExpoConfig
32+
}
33+
934
/**
1035
* Adds Sentry to the Metro config.
1136
*
1237
* Adds Debug ID to the output bundle and source maps.
1338
* Collapses Sentry frames from the stack trace view in LogBox.
1439
*/
15-
export function withSentryConfig(config: MetroConfig): MetroConfig {
40+
export function withSentryConfig(
41+
config: MetroConfig,
42+
{ annotateReactComponents = false }: SentryMetroConfigOptions = {},
43+
): MetroConfig {
1644
setSentryMetroDevServerEnvFlag();
1745

1846
let newConfig = config;
1947

2048
newConfig = withSentryDebugId(newConfig);
2149
newConfig = withSentryFramesCollapsed(newConfig);
50+
if (annotateReactComponents) {
51+
newConfig = withSentryBabelTransformer(newConfig);
52+
}
2253

2354
return newConfig;
2455
}
@@ -28,7 +59,7 @@ export function withSentryConfig(config: MetroConfig): MetroConfig {
2859
*/
2960
export function getSentryExpoConfig(
3061
projectRoot: string,
31-
options: DefaultConfigOptions & { getDefaultConfig?: typeof getSentryExpoConfig } = {},
62+
options: DefaultConfigOptions & SentryExpoConfigOptions & SentryMetroConfigOptions = {},
3263
): MetroConfig {
3364
setSentryMetroDevServerEnvFlag();
3465

@@ -41,7 +72,12 @@ export function getSentryExpoConfig(
4172
],
4273
});
4374

44-
return withSentryFramesCollapsed(config);
75+
let newConfig = withSentryFramesCollapsed(config);
76+
if (options.annotateReactComponents) {
77+
newConfig = withSentryBabelTransformer(newConfig);
78+
}
79+
80+
return newConfig;
4581
}
4682

4783
function loadExpoMetroConfigModule(): {
@@ -64,6 +100,32 @@ function loadExpoMetroConfigModule(): {
64100
}
65101
}
66102

103+
function withSentryBabelTransformer(config: MetroConfig): MetroConfig {
104+
const defaultBabelTransformerPath = config.transformer && config.transformer.babelTransformerPath;
105+
logger.debug('Default Babel transformer path from `config.transformer`:', defaultBabelTransformerPath);
106+
107+
if (!defaultBabelTransformerPath && !canUseSentryBabelTransformer(config.projectRoot)) {
108+
// eslint-disable-next-line no-console
109+
console.warn('Sentry Babel transformer cannot be used. Not adding it ...');
110+
return config;
111+
}
112+
113+
if (defaultBabelTransformerPath) {
114+
saveDefaultBabelTransformerPath(config.projectRoot || '.', defaultBabelTransformerPath);
115+
process.on('exit', () => {
116+
cleanDefaultBabelTransformerPath(config.projectRoot || '.');
117+
});
118+
}
119+
120+
return {
121+
...config,
122+
transformer: {
123+
...config.transformer,
124+
babelTransformerPath: require.resolve('./sentryBabelTransformer'),
125+
},
126+
};
127+
}
128+
67129
type MetroCustomSerializer = Required<Required<MetroConfig>['serializer']>['customSerializer'] | undefined;
68130

69131
function withSentryDebugId(config: MetroConfig): MetroConfig {
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import componentAnnotatePlugin from '@sentry/babel-plugin-component-annotate';
2+
3+
import { enableLogger } from './enableLogger';
4+
import { loadDefaultBabelTransformer } from './sentryBabelTransformerUtils';
5+
import type { BabelTransformer, BabelTransformerArgs } from './vendor/metro/metroBabelTransformer';
6+
7+
enableLogger();
8+
9+
/**
10+
* Creates a Babel transformer with Sentry component annotation plugin.
11+
*/
12+
function createSentryBabelTransformer(): BabelTransformer {
13+
let defaultTransformer: BabelTransformer | undefined;
14+
15+
// Using spread operator to avoid any conflicts with the default transformer
16+
const transform: BabelTransformer['transform'] = (...args) => {
17+
const transformerArgs = args[0];
18+
const projectRoot = transformerArgs.options.projectRoot;
19+
20+
if (!defaultTransformer) {
21+
defaultTransformer = loadDefaultBabelTransformer(projectRoot);
22+
}
23+
24+
addSentryComponentAnnotatePlugin(transformerArgs);
25+
return defaultTransformer.transform(...args);
26+
};
27+
28+
return {
29+
...defaultTransformer,
30+
transform,
31+
};
32+
}
33+
34+
function addSentryComponentAnnotatePlugin(args: BabelTransformerArgs | undefined): void {
35+
if (!args || typeof args.filename !== 'string' || !Array.isArray(args.plugins)) {
36+
return undefined;
37+
}
38+
39+
if (!args.filename.includes('node_modules')) {
40+
args.plugins.push(componentAnnotatePlugin);
41+
}
42+
}
43+
44+
const sentryBabelTransformer = createSentryBabelTransformer();
45+
// With TS set to `commonjs` this will be translated to `module.exports = sentryBabelTransformer;`
46+
// which will be correctly picked up by Metro
47+
export = sentryBabelTransformer;
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { logger } from '@sentry/utils';
2+
import * as fs from 'fs';
3+
import * as path from 'path';
4+
5+
import type { BabelTransformer } from './vendor/metro/metroBabelTransformer';
6+
7+
/**
8+
* Saves default Babel transformer path to the project root.
9+
*/
10+
export function saveDefaultBabelTransformerPath(projectRoot: string, defaultBabelTransformerPath: string): void {
11+
try {
12+
fs.mkdirSync(path.join(projectRoot, '.sentry'), { recursive: true });
13+
fs.writeFileSync(getDefaultBabelTransformerPath(projectRoot), defaultBabelTransformerPath);
14+
logger.debug('Saved default Babel transformer path');
15+
} catch (e) {
16+
// eslint-disable-next-line no-console
17+
console.error('[Sentry] Failed to save default Babel transformer path:', e);
18+
}
19+
}
20+
21+
/**
22+
* Reads default Babel transformer path from the project root.
23+
*/
24+
export function readDefaultBabelTransformerPath(projectRoot: string): string | undefined {
25+
try {
26+
if (fs.existsSync(getDefaultBabelTransformerPath(projectRoot))) {
27+
return fs.readFileSync(getDefaultBabelTransformerPath(projectRoot)).toString();
28+
}
29+
} catch (e) {
30+
// eslint-disable-next-line no-console
31+
console.error('[Sentry] Failed to read default Babel transformer path:', e);
32+
}
33+
return undefined;
34+
}
35+
36+
/**
37+
* Cleans default Babel transformer path from the project root.
38+
*/
39+
export function cleanDefaultBabelTransformerPath(projectRoot: string): void {
40+
try {
41+
if (fs.existsSync(getDefaultBabelTransformerPath(projectRoot))) {
42+
fs.unlinkSync(getDefaultBabelTransformerPath(projectRoot));
43+
}
44+
logger.debug('Cleaned default Babel transformer path');
45+
} catch (e) {
46+
// eslint-disable-next-line no-console
47+
console.error('[Sentry] Failed to clean default Babel transformer path:', e);
48+
}
49+
}
50+
51+
function getDefaultBabelTransformerPath(from: string): string {
52+
return path.join(from, '.sentry/.defaultBabelTransformerPath');
53+
}
54+
55+
/**
56+
* Loads default Babel transformer from `@react-native/metro-config` -> `@react-native/metro-babel-transformer`.
57+
*/
58+
export function loadDefaultBabelTransformer(projectRoot: string): BabelTransformer {
59+
const defaultBabelTransformerPath = readDefaultBabelTransformerPath(projectRoot);
60+
if (defaultBabelTransformerPath) {
61+
logger.debug(`Loading default Babel transformer from ${defaultBabelTransformerPath}`);
62+
// eslint-disable-next-line @typescript-eslint/no-var-requires
63+
return require(defaultBabelTransformerPath);
64+
}
65+
66+
const reactNativeMetroConfigPath = resolveReactNativeMetroConfigPath(projectRoot);
67+
if (!reactNativeMetroConfigPath) {
68+
throw new Error('Cannot resolve `@react-native/metro-config` to find `@react-native/metro-babel-transformer`.');
69+
}
70+
71+
let defaultTransformerPath: string;
72+
try {
73+
defaultTransformerPath = require.resolve('@react-native/metro-babel-transformer', {
74+
paths: [reactNativeMetroConfigPath],
75+
});
76+
logger.debug(`Resolved @react-native/metro-babel-transformer to ${defaultTransformerPath}`);
77+
} catch (e) {
78+
throw new Error('Cannot load `@react-native/metro-babel-transformer` from `${reactNativeMetroConfig}`.');
79+
}
80+
81+
// eslint-disable-next-line @typescript-eslint/no-var-requires
82+
const defaultTransformer = require(defaultTransformerPath);
83+
return defaultTransformer;
84+
}
85+
86+
/**
87+
* Checks current environment and installed dependencies to determine if Sentry Babel transformer can be used.
88+
*/
89+
export function canUseSentryBabelTransformer(projectRoot?: string): boolean {
90+
return !!resolveReactNativeMetroConfigPath(projectRoot);
91+
}
92+
93+
/**
94+
* Resolves path to the installed `@react-native/metro-config` package.
95+
* Available since React Native 0.72
96+
*/
97+
function resolveReactNativeMetroConfigPath(projectRoot?: string): string | undefined {
98+
try {
99+
const p = require.resolve('@react-native/metro-config', projectRoot ? { paths: [projectRoot] } : undefined);
100+
logger.debug(`Resolved @react-native/metro-config to ${p}`);
101+
return p;
102+
} catch (e) {
103+
// return undefined;
104+
}
105+
logger.debug('Failed to resolve @react-native/metro-config');
106+
return undefined;
107+
}

0 commit comments

Comments
 (0)