Skip to content

feat(node): Add an instrumentation interface for Hono #17366

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 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: 14 additions & 0 deletions packages/node/src/integrations/tracing/hono/enums.ts
Copy link
Member

Choose a reason for hiding this comment

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

We don't use enums in our repository (also see related PR). However, I think this file can be deleted, as those enums are not used anywhere.

Copy link
Author

Choose a reason for hiding this comment

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

I have removed the enums as you pointed out.
These values will be needed when specifying span attributes, so at that time I plan to define them as constants with primitive values.
2a6cd6e

Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export enum AttributeNames {
HONO_TYPE = 'hono.type',
HONO_NAME = 'hono.name',
}

export enum HonoTypes {
MIDDLEWARE = 'middleware',
REQUEST_HANDLER = 'request_handler',
}

export enum HonoNames {
MIDDLEWARE = 'middleware',
REQUEST_HANDLER = 'request handler',
}
38 changes: 38 additions & 0 deletions packages/node/src/integrations/tracing/hono/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type { IntegrationFn } from '@sentry/core';
import { defineIntegration } from '@sentry/core';
import { generateInstrumentOnce } from '@sentry/node-core';
import { HonoInstrumentation } from './instrumentation';

const INTEGRATION_NAME = 'Hono';

export const instrumentHono = generateInstrumentOnce(
INTEGRATION_NAME,
() => new HonoInstrumentation(),
);

const _honoIntegration = (() => {
return {
name: INTEGRATION_NAME,
setupOnce() {
instrumentHono();
},
};
}) satisfies IntegrationFn;

/**
* Adds Sentry tracing instrumentation for [Hono](https://hono.dev/).
*
* If you also want to capture errors, you need to call `setupHonoErrorHandler(app)` after you set up your Hono server.
*
* For more information, see the [hono documentation](https://docs.sentry.io/platforms/javascript/guides/hono/).
*
* @example
* ```javascript
* const Sentry = require('@sentry/node');
*
* Sentry.init({
* integrations: [Sentry.honoIntegration()],
* })
* ```
*/
export const honoIntegration = defineIntegration(_honoIntegration);
88 changes: 88 additions & 0 deletions packages/node/src/integrations/tracing/hono/instrumentation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { InstrumentationBase,InstrumentationNodeModuleDefinition } from '@opentelemetry/instrumentation';
Copy link
Member

Choose a reason for hiding this comment

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

Run yarn fix to fix linting and the code formatting with prettier.

Copy link
Author

Choose a reason for hiding this comment

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

I’ve fixed it. I’ll be careful not to forget next time.
98a2c37

import type { HandlerInterface, Hono, HonoInstance, MiddlewareHandlerInterface, OnHandlerInterface } from './types';

const PACKAGE_NAME = '@sentry/instrumentation-hono';
const PACKAGE_VERSION = '0.0.1';

