From a7047842611fe934a1671c0898b8751dcc1376c6 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Wed, 11 Mar 2026 14:37:58 +0100 Subject: [PATCH 1/2] feat(core): Generate sentry.options.json from Expo plugin config Adds an `options` property to the Expo config plugin that generates `sentry.options.json` during prebuild, removing the need to manually create the file when using `useNativeInit: true`. Closes #5664 Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 14 +++- packages/core/plugin/src/utils.ts | 12 +-- packages/core/plugin/src/withSentry.ts | 18 +++-- .../expo-plugin/writeSentryOptions.test.ts | 77 +++++++++++++++++++ .../writeSentryOptionsEnvironment.test.ts | 60 --------------- samples/expo/.gitignore | 3 + samples/expo/app.json | 24 +++++- samples/expo/sentry.options.json | 18 ----- 8 files changed, 130 insertions(+), 96 deletions(-) create mode 100644 packages/core/test/expo-plugin/writeSentryOptions.test.ts delete mode 100644 packages/core/test/expo-plugin/writeSentryOptionsEnvironment.test.ts delete mode 100644 samples/expo/sentry.options.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b1726e96c..2bb47c78a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,7 +26,19 @@ ```json ["@sentry/react-native/expo", { "useNativeInit": true, - "environment": "staging" + "options": { + "environment": "staging" + } + }] + ``` +- Generate `sentry.options.json` from the Expo config plugin `options` property ([#5664](https://github.com/getsentry/sentry-react-native/issues/5664)) + ```json + ["@sentry/react-native/expo", { + "useNativeInit": true, + "options": { + "dsn": "https://key@sentry.io/123", + "tracesSampleRate": 1.0 + } }] ``` diff --git a/packages/core/plugin/src/utils.ts b/packages/core/plugin/src/utils.ts index 79eba7e080..cc1499a9c6 100644 --- a/packages/core/plugin/src/utils.ts +++ b/packages/core/plugin/src/utils.ts @@ -12,19 +12,19 @@ export function writeSentryPropertiesTo(filepath: string, sentryProperties: stri const SENTRY_OPTIONS_FILE_NAME = 'sentry.options.json'; -export function writeSentryOptionsEnvironment(projectRoot: string, environment: string): void { +export function writeSentryOptions(projectRoot: string, pluginOptions: Record): void { const optionsFilePath = path.resolve(projectRoot, SENTRY_OPTIONS_FILE_NAME); - let options: Record = {}; + let existingOptions: Record = {}; if (fs.existsSync(optionsFilePath)) { try { - options = JSON.parse(fs.readFileSync(optionsFilePath, 'utf8')); + existingOptions = JSON.parse(fs.readFileSync(optionsFilePath, 'utf8')); } catch (e) { - warnOnce(`Failed to parse ${SENTRY_OPTIONS_FILE_NAME}: ${e}. The environment will not be set.`); + warnOnce(`Failed to parse ${SENTRY_OPTIONS_FILE_NAME}: ${e}. The options will not be set.`); return; } } - options.environment = environment; - fs.writeFileSync(optionsFilePath, `${JSON.stringify(options, null, 2)}\n`); + const mergedOptions = { ...existingOptions, ...pluginOptions }; + fs.writeFileSync(optionsFilePath, `${JSON.stringify(mergedOptions, null, 2)}\n`); } diff --git a/packages/core/plugin/src/withSentry.ts b/packages/core/plugin/src/withSentry.ts index b33fde0db9..8331dbf2c4 100644 --- a/packages/core/plugin/src/withSentry.ts +++ b/packages/core/plugin/src/withSentry.ts @@ -2,7 +2,7 @@ import type { ExpoConfig } from '@expo/config-types'; import type { ConfigPlugin } from 'expo/config-plugins'; import { createRunOncePlugin, withDangerousMod } from 'expo/config-plugins'; import { bold, warnOnce } from './logger'; -import { writeSentryOptionsEnvironment } from './utils'; +import { writeSentryOptions } from './utils'; import { PLUGIN_NAME, PLUGIN_VERSION } from './version'; import { withSentryAndroid } from './withSentryAndroid'; import type { SentryAndroidGradlePluginOptions } from './withSentryAndroidGradlePlugin'; @@ -15,7 +15,7 @@ interface PluginProps { authToken?: string; url?: string; useNativeInit?: boolean; - environment?: string; + options?: Record; experimental_android?: SentryAndroidGradlePluginOptions; } @@ -28,9 +28,13 @@ const withSentryPlugin: ConfigPlugin = (config, props) => { } let cfg = config; - const environment = props?.environment ?? process.env.SENTRY_ENVIRONMENT; + const pluginOptions = props?.options ? { ...props.options } : {}; + const environment = process.env.SENTRY_ENVIRONMENT; if (environment) { - cfg = withSentryOptionsEnvironment(cfg, environment); + pluginOptions.environment = environment; + } + if (Object.keys(pluginOptions).length > 0) { + cfg = withSentryOptionsFile(cfg, pluginOptions); } if (sentryProperties !== null) { try { @@ -87,20 +91,20 @@ ${project ? `defaults.project=${project}` : missingProjectMessage} ${authToken ? `${existingAuthTokenMessage}\nauth.token=${authToken}` : missingAuthTokenMessage}`; } -function withSentryOptionsEnvironment(config: ExpoConfig, environment: string): ExpoConfig { +function withSentryOptionsFile(config: ExpoConfig, pluginOptions: Record): 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); + writeSentryOptions(mod.modRequest.projectRoot, pluginOptions); return mod; }, ]); cfg = withDangerousMod(cfg, [ 'ios', mod => { - writeSentryOptionsEnvironment(mod.modRequest.projectRoot, environment); + writeSentryOptions(mod.modRequest.projectRoot, pluginOptions); return mod; }, ]); diff --git a/packages/core/test/expo-plugin/writeSentryOptions.test.ts b/packages/core/test/expo-plugin/writeSentryOptions.test.ts new file mode 100644 index 0000000000..9f00fb3e0c --- /dev/null +++ b/packages/core/test/expo-plugin/writeSentryOptions.test.ts @@ -0,0 +1,77 @@ +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { writeSentryOptions } from '../../plugin/src/utils'; + +jest.mock('../../plugin/src/logger'); + +describe('writeSentryOptions', () => { + 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 when file does not exist', () => { + writeSentryOptions(tempDir, { dsn: 'https://key@sentry.io/123', environment: 'staging' }); + + const filePath = path.join(tempDir, 'sentry.options.json'); + const content = JSON.parse(fs.readFileSync(filePath, 'utf8')); + expect(content).toEqual({ dsn: 'https://key@sentry.io/123', environment: 'staging' }); + }); + + test('merges options into 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' })); + + writeSentryOptions(tempDir, { environment: 'staging', debug: true }); + + const content = JSON.parse(fs.readFileSync(filePath, 'utf8')); + expect(content).toEqual({ dsn: 'https://key@sentry.io/123', environment: 'staging', debug: true }); + }); + + test('plugin options take precedence over existing file values', () => { + const filePath = path.join(tempDir, 'sentry.options.json'); + fs.writeFileSync(filePath, JSON.stringify({ dsn: 'https://old@sentry.io/1', debug: false })); + + writeSentryOptions(tempDir, { dsn: 'https://new@sentry.io/2' }); + + const content = JSON.parse(fs.readFileSync(filePath, 'utf8')); + expect(content.dsn).toBe('https://new@sentry.io/2'); + expect(content.debug).toBe(false); // preserved from existing file + }); + + 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{{{'); + + writeSentryOptions(tempDir, { environment: 'staging' }); + + expect(warnOnce).toHaveBeenCalledWith(expect.stringContaining('Failed to parse')); + // File should remain unchanged + expect(fs.readFileSync(filePath, 'utf8')).toBe('invalid json{{{'); + }); + + test('writes multiple options at once', () => { + writeSentryOptions(tempDir, { + dsn: 'https://key@sentry.io/123', + debug: true, + tracesSampleRate: 0.5, + environment: 'production', + }); + + const filePath = path.join(tempDir, 'sentry.options.json'); + const content = JSON.parse(fs.readFileSync(filePath, 'utf8')); + expect(content).toEqual({ + dsn: 'https://key@sentry.io/123', + debug: true, + tracesSampleRate: 0.5, + environment: 'production', + }); + }); +}); diff --git a/packages/core/test/expo-plugin/writeSentryOptionsEnvironment.test.ts b/packages/core/test/expo-plugin/writeSentryOptionsEnvironment.test.ts deleted file mode 100644 index 4432ddabf5..0000000000 --- a/packages/core/test/expo-plugin/writeSentryOptionsEnvironment.test.ts +++ /dev/null @@ -1,60 +0,0 @@ -import * as fs from 'fs'; -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; - - 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'); - }); - - 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{{{'); - }); -}); diff --git a/samples/expo/.gitignore b/samples/expo/.gitignore index f534ebd0c8..e9bf3b7784 100644 --- a/samples/expo/.gitignore +++ b/samples/expo/.gitignore @@ -38,6 +38,9 @@ yarn-error.* /android /ios +# Generated by @sentry/react-native expo plugin +sentry.options.json + # @generated expo-cli sync-2b81b286409207a5da26e14c78851eb30d8ccbdb # The following patterns were generated by expo-cli diff --git a/samples/expo/app.json b/samples/expo/app.json index 7b1d13c610..ccb01467be 100644 --- a/samples/expo/app.json +++ b/samples/expo/app.json @@ -13,9 +13,7 @@ "resizeMode": "contain", "backgroundColor": "#ffffff" }, - "assetBundlePatterns": [ - "**/*" - ], + "assetBundlePatterns": ["**/*"], "ios": { "supportsTablet": true, "bundleIdentifier": "io.sentry.expo.sample", @@ -46,6 +44,24 @@ "project": "sentry-react-native", "organization": "sentry-sdks", "useNativeInit": true, + "options": { + "dsn": "https://1df17bd4e543fdb31351dee1768bb679@o447951.ingest.sentry.io/5428561", + "debug": true, + "environment": "dev", + "enableUserInteractionTracing": true, + "enableAutoSessionTracking": true, + "sessionTrackingIntervalMillis": 30000, + "enableTracing": true, + "tracesSampleRate": 1.0, + "attachStacktrace": true, + "attachScreenshot": true, + "attachViewHierarchy": true, + "enableCaptureFailedRequests": true, + "profilesSampleRate": 1.0, + "replaysSessionSampleRate": 1.0, + "replaysOnErrorSampleRate": 1.0, + "spotlight": true + }, "experimental_android": { "enableAndroidGradlePlugin": true, "autoUploadProguardMapping": true, @@ -90,4 +106,4 @@ "url": "https://u.expo.dev/00000000-0000-0000-0000-000000000000" } } -} \ No newline at end of file +} diff --git a/samples/expo/sentry.options.json b/samples/expo/sentry.options.json deleted file mode 100644 index 53ae525bc0..0000000000 --- a/samples/expo/sentry.options.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "dsn": "https://1df17bd4e543fdb31351dee1768bb679@o447951.ingest.sentry.io/5428561", - "debug": true, - "environment": "dev", - "enableUserInteractionTracing": true, - "enableAutoSessionTracking": true, - "sessionTrackingIntervalMillis": 30000, - "enableTracing": true, - "tracesSampleRate": 1.0, - "attachStacktrace": true, - "attachScreenshot": true, - "attachViewHierarchy": true, - "enableCaptureFailedRequests": true, - "profilesSampleRate": 1.0, - "replaysSessionSampleRate": 1.0, - "replaysOnErrorSampleRate": 1.0, - "spotlight": true -} From eb37aa4fd09c94f10be3a65d5623faaf62865fb4 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Wed, 11 Mar 2026 14:42:38 +0100 Subject: [PATCH 2/2] Update changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bb47c78a0..44ea41aa91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,7 +31,7 @@ } }] ``` -- Generate `sentry.options.json` from the Expo config plugin `options` property ([#5664](https://github.com/getsentry/sentry-react-native/issues/5664)) +- Generate `sentry.options.json` from the Expo config plugin `options` property ([#5804](https://github.com/getsentry/sentry-react-native/pull/5804/)) ```json ["@sentry/react-native/expo", { "useNativeInit": true,