From aa974897858737f8b39d4c31f88b6e322be32016 Mon Sep 17 00:00:00 2001 From: kumarabhirup Date: Sun, 15 Mar 2026 04:33:44 -0700 Subject: [PATCH] =?UTF-8?q?=F0=9F=91=8C=20IMPROVE:=20feat(telemetry):=20ad?= =?UTF-8?q?d=20DenchClaw=20and=20OpenClaw=20version=20tracking?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Integrate version tracking for DenchClaw and OpenClaw into the telemetry system. The versions are now read from the package.json and environment variables, and are included in the PostHog client initialization and telemetry events. This enhancement allows for better monitoring and analytics of the versions in use. --- apps/web/app/components/posthog-provider.tsx | 8 ++++++ apps/web/lib/telemetry.ts | 4 +++ apps/web/next.config.ts | 18 +++++++++++++ extensions/posthog-analytics/index.ts | 14 +++++++++-- extensions/posthog-analytics/lib/build-env.ts | 2 ++ .../posthog-analytics/lib/posthog-client.ts | 13 +++++++--- scripts/build-plugin-env.mjs | 21 ++++++++++++++-- src/telemetry/plugin-key-fallback.test.ts | 20 +++++++++++++-- src/telemetry/telemetry.ts | 3 +++ src/version.ts | 25 ++++++++++++++++++- 10 files changed, 118 insertions(+), 10 deletions(-) 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; + } +}