From b111aaf89caba586dabbd301db17308e5db6bee1 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Mon, 16 Mar 2026 09:20:42 +0100 Subject: [PATCH 1/5] feat(core): Support SENTRY_ENVIRONMENT in bare React Native builds Read SENTRY_ENVIRONMENT env variable during the sentry.options.json copy step in both Gradle and Xcode build scripts, overriding the environment value in the destination copy without modifying the source. Closes #5779 Co-Authored-By: Claude Opus 4.6 --- packages/core/scripts/sentry-xcode.sh | 12 ++- packages/core/sentry.gradle | 16 +++- .../test/scripts/sentry-xcode-scripts.test.ts | 74 +++++++++++++++++++ 3 files changed, 97 insertions(+), 5 deletions(-) diff --git a/packages/core/scripts/sentry-xcode.sh b/packages/core/scripts/sentry-xcode.sh index b73ffbca35..5293d69b74 100755 --- a/packages/core/scripts/sentry-xcode.sh +++ b/packages/core/scripts/sentry-xcode.sh @@ -100,7 +100,17 @@ if [ "$SENTRY_COPY_OPTIONS_FILE" = true ]; then elif [ ! -f "$SENTRY_OPTIONS_FILE_PATH" ]; then echo "[Sentry] $SENTRY_OPTIONS_FILE_PATH not found. $SENTRY_OPTIONS_FILE_ERROR_MESSAGE_POSTFIX" 1>&2 else - cp "$SENTRY_OPTIONS_FILE_PATH" "$SENTRY_OPTIONS_FILE_DESTINATION_PATH" + if [ -n "$SENTRY_ENVIRONMENT" ]; then + "$LOCAL_NODE_BINARY" -e " + var fs = require('fs'); + var opts = JSON.parse(fs.readFileSync('$SENTRY_OPTIONS_FILE_PATH', 'utf8')); + opts.environment = process.env.SENTRY_ENVIRONMENT; + fs.writeFileSync('$SENTRY_OPTIONS_FILE_DESTINATION_PATH', JSON.stringify(opts)); + " + echo "[Sentry] Overriding 'environment' from SENTRY_ENVIRONMENT environment variable" + else + cp "$SENTRY_OPTIONS_FILE_PATH" "$SENTRY_OPTIONS_FILE_DESTINATION_PATH" + fi echo "[Sentry] Copied $SENTRY_OPTIONS_FILE_PATH to $SENTRY_OPTIONS_FILE_DESTINATION_PATH" fi fi diff --git a/packages/core/sentry.gradle b/packages/core/sentry.gradle index 91171e9c3b..3fa97b130d 100644 --- a/packages/core/sentry.gradle +++ b/packages/core/sentry.gradle @@ -39,10 +39,18 @@ tasks.register("copySentryJsonConfiguration") { androidAssetsDir.mkdirs() } - copy { - from sentryOptionsFile - into androidAssetsDir - rename { String fileName -> configFile } + def sentryEnv = System.getenv('SENTRY_ENVIRONMENT') + if (sentryEnv) { + def content = new groovy.json.JsonSlurper().parseText(sentryOptionsFile.text) + content.environment = sentryEnv + new File(androidAssetsDir, configFile).text = groovy.json.JsonOutput.toJson(content) + logger.lifecycle("Overriding 'environment' from SENTRY_ENVIRONMENT environment variable") + } else { + copy { + from sentryOptionsFile + into androidAssetsDir + rename { String fileName -> configFile } + } } logger.lifecycle("Copied ${configFile} to Android assets") } else { diff --git a/packages/core/test/scripts/sentry-xcode-scripts.test.ts b/packages/core/test/scripts/sentry-xcode-scripts.test.ts index b9154fd752..2bc5fdca51 100644 --- a/packages/core/test/scripts/sentry-xcode-scripts.test.ts +++ b/packages/core/test/scripts/sentry-xcode-scripts.test.ts @@ -455,6 +455,80 @@ describe('sentry-xcode.sh', () => { expect(result.stdout).toContain('skipping sourcemaps upload'); }); + describe('sentry.options.json SENTRY_ENVIRONMENT override', () => { + it('copies file without modification when SENTRY_ENVIRONMENT is not set', () => { + const optionsContent = JSON.stringify({ dsn: 'https://key@sentry.io/123', environment: 'production' }); + const optionsFile = path.join(tempDir, 'sentry.options.json'); + fs.writeFileSync(optionsFile, optionsContent); + + const buildDir = path.join(tempDir, 'build'); + const resourcesPath = 'Resources'; + fs.mkdirSync(path.join(buildDir, resourcesPath), { recursive: true }); + + const result = runScript({ + SENTRY_DISABLE_AUTO_UPLOAD: 'true', + SENTRY_COPY_OPTIONS_FILE: 'true', + SENTRY_OPTIONS_FILE_PATH: optionsFile, + CONFIGURATION_BUILD_DIR: buildDir, + UNLOCALIZED_RESOURCES_FOLDER_PATH: resourcesPath, + }); + + expect(result.exitCode).toBe(0); + const destPath = path.join(buildDir, resourcesPath, 'sentry.options.json'); + const copied = JSON.parse(fs.readFileSync(destPath, 'utf8')); + expect(copied.dsn).toBe('https://key@sentry.io/123'); + expect(copied.environment).toBe('production'); + }); + + it('overrides environment from SENTRY_ENVIRONMENT env var', () => { + const optionsContent = JSON.stringify({ dsn: 'https://key@sentry.io/123', environment: 'production' }); + const optionsFile = path.join(tempDir, 'sentry.options.json'); + fs.writeFileSync(optionsFile, optionsContent); + + const buildDir = path.join(tempDir, 'build'); + const resourcesPath = 'Resources'; + fs.mkdirSync(path.join(buildDir, resourcesPath), { recursive: true }); + + const result = runScript({ + SENTRY_DISABLE_AUTO_UPLOAD: 'true', + SENTRY_COPY_OPTIONS_FILE: 'true', + SENTRY_OPTIONS_FILE_PATH: optionsFile, + CONFIGURATION_BUILD_DIR: buildDir, + UNLOCALIZED_RESOURCES_FOLDER_PATH: resourcesPath, + SENTRY_ENVIRONMENT: 'staging', + }); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('Overriding'); + const destPath = path.join(buildDir, resourcesPath, 'sentry.options.json'); + const copied = JSON.parse(fs.readFileSync(destPath, 'utf8')); + expect(copied.environment).toBe('staging'); + expect(copied.dsn).toBe('https://key@sentry.io/123'); + }); + + it('does not modify the source sentry.options.json', () => { + const optionsContent = JSON.stringify({ dsn: 'https://key@sentry.io/123', environment: 'production' }); + const optionsFile = path.join(tempDir, 'sentry.options.json'); + fs.writeFileSync(optionsFile, optionsContent); + + const buildDir = path.join(tempDir, 'build'); + const resourcesPath = 'Resources'; + fs.mkdirSync(path.join(buildDir, resourcesPath), { recursive: true }); + + runScript({ + SENTRY_DISABLE_AUTO_UPLOAD: 'true', + SENTRY_COPY_OPTIONS_FILE: 'true', + SENTRY_OPTIONS_FILE_PATH: optionsFile, + CONFIGURATION_BUILD_DIR: buildDir, + UNLOCALIZED_RESOURCES_FOLDER_PATH: resourcesPath, + SENTRY_ENVIRONMENT: 'staging', + }); + + const source = JSON.parse(fs.readFileSync(optionsFile, 'utf8')); + expect(source.environment).toBe('production'); + }); + }); + describe('SOURCEMAP_FILE path resolution', () => { // Returns a mock sentry-cli that prints the SOURCEMAP_FILE env var it received. const makeSourcemapEchoScript = (dir: string): string => { From 749af342829b5a189255c12178ea4c11be4574d6 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Mon, 16 Mar 2026 09:26:45 +0100 Subject: [PATCH 2/5] docs: Add changelog for SENTRY_ENVIRONMENT bare RN support Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index add9b2b74e..2a8f4c17f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ > make sure you follow our [migration guide](https://docs.sentry.io/platforms/react-native/migration/) first. +## Unreleased + +### Features + +- Support `SENTRY_ENVIRONMENT` in bare React Native builds ([#5823](https://github.com/getsentry/sentry-react-native/pull/5823)) + ## 8.4.0 ### Fixes From 77e5915c7501dfc713b986630216936784bcfec1 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Mon, 16 Mar 2026 09:43:11 +0100 Subject: [PATCH 3/5] fix(core): Add error handling for SENTRY_ENVIRONMENT override in build scripts Wrap JSON parsing in try-catch (Gradle) and if-guard (Xcode) so invalid sentry.options.json falls back to a plain copy instead of failing the build. Co-Authored-By: Claude Opus 4.6 --- packages/core/scripts/sentry-xcode.sh | 10 +++++--- packages/core/sentry.gradle | 17 ++++++++++---- .../test/scripts/sentry-xcode-scripts.test.ts | 23 +++++++++++++++++++ 3 files changed, 43 insertions(+), 7 deletions(-) diff --git a/packages/core/scripts/sentry-xcode.sh b/packages/core/scripts/sentry-xcode.sh index 5293d69b74..baa1e0ac7e 100755 --- a/packages/core/scripts/sentry-xcode.sh +++ b/packages/core/scripts/sentry-xcode.sh @@ -101,13 +101,17 @@ if [ "$SENTRY_COPY_OPTIONS_FILE" = true ]; then echo "[Sentry] $SENTRY_OPTIONS_FILE_PATH not found. $SENTRY_OPTIONS_FILE_ERROR_MESSAGE_POSTFIX" 1>&2 else if [ -n "$SENTRY_ENVIRONMENT" ]; then - "$LOCAL_NODE_BINARY" -e " + if "$LOCAL_NODE_BINARY" -e " var fs = require('fs'); var opts = JSON.parse(fs.readFileSync('$SENTRY_OPTIONS_FILE_PATH', 'utf8')); opts.environment = process.env.SENTRY_ENVIRONMENT; fs.writeFileSync('$SENTRY_OPTIONS_FILE_DESTINATION_PATH', JSON.stringify(opts)); - " - echo "[Sentry] Overriding 'environment' from SENTRY_ENVIRONMENT environment variable" + " 2>/dev/null; then + echo "[Sentry] Overriding 'environment' from SENTRY_ENVIRONMENT environment variable" + else + echo "[Sentry] Failed to override environment, copying file as-is." 1>&2 + cp "$SENTRY_OPTIONS_FILE_PATH" "$SENTRY_OPTIONS_FILE_DESTINATION_PATH" + fi else cp "$SENTRY_OPTIONS_FILE_PATH" "$SENTRY_OPTIONS_FILE_DESTINATION_PATH" fi diff --git a/packages/core/sentry.gradle b/packages/core/sentry.gradle index 3fa97b130d..04b6af85dd 100644 --- a/packages/core/sentry.gradle +++ b/packages/core/sentry.gradle @@ -41,10 +41,19 @@ tasks.register("copySentryJsonConfiguration") { def sentryEnv = System.getenv('SENTRY_ENVIRONMENT') if (sentryEnv) { - def content = new groovy.json.JsonSlurper().parseText(sentryOptionsFile.text) - content.environment = sentryEnv - new File(androidAssetsDir, configFile).text = groovy.json.JsonOutput.toJson(content) - logger.lifecycle("Overriding 'environment' from SENTRY_ENVIRONMENT environment variable") + try { + def content = new groovy.json.JsonSlurper().parseText(sentryOptionsFile.text) + content.environment = sentryEnv + new File(androidAssetsDir, configFile).text = groovy.json.JsonOutput.toJson(content) + logger.lifecycle("Overriding 'environment' from SENTRY_ENVIRONMENT environment variable") + } catch (Exception e) { + logger.warn("Failed to override environment in ${configFile}: ${e.message}. Copying file as-is.") + copy { + from sentryOptionsFile + into androidAssetsDir + rename { String fileName -> configFile } + } + } } else { copy { from sentryOptionsFile diff --git a/packages/core/test/scripts/sentry-xcode-scripts.test.ts b/packages/core/test/scripts/sentry-xcode-scripts.test.ts index 2bc5fdca51..ec0056fe7b 100644 --- a/packages/core/test/scripts/sentry-xcode-scripts.test.ts +++ b/packages/core/test/scripts/sentry-xcode-scripts.test.ts @@ -527,6 +527,29 @@ describe('sentry-xcode.sh', () => { const source = JSON.parse(fs.readFileSync(optionsFile, 'utf8')); expect(source.environment).toBe('production'); }); + + it('falls back to plain copy when sentry.options.json contains invalid JSON', () => { + const optionsFile = path.join(tempDir, 'sentry.options.json'); + fs.writeFileSync(optionsFile, 'invalid json{{{'); + + const buildDir = path.join(tempDir, 'build'); + const resourcesPath = 'Resources'; + fs.mkdirSync(path.join(buildDir, resourcesPath), { recursive: true }); + + const result = runScript({ + SENTRY_DISABLE_AUTO_UPLOAD: 'true', + SENTRY_COPY_OPTIONS_FILE: 'true', + SENTRY_OPTIONS_FILE_PATH: optionsFile, + CONFIGURATION_BUILD_DIR: buildDir, + UNLOCALIZED_RESOURCES_FOLDER_PATH: resourcesPath, + SENTRY_ENVIRONMENT: 'staging', + }); + + expect(result.exitCode).toBe(0); + const destPath = path.join(buildDir, resourcesPath, 'sentry.options.json'); + expect(fs.readFileSync(destPath, 'utf8')).toBe('invalid json{{{'); + expect(result.stdout).toContain('Copied'); + }); }); describe('SOURCEMAP_FILE path resolution', () => { From 7d38de42f777ca62f7e2d8b56c532daaa3de6b35 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Mon, 16 Mar 2026 10:00:41 +0100 Subject: [PATCH 4/5] fix(ios): Pass file paths via process.argv instead of shell interpolation Avoids breaking inline JS when paths contain special characters like single quotes. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/core/scripts/sentry-xcode.sh | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/core/scripts/sentry-xcode.sh b/packages/core/scripts/sentry-xcode.sh index baa1e0ac7e..98192d9d61 100755 --- a/packages/core/scripts/sentry-xcode.sh +++ b/packages/core/scripts/sentry-xcode.sh @@ -103,10 +103,12 @@ if [ "$SENTRY_COPY_OPTIONS_FILE" = true ]; then if [ -n "$SENTRY_ENVIRONMENT" ]; then if "$LOCAL_NODE_BINARY" -e " var fs = require('fs'); - var opts = JSON.parse(fs.readFileSync('$SENTRY_OPTIONS_FILE_PATH', 'utf8')); + var sourcePath = process.argv[1]; + var destinationPath = process.argv[2]; + var opts = JSON.parse(fs.readFileSync(sourcePath, 'utf8')); opts.environment = process.env.SENTRY_ENVIRONMENT; - fs.writeFileSync('$SENTRY_OPTIONS_FILE_DESTINATION_PATH', JSON.stringify(opts)); - " 2>/dev/null; then + fs.writeFileSync(destinationPath, JSON.stringify(opts)); + " -- "$SENTRY_OPTIONS_FILE_PATH" "$SENTRY_OPTIONS_FILE_DESTINATION_PATH" 2>/dev/null; then echo "[Sentry] Overriding 'environment' from SENTRY_ENVIRONMENT environment variable" else echo "[Sentry] Failed to override environment, copying file as-is." 1>&2 From 6cf39f143231911d06746fa519cc4d7d19a7241f Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Mon, 16 Mar 2026 14:02:47 +0100 Subject: [PATCH 5/5] refactor(core): Unify copy logic in build scripts Always copy the file first, then override environment in-place if SENTRY_ENVIRONMENT is set. This removes duplicate copy blocks. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/core/scripts/sentry-xcode.sh | 16 +++++++--------- packages/core/sentry.gradle | 24 ++++++++++-------------- 2 files changed, 17 insertions(+), 23 deletions(-) diff --git a/packages/core/scripts/sentry-xcode.sh b/packages/core/scripts/sentry-xcode.sh index 98192d9d61..069f08be01 100755 --- a/packages/core/scripts/sentry-xcode.sh +++ b/packages/core/scripts/sentry-xcode.sh @@ -100,22 +100,20 @@ if [ "$SENTRY_COPY_OPTIONS_FILE" = true ]; then elif [ ! -f "$SENTRY_OPTIONS_FILE_PATH" ]; then echo "[Sentry] $SENTRY_OPTIONS_FILE_PATH not found. $SENTRY_OPTIONS_FILE_ERROR_MESSAGE_POSTFIX" 1>&2 else + cp "$SENTRY_OPTIONS_FILE_PATH" "$SENTRY_OPTIONS_FILE_DESTINATION_PATH" + if [ -n "$SENTRY_ENVIRONMENT" ]; then if "$LOCAL_NODE_BINARY" -e " var fs = require('fs'); - var sourcePath = process.argv[1]; - var destinationPath = process.argv[2]; - var opts = JSON.parse(fs.readFileSync(sourcePath, 'utf8')); + var destPath = process.argv[1]; + var opts = JSON.parse(fs.readFileSync(destPath, 'utf8')); opts.environment = process.env.SENTRY_ENVIRONMENT; - fs.writeFileSync(destinationPath, JSON.stringify(opts)); - " -- "$SENTRY_OPTIONS_FILE_PATH" "$SENTRY_OPTIONS_FILE_DESTINATION_PATH" 2>/dev/null; then + fs.writeFileSync(destPath, JSON.stringify(opts)); + " -- "$SENTRY_OPTIONS_FILE_DESTINATION_PATH" 2>/dev/null; then echo "[Sentry] Overriding 'environment' from SENTRY_ENVIRONMENT environment variable" else - echo "[Sentry] Failed to override environment, copying file as-is." 1>&2 - cp "$SENTRY_OPTIONS_FILE_PATH" "$SENTRY_OPTIONS_FILE_DESTINATION_PATH" + echo "[Sentry] Failed to override environment, copied file as-is." 1>&2 fi - else - cp "$SENTRY_OPTIONS_FILE_PATH" "$SENTRY_OPTIONS_FILE_DESTINATION_PATH" fi echo "[Sentry] Copied $SENTRY_OPTIONS_FILE_PATH to $SENTRY_OPTIONS_FILE_DESTINATION_PATH" fi diff --git a/packages/core/sentry.gradle b/packages/core/sentry.gradle index 04b6af85dd..86dcc1cbbf 100644 --- a/packages/core/sentry.gradle +++ b/packages/core/sentry.gradle @@ -39,26 +39,22 @@ tasks.register("copySentryJsonConfiguration") { androidAssetsDir.mkdirs() } + copy { + from sentryOptionsFile + into androidAssetsDir + rename { String fileName -> configFile } + } + def sentryEnv = System.getenv('SENTRY_ENVIRONMENT') if (sentryEnv) { try { - def content = new groovy.json.JsonSlurper().parseText(sentryOptionsFile.text) + def destFile = new File(androidAssetsDir, configFile) + def content = new groovy.json.JsonSlurper().parseText(destFile.text) content.environment = sentryEnv - new File(androidAssetsDir, configFile).text = groovy.json.JsonOutput.toJson(content) + destFile.text = groovy.json.JsonOutput.toJson(content) logger.lifecycle("Overriding 'environment' from SENTRY_ENVIRONMENT environment variable") } catch (Exception e) { - logger.warn("Failed to override environment in ${configFile}: ${e.message}. Copying file as-is.") - copy { - from sentryOptionsFile - into androidAssetsDir - rename { String fileName -> configFile } - } - } - } else { - copy { - from sentryOptionsFile - into androidAssetsDir - rename { String fileName -> configFile } + logger.warn("Failed to override environment in ${configFile}: ${e.message}. Copied file as-is.") } } logger.lifecycle("Copied ${configFile} to Android assets")