@oneuptime/common 12.0.1 → 12.0.3

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 (385) hide show
  1. package/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.ts +14 -0
  2. package/Models/DatabaseModels/Host.ts +12 -4
  3. package/Models/DatabaseModels/Index.ts +2 -0
  4. package/Models/DatabaseModels/KubernetesContainer.ts +5 -4
  5. package/Models/DatabaseModels/KubernetesResource.ts +8 -7
  6. package/Models/DatabaseModels/LogDropFilter.ts +9 -20
  7. package/Models/DatabaseModels/PodmanResource.ts +6 -6
  8. package/Models/DatabaseModels/ServiceLevelObjective.ts +91 -0
  9. package/Models/DatabaseModels/StatusPage.ts +95 -0
  10. package/Models/DatabaseModels/StatusPageMonitorRule.ts +911 -0
  11. package/Models/DatabaseModels/StatusPageResource.ts +73 -0
  12. package/Models/DatabaseModels/TraceDropFilter.ts +9 -20
  13. package/Server/API/BaseAPI.ts +13 -2
  14. package/Server/API/DashboardAPI.ts +39 -0
  15. package/Server/API/TeamMemberAPI.ts +85 -0
  16. package/Server/EnvironmentConfig.ts +31 -1
  17. package/Server/Infrastructure/GlobalCache.ts +115 -0
  18. package/Server/Infrastructure/Postgres/DataSourceOptions.ts +16 -0
  19. package/Server/Infrastructure/Postgres/SchemaMigrations/1785900000000-AddSloMonitorLabelRule.ts +84 -0
  20. package/Server/Infrastructure/Postgres/SchemaMigrations/1785915638551-IncreaseHostIpAddressesLength.ts +40 -0
  21. package/Server/Infrastructure/Postgres/SchemaMigrations/1785930000000-WidenInventoryResourceNameColumns.ts +131 -0
  22. package/Server/Infrastructure/Postgres/SchemaMigrations/1785930709405-AddStatusPageReportPeriod.ts +25 -0
  23. package/Server/Infrastructure/Postgres/SchemaMigrations/1786005052769-AddStatusPageMonitorRule.ts +123 -0
  24. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +10 -0
  25. package/Server/Middleware/ProjectAuthorization.ts +66 -4
  26. package/Server/Services/AlertStateTimelineService.ts +20 -0
  27. package/Server/Services/BillingService.ts +600 -51
  28. package/Server/Services/CephClusterService.ts +27 -45
  29. package/Server/Services/CephResourceService.ts +43 -4
  30. package/Server/Services/CloudResourceService.ts +24 -42
  31. package/Server/Services/DatabaseService.ts +198 -6
  32. package/Server/Services/DockerHostService.ts +20 -38
  33. package/Server/Services/DockerResourceService.ts +62 -8
  34. package/Server/Services/DockerSwarmClusterService.ts +28 -46
  35. package/Server/Services/DockerSwarmResourceService.ts +53 -4
  36. package/Server/Services/HostService.ts +47 -57
  37. package/Server/Services/IncidentStateTimelineService.ts +20 -0
  38. package/Server/Services/Index.ts +2 -0
  39. package/Server/Services/IoTDeviceService.ts +1 -17
  40. package/Server/Services/IoTFleetService.ts +20 -38
  41. package/Server/Services/KubernetesClusterService.ts +18 -36
  42. package/Server/Services/KubernetesContainerService.ts +50 -9
  43. package/Server/Services/KubernetesResourceService.ts +53 -7
  44. package/Server/Services/LabelService.ts +153 -1
  45. package/Server/Services/MetricTypeService.ts +84 -0
  46. package/Server/Services/MonitorService.ts +146 -14
  47. package/Server/Services/MonitorStatusTimelineService.ts +29 -0
  48. package/Server/Services/MonitorTemplateService.ts +65 -0
  49. package/Server/Services/PodmanHostService.ts +20 -38
  50. package/Server/Services/PodmanResourceService.ts +62 -8
  51. package/Server/Services/ProjectService.ts +165 -35
  52. package/Server/Services/ProxmoxClusterService.ts +24 -42
  53. package/Server/Services/ProxmoxResourceService.ts +43 -4
  54. package/Server/Services/RumApplicationService.ts +20 -38
  55. package/Server/Services/ScheduledMaintenanceStateTimelineService.ts +21 -0
  56. package/Server/Services/ServerlessFunctionService.ts +25 -43
  57. package/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.ts +426 -0
  58. package/Server/Services/ServiceLevelObjectiveService.ts +39 -0
  59. package/Server/Services/ServiceService.ts +299 -73
  60. package/Server/Services/StatusPageMonitorRuleEngineService.ts +752 -0
  61. package/Server/Services/StatusPageMonitorRuleService.ts +471 -0
  62. package/Server/Services/StatusPageService.ts +95 -36
  63. package/Server/Services/StatusPageSubscriberNotificationTemplateService.ts +18 -1
  64. package/Server/Services/StatusPageSubscriberService.ts +3 -0
  65. package/Server/Services/TelemetryEntityRelationshipService.ts +1 -0
  66. package/Server/Services/TelemetryEntityService.ts +1 -0
  67. package/Server/Types/Database/JSONColumnQuery.ts +621 -0
  68. package/Server/Types/Database/Permissions/PublicPermission.ts +9 -2
  69. package/Server/Types/Database/QueryHelper.ts +17 -32
  70. package/Server/Utils/Database/ProjectScopedReferenceValidator.ts +170 -39
  71. package/Server/Utils/Database/TruncateColumnValue.ts +84 -0
  72. package/Server/Utils/Monitor/MonitorResource.ts +63 -36
  73. package/Server/Utils/Monitor/MonitorStatusTimeline.ts +36 -0
  74. package/Server/Utils/Monitor/MonitorStepsProjectValidator.ts +148 -127
  75. package/Server/Utils/Monitor/MonitorStepsReferenceExtractor.ts +434 -0
  76. package/Server/Utils/SingleFlight.ts +83 -0
  77. package/Server/Utils/StatusPage/MonitorRulePatternValidator.ts +46 -0
  78. package/Server/Utils/Telemetry/EntityRegistry.ts +107 -26
  79. package/Server/Utils/Telemetry/ResourceHeartbeat.ts +314 -0
  80. package/Server/Utils/Telemetry/Telemetry.ts +351 -122
  81. package/Tests/App/Dashboard/UsersTableGroupsByPerson.test.tsx +448 -0
  82. package/Tests/App/Dashboard/WidgetCatalog.test.ts +2 -0
  83. package/Tests/Models/DatabaseModels/ColumnTransformerInvariants.test.ts +306 -0
  84. package/Tests/Models/DatabaseModels/NumberColumnDeserialization.test.ts +333 -0
  85. package/Tests/Models/DatabaseModels/StatusPageMonitorRuleModel.test.ts +190 -0
  86. package/Tests/Server/API/BaseAPIApiKeyAuth.test.ts +704 -0
  87. package/Tests/Server/API/DashboardPublicResourceListAPI.test.ts +26 -0
  88. package/Tests/Server/API/TeamMemberRemoveUserFromProjectAPI.test.ts +303 -0
  89. package/Tests/Server/Infrastructure/GlobalCache.test.ts +226 -0
  90. package/Tests/Server/Infrastructure/PostgresLockTimeoutOptions.test.ts +181 -0
  91. package/Tests/Server/Middleware/MasterAdminAuthorization.test.ts +350 -0
  92. package/Tests/Server/Middleware/ProjectAuthorizationApiKeyHeader.test.ts +362 -0
  93. package/Tests/Server/Middleware/ProjectAuthorizationApiKeyMiddleware.test.ts +687 -0
  94. package/Tests/Server/Services/BillingService.test.ts +78 -3
  95. package/Tests/Server/Services/BillingServiceChangePlanCancel.test.ts +645 -0
  96. package/Tests/Server/Services/BillingServiceTrialPlanChange.test.ts +1005 -0
  97. package/Tests/Server/Services/DatabaseServiceUpdateColumnsIfUnlocked.test.ts +342 -0
  98. package/Tests/Server/Services/DropFilterSaveValidation.test.ts +245 -0
  99. package/Tests/Server/Services/HookFreeWriteLengthClamp.test.ts +503 -0
  100. package/Tests/Server/Services/HostIpAddressesColumnWidth.test.ts +198 -0
  101. package/Tests/Server/Services/HostServiceUpdateLastSeen.test.ts +658 -0
  102. package/Tests/Server/Services/InventoryResourceNameColumnWidth.test.ts +500 -0
  103. package/Tests/Server/Services/InventoryUpsertSchemaParity.test.ts +586 -0
  104. package/Tests/Server/Services/InventoryUpsertTruncationGuard.test.ts +1242 -0
  105. package/Tests/Server/Services/MetricTypeAttachServices.test.ts +222 -0
  106. package/Tests/Server/Services/MissingReferenceWriteGuard.test.ts +614 -0
  107. package/Tests/Server/Services/MonitorStatusTimelineFastPath.test.ts +23 -0
  108. package/Tests/Server/Services/ProjectServiceChangePlan.test.ts +650 -0
  109. package/Tests/Server/Services/ProjectServiceExtendTrial.test.ts +1 -0
  110. package/Tests/Server/Services/ResourceUpdateLastSeenLivenessFallback.test.ts +636 -0
  111. package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.test.ts +770 -0
  112. package/Tests/Server/Services/ServiceLevelObjectiveService.test.ts +130 -0
  113. package/Tests/Server/Services/ServiceUpdateLastSeenSplitGates.test.ts +705 -0
  114. package/Tests/Server/Services/SloMonitorLabelRuleHooks.test.ts +352 -0
  115. package/Tests/Server/Services/StatusPageMonitorRuleEngineService.test.ts +1719 -0
  116. package/Tests/Server/Services/StatusPageMonitorRuleHooks.test.ts +1212 -0
  117. package/Tests/Server/Services/StatusPageMonitorRuleMonitorCreateHook.test.ts +228 -0
  118. package/Tests/Server/Services/StatusPageSubscriberReport.test.ts +29 -13
  119. package/Tests/Server/TestingUtils/Services/BillingServiceHelper.ts +69 -8
  120. package/Tests/Server/Types/Database/JSONColumnQuery.test.ts +593 -0
  121. package/Tests/Server/Types/Database/Permissions/PublicPermission.test.ts +495 -0
  122. package/Tests/Server/Types/Database/QueryUtil.test.ts +113 -0
  123. package/Tests/Server/Utils/APIKey/AccessPermission.test.ts +267 -0
  124. package/Tests/Server/Utils/Database/ProjectScopedReferenceValidator.test.ts +260 -13
  125. package/Tests/Server/Utils/Database/TruncateColumnValue.test.ts +179 -0
  126. package/Tests/Server/Utils/EntityRegistryRowFence.test.ts +302 -0
  127. package/Tests/Server/Utils/MetricTypeIndexConvergence.test.ts +598 -0
  128. package/Tests/Server/Utils/Monitor/MonitorStatusTimelineUnusableStatus.test.ts +154 -0
  129. package/Tests/Server/Utils/Monitor/MonitorStepsProjectValidator.test.ts +638 -92
  130. package/Tests/Server/Utils/Monitor/MonitorStepsReferenceExtractor.test.ts +724 -0
  131. package/Tests/Server/Utils/SingleFlight.test.ts +221 -0
  132. package/Tests/Types/Dashboard/NetworkDashboardTemplate.test.ts +793 -0
  133. package/Tests/Types/Database/BigIntColumnTransformer.test.ts +147 -0
  134. package/Tests/Types/Database/DatabasePropertyTransformer.test.ts +140 -0
  135. package/Tests/Types/Database/NumericColumnValue.test.ts +127 -0
  136. package/Tests/Types/Decimal.test.ts +37 -0
  137. package/Tests/Types/Events/Recurring.test.ts +157 -0
  138. package/Tests/Types/Monitor/SnmpVersionParsing.test.ts +152 -0
  139. package/Tests/Types/Port.test.ts +38 -0
  140. package/Tests/UI/Components/LogsHistogram.test.tsx +276 -0
  141. package/Tests/UI/Components/ModelTable/ModelTableGroupsProjectUsers.test.tsx +375 -0
  142. package/Tests/UI/Components/ModelTable/useCustomFieldColumns.test.tsx +196 -0
  143. package/Tests/UI/Components/UseLiveLogsRefresh.test.tsx +489 -0
  144. package/Tests/UI/Components/UseLogsHistogram.test.tsx +500 -0
  145. package/Tests/UI/Utils/ProjectUsersModelAPI.test.ts +739 -0
  146. package/Tests/UI/Utils/TeamMembersByUser.test.ts +633 -0
  147. package/Tests/Utils/Dashboard/Components/DashboardComponentsUtil.test.ts +120 -0
  148. package/Tests/Utils/Dashboard/DashboardNetworkMapComponent.test.ts +327 -0
  149. package/Tests/Utils/StatusPage/ReportPeriod.test.ts +218 -0
  150. package/Tests/Utils/Telemetry/HostIpAddresses.test.ts +291 -0
  151. package/Types/CustomField/CustomFieldDefinition.ts +24 -0
  152. package/Types/Dashboard/DashboardComponentType.ts +1 -0
  153. package/Types/Dashboard/DashboardComponents/ComponentArgument.ts +1 -0
  154. package/Types/Dashboard/DashboardComponents/DashboardNetworkMapComponent.ts +40 -0
  155. package/Types/Dashboard/DashboardTemplates.ts +397 -0
  156. package/Types/Database/BigIntColumnTransformer.ts +74 -0
  157. package/Types/Database/DatabaseProperty.ts +26 -0
  158. package/Types/Database/NumericColumnValue.ts +128 -0
  159. package/Types/Date.ts +112 -0
  160. package/Types/Decimal.ts +16 -4
  161. package/Types/Events/Recurring.ts +99 -1
  162. package/Types/Permission.ts +48 -0
  163. package/Types/Port.ts +16 -4
  164. package/Types/StatusPage/StatusPageReport.ts +11 -0
  165. package/Types/StatusPage/StatusPageReportPeriodType.ts +21 -0
  166. package/UI/Components/LogsViewer/useLiveLogsRefresh.ts +86 -0
  167. package/UI/Components/LogsViewer/useLogsHistogram.ts +113 -0
  168. package/UI/Components/ModelTable/CustomFieldColumns.tsx +2 -6
  169. package/UI/Components/ModelTable/useCustomFieldColumns.ts +21 -2
  170. package/UI/Utils/ModelAPI/ProjectUsersModelAPI.ts +305 -0
  171. package/UI/Utils/TeamMembersByUser.ts +302 -0
  172. package/Utils/Dashboard/Components/DashboardNetworkMapComponent.ts +136 -0
  173. package/Utils/Dashboard/Components/Index.ts +7 -0
  174. package/Utils/StatusPage/ReportPeriod.ts +236 -0
  175. package/Utils/Telemetry/HostIpAddresses.ts +98 -0
  176. package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js +12 -0
  177. package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js.map +1 -1
  178. package/build/dist/Models/DatabaseModels/Host.js +12 -4
  179. package/build/dist/Models/DatabaseModels/Host.js.map +1 -1
  180. package/build/dist/Models/DatabaseModels/Index.js +2 -0
  181. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  182. package/build/dist/Models/DatabaseModels/KubernetesContainer.js +4 -4
  183. package/build/dist/Models/DatabaseModels/KubernetesContainer.js.map +1 -1
  184. package/build/dist/Models/DatabaseModels/KubernetesResource.js +7 -7
  185. package/build/dist/Models/DatabaseModels/KubernetesResource.js.map +1 -1
  186. package/build/dist/Models/DatabaseModels/LogDropFilter.js +9 -20
  187. package/build/dist/Models/DatabaseModels/LogDropFilter.js.map +1 -1
  188. package/build/dist/Models/DatabaseModels/PodmanResource.js +6 -6
  189. package/build/dist/Models/DatabaseModels/PodmanResource.js.map +1 -1
  190. package/build/dist/Models/DatabaseModels/ServiceLevelObjective.js +87 -0
  191. package/build/dist/Models/DatabaseModels/ServiceLevelObjective.js.map +1 -1
  192. package/build/dist/Models/DatabaseModels/StatusPage.js +98 -0
  193. package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
  194. package/build/dist/Models/DatabaseModels/StatusPageMonitorRule.js +925 -0
  195. package/build/dist/Models/DatabaseModels/StatusPageMonitorRule.js.map +1 -0
  196. package/build/dist/Models/DatabaseModels/StatusPageResource.js +72 -0
  197. package/build/dist/Models/DatabaseModels/StatusPageResource.js.map +1 -1
  198. package/build/dist/Models/DatabaseModels/TraceDropFilter.js +9 -20
  199. package/build/dist/Models/DatabaseModels/TraceDropFilter.js.map +1 -1
  200. package/build/dist/Server/API/BaseAPI.js +11 -1
  201. package/build/dist/Server/API/BaseAPI.js.map +1 -1
  202. package/build/dist/Server/API/DashboardAPI.js +39 -0
  203. package/build/dist/Server/API/DashboardAPI.js.map +1 -1
  204. package/build/dist/Server/API/TeamMemberAPI.js +57 -2
  205. package/build/dist/Server/API/TeamMemberAPI.js.map +1 -1
  206. package/build/dist/Server/EnvironmentConfig.js +27 -1
  207. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  208. package/build/dist/Server/Infrastructure/GlobalCache.js +91 -0
  209. package/build/dist/Server/Infrastructure/GlobalCache.js.map +1 -1
  210. package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js +5 -3
  211. package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js.map +1 -1
  212. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785900000000-AddSloMonitorLabelRule.js +45 -0
  213. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785900000000-AddSloMonitorLabelRule.js.map +1 -0
  214. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785915638551-IncreaseHostIpAddressesLength.js +31 -0
  215. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785915638551-IncreaseHostIpAddressesLength.js.map +1 -0
  216. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930000000-WidenInventoryResourceNameColumns.js +90 -0
  217. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930000000-WidenInventoryResourceNameColumns.js.map +1 -0
  218. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930709405-AddStatusPageReportPeriod.js +14 -0
  219. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930709405-AddStatusPageReportPeriod.js.map +1 -0
  220. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786005052769-AddStatusPageMonitorRule.js +48 -0
  221. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786005052769-AddStatusPageMonitorRule.js.map +1 -0
  222. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +10 -0
  223. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  224. package/build/dist/Server/Middleware/ProjectAuthorization.js +59 -4
  225. package/build/dist/Server/Middleware/ProjectAuthorization.js.map +1 -1
  226. package/build/dist/Server/Services/AlertStateTimelineService.js +19 -0
  227. package/build/dist/Server/Services/AlertStateTimelineService.js.map +1 -1
  228. package/build/dist/Server/Services/BillingService.js +466 -36
  229. package/build/dist/Server/Services/BillingService.js.map +1 -1
  230. package/build/dist/Server/Services/CephClusterService.js +26 -39
  231. package/build/dist/Server/Services/CephClusterService.js.map +1 -1
  232. package/build/dist/Server/Services/CephResourceService.js +28 -3
  233. package/build/dist/Server/Services/CephResourceService.js.map +1 -1
  234. package/build/dist/Server/Services/CloudResourceService.js +23 -36
  235. package/build/dist/Server/Services/CloudResourceService.js.map +1 -1
  236. package/build/dist/Server/Services/DatabaseService.js +155 -2
  237. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  238. package/build/dist/Server/Services/DockerHostService.js +19 -32
  239. package/build/dist/Server/Services/DockerHostService.js.map +1 -1
  240. package/build/dist/Server/Services/DockerResourceService.js +34 -4
  241. package/build/dist/Server/Services/DockerResourceService.js.map +1 -1
  242. package/build/dist/Server/Services/DockerSwarmClusterService.js +27 -40
  243. package/build/dist/Server/Services/DockerSwarmClusterService.js.map +1 -1
  244. package/build/dist/Server/Services/DockerSwarmResourceService.js +28 -3
  245. package/build/dist/Server/Services/DockerSwarmResourceService.js.map +1 -1
  246. package/build/dist/Server/Services/HostService.js +58 -64
  247. package/build/dist/Server/Services/HostService.js.map +1 -1
  248. package/build/dist/Server/Services/IncidentStateTimelineService.js +19 -0
  249. package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
  250. package/build/dist/Server/Services/Index.js +2 -0
  251. package/build/dist/Server/Services/Index.js.map +1 -1
  252. package/build/dist/Server/Services/IoTDeviceService.js +1 -8
  253. package/build/dist/Server/Services/IoTDeviceService.js.map +1 -1
  254. package/build/dist/Server/Services/IoTFleetService.js +19 -32
  255. package/build/dist/Server/Services/IoTFleetService.js.map +1 -1
  256. package/build/dist/Server/Services/KubernetesClusterService.js +17 -30
  257. package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
  258. package/build/dist/Server/Services/KubernetesContainerService.js +29 -3
  259. package/build/dist/Server/Services/KubernetesContainerService.js.map +1 -1
  260. package/build/dist/Server/Services/KubernetesResourceService.js +32 -4
  261. package/build/dist/Server/Services/KubernetesResourceService.js.map +1 -1
  262. package/build/dist/Server/Services/LabelService.js +127 -0
  263. package/build/dist/Server/Services/LabelService.js.map +1 -1
  264. package/build/dist/Server/Services/MetricTypeService.js +74 -0
  265. package/build/dist/Server/Services/MetricTypeService.js.map +1 -1
  266. package/build/dist/Server/Services/MonitorService.js +128 -9
  267. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  268. package/build/dist/Server/Services/MonitorStatusTimelineService.js +28 -0
  269. package/build/dist/Server/Services/MonitorStatusTimelineService.js.map +1 -1
  270. package/build/dist/Server/Services/MonitorTemplateService.js +60 -0
  271. package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
  272. package/build/dist/Server/Services/PodmanHostService.js +19 -32
  273. package/build/dist/Server/Services/PodmanHostService.js.map +1 -1
  274. package/build/dist/Server/Services/PodmanResourceService.js +34 -4
  275. package/build/dist/Server/Services/PodmanResourceService.js.map +1 -1
  276. package/build/dist/Server/Services/ProjectService.js +135 -21
  277. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  278. package/build/dist/Server/Services/ProxmoxClusterService.js +23 -36
  279. package/build/dist/Server/Services/ProxmoxClusterService.js.map +1 -1
  280. package/build/dist/Server/Services/ProxmoxResourceService.js +28 -3
  281. package/build/dist/Server/Services/ProxmoxResourceService.js.map +1 -1
  282. package/build/dist/Server/Services/RumApplicationService.js +19 -32
  283. package/build/dist/Server/Services/RumApplicationService.js.map +1 -1
  284. package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js +20 -0
  285. package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js.map +1 -1
  286. package/build/dist/Server/Services/ServerlessFunctionService.js +24 -37
  287. package/build/dist/Server/Services/ServerlessFunctionService.js.map +1 -1
  288. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js +347 -0
  289. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js.map +1 -0
  290. package/build/dist/Server/Services/ServiceLevelObjectiveService.js +33 -1
  291. package/build/dist/Server/Services/ServiceLevelObjectiveService.js.map +1 -1
  292. package/build/dist/Server/Services/ServiceService.js +236 -61
  293. package/build/dist/Server/Services/ServiceService.js.map +1 -1
  294. package/build/dist/Server/Services/StatusPageMonitorRuleEngineService.js +603 -0
  295. package/build/dist/Server/Services/StatusPageMonitorRuleEngineService.js.map +1 -0
  296. package/build/dist/Server/Services/StatusPageMonitorRuleService.js +401 -0
  297. package/build/dist/Server/Services/StatusPageMonitorRuleService.js.map +1 -0
  298. package/build/dist/Server/Services/StatusPageService.js +69 -46
  299. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  300. package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js +17 -1
  301. package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js.map +1 -1
  302. package/build/dist/Server/Services/StatusPageSubscriberService.js +3 -0
  303. package/build/dist/Server/Services/StatusPageSubscriberService.js.map +1 -1
  304. package/build/dist/Server/Services/TelemetryEntityRelationshipService.js +1 -0
  305. package/build/dist/Server/Services/TelemetryEntityRelationshipService.js.map +1 -1
  306. package/build/dist/Server/Services/TelemetryEntityService.js +1 -0
  307. package/build/dist/Server/Services/TelemetryEntityService.js.map +1 -1
  308. package/build/dist/Server/Types/Database/JSONColumnQuery.js +402 -0
  309. package/build/dist/Server/Types/Database/JSONColumnQuery.js.map +1 -0
  310. package/build/dist/Server/Types/Database/Permissions/PublicPermission.js +9 -2
  311. package/build/dist/Server/Types/Database/Permissions/PublicPermission.js.map +1 -1
  312. package/build/dist/Server/Types/Database/QueryHelper.js +17 -27
  313. package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
  314. package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js +105 -29
  315. package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js.map +1 -1
  316. package/build/dist/Server/Utils/Database/TruncateColumnValue.js +32 -0
  317. package/build/dist/Server/Utils/Database/TruncateColumnValue.js.map +1 -0
  318. package/build/dist/Server/Utils/Monitor/MonitorResource.js +56 -32
  319. package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
  320. package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js +30 -3
  321. package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js.map +1 -1
  322. package/build/dist/Server/Utils/Monitor/MonitorStepsProjectValidator.js +110 -98
  323. package/build/dist/Server/Utils/Monitor/MonitorStepsProjectValidator.js.map +1 -1
  324. package/build/dist/Server/Utils/Monitor/MonitorStepsReferenceExtractor.js +312 -0
  325. package/build/dist/Server/Utils/Monitor/MonitorStepsReferenceExtractor.js.map +1 -0
  326. package/build/dist/Server/Utils/SingleFlight.js +76 -0
  327. package/build/dist/Server/Utils/SingleFlight.js.map +1 -0
  328. package/build/dist/Server/Utils/StatusPage/MonitorRulePatternValidator.js +29 -0
  329. package/build/dist/Server/Utils/StatusPage/MonitorRulePatternValidator.js.map +1 -0
  330. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +78 -19
  331. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
  332. package/build/dist/Server/Utils/Telemetry/ResourceHeartbeat.js +160 -0
  333. package/build/dist/Server/Utils/Telemetry/ResourceHeartbeat.js.map +1 -0
  334. package/build/dist/Server/Utils/Telemetry/Telemetry.js +279 -104
  335. package/build/dist/Server/Utils/Telemetry/Telemetry.js.map +1 -1
  336. package/build/dist/Types/CustomField/CustomFieldDefinition.js +2 -0
  337. package/build/dist/Types/CustomField/CustomFieldDefinition.js.map +1 -0
  338. package/build/dist/Types/Dashboard/DashboardComponentType.js +1 -0
  339. package/build/dist/Types/Dashboard/DashboardComponentType.js.map +1 -1
  340. package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js +1 -0
  341. package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js.map +1 -1
  342. package/build/dist/Types/Dashboard/DashboardComponents/DashboardNetworkMapComponent.js +2 -0
  343. package/build/dist/Types/Dashboard/DashboardComponents/DashboardNetworkMapComponent.js.map +1 -0
  344. package/build/dist/Types/Dashboard/DashboardTemplates.js +368 -0
  345. package/build/dist/Types/Dashboard/DashboardTemplates.js.map +1 -1
  346. package/build/dist/Types/Database/BigIntColumnTransformer.js +66 -0
  347. package/build/dist/Types/Database/BigIntColumnTransformer.js.map +1 -0
  348. package/build/dist/Types/Database/DatabaseProperty.js +25 -0
  349. package/build/dist/Types/Database/DatabaseProperty.js.map +1 -1
  350. package/build/dist/Types/Database/NumericColumnValue.js +97 -0
  351. package/build/dist/Types/Database/NumericColumnValue.js.map +1 -0
  352. package/build/dist/Types/Date.js +68 -0
  353. package/build/dist/Types/Date.js.map +1 -1
  354. package/build/dist/Types/Decimal.js +14 -3
  355. package/build/dist/Types/Decimal.js.map +1 -1
  356. package/build/dist/Types/Events/Recurring.js +63 -0
  357. package/build/dist/Types/Events/Recurring.js.map +1 -1
  358. package/build/dist/Types/Permission.js +42 -0
  359. package/build/dist/Types/Permission.js.map +1 -1
  360. package/build/dist/Types/Port.js +15 -5
  361. package/build/dist/Types/Port.js.map +1 -1
  362. package/build/dist/Types/StatusPage/StatusPageReportPeriodType.js +22 -0
  363. package/build/dist/Types/StatusPage/StatusPageReportPeriodType.js.map +1 -0
  364. package/build/dist/UI/Components/LogsViewer/useLiveLogsRefresh.js +44 -0
  365. package/build/dist/UI/Components/LogsViewer/useLiveLogsRefresh.js.map +1 -0
  366. package/build/dist/UI/Components/LogsViewer/useLogsHistogram.js +75 -0
  367. package/build/dist/UI/Components/LogsViewer/useLogsHistogram.js.map +1 -0
  368. package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js.map +1 -1
  369. package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js +18 -2
  370. package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js.map +1 -1
  371. package/build/dist/UI/Utils/ModelAPI/ProjectUsersModelAPI.js +213 -0
  372. package/build/dist/UI/Utils/ModelAPI/ProjectUsersModelAPI.js.map +1 -0
  373. package/build/dist/UI/Utils/TeamMembersByUser.js +195 -0
  374. package/build/dist/UI/Utils/TeamMembersByUser.js.map +1 -0
  375. package/build/dist/Utils/Dashboard/Components/DashboardNetworkMapComponent.js +114 -0
  376. package/build/dist/Utils/Dashboard/Components/DashboardNetworkMapComponent.js.map +1 -0
  377. package/build/dist/Utils/Dashboard/Components/Index.js +4 -0
  378. package/build/dist/Utils/Dashboard/Components/Index.js.map +1 -1
  379. package/build/dist/Utils/StatusPage/ReportPeriod.js +131 -0
  380. package/build/dist/Utils/StatusPage/ReportPeriod.js.map +1 -0
  381. package/build/dist/Utils/Telemetry/HostIpAddresses.js +82 -0
  382. package/build/dist/Utils/Telemetry/HostIpAddresses.js.map +1 -0
  383. package/jest.config.json +2 -0
  384. package/package.json +1 -1
  385. package/tsconfig.json +12 -1
