diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/sentry.client.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/sentry.client.config.ts
index 85bd765c9c44..f2c7e4aef94d 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-13/sentry.client.config.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-13/sentry.client.config.ts
@@ -1,3 +1,5 @@
+'use client';
+
import * as Sentry from '@sentry/nextjs';
Sentry.init({
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-14/sentry.client.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-14/sentry.client.config.ts
index 85bd765c9c44..f2c7e4aef94d 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-14/sentry.client.config.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-14/sentry.client.config.ts
@@ -1,3 +1,5 @@
+'use client';
+
import * as Sentry from '@sentry/nextjs';
Sentry.init({
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/sentry.client.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-15/sentry.client.config.ts
index 85bd765c9c44..f2c7e4aef94d 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-15/sentry.client.config.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-15/sentry.client.config.ts
@@ -1,3 +1,5 @@
+'use client';
+
import * as Sentry from '@sentry/nextjs';
Sentry.init({
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/sentry.client.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/sentry.client.config.ts
index 85bd765c9c44..f2c7e4aef94d 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/sentry.client.config.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/sentry.client.config.ts
@@ -1,3 +1,5 @@
+'use client';
+
import * as Sentry from '@sentry/nextjs';
Sentry.init({
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-t3/sentry.client.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-t3/sentry.client.config.ts
index 0e3121a8f01b..6d63ba9325fe 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-t3/sentry.client.config.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-t3/sentry.client.config.ts
@@ -1,3 +1,5 @@
+'use client';
+
import * as Sentry from '@sentry/nextjs';
Sentry.init({
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/app/layout.tsx b/dev-packages/e2e-tests/test-applications/nextjs-turbo/app/layout.tsx
index c8f9cee0b787..999836e58b3b 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-turbo/app/layout.tsx
+++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/app/layout.tsx
@@ -1,7 +1,12 @@
+import { HackComponentToRunSideEffectsInSentryClientConfig } from '../sentry.client.config';
+
export default function Layout({ children }: { children: React.ReactNode }) {
return (
-
{children}
+
+
+ {children}
+
);
}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/pages/[param]/client-trace-propagation.tsx b/dev-packages/e2e-tests/test-applications/nextjs-turbo/pages/[param]/pages-router-client-trace-propagation.tsx
similarity index 100%
rename from dev-packages/e2e-tests/test-applications/nextjs-turbo/pages/[param]/client-trace-propagation.tsx
rename to dev-packages/e2e-tests/test-applications/nextjs-turbo/pages/[param]/pages-router-client-trace-propagation.tsx
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/pages/_app.tsx b/dev-packages/e2e-tests/test-applications/nextjs-turbo/pages/_app.tsx
new file mode 100644
index 000000000000..6b90ee6bc586
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/pages/_app.tsx
@@ -0,0 +1,6 @@
+import type { AppProps } from 'next/app';
+import '../sentry.client.config';
+
+export default function CustomApp({ Component, pageProps }: AppProps) {
+ return ;
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/sentry.client.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-turbo/sentry.client.config.ts
index 85bd765c9c44..7a49f1b55e11 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-turbo/sentry.client.config.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/sentry.client.config.ts
@@ -1,9 +1,17 @@
+'use client';
+
import * as Sentry from '@sentry/nextjs';
-Sentry.init({
- environment: 'qa', // dynamic sampling bias to keep transactions
- dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN,
- tunnel: `http://localhost:3031/`, // proxy server
- tracesSampleRate: 1.0,
- sendDefaultPii: true,
-});
+if (typeof window !== 'undefined') {
+ Sentry.init({
+ environment: 'qa', // dynamic sampling bias to keep transactions
+ dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN,
+ tunnel: `http://localhost:3031/`, // proxy server
+ tracesSampleRate: 1.0,
+ sendDefaultPii: true,
+ });
+}
+
+export function HackComponentToRunSideEffectsInSentryClientConfig() {
+ return null;
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/tests/pages-router/client-trace-propagation.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-turbo/tests/pages-router/client-trace-propagation.test.ts
index 6991466acb71..20a9181d7f8e 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-turbo/tests/pages-router/client-trace-propagation.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/tests/pages-router/client-trace-propagation.test.ts
@@ -1,25 +1,20 @@
import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';
-import { extractTraceparentData } from '@sentry/utils';
test('Should propagate traces from server to client in pages router', async ({ page }) => {
const serverTransactionPromise = waitForTransaction('nextjs-turbo', async transactionEvent => {
- return transactionEvent?.transaction === 'GET /[param]/client-trace-propagation';
+ return transactionEvent?.transaction === 'GET /[param]/pages-router-client-trace-propagation';
});
- await page.goto(`/123/client-trace-propagation`);
-
- const sentryTraceLocator = await page.locator('meta[name="sentry-trace"]');
- const sentryTraceValue = await sentryTraceLocator.getAttribute('content');
- expect(sentryTraceValue).toMatch(/^[a-f0-9]{32}-[a-f0-9]{16}-[0-1]$/);
-
- const baggageLocator = await page.locator('meta[name="baggage"]');
- const baggageValue = await baggageLocator.getAttribute('content');
- expect(baggageValue).toMatch(/sentry-public_key=/);
+ const pageloadTransactionPromise = waitForTransaction('nextjs-turbo', async transactionEvent => {
+ return transactionEvent?.transaction === '/[param]/pages-router-client-trace-propagation';
+ });
- const traceparentData = extractTraceparentData(sentryTraceValue!);
+ await page.goto(`/123/pages-router-client-trace-propagation`);
const serverTransaction = await serverTransactionPromise;
+ const pageloadTransaction = await pageloadTransactionPromise;
- expect(serverTransaction.contexts?.trace?.trace_id).toBe(traceparentData?.traceId);
+ expect(serverTransaction.contexts?.trace?.trace_id).toBeDefined();
+ expect(pageloadTransaction.contexts?.trace?.trace_id).toBe(serverTransaction.contexts?.trace?.trace_id);
});
diff --git a/packages/nextjs/src/config/loaders/valueInjectionLoader.ts b/packages/nextjs/src/config/loaders/valueInjectionLoader.ts
index bf89ce90ac2c..c3d7b499fabb 100644
--- a/packages/nextjs/src/config/loaders/valueInjectionLoader.ts
+++ b/packages/nextjs/src/config/loaders/valueInjectionLoader.ts
@@ -1,9 +1,20 @@
+// Rollup doesn't like if we put the directive regex as a literal (?). No idea why.
+/* eslint-disable @sentry-internal/sdk/no-regexp-constructor */
+
import type { LoaderThis } from './types';
-type LoaderOptions = {
+export type ValueInjectionLoaderOptions = {
values: Record;
};
+// We need to be careful not to inject anything before any `"use strict";`s or "use client"s or really any other directive.
+// As an additional complication directives may come after any number of comments.
+// This regex is shamelessly stolen from: https://github.com/getsentry/sentry-javascript-bundler-plugins/blob/7f984482c73e4284e8b12a08dfedf23b5a82f0af/packages/bundler-plugin-core/src/index.ts#L535-L539
+const SKIP_COMMENT_AND_DIRECTIVE_REGEX =
+ // Note: CodeQL complains that this regex potentially has n^2 runtime. This likely won't affect realistic files.
+ // biome-ignore lint/nursery/useRegexLiterals: No user input
+ new RegExp('^(?:\\s*|/\\*(?:.|\\r|\\n)*?\\*/|//.*[\\n\\r])*(?:"[^"]*";?|\'[^\']*\';?)?');
+
/**
* Set values on the global/window object at the start of a module.
*
@@ -11,16 +22,22 @@ type LoaderOptions = {
* - `values`: An object where the keys correspond to the keys of the global values to set and the values
* correspond to the values of the values on the global object. Values must be JSON serializable.
*/
-export default function valueInjectionLoader(this: LoaderThis, userCode: string): string {
+export default function valueInjectionLoader(this: LoaderThis, userCode: string): string {
// We know one or the other will be defined, depending on the version of webpack being used
const { values } = 'getOptions' in this ? this.getOptions() : this.query;
// We do not want to cache injected values across builds
this.cacheable(false);
- const injectedCode = Object.entries(values)
- .map(([key, value]) => `globalThis["${key}"] = ${JSON.stringify(value)};`)
- .join('\n');
+ // Not putting any newlines in the generated code will decrease the likelihood of sourcemaps breaking
+ const injectedCode =
+ // eslint-disable-next-line prefer-template
+ ';' +
+ Object.entries(values)
+ .map(([key, value]) => `globalThis["${key}"] = ${JSON.stringify(value)};`)
+ .join('');
- return `${injectedCode}\n${userCode}`;
+ return userCode.replace(SKIP_COMMENT_AND_DIRECTIVE_REGEX, match => {
+ return match + injectedCode;
+ });
}
diff --git a/packages/nextjs/test/config/__snapshots__/valueInjectionLoader.test.ts.snap b/packages/nextjs/test/config/__snapshots__/valueInjectionLoader.test.ts.snap
new file mode 100644
index 000000000000..ca901580da63
--- /dev/null
+++ b/packages/nextjs/test/config/__snapshots__/valueInjectionLoader.test.ts.snap
@@ -0,0 +1,83 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`valueInjectionLoader should correctly insert values for basic config 1`] = `
+"
+ ;globalThis[\\"foo\\"] = \\"bar\\";import * as Sentry from '@sentry/nextjs';
+ Sentry.init();
+ "
+`;
+
+exports[`valueInjectionLoader should correctly insert values with a misplaced directive 1`] = `
+"
+ ;globalThis[\\"foo\\"] = \\"bar\\";console.log('This will render the directive useless');
+ \\"use client\\";
+
+
+
+ import * as Sentry from '@sentry/nextjs';
+ Sentry.init();
+ "
+`;
+
+exports[`valueInjectionLoader should correctly insert values with directive 1`] = `
+"
+ \\"use client\\";globalThis[\\"foo\\"] = \\"bar\\";
+ import * as Sentry from '@sentry/nextjs';
+ Sentry.init();
+ "
+`;
+
+exports[`valueInjectionLoader should correctly insert values with directive and block comments 1`] = `
+"
+ /* test */
+ \\"use client\\";;globalThis[\\"foo\\"] = \\"bar\\";
+ import * as Sentry from '@sentry/nextjs';
+ Sentry.init();
+ "
+`;
+
+exports[`valueInjectionLoader should correctly insert values with directive and inline comments 1`] = `
+"
+ // test
+ \\"use client\\";;globalThis[\\"foo\\"] = \\"bar\\";
+ import * as Sentry from '@sentry/nextjs';
+ Sentry.init();
+ "
+`;
+
+exports[`valueInjectionLoader should correctly insert values with directive and multiline block comments 1`] = `
+"
+ /*
+ test
+ */
+ \\"use client\\";;globalThis[\\"foo\\"] = \\"bar\\";
+ import * as Sentry from '@sentry/nextjs';
+ Sentry.init();
+ "
+`;
+
+exports[`valueInjectionLoader should correctly insert values with directive and multiline block comments and a bunch of whitespace 1`] = `
+"
+ /*
+ test
+ */
+
+
+
+
+ \\"use client\\";;globalThis[\\"foo\\"] = \\"bar\\";
+
+
+
+ import * as Sentry from '@sentry/nextjs';
+ Sentry.init();
+ "
+`;
+
+exports[`valueInjectionLoader should correctly insert values with directive and semicolon 1`] = `
+"
+ \\"use client\\";;globalThis[\\"foo\\"] = \\"bar\\";
+ import * as Sentry from '@sentry/nextjs';
+ Sentry.init();
+ "
+`;
diff --git a/packages/nextjs/test/config/valueInjectionLoader.test.ts b/packages/nextjs/test/config/valueInjectionLoader.test.ts
new file mode 100644
index 000000000000..2d810ad87c5a
--- /dev/null
+++ b/packages/nextjs/test/config/valueInjectionLoader.test.ts
@@ -0,0 +1,146 @@
+import type { LoaderThis } from '../../src/config/loaders/types';
+import type { ValueInjectionLoaderOptions } from '../../src/config/loaders/valueInjectionLoader';
+import valueInjectionLoader from '../../src/config/loaders/valueInjectionLoader';
+
+const defaultLoaderThis = {
+ addDependency: () => undefined,
+ async: () => undefined,
+ cacheable: () => undefined,
+ callback: () => undefined,
+};
+
+const loaderThis = {
+ ...defaultLoaderThis,
+ resourcePath: './client.config.ts',
+ getOptions() {
+ return {
+ values: {
+ foo: 'bar',
+ },
+ };
+ },
+} satisfies LoaderThis;
+
+describe('valueInjectionLoader', () => {
+ it('should correctly insert values for basic config', () => {
+ const userCode = `
+ import * as Sentry from '@sentry/nextjs';
+ Sentry.init();
+ `;
+
+ const result = valueInjectionLoader.call(loaderThis, userCode);
+
+ expect(result).toMatchSnapshot();
+ expect(result).toMatch(';globalThis["foo"] = "bar";');
+ });
+
+ it('should correctly insert values with directive', () => {
+ const userCode = `
+ "use client"
+ import * as Sentry from '@sentry/nextjs';
+ Sentry.init();
+ `;
+
+ const result = valueInjectionLoader.call(loaderThis, userCode);
+
+ expect(result).toMatchSnapshot();
+ expect(result).toMatch(';globalThis["foo"] = "bar";');
+ });
+
+ it('should correctly insert values with directive and semicolon', () => {
+ const userCode = `
+ "use client";
+ import * as Sentry from '@sentry/nextjs';
+ Sentry.init();
+ `;
+
+ const result = valueInjectionLoader.call(loaderThis, userCode);
+
+ expect(result).toMatchSnapshot();
+ expect(result).toMatch(';globalThis["foo"] = "bar";');
+ });
+
+ it('should correctly insert values with directive and inline comments', () => {
+ const userCode = `
+ // test
+ "use client";
+ import * as Sentry from '@sentry/nextjs';
+ Sentry.init();
+ `;
+
+ const result = valueInjectionLoader.call(loaderThis, userCode);
+
+ expect(result).toMatchSnapshot();
+ expect(result).toMatch(';globalThis["foo"] = "bar";');
+ });
+
+ it('should correctly insert values with directive and block comments', () => {
+ const userCode = `
+ /* test */
+ "use client";
+ import * as Sentry from '@sentry/nextjs';
+ Sentry.init();
+ `;
+
+ const result = valueInjectionLoader.call(loaderThis, userCode);
+
+ expect(result).toMatchSnapshot();
+ expect(result).toMatch(';globalThis["foo"] = "bar";');
+ });
+
+ it('should correctly insert values with directive and multiline block comments', () => {
+ const userCode = `
+ /*
+ test
+ */
+ "use client";
+ import * as Sentry from '@sentry/nextjs';
+ Sentry.init();
+ `;
+
+ const result = valueInjectionLoader.call(loaderThis, userCode);
+
+ expect(result).toMatchSnapshot();
+ expect(result).toMatch(';globalThis["foo"] = "bar";');
+ });
+
+ it('should correctly insert values with directive and multiline block comments and a bunch of whitespace', () => {
+ const userCode = `
+ /*
+ test
+ */
+
+
+
+
+ "use client";
+
+
+
+ import * as Sentry from '@sentry/nextjs';
+ Sentry.init();
+ `;
+
+ const result = valueInjectionLoader.call(loaderThis, userCode);
+
+ expect(result).toMatchSnapshot();
+ expect(result).toMatch(';globalThis["foo"] = "bar";');
+ });
+
+ it('should correctly insert values with a misplaced directive', () => {
+ const userCode = `
+ console.log('This will render the directive useless');
+ "use client";
+
+
+
+ import * as Sentry from '@sentry/nextjs';
+ Sentry.init();
+ `;
+
+ const result = valueInjectionLoader.call(loaderThis, userCode);
+
+ expect(result).toMatchSnapshot();
+ expect(result).toMatch(';globalThis["foo"] = "bar";');
+ });
+});