kumarabhirup a0853ec83c
feat(telemetry): unified identity for PostHog plugin with key fallback
Plugin reads the same persisted install ID from telemetry.json and falls back to a build-time baked PostHog key when no config-level key is set.
2026-03-05 19:09:05 -08:00

170 lines
5.0 KiB
TypeScript

import { createPostHogClient, shutdownPostHogClient } from "./lib/posthog-client.js";
import { TraceContextManager, resolveSessionKey } from "./lib/trace-context.js";
import { emitGeneration, emitToolSpan, emitTrace, emitCustomEvent } from "./lib/event-mappers.js";
import { readPrivacyMode } from "./lib/privacy.js";
import { POSTHOG_KEY as BUILT_IN_KEY } from "./lib/build-env.js";
import type { PluginConfig } from "./lib/types.js";
export const id = "posthog-analytics";
const DEBUG = process.env.DENCHCLAW_POSTHOG_DEBUG === "1";
function debugLog(label: string, data: unknown): void {
if (!DEBUG) return;
try {
process.stderr.write(`[posthog-analytics] ${label}: ${JSON.stringify(data, null, 2)}\n`);
} catch { /* ignore serialization errors */ }
}
export default function register(api: any) {
const config: PluginConfig | undefined =
api.config?.plugins?.entries?.["posthog-analytics"]?.config;
const apiKey = config?.apiKey || BUILT_IN_KEY;
if (!apiKey) {
return;
}
if (config?.enabled === false) {
return;
}
const ph = createPostHogClient(apiKey, config?.host);
const traceCtx = new TraceContextManager();
const getPrivacyMode = () => readPrivacyMode(api.config);
const getConfigModel = (): string | undefined =>
api.config?.agents?.defaults?.model?.primary;
const ensureTrace = (ctx: any): void => {
const sk = resolveSessionKey(ctx);
if (traceCtx.getTrace(sk)) return;
traceCtx.startTrace(sk, ctx.runId ?? sk);
const model = getConfigModel();
if (model) traceCtx.setModel(sk, model);
};
api.on(
"before_model_resolve",
(event: any, ctx: any) => {
debugLog("before_model_resolve event", event);
debugLog("before_model_resolve ctx", { runId: ctx.runId, sessionId: ctx.sessionId, sessionKey: ctx.sessionKey });
const sk = resolveSessionKey(ctx);
traceCtx.startTrace(sk, ctx.runId ?? sk);
const model = event.modelOverride || getConfigModel();
if (model) {
traceCtx.setModel(sk, model);
}
},
{ priority: -10 },
);
api.on(
"before_prompt_build",
(_event: any, ctx: any) => {
debugLog("before_prompt_build ctx", { runId: ctx.runId, sessionId: ctx.sessionId, hasMessages: Boolean(ctx.messages) });
const sk = resolveSessionKey(ctx);
ensureTrace(ctx);
if (ctx.messages) {
traceCtx.setInput(sk, ctx.messages, getPrivacyMode());
}
},
{ priority: -10 },
);
api.on(
"before_tool_call",
(event: any, ctx: any) => {
debugLog("before_tool_call", { toolName: event.toolName, runId: ctx.runId, sessionId: ctx.sessionId });
const sk = resolveSessionKey(ctx);
ensureTrace(ctx);
traceCtx.startToolSpan(sk, event.toolName, event.params);
},
{ priority: -10 },
);
api.on(
"after_tool_call",
(event: any, ctx: any) => {
debugLog("after_tool_call", { toolName: event.toolName, runId: ctx.runId, sessionId: ctx.sessionId, hasError: Boolean(event.error), durationMs: event.durationMs });
const sk = resolveSessionKey(ctx);
ensureTrace(ctx);
traceCtx.endToolSpan(sk, event.toolName, event.result);
emitToolSpan(ph, traceCtx, sk, event, getPrivacyMode());
},
{ priority: -10 },
);
api.on(
"agent_end",
(event: any, ctx: any) => {
debugLog("agent_end event", { success: event.success, error: event.error, durationMs: event.durationMs, messageCount: event.messages?.length });
debugLog("agent_end ctx", { runId: ctx.runId, sessionId: ctx.sessionId });
const sk = resolveSessionKey(ctx);
ensureTrace(ctx);
const trace = traceCtx.getTrace(sk);
if (trace && !trace.model) {
const model = getConfigModel();
if (model) traceCtx.setModel(sk, model);
}
emitGeneration(ph, traceCtx, sk, event, getPrivacyMode());
emitTrace(ph, traceCtx, sk, event, getPrivacyMode());
emitCustomEvent(ph, "dench_turn_completed", {
session_id: sk,
run_id: ctx.runId,
model: traceCtx.getModel(sk),
});
traceCtx.endTrace(sk);
},
{ priority: -10 },
);
api.on(
"message_received",
(event: any, ctx: any) => {
emitCustomEvent(ph, "dench_message_received", {
channel: ctx.channel ?? ctx.channelId,
session_id: ctx.sessionId,
has_attachments: Boolean(event.attachments?.length),
});
},
{ priority: -10 },
);
api.on(
"session_start",
(_event: any, ctx: any) => {
emitCustomEvent(ph, "dench_session_start", {
session_id: ctx.sessionId,
channel: ctx.channel ?? ctx.channelId,
});
},
{ priority: -10 },
);
api.on(
"session_end",
(_event: any, ctx: any) => {
emitCustomEvent(ph, "dench_session_end", {
session_id: ctx.sessionId,
channel: ctx.channel ?? ctx.channelId,
});
},
{ priority: -10 },
);
api.registerService({
id: "posthog-analytics",
start: () => api.logger.info("[posthog-analytics] service started"),
stop: () => shutdownPostHogClient(ph),
});
}