Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/openui-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion packages/openui-cli/package.json
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
6 changes: 6 additions & 0 deletions packages/openui-cli/src/auth/mint.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -43,6 +44,11 @@ export async function mintCloudApiKey(projectName: string): Promise<string> {
}
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;
}

Expand Down
49 changes: 39 additions & 10 deletions packages/openui-cli/src/lib/telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };

Expand Down Expand Up @@ -72,13 +79,17 @@ 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,
os_release: os.release(),
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, {
Expand All @@ -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;
Expand All @@ -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();
}
Expand All @@ -124,20 +135,38 @@ 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 {
await Promise.race([
this.client.shutdown(),
new Promise<void>((r) => setTimeout(r, SHUTDOWN_TIMEOUT_MS)),
]);
} catch {
/* swallow */
} catch (error) {
debugLogPostHogFailure("shutdown", error);
}
}
}
Expand Down
Loading