diff --git a/CHANGELOG.md b/CHANGELOG.md
index 193f6a328043..d41f4ef264dd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,6 +18,10 @@ Work in this release was contributed by @kunal649 and @bardrop. Thank you for yo
HTTP spans that the SDK builds from a `URL` now send `url.scheme` without the trailing colon (`https` instead of `https:`) and `url.port` as a number instead of a string, as the Sentry conventions define them and as Node `http.server` spans already do. This changes `http.server` spans from `@sentry/cloudflare`, `Bun.serve`, Deno and Nitro, and outgoing `node:http` client spans. Update saved queries, alerts and dashboards that filter on `url.scheme:"https:"` or on `url.port` as a string.
+- **fix(nuxt): Name the database system in `db.system.name` of db0 spans ([#26285](https://github.com/getsentry/sentry-javascript/pull/26285))**
+
+ Nitro database (db0) spans now set `db.system.name` to the database system of the connector's dialect (`mysql`, `postgresql` or `sqlite`), as the OpenTelemetry conventions define it, instead of the connector name. For example, spans of the `mysql2` and `planetscale` connectors now report `mysql`, and spans of `better-sqlite3` and `cloudflare-d1` now report `sqlite`. Update saved queries, alerts and dashboards that filter on the connector names.
+
## 11.12.0
### Important Changes
diff --git a/dev-packages/e2e-tests/README.md b/dev-packages/e2e-tests/README.md
index 52e30cf48569..67aeced12b3d 100644
--- a/dev-packages/e2e-tests/README.md
+++ b/dev-packages/e2e-tests/README.md
@@ -175,13 +175,21 @@ is the reference setup.
values that come from its `init()` (`sdk.name`, default integrations) are Node-only. Next.js is different: it loads
`instrumentation.ts` on every runtime, so `nextjs-16` calls `init()` of `@sentry/nextjs` on Bun and Deno too. Its
Bun and Deno variants build the app on that runtime, so `@sentry/nextjs` loads its `bun` or `deno` entry, which adds
- the integrations of `@sentry/bun` or `@sentry/deno`.
+ the integrations of `@sentry/bun` or `@sentry/deno`. Nuxt is similar: `@sentry/nuxt` bundles `sentry.server.config.ts`
+ into the Nitro output, so `nuxt-4` calls `init()` of `@sentry/nuxt` on Bun and Deno too. `sentry.server.config.bun.ts`
+ and `sentry.server.config.deno.ts` add the integrations of `@sentry/bun` or `@sentry/deno`. On Cloudflare, `nuxt-4`
+ builds with the `cloudflare_module` preset and sets up the SDK with `sentryCloudflareNitroPlugin`, which
+ `nuxt.config.ts` adds only for that runtime. Like the documented Cloudflare setup, that build has no
+ `sentry.server.config.ts`: a runtime-specific file can replace a file but not remove it, so the build command deletes
+ it. Nitro turns every file under `server/api` into a route, so
+ `nuxt.config.ts` lists the runtime-specific files there (such as `db-mongoose.cloudflare.ts`) in Nitro's `ignore`.
- **Start commands**: `playwright.config.mjs` selects the start command with `getRuntime()`. Bun and Deno use the same build
as Node and only change the start command, for example
`bun --bun --preload ./instrument.bun.mjs ./node_modules/@react-router/serve/bin.cjs ./build/server/index.js` and
`deno run -A --preload=@sentry/deno/import --preload=./instrument.deno.mjs ./node_modules/@react-router/serve/bin.cjs ./build/server/index.js`.
The app's `deno.json` maps `@sentry/deno/import` to `node_modules`, because the e2e dependencies are `file:` tarballs
- that `npm:@sentry/deno/import` does not resolve.
+ that `npm:@sentry/deno/import` does not resolve. `nuxt-4` starts the Nitro output with no preload:
+ `bun .output/server/index.mjs` and `deno run -A .output/server/index.mjs`.
- **Cloudflare**: the app has the Cloudflare dependencies installed all the time. The Cloudflare build has its own
`vite.cloudflare.config.ts` with `@cloudflare/vite-plugin` and `sentryCloudflareVitePlugin` from
`@sentry/cloudflare/vite`, which the runner uses in place of `vite.config.ts` (see runtime-specific files). List the
@@ -213,7 +221,8 @@ is the reference setup.
example Express behind `react-router-serve`), so those produce no spans on Bun. Where a test depends on them, branch
on `RUNTIME` and say why in a comment. An app that starts its own server can instead bundle it with
`@sentry/bun/plugin`, as `nestjs-12/bun-build.mjs` does. Bun 1.3 renames classes in bundles (`class Foo2`), which
- changes span names taken from class names, so `nestjs-12` pins Bun 1.4 (see #26289).
+ changes span names taken from class names, so `nestjs-12` pins Bun 1.4 (see #26289). `nuxt-4` builds with
+ `bun --bun nuxt build` on Bun, because the Nitro prerenderer loads its `bun-sqlite` connector in the build process.
```json
"sentryTest": {
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4-cloudflare/app/app.vue b/dev-packages/e2e-tests/test-applications/nuxt-4-cloudflare/app/app.vue
deleted file mode 100644
index 826fed9086a1..000000000000
--- a/dev-packages/e2e-tests/test-applications/nuxt-4-cloudflare/app/app.vue
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
Nuxt on Cloudflare
-
-
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4-cloudflare/docker-compose.yml b/dev-packages/e2e-tests/test-applications/nuxt-4-cloudflare/docker-compose.yml
deleted file mode 100644
index f667d39fb2f0..000000000000
--- a/dev-packages/e2e-tests/test-applications/nuxt-4-cloudflare/docker-compose.yml
+++ /dev/null
@@ -1,18 +0,0 @@
-services:
- db:
- image: mysql:8.0
- restart: always
- container_name: e2e-tests-nuxt-4-cloudflare-mysql
- # The `mysql` 2.x driver doesn't speak MySQL 8's default
- # `caching_sha2_password` auth, so force the legacy plugin.
- command: ['--default-authentication-plugin=mysql_native_password']
- ports:
- - '3306:3306'
- environment:
- MYSQL_ROOT_PASSWORD: docker
- healthcheck:
- test: ['CMD-SHELL', 'mysqladmin ping -h 127.0.0.1 -uroot -pdocker']
- interval: 2s
- timeout: 3s
- retries: 30
- start_period: 10s
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4-cloudflare/global-setup.mjs b/dev-packages/e2e-tests/test-applications/nuxt-4-cloudflare/global-setup.mjs
deleted file mode 100644
index 9ba25cd71638..000000000000
--- a/dev-packages/e2e-tests/test-applications/nuxt-4-cloudflare/global-setup.mjs
+++ /dev/null
@@ -1,14 +0,0 @@
-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 MySQL via Docker Compose. `--wait` blocks until the healthcheck in
- // docker-compose.yml passes, so the worker can connect on the first request.
- execSync('docker compose up -d --wait', {
- cwd: __dirname,
- stdio: 'inherit',
- });
-}
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4-cloudflare/global-teardown.mjs b/dev-packages/e2e-tests/test-applications/nuxt-4-cloudflare/global-teardown.mjs
deleted file mode 100644
index 2742279431ad..000000000000
--- a/dev-packages/e2e-tests/test-applications/nuxt-4-cloudflare/global-teardown.mjs
+++ /dev/null
@@ -1,12 +0,0 @@
-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',
- });
-}
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4-cloudflare/nuxt.config.ts b/dev-packages/e2e-tests/test-applications/nuxt-4-cloudflare/nuxt.config.ts
deleted file mode 100644
index b83483ac0363..000000000000
--- a/dev-packages/e2e-tests/test-applications/nuxt-4-cloudflare/nuxt.config.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-// https://nuxt.com/docs/api/configuration/nuxt-config
-export default defineNuxtConfig({
- compatibilityDate: '2025-06-06',
- modules: ['@sentry/nuxt/module'],
- // The tunnel variant serves the app, and so the tunnel, under a base URL.
- ...(process.env.E2E_TEST_TUNNEL && { app: { baseURL: '/app/' } }),
- nitro: {
- preset: 'cloudflare_module',
- cloudflare: {
- nodeCompat: true,
- deployConfig: false,
- },
- // The bundled `mysql` driver pulls in `readable-stream`, whose base `require('stream')` has no
- // usable prototype under Nitro's unenv polyfill (throws `superCtor.prototype ... undefined`).
- // Alias it to workerd's native `node:stream`, which has a real `Readable`.
- alias: {
- 'readable-stream': 'node:stream',
- },
- // The tunnel variant serves the managed tunnel.
- replace: {
- __E2E_TEST_TUNNEL__: JSON.stringify(!!process.env.E2E_TEST_TUNNEL),
- },
- },
-});
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4-cloudflare/package.json b/dev-packages/e2e-tests/test-applications/nuxt-4-cloudflare/package.json
deleted file mode 100644
index 7fb1258723d6..000000000000
--- a/dev-packages/e2e-tests/test-applications/nuxt-4-cloudflare/package.json
+++ /dev/null
@@ -1,35 +0,0 @@
-{
- "name": "nuxt-4-cloudflare",
- "type": "module",
- "private": true,
- "scripts": {
- "build": "nuxt build",
- "dev": "nuxt dev",
- "preview": "wrangler dev --var \"E2E_TEST_DSN:$E2E_TEST_DSN\" --port 3030 --log-level=$(test $CI && echo 'none' || echo 'log')",
- "test": "playwright test",
- "test:build": "pnpm install && pnpm build",
- "test:assert": "pnpm test",
- "test:build:tunnel": "pnpm install && E2E_TEST_TUNNEL=1 pnpm build",
- "test:assert:tunnel": "E2E_TEST_TUNNEL=1 pnpm test tunnel"
- },
- "dependencies": {
- "@sentry/nuxt": "file:../../packed/sentry-nuxt-packed.tgz",
- "@sentry/cloudflare": "file:../../packed/sentry-cloudflare-packed.tgz",
- "mysql": "2.18.1",
- "nuxt": "^4.1.2"
- },
- "devDependencies": {
- "@playwright/test": "1.64.1",
- "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz",
- "wrangler": "^4.72.0"
- },
- "sentryTest": {
- "variants": [
- {
- "build-command": "pnpm test:build:tunnel",
- "assert-command": "pnpm test:assert:tunnel",
- "label": "nuxt-4-cloudflare (tunnel)"
- }
- ]
- }
-}
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4-cloudflare/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/nuxt-4-cloudflare/playwright.config.mjs
deleted file mode 100644
index a1e9207cdfa1..000000000000
--- a/dev-packages/e2e-tests/test-applications/nuxt-4-cloudflare/playwright.config.mjs
+++ /dev/null
@@ -1,17 +0,0 @@
-import { getPlaywrightConfig } from '@sentry-internal/test-utils';
-
-// `nuxt build` (where the Sentry Nuxt module's orchestrion transform runs over Nitro's Cloudflare
-// preset) produces the worker; `pnpm preview` (`wrangler dev`) serves it. `globalSetup` spins up the
-// MySQL container the worker connects to.
-const config = getPlaywrightConfig(
- {
- startCommand: 'pnpm preview',
- port: 3030,
- },
- {
- globalSetup: './global-setup.mjs',
- globalTeardown: './global-teardown.mjs',
- },
-);
-
-export default config;
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4-cloudflare/pnpm-workspace.yaml b/dev-packages/e2e-tests/test-applications/nuxt-4-cloudflare/pnpm-workspace.yaml
deleted file mode 100644
index fd29a82085e0..000000000000
--- a/dev-packages/e2e-tests/test-applications/nuxt-4-cloudflare/pnpm-workspace.yaml
+++ /dev/null
@@ -1,4 +0,0 @@
-allowBuilds:
- esbuild: true
- workerd: true
-minimumReleaseAge: 0
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4-cloudflare/server/api/anchor.ts b/dev-packages/e2e-tests/test-applications/nuxt-4-cloudflare/server/api/anchor.ts
deleted file mode 100644
index cee7601e363b..000000000000
--- a/dev-packages/e2e-tests/test-applications/nuxt-4-cloudflare/server/api/anchor.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-import { defineEventHandler } from '#imports';
-
-// A plain route for tests to request after the tunnel traffic.
-export default defineEventHandler(() => 'ok');
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4-cloudflare/server/api/db-mysql.ts b/dev-packages/e2e-tests/test-applications/nuxt-4-cloudflare/server/api/db-mysql.ts
deleted file mode 100644
index 0045cb7576d3..000000000000
--- a/dev-packages/e2e-tests/test-applications/nuxt-4-cloudflare/server/api/db-mysql.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import { defineEventHandler } from '#imports';
-import mysql from 'mysql';
-
-export default defineEventHandler(() => {
- const connection = mysql.createConnection({
- host: '127.0.0.1',
- port: 3306,
- user: 'root',
- password: 'docker',
- });
-
- connection.on('error', () => {
- // no-op
- });
-
- return new Promise((resolve, reject) => {
- connection.query('SELECT 1 + 1 AS solution', error => {
- if (error) {
- connection.end();
- reject(error);
- return;
- }
-
- connection.query('SELECT NOW()', nestedError => {
- connection.end();
- if (nestedError) {
- reject(nestedError);
- return;
- }
-
- resolve({ status: 'ok' });
- });
- });
- });
-});
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4-cloudflare/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/nuxt-4-cloudflare/start-event-proxy.mjs
deleted file mode 100644
index 5d537fb7c877..000000000000
--- a/dev-packages/e2e-tests/test-applications/nuxt-4-cloudflare/start-event-proxy.mjs
+++ /dev/null
@@ -1,6 +0,0 @@
-import { startEventProxyServer } from '@sentry-internal/test-utils';
-
-startEventProxyServer({
- port: 3031,
- proxyServerName: 'nuxt-4-cloudflare',
-});
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4-cloudflare/tests/db.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4-cloudflare/tests/db.test.ts
deleted file mode 100644
index 62bfdb837e53..000000000000
--- a/dev-packages/e2e-tests/test-applications/nuxt-4-cloudflare/tests/db.test.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-import { expect, test } from '@playwright/test';
-import { collectStreamedSpans, getSpanOp } from '@sentry-internal/test-utils';
-
-// Streamed child spans arrive across several envelopes, so collect until the request's segment
-// span and the expected db spans have all been flushed. The streamed segment name is method-only
-// (`GET`), so the segment is selected via `url.path`.
-function collectDbSpans(minDbSpans: number) {
- return collectStreamedSpans(
- 'nuxt-4-cloudflare',
- spans =>
- spans.some(span => span.is_segment && span.attributes['url.path']?.value === '/api/db-mysql') &&
- spans.filter(span => getSpanOp(span) === 'db').length >= minDbSpans,
- );
-}
-
-test('a real mysql query emits a db span with orchestrion-channel attributes', async ({ request }) => {
- const spansPromise = collectDbSpans(1);
-
- const res = await request.get('/api/db-mysql');
- expect(res.status()).toBe(200);
-
- const spans = await spansPromise;
- const rootSpan = spans.find(span => span.is_segment && span.attributes['url.path']?.value === '/api/db-mysql');
- expect(rootSpan).toBeDefined();
- expect(getSpanOp(rootSpan!)).toBe('http.server');
-
- const dbSpans = spans.filter(span => getSpanOp(span) === 'db' && span.trace_id === rootSpan!.trace_id);
-
- const firstQuery = dbSpans.find(span => span.attributes['db.query.text']?.value === 'SELECT ? + ? AS solution');
- expect(firstQuery).toBeDefined();
- expect(firstQuery!.name).toBe('SELECT');
- expect(firstQuery!.attributes).toMatchObject({
- 'sentry.origin': { type: 'string', value: 'auto.db.mysql' },
- 'db.system.name': { type: 'string', value: 'mysql' },
- 'db.query.text': { type: 'string', value: 'SELECT ? + ? AS solution' },
- 'server.address': { type: 'string', value: '127.0.0.1' },
- 'server.port': { type: 'integer', value: 3306 },
- 'db.user': { type: 'string', value: 'root' },
- });
-});
-
-test('a nested query lands on the same trace (async context restored)', async ({ request }) => {
- const spansPromise = collectDbSpans(2);
-
- const res = await request.get('/api/db-mysql');
- expect(res.status()).toBe(200);
-
- const spans = await spansPromise;
- const rootSpan = spans.find(span => span.is_segment && span.attributes['url.path']?.value === '/api/db-mysql');
- expect(rootSpan).toBeDefined();
-
- const queryTexts = spans
- .filter(span => getSpanOp(span) === 'db' && span.trace_id === rootSpan!.trace_id)
- .map(span => span.attributes['db.query.text']?.value);
- expect(queryTexts).toContain('SELECT ? + ? AS solution');
- expect(queryTexts).toContain('SELECT NOW()');
-});
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4-cloudflare/tests/tunnel.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4-cloudflare/tests/tunnel.test.ts
deleted file mode 100644
index 33c00290d888..000000000000
--- a/dev-packages/e2e-tests/test-applications/nuxt-4-cloudflare/tests/tunnel.test.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import { expect, test } from '@playwright/test';
-import { isServerSpanAt, makeClientReportEnvelope, watchForTunnelSpan } from '@sentry-internal/test-utils';
-
-test.skip(!process.env.E2E_TEST_TUNNEL, 'Only runs in the tunnel variant');
-
-// The isolate's first tunnel request is the one a late span filter would let through, so this stays the only test.
-test('answers the managed tunnel set up by `sentryCloudflareNitroPlugin`, without a span of it', async ({
- request,
-}) => {
- const dsn = 'http://public@localhost:3031/1337';
- const raceTunnelSpan = watchForTunnelSpan('nuxt-4-cloudflare', isServerSpanAt(/^\/app\/qpo967/));
-
- // The path the SDK derives from `dsn`.
- const response = await request.post('/app/qpo967', {
- data: makeClientReportEnvelope(dsn),
- headers: { 'Content-Type': 'application/x-sentry-envelope' },
- });
-
- expect(response.status()).toBe(200);
- expect(await raceTunnelSpan(isServerSpanAt('/app/api/anchor'), () => request.get('/app/api/anchor'))).toBe('anchor');
-});
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4-cloudflare/tsconfig.json b/dev-packages/e2e-tests/test-applications/nuxt-4-cloudflare/tsconfig.json
deleted file mode 100644
index a746f2a70c28..000000000000
--- a/dev-packages/e2e-tests/test-applications/nuxt-4-cloudflare/tsconfig.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- // https://nuxt.com/docs/guide/concepts/typescript
- "extends": "./.nuxt/tsconfig.json"
-}
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4-cloudflare/wrangler.jsonc b/dev-packages/e2e-tests/test-applications/nuxt-4-cloudflare/wrangler.jsonc
deleted file mode 100644
index 9f9e885fa782..000000000000
--- a/dev-packages/e2e-tests/test-applications/nuxt-4-cloudflare/wrangler.jsonc
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "$schema": "node_modules/wrangler/config-schema.json",
- "name": "nuxt-4-cloudflare",
- "main": "./.output/server/index.mjs",
- "compatibility_date": "2026-06-29",
- "compatibility_flags": ["nodejs_compat"],
- "assets": {
- "directory": "./.output/public",
- "binding": "ASSETS",
- },
-}
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4-cloudflare/server/plugins/sentry.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/cloudflare/sentry.ts
similarity index 62%
rename from dev-packages/e2e-tests/test-applications/nuxt-4-cloudflare/server/plugins/sentry.ts
rename to dev-packages/e2e-tests/test-applications/nuxt-4/cloudflare/sentry.ts
index e4cc28eaf3ae..3d620c3a875a 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-4-cloudflare/server/plugins/sentry.ts
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/cloudflare/sentry.ts
@@ -6,10 +6,14 @@ declare const __E2E_TEST_TUNNEL__: boolean;
export default defineNitroPlugin(
sentryCloudflareNitroPlugin({
- // The tunnel forwards to the DSN host, which is the event proxy in the tunnel variant.
dsn: __E2E_TEST_TUNNEL__ ? 'http://public@localhost:3031/1337' : 'https://public@dsn.ingest.sentry.io/1337',
tracesSampleRate: 1.0,
+ enableLogs: true,
tunnel: 'http://localhost:3031/', // proxy server
- integrations: __E2E_TEST_TUNNEL__ ? [serveTunnelIntegration()] : [],
+ // Several tests trigger the same server error one after another, which Dedupe would drop.
+ integrations: integrations => [
+ ...integrations.filter(integration => integration.name !== 'Dedupe'),
+ ...(__E2E_TEST_TUNNEL__ ? [serveTunnelIntegration()] : []),
+ ],
}),
);
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/instrument-preload.bun.mjs b/dev-packages/e2e-tests/test-applications/nuxt-4/instrument-preload.bun.mjs
new file mode 100644
index 000000000000..fd33b1df4164
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/instrument-preload.bun.mjs
@@ -0,0 +1,11 @@
+// Simulates a v10-style `bun --preload` that fully initializes the SDK
+// before the config bundled into the server build runs its own `Sentry.init`.
+import { bunHttpServerIntegration } from '@sentry/bun';
+import * as Sentry from '@sentry/nuxt';
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ tracesSampleRate: 1.0,
+ tunnel: 'http://localhost:3031/',
+ integrations: [bunHttpServerIntegration()],
+});
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/nuxt.config.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/nuxt.config.ts
index 272aaf5061c7..1ede33ec99e4 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-4/nuxt.config.ts
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/nuxt.config.ts
@@ -1,5 +1,16 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
const tunnelVariant = !!process.env.E2E_TEST_TUNNEL;
+// The runtime variants set `RUNTIME` for the build, see dev-packages/e2e-tests/README.md.
+const runtime = process.env.RUNTIME || 'node';
+const cloudflare = runtime === 'cloudflare';
+
+// `node:sqlite` is not available on Bun, which has its own SQLite module, and on Cloudflare, which has D1.
+function database(name: string, binding: string) {
+ if (cloudflare) {
+ return { connector: 'cloudflare-d1', options: { bindingName: binding } };
+ }
+ return { connector: runtime === 'bun' ? 'bun-sqlite' : 'sqlite', options: { name } };
+}
export default defineNuxtConfig({
compatibilityDate: '2025-06-06',
@@ -31,22 +42,28 @@ export default defineNuxtConfig({
},
},
nitro: {
+ ...(cloudflare && {
+ preset: 'cloudflare_module',
+ cloudflare: { nodeCompat: true, deployConfig: false },
+ // Sets up the SDK on Cloudflare, where the Nitro preset does not use `sentry.server.config.ts`.
+ plugins: ['~~/cloudflare/sentry.ts'],
+ // The bundled `mysql` driver pulls in `readable-stream`, whose base `require('stream')` has no
+ // usable prototype under Nitro's unenv polyfill (throws `superCtor.prototype ... undefined`).
+ // Alias it to workerd's native `node:stream`, which has a real `Readable`.
+ alias: { 'readable-stream': 'node:stream' },
+ }),
+ // Files for one runtime replace their counterpart in the copy of the app for that runtime, see
+ // dev-packages/e2e-tests/README.md, and are no routes of their own.
+ ignore: ['**/*.cloudflare.ts'],
+ // A Worker has no access to the env vars of the test run, so the tunnel variant is set at build time.
+ replace: { __E2E_TEST_TUNNEL__: JSON.stringify(tunnelVariant) },
experimental: {
database: true,
},
database: {
- default: {
- connector: 'sqlite',
- options: { name: 'db' },
- },
- users: {
- connector: 'sqlite',
- options: { name: 'users_db' },
- },
- analytics: {
- connector: 'sqlite',
- options: { name: 'analytics_db' },
- },
+ default: database('db', 'DB'),
+ users: database('users_db', 'USERS_DB'),
+ analytics: database('analytics_db', 'ANALYTICS_DB'),
},
storage: {
'test-storage': {
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/package.json b/dev-packages/e2e-tests/test-applications/nuxt-4/package.json
index a1cd9f3a8596..f1c6d3f6ac46 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-4/package.json
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/package.json
@@ -8,6 +8,9 @@
"generate": "nuxt generate",
"preview": "nuxt preview",
"start": "node .output/server/index.mjs",
+ "start:bun": "bun .output/server/index.mjs",
+ "start:deno": "deno run -A .output/server/index.mjs",
+ "start:cloudflare": "wrangler dev --port 3030",
"start:import": "node --import ./.output/server/sentry.server.config.mjs .output/server/index.mjs",
"clean": "npx nuxi cleanup",
"test": "playwright test",
@@ -18,12 +21,17 @@
"test:assert": "pnpm test:prod && pnpm test:dev",
"test:prod:import": "TEST_ENV=production-import playwright test",
"test:build:tunnel-basepath": "pnpm install && E2E_TEST_TUNNEL=1 E2E_TEST_BASE_PATH=/app node ./scripts/build-with-prerender-event-sink.mjs",
+ "test:build:cloudflare": "rm sentry.server.config.ts && pnpm test:build",
+ "test:build:tunnel-basepath-cloudflare": "rm sentry.server.config.ts && pnpm test:build:tunnel-basepath",
"test:assert:tunnel-basepath": "E2E_TEST_TUNNEL=1 E2E_TEST_BASE_PATH=/app TEST_ENV=production playwright test tunnel",
"test:assert:tunnel-basepath-dev": "E2E_TEST_TUNNEL=1 E2E_TEST_BASE_PATH=/app TEST_ENV=development playwright test tunnel"
},
"//": "Pin ioredis to 5.10.1 because that's the last version before it publishes its own diagnostics channels",
"dependencies": {
"@pinia/nuxt": "^0.5.5",
+ "@sentry/bun": "file:../../packed/sentry-bun-packed.tgz",
+ "@sentry/cloudflare": "file:../../packed/sentry-cloudflare-packed.tgz",
+ "@sentry/deno": "file:../../packed/sentry-deno-packed.tgz",
"@sentry/nuxt": "file:../../packed/sentry-nuxt-packed.tgz",
"ioredis": "5.10.1",
"mongoose": "^9.10.2",
@@ -33,7 +41,8 @@
"devDependencies": {
"@playwright/test": "1.64.1",
"@sentry/conventions": "0.32.0",
- "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz"
+ "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz",
+ "wrangler": "^4.72.0"
},
"sentryTest": {
"variants": [
@@ -61,6 +70,31 @@
"build-command": "pnpm install",
"assert-command": "pnpm test:assert:tunnel-basepath-dev",
"label": "nuxt-4 (tunnel-basepath-dev)"
+ },
+ {
+ "runtime": "bun",
+ "assert-command": "pnpm test:prod",
+ "label": "nuxt-4 (bun)"
+ },
+ {
+ "runtime": "deno",
+ "deno-version": "v2.9.0",
+ "assert-command": "pnpm test:prod",
+ "label": "nuxt-4 (deno)"
+ },
+ {
+ "runtime": "cloudflare",
+ "build-command": "pnpm test:build:cloudflare",
+ "assert-command": "pnpm test:prod",
+ "label": "nuxt-4 (cloudflare)"
+ }
+ ],
+ "optionalVariants": [
+ {
+ "runtime": "cloudflare",
+ "build-command": "pnpm test:build:tunnel-basepath-cloudflare",
+ "assert-command": "pnpm test:assert:tunnel-basepath",
+ "label": "nuxt-4 (tunnel-basepath-cloudflare)"
}
],
"canaryVariants": [
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/playwright.config.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/playwright.config.ts
index bc80fed9ac7d..bebffa04b20c 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-4/playwright.config.ts
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/playwright.config.ts
@@ -1,4 +1,4 @@
-import { getPlaywrightConfig } from '@sentry-internal/test-utils';
+import { getPlaywrightConfig, getRuntime } from '@sentry-internal/test-utils';
const testEnv = process.env.TEST_ENV;
@@ -6,6 +6,14 @@ if (!testEnv) {
throw new Error('No test env defined');
}
+// The server config is bundled into the Nitro output, so every runtime starts the server entry without a preload.
+const productionStartCommands = {
+ node: 'pnpm start',
+ bun: 'pnpm start:bun',
+ deno: 'pnpm start:deno',
+ cloudflare: 'pnpm start:cloudflare',
+};
+
const getStartCommand = () => {
if (testEnv === 'development') {
// The Sentry server config is bundled into the dev server via a nitro plugin, so no preload is needed.
@@ -13,7 +21,7 @@ const getStartCommand = () => {
}
if (testEnv === 'production') {
- return 'pnpm start';
+ return productionStartCommands[getRuntime()];
}
// Runs the suite with the compat shim preloaded, like existing `--import` deploy commands do.
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/pnpm-workspace.yaml b/dev-packages/e2e-tests/test-applications/nuxt-4/pnpm-workspace.yaml
index 8525919fe522..524e15b28e94 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-4/pnpm-workspace.yaml
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/pnpm-workspace.yaml
@@ -1,4 +1,5 @@
allowBuilds:
esbuild: true
vue-demi: true
+ workerd: true
minimumReleaseAge: 0
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/scripts/build-with-prerender-event-sink.mjs b/dev-packages/e2e-tests/test-applications/nuxt-4/scripts/build-with-prerender-event-sink.mjs
index 421bfe312c74..ad554b2bb70f 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-4/scripts/build-with-prerender-event-sink.mjs
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/scripts/build-with-prerender-event-sink.mjs
@@ -18,7 +18,10 @@ const sinkAvailable = await new Promise(resolve => {
sink.listen(3031, () => resolve(true));
});
-const build = spawn('nuxt', ['build'], { stdio: 'inherit', shell: true });
+// The prerenderer runs the server bundle in the build process, and the Bun build has to run on Bun to load the
+// `bun-sqlite` connector there.
+const nuxt = process.env.RUNTIME === 'bun' ? 'bun --bun nuxt' : 'nuxt';
+const build = spawn(nuxt, ['build'], { stdio: 'inherit', shell: true });
const exitCode = await new Promise(resolve => build.on('exit', resolve));
sink.close();
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/sentry.server.config.bun.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/sentry.server.config.bun.ts
new file mode 100644
index 000000000000..b18678256211
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/sentry.server.config.bun.ts
@@ -0,0 +1,18 @@
+import { bunHttpServerIntegration, fetchIntegration } from '@sentry/bun';
+import * as Sentry from '@sentry/nuxt';
+
+Sentry.init({
+ dsn: process.env.E2E_TEST_TUNNEL ? 'http://public@localhost:3031/1337' : 'https://public@dsn.ingest.sentry.io/1337',
+ tracesSampleRate: 1.0, // Capture 100% of the transactions
+ enableLogs: true,
+ tunnel: 'http://localhost:3031/', // proxy server
+ // Several tests trigger the same server error one after another, which Dedupe would drop.
+ integrations: integrations => [
+ ...integrations.filter(integration => integration.name !== 'Dedupe'),
+ // Bun emits neither the `node:http` diagnostics channel nor the undici channels the Node SDK listens to, so these
+ // replace its incoming request and outgoing `fetch` instrumentation.
+ bunHttpServerIntegration(),
+ fetchIntegration(),
+ ...(process.env.E2E_TEST_TUNNEL ? [Sentry.serveTunnelIntegration()] : []),
+ ],
+});
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/sentry.server.config.deno.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/sentry.server.config.deno.ts
new file mode 100644
index 000000000000..2e32b70cb59d
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/sentry.server.config.deno.ts
@@ -0,0 +1,16 @@
+import { fetchIntegration } from '@sentry/deno';
+import * as Sentry from '@sentry/nuxt';
+
+Sentry.init({
+ dsn: process.env.E2E_TEST_TUNNEL ? 'http://public@localhost:3031/1337' : 'https://public@dsn.ingest.sentry.io/1337',
+ tracesSampleRate: 1.0, // Capture 100% of the transactions
+ enableLogs: true,
+ tunnel: 'http://localhost:3031/', // proxy server
+ // Several tests trigger the same server error one after another, which Dedupe would drop.
+ integrations: integrations => [
+ ...integrations.filter(integration => integration.name !== 'Dedupe'),
+ // The `fetch` of Deno is not undici, so the Node SDK does not see outgoing requests.
+ fetchIntegration(),
+ ...(process.env.E2E_TEST_TUNNEL ? [Sentry.serveTunnelIntegration()] : []),
+ ],
+});
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/sentry.server.config.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/sentry.server.config.ts
index c956224de1fc..95c65c41549f 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-4/sentry.server.config.ts
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/sentry.server.config.ts
@@ -3,6 +3,7 @@ import * as Sentry from '@sentry/nuxt';
Sentry.init({
dsn: process.env.E2E_TEST_TUNNEL ? 'http://public@localhost:3031/1337' : 'https://public@dsn.ingest.sentry.io/1337',
tracesSampleRate: 1.0, // Capture 100% of the transactions
+ enableLogs: true,
tunnel: 'http://localhost:3031/', // proxy server
// Opt into the Sentry OpenTelemetry tracer provider in the "(tracer provider)" e2e variant.
// Leaving it `undefined` otherwise keeps the SDK's default (no provider).
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/db-mongoose.cloudflare.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/db-mongoose.cloudflare.ts
new file mode 100644
index 000000000000..163da48c85d9
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/db-mongoose.cloudflare.ts
@@ -0,0 +1,6 @@
+import { createError, defineEventHandler } from '#imports';
+
+// Replaces `db-mongoose.ts` on Cloudflare, see `MONGOOSE_UNSUPPORTED` in `tests/runtime.ts`.
+export default defineEventHandler(() => {
+ throw createError({ statusCode: 501 });
+});
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/db-test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/db-test.ts
index 4fa90c6e005d..e9487afa9800 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/db-test.ts
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/db-test.ts
@@ -16,10 +16,10 @@ export default defineEventHandler(async event => {
case 'prepare-all': {
await db.exec('CREATE TABLE IF NOT EXISTS products (id INTEGER PRIMARY KEY, name TEXT, price REAL)');
- await db.exec(`INSERT OR REPLACE INTO products (id, name, price) VALUES
- (1, 'Product A', 10.99),
- (2, 'Product B', 20.50),
- (3, 'Product C', 15.25)`);
+ // One line, because D1 runs every line of `exec` as a statement of its own.
+ await db.exec(
+ `INSERT OR REPLACE INTO products (id, name, price) VALUES (1, 'Product A', 10.99), (2, 'Product B', 20.50), (3, 'Product C', 15.25)`,
+ );
const stmt = db.prepare('SELECT * FROM products WHERE price > ?');
const results = await stmt.all(10);
return { success: true, count: results.length, results };
@@ -34,10 +34,9 @@ export default defineEventHandler(async event => {
case 'prepare-bind': {
await db.exec('CREATE TABLE IF NOT EXISTS items (id INTEGER PRIMARY KEY, category TEXT, value INTEGER)');
- await db.exec(`INSERT OR REPLACE INTO items (id, category, value) VALUES
- (1, 'electronics', 100),
- (2, 'books', 50),
- (3, 'electronics', 200)`);
+ await db.exec(
+ `INSERT OR REPLACE INTO items (id, category, value) VALUES (1, 'electronics', 100), (2, 'books', 50), (3, 'electronics', 200)`,
+ );
const stmt = db.prepare('SELECT * FROM items WHERE category = ?');
const boundStmt = stmt.bind('electronics');
const results = await boundStmt.all();
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/echo-headers.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/echo-headers.ts
new file mode 100644
index 000000000000..6474ada63d5f
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/echo-headers.ts
@@ -0,0 +1,6 @@
+import { defineEventHandler, getHeader } from '#imports';
+
+export default defineEventHandler(event => ({
+ 'sentry-trace': getHeader(event, 'sentry-trace'),
+ baggage: getHeader(event, 'baggage'),
+}));
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/isolation.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/isolation.ts
new file mode 100644
index 000000000000..3748637cc023
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/isolation.ts
@@ -0,0 +1,13 @@
+import * as Sentry from '@sentry/nuxt';
+import { defineEventHandler, getQuery } from '#imports';
+
+export default defineEventHandler(async event => {
+ const tag = String(getQuery(event).tag);
+ Sentry.setTag('isolation-tag', tag);
+
+ // Lets the concurrent requests of the test set their tags before any of them captures its event.
+ await new Promise(resolve => setTimeout(resolve, 200));
+
+ Sentry.captureMessage(`isolation ${tag}`);
+ return { tag };
+});
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/log.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/log.ts
new file mode 100644
index 000000000000..10951fde0abe
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/log.ts
@@ -0,0 +1,7 @@
+import * as Sentry from '@sentry/nuxt';
+import { defineEventHandler } from '#imports';
+
+export default defineEventHandler(() => {
+ Sentry.logger.info('nuxt-4 server log', { 'e2e.attr': 'value' });
+ return { ok: true };
+});
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/outgoing-fetch.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/outgoing-fetch.ts
new file mode 100644
index 000000000000..c947d95e5017
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/outgoing-fetch.ts
@@ -0,0 +1,7 @@
+import { defineEventHandler, getRequestURL } from '#imports';
+
+export default defineEventHandler(async event => {
+ // The runtime's own `fetch`, not Nitro's `$fetch`, which calls a local route without a request.
+ const response = await fetch(new URL('/api/echo-headers', getRequestURL(event).origin));
+ return response.json();
+});
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4-cloudflare/server/api/post-error.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/post-error.ts
similarity index 100%
rename from dev-packages/e2e-tests/test-applications/nuxt-4-cloudflare/server/api/post-error.ts
rename to dev-packages/e2e-tests/test-applications/nuxt-4/server/api/post-error.ts
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/trace-continuation.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/trace-continuation.ts
new file mode 100644
index 000000000000..077c572679c6
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/trace-continuation.ts
@@ -0,0 +1,7 @@
+import * as Sentry from '@sentry/nuxt';
+import { defineEventHandler } from '#imports';
+
+export default defineEventHandler(() => {
+ Sentry.captureMessage('trace continuation');
+ return { ok: true };
+});
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/server/middleware/03.auth.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/server/middleware/03.auth.ts
index 471d1d32d7a4..a2ddef1c4c34 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-4/server/middleware/03.auth.ts
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/server/middleware/03.auth.ts
@@ -1,8 +1,10 @@
import { createError, defineEventHandler, getHeader, setHeader, getQuery } from '#imports';
+declare const __E2E_TEST_TUNNEL__: boolean;
+
export default defineEventHandler(async event => {
// The tunnel variant checks that the managed tunnel answers before an auth middleware like this one.
- if (process.env.E2E_TEST_TUNNEL && event.method === 'POST' && getHeader(event, 'x-auth') !== 'ok') {
+ if (__E2E_TEST_TUNNEL__ && event.method === 'POST' && getHeader(event, 'x-auth') !== 'ok') {
throw createError({ statusCode: 401, statusMessage: 'blocked by user auth' });
}
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/build-injection.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/build-injection.test.ts
index e00258723c71..6aeef1e31e88 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/build-injection.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/build-injection.test.ts
@@ -2,6 +2,7 @@ import { existsSync, readFileSync, readdirSync } from 'node:fs';
import path from 'node:path';
import { expect, test } from '@playwright/test';
import { isViteServer, NITRO_ONLY, NO_IMPORT_CONFIG } from './isViteServer';
+import { RUNTIME } from './runtime';
function readServerBundle(): string {
const serverDir = path.join(process.cwd(), '.output/server');
@@ -11,6 +12,11 @@ function readServerBundle(): string {
.join('\n');
}
+// The Cloudflare preset minifies the Worker, which renames the `tracingChannel` import.
+function tracingChannelCall(name: string): RegExp {
+ return new RegExp(`${RUNTIME === 'cloudflare' ? '[\\w$]+' : 'tracingChannel'}\\(["']${name}["']\\)`);
+}
+
function readClientBundle(): string {
const serverDir = path.join(process.cwd(), '.output/public');
return readdirSync(serverDir, { recursive: true })
@@ -34,15 +40,15 @@ test.describe('Orchestrion build-time injection', () => {
// Each transformed module carries the module-injected snippet that records it
// on the global marker (and registers its subscriber factory) when evaluated.
expect(serverBundle).toContain('orchestrionModuleInjected');
- expect(serverBundle).toMatch(/tracingChannel\(["']orchestrion:mysql:query["']\)/);
+ expect(serverBundle).toMatch(tracingChannelCall('orchestrion:mysql:query'));
});
test('injects diagnostics-channel publishers into ioredis', () => {
// Only a server route imports ioredis.
test.skip(isViteServer, NITRO_ONLY);
- expect(serverBundle).toMatch(/tracingChannel\(["']orchestrion:ioredis:command["']\)/);
- expect(serverBundle).toMatch(/tracingChannel\(["']orchestrion:ioredis:connect["']\)/);
+ expect(serverBundle).toMatch(tracingChannelCall('orchestrion:ioredis:command'));
+ expect(serverBundle).toMatch(tracingChannelCall('orchestrion:ioredis:connect'));
});
test('does not inject diagnostics-channel publishers into the client build', () => {
@@ -51,9 +57,12 @@ test.describe('Orchestrion build-time injection', () => {
});
});
+const NO_SERVER_CONFIG = 'Cloudflare: `sentryCloudflareNitroPlugin` sets up the SDK, not `sentry.server.config.ts`';
+
test.describe('Sentry server config injection', () => {
test('evaluates Sentry.init before nitro runs its plugins', () => {
test.skip(isViteServer, NITRO_ONLY);
+ test.skip(RUNTIME === 'cloudflare', NO_SERVER_CONFIG);
const nitroChunk = readFileSync(path.join(process.cwd(), '.output/server/chunks/nitro/nitro.mjs'), 'utf8');
@@ -72,6 +81,12 @@ test.describe('Sentry server config injection', () => {
const shimPath = path.join(process.cwd(), '.output/server/sentry.server.config.mjs');
+ if (RUNTIME === 'cloudflare') {
+ // A Worker is never started with `--import`, so there is nothing to keep working.
+ expect(existsSync(shimPath)).toBe(false);
+ return;
+ }
+
expect(existsSync(shimPath)).toBe(true);
expect(readFileSync(shimPath, 'utf8')).toContain('no longer needed');
});
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/cache.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/cache.test.ts
index 812ae2641ca0..644a371447c0 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/cache.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/cache.test.ts
@@ -1,8 +1,10 @@
import { expect, test } from '@playwright/test';
import { collectStreamedSpansUntilSegment } from '@sentry-internal/test-utils';
import { isViteServer, NITRO_ONLY } from './isViteServer';
+import { NO_SERVER_CONFIG_INSTRUMENTATION, RUNTIME } from './runtime';
test.skip(isViteServer, NITRO_ONLY);
+test.skip(RUNTIME === 'cloudflare', NO_SERVER_CONFIG_INSTRUMENTATION);
test.describe('Cache Instrumentation', () => {
const SEMANTIC_ATTRIBUTE_CACHE_KEY = 'cache.key';
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/database-multi.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/database-multi.test.ts
index 50a0f6ae09d6..70fe76fb4c5c 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/database-multi.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/database-multi.test.ts
@@ -1,8 +1,10 @@
import { expect, test } from '@playwright/test';
import { collectStreamedSpansUntilSegment, getSpanOp } from '@sentry-internal/test-utils';
import { isViteServer, NITRO_ONLY } from './isViteServer';
+import { NO_SERVER_CONFIG_INSTRUMENTATION, RUNTIME } from './runtime';
test.skip(isViteServer, NITRO_ONLY);
+test.skip(RUNTIME === 'cloudflare', NO_SERVER_CONFIG_INSTRUMENTATION);
async function collectDbSpans() {
const spans = await collectStreamedSpansUntilSegment(
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/database.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/database.test.ts
index ab6071727d6f..74ca13e02b93 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/database.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/database.test.ts
@@ -1,8 +1,10 @@
import { expect, test } from '@playwright/test';
import { collectStreamedSpansUntilSegment, getSpanOp, waitForError } from '@sentry-internal/test-utils';
import { isViteServer, NITRO_ONLY } from './isViteServer';
+import { NO_SERVER_CONFIG_INSTRUMENTATION, RUNTIME } from './runtime';
test.skip(isViteServer, NITRO_ONLY);
+test.skip(RUNTIME === 'cloudflare', NO_SERVER_CONFIG_INSTRUMENTATION);
async function collectDbSpans() {
const spans = await collectStreamedSpansUntilSegment(
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/db-drivers.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/db-drivers.test.ts
index 4702225dda1b..912fb27f4a91 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/db-drivers.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/db-drivers.test.ts
@@ -1,6 +1,7 @@
import { expect, test } from '@playwright/test';
import { collectStreamedSpansUntilSegment, getSpanOp } from '@sentry-internal/test-utils';
import { isViteServer, NITRO_ONLY } from './isViteServer';
+import { MONGOOSE_UNSUPPORTED, RUNTIME } from './runtime';
test.skip(isViteServer, NITRO_ONLY);
@@ -73,6 +74,8 @@ 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 }) => {
+ test.skip(RUNTIME === 'cloudflare', MONGOOSE_UNSUPPORTED);
+
const spansPromise = collectRequestSpans('/api/db-mongoose');
const response = await fetch(`${baseURL}/api/db-mongoose`);
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/import-compat.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/import-compat.test.ts
index be1316fb61a2..0b65f1c36d15 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/import-compat.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/import-compat.test.ts
@@ -3,10 +3,14 @@ import { spawn } from 'node:child_process';
import { expect, test } from '@playwright/test';
import { getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils';
import { isViteServer, NO_IMPORT_CONFIG } from './isViteServer';
+import { getServerCommand, NO_PROCESS, RUNTIME } from './runtime';
-// `node --import` start commands must keep working now that the config is bundled: the emitted
-// config file is a shim that only prints a removal hint, and a preload that really initializes
-// the SDK must not cause a second init. Each test spawns its own server on a dedicated port.
+test.skip(RUNTIME === 'cloudflare', NO_PROCESS);
+
+// `node --import` start commands (and their `--preload` counterparts on Bun and Deno) must keep
+// working now that the config is bundled: the emitted config file is a shim that only prints a
+// removal hint, and a preload that really initializes the SDK must not cause a second init. Each
+// test spawns its own server on a dedicated port.
interface PreloadedServer {
child: ChildProcess;
@@ -14,7 +18,8 @@ interface PreloadedServer {
}
async function startServerWithPreload(preloadPath: string, port: string): Promise {
- const child = spawn('node', ['--import', preloadPath, '.output/server/index.mjs'], {
+ const [command, args] = getServerCommand(preloadPath);
+ const child = spawn(command, args, {
env: { ...process.env, PORT: port },
});
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/logs.server.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/logs.server.test.ts
new file mode 100644
index 000000000000..f74ae4258bb0
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/logs.server.test.ts
@@ -0,0 +1,31 @@
+import { expect, test } from '@playwright/test';
+import { waitForEnvelopeItem, waitForStreamedSpan } from '@sentry-internal/test-utils';
+import { isViteServer, NITRO_ONLY } from './isViteServer';
+
+test.skip(isViteServer, NITRO_ONLY);
+
+interface LogContainer {
+ items: { trace_id?: string; body: string; level: string; attributes?: Record }[];
+}
+
+test('sends a log of a server route in the trace of the request', async ({ request }) => {
+ const segmentPromise = waitForStreamedSpan(
+ 'nuxt-4',
+ span => span.is_segment && span.attributes['url.path']?.value === '/api/log',
+ );
+
+ await request.get('/api/log');
+
+ const segment = await segmentPromise;
+ // The log can arrive before the segment, so the search starts at the first envelope of the proxy.
+ const logItem = await waitForEnvelopeItem(
+ 'nuxt-4',
+ item => item[0].type === 'log' && (item[1] as LogContainer).items.some(log => log.trace_id === segment.trace_id),
+ 0,
+ );
+ const log = (logItem[1] as LogContainer).items.find(log => log.trace_id === segment.trace_id)!;
+
+ expect(log.body).toBe('nuxt-4 server log');
+ expect(log.level).toBe('info');
+ expect(log.attributes?.['e2e.attr']).toEqual({ value: 'value', type: 'string' });
+});
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/middleware.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/middleware.test.ts
index cc12ce192ebd..195cc9b10567 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/middleware.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/middleware.test.ts
@@ -2,8 +2,10 @@ import { expect, test } from '@playwright/test';
import { collectStreamedSpansUntilSegment, getSpanOp, waitForError } from '@sentry-internal/test-utils';
import { IMPORT_SURFACES } from './importSurfaces';
import { isViteServer, NITRO_ONLY } from './isViteServer';
+import { NO_SERVER_CONFIG_INSTRUMENTATION, RUNTIME } from './runtime';
test.skip(isViteServer, NITRO_ONLY);
+test.skip(RUNTIME === 'cloudflare', NO_SERVER_CONFIG_INSTRUMENTATION);
// Nitro runs every file in `server/middleware/` on every request, so each surface tells its spans apart by
// middleware name. `nuxt/server` has no object syntax (`onRequest`, `onBeforeResponse`) and no `eventHandler`
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/runtime.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/runtime.ts
new file mode 100644
index 000000000000..f3640262c4db
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/runtime.ts
@@ -0,0 +1,31 @@
+import { getRuntime } from '@sentry-internal/test-utils';
+
+export const RUNTIME = getRuntime();
+
+export const NO_PROCESS = 'Cloudflare: the Worker is no process the tests can start with a preload or watch exit';
+
+// Without Sentry too, the Cloudflare preset of Nitro allows no externals and fails the build on the optional
+// `require()`s of the `mongodb` driver (for example `@aws-sdk/credential-providers`).
+export const MONGOOSE_UNSUPPORTED = 'Cloudflare: Nitro cannot bundle the `mongodb` driver for Workers';
+
+export const SERVER_ORIGIN = RUNTIME === 'cloudflare' ? 'auto.http.cloudflare' : 'auto.http.http_server';
+
+// The Cloudflare variant builds without `sentry.server.config.ts`, as the Cloudflare setup of `@sentry/nuxt` documents.
+// The module adds the db0, storage, cache and middleware instrumentation only with that file.
+export const NO_SERVER_CONFIG_INSTRUMENTATION =
+ 'Cloudflare: `@sentry/nuxt` instruments db0, storage, cache and middleware only with a `sentry.server.config.ts`';
+
+/** The command and arguments that start the built server on the runtime of the variant. */
+export function getServerCommand(preload?: string): [string, string[]] {
+ const entry = '.output/server/index.mjs';
+
+ if (RUNTIME === 'bun') {
+ return ['bun', preload ? ['--preload', preload, entry] : [entry]];
+ }
+
+ if (RUNTIME === 'deno') {
+ return ['deno', ['run', '-A', ...(preload ? [`--preload=${preload}`] : []), entry]];
+ }
+
+ return ['node', preload ? ['--import', preload, entry] : [entry]];
+}
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/scopes.server.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/scopes.server.test.ts
new file mode 100644
index 000000000000..c00cadd3eab5
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/scopes.server.test.ts
@@ -0,0 +1,19 @@
+import { expect, test } from '@playwright/test';
+import { waitForError } from '@sentry-internal/test-utils';
+import { isViteServer, NITRO_ONLY } from './isViteServer';
+
+test.skip(isViteServer, NITRO_ONLY);
+
+test('isolates the scope data and trace of concurrent requests', async ({ request }) => {
+ const tags = ['alpha', 'beta', 'gamma'];
+ const eventPromises = tags.map(tag => waitForError('nuxt-4', event => event.message === `isolation ${tag}`));
+
+ await Promise.all(tags.map(tag => request.get(`/api/isolation?tag=${tag}`)));
+ const events = await Promise.all(eventPromises);
+
+ events.forEach((event, index) => {
+ expect(event.tags?.['isolation-tag']).toBe(tags[index]);
+ expect(event.transaction).toBe('GET /api/isolation');
+ });
+ expect(new Set(events.map(event => event.contexts?.trace?.trace_id)).size).toBe(tags.length);
+});
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/startup-error.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/startup-error.test.ts
index b51a0719c206..e7f1ccae231f 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/startup-error.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/startup-error.test.ts
@@ -2,17 +2,25 @@ import { spawn } from 'node:child_process';
import { expect, test } from '@playwright/test';
import { waitForError, waitForSession } from '@sentry-internal/test-utils';
import { isViteServer, NITRO_ONLY } from './isViteServer';
+import { getServerCommand, NO_PROCESS, RUNTIME } from './runtime';
test.skip(isViteServer, NITRO_ONLY);
+test.skip(RUNTIME === 'cloudflare', NO_PROCESS);
// Errors thrown between server start and `listen` (module evaluation, nitro plugin runs) must be
// captured and flushed before the process exits. Each test spawns its own server process because
// the crash kills it; events still reach the shared event proxy via the tunnel.
+// Deno hands an error thrown while the modules evaluate to the `unhandledRejection` listeners, where Node and Bun
+// treat it as an uncaught exception. The listener of the SDK (mode `warn`) keeps the process alive, so on Deno it
+// exits with 0 once it has nothing left to do.
+const CRASH_EXIT_CODE = RUNTIME === 'deno' ? 0 : 1;
+
/** Starts the built server and resolves with its exit code. */
function spawnCrashingServer(env: Record): Promise {
return new Promise((resolve, reject) => {
- const child = spawn('node', ['.output/server/index.mjs'], {
+ const [command, args] = getServerCommand();
+ const child = spawn(command, args, {
// Session tracking needs a release
env: { ...process.env, SENTRY_RELEASE: 'startup-error-test', ...env },
});
@@ -29,7 +37,7 @@ test('captures error and crashed session when a nitro plugin throws during start
const [errorEvent, session] = await Promise.all([errorPromise, sessionPromise]);
- expect(exitCode).toBe(1);
+ expect(exitCode).toBe(CRASH_EXIT_CODE);
expect(errorEvent.exception?.values?.[0]?.value).toBe('startup-crash-test');
expect(errorEvent.exception?.values?.[0]?.mechanism?.handled).toBe(false);
expect(session.status).toBe('crashed');
@@ -43,7 +51,7 @@ test('captures error when a server module throws during bundle evaluation', asyn
const errorEvent = await errorPromise;
- expect(exitCode).toBe(1);
+ expect(exitCode).toBe(CRASH_EXIT_CODE);
expect(errorEvent.exception?.values?.[0]?.value).toBe('eval-crash-test');
expect(errorEvent.exception?.values?.[0]?.mechanism?.handled).toBe(false);
});
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/storage-aliases.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/storage-aliases.test.ts
index 9152c4d82905..b9eb33fc8f1d 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/storage-aliases.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/storage-aliases.test.ts
@@ -1,8 +1,10 @@
import { expect, test } from '@playwright/test';
import { collectStreamedSpansUntilSegment } from '@sentry-internal/test-utils';
import { isViteServer, NITRO_ONLY } from './isViteServer';
+import { NO_SERVER_CONFIG_INSTRUMENTATION, RUNTIME } from './runtime';
test.skip(isViteServer, NITRO_ONLY);
+test.skip(RUNTIME === 'cloudflare', NO_SERVER_CONFIG_INSTRUMENTATION);
async function collectStorageSpans(route: string) {
const spans = await collectStreamedSpansUntilSegment('nuxt-4', span => span.attributes['url.path']?.value === route);
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/storage.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/storage.test.ts
index 1c4ea12af2db..a602f881c3cc 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/storage.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/storage.test.ts
@@ -1,8 +1,10 @@
import { expect, test } from '@playwright/test';
import { collectStreamedSpansUntilSegment } from '@sentry-internal/test-utils';
import { isViteServer, NITRO_ONLY } from './isViteServer';
+import { NO_SERVER_CONFIG_INSTRUMENTATION, RUNTIME } from './runtime';
test.skip(isViteServer, NITRO_ONLY);
+test.skip(RUNTIME === 'cloudflare', NO_SERVER_CONFIG_INSTRUMENTATION);
async function collectStorageSpans(route: string) {
const spans = await collectStreamedSpansUntilSegment('nuxt-4', span => span.attributes['url.path']?.value === route);
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/trace-propagation.server.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/trace-propagation.server.test.ts
new file mode 100644
index 000000000000..11c726618ada
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/trace-propagation.server.test.ts
@@ -0,0 +1,59 @@
+import { expect, test } from '@playwright/test';
+import { HTTP_CLIENT, HTTP_SERVER } from '@sentry/conventions/op';
+import type { SerializedStreamedSpan } from '@sentry-internal/test-utils';
+import { collectStreamedSpans, getSpanOp, waitForError, waitForStreamedSpan } from '@sentry-internal/test-utils';
+import { isViteServer, NITRO_ONLY } from './isViteServer';
+
+test.skip(isViteServer, NITRO_ONLY);
+
+test('continues an incoming trace in the server segment and its events', async ({ request }) => {
+ const traceId = crypto.randomUUID().replace(/-/g, '');
+ const parentSpanId = traceId.slice(0, 16);
+
+ const segmentPromise = waitForStreamedSpan('nuxt-4', span => span.is_segment && span.trace_id === traceId);
+ const eventPromise = waitForError('nuxt-4', event => event.message === 'trace continuation');
+
+ await request.get('/api/trace-continuation', {
+ headers: {
+ 'sentry-trace': `${traceId}-${parentSpanId}-1`,
+ baggage: `sentry-trace_id=${traceId},sentry-sample_rate=1,sentry-sampled=true`,
+ },
+ });
+
+ const [segment, event] = await Promise.all([segmentPromise, eventPromise]);
+
+ expect(getSpanOp(segment)).toBe(HTTP_SERVER);
+ expect(segment.parent_span_id).toBe(parentSpanId);
+ expect(segment.attributes['url.path']?.value).toBe('/api/trace-continuation');
+ expect(event.contexts?.trace?.trace_id).toBe(traceId);
+ expect(event.contexts?.trace?.span_id).toBe(segment.span_id);
+});
+
+test('propagates the trace to an outgoing `fetch` request', async ({ request }) => {
+ const isOutgoingSegment = (span: SerializedStreamedSpan) =>
+ span.is_segment && span.attributes['url.path']?.value === '/api/outgoing-fetch';
+ const isIncomingSegment = (span: SerializedStreamedSpan) =>
+ span.is_segment && span.attributes['url.path']?.value === '/api/echo-headers';
+ const isHttpClientSpan = (span: SerializedStreamedSpan) =>
+ getSpanOp(span) === HTTP_CLIENT && String(span.attributes['url.full']?.value).endsWith('/api/echo-headers');
+
+ // Both segments and the http.client span in between share one trace, and `collectStreamedSpans`
+ // evaluates one trace at a time, so requiring all three keeps them paired.
+ const spansPromise = collectStreamedSpans(
+ 'nuxt-4',
+ spans => spans.some(isOutgoingSegment) && spans.some(isIncomingSegment) && spans.some(isHttpClientSpan),
+ );
+
+ const headers = await (await request.get('/api/outgoing-fetch')).json();
+
+ const spans = await spansPromise;
+ const outgoingSegment = spans.find(isOutgoingSegment)!;
+ const incomingSegment = spans.find(isIncomingSegment)!;
+ const httpClientSpan = spans.find(isHttpClientSpan)!;
+
+ expect(httpClientSpan.parent_span_id).toBe(outgoingSegment.span_id);
+ expect(incomingSegment.parent_span_id).toBe(httpClientSpan.span_id);
+ expect(headers['sentry-trace']).toBe(`${outgoingSegment.trace_id}-${httpClientSpan.span_id}-1`);
+ expect(headers.baggage).toContain(`sentry-trace_id=${outgoingSegment.trace_id}`);
+ expect(headers.baggage).toContain('sentry-sampled=true');
+});
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/tracing.server.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/tracing.server.test.ts
index a0c8630bd730..25d54633ebf7 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/tracing.server.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/tracing.server.test.ts
@@ -2,6 +2,7 @@ import { expect, test } from '@playwright/test';
import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils';
import { IMPORT_SURFACES } from './importSurfaces';
import { isViteServer, NITRO_ONLY } from './isViteServer';
+import { SERVER_ORIGIN } from './runtime';
test('sends a server root span on pageload', async ({ page }) => {
const serverSpanPromise = waitForStreamedSpan('nuxt-4', span => {
@@ -13,7 +14,7 @@ test('sends a server root span on pageload', async ({ page }) => {
const serverSpan = await serverSpanPromise;
expect(getSpanOp(serverSpan)).toBe('http.server');
- expect(serverSpan.attributes['sentry.origin']?.value).toBe('auto.http.http_server');
+ expect(serverSpan.attributes['sentry.origin']?.value).toBe(SERVER_ORIGIN);
});
test('does not send spans for build asset folder "_nuxt"', async ({ page }) => {
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/tracing.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/tracing.test.ts
index b67402a6ae23..ce154d246a9e 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/tracing.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/tracing.test.ts
@@ -1,11 +1,47 @@
+import type { APIRequestContext } from '@playwright/test';
import { expect, test } from '@playwright/test';
import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils';
import { IMPORT_SURFACES } from './importSurfaces';
import { isViteServer, NITRO_ONLY, NO_META_TAGS } from './isViteServer';
+import { RUNTIME, SERVER_ORIGIN } from './runtime';
+
+// `sentryCloudflareNitroPlugin` takes the trace data of the meta tags when the request starts, before the route
+// names the segment.
+const CLOUDFLARE_META_TAGS_BEFORE_ROUTE = 'Cloudflare meta tags are created before the route is known';
+
+async function getBaggageMetaTagAndServerSpan(request: APIRequestContext, path: string) {
+ const serverSpanPromise = waitForStreamedSpan('nuxt-4', span => {
+ return span.is_segment && span.name.includes('GET /test-param/');
+ });
+
+ const [response, serverSpan] = await Promise.all([request.get(path), serverSpanPromise]);
+ const matches = [...(await response.text()).matchAll(/ {
const PARAM = 's0me-param';
+ test('puts the parametrized route in the baggage meta tag', async ({ request }) => {
+ test.fail(isViteServer, NO_META_TAGS);
+ test.skip(RUNTIME === 'cloudflare', CLOUDFLARE_META_TAGS_BEFORE_ROUTE);
+
+ const { baggageMetaTagContent, serverSpan } = await getBaggageMetaTagAndServerSpan(request, `/test-param/${PARAM}`);
+ expect(baggageMetaTagContent).toContain(`sentry-trace_id=${serverSpan.trace_id}`);
+ // URL-encoded for parametrized 'GET /test-param/s0me-param' -> `GET /test-param/:param`
+ expect(baggageMetaTagContent).toContain(`sentry-transaction=GET%20%2Ftest-param%2F%3Aparam`);
+ });
+
+ test('leaves the transaction out of the baggage meta tag on Cloudflare', async ({ request }) => {
+ test.skip(RUNTIME !== 'cloudflare', CLOUDFLARE_META_TAGS_BEFORE_ROUTE);
+
+ const { baggageMetaTagContent, serverSpan } = await getBaggageMetaTagAndServerSpan(request, `/test-param/${PARAM}`);
+ expect(baggageMetaTagContent).toContain(`sentry-trace_id=${serverSpan.trace_id}`);
+ expect(baggageMetaTagContent).not.toContain('sentry-transaction=');
+ });
+
test('capture a distributed pageload trace', async ({ page }) => {
test.fail(isViteServer, NO_META_TAGS);
@@ -27,8 +63,6 @@ test.describe('distributed tracing', () => {
await expect(page.locator('meta[name="baggage"]')).toHaveCount(1);
const baggageMetaTagContent = await page.locator('meta[name="baggage"]').getAttribute('content');
- // URL-encoded for parametrized 'GET /test-param/s0me-param' -> `GET /test-param/:param`
- expect(baggageMetaTagContent).toContain(`sentry-transaction=GET%20%2Ftest-param%2F%3Aparam`);
expect(baggageMetaTagContent).toContain(`sentry-trace_id=${serverSpan.trace_id}`);
expect(baggageMetaTagContent).toContain('sentry-sampled=true');
expect(baggageMetaTagContent).toContain('sentry-sample_rate=1');
@@ -55,7 +89,7 @@ test.describe('distributed tracing', () => {
is_segment: true,
attributes: expect.objectContaining({
'sentry.op': { type: 'string', value: 'http.server' },
- 'sentry.origin': { type: 'string', value: 'auto.http.http_server' },
+ 'sentry.origin': { type: 'string', value: SERVER_ORIGIN },
'sentry.segment.name.source': { type: 'string', value: 'route' },
}),
});
@@ -135,7 +169,7 @@ test.describe('distributed tracing', () => {
is_segment: true,
attributes: expect.objectContaining({
'sentry.op': { type: 'string', value: 'http.server' },
- 'sentry.origin': { type: 'string', value: 'auto.http.http_server' },
+ 'sentry.origin': { type: 'string', value: SERVER_ORIGIN },
'sentry.segment.name.source': { type: 'string', value: 'route' },
}),
});
@@ -146,7 +180,7 @@ test.describe('distributed tracing', () => {
parent_span_id: httpClientSpan?.span_id, // http.client span is parent
attributes: expect.objectContaining({
'sentry.op': { type: 'string', value: 'http.server' },
- 'sentry.origin': { type: 'string', value: 'auto.http.http_server' },
+ 'sentry.origin': { type: 'string', value: SERVER_ORIGIN },
}),
});
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4-cloudflare/tests/tunnel-request-body.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/tunnel-request-body.test.ts
similarity index 56%
rename from dev-packages/e2e-tests/test-applications/nuxt-4-cloudflare/tests/tunnel-request-body.test.ts
rename to dev-packages/e2e-tests/test-applications/nuxt-4/tests/tunnel-request-body.test.ts
index aaca20e2a3eb..24173103eb56 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-4-cloudflare/tests/tunnel-request-body.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/tunnel-request-body.test.ts
@@ -4,9 +4,13 @@ import { waitForError } from '@sentry-internal/test-utils';
test.skip(!process.env.E2E_TEST_TUNNEL, 'Only runs in the tunnel variant');
test('keeps the body of a request that is not a tunnel request off its error event', async ({ request }) => {
- const errorPromise = waitForError('nuxt-4-cloudflare', event => event.exception?.values?.[0]?.value === 'POST error');
+ const errorPromise = waitForError('nuxt-4', event => event.exception?.values?.[0]?.value === 'POST error');
- await request.post('/app/api/post-error', { data: { note: 'stays off the event' } });
+ await request.post(`${process.env.E2E_TEST_BASE_PATH ?? ''}/api/post-error`, {
+ data: { note: 'stays off the event' },
+ // Passes the auth middleware of the tunnel variant.
+ headers: { 'x-auth': 'ok' },
+ });
const error = await errorPromise;
expect(error.request?.method).toBe('POST');
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/wrangler.jsonc b/dev-packages/e2e-tests/test-applications/nuxt-4/wrangler.jsonc
new file mode 100644
index 000000000000..35ba0deb951f
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/wrangler.jsonc
@@ -0,0 +1,23 @@
+{
+ "$schema": "node_modules/wrangler/config-schema.json",
+ "name": "nuxt-4",
+ "main": "./.output/server/index.mjs",
+ "compatibility_date": "2026-06-29",
+ "compatibility_flags": ["nodejs_compat"],
+ "assets": {
+ "directory": "./.output/public",
+ "binding": "ASSETS",
+ // Like Nitro on Node, serve pre-rendered pages at their path instead of redirecting to a trailing slash.
+ "html_handling": "drop-trailing-slash",
+ },
+ // `wrangler dev` keeps these in a local SQLite file, so they need no real database.
+ "d1_databases": [
+ { "binding": "DB", "database_name": "db", "database_id": "00000000-0000-0000-0000-000000000001" },
+ { "binding": "USERS_DB", "database_name": "users_db", "database_id": "00000000-0000-0000-0000-000000000002" },
+ {
+ "binding": "ANALYTICS_DB",
+ "database_name": "analytics_db",
+ "database_id": "00000000-0000-0000-0000-000000000003",
+ },
+ ],
+}
diff --git a/packages/nuxt/src/module.ts b/packages/nuxt/src/module.ts
index 578d5c68379f..8d613d4dd533 100644
--- a/packages/nuxt/src/module.ts
+++ b/packages/nuxt/src/module.ts
@@ -92,17 +92,19 @@ export default defineNuxtModule({
} else {
setupNitroServer(nuxt, serverApi, serverConfigFile, moduleOptions);
}
-
- if (isMinNuxtV4) {
- addPlugin({ src: moduleDirResolver.resolve('./runtime/plugins/route-detector.server'), mode: 'server' });
- } else {
- addPlugin({ src: moduleDirResolver.resolve('./runtime/plugins/route-detector-legacy.server'), mode: 'server' });
- }
} else {
// The Cloudflare setup initializes the SDK from `sentryCloudflareNitroPlugin`, so it needs the tunnel too.
addTunnelPlugin(serverApi);
}
+ // Names the server spans of pages after their route, with the SDK set up from the server config or from
+ // `sentryCloudflareNitroPlugin`.
+ if (isMinNuxtV4) {
+ addPlugin({ src: moduleDirResolver.resolve('./runtime/plugins/route-detector.server'), mode: 'server' });
+ } else {
+ addPlugin({ src: moduleDirResolver.resolve('./runtime/plugins/route-detector-legacy.server'), mode: 'server' });
+ }
+
if (clientConfigFile || serverConfigFile) {
setupSourceMaps(moduleOptions, nuxt, addVitePlugin);
}
diff --git a/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts b/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts
index ae8e8c68ed2d..0f4aa12b2898 100644
--- a/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts
+++ b/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts
@@ -2,7 +2,13 @@ import type { ExecutionContext } from '@cloudflare/workers-types';
import type { CloudflareOptions } from '@sentry/cloudflare';
import { getDefaultIntegrations, setAsyncLocalStorageAsyncContextStrategy } from '@sentry/cloudflare';
import { wrapRequestHandler } from '@sentry/cloudflare/request';
-import { debug, getDefaultIsolationScope, getIsolationScope, getTraceData } from '@sentry/core';
+import {
+ debug,
+ getDefaultIsolationScope,
+ getIsolationScope,
+ getTraceData,
+ stripUrlQueryAndFragment,
+} from '@sentry/core';
import { _INTERNAL_isServedTunnelRequest as isServedTunnelRequest } from '@sentry/core/server';
import type { H3Event } from 'h3';
import type { NitroApp, NitroAppPlugin } from 'nitropack';
@@ -75,7 +81,10 @@ export const sentryCloudflareNitroPlugin =
request,
context: runtime.cloudflare.context,
},
- () => appFetch(request),
+ () => {
+ setRequestTransactionName(request.method, new URL(request.url).pathname);
+ return appFetch(request);
+ },
);
};
nitroApp.hooks.hook('error', sentryCaptureErrorHook);
@@ -120,6 +129,8 @@ export const sentryCloudflareNitroPlugin =
};
return wrapRequestHandler(requestHandlerOptions, () => {
+ setRequestTransactionName(event.method, pathname);
+
const isolationScope = getIsolationScope();
const newIsolationScope =
isolationScope === getDefaultIsolationScope() ? isolationScope.clone() : isolationScope;
@@ -170,3 +181,11 @@ export const sentryCloudflareNitroPlugin =
nitroApp.hooks.hook('error', sentryCaptureErrorHook);
};
+
+/**
+ * Names the events of the request after its method and path until a route names them, like the `node:http`
+ * instrumentation does on Node. Without it, events captured outside Nitro's `error` hook have no transaction.
+ */
+function setRequestTransactionName(method: string, path: string): void {
+ getIsolationScope().setTransactionName(`${method.toUpperCase()} ${stripUrlQueryAndFragment(path)}`);
+}
diff --git a/packages/nuxt/src/runtime/utils/database-span-data.ts b/packages/nuxt/src/runtime/utils/database-span-data.ts
index ed3a4804a656..aa819fe62b74 100644
--- a/packages/nuxt/src/runtime/utils/database-span-data.ts
+++ b/packages/nuxt/src/runtime/utils/database-span-data.ts
@@ -45,7 +45,11 @@ export function getDatabaseSpanData(config?: DatabaseConnectionConfig): Partial<
};
}
- if ((['better-sqlite3', 'bun', 'sqlite', 'sqlite3'] as ConnectorName[]).includes(config.connector)) {
+ if (
+ (['better-sqlite3', 'bun', 'bun-sqlite', 'node-sqlite', 'sqlite', 'sqlite3'] as ConnectorName[]).includes(
+ config.connector,
+ )
+ ) {
return {
// DB is the default file name in nitro for sqlite-like connectors
[DB_NAMESPACE]: `${config.options?.name ?? 'db'}.sqlite`,
diff --git a/packages/nuxt/src/runtime/utils/instrumentDatabase.ts b/packages/nuxt/src/runtime/utils/instrumentDatabase.ts
index 6dc9a4d84818..9d5e2ca8a610 100644
--- a/packages/nuxt/src/runtime/utils/instrumentDatabase.ts
+++ b/packages/nuxt/src/runtime/utils/instrumentDatabase.ts
@@ -73,7 +73,8 @@ function instrumentDatabase(db: MaybeInstrumentedDatabase, config?: DatabaseConn
}
const metadata: DatabaseSpanData = {
- [DB_SYSTEM_NAME]: config?.connector ?? db.dialect,
+ // A connector name like `bun-sqlite` or `mysql2` names the driver, the dialect names the database system.
+ [DB_SYSTEM_NAME]: db.dialect ?? config?.connector,
...getDatabaseSpanData(config),
};
@@ -85,7 +86,18 @@ function instrumentDatabase(db: MaybeInstrumentedDatabase, config?: DatabaseConn
apply(target, thisArg, args: Parameters) {
const [query] = args;
- return instrumentPreparedStatement(target.apply(thisArg, args), query, metadata, dialect);
+ let statement: PreparedStatement;
+ try {
+ statement = target.apply(thisArg, args);
+ } catch (error) {
+ // Connectors like `bun-sqlite` and `better-sqlite3` compile the statement in `prepare`, so an invalid query
+ // throws here and not when the statement runs.
+ startSpan(createStartSpanOptions(query, metadata, dialect), span => captureDatabaseError(span, error));
+ void flushIfServerless();
+ throw error;
+ }
+
+ return instrumentPreparedStatement(statement, query, metadata, dialect);
},
});
@@ -224,13 +236,7 @@ function handleSpanStart(fn: () => unknown, breadcrumbOpts?: { query: string; di
return result;
} catch (error) {
- span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' });
- captureException(error, {
- mechanism: {
- handled: false,
- type: NUXT_DATABASE_ORIGIN,
- },
- });
+ captureDatabaseError(span, error);
// Re-throw the error to be handled by the caller
throw error;
@@ -240,6 +246,16 @@ function handleSpanStart(fn: () => unknown, breadcrumbOpts?: { query: string; di
};
}
+function captureDatabaseError(span: Span, error: unknown): void {
+ span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' });
+ captureException(error, {
+ mechanism: {
+ handled: false,
+ type: NUXT_DATABASE_ORIGIN,
+ },
+ });
+}
+
function createBreadcrumb(query: string, dialect: SqlDialect | undefined): void {
// The breadcrumb carries the same query text as the span, so it is sanitized and guarded the same way.
const queryText = query ? sanitizeSqlQuery(query, dialect) : undefined;
diff --git a/packages/nuxt/test/runtime/utils/database-span-data.test.ts b/packages/nuxt/test/runtime/utils/database-span-data.test.ts
index fc4f4b376af8..8d4e19bc82e2 100644
--- a/packages/nuxt/test/runtime/utils/database-span-data.test.ts
+++ b/packages/nuxt/test/runtime/utils/database-span-data.test.ts
@@ -124,7 +124,7 @@ describe('getDatabaseSpanData', () => {
});
describe('SQLite-like connectors', () => {
- it.each(['better-sqlite3', 'bun', 'sqlite', 'sqlite3'] as ConnectorName[])(
+ it.each(['better-sqlite3', 'bun', 'bun-sqlite', 'node-sqlite', 'sqlite', 'sqlite3'] as ConnectorName[])(
'should extract database name for %s',
connector => {
const config: DatabaseConfig = {
@@ -141,7 +141,7 @@ describe('getDatabaseSpanData', () => {
},
);
- it.each(['better-sqlite3', 'bun', 'sqlite', 'sqlite3'] as ConnectorName[])(
+ it.each(['better-sqlite3', 'bun', 'bun-sqlite', 'node-sqlite', 'sqlite', 'sqlite3'] as ConnectorName[])(
'should use default name for %s when name is not provided',
connector => {
const config: DatabaseConfig = {
@@ -156,7 +156,7 @@ describe('getDatabaseSpanData', () => {
},
);
- it.each(['better-sqlite3', 'bun', 'sqlite', 'sqlite3'] as ConnectorName[])(
+ it.each(['better-sqlite3', 'bun', 'bun-sqlite', 'node-sqlite', 'sqlite', 'sqlite3'] as ConnectorName[])(
'should handle missing options for %s',
connector => {
const config: DatabaseConfig = {
diff --git a/packages/nuxt/test/runtime/utils/instrumentDatabase.test.ts b/packages/nuxt/test/runtime/utils/instrumentDatabase.test.ts
new file mode 100644
index 000000000000..92e69fab3e22
--- /dev/null
+++ b/packages/nuxt/test/runtime/utils/instrumentDatabase.test.ts
@@ -0,0 +1,71 @@
+import { DB_NAMESPACE, DB_QUERY_TEXT, DB_SYSTEM_NAME, SENTRY_ORIGIN } from '@sentry/conventions/attributes';
+import * as SentryCore from '@sentry/core';
+import type * as SentryCoreServer from '@sentry/core/server';
+import { flushIfServerless } from '@sentry/core/server';
+import type { Database } from 'db0';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+import { createDatabasePlugin } from '../../../src/runtime/utils/instrumentDatabase';
+
+const span = { setStatus: vi.fn() };
+
+vi.mock('@sentry/core/server', async importOriginal => ({
+ ...(await importOriginal()),
+ flushIfServerless: vi.fn(),
+}));
+
+vi.mock('@sentry/core', async importOriginal => ({
+ ...(await importOriginal()),
+ captureException: vi.fn(),
+ startSpan: vi.fn((_options, callback) => callback(span)),
+}));
+
+function createDatabase(prepare: Database['prepare']): Database {
+ return { dialect: 'sqlite', prepare, sql: vi.fn(), exec: vi.fn() } as unknown as Database;
+}
+
+describe('createDatabasePlugin', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('captures an error that `prepare` throws and rethrows it', () => {
+ const error = new Error('no such table: missing');
+ const db = createDatabase(() => {
+ throw error;
+ });
+
+ createDatabasePlugin(() => db, { default: { connector: 'bun-sqlite' } });
+
+ expect(() => db.prepare('SELECT * FROM missing')).toThrow(error);
+ expect(SentryCore.startSpan).toHaveBeenCalledWith(
+ expect.objectContaining({
+ attributes: expect.objectContaining({
+ [DB_QUERY_TEXT]: 'SELECT * FROM missing',
+ [DB_SYSTEM_NAME]: 'sqlite',
+ [SENTRY_ORIGIN]: 'auto.db.nuxt',
+ }),
+ }),
+ expect.any(Function),
+ );
+ expect(span.setStatus).toHaveBeenCalledWith({ code: SentryCore.SPAN_STATUS_ERROR, message: 'internal_error' });
+ expect(SentryCore.captureException).toHaveBeenCalledWith(error, {
+ mechanism: { handled: false, type: 'auto.db.nuxt' },
+ });
+ expect(flushIfServerless).toHaveBeenCalledTimes(1);
+ });
+
+ it('names the database system after the dialect, not the connector', async () => {
+ const statement = { all: vi.fn(), get: vi.fn(), run: vi.fn(), bind: vi.fn() };
+ const db = createDatabase(() => statement as unknown as ReturnType);
+
+ createDatabasePlugin(() => db, { default: { connector: 'bun-sqlite' } });
+ await db.prepare('SELECT 1').all();
+
+ expect(SentryCore.startSpan).toHaveBeenCalledWith(
+ expect.objectContaining({
+ attributes: expect.objectContaining({ [DB_SYSTEM_NAME]: 'sqlite', [DB_NAMESPACE]: 'db.sqlite' }),
+ }),
+ expect.any(Function),
+ );
+ });
+});
diff --git a/packages/server-utils/src/orchestrion/bundler/rollup.ts b/packages/server-utils/src/orchestrion/bundler/rollup.ts
index fc0403927a11..651b5b89708d 100644
--- a/packages/server-utils/src/orchestrion/bundler/rollup.ts
+++ b/packages/server-utils/src/orchestrion/bundler/rollup.ts
@@ -45,7 +45,7 @@ export interface CommonJSInteropOptions {
* `@rollup/plugin-commonjs` options for builds that bundle CommonJS packages while their
* dependencies stay external. Builtin `require()`s unwrap to the module (a namespace breaks
* direct calls) and convert inside `try` blocks (a bare `require` throws in ESM output).
- * Externals keep `'auto'`, which {@link sentryCommonJSInteropPlugin} fixes for Node >= 23.
+ * Externals keep `'auto'`, which {@link sentryCommonJSInteropPlugin} fixes for Node >= 23, Deno and Bun.
*/
export function commonJSInteropOptions(): CommonJSInteropOptions {
return {
@@ -57,14 +57,19 @@ export function commonJSInteropOptions(): CommonJSInteropOptions {
const COMMONJS_HELPERS_ID = '\0commonjsHelpers.js';
// The plugin's `'auto'` helper unwraps a required external module only when `default` is the
-// sole key on its namespace. Node >= 23 adds a `'module.exports'` key to every CommonJS
-// namespace (nodejs/node#53848), so the check never passes. Ignoring that key restores it.
+// sole key on its namespace. Node >= 23 and Deno add a `'module.exports'` key to every CommonJS
+// namespace (nodejs/node#53848), so ignoring that key restores the check there. Bun adds a key
+// for each own property of `module.exports`, which for a function includes the non-enumerable
+// `length` and `name`. So on Bun, a namespace whose keys all mirror its function default, at least
+// one of them a non-enumerable property, is unwrapped. An ES module namespace can match only when
+// its named exports equal the same properties of its default export, which loses nothing.
const BROKEN_NAMESPACE_CHECK = 'Object.keys(n).length === 1';
-const FIXED_NAMESPACE_CHECK = "Object.keys(n).filter(k => k !== 'module.exports').length === 1";
+const FIXED_NAMESPACE_CHECK =
+ "(Object.keys(n).filter(k => k !== 'module.exports').length === 1 || (typeof n['default'] === 'function' && Object.keys(n).every(k => k === 'default' || k === 'module.exports' || n[k] === n['default'][k]) && Object.keys(n).some(k => Object.getOwnPropertyDescriptor(n['default'], k)?.enumerable === false)))";
/**
- * Fixes `@rollup/plugin-commonjs`' `'auto'` interop for Node >= 23 by patching the broken
- * namespace check in its helpers module. The patch applies only while the exact broken
+ * Fixes `@rollup/plugin-commonjs`' `'auto'` interop for Node >= 23, Deno and Bun by patching the
+ * broken namespace check in its helpers module. The patch applies only while the exact broken
* expression exists, so it retires itself once the plugin is fixed upstream.
*/
export function sentryCommonJSInteropPlugin(): Plugin {
diff --git a/packages/server-utils/test/orchestrion/rollup-plugin.test.ts b/packages/server-utils/test/orchestrion/rollup-plugin.test.ts
index 6f7ef28041bb..e328e5f06f81 100644
--- a/packages/server-utils/test/orchestrion/rollup-plugin.test.ts
+++ b/packages/server-utils/test/orchestrion/rollup-plugin.test.ts
@@ -86,19 +86,57 @@ describe('sentryCommonJSInteropPlugin', () => {
it('patches the namespace check in the commonjs helpers module', () => {
const result = transform(brokenHelper, '\0commonjsHelpers.js');
- expect(result?.code).toContain("Object.keys(n).filter(k => k !== 'module.exports').length === 1");
expect(result?.code).not.toContain('Object.keys(n).length === 1');
});
+ describe('patched helper', () => {
+ const patched = transform(brokenHelper, '\0commonjsHelpers.js')!.code.replace('export function', 'function');
+ // oxlint-disable-next-line typescript/no-implied-eval -- evaluates the patched helper source
+ const getDefault = new Function(`${patched}; return getDefaultExportFromNamespaceIfNotNamed;`)() as (
+ n: Record,
+ ) => unknown;
+
+ const cjsExports = Object.assign(function defaults() {}, { helper: 1 });
+
+ it('unwraps a CommonJS namespace on Node < 23', () => {
+ expect(getDefault({ default: cjsExports })).toBe(cjsExports);
+ });
+
+ it('unwraps a CommonJS namespace on Node >= 23 and Deno', () => {
+ expect(getDefault({ default: cjsExports, 'module.exports': cjsExports })).toBe(cjsExports);
+ });
+
+ it('unwraps the namespace of a CommonJS function export on Bun', () => {
+ const namespace = {
+ default: cjsExports,
+ helper: cjsExports.helper,
+ length: cjsExports.length,
+ name: cjsExports.name,
+ prototype: cjsExports.prototype,
+ };
+
+ expect(getDefault(namespace)).toBe(cjsExports);
+ });
+
+ it('keeps the namespace of an ES module with named exports', () => {
+ const namespace = { default: cjsExports, helper: cjsExports.helper };
+
+ expect(getDefault(namespace)).toBe(namespace);
+ });
+
+ it('keeps the namespace of an ES module that exports a `name` next to its default function', () => {
+ const namespace = { default: function plugin() {}, name: 'my-plugin' };
+
+ expect(getDefault(namespace)).toBe(namespace);
+ });
+ });
+
it('ignores other modules', () => {
expect(transform(brokenHelper, '/app/node_modules/mongoose/lib/index.js')).toBeNull();
});
it('leaves an already-fixed helpers module unchanged', () => {
- const fixedHelper = brokenHelper.replace(
- 'Object.keys(n).length === 1',
- "Object.keys(n).filter(k => k !== 'module.exports').length === 1",
- );
+ const fixedHelper = transform(brokenHelper, '\0commonjsHelpers.js')!.code;
expect(transform(fixedHelper, '\0commonjsHelpers.js')).toBeNull();
});