From 34e421ad671e7d653f04b06ba7249071367658dc Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Thu, 21 May 2026 10:31:29 +0200 Subject: [PATCH 1/6] feat(core): Add `disableAutoUpload` option to Expo plugin Closes #3552 Co-Authored-By: Claude Opus 4.6 --- packages/core/plugin/src/withSentry.ts | 13 +- packages/core/plugin/src/withSentryAndroid.ts | 24 +++- packages/core/plugin/src/withSentryIOS.ts | 46 +++++-- .../expo-plugin/modifyAppBuildGradle.test.ts | 28 ++++ .../expo-plugin/modifyXcodeProject.test.ts | 125 +++++++++++++++++- 5 files changed, 216 insertions(+), 20 deletions(-) diff --git a/packages/core/plugin/src/withSentry.ts b/packages/core/plugin/src/withSentry.ts index 67d8af3579..9db2b5352c 100644 --- a/packages/core/plugin/src/withSentry.ts +++ b/packages/core/plugin/src/withSentry.ts @@ -18,6 +18,7 @@ interface PluginProps { authToken?: string; url?: string; useNativeInit?: boolean; + disableAutoUpload?: boolean; options?: Record; experimental_android?: SentryAndroidGradlePluginOptions; } @@ -67,7 +68,11 @@ const withSentryPlugin: ConfigPlugin = (config, props) => { } if (sentryProperties !== null) { try { - cfg = withSentryAndroid(cfg, { sentryProperties, useNativeInit: props?.useNativeInit }); + cfg = withSentryAndroid(cfg, { + sentryProperties, + useNativeInit: props?.useNativeInit, + disableAutoUpload: props?.disableAutoUpload, + }); } catch (e) { warnOnce(`There was a problem with configuring your native Android project: ${e}`); } @@ -80,7 +85,11 @@ const withSentryPlugin: ConfigPlugin = (config, props) => { } } try { - cfg = withSentryIOS(cfg, { sentryProperties, useNativeInit: props?.useNativeInit }); + cfg = withSentryIOS(cfg, { + sentryProperties, + useNativeInit: props?.useNativeInit, + disableAutoUpload: props?.disableAutoUpload, + }); } catch (e) { warnOnce(`There was a problem with configuring your native iOS project: ${e}`); } diff --git a/packages/core/plugin/src/withSentryAndroid.ts b/packages/core/plugin/src/withSentryAndroid.ts index a9276d4c3c..a4d1b49f0a 100644 --- a/packages/core/plugin/src/withSentryAndroid.ts +++ b/packages/core/plugin/src/withSentryAndroid.ts @@ -7,13 +7,14 @@ import * as path from 'path'; import { warnOnce } from './logger'; import { writeSentryPropertiesTo } from './utils'; -export const withSentryAndroid: ConfigPlugin<{ sentryProperties: string; useNativeInit: boolean | undefined }> = ( - config, - { sentryProperties, useNativeInit = false }, -) => { +export const withSentryAndroid: ConfigPlugin<{ + sentryProperties: string; + useNativeInit: boolean | undefined; + disableAutoUpload: boolean | undefined; +}> = (config, { sentryProperties, useNativeInit = false, disableAutoUpload = false }) => { const appBuildGradleCfg = withAppBuildGradle(config, config => { if (config.modResults.language === 'groovy') { - config.modResults.contents = modifyAppBuildGradle(config.modResults.contents); + config.modResults.contents = modifyAppBuildGradle(config.modResults.contents, disableAutoUpload); } else { throw new Error('Cannot configure Sentry in the app gradle because the build.gradle is not groovy'); } @@ -39,8 +40,14 @@ const resolveSentryReactNativePackageJsonPath = * Writes to projectDirectory/android/app/build.gradle, * adding the relevant @sentry/react-native script. */ -export function modifyAppBuildGradle(buildGradle: string): string { +export function modifyAppBuildGradle(buildGradle: string, disableAutoUpload: boolean = false): string { if (buildGradle.includes('sentry.gradle')) { + if (disableAutoUpload && !buildGradle.includes('shouldSentryAutoUploadGeneral')) { + return buildGradle.replace( + /^(apply from:.*sentry\.gradle.*)$/m, + `$1\nproject.ext.shouldSentryAutoUploadGeneral = { -> return false }`, + ); + } return buildGradle; } @@ -56,8 +63,11 @@ export function modifyAppBuildGradle(buildGradle: string): string { } const applyFrom = `apply from: new File(${resolveSentryReactNativePackageJsonPath}, "sentry.gradle")`; + const disableUploadOverride = disableAutoUpload + ? `\nproject.ext.shouldSentryAutoUploadGeneral = { -> return false }` + : ''; - return buildGradle.replace(pattern, match => `${applyFrom}\n\n${match}`); + return buildGradle.replace(pattern, match => `${applyFrom}${disableUploadOverride}\n\n${match}`); } export function modifyMainApplication(config: ExpoConfig): ExpoConfig { diff --git a/packages/core/plugin/src/withSentryIOS.ts b/packages/core/plugin/src/withSentryIOS.ts index 3cd6a2aa60..cb96883bc1 100644 --- a/packages/core/plugin/src/withSentryIOS.ts +++ b/packages/core/plugin/src/withSentryIOS.ts @@ -14,11 +14,13 @@ const SENTRY_REACT_NATIVE_XCODE_PATH = "`\"$NODE_BINARY\" --print \"require('path').dirname(require.resolve('@sentry/react-native/package.json')) + '/scripts/sentry-xcode.sh'\"`"; const SENTRY_REACT_NATIVE_XCODE_DEBUG_FILES_PATH = "`${NODE_BINARY:-node} --print \"require('path').dirname(require.resolve('@sentry/react-native/package.json')) + '/scripts/sentry-xcode-debug-files.sh'\"`"; +const SENTRY_DISABLE_AUTO_UPLOAD_EXPORT = 'export SENTRY_DISABLE_AUTO_UPLOAD=true'; -export const withSentryIOS: ConfigPlugin<{ sentryProperties: string; useNativeInit: boolean | undefined }> = ( - config, - { sentryProperties, useNativeInit = false }, -) => { +export const withSentryIOS: ConfigPlugin<{ + sentryProperties: string; + useNativeInit: boolean | undefined; + disableAutoUpload: boolean | undefined; +}> = (config, { sentryProperties, useNativeInit = false, disableAutoUpload = false }) => { const xcodeProjectCfg = withXcodeProject(config, config => { const xcodeProject: XcodeProject = config.modResults; @@ -27,17 +29,22 @@ export const withSentryIOS: ConfigPlugin<{ sentryProperties: string; useNativeIn 'PBXShellScriptBuildPhase', ); if (!sentryBuildPhase) { + const debugFilesScript = disableAutoUpload + ? `${SENTRY_DISABLE_AUTO_UPLOAD_EXPORT}\n/bin/sh ${SENTRY_REACT_NATIVE_XCODE_DEBUG_FILES_PATH}` + : `/bin/sh ${SENTRY_REACT_NATIVE_XCODE_DEBUG_FILES_PATH}`; xcodeProject.addBuildPhase([], 'PBXShellScriptBuildPhase', 'Upload Debug Symbols to Sentry', null, { shellPath: '/bin/sh', - shellScript: `/bin/sh ${SENTRY_REACT_NATIVE_XCODE_DEBUG_FILES_PATH}`, + shellScript: debugFilesScript, }); + } else if (disableAutoUpload) { + addDisableAutoUploadToExistingScript(sentryBuildPhase); } const bundleReactNativePhase = xcodeProject.pbxItemByComment( 'Bundle React Native code and images', 'PBXShellScriptBuildPhase', ); - modifyExistingXcodeBuildScript(bundleReactNativePhase); + modifyExistingXcodeBuildScript(bundleReactNativePhase, disableAutoUpload); return config; }); @@ -53,7 +60,7 @@ export const withSentryIOS: ConfigPlugin<{ sentryProperties: string; useNativeIn ]); }; -export function modifyExistingXcodeBuildScript(script: BuildPhase): void { +export function modifyExistingXcodeBuildScript(script: BuildPhase, disableAutoUpload: boolean = false): void { if (!script.shellScript.match(/(packager|scripts)\/react-native-xcode\.sh\b/)) { warnOnce( `'react-native-xcode.sh' not found in 'Bundle React Native code and images'. @@ -63,6 +70,9 @@ Please open a bug report at https://github.com/getsentry/sentry-react-native`, } if (script.shellScript.includes('sentry-xcode.sh')) { + if (disableAutoUpload) { + addDisableAutoUploadToExistingScript(script); + } warnOnce("The latest 'sentry-xcode.sh' script already exists in 'Bundle React Native code and images'."); return; } @@ -77,16 +87,32 @@ Run npx expo prebuild --clean`, } const code = JSON.parse(script.shellScript); - script.shellScript = JSON.stringify(addSentryWithBundledScriptsToBundleShellScript(code)); + script.shellScript = JSON.stringify(addSentryWithBundledScriptsToBundleShellScript(code, disableAutoUpload)); } -export function addSentryWithBundledScriptsToBundleShellScript(script: string): string { +export function addSentryWithBundledScriptsToBundleShellScript( + script: string, + disableAutoUpload: boolean = false, +): string { + const disableAutoUploadExport = disableAutoUpload ? `${SENTRY_DISABLE_AUTO_UPLOAD_EXPORT}\n` : ''; return script.replace( /^.*?(packager|scripts)\/react-native-xcode\.sh\s*(\\'\\\\")?/m, - (match: string) => `/bin/sh ${SENTRY_REACT_NATIVE_XCODE_PATH} ${match}`, + (match: string) => `${disableAutoUploadExport}/bin/sh ${SENTRY_REACT_NATIVE_XCODE_PATH} ${match}`, ); } +export function addDisableAutoUploadToExistingScript(script: BuildPhase): void { + if (script.shellScript.includes('SENTRY_DISABLE_AUTO_UPLOAD')) { + return; + } + try { + const code = JSON.parse(script.shellScript); + script.shellScript = JSON.stringify(`${SENTRY_DISABLE_AUTO_UPLOAD_EXPORT}\n${code}`); + } catch { + script.shellScript = `${SENTRY_DISABLE_AUTO_UPLOAD_EXPORT}\n${script.shellScript}`; + } +} + export function modifyAppDelegate(config: ExpoConfig): ExpoConfig { return withAppDelegate(config, async config => { if (!config.modResults?.path) { diff --git a/packages/core/test/expo-plugin/modifyAppBuildGradle.test.ts b/packages/core/test/expo-plugin/modifyAppBuildGradle.test.ts index 0dcc9b33d6..72d1681467 100644 --- a/packages/core/test/expo-plugin/modifyAppBuildGradle.test.ts +++ b/packages/core/test/expo-plugin/modifyAppBuildGradle.test.ts @@ -51,4 +51,32 @@ describe('Configures Android native project correctly', () => { modifyAppBuildGradle(buildGradleWithOutReactGradleScript); expect(warnOnce).toHaveBeenCalled(); }); + + it('Adds shouldSentryAutoUploadGeneral override when disableAutoUpload is true', () => { + const result = modifyAppBuildGradle(buildGradleWithOutSentry, true); + expect(result).toContain('project.ext.shouldSentryAutoUploadGeneral = { -> return false }'); + expect(result).toContain('sentry.gradle'); + }); + + it('Does not add shouldSentryAutoUploadGeneral override when disableAutoUpload is false', () => { + const result = modifyAppBuildGradle(buildGradleWithOutSentry, false); + expect(result).not.toContain('shouldSentryAutoUploadGeneral'); + }); + + it('Adds override to already-configured build.gradle on re-prebuild', () => { + const result = modifyAppBuildGradle(buildGradleWithSentry, true); + expect(result).toContain('project.ext.shouldSentryAutoUploadGeneral = { -> return false }'); + }); + + it('Does not duplicate override if already present', () => { + const gradleWithOverride = ` +apply from: new File(["node", "--print", "require('path').dirname(require.resolve('@sentry/react-native/package.json'))"].execute().text.trim(), "sentry.gradle") +project.ext.shouldSentryAutoUploadGeneral = { -> return false } + +android { +} +`; + const result = modifyAppBuildGradle(gradleWithOverride, true); + expect(result).toBe(gradleWithOverride); + }); }); diff --git a/packages/core/test/expo-plugin/modifyXcodeProject.test.ts b/packages/core/test/expo-plugin/modifyXcodeProject.test.ts index 1cdf1760c8..db85c525f1 100644 --- a/packages/core/test/expo-plugin/modifyXcodeProject.test.ts +++ b/packages/core/test/expo-plugin/modifyXcodeProject.test.ts @@ -1,5 +1,9 @@ import { warnOnce } from '../../plugin/src/logger'; -import { modifyExistingXcodeBuildScript } from '../../plugin/src/withSentryIOS'; +import { + addDisableAutoUploadToExistingScript, + addSentryWithBundledScriptsToBundleShellScript, + modifyExistingXcodeBuildScript, +} from '../../plugin/src/withSentryIOS'; jest.mock('../../plugin/src/logger'); @@ -77,6 +81,125 @@ describe('Configures iOS native project correctly', () => { }); }); +describe('disableAutoUpload option for Bundle React Native code phase', () => { + let consoleWarnMock: jest.SpyInstance; + + beforeEach(() => { + consoleWarnMock = jest.spyOn(console, 'warn').mockImplementation(); + }); + + afterEach(() => { + consoleWarnMock.mockRestore(); + }); + + it('Prepends export on its own line when disableAutoUpload is true', () => { + const script = Object.assign({}, buildScriptWithoutSentry); + modifyExistingXcodeBuildScript(script, true); + const parsed = JSON.parse(script.shellScript); + expect(parsed).toMatch(/^export SENTRY_DISABLE_AUTO_UPLOAD=true\n/m); + expect(parsed).toContain('sentry-xcode.sh'); + }); + + it('Does not prepend export when disableAutoUpload is false', () => { + const script = Object.assign({}, buildScriptWithoutSentry); + modifyExistingXcodeBuildScript(script, false); + const parsed = JSON.parse(script.shellScript); + expect(parsed).not.toContain('SENTRY_DISABLE_AUTO_UPLOAD'); + }); + + it('Injects export into already-configured bundle script on re-prebuild', () => { + const script = Object.assign({}, buildScriptWithSentry); + modifyExistingXcodeBuildScript(script, true); + const parsed = JSON.parse(script.shellScript); + expect(parsed).toMatch(/^export SENTRY_DISABLE_AUTO_UPLOAD=true\n/); + expect(parsed).toContain('sentry-xcode.sh'); + }); + + it('Does not duplicate export if already present in bundle script', () => { + const scriptWithExport = { + shellScript: JSON.stringify(`export SENTRY_DISABLE_AUTO_UPLOAD=true +" +export NODE_BINARY=node +/bin/sh \`"$NODE_BINARY" --print "require('path').dirname(require.resolve('@sentry/react-native/package.json')) + '/scripts/sentry-xcode.sh'"\` ../node_modules/react-native/scripts/react-native-xcode.sh +"`), + }; + const before = scriptWithExport.shellScript; + modifyExistingXcodeBuildScript(scriptWithExport, true); + expect(scriptWithExport.shellScript).toBe(before); + }); + + it('Does not modify already-configured script when disableAutoUpload is false', () => { + const script = Object.assign({}, buildScriptWithSentry); + const before = script.shellScript; + modifyExistingXcodeBuildScript(script, false); + expect(script.shellScript).toBe(before); + }); + + it('addSentryWithBundledScriptsToBundleShellScript uses real newline, not literal backslash-n', () => { + const input = `" +export NODE_BINARY=node +../node_modules/react-native/scripts/react-native-xcode.sh +"`; + const result = addSentryWithBundledScriptsToBundleShellScript(input, true); + expect(result).toContain('export SENTRY_DISABLE_AUTO_UPLOAD=true\n/bin/sh'); + expect(result).not.toContain('true\\n'); + }); + + it('Produces a valid shell script after JSON round-trip', () => { + const script = Object.assign({}, buildScriptWithoutSentry); + modifyExistingXcodeBuildScript(script, true); + const parsed = JSON.parse(script.shellScript); + const lines = parsed.split('\n'); + expect(lines).toContainEqual('export SENTRY_DISABLE_AUTO_UPLOAD=true'); + }); +}); + +describe('addDisableAutoUploadToExistingScript', () => { + const debugFilesShellScript = + "/bin/sh `${NODE_BINARY:-node} --print \"require('path').dirname(require.resolve('@sentry/react-native/package.json')) + '/scripts/sentry-xcode-debug-files.sh'\"`"; + + it('injects export into JSON-encoded shellScript', () => { + const script = { shellScript: JSON.stringify(debugFilesShellScript) }; + addDisableAutoUploadToExistingScript(script); + const parsed = JSON.parse(script.shellScript); + expect(parsed).toMatch(/^export SENTRY_DISABLE_AUTO_UPLOAD=true\n/); + expect(parsed).toContain('sentry-xcode-debug-files.sh'); + }); + + it('injects export into non-JSON-encoded shellScript via fallback', () => { + const script = { shellScript: debugFilesShellScript }; + addDisableAutoUploadToExistingScript(script); + expect(script.shellScript).toMatch(/^export SENTRY_DISABLE_AUTO_UPLOAD=true\n/); + expect(script.shellScript).toContain('sentry-xcode-debug-files.sh'); + }); + + it('does not duplicate export if already present in JSON-encoded script', () => { + const script = { + shellScript: JSON.stringify(`export SENTRY_DISABLE_AUTO_UPLOAD=true\n${debugFilesShellScript}`), + }; + const before = script.shellScript; + addDisableAutoUploadToExistingScript(script); + expect(script.shellScript).toBe(before); + }); + + it('does not duplicate export if already present in raw script', () => { + const script = { + shellScript: `export SENTRY_DISABLE_AUTO_UPLOAD=true\n${debugFilesShellScript}`, + }; + const before = script.shellScript; + addDisableAutoUploadToExistingScript(script); + expect(script.shellScript).toBe(before); + }); + + it('works on the bundle phase script format (JSON-encoded with inner quotes)', () => { + const script = Object.assign({}, buildScriptWithSentry); + addDisableAutoUploadToExistingScript(script); + const parsed = JSON.parse(script.shellScript); + expect(parsed).toMatch(/^export SENTRY_DISABLE_AUTO_UPLOAD=true\n/); + expect(parsed).toContain('sentry-xcode.sh'); + }); +}); + describe('Upload Debug Symbols to Sentry build phase', () => { let mockXcodeProject: any; let addBuildPhaseSpy: jest.Mock; From e359bd2ae3ab0fa687065e1d38b0db004226771e Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Thu, 21 May 2026 10:34:45 +0200 Subject: [PATCH 2/6] docs: Add changelog entry for disableAutoUpload Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 69e405d923..7174253c2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ - Expose `addConsoleInstrumentationFilter` from `@sentry/core` ([#6180](https://github.com/getsentry/sentry-react-native/pull/6180)) - Expose experimental `captureSurfaceViews` option for Android Session Replay ([#6175](https://github.com/getsentry/sentry-react-native/pull/6175)) - Add OTA SDK version to native `sdk.packages` when JS bundle version differs from built-in version ([#6191](https://github.com/getsentry/sentry-react-native/pull/6191)) +- Add `disableAutoUpload` option to Expo plugin to disable source map and debug symbol uploads ([#6195](https://github.com/getsentry/sentry-react-native/pull/6195)) ### Fixes From 00340cca9ed94d25a565e846f557e12ac80ce4c0 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Thu, 21 May 2026 11:40:34 +0200 Subject: [PATCH 3/6] Fix changelog --- CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65f7af7d11..f17b66f505 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ > make sure you follow our [migration guide](https://docs.sentry.io/platforms/react-native/migration/) first. +## Unreleased + +### Features + +- Add `disableAutoUpload` option to Expo plugin to disable source map and debug symbol uploads ([#6195](https://github.com/getsentry/sentry-react-native/pull/6195)) + ## 8.12.0 ### Features @@ -21,7 +27,6 @@ - Expose `addConsoleInstrumentationFilter` from `@sentry/core` ([#6180](https://github.com/getsentry/sentry-react-native/pull/6180)) - Expose experimental `captureSurfaceViews` option for Android Session Replay ([#6175](https://github.com/getsentry/sentry-react-native/pull/6175)) - Add OTA SDK version to native `sdk.packages` when JS bundle version differs from built-in version ([#6191](https://github.com/getsentry/sentry-react-native/pull/6191)) -- Add `disableAutoUpload` option to Expo plugin to disable source map and debug symbol uploads ([#6195](https://github.com/getsentry/sentry-react-native/pull/6195)) ### Fixes From fd2f50fb5ee2033fa8b038b84fb751249789c75c Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Thu, 21 May 2026 12:08:07 +0200 Subject: [PATCH 4/6] fix(core): Place disableAutoUpload export inside pbxproj script delimiters On re-prebuild, addDisableAutoUploadToExistingScript was prepending the export before the " delimiter that wraps shell script content in pbxproj files. This places it inside the delimiter, consistent with the fresh-prebuild path. Co-Authored-By: Claude Opus 4.6 --- packages/core/plugin/src/withSentryIOS.ts | 10 ++++++- .../expo-plugin/modifyXcodeProject.test.ts | 29 +++++++++++++++---- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/packages/core/plugin/src/withSentryIOS.ts b/packages/core/plugin/src/withSentryIOS.ts index cb96883bc1..260a6b973f 100644 --- a/packages/core/plugin/src/withSentryIOS.ts +++ b/packages/core/plugin/src/withSentryIOS.ts @@ -107,12 +107,20 @@ export function addDisableAutoUploadToExistingScript(script: BuildPhase): void { } try { const code = JSON.parse(script.shellScript); - script.shellScript = JSON.stringify(`${SENTRY_DISABLE_AUTO_UPLOAD_EXPORT}\n${code}`); + script.shellScript = JSON.stringify(insertExportAfterDelimiter(code)); } catch { script.shellScript = `${SENTRY_DISABLE_AUTO_UPLOAD_EXPORT}\n${script.shellScript}`; } } +function insertExportAfterDelimiter(script: string): string { + if (script.startsWith('"')) { + const rest = script.slice(1).replace(/^\n/, ''); + return `"\n${SENTRY_DISABLE_AUTO_UPLOAD_EXPORT}\n${rest}`; + } + return `${SENTRY_DISABLE_AUTO_UPLOAD_EXPORT}\n${script}`; +} + export function modifyAppDelegate(config: ExpoConfig): ExpoConfig { return withAppDelegate(config, async config => { if (!config.modResults?.path) { diff --git a/packages/core/test/expo-plugin/modifyXcodeProject.test.ts b/packages/core/test/expo-plugin/modifyXcodeProject.test.ts index db85c525f1..f03de72ed0 100644 --- a/packages/core/test/expo-plugin/modifyXcodeProject.test.ts +++ b/packages/core/test/expo-plugin/modifyXcodeProject.test.ts @@ -96,8 +96,10 @@ describe('disableAutoUpload option for Bundle React Native code phase', () => { const script = Object.assign({}, buildScriptWithoutSentry); modifyExistingXcodeBuildScript(script, true); const parsed = JSON.parse(script.shellScript); - expect(parsed).toMatch(/^export SENTRY_DISABLE_AUTO_UPLOAD=true\n/m); + expect(parsed).toContain('export SENTRY_DISABLE_AUTO_UPLOAD=true'); expect(parsed).toContain('sentry-xcode.sh'); + expect(parsed).toMatch(/^"/); + expect(parsed).toMatch(/"$/); }); it('Does not prepend export when disableAutoUpload is false', () => { @@ -111,14 +113,14 @@ describe('disableAutoUpload option for Bundle React Native code phase', () => { const script = Object.assign({}, buildScriptWithSentry); modifyExistingXcodeBuildScript(script, true); const parsed = JSON.parse(script.shellScript); - expect(parsed).toMatch(/^export SENTRY_DISABLE_AUTO_UPLOAD=true\n/); + expect(parsed).toMatch(/^"\nexport SENTRY_DISABLE_AUTO_UPLOAD=true\n/); expect(parsed).toContain('sentry-xcode.sh'); }); it('Does not duplicate export if already present in bundle script', () => { const scriptWithExport = { - shellScript: JSON.stringify(`export SENTRY_DISABLE_AUTO_UPLOAD=true -" + shellScript: JSON.stringify(`" +export SENTRY_DISABLE_AUTO_UPLOAD=true export NODE_BINARY=node /bin/sh \`"$NODE_BINARY" --print "require('path').dirname(require.resolve('@sentry/react-native/package.json')) + '/scripts/sentry-xcode.sh'"\` ../node_modules/react-native/scripts/react-native-xcode.sh "`), @@ -152,6 +154,23 @@ export NODE_BINARY=node const lines = parsed.split('\n'); expect(lines).toContainEqual('export SENTRY_DISABLE_AUTO_UPLOAD=true'); }); + + it('Fresh-prebuild and re-prebuild both place export inside delimiters', () => { + const freshScript = Object.assign({}, buildScriptWithoutSentry); + modifyExistingXcodeBuildScript(freshScript, true); + const freshParsed = JSON.parse(freshScript.shellScript); + + const rePrebuildScript = Object.assign({}, buildScriptWithSentry); + modifyExistingXcodeBuildScript(rePrebuildScript, true); + const rePrebuildParsed = JSON.parse(rePrebuildScript.shellScript); + + for (const parsed of [freshParsed, rePrebuildParsed]) { + expect(parsed).toMatch(/^"/); + expect(parsed).toMatch(/"$/); + expect(parsed).toContain('export SENTRY_DISABLE_AUTO_UPLOAD=true'); + expect(parsed).toContain('sentry-xcode.sh'); + } + }); }); describe('addDisableAutoUploadToExistingScript', () => { @@ -195,7 +214,7 @@ describe('addDisableAutoUploadToExistingScript', () => { const script = Object.assign({}, buildScriptWithSentry); addDisableAutoUploadToExistingScript(script); const parsed = JSON.parse(script.shellScript); - expect(parsed).toMatch(/^export SENTRY_DISABLE_AUTO_UPLOAD=true\n/); + expect(parsed).toMatch(/^"\nexport SENTRY_DISABLE_AUTO_UPLOAD=true\n/); expect(parsed).toContain('sentry-xcode.sh'); }); }); From eaa6ae27177189bff92e0e2a9c859fc3aeb09c65 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Thu, 21 May 2026 14:52:00 +0200 Subject: [PATCH 5/6] fix(core): Remove disableAutoUpload injection when toggling back to false When disableAutoUpload was set to true and then changed back to false, the injected export/override was left in place on re-prebuild, silently keeping uploads disabled. Now both iOS and Android remove the injection when disableAutoUpload is false. Also removes the misleading "script already exists" warning that fired after successfully injecting the auto-upload export on iOS. Co-Authored-By: Claude Opus 4.6 --- packages/core/plugin/src/withSentryAndroid.ts | 6 ++ packages/core/plugin/src/withSentryIOS.ts | 17 ++++- .../expo-plugin/modifyAppBuildGradle.test.ts | 19 ++++++ .../expo-plugin/modifyXcodeProject.test.ts | 64 +++++++++++++++++++ 4 files changed, 105 insertions(+), 1 deletion(-) diff --git a/packages/core/plugin/src/withSentryAndroid.ts b/packages/core/plugin/src/withSentryAndroid.ts index a4d1b49f0a..246b16ae50 100644 --- a/packages/core/plugin/src/withSentryAndroid.ts +++ b/packages/core/plugin/src/withSentryAndroid.ts @@ -48,6 +48,12 @@ export function modifyAppBuildGradle(buildGradle: string, disableAutoUpload: boo `$1\nproject.ext.shouldSentryAutoUploadGeneral = { -> return false }`, ); } + if (!disableAutoUpload && buildGradle.includes('shouldSentryAutoUploadGeneral')) { + return buildGradle.replace( + /\nproject\.ext\.shouldSentryAutoUploadGeneral = \{ -> return false \}\n?/, + '\n', + ); + } return buildGradle; } diff --git a/packages/core/plugin/src/withSentryIOS.ts b/packages/core/plugin/src/withSentryIOS.ts index 260a6b973f..6acb2b135c 100644 --- a/packages/core/plugin/src/withSentryIOS.ts +++ b/packages/core/plugin/src/withSentryIOS.ts @@ -38,6 +38,8 @@ export const withSentryIOS: ConfigPlugin<{ }); } else if (disableAutoUpload) { addDisableAutoUploadToExistingScript(sentryBuildPhase); + } else { + removeDisableAutoUploadFromExistingScript(sentryBuildPhase); } const bundleReactNativePhase = xcodeProject.pbxItemByComment( @@ -72,8 +74,9 @@ Please open a bug report at https://github.com/getsentry/sentry-react-native`, if (script.shellScript.includes('sentry-xcode.sh')) { if (disableAutoUpload) { addDisableAutoUploadToExistingScript(script); + } else { + removeDisableAutoUploadFromExistingScript(script); } - warnOnce("The latest 'sentry-xcode.sh' script already exists in 'Bundle React Native code and images'."); return; } @@ -121,6 +124,18 @@ function insertExportAfterDelimiter(script: string): string { return `${SENTRY_DISABLE_AUTO_UPLOAD_EXPORT}\n${script}`; } +export function removeDisableAutoUploadFromExistingScript(script: BuildPhase): void { + if (!script.shellScript.includes('SENTRY_DISABLE_AUTO_UPLOAD')) { + return; + } + try { + const code = JSON.parse(script.shellScript); + script.shellScript = JSON.stringify(code.replace(/^export SENTRY_DISABLE_AUTO_UPLOAD=true\n?/m, '')); + } catch { + script.shellScript = script.shellScript.replace(/^export SENTRY_DISABLE_AUTO_UPLOAD=true\n?/m, ''); + } +} + export function modifyAppDelegate(config: ExpoConfig): ExpoConfig { return withAppDelegate(config, async config => { if (!config.modResults?.path) { diff --git a/packages/core/test/expo-plugin/modifyAppBuildGradle.test.ts b/packages/core/test/expo-plugin/modifyAppBuildGradle.test.ts index 72d1681467..a7d24c159a 100644 --- a/packages/core/test/expo-plugin/modifyAppBuildGradle.test.ts +++ b/packages/core/test/expo-plugin/modifyAppBuildGradle.test.ts @@ -79,4 +79,23 @@ android { const result = modifyAppBuildGradle(gradleWithOverride, true); expect(result).toBe(gradleWithOverride); }); + + it('Removes override when toggling disableAutoUpload back to false', () => { + const gradleWithOverride = ` +apply from: new File(["node", "--print", "require('path').dirname(require.resolve('@sentry/react-native/package.json'))"].execute().text.trim(), "sentry.gradle") +project.ext.shouldSentryAutoUploadGeneral = { -> return false } + +android { +} +`; + const result = modifyAppBuildGradle(gradleWithOverride, false); + expect(result).not.toContain('shouldSentryAutoUploadGeneral'); + expect(result).toContain('sentry.gradle'); + expect(result).toContain('android {'); + }); + + it('No-ops when toggling to false and override is not present', () => { + const result = modifyAppBuildGradle(buildGradleWithSentry, false); + expect(result).toBe(buildGradleWithSentry); + }); }); diff --git a/packages/core/test/expo-plugin/modifyXcodeProject.test.ts b/packages/core/test/expo-plugin/modifyXcodeProject.test.ts index f03de72ed0..59e095e2e8 100644 --- a/packages/core/test/expo-plugin/modifyXcodeProject.test.ts +++ b/packages/core/test/expo-plugin/modifyXcodeProject.test.ts @@ -3,6 +3,7 @@ import { addDisableAutoUploadToExistingScript, addSentryWithBundledScriptsToBundleShellScript, modifyExistingXcodeBuildScript, + removeDisableAutoUploadFromExistingScript, } from '../../plugin/src/withSentryIOS'; jest.mock('../../plugin/src/logger'); @@ -219,6 +220,69 @@ describe('addDisableAutoUploadToExistingScript', () => { }); }); +describe('removeDisableAutoUploadFromExistingScript', () => { + const debugFilesShellScript = + "/bin/sh `${NODE_BINARY:-node} --print \"require('path').dirname(require.resolve('@sentry/react-native/package.json')) + '/scripts/sentry-xcode-debug-files.sh'\"`"; + + it('removes export from JSON-encoded shellScript', () => { + const script = { + shellScript: JSON.stringify(`export SENTRY_DISABLE_AUTO_UPLOAD=true\n${debugFilesShellScript}`), + }; + removeDisableAutoUploadFromExistingScript(script); + const parsed = JSON.parse(script.shellScript); + expect(parsed).not.toContain('SENTRY_DISABLE_AUTO_UPLOAD'); + expect(parsed).toContain('sentry-xcode-debug-files.sh'); + }); + + it('removes export from non-JSON-encoded shellScript', () => { + const script = { + shellScript: `export SENTRY_DISABLE_AUTO_UPLOAD=true\n${debugFilesShellScript}`, + }; + removeDisableAutoUploadFromExistingScript(script); + expect(script.shellScript).not.toContain('SENTRY_DISABLE_AUTO_UPLOAD'); + expect(script.shellScript).toContain('sentry-xcode-debug-files.sh'); + }); + + it('removes export from bundle phase script (JSON-encoded with inner quotes)', () => { + const script = Object.assign({}, buildScriptWithSentry); + addDisableAutoUploadToExistingScript(script); + expect(script.shellScript).toContain('SENTRY_DISABLE_AUTO_UPLOAD'); + removeDisableAutoUploadFromExistingScript(script); + const parsed = JSON.parse(script.shellScript); + expect(parsed).not.toContain('SENTRY_DISABLE_AUTO_UPLOAD'); + expect(parsed).toContain('sentry-xcode.sh'); + }); + + it('no-ops when export is not present', () => { + const script = { shellScript: JSON.stringify(debugFilesShellScript) }; + const before = script.shellScript; + removeDisableAutoUploadFromExistingScript(script); + expect(script.shellScript).toBe(before); + }); +}); + +describe('disableAutoUpload toggle: re-prebuild with false removes prior injection', () => { + let consoleWarnMock: jest.SpyInstance; + + beforeEach(() => { + consoleWarnMock = jest.spyOn(console, 'warn').mockImplementation(); + }); + + afterEach(() => { + consoleWarnMock.mockRestore(); + }); + + it('removes export from bundle script when toggling disableAutoUpload back to false', () => { + const script = Object.assign({}, buildScriptWithSentry); + modifyExistingXcodeBuildScript(script, true); + expect(JSON.parse(script.shellScript)).toContain('SENTRY_DISABLE_AUTO_UPLOAD'); + + modifyExistingXcodeBuildScript(script, false); + expect(JSON.parse(script.shellScript)).not.toContain('SENTRY_DISABLE_AUTO_UPLOAD'); + expect(JSON.parse(script.shellScript)).toContain('sentry-xcode.sh'); + }); +}); + describe('Upload Debug Symbols to Sentry build phase', () => { let mockXcodeProject: any; let addBuildPhaseSpy: jest.Mock; From 17fd04fb8e83af7f0c634e0bf0f9af379355c62a Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Thu, 21 May 2026 16:31:43 +0200 Subject: [PATCH 6/6] style(core): Fix formatting in withSentryAndroid Co-Authored-By: Claude Opus 4.6 --- packages/core/plugin/src/withSentryAndroid.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/core/plugin/src/withSentryAndroid.ts b/packages/core/plugin/src/withSentryAndroid.ts index 246b16ae50..b5dfc6f949 100644 --- a/packages/core/plugin/src/withSentryAndroid.ts +++ b/packages/core/plugin/src/withSentryAndroid.ts @@ -49,10 +49,7 @@ export function modifyAppBuildGradle(buildGradle: string, disableAutoUpload: boo ); } if (!disableAutoUpload && buildGradle.includes('shouldSentryAutoUploadGeneral')) { - return buildGradle.replace( - /\nproject\.ext\.shouldSentryAutoUploadGeneral = \{ -> return false \}\n?/, - '\n', - ); + return buildGradle.replace(/\nproject\.ext\.shouldSentryAutoUploadGeneral = \{ -> return false \}\n?/, '\n'); } return buildGradle; }