Skip to content

Commit db7ef49

Browse files
authored
feat(tracing): export wrapHandlerWithTracing for manual handler wrapping (#1369)
1 parent 906e89d commit db7ef49

2 files changed

Lines changed: 105 additions & 0 deletions

File tree

src/tracing.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,3 +142,31 @@ export function tracingPlugin(traceOpts?: TracingPluginOptions): H3Plugin {
142142
return h3;
143143
};
144144
}
145+
146+
/**
147+
* Wraps an event handler so its execution is traced via the `h3.request`
148+
* diagnostics channel with `type: "route"`. Intended to be called once per
149+
* handler at initialization time (e.g. during codegen or module load), not
150+
* per request.
151+
*
152+
* Returns the handler unchanged when `diagnostics_channel` is unavailable
153+
* or the handler is already traced.
154+
*/
155+
export function wrapHandlerWithTracing(handler: EventHandler): EventHandler {
156+
const { tracingChannel } = globalThis.process?.getBuiltinModule?.("diagnostics_channel") ?? {};
157+
if (!tracingChannel) {
158+
return handler;
159+
}
160+
if ((handler as MaybeTracedEventHandler).__traced__) {
161+
return handler;
162+
}
163+
const channel = tracingChannel("h3.request");
164+
const wrapped: MaybeTracedEventHandler = (...args) => {
165+
return channel.tracePromise(async () => handler(...args), {
166+
event: args[0],
167+
type: "route",
168+
} satisfies TracingRequestEvent);
169+
};
170+
wrapped.__traced__ = true;
171+
return wrapped;
172+
}

test/tracing.test.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1157,3 +1157,80 @@ describe("tracing channels for H3Core instances", () => {
11571157
}
11581158
});
11591159
});
1160+
1161+
describe("wrapHandlerWithTracing", () => {
1162+
it("wraps a handler so route traces are emitted", async () => {
1163+
const listener = createTracingListener();
1164+
const { H3Core } = await import("../src/h3.ts");
1165+
const { wrapHandlerWithTracing } = await import("../src/tracing.ts");
1166+
const { H3Event } = await import("../src/event.ts");
1167+
1168+
try {
1169+
const app = new H3Core();
1170+
const routeHandler = wrapHandlerWithTracing(() => "traced response");
1171+
1172+
app["~findRoute"] = (event: any) => {
1173+
if (event.url.pathname === "/test" && event.req.method === "GET") {
1174+
return { data: { method: "GET", route: "/test", handler: routeHandler }, params: {} };
1175+
}
1176+
return undefined;
1177+
};
1178+
1179+
const request = new Request("http://localhost/test", { method: "GET" });
1180+
const event = new H3Event(request, undefined, app as any);
1181+
1182+
await app.handler(event);
1183+
await new Promise((resolve) => setTimeout(resolve, 10));
1184+
1185+
const routeEvents = listener.events.filter((e) => e.asyncStart?.data.type === "route");
1186+
expect(routeEvents.some((e) => e.asyncStart?.data.event.url.pathname === "/test")).toBe(true);
1187+
} finally {
1188+
listener.cleanup();
1189+
}
1190+
});
1191+
1192+
it("is idempotent — wrapping twice returns the same wrapper", async () => {
1193+
const { wrapHandlerWithTracing } = await import("../src/tracing.ts");
1194+
1195+
const handler = () => "ok";
1196+
const wrapped = wrapHandlerWithTracing(handler);
1197+
const doubleWrapped = wrapHandlerWithTracing(wrapped);
1198+
1199+
expect(doubleWrapped).toBe(wrapped);
1200+
expect((wrapped as any).__traced__).toBe(true);
1201+
});
1202+
1203+
it("emits exactly one trace per request on a pre-wrapped handler", async () => {
1204+
const listener = createTracingListener();
1205+
const { H3Core } = await import("../src/h3.ts");
1206+
const { wrapHandlerWithTracing } = await import("../src/tracing.ts");
1207+
const { H3Event } = await import("../src/event.ts");
1208+
1209+
try {
1210+
const app = new H3Core();
1211+
const routeHandler = wrapHandlerWithTracing(() => "ok");
1212+
1213+
app["~findRoute"] = () => ({
1214+
data: { method: "GET" as const, route: "/test", handler: routeHandler },
1215+
params: {},
1216+
});
1217+
1218+
const run = async () => {
1219+
const request = new Request("http://localhost/test", { method: "GET" });
1220+
const event = new H3Event(request, undefined, app as any);
1221+
await app.handler(event);
1222+
};
1223+
1224+
await run();
1225+
await run();
1226+
await run();
1227+
1228+
await new Promise((resolve) => setTimeout(resolve, 10));
1229+
1230+
const routeAsyncStarts = listener.events.filter((e) => e.asyncStart?.data.type === "route");
1231+
expect(routeAsyncStarts.length).toBe(3);
1232+
} finally {
1233+
listener.cleanup();
1234+
}
1235+
});
1236+
});

0 commit comments

Comments
 (0)