Skip to content

Commit d1e8e55

Browse files
author
Luca Forstner
authored
feat(nextjs): Switch to injecting instrumentation-client.ts (#918)
1 parent ad7b8f2 commit d1e8e55

File tree

6 files changed

+248
-114
lines changed

6 files changed

+248
-114
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## Unreleased
44

5+
- feat(nextjs): Switch to injecting `instrumentation-client.ts` ([#918](https://github.com/getsentry/sentry-wizard/pull/918))
56
- feat(remix): New Remix example page ([#917](https://github.com/getsentry/sentry-wizard/pull/917))
67
- feat(nuxt): New Nuxt example page ([#916](https://github.com/getsentry/sentry-wizard/pull/916))
78
- feat(sveltekit): New Sveltekit example page ([#913](https://github.com/getsentry/sentry-wizard/pull/913))

e2e-tests/tests/nextjs-14.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,16 +105,16 @@ describe('NextJS-14', () => {
105105

106106
test('config files created', () => {
107107
checkFileExists(`${projectDir}/sentry.server.config.ts`);
108-
checkFileExists(`${projectDir}/sentry.client.config.ts`);
109108
checkFileExists(`${projectDir}/sentry.edge.config.ts`);
110109
});
111110

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

116-
test('instrumentation file exists', () => {
115+
test('instrumentation files exists', () => {
117116
checkFileExists(`${projectDir}/src/instrumentation.ts`);
117+
checkFileExists(`${projectDir}/src/instrumentation-client.ts`);
118118
});
119119

120120
test('instrumentation file contains Sentry initialization', () => {

e2e-tests/tests/nextjs-15.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,16 +104,16 @@ describe('NextJS-15', () => {
104104

105105
test('config files created', () => {
106106
checkFileExists(`${projectDir}/sentry.server.config.ts`);
107-
checkFileExists(`${projectDir}/sentry.client.config.ts`);
108107
checkFileExists(`${projectDir}/sentry.edge.config.ts`);
109108
});
110109

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

115-
test('instrumentation file exists', () => {
114+
test('instrumentation files exists', () => {
116115
checkFileExists(`${projectDir}/src/instrumentation.ts`);
116+
checkFileExists(`${projectDir}/src/instrumentation-client.ts`);
117117
});
118118

119119
test('instrumentation file contains Sentry initialization', () => {

src/nextjs/nextjs-wizard.ts

Lines changed: 105 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,16 @@ import {
4141
getNextjsConfigEsmCopyPasteSnippet,
4242
getNextjsConfigMjsTemplate,
4343
getRootLayout,
44-
getSentryConfigContents,
44+
getSentryServersideConfigContents,
45+
getInstrumentationClientFileContents,
4546
getSentryDefaultGlobalErrorPage,
4647
getSentryDefaultUnderscoreErrorPage,
4748
getSentryExampleAppDirApiRoute,
4849
getSentryExamplePageContents,
4950
getSentryExamplePagesDirApiRoute,
5051
getSimpleUnderscoreErrorCopyPasteSnippet,
5152
getWithSentryConfigOptionsTemplate,
53+
getInstrumentationClientHookCopyPasteSnippet,
5254
} from './templates';
5355
import { getNextJsVersionBucket } from './utils';
5456

@@ -99,7 +101,7 @@ export async function runNextjsWizardWithTelemetry(
99101

100102
const { packageManager: packageManagerFromInstallStep } =
101103
await installPackage({
102-
packageName: '@sentry/nextjs@^9',
104+
packageName: '@sentry/nextjs@latest',
103105
packageNameDisplayLabel: '@sentry/nextjs',
104106
alreadyInstalled: !!packageJson?.dependencies?.['@sentry/nextjs'],
105107
forceInstall,
@@ -311,8 +313,8 @@ export async function runNextjsWizardWithTelemetry(
311313
if (isLikelyUsingTurbopack || isLikelyUsingTurbopack === null) {
312314
await abortIfCancelled(
313315
clack.select({
314-
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(
315-
`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.`,
316+
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(
317+
`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.`,
316318
)}`,
317319
options: [
318320
{
@@ -392,7 +394,7 @@ async function createOrMergeNextJsFiles(
392394

393395
const typeScriptDetected = isUsingTypeScript();
394396

395-
const configVariants = ['server', 'client', 'edge'] as const;
397+
const configVariants = ['server', 'edge'] as const;
396398

397399
for (const configVariant of configVariants) {
398400
await traceStep(`create-sentry-${configVariant}-config`, async () => {
@@ -444,7 +446,7 @@ async function createOrMergeNextJsFiles(
444446
if (shouldWriteFile) {
445447
await fs.promises.writeFile(
446448
path.join(process.cwd(), typeScriptDetected ? tsConfig : jsConfig),
447-
getSentryConfigContents(
449+
getSentryServersideConfigContents(
448450
selectedProject.keys[0].dsn.public,
449451
configVariant,
450452
selectedFeatures,
@@ -532,6 +534,7 @@ async function createOrMergeNextJsFiles(
532534
getInstrumentationHookCopyPasteSnippet(
533535
newInstrumentationHookLocation,
534536
),
537+
"create the file if it doesn't already exist",
535538
);
536539
}
537540
} else {
@@ -546,6 +549,102 @@ async function createOrMergeNextJsFiles(
546549
}
547550
});
548551

552+
await traceStep('setup-instrumentation-client-hook', async () => {
553+
const hasRootAppDirectory = hasDirectoryPathFromRoot('app');
554+
const hasRootPagesDirectory = hasDirectoryPathFromRoot('pages');
555+
const hasSrcDirectory = hasDirectoryPathFromRoot('src');
556+
557+
let instrumentationClientHookLocation: 'src' | 'root' | 'does-not-exist';
558+
559+
const instrumentationClientTsExists = fs.existsSync(
560+
path.join(process.cwd(), 'instrumentation-client.ts'),
561+
);
562+
const instrumentationClientJsExists = fs.existsSync(
563+
path.join(process.cwd(), 'instrumentation-client.js'),
564+
);
565+
const srcInstrumentationClientTsExists = fs.existsSync(
566+
path.join(process.cwd(), 'src', 'instrumentation-client.ts'),
567+
);
568+
const srcInstrumentationClientJsExists = fs.existsSync(
569+
path.join(process.cwd(), 'src', 'instrumentation-client.js'),
570+
);
571+
572+
// https://nextjs.org/docs/app/building-your-application/configuring/src-directory
573+
// https://nextjs.org/docs/app/api-reference/file-conventions/instrumentation
574+
// The logic for where Next.js picks up the instrumentation file is as follows:
575+
// - If there is either an `app` folder or a `pages` folder in the root directory of your Next.js app, Next.js looks
576+
// for an `instrumentation.ts` file in the root of the Next.js app.
577+
// - Otherwise, if there is neither an `app` folder or a `pages` folder in the rood directory of your Next.js app,
578+
// AND if there is an `src` folder, Next.js will look for the `instrumentation.ts` file in the `src` folder.
579+
if (hasRootPagesDirectory || hasRootAppDirectory) {
580+
if (instrumentationClientJsExists || instrumentationClientTsExists) {
581+
instrumentationClientHookLocation = 'root';
582+
} else {
583+
instrumentationClientHookLocation = 'does-not-exist';
584+
}
585+
} else {
586+
if (
587+
srcInstrumentationClientTsExists ||
588+
srcInstrumentationClientJsExists
589+
) {
590+
instrumentationClientHookLocation = 'src';
591+
} else {
592+
instrumentationClientHookLocation = 'does-not-exist';
593+
}
594+
}
595+
596+
const newInstrumentationClientFileName = `instrumentation-client.${
597+
typeScriptDetected ? 'ts' : 'js'
598+
}`;
599+
600+
if (instrumentationClientHookLocation === 'does-not-exist') {
601+
let newInstrumentationClientHookLocation: 'root' | 'src';
602+
if (hasRootPagesDirectory || hasRootAppDirectory) {
603+
newInstrumentationClientHookLocation = 'root';
604+
} else if (hasSrcDirectory) {
605+
newInstrumentationClientHookLocation = 'src';
606+
} else {
607+
newInstrumentationClientHookLocation = 'root';
608+
}
609+
610+
const newInstrumentationClientHookPath =
611+
newInstrumentationClientHookLocation === 'root'
612+
? path.join(process.cwd(), newInstrumentationClientFileName)
613+
: path.join(process.cwd(), 'src', newInstrumentationClientFileName);
614+
615+
const successfullyCreated = await createNewConfigFile(
616+
newInstrumentationClientHookPath,
617+
getInstrumentationClientFileContents(
618+
selectedProject.keys[0].dsn.public,
619+
selectedFeatures,
620+
),
621+
);
622+
623+
if (!successfullyCreated) {
624+
await showCopyPasteInstructions(
625+
newInstrumentationClientFileName,
626+
getInstrumentationClientHookCopyPasteSnippet(
627+
selectedProject.keys[0].dsn.public,
628+
selectedFeatures,
629+
),
630+
"create the file if it doesn't already exist",
631+
);
632+
}
633+
} else {
634+
await showCopyPasteInstructions(
635+
srcInstrumentationClientTsExists || instrumentationClientTsExists
636+
? 'instrumentation-client.ts'
637+
: srcInstrumentationClientJsExists || instrumentationClientJsExists
638+
? 'instrumentation-client.js'
639+
: newInstrumentationClientFileName,
640+
getInstrumentationClientHookCopyPasteSnippet(
641+
selectedProject.keys[0].dsn.public,
642+
selectedFeatures,
643+
),
644+
);
645+
}
646+
});
647+
549648
await traceStep('setup-next-config', async () => {
550649
const withSentryConfigOptionsTemplate = getWithSentryConfigOptionsTemplate({
551650
orgSlug: selectedProject.organization.slug,

src/nextjs/templates.ts

Lines changed: 53 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -127,9 +127,9 @@ function getClientIntegrationsSnippet(features: { replay: boolean }) {
127127
return '';
128128
}
129129

130-
export function getSentryConfigContents(
130+
export function getSentryServersideConfigContents(
131131
dsn: string,
132-
config: 'server' | 'client' | 'edge',
132+
config: 'server' | 'edge',
133133
selectedFeaturesMap: {
134134
replay: boolean;
135135
performance: boolean;
@@ -139,10 +139,6 @@ export function getSentryConfigContents(
139139
if (config === 'server') {
140140
primer = `// This file configures the initialization of Sentry on the server.
141141
// The config you add here will be used whenever the server handles a request.
142-
// https://docs.sentry.io/platforms/javascript/guides/nextjs/`;
143-
} else if (config === 'client') {
144-
primer = `// This file configures the initialization of Sentry on the client.
145-
// The config you add here will be used whenever a users loads a page in their browser.
146142
// https://docs.sentry.io/platforms/javascript/guides/nextjs/`;
147143
} else if (config === 'edge') {
148144
primer = `// This file configures the initialization of Sentry for edge features (middleware, edge routes, and so on).
@@ -151,14 +147,43 @@ export function getSentryConfigContents(
151147
// https://docs.sentry.io/platforms/javascript/guides/nextjs/`;
152148
}
153149

150+
let performanceOptions = '';
151+
if (selectedFeaturesMap.performance) {
152+
performanceOptions += `
153+
154+
// Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
155+
tracesSampleRate: 1,`;
156+
}
157+
158+
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
159+
return `${primer}
160+
161+
import * as Sentry from "@sentry/nextjs";
162+
163+
Sentry.init({
164+
dsn: "${dsn}",${performanceOptions}
165+
166+
// Setting this option to true will print useful information to the console while you're setting up Sentry.
167+
debug: false,
168+
});
169+
`;
170+
}
171+
172+
export function getInstrumentationClientFileContents(
173+
dsn: string,
174+
selectedFeaturesMap: {
175+
replay: boolean;
176+
performance: boolean;
177+
},
178+
): string {
154179
const integrationsOptions = getClientIntegrationsSnippet({
155-
replay: config === 'client' && selectedFeaturesMap.replay,
180+
replay: selectedFeaturesMap.replay,
156181
});
157182

158183
let replayOptions = '';
159-
if (config === 'client') {
160-
if (selectedFeaturesMap.replay) {
161-
replayOptions += `
184+
185+
if (selectedFeaturesMap.replay) {
186+
replayOptions += `
162187
163188
// Define how likely Replay events are sampled.
164189
// This sets the sample rate to be 10%. You may want this to be 100% while
@@ -167,7 +192,6 @@ export function getSentryConfigContents(
167192
168193
// Define how likely Replay events are sampled when an error occurs.
169194
replaysOnErrorSampleRate: 1.0,`;
170-
}
171195
}
172196

173197
let performanceOptions = '';
@@ -178,8 +202,9 @@ export function getSentryConfigContents(
178202
tracesSampleRate: 1,`;
179203
}
180204

181-
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
182-
return `${primer}
205+
return `// This file configures the initialization of Sentry on the client.
206+
// The added config here will be used whenever a users loads a page in their browser.
207+
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
183208
184209
import * as Sentry from "@sentry/nextjs";
185210
@@ -188,8 +213,7 @@ Sentry.init({
188213
189214
// Setting this option to true will print useful information to the console while you're setting up Sentry.
190215
debug: false,
191-
});
192-
`;
216+
});`;
193217
}
194218

195219
export function getSentryExamplePageContents(options: {
@@ -229,7 +253,7 @@ export default function Page() {
229253
</h1>
230254
231255
<p className="description">
232-
Click the button below, and view the sample error on the Sentry <a target="_blank" href="${issuesPageLink}">Issues Page</a>.
256+
Click the button below, and view the sample error on the Sentry <a target="_blank" href="${issuesPageLink}">Issues Page</a>.
233257
For more details about setting up Sentry, <a target="_blank" href="https://docs.sentry.io/platforms/javascript/guides/nextjs/">read our docs</a>.
234258
</p>
235259
@@ -500,6 +524,18 @@ ${plus('export const onRequestError = Sentry.captureRequestError;')}
500524
});
501525
}
502526

527+
export function getInstrumentationClientHookCopyPasteSnippet(
528+
dsn: string,
529+
selectedFeaturesMap: {
530+
replay: boolean;
531+
performance: boolean;
532+
},
533+
) {
534+
return makeCodeSnippet(true, (unchanged, plus) => {
535+
return plus(getInstrumentationClientFileContents(dsn, selectedFeaturesMap));
536+
});
537+
}
538+
503539
export function getSentryDefaultGlobalErrorPage(isTs: boolean) {
504540
return isTs
505541
? `"use client";
@@ -601,7 +637,7 @@ export default function GlobalError(${chalk.green('{ error }')}) {
601637
export const getRootLayout = (
602638
isTs: boolean,
603639
) => `// This file was generated by the Sentry wizard because we couldn't find a root layout file.
604-
// You can delete this file at any time.
640+
// You can delete this file at any time.
605641
606642
export const metadata = {
607643
title: 'Sentry NextJS Example',

0 commit comments

Comments
 (0)