Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add exclude Web Replay option to Metro plugin #4006

Merged
merged 40 commits into from
Sep 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
73c80fc
soft rollback later
lucas-zimerman Aug 2, 2024
8b42ad9
add sentry exclude for replay
lucas-zimerman Aug 9, 2024
bac3255
Merge remote-tracking branch 'origin/main' into ref/exclude-replay
lucas-zimerman Aug 9, 2024
76764d1
changelog
lucas-zimerman Aug 9, 2024
ce604ba
clear tests
lucas-zimerman Aug 9, 2024
3f909f1
clearup PT2
lucas-zimerman Aug 9, 2024
4651c7e
Merge branch 'main' into ref/exclude-replay
lucas-zimerman Aug 9, 2024
9318296
mergeConfig not compatible with old RN 0.65.3
lucas-zimerman Aug 9, 2024
cc4f11d
Merge remote-tracking branch 'origin/ref/exclude-replay' into ref/exc…
lucas-zimerman Aug 9, 2024
3d1b3a1
add changelog snippet, also remove it on expo
lucas-zimerman Aug 9, 2024
c0556fc
Update CHANGELOG.md
lucas-zimerman Aug 15, 2024
0799e0e
Update src/js/tools/metroconfig.ts
lucas-zimerman Aug 15, 2024
7011eb4
Merge branch 'main' into ref/exclude-replay
lucas-zimerman Aug 15, 2024
271bebc
add tests and requested changes
lucas-zimerman Aug 15, 2024
060294d
changelog
lucas-zimerman Aug 15, 2024
27132ce
fix
lucas-zimerman Aug 15, 2024
46ab2a9
Merge branch 'main' into ref/exclude-replay
krystofwoldrich Aug 28, 2024
e663679
bad merge
krystofwoldrich Aug 28, 2024
262c1a7
Update CHANGELOG.md change to feature
krystofwoldrich Aug 28, 2024
ea84b51
requested changes
lucas-zimerman Aug 30, 2024
2f852f8
Merge branch 'main' into ref/exclude-replay
lucas-zimerman Aug 30, 2024
ff7a001
lint fix
lucas-zimerman Sep 2, 2024
984c19e
test disable code
lucas-zimerman Sep 3, 2024
9b155ed
test compatibility with old metro
lucas-zimerman Sep 3, 2024
661a8d0
add tests for old metro
lucas-zimerman Sep 3, 2024
cf29b11
unified tests and replay check
lucas-zimerman Sep 4, 2024
eaa090f
add test for null platform
lucas-zimerman Sep 4, 2024
6b58292
test resolveRequest as null
lucas-zimerman Sep 4, 2024
25437a4
document why null
lucas-zimerman Sep 4, 2024
b48308d
nit
lucas-zimerman Sep 4, 2024
208ae13
test refactor
lucas-zimerman Sep 5, 2024
a843d73
add error
lucas-zimerman Sep 5, 2024
7366928
force error on all versions.
lucas-zimerman Sep 5, 2024
204db68
test console error.
lucas-zimerman Sep 5, 2024
631e930
console error ok, logger logs?
lucas-zimerman Sep 5, 2024
faa94ec
logger doesnt work, should we brek the process?
lucas-zimerman Sep 5, 2024
47d6cd1
final tests
lucas-zimerman Sep 5, 2024
97f0c28
yran fix and set default to true
lucas-zimerman Sep 5, 2024
03b8acc
Apply suggestions from code review
krystofwoldrich Sep 6, 2024
3d5a1c0
Update src/js/tools/metroconfig.ts
krystofwoldrich Sep 6, 2024
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 @@ -72,6 +72,9 @@ wheelhouse
.yalc/
yalc.lock

# Yarn
.yarn

# E2E tests
test/react-native/versions
node_modules.bak
Expand Down
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
# Changelog

## Unreleased

### Features

