Path: blob/main/extensions/copilot/src/platform/otel/common/test/chatMLFetcherSpanLifecycle.spec.ts
13406 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { describe, expect, it } from 'vitest';6import { CopilotChatAttr, GenAiAttr, GenAiOperationName, GenAiProviderName } from '../genAiAttributes';7import { SpanKind, SpanStatusCode } from '../otelService';8import { CapturingOTelService } from './capturingOTelService';910/**11* Tests the two-phase span lifecycle used in chatMLFetcher:12* 1. _doFetch creates a span, returns it alongside the result13* 2. fetchMany enriches it with token usage and response data, then ends it14*15* This pattern is unique because the span is created in one method16* and ended in another — testing lifecycle correctness.17*/18describe('chatMLFetcher Span Lifecycle', () => {19it('span is created with model and conversation ID in _doFetch phase', () => {20const otel = new CapturingOTelService();2122// Phase 1: _doFetch creates the span23const span = otel.startSpan('chat gpt-4o', {24kind: SpanKind.CLIENT,25attributes: {26[GenAiAttr.OPERATION_NAME]: GenAiOperationName.CHAT,27[GenAiAttr.PROVIDER_NAME]: GenAiProviderName.GITHUB,28[GenAiAttr.REQUEST_MODEL]: 'gpt-4o',29[GenAiAttr.CONVERSATION_ID]: 'req-abc',30[GenAiAttr.REQUEST_MAX_TOKENS]: 2048,31[CopilotChatAttr.MAX_PROMPT_TOKENS]: 128000,32},33});3435const s = otel.spans[0];36expect(s.name).toBe('chat gpt-4o');37expect(s.kind).toBe(SpanKind.CLIENT);38expect(s.attributes[GenAiAttr.REQUEST_MODEL]).toBe('gpt-4o');39expect(s.attributes[GenAiAttr.CONVERSATION_ID]).toBe('req-abc');40expect(s.ended).toBe(false);4142// Phase 2: fetchMany enriches with response data43span.setAttributes({44[GenAiAttr.USAGE_INPUT_TOKENS]: 1500,45[GenAiAttr.USAGE_OUTPUT_TOKENS]: 250,46[GenAiAttr.RESPONSE_MODEL]: 'gpt-4o-2024-08-06',47[GenAiAttr.RESPONSE_ID]: 'chatcmpl-xyz',48[GenAiAttr.RESPONSE_FINISH_REASONS]: ['stop'],49[CopilotChatAttr.TIME_TO_FIRST_TOKEN]: 450,50});51span.setStatus(SpanStatusCode.OK);52span.end();5354expect(s.attributes[GenAiAttr.USAGE_INPUT_TOKENS]).toBe(1500);55expect(s.attributes[GenAiAttr.RESPONSE_MODEL]).toBe('gpt-4o-2024-08-06');56expect(s.statusCode).toBe(SpanStatusCode.OK);57expect(s.ended).toBe(true);58});5960it('span is ended on error path (not leaked)', () => {61const otel = new CapturingOTelService();6263// Phase 1: span created64const span = otel.startSpan('chat gpt-4o', {65kind: SpanKind.CLIENT,66attributes: { [GenAiAttr.OPERATION_NAME]: GenAiOperationName.CHAT },67});6869// Phase 2: error occurs — fetchMany Error path70span.setStatus(SpanStatusCode.ERROR, 'Connection reset');71span.setAttribute('error.type', 'FetchError');72span.recordException(new Error('Connection reset'));73span.end();7475const s = otel.spans[0];76expect(s.statusCode).toBe(SpanStatusCode.ERROR);77expect(s.ended).toBe(true);78expect(s.exceptions).toHaveLength(1);79});8081it('operation duration metric is recorded in _doFetch finally block', () => {82const otel = new CapturingOTelService();8384// Simulate the finally block in _doFetch85const durationSec = 3.5;86otel.recordMetric('gen_ai.client.operation.duration', durationSec, {87[GenAiAttr.OPERATION_NAME]: GenAiOperationName.CHAT,88[GenAiAttr.PROVIDER_NAME]: GenAiProviderName.GITHUB,89[GenAiAttr.REQUEST_MODEL]: 'gpt-4o',90});9192expect(otel.metrics).toHaveLength(1);93expect(otel.metrics[0].name).toBe('gen_ai.client.operation.duration');94expect(otel.metrics[0].value).toBe(3.5);95});9697it('debug name attribute is set after span is returned to fetchMany', () => {98const otel = new CapturingOTelService();99100const span = otel.startSpan('chat gpt-4o', {101kind: SpanKind.CLIENT,102attributes: { [GenAiAttr.OPERATION_NAME]: GenAiOperationName.CHAT },103});104105// fetchMany adds debug name after receiving the span from _doFetch106span.setAttribute(GenAiAttr.AGENT_NAME, 'agentMode');107span.end();108109expect(otel.spans[0].attributes[GenAiAttr.AGENT_NAME]).toBe('agentMode');110});111});112113114