From 79e5d22b69f9602c8002bd8e9f770b284d467885 Mon Sep 17 00:00:00 2001 From: Dirk Baeumer Date: Mon, 27 Oct 2025 16:38:08 +0100 Subject: [PATCH 1/5] WIP --- .../src/completionsTelemetryServiceBridge.ts | 4 +++ .../lib/src/ghostText/ghostText.ts | 10 +++++- .../lib/src/prompt/contextProviderRegistry.ts | 9 ++--- .../src/prompt/contextProviderStatistics.ts | 15 ++++++++ .../src/common/baseContextProviders.ts | 35 +++++++++++-------- .../src/common/contextProvider.ts | 5 +-- .../serverPlugin/src/common/protocol.ts | 5 +++ .../vscode-node/inspector.ts | 3 ++ .../vscode-node/languageContextService.ts | 2 ++ .../typescriptContext/vscode-node/types.ts | 13 +++++-- src/platform/inlineCompletions/common/api.ts | 6 ++++ .../common/languageContextService.ts | 14 ++++++++ 12 files changed, 97 insertions(+), 24 deletions(-) diff --git a/src/extension/completions-core/vscode-node/bridge/src/completionsTelemetryServiceBridge.ts b/src/extension/completions-core/vscode-node/bridge/src/completionsTelemetryServiceBridge.ts index 9b5777832b..5ed57f520a 100644 --- a/src/extension/completions-core/vscode-node/bridge/src/completionsTelemetryServiceBridge.ts +++ b/src/extension/completions-core/vscode-node/bridge/src/completionsTelemetryServiceBridge.ts @@ -20,6 +20,10 @@ export class CompletionsTelemetryServiceBridge { this.enhancedReporter = undefined; } + public getTelemetryService(): ITelemetryService { + return this.telemetryService; + } + sendGHTelemetryEvent(eventName: string, properties?: TelemetryEventProperties, measurements?: TelemetryEventMeasurements, store?: TelemetryStore): void { this.telemetryService.sendGHTelemetryEvent(wrapEventNameForPrefixRemoval(`copilot/${eventName}`), properties, measurements); this.getSpyReporters(store ?? TelemetryStore.Standard)?.sendTelemetryEvent(eventName, properties as TelemetryProperties, measurements as TelemetryMeasurements); diff --git a/src/extension/completions-core/vscode-node/lib/src/ghostText/ghostText.ts b/src/extension/completions-core/vscode-node/lib/src/ghostText/ghostText.ts index 9283abf1c7..7746f8f1a5 100644 --- a/src/extension/completions-core/vscode-node/lib/src/ghostText/ghostText.ts +++ b/src/extension/completions-core/vscode-node/lib/src/ghostText/ghostText.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { createSha256Hash } from '../../../../../../util/common/crypto'; import { generateUuid } from '../../../../../../util/vs/base/common/uuid'; +import { CompletionsTelemetryServiceBridge } from '../../../bridge/src/completionsTelemetryServiceBridge'; import { isSupportedLanguageId } from '../../../prompt/src/parse'; import { initializeTokenizers } from '../../../prompt/src/tokenization'; import { CancellationTokenSource, CancellationToken as ICancellationToken } from '../../../types/src'; @@ -28,6 +29,7 @@ import { APIChoice, getTemperatureForSamples } from '../openai/openai'; import { CopilotNamedAnnotationList } from '../openai/stream'; import { StatusReporter } from '../progress'; import { ContextProviderBridge } from '../prompt/components/contextProviderBridge'; +import { ContextProviderStatistics } from '../prompt/contextProviderStatistics'; import { ContextIndentation, contextIndentation, @@ -1094,7 +1096,13 @@ export async function getGhostText( options ); ctx.get(CompletionNotifier).notifyRequest(completionState, id, telemetryData, token, options); - return await getGhostTextWithoutAbortHandling(ctx, completionState, id, telemetryData, token, options); + const result = await getGhostTextWithoutAbortHandling(ctx, completionState, id, telemetryData, token, options); + const statistics = ctx.get(ContextProviderStatistics).getStatisticsForCompletion(id); + const telemetryService = ctx.get(CompletionsTelemetryServiceBridge).getTelemetryService(); + for (const statistic of statistics.getAllUsageStatistics()) { + console.log(statistic); + } + return result; } catch (e) { // The cancellation token may be called after the request is done but while we still process data. // The underlying implementation catches abort errors for specific scenarios but we still have uncovered paths. diff --git a/src/extension/completions-core/vscode-node/lib/src/prompt/contextProviderRegistry.ts b/src/extension/completions-core/vscode-node/lib/src/prompt/contextProviderRegistry.ts index 9d38577bc8..ce3a400b56 100644 --- a/src/extension/completions-core/vscode-node/lib/src/prompt/contextProviderRegistry.ts +++ b/src/extension/completions-core/vscode-node/lib/src/prompt/contextProviderRegistry.ts @@ -233,6 +233,10 @@ class CoreContextProviderRegistry extends ContextProviderRegistry { const pendingContextItem = provider.resolver.resolve(request, providerCancellationTokenSource.token); resolutionMap.set(provider.id, pendingContextItem); } + + const statistics = this.ctx.get(ContextProviderStatistics).getStatisticsForCompletion(completionId); + statistics.setOpportunityId(opportunityId); + const results = await resolveAll(resolutionMap, providerCancellationTokenSource.token); // Once done, clear the timeout so that we don't cancel the request once it has finished. @@ -302,10 +306,7 @@ class CoreContextProviderRegistry extends ContextProviderRegistry { resolvedContextItems.push(resolvedContextItem); } - this.ctx - .get(ContextProviderStatistics) - .getStatisticsForCompletion(completionId) - .setLastResolution(provider.id, result.status); + statistics.setLastResolution(provider.id, result.status); } else { // This can't happen logger.error(this.ctx, `Context provider ${provider.id} not found in results`); diff --git a/src/extension/completions-core/vscode-node/lib/src/prompt/contextProviderStatistics.ts b/src/extension/completions-core/vscode-node/lib/src/prompt/contextProviderStatistics.ts index 8d8998c54b..b588f2c330 100644 --- a/src/extension/completions-core/vscode-node/lib/src/prompt/contextProviderStatistics.ts +++ b/src/extension/completions-core/vscode-node/lib/src/prompt/contextProviderStatistics.ts @@ -54,11 +54,18 @@ export class ContextProviderStatistics { } export class PerCompletionContextProviderStatistics { + + public opportunityId: string | undefined; + // Keyed by the providerId, contains an array of tuples [context item, expectation] protected _expectations = new Map(); protected _lastResolution = new Map(); protected _statistics = new Map(); + constructor() { + this.opportunityId = undefined; + } + addExpectations(providerId: string, expectations: [SupportedContextItemWithId, PromptExpectation][]) { const providerExpectations = this._expectations.get(providerId) ?? []; this._expectations.set(providerId, [...providerExpectations, ...expectations]); @@ -72,10 +79,18 @@ export class PerCompletionContextProviderStatistics { this._lastResolution.set(providerId, resolution); } + setOpportunityId(opportunityId: string) { + this.opportunityId = opportunityId; + } + get(providerId: string): ContextUsageStatistics | undefined { return this._statistics.get(providerId); } + getAllUsageStatistics(): IterableIterator { + return this._statistics.values(); + } + computeMatch(promptMatchers: PromptMatcher[]) { try { for (const [providerId, expectations] of this._expectations) { diff --git a/src/extension/typescriptContext/serverPlugin/src/common/baseContextProviders.ts b/src/extension/typescriptContext/serverPlugin/src/common/baseContextProviders.ts index 4e7cac3cf0..ce6a238097 100644 --- a/src/extension/typescriptContext/serverPlugin/src/common/baseContextProviders.ts +++ b/src/extension/typescriptContext/serverPlugin/src/common/baseContextProviders.ts @@ -109,7 +109,9 @@ export class SignatureRunnable extends FunctionLikeContextRunnable { protected override createRunnableResult(result: ContextResult): RunnableResult { const scope = this.getCacheScope(); const cacheInfo: CacheInfo | undefined = scope !== undefined ? { emitMode: EmitMode.ClientBased, scope } : undefined; - return result.createRunnableResult(this.id, this.priority, SpeculativeKind.emit, cacheInfo); + const runnableResult = result.createRunnableResult(this.id, this.priority, SpeculativeKind.emit, cacheInfo); + runnableResult.debugId = this.getDebugId(); + return runnableResult; } protected override run(result: RunnableResult, token: tt.CancellationToken): void { @@ -141,22 +143,27 @@ export class SignatureRunnable extends FunctionLikeContextRunnable { } } + private getDebugId(): string | undefined { + if (!this.session.host.isDebugging()) { + return undefined; + } + const startPos = this.declaration.parameters.pos; + const endPos = this.declaration.type?.end ?? this.declaration.parameters.end; + const sourceFile = this.declaration.getSourceFile(); + const start = ts.getLineAndCharacterOfPosition(sourceFile, startPos); + const end = ts.getLineAndCharacterOfPosition(sourceFile, endPos); + return `SignatureRunnable:${sourceFile.fileName}:[${start.line},${start.character},${end.line},${end.character}]`; + + } + private static computeId(session: ComputeContextSession, declaration: tt.FunctionLikeDeclarationBase): string { - const host = session.host; const startPos = declaration.parameters.pos; const endPos = declaration.type?.end ?? declaration.parameters.end; - if (host.isDebugging()) { - const sourceFile = declaration.getSourceFile(); - const start = ts.getLineAndCharacterOfPosition(sourceFile, startPos); - const end = ts.getLineAndCharacterOfPosition(sourceFile, endPos); - return `SignatureRunnable:${declaration.getSourceFile().fileName}:[${start.line},${start.character},${end.line},${end.character}]`; - } else { - const hash = session.host.createHash('md5'); // CodeQL [SM04514] The 'md5' algorithm is used to compute a shorter string to represent a symbol in a map. It has no security implications. - const sourceFile = declaration.getSourceFile(); - hash.update(sourceFile.fileName); - hash.update(`[${startPos},${endPos}]`); - return `SignatureRunnable:${hash.digest('base64')}`; - } + const hash = session.host.createHash('md5'); // CodeQL [SM04514] The 'md5' algorithm is used to compute a shorter string to represent a symbol in a map. It has no security implications. + const sourceFile = declaration.getSourceFile(); + hash.update(sourceFile.fileName); + hash.update(`[${startPos},${endPos}]`); + return `SignatureRunnable:${hash.digest('base64')}`; } } diff --git a/src/extension/typescriptContext/serverPlugin/src/common/contextProvider.ts b/src/extension/typescriptContext/serverPlugin/src/common/contextProvider.ts index 460e924e3a..13a4372ac6 100644 --- a/src/extension/typescriptContext/serverPlugin/src/common/contextProvider.ts +++ b/src/extension/typescriptContext/serverPlugin/src/common/contextProvider.ts @@ -430,6 +430,7 @@ export class RunnableResult { public readonly priority: number; public readonly items: ContextItem[]; + public debugId: string | undefined; constructor(id: ContextRunnableResultId, priority: number, runnableResultContext: RunnableResultContext, primaryBudget: CharacterBudget, secondaryBudget: CharacterBudget, speculativeKind: SpeculativeKind, cache?: CacheInfo | undefined) { this.id = id; @@ -511,7 +512,8 @@ export class RunnableResult { priority: this.priority, items: this.items, cache: this.cache, - speculativeKind: this.speculativeKind + speculativeKind: this.speculativeKind, + debugId: this.debugId }; } } @@ -834,7 +836,6 @@ export abstract class AbstractContextRunnable implements ContextRunnable { this.cost = cost; } - public initialize(result: ContextResult): void { if (this.result !== undefined) { throw new Error('Runnable already initialized'); diff --git a/src/extension/typescriptContext/serverPlugin/src/common/protocol.ts b/src/extension/typescriptContext/serverPlugin/src/common/protocol.ts index 8cc67bfa36..3684300f3c 100644 --- a/src/extension/typescriptContext/serverPlugin/src/common/protocol.ts +++ b/src/extension/typescriptContext/serverPlugin/src/common/protocol.ts @@ -286,6 +286,11 @@ export type ContextRunnableResult = { * document and position. */ speculativeKind: SpeculativeKind; + + /** + * A human readable id to ease debugging. + */ + debugId?: ContextRunnableResultId | undefined; } export type CachedContextRunnableResult = { diff --git a/src/extension/typescriptContext/vscode-node/inspector.ts b/src/extension/typescriptContext/vscode-node/inspector.ts index ad7326720b..0f710fd6dd 100644 --- a/src/extension/typescriptContext/vscode-node/inspector.ts +++ b/src/extension/typescriptContext/vscode-node/inspector.ts @@ -218,6 +218,9 @@ class TreeRunnableResult { result.push(new TreeCacheInfo(this.from.cache)); } result.push(new TreePropertyItem(this, 'priority', this.from.priority.toString())); + if (this.from.debugId !== undefined) { + result.push(new TreePropertyItem(this, 'debugId', this.from.debugId)); + } return result; } diff --git a/src/extension/typescriptContext/vscode-node/languageContextService.ts b/src/extension/typescriptContext/vscode-node/languageContextService.ts index 92e14115a4..5431d7e5ca 100644 --- a/src/extension/typescriptContext/vscode-node/languageContextService.ts +++ b/src/extension/typescriptContext/vscode-node/languageContextService.ts @@ -2001,6 +2001,7 @@ export class InlineCompletionContribution implements vscode.Disposable, TokenBud if (item.kind === ContextKind.Snippet) { const converted: Copilot.CodeSnippet = { importance: item.priority * 100, + id: item.id, uri: item.uri.toString(), value: item.value }; @@ -2011,6 +2012,7 @@ export class InlineCompletionContribution implements vscode.Disposable, TokenBud } else if (item.kind === ContextKind.Trait) { const converted: Copilot.Trait = { importance: item.priority * 100, + id: item.id, name: item.name, value: item.value }; diff --git a/src/extension/typescriptContext/vscode-node/types.ts b/src/extension/typescriptContext/vscode-node/types.ts index 991b25039f..4c827a487f 100644 --- a/src/extension/typescriptContext/vscode-node/types.ts +++ b/src/extension/typescriptContext/vscode-node/types.ts @@ -14,6 +14,7 @@ export type ResolvedRunnableResult = { priority: number; items: protocol.FullContextItem[]; cache?: protocol.CacheInfo; + debugId?: protocol.ContextRunnableResultId | undefined; } export namespace ResolvedRunnableResult { export function from(result: protocol.ContextRunnableResult, items: protocol.FullContextItem[]): ResolvedRunnableResult { @@ -140,6 +141,8 @@ export class ContextItemResultBuilder implements ContextItemSummary { public contextComputeTime: number; public totalTime: number; + private counter: number; + constructor(totalTime: number) { this.seenRunnableResults = new Set(); this.seenContextItems = new Set(); @@ -157,6 +160,8 @@ export class ContextItemResultBuilder implements ContextItemSummary { this.serverTime = -1; this.contextComputeTime = -1; this.totalTime = totalTime; + + this.counter = 0; } public updateResponse(result: protocol.ContextRequestResult, token: vscode.CancellationToken): void { @@ -181,7 +186,7 @@ export class ContextItemResultBuilder implements ContextItemSummary { } this.seenContextItems.add(item.key); } - const converted = ContextItemResultBuilder.doConvert(item, runnableResult.priority); + const converted = ContextItemResultBuilder.doConvert(item, runnableResult.priority, (this.counter++).toString()); if (converted === undefined) { continue; } @@ -193,7 +198,7 @@ export class ContextItemResultBuilder implements ContextItemSummary { public *convert(runnableResult: ResolvedRunnableResult): IterableIterator { Stats.update(this.stats, runnableResult); for (const item of runnableResult.items) { - const converted = ContextItemResultBuilder.doConvert(item, runnableResult.priority); + const converted = ContextItemResultBuilder.doConvert(item, runnableResult.priority, (this.counter++).toString()); if (converted === undefined) { continue; } @@ -202,11 +207,12 @@ export class ContextItemResultBuilder implements ContextItemSummary { } } - private static doConvert(item: protocol.ContextItem, priority: number): ContextItem | undefined { + private static doConvert(item: protocol.ContextItem, priority: number, id: string): ContextItem | undefined { switch (item.kind) { case protocol.ContextKind.Snippet: return { kind: ContextKind.Snippet, + id: id, priority: priority, uri: vscode.Uri.file(item.fileName), additionalUris: item.additionalFileNames?.map(uri => vscode.Uri.file(uri)), @@ -215,6 +221,7 @@ export class ContextItemResultBuilder implements ContextItemSummary { case protocol.ContextKind.Trait: return { kind: ContextKind.Trait, + id: id, priority: priority, name: item.name, value: item.value diff --git a/src/platform/inlineCompletions/common/api.ts b/src/platform/inlineCompletions/common/api.ts index f244b3f96d..4d55f78428 100644 --- a/src/platform/inlineCompletions/common/api.ts +++ b/src/platform/inlineCompletions/common/api.ts @@ -177,6 +177,12 @@ export namespace Copilot { * Default value is 0. */ importance?: number; + + /** + * A unique ID for the context item, used to provide detailed statistics about + * the item's usage. If an ID is not provided, it will be generated randomly. + */ + id?: string; } // A key-value pair used for short string snippets. diff --git a/src/platform/languageServer/common/languageContextService.ts b/src/platform/languageServer/common/languageContextService.ts index 39ec8150e7..cf055224e4 100644 --- a/src/platform/languageServer/common/languageContextService.ts +++ b/src/platform/languageServer/common/languageContextService.ts @@ -23,6 +23,13 @@ export interface SnippetContext { */ kind: ContextKind.Snippet; + /** + * A unique ID for the context item, used to provide + * detailed statistics about the item's usage. If an ID + * is not provided, it will be generated randomly. + */ + id?: string; + /** * The priority of the snippet. Value range is [0, 1]. */ @@ -50,6 +57,13 @@ export interface TraitContext { */ kind: ContextKind.Trait; + /** + * A unique ID for the context item, used to provide + * detailed statistics about the item's usage. If an ID + * is not provided, it will be generated randomly. + */ + id?: string; + /** * The priority of the context. */ From 51651f7f1f1943f1bfe9283649ae027292376cb6 Mon Sep 17 00:00:00 2001 From: Dirk Baeumer Date: Mon, 27 Oct 2025 17:05:17 +0100 Subject: [PATCH 2/5] WIP --- src/extension/typescriptContext/common/serverProtocol.ts | 5 +++++ .../serverPlugin/src/common/baseContextProviders.ts | 4 ++-- .../serverPlugin/src/common/contextProvider.ts | 4 ++-- .../typescriptContext/serverPlugin/src/common/protocol.ts | 4 ++-- src/extension/typescriptContext/vscode-node/inspector.ts | 4 ++-- src/extension/typescriptContext/vscode-node/types.ts | 5 +++-- 6 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/extension/typescriptContext/common/serverProtocol.ts b/src/extension/typescriptContext/common/serverProtocol.ts index 8cc67bfa36..4d769d4a05 100644 --- a/src/extension/typescriptContext/common/serverProtocol.ts +++ b/src/extension/typescriptContext/common/serverProtocol.ts @@ -286,6 +286,11 @@ export type ContextRunnableResult = { * document and position. */ speculativeKind: SpeculativeKind; + + /** + * A human readable path to ease debugging. + */ + debugPath?: ContextRunnableResultId | undefined; } export type CachedContextRunnableResult = { diff --git a/src/extension/typescriptContext/serverPlugin/src/common/baseContextProviders.ts b/src/extension/typescriptContext/serverPlugin/src/common/baseContextProviders.ts index ce6a238097..91f7c039cd 100644 --- a/src/extension/typescriptContext/serverPlugin/src/common/baseContextProviders.ts +++ b/src/extension/typescriptContext/serverPlugin/src/common/baseContextProviders.ts @@ -110,7 +110,7 @@ export class SignatureRunnable extends FunctionLikeContextRunnable { const scope = this.getCacheScope(); const cacheInfo: CacheInfo | undefined = scope !== undefined ? { emitMode: EmitMode.ClientBased, scope } : undefined; const runnableResult = result.createRunnableResult(this.id, this.priority, SpeculativeKind.emit, cacheInfo); - runnableResult.debugId = this.getDebugId(); + runnableResult.debugPath = this.getDebugPath(); return runnableResult; } @@ -143,7 +143,7 @@ export class SignatureRunnable extends FunctionLikeContextRunnable { } } - private getDebugId(): string | undefined { + private getDebugPath(): string | undefined { if (!this.session.host.isDebugging()) { return undefined; } diff --git a/src/extension/typescriptContext/serverPlugin/src/common/contextProvider.ts b/src/extension/typescriptContext/serverPlugin/src/common/contextProvider.ts index 13a4372ac6..27c2e13d6e 100644 --- a/src/extension/typescriptContext/serverPlugin/src/common/contextProvider.ts +++ b/src/extension/typescriptContext/serverPlugin/src/common/contextProvider.ts @@ -430,7 +430,7 @@ export class RunnableResult { public readonly priority: number; public readonly items: ContextItem[]; - public debugId: string | undefined; + public debugPath: string | undefined; constructor(id: ContextRunnableResultId, priority: number, runnableResultContext: RunnableResultContext, primaryBudget: CharacterBudget, secondaryBudget: CharacterBudget, speculativeKind: SpeculativeKind, cache?: CacheInfo | undefined) { this.id = id; @@ -513,7 +513,7 @@ export class RunnableResult { items: this.items, cache: this.cache, speculativeKind: this.speculativeKind, - debugId: this.debugId + debugPath: this.debugPath }; } } diff --git a/src/extension/typescriptContext/serverPlugin/src/common/protocol.ts b/src/extension/typescriptContext/serverPlugin/src/common/protocol.ts index 3684300f3c..dea001b002 100644 --- a/src/extension/typescriptContext/serverPlugin/src/common/protocol.ts +++ b/src/extension/typescriptContext/serverPlugin/src/common/protocol.ts @@ -288,9 +288,9 @@ export type ContextRunnableResult = { speculativeKind: SpeculativeKind; /** - * A human readable id to ease debugging. + * A human readable path to the signature to ease debugging. */ - debugId?: ContextRunnableResultId | undefined; + debugPath?: ContextRunnableResultId | undefined; } export type CachedContextRunnableResult = { diff --git a/src/extension/typescriptContext/vscode-node/inspector.ts b/src/extension/typescriptContext/vscode-node/inspector.ts index 0f710fd6dd..7d776f2cc8 100644 --- a/src/extension/typescriptContext/vscode-node/inspector.ts +++ b/src/extension/typescriptContext/vscode-node/inspector.ts @@ -218,8 +218,8 @@ class TreeRunnableResult { result.push(new TreeCacheInfo(this.from.cache)); } result.push(new TreePropertyItem(this, 'priority', this.from.priority.toString())); - if (this.from.debugId !== undefined) { - result.push(new TreePropertyItem(this, 'debugId', this.from.debugId)); + if (this.from.debugPath !== undefined) { + result.push(new TreePropertyItem(this, 'debugPath', this.from.debugPath)); } return result; diff --git a/src/extension/typescriptContext/vscode-node/types.ts b/src/extension/typescriptContext/vscode-node/types.ts index 4c827a487f..fc2037916e 100644 --- a/src/extension/typescriptContext/vscode-node/types.ts +++ b/src/extension/typescriptContext/vscode-node/types.ts @@ -14,7 +14,7 @@ export type ResolvedRunnableResult = { priority: number; items: protocol.FullContextItem[]; cache?: protocol.CacheInfo; - debugId?: protocol.ContextRunnableResultId | undefined; + debugPath?: protocol.ContextRunnableResultId | undefined; } export namespace ResolvedRunnableResult { export function from(result: protocol.ContextRunnableResult, items: protocol.FullContextItem[]): ResolvedRunnableResult { @@ -23,7 +23,8 @@ export namespace ResolvedRunnableResult { state: result.state, priority: result.priority, items: items, - cache: result.cache + cache: result.cache, + debugPath: result.debugPath }; } } From 13c381802f3f03278b6798ba2d0434acb2f0e372 Mon Sep 17 00:00:00 2001 From: Dirk Baeumer Date: Mon, 27 Oct 2025 17:39:11 +0100 Subject: [PATCH 3/5] Send context provider telemetry to vscode-chat as well --- .../lib/src/ghostText/ghostText.ts | 29 +++++++++++++++++-- .../src/prompt/contextProviderStatistics.ts | 4 +-- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/extension/completions-core/vscode-node/lib/src/ghostText/ghostText.ts b/src/extension/completions-core/vscode-node/lib/src/ghostText/ghostText.ts index 7746f8f1a5..5a8bd91e88 100644 --- a/src/extension/completions-core/vscode-node/lib/src/ghostText/ghostText.ts +++ b/src/extension/completions-core/vscode-node/lib/src/ghostText/ghostText.ts @@ -1098,9 +1098,34 @@ export async function getGhostText( ctx.get(CompletionNotifier).notifyRequest(completionState, id, telemetryData, token, options); const result = await getGhostTextWithoutAbortHandling(ctx, completionState, id, telemetryData, token, options); const statistics = ctx.get(ContextProviderStatistics).getStatisticsForCompletion(id); + const opportunityId = options?.opportunityId ?? 'unknown'; const telemetryService = ctx.get(CompletionsTelemetryServiceBridge).getTelemetryService(); - for (const statistic of statistics.getAllUsageStatistics()) { - console.log(statistic); + for (const [providerId, statistic] of statistics.getAllUsageStatistics()) { + /* __GDPR__ + "context-plugin.completion-context-stats" : { + "owner": "dirkb", + "comment": "Telemetry for copilot inline completion context", + "requestId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The request correlation id" }, + "opportunityId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The opportunity id" }, + "providerId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The context provider id" }, + "resolution": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The resolution of the context" }, + "usage": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "How the context was used" }, + "usageDetails": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Additional details about the usage as a JSON string" } + } + */ + telemetryService.sendMSFTTelemetryEvent( + 'context-plugin.completion-context-stats', + { + requestId: id, + opportunityId, + providerId, + resolution: statistic.resolution, + usage: statistic.usage, + usageDetails: JSON.stringify(statistic.usageDetails), + }, + { + } + ); } return result; } catch (e) { diff --git a/src/extension/completions-core/vscode-node/lib/src/prompt/contextProviderStatistics.ts b/src/extension/completions-core/vscode-node/lib/src/prompt/contextProviderStatistics.ts index b588f2c330..665692cac2 100644 --- a/src/extension/completions-core/vscode-node/lib/src/prompt/contextProviderStatistics.ts +++ b/src/extension/completions-core/vscode-node/lib/src/prompt/contextProviderStatistics.ts @@ -87,8 +87,8 @@ export class PerCompletionContextProviderStatistics { return this._statistics.get(providerId); } - getAllUsageStatistics(): IterableIterator { - return this._statistics.values(); + getAllUsageStatistics(): IterableIterator<[string, ContextUsageStatistics]> { + return this._statistics.entries(); } computeMatch(promptMatchers: PromptMatcher[]) { From d478ab2da2b5b1bea7ce8fc2c5b6586700c4337b Mon Sep 17 00:00:00 2001 From: Dirk Baeumer Date: Tue, 28 Oct 2025 12:18:34 +0100 Subject: [PATCH 4/5] Better event name --- .../vscode-node/lib/src/ghostText/ghostText.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/extension/completions-core/vscode-node/lib/src/ghostText/ghostText.ts b/src/extension/completions-core/vscode-node/lib/src/ghostText/ghostText.ts index 58390393a0..5cf13fde7d 100644 --- a/src/extension/completions-core/vscode-node/lib/src/ghostText/ghostText.ts +++ b/src/extension/completions-core/vscode-node/lib/src/ghostText/ghostText.ts @@ -1101,7 +1101,7 @@ export async function getGhostText( const telemetryService = ctx.get(CompletionsTelemetryServiceBridge).getTelemetryService(); for (const [providerId, statistic] of statistics.getAllUsageStatistics()) { /* __GDPR__ - "context-plugin.completion-context-stats" : { + "context-provider.completion-stats" : { "owner": "dirkb", "comment": "Telemetry for copilot inline completion context", "requestId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The request correlation id" }, @@ -1113,7 +1113,7 @@ export async function getGhostText( } */ telemetryService.sendMSFTTelemetryEvent( - 'context-plugin.completion-context-stats', + 'context-provider.completion-stats', { requestId: id, opportunityId, From 8fef078b908a14853d903888ef6f71431dcd23da Mon Sep 17 00:00:00 2001 From: Dirk Baeumer Date: Tue, 28 Oct 2025 14:10:35 +0100 Subject: [PATCH 5/5] Address PR comments --- .../src/common/baseContextProviders.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/extension/typescriptContext/serverPlugin/src/common/baseContextProviders.ts b/src/extension/typescriptContext/serverPlugin/src/common/baseContextProviders.ts index 91f7c039cd..c5a589b512 100644 --- a/src/extension/typescriptContext/serverPlugin/src/common/baseContextProviders.ts +++ b/src/extension/typescriptContext/serverPlugin/src/common/baseContextProviders.ts @@ -147,9 +147,7 @@ export class SignatureRunnable extends FunctionLikeContextRunnable { if (!this.session.host.isDebugging()) { return undefined; } - const startPos = this.declaration.parameters.pos; - const endPos = this.declaration.type?.end ?? this.declaration.parameters.end; - const sourceFile = this.declaration.getSourceFile(); + const { sourceFile, startPos, endPos } = SignatureRunnable.getSourceFileAndPositions(this.declaration); const start = ts.getLineAndCharacterOfPosition(sourceFile, startPos); const end = ts.getLineAndCharacterOfPosition(sourceFile, endPos); return `SignatureRunnable:${sourceFile.fileName}:[${start.line},${start.character},${end.line},${end.character}]`; @@ -157,14 +155,19 @@ export class SignatureRunnable extends FunctionLikeContextRunnable { } private static computeId(session: ComputeContextSession, declaration: tt.FunctionLikeDeclarationBase): string { - const startPos = declaration.parameters.pos; - const endPos = declaration.type?.end ?? declaration.parameters.end; + const { sourceFile, startPos, endPos } = SignatureRunnable.getSourceFileAndPositions(declaration); const hash = session.host.createHash('md5'); // CodeQL [SM04514] The 'md5' algorithm is used to compute a shorter string to represent a symbol in a map. It has no security implications. - const sourceFile = declaration.getSourceFile(); hash.update(sourceFile.fileName); hash.update(`[${startPos},${endPos}]`); return `SignatureRunnable:${hash.digest('base64')}`; } + + private static getSourceFileAndPositions(declaration: tt.FunctionLikeDeclarationBase): { sourceFile: tt.SourceFile; startPos: number; endPos: number } { + const startPos = declaration.parameters.pos; + const endPos = declaration.type?.end ?? declaration.parameters.end; + const sourceFile = declaration.getSourceFile(); + return { sourceFile, startPos, endPos }; + } } export class TypeOfLocalsRunnable extends AbstractContextRunnable {