Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

- Fix duplicate JS error reporting on iOS New Architecture when the native SDK is initialized early via `sentry.options.json` ("Capture App Start Errors"). It's done by applying the `ExceptionsManager.reportException` C++ wrapper filter in both init paths ([#6145](https://github.com/getsentry/sentry-react-native/pull/6145))
- Fix boolean options from `sentry.options.json` being ignored on Android when using `RNSentrySDK.init` ([#6130](https://github.com/getsentry/sentry-react-native/pull/6130))
- Fix `sentry-expo-upload-sourcemaps` failing for projects with `devEngines.packageManager` set to non-npm managers ([#6155](https://github.com/getsentry/sentry-react-native/pull/6155))

### Dependencies

Expand Down
87 changes: 63 additions & 24 deletions packages/core/test/scripts/expo-upload-sourcemaps.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,19 +295,6 @@ process.exit(exitCode);
});

describe('sentry.properties fallback', () => {
let mockNpxScript: string;
let mockBinDir: string;

beforeEach(() => {
// Create a mock npx that makes `expo config --json` fail fast
// so the script falls through to the sentry.properties fallback
mockBinDir = path.join(tempDir, 'mock-bin');
fs.mkdirSync(mockBinDir, { recursive: true });
mockNpxScript = path.join(mockBinDir, 'npx');
fs.writeFileSync(mockNpxScript, '#!/usr/bin/env node\nprocess.exit(1);\n');
fs.chmodSync(mockNpxScript, '755');
});

const createSentryProperties = (dir: string, content: string) => {
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(path.join(dir, 'sentry.properties'), content);
Expand All @@ -320,8 +307,7 @@ process.exit(exitCode);
const defaultEnv = {
SENTRY_AUTH_TOKEN: 'test-token',
SENTRY_CLI_EXECUTABLE: mockSentryCliScript,
// Put mock npx first in PATH so expo config fails fast
PATH: `${mockBinDir}:${process.env.PATH}`,
NODE_PATH: path.join(tempDir, 'nonexistent_modules'),
};

const result = spawnSync(process.execPath, [EXPO_UPLOAD_SCRIPT, outputDir], {
Expand Down Expand Up @@ -415,28 +401,30 @@ process.exit(exitCode);
expoConfig: Record<string, unknown>,
env: Record<string, string | undefined> = {},
): { stdout: string; stderr: string; exitCode: number } => {
// Create a mock npx that outputs the given expo config as JSON
const mockBinDir = path.join(tempDir, 'mock-bin-expo');
fs.mkdirSync(mockBinDir, { recursive: true });
const mockNpxScript = path.join(mockBinDir, 'npx');
// The mock npx script outputs the config JSON when called with 'expo config --json'
// Create a mock expo/bin/cli that outputs the given expo config as JSON
const mockExpoCliDir = path.join(tempDir, 'node_modules', 'expo', 'bin');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

expo/bin/cli is not a public API โ€”ย ok, it works today but if Expo renames/relocates it, sourcemap upload will silently fall back to sentry.properties/env vars

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what I'm saying is that it's probably better to resolve expo/package.json and read its bin.expo field

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea @alwx ๐Ÿ‘ Update with 6ba2bfe

fs.mkdirSync(mockExpoCliDir, { recursive: true });
fs.writeFileSync(
mockNpxScript,
path.join(mockExpoCliDir, 'cli'),
`#!/usr/bin/env node
const args = process.argv.slice(2);
if (args.includes('expo') && args.includes('config') && args.includes('--json')) {
if (args.includes('config') && args.includes('--json')) {
process.stdout.write(${JSON.stringify(JSON.stringify(expoConfig))});
process.exit(0);
}
process.exit(1);
`,
);
fs.chmodSync(mockNpxScript, '755');
fs.chmodSync(path.join(mockExpoCliDir, 'cli'), '755');
fs.writeFileSync(
path.join(tempDir, 'node_modules', 'expo', 'package.json'),
JSON.stringify({ name: 'expo', version: '0.0.0', bin: { expo: 'bin/cli' } }),
);

const defaultEnv = {
SENTRY_AUTH_TOKEN: 'test-token',
SENTRY_CLI_EXECUTABLE: mockSentryCliScript,
PATH: `${mockBinDir}:${process.env.PATH}`,
NODE_PATH: path.join(tempDir, 'node_modules'),
};

const result = spawnSync(process.execPath, [EXPO_UPLOAD_SCRIPT, outputDir], {
Expand Down Expand Up @@ -536,6 +524,57 @@ process.exit(1);
});
});

describe('expo resolution via cwd fallback (pnpm strict isolation)', () => {
it('resolves expo from cwd when not resolvable from the script location', () => {
createAssets(['bundle.js', 'bundle.js.map']);

const expoConfig = {
plugins: [
['@sentry/react-native/expo', { organization: 'cwd-org', project: 'cwd-project', url: 'https://sentry.io/' }],
],
};

// Place mock expo only under tempDir/node_modules โ€” reachable via cwd, not via NODE_PATH
const mockExpoCliDir = path.join(tempDir, 'node_modules', 'expo', 'bin');
fs.mkdirSync(mockExpoCliDir, { recursive: true });
fs.writeFileSync(
path.join(mockExpoCliDir, 'cli'),
`#!/usr/bin/env node
const args = process.argv.slice(2);
if (args.includes('config') && args.includes('--json')) {
process.stdout.write(${JSON.stringify(JSON.stringify(expoConfig))});
process.exit(0);
}
process.exit(1);
`,
);
fs.chmodSync(path.join(mockExpoCliDir, 'cli'), '755');
fs.writeFileSync(
path.join(tempDir, 'node_modules', 'expo', 'package.json'),
JSON.stringify({ name: 'expo', version: '0.0.0', bin: { expo: 'bin/cli' } }),
);

const result = spawnSync(process.execPath, [EXPO_UPLOAD_SCRIPT, outputDir], {
cwd: tempDir,
env: {
...process.env,
SENTRY_AUTH_TOKEN: 'test-token',
SENTRY_CLI_EXECUTABLE: mockSentryCliScript,
SENTRY_ORG: undefined,
SENTRY_PROJECT: undefined,
SENTRY_URL: undefined,
MOCK_CLI_EXIT_CODE: '0',
NODE_PATH: path.join(tempDir, 'nonexistent_modules'),
},
encoding: 'utf8',
timeout: 10000,
});

expect(result.status).toBe(0);
expect(result.stdout).toContain('SENTRY_ORG resolved to cwd-org');
});
});

describe('sourcemap processing', () => {
it('converts debugId to debug_id in sourcemaps', () => {
createAssets(['bundle.js', 'bundle.js.map']);
Expand Down
10 changes: 9 additions & 1 deletion packages/expo-upload-sourcemaps/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,15 @@ function getEnvVar(varname) {

function getSentryPluginPropertiesFromExpoConfig() {
try {
const result = spawnSync('npx', ['expo', 'config', '--json'], { encoding: 'utf8' });
let expoPkgJson;
try {
expoPkgJson = require.resolve('expo/package.json');
} catch {
expoPkgJson = require.resolve('expo/package.json', { paths: [process.cwd()] });
}
Comment thread
sentry-warden[bot] marked this conversation as resolved.
const expoPkg = JSON.parse(fs.readFileSync(expoPkgJson, 'utf8'));
const expoCli = path.resolve(path.dirname(expoPkgJson), expoPkg.bin.expo);
const result = spawnSync(process.execPath, [expoCli, 'config', '--json'], { encoding: 'utf8' });
if (result.error || result.status !== 0) {
throw result.error || new Error(`expo config exited with status ${result.status}`);
}
Expand Down
Loading