Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions dev-packages/e2e-tests/test-applications/nuxt-4/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,22 @@ services:
retries: 30
start_period: 10s

mongo:
image: mongo:8
restart: always
container_name: e2e-tests-nuxt-4-mongo
ports:
- '27017:27017'
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: docker
healthcheck:
test: ['CMD-SHELL', 'mongosh --quiet --eval "db.adminCommand(''ping'').ok"']
interval: 2s
timeout: 3s
retries: 30
start_period: 10s

redis:
image: redis:7
restart: always
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));

export default async function globalSetup() {
// Start MySQL + Redis via Docker Compose. `--wait` blocks until the
// Start MySQL, MongoDB, and Redis via Docker Compose. `--wait` blocks until the
// healthchecks in docker-compose.yml pass, so the app can connect immediately.
execSync('docker compose up -d --wait', {
cwd: __dirname,
Expand Down
4 changes: 2 additions & 2 deletions dev-packages/e2e-tests/test-applications/nuxt-4/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"@pinia/nuxt": "^0.5.5",
"@sentry/nuxt": "file:../../packed/sentry-nuxt-packed.tgz",
"ioredis": "5.10.1",
"mongoose": "^9.10.2",
"mysql": "^2.18.1",
"nuxt": "^4.1.2"
},
Expand All @@ -31,8 +32,7 @@
"@sentry-internal/test-utils": "link:../../../test-utils"
},
"volta": {
"extends": "../../package.json",
"node": "22.20.0"
"extends": "../../package.json"
},
"sentryTest": {
"variants": [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { defineEventHandler } from '#imports';
import mongoose from 'mongoose';

const BlogPost = mongoose.model('BlogPost', new mongoose.Schema({ title: String }));

// SCRAM-SHA-1 on purpose: this handshake lazily `require()`s the `crypto` builtin inside a try
// block, which only works when the bundled driver gets a real interop for builtin requires (#24775).
const MONGO_URL = 'mongodb://root:docker@127.0.0.1:27017/test?authSource=admin&authMechanism=SCRAM-SHA-1';

export default defineEventHandler(async () => {
if (mongoose.connection.readyState !== mongoose.ConnectionStates.connected) {
await mongoose.connect(MONGO_URL);
}

await new BlogPost({ title: 'test-post' }).save();
const found = await BlogPost.findOne({ title: 'test-post' });

return { title: found?.title };
});
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,46 @@ test('Instruments ioredis automatically', async ({ baseURL }) => {
);
});

// The Nitro bundle force-inlines the instrumented drivers while their CommonJS dependencies and
// Node builtins stay external, and every `require()` across that boundary needs working interop.
test('Instruments mongoose automatically', async ({ baseURL }) => {
const spansPromise = collectRequestSpans('/api/db-mongoose');

const response = await fetch(`${baseURL}/api/db-mongoose`);
expect(response.status).toBe(200);
await expect(response.json()).resolves.toEqual({ title: 'test-post' });

const spans = await spansPromise;

const rootSpan = spans.find(span => span.is_segment);
expect(rootSpan).toBeDefined();
expect(getSpanOp(rootSpan!)).toBe('http.server');

const mongooseSpans = spans.filter(
span => span.attributes['sentry.origin']?.value === 'auto.db.mongoose.diagnostic_channel',
);
expect(mongooseSpans).toHaveLength(2);

const saveSpan = mongooseSpans.find(span => span.name === 'save blogposts');
expect(saveSpan?.status).toBe('ok');
expect(saveSpan?.is_segment).toBe(false);
expect(saveSpan?.attributes['sentry.op']).toEqual({ type: 'string', value: 'db' });
expect(saveSpan?.attributes['db.system.name']).toEqual({ type: 'string', value: 'mongodb' });
expect(saveSpan?.attributes['db.namespace']).toEqual({ type: 'string', value: 'test' });
expect(saveSpan?.attributes['db.collection.name']).toEqual({ type: 'string', value: 'blogposts' });
expect(saveSpan?.attributes['db.operation.name']).toEqual({ type: 'string', value: 'save' });

const findOneSpan = mongooseSpans.find(span => span.name === 'findOne blogposts');
expect(findOneSpan?.status).toBe('ok');
expect(findOneSpan?.is_segment).toBe(false);
expect(findOneSpan?.attributes['sentry.op']).toEqual({ type: 'string', value: 'db' });
expect(findOneSpan?.attributes['db.system.name']).toEqual({ type: 'string', value: 'mongodb' });
expect(findOneSpan?.attributes['db.namespace']).toEqual({ type: 'string', value: 'test' });
expect(findOneSpan?.attributes['db.collection.name']).toEqual({ type: 'string', value: 'blogposts' });
expect(findOneSpan?.attributes['db.operation.name']).toEqual({ type: 'string', value: 'findOne' });
expect(findOneSpan?.attributes['db.query.text']).toEqual({ type: 'string', value: '{"title":"?"}' });
});

test('Instruments mysql automatically', async ({ baseURL }) => {
const spansPromise = collectRequestSpans('/api/db-mysql');

Expand Down
16 changes: 16 additions & 0 deletions dev-packages/e2e-tests/test-applications/nuxt-5/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
services:
mongo:
image: mongo:8
restart: always
container_name: e2e-tests-nuxt-5-mongo
ports:
- '27017:27017'
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: docker
healthcheck:
test: ['CMD-SHELL', 'mongosh --quiet --eval "db.adminCommand(''ping'').ok"']
interval: 2s
timeout: 3s
retries: 30
start_period: 10s
14 changes: 14 additions & 0 deletions dev-packages/e2e-tests/test-applications/nuxt-5/global-setup.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { execSync } from 'child_process';
import { dirname } from 'path';
import { fileURLToPath } from 'url';

const __dirname = dirname(fileURLToPath(import.meta.url));

export default async function globalSetup() {
// Start MongoDB via Docker Compose. `--wait` blocks until the
// healthcheck in docker-compose.yml passes, so the app can connect immediately.
execSync('docker compose up -d --wait', {
cwd: __dirname,
stdio: 'inherit',
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { execSync } from 'child_process';
import { dirname } from 'path';
import { fileURLToPath } from 'url';

const __dirname = dirname(fileURLToPath(import.meta.url));

export default async function globalTeardown() {
execSync('docker compose down --volumes', {
cwd: __dirname,
stdio: 'inherit',
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"dependencies": {
"@pinia/nuxt": "^0.11.3",
"@sentry/nuxt": "file:../../packed/sentry-nuxt-packed.tgz",
"mongoose": "^9.10.2",
"ofetch": "^2.0.0-alpha.3",
"nitro": "latest",
"nuxt": "npm:nuxt-nightly@5x",
Expand All @@ -49,7 +50,6 @@
},
"volta": {
"extends": "../../package.json",
"node": "22.20.0",
"pnpm": "11.10.0"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,8 @@ const config = getPlaywrightConfig({
startCommand: getStartCommand(),
});

export default config;
export default {
...config,
globalSetup: './global-setup.mjs',
globalTeardown: './global-teardown.mjs',
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import mongoose from 'mongoose';
import { defineHandler } from 'nitro';

const BlogPost = mongoose.model('BlogPost', new mongoose.Schema({ title: String }));

// SCRAM-SHA-1 on purpose: this handshake lazily `require()`s the `crypto` builtin inside a try
// block, which only works when the bundled driver gets a real interop for builtin requires (#24775).
const MONGO_URL = 'mongodb://root:docker@127.0.0.1:27017/test?authSource=admin&authMechanism=SCRAM-SHA-1';

export default defineHandler(async () => {
if (mongoose.connection.readyState !== mongoose.ConnectionStates.connected) {
await mongoose.connect(MONGO_URL);
}

await new BlogPost({ title: 'test-post' }).save();
const found = await BlogPost.findOne({ title: 'test-post' });

return { title: found?.title };
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { expect, test } from '@playwright/test';
import { collectStreamedSpansUntilSegment } from '@sentry-internal/test-utils';

// The Nitro bundle force-inlines the instrumented drivers while their CommonJS dependencies and
// Node builtins stay external, and every `require()` across that boundary needs working interop
// (#24775). mongoose covers both: `new mquery()`, and SCRAM-SHA-1 auth lazily requiring `crypto`.
async function collectRequestSpans() {
const spans = await collectStreamedSpansUntilSegment(
'nuxt-5',
span => span.attributes['url.path']?.value === '/api/db-mongoose',
);
const rootSpan = spans.find(span => span.is_segment && span.attributes['url.path']?.value === '/api/db-mongoose');

return spans.filter(span => span.trace_id === rootSpan?.trace_id);
}

test('Instruments mongoose automatically', async ({ baseURL }) => {
const spansPromise = collectRequestSpans();

const response = await fetch(`${baseURL}/api/db-mongoose`);
expect(response.status).toBe(200);
await expect(response.json()).resolves.toEqual({ title: 'test-post' });

const spans = await spansPromise;

const mongooseSpans = spans.filter(
span => span.attributes['sentry.origin']?.value === 'auto.db.mongoose.diagnostic_channel',
);
expect(mongooseSpans).toHaveLength(2);

const saveSpan = mongooseSpans.find(span => span.name === 'save blogposts');
expect(saveSpan?.status).toBe('ok');
expect(saveSpan?.is_segment).toBe(false);
expect(saveSpan?.attributes['sentry.op']).toEqual({ type: 'string', value: 'db' });
expect(saveSpan?.attributes['db.system.name']).toEqual({ type: 'string', value: 'mongodb' });
expect(saveSpan?.attributes['db.namespace']).toEqual({ type: 'string', value: 'test' });
expect(saveSpan?.attributes['db.collection.name']).toEqual({ type: 'string', value: 'blogposts' });
expect(saveSpan?.attributes['db.operation.name']).toEqual({ type: 'string', value: 'save' });

const findOneSpan = mongooseSpans.find(span => span.name === 'findOne blogposts');
expect(findOneSpan?.status).toBe('ok');
expect(findOneSpan?.is_segment).toBe(false);
expect(findOneSpan?.attributes['sentry.op']).toEqual({ type: 'string', value: 'db' });
expect(findOneSpan?.attributes['db.system.name']).toEqual({ type: 'string', value: 'mongodb' });
expect(findOneSpan?.attributes['db.namespace']).toEqual({ type: 'string', value: 'test' });
expect(findOneSpan?.attributes['db.collection.name']).toEqual({ type: 'string', value: 'blogposts' });
expect(findOneSpan?.attributes['db.operation.name']).toEqual({ type: 'string', value: 'findOne' });
expect(findOneSpan?.attributes['db.query.text']).toEqual({ type: 'string', value: '{"title":"?"}' });
});
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@
"vitest": "^3.2.7"
},
"volta": {
"node": "20.19.5",
"extends": "../../package.json"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@
"vitest": "^3.2.7"
},
"volta": {
"node": "20.19.5",
"extends": "../../package.json"
}
}
15 changes: 13 additions & 2 deletions packages/nuxt/src/vite/orchestrion.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import type { Nuxt } from '@nuxt/schema';
import { INSTRUMENTED_MODULE_NAMES } from '@sentry/server-utils/orchestrion/config';
import { sentryOrchestrionPlugin } from '@sentry/server-utils/orchestrion/rollup';
import {
commonJSInteropOptions,
sentryCommonJSInteropPlugin,
sentryOrchestrionPlugin,
} from '@sentry/server-utils/orchestrion/rollup';
import type { NitroConfig } from 'nitropack';
import { isCloudflarePreset } from './utils';

Expand Down Expand Up @@ -49,11 +53,18 @@ export function setupOrchestrion(nuxt: Nuxt, hasServerConfig: boolean, buildTime
nitroConfig.rollupConfig.plugins = [nitroConfig.rollupConfig.plugins];
}

nitroConfig.rollupConfig.plugins.push(sentryOrchestrionPlugin({}));
nitroConfig.rollupConfig.plugins.push(sentryOrchestrionPlugin({}), sentryCommonJSInteropPlugin());

const externals = (nitroConfig.externals ||= {});
const inline = externals.inline;
const existingInline = Array.isArray(inline) ? inline : inline ? [inline] : [];
externals.inline = [...new Set([...existingInline, ...INSTRUMENTED_MODULE_NAMES, ...IORedisDependencies])];

// The inlined drivers `require()` CommonJS dependencies that stay external. Fix the interop
// for those requires (see `commonJSInteropOptions`). User-provided options win.
const commonJS = (nitroConfig.commonJS ||= {});
const interop = commonJSInteropOptions();
commonJS.requireReturnsDefault ??= interop.requireReturnsDefault;
commonJS.ignoreTryCatch ??= interop.ignoreTryCatch;
});
}
30 changes: 28 additions & 2 deletions packages/nuxt/test/vite/orchestrion.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import type { Nuxt } from '@nuxt/schema';
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';

const mockSentryOrchestrionPlugin = vi.fn(() => ({ name: 'sentry-orchestrion-plugin' }));
const mockCommonJSInteropOptions = {
requireReturnsDefault: vi.fn(),
ignoreTryCatch: vi.fn(),
};

function createMockNuxt(options: { _prepare?: boolean; dev?: boolean } = {}) {
const hooks: Record<string, Array<(...args: any[]) => void | Promise<void>>> = {};
Expand All @@ -27,6 +31,8 @@ describe('setupOrchestrion', () => {
}));
vi.doMock('@sentry/server-utils/orchestrion/rollup', () => ({
sentryOrchestrionPlugin: mockSentryOrchestrionPlugin,
sentryCommonJSInteropPlugin: () => ({ name: 'sentry-commonjs-interop' }),
commonJSInteropOptions: () => mockCommonJSInteropOptions,
}));
// The module reaches `@sentry/core` and `@nuxt/kit` through `./utils`. Transforming those
// charged the first test, which timed out on slower CI runners. The tests never reset the
Expand Down Expand Up @@ -56,7 +62,11 @@ describe('setupOrchestrion', () => {
await mockNuxt.triggerHook('nitro:config', nitroConfig);

expect(mockSentryOrchestrionPlugin).toHaveBeenCalledOnce();
expect(nitroConfig.rollupConfig.plugins).toEqual([existingPlugin, { name: 'sentry-orchestrion-plugin' }]);
expect(nitroConfig.rollupConfig.plugins).toEqual([
existingPlugin,
{ name: 'sentry-orchestrion-plugin' },
{ name: 'sentry-commonjs-interop' },
]);
expect(nitroConfig.externals.inline).toEqual(['ioredis', 'custom-dependency', 'mysql', 'standard-as-callback']);
});

Expand Down Expand Up @@ -103,8 +113,24 @@ describe('setupOrchestrion', () => {
await mockNuxt.triggerHook('nitro:config', nitroConfig);

expect(nitroConfig).toEqual({
rollupConfig: { plugins: [{ name: 'sentry-orchestrion-plugin' }] },
rollupConfig: { plugins: [{ name: 'sentry-orchestrion-plugin' }, { name: 'sentry-commonjs-interop' }] },
externals: { inline: ['mysql', 'ioredis', 'standard-as-callback'] },
commonJS: mockCommonJSInteropOptions,
});
});

it('preserves user-provided CommonJS options', async () => {
const { setupOrchestrion } = await import('../../src/vite/orchestrion');
const mockNuxt = createMockNuxt();
const userRequireReturnsDefault = vi.fn();
const nitroConfig = { commonJS: { requireReturnsDefault: userRequireReturnsDefault } };

setupOrchestrion(mockNuxt as unknown as Nuxt, true);
await mockNuxt.triggerHook('nitro:config', nitroConfig);

expect(nitroConfig.commonJS).toEqual({
requireReturnsDefault: userRequireReturnsDefault,
ignoreTryCatch: mockCommonJSInteropOptions.ignoreTryCatch,
});
});

Expand Down
Loading
Loading