From acb0f6b0063773b214ff133b7d81620039f6696c Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sat, 11 Apr 2026 21:25:27 +0900 Subject: [PATCH] fix(install): isolate installer telemetry failures Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/cli/cli-installer.telemetry.test.ts | 73 +++++++++++++++++++++++++ src/cli/cli-installer.ts | 72 +++++++++++++++++------- 2 files changed, 125 insertions(+), 20 deletions(-) create mode 100644 src/cli/cli-installer.telemetry.test.ts diff --git a/src/cli/cli-installer.telemetry.test.ts b/src/cli/cli-installer.telemetry.test.ts new file mode 100644 index 000000000..c4bdee652 --- /dev/null +++ b/src/cli/cli-installer.telemetry.test.ts @@ -0,0 +1,73 @@ +import { afterEach, describe, expect, it, mock, spyOn } from "bun:test" +import * as configManager from "./config-manager" +import type { InstallArgs } from "./types" + +describe("runCliInstaller telemetry isolation", () => { + afterEach(() => { + mock.restore() + }) + + it("does not crash CLI install when telemetry shutdown throws", async () => { + // given + const restoreSpies = [ + spyOn(configManager, "detectCurrentConfig").mockReturnValue({ + isInstalled: false, + installedVersion: null, + hasClaude: false, + isMax20: false, + hasOpenAI: false, + hasGemini: false, + hasCopilot: false, + hasOpencodeZen: false, + hasZaiCodingPlan: false, + hasKimiForCoding: false, + hasOpencodeGo: false, + }), + spyOn(configManager, "isOpenCodeInstalled").mockResolvedValue(true), + spyOn(configManager, "getOpenCodeVersion").mockResolvedValue("1.4.0"), + spyOn(configManager, "addPluginToOpenCodeConfig").mockResolvedValue({ + success: true, + configPath: "/tmp/opencode.jsonc", + }), + spyOn(configManager, "writeOmoConfig").mockReturnValue({ + success: true, + configPath: "/tmp/oh-my-opencode.jsonc", + }), + ] + + mock.module("../shared/posthog", () => ({ + createCliPostHog: mock(() => ({ + trackActive: mock(() => {}), + capture: mock(() => {}), + captureException: mock(() => {}), + shutdown: mock(async () => { + throw new Error("shutdown failed") + }), + })), + getPostHogDistinctId: mock(() => "install-distinct-id"), + })) + + const { runCliInstaller } = await import(`./cli-installer?telemetry=${Date.now()}-${Math.random()}`) + const args: InstallArgs = { + tui: false, + claude: "no", + openai: "yes", + gemini: "no", + copilot: "yes", + opencodeZen: "no", + zaiCodingPlan: "no", + kimiForCoding: "no", + opencodeGo: "no", + } + + // when + const result = await runCliInstaller(args, "3.4.0") + + // then + expect(result).toBe(0) + + for (const spy of restoreSpies) { + spy.mockRestore() + } + }) +}) diff --git a/src/cli/cli-installer.ts b/src/cli/cli-installer.ts index 865ad649b..5db015531 100644 --- a/src/cli/cli-installer.ts +++ b/src/cli/cli-installer.ts @@ -65,8 +65,16 @@ export async function runCliInstaller(args: InstallArgs, version: string): Promi const unsupportedVersionMessage = getUnsupportedOpenCodeVersionMessage(openCodeVersion) if (unsupportedVersionMessage) { printWarning(unsupportedVersionMessage) - posthog.capture({ distinctId, event: "install_failed", properties: { command: "install", reason: "unsupported_opencode_version", is_update: isUpdate } }) - await posthog.shutdown() + try { + posthog.capture({ distinctId, event: "install_failed", properties: { command: "install", reason: "unsupported_opencode_version", is_update: isUpdate } }) + } catch { + // telemetry failure is non-fatal, silently ignore + } + try { + await posthog.shutdown() + } catch { + // telemetry failure is non-fatal, silently ignore + } return 1 } } @@ -82,8 +90,16 @@ export async function runCliInstaller(args: InstallArgs, version: string): Promi const pluginResult = await addPluginToOpenCodeConfig(version) if (!pluginResult.success) { printError(`Failed: ${pluginResult.error}`) - posthog.capture({ distinctId, event: "install_failed", properties: { command: "install", reason: "plugin_config_write_failed", is_update: isUpdate } }) - await posthog.shutdown() + try { + posthog.capture({ distinctId, event: "install_failed", properties: { command: "install", reason: "plugin_config_write_failed", is_update: isUpdate } }) + } catch { + // telemetry failure is non-fatal, silently ignore + } + try { + await posthog.shutdown() + } catch { + // telemetry failure is non-fatal, silently ignore + } return 1 } printSuccess( @@ -94,8 +110,16 @@ export async function runCliInstaller(args: InstallArgs, version: string): Promi const omoResult = writeOmoConfig(config) if (!omoResult.success) { printError(`Failed: ${omoResult.error}`) - posthog.capture({ distinctId, event: "install_failed", properties: { command: "install", reason: "omo_config_write_failed", is_update: isUpdate } }) - await posthog.shutdown() + try { + posthog.capture({ distinctId, event: "install_failed", properties: { command: "install", reason: "omo_config_write_failed", is_update: isUpdate } }) + } catch { + // telemetry failure is non-fatal, silently ignore + } + try { + await posthog.shutdown() + } catch { + // telemetry failure is non-fatal, silently ignore + } return 1 } printSuccess(`Config written ${SYMBOLS.arrow} ${color.dim(omoResult.configPath)}`) @@ -144,20 +168,28 @@ export async function runCliInstaller(args: InstallArgs, version: string): Promi console.log(color.dim("oMoMoMoMo... Enjoy!")) console.log() - posthog.capture({ - distinctId, - event: "install_completed", - properties: { - command: "install", - is_update: isUpdate, - has_claude: config.hasClaude, - has_openai: config.hasOpenAI, - has_gemini: config.hasGemini, - has_copilot: config.hasCopilot, - has_opencode_zen: config.hasOpencodeZen, - }, - }) - await posthog.shutdown() + try { + posthog.capture({ + distinctId, + event: "install_completed", + properties: { + command: "install", + is_update: isUpdate, + has_claude: config.hasClaude, + has_openai: config.hasOpenAI, + has_gemini: config.hasGemini, + has_copilot: config.hasCopilot, + has_opencode_zen: config.hasOpencodeZen, + }, + }) + } catch { + // telemetry failure is non-fatal, silently ignore + } + try { + await posthog.shutdown() + } catch { + // telemetry failure is non-fatal, silently ignore + } if ((config.hasClaude || config.hasGemini || config.hasCopilot) && !args.skipAuth) { printBox(