diff --git a/apps/web/app/api/chat/route.ts b/apps/web/app/api/chat/route.ts index 8e91b9c60bd..efc2f026b6e 100644 --- a/apps/web/app/api/chat/route.ts +++ b/apps/web/app/api/chat/route.ts @@ -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:"); diff --git a/apps/web/app/api/feedback/route.ts b/apps/web/app/api/feedback/route.ts index 0bbf8647bf1..0ecec894542 100644 --- a/apps/web/app/api/feedback/route.ts +++ b/apps/web/app/api/feedback/route.ts @@ -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, ); diff --git a/apps/web/app/components/posthog-provider.tsx b/apps/web/app/components/posthog-provider.tsx index 582d79ba7cb..c209ce1b88d 100644 --- a/apps/web/app/components/posthog-provider.tsx +++ b/apps/web/app/components/posthog-provider.tsx @@ -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}; diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index 13edae493b3..0f46760724f 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -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 ( @@ -42,7 +45,7 @@ export default function RootLayout({ - + {children} diff --git a/apps/web/lib/telemetry.ts b/apps/web/lib/telemetry.ts index 8c9de8b37b4..2c4c7add8d5 100644 --- a/apps/web/lib/telemetry.ts +++ b/apps/web/lib/telemetry.ts @@ -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 = {}; + 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, @@ -27,7 +63,7 @@ export function trackServer( if (!ph) return; ph.capture({ - distinctId: distinctId || randomUUID(), + distinctId: distinctId || getOrCreateAnonymousId(), event, properties: { ...properties,