UNPKG

@sentry/wizard

Version:

Sentry wizard helping you to configure your project

486 lines (432 loc) 14.5 kB
/* eslint-disable max-lines */ // @ts-ignore - clack is ESM and TS complains about that. It works though import clack from '@clack/prompts'; import chalk from 'chalk'; import * as fs from 'fs'; import { CliSetupConfigContent, abortIfCancelled, addSentryCliConfig, confirmContinueIfNoOrDirtyGitRepo, confirmContinueIfPackageVersionNotSupported, ensurePackageIsInstalled, getOrAskForProjectData, getPackageDotJson, installPackage, printWelcome, propertiesCliSetupConfig, } from '../utils/clack-utils'; import { getPackageVersion, hasPackageInstalled } from '../utils/package-json'; import { podInstall } from '../apple/cocoapod'; import { platform } from 'os'; import { getValidExistingBuildPhases, findBundlePhase, patchBundlePhase, findDebugFilesUploadPhase, addDebugFilesUploadPhaseWithCli, writeXcodeProject, addSentryWithCliToBundleShellScript, addSentryWithBundledScriptsToBundleShellScript, addDebugFilesUploadPhaseWithBundledScripts, } from './xcode'; import { doesAppBuildGradleIncludeRNSentryGradlePlugin, addRNSentryGradlePlugin, writeAppBuildGradle, } from './gradle'; import { runReactNativeUninstall } from './uninstall'; import { APP_BUILD_GRADLE, XCODE_PROJECT, getFirstMatchedPath } from './glob'; import { ReactNativeWizardOptions } from './options'; import { addSentryInit } from './javascript'; import { traceStep, withTelemetry } from '../telemetry'; import * as Sentry from '@sentry/node'; import { fulfillsVersionRange } from '../utils/semver'; import { getIssueStreamUrl } from '../utils/url'; import { patchMetroConfigWithSentrySerializer, patchMetroWithSentryConfig, } from './metro'; import { patchExpoAppConfig, printSentryExpoMigrationOutro } from './expo'; import { addSentryToExpoMetroConfig } from './expo-metro'; import { addExpoEnvLocal } from './expo-env-file'; // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const xcode = require('xcode'); export const RN_SDK_PACKAGE = '@sentry/react-native'; export const RN_PACKAGE = 'react-native'; export const RN_HUMAN_NAME = 'React Native'; export const SUPPORTED_RN_RANGE = '>=0.69.0'; export const SUPPORTED_EXPO_RANGE = '>=50.0.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'; /** * The following SDK version ship with Sentry Metro plugin */ export const SDK_SENTRY_METRO_PLUGIN_SUPPORTED_SDK_RANGE = '>=5.11.0'; /** * The following SDK version ship with bundled Expo plugin */ export const SDK_EXPO_SUPPORTED_SDK_RANGE = `>=5.16.0`; // The following SDK version shipped `withSentryConfig` export const SDK_SENTRY_METRO_WITH_SENTRY_CONFIG_SUPPORTED_SDK_RANGE = '>=5.17.0'; export type RNCliSetupConfigContent = Pick< Required<CliSetupConfigContent>, 'authToken' | 'org' | 'project' | 'url' >; export async function runReactNativeWizard( params: ReactNativeWizardOptions, ): Promise<void> { return withTelemetry( { enabled: params.telemetryEnabled, integration: 'react-native', }, () => runReactNativeWizardWithTelemetry(params), ); } export async function runReactNativeWizardWithTelemetry( options: ReactNativeWizardOptions, ): Promise<void> { if (options.uninstall) { Sentry.setTag('uninstall', true); return runReactNativeUninstall(options); } printWelcome({ wizardName: 'Sentry React Native Wizard', promoCode: options.promoCode, telemetryEnabled: options.telemetryEnabled, }); await confirmContinueIfNoOrDirtyGitRepo(); const packageJson = await getPackageDotJson(); const hasInstalled = (dep: string) => hasPackageInstalled(dep, packageJson); if (hasInstalled('sentry-expo')) { Sentry.setTag('has-sentry-expo-installed', true); printSentryExpoMigrationOutro(); return; } await ensurePackageIsInstalled(packageJson, RN_PACKAGE, RN_HUMAN_NAME); const rnVersion = getPackageVersion(RN_PACKAGE, packageJson); if (rnVersion) { await confirmContinueIfPackageVersionNotSupported({ packageName: RN_HUMAN_NAME, packageVersion: rnVersion, packageId: RN_PACKAGE, acceptableVersions: SUPPORTED_RN_RANGE, note: `Please upgrade to ${SUPPORTED_RN_RANGE} if you wish to use the Sentry Wizard. Or setup using ${chalk.cyan( 'https://docs.sentry.io/platforms/react-native/manual-setup/manual-setup/', )}`, }); } await installPackage({ packageName: RN_SDK_PACKAGE, alreadyInstalled: hasPackageInstalled(RN_SDK_PACKAGE, packageJson), }); const sdkVersion = getPackageVersion( RN_SDK_PACKAGE, await getPackageDotJson(), ); const expoVersion = getPackageVersion('expo', packageJson); const isExpo = !!expoVersion; if (expoVersion && sdkVersion) { await confirmContinueIfPackageVersionNotSupported({ packageName: 'Sentry React Native SDK', packageVersion: sdkVersion, packageId: RN_SDK_PACKAGE, acceptableVersions: SDK_EXPO_SUPPORTED_SDK_RANGE, note: `Please upgrade to ${SDK_EXPO_SUPPORTED_SDK_RANGE} to continue with the wizard in this Expo project.`, }); await confirmContinueIfPackageVersionNotSupported({ packageName: 'Expo SDK', packageVersion: expoVersion, packageId: 'expo', acceptableVersions: SUPPORTED_EXPO_RANGE, note: `Please upgrade to ${SUPPORTED_EXPO_RANGE} to continue with the wizard in this Expo project.`, }); } const { selectedProject, authToken, sentryUrl } = await getOrAskForProjectData(options, 'react-native'); const orgSlug = selectedProject.organization.slug; const projectSlug = selectedProject.slug; const projectId = selectedProject.id; const cliConfig: RNCliSetupConfigContent = { authToken, org: orgSlug, project: projectSlug, url: sentryUrl, }; await traceStep('patch-app-js', () => addSentryInit({ dsn: selectedProject.keys[0].dsn.public }), ); if (isExpo) { await traceStep('patch-expo-app-config', () => patchExpoAppConfig(cliConfig), ); await traceStep('add-expo-env-local', () => addExpoEnvLocal(cliConfig)); } if (isExpo) { await traceStep('patch-metro-config', addSentryToExpoMetroConfig); } else { await traceStep('patch-metro-config', () => addSentryToMetroConfig({ sdkVersion }), ); } if (fs.existsSync('ios')) { Sentry.setTag('patch-ios', true); await traceStep('patch-xcode-files', () => patchXcodeFiles(cliConfig, { sdkVersion }), ); } if (fs.existsSync('android')) { Sentry.setTag('patch-android', true); await traceStep('patch-android-files', () => patchAndroidFiles(cliConfig)); } const confirmedFirstException = await confirmFirstSentryException( sentryUrl, orgSlug, projectId, ); Sentry.setTag('user-confirmed-first-error', confirmedFirstException); if (confirmedFirstException) { clack.outro( `${chalk.green('Everything is set up!')} ${chalk.dim( 'If you encounter any issues, let us know here: https://github.com/getsentry/sentry-react-native/issues', )}`, ); } else { clack.outro( `${chalk.dim( 'Let us know here: https://github.com/getsentry/sentry-react-native/issues', )}`, ); } } function addSentryToMetroConfig({ sdkVersion, }: { sdkVersion: string | undefined; }) { if ( sdkVersion && fulfillsVersionRange({ version: sdkVersion, acceptableVersions: SDK_SENTRY_METRO_WITH_SENTRY_CONFIG_SUPPORTED_SDK_RANGE, canBeLatest: true, }) ) { return patchMetroWithSentryConfig(); } if ( sdkVersion && fulfillsVersionRange({ version: sdkVersion, acceptableVersions: SDK_SENTRY_METRO_PLUGIN_SUPPORTED_SDK_RANGE, canBeLatest: true, }) ) { return patchMetroConfigWithSentrySerializer(); } } async function confirmFirstSentryException( url: string, orgSlug: string, projectId: string, ) { const issuesStreamUrl = getIssueStreamUrl({ url, orgSlug, projectId }); clack.log .step(`To make sure everything is set up correctly, put the following code snippet into your application. The snippet will create a button that, when tapped, sends a test event to Sentry. After that check your project issues: ${chalk.cyan(issuesStreamUrl)}`); // We want the code snippet to be easily copy-pasteable, without any clack artifacts // eslint-disable-next-line no-console console.log( chalk.greenBright(` <Button title='Try!' onPress={ () => { Sentry.captureException(new Error('First error')) }}/> `), ); const firstErrorConfirmed = clack.confirm({ message: `Have you successfully sent a test event?`, }); return firstErrorConfirmed; } async function patchXcodeFiles( config: RNCliSetupConfigContent, context: { sdkVersion: string | undefined; }, ) { await addSentryCliConfig(config, { ...propertiesCliSetupConfig, name: 'source maps and iOS debug files', filename: 'ios/sentry.properties', gitignore: false, }); if (platform() === 'darwin' && (await confirmPodInstall())) { await traceStep('pod-install', () => podInstall('ios')); } const xcodeProjectPath = traceStep('find-xcode-project', () => getFirstMatchedPath(XCODE_PROJECT), ); Sentry.setTag( 'xcode-project-status', xcodeProjectPath ? 'found' : 'not-found', ); if (!xcodeProjectPath) { clack.log.warn( `Could not find Xcode project file using ${chalk.cyan(XCODE_PROJECT)}.`, ); return; } // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const [xcodeProject, buildPhasesMap] = traceStep( 'parse-xcode-project', () => { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call const project = xcode.project(xcodeProjectPath); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call project.parseSync(); const map = getValidExistingBuildPhases(project); return [project, map]; }, ); Sentry.setTag('xcode-project-status', 'parsed'); traceStep('patch-bundle-phase', () => { const bundlePhase = findBundlePhase(buildPhasesMap); Sentry.setTag( 'xcode-bundle-phase-status', bundlePhase ? 'found' : 'not-found', ); 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'); }); traceStep('add-debug-files-upload-phase', () => { const debugFilesUploadPhaseExists = !!findDebugFilesUploadPhase(buildPhasesMap); Sentry.setTag( 'xcode-debug-files-upload-phase-status', debugFilesUploadPhaseExists ? 'already-exists' : undefined, ); 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'); }); traceStep('write-xcode-project', () => { writeXcodeProject(xcodeProjectPath, xcodeProject); }); Sentry.setTag('xcode-project-status', 'patched'); } async function patchAndroidFiles(config: RNCliSetupConfigContent) { await addSentryCliConfig(config, { ...propertiesCliSetupConfig, name: 'source maps and iOS debug files', filename: 'android/sentry.properties', gitignore: false, }); const appBuildGradlePath = traceStep('find-app-build-gradle', () => getFirstMatchedPath(APP_BUILD_GRADLE), ); Sentry.setTag( 'app-build-gradle-status', appBuildGradlePath ? 'found' : 'not-found', ); if (!appBuildGradlePath) { clack.log.warn( `Could not find Android ${chalk.cyan( 'app/build.gradle', )} file using ${chalk.cyan(APP_BUILD_GRADLE)}.`, ); return; } const appBuildGradle = traceStep('read-app-build-gradle', () => fs.readFileSync(appBuildGradlePath, 'utf-8'), ); const includesSentry = doesAppBuildGradleIncludeRNSentryGradlePlugin(appBuildGradle); if (includesSentry) { Sentry.setTag('app-build-gradle-status', 'already-includes-sentry'); clack.log.warn( `Android ${chalk.cyan('app/build.gradle')} file already includes Sentry.`, ); return; } const patchedAppBuildGradle = traceStep('add-rn-sentry-gradle-plugin', () => addRNSentryGradlePlugin(appBuildGradle), ); if (!doesAppBuildGradleIncludeRNSentryGradlePlugin(patchedAppBuildGradle)) { Sentry.setTag( 'app-build-gradle-status', 'failed-to-add-rn-sentry-gradle-plugin', ); clack.log.warn( `Could not add Sentry RN Gradle Plugin to ${chalk.cyan( 'app/build.gradle', )}.`, ); return; } Sentry.setTag('app-build-gradle-status', 'added-rn-sentry-gradle-plugin'); clack.log.success( `Added Sentry RN Gradle Plugin to ${chalk.bold('app/build.gradle')}.`, ); traceStep('write-app-build-gradle', () => writeAppBuildGradle(appBuildGradlePath, patchedAppBuildGradle), ); clack.log.success( chalk.green(`Android ${chalk.cyan('app/build.gradle')} saved.`), ); } async function confirmPodInstall(): Promise<boolean> { return traceStep('confirm-pod-install', async () => { const continueWithPodInstall = await abortIfCancelled( clack.select({ message: 'Do you want to run `pod install` now?', options: [ { value: true, label: 'Yes', hint: 'Recommended for smaller projects, this might take several minutes', }, { value: false, label: `No, I'll do it later` }, ], initialValue: true, }), ); Sentry.setTag('continue-with-pod-install', continueWithPodInstall); return continueWithPodInstall; }); }