Skip to content

Commit

Permalink
feat(reactnative): Use Xcode scripts bundled with Sentry RN SDK (#499)
Browse files Browse the repository at this point in the history
  • Loading branch information
krystofwoldrich authored Nov 17, 2023
1 parent 225e676 commit 5982df3
Show file tree
Hide file tree
Showing 4 changed files with 201 additions and 21 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
58 changes: 53 additions & 5 deletions src/react-native/react-native-wizard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,11 @@ import {
findBundlePhase,
patchBundlePhase,
findDebugFilesUploadPhase,
addDebugFilesUploadPhase,
addDebugFilesUploadPhaseWithCli,
writeXcodeProject,
addSentryWithCliToBundleShellScript,
addSentryWithBundledScriptsToBundleShellScript,
addDebugFilesUploadPhaseWithBundledScripts,
} from './xcode';
import {
doesAppBuildGradleIncludeRNSentryGradlePlugin,
Expand All @@ -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
Expand All @@ -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<CliSetupConfigContent>,
'authToken' | 'org' | 'project' | 'url'
Expand Down Expand Up @@ -120,14 +128,20 @@ 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 }),
);

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')) {
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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');
});

Expand All @@ -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');
});

Expand Down
82 changes: 70 additions & 12 deletions src/react-native/xcode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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')}.`,
);
Expand All @@ -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',
Expand Down Expand Up @@ -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',
)
);
}

Expand All @@ -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' +
Expand All @@ -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 },
Expand Down Expand Up @@ -204,13 +258,17 @@ export function unPatchDebugFilesUploadPhase(
export function findDebugFilesUploadPhase(
buildPhasesMap: Record<string, BuildPhase>,
): [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
Expand Down
79 changes: 76 additions & 3 deletions test/react-native/xcode.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
/* eslint-disable no-useless-escape */
import {
addSentryToBundleShellScript,
addSentryWithBundledScriptsToBundleShellScript,
addSentryWithCliToBundleShellScript,
doesBundlePhaseIncludeSentry,
findBundlePhase,
findDebugFilesUploadPhase,
removeSentryFromBundleShellScript,
} 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
Expand All @@ -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,
);
});
});

Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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: {
Expand Down

0 comments on commit 5982df3

Please sign in to comment.