desktop reads everything by itself: one background read scheduler (4 per store, starts spaced per account, App Store Connect X-Rate-Limit slows it below 20 % and pauses at 100 left, backoff with jitter on 429 / RESOURCE_EXHAUSTED, visible app first, one read per key), per-method TTLs over the saved reads, Settings auto-read (on) and interval (15 min, paused while hidden), Refresh all, a refresh icon and 'read 3 min ago' / 'couldn't refresh' per card, writes invalidate, status line with progress and quota; quota reaches the window through ToolResult.quota (cli/lib/quota.ts); Health top issues open with versions, sample stack (health --issue / --signature, read health_issue), Open in Play Console, Copy for AI and likely-cause hints (cli/lib/crash-hints.ts); tests/scheduler.test.ts, tests/quota-crash-hints.test.ts, boot check auto-read case
This commit is contained in:
parent
89256f1e80
commit
0fb4ce0001
46 changed files with 1531 additions and 141 deletions
|
|
@ -14,7 +14,7 @@ import type { Ctx } from './lib/ctx';
|
|||
import { REPORTING_SCOPE } from './lib/google-auth';
|
||||
import {
|
||||
type AppleHealth, ANR_METRICS, betaCrashCounts, CRASH_METRICS, deviceBreakdown, healthFindings, healthMarkdown, type HealthSnapshot, healthTable,
|
||||
issuesQuery, latestDailyEnd, metricQuery, parseIssues, parseMetricRows, parsePerfPower, type PlayHealth, ymd, ymdString,
|
||||
type IssueSample, issuesQuery, latestDailyEnd, metricQuery, parseDiagnosticLogs, parseErrorReports, parseIssues, parseMetricRows, parsePerfPower, type PlayHealth, reportsQuery, ymd, ymdString,
|
||||
} from './lib/health';
|
||||
import { done, log, printJson, warn } from './lib/log';
|
||||
import { plainError, storeReadError } from './lib/google-errors';
|
||||
|
|
@ -55,7 +55,7 @@ export async function collectAppleHealth(root: string, bundleId: string, appIdHi
|
|||
for (const b of builds) {
|
||||
log('health', `apple: diagnostic signatures of build ${b.attributes.version}`);
|
||||
for (const s of ((await c.asc('GET', `/v1/builds/${b.id}/diagnosticSignatures?limit=10`).catch(none({ data: [] }))).data ?? []) as J[])
|
||||
signatures.push({ build: b.attributes.version, type: s.attributes.diagnosticType, signature: s.attributes.signature ?? '', weight: Number(s.attributes.weight ?? 0) });
|
||||
signatures.push({ build: b.attributes.version, type: s.attributes.diagnosticType, signature: s.attributes.signature ?? '', weight: Number(s.attributes.weight ?? 0), ...(s.id ? { id: String(s.id) } : {}) });
|
||||
}
|
||||
signatures.sort((a, b) => b.weight - a.weight);
|
||||
log('health', 'apple: TestFlight crash reports');
|
||||
|
|
@ -65,6 +65,24 @@ export async function collectAppleHealth(root: string, bundleId: string, appIdHi
|
|||
return { metrics, signatures: signatures.slice(0, 15), betaCrashes: betaCrashCounts(subs, buildNo, since) };
|
||||
}
|
||||
|
||||
/** One sample report of a Play error issue (`--issue <id>`, errorReports:search over the last `days`). */
|
||||
export async function playIssueSample(root: string, pkg: string, issueId: string, days: number): Promise<IssueSample | null> {
|
||||
const g = googleClient(root, REPORTING_SCOPE);
|
||||
const app = `${REPORTING_API}/apps/${encodeURIComponent(pkg)}`;
|
||||
const set = await g.json<Parameters<typeof latestDailyEnd>[0]>('GET', `${app}/crashRateMetricSet`);
|
||||
const end = latestDailyEnd(set) ?? ymd(new Date(), 2);
|
||||
log('health', `play: a sample report of issue ${issueId}`);
|
||||
return parseErrorReports(await g.json('GET', `${app}/errorReports:search?${reportsQuery(end, days, issueId)}`));
|
||||
}
|
||||
|
||||
/** The logs of one App Store diagnostic signature (`--signature <id>`). */
|
||||
export async function appleSignatureSample(root: string, bundleId: string, appIdHint: string | undefined, id: string): Promise<IssueSample | null> {
|
||||
if (!/^[A-Za-z0-9_-]{1,200}$/.test(id)) throw new Error('--signature: a diagnostic signature id');
|
||||
const c = await ascClient(root, bundleId, appIdHint);
|
||||
log('health', `apple: logs of diagnostic signature ${id}`);
|
||||
return parseDiagnosticLogs(await c.getAs(`/v1/diagnosticSignatures/${encodeURIComponent(id)}/logs?limit=1`, 'application/vnd.apple.diagnostic-logs+json'));
|
||||
}
|
||||
|
||||
/** Both stores (or the chosen one); a store that cannot be read becomes { error } instead of failing the run. */
|
||||
export async function collectHealth(ctx: Ctx, o: { apple: boolean; play: boolean; days: number }): Promise<HealthSnapshot> {
|
||||
const { config, root } = ctx.loaded;
|
||||
|
|
@ -91,10 +109,23 @@ export async function run(ctx: Ctx): Promise<void> {
|
|||
const weekly = a.has('weekly');
|
||||
const days = a.num('days', 7);
|
||||
if (!(days >= 1 && days <= 90)) throw new Error('--days: 1-90');
|
||||
const issue = a.get('issue'), signature = a.get('signature');
|
||||
if (issue || signature) {
|
||||
// one issue's sample stack (the desktop's Health detail): --issue <Play error issue id> or --signature <App Store id>
|
||||
const { config, root } = ctx.loaded;
|
||||
if (issue && !config.play) throw new Error('--issue: the config has no play section');
|
||||
if (signature && !config.apple) throw new Error('--signature: the config has no apple section');
|
||||
const sample = issue ? await playIssueSample(root, config.play!.packageName, issue, Math.max(days, 28)) : await appleSignatureSample(root, config.apple!.bundleId, config.apple!.appId, signature!);
|
||||
if (a.has('json')) { printJson({ sample }); return; }
|
||||
console.log(sample ? [`${sample.store} ${sample.type ?? ''} ${sample.version ?? ''} ${sample.device ?? ''} ${sample.os ?? ''}`.replace(/\s+/g, ' ').trim(), ...sample.stack].join('\n') : 'no sample report in the window');
|
||||
return;
|
||||
}
|
||||
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}`);
|
||||
const play = h.play && !('error' in h.play) ? h.play : null;
|
||||
for (const i of play?.issues.filter(x => x.hint) ?? []) console.log(`\nlikely cause of ${i.cause.slice(0, 80)}: ${i.hint!.cause}\nfix: ${i.hint!.fix}`);
|
||||
console.log('\n' + formatFindings(healthFindings(h)));
|
||||
if (weekly) {
|
||||
const dir = ctx.loaded.resolve(a.get('out') ?? '.storebound/health');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue