diff --git a/src/lib/programs/error-tracking-upload-source-maps/index.ts b/src/lib/programs/error-tracking-upload-source-maps/index.ts index 80db004f5..e4fb26f4a 100644 --- a/src/lib/programs/error-tracking-upload-source-maps/index.ts +++ b/src/lib/programs/error-tracking-upload-source-maps/index.ts @@ -41,7 +41,7 @@ function ensurePostHogCli(variant: SkillVariant): void { analytics.captureException( result.errorObject ?? new Error(`posthog-cli pre-install failed: ${result.error}`), - { source: 'source_maps_cli_preinstall', variant }, + { source: 'source_maps_cli_preinstall', variant, detail: result.detail }, ); getUI().log.warn( `Could not pre-install posthog-cli (${result.error}). Your release build ` + diff --git a/src/steps/install-cli-steering/__tests__/install-cli-steering.test.ts b/src/steps/install-cli-steering/__tests__/install-cli-steering.test.ts index 19a0b9c8f..ee72eb6a3 100644 --- a/src/steps/install-cli-steering/__tests__/install-cli-steering.test.ts +++ b/src/steps/install-cli-steering/__tests__/install-cli-steering.test.ts @@ -82,6 +82,80 @@ describe('install-cli-steering', () => { expect(result.error).toContain('install failed'); }); + it('gives every npm failure a stable exception message so they group', () => { + spawnSyncMock.mockReturnValue({ + status: 1, + stdout: '', + stderr: `npm error path ${os.homedir()}\\AppData\\Roaming\\npm\n`, + }); + + const result = installOrUpdatePostHogCli(); + expect(result.errorObject?.message).toBe( + 'npm install --global @posthog/cli@latest failed', + ); + }); + + it('drops personal data from the reported detail', () => { + const home = os.homedir(); + spawnSyncMock.mockReturnValue({ + status: 1, + stdout: '', + stderr: [ + 'npm error code E404', + 'npm error 404 Not Found - GET https://registry.npmjs.org/@posthog/cli', + `npm error path ${home}\\AppData\\Roaming\\npm`, + `npm error command ${home}\\node.exe install`, + `npm error A complete log is in ${home}\\npm-cache\\log`, + ].join('\n'), + }); + + const result = installOrUpdatePostHogCli(); + expect(result.detail).not.toContain(home); + expect(result.detail).not.toContain('npm error path'); + expect(result.detail).not.toContain('npm error command'); + // Keeps the diagnosable bits: HTTP status and the failing registry URL. + expect(result.detail).toContain('E404'); + expect(result.detail).toContain('registry.npmjs.org'); + expect(result.error).not.toContain(home); + }); + + it('keeps the exit status when npm fails with no output', () => { + spawnSyncMock.mockReturnValue({ + status: 1, + signal: null, + stdout: '', + stderr: '', + }); + + const result = installOrUpdatePostHogCli(); + expect(result.success).toBe(false); + // Grouping message stays stable... + expect(result.errorObject?.message).toBe( + 'npm install --global @posthog/cli@latest failed', + ); + // ...but the exit status survives in the error and detail so a silent + // failure is still distinguishable. + expect(result.error).toContain('exited with status 1'); + expect(result.detail).toContain('exited with status 1'); + }); + + it('keeps the terminating signal when npm is killed with no output', () => { + spawnSyncMock.mockReturnValue({ + status: null, + signal: 'SIGKILL', + stdout: '', + stderr: '', + }); + + const result = installOrUpdatePostHogCli(); + expect(result.success).toBe(false); + expect(result.errorObject?.message).toBe( + 'npm install --global @posthog/cli@latest failed', + ); + expect(result.error).toContain('terminated by signal SIGKILL'); + expect(result.detail).toContain('terminated by signal SIGKILL'); + }); + it('explains when npm itself cannot be run', () => { spawnSyncMock.mockReturnValue({ error: new Error('spawn npm ENOENT'), diff --git a/src/steps/install-cli-steering/index.ts b/src/steps/install-cli-steering/index.ts index 45e133219..12d7451e6 100644 --- a/src/steps/install-cli-steering/index.ts +++ b/src/steps/install-cli-steering/index.ts @@ -73,6 +73,12 @@ export interface CliInstallResult { * false. */ errorObject?: Error; + /** + * Sanitized npm output for callers to attach as an exception property. Keeps + * the HTTP status and CLI version out of the grouping message while the + * message itself stays stable. Truncated and free of personal data. + */ + detail?: string; } const spawnOptions = { @@ -82,6 +88,31 @@ const spawnOptions = { shell: process.platform === 'win32', }; +/** Stable message so npm install failures group into one error-tracking issue. */ +const NPM_INSTALL_FAILED_MESSAGE = + 'npm install --global @posthog/cli@latest failed'; + +/** Cap on the sanitized npm output stored as an exception property. */ +const NPM_FAILURE_DETAIL_LIMIT = 1000; + +/** + * Strip personal data from npm's output before it reaches error tracking. npm + * prints `npm error path