diff --git a/src/nuxt/nuxt-wizard.ts b/src/nuxt/nuxt-wizard.ts index e03cb9e8..ec7fe842 100644 --- a/src/nuxt/nuxt-wizard.ts +++ b/src/nuxt/nuxt-wizard.ts @@ -8,15 +8,25 @@ import { abort, abortIfCancelled, addDotEnvSentryBuildPluginFile, + askShouldCreateExampleComponent, + askShouldCreateExamplePage, confirmContinueIfNoOrDirtyGitRepo, ensurePackageIsInstalled, getOrAskForProjectData, getPackageDotJson, installPackage, printWelcome, + runPrettierIfInstalled, } from '../utils/clack-utils'; import { getPackageVersion, hasPackageInstalled } from '../utils/package-json'; import { addSDKModule, getNuxtConfig, createConfigFiles } from './sdk-setup'; +import { + createExampleComponent, + createExamplePage, + supportsExamplePage, +} from './sdk-example'; +import { isNuxtV4 } from './utils'; +import chalk from 'chalk'; export function runNuxtWizard(options: WizardOptions) { return withTelemetry( @@ -47,7 +57,7 @@ export async function runNuxtWizardWithTelemetry( const nuxtVersion = getPackageVersion('nuxt', packageJson); Sentry.setTag('nuxt-version', nuxtVersion); - const minVer = minVersion(nuxtVersion || 'none'); + const minVer = minVersion(nuxtVersion || '0.0.0'); if (!nuxtVersion || !minVer || lt(minVer, '3.13.2')) { clack.log.warn( @@ -87,13 +97,70 @@ export async function runNuxtWizardWithTelemetry( const nuxtConfig = await traceStep('load-nuxt-config', getNuxtConfig); - await traceStep('configure-sdk', async () => { - await addSDKModule(nuxtConfig, { - org: selectedProject.organization.slug, - project: selectedProject.slug, - url: selfHosted ? sentryUrl : undefined, - }); + const projectData = { + org: selectedProject.organization.slug, + project: selectedProject.slug, + projectId: selectedProject.id, + url: sentryUrl, + selfHosted, + }; + await traceStep('configure-sdk', async () => { + await addSDKModule(nuxtConfig, projectData); await createConfigFiles(selectedProject.keys[0].dsn.public); }); + + let shouldCreateExamplePage = false; + let shouldCreateExampleButton = false; + + const isV4 = await isNuxtV4(nuxtConfig, nuxtVersion); + const canCreateExamplePage = await supportsExamplePage(isV4); + Sentry.setTag('supports-example-page-creation', canCreateExamplePage); + + if (canCreateExamplePage) { + shouldCreateExamplePage = await askShouldCreateExamplePage(); + + if (shouldCreateExamplePage) { + await traceStep('create-example-page', async () => + createExamplePage(isV4, projectData), + ); + } + } else { + shouldCreateExampleButton = await askShouldCreateExampleComponent(); + + if (shouldCreateExampleButton) { + await traceStep('create-example-component', async () => + createExampleComponent(isV4), + ); + } + } + + await runPrettierIfInstalled(); + + clack.outro( + buildOutroMessage(shouldCreateExamplePage, shouldCreateExampleButton), + ); +} + +function buildOutroMessage( + shouldCreateExamplePage: boolean, + shouldCreateExampleButton: boolean, +): string { + let msg = chalk.green('\nSuccessfully installed the Sentry Nuxt SDK!'); + + if (shouldCreateExamplePage) { + msg += `\n\nYou can validate your setup by visiting ${chalk.cyan( + '"/sentry-example-page"', + )}.`; + } + if (shouldCreateExampleButton) { + msg += `\n\nYou can validate your setup by adding the ${chalk.cyan( + '`SentryExampleButton`', + )} 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/`; + + return msg; } diff --git a/src/nuxt/sdk-example.ts b/src/nuxt/sdk-example.ts new file mode 100644 index 00000000..7f7ae1aa --- /dev/null +++ b/src/nuxt/sdk-example.ts @@ -0,0 +1,135 @@ +import * as fs from 'fs'; +import * as path from 'path'; +// @ts-expect-error - clack is ESM and TS complains about that. It works though +import clack from '@clack/prompts'; +import { + getIndexRouteTemplate, + getSentryExampleApiTemplate, + getSentryExamplePageTemplate, + getSentryErrorButtonTemplate, +} from './templates'; +import { abort, isUsingTypeScript } from '../utils/clack-utils'; +import chalk from 'chalk'; +import * as Sentry from '@sentry/node'; + +function getSrcDirectory(isNuxtV4: boolean) { + // In nuxt v4, the src directory is `app/` unless + // users already had a `pages` directory + return isNuxtV4 && !fs.existsSync(path.resolve('pages')) ? 'app' : '.'; +} + +export async function supportsExamplePage(isNuxtV4: boolean) { + // We currently only support creating an example page + // if users can reliably access it without having to + // add code changes themselves. + // + // If users have an `app.vue` layout without the + // needed component to render routes (), + // we bail out of creating an example page altogether. + const src = getSrcDirectory(isNuxtV4); + const app = path.join(src, 'app.vue'); + + // If there's no `app.vue` layout, nuxt automatically renders + // the routes. + if (!fs.existsSync(path.resolve(app))) { + return true; + } + + const content = await fs.promises.readFile(path.resolve(app), 'utf8'); + return !!content.match(/ { export async function addSDKModule( config: string, - options: { org: string; project: string; url?: string }, + options: { org: string; project: string; url: string; selfHosted: boolean }, ): Promise { clack.log.info('Adding Sentry Nuxt Module to Nuxt config.'); @@ -66,7 +66,7 @@ export async function addSDKModule( sourceMapsUploadOptions: { org: options.org, project: options.project, - ...(options.url && { url: options.url }), + ...(options.selfHosted && { url: options.url }), }, }); addNuxtModule(mod, '@sentry/nuxt/module', 'sourcemap', { client: true }); diff --git a/src/nuxt/templates.ts b/src/nuxt/templates.ts index dd445ce8..72dd3b62 100644 --- a/src/nuxt/templates.ts +++ b/src/nuxt/templates.ts @@ -1,3 +1,4 @@ +import { getIssueStreamUrl } from '../utils/url'; type SelectedSentryFeatures = { performance: boolean; replay: boolean; @@ -103,3 +104,174 @@ Sentry.init({ }); `; } + +export function getIndexRouteTemplate(): string { + return ` + +`; +} + +export function getSentryExamplePageTemplate(options: { + url: string; + org: string; + projectId: string; +}): string { + const { url, org, projectId } = options; + const issuesPageLink = getIssueStreamUrl({ url, orgSlug: org, projectId }); + + return ` + + + + + + +`; +} + +export function getSentryExampleApiTemplate() { + return `// This is just a very simple API route that throws an example error. +// Feel free to delete this file. +import { defineEventHandler } from '#imports'; + +export default defineEventHandler(event => { + throw new Error("Sentry Example API Route Error"); +}); +`; +} + +export function getSentryErrorButtonTemplate() { + return ` + + + + + + +`; +} diff --git a/src/nuxt/utils.ts b/src/nuxt/utils.ts new file mode 100644 index 00000000..d90339e7 --- /dev/null +++ b/src/nuxt/utils.ts @@ -0,0 +1,32 @@ +import { gte, minVersion } from 'semver'; +// @ts-expect-error - magicast is ESM and TS complains about that. It works though +import { loadFile } from 'magicast'; + +export async function isNuxtV4( + nuxtConfig: string, + packageVersion: string | undefined, +) { + if (!packageVersion) { + return false; + } + + const minVer = minVersion(packageVersion); + if (minVer && gte(minVer, '4.0.0')) { + return true; + } + + // 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; + + if (config && config.future && config.future.compatibilityVersion === 4) { + return true; + } + + return false; +} diff --git a/src/utils/clack-utils.ts b/src/utils/clack-utils.ts index e172da22..9a347067 100644 --- a/src/utils/clack-utils.ts +++ b/src/utils/clack-utils.ts @@ -1415,6 +1415,24 @@ export async function askShouldCreateExamplePage( ); } +export async function askShouldCreateExampleComponent(): Promise { + return traceStep('ask-create-example-component', () => + abortIfCancelled( + clack.select({ + message: `Do you want to create an example component to test your Sentry setup?`, + options: [ + { + value: true, + label: 'Yes', + hint: 'Recommended - Check your git status before committing!', + }, + { value: false, label: 'No' }, + ], + }), + ), + ); +} + export async function featureSelectionPrompt>( features: F, ): Promise<{ [key in F[number]['id']]: boolean }> {