@oneuptime/common 13.0.0 → 13.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 (1443) hide show
  1. package/Models/AnalyticsModels/RumSession.ts +34 -0
  2. package/Models/DatabaseModels/Alert.ts +55 -0
  3. package/Models/DatabaseModels/AlertEpisodeLabelRule.ts +2 -2
  4. package/Models/DatabaseModels/AlertEpisodeOnCallRule.ts +2 -2
  5. package/Models/DatabaseModels/AlertEpisodeOwnerRule.ts +2 -2
  6. package/Models/DatabaseModels/AlertEpisodePrivacyRule.ts +2 -2
  7. package/Models/DatabaseModels/AlertGroupingRule.ts +2 -2
  8. package/Models/DatabaseModels/AlertLabelRule.ts +2 -2
  9. package/Models/DatabaseModels/AlertOnCallRule.ts +2 -2
  10. package/Models/DatabaseModels/AlertOwnerRule.ts +2 -2
  11. package/Models/DatabaseModels/AlertPrivacyRule.ts +2 -2
  12. package/Models/DatabaseModels/AlertReminderRule.ts +6 -4
  13. package/Models/DatabaseModels/AutoRemediationRule.ts +2 -2
  14. package/Models/DatabaseModels/CephClusterLabelRule.ts +2 -2
  15. package/Models/DatabaseModels/CephClusterOwnerRule.ts +2 -2
  16. package/Models/DatabaseModels/CloudResource.ts +45 -7
  17. package/Models/DatabaseModels/CloudResourceLabelRule.ts +2 -2
  18. package/Models/DatabaseModels/CloudResourceOwnerRule.ts +2 -2
  19. package/Models/DatabaseModels/CodeRepository.ts +85 -0
  20. package/Models/DatabaseModels/DashboardLabelRule.ts +2 -2
  21. package/Models/DatabaseModels/DashboardOwnerRule.ts +2 -2
  22. package/Models/DatabaseModels/DatabaseBaseModel/RelationOnlyRuleBaseModel.ts +11 -0
  23. package/Models/DatabaseModels/DatabaseBaseModel/RuleBaseModel.ts +44 -0
  24. package/Models/DatabaseModels/DockerHostLabelRule.ts +2 -2
  25. package/Models/DatabaseModels/DockerHostOwnerRule.ts +2 -2
  26. package/Models/DatabaseModels/DockerSwarmClusterLabelRule.ts +2 -2
  27. package/Models/DatabaseModels/DockerSwarmClusterOwnerRule.ts +2 -2
  28. package/Models/DatabaseModels/GoogleSecOpsConnection.ts +57 -7
  29. package/Models/DatabaseModels/GoogleSecOpsConnectionRun.ts +263 -0
  30. package/Models/DatabaseModels/HostLabelRule.ts +2 -2
  31. package/Models/DatabaseModels/HostOwnerRule.ts +2 -2
  32. package/Models/DatabaseModels/Incident.ts +55 -0
  33. package/Models/DatabaseModels/IncidentEpisodeLabelRule.ts +2 -2
  34. package/Models/DatabaseModels/IncidentEpisodeOnCallRule.ts +2 -2
  35. package/Models/DatabaseModels/IncidentEpisodeOwnerRule.ts +2 -2
  36. package/Models/DatabaseModels/IncidentEpisodePrivacyRule.ts +2 -2
  37. package/Models/DatabaseModels/IncidentGroupingRule.ts +2 -2
  38. package/Models/DatabaseModels/IncidentLabelRule.ts +2 -2
  39. package/Models/DatabaseModels/IncidentOnCallRule.ts +2 -2
  40. package/Models/DatabaseModels/IncidentOwnerRule.ts +2 -2
  41. package/Models/DatabaseModels/IncidentPrivacyRule.ts +2 -2
  42. package/Models/DatabaseModels/IncidentReminderRule.ts +6 -4
  43. package/Models/DatabaseModels/IncidentSlaRule.ts +2 -2
  44. package/Models/DatabaseModels/IncomingCallPolicy.ts +3 -4
  45. package/Models/DatabaseModels/IncomingCallPolicyLabelRule.ts +2 -2
  46. package/Models/DatabaseModels/IncomingCallPolicyOwnerRule.ts +2 -2
  47. package/Models/DatabaseModels/IncomingCallPolicyPhoneNumber.ts +254 -0
  48. package/Models/DatabaseModels/Index.ts +18 -0
  49. package/Models/DatabaseModels/IoTFleetLabelRule.ts +2 -2
  50. package/Models/DatabaseModels/IoTFleetOwnerRule.ts +2 -2
  51. package/Models/DatabaseModels/KubernetesClusterLabelRule.ts +2 -2
  52. package/Models/DatabaseModels/KubernetesClusterOwnerRule.ts +2 -2
  53. package/Models/DatabaseModels/MonitorLabelRule.ts +2 -2
  54. package/Models/DatabaseModels/MonitorOwnerRule.ts +2 -2
  55. package/Models/DatabaseModels/NetworkDeviceAutoImportRule.ts +2 -2
  56. package/Models/DatabaseModels/NetworkDeviceLabelRule.ts +2 -2
  57. package/Models/DatabaseModels/NetworkDeviceOwnerRule.ts +2 -2
  58. package/Models/DatabaseModels/NetworkSiteAssignmentRule.ts +2 -2
  59. package/Models/DatabaseModels/OnCallDutyPolicyLabelRule.ts +2 -2
  60. package/Models/DatabaseModels/OnCallDutyPolicyOwnerRule.ts +2 -2
  61. package/Models/DatabaseModels/OnCallDutyPolicyScheduleLabelRule.ts +2 -2
  62. package/Models/DatabaseModels/OnCallDutyPolicyScheduleOwnerRule.ts +2 -2
  63. package/Models/DatabaseModels/PodmanHostLabelRule.ts +2 -2
  64. package/Models/DatabaseModels/PodmanHostOwnerRule.ts +2 -2
  65. package/Models/DatabaseModels/ProxmoxClusterLabelRule.ts +2 -2
  66. package/Models/DatabaseModels/ProxmoxClusterOwnerRule.ts +2 -2
  67. package/Models/DatabaseModels/RumApplicationLabelRule.ts +2 -2
  68. package/Models/DatabaseModels/RumApplicationOwnerRule.ts +2 -2
  69. package/Models/DatabaseModels/RunbookLabelRule.ts +2 -2
  70. package/Models/DatabaseModels/RunbookOwnerRule.ts +2 -2
  71. package/Models/DatabaseModels/RunbookRule.ts +2 -2
  72. package/Models/DatabaseModels/ScheduledMaintenance.ts +55 -0
  73. package/Models/DatabaseModels/ScheduledMaintenanceLabelRule.ts +2 -2
  74. package/Models/DatabaseModels/ScheduledMaintenanceOwnerRule.ts +2 -2
  75. package/Models/DatabaseModels/ScheduledMaintenanceReminderRule.ts +6 -4
  76. package/Models/DatabaseModels/ServerlessFunctionLabelRule.ts +2 -2
  77. package/Models/DatabaseModels/ServerlessFunctionOwnerRule.ts +2 -2
  78. package/Models/DatabaseModels/ServiceLabelRule.ts +2 -2
  79. package/Models/DatabaseModels/ServiceLevelObjectiveBurnRateRule.ts +283 -4
  80. package/Models/DatabaseModels/ServiceOwnerRule.ts +2 -2
  81. package/Models/DatabaseModels/StatusPageLabelRule.ts +2 -2
  82. package/Models/DatabaseModels/StatusPageMonitorRule.ts +2 -2
  83. package/Models/DatabaseModels/StatusPageOwnerRule.ts +2 -2
  84. package/Models/DatabaseModels/User.ts +51 -2
  85. package/Models/DatabaseModels/UserNotificationEmailRollupItem.ts +43 -2
  86. package/Models/DatabaseModels/UserWebAuthn.ts +17 -0
  87. package/Models/DatabaseModels/VMwareResource.ts +1149 -0
  88. package/Models/DatabaseModels/VMwareVCenter.ts +1122 -0
  89. package/Models/DatabaseModels/VMwareVCenterFeed.ts +625 -0
  90. package/Models/DatabaseModels/VMwareVCenterLabelRule.ts +514 -0
  91. package/Models/DatabaseModels/VMwareVCenterOwnerRule.ts +596 -0
  92. package/Models/DatabaseModels/VMwareVCenterOwnerTeam.ts +485 -0
  93. package/Models/DatabaseModels/VMwareVCenterOwnerUser.ts +484 -0
  94. package/Models/DatabaseModels/WorkflowLabelRule.ts +2 -2
  95. package/Models/DatabaseModels/WorkflowOwnerRule.ts +2 -2
  96. package/Models/DatabaseModels/WorkflowVariable.ts +22 -1
  97. package/Server/API/AIAgentDataAPI.ts +79 -4
  98. package/Server/API/AIAgentGitHubAPI.ts +523 -0
  99. package/Server/API/AIChatAPI.ts +20 -1
  100. package/Server/API/BillingAPI.ts +33 -0
  101. package/Server/API/DashboardAPI.ts +10 -0
  102. package/Server/API/GitHubAPI.ts +47 -5
  103. package/Server/API/GoogleSecOpsConnectionAPI.ts +79 -0
  104. package/Server/API/TelemetryAPI.ts +323 -2
  105. package/Server/API/UserWebAuthnAPI.ts +21 -1
  106. package/Server/API/VMwareResourceAPI.ts +143 -0
  107. package/Server/Infrastructure/Postgres/SchemaMigrations/1791800000000-SplitWebAuthnChallengeByPurpose.ts +68 -0
  108. package/Server/Infrastructure/Postgres/SchemaMigrations/1791900000000-AddSeverityAndStateToNotificationEmailRollup.ts +26 -0
  109. package/Server/Infrastructure/Postgres/SchemaMigrations/1792000000000-AddVMwareTables.ts +636 -0
  110. package/Server/Infrastructure/Postgres/SchemaMigrations/1792100000000-AddGitHubCommandSettingsToCodeRepository.ts +47 -0
  111. package/Server/Infrastructure/Postgres/SchemaMigrations/1792200000000-AddUserWebAuthnPurpose.ts +17 -0
  112. package/Server/Infrastructure/Postgres/SchemaMigrations/1792300000000-AddGoogleSecOpsDiagnostics.ts +83 -0
  113. package/Server/Infrastructure/Postgres/SchemaMigrations/1792400000000-AddSloBurnRateRuleIncidents.ts +111 -0
  114. package/Server/Infrastructure/Postgres/SchemaMigrations/1792500000000-AddConfigurableRuleCriteria.ts +644 -0
  115. package/Server/Infrastructure/Postgres/SchemaMigrations/1792600000000-AddIncomingCallPolicyPhoneNumbers.ts +72 -0
  116. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +18 -0
  117. package/Server/Middleware/IdentityRateLimit.ts +101 -0
  118. package/Server/Middleware/MultipartFormData.ts +46 -21
  119. package/Server/Services/AIRunService.ts +13 -0
  120. package/Server/Services/AlertEpisodeLabelRuleEngineService.ts +21 -0
  121. package/Server/Services/AlertEpisodeOnCallRuleEngineService.ts +21 -0
  122. package/Server/Services/AlertEpisodeOwnerRuleEngineService.ts +21 -0
  123. package/Server/Services/AlertEpisodePrivacyRuleEngineService.ts +21 -0
  124. package/Server/Services/AlertGroupingEngineService.ts +27 -0
  125. package/Server/Services/AlertLabelRuleEngineService.ts +27 -0
  126. package/Server/Services/AlertOnCallRuleEngineService.ts +27 -0
  127. package/Server/Services/AlertOwnerRuleEngineService.ts +27 -0
  128. package/Server/Services/AlertPrivacyRuleEngineService.ts +27 -0
  129. package/Server/Services/AlertReminderRuleService.ts +65 -33
  130. package/Server/Services/AutoRemediationRuleEngineService.ts +90 -8
  131. package/Server/Services/BillingPaymentMethodService.ts +99 -28
  132. package/Server/Services/BillingService.ts +287 -20
  133. package/Server/Services/CephClusterLabelRuleEngineService.ts +20 -0
  134. package/Server/Services/CephClusterOwnerRuleEngineService.ts +20 -0
  135. package/Server/Services/CloudResourceInstanceService.ts +70 -0
  136. package/Server/Services/CloudResourceLabelRuleEngineService.ts +20 -0
  137. package/Server/Services/CloudResourceOwnerRuleEngineService.ts +20 -0
  138. package/Server/Services/DashboardLabelRuleEngineService.ts +20 -0
  139. package/Server/Services/DashboardOwnerRuleEngineService.ts +20 -0
  140. package/Server/Services/DatabaseService.ts +409 -6
  141. package/Server/Services/DockerHostLabelRuleEngineService.ts +20 -0
  142. package/Server/Services/DockerHostOwnerRuleEngineService.ts +20 -0
  143. package/Server/Services/DockerSwarmClusterLabelRuleEngineService.ts +23 -0
  144. package/Server/Services/DockerSwarmClusterOwnerRuleEngineService.ts +23 -0
  145. package/Server/Services/ExceptionAggregationService.ts +1 -0
  146. package/Server/Services/GoogleSecOpsConnectionRunService.ts +10 -0
  147. package/Server/Services/GoogleSecOpsConnectionService.ts +12 -0
  148. package/Server/Services/HostLabelRuleEngineService.ts +13 -0
  149. package/Server/Services/HostOwnerRuleEngineService.ts +13 -0
  150. package/Server/Services/IncidentEpisodeLabelRuleEngineService.ts +21 -0
  151. package/Server/Services/IncidentEpisodeOnCallRuleEngineService.ts +21 -0
  152. package/Server/Services/IncidentEpisodeOwnerRuleEngineService.ts +21 -0
  153. package/Server/Services/IncidentEpisodePrivacyRuleEngineService.ts +21 -0
  154. package/Server/Services/IncidentGroupingEngineService.ts +64 -13
  155. package/Server/Services/IncidentLabelRuleEngineService.ts +64 -9
  156. package/Server/Services/IncidentOnCallRuleEngineService.ts +64 -10
  157. package/Server/Services/IncidentOwnerRuleEngineService.ts +64 -10
  158. package/Server/Services/IncidentPrivacyRuleEngineService.ts +64 -10
  159. package/Server/Services/IncidentReminderRuleService.ts +66 -34
  160. package/Server/Services/IncidentSlaRuleService.ts +55 -12
  161. package/Server/Services/IncomingCallPolicyLabelRuleEngineService.ts +20 -0
  162. package/Server/Services/IncomingCallPolicyOwnerRuleEngineService.ts +20 -0
  163. package/Server/Services/IncomingCallPolicyPhoneNumberService.ts +184 -0
  164. package/Server/Services/IncomingCallPolicyService.ts +276 -5
  165. package/Server/Services/Index.ts +18 -0
  166. package/Server/Services/IoTFleetLabelRuleEngineService.ts +20 -0
  167. package/Server/Services/IoTFleetOwnerRuleEngineService.ts +20 -0
  168. package/Server/Services/KubernetesClusterLabelRuleEngineService.ts +23 -0
  169. package/Server/Services/KubernetesClusterOwnerRuleEngineService.ts +23 -0
  170. package/Server/Services/LabelService.ts +50 -7
  171. package/Server/Services/LogAggregationService.ts +1 -0
  172. package/Server/Services/MetricAggregationService.ts +1 -0
  173. package/Server/Services/MobilePasskeyLoginService.ts +216 -0
  174. package/Server/Services/MonitorLabelRuleEngineService.ts +20 -0
  175. package/Server/Services/MonitorOwnerRuleEngineService.ts +20 -0
  176. package/Server/Services/MonitorService.ts +51 -1
  177. package/Server/Services/MonitorTemplateService.ts +91 -29
  178. package/Server/Services/NetworkDeviceAutoImportRuleEngineService.ts +134 -1
  179. package/Server/Services/NetworkDeviceAutoImportRuleService.ts +269 -3
  180. package/Server/Services/NetworkDeviceLabelRuleEngineService.ts +26 -0
  181. package/Server/Services/NetworkDeviceOwnerRuleEngineService.ts +25 -0
  182. package/Server/Services/NetworkDeviceService.ts +2 -0
  183. package/Server/Services/NetworkSiteAssignmentRuleService.ts +90 -1
  184. package/Server/Services/NetworkSiteService.ts +242 -29
  185. package/Server/Services/NetworkSiteTypeService.ts +177 -77
  186. package/Server/Services/OnCallDutyPolicyLabelRuleEngineService.ts +20 -0
  187. package/Server/Services/OnCallDutyPolicyOwnerRuleEngineService.ts +20 -0
  188. package/Server/Services/OnCallDutyPolicyScheduleLabelRuleEngineService.ts +22 -0
  189. package/Server/Services/OnCallDutyPolicyScheduleOwnerRuleEngineService.ts +22 -0
  190. package/Server/Services/OpenTelemetryIngestService.ts +17 -0
  191. package/Server/Services/PayAsYouGoBillingService.ts +446 -0
  192. package/Server/Services/PodmanHostLabelRuleEngineService.ts +20 -0
  193. package/Server/Services/PodmanHostOwnerRuleEngineService.ts +20 -0
  194. package/Server/Services/ProjectCallSMSConfigService.ts +156 -7
  195. package/Server/Services/ProjectService.ts +46 -2
  196. package/Server/Services/ProxmoxClusterLabelRuleEngineService.ts +25 -0
  197. package/Server/Services/ProxmoxClusterOwnerRuleEngineService.ts +25 -0
  198. package/Server/Services/RumApplicationLabelRuleEngineService.ts +22 -0
  199. package/Server/Services/RumApplicationOwnerRuleEngineService.ts +22 -0
  200. package/Server/Services/RunbookLabelRuleEngineService.ts +20 -0
  201. package/Server/Services/RunbookOwnerRuleEngineService.ts +20 -0
  202. package/Server/Services/RunbookRuleEngineService.ts +18 -1
  203. package/Server/Services/ScheduledMaintenanceLabelRuleEngineService.ts +62 -10
  204. package/Server/Services/ScheduledMaintenanceOwnerRuleEngineService.ts +62 -11
  205. package/Server/Services/ScheduledMaintenanceReminderRuleService.ts +48 -18
  206. package/Server/Services/ServerlessFunctionInstanceService.ts +71 -0
  207. package/Server/Services/ServerlessFunctionLabelRuleEngineService.ts +25 -0
  208. package/Server/Services/ServerlessFunctionOwnerRuleEngineService.ts +25 -0
  209. package/Server/Services/ServiceLabelRuleEngineService.ts +20 -0
  210. package/Server/Services/ServiceLevelObjectiveBurnRateRuleService.ts +675 -12
  211. package/Server/Services/ServiceLevelObjectiveService.ts +55 -13
  212. package/Server/Services/ServiceOwnerRuleEngineService.ts +20 -0
  213. package/Server/Services/StatusPageLabelRuleEngineService.ts +20 -0
  214. package/Server/Services/StatusPageMonitorRuleEngineService.ts +31 -2
  215. package/Server/Services/StatusPageMonitorRuleService.ts +34 -1
  216. package/Server/Services/StatusPageOwnerRuleEngineService.ts +20 -0
  217. package/Server/Services/TeamMemberService.ts +96 -36
  218. package/Server/Services/TelemetryIngestionKeyService.ts +80 -10
  219. package/Server/Services/TelemetryUsageBillingService.ts +95 -5
  220. package/Server/Services/TraceAggregationService.ts +5 -4
  221. package/Server/Services/UserService.ts +23 -50
  222. package/Server/Services/UserWebAuthnService.ts +592 -133
  223. package/Server/Services/VMwareResourceService.ts +648 -0
  224. package/Server/Services/VMwareVCenterFeedService.ts +99 -0
  225. package/Server/Services/VMwareVCenterLabelRuleEngineService.ts +265 -0
  226. package/Server/Services/VMwareVCenterLabelRuleService.ts +14 -0
  227. package/Server/Services/VMwareVCenterOwnerRuleEngineService.ts +277 -0
  228. package/Server/Services/VMwareVCenterOwnerRuleService.ts +14 -0
  229. package/Server/Services/VMwareVCenterOwnerTeamService.ts +139 -0
  230. package/Server/Services/VMwareVCenterOwnerUserService.ts +133 -0
  231. package/Server/Services/VMwareVCenterService.ts +629 -0
  232. package/Server/Services/WorkflowLabelRuleEngineService.ts +20 -0
  233. package/Server/Services/WorkflowOwnerRuleEngineService.ts +20 -0
  234. package/Server/Services/WorkflowVariableService.ts +203 -0
  235. package/Server/Static/Brand/README.md +10 -5
  236. package/Server/Static/Brand/favicons/oneuptime-up-v1.svg +8 -0
  237. package/Server/Types/Billing/MeteredPlan/ActiveMonitoringMeteredPlan.ts +17 -0
  238. package/Server/Types/Billing/MeteredPlan/TelemetryMeteredPlan.ts +31 -1
  239. package/Server/Types/Database/QueryHelper.ts +88 -0
  240. package/Server/Utils/AI/Toolbox/SloTools.ts +19 -2
  241. package/Server/Utils/CodeRepository/GitHub/GitHub.ts +7 -0
  242. package/Server/Utils/CodeRepository/GitHub/GitHubAgentTaskTrigger.ts +341 -0
  243. package/Server/Utils/CodeRepository/GitHub/GitHubCommandAuthorizer.ts +274 -0
  244. package/Server/Utils/CodeRepository/GitHub/GitHubCommandParser.ts +375 -0
  245. package/Server/Utils/CodeRepository/GitHub/GitHubConversation.ts +846 -0
  246. package/Server/Utils/CodeRepository/GitHub/GitHubReplyComposer.ts +360 -0
  247. package/Server/Utils/CodeRepository/GitHub/GitHubRunReply.ts +421 -0
  248. package/Server/Utils/CodeRepository/GitHub/GitHubWebhookEvents.ts +140 -0
  249. package/Server/Utils/CodeRepository/GitHub/GitHubWebhookHandler.ts +906 -0
  250. package/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.ts +143 -0
  251. package/Server/Utils/DataSource/EgressGuard.ts +160 -15
  252. package/Server/Utils/Database/TruncateColumnValue.ts +1 -1
  253. package/Server/Utils/EmailRollup/EmailRollupFlushRunner.ts +2 -0
  254. package/Server/Utils/EmailRollup/EmailRollupRenderer.ts +33 -0
  255. package/Server/Utils/EmailRollup/EmailRollupWriter.ts +44 -0
  256. package/Server/Utils/IncomingCallPhoneNumber.ts +29 -16
  257. package/Server/Utils/LogRedaction.ts +2 -1
  258. package/Server/Utils/Monitor/MonitorAlert.ts +1 -1
  259. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +302 -3
  260. package/Server/Utils/Monitor/MonitorMaintenanceSuppression.ts +26 -4
  261. package/Server/Utils/Monitor/MonitorResourceContext.ts +5 -3
  262. package/Server/Utils/Monitor/MonitorStepResourceIdentity.ts +14 -0
  263. package/Server/Utils/Monitor/MonitorTemplateUtil.ts +1 -0
  264. package/Server/Utils/Monitor/SeriesResourceLabels.ts +26 -0
  265. package/Server/Utils/Monitor/SeriesResourceLinker.ts +28 -5
  266. package/Server/Utils/Profiling.ts +13 -1
  267. package/Server/Utils/ResourceFeed/ResourceFeedUtil.ts +3 -3
  268. package/Server/Utils/Rules/MonitorRuleCriteriaCache.ts +38 -0
  269. package/Server/Utils/SecurityEvent/ConnectorErrorMessage.ts +14 -9
  270. package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.ts +116 -84
  271. package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.ts +594 -195
  272. package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsRunExecutor.ts +466 -0
  273. package/Server/Utils/SessionReplay/SessionReplayReadService.ts +498 -0
  274. package/Server/Utils/Telemetry/EntityRegistry.ts +5 -0
  275. package/Server/Utils/Telemetry/ResourceHeartbeat.ts +2 -1
  276. package/Server/Utils/Telemetry/TelemetryEntity.ts +181 -1
  277. package/Server/Utils/Telemetry/VMwareSnapshotScan.ts +1352 -0
  278. package/Server/Utils/Telemetry.ts +3 -1
  279. package/Server/Utils/VendorAssets.ts +3 -0
  280. package/Tests/App/Accounts/LoginPasskey.test.tsx +529 -0
  281. package/Tests/App/Accounts/MobilePasskey.test.tsx +620 -0
  282. package/Tests/App/Accounts/PasswordRequirements.test.tsx +254 -0
  283. package/Tests/App/Accounts/RegisterPassword.test.tsx +553 -0
  284. package/Tests/App/Dashboard/AIInsightViewTriage.test.tsx +348 -0
  285. package/Tests/App/Dashboard/ArchiveResourceCard.test.tsx +476 -0
  286. package/Tests/App/Dashboard/AskAiAvailability.test.ts +224 -0
  287. package/Tests/App/Dashboard/AskAiDisabled.test.tsx +432 -0
  288. package/Tests/App/Dashboard/BackupCodes.test.tsx +414 -0
  289. package/Tests/App/Dashboard/BillingPaidUsageFlow.test.tsx +530 -0
  290. package/Tests/App/Dashboard/BillingPaymentMethodForm.test.tsx +195 -0
  291. package/Tests/App/Dashboard/BreadcrumbCoverage.test.tsx +199 -0
  292. package/Tests/App/Dashboard/CloudDocumentationCard.test.tsx +223 -0
  293. package/Tests/App/Dashboard/CloudDocumentationMarkdown.test.ts +215 -0
  294. package/Tests/App/Dashboard/CloudFleetSummary.test.ts +183 -0
  295. package/Tests/App/Dashboard/CloudFleetSummaryComponent.test.tsx +248 -0
  296. package/Tests/App/Dashboard/CloudResourceTelemetryScope.test.ts +215 -0
  297. package/Tests/App/Dashboard/CriteriaFilterDefaults.test.ts +3 -0
  298. package/Tests/App/Dashboard/DashboardHeaderRequests.test.tsx +441 -0
  299. package/Tests/App/Dashboard/DashboardLazyPages.test.tsx +883 -0
  300. package/Tests/App/Dashboard/DashboardPersistentSideMenuLayouts.test.tsx +702 -0
  301. package/Tests/App/Dashboard/DashboardToolbarPopupLayering.test.tsx +566 -0
  302. package/Tests/App/Dashboard/DiscoveryScanLivePage.test.tsx +376 -0
  303. package/Tests/App/Dashboard/DiscoveryScanLiveUpdates.test.tsx +642 -0
  304. package/Tests/App/Dashboard/DiscoveryScanProgress.test.tsx +267 -0
  305. package/Tests/App/Dashboard/DiscoveryScanWizardValidation.test.tsx +69 -231
  306. package/Tests/App/Dashboard/ExceptionsLandingRedirect.test.tsx +63 -0
  307. package/Tests/App/Dashboard/FeedOrdering.test.ts +150 -0
  308. package/Tests/App/Dashboard/GoogleSecOpsConnectionsErrors.test.tsx +423 -0
  309. package/Tests/App/Dashboard/GoogleSecOpsDiagnostics.test.tsx +724 -0
  310. package/Tests/App/Dashboard/InventoryItemSideMenu.test.tsx +155 -0
  311. package/Tests/App/Dashboard/InvestigationFeedRefresh.test.tsx +11 -0
  312. package/Tests/App/Dashboard/MonitorTemplateSyncFields.test.tsx +539 -0
  313. package/Tests/App/Dashboard/OnCallLayerCardOverrides.test.tsx +24 -7
  314. package/Tests/App/Dashboard/OnCallOverridePresentation.test.ts +496 -0
  315. package/Tests/App/Dashboard/OnCallScheduleOverrideClarity.test.tsx +677 -0
  316. package/Tests/App/Dashboard/OnCallSchedulePreviewOverrides.test.tsx +31 -3
  317. package/Tests/App/Dashboard/PaidUsageConsent.test.tsx +180 -0
  318. package/Tests/App/Dashboard/PasskeySettings.test.tsx +1364 -0
  319. package/Tests/App/Dashboard/PayAsYouGoConsentGate.test.tsx +176 -275
  320. package/Tests/App/Dashboard/PayAsYouGoNotices.test.tsx +380 -233
  321. package/Tests/App/Dashboard/ProductMenuBreadcrumbs.test.tsx +3 -23
  322. package/Tests/App/Dashboard/ResourceArchiveSettings.test.tsx +347 -0
  323. package/Tests/App/Dashboard/ResourceAuditLogsSideMenus.test.tsx +263 -0
  324. package/Tests/App/Dashboard/ResourceFeed.test.tsx +414 -17
  325. package/Tests/App/Dashboard/ResourceFeedPageWiring.test.ts +8 -1
  326. package/Tests/App/Dashboard/RumSessionReplaySideMenu.test.tsx +138 -0
  327. package/Tests/App/Dashboard/SloDashboardTemplateWidgets.test.tsx +940 -0
  328. package/Tests/App/Dashboard/TelemetryIngestionKeyEdit.test.tsx +378 -0
  329. package/Tests/App/Dashboard/TelemetryIngestionKeyForm.test.tsx +735 -0
  330. package/Tests/App/Dashboard/TwoFactorStatus.test.tsx +427 -0
  331. package/Tests/App/Dashboard/VMwareDashboardWidgets.test.ts +573 -0
  332. package/Tests/App/Dashboard/WidgetCatalog.test.ts +2 -0
  333. package/Tests/App/Dashboard/WorkflowVariablesTable.test.tsx +768 -0
  334. package/Tests/App/StatusPage/BulkAddStatusPageMonitorsModal.test.tsx +13 -3
  335. package/Tests/App/StatusPage/StatusPageOverviewLiveAndSearch.test.tsx +532 -40
  336. package/Tests/App/StatusPage/StatusPageResourceSearchBox.test.tsx +19 -2
  337. package/Tests/Fixtures/MobilePasskeyUser.json +8 -0
  338. package/Tests/Models/AnalyticsModels/RumSessionReplayColumns.test.ts +127 -4
  339. package/Tests/Models/DatabaseModels/DomainRoleTierCoverage.test.ts +5 -4
  340. package/Tests/Models/DatabaseModels/IncomingCallPolicyPhoneNumber.test.ts +362 -0
  341. package/Tests/Models/DatabaseModels/ResourceFeedModels.test.ts +25 -6
  342. package/Tests/Models/DatabaseModels/RuleBaseModel.test.ts +69 -0
  343. package/Tests/Models/DatabaseModels/StaticRuleCriteriaInheritance.test.ts +181 -0
  344. package/Tests/Models/DatabaseModels/VMwareModels.test.ts +1134 -0
  345. package/Tests/Models/RunnerVersionLabel.test.ts +4 -2
  346. package/Tests/Server/API/AIChatProvidersAIEnabled.test.ts +259 -0
  347. package/Tests/Server/API/AIChatRouteAuthorization.test.ts +31 -2
  348. package/Tests/Server/API/BillingPayAsYouGoStatus.test.ts +116 -0
  349. package/Tests/Server/API/DashboardPublicResourceListAPI.test.ts +29 -1
  350. package/Tests/Server/API/EnterpriseLicenseReportUserCount.test.ts +13 -2
  351. package/Tests/Server/API/GitHubWebhookSignature.test.ts +368 -0
  352. package/Tests/Server/API/GoogleSecOpsConnectionAPI.test.ts +248 -0
  353. package/Tests/Server/API/SessionReplayAPI.test.ts +1303 -145
  354. package/Tests/Server/API/UserTwoFactorSelfService.test.ts +407 -0
  355. package/Tests/Server/API/UserWebAuthnAuthenticationOptionsRoute.test.ts +330 -0
  356. package/Tests/Server/API/UserWebAuthnPasskeyAPI.test.ts +172 -0
  357. package/Tests/Server/API/VMwareResourceAPI.test.ts +546 -0
  358. package/Tests/Server/Infrastructure/Postgres/AddConfigurableRuleCriteriaMigration.test.ts +611 -0
  359. package/Tests/Server/Infrastructure/Postgres/UserNotificationEmailRollupMetadataMigration.test.ts +85 -0
  360. package/Tests/Server/Infrastructure/Postgres/UserNotificationEmailRollupTableMigration.test.ts +34 -3
  361. package/Tests/Server/Middleware/MultipartFormData.test.ts +61 -3
  362. package/Tests/Server/Middleware/PasskeyRateLimit.test.ts +141 -0
  363. package/Tests/Server/Middleware/TelemetryIngestionDisabled.test.ts +211 -0
  364. package/Tests/Server/Services/AlertIncidentRuleCriteriaEngineServices.test.ts +651 -0
  365. package/Tests/Server/Services/AutoImportRuleRearmRecentScans.test.ts +343 -0
  366. package/Tests/Server/Services/AutoImportRuleWriteRearmsScans.test.ts +381 -0
  367. package/Tests/Server/Services/AutoRemediationRuleEngineService.test.ts +79 -0
  368. package/Tests/Server/Services/BillingPaymentMethodServiceSync.test.ts +175 -0
  369. package/Tests/Server/Services/BillingService.test.ts +26 -11
  370. package/Tests/Server/Services/BillingServiceMeteredPaymentAuthorization.test.ts +245 -0
  371. package/Tests/Server/Services/BillingServicePaymentMethodReads.test.ts +362 -0
  372. package/Tests/Server/Services/CloudResourceInstanceStalePrune.test.ts +188 -0
  373. package/Tests/Server/Services/DatabaseServiceRelationOnlyRuleRollout.test.ts +330 -0
  374. package/Tests/Server/Services/DatabaseServiceRuleCriteriaValidation.test.ts +522 -0
  375. package/Tests/Server/Services/DiscoveryScanClaimHookFreeSafety.test.ts +16 -2
  376. package/Tests/Server/Services/GoogleSecOpsDiagnosticsModel.test.ts +153 -0
  377. package/Tests/Server/Services/HookFreeWriteLengthClamp.test.ts +13 -0
  378. package/Tests/Server/Services/IncidentSlaRuleCriteriaCorrelation.test.ts +99 -0
  379. package/Tests/Server/Services/IncomingCallPolicyPhoneNumberService.test.ts +1540 -0
  380. package/Tests/Server/Services/InventoryItemPromotionGate.test.ts +5 -0
  381. package/Tests/Server/Services/InventoryUpsertSchemaParity.test.ts +36 -0
  382. package/Tests/Server/Services/InventoryUpsertTruncationGuard.test.ts +151 -0
  383. package/Tests/Server/Services/MobilePasskeyLoginIntegration.test.ts +231 -0
  384. package/Tests/Server/Services/MobilePasskeyLoginService.test.ts +315 -0
  385. package/Tests/Server/Services/MonitorPayAsYouGoBilling.test.ts +302 -0
  386. package/Tests/Server/Services/MonitorTemplateServiceFieldSync.test.ts +994 -0
  387. package/Tests/Server/Services/NetworkDeviceAutoImportRuleService.test.ts +22 -0
  388. package/Tests/Server/Services/NetworkDeviceRuleEngines.test.ts +179 -2
  389. package/Tests/Server/Services/NetworkSiteAssignmentRuleService.test.ts +170 -0
  390. package/Tests/Server/Services/NetworkSiteService.test.ts +559 -53
  391. package/Tests/Server/Services/NetworkSiteTypeService.test.ts +122 -55
  392. package/Tests/Server/Services/OpenTelemetryResourceRetentionMemo.test.ts +7 -1
  393. package/Tests/Server/Services/PayAsYouGoBillingService.test.ts +857 -0
  394. package/Tests/Server/Services/PayAsYouGoStripeRateLimitBudget.test.ts +379 -0
  395. package/Tests/Server/Services/ProjectServiceExtendTrial.test.ts +99 -1
  396. package/Tests/Server/Services/ProjectServiceSubscriptionSeats.test.ts +134 -0
  397. package/Tests/Server/Services/ReminderRuleCriteria.test.ts +173 -0
  398. package/Tests/Server/Services/ResourceFeedServices.test.ts +3 -2
  399. package/Tests/Server/Services/ResourceRuleCriteriaEngineServices.test.ts +170 -0
  400. package/Tests/Server/Services/ResourceUpdateLastSeenLivenessFallback.test.ts +32 -6
  401. package/Tests/Server/Services/ScheduledMaintenanceRuleCriteriaCorrelation.test.ts +154 -0
  402. package/Tests/Server/Services/SecurityEventLastErrorColumnWidth.test.ts +363 -175
  403. package/Tests/Server/Services/ServerlessFunctionInstanceStalePrune.test.ts +200 -0
  404. package/Tests/Server/Services/ServiceLevelObjectiveBurnRateRuleService.test.ts +2450 -470
  405. package/Tests/Server/Services/ServiceLevelObjectiveService.test.ts +464 -121
  406. package/Tests/Server/Services/StaticRuleCriteriaRuntimeCoverage.test.ts +281 -0
  407. package/Tests/Server/Services/StatusPageMonitorRuleEngineService.test.ts +132 -0
  408. package/Tests/Server/Services/StatusPageMonitorRuleHooks.test.ts +159 -14
  409. package/Tests/Server/Services/TeamMemberAutoAcceptInvitation.test.ts +3 -0
  410. package/Tests/Server/Services/TeamMemberBillingSeatSync.test.ts +762 -0
  411. package/Tests/Server/Services/TeamMemberInviteRegistrationToken.test.ts +3 -0
  412. package/Tests/Server/Services/TelemetryIngestionKeyPolicyResolution.test.ts +72 -0
  413. package/Tests/Server/Services/TelemetryIngestionKeyValidation.test.ts +17 -4
  414. package/Tests/Server/Services/TelemetryPayAsYouGoBilling.test.ts +355 -0
  415. package/Tests/Server/Services/TraceAnalyticsResourceDisplayNames.test.ts +41 -4
  416. package/Tests/Server/Services/UserEmailChangePasswordResetExpiry.test.ts +6 -8
  417. package/Tests/Server/Services/UserNotificationEmailRollupModels.test.ts +37 -0
  418. package/Tests/Server/Services/UserTwoFactorAuthAdmin.test.ts +203 -34
  419. package/Tests/Server/Services/UserWebAuthnPasskeyIntegration.test.ts +401 -0
  420. package/Tests/Server/Services/UserWebAuthnPasskeyService.test.ts +769 -0
  421. package/Tests/Server/Services/UserWebAuthnUserVerification.test.ts +649 -0
  422. package/Tests/Server/Services/VMwareResourceService.test.ts +945 -0
  423. package/Tests/Server/Services/VMwareVCenterService.test.ts +722 -0
  424. package/Tests/Server/Services/WorkflowVariableRenameUniqueness.test.ts +842 -0
  425. package/Tests/Server/TestingUtils/PasskeyHttpIntegration.ts +102 -0
  426. package/Tests/Server/TestingUtils/SecurityKey.ts +464 -0
  427. package/Tests/Server/TestingUtils/WebAuthnServiceMocks.ts +60 -0
  428. package/Tests/Server/Types/Billing/PayAsYouGoMeteredAuthorization.test.ts +573 -0
  429. package/Tests/Server/Types/Database/Permissions/ProjectCustomerSupportAccessColumn.test.ts +261 -0
  430. package/Tests/Server/Types/Database/Permissions/WorkflowVariableColumnPermission.test.ts +95 -0
  431. package/Tests/Server/Types/Database/QueryHelperJsonArrayObjects.test.ts +128 -0
  432. package/Tests/Server/Utils/AI/SloTools.test.ts +190 -1
  433. package/Tests/Server/Utils/AI/Toolbox/AIToolbox.test.ts +461 -0
  434. package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +25 -0
  435. package/Tests/Server/Utils/Billing/BillingFailureNoticeThrottle.test.ts +359 -0
  436. package/Tests/Server/Utils/CodeRepository/GitHub/GitHubWebhookEvents.test.ts +318 -0
  437. package/Tests/Server/Utils/CodeRepository/GitHubAgentTaskTrigger.test.ts +2101 -0
  438. package/Tests/Server/Utils/CodeRepository/GitHubCommandAuthorizer.test.ts +883 -0
  439. package/Tests/Server/Utils/CodeRepository/GitHubCommandParser.test.ts +1474 -0
  440. package/Tests/Server/Utils/CodeRepository/GitHubConversation.test.ts +1526 -0
  441. package/Tests/Server/Utils/CodeRepository/GitHubReplyComposer.test.ts +1389 -0
  442. package/Tests/Server/Utils/CodeRepository/GitHubRunReply.test.ts +1707 -0
  443. package/Tests/Server/Utils/CodeRepository/GitHubWebhookHandler.test.ts +2202 -0
  444. package/Tests/Server/Utils/CustomField/CustomFieldDefinitionMappingHooks.test.ts +220 -0
  445. package/Tests/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.test.ts +345 -0
  446. package/Tests/Server/Utils/Dashboard/SloTemplatePublicDashboard.test.ts +1043 -0
  447. package/Tests/Server/Utils/DataSource/EgressGuard.test.ts +896 -0
  448. package/Tests/Server/Utils/EmailRollup/EmailRollupMetadataIntegration.test.ts +246 -0
  449. package/Tests/Server/Utils/EmailRollup/EmailRollupRenderer.test.ts +299 -1
  450. package/Tests/Server/Utils/EmailRollup/EmailRollupTestHarness.ts +6 -0
  451. package/Tests/Server/Utils/EmailRollup/EmailRollupWriter.test.ts +185 -0
  452. package/Tests/Server/Utils/Execute.test.ts +302 -0
  453. package/Tests/Server/Utils/GracefulShutdown.test.ts +565 -0
  454. package/Tests/Server/Utils/IncomingCallPhoneNumber.test.ts +36 -20
  455. package/Tests/Server/Utils/LogRedaction.test.ts +33 -0
  456. package/Tests/Server/Utils/Monitor/CapturedMetricAttributeUtil.test.ts +2 -1
  457. package/Tests/Server/Utils/Monitor/Criteria/TelemetryStepMetricAlias.test.ts +10 -0
  458. package/Tests/Server/Utils/Monitor/MonitorAlertResourceLinking.test.ts +66 -1
  459. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluator.test.ts +362 -0
  460. package/Tests/Server/Utils/Monitor/MonitorDependencySuppressionCreatorSkip.test.ts +1 -0
  461. package/Tests/Server/Utils/Monitor/MonitorIncidentResourceLinking.test.ts +60 -1
  462. package/Tests/Server/Utils/Monitor/MonitorMaintenanceSuppression.test.ts +60 -0
  463. package/Tests/Server/Utils/Monitor/MonitorRootCauseTargetResolution.test.ts +423 -0
  464. package/Tests/Server/Utils/Monitor/MonitorStepResourceIdentity.test.ts +6 -0
  465. package/Tests/Server/Utils/Monitor/MonitorSummaryPersistence.test.ts +1 -0
  466. package/Tests/Server/Utils/Monitor/PerSeriesSecondEntityAlerting.test.ts +1 -0
  467. package/Tests/Server/Utils/Monitor/SeriesResourceLinker.test.ts +46 -2
  468. package/Tests/Server/Utils/Monitor/SloTemplateMetricEmitterContract.test.ts +750 -0
  469. package/Tests/Server/Utils/ProductAnalytics.test.ts +404 -0
  470. package/Tests/Server/Utils/Profiling.test.ts +366 -0
  471. package/Tests/Server/Utils/Rules/MonitorRuleCriteriaCache.test.ts +99 -0
  472. package/Tests/Server/Utils/SecurityEvent/ConnectorErrorMessage.test.ts +81 -45
  473. package/Tests/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsRunExecutor.test.ts +711 -0
  474. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsClickhouseIntegration.test.ts +223 -0
  475. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsClient.test.ts +69 -1
  476. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsDiagnostics.test.ts +838 -0
  477. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsErrorHandling.test.ts +241 -28
  478. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsHttpIntegration.test.ts +651 -0
  479. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPoller.test.ts +11 -0
  480. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerFailureTaxonomy.test.ts +13 -12
  481. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerHardening.test.ts +112 -127
  482. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsRequestContract.test.ts +12 -18
  483. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsResponseParsing.test.ts +43 -0
  484. package/Tests/Server/Utils/SessionReplay/SessionReplayReadServiceQueries.test.ts +558 -0
  485. package/Tests/Server/Utils/Telemetry/TelemetryEntity.test.ts +580 -0
  486. package/Tests/Server/Utils/Telemetry/VMwareSnapshotScan.test.ts +1748 -0
  487. package/Tests/Server/Utils/VendorAssets.test.ts +17 -0
  488. package/Tests/Types/Cloud/CloudPlatform.test.ts +574 -0
  489. package/Tests/Types/Dashboard/DashboardTemplateEditorialInvariants.test.ts +659 -0
  490. package/Tests/Types/Dashboard/DashboardTemplateInvariants.test.ts +97 -0
  491. package/Tests/Types/Dashboard/SloDashboardTemplate.test.ts +1179 -0
  492. package/Tests/Types/Database/AccessControl/OwnedThrough.test.ts +136 -0
  493. package/Tests/Types/Monitor/MetricAndProfileDefaultCriteria.test.ts +2 -0
  494. package/Tests/Types/Monitor/MonitorCriteriaMetricVariables.test.ts +9 -0
  495. package/Tests/Types/Monitor/MonitorStepDefaultTelemetryConfig.test.ts +1 -0
  496. package/Tests/Types/Monitor/MonitorStepInfraMonitorUtils.test.ts +53 -8
  497. package/Tests/Types/Monitor/MonitorStepInfrastructureMonitors.test.ts +97 -0
  498. package/Tests/Types/Monitor/MonitorStepMetricViewConfigUtil.test.ts +1 -0
  499. package/Tests/Types/Monitor/MonitorStepTemplateSyncFields.test.ts +107 -0
  500. package/Tests/Types/Monitor/MonitorTemplateSyncField.test.ts +172 -0
  501. package/Tests/Types/Monitor/MonitorType.test.ts +1 -0
  502. package/Tests/Types/Monitor/MonitorTypeKeywords.test.ts +2 -0
  503. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationAlertDebuggability.test.ts +1 -0
  504. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationCatalog.test.ts +9 -0
  505. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationCoverage.test.ts +1 -1
  506. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationNotificationMode.test.ts +1 -1
  507. package/Tests/Types/Monitor/SeriesContext/SeriesDebugHints.test.ts +131 -0
  508. package/Tests/Types/Monitor/SeriesContext/SeriesLabelDisplay.test.ts +10 -0
  509. package/Tests/Types/Monitor/VMwareAlertTemplates.test.ts +1608 -0
  510. package/Tests/Types/Monitor/VMwareMetricCatalog.test.ts +488 -0
  511. package/Tests/Types/OnCallDutyPolicy/OverrideProvenance.test.ts +295 -0
  512. package/Tests/Types/Rum/SessionReplayApiContracts.test.ts +180 -1
  513. package/Tests/Types/Runner/RunnerLiveStatus.test.ts +2 -2
  514. package/Tests/Types/SignupPassword.test.ts +205 -0
  515. package/Tests/Types/StatusPage/StatusPageLanguage.test.ts +44 -0
  516. package/Tests/Types/SupportedLanguages.test.ts +119 -0
  517. package/Tests/Types/Telemetry/EntityTypeGroups.test.ts +19 -0
  518. package/Tests/UI/ColorPickerBuild.test.ts +86 -0
  519. package/Tests/UI/Components/AiInvestigationSettingsCard.test.tsx +13 -6
  520. package/Tests/UI/Components/Alert.test.tsx +532 -93
  521. package/Tests/UI/Components/Breadcrumbs.test.tsx +32 -0
  522. package/Tests/UI/Components/Feed.test.tsx +241 -0
  523. package/Tests/UI/Components/Forms/BasicFormStepNavigation.test.tsx +368 -0
  524. package/Tests/UI/Components/Forms/FormSummary.test.tsx +454 -0
  525. package/Tests/UI/Components/Forms/ModelFormJsonColumns.test.tsx +290 -0
  526. package/Tests/UI/Components/Forms/ModelFormPreservesUserInput.test.tsx +937 -0
  527. package/Tests/UI/Components/Forms/ModelFormRuleCriteria.test.tsx +331 -0
  528. package/Tests/UI/Components/Forms/ModelFormWriteOnlyColumnSelect.test.tsx +230 -0
  529. package/Tests/UI/Components/Forms/ValidationFormSteps.test.ts +3 -4
  530. package/Tests/UI/Components/IconOpticalSize.test.tsx +79 -31
  531. package/Tests/UI/Components/LabelRuleImportExport.test.tsx +907 -0
  532. package/Tests/UI/Components/ModelTable/ModelTableCreateAfterEdit.test.tsx +514 -0
  533. package/Tests/UI/Components/ModelTable/ModelTableWrapContent.test.tsx +6 -6
  534. package/Tests/UI/Components/MonitorGraphs/Uptime.test.tsx +301 -0
  535. package/Tests/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.test.ts +2 -1
  536. package/Tests/UI/Components/NavBar.test.tsx +91 -2
  537. package/Tests/UI/Components/PencilButton.test.tsx +211 -0
  538. package/Tests/UI/Components/RuleCriteria/RuleCriteriaBuilder.test.tsx +510 -0
  539. package/Tests/UI/Components/RuleCriteria/RuleCriteriaModelTable.test.tsx +350 -0
  540. package/Tests/UI/Components/TableSortAndMobileColumns.test.tsx +50 -0
  541. package/Tests/UI/Components/Telemetry/TelemetryRetentionConfigForm.test.tsx +329 -0
  542. package/Tests/UI/Components/TooltipLazyContent.test.tsx +373 -0
  543. package/Tests/UI/Components/useFeedItems.test.tsx +107 -0
  544. package/Tests/UI/MicrosoftTeams/MicrosoftTeamsMessagingEndpointGuidance.test.tsx +43 -12
  545. package/Tests/UI/ReactDomBuild.test.ts +104 -0
  546. package/Tests/UI/ReactRouterSingletonBuild.test.ts +11 -1
  547. package/Tests/UI/Rum/ChunkLoader.test.ts +401 -0
  548. package/Tests/UI/Rum/RecordingHealthCard.test.tsx +1 -1
  549. package/Tests/UI/Rum/RecordingHealthStrip.test.tsx +7 -1
  550. package/Tests/UI/Rum/ReplayCorrelationPanel.test.tsx +2 -0
  551. package/Tests/UI/Rum/ReplayEngine.test.ts +1069 -5
  552. package/Tests/UI/Rum/ReplayHeader.test.tsx +579 -2
  553. package/Tests/UI/Rum/ReplayPlayerChrome.test.tsx +23 -5
  554. package/Tests/UI/Rum/ReplayRail.test.tsx +143 -1
  555. package/Tests/UI/Rum/ReplayRenderBudget.test.tsx +338 -0
  556. package/Tests/UI/Rum/ReplaySignals.test.ts +179 -0
  557. package/Tests/UI/Rum/ReplayStage.test.tsx +210 -3
  558. package/Tests/UI/Rum/ReplayTimeline.test.tsx +180 -1
  559. package/Tests/UI/Rum/ReplayUserSessionsMenu.test.tsx +405 -0
  560. package/Tests/UI/Rum/SessionReplayEmptyState.test.tsx +13 -16
  561. package/Tests/UI/Rum/SessionReplayFacets.test.tsx +199 -0
  562. package/Tests/UI/Rum/SessionReplaySearchBar.test.tsx +58 -21
  563. package/Tests/UI/Rum/SessionReplaySetupGuide.test.tsx +2 -0
  564. package/Tests/UI/Rum/SessionReplayTable.test.tsx +717 -4
  565. package/Tests/UI/Rum/SessionReplayUsersPage.test.tsx +435 -0
  566. package/Tests/UI/Rum/SessionReplayUsersTable.test.tsx +577 -0
  567. package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +149 -1
  568. package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +30 -1
  569. package/Tests/UI/Utils/DashboardStackingLayers.test.ts +197 -0
  570. package/Tests/UI/Utils/LabelRuleImportExport.test.ts +738 -0
  571. package/Tests/UI/Utils/ModelAPITenantHeader.test.ts +109 -2
  572. package/Tests/UI/Utils/Navigation.test.ts +30 -0
  573. package/Tests/UI/Utils/UseDashboardGridDnd.test.tsx +7 -2
  574. package/Tests/UI/Utils/UserEmailRegistrationStatus.test.tsx +295 -0
  575. package/Tests/UI/Utils/WebAuthn.test.ts +265 -0
  576. package/Tests/Utils/Dashboard/Components/DashboardComponentDefaults.test.ts +5 -0
  577. package/Tests/Utils/Dashboard/Components/DashboardVMwareListComponents.test.ts +548 -0
  578. package/Tests/Utils/Dashboard/DashboardResourceListSharedArgs.test.ts +17 -3
  579. package/Tests/Utils/Dashboard/SloTemplateVariableScoping.test.ts +880 -0
  580. package/Tests/Utils/LabelRuleImportExport.test.ts +741 -0
  581. package/Tests/Utils/Monitor/MonitorSummarySnapshotUtil.test.ts +1 -0
  582. package/Tests/Utils/Monitor/MonitorTemplateSyncUtil.test.ts +590 -0
  583. package/Tests/Utils/NetworkDiscovery/AutoImportRuleMatcher.test.ts +93 -0
  584. package/Tests/Utils/NetworkSite/CidrMatchUtil.test.ts +138 -0
  585. package/Tests/Utils/NetworkSite/TypeHierarchyUtil.test.ts +174 -0
  586. package/Tests/Utils/RequestFailedDetails.test.ts +325 -0
  587. package/Tests/Utils/Rules/RuleCriteriaMatcher.test.ts +1424 -0
  588. package/Tests/Utils/Rum/SessionIdentity.test.ts +72 -0
  589. package/Tests/Utils/Slo/SloBurnRateRuleState.test.ts +374 -28
  590. package/Tests/Utils/Telemetry/CloudInstanceIdentity.test.ts +255 -0
  591. package/Tests/Utils/Telemetry/EntityRelationship.test.ts +167 -0
  592. package/Tests/Utils/Telemetry/OriginAllowList.test.ts +118 -0
  593. package/Tests/Utils/WebAuthnTestUtil.ts +79 -0
  594. package/Tests/__mocks__/simplewebauthn.js +33 -4
  595. package/Types/AI/CodeFixTaskContext.ts +122 -0
  596. package/Types/AI/CodeFixTaskType.ts +62 -0
  597. package/Types/Accounts/AccountsLanguage.ts +1 -0
  598. package/Types/AdminDashboard/AdminDashboardLanguage.ts +1 -0
  599. package/Types/Calendar/CalendarEvent.ts +16 -0
  600. package/Types/Cloud/CloudPlatform.ts +372 -0
  601. package/Types/CodeRepository/GitHubCommand.ts +69 -0
  602. package/Types/Dashboard/DashboardComponentType.ts +2 -0
  603. package/Types/Dashboard/DashboardComponents/ComponentArgument.ts +1 -0
  604. package/Types/Dashboard/DashboardComponents/DashboardVMwareHostListComponent.ts +23 -0
  605. package/Types/Dashboard/DashboardComponents/DashboardVMwareVirtualMachineListComponent.ts +30 -0
  606. package/Types/Dashboard/DashboardLanguage.ts +1 -0
  607. package/Types/Dashboard/DashboardTemplates.ts +779 -0
  608. package/Types/Docs/DocsLanguage.ts +1 -0
  609. package/Types/Exception/EgressGuardException.ts +65 -0
  610. package/Types/Icon/IconProp.ts +1 -0
  611. package/Types/Monitor/MetricMonitor/MetricMonitorResponse.ts +36 -0
  612. package/Types/Monitor/MonitorCriteriaInstance.ts +1 -0
  613. package/Types/Monitor/MonitorStep.ts +78 -17
  614. package/Types/Monitor/MonitorStepMetricViewConfigUtil.ts +1 -0
  615. package/Types/Monitor/MonitorStepVMwareMonitor.ts +127 -0
  616. package/Types/Monitor/MonitorTemplateSyncField.ts +291 -0
  617. package/Types/Monitor/MonitorType.ts +24 -0
  618. package/Types/Monitor/Recommendation/MonitorRecommendationCatalog.ts +33 -0
  619. package/Types/Monitor/Recommendation/MonitorRecommendationTypes.ts +2 -0
  620. package/Types/Monitor/Recommendation/MonitorRecommendationUtil.ts +3 -0
  621. package/Types/Monitor/SeriesContext/SeriesDebugHints.ts +158 -0
  622. package/Types/Monitor/SeriesContext/SeriesLabelDisplay.ts +61 -0
  623. package/Types/Monitor/VMwareAlertTemplates.ts +1070 -0
  624. package/Types/Monitor/VMwareMetricCatalog.ts +914 -0
  625. package/Types/OnCallDutyPolicy/ScheduleShiftUtil.ts +29 -0
  626. package/Types/OnCallDutyPolicy/UserOverrideUtil.ts +16 -0
  627. package/Types/Password.ts +84 -5
  628. package/Types/Permission.ts +265 -0
  629. package/Types/Probe/RequestFailedDetails.ts +7 -0
  630. package/Types/Rules/RuleCriteria.ts +51 -0
  631. package/Types/Rules/RuleCriteriaFieldRegistry.ts +437 -0
  632. package/Types/Rum/SessionReplay.ts +75 -0
  633. package/Types/Rum/SessionReplayApi.ts +93 -0
  634. package/Types/Runner/RunnerLiveStatus.ts +1 -1
  635. package/Types/SecurityEvent/GoogleSecOpsDiagnostics.ts +56 -0
  636. package/Types/StatusPage/StatusPageLanguage.ts +1 -0
  637. package/Types/Telemetry/EntityType.ts +21 -0
  638. package/Types/Telemetry/ServiceType.ts +1 -0
  639. package/Typings/Index.d.ts +63 -0
  640. package/UI/Components/Alerts/Alert.tsx +102 -72
  641. package/UI/Components/Breadcrumbs/Breadcrumbs.tsx +2 -2
  642. package/UI/Components/Button/Button.tsx +4 -1
  643. package/UI/Components/Calendar/Calendar.css +78 -0
  644. package/UI/Components/Calendar/Calendar.tsx +28 -3
  645. package/UI/Components/Dropdown/Dropdown.tsx +2 -0
  646. package/UI/Components/Feed/Feed.tsx +15 -0
  647. package/UI/Components/Feed/useFeedItems.ts +227 -0
  648. package/UI/Components/Forms/BasicForm.tsx +86 -23
  649. package/UI/Components/Forms/Fields/ColorPicker.tsx +2 -1
  650. package/UI/Components/Forms/Fields/FormField.tsx +16 -3
  651. package/UI/Components/Forms/ModelForm.tsx +239 -6
  652. package/UI/Components/Forms/Types/Field.ts +8 -1
  653. package/UI/Components/Graphs/DayUptimeGraph.tsx +8 -10
  654. package/UI/Components/Icon/Icon.tsx +19 -35
  655. package/UI/Components/Input/Input.tsx +42 -7
  656. package/UI/Components/LabelRule/ImportLabelRulesModal.tsx +441 -0
  657. package/UI/Components/LabelRule/LabelRuleTable.tsx +142 -0
  658. package/UI/Components/ModelTable/BaseModelTable.tsx +22 -5
  659. package/UI/Components/ModelTable/ModelTable.tsx +33 -6
  660. package/UI/Components/MonitorGraphs/Uptime.tsx +17 -27
  661. package/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.ts +3 -1
  662. package/UI/Components/Navbar/NavBar.tsx +1 -0
  663. package/UI/Components/RuleCriteria/RuleCriteriaBuilder.tsx +735 -0
  664. package/UI/Components/RuleCriteria/RuleCriteriaModelForm.tsx +209 -0
  665. package/UI/Components/RuleCriteria/RuleCriteriaModelTable.tsx +205 -0
  666. package/UI/Components/RuleCriteria/RuleCriteriaSummary.tsx +123 -0
  667. package/UI/Components/Table/Table.tsx +3 -0
  668. package/UI/Components/Table/TableBody.tsx +3 -0
  669. package/UI/Components/Table/TableRow.tsx +8 -2
  670. package/UI/Components/Telemetry/TelemetryRetentionConfigForm.tsx +14 -2
  671. package/UI/Components/Tooltip/Tooltip.tsx +52 -6
  672. package/UI/Styles/Theme.css +1 -0
  673. package/UI/Utils/DashboardStackingLayers.ts +85 -0
  674. package/UI/Utils/LabelRuleImportExport.ts +418 -0
  675. package/UI/Utils/ModelAPI/ModelAPI.ts +4 -3
  676. package/UI/Utils/Navigation.ts +1 -2
  677. package/UI/Utils/TelemetryService.ts +4 -3
  678. package/UI/Utils/UseDashboardGridDnd.ts +4 -1
  679. package/UI/Utils/UserEmailRegistrationStatus.ts +33 -5
  680. package/UI/Utils/WebAuthn.ts +220 -0
  681. package/UI/esbuild-config.js +50 -0
  682. package/Utils/API.ts +34 -0
  683. package/Utils/Dashboard/Components/DashboardVMwareHostListComponent.ts +38 -0
  684. package/Utils/Dashboard/Components/DashboardVMwareResourceListShared.ts +70 -0
  685. package/Utils/Dashboard/Components/DashboardVMwareVirtualMachineListComponent.ts +76 -0
  686. package/Utils/Dashboard/Components/Index.ts +16 -0
  687. package/Utils/Faker.ts +93 -6
  688. package/Utils/LabelRuleImportExport.ts +789 -0
  689. package/Utils/Monitor/MonitorTemplateSyncUtil.ts +164 -0
  690. package/Utils/NetworkDiscovery/AutoImportRuleMatcher.ts +108 -8
  691. package/Utils/NetworkDiscovery/DiscoveryScanStatus.ts +8 -0
  692. package/Utils/NetworkSite/CidrMatchUtil.ts +102 -0
  693. package/Utils/NetworkSite/TypeHierarchyUtil.ts +152 -2
  694. package/Utils/Rules/RuleCriteriaMatcher.ts +931 -0
  695. package/Utils/Rum/SessionIdentity.ts +16 -0
  696. package/Utils/Slo/SloBurnRateRuleState.ts +87 -33
  697. package/Utils/Telemetry/CloudInstanceIdentity.ts +94 -0
  698. package/Utils/Telemetry/EntityKey.ts +19 -0
  699. package/Utils/Telemetry/EntityRelationship.ts +19 -0
  700. package/Utils/Telemetry/OriginAllowList.ts +74 -0
  701. package/build/dist/Models/AnalyticsModels/RumSession.js +32 -0
  702. package/build/dist/Models/AnalyticsModels/RumSession.js.map +1 -1
  703. package/build/dist/Models/DatabaseModels/Alert.js +54 -0
  704. package/build/dist/Models/DatabaseModels/Alert.js.map +1 -1
  705. package/build/dist/Models/DatabaseModels/AlertEpisodeLabelRule.js +2 -2
  706. package/build/dist/Models/DatabaseModels/AlertEpisodeLabelRule.js.map +1 -1
  707. package/build/dist/Models/DatabaseModels/AlertEpisodeOnCallRule.js +2 -2
  708. package/build/dist/Models/DatabaseModels/AlertEpisodeOnCallRule.js.map +1 -1
  709. package/build/dist/Models/DatabaseModels/AlertEpisodeOwnerRule.js +2 -2
  710. package/build/dist/Models/DatabaseModels/AlertEpisodeOwnerRule.js.map +1 -1
  711. package/build/dist/Models/DatabaseModels/AlertEpisodePrivacyRule.js +2 -2
  712. package/build/dist/Models/DatabaseModels/AlertEpisodePrivacyRule.js.map +1 -1
  713. package/build/dist/Models/DatabaseModels/AlertGroupingRule.js +2 -2
  714. package/build/dist/Models/DatabaseModels/AlertGroupingRule.js.map +1 -1
  715. package/build/dist/Models/DatabaseModels/AlertLabelRule.js +2 -2
  716. package/build/dist/Models/DatabaseModels/AlertLabelRule.js.map +1 -1
  717. package/build/dist/Models/DatabaseModels/AlertOnCallRule.js +2 -2
  718. package/build/dist/Models/DatabaseModels/AlertOnCallRule.js.map +1 -1
  719. package/build/dist/Models/DatabaseModels/AlertOwnerRule.js +2 -2
  720. package/build/dist/Models/DatabaseModels/AlertOwnerRule.js.map +1 -1
  721. package/build/dist/Models/DatabaseModels/AlertPrivacyRule.js +2 -2
  722. package/build/dist/Models/DatabaseModels/AlertPrivacyRule.js.map +1 -1
  723. package/build/dist/Models/DatabaseModels/AlertReminderRule.js +6 -5
  724. package/build/dist/Models/DatabaseModels/AlertReminderRule.js.map +1 -1
  725. package/build/dist/Models/DatabaseModels/AutoRemediationRule.js +2 -2
  726. package/build/dist/Models/DatabaseModels/AutoRemediationRule.js.map +1 -1
  727. package/build/dist/Models/DatabaseModels/CephClusterLabelRule.js +2 -2
  728. package/build/dist/Models/DatabaseModels/CephClusterLabelRule.js.map +1 -1
  729. package/build/dist/Models/DatabaseModels/CephClusterOwnerRule.js +2 -2
  730. package/build/dist/Models/DatabaseModels/CephClusterOwnerRule.js.map +1 -1
  731. package/build/dist/Models/DatabaseModels/CloudResource.js +44 -7
  732. package/build/dist/Models/DatabaseModels/CloudResource.js.map +1 -1
  733. package/build/dist/Models/DatabaseModels/CloudResourceLabelRule.js +2 -2
  734. package/build/dist/Models/DatabaseModels/CloudResourceLabelRule.js.map +1 -1
  735. package/build/dist/Models/DatabaseModels/CloudResourceOwnerRule.js +2 -2
  736. package/build/dist/Models/DatabaseModels/CloudResourceOwnerRule.js.map +1 -1
  737. package/build/dist/Models/DatabaseModels/CodeRepository.js +87 -0
  738. package/build/dist/Models/DatabaseModels/CodeRepository.js.map +1 -1
  739. package/build/dist/Models/DatabaseModels/DashboardLabelRule.js +2 -2
  740. package/build/dist/Models/DatabaseModels/DashboardLabelRule.js.map +1 -1
  741. package/build/dist/Models/DatabaseModels/DashboardOwnerRule.js +2 -2
  742. package/build/dist/Models/DatabaseModels/DashboardOwnerRule.js.map +1 -1
  743. package/build/dist/Models/DatabaseModels/DatabaseBaseModel/RelationOnlyRuleBaseModel.js +12 -0
  744. package/build/dist/Models/DatabaseModels/DatabaseBaseModel/RelationOnlyRuleBaseModel.js.map +1 -0
  745. package/build/dist/Models/DatabaseModels/DatabaseBaseModel/RuleBaseModel.js +50 -0
  746. package/build/dist/Models/DatabaseModels/DatabaseBaseModel/RuleBaseModel.js.map +1 -0
  747. package/build/dist/Models/DatabaseModels/DockerHostLabelRule.js +2 -2
  748. package/build/dist/Models/DatabaseModels/DockerHostLabelRule.js.map +1 -1
  749. package/build/dist/Models/DatabaseModels/DockerHostOwnerRule.js +2 -2
  750. package/build/dist/Models/DatabaseModels/DockerHostOwnerRule.js.map +1 -1
  751. package/build/dist/Models/DatabaseModels/DockerSwarmClusterLabelRule.js +2 -2
  752. package/build/dist/Models/DatabaseModels/DockerSwarmClusterLabelRule.js.map +1 -1
  753. package/build/dist/Models/DatabaseModels/DockerSwarmClusterOwnerRule.js +2 -2
  754. package/build/dist/Models/DatabaseModels/DockerSwarmClusterOwnerRule.js.map +1 -1
  755. package/build/dist/Models/DatabaseModels/GoogleSecOpsConnection.js +60 -7
  756. package/build/dist/Models/DatabaseModels/GoogleSecOpsConnection.js.map +1 -1
  757. package/build/dist/Models/DatabaseModels/GoogleSecOpsConnectionRun.js +287 -0
  758. package/build/dist/Models/DatabaseModels/GoogleSecOpsConnectionRun.js.map +1 -0
  759. package/build/dist/Models/DatabaseModels/HostLabelRule.js +2 -2
  760. package/build/dist/Models/DatabaseModels/HostLabelRule.js.map +1 -1
  761. package/build/dist/Models/DatabaseModels/HostOwnerRule.js +2 -2
  762. package/build/dist/Models/DatabaseModels/HostOwnerRule.js.map +1 -1
  763. package/build/dist/Models/DatabaseModels/Incident.js +54 -0
  764. package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
  765. package/build/dist/Models/DatabaseModels/IncidentEpisodeLabelRule.js +2 -2
  766. package/build/dist/Models/DatabaseModels/IncidentEpisodeLabelRule.js.map +1 -1
  767. package/build/dist/Models/DatabaseModels/IncidentEpisodeOnCallRule.js +2 -2
  768. package/build/dist/Models/DatabaseModels/IncidentEpisodeOnCallRule.js.map +1 -1
  769. package/build/dist/Models/DatabaseModels/IncidentEpisodeOwnerRule.js +2 -2
  770. package/build/dist/Models/DatabaseModels/IncidentEpisodeOwnerRule.js.map +1 -1
  771. package/build/dist/Models/DatabaseModels/IncidentEpisodePrivacyRule.js +2 -2
  772. package/build/dist/Models/DatabaseModels/IncidentEpisodePrivacyRule.js.map +1 -1
  773. package/build/dist/Models/DatabaseModels/IncidentGroupingRule.js +2 -2
  774. package/build/dist/Models/DatabaseModels/IncidentGroupingRule.js.map +1 -1
  775. package/build/dist/Models/DatabaseModels/IncidentLabelRule.js +2 -2
  776. package/build/dist/Models/DatabaseModels/IncidentLabelRule.js.map +1 -1
  777. package/build/dist/Models/DatabaseModels/IncidentOnCallRule.js +2 -2
  778. package/build/dist/Models/DatabaseModels/IncidentOnCallRule.js.map +1 -1
  779. package/build/dist/Models/DatabaseModels/IncidentOwnerRule.js +2 -2
  780. package/build/dist/Models/DatabaseModels/IncidentOwnerRule.js.map +1 -1
  781. package/build/dist/Models/DatabaseModels/IncidentPrivacyRule.js +2 -2
  782. package/build/dist/Models/DatabaseModels/IncidentPrivacyRule.js.map +1 -1
  783. package/build/dist/Models/DatabaseModels/IncidentReminderRule.js +6 -5
  784. package/build/dist/Models/DatabaseModels/IncidentReminderRule.js.map +1 -1
  785. package/build/dist/Models/DatabaseModels/IncidentSlaRule.js +2 -2
  786. package/build/dist/Models/DatabaseModels/IncidentSlaRule.js.map +1 -1
  787. package/build/dist/Models/DatabaseModels/IncomingCallPolicy.js +3 -4
  788. package/build/dist/Models/DatabaseModels/IncomingCallPolicy.js.map +1 -1
  789. package/build/dist/Models/DatabaseModels/IncomingCallPolicyLabelRule.js +2 -2
  790. package/build/dist/Models/DatabaseModels/IncomingCallPolicyLabelRule.js.map +1 -1
  791. package/build/dist/Models/DatabaseModels/IncomingCallPolicyOwnerRule.js +2 -2
  792. package/build/dist/Models/DatabaseModels/IncomingCallPolicyOwnerRule.js.map +1 -1
  793. package/build/dist/Models/DatabaseModels/IncomingCallPolicyPhoneNumber.js +277 -0
  794. package/build/dist/Models/DatabaseModels/IncomingCallPolicyPhoneNumber.js.map +1 -0
  795. package/build/dist/Models/DatabaseModels/Index.js +18 -0
  796. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  797. package/build/dist/Models/DatabaseModels/IoTFleetLabelRule.js +2 -2
  798. package/build/dist/Models/DatabaseModels/IoTFleetLabelRule.js.map +1 -1
  799. package/build/dist/Models/DatabaseModels/IoTFleetOwnerRule.js +2 -2
  800. package/build/dist/Models/DatabaseModels/IoTFleetOwnerRule.js.map +1 -1
  801. package/build/dist/Models/DatabaseModels/KubernetesClusterLabelRule.js +2 -2
  802. package/build/dist/Models/DatabaseModels/KubernetesClusterLabelRule.js.map +1 -1
  803. package/build/dist/Models/DatabaseModels/KubernetesClusterOwnerRule.js +2 -2
  804. package/build/dist/Models/DatabaseModels/KubernetesClusterOwnerRule.js.map +1 -1
  805. package/build/dist/Models/DatabaseModels/MonitorLabelRule.js +2 -2
  806. package/build/dist/Models/DatabaseModels/MonitorLabelRule.js.map +1 -1
  807. package/build/dist/Models/DatabaseModels/MonitorOwnerRule.js +2 -2
  808. package/build/dist/Models/DatabaseModels/MonitorOwnerRule.js.map +1 -1
  809. package/build/dist/Models/DatabaseModels/NetworkDeviceAutoImportRule.js +2 -2
  810. package/build/dist/Models/DatabaseModels/NetworkDeviceAutoImportRule.js.map +1 -1
  811. package/build/dist/Models/DatabaseModels/NetworkDeviceLabelRule.js +2 -2
  812. package/build/dist/Models/DatabaseModels/NetworkDeviceLabelRule.js.map +1 -1
  813. package/build/dist/Models/DatabaseModels/NetworkDeviceOwnerRule.js +2 -2
  814. package/build/dist/Models/DatabaseModels/NetworkDeviceOwnerRule.js.map +1 -1
  815. package/build/dist/Models/DatabaseModels/NetworkSiteAssignmentRule.js +2 -2
  816. package/build/dist/Models/DatabaseModels/NetworkSiteAssignmentRule.js.map +1 -1
  817. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyLabelRule.js +2 -2
  818. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyLabelRule.js.map +1 -1
  819. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyOwnerRule.js +2 -2
  820. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyOwnerRule.js.map +1 -1
  821. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLabelRule.js +2 -2
  822. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLabelRule.js.map +1 -1
  823. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleOwnerRule.js +2 -2
  824. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleOwnerRule.js.map +1 -1
  825. package/build/dist/Models/DatabaseModels/PodmanHostLabelRule.js +2 -2
  826. package/build/dist/Models/DatabaseModels/PodmanHostLabelRule.js.map +1 -1
  827. package/build/dist/Models/DatabaseModels/PodmanHostOwnerRule.js +2 -2
  828. package/build/dist/Models/DatabaseModels/PodmanHostOwnerRule.js.map +1 -1
  829. package/build/dist/Models/DatabaseModels/ProxmoxClusterLabelRule.js +2 -2
  830. package/build/dist/Models/DatabaseModels/ProxmoxClusterLabelRule.js.map +1 -1
  831. package/build/dist/Models/DatabaseModels/ProxmoxClusterOwnerRule.js +2 -2
  832. package/build/dist/Models/DatabaseModels/ProxmoxClusterOwnerRule.js.map +1 -1
  833. package/build/dist/Models/DatabaseModels/RumApplicationLabelRule.js +2 -2
  834. package/build/dist/Models/DatabaseModels/RumApplicationLabelRule.js.map +1 -1
  835. package/build/dist/Models/DatabaseModels/RumApplicationOwnerRule.js +2 -2
  836. package/build/dist/Models/DatabaseModels/RumApplicationOwnerRule.js.map +1 -1
  837. package/build/dist/Models/DatabaseModels/RunbookLabelRule.js +2 -2
  838. package/build/dist/Models/DatabaseModels/RunbookLabelRule.js.map +1 -1
  839. package/build/dist/Models/DatabaseModels/RunbookOwnerRule.js +2 -2
  840. package/build/dist/Models/DatabaseModels/RunbookOwnerRule.js.map +1 -1
  841. package/build/dist/Models/DatabaseModels/RunbookRule.js +2 -2
  842. package/build/dist/Models/DatabaseModels/RunbookRule.js.map +1 -1
  843. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js +54 -0
  844. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js.map +1 -1
  845. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceLabelRule.js +2 -2
  846. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceLabelRule.js.map +1 -1
  847. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceOwnerRule.js +2 -2
  848. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceOwnerRule.js.map +1 -1
  849. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceReminderRule.js +6 -5
  850. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceReminderRule.js.map +1 -1
  851. package/build/dist/Models/DatabaseModels/ServerlessFunctionLabelRule.js +2 -2
  852. package/build/dist/Models/DatabaseModels/ServerlessFunctionLabelRule.js.map +1 -1
  853. package/build/dist/Models/DatabaseModels/ServerlessFunctionOwnerRule.js +2 -2
  854. package/build/dist/Models/DatabaseModels/ServerlessFunctionOwnerRule.js.map +1 -1
  855. package/build/dist/Models/DatabaseModels/ServiceLabelRule.js +2 -2
  856. package/build/dist/Models/DatabaseModels/ServiceLabelRule.js.map +1 -1
  857. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveBurnRateRule.js +281 -4
  858. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveBurnRateRule.js.map +1 -1
  859. package/build/dist/Models/DatabaseModels/ServiceOwnerRule.js +2 -2
  860. package/build/dist/Models/DatabaseModels/ServiceOwnerRule.js.map +1 -1
  861. package/build/dist/Models/DatabaseModels/StatusPageLabelRule.js +2 -2
  862. package/build/dist/Models/DatabaseModels/StatusPageLabelRule.js.map +1 -1
  863. package/build/dist/Models/DatabaseModels/StatusPageMonitorRule.js +2 -2
  864. package/build/dist/Models/DatabaseModels/StatusPageMonitorRule.js.map +1 -1
  865. package/build/dist/Models/DatabaseModels/StatusPageOwnerRule.js +2 -2
  866. package/build/dist/Models/DatabaseModels/StatusPageOwnerRule.js.map +1 -1
  867. package/build/dist/Models/DatabaseModels/User.js +57 -4
  868. package/build/dist/Models/DatabaseModels/User.js.map +1 -1
  869. package/build/dist/Models/DatabaseModels/UserNotificationEmailRollupItem.js +47 -2
  870. package/build/dist/Models/DatabaseModels/UserNotificationEmailRollupItem.js.map +1 -1
  871. package/build/dist/Models/DatabaseModels/UserWebAuthn.js +18 -0
  872. package/build/dist/Models/DatabaseModels/UserWebAuthn.js.map +1 -1
  873. package/build/dist/Models/DatabaseModels/VMwareResource.js +1201 -0
  874. package/build/dist/Models/DatabaseModels/VMwareResource.js.map +1 -0
  875. package/build/dist/Models/DatabaseModels/VMwareVCenter.js +1149 -0
  876. package/build/dist/Models/DatabaseModels/VMwareVCenter.js.map +1 -0
  877. package/build/dist/Models/DatabaseModels/VMwareVCenterFeed.js +648 -0
  878. package/build/dist/Models/DatabaseModels/VMwareVCenterFeed.js.map +1 -0
  879. package/build/dist/Models/DatabaseModels/VMwareVCenterLabelRule.js +522 -0
  880. package/build/dist/Models/DatabaseModels/VMwareVCenterLabelRule.js.map +1 -0
  881. package/build/dist/Models/DatabaseModels/VMwareVCenterOwnerRule.js +603 -0
  882. package/build/dist/Models/DatabaseModels/VMwareVCenterOwnerRule.js.map +1 -0
  883. package/build/dist/Models/DatabaseModels/VMwareVCenterOwnerTeam.js +503 -0
  884. package/build/dist/Models/DatabaseModels/VMwareVCenterOwnerTeam.js.map +1 -0
  885. package/build/dist/Models/DatabaseModels/VMwareVCenterOwnerUser.js +502 -0
  886. package/build/dist/Models/DatabaseModels/VMwareVCenterOwnerUser.js.map +1 -0
  887. package/build/dist/Models/DatabaseModels/WorkflowLabelRule.js +2 -2
  888. package/build/dist/Models/DatabaseModels/WorkflowLabelRule.js.map +1 -1
  889. package/build/dist/Models/DatabaseModels/WorkflowOwnerRule.js +2 -2
  890. package/build/dist/Models/DatabaseModels/WorkflowOwnerRule.js.map +1 -1
  891. package/build/dist/Models/DatabaseModels/WorkflowVariable.js +22 -1
  892. package/build/dist/Models/DatabaseModels/WorkflowVariable.js.map +1 -1
  893. package/build/dist/Server/API/AIAgentDataAPI.js +63 -3
  894. package/build/dist/Server/API/AIAgentDataAPI.js.map +1 -1
  895. package/build/dist/Server/API/AIAgentGitHubAPI.js +363 -0
  896. package/build/dist/Server/API/AIAgentGitHubAPI.js.map +1 -0
  897. package/build/dist/Server/API/AIChatAPI.js +19 -1
  898. package/build/dist/Server/API/AIChatAPI.js.map +1 -1
  899. package/build/dist/Server/API/BillingAPI.js +22 -0
  900. package/build/dist/Server/API/BillingAPI.js.map +1 -1
  901. package/build/dist/Server/API/DashboardAPI.js +10 -0
  902. package/build/dist/Server/API/DashboardAPI.js.map +1 -1
  903. package/build/dist/Server/API/GitHubAPI.js +33 -5
  904. package/build/dist/Server/API/GitHubAPI.js.map +1 -1
  905. package/build/dist/Server/API/GoogleSecOpsConnectionAPI.js +56 -0
  906. package/build/dist/Server/API/GoogleSecOpsConnectionAPI.js.map +1 -0
  907. package/build/dist/Server/API/TelemetryAPI.js +211 -4
  908. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  909. package/build/dist/Server/API/UserWebAuthnAPI.js +17 -1
  910. package/build/dist/Server/API/UserWebAuthnAPI.js.map +1 -1
  911. package/build/dist/Server/API/VMwareResourceAPI.js +106 -0
  912. package/build/dist/Server/API/VMwareResourceAPI.js.map +1 -0
  913. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791800000000-SplitWebAuthnChallengeByPurpose.js +41 -0
  914. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791800000000-SplitWebAuthnChallengeByPurpose.js.map +1 -0
  915. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791900000000-AddSeverityAndStateToNotificationEmailRollup.js +14 -0
  916. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791900000000-AddSeverityAndStateToNotificationEmailRollup.js.map +1 -0
  917. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1792000000000-AddVMwareTables.js +241 -0
  918. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1792000000000-AddVMwareTables.js.map +1 -0
  919. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1792100000000-AddGitHubCommandSettingsToCodeRepository.js +36 -0
  920. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1792100000000-AddGitHubCommandSettingsToCodeRepository.js.map +1 -0
  921. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1792200000000-AddUserWebAuthnPurpose.js +12 -0
  922. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1792200000000-AddUserWebAuthnPurpose.js.map +1 -0
  923. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1792300000000-AddGoogleSecOpsDiagnostics.js +34 -0
  924. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1792300000000-AddGoogleSecOpsDiagnostics.js.map +1 -0
  925. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1792400000000-AddSloBurnRateRuleIncidents.js +60 -0
  926. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1792400000000-AddSloBurnRateRuleIncidents.js.map +1 -0
  927. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1792500000000-AddConfigurableRuleCriteria.js +301 -0
  928. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1792500000000-AddConfigurableRuleCriteria.js.map +1 -0
  929. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1792600000000-AddIncomingCallPolicyPhoneNumbers.js +30 -0
  930. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1792600000000-AddIncomingCallPolicyPhoneNumbers.js.map +1 -0
  931. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +18 -0
  932. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  933. package/build/dist/Server/Middleware/IdentityRateLimit.js +80 -0
  934. package/build/dist/Server/Middleware/IdentityRateLimit.js.map +1 -1
  935. package/build/dist/Server/Middleware/MultipartFormData.js +38 -21
  936. package/build/dist/Server/Middleware/MultipartFormData.js.map +1 -1
  937. package/build/dist/Server/Services/AIRunService.js +14 -1
  938. package/build/dist/Server/Services/AIRunService.js.map +1 -1
  939. package/build/dist/Server/Services/AlertEpisodeLabelRuleEngineService.js +17 -0
  940. package/build/dist/Server/Services/AlertEpisodeLabelRuleEngineService.js.map +1 -1
  941. package/build/dist/Server/Services/AlertEpisodeOnCallRuleEngineService.js +17 -0
  942. package/build/dist/Server/Services/AlertEpisodeOnCallRuleEngineService.js.map +1 -1
  943. package/build/dist/Server/Services/AlertEpisodeOwnerRuleEngineService.js +17 -0
  944. package/build/dist/Server/Services/AlertEpisodeOwnerRuleEngineService.js.map +1 -1
  945. package/build/dist/Server/Services/AlertEpisodePrivacyRuleEngineService.js +17 -0
  946. package/build/dist/Server/Services/AlertEpisodePrivacyRuleEngineService.js.map +1 -1
  947. package/build/dist/Server/Services/AlertGroupingEngineService.js +21 -0
  948. package/build/dist/Server/Services/AlertGroupingEngineService.js.map +1 -1
  949. package/build/dist/Server/Services/AlertLabelRuleEngineService.js +21 -0
  950. package/build/dist/Server/Services/AlertLabelRuleEngineService.js.map +1 -1
  951. package/build/dist/Server/Services/AlertOnCallRuleEngineService.js +21 -0
  952. package/build/dist/Server/Services/AlertOnCallRuleEngineService.js.map +1 -1
  953. package/build/dist/Server/Services/AlertOwnerRuleEngineService.js +21 -0
  954. package/build/dist/Server/Services/AlertOwnerRuleEngineService.js.map +1 -1
  955. package/build/dist/Server/Services/AlertPrivacyRuleEngineService.js +21 -0
  956. package/build/dist/Server/Services/AlertPrivacyRuleEngineService.js.map +1 -1
  957. package/build/dist/Server/Services/AlertReminderRuleService.js +44 -26
  958. package/build/dist/Server/Services/AlertReminderRuleService.js.map +1 -1
  959. package/build/dist/Server/Services/AutoRemediationRuleEngineService.js +54 -11
  960. package/build/dist/Server/Services/AutoRemediationRuleEngineService.js.map +1 -1
  961. package/build/dist/Server/Services/BillingPaymentMethodService.js +71 -21
  962. package/build/dist/Server/Services/BillingPaymentMethodService.js.map +1 -1
  963. package/build/dist/Server/Services/BillingService.js +210 -22
  964. package/build/dist/Server/Services/BillingService.js.map +1 -1
  965. package/build/dist/Server/Services/CephClusterLabelRuleEngineService.js +16 -0
  966. package/build/dist/Server/Services/CephClusterLabelRuleEngineService.js.map +1 -1
  967. package/build/dist/Server/Services/CephClusterOwnerRuleEngineService.js +16 -0
  968. package/build/dist/Server/Services/CephClusterOwnerRuleEngineService.js.map +1 -1
  969. package/build/dist/Server/Services/CloudResourceInstanceService.js +66 -0
  970. package/build/dist/Server/Services/CloudResourceInstanceService.js.map +1 -1
  971. package/build/dist/Server/Services/CloudResourceLabelRuleEngineService.js +16 -0
  972. package/build/dist/Server/Services/CloudResourceLabelRuleEngineService.js.map +1 -1
  973. package/build/dist/Server/Services/CloudResourceOwnerRuleEngineService.js +16 -0
  974. package/build/dist/Server/Services/CloudResourceOwnerRuleEngineService.js.map +1 -1
  975. package/build/dist/Server/Services/DashboardLabelRuleEngineService.js +16 -0
  976. package/build/dist/Server/Services/DashboardLabelRuleEngineService.js.map +1 -1
  977. package/build/dist/Server/Services/DashboardOwnerRuleEngineService.js +16 -0
  978. package/build/dist/Server/Services/DashboardOwnerRuleEngineService.js.map +1 -1
  979. package/build/dist/Server/Services/DatabaseService.js +235 -10
  980. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  981. package/build/dist/Server/Services/DockerHostLabelRuleEngineService.js +16 -0
  982. package/build/dist/Server/Services/DockerHostLabelRuleEngineService.js.map +1 -1
  983. package/build/dist/Server/Services/DockerHostOwnerRuleEngineService.js +16 -0
  984. package/build/dist/Server/Services/DockerHostOwnerRuleEngineService.js.map +1 -1
  985. package/build/dist/Server/Services/DockerSwarmClusterLabelRuleEngineService.js +16 -0
  986. package/build/dist/Server/Services/DockerSwarmClusterLabelRuleEngineService.js.map +1 -1
  987. package/build/dist/Server/Services/DockerSwarmClusterOwnerRuleEngineService.js +16 -0
  988. package/build/dist/Server/Services/DockerSwarmClusterOwnerRuleEngineService.js.map +1 -1
  989. package/build/dist/Server/Services/ExceptionAggregationService.js +1 -0
  990. package/build/dist/Server/Services/ExceptionAggregationService.js.map +1 -1
  991. package/build/dist/Server/Services/GoogleSecOpsConnectionRunService.js +9 -0
  992. package/build/dist/Server/Services/GoogleSecOpsConnectionRunService.js.map +1 -0
  993. package/build/dist/Server/Services/GoogleSecOpsConnectionService.js +7 -0
  994. package/build/dist/Server/Services/GoogleSecOpsConnectionService.js.map +1 -1
  995. package/build/dist/Server/Services/HostLabelRuleEngineService.js +12 -0
  996. package/build/dist/Server/Services/HostLabelRuleEngineService.js.map +1 -1
  997. package/build/dist/Server/Services/HostOwnerRuleEngineService.js +12 -0
  998. package/build/dist/Server/Services/HostOwnerRuleEngineService.js.map +1 -1
  999. package/build/dist/Server/Services/IncidentEpisodeLabelRuleEngineService.js +17 -0
  1000. package/build/dist/Server/Services/IncidentEpisodeLabelRuleEngineService.js.map +1 -1
  1001. package/build/dist/Server/Services/IncidentEpisodeOnCallRuleEngineService.js +17 -0
  1002. package/build/dist/Server/Services/IncidentEpisodeOnCallRuleEngineService.js.map +1 -1
  1003. package/build/dist/Server/Services/IncidentEpisodeOwnerRuleEngineService.js +17 -0
  1004. package/build/dist/Server/Services/IncidentEpisodeOwnerRuleEngineService.js.map +1 -1
  1005. package/build/dist/Server/Services/IncidentEpisodePrivacyRuleEngineService.js +17 -0
  1006. package/build/dist/Server/Services/IncidentEpisodePrivacyRuleEngineService.js.map +1 -1
  1007. package/build/dist/Server/Services/IncidentGroupingEngineService.js +39 -13
  1008. package/build/dist/Server/Services/IncidentGroupingEngineService.js.map +1 -1
  1009. package/build/dist/Server/Services/IncidentLabelRuleEngineService.js +39 -9
  1010. package/build/dist/Server/Services/IncidentLabelRuleEngineService.js.map +1 -1
  1011. package/build/dist/Server/Services/IncidentOnCallRuleEngineService.js +39 -10
  1012. package/build/dist/Server/Services/IncidentOnCallRuleEngineService.js.map +1 -1
  1013. package/build/dist/Server/Services/IncidentOwnerRuleEngineService.js +39 -10
  1014. package/build/dist/Server/Services/IncidentOwnerRuleEngineService.js.map +1 -1
  1015. package/build/dist/Server/Services/IncidentPrivacyRuleEngineService.js +39 -10
  1016. package/build/dist/Server/Services/IncidentPrivacyRuleEngineService.js.map +1 -1
  1017. package/build/dist/Server/Services/IncidentReminderRuleService.js +44 -26
  1018. package/build/dist/Server/Services/IncidentReminderRuleService.js.map +1 -1
  1019. package/build/dist/Server/Services/IncidentSlaRuleService.js +33 -12
  1020. package/build/dist/Server/Services/IncidentSlaRuleService.js.map +1 -1
  1021. package/build/dist/Server/Services/IncomingCallPolicyLabelRuleEngineService.js +16 -0
  1022. package/build/dist/Server/Services/IncomingCallPolicyLabelRuleEngineService.js.map +1 -1
  1023. package/build/dist/Server/Services/IncomingCallPolicyOwnerRuleEngineService.js +16 -0
  1024. package/build/dist/Server/Services/IncomingCallPolicyOwnerRuleEngineService.js.map +1 -1
  1025. package/build/dist/Server/Services/IncomingCallPolicyPhoneNumberService.js +127 -0
  1026. package/build/dist/Server/Services/IncomingCallPolicyPhoneNumberService.js.map +1 -0
  1027. package/build/dist/Server/Services/IncomingCallPolicyService.js +187 -4
  1028. package/build/dist/Server/Services/IncomingCallPolicyService.js.map +1 -1
  1029. package/build/dist/Server/Services/Index.js +18 -0
  1030. package/build/dist/Server/Services/Index.js.map +1 -1
  1031. package/build/dist/Server/Services/IoTFleetLabelRuleEngineService.js +16 -0
  1032. package/build/dist/Server/Services/IoTFleetLabelRuleEngineService.js.map +1 -1
  1033. package/build/dist/Server/Services/IoTFleetOwnerRuleEngineService.js +16 -0
  1034. package/build/dist/Server/Services/IoTFleetOwnerRuleEngineService.js.map +1 -1
  1035. package/build/dist/Server/Services/KubernetesClusterLabelRuleEngineService.js +16 -0
  1036. package/build/dist/Server/Services/KubernetesClusterLabelRuleEngineService.js.map +1 -1
  1037. package/build/dist/Server/Services/KubernetesClusterOwnerRuleEngineService.js +16 -0
  1038. package/build/dist/Server/Services/KubernetesClusterOwnerRuleEngineService.js.map +1 -1
  1039. package/build/dist/Server/Services/LabelService.js +39 -7
  1040. package/build/dist/Server/Services/LabelService.js.map +1 -1
  1041. package/build/dist/Server/Services/LogAggregationService.js +1 -0
  1042. package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
  1043. package/build/dist/Server/Services/MetricAggregationService.js +1 -0
  1044. package/build/dist/Server/Services/MetricAggregationService.js.map +1 -1
  1045. package/build/dist/Server/Services/MobilePasskeyLoginService.js +137 -0
  1046. package/build/dist/Server/Services/MobilePasskeyLoginService.js.map +1 -0
  1047. package/build/dist/Server/Services/MonitorLabelRuleEngineService.js +16 -0
  1048. package/build/dist/Server/Services/MonitorLabelRuleEngineService.js.map +1 -1
  1049. package/build/dist/Server/Services/MonitorOwnerRuleEngineService.js +16 -0
  1050. package/build/dist/Server/Services/MonitorOwnerRuleEngineService.js.map +1 -1
  1051. package/build/dist/Server/Services/MonitorService.js +34 -1
  1052. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  1053. package/build/dist/Server/Services/MonitorTemplateService.js +64 -26
  1054. package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
  1055. package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js +126 -1
  1056. package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js.map +1 -1
  1057. package/build/dist/Server/Services/NetworkDeviceAutoImportRuleService.js +199 -3
  1058. package/build/dist/Server/Services/NetworkDeviceAutoImportRuleService.js.map +1 -1
  1059. package/build/dist/Server/Services/NetworkDeviceLabelRuleEngineService.js +17 -0
  1060. package/build/dist/Server/Services/NetworkDeviceLabelRuleEngineService.js.map +1 -1
  1061. package/build/dist/Server/Services/NetworkDeviceOwnerRuleEngineService.js +16 -0
  1062. package/build/dist/Server/Services/NetworkDeviceOwnerRuleEngineService.js.map +1 -1
  1063. package/build/dist/Server/Services/NetworkDeviceService.js +2 -0
  1064. package/build/dist/Server/Services/NetworkDeviceService.js.map +1 -1
  1065. package/build/dist/Server/Services/NetworkSiteAssignmentRuleService.js +56 -1
  1066. package/build/dist/Server/Services/NetworkSiteAssignmentRuleService.js.map +1 -1
  1067. package/build/dist/Server/Services/NetworkSiteService.js +187 -22
  1068. package/build/dist/Server/Services/NetworkSiteService.js.map +1 -1
  1069. package/build/dist/Server/Services/NetworkSiteTypeService.js +126 -51
  1070. package/build/dist/Server/Services/NetworkSiteTypeService.js.map +1 -1
  1071. package/build/dist/Server/Services/OnCallDutyPolicyLabelRuleEngineService.js +16 -0
  1072. package/build/dist/Server/Services/OnCallDutyPolicyLabelRuleEngineService.js.map +1 -1
  1073. package/build/dist/Server/Services/OnCallDutyPolicyOwnerRuleEngineService.js +16 -0
  1074. package/build/dist/Server/Services/OnCallDutyPolicyOwnerRuleEngineService.js.map +1 -1
  1075. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLabelRuleEngineService.js +16 -0
  1076. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLabelRuleEngineService.js.map +1 -1
  1077. package/build/dist/Server/Services/OnCallDutyPolicyScheduleOwnerRuleEngineService.js +16 -0
  1078. package/build/dist/Server/Services/OnCallDutyPolicyScheduleOwnerRuleEngineService.js.map +1 -1
  1079. package/build/dist/Server/Services/OpenTelemetryIngestService.js +28 -13
  1080. package/build/dist/Server/Services/OpenTelemetryIngestService.js.map +1 -1
  1081. package/build/dist/Server/Services/PayAsYouGoBillingService.js +297 -0
  1082. package/build/dist/Server/Services/PayAsYouGoBillingService.js.map +1 -0
  1083. package/build/dist/Server/Services/PodmanHostLabelRuleEngineService.js +16 -0
  1084. package/build/dist/Server/Services/PodmanHostLabelRuleEngineService.js.map +1 -1
  1085. package/build/dist/Server/Services/PodmanHostOwnerRuleEngineService.js +16 -0
  1086. package/build/dist/Server/Services/PodmanHostOwnerRuleEngineService.js.map +1 -1
  1087. package/build/dist/Server/Services/ProjectCallSMSConfigService.js +115 -6
  1088. package/build/dist/Server/Services/ProjectCallSMSConfigService.js.map +1 -1
  1089. package/build/dist/Server/Services/ProjectService.js +38 -2
  1090. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  1091. package/build/dist/Server/Services/ProxmoxClusterLabelRuleEngineService.js +16 -0
  1092. package/build/dist/Server/Services/ProxmoxClusterLabelRuleEngineService.js.map +1 -1
  1093. package/build/dist/Server/Services/ProxmoxClusterOwnerRuleEngineService.js +16 -0
  1094. package/build/dist/Server/Services/ProxmoxClusterOwnerRuleEngineService.js.map +1 -1
  1095. package/build/dist/Server/Services/RumApplicationLabelRuleEngineService.js +16 -0
  1096. package/build/dist/Server/Services/RumApplicationLabelRuleEngineService.js.map +1 -1
  1097. package/build/dist/Server/Services/RumApplicationOwnerRuleEngineService.js +16 -0
  1098. package/build/dist/Server/Services/RumApplicationOwnerRuleEngineService.js.map +1 -1
  1099. package/build/dist/Server/Services/RunbookLabelRuleEngineService.js +16 -0
  1100. package/build/dist/Server/Services/RunbookLabelRuleEngineService.js.map +1 -1
  1101. package/build/dist/Server/Services/RunbookOwnerRuleEngineService.js +16 -0
  1102. package/build/dist/Server/Services/RunbookOwnerRuleEngineService.js.map +1 -1
  1103. package/build/dist/Server/Services/RunbookRuleEngineService.js +12 -0
  1104. package/build/dist/Server/Services/RunbookRuleEngineService.js.map +1 -1
  1105. package/build/dist/Server/Services/ScheduledMaintenanceLabelRuleEngineService.js +38 -9
  1106. package/build/dist/Server/Services/ScheduledMaintenanceLabelRuleEngineService.js.map +1 -1
  1107. package/build/dist/Server/Services/ScheduledMaintenanceOwnerRuleEngineService.js +38 -10
  1108. package/build/dist/Server/Services/ScheduledMaintenanceOwnerRuleEngineService.js.map +1 -1
  1109. package/build/dist/Server/Services/ScheduledMaintenanceReminderRuleService.js +32 -14
  1110. package/build/dist/Server/Services/ScheduledMaintenanceReminderRuleService.js.map +1 -1
  1111. package/build/dist/Server/Services/ServerlessFunctionInstanceService.js +66 -0
  1112. package/build/dist/Server/Services/ServerlessFunctionInstanceService.js.map +1 -1
  1113. package/build/dist/Server/Services/ServerlessFunctionLabelRuleEngineService.js +16 -0
  1114. package/build/dist/Server/Services/ServerlessFunctionLabelRuleEngineService.js.map +1 -1
  1115. package/build/dist/Server/Services/ServerlessFunctionOwnerRuleEngineService.js +16 -0
  1116. package/build/dist/Server/Services/ServerlessFunctionOwnerRuleEngineService.js.map +1 -1
  1117. package/build/dist/Server/Services/ServiceLabelRuleEngineService.js +16 -0
  1118. package/build/dist/Server/Services/ServiceLabelRuleEngineService.js.map +1 -1
  1119. package/build/dist/Server/Services/ServiceLevelObjectiveBurnRateRuleService.js +554 -12
  1120. package/build/dist/Server/Services/ServiceLevelObjectiveBurnRateRuleService.js.map +1 -1
  1121. package/build/dist/Server/Services/ServiceLevelObjectiveService.js +50 -14
  1122. package/build/dist/Server/Services/ServiceLevelObjectiveService.js.map +1 -1
  1123. package/build/dist/Server/Services/ServiceOwnerRuleEngineService.js +16 -0
  1124. package/build/dist/Server/Services/ServiceOwnerRuleEngineService.js.map +1 -1
  1125. package/build/dist/Server/Services/StatusPageLabelRuleEngineService.js +16 -0
  1126. package/build/dist/Server/Services/StatusPageLabelRuleEngineService.js.map +1 -1
  1127. package/build/dist/Server/Services/StatusPageMonitorRuleEngineService.js +25 -2
  1128. package/build/dist/Server/Services/StatusPageMonitorRuleEngineService.js.map +1 -1
  1129. package/build/dist/Server/Services/StatusPageMonitorRuleService.js +19 -1
  1130. package/build/dist/Server/Services/StatusPageMonitorRuleService.js.map +1 -1
  1131. package/build/dist/Server/Services/StatusPageOwnerRuleEngineService.js +16 -0
  1132. package/build/dist/Server/Services/StatusPageOwnerRuleEngineService.js.map +1 -1
  1133. package/build/dist/Server/Services/TeamMemberService.js +85 -28
  1134. package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
  1135. package/build/dist/Server/Services/TelemetryIngestionKeyService.js +61 -10
  1136. package/build/dist/Server/Services/TelemetryIngestionKeyService.js.map +1 -1
  1137. package/build/dist/Server/Services/TelemetryUsageBillingService.js +62 -5
  1138. package/build/dist/Server/Services/TelemetryUsageBillingService.js.map +1 -1
  1139. package/build/dist/Server/Services/TraceAggregationService.js +5 -4
  1140. package/build/dist/Server/Services/TraceAggregationService.js.map +1 -1
  1141. package/build/dist/Server/Services/UserService.js +14 -49
  1142. package/build/dist/Server/Services/UserService.js.map +1 -1
  1143. package/build/dist/Server/Services/UserWebAuthnService.js +345 -113
  1144. package/build/dist/Server/Services/UserWebAuthnService.js.map +1 -1
  1145. package/build/dist/Server/Services/VMwareResourceService.js +395 -0
  1146. package/build/dist/Server/Services/VMwareResourceService.js.map +1 -0
  1147. package/build/dist/Server/Services/VMwareVCenterFeedService.js +89 -0
  1148. package/build/dist/Server/Services/VMwareVCenterFeedService.js.map +1 -0
  1149. package/build/dist/Server/Services/VMwareVCenterLabelRuleEngineService.js +213 -0
  1150. package/build/dist/Server/Services/VMwareVCenterLabelRuleEngineService.js.map +1 -0
  1151. package/build/dist/Server/Services/VMwareVCenterLabelRuleService.js +13 -0
  1152. package/build/dist/Server/Services/VMwareVCenterLabelRuleService.js.map +1 -0
  1153. package/build/dist/Server/Services/VMwareVCenterOwnerRuleEngineService.js +230 -0
  1154. package/build/dist/Server/Services/VMwareVCenterOwnerRuleEngineService.js.map +1 -0
  1155. package/build/dist/Server/Services/VMwareVCenterOwnerRuleService.js +13 -0
  1156. package/build/dist/Server/Services/VMwareVCenterOwnerRuleService.js.map +1 -0
  1157. package/build/dist/Server/Services/VMwareVCenterOwnerTeamService.js +129 -0
  1158. package/build/dist/Server/Services/VMwareVCenterOwnerTeamService.js.map +1 -0
  1159. package/build/dist/Server/Services/VMwareVCenterOwnerUserService.js +122 -0
  1160. package/build/dist/Server/Services/VMwareVCenterOwnerUserService.js.map +1 -0
  1161. package/build/dist/Server/Services/VMwareVCenterService.js +533 -0
  1162. package/build/dist/Server/Services/VMwareVCenterService.js.map +1 -0
  1163. package/build/dist/Server/Services/WorkflowLabelRuleEngineService.js +16 -0
  1164. package/build/dist/Server/Services/WorkflowLabelRuleEngineService.js.map +1 -1
  1165. package/build/dist/Server/Services/WorkflowOwnerRuleEngineService.js +16 -0
  1166. package/build/dist/Server/Services/WorkflowOwnerRuleEngineService.js.map +1 -1
  1167. package/build/dist/Server/Services/WorkflowVariableService.js +169 -0
  1168. package/build/dist/Server/Services/WorkflowVariableService.js.map +1 -1
  1169. package/build/dist/Server/Types/Billing/MeteredPlan/ActiveMonitoringMeteredPlan.js +12 -1
  1170. package/build/dist/Server/Types/Billing/MeteredPlan/ActiveMonitoringMeteredPlan.js.map +1 -1
  1171. package/build/dist/Server/Types/Billing/MeteredPlan/TelemetryMeteredPlan.js +25 -2
  1172. package/build/dist/Server/Types/Billing/MeteredPlan/TelemetryMeteredPlan.js.map +1 -1
  1173. package/build/dist/Server/Types/Database/QueryHelper.js +67 -0
  1174. package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
  1175. package/build/dist/Server/Utils/AI/Toolbox/SloTools.js +21 -4
  1176. package/build/dist/Server/Utils/AI/Toolbox/SloTools.js.map +1 -1
  1177. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHub.js.map +1 -1
  1178. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHubAgentTaskTrigger.js +275 -0
  1179. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHubAgentTaskTrigger.js.map +1 -0
  1180. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHubCommandAuthorizer.js +226 -0
  1181. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHubCommandAuthorizer.js.map +1 -0
  1182. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHubCommandParser.js +297 -0
  1183. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHubCommandParser.js.map +1 -0
  1184. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHubConversation.js +582 -0
  1185. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHubConversation.js.map +1 -0
  1186. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHubReplyComposer.js +275 -0
  1187. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHubReplyComposer.js.map +1 -0
  1188. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHubRunReply.js +365 -0
  1189. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHubRunReply.js.map +1 -0
  1190. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHubWebhookEvents.js +103 -0
  1191. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHubWebhookEvents.js.map +1 -0
  1192. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHubWebhookHandler.js +661 -0
  1193. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHubWebhookHandler.js.map +1 -0
  1194. package/build/dist/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.js +113 -0
  1195. package/build/dist/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.js.map +1 -1
  1196. package/build/dist/Server/Utils/DataSource/EgressGuard.js +99 -15
  1197. package/build/dist/Server/Utils/DataSource/EgressGuard.js.map +1 -1
  1198. package/build/dist/Server/Utils/EmailRollup/EmailRollupFlushRunner.js +2 -0
  1199. package/build/dist/Server/Utils/EmailRollup/EmailRollupFlushRunner.js.map +1 -1
  1200. package/build/dist/Server/Utils/EmailRollup/EmailRollupRenderer.js +18 -0
  1201. package/build/dist/Server/Utils/EmailRollup/EmailRollupRenderer.js.map +1 -1
  1202. package/build/dist/Server/Utils/EmailRollup/EmailRollupWriter.js +31 -2
  1203. package/build/dist/Server/Utils/EmailRollup/EmailRollupWriter.js.map +1 -1
  1204. package/build/dist/Server/Utils/IncomingCallPhoneNumber.js +9 -3
  1205. package/build/dist/Server/Utils/IncomingCallPhoneNumber.js.map +1 -1
  1206. package/build/dist/Server/Utils/LogRedaction.js +2 -1
  1207. package/build/dist/Server/Utils/LogRedaction.js.map +1 -1
  1208. package/build/dist/Server/Utils/Monitor/MonitorAlert.js +1 -1
  1209. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +219 -4
  1210. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  1211. package/build/dist/Server/Utils/Monitor/MonitorMaintenanceSuppression.js +12 -4
  1212. package/build/dist/Server/Utils/Monitor/MonitorMaintenanceSuppression.js.map +1 -1
  1213. package/build/dist/Server/Utils/Monitor/MonitorResourceContext.js +5 -3
  1214. package/build/dist/Server/Utils/Monitor/MonitorResourceContext.js.map +1 -1
  1215. package/build/dist/Server/Utils/Monitor/MonitorStepResourceIdentity.js +22 -9
  1216. package/build/dist/Server/Utils/Monitor/MonitorStepResourceIdentity.js.map +1 -1
  1217. package/build/dist/Server/Utils/Monitor/MonitorTemplateUtil.js +1 -0
  1218. package/build/dist/Server/Utils/Monitor/MonitorTemplateUtil.js.map +1 -1
  1219. package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js +21 -0
  1220. package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js.map +1 -1
  1221. package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js +21 -6
  1222. package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js.map +1 -1
  1223. package/build/dist/Server/Utils/Profiling.js +10 -1
  1224. package/build/dist/Server/Utils/Profiling.js.map +1 -1
  1225. package/build/dist/Server/Utils/ResourceFeed/ResourceFeedUtil.js +3 -3
  1226. package/build/dist/Server/Utils/Rules/MonitorRuleCriteriaCache.js +30 -0
  1227. package/build/dist/Server/Utils/Rules/MonitorRuleCriteriaCache.js.map +1 -0
  1228. package/build/dist/Server/Utils/SecurityEvent/ConnectorErrorMessage.js +9 -9
  1229. package/build/dist/Server/Utils/SecurityEvent/ConnectorErrorMessage.js.map +1 -1
  1230. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js +92 -71
  1231. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js.map +1 -1
  1232. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js +428 -143
  1233. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js.map +1 -1
  1234. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsRunExecutor.js +376 -0
  1235. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsRunExecutor.js.map +1 -0
  1236. package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js +344 -0
  1237. package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js.map +1 -1
  1238. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +5 -0
  1239. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
  1240. package/build/dist/Server/Utils/Telemetry/ResourceHeartbeat.js.map +1 -1
  1241. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js +136 -1
  1242. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js.map +1 -1
  1243. package/build/dist/Server/Utils/Telemetry/VMwareSnapshotScan.js +933 -0
  1244. package/build/dist/Server/Utils/Telemetry/VMwareSnapshotScan.js.map +1 -0
  1245. package/build/dist/Server/Utils/Telemetry.js +1 -1
  1246. package/build/dist/Server/Utils/Telemetry.js.map +1 -1
  1247. package/build/dist/Server/Utils/VendorAssets.js +2 -0
  1248. package/build/dist/Server/Utils/VendorAssets.js.map +1 -1
  1249. package/build/dist/Types/AI/CodeFixTaskContext.js +40 -0
  1250. package/build/dist/Types/AI/CodeFixTaskContext.js.map +1 -1
  1251. package/build/dist/Types/AI/CodeFixTaskType.js +57 -0
  1252. package/build/dist/Types/AI/CodeFixTaskType.js.map +1 -1
  1253. package/build/dist/Types/Accounts/AccountsLanguage.js +1 -0
  1254. package/build/dist/Types/Accounts/AccountsLanguage.js.map +1 -1
  1255. package/build/dist/Types/AdminDashboard/AdminDashboardLanguage.js +1 -0
  1256. package/build/dist/Types/AdminDashboard/AdminDashboardLanguage.js.map +1 -1
  1257. package/build/dist/Types/Cloud/CloudPlatform.js +277 -0
  1258. package/build/dist/Types/Cloud/CloudPlatform.js.map +1 -0
  1259. package/build/dist/Types/CodeRepository/GitHubCommand.js +52 -0
  1260. package/build/dist/Types/CodeRepository/GitHubCommand.js.map +1 -0
  1261. package/build/dist/Types/Dashboard/DashboardComponentType.js +2 -0
  1262. package/build/dist/Types/Dashboard/DashboardComponentType.js.map +1 -1
  1263. package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js +1 -0
  1264. package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js.map +1 -1
  1265. package/build/dist/Types/Dashboard/DashboardComponents/DashboardVMwareHostListComponent.js +2 -0
  1266. package/build/dist/Types/Dashboard/DashboardComponents/DashboardVMwareHostListComponent.js.map +1 -0
  1267. package/build/dist/Types/Dashboard/DashboardComponents/DashboardVMwareVirtualMachineListComponent.js +2 -0
  1268. package/build/dist/Types/Dashboard/DashboardComponents/DashboardVMwareVirtualMachineListComponent.js.map +1 -0
  1269. package/build/dist/Types/Dashboard/DashboardLanguage.js +1 -0
  1270. package/build/dist/Types/Dashboard/DashboardLanguage.js.map +1 -1
  1271. package/build/dist/Types/Dashboard/DashboardTemplates.js +711 -0
  1272. package/build/dist/Types/Dashboard/DashboardTemplates.js.map +1 -1
  1273. package/build/dist/Types/Docs/DocsLanguage.js +1 -0
  1274. package/build/dist/Types/Docs/DocsLanguage.js.map +1 -1
  1275. package/build/dist/Types/Exception/EgressGuardException.js +59 -0
  1276. package/build/dist/Types/Exception/EgressGuardException.js.map +1 -0
  1277. package/build/dist/Types/Icon/IconProp.js +1 -0
  1278. package/build/dist/Types/Icon/IconProp.js.map +1 -1
  1279. package/build/dist/Types/Monitor/MonitorCriteriaInstance.js +1 -0
  1280. package/build/dist/Types/Monitor/MonitorCriteriaInstance.js.map +1 -1
  1281. package/build/dist/Types/Monitor/MonitorStep.js +61 -18
  1282. package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
  1283. package/build/dist/Types/Monitor/MonitorStepMetricViewConfigUtil.js +1 -0
  1284. package/build/dist/Types/Monitor/MonitorStepMetricViewConfigUtil.js.map +1 -1
  1285. package/build/dist/Types/Monitor/MonitorStepVMwareMonitor.js +71 -0
  1286. package/build/dist/Types/Monitor/MonitorStepVMwareMonitor.js.map +1 -0
  1287. package/build/dist/Types/Monitor/MonitorTemplateSyncField.js +211 -0
  1288. package/build/dist/Types/Monitor/MonitorTemplateSyncField.js.map +1 -0
  1289. package/build/dist/Types/Monitor/MonitorType.js +23 -0
  1290. package/build/dist/Types/Monitor/MonitorType.js.map +1 -1
  1291. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js +28 -0
  1292. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js.map +1 -1
  1293. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js +1 -0
  1294. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js.map +1 -1
  1295. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationUtil.js +2 -0
  1296. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationUtil.js.map +1 -1
  1297. package/build/dist/Types/Monitor/SeriesContext/SeriesDebugHints.js +120 -0
  1298. package/build/dist/Types/Monitor/SeriesContext/SeriesDebugHints.js.map +1 -1
  1299. package/build/dist/Types/Monitor/SeriesContext/SeriesLabelDisplay.js +60 -0
  1300. package/build/dist/Types/Monitor/SeriesContext/SeriesLabelDisplay.js.map +1 -1
  1301. package/build/dist/Types/Monitor/VMwareAlertTemplates.js +775 -0
  1302. package/build/dist/Types/Monitor/VMwareAlertTemplates.js.map +1 -0
  1303. package/build/dist/Types/Monitor/VMwareMetricCatalog.js +799 -0
  1304. package/build/dist/Types/Monitor/VMwareMetricCatalog.js.map +1 -0
  1305. package/build/dist/Types/OnCallDutyPolicy/ScheduleShiftUtil.js +25 -0
  1306. package/build/dist/Types/OnCallDutyPolicy/ScheduleShiftUtil.js.map +1 -1
  1307. package/build/dist/Types/OnCallDutyPolicy/UserOverrideUtil.js +5 -0
  1308. package/build/dist/Types/OnCallDutyPolicy/UserOverrideUtil.js.map +1 -1
  1309. package/build/dist/Types/Password.js +63 -5
  1310. package/build/dist/Types/Password.js.map +1 -1
  1311. package/build/dist/Types/Permission.js +234 -0
  1312. package/build/dist/Types/Permission.js.map +1 -1
  1313. package/build/dist/Types/Probe/RequestFailedDetails.js +7 -0
  1314. package/build/dist/Types/Probe/RequestFailedDetails.js.map +1 -1
  1315. package/build/dist/Types/Rules/RuleCriteria.js +18 -0
  1316. package/build/dist/Types/Rules/RuleCriteria.js.map +1 -0
  1317. package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js +426 -0
  1318. package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js.map +1 -0
  1319. package/build/dist/Types/Rum/SessionReplay.js +64 -0
  1320. package/build/dist/Types/Rum/SessionReplay.js.map +1 -1
  1321. package/build/dist/Types/Rum/SessionReplayApi.js.map +1 -1
  1322. package/build/dist/Types/SecurityEvent/GoogleSecOpsDiagnostics.js +2 -0
  1323. package/build/dist/Types/SecurityEvent/GoogleSecOpsDiagnostics.js.map +1 -0
  1324. package/build/dist/Types/StatusPage/StatusPageLanguage.js +1 -0
  1325. package/build/dist/Types/StatusPage/StatusPageLanguage.js.map +1 -1
  1326. package/build/dist/Types/Telemetry/EntityType.js +21 -0
  1327. package/build/dist/Types/Telemetry/EntityType.js.map +1 -1
  1328. package/build/dist/Types/Telemetry/ServiceType.js +1 -0
  1329. package/build/dist/Types/Telemetry/ServiceType.js.map +1 -1
  1330. package/build/dist/UI/Components/Alerts/Alert.js +63 -41
  1331. package/build/dist/UI/Components/Alerts/Alert.js.map +1 -1
  1332. package/build/dist/UI/Components/Breadcrumbs/Breadcrumbs.js +2 -2
  1333. package/build/dist/UI/Components/Breadcrumbs/Breadcrumbs.js.map +1 -1
  1334. package/build/dist/UI/Components/Button/Button.js +3 -1
  1335. package/build/dist/UI/Components/Button/Button.js.map +1 -1
  1336. package/build/dist/UI/Components/Calendar/Calendar.js +24 -4
  1337. package/build/dist/UI/Components/Calendar/Calendar.js.map +1 -1
  1338. package/build/dist/UI/Components/Dropdown/Dropdown.js +1 -1
  1339. package/build/dist/UI/Components/Dropdown/Dropdown.js.map +1 -1
  1340. package/build/dist/UI/Components/Feed/Feed.js +4 -1
  1341. package/build/dist/UI/Components/Feed/Feed.js.map +1 -1
  1342. package/build/dist/UI/Components/Feed/useFeedItems.js +137 -0
  1343. package/build/dist/UI/Components/Feed/useFeedItems.js.map +1 -0
  1344. package/build/dist/UI/Components/Forms/BasicForm.js +66 -22
  1345. package/build/dist/UI/Components/Forms/BasicForm.js.map +1 -1
  1346. package/build/dist/UI/Components/Forms/Fields/ColorPicker.js +1 -1
  1347. package/build/dist/UI/Components/Forms/Fields/ColorPicker.js.map +1 -1
  1348. package/build/dist/UI/Components/Forms/Fields/FormField.js +10 -3
  1349. package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
  1350. package/build/dist/UI/Components/Forms/ModelForm.js +154 -7
  1351. package/build/dist/UI/Components/Forms/ModelForm.js.map +1 -1
  1352. package/build/dist/UI/Components/Graphs/DayUptimeGraph.js +5 -6
  1353. package/build/dist/UI/Components/Graphs/DayUptimeGraph.js.map +1 -1
  1354. package/build/dist/UI/Components/Icon/Icon.js +17 -35
  1355. package/build/dist/UI/Components/Icon/Icon.js.map +1 -1
  1356. package/build/dist/UI/Components/Input/Input.js +37 -8
  1357. package/build/dist/UI/Components/Input/Input.js.map +1 -1
  1358. package/build/dist/UI/Components/LabelRule/ImportLabelRulesModal.js +228 -0
  1359. package/build/dist/UI/Components/LabelRule/ImportLabelRulesModal.js.map +1 -0
  1360. package/build/dist/UI/Components/LabelRule/LabelRuleTable.js +102 -0
  1361. package/build/dist/UI/Components/LabelRule/LabelRuleTable.js.map +1 -0
  1362. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +15 -2
  1363. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  1364. package/build/dist/UI/Components/ModelTable/ModelTable.js +25 -6
  1365. package/build/dist/UI/Components/ModelTable/ModelTable.js.map +1 -1
  1366. package/build/dist/UI/Components/MonitorGraphs/Uptime.js +14 -15
  1367. package/build/dist/UI/Components/MonitorGraphs/Uptime.js.map +1 -1
  1368. package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.js +2 -0
  1369. package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.js.map +1 -1
  1370. package/build/dist/UI/Components/Navbar/NavBar.js +1 -0
  1371. package/build/dist/UI/Components/Navbar/NavBar.js.map +1 -1
  1372. package/build/dist/UI/Components/RuleCriteria/RuleCriteriaBuilder.js +383 -0
  1373. package/build/dist/UI/Components/RuleCriteria/RuleCriteriaBuilder.js.map +1 -0
  1374. package/build/dist/UI/Components/RuleCriteria/RuleCriteriaModelForm.js +124 -0
  1375. package/build/dist/UI/Components/RuleCriteria/RuleCriteriaModelForm.js.map +1 -0
  1376. package/build/dist/UI/Components/RuleCriteria/RuleCriteriaModelTable.js +126 -0
  1377. package/build/dist/UI/Components/RuleCriteria/RuleCriteriaModelTable.js.map +1 -0
  1378. package/build/dist/UI/Components/RuleCriteria/RuleCriteriaSummary.js +63 -0
  1379. package/build/dist/UI/Components/RuleCriteria/RuleCriteriaSummary.js.map +1 -0
  1380. package/build/dist/UI/Components/Table/Table.js +1 -1
  1381. package/build/dist/UI/Components/Table/Table.js.map +1 -1
  1382. package/build/dist/UI/Components/Table/TableBody.js +4 -2
  1383. package/build/dist/UI/Components/Table/TableBody.js.map +1 -1
  1384. package/build/dist/UI/Components/Table/TableRow.js +3 -2
  1385. package/build/dist/UI/Components/Table/TableRow.js.map +1 -1
  1386. package/build/dist/UI/Components/Telemetry/TelemetryRetentionConfigForm.js +14 -2
  1387. package/build/dist/UI/Components/Telemetry/TelemetryRetentionConfigForm.js.map +1 -1
  1388. package/build/dist/UI/Components/Tooltip/Tooltip.js +33 -4
  1389. package/build/dist/UI/Components/Tooltip/Tooltip.js.map +1 -1
  1390. package/build/dist/UI/Utils/DashboardStackingLayers.js +44 -0
  1391. package/build/dist/UI/Utils/DashboardStackingLayers.js.map +1 -0
  1392. package/build/dist/UI/Utils/LabelRuleImportExport.js +271 -0
  1393. package/build/dist/UI/Utils/LabelRuleImportExport.js.map +1 -0
  1394. package/build/dist/UI/Utils/ModelAPI/ModelAPI.js +4 -4
  1395. package/build/dist/UI/Utils/ModelAPI/ModelAPI.js.map +1 -1
  1396. package/build/dist/UI/Utils/Navigation.js.map +1 -1
  1397. package/build/dist/UI/Utils/TelemetryService.js +3 -2
  1398. package/build/dist/UI/Utils/TelemetryService.js.map +1 -1
  1399. package/build/dist/UI/Utils/UseDashboardGridDnd.js +2 -1
  1400. package/build/dist/UI/Utils/UseDashboardGridDnd.js.map +1 -1
  1401. package/build/dist/UI/Utils/UserEmailRegistrationStatus.js +21 -4
  1402. package/build/dist/UI/Utils/UserEmailRegistrationStatus.js.map +1 -1
  1403. package/build/dist/UI/Utils/WebAuthn.js +121 -0
  1404. package/build/dist/UI/Utils/WebAuthn.js.map +1 -0
  1405. package/build/dist/Utils/API.js +29 -0
  1406. package/build/dist/Utils/API.js.map +1 -1
  1407. package/build/dist/Utils/Dashboard/Components/DashboardVMwareHostListComponent.js +33 -0
  1408. package/build/dist/Utils/Dashboard/Components/DashboardVMwareHostListComponent.js.map +1 -0
  1409. package/build/dist/Utils/Dashboard/Components/DashboardVMwareResourceListShared.js +54 -0
  1410. package/build/dist/Utils/Dashboard/Components/DashboardVMwareResourceListShared.js.map +1 -0
  1411. package/build/dist/Utils/Dashboard/Components/DashboardVMwareVirtualMachineListComponent.js +60 -0
  1412. package/build/dist/Utils/Dashboard/Components/DashboardVMwareVirtualMachineListComponent.js.map +1 -0
  1413. package/build/dist/Utils/Dashboard/Components/Index.js +8 -0
  1414. package/build/dist/Utils/Dashboard/Components/Index.js.map +1 -1
  1415. package/build/dist/Utils/Faker.js +76 -6
  1416. package/build/dist/Utils/Faker.js.map +1 -1
  1417. package/build/dist/Utils/LabelRuleImportExport.js +510 -0
  1418. package/build/dist/Utils/LabelRuleImportExport.js.map +1 -0
  1419. package/build/dist/Utils/Monitor/MonitorTemplateSyncUtil.js +110 -0
  1420. package/build/dist/Utils/Monitor/MonitorTemplateSyncUtil.js.map +1 -0
  1421. package/build/dist/Utils/NetworkDiscovery/AutoImportRuleMatcher.js +81 -5
  1422. package/build/dist/Utils/NetworkDiscovery/AutoImportRuleMatcher.js.map +1 -1
  1423. package/build/dist/Utils/NetworkDiscovery/DiscoveryScanStatus.js +6 -0
  1424. package/build/dist/Utils/NetworkDiscovery/DiscoveryScanStatus.js.map +1 -1
  1425. package/build/dist/Utils/NetworkSite/CidrMatchUtil.js +77 -0
  1426. package/build/dist/Utils/NetworkSite/CidrMatchUtil.js.map +1 -1
  1427. package/build/dist/Utils/NetworkSite/TypeHierarchyUtil.js +109 -2
  1428. package/build/dist/Utils/NetworkSite/TypeHierarchyUtil.js.map +1 -1
  1429. package/build/dist/Utils/Rules/RuleCriteriaMatcher.js +526 -0
  1430. package/build/dist/Utils/Rules/RuleCriteriaMatcher.js.map +1 -0
  1431. package/build/dist/Utils/Rum/SessionIdentity.js +13 -1
  1432. package/build/dist/Utils/Rum/SessionIdentity.js.map +1 -1
  1433. package/build/dist/Utils/Slo/SloBurnRateRuleState.js +28 -15
  1434. package/build/dist/Utils/Slo/SloBurnRateRuleState.js.map +1 -1
  1435. package/build/dist/Utils/Telemetry/CloudInstanceIdentity.js +88 -0
  1436. package/build/dist/Utils/Telemetry/CloudInstanceIdentity.js.map +1 -0
  1437. package/build/dist/Utils/Telemetry/EntityKey.js +15 -0
  1438. package/build/dist/Utils/Telemetry/EntityKey.js.map +1 -1
  1439. package/build/dist/Utils/Telemetry/EntityRelationship.js +13 -0
  1440. package/build/dist/Utils/Telemetry/EntityRelationship.js.map +1 -1
  1441. package/build/dist/Utils/Telemetry/OriginAllowList.js +61 -0
  1442. package/build/dist/Utils/Telemetry/OriginAllowList.js.map +1 -1
  1443. package/package.json +20 -20