- Exclude Sentry Web Replay by default, reducing the code in 130KB. ([#4006](https://github.com/getsentry/sentry-react-native/pull/4006))
- You can keep Sentry Web Replay by setting `includeWebReplay` to `true` in your metro config as shown in the snippet:

```js
// For Expo
const { getSentryExpoConfig } = require("@sentry/react-native/metro");
const config = getSentryExpoConfig(__dirname, { includeWebReplay: true });

// For RN
const { getDefaultConfig } = require('@react-native/metro-config');
const { withSentryConfig } = require('@sentry/react-native/metro');
module.exports = withSentryConfig(getDefaultConfig(__dirname), { includeWebReplay: true });
```

## 5.31.1

### Fixes
Expand Down
76 changes: 74 additions & 2 deletions src/js/tools/metroconfig.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { logger } from '@sentry/utils';
import type { MetroConfig, MixedOutput, Module, ReadOnlyGraph } from 'metro';
import type { CustomResolutionContext, CustomResolver, Resolution } from 'metro-resolver';
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();
Expand All @@ -18,6 +18,11 @@ export interface SentryMetroConfigOptions {
* @default false
*/
annotateReactComponents?: boolean;
/**
lucas-zimerman marked this conversation as resolved.
Show resolved Hide resolved
* Adds the Sentry replay package for web.
* @default true
*/
includeWebReplay?: boolean;
}

export interface SentryExpoConfigOptions {
Expand All @@ -35,7 +40,7 @@ export interface SentryExpoConfigOptions {
*/
export function withSentryConfig(
config: MetroConfig,
{ annotateReactComponents = false }: SentryMetroConfigOptions = {},
{ annotateReactComponents = false, includeWebReplay = true }: SentryMetroConfigOptions = {},
): MetroConfig {
setSentryMetroDevServerEnvFlag();

Expand All @@ -46,6 +51,9 @@ export function withSentryConfig(
if (annotateReactComponents) {
newConfig = withSentryBabelTransformer(newConfig);
}
if (includeWebReplay === false) {
newConfig = withSentryResolver(newConfig, includeWebReplay);
}

return newConfig;
}
Expand Down Expand Up @@ -73,6 +81,10 @@ export function getSentryExpoConfig(
newConfig = withSentryBabelTransformer(newConfig);
}

if (options.includeWebReplay === false) {
newConfig = withSentryResolver(newConfig, options.includeWebReplay);
}

return newConfig;
}

Expand Down Expand Up @@ -146,6 +158,66 @@ function withSentryDebugId(config: MetroConfig): MetroConfig {
};
}

// Based on: https://github.com/facebook/metro/blob/c21daba415ea26511e157f794689caab9abe8236/packages/metro-resolver/src/resolve.js#L86-L91
type CustomResolverBeforeMetro068 = (
context: CustomResolutionContext,
realModuleName: string,
platform: string | null,
moduleName?: string,
) => Resolution;

/**
* Includes `@sentry/replay` packages based on the `includeWebReplay` flag and current bundle `platform`.
*/
export function withSentryResolver(config: MetroConfig, includeWebReplay: boolean | undefined): MetroConfig {
const originalResolver = config.resolver?.resolveRequest as CustomResolver | CustomResolverBeforeMetro068 | undefined;

const sentryResolverRequest: CustomResolver = (
context: CustomResolutionContext,
moduleName: string,
platform: string | null,
oldMetroModuleName?: string,
) => {
if (
(includeWebReplay === false ||
(includeWebReplay === undefined && (platform === 'android' || platform === 'ios'))) &&
(oldMetroModuleName ?? moduleName).includes('@sentry/replay')
) {
return { type: 'empty' } as Resolution;
}
if (originalResolver) {
return oldMetroModuleName
? originalResolver(context, moduleName, platform, oldMetroModuleName)
: originalResolver(context, moduleName, platform);
}

// Prior 0.68, resolve context.resolveRequest is sentryResolver itself, where on later version it is the default resolver.
if (context.resolveRequest === sentryResolverRequest) {
// eslint-disable-next-line no-console
console.error(
`Error: [@sentry/react-native/metro] Can not resolve the defaultResolver on Metro older than 0.68.
Please follow one of the following options:
- Include your resolverRequest on your metroconfig.
- Update your Metro version to 0.68 or higher.
- Set includeWebReplay as true on your metro config.
- If you are still facing issues, report the issue at http://www.github.com/getsentry/sentry-react-native/issues`,
);
// Return required for test.
return process.exit(-1);
}

return context.resolveRequest(context, moduleName, platform);
};

return {
...config,
resolver: {
...config.resolver,
resolveRequest: sentryResolverRequest,
},
};
}

type MetroFrame = Parameters<Required<Required<MetroConfig>['symbolicator']>['customizeFrame']>[0];
type MetroCustomizeFrame = { readonly collapse?: boolean };
type MetroCustomizeFrameReturnValue =
Expand Down
191 changes: 190 additions & 1 deletion test/tools/metroconfig.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ import type { MetroConfig } from 'metro';
import * as path from 'path';
import * as process from 'process';

import { withSentryBabelTransformer, withSentryFramesCollapsed } from '../../src/js/tools/metroconfig';
import {
withSentryBabelTransformer,
withSentryFramesCollapsed,
withSentryResolver,
} from '../../src/js/tools/metroconfig';

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

Expand Down Expand Up @@ -108,6 +112,191 @@ describe('metroconfig', () => {
);
});
});

