diff --git a/CHANGELOG.md b/CHANGELOG.md index 253a9111..2fce80de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,12 @@ ## Unreleased +- feat(reactnative): Use Xcode scripts bundled with Sentry RN SDK (#499) - feat(reactnative): Make `pod install` step optional (#501) +- feat(remix): Add Vite support (#495) ## 3.16.5 -- feat(remix): Add Vite support (#495) - fix(wizard): Update wizard API data type and issue stream url creation (#500) ## 3.16.4 diff --git a/src/react-native/react-native-wizard.ts b/src/react-native/react-native-wizard.ts index 476f1438..b0b4d170 100644 --- a/src/react-native/react-native-wizard.ts +++ b/src/react-native/react-native-wizard.ts @@ -27,8 +27,11 @@ import { findBundlePhase, patchBundlePhase, findDebugFilesUploadPhase, - addDebugFilesUploadPhase, + addDebugFilesUploadPhaseWithCli, writeXcodeProject, + addSentryWithCliToBundleShellScript, + addSentryWithBundledScriptsToBundleShellScript, + addDebugFilesUploadPhaseWithBundledScripts, } from './xcode'; import { doesAppBuildGradleIncludeRNSentryGradlePlugin, @@ -45,6 +48,7 @@ import { } from './javascript'; import { traceStep, withTelemetry } from '../telemetry'; import * as Sentry from '@sentry/node'; +import { fulfillsVersionRange } from '../utils/semver'; import { getIssueStreamUrl } from '../utils/url'; // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment @@ -57,6 +61,10 @@ export const RN_HUMAN_NAME = 'React Native'; export const SUPPORTED_RN_RANGE = '>=0.69.0'; +// The following SDK version ship with bundled Xcode scripts +// which simplifies the Xcode Build Phases setup. +export const SDK_XCODE_SCRIPTS_SUPPORTED_SDK_RANGE = '>=5.11.0'; + export type RNCliSetupConfigContent = Pick< Required, 'authToken' | 'org' | 'project' | 'url' @@ -120,6 +128,10 @@ export async function runReactNativeWizardWithTelemetry( packageName: RN_SDK_PACKAGE, alreadyInstalled: hasPackageInstalled(RN_SDK_PACKAGE, packageJson), }); + const sdkVersion = getPackageVersion( + RN_SDK_PACKAGE, + await getPackageDotJson(), + ); await traceStep('patch-js', () => addSentryInit({ dsn: selectedProject.keys[0].dsn.public }), @@ -127,7 +139,9 @@ export async function runReactNativeWizardWithTelemetry( if (fs.existsSync('ios')) { Sentry.setTag('patch-ios', true); - await traceStep('patch-xcode-files', () => patchXcodeFiles(cliConfig)); + await traceStep('patch-xcode-files', () => + patchXcodeFiles(cliConfig, { sdkVersion }), + ); } if (fs.existsSync('android')) { @@ -241,7 +255,12 @@ ${chalk.cyan(issuesStreamUrl)}`); return firstErrorConfirmed; } -async function patchXcodeFiles(config: RNCliSetupConfigContent) { +async function patchXcodeFiles( + config: RNCliSetupConfigContent, + context: { + sdkVersion: string | undefined; + }, +) { await addSentryCliConfig(config, { ...propertiesCliSetupConfig, name: 'source maps and iOS debug files', @@ -288,7 +307,21 @@ async function patchXcodeFiles(config: RNCliSetupConfigContent) { 'xcode-bundle-phase-status', bundlePhase ? 'found' : 'not-found', ); - patchBundlePhase(bundlePhase); + if ( + context.sdkVersion && + fulfillsVersionRange({ + version: context.sdkVersion, + acceptableVersions: SDK_XCODE_SCRIPTS_SUPPORTED_SDK_RANGE, + canBeLatest: true, + }) + ) { + patchBundlePhase( + bundlePhase, + addSentryWithBundledScriptsToBundleShellScript, + ); + } else { + patchBundlePhase(bundlePhase, addSentryWithCliToBundleShellScript); + } Sentry.setTag('xcode-bundle-phase-status', 'patched'); }); @@ -299,7 +332,22 @@ async function patchXcodeFiles(config: RNCliSetupConfigContent) { 'xcode-debug-files-upload-phase-status', debugFilesUploadPhaseExists ? 'already-exists' : undefined, ); - addDebugFilesUploadPhase(xcodeProject, { debugFilesUploadPhaseExists }); + if ( + context.sdkVersion && + fulfillsVersionRange({ + version: context.sdkVersion, + acceptableVersions: SDK_XCODE_SCRIPTS_SUPPORTED_SDK_RANGE, + canBeLatest: true, + }) + ) { + addDebugFilesUploadPhaseWithBundledScripts(xcodeProject, { + debugFilesUploadPhaseExists, + }); + } else { + addDebugFilesUploadPhaseWithCli(xcodeProject, { + debugFilesUploadPhaseExists, + }); + } Sentry.setTag('xcode-debug-files-upload-phase-status', 'added'); }); diff --git a/src/react-native/xcode.ts b/src/react-native/xcode.ts index 3b9c3dbd..662e1dd6 100644 --- a/src/react-native/xcode.ts +++ b/src/react-native/xcode.ts @@ -21,7 +21,10 @@ export function getValidExistingBuildPhases(xcodeProject: any): BuildPhaseMap { return map; } -export function patchBundlePhase(bundlePhase: BuildPhase | undefined) { +export function patchBundlePhase( + bundlePhase: BuildPhase | undefined, + patch: (script: string) => string, +) { if (!bundlePhase) { clack.log.warn( `Could not find ${chalk.cyan( @@ -42,9 +45,7 @@ export function patchBundlePhase(bundlePhase: BuildPhase | undefined) { } const script: string = JSON.parse(bundlePhase.shellScript); - bundlePhase.shellScript = JSON.stringify( - addSentryToBundleShellScript(script), - ); + bundlePhase.shellScript = JSON.stringify(patch(script)); clack.log.success( `Patched Build phase ${chalk.cyan('Bundle React Native code and images')}.`, ); @@ -60,7 +61,10 @@ export function unPatchBundlePhase(bundlePhase: BuildPhase | undefined) { return; } - if (!bundlePhase.shellScript.match(/sentry-cli\s+react-native\s+xcode/i)) { + if ( + !bundlePhase.shellScript.match(/sentry-cli\s+react-native\s+xcode/i) && + !bundlePhase.shellScript.includes('sentry-xcode.sh') + ) { clack.log.success( `Build phase ${chalk.cyan( 'Bundle React Native code and images', @@ -97,6 +101,12 @@ export function removeSentryFromBundleShellScript(script: string): string { /\.\.\/node_modules\/@sentry\/cli\/bin\/sentry-cli\s+react-native\s+xcode\s+\$REACT_NATIVE_XCODE/i, '$REACT_NATIVE_XCODE', ) + .replace( + // eslint-disable-next-line no-useless-escape + /\"\/bin\/sh.*?sentry-xcode.sh\s+\$REACT_NATIVE_XCODE/i, + // eslint-disable-next-line no-useless-escape + '"$REACT_NATIVE_XCODE', + ) ); } @@ -107,10 +117,25 @@ export function findBundlePhase(buildPhases: BuildPhaseMap) { } export function doesBundlePhaseIncludeSentry(buildPhase: BuildPhase) { - return !!buildPhase.shellScript.match(/sentry-cli\s+react-native\s+xcode/i); + const containsSentryCliRNCommand = !!buildPhase.shellScript.match( + /sentry-cli\s+react-native\s+xcode/i, + ); + const containsBundledScript = + buildPhase.shellScript.includes('sentry-xcode.sh'); + return containsSentryCliRNCommand || containsBundledScript; +} + +export function addSentryWithBundledScriptsToBundleShellScript( + script: string, +): string { + return script.replace( + '$REACT_NATIVE_XCODE', + // eslint-disable-next-line no-useless-escape + '\\"/bin/sh ../node_modules/@sentry/react-native/scripts/sentry-xcode.sh $REACT_NATIVE_XCODE\\"', + ); } -export function addSentryToBundleShellScript(script: string): string { +export function addSentryWithCliToBundleShellScript(script: string): string { return ( 'export SENTRY_PROPERTIES=sentry.properties\n' + 'export EXTRA_PACKAGER_ARGS="--sourcemap-output $DERIVED_FILE_DIR/main.jsbundle.map"\n' + @@ -124,7 +149,36 @@ export function addSentryToBundleShellScript(script: string): string { ); } -export function addDebugFilesUploadPhase( +export function addDebugFilesUploadPhaseWithBundledScripts( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + xcodeProject: any, + { debugFilesUploadPhaseExists }: { debugFilesUploadPhaseExists: boolean }, +) { + if (debugFilesUploadPhaseExists) { + clack.log.warn( + `Build phase ${chalk.cyan( + 'Upload Debug Symbols to Sentry', + )} already exists.`, + ); + return; + } + + xcodeProject.addBuildPhase( + [], + 'PBXShellScriptBuildPhase', + 'Upload Debug Symbols to Sentry', + null, + { + shellPath: '/bin/sh', + shellScript: `/bin/sh ../node_modules/@sentry/react-native/scripts/sentry-xcode-debug-files.sh`, + }, + ); + clack.log.success( + `Added Build phase ${chalk.cyan('Upload Debug Symbols to Sentry')}.`, + ); +} + +export function addDebugFilesUploadPhaseWithCli( // eslint-disable-next-line @typescript-eslint/no-explicit-any xcodeProject: any, { debugFilesUploadPhaseExists }: { debugFilesUploadPhaseExists: boolean }, @@ -204,13 +258,17 @@ export function unPatchDebugFilesUploadPhase( export function findDebugFilesUploadPhase( buildPhasesMap: Record, ): [key: string, buildPhase: BuildPhase] | undefined { - return Object.entries(buildPhasesMap).find( - ([_, buildPhase]) => + return Object.entries(buildPhasesMap).find(([_, buildPhase]) => { + const containsCliDebugUpload = typeof buildPhase !== 'string' && !!buildPhase.shellScript.match( /sentry-cli\s+(upload-dsym|debug-files upload)\b/, - ), - ); + ); + const containsBundledDebugUpload = + typeof buildPhase !== 'string' && + buildPhase.shellScript.includes('sentry-xcode-debug-files.sh'); + return containsCliDebugUpload || containsBundledDebugUpload; + }); } // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/test/react-native/xcode.test.ts b/test/react-native/xcode.test.ts index d690a3b1..de6d2639 100644 --- a/test/react-native/xcode.test.ts +++ b/test/react-native/xcode.test.ts @@ -1,6 +1,7 @@ /* eslint-disable no-useless-escape */ import { - addSentryToBundleShellScript, + addSentryWithBundledScriptsToBundleShellScript, + addSentryWithCliToBundleShellScript, doesBundlePhaseIncludeSentry, findBundlePhase, findDebugFilesUploadPhase, @@ -8,7 +9,7 @@ import { } from '../../src/react-native/xcode'; describe('react-native xcode', () => { - describe('addSentryToBundleShellScript', () => { + describe('addSentryWithCliToBundleShellScript', () => { it('adds sentry cli to rn bundle build phase', () => { const input = `set -e @@ -30,7 +31,31 @@ REACT_NATIVE_XCODE="../node_modules/react-native/scripts/react-native-xcode.sh" /bin/sh -c "$WITH_ENVIRONMENT ../node_modules/@sentry/react-native/scripts/collect-modules.sh" `; - expect(addSentryToBundleShellScript(input)).toBe(expectedOutput); + expect(addSentryWithCliToBundleShellScript(input)).toBe(expectedOutput); + }); + }); + + describe('addSentryBundledScriptsToBundleShellScript', () => { + it('adds sentry cli to rn bundle build phase', () => { + const input = `set -e + +WITH_ENVIRONMENT="../node_modules/react-native/scripts/xcode/with-environment.sh" +REACT_NATIVE_XCODE="../node_modules/react-native/scripts/react-native-xcode.sh" + +/bin/sh -c "$WITH_ENVIRONMENT $REACT_NATIVE_XCODE"`; + // actual shell script looks like this: + // /bin/sh -c "$WITH_ENVIRONMENT \"$REACT_NATIVE_XCODE\"" + // but during parsing xcode library removes the quotes + const expectedOutput = `set -e + +WITH_ENVIRONMENT="../node_modules/react-native/scripts/xcode/with-environment.sh" +REACT_NATIVE_XCODE="../node_modules/react-native/scripts/react-native-xcode.sh" + +/bin/sh -c "$WITH_ENVIRONMENT \\"/bin/sh ../node_modules/@sentry/react-native/scripts/sentry-xcode.sh $REACT_NATIVE_XCODE\\""`; + + expect(addSentryWithBundledScriptsToBundleShellScript(input)).toBe( + expectedOutput, + ); }); }); @@ -59,6 +84,23 @@ REACT_NATIVE_XCODE="../node_modules/react-native/scripts/react-native-xcode.sh" expect(removeSentryFromBundleShellScript(input)).toBe(expectedOutput); }); + + it('removes sentry bundled scripts from rn bundle build phase', () => { + const input = `set -e + +WITH_ENVIRONMENT="../node_modules/react-native/scripts/xcode/with-environment.sh" +REACT_NATIVE_XCODE="../node_modules/react-native/scripts/react-native-xcode.sh" + +/bin/sh -c "$WITH_ENVIRONMENT \"/bin/sh ../node_modules/@sentry/react-native/scripts/sentry-xcode.sh $REACT_NATIVE_XCODE\""`; + const expectedOutput = `set -e + +WITH_ENVIRONMENT="../node_modules/react-native/scripts/xcode/with-environment.sh" +REACT_NATIVE_XCODE="../node_modules/react-native/scripts/react-native-xcode.sh" + +/bin/sh -c "$WITH_ENVIRONMENT \"$REACT_NATIVE_XCODE\""`; + + expect(removeSentryFromBundleShellScript(input)).toBe(expectedOutput); + }); }); describe('findBundlePhase', () => { @@ -138,6 +180,19 @@ SENTRY_CLI="sentry-cli react-native xcode" expect(doesBundlePhaseIncludeSentry(input)).toBeTruthy(); }); + it('returns true for script containing sentry bundled script', () => { + const input = { + shellScript: `set -e +WITH_ENVIRONMENT="../node_modules/react-native/scripts/xcode/with-environment.sh" +REACT_NATIVE_XCODE="../node_modules/react-native/scripts/react-native-xcode.sh" +SENTRY_CLI="sentry-cli react-native xcode" + +/bin/sh -c "$WITH_ENVIRONMENT \\"/bin/sh ../node_modules/@sentry/react-native/scripts/sentry-xcode.sh $REACT_NATIVE_XCODE"\\" +`, + }; + expect(doesBundlePhaseIncludeSentry(input)).toBeTruthy(); + }); + it('returns false', () => { const input = { // note sentry-cli can be part of the script but doesn't call react native xcode script @@ -221,6 +276,24 @@ sentry-cli upload-dsym path/to/dsym --include-sources expect(findDebugFilesUploadPhase(input)).toEqual(expected); }); + it('returns debug files build phase using bundled scripts', () => { + const input = { + 1: { + shellScript: 'foo', + }, + 2: { + shellScript: `/bin/sh ../node_modules/@sentry/react-native/scripts/sentry-xcode-debug-files.sh`, + }, + }; + const expected = [ + '2', + { + shellScript: `/bin/sh ../node_modules/@sentry/react-native/scripts/sentry-xcode-debug-files.sh`, + }, + ]; + expect(findDebugFilesUploadPhase(input)).toEqual(expected); + }); + it('returns undefined if build phase not present', () => { const input = { 1: {