豆豆友情提示:这是一个非官方 GitHub 代理镜像,主要用于网络测试或访问加速。请勿在此进行登录、注册或处理任何敏感信息。进行这些操作请务必访问官方网站 github.com。 Raw 内容也通过此代理提供。
Skip to content

Commit 111403d

Browse files
digitaraldHarald Kirschner
andauthored
Add OTel metrics for edit acceptance and code survival telemetry (#4792)
Co-authored-by: Harald Kirschner <digitarald@gmail.com>
1 parent 48f5bd9 commit 111403d

File tree

4 files changed

+121
-13
lines changed

4 files changed

+121
-13
lines changed

src/extension/conversation/vscode-node/userActions.ts

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ import { EditSurvivalResult } from '../../../platform/editSurvivalTracking/commo
1010
import { ILanguageDiagnosticsService } from '../../../platform/languages/common/languageDiagnosticsService';
1111
import { IMultiFileEditInternalTelemetryService } from '../../../platform/multiFileEdit/common/multiFileEditQualityTelemetry';
1212
import { INotebookService } from '../../../platform/notebook/common/notebookService';
13+
import { GenAiMetrics } from '../../../platform/otel/common/genAiMetrics';
14+
import type { EditOutcome } from '../../../platform/otel/common/genAiAttributes';
15+
import { IOTelService } from '../../../platform/otel/common/otelService';
1316
import { ISurveyService } from '../../../platform/survey/common/surveyService';
1417
import { ITelemetryService, TelemetryEventMeasurements, TelemetryEventProperties } from '../../../platform/telemetry/common/telemetry';
1518
import { isNotebookCellOrNotebookChatInput } from '../../../util/common/notebooks';
@@ -44,7 +47,8 @@ export class UserFeedbackService implements IUserFeedbackService {
4447
@ISurveyService private readonly surveyService: ISurveyService,
4548
@ILanguageDiagnosticsService private readonly languageDiagnosticsService: ILanguageDiagnosticsService,
4649
@IMultiFileEditInternalTelemetryService private readonly multiFileEditTelemetryService: IMultiFileEditInternalTelemetryService,
47-
@INotebookService private readonly notebookService: INotebookService
50+
@INotebookService private readonly notebookService: INotebookService,
51+
@IOTelService private readonly otelService: IOTelService
4852
) { }
4953

5054
handleUserAction(e: vscode.ChatUserActionEvent, agentId: string): void {
@@ -200,6 +204,8 @@ export class UserFeedbackService implements IUserFeedbackService {
200204
isNotebookCell: e.action.uri.scheme === Schemas.vscodeNotebookCell ? 1 : 0
201205
});
202206

207+
GenAiMetrics.recordChatEditOutcome(this.otelService, 'chat_editing', outcomes.get(e.action.outcome) ?? 'unknown', document?.languageId, e.action.hasRemainingEdits);
208+
203209
if (result.metadata?.responseId
204210
&& (e.action.outcome === vscode.ChatEditingSessionActionOutcome.Accepted
205211
|| e.action.outcome === vscode.ChatEditingSessionActionOutcome.Rejected)
@@ -234,6 +240,7 @@ export class UserFeedbackService implements IUserFeedbackService {
234240
measurements,
235241
'edit.hunk.action'
236242
);
243+
GenAiMetrics.recordEditAcceptance(this.otelService, 'chat_editing_hunk', outcome, document?.languageId);
237244
}
238245
break;
239246
}
@@ -438,7 +445,7 @@ export class UserFeedbackService implements IUserFeedbackService {
438445
};
439446

440447
if (kind === InteractiveEditorResponseFeedbackKind.Accepted && response.editSurvivalTracker) {
441-
response.editSurvivalTracker.startReporter(res => reportInlineEditSurvivalEvent(res, sharedProps, sharedMeasures));
448+
response.editSurvivalTracker.startReporter(res => reportInlineEditSurvivalEvent(res, sharedProps, sharedMeasures, this.otelService));
442449
}
443450
(response as any).editSurvivalTracker = undefined; // TODO@jrieken
444451

