Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Unreleased

- feat(nextjs): Switch to injecting `instrumentation-client.ts` ([#918](https://github.com/getsentry/sentry-wizard/pull/918))
- feat(remix): New Remix example page ([#917](https://github.com/getsentry/sentry-wizard/pull/917))
- feat(nuxt): New Nuxt example page ([#916](https://github.com/getsentry/sentry-wizard/pull/916))
- feat(sveltekit): New Sveltekit example page ([#913](https://github.com/getsentry/sentry-wizard/pull/913))
Expand Down
4 changes: 2 additions & 2 deletions e2e-tests/tests/nextjs-14.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,16 +105,16 @@ describe('NextJS-14', () => {

test('config files created', () => {
checkFileExists(`${projectDir}/sentry.server.config.ts`);
checkFileExists(`${projectDir}/sentry.client.config.ts`);
checkFileExists(`${projectDir}/sentry.edge.config.ts`);
});

test('global error file exists', () => {
checkFileExists(`${projectDir}/src/app/global-error.tsx`);
});

test('instrumentation file exists', () => {
test('instrumentation files exists', () => {
checkFileExists(`${projectDir}/src/instrumentation.ts`);
checkFileExists(`${projectDir}/src/instrumentation-client.ts`);
});

test('instrumentation file contains Sentry initialization', () => {
Expand Down
4 changes: 2 additions & 2 deletions e2e-tests/tests/nextjs-15.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,16 +104,16 @@ describe('NextJS-15', () => {

test('config files created', () => {
checkFileExists(`${projectDir}/sentry.server.config.ts`);
checkFileExists(`${projectDir}/sentry.client.config.ts`);
checkFileExists(`${projectDir}/sentry.edge.config.ts`);
});

test('global error file exists', () => {
checkFileExists(`${projectDir}/src/app/global-error.tsx`);
});

test('instrumentation file exists', () => {
test('instrumentation files exists', () => {
checkFileExists(`${projectDir}/src/instrumentation.ts`);
checkFileExists(`${projectDir}/src/instrumentation-client.ts`);
});

test('instrumentation file contains Sentry initialization', () => {
Expand Down
111 changes: 105 additions & 6 deletions src/nextjs/nextjs-wizard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,16 @@ import {
getNextjsConfigEsmCopyPasteSnippet,
getNextjsConfigMjsTemplate,
getRootLayout,
getSentryConfigContents,
getSentryServersideConfigContents,
getInstrumentationClientFileContents,
getSentryDefaultGlobalErrorPage,
getSentryDefaultUnderscoreErrorPage,
getSentryExampleAppDirApiRoute,
getSentryExamplePageContents,
getSentryExamplePagesDirApiRoute,
getSimpleUnderscoreErrorCopyPasteSnippet,
getWithSentryConfigOptionsTemplate,
getInstrumentationClientHookCopyPasteSnippet,
} from './templates';
import { getNextJsVersionBucket } from './utils';

Expand Down Expand Up @@ -99,7 +101,7 @@ export async function runNextjsWizardWithTelemetry(

const { packageManager: packageManagerFromInstallStep } =
await installPackage({
packageName: '@sentry/nextjs@^9',
packageName: '@sentry/nextjs@latest',
packageNameDisplayLabel: '@sentry/nextjs',
alreadyInstalled: !!packageJson?.dependencies?.['@sentry/nextjs'],
forceInstall,
Expand Down Expand Up @@ -311,8 +313,8 @@ export async function runNextjsWizardWithTelemetry(
if (isLikelyUsingTurbopack || isLikelyUsingTurbopack === null) {
await abortIfCancelled(
clack.select({
message: `Warning: The Sentry SDK doesn't yet fully support Turbopack in dev mode. The SDK will not be loaded in the browser, and serverside instrumentation will be inaccurate or incomplete. Production builds will still fully work. ${chalk.bold(
`To continue this setup, if you are using Turbopack, temporarily remove \`--turbo\` or \`--turbopack\` from your dev command until you have verified the SDK is working as expected.`,
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.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: [
{
Expand Down Expand Up @@ -392,7 +394,7 @@ async function createOrMergeNextJsFiles(

const typeScriptDetected = isUsingTypeScript();

const configVariants = ['server', 'client', 'edge'] as const;
const configVariants = ['server', 'edge'] as const;

for (const configVariant of configVariants) {
await traceStep(`create-sentry-${configVariant}-config`, async () => {
Expand Down Expand Up @@ -444,7 +446,7 @@ async function createOrMergeNextJsFiles(
if (shouldWriteFile) {
await fs.promises.writeFile(
path.join(process.cwd(), typeScriptDetected ? tsConfig : jsConfig),
getSentryConfigContents(
getSentryServersideConfigContents(
selectedProject.keys[0].dsn.public,
configVariant,
selectedFeatures,
Expand Down Expand Up @@ -532,6 +534,7 @@ async function createOrMergeNextJsFiles(
getInstrumentationHookCopyPasteSnippet(
newInstrumentationHookLocation,
),
"create the file if it doesn't already exist",
);
}
} else {
Expand All @@ -546,6 +549,102 @@ async function createOrMergeNextJsFiles(
}
});

await traceStep('setup-instrumentation-client-hook', async () => {
const hasRootAppDirectory = hasDirectoryPathFromRoot('app');
const hasRootPagesDirectory = hasDirectoryPathFromRoot('pages');
const hasSrcDirectory = hasDirectoryPathFromRoot('src');

let instrumentationClientHookLocation: 'src' | 'root' | 'does-not-exist';

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: 'root' | 'src';
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 createNewConfigFile(
newInstrumentationClientHookPath,
getInstrumentationClientFileContents(
selectedProject.keys[0].dsn.public,
selectedFeatures,
),
);

if (!successfullyCreated) {
await showCopyPasteInstructions(
newInstrumentationClientFileName,
getInstrumentationClientHookCopyPasteSnippet(
selectedProject.keys[0].dsn.public,
selectedFeatures,
),
"create the file if it doesn't already exist",
);
}
} else {
await showCopyPasteInstructions(
srcInstrumentationClientTsExists || instrumentationClientTsExists
? 'instrumentation-client.ts'
: srcInstrumentationClientJsExists || instrumentationClientJsExists
? 'instrumentation-client.js'
: newInstrumentationClientFileName,
getInstrumentationClientHookCopyPasteSnippet(
selectedProject.keys[0].dsn.public,
selectedFeatures,
),
);
}
});

await traceStep('setup-next-config', async () => {
const withSentryConfigOptionsTemplate = getWithSentryConfigOptionsTemplate({
orgSlug: selectedProject.organization.slug,
Expand Down
70 changes: 53 additions & 17 deletions src/nextjs/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,9 +127,9 @@
return '';
}

export function getSentryConfigContents(
export function getSentryServersideConfigContents(
dsn: string,
config: 'server' | 'client' | 'edge',
config: 'server' | 'edge',
selectedFeaturesMap: {
replay: boolean;
performance: boolean;
Expand All @@ -139,10 +139,6 @@
if (config === 'server') {
primer = `// This file configures the initialization of Sentry on the server.
// The config you add here will be used whenever the server handles a request.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/`;
} else if (config === 'client') {
primer = `// This file configures the initialization of Sentry on the client.
// The config you add here will be used whenever a users loads a page in their browser.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/`;
} else if (config === 'edge') {
primer = `// This file configures the initialization of Sentry for edge features (middleware, edge routes, and so on).
Expand All @@ -151,14 +147,43 @@
// https://docs.sentry.io/platforms/javascript/guides/nextjs/`;
}

let performanceOptions = '';
if (selectedFeaturesMap.performance) {
performanceOptions += `

// Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
tracesSampleRate: 1,`;
}

// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
return `${primer}

import * as Sentry from "@sentry/nextjs";

Sentry.init({
dsn: "${dsn}",${performanceOptions}

// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,
});
`;
}

export function getInstrumentationClientFileContents(
dsn: string,
selectedFeaturesMap: {
replay: boolean;
performance: boolean;
},
): string {
const integrationsOptions = getClientIntegrationsSnippet({
replay: config === 'client' && selectedFeaturesMap.replay,
replay: selectedFeaturesMap.replay,
});

let replayOptions = '';
if (config === 'client') {
if (selectedFeaturesMap.replay) {
replayOptions += `

if (selectedFeaturesMap.replay) {
replayOptions += `

// Define how likely Replay events are sampled.
// This sets the sample rate to be 10%. You may want this to be 100% while
Expand All @@ -167,7 +192,6 @@

// Define how likely Replay events are sampled when an error occurs.
replaysOnErrorSampleRate: 1.0,`;
}
}

let performanceOptions = '';
Expand All @@ -178,8 +202,9 @@
tracesSampleRate: 1,`;
}

// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
return `${primer}
return `// This file configures the initialization of Sentry on the client.
// The added config here will be used whenever a users loads a page in their browser.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/

import * as Sentry from "@sentry/nextjs";

Expand All @@ -188,8 +213,7 @@

// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,
});
`;
});`;
}

export function getSentryExamplePageContents(options: {
Expand Down Expand Up @@ -229,7 +253,7 @@
</h1>

<p className="description">
Click the button below, and view the sample error on the Sentry <a target="_blank" href="${issuesPageLink}">Issues Page</a>.
Click the button below, and view the sample error on the Sentry <a target="_blank" href="${issuesPageLink}">Issues Page</a>.
For more details about setting up Sentry, <a target="_blank" href="https://docs.sentry.io/platforms/javascript/guides/nextjs/">read our docs</a>.
</p>

Expand Down Expand Up @@ -500,6 +524,18 @@
});
}

export function getInstrumentationClientHookCopyPasteSnippet(
dsn: string,
selectedFeaturesMap: {
replay: boolean;
performance: boolean;
},

Check warning on line 532 in src/nextjs/templates.ts

View check run for this annotation

Codecov / codecov/patch

src/nextjs/templates.ts#L532

Added line #L532 was not covered by tests
) {
return makeCodeSnippet(true, (unchanged, plus) => {
return plus(getInstrumentationClientFileContents(dsn, selectedFeaturesMap));

Check warning on line 535 in src/nextjs/templates.ts

View check run for this annotation

Codecov / codecov/patch

src/nextjs/templates.ts#L534-L535

Added lines #L534 - L535 were not covered by tests
});
}

export function getSentryDefaultGlobalErrorPage(isTs: boolean) {
return isTs
? `"use client";
Expand Down Expand Up @@ -601,7 +637,7 @@
export const getRootLayout = (
isTs: boolean,
) => `// This file was generated by the Sentry wizard because we couldn't find a root layout file.
// You can delete this file at any time.
// You can delete this file at any time.

export const metadata = {
title: 'Sentry NextJS Example',
Expand Down
Loading
Loading