Add optional name, email, avatar, and denchOrgId fields to telemetry.json. When present, all telemetry layers (CLI, web server, web client, OpenClaw plugin) call PostHog identify() with $name, $email, $avatar, and dench_org_id person properties. Remove $process_person_profile:false from all layers so every install gets a PostHog person profile. Enable session replay with masking controlled by privacy mode (all text/inputs masked when on, nothing masked when off).
165 lines
4.8 KiB
TypeScript
165 lines
4.8 KiB
TypeScript
import { randomUUID } from "node:crypto";
|
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
import { homedir } from "node:os";
|
|
import { join, dirname } from "node:path";
|
|
import { PostHog } from "posthog-node";
|
|
|
|
const POSTHOG_KEY = process.env.NEXT_PUBLIC_POSTHOG_KEY || "";
|
|
const POSTHOG_HOST = "https://us.i.posthog.com";
|
|
const DENCHCLAW_VERSION = process.env.NEXT_PUBLIC_DENCHCLAW_VERSION || "";
|
|
const OPENCLAW_VERSION = process.env.NEXT_PUBLIC_OPENCLAW_VERSION || "";
|
|
|
|
let client: PostHog | null = null;
|
|
|
|
function ensureClient(): PostHog | null {
|
|
if (!POSTHOG_KEY) return null;
|
|
if (!client) {
|
|
client = new PostHog(POSTHOG_KEY, {
|
|
host: POSTHOG_HOST,
|
|
flushAt: 10,
|
|
flushInterval: 30_000,
|
|
});
|
|
}
|
|
return client;
|
|
}
|
|
|
|
export type PersonInfo = {
|
|
name?: string;
|
|
email?: string;
|
|
avatar?: string;
|
|
denchOrgId?: string;
|
|
};
|
|
|
|
let _cachedAnonymousId: string | null = null;
|
|
let _cachedPersonInfo: PersonInfo | null | undefined = undefined;
|
|
let _cachedPrivacyMode: boolean | undefined = undefined;
|
|
|
|
/**
|
|
* 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 || homedir(), ".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;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Read optional person identity fields from telemetry.json.
|
|
* Returns null when no identity fields are set.
|
|
*/
|
|
export function readPersonInfo(): PersonInfo | null {
|
|
if (_cachedPersonInfo !== undefined) return _cachedPersonInfo;
|
|
|
|
try {
|
|
const stateDir = join(process.env.HOME || homedir(), ".openclaw-dench");
|
|
const configPath = join(stateDir, "telemetry.json");
|
|
|
|
if (!existsSync(configPath)) {
|
|
_cachedPersonInfo = null;
|
|
return null;
|
|
}
|
|
const raw = JSON.parse(readFileSync(configPath, "utf-8")) as Record<string, unknown>;
|
|
const info: PersonInfo = {};
|
|
if (typeof raw.name === "string" && raw.name) info.name = raw.name;
|
|
if (typeof raw.email === "string" && raw.email) info.email = raw.email;
|
|
if (typeof raw.avatar === "string" && raw.avatar) info.avatar = raw.avatar;
|
|
if (typeof raw.denchOrgId === "string" && raw.denchOrgId) info.denchOrgId = raw.denchOrgId;
|
|
|
|
_cachedPersonInfo = Object.keys(info).length > 0 ? info : null;
|
|
return _cachedPersonInfo;
|
|
} catch {
|
|
_cachedPersonInfo = null;
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Read privacy mode from telemetry.json.
|
|
* Default is true (privacy on) when the file is missing or unreadable.
|
|
*/
|
|
export function readPrivacyMode(): boolean {
|
|
if (_cachedPrivacyMode !== undefined) return _cachedPrivacyMode;
|
|
|
|
try {
|
|
const stateDir = join(process.env.HOME || homedir(), ".openclaw-dench");
|
|
const configPath = join(stateDir, "telemetry.json");
|
|
|
|
if (!existsSync(configPath)) {
|
|
_cachedPrivacyMode = true;
|
|
return true;
|
|
}
|
|
const raw = JSON.parse(readFileSync(configPath, "utf-8")) as Record<string, unknown>;
|
|
_cachedPrivacyMode = raw.privacyMode !== false;
|
|
return _cachedPrivacyMode;
|
|
} catch {
|
|
_cachedPrivacyMode = true;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
function personInfoToPostHogProps(person: PersonInfo): Record<string, string> {
|
|
const props: Record<string, string> = {};
|
|
if (person.name) props.$name = person.name;
|
|
if (person.email) props.$email = person.email;
|
|
if (person.avatar) props.$avatar = person.avatar;
|
|
if (person.denchOrgId) props.dench_org_id = person.denchOrgId;
|
|
return props;
|
|
}
|
|
|
|
let _identified = false;
|
|
|
|
export function trackServer(
|
|
event: string,
|
|
properties?: Record<string, unknown>,
|
|
distinctId?: string,
|
|
): void {
|
|
const ph = ensureClient();
|
|
if (!ph) return;
|
|
|
|
const id = distinctId || getOrCreateAnonymousId();
|
|
|
|
if (!_identified) {
|
|
_identified = true;
|
|
const person = readPersonInfo();
|
|
if (person) {
|
|
ph.identify({
|
|
distinctId: id,
|
|
properties: personInfoToPostHogProps(person),
|
|
});
|
|
}
|
|
}
|
|
|
|
ph.capture({
|
|
distinctId: id,
|
|
event,
|
|
properties: {
|
|
...properties,
|
|
denchclaw_version: DENCHCLAW_VERSION || undefined,
|
|
openclaw_version: OPENCLAW_VERSION || undefined,
|
|
},
|
|
});
|
|
}
|