From 8c02be118f015b694376affdd716ac5b5021705a Mon Sep 17 00:00:00 2001 From: Pujarini Mohapatra Date: Mon, 12 Oct 2026 21:18:56 +0100 Subject: [PATCH 1/5] Add model observation inference attributes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/a365/contracts.ts | 6 +++++ src/a365/scopes/InferenceScope.ts | 32 +++++++++++++++++++++++++- src/genai/semconv.ts | 4 ++++ test/internal/unit/a365/scopes.test.ts | 22 ++++++++++++++++++ 4 files changed, 63 insertions(+), 1 deletion(-) diff --git a/src/a365/contracts.ts b/src/a365/contracts.ts index d2382a1d7..f0b243c28 100644 --- a/src/a365/contracts.ts +++ b/src/a365/contracts.ts @@ -469,6 +469,12 @@ export interface InferenceDetails { inputTokens?: number; /** Number of output (completion) tokens. */ outputTokens?: number; + /** Canonical model name returned by the provider. */ + responseModel?: string; + /** Number of input tokens read from cache. */ + cacheReadInputTokens?: number; + /** Number of input tokens written to cache. */ + cacheWriteInputTokens?: number; /** Finish reasons returned by the model. */ finishReasons?: string[]; /** The model's thought process / reasoning trace. */ diff --git a/src/a365/scopes/InferenceScope.ts b/src/a365/scopes/InferenceScope.ts index b75ec5e28..19088beb7 100644 --- a/src/a365/scopes/InferenceScope.ts +++ b/src/a365/scopes/InferenceScope.ts @@ -52,7 +52,7 @@ export class InferenceScope extends OpenTelemetryScope { const resolvedSpanDetails: SpanDetails = { ...spanDetails, spanKind: SpanKind.CLIENT }; super( - details.operationName.toString(), + details.operationName.toString().toLowerCase(), `${details.operationName} ${details.model}`, agentDetails, resolvedSpanDetails, @@ -62,9 +62,18 @@ export class InferenceScope extends OpenTelemetryScope { // Core inference information this.setTagMaybe(OpenTelemetryConstants.GEN_AI_REQUEST_MODEL_KEY, details.model); + this.setTagMaybe(OpenTelemetryConstants.GEN_AI_RESPONSE_MODEL_KEY, details.responseModel); this.setTagMaybe(OpenTelemetryConstants.GEN_AI_PROVIDER_NAME_KEY, details.providerName); this.setTagMaybe(OpenTelemetryConstants.GEN_AI_USAGE_INPUT_TOKENS_KEY, details.inputTokens); this.setTagMaybe(OpenTelemetryConstants.GEN_AI_USAGE_OUTPUT_TOKENS_KEY, details.outputTokens); + this.setTagMaybe( + OpenTelemetryConstants.GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS_KEY, + details.cacheReadInputTokens, + ); + this.setTagMaybe( + OpenTelemetryConstants.GEN_AI_USAGE_CACHE_WRITE_INPUT_TOKENS_KEY, + details.cacheWriteInputTokens, + ); this.setTagMaybe( OpenTelemetryConstants.GEN_AI_RESPONSE_FINISH_REASONS_KEY, details.finishReasons, @@ -93,6 +102,27 @@ export class InferenceScope extends OpenTelemetryScope { this.setTagMaybe(OpenTelemetryConstants.GEN_AI_USAGE_OUTPUT_TOKENS_KEY, outputTokens); } + /** Records the canonical model name returned by the provider. */ + public recordResponseModel(responseModel: string): void { + this.setTagMaybe(OpenTelemetryConstants.GEN_AI_RESPONSE_MODEL_KEY, responseModel); + } + + /** Records the number of input tokens read from cache. */ + public recordCacheReadInputTokens(cacheReadInputTokens: number): void { + this.setTagMaybe( + OpenTelemetryConstants.GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS_KEY, + cacheReadInputTokens, + ); + } + + /** Records the number of input tokens written to cache. */ + public recordCacheWriteInputTokens(cacheWriteInputTokens: number): void { + this.setTagMaybe( + OpenTelemetryConstants.GEN_AI_USAGE_CACHE_WRITE_INPUT_TOKENS_KEY, + cacheWriteInputTokens, + ); + } + /** Records the finish reasons. */ public recordFinishReasons(finishReasons: string[]): void { if (finishReasons && finishReasons.length > 0) { diff --git a/src/genai/semconv.ts b/src/genai/semconv.ts index 0936a3469..c3447c107 100644 --- a/src/genai/semconv.ts +++ b/src/genai/semconv.ts @@ -49,6 +49,10 @@ export const ATTR_GEN_AI_OUTPUT_MESSAGES = "gen_ai.output.messages" as const; // GenAI usage export const ATTR_GEN_AI_USAGE_INPUT_TOKENS = "gen_ai.usage.input_tokens" as const; export const ATTR_GEN_AI_USAGE_OUTPUT_TOKENS = "gen_ai.usage.output_tokens" as const; +export const ATTR_GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS = + "gen_ai.usage.cache_read.input_tokens" as const; +export const ATTR_GEN_AI_USAGE_CACHE_WRITE_INPUT_TOKENS = + "gen_ai.usage.cache_write.input_tokens" as const; // GenAI agent export const ATTR_GEN_AI_AGENT_ID = "gen_ai.agent.id" as const; diff --git a/test/internal/unit/a365/scopes.test.ts b/test/internal/unit/a365/scopes.test.ts index 9b0ccd4da..b209a5af9 100644 --- a/test/internal/unit/a365/scopes.test.ts +++ b/test/internal/unit/a365/scopes.test.ts @@ -751,6 +751,9 @@ describe("Scopes", () => { providerName: "openai", inputTokens: 100, outputTokens: 150, + responseModel: "gpt-4-2024-11-20", + cacheReadInputTokens: 12, + cacheWriteInputTokens: 7, finishReasons: ["stop"], }; @@ -783,6 +786,22 @@ describe("Scopes", () => { expect(keySet).toContain("user.name"); expect(keySet).toContain("user.email"); expect(keySet).toContain("client.address"); + expect(calls).toEqual( + expect.arrayContaining([ + { + key: OpenTelemetryConstants.GEN_AI_RESPONSE_MODEL_KEY, + val: "gpt-4-2024-11-20", + }, + { + key: OpenTelemetryConstants.GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS_KEY, + val: 12, + }, + { + key: OpenTelemetryConstants.GEN_AI_USAGE_CACHE_WRITE_INPUT_TOKENS_KEY, + val: 7, + }, + ]), + ); scope?.dispose(); spy.mockRestore(); }); @@ -834,6 +853,9 @@ describe("Scopes", () => { expect(() => scope?.recordOutputMessages(["Generated response"])).not.toThrow(); expect(() => scope?.recordInputTokens(50)).not.toThrow(); expect(() => scope?.recordOutputTokens(100)).not.toThrow(); + expect(() => scope?.recordResponseModel("gpt-4-2024-11-20")).not.toThrow(); + expect(() => scope?.recordCacheReadInputTokens(12)).not.toThrow(); + expect(() => scope?.recordCacheWriteInputTokens(7)).not.toThrow(); expect(() => scope?.recordFinishReasons(["stop", "length"])).not.toThrow(); scope?.dispose(); }); From ca714562d7d93dba7ffa97929e918bae4679370a Mon Sep 17 00:00:00 2001 From: Pujarini Mohapatra Date: Tue, 13 Oct 2026 10:17:48 +0100 Subject: [PATCH 2/5] Document Model Observation attributes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93656d652..6cb3c67fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## [Unreleased] +### Features Added +- Add privacy-safe Model Observation attributes to `InferenceScope`, including the canonical served response model and cache read/write input-token counts. Attributes can be supplied when the scope starts or recorded after the response is available; operation names are normalized to lowercase and cache-token semantic-convention constants are exported. [#261](https://github.com/microsoft/opentelemetry-distro-javascript/pull/261) + ## [1.5.0] - 2026-10-06 ### Bugs Fixed From cc2a4de14df076d997cf11d9dc616d8af27b70e7 Mon Sep 17 00:00:00 2001 From: Pujarini Mohapatra Date: Tue, 13 Oct 2026 10:36:31 +0100 Subject: [PATCH 3/5] Fix inference operation normalization Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/a365/exporter/utils.ts | 10 +++++--- src/a365/scopes/InferenceScope.ts | 14 ++++++++++- src/genai/semconv.ts | 2 ++ .../unit/a365/agent365Exporter.test.ts | 16 +++++++++++++ test/internal/unit/a365/scopes.test.ts | 24 +++++++++++++++++++ 5 files changed, 62 insertions(+), 4 deletions(-) diff --git a/src/a365/exporter/utils.ts b/src/a365/exporter/utils.ts index 399f42dfd..0e3bf65df 100644 --- a/src/a365/exporter/utils.ts +++ b/src/a365/exporter/utils.ts @@ -14,9 +14,11 @@ import { ATTR_MICROSOFT_TENANT_ID, GEN_AI_OPERATION_APPLY_GUARDRAIL, GEN_AI_OPERATION_CHAT, + GEN_AI_OPERATION_GENERATE_CONTENT, GEN_AI_OPERATION_EXECUTE_TOOL, GEN_AI_OPERATION_INVOKE_AGENT, GEN_AI_OPERATION_OUTPUT_MESSAGES, + GEN_AI_OPERATION_TEXT_COMPLETION, } from "../../genai/semconv.js"; // Message attribute keys that receive special truncation handling const MESSAGE_ATTR_KEYS: Set = new Set([ @@ -36,9 +38,11 @@ export const GEN_AI_OPERATION_NAMES: ReadonlySet = new Set([ GEN_AI_OPERATION_OUTPUT_MESSAGES, // 'output_messages' GEN_AI_OPERATION_APPLY_GUARDRAIL, // 'apply_guardrail' GEN_AI_OPERATION_CHAT, // 'chat' - "Chat", // InferenceOperationType.CHAT - "TextCompletion", // InferenceOperationType.TEXT_COMPLETION - "GenerateContent", // InferenceOperationType.GENERATE_CONTENT + GEN_AI_OPERATION_TEXT_COMPLETION, // 'text_completion' + GEN_AI_OPERATION_GENERATE_CONTENT, // 'generate_content' + "Chat", // Legacy InferenceOperationType.CHAT value + "TextCompletion", // Legacy InferenceOperationType.TEXT_COMPLETION value + "GenerateContent", // Legacy InferenceOperationType.GENERATE_CONTENT value ]); /** diff --git a/src/a365/scopes/InferenceScope.ts b/src/a365/scopes/InferenceScope.ts index 19088beb7..0b2b5f985 100644 --- a/src/a365/scopes/InferenceScope.ts +++ b/src/a365/scopes/InferenceScope.ts @@ -4,6 +4,7 @@ import { SpanKind } from "@opentelemetry/api"; import { OpenTelemetryScope } from "./OpenTelemetryScope.js"; import { OpenTelemetryConstants } from "../constants.js"; +import { InferenceOperationType } from "../contracts.js"; import type { InferenceDetails, AgentDetails, @@ -52,7 +53,7 @@ export class InferenceScope extends OpenTelemetryScope { const resolvedSpanDetails: SpanDetails = { ...spanDetails, spanKind: SpanKind.CLIENT }; super( - details.operationName.toString().toLowerCase(), + getOperationName(details.operationName), `${details.operationName} ${details.model}`, agentDetails, resolvedSpanDetails, @@ -90,6 +91,17 @@ export class InferenceScope extends OpenTelemetryScope { this.setTagMaybe(OpenTelemetryConstants.SERVER_PORT_KEY, details.endpoint.port); } } + + function getOperationName(operationName: InferenceOperationType): string { + switch (operationName) { + case InferenceOperationType.CHAT: + return "chat"; + case InferenceOperationType.TEXT_COMPLETION: + return "text_completion"; + case InferenceOperationType.GENERATE_CONTENT: + return "generate_content"; + } + } } /** Records the number of input tokens. */ diff --git a/src/genai/semconv.ts b/src/genai/semconv.ts index c3447c107..e551d65dc 100644 --- a/src/genai/semconv.ts +++ b/src/genai/semconv.ts @@ -17,6 +17,8 @@ export const GEN_AI_OPERATION_EXECUTE_TOOL = "execute_tool" as const; export const GEN_AI_OPERATION_OUTPUT_MESSAGES = "output_messages" as const; export const GEN_AI_OPERATION_APPLY_GUARDRAIL = "apply_guardrail" as const; export const GEN_AI_OPERATION_CHAT = "chat" as const; +export const GEN_AI_OPERATION_TEXT_COMPLETION = "text_completion" as const; +export const GEN_AI_OPERATION_GENERATE_CONTENT = "generate_content" as const; // --- Attributes (ATTR_ prefix, following OTel convention) --- diff --git a/test/internal/unit/a365/agent365Exporter.test.ts b/test/internal/unit/a365/agent365Exporter.test.ts index 1712e74bb..e3c5563ac 100644 --- a/test/internal/unit/a365/agent365Exporter.test.ts +++ b/test/internal/unit/a365/agent365Exporter.test.ts @@ -1806,6 +1806,22 @@ describe("Exporter utils", () => { assert.strictEqual(groups.get("t2:a2")?.length, 1); }); + it("should retain inference spans with canonical multiword operation names", () => { + const spans = ["text_completion", "generate_content"].map((operationName) => + makeSpan({ + attributes: { + "microsoft.tenant.id": "t1", + "gen_ai.agent.id": "a1", + "gen_ai.operation.name": operationName, + }, + }), + ); + + const groups = partitionByIdentity(spans); + + assert.strictEqual(groups.get("t1:a1")?.length, 2); + }); + it("should skip spans without identity attributes", () => { const spans = [ makeSpan({ attributes: { "gen_ai.operation.name": "invoke_agent" } }), diff --git a/test/internal/unit/a365/scopes.test.ts b/test/internal/unit/a365/scopes.test.ts index b209a5af9..8614606d8 100644 --- a/test/internal/unit/a365/scopes.test.ts +++ b/test/internal/unit/a365/scopes.test.ts @@ -858,6 +858,30 @@ describe("Scopes", () => { expect(() => scope?.recordCacheWriteInputTokens(7)).not.toThrow(); expect(() => scope?.recordFinishReasons(["stop", "length"])).not.toThrow(); scope?.dispose(); + + const spans = sharedExporter.getFinishedSpans(); + const attributes = spans[spans.length - 1].attributes; + expect(attributes[OpenTelemetryConstants.GEN_AI_RESPONSE_MODEL_KEY]).toBe("gpt-4-2024-11-20"); + expect(attributes[OpenTelemetryConstants.GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS_KEY]).toBe(12); + expect(attributes[OpenTelemetryConstants.GEN_AI_USAGE_CACHE_WRITE_INPUT_TOKENS_KEY]).toBe(7); + }); + + it.each([ + [InferenceOperationType.CHAT, "chat"], + [InferenceOperationType.TEXT_COMPLETION, "text_completion"], + [InferenceOperationType.GENERATE_CONTENT, "generate_content"], + ])("should map %s to canonical operation name %s", (operationName, expected) => { + const scope = InferenceScope.start( + testRequest, + { operationName, model: "gpt-4" }, + testAgentDetails, + ); + scope.dispose(); + + const spans = sharedExporter.getFinishedSpans(); + expect( + spans[spans.length - 1].attributes[OpenTelemetryConstants.GEN_AI_OPERATION_NAME_KEY], + ).toBe(expected); }); it("should set conversationId and channel tags when provided", () => { From ae0c7c66b81ec86fd299dd1fd834af2bf0071f91 Mon Sep 17 00:00:00 2001 From: Pujarini Mohapatra Date: Thu, 15 Oct 2026 10:26:05 +0100 Subject: [PATCH 4/5] Document canonical text_completion/generate_content operation names Add the two missing canonical operation values to the baggage enrichment allowlist documentation so it matches GEN_AI_OPERATION_NAMES. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- A365_DOCUMENTATION.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/A365_DOCUMENTATION.md b/A365_DOCUMENTATION.md index f90ed5aab..eb28f560c 100644 --- a/A365_DOCUMENTATION.md +++ b/A365_DOCUMENTATION.md @@ -203,7 +203,8 @@ baggageScope.run(() => { baggage only. - Automatic baggage-to-span enrichment only runs for recognized GenAI spans whose `gen_ai.operation.name` is `invoke_agent`, `execute_tool`, `output_messages`, - `apply_guardrail`, `chat`, `Chat`, `TextCompletion`, or `GenerateContent`. + `apply_guardrail`, `chat`, `text_completion`, `generate_content`, `Chat`, `TextCompletion`, or + `GenerateContent`. - For the built-in LangChain and OpenAI Agents instrumentations, enrichment also recognizes their exact instrumentation scope names when the final GenAI operation is not available at span start. Configured custom tracer names, scope prefixes, and unrelated child scopes are not From 43e8a2e422336a47f0d9c6a506915a2ca5fed107 Mon Sep 17 00:00:00 2001 From: Pujarini Mohapatra Date: Mon, 19 Oct 2026 11:38:39 +0100 Subject: [PATCH 5/5] Fix inference operation normalization and span naming Accept canonical inference strings alongside enum values and resolve the operation once for both the attribute and span name. Cover all supported inputs through scope creation and export partitioning, and document the behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- A365_DOCUMENTATION.md | 5 ++++ CHANGELOG.md | 2 +- src/a365/contracts.ts | 4 +-- src/a365/scopes/InferenceScope.ts | 9 ++++-- test/internal/unit/a365/scopes.test.ts | 40 +++++++++++++++++--------- 5 files changed, 41 insertions(+), 19 deletions(-) diff --git a/A365_DOCUMENTATION.md b/A365_DOCUMENTATION.md index eb28f560c..1ca1e6fbf 100644 --- a/A365_DOCUMENTATION.md +++ b/A365_DOCUMENTATION.md @@ -139,6 +139,11 @@ When you provide it, those scopes write `microsoft.session.id` directly on the c span instead of relying on later baggage enrichment. `OutputScope` does not currently propagate `request.sessionId` directly. +`InferenceScope` accepts `InferenceOperationType` enum values or the canonical strings +`chat`, `text_completion`, and `generate_content` in `details.operationName`. Both +`gen_ai.operation.name` and the span name use the canonical operation; for example, +`InferenceOperationType.TEXT_COMPLETION` with model `gpt-4` produces `text_completion gpt-4`. + `InvokeAgentScope.start()` captures request parameters immediately, while `recordResponseParameters()` captures response and usage values after the agent completes. diff --git a/CHANGELOG.md b/CHANGELOG.md index 6cb3c67fe..55d79c752 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ ## [Unreleased] ### Features Added -- Add privacy-safe Model Observation attributes to `InferenceScope`, including the canonical served response model and cache read/write input-token counts. Attributes can be supplied when the scope starts or recorded after the response is available; operation names are normalized to lowercase and cache-token semantic-convention constants are exported. [#261](https://github.com/microsoft/opentelemetry-distro-javascript/pull/261) +- Add privacy-safe Model Observation attributes to `InferenceScope`, including the canonical served response model and cache read/write input-token counts. Attributes can be supplied when the scope starts or recorded after the response is available; inference operations accept enum values or canonical strings and use canonical OpenTelemetry names for both operation attributes and span names. Cache-token semantic-convention constants are exported. [#261](https://github.com/microsoft/opentelemetry-distro-javascript/pull/261) ## [1.5.0] - 2026-10-06 diff --git a/src/a365/contracts.ts b/src/a365/contracts.ts index f0b243c28..5058d3252 100644 --- a/src/a365/contracts.ts +++ b/src/a365/contracts.ts @@ -459,8 +459,8 @@ export interface ToolCallDetails { /** Details for an inference call. */ export interface InferenceDetails { - /** The inference operation type (e.g. chat, text completion). */ - operationName: InferenceOperationType; + /** The inference operation enum value or canonical OpenTelemetry operation name. */ + operationName: InferenceOperationType | "chat" | "text_completion" | "generate_content"; /** Name of the model used for inference. */ model: string; /** Name of the provider serving the model. */ diff --git a/src/a365/scopes/InferenceScope.ts b/src/a365/scopes/InferenceScope.ts index 0b2b5f985..9be85a2a7 100644 --- a/src/a365/scopes/InferenceScope.ts +++ b/src/a365/scopes/InferenceScope.ts @@ -51,10 +51,11 @@ export class InferenceScope extends OpenTelemetryScope { // spanKind for InferenceScope is always CLIENT const resolvedSpanDetails: SpanDetails = { ...spanDetails, spanKind: SpanKind.CLIENT }; + const operationName = getOperationName(details.operationName); super( - getOperationName(details.operationName), - `${details.operationName} ${details.model}`, + operationName, + `${operationName} ${details.model}`, agentDetails, resolvedSpanDetails, userDetails, @@ -92,7 +93,7 @@ export class InferenceScope extends OpenTelemetryScope { } } - function getOperationName(operationName: InferenceOperationType): string { + function getOperationName(operationName: InferenceDetails["operationName"]): string { switch (operationName) { case InferenceOperationType.CHAT: return "chat"; @@ -100,6 +101,8 @@ export class InferenceScope extends OpenTelemetryScope { return "text_completion"; case InferenceOperationType.GENERATE_CONTENT: return "generate_content"; + default: + return operationName; } } } diff --git a/test/internal/unit/a365/scopes.test.ts b/test/internal/unit/a365/scopes.test.ts index 8614606d8..a68392b26 100644 --- a/test/internal/unit/a365/scopes.test.ts +++ b/test/internal/unit/a365/scopes.test.ts @@ -36,6 +36,7 @@ import type { } from "../../../../src/a365/index.js"; import { InferenceOperationType, MessageRole } from "../../../../src/a365/index.js"; import { safeSerializeToJson } from "../../../../src/a365/message-utils.js"; +import { partitionByIdentity } from "../../../../src/a365/exporter/utils.js"; let sharedExporter: InMemorySpanExporter; let contextManager: AsyncLocalStorageContextManager; @@ -866,23 +867,36 @@ describe("Scopes", () => { expect(attributes[OpenTelemetryConstants.GEN_AI_USAGE_CACHE_WRITE_INPUT_TOKENS_KEY]).toBe(7); }); - it.each([ + it.each<[InferenceDetails["operationName"], string]>([ [InferenceOperationType.CHAT, "chat"], [InferenceOperationType.TEXT_COMPLETION, "text_completion"], [InferenceOperationType.GENERATE_CONTENT, "generate_content"], - ])("should map %s to canonical operation name %s", (operationName, expected) => { - const scope = InferenceScope.start( - testRequest, - { operationName, model: "gpt-4" }, - testAgentDetails, - ); - scope.dispose(); + ["chat", "chat"], + ["text_completion", "text_completion"], + ["generate_content", "generate_content"], + ])( + "should normalize %s to %s and retain the inference span for export", + (operationName, expected) => { + const spanCountBefore = sharedExporter.getFinishedSpans().length; + const scope = InferenceScope.start( + testRequest, + { operationName, model: "gpt-4" }, + testAgentDetails, + ); + scope.dispose(); - const spans = sharedExporter.getFinishedSpans(); - expect( - spans[spans.length - 1].attributes[OpenTelemetryConstants.GEN_AI_OPERATION_NAME_KEY], - ).toBe(expected); - }); + const spans = sharedExporter.getFinishedSpans(); + expect(spans).toHaveLength(spanCountBefore + 1); + const span = spans[spans.length - 1]; + expect(span.attributes[OpenTelemetryConstants.GEN_AI_OPERATION_NAME_KEY]).toBe(expected); + expect(span.name).toBe(`${expected} gpt-4`); + const groups = partitionByIdentity([span]); + expect(groups.size).toBe(1); + expect(groups.get(`${testAgentDetails.tenantId}:${testAgentDetails.agentId}`)).toEqual([ + span, + ]); + }, + ); it("should set conversationId and channel tags when provided", () => { const spy = vi.spyOn(OpenTelemetryScope.prototype as any, "setTagMaybe");