@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/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
- async function listReports(_client, _packageName, reportType, _year, _month) {
3905
- throw new GpcError(
3906
- `Report listing for "${reportType}" is not available through the Google Play Developer API.`,
3907
- "REPORT_NOT_SUPPORTED",
3908
- 1,
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
- async function downloadReport(_client, _packageName, reportType, _year, _month) {
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
- `Report download for "${reportType}" is not available through the Google Play Developer API.`,
3915
- "REPORT_NOT_SUPPORTED",
3916
- 1,
3917
- 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."
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
- fromBuffer(buffer, { lazyEntries: true }, (err, zipfile) => {
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
- downloadReport,
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,