Skip to content

Commit e68865a

Browse files
authored
fix(nextjs): Respect directives in value injection loader (#14083)
This PR is in preparation for turbopack (#8105). In the future, `sentry.client.config.ts` will likely need to be configured with a `"use client"` directive so that turbopack knows it needs to be treated as a file on the client. Our value injection loader currently always prepends the `sentry.client.config.ts` file with statements, rendering any directives in the file useless and crashing turbopack when the file is attempted to be imported somewhere. This PR detects any comments and directives on top of a file to only inject values after.
1 parent ff8e780 commit e68865a

File tree

13 files changed

+297
-27
lines changed

13 files changed

+297
-27
lines changed

dev-packages/e2e-tests/test-applications/nextjs-13/sentry.client.config.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
'use client';
2+
13
import * as Sentry from '@sentry/nextjs';
24

35
Sentry.init({

dev-packages/e2e-tests/test-applications/nextjs-14/sentry.client.config.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
'use client';
2+
13
import * as Sentry from '@sentry/nextjs';
24

35
Sentry.init({

dev-packages/e2e-tests/test-applications/nextjs-15/sentry.client.config.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
'use client';
2+
13
import * as Sentry from '@sentry/nextjs';
24

35
Sentry.init({

dev-packages/e2e-tests/test-applications/nextjs-app-dir/sentry.client.config.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
'use client';
2+
13
import * as Sentry from '@sentry/nextjs';
24

35
Sentry.init({

dev-packages/e2e-tests/test-applications/nextjs-t3/sentry.client.config.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
'use client';
2+
13
import * as Sentry from '@sentry/nextjs';
24

35
Sentry.init({
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1+
import { HackComponentToRunSideEffectsInSentryClientConfig } from '../sentry.client.config';
2+
13
export default function Layout({ children }: { children: React.ReactNode }) {
24
return (
35
<html lang="en">
4-
<body>{children}</body>
6+
<body>
7+
<HackComponentToRunSideEffectsInSentryClientConfig />
8+
{children}
9+
</body>
510
</html>
611
);
712
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import type { AppProps } from 'next/app';
2+
import '../sentry.client.config';
3+
4+
export default function CustomApp({ Component, pageProps }: AppProps) {
5+
return <Component {...pageProps} />;
6+
}
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
1+
'use client';
2+
13
import * as Sentry from '@sentry/nextjs';
24

3-
Sentry.init({
4-
environment: 'qa', // dynamic sampling bias to keep transactions
5-
dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN,
6-
tunnel: `http://localhost:3031/`, // proxy server
7-
tracesSampleRate: 1.0,
8-
sendDefaultPii: true,
9-
});
5+
if (typeof window !== 'undefined') {
6+
Sentry.init({
7+
environment: 'qa', // dynamic sampling bias to keep transactions
8+
dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN,
9+
tunnel: `http://localhost:3031/`, // proxy server
10+
tracesSampleRate: 1.0,
11+
sendDefaultPii: true,
12+
});
13+
}
14+
15+
export function HackComponentToRunSideEffectsInSentryClientConfig() {
16+
return null;
17+
}
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,20 @@
11
import { expect, test } from '@playwright/test';
22
import { waitForTransaction } from '@sentry-internal/test-utils';
3-
import { extractTraceparentData } from '@sentry/utils';
43

54
test('Should propagate traces from server to client in pages router', async ({ page }) => {
65
const serverTransactionPromise = waitForTransaction('nextjs-turbo', async transactionEvent => {
7-
return transactionEvent?.transaction === 'GET /[param]/client-trace-propagation';
6+
return transactionEvent?.transaction === 'GET /[param]/pages-router-client-trace-propagation';
87
});
98

10-
await page.goto(`/123/client-trace-propagation`);
11-
12-
const sentryTraceLocator = await page.locator('meta[name="sentry-trace"]');
13-
const sentryTraceValue = await sentryTraceLocator.getAttribute('content');
14-
expect(sentryTraceValue).toMatch(/^[a-f0-9]{32}-[a-f0-9]{16}-[0-1]$/);
15-
16-
const baggageLocator = await page.locator('meta[name="baggage"]');
17-
const baggageValue = await baggageLocator.getAttribute('content');
18-
expect(baggageValue).toMatch(/sentry-public_key=/);
9+
const pageloadTransactionPromise = waitForTransaction('nextjs-turbo', async transactionEvent => {
10+
return transactionEvent?.transaction === '/[param]/pages-router-client-trace-propagation';
11+
});
1912

20-
const traceparentData = extractTraceparentData(sentryTraceValue!);
13+
await page.goto(`/123/pages-router-client-trace-propagation`);
2114

2215
const serverTransaction = await serverTransactionPromise;
16+
const pageloadTransaction = await pageloadTransactionPromise;
2317

24-
expect(serverTransaction.contexts?.trace?.trace_id).toBe(traceparentData?.traceId);
18+
expect(serverTransaction.contexts?.trace?.trace_id).toBeDefined();
19+
expect(pageloadTransaction.contexts?.trace?.trace_id).toBe(serverTransaction.contexts?.trace?.trace_id);
2520
});
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,43 @@
1+
// Rollup doesn't like if we put the directive regex as a literal (?). No idea why.
2+
/* eslint-disable @sentry-internal/sdk/no-regexp-constructor */
3+
14
import type { LoaderThis } from './types';
25

3-
type LoaderOptions = {
6+
export type ValueInjectionLoaderOptions = {
47
values: Record<string, unknown>;
58
};
69

10+
// We need to be careful not to inject anything before any `"use strict";`s or "use client"s or really any other directive.
11+
// As an additional complication directives may come after any number of comments.
12+
// 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
13+
const SKIP_COMMENT_AND_DIRECTIVE_REGEX =
14+
// Note: CodeQL complains that this regex potentially has n^2 runtime. This likely won't affect realistic files.
15+
// biome-ignore lint/nursery/useRegexLiterals: No user input
16+
new RegExp('^(?:\\s*|/\\*(?:.|\\r|\\n)*?\\*/|//.*[\\n\\r])*(?:"[^"]*";?|\'[^\']*\';?)?');
17+
718
/**
819
* Set values on the global/window object at the start of a module.
920
*
1021
* Options:
1122
* - `values`: An object where the keys correspond to the keys of the global values to set and the values
1223
* correspond to the values of the values on the global object. Values must be JSON serializable.
1324
*/
14-
export default function valueInjectionLoader(this: LoaderThis<LoaderOptions>, userCode: string): string {
25+
export default function valueInjectionLoader(this: LoaderThis<ValueInjectionLoaderOptions>, userCode: string): string {
1526
// We know one or the other will be defined, depending on the version of webpack being used
1627
const { values } = 'getOptions' in this ? this.getOptions() : this.query;
1728

1829
// We do not want to cache injected values across builds
1930
this.cacheable(false);
2031

21-
const injectedCode = Object.entries(values)
22-
.map(([key, value]) => `globalThis["${key}"] = ${JSON.stringify(value)};`)
23-
.join('\n');
32+
// Not putting any newlines in the generated code will decrease the likelihood of sourcemaps breaking
33+
const injectedCode =
34+
// eslint-disable-next-line prefer-template
35+
';' +
36+
Object.entries(values)
37+
.map(([key, value]) => `globalThis["${key}"] = ${JSON.stringify(value)};`)
38+
.join('');
2439

25-
return `${injectedCode}\n${userCode}`;
40+
return userCode.replace(SKIP_COMMENT_AND_DIRECTIVE_REGEX, match => {
41+
return match + injectedCode;
42+
});
2643
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`valueInjectionLoader should correctly insert values for basic config 1`] = `
4+
"
5+
;globalThis[\\"foo\\"] = \\"bar\\";import * as Sentry from '@sentry/nextjs';
6+
Sentry.init();
7+
"
8+
`;
9+
10+
exports[`valueInjectionLoader should correctly insert values with a misplaced directive 1`] = `
11+
"
12+
;globalThis[\\"foo\\"] = \\"bar\\";console.log('This will render the directive useless');
13+
\\"use client\\";
14+
15+
16+
17+
import * as Sentry from '@sentry/nextjs';
18+
Sentry.init();
19+
"
20+
`;
21+
22+
exports[`valueInjectionLoader should correctly insert values with directive 1`] = `
23+
"
24+
\\"use client\\";globalThis[\\"foo\\"] = \\"bar\\";
25+
import * as Sentry from '@sentry/nextjs';
26+
Sentry.init();
27+
"
28+
`;
29+
30+
exports[`valueInjectionLoader should correctly insert values with directive and block comments 1`] = `
31+
"
32+
/* test */
33+
\\"use client\\";;globalThis[\\"foo\\"] = \\"bar\\";
34+
import * as Sentry from '@sentry/nextjs';
35+
Sentry.init();
36+
"
37+
`;
38+
39+
exports[`valueInjectionLoader should correctly insert values with directive and inline comments 1`] = `
40+
"
41+
// test
42+
\\"use client\\";;globalThis[\\"foo\\"] = \\"bar\\";
43+
import * as Sentry from '@sentry/nextjs';
44+
Sentry.init();
45+
"
46+
`;
47+
48+
exports[`valueInjectionLoader should correctly insert values with directive and multiline block comments 1`] = `
49+
"
50+
/*
51+
test
52+
*/
53+
\\"use client\\";;globalThis[\\"foo\\"] = \\"bar\\";
54+
import * as Sentry from '@sentry/nextjs';
55+
Sentry.init();
56+
"
57+
`;
58+
59+
exports[`valueInjectionLoader should correctly insert values with directive and multiline block comments and a bunch of whitespace 1`] = `
60+
"
61+
/*
62+
test
63+
*/
64+
65+
66+
67+
68+
\\"use client\\";;globalThis[\\"foo\\"] = \\"bar\\";
69+
70+
71+
72+
import * as Sentry from '@sentry/nextjs';
73+
Sentry.init();
74+
"
75+
`;
76+
77+
exports[`valueInjectionLoader should correctly insert values with directive and semicolon 1`] = `
78+
"
79+
\\"use client\\";;globalThis[\\"foo\\"] = \\"bar\\";
80+
import * as Sentry from '@sentry/nextjs';
81+
Sentry.init();
82+
"
83+
`;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import type { LoaderThis } from '../../src/config/loaders/types';
2+
import type { ValueInjectionLoaderOptions } from '../../src/config/loaders/valueInjectionLoader';
3+
import valueInjectionLoader from '../../src/config/loaders/valueInjectionLoader';
4+
5+
const defaultLoaderThis = {
6+
addDependency: () => undefined,
7+
async: () => undefined,
8+
cacheable: () => undefined,
9+
callback: () => undefined,
10+
};
11+
12+
const loaderThis = {
13+
...defaultLoaderThis,
14+
resourcePath: './client.config.ts',
15+
getOptions() {
16+
return {
17+
values: {
18+
foo: 'bar',
19+
},
20+
};
21+
},
22+
} satisfies LoaderThis<ValueInjectionLoaderOptions>;
23+
24+
describe('valueInjectionLoader', () => {
25+
it('should correctly insert values for basic config', () => {
26+
const userCode = `
27+
import * as Sentry from '@sentry/nextjs';
28+
Sentry.init();
29+
`;
30+
31+
const result = valueInjectionLoader.call(loaderThis, userCode);
32+
33+
expect(result).toMatchSnapshot();
34+
expect(result).toMatch(';globalThis["foo"] = "bar";');
35+
});
36+
37+
it('should correctly insert values with directive', () => {
38+
const userCode = `
39+
"use client"
40+
import * as Sentry from '@sentry/nextjs';
41+
Sentry.init();
42+
`;
43+
44+
const result = valueInjectionLoader.call(loaderThis, userCode);
45+
46+
expect(result).toMatchSnapshot();
47+
expect(result).toMatch(';globalThis["foo"] = "bar";');
48+
});
49+
50+
it('should correctly insert values with directive and semicolon', () => {
51+
const userCode = `
52+
"use client";
53+
import * as Sentry from '@sentry/nextjs';
54+
Sentry.init();
55+
`;
56+
57+
const result = valueInjectionLoader.call(loaderThis, userCode);
58+
59+
expect(result).toMatchSnapshot();
60+
expect(result).toMatch(';globalThis["foo"] = "bar";');
61+
});
62+
63+
it('should correctly insert values with directive and inline comments', () => {
64+
const userCode = `
65+
// test
66+
"use client";
67+
import * as Sentry from '@sentry/nextjs';
68+
Sentry.init();
69+
`;
70+
71+
const result = valueInjectionLoader.call(loaderThis, userCode);
72+
73+
expect(result).toMatchSnapshot();
74+
expect(result).toMatch(';globalThis["foo"] = "bar";');
75+
});
76+
77+
it('should correctly insert values with directive and block comments', () => {
78+
const userCode = `
79+
/* test */
80+
"use client";
81+
import * as Sentry from '@sentry/nextjs';
82+
Sentry.init();
83+
`;
84+
85+
const result = valueInjectionLoader.call(loaderThis, userCode);
86+
87+
expect(result).toMatchSnapshot();
88+
expect(result).toMatch(';globalThis["foo"] = "bar";');
89+
});
90+
91+
it('should correctly insert values with directive and multiline block comments', () => {
92+
const userCode = `
93+
/*
94+
test
95+
*/
96+
"use client";
97+
import * as Sentry from '@sentry/nextjs';
98+
Sentry.init();
99+
`;
100+
101+
const result = valueInjectionLoader.call(loaderThis, userCode);
102+
103+
expect(result).toMatchSnapshot();
104+
expect(result).toMatch(';globalThis["foo"] = "bar";');
105+
});
106+
107+
it('should correctly insert values with directive and multiline block comments and a bunch of whitespace', () => {
108+
const userCode = `
109+
/*
110+
test
111+
*/
112+
113+
114+
115+
116+
"use client";
117+
118+
119+
120+
import * as Sentry from '@sentry/nextjs';
121+
Sentry.init();
122+
`;
123+
124+
const result = valueInjectionLoader.call(loaderThis, userCode);
125+
126+
expect(result).toMatchSnapshot();
127+
expect(result).toMatch(';globalThis["foo"] = "bar";');
128+
});
129+
130+
it('should correctly insert values with a misplaced directive', () => {
131+
const userCode = `
132+
console.log('This will render the directive useless');
133+
"use client";
134+
135+
136+
137+
import * as Sentry from '@sentry/nextjs';
138+
Sentry.init();
139+
`;
140+
141+
const result = valueInjectionLoader.call(loaderThis, userCode);
142+
143+
expect(result).toMatchSnapshot();
144+
expect(result).toMatch(';globalThis["foo"] = "bar";');
145+
});
146+
});

0 commit comments

Comments
 (0)