diff --git a/apps/web/app/components/posthog-provider.tsx b/apps/web/app/components/posthog-provider.tsx index 5faf1ef43fc..fcebb9b7805 100644 --- a/apps/web/app/components/posthog-provider.tsx +++ b/apps/web/app/components/posthog-provider.tsx @@ -7,6 +7,8 @@ import { usePathname, useSearchParams } from "next/navigation"; 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 initialized = false; @@ -25,6 +27,12 @@ function initPostHog(anonymousId?: string) { ? { distinctID: anonymousId, isIdentifiedID: false } : undefined, }); + + const superProps: Record = {}; + if (DENCHCLAW_VERSION) superProps.denchclaw_version = DENCHCLAW_VERSION; + if (OPENCLAW_VERSION) superProps.openclaw_version = OPENCLAW_VERSION; + if (Object.keys(superProps).length > 0) posthog.register(superProps); + initialized = true; } diff --git a/apps/web/lib/telemetry.ts b/apps/web/lib/telemetry.ts index cc89a1c5c0e..964393d48fb 100644 --- a/apps/web/lib/telemetry.ts +++ b/apps/web/lib/telemetry.ts @@ -6,6 +6,8 @@ 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; @@ -68,6 +70,8 @@ export function trackServer( event, properties: { ...properties, + denchclaw_version: DENCHCLAW_VERSION || undefined, + openclaw_version: OPENCLAW_VERSION || undefined, $process_person_profile: false, }, }); diff --git a/apps/web/next.config.ts b/apps/web/next.config.ts index 56b3ab6816c..ba59878e83f 100644 --- a/apps/web/next.config.ts +++ b/apps/web/next.config.ts @@ -1,8 +1,26 @@ import type { NextConfig } from "next"; +import { readFileSync } from "node:fs"; import path from "node:path"; import { homedir } from "node:os"; +import { createRequire } from "node:module"; + +const rootPkg = JSON.parse( + readFileSync(path.join(import.meta.dirname, "..", "..", "package.json"), "utf-8"), +) as { version?: string }; + +let openclawVersion = ""; +try { + const req = createRequire(import.meta.url); + const oclPkg = req("openclaw/package.json") as { version?: string }; + openclawVersion = oclPkg.version ?? ""; +} catch { /* openclaw not resolvable at build time */ } const nextConfig: NextConfig = { + env: { + NEXT_PUBLIC_DENCHCLAW_VERSION: rootPkg.version ?? "", + NEXT_PUBLIC_OPENCLAW_VERSION: openclawVersion, + }, + // Produce a self-contained standalone build so npm global installs // can run the web app with `node server.js` — no npm install or // next build required at runtime. diff --git a/extensions/posthog-analytics/index.ts b/extensions/posthog-analytics/index.ts index 627e322fb04..153af840d23 100644 --- a/extensions/posthog-analytics/index.ts +++ b/extensions/posthog-analytics/index.ts @@ -2,7 +2,11 @@ import { createPostHogClient, shutdownPostHogClient } from "./lib/posthog-client 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 { + POSTHOG_KEY as BUILT_IN_KEY, + DENCHCLAW_VERSION, + OPENCLAW_VERSION, +} from "./lib/build-env.js"; import type { PluginConfig } from "./lib/types.js"; export const id = "posthog-analytics"; @@ -29,7 +33,13 @@ export default function register(api: any) { return; } - const ph = createPostHogClient(apiKey, config?.host); + const versionProps: Record = {}; + const dcv = DENCHCLAW_VERSION || process.env.npm_package_version; + if (dcv) versionProps.denchclaw_version = dcv; + const ocv = OPENCLAW_VERSION || process.env.OPENCLAW_VERSION || process.env.OPENCLAW_SERVICE_VERSION; + if (ocv) versionProps.openclaw_version = ocv; + + const ph = createPostHogClient(apiKey, config?.host, versionProps); const traceCtx = new TraceContextManager(); const getPrivacyMode = () => readPrivacyMode(api.config); diff --git a/extensions/posthog-analytics/lib/build-env.ts b/extensions/posthog-analytics/lib/build-env.ts index 64d928042fa..1083d6f7575 100644 --- a/extensions/posthog-analytics/lib/build-env.ts +++ b/extensions/posthog-analytics/lib/build-env.ts @@ -1 +1,3 @@ export const POSTHOG_KEY = ""; +export const DENCHCLAW_VERSION = ""; +export const OPENCLAW_VERSION = ""; diff --git a/extensions/posthog-analytics/lib/posthog-client.ts b/extensions/posthog-analytics/lib/posthog-client.ts index d23dfd8eeb4..165e73b841d 100644 --- a/extensions/posthog-analytics/lib/posthog-client.ts +++ b/extensions/posthog-analytics/lib/posthog-client.ts @@ -15,12 +15,14 @@ export interface CaptureEvent { export class PostHogClient { private apiKey: string; private host: string; + private globalProperties: Record; private queue: Array> = []; private timer: ReturnType | null = null; - constructor(apiKey: string, host?: string) { + constructor(apiKey: string, host?: string, globalProperties?: Record) { this.apiKey = apiKey; this.host = (host || DEFAULT_HOST).replace(/\/$/, ""); + this.globalProperties = globalProperties ?? {}; this.timer = setInterval(() => this.flush(), FLUSH_INTERVAL_MS); if (this.timer.unref) this.timer.unref(); } @@ -30,6 +32,7 @@ export class PostHogClient { event: event.event, distinct_id: event.distinctId, properties: { + ...this.globalProperties, ...event.properties, $lib: "denchclaw-posthog-plugin", }, @@ -68,8 +71,12 @@ export class PostHogClient { } } -export function createPostHogClient(apiKey: string, host?: string): PostHogClient { - return new PostHogClient(apiKey, host); +export function createPostHogClient( + apiKey: string, + host?: string, + globalProperties?: Record, +): PostHogClient { + return new PostHogClient(apiKey, host, globalProperties); } export async function shutdownPostHogClient(client: PostHogClient): Promise { diff --git a/scripts/build-plugin-env.mjs b/scripts/build-plugin-env.mjs index 48d5e9f1199..629fa41cf5b 100644 --- a/scripts/build-plugin-env.mjs +++ b/scripts/build-plugin-env.mjs @@ -1,7 +1,24 @@ -import { writeFileSync } from "node:fs"; +import { readFileSync, writeFileSync } from "node:fs"; +import { createRequire } from "node:module"; const key = process.env.POSTHOG_KEY || ""; + +const rootPkg = JSON.parse(readFileSync("package.json", "utf-8")); +const denchclawVersion = rootPkg.version || ""; + +let openclawVersion = ""; +try { + const req = createRequire(import.meta.url); + const oclPkg = req("openclaw/package.json"); + openclawVersion = oclPkg.version || ""; +} catch { /* openclaw not resolvable at build time */ } + writeFileSync( "extensions/posthog-analytics/lib/build-env.js", - `export const POSTHOG_KEY = ${JSON.stringify(key)};\n`, + [ + `export const POSTHOG_KEY = ${JSON.stringify(key)};`, + `export const DENCHCLAW_VERSION = ${JSON.stringify(denchclawVersion)};`, + `export const OPENCLAW_VERSION = ${JSON.stringify(openclawVersion)};`, + "", + ].join("\n"), ); diff --git a/src/telemetry/plugin-key-fallback.test.ts b/src/telemetry/plugin-key-fallback.test.ts index c3e6e5782b8..3d2e5373f8b 100644 --- a/src/telemetry/plugin-key-fallback.test.ts +++ b/src/telemetry/plugin-key-fallback.test.ts @@ -44,6 +44,8 @@ describe("posthog-analytics plugin key fallback", () => { it("uses api.config.apiKey when provided", async () => { vi.doMock("../../extensions/posthog-analytics/lib/build-env.js", () => ({ POSTHOG_KEY: "built-in-key", + DENCHCLAW_VERSION: "", + OPENCLAW_VERSION: "", })); const { default: register } = await import( @@ -52,12 +54,18 @@ describe("posthog-analytics plugin key fallback", () => { const api = createMockApi({ apiKey: "config-key", enabled: true }); register(api); - expect(mockCreatePostHogClient).toHaveBeenCalledWith("config-key", undefined); + expect(mockCreatePostHogClient).toHaveBeenCalledWith( + "config-key", + undefined, + expect.any(Object), + ); }); it("falls back to built-in key when api.config has no apiKey", async () => { vi.doMock("../../extensions/posthog-analytics/lib/build-env.js", () => ({ POSTHOG_KEY: "built-in-key", + DENCHCLAW_VERSION: "", + OPENCLAW_VERSION: "", })); const { default: register } = await import( @@ -66,12 +74,18 @@ describe("posthog-analytics plugin key fallback", () => { const api = createMockApi(); register(api); - expect(mockCreatePostHogClient).toHaveBeenCalledWith("built-in-key", undefined); + expect(mockCreatePostHogClient).toHaveBeenCalledWith( + "built-in-key", + undefined, + expect.any(Object), + ); }); it("does not initialize when neither config nor built-in key is available", async () => { vi.doMock("../../extensions/posthog-analytics/lib/build-env.js", () => ({ POSTHOG_KEY: "", + DENCHCLAW_VERSION: "", + OPENCLAW_VERSION: "", })); const { default: register } = await import( @@ -87,6 +101,8 @@ describe("posthog-analytics plugin key fallback", () => { it("registers lifecycle hooks when built-in key is used", async () => { vi.doMock("../../extensions/posthog-analytics/lib/build-env.js", () => ({ POSTHOG_KEY: "built-in-key", + DENCHCLAW_VERSION: "", + OPENCLAW_VERSION: "", })); const { default: register } = await import( diff --git a/src/telemetry/telemetry.ts b/src/telemetry/telemetry.ts index 63efb45fd94..89fe9988ce9 100644 --- a/src/telemetry/telemetry.ts +++ b/src/telemetry/telemetry.ts @@ -1,5 +1,6 @@ import { PostHog } from "posthog-node"; import { readTelemetryConfig, getOrCreateAnonymousId } from "./config.js"; +import { VERSION, resolveOpenClawVersion } from "../version.js"; const POSTHOG_KEY = process.env.POSTHOG_KEY || ""; const POSTHOG_HOST = "https://us.i.posthog.com"; @@ -27,6 +28,8 @@ function getMachineContext(): Record { os: process.platform, arch: process.arch, node_version: process.version, + denchclaw_version: VERSION, + openclaw_version: resolveOpenClawVersion(), }; } diff --git a/src/version.ts b/src/version.ts index e2bb7164d21..de54e6cc092 100644 --- a/src/version.ts +++ b/src/version.ts @@ -88,7 +88,7 @@ export function resolveRuntimeServiceVersion( ); } -// Single source of truth for the current OpenClaw version. +// Single source of truth for the current DenchClaw version. // - Embedded/bundled builds: injected define or env var. // - Dev/npm builds: package.json. export const VERSION = @@ -96,3 +96,26 @@ export const VERSION = process.env.OPENCLAW_BUNDLED_VERSION || resolveVersionFromModuleUrl(import.meta.url) || "0.0.0"; + +let _cachedOpenClawVersion: string | undefined; + +export function resolveOpenClawVersion(): string | undefined { + if (_cachedOpenClawVersion !== undefined) return _cachedOpenClawVersion || undefined; + + const envVersion = (process.env.OPENCLAW_VERSION || process.env.OPENCLAW_SERVICE_VERSION)?.trim(); + if (envVersion) { + _cachedOpenClawVersion = envVersion; + return envVersion; + } + + try { + const req = createRequire(import.meta.url); + const pkg = req("openclaw/package.json") as { version?: string }; + const v = pkg.version?.trim(); + _cachedOpenClawVersion = v || ""; + return v || undefined; + } catch { + _cachedOpenClawVersion = ""; + return undefined; + } +}