UNPKG

@sentry/wizard

Version:

Sentry wizard helping you to configure your project

680 lines (679 loc) 40.1 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 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 { selectedProject, authToken, selfHosted, sentryUrl } = 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, }); await (0, telemetry_1.traceStep)('configure-sdk', async () => { const tunnelRoute = await askShouldSetTunnelRoute(); await createOrMergeNextJsFiles(selectedProject, selfHosted, sentryUrl, { tunnelRoute, }); }); 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)); } 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.3.0 (or 15.3.0-canary.8) or later. ${chalk_1.default.bold(`If you are using Turbopack with an older Next.js version, temporarily remove \`--turbo\` or \`--turbopack\` from your dev command until you have verified the SDK is working as expected. Note that the SDK will continue to work for non-Turbopack production builds.`)}`, 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 { await (0, sourcemaps_wizard_1.setupCI)('nextjs', authToken, options.comingFrom); } const packageManagerForOutro = packageManagerFromInstallStep ?? (await (0, clack_1.getPackageManager)()); await (0, clack_1.runPrettierIfInstalled)({ 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) { 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)(selectedProject.keys[0].dsn.public, configVariant, selectedFeatures), { 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)(selectedProject.keys[0].dsn.public, selectedFeatures)); if (!successfullyCreated) { await (0, clack_1.showCopyPasteInstructions)({ filename: newInstrumentationClientFileName, codeSnippet: (0, templates_1.getInstrumentationClientHookCopyPasteSnippet)(selectedProject.keys[0].dsn.public, selectedFeatures), 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)(selectedProject.keys[0].dsn.public, selectedFeatures), }); } }); 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', }); // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access const expressionToWrap = (0, magicast_1.generateCode)(mod.exports.default.$ast).code; // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment mod.exports.default = magicast_1.builders.raw(`withSentryConfig( ${expressionToWrap}, ${withSentryConfigOptionsTemplate} )`); const newCode = mod.generate().code; await fs.promises.writeFile(path.join(process.cwd(), foundNextConfigFileFilename), newCode, { encoding: 'utf8', flag: 'w', }); 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'); } } 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)(); } } } }); } 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) { 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, }); 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 }), { 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, }); 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 }), { 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