diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml
index ac4e1df08841..bbfdba31161f 100644
--- a/.github/workflows/canary.yml
+++ b/.github/workflows/canary.yml
@@ -120,6 +120,9 @@ jobs:
- test-application: 'nestjs-microservices'
build-command: 'test:build-latest'
label: 'nestjs-microservices (latest)'
+ - test-application: 'nitro-3'
+ build-command: 'test:build-canary'
+ label: 'nitro-3 (canary)'
steps:
- name: Check out current commit
diff --git a/dev-packages/e2e-tests/test-applications/nitro-3/.npmrc b/dev-packages/e2e-tests/test-applications/nitro-3/.npmrc
new file mode 100644
index 000000000000..070f80f05092
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nitro-3/.npmrc
@@ -0,0 +1,2 @@
+@sentry:registry=http://127.0.0.1:4873
+@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/nitro-3/index.html b/dev-packages/e2e-tests/test-applications/nitro-3/index.html
new file mode 100644
index 000000000000..4e9315ac391e
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nitro-3/index.html
@@ -0,0 +1,11 @@
+
+
+
+
+ Nitro E2E Test
+
+
+ Nitro E2E Test App
+
+
+
diff --git a/dev-packages/e2e-tests/test-applications/nitro-3/instrument.mjs b/dev-packages/e2e-tests/test-applications/nitro-3/instrument.mjs
new file mode 100644
index 000000000000..53b80d309a5b
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nitro-3/instrument.mjs
@@ -0,0 +1,8 @@
+import * as Sentry from '@sentry/nitro';
+
+Sentry.init({
+ environment: 'qa', // dynamic sampling bias to keep transactions
+ dsn: process.env.E2E_TEST_DSN,
+ tunnel: `http://localhost:3031/`, // proxy server
+ tracesSampleRate: 1,
+});
diff --git a/dev-packages/e2e-tests/test-applications/nitro-3/package.json b/dev-packages/e2e-tests/test-applications/nitro-3/package.json
new file mode 100644
index 000000000000..ab92769115d1
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nitro-3/package.json
@@ -0,0 +1,29 @@
+{
+ "name": "nitro-3",
+ "version": "1.0.0",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "build": "vite build",
+ "start": "PORT=3030 NODE_OPTIONS='--import ./instrument.mjs' node .output/server/index.mjs",
+ "clean": "npx rimraf node_modules pnpm-lock.yaml .output",
+ "test": "playwright test",
+ "test:build": "pnpm install && pnpm build",
+ "test:assert": "pnpm test"
+ },
+ "dependencies": {
+ "@sentry/browser": "latest || *",
+ "@sentry/nitro": "latest || *"
+ },
+ "devDependencies": {
+ "@playwright/test": "~1.56.0",
+ "@sentry-internal/test-utils": "link:../../../test-utils",
+ "@sentry/core": "latest || *",
+ "nitro": "^3.0.260415-beta",
+ "rolldown": "latest",
+ "vite": "latest"
+ },
+ "volta": {
+ "extends": "../../package.json"
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/nitro-3/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/nitro-3/playwright.config.mjs
new file mode 100644
index 000000000000..31f2b913b58b
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nitro-3/playwright.config.mjs
@@ -0,0 +1,7 @@
+import { getPlaywrightConfig } from '@sentry-internal/test-utils';
+
+const config = getPlaywrightConfig({
+ startCommand: `pnpm start`,
+});
+
+export default config;
diff --git a/dev-packages/e2e-tests/test-applications/nitro-3/server/api/index.ts b/dev-packages/e2e-tests/test-applications/nitro-3/server/api/index.ts
new file mode 100644
index 000000000000..a9fca21eecfb
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nitro-3/server/api/index.ts
@@ -0,0 +1,5 @@
+import { defineHandler } from 'nitro/h3';
+
+export default defineHandler(() => {
+ return { status: 'ok' };
+});
diff --git a/dev-packages/e2e-tests/test-applications/nitro-3/server/api/test-error.ts b/dev-packages/e2e-tests/test-applications/nitro-3/server/api/test-error.ts
new file mode 100644
index 000000000000..170efb1977ab
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nitro-3/server/api/test-error.ts
@@ -0,0 +1,5 @@
+import { defineHandler } from 'nitro/h3';
+
+export default defineHandler(() => {
+ throw new Error('This is a test error');
+});
diff --git a/dev-packages/e2e-tests/test-applications/nitro-3/server/api/test-isolation/[id].ts b/dev-packages/e2e-tests/test-applications/nitro-3/server/api/test-isolation/[id].ts
new file mode 100644
index 000000000000..a8c2cd7a99f5
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nitro-3/server/api/test-isolation/[id].ts
@@ -0,0 +1,10 @@
+import { getDefaultIsolationScope, setTag } from '@sentry/core';
+import { defineHandler } from 'nitro/h3';
+
+export default defineHandler(() => {
+ setTag('my-isolated-tag', true);
+ // Check if the tag leaked into the default (global) isolation scope
+ setTag('my-global-scope-isolated-tag', getDefaultIsolationScope().getScopeData().tags['my-isolated-tag']);
+
+ throw new Error('Isolation test error');
+});
diff --git a/dev-packages/e2e-tests/test-applications/nitro-3/server/api/test-nesting.ts b/dev-packages/e2e-tests/test-applications/nitro-3/server/api/test-nesting.ts
new file mode 100644
index 000000000000..687c6f3f1e9a
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nitro-3/server/api/test-nesting.ts
@@ -0,0 +1,16 @@
+import { startSpan } from '@sentry/nitro';
+import { defineHandler } from 'nitro/h3';
+
+export default defineHandler(() => {
+ startSpan({ name: 'db.select', op: 'db' }, () => {
+ // simulate a select query
+ });
+
+ startSpan({ name: 'db.insert', op: 'db' }, () => {
+ startSpan({ name: 'db.serialize', op: 'serialize' }, () => {
+ // simulate serializing data before insert
+ });
+ });
+
+ return { status: 'ok', nesting: true };
+});
diff --git a/dev-packages/e2e-tests/test-applications/nitro-3/server/api/test-param/[id].ts b/dev-packages/e2e-tests/test-applications/nitro-3/server/api/test-param/[id].ts
new file mode 100644
index 000000000000..ef67525b36ba
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nitro-3/server/api/test-param/[id].ts
@@ -0,0 +1,6 @@
+import { defineHandler } from 'nitro/h3';
+
+export default defineHandler(event => {
+ const id = event.req.url;
+ return { id };
+});
diff --git a/dev-packages/e2e-tests/test-applications/nitro-3/server/api/test-transaction.ts b/dev-packages/e2e-tests/test-applications/nitro-3/server/api/test-transaction.ts
new file mode 100644
index 000000000000..b488b371310d
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nitro-3/server/api/test-transaction.ts
@@ -0,0 +1,5 @@
+import { defineHandler } from 'nitro/h3';
+
+export default defineHandler(() => {
+ return { status: 'ok', transaction: true };
+});
diff --git a/dev-packages/e2e-tests/test-applications/nitro-3/server/middleware/test.ts b/dev-packages/e2e-tests/test-applications/nitro-3/server/middleware/test.ts
new file mode 100644
index 000000000000..92d8f80c3756
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nitro-3/server/middleware/test.ts
@@ -0,0 +1,10 @@
+import { defineHandler, getQuery, setResponseHeader } from 'nitro/h3';
+
+export default defineHandler(event => {
+ setResponseHeader(event, 'x-sentry-test-middleware', 'executed');
+
+ const query = getQuery(event);
+ if (query['middleware-error'] === '1') {
+ throw new Error('Middleware error');
+ }
+});
diff --git a/dev-packages/e2e-tests/test-applications/nitro-3/src/main.ts b/dev-packages/e2e-tests/test-applications/nitro-3/src/main.ts
new file mode 100644
index 000000000000..d27d0ba1763a
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nitro-3/src/main.ts
@@ -0,0 +1,10 @@
+import * as Sentry from '@sentry/browser';
+
+// Let's us test trace propagation
+Sentry.init({
+ environment: 'qa',
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ tunnel: 'http://localhost:3031/', // proxy server
+ integrations: [Sentry.browserTracingIntegration()],
+ tracesSampleRate: 1.0,
+});
diff --git a/dev-packages/e2e-tests/test-applications/nitro-3/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/nitro-3/start-event-proxy.mjs
new file mode 100644
index 000000000000..928e68908661
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nitro-3/start-event-proxy.mjs
@@ -0,0 +1,6 @@
+import { startEventProxyServer } from '@sentry-internal/test-utils';
+
+startEventProxyServer({
+ port: 3031,
+ proxyServerName: 'nitro-3',
+});
diff --git a/dev-packages/e2e-tests/test-applications/nitro-3/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/nitro-3/tests/errors.test.ts
new file mode 100644
index 000000000000..8e419ac9ba62
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nitro-3/tests/errors.test.ts
@@ -0,0 +1,45 @@
+import { expect, test } from '@playwright/test';
+import { waitForError } from '@sentry-internal/test-utils';
+
+test('Sends an error event to Sentry', async ({ request }) => {
+ const errorEventPromise = waitForError('nitro-3', event => {
+ return !event.type && !!event.exception?.values?.some(v => v.value === 'This is a test error');
+ });
+
+ await request.get('/api/test-error');
+
+ const errorEvent = await errorEventPromise;
+
+ // Nitro wraps thrown errors in an HTTPError with .cause, producing a chained exception
+ expect(errorEvent.exception?.values).toHaveLength(2);
+
+ // The innermost exception (values[0]) is the original thrown error
+ expect(errorEvent.exception?.values?.[0]?.type).toBe('Error');
+ expect(errorEvent.exception?.values?.[0]?.value).toBe('This is a test error');
+ expect(errorEvent.exception?.values?.[0]?.mechanism).toEqual(
+ expect.objectContaining({
+ handled: false,
+ type: 'auto.function.nitro.captureErrorHook',
+ }),
+ );
+
+ // The outermost exception (values[1]) is the HTTPError wrapper
+ expect(errorEvent.exception?.values?.[1]?.type).toBe('HTTPError');
+ expect(errorEvent.exception?.values?.[1]?.value).toBe('This is a test error');
+});
+
+test('Does not send 404 errors to Sentry', async ({ request }) => {
+ let errorReceived = false;
+
+ void waitForError('nitro-3', event => {
+ if (!event.type) {
+ errorReceived = true;
+ return true;
+ }
+ return false;
+ });
+
+ await request.get('/api/non-existent-route');
+
+ expect(errorReceived).toBe(false);
+});
diff --git a/dev-packages/e2e-tests/test-applications/nitro-3/tests/isolation.test.ts b/dev-packages/e2e-tests/test-applications/nitro-3/tests/isolation.test.ts
new file mode 100644
index 000000000000..7234fa0948ca
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nitro-3/tests/isolation.test.ts
@@ -0,0 +1,25 @@
+import { expect, test } from '@playwright/test';
+import { waitForError, waitForTransaction } from '@sentry-internal/test-utils';
+
+test('Isolation scope prevents tag leaking between requests', async ({ request }) => {
+ const transactionEventPromise = waitForTransaction('nitro-3', event => {
+ return event?.transaction === 'GET /api/test-isolation/:id';
+ });
+
+ const errorPromise = waitForError('nitro-3', event => {
+ return !event.type && !!event.exception?.values?.some(v => v.value === 'Isolation test error');
+ });
+
+ await request.get('/api/test-isolation/1').catch(() => {
+ // noop - route throws
+ });
+
+ const transactionEvent = await transactionEventPromise;
+ const error = await errorPromise;
+
+ // Assert that isolation scope works properly
+ expect(error.tags?.['my-isolated-tag']).toBe(true);
+ expect(error.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
+ expect(transactionEvent.tags?.['my-isolated-tag']).toBe(true);
+ expect(transactionEvent.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
+});
diff --git a/dev-packages/e2e-tests/test-applications/nitro-3/tests/middleware.test.ts b/dev-packages/e2e-tests/test-applications/nitro-3/tests/middleware.test.ts
new file mode 100644
index 000000000000..eec281d28f98
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nitro-3/tests/middleware.test.ts
@@ -0,0 +1,40 @@
+import { expect, test } from '@playwright/test';
+import { waitForError, waitForTransaction } from '@sentry-internal/test-utils';
+
+test('Creates middleware spans for requests', async ({ request }) => {
+ const transactionEventPromise = waitForTransaction('nitro-3', event => {
+ return event?.transaction === 'GET /api/test-transaction';
+ });
+
+ const response = await request.get('/api/test-transaction');
+
+ expect(response.headers()['x-sentry-test-middleware']).toBe('executed');
+
+ const transactionEvent = await transactionEventPromise;
+
+ // h3 middleware spans have origin auto.http.nitro.h3 and op middleware.nitro
+ const h3MiddlewareSpans = transactionEvent.spans?.filter(
+ span => span.origin === 'auto.http.nitro.h3' && span.op === 'middleware.nitro',
+ );
+ expect(h3MiddlewareSpans?.length).toBeGreaterThanOrEqual(1);
+});
+
+test('Captures errors thrown in middleware with error status on span', async ({ request }) => {
+ const errorEventPromise = waitForError('nitro-3', event => {
+ return !event.type && !!event.exception?.values?.some(v => v.value === 'Middleware error');
+ });
+
+ const transactionEventPromise = waitForTransaction('nitro-3', event => {
+ return event?.transaction === 'GET /api/test-transaction' && event?.contexts?.trace?.status === 'internal_error';
+ });
+
+ await request.get('/api/test-transaction?middleware-error=1');
+
+ const errorEvent = await errorEventPromise;
+ expect(errorEvent.exception?.values?.some(v => v.value === 'Middleware error')).toBe(true);
+
+ const transactionEvent = await transactionEventPromise;
+
+ // The transaction span should have error status
+ expect(transactionEvent.contexts?.trace?.status).toBe('internal_error');
+});
diff --git a/dev-packages/e2e-tests/test-applications/nitro-3/tests/span-nesting.test.ts b/dev-packages/e2e-tests/test-applications/nitro-3/tests/span-nesting.test.ts
new file mode 100644
index 000000000000..090f8af36fb2
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nitro-3/tests/span-nesting.test.ts
@@ -0,0 +1,146 @@
+import { expect, test } from '@playwright/test';
+import { waitForTransaction } from '@sentry-internal/test-utils';
+
+test('Span nesting: all spans share the same trace_id', async ({ request }) => {
+ const transactionEventPromise = waitForTransaction('nitro-3', event => {
+ return event?.transaction === 'GET /api/test-nesting';
+ });
+
+ await request.get('/api/test-nesting');
+
+ const event = await transactionEventPromise;
+ const traceId = event.contexts?.trace?.trace_id;
+
+ expect(traceId).toMatch(/[a-f0-9]{32}/);
+
+ // Every child span must belong to the same trace
+ for (const span of event.spans ?? []) {
+ expect(span.trace_id).toBe(traceId);
+ }
+});
+
+test('Span nesting: h3 middleware spans are children of the srvx request span', async ({ request }) => {
+ const transactionEventPromise = waitForTransaction('nitro-3', event => {
+ return event?.transaction === 'GET /api/test-nesting';
+ });
+
+ await request.get('/api/test-nesting');
+
+ const event = await transactionEventPromise;
+
+ // Find the srvx request span
+ const srvxSpan = event.spans?.find(span => span.origin === 'auto.http.nitro.srvx' && span.op === 'http.server');
+ expect(srvxSpan).toBeDefined();
+
+ // All h3 middleware spans should be children of the srvx span
+ const h3Spans = event.spans?.filter(span => span.origin === 'auto.http.nitro.h3');
+ expect(h3Spans?.length).toBeGreaterThanOrEqual(1);
+
+ for (const span of h3Spans ?? []) {
+ expect(span.parent_span_id).toBe(srvxSpan!.span_id);
+ }
+});
+
+test('Span nesting: manual startSpan calls inside route handler are children of the srvx request span', async ({
+ request,
+}) => {
+ const transactionEventPromise = waitForTransaction('nitro-3', event => {
+ return event?.transaction === 'GET /api/test-nesting';
+ });
+
+ await request.get('/api/test-nesting');
+
+ const event = await transactionEventPromise;
+
+ // Find the srvx request span — this is the parent of all h3 and manual spans
+ const srvxSpan = event.spans?.find(span => span.origin === 'auto.http.nitro.srvx' && span.op === 'http.server');
+ expect(srvxSpan).toBeDefined();
+ const srvxSpanId = srvxSpan!.span_id;
+
+ // Find the manually created db spans
+ const dbSelectSpan = event.spans?.find(span => span.op === 'db' && span.description === 'db.select');
+ const dbInsertSpan = event.spans?.find(span => span.op === 'db' && span.description === 'db.insert');
+ expect(dbSelectSpan).toBeDefined();
+ expect(dbInsertSpan).toBeDefined();
+
+ // FIXME: Once nitro's h3 tracing plugin emits a separate span for route handlers (type: "route"),
+ // the db spans should be children of the h3 route handler span, not the srvx span directly.
+ // Currently nitro bypasses h3's ~routes for file-based routing, so h3 only emits middleware spans.
+ // Both db spans should be children of the srvx request span
+ expect(dbSelectSpan!.parent_span_id).toBe(srvxSpanId);
+ expect(dbInsertSpan!.parent_span_id).toBe(srvxSpanId);
+
+ // Both db spans should be siblings (same parent)
+ expect(dbSelectSpan!.parent_span_id).toBe(dbInsertSpan!.parent_span_id);
+
+ // The serialize span should be nested inside the db.insert span
+ const serializeSpan = event.spans?.find(span => span.op === 'serialize' && span.description === 'db.serialize');
+ expect(serializeSpan).toBeDefined();
+ expect(serializeSpan!.parent_span_id).toBe(dbInsertSpan!.span_id);
+});
+
+// FIXME: Nitro's file-based routing bypasses h3's ~routes, so h3's tracing plugin never wraps
+// route handlers with type: "route". Once this is fixed upstream or we add our own wrapping,
+// uncomment these tests to verify the h3 route handler span exists and is the parent of manual spans.
+//
+// test('Span nesting: h3 route handler span is a child of the srvx request span', async ({ request }) => {
+// const transactionEventPromise = waitForTransaction('nitro-3', event => {
+// return event?.transaction === 'GET /api/test-nesting';
+// });
+//
+// await request.get('/api/test-nesting');
+//
+// const event = await transactionEventPromise;
+//
+// const srvxSpan = event.spans?.find(span => span.origin === 'auto.http.nitro.srvx' && span.op === 'http.server');
+// expect(srvxSpan).toBeDefined();
+//
+// const h3HandlerSpan = event.spans?.find(
+// span => span.origin === 'auto.http.nitro.h3' && span.op === 'http.server',
+// );
+// expect(h3HandlerSpan).toBeDefined();
+// expect(h3HandlerSpan!.parent_span_id).toBe(srvxSpan!.span_id);
+// });
+//
+// test('Span nesting: manual startSpan calls are children of the h3 route handler span', async ({ request }) => {
+// const transactionEventPromise = waitForTransaction('nitro-3', event => {
+// return event?.transaction === 'GET /api/test-nesting';
+// });
+//
+// await request.get('/api/test-nesting');
+//
+// const event = await transactionEventPromise;
+//
+// const h3HandlerSpan = event.spans?.find(
+// span => span.origin === 'auto.http.nitro.h3' && span.op === 'http.server',
+// );
+// expect(h3HandlerSpan).toBeDefined();
+//
+// const dbSelectSpan = event.spans?.find(span => span.op === 'db' && span.description === 'db.select');
+// const dbInsertSpan = event.spans?.find(span => span.op === 'db' && span.description === 'db.insert');
+// expect(dbSelectSpan!.parent_span_id).toBe(h3HandlerSpan!.span_id);
+// expect(dbInsertSpan!.parent_span_id).toBe(h3HandlerSpan!.span_id);
+// });
+
+test('Span nesting: middleware spans start before manual spans in the span tree', async ({ request }) => {
+ const transactionEventPromise = waitForTransaction('nitro-3', event => {
+ return event?.transaction === 'GET /api/test-nesting';
+ });
+
+ await request.get('/api/test-nesting');
+
+ const event = await transactionEventPromise;
+
+ // Middleware spans should start before the manual db spans
+ const middlewareSpans = event.spans?.filter(span => span.op === 'middleware.nitro') ?? [];
+ const dbSpans = event.spans?.filter(span => span.op === 'db') ?? [];
+
+ expect(middlewareSpans.length).toBeGreaterThanOrEqual(1);
+ expect(dbSpans.length).toBeGreaterThanOrEqual(1);
+
+ const earliestMiddlewareStart = Math.min(...middlewareSpans.map(s => s.start_timestamp));
+ const earliestDbStart = Math.min(...dbSpans.map(s => s.start_timestamp));
+
+ // Middleware should start before the db spans
+ expect(earliestMiddlewareStart).toBeLessThanOrEqual(earliestDbStart);
+});
diff --git a/dev-packages/e2e-tests/test-applications/nitro-3/tests/trace-propagation.test.ts b/dev-packages/e2e-tests/test-applications/nitro-3/tests/trace-propagation.test.ts
new file mode 100644
index 000000000000..705521ad759d
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nitro-3/tests/trace-propagation.test.ts
@@ -0,0 +1,16 @@
+import { expect, test } from '@playwright/test';
+import { waitForTransaction } from '@sentry-internal/test-utils';
+
+test('Propagates server trace to client pageload via Server-Timing headers', async ({ page }) => {
+ const clientTxnPromise = waitForTransaction('nitro-3', event => {
+ return event?.contexts?.trace?.op === 'pageload';
+ });
+
+ await page.goto('/');
+
+ const clientTxn = await clientTxnPromise;
+
+ expect(clientTxn.contexts?.trace?.trace_id).toBeDefined();
+ expect(clientTxn.contexts?.trace?.trace_id).toMatch(/[a-f0-9]{32}/);
+ expect(clientTxn.contexts?.trace?.op).toBe('pageload');
+});
diff --git a/dev-packages/e2e-tests/test-applications/nitro-3/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/nitro-3/tests/transactions.test.ts
new file mode 100644
index 000000000000..48de9c4349df
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nitro-3/tests/transactions.test.ts
@@ -0,0 +1,78 @@
+import { expect, test } from '@playwright/test';
+import { waitForTransaction } from '@sentry-internal/test-utils';
+
+test('Sends a transaction event for a successful route', async ({ request }) => {
+ const transactionEventPromise = waitForTransaction('nitro-3', transactionEvent => {
+ return transactionEvent?.transaction === 'GET /api/test-transaction';
+ });
+
+ await request.get('/api/test-transaction');
+
+ const transactionEvent = await transactionEventPromise;
+
+ expect(transactionEvent).toEqual(
+ expect.objectContaining({
+ transaction: 'GET /api/test-transaction',
+ type: 'transaction',
+ }),
+ );
+
+ // srvx.request creates a span for the request
+ const srvxSpans = transactionEvent.spans?.filter(span => span.origin === 'auto.http.nitro.srvx');
+ expect(srvxSpans?.length).toBeGreaterThanOrEqual(1);
+
+ // h3 creates a child span for the route handler
+ const h3Spans = transactionEvent.spans?.filter(span => span.origin === 'auto.http.nitro.h3');
+ expect(h3Spans?.length).toBeGreaterThanOrEqual(1);
+});
+
+test('Sets correct HTTP status code on transaction', async ({ request }) => {
+ const transactionEventPromise = waitForTransaction('nitro-3', transactionEvent => {
+ return transactionEvent?.transaction === 'GET /api/test-transaction';
+ });
+
+ await request.get('/api/test-transaction');
+
+ const transactionEvent = await transactionEventPromise;
+
+ expect(transactionEvent.contexts?.trace?.data).toEqual(
+ expect.objectContaining({
+ 'http.response.status_code': 200,
+ }),
+ );
+
+ expect(transactionEvent.contexts?.trace?.status).toBe('ok');
+});
+
+test('Uses parameterized route for transaction name', async ({ request }) => {
+ const transactionEventPromise = waitForTransaction('nitro-3', transactionEvent => {
+ return transactionEvent?.transaction === 'GET /api/test-param/:id';
+ });
+
+ await request.get('/api/test-param/123');
+
+ const transactionEvent = await transactionEventPromise;
+
+ expect(transactionEvent).toEqual(
+ expect.objectContaining({
+ transaction: 'GET /api/test-param/:id',
+ transaction_info: expect.objectContaining({ source: 'route' }),
+ type: 'transaction',
+ }),
+ );
+
+ expect(transactionEvent.contexts?.trace?.data).toEqual(
+ expect.objectContaining({
+ 'http.route': '/api/test-param/:id',
+ }),
+ );
+});
+
+test('Sets Server-Timing response headers for trace propagation', async ({ request }) => {
+ const response = await request.get('/api/test-transaction');
+ const headers = response.headers();
+
+ expect(headers['server-timing']).toBeDefined();
+ expect(headers['server-timing']).toContain('sentry-trace;desc="');
+ expect(headers['server-timing']).toContain('baggage;desc="');
+});
diff --git a/dev-packages/e2e-tests/test-applications/nitro-3/tsconfig.json b/dev-packages/e2e-tests/test-applications/nitro-3/tsconfig.json
new file mode 100644
index 000000000000..b9a951fbebb1
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nitro-3/tsconfig.json
@@ -0,0 +1,14 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "paths": {
+ "~/*": ["./*"]
+ }
+ },
+ "include": ["src/**/*.ts", "routes/**/*.ts", "vite.config.ts"]
+}
diff --git a/dev-packages/e2e-tests/test-applications/nitro-3/vite.config.ts b/dev-packages/e2e-tests/test-applications/nitro-3/vite.config.ts
new file mode 100644
index 000000000000..d488f8298777
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nitro-3/vite.config.ts
@@ -0,0 +1,15 @@
+import { withSentryConfig } from '@sentry/nitro';
+import { nitro } from 'nitro/vite';
+import { defineConfig } from 'vite';
+
+export default defineConfig({
+ plugins: [
+ nitro(
+ // FIXME: Nitro plugin has a type issue
+ // @ts-expect-error
+ withSentryConfig({
+ serverDir: './server',
+ }),
+ ),
+ ],
+});
diff --git a/packages/nitro/package.json b/packages/nitro/package.json
index cdce5bff3685..2f5ee0d52fa8 100644
--- a/packages/nitro/package.json
+++ b/packages/nitro/package.json
@@ -39,9 +39,11 @@
},
"dependencies": {
"@sentry/core": "10.49.0",
- "@sentry/node": "10.49.0"
+ "@sentry/node": "10.49.0",
+ "@sentry/opentelemetry": "10.49.0"
},
"devDependencies": {
+ "h3": "^2.0.1-rc.13",
"nitro": "^3.0.260415-beta"
},
"scripts": {
diff --git a/packages/nitro/rollup.npm.config.mjs b/packages/nitro/rollup.npm.config.mjs
index f92d004777ad..1e41829a3a3a 100644
--- a/packages/nitro/rollup.npm.config.mjs
+++ b/packages/nitro/rollup.npm.config.mjs
@@ -3,11 +3,10 @@ import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollu
export default [
...makeNPMConfigVariants(
makeBaseNPMConfig({
- entrypoints: ['src/index.ts'],
+ entrypoints: ['src/index.ts', 'src/runtime/plugins/server.ts'],
packageSpecificConfig: {
- external: [/^nitro/],
+ external: [/^nitro/, /^h3/, /^srvx/, /^@sentry\/opentelemetry/],
},
}),
- { emitCjs: false },
),
];
diff --git a/packages/nitro/src/config.ts b/packages/nitro/src/config.ts
index 0a945bcdd82e..219eb453fb18 100644
--- a/packages/nitro/src/config.ts
+++ b/packages/nitro/src/config.ts
@@ -12,9 +12,7 @@ type SentryNitroOptions = {
* @returns The modified config to be exported
*/
export function withSentryConfig(config: NitroConfig, moduleOptions?: SentryNitroOptions): NitroConfig {
- setupSentryNitroModule(config, moduleOptions);
-
- return config;
+ return setupSentryNitroModule(config, moduleOptions);
}
/**
@@ -25,6 +23,10 @@ export function setupSentryNitroModule(
_moduleOptions?: SentryNitroOptions,
_serverConfigFile?: string,
): NitroConfig {
+ if (!config.tracingChannel) {
+ config.tracingChannel = true;
+ }
+
config.modules = config.modules || [];
config.modules.push(createNitroModule());
diff --git a/packages/nitro/src/instruments/instrumentServer.ts b/packages/nitro/src/instruments/instrumentServer.ts
new file mode 100644
index 000000000000..ec891055558b
--- /dev/null
+++ b/packages/nitro/src/instruments/instrumentServer.ts
@@ -0,0 +1,12 @@
+import type { Nitro } from 'nitro/types';
+import { addPlugin } from '../utils/plugin';
+import { createResolver } from '../utils/resolver';
+
+/**
+ * Sets up the Nitro server instrumentation plugin
+ * @param nitro - The Nitro instance.
+ */
+export function instrumentServer(nitro: Nitro): void {
+ const moduleResolver = createResolver(import.meta.url);
+ addPlugin(nitro, moduleResolver.resolve('../runtime/plugins/server'));
+}
diff --git a/packages/nitro/src/module.ts b/packages/nitro/src/module.ts
index 9c2c900b1717..1f0955301813 100644
--- a/packages/nitro/src/module.ts
+++ b/packages/nitro/src/module.ts
@@ -1,4 +1,5 @@
import type { NitroModule } from 'nitro/types';
+import { instrumentServer } from './instruments/instrumentServer';
/**
* Creates a Nitro module to setup the Sentry SDK.
@@ -6,8 +7,8 @@ import type { NitroModule } from 'nitro/types';
export function createNitroModule(): NitroModule {
return {
name: 'sentry',
- setup: _nitro => {
- // TODO: Setup the Sentry SDK.
+ setup: nitro => {
+ instrumentServer(nitro);
},
};
}
diff --git a/packages/nitro/src/runtime/README.md b/packages/nitro/src/runtime/README.md
new file mode 100644
index 000000000000..43c190e6d015
--- /dev/null
+++ b/packages/nitro/src/runtime/README.md
@@ -0,0 +1,5 @@
+# Nitro Runtime
+
+This directory contains the runtime code for Nitro, this includes plugins or any runtime code they may use.
+
+Do not mix runtime code with other code, this directory will be packaged with the SDK and shipped as-is.
diff --git a/packages/nitro/src/runtime/hooks/captureErrorHook.ts b/packages/nitro/src/runtime/hooks/captureErrorHook.ts
new file mode 100644
index 000000000000..38ab4fd699fa
--- /dev/null
+++ b/packages/nitro/src/runtime/hooks/captureErrorHook.ts
@@ -0,0 +1,84 @@
+import {
+ captureException,
+ flushIfServerless,
+ getActiveSpan,
+ getClient,
+ getCurrentScope,
+ getRootSpan,
+ parseUrl,
+ SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
+} from '@sentry/core';
+import { HTTPError } from 'h3';
+import type { CapturedErrorContext } from 'nitro/types';
+
+/**
+ * Extracts the relevant context information from the error context (HTTPEvent in Nitro Error)
+ * and creates a structured context object.
+ */
+function extractErrorContext(errorContext: CapturedErrorContext | undefined): Record {
+ const ctx: Record = {};
+
+ if (!errorContext) {
+ return ctx;
+ }
+
+ if (errorContext.event) {
+ ctx.method = errorContext.event.req.method;
+ ctx.path = parseUrl(errorContext.event.req.url).path;
+ }
+
+ if (Array.isArray(errorContext.tags)) {
+ ctx.tags = errorContext.tags;
+ }
+
+ return ctx;
+}
+
+/**
+ * Hook that can be added in a Nitro plugin. It captures an error and sends it to Sentry.
+ */
+export async function captureErrorHook(error: Error, errorContext: CapturedErrorContext): Promise {
+ const sentryClient = getClient();
+ const sentryClientOptions = sentryClient?.getOptions();
+
+ if (
+ sentryClientOptions &&
+ 'enableNitroErrorHandler' in sentryClientOptions &&
+ sentryClientOptions.enableNitroErrorHandler === false
+ ) {
+ return;
+ }
+
+ // Do not report HTTPErrors with 3xx or 4xx status codes
+ if (HTTPError.isError(error) && error.status >= 300 && error.status < 500) {
+ return;
+ }
+
+ const method = errorContext.event?.req.method ?? '';
+ let path: string | null = null;
+
+ try {
+ if (errorContext.event?.req.url) {
+ path = new URL(errorContext.event.req.url).pathname;
+ }
+ } catch {
+ // If URL parsing fails, leave path as null
+ }
+
+ if (path) {
+ getCurrentScope().setTransactionName(`${method} ${path}`);
+ const activeSpan = getActiveSpan();
+ const activeRootSpan = activeSpan ? getRootSpan(activeSpan) : undefined;
+ activeRootSpan?.updateName(`${method} ${path}`);
+ activeRootSpan?.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route');
+ }
+
+ const structuredContext = extractErrorContext(errorContext);
+
+ captureException(error, {
+ captureContext: { contexts: { nitro: structuredContext } },
+ mechanism: { handled: false, type: 'auto.function.nitro.captureErrorHook' },
+ });
+
+ await flushIfServerless();
+}
diff --git a/packages/nitro/src/runtime/hooks/captureTracingEvents.ts b/packages/nitro/src/runtime/hooks/captureTracingEvents.ts
new file mode 100644
index 000000000000..53de89e143ef
--- /dev/null
+++ b/packages/nitro/src/runtime/hooks/captureTracingEvents.ts
@@ -0,0 +1,280 @@
+import {
+ captureException,
+ getActiveSpan,
+ getClient,
+ getHttpSpanDetailsFromUrlObject,
+ getRootSpan,
+ GLOBAL_OBJ,
+ httpHeadersToSpanAttributes,
+ parseStringToURLObject,
+ SEMANTIC_ATTRIBUTE_SENTRY_OP,
+ SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
+ SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
+ setHttpStatus,
+ type Span,
+ SPAN_STATUS_ERROR,
+ startSpanManual,
+ updateSpanName,
+} from '@sentry/core';
+import { tracingChannel, type TracingChannelContextWithSpan } from '@sentry/opentelemetry/tracingChannel';
+import type { TracingRequestEvent as H3TracingRequestEvent } from 'h3/tracing';
+import type { RequestEvent as SrvxRequestEvent } from 'srvx/tracing';
+import { setServerTimingHeaders } from './setServerTimingHeaders';
+
+/**
+ * Global object with the trace channels
+ */
+const globalWithTraceChannels = GLOBAL_OBJ as typeof GLOBAL_OBJ & {
+ __SENTRY_NITRO_HTTP_CHANNELS_INSTRUMENTED__: boolean;
+};
+
+/**
+ * Captures tracing events emitted by Nitro tracing channels.
+ */
+export function captureTracingEvents(): void {
+ if (globalWithTraceChannels.__SENTRY_NITRO_HTTP_CHANNELS_INSTRUMENTED__) {
+ return;
+ }
+
+ setupH3TracingChannels();
+ setupSrvxTracingChannels();
+ globalWithTraceChannels.__SENTRY_NITRO_HTTP_CHANNELS_INSTRUMENTED__ = true;
+}
+
+/**
+ * No-op function to satisfy the tracing channel subscribe callbacks
+ */
+const NOOP = (): void => {};
+
+/**
+ * Extracts the HTTP status code from a tracing channel result.
+ * The result is the return value of the traced handler, which is a Response for srvx
+ * and may or may not be a Response for h3.
+ */
+function getResponseStatusCode(result: unknown): number | undefined {
+ if (result && typeof result === 'object' && 'status' in result && typeof result.status === 'number') {
+ return result.status;
+ }
+ return undefined;
+}
+
+function onTraceEnd(data: TracingChannelContextWithSpan<{ result?: unknown }>): void {
+ const statusCode = getResponseStatusCode(data.result);
+ if (data._sentrySpan && statusCode !== undefined) {
+ setHttpStatus(data._sentrySpan, statusCode);
+ }
+
+ data._sentrySpan?.end();
+}
+
+function onTraceError(data: TracingChannelContextWithSpan<{ error: unknown }>): void {
+ captureException(data.error, { mechanism: { type: 'auto.http.nitro.onTraceError', handled: false } });
+ data._sentrySpan?.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' });
+ data._sentrySpan?.end();
+}
+
+/**
+ * Extracts the parameterized route pattern from the h3 event context.
+ */
+function getParameterizedRoute(event: H3TracingRequestEvent['event']): string | undefined {
+ const matchedRoute = event.context?.matchedRoute;
+ if (!matchedRoute) {
+ return undefined;
+ }
+
+ const routePath = matchedRoute.route;
+
+ // Skip catch-all routes as they're not useful for transaction grouping
+ if (!routePath || routePath === '/**') {
+ return undefined;
+ }
+
+ return routePath;
+}
+
+function setupH3TracingChannels(): void {
+ const h3Channel = tracingChannel('h3.request', data => {
+ const parsedUrl = parseStringToURLObject(data.event.url.href);
+ const routePattern = getParameterizedRoute(data.event);
+
+ const [spanName, urlAttributes] = getHttpSpanDetailsFromUrlObject(
+ parsedUrl,
+ 'server',
+ 'auto.http.nitro.h3',
+ { method: data.event.req.method },
+ routePattern,
+ );
+
+ return startSpanManual(
+ {
+ name: spanName,
+ attributes: {
+ ...urlAttributes,
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.nitro.h3',
+ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: data?.type === 'middleware' ? 'middleware.nitro' : 'http.server',
+ },
+ },
+ span => {
+ setParameterizedRouteAttributes(span, data.event);
+
+ return span;
+ },
+ );
+ });
+
+ h3Channel.subscribe({
+ start: (data: H3TracingRequestEvent) => {
+ setServerTimingHeaders(data.event);
+ },
+ asyncStart: NOOP,
+ end: NOOP,
+ asyncEnd: (data: TracingChannelContextWithSpan) => {
+ onTraceEnd(data);
+
+ if (!data._sentrySpan) {
+ return;
+ }
+
+ // Update the root span (srvx transaction) with the parameterized route name.
+ // The srvx span is created before h3 resolves the route, so it initially has the raw URL.
+ // Note: data.type is always 'middleware' in asyncEnd regardless of handler type,
+ // so we rely on getParameterizedRoute() to filter out catch-all routes instead.
+ const rootSpan = getRootSpan(data._sentrySpan);
+ if (rootSpan && rootSpan !== data._sentrySpan) {
+ const routePattern = getParameterizedRoute(data.event);
+ if (routePattern) {
+ const method = data.event.req.method || 'GET';
+ updateSpanName(rootSpan, `${method} ${routePattern}`);
+ rootSpan.setAttributes({
+ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
+ 'http.route': routePattern,
+ });
+ }
+ }
+ },
+ error: onTraceError,
+ });
+}
+
+function setupSrvxTracingChannels(): void {
+ // Store the parent span per-request so middleware and fetch share the same parent.
+ // WeakMap ensures per-request isolation in concurrent environments and automatic cleanup.
+ const requestParentSpans = new WeakMap();
+
+ const fetchChannel = tracingChannel('srvx.request', data => {
+ const parsedUrl = data.request._url ? parseStringToURLObject(data.request._url.href) : undefined;
+ const [spanName, urlAttributes] = getHttpSpanDetailsFromUrlObject(parsedUrl, 'server', 'auto.http.nitro.srvx', {
+ method: data.request.method,
+ });
+
+ const sendDefaultPii = getClient()?.getOptions().sendDefaultPii ?? false;
+ const headerAttributes = httpHeadersToSpanAttributes(
+ Object.fromEntries(data.request.headers.entries()),
+ sendDefaultPii,
+ );
+
+ return startSpanManual(
+ {
+ name: spanName,
+ attributes: {
+ ...urlAttributes,
+ ...headerAttributes,
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.nitro.srvx',
+ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: data.middleware ? 'middleware.nitro' : 'http.server',
+ 'server.port': data.server.options.port,
+ },
+ // Use the same parent span as middleware to make them siblings
+ parentSpan: requestParentSpans.get(data.request) || undefined,
+ },
+ span => span,
+ );
+ });
+
+ // Subscribe to events (span already created in bindStore)
+ fetchChannel.subscribe({
+ start: () => {},
+ asyncStart: () => {},
+ end: () => {},
+ asyncEnd: data => {
+ onTraceEnd(data);
+
+ // Clean up parent span reference after the fetch handler completes.
+ requestParentSpans.delete(data.request);
+ },
+ error: data => {
+ onTraceError(data);
+ // Clean up parent span reference on error too
+ requestParentSpans.delete(data.request);
+ },
+ });
+
+ const middlewareChannel = tracingChannel('srvx.middleware', data => {
+ // For the first middleware, capture the current parent span per-request
+ if (data.middleware?.index === 0) {
+ const activeSpan = getActiveSpan();
+ if (activeSpan) {
+ requestParentSpans.set(data.request, activeSpan);
+ }
+ }
+
+ const parsedUrl = data.request._url ? parseStringToURLObject(data.request._url.href) : undefined;
+ const [, urlAttributes] = getHttpSpanDetailsFromUrlObject(parsedUrl, 'server', 'auto.http.nitro.srvx', {
+ method: data.request.method,
+ });
+
+ // Create span as a child of the original parent, not the previous middleware
+ return startSpanManual(
+ {
+ name: `${data.middleware?.handler.name ?? 'unknown'} - ${data.request.method} ${data.request._url?.pathname}`,
+ attributes: {
+ ...urlAttributes,
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.nitro.srvx',
+ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'middleware.nitro',
+ },
+ parentSpan: requestParentSpans.get(data.request) || undefined,
+ },
+ span => span,
+ );
+ });
+
+ // Subscribe to events (span already created in bindStore)
+ middlewareChannel.subscribe({
+ start: () => {},
+ asyncStart: () => {},
+ end: () => {},
+ asyncEnd: onTraceEnd,
+ error: onTraceError,
+ });
+}
+
+/**
+ * Sets the parameterized route attributes on the span.
+ */
+function setParameterizedRouteAttributes(span: Span, event: H3TracingRequestEvent['event']): void {
+ const rootSpan = getRootSpan(span);
+ if (!rootSpan) {
+ return;
+ }
+
+ const matchedRoutePath = getParameterizedRoute(event);
+ if (!matchedRoutePath) {
+ return;
+ }
+
+ rootSpan.setAttributes({
+ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
+ 'http.route': matchedRoutePath,
+ });
+
+ const params = event.context?.params;
+
+ if (params && typeof params === 'object') {
+ Object.entries(params).forEach(([key, value]) => {
+ // Based on this convention: https://getsentry.github.io/sentry-conventions/generated/attributes/url.html#urlpathparameterkey
+ rootSpan.setAttributes({
+ [`url.path.parameter.${key}`]: String(value),
+ [`params.${key}`]: String(value),
+ });
+ });
+ }
+}
diff --git a/packages/nitro/src/runtime/hooks/setServerTimingHeaders.ts b/packages/nitro/src/runtime/hooks/setServerTimingHeaders.ts
new file mode 100644
index 000000000000..4573f8171c19
--- /dev/null
+++ b/packages/nitro/src/runtime/hooks/setServerTimingHeaders.ts
@@ -0,0 +1,27 @@
+import { getTraceData } from '@sentry/core';
+import type { TracingRequestEvent as H3TracingRequestEvent } from 'h3/tracing';
+
+/**
+ * Sets Server-Timing response headers for trace propagation to the client.
+ * The browser SDK reads these via the Performance API to connect pageload traces.
+ */
+export function setServerTimingHeaders(event: H3TracingRequestEvent['event']): void {
+ if (event.context._sentryServerTimingSet) {
+ return;
+ }
+
+ const headers = event.res?.headers;
+ if (!headers) {
+ return;
+ }
+
+ const traceData = getTraceData();
+ if (traceData['sentry-trace']) {
+ headers.append('Server-Timing', `sentry-trace;desc="${traceData['sentry-trace']}"`);
+ }
+ if (traceData.baggage) {
+ headers.append('Server-Timing', `baggage;desc="${traceData.baggage}"`);
+ }
+
+ event.context._sentryServerTimingSet = true;
+}
diff --git a/packages/nitro/src/runtime/plugins/server.ts b/packages/nitro/src/runtime/plugins/server.ts
new file mode 100644
index 000000000000..2feee84bcc55
--- /dev/null
+++ b/packages/nitro/src/runtime/plugins/server.ts
@@ -0,0 +1,9 @@
+import { definePlugin } from 'nitro';
+import { captureErrorHook } from '../hooks/captureErrorHook';
+import { captureTracingEvents } from '../hooks/captureTracingEvents';
+
+export default definePlugin(nitroApp => {
+ nitroApp.hooks.hook('error', captureErrorHook);
+
+ captureTracingEvents();
+});
diff --git a/packages/nitro/test/runtime/hooks/captureErrorHook.test.ts b/packages/nitro/test/runtime/hooks/captureErrorHook.test.ts
new file mode 100644
index 000000000000..804ef569a619
--- /dev/null
+++ b/packages/nitro/test/runtime/hooks/captureErrorHook.test.ts
@@ -0,0 +1,168 @@
+import * as SentryCore from '@sentry/core';
+import { HTTPError } from 'h3';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+import { captureErrorHook } from '../../../src/runtime/hooks/captureErrorHook';
+
+vi.mock('@sentry/core', async importOriginal => {
+ const mod = await importOriginal();
+ return {
+ ...(mod as any),
+ captureException: vi.fn(),
+ flushIfServerless: vi.fn(),
+ getClient: vi.fn(),
+ getCurrentScope: vi.fn(() => ({
+ setTransactionName: vi.fn(),
+ })),
+ };
+});
+
+describe('captureErrorHook', () => {
+ const mockErrorContext = {
+ event: {
+ req: { method: 'GET', url: 'http://localhost/test-path' },
+ },
+ };
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ (SentryCore.getClient as any).mockReturnValue({
+ getOptions: () => ({}),
+ });
+ (SentryCore.flushIfServerless as any).mockResolvedValue(undefined);
+ });
+
+ it('should capture regular errors', async () => {
+ const error = new Error('Test error');
+
+ await captureErrorHook(error, mockErrorContext);
+
+ expect(SentryCore.captureException).toHaveBeenCalledWith(
+ error,
+ expect.objectContaining({
+ mechanism: { handled: false, type: 'auto.function.nitro.captureErrorHook' },
+ }),
+ );
+ });
+
+ it('should include structured context with method and path', async () => {
+ const error = new Error('Test error');
+
+ await captureErrorHook(error, mockErrorContext);
+
+ expect(SentryCore.captureException).toHaveBeenCalledWith(
+ error,
+ expect.objectContaining({
+ captureContext: {
+ contexts: {
+ nitro: { method: 'GET', path: '/test-path' },
+ },
+ },
+ }),
+ );
+ });
+
+ it('should set transaction name from method and path', async () => {
+ const mockSetTransactionName = vi.fn();
+ (SentryCore.getCurrentScope as any).mockReturnValue({
+ setTransactionName: mockSetTransactionName,
+ });
+
+ const error = new Error('Test error');
+
+ await captureErrorHook(error, mockErrorContext);
+
+ expect(mockSetTransactionName).toHaveBeenCalledWith('GET /test-path');
+ });
+
+ it('should skip HTTPError with 4xx status codes', async () => {
+ const error = new HTTPError({ status: 404, message: 'Not found' });
+
+ await captureErrorHook(error, mockErrorContext);
+
+ expect(SentryCore.captureException).not.toHaveBeenCalled();
+ });
+
+ it('should skip HTTPError with 3xx status codes', async () => {
+ const error = new HTTPError({ status: 302, message: 'Redirect' });
+
+ await captureErrorHook(error, mockErrorContext);
+
+ expect(SentryCore.captureException).not.toHaveBeenCalled();
+ });
+
+ it('should capture HTTPError with 5xx status codes', async () => {
+ const error = new HTTPError({ status: 500, message: 'Server error' });
+
+ await captureErrorHook(error, mockErrorContext);
+
+ expect(SentryCore.captureException).toHaveBeenCalledWith(
+ error,
+ expect.objectContaining({
+ mechanism: { handled: false, type: 'auto.function.nitro.captureErrorHook' },
+ }),
+ );
+ });
+
+ it('should skip when enableNitroErrorHandler is false', async () => {
+ (SentryCore.getClient as any).mockReturnValue({
+ getOptions: () => ({ enableNitroErrorHandler: false }),
+ });
+
+ const error = new Error('Test error');
+
+ await captureErrorHook(error, mockErrorContext);
+
+ expect(SentryCore.captureException).not.toHaveBeenCalled();
+ });
+
+ it('should call flushIfServerless after capturing', async () => {
+ const error = new Error('Test error');
+
+ await captureErrorHook(error, mockErrorContext);
+
+ expect(SentryCore.flushIfServerless).toHaveBeenCalled();
+ });
+
+ it('should handle missing event in error context', async () => {
+ const error = new Error('Test error');
+ const contextWithoutEvent = {
+ event: undefined,
+ };
+
+ await captureErrorHook(error, contextWithoutEvent);
+
+ expect(SentryCore.captureException).toHaveBeenCalledWith(
+ error,
+ expect.objectContaining({
+ captureContext: {
+ contexts: {
+ nitro: {},
+ },
+ },
+ }),
+ );
+ });
+
+ it('should include tags in structured context when available', async () => {
+ const error = new Error('Test error');
+ const contextWithTags = {
+ event: {
+ req: { method: 'POST', url: 'http://localhost/api/test' },
+ } as any,
+ tags: ['tag1', 'tag2'],
+ };
+
+ await captureErrorHook(error, contextWithTags);
+
+ expect(SentryCore.captureException).toHaveBeenCalledWith(
+ error,
+ expect.objectContaining({
+ captureContext: {
+ contexts: {
+ nitro: { method: 'POST', path: '/api/test', tags: ['tag1', 'tag2'] },
+ },
+ },
+ }),
+ );
+ });
+});
diff --git a/packages/nitro/tsconfig.test.json b/packages/nitro/tsconfig.test.json
index da5a816712e3..c41efeacd92f 100644
--- a/packages/nitro/tsconfig.test.json
+++ b/packages/nitro/tsconfig.test.json
@@ -1,7 +1,7 @@
{
"extends": "./tsconfig.json",
- "include": ["test/**/*"],
+ "include": ["test/**/*", "vite.config.ts"],
"compilerOptions": {
// should include all types from `./tsconfig.json` plus types for all test frameworks used
diff --git a/packages/nitro/vite.config.ts b/packages/nitro/vite.config.ts
new file mode 100644
index 000000000000..4c0db8cdc068
--- /dev/null
+++ b/packages/nitro/vite.config.ts
@@ -0,0 +1,11 @@
+import baseConfig from '../../vite/vite.config';
+
+export default {
+ ...baseConfig,
+ test: {
+ typecheck: {
+ enabled: true,
+ tsconfig: './tsconfig.test.json',
+ },
+ },
+};
diff --git a/packages/opentelemetry/src/tracingChannel.ts b/packages/opentelemetry/src/tracingChannel.ts
index 984986b7cdcb..5548201c5f4c 100644
--- a/packages/opentelemetry/src/tracingChannel.ts
+++ b/packages/opentelemetry/src/tracingChannel.ts
@@ -18,7 +18,7 @@ import { DEBUG_BUILD } from './debug-build';
*/
export type OtelTracingChannelTransform = (data: TData) => Span;
-type WithSpan = TData & { _sentrySpan?: Span };
+export type TracingChannelContextWithSpan = TContext & { _sentrySpan?: Span };
/**
* A TracingChannel whose `subscribe` / `unsubscribe` accept partial subscriber
@@ -26,7 +26,7 @@ type WithSpan = TData & { _sentrySpan?: Span };
*/
export interface OtelTracingChannel<
TData extends object = object,
- TDataWithSpan extends object = WithSpan,
+ TDataWithSpan extends object = TracingChannelContextWithSpan,
> extends Omit, 'subscribe' | 'unsubscribe'> {
subscribe(subscribers: Partial>): void;
unsubscribe(subscribers: Partial>): void;
@@ -52,10 +52,10 @@ interface ContextApi {
export function tracingChannel(
channelNameOrInstance: string,
transformStart: OtelTracingChannelTransform,
-): OtelTracingChannel> {
- const channel = nativeTracingChannel, WithSpan>(
+): OtelTracingChannel> {
+ const channel = nativeTracingChannel, TracingChannelContextWithSpan>(
channelNameOrInstance,
- ) as unknown as OtelTracingChannel>;
+ ) as unknown as OtelTracingChannel>;
let lookup: AsyncLocalStorageLookup | undefined;
try {
@@ -78,7 +78,7 @@ export function tracingChannel(
// Bind the start channel so that each trace invocation runs the transform
// and stores the resulting context (with span) in AsyncLocalStorage.
// @ts-expect-error bindStore types don't account for AsyncLocalStorage of a different generic type
- channel.start.bindStore(otelStorage, (data: WithSpan) => {
+ channel.start.bindStore(otelStorage, (data: TracingChannelContextWithSpan) => {
const span = transformStart(data);
// Store the span on data so downstream event handlers (asyncEnd, error, etc.) can access it.
diff --git a/yarn.lock b/yarn.lock
index 3422b0cb19ad..a3f5b8545c58 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5308,10 +5308,10 @@
"@emnapi/runtime" "^1.4.3"
"@tybys/wasm-util" "^0.10.0"
-"@napi-rs/wasm-runtime@^1.1.4":
- version "1.1.4"
- resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz#a46bbfedc29751b7170c5d23bc1d8ee8c7e3c1e1"
- integrity sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==
+"@napi-rs/wasm-runtime@^1.1.3":
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.3.tgz#1eeb8699770481306e5fcd84471f20fcb6177336"
+ integrity sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ==
dependencies:
"@tybys/wasm-util" "^0.10.1"
@@ -6445,10 +6445,10 @@
resolved "https://registry.yarnpkg.com/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.76.0.tgz#3dbef82283f871c9cb59325c9daf4f740d11a6e9"
integrity sha512-0jLzzmnu8/mqNhKBnNS2lFUbPEzRdj5ReiZwHGHpjma0+ullmmwP2AqSEqx3ssHDK9CpcEMdKOK2LsbCfhHKIA==
-"@oxc-project/types@=0.126.0":
- version "0.126.0"
- resolved "https://registry.yarnpkg.com/@oxc-project/types/-/types-0.126.0.tgz#9d9fa6fe9af5bc6c45996c6d9b9a3b3a4cd500e5"
- integrity sha512-oGfVtjAgwQVVpfBrbtk4e1XDyWHRFta6BS3GWVzrF8xYBT2VGQAk39yJS/wFSMrZqoiCU4oghT3Ch0HaHGIHcQ==
+"@oxc-project/types@=0.124.0":
+ version "0.124.0"
+ resolved "https://registry.yarnpkg.com/@oxc-project/types/-/types-0.124.0.tgz#1dfd7b3fbb98febc2f91b505f48c940db73c8701"
+ integrity sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==
"@oxc-project/types@^0.76.0":
version "0.76.0"
@@ -7160,86 +7160,91 @@
dependencies:
web-streams-polyfill "^3.1.1"
-"@rolldown/binding-android-arm64@1.0.0-rc.16":
- version "1.0.0-rc.16"
- resolved "https://registry.yarnpkg.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.16.tgz#9af7872d363738e7a2aaa1c1be8cad57adf75798"
- integrity sha512-rhY3k7Bsae9qQfOtph2Pm2jZEA+s8Gmjoz4hhmx70K9iMQ/ddeae+xhRQcM5IuVx5ry1+bGfkvMn7D6MJggVSA==
-
-"@rolldown/binding-darwin-arm64@1.0.0-rc.16":
- version "1.0.0-rc.16"
- resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.16.tgz#88f394f20c664ac2c51fe5d5d364b94bbf8ef430"
- integrity sha512-rNz0yK078yrNn3DrdgN+PKiMOW8HfQ92jQiXxwX8yW899ayV00MLVdaCNeVBhG/TbH3ouYVObo8/yrkiectkcQ==
-
-"@rolldown/binding-darwin-x64@1.0.0-rc.16":
- version "1.0.0-rc.16"
- resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.16.tgz#d5350b1d3d13fddb1bc5abb00cadc07787a5d6fa"
- integrity sha512-r/OmdR00HmD4i79Z//xO06uEPOq5hRXdhw7nzkxQxwSavs3PSHa1ijntdpOiZ2mzOQ3fVVu8C1M19FoNM+dMUQ==
-
-"@rolldown/binding-freebsd-x64@1.0.0-rc.16":
- version "1.0.0-rc.16"
- resolved "https://registry.yarnpkg.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.16.tgz#116fe2b906ef658e913bd1419775114dee97c35f"
- integrity sha512-KcRE5w8h0OnjUatG8pldyD14/CQ5Phs1oxfR+3pKDjboHRo9+MkqQaiIZlZRpsxC15paeXme/I127tUa9TXJ6g==
-
-"@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.16":
- version "1.0.0-rc.16"
- resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.16.tgz#3a72b393936c580b40aa66230cdc30ac20fb0409"
- integrity sha512-bT0guA1bpxEJ/ZhTRniQf7rNF8ybvXOuWbNIeLABaV5NGjx4EtOWBTSRGWFU9ZWVkPOZ+HNFP8RMcBokBiZ0Kg==
-
-"@rolldown/binding-linux-arm64-gnu@1.0.0-rc.16":
- version "1.0.0-rc.16"
- resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.16.tgz#3ec9b2dce7b5c29d37272fa3a1aee6159badfb76"
- integrity sha512-+tHktCHWV8BDQSjemUqm/Jl/TPk3QObCTIjmdDy/nlupcujZghmKK2962LYrqFpWu+ai01AN/REOH3NEpqvYQg==
-
-"@rolldown/binding-linux-arm64-musl@1.0.0-rc.16":
- version "1.0.0-rc.16"
- resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.16.tgz#4103d75b7e7f2650d32fef0df01ff5441657b6ee"
- integrity sha512-3fPzdREH806oRLxpTWW1Gt4tQHs0TitZFOECB2xzCFLPKnSOy90gwA7P29cksYilFO6XVRY1kzga0cL2nRjKPg==
-
-"@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.16":
- version "1.0.0-rc.16"
- resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.16.tgz#4bff51a9d0c4c5ec402ac10f41cef22d6a21889c"
- integrity sha512-EKwI1tSrLs7YVw+JPJT/G2dJQ1jl9qlTTTEG0V2Ok/RdOenRfBw2PQdLPyjhIu58ocdBfP7vIRN/pvMsPxs/AQ==
-
-"@rolldown/binding-linux-s390x-gnu@1.0.0-rc.16":
- version "1.0.0-rc.16"
- resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.16.tgz#7b9399eda0b2e49c7e5d2b98172196565de3709f"
- integrity sha512-Uknladnb3Sxqu6SEcqBldQyJUpk8NleooZEc0MbRBJ4inEhRYWZX0NJu12vNf2mqAq7gsofAxHrGghiUYjhaLQ==
-
-"@rolldown/binding-linux-x64-gnu@1.0.0-rc.16":
- version "1.0.0-rc.16"
- resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.16.tgz#82b64f4c9aa018718c27a11fc5f8e9141f1c3276"
- integrity sha512-FIb8+uG49sZBtLTn+zt1AJ20TqVcqWeSIyoVt0or7uAWesgKaHbiBh6OpA/k9v0LTt+PTrb1Lao133kP4uVxkg==
-
-"@rolldown/binding-linux-x64-musl@1.0.0-rc.16":
- version "1.0.0-rc.16"
- resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.16.tgz#710c4bf32715d5564fd7bb39bfbe9195f0e8b9a6"
- integrity sha512-RuERhF9/EgWxZEXYWCOaViUWHIboceK4/ivdtQ3R0T44NjLkIIlGIAVAuCddFxsZ7vnRHtNQUrt2vR2n2slB2w==
-
-"@rolldown/binding-openharmony-arm64@1.0.0-rc.16":
- version "1.0.0-rc.16"
- resolved "https://registry.yarnpkg.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.16.tgz#ab5cc4736ff363c4fad67c017edf4634c036e82a"
- integrity sha512-mXcXnvd9GpazCxeUCCnZ2+YF7nut+ZOEbE4GtaiPtyY6AkhZWbK70y1KK3j+RDhjVq5+U8FySkKRb/+w0EeUwA==
-
-"@rolldown/binding-wasm32-wasi@1.0.0-rc.16":
- version "1.0.0-rc.16"
- resolved "https://registry.yarnpkg.com/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.16.tgz#906dec98ca584cec655a336fca870ac7095fbe93"
- integrity sha512-3Q2KQxnC8IJOLqXmUMoYwyIPZU9hzRbnHaoV3Euz+VVnjZKcY8ktnNP8T9R4/GGQtb27C/UYKABxesKWb8lsvQ==
+"@rolldown/binding-android-arm64@1.0.0-rc.15":
+ version "1.0.0-rc.15"
+ resolved "https://registry.yarnpkg.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.15.tgz#ca20574c469ade7b941f90c9af5e83e7c67f06b7"
+ integrity sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==
+
+"@rolldown/binding-darwin-arm64@1.0.0-rc.15":
+ version "1.0.0-rc.15"
+ resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.15.tgz#ce2c5c7fc4958dfc94783dc09b3d09f3c2e1d072"
+ integrity sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==
+
+"@rolldown/binding-darwin-x64@1.0.0-rc.15":
+ version "1.0.0-rc.15"
+ resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.15.tgz#251ecdf1fdb751031cb6486907c105daaf9dab21"
+ integrity sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==
+
+"@rolldown/binding-freebsd-x64@1.0.0-rc.15":
+ version "1.0.0-rc.15"
+ resolved "https://registry.yarnpkg.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.15.tgz#dbcfe95f409bf671a77bd83bff0fdc877d217728"
+ integrity sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==
+
+"@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15":
+ version "1.0.0-rc.15"
+ resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.15.tgz#ea002b45445be6f9ed1883a834b335bc2ccd510f"
+ integrity sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==
+
+"@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15":
+ version "1.0.0-rc.15"
+ resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.15.tgz#12b96e7e7821a9dc2cd5c670ad56882987ed5c62"
+ integrity sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==
+
+"@rolldown/binding-linux-arm64-musl@1.0.0-rc.15":
+ version "1.0.0-rc.15"
+ resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.15.tgz#738b0f62f0b65bf676dfe48595017f1883859d1f"
+ integrity sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==
+
+"@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15":
+ version "1.0.0-rc.15"
+ resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.15.tgz#3088b9fbc2783033985b558316f87f39281bc533"
+ integrity sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==
+
+"@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15":
+ version "1.0.0-rc.15"
+ resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.15.tgz#ac0aa6f1b72e3151d56c43145a71c745cf862a9a"
+ integrity sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==
+
+"@rolldown/binding-linux-x64-gnu@1.0.0-rc.15":
+ version "1.0.0-rc.15"
+ resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.15.tgz#b8cf27aa5be6da641c22dad5665d0240551d2dec"
+ integrity sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==
+
+"@rolldown/binding-linux-x64-musl@1.0.0-rc.15":
+ version "1.0.0-rc.15"
+ resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.15.tgz#4531f9eca77963935026634ba9b61c2535340534"
+ integrity sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==
+
+"@rolldown/binding-openharmony-arm64@1.0.0-rc.15":
+ version "1.0.0-rc.15"
+ resolved "https://registry.yarnpkg.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.15.tgz#66ff691a65f9325171bced98e353b4cc4b0095c3"
+ integrity sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==
+
+"@rolldown/binding-wasm32-wasi@1.0.0-rc.15":
+ version "1.0.0-rc.15"
+ resolved "https://registry.yarnpkg.com/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.15.tgz#7db6c90aa510eef65d7d0f14e8ca23775e8e5eee"
+ integrity sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==
dependencies:
"@emnapi/core" "1.9.2"
"@emnapi/runtime" "1.9.2"
- "@napi-rs/wasm-runtime" "^1.1.4"
+ "@napi-rs/wasm-runtime" "^1.1.3"
-"@rolldown/binding-win32-arm64-msvc@1.0.0-rc.16":
- version "1.0.0-rc.16"
- resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.16.tgz#19dd3cf898727fad4f9209cf2aae829a789a9348"
- integrity sha512-tj7XRemQcOcFwv7qhpUxMTBbI5mWMlE4c1Omhg5+h8GuLXzyj8HviYgR+bB2DMDgRqUE+jiDleqSCRjx4aYk/Q==
+"@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15":
+ version "1.0.0-rc.15"
+ resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.15.tgz#81f9097abbd4493cc13373b26f5a3da8461dbb47"
+ integrity sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==
-"@rolldown/binding-win32-x64-msvc@1.0.0-rc.16":
- version "1.0.0-rc.16"
- resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.16.tgz#94f8930ac50d62c5d9a1a14855125aa945a14234"
- integrity sha512-PH5DRZT+F4f2PTXRXR8uJxnBq2po/xFtddyabTJVJs/ZYVHqXPEgNIr35IHTEa6bpa0Q8Awg+ymkTaGnKITw4g==
+"@rolldown/binding-win32-x64-msvc@1.0.0-rc.15":
+ version "1.0.0-rc.15"
+ resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.15.tgz#cef11bc89149f3a77771727be75490fbb13ae193"
+ integrity sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==
+
+"@rolldown/pluginutils@1.0.0-rc.15":
+ version "1.0.0-rc.15"
+ resolved "https://registry.yarnpkg.com/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.15.tgz#e75d7731593e195d23710f9ff49bf5c745c96682"
+ integrity sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==
-"@rolldown/pluginutils@1.0.0-rc.16", "@rolldown/pluginutils@^1.0.0-beta.9":
+"@rolldown/pluginutils@^1.0.0-beta.9":
version "1.0.0-rc.16"
resolved "https://registry.yarnpkg.com/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.16.tgz#bc27c8f906309b57c6c10eddb21043fd8e86b87e"
integrity sha512-45+YtqxLYKDWQouLKCrpIZhke+nXxhsw+qAHVzHDVwttyBlHNBVs2K25rDXrZzhpTp9w1FlAlvweV1H++fdZoA==
@@ -18356,7 +18361,7 @@ h3@^1.10.0, h3@^1.12.0, h3@^1.15.3, h3@^1.15.5:
ufo "^1.6.3"
uncrypto "^0.1.3"
-h3@^2.0.1-rc.16, h3@^2.0.1-rc.20:
+h3@^2.0.1-rc.13, h3@^2.0.1-rc.16, h3@^2.0.1-rc.20:
version "2.0.1-rc.20"
resolved "https://registry.yarnpkg.com/h3/-/h3-2.0.1-rc.20.tgz#51050db30afb0b6e69718d88cccc23666fbe8039"
integrity sha512-28ljodXuUp0fZovdiSRq4G9OgrxCztrJe5VdYzXAB7ueRvI7pIUqLU14Xi3XqdYJ/khXjfpUOOD2EQa6CmBgsg==
@@ -26517,28 +26522,28 @@ roarr@^7.0.4:
semver-compare "^1.0.0"
rolldown@^1.0.0-rc.15, rolldown@^1.0.0-rc.8:
- version "1.0.0-rc.16"
- resolved "https://registry.yarnpkg.com/rolldown/-/rolldown-1.0.0-rc.16.tgz#47c1e6b088be3f531a9aacbdb8a90e2255f02702"
- integrity sha512-rzi5WqKzEZw3SooTt7cgm4eqIoujPIyGcJNGFL7iPEuajQw7vxMHUkXylu4/vhCkJGXsgRmxqMKXUpT6FEgl0g==
+ version "1.0.0-rc.15"
+ resolved "https://registry.yarnpkg.com/rolldown/-/rolldown-1.0.0-rc.15.tgz#ea3526443b2dbe834e9f8f6c1fde6232ec687170"
+ integrity sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==
dependencies:
- "@oxc-project/types" "=0.126.0"
- "@rolldown/pluginutils" "1.0.0-rc.16"
+ "@oxc-project/types" "=0.124.0"
+ "@rolldown/pluginutils" "1.0.0-rc.15"
optionalDependencies:
- "@rolldown/binding-android-arm64" "1.0.0-rc.16"
- "@rolldown/binding-darwin-arm64" "1.0.0-rc.16"
- "@rolldown/binding-darwin-x64" "1.0.0-rc.16"
- "@rolldown/binding-freebsd-x64" "1.0.0-rc.16"
- "@rolldown/binding-linux-arm-gnueabihf" "1.0.0-rc.16"
- "@rolldown/binding-linux-arm64-gnu" "1.0.0-rc.16"
- "@rolldown/binding-linux-arm64-musl" "1.0.0-rc.16"
- "@rolldown/binding-linux-ppc64-gnu" "1.0.0-rc.16"
- "@rolldown/binding-linux-s390x-gnu" "1.0.0-rc.16"
- "@rolldown/binding-linux-x64-gnu" "1.0.0-rc.16"
- "@rolldown/binding-linux-x64-musl" "1.0.0-rc.16"
- "@rolldown/binding-openharmony-arm64" "1.0.0-rc.16"
- "@rolldown/binding-wasm32-wasi" "1.0.0-rc.16"
- "@rolldown/binding-win32-arm64-msvc" "1.0.0-rc.16"
- "@rolldown/binding-win32-x64-msvc" "1.0.0-rc.16"
+ "@rolldown/binding-android-arm64" "1.0.0-rc.15"
+ "@rolldown/binding-darwin-arm64" "1.0.0-rc.15"
+ "@rolldown/binding-darwin-x64" "1.0.0-rc.15"
+ "@rolldown/binding-freebsd-x64" "1.0.0-rc.15"
+ "@rolldown/binding-linux-arm-gnueabihf" "1.0.0-rc.15"
+ "@rolldown/binding-linux-arm64-gnu" "1.0.0-rc.15"
+ "@rolldown/binding-linux-arm64-musl" "1.0.0-rc.15"
+ "@rolldown/binding-linux-ppc64-gnu" "1.0.0-rc.15"
+ "@rolldown/binding-linux-s390x-gnu" "1.0.0-rc.15"
+ "@rolldown/binding-linux-x64-gnu" "1.0.0-rc.15"
+ "@rolldown/binding-linux-x64-musl" "1.0.0-rc.15"
+ "@rolldown/binding-openharmony-arm64" "1.0.0-rc.15"
+ "@rolldown/binding-wasm32-wasi" "1.0.0-rc.15"
+ "@rolldown/binding-win32-arm64-msvc" "1.0.0-rc.15"
+ "@rolldown/binding-win32-x64-msvc" "1.0.0-rc.15"
rollup-plugin-cleanup@^3.2.1:
version "3.2.1"