Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 ` +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down
53 changes: 45 additions & 8 deletions src/steps/install-cli-steering/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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 <dir>` and `npm error command <cmd>` lines that carry
* the user's name and home directory, so drop those lines and redact any home
* directory that remains.
*/
function sanitizeNpmFailure(raw: string): string {
const home = os.homedir();
const cleaned = raw
.split('\n')
.filter((line) => {
const normalized = line.trim().toLowerCase();
return !/^npm (error|err!) (path|command)\b/.test(normalized);
})
.join('\n');
return (home ? cleaned.split(home).join('~') : cleaned).trim();
}

/**
* Install or update the PostHog CLI in the user's environment. `npm install
* --global @posthog/cli@latest` covers both first-time installs and upgrades
Expand All @@ -101,16 +132,22 @@ export function installOrUpdatePostHogCli(): CliInstallResult {
};
}
if (result.status !== 0) {
const detail = (result.stderr || result.stdout || '').trim();
const message =
detail ||
`npm install --global @posthog/cli@latest exited with status ${
result.status ?? 'unknown'
}`;
const detail = sanitizeNpmFailure(result.stderr || result.stdout || '');
// A quieted npm (e.g. `--loglevel=silent` from .npmrc) can exit non-zero
// with no output. The exit status or terminating signal is then the only
// diagnostic left, so fall back to it instead of a bare constant. Neither
// is personal data, and the stable `errorObject` message still groups these
// failures into one issue regardless of the fallback text.
const exitDetail = result.signal
? `npm install --global @posthog/cli@latest terminated by signal ${result.signal}`
: `npm install --global @posthog/cli@latest exited with status ${
result.status ?? 'unknown'
}`;
return {
success: false,
error: message,
errorObject: new Error(message),
error: detail || exitDetail,
errorObject: new Error(NPM_INSTALL_FAILED_MESSAGE),
detail: detail.slice(0, NPM_FAILURE_DETAIL_LIMIT) || exitDetail,
};
}
return { success: true };
Expand Down
Loading