@sentry/wizard
Version:
Sentry wizard helping you to configure your project
751 lines (750 loc) • 43.4 kB
JavaScript
;
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