From c336430725cc0ed0a59ff0e1f31eecc519b9727e Mon Sep 17 00:00:00 2001 From: Andrei <168741329+andreiborza@users.noreply.github.com> Date: Fri, 15 Nov 2024 09:20:22 +0100 Subject: [PATCH 1/9] feat(nuxt): Add Nuxt menu option and install SDK (#711) * feat(nuxt): Add Nuxt menu option and install SDK * Mark sentry dot env file in cyan color --- src/nuxt/nuxt-wizard.ts | 88 ++++++++++++++++++++++++++++++++++++++++ src/run.ts | 7 ++++ src/utils/clack-utils.ts | 12 +++--- 3 files changed, 102 insertions(+), 5 deletions(-) create mode 100644 src/nuxt/nuxt-wizard.ts diff --git a/src/nuxt/nuxt-wizard.ts b/src/nuxt/nuxt-wizard.ts new file mode 100644 index 00000000..d0414811 --- /dev/null +++ b/src/nuxt/nuxt-wizard.ts @@ -0,0 +1,88 @@ +// @ts-ignore - clack is ESM and TS complains about that. It works though +import * as clack from '@clack/prompts'; +import * as Sentry from '@sentry/node'; +import { lt, minVersion } from 'semver'; +import type { WizardOptions } from '../utils/types'; +import { withTelemetry } from '../telemetry'; +import { + abort, + abortIfCancelled, + addDotEnvSentryBuildPluginFile, + confirmContinueIfNoOrDirtyGitRepo, + ensurePackageIsInstalled, + getOrAskForProjectData, + getPackageDotJson, + installPackage, + printWelcome, +} from '../utils/clack-utils'; +import { getPackageVersion, hasPackageInstalled } from '../utils/package-json'; + +export function runNuxtWizard(options: WizardOptions) { + return withTelemetry( + { + enabled: options.telemetryEnabled, + integration: 'nuxt', + wizardOptions: options, + }, + () => runNuxtWizardWithTelemetry(options), + ); +} + +export async function runNuxtWizardWithTelemetry( + options: WizardOptions, +): Promise { + printWelcome({ + wizardName: 'Sentry Nuxt Wizard', + promoCode: options.promoCode, + telemetryEnabled: options.telemetryEnabled, + }); + + await confirmContinueIfNoOrDirtyGitRepo(); + + const packageJson = await getPackageDotJson(); + + await ensurePackageIsInstalled(packageJson, 'nuxt', 'Nuxt'); + + const nuxtVersion = getPackageVersion('nuxt', packageJson); + Sentry.setTag('nuxt-version', nuxtVersion); + + const minVer = minVersion(nuxtVersion || 'none'); + + if (!nuxtVersion || !minVer || lt(minVer, '3.13.2')) { + clack.log.warn( + "It seems you're using a Nuxt version <3.13.2 which is not supported by Sentry.\nWe recommend upgrading to the latest version before you continue.", + ); + const shouldContinue = await abortIfCancelled( + clack.select({ + message: 'Do you want to continue anyway?', + options: [ + { + label: 'Yes, continue', + hint: 'The SDK might not work correctly', + value: true, + }, + { label: "No, I'll upgrade first", value: false }, + ], + }), + ); + if (!shouldContinue) { + await abort('Exiting Wizard', 0); + return; + } + } + + const { authToken } = await getOrAskForProjectData( + options, + 'javascript-nuxt', + ); + + const sdkAlreadyInstalled = hasPackageInstalled('@sentry/nuxt', packageJson); + Sentry.setTag('sdk-already-installed', sdkAlreadyInstalled); + + await installPackage({ + packageName: '@sentry/nuxt', + alreadyInstalled: sdkAlreadyInstalled, + }); + + await addDotEnvSentryBuildPluginFile(authToken); +} diff --git a/src/run.ts b/src/run.ts index 682a4bde..a0dc6192 100644 --- a/src/run.ts +++ b/src/run.ts @@ -8,6 +8,7 @@ import type { PreselectedProject, WizardOptions } from './utils/types'; import { runAndroidWizard } from './android/android-wizard'; import { runAppleWizard } from './apple/apple-wizard'; import { runNextjsWizard } from './nextjs/nextjs-wizard'; +import { runNuxtWizard } from './nuxt/nuxt-wizard'; import { runRemixWizard } from './remix/remix-wizard'; import { runSvelteKitWizard } from './sveltekit/sveltekit-wizard'; import { runSourcemapsWizard } from './sourcemaps/sourcemaps-wizard'; @@ -22,6 +23,7 @@ type WizardIntegration = | 'cordova' | 'electron' | 'nextjs' + | 'nuxt' | 'remix' | 'sveltekit' | 'sourcemaps'; @@ -103,6 +105,7 @@ export async function run(argv: Args) { { value: 'cordova', label: 'Cordova' }, { value: 'electron', label: 'Electron' }, { value: 'nextjs', label: 'Next.js' }, + { value: 'nuxt', label: 'Nuxt' }, { value: 'remix', label: 'Remix' }, { value: 'sveltekit', label: 'SvelteKit' }, { value: 'sourcemaps', label: 'Configure Source Maps Upload' }, @@ -148,6 +151,10 @@ export async function run(argv: Args) { await runNextjsWizard(wizardOptions); break; + case 'nuxt': + await runNuxtWizard(wizardOptions); + break; + case 'remix': await runRemixWizard(wizardOptions); break; diff --git a/src/utils/clack-utils.ts b/src/utils/clack-utils.ts index 7fd35ee3..ff2f6524 100644 --- a/src/utils/clack-utils.ts +++ b/src/utils/clack-utils.ts @@ -597,7 +597,7 @@ SENTRY_AUTH_TOKEN=${authToken} if (hasAuthToken) { clack.log.warn( - `${chalk.bold( + `${chalk.bold.cyan( SENTRY_DOT_ENV_FILE, )} already has auth token. Will not add one.`, ); @@ -612,11 +612,11 @@ SENTRY_AUTH_TOKEN=${authToken} }, ); clack.log.success( - `Added auth token to ${chalk.bold(SENTRY_DOT_ENV_FILE)}`, + `Added auth token to ${chalk.bold.cyan(SENTRY_DOT_ENV_FILE)}`, ); } catch { clack.log.warning( - `Failed to add auth token to ${chalk.bold( + `Failed to add auth token to ${chalk.bold.cyan( SENTRY_DOT_ENV_FILE, )}. Uploading source maps during build will likely not work locally.`, ); @@ -629,13 +629,13 @@ SENTRY_AUTH_TOKEN=${authToken} flag: 'w', }); clack.log.success( - `Created ${chalk.bold( + `Created ${chalk.bold.cyan( SENTRY_DOT_ENV_FILE, )} with auth token for you to test source map uploading locally.`, ); } catch { clack.log.warning( - `Failed to create ${chalk.bold( + `Failed to create ${chalk.bold.cyan( SENTRY_DOT_ENV_FILE, )} with auth token. Uploading source maps during build will likely not work locally.`, ); @@ -854,6 +854,7 @@ export async function getOrAskForProjectData( options: WizardOptions, platform?: | 'javascript-nextjs' + | 'javascript-nuxt' | 'javascript-remix' | 'javascript-sveltekit' | 'apple-ios' @@ -1015,6 +1016,7 @@ async function askForWizardLogin(options: { promoCode?: string; platform?: | 'javascript-nextjs' + | 'javascript-nuxt' | 'javascript-remix' | 'javascript-sveltekit' | 'apple-ios' From 602a6f9be73bc8a2faeec9aa6abac5016d3b8850 Mon Sep 17 00:00:00 2001 From: Andrei <168741329+andreiborza@users.noreply.github.com> Date: Fri, 22 Nov 2024 14:58:47 +0100 Subject: [PATCH 2/9] feat(nuxt): Add sdk module to nuxt config and create client and server config files (#713) * feat(nuxt): Add sdk module to nuxt config and create client and server config files * Log snippets when user denies overwriting their configs --- lib/Constants.ts | 1 + src/nuxt/nuxt-wizard.ts | 21 +++- src/nuxt/sdk-setup.ts | 191 +++++++++++++++++++++++++++++++++ src/nuxt/templates.ts | 105 ++++++++++++++++++ test/nuxt/templates.test.ts | 205 ++++++++++++++++++++++++++++++++++++ 5 files changed, 518 insertions(+), 5 deletions(-) create mode 100644 src/nuxt/sdk-setup.ts create mode 100644 src/nuxt/templates.ts create mode 100644 test/nuxt/templates.test.ts diff --git a/lib/Constants.ts b/lib/Constants.ts index 27f515c0..4350d9a0 100644 --- a/lib/Constants.ts +++ b/lib/Constants.ts @@ -6,6 +6,7 @@ export enum Integration { cordova = 'cordova', electron = 'electron', nextjs = 'nextjs', + nuxt = 'nuxt', remix = 'remix', sveltekit = 'sveltekit', sourcemaps = 'sourcemaps', diff --git a/src/nuxt/nuxt-wizard.ts b/src/nuxt/nuxt-wizard.ts index d0414811..e03cb9e8 100644 --- a/src/nuxt/nuxt-wizard.ts +++ b/src/nuxt/nuxt-wizard.ts @@ -3,7 +3,7 @@ import * as clack from '@clack/prompts'; import * as Sentry from '@sentry/node'; import { lt, minVersion } from 'semver'; import type { WizardOptions } from '../utils/types'; -import { withTelemetry } from '../telemetry'; +import { traceStep, withTelemetry } from '../telemetry'; import { abort, abortIfCancelled, @@ -16,6 +16,7 @@ import { printWelcome, } from '../utils/clack-utils'; import { getPackageVersion, hasPackageInstalled } from '../utils/package-json'; +import { addSDKModule, getNuxtConfig, createConfigFiles } from './sdk-setup'; export function runNuxtWizard(options: WizardOptions) { return withTelemetry( @@ -71,10 +72,8 @@ export async function runNuxtWizardWithTelemetry( } } - const { authToken } = await getOrAskForProjectData( - options, - 'javascript-nuxt', - ); + const { authToken, selectedProject, selfHosted, sentryUrl } = + await getOrAskForProjectData(options, 'javascript-nuxt'); const sdkAlreadyInstalled = hasPackageInstalled('@sentry/nuxt', packageJson); Sentry.setTag('sdk-already-installed', sdkAlreadyInstalled); @@ -85,4 +84,16 @@ export async function runNuxtWizardWithTelemetry( }); await addDotEnvSentryBuildPluginFile(authToken); + + 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, + }); + + await createConfigFiles(selectedProject.keys[0].dsn.public); + }); } diff --git a/src/nuxt/sdk-setup.ts b/src/nuxt/sdk-setup.ts new file mode 100644 index 00000000..0d0ce285 --- /dev/null +++ b/src/nuxt/sdk-setup.ts @@ -0,0 +1,191 @@ +// @ts-expect-error - clack is ESM and TS complains about that. It works though +import 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 } 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'; +import { + getConfigBody, + getDefaultNuxtConfig, + getSentryConfigContents, +} from './templates'; +import { + abort, + abortIfCancelled, + featureSelectionPrompt, + isUsingTypeScript, +} from '../utils/clack-utils'; +import { traceStep } from '../telemetry'; + +const possibleNuxtConfig = [ + 'nuxt.config.js', + 'nuxt.config.mjs', + 'nuxt.config.cjs', + 'nuxt.config.ts', + 'nuxt.config.mts', + 'nuxt.config.cts', +]; + +export async function getNuxtConfig(): Promise { + let configFile = possibleNuxtConfig.find((fileName) => + fs.existsSync(path.join(process.cwd(), fileName)), + ); + + if (!configFile) { + clack.log.info('No Nuxt config file found, creating a new one.'); + Sentry.setTag('nuxt-config-strategy', 'create'); + // nuxt recommends its config to be .ts by default + configFile = 'nuxt.config.ts'; + + await fs.promises.writeFile( + path.join(process.cwd(), configFile), + getDefaultNuxtConfig(), + { encoding: 'utf-8', flag: 'w' }, + ); + + clack.log.success(`Created ${chalk.cyan('nuxt.config.ts')}.`); + } + + return path.join(process.cwd(), configFile); +} + +export async function addSDKModule( + config: string, + options: { org: string; project: string; url?: string }, +): Promise { + clack.log.info('Adding Sentry Nuxt Module to Nuxt config.'); + + try { + const mod = await loadFile(config); + + addNuxtModule(mod, '@sentry/nuxt/module', 'sentry', { + sourceMapsUploadOptions: { + org: options.org, + project: options.project, + ...(options.url && { url: options.url }), + }, + }); + addNuxtModule(mod, '@sentry/nuxt/module', 'sourcemap', { client: true }); + + const { code } = generateCode(mod); + + await fs.promises.writeFile(config, code, { encoding: 'utf-8', flag: 'w' }); + } catch (e: unknown) { + 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'); + } +} + +export async function createConfigFiles(dsn: string) { + const selectedFeatures = await featureSelectionPrompt([ + { + id: 'performance', + prompt: `Do you want to enable ${chalk.bold( + 'Tracing', + )} to track the performance of your application?`, + enabledHint: 'recommended', + }, + { + id: 'replay', + prompt: `Do you want to enable ${chalk.bold( + 'Sentry Session Replay', + )} to get a video-like reproduction of errors during a user session?`, + enabledHint: 'recommended, but increases bundle size', + }, + ] as const); + + const typeScriptDetected = isUsingTypeScript(); + + const configVariants = ['server', 'client'] as const; + + 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 jsConfigExists = fs.existsSync(path.join(process.cwd(), jsConfig)); + const tsConfigExists = fs.existsSync(path.join(process.cwd(), tsConfig)); + + let shouldWriteFile = true; + + if (jsConfigExists || tsConfigExists) { + const existingConfigs = []; + + if (jsConfigExists) { + existingConfigs.push(jsConfig); + } + + if (tsConfigExists) { + 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.cyan(jsConfig)}.`); + } + if (tsConfigExists) { + fs.unlinkSync(path.join(process.cwd(), tsConfig)); + clack.log.warn(`Removed existing ${chalk.cyan(tsConfig)}.`); + } + } + } + + if (shouldWriteFile) { + await fs.promises.writeFile( + path.join(process.cwd(), typeScriptDetected ? tsConfig : jsConfig), + getSentryConfigContents(dsn, configVariant, selectedFeatures), + { encoding: 'utf8', flag: 'w' }, + ); + clack.log.success( + `Created fresh ${chalk.cyan( + typeScriptDetected ? tsConfig : jsConfig, + )}.`, + ); + Sentry.setTag(`created-${configVariant}-config`, true); + } else { + clack.log.info( + `Okay, here are the changes your ${chalk.cyan( + typeScriptDetected ? tsConfig : jsConfig, + )} should contain:`, + ); + // eslint-disable-next-line no-console + console.log( + '\n\n ' + + getConfigBody(dsn, configVariant, selectedFeatures) + + '\n\n', + ); + } + }); + } +} diff --git a/src/nuxt/templates.ts b/src/nuxt/templates.ts new file mode 100644 index 00000000..dd445ce8 --- /dev/null +++ b/src/nuxt/templates.ts @@ -0,0 +1,105 @@ +type SelectedSentryFeatures = { + performance: boolean; + replay: boolean; +}; + +export function getDefaultNuxtConfig(): string { + return `// https://nuxt.com/docs/api/configuration/nuxt-config +export default defineNuxtConfig({ + compatibilityDate: '2024-04-03', + devtools: { enabled: true } +}) +`; +} + +export function getSentryConfigContents( + dsn: string, + config: 'client' | 'server', + selectedFeatures: SelectedSentryFeatures, +): string { + if (config === 'client') { + return getSentryClientConfigContents(dsn, selectedFeatures); + } + + return getSentryServerConfigContents(dsn, selectedFeatures); +} + +const featuresConfigMap: Record = { + performance: [ + ' // We recommend adjusting this value in production, or using tracesSampler', + ' // for finer control', + ' tracesSampleRate: 1.0,', + ].join('\n'), + replay: [ + ' // This sets the sample rate to be 10%. You may want this to be 100% while', + ' // in development and sample at a lower rate in production', + ' replaysSessionSampleRate: 0.1,', + ' ', + ' // If the entire session is not sampled, use the below sample rate to sample', + ' // sessions when an error occurs.', + ' replaysOnErrorSampleRate: 1.0,', + ' ', + " // If you don't want to use Session Replay, just remove the line below:", + ' integrations: [Sentry.replayIntegration()],', + ].join('\n'), +}; + +const featuresMap: Record< + 'client' | 'server', + Array +> = { + client: ['performance', 'replay'], + server: ['performance'], +}; + +export function getConfigBody( + dsn: string, + variant: 'client' | 'server', + selectedFeatures: SelectedSentryFeatures, +) { + return [ + `dsn: "${dsn}",`, + Object.entries(selectedFeatures) + .map(([feature, activated]: [keyof SelectedSentryFeatures, boolean]) => { + return featuresMap[variant].includes(feature) && activated + ? featuresConfigMap[feature] + : null; + }) + .filter(Boolean) + .join('\n\n'), + ] + .filter(Boolean) + .join('\n\n'); +} + +function getSentryClientConfigContents( + dsn: string, + selectedFeatures: SelectedSentryFeatures, +): string { + return `import * as Sentry from "@sentry/nuxt"; + +Sentry.init({ + // If set up, you can use your runtime config here + // dsn: useRuntimeConfig().public.sentry.dsn, + ${getConfigBody(dsn, 'client', selectedFeatures)} + + // Setting this option to true will print useful information to the console while you're setting up Sentry. + debug: false, +}); +`; +} + +function getSentryServerConfigContents( + dsn: string, + selectedFeatures: SelectedSentryFeatures, +): string { + return `import * as Sentry from "@sentry/nuxt"; + +Sentry.init({ + ${getConfigBody(dsn, 'server', selectedFeatures)} + + // Setting this option to true will print useful information to the console while you're setting up Sentry. + debug: false, +}); +`; +} diff --git a/test/nuxt/templates.test.ts b/test/nuxt/templates.test.ts new file mode 100644 index 00000000..e73f90a0 --- /dev/null +++ b/test/nuxt/templates.test.ts @@ -0,0 +1,205 @@ +import { + getDefaultNuxtConfig, + getSentryConfigContents, +} from '../../src/nuxt/templates'; + +describe('Nuxt code templates', () => { + describe('getDefaultNuxtConfig', () => { + it('returns a default nuxt config', () => { + expect(getDefaultNuxtConfig()).toMatchInlineSnapshot(` + "// https://nuxt.com/docs/api/configuration/nuxt-config + export default defineNuxtConfig({ + compatibilityDate: '2024-04-03', + devtools: { enabled: true } + }) + " +`); + }); + }); + + describe('getSentryConfigContents', () => { + describe('client config', () => { + it('generates Sentry config with all features enabled', () => { + const template = getSentryConfigContents( + 'https://sentry.io/123', + 'client', + { + performance: true, + replay: true, + }, + ); + + expect(template).toMatchInlineSnapshot(` + "import * as Sentry from "@sentry/nuxt"; + + Sentry.init({ + // If set up, you can use your runtime config here + // dsn: useRuntimeConfig().public.sentry.dsn, + dsn: "https://sentry.io/123", + + // We recommend adjusting this value in production, or using tracesSampler + // for finer control + tracesSampleRate: 1.0, + + // This sets the sample rate to be 10%. You may want this to be 100% while + // in development and sample at a lower rate in production + replaysSessionSampleRate: 0.1, + + // If the entire session is not sampled, use the below sample rate to sample + // sessions when an error occurs. + replaysOnErrorSampleRate: 1.0, + + // If you don't want to use Session Replay, just remove the line below: + integrations: [Sentry.replayIntegration()], + + // Setting this option to true will print useful information to the console while you're setting up Sentry. + debug: false, + }); + " + `); + }); + + it('generates Sentry config with performance monitoring disabled', () => { + const template = getSentryConfigContents( + 'https://sentry.io/123', + 'client', + { + performance: false, + replay: true, + }, + ); + + expect(template).toMatchInlineSnapshot(` + "import * as Sentry from "@sentry/nuxt"; + + Sentry.init({ + // If set up, you can use your runtime config here + // dsn: useRuntimeConfig().public.sentry.dsn, + dsn: "https://sentry.io/123", + + // This sets the sample rate to be 10%. You may want this to be 100% while + // in development and sample at a lower rate in production + replaysSessionSampleRate: 0.1, + + // If the entire session is not sampled, use the below sample rate to sample + // sessions when an error occurs. + replaysOnErrorSampleRate: 1.0, + + // If you don't want to use Session Replay, just remove the line below: + integrations: [Sentry.replayIntegration()], + + // Setting this option to true will print useful information to the console while you're setting up Sentry. + debug: false, + }); + " + `); + }); + + it('generates Sentry config with session replay disabled', () => { + const template = getSentryConfigContents( + 'https://sentry.io/123', + 'client', + { + performance: true, + replay: false, + }, + ); + + expect(template).toMatchInlineSnapshot(` + "import * as Sentry from "@sentry/nuxt"; + + Sentry.init({ + // If set up, you can use your runtime config here + // dsn: useRuntimeConfig().public.sentry.dsn, + dsn: "https://sentry.io/123", + + // We recommend adjusting this value in production, or using tracesSampler + // for finer control + tracesSampleRate: 1.0, + + // Setting this option to true will print useful information to the console while you're setting up Sentry. + debug: false, + }); + " + `); + }); + + it('generates Sentry config with performance monitoring and session replay disabled', () => { + const template = getSentryConfigContents( + 'https://sentry.io/123', + 'client', + { + performance: false, + replay: false, + }, + ); + + expect(template).toMatchInlineSnapshot(` + "import * as Sentry from "@sentry/nuxt"; + + Sentry.init({ + // If set up, you can use your runtime config here + // dsn: useRuntimeConfig().public.sentry.dsn, + dsn: "https://sentry.io/123", + + // Setting this option to true will print useful information to the console while you're setting up Sentry. + debug: false, + }); + " + `); + }); + }); + + describe('server config', () => { + it('generates Sentry config with all features enabled', () => { + const template = getSentryConfigContents( + 'https://sentry.io/123', + 'server', + { + performance: true, + replay: true, + }, + ); + + expect(template).toMatchInlineSnapshot(` + "import * as Sentry from "@sentry/nuxt"; + + Sentry.init({ + dsn: "https://sentry.io/123", + + // We recommend adjusting this value in production, or using tracesSampler + // for finer control + tracesSampleRate: 1.0, + + // Setting this option to true will print useful information to the console while you're setting up Sentry. + debug: false, + }); + " + `); + }); + + it('generates Sentry config with performance monitoring disabled', () => { + const template = getSentryConfigContents( + 'https://sentry.io/123', + 'server', + { + performance: false, + replay: true, + }, + ); + + expect(template).toMatchInlineSnapshot(` + "import * as Sentry from "@sentry/nuxt"; + + Sentry.init({ + dsn: "https://sentry.io/123", + + // Setting this option to true will print useful information to the console while you're setting up Sentry. + debug: false, + }); + " + `); + }); + }); + }); +}); From bffd18b5a74f14faa3b0dae04be747d6e860fba6 Mon Sep 17 00:00:00 2001 From: Andrei <168741329+andreiborza@users.noreply.github.com> Date: Fri, 22 Nov 2024 15:11:25 +0100 Subject: [PATCH 3/9] feat(nuxt): Add example page/component creation and final messaging (#717) --- src/nuxt/nuxt-wizard.ts | 81 ++++++++++++++++-- src/nuxt/sdk-example.ts | 135 ++++++++++++++++++++++++++++++ src/nuxt/sdk-setup.ts | 4 +- src/nuxt/templates.ts | 172 +++++++++++++++++++++++++++++++++++++++ src/nuxt/utils.ts | 32 ++++++++ src/utils/clack-utils.ts | 18 ++++ 6 files changed, 433 insertions(+), 9 deletions(-) create mode 100644 src/nuxt/sdk-example.ts create mode 100644 src/nuxt/utils.ts 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 ff2f6524..5a398536 100644 --- a/src/utils/clack-utils.ts +++ b/src/utils/clack-utils.ts @@ -1419,6 +1419,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 }> { From 011afbe0222eec10fd73260fb5e009ce8555539a Mon Sep 17 00:00:00 2001 From: Andrei <168741329+andreiborza@users.noreply.github.com> Date: Mon, 25 Nov 2024 13:22:28 +0100 Subject: [PATCH 4/9] feat(nuxt): Add nuxt 3 and 4 e2e test apps (#718) * feat(nuxt): Add nuxt-3 e2e tests * Add nuxt-3 e2e tests * Add describe level * Add nuxt-4 e2e test app * Add nuxt 4 e2e test app * Remove leaking test helper --- CHANGELOG.md | 6 + .../nuxt-3-test-app/.gitignore | 24 +++ .../nuxt-3-test-app/README.md | 75 ++++++++ .../nuxt-3-test-app/nuxt.config.ts | 5 + .../nuxt-3-test-app/package.json | 18 ++ .../nuxt-3-test-app/public/favicon.ico | Bin 0 -> 4286 bytes .../nuxt-3-test-app/public/robots.txt | 1 + .../nuxt-3-test-app/server/tsconfig.json | 3 + .../nuxt-3-test-app/tsconfig.json | 4 + e2e-tests/test-applications/nuxt-4-test-app | 1 + e2e-tests/tests/nuxt-3.test.ts | 169 ++++++++++++++++++ e2e-tests/tests/nuxt-4.test.ts | 168 +++++++++++++++++ 12 files changed, 474 insertions(+) create mode 100644 e2e-tests/test-applications/nuxt-3-test-app/.gitignore create mode 100644 e2e-tests/test-applications/nuxt-3-test-app/README.md create mode 100644 e2e-tests/test-applications/nuxt-3-test-app/nuxt.config.ts create mode 100644 e2e-tests/test-applications/nuxt-3-test-app/package.json create mode 100644 e2e-tests/test-applications/nuxt-3-test-app/public/favicon.ico create mode 100644 e2e-tests/test-applications/nuxt-3-test-app/public/robots.txt create mode 100644 e2e-tests/test-applications/nuxt-3-test-app/server/tsconfig.json create mode 100644 e2e-tests/test-applications/nuxt-3-test-app/tsconfig.json create mode 160000 e2e-tests/test-applications/nuxt-4-test-app create mode 100644 e2e-tests/tests/nuxt-3.test.ts create mode 100644 e2e-tests/tests/nuxt-4.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e34f455..b9c54d9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,13 @@ - feat: Pin JS SDK versions to v8 (#712) - Remove enableTracing for Cocoa ([#715](https://github.com/getsentry/sentry-wizard/pull/715)) +- feat(nuxt): Add nuxt wizard ([#719](https://github.com/getsentry/sentry-wizard/pull/719)) + +Set up the Sentry Nuxt SDK in your app with one command: +```sh +npx @sentry/wizard@latest -i nuxt +``` ## 3.34.4 diff --git a/e2e-tests/test-applications/nuxt-3-test-app/.gitignore b/e2e-tests/test-applications/nuxt-3-test-app/.gitignore new file mode 100644 index 00000000..4a7f73a2 --- /dev/null +++ b/e2e-tests/test-applications/nuxt-3-test-app/.gitignore @@ -0,0 +1,24 @@ +# Nuxt dev/build outputs +.output +.data +.nuxt +.nitro +.cache +dist + +# Node dependencies +node_modules + +# Logs +logs +*.log + +# Misc +.DS_Store +.fleet +.idea + +# Local env files +.env +.env.* +!.env.example diff --git a/e2e-tests/test-applications/nuxt-3-test-app/README.md b/e2e-tests/test-applications/nuxt-3-test-app/README.md new file mode 100644 index 00000000..25b58212 --- /dev/null +++ b/e2e-tests/test-applications/nuxt-3-test-app/README.md @@ -0,0 +1,75 @@ +# Nuxt Minimal Starter + +Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more. + +## Setup + +Make sure to install dependencies: + +```bash +# npm +npm install + +# pnpm +pnpm install + +# yarn +yarn install + +# bun +bun install +``` + +## Development Server + +Start the development server on `http://localhost:3000`: + +```bash +# npm +npm run dev + +# pnpm +pnpm dev + +# yarn +yarn dev + +# bun +bun run dev +``` + +## Production + +Build the application for production: + +```bash +# npm +npm run build + +# pnpm +pnpm build + +# yarn +yarn build + +# bun +bun run build +``` + +Locally preview production build: + +```bash +# npm +npm run preview + +# pnpm +pnpm preview + +# yarn +yarn preview + +# bun +bun run preview +``` + +Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information. diff --git a/e2e-tests/test-applications/nuxt-3-test-app/nuxt.config.ts b/e2e-tests/test-applications/nuxt-3-test-app/nuxt.config.ts new file mode 100644 index 00000000..8ae12e6c --- /dev/null +++ b/e2e-tests/test-applications/nuxt-3-test-app/nuxt.config.ts @@ -0,0 +1,5 @@ +// https://nuxt.com/docs/api/configuration/nuxt-config +export default defineNuxtConfig({ + compatibilityDate: '2024-11-01', + devtools: { enabled: true } +}) diff --git a/e2e-tests/test-applications/nuxt-3-test-app/package.json b/e2e-tests/test-applications/nuxt-3-test-app/package.json new file mode 100644 index 00000000..3ca1ecf9 --- /dev/null +++ b/e2e-tests/test-applications/nuxt-3-test-app/package.json @@ -0,0 +1,18 @@ +{ + "name": "nuxt-app", + "private": true, + "type": "module", + "scripts": { + "build": "nuxt build", + "dev": "nuxt dev", + "generate": "nuxt generate", + "preview": "nuxt preview", + "postinstall": "nuxt prepare", + "start": "node .output/server/index.mjs" + }, + "dependencies": { + "nuxt": "^3.14.1592", + "vue": "latest", + "vue-router": "latest" + } +} diff --git a/e2e-tests/test-applications/nuxt-3-test-app/public/favicon.ico b/e2e-tests/test-applications/nuxt-3-test-app/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..18993ad91cfd43e03b074dd0b5cc3f37ab38e49c GIT binary patch literal 4286 zcmeHLOKuuL5PjK%MHWVi6lD zOGiREbCw`xmFozJ^aNatJY>w+g ze6a2@u~m#^BZm@8wco9#Crlli0uLb^3E$t2-WIc^#(?t)*@`UpuofJ(Uyh@F>b3Ph z$D^m8Xq~pTkGJ4Q`Q2)te3mgkWYZ^Ijq|hkiP^9`De={bQQ%heZC$QU2UpP(-tbl8 zPWD2abEew;oat@w`uP3J^YpsgT%~jT(Dk%oU}sa$7|n6hBjDj`+I;RX(>)%lm_7N{+B7Mu%H?422lE%MBJH!!YTN2oT7xr>>N-8OF$C&qU^ z>vLsa{$0X%q1fjOe3P1mCv#lN{xQ4_*HCSAZjTb1`}mlc+9rl8$B3OP%VT@mch_~G z7Y+4b{r>9e=M+7vSI;BgB?ryZDY4m>&wcHSn81VH1N~`0gvwH{ z8dv#hG|OK`>1;j7tM#B)Z7zDN?{6=dUal}$e { + const projectDir = path.resolve( + __dirname, + '../test-applications/nuxt-3-test-app', + ); + + beforeAll(async () => { + await runWizardOnNuxtProject(projectDir); + }); + + afterAll(() => { + revertLocalChanges(projectDir); + cleanupGit(projectDir); + }); + + testNuxtProjectSetup(projectDir); + + testNuxtProjectConfigs(projectDir); + + testNuxtProjectBuildsAndRuns(projectDir); +}); + +async function runWizardOnNuxtProject(projectDir: string): Promise { + const integration = Integration.nuxt; + + const wizardInstance = startWizardInstance(integration, projectDir); + const packageManagerPrompted = await wizardInstance.waitForOutput( + 'Please select your package manager.', + ); + + const tracingOptionPrompted = + packageManagerPrompted && + (await wizardInstance.sendStdinAndWaitForOutput( + // Selecting `yarn` as the package manager + [KEYS.DOWN, KEYS.ENTER], + // "Do you want to enable Tracing", sometimes doesn't work as `Tracing` can be printed in bold. + 'to track the performance of your application?', + { + timeout: 240_000, + }, + )); + + const replayOptionPrompted = + tracingOptionPrompted && + (await wizardInstance.sendStdinAndWaitForOutput( + [KEYS.ENTER], + // "Do you want to enable Sentry Session Replay", sometimes doesn't work as `Sentry Session Replay` can be printed in bold. + 'to get a video-like reproduction of errors during a user session?', + )); + + replayOptionPrompted && + (await wizardInstance.sendStdinAndWaitForOutput( + [KEYS.ENTER], + 'Do you want to create an example page', + { + optional: true, + }, + )); + + await wizardInstance.sendStdinAndWaitForOutput( + [KEYS.ENTER, KEYS.ENTER], + 'Successfully installed the Sentry Nuxt SDK!', + ); + + wizardInstance.kill(); +} + +function testNuxtProjectSetup(projectDir: string) { + const integration = Integration.nuxt; + + test('package.json is updated correctly', () => { + checkPackageJson(projectDir, integration); + }); + + test('.env-sentry-build-plugin is created and contains the auth token', () => { + checkEnvBuildPlugin(projectDir); + }); + + test('config files created', () => { + checkFileExists(`${projectDir}/sentry.server.config.ts`); + checkFileExists(`${projectDir}/sentry.client.config.ts`); + }); + + test('example page exists', () => { + checkFileExists(`${projectDir}/pages/sentry-example-page.vue`); + checkFileExists(`${projectDir}/server/api/sentry-example-api.ts`); + }); +} + +function testNuxtProjectConfigs(projectDir: string) { + test('nuxt config contains sentry module', () => { + checkFileContents(path.resolve(projectDir, 'nuxt.config.ts'), [ + "modules: ['@sentry/nuxt/module'],", + 'sentry: {', + ' sourceMapsUploadOptions: {', + ` org: '${TEST_ARGS.ORG_SLUG}',`, + ` project: '${TEST_ARGS.PROJECT_SLUG}'`, + ' }', + '},', + 'sourcemap: {', + ' client: true', + '}', + ]); + }); + + test('sentry.client.config.ts contents', () => { + checkFileContents(path.resolve(projectDir, 'sentry.client.config.ts'), [ + 'import * as Sentry from "@sentry/nuxt";', + 'Sentry.init({', + ' // If set up, you can use your runtime config here', + ' // dsn: useRuntimeConfig().public.sentry.dsn,', + ` dsn: "${TEST_ARGS.PROJECT_DSN}",`, + ' // We recommend adjusting this value in production, or using tracesSampler', + ' // for finer control', + ' tracesSampleRate: 1.0,', + ' // This sets the sample rate to be 10%. You may want this to be 100% while', + ' // in development and sample at a lower rate in production', + ' replaysSessionSampleRate: 0.1,', + ' // If the entire session is not sampled, use the below sample rate to sample', + ' // sessions when an error occurs.', + ' replaysOnErrorSampleRate: 1.0,', + " // If you don't want to use Session Replay, just remove the line below:", + ' integrations: [Sentry.replayIntegration()],', + " // Setting this option to true will print useful information to the console while you're setting up Sentry.", + ' debug: false,', + '});', + ]); + }); + + test('sentry.server.config.ts contents', () => { + checkFileContents(path.resolve(projectDir, 'sentry.server.config.ts'), [ + 'import * as Sentry from "@sentry/nuxt";', + 'Sentry.init({', + ` dsn: "${TEST_ARGS.PROJECT_DSN}",`, + ' // We recommend adjusting this value in production, or using tracesSampler', + ' // for finer control', + ' tracesSampleRate: 1.0,', + " // Setting this option to true will print useful information to the console while you're setting up Sentry.", + ' debug: false,', + '});', + ]); + }); +} + +function testNuxtProjectBuildsAndRuns(projectDir: string) { + test('builds successfully', async () => { + await checkIfBuilds(projectDir, 'preview this build'); + }); + + test('runs on prod mode correctly', async () => { + await checkIfRunsOnProdMode(projectDir, 'Listening on'); + }); +} diff --git a/e2e-tests/tests/nuxt-4.test.ts b/e2e-tests/tests/nuxt-4.test.ts new file mode 100644 index 00000000..cf30ea9c --- /dev/null +++ b/e2e-tests/tests/nuxt-4.test.ts @@ -0,0 +1,168 @@ +import * as path from 'path'; +import { Integration } from '../../lib/Constants'; +import { cleanupGit, revertLocalChanges } from '../utils'; +import { + checkEnvBuildPlugin, + checkFileContents, + checkFileExists, + checkIfBuilds, + checkIfRunsOnProdMode, + checkPackageJson, + KEYS, + startWizardInstance, + TEST_ARGS, +} from '../utils'; + +describe('Nuxt-4', () => { + const projectDir = path.resolve( + __dirname, + '../test-applications/nuxt-4-test-app', + ); + + beforeAll(async () => { + await runWizardOnNuxtProject(projectDir); + }); + + afterAll(() => { + revertLocalChanges(projectDir); + cleanupGit(projectDir); + }); + + testNuxtProjectSetup(projectDir); + + testNuxtProjectConfigs(projectDir); + + testNuxtProjectBuildsAndRuns(projectDir); +}); + +async function runWizardOnNuxtProject(projectDir: string): Promise { + const integration = Integration.nuxt; + + const wizardInstance = startWizardInstance(integration, projectDir); + const packageManagerPrompted = await wizardInstance.waitForOutput( + 'Please select your package manager.', + ); + + const tracingOptionPrompted = + packageManagerPrompted && + (await wizardInstance.sendStdinAndWaitForOutput( + // Selecting `yarn` as the package manager + [KEYS.DOWN, KEYS.ENTER], + // "Do you want to enable Tracing", sometimes doesn't work as `Tracing` can be printed in bold. + 'to track the performance of your application?', + { + timeout: 240_000, + }, + )); + + const replayOptionPrompted = + tracingOptionPrompted && + (await wizardInstance.sendStdinAndWaitForOutput( + [KEYS.ENTER], + // "Do you want to enable Sentry Session Replay", sometimes doesn't work as `Sentry Session Replay` can be printed in bold. + 'to get a video-like reproduction of errors during a user session?', + )); + + replayOptionPrompted && + (await wizardInstance.sendStdinAndWaitForOutput( + [KEYS.ENTER], + 'Do you want to create an example page', + { + optional: true, + }, + )); + + await wizardInstance.sendStdinAndWaitForOutput( + [KEYS.ENTER, KEYS.ENTER], + 'Successfully installed the Sentry Nuxt SDK!', + ); + + wizardInstance.kill(); +} + +function testNuxtProjectSetup(projectDir: string) { + const integration = Integration.nuxt; + + test('package.json is updated correctly', () => { + checkPackageJson(projectDir, integration); + }); + + test('.env-sentry-build-plugin is created and contains the auth token', () => { + checkEnvBuildPlugin(projectDir); + }); + + test('config files created', () => { + checkFileExists(`${projectDir}/sentry.server.config.ts`); + checkFileExists(`${projectDir}/sentry.client.config.ts`); + }); + + test('example page exists', () => { + checkFileExists(`${projectDir}'/app/pages/sentry-example-page.vue`); + checkFileExists(`${projectDir}/server/api/sentry-example-api.ts`); + }); +} + +function testNuxtProjectConfigs(projectDir: string) { + test('nuxt config contains sentry module', () => { + checkFileContents(path.resolve(projectDir, 'nuxt.config.ts'), [ + "modules: ['@sentry/nuxt/module'],", + 'sentry: {', + ' sourceMapsUploadOptions: {', + ` org: '${TEST_ARGS.ORG_SLUG}',`, + ` project: '${TEST_ARGS.PROJECT_SLUG}'`, + ' }', + '},', + 'sourcemap: {', + ' client: true', + '}', + ]); + }); + + test('sentry.client.config.ts contents', () => { + checkFileContents(path.resolve(projectDir, 'sentry.client.config.ts'), [ + 'import * as Sentry from "@sentry/nuxt";', + 'Sentry.init({', + ' // If set up, you can use your runtime config here', + ' // dsn: useRuntimeConfig().public.sentry.dsn,', + ` dsn: "${TEST_ARGS.PROJECT_DSN}",`, + ' // We recommend adjusting this value in production, or using tracesSampler', + ' // for finer control', + ' tracesSampleRate: 1.0,', + ' // This sets the sample rate to be 10%. You may want this to be 100% while', + ' // in development and sample at a lower rate in production', + ' replaysSessionSampleRate: 0.1,', + ' // If the entire session is not sampled, use the below sample rate to sample', + ' // sessions when an error occurs.', + ' replaysOnErrorSampleRate: 1.0,', + " // If you don't want to use Session Replay, just remove the line below:", + ' integrations: [Sentry.replayIntegration()],', + " // Setting this option to true will print useful information to the console while you're setting up Sentry.", + ' debug: false,', + '});', + ]); + }); + + test('sentry.server.config.ts contents', () => { + checkFileContents(path.resolve(projectDir, 'sentry.server.config.ts'), [ + 'import * as Sentry from "@sentry/nuxt";', + 'Sentry.init({', + ` dsn: "${TEST_ARGS.PROJECT_DSN}",`, + ' // We recommend adjusting this value in production, or using tracesSampler', + ' // for finer control', + ' tracesSampleRate: 1.0,', + " // Setting this option to true will print useful information to the console while you're setting up Sentry.", + ' debug: false,', + '});', + ]); + }); +} + +function testNuxtProjectBuildsAndRuns(projectDir: string) { + test('builds successfully', async () => { + await checkIfBuilds(projectDir, 'preview this build'); + }); + + test('runs on prod mode correctly', async () => { + await checkIfRunsOnProdMode(projectDir, 'Listening on'); + }); +} From 89a31bf4e39a5b3fd0b5cf1752283804640f5001 Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Mon, 25 Nov 2024 16:04:16 +0100 Subject: [PATCH 5/9] change client source map to hidden --- src/nuxt/sdk-setup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nuxt/sdk-setup.ts b/src/nuxt/sdk-setup.ts index 2bafa7aa..40225796 100644 --- a/src/nuxt/sdk-setup.ts +++ b/src/nuxt/sdk-setup.ts @@ -69,7 +69,7 @@ export async function addSDKModule( ...(options.selfHosted && { url: options.url }), }, }); - addNuxtModule(mod, '@sentry/nuxt/module', 'sourcemap', { client: true }); + addNuxtModule(mod, '@sentry/nuxt/module', 'sourcemap', { client: 'hidden' }); const { code } = generateCode(mod); From 95fcb5e6cf7db4cdd8119becdf980f2bef5d773b Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Mon, 25 Nov 2024 17:30:34 +0100 Subject: [PATCH 6/9] add scoped styles --- src/nuxt/templates.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/nuxt/templates.ts b/src/nuxt/templates.ts index 72dd3b62..65cda59f 100644 --- a/src/nuxt/templates.ts +++ b/src/nuxt/templates.ts @@ -178,7 +178,7 @@ Feel free to delete this file. -