@gpc-cli/core 0.9.23 → 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 +256 -55
- 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;
|
|
@@ -3183,23 +3183,23 @@ async function deleteOneTimeOffer(client, packageName, productId, offerId) {
|
|
|
3183
3183
|
}
|
|
3184
3184
|
|
|
3185
3185
|
// src/utils/spinner.ts
|
|
3186
|
-
import
|
|
3186
|
+
import process3 from "process";
|
|
3187
3187
|
var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
3188
3188
|
var INTERVAL_MS = 80;
|
|
3189
3189
|
function createSpinner(message) {
|
|
3190
|
-
const isTTY =
|
|
3190
|
+
const isTTY = process3.stderr.isTTY === true;
|
|
3191
3191
|
let frameIndex = 0;
|
|
3192
3192
|
let timer;
|
|
3193
3193
|
let currentMessage = message;
|
|
3194
3194
|
let started = false;
|
|
3195
3195
|
function clearLine() {
|
|
3196
3196
|
if (isTTY) {
|
|
3197
|
-
|
|
3197
|
+
process3.stderr.write("\r\x1B[K");
|
|
3198
3198
|
}
|
|
3199
3199
|
}
|
|
3200
3200
|
function renderFrame() {
|
|
3201
3201
|
const frame = FRAMES[frameIndex % FRAMES.length];
|
|
3202
|
-
|
|
3202
|
+
process3.stderr.write(`\r\x1B[K${frame} ${currentMessage}`);
|
|
3203
3203
|
frameIndex++;
|
|
3204
3204
|
}
|
|
3205
3205
|
return {
|
|
@@ -3207,7 +3207,7 @@ function createSpinner(message) {
|
|
|
3207
3207
|
if (started) return;
|
|
3208
3208
|
started = true;
|
|
3209
3209
|
if (!isTTY) {
|
|
3210
|
-
|
|
3210
|
+
process3.stderr.write(`${currentMessage}
|
|
3211
3211
|
`);
|
|
3212
3212
|
return;
|
|
3213
3213
|
}
|
|
@@ -3222,10 +3222,10 @@ function createSpinner(message) {
|
|
|
3222
3222
|
const text = msg ?? currentMessage;
|
|
3223
3223
|
if (isTTY) {
|
|
3224
3224
|
clearLine();
|
|
3225
|
-
|
|
3225
|
+
process3.stderr.write(`\u2714 ${text}
|
|
3226
3226
|
`);
|
|
3227
3227
|
} else if (!started) {
|
|
3228
|
-
|
|
3228
|
+
process3.stderr.write(`${text}
|
|
3229
3229
|
`);
|
|
3230
3230
|
}
|
|
3231
3231
|
started = false;
|
|
@@ -3238,10 +3238,10 @@ function createSpinner(message) {
|
|
|
3238
3238
|
const text = msg ?? currentMessage;
|
|
3239
3239
|
if (isTTY) {
|
|
3240
3240
|
clearLine();
|
|
3241
|
-
|
|
3241
|
+
process3.stderr.write(`\u2718 ${text}
|
|
3242
3242
|
`);
|
|
3243
3243
|
} else if (!started) {
|
|
3244
|
-
|
|
3244
|
+
process3.stderr.write(`${text}
|
|
3245
3245
|
`);
|
|
3246
3246
|
}
|
|
3247
3247
|
started = false;
|
|
@@ -3975,6 +3975,7 @@ function compareBundles(before, after) {
|
|
|
3975
3975
|
|
|
3976
3976
|
// src/commands/status.ts
|
|
3977
3977
|
import { mkdir as mkdir5, readFile as readFile10, writeFile as writeFile7 } from "fs/promises";
|
|
3978
|
+
import { execSync } from "child_process";
|
|
3978
3979
|
import { join as join7 } from "path";
|
|
3979
3980
|
import { getCacheDir } from "@gpc-cli/config";
|
|
3980
3981
|
var DEFAULT_TTL_SECONDS = 3600;
|
|
@@ -3996,12 +3997,11 @@ async function saveStatusCache(packageName, data, ttlSeconds = DEFAULT_TTL_SECON
|
|
|
3996
3997
|
try {
|
|
3997
3998
|
const dir = getCacheDir();
|
|
3998
3999
|
await mkdir5(dir, { recursive: true });
|
|
3999
|
-
const entry = {
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
};
|
|
4004
|
-
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
|
+
});
|
|
4005
4005
|
} catch {
|
|
4006
4006
|
}
|
|
4007
4007
|
}
|
|
@@ -4021,9 +4021,9 @@ var WARN_MARGIN = 0.2;
|
|
|
4021
4021
|
function toApiDate(d) {
|
|
4022
4022
|
return { year: d.getUTCFullYear(), month: d.getUTCMonth() + 1, day: d.getUTCDate() };
|
|
4023
4023
|
}
|
|
4024
|
-
async function queryVitalForStatus(reporting, packageName, metricSet, days) {
|
|
4024
|
+
async function queryVitalForStatus(reporting, packageName, metricSet, days, offsetDays = 0) {
|
|
4025
4025
|
const DAY_MS = 24 * 60 * 60 * 1e3;
|
|
4026
|
-
const baseMs = Date.now() - 2 * DAY_MS;
|
|
4026
|
+
const baseMs = Date.now() - 2 * DAY_MS - offsetDays * DAY_MS;
|
|
4027
4027
|
const end = new Date(baseMs);
|
|
4028
4028
|
const start = new Date(baseMs - days * DAY_MS);
|
|
4029
4029
|
const metrics = METRIC_SET_METRICS2[metricSet] ?? ["distinctUsers"];
|
|
@@ -4044,13 +4044,26 @@ async function queryVitalForStatus(reporting, packageName, metricSet, days) {
|
|
|
4044
4044
|
if (values.length === 0) return void 0;
|
|
4045
4045
|
return values.reduce((a, b) => a + b, 0) / values.length;
|
|
4046
4046
|
}
|
|
4047
|
-
function
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
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" };
|
|
4054
4067
|
}
|
|
4055
4068
|
function computeReviewSentiment(reviews, windowDays) {
|
|
4056
4069
|
const now = Date.now();
|
|
@@ -4089,6 +4102,7 @@ function computeReviewSentiment(reviews, windowDays) {
|
|
|
4089
4102
|
}
|
|
4090
4103
|
async function getAppStatus(client, reporting, packageName, options = {}) {
|
|
4091
4104
|
const days = options.days ?? 7;
|
|
4105
|
+
const sections = new Set(options.sections ?? ["releases", "vitals", "reviews"]);
|
|
4092
4106
|
const thresholds = {
|
|
4093
4107
|
crashRate: options.vitalThresholds?.crashRate ?? DEFAULT_THRESHOLDS.crashRate,
|
|
4094
4108
|
anrRate: options.vitalThresholds?.anrRate ?? DEFAULT_THRESHOLDS.anrRate,
|
|
@@ -4096,12 +4110,12 @@ async function getAppStatus(client, reporting, packageName, options = {}) {
|
|
|
4096
4110
|
slowRenderingRate: options.vitalThresholds?.slowRenderingRate ?? DEFAULT_THRESHOLDS.slowRenderingRate
|
|
4097
4111
|
};
|
|
4098
4112
|
const [releasesResult, crashesResult, anrResult, slowStartResult, slowRenderResult, reviewsResult] = await Promise.allSettled([
|
|
4099
|
-
getReleasesStatus(client, packageName),
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
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([])
|
|
4105
4119
|
]);
|
|
4106
4120
|
const rawReleases = releasesResult.status === "fulfilled" ? releasesResult.value : [];
|
|
4107
4121
|
const releases = rawReleases.map((r) => ({
|
|
@@ -4110,36 +4124,49 @@ async function getAppStatus(client, reporting, packageName, options = {}) {
|
|
|
4110
4124
|
status: r.status,
|
|
4111
4125
|
userFraction: r.userFraction ?? null
|
|
4112
4126
|
}));
|
|
4113
|
-
const
|
|
4114
|
-
const
|
|
4115
|
-
const
|
|
4116
|
-
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;
|
|
4117
4131
|
const rawReviews = reviewsResult.status === "fulfilled" ? reviewsResult.value : [];
|
|
4118
4132
|
const reviews = computeReviewSentiment(rawReviews, 30);
|
|
4119
|
-
const fetchedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4120
4133
|
return {
|
|
4121
4134
|
packageName,
|
|
4122
|
-
fetchedAt,
|
|
4135
|
+
fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4123
4136
|
cached: false,
|
|
4124
4137
|
releases,
|
|
4125
4138
|
vitals: {
|
|
4126
4139
|
windowDays: days,
|
|
4127
|
-
crashes: toVitalMetric(
|
|
4128
|
-
anr: toVitalMetric(
|
|
4129
|
-
slowStarts: toVitalMetric(
|
|
4130
|
-
|
|
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
|
+
)
|
|
4131
4154
|
},
|
|
4132
4155
|
reviews
|
|
4133
4156
|
};
|
|
4134
4157
|
}
|
|
4135
4158
|
function vitalIndicator(metric) {
|
|
4136
|
-
if (metric.status === "unknown") return "
|
|
4159
|
+
if (metric.status === "unknown") return "\u2014";
|
|
4137
4160
|
if (metric.status === "breach") return "\u2717";
|
|
4138
4161
|
if (metric.status === "warn") return "\u26A0";
|
|
4139
4162
|
return "\u2713";
|
|
4140
4163
|
}
|
|
4164
|
+
function vitalTrendArrow(metric) {
|
|
4165
|
+
if (!metric.trend || metric.trend === "flat") return "";
|
|
4166
|
+
return metric.trend === "up" ? " \u2191" : " \u2193";
|
|
4167
|
+
}
|
|
4141
4168
|
function formatVitalValue(metric) {
|
|
4142
|
-
if (metric.value === void 0) return "
|
|
4169
|
+
if (metric.value === void 0) return "\u2014";
|
|
4143
4170
|
return `${(metric.value * 100).toFixed(2)}%`;
|
|
4144
4171
|
}
|
|
4145
4172
|
function formatFraction(fraction) {
|
|
@@ -4147,7 +4174,7 @@ function formatFraction(fraction) {
|
|
|
4147
4174
|
return `${Math.round(fraction * 100)}%`;
|
|
4148
4175
|
}
|
|
4149
4176
|
function formatRating(rating) {
|
|
4150
|
-
if (rating === void 0) return "
|
|
4177
|
+
if (rating === void 0) return "\u2014";
|
|
4151
4178
|
return `\u2605 ${rating.toFixed(1)}`;
|
|
4152
4179
|
}
|
|
4153
4180
|
function formatTrend(current, previous) {
|
|
@@ -4156,6 +4183,9 @@ function formatTrend(current, previous) {
|
|
|
4156
4183
|
if (current < previous) return ` \u2193 from ${previous.toFixed(1)}`;
|
|
4157
4184
|
return "";
|
|
4158
4185
|
}
|
|
4186
|
+
function allVitalsUnknown(vitals) {
|
|
4187
|
+
return vitals.crashes.status === "unknown" && vitals.anr.status === "unknown" && vitals.slowStarts.status === "unknown" && vitals.slowRender.status === "unknown";
|
|
4188
|
+
}
|
|
4159
4189
|
function formatStatusTable(status) {
|
|
4160
4190
|
const lines = [];
|
|
4161
4191
|
const cachedLabel = status.cached ? ` (cached ${new Date(status.fetchedAt).toLocaleTimeString()})` : ` (fetched ${new Date(status.fetchedAt).toLocaleTimeString()})`;
|
|
@@ -4176,23 +4206,188 @@ function formatStatusTable(status) {
|
|
|
4176
4206
|
}
|
|
4177
4207
|
lines.push("");
|
|
4178
4208
|
lines.push(`VITALS (last ${status.vitals.windowDays} days)`);
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
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
|
+
}
|
|
4186
4224
|
lines.push("");
|
|
4187
4225
|
lines.push(`REVIEWS (last ${status.reviews.windowDays} days)`);
|
|
4188
4226
|
const { averageRating, previousAverageRating, totalNew, positivePercent } = status.reviews;
|
|
4189
|
-
|
|
4190
|
-
|
|
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
|
+
};
|
|
4191
4301
|
lines.push(
|
|
4192
|
-
` ${
|
|
4302
|
+
` Crash rate: ${fmtRate(diff.crashRate.from)} \u2192 ${fmtRate(diff.crashRate.to)} (${fmtDelta(diff.crashRate.delta)})`
|
|
4193
4303
|
);
|
|
4304
|
+
lines.push(
|
|
4305
|
+
` ANR rate: ${fmtRate(diff.anrRate.from)} \u2192 ${fmtRate(diff.anrRate.to)} (${fmtDelta(diff.anrRate.delta)})`
|
|
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})`);
|
|
4194
4312
|
return lines.join("\n");
|
|
4195
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
|
+
}
|
|
4196
4391
|
function statusHasBreach(status) {
|
|
4197
4392
|
return status.vitals.crashes.status === "breach" || status.vitals.anr.status === "breach" || status.vitals.slowStarts.status === "breach" || status.vitals.slowRender.status === "breach";
|
|
4198
4393
|
}
|
|
@@ -4220,6 +4415,7 @@ export {
|
|
|
4220
4415
|
clearAuditLog,
|
|
4221
4416
|
compareBundles,
|
|
4222
4417
|
compareVitalsTrend,
|
|
4418
|
+
computeStatusDiff,
|
|
4223
4419
|
consumeProductPurchase,
|
|
4224
4420
|
convertRegionPrices,
|
|
4225
4421
|
createAuditEntry,
|
|
@@ -4265,6 +4461,8 @@ export {
|
|
|
4265
4461
|
formatJunit,
|
|
4266
4462
|
formatOutput,
|
|
4267
4463
|
formatSlackPayload,
|
|
4464
|
+
formatStatusDiff,
|
|
4465
|
+
formatStatusSummary,
|
|
4268
4466
|
formatStatusTable,
|
|
4269
4467
|
generateMigrationPlan,
|
|
4270
4468
|
generateNotesFromGit,
|
|
@@ -4340,16 +4538,19 @@ export {
|
|
|
4340
4538
|
removeUser,
|
|
4341
4539
|
replyToReview,
|
|
4342
4540
|
revokeSubscriptionPurchase,
|
|
4541
|
+
runWatchLoop,
|
|
4343
4542
|
safePath,
|
|
4344
4543
|
safePathWithin,
|
|
4345
4544
|
saveStatusCache,
|
|
4346
4545
|
scaffoldPlugin,
|
|
4347
4546
|
searchAuditEvents,
|
|
4348
4547
|
searchVitalsErrors,
|
|
4548
|
+
sendNotification,
|
|
4349
4549
|
sendWebhook,
|
|
4350
4550
|
sortResults,
|
|
4351
4551
|
statusHasBreach,
|
|
4352
4552
|
syncInAppProducts,
|
|
4553
|
+
trackBreachState,
|
|
4353
4554
|
updateAppDetails,
|
|
4354
4555
|
updateDataSafety,
|
|
4355
4556
|
updateInAppProduct,
|