From ef97d248e1b9ba885edcc5aa3d4db46be8cbbf28 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Fri, 26 Jul 2024 13:41:19 +0100 Subject: [PATCH 01/11] feat(nextjs): Add feature selection --- src/nextjs/nextjs-wizard.ts | 35 ++++++++++++++++++++++++++++++++++- src/nextjs/templates.ts | 11 ++++++++--- src/utils/clack-utils.ts | 28 +++++++++++++++++++++++++++- src/utils/types.ts | 6 ++++++ 4 files changed, 75 insertions(+), 5 deletions(-) diff --git a/src/nextjs/nextjs-wizard.ts b/src/nextjs/nextjs-wizard.ts index 861f3462..67f48967 100644 --- a/src/nextjs/nextjs-wizard.ts +++ b/src/nextjs/nextjs-wizard.ts @@ -14,9 +14,11 @@ import { abortIfCancelled, addDotEnvSentryBuildPluginFile, askShouldCreateExamplePage, + askShouldUseDefaulFeatureSet, confirmContinueIfNoOrDirtyGitRepo, createNewConfigFile, ensurePackageIsInstalled, + featureSelectionPrompt, getOrAskForProjectData, getPackageDotJson, installPackage, @@ -24,7 +26,7 @@ import { printWelcome, showCopyPasteInstructions, } from '../utils/clack-utils'; -import type { SentryProjectData, WizardOptions } from '../utils/types'; +import type { Feature, SentryProjectData, WizardOptions } from '../utils/types'; import { getFullUnderscoreErrorCopyPasteSnippet, getGlobalErrorCopyPasteSnippet, @@ -47,6 +49,21 @@ import { getPackageVersion, hasPackageInstalled } from '../utils/package-json'; import { getNextJsVersionBucket } from './utils'; import { configureCI } from '../sourcemaps/sourcemaps-wizard'; +export const NEXTJS_FEATURE_SET: Feature[] = [ + { + id: 'performance', + name: 'Performance Monitoring', + }, + { + id: 'replay', + name: 'Session Replay', + }, + { + id: 'spotlight', + name: 'Spotlight', + }, +]; + export function runNextjsWizard(options: WizardOptions) { return withTelemetry( { @@ -333,6 +350,21 @@ async function createOrMergeNextJsFiles( sentryUrl: string, sdkConfigOptions: SDKConfigOptions, ) { + // let selectedFeatures = NEXTJS_FEATURE_SET.map((feature) => feature.id); + const useDefaultFeatureSet = await askShouldUseDefaulFeatureSet(); + + const selectedFeatures = useDefaultFeatureSet + ? NEXTJS_FEATURE_SET.map((feature) => feature.id) + : ((await featureSelectionPrompt(NEXTJS_FEATURE_SET)) as string[]); + + const selectedFeaturesMap = selectedFeatures.reduce( + (acc: Record, feature: string) => { + acc[feature] = true; + return acc; + }, + {}, + ); + const typeScriptDetected = isUsingTypeScript(); const configVariants = ['server', 'client', 'edge'] as const; @@ -390,6 +422,7 @@ async function createOrMergeNextJsFiles( getSentryConfigContents( selectedProject.keys[0].dsn.public, configVariant, + selectedFeaturesMap, ), { encoding: 'utf8', flag: 'w' }, ); diff --git a/src/nextjs/templates.ts b/src/nextjs/templates.ts index fcd35339..623e840a 100644 --- a/src/nextjs/templates.ts +++ b/src/nextjs/templates.ts @@ -115,6 +115,7 @@ export default withSentryConfig( export function getSentryConfigContents( dsn: string, config: 'server' | 'client' | 'edge', + selectedFeaturesMap: Record, ): string { let primer; if (config === 'server') { @@ -133,7 +134,7 @@ export function getSentryConfigContents( } let additionalOptions = ''; - if (config === 'client') { + if (config === 'client' && 'replay' in selectedFeaturesMap) { additionalOptions = ` replaysOnErrorSampleRate: 1.0, @@ -167,10 +168,14 @@ import * as Sentry from "@sentry/nextjs"; Sentry.init({ dsn: "${dsn}", - + ${ + 'performance' in selectedFeaturesMap + ? ` // 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,${additionalOptions}${spotlightOption} }); diff --git a/src/utils/clack-utils.ts b/src/utils/clack-utils.ts index 2808f13d..651063d2 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,29 @@ export async function askShouldCreateExamplePage( ), ); } + +export async function askShouldUseDefaulFeatureSet(): Promise { + return traceStep('ask-use-default-feature-set', () => + abortIfCancelled( + clack.confirm({ + message: 'Do you want to use the default feature set?', + initialValue: true, + }), + ), + ); +} + +export async function featureSelectionPrompt(features: Feature[]) { + return traceStep('feature-selection', async () => { + return clack.multiselect({ + message: 'Which optional features do you want to set up?', + options: features.map((feature) => ({ + value: feature.id, + label: feature.name, + hint: feature.recommended ? '(recommended)' : undefined, + })), + initialValues: features.map((feature) => feature.id), + required: true, + }); + }); +} diff --git a/src/utils/types.ts b/src/utils/types.ts index 6b13db73..405802cd 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -44,3 +44,9 @@ export type WizardOptions = { selfHosted: boolean; }; }; + +export interface Feature { + id: string; + name: string; + recommended?: boolean; +} From 58e1f97cd9ad6c429b9954e2b46dbc23399b089e Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Tue, 30 Jul 2024 12:01:47 +0100 Subject: [PATCH 02/11] Add profiling option. --- src/nextjs/nextjs-wizard.ts | 4 ++++ src/nextjs/templates.ts | 47 +++++++++++++++++++++++++++++++++++-- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/src/nextjs/nextjs-wizard.ts b/src/nextjs/nextjs-wizard.ts index 67f48967..69d0f77e 100644 --- a/src/nextjs/nextjs-wizard.ts +++ b/src/nextjs/nextjs-wizard.ts @@ -58,6 +58,10 @@ export const NEXTJS_FEATURE_SET: Feature[] = [ id: 'replay', name: 'Session Replay', }, + { + id: 'profiling', + name: 'Profiling', + }, { id: 'spotlight', name: 'Spotlight', diff --git a/src/nextjs/templates.ts b/src/nextjs/templates.ts index 623e840a..31a982bd 100644 --- a/src/nextjs/templates.ts +++ b/src/nextjs/templates.ts @@ -112,6 +112,42 @@ export default withSentryConfig( `; } +export function getReplayConfigSnippet() { + return ` + replaysOnErrorSampleRate: 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, +`; +} + +export function getIntegrationsSnippet( + selectedFeaturesMap: Record, +) { + if (selectedFeaturesMap.replay || selectedFeaturesMap.profiling) { + return `integrations: [${ + selectedFeaturesMap.replay + ? ` + Sentry.replayIntegration({ + // Additional Replay configuration goes in here, for example: + maskAllText: true, + blockAllMedia: true, + }), + ` + : '' + } + ${ + selectedFeaturesMap.profiling + ? 'Sentry.browserProfilingIntegration(),' + : '' + } + ],`; + } + + return ''; +} + export function getSentryConfigContents( dsn: string, config: 'server' | 'client' | 'edge', @@ -152,10 +188,17 @@ export function getSentryConfigContents( ],`; } + if (config === 'client') { + additionalOptions = `${ + selectedFeaturesMap.replay ? getReplayConfigSnippet() : '' + } + ${getIntegrationsSnippet(selectedFeaturesMap)} + `; + } + let spotlightOption = ''; - if (config === 'server') { + if (config === 'server' && 'spotlight' in selectedFeaturesMap) { spotlightOption = ` - // Uncomment the line below to enable Spotlight (https://spotlightjs.com) // spotlight: process.env.NODE_ENV === 'development', `; From 3129121ceee8c1b2272c904cffea4a2d4eed7fc2 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Tue, 30 Jul 2024 13:56:47 +0100 Subject: [PATCH 03/11] Resolve conflict --- src/nextjs/nextjs-wizard.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/nextjs/nextjs-wizard.ts b/src/nextjs/nextjs-wizard.ts index 69d0f77e..9911a6ec 100644 --- a/src/nextjs/nextjs-wizard.ts +++ b/src/nextjs/nextjs-wizard.ts @@ -354,7 +354,6 @@ async function createOrMergeNextJsFiles( sentryUrl: string, sdkConfigOptions: SDKConfigOptions, ) { - // let selectedFeatures = NEXTJS_FEATURE_SET.map((feature) => feature.id); const useDefaultFeatureSet = await askShouldUseDefaulFeatureSet(); const selectedFeatures = useDefaultFeatureSet From 9762d8bdc52c14f2e5115c96c2adb1bc6ac70396 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Tue, 30 Jul 2024 14:13:29 +0100 Subject: [PATCH 04/11] Add CHANGELOG entry. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa934a81..b6f7e7c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - fix(nextjs): Add missing Error.getInitialProps calls in Next.js error page snippets (#632) - fix/feat: Improve error logging for package installation (#635) - fix: Properly close open handles (#638) +- feat(nextjs): Add feature selection (#631) ## 3.25.2 From 37eb3a28a03b505932a7a1a8746555cfafcf9eda Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Mon, 5 Aug 2024 13:25:46 +0100 Subject: [PATCH 05/11] Address review comments. --- src/nextjs/nextjs-wizard.ts | 17 +++++++---------- src/nextjs/templates.ts | 7 +------ src/utils/clack-utils.ts | 17 ++++------------- src/utils/types.ts | 1 + 4 files changed, 13 insertions(+), 29 deletions(-) diff --git a/src/nextjs/nextjs-wizard.ts b/src/nextjs/nextjs-wizard.ts index 9911a6ec..d73c3154 100644 --- a/src/nextjs/nextjs-wizard.ts +++ b/src/nextjs/nextjs-wizard.ts @@ -14,7 +14,6 @@ import { abortIfCancelled, addDotEnvSentryBuildPluginFile, askShouldCreateExamplePage, - askShouldUseDefaulFeatureSet, confirmContinueIfNoOrDirtyGitRepo, createNewConfigFile, ensurePackageIsInstalled, @@ -53,18 +52,18 @@ export const NEXTJS_FEATURE_SET: Feature[] = [ { id: 'performance', name: 'Performance Monitoring', + description: 'Monitor your app performance and find bottlenecks.', + recommended: true, }, { id: 'replay', name: 'Session Replay', - }, - { - id: 'profiling', - name: 'Profiling', + description: 'Replay user sessions to reproduce and fix bugs.', }, { id: 'spotlight', name: 'Spotlight', + description: `Use Sentry's Spotlight debug tool (https://spotlightjs.com/) for your development workflow.`, }, ]; @@ -354,11 +353,9 @@ async function createOrMergeNextJsFiles( sentryUrl: string, sdkConfigOptions: SDKConfigOptions, ) { - const useDefaultFeatureSet = await askShouldUseDefaulFeatureSet(); - - const selectedFeatures = useDefaultFeatureSet - ? NEXTJS_FEATURE_SET.map((feature) => feature.id) - : ((await featureSelectionPrompt(NEXTJS_FEATURE_SET)) as string[]); + const selectedFeatures = (await featureSelectionPrompt( + NEXTJS_FEATURE_SET, + )) as string[]; const selectedFeaturesMap = selectedFeatures.reduce( (acc: Record, feature: string) => { diff --git a/src/nextjs/templates.ts b/src/nextjs/templates.ts index 31a982bd..89e8fd74 100644 --- a/src/nextjs/templates.ts +++ b/src/nextjs/templates.ts @@ -125,7 +125,7 @@ export function getReplayConfigSnippet() { export function getIntegrationsSnippet( selectedFeaturesMap: Record, ) { - if (selectedFeaturesMap.replay || selectedFeaturesMap.profiling) { + if (selectedFeaturesMap.replay) { return `integrations: [${ selectedFeaturesMap.replay ? ` @@ -137,11 +137,6 @@ export function getIntegrationsSnippet( ` : '' } - ${ - selectedFeaturesMap.profiling - ? 'Sentry.browserProfilingIntegration(),' - : '' - } ],`; } diff --git a/src/utils/clack-utils.ts b/src/utils/clack-utils.ts index 651063d2..3f717ca3 100644 --- a/src/utils/clack-utils.ts +++ b/src/utils/clack-utils.ts @@ -1278,25 +1278,16 @@ export async function askShouldCreateExamplePage( ); } -export async function askShouldUseDefaulFeatureSet(): Promise { - return traceStep('ask-use-default-feature-set', () => - abortIfCancelled( - clack.confirm({ - message: 'Do you want to use the default feature set?', - initialValue: true, - }), - ), - ); -} - export async function featureSelectionPrompt(features: Feature[]) { return traceStep('feature-selection', async () => { return clack.multiselect({ - message: 'Which optional features do you want to set up?', + message: 'Which Sentry features do you want to set up?', options: features.map((feature) => ({ value: feature.id, label: feature.name, - hint: feature.recommended ? '(recommended)' : undefined, + hint: feature.recommended + ? `(recommended) - ${feature.description}` + : feature.description, })), initialValues: features.map((feature) => feature.id), required: true, diff --git a/src/utils/types.ts b/src/utils/types.ts index 405802cd..6ed6b6b1 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -48,5 +48,6 @@ export type WizardOptions = { export interface Feature { id: string; name: string; + description: string; recommended?: boolean; } From d272159080129f304f57e9b157d197ad95b8dc88 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Mon, 5 Aug 2024 13:36:03 +0100 Subject: [PATCH 06/11] Fix CHANGELOG --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b6f7e7c5..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) @@ -9,7 +13,6 @@ - fix(nextjs): Add missing Error.getInitialProps calls in Next.js error page snippets (#632) - fix/feat: Improve error logging for package installation (#635) - fix: Properly close open handles (#638) -- feat(nextjs): Add feature selection (#631) ## 3.25.2 From 6619662ac62ba3dd2bd76799768dd398ff9717d3 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Mon, 5 Aug 2024 13:42:19 +0100 Subject: [PATCH 07/11] Move `recommended` to the feature label --- src/utils/clack-utils.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/utils/clack-utils.ts b/src/utils/clack-utils.ts index 3f717ca3..a9015ed6 100644 --- a/src/utils/clack-utils.ts +++ b/src/utils/clack-utils.ts @@ -1284,10 +1284,8 @@ export async function featureSelectionPrompt(features: Feature[]) { message: 'Which Sentry features do you want to set up?', options: features.map((feature) => ({ value: feature.id, - label: feature.name, - hint: feature.recommended - ? `(recommended) - ${feature.description}` - : feature.description, + label: `${feature.name} ${feature.recommended ? '[recommended]' : ''}`, + hint: feature.description, })), initialValues: features.map((feature) => feature.id), required: true, From 8e3785ae0c201ffb2cd561d94e5b979744b55475 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Tue, 6 Aug 2024 10:42:21 +0100 Subject: [PATCH 08/11] Switch to yes/no prompts. --- src/nextjs/nextjs-wizard.ts | 10 ++++++---- src/utils/clack-utils.ts | 35 +++++++++++++++++++++++++---------- src/utils/types.ts | 4 ++-- 3 files changed, 33 insertions(+), 16 deletions(-) diff --git a/src/nextjs/nextjs-wizard.ts b/src/nextjs/nextjs-wizard.ts index d73c3154..5cc34fd6 100644 --- a/src/nextjs/nextjs-wizard.ts +++ b/src/nextjs/nextjs-wizard.ts @@ -52,18 +52,20 @@ export const NEXTJS_FEATURE_SET: Feature[] = [ { id: 'performance', name: 'Performance Monitoring', - description: 'Monitor your app performance and find bottlenecks.', - recommended: true, + enabledHint: 'Monitor your app performance and find bottlenecks.', + disabledHint: 'Skip setting up Performance Monitoring.', }, { id: 'replay', name: 'Session Replay', - description: 'Replay user sessions to reproduce and fix bugs.', + enabledHint: 'Replay user sessions to reproduce and fix bugs.', + disabledHint: 'Do not set up Session Replay.', }, { id: 'spotlight', name: 'Spotlight', - description: `Use Sentry's Spotlight debug tool (https://spotlightjs.com/) for your development workflow.`, + enabledHint: `Use Sentry's Spotlight debug tool (https://spotlightjs.com/) for your development workflow.`, + disabledHint: 'Skip setting up Spotlight.', }, ]; diff --git a/src/utils/clack-utils.ts b/src/utils/clack-utils.ts index a9015ed6..c39f1688 100644 --- a/src/utils/clack-utils.ts +++ b/src/utils/clack-utils.ts @@ -1280,15 +1280,30 @@ export async function askShouldCreateExamplePage( export async function featureSelectionPrompt(features: Feature[]) { return traceStep('feature-selection', async () => { - return clack.multiselect({ - message: 'Which Sentry features do you want to set up?', - options: features.map((feature) => ({ - value: feature.id, - label: `${feature.name} ${feature.recommended ? '[recommended]' : ''}`, - hint: feature.description, - })), - initialValues: features.map((feature) => feature.id), - required: true, - }); + const selectedFeatures = []; + + for (const feature of features) { + const selected = await clack.select({ + message: `Do you want to set up ${feature.name}?`, + options: [ + { + value: true, + label: 'Yes', + hint: feature.enabledHint, + }, + { + value: false, + label: 'No', + hint: feature.disabledHint, + }, + ], + }); + + if (selected) { + selectedFeatures.push(feature.id); + } + } + + return selectedFeatures; }); } diff --git a/src/utils/types.ts b/src/utils/types.ts index 6ed6b6b1..5c45d2c0 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -48,6 +48,6 @@ export type WizardOptions = { export interface Feature { id: string; name: string; - description: string; - recommended?: boolean; + enabledHint?: string; + disabledHint?: string; } From 0c9958bd458d607985085c5e703e068705ef0787 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Tue, 6 Aug 2024 10:44:50 +0100 Subject: [PATCH 09/11] Fix linter. --- src/nextjs/nextjs-wizard.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/nextjs/nextjs-wizard.ts b/src/nextjs/nextjs-wizard.ts index 5cc34fd6..0d6599a0 100644 --- a/src/nextjs/nextjs-wizard.ts +++ b/src/nextjs/nextjs-wizard.ts @@ -355,9 +355,7 @@ async function createOrMergeNextJsFiles( sentryUrl: string, sdkConfigOptions: SDKConfigOptions, ) { - const selectedFeatures = (await featureSelectionPrompt( - NEXTJS_FEATURE_SET, - )) as string[]; + const selectedFeatures = await featureSelectionPrompt(NEXTJS_FEATURE_SET); const selectedFeaturesMap = selectedFeatures.reduce( (acc: Record, feature: string) => { From 7081c485b24203f02c8eb29760b12982392b619f Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Wed, 7 Aug 2024 15:49:08 +0200 Subject: [PATCH 10/11] adjustments --- src/nextjs/nextjs-wizard.ts | 56 +++++++----------- src/nextjs/templates.ts | 88 +++++++++-------------------- src/run.ts | 2 +- src/sourcemaps/sourcemaps-wizard.ts | 2 +- src/utils/clack-utils.ts | 45 ++++++++------- src/utils/types.ts | 2 +- test/nextjs/templates.test.ts | 2 +- 7 files changed, 77 insertions(+), 120 deletions(-) diff --git a/src/nextjs/nextjs-wizard.ts b/src/nextjs/nextjs-wizard.ts index 0d6599a0..48a4e824 100644 --- a/src/nextjs/nextjs-wizard.ts +++ b/src/nextjs/nextjs-wizard.ts @@ -25,7 +25,7 @@ import { printWelcome, showCopyPasteInstructions, } from '../utils/clack-utils'; -import type { Feature, SentryProjectData, WizardOptions } from '../utils/types'; +import type { SentryProjectData, WizardOptions } from '../utils/types'; import { getFullUnderscoreErrorCopyPasteSnippet, getGlobalErrorCopyPasteSnippet, @@ -48,27 +48,6 @@ import { getPackageVersion, hasPackageInstalled } from '../utils/package-json'; import { getNextJsVersionBucket } from './utils'; import { configureCI } from '../sourcemaps/sourcemaps-wizard'; -export const NEXTJS_FEATURE_SET: Feature[] = [ - { - id: 'performance', - name: 'Performance Monitoring', - enabledHint: 'Monitor your app performance and find bottlenecks.', - disabledHint: 'Skip setting up Performance Monitoring.', - }, - { - id: 'replay', - name: 'Session Replay', - enabledHint: 'Replay user sessions to reproduce and fix bugs.', - disabledHint: 'Do not set up Session Replay.', - }, - { - id: 'spotlight', - name: 'Spotlight', - enabledHint: `Use Sentry's Spotlight debug tool (https://spotlightjs.com/) for your development workflow.`, - disabledHint: 'Skip setting up Spotlight.', - }, -]; - export function runNextjsWizard(options: WizardOptions) { return withTelemetry( { @@ -355,15 +334,22 @@ async function createOrMergeNextJsFiles( sentryUrl: string, sdkConfigOptions: SDKConfigOptions, ) { - const selectedFeatures = await featureSelectionPrompt(NEXTJS_FEATURE_SET); - - const selectedFeaturesMap = selectedFeatures.reduce( - (acc: Record, feature: string) => { - acc[feature] = true; - return acc; + 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(); @@ -422,7 +408,7 @@ async function createOrMergeNextJsFiles( getSentryConfigContents( selectedProject.keys[0].dsn.public, configVariant, - selectedFeaturesMap, + selectedFeatures, ), { encoding: 'utf8', flag: 'w' }, ); @@ -887,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', @@ -900,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, }), ); @@ -924,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', @@ -932,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 89e8fd74..a36eefe0 100644 --- a/src/nextjs/templates.ts +++ b/src/nextjs/templates.ts @@ -112,31 +112,13 @@ export default withSentryConfig( `; } -export function getReplayConfigSnippet() { - return ` - replaysOnErrorSampleRate: 1.0, +function getClientIntegrationsSnippet(features: { replay: boolean }) { + if (features.replay) { + return ` - // 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, -`; -} - -export function getIntegrationsSnippet( - selectedFeaturesMap: Record, -) { - if (selectedFeaturesMap.replay) { - return `integrations: [${ - selectedFeaturesMap.replay - ? ` - Sentry.replayIntegration({ - // Additional Replay configuration goes in here, for example: - maskAllText: true, - blockAllMedia: true, - }), - ` - : '' - } + // Add optional integrations for additional features + integrations: [ + Sentry.replayIntegration(), ],`; } @@ -146,7 +128,10 @@ export function getIntegrationsSnippet( export function getSentryConfigContents( dsn: string, config: 'server' | 'client' | 'edge', - selectedFeaturesMap: Record, + selectedFeaturesMap: { + replay: boolean; + performance: boolean; + }, ): string { let primer; if (config === 'server') { @@ -164,39 +149,29 @@ export function getSentryConfigContents( // https://docs.sentry.io/platforms/javascript/guides/nextjs/`; } - let additionalOptions = ''; - if (config === 'client' && 'replay' in selectedFeaturesMap) { - additionalOptions = ` - replaysOnErrorSampleRate: 1.0, + const integrationsOptions = getClientIntegrationsSnippet(selectedFeaturesMap); + let replayOptions = ''; + if (config === 'client') { + 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, - }), - ],`; - } - - if (config === 'client') { - additionalOptions = `${ - selectedFeaturesMap.replay ? getReplayConfigSnippet() : '' + // Define how likely Replay events are sampled when an error occurs. + replaysOnErrorSampleRate: 1.0,`; } - ${getIntegrationsSnippet(selectedFeaturesMap)} - `; } - let spotlightOption = ''; - if (config === 'server' && 'spotlight' in selectedFeaturesMap) { - spotlightOption = ` - // Uncomment the line below to enable Spotlight (https://spotlightjs.com) - // spotlight: process.env.NODE_ENV === 'development', - `; + let performanceOptions = ''; + if (selectedFeaturesMap.performance) { + performanceOptions += ` + + // 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 @@ -205,17 +180,10 @@ export function getSentryConfigContents( import * as Sentry from "@sentry/nextjs"; Sentry.init({ - dsn: "${dsn}", - ${ - 'performance' in selectedFeaturesMap - ? ` - // 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 c39f1688..906cb279 100644 --- a/src/utils/clack-utils.ts +++ b/src/utils/clack-utils.ts @@ -1278,32 +1278,35 @@ export async function askShouldCreateExamplePage( ); } -export async function featureSelectionPrompt(features: Feature[]) { +export async function featureSelectionPrompt>( + features: F, +): Promise<{ [key in F[number]['id']]: boolean }> { return traceStep('feature-selection', async () => { - const selectedFeatures = []; + const selectedFeatures: Record = {}; for (const feature of features) { - const selected = await clack.select({ - message: `Do you want to set up ${feature.name}?`, - options: [ - { - value: true, - label: 'Yes', - hint: feature.enabledHint, - }, - { - value: false, - label: 'No', - hint: feature.disabledHint, - }, - ], - }); + 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, + }, + ], + }), + ); - if (selected) { - selectedFeatures.push(feature.id); - } + selectedFeatures[feature.id] = selected; } - return selectedFeatures; + return selectedFeatures as { [key in F[number]['id']]: boolean }; }); } diff --git a/src/utils/types.ts b/src/utils/types.ts index 5c45d2c0..86e05e8a 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -47,7 +47,7 @@ export type WizardOptions = { export interface Feature { id: string; - name: string; + prompt: string; enabledHint?: string; disabledHint?: string; } diff --git a/test/nextjs/templates.test.ts b/test/nextjs/templates.test.ts index 5c0c345e..f2e7d988 100644 --- a/test/nextjs/templates.test.ts +++ b/test/nextjs/templates.test.ts @@ -1,6 +1,6 @@ import { getWithSentryConfigOptionsTemplate } from '../../src/nextjs/templates'; -describe('NextJS code templates', () => { +describe('Next.js code templates', () => { describe('getWithSentryConfigOptionsTemplate', () => { it('generates options for SaaS', () => { const template = getWithSentryConfigOptionsTemplate({ From 85794d288f8bfda81f74ee35c44c7fc22b6c3bf2 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Wed, 7 Aug 2024 14:15:51 +0100 Subject: [PATCH 11/11] Add tests --- test/nextjs/templates.test.ts | 265 +++++++++++++++++++++++++++++++++- 1 file changed, 264 insertions(+), 1 deletion(-) diff --git a/test/nextjs/templates.test.ts b/test/nextjs/templates.test.ts index f2e7d988..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('getWithSentryConfigOptionsTemplate', () => { it('generates options for SaaS', () => { const template = getWithSentryConfigOptionsTemplate({