/** * Implicit telemetry — 對應 SDD .agents/specs/llm-interface/ M1.2 * * 每次 deploy / run / validate 失敗,cypher-executor 自動寫 KBDB block * type=agent-telemetry,含 event_type / workflow_name / error_code / * duration_ms / api_key_hash / agent_user_agent。 * * 隱私:api_key SHA-256 截 16 字元(不可逆,可聚合),workflow 內容不 log。 * * 設計:不阻擋主流程,fetch fire-and-forget;錯誤只 console.warn 不 throw。 * * 注意:本 module 屬 orchestrator 自身能力(觀測自己),不違反「業務邏輯走 WASM」鐵律。 * 跟 trigger_workflow / scheduled() 同類,是 cypher-executor 自我管理的一部分。 */ import type { Bindings, ExecutionContext } from '../types'; export type TelemetryEvent = | 'deploy_success' | 'deploy_fail' | 'run_success' | 'run_fail' | 'validation_error' | 'mcp_tool_call' | 'node_success' // 單一 node 跑完(給 step-level 效能分析用) | 'node_failure'; // 單一 node 失敗 export interface TelemetryRecord { event_type: TelemetryEvent; workflow_name?: string; component_id?: string; error_code?: string; duration_ms: number; api_key_hash: string; agent_user_agent?: string; } /** * api_key → SHA-256 hex 截前 16 字元 * 不可逆,可用來聚合(同一用戶不同 event 統計),不會洩漏原 key */ export async function hashApiKey(apiKey: string): Promise { if (!apiKey) return 'anon'; const encoder = new TextEncoder(); const data = encoder.encode(apiKey); const hashBuffer = await crypto.subtle.digest('SHA-256', data); const hashArray = Array.from(new Uint8Array(hashBuffer)); return hashArray .slice(0, 8) // 8 bytes = 16 hex chars .map(b => b.toString(16).padStart(2, '0')) .join(''); } /** * KBDB upsert URL(內部走 workers.dev 避同 zone 自循環) * 對應 .claude/rules/03-component-architecture.md */ function kbdbCreateBlockUrl(env: Bindings): string { const subdomain = env.WORKER_SUBDOMAIN || 'uncle6-me'; return `https://arcrun-kbdb-create-block.${subdomain}.workers.dev`; } /** * 寫一筆 telemetry block 到 KBDB。fire-and-forget。 * * 寫不進去也不擋主流程 —— 平台自己的觀測絕不能讓 user-facing 流程失敗。 * * 用 ctx.waitUntil 確保即使主 request 已回,背景仍會跑完。 */ export function recordTelemetry( env: Bindings, apiKey: string | undefined, record: Omit, ctx?: ExecutionContext, ): void { const promise = (async () => { try { const api_key_hash = await hashApiKey(apiKey ?? ''); // platform telemetry 用一個系統 ak(讀 env.PLATFORM_API_KEY),所有 telemetry // 都聚集在 platform user_id 下,避免污染用戶自己的 KBDB namespace const platformKey = env.PLATFORM_API_KEY || apiKey || ''; if (!platformKey) { // 沒 platform key + 沒用戶 key → 無處可寫,skip console.warn('[telemetry] no api_key, skipping'); return; } const body = { api_key: platformKey, type: 'agent-telemetry', source: 'cypher-executor', user_id: 'platform_telemetry', content: JSON.stringify(record), metadata_json: JSON.stringify({ ...record, api_key_hash }), tags_json: JSON.stringify([ 'agent-telemetry', `event:${record.event_type}`, ]), }; const res = await fetch(kbdbCreateBlockUrl(env), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); if (!res.ok) { console.warn( '[telemetry] write failed', res.status, await res.text().catch(() => 'no body'), ); } } catch (e) { console.warn('[telemetry] exception', e); } })(); if (ctx?.waitUntil) { ctx.waitUntil(promise); } // 沒 ctx.waitUntil 的情況(直接從 host function call)也讓 promise 自己跑,可能被 cancel 也接受 }