From 1134250d7e15911f1c15b2b2d04fa638495192a9 Mon Sep 17 00:00:00 2001 From: "posthog-eu[bot]" <226701856+posthog-eu[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2026 03:03:02 +0000 Subject: [PATCH 1/2] fix(cli): classify auth failures by type, not message text Convert the bad-key throw in `resolveUserIdFromApiKey` and the RBAC throw in `checkAppExistsAndHasPermissionOrgErr` from plain `Error` to `CliUserError`, so `shouldCapturePosthogException` skips them by type. A later reword of either message can no longer silently break the error-tracking filter, and the RBAC app id and permission key move into `CliUserError` context so error tracking maps every app to one issue instead of one per app. Generated-By: PostHog Code Task-Id: ab1dbac6-b0af-45ff-980a-8fd68253b5d7 --- cli/src/api/app.ts | 8 +++++--- cli/src/utils.ts | 5 ++++- cli/test/test-posthog-exception.mjs | 11 +++++++++++ 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/cli/src/api/app.ts b/cli/src/api/app.ts index 7d3b6d6174..cd956602b8 100644 --- a/cli/src/api/app.ts +++ b/cli/src/api/app.ts @@ -2,6 +2,7 @@ import type { SupabaseClient } from '@supabase/supabase-js' import type { Database } from '../types/supabase.types' import { log } from '@clack/prompts' import { buildCliRequestHeaders } from '../analytics/cli-headers' +import { CliUserError } from '../shared/cli-user-error' import { appAddHintMessage, formatCapgoApiErrorBody, getCapgoCliHttpStatus, hasCliPermission, invokeCapgoCliApi, isCapgoManagedSupabaseHost, resolveCapgoPublicApiHost, show2FADeniedError } from '../utils' export async function checkAppExists( @@ -217,10 +218,11 @@ export async function checkAppExistsAndHasPermissionOrgErr( } if (!(await hasCliPermission(supabase, apikey, requiredPermissionKey, { appId: appid, channelId: channelId ?? null }))) { - const msg = `Insufficient permissions for app ${appid}. Required RBAC permission for this action: ${requiredPermissionKey}.` if (!silent) - log.error(msg) - throw new Error(msg) + log.error(`Insufficient permissions for app ${appid}. Required RBAC permission for this action: ${requiredPermissionKey}.`) + // Keep the app id and permission key OUT of the CliUserError message: they + // go in context so error tracking does not fingerprint one issue per app. + throw new CliUserError('Insufficient permissions for app. Required RBAC permission for this action.', { appId: appid, requiredPermission: requiredPermissionKey }) } return true diff --git a/cli/src/utils.ts b/cli/src/utils.ts index 45eedfb8dd..5aca2c2fa9 100644 --- a/cli/src/utils.ts +++ b/cli/src/utils.ts @@ -2051,7 +2051,10 @@ export async function resolveUserIdFromApiKey(supabase: SupabaseClient if (!userId) { if (!silent) log.error(`Capgo authentication failed: invalid Capgo API key or insufficient Capgo permissions.`) - throw new Error('Capgo authentication failed: invalid Capgo API key or insufficient Capgo permissions.') + // Throw a CliUserError so error tracking skips this by type: a bad or + // missing API key is an expected user-configuration failure, not a crash. + // Type classification stays true even if this wording changes later. + throw new CliUserError('Capgo authentication failed: invalid Capgo API key or insufficient Capgo permissions.') } return userId } diff --git a/cli/test/test-posthog-exception.mjs b/cli/test/test-posthog-exception.mjs index ed890c0d93..acfc1483d5 100644 --- a/cli/test/test-posthog-exception.mjs +++ b/cli/test/test-posthog-exception.mjs @@ -217,6 +217,17 @@ try { // never opens an error tracking issue. assert.equal(shouldCapturePosthogException(new CliUserError('Login cancelled')), false) assert.equal(shouldCapturePosthogException(new CliUserError('Upload cancelled by user')), false) + // `resolveUserIdFromApiKey` throws a bad-key failure as CliUserError, so a + // later reword of the message can no longer break the filter by substring. + assert.equal(shouldCapturePosthogException(new CliUserError('Capgo authentication failed: invalid Capgo API key or insufficient Capgo permissions.')), false) + // `checkAppExistsAndHasPermissionOrgErr` throws the RBAC failure as + // CliUserError; the app id and permission key live in context, so every app + // maps to one issue instead of one issue per app. + assert.equal(shouldCapturePosthogException(new CliUserError('Insufficient permissions for app. Required RBAC permission for this action.', { appId: 'com.example.app', requiredPermission: 'app.write' })), false) + assert.equal( + new CliUserError('Insufficient permissions for app. Required RBAC permission for this action.', { appId: 'com.a' }).message, + new CliUserError('Insufficient permissions for app. Required RBAC permission for this action.', { appId: 'com.b' }).message, + ) // Two failures on different channels must be treated identically (one issue, // not one per channel), since the channel name lives in context, not the message. assert.equal( From d3735c61c3b597b5f230ccbd671f6995a3e55040 Mon Sep 17 00:00:00 2001 From: "posthog-eu[bot]" <226701856+posthog-eu[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2026 03:20:00 +0000 Subject: [PATCH 2/2] fix(cli): keep RBAC permission key in insufficient-permissions message MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CLI integration test `cli-preview-lifecycle` asserts the thrown error includes the required RBAC permission key. Keep the permission key in the CliUserError message — it is a small bounded enum, so it does not proliferate error-tracking fingerprints — and move only the high-cardinality app id into context. Matches the pattern in cli/src/channel/currentBundle.ts. Generated-By: PostHog Code Task-Id: ab1dbac6-b0af-45ff-980a-8fd68253b5d7 --- cli/src/api/app.ts | 7 ++++--- cli/test/test-posthog-exception.mjs | 11 ++++++----- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/cli/src/api/app.ts b/cli/src/api/app.ts index cd956602b8..5f49b618d4 100644 --- a/cli/src/api/app.ts +++ b/cli/src/api/app.ts @@ -220,9 +220,10 @@ export async function checkAppExistsAndHasPermissionOrgErr( if (!(await hasCliPermission(supabase, apikey, requiredPermissionKey, { appId: appid, channelId: channelId ?? null }))) { if (!silent) log.error(`Insufficient permissions for app ${appid}. Required RBAC permission for this action: ${requiredPermissionKey}.`) - // Keep the app id and permission key OUT of the CliUserError message: they - // go in context so error tracking does not fingerprint one issue per app. - throw new CliUserError('Insufficient permissions for app. Required RBAC permission for this action.', { appId: appid, requiredPermission: requiredPermissionKey }) + // Keep the app id OUT of the CliUserError message so error tracking does not + // fingerprint one issue per app; it goes in context. The permission key is a + // small bounded enum, so it stays in the message (as in currentBundle.ts). + throw new CliUserError(`Insufficient permissions for app. Required RBAC permission for this action: ${requiredPermissionKey}.`, { appId: appid }) } return true diff --git a/cli/test/test-posthog-exception.mjs b/cli/test/test-posthog-exception.mjs index acfc1483d5..4c5b7a83fe 100644 --- a/cli/test/test-posthog-exception.mjs +++ b/cli/test/test-posthog-exception.mjs @@ -221,12 +221,13 @@ try { // later reword of the message can no longer break the filter by substring. assert.equal(shouldCapturePosthogException(new CliUserError('Capgo authentication failed: invalid Capgo API key or insufficient Capgo permissions.')), false) // `checkAppExistsAndHasPermissionOrgErr` throws the RBAC failure as - // CliUserError; the app id and permission key live in context, so every app - // maps to one issue instead of one issue per app. - assert.equal(shouldCapturePosthogException(new CliUserError('Insufficient permissions for app. Required RBAC permission for this action.', { appId: 'com.example.app', requiredPermission: 'app.write' })), false) + // CliUserError; the app id lives in context (the permission key is a bounded + // enum and stays in the message), so every app maps to one issue per + // permission instead of one issue per app. + assert.equal(shouldCapturePosthogException(new CliUserError('Insufficient permissions for app. Required RBAC permission for this action: app.write.', { appId: 'com.example.app' })), false) assert.equal( - new CliUserError('Insufficient permissions for app. Required RBAC permission for this action.', { appId: 'com.a' }).message, - new CliUserError('Insufficient permissions for app. Required RBAC permission for this action.', { appId: 'com.b' }).message, + new CliUserError('Insufficient permissions for app. Required RBAC permission for this action: channel.delete.', { appId: 'com.a' }).message, + new CliUserError('Insufficient permissions for app. Required RBAC permission for this action: channel.delete.', { appId: 'com.b' }).message, ) // Two failures on different channels must be treated identically (one issue, // not one per channel), since the channel name lives in context, not the message.