Skip to content

Commit 06d4e24

Browse files
antonisclaude
andauthored
feat(core): Add environment option to Expo config plugin (#5796)
* feat(core): Add `environment` option to Expo config plugin Closes #5779 Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 078ebe1 commit 06d4e24

4 files changed

Lines changed: 115 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,13 @@
2222
Sentry.wrapExpoAsset(Asset);
2323
```
2424
- Adds tags with Expo Updates context variables to make them searchable and filterable ([#5788](https://github.com/getsentry/sentry-react-native/pull/5788))
25+
- 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))
26+
```json
27+
["@sentry/react-native/expo", {
28+
"useNativeInit": true,
29+
"environment": "staging"
30+
}]
31+
```
2532

2633
### Fixes
2734

packages/core/plugin/src/utils.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as fs from 'fs';
22
import * as path from 'path';
3+
import { warnOnce } from './logger';
34

45
export function writeSentryPropertiesTo(filepath: string, sentryProperties: string): void {
56
if (!fs.existsSync(filepath)) {
@@ -8,3 +9,22 @@ export function writeSentryPropertiesTo(filepath: string, sentryProperties: stri
89

910
fs.writeFileSync(path.resolve(filepath, 'sentry.properties'), sentryProperties);
1011
}
12+
13+
const SENTRY_OPTIONS_FILE_NAME = 'sentry.options.json';
14+
15+
export function writeSentryOptionsEnvironment(projectRoot: string, environment: string): void {
16+
const optionsFilePath = path.resolve(projectRoot, SENTRY_OPTIONS_FILE_NAME);
17+
18+
let options: Record<string, unknown> = {};
19+
if (fs.existsSync(optionsFilePath)) {
20+
try {
21+
options = JSON.parse(fs.readFileSync(optionsFilePath, 'utf8'));
22+
} catch (e) {
23+
warnOnce(`Failed to parse ${SENTRY_OPTIONS_FILE_NAME}: ${e}. The environment will not be set.`);
24+
return;
25+
}
26+
}
27+
28+
options.environment = environment;
29+
fs.writeFileSync(optionsFilePath, `${JSON.stringify(options, null, 2)}\n`);
30+
}

packages/core/plugin/src/withSentry.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import type { ExpoConfig } from '@expo/config-types';
12
import type { ConfigPlugin } from 'expo/config-plugins';
2-
import { createRunOncePlugin } from 'expo/config-plugins';
3+
import { createRunOncePlugin, withDangerousMod } from 'expo/config-plugins';
34
import { bold, warnOnce } from './logger';
5+
import { writeSentryOptionsEnvironment } from './utils';
46
import { PLUGIN_NAME, PLUGIN_VERSION } from './version';
57
import { withSentryAndroid } from './withSentryAndroid';
68
import type { SentryAndroidGradlePluginOptions } from './withSentryAndroidGradlePlugin';
@@ -13,6 +15,7 @@ interface PluginProps {
1315
authToken?: string;
1416
url?: string;
1517
useNativeInit?: boolean;
18+
environment?: string;
1619
experimental_android?: SentryAndroidGradlePluginOptions;
1720
}
1821

@@ -25,6 +28,10 @@ const withSentryPlugin: ConfigPlugin<PluginProps | void> = (config, props) => {
2528
}
2629

2730
let cfg = config;
31+
const environment = props?.environment ?? process.env.SENTRY_ENVIRONMENT;
32+
if (environment) {
33+
cfg = withSentryOptionsEnvironment(cfg, environment);
34+
}
2835
if (sentryProperties !== null) {
2936
try {
3037
cfg = withSentryAndroid(cfg, { sentryProperties, useNativeInit: props?.useNativeInit });
@@ -80,6 +87,26 @@ ${project ? `defaults.project=${project}` : missingProjectMessage}
8087
${authToken ? `${existingAuthTokenMessage}\nauth.token=${authToken}` : missingAuthTokenMessage}`;
8188
}
8289

90+
function withSentryOptionsEnvironment(config: ExpoConfig, environment: string): ExpoConfig {
91+
// withDangerousMod requires a platform key, but sentry.options.json is at the project root.
92+
// We apply to both platforms so it works with `expo prebuild --platform ios` or `--platform android`.
93+
let cfg = withDangerousMod(config, [
94+
'android',
95+
mod => {
96+
writeSentryOptionsEnvironment(mod.modRequest.projectRoot, environment);
97+
return mod;
98+
},
99+
]);
100+
cfg = withDangerousMod(cfg, [
101+
'ios',
102+
mod => {
103+
writeSentryOptionsEnvironment(mod.modRequest.projectRoot, environment);
104+
return mod;
105+
},
106+
]);
107+
return cfg;
108+
}
109+
83110
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
84111
const withSentry = createRunOncePlugin(withSentryPlugin, PLUGIN_NAME, PLUGIN_VERSION);
85112

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import * as fs from 'fs';
2+
import * as os from 'os';
3+
import * as path from 'path';
4+
import { writeSentryOptionsEnvironment } from '../../plugin/src/utils';
5+
6+
jest.mock('../../plugin/src/logger');
7+
8+
describe('writeSentryOptionsEnvironment', () => {
9+
let tempDir: string;
10+
11+
beforeEach(() => {
12+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sentry-options-test-'));
13+
});
14+
15+
afterEach(() => {
16+
fs.rmSync(tempDir, { recursive: true, force: true });
17+
});
18+
19+
test('creates sentry.options.json with environment when file does not exist', () => {
20+
writeSentryOptionsEnvironment(tempDir, 'staging');
21+
22+
const filePath = path.join(tempDir, 'sentry.options.json');
23+
const content = JSON.parse(fs.readFileSync(filePath, 'utf8'));
24+
expect(content).toEqual({ environment: 'staging' });
25+
});
26+
27+
test('sets environment in existing sentry.options.json', () => {
28+
const filePath = path.join(tempDir, 'sentry.options.json');
29+
fs.writeFileSync(filePath, JSON.stringify({ dsn: 'https://key@sentry.io/123', environment: 'production' }));
30+
31+
writeSentryOptionsEnvironment(tempDir, 'staging');
32+
33+
const content = JSON.parse(fs.readFileSync(filePath, 'utf8'));
34+
expect(content.environment).toBe('staging');
35+
expect(content.dsn).toBe('https://key@sentry.io/123');
36+
});
37+
38+
test('adds environment to existing sentry.options.json without environment', () => {
39+
const filePath = path.join(tempDir, 'sentry.options.json');
40+
fs.writeFileSync(filePath, JSON.stringify({ dsn: 'https://key@sentry.io/123' }));
41+
42+
writeSentryOptionsEnvironment(tempDir, 'staging');
43+
44+
const content = JSON.parse(fs.readFileSync(filePath, 'utf8'));
45+
expect(content.environment).toBe('staging');
46+
expect(content.dsn).toBe('https://key@sentry.io/123');
47+
});
48+
49+
test('does not crash and warns when sentry.options.json contains invalid JSON', () => {
50+
const { warnOnce } = require('../../plugin/src/logger');
51+
const filePath = path.join(tempDir, 'sentry.options.json');
52+
fs.writeFileSync(filePath, 'invalid json{{{');
53+
54+
writeSentryOptionsEnvironment(tempDir, 'staging');
55+
56+
expect(warnOnce).toHaveBeenCalledWith(expect.stringContaining('Failed to parse'));
57+
// File should remain unchanged
58+
expect(fs.readFileSync(filePath, 'utf8')).toBe('invalid json{{{');
59+
});
60+
});

0 commit comments

Comments
 (0)