From 983f3f0b376d4c982187a32e432fdccb532561ed Mon Sep 17 00:00:00 2001 From: Cody De Arkland Date: Wed, 7 Jan 2026 21:05:23 -0800 Subject: [PATCH 01/12] feat(nextjs): Add --skip-auth flag for headless CLI operation Add support for fully headless/automated Sentry setup without browser authentication. This enables CI/CD pipelines and AI agents to scaffold Sentry configuration with environment variable placeholders. New CLI flags: - --skip-auth: Skip authentication, use env var placeholders - --tracing: Enable performance monitoring - --replay: Enable Session Replay - --logs: Enable Sentry Logs - --tunnel-route: Enable tunnel route - --mcp-cursor/vscode/claude/opencode/jetbrains: Add MCP configs In skip-auth mode: - Config files use process.env.SENTRY_DSN / NEXT_PUBLIC_SENTRY_DSN - next.config uses process.env.SENTRY_ORG / SENTRY_PROJECT - Creates .env.example documenting required variables - MCP configs use base URL without project scope - Skips example page, CI setup, and turbopack warning --- CHANGELOG.md | 37 ++++ bin.ts | 52 +++++ src/nextjs/nextjs-wizard.ts | 390 +++++++++++++++++++++++++--------- src/nextjs/templates.ts | 56 ++++- src/run.ts | 23 ++ src/utils/clack/mcp-config.ts | 10 +- src/utils/types.ts | 70 ++++++ 7 files changed, 524 insertions(+), 114 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f525f2d3..8b9fd271f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,42 @@ # Changelog +## Unreleased + +### Features + +- feat(nextjs): Add `--skip-auth` flag for headless CLI operation ([#TBD](https://github.com/getsentry/sentry-wizard/pull/TBD)) + + The Next.js wizard now supports a `--skip-auth` mode that scaffolds all Sentry files with environment + variable placeholders instead of connecting to Sentry for authentication. This enables fully headless + CLI operation where configuration values can be populated later (e.g., by an AI agent or CI/CD). + + New CLI flags: + + - `--skip-auth`: Skip Sentry authentication and use env var placeholders + - `--tracing`: Enable performance/tracing (with `--skip-auth`) + - `--replay`: Enable Session Replay (with `--skip-auth`) + - `--logs`: Enable Sentry Logs (with `--skip-auth`) + - `--tunnel-route`: Enable tunnel route (with `--skip-auth`) + - `--mcp-cursor`: Add MCP config for Cursor (with `--skip-auth`) + - `--mcp-vscode`: Add MCP config for VS Code (with `--skip-auth`) + - `--mcp-claude`: Add MCP config for Claude Code (with `--skip-auth`) + - `--mcp-opencode`: Add MCP config for OpenCode (with `--skip-auth`) + - `--mcp-jetbrains`: Show MCP config for JetBrains IDEs (with `--skip-auth`) + + Example usage: + + ```bash + npx @sentry/wizard -i nextjs --skip-auth --tracing --replay --logs --mcp-opencode --ignore-git-changes + ``` + + When using `--skip-auth`, the wizard: + + - Creates config files with `process.env.SENTRY_DSN` / `process.env.NEXT_PUBLIC_SENTRY_DSN` + - Uses `process.env.SENTRY_ORG` and `process.env.SENTRY_PROJECT` in next.config + - Creates `.env.example` documenting required environment variables + - Skips example page creation, CI setup, and turbopack warning + - Uses base MCP URL (`mcp.sentry.dev/mcp`) without project scope + ## 6.10.0 - chore(deps): Upgrade `@sentry/node` from v7 to v10.29.0 ([#1126](https://github.com/getsentry/sentry-wizard/pull/1126)) diff --git a/bin.ts b/bin.ts index 6692c1e47..b75aa772c 100644 --- a/bin.ts +++ b/bin.ts @@ -160,6 +160,58 @@ const argv = yargs(hideBin(process.argv), process.cwd()) 'Enable Spotlight for local development. This does not require a Sentry account or project.', type: 'boolean', }, + 'skip-auth': { + default: false, + describe: + 'Skip Sentry authentication and use environment variable placeholders. Enables fully headless CLI operation.', + type: 'boolean', + }, + tracing: { + default: false, + describe: 'Enable performance/tracing monitoring (used with --skip-auth)', + type: 'boolean', + }, + replay: { + default: false, + describe: 'Enable Session Replay (used with --skip-auth)', + type: 'boolean', + }, + logs: { + default: false, + describe: 'Enable Sentry Logs (used with --skip-auth)', + type: 'boolean', + }, + 'tunnel-route': { + default: false, + describe: + 'Enable tunnel route for ad-blocker circumvention (used with --skip-auth)', + type: 'boolean', + }, + 'mcp-cursor': { + default: false, + describe: 'Add MCP config for Cursor (used with --skip-auth)', + type: 'boolean', + }, + 'mcp-vscode': { + default: false, + describe: 'Add MCP config for VS Code (used with --skip-auth)', + type: 'boolean', + }, + 'mcp-claude': { + default: false, + describe: 'Add MCP config for Claude Code (used with --skip-auth)', + type: 'boolean', + }, + 'mcp-opencode': { + default: false, + describe: 'Add MCP config for OpenCode (used with --skip-auth)', + type: 'boolean', + }, + 'mcp-jetbrains': { + default: false, + describe: 'Show MCP config for JetBrains IDEs (used with --skip-auth)', + type: 'boolean', + }, 'xcode-project-dir': xcodeProjectDirOption, ...PRESELECTED_PROJECT_OPTIONS, }) diff --git a/src/nextjs/nextjs-wizard.ts b/src/nextjs/nextjs-wizard.ts index a86750d5f..56ebc867d 100644 --- a/src/nextjs/nextjs-wizard.ts +++ b/src/nextjs/nextjs-wizard.ts @@ -31,7 +31,14 @@ import { } from '../utils/clack'; import { getPackageVersion, hasPackageInstalled } from '../utils/package-json'; import type { SentryProjectData, WizardOptions } from '../utils/types'; -import { offerProjectScopedMcpConfig } from '../utils/clack/mcp-config'; +import { + offerProjectScopedMcpConfig, + addCursorMcpConfig, + addVsCodeMcpConfig, + addClaudeCodeMcpConfig, + addOpenCodeMcpConfig, + showJetBrainsMcpConfig, +} from '../utils/clack/mcp-config'; import { getFullUnderscoreErrorCopyPasteSnippet, getGlobalErrorCopyPasteSnippet, @@ -54,6 +61,7 @@ import { getInstrumentationClientHookCopyPasteSnippet, getRootLayoutWithGenerateMetadata, getGenerateMetadataSnippet, + getSentryEnvExampleContents, } from './templates'; import { getMaybeAppDirLocation, @@ -77,7 +85,7 @@ export function runNextjsWizard(options: WizardOptions) { export async function runNextjsWizardWithTelemetry( options: WizardOptions, ): Promise { - const { promoCode, telemetryEnabled, forceInstall } = options; + const { promoCode, telemetryEnabled, forceInstall, skipAuth } = options; printWelcome({ wizardName: 'Sentry Next.js Wizard', @@ -99,37 +107,29 @@ export async function runNextjsWizardWithTelemetry( const nextVersion = getPackageVersion('next', packageJson); Sentry.setTag('nextjs-version', getNextJsVersionBucket(nextVersion)); - const projectData = await getOrAskForProjectData( - options, - 'javascript-nextjs', - ); - - const sdkAlreadyInstalled = hasPackageInstalled( - '@sentry/nextjs', - packageJson, - ); - Sentry.setTag('sdk-already-installed', sdkAlreadyInstalled); - - const { packageManager: packageManagerFromInstallStep } = - await installPackage({ - packageName: '@sentry/nextjs@^10', - packageNameDisplayLabel: '@sentry/nextjs', - alreadyInstalled: !!packageJson?.dependencies?.['@sentry/nextjs'], - forceInstall, - }); - + // In skip-auth mode, we don't need to authenticate with Sentry + // We'll use environment variable placeholders instead let selectedProject: SentryProjectData; let authToken: string; let selfHosted: boolean; let sentryUrl: string; let spotlight: boolean; + let useEnvVars: boolean; + + if (skipAuth) { + Sentry.setTag('skip-auth-mode', true); + clack.log.info( + chalk.cyan( + 'Running in skip-auth mode. Environment variable placeholders will be used.', + ), + ); - if (projectData.spotlight) { - // Spotlight mode: use empty DSN and skip auth - spotlight = true; + // Skip auth mode: use placeholder data with env vars + spotlight = false; selfHosted = false; - sentryUrl = ''; + sentryUrl = 'https://sentry.io/'; authToken = ''; + useEnvVars = true; // Create a minimal project structure for type compatibility selectedProject = { id: '', @@ -138,13 +138,55 @@ export async function runNextjsWizardWithTelemetry( keys: [{ dsn: { public: '' } }], }; } else { - spotlight = false; - ({ selectedProject, authToken, selfHosted, sentryUrl } = projectData); + useEnvVars = false; + const projectData = await getOrAskForProjectData( + options, + 'javascript-nextjs', + ); + + if (projectData.spotlight) { + // Spotlight mode: use empty DSN and skip auth + spotlight = true; + selfHosted = false; + sentryUrl = ''; + authToken = ''; + // Create a minimal project structure for type compatibility + selectedProject = { + id: '', + slug: '', + organization: { id: '', slug: '', name: '' }, + keys: [{ dsn: { public: '' } }], + }; + } else { + spotlight = false; + ({ selectedProject, authToken, selfHosted, sentryUrl } = projectData); + } } - const { logsEnabled } = await traceStep('configure-sdk', async () => { - const tunnelRoute = await askShouldSetTunnelRoute(); + const sdkAlreadyInstalled = hasPackageInstalled( + '@sentry/nextjs', + packageJson, + ); + Sentry.setTag('sdk-already-installed', sdkAlreadyInstalled); + + const { packageManager: packageManagerFromInstallStep } = + await installPackage({ + packageName: '@sentry/nextjs@^10', + packageNameDisplayLabel: '@sentry/nextjs', + alreadyInstalled: !!packageJson?.dependencies?.['@sentry/nextjs'], + forceInstall, + }); + + // Determine tunnel route setting + let tunnelRoute: boolean; + if (skipAuth) { + // In skip-auth mode, use CLI flag (defaults to false) + tunnelRoute = options.tunnelRoute ?? false; + } else { + tunnelRoute = await askShouldSetTunnelRoute(); + } + const { logsEnabled } = await traceStep('configure-sdk', async () => { return await createOrMergeNextJsFiles( selectedProject, selfHosted, @@ -153,6 +195,8 @@ export async function runNextjsWizardWithTelemetry( tunnelRoute, }, spotlight, + skipAuth ? options : undefined, + useEnvVars, ); }); @@ -377,78 +421,181 @@ export async function runNextjsWizardWithTelemetry( } }); - const shouldCreateExamplePage = await askShouldCreateExamplePage(); - if (shouldCreateExamplePage) { - await traceStep('create-example-page', async () => - createExamplePage( - selfHosted, - selectedProject, - sentryUrl, - typeScriptDetected, - logsEnabled, - ), - ); + // In skip-auth mode, skip example page creation + let shouldCreateExamplePage = false; + if (!skipAuth) { + shouldCreateExamplePage = await askShouldCreateExamplePage(); + if (shouldCreateExamplePage) { + await traceStep('create-example-page', async () => + createExamplePage( + selfHosted, + selectedProject, + sentryUrl, + typeScriptDetected, + logsEnabled, + ), + ); + } } - if (!spotlight) { + // In skip-auth mode, create .env.example instead of .env.sentry-build-plugin + if (skipAuth) { + await traceStep('create-env-example', async () => { + const envExamplePath = path.join(process.cwd(), '.env.example'); + const envExampleExists = fs.existsSync(envExamplePath); + + if (envExampleExists) { + // Append Sentry env vars to existing .env.example + const existingContent = fs.readFileSync(envExamplePath, 'utf8'); + if (!existingContent.includes('SENTRY_DSN')) { + await fs.promises.appendFile( + envExamplePath, + '\n' + getSentryEnvExampleContents(), + 'utf8', + ); + clack.log.success( + `Added Sentry environment variables to ${chalk.cyan( + '.env.example', + )}.`, + ); + } else { + clack.log.info( + `${chalk.cyan( + '.env.example', + )} already contains Sentry configuration.`, + ); + } + } else { + await fs.promises.writeFile( + envExamplePath, + getSentryEnvExampleContents(), + { encoding: 'utf8', flag: 'w' }, + ); + clack.log.success(`Created ${chalk.cyan('.env.example')}.`); + } + }); + } else if (!spotlight) { await addDotEnvSentryBuildPluginFile(authToken); } - const isLikelyUsingTurbopack = await checkIfLikelyIsUsingTurbopack(); - if (isLikelyUsingTurbopack || isLikelyUsingTurbopack === null) { - await abortIfCancelled( - clack.select({ - message: - 'Warning: The Sentry SDK is only compatible with Turbopack on Next.js version 15.4.1 or later.', - options: [ - { - label: 'I understand.', - hint: 'press enter', - value: true, - }, - ], - initialValue: true, - }), - ); + // Skip turbopack warning in skip-auth mode + if (!skipAuth) { + const isLikelyUsingTurbopack = await checkIfLikelyIsUsingTurbopack(); + if (isLikelyUsingTurbopack || isLikelyUsingTurbopack === null) { + await abortIfCancelled( + clack.select({ + message: + 'Warning: The Sentry SDK is only compatible with Turbopack on Next.js version 15.4.1 or later.', + options: [ + { + label: 'I understand.', + hint: 'press enter', + value: true, + }, + ], + initialValue: true, + }), + ); + } } - const mightBeUsingVercel = fs.existsSync( - path.join(process.cwd(), 'vercel.json'), - ); - - if (mightBeUsingVercel && !options.comingFrom) { - clack.log.info( - "▲ It seems like you're using Vercel. We recommend using the Sentry Vercel \ - integration to set up an auth token for Vercel deployments: https://vercel.com/integrations/sentry", + // Skip CI setup in skip-auth mode + if (!skipAuth) { + const mightBeUsingVercel = fs.existsSync( + path.join(process.cwd(), 'vercel.json'), ); - } else if (!spotlight) { - await setupCI('nextjs', authToken, options.comingFrom); + + if (mightBeUsingVercel && !options.comingFrom) { + clack.log.info( + "▲ It seems like you're using Vercel. We recommend using the Sentry Vercel \ + integration to set up an auth token for Vercel deployments: https://vercel.com/integrations/sentry", + ); + } else if (!spotlight) { + await setupCI('nextjs', authToken, options.comingFrom); + } } const packageManagerForOutro = packageManagerFromInstallStep ?? (await getPackageManager()); - // Offer optional project-scoped MCP config for Sentry with org and project scope - await offerProjectScopedMcpConfig( - selectedProject.organization.slug, - selectedProject.slug, - ); + // Handle MCP config based on mode + if (skipAuth) { + // In skip-auth mode, use CLI flags to determine which MCP configs to create + // Use base MCP URL without org/project scope + const hasMcpFlags = + options.mcpCursor || + options.mcpVscode || + options.mcpClaude || + options.mcpOpencode || + options.mcpJetbrains; + + if (hasMcpFlags) { + clack.log.info('Adding MCP configurations...'); + + if (options.mcpCursor) { + await addCursorMcpConfig(); + } + if (options.mcpVscode) { + await addVsCodeMcpConfig(); + } + if (options.mcpClaude) { + await addClaudeCodeMcpConfig(); + } + if (options.mcpOpencode) { + await addOpenCodeMcpConfig(); + } + if (options.mcpJetbrains) { + await showJetBrainsMcpConfig(); + } + } + } else { + // Offer optional project-scoped MCP config for Sentry with org and project scope + await offerProjectScopedMcpConfig( + selectedProject.organization.slug, + selectedProject.slug, + ); + } // Run formatters as the last step to fix any formatting issues in generated/modified files await runFormatters({ cwd: undefined }); - clack.outro(` + // Different outro message for skip-auth mode + if (skipAuth) { + clack.outro(` +${chalk.green('Successfully scaffolded the Sentry Next.js SDK!')} + +${chalk.yellow('Next steps:')} +1. Copy ${chalk.cyan('.env.example')} to ${chalk.cyan('.env.local')} +2. Fill in your Sentry DSN, org, project, and auth token from ${chalk.cyan( + 'https://sentry.io', + )} +3. Restart your dev environment (e.g. ${chalk.cyan( + `${packageManagerForOutro.runScriptCommand} dev`, + )}) + +${chalk.dim('Environment variables needed:')} + - SENTRY_DSN / NEXT_PUBLIC_SENTRY_DSN + - SENTRY_ORG + - SENTRY_PROJECT + - SENTRY_AUTH_TOKEN + +${chalk.dim( + 'If you encounter any issues, let us know here: https://github.com/getsentry/sentry-javascript/issues', +)}`); + } else { + clack.outro(` ${chalk.green('Successfully installed the Sentry Next.js SDK!')} ${ - shouldCreateExamplePage - ? `\n\nYou can validate your setup by (re)starting your dev environment (e.g. ${chalk.cyan( - `${packageManagerForOutro.runScriptCommand} dev`, - )}) and visiting ${chalk.cyan('"/sentry-example-page"')}` - : '' - } + shouldCreateExamplePage + ? `\n\nYou can validate your setup by (re)starting your dev environment (e.g. ${chalk.cyan( + `${packageManagerForOutro.runScriptCommand} dev`, + )}) and visiting ${chalk.cyan('"/sentry-example-page"')}` + : '' + } ${chalk.dim( 'If you encounter any issues, let us know here: https://github.com/getsentry/sentry-javascript/issues', )}`); + } } type SDKConfigOptions = { @@ -461,31 +608,61 @@ async function createOrMergeNextJsFiles( sentryUrl: string, sdkConfigOptions: SDKConfigOptions, spotlight = false, + skipAuthOptions?: WizardOptions, + useEnvVars = false, ): Promise<{ logsEnabled: boolean }> { const dsn = selectedProject.keys[0].dsn.public; - 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( - 'Session Replay', - )} to get a video-like reproduction of errors during a user session?`, - enabledHint: 'recommended, but increases bundle size', - }, - { - id: 'logs', - prompt: `Do you want to enable ${chalk.bold( - 'Logs', - )} to send your application logs to Sentry?`, - enabledHint: 'recommended', - }, - ] as const); + + // In skip-auth mode, use CLI flags for features instead of prompts + let selectedFeatures: { + performance: boolean; + replay: boolean; + logs: boolean; + }; + + if (skipAuthOptions) { + // Use CLI flags (default to false if not provided) + selectedFeatures = { + performance: skipAuthOptions.tracing ?? false, + replay: skipAuthOptions.replay ?? false, + logs: skipAuthOptions.logs ?? false, + }; + clack.log.info( + `Features enabled: ${chalk.cyan( + [ + selectedFeatures.performance && 'Tracing', + selectedFeatures.replay && 'Session Replay', + selectedFeatures.logs && 'Logs', + ] + .filter(Boolean) + .join(', ') || 'None', + )}`, + ); + } else { + 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( + 'Session Replay', + )} to get a video-like reproduction of errors during a user session?`, + enabledHint: 'recommended, but increases bundle size', + }, + { + id: 'logs', + prompt: `Do you want to enable ${chalk.bold( + 'Logs', + )} to send your application logs to Sentry?`, + enabledHint: 'recommended', + }, + ] as const); + } const typeScriptDetected = isUsingTypeScript(); @@ -546,6 +723,7 @@ async function createOrMergeNextJsFiles( configVariant, selectedFeatures, spotlight, + useEnvVars, ), { encoding: 'utf8', flag: 'w' }, ); @@ -713,7 +891,12 @@ async function createOrMergeNextJsFiles( const successfullyCreated = await createNewConfigFile( newInstrumentationClientHookPath, - getInstrumentationClientFileContents(dsn, selectedFeatures, spotlight), + getInstrumentationClientFileContents( + dsn, + selectedFeatures, + spotlight, + useEnvVars, + ), ); if (!successfullyCreated) { @@ -751,6 +934,7 @@ async function createOrMergeNextJsFiles( selfHosted, sentryUrl, tunnelRoute: sdkConfigOptions.tunnelRoute, + useEnvVars, }); const nextConfigPossibleFilesMap = { diff --git a/src/nextjs/templates.ts b/src/nextjs/templates.ts index 08ee1338a..80130d308 100644 --- a/src/nextjs/templates.ts +++ b/src/nextjs/templates.ts @@ -1,12 +1,43 @@ import chalk from 'chalk'; import { makeCodeSnippet } from '../utils/clack'; +/** + * Generates the content for .env.example file with Sentry environment variables. + * Used in --skip-auth mode to document required environment variables. + */ +export function getSentryEnvExampleContents(): string { + return `# Sentry Configuration +# These environment variables are required for Sentry to work properly. +# Copy this file to .env.local and fill in the values from your Sentry project. + +# Your Sentry DSN (from Project Settings > Client Keys) +# Used on the server-side +SENTRY_DSN= + +# Your Sentry DSN for client-side (same value as SENTRY_DSN) +# The NEXT_PUBLIC_ prefix exposes this to the browser +NEXT_PUBLIC_SENTRY_DSN= + +# Your Sentry organization slug (from Organization Settings) +SENTRY_ORG= + +# Your Sentry project slug (from Project Settings) +SENTRY_PROJECT= + +# Auth token for source map uploads (from Organization Settings > Auth Tokens) +# Required for source map uploads during build +# Create a token with org:read and project:releases scopes +SENTRY_AUTH_TOKEN= +`; +} + type WithSentryConfigOptions = { orgSlug: string; projectSlug: string; selfHosted: boolean; sentryUrl: string; tunnelRoute: boolean; + useEnvVars?: boolean; }; export function getWithSentryConfigOptionsTemplate({ @@ -15,15 +46,19 @@ export function getWithSentryConfigOptionsTemplate({ selfHosted, tunnelRoute, sentryUrl, + useEnvVars = false, }: WithSentryConfigOptions): string { + const orgValue = useEnvVars ? 'process.env.SENTRY_ORG' : `"${orgSlug}"`; + const projectValue = useEnvVars + ? 'process.env.SENTRY_PROJECT' + : `"${projectSlug}"`; + return `{ // For all available options, see: // https://www.npmjs.com/package/@sentry/webpack-plugin#options - org: "${orgSlug}", - project: "${projectSlug}",${ - selfHosted ? `\n sentryUrl: "${sentryUrl}",` : '' - } + org: ${orgValue}, + project: ${projectValue},${selfHosted ? `\n sentryUrl: "${sentryUrl}",` : ''} // Only print logs for uploading source maps in CI silent: !process.env.CI, @@ -138,6 +173,7 @@ export function getSentryServersideConfigContents( logs: boolean; }, spotlight = false, + useEnvVars = false, ): string { let primer = ''; if (config === 'server') { @@ -169,13 +205,15 @@ export function getSentryServersideConfigContents( const spotlightOptions = getSpotlightOption(spotlight); + const dsnValue = useEnvVars ? 'process.env.SENTRY_DSN' : `"${dsn}"`; + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions return `${primer} import * as Sentry from "@sentry/nextjs"; Sentry.init({ - dsn: "${dsn}",${performanceOptions}${logsOptions} + dsn: ${dsnValue},${performanceOptions}${logsOptions} // Enable sending user PII (Personally Identifiable Information) // https://docs.sentry.io/platforms/javascript/guides/nextjs/configuration/options/#sendDefaultPii @@ -192,6 +230,7 @@ export function getInstrumentationClientFileContents( logs: boolean; }, spotlight = false, + useEnvVars = false, ): string { const integrationsOptions = getClientIntegrationsSnippet({ replay: selectedFeaturesMap.replay, @@ -228,6 +267,11 @@ export function getInstrumentationClientFileContents( const spotlightOptions = getSpotlightOption(spotlight); + // Client-side needs NEXT_PUBLIC_ prefix for env vars + const dsnValue = useEnvVars + ? 'process.env.NEXT_PUBLIC_SENTRY_DSN' + : `"${dsn}"`; + return `// This file configures the initialization of Sentry on the client. // The added config here will be used whenever a users loads a page in their browser. // https://docs.sentry.io/platforms/javascript/guides/nextjs/ @@ -235,7 +279,7 @@ export function getInstrumentationClientFileContents( import * as Sentry from "@sentry/nextjs"; Sentry.init({ - dsn: "${dsn}",${integrationsOptions}${performanceOptions}${logsOptions}${replayOptions} + dsn: ${dsnValue},${integrationsOptions}${performanceOptions}${logsOptions}${replayOptions} // Enable sending user PII (Personally Identifiable Information) // https://docs.sentry.io/platforms/javascript/guides/nextjs/configuration/options/#sendDefaultPii diff --git a/src/run.ts b/src/run.ts index c3304b08e..5c6b51459 100644 --- a/src/run.ts +++ b/src/run.ts @@ -68,6 +68,18 @@ type Args = { comingFrom?: string; ignoreGitChanges?: boolean; xcodeProjectDir?: string; + + // Headless mode options + skipAuth?: boolean; + tracing?: boolean; + replay?: boolean; + logs?: boolean; + tunnelRoute?: boolean; + mcpCursor?: boolean; + mcpVscode?: boolean; + mcpClaude?: boolean; + mcpOpencode?: boolean; + mcpJetbrains?: boolean; }; function preSelectedProjectArgsToObject( @@ -156,6 +168,17 @@ export async function run(argv: Args) { comingFrom: finalArgs.comingFrom, ignoreGitChanges: finalArgs.ignoreGitChanges, spotlight: finalArgs.spotlight, + // Headless mode options + skipAuth: finalArgs.skipAuth, + tracing: finalArgs.tracing, + replay: finalArgs.replay, + logs: finalArgs.logs, + tunnelRoute: finalArgs.tunnelRoute, + mcpCursor: finalArgs.mcpCursor, + mcpVscode: finalArgs.mcpVscode, + mcpClaude: finalArgs.mcpClaude, + mcpOpencode: finalArgs.mcpOpencode, + mcpJetbrains: finalArgs.mcpJetbrains, }; switch (integration) { diff --git a/src/utils/clack/mcp-config.ts b/src/utils/clack/mcp-config.ts index 99c0655d3..4c97d738d 100644 --- a/src/utils/clack/mcp-config.ts +++ b/src/utils/clack/mcp-config.ts @@ -143,7 +143,7 @@ function getGenericMcpJsonSnippet( return JSON.stringify(obj, null, 2); } -async function addCursorMcpConfig( +export async function addCursorMcpConfig( orgSlug?: string, projectSlug?: string, ): Promise { @@ -172,7 +172,7 @@ async function addCursorMcpConfig( } } -async function addVsCodeMcpConfig( +export async function addVsCodeMcpConfig( orgSlug?: string, projectSlug?: string, ): Promise { @@ -202,7 +202,7 @@ async function addVsCodeMcpConfig( } } -async function addClaudeCodeMcpConfig( +export async function addClaudeCodeMcpConfig( orgSlug?: string, projectSlug?: string, ): Promise { @@ -229,7 +229,7 @@ async function addClaudeCodeMcpConfig( } } -async function addOpenCodeMcpConfig( +export async function addOpenCodeMcpConfig( orgSlug?: string, projectSlug?: string, ): Promise { @@ -296,7 +296,7 @@ async function copyToClipboard(text: string): Promise { /** * Shows MCP configuration for JetBrains IDEs with copy-to-clipboard option */ -async function showJetBrainsMcpConfig( +export async function showJetBrainsMcpConfig( orgSlug?: string, projectSlug?: string, ): Promise { diff --git a/src/utils/types.ts b/src/utils/types.ts index 9f4e2dd49..2be5badb1 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -89,6 +89,76 @@ export type WizardOptions = { * This can be passed via the `--spotlight` arg. */ spotlight?: boolean; + + /** + * Skip Sentry authentication and use environment variable placeholders. + * Enables fully headless CLI operation where an agent can populate values later. + * This can be passed via the `--skip-auth` arg. + */ + skipAuth?: boolean; + + /** + * Enable performance/tracing monitoring. + * Used with --skip-auth for headless mode. + * This can be passed via the `--tracing` arg. + */ + tracing?: boolean; + + /** + * Enable Session Replay. + * Used with --skip-auth for headless mode. + * This can be passed via the `--replay` arg. + */ + replay?: boolean; + + /** + * Enable Sentry Logs. + * Used with --skip-auth for headless mode. + * This can be passed via the `--logs` arg. + */ + logs?: boolean; + + /** + * Enable tunnel route for ad-blocker circumvention. + * Used with --skip-auth for headless mode. + * This can be passed via the `--tunnel-route` arg. + */ + tunnelRoute?: boolean; + + /** + * Add MCP config for Cursor. + * Used with --skip-auth for headless mode. + * This can be passed via the `--mcp-cursor` arg. + */ + mcpCursor?: boolean; + + /** + * Add MCP config for VS Code. + * Used with --skip-auth for headless mode. + * This can be passed via the `--mcp-vscode` arg. + */ + mcpVscode?: boolean; + + /** + * Add MCP config for Claude Code. + * Used with --skip-auth for headless mode. + * This can be passed via the `--mcp-claude` arg. + */ + mcpClaude?: boolean; + + /** + * Add MCP config for OpenCode. + * Used with --skip-auth for headless mode. + * This can be passed via the `--mcp-opencode` arg. + */ + mcpOpencode?: boolean; + + /** + * Show MCP config for JetBrains IDEs. + * Used with --skip-auth for headless mode. + * This can be passed via the `--mcp-jetbrains` arg. + */ + mcpJetbrains?: boolean; }; export interface Feature { From e2f89b68c302ed6e87cd0da0ea925a0b07ecf2b1 Mon Sep 17 00:00:00 2001 From: Cody De Arkland Date: Thu, 8 Jan 2026 11:52:31 -0800 Subject: [PATCH 02/12] refactor(nextjs): Make CLI flags independent and consolidate MCP options Address PR review feedback: 1. Revert CHANGELOG.md - use PR description instead 2. Simplify to single NEXT_PUBLIC_SENTRY_DSN env var (works for both server and client) 3. Consolidate MCP flags into single --mcp array option: --mcp cursor,vscode or --mcp cursor --mcp vscode 4. Make all flags work independently (not tied to --skip-auth): - --tracing, --replay, --logs: use flag if set, prompt otherwise - --tunnel-route: use flag if set, prompt otherwise - --example-page: use flag if set, prompt otherwise (defaults to false in skip-auth mode) 5. Turbopack warning: log as warning in skip-auth mode instead of skip 6. CI setup: show helpful log message in skip-auth mode instead of skip --- CHANGELOG.md | 37 ------- bin.ts | 43 +++----- src/nextjs/nextjs-wizard.ts | 212 +++++++++++++++++++++--------------- src/nextjs/templates.ts | 11 +- src/run.ts | 14 +-- src/utils/types.ts | 46 +++----- 6 files changed, 157 insertions(+), 206 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b9fd271f..6f525f2d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,42 +1,5 @@ # Changelog -## Unreleased - -### Features - -- feat(nextjs): Add `--skip-auth` flag for headless CLI operation ([#TBD](https://github.com/getsentry/sentry-wizard/pull/TBD)) - - The Next.js wizard now supports a `--skip-auth` mode that scaffolds all Sentry files with environment - variable placeholders instead of connecting to Sentry for authentication. This enables fully headless - CLI operation where configuration values can be populated later (e.g., by an AI agent or CI/CD). - - New CLI flags: - - - `--skip-auth`: Skip Sentry authentication and use env var placeholders - - `--tracing`: Enable performance/tracing (with `--skip-auth`) - - `--replay`: Enable Session Replay (with `--skip-auth`) - - `--logs`: Enable Sentry Logs (with `--skip-auth`) - - `--tunnel-route`: Enable tunnel route (with `--skip-auth`) - - `--mcp-cursor`: Add MCP config for Cursor (with `--skip-auth`) - - `--mcp-vscode`: Add MCP config for VS Code (with `--skip-auth`) - - `--mcp-claude`: Add MCP config for Claude Code (with `--skip-auth`) - - `--mcp-opencode`: Add MCP config for OpenCode (with `--skip-auth`) - - `--mcp-jetbrains`: Show MCP config for JetBrains IDEs (with `--skip-auth`) - - Example usage: - - ```bash - npx @sentry/wizard -i nextjs --skip-auth --tracing --replay --logs --mcp-opencode --ignore-git-changes - ``` - - When using `--skip-auth`, the wizard: - - - Creates config files with `process.env.SENTRY_DSN` / `process.env.NEXT_PUBLIC_SENTRY_DSN` - - Uses `process.env.SENTRY_ORG` and `process.env.SENTRY_PROJECT` in next.config - - Creates `.env.example` documenting required environment variables - - Skips example page creation, CI setup, and turbopack warning - - Uses base MCP URL (`mcp.sentry.dev/mcp`) without project scope - ## 6.10.0 - chore(deps): Upgrade `@sentry/node` from v7 to v10.29.0 ([#1126](https://github.com/getsentry/sentry-wizard/pull/1126)) diff --git a/bin.ts b/bin.ts index b75aa772c..1a7ba64d1 100644 --- a/bin.ts +++ b/bin.ts @@ -167,50 +167,33 @@ const argv = yargs(hideBin(process.argv), process.cwd()) type: 'boolean', }, tracing: { - default: false, - describe: 'Enable performance/tracing monitoring (used with --skip-auth)', + describe: + 'Enable performance/tracing monitoring. When set, skips the tracing prompt.', type: 'boolean', }, replay: { - default: false, - describe: 'Enable Session Replay (used with --skip-auth)', + describe: 'Enable Session Replay. When set, skips the replay prompt.', type: 'boolean', }, logs: { - default: false, - describe: 'Enable Sentry Logs (used with --skip-auth)', + describe: 'Enable Sentry Logs. When set, skips the logs prompt.', type: 'boolean', }, 'tunnel-route': { - default: false, describe: - 'Enable tunnel route for ad-blocker circumvention (used with --skip-auth)', - type: 'boolean', - }, - 'mcp-cursor': { - default: false, - describe: 'Add MCP config for Cursor (used with --skip-auth)', - type: 'boolean', - }, - 'mcp-vscode': { - default: false, - describe: 'Add MCP config for VS Code (used with --skip-auth)', + 'Enable tunnel route for ad-blocker circumvention. When set, skips the tunnel route prompt.', type: 'boolean', }, - 'mcp-claude': { - default: false, - describe: 'Add MCP config for Claude Code (used with --skip-auth)', - type: 'boolean', - }, - 'mcp-opencode': { - default: false, - describe: 'Add MCP config for OpenCode (used with --skip-auth)', + 'example-page': { + describe: + 'Create an example page to test Sentry. When set, skips the example page prompt.', type: 'boolean', }, - 'mcp-jetbrains': { - default: false, - describe: 'Show MCP config for JetBrains IDEs (used with --skip-auth)', - type: 'boolean', + mcp: { + describe: + 'Add MCP (Model Context Protocol) config for specified IDE(s). Options: cursor, vscode, claude, opencode, jetbrains', + type: 'array', + choices: ['cursor', 'vscode', 'claude', 'opencode', 'jetbrains'], }, 'xcode-project-dir': xcodeProjectDirOption, ...PRESELECTED_PROJECT_OPTIONS, diff --git a/src/nextjs/nextjs-wizard.ts b/src/nextjs/nextjs-wizard.ts index 56ebc867d..3a08835c4 100644 --- a/src/nextjs/nextjs-wizard.ts +++ b/src/nextjs/nextjs-wizard.ts @@ -177,14 +177,8 @@ export async function runNextjsWizardWithTelemetry( forceInstall, }); - // Determine tunnel route setting - let tunnelRoute: boolean; - if (skipAuth) { - // In skip-auth mode, use CLI flag (defaults to false) - tunnelRoute = options.tunnelRoute ?? false; - } else { - tunnelRoute = await askShouldSetTunnelRoute(); - } + // Determine tunnel route setting - use CLI flag if provided, otherwise prompt + const tunnelRoute = options.tunnelRoute ?? (await askShouldSetTunnelRoute()); const { logsEnabled } = await traceStep('configure-sdk', async () => { return await createOrMergeNextJsFiles( @@ -195,7 +189,7 @@ export async function runNextjsWizardWithTelemetry( tunnelRoute, }, spotlight, - skipAuth ? options : undefined, + options, useEnvVars, ); }); @@ -421,21 +415,21 @@ export async function runNextjsWizardWithTelemetry( } }); - // In skip-auth mode, skip example page creation - let shouldCreateExamplePage = false; - if (!skipAuth) { - shouldCreateExamplePage = await askShouldCreateExamplePage(); - if (shouldCreateExamplePage) { - await traceStep('create-example-page', async () => - createExamplePage( - selfHosted, - selectedProject, - sentryUrl, - typeScriptDetected, - logsEnabled, - ), - ); - } + // Example page - use CLI flag if provided, otherwise prompt (skip in skip-auth mode if not explicitly enabled) + const shouldCreateExamplePage = + options.examplePage ?? + (skipAuth ? false : await askShouldCreateExamplePage()); + + if (shouldCreateExamplePage) { + await traceStep('create-example-page', async () => + createExamplePage( + selfHosted, + selectedProject, + sentryUrl, + typeScriptDetected, + logsEnabled, + ), + ); } // In skip-auth mode, create .env.example instead of .env.sentry-build-plugin @@ -478,10 +472,14 @@ export async function runNextjsWizardWithTelemetry( await addDotEnvSentryBuildPluginFile(authToken); } - // Skip turbopack warning in skip-auth mode - if (!skipAuth) { - const isLikelyUsingTurbopack = await checkIfLikelyIsUsingTurbopack(); - if (isLikelyUsingTurbopack || isLikelyUsingTurbopack === null) { + // Turbopack warning - log in skip-auth mode, prompt otherwise + const isLikelyUsingTurbopack = await checkIfLikelyIsUsingTurbopack(); + if (isLikelyUsingTurbopack || isLikelyUsingTurbopack === null) { + if (skipAuth) { + clack.log.warn( + 'The Sentry SDK is only compatible with Turbopack on Next.js version 15.4.1 or later.', + ); + } else { await abortIfCancelled( clack.select({ message: @@ -499,56 +497,57 @@ export async function runNextjsWizardWithTelemetry( } } - // Skip CI setup in skip-auth mode - if (!skipAuth) { - const mightBeUsingVercel = fs.existsSync( - path.join(process.cwd(), 'vercel.json'), - ); + // CI setup - log in skip-auth mode, prompt/setup otherwise + const mightBeUsingVercel = fs.existsSync( + path.join(process.cwd(), 'vercel.json'), + ); - if (mightBeUsingVercel && !options.comingFrom) { - clack.log.info( - "▲ It seems like you're using Vercel. We recommend using the Sentry Vercel \ - integration to set up an auth token for Vercel deployments: https://vercel.com/integrations/sentry", - ); - } else if (!spotlight) { - await setupCI('nextjs', authToken, options.comingFrom); - } + if (mightBeUsingVercel && !options.comingFrom) { + clack.log.info( + "▲ It seems like you're using Vercel. We recommend using the Sentry Vercel \ + integration to set up an auth token for Vercel deployments: https://vercel.com/integrations/sentry", + ); + } else if (skipAuth) { + clack.log.info( + `To upload source maps in CI, set ${chalk.cyan( + 'SENTRY_AUTH_TOKEN', + )} environment variable. ` + + `Create a token at ${chalk.cyan( + 'https://sentry.io/orgredirect/organizations/:orgslug/settings/auth-tokens/', + )}`, + ); + } else if (!spotlight) { + await setupCI('nextjs', authToken, options.comingFrom); } const packageManagerForOutro = packageManagerFromInstallStep ?? (await getPackageManager()); - // Handle MCP config based on mode - if (skipAuth) { - // In skip-auth mode, use CLI flags to determine which MCP configs to create - // Use base MCP URL without org/project scope - const hasMcpFlags = - options.mcpCursor || - options.mcpVscode || - options.mcpClaude || - options.mcpOpencode || - options.mcpJetbrains; - - if (hasMcpFlags) { - clack.log.info('Adding MCP configurations...'); - - if (options.mcpCursor) { - await addCursorMcpConfig(); - } - if (options.mcpVscode) { - await addVsCodeMcpConfig(); - } - if (options.mcpClaude) { - await addClaudeCodeMcpConfig(); - } - if (options.mcpOpencode) { - await addOpenCodeMcpConfig(); - } - if (options.mcpJetbrains) { - await showJetBrainsMcpConfig(); - } + // Handle MCP config - if --mcp flag provided, use it; otherwise offer interactive selection + if (options.mcp && options.mcp.length > 0) { + // Use CLI-provided MCP providers + // In skip-auth mode, use base MCP URL without org/project scope + const orgSlug = skipAuth ? undefined : selectedProject.organization.slug; + const projectSlug = skipAuth ? undefined : selectedProject.slug; + + clack.log.info('Adding MCP configurations...'); + + if (options.mcp.includes('cursor')) { + await addCursorMcpConfig(orgSlug, projectSlug); } - } else { + if (options.mcp.includes('vscode')) { + await addVsCodeMcpConfig(orgSlug, projectSlug); + } + if (options.mcp.includes('claude')) { + await addClaudeCodeMcpConfig(orgSlug, projectSlug); + } + if (options.mcp.includes('opencode')) { + await addOpenCodeMcpConfig(orgSlug, projectSlug); + } + if (options.mcp.includes('jetbrains')) { + await showJetBrainsMcpConfig(orgSlug, projectSlug); + } + } else if (!skipAuth) { // Offer optional project-scoped MCP config for Sentry with org and project scope await offerProjectScopedMcpConfig( selectedProject.organization.slug, @@ -574,7 +573,7 @@ ${chalk.yellow('Next steps:')} )}) ${chalk.dim('Environment variables needed:')} - - SENTRY_DSN / NEXT_PUBLIC_SENTRY_DSN + - NEXT_PUBLIC_SENTRY_DSN - SENTRY_ORG - SENTRY_PROJECT - SENTRY_AUTH_TOKEN @@ -608,24 +607,29 @@ async function createOrMergeNextJsFiles( sentryUrl: string, sdkConfigOptions: SDKConfigOptions, spotlight = false, - skipAuthOptions?: WizardOptions, + wizardOptions: WizardOptions, useEnvVars = false, ): Promise<{ logsEnabled: boolean }> { const dsn = selectedProject.keys[0].dsn.public; - // In skip-auth mode, use CLI flags for features instead of prompts + // Check if CLI flags are provided for features + // If a flag is set (true or false), use it; otherwise prompt the user + const tracingFlagProvided = wizardOptions.tracing !== undefined; + const replayFlagProvided = wizardOptions.replay !== undefined; + const logsFlagProvided = wizardOptions.logs !== undefined; + let selectedFeatures: { performance: boolean; replay: boolean; logs: boolean; }; - if (skipAuthOptions) { - // Use CLI flags (default to false if not provided) + // If all flags are provided via CLI, skip prompts entirely + if (tracingFlagProvided && replayFlagProvided && logsFlagProvided) { selectedFeatures = { - performance: skipAuthOptions.tracing ?? false, - replay: skipAuthOptions.replay ?? false, - logs: skipAuthOptions.logs ?? false, + performance: wizardOptions.tracing ?? false, + replay: wizardOptions.replay ?? false, + logs: wizardOptions.logs ?? false, }; clack.log.info( `Features enabled: ${chalk.cyan( @@ -639,29 +643,57 @@ async function createOrMergeNextJsFiles( )}`, ); } else { - selectedFeatures = await featureSelectionPrompt([ - { - id: 'performance', + // Build list of features to prompt for (only those not provided via CLI) + const featuresToPrompt = []; + + if (!tracingFlagProvided) { + featuresToPrompt.push({ + id: 'performance' as const, prompt: `Do you want to enable ${chalk.bold( 'Tracing', )} to track the performance of your application?`, enabledHint: 'recommended', - }, - { - id: 'replay', + }); + } + + if (!replayFlagProvided) { + featuresToPrompt.push({ + id: 'replay' as const, prompt: `Do you want to enable ${chalk.bold( 'Session Replay', )} to get a video-like reproduction of errors during a user session?`, enabledHint: 'recommended, but increases bundle size', - }, - { - id: 'logs', + }); + } + + if (!logsFlagProvided) { + featuresToPrompt.push({ + id: 'logs' as const, prompt: `Do you want to enable ${chalk.bold( 'Logs', )} to send your application logs to Sentry?`, enabledHint: 'recommended', - }, - ] as const); + }); + } + + // Prompt for features not provided via CLI + const promptedFeatures = + featuresToPrompt.length > 0 + ? await featureSelectionPrompt(featuresToPrompt) + : { performance: false, replay: false, logs: false }; + + // Merge CLI-provided flags with prompted values + selectedFeatures = { + performance: tracingFlagProvided + ? wizardOptions.tracing ?? false + : promptedFeatures.performance, + replay: replayFlagProvided + ? wizardOptions.replay ?? false + : promptedFeatures.replay, + logs: logsFlagProvided + ? wizardOptions.logs ?? false + : promptedFeatures.logs, + }; } const typeScriptDetected = isUsingTypeScript(); diff --git a/src/nextjs/templates.ts b/src/nextjs/templates.ts index 80130d308..23d1fd51a 100644 --- a/src/nextjs/templates.ts +++ b/src/nextjs/templates.ts @@ -11,11 +11,7 @@ export function getSentryEnvExampleContents(): string { # Copy this file to .env.local and fill in the values from your Sentry project. # Your Sentry DSN (from Project Settings > Client Keys) -# Used on the server-side -SENTRY_DSN= - -# Your Sentry DSN for client-side (same value as SENTRY_DSN) -# The NEXT_PUBLIC_ prefix exposes this to the browser +# The NEXT_PUBLIC_ prefix exposes this to the browser for client-side error reporting NEXT_PUBLIC_SENTRY_DSN= # Your Sentry organization slug (from Organization Settings) @@ -205,7 +201,10 @@ export function getSentryServersideConfigContents( const spotlightOptions = getSpotlightOption(spotlight); - const dsnValue = useEnvVars ? 'process.env.SENTRY_DSN' : `"${dsn}"`; + // Use NEXT_PUBLIC_SENTRY_DSN for consistency - it works on both server and client + const dsnValue = useEnvVars + ? 'process.env.NEXT_PUBLIC_SENTRY_DSN' + : `"${dsn}"`; // eslint-disable-next-line @typescript-eslint/restrict-template-expressions return `${primer} diff --git a/src/run.ts b/src/run.ts index 5c6b51459..c9b48b763 100644 --- a/src/run.ts +++ b/src/run.ts @@ -75,11 +75,8 @@ type Args = { replay?: boolean; logs?: boolean; tunnelRoute?: boolean; - mcpCursor?: boolean; - mcpVscode?: boolean; - mcpClaude?: boolean; - mcpOpencode?: boolean; - mcpJetbrains?: boolean; + examplePage?: boolean; + mcp?: string[]; }; function preSelectedProjectArgsToObject( @@ -174,11 +171,8 @@ export async function run(argv: Args) { replay: finalArgs.replay, logs: finalArgs.logs, tunnelRoute: finalArgs.tunnelRoute, - mcpCursor: finalArgs.mcpCursor, - mcpVscode: finalArgs.mcpVscode, - mcpClaude: finalArgs.mcpClaude, - mcpOpencode: finalArgs.mcpOpencode, - mcpJetbrains: finalArgs.mcpJetbrains, + examplePage: finalArgs.examplePage, + mcp: finalArgs.mcp, }; switch (integration) { diff --git a/src/utils/types.ts b/src/utils/types.ts index 2be5badb1..be8b9fccd 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -99,66 +99,46 @@ export type WizardOptions = { /** * Enable performance/tracing monitoring. - * Used with --skip-auth for headless mode. + * When set, skips the tracing prompt. * This can be passed via the `--tracing` arg. */ tracing?: boolean; /** * Enable Session Replay. - * Used with --skip-auth for headless mode. + * When set, skips the replay prompt. * This can be passed via the `--replay` arg. */ replay?: boolean; /** * Enable Sentry Logs. - * Used with --skip-auth for headless mode. + * When set, skips the logs prompt. * This can be passed via the `--logs` arg. */ logs?: boolean; /** * Enable tunnel route for ad-blocker circumvention. - * Used with --skip-auth for headless mode. + * When set, skips the tunnel route prompt. * This can be passed via the `--tunnel-route` arg. */ tunnelRoute?: boolean; /** - * Add MCP config for Cursor. - * Used with --skip-auth for headless mode. - * This can be passed via the `--mcp-cursor` arg. + * Create an example page to test Sentry. + * When set, skips the example page prompt. + * This can be passed via the `--example-page` arg. */ - mcpCursor?: boolean; + examplePage?: boolean; /** - * Add MCP config for VS Code. - * Used with --skip-auth for headless mode. - * This can be passed via the `--mcp-vscode` arg. + * MCP (Model Context Protocol) providers to configure. + * Options: cursor, vscode, claude, opencode, jetbrains + * This can be passed via the `--mcp` arg. + * Example: `--mcp cursor --mcp vscode` or `--mcp cursor,vscode` */ - mcpVscode?: boolean; - - /** - * Add MCP config for Claude Code. - * Used with --skip-auth for headless mode. - * This can be passed via the `--mcp-claude` arg. - */ - mcpClaude?: boolean; - - /** - * Add MCP config for OpenCode. - * Used with --skip-auth for headless mode. - * This can be passed via the `--mcp-opencode` arg. - */ - mcpOpencode?: boolean; - - /** - * Show MCP config for JetBrains IDEs. - * Used with --skip-auth for headless mode. - * This can be passed via the `--mcp-jetbrains` arg. - */ - mcpJetbrains?: boolean; + mcp?: string[]; }; export interface Feature { From 658072a8365246b59ebd2cdc53ae3ffdecc2edcf Mon Sep 17 00:00:00 2001 From: Cody De Arkland Date: Thu, 22 Jan 2026 09:51:01 -0800 Subject: [PATCH 03/12] ref(nextjs): rename --skip-auth to --non-interactive --- bin.ts | 4 ++-- e2e-tests/tests/help-message.test.ts | 17 +++++++++++++++++ src/nextjs/nextjs-wizard.ts | 28 +++++++++++++++------------- src/run.ts | 4 ++-- src/utils/types.ts | 8 ++++---- 5 files changed, 40 insertions(+), 21 deletions(-) diff --git a/bin.ts b/bin.ts index 1a7ba64d1..0feca8ef1 100644 --- a/bin.ts +++ b/bin.ts @@ -160,10 +160,10 @@ const argv = yargs(hideBin(process.argv), process.cwd()) 'Enable Spotlight for local development. This does not require a Sentry account or project.', type: 'boolean', }, - 'skip-auth': { + 'non-interactive': { default: false, describe: - 'Skip Sentry authentication and use environment variable placeholders. Enables fully headless CLI operation.', + 'Run in non-interactive mode. Skips all prompts and uses environment variable placeholders for auth.', type: 'boolean', }, tracing: { diff --git a/e2e-tests/tests/help-message.test.ts b/e2e-tests/tests/help-message.test.ts index 9fc84bffc..eeff8cf0b 100644 --- a/e2e-tests/tests/help-message.test.ts +++ b/e2e-tests/tests/help-message.test.ts @@ -54,6 +54,23 @@ describe('--help command', () => { --spotlight Enable Spotlight for local development. This does not require a Sentry account or project. [boolean] [default: false] + --non-interactive Run in non-interactive mode. Skips all prompts and + uses environment variable placeholders for auth. + [boolean] [default: false] + --tracing Enable performance/tracing monitoring. When set, + skips the tracing prompt. [boolean] + --replay Enable Session Replay. When set, skips the replay + prompt. [boolean] + --logs Enable Sentry Logs. When set, skips the logs prompt. + [boolean] + --tunnel-route Enable tunnel route for ad-blocker circumvention. + When set, skips the tunnel route prompt. [boolean] + --example-page Create an example page to test Sentry. When set, + skips the example page prompt. [boolean] + --mcp Add MCP (Model Context Protocol) config for + specified IDE(s). Options: cursor, vscode, claude, + opencode, jetbrains + [array] [choices: "cursor", "vscode", "claude", "opencode", "jetbrains"] --version Show version number [boolean] " `); diff --git a/src/nextjs/nextjs-wizard.ts b/src/nextjs/nextjs-wizard.ts index 3a08835c4..2574a3446 100644 --- a/src/nextjs/nextjs-wizard.ts +++ b/src/nextjs/nextjs-wizard.ts @@ -85,7 +85,7 @@ export function runNextjsWizard(options: WizardOptions) { export async function runNextjsWizardWithTelemetry( options: WizardOptions, ): Promise { - const { promoCode, telemetryEnabled, forceInstall, skipAuth } = options; + const { promoCode, telemetryEnabled, forceInstall, nonInteractive } = options; printWelcome({ wizardName: 'Sentry Next.js Wizard', @@ -107,7 +107,7 @@ export async function runNextjsWizardWithTelemetry( const nextVersion = getPackageVersion('next', packageJson); Sentry.setTag('nextjs-version', getNextJsVersionBucket(nextVersion)); - // In skip-auth mode, we don't need to authenticate with Sentry + // In non-interactive mode, we don't need to authenticate with Sentry // We'll use environment variable placeholders instead let selectedProject: SentryProjectData; let authToken: string; @@ -116,11 +116,11 @@ export async function runNextjsWizardWithTelemetry( let spotlight: boolean; let useEnvVars: boolean; - if (skipAuth) { - Sentry.setTag('skip-auth-mode', true); + if (nonInteractive) { + Sentry.setTag('non-interactive-mode', true); clack.log.info( chalk.cyan( - 'Running in skip-auth mode. Environment variable placeholders will be used.', + 'Running in non-interactive mode. Environment variable placeholders will be used.', ), ); @@ -418,7 +418,7 @@ export async function runNextjsWizardWithTelemetry( // Example page - use CLI flag if provided, otherwise prompt (skip in skip-auth mode if not explicitly enabled) const shouldCreateExamplePage = options.examplePage ?? - (skipAuth ? false : await askShouldCreateExamplePage()); + (nonInteractive ? false : await askShouldCreateExamplePage()); if (shouldCreateExamplePage) { await traceStep('create-example-page', async () => @@ -433,7 +433,7 @@ export async function runNextjsWizardWithTelemetry( } // In skip-auth mode, create .env.example instead of .env.sentry-build-plugin - if (skipAuth) { + if (nonInteractive) { await traceStep('create-env-example', async () => { const envExamplePath = path.join(process.cwd(), '.env.example'); const envExampleExists = fs.existsSync(envExamplePath); @@ -475,7 +475,7 @@ export async function runNextjsWizardWithTelemetry( // Turbopack warning - log in skip-auth mode, prompt otherwise const isLikelyUsingTurbopack = await checkIfLikelyIsUsingTurbopack(); if (isLikelyUsingTurbopack || isLikelyUsingTurbopack === null) { - if (skipAuth) { + if (nonInteractive) { clack.log.warn( 'The Sentry SDK is only compatible with Turbopack on Next.js version 15.4.1 or later.', ); @@ -507,7 +507,7 @@ export async function runNextjsWizardWithTelemetry( "▲ It seems like you're using Vercel. We recommend using the Sentry Vercel \ integration to set up an auth token for Vercel deployments: https://vercel.com/integrations/sentry", ); - } else if (skipAuth) { + } else if (nonInteractive) { clack.log.info( `To upload source maps in CI, set ${chalk.cyan( 'SENTRY_AUTH_TOKEN', @@ -527,8 +527,10 @@ export async function runNextjsWizardWithTelemetry( if (options.mcp && options.mcp.length > 0) { // Use CLI-provided MCP providers // In skip-auth mode, use base MCP URL without org/project scope - const orgSlug = skipAuth ? undefined : selectedProject.organization.slug; - const projectSlug = skipAuth ? undefined : selectedProject.slug; + const orgSlug = nonInteractive + ? undefined + : selectedProject.organization.slug; + const projectSlug = nonInteractive ? undefined : selectedProject.slug; clack.log.info('Adding MCP configurations...'); @@ -547,7 +549,7 @@ export async function runNextjsWizardWithTelemetry( if (options.mcp.includes('jetbrains')) { await showJetBrainsMcpConfig(orgSlug, projectSlug); } - } else if (!skipAuth) { + } else if (!nonInteractive) { // Offer optional project-scoped MCP config for Sentry with org and project scope await offerProjectScopedMcpConfig( selectedProject.organization.slug, @@ -559,7 +561,7 @@ export async function runNextjsWizardWithTelemetry( await runFormatters({ cwd: undefined }); // Different outro message for skip-auth mode - if (skipAuth) { + if (nonInteractive) { clack.outro(` ${chalk.green('Successfully scaffolded the Sentry Next.js SDK!')} diff --git a/src/run.ts b/src/run.ts index c9b48b763..1fbaf63a5 100644 --- a/src/run.ts +++ b/src/run.ts @@ -70,7 +70,7 @@ type Args = { xcodeProjectDir?: string; // Headless mode options - skipAuth?: boolean; + nonInteractive?: boolean; tracing?: boolean; replay?: boolean; logs?: boolean; @@ -166,7 +166,7 @@ export async function run(argv: Args) { ignoreGitChanges: finalArgs.ignoreGitChanges, spotlight: finalArgs.spotlight, // Headless mode options - skipAuth: finalArgs.skipAuth, + nonInteractive: finalArgs.nonInteractive, tracing: finalArgs.tracing, replay: finalArgs.replay, logs: finalArgs.logs, diff --git a/src/utils/types.ts b/src/utils/types.ts index be8b9fccd..dd8f779d7 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -91,11 +91,11 @@ export type WizardOptions = { spotlight?: boolean; /** - * Skip Sentry authentication and use environment variable placeholders. - * Enables fully headless CLI operation where an agent can populate values later. - * This can be passed via the `--skip-auth` arg. + * Run in non-interactive mode. Skips all prompts and uses environment variable + * placeholders for authentication. Enables fully headless CLI operation. + * This can be passed via the `--non-interactive` arg. */ - skipAuth?: boolean; + nonInteractive?: boolean; /** * Enable performance/tracing monitoring. From c7951096a0c54e74cc9743956ff5c33527f0ce5c Mon Sep 17 00:00:00 2001 From: Cody De Arkland Date: Thu, 22 Jan 2026 09:52:22 -0800 Subject: [PATCH 04/12] test(e2e): add non-interactive mode test for Next.js --- .../tests/nextjs-non-interactive.test.ts | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 e2e-tests/tests/nextjs-non-interactive.test.ts diff --git a/e2e-tests/tests/nextjs-non-interactive.test.ts b/e2e-tests/tests/nextjs-non-interactive.test.ts new file mode 100644 index 000000000..19c3f3e93 --- /dev/null +++ b/e2e-tests/tests/nextjs-non-interactive.test.ts @@ -0,0 +1,105 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { Integration } from '../../lib/Constants'; +import { createIsolatedTestEnv } from '../utils'; +import { + checkFileContents, + checkFileExists, + checkIfBuilds, + checkPackageJson, +} from '../utils'; +import { describe, beforeAll, afterAll, test, expect } from 'vitest'; + +//@ts-expect-error - clifty is ESM only +import { withEnv } from 'clifty'; + +describe('NextJS Non-Interactive Mode', () => { + const integration = Integration.nextjs; + let wizardExitCode: number; + + const { projectDir, cleanup } = createIsolatedTestEnv('nextjs-15-test-app'); + + beforeAll(async () => { + const binName = process.env.SENTRY_WIZARD_E2E_TEST_BIN + ? ['dist-bin', `sentry-wizard-${process.platform}-${process.arch}`] + : ['dist', 'bin.js']; + const binPath = path.join(__dirname, '..', '..', ...binName); + + // Run wizard in non-interactive mode with all features enabled + const command = [ + binPath, + '-i', + integration, + '--non-interactive', + '--tracing', + '--replay', + '--logs', + '--example-page', + '--disable-telemetry', + ].join(' '); + + wizardExitCode = await withEnv({ + cwd: projectDir, + debug: true, + }) + .defineInteraction() + .expectOutput('Running in non-interactive mode') + .expectOutput('Successfully installed the Sentry Next.js SDK!') + .run(command); + }); + + afterAll(() => { + cleanup(); + }); + + test('exits with exit code 0', () => { + expect(wizardExitCode).toBe(0); + }); + + test('package.json is updated correctly', () => { + checkPackageJson(projectDir, '@sentry/nextjs'); + }); + + test('.env.example is created with placeholder values', () => { + checkFileExists(`${projectDir}/.env.example`); + checkFileContents( + `${projectDir}/.env.example`, + 'SENTRY_AUTH_TOKEN=your-auth-token', + ); + }); + + test('instrumentation file is created', () => { + const hasJs = fs.existsSync(`${projectDir}/src/instrumentation.js`); + const hasTs = fs.existsSync(`${projectDir}/src/instrumentation.ts`); + expect(hasJs || hasTs).toBe(true); + }); + + test('sentry.client.config is created', () => { + const hasJs = fs.existsSync(`${projectDir}/sentry.client.config.js`); + const hasTs = fs.existsSync(`${projectDir}/sentry.client.config.ts`); + expect(hasJs || hasTs).toBe(true); + }); + + test('sentry.server.config is created', () => { + const hasJs = fs.existsSync(`${projectDir}/sentry.server.config.js`); + const hasTs = fs.existsSync(`${projectDir}/sentry.server.config.ts`); + expect(hasJs || hasTs).toBe(true); + }); + + test('sentry.edge.config is created', () => { + const hasJs = fs.existsSync(`${projectDir}/sentry.edge.config.js`); + const hasTs = fs.existsSync(`${projectDir}/sentry.edge.config.ts`); + expect(hasJs || hasTs).toBe(true); + }); + + test('example page is created', () => { + const hasExample = + fs.existsSync(`${projectDir}/app/sentry-example-page`) || + fs.existsSync(`${projectDir}/src/app/sentry-example-page`); + expect(hasExample).toBe(true); + }); + + test('builds correctly', async () => { + await checkIfBuilds(projectDir); + }); +}); From 4f0c776f25a96049b6defce1cc85889e3105647b Mon Sep 17 00:00:00 2001 From: Cody De Arkland Date: Mon, 2 Feb 2026 20:48:47 -0800 Subject: [PATCH 05/12] fix(nextjs): address PR review comments for non-interactive mode - Add [NextJS only] prefix to all new CLI flag descriptions - Change chalk.dim to chalk.cyan for env vars in outro message - Update help-message.test.ts with new flag descriptions - Add checkFileDoesNotExist/checkFileDoesNotContain to eslint assertion functions - Fix and expand e2e tests for non-interactive mode: - Fix .env.example assertion (empty placeholders, not 'your-auth-token') - Fix config file checks (instrumentation-client.ts, not sentry.client.config.ts) - Add assertions for env var usage in config files - Add test suite for MCP configuration with base URL - Add test suite for minimal mode (no feature flags) --- .eslintrc.js | 2 + bin.ts | 16 +- e2e-tests/tests/help-message.test.ts | 31 +-- .../tests/nextjs-non-interactive.test.ts | 252 ++++++++++++++++-- src/nextjs/nextjs-wizard.ts | 2 +- 5 files changed, 265 insertions(+), 38 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 0a0b662c9..81479c3cc 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -49,6 +49,8 @@ module.exports = { assertFunctionNames: [ 'expect', 'checkFileContents', + 'checkFileDoesNotContain', + 'checkFileDoesNotExist', 'checkSentryProperties', 'checkPackageJson', 'checkIfBuilds', diff --git a/bin.ts b/bin.ts index 0feca8ef1..fcf46773a 100644 --- a/bin.ts +++ b/bin.ts @@ -163,35 +163,37 @@ const argv = yargs(hideBin(process.argv), process.cwd()) 'non-interactive': { default: false, describe: - 'Run in non-interactive mode. Skips all prompts and uses environment variable placeholders for auth.', + '[NextJS only] Run in non-interactive mode. Skips all prompts and uses environment variable placeholders for auth.', type: 'boolean', }, tracing: { describe: - 'Enable performance/tracing monitoring. When set, skips the tracing prompt.', + '[NextJS only] Enable performance/tracing monitoring. When set, skips the tracing prompt.', type: 'boolean', }, replay: { - describe: 'Enable Session Replay. When set, skips the replay prompt.', + describe: + '[NextJS only] Enable Session Replay. When set, skips the replay prompt.', type: 'boolean', }, logs: { - describe: 'Enable Sentry Logs. When set, skips the logs prompt.', + describe: + '[NextJS only] Enable Sentry Logs. When set, skips the logs prompt.', type: 'boolean', }, 'tunnel-route': { describe: - 'Enable tunnel route for ad-blocker circumvention. When set, skips the tunnel route prompt.', + '[NextJS only] Enable tunnel route for ad-blocker circumvention. When set, skips the tunnel route prompt.', type: 'boolean', }, 'example-page': { describe: - 'Create an example page to test Sentry. When set, skips the example page prompt.', + '[NextJS only] Create an example page to test Sentry. When set, skips the example page prompt.', type: 'boolean', }, mcp: { describe: - 'Add MCP (Model Context Protocol) config for specified IDE(s). Options: cursor, vscode, claude, opencode, jetbrains', + '[NextJS only] Add MCP (Model Context Protocol) config for specified IDE(s). Options: cursor, vscode, claude, opencode, jetbrains', type: 'array', choices: ['cursor', 'vscode', 'claude', 'opencode', 'jetbrains'], }, diff --git a/e2e-tests/tests/help-message.test.ts b/e2e-tests/tests/help-message.test.ts index eeff8cf0b..1057e8abd 100644 --- a/e2e-tests/tests/help-message.test.ts +++ b/e2e-tests/tests/help-message.test.ts @@ -54,22 +54,23 @@ describe('--help command', () => { --spotlight Enable Spotlight for local development. This does not require a Sentry account or project. [boolean] [default: false] - --non-interactive Run in non-interactive mode. Skips all prompts and - uses environment variable placeholders for auth. - [boolean] [default: false] - --tracing Enable performance/tracing monitoring. When set, - skips the tracing prompt. [boolean] - --replay Enable Session Replay. When set, skips the replay + --non-interactive [NextJS only] Run in non-interactive mode. Skips all + prompts and uses environment variable placeholders + for auth. [boolean] [default: false] + --tracing [NextJS only] Enable performance/tracing monitoring. + When set, skips the tracing prompt. [boolean] + --replay [NextJS only] Enable Session Replay. When set, skips + the replay prompt. [boolean] + --logs [NextJS only] Enable Sentry Logs. When set, skips + the logs prompt. [boolean] + --tunnel-route [NextJS only] Enable tunnel route for ad-blocker + circumvention. When set, skips the tunnel route prompt. [boolean] - --logs Enable Sentry Logs. When set, skips the logs prompt. - [boolean] - --tunnel-route Enable tunnel route for ad-blocker circumvention. - When set, skips the tunnel route prompt. [boolean] - --example-page Create an example page to test Sentry. When set, - skips the example page prompt. [boolean] - --mcp Add MCP (Model Context Protocol) config for - specified IDE(s). Options: cursor, vscode, claude, - opencode, jetbrains + --example-page [NextJS only] Create an example page to test Sentry. + When set, skips the example page prompt. [boolean] + --mcp [NextJS only] Add MCP (Model Context Protocol) config + for specified IDE(s). Options: cursor, vscode, + claude, opencode, jetbrains [array] [choices: "cursor", "vscode", "claude", "opencode", "jetbrains"] --version Show version number [boolean] " diff --git a/e2e-tests/tests/nextjs-non-interactive.test.ts b/e2e-tests/tests/nextjs-non-interactive.test.ts index 19c3f3e93..6796d77f4 100644 --- a/e2e-tests/tests/nextjs-non-interactive.test.ts +++ b/e2e-tests/tests/nextjs-non-interactive.test.ts @@ -1,18 +1,29 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; import { Integration } from '../../lib/Constants'; -import { createIsolatedTestEnv } from '../utils'; import { checkFileContents, + checkFileDoesNotExist, checkFileExists, checkIfBuilds, checkPackageJson, + createIsolatedTestEnv, } from '../utils'; import { describe, beforeAll, afterAll, test, expect } from 'vitest'; //@ts-expect-error - clifty is ESM only import { withEnv } from 'clifty'; +/** + * Helper to get the wizard binary path + */ +function getWizardBinPath(): string { + const binName = process.env.SENTRY_WIZARD_E2E_TEST_BIN + ? ['dist-bin', `sentry-wizard-${process.platform}-${process.arch}`] + : ['dist', 'bin.js']; + return path.join(__dirname, '..', '..', ...binName); +} + describe('NextJS Non-Interactive Mode', () => { const integration = Integration.nextjs; let wizardExitCode: number; @@ -20,10 +31,7 @@ describe('NextJS Non-Interactive Mode', () => { const { projectDir, cleanup } = createIsolatedTestEnv('nextjs-15-test-app'); beforeAll(async () => { - const binName = process.env.SENTRY_WIZARD_E2E_TEST_BIN - ? ['dist-bin', `sentry-wizard-${process.platform}-${process.arch}`] - : ['dist', 'bin.js']; - const binPath = path.join(__dirname, '..', '..', ...binName); + const binPath = getWizardBinPath(); // Run wizard in non-interactive mode with all features enabled const command = [ @@ -44,7 +52,7 @@ describe('NextJS Non-Interactive Mode', () => { }) .defineInteraction() .expectOutput('Running in non-interactive mode') - .expectOutput('Successfully installed the Sentry Next.js SDK!') + .expectOutput('Successfully scaffolded the Sentry Next.js SDK!') .run(command); }); @@ -62,10 +70,16 @@ describe('NextJS Non-Interactive Mode', () => { test('.env.example is created with placeholder values', () => { checkFileExists(`${projectDir}/.env.example`); - checkFileContents( - `${projectDir}/.env.example`, - 'SENTRY_AUTH_TOKEN=your-auth-token', - ); + checkFileContents(`${projectDir}/.env.example`, [ + 'NEXT_PUBLIC_SENTRY_DSN=', + 'SENTRY_ORG=', + 'SENTRY_PROJECT=', + 'SENTRY_AUTH_TOKEN=', + ]); + }); + + test('.env.sentry-build-plugin should NOT exist in non-interactive mode', () => { + checkFileDoesNotExist(`${projectDir}/.env.sentry-build-plugin`); }); test('instrumentation file is created', () => { @@ -74,22 +88,54 @@ describe('NextJS Non-Interactive Mode', () => { expect(hasJs || hasTs).toBe(true); }); - test('sentry.client.config is created', () => { - const hasJs = fs.existsSync(`${projectDir}/sentry.client.config.js`); - const hasTs = fs.existsSync(`${projectDir}/sentry.client.config.ts`); + test('instrumentation-client file is created and uses env var for DSN', () => { + const hasJs = fs.existsSync(`${projectDir}/src/instrumentation-client.js`); + const hasTs = fs.existsSync(`${projectDir}/src/instrumentation-client.ts`); expect(hasJs || hasTs).toBe(true); + + const filePath = hasTs + ? `${projectDir}/src/instrumentation-client.ts` + : `${projectDir}/src/instrumentation-client.js`; + checkFileContents(filePath, 'process.env.NEXT_PUBLIC_SENTRY_DSN'); }); - test('sentry.server.config is created', () => { + test('sentry.server.config is created and uses env var for DSN', () => { const hasJs = fs.existsSync(`${projectDir}/sentry.server.config.js`); const hasTs = fs.existsSync(`${projectDir}/sentry.server.config.ts`); expect(hasJs || hasTs).toBe(true); + + const filePath = hasTs + ? `${projectDir}/sentry.server.config.ts` + : `${projectDir}/sentry.server.config.js`; + checkFileContents(filePath, 'process.env.NEXT_PUBLIC_SENTRY_DSN'); }); - test('sentry.edge.config is created', () => { + test('sentry.edge.config is created and uses env var for DSN', () => { const hasJs = fs.existsSync(`${projectDir}/sentry.edge.config.js`); const hasTs = fs.existsSync(`${projectDir}/sentry.edge.config.ts`); expect(hasJs || hasTs).toBe(true); + + const filePath = hasTs + ? `${projectDir}/sentry.edge.config.ts` + : `${projectDir}/sentry.edge.config.js`; + checkFileContents(filePath, 'process.env.NEXT_PUBLIC_SENTRY_DSN'); + }); + + test('next.config uses env vars for org and project', () => { + const hasJs = fs.existsSync(`${projectDir}/next.config.js`); + const hasMjs = fs.existsSync(`${projectDir}/next.config.mjs`); + const hasTs = fs.existsSync(`${projectDir}/next.config.ts`); + expect(hasJs || hasMjs || hasTs).toBe(true); + + const filePath = hasTs + ? `${projectDir}/next.config.ts` + : hasMjs + ? `${projectDir}/next.config.mjs` + : `${projectDir}/next.config.js`; + checkFileContents(filePath, [ + 'process.env.SENTRY_ORG', + 'process.env.SENTRY_PROJECT', + ]); }); test('example page is created', () => { @@ -99,7 +145,183 @@ describe('NextJS Non-Interactive Mode', () => { expect(hasExample).toBe(true); }); + test('instrumentation-client contains tracing config (--tracing flag)', () => { + const filePath = fs.existsSync( + `${projectDir}/src/instrumentation-client.ts`, + ) + ? `${projectDir}/src/instrumentation-client.ts` + : `${projectDir}/src/instrumentation-client.js`; + checkFileContents(filePath, 'tracesSampleRate'); + }); + + test('instrumentation-client contains replay config (--replay flag)', () => { + const filePath = fs.existsSync( + `${projectDir}/src/instrumentation-client.ts`, + ) + ? `${projectDir}/src/instrumentation-client.ts` + : `${projectDir}/src/instrumentation-client.js`; + checkFileContents(filePath, 'replayIntegration'); + }); + + test('instrumentation-client contains logs config (--logs flag)', () => { + const filePath = fs.existsSync( + `${projectDir}/src/instrumentation-client.ts`, + ) + ? `${projectDir}/src/instrumentation-client.ts` + : `${projectDir}/src/instrumentation-client.js`; + checkFileContents(filePath, 'enableLogs: true'); + }); + test('builds correctly', async () => { await checkIfBuilds(projectDir); }); }); + +describe('NextJS Non-Interactive Mode with MCP', () => { + const integration = Integration.nextjs; + let wizardExitCode: number; + + const { projectDir, cleanup } = createIsolatedTestEnv('nextjs-15-test-app'); + + beforeAll(async () => { + const binPath = getWizardBinPath(); + + // Run wizard in non-interactive mode with MCP configuration + const command = [ + binPath, + '-i', + integration, + '--non-interactive', + '--tracing', + '--mcp', + 'cursor', + '--mcp', + 'opencode', + '--disable-telemetry', + ].join(' '); + + wizardExitCode = await withEnv({ + cwd: projectDir, + debug: true, + }) + .defineInteraction() + .expectOutput('Running in non-interactive mode') + .expectOutput('Adding MCP configurations') + .expectOutput('Successfully scaffolded the Sentry Next.js SDK!') + .run(command); + }); + + afterAll(() => { + cleanup(); + }); + + test('exits with exit code 0', () => { + expect(wizardExitCode).toBe(0); + }); + + test('Cursor MCP config is created with base URL (no org/project scope)', () => { + checkFileExists(`${projectDir}/.cursor/mcp.json`); + checkFileContents(`${projectDir}/.cursor/mcp.json`, [ + '"mcpServers"', + '"Sentry"', + 'https://mcp.sentry.dev/mcp', + ]); + }); + + test('OpenCode MCP config is created with base URL (no org/project scope)', () => { + checkFileExists(`${projectDir}/.opencode/mcp.json`); + checkFileContents(`${projectDir}/.opencode/mcp.json`, [ + '"mcpServers"', + '"Sentry"', + 'https://mcp.sentry.dev/mcp', + ]); + }); +}); + +describe('NextJS Non-Interactive Mode - Minimal', () => { + const integration = Integration.nextjs; + let wizardExitCode: number; + + const { projectDir, cleanup } = createIsolatedTestEnv('nextjs-15-test-app'); + + beforeAll(async () => { + const binPath = getWizardBinPath(); + + // Run wizard in non-interactive mode with NO feature flags + // This tests the default behavior where features are disabled + const command = [ + binPath, + '-i', + integration, + '--non-interactive', + '--disable-telemetry', + ].join(' '); + + wizardExitCode = await withEnv({ + cwd: projectDir, + debug: true, + }) + .defineInteraction() + .expectOutput('Running in non-interactive mode') + .expectOutput('Successfully scaffolded the Sentry Next.js SDK!') + .run(command); + }); + + afterAll(() => { + cleanup(); + }); + + test('exits with exit code 0', () => { + expect(wizardExitCode).toBe(0); + }); + + test('package.json is updated correctly', () => { + checkPackageJson(projectDir, '@sentry/nextjs'); + }); + + test('instrumentation-client does NOT contain tracing config when --tracing not provided', () => { + const filePath = fs.existsSync( + `${projectDir}/src/instrumentation-client.ts`, + ) + ? `${projectDir}/src/instrumentation-client.ts` + : `${projectDir}/src/instrumentation-client.js`; + + const content = fs.readFileSync(filePath, 'utf-8'); + expect(content).not.toContain('tracesSampleRate'); + }); + + test('instrumentation-client does NOT contain replay config when --replay not provided', () => { + const filePath = fs.existsSync( + `${projectDir}/src/instrumentation-client.ts`, + ) + ? `${projectDir}/src/instrumentation-client.ts` + : `${projectDir}/src/instrumentation-client.js`; + + const content = fs.readFileSync(filePath, 'utf-8'); + expect(content).not.toContain('replayIntegration'); + }); + + test('instrumentation-client does NOT contain logs config when --logs not provided', () => { + const filePath = fs.existsSync( + `${projectDir}/src/instrumentation-client.ts`, + ) + ? `${projectDir}/src/instrumentation-client.ts` + : `${projectDir}/src/instrumentation-client.js`; + + const content = fs.readFileSync(filePath, 'utf-8'); + expect(content).not.toContain('enableLogs'); + }); + + test('example page is NOT created when --example-page not provided', () => { + const hasExample = + fs.existsSync(`${projectDir}/app/sentry-example-page`) || + fs.existsSync(`${projectDir}/src/app/sentry-example-page`); + expect(hasExample).toBe(false); + }); + + test('no MCP config files created when --mcp not provided', () => { + expect(fs.existsSync(`${projectDir}/.cursor/mcp.json`)).toBe(false); + expect(fs.existsSync(`${projectDir}/.vscode/mcp.json`)).toBe(false); + expect(fs.existsSync(`${projectDir}/.opencode/mcp.json`)).toBe(false); + }); +}); diff --git a/src/nextjs/nextjs-wizard.ts b/src/nextjs/nextjs-wizard.ts index 2574a3446..ea3b68a3c 100644 --- a/src/nextjs/nextjs-wizard.ts +++ b/src/nextjs/nextjs-wizard.ts @@ -574,7 +574,7 @@ ${chalk.yellow('Next steps:')} `${packageManagerForOutro.runScriptCommand} dev`, )}) -${chalk.dim('Environment variables needed:')} +${chalk.cyan('Environment variables needed:')} - NEXT_PUBLIC_SENTRY_DSN - SENTRY_ORG - SENTRY_PROJECT From c8df268f844204e6799d29992b6e160689be9e93 Mon Sep 17 00:00:00 2001 From: Cody De Arkland Date: Mon, 2 Feb 2026 20:56:28 -0800 Subject: [PATCH 06/12] fix(e2e): fix non-interactive mode tests and wizard prompts - Fix help-message test snapshot for mcp flag line wrapping - Add package-lock.json creation in non-interactive tests to auto-detect npm - Fix tunnel route to default to false in non-interactive mode (avoid prompt) --- e2e-tests/tests/help-message.test.ts | 6 ++--- .../tests/nextjs-non-interactive.test.ts | 27 +++++++++++++++++++ src/nextjs/nextjs-wizard.ts | 5 +++- 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/e2e-tests/tests/help-message.test.ts b/e2e-tests/tests/help-message.test.ts index 1057e8abd..f83006d3c 100644 --- a/e2e-tests/tests/help-message.test.ts +++ b/e2e-tests/tests/help-message.test.ts @@ -68,9 +68,9 @@ describe('--help command', () => { prompt. [boolean] --example-page [NextJS only] Create an example page to test Sentry. When set, skips the example page prompt. [boolean] - --mcp [NextJS only] Add MCP (Model Context Protocol) config - for specified IDE(s). Options: cursor, vscode, - claude, opencode, jetbrains + --mcp [NextJS only] Add MCP (Model Context Protocol) + config for specified IDE(s). Options: cursor, + vscode, claude, opencode, jetbrains [array] [choices: "cursor", "vscode", "claude", "opencode", "jetbrains"] --version Show version number [boolean] " diff --git a/e2e-tests/tests/nextjs-non-interactive.test.ts b/e2e-tests/tests/nextjs-non-interactive.test.ts index 6796d77f4..be47bb60c 100644 --- a/e2e-tests/tests/nextjs-non-interactive.test.ts +++ b/e2e-tests/tests/nextjs-non-interactive.test.ts @@ -7,6 +7,7 @@ import { checkFileExists, checkIfBuilds, checkPackageJson, + createFile, createIsolatedTestEnv, } from '../utils'; import { describe, beforeAll, afterAll, test, expect } from 'vitest'; @@ -24,6 +25,23 @@ function getWizardBinPath(): string { return path.join(__dirname, '..', '..', ...binName); } +/** + * Create a minimal package-lock.json to ensure npm is auto-detected as the package manager. + * This is necessary for non-interactive mode since we can't prompt for package manager selection. + */ +function createPackageLockForNpm(projectDir: string): void { + const packageLock = { + name: 'nextjs-test-app', + lockfileVersion: 3, + requires: true, + packages: {}, + }; + createFile( + path.join(projectDir, 'package-lock.json'), + JSON.stringify(packageLock, null, 2), + ); +} + describe('NextJS Non-Interactive Mode', () => { const integration = Integration.nextjs; let wizardExitCode: number; @@ -31,6 +49,9 @@ describe('NextJS Non-Interactive Mode', () => { const { projectDir, cleanup } = createIsolatedTestEnv('nextjs-15-test-app'); beforeAll(async () => { + // Create package-lock.json so npm is auto-detected (avoids package manager prompt) + createPackageLockForNpm(projectDir); + const binPath = getWizardBinPath(); // Run wizard in non-interactive mode with all features enabled @@ -184,6 +205,9 @@ describe('NextJS Non-Interactive Mode with MCP', () => { const { projectDir, cleanup } = createIsolatedTestEnv('nextjs-15-test-app'); beforeAll(async () => { + // Create package-lock.json so npm is auto-detected (avoids package manager prompt) + createPackageLockForNpm(projectDir); + const binPath = getWizardBinPath(); // Run wizard in non-interactive mode with MCP configuration @@ -245,6 +269,9 @@ describe('NextJS Non-Interactive Mode - Minimal', () => { const { projectDir, cleanup } = createIsolatedTestEnv('nextjs-15-test-app'); beforeAll(async () => { + // Create package-lock.json so npm is auto-detected (avoids package manager prompt) + createPackageLockForNpm(projectDir); + const binPath = getWizardBinPath(); // Run wizard in non-interactive mode with NO feature flags diff --git a/src/nextjs/nextjs-wizard.ts b/src/nextjs/nextjs-wizard.ts index ea3b68a3c..d8a9c886e 100644 --- a/src/nextjs/nextjs-wizard.ts +++ b/src/nextjs/nextjs-wizard.ts @@ -178,7 +178,10 @@ export async function runNextjsWizardWithTelemetry( }); // Determine tunnel route setting - use CLI flag if provided, otherwise prompt - const tunnelRoute = options.tunnelRoute ?? (await askShouldSetTunnelRoute()); + // In non-interactive mode, default to false if not explicitly set + const tunnelRoute = + options.tunnelRoute ?? + (nonInteractive ? false : await askShouldSetTunnelRoute()); const { logsEnabled } = await traceStep('configure-sdk', async () => { return await createOrMergeNextJsFiles( From 49ca713515e388ed5b5a540d52fae1ecaa3a5849 Mon Sep 17 00:00:00 2001 From: Cody De Arkland Date: Mon, 2 Feb 2026 21:01:46 -0800 Subject: [PATCH 07/12] ref(nextjs): simplify feature selection logic per reviewer feedback - Remove special case for 'all flags provided' - Build featuresToPrompt array first, then check length - Only log features when some were provided via CLI --- src/nextjs/nextjs-wizard.ts | 119 +++++++++++++++--------------------- 1 file changed, 50 insertions(+), 69 deletions(-) diff --git a/src/nextjs/nextjs-wizard.ts b/src/nextjs/nextjs-wizard.ts index d8a9c886e..e9a8cd3b7 100644 --- a/src/nextjs/nextjs-wizard.ts +++ b/src/nextjs/nextjs-wizard.ts @@ -617,25 +617,58 @@ async function createOrMergeNextJsFiles( ): Promise<{ logsEnabled: boolean }> { const dsn = selectedProject.keys[0].dsn.public; - // Check if CLI flags are provided for features - // If a flag is set (true or false), use it; otherwise prompt the user - const tracingFlagProvided = wizardOptions.tracing !== undefined; - const replayFlagProvided = wizardOptions.replay !== undefined; - const logsFlagProvided = wizardOptions.logs !== undefined; - - let selectedFeatures: { - performance: boolean; - replay: boolean; - logs: boolean; + // Build list of features to prompt for (only those not provided via CLI) + const featuresToPrompt: Array<{ + id: 'performance' | 'replay' | 'logs'; + prompt: string; + enabledHint: string; + }> = []; + + if (wizardOptions.tracing === undefined) { + featuresToPrompt.push({ + id: 'performance', + prompt: `Do you want to enable ${chalk.bold( + 'Tracing', + )} to track the performance of your application?`, + enabledHint: 'recommended', + }); + } + + if (wizardOptions.replay === undefined) { + featuresToPrompt.push({ + id: 'replay', + prompt: `Do you want to enable ${chalk.bold( + 'Session Replay', + )} to get a video-like reproduction of errors during a user session?`, + enabledHint: 'recommended, but increases bundle size', + }); + } + + if (wizardOptions.logs === undefined) { + featuresToPrompt.push({ + id: 'logs', + prompt: `Do you want to enable ${chalk.bold( + 'Logs', + )} to send your application logs to Sentry?`, + enabledHint: 'recommended', + }); + } + + // Prompt for features not provided via CLI + const promptedFeatures = + featuresToPrompt.length > 0 + ? await featureSelectionPrompt(featuresToPrompt) + : { performance: false, replay: false, logs: false }; + + // Merge CLI-provided flags with prompted values + const selectedFeatures = { + performance: wizardOptions.tracing ?? promptedFeatures.performance, + replay: wizardOptions.replay ?? promptedFeatures.replay, + logs: wizardOptions.logs ?? promptedFeatures.logs, }; - // If all flags are provided via CLI, skip prompts entirely - if (tracingFlagProvided && replayFlagProvided && logsFlagProvided) { - selectedFeatures = { - performance: wizardOptions.tracing ?? false, - replay: wizardOptions.replay ?? false, - logs: wizardOptions.logs ?? false, - }; + // Log selected features when any were provided via CLI + if (featuresToPrompt.length < 3) { clack.log.info( `Features enabled: ${chalk.cyan( [ @@ -647,58 +680,6 @@ async function createOrMergeNextJsFiles( .join(', ') || 'None', )}`, ); - } else { - // Build list of features to prompt for (only those not provided via CLI) - const featuresToPrompt = []; - - if (!tracingFlagProvided) { - featuresToPrompt.push({ - id: 'performance' as const, - prompt: `Do you want to enable ${chalk.bold( - 'Tracing', - )} to track the performance of your application?`, - enabledHint: 'recommended', - }); - } - - if (!replayFlagProvided) { - featuresToPrompt.push({ - id: 'replay' as const, - prompt: `Do you want to enable ${chalk.bold( - 'Session Replay', - )} to get a video-like reproduction of errors during a user session?`, - enabledHint: 'recommended, but increases bundle size', - }); - } - - if (!logsFlagProvided) { - featuresToPrompt.push({ - id: 'logs' as const, - prompt: `Do you want to enable ${chalk.bold( - 'Logs', - )} to send your application logs to Sentry?`, - enabledHint: 'recommended', - }); - } - - // Prompt for features not provided via CLI - const promptedFeatures = - featuresToPrompt.length > 0 - ? await featureSelectionPrompt(featuresToPrompt) - : { performance: false, replay: false, logs: false }; - - // Merge CLI-provided flags with prompted values - selectedFeatures = { - performance: tracingFlagProvided - ? wizardOptions.tracing ?? false - : promptedFeatures.performance, - replay: replayFlagProvided - ? wizardOptions.replay ?? false - : promptedFeatures.replay, - logs: logsFlagProvided - ? wizardOptions.logs ?? false - : promptedFeatures.logs, - }; } const typeScriptDetected = isUsingTypeScript(); From 0dd7e0045ec4635109fc18afb89e0394606ec54d Mon Sep 17 00:00:00 2001 From: Cody De Arkland Date: Mon, 2 Feb 2026 21:10:12 -0800 Subject: [PATCH 08/12] fix(e2e): add --ignore-git-changes to non-interactive tests The package-lock.json we create for npm detection shows as untracked, causing the git dirty check prompt which hangs in non-interactive mode. --- e2e-tests/tests/nextjs-non-interactive.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/e2e-tests/tests/nextjs-non-interactive.test.ts b/e2e-tests/tests/nextjs-non-interactive.test.ts index be47bb60c..4f7b8c530 100644 --- a/e2e-tests/tests/nextjs-non-interactive.test.ts +++ b/e2e-tests/tests/nextjs-non-interactive.test.ts @@ -65,6 +65,7 @@ describe('NextJS Non-Interactive Mode', () => { '--logs', '--example-page', '--disable-telemetry', + '--ignore-git-changes', ].join(' '); wizardExitCode = await withEnv({ @@ -222,6 +223,7 @@ describe('NextJS Non-Interactive Mode with MCP', () => { '--mcp', 'opencode', '--disable-telemetry', + '--ignore-git-changes', ].join(' '); wizardExitCode = await withEnv({ @@ -282,6 +284,7 @@ describe('NextJS Non-Interactive Mode - Minimal', () => { integration, '--non-interactive', '--disable-telemetry', + '--ignore-git-changes', ].join(' '); wizardExitCode = await withEnv({ From d3fe097c9ffd36d84da0a520a1b1bf1839399dff Mon Sep 17 00:00:00 2001 From: Cody De Arkland Date: Mon, 2 Feb 2026 21:37:47 -0800 Subject: [PATCH 09/12] fix(e2e): add timeout to expectOutput for npm install in CI --- e2e-tests/tests/nextjs-non-interactive.test.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/e2e-tests/tests/nextjs-non-interactive.test.ts b/e2e-tests/tests/nextjs-non-interactive.test.ts index 4f7b8c530..46fef9d53 100644 --- a/e2e-tests/tests/nextjs-non-interactive.test.ts +++ b/e2e-tests/tests/nextjs-non-interactive.test.ts @@ -74,7 +74,9 @@ describe('NextJS Non-Interactive Mode', () => { }) .defineInteraction() .expectOutput('Running in non-interactive mode') - .expectOutput('Successfully scaffolded the Sentry Next.js SDK!') + .expectOutput('Successfully scaffolded the Sentry Next.js SDK!', { + timeout: 240_000, // npm install can take a while in CI + }) .run(command); }); @@ -233,7 +235,9 @@ describe('NextJS Non-Interactive Mode with MCP', () => { .defineInteraction() .expectOutput('Running in non-interactive mode') .expectOutput('Adding MCP configurations') - .expectOutput('Successfully scaffolded the Sentry Next.js SDK!') + .expectOutput('Successfully scaffolded the Sentry Next.js SDK!', { + timeout: 240_000, // npm install can take a while in CI + }) .run(command); }); @@ -293,7 +297,9 @@ describe('NextJS Non-Interactive Mode - Minimal', () => { }) .defineInteraction() .expectOutput('Running in non-interactive mode') - .expectOutput('Successfully scaffolded the Sentry Next.js SDK!') + .expectOutput('Successfully scaffolded the Sentry Next.js SDK!', { + timeout: 240_000, // npm install can take a while in CI + }) .run(command); }); From ddc7e141a755e2803b398b31f34c6ee8dc6ce362 Mon Sep 17 00:00:00 2001 From: Cody De Arkland Date: Mon, 2 Feb 2026 21:48:22 -0800 Subject: [PATCH 10/12] fix(nextjs): skip feature prompts in non-interactive mode In non-interactive mode, features not provided via CLI flags should default to false instead of prompting the user. This was causing the wizard to hang waiting for input in headless environments. --- src/nextjs/nextjs-wizard.ts | 57 +++++++++++++++++++------------------ 1 file changed, 30 insertions(+), 27 deletions(-) diff --git a/src/nextjs/nextjs-wizard.ts b/src/nextjs/nextjs-wizard.ts index e9a8cd3b7..5b68cbc05 100644 --- a/src/nextjs/nextjs-wizard.ts +++ b/src/nextjs/nextjs-wizard.ts @@ -618,43 +618,46 @@ async function createOrMergeNextJsFiles( const dsn = selectedProject.keys[0].dsn.public; // Build list of features to prompt for (only those not provided via CLI) + // In non-interactive mode, skip prompting entirely - unprovided flags default to false const featuresToPrompt: Array<{ id: 'performance' | 'replay' | 'logs'; prompt: string; enabledHint: string; }> = []; - if (wizardOptions.tracing === undefined) { - featuresToPrompt.push({ - id: 'performance', - prompt: `Do you want to enable ${chalk.bold( - 'Tracing', - )} to track the performance of your application?`, - enabledHint: 'recommended', - }); - } + if (!wizardOptions.nonInteractive) { + if (wizardOptions.tracing === undefined) { + featuresToPrompt.push({ + id: 'performance', + prompt: `Do you want to enable ${chalk.bold( + 'Tracing', + )} to track the performance of your application?`, + enabledHint: 'recommended', + }); + } - if (wizardOptions.replay === undefined) { - featuresToPrompt.push({ - id: 'replay', - prompt: `Do you want to enable ${chalk.bold( - 'Session Replay', - )} to get a video-like reproduction of errors during a user session?`, - enabledHint: 'recommended, but increases bundle size', - }); - } + if (wizardOptions.replay === undefined) { + featuresToPrompt.push({ + id: 'replay', + prompt: `Do you want to enable ${chalk.bold( + 'Session Replay', + )} to get a video-like reproduction of errors during a user session?`, + enabledHint: 'recommended, but increases bundle size', + }); + } - if (wizardOptions.logs === undefined) { - featuresToPrompt.push({ - id: 'logs', - prompt: `Do you want to enable ${chalk.bold( - 'Logs', - )} to send your application logs to Sentry?`, - enabledHint: 'recommended', - }); + if (wizardOptions.logs === undefined) { + featuresToPrompt.push({ + id: 'logs', + prompt: `Do you want to enable ${chalk.bold( + 'Logs', + )} to send your application logs to Sentry?`, + enabledHint: 'recommended', + }); + } } - // Prompt for features not provided via CLI + // Prompt for features not provided via CLI (empty in non-interactive mode) const promptedFeatures = featuresToPrompt.length > 0 ? await featureSelectionPrompt(featuresToPrompt) From 7e7a12aaec42610b9ed41dfd7964d900b8342b68 Mon Sep 17 00:00:00 2001 From: Cody De Arkland Date: Mon, 2 Feb 2026 21:56:02 -0800 Subject: [PATCH 11/12] fix(e2e): correct OpenCode MCP config file path in tests OpenCode uses opencode.json at project root, not .opencode/mcp.json --- e2e-tests/tests/nextjs-non-interactive.test.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/e2e-tests/tests/nextjs-non-interactive.test.ts b/e2e-tests/tests/nextjs-non-interactive.test.ts index 46fef9d53..f05d78073 100644 --- a/e2e-tests/tests/nextjs-non-interactive.test.ts +++ b/e2e-tests/tests/nextjs-non-interactive.test.ts @@ -259,9 +259,10 @@ describe('NextJS Non-Interactive Mode with MCP', () => { }); test('OpenCode MCP config is created with base URL (no org/project scope)', () => { - checkFileExists(`${projectDir}/.opencode/mcp.json`); - checkFileContents(`${projectDir}/.opencode/mcp.json`, [ - '"mcpServers"', + // OpenCode uses opencode.json at project root, not .opencode/mcp.json + checkFileExists(`${projectDir}/opencode.json`); + checkFileContents(`${projectDir}/opencode.json`, [ + '"mcp"', '"Sentry"', 'https://mcp.sentry.dev/mcp', ]); @@ -358,6 +359,6 @@ describe('NextJS Non-Interactive Mode - Minimal', () => { test('no MCP config files created when --mcp not provided', () => { expect(fs.existsSync(`${projectDir}/.cursor/mcp.json`)).toBe(false); expect(fs.existsSync(`${projectDir}/.vscode/mcp.json`)).toBe(false); - expect(fs.existsSync(`${projectDir}/.opencode/mcp.json`)).toBe(false); + expect(fs.existsSync(`${projectDir}/opencode.json`)).toBe(false); }); }); From ccd4c0c566ea7dfe227544e0caa1f5f50e036445 Mon Sep 17 00:00:00 2001 From: Cody De Arkland Date: Mon, 2 Feb 2026 22:03:08 -0800 Subject: [PATCH 12/12] fix(e2e): move timeout to correct expectOutput for MCP test The npm install timeout needs to be on the 'Adding MCP configurations' expectation since that's the first output after npm install completes. --- e2e-tests/tests/nextjs-non-interactive.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/e2e-tests/tests/nextjs-non-interactive.test.ts b/e2e-tests/tests/nextjs-non-interactive.test.ts index f05d78073..e8997651e 100644 --- a/e2e-tests/tests/nextjs-non-interactive.test.ts +++ b/e2e-tests/tests/nextjs-non-interactive.test.ts @@ -234,10 +234,10 @@ describe('NextJS Non-Interactive Mode with MCP', () => { }) .defineInteraction() .expectOutput('Running in non-interactive mode') - .expectOutput('Adding MCP configurations') - .expectOutput('Successfully scaffolded the Sentry Next.js SDK!', { + .expectOutput('Adding MCP configurations', { timeout: 240_000, // npm install can take a while in CI }) + .expectOutput('Successfully scaffolded the Sentry Next.js SDK!') .run(command); });