From db402e1b0d529e810009623c6d8e7e09bb3674a5 Mon Sep 17 00:00:00 2001 From: "posthog-eu[bot]" <226701856+posthog-eu[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2026 02:26:33 +0000 Subject: [PATCH] fix(cli): throw CliUserError for missing package.json Missing package.json was thrown as a plain Error, so shouldCapturePosthogException sent it to error tracking as an unhandled_error. The message interpolated the absolute path, so the fingerprint varied per user and each run could open its own issue. Throw CliUserError at the three missing-package.json sites (readPackageJson, getAllPackagesDependencies, getDeclaredPackageVersionMap) so error tracking skips the expected user error. Keep the thrown message constant and pass the path in context, so the fingerprint stays stable. log.error still prints the full, path-specific guidance to the user. Generated-By: PostHog Code Task-Id: 602f3bee-b522-4c63-8bee-e669432fb8ae --- cli/src/utils.ts | 13 ++++++++++--- cli/test/test-package-json-guard.mjs | 20 +++++++++++++------- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/cli/src/utils.ts b/cli/src/utils.ts index 45eedfb8dd..974b081452 100644 --- a/cli/src/utils.ts +++ b/cli/src/utils.ts @@ -305,7 +305,10 @@ function readPackageJson(f: string = findRoot(cwd()), file: string | undefined = ? `Package.json at ${packageJsonPath} does not exist` : `No package.json found at ${packageJsonPath}. Run this command from your project root (the folder that contains package.json), or pass --package-json to point at it (for example in a monorepo).` log.error(message) - throw new Error(message) + // Expected user error, not a crash: throw a CliUserError so error tracking + // skips it. Keep the thrown message constant (the path goes in context) so + // it fingerprints one issue instead of one per absolute path. + throw new CliUserError('package.json not found. Run this command from your project root, or pass --package-json .', { packageJsonPath }) } const packageJson = readFileSync(packageJsonPath) return JSON.parse(packageJson as any) @@ -498,7 +501,9 @@ export async function getAllPackagesDependencies(f: string = findRoot(cwd()), fi if (!existsSync(file)) { const message = `Package.json at ${file} does not exist` log.error(message) - throw new Error(message) + // Expected user error, not a crash: constant thrown message, path in + // context, so error tracking skips it and does not fingerprint per path. + throw new CliUserError('package.json not found. Run this command from your project root, or pass --package-json .', { packageJsonPath: file }) } } } @@ -573,7 +578,9 @@ export async function getDeclaredPackageVersionMap(f: string = findRoot(cwd()), if (!existsSync(file)) { const message = `Package.json at ${file} does not exist` log.error(message) - throw new Error(message) + // Expected user error, not a crash: constant thrown message, path in + // context, so error tracking skips it and does not fingerprint per path. + throw new CliUserError('package.json not found. Run this command from your project root, or pass --package-json .', { packageJsonPath: file }) } } } diff --git a/cli/test/test-package-json-guard.mjs b/cli/test/test-package-json-guard.mjs index 370efd918a..ae60c0552b 100644 --- a/cli/test/test-package-json-guard.mjs +++ b/cli/test/test-package-json-guard.mjs @@ -23,17 +23,21 @@ function makeDir(name) { // Regression: running `capgo init` (and doctor) from a directory without a // package.json used to escape as a raw Node ENOENT stack trace. readPackageJson -// must now guard the default path and surface an actionable message. -await test('getBundleVersion throws a helpful message when package.json is missing', async () => { +// must now guard the default path and throw a CliUserError with a constant, +// actionable message, so error tracking skips it and does not open one issue +// per absolute path. +await test('getBundleVersion throws a CliUserError when package.json is missing', async () => { const dir = makeDir('missing-pkg') try { assert.throws( () => getBundleVersion(dir), (error) => { assert.ok(!(error instanceof Error && 'code' in error && error.code === 'ENOENT'), 'should not surface a raw ENOENT') - assert.match(error.message, /No package\.json found at/) - assert.match(error.message, /project root/) - assert.match(error.message, /--package-json/) + assert.equal(error.name, 'CliUserError') + // Constant, actionable message (no interpolated path) so error tracking + // fingerprints one issue; the path travels in context instead. + assert.equal(error.message, 'package.json not found. Run this command from your project root, or pass --package-json .') + assert.match(error.context.packageJsonPath, /package\.json$/) return true }, ) @@ -43,14 +47,16 @@ await test('getBundleVersion throws a helpful message when package.json is missi } }) -await test('getPackageScripts throws the explicit-path message when a supplied package.json is missing', async () => { +await test('getPackageScripts throws a CliUserError with the path in context for an explicit package.json', async () => { const dir = makeDir('missing-explicit-pkg') try { const missing = join(dir, 'package.json') assert.throws( () => getPackageScripts(undefined, missing), (error) => { - assert.match(error.message, /Package\.json at .*package\.json does not exist/) + assert.equal(error.name, 'CliUserError') + assert.equal(error.message, 'package.json not found. Run this command from your project root, or pass --package-json .') + assert.equal(error.context.packageJsonPath, missing) return true }, )