diff --git a/CHANGELOG.md b/CHANGELOG.md index d24db260..80497b0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Unreleased + +feat(nextjs): Add telemetry collection to NextJS wizard (#458) + ## 3.13.0 - enh(android): Show link to issues page after setup is complete (#448) diff --git a/src/android/android-wizard.ts b/src/android/android-wizard.ts index 63df0b5a..b44ffa56 100644 --- a/src/android/android-wizard.ts +++ b/src/android/android-wizard.ts @@ -165,9 +165,8 @@ async function runAndroidWizardWithTelemetry( 'sentry.properties', )} file.`, ); - await traceStep('Add SentryCli Config', () => - addSentryCliConfig(authToken, proguardMappingCliSetupConfig), - ); + + await addSentryCliConfig(authToken, proguardMappingCliSetupConfig); // ======== OUTRO ======== const issuesPageLink = selfHosted diff --git a/src/nextjs/nextjs-wizard.ts b/src/nextjs/nextjs-wizard.ts index 734fdce5..1c9803bb 100644 --- a/src/nextjs/nextjs-wizard.ts +++ b/src/nextjs/nextjs-wizard.ts @@ -7,6 +7,8 @@ import * as fs from 'fs'; import { builders, generateCode, parseModule } from 'magicast'; import * as path from 'path'; +import * as Sentry from '@sentry/node'; + import { abort, abortIfCancelled, @@ -19,7 +21,7 @@ import { isUsingTypeScript, printWelcome, } from '../utils/clack-utils'; -import { WizardOptions } from '../utils/types'; +import { SentryProjectData, WizardOptions } from '../utils/types'; import { getNextjsConfigCjsAppendix, getNextjsConfigCjsTemplate, @@ -31,88 +33,158 @@ import { getSentryExampleAppDirApiRoute, getSentryExamplePageContents, } from './templates'; +import { traceStep, withTelemetry } from '../telemetry'; +import { getPackageVersion, hasPackageInstalled } from '../utils/package-json'; +import { getNextJsVersionBucket } from './utils'; + +export function runNextjsWizard(options: WizardOptions) { + return withTelemetry( + { + enabled: options.telemetryEnabled, + integration: 'nextjs', + }, + () => runNextjsWizardWithTelemetry(options), + ); +} -// eslint-disable-next-line complexity -export async function runNextjsWizard(options: WizardOptions): Promise { +export async function runNextjsWizardWithTelemetry( + options: WizardOptions, +): Promise { printWelcome({ wizardName: 'Sentry Next.js Wizard', promoCode: options.promoCode, + telemetryEnabled: options.telemetryEnabled, }); await confirmContinueEvenThoughNoGitRepo(); const packageJson = await getPackageDotJson(); + await ensurePackageIsInstalled(packageJson, 'next', 'Next.js'); + const nextVersion = getPackageVersion('next', packageJson); + Sentry.setTag('nextjs-version', getNextJsVersionBucket(nextVersion)); + const { selectedProject, authToken, selfHosted, sentryUrl } = await getOrAskForProjectData(options, 'javascript-nextjs'); + const sdkAlreadyInstalled = hasPackageInstalled( + '@sentry/nextjs', + packageJson, + ); + Sentry.setTag('sdk-already-installed', sdkAlreadyInstalled); + await installPackage({ packageName: '@sentry/nextjs', alreadyInstalled: !!packageJson?.dependencies?.['@sentry/nextjs'], }); - const typeScriptDetected = isUsingTypeScript(); + await traceStep('configure-sdk', async () => + createOrMergeNextJsFiles(selectedProject, selfHosted, sentryUrl), + ); - const configVariants = ['server', 'client', 'edge'] as const; + await traceStep('create-example-page', async () => + createExamplePage(selfHosted, selectedProject, sentryUrl), + ); - for (const configVariant of configVariants) { - const jsConfig = `sentry.${configVariant}.config.js`; - const tsConfig = `sentry.${configVariant}.config.ts`; + await addSentryCliConfig(authToken); + + const mightBeUsingVercel = fs.existsSync( + path.join(process.cwd(), 'vercel.json'), + ); - const jsConfigExists = fs.existsSync(path.join(process.cwd(), jsConfig)); - const tsConfigExists = fs.existsSync(path.join(process.cwd(), tsConfig)); + clack.outro( + `${chalk.green('Everything is set up!')} - let shouldWriteFile = true; + ${chalk.cyan( + 'You can validate your setup by starting your dev environment (`next dev`) and visiting "/sentry-example-page".', + )} +${ + mightBeUsingVercel + ? ` + ▲ It seems like you're using Vercel. We recommend using the Sentry Vercel integration: https://vercel.com/integrations/sentry +` + : '' +} + ${chalk.dim( + 'If you encounter any issues, let us know here: https://github.com/getsentry/sentry-javascript/issues', + )}`, + ); +} - if (jsConfigExists || tsConfigExists) { - const existingConfigs = []; +async function createOrMergeNextJsFiles( + selectedProject: SentryProjectData, + selfHosted: boolean, + sentryUrl: string, +) { + const typeScriptDetected = isUsingTypeScript(); - if (jsConfigExists) { - existingConfigs.push(jsConfig); - } + const configVariants = ['server', 'client', 'edge'] as const; - if (tsConfigExists) { - existingConfigs.push(tsConfig); - } + for (const configVariant of configVariants) { + await traceStep(`create-sentry-${configVariant}-config`, async () => { + const jsConfig = `sentry.${configVariant}.config.js`; + const tsConfig = `sentry.${configVariant}.config.ts`; - const overwriteExistingConfigs = await abortIfCancelled( - clack.confirm({ - message: `Found existing Sentry ${configVariant} config (${existingConfigs.join( - ', ', - )}). Overwrite ${existingConfigs.length > 1 ? 'them' : 'it'}?`, - }), - ); + const jsConfigExists = fs.existsSync(path.join(process.cwd(), jsConfig)); + const tsConfigExists = fs.existsSync(path.join(process.cwd(), tsConfig)); - shouldWriteFile = overwriteExistingConfigs; + let shouldWriteFile = true; + + if (jsConfigExists || tsConfigExists) { + const existingConfigs = []; - if (overwriteExistingConfigs) { if (jsConfigExists) { - fs.unlinkSync(path.join(process.cwd(), jsConfig)); - clack.log.warn(`Removed existing ${chalk.bold(jsConfig)}.`); + existingConfigs.push(jsConfig); } + if (tsConfigExists) { - fs.unlinkSync(path.join(process.cwd(), tsConfig)); - clack.log.warn(`Removed existing ${chalk.bold(tsConfig)}.`); + existingConfigs.push(tsConfig); + } + + const overwriteExistingConfigs = await abortIfCancelled( + clack.confirm({ + message: `Found existing Sentry ${configVariant} config (${existingConfigs.join( + ', ', + )}). Overwrite ${existingConfigs.length > 1 ? 'them' : 'it'}?`, + }), + ); + Sentry.setTag( + `overwrite-${configVariant}-config`, + overwriteExistingConfigs, + ); + + shouldWriteFile = overwriteExistingConfigs; + + if (overwriteExistingConfigs) { + if (jsConfigExists) { + fs.unlinkSync(path.join(process.cwd(), jsConfig)); + clack.log.warn(`Removed existing ${chalk.bold(jsConfig)}.`); + } + if (tsConfigExists) { + fs.unlinkSync(path.join(process.cwd(), tsConfig)); + clack.log.warn(`Removed existing ${chalk.bold(tsConfig)}.`); + } } } - } - if (shouldWriteFile) { - await fs.promises.writeFile( - path.join(process.cwd(), typeScriptDetected ? tsConfig : jsConfig), - getSentryConfigContents( - selectedProject.keys[0].dsn.public, - configVariant, - ), - { encoding: 'utf8', flag: 'w' }, - ); - clack.log.success( - `Created fresh ${chalk.bold( - typeScriptDetected ? tsConfig : jsConfig, - )}.`, - ); - } + if (shouldWriteFile) { + await fs.promises.writeFile( + path.join(process.cwd(), typeScriptDetected ? tsConfig : jsConfig), + getSentryConfigContents( + selectedProject.keys[0].dsn.public, + configVariant, + ), + { encoding: 'utf8', flag: 'w' }, + ); + clack.log.success( + `Created fresh ${chalk.bold( + typeScriptDetected ? tsConfig : jsConfig, + )}.`, + ); + Sentry.setTag(`created-${configVariant}-config`, true); + } + }); } const sentryWebpackOptionsTemplate = getNextjsWebpackPluginOptionsTemplate( @@ -126,162 +198,179 @@ export async function runNextjsWizard(options: WizardOptions): Promise { const nextConfigJs = 'next.config.js'; const nextConfigMjs = 'next.config.mjs'; - const nextConfigJsExists = fs.existsSync( - path.join(process.cwd(), nextConfigJs), - ); - const nextConfigMjsExists = fs.existsSync( - path.join(process.cwd(), nextConfigMjs), - ); - - if (!nextConfigJsExists && !nextConfigMjsExists) { - await fs.promises.writeFile( + await traceStep('setup-next-config', async () => { + const nextConfigJsExists = fs.existsSync( path.join(process.cwd(), nextConfigJs), - getNextjsConfigCjsTemplate( - sentryWebpackOptionsTemplate, - sentryBuildOptionsTemplate, - ), - { encoding: 'utf8', flag: 'w' }, - ); - - clack.log.success( - `Created ${chalk.bold('next.config.js')} with Sentry configuration.`, ); - } - - if (nextConfigJsExists) { - const nextConfgiJsContent = fs.readFileSync( - path.join(process.cwd(), nextConfigJs), - 'utf8', + const nextConfigMjsExists = fs.existsSync( + path.join(process.cwd(), nextConfigMjs), ); - const probablyIncludesSdk = - nextConfgiJsContent.includes('@sentry/nextjs') && - nextConfgiJsContent.includes('withSentryConfig'); - - let shouldInject = true; - - if (probablyIncludesSdk) { - const injectAnyhow = await abortIfCancelled( - clack.confirm({ - message: `${chalk.bold( - nextConfigJs, - )} already contains Sentry SDK configuration. Should the wizard modify it anyways?`, - }), - ); - - shouldInject = injectAnyhow; - } + if (!nextConfigJsExists && !nextConfigMjsExists) { + Sentry.setTag('next-config-strategy', 'create'); - if (shouldInject) { - await fs.promises.appendFile( + await fs.promises.writeFile( path.join(process.cwd(), nextConfigJs), - getNextjsConfigCjsAppendix( + getNextjsConfigCjsTemplate( sentryWebpackOptionsTemplate, sentryBuildOptionsTemplate, ), - 'utf8', + { encoding: 'utf8', flag: 'w' }, ); clack.log.success( - `Added Sentry configuration to ${chalk.bold(nextConfigJs)}. ${chalk.dim( - '(you probably want to clean this up a bit!)', - )}`, + `Created ${chalk.bold('next.config.js')} with Sentry configuration.`, ); } - } - if (nextConfigMjsExists) { - const nextConfgiMjsContent = fs.readFileSync( - path.join(process.cwd(), nextConfigMjs), - 'utf8', - ); + if (nextConfigJsExists) { + Sentry.setTag('next-config-strategy', 'modify'); - const probablyIncludesSdk = - nextConfgiMjsContent.includes('@sentry/nextjs') && - nextConfgiMjsContent.includes('withSentryConfig'); + const nextConfgiJsContent = fs.readFileSync( + path.join(process.cwd(), nextConfigJs), + 'utf8', + ); - let shouldInject = true; + const probablyIncludesSdk = + nextConfgiJsContent.includes('@sentry/nextjs') && + nextConfgiJsContent.includes('withSentryConfig'); - if (probablyIncludesSdk) { - const injectAnyhow = await abortIfCancelled( - clack.confirm({ - message: `${chalk.bold( - nextConfigMjs, - )} already contains Sentry SDK configuration. Should the wizard modify it anyways?`, - }), - ); + let shouldInject = true; - shouldInject = injectAnyhow; - } + if (probablyIncludesSdk) { + const injectAnyhow = await abortIfCancelled( + clack.confirm({ + message: `${chalk.bold( + nextConfigJs, + )} already contains Sentry SDK configuration. Should the wizard modify it anyways?`, + }), + ); - try { - if (shouldInject) { - const mod = parseModule(nextConfgiMjsContent); - mod.imports.$add({ - from: '@sentry/nextjs', - imported: 'withSentryConfig', - local: 'withSentryConfig', - }); - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access - const expressionToWrap = generateCode(mod.exports.default.$ast).code; - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - mod.exports.default = builders.raw(`withSentryConfig( - ${expressionToWrap}, - ${sentryWebpackOptionsTemplate}, - ${sentryBuildOptionsTemplate} -)`); - const newCode = mod.generate().code; + shouldInject = injectAnyhow; + } - await fs.promises.writeFile( - path.join(process.cwd(), nextConfigMjs), - newCode, - { - encoding: 'utf8', - flag: 'w', - }, + if (shouldInject) { + await fs.promises.appendFile( + path.join(process.cwd(), nextConfigJs), + getNextjsConfigCjsAppendix( + sentryWebpackOptionsTemplate, + sentryBuildOptionsTemplate, + ), + 'utf8', ); + clack.log.success( `Added Sentry configuration to ${chalk.bold( - nextConfigMjs, + nextConfigJs, )}. ${chalk.dim('(you probably want to clean this up a bit!)')}`, ); } - } catch { - clack.log.warn( - chalk.yellow( - `Something went wrong writing to ${chalk.bold(nextConfigMjs)}`, - ), - ); - clack.log.info( - `Please put the following code snippet into ${chalk.bold( - nextConfigMjs, - )}: ${chalk.dim('You probably have to clean it up a bit.')}\n`, - ); - // eslint-disable-next-line no-console - console.log( - getNextjsConfigEsmCopyPasteSnippet( - sentryWebpackOptionsTemplate, - sentryBuildOptionsTemplate, - ), + Sentry.setTag('next-config-mod-result', 'success'); + } + + if (nextConfigMjsExists) { + const nextConfgiMjsContent = fs.readFileSync( + path.join(process.cwd(), nextConfigMjs), + 'utf8', ); - const shouldContinue = await abortIfCancelled( - clack.confirm({ - message: `Are you done putting the snippet above into ${chalk.bold( + const probablyIncludesSdk = + nextConfgiMjsContent.includes('@sentry/nextjs') && + nextConfgiMjsContent.includes('withSentryConfig'); + + let shouldInject = true; + + if (probablyIncludesSdk) { + const injectAnyhow = await abortIfCancelled( + clack.confirm({ + message: `${chalk.bold( + nextConfigMjs, + )} already contains Sentry SDK configuration. Should the wizard modify it anyways?`, + }), + ); + + shouldInject = injectAnyhow; + } + + try { + if (shouldInject) { + const mod = parseModule(nextConfgiMjsContent); + mod.imports.$add({ + from: '@sentry/nextjs', + imported: 'withSentryConfig', + local: 'withSentryConfig', + }); + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access + const expressionToWrap = generateCode(mod.exports.default.$ast).code; + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + mod.exports.default = builders.raw(`withSentryConfig( + ${expressionToWrap}, + ${sentryWebpackOptionsTemplate}, + ${sentryBuildOptionsTemplate} +)`); + const newCode = mod.generate().code; + + await fs.promises.writeFile( + path.join(process.cwd(), nextConfigMjs), + newCode, + { + encoding: 'utf8', + flag: 'w', + }, + ); + clack.log.success( + `Added Sentry configuration to ${chalk.bold( + nextConfigMjs, + )}. ${chalk.dim('(you probably want to clean this up a bit!)')}`, + ); + + Sentry.setTag('next-config-mod-result', 'success'); + } + } catch { + Sentry.setTag('next-config-mod-result', 'fail'); + clack.log.warn( + chalk.yellow( + `Something went wrong writing to ${chalk.bold(nextConfigMjs)}`, + ), + ); + clack.log.info( + `Please put the following code snippet into ${chalk.bold( nextConfigMjs, - )}?`, - active: 'Yes', - inactive: 'No, get me out of here', - }), - ); + )}: ${chalk.dim('You probably have to clean it up a bit.')}\n`, + ); + + // eslint-disable-next-line no-console + console.log( + getNextjsConfigEsmCopyPasteSnippet( + sentryWebpackOptionsTemplate, + sentryBuildOptionsTemplate, + ), + ); + + const shouldContinue = await abortIfCancelled( + clack.confirm({ + message: `Are you done putting the snippet above into ${chalk.bold( + nextConfigMjs, + )}?`, + active: 'Yes', + inactive: 'No, get me out of here', + }), + ); - if (!shouldContinue) { - await abort(); + if (!shouldContinue) { + await abort(); + } } } - } + }); +} +async function createExamplePage( + selfHosted: boolean, + selectedProject: SentryProjectData, + sentryUrl: string, +): Promise { const srcDir = path.join(process.cwd(), 'src'); const maybePagesDirPath = path.join(process.cwd(), 'pages'); const maybeSrcPagesDirPath = path.join(srcDir, 'pages'); @@ -316,6 +405,8 @@ export async function runNextjsWizard(options: WizardOptions): Promise { }); } + Sentry.setTag('nextjs-app-dir', !!appLocation); + if (appLocation) { const examplePageContents = getSentryExamplePageContents({ selfHosted, @@ -415,28 +506,4 @@ export async function runNextjsWizard(options: WizardOptions): Promise { )}.`, ); } - - await addSentryCliConfig(authToken); - - const mightBeUsingVercel = fs.existsSync( - path.join(process.cwd(), 'vercel.json'), - ); - - clack.outro( - `${chalk.green('Everything is set up!')} - - ${chalk.cyan( - 'You can validate your setup by starting your dev environment (`next dev`) and visiting "/sentry-example-page".', - )} -${ - mightBeUsingVercel - ? ` - ▲ It seems like you're using Vercel. We recommend using the Sentry Vercel integration: https://vercel.com/integrations/sentry -` - : '' -} - ${chalk.dim( - 'If you encounter any issues, let us know here: https://github.com/getsentry/sentry-javascript/issues', - )}`, - ); } diff --git a/src/nextjs/utils.ts b/src/nextjs/utils.ts new file mode 100644 index 00000000..7e50d6de --- /dev/null +++ b/src/nextjs/utils.ts @@ -0,0 +1,21 @@ +import { major, minVersion } from 'semver'; + +export function getNextJsVersionBucket(version: string | undefined) { + if (!version) { + return 'none'; + } + + try { + const minVer = minVersion(version); + if (!minVer) { + return 'invalid'; + } + const majorVersion = major(minVer); + if (majorVersion >= 11) { + return `${majorVersion}.x`; + } + return '<11.0.0'; + } catch { + return 'unknown'; + } +} diff --git a/src/remix/remix-wizard.ts b/src/remix/remix-wizard.ts index 065eaf70..b5f34638 100644 --- a/src/remix/remix-wizard.ts +++ b/src/remix/remix-wizard.ts @@ -56,12 +56,10 @@ async function runRemixWizardWithTelemetry( const { selectedProject, authToken, sentryUrl } = await getOrAskForProjectData(options, 'javascript-remix'); - await traceStep('Install Sentry SDK', () => - installPackage({ - packageName: '@sentry/remix', - alreadyInstalled: hasPackageInstalled('@sentry/remix', packageJson), - }), - ); + await installPackage({ + packageName: '@sentry/remix', + alreadyInstalled: hasPackageInstalled('@sentry/remix', packageJson), + }); const dsn = selectedProject.keys[0].dsn.public; diff --git a/src/sourcemaps/sourcemaps-wizard.ts b/src/sourcemaps/sourcemaps-wizard.ts index fc8d0bdd..db9e312e 100644 --- a/src/sourcemaps/sourcemaps-wizard.ts +++ b/src/sourcemaps/sourcemaps-wizard.ts @@ -70,7 +70,7 @@ You can turn this off by running the wizard with the '--disable-telemetry' flag. return; } - await traceStep('detect-git', confirmContinueEvenThoughNoGitRepo); + await confirmContinueEvenThoughNoGitRepo(); await traceStep('check-sdk-version', ensureMinimumSdkVersionIsInstalled); diff --git a/src/sveltekit/sveltekit-wizard.ts b/src/sveltekit/sveltekit-wizard.ts index 29633d08..e90b69d4 100644 --- a/src/sveltekit/sveltekit-wizard.ts +++ b/src/sveltekit/sveltekit-wizard.ts @@ -43,12 +43,11 @@ export async function runSvelteKitWizardWithTelemetry( telemetryEnabled: options.telemetryEnabled, }); - await traceStep('detect-git', confirmContinueEvenThoughNoGitRepo); + await confirmContinueEvenThoughNoGitRepo(); const packageJson = await getPackageDotJson(); - await traceStep('detect-framework-version', () => - ensurePackageIsInstalled(packageJson, '@sveltejs/kit', 'Sveltekit'), - ); + + await ensurePackageIsInstalled(packageJson, '@sveltejs/kit', 'Sveltekit'); const kitVersion = getPackageVersion('@sveltejs/kit', packageJson); const kitVersionBucket = getKitVersionBucket(kitVersion); @@ -91,14 +90,12 @@ export async function runSvelteKitWizardWithTelemetry( ); Sentry.setTag('sdk-already-installed', sdkAlreadyInstalled); - await traceStep('install-sdk', () => - installPackage({ - packageName: '@sentry/sveltekit', - alreadyInstalled: sdkAlreadyInstalled, - }), - ); + await installPackage({ + packageName: '@sentry/sveltekit', + alreadyInstalled: sdkAlreadyInstalled, + }); - await traceStep('add-cli-config', () => addSentryCliConfig(authToken)); + await addSentryCliConfig(authToken); const svelteConfig = await traceStep('load-svelte-config', loadSvelteConfig); diff --git a/src/utils/clack-utils.ts b/src/utils/clack-utils.ts index 924c97b4..9d6f303f 100644 --- a/src/utils/clack-utils.ts +++ b/src/utils/clack-utils.ts @@ -148,24 +148,26 @@ You can turn this off at any time by running ${chalk.cyanBright( } export async function confirmContinueEvenThoughNoGitRepo(): Promise { - try { - childProcess.execSync('git rev-parse --is-inside-work-tree', { - stdio: 'ignore', - }); - } catch { - const continueWithoutGit = await abortIfCancelled( - clack.confirm({ - message: - 'You are not inside a git repository. The wizard will create and update files. Do you still want to continue?', - }), - ); + return traceStep('detect-git', async () => { + try { + childProcess.execSync('git rev-parse --is-inside-work-tree', { + stdio: 'ignore', + }); + } catch { + const continueWithoutGit = await abortIfCancelled( + clack.confirm({ + message: + 'You are not inside a git repository. The wizard will create and update files. Do you still want to continue?', + }), + ); - Sentry.setTag('continue-without-git', continueWithoutGit); + Sentry.setTag('continue-without-git', continueWithoutGit); - if (!continueWithoutGit) { - await abort(undefined, 0); + if (!continueWithoutGit) { + await abort(undefined, 0); + } } - } + }); } export async function askToInstallSentryCLI(): Promise { @@ -207,123 +209,127 @@ export async function installPackage({ alreadyInstalled: boolean; askBeforeUpdating?: boolean; }): Promise { - if (alreadyInstalled && askBeforeUpdating) { - const shouldUpdatePackage = await abortIfCancelled( - clack.confirm({ - message: `The ${chalk.bold.cyan( - packageName, - )} package is already installed. Do you want to update it to the latest version?`, - }), - ); + return traceStep('install-package', async () => { + if (alreadyInstalled && askBeforeUpdating) { + const shouldUpdatePackage = await abortIfCancelled( + clack.confirm({ + message: `The ${chalk.bold.cyan( + packageName, + )} package is already installed. Do you want to update it to the latest version?`, + }), + ); - if (!shouldUpdatePackage) { - return; + if (!shouldUpdatePackage) { + return; + } } - } - - const sdkInstallSpinner = clack.spinner(); - const packageManager = await getPackageManager(); + const sdkInstallSpinner = clack.spinner(); - sdkInstallSpinner.start( - `${alreadyInstalled ? 'Updating' : 'Installing'} ${chalk.bold.cyan( - packageName, - )} with ${chalk.bold(packageManager.label)}.`, - ); + const packageManager = await getPackageManager(); - try { - await installPackageWithPackageManager(packageManager, packageName); - } catch (e) { - sdkInstallSpinner.stop('Installation failed.'); - clack.log.error( - `${chalk.red( - 'Encountered the following error during installation:', - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - )}\n\n${e}\n\n${chalk.dim( - 'If you think this issue is caused by the Sentry wizard, let us know here:\nhttps://github.com/getsentry/sentry-wizard/issues', - )}`, + sdkInstallSpinner.start( + `${alreadyInstalled ? 'Updating' : 'Installing'} ${chalk.bold.cyan( + packageName, + )} with ${chalk.bold(packageManager.label)}.`, ); - await abort(); - } - sdkInstallSpinner.stop( - `${alreadyInstalled ? 'Updated' : 'Installed'} ${chalk.bold.cyan( - packageName, - )} with ${chalk.bold(packageManager.label)}.`, - ); + try { + await installPackageWithPackageManager(packageManager, packageName); + } catch (e) { + sdkInstallSpinner.stop('Installation failed.'); + clack.log.error( + `${chalk.red( + 'Encountered the following error during installation:', + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + )}\n\n${e}\n\n${chalk.dim( + 'If you think this issue is caused by the Sentry wizard, let us know here:\nhttps://github.com/getsentry/sentry-wizard/issues', + )}`, + ); + await abort(); + } + + sdkInstallSpinner.stop( + `${alreadyInstalled ? 'Updated' : 'Installed'} ${chalk.bold.cyan( + packageName, + )} with ${chalk.bold(packageManager.label)}.`, + ); + }); } export async function addSentryCliConfig( authToken: string, setupConfig: CliSetupConfig = sourceMapsCliSetupConfig, ): Promise { - const configExists = fs.existsSync( - path.join(process.cwd(), setupConfig.filename), - ); - if (configExists) { - const configContents = fs.readFileSync( + return traceStep('add-sentry-cli-config', async () => { + const configExists = fs.existsSync( path.join(process.cwd(), setupConfig.filename), - 'utf8', ); - - if (setupConfig.likelyAlreadyHasAuthToken(configContents)) { - clack.log.warn( - `${chalk.bold( - setupConfig.filename, - )} already has auth token. Will not add one.`, + if (configExists) { + const configContents = fs.readFileSync( + path.join(process.cwd(), setupConfig.filename), + 'utf8', ); + + if (setupConfig.likelyAlreadyHasAuthToken(configContents)) { + clack.log.warn( + `${chalk.bold( + setupConfig.filename, + )} already has auth token. Will not add one.`, + ); + } else { + try { + await fs.promises.writeFile( + path.join(process.cwd(), setupConfig.filename), + `${configContents}\n${setupConfig.tokenContent(authToken)}\n`, + { encoding: 'utf8', flag: 'w' }, + ); + clack.log.success( + chalk.greenBright( + `Added auth token to ${chalk.bold( + setupConfig.filename, + )} for you to test uploading ${setupConfig.name} locally.`, + ), + ); + } catch { + clack.log.warning( + `Failed to add auth token to ${chalk.bold( + setupConfig.filename, + )}. Uploading ${ + setupConfig.name + } during build will likely not work locally.`, + ); + } + } } else { try { await fs.promises.writeFile( path.join(process.cwd(), setupConfig.filename), - `${configContents}\n${setupConfig.tokenContent(authToken)}\n`, + `${setupConfig.tokenContent(authToken)}\n`, { encoding: 'utf8', flag: 'w' }, ); clack.log.success( chalk.greenBright( - `Added auth token to ${chalk.bold( + `Created ${chalk.bold( setupConfig.filename, - )} for you to test uploading ${setupConfig.name} locally.`, + )} with auth token for you to test uploading ${ + setupConfig.name + } locally.`, ), ); } catch { clack.log.warning( - `Failed to add auth token to ${chalk.bold( + `Failed to create ${chalk.bold( setupConfig.filename, - )}. Uploading ${ + )} with auth token. Uploading ${ setupConfig.name } during build will likely not work locally.`, ); } } - } else { - try { - await fs.promises.writeFile( - path.join(process.cwd(), setupConfig.filename), - `${setupConfig.tokenContent(authToken)}\n`, - { encoding: 'utf8', flag: 'w' }, - ); - clack.log.success( - chalk.greenBright( - `Created ${chalk.bold( - setupConfig.filename, - )} with auth token for you to test uploading ${ - setupConfig.name - } locally.`, - ), - ); - } catch { - clack.log.warning( - `Failed to create ${chalk.bold( - setupConfig.filename, - )} with auth token. Uploading ${ - setupConfig.name - } during build will likely not work locally.`, - ); - } - } - await addAuthTokenFileToGitIgnore(setupConfig.filename); + await addAuthTokenFileToGitIgnore(setupConfig.filename); + }); } export async function addDotEnvSentryBuildPluginFile( @@ -418,26 +424,40 @@ async function addAuthTokenFileToGitIgnore(filename: string): Promise { } } +/** + * Checks if @param packageId is listed as a dependency in @param packageJson. + * If not, it will ask users if they want to continue without the package. + * + * Use this function to check if e.g. a the framework of the SDK is installed + * + * @param packageJson the package.json object + * @param packageId the npm name of the package + * @param packageName a human readable name of the package + */ export async function ensurePackageIsInstalled( packageJson: PackageDotJson, packageId: string, packageName: string, -) { - if (!hasPackageInstalled(packageId, packageJson)) { - Sentry.setTag('package-installed', false); - const continueWithoutPackage = await abortIfCancelled( - clack.confirm({ - message: `${packageName} does not seem to be installed. Do you still want to continue?`, - initialValue: false, - }), - ); +): Promise { + return traceStep('ensure-package-installed', async () => { + const installed = hasPackageInstalled(packageId, packageJson); + + Sentry.setTag(`${packageName.toLowerCase()}-installed`, installed); + + if (!installed) { + Sentry.setTag(`${packageName.toLowerCase()}-installed`, false); + const continueWithoutPackage = await abortIfCancelled( + clack.confirm({ + message: `${packageName} does not seem to be installed. Do you still want to continue?`, + initialValue: false, + }), + ); - if (!continueWithoutPackage) { - await abort(undefined, 0); + if (!continueWithoutPackage) { + await abort(undefined, 0); + } } - } else { - Sentry.setTag('package-installed', true); - } + }); } export async function getPackageDotJson(): Promise { @@ -457,7 +477,9 @@ export async function getPackageDotJson(): Promise { packageJson = JSON.parse(packageJsonFileContents); } catch { clack.log.error( - 'Unable to parse your package.json. Make sure it has a valid format!', + `Unable to parse your ${chalk.cyan( + 'package.json', + )}. Make sure it has a valid format!`, ); await abort(); diff --git a/src/utils/package-manager.ts b/src/utils/package-manager.ts index ff793a4b..8ac9e73f 100644 --- a/src/utils/package-manager.ts +++ b/src/utils/package-manager.ts @@ -4,6 +4,9 @@ import * as fs from 'fs'; import * as path from 'path'; import { promisify } from 'util'; +import * as Sentry from '@sentry/node'; +import { traceStep } from '../telemetry'; + export interface PackageManager { name: string; label: string; @@ -50,13 +53,16 @@ export const NPM: PackageManager = { export const packageManagers = [BUN, YARN, PNPM, NPM]; export function detectPackageManger(): PackageManager | null { - for (const packageManager of packageManagers) { - if (fs.existsSync(path.join(process.cwd(), packageManager.lockFile))) { - return packageManager; + return traceStep('detect-package-manager', () => { + for (const packageManager of packageManagers) { + if (fs.existsSync(path.join(process.cwd(), packageManager.lockFile))) { + Sentry.setTag('package-manager', packageManager.name); + return packageManager; + } } - } - // We make the default NPM - it's weird if we don't find any lock file - return null; + Sentry.setTag('package-manager', 'not-detected'); + return null; + }); } export async function installPackageWithPackageManager(