| 1 | import { assert, it } from "@effect/vitest"; |
| 2 | |
| 3 | import { makeTraceSpanSummary } from "./trace.ts"; |
| 4 | |
| 5 | const MINUTE_MS = 60_000; |
| 6 | |
| 7 | function 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 | |
| 19 | function 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 | |
| 34 | it("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 | |
| 75 | it("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 | |
| 95 | it("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 | }); |