mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-16 04:27:21 +00:00
Merge pull request #111 from bknd-io/feat/cli-telemetry
added basic cli telemetry
This commit is contained in:
@@ -14,6 +14,7 @@ describe("env", () => {
|
|||||||
expect(is_toggled(1)).toBe(true);
|
expect(is_toggled(1)).toBe(true);
|
||||||
expect(is_toggled(0)).toBe(false);
|
expect(is_toggled(0)).toBe(false);
|
||||||
expect(is_toggled("anything else")).toBe(false);
|
expect(is_toggled("anything else")).toBe(false);
|
||||||
|
expect(is_toggled(undefined, true)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("env()", () => {
|
test("env()", () => {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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."));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
|||||||
75
app/src/cli/utils/telemetry.ts
Normal file
75
app/src/cli/utils/telemetry.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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(given || 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",
|
||||||
|
|||||||
3
bun.lock
3
bun.lock
@@ -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=="],
|
||||||
|
|||||||
Reference in New Issue
Block a user