From 0da4c44a44983576c2867d679a357e62be03e13d Mon Sep 17 00:00:00 2001 From: "posthog[bot]" <206114724+posthog[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 19:46:13 +0000 Subject: [PATCH] fix(mcp): classify Claude Code plugin install errors as soft failures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Known user-environment failures from `claude plugin install posthog` (malformed ~/.claude/settings.json, missing PostHog marketplace, or a too-old Claude Code CLI) are now surfaced to the user via a warning log instead of being captured as wizard exceptions. Only genuinely unexpected failures still go to error tracking. This stops the install-flow path from dominating wizard error tracking with noise the wizard cannot fix — the four "Invalid JSON syntax in settings file" reports surfaced over the last two weeks all came through this single captureException call. Generated-By: PostHog Code Task-Id: 5280ec8e-0964-45e5-8981-aa8b53a3fd5a --- .../clients/__tests__/claude-code.test.ts | 63 +++++++++++++++++++ .../clients/claude-code.ts | 43 ++++++++++++- 2 files changed, 105 insertions(+), 1 deletion(-) diff --git a/src/steps/add-mcp-server-to-clients/clients/__tests__/claude-code.test.ts b/src/steps/add-mcp-server-to-clients/clients/__tests__/claude-code.test.ts index 1aa2e3b2..76fef399 100644 --- a/src/steps/add-mcp-server-to-clients/clients/__tests__/claude-code.test.ts +++ b/src/steps/add-mcp-server-to-clients/clients/__tests__/claude-code.test.ts @@ -14,6 +14,20 @@ jest.mock('../../../../utils/analytics', () => ({ jest.mock('../../../../utils/debug', () => ({ debug: jest.fn(), + logToFile: jest.fn(), +})); + +const warnMock = jest.fn(); +jest.mock('../../../../ui', () => ({ + getUI: () => ({ + log: { + info: jest.fn(), + warn: warnMock, + error: jest.fn(), + success: jest.fn(), + step: jest.fn(), + }, + }), })); describe('ClaudeCodeMCPClient — plugin methods', () => { @@ -129,6 +143,55 @@ describe('ClaudeCodeMCPClient — plugin methods', () => { ); }); + it('returns failure without captureException when settings.json is malformed', async () => { + execSyncMock.mockImplementation((cmd: string) => { + if (String(cmd).includes('plugin install')) { + throw new Error( + '✘ Failed to install plugin "posthog": Failed to update settings: Invalid JSON syntax in settings file at /home/u/.claude/settings.json', + ); + } + return Buffer.from(''); + }); + const client = new ClaudeCodeMCPClient(); + await expect(client.installPlugin()).resolves.toEqual({ success: false }); + expect(analytics.captureException).not.toHaveBeenCalled(); + expect(warnMock).toHaveBeenCalledWith( + expect.stringContaining('settings.json'), + ); + }); + + it('returns failure without captureException when marketplace is missing', async () => { + execSyncMock.mockImplementation((cmd: string) => { + if (String(cmd).includes('plugin install')) { + throw new Error( + 'Plugin posthog not found in any configured marketplace', + ); + } + return Buffer.from(''); + }); + const client = new ClaudeCodeMCPClient(); + await expect(client.installPlugin()).resolves.toEqual({ success: false }); + expect(analytics.captureException).not.toHaveBeenCalled(); + expect(warnMock).toHaveBeenCalledWith( + expect.stringContaining('marketplace'), + ); + }); + + it('returns failure without captureException when Claude Code is too old', async () => { + execSyncMock.mockImplementation((cmd: string) => { + if (String(cmd).includes('plugin install')) { + throw new Error("unknown command 'plugin'"); + } + return Buffer.from(''); + }); + const client = new ClaudeCodeMCPClient(); + await expect(client.installPlugin()).resolves.toEqual({ success: false }); + expect(analytics.captureException).not.toHaveBeenCalled(); + expect(warnMock).toHaveBeenCalledWith( + expect.stringContaining('Upgrade Claude Code'), + ); + }); + it('returns failure when no binary is found', async () => { execSyncMock.mockImplementation(() => { throw new Error('not found'); diff --git a/src/steps/add-mcp-server-to-clients/clients/claude-code.ts b/src/steps/add-mcp-server-to-clients/clients/claude-code.ts index 19c88b92..747c63a2 100644 --- a/src/steps/add-mcp-server-to-clients/clients/claude-code.ts +++ b/src/steps/add-mcp-server-to-clients/clients/claude-code.ts @@ -4,11 +4,46 @@ import { PluginCapable, PluginInstallResult } from '../plugin-client'; import { z } from 'zod'; import { execSync } from 'child_process'; import { analytics } from '../../../utils/analytics'; -import { debug } from '../../../utils/debug'; +import { debug, logToFile } from '../../../utils/debug'; +import { getUI } from '../../../ui'; import * as os from 'os'; import * as path from 'path'; import * as fs from 'fs'; +// Stderr signatures that indicate the user's local Claude Code environment is +// the problem, not the wizard. These shouldn't pollute error tracking — we +// surface them to the user and move on. +const USER_ENV_PLUGIN_INSTALL_ERRORS: Array<{ + pattern: RegExp; + userMessage: string; +}> = [ + { + pattern: /Invalid JSON syntax in settings file/i, + userMessage: + 'Your Claude Code settings.json has invalid JSON. Fix the syntax there and re-run the wizard to install the plugin.', + }, + { + pattern: /not found in any configured marketplace/i, + userMessage: + 'The PostHog marketplace is not configured in your Claude Code install, so the plugin could not be installed.', + }, + { + pattern: + /unknown command|unrecognized command|not a claude command|invalid (sub)?command/i, + userMessage: + 'Your Claude Code CLI is too old to support `plugin install`. Upgrade Claude Code and re-run the wizard to install the plugin.', + }, +]; + +function classifyPluginInstallError( + msg: string, +): { userMessage: string } | null { + for (const { pattern, userMessage } of USER_ENV_PLUGIN_INSTALL_ERRORS) { + if (pattern.test(msg)) return { userMessage }; + } + return null; +} + export const ClaudeCodeMCPConfig = DefaultMCPClientConfig; export type ClaudeCodeMCPConfig = z.infer; @@ -151,6 +186,12 @@ export class ClaudeCodeMCPClient if (msg.includes('already installed') || msg.includes('already exists')) { return Promise.resolve({ success: true, alreadyInstalled: true }); } + const classified = classifyPluginInstallError(msg); + if (classified) { + getUI().log.warn(classified.userMessage); + logToFile(`[ClaudeCodeMCPClient] plugin install soft-failed: ${msg}`); + return Promise.resolve({ success: false }); + } analytics.captureException( new Error(`Claude Code plugin install failed: ${msg}`), );