@@ -467,6 +474,10 @@ export class UserFeedbackService implements IUserFeedbackService {
467474
this.telemetryService.sendMSFTTelemetryEvent('inline.done', sharedProps, {
468475
...sharedMeasures, accepted
469476
});
477+
this.telemetryService.sendGHTelemetryEvent('inline.done', sharedProps, {
478+
...sharedMeasures, accepted
479+
});
480+
GenAiMetrics.recordEditAcceptance(this.otelService, 'inline_chat', accepted ? 'accepted' : 'rejected', languageId);
470481

471482
this.telemetryService.sendInternalMSFTTelemetryEvent('interactiveSessionDone', {
472483
language: languageId,
@@ -501,7 +512,14 @@ export class UserFeedbackService implements IUserFeedbackService {
501512
}
502513
}
503514

504-
function reportInlineEditSurvivalEvent(res: EditSurvivalResult, sharedProps: TelemetryEventProperties | undefined, sharedMeasures: TelemetryEventMeasurements | undefined) {
515+
function reportInlineEditSurvivalEvent(res: EditSurvivalResult, sharedProps: TelemetryEventProperties | undefined, sharedMeasures: TelemetryEventMeasurements | undefined, otelService: IOTelService) {
516+
const survivalMeasures = {
517+
...sharedMeasures,
518+
survivalRateFourGram: res.fourGram,
519+
survivalRateNoRevert: res.noRevert,
520+
timeDelayMs: res.timeDelayMs,
521+
didBranchChange: res.didBranchChange ? 1 : 0,
522+
};
505523
/* __GDPR__
506524
"inline.trackEditSurvival" : {
507525
"owner": "hediet",
@@ -526,16 +544,13 @@ function reportInlineEditSurvivalEvent(res: EditSurvivalResult, sharedProps: Tel
526544
"isNotebook": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Whether the document is a notebook" }
527545
}
528546
*/
529-
res.telemetryService.sendMSFTTelemetryEvent('inline.trackEditSurvival', sharedProps, {
530-
...sharedMeasures,
531-
survivalRateFourGram: res.fourGram,
532-
survivalRateNoRevert: res.noRevert,
533-
timeDelayMs: res.timeDelayMs,
534-
didBranchChange: res.didBranchChange ? 1 : 0,
535-
});
547+
res.telemetryService.sendMSFTTelemetryEvent('inline.trackEditSurvival', sharedProps, survivalMeasures);
548+
res.telemetryService.sendGHTelemetryEvent('inline.trackEditSurvival', sharedProps, survivalMeasures);
549+
GenAiMetrics.recordEditSurvivalFourGram(otelService, 'inline_chat', res.fourGram, res.timeDelayMs);
550+
GenAiMetrics.recordEditSurvivalNoRevert(otelService, 'inline_chat', res.noRevert, res.timeDelayMs);
536551
}
537552

538-
const outcomes = new Map([
553+
const outcomes = new Map<vscode.ChatEditingSessionActionOutcome, EditOutcome>([
539554
[vscode.ChatEditingSessionActionOutcome.Accepted, 'accepted'],
540555
[vscode.ChatEditingSessionActionOutcome.Rejected, 'rejected'],
541556
[vscode.ChatEditingSessionActionOutcome.Saved, 'saved']

src/platform/otel/common/genAiAttributes.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,21 @@ export const CopilotChatAttr = {
129129
DEBUG_LOG_LABEL: 'copilot_chat.debug_log_label',
130130
/** Markdown content for standalone content events */
131131
MARKDOWN_CONTENT: 'copilot_chat.markdown_content',
132+
/** Edit source: inline_chat, chat_editing, chat_editing_hunk */
133+
EDIT_SOURCE: 'copilot_chat.edit.source',
134+
/** Edit outcome: accepted, rejected, saved, unknown */
135+
EDIT_OUTCOME: 'copilot_chat.edit.outcome',
136+
/** Language identifier of the document */
137+
LANGUAGE_ID: 'copilot_chat.language_id',
138+
/** Time delay in milliseconds between acceptance and measurement */
139+
TIME_DELAY_MS: 'copilot_chat.time_delay_ms',
140+
/** Whether additional unactioned edits remain */
141+
HAS_REMAINING_EDITS: 'copilot_chat.has_remaining_edits',
132142
} as const;
133143

144+
export type EditSource = 'inline_chat' | 'chat_editing' | 'chat_editing_hunk';
145+
export type EditOutcome = 'accepted' | 'rejected' | 'saved' | 'unknown';
146+
134147
/**
135148
* Standard OTel attributes used alongside GenAI attributes.
136149
*/

src/platform/otel/common/genAiMetrics.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import { GenAiAttr, StdAttr } from './genAiAttributes';
6+
import { CopilotChatAttr, type EditOutcome, type EditSource, GenAiAttr, StdAttr } from './genAiAttributes';
77
import type { IOTelService } from './otelService';
88

99
/**
@@ -96,4 +96,37 @@ export class GenAiMetrics {
9696
static incrementSessionCount(otel: IOTelService): void {
9797
otel.incrementCounter('copilot_chat.session.count');
9898
}
99+
100+
// ── Edit Acceptance & Survival Metrics ──
101+
102+
static recordEditAcceptance(otel: IOTelService, source: EditSource, outcome: EditOutcome, languageId?: string): void {
103+
otel.incrementCounter('copilot_chat.edit.acceptance.count', 1, {
104+
[CopilotChatAttr.EDIT_SOURCE]: source,
105+
[CopilotChatAttr.EDIT_OUTCOME]: outcome,
106+
...(languageId ? { [CopilotChatAttr.LANGUAGE_ID]: languageId } : {}),
107+
});
108+
}
109+
110+
static recordEditSurvivalFourGram(otel: IOTelService, source: EditSource, score: number, timeDelayMs: number): void {
111+
otel.recordMetric('copilot_chat.edit.survival.four_gram', score, {
112+
[CopilotChatAttr.EDIT_SOURCE]: source,
113+
[CopilotChatAttr.TIME_DELAY_MS]: timeDelayMs,
114+
});
115+
}
116+
117+
static recordEditSurvivalNoRevert(otel: IOTelService, source: EditSource, score: number, timeDelayMs: number): void {
118+
otel.recordMetric('copilot_chat.edit.survival.no_revert', score, {
119+
[CopilotChatAttr.EDIT_SOURCE]: source,
120+
[CopilotChatAttr.TIME_DELAY_MS]: timeDelayMs,
121+
});
122+
}
123+
124+
static recordChatEditOutcome(otel: IOTelService, source: EditSource, outcome: EditOutcome, languageId?: string, hasRemainingEdits?: boolean): void {
125+
otel.incrementCounter('copilot_chat.chat_edit.outcome.count', 1, {
126+
[CopilotChatAttr.EDIT_SOURCE]: source,
127+
[CopilotChatAttr.EDIT_OUTCOME]: outcome,
128+
...(languageId ? { [CopilotChatAttr.LANGUAGE_ID]: languageId } : {}),
129+
...(hasRemainingEdits !== undefined ? { [CopilotChatAttr.HAS_REMAINING_EDITS]: hasRemainingEdits } : {}),
130+
});
131+
}
99132
}

src/platform/otel/common/test/agentTraceHierarchy.spec.ts

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { describe, expect, it } from 'vitest';
7-
import { GenAiAttr, GenAiOperationName, GenAiProviderName } from '../genAiAttributes';
7+
import { CopilotChatAttr, GenAiAttr, GenAiOperationName, GenAiProviderName } from '../genAiAttributes';
88
import { emitAgentTurnEvent, emitSessionStartEvent } from '../genAiEvents';
99
import { GenAiMetrics } from '../genAiMetrics';
1010
import { SpanKind, SpanStatusCode } from '../otelService';
@@ -223,4 +223,51 @@ describe('Agent Trace Hierarchy', () => {
223223
expect(otel.metrics[2].name).toBe('gen_ai.client.token.usage');
224224
expect(otel.metrics[2].value).toBe(250);
225225
});
226+
227+
it('records edit acceptance and survival metrics', () => {
228+
const otel = new CapturingOTelService();
229+
230+
GenAiMetrics.recordEditAcceptance(otel, 'inline_chat', 'accepted', 'typescript');
231+
GenAiMetrics.recordEditAcceptance(otel, 'chat_editing_hunk', 'rejected', 'python');
232+
GenAiMetrics.recordEditSurvivalFourGram(otel, 'inline_chat', 0.85, 30000);
233+
GenAiMetrics.recordEditSurvivalNoRevert(otel, 'inline_chat', 0.92, 30000);
234+
GenAiMetrics.recordChatEditOutcome(otel, 'chat_editing', 'accepted', 'typescript', false);
235+
236+
// Acceptance counters
237+
expect(otel.counters).toHaveLength(3);
238+
expect(otel.counters[0].name).toBe('copilot_chat.edit.acceptance.count');
239+
expect(otel.counters[0].attributes?.[CopilotChatAttr.EDIT_SOURCE]).toBe('inline_chat');
240+
expect(otel.counters[0].attributes?.[CopilotChatAttr.EDIT_OUTCOME]).toBe('accepted');
241+
expect(otel.counters[0].attributes?.[CopilotChatAttr.LANGUAGE_ID]).toBe('typescript');
242+
243+
expect(otel.counters[1].name).toBe('copilot_chat.edit.acceptance.count');
244+
expect(otel.counters[1].attributes?.[CopilotChatAttr.EDIT_OUTCOME]).toBe('rejected');
245+
246+
// Chat edit outcome counter
247+
expect(otel.counters[2].name).toBe('copilot_chat.chat_edit.outcome.count');
248+
expect(otel.counters[2].attributes?.[CopilotChatAttr.EDIT_SOURCE]).toBe('chat_editing');
249+
expect(otel.counters[2].attributes?.[CopilotChatAttr.EDIT_OUTCOME]).toBe('accepted');
250+
expect(otel.counters[2].attributes?.[CopilotChatAttr.HAS_REMAINING_EDITS]).toBe(false);
251+
252+
// Survival histograms
253+
expect(otel.metrics).toHaveLength(2);
254+
expect(otel.metrics[0].name).toBe('copilot_chat.edit.survival.four_gram');
255+
expect(otel.metrics[0].value).toBe(0.85);
256+
expect(otel.metrics[0].attributes?.[CopilotChatAttr.EDIT_SOURCE]).toBe('inline_chat');
257+
expect(otel.metrics[0].attributes?.[CopilotChatAttr.TIME_DELAY_MS]).toBe(30000);
258+
259+
expect(otel.metrics[1].name).toBe('copilot_chat.edit.survival.no_revert');
260+
expect(otel.metrics[1].value).toBe(0.92);
261+
});
262+
263+
it('omits optional attributes when undefined', () => {
264+
const otel = new CapturingOTelService();
265+
266+
GenAiMetrics.recordEditAcceptance(otel, 'inline_chat', 'accepted', undefined);
267+
GenAiMetrics.recordChatEditOutcome(otel, 'chat_editing', 'rejected', undefined, undefined);
268+
269+
expect(otel.counters[0].attributes?.[CopilotChatAttr.LANGUAGE_ID]).toBeUndefined();
270+
expect(otel.counters[1].attributes?.[CopilotChatAttr.LANGUAGE_ID]).toBeUndefined();
271+
expect(otel.counters[1].attributes?.[CopilotChatAttr.HAS_REMAINING_EDITS]).toBeUndefined();
272+
});
226273
});

0 commit comments

Comments
 (0)