diff --git a/CHANGELOG.md b/CHANGELOG.md index 5db35c9156..d703589db5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ ## Unreleased +### Fixes + +- Resolve relative `SOURCEMAP_FILE` paths against the project root in the Xcode build script ([#5730](https://github.com/getsentry/sentry-react-native/pull/5730)) + ### Dependencies - Bump JavaScript SDK from v10.39.0 to v10.40.0 ([#5715](https://github.com/getsentry/sentry-react-native/pull/5715)) diff --git a/packages/core/scripts/sentry-xcode.sh b/packages/core/scripts/sentry-xcode.sh index 2ab36fc1b1..973f358b39 100755 --- a/packages/core/scripts/sentry-xcode.sh +++ b/packages/core/scripts/sentry-xcode.sh @@ -15,6 +15,13 @@ RN_PROJECT_ROOT="${PROJECT_DIR}/.." [ -z "$SENTRY_PROPERTIES" ] && export SENTRY_PROPERTIES=sentry.properties [ -z "$SENTRY_DOTENV_PATH" ] && [ -f "$RN_PROJECT_ROOT/.env.sentry-build-plugin" ] && export SENTRY_DOTENV_PATH="$RN_PROJECT_ROOT/.env.sentry-build-plugin" [ -z "$SOURCEMAP_FILE" ] && export SOURCEMAP_FILE="$DERIVED_FILE_DIR/main.jsbundle.map" +# Resolve relative SOURCEMAP_FILE to absolute. The script runs from `ios/` (Xcode's PWD), +# but users typically specify paths relative to the project root. Without this, sentry-cli +# would resolve relative paths against `ios/` and fail to find the file. +# See: https://github.com/getsentry/sentry-react-native/issues/3889 +if [[ "$SOURCEMAP_FILE" != /* ]]; then + export SOURCEMAP_FILE="$(cd "$RN_PROJECT_ROOT" && pwd)/$SOURCEMAP_FILE" +fi if [ -z "$SENTRY_CLI_EXECUTABLE" ]; then # Try standard resolution safely diff --git a/packages/core/test/scripts/sentry-xcode-scripts.test.ts b/packages/core/test/scripts/sentry-xcode-scripts.test.ts index aac5d69566..7e9252ad05 100644 --- a/packages/core/test/scripts/sentry-xcode-scripts.test.ts +++ b/packages/core/test/scripts/sentry-xcode-scripts.test.ts @@ -454,4 +454,81 @@ describe('sentry-xcode.sh', () => { expect(result.stdout).toContain('SENTRY_DISABLE_AUTO_UPLOAD=true'); expect(result.stdout).toContain('skipping sourcemaps upload'); }); + + describe('SOURCEMAP_FILE path resolution', () => { + // Returns a mock sentry-cli that prints the SOURCEMAP_FILE env var it received. + const makeSourcmapEchoScript = (dir: string): string => { + const scriptPath = path.join(dir, 'mock-sentry-cli-echo-sourcemap.js'); + fs.writeFileSync( + scriptPath, + ` + const sourcemapFile = process.env.SOURCEMAP_FILE || 'not-set'; + console.log('SOURCEMAP_FILE=' + sourcemapFile); + process.exit(0); + `, + ); + return scriptPath; + }; + + it('leaves an absolute SOURCEMAP_FILE unchanged', () => { + const absolutePath = path.join(tempDir, 'absolute', 'main.jsbundle.map'); + const echoScript = makeSourcmapEchoScript(tempDir); + + const result = runScript({ + SENTRY_CLI_EXECUTABLE: echoScript, + SOURCEMAP_FILE: absolutePath, + }); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain(`SOURCEMAP_FILE=${absolutePath}`); + }); + + it('resolves a relative SOURCEMAP_FILE against the project root, not ios/', () => { + // PROJECT_DIR is tempDir (simulates the ios/ folder). + // RN_PROJECT_ROOT = PROJECT_DIR/.. = parent of tempDir. + // A user setting SOURCEMAP_FILE=relative/path.map expects it relative to the project root. + const echoScript = makeSourcmapEchoScript(tempDir); + + const result = runScript({ + SENTRY_CLI_EXECUTABLE: echoScript, + SOURCEMAP_FILE: 'relative/path.map', + }); + + const projectRoot = path.dirname(tempDir); // PROJECT_DIR/.. = RN_PROJECT_ROOT + const expectedPath = path.join(projectRoot, 'relative/path.map'); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain(`SOURCEMAP_FILE=${expectedPath}`); + }); + + it('resolves a ./prefixed SOURCEMAP_FILE against the project root', () => { + const echoScript = makeSourcmapEchoScript(tempDir); + + const result = runScript({ + SENTRY_CLI_EXECUTABLE: echoScript, + SOURCEMAP_FILE: './maps/main.jsbundle.map', + }); + + // The script concatenates: "$(cd RN_PROJECT_ROOT && pwd)/./maps/main.jsbundle.map" + // The ./ is preserved but the path is absolute and valid for sentry-cli. + const projectRoot = path.dirname(tempDir); + const expectedPath = `${projectRoot}/./maps/main.jsbundle.map`; + + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain(`SOURCEMAP_FILE=${expectedPath}`); + }); + + it('uses the absolute default SOURCEMAP_FILE when not set by the user', () => { + const echoScript = makeSourcmapEchoScript(tempDir); + + const result = runScript({ + SENTRY_CLI_EXECUTABLE: echoScript, + // SOURCEMAP_FILE intentionally not set — script should default to $DERIVED_FILE_DIR/main.jsbundle.map + DERIVED_FILE_DIR: tempDir, + }); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain(`SOURCEMAP_FILE=${tempDir}/main.jsbundle.map`); + }); + }); });