diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0e1bac4c474d..a0b161d35104 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,19 @@
- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott
+## 10.53.1
+
+- fix(core): Don't gate user data for streamed spans at scope read time ([#20827](https://github.com/getsentry/sentry-javascript/pull/20827))
+- fix(core): Include subpath type shims in published package ([#20835](https://github.com/getsentry/sentry-javascript/pull/20835))
+- ref(hono): Consolidate route patching and add clarification comments ([#20829](https://github.com/getsentry/sentry-javascript/pull/20829))
+
+
+ Internal Changes
+
+- chore(deps): Bump next from 15.5.15 to 15.5.18 in /dev-packages/e2e-tests/test-applications/nextjs-15-intl ([#20821](https://github.com/getsentry/sentry-javascript/pull/20821))
+
+
+
## 10.53.0
### Important Changes
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15-intl/package.json b/dev-packages/e2e-tests/test-applications/nextjs-15-intl/package.json
index fc3c7f813b5f..d935f67fa39e 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-15-intl/package.json
+++ b/dev-packages/e2e-tests/test-applications/nextjs-15-intl/package.json
@@ -15,7 +15,7 @@
"@types/node": "^18.19.1",
"@types/react": "18.0.26",
"@types/react-dom": "18.0.9",
- "next": "15.5.15",
+ "next": "15.5.18",
"next-intl": "^4.3.12",
"react": "latest",
"react-dom": "latest",
diff --git a/packages/core/browser.d.ts b/packages/core/browser.d.ts
new file mode 100644
index 000000000000..752c159c689d
--- /dev/null
+++ b/packages/core/browser.d.ts
@@ -0,0 +1,4 @@
+// This file is a compatibility shim for TypeScript compilers that do not
+// support the package.json `exports` field for resolving subpath exports.
+// Note: `typesVersions` in package.json may redirect this to the downleveled variant.
+export * from './build/types/browser';
diff --git a/packages/core/package.json b/packages/core/package.json
index 2c24d45a3a1a..2aa3e941176a 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -12,7 +12,9 @@
"files": [
"/build",
"browser.js",
- "server.js"
+ "browser.d.ts",
+ "server.js",
+ "server.d.ts"
],
"main": "build/cjs/index.js",
"module": "build/esm/index.js",
@@ -54,6 +56,12 @@
"<5.0": {
"build/types/index.d.ts": [
"build/types-ts3.8/index.d.ts"
+ ],
+ "browser": [
+ "build/types-ts3.8/browser.d.ts"
+ ],
+ "server": [
+ "build/types-ts3.8/server.d.ts"
]
}
},
diff --git a/packages/core/server.d.ts b/packages/core/server.d.ts
new file mode 100644
index 000000000000..7be138fe726e
--- /dev/null
+++ b/packages/core/server.d.ts
@@ -0,0 +1,4 @@
+// This file is a compatibility shim for TypeScript compilers that do not
+// support the package.json `exports` field for resolving subpath exports.
+// Note: `typesVersions` in package.json may redirect this to the downleveled variant.
+export * from './build/types/server';
diff --git a/packages/core/src/semanticAttributes.ts b/packages/core/src/semanticAttributes.ts
index 02b6a4ec08a6..9eeed6378a5f 100644
--- a/packages/core/src/semanticAttributes.ts
+++ b/packages/core/src/semanticAttributes.ts
@@ -53,13 +53,13 @@ export const SEMANTIC_ATTRIBUTE_SENTRY_SDK_NAME = 'sentry.sdk.name';
/** The version of the Sentry SDK */
export const SEMANTIC_ATTRIBUTE_SENTRY_SDK_VERSION = 'sentry.sdk.version';
-/** The user ID (gated by sendDefaultPii) */
+/** The user ID */
export const SEMANTIC_ATTRIBUTE_USER_ID = 'user.id';
-/** The user email (gated by sendDefaultPii) */
+/** The user email */
export const SEMANTIC_ATTRIBUTE_USER_EMAIL = 'user.email';
-/** The user IP address (gated by sendDefaultPii) */
+/** The user IP address */
export const SEMANTIC_ATTRIBUTE_USER_IP_ADDRESS = 'user.ip_address';
-/** The user username (gated by sendDefaultPii) */
+/** The user username */
export const SEMANTIC_ATTRIBUTE_USER_USERNAME = 'user.name';
/**
diff --git a/packages/core/src/tracing/spans/captureSpan.ts b/packages/core/src/tracing/spans/captureSpan.ts
index bed3f1790740..afbc8ad60358 100644
--- a/packages/core/src/tracing/spans/captureSpan.ts
+++ b/packages/core/src/tracing/spans/captureSpan.ts
@@ -125,7 +125,7 @@ function applyCommonSpanAttributes(
scopeData: ScopeData,
): void {
const sdk = client.getSdkMetadata();
- const { release, environment, sendDefaultPii } = client.getOptions();
+ const { release, environment } = client.getOptions();
// avoid overwriting any previously set attributes (from users or potentially our SDK instrumentation)
safeSetSpanJSONAttributes(spanJSON, {
@@ -135,14 +135,10 @@ function applyCommonSpanAttributes(
[SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_ID]: serializedSegmentSpan.span_id,
[SEMANTIC_ATTRIBUTE_SENTRY_SDK_NAME]: sdk?.sdk?.name,
[SEMANTIC_ATTRIBUTE_SENTRY_SDK_VERSION]: sdk?.sdk?.version,
- ...(sendDefaultPii
- ? {
- [SEMANTIC_ATTRIBUTE_USER_ID]: scopeData.user?.id,
- [SEMANTIC_ATTRIBUTE_USER_EMAIL]: scopeData.user?.email,
- [SEMANTIC_ATTRIBUTE_USER_IP_ADDRESS]: scopeData.user?.ip_address,
- [SEMANTIC_ATTRIBUTE_USER_USERNAME]: scopeData.user?.username,
- }
- : {}),
+ [SEMANTIC_ATTRIBUTE_USER_ID]: scopeData.user?.id,
+ [SEMANTIC_ATTRIBUTE_USER_EMAIL]: scopeData.user?.email,
+ [SEMANTIC_ATTRIBUTE_USER_IP_ADDRESS]: scopeData.user?.ip_address,
+ [SEMANTIC_ATTRIBUTE_USER_USERNAME]: scopeData.user?.username,
...scopeData.attributes,
});
}
diff --git a/packages/core/test/lib/tracing/spans/captureSpan.test.ts b/packages/core/test/lib/tracing/spans/captureSpan.test.ts
index 186f7f23a536..524f3287e82c 100644
--- a/packages/core/test/lib/tracing/spans/captureSpan.test.ts
+++ b/packages/core/test/lib/tracing/spans/captureSpan.test.ts
@@ -25,179 +25,102 @@ import { inferSpanDataFromOtelAttributes, safeSetSpanJSONAttributes } from '../.
import { getDefaultTestClientOptions, TestClient } from '../../../mocks/client';
describe('captureSpan', () => {
- it('captures user attributes iff sendDefaultPii is true', () => {
- const client = new TestClient(
- getDefaultTestClientOptions({
- dsn: 'https://dsn@ingest.f00.f00/1',
- tracesSampleRate: 1,
- release: '1.0.0',
- environment: 'staging',
- sendDefaultPii: true,
- }),
- );
-
- const span = withScope(scope => {
- scope.setClient(client);
- scope.setUser({
- id: '123',
- email: 'user@example.com',
- username: 'testuser',
- ip_address: '127.0.0.1',
- });
-
- const span = startInactiveSpan({ name: 'my-span', attributes: { 'sentry.op': 'http.client' } });
- span.end();
-
- return span;
- });
-
- const serializedSpan = captureSpan(span, client);
+ it.each([true, false, undefined])(
+ 'always applies scope user attributes to spans (sendDefaultPii: %s)',
+ sendDefaultPii => {
+ const client = new TestClient(
+ getDefaultTestClientOptions({
+ dsn: 'https://dsn@ingest.f00.f00/1',
+ tracesSampleRate: 1,
+ release: '1.0.0',
+ environment: 'staging',
+ sendDefaultPii,
+ }),
+ );
- expect(serializedSpan).toStrictEqual({
- span_id: expect.stringMatching(/^[\da-f]{16}$/),
- trace_id: expect.stringMatching(/^[\da-f]{32}$/),
- parent_span_id: undefined,
- links: undefined,
- start_timestamp: expect.any(Number),
- name: 'my-span',
- end_timestamp: expect.any(Number),
- status: 'ok',
- is_segment: true,
- attributes: {
- [SEMANTIC_ATTRIBUTE_SENTRY_OP]: {
- type: 'string',
- value: 'http.client',
- },
- [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: {
- type: 'string',
- value: 'manual',
- },
- [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: {
- type: 'integer',
- value: 1,
- },
- [SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_NAME]: {
- value: 'my-span',
- type: 'string',
- },
- [SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_ID]: {
- value: span.spanContext().spanId,
- type: 'string',
- },
- 'sentry.span.source': {
- value: 'custom',
- type: 'string',
- },
- [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: {
- value: 'custom',
- type: 'string',
- },
- [SEMANTIC_ATTRIBUTE_SENTRY_RELEASE]: {
- value: '1.0.0',
- type: 'string',
- },
- [SEMANTIC_ATTRIBUTE_SENTRY_ENVIRONMENT]: {
- value: 'staging',
- type: 'string',
- },
- [SEMANTIC_ATTRIBUTE_USER_ID]: {
- value: '123',
- type: 'string',
- },
- [SEMANTIC_ATTRIBUTE_USER_EMAIL]: {
- value: 'user@example.com',
- type: 'string',
- },
- [SEMANTIC_ATTRIBUTE_USER_USERNAME]: {
- value: 'testuser',
- type: 'string',
- },
- [SEMANTIC_ATTRIBUTE_USER_IP_ADDRESS]: {
- value: '127.0.0.1',
- type: 'string',
- },
- },
- _segmentSpan: span,
- });
- });
+ const span = withScope(scope => {
+ scope.setClient(client);
+ scope.setUser({
+ id: '123',
+ email: 'user@example.com',
+ username: 'testuser',
+ ip_address: '127.0.0.1',
+ });
- it.each([false, undefined])("doesn't capture user attributes if sendDefaultPii is %s", sendDefaultPii => {
- const client = new TestClient(
- getDefaultTestClientOptions({
- dsn: 'https://dsn@ingest.f00.f00/1',
- tracesSampleRate: 1,
- release: '1.0.0',
- environment: 'staging',
- sendDefaultPii,
- }),
- );
+ const span = startInactiveSpan({ name: 'my-span', attributes: { 'sentry.op': 'http.client' } });
+ span.end();
- const span = withScope(scope => {
- scope.setClient(client);
- scope.setUser({
- id: '123',
- email: 'user@example.com',
- username: 'testuser',
- ip_address: '127.0.0.1',
+ return span;
});
- const span = startInactiveSpan({ name: 'my-span', attributes: { 'sentry.op': 'http.client' } });
- span.end();
-
- return span;
- });
-
- expect(captureSpan(span, client)).toStrictEqual({
- span_id: expect.stringMatching(/^[\da-f]{16}$/),
- trace_id: expect.stringMatching(/^[\da-f]{32}$/),
- parent_span_id: undefined,
- links: undefined,
- start_timestamp: expect.any(Number),
- name: 'my-span',
- end_timestamp: expect.any(Number),
- status: 'ok',
- is_segment: true,
- attributes: {
- [SEMANTIC_ATTRIBUTE_SENTRY_OP]: {
- type: 'string',
- value: 'http.client',
- },
- [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: {
- type: 'string',
- value: 'manual',
- },
- [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: {
- type: 'integer',
- value: 1,
- },
- [SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_NAME]: {
- value: 'my-span',
- type: 'string',
- },
- [SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_ID]: {
- value: span.spanContext().spanId,
- type: 'string',
- },
- 'sentry.span.source': {
- value: 'custom',
- type: 'string',
- },
- [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: {
- value: 'custom',
- type: 'string',
- },
- [SEMANTIC_ATTRIBUTE_SENTRY_RELEASE]: {
- value: '1.0.0',
- type: 'string',
- },
- [SEMANTIC_ATTRIBUTE_SENTRY_ENVIRONMENT]: {
- value: 'staging',
- type: 'string',
+ expect(captureSpan(span, client)).toStrictEqual({
+ span_id: expect.stringMatching(/^[\da-f]{16}$/),
+ trace_id: expect.stringMatching(/^[\da-f]{32}$/),
+ parent_span_id: undefined,
+ links: undefined,
+ start_timestamp: expect.any(Number),
+ name: 'my-span',
+ end_timestamp: expect.any(Number),
+ status: 'ok',
+ is_segment: true,
+ attributes: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: {
+ type: 'string',
+ value: 'http.client',
+ },
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: {
+ type: 'string',
+ value: 'manual',
+ },
+ [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: {
+ type: 'integer',
+ value: 1,
+ },
+ [SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_NAME]: {
+ value: 'my-span',
+ type: 'string',
+ },
+ [SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_ID]: {
+ value: span.spanContext().spanId,
+ type: 'string',
+ },
+ 'sentry.span.source': {
+ value: 'custom',
+ type: 'string',
+ },
+ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: {
+ value: 'custom',
+ type: 'string',
+ },
+ [SEMANTIC_ATTRIBUTE_SENTRY_RELEASE]: {
+ value: '1.0.0',
+ type: 'string',
+ },
+ [SEMANTIC_ATTRIBUTE_SENTRY_ENVIRONMENT]: {
+ value: 'staging',
+ type: 'string',
+ },
+ [SEMANTIC_ATTRIBUTE_USER_ID]: {
+ value: '123',
+ type: 'string',
+ },
+ [SEMANTIC_ATTRIBUTE_USER_EMAIL]: {
+ value: 'user@example.com',
+ type: 'string',
+ },
+ [SEMANTIC_ATTRIBUTE_USER_USERNAME]: {
+ value: 'testuser',
+ type: 'string',
+ },
+ [SEMANTIC_ATTRIBUTE_USER_IP_ADDRESS]: {
+ value: '127.0.0.1',
+ type: 'string',
+ },
},
- },
- _segmentSpan: span,
- });
- });
+ _segmentSpan: span,
+ });
+ },
+ );
it('captures sdk name and version if available', () => {
const client = new TestClient(
@@ -286,6 +209,22 @@ describe('captureSpan', () => {
value: '1.0.0',
type: 'string',
},
+ [SEMANTIC_ATTRIBUTE_USER_ID]: {
+ value: '123',
+ type: 'string',
+ },
+ [SEMANTIC_ATTRIBUTE_USER_EMAIL]: {
+ value: 'user@example.com',
+ type: 'string',
+ },
+ [SEMANTIC_ATTRIBUTE_USER_USERNAME]: {
+ value: 'testuser',
+ type: 'string',
+ },
+ [SEMANTIC_ATTRIBUTE_USER_IP_ADDRESS]: {
+ value: '127.0.0.1',
+ type: 'string',
+ },
},
_segmentSpan: span,
});
diff --git a/packages/hono/src/shared/applyPatches.ts b/packages/hono/src/shared/applyPatches.ts
index 1b694ca7cfa5..9ad70c1dd62a 100644
--- a/packages/hono/src/shared/applyPatches.ts
+++ b/packages/hono/src/shared/applyPatches.ts
@@ -1,14 +1,18 @@
import type { Env, Hono } from 'hono';
-import { patchAppUse } from '../shared/patchAppUse';
-import { patchRoute } from '../shared/patchRoute';
+import { patchAppUse } from './patchAppUse';
+import { installRouteHookOnPrototype } from './patchRoute';
/**
- * Applies necessary patches to the Hono app to ensure that Sentry can properly trace middleware and route handlers.
+ * Instruments a Hono app instance for Sentry tracing in middleware and route handlers.
+ *
+ * Two strategies are needed because Hono mixes instance fields and prototype methods:
+ * - `use` is a per-instance class field (instance own property) → must be patched on the instance
+ * - `route` is a prototype method → patched once globally, covers all instances
*/
export function applyPatches(app: Hono): void {
// `app.use` (instance own property) — wraps middleware at registration time on this instance.
patchAppUse(app);
- //`HonoBase.prototype.route` — wraps sub-app middleware at mount time so that route groups (`app.route('/prefix', subApp)`) are also instrumented.
- patchRoute(app);
+ // `route()` lives on the shared prototype and is patched once globally.
+ installRouteHookOnPrototype();
}
diff --git a/packages/hono/src/shared/middlewareHandlers.ts b/packages/hono/src/shared/middlewareHandlers.ts
index 03bb6e16da58..7f961928df49 100644
--- a/packages/hono/src/shared/middlewareHandlers.ts
+++ b/packages/hono/src/shared/middlewareHandlers.ts
@@ -43,6 +43,8 @@ export function responseHandler(context: Context): void {
function updateSpanRouteName(isolationScope: Scope, context: Context): void {
const activeSpan = getActiveSpan();
+
+ // Final matched route: https://hono.dev/docs/helpers/route#using-with-index-parameter
const lastMatchedRoute = routePath(context, -1);
if (activeSpan) {
diff --git a/packages/hono/src/shared/patchAppUse.ts b/packages/hono/src/shared/patchAppUse.ts
index c0d620692278..8a6c4f46c40a 100644
--- a/packages/hono/src/shared/patchAppUse.ts
+++ b/packages/hono/src/shared/patchAppUse.ts
@@ -2,7 +2,9 @@ import { wrapMiddlewareWithSpan } from './wrapMiddlewareSpan';
import type { Env, Hono, MiddlewareHandler } from 'hono';
/**
- * Patches the Hono app so that middleware is automatically traced as Sentry spans.
+ * Patches `app.use` (instance own property) on a Hono instance to instrument middleware at registration time.
+ *
+ * Must be per-instance because `use` is a class field, not a prototype method.
*/
export function patchAppUse(app: Hono): void {
app.use = new Proxy(app.use, {
diff --git a/packages/hono/src/shared/patchRoute.ts b/packages/hono/src/shared/patchRoute.ts
index d3f732e30793..bdc1822dad4e 100644
--- a/packages/hono/src/shared/patchRoute.ts
+++ b/packages/hono/src/shared/patchRoute.ts
@@ -1,6 +1,7 @@
import { getOriginalFunction, markFunctionWrapped } from '@sentry/core';
import type { WrappedFunction } from '@sentry/core';
-import type { Env, Hono, MiddlewareHandler } from 'hono';
+import type { Hono, MiddlewareHandler } from 'hono';
+import { Hono as HonoClass } from 'hono';
import { wrapMiddlewareWithSpan } from './wrapMiddlewareSpan';
interface HonoRoute {
@@ -15,18 +16,20 @@ interface HonoBaseProto {
}
/**
- * Patches `HonoBase.prototype.route` so that when a sub-app is mounted via `app.route('/prefix', subApp)`, its middleware handlers
- * are retroactively wrapped in Sentry spans before the parent copies them.
+ * Patches `route()` on the Hono base prototype once, globally.
*
- * `route` lives on the prototype (unlike `use` which is a class field)
+ * Wraps sub-app middleware at mount time so that `app.route('/prefix', subApp)` is traced.
+ * Idempotent: safe to call multiple times.
*/
-export function patchRoute(app: Hono): void {
- const honoBaseProto = Object.getPrototypeOf(Object.getPrototypeOf(app)) as HonoBaseProto;
+export function installRouteHookOnPrototype(): void {
+ // `route` is on the base prototype, not the concrete subclass, walk up one level
+ const honoBaseProto = Object.getPrototypeOf(HonoClass.prototype) as HonoBaseProto;
if (!honoBaseProto || typeof honoBaseProto?.route !== 'function') {
return;
}
- if (getOriginalFunction(honoBaseProto.route as WrappedFunction)) {
+ // Already patched: return
+ if (getOriginalFunction(honoBaseProto.route as unknown as WrappedFunction)) {
return;
}
@@ -45,18 +48,13 @@ export function patchRoute(app: Hono): void {
}
/**
- * Figures out which handlers in a sub-app's flat routes array are middleware (and should get a span), then wraps them.
+ * Identifies middleware handlers in a sub-app's flat routes array and wraps them in spans.
*
- * The challenge: Hono stores every handler as a plain { method, path, handler } entry. There is no "isMiddleware" flag.
- * Two heuristics identify middleware:
- *
- * 1. Position within a group. `app.get('/path', mw, handler)` produces two entries with the same method+path.
- * All but the last one must be middleware, because only middleware calls `next()` to pass control to the next handler.
- *
- * 2. Function arity (# of params) for method 'ALL'. Both `.use()` and `.all()` store their handlers under method 'ALL',
- * so we can't use position alone to tell them apart when one is the last (or only) entry in its group.
- * The deciding factor: Hono's `.use()` only accepts `(context, next)` (handlers with 2+ params). While `.all()` route
- * handlers typically only accept `(context)`.
+ * Heuristics (since Hono has no "isMiddleware" flag):
+ * 1. Position: `app.get('/path', mw, handler)` produces entries with the same method+path.
+ * All but the LAST are middleware (they call `next()`).
+ * 2. Arity (# of params) for method 'ALL': `.use()` handlers always have 2+ params (context, next),
+ * while `.all()` route handlers typically have 1 (`context` only).
* See: https://github.com/honojs/hono/blob/18fe604c8cefc2628240651b1af219692e1918c1/src/hono-base.ts#L156-L168
*/
export function wrapSubAppMiddleware(routes: HonoRoute[]): void {
diff --git a/packages/hono/test/shared/applyPatches.test.ts b/packages/hono/test/shared/applyPatches.test.ts
new file mode 100644
index 000000000000..f5ea5a27cbc6
--- /dev/null
+++ b/packages/hono/test/shared/applyPatches.test.ts
@@ -0,0 +1,394 @@
+import * as SentryCore from '@sentry/core';
+import { Hono } from 'hono';
+import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest';
+import { applyPatches } from '../../src/shared/applyPatches';
+
+vi.mock('@sentry/core', async () => {
+ const actual = await vi.importActual('@sentry/core');
+ return {
+ ...actual,
+ startInactiveSpan: vi.fn((_opts: unknown) => ({
+ setStatus: vi.fn(),
+ end: vi.fn(),
+ })),
+ };
+});
+
+const startInactiveSpanMock = SentryCore.startInactiveSpan as ReturnType;
+
+const honoBaseProto = Object.getPrototypeOf(Object.getPrototypeOf(new Hono()));
+const originalRoute = honoBaseProto.route;
+
+describe('applyPatches', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ honoBaseProto.route = originalRoute;
+ });
+
+ afterAll(() => {
+ honoBaseProto.route = originalRoute;
+ });
+
+ describe('wrapSubAppMiddleware', () => {
+ it('does nothing when a sub-app has an empty routes array', async () => {
+ const app = new Hono();
+ applyPatches(app);
+
+ const emptySubApp = new Hono();
+ app.route('/empty', emptySubApp);
+
+ const res = await app.fetch(new Request('http://localhost/empty'));
+ expect(res.status).toBe(404);
+ expect(startInactiveSpanMock).not.toHaveBeenCalled();
+ });
+
+ it('skips route entries whose handler is not a function', async () => {
+ const app = new Hono();
+ applyPatches(app);
+
+ const subApp = new Hono();
+ subApp.get('/resource', () => new Response('ok'));
+
+ (subApp.routes as unknown as Array<{ handler: unknown }>)[0]!.handler = 'not-a-function';
+
+ expect(() => app.route('/api', subApp)).not.toThrow();
+ expect(startInactiveSpanMock).not.toHaveBeenCalled();
+ });
+
+ it('treats same path with different HTTP methods as separate groups', async () => {
+ const app = new Hono();
+ applyPatches(app);
+
+ const subApp = new Hono();
+ subApp.get('/resource', async function getHandler() {
+ return new Response('get');
+ });
+ subApp.post('/resource', async function postHandler() {
+ return new Response('post');
+ });
+
+ app.route('/api', subApp);
+
+ await app.fetch(new Request('http://localhost/api/resource', { method: 'GET' }));
+ await app.fetch(new Request('http://localhost/api/resource', { method: 'POST' }));
+
+ expect(startInactiveSpanMock).not.toHaveBeenCalled();
+ });
+
+ it('treats same HTTP method with different paths as separate groups', async () => {
+ const app = new Hono();
+ applyPatches(app);
+
+ const subApp = new Hono();
+ subApp.get('/alpha', async function alphaHandler() {
+ return new Response('alpha');
+ });
+ subApp.get('/beta', async function betaHandler() {
+ return new Response('beta');
+ });
+
+ app.route('/api', subApp);
+
+ await app.fetch(new Request('http://localhost/api/alpha'));
+ await app.fetch(new Request('http://localhost/api/beta'));
+
+ expect(startInactiveSpanMock).not.toHaveBeenCalled();
+ });
+
+ it('wraps inline middleware for GET /alpha but not the sole handler for GET /beta', async () => {
+ const app = new Hono();
+ applyPatches(app);
+
+ const subApp = new Hono();
+ subApp.get(
+ '/alpha',
+ async function alphaMw(_c: unknown, next: () => Promise) {
+ await next();
+ },
+ async function alphaHandler() {
+ return new Response('alpha');
+ },
+ );
+ subApp.get('/beta', async function betaHandler() {
+ return new Response('beta');
+ });
+
+ app.route('/api', subApp);
+
+ await app.fetch(new Request('http://localhost/api/alpha'));
+ await app.fetch(new Request('http://localhost/api/beta'));
+
+ const spanNames = startInactiveSpanMock.mock.calls.map((c: unknown[]) => (c[0] as { name: string }).name);
+ expect(spanNames).toHaveLength(1);
+ expect(spanNames).toContain('alphaMw');
+ expect(spanNames).not.toContain('alphaHandler');
+ expect(spanNames).not.toContain('betaHandler');
+ });
+ });
+
+ describe('route() patching', () => {
+ it('wraps middleware on sub-apps mounted via route()', async () => {
+ const app = new Hono();
+ applyPatches(app);
+
+ const subApp = new Hono();
+ subApp.use(async function subMiddleware(_c: unknown, next: () => Promise) {
+ await next();
+ });
+ subApp.get('/', () => new Response('sub'));
+
+ app.route('/sub', subApp);
+
+ await app.fetch(new Request('http://localhost/sub'));
+
+ expect(startInactiveSpanMock).toHaveBeenCalledWith(expect.objectContaining({ name: 'subMiddleware' }));
+ });
+
+ it('does not wrap sole route handlers on sub-apps', async () => {
+ const app = new Hono();
+ applyPatches(app);
+
+ const subApp = new Hono();
+ subApp.get('/', () => new Response('sub'));
+
+ app.route('/sub', subApp);
+
+ await app.fetch(new Request('http://localhost/sub'));
+
+ expect(startInactiveSpanMock).not.toHaveBeenCalled();
+ });
+
+ it('does not double-wrap handlers already wrapped by patchAppUse on the main app', async () => {
+ const app = new Hono();
+ applyPatches(app);
+
+ app.use(async function mainMiddleware(_c: unknown, next: () => Promise) {
+ await next();
+ });
+ app.get('/', () => new Response('ok'));
+
+ const parent = new Hono();
+ parent.route('/', app);
+
+ await parent.fetch(new Request('http://localhost/'));
+
+ expect(startInactiveSpanMock).toHaveBeenCalledTimes(1);
+ expect(startInactiveSpanMock).toHaveBeenCalledWith(expect.objectContaining({ name: 'mainMiddleware' }));
+ });
+
+ it('does not patch route() twice when applyPatches is called multiple times', () => {
+ const app1 = new Hono();
+ applyPatches(app1);
+
+ const patchedRoute = honoBaseProto.route;
+
+ const app2 = new Hono();
+ applyPatches(app2);
+
+ expect(honoBaseProto.route).toBe(patchedRoute);
+ });
+
+ it('stores the original route via __sentry_original__ for other libraries to unwrap', () => {
+ const app = new Hono();
+ applyPatches(app);
+
+ // oxlint-disable-next-line typescript/no-explicit-any
+ const sentryOriginal = (honoBaseProto.route as any).__sentry_original__;
+ expect(sentryOriginal).toBe(originalRoute);
+ });
+
+ it('wraps path-targeted .use("/path", handler) on sub-apps', async () => {
+ const app = new Hono();
+ applyPatches(app);
+
+ const subApp = new Hono();
+ subApp.use('/admin/*', async function adminAuth(_c: unknown, next: () => Promise) {
+ await next();
+ });
+ subApp.get('/admin/dashboard', () => new Response('dashboard'));
+
+ app.route('/api', subApp);
+ await app.fetch(new Request('http://localhost/api/admin/dashboard'));
+
+ expect(startInactiveSpanMock).toHaveBeenCalledWith(expect.objectContaining({ name: 'adminAuth' }));
+ });
+
+ it('does not wrap .all() handlers with less than 2 params (they are route handlers, not middleware)', async () => {
+ const app = new Hono();
+ applyPatches(app);
+
+ const subApp = new Hono();
+ subApp.all('/catch-all', async function allHandler() {
+ return new Response('catch-all');
+ });
+
+ app.route('/api', subApp);
+ await app.fetch(new Request('http://localhost/api/catch-all'));
+
+ expect(startInactiveSpanMock).not.toHaveBeenCalled();
+ });
+
+ it('wraps .use() middleware but not .all() handlers on the same sub-app', async () => {
+ const app = new Hono();
+ applyPatches(app);
+
+ const subApp = new Hono();
+ subApp.use(async function mw(_c: unknown, next: () => Promise) {
+ await next();
+ });
+ subApp.all('/wildcard', async function allRoute() {
+ return new Response('wildcard');
+ });
+ subApp.get('/specific', () => new Response('specific'));
+
+ app.route('/mixed', subApp);
+ await app.fetch(new Request('http://localhost/mixed/wildcard'));
+
+ const spanNames = startInactiveSpanMock.mock.calls.map((c: unknown[]) => (c[0] as { name: string }).name);
+ expect(spanNames).toContain('mw');
+ expect(spanNames).not.toContain('allRoute');
+ });
+
+ it('does not wrap sole .get()/.post()/.put()/.delete() handlers on sub-apps', async () => {
+ const app = new Hono();
+ applyPatches(app);
+
+ const subApp = new Hono();
+ subApp.get('/resource', async function getHandler() {
+ return new Response('get');
+ });
+ subApp.post('/resource', async function postHandler() {
+ return new Response('post');
+ });
+ subApp.put('/resource', async function postHandler() {
+ return new Response('put');
+ });
+ subApp.delete('/resource', async function postHandler() {
+ return new Response('delete');
+ });
+
+ app.route('/api', subApp);
+ await app.fetch(new Request('http://localhost/api/resource'));
+
+ expect(startInactiveSpanMock).not.toHaveBeenCalled();
+ });
+
+ it('wraps inline middleware in .get(path, mw, handler) on sub-apps', async () => {
+ const app = new Hono();
+ applyPatches(app);
+
+ const subApp = new Hono();
+ subApp.get(
+ '/resource',
+ async function inlineMw(_c: unknown, next: () => Promise) {
+ await next();
+ },
+ async function getHandler() {
+ return new Response('get');
+ },
+ );
+
+ app.route('/api', subApp);
+ await app.fetch(new Request('http://localhost/api/resource'));
+
+ const spanNames = startInactiveSpanMock.mock.calls.map((c: unknown[]) => (c[0] as { name: string }).name);
+ expect(spanNames).toContain('inlineMw');
+ expect(spanNames).not.toContain('getHandler');
+ });
+
+ it('wraps separately registered middleware for .get() on sub-apps', async () => {
+ const app = new Hono();
+ applyPatches(app);
+
+ const subApp = new Hono();
+ subApp.get('/resource', async function separateMw(_c: unknown, next: () => Promise) {
+ await next();
+ });
+ subApp.get('/resource', async function getHandler() {
+ return new Response('get');
+ });
+
+ app.route('/api', subApp);
+ await app.fetch(new Request('http://localhost/api/resource'));
+
+ const spanNames = startInactiveSpanMock.mock.calls.map((c: unknown[]) => (c[0] as { name: string }).name);
+ expect(spanNames).toContain('separateMw');
+ expect(spanNames).not.toContain('getHandler');
+ });
+
+ it('wraps inline middleware registered via .on() on sub-apps', async () => {
+ const app = new Hono();
+ applyPatches(app);
+
+ const subApp = new Hono();
+ subApp.on(
+ 'GET',
+ '/resource',
+ async function onMw(_c: unknown, next: () => Promise) {
+ await next();
+ },
+ async function onHandler() {
+ return new Response('on');
+ },
+ );
+
+ app.route('/api', subApp);
+ await app.fetch(new Request('http://localhost/api/resource'));
+
+ const spanNames = startInactiveSpanMock.mock.calls.map((c: unknown[]) => (c[0] as { name: string }).name);
+ expect(spanNames).toContain('onMw');
+ expect(spanNames).not.toContain('onHandler');
+ });
+
+ it('wraps middleware in nested sub-apps (sub-app mounting another sub-app)', async () => {
+ const app = new Hono();
+ applyPatches(app);
+
+ const innerSub = new Hono();
+ innerSub.use(async function innerMiddleware(_c: unknown, next: () => Promise) {
+ await next();
+ });
+ innerSub.get('/', () => new Response('inner'));
+
+ const outerSub = new Hono();
+ outerSub.use(async function outerMiddleware(_c: unknown, next: () => Promise) {
+ await next();
+ });
+ outerSub.route('/inner', innerSub);
+
+ app.route('/outer', outerSub);
+ await app.fetch(new Request('http://localhost/outer/inner'));
+
+ const spanNames = startInactiveSpanMock.mock.calls.map((c: unknown[]) => (c[0] as { name: string }).name);
+ expect(spanNames).toContain('outerMiddleware');
+ expect(spanNames).toContain('innerMiddleware');
+ });
+
+ it('handles sub-app with multiple path-targeted middleware for different paths', async () => {
+ const app = new Hono();
+ applyPatches(app);
+
+ const subApp = new Hono();
+ subApp.use('/a/*', async function mwForA(_c: unknown, next: () => Promise) {
+ await next();
+ });
+ subApp.use('/b/*', async function mwForB(_c: unknown, next: () => Promise) {
+ await next();
+ });
+ subApp.get('/a/test', () => new Response('a'));
+ subApp.get('/b/test', () => new Response('b'));
+
+ app.route('/sub', subApp);
+
+ await app.fetch(new Request('http://localhost/sub/a/test'));
+ expect(startInactiveSpanMock).toHaveBeenCalledTimes(1);
+ expect(startInactiveSpanMock).toHaveBeenCalledWith(expect.objectContaining({ name: 'mwForA' }));
+
+ startInactiveSpanMock.mockClear();
+
+ await app.fetch(new Request('http://localhost/sub/b/test'));
+ expect(startInactiveSpanMock).toHaveBeenCalledTimes(1);
+ expect(startInactiveSpanMock).toHaveBeenCalledWith(expect.objectContaining({ name: 'mwForB' }));
+ });
+ });
+});
diff --git a/packages/hono/test/shared/patchAppUse.test.ts b/packages/hono/test/shared/patchAppUse.test.ts
index ee376127baaa..a65c9c526a83 100644
--- a/packages/hono/test/shared/patchAppUse.test.ts
+++ b/packages/hono/test/shared/patchAppUse.test.ts
@@ -1,8 +1,7 @@
import * as SentryCore from '@sentry/core';
import { Hono } from 'hono';
-import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
import { patchAppUse } from '../../src/shared/patchAppUse';
-import { patchRoute } from '../../src/shared/patchRoute';
vi.mock('@sentry/core', async () => {
const actual = await vi.importActual('@sentry/core');
@@ -19,18 +18,11 @@ vi.mock('@sentry/core', async () => {
const startInactiveSpanMock = SentryCore.startInactiveSpan as ReturnType;
const captureExceptionMock = SentryCore.captureException as ReturnType;
-const honoBaseProto = Object.getPrototypeOf(Object.getPrototypeOf(new Hono()));
-const originalRoute = honoBaseProto.route;
-
describe('patchAppUse (middleware spans)', () => {
beforeEach(() => {
vi.clearAllMocks();
});
- afterAll(() => {
- honoBaseProto.route = originalRoute;
- });
-
it('wraps handlers in app.use(handler) so startInactiveSpan is called when middleware runs', async () => {
const app = new Hono();
patchAppUse(app);
@@ -84,7 +76,7 @@ describe('patchAppUse (middleware spans)', () => {
await app.fetch(new Request('http://localhost/'));
expect(startInactiveSpanMock).toHaveBeenCalledTimes(1);
- const name = startInactiveSpanMock.mock.calls[0][0].name;
+ const name = startInactiveSpanMock.mock.calls[0]![0].name;
expect(name).toMatch('');
});
});
@@ -135,12 +127,12 @@ describe('patchAppUse (middleware spans)', () => {
expect(startInactiveSpanMock).toHaveBeenCalledTimes(3);
const [firstCall, secondCall, thirdCall] = startInactiveSpanMock.mock.calls;
- expect(firstCall[0]).toMatchObject({ op: 'middleware.hono' });
- expect(secondCall[0]).toMatchObject({ op: 'middleware.hono' });
- expect(firstCall[0].name).toMatch('');
- expect(secondCall[0].name).toBe('namedMiddleware');
- expect(thirdCall[0].name).toBe('');
- expect(firstCall[0].name).not.toBe(secondCall[0].name);
+ expect(firstCall![0]).toMatchObject({ op: 'middleware.hono' });
+ expect(secondCall![0]).toMatchObject({ op: 'middleware.hono' });
+ expect(firstCall![0].name).toMatch('');
+ expect(secondCall![0].name).toBe('namedMiddleware');
+ expect(thirdCall![0].name).toBe('');
+ expect(firstCall![0].name).not.toBe(secondCall![0].name);
});
it('preserves this context when calling the original use (Proxy forwards thisArg)', () => {
@@ -163,289 +155,4 @@ describe('patchAppUse (middleware spans)', () => {
expect(fakeApp._capturedThis).toBe(fakeApp);
});
-
- describe('route() patching (sub-app / route group support)', () => {
- beforeEach(() => {
- honoBaseProto.route = originalRoute;
- });
-
- it('wraps middleware on sub-apps mounted via route()', async () => {
- const app = new Hono();
- patchAppUse(app);
- patchRoute(app);
-
- const subApp = new Hono();
- subApp.use(async function subMiddleware(_c: unknown, next: () => Promise) {
- await next();
- });
- subApp.get('/', () => new Response('sub'));
-
- app.route('/sub', subApp);
-
- await app.fetch(new Request('http://localhost/sub'));
-
- expect(startInactiveSpanMock).toHaveBeenCalledWith(expect.objectContaining({ name: 'subMiddleware' }));
- });
-
- it('does not wrap sole route handlers on sub-apps', async () => {
- const app = new Hono();
- patchAppUse(app);
- patchRoute(app);
-
- const subApp = new Hono();
- subApp.get('/', () => new Response('sub'));
-
- app.route('/sub', subApp);
-
- await app.fetch(new Request('http://localhost/sub'));
-
- expect(startInactiveSpanMock).not.toHaveBeenCalled();
- });
-
- it('does not double-wrap handlers already wrapped by patchAppUse on the main app', async () => {
- const app = new Hono();
- patchAppUse(app);
- patchRoute(app);
-
- app.use(async function mainMiddleware(_c: unknown, next: () => Promise) {
- await next();
- });
- app.get('/', () => new Response('ok'));
-
- // Mount the main app as a sub-app of another app (contrived but tests the guard)
- const parent = new Hono();
- parent.route('/', app);
-
- await parent.fetch(new Request('http://localhost/'));
-
- expect(startInactiveSpanMock).toHaveBeenCalledTimes(1);
- expect(startInactiveSpanMock).toHaveBeenCalledWith(expect.objectContaining({ name: 'mainMiddleware' }));
- });
-
- it('does not patch route() twice when patchRoute is called multiple times', () => {
- const app1 = new Hono();
- patchRoute(app1);
-
- const patchedRoute = honoBaseProto.route;
-
- const app2 = new Hono();
- patchRoute(app2);
-
- expect(honoBaseProto.route).toBe(patchedRoute);
- });
-
- it('stores the original route via __sentry_original__ for other libraries to unwrap', () => {
- const app = new Hono();
- patchRoute(app);
-
- // oxlint-disable-next-line typescript/no-explicit-any
- const sentryOriginal = (honoBaseProto.route as any).__sentry_original__;
- expect(sentryOriginal).toBe(originalRoute);
- });
-
- it('wraps path-targeted .use("/path", handler) on sub-apps', async () => {
- const app = new Hono();
- patchAppUse(app);
- patchRoute(app);
-
- const subApp = new Hono();
- subApp.use('/admin/*', async function adminAuth(_c: unknown, next: () => Promise) {
- await next();
- });
- subApp.get('/admin/dashboard', () => new Response('dashboard'));
-
- app.route('/api', subApp);
- await app.fetch(new Request('http://localhost/api/admin/dashboard'));
-
- expect(startInactiveSpanMock).toHaveBeenCalledWith(expect.objectContaining({ name: 'adminAuth' }));
- });
-
- it('does not wrap .all() handlers with less than 2 params (they are route handlers, not middleware)', async () => {
- const app = new Hono();
- patchAppUse(app);
- patchRoute(app);
-
- const subApp = new Hono();
- subApp.all('/catch-all', async function allHandler() {
- return new Response('catch-all');
- });
-
- app.route('/api', subApp);
- await app.fetch(new Request('http://localhost/api/catch-all'));
-
- expect(startInactiveSpanMock).not.toHaveBeenCalled();
- });
-
- it('wraps .use() middleware but not .all() handlers on the same sub-app', async () => {
- const app = new Hono();
- patchAppUse(app);
- patchRoute(app);
-
- const subApp = new Hono();
- subApp.use(async function mw(_c: unknown, next: () => Promise) {
- await next();
- });
- subApp.all('/wildcard', async function allRoute() {
- return new Response('wildcard');
- });
- subApp.get('/specific', () => new Response('specific'));
-
- app.route('/mixed', subApp);
- await app.fetch(new Request('http://localhost/mixed/wildcard'));
-
- const spanNames = startInactiveSpanMock.mock.calls.map((c: unknown[]) => (c[0] as { name: string }).name);
- expect(spanNames).toContain('mw');
- expect(spanNames).not.toContain('allRoute');
- });
-
- it('does not wrap sole .get()/.post()/.put()/.delete() handlers on sub-apps (they are final handlers, not middleware)', async () => {
- const app = new Hono();
- patchAppUse(app);
- patchRoute(app);
-
- const subApp = new Hono();
- subApp.get('/resource', async function getHandler() {
- return new Response('get');
- });
- subApp.post('/resource', async function postHandler() {
- return new Response('post');
- });
- subApp.put('/resource', async function postHandler() {
- return new Response('put');
- });
- subApp.delete('/resource', async function postHandler() {
- return new Response('delete');
- });
-
- app.route('/api', subApp);
- await app.fetch(new Request('http://localhost/api/resource'));
-
- expect(startInactiveSpanMock).not.toHaveBeenCalled();
- });
-
- it('wraps inline middleware in .get(path, mw, handler) on sub-apps', async () => {
- const app = new Hono();
- patchAppUse(app);
- patchRoute(app);
-
- const subApp = new Hono();
- subApp.get(
- '/resource',
- async function inlineMw(_c: unknown, next: () => Promise) {
- await next();
- },
- async function getHandler() {
- return new Response('get');
- },
- );
-
- app.route('/api', subApp);
- await app.fetch(new Request('http://localhost/api/resource'));
-
- const spanNames = startInactiveSpanMock.mock.calls.map((c: unknown[]) => (c[0] as { name: string }).name);
- expect(spanNames).toContain('inlineMw');
- expect(spanNames).not.toContain('getHandler');
- });
-
- it('wraps separately registered middleware for .get() on sub-apps', async () => {
- const app = new Hono();
- patchAppUse(app);
- patchRoute(app);
-
- const subApp = new Hono();
- subApp.get('/resource', async function separateMw(_c: unknown, next: () => Promise) {
- await next();
- });
- subApp.get('/resource', async function getHandler() {
- return new Response('get');
- });
-
- app.route('/api', subApp);
- await app.fetch(new Request('http://localhost/api/resource'));
-
- const spanNames = startInactiveSpanMock.mock.calls.map((c: unknown[]) => (c[0] as { name: string }).name);
- expect(spanNames).toContain('separateMw');
- expect(spanNames).not.toContain('getHandler');
- });
-
- it('wraps inline middleware registered via .on() on sub-apps', async () => {
- const app = new Hono();
- patchAppUse(app);
- patchRoute(app);
-
- const subApp = new Hono();
- subApp.on(
- 'GET',
- '/resource',
- async function onMw(_c: unknown, next: () => Promise) {
- await next();
- },
- async function onHandler() {
- return new Response('on');
- },
- );
-
- app.route('/api', subApp);
- await app.fetch(new Request('http://localhost/api/resource'));
-
- const spanNames = startInactiveSpanMock.mock.calls.map((c: unknown[]) => (c[0] as { name: string }).name);
- expect(spanNames).toContain('onMw');
- expect(spanNames).not.toContain('onHandler');
- });
-
- it('wraps middleware in nested sub-apps (sub-app mounting another sub-app)', async () => {
- const app = new Hono();
- patchAppUse(app);
- patchRoute(app);
-
- const innerSub = new Hono();
- innerSub.use(async function innerMiddleware(_c: unknown, next: () => Promise) {
- await next();
- });
- innerSub.get('/', () => new Response('inner'));
-
- const outerSub = new Hono();
- outerSub.use(async function outerMiddleware(_c: unknown, next: () => Promise) {
- await next();
- });
- outerSub.route('/inner', innerSub);
-
- app.route('/outer', outerSub);
- await app.fetch(new Request('http://localhost/outer/inner'));
-
- const spanNames = startInactiveSpanMock.mock.calls.map((c: unknown[]) => (c[0] as { name: string }).name);
- expect(spanNames).toContain('outerMiddleware');
- expect(spanNames).toContain('innerMiddleware');
- });
-
- it('handles sub-app with multiple path-targeted middleware for different paths', async () => {
- const app = new Hono();
- patchAppUse(app);
- patchRoute(app);
-
- const subApp = new Hono();
- subApp.use('/a/*', async function mwForA(_c: unknown, next: () => Promise) {
- await next();
- });
- subApp.use('/b/*', async function mwForB(_c: unknown, next: () => Promise) {
- await next();
- });
- subApp.get('/a/test', () => new Response('a'));
- subApp.get('/b/test', () => new Response('b'));
-
- app.route('/sub', subApp);
-
- // Hit path /a — only mwForA should fire
- await app.fetch(new Request('http://localhost/sub/a/test'));
- expect(startInactiveSpanMock).toHaveBeenCalledTimes(1);
- expect(startInactiveSpanMock).toHaveBeenCalledWith(expect.objectContaining({ name: 'mwForA' }));
-
- startInactiveSpanMock.mockClear();
-
- // Hit path /b — only mwForB should fire
- await app.fetch(new Request('http://localhost/sub/b/test'));
- expect(startInactiveSpanMock).toHaveBeenCalledTimes(1);
- expect(startInactiveSpanMock).toHaveBeenCalledWith(expect.objectContaining({ name: 'mwForB' }));
- });
- });
});
diff --git a/packages/hono/test/shared/patchRoute.test.ts b/packages/hono/test/shared/patchRoute.test.ts
deleted file mode 100644
index d9dd4d6795ad..000000000000
--- a/packages/hono/test/shared/patchRoute.test.ts
+++ /dev/null
@@ -1,141 +0,0 @@
-import * as SentryCore from '@sentry/core';
-import { Hono } from 'hono';
-import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest';
-import { patchRoute } from '../../src/shared/patchRoute';
-
-vi.mock('@sentry/core', async () => {
- const actual = await vi.importActual('@sentry/core');
- return {
- ...actual,
- startInactiveSpan: vi.fn((_opts: unknown) => ({
- setStatus: vi.fn(),
- end: vi.fn(),
- })),
- };
-});
-
-const startInactiveSpanMock = SentryCore.startInactiveSpan as ReturnType;
-
-const honoBaseProto = Object.getPrototypeOf(Object.getPrototypeOf(new Hono()));
-const originalRoute = honoBaseProto.route;
-
-describe('patchRoute', () => {
- beforeEach(() => {
- vi.clearAllMocks();
- honoBaseProto.route = originalRoute;
- });
-
- afterAll(() => {
- honoBaseProto.route = originalRoute;
- });
-
- it('is a no-op when honoBaseProto.route is not a function', () => {
- const fakeApp = Object.create({ notRoute: () => {} }) as Hono;
- // Should not throw even when the expected method shape is missing
- expect(() => patchRoute(fakeApp)).not.toThrow();
- expect(honoBaseProto.route).toBe(originalRoute);
- });
-
- describe('wrapSubAppMiddleware', () => {
- it('does nothing when a sub-app has an empty routes array', async () => {
- const app = new Hono();
- patchRoute(app);
-
- const emptySubApp = new Hono();
- // routes is an empty array — nothing to wrap, nothing should throw
- app.route('/empty', emptySubApp);
-
- const res = await app.fetch(new Request('http://localhost/empty'));
- expect(res.status).toBe(404);
- expect(startInactiveSpanMock).not.toHaveBeenCalled();
- });
-
- it('skips route entries whose handler is not a function', async () => {
- const app = new Hono();
- patchRoute(app);
-
- const subApp = new Hono();
- subApp.get('/resource', () => new Response('ok'));
-
- // Corrupt one handler to a non-function to simulate unexpected route shapes
- (subApp.routes as unknown as Array<{ handler: unknown }>)[0]!.handler = 'not-a-function';
-
- // Should not throw when iterating over the corrupted routes
- expect(() => app.route('/api', subApp)).not.toThrow();
- expect(startInactiveSpanMock).not.toHaveBeenCalled();
- });
-
- it('treats same path with different HTTP methods as separate groups', async () => {
- const app = new Hono();
- patchRoute(app);
-
- const subApp = new Hono();
- // Each of these is the sole (last) handler for its method+path group,
- // so none should be wrapped as middleware.
- subApp.get('/resource', async function getHandler() {
- return new Response('get');
- });
- subApp.post('/resource', async function postHandler() {
- return new Response('post');
- });
-
- app.route('/api', subApp);
-
- await app.fetch(new Request('http://localhost/api/resource', { method: 'GET' }));
- await app.fetch(new Request('http://localhost/api/resource', { method: 'POST' }));
-
- expect(startInactiveSpanMock).not.toHaveBeenCalled();
- });
-
- it('treats same HTTP method with different paths as separate groups', async () => {
- const app = new Hono();
- patchRoute(app);
-
- const subApp = new Hono();
- // Each is the sole handler for its own method+path group — neither is middleware.
- subApp.get('/alpha', async function alphaHandler() {
- return new Response('alpha');
- });
- subApp.get('/beta', async function betaHandler() {
- return new Response('beta');
- });
-
- app.route('/api', subApp);
-
- await app.fetch(new Request('http://localhost/api/alpha'));
- await app.fetch(new Request('http://localhost/api/beta'));
-
- expect(startInactiveSpanMock).not.toHaveBeenCalled();
- });
-
- it('wraps inline middleware for GET /alpha but not the sole handler for GET /beta', async () => {
- const app = new Hono();
- patchRoute(app);
-
- const subApp = new Hono();
- subApp.get(
- '/alpha',
- async function alphaMw(_c: unknown, next: () => Promise) {
- await next();
- },
- async function alphaHandler() {
- return new Response('alpha');
- },
- );
- subApp.get('/beta', async function betaHandler() {
- return new Response('beta');
- });
-
- app.route('/api', subApp);
-
- await app.fetch(new Request('http://localhost/api/alpha'));
- await app.fetch(new Request('http://localhost/api/beta'));
-
- const spanNames = startInactiveSpanMock.mock.calls.map((c: unknown[]) => (c[0] as { name: string }).name);
- expect(spanNames).toHaveLength(1);
- expect(spanNames).toContain('alphaMw');
- expect(spanNames).not.toContain('alphaHandler');
- expect(spanNames).not.toContain('betaHandler');
- });
- });
-});