@@ -1,12 +1,22 @@
1
1
  import Alert from "../../../Models/DatabaseModels/Alert";
2
2
  import AlertStateTimeline from "../../../Models/DatabaseModels/AlertStateTimeline";
3
+ import Incident from "../../../Models/DatabaseModels/Incident";
4
+ import IncidentStateTimeline from "../../../Models/DatabaseModels/IncidentStateTimeline";
3
5
  import ServiceLevelObjectiveBurnRateRule from "../../../Models/DatabaseModels/ServiceLevelObjectiveBurnRateRule";
4
6
  import AlertService from "../../../Server/Services/AlertService";
7
+ import AlertSeverityService from "../../../Server/Services/AlertSeverityService";
5
8
  import AlertStateTimelineService from "../../../Server/Services/AlertStateTimelineService";
6
- import ServiceLevelObjectiveBurnRateRuleService from "../../../Server/Services/ServiceLevelObjectiveBurnRateRuleService";
9
+ import IncidentService from "../../../Server/Services/IncidentService";
10
+ import IncidentSeverityService from "../../../Server/Services/IncidentSeverityService";
11
+ import IncidentStateTimelineService from "../../../Server/Services/IncidentStateTimelineService";
12
+ import ServiceLevelObjectiveBurnRateRuleService, {
13
+ NO_OUTPUT_ERROR_MESSAGE,
14
+ } from "../../../Server/Services/ServiceLevelObjectiveBurnRateRuleService";
7
15
  import CreateBy from "../../../Server/Types/Database/CreateBy";
