// Small client for the other Google APIs storebound reads with the same Play service account: the Play Developer // Reporting API (vitals) and Cloud Storage (Play statistics exports). Read-only callers; retries 429 / 5xx. import { playServiceAccount } from '../lib/credentials'; import { tokenSource } from '../lib/google-auth'; import { errorBody } from '../lib/http'; import * as rt from '../lib/rt'; export const REPORTING_API = 'https://playdeveloperreporting.googleapis.com/v1beta1'; export function googleClient(root: string, scope: string) { const token = tokenSource(playServiceAccount(root), scope); async function send(method: string, url: string, body?: unknown): Promise { for (let attempt = 0; ; attempt++) { const r = await fetch(url, { method, headers: { Authorization: 'Bearer ' + (await token()), ...(body === undefined ? {} : { 'Content-Type': 'application/json' }) }, body: body === undefined ? undefined : JSON.stringify(body) }); if ((r.status === 429 || r.status >= 500) && attempt < 4) { await r.body?.cancel(); await rt.sleep(2000 * (attempt + 1)); continue; } return r; } } return { async json(method: string, url: string, body?: unknown): Promise { const r = await send(method, url, body); const t = await r.text(); // path without the query: a long query string (errorIssues:search) pushed the API's reason out of tables if (!r.ok) throw new Error(`${method} ${url.replace(/^https:\/\/[^/]+/, '').replace(/\?.*$/, '')} -> ${r.status} ${errorBody(t, 800)}`); return (t ? JSON.parse(t) : {}) as T; }, async bytes(url: string): Promise { const r = await send('GET', url); if (!r.ok) throw new Error(`GET ${url.replace(/^https:\/\/[^/]+/, '').replace(/\?.*$/, '')} -> ${r.status} ${errorBody(await r.text(), 300)}`); return new Uint8Array(await r.arrayBuffer()); }, }; }