@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.
Files changed (266) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/AnalyticsCapabilityContext-BCke57-7.js +1 -0
  3. package/.output/public/assets/BundleActivityChart-CnsBe_1J.js +42 -0
  4. package/.output/public/assets/BundleIdDisplay-Dky75zLU.js +1 -0
  5. package/.output/public/assets/alert-C-1KvDW8.js +1 -0
  6. package/.output/public/assets/analytics-Bqz0c-BT.js +1 -0
  7. package/.output/public/assets/analytics-api-CJdrTLLO.js +1 -0
  8. package/.output/public/assets/api-B_Rh-uzC.js +1 -0
  9. package/.output/public/assets/api-keys-BurCFhzH.js +1 -0
  10. package/.output/public/assets/api-keys-api-BEDJdQg3.js +1 -0
  11. package/.output/public/assets/badge-Bf0fZ-Gx.js +1 -0
  12. package/.output/public/assets/chart-no-axes-combined-FaYMGmQS.js +1 -0
  13. package/.output/public/assets/createServerFn-gM2-LFBy.js +3 -0
  14. package/.output/public/assets/dialog-BS60BRr2.js +1 -0
  15. package/.output/public/assets/dist-DspNKfMj.js +1 -0
  16. package/.output/public/assets/download-O-yNBc9H.js +1 -0
  17. package/.output/public/assets/field-Buq148-A.js +1 -0
  18. package/.output/public/assets/index-kFhd2S78.js +10 -0
  19. package/.output/public/assets/installations-BrQ-od41.js +2 -0
  20. package/.output/public/assets/installations-CGlZ3ROz.js +1 -0
  21. package/.output/public/assets/lazyRouteComponent-rPzgA7S-.js +1 -0
  22. package/.output/public/assets/loader-circle-DCYF-wJl.js +1 -0
  23. package/.output/public/assets/preload-helper-TKOBYGYD.js +1 -0
  24. package/.output/public/assets/refresh-cw-n8XYbPPj.js +1 -0
  25. package/.output/public/assets/routes-CrdokJ80.js +1 -0
  26. package/.output/public/assets/routes-DcQnVajO.js +2 -0
  27. package/.output/public/assets/search-DLhbWw3W.js +1 -0
  28. package/.output/public/assets/shield-check-DE_hVuHu.js +1 -0
  29. package/.output/public/assets/signing-HRexfjsH.js +1 -0
  30. package/.output/public/assets/styles-eB4EtvFS.css +2 -0
  31. package/.output/public/assets/table-D8CLieHX.js +1 -0
  32. package/.output/public/assets/textarea-YJLTVAoq.js +1 -0
  33. package/.output/public/assets/useMutation-JsGVjQMY.js +1 -0
  34. package/.output/public/assets/useOpenInteractionType-4cRD5xO0.js +1 -0
  35. package/.output/server/__23tanstack-start-server-fn-resolver-DBP9TQ7K.mjs +146 -0
  36. package/.output/server/_libs/@base-ui/react+[...].mjs +16124 -0
  37. package/.output/server/_libs/@reduxjs/toolkit+[...].mjs +1554 -0
  38. package/.output/server/_libs/@tanstack/react-router+[...].mjs +41 -10
  39. package/.output/server/_libs/@tanstack/react-router-ssr-query+[...].mjs +132 -0
  40. package/.output/server/_libs/d3-array.mjs +231 -0
  41. package/.output/server/_libs/d3-color.mjs +330 -0
  42. package/.output/server/_libs/d3-format.mjs +252 -0
  43. package/.output/server/_libs/d3-interpolate.mjs +207 -0
  44. package/.output/server/_libs/d3-path.mjs +77 -0
  45. package/.output/server/_libs/d3-scale+[...].mjs +1907 -0
  46. package/.output/server/_libs/d3-shape.mjs +818 -0
  47. package/.output/server/_libs/decimal.js-light.mjs +884 -0
  48. package/.output/server/_libs/es-toolkit.mjs +1134 -0
  49. package/.output/server/_libs/eventemitter3.mjs +236 -0
  50. package/.output/server/_libs/immer.mjs +532 -0
  51. package/.output/server/_libs/lucide-react.mjs +141 -13
  52. package/.output/server/_libs/next-themes.mjs +2 -2
  53. package/.output/server/_libs/react-redux+[...].mjs +205 -0
  54. package/.output/server/_libs/recharts+[...].mjs +14117 -0
  55. package/.output/server/_libs/sonner.mjs +2 -2
  56. package/.output/server/_libs/tanstack__query-core.mjs +114 -1
  57. package/.output/server/_libs/tanstack__react-query.mjs +3 -4
  58. package/.output/server/_runtime.mjs +10 -1
  59. package/.output/server/_ssr/AnalyticsCapabilityContext-ui4DX8GO.mjs +15 -0
  60. package/.output/server/_ssr/BundleActivityChart-BiB9ZORH.mjs +288 -0
  61. package/.output/server/_ssr/BundleIdDisplay-Czk6iR6p.mjs +13 -0
  62. package/.output/server/_ssr/alert-Dim3MGY5.mjs +86 -0
  63. package/.output/server/_ssr/analytics-DtMTn8fn.mjs +858 -0
  64. package/.output/server/_ssr/analytics-api-Cg1M5v_H.mjs +71 -0
  65. package/.output/server/_ssr/analytics-input-fRRRn0uA.mjs +96 -0
  66. package/.output/server/_ssr/analytics-rpc-DY5uXvHT.mjs +155 -0
  67. package/.output/server/_ssr/api-BTSv1tZb.mjs +282 -0
  68. package/.output/server/_ssr/api-keys-BnM_o2nS.mjs +300 -0
  69. package/.output/server/_ssr/api-keys-api-CIywsSF5.mjs +60 -0
  70. package/.output/server/_ssr/api-keys-rpc-BSp1wkIV.mjs +136 -0
  71. package/.output/server/_ssr/api-rpc-cG5wd_tO.mjs +1468 -0
  72. package/.output/server/_ssr/auth-BF8mbAgF.mjs +21 -0
  73. package/.output/server/_ssr/auth-rpc-BrJ6obHJ.mjs +24 -0
  74. package/.output/server/_ssr/auth.server-DwKoZoQM.mjs +50 -0
  75. package/.output/server/_ssr/badge-DCxbEeof.mjs +26 -0
  76. package/.output/server/_ssr/bundle-signing-rpc-COoQhW8E.mjs +16 -0
  77. package/.output/server/_ssr/bundle-signing.server-D5u8KkMp.mjs +51 -0
  78. package/.output/server/_ssr/config-Dv8qLDZ0.mjs +13 -0
  79. package/.output/server/_ssr/config.server-DEiLhKmy.mjs +361 -0
  80. package/.output/server/_ssr/{createServerFn-DzU0k62V.mjs → createServerFn-B2hcXoPM.mjs} +1 -1
  81. package/.output/server/_ssr/createServerRpc-DB29C_5p.mjs +12 -0
  82. package/.output/server/_ssr/{sidebar-Cv_H0_Jv.mjs → createSsrRpc-9P0S0lJ3.mjs} +68 -50
  83. package/.output/server/_ssr/databaseRows-CwbNwYSl.mjs +166 -0
  84. package/.output/server/_ssr/deleteBundle-Cu_XOMt0.mjs +66 -0
  85. package/.output/server/_ssr/dialog-CQuRMNZ4.mjs +170 -0
  86. package/.output/server/_ssr/dist-BVSLTpoP.mjs +250 -0
  87. package/.output/server/_ssr/downloadBundle-CNnYxYO2.mjs +24 -0
  88. package/.output/server/_ssr/{empty-plugin-adapters-B4HCzgmM.mjs → empty-plugin-adapters-B5QvGpEl.mjs} +1 -1
  89. package/.output/server/_ssr/field-DRIXKjZG.mjs +74 -0
  90. package/.output/server/_ssr/{getBundleChildren-CVtSiH5t.mjs → getBundleChildren-BcRMFoDF.mjs} +10 -18
  91. package/.output/server/_ssr/installations-B-azgAvX.mjs +19 -0
  92. package/.output/server/_ssr/installations-Ctvzp2JH.mjs +535 -0
  93. package/.output/server/_ssr/router-C-KUBfOi.mjs +513 -0
  94. package/.output/server/_ssr/routes-BM4ht9jb.mjs +2286 -0
  95. package/.output/server/_ssr/routes-Z7TwC6nC.mjs +37 -0
  96. package/.output/server/_ssr/runtime.server-awTLiiXW.mjs +527 -0
  97. package/.output/server/_ssr/server-BjzmvXnO.mjs +7468 -0
  98. package/.output/server/_ssr/signing-k8GxyBDF.mjs +202 -0
  99. package/.output/server/_ssr/ssr.mjs +4 -5740
  100. package/.output/server/_ssr/start-DpViQGnt.mjs +4 -0
  101. package/.output/server/_ssr/table-v1yCvYo7.mjs +53 -0
  102. package/.output/server/_ssr/textarea-DYHnskhb.mjs +14 -0
  103. package/.output/server/_ssr/uuidv7-BbJ-9D_I.mjs +31 -0
  104. package/.output/server/_tanstack-start-manifest_v-CH4StzJu.mjs +108 -0
  105. package/.output/server/index.mjs +256 -53
  106. package/README.md +29 -8
  107. package/lib/index.d.mts +36 -0
  108. package/lib/index.mjs +4 -0
  109. package/lib/vite-internal.mjs +127 -0
  110. package/lib/vite.d.mts +10 -0
  111. package/lib/vite.mjs +5 -0
  112. package/package.json +51 -29
  113. package/public/apple-touch-icon.png +0 -0
  114. package/public/favicon-16x16.png +0 -0
  115. package/public/favicon-32x32.png +0 -0
  116. package/public/favicon.ico +0 -0
  117. package/public/logo.svg +1 -0
  118. package/public/manifest.json +30 -0
  119. package/public/robots.txt +3 -0
  120. package/src/components/AppSidebar.tsx +159 -0
  121. package/src/components/BundleIdDisplay.tsx +18 -0
  122. package/src/components/ChannelBadge.tsx +31 -0
  123. package/src/components/ConsoleAccessPage.tsx +122 -0
  124. package/src/components/EnabledStatusIcon.tsx +54 -0
  125. package/src/components/HashValueDisplay.tsx +85 -0
  126. package/src/components/HotUpdaterLogo.tsx +30 -0
  127. package/src/components/NotFoundPage.tsx +33 -0
  128. package/src/components/PlatformIcon.tsx +39 -0
  129. package/src/components/RolloutPercentageBadge.tsx +28 -0
  130. package/src/components/ThemeProvider.tsx +116 -0
  131. package/src/components/TimestampDisplay.tsx +41 -0
  132. package/src/components/features/analytics/ActivityChart.tsx +150 -0
  133. package/src/components/features/analytics/AnalyticsCapabilityContext.tsx +24 -0
  134. package/src/components/features/analytics/AnalyticsControls.tsx +115 -0
  135. package/src/components/features/analytics/AnalyticsErrorAlert.tsx +21 -0
  136. package/src/components/features/analytics/AnalyticsOverview.tsx +208 -0
  137. package/src/components/features/analytics/BundleDistribution.tsx +133 -0
  138. package/src/components/features/analytics/BundleSelector.tsx +114 -0
  139. package/src/components/features/analytics/InstallationHistoryCard.tsx +248 -0
  140. package/src/components/features/analytics/InstallationMatchesCard.tsx +132 -0
  141. package/src/components/features/analytics/InstallationPageHeader.tsx +138 -0
  142. package/src/components/features/analytics/InstallationPagination.tsx +48 -0
  143. package/src/components/features/analytics/InstallationSearch.tsx +230 -0
  144. package/src/components/features/analytics/UpdateOutcomes.tsx +147 -0
  145. package/src/components/features/api-keys/ApiKeysPage.tsx +377 -0
  146. package/src/components/features/bundles/BundleActivityChart.tsx +193 -0
  147. package/src/components/features/bundles/BundleAnalyticsSummary.tsx +123 -0
  148. package/src/components/features/bundles/BundleChildrenPanel.tsx +201 -0
  149. package/src/components/features/bundles/BundleMetadata.tsx +136 -0
  150. package/src/components/features/bundles/RolloutCohortsDialog.tsx +201 -0
  151. package/src/components/features/channels/ChannelManagementDialog.tsx +228 -0
  152. package/src/components/features/releases/ReleaseEditorSheet.tsx +879 -0
  153. package/src/components/features/releases/ReleaseStateBadge.tsx +20 -0
  154. package/src/components/features/signing/BundleSigningPage.tsx +247 -0
  155. package/src/components/ui/alert-dialog.tsx +185 -0
  156. package/src/components/ui/alert.tsx +76 -0
  157. package/src/components/ui/badge.tsx +49 -0
  158. package/src/components/ui/button.tsx +58 -0
  159. package/src/components/ui/card.tsx +89 -0
  160. package/src/components/ui/chart.tsx +371 -0
  161. package/src/components/ui/combobox.tsx +303 -0
  162. package/src/components/ui/dialog.tsx +156 -0
  163. package/src/components/ui/dropdown-menu.tsx +270 -0
  164. package/src/components/ui/field.tsx +235 -0
  165. package/src/components/ui/input-group.tsx +147 -0
  166. package/src/components/ui/input.tsx +19 -0
  167. package/src/components/ui/label.tsx +25 -0
  168. package/src/components/ui/progress.tsx +84 -0
  169. package/src/components/ui/select.tsx +205 -0
  170. package/src/components/ui/separator.tsx +23 -0
  171. package/src/components/ui/sheet.tsx +132 -0
  172. package/src/components/ui/sidebar.tsx +779 -0
  173. package/src/components/ui/skeleton.tsx +13 -0
  174. package/src/components/ui/slider.tsx +56 -0
  175. package/src/components/ui/sonner.tsx +45 -0
  176. package/src/components/ui/switch.tsx +25 -0
  177. package/src/components/ui/table.tsx +114 -0
  178. package/src/components/ui/textarea.tsx +18 -0
  179. package/src/components/ui/toggle-group.tsx +89 -0
  180. package/src/components/ui/toggle.tsx +43 -0
  181. package/src/components/ui/tooltip.tsx +66 -0
  182. package/src/hooks/use-mobile.ts +30 -0
  183. package/src/index.ts +48 -0
  184. package/src/lib/analytics-api.ts +145 -0
  185. package/src/lib/analytics-error.ts +19 -0
  186. package/src/lib/analytics-input.ts +187 -0
  187. package/src/lib/analytics-overview.ts +170 -0
  188. package/src/lib/analytics-rpc.ts +218 -0
  189. package/src/lib/api-keys-api.ts +69 -0
  190. package/src/lib/api-keys-rpc.ts +87 -0
  191. package/src/lib/api-rpc.ts +478 -0
  192. package/src/lib/api.ts +444 -0
  193. package/src/lib/auth-rpc.ts +16 -0
  194. package/src/lib/bundle-signing-api.ts +14 -0
  195. package/src/lib/bundle-signing-rpc.ts +16 -0
  196. package/src/lib/constants.ts +1 -0
  197. package/src/lib/git.ts +51 -0
  198. package/src/lib/server/auth.server.ts +43 -0
  199. package/src/lib/server/bundle-signing.server.ts +100 -0
  200. package/src/lib/server/config.server.ts +109 -0
  201. package/src/lib/server/console-runtime.server.ts +70 -0
  202. package/src/lib/server/deleteBundle.ts +145 -0
  203. package/src/lib/server/downloadBundle.ts +46 -0
  204. package/src/lib/server/getBundleChildren.ts +130 -0
  205. package/src/lib/server/listReleases.ts +61 -0
  206. package/src/lib/server/releaseActivity.ts +27 -0
  207. package/src/lib/server/releaseReachability.ts +95 -0
  208. package/src/lib/server/runtime.server.ts +172 -0
  209. package/src/lib/utils.ts +9 -0
  210. package/src/logo.svg +1 -0
  211. package/src/routeTree.gen.ts +199 -0
  212. package/src/router.tsx +21 -0
  213. package/src/routes/-installations-search.ts +19 -0
  214. package/src/routes/-releases-search.ts +74 -0
  215. package/src/routes/__root.tsx +174 -0
  216. package/src/routes/analytics.tsx +139 -0
  217. package/src/routes/api/auth/$.ts +19 -0
  218. package/src/routes/api/bundles/$bundleId/download.ts +17 -0
  219. package/src/routes/api-keys.tsx +37 -0
  220. package/src/routes/index.tsx +982 -0
  221. package/src/routes/installations.tsx +194 -0
  222. package/src/routes/signing.tsx +37 -0
  223. package/src/styles.css +152 -0
  224. package/src/virtual-modules.d.ts +21 -0
  225. package/src/vite-env.d.ts +1 -0
  226. package/src/vite-internal.ts +195 -0
  227. package/src/vite.ts +12 -0
  228. package/.output/public/assets/dist-DJX53faP.js +0 -3
  229. package/.output/public/assets/index-Iylis5kY.js +0 -10
  230. package/.output/public/assets/routes-HT21hs0C.js +0 -54
  231. package/.output/public/assets/styles-C0Re4quB.css +0 -2
  232. package/.output/server/__23tanstack-start-server-fn-resolver-CgwgZ0-i.mjs +0 -66
  233. package/.output/server/_libs/@floating-ui/core+[...].mjs +0 -698
  234. package/.output/server/_libs/@floating-ui/dom+[...].mjs +0 -644
  235. package/.output/server/_libs/@floating-ui/react-dom+[...].mjs +0 -839
  236. package/.output/server/_libs/@radix-ui/react-alert-dialog+[...].mjs +0 -2138
  237. package/.output/server/_libs/@radix-ui/react-popper+[...].mjs +0 -308
  238. package/.output/server/_libs/@radix-ui/react-select+[...].mjs +0 -1038
  239. package/.output/server/_libs/@tanstack/devtools-event-client+[...].mjs +0 -196
  240. package/.output/server/_libs/@tanstack/form-core+[...].mjs +0 -3228
  241. package/.output/server/_libs/@tanstack/react-form+[...].mjs +0 -430
  242. package/.output/server/_libs/@tanstack/react-table+[...].mjs +0 -2382
  243. package/.output/server/_libs/radix-ui__number.mjs +0 -6
  244. package/.output/server/_libs/radix-ui__primitive.mjs +0 -9
  245. package/.output/server/_libs/radix-ui__react-arrow.mjs +0 -23
  246. package/.output/server/_libs/radix-ui__react-collection.mjs +0 -78
  247. package/.output/server/_libs/radix-ui__react-direction.mjs +0 -13
  248. package/.output/server/_libs/radix-ui__react-label.mjs +0 -22
  249. package/.output/server/_libs/radix-ui__react-separator.mjs +0 -30
  250. package/.output/server/_libs/radix-ui__react-slider.mjs +0 -510
  251. package/.output/server/_libs/radix-ui__react-switch.mjs +0 -157
  252. package/.output/server/_libs/radix-ui__react-tooltip.mjs +0 -493
  253. package/.output/server/_ssr/api-rpc-BIbDUKSz.mjs +0 -292
  254. package/.output/server/_ssr/config.server-BSS366KT.mjs +0 -41
  255. package/.output/server/_ssr/deleteBundle-BgbZTtpW.mjs +0 -141
  256. package/.output/server/_ssr/dist-KboydRVk.mjs +0 -120
  257. package/.output/server/_ssr/router-C3hqEy3k.mjs +0 -307
  258. package/.output/server/_ssr/routes-B0lQvvlC.mjs +0 -3048
  259. package/.output/server/_ssr/start-02sLteYG.mjs +0 -4
  260. package/.output/server/_ssr/storageProfile-FVkjPoGb.mjs +0 -9
  261. package/.output/server/_tanstack-start-manifest_v-Bpx52WOU.mjs +0 -20
  262. package/.output/server/node_modules/tslib/modules/index.js +0 -70
  263. package/.output/server/node_modules/tslib/modules/package.json +0 -3
  264. package/.output/server/node_modules/tslib/package.json +0 -47
  265. package/.output/server/node_modules/tslib/tslib.js +0 -484
  266. 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
+ );