import { beforeEach, describe, expect, it, vi } from "vitest"; const capturedSpan = vi.hoisted(() => ({ setAttribute: vi.fn() })); vi.mock("@sentry/react", () => ({ startSpan: vi.fn((options: unknown, callback: (span: unknown) => unknown) => callback(capturedSpan), ), spanToJSON: vi.fn(() => ({ trace_id: "trace-1", span_id: "span-1" })), setHttpStatus: vi.fn(), })); import * as Sentry from "@sentry/react"; import { HTTPError } from "ky"; import { normalizeHttpRoute, traceHttpOperation } from "@/observability/http-transaction"; const startSpanMock = vi.mocked(Sentry.startSpan); const setHttpStatusMock = vi.mocked(Sentry.setHttpStatus); function lastSpanOptions(): Record { const calls = startSpanMock.mock.calls; const [options] = calls[calls.length - 1] ?? []; return (options ?? {}) as unknown as Record; } describe("normalizeHttpRoute", () => { it("replaces integer path segments with :id", () => { expect(normalizeHttpRoute("/api/workorders/9/media")).toBe("/api/workorders/:id/media"); }); it("replaces UUID path segments with :id", () => { expect(normalizeHttpRoute("/uplifts/3f2504e0-4f89-11d3-9a0c-0305e82c3301/evidence")).toBe( "/uplifts/:id/evidence", ); }); it("replaces long hex and opaque token segments with :id", () => { expect(normalizeHttpRoute("/files/deadbeefcafe1234")).toBe("/files/:id"); expect(normalizeHttpRoute("/share/Ab12Cd34Ef56Gh78")).toBe("/share/:id"); }); it("strips query strings and fragments without using their values", () => { expect(normalizeHttpRoute("/vendor-portal/dispatches?status=pending#x")).toBe( "/vendor-portal/dispatches", ); }); it("keeps descriptive segments untouched", () => { expect(normalizeHttpRoute("/vendor-operations/insights.csv")).toBe( "/vendor-operations/insights.csv", ); }); it("uses only the pathname of absolute URLs", () => { expect(normalizeHttpRoute("https://api.example.com/workorders/9/media?q=1")).toBe( "/workorders/:id/media", ); }); }); describe("traceHttpOperation", () => { beforeEach(() => { vi.clearAllMocks(); }); it("starts a forced http.client transaction with safe attributes", async () => { await traceHttpOperation( { method: "get", url: "/workorders/9/media?include=x", entryPoint: "api.apiGet" }, async () => "ok", ); const options = lastSpanOptions(); expect(startSpanMock).toHaveBeenCalledTimes(1); expect(options.name).toBe("GET /workorders/:id/media"); expect(options.op).toBe("http.client"); expect(options.kind).toBe(2); expect(options.forceTransaction).toBe(true); expect(options.attributes).toEqual({ "http.request.method": "GET", "http.route": "/workorders/:id/media", "code.function": "api.apiGet", "operation.type": "http.client", }); }); it("adds actor.type only when provided and native span ids as attributes", async () => { await traceHttpOperation( { method: "POST", url: "/vendor-portal/dispatches", entryPoint: "vendorPortalApi.call", actorType: "vendor_link", }, async () => null, ); expect(lastSpanOptions().attributes).toMatchObject({ "actor.type": "vendor_link", }); expect(capturedSpan.setAttribute).toHaveBeenCalledWith("trace_id", "trace-1"); expect(capturedSpan.setAttribute).toHaveBeenCalledWith("span_id", "span-1"); expect(capturedSpan.setAttribute).toHaveBeenCalledWith("transaction_id", "span-1"); }); it("reports the status of a returned Response", async () => { const response = new Response("{}", { status: 201 }); await traceHttpOperation({ method: "POST", url: "/x", entryPoint: "fn" }, async () => response); expect(setHttpStatusMock).toHaveBeenCalledTimes(1); expect(setHttpStatusMock).toHaveBeenCalledWith(capturedSpan, 201); }); it("exposes a single-shot status reporter for non-Response results", async () => { const result = await traceHttpOperation( { method: "GET", url: "/x", entryPoint: "fn" }, async ({ reportStatus }) => { reportStatus(204); reportStatus(500); return "done"; }, ); expect(result).toBe("done"); expect(setHttpStatusMock).toHaveBeenCalledTimes(1); expect(setHttpStatusMock).toHaveBeenCalledWith(capturedSpan, 204); }); it("reports the Ky HTTPError response status instead of 500", async () => { const httpError = new HTTPError( new Response("Unauthorized", { status: 401 }), new Request("https://api.example.test/x"), {} as never, ); await expect( traceHttpOperation({ method: "GET", url: "/x", entryPoint: "fn" }, async () => { throw httpError; }), ).rejects.toThrow(); expect(setHttpStatusMock).toHaveBeenCalledTimes(1); expect(setHttpStatusMock).toHaveBeenCalledWith(capturedSpan, 401); }); it("reports status 500 when the callback throws", async () => { await expect( traceHttpOperation({ method: "GET", url: "/x", entryPoint: "fn" }, async () => { throw new Error("boom"); }), ).rejects.toThrow("boom"); expect(setHttpStatusMock).toHaveBeenCalledWith(capturedSpan, 500); }); });