From 95122502244111d97a6c5fd564137f0b91b5a0aa Mon Sep 17 00:00:00 2001 From: Peng Fan Date: Mon, 18 May 2026 11:07:38 -0700 Subject: [PATCH 1/5] fix: scope A365SpanProcessor.onStart to GenAI spans only Non-GenAI spans (HTTP, DB, etc.) created within a BaggageScope were incorrectly receiving A365 attributes like telemetry.sdk.name and microsoft.tenant.id. Guard onStart to only process spans that carry a gen_ai.operation.name attribute or baggage entry. Mirrors microsoft/opentelemetry-distro-dotnet#99. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/a365/processors/A365SpanProcessor.ts | 14 ++ .../unit/a365/a365SpanProcessor.test.ts | 193 +++++++++++++++--- 2 files changed, 183 insertions(+), 24 deletions(-) diff --git a/src/a365/processors/A365SpanProcessor.ts b/src/a365/processors/A365SpanProcessor.ts index abdd16c..ba7a1e3 100644 --- a/src/a365/processors/A365SpanProcessor.ts +++ b/src/a365/processors/A365SpanProcessor.ts @@ -31,6 +31,9 @@ export class A365SpanProcessor implements BaseSpanProcessor { /** * Called when a span is started. * Copies relevant baggage entries to span attributes. + * Only GenAI spans are processed (those that have a `gen_ai.operation.name` + * attribute or baggage entry, i.e. invoke_agent, execute_tool, inference, + * and output_messages spans); all other spans pass through unmodified. */ onStart(span: Span, parentContext?: Context): void { const ctx = parentContext; @@ -56,6 +59,17 @@ export class A365SpanProcessor implements BaseSpanProcessor { return; } + // Only process GenAI spans — those that carry a gen_ai.operation.name + // either as a span attribute or as a baggage entry. + const hasGenAiOperationAttr = existingAttrs.has( + OpenTelemetryConstants.GEN_AI_OPERATION_NAME_KEY, + ); + const hasGenAiOperationBaggage = + baggage.getEntry(OpenTelemetryConstants.GEN_AI_OPERATION_NAME_KEY)?.value != null; + if (!hasGenAiOperationAttr && !hasGenAiOperationBaggage) { + return; + } + const baggageMap = new Map(); baggage.getAllEntries().forEach(([key, entry]) => { if (entry.value) { diff --git a/test/internal/unit/a365/a365SpanProcessor.test.ts b/test/internal/unit/a365/a365SpanProcessor.test.ts index fd0892a..3544a10 100644 --- a/test/internal/unit/a365/a365SpanProcessor.test.ts +++ b/test/internal/unit/a365/a365SpanProcessor.test.ts @@ -16,6 +16,25 @@ import { INVOKE_AGENT_ATTRIBUTES, } from "../../../../src/a365/index.js"; +/** + * Helper: creates baggage with gen_ai.operation.name set plus any additional entries. + */ +function createGenAiBaggage( + operationName: string, + extra?: Record, +) { + let baggage = propagation.createBaggage(); + baggage = baggage.setEntry(OpenTelemetryConstants.GEN_AI_OPERATION_NAME_KEY, { + value: operationName, + }); + if (extra) { + for (const [key, value] of Object.entries(extra)) { + baggage = baggage.setEntry(key, { value }); + } + } + return baggage; +} + describe("A365SpanProcessor", () => { let provider: BasicTracerProvider; let processor: A365SpanProcessor; @@ -33,8 +52,8 @@ describe("A365SpanProcessor", () => { await provider.shutdown(); }); - describe("baggage to span attribute enrichment", () => { - it("should copy generic attributes from baggage to span", () => { + describe("GenAI span filtering", () => { + it("should not mutate spans without gen_ai.operation.name", () => { const baggageEntries = { [OpenTelemetryConstants.TENANT_ID_KEY]: "tenant-123", [OpenTelemetryConstants.GEN_AI_AGENT_ID_KEY]: "agent-789", @@ -47,6 +66,106 @@ describe("A365SpanProcessor", () => { const ctx = propagation.setBaggage(context.active(), baggage); + const tracer = provider.getTracer("test"); + const testSpan = tracer.startSpan("HTTP GET /api/data", { kind: SpanKind.CLIENT }, ctx); + testSpan.end(); + + const spans = memoryExporter.getFinishedSpans(); + expect(spans).toHaveLength(1); + const attrs = spans[0].attributes; + expect(attrs[OpenTelemetryConstants.TENANT_ID_KEY]).toBeUndefined(); + expect(attrs[OpenTelemetryConstants.GEN_AI_AGENT_ID_KEY]).toBeUndefined(); + expect(attrs[OpenTelemetryConstants.GEN_AI_AGENT_NAME_KEY]).toBeUndefined(); + expect(attrs[OpenTelemetryConstants.SESSION_ID_KEY]).toBeUndefined(); + expect(attrs[OpenTelemetryConstants.TELEMETRY_SDK_NAME_KEY]).toBeUndefined(); + }); + + it("should not mutate spans when baggage has no gen_ai.operation.name even with other A365 baggage", () => { + let baggage = propagation.createBaggage(); + baggage = baggage.setEntry(OpenTelemetryConstants.TENANT_ID_KEY, { value: "tenant-123" }); + baggage = baggage.setEntry(OpenTelemetryConstants.GEN_AI_AGENT_ID_KEY, { + value: "agent-abc", + }); + baggage = baggage.setEntry(OpenTelemetryConstants.SESSION_ID_KEY, { + value: "session-xyz", + }); + + const ctx = propagation.setBaggage(context.active(), baggage); + + const tracer = provider.getTracer("test"); + const testSpan = tracer.startSpan("db-query", { kind: SpanKind.CLIENT }, ctx); + testSpan.end(); + + const spans = memoryExporter.getFinishedSpans(); + expect(spans).toHaveLength(1); + const attrs = spans[0].attributes; + expect(attrs[OpenTelemetryConstants.TENANT_ID_KEY]).toBeUndefined(); + expect(attrs[OpenTelemetryConstants.GEN_AI_AGENT_ID_KEY]).toBeUndefined(); + expect(attrs[OpenTelemetryConstants.SESSION_ID_KEY]).toBeUndefined(); + expect(attrs[OpenTelemetryConstants.TELEMETRY_SDK_NAME_KEY]).toBeUndefined(); + expect(attrs[OpenTelemetryConstants.TELEMETRY_SDK_LANGUAGE_KEY]).toBeUndefined(); + expect(attrs[OpenTelemetryConstants.TELEMETRY_SDK_VERSION_KEY]).toBeUndefined(); + }); + + it("should process spans that have gen_ai.operation.name as a span attribute", () => { + // Baggage has no gen_ai.operation.name, but the span itself does + let baggage = propagation.createBaggage(); + baggage = baggage.setEntry(OpenTelemetryConstants.TENANT_ID_KEY, { value: "tenant-123" }); + + const ctx = propagation.setBaggage(context.active(), baggage); + + const tracer = provider.getTracer("microsoft-otel-openai-agents"); + const testSpan = tracer.startSpan( + "invoke_agent test", + { + kind: SpanKind.CLIENT, + attributes: { + [OpenTelemetryConstants.GEN_AI_OPERATION_NAME_KEY]: "invoke_agent", + }, + }, + ctx, + ); + testSpan.end(); + + const spans = memoryExporter.getFinishedSpans(); + expect(spans).toHaveLength(1); + const attrs = spans[0].attributes; + expect(attrs[OpenTelemetryConstants.TENANT_ID_KEY]).toBe("tenant-123"); + expect(attrs[OpenTelemetryConstants.TELEMETRY_SDK_NAME_KEY]).toBe( + OpenTelemetryConstants.TELEMETRY_SDK_NAME_VALUE, + ); + }); + + it("should process spans from any tracer source when gen_ai.operation.name is present in baggage", () => { + const baggage = createGenAiBaggage("chat", { + [OpenTelemetryConstants.TENANT_ID_KEY]: "tenant-123", + }); + const ctx = propagation.setBaggage(context.active(), baggage); + + // Use a non-A365 tracer name (e.g. LangChain instrumentor) + const tracer = provider.getTracer("microsoft-otel-langchain"); + const testSpan = tracer.startSpan("chat span", { kind: SpanKind.CLIENT }, ctx); + testSpan.end(); + + const spans = memoryExporter.getFinishedSpans(); + expect(spans).toHaveLength(1); + const attrs = spans[0].attributes; + expect(attrs[OpenTelemetryConstants.TENANT_ID_KEY]).toBe("tenant-123"); + expect(attrs[OpenTelemetryConstants.TELEMETRY_SDK_NAME_KEY]).toBe( + OpenTelemetryConstants.TELEMETRY_SDK_NAME_VALUE, + ); + }); + }); + + describe("baggage to span attribute enrichment", () => { + it("should copy generic attributes from baggage to span", () => { + const baggage = createGenAiBaggage("chat", { + [OpenTelemetryConstants.TENANT_ID_KEY]: "tenant-123", + [OpenTelemetryConstants.GEN_AI_AGENT_ID_KEY]: "agent-789", + }); + + const ctx = propagation.setBaggage(context.active(), baggage); + const tracer = provider.getTracer("test"); const testSpan = tracer.startSpan("test-span", { kind: SpanKind.CLIENT }, ctx); testSpan.end(); @@ -59,9 +178,8 @@ describe("A365SpanProcessor", () => { }); it("should copy sessionId from baggage to span", () => { - let baggage = propagation.createBaggage(); - baggage = baggage.setEntry(OpenTelemetryConstants.SESSION_ID_KEY, { - value: "session-abc", + const baggage = createGenAiBaggage("chat", { + [OpenTelemetryConstants.SESSION_ID_KEY]: "session-abc", }); const ctx = propagation.setBaggage(context.active(), baggage); @@ -76,9 +194,8 @@ describe("A365SpanProcessor", () => { }); it("should copy sessionDescription from baggage to span", () => { - let baggage = propagation.createBaggage(); - baggage = baggage.setEntry(OpenTelemetryConstants.SESSION_DESCRIPTION_KEY, { - value: "Test session description", + const baggage = createGenAiBaggage("chat", { + [OpenTelemetryConstants.SESSION_DESCRIPTION_KEY]: "Test session description", }); const ctx = propagation.setBaggage(context.active(), baggage); @@ -95,17 +212,13 @@ describe("A365SpanProcessor", () => { }); it("should copy invoke agent attributes for invoke_agent operations", () => { - const baggageEntries = { - [OpenTelemetryConstants.GEN_AI_OPERATION_NAME_KEY]: - OpenTelemetryConstants.INVOKE_AGENT_OPERATION_NAME, - [OpenTelemetryConstants.TENANT_ID_KEY]: "tenant-123", - [OpenTelemetryConstants.USER_ID_KEY]: "caller-456", - }; - - let baggage = propagation.createBaggage(); - for (const [key, value] of Object.entries(baggageEntries)) { - baggage = baggage.setEntry(key, { value }); - } + const baggage = createGenAiBaggage( + OpenTelemetryConstants.INVOKE_AGENT_OPERATION_NAME, + { + [OpenTelemetryConstants.TENANT_ID_KEY]: "tenant-123", + [OpenTelemetryConstants.USER_ID_KEY]: "caller-456", + }, + ); const ctx = propagation.setBaggage(context.active(), baggage); @@ -121,9 +234,8 @@ describe("A365SpanProcessor", () => { }); it("should not overwrite existing span attributes", () => { - let baggage = propagation.createBaggage(); - baggage = baggage.setEntry(OpenTelemetryConstants.TENANT_ID_KEY, { - value: "tenant-from-baggage", + const baggage = createGenAiBaggage("chat", { + [OpenTelemetryConstants.TENANT_ID_KEY]: "tenant-from-baggage", }); const ctx = propagation.setBaggage(context.active(), baggage); @@ -149,6 +261,9 @@ describe("A365SpanProcessor", () => { it("should ignore empty baggage values", () => { let baggage = propagation.createBaggage(); + baggage = baggage.setEntry(OpenTelemetryConstants.GEN_AI_OPERATION_NAME_KEY, { + value: "chat", + }); baggage = baggage.setEntry(OpenTelemetryConstants.TENANT_ID_KEY, { value: "" }); const ctx = propagation.setBaggage(context.active(), baggage); @@ -163,8 +278,8 @@ describe("A365SpanProcessor", () => { expect(attrs[OpenTelemetryConstants.TENANT_ID_KEY]).toBeUndefined(); }); - it("should set telemetry SDK attributes", () => { - const baggage = propagation.createBaggage(); + it("should set telemetry SDK attributes on GenAI spans", () => { + const baggage = createGenAiBaggage("chat"); const ctx = propagation.setBaggage(context.active(), baggage); const tracer = provider.getTracer("test"); const testSpan = tracer.startSpan("test-span", { kind: SpanKind.CLIENT }, ctx); @@ -183,6 +298,36 @@ describe("A365SpanProcessor", () => { OpenTelemetryConstants.TELEMETRY_SDK_VERSION_VALUE, ); }); + + it("should enrich all four GenAI operation types", () => { + const operations = [ + OpenTelemetryConstants.INVOKE_AGENT_OPERATION_NAME, + OpenTelemetryConstants.EXECUTE_TOOL_OPERATION_NAME, + OpenTelemetryConstants.OUTPUT_MESSAGES_OPERATION_NAME, + OpenTelemetryConstants.CHAT_OPERATION_NAME, + ]; + + for (const op of operations) { + memoryExporter.reset(); + const baggage = createGenAiBaggage(op, { + [OpenTelemetryConstants.TENANT_ID_KEY]: "tenant-123", + [OpenTelemetryConstants.GEN_AI_AGENT_ID_KEY]: "agent-abc", + }); + const ctx = propagation.setBaggage(context.active(), baggage); + const tracer = provider.getTracer("test"); + const span = tracer.startSpan(`${op} span`, { kind: SpanKind.CLIENT }, ctx); + span.end(); + + const spans = memoryExporter.getFinishedSpans(); + expect(spans).toHaveLength(1); + const attrs = spans[0].attributes; + expect(attrs[OpenTelemetryConstants.TENANT_ID_KEY]).toBe("tenant-123"); + expect(attrs[OpenTelemetryConstants.GEN_AI_AGENT_ID_KEY]).toBe("agent-abc"); + expect(attrs[OpenTelemetryConstants.TELEMETRY_SDK_NAME_KEY]).toBe( + OpenTelemetryConstants.TELEMETRY_SDK_NAME_VALUE, + ); + } + }); }); describe("attribute registry application", () => { From d7a6fa3543d0e524b3d4166dad99ccbdf27e35ea Mon Sep 17 00:00:00 2001 From: Peng Fan Date: Mon, 18 May 2026 11:30:09 -0700 Subject: [PATCH 2/5] style: fix prettier formatting in A365SpanProcessor test Co-Authored-By: Claude Opus 4.6 (1M context) --- .../internal/unit/a365/a365SpanProcessor.test.ts | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/test/internal/unit/a365/a365SpanProcessor.test.ts b/test/internal/unit/a365/a365SpanProcessor.test.ts index 3544a10..7b24a73 100644 --- a/test/internal/unit/a365/a365SpanProcessor.test.ts +++ b/test/internal/unit/a365/a365SpanProcessor.test.ts @@ -19,10 +19,7 @@ import { /** * Helper: creates baggage with gen_ai.operation.name set plus any additional entries. */ -function createGenAiBaggage( - operationName: string, - extra?: Record, -) { +function createGenAiBaggage(operationName: string, extra?: Record) { let baggage = propagation.createBaggage(); baggage = baggage.setEntry(OpenTelemetryConstants.GEN_AI_OPERATION_NAME_KEY, { value: operationName, @@ -212,13 +209,10 @@ describe("A365SpanProcessor", () => { }); it("should copy invoke agent attributes for invoke_agent operations", () => { - const baggage = createGenAiBaggage( - OpenTelemetryConstants.INVOKE_AGENT_OPERATION_NAME, - { - [OpenTelemetryConstants.TENANT_ID_KEY]: "tenant-123", - [OpenTelemetryConstants.USER_ID_KEY]: "caller-456", - }, - ); + const baggage = createGenAiBaggage(OpenTelemetryConstants.INVOKE_AGENT_OPERATION_NAME, { + [OpenTelemetryConstants.TENANT_ID_KEY]: "tenant-123", + [OpenTelemetryConstants.USER_ID_KEY]: "caller-456", + }); const ctx = propagation.setBaggage(context.active(), baggage); From 5a96e5261b7e2ddfb5f916ab87cddbe365034130 Mon Sep 17 00:00:00 2001 From: Peng Fan Date: Mon, 18 May 2026 14:56:26 -0700 Subject: [PATCH 3/5] fix: check specific operation names instead of baggage presence Address review feedback: filter on known gen_ai.operation.name span attribute values (invoke_agent, execute_tool, chat, output_messages) rather than checking baggage, since operation name is set as a span attribute not baggage. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/a365/processors/A365SpanProcessor.ts | 29 +-- .../unit/a365/a365SpanProcessor.test.ts | 165 +++++++++++------- 2 files changed, 119 insertions(+), 75 deletions(-) diff --git a/src/a365/processors/A365SpanProcessor.ts b/src/a365/processors/A365SpanProcessor.ts index ba7a1e3..88fda25 100644 --- a/src/a365/processors/A365SpanProcessor.ts +++ b/src/a365/processors/A365SpanProcessor.ts @@ -20,6 +20,14 @@ import type { import { OpenTelemetryConstants } from "../constants.js"; import { GENERIC_ATTRIBUTES, INVOKE_AGENT_ATTRIBUTES } from "./util.js"; +/** Known GenAI operation names used to filter spans in the processor. */ +const GENAI_OPERATION_NAMES = new Set([ + OpenTelemetryConstants.INVOKE_AGENT_OPERATION_NAME, + OpenTelemetryConstants.EXECUTE_TOOL_OPERATION_NAME, + OpenTelemetryConstants.CHAT_OPERATION_NAME, + OpenTelemetryConstants.OUTPUT_MESSAGES_OPERATION_NAME, +]); + /** * Copies relevant baggage entries to span attributes on span start. * @@ -31,9 +39,9 @@ export class A365SpanProcessor implements BaseSpanProcessor { /** * Called when a span is started. * Copies relevant baggage entries to span attributes. - * Only GenAI spans are processed (those that have a `gen_ai.operation.name` - * attribute or baggage entry, i.e. invoke_agent, execute_tool, inference, - * and output_messages spans); all other spans pass through unmodified. + * Only GenAI spans are processed (those with a known `gen_ai.operation.name` + * span attribute: invoke_agent, execute_tool, chat, output_messages); + * all other spans pass through unmodified. */ onStart(span: Span, parentContext?: Context): void { const ctx = parentContext; @@ -59,14 +67,13 @@ export class A365SpanProcessor implements BaseSpanProcessor { return; } - // Only process GenAI spans — those that carry a gen_ai.operation.name - // either as a span attribute or as a baggage entry. - const hasGenAiOperationAttr = existingAttrs.has( - OpenTelemetryConstants.GEN_AI_OPERATION_NAME_KEY, - ); - const hasGenAiOperationBaggage = - baggage.getEntry(OpenTelemetryConstants.GEN_AI_OPERATION_NAME_KEY)?.value != null; - if (!hasGenAiOperationAttr && !hasGenAiOperationBaggage) { + // Only process GenAI spans — those with a known gen_ai.operation.name + // span attribute (invoke_agent, execute_tool, chat, output_messages). + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const operationNameAttr = (span as any).attributes?.[ + OpenTelemetryConstants.GEN_AI_OPERATION_NAME_KEY + ]; + if (!GENAI_OPERATION_NAMES.has(operationNameAttr)) { return; } diff --git a/test/internal/unit/a365/a365SpanProcessor.test.ts b/test/internal/unit/a365/a365SpanProcessor.test.ts index 7b24a73..f64d3b8 100644 --- a/test/internal/unit/a365/a365SpanProcessor.test.ts +++ b/test/internal/unit/a365/a365SpanProcessor.test.ts @@ -17,21 +17,41 @@ import { } from "../../../../src/a365/index.js"; /** - * Helper: creates baggage with gen_ai.operation.name set plus any additional entries. + * Helper: creates a baggage instance with the given entries. */ -function createGenAiBaggage(operationName: string, extra?: Record) { +function createBaggage(entries: Record) { let baggage = propagation.createBaggage(); - baggage = baggage.setEntry(OpenTelemetryConstants.GEN_AI_OPERATION_NAME_KEY, { - value: operationName, - }); - if (extra) { - for (const [key, value] of Object.entries(extra)) { - baggage = baggage.setEntry(key, { value }); - } + for (const [key, value] of Object.entries(entries)) { + baggage = baggage.setEntry(key, { value }); } return baggage; } +/** + * Helper: starts a GenAI span with `gen_ai.operation.name` as a span attribute + * and the given baggage entries in context. + */ +function startGenAiSpan( + provider: BasicTracerProvider, + operationName: string, + baggage: Record = {}, + spanName?: string, +) { + const bag = createBaggage(baggage); + const ctx = propagation.setBaggage(context.active(), bag); + const tracer = provider.getTracer("test"); + return tracer.startSpan( + spanName ?? `${operationName} span`, + { + kind: SpanKind.CLIENT, + attributes: { + [OpenTelemetryConstants.GEN_AI_OPERATION_NAME_KEY]: operationName, + }, + }, + ctx, + ); +} + describe("A365SpanProcessor", () => { let provider: BasicTracerProvider; let processor: A365SpanProcessor; @@ -133,15 +153,24 @@ describe("A365SpanProcessor", () => { ); }); - it("should process spans from any tracer source when gen_ai.operation.name is present in baggage", () => { - const baggage = createGenAiBaggage("chat", { + it("should process spans from any tracer source when gen_ai.operation.name span attribute is set", () => { + const bag = createBaggage({ [OpenTelemetryConstants.TENANT_ID_KEY]: "tenant-123", }); - const ctx = propagation.setBaggage(context.active(), baggage); + const ctx = propagation.setBaggage(context.active(), bag); // Use a non-A365 tracer name (e.g. LangChain instrumentor) const tracer = provider.getTracer("microsoft-otel-langchain"); - const testSpan = tracer.startSpan("chat span", { kind: SpanKind.CLIENT }, ctx); + const testSpan = tracer.startSpan( + "chat span", + { + kind: SpanKind.CLIENT, + attributes: { + [OpenTelemetryConstants.GEN_AI_OPERATION_NAME_KEY]: "chat", + }, + }, + ctx, + ); testSpan.end(); const spans = memoryExporter.getFinishedSpans(); @@ -152,19 +181,40 @@ describe("A365SpanProcessor", () => { OpenTelemetryConstants.TELEMETRY_SDK_NAME_VALUE, ); }); + + it("should not mutate spans with an unknown gen_ai.operation.name value", () => { + const bag = createBaggage({ + [OpenTelemetryConstants.TENANT_ID_KEY]: "tenant-123", + }); + const ctx = propagation.setBaggage(context.active(), bag); + + const tracer = provider.getTracer("test"); + const testSpan = tracer.startSpan( + "unknown-op span", + { + kind: SpanKind.CLIENT, + attributes: { + [OpenTelemetryConstants.GEN_AI_OPERATION_NAME_KEY]: "unknown_operation", + }, + }, + ctx, + ); + testSpan.end(); + + const spans = memoryExporter.getFinishedSpans(); + expect(spans).toHaveLength(1); + const attrs = spans[0].attributes; + expect(attrs[OpenTelemetryConstants.TENANT_ID_KEY]).toBeUndefined(); + expect(attrs[OpenTelemetryConstants.TELEMETRY_SDK_NAME_KEY]).toBeUndefined(); + }); }); describe("baggage to span attribute enrichment", () => { it("should copy generic attributes from baggage to span", () => { - const baggage = createGenAiBaggage("chat", { + const testSpan = startGenAiSpan(provider, "chat", { [OpenTelemetryConstants.TENANT_ID_KEY]: "tenant-123", [OpenTelemetryConstants.GEN_AI_AGENT_ID_KEY]: "agent-789", }); - - const ctx = propagation.setBaggage(context.active(), baggage); - - const tracer = provider.getTracer("test"); - const testSpan = tracer.startSpan("test-span", { kind: SpanKind.CLIENT }, ctx); testSpan.end(); const spans = memoryExporter.getFinishedSpans(); @@ -175,49 +225,38 @@ describe("A365SpanProcessor", () => { }); it("should copy sessionId from baggage to span", () => { - const baggage = createGenAiBaggage("chat", { + const testSpan = startGenAiSpan(provider, "chat", { [OpenTelemetryConstants.SESSION_ID_KEY]: "session-abc", }); - - const ctx = propagation.setBaggage(context.active(), baggage); - const tracer = provider.getTracer("test"); - const testSpan = tracer.startSpan("test-span", { kind: SpanKind.CLIENT }, ctx); testSpan.end(); const spans = memoryExporter.getFinishedSpans(); expect(spans).toHaveLength(1); - const attrs = spans[0].attributes; - expect(attrs[OpenTelemetryConstants.SESSION_ID_KEY]).toBe("session-abc"); + expect(spans[0].attributes[OpenTelemetryConstants.SESSION_ID_KEY]).toBe("session-abc"); }); it("should copy sessionDescription from baggage to span", () => { - const baggage = createGenAiBaggage("chat", { + const testSpan = startGenAiSpan(provider, "chat", { [OpenTelemetryConstants.SESSION_DESCRIPTION_KEY]: "Test session description", }); - - const ctx = propagation.setBaggage(context.active(), baggage); - const tracer = provider.getTracer("test"); - const testSpan = tracer.startSpan("test-span", { kind: SpanKind.CLIENT }, ctx); testSpan.end(); const spans = memoryExporter.getFinishedSpans(); expect(spans).toHaveLength(1); - const attrs = spans[0].attributes; - expect(attrs[OpenTelemetryConstants.SESSION_DESCRIPTION_KEY]).toBe( + expect(spans[0].attributes[OpenTelemetryConstants.SESSION_DESCRIPTION_KEY]).toBe( "Test session description", ); }); it("should copy invoke agent attributes for invoke_agent operations", () => { - const baggage = createGenAiBaggage(OpenTelemetryConstants.INVOKE_AGENT_OPERATION_NAME, { - [OpenTelemetryConstants.TENANT_ID_KEY]: "tenant-123", - [OpenTelemetryConstants.USER_ID_KEY]: "caller-456", - }); - - const ctx = propagation.setBaggage(context.active(), baggage); - - const tracer = provider.getTracer("test"); - const testSpan = tracer.startSpan("invoke_agent test", { kind: SpanKind.CLIENT }, ctx); + const testSpan = startGenAiSpan( + provider, + OpenTelemetryConstants.INVOKE_AGENT_OPERATION_NAME, + { + [OpenTelemetryConstants.TENANT_ID_KEY]: "tenant-123", + [OpenTelemetryConstants.USER_ID_KEY]: "caller-456", + }, + ); testSpan.end(); const spans = memoryExporter.getFinishedSpans(); @@ -228,11 +267,10 @@ describe("A365SpanProcessor", () => { }); it("should not overwrite existing span attributes", () => { - const baggage = createGenAiBaggage("chat", { + const bag = createBaggage({ [OpenTelemetryConstants.TENANT_ID_KEY]: "tenant-from-baggage", }); - - const ctx = propagation.setBaggage(context.active(), baggage); + const ctx = propagation.setBaggage(context.active(), bag); const tracer = provider.getTracer("test"); const testSpan = tracer.startSpan( @@ -240,6 +278,7 @@ describe("A365SpanProcessor", () => { { kind: SpanKind.CLIENT, attributes: { + [OpenTelemetryConstants.GEN_AI_OPERATION_NAME_KEY]: "chat", [OpenTelemetryConstants.TENANT_ID_KEY]: "tenant-existing", }, }, @@ -249,34 +288,35 @@ describe("A365SpanProcessor", () => { const spans = memoryExporter.getFinishedSpans(); expect(spans).toHaveLength(1); - const attrs = spans[0].attributes; - expect(attrs[OpenTelemetryConstants.TENANT_ID_KEY]).toBe("tenant-existing"); + expect(spans[0].attributes[OpenTelemetryConstants.TENANT_ID_KEY]).toBe("tenant-existing"); }); it("should ignore empty baggage values", () => { - let baggage = propagation.createBaggage(); - baggage = baggage.setEntry(OpenTelemetryConstants.GEN_AI_OPERATION_NAME_KEY, { - value: "chat", + const bag = createBaggage({ + [OpenTelemetryConstants.TENANT_ID_KEY]: "", }); - baggage = baggage.setEntry(OpenTelemetryConstants.TENANT_ID_KEY, { value: "" }); - - const ctx = propagation.setBaggage(context.active(), baggage); + const ctx = propagation.setBaggage(context.active(), bag); const tracer = provider.getTracer("test"); - const testSpan = tracer.startSpan("test-span", { kind: SpanKind.CLIENT }, ctx); + const testSpan = tracer.startSpan( + "test-span", + { + kind: SpanKind.CLIENT, + attributes: { + [OpenTelemetryConstants.GEN_AI_OPERATION_NAME_KEY]: "chat", + }, + }, + ctx, + ); testSpan.end(); const spans = memoryExporter.getFinishedSpans(); expect(spans).toHaveLength(1); - const attrs = spans[0].attributes; - expect(attrs[OpenTelemetryConstants.TENANT_ID_KEY]).toBeUndefined(); + expect(spans[0].attributes[OpenTelemetryConstants.TENANT_ID_KEY]).toBeUndefined(); }); it("should set telemetry SDK attributes on GenAI spans", () => { - const baggage = createGenAiBaggage("chat"); - const ctx = propagation.setBaggage(context.active(), baggage); - const tracer = provider.getTracer("test"); - const testSpan = tracer.startSpan("test-span", { kind: SpanKind.CLIENT }, ctx); + const testSpan = startGenAiSpan(provider, "chat"); testSpan.end(); const spans = memoryExporter.getFinishedSpans(); @@ -303,13 +343,10 @@ describe("A365SpanProcessor", () => { for (const op of operations) { memoryExporter.reset(); - const baggage = createGenAiBaggage(op, { + const span = startGenAiSpan(provider, op, { [OpenTelemetryConstants.TENANT_ID_KEY]: "tenant-123", [OpenTelemetryConstants.GEN_AI_AGENT_ID_KEY]: "agent-abc", }); - const ctx = propagation.setBaggage(context.active(), baggage); - const tracer = provider.getTracer("test"); - const span = tracer.startSpan(`${op} span`, { kind: SpanKind.CLIENT }, ctx); span.end(); const spans = memoryExporter.getFinishedSpans(); From de3baf5bfaa959167e3e4ffa5a88c293cb213d79 Mon Sep 17 00:00:00 2001 From: Peng Fan Date: Mon, 18 May 2026 14:59:52 -0700 Subject: [PATCH 4/5] refactor: reuse GEN_AI_OPERATION_NAMES from exporter/utils Export the existing GEN_AI_OPERATION_NAMES set from exporter/utils.ts and import it in A365SpanProcessor instead of duplicating the list. This also picks up the additional inference operation types (Chat, TextCompletion, GenerateContent). Co-Authored-By: Claude Opus 4.6 (1M context) --- src/a365/exporter/utils.ts | 2 +- src/a365/processors/A365SpanProcessor.ts | 11 ++--------- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/src/a365/exporter/utils.ts b/src/a365/exporter/utils.ts index 01d94db..6c750fa 100644 --- a/src/a365/exporter/utils.ts +++ b/src/a365/exporter/utils.ts @@ -31,7 +31,7 @@ const MESSAGE_ROLE_SYSTEM = "system"; * Known genAI operation names produced by the SDK scopes and auto-instrumentation. * Only spans whose gen_ai.operation.name matches one of these values are exported. */ -const GEN_AI_OPERATION_NAMES: ReadonlySet = new Set([ +export const GEN_AI_OPERATION_NAMES: ReadonlySet = new Set([ GEN_AI_OPERATION_INVOKE_AGENT, // 'invoke_agent' GEN_AI_OPERATION_EXECUTE_TOOL, // 'execute_tool' GEN_AI_OPERATION_OUTPUT_MESSAGES, // 'output_messages' diff --git a/src/a365/processors/A365SpanProcessor.ts b/src/a365/processors/A365SpanProcessor.ts index 88fda25..9ace1b2 100644 --- a/src/a365/processors/A365SpanProcessor.ts +++ b/src/a365/processors/A365SpanProcessor.ts @@ -18,16 +18,9 @@ import type { ReadableSpan, } from "@opentelemetry/sdk-trace-base"; import { OpenTelemetryConstants } from "../constants.js"; +import { GEN_AI_OPERATION_NAMES } from "../exporter/utils.js"; import { GENERIC_ATTRIBUTES, INVOKE_AGENT_ATTRIBUTES } from "./util.js"; -/** Known GenAI operation names used to filter spans in the processor. */ -const GENAI_OPERATION_NAMES = new Set([ - OpenTelemetryConstants.INVOKE_AGENT_OPERATION_NAME, - OpenTelemetryConstants.EXECUTE_TOOL_OPERATION_NAME, - OpenTelemetryConstants.CHAT_OPERATION_NAME, - OpenTelemetryConstants.OUTPUT_MESSAGES_OPERATION_NAME, -]); - /** * Copies relevant baggage entries to span attributes on span start. * @@ -73,7 +66,7 @@ export class A365SpanProcessor implements BaseSpanProcessor { const operationNameAttr = (span as any).attributes?.[ OpenTelemetryConstants.GEN_AI_OPERATION_NAME_KEY ]; - if (!GENAI_OPERATION_NAMES.has(operationNameAttr)) { + if (!GEN_AI_OPERATION_NAMES.has(operationNameAttr)) { return; } From 27a50edfb58b512e7bf9012d4b04328c402afb99 Mon Sep 17 00:00:00 2001 From: Peng Fan Date: Mon, 18 May 2026 15:05:15 -0700 Subject: [PATCH 5/5] fix: use operationNameAttr instead of baggage for isInvokeAgent check The isInvokeAgent check was still reading gen_ai.operation.name from baggage as primary source. Since operation name is a span attribute, reuse the already-extracted operationNameAttr variable. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/a365/processors/A365SpanProcessor.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/a365/processors/A365SpanProcessor.ts b/src/a365/processors/A365SpanProcessor.ts index 9ace1b2..792b4c8 100644 --- a/src/a365/processors/A365SpanProcessor.ts +++ b/src/a365/processors/A365SpanProcessor.ts @@ -78,15 +78,10 @@ export class A365SpanProcessor implements BaseSpanProcessor { }); // Determine if this is an invoke_agent operation - const operationName = - baggageMap.get(OpenTelemetryConstants.GEN_AI_OPERATION_NAME_KEY) || - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (span as any).attributes?.[OpenTelemetryConstants.GEN_AI_OPERATION_NAME_KEY]; - // eslint-disable-next-line @typescript-eslint/no-explicit-any const spanName = (span as any).name || ""; const isInvokeAgent = - operationName === OpenTelemetryConstants.INVOKE_AGENT_OPERATION_NAME || + operationNameAttr === OpenTelemetryConstants.INVOKE_AGENT_OPERATION_NAME || spanName.startsWith(OpenTelemetryConstants.INVOKE_AGENT_OPERATION_NAME); // Build target key set