Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions packages/hono/src/shared/applyPatches.ts
Original file line number Diff line number Diff line change
@@ -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<E extends Env>(app: Hono<E>): 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();
}
2 changes: 2 additions & 0 deletions packages/hono/src/shared/middlewareHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
4 changes: 3 additions & 1 deletion packages/hono/src/shared/patchAppUse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<E extends Env>(app: Hono<E>): void {
app.use = new Proxy(app.use, {
Expand Down
34 changes: 16 additions & 18 deletions packages/hono/src/shared/patchRoute.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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<E extends Env>(app: Hono<E>): 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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Already patched: return
// Already patched

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm gonna apply that in the follow-up PR so we don't need another CI run

if (getOriginalFunction(honoBaseProto.route as unknown as WrappedFunction)) {
return;
}

Expand All @@ -45,18 +48,13 @@ export function patchRoute<E extends Env>(app: Hono<E>): 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 {
Expand Down
Loading
Loading