/**
* Hono instrumentation for OpenTelemetry
*/
export class HonoInstrumentation extends InstrumentationBase {
public constructor() {
super(PACKAGE_NAME, PACKAGE_VERSION, {});
}

/**
* Initialize the instrumentation.
*/
public init(): InstrumentationNodeModuleDefinition[] {
return [
new InstrumentationNodeModuleDefinition(
'hono',
['>=4.0.0 <5'],
moduleExports => this._patch(moduleExports),
),
];
}
Copy link

Choose a reason for hiding this comment

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

Bug: Hono Instrumentation Wrapping Issue

The HonoInstrumentation replaces moduleExports.Hono with a subclass that wraps instance methods in its constructor. As InstrumentationNodeModuleDefinition lacks an onModuleUnpatch callback, disabling the instrumentation does not restore the original Hono export. This causes new Hono instances to remain wrapped, resulting in non-reversible enable/disable behavior and potential wrapper stacking upon re-enablement.

Fix in Cursor Fix in Web


/**
* Patches the module exports to instrument Hono.
*/
private _patch(moduleExports: { Hono: Hono }): { Hono: Hono } {
Copy link
Member

Choose a reason for hiding this comment

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

Lowercase the hono variable so it's easier to differentiate between the type and the variable.

Suggested change
private _patch(moduleExports: { Hono: Hono }): { Hono: Hono } {
private _patch(moduleExports: { hono: Hono }): { hono: Hono } {

Copy link
Author

Choose a reason for hiding this comment

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

Since we are patching the class exported by hono, wouldn’t it need to be uppercase here, same as the exported class name?
https://github.com/honojs/hono/blob/main/src/hono.ts#L16

Copy link
Member

Choose a reason for hiding this comment

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

ah this makes sense, yes :)

// eslint-disable-next-line @typescript-eslint/no-this-alias
const instrumentation = this;

moduleExports.Hono = class HonoWrapper extends moduleExports.Hono {
public constructor(...args: unknown[]) {
super(...args);

instrumentation._wrap(this, 'get', instrumentation._patchHandler());
instrumentation._wrap(this, 'post', instrumentation._patchHandler());
instrumentation._wrap(this, 'put', instrumentation._patchHandler());
instrumentation._wrap(this, 'delete', instrumentation._patchHandler());
instrumentation._wrap(this, 'options', instrumentation._patchHandler());
instrumentation._wrap(this, 'patch', instrumentation._patchHandler());
instrumentation._wrap(this, 'all', instrumentation._patchHandler());
instrumentation._wrap(this, 'on', instrumentation._patchOnHandler());
instrumentation._wrap(this, 'use', instrumentation._patchMiddlewareHandler());
}
};
return moduleExports;
Copy link

Choose a reason for hiding this comment

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

Bug: Hono Instrumentation Fails on Default Imports

The Hono instrumentation only patches the named Hono export, ignoring the default export. This prevents instrumentation for applications importing Hono as a default. Additionally, the Hono class is replaced in-place without using InstrumentationBase._wrap, and instance methods are wrapped within the subclass constructor. This design prevents proper unpatching, leading to persistent instrumentation where new Hono instances remain wrapped even after the instrumentation is disabled, breaking enable/disable semantics.

Fix in Cursor Fix in Web

}

/**
* Patches the route handler to instrument it.
*/
private _patchHandler(): (original: HandlerInterface) => HandlerInterface {
return function(original: HandlerInterface) {
return function wrappedHandler(this: HonoInstance, ...args: unknown[]) {
// TODO: Add OpenTelemetry tracing logic here
return original.apply(this, args);
};
};
}

/**
* Patches the 'on' handler to instrument it.
*/
private _patchOnHandler(): (original: OnHandlerInterface) => OnHandlerInterface {
return function(original: OnHandlerInterface) {
return function wrappedHandler(this: HonoInstance, ...args: unknown[]) {
// TODO: Add OpenTelemetry tracing logic here
return original.apply(this, args);
};
};
}

/**
* Patches the middleware handler to instrument it.
*/
private _patchMiddlewareHandler(): (original: MiddlewareHandlerInterface) => MiddlewareHandlerInterface {
return function(original: MiddlewareHandlerInterface) {
return function wrappedHandler(this: HonoInstance, ...args: unknown[]) {
// TODO: Add OpenTelemetry tracing logic here
return original.apply(this, args);
};
};
}
}
50 changes: 50 additions & 0 deletions packages/node/src/integrations/tracing/hono/types.ts
Copy link
Author

Choose a reason for hiding this comment

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

I extracted and defined only the minimal types necessary for instrumentation from Hono source code.

Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Vendored from: https://github.com/honojs/hono/blob/855e5b1adbf685bf4b3e6b76573aa7cb0a108d04/src/request.ts#L30
export type HonoRequest = {
path: string;
};

// Vendored from: https://github.com/honojs/hono/blob/855e5b1adbf685bf4b3e6b76573aa7cb0a108d04/src/context.ts#L291
export type Context = {
req: HonoRequest;
};

// Vendored from: https://github.com/honojs/hono/blob/855e5b1adbf685bf4b3e6b76573aa7cb0a108d04/src/types.ts#L36C1-L36C39
export type Next = () => Promise<void>;

// Vendored from: https://github.com/honojs/hono/blob/855e5b1adbf685bf4b3e6b76573aa7cb0a108d04/src/types.ts#L73
export type Handler = (c: Context, next: Next) => Promise<Response> | Response;

// Vendored from: https://github.com/honojs/hono/blob/855e5b1adbf685bf4b3e6b76573aa7cb0a108d04/src/types.ts#L80
export type MiddlewareHandler = (c: Context, next: Next) => Promise<Response | void> | Response | void;

// Vendored from: https://github.com/honojs/hono/blob/855e5b1adbf685bf4b3e6b76573aa7cb0a108d04/src/types.ts#L109
export type HandlerInterface = {
(...handlers: (Handler | MiddlewareHandler)[]): HonoInstance;
(path: string, ...handlers: (Handler | MiddlewareHandler)[]): HonoInstance;
};

// Vendored from: https://github.com/honojs/hono/blob/855e5b1adbf685bf4b3e6b76573aa7cb0a108d04/src/types.ts#L1071
export type OnHandlerInterface = {
(method: string | string[], path: string | string[], ...handlers: (Handler | MiddlewareHandler)[]): HonoInstance;
};

// Vendored from: https://github.com/honojs/hono/blob/855e5b1adbf685bf4b3e6b76573aa7cb0a108d04/src/types.ts#L679
export type MiddlewareHandlerInterface = {
(...handlers: MiddlewareHandler[]): HonoInstance;
(path: string, ...handlers: MiddlewareHandler[]): HonoInstance;
};

// Vendored from: https://github.com/honojs/hono/blob/855e5b1adbf685bf4b3e6b76573aa7cb0a108d04/src/hono-base.ts#L99
export interface HonoInstance {
get: HandlerInterface;
post: HandlerInterface;
put: HandlerInterface;
delete: HandlerInterface;
options:HandlerInterface;
patch:HandlerInterface;
all: HandlerInterface;
on: OnHandlerInterface;
use: MiddlewareHandlerInterface;
}

export type Hono = new (...args: unknown[]) => HonoInstance;