Cached layout, dynamic page
++ {id}:{Date.now()} +
+ {children} +` plain text to others, so the code is read from either format.
- const errorCode = /cf-error-code">(\d+)<|^error code: (\d+)/.exec(body)?.slice(1).find(Boolean);
-
- if (response.status === status && (status !== 500 || errorCode === '1101')) {
- return body;
- }
-
- lastAnswer = `${response.status}, cf-ray ${response.headers.get('cf-ray')}, error code ${errorCode ?? 'none'}, body: ${body.slice(0, 200)}`;
- } catch (error) {
- lastAnswer = String(error);
- }
-
- console.log(`The Worker did not answer ${url}: ${lastAnswer}`);
- await new Promise(resolve => setTimeout(resolve, 2_000));
- }
-
- throw new Error(`The Worker did not answer ${url} with status ${status} within 60s. Last answer: ${lastAnswer}`);
-}
diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/global-setup.ts b/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/global-setup.ts
index b33f187e1d94..552ccce37a94 100644
--- a/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/global-setup.ts
+++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/global-setup.ts
@@ -1,61 +1,3 @@
-import { randomBytes } from 'node:crypto';
-import { existsSync } from 'node:fs';
-import { deleteWorker, deployWorker, keepsWorker, waitForWorker } from './deployed-worker';
+import { createWorkerGlobalSetup } from '@sentry-internal/test-utils/cloudflare';
-const WORKER_PREFIX = 'e2e-send-to-sentry';
-
-/**
- * In CI the name follows the ref, so `develop`, `master` and every PR get a stable Worker that the
- * next run of the same ref overwrites. Pull request refs look like `123/merge` and merge queue refs
- * like `gh-readonly-queue/ /pr-123-`; both map to the PR's Worker.
- */
-export function getWorkerName(): string {
- if (!process.env.GITHUB_ACTIONS) {
- return `${WORKER_PREFIX}-local-${randomBytes(3).toString('hex')}`;
- }
-
- const { GITHUB_EVENT_NAME, GITHUB_REF_NAME = '' } = process.env;
- const prNumber =
- GITHUB_EVENT_NAME === 'pull_request' ? GITHUB_REF_NAME.split('/')[0] : /\/pr-(\d+)-/.exec(GITHUB_REF_NAME)?.[1];
- const ref = prNumber ? `pr-${prNumber}` : GITHUB_REF_NAME;
- // Worker names allow lowercase alphanumerics and dashes only, up to 63 characters.
- const slug = ref.toLowerCase().replace(/[^a-z0-9]+/g, '-');
-
- return `${WORKER_PREFIX}-${slug}`.slice(0, 63).replace(/-+$/, '');
-}
-
-export default async function globalSetup(): Promise {
- if (!existsSync(new URL('.wrangler/deploy/config.json', import.meta.url))) {
- throw new Error('Run `pnpm build` first: wrangler would deploy the uninstrumented source.');
- }
- const { CLOUDFLARE_ACCOUNT_ID, E2E_TEST_DSN } = process.env;
- if (!E2E_TEST_DSN) {
- throw new Error('E2E_TEST_DSN must be set to deploy the test worker.');
- }
-
- // Wrangler authenticates with `CLOUDFLARE_API_TOKEN` (CI) or a `wrangler login` session (local),
- // but it cannot pick an account on its own outside of a terminal.
- if (!CLOUDFLARE_ACCOUNT_ID) {
- throw new Error('CLOUDFLARE_ACCOUNT_ID must be set to deploy the test worker.');
- }
-
- const workerName = getWorkerName();
- const workerUrl = deployWorker(workerName, E2E_TEST_DSN);
- process.env.E2E_TEST_WORKER_NAME = workerName;
-
- try {
- await waitForWorker(workerUrl);
- } catch (error) {
- if (!keepsWorker()) {
- try {
- deleteWorker(workerName);
- } catch (deleteError) {
- // The unreachable worker is the failure to report, not the cleanup.
- console.error(`Failed to delete worker ${workerName}:`, deleteError);
- }
- }
- throw error;
- }
-
- process.env.E2E_TEST_WORKER_URL = workerUrl;
-}
+export default createWorkerGlobalSetup({ workerPrefix: 'e2e-send-to-sentry' });
diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/global-teardown.ts b/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/global-teardown.ts
index 9f012cd51101..c7bcfe696e66 100644
--- a/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/global-teardown.ts
+++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/global-teardown.ts
@@ -1,24 +1 @@
-import { deleteWorker, keepsWorker } from './deployed-worker';
-
-export default function globalTeardown(): void {
- const workerName = process.env.E2E_TEST_WORKER_NAME;
-
- if (!workerName) {
- return;
- }
-
- if (keepsWorker()) {
- console.log(`Keeping worker ${workerName} at ${process.env.E2E_TEST_WORKER_URL}`);
- return;
- }
-
- try {
- deleteWorker(workerName);
- } catch (error) {
- // A leaked worker is not an SDK failure, so it must not fail a run whose tests passed.
- console.error(
- `Failed to delete worker ${workerName}, delete it with \`wrangler delete --name ${workerName}\`:`,
- error,
- );
- }
-}
+export { workerGlobalTeardown as default } from '@sentry-internal/test-utils/cloudflare';
diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/playwright.config.ts b/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/playwright.config.ts
index 91e214f15493..dde66a7e3685 100644
--- a/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/playwright.config.ts
+++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/playwright.config.ts
@@ -1,16 +1,16 @@
-import { defineConfig } from '@playwright/test';
+import { getPlaywrightConfig } from '@sentry-internal/test-utils';
-export default defineConfig({
- testDir: './tests',
+const config = getPlaywrightConfig(undefined, {
+ // The tests read what arrived in Sentry through the sentry CLI, so there is no event proxy to start.
+ webServer: undefined,
// The worker is deployed once for the whole run and deleted again afterwards.
globalSetup: './global-setup.ts',
globalTeardown: './global-teardown.ts',
/* Spans take ~2min to become queryable via the trace endpoint. */
timeout: 210_000,
- fullyParallel: true,
- forbidOnly: !!process.env.CI,
- retries: 0,
// Every test spends most of its time polling Sentry, so run them all at once.
+ fullyParallel: true,
workers: '100%',
- reporter: process.env.CI ? [['list'], ['junit', { outputFile: 'results.junit.xml' }]] : 'list',
});
+
+export default config;
diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/src/env.d.ts b/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/src/env.d.ts
index aaf72be7640b..15557b010a50 100644
--- a/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/src/env.d.ts
+++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/src/env.d.ts
@@ -1,4 +1,5 @@
interface Env {
E2E_TEST_DSN: string;
SLEEP_WORKFLOW: Workflow;
+ AI: Ai;
}
diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/src/index.ts b/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/src/index.ts
index 93f1c24f124e..324c9161cf69 100644
--- a/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/src/index.ts
+++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/src/index.ts
@@ -27,6 +27,10 @@ export default {
}
case '/test-unhandled-error':
throw new Error('E2E test unhandled error');
+ case '/test-workers-ai': {
+ await env.AI.run('@cf/meta/llama-3.2-1b-instruct', { prompt: 'Say hi', max_tokens: 5 });
+ return Response.json({ traceId: spanContext?.traceId });
+ }
case '/test-span':
return Response.json({ spanId: spanContext?.spanId, traceId: spanContext?.traceId });
case '/test-workflow-sleep': {
diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/tests/send-to-sentry.test.ts b/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/tests/send-to-sentry.test.ts
index 8578ecb7bee7..2397db37089f 100644
--- a/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/tests/send-to-sentry.test.ts
+++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/tests/send-to-sentry.test.ts
@@ -2,13 +2,14 @@ import { randomBytes } from 'node:crypto';
import { expect, test } from '@playwright/test';
import {
EVENT_POLLING_OPTIONS,
+ fetchSpanAttributes,
fetchTrace,
findErrorInTrace,
findSpanInTrace,
flattenTrace,
traceTarget,
} from '@sentry-internal/test-utils/cli';
-import { fetchFromWorker } from '../deployed-worker';
+import { fetchFromWorker } from '@sentry-internal/test-utils/cloudflare';
// Set by global-setup.ts once the worker for this run is deployed.
const workerUrl = process.env.E2E_TEST_WORKER_URL;
@@ -73,3 +74,22 @@ test('Sends the spans of Workflow steps before the Workflow goes to sleep', asyn
);
expect(['running', 'waiting']).toContain(status);
});
+
+test('Sends a Workers AI gen_ai span to Sentry', async () => {
+ const { traceId }: { traceId: string } = JSON.parse(await fetchFromWorker(`${workerUrl}/test-workers-ai`, 200));
+
+ console.log(`Polling for gen_ai.chat span: sentry trace view ${traceTarget(traceId)}`);
+
+ let spanId: string | undefined;
+ await expect
+ .poll(() => (spanId = findSpanInTrace(traceId, 'gen_ai.chat')?.event_id), EVENT_POLLING_OPTIONS)
+ .toBeDefined();
+
+ // Sentry stores `gen_ai.response.text` as `gen_ai.output.messages`.
+ await expect
+ .poll(() => fetchSpanAttributes(traceId, spanId!), EVENT_POLLING_OPTIONS)
+ .toMatchObject({
+ 'gen_ai.input.messages': expect.stringContaining('Say hi'),
+ 'gen_ai.output.messages': expect.stringContaining('"role":"assistant"'),
+ });
+});
diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/tsconfig.node.json b/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/tsconfig.node.json
index 657ecc4ef24a..c49bedb6b3e3 100644
--- a/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/tsconfig.node.json
+++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/tsconfig.node.json
@@ -3,5 +3,5 @@
"compilerOptions": {
"types": ["node"]
},
- "include": ["tests/**/*", "deployed-worker.ts", "global-setup.ts", "global-teardown.ts", "playwright.config.ts"]
+ "include": ["tests/**/*", "global-setup.ts", "global-teardown.ts", "playwright.config.ts"]
}
diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/wrangler.jsonc b/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/wrangler.jsonc
index d6b15b1dbf5e..f1c0857ea5dd 100644
--- a/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/wrangler.jsonc
+++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/wrangler.jsonc
@@ -6,6 +6,7 @@
"compatibility_date": "2026-05-20",
"compatibility_flags": ["nodejs_compat"],
"workers_dev": true,
+ "ai": { "binding": "AI" },
// Workers Logs keep the invocations of the last 7 days, so a failed CI run can still be inspected.
"observability": { "enabled": true },
// Workflow names are unique per account, so deployWorker() renames it to the worker name.
diff --git a/dev-packages/e2e-tests/test-applications/debug-id-sourcemaps/package.json b/dev-packages/e2e-tests/test-applications/debug-id-sourcemaps/package.json
index 617e904f0bf6..660ab6ae4628 100644
--- a/dev-packages/e2e-tests/test-applications/debug-id-sourcemaps/package.json
+++ b/dev-packages/e2e-tests/test-applications/debug-id-sourcemaps/package.json
@@ -14,7 +14,7 @@
},
"devDependencies": {
"rollup": "^4.35.0",
- "vitest": "^3.2.7",
+ "vitest": "^4.1.11",
"@sentry/rollup-plugin": "^5.3.0"
},
"volta": {
diff --git a/dev-packages/e2e-tests/test-applications/debug-id-sourcemaps/tests/server.test.ts b/dev-packages/e2e-tests/test-applications/debug-id-sourcemaps/tests/server.test.ts
index 3ba3afae4fc8..0d9c81a7753c 100644
--- a/dev-packages/e2e-tests/test-applications/debug-id-sourcemaps/tests/server.test.ts
+++ b/dev-packages/e2e-tests/test-applications/debug-id-sourcemaps/tests/server.test.ts
@@ -28,56 +28,52 @@ function splitFrameContext(frame: SerializedFrame): Record {
};
}
-test(
- 'Find symbolicated event on sentry',
- async ({ expect }) => {
- const eventId = childProcess.execSync(`node ${path.join(__dirname, '..', 'dist', 'app.js')}`, {
- encoding: 'utf-8',
- });
-
- console.log(`Polling for error eventId: ${eventId}`);
+test('Find symbolicated event on sentry', { timeout: EVENT_POLLING_TIMEOUT }, async ({ expect }) => {
+ const eventId = childProcess.execSync(`node ${path.join(__dirname, '..', 'dist', 'app.js')}`, {
+ encoding: 'utf-8',
+ });
- let timedOut = false;
- setTimeout(() => {
- timedOut = true;
- }, EVENT_POLLING_TIMEOUT);
+ console.log(`Polling for error eventId: ${eventId}`);
- while (!timedOut) {
- await new Promise(resolve => setTimeout(resolve, 2000)); // poll every two seconds
- const response = await fetch(`https://sentry.io/api/0/organizations/${sentryTestOrgSlug}/eventids/${eventId}/`, {
- headers: { Authorization: `Bearer ${authToken}` },
- });
+ let timedOut = false;
+ setTimeout(() => {
+ timedOut = true;
+ }, EVENT_POLLING_TIMEOUT);
- // This is org scoped, so the auth token needs `org:read` on top of the project scopes.
- // That never resolves by waiting, so fail loudly rather than timing out.
- if (response.status === 401 || response.status === 403) {
- throw new Error(
- `Event lookup was rejected with ${response.status}: ${await response.text()}. ` +
- 'E2E_TEST_AUTH_TOKEN needs the `org:read` scope.',
- );
- }
+ while (!timedOut) {
+ await new Promise(resolve => setTimeout(resolve, 2000)); // poll every two seconds
+ const response = await fetch(`https://sentry.io/api/0/organizations/${sentryTestOrgSlug}/eventids/${eventId}/`, {
+ headers: { Authorization: `Bearer ${authToken}` },
+ });
- // A 404 means the event has not landed yet and a 429 is the shared test org being rate limited.
- // Both clear up by polling again.
- if (!response.ok) {
- expect([404, 429]).toContain(response.status);
- continue;
- }
+ // This is org scoped, so the auth token needs `org:read` on top of the project scopes.
+ // That never resolves by waiting, so fail loudly rather than timing out.
+ if (response.status === 401 || response.status === 403) {
+ throw new Error(
+ `Event lookup was rejected with ${response.status}: ${await response.text()}. ` +
+ 'E2E_TEST_AUTH_TOKEN needs the `org:read` scope.',
+ );
+ }
- const { event } = await response.json();
- const exception = event.entries.find((entry: { type: string }) => entry.type === 'exception');
- const frames: SerializedFrame[] = exception.data.values[0].stacktrace.frames;
- const topFrame = frames[frames.length - 1];
+ // A 404 means the event has not landed yet and a 429 is the shared test org being rate limited.
+ // Both clear up by polling again.
+ if (!response.ok) {
+ expect([404, 429]).toContain(response.status);
+ continue;
+ }
- if (topFrame === undefined) {
- throw new Error('Symbolicated event has no stack frames.');
- }
+ const { event } = await response.json();
+ const exception = event.entries.find((entry: { type: string }) => entry.type === 'exception');
+ const frames: SerializedFrame[] = exception.data.values[0].stacktrace.frames;
+ const topFrame = frames[frames.length - 1];
- expect(splitFrameContext(topFrame)).toMatchSnapshot();
- return;
+ if (topFrame === undefined) {
+ throw new Error('Symbolicated event has no stack frames.');
}
- throw new Error('Test timed out');
- },
- { timeout: EVENT_POLLING_TIMEOUT },
-);
+ expect(splitFrameContext(topFrame)).toMatchSnapshot();
+ return;
+ }
+
+ throw new Error('Test timed out');
+});
diff --git a/dev-packages/e2e-tests/test-applications/hono-4-legacy/.gitignore b/dev-packages/e2e-tests/test-applications/hono-4-legacy/.gitignore
new file mode 100644
index 000000000000..534f51704346
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/hono-4-legacy/.gitignore
@@ -0,0 +1,36 @@
+# prod
+dist/
+
+# dev
+.yarn/
+!.yarn/releases
+.vscode/*
+!.vscode/launch.json
+!.vscode/*.code-snippets
+.idea/workspace.xml
+.idea/usage.statistics.xml
+.idea/shelf
+
+# deps
+node_modules/
+.wrangler
+
+# env
+.env
+.env.production
+.dev.vars
+
+# logs
+logs/
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+# test
+test-results
+
+# misc
+.DS_Store
diff --git a/dev-packages/e2e-tests/test-applications/hono-4-legacy/deno.json b/dev-packages/e2e-tests/test-applications/hono-4-legacy/deno.json
new file mode 100644
index 000000000000..d7748bc95ecb
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/hono-4-legacy/deno.json
@@ -0,0 +1,11 @@
+{
+ "imports": {
+ "@sentry/hono": "npm:@sentry/hono",
+ "@sentry/hono/deno": "npm:@sentry/hono/deno",
+ "@sentry/deno": "npm:@sentry/deno",
+ "@sentry/core": "npm:@sentry/core",
+ "@opentelemetry/api": "npm:@opentelemetry/api@^1.9.0",
+ "hono": "npm:hono@^4.13.1"
+ },
+ "nodeModulesDir": "manual"
+}
diff --git a/dev-packages/e2e-tests/test-applications/hono-4-legacy/package.json b/dev-packages/e2e-tests/test-applications/hono-4-legacy/package.json
new file mode 100644
index 000000000000..ab3b68ce1f5d
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/hono-4-legacy/package.json
@@ -0,0 +1,52 @@
+{
+ "name": "hono-4-legacy",
+ "type": "module",
+ "version": "0.0.0",
+ "private": true,
+ "scripts": {
+ "dev:cf": "wrangler dev --var \"E2E_TEST_DSN:$E2E_TEST_DSN\" --log-level=$(test $CI && echo 'none' || echo 'log')",
+ "dev:node": "node --import tsx/esm --import ./src/instrument.node.ts src/entry.node.ts",
+ "dev:bun": "bun src/entry.bun.ts",
+ "dev:deno": "deno run --allow-net --allow-env --allow-read src/entry.deno.ts",
+ "build": "wrangler deploy --dry-run",
+ "test:build": "pnpm install && pnpm build",
+ "test:assert": "TEST_ENV=production playwright test"
+ },
+ "dependencies": {
+ "@sentry/bun": "latest || *",
+ "@sentry/cloudflare": "latest || *",
+ "@sentry/deno": "latest || *",
+ "@sentry/hono": "latest || *",
+ "@sentry/node": "latest || *",
+ "@hono/node-server": "^2.0.5",
+ "hono": "^4.13.1"
+ },
+ "devDependencies": {
+ "@playwright/test": "~1.63.0",
+ "@cloudflare/workers-types": "^4.20240725.0",
+ "@sentry-internal/test-utils": "link:../../../test-utils",
+ "tsx": "4.21.0",
+ "typescript": "^5.5.2",
+ "wrangler": "^4.61.0"
+ },
+ "volta": {
+ "node": "24.15.0",
+ "extends": "../../package.json"
+ },
+ "sentryTest": {
+ "variants": [
+ {
+ "assert-command": "RUNTIME=node pnpm test:assert",
+ "label": "hono-4-legacy (node)"
+ },
+ {
+ "assert-command": "RUNTIME=bun pnpm test:assert",
+ "label": "hono-4-legacy (bun)"
+ },
+ {
+ "assert-command": "RUNTIME=deno pnpm test:assert",
+ "label": "hono-4-legacy (deno)"
+ }
+ ]
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/hono-4-legacy/playwright.config.ts b/dev-packages/e2e-tests/test-applications/hono-4-legacy/playwright.config.ts
new file mode 100644
index 000000000000..5087b887e1f2
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/hono-4-legacy/playwright.config.ts
@@ -0,0 +1,30 @@
+import { getPlaywrightConfig } from '@sentry-internal/test-utils';
+import { RUNTIME, type Runtime } from './tests/constants';
+
+const testEnv = process.env.TEST_ENV;
+
+if (!testEnv) {
+ throw new Error('No test env defined');
+}
+
+const APP_PORT = 38787;
+
+const startCommands: Record = {
+ cloudflare: `pnpm dev:cf --port ${APP_PORT}`,
+ node: `pnpm dev:node`,
+ bun: `pnpm dev:bun`,
+ deno: `pnpm dev:deno`,
+};
+
+const config = getPlaywrightConfig(
+ {
+ startCommand: startCommands[RUNTIME],
+ port: APP_PORT,
+ },
+ {
+ workers: '100%',
+ retries: 0,
+ },
+);
+
+export default config;
diff --git a/dev-packages/e2e-tests/test-applications/hono-4-legacy/src/entry.bun.ts b/dev-packages/e2e-tests/test-applications/hono-4-legacy/src/entry.bun.ts
new file mode 100644
index 000000000000..6466b79717f5
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/hono-4-legacy/src/entry.bun.ts
@@ -0,0 +1,25 @@
+import { Hono } from 'hono';
+import { sentry } from '@sentry/hono/bun';
+import { addRoutes } from './routes';
+
+const app = new Hono();
+
+app.use(
+ sentry(app, {
+ dsn: process.env.E2E_TEST_DSN,
+ environment: 'qa',
+ tracesSampleRate: 1.0,
+ tunnel: 'http://localhost:3031/',
+ // Several tests trigger the same server error one after another, which Dedupe would drop.
+ integrations: integrations => integrations.filter(integration => integration.name !== 'Dedupe'),
+ }),
+);
+
+addRoutes(app);
+
+const port = Number(process.env.PORT || 38787);
+
+export default {
+ port,
+ fetch: app.fetch,
+};
diff --git a/dev-packages/e2e-tests/test-applications/hono-4-legacy/src/entry.cloudflare.ts b/dev-packages/e2e-tests/test-applications/hono-4-legacy/src/entry.cloudflare.ts
new file mode 100644
index 000000000000..e348dde56226
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/hono-4-legacy/src/entry.cloudflare.ts
@@ -0,0 +1,18 @@
+import { Hono } from 'hono';
+import { sentry } from '@sentry/hono/cloudflare';
+import { addRoutes } from './routes';
+
+const app = new Hono<{ Bindings: { E2E_TEST_DSN: string } }>();
+
+app.use(
+ sentry(app, env => ({
+ dsn: env.E2E_TEST_DSN,
+ environment: 'qa',
+ tracesSampleRate: 1.0,
+ tunnel: 'http://localhost:3031/',
+ })),
+);
+
+addRoutes(app);
+
+export default app;
diff --git a/dev-packages/e2e-tests/test-applications/hono-4-legacy/src/entry.deno.ts b/dev-packages/e2e-tests/test-applications/hono-4-legacy/src/entry.deno.ts
new file mode 100644
index 000000000000..15bd12a74111
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/hono-4-legacy/src/entry.deno.ts
@@ -0,0 +1,21 @@
+import { Hono } from 'hono';
+import { sentry } from '@sentry/hono/deno';
+import { addRoutes } from './routes';
+
+const app = new Hono();
+
+app.use(
+ sentry(app, {
+ dsn: Deno.env.get('E2E_TEST_DSN'),
+ environment: 'qa',
+ dataCollection: {},
+ tracesSampleRate: 1.0,
+ tunnel: 'http://localhost:3031/',
+ }),
+);
+
+addRoutes(app);
+
+const port = Number(Deno.env.get('PORT') || 38787);
+
+Deno.serve({ port }, app.fetch);
diff --git a/dev-packages/e2e-tests/test-applications/hono-4-legacy/src/entry.node.ts b/dev-packages/e2e-tests/test-applications/hono-4-legacy/src/entry.node.ts
new file mode 100644
index 000000000000..898a92e08be4
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/hono-4-legacy/src/entry.node.ts
@@ -0,0 +1,16 @@
+import { Hono } from 'hono';
+import { sentry } from '@sentry/hono/node';
+import { serve } from '@hono/node-server';
+import { addRoutes } from './routes';
+
+const app = new Hono();
+
+app.use(sentry(app));
+
+addRoutes(app);
+
+const port = Number(process.env.PORT || 38787);
+
+serve({ fetch: app.fetch, port }, () => {
+ console.log(`Hono (Node) listening on port ${port}`);
+});
diff --git a/dev-packages/e2e-tests/test-applications/hono-4-legacy/src/instrument.node.ts b/dev-packages/e2e-tests/test-applications/hono-4-legacy/src/instrument.node.ts
new file mode 100644
index 000000000000..3d3c93040c25
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/hono-4-legacy/src/instrument.node.ts
@@ -0,0 +1,10 @@
+import * as Sentry from '@sentry/hono/node';
+
+Sentry.init({
+ dsn: process.env.E2E_TEST_DSN,
+ environment: 'qa',
+ tracesSampleRate: 1.0,
+ tunnel: 'http://localhost:3031/',
+ // Several tests trigger the same server error one after another, which Dedupe would drop.
+ integrations: integrations => integrations.filter(integration => integration.name !== 'Dedupe'),
+});
diff --git a/dev-packages/e2e-tests/test-applications/hono-4-legacy/src/middleware.ts b/dev-packages/e2e-tests/test-applications/hono-4-legacy/src/middleware.ts
new file mode 100644
index 000000000000..9304a980dff3
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/hono-4-legacy/src/middleware.ts
@@ -0,0 +1,34 @@
+import type { MiddlewareHandler } from 'hono';
+
+// Defined as anonymous function expressions so the function name is inferred from the `const`
+// binding. A named expression (`const middlewareA = async function middlewareA() {}`) collides with
+// the binding when bundled, and Bun renames the inner function (→ `middlewareA2`), which would then
+// surface as the middleware span name. The inferred name stays stable across all runtimes.
+export const middlewareA: MiddlewareHandler = async function (c, next) {
+ // Add some delay
+ await new Promise(resolve => setTimeout(resolve, 50));
+ await next();
+};
+
+export const middlewareB: MiddlewareHandler = async function (_c, next) {
+ // Add some delay
+ await new Promise(resolve => setTimeout(resolve, 60));
+ await next();
+};
+
+let failingMiddlewareCount = 0;
+export const failingMiddleware: MiddlewareHandler = async function (_c, _next) {
+ // Each throw gets a unique suffix so the Dedupe integration doesn't collapse the identical errors
+ // that several tests (and their retries) trigger through this shared middleware — otherwise only the
+ // first would be reported and the other tests' `waitForError` would time out. Tests match on the
+ // stable `Middleware error` prefix.
+ throw new Error(`Middleware error #${(failingMiddlewareCount += 1)}`);
+};
+
+// Intentionally a NAMED function expression (unlike the anonymous ones above) so the named-function
+// path stays covered: the span name is taken from the function's own name. Under bundled Bun the inner
+// name collides with the `const` binding and is suffixed (→ `namedMiddleware2`), so the test matches on
+// the `namedMiddleware` prefix rather than an exact name.
+export const namedMiddleware: MiddlewareHandler = async function namedMiddleware(_c, next) {
+ await next();
+};
diff --git a/dev-packages/e2e-tests/test-applications/hono-4-legacy/src/route-groups/test-errors.ts b/dev-packages/e2e-tests/test-applications/hono-4-legacy/src/route-groups/test-errors.ts
new file mode 100644
index 000000000000..b8f2fd96fe93
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/hono-4-legacy/src/route-groups/test-errors.ts
@@ -0,0 +1,45 @@
+import { Hono } from 'hono';
+import { HTTPException } from 'hono/http-exception';
+
+const errorRoutes = new Hono();
+
+// Middleware that throws a 5xx HTTPException (should be captured)
+errorRoutes.use('/middleware-http-exception/*', async (_c, _next) => {
+ throw new HTTPException(503, { message: 'Service Unavailable from middleware' });
+});
+
+errorRoutes.get('/middleware-http-exception', c => c.text('should not reach'));
+
+// Middleware that throws a 4xx HTTPException (should NOT be captured)
+errorRoutes.use('/middleware-http-exception-4xx/*', async (_c, _next) => {
+ throw new HTTPException(401, { message: 'Unauthorized from middleware' });
+});
+
+errorRoutes.get('/middleware-http-exception-4xx', c => c.text('should not reach'));
+
+// Sub-app with a custom onError handler that swallows errors
+const subAppWithOnError = new Hono();
+
+subAppWithOnError.onError((err, c) => {
+ return c.text(`Handled by onError: ${err.message}`, 500);
+});
+
+subAppWithOnError.get('/fail', () => {
+ throw new Error('Error caught by custom onError');
+});
+
+errorRoutes.route('/custom-on-error', subAppWithOnError);
+
+// Nested sub-apps: parent mounts child, child route throws
+const childApp = new Hono();
+
+childApp.get('/error', () => {
+ throw new Error('Nested child app error');
+});
+
+const parentApp = new Hono();
+parentApp.route('/child', childApp);
+
+errorRoutes.route('/nested', parentApp);
+
+export { errorRoutes };
diff --git a/dev-packages/e2e-tests/test-applications/hono-4-legacy/src/route-groups/test-middleware.ts b/dev-packages/e2e-tests/test-applications/hono-4-legacy/src/route-groups/test-middleware.ts
new file mode 100644
index 000000000000..6f21c1cac33b
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/hono-4-legacy/src/route-groups/test-middleware.ts
@@ -0,0 +1,85 @@
+import { Hono } from 'hono';
+import { failingMiddleware, middlewareA, middlewareB, namedMiddleware } from '../middleware';
+
+const middlewareRoutes = new Hono();
+
+middlewareRoutes.get('/named', c => c.json({ middleware: 'named' }));
+middlewareRoutes.get('/anonymous', c => c.json({ middleware: 'anonymous' }));
+middlewareRoutes.get('/multi', c => c.json({ middleware: 'multi' }));
+middlewareRoutes.get('/error', c => c.text('should not reach'));
+middlewareRoutes.get('/param/:id', c => c.json({ paramId: c.req.param('id') }));
+middlewareRoutes.get('/declared', c => c.json({ middleware: 'declared' }));
+
+// Self-contained sub-app registering its own middleware via .use()
+const subAppWithMiddleware = new Hono();
+
+subAppWithMiddleware.use('/named/*', middlewareA);
+subAppWithMiddleware.use('/declared/*', namedMiddleware);
+subAppWithMiddleware.use('/anonymous/*', async (c, next) => {
+ c.header('X-Custom', 'anonymous');
+ await next();
+});
+subAppWithMiddleware.use('/multi/*', middlewareA, middlewareB);
+subAppWithMiddleware.use('/error/*', failingMiddleware);
+subAppWithMiddleware.use('/param/*', middlewareA);
+
+// .all() handler (1 parameter) — should NOT be wrapped as middleware by patchRoute.
+subAppWithMiddleware.all('/all-handler', async function allCatchAll(c) {
+ return c.json({ handler: 'all' });
+});
+
+subAppWithMiddleware.route('/', middlewareRoutes);
+
+// Sub-app with inline middleware for different registration styles.
+// patchRoute wraps non-last handlers per method+path group as middleware.
+const subAppWithInlineMiddleware = new Hono();
+
+const METHODS = ['get', 'post', 'put', 'delete', 'patch'] as const;
+
+// Direct method registration for each HTTP method
+METHODS.forEach(method => {
+ subAppWithInlineMiddleware[method](
+ '/direct',
+ async function inlineMiddleware(_c, next) {
+ await next();
+ },
+ c => c.text(`${method} direct response`),
+ );
+
+ subAppWithInlineMiddleware[method]('/direct/separately', async function inlineSeparateMiddleware(_c, next) {
+ await next();
+ });
+ subAppWithInlineMiddleware[method]('/direct/separately', c => c.text(`${method} direct separate response`));
+});
+
+// .all(): .all('/path', mw, handler)
+subAppWithInlineMiddleware.all(
+ '/all',
+ async function inlineMiddlewareAll(_c, next) {
+ await next();
+ },
+ c => c.text('all response'),
+);
+subAppWithInlineMiddleware.all('/all/separately', async function inlineSeparateMiddlewareAll(_c, next) {
+ await next();
+});
+subAppWithInlineMiddleware.all('/all/separately', c => c.text('all separate response'));
+
+// .on() registration for each HTTP method
+METHODS.forEach(method => {
+ subAppWithInlineMiddleware.on(
+ method,
+ '/on',
+ async function inlineMiddlewareOn(_c, next) {
+ await next();
+ },
+ c => c.text(`${method} on response`),
+ );
+
+ subAppWithInlineMiddleware.on(method, '/on/separately', async function inlineSeparateMiddlewareOn(_c, next) {
+ await next();
+ });
+ subAppWithInlineMiddleware.on(method, '/on/separately', c => c.text(`${method} on separate response`));
+});
+
+export { middlewareRoutes, subAppWithMiddleware, subAppWithInlineMiddleware };
diff --git a/dev-packages/e2e-tests/test-applications/hono-4-legacy/src/route-groups/test-multi-fetch.ts b/dev-packages/e2e-tests/test-applications/hono-4-legacy/src/route-groups/test-multi-fetch.ts
new file mode 100644
index 000000000000..ed9f0a3e131b
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/hono-4-legacy/src/route-groups/test-multi-fetch.ts
@@ -0,0 +1,118 @@
+import { Hono } from 'hono';
+import { HTTPException } from 'hono/http-exception';
+
+const ITEMS: Record = {
+ 'self-watering-plant': { name: 'Self-Watering Plant', stock: 5, price: 2500 },
+ 'solar-powered-cyberdeck': { name: 'Solar-Powered Cyberdeck', stock: 0, price: 5000 },
+};
+
+// Inventory service — standalone sub-app used as a data source.
+// Mounted on the main app AND called internally via .request() from the storefront.
+const inventoryApp = new Hono();
+
+inventoryApp.get('/item/:productId', c => {
+ const productId = c.req.param('productId');
+ const item = ITEMS[productId];
+ if (!item) {
+ throw new HTTPException(404, { message: `Item ${productId} not found` });
+ }
+ return c.json({ productId, ...item });
+});
+
+inventoryApp.get('/item/:productId/stock', c => {
+ const productId = c.req.param('productId');
+ const item = ITEMS[productId];
+ if (!item) {
+ throw new HTTPException(404, { message: `Stock check failed: ${productId}` });
+ }
+ return c.json({ productId, inStock: item.stock > 0, quantity: item.stock });
+});
+
+// Simulates an inner-route failure (a plain 5xx Error, not an HTTPException) reached only via an
+// internal .request() — used by the storefront's `/degraded` route below. Each throw gets a unique
+// suffix so the Dedupe integration doesn't collapse repeats across test retries.
+let dbErrorCount = 0;
+inventoryApp.get('/item/:productId/db-error', () => {
+ throw new Error(`inventory db is down #${(dbErrorCount += 1)}`);
+});
+
+// Storefront service — orchestrates internal .request() calls to inventoryApp.
+const storefrontApp = new Hono();
+
+storefrontApp.use('/*', async function storefrontAuth(_c, next) {
+ await new Promise(resolve => setTimeout(resolve, 10));
+ await next();
+});
+
+// Single internal fetch: look up one product
+storefrontApp.get('/product/:productId', async c => {
+ const res = await inventoryApp.request(`/item/${c.req.param('productId')}`);
+ if (!res.ok) {
+ throw new HTTPException(404, { message: 'Product not found' });
+ }
+ const item = await res.json();
+ return c.json({ product: item, source: 'storefront' });
+});
+
+// Parallel internal fetches: compare two products via Promise.all
+storefrontApp.get('/compare/:productId1/:productId2', async c => {
+ const [res1, res2] = await Promise.all([
+ inventoryApp.request(`/item/${c.req.param('productId1')}`),
+ inventoryApp.request(`/item/${c.req.param('productId2')}`),
+ ]);
+ if (!res1.ok || !res2.ok) {
+ throw new HTTPException(404, { message: 'One or more products not found' });
+ }
+ const [item1, item2] = (await Promise.all([res1.json(), res2.json()])) as [
+ Record,
+ Record,
+ ];
+ return c.json({
+ items: [item1, item2],
+ priceDifference: Math.abs(item1.price - item2.price),
+ });
+});
+
+// Sequential chained fetches: look up item, then check its stock
+storefrontApp.get('/product/:productId/availability', async c => {
+ const itemRes = await inventoryApp.request(`/item/${c.req.param('productId')}`);
+ if (!itemRes.ok) {
+ throw new HTTPException(404, { message: 'Product not found' });
+ }
+ const item: Record = await itemRes.json();
+
+ const stockRes = await inventoryApp.request(`/item/${item.productId}/stock`);
+ const stock: Record = await stockRes.json();
+
+ return c.json({
+ product: item.name,
+ available: stock.inStock,
+ quantity: stock.quantity,
+ });
+});
+
+// Degraded response: the inner route throws, but the outer handler swallows the failed internal
+// response and returns a 200 instead of propagating. The inner error should still reach Sentry (auto
+// instrumentation); the outer request stays healthy.
+storefrontApp.get('/product/:productId/degraded', async c => {
+ const res = await inventoryApp.request(`/item/${c.req.param('productId')}/db-error`);
+ if (!res.ok) {
+ return c.json({ product: null, degraded: true }, 200);
+ }
+ return c.json({ product: await res.json() });
+});
+
+// Error propagation: internal 404 causes the handler to throw a plain Error
+storefrontApp.get('/product-or-throw/:productId', async c => {
+ const res = await inventoryApp.request(`/item/${c.req.param('productId')}`);
+ if (!res.ok) {
+ throw new Error(`Failed to fetch product: ${c.req.param('productId')}`);
+ }
+ return c.json({ product: await res.json() });
+});
+
+const multiFetchRoutes = new Hono();
+multiFetchRoutes.route('/inventory', inventoryApp);
+multiFetchRoutes.route('/storefront', storefrontApp);
+
+export { multiFetchRoutes };
diff --git a/dev-packages/e2e-tests/test-applications/hono-4-legacy/src/route-groups/test-route-patterns.ts b/dev-packages/e2e-tests/test-applications/hono-4-legacy/src/route-groups/test-route-patterns.ts
new file mode 100644
index 000000000000..ac6fea1320cf
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/hono-4-legacy/src/route-groups/test-route-patterns.ts
@@ -0,0 +1,33 @@
+import { Hono } from 'hono';
+
+const routePatterns = new Hono();
+
+const METHODS = ['get', 'post', 'put', 'delete', 'patch'] as const;
+
+// Direct method registration for each HTTP method (sync handlers)
+METHODS.forEach(method => {
+ routePatterns[method]('/', c => c.text(`${method} response`));
+});
+
+// Async handler
+routePatterns.get('/async', async c => {
+ await new Promise(resolve => setTimeout(resolve, 10));
+ return c.text('async response');
+});
+
+// Dedicated route for query_string test to avoid transaction name collisions
+routePatterns.get('/query-test', c => c.text('query test response'));
+
+// Dedicated route for request data extraction tests to avoid transaction name collisions
+routePatterns.get('/request-data', c => c.text('request data response'));
+routePatterns.post('/request-data', c => c.text('request data response'));
+
+// .all() registration
+routePatterns.all('/all', c => c.text('all handler response'));
+
+// .on() registration
+METHODS.forEach(method => {
+ routePatterns.on(method, '/on', c => c.text(`${method} on response`));
+});
+
+export { routePatterns };
diff --git a/dev-packages/e2e-tests/test-applications/hono-4-legacy/src/routes.ts b/dev-packages/e2e-tests/test-applications/hono-4-legacy/src/routes.ts
new file mode 100644
index 000000000000..008a1ca6fd53
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/hono-4-legacy/src/routes.ts
@@ -0,0 +1,134 @@
+import { type Hono as HonoType, Hono } from 'hono';
+import { HTTPException } from 'hono/http-exception';
+import { failingMiddleware, middlewareA, middlewareB, namedMiddleware } from './middleware';
+import { errorRoutes } from './route-groups/test-errors';
+import { middlewareRoutes, subAppWithInlineMiddleware, subAppWithMiddleware } from './route-groups/test-middleware';
+import { multiFetchRoutes } from './route-groups/test-multi-fetch';
+import { routePatterns } from './route-groups/test-route-patterns';
+
+export function addRoutes(app: HonoType<{ Bindings?: { E2E_TEST_DSN: string } }>): void {
+ app.get('/', c => {
+ return c.text('Hello Hono!');
+ });
+
+ app.get('/test-param/:paramId', c => {
+ return c.json({ paramId: c.req.param('paramId') });
+ });
+
+ app.get('/error/:cause', c => {
+ throw new Error('This is a test error for Sentry!', {
+ cause: c.req.param('cause'),
+ });
+ });
+
+ app.get('/linked-error', () => {
+ const cause = new Error('Failure 1');
+ const errorCause = new Error('Failure 2', { cause });
+ throw new Error('Failure 3', { cause: errorCause });
+ });
+
+ app.get('/http-exception/:code', c => {
+ // oxlint-disable-next-line typescript/no-explicit-any
+ const code = Number(c.req.param('code')) as any;
+ throw new HTTPException(code, { message: `HTTPException ${code}` });
+ });
+
+ // Root-app middleware: registered on the patched main app instance
+ app.use('/test-middleware/named/*', middlewareA);
+ app.use('/test-middleware/anonymous/*', async (c, next) => {
+ c.header('X-Custom', 'anonymous');
+ await next();
+ });
+ app.use('/test-middleware/multi/*', middlewareA, middlewareB);
+ app.use('/test-middleware/error/*', failingMiddleware);
+ app.use('/test-middleware/param/*', middlewareA);
+ app.use('/test-middleware/declared/*', namedMiddleware);
+ app.route('/test-middleware', middlewareRoutes);
+
+ // Sub-app middleware: registered on the sub-app, wrapped at mount time by route() patching
+ app.route('/test-subapp-middleware', subAppWithMiddleware);
+
+ // Inline middleware patterns: direct method, .all(), .on() with inline/separate middleware
+ app.route('/test-inline-middleware', subAppWithInlineMiddleware);
+
+ // Inline middleware on the main app via HTTP method registration (not .use()).
+ app.get(
+ '/test-main-inline/get',
+ async function mainInlineGet(_c, next) {
+ await next();
+ },
+ c => c.text('main inline get'),
+ );
+ app.post(
+ '/test-main-inline/post',
+ async function mainInlinePost(_c, next) {
+ await next();
+ },
+ c => c.text('main inline post'),
+ );
+ app.all(
+ '/test-main-inline/all',
+ async function mainInlineAll(_c, next) {
+ await next();
+ },
+ c => c.text('main inline all'),
+ );
+ app.query(
+ '/test-main-inline/query',
+ async function mainInlineQuery(_c, next) {
+ await next();
+ },
+ async c => {
+ const body = await c.req.json<{ value: string }>();
+ return c.json({ method: c.req.method, value: body.value });
+ },
+ );
+
+ // Combined: .use() middleware + inline middleware via .get() on the same path.
+ app.use('/test-main-inline/combined/*', middlewareA);
+ app.get(
+ '/test-main-inline/combined/resource',
+ async function combinedInlineMw(_c, next) {
+ await next();
+ },
+ c => c.text('combined response'),
+ );
+
+ // Route patterns: HTTP methods, .all(), .on(), sync/async, errors
+ app.route('/test-routes', routePatterns);
+
+ // Error-specific routes: onError handler, nested sub-apps, middleware HTTPException
+ app.route('/test-errors', errorRoutes);
+
+ // Multi-fetch routes: storefront sub-app calls inventoryApp via .request()
+ app.route('/test-multi-fetch', multiFetchRoutes);
+
+ // .basePath() with sub-app mounting via .route()
+ const apiSubApp = new Hono();
+ apiSubApp.use(async function apiAuth(_c, next) {
+ await next();
+ });
+ apiSubApp.get('/users', c => c.json({ users: [{ id: 1, name: 'Alice' }] }));
+ apiSubApp.get('/users/:userId', c => c.json({ userId: c.req.param('userId') }));
+
+ app.basePath('/test-basepath').route('/v1', apiSubApp);
+
+ app.use(async function trailingMiddleware(_c, next) {
+ // Trailing middleware to make sure the route names are resolved correctly (not `/*`).
+ await new Promise(resolve => setTimeout(resolve, 50));
+ await next();
+ });
+
+ // .use() on the cloned instance returned by .basePath() — the clone has its own
+ // .use class field, so this tests whether middleware instrumentation propagates.
+ app
+ .basePath('/test-basepath-mw')
+ .use(async function basepathMiddleware(_c, next) {
+ await new Promise(resolve => setTimeout(resolve, 50));
+ await next();
+ })
+ .get('/hello', c => c.json({ greeting: 'world' }));
+
+ // .get() registered on the root app after .basePath()/.route() chains
+ app.get('/test-late-get', c => c.json({ registered: 'after-chains' }));
+}
diff --git a/dev-packages/e2e-tests/test-applications/hono-4-legacy/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/hono-4-legacy/start-event-proxy.mjs
new file mode 100644
index 000000000000..8f6b794b1827
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/hono-4-legacy/start-event-proxy.mjs
@@ -0,0 +1,6 @@
+import { startEventProxyServer } from '@sentry-internal/test-utils';
+
+startEventProxyServer({
+ port: 3031,
+ proxyServerName: 'hono-4-legacy',
+});
diff --git a/dev-packages/e2e-tests/test-applications/hono-4-legacy/tests/basepath-and-late-routes.test.ts b/dev-packages/e2e-tests/test-applications/hono-4-legacy/tests/basepath-and-late-routes.test.ts
new file mode 100644
index 000000000000..c7ad43b6b945
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/hono-4-legacy/tests/basepath-and-late-routes.test.ts
@@ -0,0 +1,91 @@
+import { expect, test } from '@playwright/test';
+import { waitForStreamedSpan, getSpanOp, collectStreamedSpansUntilSegment } from '@sentry-internal/test-utils';
+import { APP_NAME } from './constants';
+
+test.describe('basePath with sub-app routes', () => {
+ test('traces GET on a sub-app mounted via .basePath().route()', async ({ baseURL }) => {
+ const segmentPromise = waitForStreamedSpan(
+ APP_NAME,
+ segment =>
+ segment.is_segment && getSpanOp(segment) === 'http.server' && segment.name === 'GET /test-basepath/v1/users',
+ );
+
+ const response = await fetch(`${baseURL}/test-basepath/v1/users`);
+ expect(response.status).toBe(200);
+
+ const body = await response.json();
+ expect(body).toEqual({ users: [{ id: 1, name: 'Alice' }] });
+
+ const segment = await segmentPromise;
+ expect(segment.name).toBe('GET /test-basepath/v1/users');
+ expect(getSpanOp(segment)).toBe('http.server');
+ });
+
+ test('traces parameterized route under .basePath().route()', async ({ baseURL }) => {
+ const segmentPromise = waitForStreamedSpan(
+ APP_NAME,
+ segment =>
+ segment.is_segment &&
+ getSpanOp(segment) === 'http.server' &&
+ segment.name === 'GET /test-basepath/v1/users/:userId',
+ );
+
+ const response = await fetch(`${baseURL}/test-basepath/v1/users/42`);
+ expect(response.status).toBe(200);
+
+ const body = await response.json();
+ expect(body).toEqual({ userId: '42' });
+
+ const segment = await segmentPromise;
+ expect(segment.name).toBe('GET /test-basepath/v1/users/:userId');
+ expect(getSpanOp(segment)).toBe('http.server');
+ });
+});
+
+// TODO: this test is currently skipped because we do not yet support middleware registered on new instances (e.g. here via .basePath(..).use(...)).
+test.skip('.basePath() middleware instrumentation', () => {
+ test('creates middleware span for .use() on .basePath() clone', async ({ baseURL }) => {
+ const segmentPromise = collectStreamedSpansUntilSegment(
+ APP_NAME,
+ segment => getSpanOp(segment) === 'http.server' && segment.name === 'GET /test-basepath-mw/hello',
+ );
+
+ const response = await fetch(`${baseURL}/test-basepath-mw/hello`);
+ expect(response.status).toBe(200);
+
+ const body = await response.json();
+ expect(body).toEqual({ greeting: 'world' });
+
+ const segmentSpans = await segmentPromise;
+ const segment = segmentSpans.find(
+ segment =>
+ segment.is_segment && getSpanOp(segment) === 'http.server' && segment.name === 'GET /test-basepath-mw/hello',
+ )!;
+ expect(segment.name).toBe('GET /test-basepath-mw/hello');
+
+ const spans = segmentSpans.filter(
+ span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment.span_id,
+ );
+ const middlewareSpan = spans.find(span => getSpanOp(span) === 'middleware' && span.name === 'basepathMiddleware');
+
+ expect(middlewareSpan).toBeDefined();
+ expect(middlewareSpan?.attributes['sentry.origin']?.value).toBe('auto.middleware.hono');
+ });
+});
+
+test('traces .get() route registered after .basePath()/.route() chains', async ({ baseURL }) => {
+ const segmentPromise = waitForStreamedSpan(
+ APP_NAME,
+ segment => segment.is_segment && getSpanOp(segment) === 'http.server' && segment.name === 'GET /test-late-get',
+ );
+
+ const response = await fetch(`${baseURL}/test-late-get`);
+ expect(response.status).toBe(200);
+
+ const body = await response.json();
+ expect(body).toEqual({ registered: 'after-chains' });
+
+ const segment = await segmentPromise;
+ expect(segment.name).toBe('GET /test-late-get');
+ expect(getSpanOp(segment)).toBe('http.server');
+});
diff --git a/dev-packages/e2e-tests/test-applications/hono-4-legacy/tests/constants.ts b/dev-packages/e2e-tests/test-applications/hono-4-legacy/tests/constants.ts
new file mode 100644
index 000000000000..2aee60fc6be8
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/hono-4-legacy/tests/constants.ts
@@ -0,0 +1,5 @@
+export type Runtime = 'cloudflare' | 'node' | 'bun' | 'deno';
+
+export const RUNTIME = (process.env.RUNTIME || 'node') as Runtime;
+
+export const APP_NAME = 'hono-4-legacy';
diff --git a/dev-packages/e2e-tests/test-applications/hono-4-legacy/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/hono-4-legacy/tests/errors.test.ts
new file mode 100644
index 000000000000..e3c44c67d4d6
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/hono-4-legacy/tests/errors.test.ts
@@ -0,0 +1,329 @@
+import { expect, test } from '@playwright/test';
+import {
+ waitForError,
+ waitForStreamedSpan,
+ getSpanOp,
+ collectStreamedSpansUntilSegment,
+} from '@sentry-internal/test-utils';
+import { APP_NAME, RUNTIME } from './constants';
+
+test.describe('route handler errors', () => {
+ test('captures error with mechanism and trace correlation', async ({ baseURL }) => {
+ const errorPromise = waitForError(APP_NAME, event => {
+ return event.exception?.values?.[0]?.value === 'This is a test error for Sentry!';
+ });
+
+ const segmentPromise = waitForStreamedSpan(
+ APP_NAME,
+ segment => segment.is_segment && getSpanOp(segment) === 'http.server' && !!segment.name?.includes('/error/'),
+ );
+
+ const response = await fetch(`${baseURL}/error/test-cause`);
+ expect(response.status).toBe(500);
+
+ const errorEvent = await errorPromise;
+ const segmentEvent = await segmentPromise;
+
+ expect(segmentEvent.name).toBe('GET /error/:cause');
+
+ expect(errorEvent.exception?.values).toHaveLength(1);
+
+ const exception = errorEvent.exception?.values?.[0];
+ expect(exception?.value).toBe('This is a test error for Sentry!');
+ expect(exception?.mechanism).toEqual({
+ handled: false,
+ type: 'auto.http.hono.context_error',
+ });
+
+ expect(errorEvent.transaction).toBe('GET /error/:cause');
+ expect(errorEvent.request?.method).toBe('GET');
+ expect(errorEvent.request?.url).toContain('/error/test-cause');
+ expect(errorEvent.request?.headers).toBeDefined();
+
+ expect(errorEvent.contexts?.trace?.trace_id).toBe(segmentEvent?.trace_id);
+ });
+
+ test('captures three linked errors', async ({ baseURL }) => {
+ const errorPromise = waitForError(APP_NAME, event => {
+ return event.exception?.values?.some(exception => exception.value === 'Failure 3');
+ });
+
+ const response = await fetch(`${baseURL}/linked-error`);
+ expect(response.status).toBe(500);
+
+ const errorEvent = await errorPromise;
+ expect(errorEvent.exception?.values).toHaveLength(3);
+
+ const firstCause = errorEvent.exception?.values?.[0];
+ expect(firstCause?.value).toBe('Failure 1');
+ expect(firstCause?.mechanism).toEqual({
+ exception_id: 2,
+ handled: true,
+ parent_id: 1,
+ source: 'cause',
+ type: 'chained',
+ });
+
+ const secondCause = errorEvent.exception?.values?.[1];
+ expect(secondCause?.value).toBe('Failure 2');
+ expect(secondCause?.mechanism).toEqual({
+ exception_id: 1,
+ handled: true,
+ parent_id: 0,
+ source: 'cause',
+ type: 'chained',
+ });
+
+ const capturedError = errorEvent.exception?.values?.[2];
+ expect(capturedError?.value).toBe('Failure 3');
+ expect(capturedError?.mechanism).toEqual({
+ exception_id: 0,
+ handled: false,
+ type: 'auto.http.hono.context_error',
+ });
+
+ expect(errorEvent.transaction).toBe('GET /linked-error');
+ });
+});
+
+test.describe('HTTPException errors', () => {
+ test('captures 5xx HTTPException', async ({ baseURL }) => {
+ const errorPromise = waitForError(APP_NAME, event => {
+ return event.exception?.values?.[0]?.value === 'HTTPException 500';
+ });
+
+ const response = await fetch(`${baseURL}/http-exception/500`);
+ expect(response.status).toBe(500);
+
+ const errorEvent = await errorPromise;
+ expect(errorEvent.exception?.values?.[0]?.value).toBe('HTTPException 500');
+ expect(errorEvent.exception?.values?.[0]?.mechanism).toEqual({
+ handled: false,
+ type: 'auto.http.hono.context_error',
+ });
+ });
+
+ // On Node/Bun, httpServerSpansIntegration drops transactions for 3xx/4xx responses (ignoreStatusCodes), so we just use a request guard.
+ // On Cloudflare the transaction is available, and we additionally verify its name.
+ [301, 302].forEach(code => {
+ test(`does not capture ${code} HTTPException`, async ({ baseURL }) => {
+ let errorEventOccurred = false;
+
+ waitForError(APP_NAME, event => {
+ if (event.exception?.values?.[0]?.value === `HTTPException ${code}`) {
+ errorEventOccurred = true;
+ }
+ return false;
+ });
+
+ const segmentPromise = waitForStreamedSpan(
+ APP_NAME,
+ segment =>
+ segment.is_segment &&
+ (RUNTIME === 'cloudflare'
+ ? getSpanOp(segment) === 'http.server' && !!segment.name?.includes('/http-exception/')
+ : getSpanOp(segment) === 'http.server' && segment.name === 'GET /'),
+ );
+
+ const response = await fetch(`${baseURL}/http-exception/${code}`, { redirect: 'manual' });
+ expect(response.status).toBe(code);
+
+ if (RUNTIME !== 'cloudflare') {
+ // Simple request guard for non-Cloudflare runtimes since the other transaction is dropped for 4xx responses
+ await fetch(`${baseURL}/`);
+ }
+
+ const segment = await segmentPromise;
+
+ if (RUNTIME === 'cloudflare') {
+ expect(segment.name).toBe('GET /http-exception/:code');
+ }
+
+ expect(errorEventOccurred).toBe(false);
+ });
+ });
+
+ [401, 403, 404].forEach(code => {
+ test(`does not capture ${code} HTTPException`, async ({ baseURL }) => {
+ let errorEventOccurred = false;
+
+ waitForError(APP_NAME, event => {
+ if (event.exception?.values?.[0]?.value === `HTTPException ${code}`) {
+ errorEventOccurred = true;
+ }
+ return false;
+ });
+
+ const segmentPromise = waitForStreamedSpan(
+ APP_NAME,
+ segment =>
+ segment.is_segment &&
+ (RUNTIME === 'cloudflare'
+ ? getSpanOp(segment) === 'http.server' && !!segment.name?.includes('/http-exception/')
+ : getSpanOp(segment) === 'http.server' && segment.name === 'GET /'),
+ );
+
+ const response = await fetch(`${baseURL}/http-exception/${code}`);
+ expect(response.status).toBe(code);
+
+ if (RUNTIME !== 'cloudflare') {
+ // Simple request guard for non-Cloudflare runtimes since the other transaction is dropped for 4xx responses
+ await fetch(`${baseURL}/`);
+ }
+
+ const segment = await segmentPromise;
+
+ if (RUNTIME === 'cloudflare') {
+ expect(segment.name).toBe('GET /http-exception/:code');
+ }
+
+ expect(errorEventOccurred).toBe(false);
+ });
+ });
+});
+
+test.describe('middleware errors', () => {
+ test('captures 5xx HTTPException thrown in middleware with error span status', async ({ baseURL }) => {
+ const errorPromise = waitForError(APP_NAME, event => {
+ return event.exception?.values?.[0]?.value === 'Service Unavailable from middleware';
+ });
+
+ const segmentPromise = collectStreamedSpansUntilSegment(
+ APP_NAME,
+ segment =>
+ getSpanOp(segment) === 'http.server' && !!segment.name?.includes('/test-errors/middleware-http-exception'),
+ );
+
+ const response = await fetch(`${baseURL}/test-errors/middleware-http-exception`);
+ expect(response.status).toBe(503);
+
+ const errorEvent = await errorPromise;
+ expect(errorEvent.exception?.values?.[0]?.value).toBe('Service Unavailable from middleware');
+ expect(errorEvent.exception?.values?.[0]?.mechanism?.type).toBe('auto.http.hono.context_error');
+ expect(errorEvent.exception?.values?.[0]?.mechanism?.handled).toBe(false);
+ expect(errorEvent.transaction).toBe('GET /test-errors/middleware-http-exception');
+
+ const segmentSpans = await segmentPromise;
+ const segment = segmentSpans.find(
+ segment =>
+ segment.is_segment &&
+ getSpanOp(segment) === 'http.server' &&
+ !!segment.name?.includes('/test-errors/middleware-http-exception'),
+ )!;
+ const middlewareSpan = segmentSpans
+ .filter(span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment.span_id)
+ .find(s => getSpanOp(s) === 'middleware');
+ expect(middlewareSpan?.status).toBe('error');
+ });
+
+ test('does not capture 4xx HTTPException thrown in middleware', async ({ baseURL }) => {
+ let errorEventOccurred = false;
+
+ waitForError(APP_NAME, event => {
+ if (event.exception?.values?.[0]?.value === 'Unauthorized from middleware') {
+ errorEventOccurred = true;
+ }
+ return false;
+ });
+
+ const segmentPromise = collectStreamedSpansUntilSegment(APP_NAME, segment => {
+ if (RUNTIME === 'cloudflare') {
+ return (
+ getSpanOp(segment) === 'http.server' && !!segment.name?.includes('/test-errors/middleware-http-exception-4xx')
+ );
+ }
+ return getSpanOp(segment) === 'http.server' && segment.name === 'GET /';
+ });
+
+ const response = await fetch(`${baseURL}/test-errors/middleware-http-exception-4xx`);
+ expect(response.status).toBe(401);
+
+ if (RUNTIME !== 'cloudflare') {
+ await fetch(`${baseURL}/`);
+ }
+
+ const segmentSpans = await segmentPromise;
+ const segment = segmentSpans.find(segment => {
+ if (!segment.is_segment) return false;
+ if (RUNTIME === 'cloudflare') {
+ return (
+ getSpanOp(segment) === 'http.server' && !!segment.name?.includes('/test-errors/middleware-http-exception-4xx')
+ );
+ }
+ return getSpanOp(segment) === 'http.server' && segment.name === 'GET /';
+ })!;
+
+ if (RUNTIME === 'cloudflare') {
+ expect(segment.name).toBe('GET /test-errors/middleware-http-exception-4xx');
+
+ const middlewareSpan = segmentSpans
+ .filter(span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment.span_id)
+ .find(s => getSpanOp(s) === 'middleware');
+ expect(middlewareSpan?.status).not.toBe('error');
+ }
+
+ expect(errorEventOccurred).toBe(false);
+ });
+});
+
+test.describe('nested sub-app errors', () => {
+ test('captures error from nested child sub-app', async ({ baseURL }) => {
+ const errorPromise = waitForError(APP_NAME, event => {
+ return event.exception?.values?.[0]?.value === 'Nested child app error';
+ });
+
+ const segmentPromise = waitForStreamedSpan(
+ APP_NAME,
+ segment =>
+ segment.is_segment && getSpanOp(segment) === 'http.server' && !!segment.name?.includes('/nested/child/error'),
+ );
+
+ const response = await fetch(`${baseURL}/test-errors/nested/child/error`);
+ expect(response.status).toBe(500);
+
+ const errorEvent = await errorPromise;
+ const segment = await segmentPromise;
+
+ expect(segment.name).toBe('GET /test-errors/nested/child/error');
+
+ expect(errorEvent.exception?.values?.[0]?.value).toBe('Nested child app error');
+ expect(errorEvent.exception?.values?.[0]?.mechanism).toEqual({
+ handled: false,
+ type: 'auto.http.hono.context_error',
+ });
+ expect(errorEvent.request?.method).toBe('GET');
+ expect(errorEvent.request?.url).toContain('/test-errors/nested/child/error');
+ expect(errorEvent.request?.headers).toBeDefined();
+ });
+});
+
+test.describe('custom onError handler', () => {
+ test('captures error even when onError handles the response', async ({ baseURL }) => {
+ const errorPromise = waitForError(APP_NAME, event => {
+ return event.exception?.values?.[0]?.value === 'Error caught by custom onError';
+ });
+
+ const segmentPromise = waitForStreamedSpan(
+ APP_NAME,
+ segment =>
+ segment.is_segment && getSpanOp(segment) === 'http.server' && !!segment.name?.includes('/custom-on-error/fail'),
+ );
+
+ const response = await fetch(`${baseURL}/test-errors/custom-on-error/fail`);
+ expect(response.status).toBe(500);
+
+ const body = await response.text();
+ expect(body).toContain('Handled by onError');
+
+ const errorEvent = await errorPromise;
+ const segment = await segmentPromise;
+
+ expect(segment.name).toBe('GET /test-errors/custom-on-error/fail');
+
+ expect(errorEvent.exception?.values?.[0]?.value).toBe('Error caught by custom onError');
+ expect(errorEvent.exception?.values?.[0]?.mechanism).toEqual({
+ handled: false,
+ type: 'auto.http.hono.context_error',
+ });
+ });
+});
diff --git a/dev-packages/e2e-tests/test-applications/hono-4-legacy/tests/middleware.test.ts b/dev-packages/e2e-tests/test-applications/hono-4-legacy/tests/middleware.test.ts
new file mode 100644
index 000000000000..d760f12e6451
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/hono-4-legacy/tests/middleware.test.ts
@@ -0,0 +1,442 @@
+import { expect, test } from '@playwright/test';
+import { waitForError, getSpanOp, collectStreamedSpansUntilSegment } from '@sentry-internal/test-utils';
+import { APP_NAME } from './constants';
+
+const SCENARIOS = [
+ {
+ name: 'root app middleware',
+ prefix: '/test-middleware',
+ },
+ {
+ name: 'sub-app middleware (route group)',
+ prefix: '/test-subapp-middleware',
+ },
+] as const;
+
+for (const { name, prefix } of SCENARIOS) {
+ test.describe(name, () => {
+ test('creates a span for named middleware', async ({ baseURL }) => {
+ const segmentPromise = collectStreamedSpansUntilSegment(
+ APP_NAME,
+ segment => getSpanOp(segment) === 'http.server' && !!segment.name?.includes(`${prefix}/named`),
+ );
+
+ const response = await fetch(`${baseURL}${prefix}/named`);
+ expect(response.status).toBe(200);
+
+ const segmentSpans = await segmentPromise;
+ const segment = segmentSpans.find(
+ segment =>
+ segment.is_segment && getSpanOp(segment) === 'http.server' && !!segment.name?.includes(`${prefix}/named`),
+ )!;
+ expect(segment.name).toBe(`GET ${prefix}/named`);
+
+ const spans = segmentSpans.filter(
+ span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment.span_id,
+ );
+
+ const middlewareSpan = spans.find(span => getSpanOp(span) === 'middleware' && span.name === 'middlewareA');
+
+ expect(middlewareSpan).toEqual(
+ expect.objectContaining({
+ name: 'middlewareA',
+ attributes: expect.objectContaining({
+ 'sentry.op': { value: 'middleware', type: 'string' },
+ 'sentry.origin': { value: 'auto.middleware.hono', type: 'string' },
+ }),
+ }),
+ );
+ expect(middlewareSpan?.status).not.toBe('error');
+
+ const durationMs = (middlewareSpan!.end_timestamp - middlewareSpan!.start_timestamp) * 1000;
+ expect(durationMs).toBeGreaterThanOrEqual(49);
+ });
+
+ test('creates a span for anonymous middleware', async ({ baseURL }) => {
+ const segmentPromise = collectStreamedSpansUntilSegment(
+ APP_NAME,
+ segment => getSpanOp(segment) === 'http.server' && !!segment.name?.includes(`${prefix}/anonymous`),
+ );
+
+ const response = await fetch(`${baseURL}${prefix}/anonymous`);
+ expect(response.status).toBe(200);
+
+ const segmentSpans = await segmentPromise;
+ const segment = segmentSpans.find(
+ segment =>
+ segment.is_segment && getSpanOp(segment) === 'http.server' && !!segment.name?.includes(`${prefix}/anonymous`),
+ )!;
+ expect(segment.name).toBe(`GET ${prefix}/anonymous`);
+
+ const spans = segmentSpans.filter(
+ span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment.span_id,
+ );
+
+ const anonymousSpan = spans.find(span => getSpanOp(span) === 'middleware' && span.name === '');
+ expect(anonymousSpan).toBeDefined();
+ expect(anonymousSpan?.attributes['sentry.origin']?.value).toBe('auto.middleware.hono');
+ expect(anonymousSpan?.status).not.toBe('error');
+ });
+
+ test('creates a span for a named-function middleware (name from the function binding)', async ({ baseURL }) => {
+ const segmentPromise = collectStreamedSpansUntilSegment(
+ APP_NAME,
+ segment => getSpanOp(segment) === 'http.server' && !!segment.name?.includes(`${prefix}/declared`),
+ );
+
+ const response = await fetch(`${baseURL}${prefix}/declared`);
+ expect(response.status).toBe(200);
+
+ const segmentSpans = await segmentPromise;
+ const segment = segmentSpans.find(
+ segment =>
+ segment.is_segment && getSpanOp(segment) === 'http.server' && !!segment.name?.includes(`${prefix}/declared`),
+ )!;
+ expect(segment.name).toBe(`GET ${prefix}/declared`);
+
+ const spans = segmentSpans.filter(
+ span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment.span_id,
+ );
+
+ // Named function expression: the span name comes from the function's own name. A bundler (Bun)
+ // may append a numeric suffix when the inner name collides with the `const` binding, so match on
+ // the `namedMiddleware` prefix rather than an exact name.
+ const middlewareSpan = spans.find(
+ span => getSpanOp(span) === 'middleware' && !!span.name?.match(/^namedMiddleware\d*$/),
+ );
+
+ expect(middlewareSpan).toBeDefined();
+ expect(middlewareSpan?.attributes['sentry.origin']?.value).toBe('auto.middleware.hono');
+ expect(middlewareSpan?.status).not.toBe('error');
+ });
+
+ test('multiple middleware are sibling spans under the same parent', async ({ baseURL }) => {
+ const segmentPromise = collectStreamedSpansUntilSegment(
+ APP_NAME,
+ segment => getSpanOp(segment) === 'http.server' && !!segment.name?.includes(`${prefix}/multi`),
+ );
+
+ const response = await fetch(`${baseURL}${prefix}/multi`);
+ expect(response.status).toBe(200);
+
+ const segmentSpans = await segmentPromise;
+ const segment = segmentSpans.find(
+ segment =>
+ segment.is_segment && getSpanOp(segment) === 'http.server' && !!segment.name?.includes(`${prefix}/multi`),
+ )!;
+ expect(segment.name).toBe(`GET ${prefix}/multi`);
+
+ const spans = segmentSpans.filter(
+ span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment.span_id,
+ );
+
+ const middlewareSpans = spans.sort((a, b) => (a.start_timestamp ?? 0) - (b.start_timestamp ?? 0));
+
+ expect(middlewareSpans).toHaveLength(2);
+ expect(middlewareSpans[0].name).toBe('middlewareA');
+ expect(middlewareSpans[1].name).toBe('middlewareB');
+
+ expect(middlewareSpans[0]?.parent_span_id).toBe(middlewareSpans[1]?.parent_span_id);
+
+ // middlewareA has a 50ms delay, middlewareB has a 60ms delay
+ const aDurationMs = (middlewareSpans[0].end_timestamp - middlewareSpans[0]?.start_timestamp) * 1000;
+ const bDurationMs = (middlewareSpans[1].end_timestamp - middlewareSpans[1]?.start_timestamp) * 1000;
+ expect(aDurationMs).toBeGreaterThanOrEqual(49);
+ expect(bDurationMs).toBeGreaterThanOrEqual(59);
+ });
+
+ test('captures error thrown in middleware', async ({ baseURL }) => {
+ const errorPromise = waitForError(APP_NAME, event => {
+ return (
+ !!event.exception?.values?.[0]?.value?.startsWith('Middleware error') &&
+ !!event.request?.url?.includes(prefix)
+ );
+ });
+
+ const response = await fetch(`${baseURL}${prefix}/error`);
+ expect(response.status).toBe(500);
+
+ const errorEvent = await errorPromise;
+ expect(errorEvent.exception?.values?.[0]?.value).toMatch(/^Middleware error/);
+ expect(errorEvent.exception?.values?.[0]?.mechanism).toEqual(
+ expect.objectContaining({
+ handled: false,
+ type: 'auto.http.hono.context_error',
+ }),
+ );
+
+ // The transaction name on the error event determines the culprit shown in Sentry.
+ expect(errorEvent.transaction).toBe(`GET ${prefix}/error`);
+ });
+
+ test('sets error status on middleware span when middleware throws', async ({ baseURL }) => {
+ const segmentPromise = collectStreamedSpansUntilSegment(
+ APP_NAME,
+ segment => getSpanOp(segment) === 'http.server' && !!segment.name?.includes(`${prefix}/error`),
+ );
+
+ await fetch(`${baseURL}${prefix}/error`);
+
+ const segmentSpans = await segmentPromise;
+ const segment = segmentSpans.find(
+ segment =>
+ segment.is_segment && getSpanOp(segment) === 'http.server' && !!segment.name?.includes(`${prefix}/error`),
+ )!;
+ expect(segment.name).toBe(`GET ${prefix}/error`);
+
+ const spans = segmentSpans.filter(
+ span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment.span_id,
+ );
+
+ const failingSpan = spans.find(span => getSpanOp(span) === 'middleware' && span.status === 'error');
+
+ expect(failingSpan).toBeDefined();
+ expect(failingSpan?.status).toBe('error');
+ });
+
+ test('uses parameterized route in span name', async ({ baseURL }) => {
+ const segmentPromise = collectStreamedSpansUntilSegment(
+ APP_NAME,
+ segment => getSpanOp(segment) === 'http.server' && !!segment.name?.includes(`${prefix}/param/`),
+ );
+
+ const response = await fetch(`${baseURL}${prefix}/param/42`);
+ expect(response.status).toBe(200);
+
+ const segmentSpans = await segmentPromise;
+ const segment = segmentSpans.find(
+ segment =>
+ segment.is_segment && getSpanOp(segment) === 'http.server' && !!segment.name?.includes(`${prefix}/param/`),
+ )!;
+ expect(segment.name).toBe(`GET ${prefix}/param/:id`);
+
+ const spans = segmentSpans.filter(
+ span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment.span_id,
+ );
+ const middlewareSpan = spans.find(span => getSpanOp(span) === 'middleware' && span.name === 'middlewareA');
+ expect(middlewareSpan).toBeDefined();
+ });
+
+ test('includes request data on error events from middleware', async ({ baseURL }) => {
+ const errorPromise = waitForError(APP_NAME, event => {
+ return (
+ !!event.exception?.values?.[0]?.value?.startsWith('Middleware error') &&
+ !!event.request?.url?.includes(prefix)
+ );
+ });
+
+ await fetch(`${baseURL}${prefix}/error`);
+
+ const errorEvent = await errorPromise;
+ expect(errorEvent.request).toEqual(
+ expect.objectContaining({
+ method: 'GET',
+ url: expect.stringContaining(`${prefix}/error`),
+ }),
+ );
+ });
+ });
+}
+
+test.describe('.all() handler in sub-app', () => {
+ test('does not create middleware span for .all() route handler', async ({ baseURL }) => {
+ const segmentPromise = collectStreamedSpansUntilSegment(
+ APP_NAME,
+ segment =>
+ getSpanOp(segment) === 'http.server' && !!segment.name?.includes('/test-subapp-middleware/all-handler'),
+ );
+
+ const response = await fetch(`${baseURL}/test-subapp-middleware/all-handler`);
+ expect(response.status).toBe(200);
+
+ const body = await response.json();
+ expect(body).toEqual({ handler: 'all' });
+
+ const segmentSpans = await segmentPromise;
+ const segment = segmentSpans.find(
+ segment =>
+ segment.is_segment &&
+ getSpanOp(segment) === 'http.server' &&
+ !!segment.name?.includes('/test-subapp-middleware/all-handler'),
+ )!;
+ expect(segment.name).toBe('GET /test-subapp-middleware/all-handler');
+
+ const spans = segmentSpans.filter(
+ span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment.span_id,
+ );
+
+ // No middleware is called for this route, so there should be no spans.
+ expect(spans).toEqual([]);
+ });
+});
+
+const INLINE_PREFIX = '/test-inline-middleware';
+
+const REGISTRATION_STYLES = [
+ { name: 'direct method (.get())', path: '/direct' },
+ { name: '.all()', path: '/all' },
+ { name: '.on()', path: '/on' },
+] as const;
+
+const MIDDLEWARE_STYLES = [
+ { name: 'inline', path: '' },
+ { name: 'separately registered', path: '/separately' },
+] as const;
+
+test.describe('inline middleware spans (sub-app)', () => {
+ for (const { name: regName, path: regPath } of REGISTRATION_STYLES) {
+ for (const { name: mwName, path: mwPath } of MIDDLEWARE_STYLES) {
+ test(`creates middleware span for ${mwName} middleware via ${regName}`, async ({ baseURL }) => {
+ const fullPath = `${INLINE_PREFIX}${regPath}${mwPath}`;
+
+ const segmentPromise = collectStreamedSpansUntilSegment(
+ APP_NAME,
+ segment => getSpanOp(segment) === 'http.server' && !!segment.name?.includes(fullPath),
+ );
+
+ const response = await fetch(`${baseURL}${fullPath}`);
+ expect(response.status).toBe(200);
+
+ const segmentSpans = await segmentPromise;
+ const segment = segmentSpans.find(
+ segment => segment.is_segment && getSpanOp(segment) === 'http.server' && !!segment.name?.includes(fullPath),
+ )!;
+ expect(segment.name).toBe(`GET ${fullPath}`);
+
+ const EXPECTED_DESCRIPTIONS: Record> = {
+ '/direct': { '': 'inlineMiddleware', '/separately': 'inlineSeparateMiddleware' },
+ '/all': { '': 'inlineMiddlewareAll', '/separately': 'inlineSeparateMiddlewareAll' },
+ '/on': { '': 'inlineMiddlewareOn', '/separately': 'inlineSeparateMiddlewareOn' },
+ };
+ const expectedDescription = EXPECTED_DESCRIPTIONS[regPath]![mwPath]!;
+
+ const inlineSpan = segmentSpans
+ .filter(span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment.span_id)
+ .find(s => s.name === expectedDescription);
+ expect(inlineSpan).toBeDefined();
+ expect(getSpanOp(inlineSpan!)).toBe('middleware');
+ expect(inlineSpan?.attributes['sentry.origin']?.value).toBe('auto.middleware.hono');
+ expect(inlineSpan?.status).not.toBe('error');
+ });
+ }
+ }
+});
+
+const MAIN_INLINE_PREFIX = '/test-main-inline';
+
+const MAIN_INLINE_CASES = [
+ { name: '.get()', path: '/get', method: 'GET', expectedMiddlewareName: 'mainInlineGet' },
+ { name: '.post()', path: '/post', method: 'POST', expectedMiddlewareName: 'mainInlinePost' },
+ { name: '.all()', path: '/all', method: 'GET', expectedMiddlewareName: 'mainInlineAll' },
+] as const;
+
+test.describe('inline middleware spans (main app)', () => {
+ test('creates middleware span for inline middleware via .query()', async ({ baseURL }) => {
+ const fullPath = `${MAIN_INLINE_PREFIX}/query`;
+ const segmentPromise = collectStreamedSpansUntilSegment(
+ APP_NAME,
+ segment => getSpanOp(segment) === 'http.server' && segment.name === `QUERY ${fullPath}`,
+ );
+
+ const response = await fetch(`${baseURL}${fullPath}`, {
+ method: 'QUERY',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ value: 'query-body' }),
+ });
+ expect(response.status).toBe(200);
+ await expect(response.json()).resolves.toEqual({ method: 'QUERY', value: 'query-body' });
+
+ const segmentSpans = await segmentPromise;
+ const segment = segmentSpans.find(
+ segment => segment.is_segment && getSpanOp(segment) === 'http.server' && segment.name === `QUERY ${fullPath}`,
+ )!;
+ expect(segment.name).toBe(`QUERY ${fullPath}`);
+ expect(getSpanOp(segment)).toBe('http.server');
+ expect(segment.attributes['sentry.segment.name.source']?.value).toBe('route');
+ expect(segment.attributes['http.request.method']?.value).toBe('QUERY');
+
+ const middlewareSpans = segmentSpans
+ .filter(span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment.span_id)
+ .filter(span => getSpanOp(span) === 'middleware');
+ expect(middlewareSpans).toHaveLength(1);
+ expect(middlewareSpans[0]).toEqual(
+ expect.objectContaining({
+ name: 'mainInlineQuery',
+ attributes: expect.objectContaining({
+ 'sentry.op': { value: 'middleware', type: 'string' },
+ 'sentry.origin': { value: 'auto.middleware.hono', type: 'string' },
+ }),
+ }),
+ );
+ });
+
+ MAIN_INLINE_CASES.forEach(({ name, path, method, expectedMiddlewareName }) => {
+ test(`creates middleware span for inline middleware via ${name}`, async ({ baseURL }) => {
+ const fullPath = `${MAIN_INLINE_PREFIX}${path}`;
+
+ const segmentPromise = collectStreamedSpansUntilSegment(
+ APP_NAME,
+ segment => getSpanOp(segment) === 'http.server' && segment.name === `${method} ${fullPath}`,
+ );
+
+ const response = await fetch(`${baseURL}${fullPath}`, { method });
+ expect(response.status).toBe(200);
+
+ const segmentSpans = await segmentPromise;
+ const segment = segmentSpans.find(
+ segment =>
+ segment.is_segment && getSpanOp(segment) === 'http.server' && segment.name === `${method} ${fullPath}`,
+ )!;
+ expect(segment.name).toBe(`${method} ${fullPath}`);
+
+ const spans = segmentSpans.filter(
+ span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment.span_id,
+ );
+ const inlineSpan = spans.find(s => s.name === expectedMiddlewareName);
+
+ expect(inlineSpan).toBeDefined();
+ expect(getSpanOp(inlineSpan!)).toBe('middleware');
+ expect(inlineSpan?.attributes['sentry.origin']?.value).toBe('auto.middleware.hono');
+ expect(inlineSpan?.status).not.toBe('error');
+
+ const middlewareSpans = spans.filter(s => getSpanOp(s) === 'middleware');
+ expect(middlewareSpans).toHaveLength(1);
+ });
+ });
+
+ test('creates spans for both .use() middleware and inline middleware via .get()', async ({ baseURL }) => {
+ const fullPath = `${MAIN_INLINE_PREFIX}/combined/resource`;
+
+ const segmentPromise = collectStreamedSpansUntilSegment(
+ APP_NAME,
+ segment => getSpanOp(segment) === 'http.server' && segment.name === `GET ${fullPath}`,
+ );
+
+ const response = await fetch(`${baseURL}${fullPath}`);
+ expect(response.status).toBe(200);
+
+ const segmentSpans = await segmentPromise;
+ const segment = segmentSpans.find(
+ segment => segment.is_segment && getSpanOp(segment) === 'http.server' && segment.name === `GET ${fullPath}`,
+ )!;
+ expect(segment.name).toBe(`GET ${fullPath}`);
+
+ const spans = segmentSpans.filter(
+ span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment.span_id,
+ );
+ const middlewareSpans = spans.filter(s => getSpanOp(s) === 'middleware');
+
+ expect(middlewareSpans).toHaveLength(2);
+
+ const [spanA, spanB] = middlewareSpans.sort((a, b) => (a.name ?? '').localeCompare(b.name ?? ''));
+ expect(spanA.name).toBe('combinedInlineMw');
+ expect(getSpanOp(spanA!)).toBe('middleware');
+ expect(spanA.attributes['sentry.origin']?.value).toBe('auto.middleware.hono');
+ expect(spanA?.status).not.toBe('error');
+
+ expect(spanB.name).toBe('middlewareA');
+ expect(getSpanOp(spanB!)).toBe('middleware');
+ expect(spanB.attributes['sentry.origin']?.value).toBe('auto.middleware.hono');
+ expect(spanB?.status).not.toBe('error');
+ });
+});
diff --git a/dev-packages/e2e-tests/test-applications/hono-4-legacy/tests/multi-fetch.test.ts b/dev-packages/e2e-tests/test-applications/hono-4-legacy/tests/multi-fetch.test.ts
new file mode 100644
index 000000000000..eecd5b99bca4
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/hono-4-legacy/tests/multi-fetch.test.ts
@@ -0,0 +1,383 @@
+import { expect, test } from '@playwright/test';
+import {
+ waitForError,
+ waitForStreamedSpan,
+ getSpanOp,
+ collectStreamedSpansUntilSegment,
+} from '@sentry-internal/test-utils';
+import { APP_NAME } from './constants';
+
+const STOREFRONT = '/test-multi-fetch/storefront';
+const INVENTORY = '/test-multi-fetch/inventory';
+
+test.describe('multi-fetch: internal .request() calls between sub-apps', () => {
+ test.describe('single internal fetch', () => {
+ test('returns enriched product data and creates span with parameterized route', async ({ baseURL }) => {
+ const segmentPromise = waitForStreamedSpan(
+ APP_NAME,
+ segment =>
+ segment.is_segment &&
+ getSpanOp(segment) === 'http.server' &&
+ segment.name === `GET ${STOREFRONT}/product/:productId`,
+ );
+
+ const response = await fetch(`${baseURL}${STOREFRONT}/product/self-watering-plant`);
+ expect(response.status).toBe(200);
+
+ const body = await response.json();
+ expect(body.product).toEqual(
+ expect.objectContaining({
+ productId: 'self-watering-plant',
+ name: 'Self-Watering Plant',
+ stock: 5,
+ price: 2500,
+ }),
+ );
+ expect(body.source).toBe('storefront');
+
+ const segment = await segmentPromise;
+ expect(segment.name).toBe(`GET ${STOREFRONT}/product/:productId`);
+ expect(getSpanOp(segment)).toBe('http.server');
+ });
+
+ test('creates storefrontAuth middleware span', async ({ baseURL }) => {
+ const segmentPromise = collectStreamedSpansUntilSegment(
+ APP_NAME,
+ segment => getSpanOp(segment) === 'http.server' && segment.name === `GET ${STOREFRONT}/product/:productId`,
+ );
+
+ const response = await fetch(`${baseURL}${STOREFRONT}/product/solar-powered-cyberdeck`);
+ expect(response.status).toBe(200);
+
+ const segmentSpans = await segmentPromise;
+ const segment = segmentSpans.find(
+ segment =>
+ segment.is_segment &&
+ getSpanOp(segment) === 'http.server' &&
+ segment.name === `GET ${STOREFRONT}/product/:productId`,
+ )!;
+ const spans = segmentSpans.filter(
+ span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment.span_id,
+ );
+
+ const middlewareSpan = spans.find(span => getSpanOp(span) === 'middleware' && span.name === 'storefrontAuth');
+
+ expect(middlewareSpan).toEqual(
+ expect.objectContaining({
+ name: 'storefrontAuth',
+ attributes: expect.objectContaining({
+ 'sentry.op': { value: 'middleware', type: 'string' },
+ 'sentry.origin': { value: 'auto.middleware.hono', type: 'string' },
+ }),
+ }),
+ );
+ expect(middlewareSpan?.status).not.toBe('error');
+ });
+ });
+
+ test.describe('parallel internal fetches', () => {
+ test('aggregates data from two concurrent .request() calls', async ({ baseURL }) => {
+ const segmentPromise = waitForStreamedSpan(
+ APP_NAME,
+ segment =>
+ segment.is_segment &&
+ getSpanOp(segment) === 'http.server' &&
+ segment.name === `GET ${STOREFRONT}/compare/:productId1/:productId2`,
+ );
+
+ const response = await fetch(`${baseURL}${STOREFRONT}/compare/self-watering-plant/solar-powered-cyberdeck`);
+ expect(response.status).toBe(200);
+
+ const body = await response.json();
+ expect(body.items).toHaveLength(2);
+ expect(body.items[0].productId).toBe('self-watering-plant');
+ expect(body.items[1].productId).toBe('solar-powered-cyberdeck');
+ expect(body.priceDifference).toBe(2500);
+
+ const segment = await segmentPromise;
+ expect(segment.name).toBe(`GET ${STOREFRONT}/compare/:productId1/:productId2`);
+ });
+ });
+
+ test.describe('sequential chained fetches', () => {
+ test('composes data from item lookup followed by stock check', async ({ baseURL }) => {
+ const segmentPromise = waitForStreamedSpan(
+ APP_NAME,
+ segment =>
+ segment.is_segment &&
+ getSpanOp(segment) === 'http.server' &&
+ segment.name === `GET ${STOREFRONT}/product/:productId/availability`,
+ );
+
+ const response = await fetch(`${baseURL}${STOREFRONT}/product/self-watering-plant/availability`);
+ expect(response.status).toBe(200);
+
+ const body = await response.json();
+ expect(body).toEqual({ product: 'Self-Watering Plant', available: true, quantity: 5 });
+
+ const segment = await segmentPromise;
+ expect(segment.name).toBe(`GET ${STOREFRONT}/product/:productId/availability`);
+ expect(getSpanOp(segment)).toBe('http.server');
+ });
+
+ test('reports out-of-stock item as unavailable', async ({ baseURL }) => {
+ const segmentPromise = waitForStreamedSpan(
+ APP_NAME,
+ segment =>
+ segment.is_segment &&
+ getSpanOp(segment) === 'http.server' &&
+ segment.name === `GET ${STOREFRONT}/product/:productId/availability`,
+ );
+
+ const response = await fetch(`${baseURL}${STOREFRONT}/product/solar-powered-cyberdeck/availability`);
+ expect(response.status).toBe(200);
+
+ const body = await response.json();
+ expect(body).toEqual({ product: 'Solar-Powered Cyberdeck', available: false, quantity: 0 });
+
+ await segmentPromise;
+ });
+ });
+
+ test.describe('error propagation from internal fetch', () => {
+ test('captures error when handler throws after failed internal .request()', async ({ baseURL }) => {
+ const errorPromise = waitForError(APP_NAME, event => {
+ return event.exception?.values?.[0]?.value === 'Failed to fetch product: nonexistent';
+ });
+
+ const segmentPromise = waitForStreamedSpan(
+ APP_NAME,
+ segment =>
+ segment.is_segment &&
+ getSpanOp(segment) === 'http.server' &&
+ segment.name === `GET ${STOREFRONT}/product-or-throw/:productId`,
+ );
+
+ const response = await fetch(`${baseURL}${STOREFRONT}/product-or-throw/nonexistent`);
+ expect(response.status).toBe(500);
+
+ const errorEvent = await errorPromise;
+ expect(errorEvent.exception?.values?.[0]?.value).toBe('Failed to fetch product: nonexistent');
+ expect(errorEvent.exception?.values?.[0]?.mechanism).toEqual(expect.objectContaining({ handled: false }));
+ expect(errorEvent.transaction).toBe(`GET ${STOREFRONT}/product-or-throw/:productId`);
+
+ const segment = await segmentPromise;
+ expect(segment.name).toBe(`GET ${STOREFRONT}/product-or-throw/:productId`);
+ expect(segment?.status).toBe('error');
+ });
+
+ test('error event includes request data', async ({ baseURL }) => {
+ const errorPromise = waitForError(APP_NAME, event => {
+ return event.exception?.values?.[0]?.value === 'Failed to fetch product: missing';
+ });
+
+ await fetch(`${baseURL}${STOREFRONT}/product-or-throw/missing`);
+
+ const errorEvent = await errorPromise;
+ expect(errorEvent.request).toEqual(
+ expect.objectContaining({
+ method: 'GET',
+ url: expect.stringContaining(`${STOREFRONT}/product-or-throw/missing`),
+ }),
+ );
+ });
+ });
+
+ test.describe('error inside internal fetch (degraded response)', () => {
+ // The manual `sentry()` middleware only instruments the main app's request lifecycle. An error
+ // thrown solely inside an internal sub-app `.request()` — whose failed response the outer handler
+ // swallows — is therefore NOT captured here, unlike the orchestrion auto-instrumentation in the
+ // `hono-4` app, which instruments every dispatched context. We assert only that the outer request
+ // stays healthy.
+ test('degrades to a 200 when the inner route fails', async ({ baseURL }) => {
+ const response = await fetch(`${baseURL}${STOREFRONT}/product/self-watering-plant/degraded`);
+ expect(response.status).toBe(200);
+ await expect(response.json()).resolves.toEqual({ product: null, degraded: true });
+ });
+ });
+
+ test.describe('inventory sub-app direct access', () => {
+ test('creates its own span when accessed directly via HTTP', async ({ baseURL }) => {
+ const segmentPromise = waitForStreamedSpan(
+ APP_NAME,
+ segment =>
+ segment.is_segment &&
+ getSpanOp(segment) === 'http.server' &&
+ segment.name === `GET ${INVENTORY}/item/:productId`,
+ );
+
+ const response = await fetch(`${baseURL}${INVENTORY}/item/self-watering-plant`);
+ expect(response.status).toBe(200);
+
+ const body = await response.json();
+ expect(body).toEqual(expect.objectContaining({ productId: 'self-watering-plant', name: 'Self-Watering Plant' }));
+
+ const segment = await segmentPromise;
+ expect(segment.name).toBe(`GET ${INVENTORY}/item/:productId`);
+ expect(getSpanOp(segment)).toBe('http.server');
+ });
+ });
+
+ test.describe('trace propagation through internal .request() calls', () => {
+ test('single internal fetch produces an internal-request child span', async ({ baseURL }) => {
+ const segmentPromise = collectStreamedSpansUntilSegment(
+ APP_NAME,
+ segment => getSpanOp(segment) === 'http.server' && segment.name === `GET ${STOREFRONT}/product/:productId`,
+ );
+
+ await fetch(`${baseURL}${STOREFRONT}/product/self-watering-plant`);
+
+ const segmentSpans = await segmentPromise;
+ const segment = segmentSpans.find(
+ segment =>
+ segment.is_segment &&
+ getSpanOp(segment) === 'http.server' &&
+ segment.name === `GET ${STOREFRONT}/product/:productId`,
+ )!;
+ const traceId = segment?.trace_id;
+ const spans = segmentSpans.filter(
+ span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment.span_id,
+ );
+
+ const internalRequestSpans = spans.filter(
+ s => s.attributes['sentry.origin']?.value === 'auto.http.hono.internal_request',
+ );
+
+ expect(internalRequestSpans).toHaveLength(1);
+ expect(internalRequestSpans[0]).toEqual(
+ expect.objectContaining({
+ trace_id: traceId,
+ attributes: expect.objectContaining({
+ 'sentry.op': { value: 'http.server', type: 'string' },
+ 'sentry.origin': { value: 'auto.http.hono.internal_request', type: 'string' },
+ }),
+ }),
+ );
+ expect(internalRequestSpans[0].name).toContain('GET /item/self-watering-plant');
+ });
+
+ test('parallel internal fetches produce two sibling internal-request spans', async ({ baseURL }) => {
+ const segmentPromise = collectStreamedSpansUntilSegment(
+ APP_NAME,
+ segment =>
+ getSpanOp(segment) === 'http.server' && segment.name === `GET ${STOREFRONT}/compare/:productId1/:productId2`,
+ );
+
+ await fetch(`${baseURL}${STOREFRONT}/compare/self-watering-plant/solar-powered-cyberdeck`);
+
+ const segmentSpans = await segmentPromise;
+ const segment = segmentSpans.find(
+ segment =>
+ segment.is_segment &&
+ getSpanOp(segment) === 'http.server' &&
+ segment.name === `GET ${STOREFRONT}/compare/:productId1/:productId2`,
+ )!;
+ const traceId = segment?.trace_id;
+ const spans = segmentSpans.filter(
+ span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment.span_id,
+ );
+
+ const internalRequestSpans = spans.filter(
+ s => s.attributes['sentry.origin']?.value === 'auto.http.hono.internal_request',
+ );
+
+ expect(internalRequestSpans).toHaveLength(2);
+
+ expect(internalRequestSpans[0]?.parent_span_id).toBe(internalRequestSpans[1]?.parent_span_id);
+
+ expect(internalRequestSpans[0]?.trace_id).toBe(traceId);
+ expect(internalRequestSpans[1]?.trace_id).toBe(traceId);
+
+ expect(internalRequestSpans[0].attributes['sentry.origin']?.value).toBe('auto.http.hono.internal_request');
+ expect(internalRequestSpans[1].attributes['sentry.origin']?.value).toBe('auto.http.hono.internal_request');
+ });
+
+ test('sequential chained fetches produce two ordered internal-request spans', async ({ baseURL }) => {
+ const segmentPromise = collectStreamedSpansUntilSegment(
+ APP_NAME,
+ segment =>
+ getSpanOp(segment) === 'http.server' && segment.name === `GET ${STOREFRONT}/product/:productId/availability`,
+ );
+
+ await fetch(`${baseURL}${STOREFRONT}/product/self-watering-plant/availability`);
+
+ const segmentSpans = await segmentPromise;
+ const segment = segmentSpans.find(
+ segment =>
+ segment.is_segment &&
+ getSpanOp(segment) === 'http.server' &&
+ segment.name === `GET ${STOREFRONT}/product/:productId/availability`,
+ )!;
+ const traceId = segment?.trace_id;
+ const spans = segmentSpans.filter(
+ span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment.span_id,
+ );
+
+ const internalRequestSpans = spans
+ .filter(s => s.attributes['sentry.origin']?.value === 'auto.http.hono.internal_request')
+ .sort(
+ (a: { start_timestamp?: number }, b: { start_timestamp?: number }) =>
+ (a.start_timestamp ?? 0) - (b.start_timestamp ?? 0),
+ );
+
+ expect(internalRequestSpans).toHaveLength(2);
+
+ // Sequential: second span starts at or after first span ends (with tolerance for clock precision)
+ expect(internalRequestSpans[1].start_timestamp).toBeGreaterThanOrEqual(
+ internalRequestSpans[0].end_timestamp! - 0.001,
+ );
+
+ expect(internalRequestSpans[0]?.trace_id).toBe(traceId);
+ expect(internalRequestSpans[1]?.trace_id).toBe(traceId);
+ });
+
+ test('internal-request span has no error status for internal 4xx HTTPException', async ({ baseURL }) => {
+ const segmentPromise = collectStreamedSpansUntilSegment(
+ APP_NAME,
+ segment =>
+ getSpanOp(segment) === 'http.server' && segment.name === `GET ${STOREFRONT}/product-or-throw/:productId`,
+ );
+
+ await fetch(`${baseURL}${STOREFRONT}/product-or-throw/ghost`);
+
+ const segmentSpans = await segmentPromise;
+ const segment = segmentSpans.find(
+ segment =>
+ segment.is_segment &&
+ getSpanOp(segment) === 'http.server' &&
+ segment.name === `GET ${STOREFRONT}/product-or-throw/:productId`,
+ )!;
+ const spans = segmentSpans.filter(
+ span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment.span_id,
+ );
+
+ const internalRequestSpans = spans.filter(
+ s => s.attributes['sentry.origin']?.value === 'auto.http.hono.internal_request',
+ );
+
+ expect(internalRequestSpans).toHaveLength(1);
+ expect(internalRequestSpans[0]?.status).not.toBe('error');
+ });
+
+ test('error from failed internal fetch is correlated with the storefront trace', async ({ baseURL }) => {
+ const errorPromise = waitForError(APP_NAME, event => {
+ return event.exception?.values?.[0]?.value === 'Failed to fetch product: ghost';
+ });
+
+ const segmentPromise = waitForStreamedSpan(
+ APP_NAME,
+ segment =>
+ segment.is_segment &&
+ getSpanOp(segment) === 'http.server' &&
+ segment.name === `GET ${STOREFRONT}/product-or-throw/:productId`,
+ );
+
+ await fetch(`${baseURL}${STOREFRONT}/product-or-throw/ghost`);
+
+ const [errorEvent, segment] = await Promise.all([errorPromise, segmentPromise]);
+
+ expect(errorEvent.contexts?.trace?.trace_id).toBe(segment?.trace_id);
+ expect(errorEvent.contexts?.trace?.span_id).toBeDefined();
+ });
+ });
+});
diff --git a/dev-packages/e2e-tests/test-applications/hono-4-legacy/tests/route-patterns.test.ts b/dev-packages/e2e-tests/test-applications/hono-4-legacy/tests/route-patterns.test.ts
new file mode 100644
index 000000000000..bfebb9f5b6ab
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/hono-4-legacy/tests/route-patterns.test.ts
@@ -0,0 +1,178 @@
+import { expect, test } from '@playwright/test';
+import { waitForStreamedSpan, getSpanOp, collectStreamedSpansUntilSegment } from '@sentry-internal/test-utils';
+import { APP_NAME } from './constants';
+
+const PREFIX = '/test-routes';
+
+const REGISTRATION_STYLES = [
+ { name: 'direct method', path: '' },
+ { name: '.all()', path: '/all' },
+ { name: '.on()', path: '/on' },
+] as const;
+
+test.describe('HTTP methods', () => {
+ ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'].forEach(method => {
+ test(`sends transaction for ${method}`, async ({ baseURL }) => {
+ const segmentPromise = collectStreamedSpansUntilSegment(
+ APP_NAME,
+ segment => getSpanOp(segment) === 'http.server' && segment.name === `${method} ${PREFIX}`,
+ );
+
+ const response = await fetch(`${baseURL}${PREFIX}`, { method });
+ expect(response.status).toBe(200);
+
+ const segmentSpans = await segmentPromise;
+ const segment = segmentSpans.find(
+ segment => segment.is_segment && getSpanOp(segment) === 'http.server' && segment.name === `${method} ${PREFIX}`,
+ )!;
+ expect(segment.name).toBe(`${method} ${PREFIX}`);
+ expect(getSpanOp(segment)).toBe('http.server');
+ expect(segment.attributes?.['sentry.segment.name.source']?.value).toBe('route');
+
+ const spans = segmentSpans.filter(
+ span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment.span_id,
+ );
+ const middlewareSpans = spans.filter(s => getSpanOp(s) === 'middleware');
+ expect(middlewareSpans).toEqual([]);
+ });
+ });
+});
+
+test.describe('route registration styles', () => {
+ REGISTRATION_STYLES.forEach(({ name, path }) => {
+ test(`${name} sends transaction with route source`, async ({ baseURL }) => {
+ const segmentPromise = collectStreamedSpansUntilSegment(
+ APP_NAME,
+ segment => getSpanOp(segment) === 'http.server' && segment.name === `GET ${PREFIX}${path}`,
+ );
+
+ const response = await fetch(`${baseURL}${PREFIX}${path}`);
+ expect(response.status).toBe(200);
+
+ const segmentSpans = await segmentPromise;
+ const segment = segmentSpans.find(
+ segment =>
+ segment.is_segment && getSpanOp(segment) === 'http.server' && segment.name === `GET ${PREFIX}${path}`,
+ )!;
+ expect(segment.name).toBe(`GET ${PREFIX}${path}`);
+ expect(getSpanOp(segment)).toBe('http.server');
+ expect(segment.attributes?.['sentry.segment.name.source']?.value).toBe('route');
+
+ const spans = segmentSpans.filter(
+ span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment.span_id,
+ );
+ const middlewareSpans = spans.filter(s => getSpanOp(s) === 'middleware');
+ expect(middlewareSpans).toEqual([]);
+ });
+ });
+
+ [
+ { name: '.all()', path: '/all' },
+ { name: '.on()', path: '/on' },
+ ].forEach(({ name, path }) => {
+ test(`${name} responds to POST`, async ({ baseURL }) => {
+ const segmentPromise = collectStreamedSpansUntilSegment(
+ APP_NAME,
+ segment => getSpanOp(segment) === 'http.server' && segment.name === `POST ${PREFIX}${path}`,
+ );
+
+ const response = await fetch(`${baseURL}${PREFIX}${path}`, { method: 'POST' });
+ expect(response.status).toBe(200);
+
+ const segmentSpans = await segmentPromise;
+ const segment = segmentSpans.find(
+ segment =>
+ segment.is_segment && getSpanOp(segment) === 'http.server' && segment.name === `POST ${PREFIX}${path}`,
+ )!;
+ expect(segment.name).toBe(`POST ${PREFIX}${path}`);
+ expect(segment.attributes?.['sentry.segment.name.source']?.value).toBe('route');
+
+ const spans = segmentSpans.filter(
+ span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment.span_id,
+ );
+ const middlewareSpans = spans.filter(s => getSpanOp(s) === 'middleware');
+ expect(middlewareSpans).toEqual([]);
+ });
+ });
+});
+
+test.describe('request data extraction', () => {
+ test('includes method, url, and headers on span', async ({ baseURL }) => {
+ const segmentPromise = waitForStreamedSpan(
+ APP_NAME,
+ segment =>
+ segment.is_segment && getSpanOp(segment) === 'http.server' && segment.name === `GET ${PREFIX}/request-data`,
+ );
+
+ const response = await fetch(`${baseURL}${PREFIX}/request-data`);
+ expect(response.status).toBe(200);
+
+ const segment = await segmentPromise;
+ expect(segment.attributes['http.request.method']?.value).toBe('GET');
+ expect(segment.attributes['url.full']?.value).toContain(PREFIX);
+ expect(segment.attributes['http.request.header.host']).toBeDefined();
+ });
+
+ test('includes query_string when present', async ({ baseURL }) => {
+ const segmentPromise = waitForStreamedSpan(
+ APP_NAME,
+ segment =>
+ segment.is_segment &&
+ getSpanOp(segment) === 'http.server' &&
+ segment.name === `GET ${PREFIX}/query-test` &&
+ segment.attributes['url.query']?.value === 'foo=bar&baz=42',
+ );
+
+ const response = await fetch(`${baseURL}${PREFIX}/query-test?foo=bar&baz=42`);
+ expect(response.status).toBe(200);
+
+ const segment = await segmentPromise;
+
+ expect(segment.attributes['http.request.method']?.value).toBe('GET');
+ expect(segment.attributes['url.full']?.value).toContain(`${PREFIX}/query-test`);
+ expect(segment.attributes['url.query']?.value).toBe('foo=bar&baz=42');
+ });
+
+ test('includes request data for POST with headers', async ({ baseURL }) => {
+ const segmentPromise = waitForStreamedSpan(
+ APP_NAME,
+ segment =>
+ segment.is_segment && getSpanOp(segment) === 'http.server' && segment.name === `POST ${PREFIX}/request-data`,
+ );
+
+ const response = await fetch(`${baseURL}${PREFIX}/request-data`, {
+ method: 'POST',
+ headers: { 'X-Custom-Header': 'test-value' },
+ });
+ expect(response.status).toBe(200);
+
+ const segment = await segmentPromise;
+ expect(segment.attributes['http.request.method']?.value).toBe('POST');
+ expect(segment.attributes['url.full']?.value).toContain(PREFIX);
+ expect(segment.attributes['http.request.header.x-custom-header']?.value).toEqual(['test-value']);
+ });
+});
+
+test('async handler sends span', async ({ baseURL }) => {
+ const segmentPromise = collectStreamedSpansUntilSegment(
+ APP_NAME,
+ segment => getSpanOp(segment) === 'http.server' && segment.name === `GET ${PREFIX}/async`,
+ );
+
+ const response = await fetch(`${baseURL}${PREFIX}/async`);
+ expect(response.status).toBe(200);
+
+ const segmentSpans = await segmentPromise;
+ const segment = segmentSpans.find(
+ segment => segment.is_segment && getSpanOp(segment) === 'http.server' && segment.name === `GET ${PREFIX}/async`,
+ )!;
+ expect(segment.name).toBe(`GET ${PREFIX}/async`);
+ expect(getSpanOp(segment)).toBe('http.server');
+ expect(segment.attributes?.['sentry.segment.name.source']?.value).toBe('route');
+
+ const spans = segmentSpans.filter(
+ span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment.span_id,
+ );
+ const middlewareSpans = spans.filter(s => getSpanOp(s) === 'middleware');
+ expect(middlewareSpans).toEqual([]);
+});
diff --git a/dev-packages/e2e-tests/test-applications/hono-4-legacy/tests/tracing.test.ts b/dev-packages/e2e-tests/test-applications/hono-4-legacy/tests/tracing.test.ts
new file mode 100644
index 000000000000..23ed5c7837fc
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/hono-4-legacy/tests/tracing.test.ts
@@ -0,0 +1,144 @@
+import { expect, test } from '@playwright/test';
+import { waitForStreamedSpan, getSpanOp } from '@sentry-internal/test-utils';
+import { APP_NAME, RUNTIME } from './constants';
+
+test('sends a span for the index route', async ({ baseURL }) => {
+ const segmentPromise = waitForStreamedSpan(
+ APP_NAME,
+ segment => segment.is_segment && getSpanOp(segment) === 'http.server' && segment.name === 'GET /',
+ );
+
+ const response = await fetch(`${baseURL}/`);
+ expect(response.status).toBe(200);
+
+ const segment = await segmentPromise;
+ expect(segment.name).toBe('GET /');
+ expect(getSpanOp(segment)).toBe('http.server');
+});
+
+test('sends a span for a parameterized route', async ({ baseURL }) => {
+ const segmentPromise = waitForStreamedSpan(
+ APP_NAME,
+ segment => segment.is_segment && getSpanOp(segment) === 'http.server' && !!segment.name?.includes('/test-param/'),
+ );
+
+ const response = await fetch(`${baseURL}/test-param/123`);
+ expect(response.status).toBe(200);
+
+ const segment = await segmentPromise;
+ expect(segment.name).toBe('GET /test-param/:paramId');
+ expect(getSpanOp(segment)).toBe('http.server');
+});
+
+test('attaches HTTP connection info to the server span', async ({ baseURL, page }) => {
+ page.on('console', msg => {
+ console.log(`PAGE LOG: ${msg.text()}`);
+ });
+ const segmentPromise = waitForStreamedSpan(
+ APP_NAME,
+ segment => segment.is_segment && getSpanOp(segment) === 'http.server' && segment.name === 'GET /',
+ );
+
+ const response = await fetch(`${baseURL}/`);
+ expect(response.status).toBe(200);
+
+ const segment = await segmentPromise;
+ const data = segment.attributes ?? {};
+
+ expect(data['client.address']?.value).toEqual(expect.any(String));
+ expect(data['network.peer.address']?.value).toBe(data['client.address']?.value);
+
+ if (RUNTIME !== 'deno') {
+ // Only exposed in `hono/deno`
+ expect(data['network.transport']?.value).toBeUndefined();
+ } else {
+ expect(data['network.transport']?.value).toMatch(/tcp/);
+ }
+
+ if (RUNTIME === 'node' || RUNTIME === 'bun') {
+ // Node (@hono/node-server) and Bun expose socket-level port and address family.
+ expect(data['client.port']?.value).toEqual(expect.any(Number));
+ expect(data['network.peer.port']?.value).toBe(data['client.port']?.value);
+ expect(data['network.type']?.value).toMatch(/^ipv[46]$/);
+ } else if (RUNTIME === 'deno') {
+ expect(data['client.port']?.value).toEqual(expect.any(Number));
+ expect(data['network.peer.port']?.value).toBe(data['client.port']?.value);
+ } else if (RUNTIME === 'cloudflare') {
+ // Cloudflare Workers expose no port, address family, or transport.
+ // This could change in the future and checking for the absence of these fields allows us to notice if/when that happens.
+ expect(data['client.port']?.value).toBeUndefined();
+ expect(data['network.peer.port']?.value).toBeUndefined();
+ expect(data['network.type']?.value).toBeUndefined();
+ } else {
+ throw new Error(`No tests for runtime: ${RUNTIME}`);
+ }
+});
+
+// Regression guard against connection info attributes.
+// The conninfo middleware must only *add* attributes, never replace or clear existing ones.
+// These are the baseline attributes the server transaction carries *without* the conninfo feature
+test("preserves the baseline server.*, client.* and network.* server span attributes that the SDK sends without Hono's conninfo", async ({
+ baseURL,
+}) => {
+ const segmentPromise = waitForStreamedSpan(
+ APP_NAME,
+ segment => segment.is_segment && getSpanOp(segment) === 'http.server' && segment.name === 'GET /',
+ );
+
+ const response = await fetch(`${baseURL}/`);
+ expect(response.status).toBe(200);
+
+ const segment = await segmentPromise;
+ const data = segment.attributes ?? {};
+
+ if (RUNTIME === 'node') {
+ expect(data['server.address']?.value).toBe('localhost');
+ expect(data['server.port']?.value).toBe(Number(new URL(baseURL!).port));
+ expect(data['client.address']?.value).toEqual(expect.any(String));
+ expect(data['client.port']?.value).toEqual(expect.any(Number));
+ expect(data['network.type']?.value).toMatch(/^ipv[46]$/);
+ expect(data['network.protocol.name']?.value).toBe('http');
+ expect(data['network.protocol.version']?.value).toBe('1.1');
+ expect(data['network.transport']?.value).toBeUndefined();
+ expect(data['network.local.port']?.value).toBe(data['server.port']?.value);
+ expect(data['network.local.address']?.value).toEqual(expect.any(String));
+ expect(data['network.peer.address']?.value).toBe(data['client.address']?.value);
+ expect(data['network.peer.port']?.value).toBe(data['client.port']?.value);
+ } else if (RUNTIME === 'bun') {
+ expect(data['client.address']?.value).toEqual(expect.any(String));
+ expect(data['client.port']?.value).toEqual(expect.any(Number));
+ expect(data['network.peer.address']?.value).toBe(data['client.address']?.value);
+ expect(data['network.peer.port']?.value).toBe(data['client.port']?.value);
+ expect(data['network.type']?.value).toMatch(/^ipv[46]$/);
+ } else if (RUNTIME === 'cloudflare') {
+ expect(data['server.address']?.value).toBe('localhost');
+ expect(data['client.address']?.value).toBe('::1');
+ expect(data['network.peer.address']?.value).toBe(data['client.address']?.value);
+ expect(data['network.protocol.name']?.value).toBe('http');
+ expect(data['network.protocol.version']?.value).toBe('1.1');
+ } else if (RUNTIME === 'deno') {
+ expect(data['server.address']?.value).toBe('localhost');
+ expect(data['client.address']?.value).toEqual(expect.any(String));
+ expect(data['client.port']?.value).toEqual(expect.any(Number));
+ expect(data['network.peer.address']?.value).toBe(data['client.address']?.value);
+ expect(data['network.peer.port']?.value).toBe(data['client.port']?.value);
+ expect(data['network.transport']?.value).toBe('tcp');
+ expect(data['network.protocol.name']?.value).toBe('http');
+ } else {
+ throw new Error(`No tests for runtime: ${RUNTIME}`);
+ }
+});
+
+test('sends a span for a route that throws', async ({ baseURL }) => {
+ const segmentPromise = waitForStreamedSpan(
+ APP_NAME,
+ segment => segment.is_segment && getSpanOp(segment) === 'http.server' && !!segment.name?.includes('/error/'),
+ );
+
+ await fetch(`${baseURL}/error/test-cause`);
+
+ const segment = await segmentPromise;
+ expect(segment.name).toBe('GET /error/:cause');
+ expect(getSpanOp(segment)).toBe('http.server');
+ expect(segment?.status).toBe('error');
+});
diff --git a/dev-packages/e2e-tests/test-applications/hono-4-legacy/tsconfig.json b/dev-packages/e2e-tests/test-applications/hono-4-legacy/tsconfig.json
new file mode 100644
index 000000000000..3c4abeff44d6
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/hono-4-legacy/tsconfig.json
@@ -0,0 +1,13 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "strict": true,
+ "skipLibCheck": true,
+ "lib": ["ESNext"],
+ "jsx": "react-jsx",
+ "jsxImportSource": "hono/jsx",
+ "types": ["@cloudflare/workers-types"]
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/hono-4-legacy/wrangler.jsonc b/dev-packages/e2e-tests/test-applications/hono-4-legacy/wrangler.jsonc
new file mode 100644
index 000000000000..a3c646c03aa9
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/hono-4-legacy/wrangler.jsonc
@@ -0,0 +1,7 @@
+{
+ "$schema": "node_modules/wrangler/config-schema.json",
+ "name": "hono-4-legacy",
+ "main": "src/entry.cloudflare.ts",
+ "compatibility_date": "2026-04-20",
+ "compatibility_flags": ["nodejs_compat"],
+}
diff --git a/dev-packages/e2e-tests/test-applications/hono-4/build-bun.ts b/dev-packages/e2e-tests/test-applications/hono-4/build-bun.ts
new file mode 100644
index 000000000000..fd337d063992
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/hono-4/build-bun.ts
@@ -0,0 +1,28 @@
+// Builds `src/entry.bun.ts` with the orchestrion `bun build` plugin, emitting `dist/entry.bun.js`
+// for the server to run. The plugin injects the `orchestrion:hono:context` diagnostics
+// channel into the bundled `hono`, which the `honoIntegration` default subscribes to.
+
+// @ts-ignore -- subpath export resolved by Bun at runtime; the package tsconfig's node module
+// resolution can't see `exports` subpaths.
+import { sentryBunPlugin } from '@sentry/bun/plugin';
+import { join } from 'path';
+
+void (async () => {
+ const result = await Bun.build({
+ entrypoints: [join(__dirname, 'src/entry.bun.ts')],
+ target: 'bun',
+ outdir: join(__dirname, 'dist'),
+ // `@sentry/bun` (and its deps) stay external, so we don't bundle the whole SDK stack.
+ external: ['@sentry/bun'],
+ plugins: [sentryBunPlugin()],
+ });
+
+ if (!result.success) {
+ // eslint-disable-next-line no-console
+ console.error('BUILD_FAILED', result.logs);
+ process.exit(1);
+ }
+
+ // eslint-disable-next-line no-console
+ console.log('BUILD_OK');
+})();
diff --git a/dev-packages/e2e-tests/test-applications/hono-4/deno.json b/dev-packages/e2e-tests/test-applications/hono-4/deno.json
index d7748bc95ecb..7a7e681c4a82 100644
--- a/dev-packages/e2e-tests/test-applications/hono-4/deno.json
+++ b/dev-packages/e2e-tests/test-applications/hono-4/deno.json
@@ -1,7 +1,5 @@
{
"imports": {
- "@sentry/hono": "npm:@sentry/hono",
- "@sentry/hono/deno": "npm:@sentry/hono/deno",
"@sentry/deno": "npm:@sentry/deno",
"@sentry/core": "npm:@sentry/core",
"@opentelemetry/api": "npm:@opentelemetry/api@^1.9.0",
diff --git a/dev-packages/e2e-tests/test-applications/hono-4/package.json b/dev-packages/e2e-tests/test-applications/hono-4/package.json
index db3b18166812..a3fd78b9ec67 100644
--- a/dev-packages/e2e-tests/test-applications/hono-4/package.json
+++ b/dev-packages/e2e-tests/test-applications/hono-4/package.json
@@ -4,30 +4,35 @@
"version": "0.0.0",
"private": true,
"scripts": {
- "dev:cf": "wrangler dev --var \"E2E_TEST_DSN:$E2E_TEST_DSN\" --log-level=$(test $CI && echo 'none' || echo 'log')",
"dev:node": "node --import tsx/esm --import ./src/instrument.node.ts src/entry.node.ts",
- "dev:bun": "bun src/entry.bun.ts",
- "dev:deno": "deno run --allow-net --allow-env --allow-read src/entry.deno.ts",
- "build": "wrangler deploy --dry-run",
- "test:build": "pnpm install && pnpm build",
- "test:assert": "TEST_ENV=production playwright test"
+ "dev:bun": "bun run dist/entry.bun.js",
+ "dev:deno": "deno run --allow-net --allow-env --allow-read --preload=npm:@sentry/deno/import src/entry.deno.ts",
+ "dev:cloudflare": "wrangler dev --config ./dist/hono_4/wrangler.json --var \"E2E_TEST_DSN:$E2E_TEST_DSN\" --log-level=$(test $CI && echo 'none' || echo 'log')",
+ "test:build": "pnpm install",
+ "test": "TEST_ENV=production playwright test",
+ "test:assert": "pnpm test:assert:node",
+ "test:assert:node": "RUNTIME=node pnpm test",
+ "test:assert:bun": "bun run build-bun.ts && RUNTIME=bun pnpm test",
+ "test:assert:deno": "RUNTIME=deno pnpm test",
+ "test:assert:cloudflare": "vite build && RUNTIME=cloudflare pnpm test"
},
"dependencies": {
"@sentry/bun": "latest || *",
"@sentry/cloudflare": "latest || *",
"@sentry/deno": "latest || *",
- "@sentry/hono": "latest || *",
"@sentry/node": "latest || *",
"@hono/node-server": "^2.0.5",
"hono": "^4.13.1"
},
"devDependencies": {
"@playwright/test": "~1.63.0",
+ "@cloudflare/vite-plugin": "^1.47.0",
"@cloudflare/workers-types": "^4.20240725.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
"tsx": "4.21.0",
"typescript": "^5.5.2",
- "wrangler": "^4.61.0"
+ "vite": "^8.1.5",
+ "wrangler": "^4.114.0"
},
"volta": {
"node": "24.15.0",
@@ -36,16 +41,16 @@
"sentryTest": {
"variants": [
{
- "assert-command": "RUNTIME=node pnpm test:assert",
- "label": "hono-4 (node)"
- },
- {
- "assert-command": "RUNTIME=bun pnpm test:assert",
+ "assert-command": "pnpm test:assert:bun",
"label": "hono-4 (bun)"
},
{
- "assert-command": "RUNTIME=deno pnpm test:assert",
+ "assert-command": "pnpm test:assert:deno",
"label": "hono-4 (deno)"
+ },
+ {
+ "assert-command": "pnpm test:assert:cloudflare",
+ "label": "hono-4 (cloudflare)"
}
]
}
diff --git a/dev-packages/e2e-tests/test-applications/hono-4/playwright.config.ts b/dev-packages/e2e-tests/test-applications/hono-4/playwright.config.ts
index 5087b887e1f2..b8c04df05aee 100644
--- a/dev-packages/e2e-tests/test-applications/hono-4/playwright.config.ts
+++ b/dev-packages/e2e-tests/test-applications/hono-4/playwright.config.ts
@@ -10,7 +10,7 @@ if (!testEnv) {
const APP_PORT = 38787;
const startCommands: Record = {
- cloudflare: `pnpm dev:cf --port ${APP_PORT}`,
+ cloudflare: `pnpm dev:cloudflare --port ${APP_PORT}`,
node: `pnpm dev:node`,
bun: `pnpm dev:bun`,
deno: `pnpm dev:deno`,
diff --git a/dev-packages/e2e-tests/test-applications/hono-4/src/entry.bun.ts b/dev-packages/e2e-tests/test-applications/hono-4/src/entry.bun.ts
index e057eb78d4c5..b47f15997480 100644
--- a/dev-packages/e2e-tests/test-applications/hono-4/src/entry.bun.ts
+++ b/dev-packages/e2e-tests/test-applications/hono-4/src/entry.bun.ts
@@ -1,18 +1,11 @@
+// Import the Sentry init first so `honoIntegration` is set up (and subscribed to Hono's per-request
+// Context channel) before any request is handled.
+import './instrument.bun';
import { Hono } from 'hono';
-import { sentry } from '@sentry/hono/bun';
import { addRoutes } from './routes';
const app = new Hono();
-app.use(
- sentry(app, {
- dsn: process.env.E2E_TEST_DSN,
- environment: 'qa',
- tracesSampleRate: 1.0,
- tunnel: 'http://localhost:3031/',
- }),
-);
-
addRoutes(app);
const port = Number(process.env.PORT || 38787);
diff --git a/dev-packages/e2e-tests/test-applications/hono-4/src/entry.cloudflare.ts b/dev-packages/e2e-tests/test-applications/hono-4/src/entry.cloudflare.ts
index e348dde56226..7fb84f59947a 100644
--- a/dev-packages/e2e-tests/test-applications/hono-4/src/entry.cloudflare.ts
+++ b/dev-packages/e2e-tests/test-applications/hono-4/src/entry.cloudflare.ts
@@ -1,18 +1,8 @@
import { Hono } from 'hono';
-import { sentry } from '@sentry/hono/cloudflare';
import { addRoutes } from './routes';
const app = new Hono<{ Bindings: { E2E_TEST_DSN: string } }>();
-app.use(
- sentry(app, env => ({
- dsn: env.E2E_TEST_DSN,
- environment: 'qa',
- tracesSampleRate: 1.0,
- tunnel: 'http://localhost:3031/',
- })),
-);
-
addRoutes(app);
export default app;
diff --git a/dev-packages/e2e-tests/test-applications/hono-4/src/entry.deno.ts b/dev-packages/e2e-tests/test-applications/hono-4/src/entry.deno.ts
index 15bd12a74111..cc2411fd6cf9 100644
--- a/dev-packages/e2e-tests/test-applications/hono-4/src/entry.deno.ts
+++ b/dev-packages/e2e-tests/test-applications/hono-4/src/entry.deno.ts
@@ -1,19 +1,11 @@
+// Import the Sentry init first so `honoIntegration` is set up (and subscribed to Hono's per-request
+// Context channel) before any request is handled.
+import './instrument.deno';
import { Hono } from 'hono';
-import { sentry } from '@sentry/hono/deno';
import { addRoutes } from './routes';
const app = new Hono();
-app.use(
- sentry(app, {
- dsn: Deno.env.get('E2E_TEST_DSN'),
- environment: 'qa',
- dataCollection: {},
- tracesSampleRate: 1.0,
- tunnel: 'http://localhost:3031/',
- }),
-);
-
addRoutes(app);
const port = Number(Deno.env.get('PORT') || 38787);
diff --git a/dev-packages/e2e-tests/test-applications/hono-4/src/entry.node.ts b/dev-packages/e2e-tests/test-applications/hono-4/src/entry.node.ts
index 898a92e08be4..c67b3a206ef2 100644
--- a/dev-packages/e2e-tests/test-applications/hono-4/src/entry.node.ts
+++ b/dev-packages/e2e-tests/test-applications/hono-4/src/entry.node.ts
@@ -1,16 +1,14 @@
-import { Hono } from 'hono';
-import { sentry } from '@sentry/hono/node';
import { serve } from '@hono/node-server';
+import { Hono } from 'hono';
import { addRoutes } from './routes';
const app = new Hono();
-app.use(sentry(app));
-
addRoutes(app);
const port = Number(process.env.PORT || 38787);
serve({ fetch: app.fetch, port }, () => {
+ // eslint-disable-next-line no-console
console.log(`Hono (Node) listening on port ${port}`);
});
diff --git a/dev-packages/e2e-tests/test-applications/hono-4/src/instrument.bun.ts b/dev-packages/e2e-tests/test-applications/hono-4/src/instrument.bun.ts
new file mode 100644
index 000000000000..9cb58d8c2aa2
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/hono-4/src/instrument.bun.ts
@@ -0,0 +1,8 @@
+import * as Sentry from '@sentry/bun';
+
+Sentry.init({
+ dsn: process.env.E2E_TEST_DSN,
+ environment: 'qa',
+ tracesSampleRate: 1.0,
+ tunnel: 'http://localhost:3031/',
+});
diff --git a/dev-packages/e2e-tests/test-applications/hono-4/src/instrument.deno.ts b/dev-packages/e2e-tests/test-applications/hono-4/src/instrument.deno.ts
new file mode 100644
index 000000000000..49c9d1d452f1
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/hono-4/src/instrument.deno.ts
@@ -0,0 +1,9 @@
+import * as Sentry from '@sentry/deno';
+
+Sentry.init({
+ dsn: Deno.env.get('E2E_TEST_DSN'),
+ environment: 'qa',
+ dataCollection: {},
+ tracesSampleRate: 1.0,
+ tunnel: 'http://localhost:3031/',
+});
diff --git a/dev-packages/e2e-tests/test-applications/hono-4/src/instrument.node.ts b/dev-packages/e2e-tests/test-applications/hono-4/src/instrument.node.ts
index 82f2a3864125..5c3e586fd48b 100644
--- a/dev-packages/e2e-tests/test-applications/hono-4/src/instrument.node.ts
+++ b/dev-packages/e2e-tests/test-applications/hono-4/src/instrument.node.ts
@@ -1,4 +1,4 @@
-import * as Sentry from '@sentry/hono/node';
+import * as Sentry from '@sentry/node';
Sentry.init({
dsn: process.env.E2E_TEST_DSN,
diff --git a/dev-packages/e2e-tests/test-applications/hono-4/src/instrument.server.ts b/dev-packages/e2e-tests/test-applications/hono-4/src/instrument.server.ts
new file mode 100644
index 000000000000..d1990a0ea930
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/hono-4/src/instrument.server.ts
@@ -0,0 +1,8 @@
+import { defineCloudflareOptions } from '@sentry/cloudflare';
+
+export default defineCloudflareOptions<{ E2E_TEST_DSN: string }>(env => ({
+ dsn: env.E2E_TEST_DSN,
+ environment: 'qa',
+ tracesSampleRate: 1.0,
+ tunnel: 'http://localhost:3031/',
+}));
diff --git a/dev-packages/e2e-tests/test-applications/hono-4/src/middleware.ts b/dev-packages/e2e-tests/test-applications/hono-4/src/middleware.ts
index cc7bfae9896d..9304a980dff3 100644
--- a/dev-packages/e2e-tests/test-applications/hono-4/src/middleware.ts
+++ b/dev-packages/e2e-tests/test-applications/hono-4/src/middleware.ts
@@ -1,17 +1,34 @@
import type { MiddlewareHandler } from 'hono';
-export const middlewareA: MiddlewareHandler = async function middlewareA(c, next) {
+// Defined as anonymous function expressions so the function name is inferred from the `const`
+// binding. A named expression (`const middlewareA = async function middlewareA() {}`) collides with
+// the binding when bundled, and Bun renames the inner function (→ `middlewareA2`), which would then
+// surface as the middleware span name. The inferred name stays stable across all runtimes.
+export const middlewareA: MiddlewareHandler = async function (c, next) {
// Add some delay
await new Promise(resolve => setTimeout(resolve, 50));
await next();
};
-export const middlewareB: MiddlewareHandler = async function middlewareB(_c, next) {
+export const middlewareB: MiddlewareHandler = async function (_c, next) {
// Add some delay
await new Promise(resolve => setTimeout(resolve, 60));
await next();
};
-export const failingMiddleware: MiddlewareHandler = async function failingMiddleware(_c, _next) {
- throw new Error('Middleware error');
+let failingMiddlewareCount = 0;
+export const failingMiddleware: MiddlewareHandler = async function (_c, _next) {
+ // Each throw gets a unique suffix so the Dedupe integration doesn't collapse the identical errors
+ // that several tests (and their retries) trigger through this shared middleware — otherwise only the
+ // first would be reported and the other tests' `waitForError` would time out. Tests match on the
+ // stable `Middleware error` prefix.
+ throw new Error(`Middleware error #${(failingMiddlewareCount += 1)}`);
+};
+
+// Intentionally a NAMED function expression (unlike the anonymous ones above) so the named-function
+// path stays covered: the span name is taken from the function's own name. Under bundled Bun the inner
+// name collides with the `const` binding and is suffixed (→ `namedMiddleware2`), so the test matches on
+// the `namedMiddleware` prefix rather than an exact name.
+export const namedMiddleware: MiddlewareHandler = async function namedMiddleware(_c, next) {
+ await next();
};
diff --git a/dev-packages/e2e-tests/test-applications/hono-4/src/route-groups/test-middleware.ts b/dev-packages/e2e-tests/test-applications/hono-4/src/route-groups/test-middleware.ts
index d82201b7cdb3..6f21c1cac33b 100644
--- a/dev-packages/e2e-tests/test-applications/hono-4/src/route-groups/test-middleware.ts
+++ b/dev-packages/e2e-tests/test-applications/hono-4/src/route-groups/test-middleware.ts
@@ -1,5 +1,5 @@
import { Hono } from 'hono';
-import { failingMiddleware, middlewareA, middlewareB } from '../middleware';
+import { failingMiddleware, middlewareA, middlewareB, namedMiddleware } from '../middleware';
const middlewareRoutes = new Hono();
@@ -8,11 +8,13 @@ middlewareRoutes.get('/anonymous', c => c.json({ middleware: 'anonymous' }));
middlewareRoutes.get('/multi', c => c.json({ middleware: 'multi' }));
middlewareRoutes.get('/error', c => c.text('should not reach'));
middlewareRoutes.get('/param/:id', c => c.json({ paramId: c.req.param('id') }));
+middlewareRoutes.get('/declared', c => c.json({ middleware: 'declared' }));
// Self-contained sub-app registering its own middleware via .use()
const subAppWithMiddleware = new Hono();
subAppWithMiddleware.use('/named/*', middlewareA);
+subAppWithMiddleware.use('/declared/*', namedMiddleware);
subAppWithMiddleware.use('/anonymous/*', async (c, next) => {
c.header('X-Custom', 'anonymous');
await next();
diff --git a/dev-packages/e2e-tests/test-applications/hono-4/src/route-groups/test-multi-fetch.ts b/dev-packages/e2e-tests/test-applications/hono-4/src/route-groups/test-multi-fetch.ts
index 58eb5b504640..ed9f0a3e131b 100644
--- a/dev-packages/e2e-tests/test-applications/hono-4/src/route-groups/test-multi-fetch.ts
+++ b/dev-packages/e2e-tests/test-applications/hono-4/src/route-groups/test-multi-fetch.ts
@@ -28,6 +28,14 @@ inventoryApp.get('/item/:productId/stock', c => {
return c.json({ productId, inStock: item.stock > 0, quantity: item.stock });
});
+// Simulates an inner-route failure (a plain 5xx Error, not an HTTPException) reached only via an
+// internal .request() — used by the storefront's `/degraded` route below. Each throw gets a unique
+// suffix so the Dedupe integration doesn't collapse repeats across test retries.
+let dbErrorCount = 0;
+inventoryApp.get('/item/:productId/db-error', () => {
+ throw new Error(`inventory db is down #${(dbErrorCount += 1)}`);
+});
+
// Storefront service — orchestrates internal .request() calls to inventoryApp.
const storefrontApp = new Hono();
@@ -83,6 +91,17 @@ storefrontApp.get('/product/:productId/availability', async c => {
});
});
+// Degraded response: the inner route throws, but the outer handler swallows the failed internal
+// response and returns a 200 instead of propagating. The inner error should still reach Sentry (auto
+// instrumentation); the outer request stays healthy.
+storefrontApp.get('/product/:productId/degraded', async c => {
+ const res = await inventoryApp.request(`/item/${c.req.param('productId')}/db-error`);
+ if (!res.ok) {
+ return c.json({ product: null, degraded: true }, 200);
+ }
+ return c.json({ product: await res.json() });
+});
+
// Error propagation: internal 404 causes the handler to throw a plain Error
storefrontApp.get('/product-or-throw/:productId', async c => {
const res = await inventoryApp.request(`/item/${c.req.param('productId')}`);
diff --git a/dev-packages/e2e-tests/test-applications/hono-4/src/routes.ts b/dev-packages/e2e-tests/test-applications/hono-4/src/routes.ts
index b095618fb52b..008a1ca6fd53 100644
--- a/dev-packages/e2e-tests/test-applications/hono-4/src/routes.ts
+++ b/dev-packages/e2e-tests/test-applications/hono-4/src/routes.ts
@@ -1,6 +1,6 @@
import { type Hono as HonoType, Hono } from 'hono';
import { HTTPException } from 'hono/http-exception';
-import { failingMiddleware, middlewareA, middlewareB } from './middleware';
+import { failingMiddleware, middlewareA, middlewareB, namedMiddleware } from './middleware';
import { errorRoutes } from './route-groups/test-errors';
import { middlewareRoutes, subAppWithInlineMiddleware, subAppWithMiddleware } from './route-groups/test-middleware';
import { multiFetchRoutes } from './route-groups/test-multi-fetch';
@@ -42,6 +42,7 @@ export function addRoutes(app: HonoType<{ Bindings?: { E2E_TEST_DSN: string } }>
app.use('/test-middleware/multi/*', middlewareA, middlewareB);
app.use('/test-middleware/error/*', failingMiddleware);
app.use('/test-middleware/param/*', middlewareA);
+ app.use('/test-middleware/declared/*', namedMiddleware);
app.route('/test-middleware', middlewareRoutes);
// Sub-app middleware: registered on the sub-app, wrapped at mount time by route() patching
diff --git a/dev-packages/e2e-tests/test-applications/hono-4/tests/basepath-and-late-routes.test.ts b/dev-packages/e2e-tests/test-applications/hono-4/tests/basepath-and-late-routes.test.ts
index c7ad43b6b945..6295dc4c9e32 100644
--- a/dev-packages/e2e-tests/test-applications/hono-4/tests/basepath-and-late-routes.test.ts
+++ b/dev-packages/e2e-tests/test-applications/hono-4/tests/basepath-and-late-routes.test.ts
@@ -42,8 +42,7 @@ test.describe('basePath with sub-app routes', () => {
});
});
-// TODO: this test is currently skipped because we do not yet support middleware registered on new instances (e.g. here via .basePath(..).use(...)).
-test.skip('.basePath() middleware instrumentation', () => {
+test.describe('.basePath() middleware instrumentation', () => {
test('creates middleware span for .use() on .basePath() clone', async ({ baseURL }) => {
const segmentPromise = collectStreamedSpansUntilSegment(
APP_NAME,
diff --git a/dev-packages/e2e-tests/test-applications/hono-4/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/hono-4/tests/errors.test.ts
index e3c44c67d4d6..b315233ec15b 100644
--- a/dev-packages/e2e-tests/test-applications/hono-4/tests/errors.test.ts
+++ b/dev-packages/e2e-tests/test-applications/hono-4/tests/errors.test.ts
@@ -5,7 +5,7 @@ import {
getSpanOp,
collectStreamedSpansUntilSegment,
} from '@sentry-internal/test-utils';
-import { APP_NAME, RUNTIME } from './constants';
+import { APP_NAME } from './constants';
test.describe('route handler errors', () => {
test('captures error with mechanism and trace correlation', async ({ baseURL }) => {
@@ -103,81 +103,37 @@ test.describe('HTTPException errors', () => {
});
});
- // On Node/Bun, httpServerSpansIntegration drops transactions for 3xx/4xx responses (ignoreStatusCodes), so we just use a request guard.
- // On Cloudflare the transaction is available, and we additionally verify its name.
- [301, 302].forEach(code => {
- test(`does not capture ${code} HTTPException`, async ({ baseURL }) => {
- let errorEventOccurred = false;
-
- waitForError(APP_NAME, event => {
- if (event.exception?.values?.[0]?.value === `HTTPException ${code}`) {
- errorEventOccurred = true;
- }
- return false;
- });
-
- const segmentPromise = waitForStreamedSpan(
- APP_NAME,
- segment =>
- segment.is_segment &&
- (RUNTIME === 'cloudflare'
- ? getSpanOp(segment) === 'http.server' && !!segment.name?.includes('/http-exception/')
- : getSpanOp(segment) === 'http.server' && segment.name === 'GET /'),
- );
-
- const response = await fetch(`${baseURL}/http-exception/${code}`, { redirect: 'manual' });
- expect(response.status).toBe(code);
-
- if (RUNTIME !== 'cloudflare') {
- // Simple request guard for non-Cloudflare runtimes since the other transaction is dropped for 4xx responses
- await fetch(`${baseURL}/`);
- }
-
- const segment = await segmentPromise;
-
- if (RUNTIME === 'cloudflare') {
- expect(segment.name).toBe('GET /http-exception/:code');
+ // 3xx/4xx responses must not be captured as errors. Some runtimes drop the transaction for those
+ // status codes (httpServerSpansIntegration's ignoreStatusCodes), so instead of waiting on the
+ // HTTPException route's own (possibly dropped) transaction, we wait on a defined 2xx route's
+ // transaction as a flush guard — that one is produced on every runtime — then assert no error was
+ // captured. (Parametrized route naming is covered by tracing.test.ts on 2xx routes.)
+ const expectHttpExceptionNotCaptured = async (baseURL: string, code: number): Promise => {
+ let errorEventOccurred = false;
+ waitForError(APP_NAME, event => {
+ if (event.exception?.values?.[0]?.value === `HTTPException ${code}`) {
+ errorEventOccurred = true;
}
-
- expect(errorEventOccurred).toBe(false);
+ return false;
});
- });
- [401, 403, 404].forEach(code => {
- test(`does not capture ${code} HTTPException`, async ({ baseURL }) => {
- let errorEventOccurred = false;
-
- waitForError(APP_NAME, event => {
- if (event.exception?.values?.[0]?.value === `HTTPException ${code}`) {
- errorEventOccurred = true;
- }
- return false;
- });
-
- const segmentPromise = waitForStreamedSpan(
- APP_NAME,
- segment =>
- segment.is_segment &&
- (RUNTIME === 'cloudflare'
- ? getSpanOp(segment) === 'http.server' && !!segment.name?.includes('/http-exception/')
- : getSpanOp(segment) === 'http.server' && segment.name === 'GET /'),
- );
-
- const response = await fetch(`${baseURL}/http-exception/${code}`);
- expect(response.status).toBe(code);
-
- if (RUNTIME !== 'cloudflare') {
- // Simple request guard for non-Cloudflare runtimes since the other transaction is dropped for 4xx responses
- await fetch(`${baseURL}/`);
- }
+ const guardPromise = waitForStreamedSpan(
+ APP_NAME,
+ segment => segment.is_segment && getSpanOp(segment) === 'http.server' && segment.name === 'GET /',
+ );
- const segment = await segmentPromise;
+ const response = await fetch(`${baseURL}/http-exception/${code}`, { redirect: 'manual' });
+ expect(response.status).toBe(code);
- if (RUNTIME === 'cloudflare') {
- expect(segment.name).toBe('GET /http-exception/:code');
- }
+ await fetch(`${baseURL}/`);
+ await guardPromise;
+
+ expect(errorEventOccurred).toBe(false);
+ };
- expect(errorEventOccurred).toBe(false);
+ [301, 302, 401, 403, 404].forEach(code => {
+ test(`does not capture ${code} HTTPException`, async ({ baseURL }) => {
+ await expectHttpExceptionNotCaptured(baseURL!, code);
});
});
});
@@ -226,41 +182,18 @@ test.describe('middleware errors', () => {
return false;
});
- const segmentPromise = collectStreamedSpansUntilSegment(APP_NAME, segment => {
- if (RUNTIME === 'cloudflare') {
- return (
- getSpanOp(segment) === 'http.server' && !!segment.name?.includes('/test-errors/middleware-http-exception-4xx')
- );
- }
- return getSpanOp(segment) === 'http.server' && segment.name === 'GET /';
- });
+ // Guard on a defined 2xx route's transaction (produced on every runtime) rather than the 4xx
+ // route's own transaction, which some runtimes drop — then assert no error was captured.
+ const guardPromise = waitForStreamedSpan(
+ APP_NAME,
+ segment => segment.is_segment && getSpanOp(segment) === 'http.server' && segment.name === 'GET /',
+ );
const response = await fetch(`${baseURL}/test-errors/middleware-http-exception-4xx`);
expect(response.status).toBe(401);
- if (RUNTIME !== 'cloudflare') {
- await fetch(`${baseURL}/`);
- }
-
- const segmentSpans = await segmentPromise;
- const segment = segmentSpans.find(segment => {
- if (!segment.is_segment) return false;
- if (RUNTIME === 'cloudflare') {
- return (
- getSpanOp(segment) === 'http.server' && !!segment.name?.includes('/test-errors/middleware-http-exception-4xx')
- );
- }
- return getSpanOp(segment) === 'http.server' && segment.name === 'GET /';
- })!;
-
- if (RUNTIME === 'cloudflare') {
- expect(segment.name).toBe('GET /test-errors/middleware-http-exception-4xx');
-
- const middlewareSpan = segmentSpans
- .filter(span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment.span_id)
- .find(s => getSpanOp(s) === 'middleware');
- expect(middlewareSpan?.status).not.toBe('error');
- }
+ await fetch(`${baseURL}/`);
+ await guardPromise;
expect(errorEventOccurred).toBe(false);
});
diff --git a/dev-packages/e2e-tests/test-applications/hono-4/tests/middleware.test.ts b/dev-packages/e2e-tests/test-applications/hono-4/tests/middleware.test.ts
index 111ff3bfe34a..d760f12e6451 100644
--- a/dev-packages/e2e-tests/test-applications/hono-4/tests/middleware.test.ts
+++ b/dev-packages/e2e-tests/test-applications/hono-4/tests/middleware.test.ts
@@ -78,6 +78,38 @@ for (const { name, prefix } of SCENARIOS) {
expect(anonymousSpan?.status).not.toBe('error');
});
+ test('creates a span for a named-function middleware (name from the function binding)', async ({ baseURL }) => {
+ const segmentPromise = collectStreamedSpansUntilSegment(
+ APP_NAME,
+ segment => getSpanOp(segment) === 'http.server' && !!segment.name?.includes(`${prefix}/declared`),
+ );
+
+ const response = await fetch(`${baseURL}${prefix}/declared`);
+ expect(response.status).toBe(200);
+
+ const segmentSpans = await segmentPromise;
+ const segment = segmentSpans.find(
+ segment =>
+ segment.is_segment && getSpanOp(segment) === 'http.server' && !!segment.name?.includes(`${prefix}/declared`),
+ )!;
+ expect(segment.name).toBe(`GET ${prefix}/declared`);
+
+ const spans = segmentSpans.filter(
+ span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment.span_id,
+ );
+
+ // Named function expression: the span name comes from the function's own name. A bundler (Bun)
+ // may append a numeric suffix when the inner name collides with the `const` binding, so match on
+ // the `namedMiddleware` prefix rather than an exact name.
+ const middlewareSpan = spans.find(
+ span => getSpanOp(span) === 'middleware' && !!span.name?.match(/^namedMiddleware\d*$/),
+ );
+
+ expect(middlewareSpan).toBeDefined();
+ expect(middlewareSpan?.attributes['sentry.origin']?.value).toBe('auto.middleware.hono');
+ expect(middlewareSpan?.status).not.toBe('error');
+ });
+
test('multiple middleware are sibling spans under the same parent', async ({ baseURL }) => {
const segmentPromise = collectStreamedSpansUntilSegment(
APP_NAME,
@@ -115,14 +147,17 @@ for (const { name, prefix } of SCENARIOS) {
test('captures error thrown in middleware', async ({ baseURL }) => {
const errorPromise = waitForError(APP_NAME, event => {
- return event.exception?.values?.[0]?.value === 'Middleware error';
+ return (
+ !!event.exception?.values?.[0]?.value?.startsWith('Middleware error') &&
+ !!event.request?.url?.includes(prefix)
+ );
});
const response = await fetch(`${baseURL}${prefix}/error`);
expect(response.status).toBe(500);
const errorEvent = await errorPromise;
- expect(errorEvent.exception?.values?.[0]?.value).toBe('Middleware error');
+ expect(errorEvent.exception?.values?.[0]?.value).toMatch(/^Middleware error/);
expect(errorEvent.exception?.values?.[0]?.mechanism).toEqual(
expect.objectContaining({
handled: false,
@@ -184,7 +219,10 @@ for (const { name, prefix } of SCENARIOS) {
test('includes request data on error events from middleware', async ({ baseURL }) => {
const errorPromise = waitForError(APP_NAME, event => {
- return event.exception?.values?.[0]?.value === 'Middleware error' && !!event.request?.url?.includes(prefix);
+ return (
+ !!event.exception?.values?.[0]?.value?.startsWith('Middleware error') &&
+ !!event.request?.url?.includes(prefix)
+ );
});
await fetch(`${baseURL}${prefix}/error`);
diff --git a/dev-packages/e2e-tests/test-applications/hono-4/tests/multi-fetch.test.ts b/dev-packages/e2e-tests/test-applications/hono-4/tests/multi-fetch.test.ts
index 29298c7b8f89..b98a7e6df7ee 100644
--- a/dev-packages/e2e-tests/test-applications/hono-4/tests/multi-fetch.test.ts
+++ b/dev-packages/e2e-tests/test-applications/hono-4/tests/multi-fetch.test.ts
@@ -183,6 +183,24 @@ test.describe('multi-fetch: internal .request() calls between sub-apps', () => {
});
});
+ test.describe('error inside internal fetch (degraded response)', () => {
+ test('reports the inner-route error to Sentry even though the outer handler returns 200', async ({ baseURL }) => {
+ const errorPromise = waitForError(APP_NAME, event => {
+ return !!event.exception?.values?.[0]?.value?.startsWith('inventory db is down');
+ });
+
+ const response = await fetch(`${baseURL}${STOREFRONT}/product/self-watering-plant/degraded`);
+ expect(response.status).toBe(200);
+ await expect(response.json()).resolves.toEqual({ product: null, degraded: true });
+
+ const errorEvent = await errorPromise;
+ expect(errorEvent.exception?.values?.[0]?.value).toMatch(/^inventory db is down/);
+ expect(errorEvent.exception?.values?.[0]?.mechanism).toEqual(
+ expect.objectContaining({ handled: false, type: 'auto.http.hono.context_error' }),
+ );
+ });
+ });
+
test.describe('inventory sub-app direct access', () => {
test('creates its own span when accessed directly via HTTP', async ({ baseURL }) => {
const segmentPromise = waitForStreamedSpan(
@@ -347,8 +365,11 @@ test.describe('multi-fetch: internal .request() calls between sub-apps', () => {
});
test('error from failed internal fetch is correlated with the storefront trace', async ({ baseURL }) => {
+ // Use a param unique to this test: the `no error status` test above fetches `/ghost` too, and its
+ // identical `Failed to fetch product: ghost` error would otherwise be dropped by the Dedupe
+ // integration, so this test's `waitForError` would never fire.
const errorPromise = waitForError(APP_NAME, event => {
- return event.exception?.values?.[0]?.value === 'Failed to fetch product: ghost';
+ return event.exception?.values?.[0]?.value === 'Failed to fetch product: phantom';
});
const segmentPromise = waitForStreamedSpan(
@@ -359,7 +380,7 @@ test.describe('multi-fetch: internal .request() calls between sub-apps', () => {
segment.name === `GET ${STOREFRONT}/product-or-throw/:productId`,
);
- await fetch(`${baseURL}${STOREFRONT}/product-or-throw/ghost`);
+ await fetch(`${baseURL}${STOREFRONT}/product-or-throw/phantom`);
const [errorEvent, segment] = await Promise.all([errorPromise, segmentPromise]);
diff --git a/dev-packages/e2e-tests/test-applications/hono-4/tests/tracing.test.ts b/dev-packages/e2e-tests/test-applications/hono-4/tests/tracing.test.ts
index 23ed5c7837fc..9350259d7e5c 100644
--- a/dev-packages/e2e-tests/test-applications/hono-4/tests/tracing.test.ts
+++ b/dev-packages/e2e-tests/test-applications/hono-4/tests/tracing.test.ts
@@ -1,6 +1,78 @@
import { expect, test } from '@playwright/test';
import { waitForStreamedSpan, getSpanOp } from '@sentry-internal/test-utils';
-import { APP_NAME, RUNTIME } from './constants';
+import { APP_NAME, RUNTIME, type Runtime } from './constants';
+
+const anyString = expect.any(String) as unknown;
+const anyNumber = expect.any(Number) as unknown;
+const ipvType = expect.stringMatching(/^ipv[46]$/) as unknown;
+
+// Per-runtime `http.server` connection-info expectations. Each runtime's `getConnInfo` helper exposes
+// a different set of fields, so the expected attribute values are declared here once (keyed by
+// runtime) instead of branching inside the tests. `undefined` means the attribute must be absent.
+// Relational checks (`network.peer.*` mirroring `client.*`) are asserted in the tests, since they
+// hold on every runtime (including when both sides are absent).
+//
+// - `added`: attributes Hono's conninfo middleware contributes.
+// - `baseline`: attributes the SDK sends independent of conninfo (regression guard that conninfo only
+// adds, never clobbers).
+const CONN_INFO: Record; baseline: Record }> = {
+ node: {
+ added: {
+ 'client.address': anyString,
+ 'client.port': anyNumber,
+ 'network.type': ipvType,
+ 'network.transport': undefined,
+ },
+ baseline: {
+ 'server.address': 'localhost',
+ 'server.port': anyNumber,
+ 'client.port': anyNumber,
+ 'network.type': ipvType,
+ 'network.protocol.name': 'http',
+ 'network.protocol.version': '1.1',
+ 'network.transport': undefined,
+ 'network.local.address': anyString,
+ 'network.local.port': anyNumber,
+ },
+ },
+ bun: {
+ added: {
+ 'client.address': anyString,
+ 'client.port': anyNumber,
+ 'network.type': ipvType,
+ 'network.transport': undefined,
+ },
+ baseline: { 'client.port': anyNumber, 'network.type': ipvType },
+ },
+ deno: {
+ // Only `hono/deno` exposes `network.transport`.
+ added: { 'client.address': anyString, 'client.port': anyNumber, 'network.transport': expect.stringMatching(/tcp/) },
+ baseline: {
+ 'server.address': 'localhost',
+ 'client.port': anyNumber,
+ 'network.transport': 'tcp',
+ 'network.protocol.name': 'http',
+ },
+ },
+ cloudflare: {
+ // Cloudflare Workers expose no client address, port, address family, or transport: there is no
+ // `getConnInfo` on Workers and nothing else populates `client.address`, so all of these are
+ // absent. Asserting `undefined` here lets us notice if that ever changes.
+ added: {
+ 'client.address': undefined,
+ 'client.port': undefined,
+ 'network.type': undefined,
+ 'network.transport': undefined,
+ },
+ baseline: {
+ 'server.address': 'localhost',
+ 'network.protocol.name': 'http',
+ 'network.protocol.version': '1.1',
+ },
+ },
+};
+
+const connInfo = CONN_INFO[RUNTIME];
test('sends a span for the index route', async ({ baseURL }) => {
const segmentPromise = waitForStreamedSpan(
@@ -45,33 +117,14 @@ test('attaches HTTP connection info to the server span', async ({ baseURL, page
const segment = await segmentPromise;
const data = segment.attributes ?? {};
- expect(data['client.address']?.value).toEqual(expect.any(String));
- expect(data['network.peer.address']?.value).toBe(data['client.address']?.value);
-
- if (RUNTIME !== 'deno') {
- // Only exposed in `hono/deno`
- expect(data['network.transport']?.value).toBeUndefined();
- } else {
- expect(data['network.transport']?.value).toMatch(/tcp/);
+ for (const [key, expected] of Object.entries(connInfo.added)) {
+ expect(data[key]?.value).toEqual(expected);
}
- if (RUNTIME === 'node' || RUNTIME === 'bun') {
- // Node (@hono/node-server) and Bun expose socket-level port and address family.
- expect(data['client.port']?.value).toEqual(expect.any(Number));
- expect(data['network.peer.port']?.value).toBe(data['client.port']?.value);
- expect(data['network.type']?.value).toMatch(/^ipv[46]$/);
- } else if (RUNTIME === 'deno') {
- expect(data['client.port']?.value).toEqual(expect.any(Number));
- expect(data['network.peer.port']?.value).toBe(data['client.port']?.value);
- } else if (RUNTIME === 'cloudflare') {
- // Cloudflare Workers expose no port, address family, or transport.
- // This could change in the future and checking for the absence of these fields allows us to notice if/when that happens.
- expect(data['client.port']?.value).toBeUndefined();
- expect(data['network.peer.port']?.value).toBeUndefined();
- expect(data['network.type']?.value).toBeUndefined();
- } else {
- throw new Error(`No tests for runtime: ${RUNTIME}`);
- }
+ // conninfo must only *add* attributes, never replace: peer mirrors client on every runtime
+ // (including when both are absent).
+ expect(data['network.peer.address']?.value).toBe(data['client.address']?.value);
+ expect(data['network.peer.port']?.value).toBe(data['client.port']?.value);
});
// Regression guard against connection info attributes.
@@ -91,42 +144,13 @@ test("preserves the baseline server.*, client.* and network.* server span attrib
const segment = await segmentPromise;
const data = segment.attributes ?? {};
- if (RUNTIME === 'node') {
- expect(data['server.address']?.value).toBe('localhost');
- expect(data['server.port']?.value).toBe(Number(new URL(baseURL!).port));
- expect(data['client.address']?.value).toEqual(expect.any(String));
- expect(data['client.port']?.value).toEqual(expect.any(Number));
- expect(data['network.type']?.value).toMatch(/^ipv[46]$/);
- expect(data['network.protocol.name']?.value).toBe('http');
- expect(data['network.protocol.version']?.value).toBe('1.1');
- expect(data['network.transport']?.value).toBeUndefined();
- expect(data['network.local.port']?.value).toBe(data['server.port']?.value);
- expect(data['network.local.address']?.value).toEqual(expect.any(String));
- expect(data['network.peer.address']?.value).toBe(data['client.address']?.value);
- expect(data['network.peer.port']?.value).toBe(data['client.port']?.value);
- } else if (RUNTIME === 'bun') {
- expect(data['client.address']?.value).toEqual(expect.any(String));
- expect(data['client.port']?.value).toEqual(expect.any(Number));
- expect(data['network.peer.address']?.value).toBe(data['client.address']?.value);
- expect(data['network.peer.port']?.value).toBe(data['client.port']?.value);
- expect(data['network.type']?.value).toMatch(/^ipv[46]$/);
- } else if (RUNTIME === 'cloudflare') {
- expect(data['server.address']?.value).toBe('localhost');
- expect(data['client.address']?.value).toBe('::1');
- expect(data['network.peer.address']?.value).toBe(data['client.address']?.value);
- expect(data['network.protocol.name']?.value).toBe('http');
- expect(data['network.protocol.version']?.value).toBe('1.1');
- } else if (RUNTIME === 'deno') {
- expect(data['server.address']?.value).toBe('localhost');
- expect(data['client.address']?.value).toEqual(expect.any(String));
- expect(data['client.port']?.value).toEqual(expect.any(Number));
- expect(data['network.peer.address']?.value).toBe(data['client.address']?.value);
- expect(data['network.peer.port']?.value).toBe(data['client.port']?.value);
- expect(data['network.transport']?.value).toBe('tcp');
- expect(data['network.protocol.name']?.value).toBe('http');
- } else {
- throw new Error(`No tests for runtime: ${RUNTIME}`);
+ for (const [key, expected] of Object.entries(connInfo.baseline)) {
+ expect(data[key]?.value).toEqual(expected);
}
+
+ // Relational checks that hold on every runtime (both sides absent → still equal).
+ expect(data['network.peer.address']?.value).toBe(data['client.address']?.value);
+ expect(data['network.peer.port']?.value).toBe(data['client.port']?.value);
});
test('sends a span for a route that throws', async ({ baseURL }) => {
diff --git a/dev-packages/e2e-tests/test-applications/hono-4/vite.config.ts b/dev-packages/e2e-tests/test-applications/hono-4/vite.config.ts
new file mode 100644
index 000000000000..005f4448f6cb
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/hono-4/vite.config.ts
@@ -0,0 +1,7 @@
+import { cloudflare } from '@cloudflare/vite-plugin';
+import { sentryCloudflareVitePlugin } from '@sentry/cloudflare/vite';
+import { defineConfig } from 'vite';
+
+export default defineConfig({
+ plugins: [cloudflare(), sentryCloudflareVitePlugin()],
+});
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-11-static/src/instrument.ts b/dev-packages/e2e-tests/test-applications/nestjs-11-static/src/instrument.ts
index aa4c76f13ee5..0713640263be 100644
--- a/dev-packages/e2e-tests/test-applications/nestjs-11-static/src/instrument.ts
+++ b/dev-packages/e2e-tests/test-applications/nestjs-11-static/src/instrument.ts
@@ -10,4 +10,6 @@ Sentry.init({
// We expect the app to send a lot of events in a short time
bufferSize: 1000,
},
+ // The cron job throws the same error on every run, which Dedupe would drop after the first run.
+ integrations: integrations => integrations.filter(integration => integration.name !== 'Dedupe'),
});
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-11/src/instrument.ts b/dev-packages/e2e-tests/test-applications/nestjs-11/src/instrument.ts
index 4f16ebb36d11..e1f373de1730 100644
--- a/dev-packages/e2e-tests/test-applications/nestjs-11/src/instrument.ts
+++ b/dev-packages/e2e-tests/test-applications/nestjs-11/src/instrument.ts
@@ -9,4 +9,6 @@ Sentry.init({
// We expect the app to send a lot of events in a short time
bufferSize: 1000,
},
+ // The cron job throws the same error on every run, which Dedupe would drop after the first run.
+ integrations: integrations => integrations.filter(integration => integration.name !== 'Dedupe'),
});
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-12/src/instrument.ts b/dev-packages/e2e-tests/test-applications/nestjs-12/src/instrument.ts
index 4f16ebb36d11..e1f373de1730 100644
--- a/dev-packages/e2e-tests/test-applications/nestjs-12/src/instrument.ts
+++ b/dev-packages/e2e-tests/test-applications/nestjs-12/src/instrument.ts
@@ -9,4 +9,6 @@ Sentry.init({
// We expect the app to send a lot of events in a short time
bufferSize: 1000,
},
+ // The cron job throws the same error on every run, which Dedupe would drop after the first run.
+ integrations: integrations => integrations.filter(integration => integration.name !== 'Dedupe'),
});
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/instrument.ts b/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/instrument.ts
index 4f16ebb36d11..e1f373de1730 100644
--- a/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/instrument.ts
+++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/instrument.ts
@@ -9,4 +9,6 @@ Sentry.init({
// We expect the app to send a lot of events in a short time
bufferSize: 1000,
},
+ // The cron job throws the same error on every run, which Dedupe would drop after the first run.
+ integrations: integrations => integrations.filter(integration => integration.name !== 'Dedupe'),
});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/cached-mid-layout/[id]/layout.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/cached-mid-layout/[id]/layout.tsx
index a61f15ee7e3f..ed7d8d1716d8 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/cached-mid-layout/[id]/layout.tsx
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/cached-mid-layout/[id]/layout.tsx
@@ -1,5 +1,6 @@
import type { ReactNode } from 'react';
import { cacheLife } from 'next/cache';
+import { CachedBox } from '@/components/scenarioBox';
// The awaited param puts the request id into the cache key, so a fresh id is a guaranteed miss
// and the entry cannot come from a build-time fill. `children` passes through as an uncached hole.
@@ -15,11 +16,14 @@ export default async function CachedMidLayout({
const { id } = await params;
await new Promise(resolve => setTimeout(resolve, 100));
return (
-
-
- {id}:{Date.now()}
-
- {children}
-
+
+ Cached layout, dynamic page
+
+
+ {id}:{Date.now()}
+
+ {children}
+
+
);
}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/cached-mid-layout/[id]/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/cached-mid-layout/[id]/page.tsx
index 94196e6e5ee8..1edb269289ba 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/cached-mid-layout/[id]/page.tsx
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/cached-mid-layout/[id]/page.tsx
@@ -1,6 +1,11 @@
import { headers } from 'next/headers';
+import { DynamicBox } from '@/components/scenarioBox';
export default async function Page() {
await headers();
- return Dynamic leaf: {Date.now()}
;
+ return (
+
+ Dynamic leaf: {Date.now()}
+
+ );
}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/dynamic-layouts/[id]/layout-cached-leaf/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/dynamic-layouts/[id]/layout-cached-leaf/page.tsx
index 8e7de36d156a..494dee2483e2 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/dynamic-layouts/[id]/layout-cached-leaf/page.tsx
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/dynamic-layouts/[id]/layout-cached-leaf/page.tsx
@@ -1,13 +1,16 @@
import { cacheLife } from 'next/cache';
+import { CachedBox } from '@/components/scenarioBox';
async function CachedLeaf({ id }: { id: string }) {
'use cache';
cacheLife('hours');
await new Promise(resolve => setTimeout(resolve, 100));
return (
-
- {id}:{Date.now()}
-
+
+
+ {id}:{Date.now()}
+
+
);
}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/dynamic-layouts/[id]/layout.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/dynamic-layouts/[id]/layout.tsx
index 412587fd8f33..4b9b4cffc9bf 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/dynamic-layouts/[id]/layout.tsx
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/dynamic-layouts/[id]/layout.tsx
@@ -1,7 +1,16 @@
import type { ReactNode } from 'react';
import { headers } from 'next/headers';
+import { DynamicBox } from '@/components/scenarioBox';
export default async function DynamicLayout({ children }: { children: ReactNode }) {
await headers();
- return {children};
+ return (
+
+ Dynamic layout, cached leaf
+
+ request-time: {Date.now()}
+ {children}
+
+
+ );
}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/mixed-lifetimes/[id]/layout.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/mixed-lifetimes/[id]/layout.tsx
index 2b0b77b6e3a9..7920ef335dc2 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/mixed-lifetimes/[id]/layout.tsx
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/mixed-lifetimes/[id]/layout.tsx
@@ -1,5 +1,6 @@
import type { ReactNode } from 'react';
import { cacheLife } from 'next/cache';
+import { CachedBox } from '@/components/scenarioBox';
// Hard-expires after 2s while the page's cached component lives for hours, so one request can
// refill the layout while hitting the component: two cached levels with different origin traces.
@@ -14,11 +15,14 @@ export default async function ShortLivedLayout({
cacheLife({ revalidate: 1, expire: 2 });
const { id } = await params;
return (
-
-
- {id}:{Date.now()}
-
- {children}
-
+
+ Mixed lifetimes
+
+
+ {id}:{Date.now()}
+
+ {children}
+
+
);
}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/mixed-lifetimes/[id]/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/mixed-lifetimes/[id]/page.tsx
index 18913cbe9790..c1c0e31494f6 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/mixed-lifetimes/[id]/page.tsx
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/mixed-lifetimes/[id]/page.tsx
@@ -1,19 +1,27 @@
import { headers } from 'next/headers';
import { cacheLife } from 'next/cache';
+import { CachedBox, DynamicBox } from '@/components/scenarioBox';
async function LongLivedComponent({ id }: { id: string }) {
'use cache';
cacheLife('hours');
await new Promise(resolve => setTimeout(resolve, 100));
return (
-
- {id}:{Date.now()}
-
+
+
+ {id}:{Date.now()}
+
+
);
}
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
await headers();
- return ;
+ return (
+
+ request-time: {Date.now()}
+
+
+ );
}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/shared-layout/[id]/a/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/shared-layout/[id]/a/page.tsx
index ec8d5e21d947..606b9f01a2b2 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/shared-layout/[id]/a/page.tsx
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/shared-layout/[id]/a/page.tsx
@@ -1,6 +1,11 @@
import { headers } from 'next/headers';
+import { DynamicBox } from '@/components/scenarioBox';
export default async function Page() {
await headers();
- return Route a: {Date.now()}
;
+ return (
+
+ Route a: {Date.now()}
+
+ );
}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/shared-layout/[id]/b/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/shared-layout/[id]/b/page.tsx
index 9a1b84b2ca28..18bff4a0ed35 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/shared-layout/[id]/b/page.tsx
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/shared-layout/[id]/b/page.tsx
@@ -1,6 +1,11 @@
import { headers } from 'next/headers';
+import { DynamicBox } from '@/components/scenarioBox';
export default async function Page() {
await headers();
- return Route b: {Date.now()}
;
+ return (
+
+ Route b: {Date.now()}
+
+ );
}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/shared-layout/[id]/layout.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/shared-layout/[id]/layout.tsx
index 220fdcf92ca8..16fa37dbd71e 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/shared-layout/[id]/layout.tsx
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/shared-layout/[id]/layout.tsx
@@ -1,5 +1,6 @@
import type { ReactNode } from 'react';
import { cacheLife } from 'next/cache';
+import { CachedBox } from '@/components/scenarioBox';
// One cache entry (keyed by id) shared by the sibling routes `a` and `b` below.
export default async function SharedLayout({
@@ -14,11 +15,14 @@ export default async function SharedLayout({
const { id } = await params;
await new Promise(resolve => setTimeout(resolve, 100));
return (
-
-
- {children}
-
+
+ Shared layout across sibling routes
+
+
+ {children}
+
+
);
}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/api/use-cache-swr/route.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/api/use-cache-swr/route.ts
new file mode 100644
index 000000000000..0332efe522b3
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/api/use-cache-swr/route.ts
@@ -0,0 +1,15 @@
+import { cacheLife } from 'next/cache';
+import type { NextRequest } from 'next/server';
+
+async function getSwrValue(id: string): Promise<{ id: string; createdAt: number }> {
+ 'use cache';
+ // After `revalidate` (2s), a read serves the stale value and triggers a background refill.
+ // `expire` is long so the entry stays valid for the whole test.
+ cacheLife({ stale: 5, revalidate: 2, expire: 300 });
+ return { id, createdAt: Date.now() };
+}
+
+export async function GET(request: NextRequest) {
+ const id = request.nextUrl.searchParams.get('id') ?? 'default-id';
+ return Response.json(await getSwrValue(id));
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/cached-sibling-components/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/cached-sibling-components/page.tsx
index c8b8858f7785..c3e4050539bb 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/cached-sibling-components/page.tsx
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/cached-sibling-components/page.tsx
@@ -1,14 +1,17 @@
import { Suspense } from 'react';
import { cacheLife } from 'next/cache';
+import { CachedBox, DynamicBox } from '@/components/scenarioBox';
async function CachedSection({ id, label }: { id: string; label: string }) {
'use cache';
cacheLife('hours');
await new Promise(resolve => setTimeout(resolve, 100));
return (
-
- {label}:{id}:{Date.now()}
-
+
+
+ {label}:{id}:{Date.now()}
+
+
);
}
@@ -18,17 +21,21 @@ async function DynamicContent({ searchParams }: { searchParams: Promise<{ id?: s
// separate entries (the props are part of the cache key).
const { id = 'default-id' } = await searchParams;
return (
- <>
+
+ request-time: {Date.now()}
- >
+
);
}
export default function Page({ searchParams }: { searchParams: Promise<{ id?: string }> }) {
return (
- Loading...}>
-
-
+
+ Cached sibling components
+ Loading...}>
+
+
+
);
}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/layout.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/layout.tsx
index c8f9cee0b787..5ffd72655346 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/layout.tsx
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/layout.tsx
@@ -1,7 +1,12 @@
export default function Layout({ children }: { children: React.ReactNode }) {
return (
- {children}
+
+
+ ← Cache scenarios
+
+ {children}
+
);
}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/nested-caches/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/nested-caches/page.tsx
index 481d7e5e46e6..1115c4054b3f 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/nested-caches/page.tsx
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/nested-caches/page.tsx
@@ -1,5 +1,6 @@
import { Suspense } from 'react';
import { cacheLife } from 'next/cache';
+import { CachedBox, DynamicBox } from '@/components/scenarioBox';
async function getSlowChangingValue(id: string): Promise<{ id: string; createdAt: number }> {
'use cache';
@@ -14,20 +15,34 @@ async function ShortLivedSection({ id }: { id: string }) {
// delayed request refills the component and reads the nested entry as a hit inside that fill.
cacheLife({ revalidate: 1, expire: 2 });
const nested = await getSlowChangingValue(id);
- return {JSON.stringify({ ...nested, renderedAt: Date.now() })};
+ return (
+
+
+ {JSON.stringify({ ...nested, renderedAt: Date.now() })}
+
+
+ );
}
async function DynamicContent({ searchParams }: { searchParams: Promise<{ id?: string }> }) {
// Awaiting searchParams makes this hole dynamic, so every request renders it and consults the
// cache handler instead of serving prerendered output.
const { id = 'default-id' } = await searchParams;
- return ;
+ return (
+
+ request-time: {Date.now()}
+
+
+ );
}
export default function Page({ searchParams }: { searchParams: Promise<{ id?: string }> }) {
return (
- Loading...}>
-
-
+
+ Nested caches with different lifetimes
+ Loading...}>
+
+
+
);
}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/page.tsx
index 433db8283beb..bea2cf1d875f 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/page.tsx
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/page.tsx
@@ -1,3 +1,120 @@
+'use client';
+
+import { useEffect, useState } from 'react';
+
+type Scenario = { name: string; href: (id: string) => string; description: string };
+
+const groups: { title: string; scenarios: Scenario[] }[] = [
+ {
+ title: 'Route handlers (JSON)',
+ scenarios: [
+ {
+ name: '/api/use-cache',
+ href: id => `/api/use-cache?id=${id}`,
+ description: 'cached function that lives for hours. The second request with the same id is a hit.',
+ },
+ {
+ name: '/api/use-cache-expiring',
+ href: id => `/api/use-cache-expiring?id=${id}`,
+ description: 'the entry expires after 2 seconds. Request again after 3 seconds for a refill.',
+ },
+ {
+ name: '/api/use-cache-swr',
+ href: id => `/api/use-cache-swr?id=${id}`,
+ description:
+ 'the entry goes stale after 2 seconds. A stale read serves the old value and refills in the background.',
+ },
+ ],
+ },
+ {
+ title: 'Rendered pages',
+ scenarios: [
+ {
+ name: '/use-cache-page',
+ href: id => `/use-cache-page?id=${id}`,
+ description: 'a cached data function inside a dynamic hole.',
+ },
+ {
+ name: '/cached-sibling-components',
+ href: id => `/cached-sibling-components?id=${id}`,
+ description: 'two cached sibling components. Each section is its own cache entry.',
+ },
+ {
+ name: '/nested-caches',
+ href: id => `/nested-caches?id=${id}`,
+ description:
+ 'a cached component that expires after 2 seconds and calls a cached function that lives for hours.',
+ },
+ ],
+ },
+ {
+ title: 'Nested layouts',
+ scenarios: [
+ {
+ name: '/cached-mid-layout/[id]',
+ href: id => `/cached-mid-layout/${id}`,
+ description: 'a cached layout above a dynamic page.',
+ },
+ {
+ name: '/dynamic-layouts/[id]/layout-cached-leaf',
+ href: id => `/dynamic-layouts/${id}/layout-cached-leaf`,
+ description: 'dynamic layouts above a cached leaf component.',
+ },
+ {
+ name: '/mixed-lifetimes/[id]',
+ href: id => `/mixed-lifetimes/${id}`,
+ description: 'a cached layout that expires after 2 seconds around a cached component that lives for hours.',
+ },
+ {
+ name: '/shared-layout/[id]/a',
+ href: id => `/shared-layout/${id}/a`,
+ description: 'route a fills the layout entry that routes a and b share.',
+ },
+ {
+ name: '/shared-layout/[id]/b',
+ href: id => `/shared-layout/${id}/b`,
+ description: 'route b hits the layout entry that route a filled.',
+ },
+ ],
+ },
+ {
+ title: 'Pageload',
+ scenarios: [
+ {
+ name: '/pageload-tracing',
+ href: () => '/pageload-tracing',
+ description: 'a prerendered shell with a dynamic hole. Check the pageload and server trace connection here.',
+ },
+ ],
+ },
+];
+
+// The links are plain `` elements so that every click is a full document request with its own
+// server trace. `` would navigate through the client-side router instead.
export default function Page() {
- return Next 16 CacheComponents test app
;
+ const [id, setId] = useState('');
+ // Generated after hydration so the prerendered shell does not bake in one fixed id.
+ useEffect(() => setId(crypto.randomUUID()), []);
+
+ return (
+
+ Next 16 Cache Components scenarios
+
+ Every link carries a random id that becomes part of the cache key. The first click fills the cache. A second
+ click on the same link, or a reload of the target page, hits it. Reload this page to get fresh ids.
+
+ {groups.map(group => (
+
+ {group.title}
+
+ {group.scenarios.map(scenario => (
+ -
+ {scenario.name}: {scenario.description}
+
+ ))}
+
+
+ ))}
+
+ );
}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/use-cache-page/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/use-cache-page/page.tsx
index 1e41b4436e20..3450928fe090 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/use-cache-page/page.tsx
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/use-cache-page/page.tsx
@@ -1,5 +1,6 @@
import { Suspense } from 'react';
import { cacheLife } from 'next/cache';
+import { CachedBox, DynamicBox } from '@/components/scenarioBox';
async function getCachedPageData(id: string): Promise<{ id: string; createdAt: number }> {
'use cache';
@@ -10,9 +11,12 @@ async function getCachedPageData(id: string): Promise<{ id: string; createdAt: n
export default function Page({ searchParams }: { searchParams: Promise<{ id?: string }> }) {
return (
- Loading...}>
-
-
+
+ Cached data function in a page
+ Loading...}>
+
+
+
);
}
@@ -21,5 +25,12 @@ async function CachedContent({ searchParams }: { searchParams: Promise<{ id?: st
// cache handler instead of serving prerendered output.
const { id = 'default-id' } = await searchParams;
const data = await getCachedPageData(id);
- return {JSON.stringify(data)};
+ return (
+
+ request-time: {Date.now()}
+
+ {JSON.stringify(data)}
+
+
+ );
}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/components/scenarioBox.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/components/scenarioBox.tsx
new file mode 100644
index 000000000000..cf2d318d64cc
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/components/scenarioBox.tsx
@@ -0,0 +1,34 @@
+import type { CSSProperties, ReactNode } from 'react';
+
+const box: CSSProperties = {
+ padding: '8px 12px',
+ margin: '12px 0',
+ borderRadius: 8,
+};
+
+const badge: CSSProperties = {
+ display: 'inline-block',
+ fontSize: 12,
+ fontWeight: 600,
+ color: '#fff',
+ padding: '1px 8px',
+ borderRadius: 4,
+};
+
+export function CachedBox({ label, children }: { label: string; children: ReactNode }) {
+ return (
+
+ Cached · {label}
+ {children}
+
+ );
+}
+
+export function DynamicBox({ label, children }: { label: string; children: ReactNode }) {
+ return (
+
+ Dynamic · {label}
+ {children}
+
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/cacheOriginLinks-revalidation.spec.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/cacheOriginLinks-revalidation.spec.ts
new file mode 100644
index 000000000000..50f88a41bdbb
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/cacheOriginLinks-revalidation.spec.ts
@@ -0,0 +1,113 @@
+import { expect, test } from '@playwright/test';
+import { waitForTransaction } from '@sentry-internal/test-utils';
+
+// Background stale-while-revalidate refills. Target behavior: the revalidation gets its own trace,
+// separate from the request trace that served the stale value. The revalidation trace links back to
+// that request, and its `cache.put` becomes the `cache_origin` for future hits.
+
+/*
+
+Trace1 (fill) Trace2 (stale hit) Trace3 (hit)
+|- put key:A <----link-----o get hit=true key:A |- get hit=true key:A
+ ^ |
+ | link (type TBD in the spec) |
+TraceR: cache.revalidate --o (own trace) |
+| |
+|- put key:A <----------------------link-------------------o cache_origin
+
+*/
+
+// Not implemented yet. Unlike cacheOriginLinks-nesting.spec.ts, this test cannot document the
+// target behavior with `test.fail()`: it would wait for a `cache.revalidate` trace that never
+// arrives and time out, and Playwright reports a timeout as a real failure even under
+// `test.fail()`. So the test is `test.fixme()` until the SDK emits the revalidation trace.
+
+test('runs background revalidation in its own trace linked to the triggering request', async ({ request }) => {
+ test.skip(process.env.TEST_ENV !== 'production', 'SWR revalidation timing only holds in production');
+ test.fixme();
+
+ const id = crypto.randomUUID();
+
+ const fillTxPromise = waitForTransaction('nextjs-16-cacheComponents', transactionEvent => {
+ return (
+ transactionEvent.transaction === 'GET /api/use-cache-swr' &&
+ !!transactionEvent.spans?.some(span => span.op === 'cache.put')
+ );
+ });
+
+ await request.get(`/api/use-cache-swr?id=${id}`);
+ const fillTx = await fillTxPromise;
+
+ // Sleep past `revalidate` (2s) but not `expire`, so the next read serves the stale value and triggers a background refill.
+ await new Promise(resolve => setTimeout(resolve, 3_000));
+
+ const staleHitTxPromise = waitForTransaction('nextjs-16-cacheComponents', transactionEvent => {
+ return (
+ transactionEvent.transaction === 'GET /api/use-cache-swr' &&
+ !!transactionEvent.spans?.some(span => span.op === 'cache.get' && span.data?.['cache.hit'] === true)
+ );
+ });
+
+ const revalidationTxPromise = waitForTransaction('nextjs-16-cacheComponents', transactionEvent => {
+ return transactionEvent.contexts?.trace?.op === 'cache.revalidate';
+ });
+
+ await request.get(`/api/use-cache-swr?id=${id}`);
+ const staleHitTx = await staleHitTxPromise;
+
+ const fillPutSpan = fillTx.spans?.find(span => span.op === 'cache.put');
+ expect(fillPutSpan).toBeDefined();
+
+ // The stale response still came from the original fill.
+ const staleHitGetSpan = staleHitTx.spans?.find(span => span.op === 'cache.get' && span.data?.['cache.hit'] === true);
+ expect(staleHitGetSpan).toBeDefined();
+ expect(staleHitGetSpan?.links).toEqual([
+ {
+ trace_id: fillTx.contexts?.trace?.trace_id,
+ span_id: fillPutSpan!.span_id,
+ sampled: true,
+ attributes: { 'sentry.link.type': 'cache_origin' },
+ },
+ ]);
+
+ // The visitor never waited for the refill, so the refill is not part of the serving trace: the
+ // background revalidation is its own trace, linked back to the request that triggered it.
+ const revalidationTx = await revalidationTxPromise;
+ expect(revalidationTx.contexts?.trace?.trace_id).not.toBe(staleHitTx.contexts?.trace?.trace_id);
+ expect(revalidationTx.contexts?.trace?.links).toEqual([
+ {
+ trace_id: staleHitTx.contexts?.trace?.trace_id,
+ span_id: expect.stringMatching(/^[0-9a-f]{16}$/),
+ sampled: true,
+ // No link type for cache spans specced yet, so assert only the link target.
+ attributes: { 'sentry.link.type': expect.any(String) },
+ },
+ ]);
+
+ const revalidationPutSpan = revalidationTx.spans?.find(span => span.op === 'cache.put');
+ expect(revalidationPutSpan).toBeDefined();
+
+ // The revalidation's `cache.put` becomes the `cache_origin` for future hits.
+ const hitAfterRefillTxPromise = waitForTransaction('nextjs-16-cacheComponents', transactionEvent => {
+ return (
+ transactionEvent.transaction === 'GET /api/use-cache-swr' &&
+ !!transactionEvent.spans?.some(span => span.op === 'cache.get' && span.data?.['cache.hit'] === true)
+ );
+ });
+
+ await request.get(`/api/use-cache-swr?id=${id}`);
+ const hitAfterRefillTx = await hitAfterRefillTxPromise;
+
+ const hitAfterRefillGetSpan = hitAfterRefillTx.spans?.find(
+ span => span.op === 'cache.get' && span.data?.['cache.hit'] === true,
+ );
+ expect(hitAfterRefillGetSpan).toBeDefined();
+ expect(hitAfterRefillGetSpan?.links).toEqual([
+ {
+ trace_id: revalidationTx.contexts?.trace?.trace_id,
+ span_id: revalidationPutSpan!.span_id,
+ sampled: true,
+ attributes: { 'sentry.link.type': 'cache_origin' },
+ },
+ ]);
+});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/cached-mid-layout/[id]/layout.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/cached-mid-layout/[id]/layout.tsx
index a61f15ee7e3f..ed7d8d1716d8 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/cached-mid-layout/[id]/layout.tsx
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/cached-mid-layout/[id]/layout.tsx
@@ -1,5 +1,6 @@
import type { ReactNode } from 'react';
import { cacheLife } from 'next/cache';
+import { CachedBox } from '@/components/scenarioBox';
// The awaited param puts the request id into the cache key, so a fresh id is a guaranteed miss
// and the entry cannot come from a build-time fill. `children` passes through as an uncached hole.
@@ -15,11 +16,14 @@ export default async function CachedMidLayout({
const { id } = await params;
await new Promise(resolve => setTimeout(resolve, 100));
return (
-
-
- {id}:{Date.now()}
-
- {children}
-
+
+ Cached layout, dynamic page
+
+
+ {id}:{Date.now()}
+
+ {children}
+
+
);
}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/cached-mid-layout/[id]/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/cached-mid-layout/[id]/page.tsx
index 94196e6e5ee8..1edb269289ba 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/cached-mid-layout/[id]/page.tsx
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/cached-mid-layout/[id]/page.tsx
@@ -1,6 +1,11 @@
import { headers } from 'next/headers';
+import { DynamicBox } from '@/components/scenarioBox';
export default async function Page() {
await headers();
- return Dynamic leaf: {Date.now()}
;
+ return (
+
+ Dynamic leaf: {Date.now()}
+
+ );
}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/dynamic-layouts/[id]/layout-cached-leaf/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/dynamic-layouts/[id]/layout-cached-leaf/page.tsx
index 8e7de36d156a..494dee2483e2 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/dynamic-layouts/[id]/layout-cached-leaf/page.tsx
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/dynamic-layouts/[id]/layout-cached-leaf/page.tsx
@@ -1,13 +1,16 @@
import { cacheLife } from 'next/cache';
+import { CachedBox } from '@/components/scenarioBox';
async function CachedLeaf({ id }: { id: string }) {
'use cache';
cacheLife('hours');
await new Promise(resolve => setTimeout(resolve, 100));
return (
-
- {id}:{Date.now()}
-
+
+
+ {id}:{Date.now()}
+
+
);
}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/dynamic-layouts/[id]/layout.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/dynamic-layouts/[id]/layout.tsx
index 412587fd8f33..4b9b4cffc9bf 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/dynamic-layouts/[id]/layout.tsx
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/dynamic-layouts/[id]/layout.tsx
@@ -1,7 +1,16 @@
import type { ReactNode } from 'react';
import { headers } from 'next/headers';
+import { DynamicBox } from '@/components/scenarioBox';
export default async function DynamicLayout({ children }: { children: ReactNode }) {
await headers();
- return {children};
+ return (
+
+ Dynamic layout, cached leaf
+
+ request-time: {Date.now()}
+ {children}
+
+
+ );
}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/mixed-lifetimes/[id]/layout.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/mixed-lifetimes/[id]/layout.tsx
index 2b0b77b6e3a9..7920ef335dc2 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/mixed-lifetimes/[id]/layout.tsx
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/mixed-lifetimes/[id]/layout.tsx
@@ -1,5 +1,6 @@
import type { ReactNode } from 'react';
import { cacheLife } from 'next/cache';
+import { CachedBox } from '@/components/scenarioBox';
// Hard-expires after 2s while the page's cached component lives for hours, so one request can
// refill the layout while hitting the component: two cached levels with different origin traces.
@@ -14,11 +15,14 @@ export default async function ShortLivedLayout({
cacheLife({ revalidate: 1, expire: 2 });
const { id } = await params;
return (
-
-
- {id}:{Date.now()}
-
- {children}
-
+
+ Mixed lifetimes
+
+
+ {id}:{Date.now()}
+
+ {children}
+
+
);
}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/mixed-lifetimes/[id]/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/mixed-lifetimes/[id]/page.tsx
index 18913cbe9790..c1c0e31494f6 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/mixed-lifetimes/[id]/page.tsx
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/mixed-lifetimes/[id]/page.tsx
@@ -1,19 +1,27 @@
import { headers } from 'next/headers';
import { cacheLife } from 'next/cache';
+import { CachedBox, DynamicBox } from '@/components/scenarioBox';
async function LongLivedComponent({ id }: { id: string }) {
'use cache';
cacheLife('hours');
await new Promise(resolve => setTimeout(resolve, 100));
return (
-
- {id}:{Date.now()}
-
+
+
+ {id}:{Date.now()}
+
+
);
}
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
await headers();
- return ;
+ return (
+
+ request-time: {Date.now()}
+
+
+ );
}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/shared-layout/[id]/a/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/shared-layout/[id]/a/page.tsx
index ec8d5e21d947..606b9f01a2b2 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/shared-layout/[id]/a/page.tsx
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/shared-layout/[id]/a/page.tsx
@@ -1,6 +1,11 @@
import { headers } from 'next/headers';
+import { DynamicBox } from '@/components/scenarioBox';
export default async function Page() {
await headers();
- return Route a: {Date.now()}
;
+ return (
+
+ Route a: {Date.now()}
+
+ );
}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/shared-layout/[id]/b/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/shared-layout/[id]/b/page.tsx
index 9a1b84b2ca28..18bff4a0ed35 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/shared-layout/[id]/b/page.tsx
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/shared-layout/[id]/b/page.tsx
@@ -1,6 +1,11 @@
import { headers } from 'next/headers';
+import { DynamicBox } from '@/components/scenarioBox';
export default async function Page() {
await headers();
- return Route b: {Date.now()}
;
+ return (
+
+ Route b: {Date.now()}
+
+ );
}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/shared-layout/[id]/layout.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/shared-layout/[id]/layout.tsx
index 220fdcf92ca8..16fa37dbd71e 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/shared-layout/[id]/layout.tsx
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/shared-layout/[id]/layout.tsx
@@ -1,5 +1,6 @@
import type { ReactNode } from 'react';
import { cacheLife } from 'next/cache';
+import { CachedBox } from '@/components/scenarioBox';
// One cache entry (keyed by id) shared by the sibling routes `a` and `b` below.
export default async function SharedLayout({
@@ -14,11 +15,14 @@ export default async function SharedLayout({
const { id } = await params;
await new Promise(resolve => setTimeout(resolve, 100));
return (
-
-
- {children}
-
+
+ Shared layout across sibling routes
+
+
+ {children}
+
+
);
}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/api/use-cache-swr/route.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/api/use-cache-swr/route.ts
new file mode 100644
index 000000000000..0332efe522b3
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/api/use-cache-swr/route.ts
@@ -0,0 +1,15 @@
+import { cacheLife } from 'next/cache';
+import type { NextRequest } from 'next/server';
+
+async function getSwrValue(id: string): Promise<{ id: string; createdAt: number }> {
+ 'use cache';
+ // After `revalidate` (2s), a read serves the stale value and triggers a background refill.
+ // `expire` is long so the entry stays valid for the whole test.
+ cacheLife({ stale: 5, revalidate: 2, expire: 300 });
+ return { id, createdAt: Date.now() };
+}
+
+export async function GET(request: NextRequest) {
+ const id = request.nextUrl.searchParams.get('id') ?? 'default-id';
+ return Response.json(await getSwrValue(id));
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/cached-sibling-components/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/cached-sibling-components/page.tsx
index c8b8858f7785..c3e4050539bb 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/cached-sibling-components/page.tsx
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/cached-sibling-components/page.tsx
@@ -1,14 +1,17 @@
import { Suspense } from 'react';
import { cacheLife } from 'next/cache';
+import { CachedBox, DynamicBox } from '@/components/scenarioBox';
async function CachedSection({ id, label }: { id: string; label: string }) {
'use cache';
cacheLife('hours');
await new Promise(resolve => setTimeout(resolve, 100));
return (
-
- {label}:{id}:{Date.now()}
-
+
+
+ {label}:{id}:{Date.now()}
+
+
);
}
@@ -18,17 +21,21 @@ async function DynamicContent({ searchParams }: { searchParams: Promise<{ id?: s
// separate entries (the props are part of the cache key).
const { id = 'default-id' } = await searchParams;
return (
- <>
+
+ request-time: {Date.now()}
- >
+
);
}
export default function Page({ searchParams }: { searchParams: Promise<{ id?: string }> }) {
return (
- Loading...}>
-
-
+
+ Cached sibling components
+ Loading...}>
+
+
+
);
}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/layout.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/layout.tsx
index c8f9cee0b787..5ffd72655346 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/layout.tsx
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/layout.tsx
@@ -1,7 +1,12 @@
export default function Layout({ children }: { children: React.ReactNode }) {
return (
- {children}
+
+
+ ← Cache scenarios
+
+ {children}
+
);
}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/nested-caches/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/nested-caches/page.tsx
index 481d7e5e46e6..1115c4054b3f 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/nested-caches/page.tsx
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/nested-caches/page.tsx
@@ -1,5 +1,6 @@
import { Suspense } from 'react';
import { cacheLife } from 'next/cache';
+import { CachedBox, DynamicBox } from '@/components/scenarioBox';
async function getSlowChangingValue(id: string): Promise<{ id: string; createdAt: number }> {
'use cache';
@@ -14,20 +15,34 @@ async function ShortLivedSection({ id }: { id: string }) {
// delayed request refills the component and reads the nested entry as a hit inside that fill.
cacheLife({ revalidate: 1, expire: 2 });
const nested = await getSlowChangingValue(id);
- return {JSON.stringify({ ...nested, renderedAt: Date.now() })};
+ return (
+
+
+ {JSON.stringify({ ...nested, renderedAt: Date.now() })}
+
+
+ );
}
async function DynamicContent({ searchParams }: { searchParams: Promise<{ id?: string }> }) {
// Awaiting searchParams makes this hole dynamic, so every request renders it and consults the
// cache handler instead of serving prerendered output.
const { id = 'default-id' } = await searchParams;
- return ;
+ return (
+
+ request-time: {Date.now()}
+
+
+ );
}
export default function Page({ searchParams }: { searchParams: Promise<{ id?: string }> }) {
return (
- Loading...}>
-
-
+
+ Nested caches with different lifetimes
+ Loading...}>
+
+
+
);
}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/page.tsx
index 433db8283beb..8f7d5cf7558e 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/page.tsx
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/page.tsx
@@ -1,3 +1,115 @@
+'use client';
+
+import { useEffect, useState } from 'react';
+
+type Scenario = { name: string; href: (id: string) => string; description: string };
+
+const groups: { title: string; scenarios: Scenario[] }[] = [
+ {
+ title: 'Route handlers (JSON)',
+ scenarios: [
+ {
+ name: '/api/use-cache',
+ href: id => `/api/use-cache?id=${id}`,
+ description: 'cached function that lives for hours. The second request with the same id is a hit.',
+ },
+ {
+ name: '/api/use-cache-expiring',
+ href: id => `/api/use-cache-expiring?id=${id}`,
+ description: 'the entry expires after 2 seconds. Request again after 3 seconds for a refill.',
+ },
+ {
+ name: '/api/use-cache-swr',
+ href: id => `/api/use-cache-swr?id=${id}`,
+ description:
+ 'the entry goes stale after 2 seconds. A stale read serves the old value and refills in the background.',
+ },
+ ],
+ },
+ {
+ title: 'Rendered pages',
+ scenarios: [
+ {
+ name: '/cached-sibling-components',
+ href: id => `/cached-sibling-components?id=${id}`,
+ description: 'two cached sibling components. Each section is its own cache entry.',
+ },
+ {
+ name: '/nested-caches',
+ href: id => `/nested-caches?id=${id}`,
+ description:
+ 'a cached component that expires after 2 seconds and calls a cached function that lives for hours.',
+ },
+ ],
+ },
+ {
+ title: 'Nested layouts',
+ scenarios: [
+ {
+ name: '/cached-mid-layout/[id]',
+ href: id => `/cached-mid-layout/${id}`,
+ description: 'a cached layout above a dynamic page.',
+ },
+ {
+ name: '/dynamic-layouts/[id]/layout-cached-leaf',
+ href: id => `/dynamic-layouts/${id}/layout-cached-leaf`,
+ description: 'dynamic layouts above a cached leaf component.',
+ },
+ {
+ name: '/mixed-lifetimes/[id]',
+ href: id => `/mixed-lifetimes/${id}`,
+ description: 'a cached layout that expires after 2 seconds around a cached component that lives for hours.',
+ },
+ {
+ name: '/shared-layout/[id]/a',
+ href: id => `/shared-layout/${id}/a`,
+ description: 'route a fills the layout entry that routes a and b share.',
+ },
+ {
+ name: '/shared-layout/[id]/b',
+ href: id => `/shared-layout/${id}/b`,
+ description: 'route b hits the layout entry that route a filled.',
+ },
+ ],
+ },
+ {
+ title: 'Pageload',
+ scenarios: [
+ {
+ name: '/pageload-tracing',
+ href: () => '/pageload-tracing',
+ description: 'a prerendered shell with a dynamic hole. Check the pageload and server trace connection here.',
+ },
+ ],
+ },
+];
+
+// The links are plain `` elements so that every click is a full document request with its own
+// server trace. `` would navigate through the client-side router instead.
export default function Page() {
- return Next 16 CacheComponents test app
;
+ const [id, setId] = useState('');
+ // Generated after hydration so the prerendered shell does not bake in one fixed id.
+ useEffect(() => setId(crypto.randomUUID()), []);
+
+ return (
+
+ Next 16 Cache Components scenarios (span streaming)
+
+ Every link carries a random id that becomes part of the cache key. The first click fills the cache. A second
+ click on the same link, or a reload of the target page, hits it. Reload this page to get fresh ids.
+
+ {groups.map(group => (
+
+ {group.title}
+
+ {group.scenarios.map(scenario => (
+ -
+ {scenario.name}: {scenario.description}
+
+ ))}
+
+
+ ))}
+
+ );
}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/components/scenarioBox.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/components/scenarioBox.tsx
new file mode 100644
index 000000000000..cf2d318d64cc
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/components/scenarioBox.tsx
@@ -0,0 +1,34 @@
+import type { CSSProperties, ReactNode } from 'react';
+
+const box: CSSProperties = {
+ padding: '8px 12px',
+ margin: '12px 0',
+ borderRadius: 8,
+};
+
+const badge: CSSProperties = {
+ display: 'inline-block',
+ fontSize: 12,
+ fontWeight: 600,
+ color: '#fff',
+ padding: '1px 8px',
+ borderRadius: 4,
+};
+
+export function CachedBox({ label, children }: { label: string; children: ReactNode }) {
+ return (
+
+ Cached · {label}
+ {children}
+
+ );
+}
+
+export function DynamicBox({ label, children }: { label: string; children: ReactNode }) {
+ return (
+
+ Dynamic · {label}
+ {children}
+
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheOriginLinks-revalidation.spec.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheOriginLinks-revalidation.spec.ts
new file mode 100644
index 000000000000..b6aa21aa89c2
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheOriginLinks-revalidation.spec.ts
@@ -0,0 +1,119 @@
+import { expect, test } from '@playwright/test';
+import { collectStreamedSpans, getSpanOp } from '@sentry-internal/test-utils';
+import { CACHE_ORIGIN_LINK_ATTRIBUTES, findCacheSpan } from './cacheOriginLinks-utils';
+
+// Background stale-while-revalidate refills. Target behavior: the revalidation gets its own trace,
+// separate from the request trace that served the stale value. The revalidation trace links back to
+// that request, and its `cache.put` becomes the `cache_origin` for future hits.
+
+/*
+
+Trace1 (fill) Trace2 (stale hit) Trace3 (hit)
+|- put key:A <----link-----o get hit=true key:A |- get hit=true key:A
+ ^ |
+ | link (type TBD in the spec) |
+TraceR: cache.revalidate --o (own trace) |
+| |
+|- put key:A <----------------------link-------------------o cache_origin
+
+*/
+
+// Not implemented yet. Unlike cacheOriginLinks-nesting.spec.ts, this test cannot document the
+// target behavior with `test.fail()`: it would wait for a `cache.revalidate` trace that never
+// arrives and time out, and Playwright reports a timeout as a real failure even under
+// `test.fail()`. So the test is `test.fixme()` until the SDK emits the revalidation trace.
+
+test('runs background revalidation in its own trace linked to the triggering request', async ({ request }) => {
+ test.skip(process.env.TEST_ENV !== 'production', 'SWR revalidation timing only holds in production');
+ test.fixme();
+
+ const id = crypto.randomUUID();
+
+ const fillSpansPromise = collectStreamedSpans('nextjs-16-streaming-cacheComponents', spansOfTrace => {
+ return (
+ spansOfTrace.some(span => span.name === 'GET /api/use-cache-swr' && span.is_segment) &&
+ spansOfTrace.some(span => getSpanOp(span) === 'cache.put')
+ );
+ });
+
+ await request.get(`/api/use-cache-swr?id=${id}`);
+ const fillSpans = await fillSpansPromise;
+
+ // Sleep past `revalidate` (2s) but not `expire`, so the next read serves the stale value and
+ // triggers a background refill.
+ await new Promise(resolve => setTimeout(resolve, 3_000));
+
+ const staleHitSpansPromise = collectStreamedSpans('nextjs-16-streaming-cacheComponents', spansOfTrace => {
+ return (
+ spansOfTrace.some(span => span.name === 'GET /api/use-cache-swr' && span.is_segment) &&
+ spansOfTrace.some(span => getSpanOp(span) === 'cache.get' && span.attributes['cache.hit']?.value === true)
+ );
+ });
+
+ // The visitor never waited for the refill, so the refill is not part of the serving trace: the
+ // background revalidation is its own trace with a `cache.revalidate` segment.
+ const revalidationSpansPromise = collectStreamedSpans('nextjs-16-streaming-cacheComponents', spansOfTrace => {
+ return (
+ spansOfTrace.some(span => getSpanOp(span) === 'cache.revalidate' && span.is_segment) &&
+ spansOfTrace.some(span => getSpanOp(span) === 'cache.put')
+ );
+ });
+
+ await request.get(`/api/use-cache-swr?id=${id}`);
+ const staleHitSpans = await staleHitSpansPromise;
+
+ const fillPutSpan = findCacheSpan(fillSpans, 'cache.put');
+ expect(fillPutSpan).toBeDefined();
+
+ // The stale response still came from the original fill.
+ const staleHitGetSpan = findCacheSpan(staleHitSpans, 'cache.get', true);
+ expect(staleHitGetSpan).toBeDefined();
+ expect(staleHitGetSpan?.links).toEqual([
+ {
+ trace_id: fillPutSpan!.trace_id,
+ span_id: fillPutSpan!.span_id,
+ sampled: true,
+ attributes: CACHE_ORIGIN_LINK_ATTRIBUTES,
+ },
+ ]);
+
+ const revalidationSpans = await revalidationSpansPromise;
+ const revalidationSegment = revalidationSpans.find(span => span.is_segment && getSpanOp(span) === 'cache.revalidate');
+ expect(revalidationSegment).toBeDefined();
+ expect(revalidationSegment!.trace_id).not.toBe(staleHitGetSpan!.trace_id);
+ expect(revalidationSegment!.links).toEqual([
+ {
+ trace_id: staleHitGetSpan!.trace_id,
+ span_id: expect.stringMatching(/^[0-9a-f]{16}$/),
+ sampled: true,
+ // No link type for cache spans specced yet, so assert only the link target.
+ attributes: { 'sentry.link.type': { value: expect.any(String), type: 'string' } },
+ },
+ ]);
+
+ const revalidationPutSpan = findCacheSpan(revalidationSpans, 'cache.put');
+ expect(revalidationPutSpan).toBeDefined();
+
+ // The revalidation's `cache.put` becomes the `cache_origin` for future hits.
+ const hitAfterRefillSpansPromise = collectStreamedSpans('nextjs-16-streaming-cacheComponents', spansOfTrace => {
+ return (
+ spansOfTrace.some(span => span.name === 'GET /api/use-cache-swr' && span.is_segment) &&
+ spansOfTrace.some(span => getSpanOp(span) === 'cache.get' && span.attributes['cache.hit']?.value === true) &&
+ spansOfTrace.every(span => span.trace_id !== staleHitGetSpan!.trace_id)
+ );
+ });
+
+ await request.get(`/api/use-cache-swr?id=${id}`);
+ const hitAfterRefillSpans = await hitAfterRefillSpansPromise;
+
+ const hitAfterRefillGetSpan = findCacheSpan(hitAfterRefillSpans, 'cache.get', true);
+ expect(hitAfterRefillGetSpan).toBeDefined();
+ expect(hitAfterRefillGetSpan?.links).toEqual([
+ {
+ trace_id: revalidationPutSpan!.trace_id,
+ span_id: revalidationPutSpan!.span_id,
+ sampled: true,
+ attributes: CACHE_ORIGIN_LINK_ATTRIBUTES,
+ },
+ ]);
+});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/useCacheSpans.spec.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/useCacheSpans.spec.ts
new file mode 100644
index 000000000000..2704de1a11f6
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/useCacheSpans.spec.ts
@@ -0,0 +1,33 @@
+import { expect, test } from '@playwright/test';
+import { collectStreamedSpans, getSpanOp } from '@sentry-internal/test-utils';
+import { findCacheSpan } from './cacheOriginLinks-utils';
+
+test('uses low-cardinality names for `use cache` spans', async ({ request }) => {
+ // A fresh id makes the request a guaranteed cache miss (the id is part of the cache key), so the
+ // trace contains both a `cache.get` and a `cache.put` span.
+ const id = crypto.randomUUID();
+
+ const spansPromise = collectStreamedSpans('nextjs-16-streaming-cacheComponents', spansOfTrace => {
+ return (
+ spansOfTrace.some(span => span.name === 'GET /api/use-cache' && span.is_segment) &&
+ spansOfTrace.some(span => getSpanOp(span) === 'cache.get') &&
+ spansOfTrace.some(span => getSpanOp(span) === 'cache.put')
+ );
+ });
+
+ await request.get(`/api/use-cache?id=${id}`);
+ const spans = await spansPromise;
+
+ // With span streaming the span name is the low-cardinality op; the key digest stays on
+ // `cache.key`, where Relay reads the span description from.
+ const getSpan = findCacheSpan(spans, 'cache.get');
+ expect(getSpan).toBeDefined();
+ expect(getSpan!.name).toBe('cache.get');
+ const cacheKeyDigest = getSpan!.attributes['cache.key']?.value;
+ expect(cacheKeyDigest).toEqual([expect.stringMatching(/^[0-9a-f]{12}$/)]);
+
+ const putSpan = findCacheSpan(spans, 'cache.put');
+ expect(putSpan).toBeDefined();
+ expect(putSpan!.name).toBe('cache.put');
+ expect(putSpan!.attributes['cache.key']?.value).toEqual(cacheKeyDigest);
+});
diff --git a/dev-packages/e2e-tests/test-applications/node-mastra/src/mastra/index.ts b/dev-packages/e2e-tests/test-applications/node-mastra/src/mastra/index.ts
index 71ad1709ad55..b6f2dd975b39 100644
--- a/dev-packages/e2e-tests/test-applications/node-mastra/src/mastra/index.ts
+++ b/dev-packages/e2e-tests/test-applications/node-mastra/src/mastra/index.ts
@@ -26,6 +26,11 @@ export const mastra = new Mastra({
port: 4111,
apiRoutes: [manualRoute],
},
+ // No `bundler.externals` override: `mastra build` defaults to externalizing all non-workspace deps,
+ // so Hono (and the other instrumented modules) stay unbundled and the `--import` orchestrion hook
+ // can transform them in prod. In `mastra dev` the bundler inlines its own Hono-based server
+ // framework into the entry regardless of any `externals` config, so Hono is not
+ // orchestrion-instrumented in dev — the tests assert the un-routed span name there.
});
/**
diff --git a/dev-packages/e2e-tests/test-applications/node-mastra/tests/manual-route.test.ts b/dev-packages/e2e-tests/test-applications/node-mastra/tests/manual-route.test.ts
index e3aa759000cc..f287cc794217 100644
--- a/dev-packages/e2e-tests/test-applications/node-mastra/tests/manual-route.test.ts
+++ b/dev-packages/e2e-tests/test-applications/node-mastra/tests/manual-route.test.ts
@@ -3,6 +3,12 @@ import { collectStreamedSpans, getSpanOp, SerializedStreamedSpan } from '@sentry
const APP = 'node-mastra';
+// In `mastra dev`, Mastra inlines its Hono-based server into the dev bundle, so the `--import`
+// orchestrion hook cannot transform Hono and the request span is not route-enriched: its name is the
+// bare method and it carries no `http.route`/route name source. In prod (`mastra build`/`start`) Hono
+// is external and fully instrumented, so the span is named after the matched route.
+const IS_DEV = process.env.TEST_ENV === 'development';
+
const attrValue = (span: SerializedStreamedSpan, key: string): unknown => span.attributes?.[key]?.value;
// A plain custom route registered on the Mastra (Hono) server — see the
@@ -21,19 +27,17 @@ test('wraps a custom Mastra route in an http.server span with correct attributes
await res.json();
const spans = await spansPromise;
- const serverSpan = spans.find(isManualRouteServerSpan);
+ const serverSpan = spans.find(isManualRouteServerSpan)!;
expect(serverSpan).toBeDefined();
- expect(getSpanOp(serverSpan!)).toBe('http.server');
- expect(serverSpan!.name).toBe('GET');
- expect(attrValue(serverSpan!, 'http.request.method')).toBe('GET');
- expect(attrValue(serverSpan!, 'http.response.status_code')).toBe(200);
- expect(String(attrValue(serverSpan!, 'url.full') ?? '')).toContain('/manual-route');
-
- // Codifies current behavior: the transaction name is derived from the URL path,
- // not a route pattern. Mastra serves custom routes through Hono, which Sentry
- // does not route-instrument the way it does Express — so there is no `http.route`
- // attribute and the name source is `url` (an Express route would give `route`).
- expect(attrValue(serverSpan!, 'sentry.segment.name.source')).toBe('url');
- expect(attrValue(serverSpan!, 'http.route')).toBeUndefined();
+ expect(getSpanOp(serverSpan)).toBe('http.server');
+ expect(attrValue(serverSpan, 'http.request.method')).toBe('GET');
+ expect(attrValue(serverSpan, 'http.response.status_code')).toBe(200);
+ expect(String(attrValue(serverSpan, 'url.full') ?? '')).toContain('/manual-route');
+ expect(serverSpan.name).toBe(IS_DEV ? 'GET' : 'GET /manual-route');
+
+ if (!IS_DEV) {
+ expect(attrValue(serverSpan, 'sentry.segment.name.source')).toBe('route');
+ expect(attrValue(serverSpan, 'http.route')).toBe('/manual-route');
+ }
});
diff --git a/dev-packages/e2e-tests/test-applications/node-mastra/tests/mastra.test.ts b/dev-packages/e2e-tests/test-applications/node-mastra/tests/mastra.test.ts
index ef68fdf82b9a..7927a8f9531a 100644
--- a/dev-packages/e2e-tests/test-applications/node-mastra/tests/mastra.test.ts
+++ b/dev-packages/e2e-tests/test-applications/node-mastra/tests/mastra.test.ts
@@ -4,6 +4,12 @@ import { runAgentTurn } from './utils';
const APP = 'node-mastra';
+// In `mastra dev`, Mastra inlines its Hono-based server into the dev bundle, so the `--import`
+// orchestrion hook cannot transform Hono and the root request span is not route-enriched: its name is
+// the bare method and it carries no `http.route`/route name source. In prod (`mastra build`/`start`)
+// Hono is external and fully instrumented, so the span is named after the matched route.
+const IS_DEV = process.env.TEST_ENV === 'development';
+
const attrValue = (span: SerializedStreamedSpan, key: string): unknown => span.attributes?.[key]?.value;
const MASTRA_ORIGIN = 'auto.ai.mastra';
@@ -34,11 +40,16 @@ test('captures Mastra agent spans (invoke_agent, chat, execute_tool) with inputs
// across envelopes until the agent/model spans have also arrived. Tool spans are
// matched by op alone (not origin), so a double-instrumented span is collected
// too and caught by the assertions below.
+ const isGenerateServerSpan = (span: SerializedStreamedSpan): boolean =>
+ isOp('http.server')(span) && String(attrValue(span, 'url.full') ?? '').includes('/api/agents/');
+
const traceSpansPromise = collectStreamedSpans(
APP,
spansOfTrace =>
spansOfTrace.some(callsTool('get_weather')) &&
- ['gen_ai.invoke_agent', 'gen_ai.chat'].every(op => spansOfTrace.some(isOp(op))),
+ ['gen_ai.invoke_agent', 'gen_ai.chat'].every(op => spansOfTrace.some(isOp(op))) &&
+ // The root `http.server` span ends (and streams) after its children, so wait for it too.
+ spansOfTrace.some(isGenerateServerSpan),
);
await runAgentTurn(baseURL!, 'What is the weather in Paris?', { thread, resource: 'e2e-user' });
@@ -96,6 +107,24 @@ test('captures Mastra agent spans (invoke_agent, chat, execute_tool) with inputs
expect(attrValue(invokeAgent!, 'gen_ai.conversation.id')).toBe(thread);
expect(attrValue(chat!, 'gen_ai.conversation.id')).toBe(thread);
expect(attrValue(executeTool!, 'gen_ai.conversation.id')).toBe(thread);
+
+ // http.server span: Mastra serves the agent through its internal Hono server, so the incoming
+ // `POST /api/agents/weatherAgent/generate` request is the root `http.server` span of this trace,
+ // and the AI spans above are its children.
+ const serverSpan = traceSpans.find(isGenerateServerSpan);
+ expect(serverSpan).toBeDefined();
+ expect(getSpanOp(serverSpan!)).toBe('http.server');
+ expect(attrValue(serverSpan!, 'http.request.method')).toBe('POST');
+ expect(attrValue(serverSpan!, 'http.response.status_code')).toBe(200);
+ expect(String(attrValue(serverSpan!, 'url.full') ?? '')).toContain('/api/agents/weatherAgent/generate');
+
+ if (IS_DEV) {
+ expect(serverSpan!.name).toBe('POST');
+ } else {
+ expect(attrValue(serverSpan!, 'sentry.segment.name.source')).toBe('route');
+ expect(attrValue(serverSpan!, 'http.route')).toMatch(/^\/api\/agents\/:[^/]+\/generate$/);
+ expect(serverSpan!.name).toMatch(/^POST \/api\/agents\/:[^/]+\/generate$/);
+ }
});
test('captures a Mastra tool error as an issue and marks the tool span', async ({ baseURL }) => {
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/sentry.server.config.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/sentry.server.config.ts
index f08dea23ae03..688c63d7f00c 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/sentry.server.config.ts
+++ b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/sentry.server.config.ts
@@ -6,4 +6,6 @@ Sentry.init({
tracesSampleRate: 1.0, // Capture 100% of the transactions
tunnel: 'http://localhost:3031/', // proxy server
debug: !!process.env.DEBUG,
+ // Several tests trigger the same server error one after another, which Dedupe would drop.
+ integrations: integrations => integrations.filter(integration => integration.name !== 'Dedupe'),
});
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4-static/sentry.server.config.ts b/dev-packages/e2e-tests/test-applications/nuxt-4-static/sentry.server.config.ts
index 626a375fe574..d954cf92b242 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-4-static/sentry.server.config.ts
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4-static/sentry.server.config.ts
@@ -8,4 +8,6 @@ Sentry.init({
// Opt into the Sentry OpenTelemetry tracer provider in the "(tracer provider)" e2e variant.
// Leaving it `undefined` otherwise keeps the SDK's default (no provider).
enableOpenTelemetrySetup: process.env.E2E_TEST_OTEL_SETUP === 'true' ? true : undefined,
+ // Several tests trigger the same server error one after another, which Dedupe would drop.
+ integrations: integrations => integrations.filter(integration => integration.name !== 'Dedupe'),
});
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/docker-compose.yml b/dev-packages/e2e-tests/test-applications/nuxt-4/docker-compose.yml
index a50a0097e5ae..c87cae49aab6 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-4/docker-compose.yml
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/docker-compose.yml
@@ -17,6 +17,22 @@ services:
retries: 30
start_period: 10s
+ mongo:
+ image: mongo:8
+ restart: always
+ container_name: e2e-tests-nuxt-4-mongo
+ ports:
+ - '27017:27017'
+ environment:
+ MONGO_INITDB_ROOT_USERNAME: root
+ MONGO_INITDB_ROOT_PASSWORD: docker
+ healthcheck:
+ test: ['CMD-SHELL', 'mongosh --quiet --eval "db.adminCommand(''ping'').ok"']
+ interval: 2s
+ timeout: 3s
+ retries: 30
+ start_period: 10s
+
redis:
image: redis:7
restart: always
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/global-setup.mjs b/dev-packages/e2e-tests/test-applications/nuxt-4/global-setup.mjs
index cb48d539c466..4d0386045cb1 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-4/global-setup.mjs
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/global-setup.mjs
@@ -5,7 +5,7 @@ import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
export default async function globalSetup() {
- // Start MySQL + Redis via Docker Compose. `--wait` blocks until the
+ // Start MySQL, MongoDB, and Redis via Docker Compose. `--wait` blocks until the
// healthchecks in docker-compose.yml pass, so the app can connect immediately.
execSync('docker compose up -d --wait', {
cwd: __dirname,
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/package.json b/dev-packages/e2e-tests/test-applications/nuxt-4/package.json
index 9eb23775a79b..ac1ea1939999 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-4/package.json
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/package.json
@@ -23,6 +23,7 @@
"@pinia/nuxt": "^0.5.5",
"@sentry/nuxt": "file:../../packed/sentry-nuxt-packed.tgz",
"ioredis": "5.10.1",
+ "mongoose": "^9.10.2",
"mysql": "^2.18.1",
"nuxt": "^4.1.2"
},
@@ -31,8 +32,7 @@
"@sentry-internal/test-utils": "link:../../../test-utils"
},
"volta": {
- "extends": "../../package.json",
- "node": "22.20.0"
+ "extends": "../../package.json"
},
"sentryTest": {
"variants": [
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/sentry.server.config.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/sentry.server.config.ts
index 50ea077473eb..acce04cc9d04 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-4/sentry.server.config.ts
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/sentry.server.config.ts
@@ -7,4 +7,6 @@ Sentry.init({
// Opt into the Sentry OpenTelemetry tracer provider in the "(tracer provider)" e2e variant.
// Leaving it `undefined` otherwise keeps the SDK's default (no provider).
enableOpenTelemetrySetup: process.env.E2E_TEST_OTEL_SETUP === 'true' ? true : undefined,
+ // Several tests trigger the same server error one after another, which Dedupe would drop.
+ integrations: integrations => integrations.filter(integration => integration.name !== 'Dedupe'),
});
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/db-mongoose.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/db-mongoose.ts
new file mode 100644
index 000000000000..6a203cceaa9e
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/db-mongoose.ts
@@ -0,0 +1,19 @@
+import { defineEventHandler } from '#imports';
+import mongoose from 'mongoose';
+
+const BlogPost = mongoose.model('BlogPost', new mongoose.Schema({ title: String }));
+
+// SCRAM-SHA-1 on purpose: this handshake lazily `require()`s the `crypto` builtin inside a try
+// block, which only works when the bundled driver gets a real interop for builtin requires (#24775).
+const MONGO_URL = 'mongodb://root:docker@127.0.0.1:27017/test?authSource=admin&authMechanism=SCRAM-SHA-1';
+
+export default defineEventHandler(async () => {
+ if (mongoose.connection.readyState !== mongoose.ConnectionStates.connected) {
+ await mongoose.connect(MONGO_URL);
+ }
+
+ await new BlogPost({ title: 'test-post' }).save();
+ const found = await BlogPost.findOne({ title: 'test-post' });
+
+ return { title: found?.title };
+});
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/db-drivers.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/db-drivers.test.ts
index a093dd9e8d67..5b1bccedab4d 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/db-drivers.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/db-drivers.test.ts
@@ -67,6 +67,46 @@ test('Instruments ioredis automatically', async ({ baseURL }) => {
);
});
+// The Nitro bundle force-inlines the instrumented drivers while their CommonJS dependencies and
+// Node builtins stay external, and every `require()` across that boundary needs working interop.
+test('Instruments mongoose automatically', async ({ baseURL }) => {
+ const spansPromise = collectRequestSpans('/api/db-mongoose');
+
+ const response = await fetch(`${baseURL}/api/db-mongoose`);
+ expect(response.status).toBe(200);
+ await expect(response.json()).resolves.toEqual({ title: 'test-post' });
+
+ const spans = await spansPromise;
+
+ const rootSpan = spans.find(span => span.is_segment);
+ expect(rootSpan).toBeDefined();
+ expect(getSpanOp(rootSpan!)).toBe('http.server');
+
+ const mongooseSpans = spans.filter(
+ span => span.attributes['sentry.origin']?.value === 'auto.db.mongoose.diagnostic_channel',
+ );
+ expect(mongooseSpans).toHaveLength(2);
+
+ const saveSpan = mongooseSpans.find(span => span.name === 'save blogposts');
+ expect(saveSpan?.status).toBe('ok');
+ expect(saveSpan?.is_segment).toBe(false);
+ expect(saveSpan?.attributes['sentry.op']).toEqual({ type: 'string', value: 'db' });
+ expect(saveSpan?.attributes['db.system.name']).toEqual({ type: 'string', value: 'mongodb' });
+ expect(saveSpan?.attributes['db.namespace']).toEqual({ type: 'string', value: 'test' });
+ expect(saveSpan?.attributes['db.collection.name']).toEqual({ type: 'string', value: 'blogposts' });
+ expect(saveSpan?.attributes['db.operation.name']).toEqual({ type: 'string', value: 'save' });
+
+ const findOneSpan = mongooseSpans.find(span => span.name === 'findOne blogposts');
+ expect(findOneSpan?.status).toBe('ok');
+ expect(findOneSpan?.is_segment).toBe(false);
+ expect(findOneSpan?.attributes['sentry.op']).toEqual({ type: 'string', value: 'db' });
+ expect(findOneSpan?.attributes['db.system.name']).toEqual({ type: 'string', value: 'mongodb' });
+ expect(findOneSpan?.attributes['db.namespace']).toEqual({ type: 'string', value: 'test' });
+ expect(findOneSpan?.attributes['db.collection.name']).toEqual({ type: 'string', value: 'blogposts' });
+ expect(findOneSpan?.attributes['db.operation.name']).toEqual({ type: 'string', value: 'findOne' });
+ expect(findOneSpan?.attributes['db.query.text']).toEqual({ type: 'string', value: '{"title":"?"}' });
+});
+
test('Instruments mysql automatically', async ({ baseURL }) => {
const spansPromise = collectRequestSpans('/api/db-mysql');
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-5/docker-compose.yml b/dev-packages/e2e-tests/test-applications/nuxt-5/docker-compose.yml
new file mode 100644
index 000000000000..a392ebd433af
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nuxt-5/docker-compose.yml
@@ -0,0 +1,16 @@
+services:
+ mongo:
+ image: mongo:8
+ restart: always
+ container_name: e2e-tests-nuxt-5-mongo
+ ports:
+ - '27017:27017'
+ environment:
+ MONGO_INITDB_ROOT_USERNAME: root
+ MONGO_INITDB_ROOT_PASSWORD: docker
+ healthcheck:
+ test: ['CMD-SHELL', 'mongosh --quiet --eval "db.adminCommand(''ping'').ok"']
+ interval: 2s
+ timeout: 3s
+ retries: 30
+ start_period: 10s
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-5/global-setup.mjs b/dev-packages/e2e-tests/test-applications/nuxt-5/global-setup.mjs
new file mode 100644
index 000000000000..fbc228dc4e55
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nuxt-5/global-setup.mjs
@@ -0,0 +1,14 @@
+import { execSync } from 'child_process';
+import { dirname } from 'path';
+import { fileURLToPath } from 'url';
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+
+export default async function globalSetup() {
+ // Start MongoDB via Docker Compose. `--wait` blocks until the
+ // healthcheck in docker-compose.yml passes, so the app can connect immediately.
+ execSync('docker compose up -d --wait', {
+ cwd: __dirname,
+ stdio: 'inherit',
+ });
+}
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-5/global-teardown.mjs b/dev-packages/e2e-tests/test-applications/nuxt-5/global-teardown.mjs
new file mode 100644
index 000000000000..2742279431ad
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nuxt-5/global-teardown.mjs
@@ -0,0 +1,12 @@
+import { execSync } from 'child_process';
+import { dirname } from 'path';
+import { fileURLToPath } from 'url';
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+
+export default async function globalTeardown() {
+ execSync('docker compose down --volumes', {
+ cwd: __dirname,
+ stdio: 'inherit',
+ });
+}
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-5/package.json b/dev-packages/e2e-tests/test-applications/nuxt-5/package.json
index 36aea2e0b856..6a38a24d8f3c 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-5/package.json
+++ b/dev-packages/e2e-tests/test-applications/nuxt-5/package.json
@@ -19,9 +19,6 @@
},
"//": [
"Currently, we need to install the latest version of Nitro and the Nuxt nightlies as those contain Nuxt v5",
- "@nuxt/cli is pinned. The CLI nightlies register nitro runtime plugins that live inside the CLI package: https://github.com/nuxt/cli/pull/1570",
- "Nuxt's impound guard blocks imports from that package in the dev server runtime, so every SSR request fails in dev mode.",
- "TODO: remove the @nuxt/cli override when impound allows the CLI runtime plugins",
"TODO: remove nitro from dependencies",
"Nuxt generates `fetch.server.mjs` as a virtual module that imports `ofetch`, and since a virtual id is neither absolute nor on disk, Vite resolves from its `root` (the srcDir) instead of the importer's folder.",
"This fails because `ofetch` is a dependency of `nuxt` rather than the app, so pnpm leaves it in `.pnpm/`",
@@ -30,6 +27,7 @@
"dependencies": {
"@pinia/nuxt": "^0.11.3",
"@sentry/nuxt": "file:../../packed/sentry-nuxt-packed.tgz",
+ "mongoose": "^9.10.2",
"ofetch": "^2.0.0-alpha.3",
"nitro": "latest",
"nuxt": "npm:nuxt-nightly@5x",
@@ -39,17 +37,11 @@
"@playwright/test": "~1.63.0",
"@sentry-internal/test-utils": "link:../../../test-utils"
},
- "pnpm": {
- "overrides": {
- "@nuxt/cli": "npm:@nuxt/cli-nightly@4.0.0-20260927-110353-6eaa0eb"
- }
- },
"sentryTest": {
"optional": true
},
"volta": {
"extends": "../../package.json",
- "node": "22.20.0",
"pnpm": "11.10.0"
}
}
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-5/playwright.config.ts b/dev-packages/e2e-tests/test-applications/nuxt-5/playwright.config.ts
index 80df201381ce..308691754a0b 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-5/playwright.config.ts
+++ b/dev-packages/e2e-tests/test-applications/nuxt-5/playwright.config.ts
@@ -23,4 +23,8 @@ const config = getPlaywrightConfig({
startCommand: getStartCommand(),
});
-export default config;
+export default {
+ ...config,
+ globalSetup: './global-setup.mjs',
+ globalTeardown: './global-teardown.mjs',
+};
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-5/sentry.server.config.ts b/dev-packages/e2e-tests/test-applications/nuxt-5/sentry.server.config.ts
index 26519911072b..58d541b5228b 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-5/sentry.server.config.ts
+++ b/dev-packages/e2e-tests/test-applications/nuxt-5/sentry.server.config.ts
@@ -4,4 +4,6 @@ Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
tracesSampleRate: 1.0, // Capture 100% of the transactions
tunnel: 'http://localhost:3031/', // proxy server
+ // Several tests trigger the same server error one after another, which Dedupe would drop.
+ integrations: integrations => integrations.filter(integration => integration.name !== 'Dedupe'),
});
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-5/server/api/db-mongoose.ts b/dev-packages/e2e-tests/test-applications/nuxt-5/server/api/db-mongoose.ts
new file mode 100644
index 000000000000..5bed443ac90a
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nuxt-5/server/api/db-mongoose.ts
@@ -0,0 +1,19 @@
+import mongoose from 'mongoose';
+import { defineHandler } from 'nitro';
+
+const BlogPost = mongoose.model('BlogPost', new mongoose.Schema({ title: String }));
+
+// SCRAM-SHA-1 on purpose: this handshake lazily `require()`s the `crypto` builtin inside a try
+// block, which only works when the bundled driver gets a real interop for builtin requires (#24775).
+const MONGO_URL = 'mongodb://root:docker@127.0.0.1:27017/test?authSource=admin&authMechanism=SCRAM-SHA-1';
+
+export default defineHandler(async () => {
+ if (mongoose.connection.readyState !== mongoose.ConnectionStates.connected) {
+ await mongoose.connect(MONGO_URL);
+ }
+
+ await new BlogPost({ title: 'test-post' }).save();
+ const found = await BlogPost.findOne({ title: 'test-post' });
+
+ return { title: found?.title };
+});
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-5/tests/db-mongoose.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-5/tests/db-mongoose.test.ts
new file mode 100644
index 000000000000..e48c97ed1af6
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nuxt-5/tests/db-mongoose.test.ts
@@ -0,0 +1,49 @@
+import { expect, test } from '@playwright/test';
+import { collectStreamedSpansUntilSegment } from '@sentry-internal/test-utils';
+
+// The Nitro bundle force-inlines the instrumented drivers while their CommonJS dependencies and
+// Node builtins stay external, and every `require()` across that boundary needs working interop
+// (#24775). mongoose covers both: `new mquery()`, and SCRAM-SHA-1 auth lazily requiring `crypto`.
+async function collectRequestSpans() {
+ const spans = await collectStreamedSpansUntilSegment(
+ 'nuxt-5',
+ span => span.attributes['url.path']?.value === '/api/db-mongoose',
+ );
+ const rootSpan = spans.find(span => span.is_segment && span.attributes['url.path']?.value === '/api/db-mongoose');
+
+ return spans.filter(span => span.trace_id === rootSpan?.trace_id);
+}
+
+test('Instruments mongoose automatically', async ({ baseURL }) => {
+ const spansPromise = collectRequestSpans();
+
+ const response = await fetch(`${baseURL}/api/db-mongoose`);
+ expect(response.status).toBe(200);
+ await expect(response.json()).resolves.toEqual({ title: 'test-post' });
+
+ const spans = await spansPromise;
+
+ const mongooseSpans = spans.filter(
+ span => span.attributes['sentry.origin']?.value === 'auto.db.mongoose.diagnostic_channel',
+ );
+ expect(mongooseSpans).toHaveLength(2);
+
+ const saveSpan = mongooseSpans.find(span => span.name === 'save blogposts');
+ expect(saveSpan?.status).toBe('ok');
+ expect(saveSpan?.is_segment).toBe(false);
+ expect(saveSpan?.attributes['sentry.op']).toEqual({ type: 'string', value: 'db' });
+ expect(saveSpan?.attributes['db.system.name']).toEqual({ type: 'string', value: 'mongodb' });
+ expect(saveSpan?.attributes['db.namespace']).toEqual({ type: 'string', value: 'test' });
+ expect(saveSpan?.attributes['db.collection.name']).toEqual({ type: 'string', value: 'blogposts' });
+ expect(saveSpan?.attributes['db.operation.name']).toEqual({ type: 'string', value: 'save' });
+
+ const findOneSpan = mongooseSpans.find(span => span.name === 'findOne blogposts');
+ expect(findOneSpan?.status).toBe('ok');
+ expect(findOneSpan?.is_segment).toBe(false);
+ expect(findOneSpan?.attributes['sentry.op']).toEqual({ type: 'string', value: 'db' });
+ expect(findOneSpan?.attributes['db.system.name']).toEqual({ type: 'string', value: 'mongodb' });
+ expect(findOneSpan?.attributes['db.namespace']).toEqual({ type: 'string', value: 'test' });
+ expect(findOneSpan?.attributes['db.collection.name']).toEqual({ type: 'string', value: 'blogposts' });
+ expect(findOneSpan?.attributes['db.operation.name']).toEqual({ type: 'string', value: 'findOne' });
+ expect(findOneSpan?.attributes['db.query.text']).toEqual({ type: 'string', value: '{"title":"?"}' });
+});
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/instrument.mjs b/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/instrument.mjs
index c16240141b6d..c4bdeea732a4 100644
--- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/instrument.mjs
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/instrument.mjs
@@ -5,4 +5,6 @@ Sentry.init({
environment: 'qa', // dynamic sampling bias to keep transactions
tracesSampleRate: 1.0,
tunnel: `http://localhost:3031/`, // proxy server
+ // Several tests trigger the same server error one after another, which Dedupe would drop.
+ integrations: integrations => integrations.filter(integration => integration.name !== 'Dedupe'),
});
diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/entry.server.tsx b/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/entry.server.tsx
index 738cd1515a4d..01682288ab71 100644
--- a/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/entry.server.tsx
+++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/entry.server.tsx
@@ -1,5 +1,6 @@
import { createReadableStreamFromReadable } from '@react-router/node';
import * as Sentry from '@sentry/react-router';
+import { randomBytes } from 'node:crypto';
import { renderToPipeableStream } from 'react-dom/server';
import { ServerRouter } from 'react-router';
import { type HandleErrorFunction } from 'react-router';
@@ -11,6 +12,7 @@ const handleRequest = Sentry.createSentryHandleRequest({
ServerRouter,
renderToPipeableStream,
createReadableStreamFromReadable,
+ getNonce: () => randomBytes(16).toString('base64'),
});
export default handleRequest;
diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/csp-nonce.server.test.ts b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/csp-nonce.server.test.ts
new file mode 100644
index 000000000000..1787bde746cb
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/csp-nonce.server.test.ts
@@ -0,0 +1,23 @@
+import { expect, test } from '@playwright/test';
+
+function getScriptNonces(html: string): string[] {
+ return Array.from(html.matchAll(/` : '';
@@ -88,6 +88,19 @@ export function addSentryCodeToPage(options: {
};
}
+/**
+ * Prerendered pages would bake one trace id and sampling decision into the HTML for every visitor.
+ *
+ * `building` from `$app/environment` isn't available here (the SDK is loaded from node_modules at
+ * runtime), so we rely on `_SENTRY_SVELTEKIT_BUILDING`, which our Vite plugin sets during `vite build`
+ * and SvelteKit copies into the worker it prerenders in.
+ */
+function isSvelteKitBuilding(): boolean {
+ // `process` doesn't exist in every runtime (e.g. Cloudflare Workers without `nodejs_compat`)
+ const env = typeof process !== 'undefined' && typeof process.env === 'object' ? process.env : undefined;
+ return !!env?._SENTRY_SVELTEKIT_BUILDING;
+}
+
/**
* We only need to inject the fetch proxy script for SvelteKit versions < 2.16.0.
* Exported only for testing.
diff --git a/packages/sveltekit/src/server/index.ts b/packages/sveltekit/src/server/index.ts
index 0dc4dcc20c26..cff338b62ed0 100644
--- a/packages/sveltekit/src/server/index.ts
+++ b/packages/sveltekit/src/server/index.ts
@@ -60,6 +60,8 @@ export {
isEnabled,
knexIntegration,
kafkaIntegration,
+ honoIntegration,
+ honoMiddleware,
koaIntegration,
lastEventId,
linkedErrorsIntegration,
diff --git a/packages/sveltekit/src/vite/sentryVitePlugins.ts b/packages/sveltekit/src/vite/sentryVitePlugins.ts
index a390e3abf2ba..9052560af04b 100644
--- a/packages/sveltekit/src/vite/sentryVitePlugins.ts
+++ b/packages/sveltekit/src/vite/sentryVitePlugins.ts
@@ -78,6 +78,7 @@ export async function sentrySvelteKit(options: SentrySvelteKitPluginOptions = {}
sentryOrchestrionPlugin({
buildTimeInstrumentation: mergedOptions.buildTimeInstrumentation,
}),
+ makeBuildFlagPlugin(),
);
const sentryVitePluginsOptions = generateVitePluginOptions(mergedOptions);
@@ -108,6 +109,65 @@ export async function sentrySvelteKit(options: SentrySvelteKitPluginOptions = {}
return sentryPlugins;
}
+/**
+ * Flags `vite build` so `sentryHandle` can skip trace meta tags on pages SvelteKit renders at build time
+ * (prerendered and adapter fallback pages). Kit renders them in workers that inherit `process.env`, so the
+ * flag must stay set until the adapter ran. Afterwards we remove it again, so it doesn't leak into e.g. a
+ * server started in the same process after a programmatic build.
+ *
+ * When that point is reached depends on the Kit version:
+ * - Kit 2 renders from the SSR build's `writeBundle` and `closeBundle` hooks. In between, it runs a nested
+ * client build, whose `closeBundle` fires before any page was rendered.
+ * - Kit 3 builds all environments via Vite's app builder and renders from `buildApp` hooks afterwards.
+ * Every `closeBundle` fires before that.
+ */
+function makeBuildFlagPlugin(): Plugin {
+ let usesAppBuilder = false;
+ let isServerBuild = false;
+ let isWatchMode = false;
+
+ const clearFlag = (): void => {
+ delete process.env._SENTRY_SVELTEKIT_BUILDING;
+ };
+
+ // `buildApp` only exists as a plugin hook in Vite 7+
+ const plugin: Plugin & { buildApp?: { order: 'post'; handler: () => void } } = {
+ name: 'sentry-sveltekit-build-flag',
+ apply: 'build',
+ // Together with `order: 'post'`, this makes our hooks run after Kit's (including its adapter's)
+ enforce: 'post',
+ config() {
+ process.env._SENTRY_SVELTEKIT_BUILDING = 'true';
+ },
+ configResolved(config) {
+ usesAppBuilder = !!config.builder;
+ isServerBuild = !!config.build.ssr;
+ // Kit re-renders pages on every rebuild in watch mode
+ isWatchMode = !!config.build.watch;
+ },
+ closeBundle: {
+ order: 'post',
+ sequential: true,
+ handler() {
+ if (!usesAppBuilder && isServerBuild && !isWatchMode) {
+ clearFlag();
+ }
+ },
+ },
+ buildApp: {
+ order: 'post',
+ handler: async () => {
+ // Without an app builder config, Vite 7+ calls `buildApp` hooks *before* the actual (legacy) build
+ if (usesAppBuilder && !isWatchMode) {
+ clearFlag();
+ }
+ },
+ },
+ };
+
+ return plugin;
+}
+
// A bare subpath (not a relative import) so this plugin's `resolveId` can intercept it.
const BROWSER_TRACING_VARIANT_ID = '@sentry/sveltekit/browser-tracing-variant';
diff --git a/packages/sveltekit/test/server-common/handle.test.ts b/packages/sveltekit/test/server-common/handle.test.ts
index 0298d4f5d788..8843e63ed1e6 100644
--- a/packages/sveltekit/test/server-common/handle.test.ts
+++ b/packages/sveltekit/test/server-common/handle.test.ts
@@ -5,7 +5,7 @@ import * as SentryCore from '@sentry/core';
import { NodeClient, setCurrentClient } from '@sentry/node';
import type { Handle } from '@sveltejs/kit';
import { redirect } from '@sveltejs/kit';
-import { beforeEach, describe, expect, it, vi } from 'vitest';
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import {
addSentryCodeToPage,
FETCH_PROXY_SCRIPT,
@@ -514,6 +514,23 @@ describe('addSentryCodeToPage', () => {
expect(transformed).toContain('${FETCH_PROXY_SCRIPT}`);
});
+
+ describe('while SvelteKit builds the app', () => {
+ afterEach(() => {
+ vi.unstubAllEnvs();
+ });
+
+ it("doesn't add meta tags but still adds the fetch proxy script if _SENTRY_SVELTEKIT_BUILDING is set", () => {
+ vi.stubEnv('_SENTRY_SVELTEKIT_BUILDING', 'true');
+
+ const transformPageChunk = addSentryCodeToPage({ injectFetchProxyScript: true });
+ const transformed = transformPageChunk({ html, done: true }) as string;
+
+ expect(transformed).not.toContain('${FETCH_PROXY_SCRIPT}`);
+ });
+ });
});
describe('isFetchProxyRequired', () => {
diff --git a/packages/sveltekit/test/server/integrations/http.test.ts b/packages/sveltekit/test/server/integrations/http.test.ts
index 08a6fffcd06f..53e854d45441 100644
--- a/packages/sveltekit/test/server/integrations/http.test.ts
+++ b/packages/sveltekit/test/server/integrations/http.test.ts
@@ -1,8 +1,12 @@
import * as SentryNode from '@sentry/node';
-import { describe, expect, it, vi } from 'vitest';
+import { afterEach, describe, expect, it, vi } from 'vitest';
import { httpIntegration as svelteKitHttpIntegration } from '../../../src/server/integrations/http';
describe('httpIntegration', () => {
+ afterEach(() => {
+ vi.restoreAllMocks();
+ });
+
it('calls the original httpIntegration with incoming request span recording disabled', () => {
const sentryNodeHttpIntegration = vi.spyOn(SentryNode, 'httpIntegration');
svelteKitHttpIntegration({ breadcrumbs: false });
diff --git a/packages/sveltekit/test/vite/sentrySvelteKitPlugins.test.ts b/packages/sveltekit/test/vite/sentrySvelteKitPlugins.test.ts
index 5221b323d056..c78099729998 100644
--- a/packages/sveltekit/test/vite/sentrySvelteKitPlugins.test.ts
+++ b/packages/sveltekit/test/vite/sentrySvelteKitPlugins.test.ts
@@ -2,7 +2,7 @@ import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import type { Plugin } from 'vite';
-import { describe, expect, it, vi } from 'vitest';
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import * as autoInstrument from '../../src/vite/autoInstrument';
import { generateVitePluginOptions, sentrySvelteKit } from '../../src/vite/sentryVitePlugins';
import * as sourceMaps from '../../src/vite/sourceMaps';
@@ -69,9 +69,9 @@ describe('sentrySvelteKit()', () => {
expect(plugins).toBeInstanceOf(Array);
// 1 kit config resolver + 1 browser-tracing variant resolver + 1 OpenTelemetry API resolver
// + 1 auto instrument plugin
- // + 1 orchestrion plugin + 1 global values injection plugin + 1 modified main plugin
+ // + 1 orchestrion plugin + 1 build flag plugin + 1 global values injection plugin + 1 modified main plugin
// + 3 custom plugins
- expect(plugins).toHaveLength(10);
+ expect(plugins).toHaveLength(11);
});
it('returns the custom sentry source maps upload plugin, unmodified sourcemaps plugins and the auto-instrument plugin by default', async () => {
@@ -88,6 +88,8 @@ describe('sentrySvelteKit()', () => {
'sentry-auto-instrumentation',
// orchestrion build-time instrumentation plugin:
'sentry-orchestrion-vite',
+ // build flag plugin:
+ 'sentry-sveltekit-build-flag',
// global values injection plugin:
'sentry-sveltekit-global-values-injection-plugin',
// modified main plugin (writeBundle deferred to closeBundle):
@@ -101,7 +103,7 @@ describe('sentrySvelteKit()', () => {
it("doesn't return the sentry source maps plugins if autoUploadSourcemaps is `false`", async () => {
const plugins = await getSentrySvelteKitPlugins({ autoUploadSourceMaps: false });
- expect(plugins).toHaveLength(5); // kit config resolver + browser-tracing variant resolver + OpenTelemetry API resolver + auto instrument + orchestrion
+ expect(plugins).toHaveLength(6); // kit config resolver + browser-tracing variant resolver + OpenTelemetry API resolver + auto instrument + orchestrion + build flag
});
it("doesn't return the sentry source maps plugins if `NODE_ENV` is development", async () => {
@@ -111,7 +113,7 @@ describe('sentrySvelteKit()', () => {
const plugins = await getSentrySvelteKitPlugins({ autoUploadSourceMaps: true, autoInstrument: true });
const instrumentPlugin = plugins[3];
- expect(plugins).toHaveLength(6); // kit config resolver + browser-tracing variant resolver + OpenTelemetry API resolver + auto instrument + orchestrion + global values injection
+ expect(plugins).toHaveLength(7); // kit config resolver + browser-tracing variant resolver + OpenTelemetry API resolver + auto instrument + orchestrion + build flag + global values injection
expect(instrumentPlugin?.name).toEqual('sentry-auto-instrumentation');
process.env.NODE_ENV = previousEnv;
@@ -120,7 +122,7 @@ describe('sentrySvelteKit()', () => {
it("doesn't return the auto instrument plugin if autoInstrument is `false`", async () => {
const plugins = await getSentrySvelteKitPlugins({ autoInstrument: false });
const pluginNames = plugins.map(plugin => plugin.name);
- expect(plugins).toHaveLength(9); // kit config resolver + browser-tracing variant resolver + OpenTelemetry API resolver + orchestrion + global values injection + 1 modified main plugin + 3 custom plugins
+ expect(plugins).toHaveLength(10); // kit config resolver + browser-tracing variant resolver + OpenTelemetry API resolver + orchestrion + build flag + global values injection + 1 modified main plugin + 3 custom plugins
expect(pluginNames).not.toContain('sentry-auto-instrumentation');
});
@@ -293,6 +295,94 @@ describe('OpenTelemetry API resolver plugin', () => {
});
});
+describe('build flag plugin', () => {
+ type HookHandler = (...args: unknown[]) => void;
+ type BuildFlagPlugin = {
+ enforce: string;
+ config: HookHandler;
+ configResolved: HookHandler;
+ closeBundle: { order: string; sequential: boolean; handler: HookHandler };
+ buildApp: { order: string; handler: HookHandler };
+ };
+
+ beforeEach(() => {
+ vi.stubEnv('_SENTRY_SVELTEKIT_BUILDING', undefined);
+ });
+
+ afterEach(() => {
+ vi.unstubAllEnvs();
+ });
+
+ async function startBuild(resolvedConfig: { builder?: object; build: { ssr?: boolean; watch?: object | null } }) {
+ const plugins = await getSentrySvelteKitPlugins({ autoUploadSourceMaps: false });
+ const plugin = plugins.find(p => p.name === 'sentry-sveltekit-build-flag') as unknown as BuildFlagPlugin;
+
+ plugin.config({}, { command: 'build', mode: 'production' });
+ plugin.configResolved(resolvedConfig);
+
+ return plugin;
+ }
+
+ it('sets `_SENTRY_SVELTEKIT_BUILDING` so the prerender worker inherits it', async () => {
+ await startBuild({ build: { ssr: true } });
+
+ expect(process.env._SENTRY_SVELTEKIT_BUILDING).toBe('true');
+ });
+
+ it("runs its hooks after SvelteKit's", async () => {
+ const plugin = await startBuild({ build: { ssr: true } });
+
+ expect(plugin.enforce).toBe('post');
+ expect(plugin.closeBundle).toMatchObject({ order: 'post', sequential: true });
+ expect(plugin.buildApp).toMatchObject({ order: 'post' });
+ });
+
+ describe('Kit 2 (no app builder)', () => {
+ it('removes the flag after the SSR build', async () => {
+ const plugin = await startBuild({ build: { ssr: true } });
+
+ // Vite 7+ calls `buildApp` hooks before the legacy build even starts
+ plugin.buildApp.handler();
+ expect(process.env._SENTRY_SVELTEKIT_BUILDING).toBe('true');
+
+ plugin.closeBundle.handler();
+ expect(process.env._SENTRY_SVELTEKIT_BUILDING).toBeUndefined();
+ });
+
+ it("keeps the flag after Kit's nested client build", async () => {
+ const plugin = await startBuild({ build: { ssr: false } });
+
+ plugin.closeBundle.handler();
+
+ expect(process.env._SENTRY_SVELTEKIT_BUILDING).toBe('true');
+ });
+ });
+
+ describe('Kit 3 (app builder)', () => {
+ it('removes the flag after all `buildApp` hooks ran', async () => {
+ const plugin = await startBuild({ builder: {}, build: { ssr: true } });
+
+ plugin.closeBundle.handler();
+ expect(process.env._SENTRY_SVELTEKIT_BUILDING).toBe('true');
+
+ plugin.buildApp.handler();
+ expect(process.env._SENTRY_SVELTEKIT_BUILDING).toBeUndefined();
+ });
+ });
+
+ it.each([
+ ['Kit 2', {}],
+ ['Kit 3', { builder: {} }],
+ ])('keeps the flag in watch mode (%s)', async (_, config) => {
+ const plugin = await startBuild({ ...config, build: { ssr: true, watch: {} } });
+
+ plugin.closeBundle.handler();
+ plugin.buildApp.handler();
+
+ expect(process.env._SENTRY_SVELTEKIT_BUILDING).toBe('true');
+ });
+});
+
describe('generateVitePluginOptions', () => {
it('returns null if no relevant options are provided', () => {
const options: SentrySvelteKitPluginOptions = {};
diff --git a/packages/tanstackstart-react/package.json b/packages/tanstackstart-react/package.json
index b34dbae115dc..52ed9335a644 100644
--- a/packages/tanstackstart-react/package.json
+++ b/packages/tanstackstart-react/package.json
@@ -62,7 +62,7 @@
"@sentry/bundler-plugins": "11.1.0"
},
"devDependencies": {
- "vite": "^6.4.3"
+ "vite": "^8.3.1"
},
"scripts": {
"build": "run-p build:transpile build:types",
diff --git a/packages/vue/src/rootInstrumentation.ts b/packages/vue/src/rootInstrumentation.ts
index 4f0e30dd2b2c..5b16a7346b4b 100644
--- a/packages/vue/src/rootInstrumentation.ts
+++ b/packages/vue/src/rootInstrumentation.ts
@@ -18,7 +18,8 @@ function createRootViewModel(): VueSentry {
* builds where the Options API is compiled out and `app.mixin()` is a silent no-op (Nuxt 5 default).
*
* Vue runs all `mounted` hooks before `mount()` returns, so the wrap covers the same window as the
- * mixin's root hooks. Late mounts extend neither path; the mixin's debounce timers are per component.
+ * mixin's root hooks. Late mounts extend the span only when mixins are used: they record render activity
+ * from every component, while the `app.mount()` wrap only observes the root component.
*/
export function instrumentAppMountWithoutMixin(app: Vue, mixins: Mixins): void {
// A second wrap would duplicate the root spans (e.g. user and Nuxt SDK both add the integration).
diff --git a/packages/vue/src/tracing.ts b/packages/vue/src/tracing.ts
index bbcd1002a376..a6084cd58ff0 100644
--- a/packages/vue/src/tracing.ts
+++ b/packages/vue/src/tracing.ts
@@ -45,6 +45,7 @@ export interface VueSentry extends ViewModel {
};
$_sentryRootComponentSpan?: Span;
$_sentryRootComponentSpanTimer?: ReturnType;
+ $_sentryRootComponentSpanActivity?: number;
}
// Mappings from operation to corresponding lifecycle hook.
@@ -59,18 +60,41 @@ const HOOKS: { [key in Operation]: Hook[] } = {
update: ['beforeUpdate', 'updated'],
};
-/** End the top-level component span and activity with a debounce configured using `timeout` option */
+/**
+ * End the root component span once no render activity happened for `timeout` (a debounce).
+ *
+ * All debounce state lives on `$root`, so hooks from every component share one timer. Each component
+ * only writes a timestamp (no new timer is created). So mounting any number of components uses a single timer.
+ */
function maybeEndRootComponentSpan(vm: VueSentry, timestamp: number, timeout: number): void {
- if (vm.$_sentryRootComponentSpanTimer) {
- clearTimeout(vm.$_sentryRootComponentSpanTimer);
+ const root = vm.$root;
+ root.$_sentryRootComponentSpanActivity = timestamp;
+
+ if (!root.$_sentryRootComponentSpanTimer) {
+ scheduleRootComponentSpanEnd(root, timestamp, timeout);
}
+}
+
+/**
+ * Fires `delayMs` after the activity that scheduled it. Activity recorded in the meantime pushes
+ * the deadline out by the recorded gap, so the span always ends at the last activity timestamp.
+ */
+function scheduleRootComponentSpanEnd(root: VueSentry, activityWhenScheduled: number, delayMs: number): void {
+ root.$_sentryRootComponentSpanTimer = setTimeout(() => {
+ root.$_sentryRootComponentSpanTimer = undefined;
+
+ const lastActivity = root.$_sentryRootComponentSpanActivity ?? activityWhenScheduled;
+ if (lastActivity > activityWhenScheduled) {
+ // activity happened after this timer was scheduled: push the deadline out
+ scheduleRootComponentSpanEnd(root, lastActivity, (lastActivity - activityWhenScheduled) * 1000);
+ return;
+ }
- vm.$_sentryRootComponentSpanTimer = setTimeout(() => {
- if (vm.$root?.$_sentryRootComponentSpan) {
- vm.$root.$_sentryRootComponentSpan.end(timestamp);
- vm.$root.$_sentryRootComponentSpan = undefined;
+ if (root.$_sentryRootComponentSpan) {
+ root.$_sentryRootComponentSpan.end(lastActivity);
+ root.$_sentryRootComponentSpan = undefined;
}
- }, timeout);
+ }, delayMs);
}
/** Find if the current component exists in the provided `TracingOptions.trackComponents` array option. */
diff --git a/packages/vue/test/integration/mixinRegistration.test.ts b/packages/vue/test/integration/mixinRegistration.test.ts
index 9f9a03aa71d4..8b5eb9de4048 100644
--- a/packages/vue/test/integration/mixinRegistration.test.ts
+++ b/packages/vue/test/integration/mixinRegistration.test.ts
@@ -2,6 +2,7 @@
* @vitest-environment jsdom
*/
+import type * as SentryCore from '@sentry/core';
import { spanToJSON } from '@sentry/core';
import type { MockInstance } from 'vitest';
import { afterEach, beforeEach, describe, expect, it as baseIt, vi } from 'vitest';
@@ -10,6 +11,13 @@ import { createApp, defineAsyncComponent, h, nextTick, ref } from 'vue';
import * as Sentry from '../../src';
import type { Options, TracingOptions } from '../../src/types';
+vi.mock('@sentry/core', async importOriginal => {
+ return {
+ ...(await importOriginal()),
+ timestampInSeconds: () => Date.now() / 1000,
+ };
+});
+
const PUBLIC_DSN = 'https://username@domain/123';
const ROOT_SPAN_TIMEOUT_MS = 100;
@@ -259,9 +267,8 @@ describe('tracing mixin span creation', () => {
]);
});
- // `maybeEndRootComponentSpan` arms one debounce timer per component, so a late child never
- // clears the root's earlier timer, and the root's timer ends the span first. The twin test in
- // the disabled describe below proves the `app.mount()` wrap matches.
+ // The debounce only waits for recorded render activity, so a child that never mounts records
+ // none and cannot keep the root render span open.
it('ends the root render span before a deferred child mounts', ({ uiSpans, initSentry }) => {
const { app } = createAppWithDeferredChild();
initSentry({ sdk: { app } });
@@ -274,6 +281,36 @@ describe('tracing mixin span creation', () => {
]);
});
+ it('extends the root render span while children keep mounting within the timeout', async ({
+ uiSpans,
+ initSentry,
+ }) => {
+ const showChild = ref(false);
+ const child = { name: 'ChildComponent', render: () => h('p', 'child') };
+ const app = createApp({ name: 'RootComponent', render: () => h('div', [showChild.value ? h(child) : null]) });
+ initSentry({ sdk: { app } });
+ const container = document.createElement('div');
+
+ await Sentry.startSpan({ name: 'pageload' }, async () => {
+ app.mount(container);
+
+ vi.advanceTimersByTime(ROOT_SPAN_TIMEOUT_MS - 1);
+ showChild.value = true;
+ await nextTick();
+
+ // The original deadline passes, but the child's mount pushed it out.
+ vi.advanceTimersByTime(2);
+ expect(uiSpans.map(span => span.op)).not.toContain(UI_RENDER_SPAN_OP);
+
+ vi.advanceTimersByTime(ROOT_SPAN_TIMEOUT_MS + 1);
+ });
+
+ expect(uiSpans).toEqual([
+ { name: 'Vue ', op: UI_MOUNT_SPAN_OP },
+ { name: 'Application Render', op: UI_RENDER_SPAN_OP },
+ ]);
+ });
+
it('names UI spans after the component and preserves the original description when span streaming is enabled', ({
app,
uiSpans,
@@ -350,7 +387,8 @@ describe('tracing mixin span creation', () => {
expect(rootInstance.$el).toBe(container.firstElementChild);
});
- // Matches the mixin-path twin above: the mixin never waited for late children either.
+ // Unlike the mixin path, the `app.mount()` wrap only observes the root, so a late child can
+ // never extend the root render span here.
it('ends the root render span before a deferred child mounts', ({ uiSpans, initSentry }) => {
const { app } = createAppWithDeferredChild();
disableOptionsApi(app);
diff --git a/packages/vue/test/tracing/tracingMixin.test.ts b/packages/vue/test/tracing/tracingMixin.test.ts
index 72767c1005b6..5598c98f2f5c 100644
--- a/packages/vue/test/tracing/tracingMixin.test.ts
+++ b/packages/vue/test/tracing/tracingMixin.test.ts
@@ -1,9 +1,18 @@
import { getActiveSpan, startInactiveSpan } from '@sentry/browser';
+import type * as SentryCore from '@sentry/core';
import type { Mock } from 'vitest';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { DEFAULT_HOOKS } from '../../src/constants';
import { createTracingMixins } from '../../src/tracing';
+const clock = vi.hoisted(() => ({ now: 0 }));
+vi.mock('@sentry/core', async importOriginal => {
+ return {
+ ...(await importOriginal()),
+ timestampInSeconds: () => clock.now,
+ };
+});
+
vi.mock('@sentry/browser', () => {
return {
getActiveSpan: vi.fn(),
@@ -42,6 +51,7 @@ describe('Vue Tracing Mixins', () => {
beforeEach(() => {
vi.clearAllMocks();
+ clock.now = 0;
mockRootInstance = {
$root: null,
@@ -148,6 +158,53 @@ describe('Vue Tracing Mixins', () => {
);
});
+ describe('Root Span Debounce', () => {
+ it('arms a single shared timer on the root for any number of untracked components', () => {
+ const mixins = createTracingMixins({ trackComponents: false, timeout: 1000 });
+
+ mixins.beforeMount.call(mockRootInstance);
+ for (let i = 0; i < 100; i++) {
+ const child = { $root: mockRootInstance, componentName: `ChildComponent${i}` };
+ mixins.beforeMount.call(child);
+ mixins.mounted.call(child);
+ }
+
+ expect(vi.getTimerCount()).toBe(1);
+ });
+
+ it('ends the root span at the last recorded activity, pushing the deadline out for late activity', () => {
+ const mixins = createTracingMixins({ trackComponents: false, timeout: 1000 });
+ clock.now = 10;
+ mixins.beforeMount.call(mockRootInstance);
+ const rootSpan = mockRootInstance.$_sentryRootComponentSpan;
+
+ clock.now = 10.8;
+ mixins.mounted.call(mockVueInstance);
+
+ vi.advanceTimersByTime(1000);
+ expect(rootSpan.end).not.toHaveBeenCalled();
+
+ vi.advanceTimersByTime(800);
+ expect(rootSpan.end).toHaveBeenCalledWith(10.8);
+ });
+
+ it('records no span when a component mounts after the root span already ended', () => {
+ const mixins = createTracingMixins({ trackComponents: false, timeout: 1000 });
+ mixins.beforeMount.call(mockRootInstance);
+ const rootSpan = mockRootInstance.$_sentryRootComponentSpan;
+ vi.advanceTimersByTime(1001);
+ expect(rootSpan.end).toHaveBeenCalledTimes(1);
+
+ clock.now = 5;
+ mixins.beforeMount.call(mockVueInstance);
+ mixins.mounted.call(mockVueInstance);
+ vi.advanceTimersByTime(1001);
+
+ expect(rootSpan.end).toHaveBeenCalledTimes(1);
+ expect(mockRootInstance.$_sentryRootComponentSpan).toBeUndefined();
+ });
+ });
+
describe('Component Span Lifecycle', () => {
it('should create and end spans correctly through lifecycle hooks', () => {
const mixins = createTracingMixins({ trackComponents: true });
diff --git a/vite/vite.config.ts b/vite/vite.config.ts
index a22ca23e42f1..5c6e36c55c0d 100644
--- a/vite/vite.config.ts
+++ b/vite/vite.config.ts
@@ -5,8 +5,10 @@ const STRIP_ESM_MARKERS = /[ \t]*\/\*! rollup-include-esm-only(?:-end)? \*\/[ \t
// Mirrors the ESM path of makeEsmCjsReplacePlugin from rollup-utils. Vitest
// transforms TypeScript source to ESM so tests see only the ESM branch.
+// Runs before TS transpilation, which can move the marker comments.
const esmOnlyPlugin = {
name: 'vitest-esm-only',
+ enforce: 'pre' as const,
transform(code: string) {
if (!code.includes('rollup-include-')) return null;
return { code: code.replace(REMOVE_CJS_BLOCK, '').replace(STRIP_ESM_MARKERS, '') };
diff --git a/yarn.lock b/yarn.lock
index 2796b4ef923e..004333c9dcd0 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -190,7 +190,7 @@
"@jridgewell/gen-mapping" "^0.1.0"
"@jridgewell/trace-mapping" "^0.3.9"
-"@ampproject/remapping@^2.1.0", "@ampproject/remapping@^2.2.1", "@ampproject/remapping@^2.3.0":
+"@ampproject/remapping@^2.1.0", "@ampproject/remapping@^2.2.1":
version "2.3.0"
resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4"
integrity sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==
@@ -3549,14 +3549,6 @@
source-map-url "^0.4.1"
terser "^5.7.0"
-"@emnapi/core@1.11.1", "@emnapi/core@^1.1.0":
- version "1.11.1"
- resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.11.1.tgz#b9e1064f3a6b1631e241e638eb48d736bfd372a6"
- integrity sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==
- dependencies:
- "@emnapi/wasi-threads" "1.2.2"
- tslib "^2.4.0"
-
"@emnapi/core@1.4.5":
version "1.4.5"
resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.4.5.tgz#bfbb0cbbbb9f96ec4e2c4fd917b7bbe5495ceccb"
@@ -3565,11 +3557,12 @@
"@emnapi/wasi-threads" "1.0.4"
tslib "^2.4.0"
-"@emnapi/runtime@1.11.1":
+"@emnapi/core@^1.1.0":
version "1.11.1"
- resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.11.1.tgz#58f1f3d5d81a9b12f793ab688c96371901027c24"
- integrity sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==
+ resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.11.1.tgz#b9e1064f3a6b1631e241e638eb48d736bfd372a6"
+ integrity sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==
dependencies:
+ "@emnapi/wasi-threads" "1.2.2"
tslib "^2.4.0"
"@emnapi/runtime@1.4.5":
@@ -3631,11 +3624,6 @@
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz#815b39267f9bffd3407ea6c376ac32946e24f8d2"
integrity sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==
-"@esbuild/aix-ppc64@0.27.7":
- version "0.27.7"
- resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz#82b74f92aa78d720b714162939fb248c90addf53"
- integrity sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==
-
"@esbuild/aix-ppc64@0.28.1":
version "0.28.1"
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz#7a01a8d2ec2fbb2dac78adad09b0fa781e4082be"
@@ -3666,11 +3654,6 @@
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz#19b882408829ad8e12b10aff2840711b2da361e8"
integrity sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==
-"@esbuild/android-arm64@0.27.7":
- version "0.27.7"
- resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz#f78cb8a3121fc205a53285adb24972db385d185d"
- integrity sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==
-
"@esbuild/android-arm64@0.28.1":
version "0.28.1"
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz#b540a27d14e4afd058496a4dbec4d3f414db110a"
@@ -3706,11 +3689,6 @@
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.27.3.tgz#90be58de27915efa27b767fcbdb37a4470627d7b"
integrity sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==
-"@esbuild/android-arm@0.27.7":
- version "0.27.7"
- resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.27.7.tgz#593e10a1450bbfcac6cb321f61f468453bac209d"
- integrity sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==
-
"@esbuild/android-arm@0.28.1":
version "0.28.1"
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.28.1.tgz#704bd297de6d762de54eabbeafbf55f6756abe2f"
@@ -3741,11 +3719,6 @@
resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.27.3.tgz#d7dcc976f16e01a9aaa2f9b938fbec7389f895ac"
integrity sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==
-"@esbuild/android-x64@0.27.7":
- version "0.27.7"
- resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.27.7.tgz#453143d073326033d2d22caf9e48de4bae274b07"
- integrity sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==
-
"@esbuild/android-x64@0.28.1":
version "0.28.1"
resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.28.1.tgz#d1cb166d34b0fbf0fe8ab460a5594f24a378701e"
@@ -3776,11 +3749,6 @@
resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz#9f6cac72b3a8532298a6a4493ed639a8988e8abd"
integrity sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==
-"@esbuild/darwin-arm64@0.27.7":
- version "0.27.7"
- resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz#6f23000fb9b40b7e04b7d0606c0693bd0632f322"
- integrity sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==
-
"@esbuild/darwin-arm64@0.28.1":
version "0.28.1"
resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz#1034b26457fc886368fe61bbd09f653f6afa8e54"
@@ -3811,11 +3779,6 @@
resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz#ac61d645faa37fd650340f1866b0812e1fb14d6a"
integrity sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==
-"@esbuild/darwin-x64@0.27.7":
- version "0.27.7"
- resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz#27393dd18bb1263c663979c5f1576e00c2d024be"
- integrity sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==
-
"@esbuild/darwin-x64@0.28.1":
version "0.28.1"
resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz#65556a432a1e4d72032d8218c1932fcca1a49772"
@@ -3846,11 +3809,6 @@
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz#b8625689d73cf1830fe58c39051acdc12474ea1b"
integrity sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==
-"@esbuild/freebsd-arm64@0.27.7":
- version "0.27.7"
- resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz#22e4638fa502d1c0027077324c97640e3adf3a62"
- integrity sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==
-
"@esbuild/freebsd-arm64@0.28.1":
version "0.28.1"
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz#2e61e0592f9030d7e3dae18ee25ebc535918aef6"
@@ -3881,11 +3839,6 @@
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz#07be7dd3c9d42fe0eccd2ab9f9ded780bc53bead"
integrity sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==
-"@esbuild/freebsd-x64@0.27.7":
- version "0.27.7"
- resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz#9224b8e4fea924ce2194e3efc3e9aebf822192d6"
- integrity sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==
-
"@esbuild/freebsd-x64@0.28.1":
version "0.28.1"
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz#c95ec289959ef8079c4dca817a1e2c4be66b9bd3"
@@ -3916,11 +3869,6 @@
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz#bf31918fe5c798586460d2b3d6c46ed2c01ca0b6"
integrity sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==
-"@esbuild/linux-arm64@0.27.7":
- version "0.27.7"
- resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz#4f5d1c27527d817b35684ae21419e57c2bda0966"
- integrity sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==
-
"@esbuild/linux-arm64@0.28.1":
version "0.28.1"
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz#40b22175dda06182f3ee8141186c5ff304c4a717"
@@ -3951,11 +3899,6 @@
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz#28493ee46abec1dc3f500223cd9f8d2df08f9d11"
integrity sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==
-"@esbuild/linux-arm@0.27.7":
- version "0.27.7"
- resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz#b9e9d070c8c1c0449cf12b20eac37d70a4595921"
- integrity sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==
-
"@esbuild/linux-arm@0.28.1":
version "0.28.1"
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz#c09a0f67917592ac0de892a9be4d3814debd2a6c"
@@ -3986,11 +3929,6 @@
resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz#750752a8b30b43647402561eea764d0a41d0ee29"
integrity sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==
-"@esbuild/linux-ia32@0.27.7":
- version "0.27.7"
- resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz#3f80fb696aa96051a94047f35c85b08b21c36f9e"
- integrity sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==
-
"@esbuild/linux-ia32@0.28.1":
version "0.28.1"
resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz#a580f9c676797833891e519fc7a1337c8afd8db3"
@@ -4031,11 +3969,6 @@
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz#a5a92813a04e71198c50f05adfaf18fc1e95b9ed"
integrity sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==
-"@esbuild/linux-loong64@0.27.7":
- version "0.27.7"
- resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz#9be1f2c28210b13ebb4156221bba356fe1675205"
- integrity sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==
-
"@esbuild/linux-loong64@0.28.1":
version "0.28.1"
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz#46452cf321dc7f9e91c2fa780a56bb56e79cd68b"
@@ -4066,11 +3999,6 @@
resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz#deb45d7fd2d2161eadf1fbc593637ed766d50bb1"
integrity sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==
-"@esbuild/linux-mips64el@0.27.7":
- version "0.27.7"
- resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz#4ab5ee67a3dfcbcb5e8fd7883dae6e735b1163b8"
- integrity sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==
-
"@esbuild/linux-mips64el@0.28.1":
version "0.28.1"
resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz#4211b3184dd6608f53dcb22e39f5d34ee08852c8"
@@ -4101,11 +4029,6 @@
resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz#6f39ae0b8c4d3d2d61a65b26df79f6e12a1c3d78"
integrity sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==
-"@esbuild/linux-ppc64@0.27.7":
- version "0.27.7"
- resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz#dac78c689f6499459c4321e5c15032c12307e7ea"
- integrity sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==
-
"@esbuild/linux-ppc64@0.28.1":
version "0.28.1"
resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz#697857c2a61cb9b0b6bb6652e40c1dc5e1ca8e5d"
@@ -4136,11 +4059,6 @@
resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz#4c5c19c3916612ec8e3915187030b9df0b955c1d"
integrity sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==
-"@esbuild/linux-riscv64@0.27.7":
- version "0.27.7"
- resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz#050f7d3b355c3a98308e935bc4d6325da91b0027"
- integrity sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==
-
"@esbuild/linux-riscv64@0.28.1":
version "0.28.1"
resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz#d192943eb146a40ac4c6497d0cf7be35b986bf08"
@@ -4171,11 +4089,6 @@
resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz#9ed17b3198fa08ad5ccaa9e74f6c0aff7ad0156d"
integrity sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==
-"@esbuild/linux-s390x@0.27.7":
- version "0.27.7"
- resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz#d61f715ce61d43fe5844ad0d8f463f88cbe4fef6"
- integrity sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==
-
"@esbuild/linux-s390x@0.28.1":
version "0.28.1"
resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz#acea0356da0e0ebc08f97cf7b9c2e401e1e648dc"
@@ -4206,11 +4119,6 @@
resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz#12383dcbf71b7cf6513e58b4b08d95a710bf52a5"
integrity sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==
-"@esbuild/linux-x64@0.27.7":
- version "0.27.7"
- resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz#ca8e1aa478fc8209257bf3ac8f79c4dc2982f32a"
- integrity sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==
-
"@esbuild/linux-x64@0.28.1":
version "0.28.1"
resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz#6f0c3ce0cb64c534b70c4c45ecb2c16d34e35dfd"
@@ -4236,11 +4144,6 @@
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz#dd0cb2fa543205fcd931df44f4786bfcce6df7d7"
integrity sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==
-"@esbuild/netbsd-arm64@0.27.7":
- version "0.27.7"
- resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz#1650f2c1b948deeb3ef948f2fc30614723c09690"
- integrity sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==
-
"@esbuild/netbsd-arm64@0.28.1":
version "0.28.1"
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz#8bcd77077a0dce3378b574fedb26d2a253b73d36"
@@ -4271,11 +4174,6 @@
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz#028ad1807a8e03e155153b2d025b506c3787354b"
integrity sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==
-"@esbuild/netbsd-x64@0.27.7":
- version "0.27.7"
- resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz#65772ab342c4b3319bf0705a211050aac1b6e320"
- integrity sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==
-
"@esbuild/netbsd-x64@0.28.1":
version "0.28.1"
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz#e7fb2a01e99c830c94e6623cd9fefb4c8fb58347"
@@ -4301,11 +4199,6 @@
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz#e3c16ff3490c9b59b969fffca87f350ffc0e2af5"
integrity sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==
-"@esbuild/openbsd-arm64@0.27.7":
- version "0.27.7"
- resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz#37ed7cfa66549d7955852fce37d0c3de4e715ea1"
- integrity sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==
-
"@esbuild/openbsd-arm64@0.28.1":
version "0.28.1"
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz#c52909372db8b86e2c55e05a8940033b5660a3b2"
@@ -4336,11 +4229,6 @@
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz#c5a4693fcb03d1cbecbf8b422422468dfc0d2a8b"
integrity sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==
-"@esbuild/openbsd-x64@0.27.7":
- version "0.27.7"
- resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz#01bf3d385855ef50cb33db7c4b52f957c34cd179"
- integrity sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==
-
"@esbuild/openbsd-x64@0.28.1":
version "0.28.1"
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz#c427b9be5a64c262ff9a7eb70b5fbbaadf446c6c"
@@ -4366,11 +4254,6 @@
resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz#082082444f12db564a0775a41e1991c0e125055e"
integrity sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==
-"@esbuild/openharmony-arm64@0.27.7":
- version "0.27.7"
- resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz#6c1f94b34086599aabda4eac8f638294b9877410"
- integrity sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==
-
"@esbuild/openharmony-arm64@0.28.1":
version "0.28.1"
resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz#dc9b147baca2e6c4b3c85571741ef4860a489097"
@@ -4401,11 +4284,6 @@
resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz#5ab036c53f929e8405c4e96e865a424160a1b537"
integrity sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==
-"@esbuild/sunos-x64@0.27.7":
- version "0.27.7"
- resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz#4b0dd17ae0a6941d2d0fd35a906392517071a90d"
- integrity sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==
-
"@esbuild/sunos-x64@0.28.1":
version "0.28.1"
resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz#ce866d12df13c15e4c99f073a3d466f6e0649b3a"
@@ -4436,11 +4314,6 @@
resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz#38de700ef4b960a0045370c171794526e589862e"
integrity sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==
-"@esbuild/win32-arm64@0.27.7":
- version "0.27.7"
- resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz#34193ab5565d6ff68ca928ac04be75102ccb2e77"
- integrity sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==
-
"@esbuild/win32-arm64@0.28.1":
version "0.28.1"
resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz#7468e3692d01d629d5941e5d83817bb80f9e39b4"
@@ -4471,11 +4344,6 @@
resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz#451b93dc03ec5d4f38619e6cd64d9f9eff06f55c"
integrity sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==
-"@esbuild/win32-ia32@0.27.7":
- version "0.27.7"
- resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz#eb67f0e4482515d8c1894ede631c327a4da9fc4d"
- integrity sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==
-
"@esbuild/win32-ia32@0.28.1":
version "0.28.1"
resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz#a5bc0063fb2bcab6d0ed63f2a1537958bc269ec6"
@@ -4506,11 +4374,6 @@
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz#0eaf705c941a218a43dba8e09f1df1d6cd2f1f17"
integrity sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==
-"@esbuild/win32-x64@0.27.7":
- version "0.27.7"
- resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz#8fe30b3088b89b4873c3a6cc87597ae3920c0a8b"
- integrity sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==
-
"@esbuild/win32-x64@0.28.1":
version "0.28.1"
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz#10064ee44f4347b90c9a02b446bbf80a91632b12"
@@ -5584,7 +5447,7 @@
"@jridgewell/resolve-uri" "^3.0.3"
"@jridgewell/sourcemap-codec" "^1.4.10"
-"@jridgewell/trace-mapping@^0.3.18", "@jridgewell/trace-mapping@^0.3.23", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25", "@jridgewell/trace-mapping@^0.3.28", "@jridgewell/trace-mapping@^0.3.31", "@jridgewell/trace-mapping@^0.3.9":
+"@jridgewell/trace-mapping@^0.3.18", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25", "@jridgewell/trace-mapping@^0.3.28", "@jridgewell/trace-mapping@^0.3.31", "@jridgewell/trace-mapping@^0.3.9":
version "0.3.31"
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz#db15d6781c931f3a251a3dac39501c98a6082fd0"
integrity sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==
@@ -5831,13 +5694,6 @@
"@emnapi/runtime" "^1.1.0"
"@tybys/wasm-util" "^0.9.0"
-"@napi-rs/wasm-runtime@^1.1.5":
- version "1.1.5"
- resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz#cccd6ebc40b991dea6936f9126b1b8155b6c4c95"
- integrity sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==
- dependencies:
- "@tybys/wasm-util" "^0.10.2"
-
"@nestjs/common@^10.0.0":
version "10.4.15"
resolved "https://registry.yarnpkg.com/@nestjs/common/-/common-10.4.15.tgz#27c291466d9100eb86fdbe6f7bbb4d1a6ad55f70"
@@ -6706,106 +6562,206 @@
resolved "https://registry.yarnpkg.com/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.143.0.tgz#de6161a53df0ec70bf78f7fd03a1bd363d4b05a1"
integrity sha512-n9uozULWflPqBtdmI8lAabLqGKNgLVNN0ZH8HfgCwpKGNtzRzauB76jTiW/3YLkcA7N1zskpi9GdVnZuu1SAvg==
+"@oxc-parser/binding-android-arm-eabi@0.152.0":
+ version "0.152.0"
+ resolved "https://registry.yarnpkg.com/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.152.0.tgz#173f10d30a2d7108cbf7d80e14726f395ef3dd2b"
+ integrity sha512-393VtirINkae4XHBTkq/yQxhOwEr4V+r0et37P2fp+sYZSdYgZTRDPo5Um8vBh0QjMZ+RmaMy4PP0qwr4cTMwA==
+
"@oxc-parser/binding-android-arm64@0.143.0":
version "0.143.0"
resolved "https://registry.yarnpkg.com/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.143.0.tgz#26fbfda4dbf2456cb5bf5871119c509c434d1fce"
integrity sha512-9BbdjHETk6O3zH/DDid9IgBtF0GlpLabNKN231uraXpRDSfY+iiZxTP5bk1Z63GBownVdhdINFIeddmMz4MzpQ==
+"@oxc-parser/binding-android-arm64@0.152.0":
+ version "0.152.0"
+ resolved "https://registry.yarnpkg.com/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.152.0.tgz#408c709b8e9dd5a3d8ec968e0b87dddfb5eab193"
+ integrity sha512-iYFcExATJNZ0rZj0o02THbD1Nc9jAW0nVgIQPee6ejbSASpbZXlIMw8jxtpzaG14JQTWwGSZ6U5dEtRJXE6V3Q==
+
"@oxc-parser/binding-darwin-arm64@0.143.0":
version "0.143.0"
resolved "https://registry.yarnpkg.com/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.143.0.tgz#2e5006d4d002006ca1570be8ae24105066c603b8"
integrity sha512-gh+6ecoHUy4/sUcolBl/1qPXKBbYNxFY0Pk0ujgQvINTMSftJY7o4yb8gOkDJPeZeB8+a+u7xTe6umoP8N5HFA==
+"@oxc-parser/binding-darwin-arm64@0.152.0":
+ version "0.152.0"
+ resolved "https://registry.yarnpkg.com/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.152.0.tgz#f2dc1f27fbbf26eb55f8f48ca8dc8a41d07e0a14"
+ integrity sha512-fnU+DfUzi5/rFy0LzmipYRz06PAUUkOS8kCJQ3Eg6oqlunC1XZXoffFqxmdL51RmcbYtUG3P8DVG1U5fckoa2A==
+
"@oxc-parser/binding-darwin-x64@0.143.0":
version "0.143.0"
resolved "https://registry.yarnpkg.com/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.143.0.tgz#6876551067865e4cf5411a3c453b22e97c6fd860"
integrity sha512-qd1hl2d+lXgHv/VQ/M9qm8TrMC5T4RqDBwtOnl+1D0QMjwcz+8AaB4JSg8STgeag0GP6a6L74XEGAsrTSJWNzQ==
+"@oxc-parser/binding-darwin-x64@0.152.0":
+ version "0.152.0"
+ resolved "https://registry.yarnpkg.com/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.152.0.tgz#1f705820f0326a0b5416249a98b38ef4dbeef431"
+ integrity sha512-Y3U2okryWTs7hDld6UhjJwLNf8/do+x4g4C+1U5SiGwrTLwbO83WJYJA8+l9iFSilWBgnfpd/+U76tfeNxUHzA==
+
"@oxc-parser/binding-freebsd-x64@0.143.0":
version "0.143.0"
resolved "https://registry.yarnpkg.com/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.143.0.tgz#9c99437e08a15a6e8cca9e48dc0f667337d37fef"
integrity sha512-M5XXcNa7aOqLPKTR41msfghKu2yQ4xWvCm11/gwU0JzOzHNk5sgW//rVEjJ+LO48+VDAMzXTSzurUVxIDKwozw==
+"@oxc-parser/binding-freebsd-x64@0.152.0":
+ version "0.152.0"
+ resolved "https://registry.yarnpkg.com/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.152.0.tgz#3b80d2e7cec517e9eb61050dd097033c73089ddc"
+ integrity sha512-XkhkRrrPffEw+ZLv/d/5pEni0buxinm/E/N6fLbJCunD1DBbeCTtVP23ESV+4CWCWdSz397asks1ViGslpmWiQ==
+
"@oxc-parser/binding-linux-arm-gnueabihf@0.143.0":
version "0.143.0"
resolved "https://registry.yarnpkg.com/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.143.0.tgz#c8803be9811eb1fca83a4bebbc9ff5d70244d250"
integrity sha512-T/GXusuOkPNQhCQCSBbcU/N8j0rAypuDBl1IyFK+lyYT594XsVz80clPC/OtbSSpBGyJxj8uYEfctxVuxVYoww==
+"@oxc-parser/binding-linux-arm-gnueabihf@0.152.0":
+ version "0.152.0"
+ resolved "https://registry.yarnpkg.com/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.152.0.tgz#536c374489cdf6bbf09d83c278c2d685dfcf2c69"
+ integrity sha512-MLzsRRbdfmdzo6JEhbC1yKU9ItfvM8tEf18HM32jCiNYZEz6J3bRBRl3cTkKR0vBdrk9ZVlcARXtwHccs+epDA==
+
"@oxc-parser/binding-linux-arm-musleabihf@0.143.0":
version "0.143.0"
resolved "https://registry.yarnpkg.com/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.143.0.tgz#26dbad4cfa6bdc976204a025b663adafd4a22401"
integrity sha512-oKu4RcBlXSqo3OC62dp6YTnQaZIurNDpCX3BnAM3+bJxt7s8J2TJKMnC0UYer1qhlRaDCg6wkTaTw+2IlsZ12w==
+"@oxc-parser/binding-linux-arm-musleabihf@0.152.0":
+ version "0.152.0"
+ resolved "https://registry.yarnpkg.com/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.152.0.tgz#7d31cb38c61d75db6d2fd940b8d36a173b3f46ba"
+ integrity sha512-HHerTEJz7Iy10Ji6BGsk4gqG6PU0UzFuBu91jIEOX89FnrI5ECWSEr7BLpkYQhp7U0QFtMnBhNfQKI0hYZK78w==
+
"@oxc-parser/binding-linux-arm64-gnu@0.143.0":
version "0.143.0"
resolved "https://registry.yarnpkg.com/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.143.0.tgz#d66424ff4b7b51fb79cbb0a0bf92f1521399cce8"
integrity sha512-WJBbD186AZmMGaSIhlktC+rPl8L3peCTXAh88Ih9uEvK0en2mPojGyCGYiL6mHtV1RPV3JyfJW5t6n5hh0lXhA==
+"@oxc-parser/binding-linux-arm64-gnu@0.152.0":
+ version "0.152.0"
+ resolved "https://registry.yarnpkg.com/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.152.0.tgz#dfa7bdc555f43729848ade572b9e2827adbbe720"
+ integrity sha512-9Bb8GyzZ2uSLzRDZ27usaKokr07mxiBe2dDF5b6bd2janmaJTT1gL51UfP7AU30yFiXrJvcRyFKREAgKwZYs0w==
+
"@oxc-parser/binding-linux-arm64-musl@0.143.0":
version "0.143.0"
resolved "https://registry.yarnpkg.com/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.143.0.tgz#003228caab0d5f68b13516af4f4cd1a2aa091b22"
integrity sha512-t1AcYOwEzgceadT4v5e+vaCCb0AncCA3v5AyzfBAz/tMq11qzVccXKzNHtkWdjBsgvTKwRkaUF3QvT4kot8vcQ==
+"@oxc-parser/binding-linux-arm64-musl@0.152.0":
+ version "0.152.0"
+ resolved "https://registry.yarnpkg.com/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.152.0.tgz#b0df90fce5c7350d18d1a26cd888e31e0ba5f0ee"
+ integrity sha512-om7iDMUroI74A/lfRRWY9/Ev3VWrVsrUZ4tyvyh8raBey/sdcfyAD7tYWc+iykAiotnmftyKTpUKFr1QnaBrAQ==
+
"@oxc-parser/binding-linux-ppc64-gnu@0.143.0":
version "0.143.0"
resolved "https://registry.yarnpkg.com/@oxc-parser/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.143.0.tgz#3066ccd33d2d67316a8672135f77a9e3b0e33fb1"
integrity sha512-RsnO/NoD8376LMJq8JS8TwI0ieNaFRTuNe2GVJntQg6gwZNMENZsEbknHdVwjpOmxdGLGodcwaGSbAeRr5Bgjw==
+"@oxc-parser/binding-linux-ppc64-gnu@0.152.0":
+ version "0.152.0"
+ resolved "https://registry.yarnpkg.com/@oxc-parser/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.152.0.tgz#ed5fcfec68a61e7205b57e84b30ff4d8a2812e07"
+ integrity sha512-Lf1OL2lMTqwDMFC6jnRb30B+WYHKV0XT+bPHcFM6xHtNVwxpobiTzUIvKH6a0LWpvfwyru8Yu6zCGHqqFHH+gg==
+
"@oxc-parser/binding-linux-riscv64-gnu@0.143.0":
version "0.143.0"
resolved "https://registry.yarnpkg.com/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.143.0.tgz#807265a203ed1a3db44d5ac261c5c33f5e32b536"
integrity sha512-48fSVfR9TZi5CASZFyv0VC6z6BCoeihFsX031mAD/oSH7d9PYsPgIqza7d9mjP7Z2KTEpTFyH6SIu0Ui6R1vdg==
+"@oxc-parser/binding-linux-riscv64-gnu@0.152.0":
+ version "0.152.0"
+ resolved "https://registry.yarnpkg.com/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.152.0.tgz#dc95bfc76c0252630cb4266a25b72e74026786d2"
+ integrity sha512-BYWSMQK+YJ9ZILStPcmmiSVq8KEG9Qdq/c4BHPC3rUEq/fEDCVjUIwdj5ZzjgZ9P9is9PKu0tcAXzq1ZXg4urQ==
+
"@oxc-parser/binding-linux-riscv64-musl@0.143.0":
version "0.143.0"
resolved "https://registry.yarnpkg.com/@oxc-parser/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.143.0.tgz#c42d104ef4f5bafa6f3fb12e4363039040f233f1"
integrity sha512-T8CpdD+SfE01DnIOD4HpVxu0ZJOfMJ/VhCvikKfaXAxkZ+9veyLM/D2hpi7Y2hFUyPmVQO3FNZHmYzV/WlVR4g==
+"@oxc-parser/binding-linux-riscv64-musl@0.152.0":
+ version "0.152.0"
+ resolved "https://registry.yarnpkg.com/@oxc-parser/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.152.0.tgz#2846b546b430981fcda466fb793e4261f02fb6bd"
+ integrity sha512-d2LqHNNNqdH185sRK5dPKWZu201JrNu9/CRFuVEuT+ht7n/QaVblEMWT9MALQPrDWYvhG10P+NSKJFquDWZ09w==
+
"@oxc-parser/binding-linux-s390x-gnu@0.143.0":
version "0.143.0"
resolved "https://registry.yarnpkg.com/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.143.0.tgz#1fb99a080d36bb0e049eb04aac2b00d46e0b85e9"
integrity sha512-QLdeMsCcacenPEFsfxnBUDF1y6opyz5+fmOz9bfD5Y7fiGCMupUCuB3KTPQhNwshIG1P9fPqar9MHxuBDd4bwQ==
+"@oxc-parser/binding-linux-s390x-gnu@0.152.0":
+ version "0.152.0"
+ resolved "https://registry.yarnpkg.com/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.152.0.tgz#6da6686b988b9e170ff0c8d4e3addbfff56cd599"
+ integrity sha512-8gBeHt5rA/8+pKhHl+zLggfa3L/V/L2at96p7ty6pBE7id6lFO19nFUc15u2PcjC4OYeYK465MKy1fhhbpFIGw==
+
"@oxc-parser/binding-linux-x64-gnu@0.143.0":
version "0.143.0"
resolved "https://registry.yarnpkg.com/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.143.0.tgz#6b3954ccda436ddd5ec6087d6fab3660462611ff"
integrity sha512-659ujfqLy6k7cuH3sbzhd8b+ztSq+i6E2E9pG78Q0BmHjAExfGIdgc8cGgMdwAozDXeZFHkJ+LXYJdWsaGdgyw==
+"@oxc-parser/binding-linux-x64-gnu@0.152.0":
+ version "0.152.0"
+ resolved "https://registry.yarnpkg.com/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.152.0.tgz#bb9d6140ae8f920a31f4295c3cc2e72616753dcf"
+ integrity sha512-ltgrSF6L+yPTVsIdaAyqKq3zTqzj/1VlNbxWZuxAe8flR3sWdyNsKvdXukoO8/ucJbLickhmieRBge+o/e8Ugw==
+
"@oxc-parser/binding-linux-x64-musl@0.143.0":
version "0.143.0"
resolved "https://registry.yarnpkg.com/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.143.0.tgz#8550941ea2a12406bf8da838f2abaf1a00701c73"
integrity sha512-/Mw/9j4TfZcnKphPrzOE6t4MMknXadcAAuVUlDRTF/ETWB5xOgQvOJV2Mh9We/bWxZdoxaGAdc+hy4GuYwQ2yQ==
+"@oxc-parser/binding-linux-x64-musl@0.152.0":
+ version "0.152.0"
+ resolved "https://registry.yarnpkg.com/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.152.0.tgz#2c21fd8c901d881be84d7818c35cabf366b5a456"
+ integrity sha512-LcMp6BvrBnewSfdkLHoSqidhoAq4syKOTBV5TYHWd+2VnGkfD/nPRRVIzTBt/Lkbt7pfOFDGtkiTntFS0p6sQQ==
+
"@oxc-parser/binding-openharmony-arm64@0.143.0":
version "0.143.0"
resolved "https://registry.yarnpkg.com/@oxc-parser/binding-openharmony-arm64/-/binding-openharmony-arm64-0.143.0.tgz#07bdd1126b872a153bfd5e07925ea69eb5369ec4"
integrity sha512-8rIKWR2BFuifbIK/1XB9wTaSdtuJ25dlE7ZQYDnEwj/2xH2vHsxnvIjHT3ZjSVuLLwGGlSslIG/fbOJ8TV8rTw==
+"@oxc-parser/binding-openharmony-arm64@0.152.0":
+ version "0.152.0"
+ resolved "https://registry.yarnpkg.com/@oxc-parser/binding-openharmony-arm64/-/binding-openharmony-arm64-0.152.0.tgz#7ff80a0a0ff265eba35a8d58583ed7829ef61d9d"
+ integrity sha512-jbhoMCslVxzRCRNQ8TSHxjYdSKRvk8O6ciLVDkahJvmyHDzBqba/bMQHM6ULKj/rgHNNbSzlIiakdx8SEfkJzA==
+
"@oxc-parser/binding-win32-arm64-msvc@0.143.0":
version "0.143.0"
resolved "https://registry.yarnpkg.com/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.143.0.tgz#031921d8fd37d754dc1b5db61a2509a7dcd90b10"
integrity sha512-5U9kQYMfRRI6Zq7KDxgbIP0RMnKrfn3gLepRMgJuRkPSUALTiRCk9d/uyhb4lGDjUdzwK7mBkKqhLgzBPCmLpQ==
+"@oxc-parser/binding-win32-arm64-msvc@0.152.0":
+ version "0.152.0"
+ resolved "https://registry.yarnpkg.com/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.152.0.tgz#5e29c076a46691a28862e4fdd0d86664f216cd42"
+ integrity sha512-JZYVlhv36mr/nertnRB7vchJECOBb1Lz6lltalILsB+o9M/is+H6BEDRLQKdqA4JKs6NjJszo46+iM5sfwj6jQ==
+
"@oxc-parser/binding-win32-ia32-msvc@0.143.0":
version "0.143.0"
resolved "https://registry.yarnpkg.com/@oxc-parser/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.143.0.tgz#61faaf9668c65ea615edcebabb78748591f40ac1"
integrity sha512-25P7AaHk4R88Yv2XH4gToDVmh0cOu+bEURQU10CRrmvgabfRArSGAP5osmwUKeSUHj0VS50upbpbRWWW/m7mHA==
+"@oxc-parser/binding-win32-ia32-msvc@0.152.0":
+ version "0.152.0"
+ resolved "https://registry.yarnpkg.com/@oxc-parser/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.152.0.tgz#455135640931fc0bb22e040073dab539a080ffe5"
+ integrity sha512-1X9oganvVsunRH0lH10UU05N+H9atpX6WsOybByUjwnfcd3h/0tYwvuv16XuHglrIUOnxV2TodbLVEwwafd64g==
+
"@oxc-parser/binding-win32-x64-msvc@0.143.0":
version "0.143.0"
resolved "https://registry.yarnpkg.com/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.143.0.tgz#b1a93532ab5a8fb1998012b74cd7efe7bfdb1b4d"
integrity sha512-ORMh3JE1s6V7ySicdRK7vgaDQnn5o+UHg9ct989PlWHbel8O9ARrmWXM6kZjrBMtNucxNayQ8g69G0VfWzhANw==
-"@oxc-project/types@=0.137.0":
- version "0.137.0"
- resolved "https://registry.yarnpkg.com/@oxc-project/types/-/types-0.137.0.tgz#56e77f8bb221fa05f18b1cd34d73f94f0954a773"
- integrity sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==
+"@oxc-parser/binding-win32-x64-msvc@0.152.0":
+ version "0.152.0"
+ resolved "https://registry.yarnpkg.com/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.152.0.tgz#c80fc3a2e18f88df2d375ebd9c666fa7d41b9cdb"
+ integrity sha512-sw0M80/dn9rcpu+Rqqph/C+MjvDDce6MlXmFjsNQtPEymha0eJy3ml6+foSUTqFfpDIDyHZZ4eoJ43IoMe8bJw==
+
+"@oxc-project/types@=0.151.0":
+ version "0.151.0"
+ resolved "https://sfw.security.sentry.io/npm/@oxc-project/types/-/types-0.151.0.tgz#37206ed3fdebb8c4c5fc49ae20d53d9b4eed2bfe"
+ integrity sha512-J1yXrIlNDZVzE3ada310xeAw7nH8yCAyLPuUIsjKatFPmfn5bS1oW+cM+QsGOtVWd5nhSpbwZWx/rue+r5Z+PA==
"@oxc-project/types@^0.143.0":
version "0.143.0"
resolved "https://registry.yarnpkg.com/@oxc-project/types/-/types-0.143.0.tgz#c3e4f3178b7b54e4dd194eac6d45258a60f0092b"
integrity sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA==
+"@oxc-project/types@^0.152.0":
+ version "0.152.0"
+ resolved "https://registry.yarnpkg.com/@oxc-project/types/-/types-0.152.0.tgz#643ff80ce7aacea025f859f13b60cbb42ae0a613"
+ integrity sha512-oM/5rLBm2tPkg0iBgkH/FOeR3PCDpY19GTgAZjMFM8h9WI9VW7cLgzp6nwtarYKmovavIQZ+Fe/RKX/8C8O/Rw==
+
"@oxc-transform/binding-android-arm-eabi@0.143.0":
version "0.143.0"
resolved "https://registry.yarnpkg.com/@oxc-transform/binding-android-arm-eabi/-/binding-android-arm-eabi-0.143.0.tgz#c41fa3a80da46fef5b78c5599d88dba7eaf47e27"
@@ -7622,6 +7578,11 @@
react-router-dom "6.30.4"
turbo-stream "2.4.1"
+"@remix-run/route-pattern@^0.24.0":
+ version "0.24.1"
+ resolved "https://sfw.security.sentry.io/npm/@remix-run/route-pattern/-/route-pattern-0.24.1.tgz#cee709b2f946940777bb4775a8334598f3c4b909"
+ integrity sha512-HRee7tz2Ct7QdTQGDErHcVTAlXa5BBV6GOLEa/INw21ceTWTWlaU13FJupYVX0k1mh608rod880+SA5NlTPPfw==
+
"@remix-run/router@1.23.3":
version "1.23.3"
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.23.3.tgz#957c098d4393d301a8aa7dccf3ef28ea5430e36a"
@@ -7688,84 +7649,80 @@
dependencies:
web-streams-polyfill "^3.1.1"
-"@rolldown/binding-android-arm64@1.1.2":
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.2.tgz#88fd6b295a411e62b7926433a45eb5e17e68bba4"
- integrity sha512-2cZ+7xRS+DBcuJBJKnfzsbleumJhBqSlJVpuzHC0nTqfd3QQ7Vx2/x5YR/D7cBamKSeWplwo82Fn9lqYUDEMfA==
+"@rolldown/binding-android-arm-eabi@1.2.11":
+ version "1.2.11"
+ resolved "https://sfw.security.sentry.io/npm/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.11.tgz#898b1c0c0c4de1ffb5997050a705f04cc102b4eb"
+ integrity sha512-A5kXfGKvKWWZE0TtPrfsvT+q4Y5d1QG8gGUzpYjGydM+fARM9MuX90PrXYXe0XbsDVgyxxNzHo6giCj90bsFNw==
-"@rolldown/binding-darwin-arm64@1.1.2":
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.2.tgz#2f840f7e6501cb52370411c2fb008119f1fbf400"
- integrity sha512-RkPMJnygxsgOYdkfqgpwY0/Fzm8d0VQe6HGU2/B00Xa9eqdLbrII+DOKAodbJAn3ZL1AJxGHkZRPYazgGY6Ljw==
+"@rolldown/binding-android-arm64@1.2.11":
+ version "1.2.11"
+ resolved "https://sfw.security.sentry.io/npm/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.11.tgz#bf8ee88baaecd1747326d6c1d05ebdad483edc34"
+ integrity sha512-z6cTycz+iJ4PVkuL4HHW4DfTfoeU/2nqYYuSOrTmH7yHK5Y0LCOnA03V4ZNxavyVaU1oOqUgIg2klN/s+USGOA==
-"@rolldown/binding-darwin-x64@1.1.2":
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.2.tgz#2ed3b66dded5140d22ca2ac58d4e1c1e3143f490"
- integrity sha512-Uiczh6vFhwyfd7WNe7Q7mCA4KxAiLdz7jPE/WGizfRpIieoyFuNVMmM8HqZ9HwudTkY6/AeMQwlNJ9NJijguWw==
+"@rolldown/binding-darwin-arm64@1.2.11":
+ version "1.2.11"
+ resolved "https://sfw.security.sentry.io/npm/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.11.tgz#4ebc103da1e0d6b24d815c1fb1d6af73245c1b1f"
+ integrity sha512-jShvqNtP6vDC6/A5JOAzbVV+DkgHqhl/ScVCJEbt+TUY6QYz7YnXcrg3sLtFBniro0f/Ld50ZwCWA6f7KYD1nQ==
-"@rolldown/binding-freebsd-x64@1.1.2":
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.2.tgz#d3d8603ae480a505eb8c12643c901b2a3771b875"
- integrity sha512-+TpdtTRgHiJFjCVFbw311SuLk3KfytPOQQn+VlAEv+gBxYPtL7E6JS9e/tk+8CwxhIZvemJKo4rTKgfWNsKkkA==
+"@rolldown/binding-darwin-x64@1.2.11":
+ version "1.2.11"
+ resolved "https://sfw.security.sentry.io/npm/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.11.tgz#ab669b448eb2cd2366a7ff01a84579a0f854f5f0"
+ integrity sha512-f2i2xiNWq1Z1l2++q2fuhZRdLAT3aqxD6vRNm1RAxpUoBcdqNB3C0s1Bt+K+PbEx2F5F4gQp6hqKkphCY/xF9w==
-"@rolldown/binding-linux-arm-gnueabihf@1.1.2":
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.2.tgz#bc36e0e33566bc80877fe4bca56fd32b241dbacb"
- integrity sha512-4lv1/tkmi7ueIVHnyreaOeUpiZP26BH9rRy6hoYfR9310A2B9nUEVRDvBx69vx64Nr3eTPPRkyciqJJs+j9Jmw==
+"@rolldown/binding-freebsd-x64@1.2.11":
+ version "1.2.11"
+ resolved "https://sfw.security.sentry.io/npm/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.11.tgz#74c26ec877c293c47c8958ab58e07e57f9382070"
+ integrity sha512-4Ir5FSOKIAMr4r0kExpt1s3bMgzJU3rA45AYOHtQpls0oNeqcYBKrWMlckrYH4KCfGLfkfn1tN1dmZPMVsdXow==
-"@rolldown/binding-linux-arm64-gnu@1.1.2":
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.2.tgz#6267a447e6bfc5a99eb030a6a99194ecc917a652"
- integrity sha512-gBSUVO0eaWgw1JMjK3gB8BMlX2Mk148s2lTiVT3e9vjVxbl7UDfMWWY8CfIaaqiXuM9fVTMxIpUz6CAo/B6Vlw==
+"@rolldown/binding-linux-arm-gnueabihf@1.2.11":
+ version "1.2.11"
+ resolved "https://sfw.security.sentry.io/npm/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.11.tgz#cbfe844867d7dbce1655c9b593be05bcaffb501d"
+ integrity sha512-/gnRDM+39BROzAN/k1OZjDPnDMcZxB/0EUxKjONO5yVkNEvlsoMDrxGNKgZi/ttFriS2gwlDNzB65pvNbFOXIQ==
-"@rolldown/binding-linux-arm64-musl@1.1.2":
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.2.tgz#68fc068f5ebc1d137eecdb651d90830f330aad48"
- integrity sha512-LjQP/iZLBu8o8PjIfk4x3At0/mT6h282pvz8Z5LAyhGbu/kDezyO7ea62rF5uoqmgnIYqbN/MqJ3Si3Aymi7xQ==
+"@rolldown/binding-linux-arm64-gnu@1.2.11":
+ version "1.2.11"
+ resolved "https://sfw.security.sentry.io/npm/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.11.tgz#2028ebac4a3f56b26ecd02f8850c74447087cbb2"
+ integrity sha512-PFaK8HwvAHbaKbBcDNQihjMKYvFnA5hiENx/l5tphTDz1E0WFp32l0A7aq7lyUwGsRw/xSrNIy/gIK4thrSCrw==
-"@rolldown/binding-linux-ppc64-gnu@1.1.2":
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.2.tgz#fe072f0bc3b713ae25b357651ced38d39e3d81e0"
- integrity sha512-X/7bVLWelEsbyWDUSXt7zVsTniLLPIY2n1rH58qr78l9i7MNbbxBWD8gI2vRfBWf4NUXJCUuQnfZDsp32LqsfQ==
+"@rolldown/binding-linux-arm64-musl@1.2.11":
+ version "1.2.11"
+ resolved "https://sfw.security.sentry.io/npm/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.11.tgz#a143325f96d5d5040dff50e292374f7467e7c82c"
+ integrity sha512-AskzJUIKRLPxkruR1wLKewGbOw+EYfU/9lOrBFj4AFrEA8hPpKFnODWNu2WLaNs0QNkEb9QIJufmVZZIL/bJlg==
-"@rolldown/binding-linux-s390x-gnu@1.1.2":
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.2.tgz#9492609384775c6edec9bfbe5b1cbba9660dddcc"
- integrity sha512-gb6dYKW/1KDorGXyy48glEBJs/sxVSC5pcVrox/pFGV4mvwSFeg2sK5L2tRkVsVlh7kueqOgg4GEcuipJcGuKg==
+"@rolldown/binding-linux-ppc64-gnu@1.2.11":
+ version "1.2.11"
+ resolved "https://sfw.security.sentry.io/npm/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.11.tgz#b38937ad8b4821682e3671ebfb609cb0e3ecb935"
+ integrity sha512-qlUGAheh2yh8afH7QBgx0PrRHN85hKnNd78x8MeMhXivuevgd8vgf6/CstOzmNKY/lLTHvNTrPy98cLnAugzJw==
-"@rolldown/binding-linux-x64-gnu@1.1.2":
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.2.tgz#74029d9f86d60fa9bcf2670b1480e22438203c98"
- integrity sha512-JY4w85pU3iAiJVMh5nuk4/Mh9GjMsupe8MrIN53rwxAZW64GKrWeJBuN6SxQg9QTU5uB1cxyhDzW8jqRn1EABw==
+"@rolldown/binding-linux-s390x-gnu@1.2.11":
+ version "1.2.11"
+ resolved "https://sfw.security.sentry.io/npm/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.11.tgz#6be2645a1ad89185e8d03f69116798c94eae19ec"
+ integrity sha512-secpEad+0vCbSfn8upFySkDskv+bGPk3THSDS9Y89yc4rb4kzqHp8Dmyd9BkQW4SnhNXBZCl/6CrO//hZahNJQ==
-"@rolldown/binding-linux-x64-musl@1.1.2":
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.2.tgz#eb3004015027a7af12f9b0ac85ffa1634015c873"
- integrity sha512-xvpA7o5KCYLB0Rwscmuylb1/zHHSUx4g4xilm4prC5jP76pEUlzBmMbgpbh7bVDbId4NcfT96gN5i6mE6UDaiw==
+"@rolldown/binding-linux-x64-gnu@1.2.11":
+ version "1.2.11"
+ resolved "https://sfw.security.sentry.io/npm/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.11.tgz#fbc9a6a6c3b3db00e35573c67894bfbaa3128040"
+ integrity sha512-mOVBT3dPpkWm8XBWPmU4bf+U6dYDLeMo/9ojUmis4N0L5uu10qra5vOyngZ7/PSdoE4G9KvRt4bloRxNjLas7A==
-"@rolldown/binding-openharmony-arm64@1.1.2":
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.2.tgz#6a02c49dc0f698614f97c68c6f7c21aadc7600b1"
- integrity sha512-p/ts6KBLjuk49Bp21XH77poQGt02iNz7ChgHep7tudPOaLinR/De/RHdxF8w8Yj4r/bF/bqXwH6PZrB2sA+Nvw==
+"@rolldown/binding-linux-x64-musl@1.2.11":
+ version "1.2.11"
+ resolved "https://sfw.security.sentry.io/npm/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.11.tgz#f3f07a30c03d1d3a673b21fd4eaba84979aa4fd4"
+ integrity sha512-Is78i9A8Ui4SqcxUwFJ9uMmjDn58IbVTjFWYdQestFEgeuEmHMLGNriXnVJKkwG2YiZjw8cP0zCTyDMdDGtOOg==
-"@rolldown/binding-wasm32-wasi@1.1.2":
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.2.tgz#3f67c083e0762b8cd6c95e8edfe1a743d7ffdf78"
- integrity sha512-VMu/wmrZ9hJzYlRhbw7jK5PODlugyKZ5mOdX78+lS8OvuFkWNQdz1pFLrI2p3P0pjXOmUZ7B48o5VnMH9QOGtg==
- dependencies:
- "@emnapi/core" "1.11.1"
- "@emnapi/runtime" "1.11.1"
- "@napi-rs/wasm-runtime" "^1.1.5"
+"@rolldown/binding-openharmony-arm64@1.2.11":
+ version "1.2.11"
+ resolved "https://sfw.security.sentry.io/npm/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.11.tgz#c3848c61dd3b15edde9c98dfa8d50edb356ec71f"
+ integrity sha512-dUCXneZ87INUMyQ0D+C0HrEBNUPNXHaPmU5GTjyKTJEiussw9Kaj5Ln8UztPe4epV/ffvgNBEadksdYhmW6xJA==
-"@rolldown/binding-win32-arm64-msvc@1.1.2":
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.2.tgz#a69c5a03b3ba36cb0b1154709293e0cefc9dd69c"
- integrity sha512-xtUJqs8qEkuSviS0n1tsohaPuz3a1SPhZywOji4Oo+sgrJs8daEDMZ0QtqL0OS7dx8PoVpg2J/ZZycPY5I2+Zg==
+"@rolldown/binding-win32-arm64-msvc@1.2.11":
+ version "1.2.11"
+ resolved "https://sfw.security.sentry.io/npm/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.11.tgz#99d9f7eb53b26f09212d646e9f50708a2f14414f"
+ integrity sha512-jByxb6qfd+bH1xUd0qnfFnb17i9sWBPY2tOavJ0l3tdr3OTu+Kvtm8cd/JV5nFt657b1VqGltxg9olOEfofXWw==
-"@rolldown/binding-win32-x64-msvc@1.1.2":
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.2.tgz#e1d9a6ccd29de00378f8dd6e275adbde5731d30a"
- integrity sha512-85YiLQqjUKgSO/Zjnf9e0XIn5Ymrh1fLDWBeAkZqpuBR/3R8TpfoHXuyblqyQrftSSgWO9qpcHN8mkyKsLraoA==
+"@rolldown/binding-win32-x64-msvc@1.2.11":
+ version "1.2.11"
+ resolved "https://sfw.security.sentry.io/npm/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.11.tgz#c7bf663043c01021f63122a8499e9aca36408467"
+ integrity sha512-/PzKqzAJ03i19oy2ItPvyvaVjOjBCNnfaJs8yvUdGBKmiESgnrJSQ2awd81QzFbbnAmu7YO9ZnJrDCb9VSJPRA==
"@rolldown/pluginutils@^1.0.0", "@rolldown/pluginutils@^1.0.1":
version "1.0.1"
@@ -9154,13 +9111,6 @@
dependencies:
tslib "^2.4.0"
-"@tybys/wasm-util@^0.10.2":
- version "0.10.2"
- resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.10.2.tgz#12b3a1b33db1f9cad4ddff1f604ab7dd00bf464e"
- integrity sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==
- dependencies:
- tslib "^2.4.0"
-
"@types/amqplib@^0.10.5":
version "0.10.5"
resolved "https://registry.yarnpkg.com/@types/amqplib/-/amqplib-0.10.5.tgz#fd883eddfbd669702a727fa10007b27c4c1e6ec7"
@@ -9549,13 +9499,20 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.63.tgz#1788fa8da838dbb5f9ea994b834278205db6ca2b"
integrity sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==
-"@types/node@^18.19.1", "@types/node@^18.6.3":
+"@types/node@^18.19.1":
version "18.19.130"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.130.tgz#da4c6324793a79defb7a62cba3947ec5add00d59"
integrity sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==
dependencies:
undici-types "~5.26.4"
+"@types/node@^20.19.0":
+ version "20.19.43"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-20.19.43.tgz#fcecf580ba42a0db55cf404c372c97973c376c97"
+ integrity sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==
+ dependencies:
+ undici-types "~6.21.0"
+
"@types/parse-json@^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
@@ -10151,85 +10108,81 @@
dependencies:
"@rolldown/pluginutils" "^1.0.1"
-"@vitest/coverage-v8@^3.2.7":
- version "3.2.7"
- resolved "https://registry.yarnpkg.com/@vitest/coverage-v8/-/coverage-v8-3.2.7.tgz#2e9ce1103445c237aaa420a7f0058125fe4a7854"
- integrity sha512-NEGWJS2XNu2PfRLQwOO3CTKj1tTETxNBdk454vDxVBhxJYhPaA/eS0nAI0c+1El1P7a60z8+i+ZrQoGESweGKg==
+"@vitest/coverage-v8@^4.1.11":
+ version "4.1.11"
+ resolved "https://sfw.security.sentry.io/npm/@vitest/coverage-v8/-/coverage-v8-4.1.11.tgz#6f0636abe7e23e86dd35127244554be2c60bc2a5"
+ integrity sha512-8MVGEFnJIcdGjcbfKmeq8z0pZHH0JlVtoVZH9Q/qwUp6wyFnEJUBMrw9DCaj+ra3vShGmhavjalMIhPNxZAUcw==
dependencies:
- "@ampproject/remapping" "^2.3.0"
"@bcoe/v8-coverage" "^1.0.2"
- ast-v8-to-istanbul "^0.3.3"
- debug "^4.4.1"
+ "@vitest/utils" "4.1.11"
+ ast-v8-to-istanbul "^1.0.0"
istanbul-lib-coverage "^3.2.2"
istanbul-lib-report "^3.0.1"
- istanbul-lib-source-maps "^5.0.6"
- istanbul-reports "^3.1.7"
- magic-string "^0.30.17"
- magicast "^0.3.5"
- std-env "^3.9.0"
- test-exclude "^7.0.1"
- tinyrainbow "^2.0.0"
+ istanbul-reports "^3.2.0"
+ magicast "^0.5.2"
+ obug "^2.1.1"
+ std-env "^4.0.0-rc.1"
+ tinyrainbow "^3.1.0"
-"@vitest/expect@3.2.7":
- version "3.2.7"
- resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-3.2.7.tgz#70a34158383d008c3bf5d802e2643317f09df6d8"
- integrity sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==
+"@vitest/expect@4.1.11":
+ version "4.1.11"
+ resolved "https://sfw.security.sentry.io/npm/@vitest/expect/-/expect-4.1.11.tgz#5f580d1f9cdbba314dbf23b2d911f8eb23878f5f"
+ integrity sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==
dependencies:
+ "@standard-schema/spec" "^1.1.0"
"@types/chai" "^5.2.2"
- "@vitest/spy" "3.2.7"
- "@vitest/utils" "3.2.7"
- chai "^5.2.0"
- tinyrainbow "^2.0.0"
+ "@vitest/spy" "4.1.11"
+ "@vitest/utils" "4.1.11"
+ chai "^6.2.2"
+ tinyrainbow "^3.1.0"
-"@vitest/mocker@3.2.7":
- version "3.2.7"
- resolved "https://registry.yarnpkg.com/@vitest/mocker/-/mocker-3.2.7.tgz#331be944cb783c642dd42bd743411aca24ea0466"
- integrity sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==
+"@vitest/mocker@4.1.11":
+ version "4.1.11"
+ resolved "https://sfw.security.sentry.io/npm/@vitest/mocker/-/mocker-4.1.11.tgz#8e2906361bc5dfa271757a858ae80643118fcbb4"
+ integrity sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==
dependencies:
- "@vitest/spy" "3.2.7"
+ "@vitest/spy" "4.1.11"
estree-walker "^3.0.3"
- magic-string "^0.30.17"
+ magic-string "^0.30.21"
-"@vitest/pretty-format@3.2.7", "@vitest/pretty-format@^3.2.7":
- version "3.2.7"
- resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-3.2.7.tgz#2a7b593f8e007e9d8ef7e7343aa30ec73fdeaf29"
- integrity sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==
+"@vitest/pretty-format@4.1.11":
+ version "4.1.11"
+ resolved "https://sfw.security.sentry.io/npm/@vitest/pretty-format/-/pretty-format-4.1.11.tgz#8b28eb8240771d6ea970e33beaeb41384b51868e"
+ integrity sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==
dependencies:
- tinyrainbow "^2.0.0"
+ tinyrainbow "^3.1.0"
-"@vitest/runner@3.2.7":
- version "3.2.7"
- resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-3.2.7.tgz#c0c080228189f1fa6cda40f59be09d746b0aca51"
- integrity sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==
+"@vitest/runner@4.1.11":
+ version "4.1.11"
+ resolved "https://sfw.security.sentry.io/npm/@vitest/runner/-/runner-4.1.11.tgz#bfbad98c8d6c3f1fb4df12056ad569821ff77f21"
+ integrity sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==
dependencies:
- "@vitest/utils" "3.2.7"
+ "@vitest/utils" "4.1.11"
pathe "^2.0.3"
- strip-literal "^3.0.0"
-"@vitest/snapshot@3.2.7":
- version "3.2.7"
- resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-3.2.7.tgz#a3a7e1950ce99ec4cf02395e20ddca403b6c818e"
- integrity sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==
+"@vitest/snapshot@4.1.11":
+ version "4.1.11"
+ resolved "https://sfw.security.sentry.io/npm/@vitest/snapshot/-/snapshot-4.1.11.tgz#df461eb165924a3155986dde68e13360f53f3d4c"
+ integrity sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==
dependencies:
- "@vitest/pretty-format" "3.2.7"
- magic-string "^0.30.17"
+ "@vitest/pretty-format" "4.1.11"
+ "@vitest/utils" "4.1.11"
+ magic-string "^0.30.21"
pathe "^2.0.3"
-"@vitest/spy@3.2.7":
- version "3.2.7"
- resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-3.2.7.tgz#ca7fbee44019523ca450395d9a2284ce9ece1f31"
- integrity sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==
- dependencies:
- tinyspy "^4.0.3"
+"@vitest/spy@4.1.11":
+ version "4.1.11"
+ resolved "https://sfw.security.sentry.io/npm/@vitest/spy/-/spy-4.1.11.tgz#0add45cae953afed9c88f98e2f6fc9164558c32a"
+ integrity sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==
-"@vitest/utils@3.2.7":
- version "3.2.7"
- resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-3.2.7.tgz#302c8126211ac4dfea87b3b5085c098d6d22e89e"
- integrity sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==
+"@vitest/utils@4.1.11":
+ version "4.1.11"
+ resolved "https://sfw.security.sentry.io/npm/@vitest/utils/-/utils-4.1.11.tgz#9b27a4293b827942b223539bfab1bd9f7eada31b"
+ integrity sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==
dependencies:
- "@vitest/pretty-format" "3.2.7"
- loupe "^3.1.4"
- tinyrainbow "^2.0.0"
+ "@vitest/pretty-format" "4.1.11"
+ convert-source-map "^2.0.0"
+ tinyrainbow "^3.1.0"
"@volar/kit@~2.4.28":
version "2.4.28"
@@ -11391,11 +11344,6 @@ assert-never@^1.1.0, assert-never@^1.2.1:
resolved "https://registry.yarnpkg.com/assert-never/-/assert-never-1.4.0.tgz#b0d4988628c87f35eb94716cc54422a63927e175"
integrity sha512-5oJg84os6NMQNl27T9LnZkvvqzvAnHu03ShCnoj6bsJwS7L8AO4lf+C/XjK/nvzEqQB744moC6V128RucQd1jA==
-assertion-error@^2.0.1:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-2.0.1.tgz#f641a196b335690b1070bf00b6e7593fec190bf7"
- integrity sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==
-
ast-kit@^2.1.2, ast-kit@^2.1.3:
version "2.2.0"
resolved "https://registry.yarnpkg.com/ast-kit/-/ast-kit-2.2.0.tgz#6d9a298acefef5bdfc5a0fa51d94d1334ef2e671"
@@ -11428,14 +11376,14 @@ ast-types@^0.16.1:
dependencies:
tslib "^2.0.1"
-ast-v8-to-istanbul@^0.3.3:
- version "0.3.3"
- resolved "https://registry.yarnpkg.com/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.3.tgz#697101c116cff6b51c0e668ba6352e7e41fe8dd5"
- integrity sha512-MuXMrSLVVoA6sYN/6Hke18vMzrT4TZNbZIj/hvh0fnYFpO+/kFXcLIaiPwXXWaQUPg4yJD8fj+lfJ7/1EBconw==
+ast-v8-to-istanbul@^1.0.0:
+ version "1.0.7"
+ resolved "https://sfw.security.sentry.io/npm/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.7.tgz#ead45f98ca59413f048f1b1466a78b6c8a1070bb"
+ integrity sha512-kFL68AG6ajd8fg248zwM9GQrUWEp79gsmjum34OEXjs4yHuUMZfYKwOLW9GMmB4oNvVrj+EAGxsP7ye2UR9UlA==
dependencies:
- "@jridgewell/trace-mapping" "^0.3.25"
+ "@jridgewell/trace-mapping" "^0.3.31"
estree-walker "^3.0.3"
- js-tokens "^9.0.1"
+ js-tokens "^10.0.0"
ast-walker-scope@^0.8.3:
version "0.8.3"
@@ -11637,13 +11585,13 @@ axios@1.16.0:
form-data "^4.0.5"
proxy-from-env "^2.1.0"
-axios@1.18.0:
- version "1.18.0"
- resolved "https://registry.yarnpkg.com/axios/-/axios-1.18.0.tgz#8a7f8854af280fcaae063272df2ed9f3837d2398"
- integrity sha512-E32NzpYKp++W7XRe52rHiXV2ehxmh3wbdgO7MHeFM+vqxLBYHzt0ElkiImtOBxtOmyp0yoC8C6uESVV84Y2/hw==
+axios@1.20.0:
+ version "1.20.0"
+ resolved "https://registry.yarnpkg.com/axios/-/axios-1.20.0.tgz#515513445aa60e71d04b6521ca6210829ccb4786"
+ integrity sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==
dependencies:
follow-redirects "^1.16.0"
- form-data "^4.0.5"
+ form-data "^4.0.6"
https-proxy-agent "^5.0.1"
proxy-from-env "^2.1.0"
@@ -12092,7 +12040,7 @@ boxen@8.0.1, boxen@^8.0.1:
widest-line "^5.0.0"
wrap-ansi "^9.0.0"
-brace-expansion@5.0.6, brace-expansion@^5.0.5:
+brace-expansion@5.0.6:
version "5.0.6"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-5.0.6.tgz#ec68fe0a641a29d8711579caf641d05bae1f2285"
integrity sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==
@@ -12100,20 +12048,27 @@ brace-expansion@5.0.6, brace-expansion@^5.0.5:
balanced-match "^4.0.2"
brace-expansion@^1.1.7:
- version "1.1.18"
- resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.18.tgz#3ce74d89885136be1535341f8c3d4425c29a5cab"
- integrity sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==
+ version "1.1.21"
+ resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.21.tgz#edf4fab5c64d051aea5a8def49aba1c7522279f3"
+ integrity sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==
dependencies:
balanced-match "^1.0.0"
concat-map "0.0.1"
brace-expansion@^2.0.1, brace-expansion@^2.0.2:
- version "2.0.2"
- resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.2.tgz#54fc53237a613d854c7bd37463aad17df87214e7"
- integrity sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==
+ version "2.1.7"
+ resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.1.7.tgz#14ea3836731823bb24740225ea43b941b436f9d1"
+ integrity sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==
dependencies:
balanced-match "^1.0.0"
+brace-expansion@^5.0.5:
+ version "5.0.12"
+ resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-5.0.12.tgz#995fbb4750a77c4d16a7dc942ff2ca6ef8e675ec"
+ integrity sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==
+ dependencies:
+ balanced-match "^4.0.2"
+
braces@^3.0.3, braces@~3.0.2:
version "3.0.3"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
@@ -12625,16 +12580,10 @@ ccount@^2.0.0:
resolved "https://registry.yarnpkg.com/ccount/-/ccount-2.0.1.tgz#17a3bf82302e0870d6da43a01311a8bc02a3ecf5"
integrity sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==
-chai@^5.2.0:
- version "5.2.0"
- resolved "https://registry.yarnpkg.com/chai/-/chai-5.2.0.tgz#1358ee106763624114addf84ab02697e411c9c05"
- integrity sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==
- dependencies:
- assertion-error "^2.0.1"
- check-error "^2.1.1"
- deep-eql "^5.0.1"
- loupe "^3.1.0"
- pathval "^2.0.0"
+chai@^6.2.2:
+ version "6.2.2"
+ resolved "https://sfw.security.sentry.io/npm/chai/-/chai-6.2.2.tgz#ae41b52c9aca87734505362717f3255facda360e"
+ integrity sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==
chainsaw@~0.1.0:
version "0.1.0"
@@ -12685,11 +12634,6 @@ chardet@^0.7.0:
resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==
-check-error@^2.1.1:
- version "2.1.1"
- resolved "https://registry.yarnpkg.com/check-error/-/check-error-2.1.1.tgz#87eb876ae71ee388fa0471fe423f494be1d96ccc"
- integrity sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==
-
"chokidar@>=3.0.0 <4.0.0", chokidar@^3.0.0, chokidar@^3.5.2, chokidar@^3.5.3:
version "3.6.0"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b"
@@ -13718,11 +13662,6 @@ dedent@^1.5.3:
resolved "https://registry.yarnpkg.com/dedent/-/dedent-1.5.3.tgz#99aee19eb9bae55a67327717b6e848d0bf777e5a"
integrity sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==
-deep-eql@^5.0.1:
- version "5.0.2"
- resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-5.0.2.tgz#4b756d8d770a9257300825d52a2c2cff99c3a341"
- integrity sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==
-
deep-equal@^2.0.5:
version "2.2.3"
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.2.3.tgz#af89dafb23a396c7da3e862abc0be27cf51d56e1"
@@ -15146,38 +15085,6 @@ esbuild@^0.25.0, esbuild@^0.25.3:
"@esbuild/win32-ia32" "0.25.12"
"@esbuild/win32-x64" "0.25.12"
-esbuild@^0.27.0:
- version "0.27.7"
- resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.27.7.tgz#bcadce22b2f3fd76f257e3a64f83a64986fea11f"
- integrity sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==
- optionalDependencies:
- "@esbuild/aix-ppc64" "0.27.7"
- "@esbuild/android-arm" "0.27.7"
- "@esbuild/android-arm64" "0.27.7"
- "@esbuild/android-x64" "0.27.7"
- "@esbuild/darwin-arm64" "0.27.7"
- "@esbuild/darwin-x64" "0.27.7"
- "@esbuild/freebsd-arm64" "0.27.7"
- "@esbuild/freebsd-x64" "0.27.7"
- "@esbuild/linux-arm" "0.27.7"
- "@esbuild/linux-arm64" "0.27.7"
- "@esbuild/linux-ia32" "0.27.7"
- "@esbuild/linux-loong64" "0.27.7"
- "@esbuild/linux-mips64el" "0.27.7"
- "@esbuild/linux-ppc64" "0.27.7"
- "@esbuild/linux-riscv64" "0.27.7"
- "@esbuild/linux-s390x" "0.27.7"
- "@esbuild/linux-x64" "0.27.7"
- "@esbuild/netbsd-arm64" "0.27.7"
- "@esbuild/netbsd-x64" "0.27.7"
- "@esbuild/openbsd-arm64" "0.27.7"
- "@esbuild/openbsd-x64" "0.27.7"
- "@esbuild/openharmony-arm64" "0.27.7"
- "@esbuild/sunos-x64" "0.27.7"
- "@esbuild/win32-arm64" "0.27.7"
- "@esbuild/win32-ia32" "0.27.7"
- "@esbuild/win32-x64" "0.27.7"
-
"esbuild@^0.27.0 || ^0.28.0", esbuild@^0.28.0, esbuild@^0.28.1, esbuild@~0.28.0:
version "0.28.2"
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.28.2.tgz#0f43bd1bad955b72d24e2261e3abe5957ccf0816"
@@ -15598,10 +15505,10 @@ expand-tilde@^2.0.0, expand-tilde@^2.0.2:
dependencies:
homedir-polyfill "^1.0.1"
-expect-type@^1.2.1:
- version "1.3.0"
- resolved "https://registry.yarnpkg.com/expect-type/-/expect-type-1.3.0.tgz#0d58ed361877a31bbc4dd6cf71bbfef7faf6bd68"
- integrity sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==
+expect-type@^1.3.0:
+ version "1.4.0"
+ resolved "https://sfw.security.sentry.io/npm/expect-type/-/expect-type-1.4.0.tgz#24edf7f0cc69a44d008567ba4594ab96f3c3a3d6"
+ integrity sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==
express@5.2.1:
version "5.2.1"
@@ -15866,14 +15773,14 @@ fast-text-encoding@^1.0.0:
integrity sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig==
fast-uri@^3.0.0, fast-uri@^3.0.1:
- version "3.1.7"
- resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.7.tgz#743157d957f3cbb4c65310e033dc2ad4ad7dc60a"
- integrity sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==
+ version "3.1.8"
+ resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.8.tgz#f7db8d942e20eead3cfe93b0beeb4eb0169e938b"
+ integrity sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==
fast-uri@^4.0.0:
- version "4.1.4"
- resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-4.1.4.tgz#2076a9d7bce86a86aa31ac40650b935fd5b1cd60"
- integrity sha512-dODXrIxlS9JSdgAnhIUKOosKV1oMtU2VtVw87QRaHzyl5jxO290Ii5tEZfCfzfWNHi3jKWwBSdQj0qIyshdZdQ==
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-4.2.1.tgz#816fb0fd61f511adcc35aadbbc1bcbf656d9e72a"
+ integrity sha512-TmHQgewjHtMq1E5QKA0tOE0yeYGQs25KZC/ziJpubRtWI15W92e6vPFydWOeZBVROSHBYw/QhD9d5OefdD6LDg==
fast-wrap-ansi@^0.2.0:
version "0.2.2"
@@ -15907,10 +15814,10 @@ fast-xml-parser@^4.4.1:
dependencies:
strnum "^1.0.5"
-fastify@^5.12.1:
- version "5.12.1"
- resolved "https://registry.yarnpkg.com/fastify/-/fastify-5.12.1.tgz#d3a736ed0d656d8ffeed909c7099809b24b9f008"
- integrity sha512-FWi+tQvwxR/PeRX7Z2mhfEF5ozJ3jn9asiiclzKXNSzJRHAYcU924aIOKAdHFJ+YIKieh3cqr1IwCOvTr41B3Q==
+fastify@^5.12.5:
+ version "5.12.5"
+ resolved "https://registry.yarnpkg.com/fastify/-/fastify-5.12.5.tgz#e3a6937e154a51332889a30ff3b8f97db52d7355"
+ integrity sha512-OB2k1dlxs5/NAABqeKV2FUHkSD2BbENsCak8yULVcymn3fHIPDVa9TI3SDnJSWYSllZmSYuZXy2gTnsT+Sut1A==
dependencies:
"@fastify/ajv-compiler" "^4.0.5"
"@fastify/error" "^4.0.0"
@@ -16197,7 +16104,7 @@ foreground-child@^3.1.0:
cross-spawn "^7.0.6"
signal-exit "^4.0.1"
-form-data@4.0.6, form-data@^4.0.0, form-data@^4.0.4, form-data@^4.0.5:
+form-data@4.0.6, form-data@^4.0.0, form-data@^4.0.4, form-data@^4.0.5, form-data@^4.0.6:
version "4.0.6"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.6.tgz#28e864e1b786dbebb68db1f452f9635278665827"
integrity sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==
@@ -16610,7 +16517,7 @@ glob@8.0.3:
minimatch "^5.0.1"
once "^1.3.0"
-glob@^10.0.0, glob@^10.3.7, glob@^10.4.1:
+glob@^10.0.0, glob@^10.3.7:
version "10.5.0"
resolved "https://registry.yarnpkg.com/glob/-/glob-10.5.0.tgz#8ec0355919cd3338c28428a23d4f24ecc5fe738c"
integrity sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==
@@ -17209,10 +17116,10 @@ homedir-polyfill@^1.0.1:
dependencies:
parse-passwd "^1.0.0"
-hono@^4.13.5:
- version "4.13.5"
- resolved "https://registry.yarnpkg.com/hono/-/hono-4.13.5.tgz#3df9463c4668a0321c39515309f5891e388e9709"
- integrity sha512-O6+/eCYRkzzzy0rPWwKLiGBR1nFuUPZynnwjxN1MBA62NNqbT0wQEzQyK2gSO5yDIDB336sXQleAhOHrzlYyKw==
+hono@^4.13.7:
+ version "4.13.7"
+ resolved "https://registry.yarnpkg.com/hono/-/hono-4.13.7.tgz#80fdab8326f03d02f049117fd968101933493600"
+ integrity sha512-c8/gF9ac8Y78/agExVocyLevgR+JlpNB444Py0FSX8pJoPdYUfUzRcXtYEYGwt6l19qIlVZPN5Mfsw9jFShmQQ==
hookable@^5.5.3:
version "5.5.3"
@@ -17757,9 +17664,9 @@ ioredis@5.10.1, ioredis@^5.10.1:
standard-as-callback "^2.1.0"
ip-address@^10.0.1:
- version "10.4.0"
- resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-10.4.0.tgz#c5910bc541b6eae287765d1e4846be0308a05d93"
- integrity sha512-oSK96Grm3aP6OrS263xVxbNDGVL7rzBtYdpGqlDG8iQdoenDoTs/nkki+DflYbAEE8Xl6o5YxhxlrKvI3nqKXQ==
+ version "10.7.2"
+ resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-10.7.2.tgz#5b3b2b7d46c6293861b82dfcd49cc203aa8f2c5c"
+ integrity sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w==
ipaddr.js@1.9.1:
version "1.9.1"
@@ -18280,19 +18187,10 @@ istanbul-lib-report@^3.0.0, istanbul-lib-report@^3.0.1:
make-dir "^4.0.0"
supports-color "^7.1.0"
-istanbul-lib-source-maps@^5.0.6:
- version "5.0.6"
- resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz#acaef948df7747c8eb5fbf1265cb980f6353a441"
- integrity sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==
- dependencies:
- "@jridgewell/trace-mapping" "^0.3.23"
- debug "^4.1.1"
- istanbul-lib-coverage "^3.0.0"
-
-istanbul-reports@^3.1.7:
- version "3.1.7"
- resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.1.7.tgz#daed12b9e1dca518e15c056e1e537e741280fa0b"
- integrity sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==
+istanbul-reports@^3.2.0:
+ version "3.2.0"
+ resolved "https://sfw.security.sentry.io/npm/istanbul-reports/-/istanbul-reports-3.2.0.tgz#cb4535162b5784aa623cee21a7252cf2c807ac93"
+ integrity sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==
dependencies:
html-escaper "^2.0.0"
istanbul-lib-report "^3.0.0"
@@ -18396,11 +18294,6 @@ js-tokens@^10.0.0:
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
-js-tokens@^9.0.1:
- version "9.0.1"
- resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-9.0.1.tgz#2ec43964658435296f6761b34e10671c2d9527f4"
- integrity sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==
-
js-yaml@^3.13.0, js-yaml@^3.13.1:
version "3.15.1"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.15.1.tgz#24bc95028f361cdaaa84745b06a109c4486773c0"
@@ -18917,6 +18810,80 @@ light-my-request@^6.0.0:
process-warning "^4.0.0"
set-cookie-parser "^2.6.0"
+lightningcss-android-arm64@1.33.0:
+ version "1.33.0"
+ resolved "https://sfw.security.sentry.io/npm/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz#9a6841f88ae50fc83502903892b41af41bc2b907"
+ integrity sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==
+
+lightningcss-darwin-arm64@1.33.0:
+ version "1.33.0"
+ resolved "https://sfw.security.sentry.io/npm/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz#c0f2c31c0bfd19fa4dd3f18e957a1f1a152097d6"
+ integrity sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==
+
+lightningcss-darwin-x64@1.33.0:
+ version "1.33.0"
+ resolved "https://sfw.security.sentry.io/npm/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz#cb0705965acb538c6683949ce6925fb3cdf7c361"
+ integrity sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==
+
+lightningcss-freebsd-x64@1.33.0:
+ version "1.33.0"
+ resolved "https://sfw.security.sentry.io/npm/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz#763538828b26bab2680dadafcc84ee78b0eb502b"
+ integrity sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==
+
+lightningcss-linux-arm-gnueabihf@1.33.0:
+ version "1.33.0"
+ resolved "https://sfw.security.sentry.io/npm/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz#6862e3176a331aedbdec1ed352b4d7d0dd0784de"
+ integrity sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==
+
+lightningcss-linux-arm64-gnu@1.33.0:
+ version "1.33.0"
+ resolved "https://sfw.security.sentry.io/npm/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz#c6a3a2ed15141daf6bdc2628930f8e39bdf473aa"
+ integrity sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==
+
+lightningcss-linux-arm64-musl@1.33.0:
+ version "1.33.0"
+ resolved "https://sfw.security.sentry.io/npm/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz#7fa1334971fc82845f9827df6ef8a0b20914bac6"
+ integrity sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==
+
+lightningcss-linux-x64-gnu@1.33.0:
+ version "1.33.0"
+ resolved "https://sfw.security.sentry.io/npm/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz#8b927862ea8c2bbc6831a46509244b50d9936e55"
+ integrity sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==
+
+lightningcss-linux-x64-musl@1.33.0:
+ version "1.33.0"
+ resolved "https://sfw.security.sentry.io/npm/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz#0c525bb077dfd94404c059cfe42dad797e96aeaf"
+ integrity sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==
+
+lightningcss-win32-arm64-msvc@1.33.0:
+ version "1.33.0"
+ resolved "https://sfw.security.sentry.io/npm/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz#850ee1103dac989cfab50e3ac22d1a69e394e63d"
+ integrity sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==
+
+lightningcss-win32-x64-msvc@1.33.0:
+ version "1.33.0"
+ resolved "https://sfw.security.sentry.io/npm/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz#e343ae152eed3609dc6e11949d1a3bf39a1c946f"
+ integrity sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==
+
+lightningcss@^1.33.0:
+ version "1.33.0"
+ resolved "https://sfw.security.sentry.io/npm/lightningcss/-/lightningcss-1.33.0.tgz#c08867d71a79385c6e190214fd72fef3e5f95f0b"
+ integrity sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==
+ dependencies:
+ detect-libc "^2.0.3"
+ optionalDependencies:
+ lightningcss-android-arm64 "1.33.0"
+ lightningcss-darwin-arm64 "1.33.0"
+ lightningcss-darwin-x64 "1.33.0"
+ lightningcss-freebsd-x64 "1.33.0"
+ lightningcss-linux-arm-gnueabihf "1.33.0"
+ lightningcss-linux-arm64-gnu "1.33.0"
+ lightningcss-linux-arm64-musl "1.33.0"
+ lightningcss-linux-x64-gnu "1.33.0"
+ lightningcss-linux-x64-musl "1.33.0"
+ lightningcss-win32-arm64-msvc "1.33.0"
+ lightningcss-win32-x64-msvc "1.33.0"
+
lilconfig@^3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.1.3.tgz#a1bcfd6257f9585bf5ae14ceeebb7b559025e4c4"
@@ -19181,11 +19148,6 @@ loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3
dependencies:
js-tokens "^3.0.0 || ^4.0.0"
-loupe@^3.1.0, loupe@^3.1.4:
- version "3.1.4"
- resolved "https://registry.yarnpkg.com/loupe/-/loupe-3.1.4.tgz#784a0060545cb38778ffb19ccde44d7870d5fdd9"
- integrity sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg==
-
lower-case@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28"
@@ -19291,14 +19253,14 @@ magic-string@^0.26.0, magic-string@^0.26.7:
dependencies:
sourcemap-codec "^1.4.8"
-magic-string@^0.30.0, magic-string@^0.30.10, magic-string@^0.30.14, magic-string@^0.30.17, magic-string@^0.30.19, magic-string@^0.30.21, magic-string@^0.30.3, magic-string@^0.30.4, magic-string@^0.30.5, magic-string@~0.30.0, magic-string@~0.30.21, magic-string@~0.30.8:
+magic-string@^0.30.0, magic-string@^0.30.10, magic-string@^0.30.14, magic-string@^0.30.19, magic-string@^0.30.21, magic-string@^0.30.3, magic-string@^0.30.4, magic-string@^0.30.5, magic-string@~0.30.0, magic-string@~0.30.21:
version "0.30.21"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.21.tgz#56763ec09a0fa8091df27879fd94d19078c00d91"
integrity sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==
dependencies:
"@jridgewell/sourcemap-codec" "^1.5.5"
-magic-string@^1.1.0:
+magic-string@^1.1.0, "magic-string@~0.30.8 || ^1.0.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-1.1.0.tgz#54a8470aabf2a04b970177f26c0308cabea74a3f"
integrity sha512-kS3VHe0nEPST2saQV4Rbkchcd3UBRkVTQHo1D3h/ZTwFDhai/mfKkmtPAtD129EOI7K3HlHIsFOt0WrI2/oU9g==
@@ -20269,9 +20231,9 @@ module-lookup-amd@^9.0.3:
requirejs-config-file "^4.0.0"
moment@^2.30.1:
- version "2.30.1"
- resolved "https://registry.yarnpkg.com/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae"
- integrity sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==
+ version "2.31.0"
+ resolved "https://registry.yarnpkg.com/moment/-/moment-2.31.0.tgz#6e19184e8005a9dfc61c9351263d4d6ea2ace7aa"
+ integrity sha512-0acOTfMiWOheYS4eoWb80yYMb/JLvVv9SHbs2PehaDzfUG0Bw855SKyk0IKTnPGa5+U2bmi3W68l1+sGLX/pvw==
mongodb-connection-string-url@^2.6.0:
version "2.6.0"
@@ -20498,10 +20460,10 @@ named-placeholders@^1.1.6:
dependencies:
lru.min "^1.1.0"
-nanoid@^3.3.17, nanoid@^3.3.6, nanoid@^3.3.8:
- version "3.3.18"
- resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.18.tgz#f66a2de1199ffde0fcf21c8a5f13106b1c081913"
- integrity sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==
+nanoid@^3.3.18, nanoid@^3.3.6, nanoid@^3.3.8:
+ version "3.3.19"
+ resolved "https://sfw.security.sentry.io/npm/nanoid/-/nanoid-3.3.19.tgz#336d4aa4bcd4fb24d2cddede7ffeae40bec03f0a"
+ integrity sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==
nanoid@^5.1.7:
version "5.1.9"
@@ -21693,6 +21655,33 @@ oxc-parser@^0.143.0:
"@oxc-parser/binding-win32-ia32-msvc" "0.143.0"
"@oxc-parser/binding-win32-x64-msvc" "0.143.0"
+oxc-parser@^0.152.0:
+ version "0.152.0"
+ resolved "https://registry.yarnpkg.com/oxc-parser/-/oxc-parser-0.152.0.tgz#1520dbf962f210d0d5dd4d6a32487f33afd08179"
+ integrity sha512-ZztsZgaCWmPeKYMdoTT5e0oV/oxD1NeLWNR3kAtr/G707cUPYaaozRlVDHLCGnszHJRszN9mJwmPNpqgwloC0A==
+ dependencies:
+ "@oxc-project/types" "^0.152.0"
+ optionalDependencies:
+ "@oxc-parser/binding-android-arm-eabi" "0.152.0"
+ "@oxc-parser/binding-android-arm64" "0.152.0"
+ "@oxc-parser/binding-darwin-arm64" "0.152.0"
+ "@oxc-parser/binding-darwin-x64" "0.152.0"
+ "@oxc-parser/binding-freebsd-x64" "0.152.0"
+ "@oxc-parser/binding-linux-arm-gnueabihf" "0.152.0"
+ "@oxc-parser/binding-linux-arm-musleabihf" "0.152.0"
+ "@oxc-parser/binding-linux-arm64-gnu" "0.152.0"
+ "@oxc-parser/binding-linux-arm64-musl" "0.152.0"
+ "@oxc-parser/binding-linux-ppc64-gnu" "0.152.0"
+ "@oxc-parser/binding-linux-riscv64-gnu" "0.152.0"
+ "@oxc-parser/binding-linux-riscv64-musl" "0.152.0"
+ "@oxc-parser/binding-linux-s390x-gnu" "0.152.0"
+ "@oxc-parser/binding-linux-x64-gnu" "0.152.0"
+ "@oxc-parser/binding-linux-x64-musl" "0.152.0"
+ "@oxc-parser/binding-openharmony-arm64" "0.152.0"
+ "@oxc-parser/binding-win32-arm64-msvc" "0.152.0"
+ "@oxc-parser/binding-win32-ia32-msvc" "0.152.0"
+ "@oxc-parser/binding-win32-x64-msvc" "0.152.0"
+
oxc-transform@^0.143.0:
version "0.143.0"
resolved "https://registry.yarnpkg.com/oxc-transform/-/oxc-transform-0.143.0.tgz#d5c029abdd1d7faac2d5901d6250535090f04b8c"
@@ -22181,11 +22170,6 @@ pathe@^2.0.1, pathe@^2.0.3:
resolved "https://registry.yarnpkg.com/pathe/-/pathe-2.0.3.tgz#3ecbec55421685b70a9da872b2cff3e1cbed1716"
integrity sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==
-pathval@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/pathval/-/pathval-2.0.0.tgz#7e2550b422601d4f6b8e26f1301bc8f15a741a25"
- integrity sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==
-
pend@~1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
@@ -22307,10 +22291,10 @@ picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.3.1:
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.2.tgz#5a942915e26b372dc0f0e6753149a16e6b1c5601"
integrity sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==
-picomatch@^4.0.2, picomatch@^4.0.3, picomatch@^4.0.4, picomatch@^4.0.5:
- version "4.0.5"
- resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.5.tgz#51ea57a17d86f605f81039595fbc40ed06a55fab"
- integrity sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==
+picomatch@^4.0.2, picomatch@^4.0.3, picomatch@^4.0.4, picomatch@^4.0.5, picomatch@^4.0.7:
+ version "4.0.7"
+ resolved "https://sfw.security.sentry.io/npm/picomatch/-/picomatch-4.0.7.tgz#6313360034ccb36b3dc61ecbdff78121f90fe21f"
+ integrity sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==
pidtree@^0.6.0:
version "0.6.0"
@@ -23061,12 +23045,12 @@ postcss@8.4.31:
picocolors "^1.0.0"
source-map-js "^1.0.2"
-postcss@^8.1.10, postcss@^8.2.14, postcss@^8.3.7, postcss@^8.4.43, postcss@^8.4.7, postcss@^8.4.8, postcss@^8.5.1, postcss@^8.5.19, postcss@^8.5.25, postcss@^8.5.3, postcss@^8.5.6:
- version "8.5.26"
- resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.26.tgz#6e75135780c7e10df3433bf2266c552d35c8c620"
- integrity sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==
+postcss@^8.1.10, postcss@^8.2.14, postcss@^8.3.7, postcss@^8.4.43, postcss@^8.4.7, postcss@^8.4.8, postcss@^8.5.1, postcss@^8.5.19, postcss@^8.5.25, postcss@^8.5.28, postcss@^8.5.3, postcss@^8.5.6:
+ version "8.5.28"
+ resolved "https://sfw.security.sentry.io/npm/postcss/-/postcss-8.5.28.tgz#da4563a99a06e62d6c1cd1acae363224bcaed6e9"
+ integrity sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==
dependencies:
- nanoid "^3.3.17"
+ nanoid "^3.3.18"
picocolors "^1.1.1"
source-map-js "^1.2.1"
@@ -24366,29 +24350,29 @@ roarr@^7.0.4:
safe-stable-stringify "^2.4.1"
semver-compare "^1.0.0"
-rolldown@^1.0.0, rolldown@^1.0.0-rc.15, rolldown@^1.0.0-rc.8:
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/rolldown/-/rolldown-1.1.2.tgz#accb41e26c872ad2c5198a39c1281c7b6b844097"
- integrity sha512-x0CrQQqCXWGeI8dTvFfN/Dnv3yMKT9hv5jFjlOreKAx9wqLq9wz7VvLLHyaAXC90/CpggTu9SisSbsJJTPSjNQ==
+rolldown@^1.0.0, rolldown@^1.0.0-rc.15, rolldown@^1.0.0-rc.8, rolldown@~1.2.9:
+ version "1.2.11"
+ resolved "https://sfw.security.sentry.io/npm/rolldown/-/rolldown-1.2.11.tgz#89c4e07d9098b88fe0b023de59d63a6216e9285c"
+ integrity sha512-qpSwIyz0jHQq5qXBTNxFmE6664rJ7O+4TvPFOiOaBSrz8IOHc1koKKSqTM2H6u1UG1+TveuC6vaDHKXFOvb1Kw==
dependencies:
- "@oxc-project/types" "=0.137.0"
+ "@oxc-project/types" "=0.151.0"
"@rolldown/pluginutils" "^1.0.0"
optionalDependencies:
- "@rolldown/binding-android-arm64" "1.1.2"
- "@rolldown/binding-darwin-arm64" "1.1.2"
- "@rolldown/binding-darwin-x64" "1.1.2"
- "@rolldown/binding-freebsd-x64" "1.1.2"
- "@rolldown/binding-linux-arm-gnueabihf" "1.1.2"
- "@rolldown/binding-linux-arm64-gnu" "1.1.2"
- "@rolldown/binding-linux-arm64-musl" "1.1.2"
- "@rolldown/binding-linux-ppc64-gnu" "1.1.2"
- "@rolldown/binding-linux-s390x-gnu" "1.1.2"
- "@rolldown/binding-linux-x64-gnu" "1.1.2"
- "@rolldown/binding-linux-x64-musl" "1.1.2"
- "@rolldown/binding-openharmony-arm64" "1.1.2"
- "@rolldown/binding-wasm32-wasi" "1.1.2"
- "@rolldown/binding-win32-arm64-msvc" "1.1.2"
- "@rolldown/binding-win32-x64-msvc" "1.1.2"
+ "@rolldown/binding-android-arm-eabi" "1.2.11"
+ "@rolldown/binding-android-arm64" "1.2.11"
+ "@rolldown/binding-darwin-arm64" "1.2.11"
+ "@rolldown/binding-darwin-x64" "1.2.11"
+ "@rolldown/binding-freebsd-x64" "1.2.11"
+ "@rolldown/binding-linux-arm-gnueabihf" "1.2.11"
+ "@rolldown/binding-linux-arm64-gnu" "1.2.11"
+ "@rolldown/binding-linux-arm64-musl" "1.2.11"
+ "@rolldown/binding-linux-ppc64-gnu" "1.2.11"
+ "@rolldown/binding-linux-s390x-gnu" "1.2.11"
+ "@rolldown/binding-linux-x64-gnu" "1.2.11"
+ "@rolldown/binding-linux-x64-musl" "1.2.11"
+ "@rolldown/binding-openharmony-arm64" "1.2.11"
+ "@rolldown/binding-win32-arm64-msvc" "1.2.11"
+ "@rolldown/binding-win32-x64-msvc" "1.2.11"
rollup-plugin-copy-assets@^2.0.3:
version "2.0.3"
@@ -24860,6 +24844,11 @@ sentry@^0.44.0:
resolved "https://sfw.security.sentry.io/npm/sentry/-/sentry-0.44.0.tgz#94d53f6cfd8df6ce1186d7a222ec38c461c1b83c"
integrity sha512-z9hagaB24WTZLKOuIdoi+XIJb2Db1k/TxPSmASvBD86H5/tqURxguLWecnraqWtDuf7ByEkUkMP7JP/mq8v89w==
+sentry@^0.45.0:
+ version "0.45.0"
+ resolved "https://registry.yarnpkg.com/sentry/-/sentry-0.45.0.tgz#0803c63b7337076f80ea384c910f9df17247dc1c"
+ integrity sha512-kgmEbLlC22lL7uneeUm9Sjz65rp5cvwYOMTxXbZCmBum4sbVre/lLT3zTV3kwXHJ9/BM8ZS0/GhZe9Cl1t6WyA==
+
serialize-javascript@^6.0.0, serialize-javascript@^6.0.1:
version "6.0.2"
resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2"
@@ -25605,14 +25594,14 @@ statuses@^2.0.1, statuses@^2.0.2, statuses@~2.0.1, statuses@~2.0.2:
resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.2.tgz#8f75eecef765b5e1cfcdc080da59409ed424e382"
integrity sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==
-std-env@^3.7.0, std-env@^3.9.0:
+std-env@^3.7.0:
version "3.10.0"
resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.10.0.tgz#d810b27e3a073047b2b5e40034881f5ea6f9c83b"
integrity sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==
-std-env@^4.0.0, std-env@^4.1.0, std-env@^4.2.0:
+std-env@^4.0.0, std-env@^4.0.0-rc.1, std-env@^4.1.0, std-env@^4.2.0:
version "4.2.0"
- resolved "https://registry.yarnpkg.com/std-env/-/std-env-4.2.0.tgz#8ebe0ec60485668ab47227b312f4254cdf80c9d3"
+ resolved "https://sfw.security.sentry.io/npm/std-env/-/std-env-4.2.0.tgz#8ebe0ec60485668ab47227b312f4254cdf80c9d3"
integrity sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==
stdin-discarder@^0.2.2:
@@ -25871,13 +25860,6 @@ strip-json-comments@~2.0.1:
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo=
-strip-literal@^3.0.0:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/strip-literal/-/strip-literal-3.1.0.tgz#222b243dd2d49c0bcd0de8906adbd84177196032"
- integrity sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==
- dependencies:
- js-tokens "^9.0.1"
-
strip-literal@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/strip-literal/-/strip-literal-4.0.0.tgz#01bb0daf7af141ce8d2328bdeecc6b1597aebc0c"
@@ -26207,15 +26189,6 @@ test-exclude@^6.0.0:
glob "^7.1.4"
minimatch "^3.0.4"
-test-exclude@^7.0.1:
- version "7.0.1"
- resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-7.0.1.tgz#20b3ba4906ac20994e275bbcafd68d510264c2a2"
- integrity sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==
- dependencies:
- "@istanbuljs/schema" "^0.1.2"
- glob "^10.4.1"
- minimatch "^9.0.4"
-
text-decoder@^1.1.0:
version "1.2.3"
resolved "https://registry.yarnpkg.com/text-decoder/-/text-decoder-1.2.3.tgz#b19da364d981b2326d5f43099c310cc80d770c65"
@@ -26308,15 +26281,15 @@ tinyclip@^0.1.15:
resolved "https://registry.yarnpkg.com/tinyclip/-/tinyclip-0.1.15.tgz#db35eaa2bd5fe627b3ef2ea38d8b0407f2bc2def"
integrity sha512-uo33abH+Ays0xYaDysoBt494Hb3hsEczMpcC0MwFl773pazORx4fmvKhclhR1wonUbB6vvpRsvVMwnhfqeMc+A==
-tinyexec@^0.3.1, tinyexec@^0.3.2:
+tinyexec@^0.3.1:
version "0.3.2"
resolved "https://registry.yarnpkg.com/tinyexec/-/tinyexec-0.3.2.tgz#941794e657a85e496577995c6eef66f53f42b3d2"
integrity sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==
-tinyexec@^1.2.4:
- version "1.3.0"
- resolved "https://registry.yarnpkg.com/tinyexec/-/tinyexec-1.3.0.tgz#aacc1dbb1d4e93e6ad8dd64944e09f9ad147a474"
- integrity sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==
+tinyexec@^1.0.2, tinyexec@^1.2.4:
+ version "1.3.1"
+ resolved "https://sfw.security.sentry.io/npm/tinyexec/-/tinyexec-1.3.1.tgz#16a2e3c6e23fafce72640e678e651db36145b9c6"
+ integrity sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==
tinyglobby@^0.2.13, tinyglobby@^0.2.14, tinyglobby@^0.2.15, tinyglobby@^0.2.16, tinyglobby@^0.2.17, tinyglobby@^0.2.2:
version "0.2.17"
@@ -26331,20 +26304,10 @@ tinypool@2.1.2:
resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-2.1.2.tgz#fcb3df44405c2530259e4bef5928a90bf46134dd"
integrity sha512-9YodfrxS9g9IbFr/KOjE5bAeJ0p61n3bW6mqvy0jtoeKd1kTW1Cxm0oulm6KX2lyM9Gl6WIe8nEbY7LWv5ZJww==
-tinypool@^1.1.1:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-1.1.1.tgz#059f2d042bd37567fbc017d3d426bdd2a2612591"
- integrity sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==
-
-tinyrainbow@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/tinyrainbow/-/tinyrainbow-2.0.0.tgz#9509b2162436315e80e3eee0fcce4474d2444294"
- integrity sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==
-
-tinyspy@^4.0.3:
- version "4.0.3"
- resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-4.0.3.tgz#d1d0f0602f4c15f1aae083a34d6d0df3363b1b52"
- integrity sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==
+tinyrainbow@^3.1.0:
+ version "3.1.1"
+ resolved "https://sfw.security.sentry.io/npm/tinyrainbow/-/tinyrainbow-3.1.1.tgz#c0168387d3d8d70b6b3c2c0936de5fee738cea20"
+ integrity sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==
tldts-core@^6.1.86:
version "6.1.86"
@@ -26890,6 +26853,11 @@ undici-types@~5.26.4:
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617"
integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
+undici-types@~6.21.0:
+ version "6.21.0"
+ resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.21.0.tgz#691d00af3909be93a7faa13be61b3a5b50ef12cb"
+ integrity sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==
+
undici-types@~7.18.0:
version "7.18.2"
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.18.2.tgz#29357a89e7b7ca4aef3bf0fd3fd0cd73884229e9"
@@ -27479,7 +27447,7 @@ vite-hot-client@^2.2.0:
resolved "https://registry.yarnpkg.com/vite-hot-client/-/vite-hot-client-2.2.0.tgz#284fa1afa63cc8781d079515884eb49d2890ac2f"
integrity sha512-76Zs9zrHbH7M7wqeyooGQKdX+yg0pQ0xuQ1PbFp4z5a0Lzn2e5IPFoCswnmqZ4GiwqB4Jo3WcDAMO9jARTJl8w==
-vite-node@3.2.4, vite-node@^3.2.2:
+vite-node@^3.2.2:
version "3.2.4"
resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-3.2.4.tgz#f3676d94c4af1e76898c162c92728bca65f7bb07"
integrity sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==
@@ -27552,20 +27520,6 @@ vite-plugin-vue-tracer@^1.4.0:
pathe "^2.0.3"
source-map-js "^1.2.1"
-vite@7.3.5:
- version "7.3.5"
- resolved "https://registry.yarnpkg.com/vite/-/vite-7.3.5.tgz#90c2d0b7b94a224e7e7dcf22d2912ff0b5291165"
- integrity sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww==
- dependencies:
- esbuild "^0.27.0"
- fdir "^6.5.0"
- picomatch "^4.0.3"
- postcss "^8.5.6"
- rollup "^4.43.0"
- tinyglobby "^0.2.15"
- optionalDependencies:
- fsevents "~2.3.3"
-
"vite@^5.0.0 || ^6.0.0 || ^7.0.0-0", vite@^7.3.1, vite@^7.3.6:
version "7.3.6"
resolved "https://registry.yarnpkg.com/vite/-/vite-7.3.6.tgz#0547a395e68d3746e9a505f1fd4469fe09b49cc4"
@@ -27591,7 +27545,20 @@ vite@^5.4.11:
optionalDependencies:
fsevents "~2.3.3"
-vite@^6.4.1, vite@^6.4.3:
+"vite@^6.0.0 || ^7.0.0 || ^8.0.0", vite@^8.3.1:
+ version "8.3.1"
+ resolved "https://sfw.security.sentry.io/npm/vite/-/vite-8.3.1.tgz#aa1099a4ccaf104a8c32492951497fa1781d9fce"
+ integrity sha512-/bvH9E9tmCXRGp2uXY3WbOldqpTwFkbha/8ANaEQ6VkxhH60KyqLwgZq6lG2y+4uT55x9+9eUHMpQ7uGnOCKjA==
+ dependencies:
+ lightningcss "^1.33.0"
+ picomatch "^4.0.7"
+ postcss "^8.5.28"
+ rolldown "~1.2.9"
+ tinyglobby "^0.2.17"
+ optionalDependencies:
+ fsevents "~2.3.3"
+
+vite@^6.4.1:
version "6.4.3"
resolved "https://registry.yarnpkg.com/vite/-/vite-6.4.3.tgz#85a164db7ce706f2a776812efa2b340f1721858e"
integrity sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==
@@ -27615,33 +27582,30 @@ vitefu@^1.0.4:
resolved "https://registry.yarnpkg.com/vitefu/-/vitefu-1.0.7.tgz#430a6b178450ee90c1ea448cf3739ce100e9c54c"
integrity sha512-eRWXLBbJjW3X5z5P5IHcSm2yYbYRPb2kQuc+oqsbAl99WB5kVsPbiiox+cymo8twTzifA6itvhr2CmjnaZZp0Q==
-vitest@^3.2.7:
- version "3.2.7"
- resolved "https://registry.yarnpkg.com/vitest/-/vitest-3.2.7.tgz#1944b6ed013a25fd26a73d18e1af92c10a57af6c"
- integrity sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==
- dependencies:
- "@types/chai" "^5.2.2"
- "@vitest/expect" "3.2.7"
- "@vitest/mocker" "3.2.7"
- "@vitest/pretty-format" "^3.2.7"
- "@vitest/runner" "3.2.7"
- "@vitest/snapshot" "3.2.7"
- "@vitest/spy" "3.2.7"
- "@vitest/utils" "3.2.7"
- chai "^5.2.0"
- debug "^4.4.1"
- expect-type "^1.2.1"
- magic-string "^0.30.17"
+vitest@^4.1.11:
+ version "4.1.11"
+ resolved "https://sfw.security.sentry.io/npm/vitest/-/vitest-4.1.11.tgz#1653c1521ae917f960d9b21877797c47dfd8bf21"
+ integrity sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==
+ dependencies:
+ "@vitest/expect" "4.1.11"
+ "@vitest/mocker" "4.1.11"
+ "@vitest/pretty-format" "4.1.11"
+ "@vitest/runner" "4.1.11"
+ "@vitest/snapshot" "4.1.11"
+ "@vitest/spy" "4.1.11"
+ "@vitest/utils" "4.1.11"
+ es-module-lexer "^2.0.0"
+ expect-type "^1.3.0"
+ magic-string "^0.30.21"
+ obug "^2.1.1"
pathe "^2.0.3"
- picomatch "^4.0.2"
- std-env "^3.9.0"
+ picomatch "^4.0.3"
+ std-env "^4.0.0-rc.1"
tinybench "^2.9.0"
- tinyexec "^0.3.2"
- tinyglobby "^0.2.14"
- tinypool "^1.1.1"
- tinyrainbow "^2.0.0"
- vite "^5.0.0 || ^6.0.0 || ^7.0.0-0"
- vite-node "3.2.4"
+ tinyexec "^1.0.2"
+ tinyglobby "^0.2.15"
+ tinyrainbow "^3.1.0"
+ vite "^6.0.0 || ^7.0.0 || ^8.0.0"
why-is-node-running "^2.3.0"
volar-service-html@~0.0.71: