@gpc-cli/core 0.9.22 → 0.9.24
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.d.ts +42 -1
- package/dist/index.js +279 -60
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -737,6 +737,8 @@ interface StatusVitalMetric {
|
|
|
737
737
|
value: number | undefined;
|
|
738
738
|
threshold: number;
|
|
739
739
|
status: "ok" | "warn" | "breach" | "unknown";
|
|
740
|
+
previousValue?: number | undefined;
|
|
741
|
+
trend?: "up" | "down" | "flat" | null;
|
|
740
742
|
}
|
|
741
743
|
interface StatusRelease {
|
|
742
744
|
track: string;
|
|
@@ -765,8 +767,34 @@ interface AppStatus {
|
|
|
765
767
|
};
|
|
766
768
|
reviews: StatusReviews;
|
|
767
769
|
}
|
|
770
|
+
interface StatusDiff {
|
|
771
|
+
versionCode: {
|
|
772
|
+
from: string | null;
|
|
773
|
+
to: string | null;
|
|
774
|
+
};
|
|
775
|
+
crashRate: {
|
|
776
|
+
from: number | null;
|
|
777
|
+
to: number | null;
|
|
778
|
+
delta: number | null;
|
|
779
|
+
};
|
|
780
|
+
anrRate: {
|
|
781
|
+
from: number | null;
|
|
782
|
+
to: number | null;
|
|
783
|
+
delta: number | null;
|
|
784
|
+
};
|
|
785
|
+
reviewCount: {
|
|
786
|
+
from: number | null;
|
|
787
|
+
to: number | null;
|
|
788
|
+
};
|
|
789
|
+
averageRating: {
|
|
790
|
+
from: number | null;
|
|
791
|
+
to: number | null;
|
|
792
|
+
delta: number | null;
|
|
793
|
+
};
|
|
794
|
+
}
|
|
768
795
|
interface GetAppStatusOptions {
|
|
769
796
|
days?: number;
|
|
797
|
+
sections?: string[];
|
|
770
798
|
vitalThresholds?: {
|
|
771
799
|
crashRate?: number;
|
|
772
800
|
anrRate?: number;
|
|
@@ -774,10 +802,23 @@ interface GetAppStatusOptions {
|
|
|
774
802
|
slowRenderingRate?: number;
|
|
775
803
|
};
|
|
776
804
|
}
|
|
805
|
+
interface WatchOptions {
|
|
806
|
+
intervalSeconds: number;
|
|
807
|
+
render: (status: AppStatus) => string;
|
|
808
|
+
fetch: () => Promise<AppStatus>;
|
|
809
|
+
save: (status: AppStatus) => Promise<void>;
|
|
810
|
+
}
|
|
777
811
|
declare function loadStatusCache(packageName: string, ttlSeconds?: number): Promise<AppStatus | null>;
|
|
778
812
|
declare function saveStatusCache(packageName: string, data: AppStatus, ttlSeconds?: number): Promise<void>;
|
|
779
813
|
declare function getAppStatus(client: PlayApiClient, reporting: ReportingApiClient, packageName: string, options?: GetAppStatusOptions): Promise<AppStatus>;
|
|
780
814
|
declare function formatStatusTable(status: AppStatus): string;
|
|
815
|
+
declare function formatStatusSummary(status: AppStatus): string;
|
|
816
|
+
declare function computeStatusDiff(prev: AppStatus, curr: AppStatus): StatusDiff;
|
|
817
|
+
declare function formatStatusDiff(diff: StatusDiff, since: string): string;
|
|
818
|
+
declare function runWatchLoop(opts: WatchOptions): Promise<void>;
|
|
819
|
+
/** Returns true if breach state changed (breach started or cleared). */
|
|
820
|
+
declare function trackBreachState(packageName: string, isBreaching: boolean): Promise<boolean>;
|
|
821
|
+
declare function sendNotification(title: string, body: string): void;
|
|
781
822
|
declare function statusHasBreach(status: AppStatus): boolean;
|
|
782
823
|
|
|
783
|
-
export { ApiError, type AppInfo, type AppStatus, type AuditEntry, type BatchSyncResult, type BundleAnalysis, type BundleComparison, type BundleEntry, type CommandContext, ConfigError, type DiscoverPluginsOptions, type DryRunPublishResult, type DryRunResult, type DryRunUploadResult, type ExportImagesOptions, type ExportImagesSummary, type FastlaneDetection, type FastlaneLane, type FileValidationResult, GOOGLE_PLAY_LANGUAGES, type GetAppStatusOptions, type GitNotesOptions, type GitReleaseNotes, GpcError, type ImageValidationResult, type InternalSharingUploadResult, type ListIapOptions, type ListSubscriptionsOptions, type ListUsersOptions, type ListVoidedOptions, type ListingDiff, type ListingsResult, type LoadedPlugin, type MigrationResult, NetworkError, type OneTimeProductDiff, PERMISSION_PROPAGATION_WARNING, type ParsedMonth, PluginManager, type PublishOptions, type PublishResult, type PushResult, type ReleaseDiff, type ReleaseNotesValidation, type ReleaseStatusResult, type ReviewExportOptions, type ReviewsFilterOptions, SENSITIVE_ARG_KEYS, SENSITIVE_KEYS, type ScaffoldOptions, type ScaffoldResult, type Spinner, type StatusRelease, type StatusReviews, type StatusVitalMetric, type SubscriptionDiff, type SyncResult, type ThresholdResult, type UploadResult, type ValidateCheck, type ValidateOptions, type ValidateResult, type VitalsOverview, type VitalsQueryOptions, type VitalsTrendComparison, type WebhookPayload, acknowledgeProductPurchase, activateBasePlan, activateOffer, activatePurchaseOption, addRecoveryTargeting, addTesters, analyzeBundle, batchSyncInAppProducts, cancelRecoveryAction, cancelSubscriptionPurchase, checkThreshold, clearAuditLog, compareBundles, compareVitalsTrend, consumeProductPurchase, convertRegionPrices, createAuditEntry, createDeviceTier, createExternalTransaction, createInAppProduct, createOffer, createOneTimeOffer, createOneTimeProduct, createPurchaseOption, createRecoveryAction, createSpinner, createSubscription, createTrack, deactivateBasePlan, deactivateOffer, deactivatePurchaseOption, deferSubscriptionPurchase, deleteBasePlan, deleteImage, deleteInAppProduct, deleteListing, deleteOffer, deleteOneTimeOffer, deleteOneTimeProduct, deleteSubscription, deployRecoveryAction, detectFastlane, detectOutputFormat, diffListings, diffListingsCommand, diffOneTimeProduct, diffReleases, diffSubscription, discoverPlugins, downloadGeneratedApk, downloadReport, exportDataSafety, exportImages, exportReviews, formatCustomPayload, formatDiscordPayload, formatJunit, formatOutput, formatSlackPayload, formatStatusTable, generateMigrationPlan, generateNotesFromGit, getAppInfo, getAppStatus, getCountryAvailability, getDataSafety, getDeviceTier, getExternalTransaction, getInAppProduct, getListings, getOffer, getOneTimeOffer, getOneTimeProduct, getProductPurchase, getPurchaseOption, getReleasesStatus, getReview, getSubscription, getSubscriptionPurchase, getUser, getVitalsAnomalies, getVitalsAnr, getVitalsBattery, getVitalsCrashes, getVitalsMemory, getVitalsOverview, getVitalsRendering, getVitalsStartup, importDataSafety, importTestersFromCsv, initAudit, inviteUser, isFinancialReportType, isStatsReportType, isValidBcp47, isValidReportType, isValidStatsDimension, listAuditEvents, listDeviceTiers, listGeneratedApks, listImages, listInAppProducts, listOffers, listOneTimeOffers, listOneTimeProducts, listPurchaseOptions, listRecoveryActions, listReports, listReviews, listSubscriptions, listTesters, listTracks, listUsers, listVoidedPurchases, loadStatusCache, migratePrices, parseAppfile, parseFastfile, parseGrantArg, parseMonth, promoteRelease, publish, pullListings, pushListings, readListingsFromDir, readReleaseNotesFromDir, redactAuditArgs, redactSensitive, refundExternalTransaction, refundOrder, removeTesters, removeUser, replyToReview, revokeSubscriptionPurchase, safePath, safePathWithin, saveStatusCache, scaffoldPlugin, searchAuditEvents, searchVitalsErrors, sendWebhook, sortResults, statusHasBreach, syncInAppProducts, updateAppDetails, updateDataSafety, updateInAppProduct, updateListing, updateOffer, updateOneTimeOffer, updateOneTimeProduct, updateRollout, updateSubscription, updateTrackConfig, updateUser, uploadExternallyHosted, uploadImage, uploadInternalSharing, uploadRelease, validateImage, validateLanguageCode, validatePackageName, validatePreSubmission, validateReleaseNotes, validateSku, validateTrackName, validateUploadFile, validateVersionCode, writeAuditLog, writeListingsToDir, writeMigrationOutput };
|
|
824
|
+
export { ApiError, type AppInfo, type AppStatus, type AuditEntry, type BatchSyncResult, type BundleAnalysis, type BundleComparison, type BundleEntry, type CommandContext, ConfigError, type DiscoverPluginsOptions, type DryRunPublishResult, type DryRunResult, type DryRunUploadResult, type ExportImagesOptions, type ExportImagesSummary, type FastlaneDetection, type FastlaneLane, type FileValidationResult, GOOGLE_PLAY_LANGUAGES, type GetAppStatusOptions, type GitNotesOptions, type GitReleaseNotes, GpcError, type ImageValidationResult, type InternalSharingUploadResult, type ListIapOptions, type ListSubscriptionsOptions, type ListUsersOptions, type ListVoidedOptions, type ListingDiff, type ListingsResult, type LoadedPlugin, type MigrationResult, NetworkError, type OneTimeProductDiff, PERMISSION_PROPAGATION_WARNING, type ParsedMonth, PluginManager, type PublishOptions, type PublishResult, type PushResult, type ReleaseDiff, type ReleaseNotesValidation, type ReleaseStatusResult, type ReviewExportOptions, type ReviewsFilterOptions, SENSITIVE_ARG_KEYS, SENSITIVE_KEYS, type ScaffoldOptions, type ScaffoldResult, type Spinner, type StatusDiff, type StatusRelease, type StatusReviews, type StatusVitalMetric, type SubscriptionDiff, type SyncResult, type ThresholdResult, type UploadResult, type ValidateCheck, type ValidateOptions, type ValidateResult, type VitalsOverview, type VitalsQueryOptions, type VitalsTrendComparison, type WatchOptions, type WebhookPayload, acknowledgeProductPurchase, activateBasePlan, activateOffer, activatePurchaseOption, addRecoveryTargeting, addTesters, analyzeBundle, batchSyncInAppProducts, cancelRecoveryAction, cancelSubscriptionPurchase, checkThreshold, clearAuditLog, compareBundles, compareVitalsTrend, computeStatusDiff, consumeProductPurchase, convertRegionPrices, createAuditEntry, createDeviceTier, createExternalTransaction, createInAppProduct, createOffer, createOneTimeOffer, createOneTimeProduct, createPurchaseOption, createRecoveryAction, createSpinner, createSubscription, createTrack, deactivateBasePlan, deactivateOffer, deactivatePurchaseOption, deferSubscriptionPurchase, deleteBasePlan, deleteImage, deleteInAppProduct, deleteListing, deleteOffer, deleteOneTimeOffer, deleteOneTimeProduct, deleteSubscription, deployRecoveryAction, detectFastlane, detectOutputFormat, diffListings, diffListingsCommand, diffOneTimeProduct, diffReleases, diffSubscription, discoverPlugins, downloadGeneratedApk, downloadReport, exportDataSafety, exportImages, exportReviews, formatCustomPayload, formatDiscordPayload, formatJunit, formatOutput, formatSlackPayload, formatStatusDiff, formatStatusSummary, formatStatusTable, generateMigrationPlan, generateNotesFromGit, getAppInfo, getAppStatus, getCountryAvailability, getDataSafety, getDeviceTier, getExternalTransaction, getInAppProduct, getListings, getOffer, getOneTimeOffer, getOneTimeProduct, getProductPurchase, getPurchaseOption, getReleasesStatus, getReview, getSubscription, getSubscriptionPurchase, getUser, getVitalsAnomalies, getVitalsAnr, getVitalsBattery, getVitalsCrashes, getVitalsMemory, getVitalsOverview, getVitalsRendering, getVitalsStartup, importDataSafety, importTestersFromCsv, initAudit, inviteUser, isFinancialReportType, isStatsReportType, isValidBcp47, isValidReportType, isValidStatsDimension, listAuditEvents, listDeviceTiers, listGeneratedApks, listImages, listInAppProducts, listOffers, listOneTimeOffers, listOneTimeProducts, listPurchaseOptions, listRecoveryActions, listReports, listReviews, listSubscriptions, listTesters, listTracks, listUsers, listVoidedPurchases, loadStatusCache, migratePrices, parseAppfile, parseFastfile, parseGrantArg, parseMonth, promoteRelease, publish, pullListings, pushListings, readListingsFromDir, readReleaseNotesFromDir, redactAuditArgs, redactSensitive, refundExternalTransaction, refundOrder, removeTesters, removeUser, replyToReview, revokeSubscriptionPurchase, runWatchLoop, safePath, safePathWithin, saveStatusCache, scaffoldPlugin, searchAuditEvents, searchVitalsErrors, sendNotification, sendWebhook, sortResults, statusHasBreach, syncInAppProducts, trackBreachState, updateAppDetails, updateDataSafety, updateInAppProduct, updateListing, updateOffer, updateOneTimeOffer, updateOneTimeProduct, updateRollout, updateSubscription, updateTrackConfig, updateUser, uploadExternallyHosted, uploadImage, uploadInternalSharing, uploadRelease, validateImage, validateLanguageCode, validatePackageName, validatePreSubmission, validateReleaseNotes, validateSku, validateTrackName, validateUploadFile, validateVersionCode, writeAuditLog, writeListingsToDir, writeMigrationOutput };
|
package/dist/index.js
CHANGED
|
@@ -39,9 +39,9 @@ var NetworkError = class extends GpcError {
|
|
|
39
39
|
};
|
|
40
40
|
|
|
41
41
|
// src/output.ts
|
|
42
|
-
import
|
|
42
|
+
import process2 from "process";
|
|
43
43
|
function detectOutputFormat() {
|
|
44
|
-
return
|
|
44
|
+
return process2.stdout.isTTY ? "table" : "json";
|
|
45
45
|
}
|
|
46
46
|
function formatOutput(data, format, redact = true) {
|
|
47
47
|
const safe = redact ? redactSensitive(data) : data;
|
|
@@ -242,11 +242,29 @@ function buildTestCase(item, commandName, index = 0) {
|
|
|
242
242
|
};
|
|
243
243
|
}
|
|
244
244
|
const record = item;
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
245
|
+
const CANDIDATE_KEYS = [
|
|
246
|
+
"name",
|
|
247
|
+
"title",
|
|
248
|
+
"sku",
|
|
249
|
+
"id",
|
|
250
|
+
"reviewId",
|
|
251
|
+
"productId",
|
|
252
|
+
"packageName",
|
|
253
|
+
"track",
|
|
254
|
+
"trackId",
|
|
255
|
+
"versionCode",
|
|
256
|
+
"region",
|
|
257
|
+
"languageCode"
|
|
258
|
+
];
|
|
259
|
+
let resolvedName = `item-${index + 1}`;
|
|
260
|
+
for (const key of CANDIDATE_KEYS) {
|
|
261
|
+
const val = record[key];
|
|
262
|
+
if (val != null && val !== "" && val !== "-") {
|
|
263
|
+
resolvedName = String(val);
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
const name = escapeXml(resolvedName);
|
|
250
268
|
const classname = `gpc.${escapeXml(commandName)}`;
|
|
251
269
|
const breached = record["breached"];
|
|
252
270
|
if (breached === true) {
|
|
@@ -3165,23 +3183,23 @@ async function deleteOneTimeOffer(client, packageName, productId, offerId) {
|
|
|
3165
3183
|
}
|
|
3166
3184
|
|
|
3167
3185
|
// src/utils/spinner.ts
|
|
3168
|
-
import
|
|
3186
|
+
import process3 from "process";
|
|
3169
3187
|
var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
3170
3188
|
var INTERVAL_MS = 80;
|
|
3171
3189
|
function createSpinner(message) {
|
|
3172
|
-
const isTTY =
|
|
3190
|
+
const isTTY = process3.stderr.isTTY === true;
|
|
3173
3191
|
let frameIndex = 0;
|
|
3174
3192
|
let timer;
|
|
3175
3193
|
let currentMessage = message;
|
|
3176
3194
|
let started = false;
|
|
3177
3195
|
function clearLine() {
|
|
3178
3196
|
if (isTTY) {
|
|
3179
|
-
|
|
3197
|
+
process3.stderr.write("\r\x1B[K");
|
|
3180
3198
|
}
|
|
3181
3199
|
}
|
|
3182
3200
|
function renderFrame() {
|
|
3183
3201
|
const frame = FRAMES[frameIndex % FRAMES.length];
|
|
3184
|
-
|
|
3202
|
+
process3.stderr.write(`\r\x1B[K${frame} ${currentMessage}`);
|
|
3185
3203
|
frameIndex++;
|
|
3186
3204
|
}
|
|
3187
3205
|
return {
|
|
@@ -3189,7 +3207,7 @@ function createSpinner(message) {
|
|
|
3189
3207
|
if (started) return;
|
|
3190
3208
|
started = true;
|
|
3191
3209
|
if (!isTTY) {
|
|
3192
|
-
|
|
3210
|
+
process3.stderr.write(`${currentMessage}
|
|
3193
3211
|
`);
|
|
3194
3212
|
return;
|
|
3195
3213
|
}
|
|
@@ -3204,10 +3222,10 @@ function createSpinner(message) {
|
|
|
3204
3222
|
const text = msg ?? currentMessage;
|
|
3205
3223
|
if (isTTY) {
|
|
3206
3224
|
clearLine();
|
|
3207
|
-
|
|
3225
|
+
process3.stderr.write(`\u2714 ${text}
|
|
3208
3226
|
`);
|
|
3209
3227
|
} else if (!started) {
|
|
3210
|
-
|
|
3228
|
+
process3.stderr.write(`${text}
|
|
3211
3229
|
`);
|
|
3212
3230
|
}
|
|
3213
3231
|
started = false;
|
|
@@ -3220,10 +3238,10 @@ function createSpinner(message) {
|
|
|
3220
3238
|
const text = msg ?? currentMessage;
|
|
3221
3239
|
if (isTTY) {
|
|
3222
3240
|
clearLine();
|
|
3223
|
-
|
|
3241
|
+
process3.stderr.write(`\u2718 ${text}
|
|
3224
3242
|
`);
|
|
3225
3243
|
} else if (!started) {
|
|
3226
|
-
|
|
3244
|
+
process3.stderr.write(`${text}
|
|
3227
3245
|
`);
|
|
3228
3246
|
}
|
|
3229
3247
|
started = false;
|
|
@@ -3957,6 +3975,7 @@ function compareBundles(before, after) {
|
|
|
3957
3975
|
|
|
3958
3976
|
// src/commands/status.ts
|
|
3959
3977
|
import { mkdir as mkdir5, readFile as readFile10, writeFile as writeFile7 } from "fs/promises";
|
|
3978
|
+
import { execSync } from "child_process";
|
|
3960
3979
|
import { join as join7 } from "path";
|
|
3961
3980
|
import { getCacheDir } from "@gpc-cli/config";
|
|
3962
3981
|
var DEFAULT_TTL_SECONDS = 3600;
|
|
@@ -3978,12 +3997,11 @@ async function saveStatusCache(packageName, data, ttlSeconds = DEFAULT_TTL_SECON
|
|
|
3978
3997
|
try {
|
|
3979
3998
|
const dir = getCacheDir();
|
|
3980
3999
|
await mkdir5(dir, { recursive: true });
|
|
3981
|
-
const entry = {
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
};
|
|
3986
|
-
await writeFile7(cacheFilePath(packageName), JSON.stringify(entry, null, 2), { encoding: "utf-8", mode: 384 });
|
|
4000
|
+
const entry = { fetchedAt: data.fetchedAt, ttl: ttlSeconds, data };
|
|
4001
|
+
await writeFile7(cacheFilePath(packageName), JSON.stringify(entry, null, 2), {
|
|
4002
|
+
encoding: "utf-8",
|
|
4003
|
+
mode: 384
|
|
4004
|
+
});
|
|
3987
4005
|
} catch {
|
|
3988
4006
|
}
|
|
3989
4007
|
}
|
|
@@ -4003,9 +4021,9 @@ var WARN_MARGIN = 0.2;
|
|
|
4003
4021
|
function toApiDate(d) {
|
|
4004
4022
|
return { year: d.getUTCFullYear(), month: d.getUTCMonth() + 1, day: d.getUTCDate() };
|
|
4005
4023
|
}
|
|
4006
|
-
async function queryVitalForStatus(reporting, packageName, metricSet, days) {
|
|
4024
|
+
async function queryVitalForStatus(reporting, packageName, metricSet, days, offsetDays = 0) {
|
|
4007
4025
|
const DAY_MS = 24 * 60 * 60 * 1e3;
|
|
4008
|
-
const baseMs = Date.now() - 2 * DAY_MS;
|
|
4026
|
+
const baseMs = Date.now() - 2 * DAY_MS - offsetDays * DAY_MS;
|
|
4009
4027
|
const end = new Date(baseMs);
|
|
4010
4028
|
const start = new Date(baseMs - days * DAY_MS);
|
|
4011
4029
|
const metrics = METRIC_SET_METRICS2[metricSet] ?? ["distinctUsers"];
|
|
@@ -4026,13 +4044,26 @@ async function queryVitalForStatus(reporting, packageName, metricSet, days) {
|
|
|
4026
4044
|
if (values.length === 0) return void 0;
|
|
4027
4045
|
return values.reduce((a, b) => a + b, 0) / values.length;
|
|
4028
4046
|
}
|
|
4029
|
-
function
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4047
|
+
async function queryVitalWithTrend(reporting, packageName, metricSet, days) {
|
|
4048
|
+
const [current, previous] = await Promise.all([
|
|
4049
|
+
queryVitalForStatus(reporting, packageName, metricSet, days, 0),
|
|
4050
|
+
queryVitalForStatus(reporting, packageName, metricSet, days, days)
|
|
4051
|
+
]);
|
|
4052
|
+
let trend = null;
|
|
4053
|
+
if (current !== void 0 && previous !== void 0) {
|
|
4054
|
+
if (current > previous) trend = "up";
|
|
4055
|
+
else if (current < previous) trend = "down";
|
|
4056
|
+
else trend = "flat";
|
|
4057
|
+
}
|
|
4058
|
+
return { current, previous, trend };
|
|
4059
|
+
}
|
|
4060
|
+
var SKIPPED_VITAL = { current: void 0, previous: void 0, trend: null };
|
|
4061
|
+
function toVitalMetric(value, threshold, previousValue, trend) {
|
|
4062
|
+
const base = previousValue !== void 0 ? { value, threshold, status: "unknown", previousValue, trend: trend ?? null } : { value, threshold, status: "unknown" };
|
|
4063
|
+
if (value === void 0) return { ...base, status: "unknown" };
|
|
4064
|
+
if (value > threshold) return { ...base, status: "breach" };
|
|
4065
|
+
if (value > threshold * (1 - WARN_MARGIN)) return { ...base, status: "warn" };
|
|
4066
|
+
return { ...base, status: "ok" };
|
|
4036
4067
|
}
|
|
4037
4068
|
function computeReviewSentiment(reviews, windowDays) {
|
|
4038
4069
|
const now = Date.now();
|
|
@@ -4071,6 +4102,7 @@ function computeReviewSentiment(reviews, windowDays) {
|
|
|
4071
4102
|
}
|
|
4072
4103
|
async function getAppStatus(client, reporting, packageName, options = {}) {
|
|
4073
4104
|
const days = options.days ?? 7;
|
|
4105
|
+
const sections = new Set(options.sections ?? ["releases", "vitals", "reviews"]);
|
|
4074
4106
|
const thresholds = {
|
|
4075
4107
|
crashRate: options.vitalThresholds?.crashRate ?? DEFAULT_THRESHOLDS.crashRate,
|
|
4076
4108
|
anrRate: options.vitalThresholds?.anrRate ?? DEFAULT_THRESHOLDS.anrRate,
|
|
@@ -4078,12 +4110,12 @@ async function getAppStatus(client, reporting, packageName, options = {}) {
|
|
|
4078
4110
|
slowRenderingRate: options.vitalThresholds?.slowRenderingRate ?? DEFAULT_THRESHOLDS.slowRenderingRate
|
|
4079
4111
|
};
|
|
4080
4112
|
const [releasesResult, crashesResult, anrResult, slowStartResult, slowRenderResult, reviewsResult] = await Promise.allSettled([
|
|
4081
|
-
getReleasesStatus(client, packageName),
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
listReviews(client, packageName, { maxResults: 500 })
|
|
4113
|
+
sections.has("releases") ? getReleasesStatus(client, packageName) : Promise.resolve([]),
|
|
4114
|
+
sections.has("vitals") ? queryVitalWithTrend(reporting, packageName, "crashRateMetricSet", days) : Promise.resolve(SKIPPED_VITAL),
|
|
4115
|
+
sections.has("vitals") ? queryVitalWithTrend(reporting, packageName, "anrRateMetricSet", days) : Promise.resolve(SKIPPED_VITAL),
|
|
4116
|
+
sections.has("vitals") ? queryVitalWithTrend(reporting, packageName, "slowStartRateMetricSet", days) : Promise.resolve(SKIPPED_VITAL),
|
|
4117
|
+
sections.has("vitals") ? queryVitalWithTrend(reporting, packageName, "slowRenderingRateMetricSet", days) : Promise.resolve(SKIPPED_VITAL),
|
|
4118
|
+
sections.has("reviews") ? listReviews(client, packageName, { maxResults: 500 }) : Promise.resolve([])
|
|
4087
4119
|
]);
|
|
4088
4120
|
const rawReleases = releasesResult.status === "fulfilled" ? releasesResult.value : [];
|
|
4089
4121
|
const releases = rawReleases.map((r) => ({
|
|
@@ -4092,36 +4124,49 @@ async function getAppStatus(client, reporting, packageName, options = {}) {
|
|
|
4092
4124
|
status: r.status,
|
|
4093
4125
|
userFraction: r.userFraction ?? null
|
|
4094
4126
|
}));
|
|
4095
|
-
const
|
|
4096
|
-
const
|
|
4097
|
-
const
|
|
4098
|
-
const
|
|
4127
|
+
const crashes = crashesResult.status === "fulfilled" ? crashesResult.value : SKIPPED_VITAL;
|
|
4128
|
+
const anr = anrResult.status === "fulfilled" ? anrResult.value : SKIPPED_VITAL;
|
|
4129
|
+
const slowStart = slowStartResult.status === "fulfilled" ? slowStartResult.value : SKIPPED_VITAL;
|
|
4130
|
+
const slowRender = slowRenderResult.status === "fulfilled" ? slowRenderResult.value : SKIPPED_VITAL;
|
|
4099
4131
|
const rawReviews = reviewsResult.status === "fulfilled" ? reviewsResult.value : [];
|
|
4100
4132
|
const reviews = computeReviewSentiment(rawReviews, 30);
|
|
4101
|
-
const fetchedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4102
4133
|
return {
|
|
4103
4134
|
packageName,
|
|
4104
|
-
fetchedAt,
|
|
4135
|
+
fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4105
4136
|
cached: false,
|
|
4106
4137
|
releases,
|
|
4107
4138
|
vitals: {
|
|
4108
4139
|
windowDays: days,
|
|
4109
|
-
crashes: toVitalMetric(
|
|
4110
|
-
anr: toVitalMetric(
|
|
4111
|
-
slowStarts: toVitalMetric(
|
|
4112
|
-
|
|
4140
|
+
crashes: toVitalMetric(crashes.current, thresholds.crashRate, crashes.previous, crashes.trend),
|
|
4141
|
+
anr: toVitalMetric(anr.current, thresholds.anrRate, anr.previous, anr.trend),
|
|
4142
|
+
slowStarts: toVitalMetric(
|
|
4143
|
+
slowStart.current,
|
|
4144
|
+
thresholds.slowStartRate,
|
|
4145
|
+
slowStart.previous,
|
|
4146
|
+
slowStart.trend
|
|
4147
|
+
),
|
|
4148
|
+
slowRender: toVitalMetric(
|
|
4149
|
+
slowRender.current,
|
|
4150
|
+
thresholds.slowRenderingRate,
|
|
4151
|
+
slowRender.previous,
|
|
4152
|
+
slowRender.trend
|
|
4153
|
+
)
|
|
4113
4154
|
},
|
|
4114
4155
|
reviews
|
|
4115
4156
|
};
|
|
4116
4157
|
}
|
|
4117
4158
|
function vitalIndicator(metric) {
|
|
4118
|
-
if (metric.status === "unknown") return "
|
|
4159
|
+
if (metric.status === "unknown") return "\u2014";
|
|
4119
4160
|
if (metric.status === "breach") return "\u2717";
|
|
4120
4161
|
if (metric.status === "warn") return "\u26A0";
|
|
4121
4162
|
return "\u2713";
|
|
4122
4163
|
}
|
|
4164
|
+
function vitalTrendArrow(metric) {
|
|
4165
|
+
if (!metric.trend || metric.trend === "flat") return "";
|
|
4166
|
+
return metric.trend === "up" ? " \u2191" : " \u2193";
|
|
4167
|
+
}
|
|
4123
4168
|
function formatVitalValue(metric) {
|
|
4124
|
-
if (metric.value === void 0) return "
|
|
4169
|
+
if (metric.value === void 0) return "\u2014";
|
|
4125
4170
|
return `${(metric.value * 100).toFixed(2)}%`;
|
|
4126
4171
|
}
|
|
4127
4172
|
function formatFraction(fraction) {
|
|
@@ -4129,7 +4174,7 @@ function formatFraction(fraction) {
|
|
|
4129
4174
|
return `${Math.round(fraction * 100)}%`;
|
|
4130
4175
|
}
|
|
4131
4176
|
function formatRating(rating) {
|
|
4132
|
-
if (rating === void 0) return "
|
|
4177
|
+
if (rating === void 0) return "\u2014";
|
|
4133
4178
|
return `\u2605 ${rating.toFixed(1)}`;
|
|
4134
4179
|
}
|
|
4135
4180
|
function formatTrend(current, previous) {
|
|
@@ -4138,6 +4183,9 @@ function formatTrend(current, previous) {
|
|
|
4138
4183
|
if (current < previous) return ` \u2193 from ${previous.toFixed(1)}`;
|
|
4139
4184
|
return "";
|
|
4140
4185
|
}
|
|
4186
|
+
function allVitalsUnknown(vitals) {
|
|
4187
|
+
return vitals.crashes.status === "unknown" && vitals.anr.status === "unknown" && vitals.slowStarts.status === "unknown" && vitals.slowRender.status === "unknown";
|
|
4188
|
+
}
|
|
4141
4189
|
function formatStatusTable(status) {
|
|
4142
4190
|
const lines = [];
|
|
4143
4191
|
const cachedLabel = status.cached ? ` (cached ${new Date(status.fetchedAt).toLocaleTimeString()})` : ` (fetched ${new Date(status.fetchedAt).toLocaleTimeString()})`;
|
|
@@ -4158,23 +4206,188 @@ function formatStatusTable(status) {
|
|
|
4158
4206
|
}
|
|
4159
4207
|
lines.push("");
|
|
4160
4208
|
lines.push(`VITALS (last ${status.vitals.windowDays} days)`);
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4209
|
+
if (allVitalsUnknown(status.vitals)) {
|
|
4210
|
+
lines.push(" No vitals data available for this period.");
|
|
4211
|
+
} else {
|
|
4212
|
+
const { crashes, anr, slowStarts, slowRender } = status.vitals;
|
|
4213
|
+
const crashVal = `${formatVitalValue(crashes)}${vitalTrendArrow(crashes)}`;
|
|
4214
|
+
const anrVal = `${formatVitalValue(anr)}${vitalTrendArrow(anr)}`;
|
|
4215
|
+
const slowStartVal = `${formatVitalValue(slowStarts)}${vitalTrendArrow(slowStarts)}`;
|
|
4216
|
+
const slowRenderVal = `${formatVitalValue(slowRender)}${vitalTrendArrow(slowRender)}`;
|
|
4217
|
+
lines.push(
|
|
4218
|
+
` crashes ${crashVal.padEnd(10)} ${vitalIndicator(crashes)} anr ${anrVal.padEnd(10)} ${vitalIndicator(anr)}`
|
|
4219
|
+
);
|
|
4220
|
+
lines.push(
|
|
4221
|
+
` slow starts ${slowStartVal.padEnd(10)} ${vitalIndicator(slowStarts)} slow render ${slowRenderVal.padEnd(10)} ${vitalIndicator(slowRender)}`
|
|
4222
|
+
);
|
|
4223
|
+
}
|
|
4168
4224
|
lines.push("");
|
|
4169
4225
|
lines.push(`REVIEWS (last ${status.reviews.windowDays} days)`);
|
|
4170
4226
|
const { averageRating, previousAverageRating, totalNew, positivePercent } = status.reviews;
|
|
4171
|
-
|
|
4172
|
-
|
|
4227
|
+
if (totalNew === 0 && averageRating === void 0) {
|
|
4228
|
+
lines.push(" No reviews in this period.");
|
|
4229
|
+
} else {
|
|
4230
|
+
const trend = formatTrend(averageRating, previousAverageRating);
|
|
4231
|
+
const positiveStr = positivePercent !== void 0 ? ` ${positivePercent}% positive` : "";
|
|
4232
|
+
lines.push(` ${formatRating(averageRating)} ${totalNew} new${positiveStr}${trend}`);
|
|
4233
|
+
}
|
|
4234
|
+
return lines.join("\n");
|
|
4235
|
+
}
|
|
4236
|
+
function formatStatusSummary(status) {
|
|
4237
|
+
const parts = [status.packageName];
|
|
4238
|
+
const latestRelease = status.releases.find((r) => r.status !== "draft") ?? status.releases[0];
|
|
4239
|
+
if (latestRelease) {
|
|
4240
|
+
parts.push(`v${latestRelease.versionCode} ${latestRelease.track}`);
|
|
4241
|
+
}
|
|
4242
|
+
const { crashes, anr } = status.vitals;
|
|
4243
|
+
if (crashes.status !== "unknown") {
|
|
4244
|
+
const arrow = crashes.trend === "up" ? " \u2191" : crashes.trend === "down" ? " \u2193" : "";
|
|
4245
|
+
parts.push(`crashes ${formatVitalValue(crashes)}${arrow} ${vitalIndicator(crashes)}`);
|
|
4246
|
+
}
|
|
4247
|
+
if (anr.status !== "unknown") {
|
|
4248
|
+
const arrow = anr.trend === "up" ? " \u2191" : anr.trend === "down" ? " \u2193" : "";
|
|
4249
|
+
parts.push(`ANR ${formatVitalValue(anr)}${arrow} ${vitalIndicator(anr)}`);
|
|
4250
|
+
}
|
|
4251
|
+
const { averageRating, totalNew } = status.reviews;
|
|
4252
|
+
if (averageRating !== void 0) {
|
|
4253
|
+
parts.push(`avg ${averageRating.toFixed(1)}\u2605`);
|
|
4254
|
+
}
|
|
4255
|
+
if (totalNew > 0) {
|
|
4256
|
+
parts.push(`${totalNew} reviews`);
|
|
4257
|
+
}
|
|
4258
|
+
return parts.join(" \xB7 ") + (statusHasBreach(status) ? " [ALERT]" : "");
|
|
4259
|
+
}
|
|
4260
|
+
function computeStatusDiff(prev, curr) {
|
|
4261
|
+
const prevVersion = prev.releases[0]?.versionCode ?? null;
|
|
4262
|
+
const currVersion = curr.releases[0]?.versionCode ?? null;
|
|
4263
|
+
const prevCrash = prev.vitals.crashes.value ?? null;
|
|
4264
|
+
const currCrash = curr.vitals.crashes.value ?? null;
|
|
4265
|
+
const prevAnr = prev.vitals.anr.value ?? null;
|
|
4266
|
+
const currAnr = curr.vitals.anr.value ?? null;
|
|
4267
|
+
const prevRating = prev.reviews.averageRating ?? null;
|
|
4268
|
+
const currRating = curr.reviews.averageRating ?? null;
|
|
4269
|
+
return {
|
|
4270
|
+
versionCode: { from: prevVersion, to: currVersion },
|
|
4271
|
+
crashRate: {
|
|
4272
|
+
from: prevCrash,
|
|
4273
|
+
to: currCrash,
|
|
4274
|
+
delta: currCrash !== null && prevCrash !== null ? currCrash - prevCrash : null
|
|
4275
|
+
},
|
|
4276
|
+
anrRate: {
|
|
4277
|
+
from: prevAnr,
|
|
4278
|
+
to: currAnr,
|
|
4279
|
+
delta: currAnr !== null && prevAnr !== null ? currAnr - prevAnr : null
|
|
4280
|
+
},
|
|
4281
|
+
reviewCount: { from: prev.reviews.totalNew, to: curr.reviews.totalNew },
|
|
4282
|
+
averageRating: {
|
|
4283
|
+
from: prevRating,
|
|
4284
|
+
to: currRating,
|
|
4285
|
+
delta: currRating !== null && prevRating !== null ? Math.round((currRating - prevRating) * 10) / 10 : null
|
|
4286
|
+
}
|
|
4287
|
+
};
|
|
4288
|
+
}
|
|
4289
|
+
function formatStatusDiff(diff, since) {
|
|
4290
|
+
const lines = [`Changes since ${since}:`];
|
|
4291
|
+
if (diff.versionCode.from !== diff.versionCode.to) {
|
|
4292
|
+
lines.push(` Version: ${diff.versionCode.from ?? "\u2014"} \u2192 ${diff.versionCode.to ?? "\u2014"}`);
|
|
4293
|
+
}
|
|
4294
|
+
const fmtRate = (v) => v !== null ? `${(v * 100).toFixed(2)}%` : "\u2014";
|
|
4295
|
+
const fmtDelta = (d, lowerIsBetter = true) => {
|
|
4296
|
+
if (d === null || Math.abs(d) < 1e-4) return "no change";
|
|
4297
|
+
const sign = d > 0 ? "+" : "";
|
|
4298
|
+
const good = lowerIsBetter ? d < 0 : d > 0;
|
|
4299
|
+
return `${sign}${(d * 100).toFixed(2)}% ${good ? "\u2713" : "\u2717"}`;
|
|
4300
|
+
};
|
|
4301
|
+
lines.push(
|
|
4302
|
+
` Crash rate: ${fmtRate(diff.crashRate.from)} \u2192 ${fmtRate(diff.crashRate.to)} (${fmtDelta(diff.crashRate.delta)})`
|
|
4303
|
+
);
|
|
4173
4304
|
lines.push(
|
|
4174
|
-
` ${
|
|
4305
|
+
` ANR rate: ${fmtRate(diff.anrRate.from)} \u2192 ${fmtRate(diff.anrRate.to)} (${fmtDelta(diff.anrRate.delta)})`
|
|
4175
4306
|
);
|
|
4307
|
+
const ratingDelta = diff.averageRating.delta;
|
|
4308
|
+
const prevR = diff.averageRating.from !== null ? `${diff.averageRating.from.toFixed(1)}\u2605` : "\u2014";
|
|
4309
|
+
const currR = diff.averageRating.to !== null ? `${diff.averageRating.to.toFixed(1)}\u2605` : "\u2014";
|
|
4310
|
+
const ratingStr = ratingDelta === null || Math.abs(ratingDelta) < 0.05 ? "no change" : `${ratingDelta > 0 ? "+" : ""}${ratingDelta.toFixed(1)} ${ratingDelta > 0 ? "\u2713" : "\u2717"}`;
|
|
4311
|
+
lines.push(` Reviews: ${prevR} \u2192 ${currR} (${ratingStr})`);
|
|
4176
4312
|
return lines.join("\n");
|
|
4177
4313
|
}
|
|
4314
|
+
async function runWatchLoop(opts) {
|
|
4315
|
+
if (opts.intervalSeconds < 10) {
|
|
4316
|
+
console.error("Error: --watch interval must be at least 10 seconds");
|
|
4317
|
+
process.exit(2);
|
|
4318
|
+
}
|
|
4319
|
+
let running = true;
|
|
4320
|
+
const cleanup = () => {
|
|
4321
|
+
running = false;
|
|
4322
|
+
process.stdout.write("\n");
|
|
4323
|
+
process.exit(0);
|
|
4324
|
+
};
|
|
4325
|
+
process.on("SIGINT", cleanup);
|
|
4326
|
+
process.on("SIGTERM", cleanup);
|
|
4327
|
+
while (running) {
|
|
4328
|
+
process.stdout.write("\x1B[2J\x1B[H");
|
|
4329
|
+
try {
|
|
4330
|
+
const status = await opts.fetch();
|
|
4331
|
+
await opts.save(status);
|
|
4332
|
+
console.log(opts.render(status));
|
|
4333
|
+
} catch (err) {
|
|
4334
|
+
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
4335
|
+
}
|
|
4336
|
+
console.log(`
|
|
4337
|
+
[gpc status] Refreshing in ${opts.intervalSeconds}s\u2026 (Ctrl+C to stop)`);
|
|
4338
|
+
for (let i = 0; i < opts.intervalSeconds && running; i++) {
|
|
4339
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
4340
|
+
}
|
|
4341
|
+
}
|
|
4342
|
+
}
|
|
4343
|
+
function breachStateFilePath(packageName) {
|
|
4344
|
+
return join7(getCacheDir(), `breach-state-${packageName}.json`);
|
|
4345
|
+
}
|
|
4346
|
+
async function trackBreachState(packageName, isBreaching) {
|
|
4347
|
+
const filePath = breachStateFilePath(packageName);
|
|
4348
|
+
let prevBreaching = false;
|
|
4349
|
+
try {
|
|
4350
|
+
const raw = await readFile10(filePath, "utf-8");
|
|
4351
|
+
prevBreaching = JSON.parse(raw).breaching;
|
|
4352
|
+
} catch {
|
|
4353
|
+
}
|
|
4354
|
+
if (prevBreaching !== isBreaching) {
|
|
4355
|
+
try {
|
|
4356
|
+
await mkdir5(getCacheDir(), { recursive: true });
|
|
4357
|
+
await writeFile7(
|
|
4358
|
+
filePath,
|
|
4359
|
+
JSON.stringify({ breaching: isBreaching, since: (/* @__PURE__ */ new Date()).toISOString() }, null, 2),
|
|
4360
|
+
{ encoding: "utf-8", mode: 384 }
|
|
4361
|
+
);
|
|
4362
|
+
} catch {
|
|
4363
|
+
}
|
|
4364
|
+
return true;
|
|
4365
|
+
}
|
|
4366
|
+
return false;
|
|
4367
|
+
}
|
|
4368
|
+
function sendNotification(title, body) {
|
|
4369
|
+
if (process.env["CI"]) return;
|
|
4370
|
+
try {
|
|
4371
|
+
const p = process.platform;
|
|
4372
|
+
if (p === "darwin") {
|
|
4373
|
+
execSync(
|
|
4374
|
+
`osascript -e 'display notification ${JSON.stringify(body)} with title ${JSON.stringify(title)}'`,
|
|
4375
|
+
{ stdio: "ignore" }
|
|
4376
|
+
);
|
|
4377
|
+
} else if (p === "linux") {
|
|
4378
|
+
execSync(`notify-send ${JSON.stringify(title)} ${JSON.stringify(body)}`, {
|
|
4379
|
+
stdio: "ignore"
|
|
4380
|
+
});
|
|
4381
|
+
} else if (p === "win32") {
|
|
4382
|
+
const escaped = (s) => s.replace(/'/g, "''");
|
|
4383
|
+
execSync(
|
|
4384
|
+
`powershell -Command "Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.MessageBox]::Show('${escaped(body)}', '${escaped(title)}')"`,
|
|
4385
|
+
{ stdio: "ignore" }
|
|
4386
|
+
);
|
|
4387
|
+
}
|
|
4388
|
+
} catch {
|
|
4389
|
+
}
|
|
4390
|
+
}
|
|
4178
4391
|
function statusHasBreach(status) {
|
|
4179
4392
|
return status.vitals.crashes.status === "breach" || status.vitals.anr.status === "breach" || status.vitals.slowStarts.status === "breach" || status.vitals.slowRender.status === "breach";
|
|
4180
4393
|
}
|
|
@@ -4202,6 +4415,7 @@ export {
|
|
|
4202
4415
|
clearAuditLog,
|
|
4203
4416
|
compareBundles,
|
|
4204
4417
|
compareVitalsTrend,
|
|
4418
|
+
computeStatusDiff,
|
|
4205
4419
|
consumeProductPurchase,
|
|
4206
4420
|
convertRegionPrices,
|
|
4207
4421
|
createAuditEntry,
|
|
@@ -4247,6 +4461,8 @@ export {
|
|
|
4247
4461
|
formatJunit,
|
|
4248
4462
|
formatOutput,
|
|
4249
4463
|
formatSlackPayload,
|
|
4464
|
+
formatStatusDiff,
|
|
4465
|
+
formatStatusSummary,
|
|
4250
4466
|
formatStatusTable,
|
|
4251
4467
|
generateMigrationPlan,
|
|
4252
4468
|
generateNotesFromGit,
|
|
@@ -4322,16 +4538,19 @@ export {
|
|
|
4322
4538
|
removeUser,
|
|
4323
4539
|
replyToReview,
|
|
4324
4540
|
revokeSubscriptionPurchase,
|
|
4541
|
+
runWatchLoop,
|
|
4325
4542
|
safePath,
|
|
4326
4543
|
safePathWithin,
|
|
4327
4544
|
saveStatusCache,
|
|
4328
4545
|
scaffoldPlugin,
|
|
4329
4546
|
searchAuditEvents,
|
|
4330
4547
|
searchVitalsErrors,
|
|
4548
|
+
sendNotification,
|
|
4331
4549
|
sendWebhook,
|
|
4332
4550
|
sortResults,
|
|
4333
4551
|
statusHasBreach,
|
|
4334
4552
|
syncInAppProducts,
|
|
4553
|
+
trackBreachState,
|
|
4335
4554
|
updateAppDetails,
|
|
4336
4555
|
updateDataSafety,
|
|
4337
4556
|
updateInAppProduct,
|