diff --git a/cli/src/app/debug.ts b/cli/src/app/debug.ts index a267e2d39a..4602d1797f 100644 --- a/cli/src/app/debug.ts +++ b/cli/src/app/debug.ts @@ -373,7 +373,7 @@ export async function debugApp(appId: string, options: AppDebugOptions) { } if (!appId) { log.error('Missing argument, you need to provide a appId, or be in a capacitor project') - throw new Error('Missing appId') + throw new CliUserError('Missing appId') } const orgId = await getOrganizationId(options.apikey!, appId, { supaHost: options.supaHost, supaAnon: options.supaAnon }) diff --git a/cli/src/app/delete.ts b/cli/src/app/delete.ts index 134bfab511..74d1156699 100644 --- a/cli/src/app/delete.ts +++ b/cli/src/app/delete.ts @@ -36,7 +36,7 @@ export async function deleteAppInternal( if (!appId) { if (!silent) log.error('Missing argument, you need to provide a appId, or be in a capacitor project') - throw new Error('Missing appId') + throw new CliUserError('Missing appId') } const supabase = await createSupabaseClient(options.apikey, options.supaHost, options.supaAnon) diff --git a/cli/src/app/set.ts b/cli/src/app/set.ts index f5d41c4214..46e050156e 100644 --- a/cli/src/app/set.ts +++ b/cli/src/app/set.ts @@ -6,6 +6,7 @@ import { intro, log, outro } from '@clack/prompts' import { checkAppExistsAndHasPermissionOrgErr, getAppIconStoragePath, resolveAppSetIconPath } from '../api/app' import { assertChannelExists, disableDownloadChannels as disableAllDownloadChannels, setDefaultDownloadChannel } from './default-channels' import { normalizeStoreUrl } from './store-url' +import { CliUserError } from '../shared/cli-user-error' import { createSupabaseClient, findSavedKey, @@ -39,7 +40,7 @@ export async function setAppInternal(appId: string, options: Options, silent = f if (!appId) { if (!silent) log.error('Missing argument, you need to provide a appId, or be in a capacitor project') - throw new Error('Missing appId') + throw new CliUserError('Missing appId') } const supabase = await createSupabaseClient(options.apikey, options.supaHost, options.supaAnon) diff --git a/cli/src/build/needed.ts b/cli/src/build/needed.ts index 3070019131..bcc7250a66 100644 --- a/cli/src/build/needed.ts +++ b/cli/src/build/needed.ts @@ -8,6 +8,7 @@ import { difference, parse } from '@std/semver' import { trackEvent } from '../analytics/track' import { check2FAComplianceForApp, checkAppExistsAndHasPermissionOrgErr } from '../api/app' import { formatTable } from '../terminal-table' +import { CliUserError } from '../shared/cli-user-error' import { checkCompatibilityCloud, createSupabaseClient, @@ -263,7 +264,7 @@ export async function getBuildNeeded( if (!resolvedAppId) { if (configError instanceof Error) throw configError - throw new Error('Missing appId') + throw new CliUserError('Missing appId') } if (!enrichedOptions.apikey) diff --git a/cli/src/build/prescan/context.ts b/cli/src/build/prescan/context.ts index c231877857..aade96c766 100644 --- a/cli/src/build/prescan/context.ts +++ b/cli/src/build/prescan/context.ts @@ -3,6 +3,7 @@ import type { SupabaseClient } from '@supabase/supabase-js' import type { Database } from '../../types/supabase.types' import type { Platform, ScanContext } from './types' import { getConfig } from '../../utils' +import { CliUserError } from '../../shared/cli-user-error' import { mergeCredentials } from '../credentials' import { withCwd } from '../cwd' @@ -28,7 +29,7 @@ export async function buildScanContext(args: BuildScanContextArgs): Promise getConfig(true))).config } catch { config = undefined } // no capacitor project — checks degrade individually const appId = args.appId ?? config?.appId - if (!appId) throw new Error('Missing appId: pass it explicitly or run inside a Capacitor project') + if (!appId) throw new CliUserError('Missing appId: pass it explicitly or run inside a Capacitor project') const credentials = args.credentials ?? (await mergeCredentials(appId, args.platform) as Record | undefined) return { diff --git a/cli/src/bundle/cleanup.ts b/cli/src/bundle/cleanup.ts index 567e67c53c..c3399baedc 100644 --- a/cli/src/bundle/cleanup.ts +++ b/cli/src/bundle/cleanup.ts @@ -82,7 +82,7 @@ export async function cleanupBundleInternal(appId: string, options: BundleCleanu if (!appId) { if (!silent) log.error('Missing argument, you need to provide a appid, or be in a capacitor project') - throw new Error('Missing appId') + throw new CliUserError('Missing appId') } const supabase = await createSupabaseClient(options.apikey, options.supaHost, options.supaAnon) diff --git a/cli/src/bundle/compatibility.ts b/cli/src/bundle/compatibility.ts index 7dbace532b..35cf000e10 100644 --- a/cli/src/bundle/compatibility.ts +++ b/cli/src/bundle/compatibility.ts @@ -4,6 +4,7 @@ import { intro, log } from '@clack/prompts' import { trackEvent } from '../analytics/track' import { check2FAComplianceForApp, checkAppExistsAndHasPermissionOrgErr } from '../api/app' import { formatTable } from '../terminal-table' +import { CliUserError } from '../shared/cli-user-error' import { checkCompatibilityCloud, createSupabaseClient, @@ -54,7 +55,7 @@ export async function checkCompatibilityInternal( if (!resolvedAppId) { if (!silent) log.error('Missing argument, you need to provide an appId, or be in a capacitor project') - throw new Error('Missing appId') + throw new CliUserError('Missing appId') } const supabase = await createSupabaseClient( diff --git a/cli/src/bundle/delete.ts b/cli/src/bundle/delete.ts index 7b04ed6748..27ae66a31f 100644 --- a/cli/src/bundle/delete.ts +++ b/cli/src/bundle/delete.ts @@ -2,6 +2,7 @@ import type { BundleDeleteOptions } from '../schemas/bundle' import { intro, log, outro } from '@clack/prompts' import { check2FAComplianceForApp, checkAppExistsAndHasPermissionOrgErr } from '../api/app' import { deleteSpecificVersion } from '../api/versions' +import { CliUserError } from '../shared/cli-user-error' import { createSupabaseClient, findSavedKey, getAppId, getConfig, getOrganizationId, resolveUserIdFromApiKey, sendEvent } from '../utils' export async function deleteBundleInternal(bundleId: string, appId: string, options: BundleDeleteOptions, silent = false) { @@ -21,7 +22,7 @@ export async function deleteBundleInternal(bundleId: string, appId: string, opti if (!appId) { if (!silent) log.error('Missing argument, you need to provide a appId, or be in a capacitor project') - throw new Error('Missing appId') + throw new CliUserError('Missing appId') } if (!bundleId) { diff --git a/cli/src/bundle/encrypt.ts b/cli/src/bundle/encrypt.ts index 275aaaf8ff..321d217cef 100644 --- a/cli/src/bundle/encrypt.ts +++ b/cli/src/bundle/encrypt.ts @@ -64,7 +64,17 @@ export async function encryptZipInternal( await ensurePublicKeyFromPrivateKey(privateKey, { silent: silent || json, json }) } else if (!userSuppliedPrivateKey) { - await ensurePublicKeyInConfig({ interactive, silent: silent || json, json }) + try { + await ensurePublicKeyInConfig({ interactive, silent: silent || json, json }) + } + catch (error) { + if (!interactive && error instanceof Error + && /missing public key|missing_public_key/i.test(error.message) + && !(error instanceof CliUserError)) { + throw new CliUserError(error.message) + } + throw error + } } extConfig = await getConfigForWrite() } @@ -85,7 +95,7 @@ export async function encryptZipInternal( else log.warning('Warning: Missing Public Key in config') } - throw new Error('Missing public key in config') + throw new CliUserError('Missing public key in config') } if (!privateKey) { diff --git a/cli/src/bundle/list.ts b/cli/src/bundle/list.ts index 48d2419810..4e0f6efd8c 100644 --- a/cli/src/bundle/list.ts +++ b/cli/src/bundle/list.ts @@ -4,6 +4,7 @@ import { trackEvent } from '../analytics/track' import { check2FAComplianceForApp, checkAppExistsAndHasPermissionOrgErr } from '../api/app' import { checkAlerts } from '../api/update' import { displayBundles, getActiveAppVersions } from '../api/versions' +import { CliUserError } from '../shared/cli-user-error' import { createSupabaseClient, findSavedKey, getAppId, getConfig, resolveUserIdFromApiKey } from '../utils' export async function listBundle(appId: string, options: OptionsBase, silent = false) { @@ -24,7 +25,7 @@ export async function listBundle(appId: string, options: OptionsBase, silent = f if (!appId) { if (!silent) log.error('Missing argument, you need to provide a appid, or be in a capacitor project') - throw new Error('Missing appId') + throw new CliUserError('Missing appId') } const supabase = await createSupabaseClient(options.apikey, options.supaHost, options.supaAnon) diff --git a/cli/src/bundle/unlink.ts b/cli/src/bundle/unlink.ts index b124c439c3..0ebac2425e 100644 --- a/cli/src/bundle/unlink.ts +++ b/cli/src/bundle/unlink.ts @@ -2,6 +2,7 @@ import { intro, log, outro } from '@clack/prompts' import { check2FAComplianceForApp, checkAppExistsAndHasPermissionOrgErr } from '../api/app' import { checkVersionNotUsedInChannel } from '../api/channels' import { getVersionData } from '../api/versions' +import { CliUserError } from '../shared/cli-user-error' import { checkPlanValid, createSupabaseClient, @@ -54,7 +55,7 @@ export async function unlinkDeviceInternal( if (!resolvedAppId) { if (!silent) log.error('Missing argument, you need to provide an appId, or be in a capacitor project') - throw new Error('Missing appId') + throw new CliUserError('Missing appId') } if (!bundle) { diff --git a/cli/src/bundle/zip.ts b/cli/src/bundle/zip.ts index 35cfc631c9..2b79a0f889 100644 --- a/cli/src/bundle/zip.ts +++ b/cli/src/bundle/zip.ts @@ -28,6 +28,7 @@ import { resolveUpdaterPackageJsonPath, } from '../recovery/bundle-zip' import { ensureNotifyAppReadyInBuildFolder, buildCiNotifyAppReadyMessage } from '../recovery/notify-app-ready' +import { CliUserError } from '../shared/cli-user-error' import { parsePackageJsonOptionPaths, resolveAppIdWithRecovery } from '../recovery/app-id' import { checkIndexPosition, searchInDirectory } from './check' @@ -44,6 +45,13 @@ function emitJsonError(error: unknown) { console.error(formatError(error)) } +function emitCliUserJsonError(error: CliUserError) { + if (error.context?.message) + emitJsonError({ error: error.message, message: error.context.message }) + else + emitJsonError({ error: error.message }) +} + export async function zipBundleInternal(appId: string, options: BundleZipOptions, silent = false): Promise { const { json } = options let { bundle, path } = options @@ -72,7 +80,7 @@ export async function zipBundleInternal(appId: string, options: BundleZipOptions intro(`Zipping ${resolvedAppId ?? 'app'}@${bundle}`) // Expected setup failures use plain Error (not CliUserError) so PostHog still captures - // real user aborts after declined recovery. notifyAppReady stays a bare Error too. + // real user aborts after declined recovery. if (bundle && !regexSemver.test(bundle)) { if (interactive) { const recoveredBundle = await recoverInvalidSemverBundle(bundle, resolveLocalSemverFallback(uuid)) @@ -116,13 +124,22 @@ export async function zipBundleInternal(appId: string, options: BundleZipOptions if (shouldCheckNotifyAppReady) { if (!searchInDirectory(path, 'notifyAppReady')) { - const recovery = await ensureNotifyAppReadyInBuildFolder({ - webDir: path, - interactive, - json, - }) - if (recovery !== 'skipped' && !searchInDirectory(path, 'notifyAppReady')) { - throw new Error(buildCiNotifyAppReadyMessage(path)) + if (interactive) { + const recovery = await ensureNotifyAppReadyInBuildFolder({ + webDir: path, + interactive, + json, + }) + if (recovery !== 'skipped' && !searchInDirectory(path, 'notifyAppReady')) { + throw new CliUserError('notifyAppReady() is missing in build folder') + } + } + else { + throw json + ? new CliUserError('notifyAppReady_not_in_source_code', { + message: buildCiNotifyAppReadyMessage(path), + }) + : new CliUserError(buildCiNotifyAppReadyMessage(path)) } } @@ -227,10 +244,15 @@ export async function zipBundleInternal(appId: string, options: BundleZipOptions } catch (error) { if (!silent) { - if (json) - emitJsonError(error) - else + if (json) { + if (error instanceof CliUserError) + emitCliUserJsonError(error) + else + emitJsonError(error) + } + else { log.error(formatError(error)) + } } throw error instanceof Error ? error : new Error(String(error)) } diff --git a/cli/src/channel/add.ts b/cli/src/channel/add.ts index 602227f111..6967f5deb9 100644 --- a/cli/src/channel/add.ts +++ b/cli/src/channel/add.ts @@ -6,6 +6,7 @@ import { trackEvent } from '../analytics/track' import { check2FAComplianceForApp, checkAppExistsAndHasPermissionOrgErr } from '../api/app' import { createChannel, findChannel } from '../api/channels' import { isChannelAlreadyExistsError } from '../init/channel-conflict' +import { CliUserError } from '../shared/cli-user-error' import { createSupabaseClient, findSavedKey, @@ -81,7 +82,7 @@ export async function addChannelInternal(channelId: string, appId: string, optio if (!appId) { if (!silent) log.error('Missing argument, you need to provide a appId, or be in a capacitor project') - throw new Error('Missing appId') + throw new CliUserError('Missing appId') } const supabase = await createSupabaseClient(options.apikey, options.supaHost, options.supaAnon, silent) diff --git a/cli/src/channel/delete.ts b/cli/src/channel/delete.ts index 7228c92177..4168ebec2d 100644 --- a/cli/src/channel/delete.ts +++ b/cli/src/channel/delete.ts @@ -3,6 +3,7 @@ import { intro, log, outro } from '@clack/prompts' import { check2FAComplianceForApp, checkAppExistsAndHasPermissionOrgErr } from '../api/app' import { delChannel, findChannel, findVersionsLinkedToChannel, isVersionLinkedToOtherChannel } from '../api/channels' import { deleteAppVersion } from '../api/versions' +import { CliUserError } from '../shared/cli-user-error' import { createSupabaseClient, findSavedKey, formatError, getAppId, getConfig, getOrganizationId, hasCliPermission, invokeCapgoCliApi, sendEvent } from '../utils' export async function deleteChannelInternal(channelId: string, appId: string, options: ChannelDeleteOptions, silent = false) { @@ -22,7 +23,7 @@ export async function deleteChannelInternal(channelId: string, appId: string, op if (!appId) { if (!silent) log.error('Missing argument, you need to provide a appId, or be in a capacitor project') - throw new Error('Missing appId') + throw new CliUserError('Missing appId') } const supabase = await createSupabaseClient(options.apikey, options.supaHost, options.supaAnon) diff --git a/cli/src/channel/list.ts b/cli/src/channel/list.ts index ca9d9b3a81..dd1070f2bc 100644 --- a/cli/src/channel/list.ts +++ b/cli/src/channel/list.ts @@ -2,6 +2,7 @@ import type { OptionsBase } from '../schemas/base' import { intro, log, outro } from '@clack/prompts' import { check2FAComplianceForApp, checkAppExistsAndHasPermissionOrgErr } from '../api/app' import { displayChannels, getActiveChannels } from '../api/channels' +import { CliUserError } from '../shared/cli-user-error' import { createSupabaseClient, findSavedKey, getAppId, getConfig, getOrganizationId, sendEvent } from '../utils' export async function listChannelsInternal(appId: string, options: OptionsBase, silent = false) { @@ -21,7 +22,7 @@ export async function listChannelsInternal(appId: string, options: OptionsBase, if (!appId) { if (!silent) log.error('Missing argument, you need to provide a appId, or be in a capacitor project') - throw new Error('Missing appId') + throw new CliUserError('Missing appId') } const supabase = await createSupabaseClient(options.apikey, options.supaHost, options.supaAnon) diff --git a/cli/src/channel/set.ts b/cli/src/channel/set.ts index 460dfe6301..3e57781fbf 100644 --- a/cli/src/channel/set.ts +++ b/cli/src/channel/set.ts @@ -8,6 +8,7 @@ import { getActiveAppVersions, getVersionData } from '../api/versions' import { sendUpdateNotificationsForChannels } from '../notifications/send-update' import { printPreviewQrForResolvedTarget, resolveChannelPreviewTarget } from '../preview/qr' import { formatTable } from '../terminal-table' +import { CliUserError } from '../shared/cli-user-error' import { channelUpdatePackageCliError, checkCompatibilityNativePackages, createSupabaseClient, findSavedKey, getAppId, getBundleVersion, getCompatibilityDetails, getConfig, getOrganizationId, invokeCapgoCliApi, isCompatible, resolveUserIdFromApiKey, sendEvent } from '../utils' /** @@ -71,7 +72,7 @@ export async function setChannelInternal(channel: string, appId: string, options if (!appId) { if (!silent) log.error('Missing argument, you need to provide a appId, or be in a capacitor project') - throw new Error('Missing appId') + throw new CliUserError('Missing appId') } if (!channel) { diff --git a/cli/src/preview/qr.ts b/cli/src/preview/qr.ts index 29b6ff6288..9f9a6fef86 100644 --- a/cli/src/preview/qr.ts +++ b/cli/src/preview/qr.ts @@ -6,6 +6,7 @@ import { chmod, mkdir } from 'node:fs/promises' import { dirname, resolve } from 'node:path' import QRCode from 'qrcode' import { buildPreviewWebUrl, type PreviewWebEnv } from './web-url' +import { CliUserError } from '../shared/cli-user-error' import { check2FAComplianceForApp, checkAppExistsAndHasPermissionOrgErr } from '../api/app' import { createSupabaseClient, findSavedKey, formatError, getAppId, getCapgoCliHttpStatus, getConfig, invokeCapgoCliApi, readCapgoCliApiErrorPayload } from '../utils' @@ -330,7 +331,7 @@ export async function getPreviewQr(appId: string, target: string | undefined, op if (!appId) { log.error('Missing argument, you need to provide a appId, or be in a capacitor project') - throw new Error('Missing appId') + throw new CliUserError('Missing appId') } const supabase = await createSupabaseClient(options.apikey, options.supaHost, options.supaAnon) diff --git a/cli/test/test-cli-recovery.mjs b/cli/test/test-cli-recovery.mjs index 5f894b6b99..3e9d26c392 100644 --- a/cli/test/test-cli-recovery.mjs +++ b/cli/test/test-cli-recovery.mjs @@ -7,6 +7,7 @@ import { join } from 'node:path' import process from 'node:process' import vm from 'node:vm' import { shouldCapturePosthogException } from '../src/posthog.ts' +import { CliUserError } from '../src/shared/cli-user-error.ts' import { saveKeyInternal } from '../src/key.ts' import { setConfigWriteTarget } from '../src/config/index.ts' import { @@ -322,7 +323,7 @@ await test('zipBundleInternal rejects declared updater missing from node_modules } }) -await test('zipBundleInternal silent notifyAppReady failure stays PostHog-capturable', async () => { +await test('zipBundleInternal silent notifyAppReady failure uses CliUserError', async () => { const root = makeTempDir('zip-silent') const webDir = join(root, 'www') mkdirSync(webDir) @@ -332,8 +333,9 @@ await test('zipBundleInternal silent notifyAppReady failure stays PostHog-captur await assert.rejects( () => zipBundleInternal('com.example.app', { path: webDir, bundle: '1.0.0' }, true), (error) => { + assert.equal(error instanceof CliUserError, true) assert.match(error.message, /notifyAppReady\(\) is missing/) - assert.equal(shouldCapturePosthogException(error), true) + assert.equal(shouldCapturePosthogException(error), false) return true }, ) diff --git a/cli/test/test-cli-user-error-config.mjs b/cli/test/test-cli-user-error-config.mjs index f9a402d3c7..27c72b9295 100644 --- a/cli/test/test-cli-user-error-config.mjs +++ b/cli/test/test-cli-user-error-config.mjs @@ -1,12 +1,17 @@ #!/usr/bin/env node +// Parallel-safe bundle tests use withCwd or absolute paths — never bare process.chdir(). import assert from 'node:assert/strict' -import { mkdtempSync, writeFileSync, rmSync } from 'node:fs' +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { chdir, cwd } from 'node:process' +import { withCwd } from '../src/build/cwd.ts' import { setConfigWriteTarget } from '../src/config/index.ts' +import { encryptZipInternal } from '../src/bundle/encrypt.ts' +import { zipBundleInternal } from '../src/bundle/zip.ts' +import { shouldCapturePosthogException } from '../src/posthog.ts' import { CliUserError } from '../src/shared/cli-user-error.ts' -import { getConfigForWrite, getOrganizationId } from '../src/utils.ts' +import { getAppId, getConfigForWrite, getOrganizationId } from '../src/utils.ts' const NO_CONFIG_MESSAGE = 'No capacitor config file found, run `cap init` first' const ORG_ID_MESSAGE = 'Cannot get organization id for app' @@ -18,13 +23,39 @@ function assertCliUserError(error, message, contextKeys = []) { assert.ok(error.context?.[key] !== undefined, `expected context.${key}`) } +async function test(name, fn) { + try { + await fn() + process.stdout.write(`✓ ${name}\n`) + } + catch (error) { + process.stderr.write(`✗ ${name}\n`) + throw error + } +} + +function makeDir(name) { + // Use /tmp explicitly — os.tmpdir() on GitHub Actions is under the workspace, + // so Capacitor's upward config search would pick up the repo's appId. + const dir = mkdtempSync(`/tmp/capgo-cli-user-error-${name}-`) + return dir +} + +function writeCapacitorProject(dir, webDirName = 'www') { + writeFileSync(join(dir, 'package.json'), JSON.stringify({ name: 'demo', version: '1.0.0' }, null, 2)) + writeFileSync( + join(dir, 'capacitor.config.json'), + JSON.stringify({ appId: 'com.example.app', webDir: webDirName }, null, 2), + ) +} + const brokenConfigDir = mkdtempSync(join(tmpdir(), 'capgo-broken-cap-config-')) const invalidConfigTarget = join(brokenConfigDir, 'capacitor.config.invalid.json') const missingConfigTarget = join(brokenConfigDir, 'capacitor.config.missing.json') const emptyConfigTarget = join(brokenConfigDir, 'capacitor.config.empty.json') writeFileSync(invalidConfigTarget, '{not json') writeFileSync(emptyConfigTarget, '{}') -const previousCwd = cwd() +const configOrgPreviousCwd = cwd() const originalFetch = globalThis.fetch try { @@ -88,7 +119,64 @@ try { } finally { setConfigWriteTarget(undefined) - chdir(previousCwd) + chdir(configOrgPreviousCwd) globalThis.fetch = originalFetch rmSync(brokenConfigDir, { recursive: true, force: true }) } + +await test('zipBundleInternal throws CliUserError when notifyAppReady is missing', async () => { + const dir = makeDir('notify-app-ready') + const webDir = join(dir, 'www') + mkdirSync(webDir) + writeFileSync(join(webDir, 'index.html'), '') + writeFileSync(join(webDir, 'main.js'), 'console.log("hello")') + + try { + await assert.rejects( + () => zipBundleInternal('com.example.app', { path: webDir, bundle: '1.0.0' }, true), + (error) => { + assert.equal(error instanceof CliUserError, true) + assert.match(error.message, /notifyAppReady\(\) is missing in the build folder/) + assert.match(error.message, /notify-app-ready/) + assert.equal(shouldCapturePosthogException(error), false) + return true + }, + ) + } + finally { + rmSync(dir, { recursive: true, force: true }) + } +}) + +await test('encryptZipInternal throws CliUserError when public key is missing from config', async () => { + const dir = makeDir('missing-public-key') + const zipPath = join(dir, 'bundle.zip') + writeFileSync(zipPath, 'zip-placeholder') + writeCapacitorProject(dir) + + try { + await withCwd(dir, async () => { + await assert.rejects( + () => encryptZipInternal(zipPath, 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', {}, true), + (error) => { + assert.equal(error instanceof CliUserError, true) + assert.match(error.message, /Missing public key in config/) + assert.equal(shouldCapturePosthogException(error), false) + return true + }, + ) + }) + } + finally { + rmSync(dir, { recursive: true, force: true }) + } +}) + +await test('missing appId guard throws CliUserError skipped by PostHog', () => { + assert.equal(getAppId('', { webDir: 'www' }), undefined) + assert.equal(getAppId(undefined, undefined), undefined) + + const error = new CliUserError('Missing appId') + assert.equal(error.message, 'Missing appId') + assert.equal(shouldCapturePosthogException(error), false) +}) diff --git a/cli/test/test-posthog-exception.mjs b/cli/test/test-posthog-exception.mjs index 30fefed6ff..7441403ecf 100644 --- a/cli/test/test-posthog-exception.mjs +++ b/cli/test/test-posthog-exception.mjs @@ -285,7 +285,9 @@ try { { appId: 'com.other.app', requiredPermissionKey: 'app.upload_bundle' }, ).message, ) - + assert.equal(shouldCapturePosthogException(new CliUserError('notifyAppReady() is missing in build folder')), false) + assert.equal(shouldCapturePosthogException(new CliUserError('Missing public key in config')), false) + assert.equal(shouldCapturePosthogException(new CliUserError('Missing appId')), false) // 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(