diff --git a/src/index.telemetry.test.ts b/src/index.telemetry.test.ts new file mode 100644 index 000000000..2d554626f --- /dev/null +++ b/src/index.telemetry.test.ts @@ -0,0 +1,129 @@ +import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test" + +const mockInitConfigContext = mock(() => {}) +const mockInjectServerAuthIntoClient = mock(() => {}) +const mockLogLegacyPluginStartupWarning = mock(() => {}) +const mockLoadPluginConfig = mock(() => ({})) +const mockIsTmuxIntegrationEnabled = mock(() => false) +const mockCreateRuntimeTmuxConfig = mock(() => ({ + enabled: false, + layout: "tiled" as const, + main_pane_size: 60, + main_pane_min_width: 80, + agent_pane_min_width: 40, + isolation: "inline" as const, +})) +const mockCreateManagers = mock(() => ({ + backgroundManager: { shutdown: async () => {} }, + skillMcpManager: { disconnectAll: async () => {} }, + configHandler: async () => {}, +})) +const mockCreateTools = mock(async () => ({ + mergedSkills: [], + availableSkills: [], + filteredTools: {}, +})) +const mockCreateHooks = mock(() => ({ + disposeHooks: () => {}, + compactionContextInjector: undefined, + compactionTodoPreserver: undefined, + claudeCodeHooks: undefined, +})) +const mockCreatePluginDispose = mock(() => async () => {}) +const mockCreatePluginInterface = mock(() => ({})) +const mockCreatePluginPostHog = mock(() => ({ + trackActive: () => { + throw new Error("telemetry failed") + }, + capture: mock(() => {}), + captureException: mock(() => {}), + shutdown: mock(async () => {}), +})) +const mockGetPostHogDistinctId = mock(() => "plugin-distinct-id") + +function installModuleMocks(): void { + mock.module("./cli/config-manager/config-context", () => ({ + initConfigContext: mockInitConfigContext, + })) + mock.module("./shared/external-plugin-detector", () => ({ + detectExternalSkillPlugin: mock(() => ({ detected: false, pluginName: null })), + getSkillPluginConflictWarning: mock(() => ""), + })) + mock.module("./shared", () => ({ + injectServerAuthIntoClient: mockInjectServerAuthIntoClient, + log: mock(() => {}), + logLegacyPluginStartupWarning: mockLogLegacyPluginStartupWarning, + })) + mock.module("./plugin-config", () => ({ + loadPluginConfig: mockLoadPluginConfig, + })) + mock.module("./create-runtime-tmux-config", () => ({ + createRuntimeTmuxConfig: mockCreateRuntimeTmuxConfig, + isTmuxIntegrationEnabled: mockIsTmuxIntegrationEnabled, + })) + mock.module("./create-managers", () => ({ + createManagers: mockCreateManagers, + })) + mock.module("./create-tools", () => ({ + createTools: mockCreateTools, + })) + mock.module("./create-hooks", () => ({ + createHooks: mockCreateHooks, + })) + mock.module("./plugin-dispose", () => ({ + createPluginDispose: mockCreatePluginDispose, + })) + mock.module("./plugin-interface", () => ({ + createPluginInterface: mockCreatePluginInterface, + })) + mock.module("./plugin-state", () => ({ + createModelCacheState: mock(() => ({})), + })) + mock.module("./shared/first-message-variant", () => ({ + createFirstMessageVariantGate: mock(() => ({ + shouldOverride: () => false, + markApplied: () => {}, + markSessionCreated: () => {}, + clear: () => {}, + })), + })) + mock.module("./openclaw", () => ({ + initializeOpenClaw: mock(async () => {}), + })) + mock.module("./tools/interactive-bash", () => ({ + interactive_bash: {}, + startBackgroundCheck: mock(() => {}), + })) + mock.module("./tools/lsp/client", () => ({ + lspManager: {}, + })) + mock.module("./shared/posthog", () => ({ + createPluginPostHog: mockCreatePluginPostHog, + getPostHogDistinctId: mockGetPostHogDistinctId, + })) +} + +describe("OhMyOpenCodePlugin telemetry isolation", () => { + beforeEach(() => { + mock.restore() + installModuleMocks() + }) + + afterEach(() => { + mock.restore() + }) + + it("does not crash plugin load when telemetry throws", async () => { + // given + const { default: plugin } = await import(`./index?telemetry=${Date.now()}-${Math.random()}`) + + // when + const result = await plugin({ + directory: "/tmp/project", + client: {}, + } as Parameters[0]) + + // then + expect(result).toMatchObject({ name: "oh-my-openagent" }) + }) +}) diff --git a/src/index.ts b/src/index.ts index 245846298..b36e6c4b9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -41,16 +41,24 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => { const posthog = createPluginPostHog() const distinctId = getPostHogDistinctId() - posthog.trackActive(distinctId, "plugin_loaded") - posthog.capture({ - distinctId, - event: "plugin_loaded", - properties: { - entry_point: "plugin", - has_openclaw: !!pluginConfig.openclaw, - tmux_enabled: isTmuxIntegrationEnabled(pluginConfig), - }, - }) + try { + posthog.trackActive(distinctId, "plugin_loaded") + } catch { + // telemetry failure is non-fatal, silently ignore + } + try { + posthog.capture({ + distinctId, + event: "plugin_loaded", + properties: { + entry_point: "plugin", + has_openclaw: !!pluginConfig.openclaw, + tmux_enabled: isTmuxIntegrationEnabled(pluginConfig), + }, + }) + } catch { + // telemetry failure is non-fatal, silently ignore + } if (pluginConfig.openclaw) { await initializeOpenClaw(pluginConfig.openclaw) }