@oneuptime/common 12.0.2 → 12.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (384) hide show
  1. package/Models/DatabaseModels/CephCluster.ts +14 -0
  2. package/Models/DatabaseModels/CodeRepository.ts +14 -6
  3. package/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.ts +14 -0
  4. package/Models/DatabaseModels/DockerHost.ts +14 -0
  5. package/Models/DatabaseModels/GlobalOidcProject.ts +18 -0
  6. package/Models/DatabaseModels/GlobalSsoProject.ts +18 -0
  7. package/Models/DatabaseModels/Index.ts +2 -0
  8. package/Models/DatabaseModels/IoTFleet.ts +14 -0
  9. package/Models/DatabaseModels/KubernetesCluster.ts +14 -0
  10. package/Models/DatabaseModels/LogDropFilter.ts +9 -20
  11. package/Models/DatabaseModels/NetworkDevice.ts +14 -0
  12. package/Models/DatabaseModels/NetworkInterface.ts +18 -0
  13. package/Models/DatabaseModels/Project.ts +16 -2
  14. package/Models/DatabaseModels/ProxmoxCluster.ts +14 -0
  15. package/Models/DatabaseModels/Service.ts +30 -0
  16. package/Models/DatabaseModels/StatusPageMonitorRule.ts +911 -0
  17. package/Models/DatabaseModels/StatusPagePrivateUser.ts +30 -0
  18. package/Models/DatabaseModels/StatusPageResource.ts +73 -0
  19. package/Models/DatabaseModels/TraceDropFilter.ts +9 -20
  20. package/Models/DatabaseModels/User.ts +30 -0
  21. package/Server/API/AIAgentDataAPI.ts +31 -0
  22. package/Server/API/BaseAPI.ts +13 -2
  23. package/Server/API/DashboardAPI.ts +458 -12
  24. package/Server/API/GitHubAPI.ts +119 -283
  25. package/Server/API/UserAPI.ts +263 -1
  26. package/Server/EnvironmentConfig.ts +38 -4
  27. package/Server/Infrastructure/GlobalCache.ts +152 -0
  28. package/Server/Infrastructure/Postgres/DataSourceOptions.ts +16 -0
  29. package/Server/Infrastructure/Postgres/SchemaMigrations/1786005052769-AddStatusPageMonitorRule.ts +123 -0
  30. package/Server/Infrastructure/Postgres/SchemaMigrations/1786018109307-AddPerUserPasswordSalt.ts +21 -0
  31. package/Server/Infrastructure/Postgres/SchemaMigrations/1786023262402-WidenHashedStringColumnsForScrypt.ts +68 -0
  32. package/Server/Infrastructure/Postgres/SchemaMigrations/1786100000000-RestoreServiceLowerNameIndex.ts +100 -0
  33. package/Server/Infrastructure/Postgres/SchemaMigrations/1786200000000-RestoreDroppedUniqueIndexes.ts +214 -0
  34. package/Server/Infrastructure/Postgres/SchemaMigrations/1786300000000-QuarantineUnboundGitHubInstallations.ts +69 -0
  35. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +12 -0
  36. package/Server/Services/AlertStateTimelineService.ts +20 -0
  37. package/Server/Services/AnalyticsDatabaseService.ts +72 -1
  38. package/Server/Services/CephClusterService.ts +26 -86
  39. package/Server/Services/CloudResourceService.ts +23 -83
  40. package/Server/Services/CodeRepositoryService.ts +105 -2
  41. package/Server/Services/DatabaseService.ts +296 -2
  42. package/Server/Services/DockerHostService.ts +19 -79
  43. package/Server/Services/DockerSwarmClusterService.ts +27 -88
  44. package/Server/Services/HostService.ts +46 -99
  45. package/Server/Services/IncidentStateTimelineService.ts +20 -0
  46. package/Server/Services/Index.ts +2 -0
  47. package/Server/Services/IoTFleetService.ts +19 -79
  48. package/Server/Services/KubernetesClusterService.ts +17 -78
  49. package/Server/Services/LabelService.ts +57 -10
  50. package/Server/Services/MetricTypeService.ts +84 -0
  51. package/Server/Services/MonitorService.ts +89 -14
  52. package/Server/Services/MonitorStatusTimelineService.ts +29 -0
  53. package/Server/Services/MonitorTemplateService.ts +65 -0
  54. package/Server/Services/OpenTelemetryIngestService.ts +230 -26
  55. package/Server/Services/PodmanHostService.ts +19 -79
  56. package/Server/Services/ProxmoxClusterService.ts +23 -83
  57. package/Server/Services/RumApplicationService.ts +19 -80
  58. package/Server/Services/ScheduledMaintenanceStateTimelineService.ts +21 -0
  59. package/Server/Services/ServerlessFunctionService.ts +24 -85
  60. package/Server/Services/ServiceService.ts +299 -73
  61. package/Server/Services/StatusPageMonitorRuleEngineService.ts +752 -0
  62. package/Server/Services/StatusPageMonitorRuleService.ts +471 -0
  63. package/Server/Services/TelemetryEntityRelationshipService.ts +1 -0
  64. package/Server/Services/TelemetryEntityService.ts +1 -0
  65. package/Server/Services/UserService.ts +184 -0
  66. package/Server/Utils/AI/Toolbox/CodeTools.ts +45 -4
  67. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +13 -0
  68. package/Server/Utils/CodeRepository/GitHub/GitHub.ts +157 -4
  69. package/Server/Utils/CodeRepository/GitHub/GitHubInstallationBinding.ts +130 -0
  70. package/Server/Utils/Database/ProjectScopedReferenceValidator.ts +170 -39
  71. package/Server/Utils/Monitor/MonitorResource.ts +63 -36
  72. package/Server/Utils/Monitor/MonitorStatusTimeline.ts +36 -0
  73. package/Server/Utils/Monitor/MonitorStepsProjectValidator.ts +148 -127
  74. package/Server/Utils/Monitor/MonitorStepsReferenceExtractor.ts +434 -0
  75. package/Server/Utils/PasswordHash.ts +306 -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/OverviewCustomFields.test.tsx +333 -0
  82. package/Tests/App/Dashboard/UserCustomFields.test.tsx +392 -0
  83. package/Tests/App/Dashboard/WidgetCatalog.test.ts +2 -0
  84. package/Tests/Models/DatabaseModels/ColumnTransformerInvariants.test.ts +306 -0
  85. package/Tests/Models/DatabaseModels/NumberColumnDeserialization.test.ts +333 -0
  86. package/Tests/Models/DatabaseModels/StatusPageMonitorRuleModel.test.ts +190 -0
  87. package/Tests/Server/API/AIAgentDataRepositoryToken.test.ts +381 -0
  88. package/Tests/Server/API/DashboardPublicAttributeValuesAPI.test.ts +943 -0
  89. package/Tests/Server/API/DashboardPublicMetricsAggregateAPI.test.ts +1296 -0
  90. package/Tests/Server/API/DashboardPublicResourceListAPI.test.ts +26 -0
  91. package/Tests/Server/API/DashboardPublicTemplatePayloads.test.ts +598 -0
  92. package/Tests/Server/API/GitHubAppInstallationBindingAPI.test.ts +539 -0
  93. package/Tests/Server/API/Helpers.ts +6 -1
  94. package/Tests/Server/API/UserProjectsAPI.test.ts +852 -0
  95. package/Tests/Server/Infrastructure/GlobalCache.test.ts +354 -0
  96. package/Tests/Server/Infrastructure/PostgresLockTimeoutOptions.test.ts +181 -0
  97. package/Tests/Server/Infrastructure/SemaphoreMutex.test.ts +215 -0
  98. package/Tests/Server/Services/AddPerUserPasswordSaltMigration.test.ts +178 -0
  99. package/Tests/Server/Services/AnalyticsDatabasePaginationStability.test.ts +582 -0
  100. package/Tests/Server/Services/AnalyticsDatabaseService.test.ts +5 -0
  101. package/Tests/Server/Services/CodeRepositoryInstallationBinding.test.ts +305 -0
  102. package/Tests/Server/Services/CodeRepositoryResolutionBinding.test.ts +147 -0
  103. package/Tests/Server/Services/DatabaseServicePerUserPasswordSalt.test.ts +870 -0
  104. package/Tests/Server/Services/DatabaseServiceUpdateColumnsIfUnlocked.test.ts +342 -0
  105. package/Tests/Server/Services/DropFilterSaveValidation.test.ts +245 -0
  106. package/Tests/Server/Services/HostServiceUpdateLastSeen.test.ts +163 -15
  107. package/Tests/Server/Services/MetricTypeAttachServices.test.ts +222 -0
  108. package/Tests/Server/Services/MissingReferenceWriteGuard.test.ts +614 -0
  109. package/Tests/Server/Services/MonitorStatusTimelineFastPath.test.ts +23 -0
  110. package/Tests/Server/Services/OpenTelemetryServiceResolutionCache.test.ts +568 -0
  111. package/Tests/Server/Services/ResourceUpdateLastSeenLivenessFallback.test.ts +305 -56
  112. package/Tests/Server/Services/RestoreDroppedUniqueIndexesMigration.test.ts +386 -0
  113. package/Tests/Server/Services/RestoreServiceLowerNameIndexMigration.test.ts +240 -0
  114. package/Tests/Server/Services/ServiceUpdateLastSeenSplitGates.test.ts +705 -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/UserServiceFirstMasterAdminElection.test.ts +885 -0
  119. package/Tests/Server/Services/WidenHashedStringColumnsForScryptMigration.test.ts +220 -0
  120. package/Tests/Server/Utils/AI/CodeTools.test.ts +47 -1
  121. package/Tests/Server/Utils/AI/CodeWriteTools.test.ts +10 -0
  122. package/Tests/Server/Utils/APIKey/AccessPermission.test.ts +267 -0
  123. package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +120 -0
  124. package/Tests/Server/Utils/CodeRepository/GitHubInstallationBinding.test.ts +292 -0
  125. package/Tests/Server/Utils/Database/ProjectScopedReferenceValidator.test.ts +260 -13
  126. package/Tests/Server/Utils/EntityRegistryRowFence.test.ts +302 -0
  127. package/Tests/Server/Utils/GitHubInstallationOwnershipVerification.test.ts +340 -0
  128. package/Tests/Server/Utils/GitHubWebhookAndTreeCacheIsolation.test.ts +250 -0
  129. package/Tests/Server/Utils/MetricTypeIndexConvergence.test.ts +598 -0
  130. package/Tests/Server/Utils/Monitor/MonitorStatusTimelineUnusableStatus.test.ts +154 -0
  131. package/Tests/Server/Utils/Monitor/MonitorStepsProjectValidator.test.ts +638 -92
  132. package/Tests/Server/Utils/Monitor/MonitorStepsReferenceExtractor.test.ts +724 -0
  133. package/Tests/Server/Utils/PasswordHash.test.ts +555 -0
  134. package/Tests/Server/Utils/SingleFlight.test.ts +221 -0
  135. package/Tests/Types/Dashboard/NetworkDashboardTemplate.test.ts +793 -0
  136. package/Tests/Types/Database/BigIntColumnTransformer.test.ts +147 -0
  137. package/Tests/Types/Database/ColumnLength.test.ts +7 -1
  138. package/Tests/Types/Database/DatabasePropertyTransformer.test.ts +140 -0
  139. package/Tests/Types/Database/NumericColumnValue.test.ts +127 -0
  140. package/Tests/Types/Decimal.test.ts +37 -0
  141. package/Tests/Types/HashedStringPerUserSalt.test.ts +476 -0
  142. package/Tests/Types/Monitor/DockerAlertTemplates.test.ts +293 -0
  143. package/Tests/Types/Monitor/HostAlertTemplates.test.ts +249 -0
  144. package/Tests/Types/Monitor/IotAlertTemplates.test.ts +320 -0
  145. package/Tests/Types/Monitor/PodmanAlertTemplates.test.ts +271 -0
  146. package/Tests/Types/Monitor/SnmpVersionParsing.test.ts +152 -0
  147. package/Tests/Types/Port.test.ts +38 -0
  148. package/Tests/UI/Components/CustomFields/CustomFieldsDetail.test.tsx +696 -0
  149. package/Tests/UI/Components/LogsHistogram.test.tsx +276 -0
  150. package/Tests/UI/Components/UseLiveLogsRefresh.test.tsx +489 -0
  151. package/Tests/UI/Components/UseLogsHistogram.test.tsx +500 -0
  152. package/Tests/UI/Utils/UserProjectsModelAPI.test.ts +714 -0
  153. package/Tests/Utils/Dashboard/Components/DashboardComponentsUtil.test.ts +120 -0
  154. package/Tests/Utils/Dashboard/DashboardNetworkMapComponent.test.ts +327 -0
  155. package/Tests/Utils/TeamMembersByProject.test.ts +655 -0
  156. package/Types/Dashboard/DashboardComponentType.ts +1 -0
  157. package/Types/Dashboard/DashboardComponents/ComponentArgument.ts +1 -0
  158. package/Types/Dashboard/DashboardComponents/DashboardNetworkMapComponent.ts +40 -0
  159. package/Types/Dashboard/DashboardTemplates.ts +397 -0
  160. package/Types/Database/BigIntColumnTransformer.ts +74 -0
  161. package/Types/Database/ColumnLength.ts +9 -1
  162. package/Types/Database/DatabaseProperty.ts +26 -0
  163. package/Types/Database/NumericColumnValue.ts +128 -0
  164. package/Types/Database/TableColumn.ts +11 -0
  165. package/Types/Database/UnsynchronizedIndex.ts +48 -0
  166. package/Types/Decimal.ts +16 -4
  167. package/Types/HashedString.ts +139 -4
  168. package/Types/Permission.ts +48 -0
  169. package/Types/Port.ts +16 -4
  170. package/UI/Components/CustomFields/CustomFieldsDetail.tsx +85 -11
  171. package/UI/Components/LogsViewer/useLiveLogsRefresh.ts +86 -0
  172. package/UI/Components/LogsViewer/useLogsHistogram.ts +113 -0
  173. package/UI/Components/Navbar/NavBar.tsx +12 -2
  174. package/UI/Components/Navbar/NavBarMenuModal.tsx +19 -7
  175. package/UI/Utils/ModelAPI/UserProjectsModelAPI.ts +257 -0
  176. package/Utils/Dashboard/Components/DashboardNetworkMapComponent.ts +136 -0
  177. package/Utils/Dashboard/Components/Index.ts +7 -0
  178. package/Utils/TeamMembersByProject.ts +237 -0
  179. package/build/dist/Models/DatabaseModels/CephCluster.js +16 -1
  180. package/build/dist/Models/DatabaseModels/CephCluster.js.map +1 -1
  181. package/build/dist/Models/DatabaseModels/CodeRepository.js +14 -6
  182. package/build/dist/Models/DatabaseModels/CodeRepository.js.map +1 -1
  183. package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js +12 -0
  184. package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js.map +1 -1
  185. package/build/dist/Models/DatabaseModels/DockerHost.js +16 -1
  186. package/build/dist/Models/DatabaseModels/DockerHost.js.map +1 -1
  187. package/build/dist/Models/DatabaseModels/GlobalOidcProject.js +16 -1
  188. package/build/dist/Models/DatabaseModels/GlobalOidcProject.js.map +1 -1
  189. package/build/dist/Models/DatabaseModels/GlobalSsoProject.js +16 -1
  190. package/build/dist/Models/DatabaseModels/GlobalSsoProject.js.map +1 -1
  191. package/build/dist/Models/DatabaseModels/Index.js +2 -0
  192. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  193. package/build/dist/Models/DatabaseModels/IoTFleet.js +16 -1
  194. package/build/dist/Models/DatabaseModels/IoTFleet.js.map +1 -1
  195. package/build/dist/Models/DatabaseModels/KubernetesCluster.js +16 -1
  196. package/build/dist/Models/DatabaseModels/KubernetesCluster.js.map +1 -1
  197. package/build/dist/Models/DatabaseModels/LogDropFilter.js +9 -20
  198. package/build/dist/Models/DatabaseModels/LogDropFilter.js.map +1 -1
  199. package/build/dist/Models/DatabaseModels/NetworkDevice.js +16 -1
  200. package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
  201. package/build/dist/Models/DatabaseModels/NetworkInterface.js +16 -1
  202. package/build/dist/Models/DatabaseModels/NetworkInterface.js.map +1 -1
  203. package/build/dist/Models/DatabaseModels/Project.js +16 -2
  204. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  205. package/build/dist/Models/DatabaseModels/ProxmoxCluster.js +16 -1
  206. package/build/dist/Models/DatabaseModels/ProxmoxCluster.js.map +1 -1
  207. package/build/dist/Models/DatabaseModels/Service.js +28 -1
  208. package/build/dist/Models/DatabaseModels/Service.js.map +1 -1
  209. package/build/dist/Models/DatabaseModels/StatusPageMonitorRule.js +925 -0
  210. package/build/dist/Models/DatabaseModels/StatusPageMonitorRule.js.map +1 -0
  211. package/build/dist/Models/DatabaseModels/StatusPagePrivateUser.js +32 -0
  212. package/build/dist/Models/DatabaseModels/StatusPagePrivateUser.js.map +1 -1
  213. package/build/dist/Models/DatabaseModels/StatusPageResource.js +72 -0
  214. package/build/dist/Models/DatabaseModels/StatusPageResource.js.map +1 -1
  215. package/build/dist/Models/DatabaseModels/TraceDropFilter.js +9 -20
  216. package/build/dist/Models/DatabaseModels/TraceDropFilter.js.map +1 -1
  217. package/build/dist/Models/DatabaseModels/User.js +32 -0
  218. package/build/dist/Models/DatabaseModels/User.js.map +1 -1
  219. package/build/dist/Server/API/AIAgentDataAPI.js +21 -2
  220. package/build/dist/Server/API/AIAgentDataAPI.js.map +1 -1
  221. package/build/dist/Server/API/BaseAPI.js +11 -1
  222. package/build/dist/Server/API/BaseAPI.js.map +1 -1
  223. package/build/dist/Server/API/DashboardAPI.js +343 -10
  224. package/build/dist/Server/API/DashboardAPI.js.map +1 -1
  225. package/build/dist/Server/API/GitHubAPI.js +94 -167
  226. package/build/dist/Server/API/GitHubAPI.js.map +1 -1
  227. package/build/dist/Server/API/UserAPI.js +187 -2
  228. package/build/dist/Server/API/UserAPI.js.map +1 -1
  229. package/build/dist/Server/EnvironmentConfig.js +34 -4
  230. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  231. package/build/dist/Server/Infrastructure/GlobalCache.js +120 -0
  232. package/build/dist/Server/Infrastructure/GlobalCache.js.map +1 -1
  233. package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js +5 -3
  234. package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js.map +1 -1
  235. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786005052769-AddStatusPageMonitorRule.js +48 -0
  236. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786005052769-AddStatusPageMonitorRule.js.map +1 -0
  237. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786018109307-AddPerUserPasswordSalt.js +14 -0
  238. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786018109307-AddPerUserPasswordSalt.js.map +1 -0
  239. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786023262402-WidenHashedStringColumnsForScrypt.js +59 -0
  240. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786023262402-WidenHashedStringColumnsForScrypt.js.map +1 -0
  241. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786100000000-RestoreServiceLowerNameIndex.js +93 -0
  242. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786100000000-RestoreServiceLowerNameIndex.js.map +1 -0
  243. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786200000000-RestoreDroppedUniqueIndexes.js +101 -0
  244. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786200000000-RestoreDroppedUniqueIndexes.js.map +1 -0
  245. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786300000000-QuarantineUnboundGitHubInstallations.js +66 -0
  246. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786300000000-QuarantineUnboundGitHubInstallations.js.map +1 -0
  247. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +12 -0
  248. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  249. package/build/dist/Server/Services/AlertStateTimelineService.js +19 -0
  250. package/build/dist/Server/Services/AlertStateTimelineService.js.map +1 -1
  251. package/build/dist/Server/Services/AnalyticsDatabaseService.js +57 -1
  252. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  253. package/build/dist/Server/Services/CephClusterService.js +26 -76
  254. package/build/dist/Server/Services/CephClusterService.js.map +1 -1
  255. package/build/dist/Server/Services/CloudResourceService.js +23 -73
  256. package/build/dist/Server/Services/CloudResourceService.js.map +1 -1
  257. package/build/dist/Server/Services/CodeRepositoryService.js +90 -2
  258. package/build/dist/Server/Services/CodeRepositoryService.js.map +1 -1
  259. package/build/dist/Server/Services/DatabaseService.js +246 -2
  260. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  261. package/build/dist/Server/Services/DockerHostService.js +19 -69
  262. package/build/dist/Server/Services/DockerHostService.js.map +1 -1
  263. package/build/dist/Server/Services/DockerSwarmClusterService.js +27 -78
  264. package/build/dist/Server/Services/DockerSwarmClusterService.js.map +1 -1
  265. package/build/dist/Server/Services/HostService.js +58 -102
  266. package/build/dist/Server/Services/HostService.js.map +1 -1
  267. package/build/dist/Server/Services/IncidentStateTimelineService.js +19 -0
  268. package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
  269. package/build/dist/Server/Services/Index.js +2 -0
  270. package/build/dist/Server/Services/Index.js.map +1 -1
  271. package/build/dist/Server/Services/IoTFleetService.js +19 -69
  272. package/build/dist/Server/Services/IoTFleetService.js.map +1 -1
  273. package/build/dist/Server/Services/KubernetesClusterService.js +17 -68
  274. package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
  275. package/build/dist/Server/Services/LabelService.js +42 -7
  276. package/build/dist/Server/Services/LabelService.js.map +1 -1
  277. package/build/dist/Server/Services/MetricTypeService.js +74 -0
  278. package/build/dist/Server/Services/MetricTypeService.js.map +1 -1
  279. package/build/dist/Server/Services/MonitorService.js +81 -9
  280. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  281. package/build/dist/Server/Services/MonitorStatusTimelineService.js +28 -0
  282. package/build/dist/Server/Services/MonitorStatusTimelineService.js.map +1 -1
  283. package/build/dist/Server/Services/MonitorTemplateService.js +60 -0
  284. package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
  285. package/build/dist/Server/Services/OpenTelemetryIngestService.js +155 -25
  286. package/build/dist/Server/Services/OpenTelemetryIngestService.js.map +1 -1
  287. package/build/dist/Server/Services/PodmanHostService.js +19 -69
  288. package/build/dist/Server/Services/PodmanHostService.js.map +1 -1
  289. package/build/dist/Server/Services/ProxmoxClusterService.js +23 -73
  290. package/build/dist/Server/Services/ProxmoxClusterService.js.map +1 -1
  291. package/build/dist/Server/Services/RumApplicationService.js +19 -70
  292. package/build/dist/Server/Services/RumApplicationService.js.map +1 -1
  293. package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js +20 -0
  294. package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js.map +1 -1
  295. package/build/dist/Server/Services/ServerlessFunctionService.js +24 -75
  296. package/build/dist/Server/Services/ServerlessFunctionService.js.map +1 -1
  297. package/build/dist/Server/Services/ServiceService.js +236 -61
  298. package/build/dist/Server/Services/ServiceService.js.map +1 -1
  299. package/build/dist/Server/Services/StatusPageMonitorRuleEngineService.js +603 -0
  300. package/build/dist/Server/Services/StatusPageMonitorRuleEngineService.js.map +1 -0
  301. package/build/dist/Server/Services/StatusPageMonitorRuleService.js +401 -0
  302. package/build/dist/Server/Services/StatusPageMonitorRuleService.js.map +1 -0
  303. package/build/dist/Server/Services/TelemetryEntityRelationshipService.js +1 -0
  304. package/build/dist/Server/Services/TelemetryEntityRelationshipService.js.map +1 -1
  305. package/build/dist/Server/Services/TelemetryEntityService.js +1 -0
  306. package/build/dist/Server/Services/TelemetryEntityService.js.map +1 -1
  307. package/build/dist/Server/Services/UserService.js +165 -0
  308. package/build/dist/Server/Services/UserService.js.map +1 -1
  309. package/build/dist/Server/Utils/AI/Toolbox/CodeTools.js +31 -1
  310. package/build/dist/Server/Utils/AI/Toolbox/CodeTools.js.map +1 -1
  311. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +13 -0
  312. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
  313. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHub.js +126 -4
  314. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHub.js.map +1 -1
  315. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHubInstallationBinding.js +136 -0
  316. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHubInstallationBinding.js.map +1 -0
  317. package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js +105 -29
  318. package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js.map +1 -1
  319. package/build/dist/Server/Utils/Monitor/MonitorResource.js +56 -32
  320. package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
  321. package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js +30 -3
  322. package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js.map +1 -1
  323. package/build/dist/Server/Utils/Monitor/MonitorStepsProjectValidator.js +110 -98
  324. package/build/dist/Server/Utils/Monitor/MonitorStepsProjectValidator.js.map +1 -1
  325. package/build/dist/Server/Utils/Monitor/MonitorStepsReferenceExtractor.js +312 -0
  326. package/build/dist/Server/Utils/Monitor/MonitorStepsReferenceExtractor.js.map +1 -0
  327. package/build/dist/Server/Utils/PasswordHash.js +237 -0
  328. package/build/dist/Server/Utils/PasswordHash.js.map +1 -0
  329. package/build/dist/Server/Utils/SingleFlight.js +76 -0
  330. package/build/dist/Server/Utils/SingleFlight.js.map +1 -0
  331. package/build/dist/Server/Utils/StatusPage/MonitorRulePatternValidator.js +29 -0
  332. package/build/dist/Server/Utils/StatusPage/MonitorRulePatternValidator.js.map +1 -0
  333. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +78 -19
  334. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
  335. package/build/dist/Server/Utils/Telemetry/ResourceHeartbeat.js +160 -0
  336. package/build/dist/Server/Utils/Telemetry/ResourceHeartbeat.js.map +1 -0
  337. package/build/dist/Server/Utils/Telemetry/Telemetry.js +279 -104
  338. package/build/dist/Server/Utils/Telemetry/Telemetry.js.map +1 -1
  339. package/build/dist/Types/Dashboard/DashboardComponentType.js +1 -0
  340. package/build/dist/Types/Dashboard/DashboardComponentType.js.map +1 -1
  341. package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js +1 -0
  342. package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js.map +1 -1
  343. package/build/dist/Types/Dashboard/DashboardComponents/DashboardNetworkMapComponent.js +2 -0
  344. package/build/dist/Types/Dashboard/DashboardComponents/DashboardNetworkMapComponent.js.map +1 -0
  345. package/build/dist/Types/Dashboard/DashboardTemplates.js +368 -0
  346. package/build/dist/Types/Dashboard/DashboardTemplates.js.map +1 -1
  347. package/build/dist/Types/Database/BigIntColumnTransformer.js +66 -0
  348. package/build/dist/Types/Database/BigIntColumnTransformer.js.map +1 -0
  349. package/build/dist/Types/Database/ColumnLength.js +9 -1
  350. package/build/dist/Types/Database/ColumnLength.js.map +1 -1
  351. package/build/dist/Types/Database/DatabaseProperty.js +25 -0
  352. package/build/dist/Types/Database/DatabaseProperty.js.map +1 -1
  353. package/build/dist/Types/Database/NumericColumnValue.js +97 -0
  354. package/build/dist/Types/Database/NumericColumnValue.js.map +1 -0
  355. package/build/dist/Types/Database/TableColumn.js.map +1 -1
  356. package/build/dist/Types/Database/UnsynchronizedIndex.js +46 -0
  357. package/build/dist/Types/Database/UnsynchronizedIndex.js.map +1 -0
  358. package/build/dist/Types/Decimal.js +14 -3
  359. package/build/dist/Types/Decimal.js.map +1 -1
  360. package/build/dist/Types/HashedString.js +95 -5
  361. package/build/dist/Types/HashedString.js.map +1 -1
  362. package/build/dist/Types/Permission.js +42 -0
  363. package/build/dist/Types/Permission.js.map +1 -1
  364. package/build/dist/Types/Port.js +15 -5
  365. package/build/dist/Types/Port.js.map +1 -1
  366. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js +51 -10
  367. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js.map +1 -1
  368. package/build/dist/UI/Components/LogsViewer/useLiveLogsRefresh.js +44 -0
  369. package/build/dist/UI/Components/LogsViewer/useLiveLogsRefresh.js.map +1 -0
  370. package/build/dist/UI/Components/LogsViewer/useLogsHistogram.js +75 -0
  371. package/build/dist/UI/Components/LogsViewer/useLogsHistogram.js.map +1 -0
  372. package/build/dist/UI/Components/Navbar/NavBar.js +3 -1
  373. package/build/dist/UI/Components/Navbar/NavBar.js.map +1 -1
  374. package/build/dist/UI/Components/Navbar/NavBarMenuModal.js +19 -7
  375. package/build/dist/UI/Components/Navbar/NavBarMenuModal.js.map +1 -1
  376. package/build/dist/UI/Utils/ModelAPI/UserProjectsModelAPI.js +156 -0
  377. package/build/dist/UI/Utils/ModelAPI/UserProjectsModelAPI.js.map +1 -0
  378. package/build/dist/Utils/Dashboard/Components/DashboardNetworkMapComponent.js +114 -0
  379. package/build/dist/Utils/Dashboard/Components/DashboardNetworkMapComponent.js.map +1 -0
  380. package/build/dist/Utils/Dashboard/Components/Index.js +4 -0
  381. package/build/dist/Utils/Dashboard/Components/Index.js.map +1 -1
  382. package/build/dist/Utils/TeamMembersByProject.js +145 -0
  383. package/build/dist/Utils/TeamMembersByProject.js.map +1 -0
  384. package/package.json +1 -1
