@sentry/wizard
Version:
Sentry wizard helping you to configure your project
326 lines (323 loc) • 17.5 kB
JavaScript
;
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