Skip to content

Commit b615f61

Browse files
mydeachargome
andauthored
feat(nextjs): Ensure all packages we auto-instrument are externalized (#16552)
So they can be instrumented by us. I think this should be fine and not really affect users, hopefully, since Next.js itself also does this already for common packages. Closes #16550 --------- Co-authored-by: Charly Gomez <[email protected]>
1 parent 4e7c7ef commit b615f61

File tree

3 files changed

+125
-7
lines changed

3 files changed

+125
-7
lines changed

packages/nextjs/src/config/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,12 @@ export type NextConfigObject = {
4545
experimental?: {
4646
instrumentationHook?: boolean;
4747
clientTraceMetadata?: string[];
48+
serverComponentsExternalPackages?: string[]; // next < v15.0.0
4849
};
4950
productionBrowserSourceMaps?: boolean;
5051
// https://nextjs.org/docs/pages/api-reference/next-config-js/env
5152
env?: Record<string, string>;
53+
serverExternalPackages?: string[]; // next >= v15.0.0
5254
};
5355

5456
export type SentryBuildOptions = {

packages/nextjs/src/config/withSentryConfig.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,35 @@ import { constructWebpackConfigFunction } from './webpack';
1717
let showedExportModeTunnelWarning = false;
1818
let showedExperimentalBuildModeWarning = false;
1919

20+
// Packages we auto-instrument need to be external for instrumentation to work
21+
// Next.js externalizes some packages by default, see: https://nextjs.org/docs/app/api-reference/config/next-config-js/serverExternalPackages
22+
// Others we need to add ourselves
23+
export const DEFAULT_SERVER_EXTERNAL_PACKAGES = [
24+
'ai',
25+
'amqplib',
26+
'connect',
27+
'dataloader',
28+
'express',
29+
'generic-pool',
30+
'graphql',
31+
'@hapi/hapi',
32+
'ioredis',
33+
'kafkajs',
34+
'koa',
35+
'lru-memoizer',
36+
'mongodb',
37+
'mongoose',
38+
'mysql',
39+
'mysql2',
40+
'knex',
41+
'pg',
42+
'pg-pool',
43+
'@node-redis/client',
44+
'@redis/client',
45+
'redis',
46+
'tedious',
47+
];
48+
2049
/**
2150
* Modifies the passed in Next.js configuration with automatic build-time instrumentation and source map upload.
2251
*
@@ -190,8 +219,10 @@ function getFinalConfigObject(
190219
);
191220
}
192221

222+
let nextMajor: number | undefined;
193223
if (nextJsVersion) {
194224
const { major, minor, patch, prerelease } = parseSemver(nextJsVersion);
225+
nextMajor = major;
195226
const isSupportedVersion =
196227
major !== undefined &&
197228
minor !== undefined &&
@@ -229,6 +260,22 @@ function getFinalConfigObject(
229260

230261
return {
231262
...incomingUserNextConfigObject,
263+
...(nextMajor && nextMajor >= 15
264+
? {
265+
serverExternalPackages: [
266+
...(incomingUserNextConfigObject.serverExternalPackages || []),
267+
...DEFAULT_SERVER_EXTERNAL_PACKAGES,
268+
],
269+
}
270+
: {
271+
experimental: {
272+
...incomingUserNextConfigObject.experimental,
273+
serverComponentsExternalPackages: [
274+
...(incomingUserNextConfigObject.experimental?.serverComponentsExternalPackages || []),
275+
...DEFAULT_SERVER_EXTERNAL_PACKAGES,
276+
],
277+
},
278+
}),
232279
webpack: constructWebpackConfigFunction(incomingUserNextConfigObject, userSentryOptions, releaseName),
233280
};
234281
}

packages/nextjs/test/config/withSentryConfig.test.ts

Lines changed: 76 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import { describe, expect, it, vi } from 'vitest';
1+
import { afterEach, describe, expect, it, vi } from 'vitest';
2+
import * as util from '../../src/config/util';
3+
import { DEFAULT_SERVER_EXTERNAL_PACKAGES } from '../../src/config/withSentryConfig';
24
import { defaultRuntimePhase, defaultsObject, exportedNextConfig, userNextConfig } from './fixtures';
35
import { materializeFinalNextConfig } from './testUtils';
46

@@ -22,10 +24,16 @@ describe('withSentryConfig', () => {
2224
it("works when user's overall config is an object", () => {
2325
const finalConfig = materializeFinalNextConfig(exportedNextConfig);
2426

25-
expect(finalConfig).toEqual(
27+
const { webpack, experimental, ...restOfFinalConfig } = finalConfig;
28+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
29+
const { webpack: _userWebpack, experimental: _userExperimental, ...restOfUserConfig } = userNextConfig;
30+
31+
expect(restOfFinalConfig).toEqual(restOfUserConfig);
32+
expect(webpack).toBeInstanceOf(Function);
33+
expect(experimental).toEqual(
2634
expect.objectContaining({
27-
...userNextConfig,
28-
webpack: expect.any(Function), // `webpack` is tested specifically elsewhere
35+
instrumentationHook: true,
36+
serverComponentsExternalPackages: expect.arrayContaining(DEFAULT_SERVER_EXTERNAL_PACKAGES),
2937
}),
3038
);
3139
});
@@ -35,10 +43,21 @@ describe('withSentryConfig', () => {
3543

3644
const finalConfig = materializeFinalNextConfig(exportedNextConfigFunction);
3745

38-
expect(finalConfig).toEqual(
46+
const { webpack, experimental, ...restOfFinalConfig } = finalConfig;
47+
const {
48+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
49+
webpack: _userWebpack,
50+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
51+
experimental: _userExperimental,
52+
...restOfUserConfig
53+
} = exportedNextConfigFunction();
54+
55+
expect(restOfFinalConfig).toEqual(restOfUserConfig);
56+
expect(webpack).toBeInstanceOf(Function);
57+
expect(experimental).toEqual(
3958
expect.objectContaining({
40-
...exportedNextConfigFunction(),
41-
webpack: expect.any(Function), // `webpack` is tested specifically elsewhere
59+
instrumentationHook: true,
60+
serverComponentsExternalPackages: expect.arrayContaining(DEFAULT_SERVER_EXTERNAL_PACKAGES),
4261
}),
4362
);
4463
});
@@ -75,4 +94,54 @@ describe('withSentryConfig', () => {
7594
consoleWarnSpy.mockRestore();
7695
}
7796
});
97+
98+
describe('server packages configuration', () => {
99+
afterEach(() => {
100+
vi.restoreAllMocks();
101+
});
102+
103+
it('uses serverExternalPackages for Next.js 15+', () => {
104+
vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.0.0');
105+
const finalConfig = materializeFinalNextConfig(exportedNextConfig);
106+
107+
expect(finalConfig.serverExternalPackages).toBeDefined();
108+
expect(finalConfig.serverExternalPackages).toEqual(expect.arrayContaining(DEFAULT_SERVER_EXTERNAL_PACKAGES));
109+
expect(finalConfig.experimental?.serverComponentsExternalPackages).toBeUndefined();
110+
});
111+
112+
it('uses experimental.serverComponentsExternalPackages for Next.js < 15', () => {
113+
vi.spyOn(util, 'getNextjsVersion').mockReturnValue('14.0.0');
114+
const finalConfig = materializeFinalNextConfig(exportedNextConfig);
115+
116+
expect(finalConfig.serverExternalPackages).toBeUndefined();
117+
expect(finalConfig.experimental?.serverComponentsExternalPackages).toBeDefined();
118+
expect(finalConfig.experimental?.serverComponentsExternalPackages).toEqual(
119+
expect.arrayContaining(DEFAULT_SERVER_EXTERNAL_PACKAGES),
120+
);
121+
});
122+
123+
it('preserves existing packages in both versions', () => {
124+
const existingPackages = ['@some/existing-package'];
125+
126+
vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.0.0');
127+
const config15 = materializeFinalNextConfig({
128+
...exportedNextConfig,
129+
serverExternalPackages: existingPackages,
130+
});
131+
expect(config15.serverExternalPackages).toEqual(
132+
expect.arrayContaining([...existingPackages, ...DEFAULT_SERVER_EXTERNAL_PACKAGES]),
133+
);
134+
135+
vi.spyOn(util, 'getNextjsVersion').mockReturnValue('14.0.0');
136+
const config14 = materializeFinalNextConfig({
137+
...exportedNextConfig,
138+
experimental: {
139+
serverComponentsExternalPackages: existingPackages,
140+
},
141+
});
142+
expect(config14.experimental?.serverComponentsExternalPackages).toEqual(
143+
expect.arrayContaining([...existingPackages, ...DEFAULT_SERVER_EXTERNAL_PACKAGES]),
144+
);
145+
});
146+
});
78147
});

0 commit comments

Comments
 (0)