Google API errors as typed findings, never raw JSON: cli/lib/google-errors.ts parses SERVICE_DISABLED (api-disabled: service, Cloud project, console page), PERMISSION_DENIED (no-app-access: which app, Users and permissions), 401 / invalid_grant (auth-revoked) and 429 (quota); health / analytics print one line with the URL, raw text in detail and the log; desktop Health tab: the line, 'Turn it on in Google Cloud' and 'Check again', raw text in a collapsed Details; Portfolio shows a Cloud project blocking several apps once. Findings exit codes are results: the sidecar reports outcome (ok / findings / declined / failed) and the command's own error line; scan / check / validate / health exit 1 with findings show and cache the findings; no bare 'Exit code N' anywhere. tests/google-errors.test.ts, boot check errors cases (api-disabled, scan-findings, scan-failed) (pitfall 57)

This commit is contained in:
CRTkafa 2026-10-13 23:20:04 +08:00
parent f69c10d3a0
commit 89256f1e80
28 changed files with 666 additions and 47 deletions

View file

@ -17,6 +17,7 @@ import {
issuesQuery, latestDailyEnd, metricQuery, parseIssues, parseMetricRows, parsePerfPower, type PlayHealth, ymd, ymdString,
} from './lib/health';
import { done, log, printJson, warn } from './lib/log';
import { plainError, storeReadError } from './lib/google-errors';
import { formatFindings } from './lib/validate-rules';
import { googleClient, REPORTING_API } from './play/google';
@ -35,7 +36,7 @@ export async function collectPlayHealth(root: string, pkg: string, days: number)
// the top issues are extra: a failure there must not hide the crash and ANR rates already read
let issues: PlayHealth['issues'] = [], issuesError: string | undefined;
try { issues = parseIssues(await g.json('GET', `${app}/errorIssues:search?${issuesQuery(end, days)}`)); } catch (e) {
issuesError = (e as Error).message;
issuesError = plainError((e as Error).message, { app: pkg });
warn('health', `play: top issues not read: ${issuesError.slice(0, 200)}`);
}
const start = ymd(new Date(Date.UTC(end.year, end.month - 1, end.day)), days);
@ -69,10 +70,10 @@ export async function collectHealth(ctx: Ctx, o: { apple: boolean; play: boolean
const { config, root } = ctx.loaded;
const h: HealthSnapshot = { date: new Date().toISOString().slice(0, 10) };
if (o.play && config.play) {
try { h.play = await collectPlayHealth(root, config.play.packageName, o.days); } catch (e) { h.play = { error: (e as Error).message }; }
try { h.play = await collectPlayHealth(root, config.play.packageName, o.days); } catch (e) { h.play = storeReadError((e as Error).message, { app: config.play.packageName }); }
}
if (o.apple && config.apple) {
try { h.apple = await collectAppleHealth(root, config.apple.bundleId, config.apple.appId, o.days); } catch (e) { h.apple = { error: (e as Error).message }; }
try { h.apple = await collectAppleHealth(root, config.apple.bundleId, config.apple.appId, o.days); } catch (e) { h.apple = storeReadError((e as Error).message); }
}
return h;
}
@ -93,6 +94,7 @@ export async function run(ctx: Ctx): Promise<void> {
const h = await collectHealth(ctx, { apple: both || a.has('apple'), play: both || a.has('play'), days });
if (a.has('json')) { printJson(h); return; }
console.log(healthTable(h));
for (const [s, x] of [['Google Play', h.play], ['App Store', h.apple]] as const) if (x && 'error' in x) console.log(`\n${s}: ${x.error}`);
console.log('\n' + formatFindings(healthFindings(h)));
if (weekly) {
const dir = ctx.loaded.resolve(a.get('out') ?? '.storebound/health');