Skip to content

Commit 02906de

Browse files
authored
Merge branch 'main' into fix/profile-app-start-timestamp-offset
2 parents a784129 + 2c735cc commit 02906de

8 files changed

Lines changed: 185 additions & 42 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,16 @@
1111
### Fixes
1212

1313
- Fix app start transaction profile offset by using the actual profiling start timestamp instead of the adjusted app start time ([#5962](https://github.com/getsentry/sentry-react-native/issues/5962))
14+
- Add `SENTRY_PROJECT_ROOT` env var to override project root in Xcode build phase scripts for monorepo setups ([#5961](https://github.com/getsentry/sentry-react-native/pull/5961))
1415

1516
### Features
1617

1718
- Enable "Open Sentry" button in Playground for Expo apps ([#5947](https://github.com/getsentry/sentry-react-native/pull/5947))
19+
- Add `attachAllThreads` option to attach full stack traces for all threads to captured events on iOS ([#5960](https://github.com/getsentry/sentry-react-native/issues/5960))
20+
21+
### Fixes
22+
23+
- Lazy-load Metro internal modules to prevent Expo 55 import errors ([#5958](https://github.com/getsentry/sentry-react-native/pull/5958))
1824

1925
### Dependencies
2026

packages/core/scripts/sentry-xcode-debug-files.sh

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@ set -e
2121
LOCAL_NODE_BINARY=${NODE_BINARY:-node}
2222

2323
# The project root by default is one level up from the ios directory
24-
RN_PROJECT_ROOT="${PROJECT_DIR}/.."
24+
# SENTRY_PROJECT_ROOT can be set to override this for monorepo setups
25+
# where the ios directory is not directly under the project root.
26+
# See: https://github.com/getsentry/sentry-react-native/issues/2880
27+
RN_PROJECT_ROOT="${SENTRY_PROJECT_ROOT:-${PROJECT_DIR}/..}"
2528

2629
[ -z "$SENTRY_PROPERTIES" ] && export SENTRY_PROPERTIES=sentry.properties
2730
[ -z "$SENTRY_DOTENV_PATH" ] && [ -f "$RN_PROJECT_ROOT/.env.sentry-build-plugin" ] && export SENTRY_DOTENV_PATH="$RN_PROJECT_ROOT/.env.sentry-build-plugin"

packages/core/scripts/sentry-xcode.sh

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ set -x -e
1010
LOCAL_NODE_BINARY=${NODE_BINARY:-node}
1111

1212
# The project root by default is one level up from the ios directory
13-
RN_PROJECT_ROOT="${PROJECT_DIR}/.."
13+
# SENTRY_PROJECT_ROOT can be set to override this for monorepo setups
14+
# where the ios directory is not directly under the project root.
15+
# See: https://github.com/getsentry/sentry-react-native/issues/2880
16+
RN_PROJECT_ROOT="${SENTRY_PROJECT_ROOT:-${PROJECT_DIR}/..}"
1417

1518
[ -z "$SENTRY_PROPERTIES" ] && export SENTRY_PROPERTIES=sentry.properties
1619
[ -z "$SENTRY_DOTENV_PATH" ] && [ -f "$RN_PROJECT_ROOT/.env.sentry-build-plugin" ] && export SENTRY_DOTENV_PATH="$RN_PROJECT_ROOT/.env.sentry-build-plugin"

packages/core/src/js/options.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,14 @@ export interface BaseReactNativeOptions {
9696
*/
9797
attachThreads?: boolean;
9898

99+
/**
100+
* When enabled, full stack traces for all threads are attached to all captured events.
101+
*
102+
* @default false
103+
* @platform ios
104+
*/
105+
attachAllThreads?: boolean;
106+
99107
/**
100108
* When enabled, certain personally identifiable information (PII) is added by active integrations.
101109
*

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

Lines changed: 44 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -32,48 +32,9 @@ import type * as bundleToStringType from 'metro/private/lib/bundleToString';
3232

3333
import type { MetroSerializer } from '../../utils';
3434

35-
// oxlint-disable-next-line typescript-eslint(no-explicit-any)
36-
let baseJSBundleModule: any;
37-
try {
38-
baseJSBundleModule = require('metro/private/DeltaBundler/Serializers/baseJSBundle');
39-
} catch {
40-
baseJSBundleModule = require('metro/src/DeltaBundler/Serializers/baseJSBundle');
41-
}
42-
43-
const baseJSBundle: typeof baseJSBundleType =
44-
typeof baseJSBundleModule === 'function'
45-
? baseJSBundleModule
46-
: (baseJSBundleModule?.baseJSBundle ?? baseJSBundleModule?.default);
47-
48-
let sourceMapString: typeof sourceMapStringType;
49-
try {
50-
const sourceMapStringModule = require('metro/private/DeltaBundler/Serializers/sourceMapString');
51-
sourceMapString = (sourceMapStringModule as { sourceMapString: typeof sourceMapStringType }).sourceMapString;
52-
} catch (e) {
53-
sourceMapString = require('metro/src/DeltaBundler/Serializers/sourceMapString');
54-
if ('sourceMapString' in sourceMapString) {
55-
// Changed to named export in https://github.com/facebook/metro/commit/34148e61200a508923315fbe387b26d1da27bf4b
56-
// Metro 0.81.0 and 0.80.10 patch
57-
sourceMapString = (sourceMapString as { sourceMapString: typeof sourceMapStringType }).sourceMapString;
58-
}
59-
}
60-
61-
// oxlint-disable-next-line typescript-eslint(no-explicit-any)
62-
let bundleToStringModule: any;
63-
try {
64-
bundleToStringModule = require('metro/private/lib/bundleToString');
65-
} catch {
66-
bundleToStringModule = require('metro/src/lib/bundleToString');
67-
}
68-
69-
const bundleToString: typeof bundleToStringType =
70-
typeof bundleToStringModule === 'function'
71-
? bundleToStringModule
72-
: (bundleToStringModule?.bundleToString ?? bundleToStringModule?.default);
73-
7435
type NewSourceMapStringExport = {
7536
// Since Metro v0.80.10 https://github.com/facebook/metro/compare/v0.80.9...v0.80.10#diff-1b836d1729e527a725305eef0cec22e44605af2700fa413f4c2489ea1a03aebcL28
76-
sourceMapString: typeof sourceMapString;
37+
sourceMapString: typeof sourceMapStringType;
7738
};
7839

7940
/**
@@ -108,6 +69,49 @@ export const getSortedModules = (
10869
* https://github.com/facebook/metro/blob/9b85f83c9cc837d8cd897aa7723be7da5b296067/packages/metro/src/Server.js#L244-L277
10970
*/
11071
export const createDefaultMetroSerializer = (): MetroSerializer => {
72+
// Lazy-load Metro internals only when serializer is created
73+
// This defers requiring Metro modules until they're actually needed (during build),
74+
// avoiding import-time failures when Metro is only a transitive dependency
75+
76+
// oxlint-disable-next-line typescript-eslint(no-explicit-any)
77+
let baseJSBundleModule: any;
78+
try {
79+
baseJSBundleModule = require('metro/private/DeltaBundler/Serializers/baseJSBundle');
80+
} catch {
81+
baseJSBundleModule = require('metro/src/DeltaBundler/Serializers/baseJSBundle');
82+
}
83+
84+
const baseJSBundle: typeof baseJSBundleType =
85+
typeof baseJSBundleModule === 'function'
86+
? baseJSBundleModule
87+
: (baseJSBundleModule?.baseJSBundle ?? baseJSBundleModule?.default);
88+
89+
let sourceMapString: typeof sourceMapStringType;
90+
try {
91+
const sourceMapStringModule = require('metro/private/DeltaBundler/Serializers/sourceMapString');
92+
sourceMapString = (sourceMapStringModule as { sourceMapString: typeof sourceMapStringType }).sourceMapString;
93+
} catch (e) {
94+
sourceMapString = require('metro/src/DeltaBundler/Serializers/sourceMapString');
95+
if ('sourceMapString' in sourceMapString) {
96+
// Changed to named export in https://github.com/facebook/metro/commit/34148e61200a508923315fbe387b26d1da27bf4b
97+
// Metro 0.81.0 and 0.80.10 patch
98+
sourceMapString = (sourceMapString as { sourceMapString: typeof sourceMapStringType }).sourceMapString;
99+
}
100+
}
101+
102+
// oxlint-disable-next-line typescript-eslint(no-explicit-any)
103+
let bundleToStringModule: any;
104+
try {
105+
bundleToStringModule = require('metro/private/lib/bundleToString');
106+
} catch {
107+
bundleToStringModule = require('metro/src/lib/bundleToString');
108+
}
109+
110+
const bundleToString: typeof bundleToStringType =
111+
typeof bundleToStringModule === 'function'
112+
? bundleToStringModule
113+
: (bundleToStringModule?.bundleToString ?? bundleToStringModule?.default);
114+
111115
return (entryPoint, preModules, graph, options) => {
112116
// baseJSBundle assigns IDs to modules in a consistent order
113117
let bundle = baseJSBundle(entryPoint, preModules, graph, options);

packages/core/test/scripts/sentry-xcode-scripts.test.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,82 @@ describe('sentry-xcode.sh', () => {
455455
expect(result.stdout).toContain('skipping sourcemaps upload');
456456
});
457457

458+
describe('SENTRY_PROJECT_ROOT override', () => {
459+
it('resolves SOURCEMAP_FILE relative to SENTRY_PROJECT_ROOT instead of PROJECT_DIR/..', () => {
460+
const customRoot = path.join(tempDir, 'monorepo-package');
461+
fs.mkdirSync(customRoot, { recursive: true });
462+
463+
const echoScript = path.join(tempDir, 'mock-sentry-cli-echo-sourcemap.js');
464+
fs.writeFileSync(
465+
echoScript,
466+
`
467+
const sourcemapFile = process.env.SOURCEMAP_FILE || 'not-set';
468+
console.log('SOURCEMAP_FILE=' + sourcemapFile);
469+
process.exit(0);
470+
`,
471+
);
472+
473+
const result = runScript({
474+
SENTRY_PROJECT_ROOT: customRoot,
475+
SENTRY_CLI_EXECUTABLE: echoScript,
476+
SOURCEMAP_FILE: 'relative/path.map',
477+
});
478+
479+
const expectedPath = path.join(customRoot, 'relative/path.map');
480+
expect(result.exitCode).toBe(0);
481+
expect(result.stdout).toContain(`SOURCEMAP_FILE=${expectedPath}`);
482+
});
483+
484+
it('resolves SOURCEMAP_FILE relative to PROJECT_DIR/.. when SENTRY_PROJECT_ROOT is not set', () => {
485+
const echoScript = path.join(tempDir, 'mock-sentry-cli-echo-sourcemap.js');
486+
fs.writeFileSync(
487+
echoScript,
488+
`
489+
const sourcemapFile = process.env.SOURCEMAP_FILE || 'not-set';
490+
console.log('SOURCEMAP_FILE=' + sourcemapFile);
491+
process.exit(0);
492+
`,
493+
);
494+
495+
const result = runScript({
496+
SENTRY_CLI_EXECUTABLE: echoScript,
497+
SOURCEMAP_FILE: 'relative/path.map',
498+
});
499+
500+
// Without SENTRY_PROJECT_ROOT, falls back to PROJECT_DIR/..
501+
const projectRoot = path.dirname(tempDir);
502+
const expectedPath = path.join(projectRoot, 'relative/path.map');
503+
expect(result.exitCode).toBe(0);
504+
expect(result.stdout).toContain(`SOURCEMAP_FILE=${expectedPath}`);
505+
});
506+
507+
it('finds sentry.options.json in SENTRY_PROJECT_ROOT', () => {
508+
const customRoot = path.join(tempDir, 'monorepo-package');
509+
fs.mkdirSync(customRoot, { recursive: true });
510+
511+
const optionsContent = JSON.stringify({ dsn: 'https://key@sentry.io/123' });
512+
fs.writeFileSync(path.join(customRoot, 'sentry.options.json'), optionsContent);
513+
514+
const buildDir = path.join(tempDir, 'build');
515+
const resourcesPath = 'Resources';
516+
fs.mkdirSync(path.join(buildDir, resourcesPath), { recursive: true });
517+
518+
const result = runScript({
519+
SENTRY_PROJECT_ROOT: customRoot,
520+
SENTRY_DISABLE_AUTO_UPLOAD: 'true',
521+
SENTRY_COPY_OPTIONS_FILE: 'true',
522+
CONFIGURATION_BUILD_DIR: buildDir,
523+
UNLOCALIZED_RESOURCES_FOLDER_PATH: resourcesPath,
524+
});
525+
526+
expect(result.exitCode).toBe(0);
527+
expect(result.stdout).toContain('Copied');
528+
const destPath = path.join(buildDir, resourcesPath, 'sentry.options.json');
529+
const copied = JSON.parse(fs.readFileSync(destPath, 'utf8'));
530+
expect(copied.dsn).toBe('https://key@sentry.io/123');
531+
});
532+
});
533+
458534
describe('sentry.options.json SENTRY_ENVIRONMENT override', () => {
459535
it('copies file without modification when SENTRY_ENVIRONMENT is not set', () => {
460536
const optionsContent = JSON.stringify({ dsn: 'https://key@sentry.io/123', environment: 'production' });

packages/core/test/tools/sentryMetroSerializer.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,32 @@ describe('Sentry Metro Serializer', () => {
232232
expect(debugId).toMatch(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/);
233233
});
234234
});
235+
236+
test('createDefaultMetroSerializer can be created without Metro internals being loaded at import time', () => {
237+
// This test verifies that the lazy-loading of Metro internals works correctly.
238+
// The createDefaultMetroSerializer function should be callable without triggering
239+
// module-level requires of Metro internals at import time.
240+
// See: https://github.com/getsentry/sentry-react-native/issues/5957
241+
242+
// Import the function
243+
const { createDefaultMetroSerializer: createSerializer } = require('../../src/js/tools/vendor/metro/utils');
244+
245+
// Create the serializer - this should succeed without loading Metro internals
246+
const serializer = createSerializer();
247+
expect(typeof serializer).toBe('function');
248+
249+
// Verify the serializer can be invoked with proper arguments and produces output
250+
const [entryPoint, preModules, graph, options] = mockMinSerializerArgs();
251+
const result = serializer(entryPoint, preModules, graph, options);
252+
253+
expect(result).toHaveProperty('code');
254+
expect(result).toHaveProperty('map');
255+
expect(typeof result.code).toBe('string');
256+
expect(typeof result.map).toBe('string');
257+
// Both code and map should exist (even if minimal for empty bundle)
258+
expect(result.code).toBeDefined();
259+
expect(result.map).toBeDefined();
260+
});
235261
});
236262

237263
function mockMinSerializerArgs(options?: {

packages/core/test/wrapper.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,23 @@ describe('Tests Native Wrapper', () => {
224224
expect(NATIVE.enableNative).toBe(true);
225225
});
226226

227+
test('passes attachAllThreads to native SDK', async () => {
228+
await NATIVE.initNativeSdk({
229+
dsn: 'test',
230+
enableNative: true,
231+
autoInitializeNativeSdk: true,
232+
attachAllThreads: true,
233+
devServerUrl: undefined,
234+
defaultSidecarUrl: undefined,
235+
mobileReplayOptions: undefined,
236+
});
237+
238+
expect(RNSentry.initNativeSdk).toHaveBeenCalled();
239+
// @ts-expect-error mock value
240+
const initParameter = RNSentry.initNativeSdk.mock.calls[0][0];
241+
expect(initParameter).toHaveProperty('attachAllThreads', true);
242+
});
243+
227244
test('filter integrations when initializing Native SDK', async () => {
228245
await NATIVE.initNativeSdk({
229246
dsn: 'test',

0 commit comments

Comments
 (0)