Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,6 @@ yalc.lock

# E2E tests
test/react-native/versions

# Created by Sentry Metro Plugin
.sentry/
19 changes: 18 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
# Changelog

## Unreleased

### Features

- 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 });
```

## 5.25.0-alpha.2

### Features
Expand Down Expand Up @@ -483,7 +500,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))
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions samples/expo/app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
3 changes: 0 additions & 3 deletions samples/expo/babel.config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
const componentAnnotatePlugin = require('@sentry/babel-plugin-component-annotate');

module.exports = function (api) {
api.cache(false);
return {
Expand All @@ -13,7 +11,6 @@ module.exports = function (api) {
},
},
],
componentAnnotatePlugin,
],
};
};
1 change: 1 addition & 0 deletions samples/expo/metro.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
Expand Down
3 changes: 0 additions & 3 deletions samples/react-native/babel.config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
const componentAnnotatePlugin = require('@sentry/babel-plugin-component-annotate');

module.exports = {
presets: ['module:@react-native/babel-preset'],
plugins: [
Expand All @@ -12,6 +10,5 @@ module.exports = {
},
],
'react-native-reanimated/plugin',
componentAnnotatePlugin,
],
};
4 changes: 3 additions & 1 deletion samples/react-native/metro.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,6 @@ const config = {
};

const m = mergeConfig(getDefaultConfig(__dirname), config);
module.exports = withSentryConfig(m);
module.exports = withSentryConfig(m, {
annotateReactComponents: true,
});
10 changes: 7 additions & 3 deletions samples/react-native/src/Screens/TrackerScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,7 @@ const TrackerScreen = () => {
return (
<View style={styles.screen}>
<Sentry.TimeToInitialDisplay record />
<View style={styles.titleContainer}>
<Text style={styles.title}>Global COVID19 Cases</Text>
</View>
<TrackerTitle />
<View style={styles.card}>
{cases ? (
<>
Expand Down Expand Up @@ -111,6 +109,12 @@ const TrackerScreen = () => {
);
};

const TrackerTitle = () => (
<View style={styles.titleContainer}>
<Text style={styles.title}>Global COVID19 Cases</Text>
</View>
);

export default Sentry.withProfiler(TrackerScreen);

const Statistic = (props: {
Expand Down
10 changes: 10 additions & 0 deletions src/js/tools/enableLogger.ts
Original file line number Diff line number Diff line change
@@ -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();
}
}
70 changes: 67 additions & 3 deletions src/js/tools/metroconfig.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,51 @@
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 { 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;
}

export interface SentryExpoConfigOptions {
/**
* Pass a custom `getDefaultConfig` function to override the default Expo configuration getter.
*/
getDefaultConfig?: typeof getSentryExpoConfig;
}

/**
* 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);
if (annotateReactComponents) {
newConfig = withSentryBabelTransformer(newConfig);
}

return newConfig;
}
Expand All @@ -28,7 +55,7 @@ export function withSentryConfig(config: MetroConfig): MetroConfig {
*/
export function getSentryExpoConfig(
projectRoot: string,
options: DefaultConfigOptions & { getDefaultConfig?: typeof getSentryExpoConfig } = {},
options: DefaultConfigOptions & SentryExpoConfigOptions & SentryMetroConfigOptions = {},
): MetroConfig {
setSentryMetroDevServerEnvFlag();

Expand All @@ -41,7 +68,12 @@ export function getSentryExpoConfig(
],
});

return withSentryFramesCollapsed(config);
let newConfig = withSentryFramesCollapsed(config);
if (options.annotateReactComponents) {
newConfig = withSentryBabelTransformer(newConfig);
}

return newConfig;
}

function loadExpoMetroConfigModule(): {
Expand All @@ -64,6 +96,38 @@ function loadExpoMetroConfigModule(): {
}
}

/**
* 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);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Sentry SDK logger is mostly intended to be used when the SDK is initialized. For build tooling we usually don't rely on it. I would do a good ol' console.log instead.


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
console.warn('Sentry Babel transformer cannot be used. Not adding it...');
return config;
}

if (defaultBabelTransformerPath) {
saveDefaultBabelTransformerPath(defaultBabelTransformerPath);
process.on('exit', () => {
cleanDefaultBabelTransformerPath();
});
}

return {
...config,
transformer: {
...config.transformer,
babelTransformerPath: require.resolve('./sentryBabelTransformer'),
},
};
}

type MetroCustomSerializer = Required<Required<MetroConfig>['serializer']>['customSerializer'] | undefined;

function withSentryDebugId(config: MetroConfig): MetroConfig {
Expand Down
43 changes: 43 additions & 0 deletions src/js/tools/sentryBabelTransformer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import componentAnnotatePlugin from '@sentry/babel-plugin-component-annotate';

import { enableLogger } from './enableLogger';
import { loadDefaultBabelTransformer } from './sentryBabelTransformerUtils';
import type { BabelTransformer, BabelTransformerArgs } from './vendor/metro/metroBabelTransformer';

enableLogger();

/**
* Creates a Babel transformer with Sentry component annotation plugin.
*/
function createSentryBabelTransformer(): BabelTransformer {
const defaultTransformer = loadDefaultBabelTransformer();

// Using spread operator to avoid any conflicts with the default transformer
const transform: BabelTransformer['transform'] = (...args) => {
const transformerArgs = args[0];

addSentryComponentAnnotatePlugin(transformerArgs);

return defaultTransformer.transform(...args);
};

return {
...defaultTransformer,
transform,
};
}

function addSentryComponentAnnotatePlugin(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);
}
}

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;
65 changes: 65 additions & 0 deletions src/js/tools/sentryBabelTransformerUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
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(defaultBabelTransformerPath: string): void {
try {
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
console.error('[Sentry] Failed to save default Babel transformer path:', e);
}
}

/**
* Reads default Babel transformer path from the project root.
*/
export function readDefaultBabelTransformerPath(): string | undefined {
try {
return fs.readFileSync(getDefaultBabelTransformerPath()).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(): void {
try {
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
// eslint-disable-next-line no-console
console.error('[Sentry] Failed to clean default Babel transformer path:', e);
}
}

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(): BabelTransformer {
const defaultBabelTransformerPath = readDefaultBabelTransformerPath();
if (!defaultBabelTransformerPath) {
throw new Error('Default Babel Transformer Path not found in `.sentry` directory.');
}

logger.debug(`Loading default Babel transformer from ${defaultBabelTransformerPath}`);
// eslint-disable-next-line @typescript-eslint/no-var-requires
return require(defaultBabelTransformerPath);
}
Loading