Skip to content

Commit 95b1bbf

Browse files
authored
Fix telemetry (#1191)
1 parent 19f692f commit 95b1bbf

File tree

5 files changed

+131
-46
lines changed

5 files changed

+131
-46
lines changed

chat-lib/package-lock.json

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

chat-lib/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@vscode/chat-lib",
3-
"version": "0.0.3",
3+
"version": "0.0.4",
44
"description": "Chat and inline editing SDK extracted from VS Code Copilot Chat",
55
"main": "dist/src/main.js",
66
"types": "dist/src/main.d.ts",
@@ -15,7 +15,7 @@
1515
},
1616
"dependencies": {
1717
"@microsoft/tiktokenizer": "^1.0.10",
18-
"@vscode/copilot-api": "^0.1.8",
18+
"@vscode/copilot-api": "^0.1.10",
1919
"@vscode/l10n": "^0.0.18",
2020
"@vscode/prompt-tsx": "^0.4.0-alpha.5",
2121
"jsonc-parser": "^3.3.1",

chat-lib/test/nesProvider.spec.ts

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ import { FetchOptions, IAbortController, IHeaders, Response } from '../src/_inte
1818
import { IFetcher } from '../src/_internal/platform/networking/common/networking';
1919
import { CancellationToken } from '../src/_internal/util/vs/base/common/cancellation';
2020
import { URI } from '../src/_internal/util/vs/base/common/uri';
21-
import { StringEdit } from '../src/_internal/util/vs/editor/common/core/edits/stringEdit';
21+
import { StringEdit, StringReplacement } from '../src/_internal/util/vs/editor/common/core/edits/stringEdit';
2222
import { OffsetRange } from '../src/_internal/util/vs/editor/common/core/ranges/offsetRange';
23-
import { createNESProvider, ITelemetrySender } from '../src/main';
23+
import { createNESProvider, ILogTarget, ITelemetrySender, LogLevel } from '../src/main';
2424
import { ICopilotTokenManager } from '../src/_internal/platform/authentication/common/copilotTokenManager';
2525
import { Emitter } from '../src/_internal/util/vs/base/common/event';
2626
import { CopilotToken } from '../src/_internal/platform/authentication/common/copilotToken';
@@ -97,8 +97,17 @@ class TestCopilotTokenManager implements ICopilotTokenManager {
9797
}
9898

9999
class TestTelemetrySender implements ITelemetrySender {
100+
events: { eventName: string; properties?: Record<string, string | undefined>; measurements?: Record<string, number | undefined> }[] = [];
100101
sendTelemetryEvent(eventName: string, properties?: Record<string, string | undefined>, measurements?: Record<string, number | undefined>): void {
101-
// No-op
102+
this.events.push({ eventName, properties, measurements });
103+
}
104+
}
105+
106+
class TestLogTarget implements ILogTarget {
107+
logs: { level: LogLevel; message: string; metadata?: any }[] = [];
108+
logIt(level: LogLevel, metadataStr: string, ...extra: any[]): void {
109+
this.logs.push({ level, message: metadataStr, metadata: extra });
110+
console.log(`[${LogLevel[level]}]${metadataStr}`, ...extra);
102111
}
103112
}
104113

@@ -121,20 +130,26 @@ describe('NESProvider Facade', () => {
121130
const myPoint = new Point(0, 1);`.trimStart()
122131
});
123132
doc.setSelection([new OffsetRange(1, 1)], undefined);
133+
const telemetrySender = new TestTelemetrySender();
134+
const logTarget = new TestLogTarget();
124135
const nextEditProvider = createNESProvider({
125136
workspace,
126137
fetcher: new TestFetcher({ '/chat/completions': await fs.readFile(path.join(__dirname, 'nesProvider.reply.txt'), 'utf8') }),
127138
copilotTokenManager: new TestCopilotTokenManager(),
128-
telemetrySender: new TestTelemetrySender(),
139+
telemetrySender,
140+
logTarget,
129141
});
130142

131143
doc.applyEdit(StringEdit.insert(11, '3D'));
132144

133145
const result = await nextEditProvider.getNextEdit(doc.id.toUri(), CancellationToken.None);
134146

135-
assert(result.result?.edit);
147+
assert(result.result);
136148

137-
doc.applyEdit(result.result.edit.toEdit());
149+
const { range, newText } = result.result;
150+
const offsetRange = OffsetRange.fromTo(range.start, range.endExclusive);
151+
const replace = StringReplacement.replace(offsetRange, newText);
152+
doc.applyEdit(replace.toEdit());
138153

139154
expect(doc.value.get().value).toMatchInlineSnapshot(`
140155
"class Point3D {
@@ -150,5 +165,16 @@ describe('NESProvider Facade', () => {
150165
151166
const myPoint = new Point(0, 1);"
152167
`);
168+
169+
nextEditProvider.handleAcceptance(result);
170+
await new Promise(resolve => setTimeout(resolve, 100)); // wait for async telemetry sending
171+
const event = telemetrySender.events.find(e => e.eventName === 'copilot-nes/provideInlineEdit');
172+
expect(event).toBeDefined();
173+
expect(event!.properties?.acceptance).toBe('accepted');
174+
175+
nextEditProvider.dispose();
176+
177+
expect(logTarget.logs.length).toBeGreaterThan(0);
178+
expect(logTarget.logs.filter(l => l.level === LogLevel.Error)).toHaveLength(0);
153179
});
154180
});

src/extension/inlineEdits/node/nextEditProviderTelemetry.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -451,7 +451,7 @@ export class NextEditProviderTelemetryBuilder extends Disposable {
451451

452452
constructor(
453453
gitExtensionService: IGitExtensionService,
454-
notebookService: INotebookService,
454+
notebookService: INotebookService | undefined,
455455
workspaceService: IWorkspaceService,
456456
providerId: string,
457457
doc: IObservableDocument,

src/lib/node/chatLibMain.ts

Lines changed: 90 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import type * as vscode from 'vscode';
77
import { DebugRecorder } from '../../extension/inlineEdits/node/debugRecorder';
88
import { INextEditProvider, NextEditProvider } from '../../extension/inlineEdits/node/nextEditProvider';
9-
import { LlmNESTelemetryBuilder } from '../../extension/inlineEdits/node/nextEditProviderTelemetry';
9+
import { LlmNESTelemetryBuilder, NextEditProviderTelemetryBuilder, TelemetrySender } from '../../extension/inlineEdits/node/nextEditProviderTelemetry';
1010
import { INextEditResult } from '../../extension/inlineEdits/node/nextEditResult';
1111
import { ChatMLFetcherImpl } from '../../extension/prompt/node/chatMLFetcher';
1212
import { XtabProvider } from '../../extension/xtab/node/xtabProvider';
@@ -115,13 +115,41 @@ export interface INESProviderOptions {
115115
readonly logTarget?: ILogTarget;
116116
}
117117

118-
export function createNESProvider(options: INESProviderOptions): INESProvider {
118+
export interface INESResult {
119+
readonly result?: {
120+
readonly newText: string;
121+
readonly range: {
122+
readonly start: number;
123+
readonly endExclusive: number;
124+
};
125+
};
126+
}
127+
128+
export interface INESProvider<T extends INESResult = INESResult> {
129+
getId(): string;
130+
getNextEdit(documentUri: vscode.Uri, cancellationToken: CancellationToken): Promise<T>;
131+
handleShown(suggestion: T): void;
132+
handleAcceptance(suggestion: T): void;
133+
handleRejection(suggestion: T): void;
134+
handleIgnored(suggestion: T, supersededByRequestUuid: T | undefined): void;
135+
dispose(): void;
136+
}
137+
138+
export function createNESProvider(options: INESProviderOptions): INESProvider<INESResult> {
119139
const instantiationService = setupServices(options);
120140
return instantiationService.createInstance(NESProvider, options);
121141
}
122142

123-
class NESProvider extends Disposable implements INESProvider {
143+
interface NESResult extends INESResult {
144+
docId: DocumentId;
145+
requestUuid: string;
146+
internalResult: INextEditResult;
147+
telemetryBuilder: NextEditProviderTelemetryBuilder;
148+
}
149+
150+
class NESProvider extends Disposable implements INESProvider<NESResult> {
124151
private readonly _nextEditProvider: INextEditProvider<INextEditResult, LlmNESTelemetryBuilder>;
152+
private readonly _telemetrySender: TelemetrySender;
125153
private readonly _debugRecorder: DebugRecorder;
126154

127155
constructor(
@@ -140,29 +168,49 @@ class NESProvider extends Disposable implements INESProvider {
140168
this._debugRecorder = this._register(new DebugRecorder(this._options.workspace));
141169

142170
this._nextEditProvider = instantiationService.createInstance(NextEditProvider, this._options.workspace, statelessNextEditProvider, historyContextProvider, xtabHistoryTracker, this._debugRecorder);
171+
this._telemetrySender = this._register(instantiationService.createInstance(TelemetrySender));
143172
}
144173

145174
getId(): string {
146175
return this._nextEditProvider.ID;
147176
}
148177

149-
handleShown(suggestion: INextEditResult): void {
150-
this._nextEditProvider.handleShown(suggestion);
178+
handleShown(result: NESResult): void {
179+
result.telemetryBuilder.setAsShown();
180+
this._nextEditProvider.handleShown(result.internalResult);
151181
}
152182

153-
handleAcceptance(docId: DocumentId, suggestion: INextEditResult): void {
154-
this._nextEditProvider.handleAcceptance(docId, suggestion);
183+
handleAcceptance(result: NESResult): void {
184+
result.telemetryBuilder.setAcceptance('accepted');
185+
result.telemetryBuilder.setStatus('accepted');
186+
this._nextEditProvider.handleAcceptance(result.docId, result.internalResult);
187+
this.handleEndOfLifetime(result);
155188
}
156189

157-
handleRejection(docId: DocumentId, suggestion: INextEditResult): void {
158-
this._nextEditProvider.handleRejection(docId, suggestion);
190+
handleRejection(result: NESResult): void {
191+
result.telemetryBuilder.setAcceptance('rejected');
192+
result.telemetryBuilder.setStatus('rejected');
193+
this._nextEditProvider.handleRejection(result.docId, result.internalResult);
194+
this.handleEndOfLifetime(result);
159195
}
160196

161-
handleIgnored(docId: DocumentId, suggestion: INextEditResult, supersededByRequestUuid: INextEditResult | undefined): void {
162-
this._nextEditProvider.handleIgnored(docId, suggestion, supersededByRequestUuid);
197+
handleIgnored(result: NESResult, supersededByRequestUuid: NESResult | undefined): void {
198+
if (supersededByRequestUuid) {
199+
result.telemetryBuilder.setSupersededBy(supersededByRequestUuid.requestUuid);
200+
}
201+
this._nextEditProvider.handleIgnored(result.docId, result.internalResult, supersededByRequestUuid?.internalResult);
202+
this.handleEndOfLifetime(result);
163203
}
164204

165-
async getNextEdit(documentUri: vscode.Uri, cancellationToken: CancellationToken): Promise<INextEditResult> {
205+
private handleEndOfLifetime(result: NESResult): void {
206+
try {
207+
this._telemetrySender.sendTelemetryForBuilder(result.telemetryBuilder);
208+
} finally {
209+
result.telemetryBuilder.dispose();
210+
}
211+
}
212+
213+
async getNextEdit(documentUri: vscode.Uri, cancellationToken: CancellationToken): Promise<NESResult> {
166214
const docId = DocumentId.create(documentUri.toString());
167215

168216
// Create minimal required context objects
@@ -183,30 +231,41 @@ class NESProvider extends Disposable implements INESProvider {
183231
}
184232

185233
// Create telemetry builder - we'll need to pass null/undefined for services we don't have
186-
const telemetryBuilder = new LlmNESTelemetryBuilder(
187-
new NullGitExtensionService(), // IGitExtensionService
234+
const telemetryBuilder = new NextEditProviderTelemetryBuilder(
235+
new NullGitExtensionService(),
188236
undefined, // INotebookService
189-
this._workspaceService, // IWorkspaceService
190-
this._nextEditProvider.ID, // providerId
191-
document, // doc
192-
this._debugRecorder, // debugRecorder
193-
undefined // requestBookmark
237+
this._workspaceService,
238+
this._nextEditProvider.ID,
239+
document,
240+
this._debugRecorder,
241+
logContext.recordingBookmark
194242
);
195-
196-
return await this._nextEditProvider.getNextEdit(docId, context, logContext, cancellationToken, telemetryBuilder);
243+
telemetryBuilder.setOpportunityId(context.requestUuid);
244+
245+
try {
246+
const internalResult = await this._nextEditProvider.getNextEdit(docId, context, logContext, cancellationToken, telemetryBuilder.nesBuilder);
247+
const result: NESResult = {
248+
result: internalResult.result ? {
249+
newText: internalResult.result.edit.newText,
250+
range: internalResult.result.edit.replaceRange,
251+
} : undefined,
252+
docId,
253+
requestUuid: context.requestUuid,
254+
internalResult,
255+
telemetryBuilder,
256+
};
257+
return result;
258+
} catch (e) {
259+
try {
260+
this._telemetrySender.sendTelemetryForBuilder(telemetryBuilder);
261+
} finally {
262+
telemetryBuilder.dispose();
263+
}
264+
throw e;
265+
}
197266
}
198267
}
199268

200-
export interface INESProvider {
201-
getId(): string;
202-
getNextEdit(documentUri: vscode.Uri, cancellationToken: CancellationToken): Promise<INextEditResult>;
203-
handleShown(suggestion: INextEditResult): void;
204-
handleAcceptance(docId: DocumentId, suggestion: INextEditResult): void;
205-
handleRejection(docId: DocumentId, suggestion: INextEditResult): void;
206-
handleIgnored(docId: DocumentId, suggestion: INextEditResult, supersededByRequestUuid: INextEditResult | undefined): void;
207-
dispose(): void;
208-
}
209-
210269
function setupServices(options: INESProviderOptions) {
211270
const { fetcher, copilotTokenManager, telemetrySender, logTarget } = options;
212271
const builder = new InstantiationServiceBuilder();

0 commit comments

Comments
 (0)