@hot-updater/server 1.0.0-rc.16 → 1.0.0-rc.18

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 (309) hide show
  1. package/dist/adapters/drizzle.cjs +5 -5
  2. package/dist/adapters/drizzle.mjs +4 -4
  3. package/dist/adapters/kysely.cjs +3 -3
  4. package/dist/adapters/kysely.mjs +2 -2
  5. package/dist/adapters/mongodb.cjs +3 -3
  6. package/dist/adapters/mongodb.mjs +2 -2
  7. package/dist/adapters/mongodbAdapter.cjs +19 -1
  8. package/dist/adapters/mongodbAdapter.mjs +19 -1
  9. package/dist/adapters/prisma.cjs +5 -5
  10. package/dist/adapters/prisma.mjs +4 -4
  11. package/dist/assembly/assemblePlugins.cjs +47 -28
  12. package/dist/assembly/assemblePlugins.d.cts +3 -7
  13. package/dist/assembly/assemblePlugins.d.mts +3 -7
  14. package/dist/assembly/assemblePlugins.mjs +46 -27
  15. package/dist/assembly/clientPlugins.cjs +86 -0
  16. package/dist/assembly/clientPlugins.d.cts +16 -0
  17. package/dist/assembly/clientPlugins.d.mts +16 -0
  18. package/dist/assembly/clientPlugins.mjs +86 -0
  19. package/dist/assembly/configError.cjs +7 -0
  20. package/dist/assembly/configError.d.cts +7 -0
  21. package/dist/assembly/configError.d.mts +7 -0
  22. package/dist/assembly/configError.mjs +7 -0
  23. package/dist/assembly/databasePlugins.cjs +5 -1
  24. package/dist/assembly/databasePlugins.d.cts +5 -5
  25. package/dist/assembly/databasePlugins.d.mts +5 -5
  26. package/dist/assembly/databasePlugins.mjs +5 -1
  27. package/dist/assembly/pluginCli.cjs +125 -0
  28. package/dist/assembly/pluginCli.d.cts +59 -0
  29. package/dist/assembly/pluginCli.d.mts +59 -0
  30. package/dist/assembly/pluginCli.mjs +121 -0
  31. package/dist/createHotUpdaterCore.cjs +14 -16
  32. package/dist/createHotUpdaterCore.d.cts +10 -3
  33. package/dist/createHotUpdaterCore.d.mts +10 -3
  34. package/dist/createHotUpdaterCore.mjs +11 -13
  35. package/dist/database/aggregateBatching.cjs +626 -0
  36. package/dist/database/aggregateBatching.d.cts +10 -0
  37. package/dist/database/aggregateBatching.d.mts +10 -0
  38. package/dist/database/aggregateBatching.mjs +625 -0
  39. package/dist/{insights/errors.cjs → database/busy.cjs} +2 -15
  40. package/dist/database/busy.d.cts +12 -0
  41. package/dist/database/busy.d.mts +12 -0
  42. package/dist/{insights/errors.mjs → database/busy.mjs} +3 -14
  43. package/dist/database/database.cjs +22 -3
  44. package/dist/database/database.d.cts +15 -2
  45. package/dist/database/database.d.mts +15 -2
  46. package/dist/database/database.mjs +22 -3
  47. package/dist/database/definitions.d.cts +10 -0
  48. package/dist/database/definitions.d.mts +10 -0
  49. package/dist/database/engine.d.cts +2 -1
  50. package/dist/database/engine.d.mts +2 -1
  51. package/dist/database/engineAggregates.cjs +7 -4
  52. package/dist/database/engineAggregates.mjs +8 -5
  53. package/dist/database/engineReads.cjs +5 -3
  54. package/dist/database/engineReads.mjs +6 -4
  55. package/dist/database/engineTransaction.cjs +2 -6
  56. package/dist/database/engineTransaction.mjs +2 -6
  57. package/dist/database/fence.cjs +2 -1
  58. package/dist/database/fence.mjs +2 -1
  59. package/dist/database/index.cjs +17 -8
  60. package/dist/database/index.d.cts +5 -4
  61. package/dist/database/index.d.mts +5 -4
  62. package/dist/database/index.mjs +6 -5
  63. package/dist/database/kv/kvAdapter.cjs +15 -3
  64. package/dist/database/kv/kvAdapter.d.cts +8 -0
  65. package/dist/database/kv/kvAdapter.d.mts +8 -0
  66. package/dist/database/kv/kvAdapter.mjs +16 -4
  67. package/dist/database/resolveSchema.cjs +4 -1
  68. package/dist/database/resolveSchema.d.cts +1 -1
  69. package/dist/database/resolveSchema.d.mts +1 -1
  70. package/dist/database/resolveSchema.mjs +4 -1
  71. package/dist/database/retention.cjs +92 -0
  72. package/dist/database/retention.mjs +90 -0
  73. package/dist/database/schema.cjs +5 -2
  74. package/dist/database/schema.d.cts +30 -1
  75. package/dist/database/schema.d.mts +30 -1
  76. package/dist/database/schema.mjs +5 -2
  77. package/dist/database/sql/sqlAdapter.cjs +8 -0
  78. package/dist/database/sql/sqlAdapter.mjs +9 -1
  79. package/dist/database/sql/sqlSchema.cjs +56 -0
  80. package/dist/database/sql/sqlSchema.mjs +55 -1
  81. package/dist/db/coreDatabase.cjs +85 -0
  82. package/dist/db/coreDatabase.d.cts +57 -0
  83. package/dist/db/coreDatabase.d.mts +57 -0
  84. package/dist/db/coreDatabase.mjs +78 -0
  85. package/dist/db/index.cjs +11 -3
  86. package/dist/db/index.d.cts +7 -3
  87. package/dist/db/index.d.mts +7 -3
  88. package/dist/db/index.mjs +6 -3
  89. package/dist/db/types.d.cts +3 -3
  90. package/dist/db/types.d.mts +3 -3
  91. package/dist/diff.cjs +3 -0
  92. package/dist/diff.d.cts +2 -0
  93. package/dist/diff.d.mts +2 -0
  94. package/dist/diff.mjs +2 -0
  95. package/dist/handler.cjs +2 -14
  96. package/dist/handler.mjs +1 -13
  97. package/dist/handlerReleaseCatalogRoutes.cjs +21 -2
  98. package/dist/handlerReleaseCatalogRoutes.mjs +21 -2
  99. package/dist/index.cjs +2 -6
  100. package/dist/index.d.cts +4 -7
  101. package/dist/index.d.mts +4 -7
  102. package/dist/index.mjs +2 -4
  103. package/dist/package.cjs +1 -1
  104. package/dist/package.mjs +1 -1
  105. package/dist/{apiKeys.cjs → plugins/api-keys/apiKeys.cjs} +1 -1
  106. package/dist/{apiKeys.d.cts → plugins/api-keys/apiKeys.d.cts} +2 -2
  107. package/dist/{apiKeys.d.mts → plugins/api-keys/apiKeys.d.mts} +2 -2
  108. package/dist/{apiKeys.mjs → plugins/api-keys/apiKeys.mjs} +1 -1
  109. package/dist/plugins/api-keys/cli.cjs +115 -0
  110. package/dist/plugins/api-keys/cli.mjs +114 -0
  111. package/dist/plugins/api-keys/index.cjs +9 -6
  112. package/dist/plugins/api-keys/index.d.cts +4 -5
  113. package/dist/plugins/api-keys/index.d.mts +4 -5
  114. package/dist/plugins/api-keys/index.mjs +9 -7
  115. package/dist/plugins/api-keys/model.d.cts +20 -2
  116. package/dist/plugins/api-keys/model.d.mts +20 -2
  117. package/dist/plugins/definePlugin.cjs +1 -1
  118. package/dist/plugins/definePlugin.d.cts +130 -3
  119. package/dist/plugins/definePlugin.d.mts +130 -3
  120. package/dist/plugins/definePlugin.mjs +1 -1
  121. package/dist/plugins/index.cjs +25 -4
  122. package/dist/plugins/index.d.cts +7 -4
  123. package/dist/plugins/index.d.mts +7 -4
  124. package/dist/plugins/index.mjs +6 -4
  125. package/dist/plugins/insights/contract.cjs +193 -0
  126. package/dist/plugins/insights/contract.mjs +189 -0
  127. package/dist/{insights → plugins/insights}/domain.d.cts +60 -15
  128. package/dist/{insights → plugins/insights}/domain.d.mts +60 -15
  129. package/dist/plugins/insights/errors.cjs +15 -0
  130. package/dist/plugins/insights/errors.mjs +14 -0
  131. package/dist/{insights → plugins/insights}/eventInput.cjs +155 -22
  132. package/dist/{insights → plugins/insights}/eventInput.mjs +155 -22
  133. package/dist/plugins/insights/eventRow.cjs +49 -0
  134. package/dist/plugins/insights/eventRow.d.cts +64 -0
  135. package/dist/plugins/insights/eventRow.d.mts +64 -0
  136. package/dist/plugins/insights/eventRow.mjs +47 -0
  137. package/dist/plugins/insights/index.cjs +106 -26
  138. package/dist/plugins/insights/index.d.cts +130 -27
  139. package/dist/plugins/insights/index.d.mts +130 -27
  140. package/dist/plugins/insights/index.mjs +106 -29
  141. package/dist/plugins/insights/model.cjs +2 -2
  142. package/dist/plugins/insights/model.d.cts +1 -1
  143. package/dist/plugins/insights/model.d.mts +1 -1
  144. package/dist/plugins/insights/model.mjs +1 -1
  145. package/dist/plugins/insights/modelTypes.d.cts +176 -0
  146. package/dist/plugins/insights/modelTypes.d.mts +176 -0
  147. package/dist/plugins/insights/overview.cjs +109 -0
  148. package/dist/plugins/insights/overview.d.cts +15 -0
  149. package/dist/plugins/insights/overview.d.mts +15 -0
  150. package/dist/plugins/insights/overview.mjs +107 -0
  151. package/dist/{insights → plugins/insights}/provider.cjs +33 -14
  152. package/dist/{insights → plugins/insights}/provider.d.cts +2 -2
  153. package/dist/{insights → plugins/insights}/provider.d.mts +2 -2
  154. package/dist/{insights → plugins/insights}/provider.mjs +30 -11
  155. package/dist/{insights → plugins/insights}/queryInput.cjs +2 -2
  156. package/dist/{insights → plugins/insights}/queryInput.mjs +2 -2
  157. package/dist/plugins/insights/reads.cjs +298 -88
  158. package/dist/plugins/insights/reads.d.cts +61 -0
  159. package/dist/plugins/insights/reads.d.mts +61 -0
  160. package/dist/plugins/insights/reads.mjs +298 -90
  161. package/dist/plugins/insights/recordEvent.cjs +223 -39
  162. package/dist/plugins/insights/recordEvent.d.cts +9 -4
  163. package/dist/plugins/insights/recordEvent.d.mts +9 -4
  164. package/dist/plugins/insights/recordEvent.mjs +221 -37
  165. package/dist/{insights → plugins/insights}/routes.cjs +3 -30
  166. package/dist/{insights → plugins/insights}/routes.mjs +4 -28
  167. package/dist/plugins/insights/schema.cjs +182 -40
  168. package/dist/plugins/insights/schema.d.cts +446 -6
  169. package/dist/plugins/insights/schema.d.mts +446 -6
  170. package/dist/plugins/insights/schema.mjs +180 -41
  171. package/dist/plugins/insights/testing/eventRow.d.mts +64 -0
  172. package/dist/plugins/insights/testing/expectInsightsIndex.mjs +18 -0
  173. package/dist/plugins/insights/testing/fixtures.d.mts +8 -0
  174. package/dist/plugins/insights/testing/fixtures.mjs +25 -0
  175. package/dist/plugins/insights/testing/index.d.mts +24 -0
  176. package/dist/plugins/insights/testing/index.mjs +25 -0
  177. package/dist/plugins/insights/testing/insightsModelTests.mjs +714 -0
  178. package/dist/plugins/insights/testing/modelTypes.d.mts +176 -0
  179. package/dist/plugins/insights/testing/setupInsightsHttpTestSuite.d.mts +11 -0
  180. package/dist/plugins/insights/testing/setupInsightsHttpTestSuite.mjs +107 -0
  181. package/dist/plugins/insights/testing/setupInsightsModelTestSuite.d.mts +11 -0
  182. package/dist/plugins/insights/testing/setupInsightsModelTestSuite.mjs +31 -0
  183. package/dist/plugins/insights/testing/types.d.mts +26 -0
  184. package/dist/{insights → plugins/insights}/types.d.cts +3 -2
  185. package/dist/{insights → plugins/insights}/types.d.mts +3 -2
  186. package/package.json +18 -7
  187. package/src/adapters/drizzle.conformance.integration.spec.ts +2 -2
  188. package/src/adapters/drizzle.readBudgets.integration.spec.ts +6 -4
  189. package/src/adapters/drizzle.spec.ts +15 -7
  190. package/src/adapters/drizzle.ts +6 -6
  191. package/src/adapters/kysely.conformance.integration.spec.ts +2 -2
  192. package/src/adapters/kysely.readBudgets.integration.spec.ts +4 -2
  193. package/src/adapters/kysely.spec.ts +13 -9
  194. package/src/adapters/kysely.ts +2 -2
  195. package/src/adapters/mongodb.integration.spec.ts +98 -7
  196. package/src/adapters/mongodb.ts +2 -2
  197. package/src/adapters/mongodbAdapter.ts +14 -0
  198. package/src/adapters/prisma.conformance.integration.spec.ts +2 -2
  199. package/src/adapters/prisma.integration.spec.ts +16 -8
  200. package/src/adapters/prisma.readBudgets.integration.spec.ts +5 -2
  201. package/src/adapters/prisma.spec.ts +17 -9
  202. package/src/adapters/prisma.ts +6 -6
  203. package/src/adapters/prismaTestClients.ts +8 -1
  204. package/src/assembly/assemblePlugins.ts +73 -40
  205. package/src/assembly/assembly.spec.ts +63 -11
  206. package/src/assembly/assembly.types.spec.ts +50 -2
  207. package/src/assembly/clientPlugins.ts +113 -0
  208. package/src/assembly/configError.ts +4 -0
  209. package/src/assembly/databasePlugins.ts +11 -5
  210. package/src/assembly/pluginCli.spec.ts +254 -0
  211. package/src/assembly/pluginCli.ts +248 -0
  212. package/src/assembly/{builtInRoutes.spec.ts → pluginRoutes.spec.ts} +10 -37
  213. package/src/createHotUpdaterCore.ts +21 -17
  214. package/src/database/aggregateBatching.spec.ts +577 -0
  215. package/src/database/aggregateBatching.ts +803 -0
  216. package/src/{insights/errors.spec.ts → database/busy.spec.ts} +2 -5
  217. package/src/{insights/errors.ts → database/busy.ts} +1 -13
  218. package/src/database/database.ts +39 -8
  219. package/src/database/definitions.ts +4 -0
  220. package/src/database/engineAggregates.spec.ts +4 -7
  221. package/src/database/engineAggregates.ts +5 -9
  222. package/src/database/engineReads.ts +7 -5
  223. package/src/database/engineTransaction.ts +1 -7
  224. package/src/database/fence.spec.ts +5 -5
  225. package/src/database/fence.ts +4 -0
  226. package/src/database/index.ts +13 -7
  227. package/src/database/kv/kvAdapter.batching.integration.spec.ts +15 -0
  228. package/src/database/kv/kvAdapter.conformance.integration.spec.ts +2 -0
  229. package/src/database/kv/kvAdapter.spec.ts +128 -1
  230. package/src/database/kv/kvAdapter.ts +30 -1
  231. package/src/database/kv/kvTestStore.ts +28 -1
  232. package/src/database/resolveSchema.ts +4 -1
  233. package/src/database/retention.spec.ts +314 -0
  234. package/src/database/retention.ts +129 -0
  235. package/src/database/schema.ts +44 -0
  236. package/src/database/sql/sqlAdapter.integration.spec.ts +4 -7
  237. package/src/database/sql/sqlAdapter.readBudgets.integration.spec.ts +18 -4
  238. package/src/database/sql/sqlAdapter.spec.ts +91 -3
  239. package/src/database/sql/sqlAdapter.ts +20 -0
  240. package/src/database/sql/sqlSchema.ts +81 -0
  241. package/src/databaseTestSuite.spec.ts +9 -4
  242. package/src/db/coreDatabase.ts +153 -0
  243. package/src/db/engineSql.spec.ts +8 -7
  244. package/src/db/index.ts +19 -2
  245. package/src/db/pluginTables.spec.ts +57 -27
  246. package/src/db/types.ts +3 -3
  247. package/src/diff.ts +6 -0
  248. package/src/handler-standalone.integration.spec.ts +2 -3
  249. package/src/handler.ts +1 -32
  250. package/src/handlerReleaseCatalogRoutes.ts +26 -2
  251. package/src/index.ts +0 -10
  252. package/src/{apiKeys.integration.spec.ts → plugins/api-keys/apiKeys.integration.spec.ts} +4 -4
  253. package/src/{apiKeys.spec.ts → plugins/api-keys/apiKeys.spec.ts} +3 -3
  254. package/src/{apiKeys.ts → plugins/api-keys/apiKeys.ts} +1 -1
  255. package/src/plugins/api-keys/apiKeysPlugin.spec.ts +11 -15
  256. package/src/plugins/api-keys/cli.spec.ts +187 -0
  257. package/src/plugins/api-keys/cli.ts +140 -0
  258. package/src/plugins/api-keys/index.ts +48 -41
  259. package/src/plugins/api-keys/model.ts +21 -2
  260. package/src/plugins/definePlugin.ts +144 -2
  261. package/src/plugins/index.ts +20 -2
  262. package/src/plugins/insights/contract.spec.ts +285 -0
  263. package/src/plugins/insights/contract.ts +372 -0
  264. package/src/{insights → plugins/insights}/domain.ts +73 -15
  265. package/src/plugins/insights/errors.ts +11 -0
  266. package/src/{insights → plugins/insights}/eventInput.ts +189 -29
  267. package/src/plugins/insights/eventRow.ts +209 -0
  268. package/src/plugins/insights/index.ts +181 -45
  269. package/src/{insights → plugins/insights}/insights.integration.spec.ts +330 -34
  270. package/src/plugins/insights/insightsRollout.integration.spec.ts +1 -2
  271. package/src/plugins/insights/model.spec.ts +3 -5
  272. package/src/plugins/insights/model.ts +2 -3
  273. package/src/plugins/insights/modelTypes.ts +197 -0
  274. package/src/plugins/insights/overview.spec.ts +64 -0
  275. package/src/plugins/insights/overview.ts +214 -0
  276. package/src/{insights → plugins/insights}/provider.spec.ts +21 -10
  277. package/src/{insights → plugins/insights}/provider.ts +58 -21
  278. package/src/{insights → plugins/insights}/queryInput.ts +2 -2
  279. package/src/plugins/insights/reads.spec.ts +340 -52
  280. package/src/plugins/insights/reads.ts +453 -123
  281. package/src/plugins/insights/recordEvent.spec.ts +474 -20
  282. package/src/plugins/insights/recordEvent.ts +301 -63
  283. package/src/plugins/insights/retention.spec.ts +115 -0
  284. package/src/{insights → plugins/insights}/routes.ts +1 -31
  285. package/src/plugins/insights/schema.ts +356 -176
  286. package/src/plugins/insights/testing/expectInsightsIndex.spec.ts +42 -0
  287. package/src/plugins/insights/testing/expectInsightsIndex.ts +20 -0
  288. package/src/plugins/insights/testing/fixtures.ts +30 -0
  289. package/src/plugins/insights/testing/index.ts +45 -0
  290. package/src/plugins/insights/testing/insightsModelTests.ts +860 -0
  291. package/src/plugins/insights/testing/setupInsightsHttpTestSuite.ts +152 -0
  292. package/src/plugins/insights/testing/setupInsightsModelTestSuite.ts +36 -0
  293. package/src/plugins/insights/testing/types.ts +30 -0
  294. package/src/{insights → plugins/insights}/types.ts +1 -1
  295. package/src/readBudgets.testFixtures.ts +2 -2
  296. package/src/releaseCatalogRoutes.integration.spec.ts +7 -0
  297. package/src/retention.spec.ts +73 -0
  298. package/src/runtime.spec.ts +11 -12
  299. package/src/runtime.testFixtures.ts +7 -2
  300. package/src/testUtilsMutation.integration.spec.ts +2 -1
  301. package/src/testUtilsPackage.integration.spec.ts +7 -3
  302. package/dist/db/builtInDatabase.cjs +0 -104
  303. package/dist/db/builtInDatabase.d.cts +0 -38
  304. package/dist/db/builtInDatabase.d.mts +0 -38
  305. package/dist/db/builtInDatabase.mjs +0 -97
  306. package/dist/plugins/builtIn.cjs +0 -7
  307. package/dist/plugins/builtIn.mjs +0 -6
  308. package/src/db/builtInDatabase.ts +0 -149
  309. package/src/plugins/builtIn.ts +0 -5
