UNPKG

@sentry/wizard

Version:

Sentry wizard helping you to configure your project

751 lines (750 loc) 43.4 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.runNextjsWizardWithTelemetry = exports.runNextjsWizard = void 0; /* eslint-disable max-lines */ // @ts-expect-error - clack is ESM and TS complains about that. It works though const prompts_1 = __importDefault(require("@clack/prompts")); const chalk_1 = __importDefault(require("chalk")); const fs = __importStar(require("fs")); // @ts-expect-error - magicast is ESM and TS complains about that. It works though const magicast_1 = require("magicast"); const path = __importStar(require("path")); const Sentry = __importStar(require("@sentry/node")); const sourcemaps_wizard_1 = require("../sourcemaps/sourcemaps-wizard"); const telemetry_1 = require("../telemetry"); const clack_1 = require("../utils/clack"); const package_json_1 = require("../utils/package-json"); const mcp_config_1 = require("../utils/clack/mcp-config"); const templates_1 = require("./templates"); const utils_1 = require("./utils"); function runNextjsWizard(options) { return (0, telemetry_1.withTelemetry)({ enabled: options.telemetryEnabled, integration: 'nextjs', wizardOptions: options, }, () => runNextjsWizardWithTelemetry(options)); } exports.runNextjsWizard = runNextjsWizard; async function runNextjsWizardWithTelemetry(options) { const { promoCode, telemetryEnabled, forceInstall } = options; (0, clack_1.printWelcome)({ wizardName: 'Sentry Next.js Wizard', promoCode, telemetryEnabled, }); const typeScriptDetected = (0, clack_1.isUsingTypeScript)(); await (0, clack_1.confirmContinueIfNoOrDirtyGitRepo)({ ignoreGitChanges: options.ignoreGitChanges, cwd: undefined, }); const packageJson = await (0, clack_1.getPackageDotJson)(); await (0, clack_1.ensurePackageIsInstalled)(packageJson, 'next', 'Next.js'); const nextVersion = (0, package_json_1.getPackageVersion)('next', packageJson); Sentry.setTag('nextjs-version', (0, utils_1.getNextJsVersionBucket)(nextVersion)); const projectData = await (0, clack_1.getOrAskForProjectData)(options, 'javascript-nextjs'); const sdkAlreadyInstalled = (0, package_json_1.hasPackageInstalled)('@sentry/nextjs', packageJson); Sentry.setTag('sdk-already-installed', sdkAlreadyInstalled); const { packageManager: packageManagerFromInstallStep } = await (0, clack_1.installPackage)({ packageName: '@sentry/nextjs@^10', packageNameDisplayLabel: '@sentry/nextjs', alreadyInstalled: !!packageJson?.dependencies?.['@sentry/nextjs'], forceInstall, }); let selectedProject; let authToken; let selfHosted; let sentryUrl; let spotlight; 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 (0, telemetry_1.traceStep)('configure-sdk', async () => { const tunnelRoute = await askShouldSetTunnelRoute(); return await createOrMergeNextJsFiles(selectedProject, selfHosted, sentryUrl, { tunnelRoute, }, spotlight); }); await (0, telemetry_1.traceStep)('create-underscoreerror-page', async () => { const srcDir = path.join(process.cwd(), 'src'); const maybePagesDirPath = path.join(process.cwd(), 'pages'); const maybeSrcPagesDirPath = path.join(srcDir, 'pages'); const pagesLocation = fs.existsSync(maybePagesDirPath) && fs.lstatSync(maybePagesDirPath).isDirectory() ? ['pages'] : fs.existsSync(maybeSrcPagesDirPath) && fs.lstatSync(maybeSrcPagesDirPath).isDirectory() ? ['src', 'pages'] : undefined; if (!pagesLocation) { return; } const underscoreErrorPageFile = fs.existsSync(path.join(process.cwd(), ...pagesLocation, '_error.tsx')) ? '_error.tsx' : fs.existsSync(path.join(process.cwd(), ...pagesLocation, '_error.ts')) ? '_error.ts' : fs.existsSync(path.join(process.cwd(), ...pagesLocation, '_error.jsx')) ? '_error.jsx' : fs.existsSync(path.join(process.cwd(), ...pagesLocation, '_error.js')) ? '_error.js' : undefined; if (!underscoreErrorPageFile) { const underscoreErrorFileName = `_error.${typeScriptDetected ? 'tsx' : 'jsx'}`; await fs.promises.writeFile(path.join(process.cwd(), ...pagesLocation, underscoreErrorFileName), (0, templates_1.getSentryDefaultUnderscoreErrorPage)(), { encoding: 'utf8', flag: 'w' }); prompts_1.default.log.success(`Created ${chalk_1.default.cyan(path.join(...pagesLocation, underscoreErrorFileName))}.`); } else if (fs .readFileSync(path.join(process.cwd(), ...pagesLocation, underscoreErrorPageFile), 'utf8') .includes('getInitialProps')) { prompts_1.default.log.info(`It seems like you already have a custom error page.\n\nPlease put the following function call in the ${chalk_1.default.bold('getInitialProps')}\nmethod of your custom error page at ${chalk_1.default.bold(path.join(...pagesLocation, underscoreErrorPageFile))}:`); // eslint-disable-next-line no-console console.log((0, templates_1.getSimpleUnderscoreErrorCopyPasteSnippet)()); const shouldContinue = await (0, clack_1.abortIfCancelled)(prompts_1.default.confirm({ message: `Did you modify your ${chalk_1.default.cyan(path.join(...pagesLocation, underscoreErrorPageFile))} file as described above?`, active: 'Yes', inactive: 'No, get me out of here', })); if (!shouldContinue) { await (0, clack_1.abort)(); } } else { prompts_1.default.log.info(`It seems like you already have a custom error page.\n\nPlease add the following code to your custom error page\nat ${chalk_1.default.cyan(path.join(...pagesLocation, underscoreErrorPageFile))}:`); // eslint-disable-next-line no-console console.log((0, templates_1.getFullUnderscoreErrorCopyPasteSnippet)(underscoreErrorPageFile === '_error.ts' || underscoreErrorPageFile === '_error.tsx')); const shouldContinue = await (0, clack_1.abortIfCancelled)(prompts_1.default.confirm({ message: `Did you add the code to your ${chalk_1.default.cyan(path.join(...pagesLocation, underscoreErrorPageFile))} file as described above?`, active: 'Yes', inactive: 'No, get me out of here', })); if (!shouldContinue) { await (0, clack_1.abort)(); } } }); await (0, telemetry_1.traceStep)('create-global-error-page', async () => { const appDirLocation = (0, utils_1.getMaybeAppDirLocation)(); if (!appDirLocation) { return; } const globalErrorPageFile = fs.existsSync(path.join(process.cwd(), ...appDirLocation, 'global-error.tsx')) ? 'global-error.tsx' : fs.existsSync(path.join(process.cwd(), ...appDirLocation, 'global-error.ts')) ? 'global-error.ts' : fs.existsSync(path.join(process.cwd(), ...appDirLocation, 'global-error.jsx')) ? 'global-error.jsx' : fs.existsSync(path.join(process.cwd(), ...appDirLocation, 'global-error.js')) ? 'global-error.js' : undefined; if (!globalErrorPageFile) { const newGlobalErrorFileName = `global-error.${typeScriptDetected ? 'tsx' : 'jsx'}`; await fs.promises.writeFile(path.join(process.cwd(), ...appDirLocation, newGlobalErrorFileName), (0, templates_1.getSentryDefaultGlobalErrorPage)(typeScriptDetected), { encoding: 'utf8', flag: 'w' }); prompts_1.default.log.success(`Created ${chalk_1.default.cyan(path.join(...appDirLocation, newGlobalErrorFileName))}.`); } else { prompts_1.default.log.info(`It seems like you already have a custom error page for your app directory.\n\nPlease add the following code to your custom error page\nat ${chalk_1.default.cyan(path.join(...appDirLocation, globalErrorPageFile))}:\n`); // eslint-disable-next-line no-console console.log((0, templates_1.getGlobalErrorCopyPasteSnippet)(globalErrorPageFile === 'global-error.ts' || globalErrorPageFile === 'global-error.tsx')); const shouldContinue = await (0, clack_1.abortIfCancelled)(prompts_1.default.confirm({ message: `Did you add the code to your ${chalk_1.default.cyan(path.join(...appDirLocation, globalErrorPageFile))} file as described above?`, active: 'Yes', inactive: 'No, get me out of here', })); if (!shouldContinue) { await (0, clack_1.abort)(); } } }); await (0, telemetry_1.traceStep)('add-generate-metadata-function', async () => { const isNext14 = (0, utils_1.getNextJsVersionBucket)(nextVersion) === '14.x'; const appDirLocation = (0, utils_1.getMaybeAppDirLocation)(); // We only need this specific change for app router on next@14 if (!appDirLocation || !isNext14) { return; } const appDirPath = path.join(process.cwd(), ...appDirLocation); const hasRootLayout = (0, utils_1.hasRootLayoutFile)(appDirPath); if (!hasRootLayout) { const newRootLayoutFilename = `layout.${typeScriptDetected ? 'tsx' : 'jsx'}`; await fs.promises.writeFile(path.join(appDirPath, newRootLayoutFilename), (0, templates_1.getRootLayoutWithGenerateMetadata)(typeScriptDetected), { encoding: 'utf8', flag: 'w' }); prompts_1.default.log.success(`Created ${chalk_1.default.cyan(path.join(...appDirLocation, newRootLayoutFilename))}.`); } else { prompts_1.default.log.info(`It seems like you already have a root layout component. Please add or modify your generateMetadata function.`); await (0, clack_1.showCopyPasteInstructions)({ filename: `layout.${typeScriptDetected ? 'tsx' : 'jsx'}`, codeSnippet: (0, templates_1.getGenerateMetadataSnippet)(typeScriptDetected), }); } }); const shouldCreateExamplePage = await (0, clack_1.askShouldCreateExamplePage)(); if (shouldCreateExamplePage) { await (0, telemetry_1.traceStep)('create-example-page', async () => createExamplePage(selfHosted, selectedProject, sentryUrl, typeScriptDetected, logsEnabled)); } if (!spotlight) { await (0, clack_1.addDotEnvSentryBuildPluginFile)(authToken); } const isLikelyUsingTurbopack = await checkIfLikelyIsUsingTurbopack(); if (isLikelyUsingTurbopack || isLikelyUsingTurbopack === null) { await (0, clack_1.abortIfCancelled)(prompts_1.default.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) { prompts_1.default.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 (0, sourcemaps_wizard_1.setupCI)('nextjs', authToken, options.comingFrom); } const packageManagerForOutro = packageManagerFromInstallStep ?? (await (0, clack_1.getPackageManager)()); // Offer optional project-scoped MCP config for Sentry with org and project scope await (0, mcp_config_1.offerProjectScopedMcpConfig)(selectedProject.organization.slug, selectedProject.slug); // Run formatters as the last step to fix any formatting issues in generated/modified files await (0, clack_1.runFormatters)({ cwd: undefined }); prompts_1.default.outro(` ${chalk_1.default.green('Successfully installed the Sentry Next.js SDK!')} ${shouldCreateExamplePage ? `\n\nYou can validate your setup by (re)starting your dev environment (e.g. ${chalk_1.default.cyan(`${packageManagerForOutro.runScriptCommand} dev`)}) and visiting ${chalk_1.default.cyan('"/sentry-example-page"')}` : ''}${shouldCreateExamplePage && isLikelyUsingTurbopack ? `\nDon't forget to remove \`--turbo\` or \`--turbopack\` from your dev command until you have verified the SDK is working. You can safely add it back afterwards.` : ''} ${chalk_1.default.dim('If you encounter any issues, let us know here: https://github.com/getsentry/sentry-javascript/issues')}`); } exports.runNextjsWizardWithTelemetry = runNextjsWizardWithTelemetry; async function createOrMergeNextJsFiles(selectedProject, selfHosted, sentryUrl, sdkConfigOptions, spotlight = false) { const dsn = selectedProject.keys[0].dsn.public; const selectedFeatures = await (0, clack_1.featureSelectionPrompt)([ { id: 'performance', prompt: `Do you want to enable ${chalk_1.default.bold('Tracing')} to track the performance of your application?`, enabledHint: 'recommended', }, { id: 'replay', prompt: `Do you want to enable ${chalk_1.default.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_1.default.bold('Logs')} to send your application logs to Sentry?`, enabledHint: 'recommended', }, ]); const typeScriptDetected = (0, clack_1.isUsingTypeScript)(); const configVariants = ['server', 'edge']; for (const configVariant of configVariants) { await (0, telemetry_1.traceStep)(`create-sentry-${configVariant}-config`, async () => { const jsConfig = `sentry.${configVariant}.config.js`; const tsConfig = `sentry.${configVariant}.config.ts`; const jsConfigExists = fs.existsSync(path.join(process.cwd(), jsConfig)); const tsConfigExists = fs.existsSync(path.join(process.cwd(), tsConfig)); let shouldWriteFile = true; if (jsConfigExists || tsConfigExists) { const existingConfigs = []; if (jsConfigExists) { existingConfigs.push(jsConfig); } if (tsConfigExists) { existingConfigs.push(tsConfig); } const overwriteExistingConfigs = await (0, clack_1.abortIfCancelled)(prompts_1.default.confirm({ message: `Found existing Sentry ${configVariant} config (${existingConfigs.join(', ')}). Overwrite ${existingConfigs.length > 1 ? 'them' : 'it'}?`, })); Sentry.setTag(`overwrite-${configVariant}-config`, overwriteExistingConfigs); shouldWriteFile = overwriteExistingConfigs; if (overwriteExistingConfigs) { if (jsConfigExists) { fs.unlinkSync(path.join(process.cwd(), jsConfig)); prompts_1.default.log.warn(`Removed existing ${chalk_1.default.cyan(jsConfig)}.`); } if (tsConfigExists) { fs.unlinkSync(path.join(process.cwd(), tsConfig)); prompts_1.default.log.warn(`Removed existing ${chalk_1.default.cyan(tsConfig)}.`); } } } if (shouldWriteFile) { await fs.promises.writeFile(path.join(process.cwd(), typeScriptDetected ? tsConfig : jsConfig), (0, templates_1.getSentryServersideConfigContents)(dsn, configVariant, selectedFeatures, spotlight), { encoding: 'utf8', flag: 'w' }); prompts_1.default.log.success(`Created fresh ${chalk_1.default.cyan(typeScriptDetected ? tsConfig : jsConfig)}.`); Sentry.setTag(`created-${configVariant}-config`, true); } }); } await (0, telemetry_1.traceStep)('setup-instrumentation-hook', async () => { const hasRootAppDirectory = hasDirectoryPathFromRoot('app'); const hasRootPagesDirectory = hasDirectoryPathFromRoot('pages'); const hasSrcDirectory = hasDirectoryPathFromRoot('src'); let instrumentationHookLocation; const instrumentationTsExists = fs.existsSync(path.join(process.cwd(), 'instrumentation.ts')); const instrumentationJsExists = fs.existsSync(path.join(process.cwd(), 'instrumentation.js')); const srcInstrumentationTsExists = fs.existsSync(path.join(process.cwd(), 'src', 'instrumentation.ts')); const srcInstrumentationJsExists = fs.existsSync(path.join(process.cwd(), 'src', 'instrumentation.js')); // https://nextjs.org/docs/app/building-your-application/configuring/src-directory // https://nextjs.org/docs/app/api-reference/file-conventions/instrumentation // The logic for where Next.js picks up the instrumentation file is as follows: // - If there is either an `app` folder or a `pages` folder in the root directory of your Next.js app, Next.js looks // for an `instrumentation.ts` file in the root of the Next.js app. // - Otherwise, if there is neither an `app` folder or a `pages` folder in the rood directory of your Next.js app, // AND if there is an `src` folder, Next.js will look for the `instrumentation.ts` file in the `src` folder. if (hasRootPagesDirectory || hasRootAppDirectory) { if (instrumentationJsExists || instrumentationTsExists) { instrumentationHookLocation = 'root'; } else { instrumentationHookLocation = 'does-not-exist'; } } else { if (srcInstrumentationTsExists || srcInstrumentationJsExists) { instrumentationHookLocation = 'src'; } else { instrumentationHookLocation = 'does-not-exist'; } } const newInstrumentationFileName = `instrumentation.${typeScriptDetected ? 'ts' : 'js'}`; if (instrumentationHookLocation === 'does-not-exist') { let newInstrumentationHookLocation; if (hasRootPagesDirectory || hasRootAppDirectory) { newInstrumentationHookLocation = 'root'; } else if (hasSrcDirectory) { newInstrumentationHookLocation = 'src'; } else { newInstrumentationHookLocation = 'root'; } const newInstrumentationHookPath = newInstrumentationHookLocation === 'root' ? path.join(process.cwd(), newInstrumentationFileName) : path.join(process.cwd(), 'src', newInstrumentationFileName); const successfullyCreated = await (0, clack_1.createNewConfigFile)(newInstrumentationHookPath, (0, templates_1.getInstrumentationHookContent)(newInstrumentationHookLocation)); if (!successfullyCreated) { await (0, clack_1.showCopyPasteInstructions)({ filename: newInstrumentationFileName, codeSnippet: (0, templates_1.getInstrumentationHookCopyPasteSnippet)(newInstrumentationHookLocation), hint: "create the file if it doesn't already exist", }); } } else { await (0, clack_1.showCopyPasteInstructions)({ filename: srcInstrumentationTsExists || instrumentationTsExists ? 'instrumentation.ts' : srcInstrumentationJsExists || instrumentationJsExists ? 'instrumentation.js' : newInstrumentationFileName, codeSnippet: (0, templates_1.getInstrumentationHookCopyPasteSnippet)(instrumentationHookLocation), }); } }); await (0, telemetry_1.traceStep)('setup-instrumentation-client-hook', async () => { const hasRootAppDirectory = hasDirectoryPathFromRoot('app'); const hasRootPagesDirectory = hasDirectoryPathFromRoot('pages'); const hasSrcDirectory = hasDirectoryPathFromRoot('src'); let instrumentationClientHookLocation; const instrumentationClientTsExists = fs.existsSync(path.join(process.cwd(), 'instrumentation-client.ts')); const instrumentationClientJsExists = fs.existsSync(path.join(process.cwd(), 'instrumentation-client.js')); const srcInstrumentationClientTsExists = fs.existsSync(path.join(process.cwd(), 'src', 'instrumentation-client.ts')); const srcInstrumentationClientJsExists = fs.existsSync(path.join(process.cwd(), 'src', 'instrumentation-client.js')); // https://nextjs.org/docs/app/building-your-application/configuring/src-directory // https://nextjs.org/docs/app/api-reference/file-conventions/instrumentation // The logic for where Next.js picks up the instrumentation file is as follows: // - If there is either an `app` folder or a `pages` folder in the root directory of your Next.js app, Next.js looks // for an `instrumentation.ts` file in the root of the Next.js app. // - Otherwise, if there is neither an `app` folder or a `pages` folder in the rood directory of your Next.js app, // AND if there is an `src` folder, Next.js will look for the `instrumentation.ts` file in the `src` folder. if (hasRootPagesDirectory || hasRootAppDirectory) { if (instrumentationClientJsExists || instrumentationClientTsExists) { instrumentationClientHookLocation = 'root'; } else { instrumentationClientHookLocation = 'does-not-exist'; } } else { if (srcInstrumentationClientTsExists || srcInstrumentationClientJsExists) { instrumentationClientHookLocation = 'src'; } else { instrumentationClientHookLocation = 'does-not-exist'; } } const newInstrumentationClientFileName = `instrumentation-client.${typeScriptDetected ? 'ts' : 'js'}`; if (instrumentationClientHookLocation === 'does-not-exist') { let newInstrumentationClientHookLocation; if (hasRootPagesDirectory || hasRootAppDirectory) { newInstrumentationClientHookLocation = 'root'; } else if (hasSrcDirectory) { newInstrumentationClientHookLocation = 'src'; } else { newInstrumentationClientHookLocation = 'root'; } const newInstrumentationClientHookPath = newInstrumentationClientHookLocation === 'root' ? path.join(process.cwd(), newInstrumentationClientFileName) : path.join(process.cwd(), 'src', newInstrumentationClientFileName); const successfullyCreated = await (0, clack_1.createNewConfigFile)(newInstrumentationClientHookPath, (0, templates_1.getInstrumentationClientFileContents)(dsn, selectedFeatures, spotlight)); if (!successfullyCreated) { await (0, clack_1.showCopyPasteInstructions)({ filename: newInstrumentationClientFileName, codeSnippet: (0, templates_1.getInstrumentationClientHookCopyPasteSnippet)(dsn, selectedFeatures, spotlight), hint: "create the file if it doesn't already exist", }); } } else { await (0, clack_1.showCopyPasteInstructions)({ filename: srcInstrumentationClientTsExists || instrumentationClientTsExists ? 'instrumentation-client.ts' : srcInstrumentationClientJsExists || instrumentationClientJsExists ? 'instrumentation-client.js' : newInstrumentationClientFileName, codeSnippet: (0, templates_1.getInstrumentationClientHookCopyPasteSnippet)(dsn, selectedFeatures, spotlight), }); } }); await (0, telemetry_1.traceStep)('setup-next-config', async () => { const withSentryConfigOptionsTemplate = (0, templates_1.getWithSentryConfigOptionsTemplate)({ orgSlug: selectedProject.organization.slug, projectSlug: selectedProject.slug, selfHosted, sentryUrl, tunnelRoute: sdkConfigOptions.tunnelRoute, }); const nextConfigPossibleFilesMap = { js: 'next.config.js', mjs: 'next.config.mjs', cjs: 'next.config.cjs', ts: 'next.config.ts', mts: 'next.config.mts', cts: 'next.config.cts', }; const foundNextConfigFile = Object.entries(nextConfigPossibleFilesMap).find(([, fileName]) => fs.existsSync(path.join(process.cwd(), fileName))); if (!foundNextConfigFile) { Sentry.setTag('next-config-strategy', 'create'); // Try to figure out whether the user prefers ESM let isTypeModule = false; try { const packageJsonText = await fs.promises.readFile(path.join(process.cwd(), 'package.json'), 'utf8'); // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const packageJson = JSON.parse(packageJsonText); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access if (packageJson.type === 'module') { isTypeModule = true; } } catch { // noop } // We are creating `next.config.(m)js` files by default as they are supported by the most Next.js versions const configFilename = isTypeModule ? nextConfigPossibleFilesMap.mjs : nextConfigPossibleFilesMap.js; const configContent = isTypeModule ? (0, templates_1.getNextjsConfigMjsTemplate)(withSentryConfigOptionsTemplate) : (0, templates_1.getNextjsConfigCjsTemplate)(withSentryConfigOptionsTemplate); await fs.promises.writeFile(path.join(process.cwd(), configFilename), configContent, { encoding: 'utf8', flag: 'w' }); prompts_1.default.log.success(`Created ${chalk_1.default.cyan(configFilename)} with Sentry configuration.`); return; } const [foundNextConfigFileType, foundNextConfigFileFilename] = foundNextConfigFile; if (foundNextConfigFileType === 'js' || foundNextConfigFileType === 'cjs') { Sentry.setTag('next-config-strategy', 'modify'); const nextConfigCjsContent = fs.readFileSync(path.join(process.cwd(), foundNextConfigFileFilename), 'utf8'); const probablyIncludesSdk = nextConfigCjsContent.includes('@sentry/nextjs') && nextConfigCjsContent.includes('withSentryConfig'); let shouldInject = true; if (probablyIncludesSdk) { const injectAnyhow = await (0, clack_1.abortIfCancelled)(prompts_1.default.confirm({ message: `${chalk_1.default.cyan(foundNextConfigFileFilename)} already contains Sentry SDK configuration. Should the wizard modify it anyways?`, })); shouldInject = injectAnyhow; } if (shouldInject) { await fs.promises.appendFile(path.join(process.cwd(), foundNextConfigFileFilename), (0, templates_1.getNextjsConfigCjsAppendix)(withSentryConfigOptionsTemplate), 'utf8'); prompts_1.default.log.success(`Added Sentry configuration to ${chalk_1.default.cyan(foundNextConfigFileFilename)}. ${chalk_1.default.dim('(you probably want to clean this up a bit!)')}`); } Sentry.setTag('next-config-mod-result', 'success'); } if (foundNextConfigFileType === 'mjs' || foundNextConfigFileType === 'mts' || foundNextConfigFileType === 'cts' || foundNextConfigFileType === 'ts') { const nextConfigMjsContent = fs.readFileSync(path.join(process.cwd(), foundNextConfigFileFilename), 'utf8'); const probablyIncludesSdk = nextConfigMjsContent.includes('@sentry/nextjs') && nextConfigMjsContent.includes('withSentryConfig'); let shouldInject = true; if (probablyIncludesSdk) { const injectAnyhow = await (0, clack_1.abortIfCancelled)(prompts_1.default.confirm({ message: `${chalk_1.default.cyan(foundNextConfigFileFilename)} already contains Sentry SDK configuration. Should the wizard modify it anyways?`, })); shouldInject = injectAnyhow; } try { if (shouldInject) { const mod = (0, magicast_1.parseModule)(nextConfigMjsContent); mod.imports.$add({ from: '@sentry/nextjs', imported: 'withSentryConfig', local: 'withSentryConfig', }); if (probablyIncludesSdk) { // Prevent double wrapping like: withSentryConfig(withSentryConfig(nextConfig), { ... }) // Use AST manipulation instead of string parsing for better reliability // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access mod.exports.default.$ast = (0, utils_1.unwrapSentryConfigAst)( // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access mod.exports.default.$ast); } // Use the shared utility function for wrapping // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment mod.exports.default = (0, utils_1.wrapWithSentryConfig)(mod.exports.default, withSentryConfigOptionsTemplate); let newCode = mod.generate().code; // Post-process to fix formatting issues that magicast doesn't handle // (needed for Biome/ESLint compatibility): // 1. Add spaces inside import braces for various import patterns // Single named import: {Foo} -> { Foo } newCode = newCode.replace(/import\s*{(\w+)}\s*from/g, 'import { $1 } from'); // Multiple named imports: {Foo,Bar} or {Foo, Bar} -> { Foo, Bar } newCode = newCode.replace(/import\s*{([^}]+)}\s*from/g, (_match, imports) => { const formatted = imports .split(',') .map((i) => i.trim()) .join(', '); return `import { ${formatted} } from`; }); // Default + named imports: Foo,{Bar} -> Foo, { Bar } newCode = newCode.replace(/import\s+(\w+)\s*,\s*{([^}]+)}\s*from/g, (_match, defaultImport, namedImports) => { const formatted = namedImports .split(',') .map((i) => i.trim()) .join(', '); return `import ${defaultImport}, { ${formatted} } from`; }); // 2. Fix trailing comma and closing format for withSentryConfig call // Biome wants: automaticVercelMonitors: true,\n }); newCode = newCode.replace(/automaticVercelMonitors:\s*true,?\s*},?\s*\);/g, 'automaticVercelMonitors: true,\n });\n'); // 3. Ensure trailing newline if (!newCode.endsWith('\n')) { newCode += '\n'; } await fs.promises.writeFile(path.join(process.cwd(), foundNextConfigFileFilename), newCode, { encoding: 'utf8', flag: 'w', }); prompts_1.default.log.success(`${probablyIncludesSdk ? 'Updated' : 'Added'} Sentry configuration ${probablyIncludesSdk ? 'in' : 'to'} ${chalk_1.default.cyan(foundNextConfigFileFilename)}. ${chalk_1.default.dim('(you probably want to clean this up a bit!)')}`); Sentry.setTag('next-config-mod-result', 'success'); } } catch { Sentry.setTag('next-config-mod-result', 'fail'); prompts_1.default.log.warn(chalk_1.default.yellow(`Something went wrong writing to ${chalk_1.default.cyan(foundNextConfigFileFilename)}.`)); prompts_1.default.log.info(`Please put the following code snippet into ${chalk_1.default.cyan(foundNextConfigFileFilename)}: ${chalk_1.default.dim('You probably have to clean it up a bit.')}\n`); // eslint-disable-next-line no-console console.log((0, templates_1.getNextjsConfigEsmCopyPasteSnippet)(withSentryConfigOptionsTemplate)); const shouldContinue = await (0, clack_1.abortIfCancelled)(prompts_1.default.confirm({ message: `Are you done putting the snippet above into ${chalk_1.default.cyan(foundNextConfigFileFilename)}?`, active: 'Yes', inactive: 'No, get me out of here', })); if (!shouldContinue) { await (0, clack_1.abort)(); } } } }); return { logsEnabled: selectedFeatures.logs }; } function hasDirectoryPathFromRoot(dirnameOrDirs) { const dirPath = Array.isArray(dirnameOrDirs) ? path.join(process.cwd(), ...dirnameOrDirs) : path.join(process.cwd(), dirnameOrDirs); return fs.existsSync(dirPath) && fs.lstatSync(dirPath).isDirectory(); } async function createExamplePage(selfHosted, selectedProject, sentryUrl, typeScriptDetected, logsEnabled) { const hasSrcDirectory = hasDirectoryPathFromRoot('src'); const hasRootAppDirectory = hasDirectoryPathFromRoot('app'); const hasRootPagesDirectory = hasDirectoryPathFromRoot('pages'); const hasSrcAppDirectory = hasDirectoryPathFromRoot(['src', 'app']); const hasSrcPagesDirectory = hasDirectoryPathFromRoot(['src', 'pages']); Sentry.setTag('nextjs-app-dir', hasRootAppDirectory || hasSrcAppDirectory); // If `pages` or an `app` directory exists in the root, we'll put the example page there. // `app` directory takes priority over `pages` directory when they coexist, so we prioritize that. // https://nextjs.org/docs/app/building-your-application/routing#the-app-router const appFolderLocation = hasRootAppDirectory ? ['app'] : hasSrcAppDirectory ? ['src', 'app'] : undefined; let pagesFolderLocation = hasRootPagesDirectory ? ['pages'] : hasSrcPagesDirectory ? ['src', 'pages'] : undefined; // If the user has neither pages nor app directory we create a pages folder for them if (!appFolderLocation && !pagesFolderLocation) { const newPagesFolderLocation = hasSrcDirectory ? ['src', 'pages'] : ['pages']; fs.mkdirSync(path.join(process.cwd(), ...newPagesFolderLocation), { recursive: true, }); pagesFolderLocation = newPagesFolderLocation; } if (appFolderLocation) { const appFolderPath = path.join(process.cwd(), ...appFolderLocation); const hasRootLayout = (0, utils_1.hasRootLayoutFile)(appFolderPath); if (!hasRootLayout) { // In case no root layout file exists, we create a simple one so that // the example page can be rendered correctly. const newRootLayoutFilename = `layout.${typeScriptDetected ? 'tsx' : 'jsx'}`; await fs.promises.writeFile(path.join(appFolderPath, newRootLayoutFilename), (0, templates_1.getRootLayout)(typeScriptDetected), { encoding: 'utf8', flag: 'w' }); prompts_1.default.log.success(`Created ${chalk_1.default.cyan(path.join(...appFolderLocation, newRootLayoutFilename))}.`); } const examplePageContents = (0, templates_1.getSentryExamplePageContents)({ selfHosted, orgSlug: selectedProject.organization.slug, projectId: selectedProject.id, sentryUrl, useClient: true, isTypeScript: typeScriptDetected, logsEnabled, }); fs.mkdirSync(path.join(appFolderPath, 'sentry-example-page'), { recursive: true, }); const newPageFileName = `page.${typeScriptDetected ? 'tsx' : 'jsx'}`; await fs.promises.writeFile(path.join(appFolderPath, 'sentry-example-page', newPageFileName), examplePageContents, { encoding: 'utf8', flag: 'w' }); prompts_1.default.log.success(`Created ${chalk_1.default.cyan(path.join(...appFolderLocation, 'sentry-example-page', newPageFileName))}.`); fs.mkdirSync(path.join(appFolderPath, 'api', 'sentry-example-api'), { recursive: true, }); const newRouteFileName = `route.${typeScriptDetected ? 'ts' : 'js'}`; await fs.promises.writeFile(path.join(appFolderPath, 'api', 'sentry-example-api', newRouteFileName), (0, templates_1.getSentryExampleAppDirApiRoute)({ isTypeScript: typeScriptDetected, logsEnabled, }), { encoding: 'utf8', flag: 'w' }); prompts_1.default.log.success(`Created ${chalk_1.default.cyan(path.join(...appFolderLocation, 'api', 'sentry-example-api', newRouteFileName))}.`); } else if (pagesFolderLocation) { const examplePageContents = (0, templates_1.getSentryExamplePageContents)({ selfHosted, orgSlug: selectedProject.organization.slug, projectId: selectedProject.id, sentryUrl, useClient: false, isTypeScript: typeScriptDetected, logsEnabled, }); const examplePageFileName = `sentry-example-page.${typeScriptDetected ? 'tsx' : 'jsx'}`; await fs.promises.writeFile(path.join(process.cwd(), ...pagesFolderLocation, examplePageFileName), examplePageContents, { encoding: 'utf8', flag: 'w' }); prompts_1.default.log.success(`Created ${chalk_1.default.cyan(path.join(...pagesFolderLocation, examplePageFileName))}.`); fs.mkdirSync(path.join(process.cwd(), ...pagesFolderLocation, 'api'), { recursive: true, }); const apiRouteFileName = `sentry-example-api.${typeScriptDetected ? 'ts' : 'js'}`; await fs.promises.writeFile(path.join(process.cwd(), ...pagesFolderLocation, 'api', apiRouteFileName), (0, templates_1.getSentryExamplePagesDirApiRoute)({ isTypeScript: typeScriptDetected, logsEnabled, }), { encoding: 'utf8', flag: 'w' }); prompts_1.default.log.success(`Created ${chalk_1.default.cyan(path.join(...pagesFolderLocation, 'api', apiRouteFileName))}.`); } } /** * Ask users if they want to set the tunnelRoute option. * We can't set this by default because it potentially increases hosting bills. * It's valuable enough to for users to justify asking the additional question. */ async function askShouldSetTunnelRoute() { return await (0, telemetry_1.traceStep)('ask-tunnelRoute-option', async (span) => { const shouldSetTunnelRoute = await (0, clack_1.abortIfCancelled)(prompts_1.default.select({ message: 'Do you want to route Sentry requests in the browser through your Next.js server to avoid ad blockers?', options: [ { label: 'Yes', value: true, hint: 'Can increase your server load and hosting bill', }, { label: 'No', value: false, hint: 'Browser errors and events might be blocked by ad blockers before being sent to Sentry', }, ], initialValue: true, })); if (!shouldSetTunnelRoute) { prompts_1.default.log.info("Sounds good! We'll leave the option commented for later, just in case :)"); } span?.setAttribute('tunnelRoute', shouldSetTunnelRoute); Sentry.setTag('tunnelRoute', shouldSetTunnelRoute); return shouldSetTunnelRoute; }); } /** * Returns true or false depending on whether we think the user is using Turbopack. May return null in case we aren't sure. */ async function checkIfLikelyIsUsingTurbopack() { let packageJsonContent; try { packageJsonContent = await fs.promises.readFile(path.join(process.cwd(), 'package.json'), 'utf8'); } catch { return null; } return packageJsonContent.includes('--turbo'); } //# sourceMappingURL=nextjs-wizard.js.map