UNPKG

@sentry/wizard

Version:

Sentry wizard helping you to configure your project

326 lines (323 loc) 17.5 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.runReactNativeWizardWithTelemetry = exports.runReactNativeWizard = exports.SUPPORTED_EXPO_RANGE = exports.SUPPORTED_RN_RANGE = exports.RN_HUMAN_NAME = exports.RN_PACKAGE = exports.RN_SDK_SUPPORTED_RANGE = exports.RN_SDK_PACKAGE = void 0; /* eslint-disable max-lines */ // @ts-expect-error - clack is ESM and TS complains about that. It works though const prompts_1 = __importDefault(require("@clack/prompts")); const chalk_1 = __importDefault(require("chalk")); const fs = __importStar(require("fs")); const Sentry = __importStar(require("@sentry/node")); const os_1 = require("os"); const cocoapod_1 = require("../apple/cocoapod"); const telemetry_1 = require("../telemetry"); const mcp_config_1 = require("../utils/clack/mcp-config"); const clack_1 = require("../utils/clack"); const package_json_1 = require("../utils/package-json"); const url_1 = require("../utils/url"); const expo_1 = require("./expo"); const expo_env_file_1 = require("./expo-env-file"); const expo_metro_1 = require("./expo-metro"); const glob_1 = require("./glob"); const gradle_1 = require("./gradle"); const javascript_1 = require("./javascript"); const metro_1 = require("./metro"); const xcode_1 = require("./xcode"); const xcode_2 = __importDefault(require("xcode")); exports.RN_SDK_PACKAGE = '@sentry/react-native'; exports.RN_SDK_SUPPORTED_RANGE = '>=6.12.0'; exports.RN_PACKAGE = 'react-native'; exports.RN_HUMAN_NAME = 'React Native'; exports.SUPPORTED_RN_RANGE = '>=0.69.0'; exports.SUPPORTED_EXPO_RANGE = '>=50.0.0'; async function runReactNativeWizard(params) { return (0, telemetry_1.withTelemetry)({ enabled: params.telemetryEnabled, integration: 'react-native', wizardOptions: params, }, () => runReactNativeWizardWithTelemetry(params)); } exports.runReactNativeWizard = runReactNativeWizard; async function runReactNativeWizardWithTelemetry(options) { const { promoCode, telemetryEnabled, forceInstall } = options; (0, clack_1.printWelcome)({ wizardName: 'Sentry React Native Wizard', promoCode, telemetryEnabled, }); await (0, clack_1.confirmContinueIfNoOrDirtyGitRepo)({ ignoreGitChanges: options.ignoreGitChanges, cwd: undefined, }); const packageJson = await (0, clack_1.getPackageDotJson)(); const hasInstalled = (dep) => (0, package_json_1.hasPackageInstalled)(dep, packageJson); if (hasInstalled('sentry-expo')) { Sentry.setTag('has-sentry-expo-installed', true); (0, expo_1.printSentryExpoMigrationOutro)(); return; } await (0, clack_1.ensurePackageIsInstalled)(packageJson, exports.RN_PACKAGE, exports.RN_HUMAN_NAME); const rnVersion = (0, package_json_1.getPackageVersion)(exports.RN_PACKAGE, packageJson); if (rnVersion) { await (0, clack_1.confirmContinueIfPackageVersionNotSupported)({ packageName: exports.RN_HUMAN_NAME, packageVersion: rnVersion, packageId: exports.RN_PACKAGE, acceptableVersions: exports.SUPPORTED_RN_RANGE, note: `Please upgrade to ${exports.SUPPORTED_RN_RANGE} if you wish to use the Sentry Wizard. Or setup using ${chalk_1.default.cyan('https://docs.sentry.io/platforms/react-native/manual-setup/manual-setup/')}`, }); } await (0, clack_1.installPackage)({ packageName: exports.RN_SDK_PACKAGE, alreadyInstalled: (0, package_json_1.hasPackageInstalled)(exports.RN_SDK_PACKAGE, packageJson), forceInstall, }); const sdkVersion = (0, package_json_1.getPackageVersion)(exports.RN_SDK_PACKAGE, await (0, clack_1.getPackageDotJson)()); if (sdkVersion) { await (0, clack_1.confirmContinueIfPackageVersionNotSupported)({ packageName: 'Sentry React Native SDK', packageVersion: sdkVersion, packageId: exports.RN_SDK_PACKAGE, acceptableVersions: exports.RN_SDK_SUPPORTED_RANGE, note: `Please upgrade to ${exports.RN_SDK_SUPPORTED_RANGE} to continue with the wizard in this project.`, }); } else { const continueWithoutSdk = await (0, clack_1.abortIfCancelled)(prompts_1.default.confirm({ message: 'Could not detect Sentry React Native SDK version. Do you want to continue anyway?', })); if (!continueWithoutSdk) { await (0, clack_1.abort)(undefined, 0); } } Sentry.setTag(`detected-sentry-react-native-sdk-version`, sdkVersion); const expoVersion = (0, package_json_1.getPackageVersion)('expo', packageJson); const isExpo = !!expoVersion; if (expoVersion) { await (0, clack_1.confirmContinueIfPackageVersionNotSupported)({ packageName: 'Expo SDK', packageVersion: expoVersion, packageId: 'expo', acceptableVersions: exports.SUPPORTED_EXPO_RANGE, note: `Please upgrade to ${exports.SUPPORTED_EXPO_RANGE} to continue with the wizard in this Expo project.`, }); } const projectData = await (0, clack_1.getOrAskForProjectData)(options, 'react-native'); if (projectData.spotlight) { prompts_1.default.log.warn('Spotlight mode is not yet supported for React Native.'); prompts_1.default.log.info('Spotlight is currently only available for Next.js.'); await (0, clack_1.abort)('Exiting wizard', 0); return; } const { selectedProject, authToken, sentryUrl } = projectData; const orgSlug = selectedProject.organization.slug; const projectSlug = selectedProject.slug; const projectId = selectedProject.id; const cliConfig = { authToken, org: orgSlug, project: projectSlug, url: sentryUrl, }; // Ask if user wants to enable Session Replay const enableSessionReplay = await (0, clack_1.abortIfCancelled)(prompts_1.default.confirm({ message: 'Do you want to enable Session Replay to help debug issues? (See https://docs.sentry.io/platforms/react-native/session-replay/)', })); Sentry.setTag('enable-session-replay', enableSessionReplay); if (enableSessionReplay) { prompts_1.default.log.info(`Session Replay will be enabled with default settings (replaysSessionSampleRate: ${javascript_1.sessionReplaySampleRate}, replaysOnErrorSampleRate: ${javascript_1.sessionReplayOnErrorSampleRate}).`); prompts_1.default.log.message('By default, all text content, images, and webviews will be masked for privacy. You can customize this in your code later.'); } // Ask if user wants to enable the Feedback Widget const enableFeedbackWidget = await (0, clack_1.abortIfCancelled)(prompts_1.default.confirm({ message: 'Do you want to enable the Feedback Widget to collect feedback from your users? (See https://docs.sentry.io/platforms/react-native/user-feedback/)', })); Sentry.setTag('enable-feedback-widget', enableFeedbackWidget); if (enableFeedbackWidget) { prompts_1.default.log.info(`The Feedback Widget will be enabled with default settings. You can show the widget by calling Sentry.showFeedbackWidget() in your code.`); } // Ask if user wants to enable Logs const enableLogs = await (0, clack_1.abortIfCancelled)(prompts_1.default.confirm({ message: 'Do you want to enable Logs? (See https://docs.sentry.io/platforms/react-native/logs/)', })); Sentry.setTag('enable-logs', enableLogs); if (enableLogs) { prompts_1.default.log.info(`Logs will be enabled with default settings. You can send logs using the Sentry.logger APIs.`); } await (0, telemetry_1.traceStep)('patch-app-js', () => (0, javascript_1.addSentryInit)({ dsn: selectedProject.keys[0].dsn.public, enableSessionReplay, enableFeedbackWidget, enableLogs, })); await (0, telemetry_1.traceStep)('patch-app-js-wrap', () => (0, javascript_1.wrapRootComponent)()); if (isExpo) { await (0, telemetry_1.traceStep)('patch-expo-app-config', () => (0, expo_1.patchExpoAppConfig)(cliConfig)); await (0, telemetry_1.traceStep)('add-expo-env-local', () => (0, expo_env_file_1.addExpoEnvLocal)(cliConfig)); } if (isExpo) { await (0, telemetry_1.traceStep)('patch-metro-config', expo_metro_1.addSentryToExpoMetroConfig); } else { await (0, telemetry_1.traceStep)('patch-metro-config', metro_1.patchMetroWithSentryConfig); } if (fs.existsSync('ios')) { Sentry.setTag('patch-ios', true); await (0, telemetry_1.traceStep)('patch-xcode-files', () => patchXcodeFiles(cliConfig)); } if (fs.existsSync('android')) { Sentry.setTag('patch-android', true); await (0, telemetry_1.traceStep)('patch-android-files', () => patchAndroidFiles(cliConfig)); } await (0, clack_1.runPrettierIfInstalled)({ cwd: undefined }); // Offer optional project-scoped MCP config for Sentry with org and project scope await (0, mcp_config_1.offerProjectScopedMcpConfig)(selectedProject.organization.slug, selectedProject.slug); const confirmedFirstException = await confirmFirstSentryException(sentryUrl, orgSlug, projectId); Sentry.setTag('user-confirmed-first-error', confirmedFirstException); if (confirmedFirstException) { prompts_1.default.outro(`${chalk_1.default.green('Everything is set up!')} ${chalk_1.default.dim('If you encounter any issues, let us know here: https://github.com/getsentry/sentry-react-native/issues')}`); } else { prompts_1.default.outro(`${chalk_1.default.dim('Let us know here: https://github.com/getsentry/sentry-react-native/issues')}`); } } exports.runReactNativeWizardWithTelemetry = runReactNativeWizardWithTelemetry; async function confirmFirstSentryException(url, orgSlug, projectId) { const issuesStreamUrl = (0, url_1.getIssueStreamUrl)({ url, orgSlug, projectId }); prompts_1.default.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_1.default.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_1.default.greenBright(` <Button title='Try!' onPress={ () => { Sentry.captureException(new Error('First error')) }}/> `)); const firstErrorConfirmed = prompts_1.default.confirm({ message: `Have you successfully sent a test event?`, }); return firstErrorConfirmed; } async function patchXcodeFiles(config) { await (0, clack_1.addSentryCliConfig)(config, { ...clack_1.propertiesCliSetupConfig, name: 'source maps and iOS debug files', filename: 'ios/sentry.properties', gitignore: false, }); if ((0, os_1.platform)() === 'darwin' && (await confirmPodInstall())) { await (0, telemetry_1.traceStep)('pod-install', () => (0, cocoapod_1.podInstall)('ios')); } const xcodeProjectPath = (0, telemetry_1.traceStep)('find-xcode-project', () => (0, glob_1.getFirstMatchedPath)(glob_1.XCODE_PROJECT)); Sentry.setTag('xcode-project-status', xcodeProjectPath ? 'found' : 'not-found'); if (!xcodeProjectPath) { prompts_1.default.log.warn(`Could not find Xcode project file using ${chalk_1.default.cyan(glob_1.XCODE_PROJECT)}.`); return; } // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const [xcodeProject, buildPhasesMap] = (0, telemetry_1.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_2.default.project(xcodeProjectPath); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call project.parseSync(); const map = (0, xcode_1.getValidExistingBuildPhases)(project); return [project, map]; }); Sentry.setTag('xcode-project-status', 'parsed'); await (0, telemetry_1.traceStep)('patch-bundle-phase', async () => { const bundlePhase = (0, xcode_1.findBundlePhase)(buildPhasesMap); Sentry.setTag('xcode-bundle-phase-status', bundlePhase ? 'found' : 'not-found'); await (0, xcode_1.patchBundlePhase)(bundlePhase, xcode_1.addSentryWithBundledScriptsToBundleShellScript); Sentry.setTag('xcode-bundle-phase-status', 'patched'); }); (0, telemetry_1.traceStep)('add-debug-files-upload-phase', () => { const debugFilesUploadPhaseExists = !!(0, xcode_1.findDebugFilesUploadPhase)(buildPhasesMap); Sentry.setTag('xcode-debug-files-upload-phase-status', debugFilesUploadPhaseExists ? 'already-exists' : undefined); (0, xcode_1.addDebugFilesUploadPhaseWithBundledScripts)(xcodeProject, { debugFilesUploadPhaseExists, }); Sentry.setTag('xcode-debug-files-upload-phase-status', 'added'); }); (0, telemetry_1.traceStep)('write-xcode-project', () => { (0, xcode_1.writeXcodeProject)(xcodeProjectPath, xcodeProject); }); Sentry.setTag('xcode-project-status', 'patched'); } async function patchAndroidFiles(config) { await (0, clack_1.addSentryCliConfig)(config, { ...clack_1.propertiesCliSetupConfig, name: 'source maps and iOS debug files', filename: 'android/sentry.properties', gitignore: false, }); const appBuildGradlePath = (0, telemetry_1.traceStep)('find-app-build-gradle', () => (0, glob_1.getFirstMatchedPath)(glob_1.APP_BUILD_GRADLE)); Sentry.setTag('app-build-gradle-status', appBuildGradlePath ? 'found' : 'not-found'); if (!appBuildGradlePath) { prompts_1.default.log.warn(`Could not find Android ${chalk_1.default.cyan('app/build.gradle')} file using ${chalk_1.default.cyan(glob_1.APP_BUILD_GRADLE)}.`); return; } const appBuildGradle = (0, telemetry_1.traceStep)('read-app-build-gradle', () => fs.readFileSync(appBuildGradlePath, 'utf-8')); const includesSentry = (0, gradle_1.doesAppBuildGradleIncludeRNSentryGradlePlugin)(appBuildGradle); if (includesSentry) { Sentry.setTag('app-build-gradle-status', 'already-includes-sentry'); prompts_1.default.log.warn(`Android ${chalk_1.default.cyan('app/build.gradle')} file already includes Sentry.`); return; } const patchedAppBuildGradle = (0, telemetry_1.traceStep)('add-rn-sentry-gradle-plugin', () => (0, gradle_1.addRNSentryGradlePlugin)(appBuildGradle)); if (!(0, gradle_1.doesAppBuildGradleIncludeRNSentryGradlePlugin)(patchedAppBuildGradle)) { Sentry.setTag('app-build-gradle-status', 'failed-to-add-rn-sentry-gradle-plugin'); prompts_1.default.log.warn(`Could not add Sentry RN Gradle Plugin to ${chalk_1.default.cyan('app/build.gradle')}.`); return; } Sentry.setTag('app-build-gradle-status', 'added-rn-sentry-gradle-plugin'); prompts_1.default.log.success(`Added Sentry RN Gradle Plugin to ${chalk_1.default.bold('app/build.gradle')}.`); (0, telemetry_1.traceStep)('write-app-build-gradle', () => (0, gradle_1.writeAppBuildGradle)(appBuildGradlePath, patchedAppBuildGradle)); prompts_1.default.log.success(chalk_1.default.green(`Android ${chalk_1.default.cyan('app/build.gradle')} saved.`)); } async function confirmPodInstall() { return (0, telemetry_1.traceStep)('confirm-pod-install', async () => { const continueWithPodInstall = await (0, clack_1.abortIfCancelled)(prompts_1.default.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; }); } //# sourceMappingURL=react-native-wizard.js.map