@hot-updater/console 0.36.7 → 1.0.0-rc.0
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/AnalyticsCapabilityContext-BCke57-7.js +1 -0
- package/.output/public/assets/BundleActivityChart-CnsBe_1J.js +42 -0
- package/.output/public/assets/BundleIdDisplay-Dky75zLU.js +1 -0
- package/.output/public/assets/alert-C-1KvDW8.js +1 -0
- package/.output/public/assets/analytics-Bqz0c-BT.js +1 -0
- package/.output/public/assets/analytics-api-CJdrTLLO.js +1 -0
- package/.output/public/assets/api-B_Rh-uzC.js +1 -0
- package/.output/public/assets/api-keys-BurCFhzH.js +1 -0
- package/.output/public/assets/api-keys-api-BEDJdQg3.js +1 -0
- package/.output/public/assets/badge-Bf0fZ-Gx.js +1 -0
- package/.output/public/assets/chart-no-axes-combined-FaYMGmQS.js +1 -0
- package/.output/public/assets/createServerFn-gM2-LFBy.js +3 -0
- package/.output/public/assets/dialog-BS60BRr2.js +1 -0
- package/.output/public/assets/dist-DspNKfMj.js +1 -0
- package/.output/public/assets/download-O-yNBc9H.js +1 -0
- package/.output/public/assets/field-Buq148-A.js +1 -0
- package/.output/public/assets/index-kFhd2S78.js +10 -0
- package/.output/public/assets/installations-BrQ-od41.js +2 -0
- package/.output/public/assets/installations-CGlZ3ROz.js +1 -0
- package/.output/public/assets/lazyRouteComponent-rPzgA7S-.js +1 -0
- package/.output/public/assets/loader-circle-DCYF-wJl.js +1 -0
- package/.output/public/assets/preload-helper-TKOBYGYD.js +1 -0
- package/.output/public/assets/refresh-cw-n8XYbPPj.js +1 -0
- package/.output/public/assets/routes-CrdokJ80.js +1 -0
- package/.output/public/assets/routes-DcQnVajO.js +2 -0
- package/.output/public/assets/search-DLhbWw3W.js +1 -0
- package/.output/public/assets/shield-check-DE_hVuHu.js +1 -0
- package/.output/public/assets/signing-HRexfjsH.js +1 -0
- package/.output/public/assets/styles-eB4EtvFS.css +2 -0
- package/.output/public/assets/table-D8CLieHX.js +1 -0
- package/.output/public/assets/textarea-YJLTVAoq.js +1 -0
- package/.output/public/assets/useMutation-JsGVjQMY.js +1 -0
- package/.output/public/assets/useOpenInteractionType-4cRD5xO0.js +1 -0
- package/.output/server/__23tanstack-start-server-fn-resolver-DBP9TQ7K.mjs +146 -0
- package/.output/server/_libs/@base-ui/react+[...].mjs +16124 -0
- package/.output/server/_libs/@reduxjs/toolkit+[...].mjs +1554 -0
- package/.output/server/_libs/@tanstack/react-router+[...].mjs +41 -10
- package/.output/server/_libs/@tanstack/react-router-ssr-query+[...].mjs +132 -0
- package/.output/server/_libs/d3-array.mjs +231 -0
- package/.output/server/_libs/d3-color.mjs +330 -0
- package/.output/server/_libs/d3-format.mjs +252 -0
- package/.output/server/_libs/d3-interpolate.mjs +207 -0
- package/.output/server/_libs/d3-path.mjs +77 -0
- package/.output/server/_libs/d3-scale+[...].mjs +1907 -0
- package/.output/server/_libs/d3-shape.mjs +818 -0
- package/.output/server/_libs/decimal.js-light.mjs +884 -0
- package/.output/server/_libs/es-toolkit.mjs +1134 -0
- package/.output/server/_libs/eventemitter3.mjs +236 -0
- package/.output/server/_libs/immer.mjs +532 -0
- package/.output/server/_libs/lucide-react.mjs +141 -13
- package/.output/server/_libs/next-themes.mjs +2 -2
- package/.output/server/_libs/react-redux+[...].mjs +205 -0
- package/.output/server/_libs/recharts+[...].mjs +14117 -0
- package/.output/server/_libs/sonner.mjs +2 -2
- package/.output/server/_libs/tanstack__query-core.mjs +114 -1
- package/.output/server/_libs/tanstack__react-query.mjs +3 -4
- package/.output/server/_runtime.mjs +10 -1
- package/.output/server/_ssr/AnalyticsCapabilityContext-ui4DX8GO.mjs +15 -0
- package/.output/server/_ssr/BundleActivityChart-BiB9ZORH.mjs +288 -0
- package/.output/server/_ssr/BundleIdDisplay-Czk6iR6p.mjs +13 -0
- package/.output/server/_ssr/alert-Dim3MGY5.mjs +86 -0
- package/.output/server/_ssr/analytics-DtMTn8fn.mjs +858 -0
- package/.output/server/_ssr/analytics-api-Cg1M5v_H.mjs +71 -0
- package/.output/server/_ssr/analytics-input-fRRRn0uA.mjs +96 -0
- package/.output/server/_ssr/analytics-rpc-DY5uXvHT.mjs +155 -0
- package/.output/server/_ssr/api-BTSv1tZb.mjs +282 -0
- package/.output/server/_ssr/api-keys-BnM_o2nS.mjs +300 -0
- package/.output/server/_ssr/api-keys-api-CIywsSF5.mjs +60 -0
- package/.output/server/_ssr/api-keys-rpc-BSp1wkIV.mjs +136 -0
- package/.output/server/_ssr/api-rpc-cG5wd_tO.mjs +1468 -0
- package/.output/server/_ssr/auth-BF8mbAgF.mjs +21 -0
- package/.output/server/_ssr/auth-rpc-BrJ6obHJ.mjs +24 -0
- package/.output/server/_ssr/auth.server-DwKoZoQM.mjs +50 -0
- package/.output/server/_ssr/badge-DCxbEeof.mjs +26 -0
- package/.output/server/_ssr/bundle-signing-rpc-COoQhW8E.mjs +16 -0
- package/.output/server/_ssr/bundle-signing.server-D5u8KkMp.mjs +51 -0
- package/.output/server/_ssr/config-Dv8qLDZ0.mjs +13 -0
- package/.output/server/_ssr/config.server-DEiLhKmy.mjs +361 -0
- package/.output/server/_ssr/{createServerFn-DzU0k62V.mjs → createServerFn-B2hcXoPM.mjs} +1 -1
- package/.output/server/_ssr/createServerRpc-DB29C_5p.mjs +12 -0
- package/.output/server/_ssr/{sidebar-Cv_H0_Jv.mjs → createSsrRpc-9P0S0lJ3.mjs} +68 -50
- package/.output/server/_ssr/databaseRows-CwbNwYSl.mjs +166 -0
- package/.output/server/_ssr/deleteBundle-Cu_XOMt0.mjs +66 -0
- package/.output/server/_ssr/dialog-CQuRMNZ4.mjs +170 -0
- package/.output/server/_ssr/dist-BVSLTpoP.mjs +250 -0
- package/.output/server/_ssr/downloadBundle-CNnYxYO2.mjs +24 -0
- package/.output/server/_ssr/{empty-plugin-adapters-B4HCzgmM.mjs → empty-plugin-adapters-B5QvGpEl.mjs} +1 -1
- package/.output/server/_ssr/field-DRIXKjZG.mjs +74 -0
- package/.output/server/_ssr/{getBundleChildren-CVtSiH5t.mjs → getBundleChildren-BcRMFoDF.mjs} +10 -18
- package/.output/server/_ssr/installations-B-azgAvX.mjs +19 -0
- package/.output/server/_ssr/installations-Ctvzp2JH.mjs +535 -0
- package/.output/server/_ssr/router-C-KUBfOi.mjs +513 -0
- package/.output/server/_ssr/routes-BM4ht9jb.mjs +2286 -0
- package/.output/server/_ssr/routes-Z7TwC6nC.mjs +37 -0
- package/.output/server/_ssr/runtime.server-awTLiiXW.mjs +527 -0
- package/.output/server/_ssr/server-BjzmvXnO.mjs +7468 -0
- package/.output/server/_ssr/signing-k8GxyBDF.mjs +202 -0
- package/.output/server/_ssr/ssr.mjs +4 -5740
- package/.output/server/_ssr/start-DpViQGnt.mjs +4 -0
- package/.output/server/_ssr/table-v1yCvYo7.mjs +53 -0
- package/.output/server/_ssr/textarea-DYHnskhb.mjs +14 -0
- package/.output/server/_ssr/uuidv7-BbJ-9D_I.mjs +31 -0
- package/.output/server/_tanstack-start-manifest_v-CH4StzJu.mjs +108 -0
- package/.output/server/index.mjs +256 -53
- package/README.md +29 -8
- package/lib/index.d.mts +36 -0
- package/lib/index.mjs +4 -0
- package/lib/vite-internal.mjs +127 -0
- package/lib/vite.d.mts +10 -0
- package/lib/vite.mjs +5 -0
- package/package.json +51 -29
- package/public/apple-touch-icon.png +0 -0
- package/public/favicon-16x16.png +0 -0
- package/public/favicon-32x32.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/logo.svg +1 -0
- package/public/manifest.json +30 -0
- package/public/robots.txt +3 -0
- package/src/components/AppSidebar.tsx +159 -0
- package/src/components/BundleIdDisplay.tsx +18 -0
- package/src/components/ChannelBadge.tsx +31 -0
- package/src/components/ConsoleAccessPage.tsx +122 -0
- package/src/components/EnabledStatusIcon.tsx +54 -0
- package/src/components/HashValueDisplay.tsx +85 -0
- package/src/components/HotUpdaterLogo.tsx +30 -0
- package/src/components/NotFoundPage.tsx +33 -0
- package/src/components/PlatformIcon.tsx +39 -0
- package/src/components/RolloutPercentageBadge.tsx +28 -0
- package/src/components/ThemeProvider.tsx +116 -0
- package/src/components/TimestampDisplay.tsx +41 -0
- package/src/components/features/analytics/ActivityChart.tsx +150 -0
- package/src/components/features/analytics/AnalyticsCapabilityContext.tsx +24 -0
- package/src/components/features/analytics/AnalyticsControls.tsx +115 -0
- package/src/components/features/analytics/AnalyticsErrorAlert.tsx +21 -0
- package/src/components/features/analytics/AnalyticsOverview.tsx +208 -0
- package/src/components/features/analytics/BundleDistribution.tsx +133 -0
- package/src/components/features/analytics/BundleSelector.tsx +114 -0
- package/src/components/features/analytics/InstallationHistoryCard.tsx +248 -0
- package/src/components/features/analytics/InstallationMatchesCard.tsx +132 -0
- package/src/components/features/analytics/InstallationPageHeader.tsx +138 -0
- package/src/components/features/analytics/InstallationPagination.tsx +48 -0
- package/src/components/features/analytics/InstallationSearch.tsx +230 -0
- package/src/components/features/analytics/UpdateOutcomes.tsx +147 -0
- package/src/components/features/api-keys/ApiKeysPage.tsx +377 -0
- package/src/components/features/bundles/BundleActivityChart.tsx +193 -0
- package/src/components/features/bundles/BundleAnalyticsSummary.tsx +123 -0
- package/src/components/features/bundles/BundleChildrenPanel.tsx +201 -0
- package/src/components/features/bundles/BundleMetadata.tsx +136 -0
- package/src/components/features/bundles/RolloutCohortsDialog.tsx +201 -0
- package/src/components/features/channels/ChannelManagementDialog.tsx +228 -0
- package/src/components/features/releases/ReleaseEditorSheet.tsx +879 -0
- package/src/components/features/releases/ReleaseStateBadge.tsx +20 -0
- package/src/components/features/signing/BundleSigningPage.tsx +247 -0
- package/src/components/ui/alert-dialog.tsx +185 -0
- package/src/components/ui/alert.tsx +76 -0
- package/src/components/ui/badge.tsx +49 -0
- package/src/components/ui/button.tsx +58 -0
- package/src/components/ui/card.tsx +89 -0
- package/src/components/ui/chart.tsx +371 -0
- package/src/components/ui/combobox.tsx +303 -0
- package/src/components/ui/dialog.tsx +156 -0
- package/src/components/ui/dropdown-menu.tsx +270 -0
- package/src/components/ui/field.tsx +235 -0
- package/src/components/ui/input-group.tsx +147 -0
- package/src/components/ui/input.tsx +19 -0
- package/src/components/ui/label.tsx +25 -0
- package/src/components/ui/progress.tsx +84 -0
- package/src/components/ui/select.tsx +205 -0
- package/src/components/ui/separator.tsx +23 -0
- package/src/components/ui/sheet.tsx +132 -0
- package/src/components/ui/sidebar.tsx +779 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/slider.tsx +56 -0
- package/src/components/ui/sonner.tsx +45 -0
- package/src/components/ui/switch.tsx +25 -0
- package/src/components/ui/table.tsx +114 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/toggle-group.tsx +89 -0
- package/src/components/ui/toggle.tsx +43 -0
- package/src/components/ui/tooltip.tsx +66 -0
- package/src/hooks/use-mobile.ts +30 -0
- package/src/index.ts +48 -0
- package/src/lib/analytics-api.ts +145 -0
- package/src/lib/analytics-error.ts +19 -0
- package/src/lib/analytics-input.ts +187 -0
- package/src/lib/analytics-overview.ts +170 -0
- package/src/lib/analytics-rpc.ts +218 -0
- package/src/lib/api-keys-api.ts +69 -0
- package/src/lib/api-keys-rpc.ts +87 -0
- package/src/lib/api-rpc.ts +478 -0
- package/src/lib/api.ts +444 -0
- package/src/lib/auth-rpc.ts +16 -0
- package/src/lib/bundle-signing-api.ts +14 -0
- package/src/lib/bundle-signing-rpc.ts +16 -0
- package/src/lib/constants.ts +1 -0
- package/src/lib/git.ts +51 -0
- package/src/lib/server/auth.server.ts +43 -0
- package/src/lib/server/bundle-signing.server.ts +100 -0
- package/src/lib/server/config.server.ts +109 -0
- package/src/lib/server/console-runtime.server.ts +70 -0
- package/src/lib/server/deleteBundle.ts +145 -0
- package/src/lib/server/downloadBundle.ts +46 -0
- package/src/lib/server/getBundleChildren.ts +130 -0
- package/src/lib/server/listReleases.ts +61 -0
- package/src/lib/server/releaseActivity.ts +27 -0
- package/src/lib/server/releaseReachability.ts +95 -0
- package/src/lib/server/runtime.server.ts +172 -0
- package/src/lib/utils.ts +9 -0
- package/src/logo.svg +1 -0
- package/src/routeTree.gen.ts +199 -0
- package/src/router.tsx +21 -0
- package/src/routes/-installations-search.ts +19 -0
- package/src/routes/-releases-search.ts +74 -0
- package/src/routes/__root.tsx +174 -0
- package/src/routes/analytics.tsx +139 -0
- package/src/routes/api/auth/$.ts +19 -0
- package/src/routes/api/bundles/$bundleId/download.ts +17 -0
- package/src/routes/api-keys.tsx +37 -0
- package/src/routes/index.tsx +982 -0
- package/src/routes/installations.tsx +194 -0
- package/src/routes/signing.tsx +37 -0
- package/src/styles.css +152 -0
- package/src/virtual-modules.d.ts +21 -0
- package/src/vite-env.d.ts +1 -0
- package/src/vite-internal.ts +195 -0
- package/src/vite.ts +12 -0
- package/.output/public/assets/dist-DJX53faP.js +0 -3
- package/.output/public/assets/index-Iylis5kY.js +0 -10
- package/.output/public/assets/routes-HT21hs0C.js +0 -54
- package/.output/public/assets/styles-C0Re4quB.css +0 -2
- package/.output/server/__23tanstack-start-server-fn-resolver-CgwgZ0-i.mjs +0 -66
- package/.output/server/_libs/@floating-ui/core+[...].mjs +0 -698
- package/.output/server/_libs/@floating-ui/dom+[...].mjs +0 -644
- package/.output/server/_libs/@floating-ui/react-dom+[...].mjs +0 -839
- package/.output/server/_libs/@radix-ui/react-alert-dialog+[...].mjs +0 -2138
- package/.output/server/_libs/@radix-ui/react-popper+[...].mjs +0 -308
- package/.output/server/_libs/@radix-ui/react-select+[...].mjs +0 -1038
- package/.output/server/_libs/@tanstack/devtools-event-client+[...].mjs +0 -196
- package/.output/server/_libs/@tanstack/form-core+[...].mjs +0 -3228
- package/.output/server/_libs/@tanstack/react-form+[...].mjs +0 -430
- package/.output/server/_libs/@tanstack/react-table+[...].mjs +0 -2382
- package/.output/server/_libs/radix-ui__number.mjs +0 -6
- package/.output/server/_libs/radix-ui__primitive.mjs +0 -9
- package/.output/server/_libs/radix-ui__react-arrow.mjs +0 -23
- package/.output/server/_libs/radix-ui__react-collection.mjs +0 -78
- package/.output/server/_libs/radix-ui__react-direction.mjs +0 -13
- package/.output/server/_libs/radix-ui__react-label.mjs +0 -22
- package/.output/server/_libs/radix-ui__react-separator.mjs +0 -30
- package/.output/server/_libs/radix-ui__react-slider.mjs +0 -510
- package/.output/server/_libs/radix-ui__react-switch.mjs +0 -157
- package/.output/server/_libs/radix-ui__react-tooltip.mjs +0 -493
- package/.output/server/_ssr/api-rpc-BIbDUKSz.mjs +0 -292
- package/.output/server/_ssr/config.server-BSS366KT.mjs +0 -41
- package/.output/server/_ssr/deleteBundle-BgbZTtpW.mjs +0 -141
- package/.output/server/_ssr/dist-KboydRVk.mjs +0 -120
- package/.output/server/_ssr/router-C3hqEy3k.mjs +0 -307
- package/.output/server/_ssr/routes-B0lQvvlC.mjs +0 -3048
- package/.output/server/_ssr/start-02sLteYG.mjs +0 -4
- package/.output/server/_ssr/storageProfile-FVkjPoGb.mjs +0 -9
- package/.output/server/_tanstack-start-manifest_v-Bpx52WOU.mjs +0 -20
- package/.output/server/node_modules/tslib/modules/index.js +0 -70
- package/.output/server/node_modules/tslib/modules/package.json +0 -3
- package/.output/server/node_modules/tslib/package.json +0 -47
- package/.output/server/node_modules/tslib/tslib.js +0 -484
- package/.output/server/package.json +0 -9
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { type QueryClient, useQuery } from "@tanstack/react-query";
|
|
2
|
+
import { redirect } from "@tanstack/react-router";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
type ActiveInstallationInput,
|
|
6
|
+
parseActiveInstallationInput,
|
|
7
|
+
} from "./analytics-input";
|
|
8
|
+
import {
|
|
9
|
+
getActiveInstallationOverviewRpc,
|
|
10
|
+
getAnalyticsCapabilitiesRpc,
|
|
11
|
+
getAnalyticsOverviewRpc,
|
|
12
|
+
type AnalyticsCapabilities,
|
|
13
|
+
} from "./analytics-rpc";
|
|
14
|
+
|
|
15
|
+
export type AnalyticsCapabilityState =
|
|
16
|
+
| { readonly status: "unresolved" }
|
|
17
|
+
| { readonly status: "unsupported" }
|
|
18
|
+
| {
|
|
19
|
+
readonly status: "supported";
|
|
20
|
+
readonly mode: "bounded";
|
|
21
|
+
readonly maxMatchingRows: number;
|
|
22
|
+
}
|
|
23
|
+
| { readonly status: "error"; readonly error: Error };
|
|
24
|
+
|
|
25
|
+
type AnalyticsCapabilityQueryResult =
|
|
26
|
+
| { readonly status: "pending" }
|
|
27
|
+
| { readonly status: "success"; readonly data: AnalyticsCapabilities }
|
|
28
|
+
| { readonly status: "error"; readonly error: Error };
|
|
29
|
+
|
|
30
|
+
export type ProtectedAnalyticsRouteDecision =
|
|
31
|
+
| "loading"
|
|
32
|
+
| "redirect"
|
|
33
|
+
| "allow"
|
|
34
|
+
| "error";
|
|
35
|
+
|
|
36
|
+
export const analyticsQueryKeys = {
|
|
37
|
+
capabilities: ["analytics", "capabilities"] as const,
|
|
38
|
+
overview: ["analytics", "overview"] as const,
|
|
39
|
+
activeInstallations: (input: ActiveInstallationInput) =>
|
|
40
|
+
[
|
|
41
|
+
"analytics",
|
|
42
|
+
"active-installations",
|
|
43
|
+
input.window,
|
|
44
|
+
input.userId ?? null,
|
|
45
|
+
] as const,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const ANALYTICS_STALE_TIME_MS = 30_000;
|
|
49
|
+
|
|
50
|
+
export const getAnalyticsCapabilityState = (
|
|
51
|
+
query: AnalyticsCapabilityQueryResult,
|
|
52
|
+
): AnalyticsCapabilityState => {
|
|
53
|
+
switch (query.status) {
|
|
54
|
+
case "pending":
|
|
55
|
+
return { status: "unresolved" };
|
|
56
|
+
case "error":
|
|
57
|
+
return { status: "error", error: query.error };
|
|
58
|
+
case "success":
|
|
59
|
+
if (!query.data.capabilities.analytics) {
|
|
60
|
+
return { status: "unsupported" };
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
status: "supported",
|
|
64
|
+
mode: "bounded",
|
|
65
|
+
maxMatchingRows: query.data.capabilities.maxMatchingRows,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export const getProtectedAnalyticsRouteDecision = (
|
|
71
|
+
capability: AnalyticsCapabilityState,
|
|
72
|
+
): ProtectedAnalyticsRouteDecision => {
|
|
73
|
+
switch (capability.status) {
|
|
74
|
+
case "unresolved":
|
|
75
|
+
return "loading";
|
|
76
|
+
case "unsupported":
|
|
77
|
+
return "redirect";
|
|
78
|
+
case "supported":
|
|
79
|
+
return "allow";
|
|
80
|
+
case "error":
|
|
81
|
+
return "error";
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export const isAnalyticsQueryEnabled = (
|
|
86
|
+
capability: AnalyticsCapabilityState,
|
|
87
|
+
): boolean => capability.status === "supported";
|
|
88
|
+
|
|
89
|
+
export const getAnalyticsCapabilitiesQueryOptions = () => ({
|
|
90
|
+
queryKey: analyticsQueryKeys.capabilities,
|
|
91
|
+
queryFn: () => getAnalyticsCapabilitiesRpc(),
|
|
92
|
+
staleTime: Infinity,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
export const useAnalyticsCapabilitiesQuery = () =>
|
|
96
|
+
useQuery(getAnalyticsCapabilitiesQueryOptions());
|
|
97
|
+
|
|
98
|
+
export const ensureAnalyticsRouteAccess = async (
|
|
99
|
+
queryClient: QueryClient,
|
|
100
|
+
): Promise<void> => {
|
|
101
|
+
const result = await queryClient.ensureQueryData(
|
|
102
|
+
getAnalyticsCapabilitiesQueryOptions(),
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
if (!result.capabilities.analytics) {
|
|
106
|
+
throw redirect({
|
|
107
|
+
to: "/",
|
|
108
|
+
search: {},
|
|
109
|
+
replace: true,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export const getAnalyticsOverviewQueryOptions = (
|
|
115
|
+
capability: AnalyticsCapabilityState,
|
|
116
|
+
) => ({
|
|
117
|
+
queryKey: analyticsQueryKeys.overview,
|
|
118
|
+
queryFn: () => getAnalyticsOverviewRpc(),
|
|
119
|
+
staleTime: ANALYTICS_STALE_TIME_MS,
|
|
120
|
+
refetchOnWindowFocus: true,
|
|
121
|
+
enabled: isAnalyticsQueryEnabled(capability),
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
export const useAnalyticsOverviewQuery = (
|
|
125
|
+
capability: AnalyticsCapabilityState,
|
|
126
|
+
) => useQuery(getAnalyticsOverviewQueryOptions(capability));
|
|
127
|
+
|
|
128
|
+
export const getActiveInstallationQueryOptions = (
|
|
129
|
+
capability: AnalyticsCapabilityState,
|
|
130
|
+
input: ActiveInstallationInput,
|
|
131
|
+
) => {
|
|
132
|
+
const normalized = parseActiveInstallationInput(input);
|
|
133
|
+
return {
|
|
134
|
+
queryKey: analyticsQueryKeys.activeInstallations(normalized),
|
|
135
|
+
queryFn: () => getActiveInstallationOverviewRpc({ data: normalized }),
|
|
136
|
+
staleTime: ANALYTICS_STALE_TIME_MS,
|
|
137
|
+
refetchOnWindowFocus: true,
|
|
138
|
+
enabled: isAnalyticsQueryEnabled(capability),
|
|
139
|
+
};
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export const useActiveInstallationQuery = (
|
|
143
|
+
capability: AnalyticsCapabilityState,
|
|
144
|
+
input: ActiveInstallationInput,
|
|
145
|
+
) => useQuery(getActiveInstallationQueryOptions(capability, input));
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type AnalyticsErrorCopy = {
|
|
2
|
+
readonly title: string;
|
|
3
|
+
readonly description: string;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export const getAnalyticsErrorCopy = (
|
|
7
|
+
error: Error,
|
|
8
|
+
fallbackTitle: string,
|
|
9
|
+
): AnalyticsErrorCopy => {
|
|
10
|
+
const match = /^Bundle event scan exceeded (\d+) rows\.$/.exec(error.message);
|
|
11
|
+
if (!match) {
|
|
12
|
+
return { title: fallbackTitle, description: error.message };
|
|
13
|
+
}
|
|
14
|
+
const limit = Number(match[1]);
|
|
15
|
+
return {
|
|
16
|
+
title: "Analytics report limit reached",
|
|
17
|
+
description: `This query matched more than ${limit.toLocaleString()} reports. Narrow the query and try again.`,
|
|
18
|
+
};
|
|
19
|
+
};
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import type { ActiveInstallationWindow } from "@hot-updater/server";
|
|
2
|
+
import type { BundleEventAnalyticsWindow } from "@hot-updater/server";
|
|
3
|
+
|
|
4
|
+
const MAX_ANALYTICS_STRING_LENGTH = 1024;
|
|
5
|
+
const MAX_ANALYTICS_LIMIT = 100;
|
|
6
|
+
|
|
7
|
+
export type BundleEventSummaryInput = {
|
|
8
|
+
readonly bundleId: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type BundleEventAnalyticsInput = BundleEventSummaryInput & {
|
|
12
|
+
readonly window: BundleEventAnalyticsWindow;
|
|
13
|
+
readonly limit?: number;
|
|
14
|
+
readonly offset?: number;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type SearchInstallationsInput = {
|
|
18
|
+
readonly query: string;
|
|
19
|
+
readonly limit?: number;
|
|
20
|
+
readonly offset?: number;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type InstallationHistoryInput = {
|
|
24
|
+
readonly installId: string;
|
|
25
|
+
readonly limit?: number;
|
|
26
|
+
readonly offset?: number;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type ActiveInstallationInput = {
|
|
30
|
+
readonly window: ActiveInstallationWindow;
|
|
31
|
+
readonly userId?: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
type AnalyticsPagination = {
|
|
35
|
+
readonly limit?: number;
|
|
36
|
+
readonly offset?: number;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export class AnalyticsInputValidationError extends Error {
|
|
40
|
+
readonly name = "AnalyticsInputValidationError";
|
|
41
|
+
|
|
42
|
+
constructor(readonly field: string) {
|
|
43
|
+
super(`Invalid analytics input: ${field}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const isRecord = (input: unknown): input is Record<string, unknown> =>
|
|
48
|
+
typeof input === "object" && input !== null && !Array.isArray(input);
|
|
49
|
+
|
|
50
|
+
const parseRecord = (input: unknown): Record<string, unknown> => {
|
|
51
|
+
if (!isRecord(input)) {
|
|
52
|
+
throw new AnalyticsInputValidationError("input");
|
|
53
|
+
}
|
|
54
|
+
return input;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const parseString = (input: Record<string, unknown>, field: string): string => {
|
|
58
|
+
const value = input[field];
|
|
59
|
+
if (typeof value !== "string") {
|
|
60
|
+
throw new AnalyticsInputValidationError(field);
|
|
61
|
+
}
|
|
62
|
+
const trimmed = value.trim();
|
|
63
|
+
if (trimmed.length === 0 || trimmed.length > MAX_ANALYTICS_STRING_LENGTH) {
|
|
64
|
+
throw new AnalyticsInputValidationError(field);
|
|
65
|
+
}
|
|
66
|
+
return trimmed;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const parseLimit = (input: Record<string, unknown>): number | undefined => {
|
|
70
|
+
const value = input.limit;
|
|
71
|
+
if (value === undefined) {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
if (
|
|
75
|
+
typeof value !== "number" ||
|
|
76
|
+
!Number.isInteger(value) ||
|
|
77
|
+
value < 1 ||
|
|
78
|
+
value > MAX_ANALYTICS_LIMIT
|
|
79
|
+
) {
|
|
80
|
+
throw new AnalyticsInputValidationError("limit");
|
|
81
|
+
}
|
|
82
|
+
return value;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const parseOffset = (input: Record<string, unknown>): number | undefined => {
|
|
86
|
+
const value = input.offset;
|
|
87
|
+
if (value === undefined) {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
|
|
91
|
+
throw new AnalyticsInputValidationError("offset");
|
|
92
|
+
}
|
|
93
|
+
return value;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const parsePagination = (
|
|
97
|
+
input: Record<string, unknown>,
|
|
98
|
+
): AnalyticsPagination => {
|
|
99
|
+
const limit = parseLimit(input);
|
|
100
|
+
const offset = parseOffset(input);
|
|
101
|
+
return {
|
|
102
|
+
...(limit === undefined ? {} : { limit }),
|
|
103
|
+
...(offset === undefined ? {} : { offset }),
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const parseWindow = (
|
|
108
|
+
input: Record<string, unknown>,
|
|
109
|
+
): BundleEventAnalyticsWindow => {
|
|
110
|
+
const value = input.window;
|
|
111
|
+
if (value === "24h" || value === "7d" || value === "30d" || value === "all") {
|
|
112
|
+
return value;
|
|
113
|
+
}
|
|
114
|
+
throw new AnalyticsInputValidationError("window");
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const parseActiveWindow = (
|
|
118
|
+
input: Record<string, unknown>,
|
|
119
|
+
): ActiveInstallationWindow => {
|
|
120
|
+
const value = input.window;
|
|
121
|
+
if (value === "24h" || value === "7d" || value === "30d") {
|
|
122
|
+
return value;
|
|
123
|
+
}
|
|
124
|
+
throw new AnalyticsInputValidationError("window");
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const parseOptionalUserId = (
|
|
128
|
+
input: Record<string, unknown>,
|
|
129
|
+
): string | undefined => {
|
|
130
|
+
if (input.userId === undefined) return undefined;
|
|
131
|
+
if (typeof input.userId !== "string") {
|
|
132
|
+
throw new AnalyticsInputValidationError("userId");
|
|
133
|
+
}
|
|
134
|
+
const userId = input.userId.trim();
|
|
135
|
+
if (userId.length === 0) return undefined;
|
|
136
|
+
if (userId.length > MAX_ANALYTICS_STRING_LENGTH) {
|
|
137
|
+
throw new AnalyticsInputValidationError("userId");
|
|
138
|
+
}
|
|
139
|
+
return userId;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export const parseActiveInstallationInput = (
|
|
143
|
+
input: unknown,
|
|
144
|
+
): ActiveInstallationInput => {
|
|
145
|
+
const record = parseRecord(input);
|
|
146
|
+
const window = parseActiveWindow(record);
|
|
147
|
+
const userId = parseOptionalUserId(record);
|
|
148
|
+
return userId === undefined ? { window } : { window, userId };
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export const parseBundleEventSummaryInput = (
|
|
152
|
+
input: unknown,
|
|
153
|
+
): BundleEventSummaryInput => {
|
|
154
|
+
const record = parseRecord(input);
|
|
155
|
+
return { bundleId: parseString(record, "bundleId") };
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
export const parseBundleEventAnalyticsInput = (
|
|
159
|
+
input: unknown,
|
|
160
|
+
): BundleEventAnalyticsInput => {
|
|
161
|
+
const record = parseRecord(input);
|
|
162
|
+
return {
|
|
163
|
+
bundleId: parseString(record, "bundleId"),
|
|
164
|
+
window: parseWindow(record),
|
|
165
|
+
...parsePagination(record),
|
|
166
|
+
};
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
export const parseSearchInstallationsInput = (
|
|
170
|
+
input: unknown,
|
|
171
|
+
): SearchInstallationsInput => {
|
|
172
|
+
const record = parseRecord(input);
|
|
173
|
+
return {
|
|
174
|
+
query: parseString(record, "query"),
|
|
175
|
+
...parsePagination(record),
|
|
176
|
+
};
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
export const parseInstallationHistoryInput = (
|
|
180
|
+
input: unknown,
|
|
181
|
+
): InstallationHistoryInput => {
|
|
182
|
+
const record = parseRecord(input);
|
|
183
|
+
return {
|
|
184
|
+
installId: parseString(record, "installId"),
|
|
185
|
+
...parsePagination(record),
|
|
186
|
+
};
|
|
187
|
+
};
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { normalizeRolloutCohortCount } from "@hot-updater/core";
|
|
2
|
+
import type { Bundle, ChannelRow, ReleaseRow } from "@hot-updater/plugin-core";
|
|
3
|
+
|
|
4
|
+
export type AnalyticsBundleMetadata = {
|
|
5
|
+
readonly platform: "ios" | "android";
|
|
6
|
+
readonly channel: string | null;
|
|
7
|
+
readonly targetAppVersion: string | null;
|
|
8
|
+
readonly fingerprintHash: string | null;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type LatestReportedBundle = {
|
|
12
|
+
readonly bundleId: string;
|
|
13
|
+
readonly trackedInstallations: number;
|
|
14
|
+
readonly observedShare: number;
|
|
15
|
+
readonly bundle: AnalyticsBundleMetadata | null;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type ConfiguredRollout = {
|
|
19
|
+
readonly releaseId: string;
|
|
20
|
+
readonly bundleId: string;
|
|
21
|
+
readonly configuredPercentage: number;
|
|
22
|
+
readonly trackedInstallations: number;
|
|
23
|
+
readonly bundle: AnalyticsBundleMetadata;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type AnalyticsOverview = {
|
|
27
|
+
readonly trackedInstallations: number;
|
|
28
|
+
readonly mostCommonLatestReportedBundle: LatestReportedBundle | null;
|
|
29
|
+
readonly latestReportedBundles: readonly LatestReportedBundle[];
|
|
30
|
+
readonly configuredRollouts: readonly ConfiguredRollout[];
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type LatestInstallationBundle = {
|
|
34
|
+
readonly lastKnownBundleId: string | null;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type InstallationBundleCount = {
|
|
38
|
+
readonly bundleId: string;
|
|
39
|
+
readonly installations: number;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const toBundleMetadata = (
|
|
43
|
+
bundle: Bundle,
|
|
44
|
+
release: ReleaseRow | undefined,
|
|
45
|
+
channelById: ReadonlyMap<string, string>,
|
|
46
|
+
): AnalyticsBundleMetadata => ({
|
|
47
|
+
platform: bundle.platform,
|
|
48
|
+
channel: release ? (channelById.get(release.channel_id) ?? null) : null,
|
|
49
|
+
targetAppVersion: release?.target_app_version ?? null,
|
|
50
|
+
fingerprintHash: release?.fingerprint_hash ?? null,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const compareCodePoints = (left: string, right: string): number =>
|
|
54
|
+
left < right ? -1 : left > right ? 1 : 0;
|
|
55
|
+
|
|
56
|
+
const createOverview = (
|
|
57
|
+
bundles: readonly Bundle[],
|
|
58
|
+
releases: readonly ReleaseRow[],
|
|
59
|
+
channels: readonly ChannelRow[],
|
|
60
|
+
trackedInstallations: number,
|
|
61
|
+
latestReportedCounts: ReadonlyMap<string, number>,
|
|
62
|
+
): AnalyticsOverview => {
|
|
63
|
+
const bundleById = new Map(bundles.map((bundle) => [bundle.id, bundle]));
|
|
64
|
+
const channelById = new Map(
|
|
65
|
+
channels.map((channel) => [channel.id, channel.name]),
|
|
66
|
+
);
|
|
67
|
+
const bundleReleases = releases
|
|
68
|
+
.filter(
|
|
69
|
+
(release): release is ReleaseRow & { readonly bundle_id: string } =>
|
|
70
|
+
release.kind === "BUNDLE" && release.bundle_id !== null,
|
|
71
|
+
)
|
|
72
|
+
.sort((left, right) => compareCodePoints(right.id, left.id));
|
|
73
|
+
const latestReleaseByBundleId = new Map<string, ReleaseRow>();
|
|
74
|
+
for (const release of bundleReleases) {
|
|
75
|
+
if (!latestReleaseByBundleId.has(release.bundle_id)) {
|
|
76
|
+
latestReleaseByBundleId.set(release.bundle_id, release);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const latestReportedBundles = [...latestReportedCounts]
|
|
80
|
+
.map(([bundleId, count]): LatestReportedBundle => {
|
|
81
|
+
const bundle = bundleById.get(bundleId);
|
|
82
|
+
return {
|
|
83
|
+
bundleId,
|
|
84
|
+
trackedInstallations: count,
|
|
85
|
+
observedShare:
|
|
86
|
+
trackedInstallations === 0 ? 0 : count / trackedInstallations,
|
|
87
|
+
bundle: bundle
|
|
88
|
+
? toBundleMetadata(
|
|
89
|
+
bundle,
|
|
90
|
+
latestReleaseByBundleId.get(bundle.id),
|
|
91
|
+
channelById,
|
|
92
|
+
)
|
|
93
|
+
: null,
|
|
94
|
+
};
|
|
95
|
+
})
|
|
96
|
+
.sort(
|
|
97
|
+
(left, right) =>
|
|
98
|
+
right.trackedInstallations - left.trackedInstallations ||
|
|
99
|
+
compareCodePoints(left.bundleId, right.bundleId),
|
|
100
|
+
);
|
|
101
|
+
const configuredRollouts = bundleReleases
|
|
102
|
+
.filter((release) => bundleById.has(release.bundle_id))
|
|
103
|
+
.map(
|
|
104
|
+
(release): ConfiguredRollout => ({
|
|
105
|
+
releaseId: release.id,
|
|
106
|
+
bundleId: release.bundle_id,
|
|
107
|
+
configuredPercentage:
|
|
108
|
+
normalizeRolloutCohortCount(release.rollout_cohort_count) / 10,
|
|
109
|
+
trackedInstallations: latestReportedCounts.get(release.bundle_id) ?? 0,
|
|
110
|
+
bundle: toBundleMetadata(
|
|
111
|
+
bundleById.get(release.bundle_id)!,
|
|
112
|
+
release,
|
|
113
|
+
channelById,
|
|
114
|
+
),
|
|
115
|
+
}),
|
|
116
|
+
)
|
|
117
|
+
.sort((left, right) => compareCodePoints(left.releaseId, right.releaseId));
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
trackedInstallations,
|
|
121
|
+
mostCommonLatestReportedBundle: latestReportedBundles[0] ?? null,
|
|
122
|
+
latestReportedBundles,
|
|
123
|
+
configuredRollouts,
|
|
124
|
+
};
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const countLatestInstallationBundles = (
|
|
128
|
+
rows: readonly LatestInstallationBundle[],
|
|
129
|
+
): ReadonlyMap<string, number> => {
|
|
130
|
+
const counts = new Map<string, number>();
|
|
131
|
+
for (const row of rows) {
|
|
132
|
+
if (row.lastKnownBundleId === null) continue;
|
|
133
|
+
counts.set(
|
|
134
|
+
row.lastKnownBundleId,
|
|
135
|
+
(counts.get(row.lastKnownBundleId) ?? 0) + 1,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
return counts;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
export const createAnalyticsOverviewFromCounts = (
|
|
142
|
+
bundles: readonly Bundle[],
|
|
143
|
+
releases: readonly ReleaseRow[],
|
|
144
|
+
channels: readonly ChannelRow[],
|
|
145
|
+
trackedInstallations: number,
|
|
146
|
+
counts: readonly InstallationBundleCount[],
|
|
147
|
+
): AnalyticsOverview =>
|
|
148
|
+
createOverview(
|
|
149
|
+
bundles,
|
|
150
|
+
releases,
|
|
151
|
+
channels,
|
|
152
|
+
trackedInstallations,
|
|
153
|
+
new Map(
|
|
154
|
+
counts.map(({ bundleId, installations }) => [bundleId, installations]),
|
|
155
|
+
),
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
export const createAnalyticsOverview = (
|
|
159
|
+
bundles: readonly Bundle[],
|
|
160
|
+
releases: readonly ReleaseRow[],
|
|
161
|
+
channels: readonly ChannelRow[],
|
|
162
|
+
latestInstallations: readonly LatestInstallationBundle[],
|
|
163
|
+
): AnalyticsOverview =>
|
|
164
|
+
createOverview(
|
|
165
|
+
bundles,
|
|
166
|
+
releases,
|
|
167
|
+
channels,
|
|
168
|
+
latestInstallations.length,
|
|
169
|
+
countLatestInstallationBundles(latestInstallations),
|
|
170
|
+
);
|