OTel: replace string literals with typed constants

- Add GenAiProviderName.GEMINI; switch geminiNativeProvider, anthropicProvider, copilotcliSession to use GenAiProviderName.* constants
- Add CopilotChatAttr.HOOK_TYPE/HOOK_INPUT/HOOK_OUTPUT/HOOK_RESULT_KIND, MODE_NAME, TOTAL_COST_USD; switch chatHookService, claudeMessageDispatch, copilotCliBridgeSpanProcessor, chatDebugFileLoggerService, otelSpanToChatDebugEvent, toolCallingLoop, claudeOTelTracker to use the new constants
- Add CopilotCliSdkAttr group for SDK-emitted hook attribute keys (github.copilot.hook.*); switch consumers in copilotCliBridgeSpanProcessor and otelSpanToChatDebugEvent
- Replace magic-number SpanStatusCode casts in copilotCliBridgeSpanProcessor with the enum members
- Expand otel barrel index.ts to re-export CopilotCliSdkAttr and previously-missing event helpers (emitEditFeedbackEvent, emitEditHunkActionEvent, emitInlineDoneEvent, emitEditSurvivalEvent, emitUserFeedbackEvent, emitCloudSessionInvokeEvent)

No behavior change.
This commit is contained in:
Zhichao Li
2026-04-29 16:37:36 -07:00
parent fbce3ede57
commit 52947bbc19
12 changed files with 64 additions and 41 deletions
@@ -16,7 +16,7 @@ import { ContextManagementResponse, CUSTOM_TOOL_SEARCH_NAME, getContextManagemen
import { IToolDeferralService } from '../../../platform/networking/common/toolDeferralService';
import { IResponseDelta, OpenAiFunctionTool } from '../../../platform/networking/common/fetch';
import { APIUsage } from '../../../platform/networking/common/openai';
import { CopilotChatAttr, emitInferenceDetailsEvent, GenAiAttr, GenAiMetrics, GenAiOperationName, type OTelModelOptions, StdAttr, toToolDefinitions, truncateForOTel } from '../../../platform/otel/common/index';
import { CopilotChatAttr, emitInferenceDetailsEvent, GenAiAttr, GenAiMetrics, GenAiOperationName, GenAiProviderName, type OTelModelOptions, StdAttr, toToolDefinitions, truncateForOTel } from '../../../platform/otel/common/index';
import { IOTelService, SpanKind, SpanStatusCode } from '../../../platform/otel/common/otelService';
import { IRequestLogger } from '../../../platform/requestLogger/common/requestLogger';
import { retrieveCapturingTokenByCorrelation, runWithCapturingToken } from '../../../platform/requestLogger/node/requestLogger';
@@ -472,7 +472,7 @@ export class AnthropicLMProvider extends AbstractLanguageModelChatProvider {
kind: SpanKind.CLIENT,
attributes: {
[GenAiAttr.OPERATION_NAME]: GenAiOperationName.CHAT,
[GenAiAttr.PROVIDER_NAME]: 'anthropic',
[GenAiAttr.PROVIDER_NAME]: GenAiProviderName.ANTHROPIC,
[GenAiAttr.REQUEST_MODEL]: model.id,
[GenAiAttr.AGENT_NAME]: 'AnthropicBYOK',
[CopilotChatAttr.MAX_PROMPT_TOKENS]: model.maxInputTokens,
@@ -9,7 +9,7 @@ import { ChatFetchResponseType, ChatLocation } from '../../../platform/chat/comm
import { ILogService } from '../../../platform/log/common/logService';
import { IResponseDelta, OpenAiFunctionTool } from '../../../platform/networking/common/fetch';
import { APIUsage } from '../../../platform/networking/common/openai';
import { CopilotChatAttr, emitInferenceDetailsEvent, GenAiAttr, GenAiMetrics, GenAiOperationName, type OTelModelOptions, StdAttr, toToolDefinitions, truncateForOTel } from '../../../platform/otel/common/index';
import { CopilotChatAttr, emitInferenceDetailsEvent, GenAiAttr, GenAiMetrics, GenAiOperationName, GenAiProviderName, type OTelModelOptions, StdAttr, toToolDefinitions, truncateForOTel } from '../../../platform/otel/common/index';
import { IOTelService, SpanKind, SpanStatusCode } from '../../../platform/otel/common/otelService';
import { IRequestLogger } from '../../../platform/requestLogger/common/requestLogger';
import { retrieveCapturingTokenByCorrelation, runWithCapturingToken } from '../../../platform/requestLogger/node/requestLogger';
@@ -337,7 +337,7 @@ export class GeminiNativeBYOKLMProvider extends AbstractLanguageModelChatProvide
kind: SpanKind.CLIENT,
attributes: {
[GenAiAttr.OPERATION_NAME]: GenAiOperationName.CHAT,
[GenAiAttr.PROVIDER_NAME]: 'gemini',
[GenAiAttr.PROVIDER_NAME]: GenAiProviderName.GEMINI,
[GenAiAttr.REQUEST_MODEL]: model.id,
[GenAiAttr.AGENT_NAME]: 'GeminiBYOK',
[CopilotChatAttr.MAX_PROMPT_TOKENS]: model.maxInputTokens,
@@ -1003,7 +1003,7 @@ export class ChatDebugFileLoggerService extends Disposable implements IChatDebug
}
case GenAiOperationName.EXECUTE_HOOK: {
const hookType = asString(span.attributes['copilot_chat.hook_type']) ?? span.name;
const hookType = asString(span.attributes[CopilotChatAttr.HOOK_TYPE]) ?? span.name;
return {
ts: span.startTime,
dur: duration,
@@ -1017,14 +1017,14 @@ export class ChatDebugFileLoggerService extends Disposable implements IChatDebug
...(span.attributes['copilot_chat.hook_command'] !== undefined
? { command: truncate(String(span.attributes['copilot_chat.hook_command']), MAX_ATTR_VALUE_LENGTH) }
: {}),
...(span.attributes['copilot_chat.hook_input'] !== undefined
? { input: truncate(String(span.attributes['copilot_chat.hook_input']), MAX_ATTR_VALUE_LENGTH) }
...(span.attributes[CopilotChatAttr.HOOK_INPUT] !== undefined
? { input: truncate(String(span.attributes[CopilotChatAttr.HOOK_INPUT]), MAX_ATTR_VALUE_LENGTH) }
: {}),
...(span.attributes['copilot_chat.hook_output'] !== undefined
? { output: truncate(String(span.attributes['copilot_chat.hook_output']), MAX_ATTR_VALUE_LENGTH) }
...(span.attributes[CopilotChatAttr.HOOK_OUTPUT] !== undefined
? { output: truncate(String(span.attributes[CopilotChatAttr.HOOK_OUTPUT]), MAX_ATTR_VALUE_LENGTH) }
: {}),
...(span.attributes['copilot_chat.hook_result_kind'] !== undefined
? { resultKind: String(span.attributes['copilot_chat.hook_result_kind']) }
...(span.attributes[CopilotChatAttr.HOOK_RESULT_KIND] !== undefined
? { resultKind: String(span.attributes[CopilotChatAttr.HOOK_RESULT_KIND]) }
: {}),
...(isError && span.status.message ? { error: span.status.message } : {}),
},
@@ -157,7 +157,7 @@ export class ChatHookService implements IChatHookService {
kind: SpanKind.INTERNAL,
attributes: {
[GenAiAttr.OPERATION_NAME]: GenAiOperationName.EXECUTE_HOOK,
'copilot_chat.hook_type': hookType,
[CopilotChatAttr.HOOK_TYPE]: hookType,
'copilot_chat.hook_command': hookCommand.command,
...(chatSessionId ? { [CopilotChatAttr.CHAT_SESSION_ID]: chatSessionId } : {}),
},
@@ -166,7 +166,7 @@ export class ChatHookService implements IChatHookService {
try {
// Capture hook input for debug panel resolve
try {
span.setAttribute('copilot_chat.hook_input', truncateForOTel(JSON.stringify(commandInput)));
span.setAttribute(CopilotChatAttr.HOOK_INPUT, truncateForOTel(JSON.stringify(commandInput)));
} catch { /* swallow serialization errors */ }
const sw = StopWatch.create();
@@ -179,7 +179,7 @@ export class ChatHookService implements IChatHookService {
const resultKind = commandResult.kind === HookCommandResultKind.Success ? 'success'
: commandResult.kind === HookCommandResultKind.NonBlockingError ? 'non_blocking_error'
: 'error';
span.setAttribute('copilot_chat.hook_result_kind', resultKind);
span.setAttribute(CopilotChatAttr.HOOK_RESULT_KIND, resultKind);
if (commandResult.kind === HookCommandResultKind.Error || commandResult.kind === HookCommandResultKind.NonBlockingError) {
hasError = true;
@@ -195,7 +195,7 @@ export class ChatHookService implements IChatHookService {
try {
const output = typeof commandResult.result === 'string' ? commandResult.result : JSON.stringify(commandResult.result);
if (output) {
span.setAttribute('copilot_chat.hook_output', truncateForOTel(output));
span.setAttribute(CopilotChatAttr.HOOK_OUTPUT, truncateForOTel(output));
}
} catch { /* swallow serialization errors */ }
}
@@ -384,7 +384,7 @@ export function handleHookStarted(
kind: SpanKind.INTERNAL,
attributes: {
[GenAiAttr.OPERATION_NAME]: GenAiOperationName.EXECUTE_HOOK,
'copilot_chat.hook_type': message.hook_event,
[CopilotChatAttr.HOOK_TYPE]: message.hook_event,
'copilot_chat.hook_command': message.hook_name,
'copilot_chat.hook_id': message.hook_id,
[CopilotChatAttr.CHAT_SESSION_ID]: sessionId,
@@ -191,7 +191,7 @@ export class ClaudeOTelTracker {
this._currentSpan.setAttribute(CopilotChatAttr.TURN_COUNT, message.num_turns);
}
if (message.total_cost_usd !== undefined) {
this._currentSpan.setAttribute('copilot_chat.total_cost_usd', message.total_cost_usd);
this._currentSpan.setAttribute(CopilotChatAttr.TOTAL_COST_USD, message.total_cost_usd);
}
const responseModel = message.modelUsage ? Object.keys(message.modelUsage)[0] : undefined;
if (responseModel) {
@@ -3,8 +3,8 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CopilotChatAttr, GenAiAttr, GenAiOperationName } from '../../../../platform/otel/common/genAiAttributes';
import type { ICompletedSpanData, IOTelService, ISpanEventRecord, SpanStatusCode } from '../../../../platform/otel/common/otelService';
import { CopilotChatAttr, CopilotCliSdkAttr, GenAiAttr, GenAiOperationName } from '../../../../platform/otel/common/genAiAttributes';
import { type ICompletedSpanData, type IOTelService, type ISpanEventRecord, SpanStatusCode } from '../../../../platform/otel/common/otelService';
/**
* Hook event data stashed by copilotcliSession for bridge enrichment.
@@ -160,8 +160,8 @@ export class CopilotCliBridgeSpanProcessor implements SpanProcessor {
// SDK native hook spans: enrich with data from session events and
// remap to execute_hook so the debug panel shows full details.
const invocationId = span.attributes['github.copilot.hook.invocation_id'];
if (span.name.startsWith('hook ') && span.attributes['github.copilot.hook.type'] && typeof invocationId === 'string') {
const invocationId = span.attributes[CopilotCliSdkAttr.HOOK_INVOCATION_ID];
if (span.name.startsWith('hook ') && span.attributes[CopilotCliSdkAttr.HOOK_TYPE] && typeof invocationId === 'string') {
const hookEndData = this._hookData.get(invocationId);
if (hookEndData?.resultKind) {
// hook.end data already arrived — enrich and inject immediately
@@ -186,15 +186,15 @@ export class CopilotCliBridgeSpanProcessor implements SpanProcessor {
const attrs = { ...span.attributes };
attrs[GenAiAttr.OPERATION_NAME] = GenAiOperationName.EXECUTE_HOOK;
attrs['copilot_chat.hook_type'] = data.hookType;
attrs[CopilotChatAttr.HOOK_TYPE] = data.hookType;
if (data.input) {
attrs['copilot_chat.hook_input'] = data.input;
attrs[CopilotChatAttr.HOOK_INPUT] = data.input;
}
if (data.output) {
attrs['copilot_chat.hook_output'] = data.output;
attrs[CopilotChatAttr.HOOK_OUTPUT] = data.output;
}
if (data.resultKind) {
attrs['copilot_chat.hook_result_kind'] = data.resultKind;
attrs[CopilotChatAttr.HOOK_RESULT_KIND] = data.resultKind;
}
const enrichedSpan: ICompletedSpanData = {
@@ -202,8 +202,8 @@ export class CopilotCliBridgeSpanProcessor implements SpanProcessor {
name: `execute_hook ${data.hookType}`,
attributes: attrs,
status: data.resultKind === 'error'
? { code: 2 as SpanStatusCode, message: data.errorMessage }
: { code: 1 as SpanStatusCode },
? { code: SpanStatusCode.ERROR, message: data.errorMessage }
: { code: SpanStatusCode.OK },
};
this._otelService.injectCompletedSpan(enrichedSpan);
}
@@ -14,7 +14,7 @@ import { ConfigKey, IConfigurationService } from '../../../../platform/configura
import { PermissiveAuthRequiredError } from '../../../../platform/github/common/githubService';
import { ILogService } from '../../../../platform/log/common/logService';
import { GenAiMetrics } from '../../../../platform/otel/common/genAiMetrics';
import { CopilotChatAttr, GenAiAttr, GenAiOperationName, IOTelService, ISpanHandle, SpanKind, SpanStatusCode, truncateForOTel, resolveWorkspaceOTelMetadata, workspaceMetadataToOTelAttributes } from '../../../../platform/otel/common/index';
import { CopilotChatAttr, GenAiAttr, GenAiOperationName, GenAiProviderName, IOTelService, ISpanHandle, SpanKind, SpanStatusCode, truncateForOTel, resolveWorkspaceOTelMetadata, workspaceMetadataToOTelAttributes } from '../../../../platform/otel/common/index';
import { CapturingToken } from '../../../../platform/requestLogger/common/capturingToken';
import { IRequestLogger, LoggedRequestKind } from '../../../../platform/requestLogger/common/requestLogger';
import { PromptTokenCategory, PromptTokenLabel } from '../../../../platform/tokenizer/node/promptTokenDetails';
@@ -548,7 +548,7 @@ export class CopilotCLISession extends DisposableStore implements ICopilotCLISes
attributes: {
[GenAiAttr.OPERATION_NAME]: GenAiOperationName.INVOKE_AGENT,
[GenAiAttr.AGENT_NAME]: 'copilotcli',
[GenAiAttr.PROVIDER_NAME]: 'github',
[GenAiAttr.PROVIDER_NAME]: GenAiProviderName.GITHUB,
[GenAiAttr.CONVERSATION_ID]: this.sessionId,
[CopilotChatAttr.SESSION_ID]: this.sessionId,
[CopilotChatAttr.CHAT_SESSION_ID]: this.sessionId,
@@ -770,7 +770,7 @@ export abstract class ToolCallingLoop<TOptions extends IToolCallingLoopOptions =
...(chatSessionId ? { [CopilotChatAttr.CHAT_SESSION_ID]: chatSessionId } : {}),
...(parentChatSessionId ? { [CopilotChatAttr.PARENT_CHAT_SESSION_ID]: parentChatSessionId } : {}),
...(debugLogLabel ? { [CopilotChatAttr.DEBUG_LOG_LABEL]: debugLogLabel } : {}),
...(customModeName ? { 'copilot_chat.mode_name': customModeName } : {}),
...(customModeName ? { [CopilotChatAttr.MODE_NAME]: customModeName } : {}),
...workspaceMetadataToOTelAttributes(resolveWorkspaceOTelMetadata(this._gitService)),
},
parentTraceContext,
@@ -5,7 +5,7 @@
import * as vscode from 'vscode';
import type { IDebugLogEntry } from '../../../platform/chat/common/chatDebugFileLoggerService';
import { CopilotChatAttr, GenAiAttr, GenAiOperationName } from '../../../platform/otel/common/index';
import { CopilotChatAttr, CopilotCliSdkAttr, GenAiAttr, GenAiOperationName } from '../../../platform/otel/common/index';
import type { ICompletedSpanData, ISpanEventData, SpanStatusCode } from '../../../platform/otel/common/otelService';
// ── Event ID conventions ──
@@ -55,7 +55,7 @@ export function completedSpanToDebugEvent(span: ICompletedSpanData): vscode.Chat
return spanToGenericEvent(span);
default:
// SDK native hook spans use 'github.copilot.hook.type' instead of gen_ai.operation.name
if (span.name.startsWith('hook ') && asString(span.attributes['github.copilot.hook.type'])) {
if (span.name.startsWith('hook ') && asString(span.attributes[CopilotCliSdkAttr.HOOK_TYPE])) {
return spanToSdkHookEvent(span);
}
return undefined;
@@ -386,10 +386,10 @@ function spanToSubagentEvent(span: ICompletedSpanData): vscode.ChatDebugSubagent
}
function resolveHookExecutionContent(span: ICompletedSpanData): vscode.ChatDebugEventHookContent {
const hookType = asString(span.attributes['copilot_chat.hook_type']) ?? 'unknown';
const hookType = asString(span.attributes[CopilotChatAttr.HOOK_TYPE]) ?? 'unknown';
const content = new vscode.ChatDebugEventHookContent(hookType);
content.command = asString(span.attributes['copilot_chat.hook_command']);
const resultKind = asString(span.attributes['copilot_chat.hook_result_kind']);
const resultKind = asString(span.attributes[CopilotChatAttr.HOOK_RESULT_KIND]);
content.result = resultKind === 'success'
? vscode.ChatDebugHookResult.Success
: resultKind === 'error'
@@ -398,8 +398,8 @@ function resolveHookExecutionContent(span: ICompletedSpanData): vscode.ChatDebug
? vscode.ChatDebugHookResult.NonBlockingError
: undefined;
content.durationInMillis = span.endTime - span.startTime;
content.input = asString(span.attributes['copilot_chat.hook_input']);
content.output = asString(span.attributes['copilot_chat.hook_output']);
content.input = asString(span.attributes[CopilotChatAttr.HOOK_INPUT]);
content.output = asString(span.attributes[CopilotChatAttr.HOOK_OUTPUT]);
if (span.status.code === 2 /* ERROR */ && span.status.message) {
content.errorMessage = span.status.message;
}
@@ -408,9 +408,9 @@ function resolveHookExecutionContent(span: ICompletedSpanData): vscode.ChatDebug
}
function spanToHookExecutionEvent(span: ICompletedSpanData): vscode.ChatDebugGenericEvent {
const hookType = asString(span.attributes['copilot_chat.hook_type']) ?? 'unknown';
const hookType = asString(span.attributes[CopilotChatAttr.HOOK_TYPE]) ?? 'unknown';
const hookCommand = asString(span.attributes['copilot_chat.hook_command']);
const resultKind = asString(span.attributes['copilot_chat.hook_result_kind']);
const resultKind = asString(span.attributes[CopilotChatAttr.HOOK_RESULT_KIND]);
const durationMs = Math.round(span.endTime - span.startTime);
const name = `Hook: ${hookType}`;
@@ -433,7 +433,7 @@ function spanToHookExecutionEvent(span: ICompletedSpanData): vscode.ChatDebugGen
* SDK uses span name "hook {type}" and attributes in the github.copilot.hook.* namespace.
*/
function spanToSdkHookEvent(span: ICompletedSpanData): vscode.ChatDebugGenericEvent {
const hookType = asString(span.attributes['github.copilot.hook.type']) ?? 'unknown';
const hookType = asString(span.attributes[CopilotCliSdkAttr.HOOK_TYPE]) ?? 'unknown';
const durationMs = span.endTime - span.startTime;
const isError = span.status.code === 2; /* ERROR */
const level = isError ? vscode.ChatDebugLogLevel.Error : vscode.ChatDebugLogLevel.Info;
@@ -21,6 +21,7 @@ export const GenAiProviderName = {
OPENAI: 'openai',
ANTHROPIC: 'anthropic',
AZURE_AI_OPENAI: 'azure.ai.openai',
GEMINI: 'gemini',
} as const;
// gen_ai.token.type values
@@ -147,6 +148,18 @@ export const CopilotChatAttr = {
REPO_REMOTE_URL: 'copilot_chat.repo.remote_url',
/** File path relative to the repository root */
FILE_RELATIVE_PATH: 'copilot_chat.file.relative_path',
/** Hook type / event name (e.g. PreToolUse, PostToolUse, Stop) */
HOOK_TYPE: 'copilot_chat.hook_type',
/** Serialized hook command input (truncated, content-gated) */
HOOK_INPUT: 'copilot_chat.hook_input',
/** Serialized hook command output (truncated, content-gated) */
HOOK_OUTPUT: 'copilot_chat.hook_output',
/** Hook result kind: 'success' or 'error' */
HOOK_RESULT_KIND: 'copilot_chat.hook_result_kind',
/** Custom chat mode name (when a custom mode is active) */
MODE_NAME: 'copilot_chat.mode_name',
/** Aggregated session cost in USD (Claude agent) */
TOTAL_COST_USD: 'copilot_chat.total_cost_usd',
} as const;
export type EditSource = 'inline_chat' | 'chat_editing' | 'chat_editing_hunk' | 'apply_patch' | 'replace_string' | 'code_mapper';
@@ -160,3 +173,13 @@ export const StdAttr = {
SERVER_ADDRESS: 'server.address',
SERVER_PORT: 'server.port',
} as const;
/**
* Attribute keys emitted by the Copilot CLI SDK's native OTel instrumentation
* (read by the bridge processor and the debug panel; the extension itself does
* not produce these).
*/
export const CopilotCliSdkAttr = {
HOOK_TYPE: 'github.copilot.hook.type',
HOOK_INVOCATION_ID: 'github.copilot.hook.invocation_id',
} as const;
@@ -3,8 +3,8 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export { CopilotChatAttr, GenAiAttr, GenAiOperationName, GenAiProviderName, GenAiTokenType, GenAiToolType, StdAttr } from './genAiAttributes';
export { emitAgentTurnEvent, emitInferenceDetailsEvent, emitSessionStartEvent, emitToolCallEvent } from './genAiEvents';
export { CopilotChatAttr, CopilotCliSdkAttr, GenAiAttr, GenAiOperationName, GenAiProviderName, GenAiTokenType, GenAiToolType, StdAttr } from './genAiAttributes';
export { emitAgentTurnEvent, emitCloudSessionInvokeEvent, emitEditFeedbackEvent, emitEditHunkActionEvent, emitEditSurvivalEvent, emitInferenceDetailsEvent, emitInlineDoneEvent, emitSessionStartEvent, emitToolCallEvent, emitUserFeedbackEvent } from './genAiEvents';
export { GenAiMetrics } from './genAiMetrics';
export { normalizeProviderMessages, toInputMessages, toOutputMessages, toSystemInstructions, toToolDefinitions, truncateForOTel } from './messageFormatters';
export { NoopOTelService } from './noopOtelService';