describe('withSentryResolver', () => {
let originalResolverMock: any;

// @ts-expect-error Can't see type CustomResolutionContext
let contextMock: CustomResolutionContext;
let config: MetroConfig = {};

beforeEach(() => {
originalResolverMock = jest.fn();
contextMock = {
resolveRequest: jest.fn(),
};

config = {
resolver: {
resolveRequest: originalResolverMock,
},
};
});

describe.each([
['new Metro', false, '0.70.0'],
['old Metro', true, '0.67.0'],
])(`on %s`, (description, oldMetro, metroVersion) => {
beforeEach(() => {
jest.resetModules();
// Mock metro/package.json
jest.mock('metro/package.json', () => ({
version: metroVersion,
}));
});

test('keep Web Replay when platform is web and includeWebReplay is true', () => {
const modifiedConfig = withSentryResolver(config, true);
resolveRequest(modifiedConfig, contextMock, '@sentry/replay', 'web');

ExpectToBeCalledWithMetroParameters(originalResolverMock, contextMock, '@sentry/replay', 'web');
});

test('removes Web Replay when platform is web and includeWebReplay is false', () => {
const modifiedConfig = withSentryResolver(config, false);
const result = resolveRequest(modifiedConfig, contextMock, '@sentry/replay', 'web');

expect(result).toEqual({ type: 'empty' });
expect(originalResolverMock).not.toHaveBeenCalled();
});

test('keep Web Replay when platform is android and includeWebReplay is true', () => {
const modifiedConfig = withSentryResolver(config, true);
resolveRequest(modifiedConfig, contextMock, '@sentry/replay', 'android');

ExpectToBeCalledWithMetroParameters(originalResolverMock, contextMock, '@sentry/replay', 'android');
});

test('removes Web Replay when platform is android and includeWebReplay is false', () => {
const modifiedConfig = withSentryResolver(config, false);
const result = resolveRequest(modifiedConfig, contextMock, '@sentry/replay', 'android');

expect(result).toEqual({ type: 'empty' });
expect(originalResolverMock).not.toHaveBeenCalled();
});

test('removes Web Replay when platform is android and includeWebReplay is undefined', () => {
const modifiedConfig = withSentryResolver(config, undefined);
const result = resolveRequest(modifiedConfig, contextMock, '@sentry/replay', 'android');

expect(result).toEqual({ type: 'empty' });
expect(originalResolverMock).not.toHaveBeenCalled();
});

test('keep Web Replay when platform is undefined and includeWebReplay is null', () => {
const modifiedConfig = withSentryResolver(config, undefined);
resolveRequest(modifiedConfig, contextMock, '@sentry/replay', null);

ExpectToBeCalledWithMetroParameters(originalResolverMock, contextMock, '@sentry/replay', null);
});

test('keep Web Replay when platform is ios and includeWebReplay is true', () => {
const modifiedConfig = withSentryResolver(config, true);
resolveRequest(modifiedConfig, contextMock, '@sentry/replay', 'ios');

ExpectToBeCalledWithMetroParameters(originalResolverMock, contextMock, '@sentry/replay', 'ios');
});

test('removes Web Replay when platform is ios and includeWebReplay is false', () => {
const modifiedConfig = withSentryResolver(config, false);
const result = resolveRequest(modifiedConfig, contextMock, '@sentry/replay', 'ios');

expect(result).toEqual({ type: 'empty' });
expect(originalResolverMock).not.toHaveBeenCalled();
});

test('removes Web Replay when platform is ios and includeWebReplay is undefined', () => {
const modifiedConfig = withSentryResolver(config, undefined);
const result = resolveRequest(modifiedConfig, contextMock, '@sentry/replay', 'ios');

expect(result).toEqual({ type: 'empty' });
expect(originalResolverMock).not.toHaveBeenCalled();
});

test('calls originalResolver when moduleName is not @sentry/replay', () => {
const modifiedConfig = withSentryResolver(config, true);
const moduleName = 'some/other/module';
resolveRequest(modifiedConfig, contextMock, moduleName, 'web');

ExpectToBeCalledWithMetroParameters(originalResolverMock, contextMock, moduleName, 'web');
});

test('calls originalResolver when moduleName is not @sentry/replay and includeWebReplay set to false', () => {
const modifiedConfig = withSentryResolver(config, false);
const moduleName = 'some/other/module';
resolveRequest(modifiedConfig, contextMock, moduleName, 'web');

ExpectToBeCalledWithMetroParameters(originalResolverMock, contextMock, moduleName, 'web');
});

test('calls default resolver on new metro resolver when originalResolver is not provided', () => {
if (oldMetro) {
return;
}

const modifiedConfig = withSentryResolver({ resolver: {} }, true);
const moduleName = 'some/other/module';
const platform = 'web';
resolveRequest(modifiedConfig, contextMock, moduleName, platform);

ExpectToBeCalledWithMetroParameters(contextMock.resolveRequest, contextMock, moduleName, platform);
});

test('throws error when running on old metro and includeWebReplay is set to false', () => {
if (!oldMetro) {
return;
}

// @ts-expect-error mock.
const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {});
const modifiedConfig = withSentryResolver({ resolver: {} }, true);
const moduleName = 'some/other/module';
resolveRequest(modifiedConfig, contextMock, moduleName, 'web');

expect(mockExit).toHaveBeenCalledWith(-1);
});

type CustomResolverBeforeMetro067 = (
// @ts-expect-error Can't see type CustomResolutionContext
context: CustomResolutionContext,
realModuleName: string,
platform: string | null,
moduleName?: string,
// @ts-expect-error Can't see type CustomResolutionContext
) => Resolution;

function resolveRequest(
metroConfig: MetroConfig,
context: any,
moduleName: string,
platform: string | null,
// @ts-expect-error Can't see type Resolution.
): Resolution {
if (oldMetro) {
const resolver = metroConfig.resolver?.resolveRequest as CustomResolverBeforeMetro067;
// On older Metro the resolveRequest is the creater resolver.
context.resolveRequest = resolver;
return resolver(context, `real${moduleName}`, platform, moduleName);
}
return (
metroConfig.resolver?.resolveRequest && metroConfig.resolver.resolveRequest(context, moduleName, platform)
);
}

function ExpectToBeCalledWithMetroParameters(
received: CustomResolverBeforeMetro067,
contextMock: CustomResolverBeforeMetro067,
moduleName: string,
platform: string | null,
) {
if (oldMetro) {
expect(received).toBeCalledWith(contextMock, `real${moduleName}`, platform, moduleName);
} else {
expect(received).toBeCalledWith(contextMock, moduleName, platform);
}
}
});
});
});

// function create mock metro frame
Expand Down
Loading