@@ -1,24 +1,23 @@
1
- import {
2
- compareInsightsText,
3
- isInsightsMovementEvent,
4
- isUUIDv7,
5
- type InsightsBundleEventFilter,
6
- type BundleEventRow,
7
- type InsightsEventCursor,
8
- type InsightsEventFilter,
9
- type InsightsModel,
10
- } from "@hot-updater/plugin-core";
1
+ import { isUUIDv7 } from "@hot-updater/plugin-core";
11
2
 
3
+ import { compareInsightsText, isInsightsMovementEvent } from "./contract";
12
4
  import type {
13
5
  ActiveInstallationWindow,
14
6
  InsightsBundleSelection,
15
- InsightsScope,
16
7
  EventHistoryRow,
17
8
  InstallationHistoryRow,
18
9
  InstallationRow,
19
10
  } from "./domain";
20
11
  import { InsightsBadRequestError } from "./errors";
21
12
  import { createBundleEventRow } from "./eventInput";
13
+ import type { BundleEventFailure, BundleEventRow } from "./eventRow";
14
+ import type {
15
+ InsightsBundleEventFilter,
16
+ InsightsEventCursor,
17
+ InsightsEventFilter,
18
+ InsightsModel,
19
+ InsightsScope,
20
+ } from "./modelTypes";
22
21
  import type {
23
22
  InsightsEventPageInput,
24
23
  InsightsInstallationEventPageInput,
@@ -33,6 +32,7 @@ const MAX_IDENTITY_LENGTH = 255;
33
32
  const MAX_CURSOR_LENGTH = 8 * 1_024;
34
33
 
35
34
  const HOUR_MS = 60 * 60 * 1_000;
35
+ const DAY_MS = 24 * HOUR_MS;
36
36
 
37
37
  const WINDOW_MS: Record<ActiveInstallationWindow, number> = {
38
38
  "24h": 24 * 60 * 60 * 1_000,
@@ -169,8 +169,8 @@ const bundleFilter = (
169
169
  return { ...scope, type: "UPDATE_APPLIED", toBundleId: bundleId };
170
170
  case "recovered":
171
171
  return { ...scope, type: "RECOVERED", fromBundleId: bundleId };
172
- case "unchanged":
173
- return { ...scope, type: "UNCHANGED", toBundleId: bundleId };
172
+ case "failed":
173
+ return { ...scope, type: "UPDATE_FAILED", toBundleId: bundleId };
174
174
  default:
175
175
  throw new InsightsBadRequestError("Invalid Insights outcome.");
176
176
  }
@@ -318,6 +318,41 @@ const assertEventRows = (
318
318
  }
319
319
  };
320
320
 
321
+ /** An `UPDATE_FAILED` row's failure, as the event lists show it. */
322
+ const toFailure = (
323
+ failure: BundleEventFailure,
324
+ ): NonNullable<EventHistoryRow["failure"]> => ({
325
+ stage: failure.stage,
326
+ reason: failure.reason,
327
+ ...(failure.resource === undefined ? {} : { resource: failure.resource }),
328
+ ...(failure.http_status === undefined
329
+ ? {}
330
+ : { httpStatus: failure.http_status }),
331
+ ...(failure.transport === undefined ? {} : { transport: failure.transport }),
332
+ ...(failure.origin_code === undefined
333
+ ? {}
334
+ : { originCode: failure.origin_code }),
335
+ ...(failure.previous_process_exit === undefined
336
+ ? {}
337
+ : { previousProcessExit: failure.previous_process_exit }),
338
+ });
339
+
340
+ /** What a row's metadata adds for its type: a failure, a delivery, an exit reason. */
341
+ const eventDetails = ({ type, metadata }: BundleEventRow) => ({
342
+ ...(type === "UPDATE_FAILED" && metadata.failure !== undefined
343
+ ? { failure: toFailure(metadata.failure) }
344
+ : {}),
345
+ ...(type === "UPDATE_DOWNLOADED" && metadata.delivery !== undefined
346
+ ? { delivery: metadata.delivery }
347
+ : {}),
348
+ ...(type === "UPDATE_DOWNLOADED" && metadata.patch_fallback === true
349
+ ? { patchFallback: true as const }
350
+ : {}),
351
+ ...(type === "RECOVERED" && metadata.previous_process_exit !== undefined
352
+ ? { previousProcessExit: metadata.previous_process_exit }
353
+ : {}),
354
+ });
355
+
321
356
  const toEventHistoryRow = (row: BundleEventRow): EventHistoryRow => ({
322
357
  appVersion: row.app_version,
323
358
  channel: row.channel,
@@ -330,7 +365,7 @@ const toEventHistoryRow = (row: BundleEventRow): EventHistoryRow => ({
330
365
  toBundleId: row.to_bundle_id,
331
366
  type: row.type,
332
367
  userId: row.user_id,
333
- username: row.metadata.username,
368
+ ...eventDetails(row),
334
369
  });
335
370
 
336
371
  const toInstallationRow = (row: BundleEventRow): InstallationRow => ({
@@ -346,7 +381,6 @@ const toInstallationRow = (row: BundleEventRow): InstallationRow => ({
346
381
  platform: row.platform,
347
382
  receivedAtMs: row.received_at_ms,
348
383
  userId: row.user_id,
349
- username: row.metadata.username,
350
384
  });
351
385
 
352
386
  interface EventBounds {
@@ -581,12 +615,15 @@ export const createInsightsProvider = (
581
615
  "Invalid reporting installation window.",
582
616
  );
583
617
  }
584
- // Whole hours, ending with the current one: the counts read only the
585
- // maintained hour and day rows, never raw events.
618
+ // Ending with the current hour and starting with the UTC day the
619
+ // window reaches into: latest-event gauges count whole UTC days and
620
+ // outcome counters whole hours, so the counts read only maintained
621
+ // rows, never raw events.
586
622
  const now = Date.now();
587
623
  const beforeReceivedAtMs =
588
624
  now - (now % HOUR_MS) + (now % HOUR_MS === 0 ? 0 : HOUR_MS);
589
- const sinceMs = Math.max(0, beforeReceivedAtMs - WINDOW_MS[window]);
625
+ const reach = Math.max(0, beforeReceivedAtMs - WINDOW_MS[window]);
626
+ const sinceMs = reach - (reach % DAY_MS);
590
627
  const measure = async (count: Promise<number>) => {
591
628
  const value = await count;
592
629
  if (!Number.isSafeInteger(value) || value < 0) {
@@ -622,7 +659,7 @@ export const createInsightsProvider = (
622
659
  downloadedReports,
623
660
  appliedReports,
624
661
  recoveredReports,
625
- unchangedReports,
662
+ failedReports,
626
663
  ] = await Promise.all([
627
664
  reporting,
628
665
  measure(
@@ -646,7 +683,7 @@ export const createInsightsProvider = (
646
683
  countOutcome("downloaded"),
647
684
  countOutcome("applied"),
648
685
  countOutcome("recovered"),
649
- countOutcome("unchanged"),
686
+ countOutcome("failed"),
650
687
  ]);
651
688
  return {
652
689
  ...scope,
@@ -660,7 +697,7 @@ export const createInsightsProvider = (
660
697
  downloadedReports,
661
698
  appliedReports,
662
699
  recoveredReports,
663
- unchangedReports,
700
+ failedReports,
664
701
  },
665
702
  };
666
703
  },
@@ -1,9 +1,9 @@
1
1
  import type {
2
2
  ActiveInstallationWindow,
3
3
  InsightsBundleSelection,
4
- InsightsScope,
5
4
  } from "./domain";
6
5
  import { InsightsBadRequestError } from "./errors";
6
+ import type { InsightsScope } from "./modelTypes";
7
7
  import type {
8
8
  InsightsEventPageInput,
9
9
  InsightsUserInstallationPageInput,
@@ -88,7 +88,7 @@ export const parseEventPageInput = (
88
88
  outcome !== "downloaded" &&
89
89
  outcome !== "applied" &&
90
90
  outcome !== "recovered" &&
91
- outcome !== "unchanged"
91
+ outcome !== "failed"
92
92
  ) {
93
93
  throw new InsightsBadRequestError("Invalid 'outcome' query parameter.");
94
94
  }
@@ -1,19 +1,19 @@
1
- import type { BundleEventRow } from "@hot-updater/plugin-core";
2
1
  import {
3
- addInsightsDistinct,
4
- countInsightsDistinct,
2
+ addDistinct,
3
+ countDistinct,
5
4
  createMemoryAdapter,
6
5
  type DatabaseAdapter,
7
- mergeInsightsDistinct,
6
+ mergeDistinct,
8
7
  } from "@hot-updater/plugin-core/internal";
9
8
  import { createPluginTestHarness } from "@hot-updater/test-utils";
10
9
  import { describe, expect, it } from "vitest";
11
10
 
12
11
  import * as engine from "../../database";
13
- import { InsightsBadRequestError } from "../../insights/errors";
14
- import { createInsightsProvider } from "../../insights/provider";
15
- import type { InsightsEventPageInput } from "../../insights/types";
12
+ import { InsightsBadRequestError } from "./errors";
13
+ import type { BundleEventRow } from "./eventRow";
16
14
  import { createInsightsModel, insights } from "./index";
15
+ import { createInsightsProvider } from "./provider";
16
+ import type { InsightsEventPageInput } from "./types";
17
17
 
18
18
  const HOUR = 3_600_000;
19
19
  const DAY = 86_400_000;
@@ -39,7 +39,6 @@ const event = (
39
39
  app_version: "1.0.0",
40
40
  channel: "production",
41
41
  metadata: {
42
- username: null,
43
42
  cohort: "1",
44
43
  update_strategy: "appVersion",
45
44
  fingerprint_hash: null,
@@ -51,9 +50,7 @@ const event = (
51
50
 
52
51
  /** The HLL estimate for these installs, as the stored sketches merge to. */
53
52
  const distinct = (installs: readonly string[]) =>
54
- countInsightsDistinct(
55
- mergeInsightsDistinct(installs.map((id) => addInsightsDistinct(null, id))),
56
- );
53
+ countDistinct(mergeDistinct(installs.map((id) => addDistinct(null, id))));
57
54
  const installs = Array.from({ length: 24 }, (_, n) => `install-${n + 1}`);
58
55
 
59
56
  /** Rows each table returned, and the point reads it served. */
@@ -108,17 +105,14 @@ describe("insights latest events by bundle", () => {
108
105
  types: ["UNCHANGED", "UPDATE_APPLIED", "RECOVERED"],
109
106
  },
110
107
  ] as const;
111
- // Whole hours from gauges, then a partial hour from heads.
112
- for (const sinceMs of [T0, T0 + 1]) {
113
- await expect(
114
- harness.api.countLatestEvents({
115
- platform: "ios",
116
- channel: "production",
117
- sinceMs,
118
- bundle,
119
- }),
120
- ).resolves.toBe(4);
121
- }
108
+ await expect(
109
+ harness.api.countLatestEvents({
110
+ platform: "ios",
111
+ channel: "production",
112
+ sinceMs: T0,
113
+ bundle,
114
+ }),
115
+ ).resolves.toBe(4);
122
116
  });
123
117
 
124
118
  it("counts a head once when a from and a to predicate of one type both match it", async () => {
@@ -152,29 +146,48 @@ describe("insights latest events by bundle", () => {
152
146
  sinceMs,
153
147
  bundle,
154
148
  });
155
- // Whole hours from gauges, then a partial hour from heads.
156
- for (const sinceMs of [T0, T0 + 1]) {
157
- // From A or to B: A→B, A→C, D→B, and B→B. The gauges alone say 5,
158
- // counting A→B under both predicates.
159
- await expect(
160
- count(
161
- [
162
- { field: "from_bundle_id", value: "A", types },
163
- { field: "to_bundle_id", value: "B", types },
164
- ],
165
- sinceMs,
166
- ),
167
- ).resolves.toBe(4);
168
- // B→B matches from B and to B through one pair: A→B, D→B, B→B.
169
- await expect(
170
- count(
171
- [
172
- { field: "from_bundle_id", value: "B", types },
173
- { field: "to_bundle_id", value: "B", types },
174
- ],
175
- sinceMs,
176
- ),
177
- ).resolves.toBe(3);
149
+ // From A or to B: A→B, A→C, D→B, and B→B. The gauges alone say 5,
150
+ // counting A→B under both predicates.
151
+ await expect(
152
+ count(
153
+ [
154
+ { field: "from_bundle_id", value: "A", types },
155
+ { field: "to_bundle_id", value: "B", types },
156
+ ],
157
+ T0,
158
+ ),
159
+ ).resolves.toBe(4);
160
+ // B→B matches from B and to B through one pair: A→B, D→B, B→B.
161
+ await expect(
162
+ count(
163
+ [
164
+ { field: "from_bundle_id", value: "B", types },
165
+ { field: "to_bundle_id", value: "B", types },
166
+ ],
167
+ T0,
168
+ ),
169
+ ).resolves.toBe(3);
170
+ });
171
+
172
+ it("counts whole UTC days only, and rejects a start inside one", async () => {
173
+ const harness = await setup();
174
+ await harness.api.recordEvent(event(1, { received_at_ms: halfPast }));
175
+ await harness.api.recordEvent(
176
+ event(2, { received_at_ms: T0 + DAY + HOUR }),
177
+ );
178
+ const since = (sinceMs: number) =>
179
+ harness.api.countLatestEvents({
180
+ platform: "ios",
181
+ channel: "production",
182
+ sinceMs,
183
+ });
184
+ await expect(since(T0)).resolves.toBe(2);
185
+ await expect(since(T0 + DAY)).resolves.toBe(1);
186
+ await expect(since(T0 + 2 * DAY)).resolves.toBe(0);
187
+ for (const sinceMs of [T0 + 1, T0 + HOUR]) {
188
+ await expect(since(sinceMs)).rejects.toMatchObject({
189
+ code: "invalid-query",
190
+ });
178
191
  }
179
192
  });
180
193
  });
@@ -241,7 +254,7 @@ describe("insights read budgets", () => {
241
254
  expect(user.tables).toEqual({ bundle_event_heads: 3 });
242
255
  });
243
256
 
244
- it("counts whole-hour windows from aggregates alone", async () => {
257
+ it("counts whole hours and whole UTC days from aggregates alone", async () => {
245
258
  const { api, read } = await setup();
246
259
  const counted = await read(() =>
247
260
  api.countEvents({
@@ -262,11 +275,11 @@ describe("insights read budgets", () => {
262
275
  api.countLatestEvents({
263
276
  platform: "ios",
264
277
  channel: "production",
265
- sinceMs: T0 + HOUR,
278
+ sinceMs: T0 + DAY,
266
279
  }),
267
280
  );
268
- // installs 6–24, plus install-1, whose head moved to day 2
269
- expect(latest.result).toBe(20);
281
+ // install-1, whose head moved to day 2
282
+ expect(latest.result).toBe(1);
270
283
  expect(Object.keys(latest.tables)).toEqual(["insights_distribution"]);
271
284
 
272
285
  const byBundle = await read(() =>
@@ -301,7 +314,9 @@ describe("insights read budgets", () => {
301
314
  launches: 25,
302
315
  failedLaunches: 0,
303
316
  });
304
- expect(Object.keys(lifetime.tables)).toEqual(["insights_overview"]);
317
+ expect(Object.keys(lifetime.tables)).toEqual([
318
+ "insights_overview_lifetime",
319
+ ]);
305
320
 
306
321
  const scope = await read(() =>
307
322
  api.getReleaseActivity({
@@ -317,9 +332,10 @@ describe("insights read budgets", () => {
317
332
  { startMs: T0 + 2 * DAY, launches: 1, failedLaunches: 0 },
318
333
  ],
319
334
  });
335
+ // Whole UTC days: the daily rollups alone.
320
336
  expect(Object.keys(scope.tables).toSorted()).toEqual([
321
- "insights_overview",
322
- "insights_sketches",
337
+ "insights_overview_daily",
338
+ "insights_sketches_daily",
323
339
  ]);
324
340
 
325
341
  const usage = await read(() =>
@@ -349,9 +365,281 @@ describe("insights read budgets", () => {
349
365
  });
350
366
  expect(Object.keys(usage.tables).toSorted()).toEqual([
351
367
  "insights_distribution",
368
+ "insights_sketches_daily",
369
+ ]);
370
+ });
371
+ });
372
+
373
+ describe("insights update failures", () => {
374
+ const setup = async () => {
375
+ const meter = metered(createMemoryAdapter());
376
+ const harness = await createPluginTestHarness(insights(), {
377
+ engine,
378
+ adapter: meter.adapter,
379
+ now: () => T0 + 30 * DAY,
380
+ });
381
+ const record = (
382
+ n: number,
383
+ install: number,
384
+ at: number,
385
+ overrides: Partial<BundleEventRow>,
386
+ metadata: Record<string, unknown> = {},
387
+ ) =>
388
+ harness.api.recordEvent(
389
+ event(n, {
390
+ install_id: `install-${install}`,
391
+ received_at_ms: at,
392
+ ...overrides,
393
+ metadata: { ...event(n).metadata, ...metadata },
394
+ } as Partial<BundleEventRow>),
395
+ );
396
+ const fail = (
397
+ n: number,
398
+ install: number,
399
+ at: number,
400
+ failure: Record<string, unknown>,
401
+ ) =>
402
+ record(
403
+ n,
404
+ install,
405
+ at,
406
+ failure.stage === "check"
407
+ ? {
408
+ type: "UPDATE_FAILED",
409
+ to_release_id: null,
410
+ to_bundle_id: "bundle-a",
411
+ }
412
+ : { type: "UPDATE_FAILED" },
413
+ { failure },
414
+ );
415
+ const denied = { stage: "download", reason: "http", http_status: 403 };
416
+ // install-1 fails to download release-b on two days, install-2 once with
417
+ // another origin code, install-3 on the network, install-4 installing.
418
+ await fail(1, 1, T0 + HOUR, { ...denied, origin_code: "AccessDenied" });
419
+ await fail(2, 1, T0 + DAY + HOUR, {
420
+ ...denied,
421
+ origin_code: "AccessDenied",
422
+ });
423
+ await fail(3, 2, T0 + 2 * HOUR, { ...denied, origin_code: "ExpiredToken" });
424
+ await fail(4, 3, T0 + 3 * HOUR, {
425
+ stage: "download",
426
+ reason: "network",
427
+ transport: "timeout",
428
+ });
429
+ await fail(5, 4, T0 + 4 * HOUR, {
430
+ stage: "install",
431
+ reason: "hash_mismatch",
432
+ });
433
+ // install-8's update check fails: its channel's, no release's.
434
+ await fail(6, 8, T0 + 5 * HOUR, { stage: "check", reason: "http" });
435
+ // Three downloads: two by patch, one that fell back from a patch.
436
+ for (const [n, install, metadata] of [
437
+ [7, 5, { delivery: "patch" }],
438
+ [8, 6, { delivery: "patch" }],
439
+ [9, 7, { delivery: "archive", patch_fallback: true }],
440
+ ] as const) {
441
+ await record(
442
+ n,
443
+ install,
444
+ T0 + 6 * HOUR,
445
+ { type: "UPDATE_DOWNLOADED" },
446
+ metadata,
447
+ );
448
+ }
449
+ // Two recoveries from release-b, one with an exit reason.
450
+ const recovered = {
451
+ type: "RECOVERED",
452
+ from_release_id: "release-b",
453
+ from_bundle_id: "bundle-b",
454
+ to_release_id: "release-a",
455
+ to_bundle_id: "bundle-a",
456
+ } as const;
457
+ await record(10, 9, T0 + 7 * HOUR, recovered, {
458
+ previous_process_exit: "CRASH",
459
+ });
460
+ await record(11, 10, T0 + 7 * HOUR, recovered);
461
+ const read = async <T>(call: () => Promise<T>) => {
462
+ meter.rows.clear();
463
+ const measured = await harness.measureReads(call);
464
+ return {
465
+ ...measured,
466
+ tables: Object.keys(Object.fromEntries(meter.rows)),
467
+ };
468
+ };
469
+ return { api: harness.api, read };
470
+ };
471
+ const scope = { platform: "ios", channel: "production" } as const;
472
+
473
+ it("reads a release's failures, installations, and breakdown over a window from aggregates only", async () => {
474
+ const { api, read } = await setup();
475
+ const failures = await read(() =>
476
+ api.getUpdateFailures({
477
+ ...scope,
478
+ releaseId: "release-b",
479
+ timeRange: { start: T0, end: T0 + 2 * DAY },
480
+ }),
481
+ );
482
+ expect(failures.result).toEqual({
483
+ coverage: { kind: "complete", sinceMs: 0 },
484
+ measuredAtMs: T0 + 30 * DAY,
485
+ failedUpdates: 5,
486
+ failedInstallations: distinct([
487
+ "install-1",
488
+ "install-2",
489
+ "install-3",
490
+ "install-4",
491
+ ]),
492
+ downloads: 3,
493
+ patchDownloads: 2,
494
+ patchFallbacks: 1,
495
+ breakdown: [
496
+ {
497
+ stage: "download",
498
+ reason: "http",
499
+ events: 3,
500
+ details: [
501
+ {
502
+ resource: null,
503
+ httpStatus: 403,
504
+ originCode: "AccessDenied",
505
+ transport: null,
506
+ events: 2,
507
+ },
508
+ {
509
+ resource: null,
510
+ httpStatus: 403,
511
+ originCode: "ExpiredToken",
512
+ transport: null,
513
+ events: 1,
514
+ },
515
+ ],
516
+ },
517
+ {
518
+ stage: "download",
519
+ reason: "network",
520
+ events: 1,
521
+ details: [
522
+ {
523
+ resource: null,
524
+ httpStatus: null,
525
+ originCode: null,
526
+ transport: "timeout",
527
+ events: 1,
528
+ },
529
+ ],
530
+ },
531
+ {
532
+ stage: "install",
533
+ reason: "hash_mismatch",
534
+ events: 1,
535
+ details: [
536
+ {
537
+ resource: null,
538
+ httpStatus: null,
539
+ originCode: null,
540
+ transport: null,
541
+ events: 1,
542
+ },
543
+ ],
544
+ },
545
+ ],
546
+ recoveries: {
547
+ failedLaunches: 2,
548
+ byExitReason: [{ exitReason: "CRASH", events: 1 }],
549
+ },
550
+ });
551
+ expect(failures.tables.toSorted()).toEqual([
552
+ "insights_failures",
553
+ "insights_overview",
352
554
  "insights_sketches",
353
555
  ]);
354
556
  });
557
+
558
+ it("reads a channel's failed checks against its active installations, from daily rows past 48 hours", async () => {
559
+ const { api, read } = await setup();
560
+ const failures = await read(() =>
561
+ api.getUpdateFailures({
562
+ ...scope,
563
+ timeRange: { start: T0, end: T0 + 3 * DAY },
564
+ }),
565
+ );
566
+ expect(failures.result).toMatchObject({
567
+ failedUpdates: 5,
568
+ failedInstallations: distinct([
569
+ "install-1",
570
+ "install-2",
571
+ "install-3",
572
+ "install-4",
573
+ ]),
574
+ downloads: 3,
575
+ // A failure is no activity: the downloads' and recoveries' installations.
576
+ checks: {
577
+ failures: 1,
578
+ failedInstallations: distinct(["install-8"]),
579
+ activeInstallations: distinct([
580
+ "install-5",
581
+ "install-6",
582
+ "install-7",
583
+ "install-9",
584
+ "install-10",
585
+ ]),
586
+ },
587
+ recoveries: { failedLaunches: 2 },
588
+ });
589
+ expect(
590
+ failures.result.breakdown!.map(({ stage, reason, events }) => [
591
+ stage,
592
+ reason,
593
+ events,
594
+ ]),
595
+ ).toEqual([
596
+ ["download", "http", 3],
597
+ ["check", "http", 1],
598
+ ["download", "network", 1],
599
+ ["install", "hash_mismatch", 1],
600
+ ]);
601
+ expect(failures.tables.toSorted()).toEqual([
602
+ "insights_failures",
603
+ "insights_overview_daily",
604
+ "insights_sketches_daily",
605
+ ]);
606
+ });
607
+
608
+ it("reads a release's failures since its first from the kept rows, and needs a range of at most 30 days for a channel", async () => {
609
+ const { api, read } = await setup();
610
+ const lifetime = await read(() =>
611
+ api.getUpdateFailures({ ...scope, releaseId: "release-b" }),
612
+ );
613
+ expect(lifetime.result).toEqual({
614
+ coverage: { kind: "complete", sinceMs: 0 },
615
+ measuredAtMs: T0 + 30 * DAY,
616
+ failedUpdates: 5,
617
+ failedInstallations: distinct([
618
+ "install-1",
619
+ "install-2",
620
+ "install-3",
621
+ "install-4",
622
+ ]),
623
+ downloads: 3,
624
+ patchDownloads: 2,
625
+ patchFallbacks: 1,
626
+ });
627
+ expect(lifetime.tables.toSorted()).toEqual([
628
+ "insights_overview_lifetime",
629
+ "insights_sketches_lifetime",
630
+ ]);
631
+ for (const invalid of [
632
+ scope,
633
+ { ...scope, platform: "web" },
634
+ { ...scope, releaseId: "release-b", timeRange: { start: 10, end: 10 } },
635
+ // Longer than the console's longest period, 30 days.
636
+ { ...scope, timeRange: { start: T0, end: T0 + 30 * DAY + 1 } },
637
+ ]) {
638
+ await expect(
639
+ api.getUpdateFailures(invalid as never),
640
+ ).rejects.toMatchObject({ code: "invalid-query" });
641
+ }
642
+ });
355
643
  });
356
644
 
357
645
  /**