Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,313 @@
import { trace } from "@opentelemetry/api";
import { describe, expect, it } from "@suaveplan/testing/runner";
import {
addAttributes,
buildSpanContextObject,
getTraceContext,
recordException,
withSpan,
} from "./span-utils.js";
describe("span-utils", () => {
const tracer = trace.getTracer("test-tracer");
describe("withSpan", () => {
it("calls fn and returns its result", { timeout: 5000 }, async () => {
const result = await withSpan(
tracer,
"test.span",
async (_span) => {
return 42;
}
);
expect(result).toBe(42);
});
it("passes the active span to fn", { timeout: 5000 }, async () => {
let receivedSpan: unknown = null;
await withSpan(tracer, "test.span", async (span) => {
receivedSpan = span;
});
expect(receivedSpan).not.toBeNull();
});
it("re-throws errors thrown by fn", { timeout: 5000 }, async () => {
const error = new Error("test failure");
await expect(
withSpan(tracer, "test.span", async () => {
throw error;
})
).rejects.toThrow("test failure");
});
it("records exception on the span when fn throws", {
timeout: 5000,
}, async () => {
const recordedExceptions: unknown[] = [];
const fakeSpan = {
recordException: (err: unknown) => {
recordedExceptions.push(err);
},
setStatus: () => {},
end: () => {},
setAttribute: () => {},
};
const patchedTracer = {
startActiveSpan<T>(
_name: string,
_opts: unknown,
fn: (span: typeof fakeSpan) => T
): T {
return fn(fakeSpan);
},
} as unknown as typeof tracer;
const error = new TypeError("boom");
await expect(
withSpan(patchedTracer, "err.span", async () => {
throw error;
})
).rejects.toThrow("boom");
expect(recordedExceptions).toHaveLength(1);
expect(recordedExceptions[0]).toBe(error);
});
it("sets initial attributes from options", {
timeout: 5000,
}, async () => {
const setAttrs: Record<string, unknown> = {};
const fakeSpan = {
recordException: () => {},
setStatus: () => {},
end: () => {},
setAttribute: (k: string, v: unknown) => {
setAttrs[k] = v;
},
};
const patchedTracer = {
startActiveSpan<T>(
_name: string,
opts: { attributes?: Record<string, unknown> },
fn: (span: typeof fakeSpan) => T
): T {
for (const [k, v] of Object.entries(
opts.attributes ?? {}
)) {
fakeSpan.setAttribute(k, v);
}
return fn(fakeSpan);
},
} as unknown as typeof tracer;
await withSpan(patchedTracer, "attr.span", async () => "ok", {
attributes: { "test.key": "test-value" },
});
expect(setAttrs["test.key"]).toBe("test-value");
});
});
describe("recordException", () => {
it("sets span status to ERROR and records the exception for Error instances", {
timeout: 5000,
}, () => {
const events: unknown[] = [];
let statusCode: unknown = null;
const span = {
recordException: (err: unknown) => {
events.push(err);
},
setStatus: (s: { code: unknown }) => {
statusCode = s.code;
},
} as Parameters<typeof recordException>[0];
const err = new RangeError("out of range");
recordException(span, err);
expect(events).toHaveLength(1);
expect(events[0]).toBe(err);
expect(statusCode).toBe(2); // SpanStatusCode.ERROR === 2
});
it("does not throw for non-Error values", { timeout: 5000 }, () => {
const span = {
recordException: () => {},
setStatus: () => {},
} as Parameters<typeof recordException>[0];
expect(() => recordException(span, "string error")).not.toThrow();
expect(() => recordException(span, 42)).not.toThrow();
expect(() => recordException(span, null)).not.toThrow();
expect(() => recordException(span, undefined)).not.toThrow();
expect(() => recordException(span, { code: 500 })).not.toThrow();
});
it("wraps non-Error values in an Error before recording", {
timeout: 5000,
}, () => {
const events: unknown[] = [];
const span = {
recordException: (err: unknown) => {
events.push(err);
},
setStatus: () => {},
} as Parameters<typeof recordException>[0];
recordException(span, "plain string");
expect(events[0]).toBeInstanceOf(Error);
expect((events[0] as Error).message).toBe("plain string");
});
});
describe("addAttributes", () => {
it("sets all attributes on the span", { timeout: 5000 }, () => {
const attrs: Record<string, unknown> = {};
const span = {
setAttribute: (k: string, v: unknown) => {
attrs[k] = v;
},
} as Parameters<typeof addAttributes>[0];
addAttributes(span, {
"http.method": "GET",
"http.status_code": 200,
"http.url": "https://example.com",
});
expect(attrs["http.method"]).toBe("GET");
expect(attrs["http.status_code"]).toBe(200);
expect(attrs["http.url"]).toBe("https://example.com");
});
it("skips undefined values", { timeout: 5000 }, () => {
const keys: string[] = [];
const span = {
setAttribute: (k: string) => {
keys.push(k);
},
} as Parameters<typeof addAttributes>[0];
addAttributes(span, {
"defined.key": "value",
"undefined.key": undefined,
});
expect(keys).toContain("defined.key");
expect(keys).not.toContain("undefined.key");
});
it("handles empty attributes map without throwing", {
timeout: 5000,
}, () => {
const span = {
setAttribute: () => {},
} as Parameters<typeof addAttributes>[0];
expect(() => addAttributes(span, {})).not.toThrow();
});
});
describe("getTraceContext", () => {
it("returns empty object when no span is active (no-op tracer)", {
timeout: 5000,
}, () => {
const ctx = getTraceContext();
expect(ctx).toEqual({});
});
it("returns an object (never throws)", { timeout: 5000 }, () => {
expect(() => getTraceContext()).not.toThrow();
});
});
describe("buildSpanContextObject", () => {
it("returns empty object for undefined span context", {
timeout: 5000,
}, () => {
expect(buildSpanContextObject(undefined)).toEqual({});
});
it("populates traceId when present", { timeout: 5000 }, () => {
const result = buildSpanContextObject({
traceId: "4bf92f3577b34da6a3ce929d0e0e4736",
spanId: "00f067aa0ba902b7",
traceFlags: 1,
});
expect(result.traceId).toBe("4bf92f3577b34da6a3ce929d0e0e4736");
});
it("populates spanId when present", { timeout: 5000 }, () => {
const result = buildSpanContextObject({
traceId: "4bf92f3577b34da6a3ce929d0e0e4736",
spanId: "00f067aa0ba902b7",
traceFlags: 1,
});
expect(result.spanId).toBe("00f067aa0ba902b7");
});
it("populates traceFlags when present", { timeout: 5000 }, () => {
const result = buildSpanContextObject({
traceId: "4bf92f3577b34da6a3ce929d0e0e4736",
spanId: "00f067aa0ba902b7",
traceFlags: 1,
});
expect(result.traceFlags).toBe(1);
});
it("omits traceId when undefined in span context", {
timeout: 5000,
}, () => {
const result = buildSpanContextObject({
traceId: undefined as unknown as string,
spanId: "00f067aa0ba902b7",
traceFlags: 1,
});
expect("traceId" in result).toBe(false);
expect(result.spanId).toBe("00f067aa0ba902b7");
expect(result.traceFlags).toBe(1);
});
it("omits spanId when undefined in span context", {
timeout: 5000,
}, () => {
const result = buildSpanContextObject({
traceId: "4bf92f3577b34da6a3ce929d0e0e4736",
spanId: undefined as unknown as string,
traceFlags: 1,
});
expect(result.traceId).toBe("4bf92f3577b34da6a3ce929d0e0e4736");
expect("spanId" in result).toBe(false);
expect(result.traceFlags).toBe(1);
});
it("omits traceFlags when undefined in span context", {
timeout: 5000,
}, () => {
const result = buildSpanContextObject({
traceId: "4bf92f3577b34da6a3ce929d0e0e4736",
spanId: "00f067aa0ba902b7",
traceFlags: undefined as unknown as number,
});
expect(result.traceId).toBe("4bf92f3577b34da6a3ce929d0e0e4736");
expect(result.spanId).toBe("00f067aa0ba902b7");
expect("traceFlags" in result).toBe(false);
});
it("getTraceContext accepts a span for testability", {
timeout: 5000,
}, async () => {
const tracer = trace.getTracer("test");
await withSpan(tracer, "ctx-test", async (span) => {
// The no-op span context has traceId/spanId/traceFlags set to
// their zero values; verify getTraceContext accepts it
const ctx = getTraceContext(span);
expect(typeof ctx).toBe("object");
});
});
});
});