1import { assert, it } from "@effect/vitest";
2
3import { makeTraceSpanSummary } from "./trace.ts";
4
5const MINUTE_MS = 60_000;
6
7function span(name: string, durationMs: number, endMs: number, exitTag = "Success") {
8 return JSON.stringify({
9 type: "effect-span",
10 name,
11 traceId: "trace",
12 spanId: "span",
13 durationMs,
14 endTimeUnixNano: String(BigInt(endMs) * 1_000_000n),
15 exit: { _tag: exitTag, cause: "cause" },
16 });
17}
18
19function browserSpan(
20 name: string,
21 status: { code: string; message?: string },
22 attributes: Record<string, unknown> = {},
23) {
24 return JSON.stringify({
25 type: "otlp-span",
26 name,
27 durationMs: 1,
28 endTimeUnixNano: "1000000",
29 status,
30 attributes,
31 });
32}
33
34it("reports count, rate, percentiles, and exits per span name", () => {
35 // Ten `refresh` spans of 1..10 ms end over minutes 0..9, and one `probe`
36 // span ends at minute 10, so the recorded window is 10 minutes.
37 const refreshes = Array.from({ length: 10 }, (_, index) =>
38 span(
39 "refresh",
40 index + 1,
41 index * MINUTE_MS,
42 index === 0 ? "Interrupted" : index === 1 ? "Failure" : "Success",
43 ),
44 );
45 const summarizer = makeTraceSpanSummary();
46 [...refreshes, span("probe", 2_500, 10 * MINUTE_MS)].forEach(summarizer.addLine);
47 const summary = summarizer.finish();
48
49 assert.strictEqual(summary.spanCount, 11);
50 assert.strictEqual(summary.minutes, 10);
51 assert.deepStrictEqual(summary.spans, [
52 {
53 name: "refresh",
54 count: 10,
55 perMinute: 1,
56 p50Ms: 5,
57 p90Ms: 9,
58 maxMs: 10,
59 interrupted: 1,
60 failures: 1,
61 },
62 {
63 name: "probe",
64 count: 1,
65 perMinute: 0.1,
66 p50Ms: 2_500,
67 p90Ms: 2_500,
68 maxMs: 2_500,
69 interrupted: 0,
70 failures: 0,
71 },
72 ]);
73});
74
75it("drops spans that ended before the window and counts unreadable lines", () => {
76 const summarizer = makeTraceSpanSummary(MINUTE_MS);
77 [
78 span("old", 1, 0),
79 "",
80 "{not json",
81 JSON.stringify({ name: "no-duration" }),
82 // Ends past the largest Date, so the report could not print it.
83 JSON.stringify({ name: "far-future", durationMs: 1, endTimeUnixNano: "9".repeat(22) }),
84 span("recent", 4, 5 * MINUTE_MS),
85 ].forEach(summarizer.addLine);
86 const summary = summarizer.finish();
87
88 assert.strictEqual(summary.skippedLineCount, 3);
89 assert.deepStrictEqual(
90 summary.spans.map((entry) => [entry.name, entry.count, entry.perMinute]),
91 [["recent", 1, undefined]],
92 );
93});
94
95it("reads failures and interrupts of browser spans from their OTLP status", () => {
96 const summarizer = makeTraceSpanSummary();
97 [
98 browserSpan("render", { code: "2", message: "boom" }),
99 browserSpan("render", { code: "0" }, { "effect.fiber.interrupted": true }),
100 // Clients before Effect 4.0.2.
101 browserSpan("render", { code: "1", message: "Interrupted" }),
102 browserSpan("render", { code: "1" }),
103 ].forEach(summarizer.addLine);
104
105 const [render] = summarizer.finish().spans;
106 assert.deepStrictEqual([render?.count, render?.interrupted, render?.failures], [4, 2, 1]);
107});