8
16
  import DeleteBy from "../../../Server/Types/Database/DeleteBy";
17
+ import { OnUpdate } from "../../../Server/Types/Database/Hooks";
9
18
  import UpdateBy from "../../../Server/Types/Database/UpdateBy";
19
+ import ProjectScopedReferenceValidator from "../../../Server/Utils/Database/ProjectScopedReferenceValidator";
10
20
  import logger from "../../../Server/Utils/Logger";
11
21
  import BadDataException from "../../../Types/Exception/BadDataException";
12
22
  import ObjectID from "../../../Types/ObjectID";
@@ -19,12 +29,19 @@ import { describe, expect, it, beforeEach, afterEach } from "@jest/globals";
19
29
  * is strictly longer than the short one and the threshold is positive;
20
30
  * the guard has to hold on create AND on partial updates that touch only
21
31
  * one of the two windows (validated against the persisted sibling),
22
- * - the alert fingerprint is the only link between a rule and the alerts it
23
- * opened, so it must be exactly `slo:<sloId>:burn-rule:<ruleId>` and
24
- * never drift,
25
- * - deleting a rule must resolve the alerts it opened, tolerating the
26
- * benign same-state race with the evaluation worker and isolating a
27
- * failure on one alert from the rest.
32
+ * - a rule must declare SOMETHING - raise an alert, declare an incident, or
33
+ * both - on create and after any update, judged against the row that will
34
+ * EXIST afterwards rather than against the payload alone,
35
+ * - the fingerprint is the only link between a rule and what it opened, and
36
+ * one fingerprint serves both tables, so it must be exactly
37
+ * `slo:<sloId>:burn-rule:<ruleId>` and never drift,
38
+ * - a severity id belonging to another project is accepted by the column but
39
+ * makes the rule unfireable forever, so both severity references are
40
+ * validated against the rule's own project on create and on update,
41
+ * - anything that stops a rule from declaring something - a delete, a
42
+ * disable, an output switched off - must resolve what it left open,
43
+ * tolerating the benign same-state race with the evaluation worker and
44
+ * isolating a failure on one record (or one rule) from the rest.
28
45
  */
