@hot-updater/console 1.1.3 → 1.1.4
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/.output/nitro.json +1 -1
- package/.output/public/assets/EventDetails-gbD_fCqc.js +1 -0
- package/.output/public/assets/{InsightsPageHeader-SYEOMClX.js → InsightsPageHeader-BRw8XkJW.js} +1 -1
- package/.output/public/assets/api-keys-D81VHHNM.js +1 -0
- package/.output/public/assets/api-keys-Dia6i8K1.js +1 -0
- package/.output/public/assets/badge-Bqf3UDee.js +1 -0
- package/.output/public/assets/circle-off-BsbZQtsF.js +1 -0
- package/.output/public/assets/field-GxF9SyUo.js +1 -0
- package/.output/public/assets/{index-C5S4DrO2.js → index-DrkdiRIn.js} +3 -3
- package/.output/public/assets/insights-C-5t8wh2.js +2 -0
- package/.output/public/assets/insights-D_ngQdiz.js +1 -0
- package/.output/public/assets/insights-KigRBDgC.js +42 -0
- package/.output/public/assets/insights-api-1ZjVu3Sb.js +1 -0
- package/.output/public/assets/insights-search-aT8BVCci.js +1 -0
- package/.output/public/assets/{insights-usage-rpc-DTL0_Rp8.js → insights-usage-rpc-DqTiXI1_.js} +1 -1
- package/.output/public/assets/insights_.distribution-Bzl6RFBR.js +2 -0
- package/.output/public/assets/insights_.distribution-Cthgttqb.js +1 -0
- package/.output/public/assets/insights_.distribution-D2c2v1bu.js +1 -0
- package/.output/public/assets/installations-2Nx8Htf-.js +1 -0
- package/.output/public/assets/{installations-we1IV0xp.js → installations-BskJ1xQr.js} +2 -2
- package/.output/public/assets/installations-K2iJ_r0h.js +1 -0
- package/.output/public/assets/{remote-config-uBgCaI-p.js → remote-config-D9X3NiwV.js} +4 -4
- package/.output/public/assets/{remote-config-DyTLPRKh.js → remote-config-DzOOiBIO.js} +1 -1
- package/.output/public/assets/remote-config-ikg7kak0.js +1 -0
- package/.output/public/assets/routes-9rb9P5Y7.js +2 -0
- package/.output/public/assets/routes-P7683vKQ.js +1 -0
- package/.output/public/assets/{select-CV8nkfsf.js → select-M3H2gxCc.js} +1 -1
- package/.output/public/assets/styles-CpiW_YdW.css +2 -0
- package/.output/public/assets/{switch-C8ycp9AH.js → switch-By_vfm8I.js} +1 -1
- package/.output/server/{__23tanstack-start-server-fn-resolver-Gb7cVpng.mjs → __23tanstack-start-server-fn-resolver-KpQ5Z_24.mjs} +40 -40
- package/.output/server/_libs/@base-ui/react+[...].mjs +77 -1
- package/.output/server/_libs/@tanstack/react-router+[...].mjs +1 -1
- package/.output/server/_libs/@tanstack/react-router-ssr-query+[...].mjs +2 -2
- package/.output/server/_libs/lucide-react.mjs +1 -1
- package/.output/server/_libs/next-themes.mjs +1 -1
- package/.output/server/_libs/react-redux+[...].mjs +1 -1
- package/.output/server/_libs/recharts+[...].mjs +1 -1
- package/.output/server/_libs/sonner.mjs +1 -1
- package/.output/server/_libs/tanstack__react-query.mjs +1 -1
- package/.output/server/_ssr/{BundleIdDisplay-DDiNTVT3.mjs → BundleIdDisplay-CjSnY65J.mjs} +3 -3
- package/.output/server/_ssr/{ConsoleFeatureUnavailable-DE65d293.mjs → ConsoleFeatureUnavailable-CHxn9f-w.mjs} +7 -7
- package/.output/server/_ssr/{EventDetails-Bg1V3JsJ.mjs → EventDetails-l6dF54zp.mjs} +8 -8
- package/.output/server/_ssr/{HashValueDisplay-BDHvSsxT.mjs → HashValueDisplay-Zs7ABoOi.mjs} +3 -3
- package/.output/server/_ssr/{InsightsPageHeader-Db-y_PRS.mjs → InsightsPageHeader-ZlAlDxQy.mjs} +8 -8
- package/.output/server/_ssr/{NotFoundPage-DHgL7Uzm.mjs → NotFoundPage-u28ndvp1.mjs} +3 -3
- package/.output/server/_ssr/{alert-dialog-MbJW0K9H.mjs → alert-dialog-B8AqlJwo.mjs} +2 -2
- package/.output/server/_ssr/{api-XOdKU3tw.mjs → api-C_0B7NXM.mjs} +3 -3
- package/.output/server/_ssr/{api-keys-v7wa2_rX.mjs → api-keys-CLH38u9B.mjs} +2 -2
- package/.output/server/_ssr/{api-keys-BHJdcnez.mjs → api-keys-pRNvn2-9.mjs} +9 -8
- package/.output/server/_ssr/{api-keys-rpc-B389z2LY.mjs → api-keys-rpc-wecWs0nA.mjs} +3 -3
- package/.output/server/_ssr/{api-rpc-T6dVPhPr.mjs → api-rpc-DWN8eOHS.mjs} +6 -41
- package/.output/server/_ssr/{auth-rpc-CzPT9U2N.mjs → auth-rpc-CLRArbIl.mjs} +4 -4
- package/.output/server/_ssr/{auth.server-LmCV7BTA.mjs → auth.server-DR9zc0EU.mjs} +2 -2
- package/.output/server/_ssr/badge-BpT7r-ng.mjs +108 -0
- package/.output/server/_ssr/{bundleActivity-M5gzBFNg.mjs → bundleActivity-0qFIcvqt.mjs} +1 -1
- package/.output/server/_ssr/{config.server-COvZCTA4.mjs → config.server-DTdsG47T.mjs} +4 -4
- package/.output/server/_ssr/{console-access-CRAcskRF.mjs → console-access-aUpQs49I.mjs} +2 -2
- package/.output/server/_ssr/{console-features-Oz0q20jJ.mjs → console-features-4xAGfZHw.mjs} +1 -1
- package/.output/server/_ssr/{console-features-api-qihYDNRo.mjs → console-features-api-DSSgCV3v.mjs} +5 -5
- package/.output/server/_ssr/{console-features-rpc-r1KcSohP.mjs → console-features-rpc-D6kMlR2A.mjs} +3 -3
- package/.output/server/_ssr/{dialog-CPbM7GQc.mjs → dialog-B4ALf7NJ.mjs} +2 -2
- package/.output/server/_ssr/{downloadBundle-CZysjmNW.mjs → downloadBundle-94jPBGSO.mjs} +1 -1
- package/.output/server/_ssr/{empty-8scgvztU.mjs → empty-Bh1FEeYF.mjs} +3 -3
- package/.output/server/_ssr/{failureReports-DKDhVsXA.mjs → failureReports-Bh22wois.mjs} +4 -4
- package/.output/server/_ssr/field-HKCaPH3e.mjs +89 -0
- package/.output/server/_ssr/{getBundleChildren-Cv9iz1As.mjs → getBundleChildren-DfxB2K8D.mjs} +1 -1
- package/.output/server/_ssr/{input-group-BTB34g3v.mjs → input-group-xm3LzDuq.mjs} +2 -2
- package/.output/server/_ssr/{insights-CGpn8W5V.mjs → insights-BKZpJ_K4.mjs} +590 -360
- package/.output/server/_ssr/{insights-BulsRVnt.mjs → insights-CCyTACIw.mjs} +5 -5
- package/.output/server/_ssr/{insights-Btnkt5-Y.mjs → insights-CNqWQLJR.mjs} +2 -2
- package/.output/server/_ssr/{insights-api-Dg3D7Vkg.mjs → insights-api-DtRFz5X3.mjs} +5 -5
- package/.output/server/_ssr/{insights-errors-BjnFUyZj.mjs → insights-errors-Iy-lBaM7.mjs} +2 -2
- package/.output/server/_ssr/{insights-failures-xaa41Buf.mjs → insights-failures-BJN3RfLu.mjs} +3 -3
- package/.output/server/_ssr/{insights-failures-rpc-DdK55wRd.mjs → insights-failures-rpc-DLZ91uOG.mjs} +12 -12
- package/.output/server/_ssr/{insights-recovery-BWQhiZaF.mjs → insights-recovery-BUuQdlA3.mjs} +1 -1
- package/.output/server/_ssr/{insights-recovery-rpc-D-dwlIPT.mjs → insights-recovery-rpc-BQHUhPi7.mjs} +6 -6
- package/.output/server/_ssr/{insights-rpc-Dn-jdkQK.mjs → insights-rpc-C6Vukyn7.mjs} +3 -3
- package/.output/server/_ssr/{insights-search-C8EWA0hu.mjs → insights-search-CBYoxT7v.mjs} +8 -7
- package/.output/server/_ssr/{insights-usage-BpBM0U0c.mjs → insights-usage-CHqvtehH.mjs} +1 -1
- package/.output/server/_ssr/{insights-usage-rpc-DIa-5vUO.mjs → insights-usage-rpc-BJ7a-xco.mjs} +6 -6
- package/.output/server/_ssr/{insights-usage-rpc-BCulD17c.mjs → insights-usage-rpc-C-lNUIsD.mjs} +5 -5
- package/.output/server/_ssr/{insightsUsage-CzhVjsc8.mjs → insightsUsage-COUG-_-U.mjs} +3 -3
- package/.output/server/_ssr/{insights_.distribution-CKe3T4k1.mjs → insights_.distribution-CTwilMPm.mjs} +5 -5
- package/.output/server/_ssr/{insights_.distribution-D7ZzykQb.mjs → insights_.distribution-DmLW8I8a.mjs} +2 -2
- package/.output/server/_ssr/{insights_.distribution-CE0ve_Rx.mjs → insights_.distribution-DztDyrBj.mjs} +14 -13
- package/.output/server/_ssr/{installations-CJHIYH0u.mjs → installations-B6MK5iA8.mjs} +2 -2
- package/.output/server/_ssr/{installations-Bu4Sk4W6.mjs → installations-Bct7ag5y.mjs} +4 -4
- package/.output/server/_ssr/{installations-CGMSRurj.mjs → installations-_N4PtYFl.mjs} +12 -11
- package/.output/server/_ssr/{release-adoption-005tQRpo.mjs → release-adoption-DuYJTGuX.mjs} +17 -7
- package/.output/server/_ssr/{release-adoption-rpc-DPMYNIlM.mjs → release-adoption-rpc-CHl7G_Zy.mjs} +8 -8
- package/.output/server/_ssr/{releaseAdoption-CtOFkwli.mjs → releaseAdoption-B39cpvfo.mjs} +42 -26
- package/.output/server/_ssr/releaseReachability-bFsuElSS.mjs +39 -0
- package/.output/server/_ssr/{remote-config-BCW6vPOD.mjs → remote-config-10-obSZ0.mjs} +3 -3
- package/.output/server/_ssr/{remote-config-DrC_3H7K.mjs → remote-config-Dj1eTYi4.mjs} +2 -2
- package/.output/server/_ssr/{remote-config-Ca-Wv5sF.mjs → remote-config-Mj05qVYZ.mjs} +18 -17
- package/.output/server/_ssr/{remote-config-rpc-CcGmTmvS.mjs → remote-config-rpc-lteqDSLn.mjs} +3 -3
- package/.output/server/_ssr/{router-C5eI7aq7.mjs → router-Dr4jjjhx.mjs} +17 -17
- package/.output/server/_ssr/{routes-Bd3qlRny.mjs → routes-B5KrCDwk.mjs} +17 -16
- package/.output/server/_ssr/{routes-DZoij0js.mjs → routes-BZbrY_qw.mjs} +17 -17
- package/.output/server/_ssr/{runtime.server-De0QVWc9.mjs → runtime.server-DsHqVqTr.mjs} +2 -2
- package/.output/server/_ssr/{select-usyW0pcU.mjs → select-BtdGNSmp.mjs} +2 -2
- package/.output/server/_ssr/{server-Bp9nEe3Z.mjs → server-BxaHSwHh.mjs} +4 -4
- package/.output/server/_ssr/{sidebar-Bqu4emj1.mjs → sidebar-CVsgosaD.mjs} +3 -3
- package/.output/server/_ssr/ssr.mjs +1 -1
- package/.output/server/_ssr/{switch-DJPcwvwD.mjs → switch-BmARgkUP.mjs} +3 -3
- package/.output/server/_ssr/{table-DEzdHi1t.mjs → table-M5P3YUfY.mjs} +2 -2
- package/.output/server/_ssr/{tabs-HzTSngsE.mjs → tabs-CrmfOlJj.mjs} +3 -3
- package/.output/server/_ssr/updateFailures-z1IQy_4q.mjs +103 -0
- package/.output/server/{_tanstack-start-manifest_v-DTOQk1l4.mjs → _tanstack-start-manifest_v-_BY9hYTm.mjs} +51 -50
- package/.output/server/index.mjs +198 -191
- package/package.json +7 -7
- package/src/components/features/insights/AppUsage.tsx +15 -2
- package/src/components/features/insights/InsightsControls.tsx +264 -246
- package/src/components/features/insights/InsightsOverview.tsx +90 -68
- package/src/components/features/insights/InsightsScope.tsx +39 -0
- package/src/components/features/insights/ReleaseActivity.tsx +4 -4
- package/src/components/features/insights/ReleaseCombobox.tsx +196 -0
- package/src/components/features/insights/RollbackButton.tsx +4 -0
- package/src/components/features/insights/UpdateFailures.tsx +7 -1
- package/src/lib/insights-failures-rpc.ts +3 -1
- package/src/lib/insights-failures.ts +5 -2
- package/src/lib/insights-recovery.ts +5 -0
- package/src/lib/insights-search.ts +10 -14
- package/src/lib/release-adoption-api.ts +24 -32
- package/src/lib/release-adoption.ts +45 -8
- package/src/lib/server/failureReports.ts +1 -1
- package/src/lib/server/releaseAdoption.ts +75 -26
- package/src/lib/server/updateFailures.ts +131 -10
- package/src/routes/insights.tsx +56 -24
- package/src/routes/insights_.distribution.tsx +2 -1
- package/.output/public/assets/EventDetails-CSX2ayg9.js +0 -1
- package/.output/public/assets/api-keys-BEdLKiib.js +0 -1
- package/.output/public/assets/api-keys-TdTtKm2b.js +0 -1
- package/.output/public/assets/chevron-right-BnvHIJdZ.js +0 -1
- package/.output/public/assets/field-DBMkBfkD.js +0 -1
- package/.output/public/assets/insights-BMKRCIg1.js +0 -2
- package/.output/public/assets/insights-CPyViuyC.js +0 -42
- package/.output/public/assets/insights-D0PbX-Rm.js +0 -1
- package/.output/public/assets/insights-api-Dmd_LgT3.js +0 -1
- package/.output/public/assets/insights-search-DAa0f7Dr.js +0 -1
- package/.output/public/assets/insights_.distribution-BaOQlBV9.js +0 -1
- package/.output/public/assets/insights_.distribution-CxXMIEb5.js +0 -2
- package/.output/public/assets/insights_.distribution-EE5OqxyL.js +0 -1
- package/.output/public/assets/installations-BgISglUQ.js +0 -1
- package/.output/public/assets/installations-DQbgRkSi.js +0 -1
- package/.output/public/assets/remote-config-AQmdmbT8.js +0 -1
- package/.output/public/assets/routes-Br26jzt9.js +0 -1
- package/.output/public/assets/routes-CyirpG2O.js +0 -2
- package/.output/public/assets/styles-DTGEAJFv.css +0 -2
- package/.output/server/_ssr/field-DHrweW4l.mjs +0 -189
- package/.output/server/_ssr/updateFailures-H6pnnYK4.mjs +0 -40
- /package/.output/public/assets/{ConsoleFeatureUnavailable-B-iNWpuE.js → ConsoleFeatureUnavailable-C0KfsKWL.js} +0 -0
- /package/.output/public/assets/{table-DgGD67Fx.js → table-ek1DmRHq.js} +0 -0
- /package/.output/public/assets/{valueToPercent-C4C1WZVn.js → valueToPercent-B44vDssN.js} +0 -0
|
@@ -4,10 +4,11 @@ import { insightsPeriodEnd, recoveryWindows } from "./insights-recovery";
|
|
|
4
4
|
import type { InsightsWindow } from "./insights-rpc";
|
|
5
5
|
import type { HealthChart } from "./insights-search";
|
|
6
6
|
import {
|
|
7
|
-
DEFAULT_ADOPTION_RELEASES,
|
|
8
7
|
type AdoptionRelease,
|
|
9
8
|
type BundleEventType,
|
|
10
9
|
type BundleEventsSeries,
|
|
10
|
+
defaultAdoptionReleases,
|
|
11
|
+
type HealthPlatform,
|
|
11
12
|
MAX_ADOPTION_RELEASES,
|
|
12
13
|
} from "./release-adoption";
|
|
13
14
|
import {
|
|
@@ -39,7 +40,10 @@ export type ReleaseHealthState = {
|
|
|
39
40
|
readonly durationMs: number;
|
|
40
41
|
readonly intervalMs: number;
|
|
41
42
|
};
|
|
42
|
-
/**
|
|
43
|
+
/**
|
|
44
|
+
* The newest bundle deployments and the newest enabled ones, newest
|
|
45
|
+
* first; undefined while loading.
|
|
46
|
+
*/
|
|
43
47
|
readonly candidates: readonly AdoptionRelease[] | undefined;
|
|
44
48
|
/** The compared bundles; undefined until they are known. */
|
|
45
49
|
readonly releases: readonly ComparedBundle[] | undefined;
|
|
@@ -64,10 +68,10 @@ export function useReleaseHealth({
|
|
|
64
68
|
focusReleaseId,
|
|
65
69
|
chart,
|
|
66
70
|
}: {
|
|
67
|
-
readonly platform:
|
|
71
|
+
readonly platform: HealthPlatform;
|
|
68
72
|
readonly channel: string;
|
|
69
73
|
readonly window: InsightsWindow;
|
|
70
|
-
/** Chosen bundles; undefined, the focus and its previous, or the
|
|
74
|
+
/** Chosen bundles; undefined, the focus and its previous, or the live ones. */
|
|
71
75
|
readonly releaseIds: readonly string[] | undefined;
|
|
72
76
|
readonly focusReleaseId: string | undefined;
|
|
73
77
|
readonly chart: HealthChart;
|
|
@@ -83,23 +87,14 @@ export function useReleaseHealth({
|
|
|
83
87
|
staleTime: 5 * 60_000,
|
|
84
88
|
});
|
|
85
89
|
const candidates = candidatesQuery.data;
|
|
86
|
-
|
|
87
|
-
focusReleaseId === undefined || candidates === undefined
|
|
88
|
-
? -1
|
|
89
|
-
: candidates.findIndex((release) => release.releaseId === focusReleaseId);
|
|
90
|
-
// A focus among the newest has its previous bundle there too, unless it is
|
|
91
|
-
// the oldest of them.
|
|
90
|
+
// A focus and the bundle deployed before it on its platform.
|
|
92
91
|
const focusQuery = useQuery({
|
|
93
92
|
queryKey: ["insights", "adoption-release", scope, focusReleaseId, true],
|
|
94
93
|
queryFn: () =>
|
|
95
94
|
getAdoptionReleaseRpc({
|
|
96
95
|
data: { ...scope, releaseId: focusReleaseId!, withPrevious: true },
|
|
97
96
|
}),
|
|
98
|
-
enabled:
|
|
99
|
-
releaseIds === undefined &&
|
|
100
|
-
focusReleaseId !== undefined &&
|
|
101
|
-
candidates !== undefined &&
|
|
102
|
-
(focusIndex === -1 || focusIndex === candidates.length - 1),
|
|
97
|
+
enabled: releaseIds === undefined && focusReleaseId !== undefined,
|
|
103
98
|
staleTime: Number.POSITIVE_INFINITY,
|
|
104
99
|
});
|
|
105
100
|
const known = new Map<string, AdoptionRelease>();
|
|
@@ -110,28 +105,24 @@ export function useReleaseHealth({
|
|
|
110
105
|
...(candidates ?? []),
|
|
111
106
|
])
|
|
112
107
|
known.set(release.releaseId, release);
|
|
113
|
-
const
|
|
114
|
-
list
|
|
115
|
-
.slice(0, DEFAULT_ADOPTION_RELEASES)
|
|
116
|
-
.map((release) => release.releaseId);
|
|
108
|
+
const live = (list: readonly AdoptionRelease[]) =>
|
|
109
|
+
defaultAdoptionReleases(list, platform);
|
|
117
110
|
const chosen: readonly string[] | undefined =
|
|
118
111
|
releaseIds !== undefined
|
|
119
112
|
? releaseIds.slice(0, MAX_ADOPTION_RELEASES)
|
|
120
113
|
: candidates === undefined
|
|
121
114
|
? undefined
|
|
122
115
|
: focusReleaseId === undefined
|
|
123
|
-
?
|
|
124
|
-
:
|
|
125
|
-
?
|
|
126
|
-
: focusQuery.data ===
|
|
127
|
-
?
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
focusQuery.data.previous?.releaseId,
|
|
134
|
-
].filter((id) => id !== undefined);
|
|
116
|
+
? live(candidates)
|
|
117
|
+
: focusQuery.data === undefined
|
|
118
|
+
? undefined
|
|
119
|
+
: focusQuery.data.release === null
|
|
120
|
+
? // A focus that is no bundle of this scope: the live ones.
|
|
121
|
+
live(candidates)
|
|
122
|
+
: [
|
|
123
|
+
focusQuery.data.release.releaseId,
|
|
124
|
+
focusQuery.data.previous?.releaseId,
|
|
125
|
+
].filter((id) => id !== undefined);
|
|
135
126
|
// A chosen bundle the newest leave out, as from a copied link.
|
|
136
127
|
const unknown = (chosen ?? []).filter(
|
|
137
128
|
(releaseId) => !known.has(releaseId) && releaseId !== focusReleaseId,
|
|
@@ -159,7 +150,8 @@ export function useReleaseHealth({
|
|
|
159
150
|
const seriesQueries = useQueries({
|
|
160
151
|
queries: reads.map(({ release, type }) => {
|
|
161
152
|
const data = {
|
|
162
|
-
|
|
153
|
+
platform: release.platform,
|
|
154
|
+
channel,
|
|
163
155
|
window,
|
|
164
156
|
endMs,
|
|
165
157
|
bundleId: release.bundleId,
|
|
@@ -6,28 +6,37 @@ import type { InsightsWindow } from "./insights-rpc";
|
|
|
6
6
|
* Each is a read of its counts over the period, and four hues stay apart.
|
|
7
7
|
*/
|
|
8
8
|
export const MAX_ADOPTION_RELEASES = 4;
|
|
9
|
-
/**
|
|
9
|
+
/**
|
|
10
|
+
* Bundles Release health compares when none are chosen: the two newest live
|
|
11
|
+
* ones of one platform, or the newest live one of each.
|
|
12
|
+
*/
|
|
10
13
|
export const DEFAULT_ADOPTION_RELEASES = 2;
|
|
11
|
-
/** Bundle deployments Release health offers to add. */
|
|
14
|
+
/** Bundle deployments of each platform Release health offers to add. */
|
|
12
15
|
export const ADOPTION_CANDIDATES = 10;
|
|
13
16
|
/** A crash rate at which Release health recommends rolling a bundle back. */
|
|
14
17
|
export const CRASH_RATE_THRESHOLD = 0.05;
|
|
15
18
|
/** Attempts below which a crash rate is too noisy to recommend anything. */
|
|
16
19
|
export const CRASH_MIN_ATTEMPTS = 20;
|
|
17
20
|
|
|
21
|
+
/** Release health's platforms: one, or both side by side. */
|
|
22
|
+
export type HealthPlatform = "all" | "ios" | "android";
|
|
23
|
+
|
|
18
24
|
/** A deployed bundle, as Release health labels and rolls it back. */
|
|
19
25
|
export type AdoptionRelease = {
|
|
20
26
|
readonly releaseId: string;
|
|
21
27
|
readonly bundleId: string;
|
|
28
|
+
readonly platform: "ios" | "android";
|
|
22
29
|
readonly deployedAtMs: number;
|
|
23
30
|
readonly targetAppVersion: string | null;
|
|
24
31
|
readonly enabled: boolean;
|
|
32
|
+
/** Whether a device can get it now: its scope's catalog still serves it. */
|
|
33
|
+
readonly live: boolean;
|
|
25
34
|
readonly revision: number;
|
|
26
35
|
};
|
|
27
36
|
|
|
28
|
-
/** A channel
|
|
37
|
+
/** A channel's newest bundle deployments, of one platform or both. */
|
|
29
38
|
export type AdoptionReleasesInput = {
|
|
30
|
-
readonly platform:
|
|
39
|
+
readonly platform: HealthPlatform;
|
|
31
40
|
readonly channel: string;
|
|
32
41
|
};
|
|
33
42
|
|
|
@@ -51,7 +60,10 @@ export type AdoptionReleaseResult = {
|
|
|
51
60
|
export type BundleEventType = "DOWNLOADED" | "LAUNCHED" | "RECOVERED";
|
|
52
61
|
|
|
53
62
|
/** One bundle's reports of one type over the period that ends at `endMs`. */
|
|
54
|
-
export type BundleEventsInput =
|
|
63
|
+
export type BundleEventsInput = {
|
|
64
|
+
/** The bundle's own platform. */
|
|
65
|
+
readonly platform: "ios" | "android";
|
|
66
|
+
readonly channel: string;
|
|
55
67
|
readonly window: InsightsWindow;
|
|
56
68
|
/** The end of an hour; every bundle of one chart reads the same one. */
|
|
57
69
|
readonly endMs: number;
|
|
@@ -90,7 +102,9 @@ export function readAdoptionReleasesInput(
|
|
|
90
102
|
): AdoptionReleasesInput {
|
|
91
103
|
if (
|
|
92
104
|
!input ||
|
|
93
|
-
(input.platform !== "
|
|
105
|
+
(input.platform !== "all" &&
|
|
106
|
+
input.platform !== "ios" &&
|
|
107
|
+
input.platform !== "android") ||
|
|
94
108
|
!isText(input.channel)
|
|
95
109
|
)
|
|
96
110
|
invalid();
|
|
@@ -117,8 +131,9 @@ export function readAdoptionReleaseInput(
|
|
|
117
131
|
export function readBundleEventsInput(
|
|
118
132
|
input: BundleEventsInput,
|
|
119
133
|
): BundleEventsInput {
|
|
120
|
-
const
|
|
134
|
+
const { platform, channel } = readAdoptionReleasesInput(input);
|
|
121
135
|
if (
|
|
136
|
+
platform === "all" ||
|
|
122
137
|
!Object.hasOwn(recoveryWindows, input.window) ||
|
|
123
138
|
typeof input.endMs !== "number" ||
|
|
124
139
|
!Number.isSafeInteger(input.endMs) ||
|
|
@@ -131,7 +146,8 @@ export function readBundleEventsInput(
|
|
|
131
146
|
)
|
|
132
147
|
invalid();
|
|
133
148
|
return {
|
|
134
|
-
|
|
149
|
+
platform: platform as BundleEventsInput["platform"],
|
|
150
|
+
channel,
|
|
135
151
|
window: input.window,
|
|
136
152
|
endMs: input.endMs,
|
|
137
153
|
bundleId: input.bundleId,
|
|
@@ -149,6 +165,27 @@ export const crashRateOf = (launched: number, recovered: number) => {
|
|
|
149
165
|
return { attempts, rate: attempts === 0 ? 0 : recovered / attempts };
|
|
150
166
|
};
|
|
151
167
|
|
|
168
|
+
/**
|
|
169
|
+
* The bundles Release health compares when none are chosen, from the
|
|
170
|
+
* candidates newest first: the two newest live bundles of one platform, or
|
|
171
|
+
* the newest live one of each; without any live one, the newest two.
|
|
172
|
+
*/
|
|
173
|
+
export const defaultAdoptionReleases = (
|
|
174
|
+
candidates: readonly AdoptionRelease[],
|
|
175
|
+
platform: HealthPlatform,
|
|
176
|
+
): readonly string[] => {
|
|
177
|
+
const live = (
|
|
178
|
+
platform === "all" ? (["ios", "android"] as const) : [platform]
|
|
179
|
+
).flatMap((each) =>
|
|
180
|
+
candidates
|
|
181
|
+
.filter((release) => release.platform === each && release.live)
|
|
182
|
+
.slice(0, platform === "all" ? 1 : DEFAULT_ADOPTION_RELEASES),
|
|
183
|
+
);
|
|
184
|
+
return (live.length > 0 ? live : candidates)
|
|
185
|
+
.slice(0, DEFAULT_ADOPTION_RELEASES)
|
|
186
|
+
.map((release) => release.releaseId);
|
|
187
|
+
};
|
|
188
|
+
|
|
152
189
|
/** Whether a bundle's crashes call for rolling it back. */
|
|
153
190
|
export const recommendsRollback = (
|
|
154
191
|
release: Pick<AdoptionRelease, "enabled">,
|
|
@@ -36,7 +36,7 @@ export async function listFailureReports(
|
|
|
36
36
|
...page.data.filter(
|
|
37
37
|
(event) =>
|
|
38
38
|
event.type === "UPDATE_FAILED" &&
|
|
39
|
-
event.platform === input.platform &&
|
|
39
|
+
(input.platform === "all" || event.platform === input.platform) &&
|
|
40
40
|
event.channel === input.channel &&
|
|
41
41
|
(input.releaseId === undefined ||
|
|
42
42
|
(event.failure?.stage !== "check" &&
|
|
@@ -8,6 +8,7 @@ import { insightsPeriodEnd, recoveryWindows } from "../insights-recovery";
|
|
|
8
8
|
import {
|
|
9
9
|
ADOPTION_CANDIDATES,
|
|
10
10
|
type AdoptionRelease,
|
|
11
|
+
type HealthPlatform,
|
|
11
12
|
type AdoptionReleaseInput,
|
|
12
13
|
type AdoptionReleaseResult,
|
|
13
14
|
type AdoptionReleasesInput,
|
|
@@ -17,31 +18,55 @@ import {
|
|
|
17
18
|
readAdoptionReleasesInput,
|
|
18
19
|
readBundleEventsInput,
|
|
19
20
|
} from "../release-adoption";
|
|
21
|
+
import { addReleaseReachability } from "./releaseReachability";
|
|
20
22
|
|
|
21
23
|
type ReleaseReads = Pick<
|
|
22
24
|
HotUpdaterCoreApi,
|
|
23
|
-
"findChannelByName" | "getRelease" | "listReleases"
|
|
25
|
+
"findChannelByName" | "getRelease" | "getReleaseCatalogRow" | "listReleases"
|
|
24
26
|
>;
|
|
25
27
|
|
|
26
28
|
/** Rows read past a focused release to find the bundle before it. */
|
|
27
29
|
const PREVIOUS_ROWS = 4;
|
|
28
30
|
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
bundleId: release.bundle_id!,
|
|
32
|
-
deployedAtMs: release.created_at_ms,
|
|
33
|
-
targetAppVersion: release.target_app_version,
|
|
34
|
-
enabled: release.enabled,
|
|
35
|
-
revision: release.revision,
|
|
36
|
-
});
|
|
31
|
+
const platformsOf = (platform: HealthPlatform) =>
|
|
32
|
+
platform === "all" ? (["ios", "android"] as const) : [platform];
|
|
37
33
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Labels the releases that have a bundle, a release without one having
|
|
36
|
+
* nothing to apply. A bundle is live when its scope's catalog still serves
|
|
37
|
+
* it: one catalog read per scope of the enabled ones.
|
|
38
|
+
*/
|
|
39
|
+
const labelled = async (
|
|
40
|
+
core: Pick<ReleaseReads, "getReleaseCatalogRow">,
|
|
41
|
+
releases: readonly ReleaseRow[],
|
|
42
|
+
): Promise<AdoptionRelease[]> => {
|
|
43
|
+
const bundles = releases.filter((release) => release.bundle_id);
|
|
44
|
+
const live = new Set(
|
|
45
|
+
(
|
|
46
|
+
await addReleaseReachability(
|
|
47
|
+
core,
|
|
48
|
+
bundles.filter((release) => release.enabled),
|
|
49
|
+
)
|
|
50
|
+
)
|
|
51
|
+
.filter((release) => !release.currentlyUnreachable)
|
|
52
|
+
.map((release) => release.id),
|
|
53
|
+
);
|
|
54
|
+
return bundles.map((release) => ({
|
|
55
|
+
releaseId: release.id,
|
|
56
|
+
bundleId: release.bundle_id!,
|
|
57
|
+
platform: release.platform,
|
|
58
|
+
deployedAtMs: release.created_at_ms,
|
|
59
|
+
targetAppVersion: release.target_app_version,
|
|
60
|
+
enabled: release.enabled,
|
|
61
|
+
live: live.has(release.id),
|
|
62
|
+
revision: release.revision,
|
|
63
|
+
}));
|
|
64
|
+
};
|
|
41
65
|
|
|
42
66
|
/**
|
|
43
|
-
*
|
|
44
|
-
*
|
|
67
|
+
* A channel's newest bundle deployments, of one platform or both, newest
|
|
68
|
+
* first: per platform, its newest releases and its newest enabled ones, so
|
|
69
|
+
* a live bundle older than the newest still shows.
|
|
45
70
|
*/
|
|
46
71
|
export async function listAdoptionReleases(
|
|
47
72
|
core: ReleaseReads,
|
|
@@ -50,18 +75,37 @@ export async function listAdoptionReleases(
|
|
|
50
75
|
const { platform, channel } = readAdoptionReleasesInput(input);
|
|
51
76
|
const row = await core.findChannelByName(channel);
|
|
52
77
|
if (row === null) return [];
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
78
|
+
const lists = await Promise.all(
|
|
79
|
+
platformsOf(platform).flatMap((each) =>
|
|
80
|
+
[{}, { enabled: true }].map((enabled) =>
|
|
81
|
+
core.listReleases({
|
|
82
|
+
filter: {
|
|
83
|
+
kind: "channelPlatform",
|
|
84
|
+
channelId: row.id,
|
|
85
|
+
platform: each,
|
|
86
|
+
...enabled,
|
|
87
|
+
},
|
|
88
|
+
order: "desc",
|
|
89
|
+
limit: ADOPTION_CANDIDATES,
|
|
90
|
+
}),
|
|
91
|
+
),
|
|
92
|
+
),
|
|
93
|
+
);
|
|
94
|
+
const releases = new Map(
|
|
95
|
+
lists.flat().map((release) => [release.id, release]),
|
|
96
|
+
);
|
|
97
|
+
return labelled(
|
|
98
|
+
core,
|
|
99
|
+
[...releases.values()].sort(
|
|
100
|
+
(left, right) => right.created_at_ms - left.created_at_ms,
|
|
101
|
+
),
|
|
59
102
|
);
|
|
60
103
|
}
|
|
61
104
|
|
|
62
105
|
/**
|
|
63
|
-
* One bundle deployment of the channel
|
|
64
|
-
*
|
|
106
|
+
* One bundle deployment of the channel, for a focus the newest ones leave
|
|
107
|
+
* out, and with `withPrevious` the bundle deployed before it on its
|
|
108
|
+
* platform.
|
|
65
109
|
*/
|
|
66
110
|
export async function getAdoptionRelease(
|
|
67
111
|
core: ReleaseReads,
|
|
@@ -77,19 +121,24 @@ export async function getAdoptionRelease(
|
|
|
77
121
|
row === null ||
|
|
78
122
|
release === null ||
|
|
79
123
|
!release.bundle_id ||
|
|
80
|
-
release.platform !== platform ||
|
|
124
|
+
(platform !== "all" && release.platform !== platform) ||
|
|
81
125
|
release.channel_id !== row.id
|
|
82
126
|
)
|
|
83
127
|
return withPrevious ? { release: null, previous: null } : { release: null };
|
|
84
|
-
if (!withPrevious) return { release:
|
|
128
|
+
if (!withPrevious) return { release: (await labelled(core, [release]))[0]! };
|
|
85
129
|
// Newest first, `after` a release lists older ones.
|
|
86
130
|
const older = await core.listReleases({
|
|
87
|
-
filter: {
|
|
131
|
+
filter: {
|
|
132
|
+
kind: "channelPlatform",
|
|
133
|
+
channelId: row.id,
|
|
134
|
+
platform: release.platform,
|
|
135
|
+
},
|
|
88
136
|
order: "desc",
|
|
89
137
|
after: release.id,
|
|
90
138
|
limit: PREVIOUS_ROWS,
|
|
91
139
|
});
|
|
92
|
-
|
|
140
|
+
const [label, previous] = await labelled(core, [release, ...older]);
|
|
141
|
+
return { release: label!, previous: previous ?? null };
|
|
93
142
|
}
|
|
94
143
|
|
|
95
144
|
/**
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { HotUpdaterCoreApi } from "@hot-updater/plugin-core";
|
|
2
|
+
import type {
|
|
3
|
+
InsightsReads,
|
|
4
|
+
InsightsUpdateFailures,
|
|
5
|
+
} from "@hot-updater/server/plugins";
|
|
2
6
|
|
|
3
7
|
import {
|
|
4
8
|
checkFailureRate,
|
|
@@ -9,11 +13,111 @@ import {
|
|
|
9
13
|
} from "../insights-failures";
|
|
10
14
|
import { insightsPeriodEnd, recoveryWindows } from "../insights-recovery";
|
|
11
15
|
|
|
12
|
-
|
|
16
|
+
const total = (
|
|
17
|
+
reports: readonly InsightsUpdateFailures[],
|
|
18
|
+
count: (report: InsightsUpdateFailures) => number,
|
|
19
|
+
) => reports.reduce((sum, report) => sum + count(report), 0);
|
|
20
|
+
|
|
21
|
+
/** Counts summed by key, most first, then in key order. */
|
|
22
|
+
const summed = <T extends { readonly events: number }>(
|
|
23
|
+
rows: readonly T[],
|
|
24
|
+
keyOf: (row: T) => string,
|
|
25
|
+
merge: (first: T, rows: readonly T[]) => T,
|
|
26
|
+
): T[] => {
|
|
27
|
+
const groups = new Map<string, T[]>();
|
|
28
|
+
for (const row of rows) {
|
|
29
|
+
const key = keyOf(row);
|
|
30
|
+
groups.set(key, [...(groups.get(key) ?? []), row]);
|
|
31
|
+
}
|
|
32
|
+
return [...groups]
|
|
33
|
+
.map(([key, group]) => ({ key, row: merge(group[0]!, group) }))
|
|
34
|
+
.sort(
|
|
35
|
+
(left, right) =>
|
|
36
|
+
right.row.events - left.row.events ||
|
|
37
|
+
(left.key < right.key ? -1 : left.key > right.key ? 1 : 0),
|
|
38
|
+
)
|
|
39
|
+
.map(({ row }) => row);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Both platforms' failures as one report. An installation runs on one
|
|
44
|
+
* platform, so its counts and installation estimates add up; the period is
|
|
45
|
+
* complete only where both are.
|
|
46
|
+
*/
|
|
47
|
+
export function mergeUpdateFailures(
|
|
48
|
+
reports: readonly InsightsUpdateFailures[],
|
|
49
|
+
): InsightsUpdateFailures {
|
|
50
|
+
if (reports.length === 1) return reports[0]!;
|
|
51
|
+
const sinceMs = reports.map(({ coverage }) => coverage.sinceMs);
|
|
52
|
+
const latestSince = sinceMs.includes(null)
|
|
53
|
+
? null
|
|
54
|
+
: Math.max(...(sinceMs as number[]));
|
|
55
|
+
const checks = reports.flatMap((report) =>
|
|
56
|
+
report.checks ? [report.checks] : [],
|
|
57
|
+
);
|
|
58
|
+
const breakdowns = reports.flatMap((report) => report.breakdown ?? []);
|
|
59
|
+
return {
|
|
60
|
+
coverage:
|
|
61
|
+
latestSince !== null &&
|
|
62
|
+
reports.every(({ coverage }) => coverage.kind === "complete")
|
|
63
|
+
? { kind: "complete", sinceMs: latestSince }
|
|
64
|
+
: { kind: "partial", sinceMs: latestSince },
|
|
65
|
+
measuredAtMs: Math.min(...reports.map((report) => report.measuredAtMs)),
|
|
66
|
+
failedUpdates: total(reports, (report) => report.failedUpdates),
|
|
67
|
+
failedInstallations: total(reports, (report) => report.failedInstallations),
|
|
68
|
+
downloads: total(reports, (report) => report.downloads),
|
|
69
|
+
patchDownloads: total(reports, (report) => report.patchDownloads),
|
|
70
|
+
patchFallbacks: total(reports, (report) => report.patchFallbacks),
|
|
71
|
+
...(checks.length === 0
|
|
72
|
+
? {}
|
|
73
|
+
: {
|
|
74
|
+
checks: {
|
|
75
|
+
failures: checks.reduce((sum, row) => sum + row.failures, 0),
|
|
76
|
+
failedInstallations: checks.reduce(
|
|
77
|
+
(sum, row) => sum + row.failedInstallations,
|
|
78
|
+
0,
|
|
79
|
+
),
|
|
80
|
+
activeInstallations: checks.reduce(
|
|
81
|
+
(sum, row) => sum + row.activeInstallations,
|
|
82
|
+
0,
|
|
83
|
+
),
|
|
84
|
+
},
|
|
85
|
+
}),
|
|
86
|
+
...(reports.some((report) => report.breakdown)
|
|
87
|
+
? {
|
|
88
|
+
breakdown: summed(
|
|
89
|
+
breakdowns,
|
|
90
|
+
({ stage, reason }) => JSON.stringify([stage, reason]),
|
|
91
|
+
(first, group) => ({
|
|
92
|
+
stage: first.stage,
|
|
93
|
+
reason: first.reason,
|
|
94
|
+
events: group.reduce((sum, row) => sum + row.events, 0),
|
|
95
|
+
details: summed(
|
|
96
|
+
group.flatMap((row) => row.details),
|
|
97
|
+
({ resource, httpStatus, originCode, transport }) =>
|
|
98
|
+
JSON.stringify([resource, httpStatus, originCode, transport]),
|
|
99
|
+
(detail, details) => ({
|
|
100
|
+
...detail,
|
|
101
|
+
events: details.reduce((sum, row) => sum + row.events, 0),
|
|
102
|
+
}),
|
|
103
|
+
),
|
|
104
|
+
}),
|
|
105
|
+
),
|
|
106
|
+
}
|
|
107
|
+
: {}),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* A release's or a channel's update failures over a period that ends with
|
|
113
|
+
* the current hour. Both platforms read each and merge, except a release's,
|
|
114
|
+
* which reads only the platform it was deployed to.
|
|
115
|
+
*/
|
|
13
116
|
export async function getUpdateFailuresReport(
|
|
14
117
|
reads: Pick<InsightsReads, "getUpdateFailures">,
|
|
15
118
|
input: UpdateFailuresInput,
|
|
16
119
|
now = Date.now(),
|
|
120
|
+
releases?: Pick<HotUpdaterCoreApi, "getRelease">,
|
|
17
121
|
): Promise<UpdateFailuresReport> {
|
|
18
122
|
readUpdateFailuresInput(input);
|
|
19
123
|
const end = insightsPeriodEnd(now);
|
|
@@ -25,10 +129,32 @@ export async function getUpdateFailuresReport(
|
|
|
25
129
|
end,
|
|
26
130
|
};
|
|
27
131
|
const scope = {
|
|
28
|
-
platform: input.platform,
|
|
29
132
|
channel: input.channel,
|
|
30
133
|
...(input.releaseId === undefined ? {} : { releaseId: input.releaseId }),
|
|
31
134
|
};
|
|
135
|
+
const release =
|
|
136
|
+
input.platform === "all" && input.releaseId !== undefined && releases
|
|
137
|
+
? await releases.getRelease(input.releaseId)
|
|
138
|
+
: null;
|
|
139
|
+
const platforms =
|
|
140
|
+
input.platform !== "all"
|
|
141
|
+
? [input.platform]
|
|
142
|
+
: release
|
|
143
|
+
? [release.platform]
|
|
144
|
+
: (["ios", "android"] as const);
|
|
145
|
+
// Both platforms: one read of each, merged.
|
|
146
|
+
const read = async (range: typeof timeRange) =>
|
|
147
|
+
mergeUpdateFailures(
|
|
148
|
+
await Promise.all(
|
|
149
|
+
platforms.map((platform) =>
|
|
150
|
+
reads.getUpdateFailures({
|
|
151
|
+
...scope,
|
|
152
|
+
platform,
|
|
153
|
+
...(range === undefined ? {} : { timeRange: range }),
|
|
154
|
+
}),
|
|
155
|
+
),
|
|
156
|
+
),
|
|
157
|
+
);
|
|
32
158
|
const previousRange =
|
|
33
159
|
timeRange && timeRange.start >= timeRange.end - timeRange.start
|
|
34
160
|
? {
|
|
@@ -37,13 +163,8 @@ export async function getUpdateFailuresReport(
|
|
|
37
163
|
}
|
|
38
164
|
: undefined;
|
|
39
165
|
const [failures, previous] = await Promise.all([
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
...(timeRange === undefined ? {} : { timeRange }),
|
|
43
|
-
}),
|
|
44
|
-
previousRange === undefined
|
|
45
|
-
? undefined
|
|
46
|
-
: reads.getUpdateFailures({ ...scope, timeRange: previousRange }),
|
|
166
|
+
read(timeRange),
|
|
167
|
+
previousRange === undefined ? undefined : read(previousRange),
|
|
47
168
|
]);
|
|
48
169
|
return {
|
|
49
170
|
...failures,
|