@gpc-cli/core 0.9.23 → 0.9.25
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 +43 -1
- package/dist/index.js +283 -69
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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;
|
|
@@ -755,6 +757,7 @@ interface AppStatus {
|
|
|
755
757
|
packageName: string;
|
|
756
758
|
fetchedAt: string;
|
|
757
759
|
cached: boolean;
|
|
760
|
+
sections: string[];
|
|
758
761
|
releases: StatusRelease[];
|
|
759
762
|
vitals: {
|
|
760
763
|
windowDays: number;
|
|
@@ -765,8 +768,34 @@ interface AppStatus {
|
|
|
765
768
|
};
|
|
766
769
|
reviews: StatusReviews;
|
|
767
770
|
}
|
|
771
|
+
interface StatusDiff {
|
|
772
|
+
versionCode: {
|
|
773
|
+
from: string | null;
|
|
774
|
+
to: string | null;
|
|
775
|
+
};
|
|
776
|
+
crashRate: {
|
|
777
|
+
from: number | null;
|
|
778
|
+
to: number | null;
|
|
779
|
+
delta: number | null;
|
|
780
|
+
};
|
|
781
|
+
anrRate: {
|
|
782
|
+
from: number | null;
|
|
783
|
+
to: number | null;
|
|
784
|
+
delta: number | null;
|
|
785
|
+
};
|
|
786
|
+
reviewCount: {
|
|
787
|
+
from: number | null;
|
|
788
|
+
to: number | null;
|
|
789
|
+
};
|
|
790
|
+
averageRating: {
|
|
791
|
+
from: number | null;
|
|
792
|
+
to: number | null;
|
|
793
|
+
delta: number | null;
|
|
794
|
+
};
|
|
795
|
+
}
|
|
768
796
|
interface GetAppStatusOptions {
|
|
769
797
|
days?: number;
|
|
798
|
+
sections?: string[];
|
|
770
799
|
vitalThresholds?: {
|
|
771
800
|
crashRate?: number;
|
|
772
801
|
anrRate?: number;
|
|
@@ -774,10 +803,23 @@ interface GetAppStatusOptions {
|
|
|
774
803
|
slowRenderingRate?: number;
|
|
775
804
|
};
|
|
776
805
|
}
|
|
806
|
+
interface WatchOptions {
|
|
807
|
+
intervalSeconds: number;
|
|
808
|
+
render: (status: AppStatus) => string;
|
|
809
|
+
fetch: () => Promise<AppStatus>;
|
|
810
|
+
save: (status: AppStatus) => Promise<void>;
|
|
811
|
+
}
|
|
777
812
|
declare function loadStatusCache(packageName: string, ttlSeconds?: number): Promise<AppStatus | null>;
|
|
778
813
|
declare function saveStatusCache(packageName: string, data: AppStatus, ttlSeconds?: number): Promise<void>;
|
|
779
814
|
declare function getAppStatus(client: PlayApiClient, reporting: ReportingApiClient, packageName: string, options?: GetAppStatusOptions): Promise<AppStatus>;
|
|
780
815
|
declare function formatStatusTable(status: AppStatus): string;
|
|
816
|
+
declare function formatStatusSummary(status: AppStatus): string;
|
|
817
|
+
declare function computeStatusDiff(prev: AppStatus, curr: AppStatus): StatusDiff;
|
|
818
|
+
declare function formatStatusDiff(diff: StatusDiff, since: string): string;
|
|
819
|
+
declare function runWatchLoop(opts: WatchOptions): Promise<void>;
|
|
820
|
+
/** Returns true if breach state changed (breach started or cleared). */
|
|
821
|
+
declare function trackBreachState(packageName: string, isBreaching: boolean): Promise<boolean>;
|
|
822
|
+
declare function sendNotification(title: string, body: string): void;
|
|
781
823
|
declare function statusHasBreach(status: AppStatus): boolean;
|
|
782
824
|
|
|
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 };
|
|
825
|
+
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;
|
|
@@ -3987,7 +3988,12 @@ async function loadStatusCache(packageName, ttlSeconds = DEFAULT_TTL_SECONDS) {
|
|
|
3987
3988
|
const entry = JSON.parse(raw);
|
|
3988
3989
|
const age = (Date.now() - new Date(entry.fetchedAt).getTime()) / 1e3;
|
|
3989
3990
|
if (age > (entry.ttl ?? ttlSeconds)) return null;
|
|
3990
|
-
|
|
3991
|
+
const data = entry.data;
|
|
3992
|
+
return {
|
|
3993
|
+
...data,
|
|
3994
|
+
sections: data.sections ?? ["releases", "vitals", "reviews"],
|
|
3995
|
+
cached: true
|
|
3996
|
+
};
|
|
3991
3997
|
} catch {
|
|
3992
3998
|
return null;
|
|
3993
3999
|
}
|
|
@@ -3996,12 +4002,11 @@ async function saveStatusCache(packageName, data, ttlSeconds = DEFAULT_TTL_SECON
|
|
|
3996
4002
|
try {
|
|
3997
4003
|
const dir = getCacheDir();
|
|
3998
4004
|
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 });
|
|
4005
|
+
const entry = { fetchedAt: data.fetchedAt, ttl: ttlSeconds, data };
|
|
4006
|
+
await writeFile7(cacheFilePath(packageName), JSON.stringify(entry, null, 2), {
|
|
4007
|
+
encoding: "utf-8",
|
|
4008
|
+
mode: 384
|
|
4009
|
+
});
|
|
4005
4010
|
} catch {
|
|
4006
4011
|
}
|
|
4007
4012
|
}
|
|
@@ -4021,9 +4026,9 @@ var WARN_MARGIN = 0.2;
|
|
|
4021
4026
|
function toApiDate(d) {
|
|
4022
4027
|
return { year: d.getUTCFullYear(), month: d.getUTCMonth() + 1, day: d.getUTCDate() };
|
|
4023
4028
|
}
|
|
4024
|
-
async function queryVitalForStatus(reporting, packageName, metricSet, days) {
|
|
4029
|
+
async function queryVitalForStatus(reporting, packageName, metricSet, days, offsetDays = 0) {
|
|
4025
4030
|
const DAY_MS = 24 * 60 * 60 * 1e3;
|
|
4026
|
-
const baseMs = Date.now() - 2 * DAY_MS;
|
|
4031
|
+
const baseMs = Date.now() - 2 * DAY_MS - offsetDays * DAY_MS;
|
|
4027
4032
|
const end = new Date(baseMs);
|
|
4028
4033
|
const start = new Date(baseMs - days * DAY_MS);
|
|
4029
4034
|
const metrics = METRIC_SET_METRICS2[metricSet] ?? ["distinctUsers"];
|
|
@@ -4044,13 +4049,26 @@ async function queryVitalForStatus(reporting, packageName, metricSet, days) {
|
|
|
4044
4049
|
if (values.length === 0) return void 0;
|
|
4045
4050
|
return values.reduce((a, b) => a + b, 0) / values.length;
|
|
4046
4051
|
}
|
|
4047
|
-
function
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4052
|
+
async function queryVitalWithTrend(reporting, packageName, metricSet, days) {
|
|
4053
|
+
const [current, previous] = await Promise.all([
|
|
4054
|
+
queryVitalForStatus(reporting, packageName, metricSet, days, 0),
|
|
4055
|
+
queryVitalForStatus(reporting, packageName, metricSet, days, days)
|
|
4056
|
+
]);
|
|
4057
|
+
let trend = null;
|
|
4058
|
+
if (current !== void 0 && previous !== void 0) {
|
|
4059
|
+
if (current > previous) trend = "up";
|
|
4060
|
+
else if (current < previous) trend = "down";
|
|
4061
|
+
else trend = "flat";
|
|
4062
|
+
}
|
|
4063
|
+
return { current, previous, trend };
|
|
4064
|
+
}
|
|
4065
|
+
var SKIPPED_VITAL = { current: void 0, previous: void 0, trend: null };
|
|
4066
|
+
function toVitalMetric(value, threshold, previousValue, trend) {
|
|
4067
|
+
const base = previousValue !== void 0 ? { value, threshold, status: "unknown", previousValue, trend: trend ?? null } : { value, threshold, status: "unknown" };
|
|
4068
|
+
if (value === void 0) return { ...base, status: "unknown" };
|
|
4069
|
+
if (value > threshold) return { ...base, status: "breach" };
|
|
4070
|
+
if (value > threshold * (1 - WARN_MARGIN)) return { ...base, status: "warn" };
|
|
4071
|
+
return { ...base, status: "ok" };
|
|
4054
4072
|
}
|
|
4055
4073
|
function computeReviewSentiment(reviews, windowDays) {
|
|
4056
4074
|
const now = Date.now();
|
|
@@ -4089,6 +4107,7 @@ function computeReviewSentiment(reviews, windowDays) {
|
|
|
4089
4107
|
}
|
|
4090
4108
|
async function getAppStatus(client, reporting, packageName, options = {}) {
|
|
4091
4109
|
const days = options.days ?? 7;
|
|
4110
|
+
const sections = new Set(options.sections ?? ["releases", "vitals", "reviews"]);
|
|
4092
4111
|
const thresholds = {
|
|
4093
4112
|
crashRate: options.vitalThresholds?.crashRate ?? DEFAULT_THRESHOLDS.crashRate,
|
|
4094
4113
|
anrRate: options.vitalThresholds?.anrRate ?? DEFAULT_THRESHOLDS.anrRate,
|
|
@@ -4096,12 +4115,12 @@ async function getAppStatus(client, reporting, packageName, options = {}) {
|
|
|
4096
4115
|
slowRenderingRate: options.vitalThresholds?.slowRenderingRate ?? DEFAULT_THRESHOLDS.slowRenderingRate
|
|
4097
4116
|
};
|
|
4098
4117
|
const [releasesResult, crashesResult, anrResult, slowStartResult, slowRenderResult, reviewsResult] = await Promise.allSettled([
|
|
4099
|
-
getReleasesStatus(client, packageName),
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
listReviews(client, packageName, { maxResults: 500 })
|
|
4118
|
+
sections.has("releases") ? getReleasesStatus(client, packageName) : Promise.resolve([]),
|
|
4119
|
+
sections.has("vitals") ? queryVitalWithTrend(reporting, packageName, "crashRateMetricSet", days) : Promise.resolve(SKIPPED_VITAL),
|
|
4120
|
+
sections.has("vitals") ? queryVitalWithTrend(reporting, packageName, "anrRateMetricSet", days) : Promise.resolve(SKIPPED_VITAL),
|
|
4121
|
+
sections.has("vitals") ? queryVitalWithTrend(reporting, packageName, "slowStartRateMetricSet", days) : Promise.resolve(SKIPPED_VITAL),
|
|
4122
|
+
sections.has("vitals") ? queryVitalWithTrend(reporting, packageName, "slowRenderingRateMetricSet", days) : Promise.resolve(SKIPPED_VITAL),
|
|
4123
|
+
sections.has("reviews") ? listReviews(client, packageName, { maxResults: 500 }) : Promise.resolve([])
|
|
4105
4124
|
]);
|
|
4106
4125
|
const rawReleases = releasesResult.status === "fulfilled" ? releasesResult.value : [];
|
|
4107
4126
|
const releases = rawReleases.map((r) => ({
|
|
@@ -4110,36 +4129,50 @@ async function getAppStatus(client, reporting, packageName, options = {}) {
|
|
|
4110
4129
|
status: r.status,
|
|
4111
4130
|
userFraction: r.userFraction ?? null
|
|
4112
4131
|
}));
|
|
4113
|
-
const
|
|
4114
|
-
const
|
|
4115
|
-
const
|
|
4116
|
-
const
|
|
4132
|
+
const crashes = crashesResult.status === "fulfilled" ? crashesResult.value : SKIPPED_VITAL;
|
|
4133
|
+
const anr = anrResult.status === "fulfilled" ? anrResult.value : SKIPPED_VITAL;
|
|
4134
|
+
const slowStart = slowStartResult.status === "fulfilled" ? slowStartResult.value : SKIPPED_VITAL;
|
|
4135
|
+
const slowRender = slowRenderResult.status === "fulfilled" ? slowRenderResult.value : SKIPPED_VITAL;
|
|
4117
4136
|
const rawReviews = reviewsResult.status === "fulfilled" ? reviewsResult.value : [];
|
|
4118
4137
|
const reviews = computeReviewSentiment(rawReviews, 30);
|
|
4119
|
-
const fetchedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4120
4138
|
return {
|
|
4121
4139
|
packageName,
|
|
4122
|
-
fetchedAt,
|
|
4140
|
+
fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4123
4141
|
cached: false,
|
|
4142
|
+
sections: [...sections],
|
|
4124
4143
|
releases,
|
|
4125
4144
|
vitals: {
|
|
4126
4145
|
windowDays: days,
|
|
4127
|
-
crashes: toVitalMetric(
|
|
4128
|
-
anr: toVitalMetric(
|
|
4129
|
-
slowStarts: toVitalMetric(
|
|
4130
|
-
|
|
4146
|
+
crashes: toVitalMetric(crashes.current, thresholds.crashRate, crashes.previous, crashes.trend),
|
|
4147
|
+
anr: toVitalMetric(anr.current, thresholds.anrRate, anr.previous, anr.trend),
|
|
4148
|
+
slowStarts: toVitalMetric(
|
|
4149
|
+
slowStart.current,
|
|
4150
|
+
thresholds.slowStartRate,
|
|
4151
|
+
slowStart.previous,
|
|
4152
|
+
slowStart.trend
|
|
4153
|
+
),
|
|
4154
|
+
slowRender: toVitalMetric(
|
|
4155
|
+
slowRender.current,
|
|
4156
|
+
thresholds.slowRenderingRate,
|
|
4157
|
+
slowRender.previous,
|
|
4158
|
+
slowRender.trend
|
|
4159
|
+
)
|
|
4131
4160
|
},
|
|
4132
4161
|
reviews
|
|
4133
4162
|
};
|
|
4134
4163
|
}
|
|
4135
4164
|
function vitalIndicator(metric) {
|
|
4136
|
-
if (metric.status === "unknown") return "
|
|
4165
|
+
if (metric.status === "unknown") return "\u2014";
|
|
4137
4166
|
if (metric.status === "breach") return "\u2717";
|
|
4138
4167
|
if (metric.status === "warn") return "\u26A0";
|
|
4139
4168
|
return "\u2713";
|
|
4140
4169
|
}
|
|
4170
|
+
function vitalTrendArrow(metric) {
|
|
4171
|
+
if (!metric.trend || metric.trend === "flat") return "";
|
|
4172
|
+
return metric.trend === "up" ? " \u2191" : " \u2193";
|
|
4173
|
+
}
|
|
4141
4174
|
function formatVitalValue(metric) {
|
|
4142
|
-
if (metric.value === void 0) return "
|
|
4175
|
+
if (metric.value === void 0) return "\u2014";
|
|
4143
4176
|
return `${(metric.value * 100).toFixed(2)}%`;
|
|
4144
4177
|
}
|
|
4145
4178
|
function formatFraction(fraction) {
|
|
@@ -4147,7 +4180,7 @@ function formatFraction(fraction) {
|
|
|
4147
4180
|
return `${Math.round(fraction * 100)}%`;
|
|
4148
4181
|
}
|
|
4149
4182
|
function formatRating(rating) {
|
|
4150
|
-
if (rating === void 0) return "
|
|
4183
|
+
if (rating === void 0) return "\u2014";
|
|
4151
4184
|
return `\u2605 ${rating.toFixed(1)}`;
|
|
4152
4185
|
}
|
|
4153
4186
|
function formatTrend(current, previous) {
|
|
@@ -4156,43 +4189,218 @@ function formatTrend(current, previous) {
|
|
|
4156
4189
|
if (current < previous) return ` \u2193 from ${previous.toFixed(1)}`;
|
|
4157
4190
|
return "";
|
|
4158
4191
|
}
|
|
4192
|
+
function allVitalsUnknown(vitals) {
|
|
4193
|
+
return vitals.crashes.status === "unknown" && vitals.anr.status === "unknown" && vitals.slowStarts.status === "unknown" && vitals.slowRender.status === "unknown";
|
|
4194
|
+
}
|
|
4159
4195
|
function formatStatusTable(status) {
|
|
4160
4196
|
const lines = [];
|
|
4197
|
+
const sectionSet = new Set(status.sections);
|
|
4161
4198
|
const cachedLabel = status.cached ? ` (cached ${new Date(status.fetchedAt).toLocaleTimeString()})` : ` (fetched ${new Date(status.fetchedAt).toLocaleTimeString()})`;
|
|
4162
4199
|
lines.push(`App: ${status.packageName}${cachedLabel}`);
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4200
|
+
if (sectionSet.has("releases")) {
|
|
4201
|
+
lines.push("");
|
|
4202
|
+
lines.push("RELEASES");
|
|
4203
|
+
if (status.releases.length === 0) {
|
|
4204
|
+
lines.push(" No releases found.");
|
|
4205
|
+
} else {
|
|
4206
|
+
const trackW = Math.max(10, ...status.releases.map((r) => r.track.length));
|
|
4207
|
+
const versionW = Math.max(7, ...status.releases.map((r) => r.versionCode.length));
|
|
4208
|
+
const statusW = Math.max(8, ...status.releases.map((r) => r.status.length));
|
|
4209
|
+
for (const r of status.releases) {
|
|
4210
|
+
lines.push(
|
|
4211
|
+
` ${r.track.padEnd(trackW)} ${r.versionCode.padEnd(versionW)} ${r.status.padEnd(statusW)} ${formatFraction(r.userFraction)}`
|
|
4212
|
+
);
|
|
4213
|
+
}
|
|
4214
|
+
}
|
|
4215
|
+
}
|
|
4216
|
+
if (sectionSet.has("vitals")) {
|
|
4217
|
+
lines.push("");
|
|
4218
|
+
lines.push(`VITALS (last ${status.vitals.windowDays} days)`);
|
|
4219
|
+
if (allVitalsUnknown(status.vitals)) {
|
|
4220
|
+
lines.push(" No vitals data available for this period.");
|
|
4221
|
+
} else {
|
|
4222
|
+
const { crashes, anr, slowStarts, slowRender } = status.vitals;
|
|
4223
|
+
const crashVal = `${formatVitalValue(crashes)}${vitalTrendArrow(crashes)}`;
|
|
4224
|
+
const anrVal = `${formatVitalValue(anr)}${vitalTrendArrow(anr)}`;
|
|
4225
|
+
const slowStartVal = `${formatVitalValue(slowStarts)}${vitalTrendArrow(slowStarts)}`;
|
|
4226
|
+
const slowRenderVal = `${formatVitalValue(slowRender)}${vitalTrendArrow(slowRender)}`;
|
|
4172
4227
|
lines.push(
|
|
4173
|
-
` ${
|
|
4228
|
+
` crashes ${crashVal.padEnd(10)} ${vitalIndicator(crashes)} anr ${anrVal.padEnd(10)} ${vitalIndicator(anr)}`
|
|
4229
|
+
);
|
|
4230
|
+
lines.push(
|
|
4231
|
+
` slow starts ${slowStartVal.padEnd(10)} ${vitalIndicator(slowStarts)} slow render ${slowRenderVal.padEnd(10)} ${vitalIndicator(slowRender)}`
|
|
4174
4232
|
);
|
|
4175
4233
|
}
|
|
4176
4234
|
}
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
);
|
|
4235
|
+
if (sectionSet.has("reviews")) {
|
|
4236
|
+
lines.push("");
|
|
4237
|
+
lines.push(`REVIEWS (last ${status.reviews.windowDays} days)`);
|
|
4238
|
+
const { averageRating, previousAverageRating, totalNew, positivePercent } = status.reviews;
|
|
4239
|
+
if (totalNew === 0 && averageRating === void 0) {
|
|
4240
|
+
lines.push(" No reviews in this period.");
|
|
4241
|
+
} else {
|
|
4242
|
+
const trend = formatTrend(averageRating, previousAverageRating);
|
|
4243
|
+
const positiveStr = positivePercent !== void 0 ? ` ${positivePercent}% positive` : "";
|
|
4244
|
+
lines.push(` ${formatRating(averageRating)} ${totalNew} new${positiveStr}${trend}`);
|
|
4245
|
+
}
|
|
4246
|
+
}
|
|
4247
|
+
return lines.join("\n");
|
|
4248
|
+
}
|
|
4249
|
+
function formatStatusSummary(status) {
|
|
4250
|
+
const parts = [status.packageName];
|
|
4251
|
+
const latestRelease = status.releases.find((r) => r.status !== "draft") ?? status.releases[0];
|
|
4252
|
+
if (latestRelease) {
|
|
4253
|
+
parts.push(`v${latestRelease.versionCode} ${latestRelease.track}`);
|
|
4254
|
+
}
|
|
4255
|
+
const { crashes, anr } = status.vitals;
|
|
4256
|
+
if (crashes.status !== "unknown") {
|
|
4257
|
+
const arrow = crashes.trend === "up" ? " \u2191" : crashes.trend === "down" ? " \u2193" : "";
|
|
4258
|
+
parts.push(`crashes ${formatVitalValue(crashes)}${arrow} ${vitalIndicator(crashes)}`);
|
|
4259
|
+
}
|
|
4260
|
+
if (anr.status !== "unknown") {
|
|
4261
|
+
const arrow = anr.trend === "up" ? " \u2191" : anr.trend === "down" ? " \u2193" : "";
|
|
4262
|
+
parts.push(`ANR ${formatVitalValue(anr)}${arrow} ${vitalIndicator(anr)}`);
|
|
4263
|
+
}
|
|
4264
|
+
const { averageRating, totalNew } = status.reviews;
|
|
4265
|
+
if (averageRating !== void 0) {
|
|
4266
|
+
parts.push(`avg ${averageRating.toFixed(1)}\u2605`);
|
|
4267
|
+
}
|
|
4268
|
+
if (totalNew > 0) {
|
|
4269
|
+
parts.push(`${totalNew} reviews`);
|
|
4270
|
+
}
|
|
4271
|
+
return parts.join(" \xB7 ") + (statusHasBreach(status) ? " [ALERT]" : "");
|
|
4272
|
+
}
|
|
4273
|
+
function computeStatusDiff(prev, curr) {
|
|
4274
|
+
const prevVersion = prev.releases[0]?.versionCode ?? null;
|
|
4275
|
+
const currVersion = curr.releases[0]?.versionCode ?? null;
|
|
4276
|
+
const prevCrash = prev.vitals.crashes.value ?? null;
|
|
4277
|
+
const currCrash = curr.vitals.crashes.value ?? null;
|
|
4278
|
+
const prevAnr = prev.vitals.anr.value ?? null;
|
|
4279
|
+
const currAnr = curr.vitals.anr.value ?? null;
|
|
4280
|
+
const prevRating = prev.reviews.averageRating ?? null;
|
|
4281
|
+
const currRating = curr.reviews.averageRating ?? null;
|
|
4282
|
+
return {
|
|
4283
|
+
versionCode: { from: prevVersion, to: currVersion },
|
|
4284
|
+
crashRate: {
|
|
4285
|
+
from: prevCrash,
|
|
4286
|
+
to: currCrash,
|
|
4287
|
+
delta: currCrash !== null && prevCrash !== null ? currCrash - prevCrash : null
|
|
4288
|
+
},
|
|
4289
|
+
anrRate: {
|
|
4290
|
+
from: prevAnr,
|
|
4291
|
+
to: currAnr,
|
|
4292
|
+
delta: currAnr !== null && prevAnr !== null ? currAnr - prevAnr : null
|
|
4293
|
+
},
|
|
4294
|
+
reviewCount: { from: prev.reviews.totalNew, to: curr.reviews.totalNew },
|
|
4295
|
+
averageRating: {
|
|
4296
|
+
from: prevRating,
|
|
4297
|
+
to: currRating,
|
|
4298
|
+
delta: currRating !== null && prevRating !== null ? Math.round((currRating - prevRating) * 10) / 10 : null
|
|
4299
|
+
}
|
|
4300
|
+
};
|
|
4301
|
+
}
|
|
4302
|
+
function formatStatusDiff(diff, since) {
|
|
4303
|
+
const lines = [`Changes since ${since}:`];
|
|
4304
|
+
if (diff.versionCode.from !== diff.versionCode.to) {
|
|
4305
|
+
lines.push(` Version: ${diff.versionCode.from ?? "\u2014"} \u2192 ${diff.versionCode.to ?? "\u2014"}`);
|
|
4306
|
+
}
|
|
4307
|
+
const fmtRate = (v) => v !== null ? `${(v * 100).toFixed(2)}%` : "\u2014";
|
|
4308
|
+
const fmtDelta = (d, lowerIsBetter = true) => {
|
|
4309
|
+
if (d === null || Math.abs(d) < 1e-4) return "no change";
|
|
4310
|
+
const sign = d > 0 ? "+" : "";
|
|
4311
|
+
const good = lowerIsBetter ? d < 0 : d > 0;
|
|
4312
|
+
return `${sign}${(d * 100).toFixed(2)}% ${good ? "\u2713" : "\u2717"}`;
|
|
4313
|
+
};
|
|
4183
4314
|
lines.push(
|
|
4184
|
-
`
|
|
4315
|
+
` Crash rate: ${fmtRate(diff.crashRate.from)} \u2192 ${fmtRate(diff.crashRate.to)} (${fmtDelta(diff.crashRate.delta)})`
|
|
4185
4316
|
);
|
|
4186
|
-
lines.push("");
|
|
4187
|
-
lines.push(`REVIEWS (last ${status.reviews.windowDays} days)`);
|
|
4188
|
-
const { averageRating, previousAverageRating, totalNew, positivePercent } = status.reviews;
|
|
4189
|
-
const trend = formatTrend(averageRating, previousAverageRating);
|
|
4190
|
-
const positiveStr = positivePercent !== void 0 ? ` ${positivePercent}% positive` : "";
|
|
4191
4317
|
lines.push(
|
|
4192
|
-
` ${
|
|
4318
|
+
` ANR rate: ${fmtRate(diff.anrRate.from)} \u2192 ${fmtRate(diff.anrRate.to)} (${fmtDelta(diff.anrRate.delta)})`
|
|
4193
4319
|
);
|
|
4320
|
+
const ratingDelta = diff.averageRating.delta;
|
|
4321
|
+
const prevR = diff.averageRating.from !== null ? `${diff.averageRating.from.toFixed(1)}\u2605` : "\u2014";
|
|
4322
|
+
const currR = diff.averageRating.to !== null ? `${diff.averageRating.to.toFixed(1)}\u2605` : "\u2014";
|
|
4323
|
+
const ratingStr = ratingDelta === null || Math.abs(ratingDelta) < 0.05 ? "no change" : `${ratingDelta > 0 ? "+" : ""}${ratingDelta.toFixed(1)} ${ratingDelta > 0 ? "\u2713" : "\u2717"}`;
|
|
4324
|
+
lines.push(` Reviews: ${prevR} \u2192 ${currR} (${ratingStr})`);
|
|
4194
4325
|
return lines.join("\n");
|
|
4195
4326
|
}
|
|
4327
|
+
async function runWatchLoop(opts) {
|
|
4328
|
+
if (opts.intervalSeconds < 10) {
|
|
4329
|
+
console.error("Error: --watch interval must be at least 10 seconds");
|
|
4330
|
+
process.exit(2);
|
|
4331
|
+
}
|
|
4332
|
+
let running = true;
|
|
4333
|
+
const cleanup = () => {
|
|
4334
|
+
running = false;
|
|
4335
|
+
process.stdout.write("\n");
|
|
4336
|
+
process.exit(0);
|
|
4337
|
+
};
|
|
4338
|
+
process.on("SIGINT", cleanup);
|
|
4339
|
+
process.on("SIGTERM", cleanup);
|
|
4340
|
+
while (running) {
|
|
4341
|
+
process.stdout.write("\x1B[2J\x1B[H");
|
|
4342
|
+
try {
|
|
4343
|
+
const status = await opts.fetch();
|
|
4344
|
+
await opts.save(status);
|
|
4345
|
+
console.log(opts.render(status));
|
|
4346
|
+
} catch (err) {
|
|
4347
|
+
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
4348
|
+
}
|
|
4349
|
+
console.log(`
|
|
4350
|
+
[gpc status] Refreshing in ${opts.intervalSeconds}s\u2026 (Ctrl+C to stop)`);
|
|
4351
|
+
for (let i = 0; i < opts.intervalSeconds && running; i++) {
|
|
4352
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
4353
|
+
}
|
|
4354
|
+
}
|
|
4355
|
+
}
|
|
4356
|
+
function breachStateFilePath(packageName) {
|
|
4357
|
+
return join7(getCacheDir(), `breach-state-${packageName}.json`);
|
|
4358
|
+
}
|
|
4359
|
+
async function trackBreachState(packageName, isBreaching) {
|
|
4360
|
+
const filePath = breachStateFilePath(packageName);
|
|
4361
|
+
let prevBreaching = false;
|
|
4362
|
+
try {
|
|
4363
|
+
const raw = await readFile10(filePath, "utf-8");
|
|
4364
|
+
prevBreaching = JSON.parse(raw).breaching;
|
|
4365
|
+
} catch {
|
|
4366
|
+
}
|
|
4367
|
+
if (prevBreaching !== isBreaching) {
|
|
4368
|
+
try {
|
|
4369
|
+
await mkdir5(getCacheDir(), { recursive: true });
|
|
4370
|
+
await writeFile7(
|
|
4371
|
+
filePath,
|
|
4372
|
+
JSON.stringify({ breaching: isBreaching, since: (/* @__PURE__ */ new Date()).toISOString() }, null, 2),
|
|
4373
|
+
{ encoding: "utf-8", mode: 384 }
|
|
4374
|
+
);
|
|
4375
|
+
} catch {
|
|
4376
|
+
}
|
|
4377
|
+
return true;
|
|
4378
|
+
}
|
|
4379
|
+
return false;
|
|
4380
|
+
}
|
|
4381
|
+
function sendNotification(title, body) {
|
|
4382
|
+
if (process.env["CI"]) return;
|
|
4383
|
+
try {
|
|
4384
|
+
const p = process.platform;
|
|
4385
|
+
if (p === "darwin") {
|
|
4386
|
+
execSync(
|
|
4387
|
+
`osascript -e 'display notification ${JSON.stringify(body)} with title ${JSON.stringify(title)}'`,
|
|
4388
|
+
{ stdio: "ignore" }
|
|
4389
|
+
);
|
|
4390
|
+
} else if (p === "linux") {
|
|
4391
|
+
execSync(`notify-send ${JSON.stringify(title)} ${JSON.stringify(body)}`, {
|
|
4392
|
+
stdio: "ignore"
|
|
4393
|
+
});
|
|
4394
|
+
} else if (p === "win32") {
|
|
4395
|
+
const escaped = (s) => s.replace(/'/g, "''");
|
|
4396
|
+
execSync(
|
|
4397
|
+
`powershell -Command "Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.MessageBox]::Show('${escaped(body)}', '${escaped(title)}')"`,
|
|
4398
|
+
{ stdio: "ignore" }
|
|
4399
|
+
);
|
|
4400
|
+
}
|
|
4401
|
+
} catch {
|
|
4402
|
+
}
|
|
4403
|
+
}
|
|
4196
4404
|
function statusHasBreach(status) {
|
|
4197
4405
|
return status.vitals.crashes.status === "breach" || status.vitals.anr.status === "breach" || status.vitals.slowStarts.status === "breach" || status.vitals.slowRender.status === "breach";
|
|
4198
4406
|
}
|
|
@@ -4220,6 +4428,7 @@ export {
|
|
|
4220
4428
|
clearAuditLog,
|
|
4221
4429
|
compareBundles,
|
|
4222
4430
|
compareVitalsTrend,
|
|
4431
|
+
computeStatusDiff,
|
|
4223
4432
|
consumeProductPurchase,
|
|
4224
4433
|
convertRegionPrices,
|
|
4225
4434
|
createAuditEntry,
|
|
@@ -4265,6 +4474,8 @@ export {
|
|
|
4265
4474
|
formatJunit,
|
|
4266
4475
|
formatOutput,
|
|
4267
4476
|
formatSlackPayload,
|
|
4477
|
+
formatStatusDiff,
|
|
4478
|
+
formatStatusSummary,
|
|
4268
4479
|
formatStatusTable,
|
|
4269
4480
|
generateMigrationPlan,
|
|
4270
4481
|
generateNotesFromGit,
|
|
@@ -4340,16 +4551,19 @@ export {
|
|
|
4340
4551
|
removeUser,
|
|
4341
4552
|
replyToReview,
|
|
4342
4553
|
revokeSubscriptionPurchase,
|
|
4554
|
+
runWatchLoop,
|
|
4343
4555
|
safePath,
|
|
4344
4556
|
safePathWithin,
|
|
4345
4557
|
saveStatusCache,
|
|
4346
4558
|
scaffoldPlugin,
|
|
4347
4559
|
searchAuditEvents,
|
|
4348
4560
|
searchVitalsErrors,
|
|
4561
|
+
sendNotification,
|
|
4349
4562
|
sendWebhook,
|
|
4350
4563
|
sortResults,
|
|
4351
4564
|
statusHasBreach,
|
|
4352
4565
|
syncInAppProducts,
|
|
4566
|
+
trackBreachState,
|
|
4353
4567
|
updateAppDetails,
|
|
4354
4568
|
updateDataSafety,
|
|
4355
4569
|
updateInAppProduct,
|