diff --git a/CHANGELOG.md b/CHANGELOG.md index aa934a81..c927ae3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Unreleased + +- feat(nextjs): Add feature selection (#631) + ## 3.26.0 - fix(nextjs): Don't add '.env.sentry-build-plugin' to .gitignore if it's already there (#610) diff --git a/src/nextjs/nextjs-wizard.ts b/src/nextjs/nextjs-wizard.ts index 861f3462..48a4e824 100644 --- a/src/nextjs/nextjs-wizard.ts +++ b/src/nextjs/nextjs-wizard.ts @@ -17,6 +17,7 @@ import { confirmContinueIfNoOrDirtyGitRepo, createNewConfigFile, ensurePackageIsInstalled, + featureSelectionPrompt, getOrAskForProjectData, getPackageDotJson, installPackage, @@ -333,6 +334,23 @@ async function createOrMergeNextJsFiles( sentryUrl: string, sdkConfigOptions: SDKConfigOptions, ) { + 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 reproduction of frontend errors via user sessions?`, + enabledHint: 'recommended, but increases bundle size', + }, + ] as const); + const typeScriptDetected = isUsingTypeScript(); const configVariants = ['server', 'client', 'edge'] as const; @@ -390,6 +408,7 @@ async function createOrMergeNextJsFiles( getSentryConfigContents( selectedProject.keys[0].dsn.public, configVariant, + selectedFeatures, ), { encoding: 'utf8', flag: 'w' }, ); @@ -854,7 +873,7 @@ async function askShouldSetTunnelRoute() { const shouldSetTunnelRoute = await abortIfCancelled( clack.select({ message: - 'Do you want to route Sentry requests in the browser through your NextJS server to avoid ad blockers?', + 'Do you want to route Sentry requests in the browser through your Next.js server to avoid ad blockers?', options: [ { label: 'Yes', @@ -867,7 +886,7 @@ async function askShouldSetTunnelRoute() { hint: 'Browser errors and events might be blocked by ad blockers before being sent to Sentry', }, ], - initialValue: false, + initialValue: true, }), ); @@ -891,7 +910,7 @@ async function askShouldEnableReactComponentAnnotation() { { label: 'Yes', value: true, - hint: 'Annotates React component names (increases bundle size)', + hint: 'Annotates React component names - increases bundle size', }, { label: 'No', @@ -899,7 +918,7 @@ async function askShouldEnableReactComponentAnnotation() { hint: 'Continue without React component annotations', }, ], - initialValue: false, + initialValue: true, }), ); diff --git a/src/nextjs/templates.ts b/src/nextjs/templates.ts index fcd35339..a36eefe0 100644 --- a/src/nextjs/templates.ts +++ b/src/nextjs/templates.ts @@ -112,9 +112,26 @@ export default withSentryConfig( `; } +function getClientIntegrationsSnippet(features: { replay: boolean }) { + if (features.replay) { + return ` + + // Add optional integrations for additional features + integrations: [ + Sentry.replayIntegration(), + ],`; + } + + return ''; +} + export function getSentryConfigContents( dsn: string, config: 'server' | 'client' | 'edge', + selectedFeaturesMap: { + replay: boolean; + performance: boolean; + }, ): string { let primer; if (config === 'server') { @@ -132,32 +149,29 @@ export function getSentryConfigContents( // https://docs.sentry.io/platforms/javascript/guides/nextjs/`; } - let additionalOptions = ''; + const integrationsOptions = getClientIntegrationsSnippet(selectedFeaturesMap); + + let replayOptions = ''; if (config === 'client') { - additionalOptions = ` - replaysOnErrorSampleRate: 1.0, + if (selectedFeaturesMap.replay) { + replayOptions += ` + // Define how likely Replay events are sampled. // 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, - // You can remove this option if you're not planning to use the Sentry Session Replay feature: - integrations: [ - Sentry.replayIntegration({ - // Additional Replay configuration goes in here, for example: - maskAllText: true, - blockAllMedia: true, - }), - ],`; + // Define how likely Replay events are sampled when an error occurs. + replaysOnErrorSampleRate: 1.0,`; + } } - let spotlightOption = ''; - if (config === 'server') { - spotlightOption = ` + let performanceOptions = ''; + if (selectedFeaturesMap.performance) { + performanceOptions += ` - // Uncomment the line below to enable Spotlight (https://spotlightjs.com) - // spotlight: process.env.NODE_ENV === 'development', - `; + // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control. + tracesSampleRate: 1,`; } // eslint-disable-next-line @typescript-eslint/restrict-template-expressions @@ -166,13 +180,10 @@ export function getSentryConfigContents( import * as Sentry from "@sentry/nextjs"; Sentry.init({ - dsn: "${dsn}", - - // Adjust this value in production, or use tracesSampler for greater control - tracesSampleRate: 1, + dsn: "${dsn}",${integrationsOptions}${performanceOptions}${replayOptions} // Setting this option to true will print useful information to the console while you're setting up Sentry. - debug: false,${additionalOptions}${spotlightOption} + debug: false, }); `; } diff --git a/src/run.ts b/src/run.ts index 8eef6c30..f6e0e66a 100644 --- a/src/run.ts +++ b/src/run.ts @@ -60,7 +60,7 @@ export async function run(argv: Args) { { value: 'android', label: 'Android' }, { value: 'cordova', label: 'Cordova' }, { value: 'electron', label: 'Electron' }, - { value: 'nextjs', label: 'NextJS' }, + { value: 'nextjs', label: 'Next.js' }, { value: 'remix', label: 'Remix' }, { value: 'sveltekit', label: 'SvelteKit' }, { value: 'sourcemaps', label: 'Configure Source Maps Upload' }, diff --git a/src/sourcemaps/sourcemaps-wizard.ts b/src/sourcemaps/sourcemaps-wizard.ts index c67d56a1..1d478e3a 100644 --- a/src/sourcemaps/sourcemaps-wizard.ts +++ b/src/sourcemaps/sourcemaps-wizard.ts @@ -141,7 +141,7 @@ async function askForUsedBundlerTool(): Promise { { label: 'Next.js', value: 'nextjs', - hint: 'Select this option if you want to set up source maps in a NextJS project.', + hint: 'Select this option if you want to set up source maps in a Next.js project.', }, { label: 'Remix', diff --git a/src/utils/clack-utils.ts b/src/utils/clack-utils.ts index 2808f13d..906cb279 100644 --- a/src/utils/clack-utils.ts +++ b/src/utils/clack-utils.ts @@ -10,7 +10,7 @@ import { setInterval } from 'timers'; import { URL } from 'url'; import * as Sentry from '@sentry/node'; import { hasPackageInstalled, PackageDotJson } from './package-json'; -import { SentryProjectData, WizardOptions } from './types'; +import { Feature, SentryProjectData, WizardOptions } from './types'; import { traceStep } from '../telemetry'; import { detectPackageManger, @@ -1277,3 +1277,36 @@ export async function askShouldCreateExamplePage( ), ); } + +export async function featureSelectionPrompt>( + features: F, +): Promise<{ [key in F[number]['id']]: boolean }> { + return traceStep('feature-selection', async () => { + const selectedFeatures: Record = {}; + + for (const feature of features) { + const selected = await abortIfCancelled( + clack.select({ + message: feature.prompt, + initialValue: true, + options: [ + { + value: true, + label: 'Yes', + hint: feature.enabledHint, + }, + { + value: false, + label: 'No', + hint: feature.disabledHint, + }, + ], + }), + ); + + selectedFeatures[feature.id] = selected; + } + + return selectedFeatures as { [key in F[number]['id']]: boolean }; + }); +} diff --git a/src/utils/types.ts b/src/utils/types.ts index 6b13db73..86e05e8a 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -44,3 +44,10 @@ export type WizardOptions = { selfHosted: boolean; }; }; + +export interface Feature { + id: string; + prompt: string; + enabledHint?: string; + disabledHint?: string; +} diff --git a/test/nextjs/templates.test.ts b/test/nextjs/templates.test.ts index 5c0c345e..65c70fc3 100644 --- a/test/nextjs/templates.test.ts +++ b/test/nextjs/templates.test.ts @@ -1,6 +1,269 @@ -import { getWithSentryConfigOptionsTemplate } from '../../src/nextjs/templates'; +import { + getSentryConfigContents, + getWithSentryConfigOptionsTemplate, +} from '../../src/nextjs/templates'; + +describe('Next.js code templates', () => { + describe('getSentryConfigContents', () => { + describe('client-side', () => { + it('generates client-side Sentry config with all features enabled', () => { + const template = getSentryConfigContents('my-dsn', 'client', { + performance: true, + replay: true, + }); + + expect(template).toMatchInlineSnapshot(` + "// This file configures the initialization of Sentry on the client. + // The config you add here will be used whenever a users loads a page in their browser. + // https://docs.sentry.io/platforms/javascript/guides/nextjs/ + + import * as Sentry from "@sentry/nextjs"; + + Sentry.init({ + dsn: "my-dsn", + + // Add optional integrations for additional features + integrations: [ + Sentry.replayIntegration(), + ], + + // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control. + tracesSampleRate: 1, + + // Define how likely Replay events are sampled. + // 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, + + // Define how likely Replay events are sampled when an error occurs. + replaysOnErrorSampleRate: 1.0, + + // Setting this option to true will print useful information to the console while you're setting up Sentry. + debug: false, + }); + " + `); + }); + + it('generates client-side Sentry config with performance monitoring disabled', () => { + const template = getSentryConfigContents('my-dsn', 'client', { + performance: false, + replay: true, + }); + + expect(template).toMatchInlineSnapshot(` + "// This file configures the initialization of Sentry on the client. + // The config you add here will be used whenever a users loads a page in their browser. + // https://docs.sentry.io/platforms/javascript/guides/nextjs/ + + import * as Sentry from "@sentry/nextjs"; + + Sentry.init({ + dsn: "my-dsn", + + // Add optional integrations for additional features + integrations: [ + Sentry.replayIntegration(), + ], + + // Define how likely Replay events are sampled. + // 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, + + // Define how likely Replay events are sampled when an error occurs. + replaysOnErrorSampleRate: 1.0, + + // Setting this option to true will print useful information to the console while you're setting up Sentry. + debug: false, + }); + " + `); + }); + + it('generates client-side Sentry config with session replay disabled', () => { + const template = getSentryConfigContents('my-dsn', 'client', { + performance: true, + replay: false, + }); + + expect(template).toMatchInlineSnapshot(` + "// This file configures the initialization of Sentry on the client. + // The config you add here will be used whenever a users loads a page in their browser. + // https://docs.sentry.io/platforms/javascript/guides/nextjs/ + + import * as Sentry from "@sentry/nextjs"; + + Sentry.init({ + dsn: "my-dsn", + + // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control. + tracesSampleRate: 1, + + // Setting this option to true will print useful information to the console while you're setting up Sentry. + debug: false, + }); + " + `); + }); + }); + + describe('server-side', () => { + it('generates server-side Sentry config with all features enabled', () => { + const template = getSentryConfigContents('my-dsn', 'server', { + performance: true, + replay: true, + }); + + expect(template).toMatchInlineSnapshot(` + "// This file configures the initialization of Sentry on the server. + // The config you add here will be used whenever the server handles a request. + // https://docs.sentry.io/platforms/javascript/guides/nextjs/ + + import * as Sentry from "@sentry/nextjs"; + + Sentry.init({ + dsn: "my-dsn", + + // Add optional integrations for additional features + integrations: [ + Sentry.replayIntegration(), + ], + + // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control. + tracesSampleRate: 1, + + // Setting this option to true will print useful information to the console while you're setting up Sentry. + debug: false, + }); + " + `); + }); + + it('generates server-side Sentry config with performance monitoring disabled', () => { + const template = getSentryConfigContents('my-dsn', 'server', { + performance: false, + replay: true, + }); + + expect(template).toMatchInlineSnapshot(` + "// This file configures the initialization of Sentry on the server. + // The config you add here will be used whenever the server handles a request. + // https://docs.sentry.io/platforms/javascript/guides/nextjs/ + + import * as Sentry from "@sentry/nextjs"; + + Sentry.init({ + dsn: "my-dsn", + + // Add optional integrations for additional features + 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 server-side Sentry config with spotlight disabled', () => { + const template = getSentryConfigContents('my-dsn', 'server', { + performance: true, + replay: true, + }); + + expect(template).toMatchInlineSnapshot(` + "// This file configures the initialization of Sentry on the server. + // The config you add here will be used whenever the server handles a request. + // https://docs.sentry.io/platforms/javascript/guides/nextjs/ + + import * as Sentry from "@sentry/nextjs"; + + Sentry.init({ + dsn: "my-dsn", + + // Add optional integrations for additional features + integrations: [ + Sentry.replayIntegration(), + ], + + // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control. + tracesSampleRate: 1, + + // Setting this option to true will print useful information to the console while you're setting up Sentry. + debug: false, + }); + " + `); + }); + }); + + describe('edge', () => { + it('generates edge Sentry config with all features enabled', () => { + const template = getSentryConfigContents('my-dsn', 'edge', { + performance: true, + replay: true, + }); + + expect(template).toMatchInlineSnapshot(` + "// This file configures the initialization of Sentry for edge features (middleware, edge routes, and so on). + // The config you add here will be used whenever one of the edge features is loaded. + // Note that this config is unrelated to the Vercel Edge Runtime and is also required when running locally. + // https://docs.sentry.io/platforms/javascript/guides/nextjs/ + + import * as Sentry from "@sentry/nextjs"; + + Sentry.init({ + dsn: "my-dsn", + + // Add optional integrations for additional features + integrations: [ + Sentry.replayIntegration(), + ], + + // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control. + tracesSampleRate: 1, + + // Setting this option to true will print useful information to the console while you're setting up Sentry. + debug: false, + }); + " + `); + }); + + it('generates edge Sentry config with performance monitoring disabled', () => { + const template = getSentryConfigContents('my-dsn', 'edge', { + performance: false, + replay: true, + }); + + expect(template).toMatchInlineSnapshot(` + "// This file configures the initialization of Sentry for edge features (middleware, edge routes, and so on). + // The config you add here will be used whenever one of the edge features is loaded. + // Note that this config is unrelated to the Vercel Edge Runtime and is also required when running locally. + // https://docs.sentry.io/platforms/javascript/guides/nextjs/ + + import * as Sentry from "@sentry/nextjs"; + + Sentry.init({ + dsn: "my-dsn", + + // Add optional integrations for additional features + integrations: [ + Sentry.replayIntegration(), + ], + + // Setting this option to true will print useful information to the console while you're setting up Sentry. + debug: false, + }); + " + `); + }); + }); + }); -describe('NextJS code templates', () => { describe('getWithSentryConfigOptionsTemplate', () => { it('generates options for SaaS', () => { const template = getWithSentryConfigOptionsTemplate({