Skip to content

Commit 7af80d6

Browse files
Merge 55ed3fa into 1e5dbde
2 parents 1e5dbde + 55ed3fa commit 7af80d6

File tree

7 files changed

+354
-4
lines changed

7 files changed

+354
-4
lines changed

packages/core/jest.config.tools.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
module.exports = {
22
collectCoverage: true,
33
preset: 'ts-jest',
4-
setupFilesAfterEnv: ['<rootDir>/test/mockConsole.ts'],
4+
setupFilesAfterEnv: ['jest-extended/all', '<rootDir>/test/mockConsole.ts'],
55
globals: {
66
__DEV__: true,
77
},

packages/core/src/js/tools/metroconfig.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import { createSentryMetroSerializer, unstable_beforeAssetSerializationPlugin }
1010
import type { DefaultConfigOptions } from './vendor/expo/expoconfig';
1111
export * from './sentryMetroSerializer';
1212
import { withSentryMiddleware } from './metroMiddleware';
13+
import { withSentryOptionsFromFile } from './sentryOptionsSerializer';
14+
import type { MetroCustomSerializer } from './utils';
1315

1416
enableLogger();
1517

@@ -30,6 +32,14 @@ export interface SentryMetroConfigOptions {
3032
* @default true
3133
*/
3234
enableSourceContextInDevelopment?: boolean;
35+
/**
36+
* Load Sentry Options from a file. If `true` it will use the default path.
37+
* If `false` it will not load any options from a file. Only options provided in the code will be used.
38+
* If `string` it will use the provided path.
39+
*
40+
* @default '{projectRoot}/sentry.options.json'
41+
*/
42+
optionsFile?: string | boolean;
3343
}
3444

3545
export interface SentryExpoConfigOptions {
@@ -51,6 +61,7 @@ export function withSentryConfig(
5161
annotateReactComponents = false,
5262
includeWebReplay = true,
5363
enableSourceContextInDevelopment = true,
64+
optionsFile = true,
5465
}: SentryMetroConfigOptions = {},
5566
): MetroConfig {
5667
setSentryMetroDevServerEnvFlag();
@@ -68,6 +79,9 @@ export function withSentryConfig(
6879
if (enableSourceContextInDevelopment) {
6980
newConfig = withSentryMiddleware(newConfig);
7081
}
82+
if (optionsFile) {
83+
newConfig = withSentryOptionsFromFile(newConfig, optionsFile);
84+
}
7185

7286
return newConfig;
7387
}
@@ -103,6 +117,10 @@ export function getSentryExpoConfig(
103117
newConfig = withSentryMiddleware(newConfig);
104118
}
105119

120+
if (options.optionsFile ?? true) {
121+
newConfig = withSentryOptionsFromFile(newConfig, options.optionsFile ?? true);
122+
}
123+
106124
return newConfig;
107125
}
108126

@@ -155,8 +173,6 @@ export function withSentryBabelTransformer(config: MetroConfig): MetroConfig {
155173
};
156174
}
157175

158-
type MetroCustomSerializer = Required<Required<MetroConfig>['serializer']>['customSerializer'] | undefined;
159-
160176
function withSentryDebugId(config: MetroConfig): MetroConfig {
161177
const customSerializer = createSentryMetroSerializer(
162178
config.serializer?.customSerializer || undefined,

packages/core/src/js/tools/sentryMetroSerializer.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export function unstable_beforeAssetSerializationPlugin({
4242
return [...addDebugIdModule(premodules, debugIdModule)];
4343
}
4444

45+
// TODO: deprecate this and afterwards rename to createSentryDebugIdSerializer
4546
/**
4647
* Creates a Metro serializer that adds Debug ID module to the plain bundle.
4748
* The Debug ID module is a virtual module that provides a debug ID in runtime.
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { logger } from '@sentry/core';
2+
import * as fs from 'fs';
3+
import type { MetroConfig, Module } from 'metro';
4+
// eslint-disable-next-line import/no-extraneous-dependencies
5+
import * as countLines from 'metro/src/lib/countLines';
6+
import * as path from 'path';
7+
8+
import type { MetroCustomSerializer, VirtualJSOutput } from './utils';
9+
import { createSet } from './utils';
10+
11+
const DEFAULT_OPTIONS_FILE_NAME = 'sentry.options.json';
12+
13+
/**
14+
* Loads Sentry options from a file in
15+
*/
16+
export function withSentryOptionsFromFile(config: MetroConfig, optionsFile: string | boolean): MetroConfig {
17+
if (optionsFile === false) {
18+
return config;
19+
}
20+
21+
const { projectRoot } = config;
22+
if (!projectRoot) {
23+
// eslint-disable-next-line no-console
24+
console.error('[@sentry/react-native/metro] Project root is required to load Sentry options from a file');
25+
return config;
26+
}
27+
28+
let optionsPath = path.join(projectRoot, DEFAULT_OPTIONS_FILE_NAME);
29+
if (typeof optionsFile === 'string' && path.isAbsolute(optionsFile)) {
30+
optionsPath = optionsFile;
31+
} else if (typeof optionsFile === 'string') {
32+
optionsPath = path.join(projectRoot, optionsFile);
33+
}
34+
35+
const originalSerializer = config.serializer?.customSerializer;
36+
if (!originalSerializer) {
37+
// It's okay to bail here because we don't expose this for direct usage, but as part of `withSentryConfig`
38+
// If used directly in RN, the user is responsible for providing a custom serializer first, Expo provides serializer in default config
39+
// eslint-disable-next-line no-console
40+
console.error(
41+
'[@sentry/react-native/metro] `config.serializer.customSerializer` is required to load Sentry options from a file',
42+
);
43+
return config;
44+
}
45+
46+
const sentryOptionsSerializer: MetroCustomSerializer = (entryPoint, preModules, graph, options) => {
47+
const sentryOptionsModule = createSentryOptionsModule(optionsPath);
48+
if (sentryOptionsModule) {
49+
(preModules as Module[]).push(sentryOptionsModule);
50+
}
51+
return originalSerializer(entryPoint, preModules, graph, options);
52+
};
53+
54+
return {
55+
...config,
56+
serializer: {
57+
...config.serializer,
58+
customSerializer: sentryOptionsSerializer,
59+
},
60+
};
61+
}
62+
63+
function createSentryOptionsModule(filePath: string): Module<VirtualJSOutput> | null {
64+
let content: string;
65+
try {
66+
content = fs.readFileSync(filePath, 'utf8');
67+
} catch (error) {
68+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
69+
logger.debug(`[@sentry/react-native/metro] Sentry options file does not exist at ${filePath}`);
70+
} else {
71+
logger.error(`[@sentry/react-native/metro] Failed to read Sentry options file at ${filePath}`);
72+
}
73+
return null;
74+
}
75+
76+
let parsedContent: Record<string, unknown>;
77+
try {
78+
parsedContent = JSON.parse(content);
79+
} catch (error) {
80+
logger.error(`[@sentry/react-native/metro] Failed to parse Sentry options file at ${filePath}`);
81+
return null;
82+
}
83+
84+
const minifiedContent = JSON.stringify(parsedContent);
85+
const optionsCode = `var __SENTRY_OPTIONS__=${minifiedContent};`;
86+
87+
logger.debug(`[@sentry/react-native/metro] Sentry options added to the bundle from file at ${filePath}`);
88+
return {
89+
dependencies: new Map(),
90+
getSource: () => Buffer.from(optionsCode),
91+
inverseDependencies: createSet(),
92+
path: '__sentry-options__',
93+
output: [
94+
{
95+
type: 'js/script/virtual',
96+
data: {
97+
code: optionsCode,
98+
lineCount: countLines(optionsCode),
99+
map: [],
100+
},
101+
},
102+
],
103+
};
104+
}

packages/core/src/js/tools/utils.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import * as crypto from 'crypto';
22
// eslint-disable-next-line import/no-extraneous-dependencies
3-
import type { Module, ReadOnlyGraph, SerializerOptions } from 'metro';
3+
import type { MetroConfig, Module, ReadOnlyGraph, SerializerOptions } from 'metro';
44
// eslint-disable-next-line import/no-extraneous-dependencies
55
import type CountingSet from 'metro/src/lib/CountingSet';
66

7+
export type MetroCustomSerializer = Required<Required<MetroConfig>['serializer']>['customSerializer'] | undefined;
8+
79
// Variant of MixedOutput
810
// https://github.com/facebook/metro/blob/9b85f83c9cc837d8cd897aa7723be7da5b296067/packages/metro/src/DeltaBundler/types.flow.js#L21
911
export type VirtualJSOutput = {

0 commit comments

Comments
 (0)