Skip to content

Commit 2b68628

Browse files
antonisclaude
andauthored
feat(core): Support SENTRY_ENVIRONMENT in bare React Native builds (#5823)
* 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 <noreply@anthropic.com> * docs: Add changelog for SENTRY_ENVIRONMENT bare RN support Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 7b1351d commit 2b68628

4 files changed

Lines changed: 130 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@
66
> make sure you follow our [migration guide](https://docs.sentry.io/platforms/react-native/migration/) first.
77
<!-- prettier-ignore-end -->
88
9+
## Unreleased
10+
11+
### Features
12+
13+
- Support `SENTRY_ENVIRONMENT` in bare React Native builds ([#5823](https://github.com/getsentry/sentry-react-native/pull/5823))
14+
915
## 8.4.0
1016

1117
### Fixes

packages/core/scripts/sentry-xcode.sh

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,20 @@ if [ "$SENTRY_COPY_OPTIONS_FILE" = true ]; then
101101
echo "[Sentry] $SENTRY_OPTIONS_FILE_PATH not found. $SENTRY_OPTIONS_FILE_ERROR_MESSAGE_POSTFIX" 1>&2
102102
else
103103
cp "$SENTRY_OPTIONS_FILE_PATH" "$SENTRY_OPTIONS_FILE_DESTINATION_PATH"
104+
105+
if [ -n "$SENTRY_ENVIRONMENT" ]; then
106+
if "$LOCAL_NODE_BINARY" -e "
107+
var fs = require('fs');
108+
var destPath = process.argv[1];
109+
var opts = JSON.parse(fs.readFileSync(destPath, 'utf8'));
110+
opts.environment = process.env.SENTRY_ENVIRONMENT;
111+
fs.writeFileSync(destPath, JSON.stringify(opts));
112+
" -- "$SENTRY_OPTIONS_FILE_DESTINATION_PATH" 2>/dev/null; then
113+
echo "[Sentry] Overriding 'environment' from SENTRY_ENVIRONMENT environment variable"
114+
else
115+
echo "[Sentry] Failed to override environment, copied file as-is." 1>&2
116+
fi
117+
fi
104118
echo "[Sentry] Copied $SENTRY_OPTIONS_FILE_PATH to $SENTRY_OPTIONS_FILE_DESTINATION_PATH"
105119
fi
106120
fi

packages/core/sentry.gradle

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,19 @@ tasks.register("copySentryJsonConfiguration") {
4444
into androidAssetsDir
4545
rename { String fileName -> configFile }
4646
}
47+
48+
def sentryEnv = System.getenv('SENTRY_ENVIRONMENT')
49+
if (sentryEnv) {
50+
try {
51+
def destFile = new File(androidAssetsDir, configFile)
52+
def content = new groovy.json.JsonSlurper().parseText(destFile.text)
53+
content.environment = sentryEnv
54+
destFile.text = groovy.json.JsonOutput.toJson(content)
55+
logger.lifecycle("Overriding 'environment' from SENTRY_ENVIRONMENT environment variable")
56+
} catch (Exception e) {
57+
logger.warn("Failed to override environment in ${configFile}: ${e.message}. Copied file as-is.")
58+
}
59+
}
4760
logger.lifecycle("Copied ${configFile} to Android assets")
4861
} else {
4962
logger.warn("${configFile} not found in app root (${appRoot})")

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

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

458+
describe('sentry.options.json SENTRY_ENVIRONMENT override', () => {
459+
it('copies file without modification when SENTRY_ENVIRONMENT is not set', () => {
460+
const optionsContent = JSON.stringify({ dsn: 'https://key@sentry.io/123', environment: 'production' });
461+
const optionsFile = path.join(tempDir, 'sentry.options.json');
462+
fs.writeFileSync(optionsFile, optionsContent);
463+
464+
const buildDir = path.join(tempDir, 'build');
465+
const resourcesPath = 'Resources';
466+
fs.mkdirSync(path.join(buildDir, resourcesPath), { recursive: true });
467+
468+
const result = runScript({
469+
SENTRY_DISABLE_AUTO_UPLOAD: 'true',
470+
SENTRY_COPY_OPTIONS_FILE: 'true',
471+
SENTRY_OPTIONS_FILE_PATH: optionsFile,
472+
CONFIGURATION_BUILD_DIR: buildDir,
473+
UNLOCALIZED_RESOURCES_FOLDER_PATH: resourcesPath,
474+
});
475+
476+
expect(result.exitCode).toBe(0);
477+
const destPath = path.join(buildDir, resourcesPath, 'sentry.options.json');
478+
const copied = JSON.parse(fs.readFileSync(destPath, 'utf8'));
479+
expect(copied.dsn).toBe('https://key@sentry.io/123');
480+
expect(copied.environment).toBe('production');
481+
});
482+
483+
it('overrides environment from SENTRY_ENVIRONMENT env var', () => {
484+
const optionsContent = JSON.stringify({ dsn: 'https://key@sentry.io/123', environment: 'production' });
485+
const optionsFile = path.join(tempDir, 'sentry.options.json');
486+
fs.writeFileSync(optionsFile, optionsContent);
487+
488+
const buildDir = path.join(tempDir, 'build');
489+
const resourcesPath = 'Resources';
490+
fs.mkdirSync(path.join(buildDir, resourcesPath), { recursive: true });
491+
492+
const result = runScript({
493+
SENTRY_DISABLE_AUTO_UPLOAD: 'true',
494+
SENTRY_COPY_OPTIONS_FILE: 'true',
495+
SENTRY_OPTIONS_FILE_PATH: optionsFile,
496+
CONFIGURATION_BUILD_DIR: buildDir,
497+
UNLOCALIZED_RESOURCES_FOLDER_PATH: resourcesPath,
498+
SENTRY_ENVIRONMENT: 'staging',
499+
});
500+
501+
expect(result.exitCode).toBe(0);
502+
expect(result.stdout).toContain('Overriding');
503+
const destPath = path.join(buildDir, resourcesPath, 'sentry.options.json');
504+
const copied = JSON.parse(fs.readFileSync(destPath, 'utf8'));
505+
expect(copied.environment).toBe('staging');
506+
expect(copied.dsn).toBe('https://key@sentry.io/123');
507+
});
508+
509+
it('does not modify the source sentry.options.json', () => {
510+
const optionsContent = JSON.stringify({ dsn: 'https://key@sentry.io/123', environment: 'production' });
511+
const optionsFile = path.join(tempDir, 'sentry.options.json');
512+
fs.writeFileSync(optionsFile, optionsContent);
513+
514+
const buildDir = path.join(tempDir, 'build');
515+
const resourcesPath = 'Resources';
516+
fs.mkdirSync(path.join(buildDir, resourcesPath), { recursive: true });
517+
518+
runScript({
519+
SENTRY_DISABLE_AUTO_UPLOAD: 'true',
520+
SENTRY_COPY_OPTIONS_FILE: 'true',
521+
SENTRY_OPTIONS_FILE_PATH: optionsFile,
522+
CONFIGURATION_BUILD_DIR: buildDir,
523+
UNLOCALIZED_RESOURCES_FOLDER_PATH: resourcesPath,
524+
SENTRY_ENVIRONMENT: 'staging',
525+
});
526+
527+
const source = JSON.parse(fs.readFileSync(optionsFile, 'utf8'));
528+
expect(source.environment).toBe('production');
529+
});
530+
531+
it('falls back to plain copy when sentry.options.json contains invalid JSON', () => {
532+
const optionsFile = path.join(tempDir, 'sentry.options.json');
533+
fs.writeFileSync(optionsFile, 'invalid json{{{');
534+
535+
const buildDir = path.join(tempDir, 'build');
536+
const resourcesPath = 'Resources';
537+
fs.mkdirSync(path.join(buildDir, resourcesPath), { recursive: true });
538+
539+
const result = runScript({
540+
SENTRY_DISABLE_AUTO_UPLOAD: 'true',
541+
SENTRY_COPY_OPTIONS_FILE: 'true',
542+
SENTRY_OPTIONS_FILE_PATH: optionsFile,
543+
CONFIGURATION_BUILD_DIR: buildDir,
544+
UNLOCALIZED_RESOURCES_FOLDER_PATH: resourcesPath,
545+
SENTRY_ENVIRONMENT: 'staging',
546+
});
547+
548+
expect(result.exitCode).toBe(0);
549+
const destPath = path.join(buildDir, resourcesPath, 'sentry.options.json');
550+
expect(fs.readFileSync(destPath, 'utf8')).toBe('invalid json{{{');
551+
expect(result.stdout).toContain('Copied');
552+
});
553+
});
554+
458555
describe('SOURCEMAP_FILE path resolution', () => {
459556
// Returns a mock sentry-cli that prints the SOURCEMAP_FILE env var it received.
460557
const makeSourcemapEchoScript = (dir: string): string => {

0 commit comments

Comments
 (0)