Environment
- SDK:
@sentry/hono@10.53.1, @sentry/bun@10.53.1
- Runtime: Bun
- Framework: Hono
Summary
The Bun Hono middleware does not follow the same initialization pattern as the Node Hono middleware.
For Node, @sentry/hono/node expects Sentry to be initialized separately, usually by loading an instrument.ts file with node --import. The middleware checks whether a client exists and warns if Sentry has not been initialized.
For Bun, @sentry/hono/bun's sentry(app, options) unconditionally calls init(options) internally. That conflicts with the Bun preload/instrumentation pattern, where the SDK is initialized from an instrument.ts file loaded via Bun preload before the application imports.
Documentation context
The preload setup was intentional. The Sentry Bun SDK docs describe creating an instrument.js/instrument.ts file and loading it with Bun preload, for example:
bun --preload ./instrument.ts server.ts
So this is not only an attempt to copy the Node --import pattern onto Bun. It is following the documented Bun SDK setup model where initialization happens in a separate instrumentation file before the application entrypoint is loaded.
The confusing part is that the Hono Bun middleware appears to expect middleware-owned initialization, while the Bun SDK docs point users toward preload-owned initialization.
What happens
When following the preload pattern in a Bun Hono service:
// instrument.ts
import * as Sentry from "@sentry/hono/bun";
Sentry.init({
dsn: process.env.SENTRY_DSN,
tracesSampleRate: 1.0,
});
and then mounting the Bun Hono middleware:
import * as Sentry from "@sentry/hono/bun";
import { Hono } from "hono";
const app = new Hono();
app.use("*", Sentry.sentry(app, {
dsn: process.env.SENTRY_DSN,
tracesSampleRate: 1.0,
}));
Sentry is initialized twice: once from the Bun preload file and again from the middleware.
In our service this produced duplicate OpenTelemetry registration/debug output and nested/duplicated http.server spans. For health check routes, we also saw catch-all transaction names such as GET /* or GET http://*/, which made it look like route filtering/naming was not working.
Catch-all route naming behavior
There also appears to be a route naming issue in the shared Hono request handler.
@sentry/hono/build/esm/shared/middlewareHandlers.js uses routePath(context, -1) to pick the route name:
const lastMatchedRoute = routePath(context, -1);
activeSpan.updateName(`${context.req.method} ${lastMatchedRoute}`);
updateSpanName(rootSpan, `${context.req.method} ${lastMatchedRoute}`);
In a Hono app with mounted sub-apps and wildcard middleware, routePath(context, -1) can return the last matched wildcard middleware route rather than the route that actually handled the request.
For example, a request to GET /readyz can have a matched route list shaped like:
ALL /* root middleware, including Sentry
GET /readyz actual handler
ALL /* later mounted sub-app middleware
The later mounted ALL /* route may not execute after /readyz handles the request, but it is still present in Hono's matched route list. Because Sentry uses routePath(context, -1), it picks the trailing wildcard and names the transaction GET /*.
After await next(), Hono's current route index points at the handler that produced the response. In our local workaround, using routePath(context) after next() returns the expected route (/readyz), while routePath(context, -1) can still return /*.
So the catch-all transaction names are not only a filtering issue. They can come from using the last matched route instead of the current resolved route after Hono has dispatched the handler.
Why this is surprising
The Node middleware and Bun middleware differ here:
// @sentry/hono/build/esm/node/middleware.js
const sentry = (app) => {
const sentryClient = getClient();
if (sentryClient === undefined) {
debug.warn("Sentry is not initialized...");
}
applyPatches(app);
return async (context, next) => { ... };
};
// @sentry/hono/build/esm/bun/middleware.js
const sentry = (app, options) => {
const isDebug = options.debug;
isDebug && debug.log("Initialized Sentry Hono middleware (Bun)");
init(options);
applyPatches(app);
return async (context, next) => { ... };
};
So Node supports the preload/import pattern cleanly, while Bun forces the middleware to own initialization.
Expected behavior
It would be helpful if Bun could support the same shape as Node, for example one of:
@sentry/hono/bun middleware does not call init() when Sentry is already initialized.
- A Bun middleware export exists that only installs the Hono request/response handling and app patches, assuming initialization happened in
instrument.ts.
- The Bun docs explicitly recommend either preload initialization or middleware-owned initialization, but not both, and show the correct Hono + Bun setup.
- Hono route naming uses the route that actually handled the request, rather than
routePath(context, -1), so mounted wildcard middleware does not collapse transactions into GET /*.
Workaround
We currently have to wrap the Bun middleware behavior locally:
- initialize Sentry in
instrument.ts through Bun preload
- avoid calling exported
Sentry.sentry(app, options) because it calls init() again
- import/use the shipped Hono
requestHandler, responseHandler, and app patching code from the installed package internals
- guard initialization with
if (!Sentry.isInitialized())
- after
await next(), correct the active/root span name using routePath(context) instead of routePath(context, -1)
That works, but it relies on internal package paths because the shared Hono handlers are not exported as public API.
Environment
@sentry/hono@10.53.1,@sentry/bun@10.53.1Summary
The Bun Hono middleware does not follow the same initialization pattern as the Node Hono middleware.
For Node,
@sentry/hono/nodeexpects Sentry to be initialized separately, usually by loading aninstrument.tsfile withnode --import. The middleware checks whether a client exists and warns if Sentry has not been initialized.For Bun,
@sentry/hono/bun'ssentry(app, options)unconditionally callsinit(options)internally. That conflicts with the Bun preload/instrumentation pattern, where the SDK is initialized from aninstrument.tsfile loaded via Bun preload before the application imports.Documentation context
The preload setup was intentional. The Sentry Bun SDK docs describe creating an
instrument.js/instrument.tsfile and loading it with Bun preload, for example:So this is not only an attempt to copy the Node
--importpattern onto Bun. It is following the documented Bun SDK setup model where initialization happens in a separate instrumentation file before the application entrypoint is loaded.The confusing part is that the Hono Bun middleware appears to expect middleware-owned initialization, while the Bun SDK docs point users toward preload-owned initialization.
What happens
When following the preload pattern in a Bun Hono service:
and then mounting the Bun Hono middleware:
Sentry is initialized twice: once from the Bun preload file and again from the middleware.
In our service this produced duplicate OpenTelemetry registration/debug output and nested/duplicated
http.serverspans. For health check routes, we also saw catch-all transaction names such asGET /*orGET http://*/, which made it look like route filtering/naming was not working.Catch-all route naming behavior
There also appears to be a route naming issue in the shared Hono request handler.
@sentry/hono/build/esm/shared/middlewareHandlers.jsusesroutePath(context, -1)to pick the route name:In a Hono app with mounted sub-apps and wildcard middleware,
routePath(context, -1)can return the last matched wildcard middleware route rather than the route that actually handled the request.For example, a request to
GET /readyzcan have a matched route list shaped like:The later mounted
ALL /*route may not execute after/readyzhandles the request, but it is still present in Hono's matched route list. Because Sentry usesroutePath(context, -1), it picks the trailing wildcard and names the transactionGET /*.After
await next(), Hono's current route index points at the handler that produced the response. In our local workaround, usingroutePath(context)afternext()returns the expected route (/readyz), whileroutePath(context, -1)can still return/*.So the catch-all transaction names are not only a filtering issue. They can come from using the last matched route instead of the current resolved route after Hono has dispatched the handler.
Why this is surprising
The Node middleware and Bun middleware differ here:
So Node supports the preload/import pattern cleanly, while Bun forces the middleware to own initialization.
Expected behavior
It would be helpful if Bun could support the same shape as Node, for example one of:
@sentry/hono/bunmiddleware does not callinit()when Sentry is already initialized.instrument.ts.routePath(context, -1), so mounted wildcard middleware does not collapse transactions intoGET /*.Workaround
We currently have to wrap the Bun middleware behavior locally:
instrument.tsthrough Bun preloadSentry.sentry(app, options)because it callsinit()againrequestHandler,responseHandler, and app patching code from the installed package internalsif (!Sentry.isInitialized())await next(), correct the active/root span name usingroutePath(context)instead ofroutePath(context, -1)That works, but it relies on internal package paths because the shared Hono handlers are not exported as public API.