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"