added basic cli telemetry

This commit is contained in:
dswbx
2025-03-14 11:40:56 +01:00
parent a55d93b5ff
commit 6015acb914
6 changed files with 141 additions and 6 deletions

View File

@@ -88,6 +88,7 @@
"postcss": "^8.5.3", "postcss": "^8.5.3",
"postcss-preset-mantine": "^1.17.0", "postcss-preset-mantine": "^1.17.0",
"postcss-simple-vars": "^7.0.1", "postcss-simple-vars": "^7.0.1",
"posthog-js-lite": "^3.4.2",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-hook-form": "^7.54.2", "react-hook-form": "^7.54.2",

View File

@@ -9,6 +9,7 @@ import { env } from "core";
import color from "picocolors"; import color from "picocolors";
import { overridePackageJson, updateBkndPackages } from "./npm"; import { overridePackageJson, updateBkndPackages } from "./npm";
import { type Template, templates } from "./templates"; import { type Template, templates } from "./templates";
import { createScoped, flush } from "cli/utils/telemetry";
const config = { const config = {
types: { types: {
@@ -48,8 +49,16 @@ function errorOutro() {
process.exit(1); process.exit(1);
} }
async function onExit() {
await flush();
}
async function action(options: { template?: string; dir?: string; integration?: string }) { async function action(options: { template?: string; dir?: string; integration?: string }) {
console.log(""); console.log("");
const $t = createScoped("create");
$t.capture("start", {
options,
});
const downloadOpts = { const downloadOpts = {
dir: options.dir || "./", dir: options.dir || "./",
@@ -68,6 +77,7 @@ async function action(options: { template?: string; dir?: string; integration?:
})(), })(),
); );
$t.properties.at = "dir";
if (!options.dir) { if (!options.dir) {
const dir = await $p.text({ const dir = await $p.text({
message: "Where to create your project?", message: "Where to create your project?",
@@ -75,24 +85,29 @@ async function action(options: { template?: string; dir?: string; integration?:
initialValue: downloadOpts.dir, initialValue: downloadOpts.dir,
}); });
if ($p.isCancel(dir)) { if ($p.isCancel(dir)) {
await onExit();
process.exit(1); process.exit(1);
} }
downloadOpts.dir = dir || "./"; downloadOpts.dir = dir || "./";
} }
$t.properties.at = "dir";
if (fs.existsSync(downloadOpts.dir)) { if (fs.existsSync(downloadOpts.dir)) {
const clean = await $p.confirm({ const clean = await $p.confirm({
message: `Directory ${color.cyan(downloadOpts.dir)} exists. Clean it?`, message: `Directory ${color.cyan(downloadOpts.dir)} exists. Clean it?`,
initialValue: false, initialValue: false,
}); });
if ($p.isCancel(clean)) { if ($p.isCancel(clean)) {
await onExit();
process.exit(1); process.exit(1);
} }
downloadOpts.clean = clean; downloadOpts.clean = clean;
$t.properties.clean = clean;
} }
// don't track name for privacy
let name = downloadOpts.dir.includes("/") let name = downloadOpts.dir.includes("/")
? downloadOpts.dir.split("/").pop() ? downloadOpts.dir.split("/").pop()
: downloadOpts.dir.replace(/[./]/g, ""); : downloadOpts.dir.replace(/[./]/g, "");
@@ -100,13 +115,17 @@ async function action(options: { template?: string; dir?: string; integration?:
if (!name || name.length === 0) name = "bknd"; if (!name || name.length === 0) name = "bknd";
let template: Template | undefined; let template: Template | undefined;
if (options.template) { if (options.template) {
$t.properties.at = "template";
template = templates.find((t) => t.key === options.template) as Template; template = templates.find((t) => t.key === options.template) as Template;
if (!template) { if (!template) {
await onExit();
$p.log.error(`Template ${color.cyan(options.template)} not found`); $p.log.error(`Template ${color.cyan(options.template)} not found`);
process.exit(1); process.exit(1);
} }
} else { } else {
$t.properties.at = "integration";
let integration: string | undefined = options.integration; let integration: string | undefined = options.integration;
if (!integration) { if (!integration) {
await $p.stream.info( await $p.stream.info(
@@ -128,8 +147,10 @@ async function action(options: { template?: string; dir?: string; integration?:
}); });
if ($p.isCancel(type)) { if ($p.isCancel(type)) {
await onExit();
process.exit(1); process.exit(1);
} }
$t.properties.type = type;
const _integration = await $p.select({ const _integration = await $p.select({
message: `Which ${color.cyan(config.types[type])} do you want to continue with?`, message: `Which ${color.cyan(config.types[type])} do you want to continue with?`,
@@ -139,11 +160,14 @@ async function action(options: { template?: string; dir?: string; integration?:
})) as any, })) as any,
}); });
if ($p.isCancel(_integration)) { if ($p.isCancel(_integration)) {
await onExit();
process.exit(1); process.exit(1);
} }
integration = String(_integration); integration = String(_integration);
$t.properties.integration = integration;
} }
if (!integration) { if (!integration) {
await onExit();
$p.log.error("No integration selected"); $p.log.error("No integration selected");
process.exit(1); process.exit(1);
} }
@@ -152,15 +176,18 @@ async function action(options: { template?: string; dir?: string; integration?:
const choices = templates.filter((t) => t.integration === integration); const choices = templates.filter((t) => t.integration === integration);
if (choices.length === 0) { if (choices.length === 0) {
await onExit();
$p.log.error(`No templates found for "${color.cyan(String(integration))}"`); $p.log.error(`No templates found for "${color.cyan(String(integration))}"`);
process.exit(1); process.exit(1);
} else if (choices.length > 1) { } else if (choices.length > 1) {
$t.properties.at = "template";
const selected_template = await $p.select({ const selected_template = await $p.select({
message: "Pick a template", message: "Pick a template",
options: choices.map((t) => ({ value: t.key, label: t.title, hint: t.description })), options: choices.map((t) => ({ value: t.key, label: t.title, hint: t.description })),
}); });
if ($p.isCancel(selected_template)) { if ($p.isCancel(selected_template)) {
await onExit();
process.exit(1); process.exit(1);
} }
@@ -170,10 +197,12 @@ async function action(options: { template?: string; dir?: string; integration?:
} }
} }
if (!template) { if (!template) {
await onExit();
$p.log.error("No template selected"); $p.log.error("No template selected");
process.exit(1); process.exit(1);
} }
$t.properties.template = template.key;
const ctx = { template, dir: downloadOpts.dir, name }; const ctx = { template, dir: downloadOpts.dir, name };
{ {
@@ -182,6 +211,8 @@ async function action(options: { template?: string; dir?: string; integration?:
$p.log.warn(color.dim("[DEV] Using local ref: ") + color.yellow(given)); $p.log.warn(color.dim("[DEV] Using local ref: ") + color.yellow(given));
}, },
}); });
$t.properties.ref = ref;
$t.capture("used");
const prefix = const prefix =
template.ref === true template.ref === true
@@ -191,7 +222,6 @@ async function action(options: { template?: string; dir?: string; integration?:
: ""; : "";
const url = `${template.path}${prefix}`; const url = `${template.path}${prefix}`;
//console.log("url", url);
const s = $p.spinner(); const s = $p.spinner();
await s.start("Downloading template..."); await s.start("Downloading template...");
try { try {
@@ -234,8 +264,10 @@ async function action(options: { template?: string; dir?: string; integration?:
}); });
if ($p.isCancel(install)) { if ($p.isCancel(install)) {
await onExit();
process.exit(1); process.exit(1);
} else if (install) { } else if (install) {
$t.properties.install = true;
const install_cmd = template.scripts?.install || "npm install"; const install_cmd = template.scripts?.install || "npm install";
const s = $p.spinner(); const s = $p.spinner();
@@ -259,6 +291,7 @@ async function action(options: { template?: string; dir?: string; integration?:
await template.postinstall(ctx); await template.postinstall(ctx);
} }
} else { } else {
$t.properties.install = false;
await $p.stream.warn( await $p.stream.warn(
(async function* () { (async function* () {
yield* typewriter( yield* typewriter(
@@ -291,5 +324,6 @@ If you need help, check ${color.cyan("https://docs.bknd.io")} or join our Discor
})(), })(),
); );
$t.capture("complete");
$p.outro(color.green("Setup complete.")); $p.outro(color.green("Setup complete."));
} }

View File

@@ -4,21 +4,36 @@ import { Command } from "commander";
import color from "picocolors"; import color from "picocolors";
import * as commands from "./commands"; import * as commands from "./commands";
import { getVersion } from "./utils/sys"; import { getVersion } from "./utils/sys";
import { capture, flush, init } from "cli/utils/telemetry";
const program = new Command(); const program = new Command();
export async function main() { export async function main() {
await init();
capture("start");
const version = await getVersion(); const version = await getVersion();
program program
.name("bknd") .name("bknd")
.description(color.yellowBright("⚡") + " bknd cli " + color.bold(color.cyan(`v${version}`))) .description(color.yellowBright("⚡") + " bknd cli " + color.bold(color.cyan(`v${version}`)))
.version(version); .version(version)
.hook("preAction", (thisCommand, actionCommand) => {
capture(`cmd_${actionCommand.name()}`);
})
.hook("postAction", async () => {
await flush();
});
// register commands // register commands
for (const command of Object.values(commands)) { for (const command of Object.values(commands)) {
command(program); command(program);
} }
program.parse(); await program.parseAsync();
} }
main().then(null).catch(console.error); main()
.then(null)
.catch(async (e) => {
await flush();
console.error(e);
});

View File

@@ -0,0 +1,75 @@
import { PostHog } from "posthog-js-lite";
import { getVersion } from "cli/utils/sys";
import { $console, env } from "core";
type Properties = { [p: string]: any };
let posthog: PostHog | null = null;
let version: string | null = null;
const enabled = env("cli_telemetry");
export async function init() {
try {
if (!enabled) {
$console.debug("Telemetry disabled");
return;
}
$console.debug("Init telemetry");
if (!posthog) {
posthog = new PostHog(process.env.POSTHOG_KEY!, {
host: process.env.POSTHOG_HOST!,
disabled: !enabled,
});
}
version = await getVersion();
} catch (e) {
$console.debug("Failed to initialize telemetry", e);
}
}
export function client(): PostHog {
if (!posthog) {
throw new Error("PostHog client not initialized. Call init() first.");
}
return posthog;
}
export function capture(event: string, properties: Properties = {}): void {
try {
if (!enabled) return;
const name = `cli_${event}`;
const props = {
...properties,
version: version!,
};
$console.debug("Capture", name, props);
client().capture(name, props);
} catch (e) {
$console.debug("Failed to capture telemetry", e);
}
}
export function createScoped(scope: string, p: Properties = {}) {
const properties = p;
const _capture = (event: string, props: Properties = {}) => {
return capture(`${scope}_${event}`, { ...properties, ...props });
};
return { capture: _capture, properties };
}
export async function flush() {
try {
if (!enabled) return;
$console.debug("Flush telemetry");
if (posthog) {
await posthog.flush();
}
} catch (e) {
$console.debug("Failed to flush telemetry", e);
}
}

View File

@@ -1,7 +1,7 @@
export type Env = {}; export type Env = {};
export const is_toggled = (given: unknown): boolean => { export const is_toggled = (given: unknown, fallback?: boolean): boolean => {
return typeof given === "string" ? [1, "1", "true"].includes(given) : Boolean(given); return typeof given === "string" ? [1, "1", "true"].includes(given) : Boolean(fallback);
}; };
export function isDebug(): boolean { export function isDebug(): boolean {
@@ -34,6 +34,13 @@ const envs = {
return typeof v === "string" ? v : undefined; return typeof v === "string" ? v : undefined;
}, },
}, },
// cli telemetry
cli_telemetry: {
key: "BKND_CLI_TELEMETRY",
validate: (v: unknown) => {
return is_toggled(v, true);
},
},
// module manager debug: { // module manager debug: {
modules_debug: { modules_debug: {
key: "BKND_MODULES_DEBUG", key: "BKND_MODULES_DEBUG",

View File

@@ -55,6 +55,7 @@
"oauth4webapi": "^2.11.1", "oauth4webapi": "^2.11.1",
"object-path-immutable": "^4.1.2", "object-path-immutable": "^4.1.2",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
"posthog-js-lite": "^3.4.2",
"radix-ui": "^1.1.3", "radix-ui": "^1.1.3",
"swr": "^2.3.3", "swr": "^2.3.3",
}, },
@@ -2694,6 +2695,8 @@
"postgres-range": ["postgres-range@1.1.4", "", {}, "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w=="], "postgres-range": ["postgres-range@1.1.4", "", {}, "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w=="],
"posthog-js-lite": ["posthog-js-lite@3.4.2", "", {}, "sha512-YYXZORZHDmgD4OnSCyfRgPnBdHPxFDtgybBGmrQV5fOyCd0Bc12fYs2KlGwrRkIs2jYftT3CdfS0jTNo5RjB7Q=="],
"prelude-ls": ["prelude-ls@1.1.2", "", {}, "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w=="], "prelude-ls": ["prelude-ls@1.1.2", "", {}, "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w=="],
"prettier": ["prettier@1.19.1", "", { "bin": { "prettier": "./bin-prettier.js" } }, "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew=="], "prettier": ["prettier@1.19.1", "", { "bin": { "prettier": "./bin-prettier.js" } }, "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew=="],