From a47139587dab4c0bb781b159069da8e554e410db Mon Sep 17 00:00:00 2001 From: SiebeBaree Date: Sun, 17 May 2026 17:13:05 +0200 Subject: [PATCH 1/3] impr: made analytics run in background for optimal performance --- bun.lock | 2 +- package.json | 2 +- src/cli.ts | 35 +++++++----- src/lib/analytics-worker.ts | 47 ++++++++++++++++ src/lib/analytics.ts | 107 ++++++++++++++++++------------------ 5 files changed, 122 insertions(+), 71 deletions(-) create mode 100644 src/lib/analytics-worker.ts diff --git a/bun.lock b/bun.lock index ef59fcc..1d2b54f 100644 --- a/bun.lock +++ b/bun.lock @@ -37,7 +37,7 @@ }, }, "overrides": { - "axios": ">=1.15.0", + "axios": "^1.15.2", "follow-redirects": ">=1.15.11", "picomatch": ">=4.0.4", "vite": ">=8.0.4", diff --git a/package.json b/package.json index 24e288a..b2321a8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@enkryptify/cli", - "version": "0.3.0", + "version": "0.3.5", "bin": { "ek": "./dist/cli.js" }, diff --git a/src/cli.ts b/src/cli.ts index 9724720..08c1242 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -9,6 +9,12 @@ import { checkForUpdate } from "@/lib/versionCheck"; import { Command } from "commander"; import { getCompletions } from "./complete/complete"; +if (process.argv[2] === "__analytics") { + const { runAnalyticsWorker } = await import("@/lib/analytics-worker"); + await runAnalyticsWorker(); + process.exit(0); +} + const isCompletion = process.argv[2] === "__complete"; const program = new Command(); @@ -44,18 +50,17 @@ if (!isCompletion && !isUpgrade) { checkForUpdate().catch(() => {}); } -program - .parseAsync(process.argv) - .catch((error) => { - if (error instanceof CLIError) { - analytics.track("cli_error", { error_code: error.errorCode, error_message: error.message }); - logger.error(error.message, { why: error.why, fix: error.fix, docs: error.docs }); - } else { - analytics.track("cli_error", { error_message: error instanceof Error ? error.message : String(error) }); - logger.error(error instanceof Error ? error.message : String(error)); - } - process.exitCode = 1; - }) - .finally(async () => { - await analytics.shutdown(); - }); +process.on("exit", () => { + analytics.shutdown(); +}); + +program.parseAsync(process.argv).catch((error) => { + if (error instanceof CLIError) { + analytics.track("cli_error", { error_code: error.errorCode, error_message: error.message }); + logger.error(error.message, { why: error.why, fix: error.fix, docs: error.docs }); + } else { + analytics.track("cli_error", { error_message: error instanceof Error ? error.message : String(error) }); + logger.error(error instanceof Error ? error.message : String(error)); + } + process.exitCode = 1; +}); diff --git a/src/lib/analytics-worker.ts b/src/lib/analytics-worker.ts new file mode 100644 index 0000000..82c2180 --- /dev/null +++ b/src/lib/analytics-worker.ts @@ -0,0 +1,47 @@ +import { env } from "@/env"; + +export type WorkerEvent = { + distinctId: string; + event: string; + properties: Record; + timestamp: string; +}; + +export type WorkerPayload = { + events: WorkerEvent[]; +}; + +const REQUEST_TIMEOUT_MS = 5000; + +async function postEvent(event: WorkerEvent): Promise { + try { + await fetch(`${env.POSTHOG_HOST}/i/v0/e/`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + api_key: env.POSTHOG_API_KEY, + distinct_id: event.distinctId, + event: event.event, + properties: event.properties, + timestamp: event.timestamp, + }), + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + }); + } catch { + // Best-effort — never throw from the analytics worker + } +} + +export async function runAnalyticsWorker(): Promise { + try { + const raw = process.env.EK_ANALYTICS_PAYLOAD; + if (!raw) return; + + const payload = JSON.parse(raw) as WorkerPayload; + if (!Array.isArray(payload.events) || payload.events.length === 0) return; + + await Promise.allSettled(payload.events.map(postEvent)); + } catch { + // Worker must never crash visibly + } +} diff --git a/src/lib/analytics.ts b/src/lib/analytics.ts index 65522c0..169994f 100644 --- a/src/lib/analytics.ts +++ b/src/lib/analytics.ts @@ -3,7 +3,6 @@ import { keyring } from "@/lib/keyring"; import { logger } from "@/lib/logger"; import { CLIError } from "@/lib/errors"; import { loadConfig, saveConfig } from "@/lib/config"; -import { PostHog } from "posthog-node"; import { randomUUID } from "crypto"; type StoredAuthData = { @@ -27,12 +26,19 @@ export type CommandTracker = { error(error: unknown): void; }; -let posthog: PostHog | null = null; +type QueuedEvent = { + distinctId: string; + event: string; + properties: Record; + timestamp: string; +}; + let enabled = false; let distinctId: string | null = null; let anonymousId: string | null = null; let superProperties: Record = {}; let noticeShown = false; +const pendingEvents: QueuedEvent[] = []; function isOptedOut(): boolean { const telemetryEnv = process.env.EK_TELEMETRY; @@ -84,6 +90,19 @@ async function showFirstRunNotice(): Promise { } } +function getSelfSpawnCommand(): string[] { + const exec = process.execPath; + const arg1 = process.argv[1]; + + // Compiled Bun binary: argv[1] is a virtual /$bunfs/ path inside the binary, + // not a real file we can re-exec — just run the binary again. + if (!arg1 || arg1.startsWith("/$bunfs/")) { + return [exec]; + } + + return [exec, arg1]; +} + export const analytics = { async init(): Promise { if (isTestEnvironment() || isOptedOut()) { @@ -92,14 +111,12 @@ export const analytics = { } try { - // Check config-level opt-out const config = await loadConfig(); if (config.settings?.telemetry === "false") { enabled = false; return; } - // Get or create anonymous ID anonymousId = config.settings?.anonymousId ?? null; if (!anonymousId) { anonymousId = randomUUID(); @@ -108,10 +125,8 @@ export const analytics = { await saveConfig(config); } - // Default distinct ID is the anonymous ID distinctId = anonymousId; - // Try to read user identity from keyring try { const authDataString = await keyring.get("enkryptify"); if (authDataString) { @@ -124,7 +139,6 @@ export const analytics = { // Best-effort, continue with anonymous ID } - // Build super properties superProperties = { cli_version: env.CLI_VERSION, os: process.platform, @@ -133,35 +147,6 @@ export const analytics = { is_ci: detectCI(), }; - // Initialize PostHog client - posthog = new PostHog(env.POSTHOG_API_KEY, { - host: env.POSTHOG_HOST, - flushAt: 10, - flushInterval: 5000, - requestTimeout: 3000, - disabled: false, - }); - - // Identify user if we have a real user ID (not anonymous) - if (distinctId !== anonymousId) { - try { - const authDataString = await keyring.get("enkryptify"); - if (authDataString) { - const parsed: unknown = JSON.parse(authDataString); - if (isValidStoredAuthData(parsed)) { - posthog.identify({ - distinctId: parsed.userId, - properties: { - email: parsed.email, - }, - }); - } - } - } catch { - // Best-effort - } - } - enabled = true; } catch { // Analytics initialization should never break the CLI @@ -170,44 +155,46 @@ export const analytics = { }, identify(userId: string, email: string): void { - if (!enabled || !posthog) return; + if (!enabled) return; try { - // If we were using an anonymous ID, alias it to the real user + const identifyProps: Record = { + ...superProperties, + $set: { email }, + }; + + // Link the prior anonymous ID to the user when transitioning from anonymous if (anonymousId && distinctId === anonymousId) { - posthog.alias({ - distinctId: userId, - alias: anonymousId, - }); + identifyProps.$anon_distinct_id = anonymousId; } - distinctId = userId; - - posthog.identify({ + pendingEvents.push({ distinctId: userId, - properties: { - email, - }, + event: "$identify", + properties: identifyProps, + timestamp: new Date().toISOString(), }); + + distinctId = userId; } catch { // Best-effort } }, track(event: string, properties?: Record): void { - if (!enabled || !posthog || !distinctId) return; + if (!enabled || !distinctId) return; try { - // Show first-run notice (fire-and-forget) void showFirstRunNotice(); - posthog.capture({ + pendingEvents.push({ distinctId, event, properties: { ...superProperties, ...properties, }, + timestamp: new Date().toISOString(), }); } catch { // Never throw from analytics @@ -242,11 +229,23 @@ export const analytics = { }; }, - async shutdown(): Promise { - if (!enabled || !posthog) return; + shutdown(): void { + if (!enabled || pendingEvents.length === 0) return; try { - await Promise.race([posthog.shutdown(), new Promise((resolve) => setTimeout(resolve, 2000))]); + const payload = JSON.stringify({ events: pendingEvents }); + pendingEvents.length = 0; + + const cmd = getSelfSpawnCommand(); + const proc = Bun.spawn([...cmd, "__analytics"], { + env: { ...process.env, EK_ANALYTICS_PAYLOAD: payload }, + stdin: "ignore", + stdout: "ignore", + stderr: "ignore", + // Detach so the worker survives the parent exiting + detached: true, + }); + proc.unref(); } catch { // Never throw from analytics shutdown } From d25801f93b7d73bc8222f938273847159ca0db98 Mon Sep 17 00:00:00 2001 From: SiebeBaree Date: Sun, 17 May 2026 17:47:24 +0200 Subject: [PATCH 2/3] fix: coderabbit comments --- src/lib/analytics-worker.ts | 2 +- src/lib/analytics.ts | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/lib/analytics-worker.ts b/src/lib/analytics-worker.ts index 82c2180..33ded25 100644 --- a/src/lib/analytics-worker.ts +++ b/src/lib/analytics-worker.ts @@ -34,7 +34,7 @@ async function postEvent(event: WorkerEvent): Promise { export async function runAnalyticsWorker(): Promise { try { - const raw = process.env.EK_ANALYTICS_PAYLOAD; + const raw = process.argv[3]; if (!raw) return; const payload = JSON.parse(raw) as WorkerPayload; diff --git a/src/lib/analytics.ts b/src/lib/analytics.ts index 169994f..a6f9e24 100644 --- a/src/lib/analytics.ts +++ b/src/lib/analytics.ts @@ -234,11 +234,8 @@ export const analytics = { try { const payload = JSON.stringify({ events: pendingEvents }); - pendingEvents.length = 0; - const cmd = getSelfSpawnCommand(); - const proc = Bun.spawn([...cmd, "__analytics"], { - env: { ...process.env, EK_ANALYTICS_PAYLOAD: payload }, + const proc = Bun.spawn([...cmd, "__analytics", payload], { stdin: "ignore", stdout: "ignore", stderr: "ignore", @@ -246,8 +243,9 @@ export const analytics = { detached: true, }); proc.unref(); + pendingEvents.length = 0; } catch { - // Never throw from analytics shutdown + // Never throw from analytics shutdown; leave pendingEvents intact } }, From 531f453f0a4a7ed0164f112b9636a3e75570b0f5 Mon Sep 17 00:00:00 2001 From: SiebeBaree Date: Sun, 17 May 2026 18:14:41 +0200 Subject: [PATCH 3/3] fix: dev build script --- scripts/build-dev.sh | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/scripts/build-dev.sh b/scripts/build-dev.sh index b4d8c44..48c7326 100755 --- a/scripts/build-dev.sh +++ b/scripts/build-dev.sh @@ -27,4 +27,11 @@ else fi bun build "$PROJECT_DIR/src/cli.ts" --compile --outfile "$DEST" "${ARGS[@]}" + +if [[ "$(uname -s)" == "Darwin" ]]; then + codesign --remove-signature "$DEST" >/dev/null 2>&1 || true + codesign --force --sign - "$DEST" >/dev/null 2>&1 || \ + echo "⚠ codesign failed — ek-dev may be killed by AMFI on macOS 26+" +fi + echo "✓ Installed ek-dev → $DEST"