import type { Counter, Histogram, Meter, MeterProvider, } from "@opentelemetry/api"; import { afterEach, beforeEach, describe, expect, it, } from "@suaveplan/testing/runner"; import { getInstruments, recordOp, resetInstruments, setStorageBrowserOpfsMeterProvider, } from "./otel.js"; interface RecordedHistogram { name: string; samples: { value: number; attrs: Record | undefined }[]; } interface RecordedCounter { name: string; additions: { value: number; attrs: Record | undefined }[]; } class StubHistogram implements Histogram { public readonly entry: RecordedHistogram; public constructor(name: string, bag: RecordedHistogram[]) { this.entry = { name, samples: [] }; bag.push(this.entry); } public record(value: number, attrs?: Record): void { this.entry.samples.push({ value, attrs }); } } class StubCounter implements Counter { public readonly entry: RecordedCounter; public constructor(name: string, bag: RecordedCounter[]) { this.entry = { name, additions: [] }; bag.push(this.entry); } public add(value: number, attrs?: Record): void { this.entry.additions.push({ value, attrs }); } } class StubMeter implements Meter { public histograms: RecordedHistogram[] = []; public counters: RecordedCounter[] = []; public createHistogram(name: string): Histogram { return new StubHistogram(name, this.histograms); } public createCounter(name: string): Counter { return new StubCounter(name, this.counters); } public createObservableGauge(): never { throw new Error("unused"); } public createUpDownCounter(): never { throw new Error("unused"); } public createObservableCounter(): never { throw new Error("unused"); } public createObservableUpDownCounter(): never { throw new Error("unused"); } public addBatchObservableCallback(): void {} public removeBatchObservableCallback(): void {} public createGauge(): never { throw new Error("unused"); } } class StubMeterProvider implements MeterProvider { public meters: StubMeter[] = []; public getMeter(): Meter { const meter = new StubMeter(); this.meters.push(meter); return meter; } } describe("storage-browser-opfs otel", () => { let provider: StubMeterProvider; beforeEach(() => { provider = new StubMeterProvider(); setStorageBrowserOpfsMeterProvider(provider); }); afterEach(() => { setStorageBrowserOpfsMeterProvider(undefined); resetInstruments(); }); function findCounter(name: string): RecordedCounter | undefined { return provider.meters[0]?.counters.find((c) => c.name === name); } function findHistogram(name: string): RecordedHistogram | undefined { return provider.meters[0]?.histograms.find((h) => h.name === name); } it("recordOp emits the documented counter and histogram set", { timeout: 5000, }, () => { recordOp({ op: "set", outcome: "success", durationMs: 12, bytes: 100, direction: "write", }); const dur = findHistogram("storage_browser_opfs.op_duration_ms"); expect(dur?.samples).toEqual([ { value: 12, attrs: { op: "set", outcome: "success" } }, ]); const writes = findCounter("storage_browser_opfs.writes_total"); expect(writes?.additions).toEqual([ { value: 1, attrs: { op: "set", outcome: "success" } }, ]); const io = findHistogram("storage_browser_opfs.io_bytes"); expect(io?.samples).toEqual([ { value: 100, attrs: { op: "set", direction: "write" } }, ]); }); it("records error counter on a failed op", { timeout: 5000 }, () => { recordOp({ op: "get", outcome: "error", durationMs: 3 }); const errs = findCounter("storage_browser_opfs.errors_total"); expect(errs?.additions).toEqual([ { value: 1, attrs: { op: "get", outcome: "error" } }, ]); }); it("records aborted operations on the errors counter", { timeout: 5000, }, () => { recordOp({ op: "get", outcome: "aborted", durationMs: 0 }); expect( findCounter("storage_browser_opfs.errors_total")?.additions[0] ?.attrs ).toEqual({ op: "get", outcome: "aborted" }); }); it("increments reads/writes/deletes counters by op kind", { timeout: 5000, }, () => { recordOp({ op: "get", outcome: "success", durationMs: 1 }); recordOp({ op: "getPartial", outcome: "success", durationMs: 1 }); recordOp({ op: "getWithMetadata", outcome: "success", durationMs: 1 }); recordOp({ op: "set", outcome: "success", durationMs: 1 }); recordOp({ op: "setPartial", outcome: "success", durationMs: 1 }); recordOp({ op: "delete", outcome: "success", durationMs: 1 }); recordOp({ op: "clear", outcome: "success", durationMs: 1 }); recordOp({ op: "exists", outcome: "success", durationMs: 1 }); recordOp({ op: "list", outcome: "success", durationMs: 1 }); expect( findCounter("storage_browser_opfs.reads_total")?.additions ).toHaveLength(3); expect( findCounter("storage_browser_opfs.writes_total")?.additions ).toHaveLength(2); expect( findCounter("storage_browser_opfs.deletes_total")?.additions ).toHaveLength(2); }); it("returns a cached instrument set on repeat getInstruments calls", { timeout: 5000, }, () => { const a = getInstruments(); const b = getInstruments(); expect(a).toBe(b); }); it("rebuilds the instrument set when getInstruments(true) is called", { timeout: 5000, }, () => { const a = getInstruments(); const b = getInstruments(true); expect(a).not.toBe(b); }); it("omits the io_bytes record when bytes is undefined", { timeout: 5000, }, () => { recordOp({ op: "list", outcome: "success", durationMs: 5 }); expect( findHistogram("storage_browser_opfs.io_bytes")?.samples ?? [] ).toHaveLength(0); }); it("uses the global metrics fallback when provider is unset", { timeout: 5000, }, () => { setStorageBrowserOpfsMeterProvider(undefined); // Should not throw — falls back to the global no-op meter provider. const inst = getInstruments(true); expect(inst.meter).toBeDefined(); }); });