@gpc-cli/core 0.10.6 → 0.10.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -21
- package/dist/index.d.ts +40 -1
- package/dist/index.js +191 -62
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -13,34 +13,39 @@ npm install @gpc-cli/core
|
|
|
13
13
|
## Usage
|
|
14
14
|
|
|
15
15
|
```typescript
|
|
16
|
+
import { resolveAuth } from "@gpc-cli/auth";
|
|
17
|
+
import { createApiClient, createReportingClient } from "@gpc-cli/api";
|
|
16
18
|
import {
|
|
17
19
|
uploadRelease,
|
|
18
20
|
promoteRelease,
|
|
19
|
-
|
|
21
|
+
getVitalsCrashes,
|
|
22
|
+
latestWorstValue,
|
|
20
23
|
listReviews,
|
|
21
24
|
analyzeBundle,
|
|
22
25
|
formatOutput,
|
|
23
26
|
} from "@gpc-cli/core";
|
|
24
27
|
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
28
|
+
const auth = await resolveAuth({ serviceAccountPath: "./play-sa.json" });
|
|
29
|
+
const client = createApiClient({ auth });
|
|
30
|
+
const reporting = createReportingClient({ auth });
|
|
31
|
+
const app = "com.example.app";
|
|
32
|
+
|
|
33
|
+
// Upload a release (validateOnly: server-side validation without committing)
|
|
34
|
+
await uploadRelease(client, app, "app.aab", { track: "internal", validateOnly: true });
|
|
35
|
+
|
|
36
|
+
// Promote between tracks at a 10% staged rollout
|
|
37
|
+
await promoteRelease(client, app, "internal", "production", { userFraction: 0.1 });
|
|
38
|
+
|
|
39
|
+
// Gate on vitals: rates are fractions (0.02 = 2%)
|
|
40
|
+
const crashes = await getVitalsCrashes(reporting, app, { days: 7 });
|
|
41
|
+
const crashRate = latestWorstValue(crashes.rows, "crashRate");
|
|
42
|
+
if (crashRate !== undefined && crashRate > 0.02) process.exitCode = 6;
|
|
43
|
+
|
|
44
|
+
// Recent reviews as a table (also: "json", "csv", "tsv")
|
|
45
|
+
const { reviews } = await listReviews(client, app);
|
|
46
|
+
console.log(formatOutput(reviews, "table"));
|
|
47
|
+
|
|
48
|
+
// Analyze bundle size offline
|
|
44
49
|
const analysis = await analyzeBundle("./app.aab");
|
|
45
50
|
```
|
|
46
51
|
|
|
@@ -52,7 +57,7 @@ const analysis = await analyzeBundle("./app.aab");
|
|
|
52
57
|
| **Listings** | `getListings`, `updateListing`, `pullListings`, `pushListings`, `diffListings` |
|
|
53
58
|
| **Images** | `listImages`, `uploadImage`, `deleteImage` |
|
|
54
59
|
| **Reviews** | `listReviews`, `getReview`, `replyToReview`, `exportReviews` |
|
|
55
|
-
| **Vitals** | `getVitalsOverview`, `getVitalsCrashes`, `getVitalsAnr`, `getVitalsStartup`, `compareVitalsTrend`, `checkThreshold`
|
|
60
|
+
| **Vitals** | `getVitalsOverview`, `getVitalsCrashes`, `getVitalsAnr`, `getVitalsStartup`, `compareVitalsTrend`, `checkThreshold`, `latestWorstValue`, `readMetricValue` |
|
|
56
61
|
| **Subscriptions** | `listSubscriptions`, `createSubscription`, `updateSubscription`, `deleteSubscription`, `listOffers`, `createOffer` |
|
|
57
62
|
| **IAP** | `listInAppProducts`, `createInAppProduct`, `syncInAppProducts` |
|
|
58
63
|
| **Purchases** | `getProductPurchase`, `acknowledgeProductPurchase`, `refundOrder` |
|
package/dist/index.d.ts
CHANGED
|
@@ -720,12 +720,43 @@ interface VitalsOverview {
|
|
|
720
720
|
slowRenderingRate?: MetricRow[];
|
|
721
721
|
excessiveWakeupRate?: MetricRow[];
|
|
722
722
|
stuckWakelockRate?: MetricRow[];
|
|
723
|
+
/** Metrics that could not be read, with the reason. Absent when every query succeeded. */
|
|
724
|
+
unavailable?: {
|
|
725
|
+
metric: string;
|
|
726
|
+
code: string;
|
|
727
|
+
reason: string;
|
|
728
|
+
}[];
|
|
723
729
|
}
|
|
724
730
|
interface ThresholdResult {
|
|
725
731
|
breached: boolean;
|
|
726
732
|
value: number | undefined;
|
|
727
733
|
threshold: number;
|
|
728
734
|
}
|
|
735
|
+
/**
|
|
736
|
+
* Numeric value of `metric` in a row (the first metric when no name is given).
|
|
737
|
+
* Google returns `metrics` as an array of `{ metric, decimalValue }`; the
|
|
738
|
+
* `MetricRow` type describes a name-keyed map, so both shapes are read. A lookup
|
|
739
|
+
* by name on the array always missed, which left `gpc watch` blind.
|
|
740
|
+
*/
|
|
741
|
+
declare function readMetricValue(row: MetricRow | undefined, metric?: string): number | undefined;
|
|
742
|
+
/**
|
|
743
|
+
* Worst (highest) value of `metric` on the most recent day in `rows`. Rows come
|
|
744
|
+
* one per day per slice (start type, report type, --dim value), so "the last
|
|
745
|
+
* row" is an arbitrary slice; a gate must compare the worst one.
|
|
746
|
+
*/
|
|
747
|
+
declare function latestWorstValue(rows: MetricRow[] | undefined, metric?: string): number | undefined;
|
|
748
|
+
/**
|
|
749
|
+
* Average of `metric` over the window for each slice, then the worst slice. Without
|
|
750
|
+
* dimensions this is the plain average; with start types it reports the worst of
|
|
751
|
+
* cold, warm and hot instead of blending them.
|
|
752
|
+
*/
|
|
753
|
+
declare function worstSliceAverage(rows: MetricRow[] | undefined, metric?: string): number | undefined;
|
|
754
|
+
/**
|
|
755
|
+
* Rejects a rate threshold that can never be reached. Google reports rates as
|
|
756
|
+
* fractions (0.02 = 2%), so 1 or more is almost always a percentage written by
|
|
757
|
+
* mistake, and it left the gate silently open.
|
|
758
|
+
*/
|
|
759
|
+
declare function assertRateThreshold(label: string, threshold: number): void;
|
|
729
760
|
declare function getVitalsOverview(reporting: ReportingApiClient, packageName: string): Promise<VitalsOverview>;
|
|
730
761
|
declare function getVitalsCrashes(reporting: ReportingApiClient, packageName: string, options?: VitalsQueryOptions): Promise<MetricSetResponse>;
|
|
731
762
|
declare function getVitalsAnr(reporting: ReportingApiClient, packageName: string, options?: VitalsQueryOptions): Promise<MetricSetResponse>;
|
|
@@ -781,6 +812,8 @@ interface WatchVitalsOptions {
|
|
|
781
812
|
onHalt?: (value: number) => Promise<void>;
|
|
782
813
|
/** Called on each poll result (value may be undefined if no data). */
|
|
783
814
|
onPoll?: (value: number | undefined, breached: boolean) => void;
|
|
815
|
+
/** Called when a poll fails; polling continues. */
|
|
816
|
+
onError?: (err: unknown) => void;
|
|
784
817
|
}
|
|
785
818
|
/**
|
|
786
819
|
* Poll vitals on an interval; invoke onHalt if threshold is breached.
|
|
@@ -810,6 +843,8 @@ interface WatchVitalReading {
|
|
|
810
843
|
value: number | undefined;
|
|
811
844
|
threshold: number | undefined;
|
|
812
845
|
breached: boolean;
|
|
846
|
+
/** Set when the metric could not be read this round (error code, or message). */
|
|
847
|
+
error?: string;
|
|
813
848
|
}
|
|
814
849
|
interface WatchEvent {
|
|
815
850
|
round: number;
|
|
@@ -824,6 +859,10 @@ interface WatchSummary {
|
|
|
824
859
|
durationMs: number;
|
|
825
860
|
finalRollout: number | null;
|
|
826
861
|
breachCount: number;
|
|
862
|
+
/** Rounds in which a metric with a threshold could not be read (games-only metrics excluded). */
|
|
863
|
+
unreadableRounds: number;
|
|
864
|
+
/** True when the last round had such an unreadable metric: the gate ended blind. */
|
|
865
|
+
lastRoundUnreadable: boolean;
|
|
827
866
|
halted: boolean;
|
|
828
867
|
events: WatchEvent[];
|
|
829
868
|
}
|
|
@@ -2013,4 +2052,4 @@ declare function readinessBadgeColor(grade: ReadinessGrade): string;
|
|
|
2013
2052
|
declare function readinessBadgeUrl(score: ReadinessScore): string;
|
|
2014
2053
|
declare function readinessBadgeMarkdown(score: ReadinessScore): string;
|
|
2015
2054
|
|
|
2016
|
-
export { ApiError, type ApiSigningFingerprint, type AppInfo, type AppStatus, type ApplyReleaseNotesResult, type AuditEntry, type BatchSyncResult, type BundleAnalysis, type BundleComparison, type BundleEntry, type BundleSizeCheckResult, type BundleSizeConfig, type BundlesWaitOptions, type ChangelogEntry, type ChecklistInput, type ChecklistItem, type ChecklistResult, type CommandContext, type CommitCluster, type CommitResult, ConfigError, type CreateEnterpriseAppParams, DEFAULT_LIMITS, DEFAULT_MODELS, DEFAULT_PREFLIGHT_CONFIG, DEFAULT_WATCH_THRESHOLDS, type DecodedNotification, type DiffToken, type DiscoverPluginsOptions, type DiscoveredPlugin, type DryRunPublishResult, type DryRunResult, type DryRunUploadResult, type ErrorReason, type ExportImagesOptions, type ExportImagesSummary, type ExternalTransactionRefundOptions, type FastlaneDetection, type FastlaneLane, type FetchChangelogOptions, type FieldLintResult, type FileValidationResult, type FinancialReportDownload, type FindingSeverity, GOOGLE_PLAY_LANGUAGES, type GameConfigDiff, type GamesPushOptions, type GamesSyncResult, type GenerateOptions, type GeneratedChangelog, type GetAppStatusOptions, type GitNotesOptions, type GitReleaseNotes, type GitRunner, GpcError, type ImageSyncDetail, type ImageSyncOptions, type ImageSyncResult, type ImageValidationResult, type InitOptions, type InitResult, type InternalSharingUploadResult, type KeystoreFingerprint, type ListGamesConfigOptions, type ListIapOptions, type ListReportsResult, type ListSubscriptionsOptions, type ListUsersOptions, type ListVoidedOptions, type ListingDiff, type ListingFieldLimits, type ListingLintResult, type ListingsResult, type LoadedPlugin, type LocaleBundle, type LocaleEntry, type MigrationResult, NetworkError, type OneTimeProductDiff, type OutputMode, PERMISSION_PROPAGATION_WARNING, PLACEHOLDER_TEXT, PLAY_STORE_LIMIT, PROVIDER_WHITELIST, type ParsedCommit, type ParsedManifest, type ParsedMonth, type PlayStoreFormat, type PlayStoreRenderOptions, PluginManager, type PreflightConfig, type PreflightFinding, type PreflightOptions, type PreflightResult, type PreflightScanner, type Provider, type PublishOptions, type PublishResult, type PushResult, type QuotaUsage, RENDERERS, REVIEW_SORT_PRESETS, type RawCommit, type ReadinessBreakdownEntry, type ReadinessGrade, type ReadinessScore, type ReadinessSignal, type ReadinessSignalStatus, type RefundReviewPreference, type ReleaseDiff, type ReleaseNotesValidation, type ReleaseStatusResult, type Renderer, type ReportObject, type ReportsAuth, type ResolveAiConfigOptions, type ResolveLocalesOptions, type ReviewAnalysis, type ReviewExportOptions, type ReviewOrderRefundOptions, type ReviewOrderRefundResult, type ReviewSortPreset, type ReviewsFilterOptions, type RevocationRefundType, type RtdnStatus, SECTION_ORDER, SENSITIVE_ARG_KEYS, SENSITIVE_KEYS, SEVERITY_ORDER, type ScaffoldOptions, type ScaffoldResult, type SigningConsistencyResult, type SigningKeyComparison, type Spinner, type StatsReportDownload, type StatusDiff, type StatusRelease, type StatusReviews, type StatusVitalMetric, type SubscriptionAnalytics, type SubscriptionDiff, type SyncResult, type ThresholdResult, type TrainConfig, type TrainState, type TranslateBundleOptions, type TranslatedBundle, type TranslationFailure, type TranslationPath, type TranslationResult, type Translator, type TranslatorConfig, type UploadResult, VALID_WATCH_METRICS, type ValidateCheck, type ValidateOptions, type ValidateResult, type ValidatedPluginApproval, type VersionVitalsComparison, type VersionVitalsRow, type VitalsOverview, type VitalsQueryOptions, type VitalsTrendComparison, type WatchAction, type WatchCallbacks, type WatchConfig, type WatchEvent, type WatchMetric, type WatchOptions, type WatchRollout, type WatchSummary, type WatchVitalReading, type WatchVitalsOptions, type WebhookPayload, type ZipCsvEntry, abortTrain, acknowledgeProductPurchase, activateBasePlan, activateOffer, addRecoveryTargeting, addTesters, advanceTrain, analyzeBundle, analyzeRemoteListings, analyzeReviews, annotateListResult, applyReleaseNotes, assignRelease, batchGetOrders, batchSyncInAppProducts, buildChecklist, buildExternalTransactionRefund, buildLocaleBundle, bundleToReleaseNotes, cancelRecoveryAction, cancelSubscriptionPurchase, cancelSubscriptionV2, checkBundleSize, checkSigningConsistency, checkThreshold, classifyError, clearAuditLog, commitWithRescue, compareBundles, compareFingerprints, compareVersionVitals, compareVitalsTrend, computeStatusDiff, consumeProductPurchase, convertRegionPrices, createAchievementConfig, createAuditEntry, createDeviceTier, createEnterpriseApp, createExternalTransaction, createGrant, createInAppProduct, createLeaderboardConfig, createOffer, createOneTimeOffer, createOneTimeProduct, createRecoveryAction, createSpinner, createSubscription, createTrack, createTranslator, deactivateBasePlan, deactivateOffer, decodeCsvText, decodeNotification, decodeStatsCsv, defaultGitRunner, deferSubscriptionPurchase, deferSubscriptionV2, deleteAchievementConfig, deleteBasePlan, deleteGrant, deleteImage, deleteInAppProduct, deleteLeaderboardConfig, deleteListing, deleteOffer, deleteOneTimeOffer, deleteOneTimeProduct, deleteSubscription, deployRecoveryAction, detectFastlane, detectOutputFormat, diffAchievementConfig, diffLeaderboardConfig, diffListings, diffListingsCommand, diffListingsEnhanced, diffOneTimeProduct, diffReleases, diffSubscription, discoverPluginEntries, discoverPlugins, dispatchWebhook, downloadFinancialReport, downloadGeneratedApk, downloadReportObject, downloadStatsReport, exportImages, exportReviews, extractCsvEntriesFromZip, fetchAggregateCost, fetchChangelog, fetchReleaseNotes, findBundle, formatChangelogEntry, formatCustomPayload, formatDiscordPayload, formatJunit, formatMoney, formatNotification, formatOutput, formatPathLabel, formatSlackPayload, formatStatusDiff, formatStatusSummary, formatStatusTable, formatWordDiff, generateChangelog, generateMigrationPlan, generateNotesFromGit, getAchievementConfig, getAllScannerNames, getApiSigningFingerprint, getAppInfo, getAppStatus, getCountryAvailability, getDeviceTier, getExternalTransaction, getInAppProduct, getKeystoreFingerprint, getLeaderboardConfig, getListings, getOffer, getOneTimeOffer, getOneTimeProduct, getOrderDetails, getProductPurchase, getProductPurchaseV2, getQuotaUsage, getReleasesStatus, getReview, getRtdnStatus, getSubscription, getSubscriptionAnalytics, getSubscriptionPurchase, getTrainStatus, getUser, getVitalsAnomalies, getVitalsAnr, getVitalsBattery, getVitalsCrashes, getVitalsErrorCount, getVitalsLmk, getVitalsMemory, getVitalsMemoryBitmap, getVitalsMemoryRss, getVitalsOverview, getVitalsRendering, getVitalsStartup, gradeFromPercent, gunzipIfNeeded, handleBreach, importDataSafety, importTestersFromCsv, initAudit, initProject, inviteUser, isFinancialReportType, isGzip, isStatsReportType, isValidBcp47, isValidReportType, isValidStatsDimension, isVersionedNotesDir, isZip, lintListing, lintListings, lintLocalListings, listAchievementConfigs, listAchievements, listAuditEvents, listBundles, listDeviceTiers, listGeneratedApks, listGrants, listImages, listInAppProducts, listLeaderboardConfigs, listLeaderboards, listOffers, listOneTimeOffers, listOneTimeProducts, listRecoveryActions, listReportObjects, listReports, listReviews, listSubscriptions, listTesters, listTracks, listUsers, listVoidedPurchases, loadPreflightConfig, loadStatusCache, maybePaginate, migratePrices, monthToken, moreResultsFooter, normalizeFingerprint, parseAppfile, parseCommit, parseFastfile, parseGrantArg, parseKeytoolOutput, parseMonth, parseRemoteUrl, pauseTrain, promoteRelease, publish, publishEnterpriseApp, pullAchievementConfigs, pullLeaderboardConfigs, pullListings, pushAchievementConfigs, pushLeaderboardConfigs, pushListings, readListingsFromDir, readReleaseNotesForVersion, readReleaseNotesFromDir, readinessBadgeColor, readinessBadgeMarkdown, readinessBadgeUrl, redactAuditArgs, redactSensitive, refundExternalTransaction, refundOrder, relativeTime, removeTesters, removeUser, renderChecklistMarkdown, renderJson, renderMarkdown, renderPlayStore, renderPlayStoreJson, renderPlayStoreMd, renderPlayStorePrompt, renderPrompt, replyToReview, reportAccessDeniedError, reportBucketNotFoundError, reportObjectNotFoundError, reportPrefix, resolveAiConfig, resolveLocales, resolveReportsBucket, retryOnUploadNotComplete, reviewOrderRefund, revokeSubscriptionPurchase, runPreflight, runWatch, runWatchLoop, safePath, safePathWithin, saveStatusCache, scaffoldPlugin, scoreReadiness, searchAuditEvents, searchVitalsErrors, sendNotification, sendWebhook, setAchievementIcon, setLeaderboardIcon, sha256File, sortResults, sortReviews, startTrain, statusHasBreach, syncImages, syncInAppProducts, topFiles, trackBreachState, translateBundle, updateAchievementConfig, updateAppDetails, updateDataSafety, updateGrant, updateInAppProduct, updateLeaderboardConfig, updateListing, updateOffer, updateOneTimeOffer, updateOneTimeProduct, updateRollout, updateSubscription, updateTrackConfig, updateUser, uploadExternallyHosted, uploadImage, uploadInternalSharing, uploadRelease, validateAndCommit, validateBundleForApply, validateImage, validateLanguageCode, validatePackageName, validatePluginForApproval, validatePreSubmission, validateReleaseNotes, validateSku, validateTrackName, validateUploadFile, validateVersionCode, waitForBundle, waitForBundleProcessing, watchVitalsWithAutoHalt, wordDiff, writeAuditLog, writeListingsToDir, writeMigrationOutput };
|
|
2055
|
+
export { ApiError, type ApiSigningFingerprint, type AppInfo, type AppStatus, type ApplyReleaseNotesResult, type AuditEntry, type BatchSyncResult, type BundleAnalysis, type BundleComparison, type BundleEntry, type BundleSizeCheckResult, type BundleSizeConfig, type BundlesWaitOptions, type ChangelogEntry, type ChecklistInput, type ChecklistItem, type ChecklistResult, type CommandContext, type CommitCluster, type CommitResult, ConfigError, type CreateEnterpriseAppParams, DEFAULT_LIMITS, DEFAULT_MODELS, DEFAULT_PREFLIGHT_CONFIG, DEFAULT_WATCH_THRESHOLDS, type DecodedNotification, type DiffToken, type DiscoverPluginsOptions, type DiscoveredPlugin, type DryRunPublishResult, type DryRunResult, type DryRunUploadResult, type ErrorReason, type ExportImagesOptions, type ExportImagesSummary, type ExternalTransactionRefundOptions, type FastlaneDetection, type FastlaneLane, type FetchChangelogOptions, type FieldLintResult, type FileValidationResult, type FinancialReportDownload, type FindingSeverity, GOOGLE_PLAY_LANGUAGES, type GameConfigDiff, type GamesPushOptions, type GamesSyncResult, type GenerateOptions, type GeneratedChangelog, type GetAppStatusOptions, type GitNotesOptions, type GitReleaseNotes, type GitRunner, GpcError, type ImageSyncDetail, type ImageSyncOptions, type ImageSyncResult, type ImageValidationResult, type InitOptions, type InitResult, type InternalSharingUploadResult, type KeystoreFingerprint, type ListGamesConfigOptions, type ListIapOptions, type ListReportsResult, type ListSubscriptionsOptions, type ListUsersOptions, type ListVoidedOptions, type ListingDiff, type ListingFieldLimits, type ListingLintResult, type ListingsResult, type LoadedPlugin, type LocaleBundle, type LocaleEntry, type MigrationResult, NetworkError, type OneTimeProductDiff, type OutputMode, PERMISSION_PROPAGATION_WARNING, PLACEHOLDER_TEXT, PLAY_STORE_LIMIT, PROVIDER_WHITELIST, type ParsedCommit, type ParsedManifest, type ParsedMonth, type PlayStoreFormat, type PlayStoreRenderOptions, PluginManager, type PreflightConfig, type PreflightFinding, type PreflightOptions, type PreflightResult, type PreflightScanner, type Provider, type PublishOptions, type PublishResult, type PushResult, type QuotaUsage, RENDERERS, REVIEW_SORT_PRESETS, type RawCommit, type ReadinessBreakdownEntry, type ReadinessGrade, type ReadinessScore, type ReadinessSignal, type ReadinessSignalStatus, type RefundReviewPreference, type ReleaseDiff, type ReleaseNotesValidation, type ReleaseStatusResult, type Renderer, type ReportObject, type ReportsAuth, type ResolveAiConfigOptions, type ResolveLocalesOptions, type ReviewAnalysis, type ReviewExportOptions, type ReviewOrderRefundOptions, type ReviewOrderRefundResult, type ReviewSortPreset, type ReviewsFilterOptions, type RevocationRefundType, type RtdnStatus, SECTION_ORDER, SENSITIVE_ARG_KEYS, SENSITIVE_KEYS, SEVERITY_ORDER, type ScaffoldOptions, type ScaffoldResult, type SigningConsistencyResult, type SigningKeyComparison, type Spinner, type StatsReportDownload, type StatusDiff, type StatusRelease, type StatusReviews, type StatusVitalMetric, type SubscriptionAnalytics, type SubscriptionDiff, type SyncResult, type ThresholdResult, type TrainConfig, type TrainState, type TranslateBundleOptions, type TranslatedBundle, type TranslationFailure, type TranslationPath, type TranslationResult, type Translator, type TranslatorConfig, type UploadResult, VALID_WATCH_METRICS, type ValidateCheck, type ValidateOptions, type ValidateResult, type ValidatedPluginApproval, type VersionVitalsComparison, type VersionVitalsRow, type VitalsOverview, type VitalsQueryOptions, type VitalsTrendComparison, type WatchAction, type WatchCallbacks, type WatchConfig, type WatchEvent, type WatchMetric, type WatchOptions, type WatchRollout, type WatchSummary, type WatchVitalReading, type WatchVitalsOptions, type WebhookPayload, type ZipCsvEntry, abortTrain, acknowledgeProductPurchase, activateBasePlan, activateOffer, addRecoveryTargeting, addTesters, advanceTrain, analyzeBundle, analyzeRemoteListings, analyzeReviews, annotateListResult, applyReleaseNotes, assertRateThreshold, assignRelease, batchGetOrders, batchSyncInAppProducts, buildChecklist, buildExternalTransactionRefund, buildLocaleBundle, bundleToReleaseNotes, cancelRecoveryAction, cancelSubscriptionPurchase, cancelSubscriptionV2, checkBundleSize, checkSigningConsistency, checkThreshold, classifyError, clearAuditLog, commitWithRescue, compareBundles, compareFingerprints, compareVersionVitals, compareVitalsTrend, computeStatusDiff, consumeProductPurchase, convertRegionPrices, createAchievementConfig, createAuditEntry, createDeviceTier, createEnterpriseApp, createExternalTransaction, createGrant, createInAppProduct, createLeaderboardConfig, createOffer, createOneTimeOffer, createOneTimeProduct, createRecoveryAction, createSpinner, createSubscription, createTrack, createTranslator, deactivateBasePlan, deactivateOffer, decodeCsvText, decodeNotification, decodeStatsCsv, defaultGitRunner, deferSubscriptionPurchase, deferSubscriptionV2, deleteAchievementConfig, deleteBasePlan, deleteGrant, deleteImage, deleteInAppProduct, deleteLeaderboardConfig, deleteListing, deleteOffer, deleteOneTimeOffer, deleteOneTimeProduct, deleteSubscription, deployRecoveryAction, detectFastlane, detectOutputFormat, diffAchievementConfig, diffLeaderboardConfig, diffListings, diffListingsCommand, diffListingsEnhanced, diffOneTimeProduct, diffReleases, diffSubscription, discoverPluginEntries, discoverPlugins, dispatchWebhook, downloadFinancialReport, downloadGeneratedApk, downloadReportObject, downloadStatsReport, exportImages, exportReviews, extractCsvEntriesFromZip, fetchAggregateCost, fetchChangelog, fetchReleaseNotes, findBundle, formatChangelogEntry, formatCustomPayload, formatDiscordPayload, formatJunit, formatMoney, formatNotification, formatOutput, formatPathLabel, formatSlackPayload, formatStatusDiff, formatStatusSummary, formatStatusTable, formatWordDiff, generateChangelog, generateMigrationPlan, generateNotesFromGit, getAchievementConfig, getAllScannerNames, getApiSigningFingerprint, getAppInfo, getAppStatus, getCountryAvailability, getDeviceTier, getExternalTransaction, getInAppProduct, getKeystoreFingerprint, getLeaderboardConfig, getListings, getOffer, getOneTimeOffer, getOneTimeProduct, getOrderDetails, getProductPurchase, getProductPurchaseV2, getQuotaUsage, getReleasesStatus, getReview, getRtdnStatus, getSubscription, getSubscriptionAnalytics, getSubscriptionPurchase, getTrainStatus, getUser, getVitalsAnomalies, getVitalsAnr, getVitalsBattery, getVitalsCrashes, getVitalsErrorCount, getVitalsLmk, getVitalsMemory, getVitalsMemoryBitmap, getVitalsMemoryRss, getVitalsOverview, getVitalsRendering, getVitalsStartup, gradeFromPercent, gunzipIfNeeded, handleBreach, importDataSafety, importTestersFromCsv, initAudit, initProject, inviteUser, isFinancialReportType, isGzip, isStatsReportType, isValidBcp47, isValidReportType, isValidStatsDimension, isVersionedNotesDir, isZip, latestWorstValue, lintListing, lintListings, lintLocalListings, listAchievementConfigs, listAchievements, listAuditEvents, listBundles, listDeviceTiers, listGeneratedApks, listGrants, listImages, listInAppProducts, listLeaderboardConfigs, listLeaderboards, listOffers, listOneTimeOffers, listOneTimeProducts, listRecoveryActions, listReportObjects, listReports, listReviews, listSubscriptions, listTesters, listTracks, listUsers, listVoidedPurchases, loadPreflightConfig, loadStatusCache, maybePaginate, migratePrices, monthToken, moreResultsFooter, normalizeFingerprint, parseAppfile, parseCommit, parseFastfile, parseGrantArg, parseKeytoolOutput, parseMonth, parseRemoteUrl, pauseTrain, promoteRelease, publish, publishEnterpriseApp, pullAchievementConfigs, pullLeaderboardConfigs, pullListings, pushAchievementConfigs, pushLeaderboardConfigs, pushListings, readListingsFromDir, readMetricValue, readReleaseNotesForVersion, readReleaseNotesFromDir, readinessBadgeColor, readinessBadgeMarkdown, readinessBadgeUrl, redactAuditArgs, redactSensitive, refundExternalTransaction, refundOrder, relativeTime, removeTesters, removeUser, renderChecklistMarkdown, renderJson, renderMarkdown, renderPlayStore, renderPlayStoreJson, renderPlayStoreMd, renderPlayStorePrompt, renderPrompt, replyToReview, reportAccessDeniedError, reportBucketNotFoundError, reportObjectNotFoundError, reportPrefix, resolveAiConfig, resolveLocales, resolveReportsBucket, retryOnUploadNotComplete, reviewOrderRefund, revokeSubscriptionPurchase, runPreflight, runWatch, runWatchLoop, safePath, safePathWithin, saveStatusCache, scaffoldPlugin, scoreReadiness, searchAuditEvents, searchVitalsErrors, sendNotification, sendWebhook, setAchievementIcon, setLeaderboardIcon, sha256File, sortResults, sortReviews, startTrain, statusHasBreach, syncImages, syncInAppProducts, topFiles, trackBreachState, translateBundle, updateAchievementConfig, updateAppDetails, updateDataSafety, updateGrant, updateInAppProduct, updateLeaderboardConfig, updateListing, updateOffer, updateOneTimeOffer, updateOneTimeProduct, updateRollout, updateSubscription, updateTrackConfig, updateUser, uploadExternallyHosted, uploadImage, uploadInternalSharing, uploadRelease, validateAndCommit, validateBundleForApply, validateImage, validateLanguageCode, validatePackageName, validatePluginForApproval, validatePreSubmission, validateReleaseNotes, validateSku, validateTrackName, validateUploadFile, validateVersionCode, waitForBundle, waitForBundleProcessing, watchVitalsWithAutoHalt, wordDiff, worstSliceAverage, writeAuditLog, writeListingsToDir, writeMigrationOutput };
|
package/dist/index.js
CHANGED
|
@@ -2686,6 +2686,100 @@ var METRIC_SET_METRICS = {
|
|
|
2686
2686
|
],
|
|
2687
2687
|
errorCountMetricSet: ["errorReportCount", "distinctUsers"]
|
|
2688
2688
|
};
|
|
2689
|
+
var REQUIRED_DIMENSIONS = {
|
|
2690
|
+
slowStartRateMetricSet: ["startType"],
|
|
2691
|
+
errorCountMetricSet: ["reportType"]
|
|
2692
|
+
};
|
|
2693
|
+
function withRequiredDimensions(metricSet, query) {
|
|
2694
|
+
const required = REQUIRED_DIMENSIONS[metricSet];
|
|
2695
|
+
if (!required) return query;
|
|
2696
|
+
const dimensions = [...required];
|
|
2697
|
+
for (const dim2 of query.dimensions ?? []) {
|
|
2698
|
+
if (!dimensions.includes(dim2)) dimensions.push(dim2);
|
|
2699
|
+
}
|
|
2700
|
+
return { ...query, dimensions };
|
|
2701
|
+
}
|
|
2702
|
+
function readMetricValue(row, metric) {
|
|
2703
|
+
const metrics = row?.metrics;
|
|
2704
|
+
if (!metrics || typeof metrics !== "object") return void 0;
|
|
2705
|
+
let entry;
|
|
2706
|
+
if (Array.isArray(metrics)) {
|
|
2707
|
+
entry = metric ? metrics.find((m) => m?.metric === metric) : metrics[0];
|
|
2708
|
+
} else {
|
|
2709
|
+
const map = metrics;
|
|
2710
|
+
const key = metric ?? Object.keys(map)[0];
|
|
2711
|
+
entry = key ? map[key] : void 0;
|
|
2712
|
+
}
|
|
2713
|
+
const raw = entry?.decimalValue?.value;
|
|
2714
|
+
if (raw === void 0) return void 0;
|
|
2715
|
+
const value = Number(raw);
|
|
2716
|
+
return Number.isFinite(value) ? value : void 0;
|
|
2717
|
+
}
|
|
2718
|
+
function rowDateKey(row) {
|
|
2719
|
+
const t = row.startTime;
|
|
2720
|
+
if (!t) return "";
|
|
2721
|
+
const pad = (n) => String(n ?? 0).padStart(2, "0");
|
|
2722
|
+
return `${t.year ?? 0}-${pad(t.month)}-${pad(t.day)}T${pad(t.hours)}`;
|
|
2723
|
+
}
|
|
2724
|
+
function rowSliceKey(row) {
|
|
2725
|
+
const dims = row.dimensions;
|
|
2726
|
+
if (!dims || typeof dims !== "object") return "";
|
|
2727
|
+
const pairs = Array.isArray(dims) ? dims.map((d) => [d?.dimension ?? "", d?.stringValue ?? d?.int64Value ?? ""]) : Object.entries(dims).map(([k, d]) => [
|
|
2728
|
+
k,
|
|
2729
|
+
d?.stringValue ?? d?.int64Value ?? ""
|
|
2730
|
+
]);
|
|
2731
|
+
return JSON.stringify(pairs.sort());
|
|
2732
|
+
}
|
|
2733
|
+
function latestWorstValue(rows, metric) {
|
|
2734
|
+
if (!rows || rows.length === 0) return void 0;
|
|
2735
|
+
const latest = rows.map(rowDateKey).reduce((a, b) => b > a ? b : a);
|
|
2736
|
+
let worst;
|
|
2737
|
+
for (const row of rows) {
|
|
2738
|
+
if (rowDateKey(row) !== latest) continue;
|
|
2739
|
+
const value = readMetricValue(row, metric);
|
|
2740
|
+
if (value !== void 0 && (worst === void 0 || value > worst)) worst = value;
|
|
2741
|
+
}
|
|
2742
|
+
return worst;
|
|
2743
|
+
}
|
|
2744
|
+
function worstSliceAverage(rows, metric) {
|
|
2745
|
+
const sums = /* @__PURE__ */ new Map();
|
|
2746
|
+
for (const row of rows ?? []) {
|
|
2747
|
+
const value = readMetricValue(row, metric);
|
|
2748
|
+
if (value === void 0) continue;
|
|
2749
|
+
const key = rowSliceKey(row);
|
|
2750
|
+
const entry = sums.get(key) ?? { total: 0, count: 0 };
|
|
2751
|
+
entry.total += value;
|
|
2752
|
+
entry.count++;
|
|
2753
|
+
sums.set(key, entry);
|
|
2754
|
+
}
|
|
2755
|
+
let worst;
|
|
2756
|
+
for (const { total, count } of sums.values()) {
|
|
2757
|
+
const avg = total / count;
|
|
2758
|
+
if (worst === void 0 || avg > worst) worst = avg;
|
|
2759
|
+
}
|
|
2760
|
+
return worst;
|
|
2761
|
+
}
|
|
2762
|
+
function assertRateThreshold(label, threshold) {
|
|
2763
|
+
if (!Number.isFinite(threshold) || threshold < 0) {
|
|
2764
|
+
throw new GpcError(
|
|
2765
|
+
`Invalid ${label} threshold: expected a fraction such as 0.02.`,
|
|
2766
|
+
"THRESHOLD_INVALID",
|
|
2767
|
+
2,
|
|
2768
|
+
"Write rates as fractions: 0.02 means 2%."
|
|
2769
|
+
);
|
|
2770
|
+
}
|
|
2771
|
+
if (threshold >= 1) {
|
|
2772
|
+
throw new GpcError(
|
|
2773
|
+
`${label} threshold ${threshold} can never be reached: rates are between 0 and 1.`,
|
|
2774
|
+
"THRESHOLD_INVALID",
|
|
2775
|
+
2,
|
|
2776
|
+
`Write rates as fractions: ${threshold / 100} means ${threshold}%. Check vitals.thresholds in your config too.`
|
|
2777
|
+
);
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2780
|
+
function isGamesOnlyMetricError(err) {
|
|
2781
|
+
return err?.code === "API_METRIC_GAMES_ONLY";
|
|
2782
|
+
}
|
|
2689
2783
|
async function getFreshnessEndDate(reporting, packageName, metricSet, aggregation = "DAILY") {
|
|
2690
2784
|
try {
|
|
2691
2785
|
const info = await reporting.getMetricSetFreshness(packageName, metricSet);
|
|
@@ -2699,12 +2793,16 @@ async function getFreshnessEndDate(reporting, packageName, metricSet, aggregatio
|
|
|
2699
2793
|
}
|
|
2700
2794
|
return void 0;
|
|
2701
2795
|
}
|
|
2796
|
+
async function latestQueryableEndDate(reporting, packageName, metricSet) {
|
|
2797
|
+
const fallback = new Date(Date.now() - 2 * 24 * 60 * 60 * 1e3);
|
|
2798
|
+
const freshnessEnd = await getFreshnessEndDate(reporting, packageName, metricSet);
|
|
2799
|
+
return freshnessEnd ?? fallback;
|
|
2800
|
+
}
|
|
2702
2801
|
function buildQuery(metricSet, options, freshnessEnd) {
|
|
2703
2802
|
const metrics = METRIC_SET_METRICS[metricSet] ?? ["errorReportCount", "distinctUsers"];
|
|
2704
2803
|
const days = options?.days ?? 30;
|
|
2705
2804
|
const DAY_MS = 24 * 60 * 60 * 1e3;
|
|
2706
|
-
const
|
|
2707
|
-
const end = freshnessEnd && freshnessEnd < yesterday ? freshnessEnd : yesterday;
|
|
2805
|
+
const end = freshnessEnd ?? new Date(Date.now() - 2 * DAY_MS);
|
|
2708
2806
|
const start = new Date(end.getTime() - days * DAY_MS);
|
|
2709
2807
|
const query = {
|
|
2710
2808
|
metrics,
|
|
@@ -2725,7 +2823,7 @@ function buildQuery(metricSet, options, freshnessEnd) {
|
|
|
2725
2823
|
if (options?.dimension) {
|
|
2726
2824
|
query.dimensions = [options.dimension];
|
|
2727
2825
|
}
|
|
2728
|
-
return query;
|
|
2826
|
+
return withRequiredDimensions(metricSet, query);
|
|
2729
2827
|
}
|
|
2730
2828
|
async function queryMetric(reporting, packageName, metricSet, options) {
|
|
2731
2829
|
const freshnessEnd = await getFreshnessEndDate(
|
|
@@ -2761,16 +2859,29 @@ async function getVitalsOverview(reporting, packageName) {
|
|
|
2761
2859
|
})
|
|
2762
2860
|
);
|
|
2763
2861
|
const overview = {};
|
|
2862
|
+
const unavailable = [];
|
|
2863
|
+
let firstFailure;
|
|
2864
|
+
let succeeded = 0;
|
|
2764
2865
|
for (let i = 0; i < metricSets.length; i++) {
|
|
2765
2866
|
const entry = metricSets[i];
|
|
2766
|
-
if (!entry) continue;
|
|
2767
|
-
const key = entry[1];
|
|
2768
2867
|
const result = results[i];
|
|
2769
|
-
if (!result) continue;
|
|
2868
|
+
if (!entry || !result) continue;
|
|
2869
|
+
const key = entry[1];
|
|
2770
2870
|
if (result.status === "fulfilled") {
|
|
2771
2871
|
overview[key] = result.value.rows || [];
|
|
2872
|
+
succeeded++;
|
|
2873
|
+
continue;
|
|
2772
2874
|
}
|
|
2875
|
+
const err = result.reason;
|
|
2876
|
+
if (!isGamesOnlyMetricError(err)) firstFailure ??= result.reason;
|
|
2877
|
+
unavailable.push({
|
|
2878
|
+
metric: key,
|
|
2879
|
+
code: typeof err?.code === "string" ? err.code : "UNKNOWN_ERROR",
|
|
2880
|
+
reason: isGamesOnlyMetricError(err) ? "Only available for apps categorised as games." : typeof err?.message === "string" ? err.message : String(result.reason)
|
|
2881
|
+
});
|
|
2773
2882
|
}
|
|
2883
|
+
if (succeeded === 0 && firstFailure !== void 0) throw firstFailure;
|
|
2884
|
+
if (unavailable.length > 0) overview.unavailable = unavailable;
|
|
2774
2885
|
return overview;
|
|
2775
2886
|
}
|
|
2776
2887
|
async function getVitalsCrashes(reporting, packageName, options) {
|
|
@@ -2780,8 +2891,7 @@ async function getVitalsAnr(reporting, packageName, options) {
|
|
|
2780
2891
|
return queryMetric(reporting, packageName, "anrRateMetricSet", options);
|
|
2781
2892
|
}
|
|
2782
2893
|
async function getVitalsStartup(reporting, packageName, options) {
|
|
2783
|
-
|
|
2784
|
-
return queryMetric(reporting, packageName, "slowStartRateMetricSet", opts);
|
|
2894
|
+
return queryMetric(reporting, packageName, "slowStartRateMetricSet", options);
|
|
2785
2895
|
}
|
|
2786
2896
|
async function getVitalsRendering(reporting, packageName, options) {
|
|
2787
2897
|
return queryMetric(reporting, packageName, "slowRenderingRateMetricSet", options);
|
|
@@ -2811,8 +2921,7 @@ async function getVitalsMemoryBitmap(reporting, packageName, options) {
|
|
|
2811
2921
|
});
|
|
2812
2922
|
}
|
|
2813
2923
|
async function getVitalsErrorCount(reporting, packageName, options) {
|
|
2814
|
-
|
|
2815
|
-
return queryMetric(reporting, packageName, "errorCountMetricSet", opts);
|
|
2924
|
+
return queryMetric(reporting, packageName, "errorCountMetricSet", options);
|
|
2816
2925
|
}
|
|
2817
2926
|
async function getVitalsAnomalies(reporting, packageName) {
|
|
2818
2927
|
return reporting.getAnomalies(packageName);
|
|
@@ -2836,7 +2945,7 @@ async function compareVitalsTrend(reporting, packageName, metricSet, days = 7) {
|
|
|
2836
2945
|
month: d.getUTCMonth() + 1,
|
|
2837
2946
|
day: d.getUTCDate()
|
|
2838
2947
|
});
|
|
2839
|
-
const makeQuery = (start, end) => ({
|
|
2948
|
+
const makeQuery = (start, end) => withRequiredDimensions(metricSet, {
|
|
2840
2949
|
metrics,
|
|
2841
2950
|
timelineSpec: {
|
|
2842
2951
|
aggregationPeriod: "DAILY",
|
|
@@ -2940,7 +3049,8 @@ function watchVitalsWithAutoHalt(reporting, packageName, options) {
|
|
|
2940
3049
|
threshold,
|
|
2941
3050
|
metricSet = "crashRateMetricSet",
|
|
2942
3051
|
onHalt,
|
|
2943
|
-
onPoll
|
|
3052
|
+
onPoll,
|
|
3053
|
+
onError
|
|
2944
3054
|
} = options;
|
|
2945
3055
|
let stopped = false;
|
|
2946
3056
|
let haltTriggered = false;
|
|
@@ -2948,16 +3058,15 @@ function watchVitalsWithAutoHalt(reporting, packageName, options) {
|
|
|
2948
3058
|
if (stopped) return;
|
|
2949
3059
|
try {
|
|
2950
3060
|
const result = await queryMetric(reporting, packageName, metricSet, { days: 1 });
|
|
2951
|
-
const
|
|
2952
|
-
const firstMetric = latestRow?.metrics ? Object.keys(latestRow.metrics)[0] : void 0;
|
|
2953
|
-
const value = firstMetric ? Number(latestRow?.metrics[firstMetric]?.decimalValue?.value) : void 0;
|
|
3061
|
+
const value = latestWorstValue(result.rows, METRIC_SET_METRICS[metricSet]?.[0]);
|
|
2954
3062
|
const breached = value !== void 0 && value > threshold;
|
|
2955
3063
|
onPoll?.(value, breached);
|
|
2956
3064
|
if (breached && !haltTriggered && onHalt) {
|
|
2957
3065
|
haltTriggered = true;
|
|
2958
3066
|
await onHalt(value);
|
|
2959
3067
|
}
|
|
2960
|
-
} catch {
|
|
3068
|
+
} catch (err) {
|
|
3069
|
+
onError?.(err);
|
|
2961
3070
|
}
|
|
2962
3071
|
if (!stopped) {
|
|
2963
3072
|
timerId = setTimeout(poll, intervalMs);
|
|
@@ -3023,33 +3132,28 @@ var WARN_MARGIN = 0.2;
|
|
|
3023
3132
|
function toApiDate(d) {
|
|
3024
3133
|
return { year: d.getUTCFullYear(), month: d.getUTCMonth() + 1, day: d.getUTCDate() };
|
|
3025
3134
|
}
|
|
3026
|
-
async function queryVitalForStatus(reporting, packageName, metricSet, days, offsetDays = 0) {
|
|
3135
|
+
async function queryVitalForStatus(reporting, packageName, metricSet, days, latestEnd, offsetDays = 0) {
|
|
3027
3136
|
const DAY_MS = 24 * 60 * 60 * 1e3;
|
|
3028
|
-
const baseMs =
|
|
3137
|
+
const baseMs = latestEnd.getTime() - offsetDays * DAY_MS;
|
|
3029
3138
|
const end = new Date(baseMs);
|
|
3030
3139
|
const start = new Date(baseMs - days * DAY_MS);
|
|
3031
3140
|
const metrics = METRIC_SET_METRICS2[metricSet] ?? ["distinctUsers"];
|
|
3032
|
-
const query = {
|
|
3141
|
+
const query = withRequiredDimensions(metricSet, {
|
|
3033
3142
|
metrics,
|
|
3034
3143
|
timelineSpec: {
|
|
3035
3144
|
aggregationPeriod: "DAILY",
|
|
3036
3145
|
startTime: toApiDate(start),
|
|
3037
3146
|
endTime: toApiDate(end)
|
|
3038
3147
|
}
|
|
3039
|
-
};
|
|
3148
|
+
});
|
|
3040
3149
|
const result = await reporting.queryMetricSet(packageName, metricSet, query);
|
|
3041
|
-
|
|
3042
|
-
const values = result.rows.map((row) => {
|
|
3043
|
-
const firstKey = Object.keys(row.metrics)[0];
|
|
3044
|
-
return firstKey ? Number(row.metrics[firstKey]?.decimalValue?.value) : NaN;
|
|
3045
|
-
}).filter((v) => !isNaN(v));
|
|
3046
|
-
if (values.length === 0) return void 0;
|
|
3047
|
-
return values.reduce((a, b) => a + b, 0) / values.length;
|
|
3150
|
+
return worstSliceAverage(result.rows, metrics[0]);
|
|
3048
3151
|
}
|
|
3049
3152
|
async function queryVitalWithTrend(reporting, packageName, metricSet, days) {
|
|
3153
|
+
const latestEnd = await latestQueryableEndDate(reporting, packageName, metricSet);
|
|
3050
3154
|
const [current, previous] = await Promise.all([
|
|
3051
|
-
queryVitalForStatus(reporting, packageName, metricSet, days, 0),
|
|
3052
|
-
queryVitalForStatus(reporting, packageName, metricSet, days, days)
|
|
3155
|
+
queryVitalForStatus(reporting, packageName, metricSet, days, latestEnd, 0),
|
|
3156
|
+
queryVitalForStatus(reporting, packageName, metricSet, days, latestEnd, days)
|
|
3053
3157
|
]);
|
|
3054
3158
|
let trend = null;
|
|
3055
3159
|
if (current !== void 0 && previous !== void 0) {
|
|
@@ -3147,7 +3251,7 @@ async function getAppStatus(client, reporting, packageName, options = {}) {
|
|
|
3147
3251
|
if (slowStartResult.status === "rejected")
|
|
3148
3252
|
sectionErrors.push(`vitals/slowStarts: ${String(slowStartResult.reason)}`);
|
|
3149
3253
|
const slowRender = slowRenderResult.status === "fulfilled" ? slowRenderResult.value : SKIPPED_VITAL;
|
|
3150
|
-
if (slowRenderResult.status === "rejected")
|
|
3254
|
+
if (slowRenderResult.status === "rejected" && !isGamesOnlyMetricError(slowRenderResult.reason))
|
|
3151
3255
|
sectionErrors.push(`vitals/slowRender: ${String(slowRenderResult.reason)}`);
|
|
3152
3256
|
const rawReviews = reviewsResult.status === "fulfilled" ? reviewsResult.value.reviews : [];
|
|
3153
3257
|
if (reviewsResult.status === "rejected")
|
|
@@ -3528,31 +3632,34 @@ async function queryWatchMetric(reporting, packageName, metric) {
|
|
|
3528
3632
|
const metricSet = METRIC_TO_SET[metric];
|
|
3529
3633
|
const metricKey = METRIC_KEY[metric];
|
|
3530
3634
|
const DAY_MS = 24 * 60 * 60 * 1e3;
|
|
3531
|
-
const end = new Date(Date.now() - 2 * DAY_MS);
|
|
3532
|
-
const start = new Date(end.getTime() - 7 * DAY_MS);
|
|
3533
3635
|
const toDate = (d) => ({
|
|
3534
3636
|
year: d.getUTCFullYear(),
|
|
3535
3637
|
month: d.getUTCMonth() + 1,
|
|
3536
3638
|
day: d.getUTCDate()
|
|
3537
3639
|
});
|
|
3538
3640
|
try {
|
|
3539
|
-
const
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
return
|
|
3554
|
-
} catch {
|
|
3555
|
-
|
|
3641
|
+
const end = await latestQueryableEndDate(reporting, packageName, metricSet);
|
|
3642
|
+
const start = new Date(end.getTime() - 7 * DAY_MS);
|
|
3643
|
+
const result = await reporting.queryMetricSet(
|
|
3644
|
+
packageName,
|
|
3645
|
+
metricSet,
|
|
3646
|
+
withRequiredDimensions(metricSet, {
|
|
3647
|
+
metrics: [metricKey, "distinctUsers"],
|
|
3648
|
+
timelineSpec: {
|
|
3649
|
+
aggregationPeriod: "DAILY",
|
|
3650
|
+
startTime: toDate(start),
|
|
3651
|
+
endTime: toDate(end)
|
|
3652
|
+
}
|
|
3653
|
+
})
|
|
3654
|
+
);
|
|
3655
|
+
return { value: latestWorstValue(result.rows, metricKey) };
|
|
3656
|
+
} catch (err) {
|
|
3657
|
+
const code = err?.code;
|
|
3658
|
+
return {
|
|
3659
|
+
value: void 0,
|
|
3660
|
+
error: typeof code === "string" ? code : err instanceof Error ? err.message : String(err),
|
|
3661
|
+
gamesOnly: isGamesOnlyMetricError(err)
|
|
3662
|
+
};
|
|
3556
3663
|
}
|
|
3557
3664
|
}
|
|
3558
3665
|
async function fetchRollout(client, packageName, track) {
|
|
@@ -3631,12 +3738,17 @@ async function runWatch(client, reporting, config, callbacks) {
|
|
|
3631
3738
|
"Watch interval must be at least 60 seconds",
|
|
3632
3739
|
"WATCH_USAGE_ERROR",
|
|
3633
3740
|
2,
|
|
3634
|
-
"Use --interval 60 or higher. Google vitals data is delayed
|
|
3741
|
+
"Use --interval 60 or higher. Google vitals data is delayed 2-4 days, so frequent polling wastes API quota."
|
|
3635
3742
|
);
|
|
3636
3743
|
}
|
|
3744
|
+
for (const [metric, threshold] of Object.entries(config.thresholds)) {
|
|
3745
|
+
if (metric !== "errorCount" && threshold !== void 0) assertRateThreshold(metric, threshold);
|
|
3746
|
+
}
|
|
3637
3747
|
let running = true;
|
|
3638
3748
|
const events = [];
|
|
3639
3749
|
let breachCount = 0;
|
|
3750
|
+
let unreadableRounds = 0;
|
|
3751
|
+
let lastRoundUnreadable = false;
|
|
3640
3752
|
let halted = false;
|
|
3641
3753
|
const startTime = Date.now();
|
|
3642
3754
|
const cleanup = () => {
|
|
@@ -3652,19 +3764,24 @@ async function runWatch(client, reporting, config, callbacks) {
|
|
|
3652
3764
|
const vitals = {};
|
|
3653
3765
|
const metricResults = await Promise.all(
|
|
3654
3766
|
config.metrics.map(async (metric) => {
|
|
3655
|
-
const
|
|
3656
|
-
return { metric,
|
|
3767
|
+
const reading = await queryWatchMetric(reporting, config.packageName, metric);
|
|
3768
|
+
return { metric, ...reading };
|
|
3657
3769
|
})
|
|
3658
3770
|
);
|
|
3659
|
-
|
|
3771
|
+
let unreadable = false;
|
|
3772
|
+
for (const { metric, value, error, gamesOnly } of metricResults) {
|
|
3660
3773
|
const threshold = config.thresholds[metric];
|
|
3774
|
+
if (error && threshold !== void 0 && !gamesOnly) unreadable = true;
|
|
3661
3775
|
const result = threshold !== void 0 ? checkThreshold(value, threshold) : { breached: false };
|
|
3662
3776
|
vitals[metric] = {
|
|
3663
3777
|
value,
|
|
3664
3778
|
threshold,
|
|
3665
|
-
breached: result.breached
|
|
3779
|
+
breached: result.breached,
|
|
3780
|
+
...error ? { error } : {}
|
|
3666
3781
|
};
|
|
3667
3782
|
}
|
|
3783
|
+
if (unreadable) unreadableRounds++;
|
|
3784
|
+
lastRoundUnreadable = unreadable;
|
|
3668
3785
|
const breaches = Object.entries(vitals).filter(([, v]) => v.breached).map(([k]) => k);
|
|
3669
3786
|
const event = {
|
|
3670
3787
|
round,
|
|
@@ -3716,6 +3833,8 @@ async function runWatch(client, reporting, config, callbacks) {
|
|
|
3716
3833
|
durationMs: Date.now() - startTime,
|
|
3717
3834
|
finalRollout: events[events.length - 1]?.rollout?.userFraction ?? null,
|
|
3718
3835
|
breachCount,
|
|
3836
|
+
unreadableRounds,
|
|
3837
|
+
lastRoundUnreadable,
|
|
3719
3838
|
halted,
|
|
3720
3839
|
events
|
|
3721
3840
|
};
|
|
@@ -5411,10 +5530,9 @@ async function advanceTrain(apiClient, reportingClient, packageName) {
|
|
|
5411
5530
|
if (state.gates) {
|
|
5412
5531
|
if (state.gates.crashes?.max !== void 0) {
|
|
5413
5532
|
const result = await getVitalsCrashes(reportingClient, packageName, { days: 1 });
|
|
5414
|
-
const
|
|
5415
|
-
|
|
5416
|
-
|
|
5417
|
-
if (value !== void 0 && value > state.gates.crashes.max / 100) {
|
|
5533
|
+
const value = latestWorstValue(result.rows, "crashRate");
|
|
5534
|
+
if (value === void 0) throw trainGateNoData("Crash", "crashes");
|
|
5535
|
+
if (value > state.gates.crashes.max / 100) {
|
|
5418
5536
|
state.status = "paused";
|
|
5419
5537
|
state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5420
5538
|
await writeTrainState(packageName, state);
|
|
@@ -5428,10 +5546,9 @@ async function advanceTrain(apiClient, reportingClient, packageName) {
|
|
|
5428
5546
|
}
|
|
5429
5547
|
if (state.gates.anr?.max !== void 0) {
|
|
5430
5548
|
const result = await getVitalsAnr(reportingClient, packageName, { days: 1 });
|
|
5431
|
-
const
|
|
5432
|
-
|
|
5433
|
-
|
|
5434
|
-
if (value !== void 0 && value > state.gates.anr.max / 100) {
|
|
5549
|
+
const value = latestWorstValue(result.rows, "anrRate");
|
|
5550
|
+
if (value === void 0) throw trainGateNoData("ANR", "anr");
|
|
5551
|
+
if (value > state.gates.anr.max / 100) {
|
|
5435
5552
|
state.status = "paused";
|
|
5436
5553
|
state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5437
5554
|
await writeTrainState(packageName, state);
|
|
@@ -5463,6 +5580,14 @@ async function executeStage(apiClient, packageName, state, stageIndex) {
|
|
|
5463
5580
|
state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5464
5581
|
await writeTrainState(packageName, state);
|
|
5465
5582
|
}
|
|
5583
|
+
function trainGateNoData(label, command) {
|
|
5584
|
+
return new GpcError(
|
|
5585
|
+
`${label} gate has no data to check, so the train was not advanced.`,
|
|
5586
|
+
"TRAIN_GATE_NO_DATA",
|
|
5587
|
+
6,
|
|
5588
|
+
`Vitals data often lags 2-4 days. Run the train again later, or check with: gpc vitals ${command} --days 3`
|
|
5589
|
+
);
|
|
5590
|
+
}
|
|
5466
5591
|
|
|
5467
5592
|
// src/commands/games.ts
|
|
5468
5593
|
import { mkdir as mkdir5, readdir as readdir4, readFile as readFile9, writeFile as writeFile5 } from "fs/promises";
|
|
@@ -10404,6 +10529,7 @@ export {
|
|
|
10404
10529
|
analyzeReviews2 as analyzeReviews,
|
|
10405
10530
|
annotateListResult,
|
|
10406
10531
|
applyReleaseNotes,
|
|
10532
|
+
assertRateThreshold,
|
|
10407
10533
|
assignRelease,
|
|
10408
10534
|
batchGetOrders,
|
|
10409
10535
|
batchSyncInAppProducts,
|
|
@@ -10558,6 +10684,7 @@ export {
|
|
|
10558
10684
|
isValidStatsDimension,
|
|
10559
10685
|
isVersionedNotesDir,
|
|
10560
10686
|
isZip,
|
|
10687
|
+
latestWorstValue,
|
|
10561
10688
|
lintListing,
|
|
10562
10689
|
lintListings,
|
|
10563
10690
|
lintLocalListings,
|
|
@@ -10609,6 +10736,7 @@ export {
|
|
|
10609
10736
|
pushLeaderboardConfigs,
|
|
10610
10737
|
pushListings,
|
|
10611
10738
|
readListingsFromDir,
|
|
10739
|
+
readMetricValue,
|
|
10612
10740
|
readReleaseNotesForVersion,
|
|
10613
10741
|
readReleaseNotesFromDir,
|
|
10614
10742
|
readinessBadgeColor,
|
|
@@ -10698,6 +10826,7 @@ export {
|
|
|
10698
10826
|
waitForBundleProcessing,
|
|
10699
10827
|
watchVitalsWithAutoHalt,
|
|
10700
10828
|
wordDiff,
|
|
10829
|
+
worstSliceAverage,
|
|
10701
10830
|
writeAuditLog,
|
|
10702
10831
|
writeListingsToDir,
|
|
10703
10832
|
writeMigrationOutput
|