Skip to content

Commit 1667b2b

Browse files
feat(expo): Automatically detect Release name and version for Expo Web (#4967)
* feat(expo): Automatically detect Release name and version for Expo Web * Revert style changes in sdk.tsx * Add changelog entry * Fix PR number * Update the PR number in the changelog --------- Co-authored-by: Antonis Lilis <antonis.lilis@gmail.com>
1 parent 901d64f commit 1667b2b

11 files changed

Lines changed: 386 additions & 50 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
99
## Unreleased
1010

11+
### Features
12+
13+
- Automatically detect Release name and version for Expo Web ([#4967](https://github.com/getsentry/sentry-react-native/pull/4967))
14+
1115
### Fixes
1216

1317
- ignoreError now filters Native errors ([#4948](https://github.com/getsentry/sentry-react-native/pull/4948))

packages/core/src/js/sdk.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { ReactNativeProfiler } from './tracing';
2525
import { useEncodePolyfill } from './transports/encodePolyfill';
2626
import { DEFAULT_BUFFER_SIZE, makeNativeTransportFactory } from './transports/native';
2727
import { getDefaultEnvironment, isExpoGo, isRunningInMetroDevServer, isWeb } from './utils/environment';
28+
import { getDefaultRelease } from './utils/release';
2829
import { safeFactory, safeTracesSampler } from './utils/safe';
2930
import { NATIVE } from './wrapper';
3031

@@ -111,6 +112,7 @@ export function init(passedOptions: ReactNativeOptions): void {
111112
const options: ReactNativeClientOptions = {
112113
...DEFAULT_OPTIONS,
113114
...passedOptions,
115+
release: passedOptions.release ?? getDefaultRelease(),
114116
enableNative,
115117
enableNativeNagger: shouldEnableNativeNagger(passedOptions.enableNativeNagger),
116118
// If custom transport factory fails the SDK won't initialize

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

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import {
99
setSentryBabelTransformerOptions,
1010
setSentryDefaultBabelTransformerPathEnv,
1111
} from './sentryBabelTransformerUtils';
12-
import { createSentryMetroSerializer, unstable_beforeAssetSerializationPlugin } from './sentryMetroSerializer';
12+
import { createSentryMetroSerializer, unstableBeforeAssetSerializationDebugIdPlugin } from './sentryMetroSerializer';
13+
import { unstableReleaseConstantsPlugin } from './sentryReleaseInjector';
1314
import type { DefaultConfigOptions } from './vendor/expo/expoconfig';
1415

1516
export * from './sentryMetroSerializer';
@@ -44,6 +45,13 @@ export interface SentryExpoConfigOptions {
4445
* Pass a custom `getDefaultConfig` function to override the default Expo configuration getter.
4546
*/
4647
getDefaultConfig?: typeof getSentryExpoConfig;
48+
49+
/**
50+
* For Expo Web, inject `release` and `version` options from `app.json`, the Expo Application Config.
51+
*
52+
* @default true
53+
*/
54+
injectReleaseForWeb?: boolean;
4755
}
4856

4957
/**
@@ -93,7 +101,8 @@ export function getSentryExpoConfig(
93101
...options,
94102
unstable_beforeAssetSerializationPlugins: [
95103
...(options.unstable_beforeAssetSerializationPlugins || []),
96-
unstable_beforeAssetSerializationPlugin,
104+
...(options.injectReleaseForWeb ?? true ? [unstableReleaseConstantsPlugin(projectRoot)] : []),
105+
unstableBeforeAssetSerializationDebugIdPlugin,
97106
],
98107
});
99108

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

Lines changed: 12 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,28 @@
11
import * as crypto from 'crypto';
22
// eslint-disable-next-line import/no-extraneous-dependencies
33
import type { MixedOutput, Module, ReadOnlyGraph } from 'metro';
4-
// eslint-disable-next-line import/no-extraneous-dependencies
5-
import * as countLines from 'metro/src/lib/countLines';
64
import type { Bundle, MetroSerializer, MetroSerializerOutput, SerializedBundle, VirtualJSOutput } from './utils';
7-
import { createDebugIdSnippet, createSet, determineDebugIdFromBundleSource, stringToUUID } from './utils';
5+
import {
6+
createDebugIdSnippet,
7+
createVirtualJSModule,
8+
determineDebugIdFromBundleSource,
9+
prependModule,
10+
stringToUUID,
11+
} from './utils';
812
import { createDefaultMetroSerializer } from './vendor/metro/utils';
913

1014
type SourceMap = Record<string, unknown>;
1115

1216
const DEBUG_ID_PLACE_HOLDER = '__debug_id_place_holder__';
1317
const DEBUG_ID_MODULE_PATH = '__debugid__';
14-
const PRELUDE_MODULE_PATH = '__prelude__';
18+
1519
const SOURCE_MAP_COMMENT = '//# sourceMappingURL=';
1620
const DEBUG_ID_COMMENT = '//# debugId=';
1721

1822
/**
1923
* Adds Sentry Debug ID polyfill module to the bundle.
2024
*/
21-
export function unstable_beforeAssetSerializationPlugin({
25+
export function unstableBeforeAssetSerializationDebugIdPlugin({
2226
premodules,
2327
debugId,
2428
}: {
@@ -38,7 +42,7 @@ export function unstable_beforeAssetSerializationPlugin({
3842
}
3943

4044
const debugIdModule = createDebugIdModule(debugId);
41-
return [...addDebugIdModule(premodules, debugIdModule)];
45+
return prependModule(premodules, debugIdModule);
4246
}
4347

4448
/**
@@ -63,7 +67,7 @@ export const createSentryMetroSerializer = (customSerializer?: MetroSerializer):
6367

6468
const debugIdModule = createDebugIdModule(DEBUG_ID_PLACE_HOLDER);
6569
options.sentryBundleCallback = createSentryBundleCallback(debugIdModule);
66-
const modifiedPreModules = addDebugIdModule(preModules, debugIdModule);
70+
const modifiedPreModules = prependModule(preModules, debugIdModule);
6771

6872
// Run wrapped serializer
6973
const serializerResult = serializer(entryPoint, modifiedPreModules, graph, options);
@@ -120,25 +124,6 @@ function createSentryBundleCallback(debugIdModule: Module<VirtualJSOutput> & { s
120124
};
121125
}
122126

123-
function addDebugIdModule(
124-
preModules: readonly Module<MixedOutput>[],
125-
debugIdModule: Module<VirtualJSOutput>,
126-
): readonly Module<MixedOutput>[] {
127-
const modifiedPreModules = [...preModules];
128-
if (
129-
modifiedPreModules.length > 0 &&
130-
modifiedPreModules[0] !== undefined &&
131-
modifiedPreModules[0].path === PRELUDE_MODULE_PATH
132-
) {
133-
// prelude module must be first as it measures the bundle startup time
134-
modifiedPreModules.unshift(preModules[0] as Module<VirtualJSOutput>);
135-
modifiedPreModules[1] = debugIdModule;
136-
} else {
137-
modifiedPreModules.unshift(debugIdModule);
138-
}
139-
return modifiedPreModules;
140-
}
141-
142127
async function extractSerializerResult(serializerResult: MetroSerializerOutput): Promise<SerializedBundle> {
143128
if (typeof serializerResult === 'string') {
144129
return { code: serializerResult, map: '{}' };
@@ -157,27 +142,7 @@ async function extractSerializerResult(serializerResult: MetroSerializerOutput):
157142
}
158143

159144
function createDebugIdModule(debugId: string): Module<VirtualJSOutput> & { setSource: (code: string) => void } {
160-
let debugIdCode = createDebugIdSnippet(debugId);
161-
162-
return {
163-
setSource: (code: string) => {
164-
debugIdCode = code;
165-
},
166-
dependencies: new Map(),
167-
getSource: () => Buffer.from(debugIdCode),
168-
inverseDependencies: createSet(),
169-
path: DEBUG_ID_MODULE_PATH,
170-
output: [
171-
{
172-
type: 'js/script/virtual',
173-
data: {
174-
code: debugIdCode,
175-
lineCount: countLines(debugIdCode),
176-
map: [],
177-
},
178-
},
179-
],
180-
};
145+
return createVirtualJSModule(DEBUG_ID_MODULE_PATH, createDebugIdSnippet(debugId));
181146
}
182147

183148
function calculateDebugId(bundle: Bundle): string {
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// eslint-disable-next-line import/no-extraneous-dependencies
2+
import type { MixedOutput, Module, ReadOnlyGraph } from 'metro';
3+
import type { VirtualJSOutput } from './utils';
4+
import { createVirtualJSModule, getExpoConfig, prependModule } from './utils';
5+
6+
const RELEASE_CONSTANTS_MODULE_PATH = '__sentryReleaseConstants__';
7+
8+
/**
9+
* Adds Sentry Release constants to the bundle.
10+
*/
11+
export const unstableReleaseConstantsPlugin =
12+
(projectRoot: string) =>
13+
({ graph, premodules }: { graph: ReadOnlyGraph<MixedOutput>; premodules: Module[]; debugId?: string }): Module[] => {
14+
const notWeb = graph.transformOptions.platform !== 'web';
15+
if (notWeb) {
16+
return premodules;
17+
}
18+
19+
const { name, version } = getExpoConfig(projectRoot);
20+
21+
if (!name || !version) {
22+
return premodules;
23+
}
24+
25+
return prependModule(
26+
premodules,
27+
createSentryReleaseModule({
28+
name,
29+
version,
30+
}),
31+
);
32+
};
33+
34+
function createSentryReleaseModule({
35+
name,
36+
version,
37+
}: {
38+
name: string;
39+
version: string;
40+
}): Module<VirtualJSOutput> & { setSource: (code: string) => void } {
41+
return createVirtualJSModule(RELEASE_CONSTANTS_MODULE_PATH, createReleaseConstantsSnippet({ name, version }));
42+
}
43+
44+
function createReleaseConstantsSnippet({ name, version }: { name: string; version: string }): string {
45+
return `var SENTRY_RELEASE;SENTRY_RELEASE={name: "${name}", version: "${version}"};`;
46+
}

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

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
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 { MixedOutput, Module, ReadOnlyGraph, SerializerOptions } from 'metro';
44
// eslint-disable-next-line import/no-extraneous-dependencies
55
import type CountingSet from 'metro/src/lib/CountingSet';
6+
// eslint-disable-next-line import/no-extraneous-dependencies
7+
import * as countLines from 'metro/src/lib/countLines';
68

79
// Variant of MixedOutput
810
// https://github.com/facebook/metro/blob/9b85f83c9cc837d8cd897aa7723be7da5b296067/packages/metro/src/DeltaBundler/types.flow.js#L21
@@ -98,3 +100,83 @@ function resolveSetCreator(): () => CountingSet<string> {
98100
}
99101

100102
export const createSet = resolveSetCreator();
103+
104+
const PRELUDE_MODULE_PATH = '__prelude__';
105+
106+
/**
107+
* Prepends the module after default required prelude modules.
108+
*/
109+
export function prependModule(
110+
modules: readonly Module<MixedOutput>[],
111+
module: Module<VirtualJSOutput>,
112+
): Module<MixedOutput>[] {
113+
const modifiedPreModules = [...modules];
114+
if (
115+
modifiedPreModules.length > 0 &&
116+
modifiedPreModules[0] !== undefined &&
117+
modifiedPreModules[0].path === PRELUDE_MODULE_PATH
118+
) {
119+
// prelude module must be first as it measures the bundle startup time
120+
modifiedPreModules.unshift(modules[0] as Module<VirtualJSOutput>);
121+
modifiedPreModules[1] = module;
122+
} else {
123+
modifiedPreModules.unshift(module);
124+
}
125+
return modifiedPreModules;
126+
}
127+
128+
/**
129+
* Creates a virtual JS module with the given path and code.
130+
*/
131+
export function createVirtualJSModule(
132+
modulePath: string,
133+
moduleCode: string,
134+
): Module<VirtualJSOutput> & { setSource: (code: string) => void } {
135+
let sourceCode = moduleCode;
136+
137+
return {
138+
setSource: (code: string) => {
139+
sourceCode = code;
140+
},
141+
dependencies: new Map(),
142+
getSource: () => Buffer.from(sourceCode),
143+
inverseDependencies: createSet(),
144+
path: modulePath,
145+
output: [
146+
{
147+
type: 'js/script/virtual',
148+
data: {
149+
code: sourceCode,
150+
lineCount: countLines(sourceCode),
151+
map: [],
152+
},
153+
},
154+
],
155+
};
156+
}
157+
158+
/**
159+
* Tries to load Expo config using `@expo/config` package.
160+
*/
161+
export function getExpoConfig(projectRoot: string): Partial<{
162+
name: string;
163+
version: string;
164+
}> {
165+
try {
166+
// eslint-disable-next-line @typescript-eslint/no-var-requires, import/no-extraneous-dependencies
167+
const expoConfig = require('@expo/config') as {
168+
getConfig?: (projectRoot: string) => { exp: Record<string, unknown> };
169+
};
170+
if (expoConfig.getConfig) {
171+
const { exp } = expoConfig.getConfig(projectRoot);
172+
return {
173+
name: typeof exp.name === 'string' && exp.name ? exp.name : undefined,
174+
version: typeof exp.version === 'string' && exp.version ? exp.version : undefined,
175+
};
176+
}
177+
} catch {
178+
// @expo/config not available, do nothing
179+
}
180+
181+
return {};
182+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { notWeb } from './environment';
2+
import { RN_GLOBAL_OBJ } from './worldwide';
3+
4+
/**
5+
*
6+
*/
7+
export function createReleaseFromGlobalReleaseConstants(): string | undefined {
8+
const globalRelease = RN_GLOBAL_OBJ.SENTRY_RELEASE;
9+
if (!globalRelease) {
10+
return undefined;
11+
}
12+
13+
const { name, version } = globalRelease;
14+
if (!name || !version) {
15+
return undefined;
16+
}
17+
18+
return `${name}@${version}`;
19+
}
20+
21+
/**
22+
*
23+
*/
24+
export function getDefaultRelease(): string | undefined {
25+
if (notWeb()) {
26+
// Mobile platforms use native release from the Release integration.
27+
return undefined;
28+
}
29+
30+
// Web platforms (Expo Web) use the global release constants.
31+
// Release set in the options is need for Session and Replay integrations.
32+
return createReleaseFromGlobalReleaseConstants();
33+
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ export interface ReactNativeInternalGlobal extends InternalGlobal {
3333
nativePerformanceNow?: () => number;
3434
TextEncoder?: TextEncoder;
3535
alert?: (message: string) => void;
36+
SENTRY_RELEASE?: {
37+
/** Used by Sentry Webpack Plugin, not used by RN, only to silence TS */
38+
id?: string;
39+
name?: string;
40+
version?: string;
41+
};
3642
}
3743

3844
type TextEncoder = {

0 commit comments

Comments
 (0)