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
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,27 @@

## Unreleased

### Features

- Add `ignoredComponents` option to `annotateReactComponents` to exclude specific components from React component annotations ([#4517](https://github.com/getsentry/sentry-react-native/pull/4517))

```js
// metro.config.js
// for React Native
const config = withSentryConfig(mergedConfig, {
annotateReactComponents: {
ignoredComponents: ['MyCustomComponent']
}
});

// for Expo
const config = getSentryExpoConfig(__dirname, {
annotateReactComponents: {
ignoredComponents: ['MyCustomComponent'],
},
});
```

### Dependencies

- Bump JavaScript SDK from v8.53.0 to v8.54.0 ([#4503](https://github.com/getsentry/sentry-react-native/pull/4503))
Expand Down
26 changes: 21 additions & 5 deletions packages/core/src/js/tools/metroconfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import * as process from 'process';
import { env } from 'process';

import { enableLogger } from './enableLogger';
import { setSentryDefaultBabelTransformerPathEnv } from './sentryBabelTransformerUtils';
import {
setSentryBabelTransformerOptions,
setSentryDefaultBabelTransformerPathEnv,
} from './sentryBabelTransformerUtils';
import { createSentryMetroSerializer, unstable_beforeAssetSerializationPlugin } from './sentryMetroSerializer';
import type { DefaultConfigOptions } from './vendor/expo/expoconfig';
export * from './sentryMetroSerializer';
Expand All @@ -18,7 +21,11 @@ export interface SentryMetroConfigOptions {
* Annotates React components with Sentry data.
* @default false
*/
annotateReactComponents?: boolean;
annotateReactComponents?:
| boolean
| {
ignoredComponents?: string[];
};
/**
* Adds the Sentry replay package for web.
* @default true
Expand Down Expand Up @@ -60,7 +67,7 @@ export function withSentryConfig(
newConfig = withSentryDebugId(newConfig);
newConfig = withSentryFramesCollapsed(newConfig);
if (annotateReactComponents) {
newConfig = withSentryBabelTransformer(newConfig);
newConfig = withSentryBabelTransformer(newConfig, annotateReactComponents);
}
if (includeWebReplay === false) {
newConfig = withSentryResolver(newConfig, includeWebReplay);
Expand Down Expand Up @@ -92,7 +99,7 @@ export function getSentryExpoConfig(

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

if (options.includeWebReplay === false) {
Expand Down Expand Up @@ -129,7 +136,10 @@ function loadExpoMetroConfigModule(): {
/**
* Adds Sentry Babel transformer to the Metro config.
*/
export function withSentryBabelTransformer(config: MetroConfig): MetroConfig {
export function withSentryBabelTransformer(
config: MetroConfig,
annotateReactComponents: true | { ignoredComponents?: string[] },
): MetroConfig {
const defaultBabelTransformerPath = config.transformer && config.transformer.babelTransformerPath;
logger.debug('Default Babel transformer path from `config.transformer`:', defaultBabelTransformerPath);

Expand All @@ -146,6 +156,12 @@ export function withSentryBabelTransformer(config: MetroConfig): MetroConfig {
setSentryDefaultBabelTransformerPathEnv(defaultBabelTransformerPath);
}

if (typeof annotateReactComponents === 'object') {
setSentryBabelTransformerOptions({
annotateReactComponents,
});
}

return {
...config,
transformer: {
Expand Down
36 changes: 1 addition & 35 deletions packages/core/src/js/tools/sentryBabelTransformer.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,8 @@
import componentAnnotatePlugin from '@sentry/babel-plugin-component-annotate';

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

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
Expand Down
88 changes: 87 additions & 1 deletion packages/core/src/js/tools/sentryBabelTransformerUtils.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import componentAnnotatePlugin from '@sentry/babel-plugin-component-annotate';
import { logger } from '@sentry/core';
import * as process from 'process';

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

export type SentryBabelTransformerOptions = { annotateReactComponents?: { ignoredComponents?: string[] } };

export const SENTRY_DEFAULT_BABEL_TRANSFORMER_PATH = 'SENTRY_DEFAULT_BABEL_TRANSFORMER_PATH';
export const SENTRY_BABEL_TRANSFORMER_OPTIONS = 'SENTRY_BABEL_TRANSFORMER_OPTIONS';

/**
* Sets default Babel transformer path to the environment variables.
Expand Down Expand Up @@ -35,3 +39,85 @@ export function loadDefaultBabelTransformer(): BabelTransformer {
// eslint-disable-next-line @typescript-eslint/no-var-requires
return require(defaultBabelTransformerPath);
}

/**
*
*/
export function setSentryBabelTransformerOptions(options: SentryBabelTransformerOptions): void {
let optionsString: string | null = null;
try {
logger.debug(`Stringifying Sentry Babel transformer options`, options);
optionsString = JSON.stringify(options);
} catch (e) {
// eslint-disable-next-line no-console
console.error('Failed to stringify Sentry Babel transformer options', e);
}

if (!optionsString) {
return;
}

logger.debug(`Sentry Babel transformer options set to ${SENTRY_BABEL_TRANSFORMER_OPTIONS}`, optionsString);
process.env[SENTRY_BABEL_TRANSFORMER_OPTIONS] = optionsString;
}

/**
*
*/
export function getSentryBabelTransformerOptions(): SentryBabelTransformerOptions | undefined {
const optionsString = process.env[SENTRY_BABEL_TRANSFORMER_OPTIONS];
if (!optionsString) {
logger.debug(
`Sentry Babel transformer options environment variable ${SENTRY_BABEL_TRANSFORMER_OPTIONS} is not set`,
);
return undefined;
}

try {
logger.debug(`Parsing Sentry Babel transformer options from ${optionsString}`);
return JSON.parse(optionsString);
} catch (e) {
// eslint-disable-next-line no-console
console.error('Failed to parse Sentry Babel transformer options', e);
return undefined;
}
}

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

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

addSentryComponentAnnotatePlugin(transformerArgs, options?.annotateReactComponents);

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

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

function addSentryComponentAnnotatePlugin(
args: BabelTransformerArgs | undefined,
options: SentryBabelTransformerOptions['annotateReactComponents'] | undefined,
): void {
if (!args || typeof args.filename !== 'string' || !Array.isArray(args.plugins)) {
return undefined;
}

if (!args.filename.includes('node_modules')) {
if (options) {
args.plugins.push([componentAnnotatePlugin, options]);
} else {
args.plugins.push(componentAnnotatePlugin);
}
}
}
71 changes: 61 additions & 10 deletions packages/core/test/tools/metroconfig.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,21 @@ import {
withSentryFramesCollapsed,
withSentryResolver,
} from '../../src/js/tools/metroconfig';
import { SENTRY_DEFAULT_BABEL_TRANSFORMER_PATH } from '../../src/js/tools/sentryBabelTransformerUtils';
import {
SENTRY_BABEL_TRANSFORMER_OPTIONS,
SENTRY_DEFAULT_BABEL_TRANSFORMER_PATH,
} from '../../src/js/tools/sentryBabelTransformerUtils';

type MetroFrame = Parameters<Required<Required<MetroConfig>['symbolicator']>['customizeFrame']>[0];

describe('metroconfig', () => {
beforeEach(() => {
jest.clearAllMocks();

// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete process.env[SENTRY_BABEL_TRANSFORMER_OPTIONS];
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete process.env[SENTRY_DEFAULT_BABEL_TRANSFORMER_PATH];
});

test('getSentryExpoConfig keeps compatible interface with Expos getDefaultConfig', () => {
Expand Down Expand Up @@ -55,35 +63,78 @@ describe('metroconfig', () => {
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);
expect(withSentryBabelTransformer(JSON.parse(JSON.stringify(input)), {})).toEqual(input);
},
);

test('save default babel transformer path to environment variable', () => {
const defaultBabelTransformerPath = '/default/babel/transformer';

withSentryBabelTransformer({
transformer: {
babelTransformerPath: defaultBabelTransformerPath,
withSentryBabelTransformer(
{
transformer: {
babelTransformerPath: defaultBabelTransformerPath,
},
},
});
{},
);

expect(process.env[SENTRY_DEFAULT_BABEL_TRANSFORMER_PATH]).toBe(defaultBabelTransformerPath);
});

test('return config with sentry babel transformer path', () => {
const defaultBabelTransformerPath = 'defaultBabelTransformerPath';

const config = withSentryBabelTransformer({
transformer: {
babelTransformerPath: defaultBabelTransformerPath,
const config = withSentryBabelTransformer(
{
transformer: {
babelTransformerPath: defaultBabelTransformerPath,
},
},
});
{},
);

expect(config.transformer?.babelTransformerPath).toBe(
require.resolve('../../src/js/tools/sentryBabelTransformer'),
);
});

test('save babel transformer options to environment variable', () => {
withSentryBabelTransformer(
{
transformer: {
babelTransformerPath: 'path/to/babel/transformer',
},
},
{
ignoredComponents: ['MyCustomComponent'],
},
);

expect(process.env[SENTRY_BABEL_TRANSFORMER_OPTIONS]).toBe(
JSON.stringify({
annotateReactComponents: {
ignoredComponents: ['MyCustomComponent'],
},
}),
);
});

test('gracefully handle none serializable babel transformer options', () => {
withSentryBabelTransformer(
{
transformer: {
babelTransformerPath: 'path/to/babel/transformer',
},
},
{
ignoredComponents: ['MyCustomComponent'],
nonSerializable: BigInt(1),
} as any,
);

expect(process.env[SENTRY_BABEL_TRANSFORMER_OPTIONS]).toBeUndefined();
});
});

describe('withSentryResolver', () => {
Expand Down
Loading
Loading