From 24c6799a8f6ede031b306f845c527f26c323d75f Mon Sep 17 00:00:00 2001 From: Zhichao Li Date: Fri, 22 May 2026 11:06:54 -0700 Subject: [PATCH 01/13] docs(otel): sprint plan for github.copilot.* parity --- .../copilot/docs/monitoring/sprint.plan.md | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 extensions/copilot/docs/monitoring/sprint.plan.md diff --git a/extensions/copilot/docs/monitoring/sprint.plan.md b/extensions/copilot/docs/monitoring/sprint.plan.md new file mode 100644 index 00000000000..6e47a509427 --- /dev/null +++ b/extensions/copilot/docs/monitoring/sprint.plan.md @@ -0,0 +1,36 @@ +# OTel CLI Parity Sprint — branch `zhichli/oteladdition` + +Reference: copilot-agent-runtime CLI PR #8037 (`e8a24076831b09a17f7f641b33885ddd5fcb4b3e`). + +Goal: bring VS Code's extension-side OTel up to the new `github.copilot.*` shape introduced in the CLI, without breaking existing `copilot_chat.*` consumers (Agent Debug Log, Chronicle, OTLP exporters, SQLite span store). + +## Locked design decisions + +| # | Decision | +|---|---| +| 1 | MCP server names hashed by default (SHA-256 hex). Raw name only when `captureContent=true`. | +| 2 | `agent.type` derived from existing `modeInstructions2.isBuiltin` flag (`builtin` / `custom`). A richer registry-source field is a follow-up. | +| 3 | Skill source emitted verbatim as VS Code's `PromptFileSource` enum value (no CLI mapping). Deferred — skill emission not part of this PR. | +| 4 | Indefinite dual-emit; legacy `copilot_chat.*` keys keep emitting, marked **Legacy** in doc, no sunset. | + +## Tasks (atomic commits) + +1. **feat(otel): add `github.copilot.*` attribute constants and hash helper** — `genAiAttributes.ts` +2. **feat(otel): dual-emit git context under `github.copilot.git.*`** — `workspaceOTelMetadata.ts` +3. **feat(otel): rename reasoning tokens key with dual-emit** — `chatMLFetcher.ts`, `otelSqliteStore.ts` +4. **feat(otel): stamp `github.copilot.agent.type` on `invoke_agent`** — `toolCallingLoop.ts` +5. **feat(otel): structured `github.copilot.tool.parameters.*` on `execute_tool`** — `toolsService.ts` +6. **feat(otel): enrich `execute_hook` with `decision` / `tool_names` / `duration_seconds`** — `chatHookService.ts` +7. **docs(otel): document `github.copilot.*` attributes and mark `copilot_chat.repo.*` legacy** — `agent_monitoring.md` +8. **docs(otel): add dual-emit policy section to OTel skill** — `.github/skills/otel/SKILL.md` + +## Deferred to follow-up PRs + +- `github.copilot.skill.invoked` span event with raw `PromptFileSource`. +- `github.copilot.mcp.server.lifecycle` event + `connection.count` counter. +- `github.copilot.context.{skills,mcp_server_names,custom_agent_names}` snapshot attrs on `invoke_agent`. +- Mode/agent registry `source` field for the richer `agent.type` enum. + +## Hiccups & Notes + +(Filled in during execution.) From a82d87c5e32a2e88c03fd2402f545a73415a635b Mon Sep 17 00:00:00 2001 From: Zhichao Li Date: Fri, 22 May 2026 11:07:52 -0700 Subject: [PATCH 02/13] feat(otel): add github.copilot.* attribute constants and hash helper --- .../platform/otel/common/genAiAttributes.ts | 80 +++++++++++++++++++ extensions/copilot/src/util/node/crypto.ts | 12 +++ 2 files changed, 92 insertions(+) diff --git a/extensions/copilot/src/platform/otel/common/genAiAttributes.ts b/extensions/copilot/src/platform/otel/common/genAiAttributes.ts index 1972c799da1..bd56d5ef4b2 100644 --- a/extensions/copilot/src/platform/otel/common/genAiAttributes.ts +++ b/extensions/copilot/src/platform/otel/common/genAiAttributes.ts @@ -189,3 +189,83 @@ export const CopilotCliSdkAttr = { HOOK_TYPE: 'github.copilot.hook.type', HOOK_INVOCATION_ID: 'github.copilot.hook.invocation_id', } as const; + +/** + * Canonical `github.copilot.*` attribute namespace shared with the Copilot CLI + * runtime (see copilot-agent-runtime `src/core/otel/otelGenAI.ts`). These + * attributes are dual-emitted alongside the legacy `copilot_chat.*` keys; new + * dashboards should prefer this namespace. + */ +export const GitHubCopilotAttr = { + /** Agent type classifier: `builtin` | `plugin` | `custom`. */ + AGENT_TYPE: 'github.copilot.agent.type', + + /** Git remote URL (normalized). Dual of `copilot_chat.repo.remote_url`. */ + GIT_REPOSITORY: 'github.copilot.git.repository', + /** Git HEAD branch. Dual of `copilot_chat.repo.head_branch_name`. */ + GIT_BRANCH: 'github.copilot.git.branch', + /** Git HEAD commit. Dual of `copilot_chat.repo.head_commit_hash`. */ + GIT_COMMIT_SHA: 'github.copilot.git.commit_sha', + /** GitHub `owner` segment derived from the remote URL (gated like the URL itself). */ + GITHUB_ORG: 'github.copilot.github.org', + + /** Hook decision result (`block` | `approve` | `non_blocking_error` | `pass`). */ + HOOK_DECISION: 'github.copilot.hook.decision', + /** Hook duration in seconds (float). */ + HOOK_DURATION_SECONDS: 'github.copilot.hook.duration', + /** JSON-encoded array of tool names a hook applies to (plural). */ + HOOK_TOOL_NAMES: 'github.copilot.hook.tool_names', + + /** SHA-256 hex of an MCP server name (always emitted). */ + MCP_SERVER_NAME_HASH: 'github.copilot.mcp.server.name_hash', + /** Raw MCP server name (gated on captureContent). */ + MCP_SERVER_NAME: 'github.copilot.mcp.server.name', + + /** Shell command (truncated to 256 chars; gated on captureContent). */ + TOOL_PARAM_COMMAND: 'github.copilot.tool.parameters.command', + /** File path argument (gated on captureContent). */ + TOOL_PARAM_FILE_PATH: 'github.copilot.tool.parameters.file_path', + /** Edit operation kind (`create`, `update`, `str_replace`, `insert`). */ + TOOL_PARAM_EDIT_TYPE: 'github.copilot.tool.parameters.edit_type', + /** Skill identifier for the invoked tool. */ + TOOL_PARAM_SKILL_NAME: 'github.copilot.tool.parameters.skill_name', + /** SHA-256 hex of the MCP server name for an MCP tool call (always emitted). */ + TOOL_PARAM_MCP_SERVER_NAME_HASH: 'github.copilot.tool.parameters.mcp_server_name_hash', + /** Raw MCP server name for an MCP tool call (gated). */ + TOOL_PARAM_MCP_SERVER_NAME: 'github.copilot.tool.parameters.mcp_server_name', + /** MCP tool name (the part after the `mcp__` prefix). */ + TOOL_PARAM_MCP_TOOL_NAME: 'github.copilot.tool.parameters.mcp_tool_name', + + /** Reasoning/thinking token count (semantic-convention-aligned). Dual of `gen_ai.usage.reasoning_tokens`. */ + USAGE_REASONING_OUTPUT_TOKENS: 'gen_ai.usage.reasoning.output_tokens', +} as const; + +export type AgentType = 'builtin' | 'plugin' | 'custom'; +export type HookDecision = 'block' | 'approve' | 'non_blocking_error' | 'pass'; +export type EditOperationType = 'create' | 'update' | 'str_replace' | 'insert'; + +/** Max length for the `tool.parameters.command` attribute (matches CLI). */ +export const TOOL_PARAM_COMMAND_MAX_LEN = 256; + +/** Tool names treated as shell-command tools for parameter extraction. */ +export const SHELL_TOOL_NAMES: ReadonlySet = new Set([ + 'bash', + 'powershell', + 'local_shell', + 'runInTerminal', + 'run_in_terminal', +]); + +/** Tool names treated as file tools for parameter extraction. */ +export const FILE_TOOL_NAMES: ReadonlySet = new Set([ + 'view', + 'create', + 'edit', + 'str_replace', + 'str_replace_editor', + 'insert', + 'readFile', + 'createFile', + 'replaceString', + 'applyPatch', +]); diff --git a/extensions/copilot/src/util/node/crypto.ts b/extensions/copilot/src/util/node/crypto.ts index 84cde33c748..d6987311f40 100644 --- a/extensions/copilot/src/util/node/crypto.ts +++ b/extensions/copilot/src/util/node/crypto.ts @@ -13,3 +13,15 @@ export async function createSha256FromStream(stream: Readable): Promise } return hash.digest('hex'); } + +/** + * Synchronous SHA-256 hex digest used to redact identifiers in OTel telemetry + * (e.g., MCP server names). Returns `''` for empty/undefined input so callers + * can short-circuit on `!result`. + */ +export function hashTelemetryValue(value: string | undefined | null): string { + if (!value) { + return ''; + } + return createHash('sha256').update(value).digest('hex'); +} From 62491e7882c0f3c8c55590119ddbd284c3a5fe80 Mon Sep 17 00:00:00 2001 From: Zhichao Li Date: Fri, 22 May 2026 11:09:10 -0700 Subject: [PATCH 03/13] feat(otel): dual-emit git context under github.copilot.git.* and derive github.org --- .../common/test/workspaceOTelMetadata.spec.ts | 24 ++++++++++++++++++- .../otel/common/workspaceOTelMetadata.ts | 22 ++++++++++++++++- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/extensions/copilot/src/platform/otel/common/test/workspaceOTelMetadata.spec.ts b/extensions/copilot/src/platform/otel/common/test/workspaceOTelMetadata.spec.ts index c611aa3bbd6..00e93ab2766 100644 --- a/extensions/copilot/src/platform/otel/common/test/workspaceOTelMetadata.spec.ts +++ b/extensions/copilot/src/platform/otel/common/test/workspaceOTelMetadata.spec.ts @@ -6,7 +6,7 @@ import { describe, expect, it } from 'vitest'; import { URI } from '../../../../util/vs/base/common/uri'; import type { IGitService, RepoContext } from '../../../git/common/gitService'; -import { CopilotChatAttr } from '../genAiAttributes'; +import { CopilotChatAttr, GitHubCopilotAttr } from '../genAiAttributes'; import { resolveWorkspaceOTelMetadata, workspaceMetadataToOTelAttributes } from '../workspaceOTelMetadata'; function createMockGitService(repoContext?: Partial): IGitService { @@ -108,4 +108,26 @@ describe('workspaceMetadataToOTelAttributes', () => { expect(attrs[CopilotChatAttr.REPO_REMOTE_URL]).toBe('github.com/org/repo'); expect(attrs[CopilotChatAttr.FILE_RELATIVE_PATH]).toBe('src/index.ts'); }); + + it('dual-emits github.copilot.git.* alongside copilot_chat.repo.* and derives github.org', () => { + const attrs = workspaceMetadataToOTelAttributes({ + headBranchName: 'feature/x', + headCommitHash: 'cafef00d', + remoteUrl: 'https://github.com/microsoft/vscode.git', + }); + expect(attrs[GitHubCopilotAttr.GIT_BRANCH]).toBe('feature/x'); + expect(attrs[GitHubCopilotAttr.GIT_COMMIT_SHA]).toBe('cafef00d'); + expect(attrs[GitHubCopilotAttr.GIT_REPOSITORY]).toBe('https://github.com/microsoft/vscode.git'); + expect(attrs[GitHubCopilotAttr.GITHUB_ORG]).toBe('microsoft'); + // Legacy keys must still be present. + expect(attrs[CopilotChatAttr.REPO_HEAD_BRANCH_NAME]).toBe('feature/x'); + }); + + it('omits github.org for non-github remotes', () => { + const attrs = workspaceMetadataToOTelAttributes({ + remoteUrl: 'https://gitlab.com/org/repo.git', + }); + expect(attrs[GitHubCopilotAttr.GIT_REPOSITORY]).toBe('https://gitlab.com/org/repo.git'); + expect(attrs[GitHubCopilotAttr.GITHUB_ORG]).toBeUndefined(); + }); }); diff --git a/extensions/copilot/src/platform/otel/common/workspaceOTelMetadata.ts b/extensions/copilot/src/platform/otel/common/workspaceOTelMetadata.ts index 72677905869..9acee3d3170 100644 --- a/extensions/copilot/src/platform/otel/common/workspaceOTelMetadata.ts +++ b/extensions/copilot/src/platform/otel/common/workspaceOTelMetadata.ts @@ -6,7 +6,7 @@ import { URI } from '../../../util/vs/base/common/uri'; import { isEqualOrParent, relativePath } from '../../../util/vs/base/common/resources'; import { getOrderedRepoInfosFromContext, type IGitService, normalizeFetchUrl, type RepoContext } from '../../git/common/gitService'; -import { CopilotChatAttr } from './genAiAttributes'; +import { CopilotChatAttr, GitHubCopilotAttr } from './genAiAttributes'; export interface WorkspaceOTelMetadata { readonly headBranchName?: string; @@ -52,6 +52,8 @@ function buildWorkspaceMetadata(repoContext: RepoContext, fileUri?: URI): Worksp /** * Convert workspace metadata to OTel attributes, omitting undefined values. + * Emits both the legacy `copilot_chat.repo.*` namespace and the canonical + * `github.copilot.git.*` namespace (CLI parity, see PR #8037). */ export function workspaceMetadataToOTelAttributes( metadata?: WorkspaceOTelMetadata, @@ -62,15 +64,33 @@ export function workspaceMetadataToOTelAttributes( const attrs: Record = {}; if (metadata.headBranchName) { attrs[CopilotChatAttr.REPO_HEAD_BRANCH_NAME] = metadata.headBranchName; + attrs[GitHubCopilotAttr.GIT_BRANCH] = metadata.headBranchName; } if (metadata.headCommitHash) { attrs[CopilotChatAttr.REPO_HEAD_COMMIT_HASH] = metadata.headCommitHash; + attrs[GitHubCopilotAttr.GIT_COMMIT_SHA] = metadata.headCommitHash; } if (metadata.remoteUrl) { attrs[CopilotChatAttr.REPO_REMOTE_URL] = metadata.remoteUrl; + attrs[GitHubCopilotAttr.GIT_REPOSITORY] = metadata.remoteUrl; + const org = extractGitHubOrg(metadata.remoteUrl); + if (org) { + attrs[GitHubCopilotAttr.GITHUB_ORG] = org; + } } if (metadata.fileRelativePath) { attrs[CopilotChatAttr.FILE_RELATIVE_PATH] = metadata.fileRelativePath; } return attrs; } + +/** + * Extract the `owner` segment from a normalized GitHub remote URL. + * Returns undefined for non-GitHub hosts or malformed inputs. + */ +function extractGitHubOrg(remoteUrl: string): string | undefined { + // Match `(https://|git@)[:/]/` — normalizeFetchUrl already + // strips credentials and `.git` suffixes. + const m = remoteUrl.match(/github\.com[/:]([^/]+)\/[^/]+\/?$/i); + return m?.[1]; +} From 554caea3653cd98836fd20824385364caf52a6c1 Mon Sep 17 00:00:00 2001 From: Zhichao Li Date: Fri, 22 May 2026 11:09:55 -0700 Subject: [PATCH 04/13] feat(otel): dual-emit reasoning tokens with gen_ai.usage.reasoning.output_tokens --- .../copilot/src/extension/prompt/node/chatMLFetcher.ts | 7 +++++-- extensions/copilot/src/platform/otel/common/index.ts | 3 ++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/extensions/copilot/src/extension/prompt/node/chatMLFetcher.ts b/extensions/copilot/src/extension/prompt/node/chatMLFetcher.ts index 3db2fa68f7d..52fc975ec4a 100644 --- a/extensions/copilot/src/extension/prompt/node/chatMLFetcher.ts +++ b/extensions/copilot/src/extension/prompt/node/chatMLFetcher.ts @@ -28,7 +28,7 @@ import { sendEngineMessagesTelemetry } from '../../../platform/networking/node/c import { CAPIWebSocketErrorEvent, IChatWebSocketManager, isCAPIWebSocketError } from '../../../platform/networking/node/chatWebSocketManager'; import { sendCommunicationErrorTelemetry } from '../../../platform/networking/node/stream'; import { ChatFailKind, ChatRequestCanceled, ChatRequestFailed, ChatResults, FetchResponseKind } from '../../../platform/openai/node/fetch'; -import { collectSystemTextsFromRequestBody, CopilotChatAttr, emitInferenceDetailsEvent, GenAiAttr, GenAiMetrics, GenAiOperationName, GenAiProviderName, normalizeProviderMessages, StdAttr, toSystemInstructions, toToolDefinitions, truncateForOTel } from '../../../platform/otel/common/index'; +import { collectSystemTextsFromRequestBody, CopilotChatAttr, emitInferenceDetailsEvent, GenAiAttr, GenAiMetrics, GenAiOperationName, GenAiProviderName, GitHubCopilotAttr, normalizeProviderMessages, StdAttr, toSystemInstructions, toToolDefinitions, truncateForOTel } from '../../../platform/otel/common/index'; import { IOTelService, ISpanHandle, SpanKind, SpanStatusCode } from '../../../platform/otel/common/otelService'; import { IRequestLogger } from '../../../platform/requestLogger/common/requestLogger'; import { getCurrentCapturingToken } from '../../../platform/requestLogger/node/requestLogger'; @@ -419,7 +419,10 @@ export class ChatMLFetcherImpl extends AbstractChatMLFetcher { [CopilotChatAttr.TIME_TO_FIRST_TOKEN]: timeToFirstToken, ...(result.serverRequestId ? { [CopilotChatAttr.SERVER_REQUEST_ID]: result.serverRequestId } : {}), ...(result.usage.completion_tokens_details?.reasoning_tokens - ? { [GenAiAttr.USAGE_REASONING_TOKENS]: result.usage.completion_tokens_details.reasoning_tokens } + ? { + [GenAiAttr.USAGE_REASONING_TOKENS]: result.usage.completion_tokens_details.reasoning_tokens, + [GitHubCopilotAttr.USAGE_REASONING_OUTPUT_TOKENS]: result.usage.completion_tokens_details.reasoning_tokens, + } : {}), ...(typeof result.usage.copilot_usage?.total_nano_aiu === 'number' ? { [CopilotChatAttr.COPILOT_USAGE_NANO_AIU]: result.usage.copilot_usage.total_nano_aiu } diff --git a/extensions/copilot/src/platform/otel/common/index.ts b/extensions/copilot/src/platform/otel/common/index.ts index ef4177129ea..99116ab6b96 100644 --- a/extensions/copilot/src/platform/otel/common/index.ts +++ b/extensions/copilot/src/platform/otel/common/index.ts @@ -3,7 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -export { CopilotChatAttr, CopilotCliSdkAttr, GenAiAttr, GenAiOperationName, GenAiProviderName, GenAiTokenType, GenAiToolType, StdAttr } from './genAiAttributes'; +export { CopilotChatAttr, CopilotCliSdkAttr, FILE_TOOL_NAMES, GenAiAttr, GenAiOperationName, GenAiProviderName, GenAiTokenType, GenAiToolType, GitHubCopilotAttr, SHELL_TOOL_NAMES, StdAttr, TOOL_PARAM_COMMAND_MAX_LEN } from './genAiAttributes'; +export type { AgentType, EditOperationType, HookDecision } from './genAiAttributes'; export { emitAgentTurnEvent, emitCloudSessionInvokeEvent, emitEditFeedbackEvent, emitEditHunkActionEvent, emitEditSurvivalEvent, emitInferenceDetailsEvent, emitInlineDoneEvent, emitSessionStartEvent, emitToolCallEvent, emitUserFeedbackEvent } from './genAiEvents'; export { GenAiMetrics } from './genAiMetrics'; export { collectSystemTextsFromRequestBody, extractTextFromContent, normalizeProviderMessages, toInputMessages, toOutputMessages, toSystemInstructions, toToolDefinitions, truncateForOTel } from './messageFormatters'; From 01bdbbe2d4e546dd5c373c29d48c400e0c3755e3 Mon Sep 17 00:00:00 2001 From: Zhichao Li Date: Fri, 22 May 2026 11:10:43 -0700 Subject: [PATCH 05/13] feat(otel): stamp github.copilot.agent.type on invoke_agent span --- .../copilot/src/extension/intents/node/toolCallingLoop.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/extensions/copilot/src/extension/intents/node/toolCallingLoop.ts b/extensions/copilot/src/extension/intents/node/toolCallingLoop.ts index 1399958d9c4..ce907086b0b 100644 --- a/extensions/copilot/src/extension/intents/node/toolCallingLoop.ts +++ b/extensions/copilot/src/extension/intents/node/toolCallingLoop.ts @@ -22,7 +22,7 @@ import { ILogService } from '../../../platform/log/common/logService'; import { isOpenAIContextManagementResponse, OpenAiFunctionDef } from '../../../platform/networking/common/fetch'; import { IMakeChatRequestOptions } from '../../../platform/networking/common/networking'; import { OpenAIContextManagementResponse } from '../../../platform/networking/common/openai'; -import { CopilotChatAttr, emitAgentTurnEvent, emitSessionStartEvent, GenAiAttr, GenAiMetrics, GenAiOperationName, GenAiProviderName, resolveWorkspaceOTelMetadata, StdAttr, truncateForOTel, workspaceMetadataToOTelAttributes } from '../../../platform/otel/common/index'; +import { CopilotChatAttr, emitAgentTurnEvent, emitSessionStartEvent, GenAiAttr, GenAiMetrics, GenAiOperationName, GenAiProviderName, GitHubCopilotAttr, resolveWorkspaceOTelMetadata, StdAttr, truncateForOTel, workspaceMetadataToOTelAttributes } from '../../../platform/otel/common/index'; import { IOTelService, ISpanHandle, SpanKind, SpanStatusCode } from '../../../platform/otel/common/otelService'; import { IRequestLogger } from '../../../platform/requestLogger/common/requestLogger'; import { getCurrentCapturingToken } from '../../../platform/requestLogger/node/requestLogger'; @@ -738,6 +738,9 @@ export abstract class ToolCallingLoop Date: Fri, 22 May 2026 11:12:29 -0700 Subject: [PATCH 06/13] feat(otel): structured github.copilot.tool.parameters.* on execute_tool spans --- .../tools/vscode-node/toolsService.ts | 16 +++ .../otel/node/extractToolParameters.ts | 117 ++++++++++++++++++ .../node/test/extractToolParameters.spec.ts | 45 +++++++ 3 files changed, 178 insertions(+) create mode 100644 extensions/copilot/src/platform/otel/node/extractToolParameters.ts create mode 100644 extensions/copilot/src/platform/otel/node/test/extractToolParameters.spec.ts diff --git a/extensions/copilot/src/extension/tools/vscode-node/toolsService.ts b/extensions/copilot/src/extension/tools/vscode-node/toolsService.ts index 7702ab47f60..7a2a7907786 100644 --- a/extensions/copilot/src/extension/tools/vscode-node/toolsService.ts +++ b/extensions/copilot/src/extension/tools/vscode-node/toolsService.ts @@ -10,6 +10,7 @@ import { ILogService } from '../../../platform/log/common/logService'; import { IChatEndpoint } from '../../../platform/networking/common/networking'; import { CopilotChatAttr, emitToolCallEvent, GenAiAttr, GenAiMetrics, GenAiOperationName, GenAiToolType, StdAttr, truncateForOTel } from '../../../platform/otel/common/index'; import { IOTelService, SpanKind, SpanStatusCode } from '../../../platform/otel/common/otelService'; +import { extractToolParameters } from '../../../platform/otel/node/extractToolParameters'; import { getCurrentCapturingToken } from '../../../platform/requestLogger/node/requestLogger'; import { IExperimentationService } from '../../../platform/telemetry/common/nullExperimentationService'; import { equals as arraysEqual } from '../../../util/vs/base/common/arrays'; @@ -152,6 +153,21 @@ export class ToolsService extends BaseToolsService { } catch { /* swallow serialization errors */ } } + // Structured `github.copilot.tool.parameters.*` (CLI parity). Always-safe + // attrs (hashes, edit_type) emit unconditionally; raw paths/commands/MCP + // names are gated on captureContent. + try { + const { attrs: paramAttrs, gatedAttrs: gatedParamAttrs } = extractToolParameters(String(name), options.input); + for (const [k, v] of Object.entries(paramAttrs)) { + span.setAttribute(k, v); + } + if (this._otelService.config.captureContent) { + for (const [k, v] of Object.entries(gatedParamAttrs)) { + span.setAttribute(k, v); + } + } + } catch { /* swallow extraction errors */ } + // For runSubagent tool, store this execute_tool span's trace context so the subagent's // invoke_agent span can be parented to THIS tool call (not the grandparent invoke_agent). const chatStreamToolCallId = (options as { chatStreamToolCallId?: string }).chatStreamToolCallId; diff --git a/extensions/copilot/src/platform/otel/node/extractToolParameters.ts b/extensions/copilot/src/platform/otel/node/extractToolParameters.ts new file mode 100644 index 00000000000..b2e38fbc800 --- /dev/null +++ b/extensions/copilot/src/platform/otel/node/extractToolParameters.ts @@ -0,0 +1,117 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { hashTelemetryValue } from '../../../util/node/crypto'; +import { + EditOperationType, + FILE_TOOL_NAMES, + GitHubCopilotAttr, + SHELL_TOOL_NAMES, + TOOL_PARAM_COMMAND_MAX_LEN, +} from '../common/genAiAttributes'; + +/** + * Result of extracting structured parameter attributes from a tool invocation. + * `gatedAttrs` are content-sensitive (raw paths, command text, MCP server + * names) and must only be emitted when `captureContent` is enabled. `attrs` + * are always safe (e.g. SHA-256 hashes, fixed enums). + */ +export interface ToolParameterAttributes { + attrs: Record; + gatedAttrs: Record; +} + +/** + * Port of the CLI's `extractToolParameters` (copilot-agent-runtime PR #8037, + * `src/core/otel/otelGenAI.ts`). Produces `github.copilot.tool.parameters.*` + * attributes for shell, file, skill, and MCP tool calls. + */ +export function extractToolParameters(toolName: string, input: unknown): ToolParameterAttributes { + const attrs: Record = {}; + const gatedAttrs: Record = {}; + + if (typeof input !== 'object' || input === null) { + return { attrs, gatedAttrs }; + } + const obj = input as Record; + + if (SHELL_TOOL_NAMES.has(toolName)) { + const command = pickFirstString(obj, ['command', 'cmd', 'commandLine']); + if (command) { + gatedAttrs[GitHubCopilotAttr.TOOL_PARAM_COMMAND] = + command.length > TOOL_PARAM_COMMAND_MAX_LEN + ? command.slice(0, TOOL_PARAM_COMMAND_MAX_LEN) + : command; + } + } + + if (FILE_TOOL_NAMES.has(toolName)) { + const filePath = pickFirstString(obj, ['file_path', 'filePath', 'path', 'uri']); + if (filePath) { + gatedAttrs[GitHubCopilotAttr.TOOL_PARAM_FILE_PATH] = filePath; + } + const editType = classifyEditType(toolName, obj); + if (editType) { + attrs[GitHubCopilotAttr.TOOL_PARAM_EDIT_TYPE] = editType; + } + } + + const skillName = pickFirstString(obj, ['skill_name', 'skillName', 'skill']); + if (skillName) { + attrs[GitHubCopilotAttr.TOOL_PARAM_SKILL_NAME] = skillName; + } + + // MCP-style tool names are `mcp__`. Extract server + tool name and hash the server. + if (toolName.startsWith('mcp_')) { + const rest = toolName.slice('mcp_'.length); + const underscore = rest.indexOf('_'); + if (underscore > 0) { + const serverName = rest.slice(0, underscore); + const mcpToolName = rest.slice(underscore + 1); + attrs[GitHubCopilotAttr.TOOL_PARAM_MCP_SERVER_NAME_HASH] = hashTelemetryValue(serverName); + attrs[GitHubCopilotAttr.TOOL_PARAM_MCP_TOOL_NAME] = mcpToolName; + gatedAttrs[GitHubCopilotAttr.TOOL_PARAM_MCP_SERVER_NAME] = serverName; + } + } + + return { attrs, gatedAttrs }; +} + +function pickFirstString(obj: Record, keys: readonly string[]): string | undefined { + for (const k of keys) { + const v = obj[k]; + if (typeof v === 'string' && v.length > 0) { + return v; + } + } + return undefined; +} + +function classifyEditType(toolName: string, obj: Record): EditOperationType | undefined { + if (toolName === 'create' || toolName === 'createFile') { + return 'create'; + } + if (toolName === 'insert') { + return 'insert'; + } + if (toolName === 'str_replace' || toolName === 'str_replace_editor' || toolName === 'replaceString') { + return 'str_replace'; + } + if (toolName === 'edit' || toolName === 'applyPatch') { + return 'update'; + } + // `view`/`readFile` have no edit_type. + if (toolName === 'view' || toolName === 'readFile') { + return undefined; + } + // Fallback: heuristic on common arg names. + if (typeof obj.old_str === 'string' || typeof obj.oldString === 'string') { + return 'str_replace'; + } + if (typeof obj.content === 'string' && typeof obj.file_text === 'undefined') { + return 'update'; + } + return undefined; +} diff --git a/extensions/copilot/src/platform/otel/node/test/extractToolParameters.spec.ts b/extensions/copilot/src/platform/otel/node/test/extractToolParameters.spec.ts new file mode 100644 index 00000000000..f0ee8ada004 --- /dev/null +++ b/extensions/copilot/src/platform/otel/node/test/extractToolParameters.spec.ts @@ -0,0 +1,45 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { describe, expect, it } from 'vitest'; +import { GitHubCopilotAttr, TOOL_PARAM_COMMAND_MAX_LEN } from '../../common/genAiAttributes'; +import { extractToolParameters } from '../extractToolParameters'; + +describe('extractToolParameters', () => { + it('returns empty attrs for non-object input', () => { + expect(extractToolParameters('bash', undefined).attrs).toEqual({}); + expect(extractToolParameters('bash', null).gatedAttrs).toEqual({}); + }); + + it('gates shell command behind captureContent and truncates to 256 chars', () => { + const long = 'echo ' + 'x'.repeat(500); + const { attrs, gatedAttrs } = extractToolParameters('bash', { command: long }); + expect(attrs).toEqual({}); + expect(gatedAttrs[GitHubCopilotAttr.TOOL_PARAM_COMMAND]).toBeDefined(); + expect(gatedAttrs[GitHubCopilotAttr.TOOL_PARAM_COMMAND].length).toBe(TOOL_PARAM_COMMAND_MAX_LEN); + }); + + it('emits MCP server hash unconditionally and raw name gated', () => { + const { attrs, gatedAttrs } = extractToolParameters('mcp_github_search_issues', {}); + expect(attrs[GitHubCopilotAttr.TOOL_PARAM_MCP_SERVER_NAME_HASH]).toMatch(/^[a-f0-9]{64}$/); + expect(attrs[GitHubCopilotAttr.TOOL_PARAM_MCP_TOOL_NAME]).toBe('search_issues'); + expect(gatedAttrs[GitHubCopilotAttr.TOOL_PARAM_MCP_SERVER_NAME]).toBe('github'); + }); + + it('classifies file edit operations and gates file_path', () => { + const { attrs, gatedAttrs } = extractToolParameters('str_replace', { + file_path: '/src/app.ts', + old_str: 'foo', + }); + expect(attrs[GitHubCopilotAttr.TOOL_PARAM_EDIT_TYPE]).toBe('str_replace'); + expect(gatedAttrs[GitHubCopilotAttr.TOOL_PARAM_FILE_PATH]).toBe('/src/app.ts'); + }); + + it('returns nothing for tools that do not match any extractor', () => { + const { attrs, gatedAttrs } = extractToolParameters('unknown_tool', { foo: 'bar' }); + expect(attrs).toEqual({}); + expect(gatedAttrs).toEqual({}); + }); +}); From 19e1ad7b74ec8d70696f52a2dbd132ebd65e5ae4 Mon Sep 17 00:00:00 2001 From: Zhichao Li Date: Fri, 22 May 2026 11:13:46 -0700 Subject: [PATCH 07/13] feat(otel): enrich execute_hook with decision, tool_names, duration --- .../chat/vscode-node/chatHookService.ts | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/extensions/copilot/src/extension/chat/vscode-node/chatHookService.ts b/extensions/copilot/src/extension/chat/vscode-node/chatHookService.ts index 25a6b059aaa..84349388673 100644 --- a/extensions/copilot/src/extension/chat/vscode-node/chatHookService.ts +++ b/extensions/copilot/src/extension/chat/vscode-node/chatHookService.ts @@ -11,7 +11,7 @@ import { HookCommandResultKind, IHookCommandResult, IHookExecutor } from '../../ import { IHooksOutputChannel } from '../../../platform/chat/common/hooksOutputChannel'; import { ISessionTranscriptService } from '../../../platform/chat/common/sessionTranscriptService'; import { ILogService } from '../../../platform/log/common/logService'; -import { CopilotChatAttr, GenAiAttr, GenAiOperationName, IOTelService, SpanKind, SpanStatusCode, truncateForOTel } from '../../../platform/otel/common/index'; +import { CopilotChatAttr, GenAiAttr, GenAiOperationName, GitHubCopilotAttr, IOTelService, SpanKind, SpanStatusCode, truncateForOTel } from '../../../platform/otel/common/index'; import { ITelemetryService } from '../../../platform/telemetry/common/telemetry'; import { raceTimeout } from '../../../util/vs/base/common/async'; import { CancellationToken } from '../../../util/vs/base/common/cancellation'; @@ -160,6 +160,12 @@ export class ChatHookService implements IChatHookService { [CopilotChatAttr.HOOK_TYPE]: hookType, 'copilot_chat.hook_command': hookCommand.command, ...(chatSessionId ? { [CopilotChatAttr.CHAT_SESSION_ID]: chatSessionId } : {}), + ...(() => { + const toolName = (commandInput as { tool_name?: unknown }).tool_name; + return typeof toolName === 'string' + ? { [GitHubCopilotAttr.HOOK_TOOL_NAMES]: JSON.stringify([toolName]) } + : {}; + })(), }, }); @@ -172,6 +178,7 @@ export class ChatHookService implements IChatHookService { const sw = StopWatch.create(); const commandResult = await this._hookExecutor.executeCommand(hookCommand, commandInput, effectiveToken); const elapsed = sw.elapsed(); + span.setAttribute(GitHubCopilotAttr.HOOK_DURATION_SECONDS, elapsed / 1000); this._logCommandResult(requestId, hookType, commandResult, elapsed); @@ -181,6 +188,14 @@ export class ChatHookService implements IChatHookService { : 'error'; span.setAttribute(CopilotChatAttr.HOOK_RESULT_KIND, resultKind); + // Map to CLI's `decision` enum for cross-surface dashboards. + const hookDecision = commandResult.kind === HookCommandResultKind.Error + ? 'block' + : commandResult.kind === HookCommandResultKind.NonBlockingError + ? 'non_blocking_error' + : 'pass'; + span.setAttribute(GitHubCopilotAttr.HOOK_DECISION, hookDecision); + if (commandResult.kind === HookCommandResultKind.Error || commandResult.kind === HookCommandResultKind.NonBlockingError) { hasError = true; // Record exit code on error @@ -205,6 +220,10 @@ export class ChatHookService implements IChatHookService { // If stopReason is set (including empty string for "stop without message"), stop processing remaining hooks if (result.stopReason !== undefined) { + // Stop signals from a successful hook still flip the decision to `block` for CLI parity. + if (hookDecision === 'pass') { + span.setAttribute(GitHubCopilotAttr.HOOK_DECISION, 'block'); + } this._log(requestId, hookType, `Stopping: ${result.stopReason}`); this._logService.debug(`[ChatHookService] Stopping after hook: ${result.stopReason}`); break; From 7fc2ce6d2e153cc29da0b6052af5148323ca8b92 Mon Sep 17 00:00:00 2001 From: Zhichao Li Date: Fri, 22 May 2026 11:15:10 -0700 Subject: [PATCH 08/13] docs(otel): document github.copilot.* attributes and mark copilot_chat.repo.* legacy --- .../docs/monitoring/agent_monitoring.md | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/extensions/copilot/docs/monitoring/agent_monitoring.md b/extensions/copilot/docs/monitoring/agent_monitoring.md index 9ccaefef64f..a157960ca83 100644 --- a/extensions/copilot/docs/monitoring/agent_monitoring.md +++ b/extensions/copilot/docs/monitoring/agent_monitoring.md @@ -114,6 +114,18 @@ OTel is **off by default** with zero overhead. It activates when: ## What Gets Exported +> ### Attribute namespaces & dual-emit policy +> +> Copilot Chat emits OTel attributes under three namespaces: +> +> - **`gen_ai.*`** — [OTel GenAI Semantic Conventions](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/gen-ai/). Use these whenever a standard key exists. +> - **`github.copilot.*`** — The canonical Copilot-specific namespace, shared with the [Copilot CLI runtime](https://github.com/github/copilot-agent-runtime). Prefer this for new dashboards and alerts. +> - **`copilot_chat.*`** — The original VS Code extension namespace. Several keys (notably `copilot_chat.repo.*` and `gen_ai.usage.reasoning_tokens`) are now **dual-emitted alongside the `github.copilot.*` equivalents**. Tables below mark these rows as **Legacy** with a pointer to the preferred key. +> +> Legacy keys continue to emit indefinitely so existing collectors, dashboards, and downstream consumers (Agent Debug Log, Chronicle, SQLite span store) keep working without changes. There is no sunset date. +> +> **Cross-surface compatibility caveat:** A few enums diverge between VS Code and the CLI runtime (e.g. `skill.source` carries VS Code's `PromptFileSource` value on extension-emitted spans and the CLI's own enum on CLI-emitted spans). Where this matters, the divergence is called out on the relevant attribute row. + ### Traces Copilot Chat emits a hierarchical span tree for each agent interaction: @@ -141,6 +153,14 @@ invoke_agent copilot [~15s] | `gen_ai.usage.output_tokens` | Recommended | `3200` | | `gen_ai.usage.cache_read.input_tokens` | When available | `8000` | | `gen_ai.usage.cache_creation.input_tokens` | When available | `4200` | +| `github.copilot.agent.type` | Always | `builtin` \| `custom` \| `plugin` | +| `github.copilot.git.repository` | When in a repo | `https://github.com/microsoft/vscode.git` | +| `github.copilot.git.branch` | When in a repo | `main` | +| `github.copilot.git.commit_sha` | When in a repo | `deadbeef...` | +| `github.copilot.github.org` | GitHub remotes only | `microsoft` | +| `copilot_chat.repo.remote_url` | **Legacy** — prefer `github.copilot.git.repository` | `https://github.com/...` | +| `copilot_chat.repo.head_branch_name` | **Legacy** — prefer `github.copilot.git.branch` | `main` | +| `copilot_chat.repo.head_commit_hash` | **Legacy** — prefer `github.copilot.git.commit_sha` | `deadbeef...` | | `copilot_chat.turn_count` | Always | `4` | | `error.type` | On error | `Error` | | `gen_ai.input.messages` | Opt-in (captureContent) | `[{"role":"user",...}]` | @@ -166,6 +186,8 @@ invoke_agent copilot [~15s] | `gen_ai.usage.output_tokens` | On response | `250` | | `gen_ai.usage.cache_read.input_tokens` | When available | `1200` | | `gen_ai.usage.cache_creation.input_tokens` | When available | `300` | +| `gen_ai.usage.reasoning.output_tokens` | When available | `512` | +| `gen_ai.usage.reasoning_tokens` | **Legacy** — prefer `gen_ai.usage.reasoning.output_tokens` | `512` | | `copilot_chat.time_to_first_token` | On response | `450` | | `server.address` | When available | `api.github.com` | | `copilot_chat.debug_name` | When available | `agentMode` | @@ -182,6 +204,13 @@ invoke_agent copilot [~15s] | `gen_ai.tool.type` | Required | `function` or `extension` (MCP tools) | | `gen_ai.tool.call.id` | Recommended | `call_abc123` | | `gen_ai.tool.description` | When available | `Read the contents of a file` | +| `github.copilot.tool.parameters.edit_type` | Edit tools | `create` \| `update` \| `str_replace` \| `insert` | +| `github.copilot.tool.parameters.skill_name` | When invoking a skill | `auto-perf-optimize` | +| `github.copilot.tool.parameters.mcp_server_name_hash` | MCP tools | SHA-256 hex of server name | +| `github.copilot.tool.parameters.mcp_tool_name` | MCP tools | `search_issues` | +| `github.copilot.tool.parameters.command` | Shell tools, opt-in (captureContent) | `npm test` (truncated to 256 chars) | +| `github.copilot.tool.parameters.file_path` | File tools, opt-in (captureContent) | `/src/app.ts` | +| `github.copilot.tool.parameters.mcp_server_name` | MCP tools, opt-in (captureContent) | `github` | | `error.type` | On error | `FileNotFoundError` | | `gen_ai.tool.call.arguments` | Opt-in (captureContent) | `{"filePath":"/src/index.ts"}` | | `gen_ai.tool.call.result` | Opt-in (captureContent) | `(file contents or summary)` | @@ -628,6 +657,18 @@ copilot-chat invoke_agent claude [~33s] **`execute_hook`** — one span per Claude hook execution (e.g., `Stop` hooks). +| Attribute | Requirement | Example | +|---|---|---| +| `gen_ai.operation.name` | Required | `execute_hook` | +| `copilot_chat.hook_type` | Required | `PreToolUse` | +| `copilot_chat.hook_result_kind` | Always | `success` \| `error` \| `non_blocking_error` | +| `github.copilot.hook.decision` | Always | `pass` \| `block` \| `non_blocking_error` | +| `github.copilot.hook.duration` | Always | `0.142` (seconds) | +| `github.copilot.hook.tool_names` | When tool-scoped | `["bash"]` (JSON array) | +| `copilot_chat.hook_input` | Always | hook input payload (truncated) | +| `copilot_chat.hook_output` | On success | hook stdout (truncated) | +| `error.type` | On error | `Error` | + --- ## Interpreting the Data From 2e9b3cf43e9c3e267b8b1b5c358d3a9001aefb48 Mon Sep 17 00:00:00 2001 From: Zhichao Li Date: Fri, 22 May 2026 11:15:51 -0700 Subject: [PATCH 09/13] docs(otel): add dual-emit policy and github.copilot.* guidance to OTel skill --- .github/skills/otel/SKILL.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/.github/skills/otel/SKILL.md b/.github/skills/otel/SKILL.md index af5c826ed14..73829290251 100644 --- a/.github/skills/otel/SKILL.md +++ b/.github/skills/otel/SKILL.md @@ -78,6 +78,29 @@ extensions/copilot/src/extension/ └── otlpFormatConversion.ts # OTLP ↔ in-memory span format ``` +## 3a. Attribute namespaces & dual-emit policy + +Three namespaces coexist on extension-emitted spans: + +| Namespace | Purpose | Status | +|---|---|---| +| `gen_ai.*` | OTel GenAI Semantic Conventions. Use whenever a standard key exists. | Canonical | +| `github.copilot.*` | Copilot-specific vendor namespace, shared with the [Copilot CLI runtime](https://github.com/github/copilot-agent-runtime) (see `src/core/otel/otelGenAI.ts`). | **Preferred — new attributes go here.** | +| `copilot_chat.*` | Original VS Code-only namespace. Several keys remain for backwards compatibility. | **Legacy — keep emitting; do not add new keys here.** | + +### Dual-emit rules + +- When adding a new attribute that belongs to Copilot's vendor namespace, emit it under `github.copilot.*` only — do **not** introduce a `copilot_chat.*` twin. +- When **renaming** an existing `copilot_chat.*` attribute to its `github.copilot.*` equivalent (e.g., `copilot_chat.repo.*` → `github.copilot.git.*`, `gen_ai.usage.reasoning_tokens` → `gen_ai.usage.reasoning.output_tokens`), **dual-emit both keys indefinitely**. Downstream readers (Agent Debug Log, Chronicle, SQLite span store, OTLP collectors) may depend on the legacy key. +- Mark the legacy row in [agent_monitoring.md](../../../extensions/copilot/docs/monitoring/agent_monitoring.md) with **Legacy** in the "Requirement" column and a pointer to the preferred key. No sunset date — legacy keys live on indefinitely. +- Hash sensitive identifiers (e.g., MCP server names) with `hashTelemetryValue` from [`util/node/crypto.ts`](../../../extensions/copilot/src/util/node/crypto.ts). Emit hashes unconditionally; raw values only when `captureContent` is enabled. + +### Cross-surface enum divergence + +A few enums emitted under `github.copilot.*` differ between VS Code and the CLI runtime — flag these explicitly in docs and dashboards: + +- `github.copilot.skill.source`: VS Code emits the raw `PromptFileSource` enum value (`github-workspace`, `copilot-personal`, `agents-workspace`, `extension-contribution`, …). The CLI emits its own enum (`project`, `personal-copilot`, `plugin`, `builtin`, `remote`, …). Reconcile server-side if joining the two surfaces. + ## 4. Service Layer & Selection `IOTelService` ([otelService.ts](../../../extensions/copilot/src/platform/otel/common/otelService.ts)) is the only abstraction consumers should depend on — never import the OTel SDK directly outside `node/otelServiceImpl.ts`. Three implementations: From 57ce213d996ad0d79b085e8c906a8253039f5bab Mon Sep 17 00:00:00 2001 From: Zhichao Li Date: Sat, 23 May 2026 22:34:23 -0700 Subject: [PATCH 10/13] fix(otel): include VS Code snake_case tool names in tool.parameters extraction VS Code's ToolName enum uses snake_case identifiers (create_file, replace_string_in_file, apply_patch, insert_edit_into_file, multi_replace_string_in_file, edit_notebook_file, read_file). The initial CLI-derived FILE_TOOL_NAMES set only contained the Copilot CLI/Claude camelCase variants, so tool.parameters.edit_type and tool.parameters.file_path were never emitted on real VS Code tool spans. Add the missing names and extend classifyEditType to handle them. Verified end-to-end in Aspire on a trace where create_file now correctly emits edit_type=create with the file_path gated by captureContent. --- .../platform/otel/common/genAiAttributes.ts | 15 ++++++++++++- .../otel/node/extractToolParameters.ts | 22 ++++++++++++++----- .../node/test/extractToolParameters.spec.ts | 10 +++++++++ 3 files changed, 41 insertions(+), 6 deletions(-) diff --git a/extensions/copilot/src/platform/otel/common/genAiAttributes.ts b/extensions/copilot/src/platform/otel/common/genAiAttributes.ts index bd56d5ef4b2..b7d19e73ee3 100644 --- a/extensions/copilot/src/platform/otel/common/genAiAttributes.ts +++ b/extensions/copilot/src/platform/otel/common/genAiAttributes.ts @@ -256,8 +256,13 @@ export const SHELL_TOOL_NAMES: ReadonlySet = new Set([ 'run_in_terminal', ]); -/** Tool names treated as file tools for parameter extraction. */ +/** + * Tool names treated as file tools for parameter extraction. Includes both + * Copilot CLI names (camelCase / Claude-style) and VS Code's `ToolName` enum + * values (snake_case, see `extension/tools/common/toolNames.ts`). + */ export const FILE_TOOL_NAMES: ReadonlySet = new Set([ + // Copilot CLI / Claude-style names 'view', 'create', 'edit', @@ -268,4 +273,12 @@ export const FILE_TOOL_NAMES: ReadonlySet = new Set([ 'createFile', 'replaceString', 'applyPatch', + // VS Code tool names (ToolName enum) + 'read_file', + 'create_file', + 'apply_patch', + 'insert_edit_into_file', + 'replace_string_in_file', + 'multi_replace_string_in_file', + 'edit_notebook_file', ]); diff --git a/extensions/copilot/src/platform/otel/node/extractToolParameters.ts b/extensions/copilot/src/platform/otel/node/extractToolParameters.ts index b2e38fbc800..367d70df5dd 100644 --- a/extensions/copilot/src/platform/otel/node/extractToolParameters.ts +++ b/extensions/copilot/src/platform/otel/node/extractToolParameters.ts @@ -90,20 +90,32 @@ function pickFirstString(obj: Record, keys: readonly string[]): } function classifyEditType(toolName: string, obj: Record): EditOperationType | undefined { - if (toolName === 'create' || toolName === 'createFile') { + if (toolName === 'create' || toolName === 'createFile' || toolName === 'create_file') { return 'create'; } if (toolName === 'insert') { return 'insert'; } - if (toolName === 'str_replace' || toolName === 'str_replace_editor' || toolName === 'replaceString') { + if ( + toolName === 'str_replace' || + toolName === 'str_replace_editor' || + toolName === 'replaceString' || + toolName === 'replace_string_in_file' || + toolName === 'multi_replace_string_in_file' + ) { return 'str_replace'; } - if (toolName === 'edit' || toolName === 'applyPatch') { + if ( + toolName === 'edit' || + toolName === 'applyPatch' || + toolName === 'apply_patch' || + toolName === 'insert_edit_into_file' || + toolName === 'edit_notebook_file' + ) { return 'update'; } - // `view`/`readFile` have no edit_type. - if (toolName === 'view' || toolName === 'readFile') { + // `view`/`readFile`/`read_file` have no edit_type. + if (toolName === 'view' || toolName === 'readFile' || toolName === 'read_file') { return undefined; } // Fallback: heuristic on common arg names. diff --git a/extensions/copilot/src/platform/otel/node/test/extractToolParameters.spec.ts b/extensions/copilot/src/platform/otel/node/test/extractToolParameters.spec.ts index f0ee8ada004..f39486c9c77 100644 --- a/extensions/copilot/src/platform/otel/node/test/extractToolParameters.spec.ts +++ b/extensions/copilot/src/platform/otel/node/test/extractToolParameters.spec.ts @@ -42,4 +42,14 @@ describe('extractToolParameters', () => { expect(attrs).toEqual({}); expect(gatedAttrs).toEqual({}); }); + + it('classifies VS Code snake_case edit tool names', () => { + expect(extractToolParameters('create_file', { filePath: '/a.ts' }).attrs[GitHubCopilotAttr.TOOL_PARAM_EDIT_TYPE]).toBe('create'); + expect(extractToolParameters('replace_string_in_file', { filePath: '/a.ts' }).attrs[GitHubCopilotAttr.TOOL_PARAM_EDIT_TYPE]).toBe('str_replace'); + expect(extractToolParameters('multi_replace_string_in_file', { filePath: '/a.ts' }).attrs[GitHubCopilotAttr.TOOL_PARAM_EDIT_TYPE]).toBe('str_replace'); + expect(extractToolParameters('apply_patch', { filePath: '/a.ts' }).attrs[GitHubCopilotAttr.TOOL_PARAM_EDIT_TYPE]).toBe('update'); + expect(extractToolParameters('insert_edit_into_file', { filePath: '/a.ts' }).attrs[GitHubCopilotAttr.TOOL_PARAM_EDIT_TYPE]).toBe('update'); + expect(extractToolParameters('edit_notebook_file', { filePath: '/a.ipynb' }).attrs[GitHubCopilotAttr.TOOL_PARAM_EDIT_TYPE]).toBe('update'); + expect(extractToolParameters('read_file', { filePath: '/a.ts' }).attrs[GitHubCopilotAttr.TOOL_PARAM_EDIT_TYPE]).toBeUndefined(); + }); }); From 9fa43ae86ef5c2b2d95efa4bf960af37a23c98fa Mon Sep 17 00:00:00 2001 From: Zhichao Li Date: Sat, 23 May 2026 23:29:18 -0700 Subject: [PATCH 11/13] fix(otel): address PR review feedback - Move USAGE_REASONING_OUTPUT_TOKENS from GitHubCopilotAttr to GenAiAttr (its value is gen_ai.usage.reasoning.output_tokens, not a github.copilot.* key) - Accept Anthropic-style mcp__server__tool double-underscore format in addition to mcp_server_tool single-underscore - Add a test for the double-underscore variant --- .../src/extension/prompt/node/chatMLFetcher.ts | 4 ++-- .../src/platform/otel/common/genAiAttributes.ts | 7 +++---- .../platform/otel/node/extractToolParameters.ts | 16 ++++++++++++++-- .../otel/node/test/extractToolParameters.spec.ts | 7 +++++++ 4 files changed, 26 insertions(+), 8 deletions(-) diff --git a/extensions/copilot/src/extension/prompt/node/chatMLFetcher.ts b/extensions/copilot/src/extension/prompt/node/chatMLFetcher.ts index 52fc975ec4a..cc5f00e0b48 100644 --- a/extensions/copilot/src/extension/prompt/node/chatMLFetcher.ts +++ b/extensions/copilot/src/extension/prompt/node/chatMLFetcher.ts @@ -28,7 +28,7 @@ import { sendEngineMessagesTelemetry } from '../../../platform/networking/node/c import { CAPIWebSocketErrorEvent, IChatWebSocketManager, isCAPIWebSocketError } from '../../../platform/networking/node/chatWebSocketManager'; import { sendCommunicationErrorTelemetry } from '../../../platform/networking/node/stream'; import { ChatFailKind, ChatRequestCanceled, ChatRequestFailed, ChatResults, FetchResponseKind } from '../../../platform/openai/node/fetch'; -import { collectSystemTextsFromRequestBody, CopilotChatAttr, emitInferenceDetailsEvent, GenAiAttr, GenAiMetrics, GenAiOperationName, GenAiProviderName, GitHubCopilotAttr, normalizeProviderMessages, StdAttr, toSystemInstructions, toToolDefinitions, truncateForOTel } from '../../../platform/otel/common/index'; +import { collectSystemTextsFromRequestBody, CopilotChatAttr, emitInferenceDetailsEvent, GenAiAttr, GenAiMetrics, GenAiOperationName, GenAiProviderName, normalizeProviderMessages, StdAttr, toSystemInstructions, toToolDefinitions, truncateForOTel } from '../../../platform/otel/common/index'; import { IOTelService, ISpanHandle, SpanKind, SpanStatusCode } from '../../../platform/otel/common/otelService'; import { IRequestLogger } from '../../../platform/requestLogger/common/requestLogger'; import { getCurrentCapturingToken } from '../../../platform/requestLogger/node/requestLogger'; @@ -421,7 +421,7 @@ export class ChatMLFetcherImpl extends AbstractChatMLFetcher { ...(result.usage.completion_tokens_details?.reasoning_tokens ? { [GenAiAttr.USAGE_REASONING_TOKENS]: result.usage.completion_tokens_details.reasoning_tokens, - [GitHubCopilotAttr.USAGE_REASONING_OUTPUT_TOKENS]: result.usage.completion_tokens_details.reasoning_tokens, + [GenAiAttr.USAGE_REASONING_OUTPUT_TOKENS]: result.usage.completion_tokens_details.reasoning_tokens, } : {}), ...(typeof result.usage.copilot_usage?.total_nano_aiu === 'number' diff --git a/extensions/copilot/src/platform/otel/common/genAiAttributes.ts b/extensions/copilot/src/platform/otel/common/genAiAttributes.ts index b7d19e73ee3..563facd9bee 100644 --- a/extensions/copilot/src/platform/otel/common/genAiAttributes.ts +++ b/extensions/copilot/src/platform/otel/common/genAiAttributes.ts @@ -65,8 +65,10 @@ export const GenAiAttr = { USAGE_OUTPUT_TOKENS: 'gen_ai.usage.output_tokens', USAGE_CACHE_READ_INPUT_TOKENS: 'gen_ai.usage.cache_read.input_tokens', USAGE_CACHE_CREATION_INPUT_TOKENS: 'gen_ai.usage.cache_creation.input_tokens', - /** Custom: reasoning/thinking token count (not yet standardized in GenAI conventions) */ + /** Legacy: reasoning/thinking token count. Prefer `USAGE_REASONING_OUTPUT_TOKENS`; this key is kept for backwards compatibility. */ USAGE_REASONING_TOKENS: 'gen_ai.usage.reasoning_tokens', + /** Reasoning/thinking output token count (semantic-convention-aligned). Dual-emitted alongside `USAGE_REASONING_TOKENS`. */ + USAGE_REASONING_OUTPUT_TOKENS: 'gen_ai.usage.reasoning.output_tokens', // Conversation CONVERSATION_ID: 'gen_ai.conversation.id', @@ -235,9 +237,6 @@ export const GitHubCopilotAttr = { TOOL_PARAM_MCP_SERVER_NAME: 'github.copilot.tool.parameters.mcp_server_name', /** MCP tool name (the part after the `mcp__` prefix). */ TOOL_PARAM_MCP_TOOL_NAME: 'github.copilot.tool.parameters.mcp_tool_name', - - /** Reasoning/thinking token count (semantic-convention-aligned). Dual of `gen_ai.usage.reasoning_tokens`. */ - USAGE_REASONING_OUTPUT_TOKENS: 'gen_ai.usage.reasoning.output_tokens', } as const; export type AgentType = 'builtin' | 'plugin' | 'custom'; diff --git a/extensions/copilot/src/platform/otel/node/extractToolParameters.ts b/extensions/copilot/src/platform/otel/node/extractToolParameters.ts index 367d70df5dd..0e90686cd76 100644 --- a/extensions/copilot/src/platform/otel/node/extractToolParameters.ts +++ b/extensions/copilot/src/platform/otel/node/extractToolParameters.ts @@ -63,8 +63,20 @@ export function extractToolParameters(toolName: string, input: unknown): ToolPar attrs[GitHubCopilotAttr.TOOL_PARAM_SKILL_NAME] = skillName; } - // MCP-style tool names are `mcp__`. Extract server + tool name and hash the server. - if (toolName.startsWith('mcp_')) { + // MCP-style tool names: VS Code/CLI use `mcp__`; Anthropic-style + // references use `mcp____`. Accept both: try double-underscore + // first, then fall back to single. + if (toolName.startsWith('mcp__')) { + const rest = toolName.slice('mcp__'.length); + const sep = rest.indexOf('__'); + if (sep > 0) { + const serverName = rest.slice(0, sep); + const mcpToolName = rest.slice(sep + 2); + attrs[GitHubCopilotAttr.TOOL_PARAM_MCP_SERVER_NAME_HASH] = hashTelemetryValue(serverName); + attrs[GitHubCopilotAttr.TOOL_PARAM_MCP_TOOL_NAME] = mcpToolName; + gatedAttrs[GitHubCopilotAttr.TOOL_PARAM_MCP_SERVER_NAME] = serverName; + } + } else if (toolName.startsWith('mcp_')) { const rest = toolName.slice('mcp_'.length); const underscore = rest.indexOf('_'); if (underscore > 0) { diff --git a/extensions/copilot/src/platform/otel/node/test/extractToolParameters.spec.ts b/extensions/copilot/src/platform/otel/node/test/extractToolParameters.spec.ts index f39486c9c77..844b0013c75 100644 --- a/extensions/copilot/src/platform/otel/node/test/extractToolParameters.spec.ts +++ b/extensions/copilot/src/platform/otel/node/test/extractToolParameters.spec.ts @@ -28,6 +28,13 @@ describe('extractToolParameters', () => { expect(gatedAttrs[GitHubCopilotAttr.TOOL_PARAM_MCP_SERVER_NAME]).toBe('github'); }); + it('also handles Anthropic-style mcp__server__tool double-underscore format', () => { + const { attrs, gatedAttrs } = extractToolParameters('mcp__github__list_issues', {}); + expect(attrs[GitHubCopilotAttr.TOOL_PARAM_MCP_SERVER_NAME_HASH]).toMatch(/^[a-f0-9]{64}$/); + expect(attrs[GitHubCopilotAttr.TOOL_PARAM_MCP_TOOL_NAME]).toBe('list_issues'); + expect(gatedAttrs[GitHubCopilotAttr.TOOL_PARAM_MCP_SERVER_NAME]).toBe('github'); + }); + it('classifies file edit operations and gates file_path', () => { const { attrs, gatedAttrs } = extractToolParameters('str_replace', { file_path: '/src/app.ts', From 48cde2511dee55146861df3ebd03b3d9c336b64b Mon Sep 17 00:00:00 2001 From: Zhichao Li Date: Sat, 23 May 2026 23:34:42 -0700 Subject: [PATCH 12/13] fix(otel): avoid undefined-branch type in execute_hook attributes spread --- .../src/extension/chat/vscode-node/chatHookService.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/extensions/copilot/src/extension/chat/vscode-node/chatHookService.ts b/extensions/copilot/src/extension/chat/vscode-node/chatHookService.ts index 84349388673..2367815f788 100644 --- a/extensions/copilot/src/extension/chat/vscode-node/chatHookService.ts +++ b/extensions/copilot/src/extension/chat/vscode-node/chatHookService.ts @@ -153,6 +153,9 @@ export class ChatHookService implements IChatHookService { const inputForLog = this._redactForLogging(commandInput as Record); this._log(requestId, hookType, `Input: ${JSON.stringify(inputForLog)}`); + const hookToolName = (commandInput as { tool_name?: unknown }).tool_name; + const hookToolNamesJson = typeof hookToolName === 'string' ? JSON.stringify([hookToolName]) : undefined; + const span = this._otelService.startSpan(`execute_hook ${hookType}`, { kind: SpanKind.INTERNAL, attributes: { @@ -160,12 +163,7 @@ export class ChatHookService implements IChatHookService { [CopilotChatAttr.HOOK_TYPE]: hookType, 'copilot_chat.hook_command': hookCommand.command, ...(chatSessionId ? { [CopilotChatAttr.CHAT_SESSION_ID]: chatSessionId } : {}), - ...(() => { - const toolName = (commandInput as { tool_name?: unknown }).tool_name; - return typeof toolName === 'string' - ? { [GitHubCopilotAttr.HOOK_TOOL_NAMES]: JSON.stringify([toolName]) } - : {}; - })(), + ...(hookToolNamesJson ? { [GitHubCopilotAttr.HOOK_TOOL_NAMES]: hookToolNamesJson } : {}), }, }); From ec79deaf7c62f2079f1da46155c78e1be49ad26b Mon Sep 17 00:00:00 2001 From: Zhichao Li Date: Sat, 23 May 2026 23:40:57 -0700 Subject: [PATCH 13/13] Chat OTel: drop sprint plan, tighten comments and docs --- .github/skills/otel/SKILL.md | 8 +---- .../docs/monitoring/agent_monitoring.md | 6 ++-- .../copilot/docs/monitoring/sprint.plan.md | 36 ------------------- .../chat/vscode-node/chatHookService.ts | 3 +- .../extension/intents/node/toolCallingLoop.ts | 2 -- .../tools/vscode-node/toolsService.ts | 5 ++- .../platform/otel/common/genAiAttributes.ts | 13 ++++--- .../otel/common/workspaceOTelMetadata.ts | 2 +- .../otel/node/extractToolParameters.ts | 12 +++---- 9 files changed, 19 insertions(+), 68 deletions(-) delete mode 100644 extensions/copilot/docs/monitoring/sprint.plan.md diff --git a/.github/skills/otel/SKILL.md b/.github/skills/otel/SKILL.md index 73829290251..c952f23b353 100644 --- a/.github/skills/otel/SKILL.md +++ b/.github/skills/otel/SKILL.md @@ -85,7 +85,7 @@ Three namespaces coexist on extension-emitted spans: | Namespace | Purpose | Status | |---|---|---| | `gen_ai.*` | OTel GenAI Semantic Conventions. Use whenever a standard key exists. | Canonical | -| `github.copilot.*` | Copilot-specific vendor namespace, shared with the [Copilot CLI runtime](https://github.com/github/copilot-agent-runtime) (see `src/core/otel/otelGenAI.ts`). | **Preferred — new attributes go here.** | +| `github.copilot.*` | Copilot-specific vendor namespace. | **Preferred — new attributes go here.** | | `copilot_chat.*` | Original VS Code-only namespace. Several keys remain for backwards compatibility. | **Legacy — keep emitting; do not add new keys here.** | ### Dual-emit rules @@ -95,12 +95,6 @@ Three namespaces coexist on extension-emitted spans: - Mark the legacy row in [agent_monitoring.md](../../../extensions/copilot/docs/monitoring/agent_monitoring.md) with **Legacy** in the "Requirement" column and a pointer to the preferred key. No sunset date — legacy keys live on indefinitely. - Hash sensitive identifiers (e.g., MCP server names) with `hashTelemetryValue` from [`util/node/crypto.ts`](../../../extensions/copilot/src/util/node/crypto.ts). Emit hashes unconditionally; raw values only when `captureContent` is enabled. -### Cross-surface enum divergence - -A few enums emitted under `github.copilot.*` differ between VS Code and the CLI runtime — flag these explicitly in docs and dashboards: - -- `github.copilot.skill.source`: VS Code emits the raw `PromptFileSource` enum value (`github-workspace`, `copilot-personal`, `agents-workspace`, `extension-contribution`, …). The CLI emits its own enum (`project`, `personal-copilot`, `plugin`, `builtin`, `remote`, …). Reconcile server-side if joining the two surfaces. - ## 4. Service Layer & Selection `IOTelService` ([otelService.ts](../../../extensions/copilot/src/platform/otel/common/otelService.ts)) is the only abstraction consumers should depend on — never import the OTel SDK directly outside `node/otelServiceImpl.ts`. Three implementations: diff --git a/extensions/copilot/docs/monitoring/agent_monitoring.md b/extensions/copilot/docs/monitoring/agent_monitoring.md index a157960ca83..fe8c2c08fbe 100644 --- a/extensions/copilot/docs/monitoring/agent_monitoring.md +++ b/extensions/copilot/docs/monitoring/agent_monitoring.md @@ -119,12 +119,10 @@ OTel is **off by default** with zero overhead. It activates when: > Copilot Chat emits OTel attributes under three namespaces: > > - **`gen_ai.*`** — [OTel GenAI Semantic Conventions](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/gen-ai/). Use these whenever a standard key exists. -> - **`github.copilot.*`** — The canonical Copilot-specific namespace, shared with the [Copilot CLI runtime](https://github.com/github/copilot-agent-runtime). Prefer this for new dashboards and alerts. -> - **`copilot_chat.*`** — The original VS Code extension namespace. Several keys (notably `copilot_chat.repo.*` and `gen_ai.usage.reasoning_tokens`) are now **dual-emitted alongside the `github.copilot.*` equivalents**. Tables below mark these rows as **Legacy** with a pointer to the preferred key. +> - **`github.copilot.*`** — Canonical Copilot-specific namespace. Prefer this for new dashboards and alerts. +> - **`copilot_chat.*`** — Original VS Code extension namespace. Several keys (notably `copilot_chat.repo.*` and `gen_ai.usage.reasoning_tokens`) are now **dual-emitted alongside the `github.copilot.*` equivalents**. Tables below mark these rows as **Legacy** with a pointer to the preferred key. > > Legacy keys continue to emit indefinitely so existing collectors, dashboards, and downstream consumers (Agent Debug Log, Chronicle, SQLite span store) keep working without changes. There is no sunset date. -> -> **Cross-surface compatibility caveat:** A few enums diverge between VS Code and the CLI runtime (e.g. `skill.source` carries VS Code's `PromptFileSource` value on extension-emitted spans and the CLI's own enum on CLI-emitted spans). Where this matters, the divergence is called out on the relevant attribute row. ### Traces diff --git a/extensions/copilot/docs/monitoring/sprint.plan.md b/extensions/copilot/docs/monitoring/sprint.plan.md deleted file mode 100644 index 6e47a509427..00000000000 --- a/extensions/copilot/docs/monitoring/sprint.plan.md +++ /dev/null @@ -1,36 +0,0 @@ -# OTel CLI Parity Sprint — branch `zhichli/oteladdition` - -Reference: copilot-agent-runtime CLI PR #8037 (`e8a24076831b09a17f7f641b33885ddd5fcb4b3e`). - -Goal: bring VS Code's extension-side OTel up to the new `github.copilot.*` shape introduced in the CLI, without breaking existing `copilot_chat.*` consumers (Agent Debug Log, Chronicle, OTLP exporters, SQLite span store). - -## Locked design decisions - -| # | Decision | -|---|---| -| 1 | MCP server names hashed by default (SHA-256 hex). Raw name only when `captureContent=true`. | -| 2 | `agent.type` derived from existing `modeInstructions2.isBuiltin` flag (`builtin` / `custom`). A richer registry-source field is a follow-up. | -| 3 | Skill source emitted verbatim as VS Code's `PromptFileSource` enum value (no CLI mapping). Deferred — skill emission not part of this PR. | -| 4 | Indefinite dual-emit; legacy `copilot_chat.*` keys keep emitting, marked **Legacy** in doc, no sunset. | - -## Tasks (atomic commits) - -1. **feat(otel): add `github.copilot.*` attribute constants and hash helper** — `genAiAttributes.ts` -2. **feat(otel): dual-emit git context under `github.copilot.git.*`** — `workspaceOTelMetadata.ts` -3. **feat(otel): rename reasoning tokens key with dual-emit** — `chatMLFetcher.ts`, `otelSqliteStore.ts` -4. **feat(otel): stamp `github.copilot.agent.type` on `invoke_agent`** — `toolCallingLoop.ts` -5. **feat(otel): structured `github.copilot.tool.parameters.*` on `execute_tool`** — `toolsService.ts` -6. **feat(otel): enrich `execute_hook` with `decision` / `tool_names` / `duration_seconds`** — `chatHookService.ts` -7. **docs(otel): document `github.copilot.*` attributes and mark `copilot_chat.repo.*` legacy** — `agent_monitoring.md` -8. **docs(otel): add dual-emit policy section to OTel skill** — `.github/skills/otel/SKILL.md` - -## Deferred to follow-up PRs - -- `github.copilot.skill.invoked` span event with raw `PromptFileSource`. -- `github.copilot.mcp.server.lifecycle` event + `connection.count` counter. -- `github.copilot.context.{skills,mcp_server_names,custom_agent_names}` snapshot attrs on `invoke_agent`. -- Mode/agent registry `source` field for the richer `agent.type` enum. - -## Hiccups & Notes - -(Filled in during execution.) diff --git a/extensions/copilot/src/extension/chat/vscode-node/chatHookService.ts b/extensions/copilot/src/extension/chat/vscode-node/chatHookService.ts index 2367815f788..d65bf8ac5fd 100644 --- a/extensions/copilot/src/extension/chat/vscode-node/chatHookService.ts +++ b/extensions/copilot/src/extension/chat/vscode-node/chatHookService.ts @@ -186,7 +186,6 @@ export class ChatHookService implements IChatHookService { : 'error'; span.setAttribute(CopilotChatAttr.HOOK_RESULT_KIND, resultKind); - // Map to CLI's `decision` enum for cross-surface dashboards. const hookDecision = commandResult.kind === HookCommandResultKind.Error ? 'block' : commandResult.kind === HookCommandResultKind.NonBlockingError @@ -218,7 +217,7 @@ export class ChatHookService implements IChatHookService { // If stopReason is set (including empty string for "stop without message"), stop processing remaining hooks if (result.stopReason !== undefined) { - // Stop signals from a successful hook still flip the decision to `block` for CLI parity. + // A stop signal from a successful hook still counts as a block. if (hookDecision === 'pass') { span.setAttribute(GitHubCopilotAttr.HOOK_DECISION, 'block'); } diff --git a/extensions/copilot/src/extension/intents/node/toolCallingLoop.ts b/extensions/copilot/src/extension/intents/node/toolCallingLoop.ts index ce907086b0b..b054cde73cc 100644 --- a/extensions/copilot/src/extension/intents/node/toolCallingLoop.ts +++ b/extensions/copilot/src/extension/intents/node/toolCallingLoop.ts @@ -738,8 +738,6 @@ export abstract class ToolCallingLoop = new Set([ ]); /** - * Tool names treated as file tools for parameter extraction. Includes both - * Copilot CLI names (camelCase / Claude-style) and VS Code's `ToolName` enum - * values (snake_case, see `extension/tools/common/toolNames.ts`). + * Tool names treated as file tools for parameter extraction. Covers VS Code's + * `ToolName` enum values (snake_case, see `extension/tools/common/toolNames.ts`) + * and the camelCase / Claude-style variants seen on external surfaces. */ export const FILE_TOOL_NAMES: ReadonlySet = new Set([ - // Copilot CLI / Claude-style names + // camelCase / Claude-style names 'view', 'create', 'edit', diff --git a/extensions/copilot/src/platform/otel/common/workspaceOTelMetadata.ts b/extensions/copilot/src/platform/otel/common/workspaceOTelMetadata.ts index 9acee3d3170..81e46aa16b4 100644 --- a/extensions/copilot/src/platform/otel/common/workspaceOTelMetadata.ts +++ b/extensions/copilot/src/platform/otel/common/workspaceOTelMetadata.ts @@ -53,7 +53,7 @@ function buildWorkspaceMetadata(repoContext: RepoContext, fileUri?: URI): Worksp /** * Convert workspace metadata to OTel attributes, omitting undefined values. * Emits both the legacy `copilot_chat.repo.*` namespace and the canonical - * `github.copilot.git.*` namespace (CLI parity, see PR #8037). + * `github.copilot.git.*` namespace. */ export function workspaceMetadataToOTelAttributes( metadata?: WorkspaceOTelMetadata, diff --git a/extensions/copilot/src/platform/otel/node/extractToolParameters.ts b/extensions/copilot/src/platform/otel/node/extractToolParameters.ts index 0e90686cd76..c3d2303767d 100644 --- a/extensions/copilot/src/platform/otel/node/extractToolParameters.ts +++ b/extensions/copilot/src/platform/otel/node/extractToolParameters.ts @@ -24,9 +24,10 @@ export interface ToolParameterAttributes { } /** - * Port of the CLI's `extractToolParameters` (copilot-agent-runtime PR #8037, - * `src/core/otel/otelGenAI.ts`). Produces `github.copilot.tool.parameters.*` - * attributes for shell, file, skill, and MCP tool calls. + * Produces structured `github.copilot.tool.parameters.*` attributes for shell, + * file, skill, and MCP tool calls. `gatedAttrs` are content-sensitive (raw + * paths, commands, MCP server names) and emit only when `captureContent` is + * enabled; `attrs` are always safe (hashes, fixed enums). */ export function extractToolParameters(toolName: string, input: unknown): ToolParameterAttributes { const attrs: Record = {}; @@ -63,9 +64,8 @@ export function extractToolParameters(toolName: string, input: unknown): ToolPar attrs[GitHubCopilotAttr.TOOL_PARAM_SKILL_NAME] = skillName; } - // MCP-style tool names: VS Code/CLI use `mcp__`; Anthropic-style - // references use `mcp____`. Accept both: try double-underscore - // first, then fall back to single. + // MCP-style tool names: VS Code emits `mcp__`; Anthropic-style + // references use `mcp____`. Accept both. if (toolName.startsWith('mcp__')) { const rest = toolName.slice('mcp__'.length); const sep = rest.indexOf('__');