Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
7.3 kB · 238 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239// Centralized analytics/telemetry logging for tool permission decisions.// All permission approve/reject events flow through logPermissionDecision(),// which fans out to Statsig analytics, OTel telemetry, and code-edit metrics.import { feature } from 'bun:bundle'import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent,} from 'src/services/analytics/index.js'import { sanitizeToolNameForAnalytics } from 'src/services/analytics/metadata.js'import { getCodeEditToolDecisionCounter } from '../../bootstrap/state.js'import type { Tool as ToolType, ToolUseContext } from '../../Tool.js'import { getLanguageName } from '../../utils/cliHighlight.js'import { SandboxManager } from '../../utils/sandbox/sandbox-adapter.js'import { logOTelEvent } from '../../utils/telemetry/events.js'import type { PermissionApprovalSource, PermissionRejectionSource,} from './PermissionContext.js'
type PermissionLogContext = { tool: ToolType input: unknown toolUseContext: ToolUseContext messageId: string toolUseID: string}
// Discriminated union: 'accept' pairs with approval sources, 'reject' with rejection sourcestype PermissionDecisionArgs = | { decision: 'accept'; source: PermissionApprovalSource | 'config' } | { decision: 'reject'; source: PermissionRejectionSource | 'config' }
const CODE_EDITING_TOOLS = ['Edit', 'Write', 'NotebookEdit']
function isCodeEditingTool(toolName: string): boolean { return CODE_EDITING_TOOLS.includes(toolName)}
// Builds OTel counter attributes for code editing tools, enriching with// language when the tool's target file path can be extracted from inputasync function buildCodeEditToolAttributes( tool: ToolType, input: unknown, decision: 'accept' | 'reject', source: string,): Promise<Record<string, string>> { // Derive language from file path if the tool exposes one (e.g., Edit, Write) let language: string | undefined if (tool.getPath && input) { const parseResult = tool.inputSchema.safeParse(input) if (parseResult.success) { const filePath = tool.getPath(parseResult.data) if (filePath) { language = await getLanguageName(filePath) } } }
return { decision, source, tool_name: tool.name, ...(language && { language }), }}
// Flattens structured source into a string label for analytics/OTel eventsfunction sourceToString( source: PermissionApprovalSource | PermissionRejectionSource,): string { if ( (feature('BASH_CLASSIFIER') || feature('TRANSCRIPT_CLASSIFIER')) && source.type === 'classifier' ) { return 'classifier' } switch (source.type) { case 'hook': return 'hook' case 'user': return source.permanent ? 'user_permanent' : 'user_temporary' case 'user_abort': return 'user_abort' case 'user_reject': return 'user_reject' default: return 'unknown' }}
function baseMetadata( messageId: string, toolName: string, waitMs: number | undefined,): { [key: string]: boolean | number | undefined } { return { messageID: messageId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, toolName: sanitizeToolNameForAnalytics(toolName), sandboxEnabled: SandboxManager.isSandboxingEnabled(), // Only include wait time when the user was actually prompted (not auto-approved) ...(waitMs !== undefined && { waiting_for_user_permission_ms: waitMs }), }}
// Emits a distinct analytics event name per approval source for funnel analysisfunction logApprovalEvent( tool: ToolType, messageId: string, source: PermissionApprovalSource | 'config', waitMs: number | undefined,): void { if (source === 'config') { // Auto-approved by allowlist in settings -- no user wait time logEvent( 'tengu_tool_use_granted_in_config', baseMetadata(messageId, tool.name, undefined), ) return } if ( (feature('BASH_CLASSIFIER') || feature('TRANSCRIPT_CLASSIFIER')) && source.type === 'classifier' ) { logEvent( 'tengu_tool_use_granted_by_classifier', baseMetadata(messageId, tool.name, waitMs), ) return } switch (source.type) { case 'user': logEvent( source.permanent ? 'tengu_tool_use_granted_in_prompt_permanent' : 'tengu_tool_use_granted_in_prompt_temporary', baseMetadata(messageId, tool.name, waitMs), ) break case 'hook': logEvent('tengu_tool_use_granted_by_permission_hook', { ...baseMetadata(messageId, tool.name, waitMs), permanent: source.permanent ?? false, }) break default: break }}
// Rejections share a single event name, differentiated by metadata fieldsfunction logRejectionEvent( tool: ToolType, messageId: string, source: PermissionRejectionSource | 'config', waitMs: number | undefined,): void { if (source === 'config') { // Denied by denylist in settings logEvent( 'tengu_tool_use_denied_in_config', baseMetadata(messageId, tool.name, undefined), ) return } logEvent('tengu_tool_use_rejected_in_prompt', { ...baseMetadata(messageId, tool.name, waitMs), // Distinguish hook rejections from user rejections via separate fields ...(source.type === 'hook' ? { isHook: true } : { hasFeedback: source.type === 'user_reject' ? source.hasFeedback : false, }), })}
// Single entry point for all permission decision logging. Called by permission// handlers after every approve/reject. Fans out to: analytics events, OTel// telemetry, code-edit OTel counters, and toolUseContext decision storage.function logPermissionDecision( ctx: PermissionLogContext, args: PermissionDecisionArgs, permissionPromptStartTimeMs?: number,): void { const { tool, input, toolUseContext, messageId, toolUseID } = ctx const { decision, source } = args
const waiting_for_user_permission_ms = permissionPromptStartTimeMs !== undefined ? Date.now() - permissionPromptStartTimeMs : undefined
// Log the analytics event if (args.decision === 'accept') { logApprovalEvent( tool, messageId, args.source, waiting_for_user_permission_ms, ) } else { logRejectionEvent( tool, messageId, args.source, waiting_for_user_permission_ms, ) }
const sourceString = source === 'config' ? 'config' : sourceToString(source)
// Track code editing tool metrics if (isCodeEditingTool(tool.name)) { void buildCodeEditToolAttributes(tool, input, decision, sourceString).then( attributes => getCodeEditToolDecisionCounter()?.add(1, attributes), ) }
// Persist decision on the context so downstream code can inspect what happened if (!toolUseContext.toolDecisions) { toolUseContext.toolDecisions = new Map() } toolUseContext.toolDecisions.set(toolUseID, { source: sourceString, decision, timestamp: Date.now(), })
void logOTelEvent('tool_decision', { decision, source: sourceString, tool_name: sanitizeToolNameForAnalytics(tool.name), })}
export { isCodeEditingTool, buildCodeEditToolAttributes, logPermissionDecision }export type { PermissionLogContext, PermissionDecisionArgs }