@sentry/wizard
Version:
Sentry wizard helping you to configure your project
367 lines (327 loc) • 11.1 kB
text/typescript
// @ts-ignore - clack is ESM and TS complains about that. It works though
import clack from '@clack/prompts';
import chalk from 'chalk';
import * as Sentry from '@sentry/node';
import {
abort,
abortIfCancelled,
confirmContinueIfNoOrDirtyGitRepo,
SENTRY_DOT_ENV_FILE,
printWelcome,
SENTRY_CLI_RC_FILE,
getOrAskForProjectData,
} from '../utils/clack-utils';
import { isUnicodeSupported } from '../utils/vendor/is-unicorn-supported';
import { SourceMapUploadToolConfigurationOptions } from './tools/types';
import { configureVitePlugin } from './tools/vite';
import { setupNpmScriptInCI, configureSentryCLI } from './tools/sentry-cli';
import { configureWebPackPlugin } from './tools/webpack';
import { configureTscSourcemapGenerationFlow } from './tools/tsc';
import { configureRollupPlugin } from './tools/rollup';
import { configureEsbuildPlugin } from './tools/esbuild';
import { WizardOptions } from '../utils/types';
import { configureCRASourcemapGenerationFlow } from './tools/create-react-app';
import { ensureMinimumSdkVersionIsInstalled } from './utils/sdk-version';
import { traceStep, withTelemetry } from '../telemetry';
import { checkIfMoreSuitableWizardExistsAndAskForRedirect } from './utils/other-wizards';
import { configureAngularSourcemapGenerationFlow } from './tools/angular';
import { detectUsedTool, SupportedTools } from './utils/detect-tool';
import { configureNextJsSourceMapsUpload } from './tools/nextjs';
import { configureRemixSourceMapsUpload } from './tools/remix';
import { detectPackageManger } from '../utils/package-manager';
import { getIssueStreamUrl } from '../utils/url';
export async function runSourcemapsWizard(
options: WizardOptions,
): Promise<void> {
return withTelemetry(
{
enabled: options.telemetryEnabled,
integration: 'sourcemaps',
},
() => runSourcemapsWizardWithTelemetry(options),
);
}
async function runSourcemapsWizardWithTelemetry(
options: WizardOptions,
): Promise<void> {
printWelcome({
wizardName: 'Sentry Source Maps Upload Configuration Wizard',
message: `This wizard will help you upload source maps to Sentry as part of your build.
Thank you for using Sentry :)${
options.telemetryEnabled
? `
(This setup wizard sends telemetry data and crash reports to Sentry.
You can turn this off by running the wizard with the '--disable-telemetry' flag.)`
: ''
}`,
promoCode: options.promoCode,
});
const moreSuitableWizard = await traceStep(
'check-framework-wizard',
checkIfMoreSuitableWizardExistsAndAskForRedirect,
);
if (moreSuitableWizard) {
await traceStep('run-framework-wizard', () => moreSuitableWizard(options));
return;
}
await confirmContinueIfNoOrDirtyGitRepo();
await traceStep('check-sdk-version', ensureMinimumSdkVersionIsInstalled);
const { selfHosted, selectedProject, sentryUrl, authToken } =
await getOrAskForProjectData(options);
const wizardOptionsWithPreSelectedProject = {
...options,
preSelectedProject: {
project: selectedProject,
authToken,
selfHosted,
},
};
const selectedTool = await traceStep('select-tool', askForUsedBundlerTool);
Sentry.setTag('selected-tool', selectedTool);
if (selectedTool === 'no-tool') {
clack.log.info(
"No Problem! But in this case, there's nothing to configure :)",
);
await abort('Exiting, have a great day!', 0);
return;
}
await traceStep('tool-setup', () =>
startToolSetupFlow(
selectedTool,
{
orgSlug: selectedProject.organization.slug,
projectSlug: selectedProject.slug,
selfHosted,
url: sentryUrl,
authToken,
},
wizardOptionsWithPreSelectedProject,
),
);
await traceStep('ci-setup', () => configureCI(selectedTool, authToken));
traceStep('outro', () =>
printOutro(
sentryUrl,
selectedProject.organization.slug,
selectedProject.id,
),
);
}
async function askForUsedBundlerTool(): Promise<SupportedTools> {
const selectedTool = await abortIfCancelled(
clack.select({
message: 'Which framework, bundler or build tool are you using?',
options: [
{
label: 'Angular',
value: 'angular',
hint: 'Select this option if you are using Angular.',
},
{
label: 'Create React App',
value: 'create-react-app',
hint: 'Select this option if you set up your app with Create React App.',
},
{
label: 'Next.js',
value: 'nextjs',
hint: 'Select this option if you want to set up source maps in a NextJS project.',
},
{
label: 'Remix',
value: 'remix',
hint: 'Select this option if you want to set up source maps in a Remix project.',
},
{
label: 'Webpack',
value: 'webpack',
hint: 'Select this if you are using Webpack and you have access to your Webpack config.',
},
{
label: 'Vite',
value: 'vite',
hint: 'Select this if you are using Vite and you have access to your Vite config.',
},
{
label: 'esbuild',
value: 'esbuild',
hint: 'Select this if you are using esbuild and you have access to your esbuild config.',
},
{
label: 'Rollup',
value: 'rollup',
hint: 'Select this if you are using Rollup and you have access to your Rollup config.',
},
{
label: 'tsc',
value: 'tsc',
hint: 'Configure source maps when using tsc as build tool',
},
{
label: 'I use another tool',
value: 'sentry-cli',
hint: 'This will configure source maps upload for you using sentry-cli',
},
{
label: "I don't minify, transpile or bundle my code",
value: 'no-tool',
hint: 'This will exit the wizard',
},
],
initialValue: await detectUsedTool(),
}),
);
return selectedTool;
}
async function startToolSetupFlow(
selctedTool: SupportedTools,
options: SourceMapUploadToolConfigurationOptions,
wizardOptions: WizardOptions,
): Promise<void> {
switch (selctedTool) {
case 'webpack':
await configureWebPackPlugin(options);
break;
case 'vite':
await configureVitePlugin(options);
break;
case 'esbuild':
await configureEsbuildPlugin(options);
break;
case 'rollup':
await configureRollupPlugin(options);
break;
case 'tsc':
await configureSentryCLI(options, configureTscSourcemapGenerationFlow);
break;
case 'create-react-app':
await configureSentryCLI(options, configureCRASourcemapGenerationFlow);
break;
case 'angular':
await configureSentryCLI(
options,
configureAngularSourcemapGenerationFlow,
);
break;
case 'nextjs':
await configureNextJsSourceMapsUpload(options, wizardOptions);
break;
case 'remix':
await configureRemixSourceMapsUpload(options, wizardOptions);
break;
default:
await configureSentryCLI(options);
break;
}
}
export async function configureCI(
selectedTool: SupportedTools,
authToken: string,
): Promise<void> {
const isUsingCI = await abortIfCancelled(
clack.select({
message: `Are you using a CI/CD tool to build and deploy your application?`,
options: [
{
label: 'Yes',
hint: 'I use a tool like GitHub Actions, GitLab, CircleCI, TravisCI, Jenkins, Vercel, ...',
value: true,
},
{
label: 'No',
hint: 'I build and deploy my application manually',
value: false,
},
],
initialValue: true,
}),
);
Sentry.setTag('using-ci', isUsingCI);
const isCliBasedFlowTool = [
'sentry-cli',
'tsc',
'angular',
'create-react-app',
].includes(selectedTool);
// some non-cli-based flows also use the .sentryclirc file
const usesSentryCliRc = selectedTool === 'nextjs';
const authTokenFile =
isCliBasedFlowTool || usesSentryCliRc
? SENTRY_CLI_RC_FILE
: SENTRY_DOT_ENV_FILE;
if (!isUsingCI) {
clack.log.info(
`No Problem! Just make sure that the Sentry auth token from ${chalk.cyan(
authTokenFile,
)} is available whenever you build and deploy your app.`,
);
return;
}
if (isCliBasedFlowTool) {
await traceStep('ci-npm-script-setup', setupNpmScriptInCI);
}
await traceStep('ci-auth-token-setup', () => setupAuthTokenInCI(authToken));
}
async function setupAuthTokenInCI(authToken: string) {
clack.log.step(
'Add the Sentry authentication token as an environment variable to your CI setup:',
);
// Intentially logging directly to console here so that the code can be copied/pasted directly
// eslint-disable-next-line no-console
console.log(
chalk.greenBright(`
SENTRY_AUTH_TOKEN=${authToken}
`),
);
clack.log.warn(
chalk.yellow('DO NOT commit this auth token to your repository!'),
);
const addedEnvVarToCI = await abortIfCancelled(
clack.select({
message: 'Did you configure CI as shown above?',
options: [
{ label: 'Yes, continue!', value: true },
{
label: "I'll do it later...",
value: false,
hint: chalk.yellow(
'You need to set the auth token to upload source maps in CI',
),
},
],
initialValue: true,
}),
);
Sentry.setTag('added-env-var-to-ci', addedEnvVarToCI);
if (!addedEnvVarToCI) {
clack.log.info("Don't forget! :)");
}
}
function printOutro(url: string, orgSlug: string, projectId: string) {
const packageManager = detectPackageManger();
const buildCommand = packageManager?.buildCommand ?? 'npm run build';
const issueStreamUrl = getIssueStreamUrl({ url, orgSlug, projectId });
const arrow = isUnicodeSupported() ? '→' : '->';
clack.outro(`${chalk.green("That's it - everything is set up!")}
${chalk.cyan(`Test and validate your setup locally with the following Steps:
1. Build your application in ${chalk.bold('production mode')}.
${chalk.gray(`${arrow} For example, run ${chalk.bold(buildCommand)}.`)}
${chalk.gray(
`${arrow} You should see source map upload logs in your console.`,
)}
2. Run your application and throw a test error.
${chalk.gray(`${arrow} The error should appear in Sentry:`)}
${chalk.gray(`${arrow} ${issueStreamUrl}`)}
3. Open the error in Sentry and verify that it's source-mapped.
${chalk.gray(
`${arrow} The stack trace should show your original source code.`,
)}
`)}
${chalk.dim(
`If you encounter any issues, please refer to the Troubleshooting Guide:
https://docs.sentry.io/platforms/javascript/sourcemaps/troubleshooting_js
If the guide doesn't help or you encounter a bug, please let us know:
https://github.com/getsentry/sentry-javascript/issues`,
)}
`);
}