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:
parent
83f6b69f82
commit
f9d454f5c7
@ -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:");
|
||||
|
||||
|
||||
@ -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,
|
||||
);
|
||||
|
||||
@ -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}</>;
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user