From 9fd70e2d561932b5c304c1e4b4d2f129f5ce13e1 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Tue, 10 Mar 2026 15:47:54 +0100 Subject: [PATCH 1/7] feat(core): Add `environment` option to Expo config plugin Closes #5779 Co-Authored-By: Claude Opus 4.6 --- packages/core/plugin/src/utils.ts | 14 ++++++ packages/core/plugin/src/withSentry.ts | 28 ++++++++++- .../writeSentryOptionsEnvironment.test.ts | 46 +++++++++++++++++++ 3 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 packages/core/test/expo-plugin/writeSentryOptionsEnvironment.test.ts diff --git a/packages/core/plugin/src/utils.ts b/packages/core/plugin/src/utils.ts index 9f4d154e12..1281dfbcea 100644 --- a/packages/core/plugin/src/utils.ts +++ b/packages/core/plugin/src/utils.ts @@ -8,3 +8,17 @@ export function writeSentryPropertiesTo(filepath: string, sentryProperties: stri fs.writeFileSync(path.resolve(filepath, 'sentry.properties'), sentryProperties); } + +const SENTRY_OPTIONS_FILE_NAME = 'sentry.options.json'; + +export function writeSentryOptionsEnvironment(projectRoot: string, environment: string): void { + const optionsFilePath = path.resolve(projectRoot, SENTRY_OPTIONS_FILE_NAME); + + let options: Record = {}; + if (fs.existsSync(optionsFilePath)) { + options = JSON.parse(fs.readFileSync(optionsFilePath, 'utf8')); + } + + options.environment = environment; + fs.writeFileSync(optionsFilePath, `${JSON.stringify(options, null, 2)}\n`); +} diff --git a/packages/core/plugin/src/withSentry.ts b/packages/core/plugin/src/withSentry.ts index 6cf2de1739..0e8f300fc5 100644 --- a/packages/core/plugin/src/withSentry.ts +++ b/packages/core/plugin/src/withSentry.ts @@ -1,6 +1,8 @@ +import type { ExpoConfig } from '@expo/config-types'; import type { ConfigPlugin } from 'expo/config-plugins'; -import { createRunOncePlugin } from 'expo/config-plugins'; +import { createRunOncePlugin, withDangerousMod } from 'expo/config-plugins'; import { bold, warnOnce } from './logger'; +import { writeSentryOptionsEnvironment } from './utils'; import { PLUGIN_NAME, PLUGIN_VERSION } from './version'; import { withSentryAndroid } from './withSentryAndroid'; import type { SentryAndroidGradlePluginOptions } from './withSentryAndroidGradlePlugin'; @@ -13,6 +15,7 @@ interface PluginProps { authToken?: string; url?: string; useNativeInit?: boolean; + environment?: string; experimental_android?: SentryAndroidGradlePluginOptions; } @@ -25,6 +28,9 @@ const withSentryPlugin: ConfigPlugin = (config, props) => { } let cfg = config; + if (props?.environment) { + cfg = withSentryOptionsEnvironment(cfg, props.environment); + } if (sentryProperties !== null) { try { cfg = withSentryAndroid(cfg, { sentryProperties, useNativeInit: props?.useNativeInit }); @@ -80,6 +86,26 @@ ${project ? `defaults.project=${project}` : missingProjectMessage} ${authToken ? `${existingAuthTokenMessage}\nauth.token=${authToken}` : missingAuthTokenMessage}`; } +function withSentryOptionsEnvironment(config: ExpoConfig, environment: string): ExpoConfig { + // withDangerousMod requires a platform key, but sentry.options.json is at the project root. + // We apply to both platforms so it works with `expo prebuild --platform ios` or `--platform android`. + let cfg = withDangerousMod(config, [ + 'android', + mod => { + writeSentryOptionsEnvironment(mod.modRequest.projectRoot, environment); + return mod; + }, + ]); + cfg = withDangerousMod(cfg, [ + 'ios', + mod => { + writeSentryOptionsEnvironment(mod.modRequest.projectRoot, environment); + return mod; + }, + ]); + return cfg; +} + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access const withSentry = createRunOncePlugin(withSentryPlugin, PLUGIN_NAME, PLUGIN_VERSION); diff --git a/packages/core/test/expo-plugin/writeSentryOptionsEnvironment.test.ts b/packages/core/test/expo-plugin/writeSentryOptionsEnvironment.test.ts new file mode 100644 index 0000000000..cb893c7f8b --- /dev/null +++ b/packages/core/test/expo-plugin/writeSentryOptionsEnvironment.test.ts @@ -0,0 +1,46 @@ +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { writeSentryOptionsEnvironment } from '../../plugin/src/utils'; + +describe('writeSentryOptionsEnvironment', () => { + let tempDir: string; + + beforeEach(() => { + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sentry-options-test-')); + }); + + afterEach(() => { + fs.rmSync(tempDir, { recursive: true, force: true }); + }); + + test('creates sentry.options.json with environment when file does not exist', () => { + writeSentryOptionsEnvironment(tempDir, 'staging'); + + const filePath = path.join(tempDir, 'sentry.options.json'); + const content = JSON.parse(fs.readFileSync(filePath, 'utf8')); + expect(content).toEqual({ environment: 'staging' }); + }); + + test('sets environment in existing sentry.options.json', () => { + const filePath = path.join(tempDir, 'sentry.options.json'); + fs.writeFileSync(filePath, JSON.stringify({ dsn: 'https://key@sentry.io/123', environment: 'production' })); + + writeSentryOptionsEnvironment(tempDir, 'staging'); + + const content = JSON.parse(fs.readFileSync(filePath, 'utf8')); + expect(content.environment).toBe('staging'); + expect(content.dsn).toBe('https://key@sentry.io/123'); + }); + + test('adds environment to existing sentry.options.json without environment', () => { + const filePath = path.join(tempDir, 'sentry.options.json'); + fs.writeFileSync(filePath, JSON.stringify({ dsn: 'https://key@sentry.io/123' })); + + writeSentryOptionsEnvironment(tempDir, 'staging'); + + const content = JSON.parse(fs.readFileSync(filePath, 'utf8')); + expect(content.environment).toBe('staging'); + expect(content.dsn).toBe('https://key@sentry.io/123'); + }); +}); From 0f2c0a8ea0dd8e1921396305d4077286ee219b01 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Tue, 10 Mar 2026 15:52:11 +0100 Subject: [PATCH 2/7] docs: Add changelog for environment config plugin option Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2063a8384b..cf3bbe37e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,14 @@ Sentry.wrapExpoAsset(Asset); ``` - Adds tags with Expo Updates context variables to make them searchable and filterable ([#5788](https://github.com/getsentry/sentry-react-native/pull/5788)) +- Add `environment` option to the Expo config plugin for native init ([#5796](https://github.com/getsentry/sentry-react-native/pull/5796)) + - Sets the environment in `sentry.options.json` during `expo prebuild`, so pre-JS crashes are tagged correctly + ```json + ["@sentry/react-native/expo", { + "useNativeInit": true, + "environment": "staging" + }] + ``` ### Fixes From 1e4276ae5ea132f92fa40f2eec2d3ef182959341 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Tue, 10 Mar 2026 16:01:51 +0100 Subject: [PATCH 3/7] feat(core): Support SENTRY_ENVIRONMENT env variable in Expo config plugin Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 2 ++ packages/core/plugin/src/withSentry.ts | 5 +++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf3bbe37e8..ece3b82575 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,12 +24,14 @@ - Adds tags with Expo Updates context variables to make them searchable and filterable ([#5788](https://github.com/getsentry/sentry-react-native/pull/5788)) - Add `environment` option to the Expo config plugin for native init ([#5796](https://github.com/getsentry/sentry-react-native/pull/5796)) - Sets the environment in `sentry.options.json` during `expo prebuild`, so pre-JS crashes are tagged correctly + - Reads from the `environment` plugin prop or the `SENTRY_ENVIRONMENT` environment variable ```json ["@sentry/react-native/expo", { "useNativeInit": true, "environment": "staging" }] ``` + - Or via `SENTRY_ENVIRONMENT` env variable (e.g. per EAS Build profile in `eas.json`) ### Fixes diff --git a/packages/core/plugin/src/withSentry.ts b/packages/core/plugin/src/withSentry.ts index 0e8f300fc5..e80ec17f24 100644 --- a/packages/core/plugin/src/withSentry.ts +++ b/packages/core/plugin/src/withSentry.ts @@ -28,8 +28,9 @@ const withSentryPlugin: ConfigPlugin = (config, props) => { } let cfg = config; - if (props?.environment) { - cfg = withSentryOptionsEnvironment(cfg, props.environment); + const environment = props?.environment || process.env.SENTRY_ENVIRONMENT; + if (environment) { + cfg = withSentryOptionsEnvironment(cfg, environment); } if (sentryProperties !== null) { try { From de127d0952d4c606e06ee38f98e354db26f32b9d Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Tue, 10 Mar 2026 16:06:52 +0100 Subject: [PATCH 4/7] docs: Simplify changelog entry Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ece3b82575..e87768e045 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,16 +22,7 @@ Sentry.wrapExpoAsset(Asset); ``` - Adds tags with Expo Updates context variables to make them searchable and filterable ([#5788](https://github.com/getsentry/sentry-react-native/pull/5788)) -- Add `environment` option to the Expo config plugin for native init ([#5796](https://github.com/getsentry/sentry-react-native/pull/5796)) - - Sets the environment in `sentry.options.json` during `expo prebuild`, so pre-JS crashes are tagged correctly - - Reads from the `environment` plugin prop or the `SENTRY_ENVIRONMENT` environment variable - ```json - ["@sentry/react-native/expo", { - "useNativeInit": true, - "environment": "staging" - }] - ``` - - Or via `SENTRY_ENVIRONMENT` env variable (e.g. per EAS Build profile in `eas.json`) +- Read `environment` from the Expo config plugin prop or `SENTRY_ENVIRONMENT` env variable into `sentry.options.json` ([#5796](https://github.com/getsentry/sentry-react-native/pull/5796)) ### Fixes From 11d7d1aac3854bc1455fa45c03b5cd039dd5e29b Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Tue, 10 Mar 2026 16:09:27 +0100 Subject: [PATCH 5/7] docs: Add example to changelog entry Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e87768e045..48c7cae198 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,7 +22,13 @@ Sentry.wrapExpoAsset(Asset); ``` - Adds tags with Expo Updates context variables to make them searchable and filterable ([#5788](https://github.com/getsentry/sentry-react-native/pull/5788)) -- Read `environment` from the Expo config plugin prop or `SENTRY_ENVIRONMENT` env variable into `sentry.options.json` ([#5796](https://github.com/getsentry/sentry-react-native/pull/5796)) +- Adds environment configuration in the Expo config plugin. This can be set with the `SENTRY_ENVIRONMENT` env variable or in `sentry.options.json` ([#5796](https://github.com/getsentry/sentry-react-native/pull/5796)) + ```json + ["@sentry/react-native/expo", { + "useNativeInit": true, + "environment": "staging" + }] + ``` ### Fixes From 1e22837a03f7c2348d0c745dd4f70fbb8bd83208 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Wed, 11 Mar 2026 11:07:05 +0100 Subject: [PATCH 6/7] fix(core): Address review feedback for environment config plugin option - Use `??` instead of `||` so explicit empty string is not ignored - Add try-catch for JSON.parse in writeSentryOptionsEnvironment - Add test for invalid JSON handling Co-Authored-By: Claude Opus 4.6 --- packages/core/plugin/src/utils.ts | 8 +++++++- packages/core/plugin/src/withSentry.ts | 2 +- .../writeSentryOptionsEnvironment.test.ts | 15 +++++++++++++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/packages/core/plugin/src/utils.ts b/packages/core/plugin/src/utils.ts index 1281dfbcea..79eba7e080 100644 --- a/packages/core/plugin/src/utils.ts +++ b/packages/core/plugin/src/utils.ts @@ -1,5 +1,6 @@ import * as fs from 'fs'; import * as path from 'path'; +import { warnOnce } from './logger'; export function writeSentryPropertiesTo(filepath: string, sentryProperties: string): void { if (!fs.existsSync(filepath)) { @@ -16,7 +17,12 @@ export function writeSentryOptionsEnvironment(projectRoot: string, environment: let options: Record = {}; if (fs.existsSync(optionsFilePath)) { - options = JSON.parse(fs.readFileSync(optionsFilePath, 'utf8')); + try { + options = JSON.parse(fs.readFileSync(optionsFilePath, 'utf8')); + } catch (e) { + warnOnce(`Failed to parse ${SENTRY_OPTIONS_FILE_NAME}: ${e}. The environment will not be set.`); + return; + } } options.environment = environment; diff --git a/packages/core/plugin/src/withSentry.ts b/packages/core/plugin/src/withSentry.ts index e80ec17f24..b33fde0db9 100644 --- a/packages/core/plugin/src/withSentry.ts +++ b/packages/core/plugin/src/withSentry.ts @@ -28,7 +28,7 @@ const withSentryPlugin: ConfigPlugin = (config, props) => { } let cfg = config; - const environment = props?.environment || process.env.SENTRY_ENVIRONMENT; + const environment = props?.environment ?? process.env.SENTRY_ENVIRONMENT; if (environment) { cfg = withSentryOptionsEnvironment(cfg, environment); } diff --git a/packages/core/test/expo-plugin/writeSentryOptionsEnvironment.test.ts b/packages/core/test/expo-plugin/writeSentryOptionsEnvironment.test.ts index cb893c7f8b..aa1aab40ac 100644 --- a/packages/core/test/expo-plugin/writeSentryOptionsEnvironment.test.ts +++ b/packages/core/test/expo-plugin/writeSentryOptionsEnvironment.test.ts @@ -3,6 +3,9 @@ import * as os from 'os'; import * as path from 'path'; import { writeSentryOptionsEnvironment } from '../../plugin/src/utils'; +jest.mock('../../plugin/src/logger'); + + describe('writeSentryOptionsEnvironment', () => { let tempDir: string; @@ -43,4 +46,16 @@ describe('writeSentryOptionsEnvironment', () => { expect(content.environment).toBe('staging'); expect(content.dsn).toBe('https://key@sentry.io/123'); }); + + test('does not crash and warns when sentry.options.json contains invalid JSON', () => { + const { warnOnce } = require('../../plugin/src/logger'); + const filePath = path.join(tempDir, 'sentry.options.json'); + fs.writeFileSync(filePath, 'invalid json{{{'); + + writeSentryOptionsEnvironment(tempDir, 'staging'); + + expect(warnOnce).toHaveBeenCalledWith(expect.stringContaining('Failed to parse')); + // File should remain unchanged + expect(fs.readFileSync(filePath, 'utf8')).toBe('invalid json{{{'); + }); }); From caeb51ea3c7bcc68ff25b9a260f81463c1602e7f Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Wed, 11 Mar 2026 11:38:28 +0100 Subject: [PATCH 7/7] Lint: remove uneeded line --- .../core/test/expo-plugin/writeSentryOptionsEnvironment.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core/test/expo-plugin/writeSentryOptionsEnvironment.test.ts b/packages/core/test/expo-plugin/writeSentryOptionsEnvironment.test.ts index aa1aab40ac..4432ddabf5 100644 --- a/packages/core/test/expo-plugin/writeSentryOptionsEnvironment.test.ts +++ b/packages/core/test/expo-plugin/writeSentryOptionsEnvironment.test.ts @@ -5,7 +5,6 @@ import { writeSentryOptionsEnvironment } from '../../plugin/src/utils'; jest.mock('../../plugin/src/logger'); - describe('writeSentryOptionsEnvironment', () => { let tempDir: string;