diff --git a/CHANGELOG.md b/CHANGELOG.md index bac57ab362..5403869e5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ ### Features +- Support `SENTRY_RELEASE` and `SENTRY_DIST` env vars in build scripts to override values in `sentry.options.json` at build time ([#6070](https://github.com/getsentry/sentry-react-native/pull/6070)) - Add `includeWebFeedback` Metro config option to exclude `@sentry-internal/feedback` from the bundle ([#6025](https://github.com/getsentry/sentry-react-native/pull/6025)) - Add rage tap detection — rapid consecutive taps on the same element emit `ui.multiClick` breadcrumbs and appear on the replay timeline with the rage click icon ([#5992](https://github.com/getsentry/sentry-react-native/pull/5992)) diff --git a/packages/core/scripts/sentry-xcode.sh b/packages/core/scripts/sentry-xcode.sh index e8b52e846a..ca83e7f2d9 100755 --- a/packages/core/scripts/sentry-xcode.sh +++ b/packages/core/scripts/sentry-xcode.sh @@ -105,17 +105,21 @@ if [ "$SENTRY_COPY_OPTIONS_FILE" = true ]; then else cp "$SENTRY_OPTIONS_FILE_PATH" "$SENTRY_OPTIONS_FILE_DESTINATION_PATH" - if [ -n "$SENTRY_ENVIRONMENT" ]; then + if [ -n "$SENTRY_ENVIRONMENT" ] || [ -n "$SENTRY_RELEASE" ] || [ -n "$SENTRY_DIST" ]; then if "$LOCAL_NODE_BINARY" -e " var fs = require('fs'); var destPath = process.argv[1]; var opts = JSON.parse(fs.readFileSync(destPath, 'utf8')); - opts.environment = process.env.SENTRY_ENVIRONMENT; + if (process.env.SENTRY_ENVIRONMENT) { opts.environment = process.env.SENTRY_ENVIRONMENT; } + if (process.env.SENTRY_RELEASE) { opts.release = process.env.SENTRY_RELEASE; } + if (process.env.SENTRY_DIST) { opts.dist = process.env.SENTRY_DIST; } fs.writeFileSync(destPath, JSON.stringify(opts)); " -- "$SENTRY_OPTIONS_FILE_DESTINATION_PATH" 2>/dev/null; then - echo "[Sentry] Overriding 'environment' from SENTRY_ENVIRONMENT environment variable" + [ -n "$SENTRY_ENVIRONMENT" ] && echo "[Sentry] Overriding 'environment' from SENTRY_ENVIRONMENT environment variable" + [ -n "$SENTRY_RELEASE" ] && echo "[Sentry] Overriding 'release' from SENTRY_RELEASE environment variable" + [ -n "$SENTRY_DIST" ] && echo "[Sentry] Overriding 'dist' from SENTRY_DIST environment variable" else - echo "[Sentry] Failed to override environment, copied file as-is." 1>&2 + echo "[Sentry] Failed to override options from environment variables, copied file as-is." 1>&2 fi fi echo "[Sentry] Copied $SENTRY_OPTIONS_FILE_PATH to $SENTRY_OPTIONS_FILE_DESTINATION_PATH" diff --git a/packages/core/sentry.gradle b/packages/core/sentry.gradle index b02973ecd6..9b99dab531 100644 --- a/packages/core/sentry.gradle +++ b/packages/core/sentry.gradle @@ -46,15 +46,21 @@ tasks.register("copySentryJsonConfiguration") { } def sentryEnv = System.getenv('SENTRY_ENVIRONMENT') - if (sentryEnv) { + def sentryRelease = System.getenv('SENTRY_RELEASE') + def sentryDist = System.getenv('SENTRY_DIST') + if (sentryEnv || sentryRelease || sentryDist) { try { def destFile = new File(androidAssetsDir, configFile) def content = new groovy.json.JsonSlurper().parseText(destFile.text) - content.environment = sentryEnv + if (sentryEnv) { content.environment = sentryEnv } + if (sentryRelease) { content.release = sentryRelease } + if (sentryDist) { content.dist = sentryDist } destFile.text = groovy.json.JsonOutput.toJson(content) - logger.lifecycle("Overriding 'environment' from SENTRY_ENVIRONMENT environment variable") + if (sentryEnv) { logger.lifecycle("Overriding 'environment' from SENTRY_ENVIRONMENT environment variable") } + if (sentryRelease) { logger.lifecycle("Overriding 'release' from SENTRY_RELEASE environment variable") } + if (sentryDist) { logger.lifecycle("Overriding 'dist' from SENTRY_DIST environment variable") } } catch (Exception e) { - logger.warn("Failed to override environment in ${configFile}: ${e.message}. Copied file as-is.") + logger.warn("Failed to override options in ${configFile}: ${e.message}. Copied file as-is.") } } logger.lifecycle("Copied ${configFile} to Android assets") diff --git a/packages/core/test/scripts/sentry-xcode-scripts.test.ts b/packages/core/test/scripts/sentry-xcode-scripts.test.ts index 1f822db47e..90eedcd6c8 100644 --- a/packages/core/test/scripts/sentry-xcode-scripts.test.ts +++ b/packages/core/test/scripts/sentry-xcode-scripts.test.ts @@ -531,8 +531,8 @@ describe('sentry-xcode.sh', () => { }); }); - describe('sentry.options.json SENTRY_ENVIRONMENT override', () => { - it('copies file without modification when SENTRY_ENVIRONMENT is not set', () => { + describe('sentry.options.json environment variable overrides', () => { + it('copies file without modification when no override env vars are 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); @@ -575,14 +575,92 @@ describe('sentry-xcode.sh', () => { }); expect(result.exitCode).toBe(0); - expect(result.stdout).toContain('Overriding'); + expect(result.stdout).toContain("Overriding 'environment' from SENTRY_ENVIRONMENT"); 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', () => { + it('overrides release from SENTRY_RELEASE env var', () => { + const optionsContent = JSON.stringify({ dsn: 'https://key@sentry.io/123' }); + 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_RELEASE: 'my-app@1.0.0+42', + }); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("Overriding 'release' from SENTRY_RELEASE"); + const destPath = path.join(buildDir, resourcesPath, 'sentry.options.json'); + const copied = JSON.parse(fs.readFileSync(destPath, 'utf8')); + expect(copied.release).toBe('my-app@1.0.0+42'); + expect(copied.dsn).toBe('https://key@sentry.io/123'); + }); + + it('overrides existing release value from SENTRY_RELEASE env var', () => { + const optionsContent = JSON.stringify({ dsn: 'https://key@sentry.io/123', release: 'old@1.0.0', dist: '1' }); + 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_RELEASE: 'new@2.0.0', + SENTRY_DIST: '2', + }); + + expect(result.exitCode).toBe(0); + const destPath = path.join(buildDir, resourcesPath, 'sentry.options.json'); + const copied = JSON.parse(fs.readFileSync(destPath, 'utf8')); + expect(copied.release).toBe('new@2.0.0'); + expect(copied.dist).toBe('2'); + }); + + it('overrides dist from SENTRY_DIST env var', () => { + const optionsContent = JSON.stringify({ dsn: 'https://key@sentry.io/123' }); + 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_DIST: '42', + }); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("Overriding 'dist' from SENTRY_DIST"); + const destPath = path.join(buildDir, resourcesPath, 'sentry.options.json'); + const copied = JSON.parse(fs.readFileSync(destPath, 'utf8')); + expect(copied.dist).toBe('42'); + expect(copied.dsn).toBe('https://key@sentry.io/123'); + }); + + it('overrides release, dist, and environment together', () => { const optionsContent = JSON.stringify({ dsn: 'https://key@sentry.io/123', environment: 'production' }); const optionsFile = path.join(tempDir, 'sentry.options.json'); fs.writeFileSync(optionsFile, optionsContent); @@ -591,6 +669,41 @@ describe('sentry-xcode.sh', () => { 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', + SENTRY_RELEASE: 'my-app@2.0.0+10', + SENTRY_DIST: '10', + }); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("Overriding 'environment' from SENTRY_ENVIRONMENT"); + expect(result.stdout).toContain("Overriding 'release' from SENTRY_RELEASE"); + expect(result.stdout).toContain("Overriding 'dist' from SENTRY_DIST"); + const destPath = path.join(buildDir, resourcesPath, 'sentry.options.json'); + const copied = JSON.parse(fs.readFileSync(destPath, 'utf8')); + expect(copied.environment).toBe('staging'); + expect(copied.release).toBe('my-app@2.0.0+10'); + expect(copied.dist).toBe('10'); + }); + + it('does not modify the source file when overriding', () => { + const optionsContent = JSON.stringify({ + dsn: 'https://key@sentry.io/123', + release: 'original@1.0.0', + 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', @@ -598,10 +711,14 @@ describe('sentry-xcode.sh', () => { CONFIGURATION_BUILD_DIR: buildDir, UNLOCALIZED_RESOURCES_FOLDER_PATH: resourcesPath, SENTRY_ENVIRONMENT: 'staging', + SENTRY_RELEASE: 'override@2.0.0', + SENTRY_DIST: '99', }); const source = JSON.parse(fs.readFileSync(optionsFile, 'utf8')); expect(source.environment).toBe('production'); + expect(source.release).toBe('original@1.0.0'); + expect(source.dist).toBeUndefined(); }); it('falls back to plain copy when sentry.options.json contains invalid JSON', () => { @@ -618,7 +735,7 @@ describe('sentry-xcode.sh', () => { SENTRY_OPTIONS_FILE_PATH: optionsFile, CONFIGURATION_BUILD_DIR: buildDir, UNLOCALIZED_RESOURCES_FOLDER_PATH: resourcesPath, - SENTRY_ENVIRONMENT: 'staging', + SENTRY_RELEASE: 'my-app@1.0.0', }); expect(result.exitCode).toBe(0);