Skip to content

Commit 307cefb

Browse files
authored
fix(nuxt): Fix CommonJS interop for force-inlined instrumented packages (#24799)
Since v11, build-time instrumentation force-inlines the instrumented drivers (mongoose, ioredis, mysql, ...) into the Nitro bundle while their CommonJS dependencies stay external. Since Node 23, CJS namespaces carry a `module.exports` key next to `default` (nodejs/node#53848), so Rollup's `'auto'` interop no longer unwraps those requires and the drivers crash at startup (`mquery is not a constructor`). No single interop mode (a `requireReturnsDefault` value) fixes this and works for all externals. `debug` needs the namespace, while `mquery` and `lodash.defaults` need `module.exports`. So we unwrap builtins plus a known list and keep `'auto'` for the rest. Fixes #24775 Fixes #24786
1 parent 325d89c commit 307cefb

20 files changed

Lines changed: 388 additions & 15 deletions

File tree

‎dev-packages/e2e-tests/test-applications/nuxt-4/docker-compose.yml‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,22 @@ services:
1717
retries: 30
1818
start_period: 10s
1919

20+
mongo:
21+
image: mongo:8
22+
restart: always
23+
container_name: e2e-tests-nuxt-4-mongo
24+
ports:
25+
- '27017:27017'
26+
environment:
27+
MONGO_INITDB_ROOT_USERNAME: root
28+
MONGO_INITDB_ROOT_PASSWORD: docker
29+
healthcheck:
30+
test: ['CMD-SHELL', 'mongosh --quiet --eval "db.adminCommand(''ping'').ok"']
31+
interval: 2s
32+
timeout: 3s
33+
retries: 30
34+
start_period: 10s
35+
2036
redis:
2137
image: redis:7
2238
restart: always

‎dev-packages/e2e-tests/test-applications/nuxt-4/global-setup.mjs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { fileURLToPath } from 'url';
55
const __dirname = dirname(fileURLToPath(import.meta.url));
66

77
export default async function globalSetup() {
8-
// Start MySQL + Redis via Docker Compose. `--wait` blocks until the
8+
// Start MySQL, MongoDB, and Redis via Docker Compose. `--wait` blocks until the
99
// healthchecks in docker-compose.yml pass, so the app can connect immediately.
1010
execSync('docker compose up -d --wait', {
1111
cwd: __dirname,

‎dev-packages/e2e-tests/test-applications/nuxt-4/package.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"@pinia/nuxt": "^0.5.5",
2424
"@sentry/nuxt": "file:../../packed/sentry-nuxt-packed.tgz",
2525
"ioredis": "5.10.1",
26+
"mongoose": "^9.10.2",
2627
"mysql": "^2.18.1",
2728
"nuxt": "^4.1.2"
2829
},
@@ -31,8 +32,7 @@
3132
"@sentry-internal/test-utils": "link:../../../test-utils"
3233
},
3334
"volta": {
34-
"extends": "../../package.json",
35-
"node": "22.20.0"
35+
"extends": "../../package.json"
3636
},
3737
"sentryTest": {
3838
"variants": [
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { defineEventHandler } from '#imports';
2+
import mongoose from 'mongoose';
3+
4+
const BlogPost = mongoose.model('BlogPost', new mongoose.Schema({ title: String }));
5+
6+
// SCRAM-SHA-1 on purpose: this handshake lazily `require()`s the `crypto` builtin inside a try
7+
// block, which only works when the bundled driver gets a real interop for builtin requires (#24775).
8+
const MONGO_URL = 'mongodb://root:docker@127.0.0.1:27017/test?authSource=admin&authMechanism=SCRAM-SHA-1';
9+
10+
export default defineEventHandler(async () => {
11+
if (mongoose.connection.readyState !== mongoose.ConnectionStates.connected) {
12+
await mongoose.connect(MONGO_URL);
13+
}
14+
15+
await new BlogPost({ title: 'test-post' }).save();
16+
const found = await BlogPost.findOne({ title: 'test-post' });
17+
18+
return { title: found?.title };
19+
});

‎dev-packages/e2e-tests/test-applications/nuxt-4/tests/db-drivers.test.ts‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,46 @@ test('Instruments ioredis automatically', async ({ baseURL }) => {
6767
);
6868
});
6969

70+
// The Nitro bundle force-inlines the instrumented drivers while their CommonJS dependencies and
71+
// Node builtins stay external, and every `require()` across that boundary needs working interop.
72+
test('Instruments mongoose automatically', async ({ baseURL }) => {
73+
const spansPromise = collectRequestSpans('/api/db-mongoose');
74+
75+
const response = await fetch(`${baseURL}/api/db-mongoose`);
76+
expect(response.status).toBe(200);
77+
await expect(response.json()).resolves.toEqual({ title: 'test-post' });
78+
79+
const spans = await spansPromise;
80+
81+
const rootSpan = spans.find(span => span.is_segment);
82+
expect(rootSpan).toBeDefined();
83+
expect(getSpanOp(rootSpan!)).toBe('http.server');
84+
85+
const mongooseSpans = spans.filter(
86+
span => span.attributes['sentry.origin']?.value === 'auto.db.mongoose.diagnostic_channel',
87+
);
88+
expect(mongooseSpans).toHaveLength(2);
89+
90+
const saveSpan = mongooseSpans.find(span => span.name === 'save blogposts');
91+
expect(saveSpan?.status).toBe('ok');
92+
expect(saveSpan?.is_segment).toBe(false);
93+
expect(saveSpan?.attributes['sentry.op']).toEqual({ type: 'string', value: 'db' });
94+
expect(saveSpan?.attributes['db.system.name']).toEqual({ type: 'string', value: 'mongodb' });
95+
expect(saveSpan?.attributes['db.namespace']).toEqual({ type: 'string', value: 'test' });
96+
expect(saveSpan?.attributes['db.collection.name']).toEqual({ type: 'string', value: 'blogposts' });
97+
expect(saveSpan?.attributes['db.operation.name']).toEqual({ type: 'string', value: 'save' });
98+
99+
const findOneSpan = mongooseSpans.find(span => span.name === 'findOne blogposts');
100+
expect(findOneSpan?.status).toBe('ok');
101+
expect(findOneSpan?.is_segment).toBe(false);
102+
expect(findOneSpan?.attributes['sentry.op']).toEqual({ type: 'string', value: 'db' });
103+
expect(findOneSpan?.attributes['db.system.name']).toEqual({ type: 'string', value: 'mongodb' });
104+
expect(findOneSpan?.attributes['db.namespace']).toEqual({ type: 'string', value: 'test' });
105+
expect(findOneSpan?.attributes['db.collection.name']).toEqual({ type: 'string', value: 'blogposts' });
106+
expect(findOneSpan?.attributes['db.operation.name']).toEqual({ type: 'string', value: 'findOne' });
107+
expect(findOneSpan?.attributes['db.query.text']).toEqual({ type: 'string', value: '{"title":"?"}' });
108+
});
109+
70110
test('Instruments mysql automatically', async ({ baseURL }) => {
71111
const spansPromise = collectRequestSpans('/api/db-mysql');
72112

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
services:
2+
mongo:
3+
image: mongo:8
4+
restart: always
5+
container_name: e2e-tests-nuxt-5-mongo
6+
ports:
7+
- '27017:27017'
8+
environment:
9+
MONGO_INITDB_ROOT_USERNAME: root
10+
MONGO_INITDB_ROOT_PASSWORD: docker
11+
healthcheck:
12+
test: ['CMD-SHELL', 'mongosh --quiet --eval "db.adminCommand(''ping'').ok"']
13+
interval: 2s
14+
timeout: 3s
15+
retries: 30
16+
start_period: 10s
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { execSync } from 'child_process';
2+
import { dirname } from 'path';
3+
import { fileURLToPath } from 'url';
4+
5+
const __dirname = dirname(fileURLToPath(import.meta.url));
6+
7+
export default async function globalSetup() {
8+
// Start MongoDB via Docker Compose. `--wait` blocks until the
9+
// healthcheck in docker-compose.yml passes, so the app can connect immediately.
10+
execSync('docker compose up -d --wait', {
11+
cwd: __dirname,
12+
stdio: 'inherit',
13+
});
14+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { execSync } from 'child_process';
2+
import { dirname } from 'path';
3+
import { fileURLToPath } from 'url';
4+
5+
const __dirname = dirname(fileURLToPath(import.meta.url));
6+
7+
export default async function globalTeardown() {
8+
execSync('docker compose down --volumes', {
9+
cwd: __dirname,
10+
stdio: 'inherit',
11+
});
12+
}

‎dev-packages/e2e-tests/test-applications/nuxt-5/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"dependencies": {
2828
"@pinia/nuxt": "^0.11.3",
2929
"@sentry/nuxt": "file:../../packed/sentry-nuxt-packed.tgz",
30+
"mongoose": "^9.10.2",
3031
"ofetch": "^2.0.0-alpha.3",
3132
"nitro": "latest",
3233
"nuxt": "npm:nuxt-nightly@5x",
@@ -41,7 +42,6 @@
4142
},
4243
"volta": {
4344
"extends": "../../package.json",
44-
"node": "22.20.0",
4545
"pnpm": "11.10.0"
4646
}
4747
}

‎dev-packages/e2e-tests/test-applications/nuxt-5/playwright.config.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,8 @@ const config = getPlaywrightConfig({
2323
startCommand: getStartCommand(),
2424
});
2525

26-
export default config;
26+
export default {
27+
...config,
28+
globalSetup: './global-setup.mjs',
29+
globalTeardown: './global-teardown.mjs',
30+
};

0 commit comments

Comments
 (0)