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:
parent
f69c10d3a0
commit
89256f1e80
28 changed files with 666 additions and 47 deletions
|
|
@ -18,6 +18,7 @@
|
|||
// (HANGS, DISK_WRITES, LAUNCHES), GET /v1/apps/{id}/betaFeedbackCrashSubmissions (TestFlight crash reports, API 4.0+).
|
||||
import type { Finding, Level } from './validate-rules';
|
||||
import { mdTable, pct, textTable } from './table';
|
||||
import { findingText, type GoogleFinding, parseGoogleError, plainError } from './google-errors';
|
||||
import { reportingEnableUrl } from './store-apps';
|
||||
|
||||
export const PLAY_THRESHOLDS = { crash: 0.0109, anr: 0.0047, perDevice: 0.08 } as const;
|
||||
|
|
@ -115,7 +116,10 @@ export interface AppleHealth {
|
|||
betaCrashes: { build: string; count: number }[];
|
||||
}
|
||||
|
||||
export interface HealthSnapshot { date: string; play?: PlayHealth | { error: string }; apple?: AppleHealth | { error: string } }
|
||||
/** A store that could not be read: `error` is one plain line (never a JSON body), `finding` the typed Google error
|
||||
* (google-errors.ts), `detail` the raw API text for logs and a collapsed "Details". */
|
||||
export interface StoreReadError { error: string; finding?: GoogleFinding; detail?: string }
|
||||
export interface HealthSnapshot { date: string; play?: PlayHealth | StoreReadError; apple?: AppleHealth | StoreReadError }
|
||||
|
||||
/** Last value of a metric over the rows (the 28-day rolling metrics: the latest row is the store's current view). */
|
||||
export function lastValue(rows: MetricRow[], metric: string): number | null {
|
||||
|
|
@ -186,11 +190,13 @@ export function healthFindings(h: HealthSnapshot, o: { unreadable?: Level; princ
|
|||
const f: Finding[] = [];
|
||||
const play = h.play;
|
||||
if (play && 'error' in play) {
|
||||
const on = reportingEnableUrl(play.error, o.principal ?? null);
|
||||
f.push(REPORTING_OFF.test(play.error)
|
||||
? { store: 'play', level: o.unreadable ?? 'warn', code: 'vitals', msg: `vitals not readable: the Google Play Developer Reporting API is off in the service account's Google Cloud project${on.project ? ` ${on.project}` : ''}`,
|
||||
fix: `enable "Google Play Developer Reporting API" for that project: ${on.url} (Enable), wait a few minutes (store-health skill)` }
|
||||
: { store: 'play', level: o.unreadable ?? 'warn', code: 'vitals', msg: `vitals not readable: ${play.error.slice(0, 200)}`, fix: 'store-health skill (Reporting API access)' });
|
||||
const raw = play.detail ?? play.error;
|
||||
const g = play.finding ?? parseGoogleError(raw);
|
||||
const on = reportingEnableUrl(raw, o.principal ?? null);
|
||||
f.push(g?.kind === 'api-disabled' || REPORTING_OFF.test(raw)
|
||||
? { store: 'play', level: o.unreadable ?? 'warn', code: 'vitals', msg: `vitals not readable: the ${g?.kind === 'api-disabled' ? g.serviceTitle : 'Google Play Developer Reporting API'} is off in the service account's Google Cloud project${(g?.kind === 'api-disabled' ? g.projectId : on.project) ? ` ${g?.kind === 'api-disabled' ? g.projectId : on.project}` : ''}`,
|
||||
fix: `enable "${g?.kind === 'api-disabled' ? g.serviceTitle : 'Google Play Developer Reporting API'}" for that project: ${g?.kind === 'api-disabled' ? g.activationUrl : on.url} (Enable), wait a few minutes (store-health skill)` }
|
||||
: { store: 'play', level: o.unreadable ?? 'warn', code: 'vitals', msg: `vitals not readable: ${g ? findingText(g) : plainError(raw).slice(0, 200)}`, fix: g?.kind === 'no-app-access' ? `Play Console > Users and permissions: ${g.usersUrl}` : 'store-health skill (Reporting API access)' });
|
||||
}
|
||||
else if (play) {
|
||||
const crash = lastValue(play.crash, 'userPerceivedCrashRate28dUserWeighted') ?? weightedAverage(play.crash, 'userPerceivedCrashRate');
|
||||
|
|
@ -209,7 +215,7 @@ export function healthFindings(h: HealthSnapshot, o: { unreadable?: Level; princ
|
|||
}
|
||||
}
|
||||
const apple = h.apple;
|
||||
if (apple && 'error' in apple) f.push({ store: 'apple', level: o.unreadable ?? 'warn', code: 'diagnostics', msg: `diagnostics not readable: ${apple.error.slice(0, 200)}` });
|
||||
if (apple && 'error' in apple) f.push({ store: 'apple', level: o.unreadable ?? 'warn', code: 'diagnostics', msg: `diagnostics not readable: ${plainError(apple.error).slice(0, 200)}` });
|
||||
else if (apple) {
|
||||
const poor = apple.metrics.filter(m => m.goal === 'poor');
|
||||
for (const m of poor) f.push({ store: 'apple', level: 'warn', code: 'perf', msg: `${m.metric} ${m.value} ${m.unit} on ${m.version}: Apple rates it poor`, fix: 'Xcode Organizer > Metrics; store-health skill' });
|
||||
|
|
@ -230,12 +236,12 @@ export function healthTable(h: HealthSnapshot): string {
|
|||
rows.push(['play', 'ANR rate (user-perceived, 28 d)', pct(lastValue(p.anr, 'userPerceivedAnrRate28dUserWeighted')), pct(PLAY_THRESHOLDS.anr)]);
|
||||
rows.push(['play', `ANR rate (user-perceived, ${p.window.days} d avg)`, pct(weightedAverage(p.anr, 'userPerceivedAnrRate')), '']);
|
||||
rows.push(['play', 'top issues', p.issuesError ? 'not read' : p.issues.length, '']);
|
||||
} else if (h.play) rows.push(['play', 'error', h.play.error.slice(0, 80), '']);
|
||||
} else if (h.play) rows.push(['play', 'not readable', h.play.finding ? h.play.finding.kind : h.play.error.slice(0, 80), '']);
|
||||
if (h.apple && !('error' in h.apple)) {
|
||||
for (const m of h.apple.metrics.slice(0, 12)) rows.push(['apple', `${m.metric} (${m.version})`, `${m.value} ${m.unit}`.trim(), m.goal ?? '']);
|
||||
rows.push(['apple', 'diagnostic signatures', h.apple.signatures.length, '']);
|
||||
rows.push(['apple', 'TestFlight crash reports', h.apple.betaCrashes.reduce((n, b) => n + b.count, 0), '']);
|
||||
} else if (h.apple) rows.push(['apple', 'error', h.apple.error.slice(0, 80), '']);
|
||||
} else if (h.apple) rows.push(['apple', 'not readable', h.apple.finding ? h.apple.finding.kind : h.apple.error.slice(0, 80), '']);
|
||||
return textTable(['store', 'metric', 'value', 'threshold / goal'], rows);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue