diff --git a/CHANGELOG.md b/CHANGELOG.md index b888b3ea..6e0c686c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - feat(nuxt): Add `import-in-the-middle` install step when using pnpm ([#727](https://github.com/getsentry/sentry-wizard/pull/727)) - fix(nuxt): Remove unused parameter in sentry-example-api template ([#734](https://github.com/getsentry/sentry-wizard/pull/734)) - fix(nuxt): Remove option to downgrade override nitropack ([#744](https://github.com/getsentry/sentry-wizard/pull/744)) +- feat(nuxt): Add deployment-platform flow with links to docs ([#747](https://github.com/getsentry/sentry-wizard/pull/747)) ## 3.36.0 diff --git a/e2e-tests/tests/nuxt-3.test.ts b/e2e-tests/tests/nuxt-3.test.ts index a371aefe..87069a89 100644 --- a/e2e-tests/tests/nuxt-3.test.ts +++ b/e2e-tests/tests/nuxt-3.test.ts @@ -55,8 +55,18 @@ async function runWizardOnNuxtProject(projectDir: string): Promise { }, )); - const tracingOptionPrompted = + const deploymentPlatformPrompted = nftOverridePrompted && + (await wizardInstance.sendStdinAndWaitForOutput( + KEYS.ENTER, + 'Please select your deployment platform.', + { + timeout: 240_000, + }, + )); + + const tracingOptionPrompted = + deploymentPlatformPrompted && (await wizardInstance.sendStdinAndWaitForOutput( KEYS.ENTER, // "Do you want to enable Tracing", sometimes doesn't work as `Tracing` can be printed in bold. diff --git a/e2e-tests/tests/nuxt-4.test.ts b/e2e-tests/tests/nuxt-4.test.ts index 4a8306f7..0f6bf93f 100644 --- a/e2e-tests/tests/nuxt-4.test.ts +++ b/e2e-tests/tests/nuxt-4.test.ts @@ -54,8 +54,18 @@ async function runWizardOnNuxtProject(projectDir: string): Promise { }, )); - const tracingOptionPrompted = + const deploymentPlatformPrompted = nftOverridePrompted && + (await wizardInstance.sendStdinAndWaitForOutput( + KEYS.ENTER, + 'Please select your deployment platform.', + { + timeout: 240_000, + }, + )); + + const tracingOptionPrompted = + deploymentPlatformPrompted && (await wizardInstance.sendStdinAndWaitForOutput( KEYS.ENTER, // "Do you want to enable Tracing", sometimes doesn't work as `Tracing` can be printed in bold. diff --git a/src/nuxt/nuxt-wizard.ts b/src/nuxt/nuxt-wizard.ts index e81b2a75..97f80cb1 100644 --- a/src/nuxt/nuxt-wizard.ts +++ b/src/nuxt/nuxt-wizard.ts @@ -26,6 +26,8 @@ import { getNuxtConfig, createConfigFiles, addNuxtOverrides, + askDeploymentPlatform, + confirmReadImportDocs, } from './sdk-setup'; import { createExampleComponent, @@ -116,8 +118,10 @@ export async function runNuxtWizardWithTelemetry( selfHosted, }; + const deploymentPlatform = await askDeploymentPlatform(); + await traceStep('configure-sdk', async () => { - await addSDKModule(nuxtConfig, projectData); + await addSDKModule(nuxtConfig, projectData, deploymentPlatform); await createConfigFiles(selectedProject.keys[0].dsn.public); }); @@ -148,6 +152,8 @@ export async function runNuxtWizardWithTelemetry( await runPrettierIfInstalled(); + await confirmReadImportDocs(deploymentPlatform); + clack.outro( buildOutroMessage(shouldCreateExamplePage, shouldCreateExampleButton), ); @@ -170,8 +176,9 @@ function buildOutroMessage( )} component to a page and triggering it.`; } - msg += `\n\nCheck out the SDK documentation for further configuration: -https://docs.sentry.io/platforms/javascript/guides/nuxt/`; + msg += `\n\nCheck out the SDK documentation for further configuration: ${chalk.underline( + 'https://docs.sentry.io/platforms/javascript/guides/nuxt/', + )}`; return msg; } diff --git a/src/nuxt/sdk-setup.ts b/src/nuxt/sdk-setup.ts index 5f5b5d6f..1ce444e3 100644 --- a/src/nuxt/sdk-setup.ts +++ b/src/nuxt/sdk-setup.ts @@ -1,10 +1,10 @@ // @ts-expect-error - clack is ESM and TS complains about that. It works though -import clack from '@clack/prompts'; +import * as clack from '@clack/prompts'; import * as Sentry from '@sentry/node'; import chalk from 'chalk'; import fs from 'fs'; // @ts-expect-error - magicast is ESM and TS complains about that. It works though -import { loadFile, generateCode, MagicastError } from 'magicast'; +import { loadFile, generateCode } from 'magicast'; // @ts-expect-error - magicast is ESM and TS complains about that. It works though import { addNuxtModule } from 'magicast/helpers'; import path from 'path'; @@ -15,18 +15,19 @@ import { getSentryConfigContents, } from './templates'; import { - abort, abortIfCancelled, askShouldAddPackageOverride, askShouldInstallPackage, featureSelectionPrompt, installPackage, isUsingTypeScript, + opn, } from '../utils/clack-utils'; import { traceStep } from '../telemetry'; import { lt, SemVer } from 'semver'; import { PackageManager, PNPM } from '../utils/package-manager'; import { hasPackageInstalled, PackageDotJson } from '../utils/package-json'; +import { deploymentPlatforms, DeploymentPlatform } from './types'; const possibleNuxtConfig = [ 'nuxt.config.js', @@ -60,10 +61,40 @@ export async function getNuxtConfig(): Promise { return path.join(process.cwd(), configFile); } +export async function askDeploymentPlatform(): Promise< + DeploymentPlatform | symbol +> { + return await abortIfCancelled( + clack.select({ + message: 'Please select your deployment platform.', + options: deploymentPlatforms.map((platform) => ({ + value: platform, + label: `${platform.charAt(0).toUpperCase()}${platform.slice(1)}`, + })), + }), + ); +} + export async function addSDKModule( config: string, options: { org: string; project: string; url: string; selfHosted: boolean }, + deploymentPlatform: DeploymentPlatform | symbol, ): Promise { + const shouldTopLevelImport = + deploymentPlatform === 'vercel' || deploymentPlatform === 'netlify'; + + if (shouldTopLevelImport) { + clack.log.warn( + `Sentry needs to be initialized before the application starts. ${chalk.cyan( + `${deploymentPlatform + .charAt(0) + .toUpperCase()}${deploymentPlatform.slice(1)}`, + )} does not support this yet.\n\nWe will inject the Sentry server-side config at the top of your Nuxt server entry file instead.\n\nThis comes with some restrictions, for more info see:\n\n${chalk.underline( + 'https://docs.sentry.io/platforms/javascript/guides/nuxt/install/top-level-import/', + )} `, + ); + } + try { const mod = await loadFile(config); @@ -73,6 +104,9 @@ export async function addSDKModule( project: options.project, ...(options.selfHosted && { url: options.url }), }, + ...(shouldTopLevelImport && { + autoInjectServerSentry: 'top-level-import', + }), }); addNuxtModule(mod, '@sentry/nuxt/module', 'sourcemap', { client: 'hidden', @@ -86,33 +120,31 @@ export async function addSDKModule( `Added Sentry Nuxt Module to ${chalk.cyan(path.basename(config))}.`, ); } catch (e: unknown) { - // Cases where users spread options are not covered by magicast, - // so we fall back to showing how to configure the nuxt config - // manually. - if (e instanceof MagicastError) { - clack.log.warn( - `Automatic configuration of ${chalk.cyan( - path.basename(config), - )} failed, please add the following settings:`, - ); - // eslint-disable-next-line no-console - console.log(`\n\n${getNuxtModuleFallbackTemplate(options)}\n\n`); - } else { - clack.log.error( - 'Error while adding the Sentry Nuxt Module to the Nuxt config.', - ); - clack.log.info( - chalk.dim( - typeof e === 'object' && e != null && 'toString' in e - ? e.toString() - : typeof e === 'string' - ? e - : 'Unknown error', - ), - ); - Sentry.captureException('Error while setting up the Nuxt SDK'); - await abort('Exiting Wizard'); - } + clack.log.error( + 'Error while adding the Sentry Nuxt Module to the Nuxt config.', + ); + clack.log.info( + chalk.dim( + typeof e === 'object' && e != null && 'toString' in e + ? e.toString() + : typeof e === 'string' + ? e + : 'Unknown error', + ), + ); + Sentry.captureException( + 'Error while setting up the Nuxt Module in nuxt config', + ); + + clack.log.warn( + `Please add the following settings to ${chalk.cyan( + path.basename(config), + )}:`, + ); + // eslint-disable-next-line no-console + console.log( + `\n\n${getNuxtModuleFallbackTemplate(options, shouldTopLevelImport)}\n\n`, + ); } } @@ -234,11 +266,11 @@ export async function addNuxtOverrides( clack.log.warn( `To ensure Sentry can properly instrument your code it needs to add version overrides for some Nuxt dependencies${ isPNPM ? ` and install ${chalk.cyan('import-in-the-middle')}.` : '.' - }\n\nFor more info see: ${chalk.cyan( + }\n\nFor more info see: ${chalk.underline( 'https://github.com/getsentry/sentry-javascript/issues/14514', )}${ isPNPM - ? `\n\nand ${chalk.cyan( + ? `\n\nand ${chalk.underline( 'https://docs.sentry.io/platforms/javascript/guides/nuxt/troubleshooting/#pnpm-dev-cannot-find-package-import-in-the-middle', )}` : '' @@ -278,3 +310,36 @@ export async function addNuxtOverrides( } } } + +export async function confirmReadImportDocs( + deploymentPlatform: DeploymentPlatform | symbol, +) { + const canImportSentryServerConfigFile = + deploymentPlatform !== 'vercel' && deploymentPlatform !== 'netlify'; + + if (!canImportSentryServerConfigFile) { + // Nothing to do, users have been set up with automatic top-level-import instead + return; + } + + const docsUrl = + 'https://docs.sentry.io/platforms/javascript/guides/nuxt/install/cli-import/#initializing-sentry-with---import'; + + clack.log.info( + `After building your Nuxt app, you need to ${chalk.bold( + '--import', + )} the Sentry server config file when running your app.\n\nFor more info, see:\n\n${chalk.underline( + docsUrl, + )}`, + ); + + const shouldOpenDocs = await abortIfCancelled( + clack.confirm({ message: 'Do you want to open the docs?' }), + ); + + if (shouldOpenDocs) { + opn(docsUrl, { wait: false }).catch(() => { + // opn throws in environments that don't have a browser (e.g. remote shells) so we just noop here + }); + } +} diff --git a/src/nuxt/templates.ts b/src/nuxt/templates.ts index f14757d5..af3449c7 100644 --- a/src/nuxt/templates.ts +++ b/src/nuxt/templates.ts @@ -14,12 +14,15 @@ export default defineNuxtConfig({ `; } -export function getNuxtModuleFallbackTemplate(options: { - org: string; - project: string; - url: string; - selfHosted: boolean; -}): string { +export function getNuxtModuleFallbackTemplate( + options: { + org: string; + project: string; + url: string; + selfHosted: boolean; + }, + shouldTopLevelImport: boolean, +): string { return ` modules: ["@sentry/nuxt/module"], sentry: { sourceMapsUploadOptions: { @@ -27,7 +30,11 @@ export function getNuxtModuleFallbackTemplate(options: { project: "${options.project}",${ options.selfHosted ? `\n url: "${options.url}",` : '' } - }, + },${ + shouldTopLevelImport + ? `\n autoInjectServerSentry: "top-level-import",` + : '' + } }, sourcemap: { client: "hidden" },`; } diff --git a/src/nuxt/types.ts b/src/nuxt/types.ts new file mode 100644 index 00000000..62555c46 --- /dev/null +++ b/src/nuxt/types.ts @@ -0,0 +1,8 @@ +export const deploymentPlatforms = [ + 'vercel', + 'netlify', + 'other', + 'none', +] as const; + +export type DeploymentPlatform = (typeof deploymentPlatforms)[number]; diff --git a/src/nuxt/utils.ts b/src/nuxt/utils.ts index d90339e7..23497c8d 100644 --- a/src/nuxt/utils.ts +++ b/src/nuxt/utils.ts @@ -1,6 +1,9 @@ +// @ts-ignore - clack is ESM and TS complains about that. It works though +import * as clack from '@clack/prompts'; import { gte, minVersion } from 'semver'; // @ts-expect-error - magicast is ESM and TS complains about that. It works though import { loadFile } from 'magicast'; +import { abortIfCancelled } from '../utils/clack-utils'; export async function isNuxtV4( nuxtConfig: string, @@ -18,14 +21,21 @@ export async function isNuxtV4( // At the time of writing, nuxt 4 is not on its own // major yet. We must read the `compatibilityVersion` // from the nuxt config. - const mod = await loadFile(nuxtConfig); - const config = - mod.exports.default.$type === 'function-call' - ? mod.exports.default.$args[0] - : mod.exports.default; + try { + const mod = await loadFile(nuxtConfig); + const config = + mod.exports.default.$type === 'function-call' + ? mod.exports.default.$args[0] + : mod.exports.default; - if (config && config.future && config.future.compatibilityVersion === 4) { - return true; + if (config && config.future && config.future.compatibilityVersion === 4) { + return true; + } + } catch { + // If we cannot parse their config, just ask. + return await abortIfCancelled( + clack.confirm({ message: 'Are you using Nuxt version 4?' }), + ); } return false; diff --git a/src/utils/clack-utils.ts b/src/utils/clack-utils.ts index b31fcae3..c15d1827 100644 --- a/src/utils/clack-utils.ts +++ b/src/utils/clack-utils.ts @@ -20,7 +20,7 @@ import { import { debug } from './debug'; import { fulfillsVersionRange } from './semver'; -const opn = require('opn') as ( +export const opn = require('opn') as ( url: string, options?: { wait?: boolean; diff --git a/test/nuxt/templates.test.ts b/test/nuxt/templates.test.ts index 70a34dd6..71a7e917 100644 --- a/test/nuxt/templates.test.ts +++ b/test/nuxt/templates.test.ts @@ -206,12 +206,38 @@ describe('Nuxt code templates', () => { describe('getNuxtModuleFallbackTemplate', () => { it('generates configuration options for the nuxt config', () => { - const template = getNuxtModuleFallbackTemplate({ - org: 'my-org', - project: 'my-project', - url: 'https://sentry.io', - selfHosted: false, - }); + const template = getNuxtModuleFallbackTemplate( + { + org: 'my-org', + project: 'my-project', + url: 'https://sentry.io', + selfHosted: false, + }, + false, + ); + + expect(template).toMatchInlineSnapshot(` + " modules: ["@sentry/nuxt/module"], + sentry: { + sourceMapsUploadOptions: { + org: "my-org", + project: "my-project", + }, + }, + sourcemap: { client: "hidden" }," + `); + }); + + it('generates configuration options for the nuxt config with top level import', () => { + const template = getNuxtModuleFallbackTemplate( + { + org: 'my-org', + project: 'my-project', + url: 'https://sentry.io', + selfHosted: false, + }, + true, + ); expect(template).toMatchInlineSnapshot(` " modules: ["@sentry/nuxt/module"], @@ -220,6 +246,7 @@ describe('Nuxt code templates', () => { org: "my-org", project: "my-project", }, + autoInjectServerSentry: "top-level-import", }, sourcemap: { client: "hidden" }," `);