29
46
 
30
47
  const PROJECT_ID: ObjectID = new ObjectID(
@@ -38,6 +55,15 @@ const OTHER_RULE_ID: ObjectID = new ObjectID(
38
55
  const RESOLVED_STATE_ID: ObjectID = new ObjectID(
39
56
  "66666666-6666-4666-8666-666666666666",
40
57
  );
58
+ const ALERT_SEVERITY_ID: ObjectID = new ObjectID(
59
+ "77777777-7777-4777-8777-777777777777",
60
+ );
61
+ const INCIDENT_SEVERITY_ID: ObjectID = new ObjectID(
62
+ "88888888-8888-4888-8888-888888888888",
63
+ );
64
+ const OTHER_PROJECT_ID: ObjectID = new ObjectID(
65
+ "99999999-9999-4999-8999-999999999999",
66
+ );
41
67
 
42
68
  const THRESHOLD_ERROR_MESSAGE: string =
43
69
  "Burn rate threshold must be greater than 0.";
@@ -51,10 +77,26 @@ const WINDOWS_NOT_NUMERIC_ERROR_MESSAGE: string =
51
77
  "Long window and short window must be a number of minutes.";
52
78
  const SAME_STATE_RACE_MESSAGE: string =
53
79
  "Alert state cannot be same as previous state.";
80
+ const SAME_STATE_INCIDENT_RACE_MESSAGE: string =
81
+ "Incident state cannot be same as previous state.";
54
82
  const DEFAULT_ROOT_CAUSE: string =
55
83
  "Alert auto-resolved because the SLO burn rate rule that created it is no longer active.";
84
+ const DEFAULT_INCIDENT_ROOT_CAUSE: string =
85
+ "Incident auto-resolved because the SLO burn rate rule that declared it is no longer active.";
86
+ /*
87
+ * The delete root cause is deliberately output-neutral: one string is stamped
88
+ * on both the alert and the incident the deleted rule left open.
89
+ */
56
90
  const DELETE_ROOT_CAUSE: string =
57
- "Alert auto-resolved because the SLO burn rate rule that created it was deleted.";
91
+ "Auto-resolved because the SLO burn rate rule that created it was deleted.";
92
+ const DISABLED_ALERT_ROOT_CAUSE: string =
93
+ "Alert auto-resolved because the SLO burn rate rule that created it was disabled.";
94
+ const DISABLED_INCIDENT_ROOT_CAUSE: string =
95
+ "Incident auto-resolved because the SLO burn rate rule that declared it was disabled.";
96
+ const ALERTS_TURNED_OFF_ROOT_CAUSE: string =
97
+ "Alert auto-resolved because the SLO burn rate rule that created it no longer raises alerts.";
98
+ const INCIDENTS_TURNED_OFF_ROOT_CAUSE: string =
99
+ "Incident auto-resolved because the SLO burn rate rule that declared it no longer declares incidents.";
58
100
 
59
101
  type RuleFields = Record<string, unknown>;
60
102
 
@@ -75,6 +117,7 @@ function makeRule(fields: RuleFields): ServiceLevelObjectiveBurnRateRule {
75
117
 
76
118
  function makeCreateBy(
77
119
  fields: RuleFields,
120
+ props?: CreateBy<ServiceLevelObjectiveBurnRateRule>["props"],
78
121
  ): CreateBy<ServiceLevelObjectiveBurnRateRule> {
79
122
  return {
80
123
  data: makeRule({
@@ -83,22 +126,52 @@ function makeCreateBy(
83
126
  name: "Fast burn",
84
127
  ...fields,
85
128
  }),
86
- props: { isRoot: true },
129
+ props: props || { isRoot: true },
87
130
  };
88
131
  }
89
132
 
133
+ /*
134
+ * A create payload whose numeric columns already satisfy the window and
135
+ * threshold guards, so a test about the output flags or the severity
136
+ * references only has to state the field it is actually about.
137
+ */
138
+ function makeValidCreateBy(
139
+ fields: RuleFields,
140
+ props?: CreateBy<ServiceLevelObjectiveBurnRateRule>["props"],
141
+ ): CreateBy<ServiceLevelObjectiveBurnRateRule> {
142
+ return makeCreateBy(
143
+ {
144
+ burnRateThreshold: 14.4,
145
+ longWindowInMinutes: 60,
146
+ shortWindowInMinutes: 5,
147
+ ...fields,
148
+ },
149
+ props,
150
+ );
151
+ }
152
+
90
153
  function makeUpdateBy(
91
154
  data: RuleFields,
155
+ props?: Record<string, unknown>,
92
156
  ): UpdateBy<ServiceLevelObjectiveBurnRateRule> {
93
157
  return {
94
158
  query: { _id: RULE_ID.toString() },
95
159
  data: data,
96
- props: { isRoot: true },
160
+ props: props || { isRoot: true },
97
161
  limit: 1,
98
162
  skip: 0,
99
163
  } as unknown as UpdateBy<ServiceLevelObjectiveBurnRateRule>;
100
164
  }
101
165
 
166
+ function makeOnUpdate(
167
+ data: RuleFields,
168
+ ): OnUpdate<ServiceLevelObjectiveBurnRateRule> {
169
+ return {
170
+ updateBy: makeUpdateBy(data),
171
+ carryForward: null,
172
+ };
173
+ }
174
+
102
175
  function makeDeleteBy(): DeleteBy<ServiceLevelObjectiveBurnRateRule> {
103
176
  return {
104
177
  query: { serviceLevelObjectiveId: SLO_ID },
@@ -116,6 +189,60 @@ function makeAlert(id: ObjectID): Alert {
116
189
  return alert;
117
190
  }
118
191
 
192
+ function makeIncident(id: ObjectID): Incident {
193
+ const incident: Incident = new Incident();
194
+ incident._id = id.toString();
195
+ incident.id = id;
196
+ incident.projectId = PROJECT_ID;
197
+ return incident;
198
+ }
199
+
200
+ interface FindByArguments {
201
+ query: Record<string, unknown>;
202
+ select: Record<string, unknown>;
203
+ limit: number;
204
+ skip: number;
205
+ props: Record<string, unknown>;
206
+ }
207
+
208
+ // The argument of the nth call to a findBy spy, typed for assertions.
209
+ function findByArgumentsAt(
210
+ spy: jest.SpyInstance,
211
+ index: number,
212
+ ): FindByArguments {
213
+ return spy.mock.calls[index]![0] as FindByArguments;
214
+ }
215
+
216
+ interface FindOneByIdArguments {
217
+ id: ObjectID;
218
+ select: Record<string, unknown>;
219
+ props: Record<string, unknown>;
220
+ }
221
+
222
+ function findOneByIdArgumentsAt(
223
+ spy: jest.SpyInstance,
224
+ index: number,
225
+ ): FindOneByIdArguments {
226
+ return spy.mock.calls[index]![0] as FindOneByIdArguments;
227
+ }
228
+
229
+ interface ValidatedReferences {
230
+ projectId: ObjectID | undefined;
231
+ subject: string | undefined;
232
+ references: Array<{
233
+ modelName: string;
234
+ id: ObjectID | undefined;
235
+ service: unknown;
236
+ }>;
237
+ }
238
+
239
+ function validatedReferencesAt(
240
+ spy: jest.SpyInstance,
241
+ index: number,
242
+ ): ValidatedReferences {
243
+ return spy.mock.calls[index]![0] as ValidatedReferences;
244
+ }
245
+
119
246
  // Calls the protected hook without widening the service's public surface.
120
247
  function callHook(name: string, ...args: Array<unknown>): Promise<unknown> {
121
248
  const hooks: Record<
@@ -301,6 +428,372 @@ describe("ServiceLevelObjectiveBurnRateRuleService.onBeforeCreate", () => {
301
428
  }
302
429
  });
303
430
 
431
+ /*
432
+ * A rule that neither raises an alert nor declares an incident is a rule the
433
+ * evaluation worker keeps evaluating forever while producing nothing, and
434
+ * nothing downstream ever revisits it. Both flags reach this hook through the
435
+ * public CRUD API, where BaseModel.fromJSON does NOT coerce Boolean columns -
436
+ * so "false" arrives as a non-empty, and therefore truthy, string.
437
+ */
438
+ describe("ServiceLevelObjectiveBurnRateRuleService.onBeforeCreate - what the rule declares", () => {
439
+ afterEach(() => {
440
+ jest.restoreAllMocks();
441
+ });
442
+
443
+ /*
444
+ * Both values are written back rather than left to the column defaults:
445
+ * validateOutputsOnUpdate and onUpdateSuccess read these off the persisted
446
+ * row later, and a create that never mentioned them would leave those hooks
447
+ * reading `undefined` for a decision this create actually made.
448
+ */
449
+ it("defaults to raising an alert and declaring no incident, and writes both decisions onto the payload", async () => {
450
+ const createBy: CreateBy<ServiceLevelObjectiveBurnRateRule> =
451
+ makeValidCreateBy({});
452
+
453
+ await expect(callHook("onBeforeCreate", createBy)).resolves.toBeDefined();
454
+
455
+ expect(createBy.data.shouldCreateAlert).toBe(true);
456
+ expect(createBy.data.shouldCreateIncident).toBe(false);
457
+ expect(typeof createBy.data.shouldCreateAlert).toBe("boolean");
458
+ expect(typeof createBy.data.shouldCreateIncident).toBe("boolean");
459
+ });
460
+
461
+ const acceptedOutputs: Array<{
462
+ label: string;
463
+ shouldCreateAlert: unknown;
464
+ shouldCreateIncident: unknown;
465
+ expectedShouldCreateAlert: boolean;
466
+ expectedShouldCreateIncident: boolean;
467
+ }> = [
468
+ {
469
+ label: "an alert-only rule",
470
+ shouldCreateAlert: true,
471
+ shouldCreateIncident: false,
472
+ expectedShouldCreateAlert: true,
473
+ expectedShouldCreateIncident: false,
474
+ },
475
+ {
476
+ label: "an incident-only rule",
477
+ shouldCreateAlert: false,
478
+ shouldCreateIncident: true,
479
+ expectedShouldCreateAlert: false,
480
+ expectedShouldCreateIncident: true,
481
+ },
482
+ {
483
+ label: "a rule that does both",
484
+ shouldCreateAlert: true,
485
+ shouldCreateIncident: true,
486
+ expectedShouldCreateAlert: true,
487
+ expectedShouldCreateIncident: true,
488
+ },
489
+ {
490
+ label: 'alerts switched off with the string "false"',
491
+ shouldCreateAlert: "false",
492
+ shouldCreateIncident: true,
493
+ expectedShouldCreateAlert: false,
494
+ expectedShouldCreateIncident: true,
495
+ },
496
+ {
497
+ label: 'alerts switched off with the string "FALSE"',
498
+ shouldCreateAlert: "FALSE",
499
+ shouldCreateIncident: true,
500
+ expectedShouldCreateAlert: false,
501
+ expectedShouldCreateIncident: true,
502
+ },
503
+ {
504
+ label: 'alerts switched off with a padded " false "',
505
+ shouldCreateAlert: " false ",
506
+ shouldCreateIncident: true,
507
+ expectedShouldCreateAlert: false,
508
+ expectedShouldCreateIncident: true,
509
+ },
510
+ {
511
+ label: 'alerts switched off with the string "0"',
512
+ shouldCreateAlert: "0",
513
+ shouldCreateIncident: true,
514
+ expectedShouldCreateAlert: false,
515
+ expectedShouldCreateIncident: true,
516
+ },
517
+ {
518
+ label: 'incidents switched on with the string "true"',
519
+ shouldCreateAlert: false,
520
+ shouldCreateIncident: "true",
521
+ expectedShouldCreateAlert: false,
522
+ expectedShouldCreateIncident: true,
523
+ },
524
+ {
525
+ label: 'incidents switched on with the string "1"',
526
+ shouldCreateAlert: false,
527
+ shouldCreateIncident: "1",
528
+ expectedShouldCreateAlert: false,
529
+ expectedShouldCreateIncident: true,
530
+ },
531
+ {
532
+ label: 'incidents switched on with an unrecognised string like "yes"',
533
+ shouldCreateAlert: false,
534
+ shouldCreateIncident: "yes",
535
+ expectedShouldCreateAlert: false,
536
+ expectedShouldCreateIncident: true,
537
+ },
538
+ {
539
+ label: "an empty string for incidents, which falls back to the default",
540
+ shouldCreateAlert: true,
541
+ shouldCreateIncident: "",
542
+ expectedShouldCreateAlert: true,
543
+ expectedShouldCreateIncident: false,
544
+ },
545
+ {
546
+ label: "an empty string for alerts, which falls back to the default",
547
+ shouldCreateAlert: "",
548
+ shouldCreateIncident: false,
549
+ expectedShouldCreateAlert: true,
550
+ expectedShouldCreateIncident: false,
551
+ },
552
+ {
553
+ // Anything that is neither boolean nor string falls through to Boolean().
554
+ label: "the numbers a hand-written API call sends instead of booleans",
555
+ shouldCreateAlert: 0,
556
+ shouldCreateIncident: 1,
557
+ expectedShouldCreateAlert: false,
558
+ expectedShouldCreateIncident: true,
559
+ },
560
+ ];
561
+
562
+ for (const acceptedOutput of acceptedOutputs) {
563
+ it(`accepts ${acceptedOutput.label}, and persists the coerced booleans`, async () => {
564
+ const createBy: CreateBy<ServiceLevelObjectiveBurnRateRule> =
565
+ makeValidCreateBy({
566
+ shouldCreateAlert: acceptedOutput.shouldCreateAlert,
567
+ shouldCreateIncident: acceptedOutput.shouldCreateIncident,
568
+ });
569
+
570
+ await expect(callHook("onBeforeCreate", createBy)).resolves.toBeDefined();
571
+
572
+ expect(createBy.data.shouldCreateAlert).toBe(
573
+ acceptedOutput.expectedShouldCreateAlert,
574
+ );
575
+ expect(createBy.data.shouldCreateIncident).toBe(
576
+ acceptedOutput.expectedShouldCreateIncident,
577
+ );
578
+ expect(typeof createBy.data.shouldCreateAlert).toBe("boolean");
579
+ expect(typeof createBy.data.shouldCreateIncident).toBe("boolean");
580
+ });
581
+ }
582
+
583
+ const rejectedOutputs: Array<{ label: string; fields: RuleFields }> = [
584
+ {
585
+ label: "both outputs switched off as real booleans",
586
+ fields: { shouldCreateAlert: false, shouldCreateIncident: false },
587
+ },
588
+ {
589
+ label: "alerts switched off with incidents never mentioned",
590
+ fields: { shouldCreateAlert: false },
591
+ },
592
+ {
593
+ label: "alerts switched off with incidents sent as an empty string",
594
+ fields: { shouldCreateAlert: false, shouldCreateIncident: "" },
595
+ },
596
+ {
597
+ label: "alerts switched off with incidents sent as null",
598
+ fields: { shouldCreateAlert: false, shouldCreateIncident: null },
599
+ },
600
+ {
601
+ label: 'alerts off as " FALSE " with incidents off as the string "0"',
602
+ fields: { shouldCreateAlert: " FALSE ", shouldCreateIncident: "0" },
603
+ },
604
+ {
605
+ label: "a numeric 0 for alerts with incidents left at their default",
606
+ fields: { shouldCreateAlert: 0 },
607
+ },
608
+ ];
609
+
610
+ for (const rejectedOutput of rejectedOutputs) {
611
+ it(`rejects ${rejectedOutput.label}`, async () => {
612
+ await expectBadData(
613
+ callHook("onBeforeCreate", makeValidCreateBy(rejectedOutput.fields)),
614
+ NO_OUTPUT_ERROR_MESSAGE,
615
+ );
616
+ });
617
+ }
618
+
619
+ /*
620
+ * The regression the coercion exists for: a plain
621
+ * `if (!data.shouldCreateAlert && !data.shouldCreateIncident)` ACCEPTS this
622
+ * payload, because the string "false" is truthy. The rule is then persisted
623
+ * declaring nothing at all, and no later hook ever looks at it again.
624
+ */
625
+ it('rejects a payload whose flags are the STRING "false", which a truthiness check would wave through', async () => {
626
+ await expectBadData(
627
+ callHook(
628
+ "onBeforeCreate",
629
+ makeValidCreateBy({
630
+ shouldCreateAlert: "false",
631
+ shouldCreateIncident: "false",
632
+ }),
633
+ ),
634
+ NO_OUTPUT_ERROR_MESSAGE,
635
+ );
636
+ });
637
+ });
638
+
639
+ /*
640
+ * Both severity columns are plain foreign keys with no project scoping of
641
+ * their own, so a severity id from another project is persisted happily and
642
+ * only fails much later - inside the evaluation worker, on every tick, where
643
+ * the throw becomes a log line. The rule looks configured and declares nothing
644
+ * forever, which is why the reference is checked at write time instead.
645
+ */
646
+ describe("ServiceLevelObjectiveBurnRateRuleService.onBeforeCreate - severity references", () => {
647
+ let validatorSpy: jest.SpyInstance;
648
+
649
+ beforeEach(() => {
650
+ validatorSpy = jest
651
+ .spyOn(
652
+ ProjectScopedReferenceValidator,
653
+ "validateReferencesBelongToProject",
654
+ )
655
+ .mockResolvedValue(undefined);
656
+ });
657
+
658
+ afterEach(() => {
659
+ jest.restoreAllMocks();
660
+ });
661
+
662
+ it("does not reach for the validator when the rule names no severity at all", async () => {
663
+ await expect(
664
+ callHook("onBeforeCreate", makeValidCreateBy({})),
665
+ ).resolves.toBeDefined();
666
+
667
+ expect(validatorSpy).not.toHaveBeenCalled();
668
+ });
669
+
670
+ const severityPayloads: Array<{
671
+ label: string;
672
+ alertSeverityId: ObjectID | undefined;
673
+ incidentSeverityId: ObjectID | undefined;
674
+ }> = [
675
+ {
676
+ label: "an alert severity alone",
677
+ alertSeverityId: ALERT_SEVERITY_ID,
678
+ incidentSeverityId: undefined,
679
+ },
680
+ {
681
+ label: "an incident severity alone",
682
+ alertSeverityId: undefined,
683
+ incidentSeverityId: INCIDENT_SEVERITY_ID,
684
+ },
685
+ {
686
+ label: "both severities",
687
+ alertSeverityId: ALERT_SEVERITY_ID,
688
+ incidentSeverityId: INCIDENT_SEVERITY_ID,
689
+ },
690
+ ];
691
+
692
+ severityPayloads.forEach(
693
+ (severityPayload: {
694
+ label: string;
695
+ alertSeverityId: ObjectID | undefined;
696
+ incidentSeverityId: ObjectID | undefined;
697
+ }): void => {
698
+ it(`validates ${severityPayload.label} against the rule's own project, in one call`, async () => {
699
+ await expect(
700
+ callHook(
701
+ "onBeforeCreate",
702
+ makeValidCreateBy({
703
+ alertSeverityId: severityPayload.alertSeverityId,
704
+ incidentSeverityId: severityPayload.incidentSeverityId,
705
+ }),
706
+ ),
707
+ ).resolves.toBeDefined();
708
+
709
+ expect(validatorSpy).toHaveBeenCalledTimes(1);
710
+
711
+ const validated: ValidatedReferences = validatedReferencesAt(
712
+ validatorSpy,
713
+ 0,
714
+ );
715
+
716
+ expect(validated.projectId).toEqual(PROJECT_ID);
717
+ // The subject is what the user-facing error message is phrased around.
718
+ expect(validated.subject).toBe("SLO burn rate rule");
719
+
720
+ /*
721
+ * Both references are always passed, absent id included - the validator
722
+ * skips the empty ones itself, and passing both keeps one lookup per
723
+ * referenced model rather than one per present id.
724
+ */
725
+ expect(validated.references).toHaveLength(2);
726
+ expect(validated.references[0]!.modelName).toBe("Alert Severity");
727
+ expect(validated.references[0]!.id).toEqual(
728
+ severityPayload.alertSeverityId,
729
+ );
730
+ expect(validated.references[0]!.service).toBe(AlertSeverityService);
731
+ expect(validated.references[1]!.modelName).toBe("Incident Severity");
732
+ expect(validated.references[1]!.id).toEqual(
733
+ severityPayload.incidentSeverityId,
734
+ );
735
+ expect(validated.references[1]!.service).toBe(IncidentSeverityService);
736
+ });
737
+ },
738
+ );
739
+
740
+ it("falls back to the caller's tenant when the payload itself carries no project", async () => {
741
+ await expect(
742
+ callHook(
743
+ "onBeforeCreate",
744
+ makeValidCreateBy(
745
+ {
746
+ projectId: undefined,
747
+ incidentSeverityId: INCIDENT_SEVERITY_ID,
748
+ },
749
+ { isRoot: true, tenantId: PROJECT_ID },
750
+ ),
751
+ ),
752
+ ).resolves.toBeDefined();
753
+
754
+ expect(validatorSpy).toHaveBeenCalledTimes(1);
755
+ expect(validatedReferencesAt(validatorSpy, 0).projectId).toEqual(
756
+ PROJECT_ID,
757
+ );
758
+ });
759
+
760
+ /*
761
+ * With no project on either side there is nothing to compare against, and
762
+ * the write is going to fail anyway on the required tenant column. Failing
763
+ * here instead would replace that clear error with a confusing severity one.
764
+ */
765
+ it("skips validation entirely when neither the payload nor the caller names a project", async () => {
766
+ await expect(
767
+ callHook(
768
+ "onBeforeCreate",
769
+ makeValidCreateBy({
770
+ projectId: undefined,
771
+ alertSeverityId: ALERT_SEVERITY_ID,
772
+ incidentSeverityId: INCIDENT_SEVERITY_ID,
773
+ }),
774
+ ),
775
+ ).resolves.toBeDefined();
776
+
777
+ expect(validatorSpy).not.toHaveBeenCalled();
778
+ });
779
+
780
+ it("lets a cross-project severity rejection abort the create", async () => {
781
+ validatorSpy.mockRejectedValue(
782
+ new BadDataException(
783
+ "This SLO burn rate rule references records that belong to a different project.",
784
+ ),
785
+ );
786
+
787
+ await expectBadData(
788
+ callHook(
789
+ "onBeforeCreate",
790
+ makeValidCreateBy({ incidentSeverityId: INCIDENT_SEVERITY_ID }),
791
+ ),
792
+ "belong to a different project",
793
+ );
794
+ });
795
+ });
796
+
304
797
  describe("ServiceLevelObjectiveBurnRateRuleService.onBeforeUpdate", () => {
305
798
  let findBySpy: jest.SpyInstance;
306
799
 
@@ -531,22 +1024,15 @@ describe("ServiceLevelObjectiveBurnRateRuleService.onBeforeUpdate", () => {
531
1024
  });
532
1025
 
533
1026
  /*
534
- * Regression: the window columns arrive as STRINGS from the dashboard (number
535
- * form fields hand Formik `e.target.value`, and neither ModelForm nor
536
- * BaseModel.fromJSON coerces Number/Decimal columns), so
537
- * `long <= short` used to be a LEXICOGRAPHIC comparison:
538
- *
539
- * long "1440", short "60" -> "1440" <= "60" is TRUE -> wrongly REJECTED
540
- * (a 24h/60m rule could not be created at all)
541
- * long "120", short "30" -> "120" <= "30" is TRUE -> wrongly REJECTED
542
- * long "60", short "360" -> "60" <= "360" is FALSE -> wrongly ACCEPTED
543
- * (a short window LONGER than the long one,
544
- * which breaks the whole multi-window design)
545
- *
546
- * Every numeric column is now coerced before validation and the coerced number
547
- * is written back onto the payload.
1027
+ * The update twin of the create guard, with the extra problem that a form
1028
+ * sends only the field the user touched: turning alerts off must be allowed on
1029
+ * a rule that already declares incidents and refused on one that declares
1030
+ * nothing else. So the check is made against the row that will EXIST after the
1031
+ * write - but only when the payload could actually leave a rule empty, because
1032
+ * reading the affected rows back costs a query on every update that touches
1033
+ * these columns.
548
1034
  */
549
- describe("ServiceLevelObjectiveBurnRateRuleService - numeric columns supplied as strings", () => {
1035
+ describe("ServiceLevelObjectiveBurnRateRuleService.onBeforeUpdate - what the rule declares", () => {
550
1036
  let findBySpy: jest.SpyInstance;
551
1037
 
552
1038
  beforeEach(() => {
@@ -557,631 +1043,2125 @@ describe("ServiceLevelObjectiveBurnRateRuleService - numeric columns supplied as
557
1043
  jest.restoreAllMocks();
558
1044
  });
559
1045
 
560
- const lexicographicTraps: Array<{
561
- label: string;
562
- longWindowInMinutes: string;
563
- shortWindowInMinutes: string;
564
- }> = [
565
- {
566
- label: "a 24 hour long window over a 60 minute short window",
567
- longWindowInMinutes: "1440",
568
- shortWindowInMinutes: "60",
569
- },
570
- {
571
- label: "a 2 hour long window over a 30 minute short window",
572
- longWindowInMinutes: "120",
573
- shortWindowInMinutes: "30",
574
- },
575
- {
576
- label: "a 6 hour long window over a 5 minute short window",
577
- longWindowInMinutes: "360",
578
- shortWindowInMinutes: "5",
579
- },
580
- ];
581
-
582
- for (const trap of lexicographicTraps) {
583
- it(`accepts ${trap.label} sent as strings, and persists both as numbers`, async () => {
584
- const createBy: CreateBy<ServiceLevelObjectiveBurnRateRule> =
585
- makeCreateBy({
586
- burnRateThreshold: "14.4",
587
- longWindowInMinutes: trap.longWindowInMinutes,
588
- shortWindowInMinutes: trap.shortWindowInMinutes,
589
- });
1046
+ it("issues no row read at all when the payload mentions neither output", async () => {
1047
+ await expect(
1048
+ callHook("onBeforeUpdate", makeUpdateBy({ isEnabled: true })),
1049
+ ).resolves.toBeDefined();
590
1050
 
591
- await expect(callHook("onBeforeCreate", createBy)).resolves.toBeDefined();
1051
+ expect(findBySpy).not.toHaveBeenCalled();
1052
+ });
592
1053
 
593
- expect(createBy.data.longWindowInMinutes).toBe(
594
- Number(trap.longWindowInMinutes),
595
- );
596
- expect(createBy.data.shortWindowInMinutes).toBe(
597
- Number(trap.shortWindowInMinutes),
598
- );
599
- expect(createBy.data.burnRateThreshold).toBe(14.4);
1054
+ it("treats explicitly null output flags as 'not being updated'", async () => {
1055
+ await expect(
1056
+ callHook(
1057
+ "onBeforeUpdate",
1058
+ makeUpdateBy({ shouldCreateAlert: null, shouldCreateIncident: null }),
1059
+ ),
1060
+ ).resolves.toBeDefined();
600
1061
 
601
- expect(typeof createBy.data.longWindowInMinutes).toBe("number");
602
- expect(typeof createBy.data.shortWindowInMinutes).toBe("number");
603
- expect(typeof createBy.data.burnRateThreshold).toBe("number");
604
- });
605
- }
1062
+ expect(findBySpy).not.toHaveBeenCalled();
1063
+ });
606
1064
 
607
- it('rejects a short window LONGER than the long window when both are strings ("60" / "360")', async () => {
1065
+ it("rejects a payload that turns both outputs off, without reading a single row", async () => {
608
1066
  await expectBadData(
609
1067
  callHook(
610
- "onBeforeCreate",
611
- makeCreateBy({
612
- burnRateThreshold: 14.4,
613
- longWindowInMinutes: "60",
614
- shortWindowInMinutes: "360",
615
- }),
1068
+ "onBeforeUpdate",
1069
+ makeUpdateBy({ shouldCreateAlert: false, shouldCreateIncident: false }),
616
1070
  ),
617
- LONG_WINDOW_ERROR_MESSAGE,
1071
+ NO_OUTPUT_ERROR_MESSAGE,
618
1072
  );
1073
+
1074
+ // Both flags are in the payload, so the resulting row is already known.
1075
+ expect(findBySpy).not.toHaveBeenCalled();
619
1076
  });
620
1077
 
621
- it("rejects string windows that are equal", async () => {
1078
+ it('rejects both outputs turned off as the strings "false" and "FALSE"', async () => {
622
1079
  await expectBadData(
623
1080
  callHook(
624
- "onBeforeCreate",
625
- makeCreateBy({
626
- burnRateThreshold: 14.4,
627
- longWindowInMinutes: "60",
628
- shortWindowInMinutes: "60",
1081
+ "onBeforeUpdate",
1082
+ makeUpdateBy({
1083
+ shouldCreateAlert: "false",
1084
+ shouldCreateIncident: "FALSE",
629
1085
  }),
630
1086
  ),
631
- LONG_WINDOW_ERROR_MESSAGE,
1087
+ NO_OUTPUT_ERROR_MESSAGE,
632
1088
  );
1089
+
1090
+ expect(findBySpy).not.toHaveBeenCalled();
633
1091
  });
634
1092
 
635
- it('rejects a short window of "0" as a string', async () => {
636
- await expectBadData(
1093
+ it("accepts a payload that turns both outputs on", async () => {
1094
+ await expect(
637
1095
  callHook(
638
- "onBeforeCreate",
639
- makeCreateBy({
640
- burnRateThreshold: 14.4,
641
- longWindowInMinutes: "60",
642
- shortWindowInMinutes: "0",
643
- }),
1096
+ "onBeforeUpdate",
1097
+ makeUpdateBy({ shouldCreateAlert: true, shouldCreateIncident: true }),
644
1098
  ),
645
- SHORT_WINDOW_ERROR_MESSAGE,
646
- );
1099
+ ).resolves.toBeDefined();
1100
+
1101
+ expect(findBySpy).not.toHaveBeenCalled();
647
1102
  });
648
1103
 
649
- const nonNumericWindows: Array<{ label: string; value: unknown }> = [
650
- { label: "an empty string", value: "" },
651
- { label: "a whitespace-only string", value: " " },
652
- { label: "a non-numeric string", value: "abc" },
653
- { label: "NaN", value: Number.NaN },
654
- { label: "a boolean", value: true },
1104
+ const outputsSwitchedOn: Array<{ label: string; fields: RuleFields }> = [
1105
+ { label: "alerts", fields: { shouldCreateAlert: true } },
1106
+ { label: "incidents", fields: { shouldCreateIncident: true } },
1107
+ {
1108
+ label: 'alerts as the string "true"',
1109
+ fields: { shouldCreateAlert: "true" },
1110
+ },
1111
+ {
1112
+ label: 'incidents as the string "1"',
1113
+ fields: { shouldCreateIncident: "1" },
1114
+ },
655
1115
  ];
656
1116
 
657
- for (const nonNumericWindow of nonNumericWindows) {
658
- it(`rejects a long window of ${nonNumericWindow.label} on create`, async () => {
659
- await expectBadData(
660
- callHook(
661
- "onBeforeCreate",
662
- makeCreateBy({
663
- burnRateThreshold: 14.4,
664
- longWindowInMinutes: nonNumericWindow.value,
665
- shortWindowInMinutes: 5,
666
- }),
667
- ),
668
- WINDOWS_NOT_NUMERIC_ERROR_MESSAGE,
669
- );
670
- });
1117
+ outputsSwitchedOn.forEach(
1118
+ (switchedOn: { label: string; fields: RuleFields }): void => {
1119
+ /*
1120
+ * A deliberate short-circuit, not an oversight: switching one output ON
1121
+ * cannot leave a rule with nothing whatever its sibling holds, so the
1122
+ * affected rows are never read.
1123
+ */
1124
+ it(`accepts turning ${switchedOn.label} on without reading any row`, async () => {
1125
+ await expect(
1126
+ callHook("onBeforeUpdate", makeUpdateBy(switchedOn.fields)),
1127
+ ).resolves.toBeDefined();
1128
+
1129
+ expect(findBySpy).not.toHaveBeenCalled();
1130
+ });
1131
+ },
1132
+ );
671
1133
 
672
- it(`rejects a short window of ${nonNumericWindow.label} on create`, async () => {
673
- await expectBadData(
674
- callHook(
675
- "onBeforeCreate",
676
- makeCreateBy({
677
- burnRateThreshold: 14.4,
678
- longWindowInMinutes: 60,
679
- shortWindowInMinutes: nonNumericWindow.value,
680
- }),
681
- ),
682
- WINDOWS_NOT_NUMERIC_ERROR_MESSAGE,
683
- );
684
- });
685
- }
1134
+ it("reads the affected rows as root, through the caller's own query, when the only flag written turns alerts off", async () => {
1135
+ findBySpy.mockResolvedValue([
1136
+ makeRule({
1137
+ _id: RULE_ID.toString(),
1138
+ id: RULE_ID,
1139
+ shouldCreateIncident: true,
1140
+ }),
1141
+ ]);
686
1142
 
687
- const rejectedThresholdStrings: Array<{ label: string; value: unknown }> = [
688
- { label: "an empty string", value: "" },
689
- { label: "a non-numeric string", value: "abc" },
690
- { label: 'the string "0"', value: "0" },
691
- { label: 'the string "-1"', value: "-1" },
692
- ];
1143
+ await expect(
1144
+ callHook("onBeforeUpdate", makeUpdateBy({ shouldCreateAlert: false })),
1145
+ ).resolves.toBeDefined();
693
1146
 
694
- for (const rejectedThresholdString of rejectedThresholdStrings) {
695
- it(`rejects a threshold of ${rejectedThresholdString.label} on create`, async () => {
696
- await expectBadData(
697
- callHook(
698
- "onBeforeCreate",
699
- makeCreateBy({
700
- burnRateThreshold: rejectedThresholdString.value,
701
- longWindowInMinutes: 60,
702
- shortWindowInMinutes: 5,
703
- }),
704
- ),
705
- THRESHOLD_ERROR_MESSAGE,
706
- );
707
- });
1147
+ expect(findBySpy).toHaveBeenCalledTimes(1);
708
1148
 
709
- it(`rejects a threshold of ${rejectedThresholdString.label} on update`, async () => {
710
- await expectBadData(
711
- callHook(
712
- "onBeforeUpdate",
713
- makeUpdateBy({ burnRateThreshold: rejectedThresholdString.value }),
714
- ),
715
- THRESHOLD_ERROR_MESSAGE,
716
- );
717
- });
718
- }
1149
+ const findByArguments: FindByArguments = findByArgumentsAt(findBySpy, 0);
719
1150
 
720
- it("coerces the optional numeric columns too", async () => {
721
- const createBy: CreateBy<ServiceLevelObjectiveBurnRateRule> = makeCreateBy({
722
- burnRateThreshold: "6",
723
- longWindowInMinutes: "360",
724
- shortWindowInMinutes: "30",
725
- minimumSampleCount: "100",
726
- refireSuppressionMinutes: "360",
1151
+ expect(findByArguments.query).toEqual({ _id: RULE_ID.toString() });
1152
+ // Only the sibling flags matter; the id keeps the read cheap to reason about.
1153
+ expect(findByArguments.select).toEqual({
1154
+ _id: true,
1155
+ shouldCreateAlert: true,
1156
+ shouldCreateIncident: true,
727
1157
  });
728
-
729
- await expect(callHook("onBeforeCreate", createBy)).resolves.toBeDefined();
730
-
731
- expect(createBy.data.minimumSampleCount).toBe(100);
732
- expect(createBy.data.refireSuppressionMinutes).toBe(360);
733
- expect(typeof createBy.data.minimumSampleCount).toBe("number");
734
- expect(typeof createBy.data.refireSuppressionMinutes).toBe("number");
1158
+ expect(findByArguments.props).toEqual({ isRoot: true });
735
1159
  });
736
1160
 
737
- it("rejects a non-numeric minimum sample count", async () => {
738
- await expectBadData(
739
- callHook(
740
- "onBeforeCreate",
741
- makeCreateBy({
742
- burnRateThreshold: 6,
743
- longWindowInMinutes: 360,
744
- shortWindowInMinutes: 30,
745
- minimumSampleCount: "abc",
746
- }),
747
- ),
748
- "Minimum sample count must be a number greater than or equal to 0.",
749
- );
750
- });
1161
+ const acceptedAgainstSibling: Array<{
1162
+ label: string;
1163
+ fields: RuleFields;
1164
+ persisted: RuleFields;
1165
+ }> = [
1166
+ {
1167
+ label: "turning alerts off on a rule that declares incidents",
1168
+ fields: { shouldCreateAlert: false },
1169
+ persisted: { shouldCreateIncident: true },
1170
+ },
1171
+ {
1172
+ label: "turning incidents off on a rule that still raises alerts",
1173
+ fields: { shouldCreateIncident: false },
1174
+ persisted: { shouldCreateAlert: true },
1175
+ },
1176
+ {
1177
+ label:
1178
+ "turning incidents off on a row that never stored shouldCreateAlert, which defaults to true",
1179
+ fields: { shouldCreateIncident: false },
1180
+ persisted: {},
1181
+ },
1182
+ {
1183
+ /*
1184
+ * Legacy rows can hold a string in a Boolean column - written before the
1185
+ * coercion existed, or by a raw API call - so the PERSISTED sibling is
1186
+ * coerced too. Reading "true" as truthy happens to work; reading "false"
1187
+ * as truthy (the case below) does not.
1188
+ */
1189
+ label: 'turning alerts off on a legacy row storing "true" for incidents',
1190
+ fields: { shouldCreateAlert: false },
1191
+ persisted: { shouldCreateIncident: "true" },
1192
+ },
1193
+ ];
751
1194
 
752
- it("rejects a negative re-fire suppression window", async () => {
753
- await expectBadData(
754
- callHook(
755
- "onBeforeUpdate",
756
- makeUpdateBy({ refireSuppressionMinutes: "-5" }),
757
- ),
758
- "Re-fire suppression must be a number of minutes greater than or equal to 0.",
759
- );
760
- });
1195
+ acceptedAgainstSibling.forEach(
1196
+ (accepted: {
1197
+ label: string;
1198
+ fields: RuleFields;
1199
+ persisted: RuleFields;
1200
+ }): void => {
1201
+ it(`accepts ${accepted.label}`, async () => {
1202
+ findBySpy.mockResolvedValue([
1203
+ makeRule({
1204
+ _id: RULE_ID.toString(),
1205
+ id: RULE_ID,
1206
+ ...accepted.persisted,
1207
+ }),
1208
+ ]);
761
1209
 
762
- it("accepts both windows as strings in one update payload and writes numbers back", async () => {
763
- const updateBy: UpdateBy<ServiceLevelObjectiveBurnRateRule> = makeUpdateBy({
764
- longWindowInMinutes: "1440",
765
- shortWindowInMinutes: "60",
766
- });
1210
+ await expect(
1211
+ callHook("onBeforeUpdate", makeUpdateBy(accepted.fields)),
1212
+ ).resolves.toBeDefined();
767
1213
 
768
- await expect(callHook("onBeforeUpdate", updateBy)).resolves.toBeDefined();
1214
+ expect(findBySpy).toHaveBeenCalledTimes(1);
1215
+ });
1216
+ },
1217
+ );
769
1218
 
770
- expect(updateBy.data.longWindowInMinutes).toBe(1440);
771
- expect(updateBy.data.shortWindowInMinutes).toBe(60);
772
- expect(typeof updateBy.data.longWindowInMinutes).toBe("number");
773
- expect(typeof updateBy.data.shortWindowInMinutes).toBe("number");
774
- expect(findBySpy).not.toHaveBeenCalled();
775
- });
1219
+ const rejectedAgainstSibling: Array<{
1220
+ label: string;
1221
+ fields: RuleFields;
1222
+ persisted: RuleFields;
1223
+ }> = [
1224
+ {
1225
+ label: "turning alerts off on a rule that declares nothing else",
1226
+ fields: { shouldCreateAlert: false },
1227
+ persisted: { shouldCreateIncident: false },
1228
+ },
1229
+ {
1230
+ label: "turning incidents off on a rule whose alerts are already off",
1231
+ fields: { shouldCreateIncident: false },
1232
+ persisted: { shouldCreateAlert: false },
1233
+ },
1234
+ {
1235
+ label:
1236
+ "turning alerts off on a row that never stored shouldCreateIncident, which defaults to false",
1237
+ fields: { shouldCreateAlert: false },
1238
+ persisted: {},
1239
+ },
1240
+ {
1241
+ /*
1242
+ * The legacy-row regression: "false" is a truthy string, so a sibling
1243
+ * read without coercion says "this rule still raises alerts" and the
1244
+ * update leaves a rule declaring nothing.
1245
+ */
1246
+ label: 'turning incidents off on a legacy row storing "false" for alerts',
1247
+ fields: { shouldCreateIncident: false },
1248
+ persisted: { shouldCreateAlert: "false" },
1249
+ },
1250
+ ];
776
1251
 
777
- it("rejects an inverted string window pair in one update payload", async () => {
778
- await expectBadData(
779
- callHook(
780
- "onBeforeUpdate",
781
- makeUpdateBy({
782
- longWindowInMinutes: "60",
783
- shortWindowInMinutes: "360",
784
- }),
785
- ),
786
- LONG_WINDOW_ERROR_MESSAGE,
787
- );
788
- });
1252
+ rejectedAgainstSibling.forEach(
1253
+ (rejected: {
1254
+ label: string;
1255
+ fields: RuleFields;
1256
+ persisted: RuleFields;
1257
+ }): void => {
1258
+ it(`rejects ${rejected.label}`, async () => {
1259
+ findBySpy.mockResolvedValue([
1260
+ makeRule({
1261
+ _id: RULE_ID.toString(),
1262
+ id: RULE_ID,
1263
+ ...rejected.persisted,
1264
+ }),
1265
+ ]);
789
1266
 
790
- it("compares a string long-window update numerically against the persisted short window", async () => {
1267
+ await expectBadData(
1268
+ callHook("onBeforeUpdate", makeUpdateBy(rejected.fields)),
1269
+ NO_OUTPUT_ERROR_MESSAGE,
1270
+ );
1271
+ });
1272
+ },
1273
+ );
1274
+
1275
+ it("rejects a bulk update as soon as ANY matched rule would be left declaring nothing", async () => {
791
1276
  findBySpy.mockResolvedValue([
792
1277
  makeRule({
793
1278
  _id: RULE_ID.toString(),
794
1279
  id: RULE_ID,
795
- longWindowInMinutes: 60,
796
- shortWindowInMinutes: 30,
1280
+ shouldCreateIncident: true,
1281
+ }),
1282
+ makeRule({
1283
+ _id: OTHER_RULE_ID.toString(),
1284
+ id: OTHER_RULE_ID,
1285
+ shouldCreateIncident: false,
797
1286
  }),
798
1287
  ]);
799
1288
 
800
- const updateBy: UpdateBy<ServiceLevelObjectiveBurnRateRule> = makeUpdateBy({
801
- longWindowInMinutes: "1440",
802
- });
803
-
804
- await expect(callHook("onBeforeUpdate", updateBy)).resolves.toBeDefined();
805
-
806
- expect(updateBy.data.longWindowInMinutes).toBe(1440);
807
- expect(typeof updateBy.data.longWindowInMinutes).toBe("number");
1289
+ /*
1290
+ * The first rule survives losing its alert because it declares incidents;
1291
+ * the second does not - and one casualty fails the whole update.
1292
+ */
1293
+ await expectBadData(
1294
+ callHook("onBeforeUpdate", makeUpdateBy({ shouldCreateAlert: false })),
1295
+ NO_OUTPUT_ERROR_MESSAGE,
1296
+ );
808
1297
  });
809
1298
 
810
- /*
811
- * Rows written before this fix can still hold a string in a Number/Decimal
812
- * column, so the PERSISTED sibling has to be coerced as well - otherwise the
813
- * comparison against it is lexicographic all over again.
814
- */
815
- it("coerces the persisted sibling window, so a legacy string row still compares numerically", async () => {
1299
+ it("accepts a bulk update that leaves every matched rule still declaring something", async () => {
816
1300
  findBySpy.mockResolvedValue([
817
1301
  makeRule({
818
1302
  _id: RULE_ID.toString(),
819
1303
  id: RULE_ID,
820
- // A row that predates coercion: the short window is stored as text.
821
- shortWindowInMinutes: "60",
1304
+ shouldCreateIncident: true,
1305
+ }),
1306
+ makeRule({
1307
+ _id: OTHER_RULE_ID.toString(),
1308
+ id: OTHER_RULE_ID,
1309
+ shouldCreateIncident: true,
822
1310
  }),
823
1311
  ]);
824
1312
 
825
1313
  await expect(
826
- callHook("onBeforeUpdate", makeUpdateBy({ longWindowInMinutes: 1440 })),
1314
+ callHook("onBeforeUpdate", makeUpdateBy({ shouldCreateAlert: false })),
827
1315
  ).resolves.toBeDefined();
828
1316
  });
829
1317
 
830
- it("rejects a short-window update that overtakes a persisted long window stored as a string", async () => {
1318
+ it("accepts a single output switched off when the query matches no rows at all", async () => {
1319
+ findBySpy.mockResolvedValue([]);
1320
+
1321
+ await expect(
1322
+ callHook("onBeforeUpdate", makeUpdateBy({ shouldCreateIncident: false })),
1323
+ ).resolves.toBeDefined();
1324
+ });
1325
+
1326
+ it("writes both coerced booleans back onto the update payload", async () => {
1327
+ const updateBy: UpdateBy<ServiceLevelObjectiveBurnRateRule> = makeUpdateBy({
1328
+ shouldCreateAlert: "false",
1329
+ shouldCreateIncident: "true",
1330
+ });
1331
+
1332
+ await expect(callHook("onBeforeUpdate", updateBy)).resolves.toBeDefined();
1333
+
1334
+ /*
1335
+ * onUpdateSuccess decides what to resolve with a strict `=== false`, so a
1336
+ * string that survived this hook would silently strand the open alert the
1337
+ * user just switched off.
1338
+ */
1339
+ expect(updateBy.data.shouldCreateAlert).toBe(false);
1340
+ expect(updateBy.data.shouldCreateIncident).toBe(true);
1341
+ expect(typeof updateBy.data.shouldCreateAlert).toBe("boolean");
1342
+ expect(typeof updateBy.data.shouldCreateIncident).toBe("boolean");
1343
+ });
1344
+
1345
+ it("writes the coerced boolean back even when only one flag is being written", async () => {
831
1346
  findBySpy.mockResolvedValue([
832
1347
  makeRule({
833
1348
  _id: RULE_ID.toString(),
834
1349
  id: RULE_ID,
835
- // "60" lexicographically precedes "360", which used to let this pass.
836
- longWindowInMinutes: "60",
1350
+ shouldCreateIncident: true,
837
1351
  }),
838
1352
  ]);
839
1353
 
840
- await expectBadData(
841
- callHook("onBeforeUpdate", makeUpdateBy({ shortWindowInMinutes: "360" })),
1354
+ const updateBy: UpdateBy<ServiceLevelObjectiveBurnRateRule> = makeUpdateBy({
1355
+ shouldCreateAlert: "0",
1356
+ });
1357
+
1358
+ await expect(callHook("onBeforeUpdate", updateBy)).resolves.toBeDefined();
1359
+
1360
+ expect(updateBy.data.shouldCreateAlert).toBe(false);
1361
+ expect(typeof updateBy.data.shouldCreateAlert).toBe("boolean");
1362
+ });
1363
+ });
1364
+
1365
+ /*
1366
+ * The update twin of the severity guard. A root or API update does not always
1367
+ * carry a tenant, so the projects to validate against come from the rows the
1368
+ * query actually matches.
1369
+ */
1370
+ describe("ServiceLevelObjectiveBurnRateRuleService.onBeforeUpdate - severity references", () => {
1371
+ let findBySpy: jest.SpyInstance;
1372
+ let validatorSpy: jest.SpyInstance;
1373
+
1374
+ beforeEach(() => {
1375
+ findBySpy = jest
1376
+ .spyOn(ServiceLevelObjectiveBurnRateRuleService, "findBy")
1377
+ .mockResolvedValue([]);
1378
+
1379
+ validatorSpy = jest
1380
+ .spyOn(
1381
+ ProjectScopedReferenceValidator,
1382
+ "validateReferencesBelongToProject",
1383
+ )
1384
+ .mockResolvedValue(undefined);
1385
+ });
1386
+
1387
+ afterEach(() => {
1388
+ jest.restoreAllMocks();
1389
+ });
1390
+
1391
+ it("neither validates nor reads any row when no severity is being changed", async () => {
1392
+ await expect(
1393
+ callHook("onBeforeUpdate", makeUpdateBy({ name: "Renamed rule" })),
1394
+ ).resolves.toBeDefined();
1395
+
1396
+ expect(validatorSpy).not.toHaveBeenCalled();
1397
+ expect(findBySpy).not.toHaveBeenCalled();
1398
+ });
1399
+
1400
+ const updatedSeverities: Array<{
1401
+ label: string;
1402
+ fields: RuleFields;
1403
+ expectedAlertSeverityId: ObjectID | undefined;
1404
+ expectedIncidentSeverityId: ObjectID | undefined;
1405
+ }> = [
1406
+ {
1407
+ label: "an alert severity alone",
1408
+ fields: { alertSeverityId: ALERT_SEVERITY_ID },
1409
+ expectedAlertSeverityId: ALERT_SEVERITY_ID,
1410
+ expectedIncidentSeverityId: undefined,
1411
+ },
1412
+ {
1413
+ label: "an incident severity alone",
1414
+ fields: { incidentSeverityId: INCIDENT_SEVERITY_ID },
1415
+ expectedAlertSeverityId: undefined,
1416
+ expectedIncidentSeverityId: INCIDENT_SEVERITY_ID,
1417
+ },
1418
+ {
1419
+ label: "both severities at once",
1420
+ fields: {
1421
+ alertSeverityId: ALERT_SEVERITY_ID,
1422
+ incidentSeverityId: INCIDENT_SEVERITY_ID,
1423
+ },
1424
+ expectedAlertSeverityId: ALERT_SEVERITY_ID,
1425
+ expectedIncidentSeverityId: INCIDENT_SEVERITY_ID,
1426
+ },
1427
+ ];
1428
+
1429
+ updatedSeverities.forEach(
1430
+ (updatedSeverity: {
1431
+ label: string;
1432
+ fields: RuleFields;
1433
+ expectedAlertSeverityId: ObjectID | undefined;
1434
+ expectedIncidentSeverityId: ObjectID | undefined;
1435
+ }): void => {
1436
+ it(`validates ${updatedSeverity.label} against the caller's tenant, without reading any row`, async () => {
1437
+ await expect(
1438
+ callHook(
1439
+ "onBeforeUpdate",
1440
+ makeUpdateBy(updatedSeverity.fields, {
1441
+ isRoot: true,
1442
+ tenantId: PROJECT_ID,
1443
+ }),
1444
+ ),
1445
+ ).resolves.toBeDefined();
1446
+
1447
+ // A known tenant makes the row read unnecessary.
1448
+ expect(findBySpy).not.toHaveBeenCalled();
1449
+ expect(validatorSpy).toHaveBeenCalledTimes(1);
1450
+
1451
+ const validated: ValidatedReferences = validatedReferencesAt(
1452
+ validatorSpy,
1453
+ 0,
1454
+ );
1455
+
1456
+ expect(validated.projectId).toEqual(PROJECT_ID);
1457
+ expect(validated.subject).toBe("SLO burn rate rule");
1458
+ expect(validated.references[0]!.id).toEqual(
1459
+ updatedSeverity.expectedAlertSeverityId,
1460
+ );
1461
+ expect(validated.references[1]!.id).toEqual(
1462
+ updatedSeverity.expectedIncidentSeverityId,
1463
+ );
1464
+ });
1465
+ },
1466
+ );
1467
+
1468
+ it("falls back to the projects of the matched rows, deduplicated, when the update carries no tenant", async () => {
1469
+ findBySpy.mockResolvedValue([
1470
+ makeRule({
1471
+ _id: RULE_ID.toString(),
1472
+ id: RULE_ID,
1473
+ projectId: PROJECT_ID,
1474
+ }),
1475
+ makeRule({
1476
+ _id: OTHER_RULE_ID.toString(),
1477
+ id: OTHER_RULE_ID,
1478
+ projectId: PROJECT_ID,
1479
+ }),
1480
+ ]);
1481
+
1482
+ await expect(
1483
+ callHook(
1484
+ "onBeforeUpdate",
1485
+ makeUpdateBy({ incidentSeverityId: INCIDENT_SEVERITY_ID }),
1486
+ ),
1487
+ ).resolves.toBeDefined();
1488
+
1489
+ expect(findBySpy).toHaveBeenCalledTimes(1);
1490
+ expect(findByArgumentsAt(findBySpy, 0).select).toEqual({
1491
+ projectId: true,
1492
+ });
1493
+ expect(findByArgumentsAt(findBySpy, 0).props).toEqual({ isRoot: true });
1494
+
1495
+ // Two rules, one project: the lookup must not run per rule.
1496
+ expect(validatorSpy).toHaveBeenCalledTimes(1);
1497
+ expect(validatedReferencesAt(validatorSpy, 0).projectId).toEqual(
1498
+ PROJECT_ID,
1499
+ );
1500
+ });
1501
+
1502
+ it("validates each distinct project when the query spans two of them", async () => {
1503
+ findBySpy.mockResolvedValue([
1504
+ makeRule({
1505
+ _id: RULE_ID.toString(),
1506
+ id: RULE_ID,
1507
+ projectId: PROJECT_ID,
1508
+ }),
1509
+ makeRule({
1510
+ _id: OTHER_RULE_ID.toString(),
1511
+ id: OTHER_RULE_ID,
1512
+ projectId: OTHER_PROJECT_ID,
1513
+ }),
1514
+ ]);
1515
+
1516
+ await expect(
1517
+ callHook(
1518
+ "onBeforeUpdate",
1519
+ makeUpdateBy({ alertSeverityId: ALERT_SEVERITY_ID }),
1520
+ ),
1521
+ ).resolves.toBeDefined();
1522
+
1523
+ expect(validatorSpy).toHaveBeenCalledTimes(2);
1524
+ expect(validatedReferencesAt(validatorSpy, 0).projectId).toEqual(
1525
+ PROJECT_ID,
1526
+ );
1527
+ expect(validatedReferencesAt(validatorSpy, 1).projectId).toEqual(
1528
+ OTHER_PROJECT_ID,
1529
+ );
1530
+ });
1531
+
1532
+ it("validates nothing when no matched row carries a project", async () => {
1533
+ findBySpy.mockResolvedValue([
1534
+ makeRule({ _id: RULE_ID.toString(), id: RULE_ID }),
1535
+ ]);
1536
+
1537
+ await expect(
1538
+ callHook(
1539
+ "onBeforeUpdate",
1540
+ makeUpdateBy({ alertSeverityId: ALERT_SEVERITY_ID }),
1541
+ ),
1542
+ ).resolves.toBeDefined();
1543
+
1544
+ expect(validatorSpy).not.toHaveBeenCalled();
1545
+ });
1546
+
1547
+ it("lets a cross-project severity rejection abort the update", async () => {
1548
+ validatorSpy.mockRejectedValue(
1549
+ new BadDataException(
1550
+ "This SLO burn rate rule references records that belong to a different project.",
1551
+ ),
1552
+ );
1553
+
1554
+ await expectBadData(
1555
+ callHook(
1556
+ "onBeforeUpdate",
1557
+ makeUpdateBy(
1558
+ { alertSeverityId: ALERT_SEVERITY_ID },
1559
+ { isRoot: true, tenantId: PROJECT_ID },
1560
+ ),
1561
+ ),
1562
+ "belong to a different project",
1563
+ );
1564
+ });
1565
+ });
1566
+
1567
+ /*
1568
+ * Regression: the window columns arrive as STRINGS from the dashboard (number
1569
+ * form fields hand Formik `e.target.value`, and neither ModelForm nor
1570
+ * BaseModel.fromJSON coerces Number/Decimal columns), so
1571
+ * `long <= short` used to be a LEXICOGRAPHIC comparison:
1572
+ *
1573
+ * long "1440", short "60" -> "1440" <= "60" is TRUE -> wrongly REJECTED
1574
+ * (a 24h/60m rule could not be created at all)
1575
+ * long "120", short "30" -> "120" <= "30" is TRUE -> wrongly REJECTED
1576
+ * long "60", short "360" -> "60" <= "360" is FALSE -> wrongly ACCEPTED
1577
+ * (a short window LONGER than the long one,
1578
+ * which breaks the whole multi-window design)
1579
+ *
1580
+ * Every numeric column is now coerced before validation and the coerced number
1581
+ * is written back onto the payload.
1582
+ */
1583
+ describe("ServiceLevelObjectiveBurnRateRuleService - numeric columns supplied as strings", () => {
1584
+ let findBySpy: jest.SpyInstance;
1585
+
1586
+ beforeEach(() => {
1587
+ findBySpy = jest.spyOn(ServiceLevelObjectiveBurnRateRuleService, "findBy");
1588
+ });
1589
+
1590
+ afterEach(() => {
1591
+ jest.restoreAllMocks();
1592
+ });
1593
+
1594
+ const lexicographicTraps: Array<{
1595
+ label: string;
1596
+ longWindowInMinutes: string;
1597
+ shortWindowInMinutes: string;
1598
+ }> = [
1599
+ {
1600
+ label: "a 24 hour long window over a 60 minute short window",
1601
+ longWindowInMinutes: "1440",
1602
+ shortWindowInMinutes: "60",
1603
+ },
1604
+ {
1605
+ label: "a 2 hour long window over a 30 minute short window",
1606
+ longWindowInMinutes: "120",
1607
+ shortWindowInMinutes: "30",
1608
+ },
1609
+ {
1610
+ label: "a 6 hour long window over a 5 minute short window",
1611
+ longWindowInMinutes: "360",
1612
+ shortWindowInMinutes: "5",
1613
+ },
1614
+ ];
1615
+
1616
+ for (const trap of lexicographicTraps) {
1617
+ it(`accepts ${trap.label} sent as strings, and persists both as numbers`, async () => {
1618
+ const createBy: CreateBy<ServiceLevelObjectiveBurnRateRule> =
1619
+ makeCreateBy({
1620
+ burnRateThreshold: "14.4",
1621
+ longWindowInMinutes: trap.longWindowInMinutes,
1622
+ shortWindowInMinutes: trap.shortWindowInMinutes,
1623
+ });
1624
+
1625
+ await expect(callHook("onBeforeCreate", createBy)).resolves.toBeDefined();
1626
+
1627
+ expect(createBy.data.longWindowInMinutes).toBe(
1628
+ Number(trap.longWindowInMinutes),
1629
+ );
1630
+ expect(createBy.data.shortWindowInMinutes).toBe(
1631
+ Number(trap.shortWindowInMinutes),
1632
+ );
1633
+ expect(createBy.data.burnRateThreshold).toBe(14.4);
1634
+
1635
+ expect(typeof createBy.data.longWindowInMinutes).toBe("number");
1636
+ expect(typeof createBy.data.shortWindowInMinutes).toBe("number");
1637
+ expect(typeof createBy.data.burnRateThreshold).toBe("number");
1638
+ });
1639
+ }
1640
+
1641
+ it('rejects a short window LONGER than the long window when both are strings ("60" / "360")', async () => {
1642
+ await expectBadData(
1643
+ callHook(
1644
+ "onBeforeCreate",
1645
+ makeCreateBy({
1646
+ burnRateThreshold: 14.4,
1647
+ longWindowInMinutes: "60",
1648
+ shortWindowInMinutes: "360",
1649
+ }),
1650
+ ),
842
1651
  LONG_WINDOW_ERROR_MESSAGE,
843
1652
  );
844
1653
  });
845
1654
 
846
- it("rejects a non-numeric single-window update before it even reads the rows", async () => {
1655
+ it("rejects string windows that are equal", async () => {
847
1656
  await expectBadData(
848
- callHook("onBeforeUpdate", makeUpdateBy({ longWindowInMinutes: "abc" })),
849
- WINDOWS_NOT_NUMERIC_ERROR_MESSAGE,
1657
+ callHook(
1658
+ "onBeforeCreate",
1659
+ makeCreateBy({
1660
+ burnRateThreshold: 14.4,
1661
+ longWindowInMinutes: "60",
1662
+ shortWindowInMinutes: "60",
1663
+ }),
1664
+ ),
1665
+ LONG_WINDOW_ERROR_MESSAGE,
1666
+ );
1667
+ });
1668
+
1669
+ it('rejects a short window of "0" as a string', async () => {
1670
+ await expectBadData(
1671
+ callHook(
1672
+ "onBeforeCreate",
1673
+ makeCreateBy({
1674
+ burnRateThreshold: 14.4,
1675
+ longWindowInMinutes: "60",
1676
+ shortWindowInMinutes: "0",
1677
+ }),
1678
+ ),
1679
+ SHORT_WINDOW_ERROR_MESSAGE,
850
1680
  );
1681
+ });
1682
+
1683
+ const nonNumericWindows: Array<{ label: string; value: unknown }> = [
1684
+ { label: "an empty string", value: "" },
1685
+ { label: "a whitespace-only string", value: " " },
1686
+ { label: "a non-numeric string", value: "abc" },
1687
+ { label: "NaN", value: Number.NaN },
1688
+ { label: "a boolean", value: true },
1689
+ ];
1690
+
1691
+ for (const nonNumericWindow of nonNumericWindows) {
1692
+ it(`rejects a long window of ${nonNumericWindow.label} on create`, async () => {
1693
+ await expectBadData(
1694
+ callHook(
1695
+ "onBeforeCreate",
1696
+ makeCreateBy({
1697
+ burnRateThreshold: 14.4,
1698
+ longWindowInMinutes: nonNumericWindow.value,
1699
+ shortWindowInMinutes: 5,
1700
+ }),
1701
+ ),
1702
+ WINDOWS_NOT_NUMERIC_ERROR_MESSAGE,
1703
+ );
1704
+ });
1705
+
1706
+ it(`rejects a short window of ${nonNumericWindow.label} on create`, async () => {
1707
+ await expectBadData(
1708
+ callHook(
1709
+ "onBeforeCreate",
1710
+ makeCreateBy({
1711
+ burnRateThreshold: 14.4,
1712
+ longWindowInMinutes: 60,
1713
+ shortWindowInMinutes: nonNumericWindow.value,
1714
+ }),
1715
+ ),
1716
+ WINDOWS_NOT_NUMERIC_ERROR_MESSAGE,
1717
+ );
1718
+ });
1719
+ }
1720
+
1721
+ const rejectedThresholdStrings: Array<{ label: string; value: unknown }> = [
1722
+ { label: "an empty string", value: "" },
1723
+ { label: "a non-numeric string", value: "abc" },
1724
+ { label: 'the string "0"', value: "0" },
1725
+ { label: 'the string "-1"', value: "-1" },
1726
+ ];
1727
+
1728
+ for (const rejectedThresholdString of rejectedThresholdStrings) {
1729
+ it(`rejects a threshold of ${rejectedThresholdString.label} on create`, async () => {
1730
+ await expectBadData(
1731
+ callHook(
1732
+ "onBeforeCreate",
1733
+ makeCreateBy({
1734
+ burnRateThreshold: rejectedThresholdString.value,
1735
+ longWindowInMinutes: 60,
1736
+ shortWindowInMinutes: 5,
1737
+ }),
1738
+ ),
1739
+ THRESHOLD_ERROR_MESSAGE,
1740
+ );
1741
+ });
1742
+
1743
+ it(`rejects a threshold of ${rejectedThresholdString.label} on update`, async () => {
1744
+ await expectBadData(
1745
+ callHook(
1746
+ "onBeforeUpdate",
1747
+ makeUpdateBy({ burnRateThreshold: rejectedThresholdString.value }),
1748
+ ),
1749
+ THRESHOLD_ERROR_MESSAGE,
1750
+ );
1751
+ });
1752
+ }
1753
+
1754
+ it("coerces the optional numeric columns too", async () => {
1755
+ const createBy: CreateBy<ServiceLevelObjectiveBurnRateRule> = makeCreateBy({
1756
+ burnRateThreshold: "6",
1757
+ longWindowInMinutes: "360",
1758
+ shortWindowInMinutes: "30",
1759
+ minimumSampleCount: "100",
1760
+ refireSuppressionMinutes: "360",
1761
+ });
1762
+
1763
+ await expect(callHook("onBeforeCreate", createBy)).resolves.toBeDefined();
1764
+
1765
+ expect(createBy.data.minimumSampleCount).toBe(100);
1766
+ expect(createBy.data.refireSuppressionMinutes).toBe(360);
1767
+ expect(typeof createBy.data.minimumSampleCount).toBe("number");
1768
+ expect(typeof createBy.data.refireSuppressionMinutes).toBe("number");
1769
+ });
1770
+
1771
+ it("rejects a non-numeric minimum sample count", async () => {
1772
+ await expectBadData(
1773
+ callHook(
1774
+ "onBeforeCreate",
1775
+ makeCreateBy({
1776
+ burnRateThreshold: 6,
1777
+ longWindowInMinutes: 360,
1778
+ shortWindowInMinutes: 30,
1779
+ minimumSampleCount: "abc",
1780
+ }),
1781
+ ),
1782
+ "Minimum sample count must be a number greater than or equal to 0.",
1783
+ );
1784
+ });
1785
+
1786
+ it("rejects a negative re-fire suppression window", async () => {
1787
+ await expectBadData(
1788
+ callHook(
1789
+ "onBeforeUpdate",
1790
+ makeUpdateBy({ refireSuppressionMinutes: "-5" }),
1791
+ ),
1792
+ "Re-fire suppression must be a number of minutes greater than or equal to 0.",
1793
+ );
1794
+ });
1795
+
1796
+ it("accepts both windows as strings in one update payload and writes numbers back", async () => {
1797
+ const updateBy: UpdateBy<ServiceLevelObjectiveBurnRateRule> = makeUpdateBy({
1798
+ longWindowInMinutes: "1440",
1799
+ shortWindowInMinutes: "60",
1800
+ });
1801
+
1802
+ await expect(callHook("onBeforeUpdate", updateBy)).resolves.toBeDefined();
1803
+
1804
+ expect(updateBy.data.longWindowInMinutes).toBe(1440);
1805
+ expect(updateBy.data.shortWindowInMinutes).toBe(60);
1806
+ expect(typeof updateBy.data.longWindowInMinutes).toBe("number");
1807
+ expect(typeof updateBy.data.shortWindowInMinutes).toBe("number");
1808
+ expect(findBySpy).not.toHaveBeenCalled();
1809
+ });
1810
+
1811
+ it("rejects an inverted string window pair in one update payload", async () => {
1812
+ await expectBadData(
1813
+ callHook(
1814
+ "onBeforeUpdate",
1815
+ makeUpdateBy({
1816
+ longWindowInMinutes: "60",
1817
+ shortWindowInMinutes: "360",
1818
+ }),
1819
+ ),
1820
+ LONG_WINDOW_ERROR_MESSAGE,
1821
+ );
1822
+ });
1823
+
1824
+ it("compares a string long-window update numerically against the persisted short window", async () => {
1825
+ findBySpy.mockResolvedValue([
1826
+ makeRule({
1827
+ _id: RULE_ID.toString(),
1828
+ id: RULE_ID,
1829
+ longWindowInMinutes: 60,
1830
+ shortWindowInMinutes: 30,
1831
+ }),
1832
+ ]);
1833
+
1834
+ const updateBy: UpdateBy<ServiceLevelObjectiveBurnRateRule> = makeUpdateBy({
1835
+ longWindowInMinutes: "1440",
1836
+ });
1837
+
1838
+ await expect(callHook("onBeforeUpdate", updateBy)).resolves.toBeDefined();
1839
+
1840
+ expect(updateBy.data.longWindowInMinutes).toBe(1440);
1841
+ expect(typeof updateBy.data.longWindowInMinutes).toBe("number");
1842
+ });
1843
+
1844
+ /*
1845
+ * Rows written before this fix can still hold a string in a Number/Decimal
1846
+ * column, so the PERSISTED sibling has to be coerced as well - otherwise the
1847
+ * comparison against it is lexicographic all over again.
1848
+ */
1849
+ it("coerces the persisted sibling window, so a legacy string row still compares numerically", async () => {
1850
+ findBySpy.mockResolvedValue([
1851
+ makeRule({
1852
+ _id: RULE_ID.toString(),
1853
+ id: RULE_ID,
1854
+ // A row that predates coercion: the short window is stored as text.
1855
+ shortWindowInMinutes: "60",
1856
+ }),
1857
+ ]);
1858
+
1859
+ await expect(
1860
+ callHook("onBeforeUpdate", makeUpdateBy({ longWindowInMinutes: 1440 })),
1861
+ ).resolves.toBeDefined();
1862
+ });
1863
+
1864
+ it("rejects a short-window update that overtakes a persisted long window stored as a string", async () => {
1865
+ findBySpy.mockResolvedValue([
1866
+ makeRule({
1867
+ _id: RULE_ID.toString(),
1868
+ id: RULE_ID,
1869
+ // "60" lexicographically precedes "360", which used to let this pass.
1870
+ longWindowInMinutes: "60",
1871
+ }),
1872
+ ]);
1873
+
1874
+ await expectBadData(
1875
+ callHook("onBeforeUpdate", makeUpdateBy({ shortWindowInMinutes: "360" })),
1876
+ LONG_WINDOW_ERROR_MESSAGE,
1877
+ );
1878
+ });
1879
+
1880
+ it("rejects a non-numeric single-window update before it even reads the rows", async () => {
1881
+ await expectBadData(
1882
+ callHook("onBeforeUpdate", makeUpdateBy({ longWindowInMinutes: "abc" })),
1883
+ WINDOWS_NOT_NUMERIC_ERROR_MESSAGE,
1884
+ );
1885
+
1886
+ expect(findBySpy).not.toHaveBeenCalled();
1887
+ });
1888
+
1889
+ it("rejects a persisted sibling that is not numeric at all, rather than accepting a NaN comparison", async () => {
1890
+ findBySpy.mockResolvedValue([
1891
+ makeRule({
1892
+ _id: RULE_ID.toString(),
1893
+ id: RULE_ID,
1894
+ shortWindowInMinutes: undefined,
1895
+ }),
1896
+ ]);
1897
+
1898
+ await expectBadData(
1899
+ callHook("onBeforeUpdate", makeUpdateBy({ longWindowInMinutes: 1440 })),
1900
+ WINDOWS_NOT_NUMERIC_ERROR_MESSAGE,
1901
+ );
1902
+ });
1903
+
1904
+ it("coerces a string single-window update even when the query matches no rows", async () => {
1905
+ findBySpy.mockResolvedValue([]);
1906
+
1907
+ const updateBy: UpdateBy<ServiceLevelObjectiveBurnRateRule> = makeUpdateBy({
1908
+ shortWindowInMinutes: "30",
1909
+ });
1910
+
1911
+ await expect(callHook("onBeforeUpdate", updateBy)).resolves.toBeDefined();
1912
+
1913
+ expect(updateBy.data.shortWindowInMinutes).toBe(30);
1914
+ expect(typeof updateBy.data.shortWindowInMinutes).toBe("number");
1915
+ });
1916
+
1917
+ it("writes a coerced threshold back onto the update payload", async () => {
1918
+ const updateBy: UpdateBy<ServiceLevelObjectiveBurnRateRule> = makeUpdateBy({
1919
+ burnRateThreshold: "2.5",
1920
+ });
1921
+
1922
+ await expect(callHook("onBeforeUpdate", updateBy)).resolves.toBeDefined();
1923
+
1924
+ expect(updateBy.data.burnRateThreshold).toBe(2.5);
1925
+ expect(typeof updateBy.data.burnRateThreshold).toBe("number");
1926
+ });
1927
+ });
1928
+
1929
+ describe("ServiceLevelObjectiveBurnRateRuleService.getBurnRateFingerprint", () => {
1930
+ afterEach(() => {
1931
+ jest.restoreAllMocks();
1932
+ });
1933
+
1934
+ it("is exactly slo:<sloId>:burn-rule:<ruleId>", () => {
1935
+ expect(
1936
+ ServiceLevelObjectiveBurnRateRuleService.getBurnRateFingerprint({
1937
+ serviceLevelObjectiveId: SLO_ID,
1938
+ burnRateRuleId: RULE_ID,
1939
+ }),
1940
+ ).toBe(`slo:${SLO_ID.toString()}:burn-rule:${RULE_ID.toString()}`);
1941
+ });
1942
+
1943
+ it("is deterministic across calls", () => {
1944
+ const first: string =
1945
+ ServiceLevelObjectiveBurnRateRuleService.getBurnRateFingerprint({
1946
+ serviceLevelObjectiveId: SLO_ID,
1947
+ burnRateRuleId: RULE_ID,
1948
+ });
1949
+ const second: string =
1950
+ ServiceLevelObjectiveBurnRateRuleService.getBurnRateFingerprint({
1951
+ serviceLevelObjectiveId: SLO_ID,
1952
+ burnRateRuleId: RULE_ID,
1953
+ });
1954
+
1955
+ expect(second).toBe(first);
1956
+ });
1957
+
1958
+ it("distinguishes two rules of the same SLO", () => {
1959
+ const first: string =
1960
+ ServiceLevelObjectiveBurnRateRuleService.getBurnRateFingerprint({
1961
+ serviceLevelObjectiveId: SLO_ID,
1962
+ burnRateRuleId: RULE_ID,
1963
+ });
1964
+ const second: string =
1965
+ ServiceLevelObjectiveBurnRateRuleService.getBurnRateFingerprint({
1966
+ serviceLevelObjectiveId: SLO_ID,
1967
+ burnRateRuleId: OTHER_RULE_ID,
1968
+ });
1969
+
1970
+ expect(second).not.toBe(first);
1971
+ });
1972
+
1973
+ it("distinguishes the same rule id under two different SLOs", () => {
1974
+ const first: string =
1975
+ ServiceLevelObjectiveBurnRateRuleService.getBurnRateFingerprint({
1976
+ serviceLevelObjectiveId: SLO_ID,
1977
+ burnRateRuleId: RULE_ID,
1978
+ });
1979
+ const second: string =
1980
+ ServiceLevelObjectiveBurnRateRuleService.getBurnRateFingerprint({
1981
+ serviceLevelObjectiveId: OTHER_RULE_ID,
1982
+ burnRateRuleId: RULE_ID,
1983
+ });
1984
+
1985
+ expect(second).not.toBe(first);
1986
+ });
1987
+
1988
+ /*
1989
+ * One fingerprint, two tables - which is what the rename away from
1990
+ * getBurnRateAlertFingerprint was about. The Incident lookup must query with
1991
+ * the SAME string the Alert lookup does, so a rule that declared both is
1992
+ * found on both sides; two fingerprints would silently strand whichever side
1993
+ * drifted, with no error anywhere to notice it by.
1994
+ */
1995
+ it("is the exact string both the alert lookup and the incident lookup query with", async () => {
1996
+ const alertFindBySpy: jest.SpyInstance = jest
1997
+ .spyOn(AlertService, "findBy")
1998
+ .mockResolvedValue([]);
1999
+ const incidentFindBySpy: jest.SpyInstance = jest
2000
+ .spyOn(IncidentService, "findBy")
2001
+ .mockResolvedValue([]);
2002
+
2003
+ await ServiceLevelObjectiveBurnRateRuleService.resolveOpenAlertsForRule({
2004
+ serviceLevelObjectiveId: SLO_ID,
2005
+ burnRateRuleId: RULE_ID,
2006
+ projectId: PROJECT_ID,
2007
+ });
2008
+ await ServiceLevelObjectiveBurnRateRuleService.resolveOpenIncidentsForRule({
2009
+ serviceLevelObjectiveId: SLO_ID,
2010
+ burnRateRuleId: RULE_ID,
2011
+ projectId: PROJECT_ID,
2012
+ });
2013
+
2014
+ const fingerprint: string =
2015
+ ServiceLevelObjectiveBurnRateRuleService.getBurnRateFingerprint({
2016
+ serviceLevelObjectiveId: SLO_ID,
2017
+ burnRateRuleId: RULE_ID,
2018
+ });
2019
+
2020
+ expect(
2021
+ findByArgumentsAt(alertFindBySpy, 0).query["seriesFingerprint"],
2022
+ ).toBe(fingerprint);
2023
+ expect(
2024
+ findByArgumentsAt(incidentFindBySpy, 0).query["seriesFingerprint"],
2025
+ ).toBe(fingerprint);
2026
+ });
2027
+ });
2028
+
2029
+ describe("ServiceLevelObjectiveBurnRateRuleService.resolveOpenAlertsForRule", () => {
2030
+ let alertFindBySpy: jest.SpyInstance;
2031
+ let resolvedStateSpy: jest.SpyInstance;
2032
+ let timelineCreateSpy: jest.SpyInstance;
2033
+
2034
+ beforeEach(() => {
2035
+ alertFindBySpy = jest.spyOn(AlertService, "findBy").mockResolvedValue([]);
2036
+
2037
+ resolvedStateSpy = jest
2038
+ .spyOn(AlertStateTimelineService, "getResolvedStateIdForProject")
2039
+ .mockResolvedValue(RESOLVED_STATE_ID);
2040
+
2041
+ timelineCreateSpy = jest
2042
+ .spyOn(AlertStateTimelineService, "create")
2043
+ .mockResolvedValue(new AlertStateTimeline());
2044
+ });
2045
+
2046
+ afterEach(() => {
2047
+ jest.restoreAllMocks();
2048
+ });
2049
+
2050
+ function timelineRows(): Array<AlertStateTimeline> {
2051
+ return timelineCreateSpy.mock.calls.map((call: Array<unknown>) => {
2052
+ return (call[0] as CreateBy<AlertStateTimeline>).data;
2053
+ });
2054
+ }
2055
+
2056
+ it("looks up only the unresolved alerts carrying this rule's fingerprint", async () => {
2057
+ await ServiceLevelObjectiveBurnRateRuleService.resolveOpenAlertsForRule({
2058
+ serviceLevelObjectiveId: SLO_ID,
2059
+ burnRateRuleId: RULE_ID,
2060
+ projectId: PROJECT_ID,
2061
+ });
2062
+
2063
+ const findByArg: {
2064
+ query: Record<string, unknown>;
2065
+ select: Record<string, unknown>;
2066
+ props: Record<string, unknown>;
2067
+ } = alertFindBySpy.mock.calls[0]![0] as {
2068
+ query: Record<string, unknown>;
2069
+ select: Record<string, unknown>;
2070
+ props: Record<string, unknown>;
2071
+ };
2072
+
2073
+ expect(findByArg.query).toEqual({
2074
+ projectId: PROJECT_ID,
2075
+ seriesFingerprint: `slo:${SLO_ID.toString()}:burn-rule:${RULE_ID.toString()}`,
2076
+ currentAlertState: { isResolvedState: false },
2077
+ });
2078
+ // projectId is needed on each row to stamp the timeline entry.
2079
+ expect(findByArg.select).toEqual({ _id: true, projectId: true });
2080
+ expect(findByArg.props).toEqual({ isRoot: true });
2081
+ });
2082
+
2083
+ it("is a no-op when the rule has no open alerts", async () => {
2084
+ await ServiceLevelObjectiveBurnRateRuleService.resolveOpenAlertsForRule({
2085
+ serviceLevelObjectiveId: SLO_ID,
2086
+ burnRateRuleId: RULE_ID,
2087
+ projectId: PROJECT_ID,
2088
+ });
2089
+
2090
+ // It must not even resolve the project's resolved-state id.
2091
+ expect(resolvedStateSpy).not.toHaveBeenCalled();
2092
+ expect(timelineCreateSpy).not.toHaveBeenCalled();
2093
+ });
2094
+
2095
+ it("appends one resolved state timeline row per open alert", async () => {
2096
+ const firstAlertId: ObjectID = ObjectID.generate();
2097
+ const secondAlertId: ObjectID = ObjectID.generate();
2098
+
2099
+ alertFindBySpy.mockResolvedValue([
2100
+ makeAlert(firstAlertId),
2101
+ makeAlert(secondAlertId),
2102
+ ]);
2103
+
2104
+ await ServiceLevelObjectiveBurnRateRuleService.resolveOpenAlertsForRule({
2105
+ serviceLevelObjectiveId: SLO_ID,
2106
+ burnRateRuleId: RULE_ID,
2107
+ projectId: PROJECT_ID,
2108
+ });
2109
+
2110
+ // The resolved state is looked up once for the whole batch.
2111
+ expect(resolvedStateSpy).toHaveBeenCalledTimes(1);
2112
+ expect(resolvedStateSpy).toHaveBeenCalledWith(PROJECT_ID);
2113
+
2114
+ const rows: Array<AlertStateTimeline> = timelineRows();
2115
+ expect(rows).toHaveLength(2);
2116
+ expect(rows[0]!.alertId).toEqual(firstAlertId);
2117
+ expect(rows[1]!.alertId).toEqual(secondAlertId);
2118
+
2119
+ for (const row of rows) {
2120
+ expect(row.alertStateId).toEqual(RESOLVED_STATE_ID);
2121
+ expect(row.projectId).toEqual(PROJECT_ID);
2122
+ }
2123
+
2124
+ for (const call of timelineCreateSpy.mock.calls) {
2125
+ const createBy: CreateBy<AlertStateTimeline> = (
2126
+ call as Array<unknown>
2127
+ )[0] as CreateBy<AlertStateTimeline>;
2128
+ expect(createBy.props).toEqual({ isRoot: true });
2129
+ }
2130
+ });
2131
+
2132
+ it("falls back to the generic root cause when the caller supplies none", async () => {
2133
+ alertFindBySpy.mockResolvedValue([makeAlert(ObjectID.generate())]);
2134
+
2135
+ await ServiceLevelObjectiveBurnRateRuleService.resolveOpenAlertsForRule({
2136
+ serviceLevelObjectiveId: SLO_ID,
2137
+ burnRateRuleId: RULE_ID,
2138
+ projectId: PROJECT_ID,
2139
+ });
2140
+
2141
+ expect(timelineRows()[0]!.rootCause).toBe(DEFAULT_ROOT_CAUSE);
2142
+ });
2143
+
2144
+ it("threads the caller's root cause onto every resolved alert", async () => {
2145
+ alertFindBySpy.mockResolvedValue([
2146
+ makeAlert(ObjectID.generate()),
2147
+ makeAlert(ObjectID.generate()),
2148
+ ]);
2149
+
2150
+ await ServiceLevelObjectiveBurnRateRuleService.resolveOpenAlertsForRule({
2151
+ serviceLevelObjectiveId: SLO_ID,
2152
+ burnRateRuleId: RULE_ID,
2153
+ projectId: PROJECT_ID,
2154
+ rootCause: "Alert auto-resolved because the SLO was disabled.",
2155
+ });
2156
+
2157
+ for (const row of timelineRows()) {
2158
+ expect(row.rootCause).toBe(
2159
+ "Alert auto-resolved because the SLO was disabled.",
2160
+ );
2161
+ }
2162
+ });
2163
+
2164
+ it("ignores an explicitly undefined root cause and uses the fallback", async () => {
2165
+ alertFindBySpy.mockResolvedValue([makeAlert(ObjectID.generate())]);
2166
+
2167
+ await ServiceLevelObjectiveBurnRateRuleService.resolveOpenAlertsForRule({
2168
+ serviceLevelObjectiveId: SLO_ID,
2169
+ burnRateRuleId: RULE_ID,
2170
+ projectId: PROJECT_ID,
2171
+ rootCause: undefined,
2172
+ });
2173
+
2174
+ expect(timelineRows()[0]!.rootCause).toBe(DEFAULT_ROOT_CAUSE);
2175
+ });
2176
+
2177
+ it("treats the same-state concurrency race as a benign no-op, not an error", async () => {
2178
+ alertFindBySpy.mockResolvedValue([makeAlert(ObjectID.generate())]);
2179
+ timelineCreateSpy.mockRejectedValue(
2180
+ new BadDataException(SAME_STATE_RACE_MESSAGE),
2181
+ );
2182
+
2183
+ const loggerErrorSpy: jest.SpyInstance = jest
2184
+ .spyOn(logger, "error")
2185
+ .mockImplementation(() => {
2186
+ return undefined;
2187
+ });
2188
+
2189
+ await expect(
2190
+ ServiceLevelObjectiveBurnRateRuleService.resolveOpenAlertsForRule({
2191
+ serviceLevelObjectiveId: SLO_ID,
2192
+ burnRateRuleId: RULE_ID,
2193
+ projectId: PROJECT_ID,
2194
+ }),
2195
+ ).resolves.toBeUndefined();
2196
+
2197
+ /*
2198
+ * The inner catch must swallow this one specific message - if it were
2199
+ * rethrown, the outer catch would log it as an error.
2200
+ */
2201
+ expect(loggerErrorSpy).not.toHaveBeenCalled();
2202
+ });
2203
+
2204
+ it("keeps resolving the remaining alerts after one alert fails, then reports the failure", async () => {
2205
+ const firstAlertId: ObjectID = ObjectID.generate();
2206
+ const secondAlertId: ObjectID = ObjectID.generate();
2207
+
2208
+ alertFindBySpy.mockResolvedValue([
2209
+ makeAlert(firstAlertId),
2210
+ makeAlert(secondAlertId),
2211
+ ]);
2212
+
2213
+ timelineCreateSpy
2214
+ .mockRejectedValueOnce(new Error("write conflict"))
2215
+ .mockResolvedValueOnce(new AlertStateTimeline());
2216
+
2217
+ /*
2218
+ * Isolation and reporting are both required, and they pull in opposite
2219
+ * directions: the second alert still has to be resolved, and the caller
2220
+ * still has to learn the first one was not. Every caller treats a clean
2221
+ * return as "nothing is open any more" — the worker stamps the rule
2222
+ * resolved on it, and the Paused / Misconfigured guard commits a status
2223
+ * change that makes the attempt one-shot — so a swallowed failure here
2224
+ * strands that alert and its on-call escalation permanently.
2225
+ */
2226
+ await expect(
2227
+ ServiceLevelObjectiveBurnRateRuleService.resolveOpenAlertsForRule({
2228
+ serviceLevelObjectiveId: SLO_ID,
2229
+ burnRateRuleId: RULE_ID,
2230
+ projectId: PROJECT_ID,
2231
+ }),
2232
+ ).rejects.toThrow("write conflict");
2233
+
2234
+ expect(timelineCreateSpy).toHaveBeenCalledTimes(2);
2235
+ expect(timelineRows()[1]!.alertId).toEqual(secondAlertId);
2236
+ });
2237
+
2238
+ it("reports the FIRST failure when more than one alert fails", async () => {
2239
+ alertFindBySpy.mockResolvedValue([
2240
+ makeAlert(ObjectID.generate()),
2241
+ makeAlert(ObjectID.generate()),
2242
+ ]);
2243
+
2244
+ timelineCreateSpy
2245
+ .mockRejectedValueOnce(new Error("first failure"))
2246
+ .mockRejectedValueOnce(new Error("second failure"));
2247
+
2248
+ await expect(
2249
+ ServiceLevelObjectiveBurnRateRuleService.resolveOpenAlertsForRule({
2250
+ serviceLevelObjectiveId: SLO_ID,
2251
+ burnRateRuleId: RULE_ID,
2252
+ projectId: PROJECT_ID,
2253
+ }),
2254
+ ).rejects.toThrow("first failure");
2255
+
2256
+ expect(timelineCreateSpy).toHaveBeenCalledTimes(2);
2257
+ });
2258
+
2259
+ it("does not swallow a non-benign BadDataException - it is logged and reported", async () => {
2260
+ alertFindBySpy.mockResolvedValue([makeAlert(ObjectID.generate())]);
2261
+ timelineCreateSpy.mockRejectedValue(
2262
+ new BadDataException("Some other validation problem."),
2263
+ );
2264
+
2265
+ const loggerErrorSpy: jest.SpyInstance = jest
2266
+ .spyOn(logger, "error")
2267
+ .mockImplementation(() => {
2268
+ return undefined;
2269
+ });
2270
+
2271
+ await expect(
2272
+ ServiceLevelObjectiveBurnRateRuleService.resolveOpenAlertsForRule({
2273
+ serviceLevelObjectiveId: SLO_ID,
2274
+ burnRateRuleId: RULE_ID,
2275
+ projectId: PROJECT_ID,
2276
+ }),
2277
+ ).rejects.toThrow("Some other validation problem.");
2278
+
2279
+ expect(loggerErrorSpy).toHaveBeenCalledTimes(1);
2280
+ });
2281
+
2282
+ it("resolves cleanly when the only failure was the benign same-state race", async () => {
2283
+ alertFindBySpy.mockResolvedValue([
2284
+ makeAlert(ObjectID.generate()),
2285
+ makeAlert(ObjectID.generate()),
2286
+ ]);
2287
+
2288
+ /*
2289
+ * The race is not a failure: two writers agreeing the alert is resolved is
2290
+ * the outcome the caller wanted. Counting it would make a healthy resolve
2291
+ * look like one that needs retrying.
2292
+ */
2293
+ timelineCreateSpy
2294
+ .mockRejectedValueOnce(new BadDataException(SAME_STATE_RACE_MESSAGE))
2295
+ .mockResolvedValueOnce(new AlertStateTimeline());
2296
+
2297
+ await expect(
2298
+ ServiceLevelObjectiveBurnRateRuleService.resolveOpenAlertsForRule({
2299
+ serviceLevelObjectiveId: SLO_ID,
2300
+ burnRateRuleId: RULE_ID,
2301
+ projectId: PROJECT_ID,
2302
+ }),
2303
+ ).resolves.toBeUndefined();
2304
+ });
2305
+ });
2306
+
2307
+ /*
2308
+ * The incident twin of the suite above: same shape, different table. It is a
2309
+ * separate method rather than a branch of the alert path because the two
2310
+ * lifecycles are resolved independently - switching `shouldCreateIncident` off
2311
+ * must close the incident and leave the alert exactly where it is.
2312
+ */
2313
+ describe("ServiceLevelObjectiveBurnRateRuleService.resolveOpenIncidentsForRule", () => {
2314
+ let incidentFindBySpy: jest.SpyInstance;
2315
+ let resolvedStateSpy: jest.SpyInstance;
2316
+ let timelineCreateSpy: jest.SpyInstance;
2317
+
2318
+ beforeEach(() => {
2319
+ incidentFindBySpy = jest
2320
+ .spyOn(IncidentService, "findBy")
2321
+ .mockResolvedValue([]);
2322
+
2323
+ resolvedStateSpy = jest
2324
+ .spyOn(IncidentStateTimelineService, "getResolvedStateIdForProject")
2325
+ .mockResolvedValue(RESOLVED_STATE_ID);
2326
+
2327
+ timelineCreateSpy = jest
2328
+ .spyOn(IncidentStateTimelineService, "create")
2329
+ .mockResolvedValue(new IncidentStateTimeline());
2330
+ });
2331
+
2332
+ afterEach(() => {
2333
+ jest.restoreAllMocks();
2334
+ });
2335
+
2336
+ function timelineRows(): Array<IncidentStateTimeline> {
2337
+ return timelineCreateSpy.mock.calls.map((call: Array<unknown>) => {
2338
+ return (call[0] as CreateBy<IncidentStateTimeline>).data;
2339
+ });
2340
+ }
2341
+
2342
+ it("looks up only the unresolved incidents carrying this rule's fingerprint", async () => {
2343
+ await ServiceLevelObjectiveBurnRateRuleService.resolveOpenIncidentsForRule({
2344
+ serviceLevelObjectiveId: SLO_ID,
2345
+ burnRateRuleId: RULE_ID,
2346
+ projectId: PROJECT_ID,
2347
+ });
2348
+
2349
+ const findByArguments: FindByArguments = findByArgumentsAt(
2350
+ incidentFindBySpy,
2351
+ 0,
2352
+ );
2353
+
2354
+ expect(findByArguments.query).toEqual({
2355
+ projectId: PROJECT_ID,
2356
+ seriesFingerprint: `slo:${SLO_ID.toString()}:burn-rule:${RULE_ID.toString()}`,
2357
+ currentIncidentState: { isResolvedState: false },
2358
+ });
2359
+ // projectId is needed on each row to stamp the timeline entry.
2360
+ expect(findByArguments.select).toEqual({ _id: true, projectId: true });
2361
+ expect(findByArguments.props).toEqual({ isRoot: true });
2362
+ });
851
2363
 
852
- expect(findBySpy).not.toHaveBeenCalled();
2364
+ /*
2365
+ * getResolvedStateIdForProject THROWS when a project has no resolved
2366
+ * incident state, so asking for it before there is anything to resolve would
2367
+ * fail a project for a state it never needed.
2368
+ */
2369
+ it("is a no-op when the rule has no open incidents, and never asks for the resolved state", async () => {
2370
+ await ServiceLevelObjectiveBurnRateRuleService.resolveOpenIncidentsForRule({
2371
+ serviceLevelObjectiveId: SLO_ID,
2372
+ burnRateRuleId: RULE_ID,
2373
+ projectId: PROJECT_ID,
2374
+ });
2375
+
2376
+ expect(resolvedStateSpy).not.toHaveBeenCalled();
2377
+ expect(timelineCreateSpy).not.toHaveBeenCalled();
853
2378
  });
854
2379
 
855
- it("rejects a persisted sibling that is not numeric at all, rather than accepting a NaN comparison", async () => {
856
- findBySpy.mockResolvedValue([
857
- makeRule({
858
- _id: RULE_ID.toString(),
859
- id: RULE_ID,
860
- shortWindowInMinutes: undefined,
861
- }),
2380
+ it("appends one resolved state timeline row per open incident", async () => {
2381
+ const firstIncidentId: ObjectID = ObjectID.generate();
2382
+ const secondIncidentId: ObjectID = ObjectID.generate();
2383
+
2384
+ incidentFindBySpy.mockResolvedValue([
2385
+ makeIncident(firstIncidentId),
2386
+ makeIncident(secondIncidentId),
862
2387
  ]);
863
2388
 
864
- await expectBadData(
865
- callHook("onBeforeUpdate", makeUpdateBy({ longWindowInMinutes: 1440 })),
866
- WINDOWS_NOT_NUMERIC_ERROR_MESSAGE,
867
- );
2389
+ await ServiceLevelObjectiveBurnRateRuleService.resolveOpenIncidentsForRule({
2390
+ serviceLevelObjectiveId: SLO_ID,
2391
+ burnRateRuleId: RULE_ID,
2392
+ projectId: PROJECT_ID,
2393
+ });
2394
+
2395
+ // The resolved state is looked up once for the whole batch.
2396
+ expect(resolvedStateSpy).toHaveBeenCalledTimes(1);
2397
+ expect(resolvedStateSpy).toHaveBeenCalledWith(PROJECT_ID);
2398
+
2399
+ const rows: Array<IncidentStateTimeline> = timelineRows();
2400
+ expect(rows).toHaveLength(2);
2401
+ expect(rows[0]!.incidentId).toEqual(firstIncidentId);
2402
+ expect(rows[1]!.incidentId).toEqual(secondIncidentId);
2403
+
2404
+ for (const row of rows) {
2405
+ expect(row.incidentStateId).toEqual(RESOLVED_STATE_ID);
2406
+ expect(row.projectId).toEqual(PROJECT_ID);
2407
+ }
2408
+
2409
+ for (const call of timelineCreateSpy.mock.calls) {
2410
+ const createBy: CreateBy<IncidentStateTimeline> = (
2411
+ call as Array<unknown>
2412
+ )[0] as CreateBy<IncidentStateTimeline>;
2413
+ expect(createBy.props).toEqual({ isRoot: true });
2414
+ }
868
2415
  });
869
2416
 
870
- it("coerces a string single-window update even when the query matches no rows", async () => {
871
- findBySpy.mockResolvedValue([]);
2417
+ it("falls back to the generic root cause when the caller supplies none", async () => {
2418
+ incidentFindBySpy.mockResolvedValue([makeIncident(ObjectID.generate())]);
872
2419
 
873
- const updateBy: UpdateBy<ServiceLevelObjectiveBurnRateRule> = makeUpdateBy({
874
- shortWindowInMinutes: "30",
2420
+ await ServiceLevelObjectiveBurnRateRuleService.resolveOpenIncidentsForRule({
2421
+ serviceLevelObjectiveId: SLO_ID,
2422
+ burnRateRuleId: RULE_ID,
2423
+ projectId: PROJECT_ID,
875
2424
  });
876
2425
 
877
- await expect(callHook("onBeforeUpdate", updateBy)).resolves.toBeDefined();
878
-
879
- expect(updateBy.data.shortWindowInMinutes).toBe(30);
880
- expect(typeof updateBy.data.shortWindowInMinutes).toBe("number");
2426
+ expect(timelineRows()[0]!.rootCause).toBe(DEFAULT_INCIDENT_ROOT_CAUSE);
881
2427
  });
882
2428
 
883
- it("writes a coerced threshold back onto the update payload", async () => {
884
- const updateBy: UpdateBy<ServiceLevelObjectiveBurnRateRule> = makeUpdateBy({
885
- burnRateThreshold: "2.5",
2429
+ it("ignores an explicitly undefined root cause and uses the fallback", async () => {
2430
+ incidentFindBySpy.mockResolvedValue([makeIncident(ObjectID.generate())]);
2431
+
2432
+ await ServiceLevelObjectiveBurnRateRuleService.resolveOpenIncidentsForRule({
2433
+ serviceLevelObjectiveId: SLO_ID,
2434
+ burnRateRuleId: RULE_ID,
2435
+ projectId: PROJECT_ID,
2436
+ rootCause: undefined,
886
2437
  });
887
2438
 
888
- await expect(callHook("onBeforeUpdate", updateBy)).resolves.toBeDefined();
2439
+ expect(timelineRows()[0]!.rootCause).toBe(DEFAULT_INCIDENT_ROOT_CAUSE);
2440
+ });
889
2441
 
890
- expect(updateBy.data.burnRateThreshold).toBe(2.5);
891
- expect(typeof updateBy.data.burnRateThreshold).toBe("number");
2442
+ it("threads the caller's root cause onto every resolved incident", async () => {
2443
+ incidentFindBySpy.mockResolvedValue([
2444
+ makeIncident(ObjectID.generate()),
2445
+ makeIncident(ObjectID.generate()),
2446
+ ]);
2447
+
2448
+ await ServiceLevelObjectiveBurnRateRuleService.resolveOpenIncidentsForRule({
2449
+ serviceLevelObjectiveId: SLO_ID,
2450
+ burnRateRuleId: RULE_ID,
2451
+ projectId: PROJECT_ID,
2452
+ rootCause: DELETE_ROOT_CAUSE,
2453
+ });
2454
+
2455
+ for (const row of timelineRows()) {
2456
+ expect(row.rootCause).toBe(DELETE_ROOT_CAUSE);
2457
+ }
892
2458
  });
893
- });
894
2459
 
895
- describe("ServiceLevelObjectiveBurnRateRuleService.getBurnRateAlertFingerprint", () => {
896
- it("is exactly slo:<sloId>:burn-rule:<ruleId>", () => {
897
- expect(
898
- ServiceLevelObjectiveBurnRateRuleService.getBurnRateAlertFingerprint({
2460
+ it("treats the same-state concurrency race as a benign no-op, not an error", async () => {
2461
+ incidentFindBySpy.mockResolvedValue([makeIncident(ObjectID.generate())]);
2462
+ timelineCreateSpy.mockRejectedValue(
2463
+ new BadDataException(SAME_STATE_INCIDENT_RACE_MESSAGE),
2464
+ );
2465
+
2466
+ const loggerDebugSpy: jest.SpyInstance = jest
2467
+ .spyOn(logger, "debug")
2468
+ .mockImplementation(() => {
2469
+ return undefined;
2470
+ });
2471
+ const loggerErrorSpy: jest.SpyInstance = jest
2472
+ .spyOn(logger, "error")
2473
+ .mockImplementation(() => {
2474
+ return undefined;
2475
+ });
2476
+
2477
+ await expect(
2478
+ ServiceLevelObjectiveBurnRateRuleService.resolveOpenIncidentsForRule({
899
2479
  serviceLevelObjectiveId: SLO_ID,
900
2480
  burnRateRuleId: RULE_ID,
2481
+ projectId: PROJECT_ID,
901
2482
  }),
902
- ).toBe(`slo:${SLO_ID.toString()}:burn-rule:${RULE_ID.toString()}`);
2483
+ ).resolves.toBeUndefined();
2484
+
2485
+ /*
2486
+ * The evaluation worker and a lifecycle hook can both decide to resolve
2487
+ * the same incident; the loser's dedupe throws this exact message and it
2488
+ * is a no-op, not a failure. If the inner catch rethrew it, the outer
2489
+ * catch would log it as an error.
2490
+ */
2491
+ expect(loggerDebugSpy).toHaveBeenCalledTimes(1);
2492
+ expect(loggerErrorSpy).not.toHaveBeenCalled();
903
2493
  });
904
2494
 
905
- it("is deterministic across calls", () => {
906
- const first: string =
907
- ServiceLevelObjectiveBurnRateRuleService.getBurnRateAlertFingerprint({
908
- serviceLevelObjectiveId: SLO_ID,
909
- burnRateRuleId: RULE_ID,
2495
+ it("logs any other BadDataException rather than swallowing it", async () => {
2496
+ incidentFindBySpy.mockResolvedValue([makeIncident(ObjectID.generate())]);
2497
+ timelineCreateSpy.mockRejectedValue(
2498
+ new BadDataException("Incident state is not in the right order."),
2499
+ );
2500
+
2501
+ const loggerErrorSpy: jest.SpyInstance = jest
2502
+ .spyOn(logger, "error")
2503
+ .mockImplementation(() => {
2504
+ return undefined;
910
2505
  });
911
- const second: string =
912
- ServiceLevelObjectiveBurnRateRuleService.getBurnRateAlertFingerprint({
2506
+
2507
+ await expect(
2508
+ ServiceLevelObjectiveBurnRateRuleService.resolveOpenIncidentsForRule({
913
2509
  serviceLevelObjectiveId: SLO_ID,
914
2510
  burnRateRuleId: RULE_ID,
915
- });
2511
+ projectId: PROJECT_ID,
2512
+ }),
2513
+ ).rejects.toThrow("Incident state is not in the right order.");
916
2514
 
917
- expect(second).toBe(first);
2515
+ // Matching on the message, not the type, is what keeps this one visible.
2516
+ expect(loggerErrorSpy).toHaveBeenCalledTimes(1);
918
2517
  });
919
2518
 
920
- it("distinguishes two rules of the same SLO", () => {
921
- const first: string =
922
- ServiceLevelObjectiveBurnRateRuleService.getBurnRateAlertFingerprint({
923
- serviceLevelObjectiveId: SLO_ID,
924
- burnRateRuleId: RULE_ID,
2519
+ it("keeps resolving the remaining incidents after one incident fails, then reports the failure", async () => {
2520
+ const firstIncidentId: ObjectID = ObjectID.generate();
2521
+ const secondIncidentId: ObjectID = ObjectID.generate();
2522
+
2523
+ incidentFindBySpy.mockResolvedValue([
2524
+ makeIncident(firstIncidentId),
2525
+ makeIncident(secondIncidentId),
2526
+ ]);
2527
+
2528
+ timelineCreateSpy
2529
+ .mockRejectedValueOnce(new Error("write conflict"))
2530
+ .mockResolvedValueOnce(new IncidentStateTimeline());
2531
+
2532
+ const loggerErrorSpy: jest.SpyInstance = jest
2533
+ .spyOn(logger, "error")
2534
+ .mockImplementation(() => {
2535
+ return undefined;
925
2536
  });
926
- const second: string =
927
- ServiceLevelObjectiveBurnRateRuleService.getBurnRateAlertFingerprint({
2537
+
2538
+ /*
2539
+ * The same isolation-and-reporting pair the alert twin carries: the second
2540
+ * incident still resolves, and the caller still hears that the first one
2541
+ * did not. A clean return here is what makes the worker stamp the rule
2542
+ * resolved and stop trying.
2543
+ */
2544
+ await expect(
2545
+ ServiceLevelObjectiveBurnRateRuleService.resolveOpenIncidentsForRule({
928
2546
  serviceLevelObjectiveId: SLO_ID,
929
- burnRateRuleId: OTHER_RULE_ID,
930
- });
2547
+ burnRateRuleId: RULE_ID,
2548
+ projectId: PROJECT_ID,
2549
+ }),
2550
+ ).rejects.toThrow("write conflict");
931
2551
 
932
- expect(second).not.toBe(first);
2552
+ expect(timelineCreateSpy).toHaveBeenCalledTimes(2);
2553
+ expect(timelineRows()[1]!.incidentId).toEqual(secondIncidentId);
2554
+ expect(loggerErrorSpy).toHaveBeenCalledTimes(1);
933
2555
  });
934
2556
 
935
- it("distinguishes the same rule id under two different SLOs", () => {
936
- const first: string =
937
- ServiceLevelObjectiveBurnRateRuleService.getBurnRateAlertFingerprint({
2557
+ it("resolves cleanly when the only failure was the benign same-state race", async () => {
2558
+ incidentFindBySpy.mockResolvedValue([
2559
+ makeIncident(ObjectID.generate()),
2560
+ makeIncident(ObjectID.generate()),
2561
+ ]);
2562
+
2563
+ timelineCreateSpy
2564
+ .mockRejectedValueOnce(
2565
+ new BadDataException(
2566
+ "Incident state cannot be same as previous state.",
2567
+ ),
2568
+ )
2569
+ .mockResolvedValueOnce(new IncidentStateTimeline());
2570
+
2571
+ await expect(
2572
+ ServiceLevelObjectiveBurnRateRuleService.resolveOpenIncidentsForRule({
938
2573
  serviceLevelObjectiveId: SLO_ID,
939
2574
  burnRateRuleId: RULE_ID,
940
- });
941
- const second: string =
942
- ServiceLevelObjectiveBurnRateRuleService.getBurnRateAlertFingerprint({
943
- serviceLevelObjectiveId: OTHER_RULE_ID,
2575
+ projectId: PROJECT_ID,
2576
+ }),
2577
+ ).resolves.toBeUndefined();
2578
+ });
2579
+
2580
+ /*
2581
+ * A project with something open but NO resolved incident state cannot be
2582
+ * papered over: the incidents would be left open with nothing recording why,
2583
+ * so the caller has to hear about it.
2584
+ */
2585
+ it("propagates a failure to look up the project's resolved incident state", async () => {
2586
+ incidentFindBySpy.mockResolvedValue([makeIncident(ObjectID.generate())]);
2587
+ resolvedStateSpy.mockRejectedValue(
2588
+ new BadDataException("No resolved state found for the project"),
2589
+ );
2590
+
2591
+ await expect(
2592
+ ServiceLevelObjectiveBurnRateRuleService.resolveOpenIncidentsForRule({
2593
+ serviceLevelObjectiveId: SLO_ID,
944
2594
  burnRateRuleId: RULE_ID,
945
- });
2595
+ projectId: PROJECT_ID,
2596
+ }),
2597
+ ).rejects.toThrow("No resolved state found for the project");
946
2598
 
947
- expect(second).not.toBe(first);
2599
+ expect(timelineCreateSpy).not.toHaveBeenCalled();
948
2600
  });
949
2601
  });
950
2602
 
951
- describe("ServiceLevelObjectiveBurnRateRuleService.resolveOpenAlertsForRule", () => {
952
- let alertFindBySpy: jest.SpyInstance;
953
- let resolvedStateSpy: jest.SpyInstance;
954
- let timelineCreateSpy: jest.SpyInstance;
2603
+ /*
2604
+ * What every "this rule can no longer justify what it opened" caller wants:
2605
+ * close BOTH sides, because a rule's configuration can have changed since a
2606
+ * record was opened, so "it declares incidents now" is not a safe proxy for
2607
+ * "it has no open alert".
2608
+ */
2609
+ describe("ServiceLevelObjectiveBurnRateRuleService.resolveOpenAlertsAndIncidentsForRule", () => {
2610
+ const resolveData: {
2611
+ serviceLevelObjectiveId: ObjectID;
2612
+ burnRateRuleId: ObjectID;
2613
+ projectId: ObjectID;
2614
+ rootCause: string;
2615
+ } = {
2616
+ serviceLevelObjectiveId: SLO_ID,
2617
+ burnRateRuleId: RULE_ID,
2618
+ projectId: PROJECT_ID,
2619
+ rootCause: DELETE_ROOT_CAUSE,
2620
+ };
2621
+
2622
+ let resolveAlertsSpy: jest.SpyInstance;
2623
+ let resolveIncidentsSpy: jest.SpyInstance;
955
2624
 
956
2625
  beforeEach(() => {
957
- alertFindBySpy = jest.spyOn(AlertService, "findBy").mockResolvedValue([]);
2626
+ resolveAlertsSpy = jest
2627
+ .spyOn(
2628
+ ServiceLevelObjectiveBurnRateRuleService,
2629
+ "resolveOpenAlertsForRule",
2630
+ )
2631
+ .mockResolvedValue(undefined);
2632
+ resolveIncidentsSpy = jest
2633
+ .spyOn(
2634
+ ServiceLevelObjectiveBurnRateRuleService,
2635
+ "resolveOpenIncidentsForRule",
2636
+ )
2637
+ .mockResolvedValue(undefined);
2638
+ });
958
2639
 
959
- resolvedStateSpy = jest
960
- .spyOn(AlertStateTimelineService, "getResolvedStateIdForProject")
961
- .mockResolvedValue(RESOLVED_STATE_ID);
2640
+ afterEach(() => {
2641
+ jest.restoreAllMocks();
2642
+ });
962
2643
 
963
- timelineCreateSpy = jest
964
- .spyOn(AlertStateTimelineService, "create")
965
- .mockResolvedValue(new AlertStateTimeline());
2644
+ it("closes both sides, handing each the caller's data unchanged", async () => {
2645
+ await expect(
2646
+ ServiceLevelObjectiveBurnRateRuleService.resolveOpenAlertsAndIncidentsForRule(
2647
+ resolveData,
2648
+ ),
2649
+ ).resolves.toBeUndefined();
2650
+
2651
+ expect(resolveAlertsSpy).toHaveBeenCalledTimes(1);
2652
+ expect(resolveAlertsSpy).toHaveBeenCalledWith(resolveData);
2653
+ expect(resolveIncidentsSpy).toHaveBeenCalledTimes(1);
2654
+ expect(resolveIncidentsSpy).toHaveBeenCalledWith(resolveData);
2655
+ });
2656
+
2657
+ /*
2658
+ * The reason for the try/catch around each half: a failure on one side must
2659
+ * not strand the other, which is exactly what a plain `await a(); await b();`
2660
+ * would do.
2661
+ */
2662
+ it("still closes the incidents when the alert pass throws, then rethrows the alert error", async () => {
2663
+ resolveAlertsSpy.mockRejectedValue(new Error("alert service down"));
2664
+
2665
+ await expect(
2666
+ ServiceLevelObjectiveBurnRateRuleService.resolveOpenAlertsAndIncidentsForRule(
2667
+ resolveData,
2668
+ ),
2669
+ ).rejects.toThrow("alert service down");
2670
+
2671
+ expect(resolveIncidentsSpy).toHaveBeenCalledTimes(1);
2672
+ });
2673
+
2674
+ it("still closes the alerts when the incident pass throws, then rethrows the incident error", async () => {
2675
+ resolveIncidentsSpy.mockRejectedValue(new Error("incident service down"));
2676
+
2677
+ await expect(
2678
+ ServiceLevelObjectiveBurnRateRuleService.resolveOpenAlertsAndIncidentsForRule(
2679
+ resolveData,
2680
+ ),
2681
+ ).rejects.toThrow("incident service down");
2682
+
2683
+ expect(resolveAlertsSpy).toHaveBeenCalledTimes(1);
2684
+ });
2685
+
2686
+ it("rethrows the FIRST error when both passes throw, and runs both anyway", async () => {
2687
+ resolveAlertsSpy.mockRejectedValue(new Error("alert service down"));
2688
+ resolveIncidentsSpy.mockRejectedValue(new Error("incident service down"));
2689
+
2690
+ await expect(
2691
+ ServiceLevelObjectiveBurnRateRuleService.resolveOpenAlertsAndIncidentsForRule(
2692
+ resolveData,
2693
+ ),
2694
+ ).rejects.toThrow("alert service down");
2695
+
2696
+ expect(resolveAlertsSpy).toHaveBeenCalledTimes(1);
2697
+ expect(resolveIncidentsSpy).toHaveBeenCalledTimes(1);
2698
+ });
2699
+ });
2700
+
2701
+ /*
2702
+ * A rule stops declaring something - it is disabled, or one of its two outputs
2703
+ * is switched off - and whatever it already has open is an orphan. The
2704
+ * evaluation worker cannot clean it up: it only loads ENABLED rules, and for
2705
+ * an enabled rule it only resolves the output it is still allowed to create.
2706
+ * So without this hook the alert (or incident) and its on-call escalation
2707
+ * would keep paging someone about a rule that can no longer justify the page.
2708
+ *
2709
+ * It runs after the write commits, so a rejected update leaves everything
2710
+ * exactly where it was.
2711
+ */
2712
+ describe("ServiceLevelObjectiveBurnRateRuleService.onUpdateSuccess", () => {
2713
+ let findOneByIdSpy: jest.SpyInstance;
2714
+ let resolveAlertsSpy: jest.SpyInstance;
2715
+ let resolveIncidentsSpy: jest.SpyInstance;
2716
+ let clearStateSpy: jest.SpyInstance;
2717
+ let loggerErrorSpy: jest.SpyInstance;
2718
+
2719
+ beforeEach(() => {
2720
+ findOneByIdSpy = jest
2721
+ .spyOn(ServiceLevelObjectiveBurnRateRuleService, "findOneById")
2722
+ .mockResolvedValue(
2723
+ makeRule({
2724
+ _id: RULE_ID.toString(),
2725
+ id: RULE_ID,
2726
+ projectId: PROJECT_ID,
2727
+ serviceLevelObjectiveId: SLO_ID,
2728
+ }),
2729
+ );
2730
+ resolveAlertsSpy = jest
2731
+ .spyOn(
2732
+ ServiceLevelObjectiveBurnRateRuleService,
2733
+ "resolveOpenAlertsForRule",
2734
+ )
2735
+ .mockResolvedValue(undefined);
2736
+ resolveIncidentsSpy = jest
2737
+ .spyOn(
2738
+ ServiceLevelObjectiveBurnRateRuleService,
2739
+ "resolveOpenIncidentsForRule",
2740
+ )
2741
+ .mockResolvedValue(undefined);
2742
+ clearStateSpy = jest
2743
+ .spyOn(
2744
+ ServiceLevelObjectiveBurnRateRuleService,
2745
+ "clearOpenOutputStateForRule",
2746
+ )
2747
+ .mockResolvedValue(undefined);
2748
+ loggerErrorSpy = jest.spyOn(logger, "error").mockImplementation(() => {
2749
+ return undefined;
2750
+ });
966
2751
  });
967
2752
 
968
2753
  afterEach(() => {
969
2754
  jest.restoreAllMocks();
970
2755
  });
971
2756
 
972
- function timelineRows(): Array<AlertStateTimeline> {
973
- return timelineCreateSpy.mock.calls.map((call: Array<unknown>) => {
974
- return (call[0] as CreateBy<AlertStateTimeline>).data;
975
- });
976
- }
2757
+ it("resolves both the alerts and the incidents of every updated rule when the rule is disabled", async () => {
2758
+ await callHook("onUpdateSuccess", makeOnUpdate({ isEnabled: false }), [
2759
+ RULE_ID,
2760
+ OTHER_RULE_ID,
2761
+ ]);
977
2762
 
978
- it("looks up only the unresolved alerts carrying this rule's fingerprint", async () => {
979
- await ServiceLevelObjectiveBurnRateRuleService.resolveOpenAlertsForRule({
2763
+ expect(resolveAlertsSpy).toHaveBeenCalledTimes(2);
2764
+ expect(resolveIncidentsSpy).toHaveBeenCalledTimes(2);
2765
+
2766
+ // The rule id comes from the updated ids, not from the row that was read.
2767
+ expect(resolveAlertsSpy).toHaveBeenNthCalledWith(1, {
980
2768
  serviceLevelObjectiveId: SLO_ID,
981
2769
  burnRateRuleId: RULE_ID,
982
2770
  projectId: PROJECT_ID,
2771
+ rootCause: DISABLED_ALERT_ROOT_CAUSE,
983
2772
  });
984
-
985
- const findByArg: {
986
- query: Record<string, unknown>;
987
- select: Record<string, unknown>;
988
- props: Record<string, unknown>;
989
- } = alertFindBySpy.mock.calls[0]![0] as {
990
- query: Record<string, unknown>;
991
- select: Record<string, unknown>;
992
- props: Record<string, unknown>;
993
- };
994
-
995
- expect(findByArg.query).toEqual({
2773
+ expect(resolveAlertsSpy).toHaveBeenNthCalledWith(2, {
2774
+ serviceLevelObjectiveId: SLO_ID,
2775
+ burnRateRuleId: OTHER_RULE_ID,
996
2776
  projectId: PROJECT_ID,
997
- seriesFingerprint: `slo:${SLO_ID.toString()}:burn-rule:${RULE_ID.toString()}`,
998
- currentAlertState: { isResolvedState: false },
2777
+ rootCause: DISABLED_ALERT_ROOT_CAUSE,
999
2778
  });
1000
- // projectId is needed on each row to stamp the timeline entry.
1001
- expect(findByArg.select).toEqual({ _id: true, projectId: true });
1002
- expect(findByArg.props).toEqual({ isRoot: true });
1003
- });
1004
-
1005
- it("is a no-op when the rule has no open alerts", async () => {
1006
- await ServiceLevelObjectiveBurnRateRuleService.resolveOpenAlertsForRule({
2779
+ expect(resolveIncidentsSpy).toHaveBeenNthCalledWith(1, {
1007
2780
  serviceLevelObjectiveId: SLO_ID,
1008
2781
  burnRateRuleId: RULE_ID,
1009
2782
  projectId: PROJECT_ID,
2783
+ rootCause: DISABLED_INCIDENT_ROOT_CAUSE,
2784
+ });
2785
+ expect(resolveIncidentsSpy).toHaveBeenNthCalledWith(2, {
2786
+ serviceLevelObjectiveId: SLO_ID,
2787
+ burnRateRuleId: OTHER_RULE_ID,
2788
+ projectId: PROJECT_ID,
2789
+ rootCause: DISABLED_INCIDENT_ROOT_CAUSE,
1010
2790
  });
1011
2791
 
1012
- // It must not even resolve the project's resolved-state id.
1013
- expect(resolvedStateSpy).not.toHaveBeenCalled();
1014
- expect(timelineCreateSpy).not.toHaveBeenCalled();
2792
+ /*
2793
+ * And each rule is told to forget what it had open, per rule id. Without
2794
+ * this the worker's firing gate keeps reading the stale created stamp and
2795
+ * declares nothing once the rule is re-enabled mid-burn. The edge cases
2796
+ * live in their own describe below.
2797
+ */
2798
+ expect(clearStateSpy).toHaveBeenCalledTimes(2);
2799
+ expect(clearStateSpy).toHaveBeenNthCalledWith(1, {
2800
+ burnRateRuleId: RULE_ID,
2801
+ clearAlert: true,
2802
+ clearIncident: true,
2803
+ });
2804
+ expect(clearStateSpy).toHaveBeenNthCalledWith(2, {
2805
+ burnRateRuleId: OTHER_RULE_ID,
2806
+ clearAlert: true,
2807
+ clearIncident: true,
2808
+ });
1015
2809
  });
1016
2810
 
1017
- it("appends one resolved state timeline row per open alert", async () => {
1018
- const firstAlertId: ObjectID = ObjectID.generate();
1019
- const secondAlertId: ObjectID = ObjectID.generate();
1020
-
1021
- alertFindBySpy.mockResolvedValue([
1022
- makeAlert(firstAlertId),
1023
- makeAlert(secondAlertId),
1024
- ]);
2811
+ it("resolves only the alerts when the rule stops raising alerts", async () => {
2812
+ await callHook(
2813
+ "onUpdateSuccess",
2814
+ makeOnUpdate({ shouldCreateAlert: false }),
2815
+ [RULE_ID],
2816
+ );
1025
2817
 
1026
- await ServiceLevelObjectiveBurnRateRuleService.resolveOpenAlertsForRule({
2818
+ expect(resolveAlertsSpy).toHaveBeenCalledTimes(1);
2819
+ expect(resolveAlertsSpy).toHaveBeenCalledWith({
1027
2820
  serviceLevelObjectiveId: SLO_ID,
1028
2821
  burnRateRuleId: RULE_ID,
1029
2822
  projectId: PROJECT_ID,
2823
+ rootCause: ALERTS_TURNED_OFF_ROOT_CAUSE,
1030
2824
  });
1031
-
1032
- // The resolved state is looked up once for the whole batch.
1033
- expect(resolvedStateSpy).toHaveBeenCalledTimes(1);
1034
- expect(resolvedStateSpy).toHaveBeenCalledWith(PROJECT_ID);
1035
-
1036
- const rows: Array<AlertStateTimeline> = timelineRows();
1037
- expect(rows).toHaveLength(2);
1038
- expect(rows[0]!.alertId).toEqual(firstAlertId);
1039
- expect(rows[1]!.alertId).toEqual(secondAlertId);
1040
-
1041
- for (const row of rows) {
1042
- expect(row.alertStateId).toEqual(RESOLVED_STATE_ID);
1043
- expect(row.projectId).toEqual(PROJECT_ID);
1044
- }
1045
-
1046
- for (const call of timelineCreateSpy.mock.calls) {
1047
- const createBy: CreateBy<AlertStateTimeline> = (
1048
- call as Array<unknown>
1049
- )[0] as CreateBy<AlertStateTimeline>;
1050
- expect(createBy.props).toEqual({ isRoot: true });
1051
- }
2825
+ // The incident the rule still declares must be left alone.
2826
+ expect(resolveIncidentsSpy).not.toHaveBeenCalled();
1052
2827
  });
1053
2828
 
1054
- it("falls back to the generic root cause when the caller supplies none", async () => {
1055
- alertFindBySpy.mockResolvedValue([makeAlert(ObjectID.generate())]);
2829
+ it("resolves only the incidents when the rule stops declaring incidents", async () => {
2830
+ await callHook(
2831
+ "onUpdateSuccess",
2832
+ makeOnUpdate({ shouldCreateIncident: false }),
2833
+ [RULE_ID],
2834
+ );
1056
2835
 
1057
- await ServiceLevelObjectiveBurnRateRuleService.resolveOpenAlertsForRule({
2836
+ expect(resolveIncidentsSpy).toHaveBeenCalledTimes(1);
2837
+ expect(resolveIncidentsSpy).toHaveBeenCalledWith({
1058
2838
  serviceLevelObjectiveId: SLO_ID,
1059
2839
  burnRateRuleId: RULE_ID,
1060
2840
  projectId: PROJECT_ID,
2841
+ rootCause: INCIDENTS_TURNED_OFF_ROOT_CAUSE,
1061
2842
  });
2843
+ expect(resolveAlertsSpy).not.toHaveBeenCalled();
2844
+ });
1062
2845
 
1063
- expect(timelineRows()[0]!.rootCause).toBe(DEFAULT_ROOT_CAUSE);
2846
+ it("prefers the 'was disabled' root causes when one payload both disables the rule and switches an output off", async () => {
2847
+ await callHook(
2848
+ "onUpdateSuccess",
2849
+ makeOnUpdate({ isEnabled: false, shouldCreateIncident: false }),
2850
+ [RULE_ID],
2851
+ );
2852
+
2853
+ expect(resolveAlertsSpy).toHaveBeenCalledWith(
2854
+ expect.objectContaining({ rootCause: DISABLED_ALERT_ROOT_CAUSE }),
2855
+ );
2856
+ expect(resolveIncidentsSpy).toHaveBeenCalledWith(
2857
+ expect.objectContaining({ rootCause: DISABLED_INCIDENT_ROOT_CAUSE }),
2858
+ );
1064
2859
  });
1065
2860
 
1066
- it("threads the caller's root cause onto every resolved alert", async () => {
1067
- alertFindBySpy.mockResolvedValue([
1068
- makeAlert(ObjectID.generate()),
1069
- makeAlert(ObjectID.generate()),
2861
+ it("loads nothing and resolves nothing when the payload carries none of the three flags", async () => {
2862
+ await callHook("onUpdateSuccess", makeOnUpdate({ name: "Renamed rule" }), [
2863
+ RULE_ID,
1070
2864
  ]);
1071
2865
 
1072
- await ServiceLevelObjectiveBurnRateRuleService.resolveOpenAlertsForRule({
1073
- serviceLevelObjectiveId: SLO_ID,
1074
- burnRateRuleId: RULE_ID,
1075
- projectId: PROJECT_ID,
1076
- rootCause: "Alert auto-resolved because the SLO was disabled.",
1077
- });
1078
-
1079
- for (const row of timelineRows()) {
1080
- expect(row.rootCause).toBe(
1081
- "Alert auto-resolved because the SLO was disabled.",
1082
- );
1083
- }
2866
+ expect(findOneByIdSpy).not.toHaveBeenCalled();
2867
+ expect(resolveAlertsSpy).not.toHaveBeenCalled();
2868
+ expect(resolveIncidentsSpy).not.toHaveBeenCalled();
1084
2869
  });
1085
2870
 
1086
- it("ignores an explicitly undefined root cause and uses the fallback", async () => {
1087
- alertFindBySpy.mockResolvedValue([makeAlert(ObjectID.generate())]);
2871
+ it("resolves nothing when a flag is switched ON", async () => {
2872
+ await callHook(
2873
+ "onUpdateSuccess",
2874
+ makeOnUpdate({ isEnabled: true, shouldCreateIncident: true }),
2875
+ [RULE_ID],
2876
+ );
1088
2877
 
1089
- await ServiceLevelObjectiveBurnRateRuleService.resolveOpenAlertsForRule({
1090
- serviceLevelObjectiveId: SLO_ID,
1091
- burnRateRuleId: RULE_ID,
1092
- projectId: PROJECT_ID,
1093
- rootCause: undefined,
1094
- });
2878
+ expect(findOneByIdSpy).not.toHaveBeenCalled();
2879
+ expect(resolveAlertsSpy).not.toHaveBeenCalled();
2880
+ });
1095
2881
 
1096
- expect(timelineRows()[0]!.rootCause).toBe(DEFAULT_ROOT_CAUSE);
2882
+ /*
2883
+ * The strict `=== false` reads here are only sound because onBeforeUpdate
2884
+ * coerced both flags first. This pins that dependency rather than the
2885
+ * behaviour being desirable on its own: a payload that somehow reached this
2886
+ * hook still holding the raw string resolves NOTHING, so the coercion in
2887
+ * validateOutputsOnUpdate is load-bearing, not tidiness.
2888
+ */
2889
+ it('does nothing with an uncoerced "false" string, which onBeforeUpdate is responsible for turning into a boolean', async () => {
2890
+ await callHook(
2891
+ "onUpdateSuccess",
2892
+ makeOnUpdate({ shouldCreateAlert: "false" }),
2893
+ [RULE_ID],
2894
+ );
2895
+
2896
+ expect(findOneByIdSpy).not.toHaveBeenCalled();
2897
+ expect(resolveAlertsSpy).not.toHaveBeenCalled();
1097
2898
  });
1098
2899
 
1099
- it("treats the same-state concurrency race as a benign no-op, not an error", async () => {
1100
- alertFindBySpy.mockResolvedValue([makeAlert(ObjectID.generate())]);
1101
- timelineCreateSpy.mockRejectedValue(
1102
- new BadDataException(SAME_STATE_RACE_MESSAGE),
2900
+ it("reads each updated rule as root, selecting only what a fingerprint needs", async () => {
2901
+ await callHook("onUpdateSuccess", makeOnUpdate({ isEnabled: false }), [
2902
+ RULE_ID,
2903
+ ]);
2904
+
2905
+ const findOneByIdArguments: FindOneByIdArguments = findOneByIdArgumentsAt(
2906
+ findOneByIdSpy,
2907
+ 0,
1103
2908
  );
1104
2909
 
1105
- const loggerErrorSpy: jest.SpyInstance = jest
1106
- .spyOn(logger, "error")
1107
- .mockImplementation(() => {
1108
- return undefined;
1109
- });
2910
+ expect(findOneByIdArguments.id).toEqual(RULE_ID);
2911
+ expect(findOneByIdArguments.select).toEqual({
2912
+ _id: true,
2913
+ projectId: true,
2914
+ serviceLevelObjectiveId: true,
2915
+ });
2916
+ expect(findOneByIdArguments.props).toEqual({ isRoot: true });
2917
+ });
1110
2918
 
1111
- await expect(
1112
- ServiceLevelObjectiveBurnRateRuleService.resolveOpenAlertsForRule({
2919
+ it("does not read anything when the update matched no rows", async () => {
2920
+ await callHook("onUpdateSuccess", makeOnUpdate({ isEnabled: false }), []);
2921
+
2922
+ expect(findOneByIdSpy).not.toHaveBeenCalled();
2923
+ });
2924
+
2925
+ const unresolvableRules: Array<{
2926
+ label: string;
2927
+ rule: ServiceLevelObjectiveBurnRateRule | null;
2928
+ }> = [
2929
+ { label: "a rule that no longer loads", rule: null },
2930
+ {
2931
+ label: "a rule that loads without a projectId",
2932
+ rule: makeRule({
2933
+ _id: RULE_ID.toString(),
2934
+ id: RULE_ID,
1113
2935
  serviceLevelObjectiveId: SLO_ID,
1114
- burnRateRuleId: RULE_ID,
2936
+ }),
2937
+ },
2938
+ {
2939
+ label: "a rule that loads without a serviceLevelObjectiveId",
2940
+ rule: makeRule({
2941
+ _id: RULE_ID.toString(),
2942
+ id: RULE_ID,
1115
2943
  projectId: PROJECT_ID,
1116
2944
  }),
1117
- ).resolves.toBeUndefined();
2945
+ },
2946
+ ];
2947
+
2948
+ unresolvableRules.forEach(
2949
+ (unresolvable: {
2950
+ label: string;
2951
+ rule: ServiceLevelObjectiveBurnRateRule | null;
2952
+ }): void => {
2953
+ /*
2954
+ * A rule deleted between the update and this hook, or one whose select
2955
+ * came back short, has nothing to build a fingerprint from - skipping is
2956
+ * right, and it must not turn a successful update into a thrown error.
2957
+ */
2958
+ it(`skips ${unresolvable.label} without throwing`, async () => {
2959
+ findOneByIdSpy.mockResolvedValue(unresolvable.rule);
2960
+
2961
+ await expect(
2962
+ callHook("onUpdateSuccess", makeOnUpdate({ isEnabled: false }), [
2963
+ RULE_ID,
2964
+ ]),
2965
+ ).resolves.toBeDefined();
2966
+
2967
+ expect(resolveAlertsSpy).not.toHaveBeenCalled();
2968
+ expect(resolveIncidentsSpy).not.toHaveBeenCalled();
2969
+ expect(loggerErrorSpy).not.toHaveBeenCalled();
2970
+ });
2971
+ },
2972
+ );
2973
+
2974
+ it("logs the failure of one rule and still resolves the rest", async () => {
2975
+ resolveAlertsSpy
2976
+ .mockRejectedValueOnce(new Error("alert service down"))
2977
+ .mockResolvedValueOnce(undefined);
2978
+
2979
+ await expect(
2980
+ callHook("onUpdateSuccess", makeOnUpdate({ isEnabled: false }), [
2981
+ RULE_ID,
2982
+ OTHER_RULE_ID,
2983
+ ]),
2984
+ ).resolves.toBeDefined();
2985
+
2986
+ expect(loggerErrorSpy).toHaveBeenCalledTimes(1);
2987
+ expect(resolveAlertsSpy).toHaveBeenCalledTimes(2);
1118
2988
 
1119
2989
  /*
1120
- * The inner catch must swallow this one specific message - if it were
1121
- * rethrown, the outer catch would log it as an error.
2990
+ * Isolation runs in BOTH directions here: a failed alert resolve does not
2991
+ * cost the same rule its incident resolve, and it does not cost the next
2992
+ * rule anything either. The worker cannot retry any of this — it only ever
2993
+ * loads rules where isEnabled is true — so whatever is skipped here stays
2994
+ * open forever.
1122
2995
  */
1123
- expect(loggerErrorSpy).not.toHaveBeenCalled();
2996
+ expect(resolveIncidentsSpy).toHaveBeenCalledTimes(2);
2997
+ expect(resolveIncidentsSpy).toHaveBeenCalledWith(
2998
+ expect.objectContaining({ burnRateRuleId: RULE_ID }),
2999
+ );
3000
+ expect(resolveIncidentsSpy).toHaveBeenCalledWith(
3001
+ expect.objectContaining({ burnRateRuleId: OTHER_RULE_ID }),
3002
+ );
1124
3003
  });
1125
3004
 
1126
- it("keeps resolving the remaining alerts after one alert fails", async () => {
1127
- const firstAlertId: ObjectID = ObjectID.generate();
1128
- const secondAlertId: ObjectID = ObjectID.generate();
3005
+ it("returns the onUpdate object it was handed", async () => {
3006
+ const onUpdate: OnUpdate<ServiceLevelObjectiveBurnRateRule> = makeOnUpdate({
3007
+ isEnabled: false,
3008
+ });
1129
3009
 
1130
- alertFindBySpy.mockResolvedValue([
1131
- makeAlert(firstAlertId),
1132
- makeAlert(secondAlertId),
3010
+ const result: unknown = await callHook("onUpdateSuccess", onUpdate, [
3011
+ RULE_ID,
1133
3012
  ]);
1134
3013
 
1135
- timelineCreateSpy
1136
- .mockRejectedValueOnce(new Error("write conflict"))
1137
- .mockResolvedValueOnce(new AlertStateTimeline());
3014
+ expect(result).toBe(onUpdate);
3015
+ });
3016
+ });
1138
3017
 
1139
- await expect(
1140
- ServiceLevelObjectiveBurnRateRuleService.resolveOpenAlertsForRule({
1141
- serviceLevelObjectiveId: SLO_ID,
1142
- burnRateRuleId: RULE_ID,
1143
- projectId: PROJECT_ID,
1144
- }),
1145
- ).resolves.toBeUndefined();
3018
+ /*
3019
+ * The rule's own lifecycle columns are what the evaluation worker reads to
3020
+ * decide whether an output is already open. Closing the record without
3021
+ * clearing them told the next tick "an alert is already open" when there was
3022
+ * none — and the worker then declared nothing for the rest of the burn. These
3023
+ * pin the clear, and pin that it only covers the side that actually closed.
3024
+ */
3025
+ describe("ServiceLevelObjectiveBurnRateRuleService.onUpdateSuccess forgets what it closed", () => {
3026
+ let findOneByIdSpy: jest.SpyInstance;
3027
+ let resolveAlertsSpy: jest.SpyInstance;
3028
+ let resolveIncidentsSpy: jest.SpyInstance;
3029
+ let clearStateSpy: jest.SpyInstance;
1146
3030
 
1147
- expect(timelineCreateSpy).toHaveBeenCalledTimes(2);
1148
- expect(timelineRows()[1]!.alertId).toEqual(secondAlertId);
3031
+ beforeEach(() => {
3032
+ findOneByIdSpy = jest
3033
+ .spyOn(ServiceLevelObjectiveBurnRateRuleService, "findOneById")
3034
+ .mockResolvedValue(
3035
+ makeRule({
3036
+ _id: RULE_ID.toString(),
3037
+ id: RULE_ID,
3038
+ projectId: PROJECT_ID,
3039
+ serviceLevelObjectiveId: SLO_ID,
3040
+ }),
3041
+ );
3042
+ resolveAlertsSpy = jest
3043
+ .spyOn(
3044
+ ServiceLevelObjectiveBurnRateRuleService,
3045
+ "resolveOpenAlertsForRule",
3046
+ )
3047
+ .mockResolvedValue(undefined);
3048
+ resolveIncidentsSpy = jest
3049
+ .spyOn(
3050
+ ServiceLevelObjectiveBurnRateRuleService,
3051
+ "resolveOpenIncidentsForRule",
3052
+ )
3053
+ .mockResolvedValue(undefined);
3054
+ clearStateSpy = jest
3055
+ .spyOn(
3056
+ ServiceLevelObjectiveBurnRateRuleService,
3057
+ "clearOpenOutputStateForRule",
3058
+ )
3059
+ .mockResolvedValue(undefined);
3060
+ jest.spyOn(logger, "error").mockImplementation(() => {
3061
+ return undefined;
3062
+ });
1149
3063
  });
1150
3064
 
1151
- it("does not swallow a non-benign BadDataException silently - it is logged", async () => {
1152
- alertFindBySpy.mockResolvedValue([makeAlert(ObjectID.generate())]);
1153
- timelineCreateSpy.mockRejectedValue(
1154
- new BadDataException("Some other validation problem."),
3065
+ afterEach(() => {
3066
+ jest.restoreAllMocks();
3067
+ });
3068
+
3069
+ it("forgets both outputs when a firing rule is disabled", async () => {
3070
+ await callHook("onUpdateSuccess", makeOnUpdate({ isEnabled: false }), [
3071
+ RULE_ID,
3072
+ ]);
3073
+
3074
+ expect(clearStateSpy).toHaveBeenCalledTimes(1);
3075
+ expect(clearStateSpy).toHaveBeenCalledWith({
3076
+ burnRateRuleId: RULE_ID,
3077
+ clearAlert: true,
3078
+ clearIncident: true,
3079
+ });
3080
+ });
3081
+
3082
+ it("forgets only the alert when only alerts were switched off", async () => {
3083
+ await callHook(
3084
+ "onUpdateSuccess",
3085
+ makeOnUpdate({ shouldCreateAlert: false }),
3086
+ [RULE_ID],
1155
3087
  );
1156
3088
 
1157
- const loggerErrorSpy: jest.SpyInstance = jest
1158
- .spyOn(logger, "error")
1159
- .mockImplementation(() => {
1160
- return undefined;
1161
- });
3089
+ expect(clearStateSpy).toHaveBeenCalledWith({
3090
+ burnRateRuleId: RULE_ID,
3091
+ clearAlert: true,
3092
+ clearIncident: false,
3093
+ });
3094
+ });
1162
3095
 
1163
- await expect(
1164
- ServiceLevelObjectiveBurnRateRuleService.resolveOpenAlertsForRule({
1165
- serviceLevelObjectiveId: SLO_ID,
1166
- burnRateRuleId: RULE_ID,
1167
- projectId: PROJECT_ID,
1168
- }),
1169
- ).resolves.toBeUndefined();
3096
+ it("forgets only the incident when only incidents were switched off", async () => {
3097
+ await callHook(
3098
+ "onUpdateSuccess",
3099
+ makeOnUpdate({ shouldCreateIncident: false }),
3100
+ [RULE_ID],
3101
+ );
1170
3102
 
1171
- expect(loggerErrorSpy).toHaveBeenCalledTimes(1);
3103
+ expect(clearStateSpy).toHaveBeenCalledWith({
3104
+ burnRateRuleId: RULE_ID,
3105
+ clearAlert: false,
3106
+ clearIncident: true,
3107
+ });
3108
+ });
3109
+
3110
+ it("does not forget a side whose resolve threw — its record is still open", async () => {
3111
+ resolveAlertsSpy.mockRejectedValueOnce(new Error("write conflict"));
3112
+
3113
+ await callHook("onUpdateSuccess", makeOnUpdate({ isEnabled: false }), [
3114
+ RULE_ID,
3115
+ ]);
3116
+
3117
+ /*
3118
+ * Clearing the alert column here would tell the worker to declare a second
3119
+ * alert on top of the one that is still open.
3120
+ */
3121
+ expect(clearStateSpy).toHaveBeenCalledWith({
3122
+ burnRateRuleId: RULE_ID,
3123
+ clearAlert: false,
3124
+ clearIncident: true,
3125
+ });
3126
+ });
3127
+
3128
+ it("resolves the incident even when the alert resolve throws", async () => {
3129
+ resolveAlertsSpy.mockRejectedValueOnce(new Error("write conflict"));
3130
+
3131
+ await callHook("onUpdateSuccess", makeOnUpdate({ isEnabled: false }), [
3132
+ RULE_ID,
3133
+ ]);
3134
+
3135
+ /*
3136
+ * The two outputs are independent lifecycles. Before they had their own
3137
+ * try blocks, an alert that failed to resolve took the incident's resolve
3138
+ * with it, leaving a live incident escalating for a disabled rule — and
3139
+ * the worker never revisits a disabled rule, so nothing retried it.
3140
+ */
3141
+ expect(resolveIncidentsSpy).toHaveBeenCalledTimes(1);
3142
+ });
3143
+
3144
+ it("clears nothing for a rule it could not read", async () => {
3145
+ findOneByIdSpy.mockResolvedValue(null);
3146
+
3147
+ await callHook("onUpdateSuccess", makeOnUpdate({ isEnabled: false }), [
3148
+ RULE_ID,
3149
+ ]);
3150
+
3151
+ expect(clearStateSpy).not.toHaveBeenCalled();
1172
3152
  });
1173
3153
  });
1174
3154
 
1175
3155
  describe("ServiceLevelObjectiveBurnRateRuleService.onBeforeDelete", () => {
1176
3156
  let findBySpy: jest.SpyInstance;
1177
- let resolveOpenAlertsSpy: jest.SpyInstance;
3157
+ let resolveOpenAlertsAndIncidentsSpy: jest.SpyInstance;
1178
3158
 
1179
3159
  beforeEach(() => {
1180
3160
  findBySpy = jest.spyOn(ServiceLevelObjectiveBurnRateRuleService, "findBy");
1181
- resolveOpenAlertsSpy = jest
3161
+ resolveOpenAlertsAndIncidentsSpy = jest
1182
3162
  .spyOn(
1183
3163
  ServiceLevelObjectiveBurnRateRuleService,
1184
- "resolveOpenAlertsForRule",
3164
+ "resolveOpenAlertsAndIncidentsForRule",
1185
3165
  )
1186
3166
  .mockResolvedValue(undefined);
1187
3167
  });
@@ -1190,7 +3170,7 @@ describe("ServiceLevelObjectiveBurnRateRuleService.onBeforeDelete", () => {
1190
3170
  jest.restoreAllMocks();
1191
3171
  });
1192
3172
 
1193
- it("resolves the open alerts of every rule about to be deleted", async () => {
3173
+ it("resolves the open alerts AND incidents of every rule about to be deleted", async () => {
1194
3174
  const first: ServiceLevelObjectiveBurnRateRule = makeRule({
1195
3175
  _id: RULE_ID.toString(),
1196
3176
  id: RULE_ID,
@@ -1208,14 +3188,14 @@ describe("ServiceLevelObjectiveBurnRateRuleService.onBeforeDelete", () => {
1208
3188
 
1209
3189
  const result: unknown = await callHook("onBeforeDelete", makeDeleteBy());
1210
3190
 
1211
- expect(resolveOpenAlertsSpy).toHaveBeenCalledTimes(2);
1212
- expect(resolveOpenAlertsSpy).toHaveBeenNthCalledWith(1, {
3191
+ expect(resolveOpenAlertsAndIncidentsSpy).toHaveBeenCalledTimes(2);
3192
+ expect(resolveOpenAlertsAndIncidentsSpy).toHaveBeenNthCalledWith(1, {
1213
3193
  serviceLevelObjectiveId: SLO_ID,
1214
3194
  burnRateRuleId: RULE_ID,
1215
3195
  projectId: PROJECT_ID,
1216
3196
  rootCause: DELETE_ROOT_CAUSE,
1217
3197
  });
1218
- expect(resolveOpenAlertsSpy).toHaveBeenNthCalledWith(2, {
3198
+ expect(resolveOpenAlertsAndIncidentsSpy).toHaveBeenNthCalledWith(2, {
1219
3199
  serviceLevelObjectiveId: SLO_ID,
1220
3200
  burnRateRuleId: OTHER_RULE_ID,
1221
3201
  projectId: PROJECT_ID,
@@ -1257,7 +3237,7 @@ describe("ServiceLevelObjectiveBurnRateRuleService.onBeforeDelete", () => {
1257
3237
  expect(findByArg.skip).toBe(0);
1258
3238
  expect(findByArg.props).toEqual({ isRoot: true });
1259
3239
 
1260
- expect(resolveOpenAlertsSpy).not.toHaveBeenCalled();
3240
+ expect(resolveOpenAlertsAndIncidentsSpy).not.toHaveBeenCalled();
1261
3241
  });
1262
3242
 
1263
3243
  it("skips rows missing the identity it needs to build a fingerprint", async () => {
@@ -1280,10 +3260,10 @@ describe("ServiceLevelObjectiveBurnRateRuleService.onBeforeDelete", () => {
1280
3260
 
1281
3261
  await callHook("onBeforeDelete", makeDeleteBy());
1282
3262
 
1283
- expect(resolveOpenAlertsSpy).not.toHaveBeenCalled();
3263
+ expect(resolveOpenAlertsAndIncidentsSpy).not.toHaveBeenCalled();
1284
3264
  });
1285
3265
 
1286
- it("does not block the delete when resolving one rule's alerts throws", async () => {
3266
+ it("does not block the delete when resolving one rule's records throws", async () => {
1287
3267
  findBySpy.mockResolvedValue([
1288
3268
  makeRule({
1289
3269
  _id: RULE_ID.toString(),
@@ -1299,7 +3279,7 @@ describe("ServiceLevelObjectiveBurnRateRuleService.onBeforeDelete", () => {
1299
3279
  }),
1300
3280
  ]);
1301
3281
 
1302
- resolveOpenAlertsSpy
3282
+ resolveOpenAlertsAndIncidentsSpy
1303
3283
  .mockRejectedValueOnce(new Error("alert service down"))
1304
3284
  .mockResolvedValueOnce(undefined);
1305
3285
 
@@ -1308,6 +3288,6 @@ describe("ServiceLevelObjectiveBurnRateRuleService.onBeforeDelete", () => {
1308
3288
  ).resolves.toBeDefined();
1309
3289
 
1310
3290
  // The failure on the first rule must not abort the second.
1311
- expect(resolveOpenAlertsSpy).toHaveBeenCalledTimes(2);
3291
+ expect(resolveOpenAlertsAndIncidentsSpy).toHaveBeenCalledTimes(2);
1312
3292
  });
1313
3293
  });