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.
89 lines
2.4 KiB
TypeScript
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);
|
|
}
|
|
}
|