feat(web): forward client identity to server and improve feedback traces

Bootstrap posthog-js with the persisted install ID from the server, forward distinctId to API routes, and restructure feedback traces to use chronological conversation order.
This commit is contained in:
kumarabhirup 2026-03-05 19:09:19 -08:00
parent 83f6b69f82
commit f9d454f5c7
No known key found for this signature in database
GPG Key ID: DB7CA2289CAB0167
5 changed files with 76 additions and 19 deletions

View File

@ -44,7 +44,8 @@ export async function POST(req: Request) {
messages,
sessionId,
sessionKey,
}: { messages: UIMessage[]; sessionId?: string; sessionKey?: string } = await req.json();
distinctId,
}: { messages: UIMessage[]; sessionId?: string; sessionKey?: string; distinctId?: string } = await req.json();
const lastUserMessage = messages.filter((m) => m.role === "user").pop();
const userText =
@ -60,10 +61,14 @@ export async function POST(req: Request) {
return new Response("No message provided", { status: 400 });
}
trackServer("chat_message_sent", {
message_length: userText.length,
is_subagent: typeof sessionKey === "string" && sessionKey.includes(":subagent:"),
});
trackServer(
"chat_message_sent",
{
message_length: userText.length,
is_subagent: typeof sessionKey === "string" && sessionKey.includes(":subagent:"),
},
distinctId,
);
const isSubagentSession = typeof sessionKey === "string" && sessionKey.includes(":subagent:");

View File

@ -62,12 +62,14 @@ export async function POST(req: Request) {
}
const conversation = lines.slice(0, cutoff);
const inputState = conversation
.filter((m) => m.role === "user")
.map((m) => ({ role: "user" as const, content: extractTextContent(m) }));
const outputState = conversation
.filter((m) => m.role === "assistant")
.map((m) => ({ role: "assistant" as const, content: extractTextContent(m) }));
const chronological = conversation.map((m) => ({
role: m.role as "user" | "assistant",
content: extractTextContent(m),
}));
const lastAssistant = [...conversation]
.reverse()
.find((m) => m.role === "assistant");
trackServer(
"$ai_trace",
@ -75,8 +77,10 @@ export async function POST(req: Request) {
$ai_trace_id: sessionId,
$ai_session_id: sessionId,
$ai_span_name: "chat_session",
$ai_input_state: inputState.length > 0 ? inputState : undefined,
$ai_output_state: outputState.length > 0 ? outputState : undefined,
$ai_input_state: chronological.length > 0 ? chronological : undefined,
$ai_output_state: lastAssistant
? [{ role: "assistant" as const, content: extractTextContent(lastAssistant) }]
: undefined,
},
distinctId,
);

View File

@ -2,7 +2,7 @@
import posthog from "posthog-js";
import { PostHogProvider as PHProvider } from "posthog-js/react";
import { useEffect } from "react";
import { useEffect, useRef } from "react";
import { usePathname, useSearchParams } from "next/navigation";
const POSTHOG_KEY = process.env.NEXT_PUBLIC_POSTHOG_KEY || "";
@ -10,7 +10,7 @@ const POSTHOG_HOST = "https://us.i.posthog.com";
let initialized = false;
function initPostHog() {
function initPostHog(anonymousId?: string) {
if (initialized || !POSTHOG_KEY || typeof window === "undefined") return;
posthog.init(POSTHOG_KEY, {
@ -21,6 +21,9 @@ function initPostHog() {
autocapture: false,
disable_session_recording: true,
person_profiles: "identified_only",
bootstrap: anonymousId
? { distinctID: anonymousId, isIdentifiedID: false }
: undefined,
});
initialized = true;
}
@ -40,12 +43,18 @@ function PageviewTracker() {
export function PostHogProvider({
children,
anonymousId,
}: {
children: React.ReactNode;
anonymousId?: string;
}) {
const initRef = useRef(false);
useEffect(() => {
initPostHog();
}, []);
if (initRef.current) return;
initRef.current = true;
initPostHog(anonymousId);
}, [anonymousId]);
if (!POSTHOG_KEY) return <>{children}</>;

View File

@ -1,5 +1,6 @@
import type { Metadata, Viewport } from "next";
import { Suspense } from "react";
import { getOrCreateAnonymousId } from "@/lib/telemetry";
import { PostHogProvider } from "./components/posthog-provider";
import "./globals.css";
@ -20,6 +21,8 @@ export default function RootLayout({
}: {
children: React.ReactNode;
}) {
const anonymousId = getOrCreateAnonymousId();
return (
<html lang="en" suppressHydrationWarning>
<head>
@ -42,7 +45,7 @@ export default function RootLayout({
</head>
<body className="antialiased">
<Suspense fallback={null}>
<PostHogProvider>
<PostHogProvider anonymousId={anonymousId}>
{children}
</PostHogProvider>
</Suspense>

View File

@ -1,4 +1,6 @@
import { randomUUID } from "node:crypto";
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
import { join, dirname } from "node:path";
import { PostHog } from "posthog-node";
const POSTHOG_KEY = process.env.NEXT_PUBLIC_POSTHOG_KEY || "";
@ -18,6 +20,40 @@ function ensureClient(): PostHog | null {
return client;
}
let _cachedAnonymousId: string | null = null;
/**
* Read the persisted install-scoped anonymous ID from ~/.openclaw-dench/telemetry.json,
* generating and writing one if absent.
*/
export function getOrCreateAnonymousId(): string {
if (_cachedAnonymousId) return _cachedAnonymousId;
try {
const stateDir = join(process.env.HOME || "~", ".openclaw-dench");
const configPath = join(stateDir, "telemetry.json");
let raw: Record<string, unknown> = {};
if (existsSync(configPath)) {
raw = JSON.parse(readFileSync(configPath, "utf-8"));
}
if (typeof raw.anonymousId === "string" && raw.anonymousId) {
_cachedAnonymousId = raw.anonymousId;
return raw.anonymousId;
}
const id = randomUUID();
raw.anonymousId = id;
mkdirSync(dirname(configPath), { recursive: true });
writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n", "utf-8");
_cachedAnonymousId = id;
return id;
} catch {
const id = randomUUID();
_cachedAnonymousId = id;
return id;
}
}
export function trackServer(
event: string,
properties?: Record<string, unknown>,
@ -27,7 +63,7 @@ export function trackServer(
if (!ph) return;
ph.capture({
distinctId: distinctId || randomUUID(),
distinctId: distinctId || getOrCreateAnonymousId(),
event,
properties: {
...properties,