analytics and reports for a collector: store-only configs, --json snapshot, App Usage and Play install stock numbers, per-day sales ranges for both stores
This commit is contained in:
parent
b59665c661
commit
5833776a4b
15 changed files with 651 additions and 60 deletions
|
|
@ -20,7 +20,7 @@
|
|||
// The demo account for App Review can also come from env: REVIEW_DEMO_USER, REVIEW_DEMO_PASSWORD.
|
||||
// 4. the default profile (none named): store keys only
|
||||
// 5. ASC_P8 only: ~/.appstoreconnect/private_keys/AuthKey_<ASC_KEY_ID>.p8 (where Xcode / altool keep it)
|
||||
// A profile file: { "ASC_KEY_ID", "ASC_ISSUER_ID", "ASC_P8", "PLAY_SERVICE_ACCOUNT", "ASC_VENDOR_NUMBER", "label"? };
|
||||
// A profile file: { "ASC_KEY_ID", "ASC_ISSUER_ID", "ASC_P8", "PLAY_SERVICE_ACCOUNT", "ASC_VENDOR_NUMBER", "reportsBucket"?, "label"? };
|
||||
// with a credential broker (cli/lib/broker.ts) Play needs no key file: "PLAY_SERVICE_ACCOUNT_EMAIL" names the account
|
||||
// the broker mints tokens for, and "broker": false keeps a profile on its local key.
|
||||
// key paths relative to the credentials folder (`accounts import` puts the key files in <name>/, mode 0600), or env
|
||||
|
|
@ -46,7 +46,7 @@ function fileStore(root: string): Store {
|
|||
export function credentialValue(root: string, name: string): string | undefined { return value(root, name); }
|
||||
/** The store credential names a profile holds (the same as the env names): per developer account, so the vendor number
|
||||
* for reports too; an app's Android upload keystore stays in the project. */
|
||||
export const PROFILE_KEYS = ['PLAY_SERVICE_ACCOUNT', 'ASC_KEY_ID', 'ASC_ISSUER_ID', 'ASC_P8', 'ASC_VENDOR_NUMBER', 'PLAY_DEVELOPER_ID', 'PLAY_SERVICE_ACCOUNT_EMAIL'] as const;
|
||||
export const PROFILE_KEYS = ['PLAY_SERVICE_ACCOUNT', 'ASC_KEY_ID', 'ASC_ISSUER_ID', 'ASC_P8', 'ASC_VENDOR_NUMBER', 'PLAY_DEVELOPER_ID', 'PLAY_SERVICE_ACCOUNT_EMAIL', 'PLAY_REPORTS_BUCKET'] as const;
|
||||
export const PROFILE_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
||||
|
||||
export const profileDir = (): string => process.env.STOREBOUND_CREDENTIALS_DIR || join(homedir(), '.storebound', 'credentials');
|
||||
|
|
@ -66,7 +66,9 @@ export function profileEnv(profile: string): Record<string, string> {
|
|||
const out: Record<string, string> = {};
|
||||
for (const k of PROFILE_KEYS) {
|
||||
const env = process.env[profileEnvName(profile, k)];
|
||||
const v = env || (typeof stored[k] === 'string' ? stored[k] as string : '');
|
||||
// PLAY_REPORTS_BUCKET is also the profile field `reportsBucket` (a bucket name, not a secret)
|
||||
const own = stored[k] ?? (k === 'PLAY_REPORTS_BUCKET' ? stored.reportsBucket : undefined);
|
||||
const v = env || (typeof own === 'string' ? own : '');
|
||||
if (!v) continue;
|
||||
// a path in the file: relative to the profile folder (inline JSON / PEM stays as it is)
|
||||
const isPath = !env && !/^\s*\{/.test(v) && !/BEGIN PRIVATE KEY/.test(v) && (k === 'PLAY_SERVICE_ACCOUNT' || k === 'ASC_P8');
|
||||
|
|
@ -346,6 +348,16 @@ export function ascVendorNumber(root: string): string | null {
|
|||
return v && /^\d{5,12}$/.test(v.trim()) ? v.trim() : null;
|
||||
}
|
||||
|
||||
/** The Play reports bucket (analytics statistics, sales / earnings reports): play.reportsBucket of the config, else env
|
||||
* PLAY_REPORTS_BUCKET, else the credential profile's `reportsBucket` (a store-only config has no play.reportsBucket).
|
||||
* Null when none is set; a value that is not a Play reports bucket throws (a bucket name is not a secret). */
|
||||
export function playReportsBucket(root: string, fromConfig?: string): string | null {
|
||||
const v = (fromConfig ?? value(root, 'PLAY_REPORTS_BUCKET') ?? '').trim();
|
||||
if (!v) return null;
|
||||
if (!/^(gs:\/\/)?pubsite_prod(_rev)?_\d+\/?$/.test(v)) throw new Error(`reports bucket "${v}": the Play reports bucket, e.g. "pubsite_prod_rev_<number>" (play.reportsBucket, env PLAY_REPORTS_BUCKET or the profile field reportsBucket)`);
|
||||
return v;
|
||||
}
|
||||
|
||||
/** Credential paths that do not exist (a machine move left another computer's absolute paths) and an upload keystore
|
||||
* the stored password / alias do not open: each names the field. `keytool` runs only when it is on PATH. */
|
||||
export function credentialFileIssues(root: string, run: (argv: string[]) => { ok: boolean; out: string } | null = keytool): string[] {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue