kumarabhirup fdd89b4e6f
feat: add PostHog AI observability, feedback UI, and telemetry privacy mode
Integrate PostHog LLM Analytics via a bundled OpenClaw plugin that captures
$ai_generation, $ai_span, and $ai_trace events with configurable privacy
mode (content redaction on by default). Add like/dislike feedback buttons
to the web chat UI backed by a /api/feedback route. Extend the CLI with
`telemetry privacy on|off` subcommands and fix command delegation so
telemetry subcommands aren't forwarded to OpenClaw. Harden the web runtime
installer to auto-flatten pnpm standalone deps and dereference dangling
symlinks, preventing "Cannot find module 'next'" crashes in dev. Move
plugin installation before onboard in bootstrap so the gateway starts
with plugins.allow already configured.
2026-03-05 12:28:08 -08:00

89 lines
2.4 KiB
TypeScript

import { randomUUID } from "node:crypto";
import { redactMessages } from "./privacy.js";
import type { TraceEntry, ToolSpanEntry } from "./types.js";
/**
* Tracks in-flight trace and span state per agent run.
* Each `runId` maps to one trace containing zero or more tool spans.
*/
export class TraceContextManager {
private traces = new Map<string, TraceEntry>();
startTrace(sessionId: string, runId: string): void {
this.traces.set(runId, {
traceId: randomUUID(),
sessionId,
runId,
startedAt: Date.now(),
toolSpans: [],
});
}
setModel(runId: string, model: string): void {
const t = this.traces.get(runId);
if (!t) return;
t.model = model;
const slashIdx = model.indexOf("/");
if (slashIdx > 0) {
t.provider = model.slice(0, slashIdx);
}
}
setInput(runId: string, messages: unknown, privacyMode: boolean): void {
const t = this.traces.get(runId);
if (!t) return;
t.input = privacyMode ? redactMessages(messages) : messages;
}
startToolSpan(runId: string, toolName: string, params?: unknown): void {
const t = this.traces.get(runId);
if (!t) return;
t.toolSpans.push({
toolName,
spanId: randomUUID(),
startedAt: Date.now(),
params,
});
}
endToolSpan(runId: string, toolName: string, result?: unknown): void {
const t = this.traces.get(runId);
if (!t) return;
for (let i = t.toolSpans.length - 1; i >= 0; i--) {
const span = t.toolSpans[i];
if (span.toolName === toolName && !span.endedAt) {
span.endedAt = Date.now();
span.result = result;
span.isError =
result != null &&
typeof result === "object" &&
"error" in (result as Record<string, unknown>);
break;
}
}
}
getTrace(runId: string): TraceEntry | undefined {
return this.traces.get(runId);
}
getModel(runId: string): string | undefined {
return this.traces.get(runId)?.model;
}
getLastToolSpan(runId: string): ToolSpanEntry | undefined {
const t = this.traces.get(runId);
if (!t || t.toolSpans.length === 0) return undefined;
return t.toolSpans[t.toolSpans.length - 1];
}
endTrace(runId: string): void {
const t = this.traces.get(runId);
if (t) {
t.endedAt = Date.now();
}
// Clean up after a short delay to allow final event emission.
setTimeout(() => this.traces.delete(runId), 5_000);
}
}