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
28 changes: 28 additions & 0 deletions src/tracing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,3 +142,31 @@ export function tracingPlugin(traceOpts?: TracingPluginOptions): H3Plugin {
return h3;
};
}

/**
* Wraps an event handler so its execution is traced via the `h3.request`
* diagnostics channel with `type: "route"`. Intended to be called once per
* handler at initialization time (e.g. during codegen or module load), not
* per request.
*
* Returns the handler unchanged when `diagnostics_channel` is unavailable
* or the handler is already traced.
*/
export function wrapHandlerWithTracing(handler: EventHandler): EventHandler {
const { tracingChannel } = globalThis.process?.getBuiltinModule?.("diagnostics_channel") ?? {};
if (!tracingChannel) {
return handler;
}
if ((handler as MaybeTracedEventHandler).__traced__) {
return handler;
}
const channel = tracingChannel("h3.request");
const wrapped: MaybeTracedEventHandler = (...args) => {
return channel.tracePromise(async () => handler(...args), {
event: args[0],
type: "route",
} satisfies TracingRequestEvent);
};
wrapped.__traced__ = true;
return wrapped;
}
77 changes: 77 additions & 0 deletions test/tracing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1157,3 +1157,80 @@ describe("tracing channels for H3Core instances", () => {
}
});
});

describe("wrapHandlerWithTracing", () => {
it("wraps a handler so route traces are emitted", async () => {
const listener = createTracingListener();
const { H3Core } = await import("../src/h3.ts");
const { wrapHandlerWithTracing } = await import("../src/tracing.ts");
const { H3Event } = await import("../src/event.ts");

try {
const app = new H3Core();
const routeHandler = wrapHandlerWithTracing(() => "traced response");

app["~findRoute"] = (event: any) => {
if (event.url.pathname === "/test" && event.req.method === "GET") {
return { data: { method: "GET", route: "/test", handler: routeHandler }, params: {} };
}
return undefined;
};

const request = new Request("http://localhost/test", { method: "GET" });
const event = new H3Event(request, undefined, app as any);

await app.handler(event);
await new Promise((resolve) => setTimeout(resolve, 10));

const routeEvents = listener.events.filter((e) => e.asyncStart?.data.type === "route");
expect(routeEvents.some((e) => e.asyncStart?.data.event.url.pathname === "/test")).toBe(true);
} finally {
listener.cleanup();
}
});

it("is idempotent — wrapping twice returns the same wrapper", async () => {
const { wrapHandlerWithTracing } = await import("../src/tracing.ts");

const handler = () => "ok";
const wrapped = wrapHandlerWithTracing(handler);
const doubleWrapped = wrapHandlerWithTracing(wrapped);

expect(doubleWrapped).toBe(wrapped);
expect((wrapped as any).__traced__).toBe(true);
});

it("emits exactly one trace per request on a pre-wrapped handler", async () => {
const listener = createTracingListener();
const { H3Core } = await import("../src/h3.ts");
const { wrapHandlerWithTracing } = await import("../src/tracing.ts");
const { H3Event } = await import("../src/event.ts");

try {
const app = new H3Core();
const routeHandler = wrapHandlerWithTracing(() => "ok");

app["~findRoute"] = () => ({
data: { method: "GET" as const, route: "/test", handler: routeHandler },
params: {},
});

const run = async () => {
const request = new Request("http://localhost/test", { method: "GET" });
const event = new H3Event(request, undefined, app as any);
await app.handler(event);
};

await run();
await run();
await run();

await new Promise((resolve) => setTimeout(resolve, 10));

const routeAsyncStarts = listener.events.filter((e) => e.asyncStart?.data.type === "route");
expect(routeAsyncStarts.length).toBe(3);
} finally {
listener.cleanup();
}
});
});
Loading