Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/platform/otel/common/test/genAiEvents.spec.ts
13406 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { describe, expect, it, vi } from 'vitest';
7
import { Event } from '../../../../util/vs/base/common/event';
8
import { CopilotChatAttr, GenAiAttr, GenAiOperationName, StdAttr } from '../genAiAttributes';
9
import { emitAgentTurnEvent, emitEditFeedbackEvent, emitEditSurvivalEvent, emitInferenceDetailsEvent, emitSessionStartEvent, emitToolCallEvent } from '../genAiEvents';
10
import { resolveOTelConfig } from '../otelConfig';
11
import type { IOTelService } from '../otelService';
12
13
function createMockOTel(captureContent = false): IOTelService & { emitLogRecord: ReturnType<typeof vi.fn> } {
14
const config = resolveOTelConfig({
15
env: captureContent ? { 'COPILOT_OTEL_ENABLED': 'true', 'COPILOT_OTEL_CAPTURE_CONTENT': 'true' } : { 'COPILOT_OTEL_ENABLED': 'true' },
16
extensionVersion: '1.0.0',
17
sessionId: 'test',
18
});
19
return {
20
_serviceBrand: undefined!,
21
config,
22
startSpan: vi.fn(),
23
startActiveSpan: vi.fn(),
24
getActiveTraceContext: vi.fn(),
25
storeTraceContext: vi.fn(),
26
getStoredTraceContext: vi.fn(),
27
runWithTraceContext: vi.fn((_ctx: any, fn: any) => fn()),
28
recordMetric: vi.fn(),
29
incrementCounter: vi.fn(),
30
emitLogRecord: vi.fn(),
31
flush: vi.fn(),
32
shutdown: vi.fn(),
33
injectCompletedSpan: vi.fn(),
34
onDidCompleteSpan: Event.None,
35
onDidEmitSpanEvent: Event.None,
36
};
37
}
38
39
describe('emitInferenceDetailsEvent', () => {
40
it('emits event with standard attributes', () => {
41
const otel = createMockOTel();
42
emitInferenceDetailsEvent(otel,
43
{ model: 'gpt-4o', temperature: 0.7, maxTokens: 4096 },
44
{ id: 'resp-1', model: 'gpt-4o', finishReasons: ['stop'], inputTokens: 100, outputTokens: 50 },
45
);
46
47
expect(otel.emitLogRecord).toHaveBeenCalledOnce();
48
const [body, attrs] = otel.emitLogRecord.mock.calls[0];
49
expect(body).toContain('gpt-4o');
50
expect(attrs['event.name']).toBe('gen_ai.client.inference.operation.details');
51
expect(attrs[GenAiAttr.OPERATION_NAME]).toBe(GenAiOperationName.CHAT);
52
expect(attrs[GenAiAttr.REQUEST_MODEL]).toBe('gpt-4o');
53
expect(attrs[GenAiAttr.RESPONSE_MODEL]).toBe('gpt-4o');
54
expect(attrs[GenAiAttr.RESPONSE_ID]).toBe('resp-1');
55
expect(attrs[GenAiAttr.USAGE_INPUT_TOKENS]).toBe(100);
56
expect(attrs[GenAiAttr.USAGE_OUTPUT_TOKENS]).toBe(50);
57
expect(attrs[GenAiAttr.REQUEST_TEMPERATURE]).toBe(0.7);
58
expect(attrs[GenAiAttr.REQUEST_MAX_TOKENS]).toBe(4096);
59
});
60
61
it('does not include content attributes when captureContent is false', () => {
62
const otel = createMockOTel(false);
63
emitInferenceDetailsEvent(otel,
64
{ model: 'gpt-4o', messages: [{ role: 'user', text: 'secret' }] },
65
{ id: 'resp-1' },
66
);
67
68
const attrs = otel.emitLogRecord.mock.calls[0][1];
69
expect(attrs).not.toHaveProperty(GenAiAttr.INPUT_MESSAGES);
70
expect(attrs).not.toHaveProperty(GenAiAttr.SYSTEM_INSTRUCTIONS);
71
expect(attrs).not.toHaveProperty(GenAiAttr.TOOL_DEFINITIONS);
72
});
73
74
it('includes content attributes when captureContent is true', () => {
75
const otel = createMockOTel(true);
76
const messages = [{ role: 'user', content: 'hello' }];
77
const systemMsg = 'You are helpful';
78
const tools = [{ name: 'readFile' }];
79
80
emitInferenceDetailsEvent(otel,
81
{ model: 'gpt-4o', messages, systemMessage: systemMsg, tools },
82
undefined,
83
);
84
85
const attrs = otel.emitLogRecord.mock.calls[0][1];
86
// Messages should be normalized to OTel GenAI format
87
expect(attrs[GenAiAttr.INPUT_MESSAGES]).toBe(JSON.stringify([{ role: 'user', parts: [{ type: 'text', content: 'hello' }] }]));
88
// System instructions should be wrapped in OTel format
89
expect(attrs[GenAiAttr.SYSTEM_INSTRUCTIONS]).toBe(JSON.stringify([{ type: 'text', content: 'You are helpful' }]));
90
expect(attrs[GenAiAttr.TOOL_DEFINITIONS]).toBe(JSON.stringify(tools));
91
});
92
93
it('includes error.type when error is provided', () => {
94
const otel = createMockOTel();
95
emitInferenceDetailsEvent(otel,
96
{ model: 'gpt-4o' },
97
undefined,
98
{ type: 'TimeoutError', message: 'request timed out' },
99
);
100
101
const attrs = otel.emitLogRecord.mock.calls[0][1];
102
expect(attrs[StdAttr.ERROR_TYPE]).toBe('TimeoutError');
103
});
104
105
it('handles undefined response', () => {
106
const otel = createMockOTel();
107
emitInferenceDetailsEvent(otel, { model: 'gpt-4o' }, undefined);
108
109
const attrs = otel.emitLogRecord.mock.calls[0][1];
110
expect(attrs).not.toHaveProperty(GenAiAttr.RESPONSE_MODEL);
111
expect(attrs).not.toHaveProperty(GenAiAttr.RESPONSE_ID);
112
});
113
});
114
115
describe('emitSessionStartEvent', () => {
116
it('emits session start with required attributes', () => {
117
const otel = createMockOTel();
118
emitSessionStartEvent(otel, 'sess-123', 'gpt-4o', 'copilot');
119
120
expect(otel.emitLogRecord).toHaveBeenCalledWith('copilot_chat.session.start', {
121
'event.name': 'copilot_chat.session.start',
122
'session.id': 'sess-123',
123
[GenAiAttr.REQUEST_MODEL]: 'gpt-4o',
124
[GenAiAttr.AGENT_NAME]: 'copilot',
125
});
126
});
127
});
128
129
describe('emitToolCallEvent', () => {
130
it('emits success tool call event', () => {
131
const otel = createMockOTel();
132
emitToolCallEvent(otel, 'readFile', 150, true);
133
134
const [body, attrs] = otel.emitLogRecord.mock.calls[0];
135
expect(body).toContain('readFile');
136
expect(attrs['event.name']).toBe('copilot_chat.tool.call');
137
expect(attrs[GenAiAttr.TOOL_NAME]).toBe('readFile');
138
expect(attrs['duration_ms']).toBe(150);
139
expect(attrs['success']).toBe(true);
140
expect(attrs).not.toHaveProperty(StdAttr.ERROR_TYPE);
141
});
142
143
it('includes error type on failure', () => {
144
const otel = createMockOTel();
145
emitToolCallEvent(otel, 'runCommand', 5000, false, 'TimeoutError');
146
147
const attrs = otel.emitLogRecord.mock.calls[0][1];
148
expect(attrs['success']).toBe(false);
149
expect(attrs[StdAttr.ERROR_TYPE]).toBe('TimeoutError');
150
});
151
});
152
153
describe('emitAgentTurnEvent', () => {
154
it('emits turn event with all attributes', () => {
155
const otel = createMockOTel();
156
emitAgentTurnEvent(otel, 3, 500, 200, 2);
157
158
const [body, attrs] = otel.emitLogRecord.mock.calls[0];
159
expect(body).toContain('3');
160
expect(attrs['event.name']).toBe('copilot_chat.agent.turn');
161
expect(attrs['turn.index']).toBe(3);
162
expect(attrs[GenAiAttr.USAGE_INPUT_TOKENS]).toBe(500);
163
expect(attrs[GenAiAttr.USAGE_OUTPUT_TOKENS]).toBe(200);
164
expect(attrs['tool_call_count']).toBe(2);
165
});
166
});
167
168
describe('emitEditFeedbackEvent', () => {
169
it('includes workspace metadata when provided', () => {
170
const otel = createMockOTel();
171
emitEditFeedbackEvent(otel, 'accepted', 'typescript', 'copilot', 'req-1', 'agent', false, false, {
172
headBranchName: 'main',
173
headCommitHash: 'abc123',
174
remoteUrl: 'github.com/org/repo',
175
fileRelativePath: 'src/app.ts',
176
});
177
178
const attrs = otel.emitLogRecord.mock.calls[0][1];
179
expect(attrs[CopilotChatAttr.REPO_HEAD_BRANCH_NAME]).toBe('main');
180
expect(attrs[CopilotChatAttr.REPO_HEAD_COMMIT_HASH]).toBe('abc123');
181
expect(attrs[CopilotChatAttr.REPO_REMOTE_URL]).toBe('github.com/org/repo');
182
expect(attrs[CopilotChatAttr.FILE_RELATIVE_PATH]).toBe('src/app.ts');
183
});
184
});
185
186
describe('emitEditSurvivalEvent', () => {
187
it('includes workspace metadata alongside survival data', () => {
188
const otel = createMockOTel();
189
emitEditSurvivalEvent(otel, 'apply_patch', 0.95, 0.88, 30000, false, 'req-1', {
190
headBranchName: 'feature/x',
191
headCommitHash: 'deadbeef',
192
});
193
194
const attrs = otel.emitLogRecord.mock.calls[0][1];
195
expect(attrs['event.name']).toBe('copilot_chat.edit.survival');
196
expect(attrs['survival_rate_four_gram']).toBe(0.95);
197
expect(attrs[CopilotChatAttr.REPO_HEAD_BRANCH_NAME]).toBe('feature/x');
198
expect(attrs[CopilotChatAttr.REPO_HEAD_COMMIT_HASH]).toBe('deadbeef');
199
});
200
});
201
202