From cac2eda425bb6d822192ed946624f60a27b128ff Mon Sep 17 00:00:00 2001 From: zahlekhan Date: Mon, 6 Jul 2026 20:30:29 +0530 Subject: [PATCH] Track CLI OAuth users in PostHog --- packages/openui-cli/README.md | 2 +- packages/openui-cli/package.json | 2 +- packages/openui-cli/src/auth/mint.ts | 6 +++ packages/openui-cli/src/lib/telemetry.ts | 49 +++++++++++++++++++----- 4 files changed, 47 insertions(+), 12 deletions(-) diff --git a/packages/openui-cli/README.md b/packages/openui-cli/README.md index 72ce361c8..1f22abe13 100644 --- a/packages/openui-cli/README.md +++ b/packages/openui-cli/README.md @@ -182,7 +182,7 @@ node dist/index.js generate --help ## Telemetry -The CLI sends anonymous usage analytics. Disable it with the global `--no-telemetry` flag or by setting `DO_NOT_TRACK=1` in the environment. +The CLI sends usage analytics; OAuth sign-ins may link usage to your OIDC account ID. It does not send code, prompts, API keys, email, or name. Disable telemetry with `--no-telemetry` or `DO_NOT_TRACK=1`. ```bash openui create --no-telemetry diff --git a/packages/openui-cli/package.json b/packages/openui-cli/package.json index feca0de11..aeea95ac6 100644 --- a/packages/openui-cli/package.json +++ b/packages/openui-cli/package.json @@ -1,6 +1,6 @@ { "name": "@openuidev/cli", - "version": "0.1.2", + "version": "0.1.3", "description": "CLI for OpenUI — scaffold generative UI chat apps and generate LLM system prompts from component libraries", "bin": { "openui": "dist/index.js" diff --git a/packages/openui-cli/src/auth/mint.ts b/packages/openui-cli/src/auth/mint.ts index bec7d2f19..cfb2738c9 100644 --- a/packages/openui-cli/src/auth/mint.ts +++ b/packages/openui-cli/src/auth/mint.ts @@ -1,3 +1,4 @@ +import { telemetry } from "../lib/telemetry"; import { Authenticator } from "./authenticator"; // Thesys console OAuth + key mint (same flow as create-c1-app). The OpenUI Cloud @@ -43,6 +44,11 @@ export async function mintCloudApiKey(projectName: string): Promise { } const data = (await res.json()) as { apiKey?: string }; if (!data.apiKey) throw new Error("The server did not return an API key."); + + const oidcSub = + (profile["sub"] as string | undefined) ?? (userInfo?.["sub"] as string | undefined); + if (oidcSub) telemetry.aliasOidcSubject(oidcSub); + return data.apiKey; } diff --git a/packages/openui-cli/src/lib/telemetry.ts b/packages/openui-cli/src/lib/telemetry.ts index df76a5ce8..42217ae01 100644 --- a/packages/openui-cli/src/lib/telemetry.ts +++ b/packages/openui-cli/src/lib/telemetry.ts @@ -10,12 +10,19 @@ const POSTHOG_HOST = process.env["OPENUI_POSTHOG_HOST"] ?? "https://us.i.posthog const SHUTDOWN_TIMEOUT_MS = 2000; const isTruthyEnv = (v?: string) => v === "1" || v?.toLowerCase() === "true"; +const isTelemetryDebug = () => process.env["OPENUI_TELEMETRY_DEBUG"] === "1"; const configDir = () => path.join(process.env["XDG_CONFIG_HOME"] ?? path.join(os.homedir(), ".config"), "openui"); const isCi = () => { const e = process.env; return isTruthyEnv(e["CI"]) || !!e["GITHUB_ACTIONS"] || !!e["GITLAB_CI"] || !!e["BUILDKITE"]; }; +const isInteractiveTerminal = () => Boolean(process.stdin.isTTY && process.stdout.isTTY); +const debugLogPostHogFailure = (stage: string, error: unknown) => { + if (!isTelemetryDebug()) return; + const message = error instanceof Error ? error.message : String(error); + console.warn(`[OpenUI telemetry] PostHog ${stage} failed: ${message}`); +}; type Stored = { distinctId: string; firstRunNoticeShown?: boolean }; @@ -72,6 +79,7 @@ export class Telemetry { if (optedOut) return; // enabled stays false → all capture() are no-ops const state = loadOrCreateState(); this.distinctId = state.distinctId; + const interactiveTerminal = isInteractiveTerminal(); this.superProps = { cli_version: opts.cliVersion, os: process.platform, @@ -79,6 +87,9 @@ export class Telemetry { arch: process.arch, node_version: process.version, ci: isCi(), + stdin_is_tty: Boolean(process.stdin.isTTY), + stdout_is_tty: Boolean(process.stdout.isTTY), + is_interactive_terminal: interactiveTerminal, }; try { this.client = new PostHog(POSTHOG_KEY, { @@ -88,8 +99,9 @@ export class Telemetry { }); // Telemetry is best-effort: swallow network/flush errors so an offline CLI // run never spams the user's console with PostHog stack traces. - this.client.on("error", () => {}); - } catch { + this.client.on("error", (error) => debugLogPostHogFailure("request", error)); + } catch (error) { + debugLogPostHogFailure("init", error); return; } this.enabled = true; @@ -101,12 +113,11 @@ export class Telemetry { if (typeof args[0] === "string" && args[0].includes("flushing PostHog")) return; origError(...args); }; - if (process.env["OPENUI_TELEMETRY_DEBUG"] === "1") this.client.debug(); + if (isTelemetryDebug()) this.client.debug(); if (state.isFirstRun) { process.stderr.write( - "\nā—† OpenUI CLI collects anonymous usage analytics to improve the tool.\n" + - " No code, prompts, keys, or personal data are collected.\n" + - " Opt out anytime: set DO_NOT_TRACK=1 or pass --no-telemetry.\n\n", + "\nā—† OpenUI CLI collects usage analytics; OAuth sign-ins may link usage to your OIDC account ID.\n" + + " No code, prompts, API keys, email, or name are collected. Opt out: set DO_NOT_TRACK=1 or pass --no-telemetry.\n\n", ); state.persist(); } @@ -124,11 +135,29 @@ export class Telemetry { event, properties: { ...this.superProps, ...properties }, }); - } catch { - /* telemetry must never throw */ + } catch (error) { + debugLogPostHogFailure("capture", error); + } + } + + alias(distinctId: string, alias: string) { + if (!this.enabled || !this.client) return; + if (!distinctId || !alias || distinctId === alias) return; + try { + this.client.alias({ distinctId, alias }); + } catch (error) { + debugLogPostHogFailure("alias", error); } } + aliasOidcSubject(oidcSub: string) { + if (!this.enabled || !this.client || !oidcSub || oidcSub === this.distinctId) { + return; + } + + this.alias(oidcSub, this.distinctId); + } + async shutdown() { if (!this.enabled || !this.client) return; try { @@ -136,8 +165,8 @@ export class Telemetry { this.client.shutdown(), new Promise((r) => setTimeout(r, SHUTDOWN_TIMEOUT_MS)), ]); - } catch { - /* swallow */ + } catch (error) { + debugLogPostHogFailure("shutdown", error); } } }