@@ -0,0 +1,636 @@
1
+ import CephClusterService from "../../../Server/Services/CephClusterService";
2
+ import CloudResourceService from "../../../Server/Services/CloudResourceService";
3
+ import DockerHostService from "../../../Server/Services/DockerHostService";
4
+ import DockerSwarmClusterService from "../../../Server/Services/DockerSwarmClusterService";
5
+ import GlobalCache from "../../../Server/Infrastructure/GlobalCache";
6
+ import HostService from "../../../Server/Services/HostService";
7
+ import IoTFleetService from "../../../Server/Services/IoTFleetService";
8
+ import KubernetesClusterService from "../../../Server/Services/KubernetesClusterService";
9
+ import PodmanHostService from "../../../Server/Services/PodmanHostService";
10
+ import ProxmoxClusterService from "../../../Server/Services/ProxmoxClusterService";
11
+ import RumApplicationService from "../../../Server/Services/RumApplicationService";
12
+ import ServerlessFunctionService from "../../../Server/Services/ServerlessFunctionService";
13
+ import SingleFlight from "../../../Server/Utils/SingleFlight";
14
+ import ObjectID from "../../../Types/ObjectID";
15
+ import {
16
+ afterEach,
17
+ beforeEach,
18
+ describe,
19
+ expect,
20
+ jest,
21
+ test,
22
+ } from "@jest/globals";
23
+
24
+ /*
25
+ * Every resource type's liveness heartbeat, pinned together.
26
+ *
27
+ * Eleven services write the same two liveness columns (lastSeenAt +
28
+ * otelCollectorStatus) alongside optional columns harvested from
29
+ * OpenTelemetry resource attributes, and each has its own markDisconnected*
30
+ * job that flips the resource to "disconnected" 15 minutes after lastSeenAt
31
+ * stops advancing. They now share one implementation — ResourceHeartbeat —
32
+ * because eleven hand-copied versions meant eleven copies of the same defect.
33
+ *
34
+ * WHAT THE DEFECT WAS. The throttle keyed on the row id but STORED a
35
+ * fingerprint of the incoming payload, skipping only on a match. That gives
36
+ * zero throttling whenever the fingerprinted payload changes as fast as the
37
+ * batches arrive, and on these paths it does:
38
+ *
39
+ * - Cluster and fleet snapshots hash per-scrape GAUGES into the
40
+ * fingerprint — runningTaskCount, osdUpCount, onlineDeviceCount,
41
+ * capacityUsedPercent. A float utilisation percentage changes on
42
+ * literally every scrape, so the fingerprint never repeated and the
43
+ * window never engaged.
44
+ * - Four of these paths (Proxmox, Ceph, IoTFleet and DockerSwarm snapshot
45
+ * writebacks) have NO upstream shouldRunMaintenance fence either, so that
46
+ * broken throttle was the only thing standing between the ingest firehose
47
+ * and a row shared by every node in the cluster.
48
+ *
49
+ * Result: one UPDATE per batch per shared row, which is the row-lock convoy
50
+ * that took production down.
51
+ *
52
+ * WHAT IS PINNED HERE, per service:
53
+ *
54
+ * 1. Liveness is gated on key PRESENCE, so no payload — however fast it
55
+ * churns — can force a second heartbeat inside a window. This is the
56
+ * assertion that would have caught the outage.
57
+ * 2. Enrichment is bounded to one write per window even under continuous
58
+ * change.
59
+ * 3. Liveness survives a failing enriched write (issue #3006), a Redis
60
+ * outage (fail open), and losing the row-lock race.
61
+ *
62
+ * Everything external is mocked — no Postgres, no Redis.
63
+ */
64
+
65
+ type WriteCall = { id: ObjectID; data: Record<string, unknown> };
66
+
67
+ type ServiceCase = {
68
+ /** Name of the service, used as the describe() title. */
69
+ name: string;
70
+ service: {
71
+ updateLastSeen(id: ObjectID, extra?: never): Promise<void>;
72
+ updateColumnsByIdIfUnlockedWithoutHooks(input: {
73
+ id: ObjectID;
74
+ data: unknown;
75
+ }): Promise<boolean>;
76
+ };
77
+ /** The Redis namespace this model's gates live under. */
78
+ namespace: string;
79
+ /** A metadata payload this service accepts, and a different one. */
80
+ extra: Record<string, unknown>;
81
+ otherExtra: Record<string, unknown>;
82
+ /** A string column whose value the collector supplies. */
83
+ oversizedExtra: Record<string, unknown>;
84
+ /**
85
+ * A payload whose values move on every scrape, if this service has one.
86
+ * These are what defeated the old throttle — see the header.
87
+ */
88
+ churningExtras?: Array<Record<string, unknown>>;
89
+ };
90
+
91
+ /** Longer than any bounded column on any of these models. */
92
+ const OVERSIZED: string = "x".repeat(5000);
93
+
94
+ /* eslint-disable @typescript-eslint/no-explicit-any */
95
+ const SERVICE_CASES: Array<ServiceCase> = [
96
+ {
97
+ name: "DockerHostService",
98
+ service: DockerHostService as any,
99
+ namespace: "docker-host-last-seen",
100
+ extra: { osType: "linux" },
101
+ otherExtra: { osType: "windows" },
102
+ oversizedExtra: { osVersion: OVERSIZED },
103
+ },
104
+ {
105
+ name: "PodmanHostService",
106
+ service: PodmanHostService as any,
107
+ namespace: "podman-host-last-seen",
108
+ extra: { osType: "linux" },
109
+ otherExtra: { osType: "windows" },
110
+ oversizedExtra: { osVersion: OVERSIZED },
111
+ },
112
+ {
113
+ name: "KubernetesClusterService",
114
+ service: KubernetesClusterService as any,
115
+ namespace: "k8s-cluster-last-seen",
116
+ extra: { agentVersion: "1.2.3" },
117
+ otherExtra: { agentVersion: "1.2.4" },
118
+ oversizedExtra: { agentVersion: OVERSIZED },
119
+ },
120
+ {
121
+ name: "ProxmoxClusterService",
122
+ service: ProxmoxClusterService as any,
123
+ namespace: "proxmox-cluster-last-seen",
124
+ extra: { pveVersion: "8.1.4" },
125
+ otherExtra: { pveVersion: "8.2.0" },
126
+ oversizedExtra: { pveVersion: OVERSIZED },
127
+ churningExtras: [
128
+ { pveVersion: "8.1.4", onlineNodeCount: 11, guestCount: 240 },
129
+ { pveVersion: "8.1.4", onlineNodeCount: 12, guestCount: 241 },
130
+ ],
131
+ },
132
+ {
133
+ name: "IoTFleetService",
134
+ service: IoTFleetService as any,
135
+ namespace: "iot-fleet-last-seen",
136
+ extra: { agentVersion: "1.2.3" },
137
+ otherExtra: { agentVersion: "1.2.4" },
138
+ oversizedExtra: { agentVersion: OVERSIZED },
139
+ churningExtras: [
140
+ { agentVersion: "1.2.3", onlineDeviceCount: 8014 },
141
+ { agentVersion: "1.2.3", onlineDeviceCount: 8015 },
142
+ ],
143
+ },
144
+ {
145
+ name: "DockerSwarmClusterService",
146
+ service: DockerSwarmClusterService as any,
147
+ namespace: "docker-swarm-cluster-last-seen",
148
+ extra: { dockerVersion: "25.0.3" },
149
+ otherExtra: { dockerVersion: "26.0.0" },
150
+ oversizedExtra: { swarmId: OVERSIZED },
151
+ churningExtras: [
152
+ { dockerVersion: "25.0.3", runningTaskCount: 512, taskCount: 530 },
153
+ { dockerVersion: "25.0.3", runningTaskCount: 513, taskCount: 530 },
154
+ ],
155
+ },
156
+ {
157
+ name: "CephClusterService",
158
+ service: CephClusterService as any,
159
+ namespace: "ceph-cluster-last-seen",
160
+ extra: { cephVersion: "18.2.2" },
161
+ otherExtra: { cephVersion: "19.0.0" },
162
+ oversizedExtra: { fsid: OVERSIZED },
163
+ /*
164
+ * The worst of them: a float utilisation percentage moves on every single
165
+ * scrape, so the old fingerprint was guaranteed never to repeat.
166
+ */
167
+ churningExtras: [
168
+ { cephVersion: "18.2.2", capacityUsedPercent: 61.4, osdUpCount: 48 },
169
+ { cephVersion: "18.2.2", capacityUsedPercent: 61.41, osdUpCount: 48 },
170
+ ],
171
+ },
172
+ {
173
+ name: "ServerlessFunctionService",
174
+ service: ServerlessFunctionService as any,
175
+ namespace: "serverless-function-last-seen",
176
+ extra: { runtimeName: "nodejs" },
177
+ otherExtra: { runtimeName: "python" },
178
+ oversizedExtra: { functionVersion: OVERSIZED },
179
+ },
180
+ {
181
+ name: "CloudResourceService",
182
+ service: CloudResourceService as any,
183
+ namespace: "cloud-resource-last-seen",
184
+ extra: { cloudProvider: "aws" },
185
+ otherExtra: { cloudProvider: "gcp" },
186
+ oversizedExtra: { cloudRegion: OVERSIZED },
187
+ },
188
+ {
189
+ name: "RumApplicationService",
190
+ service: RumApplicationService as any,
191
+ namespace: "rum-application-last-seen",
192
+ extra: { sdkLanguage: "webjs" },
193
+ otherExtra: { sdkLanguage: "swift" },
194
+ oversizedExtra: { clientType: OVERSIZED },
195
+ },
196
+ {
197
+ /*
198
+ * Host rides the same table so the eleven paths stay pinned together —
199
+ * its own richer regression coverage lives in
200
+ * HostServiceUpdateLastSeen.test.ts.
201
+ */
202
+ name: "HostService",
203
+ service: HostService as any,
204
+ namespace: "host-last-seen",
205
+ extra: { osType: "linux" },
206
+ otherExtra: { osType: "windows" },
207
+ oversizedExtra: { hostId: OVERSIZED },
208
+ churningExtras: [
209
+ { osType: "linux", processCount: 412, totalMemoryBytes: 17179869184 },
210
+ { osType: "linux", processCount: 413, totalMemoryBytes: 17179869184 },
211
+ ],
212
+ },
213
+ ];
214
+ /* eslint-enable @typescript-eslint/no-explicit-any */
215
+
216
+ describe.each(SERVICE_CASES)(
217
+ "$name.updateLastSeen",
218
+ ({
219
+ service,
220
+ namespace,
221
+ extra,
222
+ otherExtra,
223
+ oversizedExtra,
224
+ churningExtras,
225
+ }: ServiceCase) => {
226
+ let writes: Array<WriteCall>;
227
+ let cache: Map<string, string>;
228
+ let deletedCacheKeys: Array<string>;
229
+ /** Set false to simulate the row being held by another writer. */
230
+ let writeLands: boolean;
231
+
232
+ const RESOURCE_ID: ObjectID = ObjectID.generate();
233
+
234
+ /**
235
+ * A faithful in-memory Redis. SET NX succeeds only on an absent key and
236
+ * compare-and-claim only on a differing value — stubbing either to a
237
+ * constant would make this suite agree with a broken implementation.
238
+ */
239
+ function mockCache(): void {
240
+ jest
241
+ .spyOn(GlobalCache, "setStringIfNotExists")
242
+ .mockImplementation(async (ns: string, key: string, value: string) => {
243
+ const full: string = `${ns}:${key}`;
244
+ if (cache.has(full)) {
245
+ return false;
246
+ }
247
+ cache.set(full, value);
248
+ return true;
249
+ });
250
+
251
+ jest
252
+ .spyOn(GlobalCache, "setStringIfChanged")
253
+ .mockImplementation(async (ns: string, key: string, value: string) => {
254
+ const full: string = `${ns}:${key}`;
255
+ if (cache.get(full) === value) {
256
+ return false;
257
+ }
258
+ cache.set(full, value);
259
+ return true;
260
+ });
261
+
262
+ jest
263
+ .spyOn(GlobalCache, "deleteKey")
264
+ .mockImplementation(async (ns: string, key: string) => {
265
+ const full: string = `${ns}:${key}`;
266
+ deletedCacheKeys.push(full);
267
+ cache.delete(full);
268
+ });
269
+ }
270
+
271
+ /** Records every hook-free UPDATE instead of issuing it. */
272
+ function mockWritesSucceeding(): void {
273
+ jest
274
+ .spyOn(service, "updateColumnsByIdIfUnlockedWithoutHooks")
275
+ .mockImplementation(async (input: { id: ObjectID; data: unknown }) => {
276
+ writes.push({
277
+ id: input.id,
278
+ data: { ...(input.data as Record<string, unknown>) },
279
+ });
280
+ return writeLands;
281
+ });
282
+ }
283
+
284
+ /**
285
+ * Fails any write carrying more than the two liveness columns — the
286
+ * shape of a Postgres rejection caused by one bad optional column.
287
+ */
288
+ function mockEnrichedWriteFailing(): void {
289
+ jest
290
+ .spyOn(service, "updateColumnsByIdIfUnlockedWithoutHooks")
291
+ .mockImplementation(async (input: { id: ObjectID; data: unknown }) => {
292
+ const data: Record<string, unknown> = {
293
+ ...(input.data as Record<string, unknown>),
294
+ };
295
+ writes.push({ id: input.id, data: data });
296
+ if (Object.keys(data).length > 2) {
297
+ throw new Error("value too long for type character varying(100)");
298
+ }
299
+ return true;
300
+ });
301
+ }
302
+
303
+ function lastWrite(): Record<string, unknown> {
304
+ return writes[writes.length - 1]!.data;
305
+ }
306
+
307
+ /** Re-opens the liveness window without touching the enrichment gates. */
308
+ function expireLivenessWindow(): void {
309
+ cache.delete(`${namespace}:${RESOURCE_ID.toString()}`);
310
+ SingleFlight.clear();
311
+ }
312
+
313
+ beforeEach(() => {
314
+ writes = [];
315
+ cache = new Map<string, string>();
316
+ deletedCacheKeys = [];
317
+ writeLands = true;
318
+ SingleFlight.clear();
319
+ mockCache();
320
+ });
321
+
322
+ afterEach(() => {
323
+ jest.restoreAllMocks();
324
+ SingleFlight.clear();
325
+ });
326
+
327
+ describe("liveness columns", () => {
328
+ beforeEach(() => {
329
+ mockWritesSucceeding();
330
+ });
331
+
332
+ test("always writes lastSeenAt and marks the collector connected", async () => {
333
+ await service.updateLastSeen(RESOURCE_ID);
334
+
335
+ expect(writes).toHaveLength(1);
336
+ expect(lastWrite()["otelCollectorStatus"]).toBe("connected");
337
+ expect(lastWrite()["lastSeenAt"]).toBeInstanceOf(Date);
338
+ });
339
+
340
+ test("writes to the resource it was given", async () => {
341
+ await service.updateLastSeen(RESOURCE_ID);
342
+
343
+ expect(writes[0]!.id.toString()).toBe(RESOURCE_ID.toString());
344
+ });
345
+
346
+ test("skips the write when the same metadata was written recently", async () => {
347
+ await service.updateLastSeen(RESOURCE_ID, extra as never);
348
+ SingleFlight.clear();
349
+ await service.updateLastSeen(RESOURCE_ID, extra as never);
350
+
351
+ expect(writes).toHaveLength(1);
352
+ });
353
+
354
+ test("passes collector metadata through to the write path", async () => {
355
+ await service.updateLastSeen(RESOURCE_ID, extra as never);
356
+
357
+ for (const [column, value] of Object.entries(extra)) {
358
+ expect(lastWrite()[column]).toBe(value);
359
+ }
360
+ });
361
+
362
+ /*
363
+ * A changed attribute still lands — but in the next window, not on the
364
+ * very next batch. That is a deliberate narrowing of the old contract:
365
+ * change detection cannot tell a real update apart from a gauge that
366
+ * moves every scrape, both present as "it changed", and only one of
367
+ * those is bounded. Bounding both is what removes the convoy. The delay
368
+ * is at most one throttle window and `lastSeenAt` is unaffected, since
369
+ * it rides the separate liveness gate.
370
+ */
371
+ test("a metadata change lands in the next window", async () => {
372
+ await service.updateLastSeen(RESOURCE_ID, extra as never);
373
+ expireLivenessWindow();
374
+ cache.delete(`${namespace}-write-window:${RESOURCE_ID.toString()}`);
375
+
376
+ await service.updateLastSeen(RESOURCE_ID, otherExtra as never);
377
+
378
+ expect(writes).toHaveLength(2);
379
+ for (const [column, value] of Object.entries(otherExtra)) {
380
+ expect(lastWrite()[column]).toBe(value);
381
+ }
382
+ });
383
+
384
+ /*
385
+ * THE OUTAGE, reproduced per service. Continuously-changing metadata
386
+ * must not be able to buy more than one write per window. Under the old
387
+ * throttle each of these calls issued its own UPDATE against a row
388
+ * shared by every node reporting into it.
389
+ */
390
+ test("continuously changing metadata cannot force a write per batch", async () => {
391
+ for (let batch: number = 0; batch < 20; batch++) {
392
+ SingleFlight.clear();
393
+ await service.updateLastSeen(RESOURCE_ID, {
394
+ ...extra,
395
+ ...(batch % 2 === 0 ? otherExtra : {}),
396
+ } as never);
397
+ }
398
+
399
+ expect(writes.length).toBeLessThanOrEqual(2);
400
+ });
401
+
402
+ if (churningExtras) {
403
+ /*
404
+ * The concrete production shape for this service: a payload whose
405
+ * gauges move on every scrape (task counts, online device counts,
406
+ * capacity percentages). These are hashed into the fingerprint, so
407
+ * the old throttle never engaged for exactly the resources producing
408
+ * the most batches.
409
+ */
410
+ test("per-scrape gauges cannot defeat the throttle", async () => {
411
+ for (let scrape: number = 0; scrape < 20; scrape++) {
412
+ SingleFlight.clear();
413
+ await service.updateLastSeen(
414
+ RESOURCE_ID,
415
+ churningExtras[scrape % churningExtras.length] as never,
416
+ );
417
+ }
418
+
419
+ expect(writes.length).toBeLessThanOrEqual(2);
420
+ });
421
+
422
+ test("the gauges are still written when a write happens", async () => {
423
+ await service.updateLastSeen(RESOURCE_ID, churningExtras[0] as never);
424
+
425
+ for (const [column, value] of Object.entries(churningExtras[0]!)) {
426
+ expect(lastWrite()[column]).toBe(value);
427
+ }
428
+ });
429
+ }
430
+ });
431
+
432
+ describe("fallback when the enriched write fails", () => {
433
+ beforeEach(() => {
434
+ mockEnrichedWriteFailing();
435
+ });
436
+
437
+ test("does not throw — ingest keeps going", async () => {
438
+ await expect(
439
+ service.updateLastSeen(RESOURCE_ID, oversizedExtra as never),
440
+ ).resolves.toBeUndefined();
441
+ });
442
+
443
+ test("retries with liveness only, so lastSeenAt still advances", async () => {
444
+ await service.updateLastSeen(RESOURCE_ID, oversizedExtra as never);
445
+
446
+ expect(writes).toHaveLength(2);
447
+ expect(Object.keys(lastWrite()).sort()).toEqual([
448
+ "lastSeenAt",
449
+ "otelCollectorStatus",
450
+ ]);
451
+ expect(lastWrite()["otelCollectorStatus"]).toBe("connected");
452
+ expect(lastWrite()["lastSeenAt"]).toBeInstanceOf(Date);
453
+ });
454
+
455
+ test("the retry targets the same resource", async () => {
456
+ await service.updateLastSeen(RESOURCE_ID, oversizedExtra as never);
457
+
458
+ expect(writes[1]!.id.toString()).toBe(RESOURCE_ID.toString());
459
+ });
460
+
461
+ /*
462
+ * The enrichment gates were claimed before the write that failed, so
463
+ * without releasing them the next batch would short-circuit and the
464
+ * retry would never happen.
465
+ */
466
+ test("re-opens the enrichment gates so the next batch retries", async () => {
467
+ await service.updateLastSeen(RESOURCE_ID, oversizedExtra as never);
468
+
469
+ expect(deletedCacheKeys).toContain(
470
+ `${namespace}-fingerprint:${RESOURCE_ID.toString()}`,
471
+ );
472
+ expect(deletedCacheKeys).toContain(
473
+ `${namespace}-write-window:${RESOURCE_ID.toString()}`,
474
+ );
475
+ });
476
+
477
+ test("the next batch retries rather than short-circuiting", async () => {
478
+ await service.updateLastSeen(RESOURCE_ID, oversizedExtra as never);
479
+ writes = [];
480
+ expireLivenessWindow();
481
+
482
+ await service.updateLastSeen(RESOURCE_ID, oversizedExtra as never);
483
+
484
+ // Enriched attempt + liveness fallback, not a silent no-op.
485
+ expect(writes).toHaveLength(2);
486
+ });
487
+
488
+ test("a failing cache delete does not stop the liveness retry", async () => {
489
+ jest
490
+ .spyOn(GlobalCache, "deleteKey")
491
+ .mockRejectedValue(new Error("redis down"));
492
+
493
+ await service.updateLastSeen(RESOURCE_ID, oversizedExtra as never);
494
+
495
+ expect(Object.keys(lastWrite()).sort()).toEqual([
496
+ "lastSeenAt",
497
+ "otelCollectorStatus",
498
+ ]);
499
+ });
500
+
501
+ test("surfaces a genuinely broken database instead of hiding it", async () => {
502
+ // Both attempts fail — the caller must find out.
503
+ jest
504
+ .spyOn(service, "updateColumnsByIdIfUnlockedWithoutHooks")
505
+ .mockRejectedValue(new Error("connection terminated"));
506
+
507
+ await expect(
508
+ service.updateLastSeen(RESOURCE_ID, extra as never),
509
+ ).rejects.toThrow("connection terminated");
510
+ });
511
+ });
512
+
513
+ describe("cache outages fail open", () => {
514
+ beforeEach(() => {
515
+ mockWritesSucceeding();
516
+ });
517
+
518
+ /*
519
+ * Never mark a live resource disconnected because the cache went down.
520
+ * Affordable only because the write cannot queue — see the SKIP LOCKED
521
+ * note on the write primitive.
522
+ */
523
+ test("a liveness gate failure still writes", async () => {
524
+ jest
525
+ .spyOn(GlobalCache, "setStringIfNotExists")
526
+ .mockRejectedValue(new Error("redis down"));
527
+
528
+ await service.updateLastSeen(RESOURCE_ID, extra as never);
529
+
530
+ expect(writes).toHaveLength(1);
531
+ expect(lastWrite()["lastSeenAt"]).toBeInstanceOf(Date);
532
+ });
533
+
534
+ test("keeps writing liveness for as long as the cache is down", async () => {
535
+ jest
536
+ .spyOn(GlobalCache, "setStringIfNotExists")
537
+ .mockRejectedValue(new Error("redis down"));
538
+
539
+ await service.updateLastSeen(RESOURCE_ID, extra as never);
540
+ SingleFlight.clear();
541
+ await service.updateLastSeen(RESOURCE_ID, extra as never);
542
+
543
+ expect(writes).toHaveLength(2);
544
+ });
545
+
546
+ /*
547
+ * Enrichment gets the OPPOSITE treatment: opening that gate during an
548
+ * outage would restore the per-batch write storm, and nothing is
549
+ * mis-reported by writing a descriptive column a little later.
550
+ */
551
+ test("a change-detection failure suppresses enrichment, not liveness", async () => {
552
+ jest
553
+ .spyOn(GlobalCache, "setStringIfChanged")
554
+ .mockRejectedValue(new Error("redis down"));
555
+
556
+ await service.updateLastSeen(RESOURCE_ID, extra as never);
557
+
558
+ expect(writes).toHaveLength(1);
559
+ expect(Object.keys(lastWrite()).sort()).toEqual([
560
+ "lastSeenAt",
561
+ "otelCollectorStatus",
562
+ ]);
563
+ });
564
+ });
565
+
566
+ describe("losing the row-lock race", () => {
567
+ beforeEach(() => {
568
+ mockWritesSucceeding();
569
+ writeLands = false;
570
+ });
571
+
572
+ test("uses the non-blocking write primitive", async () => {
573
+ await service.updateLastSeen(RESOURCE_ID);
574
+
575
+ expect(
576
+ service.updateColumnsByIdIfUnlockedWithoutHooks,
577
+ ).toHaveBeenCalled();
578
+ });
579
+
580
+ /*
581
+ * A skipped liveness bump is a non-event — the writer holding the lock
582
+ * is storing the same timestamp. Retrying it would generate pointless
583
+ * statements against a row that is demonstrably being written.
584
+ */
585
+ test("a skipped liveness-only write is dropped silently", async () => {
586
+ await service.updateLastSeen(RESOURCE_ID);
587
+
588
+ expect(deletedCacheKeys).toHaveLength(0);
589
+ });
590
+
591
+ /*
592
+ * Enrichment is different: the lock holder may be writing an OLDER
593
+ * payload, and the gates this call claimed would suppress the newer one
594
+ * for the rest of the window.
595
+ */
596
+ test("a skipped enrichment write re-opens the gates", async () => {
597
+ await service.updateLastSeen(RESOURCE_ID, extra as never);
598
+
599
+ expect(deletedCacheKeys).toContain(
600
+ `${namespace}-fingerprint:${RESOURCE_ID.toString()}`,
601
+ );
602
+ });
603
+ });
604
+ },
605
+ );
606
+
607
+ describe("liveness fallback coverage", () => {
608
+ test("covers every service that writes liveness on the hook-free path", () => {
609
+ /*
610
+ * If a twelfth resource type grows an updateLastSeen, it belongs in
611
+ * SERVICE_CASES — that is the whole point of this suite. Bumping this
612
+ * number without adding the case defeats it.
613
+ */
614
+ expect(SERVICE_CASES).toHaveLength(11);
615
+ });
616
+
617
+ test("no service is listed twice", () => {
618
+ const names: Array<string> = SERVICE_CASES.map((c: ServiceCase) => {
619
+ return c.name;
620
+ });
621
+
622
+ expect(new Set(names).size).toBe(names.length);
623
+ });
624
+
625
+ /*
626
+ * The namespace in each case must match the one the service actually uses,
627
+ * or the gate assertions above silently check keys nobody writes.
628
+ */
629
+ test("no namespace is shared between services", () => {
630
+ const namespaces: Array<string> = SERVICE_CASES.map((c: ServiceCase) => {
631
+ return c.namespace;
632
+ });
633
+
634
+ expect(new Set(namespaces).size).toBe(namespaces.length);
635
+ });
636
+ });