@@ -0,0 +1,598 @@
1
+ import GlobalCache from "../../../Server/Infrastructure/GlobalCache";
2
+ import MetricTypeService from "../../../Server/Services/MetricTypeService";
3
+ import TelemetryUtil from "../../../Server/Utils/Telemetry/Telemetry";
4
+ import MetricType from "../../../Models/DatabaseModels/MetricType";
5
+ import Service from "../../../Models/DatabaseModels/Service";
6
+ import Dictionary from "../../../Types/Dictionary";
7
+ import ObjectID from "../../../Types/ObjectID";
8
+ import {
9
+ afterEach,
10
+ beforeEach,
11
+ describe,
12
+ expect,
13
+ jest,
14
+ test,
15
+ } from "@jest/globals";
16
+
17
+ /*
18
+ * Regression suite for the metric-catalog writer on the ingest hot path.
19
+ *
20
+ * `indexMetricNameServiceNameMap` reconciles one MetricType row per (project,
21
+ * metric name). It runs once per distinct metric name per batch, and it was
22
+ * the only per-batch Postgres writer in the pipeline with no fence, no cache
23
+ * and no dedup of any kind. Two independent defects made it write on EVERY
24
+ * batch, forever:
25
+ *
26
+ * 1. THE DIFF WAS STRUCTURALLY UNSATISFIABLE. Rows are created with
27
+ * `description || ""`, so the stored value is `""` when OTLP omits the
28
+ * field — but the incoming value for that same case is `undefined`,
29
+ * because protobuf omits unset scalars. `"" !== undefined` is true, so
30
+ * "has anything changed?" answered YES on every batch for every metric
31
+ * without a description or unit. The counter-semantics fields right below
32
+ * already had the correct `!== undefined` guard; description and unit were
33
+ * simply left behind.
34
+ *
35
+ * 2. THE WRITE SHIPPED THE WHOLE RELATION. `services` is an EntityArray, and
36
+ * its mere presence as a key routes DatabaseService to
37
+ * `getRepository().save()` — a real BEGIN/COMMIT that reloads the entity,
38
+ * bumps `version`, and DELETEs then re-INSERTs junction rows, holding the
39
+ * row's write lock across every round trip. It was the only multi-
40
+ * round-trip lock hold on the ingest path.
41
+ *
42
+ * Defect 2 was also silently LOSING DATA: save() deletes any association not
43
+ * present in the array it is given, and each worker only knows the services in
44
+ * its own batch. Two workers therefore deleted each other's associations and
45
+ * re-inserted them next batch — permanent churn, with real service-to-metric
46
+ * links missing from the UI in between.
47
+ *
48
+ * Everything external is mocked — no Postgres, no Redis.
49
+ */
50
+
51
+ const PROJECT_ID: ObjectID = ObjectID.generate();
52
+ const METRIC_TYPE_ID: ObjectID = ObjectID.generate();
53
+ const SERVICE_A: ObjectID = ObjectID.generate();
54
+ const SERVICE_B: ObjectID = ObjectID.generate();
55
+
56
+ type ScalarUpdate = { id: ObjectID; data: Record<string, unknown> };
57
+ type AttachCall = { metricTypeId: ObjectID; serviceIds: Array<ObjectID> };
58
+
59
+ let cache: Map<string, string>;
60
+ let scalarUpdates: Array<ScalarUpdate>;
61
+ let attachCalls: Array<AttachCall>;
62
+ let creates: Array<MetricType>;
63
+ let saveUpdates: Array<unknown>;
64
+ let existingRow: MetricType | null;
65
+
66
+ /** A stored row as findOneBy would return it. */
67
+ function storedRow(overrides?: Partial<MetricType>): MetricType {
68
+ const row: MetricType = new MetricType();
69
+ row.id = METRIC_TYPE_ID;
70
+ row._id = METRIC_TYPE_ID.toString();
71
+ row.name = "http.server.duration";
72
+ // Created via `description || ""` — this is the value that is really stored.
73
+ row.description = "";
74
+ row.unit = "";
75
+ row.services = [];
76
+ return Object.assign(row, overrides || {});
77
+ }
78
+
79
+ /** The batch's view of a metric, as the ingest pipeline builds it. */
80
+ function incoming(overrides?: Partial<MetricType>): MetricType {
81
+ const metric: MetricType = new MetricType();
82
+ metric.services = [];
83
+ return Object.assign(metric, overrides || {});
84
+ }
85
+
86
+ function serviceRef(id: ObjectID): Service {
87
+ const service: Service = new Service();
88
+ service.id = id;
89
+ return service;
90
+ }
91
+
92
+ function batch(metric: MetricType): Dictionary<MetricType> {
93
+ return { "http.server.duration": metric };
94
+ }
95
+
96
+ beforeEach(() => {
97
+ cache = new Map<string, string>();
98
+ scalarUpdates = [];
99
+ attachCalls = [];
100
+ creates = [];
101
+ saveUpdates = [];
102
+ existingRow = storedRow();
103
+
104
+ jest
105
+ .spyOn(GlobalCache, "setStringIfChanged")
106
+ .mockImplementation(
107
+ async (namespace: string, key: string, value: string) => {
108
+ const full: string = `${namespace}:${key}`;
109
+ if (cache.get(full) === value) {
110
+ return false;
111
+ }
112
+ cache.set(full, value);
113
+ return true;
114
+ },
115
+ );
116
+
117
+ jest
118
+ .spyOn(GlobalCache, "deleteKey")
119
+ .mockImplementation(async (namespace: string, key: string) => {
120
+ cache.delete(`${namespace}:${key}`);
121
+ });
122
+
123
+ jest.spyOn(MetricTypeService, "findOneBy").mockImplementation(async () => {
124
+ return existingRow;
125
+ });
126
+
127
+ jest
128
+ .spyOn(MetricTypeService, "updateColumnsByIdWithoutHooks")
129
+ .mockImplementation(async (input: { id: ObjectID; data: unknown }) => {
130
+ scalarUpdates.push({
131
+ id: input.id,
132
+ data: { ...(input.data as Record<string, unknown>) },
133
+ });
134
+ });
135
+
136
+ jest
137
+ .spyOn(MetricTypeService, "attachServices")
138
+ .mockImplementation(
139
+ async (input: {
140
+ metricTypeId: ObjectID;
141
+ serviceIds: Array<ObjectID>;
142
+ }) => {
143
+ attachCalls.push({
144
+ metricTypeId: input.metricTypeId,
145
+ serviceIds: [...input.serviceIds],
146
+ });
147
+ },
148
+ );
149
+
150
+ jest
151
+ .spyOn(MetricTypeService, "create")
152
+ .mockImplementation(async (input: { data: MetricType }) => {
153
+ creates.push(input.data);
154
+ return input.data;
155
+ });
156
+
157
+ /*
158
+ * The transactional relation-writing path. Nothing on the ingest path may
159
+ * reach it — if this records a call, the multi-round-trip lock hold is back.
160
+ */
161
+ jest
162
+ .spyOn(MetricTypeService, "updateOneById")
163
+ .mockImplementation(async (input: unknown) => {
164
+ saveUpdates.push(input);
165
+ return 1;
166
+ });
167
+ });
168
+
169
+ afterEach(() => {
170
+ jest.restoreAllMocks();
171
+ });
172
+
173
+ describe("the diff converges instead of re-firing every batch", () => {
174
+ /*
175
+ * The headline regression. A metric with no description and no unit — the
176
+ * overwhelmingly common case — used to report a change on every single
177
+ * batch, forever.
178
+ */
179
+ test("a metric with no description or unit writes nothing", async () => {
180
+ await TelemetryUtil.indexMetricNameServiceNameMap({
181
+ projectId: PROJECT_ID,
182
+ metricNameServiceNameMap: batch(incoming()),
183
+ });
184
+
185
+ expect(scalarUpdates).toHaveLength(0);
186
+ expect(attachCalls).toHaveLength(0);
187
+ expect(saveUpdates).toHaveLength(0);
188
+ });
189
+
190
+ /*
191
+ * The specific comparison that was wrong: stored `""` against incoming
192
+ * `undefined`. Checked with the fence disabled so it is the DIFF being
193
+ * tested, not the throttle in front of it.
194
+ */
195
+ test("stored empty-string against incoming undefined is not a change", async () => {
196
+ jest.spyOn(GlobalCache, "setStringIfChanged").mockResolvedValue(true);
197
+
198
+ for (let i: number = 0; i < 5; i++) {
199
+ await TelemetryUtil.indexMetricNameServiceNameMap({
200
+ projectId: PROJECT_ID,
201
+ metricNameServiceNameMap: batch(incoming()),
202
+ });
203
+ }
204
+
205
+ expect(scalarUpdates).toHaveLength(0);
206
+ });
207
+
208
+ test("an unchanged description is not a change", async () => {
209
+ jest.spyOn(GlobalCache, "setStringIfChanged").mockResolvedValue(true);
210
+ existingRow = storedRow({ description: "Duration", unit: "ms" });
211
+
212
+ await TelemetryUtil.indexMetricNameServiceNameMap({
213
+ projectId: PROJECT_ID,
214
+ metricNameServiceNameMap: batch(
215
+ incoming({ description: "Duration", unit: "ms" }),
216
+ ),
217
+ });
218
+
219
+ expect(scalarUpdates).toHaveLength(0);
220
+ });
221
+
222
+ test("a genuinely new description is written", async () => {
223
+ await TelemetryUtil.indexMetricNameServiceNameMap({
224
+ projectId: PROJECT_ID,
225
+ metricNameServiceNameMap: batch(incoming({ description: "Duration" })),
226
+ });
227
+
228
+ expect(scalarUpdates).toHaveLength(1);
229
+ expect(scalarUpdates[0]!.data["description"]).toBe("Duration");
230
+ });
231
+
232
+ /*
233
+ * Absent means "this producer didn't say", not "clear it" — the same rule
234
+ * the counter-semantics fields have always used. A producer that omits the
235
+ * description must not wipe one another producer supplied, or the two flip-
236
+ * flop the row forever.
237
+ */
238
+ test("an omitted description never clears a stored one", async () => {
239
+ jest.spyOn(GlobalCache, "setStringIfChanged").mockResolvedValue(true);
240
+ existingRow = storedRow({ description: "Duration", unit: "ms" });
241
+
242
+ await TelemetryUtil.indexMetricNameServiceNameMap({
243
+ projectId: PROJECT_ID,
244
+ metricNameServiceNameMap: batch(incoming()),
245
+ });
246
+
247
+ expect(scalarUpdates).toHaveLength(0);
248
+ });
249
+
250
+ test("counter semantics still converge", async () => {
251
+ jest.spyOn(GlobalCache, "setStringIfChanged").mockResolvedValue(true);
252
+ existingRow = storedRow({ isMonotonic: true });
253
+
254
+ await TelemetryUtil.indexMetricNameServiceNameMap({
255
+ projectId: PROJECT_ID,
256
+ metricNameServiceNameMap: batch(incoming({ isMonotonic: true })),
257
+ });
258
+
259
+ expect(scalarUpdates).toHaveLength(0);
260
+ });
261
+
262
+ test("a changed isMonotonic is written", async () => {
263
+ existingRow = storedRow({ isMonotonic: false });
264
+
265
+ await TelemetryUtil.indexMetricNameServiceNameMap({
266
+ projectId: PROJECT_ID,
267
+ metricNameServiceNameMap: batch(incoming({ isMonotonic: true })),
268
+ });
269
+
270
+ expect(scalarUpdates[0]!.data["isMonotonic"]).toBe(true);
271
+ });
272
+ });
273
+
274
+ describe("the write never reopens the transactional relation path", () => {
275
+ /*
276
+ * `services` present as a key — even as an empty array — routes
277
+ * DatabaseService to save(). The scalar write must therefore carry only
278
+ * scalar columns.
279
+ */
280
+ test("a scalar-only change takes the single-statement path", async () => {
281
+ await TelemetryUtil.indexMetricNameServiceNameMap({
282
+ projectId: PROJECT_ID,
283
+ metricNameServiceNameMap: batch(incoming({ description: "Duration" })),
284
+ });
285
+
286
+ expect(saveUpdates).toHaveLength(0);
287
+ expect(scalarUpdates).toHaveLength(1);
288
+ });
289
+
290
+ test("the scalar write carries no `services` key at all", async () => {
291
+ await TelemetryUtil.indexMetricNameServiceNameMap({
292
+ projectId: PROJECT_ID,
293
+ metricNameServiceNameMap: batch(incoming({ description: "Duration" })),
294
+ });
295
+
296
+ expect(Object.keys(scalarUpdates[0]!.data)).not.toContain("services");
297
+ });
298
+
299
+ test("the scalar write carries only the columns that changed", async () => {
300
+ existingRow = storedRow({ description: "Duration" });
301
+
302
+ await TelemetryUtil.indexMetricNameServiceNameMap({
303
+ projectId: PROJECT_ID,
304
+ metricNameServiceNameMap: batch(
305
+ incoming({ description: "Duration", unit: "ms" }),
306
+ ),
307
+ });
308
+
309
+ expect(Object.keys(scalarUpdates[0]!.data)).toEqual(["unit"]);
310
+ });
311
+
312
+ /*
313
+ * The data-loss fix. Associations are added, never replaced — a worker that
314
+ * only knows its own batch's services must not be able to express "and
315
+ * delete every other one".
316
+ */
317
+ test("a new association is added additively", async () => {
318
+ existingRow = storedRow({ services: [serviceRef(SERVICE_A)] });
319
+
320
+ await TelemetryUtil.indexMetricNameServiceNameMap({
321
+ projectId: PROJECT_ID,
322
+ metricNameServiceNameMap: batch(
323
+ incoming({ services: [serviceRef(SERVICE_B)] }),
324
+ ),
325
+ });
326
+
327
+ expect(saveUpdates).toHaveLength(0);
328
+ expect(attachCalls).toHaveLength(1);
329
+ expect(
330
+ attachCalls[0]!.serviceIds.map((id: ObjectID) => {
331
+ return id.toString();
332
+ }),
333
+ ).toEqual([SERVICE_B.toString()]);
334
+ });
335
+
336
+ test("only the missing associations are sent", async () => {
337
+ existingRow = storedRow({ services: [serviceRef(SERVICE_A)] });
338
+
339
+ await TelemetryUtil.indexMetricNameServiceNameMap({
340
+ projectId: PROJECT_ID,
341
+ metricNameServiceNameMap: batch(
342
+ incoming({ services: [serviceRef(SERVICE_A), serviceRef(SERVICE_B)] }),
343
+ ),
344
+ });
345
+
346
+ expect(
347
+ attachCalls[0]!.serviceIds.map((id: ObjectID) => {
348
+ return id.toString();
349
+ }),
350
+ ).toEqual([SERVICE_B.toString()]);
351
+ });
352
+
353
+ test("an already-associated service writes nothing", async () => {
354
+ jest.spyOn(GlobalCache, "setStringIfChanged").mockResolvedValue(true);
355
+ existingRow = storedRow({ services: [serviceRef(SERVICE_A)] });
356
+
357
+ await TelemetryUtil.indexMetricNameServiceNameMap({
358
+ projectId: PROJECT_ID,
359
+ metricNameServiceNameMap: batch(
360
+ incoming({ services: [serviceRef(SERVICE_A)] }),
361
+ ),
362
+ });
363
+
364
+ expect(attachCalls).toHaveLength(0);
365
+ expect(scalarUpdates).toHaveLength(0);
366
+ });
367
+ });
368
+
369
+ describe("the fence keeps the steady state off the database entirely", () => {
370
+ /*
371
+ * Fenced BEFORE the lookup, so a repeat batch costs no SELECT either — the
372
+ * read fan-out was as much of the pool pressure as the write.
373
+ */
374
+ test("a repeated batch does not even read", async () => {
375
+ await TelemetryUtil.indexMetricNameServiceNameMap({
376
+ projectId: PROJECT_ID,
377
+ metricNameServiceNameMap: batch(incoming({ description: "Duration" })),
378
+ });
379
+
380
+ const readsAfterFirst: number = (MetricTypeService.findOneBy as jest.Mock)
381
+ .mock.calls.length;
382
+
383
+ await TelemetryUtil.indexMetricNameServiceNameMap({
384
+ projectId: PROJECT_ID,
385
+ metricNameServiceNameMap: batch(incoming({ description: "Duration" })),
386
+ });
387
+
388
+ expect((MetricTypeService.findOneBy as jest.Mock).mock.calls.length).toBe(
389
+ readsAfterFirst,
390
+ );
391
+ });
392
+
393
+ /*
394
+ * The batch's service set must be part of the fence KEY. `servicesInMap`
395
+ * holds only the services in THIS batch, so keying on the metric name alone
396
+ * would let a batch from service A satisfy the fence and suppress the
397
+ * association a concurrent batch from service B still needs.
398
+ */
399
+ test("a different service set is not suppressed by the fence", async () => {
400
+ existingRow = storedRow({ services: [] });
401
+
402
+ await TelemetryUtil.indexMetricNameServiceNameMap({
403
+ projectId: PROJECT_ID,
404
+ metricNameServiceNameMap: batch(
405
+ incoming({ services: [serviceRef(SERVICE_A)] }),
406
+ ),
407
+ });
408
+
409
+ await TelemetryUtil.indexMetricNameServiceNameMap({
410
+ projectId: PROJECT_ID,
411
+ metricNameServiceNameMap: batch(
412
+ incoming({ services: [serviceRef(SERVICE_B)] }),
413
+ ),
414
+ });
415
+
416
+ expect(attachCalls).toHaveLength(2);
417
+ });
418
+
419
+ test("a changed scalar re-opens the fence within the window", async () => {
420
+ await TelemetryUtil.indexMetricNameServiceNameMap({
421
+ projectId: PROJECT_ID,
422
+ metricNameServiceNameMap: batch(incoming({ description: "Duration" })),
423
+ });
424
+
425
+ existingRow = storedRow({ description: "Duration" });
426
+
427
+ await TelemetryUtil.indexMetricNameServiceNameMap({
428
+ projectId: PROJECT_ID,
429
+ metricNameServiceNameMap: batch(incoming({ description: "Latency" })),
430
+ });
431
+
432
+ expect(scalarUpdates).toHaveLength(2);
433
+ expect(scalarUpdates[1]!.data["description"]).toBe("Latency");
434
+ });
435
+
436
+ test("a cache outage fails open so the catalog still converges", async () => {
437
+ jest
438
+ .spyOn(GlobalCache, "setStringIfChanged")
439
+ .mockRejectedValue(new Error("redis down"));
440
+
441
+ await TelemetryUtil.indexMetricNameServiceNameMap({
442
+ projectId: PROJECT_ID,
443
+ metricNameServiceNameMap: batch(incoming({ description: "Duration" })),
444
+ });
445
+
446
+ expect(scalarUpdates).toHaveLength(1);
447
+ });
448
+ });
449
+
450
+ describe("one bad metric cannot stop the rest of the batch", () => {
451
+ /*
452
+ * The loop used to let any failure escape, and the only caller just logs —
453
+ * so a single unwritable row silently stopped every REMAINING metric name in
454
+ * the batch from being indexed at all.
455
+ */
456
+ test("a failure on one metric still indexes the others", async () => {
457
+ jest
458
+ .spyOn(MetricTypeService, "updateColumnsByIdWithoutHooks")
459
+ .mockImplementation(async (input: { id: ObjectID; data: unknown }) => {
460
+ const data: Record<string, unknown> = input.data as Record<
461
+ string,
462
+ unknown
463
+ >;
464
+ if (data["description"] === "poison") {
465
+ throw new Error("value too long for type character varying(100)");
466
+ }
467
+ scalarUpdates.push({ id: input.id, data: { ...data } });
468
+ });
469
+
470
+ await TelemetryUtil.indexMetricNameServiceNameMap({
471
+ projectId: PROJECT_ID,
472
+ metricNameServiceNameMap: {
473
+ "metric.bad": incoming({ description: "poison" }),
474
+ "metric.good": incoming({ description: "fine" }),
475
+ },
476
+ });
477
+
478
+ expect(scalarUpdates).toHaveLength(1);
479
+ expect(scalarUpdates[0]!.data["description"]).toBe("fine");
480
+ });
481
+
482
+ test("does not throw at the caller", async () => {
483
+ jest
484
+ .spyOn(MetricTypeService, "updateColumnsByIdWithoutHooks")
485
+ .mockRejectedValue(new Error("connection terminated"));
486
+
487
+ await expect(
488
+ TelemetryUtil.indexMetricNameServiceNameMap({
489
+ projectId: PROJECT_ID,
490
+ metricNameServiceNameMap: batch(incoming({ description: "Duration" })),
491
+ }),
492
+ ).resolves.toBeUndefined();
493
+ });
494
+
495
+ test("re-opens the fence after a failure so the next batch retries", async () => {
496
+ let fail: boolean = true;
497
+ jest
498
+ .spyOn(MetricTypeService, "updateColumnsByIdWithoutHooks")
499
+ .mockImplementation(async (input: { id: ObjectID; data: unknown }) => {
500
+ if (fail) {
501
+ throw new Error("transient");
502
+ }
503
+ scalarUpdates.push({
504
+ id: input.id,
505
+ data: { ...(input.data as Record<string, unknown>) },
506
+ });
507
+ });
508
+
509
+ await TelemetryUtil.indexMetricNameServiceNameMap({
510
+ projectId: PROJECT_ID,
511
+ metricNameServiceNameMap: batch(incoming({ description: "Duration" })),
512
+ });
513
+
514
+ fail = false;
515
+
516
+ await TelemetryUtil.indexMetricNameServiceNameMap({
517
+ projectId: PROJECT_ID,
518
+ metricNameServiceNameMap: batch(incoming({ description: "Duration" })),
519
+ });
520
+
521
+ expect(scalarUpdates).toHaveLength(1);
522
+ });
523
+ });
524
+
525
+ describe("first contact and the create race", () => {
526
+ test("creates the row when none exists", async () => {
527
+ existingRow = null;
528
+
529
+ await TelemetryUtil.indexMetricNameServiceNameMap({
530
+ projectId: PROJECT_ID,
531
+ metricNameServiceNameMap: batch(
532
+ incoming({
533
+ description: "Duration",
534
+ services: [serviceRef(SERVICE_A)],
535
+ }),
536
+ ),
537
+ });
538
+
539
+ expect(creates).toHaveLength(1);
540
+ expect(creates[0]!.name).toBe("http.server.duration");
541
+ expect(creates[0]!.description).toBe("Duration");
542
+ expect(creates[0]!.services).toHaveLength(1);
543
+ });
544
+
545
+ /*
546
+ * Uniqueness on (projectId, name) is enforced in application code, not by a
547
+ * database constraint, so two workers can both decide to create the same
548
+ * metric. The loser must reconcile against the winner rather than surfacing
549
+ * a spurious error and abandoning the rest of the batch.
550
+ */
551
+ test("a lost create race reconciles against the winning row", async () => {
552
+ existingRow = null;
553
+
554
+ jest
555
+ .spyOn(MetricTypeService, "create")
556
+ .mockRejectedValue(
557
+ new Error("Metric Type with this name already exists"),
558
+ );
559
+
560
+ jest
561
+ .spyOn(MetricTypeService, "findOneBy")
562
+ .mockImplementationOnce(async () => {
563
+ return null;
564
+ })
565
+ .mockImplementation(async () => {
566
+ return storedRow({ services: [] });
567
+ });
568
+
569
+ await TelemetryUtil.indexMetricNameServiceNameMap({
570
+ projectId: PROJECT_ID,
571
+ metricNameServiceNameMap: batch(
572
+ incoming({
573
+ description: "Duration",
574
+ services: [serviceRef(SERVICE_A)],
575
+ }),
576
+ ),
577
+ });
578
+
579
+ expect(scalarUpdates).toHaveLength(1);
580
+ expect(attachCalls).toHaveLength(1);
581
+ });
582
+
583
+ test("a create failure with no winner is reported, not swallowed silently", async () => {
584
+ existingRow = null;
585
+
586
+ jest
587
+ .spyOn(MetricTypeService, "create")
588
+ .mockRejectedValue(new Error("null value in column violates not-null"));
589
+
590
+ await TelemetryUtil.indexMetricNameServiceNameMap({
591
+ projectId: PROJECT_ID,
592
+ metricNameServiceNameMap: batch(incoming({ description: "Duration" })),
593
+ });
594
+
595
+ // Reported via the per-metric catch; the fence is re-opened for a retry.
596
+ expect(cache.size).toBe(0);
597
+ });
598
+ });