@gpc-cli/core 0.9.77 → 0.9.78
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/index.d.ts +141 -4
- package/dist/index.js +389 -15
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -3843,6 +3843,255 @@ async function convertRegionPrices(client, packageName, currencyCode, amount) {
|
|
|
3843
3843
|
});
|
|
3844
3844
|
}
|
|
3845
3845
|
|
|
3846
|
+
// src/reports/errors.ts
|
|
3847
|
+
function reportAccessDeniedError(bucket) {
|
|
3848
|
+
return new GpcError(
|
|
3849
|
+
`Access denied reading the Play reports bucket "${bucket}".`,
|
|
3850
|
+
"REPORT_ACCESS_DENIED",
|
|
3851
|
+
4,
|
|
3852
|
+
"Grant the service account access: Play Console -> Users & permissions -> the service account -> Account permissions -> enable 'View app information and download bulk reports (read-only)', then wait a few minutes for it to propagate."
|
|
3853
|
+
);
|
|
3854
|
+
}
|
|
3855
|
+
function reportAuthRejectedError(bucket) {
|
|
3856
|
+
return new GpcError(
|
|
3857
|
+
`Authentication was rejected reading the Play reports bucket "${bucket}".`,
|
|
3858
|
+
"REPORT_AUTH_REJECTED",
|
|
3859
|
+
3,
|
|
3860
|
+
"The access token was not accepted. Check the service account key is valid (gpc auth status) and that your system clock is accurate, then retry."
|
|
3861
|
+
);
|
|
3862
|
+
}
|
|
3863
|
+
function reportBucketNotFoundError(bucket) {
|
|
3864
|
+
return new GpcError(
|
|
3865
|
+
`Play reports bucket "${bucket}" was not found.`,
|
|
3866
|
+
"REPORT_BUCKET_NOT_FOUND",
|
|
3867
|
+
4,
|
|
3868
|
+
"Confirm the bucket name. Copy the exact Cloud Storage URI from Play Console -> Download reports, then set it with --bucket or the 'reports.bucket' config key. The default is pubsite_prod_<developerId>."
|
|
3869
|
+
);
|
|
3870
|
+
}
|
|
3871
|
+
function reportObjectNotFoundError(opts) {
|
|
3872
|
+
const { reportType, month, dimension, available } = opts;
|
|
3873
|
+
const dimPart = dimension ? ` dimension "${dimension}"` : "";
|
|
3874
|
+
const availPart = available && available.length ? ` Available: ${available.join(", ")}.` : " No matching report objects were found for that month.";
|
|
3875
|
+
return new GpcError(
|
|
3876
|
+
`No "${reportType}" report found for ${month}${dimPart}.`,
|
|
3877
|
+
"REPORT_OBJECT_NOT_FOUND",
|
|
3878
|
+
4,
|
|
3879
|
+
`Check the month is complete and the report type/dimension exist.${availPart} Reports for the current month may not be published until it ends.`
|
|
3880
|
+
);
|
|
3881
|
+
}
|
|
3882
|
+
|
|
3883
|
+
// src/reports/gcs.ts
|
|
3884
|
+
var GCS_API_BASE = "https://storage.googleapis.com/storage/v1/b";
|
|
3885
|
+
var LIST_TIMEOUT_MS = 6e4;
|
|
3886
|
+
var DOWNLOAD_TIMEOUT_MS = 12e4;
|
|
3887
|
+
function networkError(bucket, cause) {
|
|
3888
|
+
const detail = cause instanceof Error ? cause.message : String(cause);
|
|
3889
|
+
return new GpcError(
|
|
3890
|
+
`Network error reading the Play reports bucket "${bucket}": ${detail}`,
|
|
3891
|
+
"NETWORK_ERROR",
|
|
3892
|
+
5,
|
|
3893
|
+
"Check your internet connection and retry. Google Cloud Storage may also be briefly unavailable."
|
|
3894
|
+
);
|
|
3895
|
+
}
|
|
3896
|
+
function maxRetries() {
|
|
3897
|
+
const parsed = Number(process.env["GPC_MAX_RETRIES"]);
|
|
3898
|
+
return Number.isInteger(parsed) && parsed >= 0 && parsed <= 10 ? parsed : 3;
|
|
3899
|
+
}
|
|
3900
|
+
var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
|
|
3901
|
+
async function gcsFetch(auth, bucket, url, timeoutMs) {
|
|
3902
|
+
const retries = maxRetries();
|
|
3903
|
+
let lastError;
|
|
3904
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
3905
|
+
if (attempt > 0) {
|
|
3906
|
+
await new Promise((r) => setTimeout(r, Math.min(1e3 * 2 ** (attempt - 1), 8e3)));
|
|
3907
|
+
}
|
|
3908
|
+
const token = await auth.getAccessToken();
|
|
3909
|
+
let res;
|
|
3910
|
+
try {
|
|
3911
|
+
res = await fetch(url, {
|
|
3912
|
+
headers: { authorization: `Bearer ${token}` },
|
|
3913
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
3914
|
+
});
|
|
3915
|
+
} catch (err) {
|
|
3916
|
+
lastError = err;
|
|
3917
|
+
continue;
|
|
3918
|
+
}
|
|
3919
|
+
if (RETRYABLE_STATUSES.has(res.status) && attempt < retries) {
|
|
3920
|
+
continue;
|
|
3921
|
+
}
|
|
3922
|
+
if (res.status === 401) {
|
|
3923
|
+
throw reportAuthRejectedError(bucket);
|
|
3924
|
+
}
|
|
3925
|
+
if (res.status === 403) {
|
|
3926
|
+
throw reportAccessDeniedError(bucket);
|
|
3927
|
+
}
|
|
3928
|
+
return res;
|
|
3929
|
+
}
|
|
3930
|
+
throw networkError(bucket, lastError ?? new Error("retries exhausted"));
|
|
3931
|
+
}
|
|
3932
|
+
async function listReportObjects(auth, bucket, options) {
|
|
3933
|
+
const params = new URLSearchParams({ prefix: options.prefix });
|
|
3934
|
+
if (options.maxResults) params.set("maxResults", String(options.maxResults));
|
|
3935
|
+
if (options.pageToken) params.set("pageToken", options.pageToken);
|
|
3936
|
+
const url = `${GCS_API_BASE}/${encodeURIComponent(bucket)}/o?${params.toString()}`;
|
|
3937
|
+
const res = await gcsFetch(auth, bucket, url, LIST_TIMEOUT_MS);
|
|
3938
|
+
if (res.status === 404) {
|
|
3939
|
+
throw reportBucketNotFoundError(bucket);
|
|
3940
|
+
}
|
|
3941
|
+
if (!res.ok) {
|
|
3942
|
+
throw new GpcError(
|
|
3943
|
+
`Google Cloud Storage returned HTTP ${res.status} listing the reports bucket "${bucket}".`,
|
|
3944
|
+
"REPORT_LIST_FAILED",
|
|
3945
|
+
4,
|
|
3946
|
+
"Retry in a moment. If the error persists, verify the bucket name and the service account's report access."
|
|
3947
|
+
);
|
|
3948
|
+
}
|
|
3949
|
+
let body;
|
|
3950
|
+
try {
|
|
3951
|
+
body = await res.json();
|
|
3952
|
+
} catch (err) {
|
|
3953
|
+
throw networkError(bucket, err);
|
|
3954
|
+
}
|
|
3955
|
+
const objects = (body.items ?? []).filter((item) => typeof item.name === "string").map((item) => ({
|
|
3956
|
+
name: item.name,
|
|
3957
|
+
size: Number(item.size ?? 0),
|
|
3958
|
+
updated: item.updated
|
|
3959
|
+
}));
|
|
3960
|
+
return { objects, nextPageToken: body.nextPageToken };
|
|
3961
|
+
}
|
|
3962
|
+
async function downloadReportObject(auth, bucket, objectName) {
|
|
3963
|
+
const url = `${GCS_API_BASE}/${encodeURIComponent(bucket)}/o/${encodeURIComponent(objectName)}?alt=media`;
|
|
3964
|
+
const res = await gcsFetch(auth, bucket, url, DOWNLOAD_TIMEOUT_MS);
|
|
3965
|
+
if (res.status === 404) {
|
|
3966
|
+
throw new GpcError(
|
|
3967
|
+
`Report object "${objectName}" was not found in bucket "${bucket}".`,
|
|
3968
|
+
"REPORT_OBJECT_NOT_FOUND",
|
|
3969
|
+
4,
|
|
3970
|
+
"List available reports first: gpc reports list <report-type> --month <YYYY-MM>"
|
|
3971
|
+
);
|
|
3972
|
+
}
|
|
3973
|
+
if (!res.ok) {
|
|
3974
|
+
throw new GpcError(
|
|
3975
|
+
`Google Cloud Storage returned HTTP ${res.status} downloading "${objectName}".`,
|
|
3976
|
+
"REPORT_DOWNLOAD_FAILED",
|
|
3977
|
+
4,
|
|
3978
|
+
"Retry in a moment. If the error persists, list the reports to confirm the object still exists."
|
|
3979
|
+
);
|
|
3980
|
+
}
|
|
3981
|
+
try {
|
|
3982
|
+
return Buffer.from(await res.arrayBuffer());
|
|
3983
|
+
} catch (err) {
|
|
3984
|
+
throw networkError(bucket, err);
|
|
3985
|
+
}
|
|
3986
|
+
}
|
|
3987
|
+
|
|
3988
|
+
// src/reports/decode.ts
|
|
3989
|
+
import { gunzipSync } from "zlib";
|
|
3990
|
+
import { fromBuffer } from "yauzl";
|
|
3991
|
+
var GZIP_MAGIC = [31, 139];
|
|
3992
|
+
var ZIP_MAGIC = [80, 75];
|
|
3993
|
+
function isGzip(buf) {
|
|
3994
|
+
return buf.length >= 2 && buf[0] === GZIP_MAGIC[0] && buf[1] === GZIP_MAGIC[1];
|
|
3995
|
+
}
|
|
3996
|
+
function isZip(buf) {
|
|
3997
|
+
return buf.length >= 2 && buf[0] === ZIP_MAGIC[0] && buf[1] === ZIP_MAGIC[1];
|
|
3998
|
+
}
|
|
3999
|
+
var MAX_INFLATED_BYTES = 512 * 1024 * 1024;
|
|
4000
|
+
function gunzipIfNeeded(buf) {
|
|
4001
|
+
return isGzip(buf) ? gunzipSync(buf, { maxOutputLength: MAX_INFLATED_BYTES }) : buf;
|
|
4002
|
+
}
|
|
4003
|
+
function decodeCsvText(buf) {
|
|
4004
|
+
if (buf.length >= 2 && buf[0] === 255 && buf[1] === 254) {
|
|
4005
|
+
return buf.subarray(2).toString("utf16le");
|
|
4006
|
+
}
|
|
4007
|
+
if (buf.length >= 2 && buf[0] === 254 && buf[1] === 255) {
|
|
4008
|
+
const swapped = Buffer.from(buf.subarray(2));
|
|
4009
|
+
swapped.swap16();
|
|
4010
|
+
return swapped.toString("utf16le");
|
|
4011
|
+
}
|
|
4012
|
+
if (buf.length >= 3 && buf[0] === 239 && buf[1] === 187 && buf[2] === 191) {
|
|
4013
|
+
return buf.subarray(3).toString("utf8");
|
|
4014
|
+
}
|
|
4015
|
+
return buf.toString("utf8");
|
|
4016
|
+
}
|
|
4017
|
+
function decodeStatsCsv(buf) {
|
|
4018
|
+
return decodeCsvText(gunzipIfNeeded(buf));
|
|
4019
|
+
}
|
|
4020
|
+
var ZIP_READ_TIMEOUT_MS = 3e4;
|
|
4021
|
+
function extractCsvEntriesFromZip(buffer) {
|
|
4022
|
+
return new Promise((resolve2, reject) => {
|
|
4023
|
+
let settled = false;
|
|
4024
|
+
const timer = setTimeout(() => {
|
|
4025
|
+
settleReject(new Error("Timed out reading report archive"));
|
|
4026
|
+
}, ZIP_READ_TIMEOUT_MS);
|
|
4027
|
+
timer.unref?.();
|
|
4028
|
+
function settleResolve(v) {
|
|
4029
|
+
if (settled) return;
|
|
4030
|
+
settled = true;
|
|
4031
|
+
clearTimeout(timer);
|
|
4032
|
+
resolve2(v);
|
|
4033
|
+
}
|
|
4034
|
+
function settleReject(e) {
|
|
4035
|
+
if (settled) return;
|
|
4036
|
+
settled = true;
|
|
4037
|
+
clearTimeout(timer);
|
|
4038
|
+
reject(e);
|
|
4039
|
+
}
|
|
4040
|
+
try {
|
|
4041
|
+
fromBuffer(buffer, { lazyEntries: true }, (err, openedZip) => {
|
|
4042
|
+
if (err || !openedZip) {
|
|
4043
|
+
settleReject(err ?? new Error("Failed to open report archive"));
|
|
4044
|
+
return;
|
|
4045
|
+
}
|
|
4046
|
+
const zipfile = openedZip;
|
|
4047
|
+
const entries = [];
|
|
4048
|
+
let pending = 0;
|
|
4049
|
+
let entriesDone = false;
|
|
4050
|
+
function fail(e) {
|
|
4051
|
+
try {
|
|
4052
|
+
zipfile.close();
|
|
4053
|
+
} catch {
|
|
4054
|
+
}
|
|
4055
|
+
settleReject(e);
|
|
4056
|
+
}
|
|
4057
|
+
function maybeResolve() {
|
|
4058
|
+
if (!settled && entriesDone && pending === 0) settleResolve(entries);
|
|
4059
|
+
}
|
|
4060
|
+
zipfile.on("entry", (entry) => {
|
|
4061
|
+
if (/\/$/.test(entry.fileName) || !/\.csv$/i.test(entry.fileName)) {
|
|
4062
|
+
zipfile.readEntry();
|
|
4063
|
+
return;
|
|
4064
|
+
}
|
|
4065
|
+
pending++;
|
|
4066
|
+
zipfile.openReadStream(entry, (streamErr, stream) => {
|
|
4067
|
+
if (streamErr || !stream) {
|
|
4068
|
+
fail(streamErr ?? new Error(`Failed to read ${entry.fileName}`));
|
|
4069
|
+
return;
|
|
4070
|
+
}
|
|
4071
|
+
const chunks = [];
|
|
4072
|
+
stream.on("data", (c) => chunks.push(c));
|
|
4073
|
+
stream.on("error", fail);
|
|
4074
|
+
stream.on("end", () => {
|
|
4075
|
+
entries.push({ name: entry.fileName, text: decodeStatsCsv(Buffer.concat(chunks)) });
|
|
4076
|
+
pending--;
|
|
4077
|
+
zipfile.readEntry();
|
|
4078
|
+
maybeResolve();
|
|
4079
|
+
});
|
|
4080
|
+
});
|
|
4081
|
+
});
|
|
4082
|
+
zipfile.on("end", () => {
|
|
4083
|
+
entriesDone = true;
|
|
4084
|
+
maybeResolve();
|
|
4085
|
+
});
|
|
4086
|
+
zipfile.on("error", fail);
|
|
4087
|
+
zipfile.readEntry();
|
|
4088
|
+
});
|
|
4089
|
+
} catch (e) {
|
|
4090
|
+
settleReject(e instanceof Error ? e : new Error(String(e)));
|
|
4091
|
+
}
|
|
4092
|
+
});
|
|
4093
|
+
}
|
|
4094
|
+
|
|
3846
4095
|
// src/commands/reports.ts
|
|
3847
4096
|
var FINANCIAL_REPORT_TYPES = /* @__PURE__ */ new Set([
|
|
3848
4097
|
"earnings",
|
|
@@ -3865,6 +4114,7 @@ var VALID_DIMENSIONS = /* @__PURE__ */ new Set([
|
|
|
3865
4114
|
"device",
|
|
3866
4115
|
"app_version",
|
|
3867
4116
|
"carrier",
|
|
4117
|
+
"traffic_source",
|
|
3868
4118
|
"overview"
|
|
3869
4119
|
]);
|
|
3870
4120
|
function isFinancialReportType(type) {
|
|
@@ -3901,22 +4151,131 @@ function parseMonth(monthStr) {
|
|
|
3901
4151
|
}
|
|
3902
4152
|
return { year, month };
|
|
3903
4153
|
}
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
isFinancialReportType(reportType) ? "Financial reports are delivered via Google Cloud Storage. Access them from Play Console \u2192 Download reports \u2192 Financial." : "Stats reports are delivered via Google Cloud Storage. For real-time metrics, use 'gpc vitals' commands."
|
|
3910
|
-
);
|
|
4154
|
+
function monthToken(parsed) {
|
|
4155
|
+
return `${parsed.year}${String(parsed.month).padStart(2, "0")}`;
|
|
4156
|
+
}
|
|
4157
|
+
function nameHasMonth(name, token) {
|
|
4158
|
+
return new RegExp(`(?<![0-9])${token}(?![0-9])`).test(name);
|
|
3911
4159
|
}
|
|
3912
|
-
|
|
4160
|
+
function resolveReportsBucket(config) {
|
|
4161
|
+
const explicit = config.reports?.bucket?.trim();
|
|
4162
|
+
if (explicit) {
|
|
4163
|
+
const reduced = explicit.replace(/^gs:\/\//, "").replace(/\/.*$/, "");
|
|
4164
|
+
if (!/^[a-z0-9][a-z0-9._-]{1,221}$/.test(reduced)) {
|
|
4165
|
+
throw new GpcError(
|
|
4166
|
+
`Invalid reports bucket name "${reduced}".`,
|
|
4167
|
+
"REPORT_BUCKET_INVALID",
|
|
4168
|
+
2,
|
|
4169
|
+
"Copy the exact Cloud Storage URI from Play Console -> Download reports. The bucket name looks like pubsite_prod_<developerId>."
|
|
4170
|
+
);
|
|
4171
|
+
}
|
|
4172
|
+
return reduced;
|
|
4173
|
+
}
|
|
4174
|
+
if (config.developerId) {
|
|
4175
|
+
return `pubsite_prod_${config.developerId}`;
|
|
4176
|
+
}
|
|
3913
4177
|
throw new GpcError(
|
|
3914
|
-
|
|
3915
|
-
"
|
|
3916
|
-
|
|
3917
|
-
|
|
4178
|
+
"Cannot determine the Play reports bucket: no bucket configured and no developer id set.",
|
|
4179
|
+
"REPORT_BUCKET_UNKNOWN",
|
|
4180
|
+
2,
|
|
4181
|
+
"Set your developer id (gpc config set developerId <id> or GPC_DEVELOPER_ID) so the default pubsite_prod_<developerId> bucket can be derived, or pass the bucket directly with --bucket / GPC_REPORTS_BUCKET / the reports.bucket config key. The exact URI is shown in Play Console -> Download reports."
|
|
3918
4182
|
);
|
|
3919
4183
|
}
|
|
4184
|
+
function reportPrefix(reportType, packageName) {
|
|
4185
|
+
switch (reportType) {
|
|
4186
|
+
case "earnings":
|
|
4187
|
+
return "earnings/";
|
|
4188
|
+
case "sales":
|
|
4189
|
+
case "estimated_sales":
|
|
4190
|
+
return "sales/";
|
|
4191
|
+
case "play_balance":
|
|
4192
|
+
return "play_balance/";
|
|
4193
|
+
case "reviews":
|
|
4194
|
+
return packageName ? `reviews/reviews_${packageName}_` : "reviews/";
|
|
4195
|
+
case "subscriptions":
|
|
4196
|
+
return packageName ? `financial-stats/subscriptions/subscriptions_${packageName}_` : "financial-stats/subscriptions/";
|
|
4197
|
+
default:
|
|
4198
|
+
return packageName ? `stats/${reportType}/${reportType}_${packageName}_` : `stats/${reportType}/`;
|
|
4199
|
+
}
|
|
4200
|
+
}
|
|
4201
|
+
async function listReports(auth, bucket, reportType, options = {}) {
|
|
4202
|
+
const packageName = isStatsReportType(reportType) ? options.packageName : void 0;
|
|
4203
|
+
const basePrefix = reportPrefix(reportType, packageName);
|
|
4204
|
+
const serverSideMonth = Boolean(packageName && options.month);
|
|
4205
|
+
const prefix = serverSideMonth ? `${basePrefix}${monthToken(options.month)}` : basePrefix;
|
|
4206
|
+
const { objects, nextPageToken } = await listReportObjects(auth, bucket, {
|
|
4207
|
+
prefix,
|
|
4208
|
+
maxResults: options.maxResults,
|
|
4209
|
+
pageToken: options.pageToken
|
|
4210
|
+
});
|
|
4211
|
+
const reports = options.month && !serverSideMonth ? objects.filter((o) => nameHasMonth(o.name, monthToken(options.month))) : objects;
|
|
4212
|
+
return { reports, nextPageToken };
|
|
4213
|
+
}
|
|
4214
|
+
async function downloadStatsReport(auth, bucket, packageName, reportType, month, dimension) {
|
|
4215
|
+
const token = monthToken(month);
|
|
4216
|
+
const prefix = `${reportPrefix(reportType, packageName)}${token}`;
|
|
4217
|
+
const { objects } = await listReportObjects(auth, bucket, { prefix });
|
|
4218
|
+
const wanted = reportType === "reviews" ? objects.find((o) => o.name.endsWith(`${token}.csv`)) : objects.find((o) => o.name.endsWith(`${token}_${dimension}.csv`));
|
|
4219
|
+
if (!wanted) {
|
|
4220
|
+
const available = objects.map((o) => new RegExp(`(?<![0-9])${token}_([a-z_]+)\\.csv$`).exec(o.name)?.[1]).filter((d) => Boolean(d));
|
|
4221
|
+
throw reportObjectNotFoundError({
|
|
4222
|
+
reportType,
|
|
4223
|
+
month: `${month.year}-${String(month.month).padStart(2, "0")}`,
|
|
4224
|
+
dimension: reportType === "reviews" ? void 0 : dimension,
|
|
4225
|
+
available: [...new Set(available)]
|
|
4226
|
+
});
|
|
4227
|
+
}
|
|
4228
|
+
const bytes = await downloadReportObject(auth, bucket, wanted.name);
|
|
4229
|
+
try {
|
|
4230
|
+
return { objectName: wanted.name, csv: decodeStatsCsv(bytes) };
|
|
4231
|
+
} catch (err) {
|
|
4232
|
+
throw new GpcError(
|
|
4233
|
+
`Could not decode the report "${wanted.name}": ${err instanceof Error ? err.message : String(err)}`,
|
|
4234
|
+
"REPORT_DECODE_FAILED",
|
|
4235
|
+
4,
|
|
4236
|
+
"Re-download in case the transfer was corrupted. If the error persists, the object may be malformed; download it from Play Console -> Download reports to compare."
|
|
4237
|
+
);
|
|
4238
|
+
}
|
|
4239
|
+
}
|
|
4240
|
+
async function downloadFinancialReport(auth, bucket, reportType, month, options = {}) {
|
|
4241
|
+
const token = monthToken(month);
|
|
4242
|
+
const { objects } = await listReportObjects(auth, bucket, {
|
|
4243
|
+
prefix: reportPrefix(reportType)
|
|
4244
|
+
});
|
|
4245
|
+
const matches = objects.filter((o) => nameHasMonth(o.name, token));
|
|
4246
|
+
if (matches.length === 0) {
|
|
4247
|
+
const availableMonths = [
|
|
4248
|
+
...new Set(
|
|
4249
|
+
objects.map((o) => /(?<![0-9])(\d{4})(\d{2})(?![0-9])/.exec(o.name)).filter((m) => m !== null).map((m) => `${m[1]}-${m[2]}`)
|
|
4250
|
+
)
|
|
4251
|
+
].slice(-6);
|
|
4252
|
+
throw reportObjectNotFoundError({
|
|
4253
|
+
reportType,
|
|
4254
|
+
month: `${month.year}-${String(month.month).padStart(2, "0")}`,
|
|
4255
|
+
available: availableMonths
|
|
4256
|
+
});
|
|
4257
|
+
}
|
|
4258
|
+
const chosen = matches[matches.length - 1];
|
|
4259
|
+
const bytes = await downloadReportObject(auth, bucket, chosen.name);
|
|
4260
|
+
if (isZip(bytes)) {
|
|
4261
|
+
if (options.extractEntries === false) {
|
|
4262
|
+
return { objectName: chosen.name, kind: "zip", entries: [], raw: bytes };
|
|
4263
|
+
}
|
|
4264
|
+
let entries;
|
|
4265
|
+
try {
|
|
4266
|
+
entries = await extractCsvEntriesFromZip(bytes);
|
|
4267
|
+
} catch (err) {
|
|
4268
|
+
throw new GpcError(
|
|
4269
|
+
`Could not read the report archive "${chosen.name}": ${err instanceof Error ? err.message : String(err)}`,
|
|
4270
|
+
"REPORT_ARCHIVE_UNREADABLE",
|
|
4271
|
+
4,
|
|
4272
|
+
"Save the raw archive instead (--output-file report.zip) and open it locally, or re-download in case the transfer was corrupted."
|
|
4273
|
+
);
|
|
4274
|
+
}
|
|
4275
|
+
return { objectName: chosen.name, kind: "zip", entries, raw: bytes };
|
|
4276
|
+
}
|
|
4277
|
+
return { objectName: chosen.name, kind: "csv", text: decodeStatsCsv(bytes) };
|
|
4278
|
+
}
|
|
3920
4279
|
|
|
3921
4280
|
// src/commands/users.ts
|
|
3922
4281
|
import { paginateAll as paginateAll5 } from "@gpc-cli/api";
|
|
@@ -5376,7 +5735,7 @@ async function loadPreflightConfig(configPath) {
|
|
|
5376
5735
|
|
|
5377
5736
|
// src/preflight/aab-reader.ts
|
|
5378
5737
|
import { readFile as readFile11 } from "fs/promises";
|
|
5379
|
-
import { fromBuffer } from "yauzl";
|
|
5738
|
+
import { fromBuffer as fromBuffer2 } from "yauzl";
|
|
5380
5739
|
|
|
5381
5740
|
// src/preflight/manifest-parser.ts
|
|
5382
5741
|
import protobuf from "protobufjs";
|
|
@@ -5637,7 +5996,7 @@ function openAndScan(buffer, manifestPath = AAB_MANIFEST_PATH) {
|
|
|
5637
5996
|
reject(error);
|
|
5638
5997
|
}
|
|
5639
5998
|
try {
|
|
5640
|
-
|
|
5999
|
+
fromBuffer2(buffer, { lazyEntries: true }, (err, zipfile) => {
|
|
5641
6000
|
if (err || !zipfile) {
|
|
5642
6001
|
settleReject(err ?? new Error("Failed to open AAB file"));
|
|
5643
6002
|
return;
|
|
@@ -9444,7 +9803,9 @@ export {
|
|
|
9444
9803
|
createTranslator,
|
|
9445
9804
|
deactivateBasePlan,
|
|
9446
9805
|
deactivateOffer,
|
|
9806
|
+
decodeCsvText,
|
|
9447
9807
|
decodeNotification,
|
|
9808
|
+
decodeStatsCsv,
|
|
9448
9809
|
defaultGitRunner,
|
|
9449
9810
|
deferSubscriptionPurchase,
|
|
9450
9811
|
deferSubscriptionV2,
|
|
@@ -9472,10 +9833,13 @@ export {
|
|
|
9472
9833
|
diffSubscription,
|
|
9473
9834
|
discoverPlugins,
|
|
9474
9835
|
dispatchWebhook,
|
|
9836
|
+
downloadFinancialReport,
|
|
9475
9837
|
downloadGeneratedApk,
|
|
9476
|
-
|
|
9838
|
+
downloadReportObject,
|
|
9839
|
+
downloadStatsReport,
|
|
9477
9840
|
exportImages,
|
|
9478
9841
|
exportReviews,
|
|
9842
|
+
extractCsvEntriesFromZip,
|
|
9479
9843
|
fetchAggregateCost,
|
|
9480
9844
|
fetchChangelog,
|
|
9481
9845
|
fetchReleaseNotes,
|
|
@@ -9534,6 +9898,7 @@ export {
|
|
|
9534
9898
|
getVitalsRendering,
|
|
9535
9899
|
getVitalsStartup,
|
|
9536
9900
|
gradeFromPercent,
|
|
9901
|
+
gunzipIfNeeded,
|
|
9537
9902
|
handleBreach,
|
|
9538
9903
|
importDataSafety,
|
|
9539
9904
|
importTestersFromCsv,
|
|
@@ -9541,11 +9906,13 @@ export {
|
|
|
9541
9906
|
initProject,
|
|
9542
9907
|
inviteUser,
|
|
9543
9908
|
isFinancialReportType,
|
|
9909
|
+
isGzip,
|
|
9544
9910
|
isStatsReportType,
|
|
9545
9911
|
isValidBcp47,
|
|
9546
9912
|
isValidReportType,
|
|
9547
9913
|
isValidStatsDimension,
|
|
9548
9914
|
isVersionedNotesDir,
|
|
9915
|
+
isZip,
|
|
9549
9916
|
lintListing,
|
|
9550
9917
|
lintListings,
|
|
9551
9918
|
lintLocalListings,
|
|
@@ -9564,6 +9931,7 @@ export {
|
|
|
9564
9931
|
listOneTimeOffers,
|
|
9565
9932
|
listOneTimeProducts,
|
|
9566
9933
|
listRecoveryActions,
|
|
9934
|
+
listReportObjects,
|
|
9567
9935
|
listReports,
|
|
9568
9936
|
listReviews,
|
|
9569
9937
|
listSubscriptions,
|
|
@@ -9575,6 +9943,7 @@ export {
|
|
|
9575
9943
|
loadStatusCache,
|
|
9576
9944
|
maybePaginate,
|
|
9577
9945
|
migratePrices,
|
|
9946
|
+
monthToken,
|
|
9578
9947
|
moreResultsFooter,
|
|
9579
9948
|
normalizeFingerprint,
|
|
9580
9949
|
parseAppfile,
|
|
@@ -9616,8 +9985,13 @@ export {
|
|
|
9616
9985
|
renderPlayStorePrompt,
|
|
9617
9986
|
renderPrompt,
|
|
9618
9987
|
replyToReview,
|
|
9988
|
+
reportAccessDeniedError,
|
|
9989
|
+
reportBucketNotFoundError,
|
|
9990
|
+
reportObjectNotFoundError,
|
|
9991
|
+
reportPrefix,
|
|
9619
9992
|
resolveAiConfig,
|
|
9620
9993
|
resolveLocales,
|
|
9994
|
+
resolveReportsBucket,
|
|
9621
9995
|
retryOnUploadNotComplete,
|
|
9622
9996
|
revokeSubscriptionPurchase,
|
|
9623
9997
|
runPreflight,
|