From 188ecb691b99ac0bfeb56f9c5f919e1878986e37 Mon Sep 17 00:00:00 2001 From: "superlog-responder[bot]" <312149260+superlog-responder[bot]@users.noreply.github.com> Date: Wed, 7 Oct 2026 16:05:13 +0000 Subject: [PATCH 1/2] Add safe span attribute helper --- src/index.ts | 1 + src/span.ts | 36 ++++++++++++++++++++++++++++++++++++ test/span.test.ts | 43 ++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 79 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 1161e0b..70772f1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,6 @@ export { recordSpanError, + setSpanAttributesSafe, spanErrorAttributes, withSpan, type WithSpanOptions, diff --git a/src/span.ts b/src/span.ts index 3fd69ff..67fc77a 100644 --- a/src/span.ts +++ b/src/span.ts @@ -3,6 +3,7 @@ import { SpanStatusCode, trace, type Attributes, + type AttributeValue, type Span, type SpanOptions, type Tracer, @@ -42,12 +43,47 @@ export function recordSpanError(span: Span, err: unknown): void { span.setAttributes(spanErrorAttributes(err)); } +export function setSpanAttributesSafe( + span: Span, + attributes: Record, +): number { + let count = 0; + + for (const [key, value] of Object.entries(attributes)) { + if (!isAttributeValue(value)) continue; + span.setAttribute(key, value); + count += 1; + } + + return count; +} + export function spanErrorAttributes(err: unknown): Attributes { return { "error.type": errorType(err), }; } +function isAttributeValue(value: unknown): value is AttributeValue { + if ( + typeof value === "string" || + typeof value === "number" || + typeof value === "boolean" + ) { + return true; + } + + return ( + Array.isArray(value) && + value.every( + (item) => + typeof item === "string" || + typeof item === "number" || + typeof item === "boolean", + ) + ); +} + function toException(err: unknown): Error { if (err instanceof Error) return err; return new Error(typeof err === "string" ? err : JSON.stringify(err)); diff --git a/test/span.test.ts b/test/span.test.ts index 3c8353c..97cbd6f 100644 --- a/test/span.test.ts +++ b/test/span.test.ts @@ -1,6 +1,11 @@ import { SpanStatusCode } from "@opentelemetry/api"; import { describe, expect, it, vi } from "vitest"; -import { recordSpanError, spanErrorAttributes, withSpan } from "../src/index.js"; +import { + recordSpanError, + setSpanAttributesSafe, + spanErrorAttributes, + withSpan, +} from "../src/index.js"; describe("span helpers", () => { it("records low-cardinality error.type", () => { @@ -26,6 +31,42 @@ describe("span helpers", () => { expect(span.setAttributes).toHaveBeenCalledWith({ "error.type": "Error" }); }); + it("sets valid span attributes and returns the number set", () => { + const span = { + setAttribute: vi.fn(), + }; + + const count = setSpanAttributesSafe(span as never, { + string: "value", + number: 42, + boolean: true, + strings: ["one", "two"], + numbers: [1, 2], + booleans: [true, false], + }); + + expect(count).toBe(6); + expect(span.setAttribute).toHaveBeenCalledTimes(6); + expect(span.setAttribute).toHaveBeenCalledWith("string", "value"); + expect(span.setAttribute).toHaveBeenCalledWith("numbers", [1, 2]); + }); + + it("drops null, undefined, and invalid attribute values", () => { + const span = { + setAttribute: vi.fn(), + }; + + const count = setSpanAttributesSafe(span as never, { + null: null, + undefined, + object: { value: "invalid" }, + function: () => "invalid", + }); + + expect(count).toBe(0); + expect(span.setAttribute).not.toHaveBeenCalled(); + }); + it("runs callback using a supplied tracer and ends spans", async () => { const span = { end: vi.fn(), From 7a222b8c6009961d6f80b2dc55a6a2af310aaf87 Mon Sep 17 00:00:00 2001 From: "superlog-responder[bot]" <312149260+superlog-responder[bot]@users.noreply.github.com> Date: Wed, 7 Oct 2026 16:09:05 +0000 Subject: [PATCH 2/2] Validate span attribute arrays and keys --- src/span.ts | 24 ++++++++++++++---------- test/span.test.ts | 4 +++- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/span.ts b/src/span.ts index 67fc77a..00a056d 100644 --- a/src/span.ts +++ b/src/span.ts @@ -50,7 +50,7 @@ export function setSpanAttributesSafe( let count = 0; for (const [key, value] of Object.entries(attributes)) { - if (!isAttributeValue(value)) continue; + if (key.length === 0 || !isAttributeValue(value)) continue; span.setAttribute(key, value); count += 1; } @@ -73,15 +73,19 @@ function isAttributeValue(value: unknown): value is AttributeValue { return true; } - return ( - Array.isArray(value) && - value.every( - (item) => - typeof item === "string" || - typeof item === "number" || - typeof item === "boolean", - ) - ); + if (!Array.isArray(value)) return false; + if (value.length === 0) return true; + + const elementType = typeof value[0]; + if ( + elementType !== "string" && + elementType !== "number" && + elementType !== "boolean" + ) { + return false; + } + + return value.every((item) => typeof item === elementType); } function toException(err: unknown): Error { diff --git a/test/span.test.ts b/test/span.test.ts index 97cbd6f..a45951a 100644 --- a/test/span.test.ts +++ b/test/span.test.ts @@ -51,16 +51,18 @@ describe("span helpers", () => { expect(span.setAttribute).toHaveBeenCalledWith("numbers", [1, 2]); }); - it("drops null, undefined, and invalid attribute values", () => { + it("drops empty keys, null, undefined, and invalid attribute values", () => { const span = { setAttribute: vi.fn(), }; const count = setSpanAttributesSafe(span as never, { + "": "invalid", null: null, undefined, object: { value: "invalid" }, function: () => "invalid", + mixed: ["one", 2], }); expect(count).toBe(0);