@oneuptime/common 9.4.7 → 9.4.8

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 (304) hide show
  1. package/Models/DatabaseModels/Alert.ts +76 -0
  2. package/Models/DatabaseModels/AlertEpisode.ts +1201 -0
  3. package/Models/DatabaseModels/AlertEpisodeFeed.ts +529 -0
  4. package/Models/DatabaseModels/AlertEpisodeInternalNote.ts +455 -0
  5. package/Models/DatabaseModels/AlertEpisodeMember.ts +586 -0
  6. package/Models/DatabaseModels/AlertEpisodeOwnerTeam.ts +421 -0
  7. package/Models/DatabaseModels/AlertEpisodeOwnerUser.ts +419 -0
  8. package/Models/DatabaseModels/AlertEpisodeStateTimeline.ts +523 -0
  9. package/Models/DatabaseModels/AlertFeed.ts +1 -0
  10. package/Models/DatabaseModels/AlertGroupingRule.ts +1432 -0
  11. package/Models/DatabaseModels/Index.ts +18 -0
  12. package/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.ts +70 -0
  13. package/Models/DatabaseModels/StatusPageDomain.ts +2 -0
  14. package/Models/DatabaseModels/WorkspaceNotificationLog.ts +57 -0
  15. package/Server/API/SlackAPI.ts +21 -0
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/1768938069147-MigrationName.ts +751 -0
  17. package/Server/Infrastructure/Postgres/SchemaMigrations/1769125561322-MigrationName.ts +41 -0
  18. package/Server/Infrastructure/Postgres/SchemaMigrations/1769170578688-MigrationName.ts +29 -0
  19. package/Server/Infrastructure/Postgres/SchemaMigrations/1769172358833-MigrationName.ts +177 -0
  20. package/Server/Infrastructure/Postgres/SchemaMigrations/1769176450526-MigrationName.ts +71 -0
  21. package/Server/Infrastructure/Postgres/SchemaMigrations/1769190495840-MigrationName.ts +35 -0
  22. package/Server/Infrastructure/Postgres/SchemaMigrations/1769199303656-MigrationName.ts +29 -0
  23. package/Server/Infrastructure/Postgres/SchemaMigrations/1769202898645-MigrationName.ts +29 -0
  24. package/Server/Infrastructure/Postgres/SchemaMigrations/1769428619414-MigrationName.ts +35 -0
  25. package/Server/Infrastructure/Postgres/SchemaMigrations/1769428821686-MigrationName.ts +47 -0
  26. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +20 -0
  27. package/Server/Services/AlertEpisodeFeedService.ts +94 -0
  28. package/Server/Services/AlertEpisodeInternalNoteService.ts +71 -0
  29. package/Server/Services/AlertEpisodeMemberService.ts +267 -0
  30. package/Server/Services/AlertEpisodeOwnerTeamService.ts +10 -0
  31. package/Server/Services/AlertEpisodeOwnerUserService.ts +10 -0
  32. package/Server/Services/AlertEpisodeService.ts +988 -0
  33. package/Server/Services/AlertEpisodeStateTimelineService.ts +557 -0
  34. package/Server/Services/AlertGroupingEngineService.ts +1120 -0
  35. package/Server/Services/AlertGroupingRuleService.ts +14 -0
  36. package/Server/Services/AlertService.ts +12 -0
  37. package/Server/Services/CallService.ts +2 -0
  38. package/Server/Services/Index.ts +21 -0
  39. package/Server/Services/MailService.ts +5 -0
  40. package/Server/Services/OnCallDutyPolicyService.ts +5 -0
  41. package/Server/Services/SmsService.ts +2 -0
  42. package/Server/Services/UserNotificationSettingService.ts +23 -0
  43. package/Server/Services/WhatsAppService.ts +5 -0
  44. package/Server/Services/WorkspaceNotificationRuleService.ts +26 -0
  45. package/Server/Utils/AnalyticsDatabase/Statement.ts +6 -2
  46. package/Server/Utils/WhatsAppTemplateUtil.ts +13 -0
  47. package/Server/Utils/Workspace/MicrosoftTeams/Actions/ActionTypes.ts +18 -0
  48. package/Server/Utils/Workspace/MicrosoftTeams/Actions/AlertEpisode.ts +689 -0
  49. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +16 -0
  50. package/Server/Utils/Workspace/Slack/Actions/ActionTypes.ts +11 -0
  51. package/Server/Utils/Workspace/Slack/Actions/AlertEpisode.ts +915 -0
  52. package/Server/Utils/Workspace/Slack/Messages/AlertEpisode.ts +120 -0
  53. package/Server/Utils/Workspace/WorkspaceMessages/AlertEpisode.ts +74 -0
  54. package/Tests/Server/Services/AlertEpisodeMemberService.test.ts +200 -0
  55. package/Tests/Server/Services/AlertEpisodeService.test.ts +240 -0
  56. package/Tests/Server/Services/AlertGroupingEngineService.test.ts +542 -0
  57. package/Tests/Server/Services/AlertGroupingRuleService.test.ts +383 -0
  58. package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +1 -1
  59. package/Tests/UI/Components/Input.test.tsx +1 -1
  60. package/Tests/UI/Components/TextArea.test.tsx +2 -2
  61. package/Types/BaseDatabase/SortOrder.ts +9 -0
  62. package/Types/Email/EmailTemplateType.ts +5 -0
  63. package/Types/NotificationRule/NotificationRuleType.ts +1 -0
  64. package/Types/NotificationSetting/NotificationSettingEventType.ts +7 -0
  65. package/Types/Permission.ts +309 -0
  66. package/Types/UserNotification/UserNotificationEventType.ts +1 -0
  67. package/Types/WhatsApp/WhatsAppTemplates.ts +20 -0
  68. package/Types/Workspace/NotificationRules/EventType.ts +1 -0
  69. package/Types/Workspace/NotificationRules/NotificationRuleCondition.ts +32 -3
  70. package/UI/Components/Accordion/Accordion.tsx +20 -2
  71. package/UI/Components/Alerts/Alert.tsx +1 -0
  72. package/UI/Components/Button/Button.tsx +29 -0
  73. package/UI/Components/CardSelect/CardSelect.tsx +5 -1
  74. package/UI/Components/Checkbox/Checkbox.tsx +7 -3
  75. package/UI/Components/ColorCircle/ColorCircle.tsx +2 -0
  76. package/UI/Components/ColorViewer/ColorViewer.tsx +19 -3
  77. package/UI/Components/CopyableButton/CopyableButton.tsx +22 -5
  78. package/UI/Components/Detail/Detail.tsx +1 -1
  79. package/UI/Components/Dropdown/Dropdown.tsx +14 -1
  80. package/UI/Components/Forms/Fields/FormField.tsx +28 -0
  81. package/UI/Components/FullPageModal/FullPageModal.tsx +35 -4
  82. package/UI/Components/Input/Input.tsx +14 -2
  83. package/UI/Components/Link/Link.tsx +1 -0
  84. package/UI/Components/Loader/Loader.tsx +8 -2
  85. package/UI/Components/Markdown.tsx/MarkdownViewer.tsx +76 -1
  86. package/UI/Components/Modal/Modal.tsx +47 -3
  87. package/UI/Components/ModelTable/BaseModelTable.tsx +42 -1
  88. package/UI/Components/MoreMenu/MoreMenu.tsx +84 -2
  89. package/UI/Components/OrderedStatesList/OrderedStatesList.tsx +30 -8
  90. package/UI/Components/Pagination/Pagination.tsx +113 -8
  91. package/UI/Components/ProgressBar/ProgressBar.tsx +12 -2
  92. package/UI/Components/Radio/Radio.tsx +21 -3
  93. package/UI/Components/SideMenu/CountModelSideMenuItem.tsx +54 -27
  94. package/UI/Components/StatusBubble/StatusBubble.tsx +7 -2
  95. package/UI/Components/Table/TableHeader.tsx +20 -3
  96. package/UI/Components/Tabs/Tab.tsx +16 -1
  97. package/UI/Components/Tabs/Tabs.tsx +12 -1
  98. package/UI/Components/TextArea/TextArea.tsx +12 -2
  99. package/UI/Components/Toggle/Toggle.tsx +14 -3
  100. package/UI/Components/Tooltip/Tooltip.tsx +11 -1
  101. package/UI/Components/TopAlert/TopAlert.tsx +2 -0
  102. package/build/dist/Models/DatabaseModels/Alert.js +77 -0
  103. package/build/dist/Models/DatabaseModels/Alert.js.map +1 -1
  104. package/build/dist/Models/DatabaseModels/AlertEpisode.js +1225 -0
  105. package/build/dist/Models/DatabaseModels/AlertEpisode.js.map +1 -0
  106. package/build/dist/Models/DatabaseModels/AlertEpisodeFeed.js +553 -0
  107. package/build/dist/Models/DatabaseModels/AlertEpisodeFeed.js.map +1 -0
  108. package/build/dist/Models/DatabaseModels/AlertEpisodeInternalNote.js +467 -0
  109. package/build/dist/Models/DatabaseModels/AlertEpisodeInternalNote.js.map +1 -0
  110. package/build/dist/Models/DatabaseModels/AlertEpisodeMember.js +607 -0
  111. package/build/dist/Models/DatabaseModels/AlertEpisodeMember.js.map +1 -0
  112. package/build/dist/Models/DatabaseModels/AlertEpisodeOwnerTeam.js +437 -0
  113. package/build/dist/Models/DatabaseModels/AlertEpisodeOwnerTeam.js.map +1 -0
  114. package/build/dist/Models/DatabaseModels/AlertEpisodeOwnerUser.js +436 -0
  115. package/build/dist/Models/DatabaseModels/AlertEpisodeOwnerUser.js.map +1 -0
  116. package/build/dist/Models/DatabaseModels/AlertEpisodeStateTimeline.js +546 -0
  117. package/build/dist/Models/DatabaseModels/AlertEpisodeStateTimeline.js.map +1 -0
  118. package/build/dist/Models/DatabaseModels/AlertFeed.js +1 -0
  119. package/build/dist/Models/DatabaseModels/AlertFeed.js.map +1 -1
  120. package/build/dist/Models/DatabaseModels/AlertGroupingRule.js +1437 -0
  121. package/build/dist/Models/DatabaseModels/AlertGroupingRule.js.map +1 -0
  122. package/build/dist/Models/DatabaseModels/Index.js +16 -0
  123. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  124. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.js +69 -0
  125. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.js.map +1 -1
  126. package/build/dist/Models/DatabaseModels/StatusPageDomain.js +2 -0
  127. package/build/dist/Models/DatabaseModels/StatusPageDomain.js.map +1 -1
  128. package/build/dist/Models/DatabaseModels/WorkspaceNotificationLog.js +58 -0
  129. package/build/dist/Models/DatabaseModels/WorkspaceNotificationLog.js.map +1 -1
  130. package/build/dist/Server/API/SlackAPI.js +18 -0
  131. package/build/dist/Server/API/SlackAPI.js.map +1 -1
  132. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1768938069147-MigrationName.js +266 -0
  133. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1768938069147-MigrationName.js.map +1 -0
  134. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1769125561322-MigrationName.js +20 -0
  135. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1769125561322-MigrationName.js.map +1 -0
  136. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1769170578688-MigrationName.js +16 -0
  137. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1769170578688-MigrationName.js.map +1 -0
  138. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1769172358833-MigrationName.js +68 -0
  139. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1769172358833-MigrationName.js.map +1 -0
  140. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1769176450526-MigrationName.js +30 -0
  141. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1769176450526-MigrationName.js.map +1 -0
  142. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1769190495840-MigrationName.js +18 -0
  143. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1769190495840-MigrationName.js.map +1 -0
  144. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1769199303656-MigrationName.js +16 -0
  145. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1769199303656-MigrationName.js.map +1 -0
  146. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1769202898645-MigrationName.js +16 -0
  147. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1769202898645-MigrationName.js.map +1 -0
  148. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1769428619414-MigrationName.js +18 -0
  149. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1769428619414-MigrationName.js.map +1 -0
  150. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1769428821686-MigrationName.js +22 -0
  151. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1769428821686-MigrationName.js.map +1 -0
  152. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +20 -0
  153. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  154. package/build/dist/Server/Services/AlertEpisodeFeedService.js +83 -0
  155. package/build/dist/Server/Services/AlertEpisodeFeedService.js.map +1 -0
  156. package/build/dist/Server/Services/AlertEpisodeInternalNoteService.js +70 -0
  157. package/build/dist/Server/Services/AlertEpisodeInternalNoteService.js.map +1 -0
  158. package/build/dist/Server/Services/AlertEpisodeMemberService.js +256 -0
  159. package/build/dist/Server/Services/AlertEpisodeMemberService.js.map +1 -0
  160. package/build/dist/Server/Services/AlertEpisodeOwnerTeamService.js +9 -0
  161. package/build/dist/Server/Services/AlertEpisodeOwnerTeamService.js.map +1 -0
  162. package/build/dist/Server/Services/AlertEpisodeOwnerUserService.js +9 -0
  163. package/build/dist/Server/Services/AlertEpisodeOwnerUserService.js.map +1 -0
  164. package/build/dist/Server/Services/AlertEpisodeService.js +885 -0
  165. package/build/dist/Server/Services/AlertEpisodeService.js.map +1 -0
  166. package/build/dist/Server/Services/AlertEpisodeStateTimelineService.js +494 -0
  167. package/build/dist/Server/Services/AlertEpisodeStateTimelineService.js.map +1 -0
  168. package/build/dist/Server/Services/AlertGroupingEngineService.js +893 -0
  169. package/build/dist/Server/Services/AlertGroupingEngineService.js.map +1 -0
  170. package/build/dist/Server/Services/AlertGroupingRuleService.js +13 -0
  171. package/build/dist/Server/Services/AlertGroupingRuleService.js.map +1 -0
  172. package/build/dist/Server/Services/AlertService.js +11 -0
  173. package/build/dist/Server/Services/AlertService.js.map +1 -1
  174. package/build/dist/Server/Services/CallService.js +11 -10
  175. package/build/dist/Server/Services/CallService.js.map +1 -1
  176. package/build/dist/Server/Services/Index.js +18 -0
  177. package/build/dist/Server/Services/Index.js.map +1 -1
  178. package/build/dist/Server/Services/MailService.js +3 -0
  179. package/build/dist/Server/Services/MailService.js.map +1 -1
  180. package/build/dist/Server/Services/OnCallDutyPolicyService.js +3 -0
  181. package/build/dist/Server/Services/OnCallDutyPolicyService.js.map +1 -1
  182. package/build/dist/Server/Services/SmsService.js +11 -10
  183. package/build/dist/Server/Services/SmsService.js.map +1 -1
  184. package/build/dist/Server/Services/UserNotificationSettingService.js +9 -0
  185. package/build/dist/Server/Services/UserNotificationSettingService.js.map +1 -1
  186. package/build/dist/Server/Services/WhatsAppService.js +3 -0
  187. package/build/dist/Server/Services/WhatsAppService.js.map +1 -1
  188. package/build/dist/Server/Services/WorkspaceNotificationRuleService.js +25 -0
  189. package/build/dist/Server/Services/WorkspaceNotificationRuleService.js.map +1 -1
  190. package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js +4 -2
  191. package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js.map +1 -1
  192. package/build/dist/Server/Utils/WhatsAppTemplateUtil.js +8 -0
  193. package/build/dist/Server/Utils/WhatsAppTemplateUtil.js.map +1 -1
  194. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/ActionTypes.js +17 -0
  195. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/ActionTypes.js.map +1 -1
  196. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/AlertEpisode.js +545 -0
  197. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/AlertEpisode.js.map +1 -0
  198. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +13 -0
  199. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  200. package/build/dist/Server/Utils/Workspace/Slack/Actions/ActionTypes.js +10 -0
  201. package/build/dist/Server/Utils/Workspace/Slack/Actions/ActionTypes.js.map +1 -1
  202. package/build/dist/Server/Utils/Workspace/Slack/Actions/AlertEpisode.js +651 -0
  203. package/build/dist/Server/Utils/Workspace/Slack/Actions/AlertEpisode.js.map +1 -0
  204. package/build/dist/Server/Utils/Workspace/Slack/Messages/AlertEpisode.js +100 -0
  205. package/build/dist/Server/Utils/Workspace/Slack/Messages/AlertEpisode.js.map +1 -0
  206. package/build/dist/Server/Utils/Workspace/WorkspaceMessages/AlertEpisode.js +70 -0
  207. package/build/dist/Server/Utils/Workspace/WorkspaceMessages/AlertEpisode.js.map +1 -0
  208. package/build/dist/Tests/Server/Services/AlertEpisodeMemberService.test.js +165 -0
  209. package/build/dist/Tests/Server/Services/AlertEpisodeMemberService.test.js.map +1 -0
  210. package/build/dist/Tests/Server/Services/AlertEpisodeService.test.js +193 -0
  211. package/build/dist/Tests/Server/Services/AlertEpisodeService.test.js.map +1 -0
  212. package/build/dist/Tests/Server/Services/AlertGroupingEngineService.test.js +412 -0
  213. package/build/dist/Tests/Server/Services/AlertGroupingEngineService.test.js.map +1 -0
  214. package/build/dist/Tests/Server/Services/AlertGroupingRuleService.test.js +308 -0
  215. package/build/dist/Tests/Server/Services/AlertGroupingRuleService.test.js.map +1 -0
  216. package/build/dist/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.js +1 -1
  217. package/build/dist/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.js.map +1 -1
  218. package/build/dist/Tests/UI/Components/Input.test.js +1 -1
  219. package/build/dist/Tests/UI/Components/Input.test.js.map +1 -1
  220. package/build/dist/Tests/UI/Components/TextArea.test.js +2 -2
  221. package/build/dist/Tests/UI/Components/TextArea.test.js.map +1 -1
  222. package/build/dist/Types/BaseDatabase/SortOrder.js +5 -0
  223. package/build/dist/Types/BaseDatabase/SortOrder.js.map +1 -1
  224. package/build/dist/Types/Email/EmailTemplateType.js +4 -0
  225. package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
  226. package/build/dist/Types/NotificationRule/NotificationRuleType.js +1 -0
  227. package/build/dist/Types/NotificationRule/NotificationRuleType.js.map +1 -1
  228. package/build/dist/Types/NotificationSetting/NotificationSettingEventType.js +5 -0
  229. package/build/dist/Types/NotificationSetting/NotificationSettingEventType.js.map +1 -1
  230. package/build/dist/Types/Permission.js +264 -0
  231. package/build/dist/Types/Permission.js.map +1 -1
  232. package/build/dist/Types/UserNotification/UserNotificationEventType.js +1 -0
  233. package/build/dist/Types/UserNotification/UserNotificationEventType.js.map +1 -1
  234. package/build/dist/Types/WhatsApp/WhatsAppTemplates.js +12 -0
  235. package/build/dist/Types/WhatsApp/WhatsAppTemplates.js.map +1 -1
  236. package/build/dist/Types/Workspace/NotificationRules/EventType.js +1 -0
  237. package/build/dist/Types/Workspace/NotificationRules/EventType.js.map +1 -1
  238. package/build/dist/Types/Workspace/NotificationRules/NotificationRuleCondition.js +28 -3
  239. package/build/dist/Types/Workspace/NotificationRules/NotificationRuleCondition.js.map +1 -1
  240. package/build/dist/UI/Components/Accordion/Accordion.js +10 -3
  241. package/build/dist/UI/Components/Accordion/Accordion.js.map +1 -1
  242. package/build/dist/UI/Components/Alerts/Alert.js +1 -1
  243. package/build/dist/UI/Components/Alerts/Alert.js.map +1 -1
  244. package/build/dist/UI/Components/Button/Button.js +8 -2
  245. package/build/dist/UI/Components/Button/Button.js.map +1 -1
  246. package/build/dist/UI/Components/CardSelect/CardSelect.js +1 -1
  247. package/build/dist/UI/Components/CardSelect/CardSelect.js.map +1 -1
  248. package/build/dist/UI/Components/Checkbox/Checkbox.js +2 -2
  249. package/build/dist/UI/Components/Checkbox/Checkbox.js.map +1 -1
  250. package/build/dist/UI/Components/ColorCircle/ColorCircle.js +1 -1
  251. package/build/dist/UI/Components/ColorCircle/ColorCircle.js.map +1 -1
  252. package/build/dist/UI/Components/ColorViewer/ColorViewer.js +12 -3
  253. package/build/dist/UI/Components/ColorViewer/ColorViewer.js.map +1 -1
  254. package/build/dist/UI/Components/CopyableButton/CopyableButton.js +12 -5
  255. package/build/dist/UI/Components/CopyableButton/CopyableButton.js.map +1 -1
  256. package/build/dist/UI/Components/Detail/Detail.js +1 -1
  257. package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
  258. package/build/dist/UI/Components/Dropdown/Dropdown.js +5 -3
  259. package/build/dist/UI/Components/Dropdown/Dropdown.js.map +1 -1
  260. package/build/dist/UI/Components/Forms/Fields/FormField.js +19 -1
  261. package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
  262. package/build/dist/UI/Components/FullPageModal/FullPageModal.js +24 -5
  263. package/build/dist/UI/Components/FullPageModal/FullPageModal.js.map +1 -1
  264. package/build/dist/UI/Components/Input/Input.js +3 -3
  265. package/build/dist/UI/Components/Input/Input.js.map +1 -1
  266. package/build/dist/UI/Components/Link/Link.js +1 -1
  267. package/build/dist/UI/Components/Link/Link.js.map +1 -1
  268. package/build/dist/UI/Components/Loader/Loader.js +6 -4
  269. package/build/dist/UI/Components/Loader/Loader.js.map +1 -1
  270. package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js +56 -3
  271. package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js.map +1 -1
  272. package/build/dist/UI/Components/Modal/Modal.js +28 -3
  273. package/build/dist/UI/Components/Modal/Modal.js.map +1 -1
  274. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +23 -1
  275. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  276. package/build/dist/UI/Components/MoreMenu/MoreMenu.js +67 -6
  277. package/build/dist/UI/Components/MoreMenu/MoreMenu.js.map +1 -1
  278. package/build/dist/UI/Components/OrderedStatesList/OrderedStatesList.js +14 -3
  279. package/build/dist/UI/Components/OrderedStatesList/OrderedStatesList.js.map +1 -1
  280. package/build/dist/UI/Components/Pagination/Pagination.js +69 -13
  281. package/build/dist/UI/Components/Pagination/Pagination.js.map +1 -1
  282. package/build/dist/UI/Components/ProgressBar/ProgressBar.js +2 -2
  283. package/build/dist/UI/Components/ProgressBar/ProgressBar.js.map +1 -1
  284. package/build/dist/UI/Components/Radio/Radio.js +8 -5
  285. package/build/dist/UI/Components/Radio/Radio.js.map +1 -1
  286. package/build/dist/UI/Components/SideMenu/CountModelSideMenuItem.js +23 -4
  287. package/build/dist/UI/Components/SideMenu/CountModelSideMenuItem.js.map +1 -1
  288. package/build/dist/UI/Components/StatusBubble/StatusBubble.js +2 -2
  289. package/build/dist/UI/Components/StatusBubble/StatusBubble.js.map +1 -1
  290. package/build/dist/UI/Components/Table/TableHeader.js +12 -4
  291. package/build/dist/UI/Components/Table/TableHeader.js.map +1 -1
  292. package/build/dist/UI/Components/Tabs/Tab.js +8 -1
  293. package/build/dist/UI/Components/Tabs/Tab.js.map +1 -1
  294. package/build/dist/UI/Components/Tabs/Tabs.js +4 -3
  295. package/build/dist/UI/Components/Tabs/Tabs.js.map +1 -1
  296. package/build/dist/UI/Components/TextArea/TextArea.js +3 -3
  297. package/build/dist/UI/Components/TextArea/TextArea.js.map +1 -1
  298. package/build/dist/UI/Components/Toggle/Toggle.js +7 -4
  299. package/build/dist/UI/Components/Toggle/Toggle.js.map +1 -1
  300. package/build/dist/UI/Components/Tooltip/Tooltip.js +4 -1
  301. package/build/dist/UI/Components/Tooltip/Tooltip.js.map +1 -1
  302. package/build/dist/UI/Components/TopAlert/TopAlert.js +1 -1
  303. package/build/dist/UI/Components/TopAlert/TopAlert.js.map +1 -1
  304. package/package.json +2 -1
@@ -0,0 +1,1120 @@
1
+ import ObjectID from "../../Types/ObjectID";
2
+ import AlertGroupingRule from "../../Models/DatabaseModels/AlertGroupingRule";
3
+ import Alert from "../../Models/DatabaseModels/Alert";
4
+ import AlertEpisode from "../../Models/DatabaseModels/AlertEpisode";
5
+ import AlertEpisodeMember, {
6
+ AlertEpisodeMemberAddedBy,
7
+ } from "../../Models/DatabaseModels/AlertEpisodeMember";
8
+ import Label from "../../Models/DatabaseModels/Label";
9
+ import Monitor from "../../Models/DatabaseModels/Monitor";
10
+ import AlertSeverity from "../../Models/DatabaseModels/AlertSeverity";
11
+ import ServiceMonitor from "../../Models/DatabaseModels/ServiceMonitor";
12
+ import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
13
+ import logger from "../Utils/Logger";
14
+ import SortOrder from "../../Types/BaseDatabase/SortOrder";
15
+ import OneUptimeDate from "../../Types/Date";
16
+ import QueryHelper from "../Types/Database/QueryHelper";
17
+ import AlertGroupingRuleService from "./AlertGroupingRuleService";
18
+ import AlertEpisodeService from "./AlertEpisodeService";
19
+ import AlertEpisodeMemberService from "./AlertEpisodeMemberService";
20
+ import MonitorService from "./MonitorService";
21
+ import ServiceMonitorService from "./ServiceMonitorService";
22
+ import Semaphore, { SemaphoreMutex } from "../Infrastructure/Semaphore";
23
+ import AlertEpisodeFeedService from "./AlertEpisodeFeedService";
24
+ import AlertFeedService from "./AlertFeedService";
25
+ import { AlertEpisodeFeedEventType } from "../../Models/DatabaseModels/AlertEpisodeFeed";
26
+ import { AlertFeedEventType } from "../../Models/DatabaseModels/AlertFeed";
27
+ import { Green500, Purple500 } from "../../Types/BrandColors";
28
+
29
+ export interface GroupingResult {
30
+ grouped: boolean;
31
+ episodeId?: ObjectID;
32
+ isNewEpisode?: boolean;
33
+ wasReopened?: boolean;
34
+ }
35
+
36
+ class AlertGroupingEngineServiceClass {
37
+ @CaptureSpan()
38
+ public async processAlert(alert: Alert): Promise<GroupingResult> {
39
+ logger.debug(`Processing alert ${alert.id} for grouping`);
40
+
41
+ try {
42
+ if (!alert.id || !alert.projectId) {
43
+ logger.warn("Alert missing id or projectId, skipping grouping");
44
+ return { grouped: false };
45
+ }
46
+
47
+ // If alert already has an episode, don't reprocess
48
+ if (alert.alertEpisodeId) {
49
+ return { grouped: true, episodeId: alert.alertEpisodeId };
50
+ }
51
+
52
+ // Get enabled rules sorted by priority
53
+ const rules: Array<AlertGroupingRule> =
54
+ await AlertGroupingRuleService.findBy({
55
+ query: {
56
+ projectId: alert.projectId,
57
+ isEnabled: true,
58
+ },
59
+ sort: {
60
+ priority: SortOrder.Ascending,
61
+ },
62
+ props: {
63
+ isRoot: true,
64
+ },
65
+ select: {
66
+ _id: true,
67
+ name: true,
68
+ priority: true,
69
+ // Match criteria fields
70
+ monitors: {
71
+ _id: true,
72
+ },
73
+ alertSeverities: {
74
+ _id: true,
75
+ },
76
+ alertLabels: {
77
+ _id: true,
78
+ },
79
+ monitorLabels: {
80
+ _id: true,
81
+ },
82
+ alertTitlePattern: true,
83
+ alertDescriptionPattern: true,
84
+ monitorNamePattern: true,
85
+ monitorDescriptionPattern: true,
86
+ // Group by fields
87
+ groupByMonitor: true,
88
+ groupBySeverity: true,
89
+ groupByAlertTitle: true,
90
+ groupByService: true,
91
+ // Time settings
92
+ enableTimeWindow: true,
93
+ timeWindowMinutes: true,
94
+ episodeTitleTemplate: true,
95
+ episodeDescriptionTemplate: true,
96
+ enableResolveDelay: true,
97
+ resolveDelayMinutes: true,
98
+ enableReopenWindow: true,
99
+ reopenWindowMinutes: true,
100
+ enableInactivityTimeout: true,
101
+ inactivityTimeoutMinutes: true,
102
+ defaultAssignToUserId: true,
103
+ defaultAssignToTeamId: true,
104
+ onCallDutyPolicies: {
105
+ _id: true,
106
+ },
107
+ },
108
+ limit: 100,
109
+ skip: 0,
110
+ });
111
+
112
+ if (rules.length === 0) {
113
+ logger.debug(
114
+ `No enabled grouping rules found for project ${alert.projectId}`,
115
+ );
116
+ return { grouped: false };
117
+ }
118
+
119
+ logger.debug(
120
+ `Found ${rules.length} enabled grouping rules for project ${alert.projectId}`,
121
+ );
122
+
123
+ // Find first matching rule
124
+ for (const rule of rules) {
125
+ const matches: boolean = await this.doesAlertMatchRule(alert, rule);
126
+
127
+ if (matches) {
128
+ logger.debug(
129
+ `Alert ${alert.id} matches rule ${rule.name || rule.id}`,
130
+ );
131
+
132
+ // Try to find existing episode or create new one
133
+ const result: GroupingResult = await this.groupAlertWithRule(
134
+ alert,
135
+ rule,
136
+ );
137
+ return result;
138
+ }
139
+ }
140
+
141
+ logger.debug(`Alert ${alert.id} did not match any grouping rules`);
142
+ return { grouped: false };
143
+ } catch (error) {
144
+ logger.error(`Error processing alert for grouping: ${error}`);
145
+ return { grouped: false };
146
+ }
147
+ }
148
+
149
+ @CaptureSpan()
150
+ private async doesAlertMatchRule(
151
+ alert: Alert,
152
+ rule: AlertGroupingRule,
153
+ ): Promise<boolean> {
154
+ logger.debug(
155
+ `Checking if alert ${alert.id} matches rule ${rule.name || rule.id}`,
156
+ );
157
+
158
+ // Check monitor IDs - if monitors are specified, alert must be from one of them
159
+ if (rule.monitors && rule.monitors.length > 0) {
160
+ if (!alert.monitorId) {
161
+ return false;
162
+ }
163
+ const monitorIds: Array<string> = rule.monitors.map((m: Monitor) => {
164
+ return m.id?.toString() || "";
165
+ });
166
+ const alertMonitorIdStr: string = alert.monitorId.toString();
167
+ if (!monitorIds.includes(alertMonitorIdStr)) {
168
+ return false;
169
+ }
170
+ }
171
+
172
+ // Check alert severity IDs - if severities are specified, alert must have one of them
173
+ if (rule.alertSeverities && rule.alertSeverities.length > 0) {
174
+ if (!alert.alertSeverityId) {
175
+ return false;
176
+ }
177
+ const severityIds: Array<string> = rule.alertSeverities.map(
178
+ (s: AlertSeverity) => {
179
+ return s.id?.toString() || "";
180
+ },
181
+ );
182
+ const alertSeverityIdStr: string = alert.alertSeverityId.toString();
183
+ if (!severityIds.includes(alertSeverityIdStr)) {
184
+ return false;
185
+ }
186
+ }
187
+
188
+ // Check alert label IDs - if alert labels are specified, alert must have at least one of them
189
+ if (rule.alertLabels && rule.alertLabels.length > 0) {
190
+ if (!alert.labels || alert.labels.length === 0) {
191
+ return false;
192
+ }
193
+ const ruleLabelIds: Array<string> = rule.alertLabels.map((l: Label) => {
194
+ return l.id?.toString() || "";
195
+ });
196
+ const alertLabelIds: Array<string> = alert.labels.map((l: Label) => {
197
+ return l.id?.toString() || "";
198
+ });
199
+ const hasMatchingLabel: boolean = ruleLabelIds.some((labelId: string) => {
200
+ return alertLabelIds.includes(labelId);
201
+ });
202
+ if (!hasMatchingLabel) {
203
+ return false;
204
+ }
205
+ }
206
+
207
+ // Check monitor-related criteria (labels, name pattern, description pattern)
208
+ const hasMonitorCriteria: boolean = Boolean(
209
+ (rule.monitorLabels && rule.monitorLabels.length > 0) ||
210
+ rule.monitorNamePattern ||
211
+ rule.monitorDescriptionPattern,
212
+ );
213
+
214
+ if (hasMonitorCriteria) {
215
+ if (!alert.monitorId) {
216
+ return false;
217
+ }
218
+
219
+ // Load monitor with all needed fields
220
+ const monitor: Monitor | null = await MonitorService.findOneById({
221
+ id: alert.monitorId,
222
+ select: {
223
+ name: true,
224
+ description: true,
225
+ labels: {
226
+ _id: true,
227
+ },
228
+ },
229
+ props: {
230
+ isRoot: true,
231
+ },
232
+ });
233
+
234
+ if (!monitor) {
235
+ return false;
236
+ }
237
+
238
+ // Check monitor labels
239
+ if (rule.monitorLabels && rule.monitorLabels.length > 0) {
240
+ if (!monitor.labels || monitor.labels.length === 0) {
241
+ return false;
242
+ }
243
+
244
+ const ruleMonitorLabelIds: Array<string> = rule.monitorLabels.map(
245
+ (l: Label) => {
246
+ return l.id?.toString() || "";
247
+ },
248
+ );
249
+ const monitorLabelIds: Array<string> = monitor.labels.map(
250
+ (l: Label) => {
251
+ return l.id?.toString() || "";
252
+ },
253
+ );
254
+ const hasMatchingMonitorLabel: boolean = ruleMonitorLabelIds.some(
255
+ (labelId: string) => {
256
+ return monitorLabelIds.includes(labelId);
257
+ },
258
+ );
259
+ if (!hasMatchingMonitorLabel) {
260
+ return false;
261
+ }
262
+ }
263
+
264
+ // Check monitor name pattern (regex)
265
+ if (rule.monitorNamePattern) {
266
+ if (!monitor.name) {
267
+ return false;
268
+ }
269
+ try {
270
+ const regex: RegExp = new RegExp(rule.monitorNamePattern, "i");
271
+ if (!regex.test(monitor.name)) {
272
+ return false;
273
+ }
274
+ } catch {
275
+ logger.warn(
276
+ `Invalid regex pattern in rule ${rule.id}: ${rule.monitorNamePattern}`,
277
+ );
278
+ return false;
279
+ }
280
+ }
281
+
282
+ // Check monitor description pattern (regex)
283
+ if (rule.monitorDescriptionPattern) {
284
+ if (!monitor.description) {
285
+ return false;
286
+ }
287
+ try {
288
+ const regex: RegExp = new RegExp(rule.monitorDescriptionPattern, "i");
289
+ if (!regex.test(monitor.description)) {
290
+ return false;
291
+ }
292
+ } catch {
293
+ logger.warn(
294
+ `Invalid regex pattern in rule ${rule.id}: ${rule.monitorDescriptionPattern}`,
295
+ );
296
+ return false;
297
+ }
298
+ }
299
+ }
300
+
301
+ // Check alert title pattern (regex)
302
+ if (rule.alertTitlePattern) {
303
+ if (!alert.title) {
304
+ return false;
305
+ }
306
+ try {
307
+ const regex: RegExp = new RegExp(rule.alertTitlePattern, "i");
308
+ if (!regex.test(alert.title)) {
309
+ return false;
310
+ }
311
+ } catch {
312
+ logger.warn(
313
+ `Invalid regex pattern in rule ${rule.id}: ${rule.alertTitlePattern}`,
314
+ );
315
+ return false;
316
+ }
317
+ }
318
+
319
+ // Check alert description pattern (regex)
320
+ if (rule.alertDescriptionPattern) {
321
+ if (!alert.description) {
322
+ return false;
323
+ }
324
+ try {
325
+ const regex: RegExp = new RegExp(rule.alertDescriptionPattern, "i");
326
+ if (!regex.test(alert.description)) {
327
+ return false;
328
+ }
329
+ } catch {
330
+ logger.warn(
331
+ `Invalid regex pattern in rule ${rule.id}: ${rule.alertDescriptionPattern}`,
332
+ );
333
+ return false;
334
+ }
335
+ }
336
+
337
+ // If no criteria specified (all fields empty), rule matches all alerts
338
+ logger.debug(
339
+ `Rule ${rule.name || rule.id} matched alert ${alert.id} (all criteria passed)`,
340
+ );
341
+ return true;
342
+ }
343
+
344
+ @CaptureSpan()
345
+ private async groupAlertWithRule(
346
+ alert: Alert,
347
+ rule: AlertGroupingRule,
348
+ ): Promise<GroupingResult> {
349
+ // Build the grouping key based on groupBy fields
350
+ const groupingKey: string = await this.buildGroupingKey(alert, rule);
351
+
352
+ // Create mutex key to prevent race conditions when creating episodes
353
+ const mutexKey: string = `${alert.projectId?.toString()}-${rule.id?.toString()}-${groupingKey}`;
354
+
355
+ let mutex: SemaphoreMutex | null = null;
356
+
357
+ try {
358
+ /*
359
+ * Acquire mutex to prevent concurrent episode creation for the same grouping key
360
+ * This is critical - we must have the lock before proceeding to prevent race conditions
361
+ */
362
+ logger.debug(
363
+ `Acquiring mutex for grouping key: ${mutexKey} for alert ${alert.id}`,
364
+ );
365
+ mutex = await Semaphore.lock({
366
+ key: mutexKey,
367
+ namespace: "AlertGroupingEngine.groupAlertWithRule",
368
+ lockTimeout: 30000, // 30 seconds - enough time to complete episode creation
369
+ acquireTimeout: 60000, // Wait up to 60 seconds to acquire the lock
370
+ });
371
+ logger.debug(
372
+ `Acquired mutex for grouping key: ${mutexKey} for alert ${alert.id}`,
373
+ );
374
+
375
+ // Calculate time window cutoff (only if time window is enabled)
376
+ let timeWindowCutoff: Date | null = null;
377
+ if (rule.enableTimeWindow) {
378
+ const timeWindowMinutes: number = rule.timeWindowMinutes || 60;
379
+ timeWindowCutoff = OneUptimeDate.getSomeMinutesAgo(timeWindowMinutes);
380
+ }
381
+
382
+ // Find existing active episode that matches
383
+ const existingEpisode: AlertEpisode | null =
384
+ await this.findMatchingActiveEpisode(
385
+ alert.projectId!,
386
+ rule.id!,
387
+ groupingKey,
388
+ timeWindowCutoff,
389
+ );
390
+
391
+ if (existingEpisode && existingEpisode.id) {
392
+ // Add alert to existing episode
393
+ await this.addAlertToEpisode(
394
+ alert,
395
+ existingEpisode.id,
396
+ AlertEpisodeMemberAddedBy.Rule,
397
+ rule.id!,
398
+ rule,
399
+ false, // isNewEpisode
400
+ false, // wasReopened
401
+ );
402
+
403
+ // Update episode severity if alert has higher severity
404
+ if (alert.alertSeverityId) {
405
+ await AlertEpisodeService.updateEpisodeSeverity({
406
+ episodeId: existingEpisode.id,
407
+ severityId: alert.alertSeverityId,
408
+ onlyIfHigher: true,
409
+ });
410
+ }
411
+
412
+ return {
413
+ grouped: true,
414
+ episodeId: existingEpisode.id,
415
+ isNewEpisode: false,
416
+ };
417
+ }
418
+
419
+ // Check if we can reopen a recently resolved episode (only if enabled)
420
+ if (rule.enableReopenWindow) {
421
+ const reopenWindowMinutes: number = rule.reopenWindowMinutes || 0;
422
+ if (reopenWindowMinutes > 0) {
423
+ const reopenCutoff: Date =
424
+ OneUptimeDate.getSomeMinutesAgo(reopenWindowMinutes);
425
+ const recentlyResolvedEpisode: AlertEpisode | null =
426
+ await this.findRecentlyResolvedEpisode(
427
+ alert.projectId!,
428
+ rule.id!,
429
+ groupingKey,
430
+ reopenCutoff,
431
+ );
432
+
433
+ if (recentlyResolvedEpisode && recentlyResolvedEpisode.id) {
434
+ // Reopen the episode
435
+ await AlertEpisodeService.reopenEpisode(recentlyResolvedEpisode.id);
436
+
437
+ // Add alert to reopened episode
438
+ await this.addAlertToEpisode(
439
+ alert,
440
+ recentlyResolvedEpisode.id,
441
+ AlertEpisodeMemberAddedBy.Rule,
442
+ rule.id!,
443
+ rule,
444
+ false, // isNewEpisode
445
+ true, // wasReopened
446
+ );
447
+
448
+ // Update episode severity if alert has higher severity
449
+ if (alert.alertSeverityId) {
450
+ await AlertEpisodeService.updateEpisodeSeverity({
451
+ episodeId: recentlyResolvedEpisode.id,
452
+ severityId: alert.alertSeverityId,
453
+ onlyIfHigher: true,
454
+ });
455
+ }
456
+
457
+ return {
458
+ grouped: true,
459
+ episodeId: recentlyResolvedEpisode.id,
460
+ isNewEpisode: false,
461
+ wasReopened: true,
462
+ };
463
+ }
464
+ }
465
+ }
466
+
467
+ // Create new episode
468
+ const newEpisode: AlertEpisode | null = await this.createNewEpisode(
469
+ alert,
470
+ rule,
471
+ groupingKey,
472
+ );
473
+
474
+ if (newEpisode && newEpisode.id) {
475
+ // Add alert to new episode
476
+ await this.addAlertToEpisode(
477
+ alert,
478
+ newEpisode.id,
479
+ AlertEpisodeMemberAddedBy.Rule,
480
+ rule.id!,
481
+ rule,
482
+ true, // isNewEpisode
483
+ false, // wasReopened
484
+ );
485
+
486
+ return { grouped: true, episodeId: newEpisode.id, isNewEpisode: true };
487
+ }
488
+
489
+ return { grouped: false };
490
+ } finally {
491
+ // Release mutex
492
+ if (mutex) {
493
+ try {
494
+ logger.debug(
495
+ `Releasing mutex for grouping key: ${mutexKey} for alert ${alert.id}`,
496
+ );
497
+ await Semaphore.release(mutex);
498
+ logger.debug(
499
+ `Released mutex for grouping key: ${mutexKey} for alert ${alert.id}`,
500
+ );
501
+ } catch (err) {
502
+ logger.error(
503
+ `Error releasing mutex for grouping key: ${mutexKey}: ${err}`,
504
+ );
505
+ }
506
+ }
507
+ }
508
+ }
509
+
510
+ @CaptureSpan()
511
+ private async buildGroupingKey(
512
+ alert: Alert,
513
+ rule: AlertGroupingRule,
514
+ ): Promise<string> {
515
+ const parts: Array<string> = [];
516
+
517
+ /*
518
+ * Group by service - only if explicitly enabled
519
+ * Must be checked before monitor since service contains multiple monitors
520
+ */
521
+ if (rule.groupByService && alert.monitorId) {
522
+ const serviceMonitor: ServiceMonitor | null =
523
+ await ServiceMonitorService.findOneBy({
524
+ query: {
525
+ monitorId: alert.monitorId,
526
+ },
527
+ select: {
528
+ serviceId: true,
529
+ },
530
+ props: {
531
+ isRoot: true,
532
+ },
533
+ });
534
+
535
+ if (serviceMonitor?.serviceId) {
536
+ parts.push(`service:${serviceMonitor.serviceId.toString()}`);
537
+ }
538
+ }
539
+
540
+ // Group by monitor - only if explicitly enabled
541
+ if (rule.groupByMonitor && alert.monitorId) {
542
+ parts.push(`monitor:${alert.monitorId.toString()}`);
543
+ }
544
+
545
+ // Group by severity - only if explicitly enabled
546
+ if (rule.groupBySeverity && alert.alertSeverityId) {
547
+ parts.push(`severity:${alert.alertSeverityId.toString()}`);
548
+ }
549
+
550
+ // Group by alert title - only if explicitly enabled
551
+ if (rule.groupByAlertTitle && alert.title) {
552
+ // Normalize title for grouping (remove numbers, etc.)
553
+ const normalizedTitle: string = alert.title
554
+ .toLowerCase()
555
+ .replace(/\d+/g, "X");
556
+ parts.push(`title:${normalizedTitle}`);
557
+ }
558
+
559
+ // If no group by options are enabled, all matching alerts go into a single episode
560
+ return parts.join("|") || "default";
561
+ }
562
+
563
+ @CaptureSpan()
564
+ private async findMatchingActiveEpisode(
565
+ projectId: ObjectID,
566
+ ruleId: ObjectID,
567
+ groupingKey: string,
568
+ timeWindowCutoff: Date | null,
569
+ ): Promise<AlertEpisode | null> {
570
+ /*
571
+ * Find active episode with matching rule and grouping key
572
+ * Active episodes have resolvedAt = null (not yet resolved)
573
+ * If time window is enabled, also filter by lastAlertAddedAt
574
+ * If time window is disabled (timeWindowCutoff is null), find any matching active episode
575
+ */
576
+ interface EpisodeQueryType {
577
+ projectId: ObjectID;
578
+ alertGroupingRuleId: ObjectID;
579
+ groupingKey: string;
580
+ resolvedAt: null;
581
+ lastAlertAddedAt?: ReturnType<typeof QueryHelper.greaterThanEqualTo>;
582
+ }
583
+
584
+ const query: EpisodeQueryType = {
585
+ projectId: projectId,
586
+ alertGroupingRuleId: ruleId,
587
+ groupingKey: groupingKey,
588
+ resolvedAt: null, // Only find active (non-resolved) episodes
589
+ };
590
+
591
+ // Only add time window filter if enabled
592
+ if (timeWindowCutoff) {
593
+ query.lastAlertAddedAt = QueryHelper.greaterThanEqualTo(timeWindowCutoff);
594
+ }
595
+
596
+ const episode: AlertEpisode | null = await AlertEpisodeService.findOneBy({
597
+ query: query as any,
598
+ sort: {
599
+ lastAlertAddedAt: SortOrder.Descending,
600
+ },
601
+ select: {
602
+ _id: true,
603
+ lastAlertAddedAt: true,
604
+ },
605
+ props: {
606
+ isRoot: true,
607
+ },
608
+ });
609
+
610
+ return episode;
611
+ }
612
+
613
+ @CaptureSpan()
614
+ private async findRecentlyResolvedEpisode(
615
+ projectId: ObjectID,
616
+ ruleId: ObjectID,
617
+ groupingKey: string,
618
+ reopenCutoff: Date,
619
+ ): Promise<AlertEpisode | null> {
620
+ // Find recently resolved episode with matching rule and grouping key
621
+ const episode: AlertEpisode | null = await AlertEpisodeService.findOneBy({
622
+ query: {
623
+ projectId: projectId,
624
+ alertGroupingRuleId: ruleId,
625
+ groupingKey: groupingKey,
626
+ resolvedAt: QueryHelper.greaterThanEqualTo(reopenCutoff),
627
+ },
628
+ sort: {
629
+ resolvedAt: SortOrder.Descending,
630
+ },
631
+ select: {
632
+ _id: true,
633
+ resolvedAt: true,
634
+ },
635
+ props: {
636
+ isRoot: true,
637
+ },
638
+ });
639
+
640
+ return episode;
641
+ }
642
+
643
+ @CaptureSpan()
644
+ private async createNewEpisode(
645
+ alert: Alert,
646
+ rule: AlertGroupingRule,
647
+ groupingKey: string,
648
+ ): Promise<AlertEpisode | null> {
649
+ // Generate episode title from template (with initial alertCount of 1)
650
+ const title: string = this.generateEpisodeTitle(
651
+ alert,
652
+ rule.episodeTitleTemplate,
653
+ 1, // Initial alert count
654
+ );
655
+
656
+ // Generate episode description from template (with initial alertCount of 1)
657
+ const description: string | undefined = this.generateEpisodeDescription(
658
+ alert,
659
+ rule.episodeDescriptionTemplate,
660
+ 1, // Initial alert count
661
+ );
662
+
663
+ const newEpisode: AlertEpisode = new AlertEpisode();
664
+ newEpisode.projectId = alert.projectId!;
665
+ newEpisode.title = title;
666
+ if (description) {
667
+ newEpisode.description = description;
668
+ }
669
+ /*
670
+ * Store preprocessed templates for dynamic variable updates
671
+ * Static variables are replaced, dynamic ones (like {{alertCount}}) remain as placeholders
672
+ */
673
+ if (rule.episodeTitleTemplate) {
674
+ newEpisode.titleTemplate = this.preprocessTemplate(
675
+ alert,
676
+ rule.episodeTitleTemplate,
677
+ );
678
+ }
679
+ if (rule.episodeDescriptionTemplate) {
680
+ newEpisode.descriptionTemplate = this.preprocessTemplate(
681
+ alert,
682
+ rule.episodeDescriptionTemplate,
683
+ );
684
+ }
685
+ newEpisode.alertGroupingRuleId = rule.id!;
686
+ newEpisode.groupingKey = groupingKey;
687
+ newEpisode.isManuallyCreated = false;
688
+
689
+ // Set severity from alert
690
+ if (alert.alertSeverityId) {
691
+ newEpisode.alertSeverityId = alert.alertSeverityId;
692
+ }
693
+
694
+ // Set default ownership from rule
695
+ if (rule.defaultAssignToUserId) {
696
+ newEpisode.assignedToUserId = rule.defaultAssignToUserId;
697
+ }
698
+
699
+ if (rule.defaultAssignToTeamId) {
700
+ newEpisode.assignedToTeamId = rule.defaultAssignToTeamId;
701
+ }
702
+
703
+ // Copy on-call policies from rule
704
+ if (rule.onCallDutyPolicies && rule.onCallDutyPolicies.length > 0) {
705
+ newEpisode.onCallDutyPolicies = rule.onCallDutyPolicies;
706
+ }
707
+
708
+ try {
709
+ const createdEpisode: AlertEpisode = await AlertEpisodeService.create({
710
+ data: newEpisode,
711
+ props: {
712
+ isRoot: true,
713
+ },
714
+ });
715
+
716
+ // Add episode feed entry for episode creation
717
+ if (createdEpisode.id) {
718
+ const groupByParts: Array<string> = [];
719
+
720
+ if (rule.groupByMonitor) {
721
+ groupByParts.push("Monitor");
722
+ }
723
+ if (rule.groupBySeverity) {
724
+ groupByParts.push("Severity");
725
+ }
726
+ if (rule.groupByAlertTitle) {
727
+ groupByParts.push("Alert Title");
728
+ }
729
+ if (rule.groupByService) {
730
+ groupByParts.push("Service");
731
+ }
732
+
733
+ const groupByDescription: string =
734
+ groupByParts.length > 0
735
+ ? `Grouping by: ${groupByParts.join(", ")}`
736
+ : "Grouping all matching alerts together";
737
+
738
+ let moreInfo: string = `**Rule:** ${rule.name || "Unnamed Rule"}\n\n`;
739
+ moreInfo += `**Grouping Key:** \`${groupingKey}\`\n\n`;
740
+ moreInfo += `**${groupByDescription}**`;
741
+
742
+ if (rule.enableTimeWindow && rule.timeWindowMinutes) {
743
+ moreInfo += `\n\n**Time Window:** ${rule.timeWindowMinutes} minutes`;
744
+ }
745
+
746
+ try {
747
+ await AlertEpisodeFeedService.createAlertEpisodeFeedItem({
748
+ alertEpisodeId: createdEpisode.id,
749
+ projectId: alert.projectId!,
750
+ alertEpisodeFeedEventType: AlertEpisodeFeedEventType.EpisodeCreated,
751
+ displayColor: Green500,
752
+ feedInfoInMarkdown: `🔔 **Episode Created** by grouping rule **${rule.name || "Unnamed Rule"}**`,
753
+ moreInformationInMarkdown: moreInfo,
754
+ });
755
+ } catch (feedError) {
756
+ logger.error(
757
+ `Error creating episode feed for episode creation: ${feedError}`,
758
+ );
759
+ }
760
+ }
761
+
762
+ return createdEpisode;
763
+ } catch (error) {
764
+ logger.error(`Error creating new episode: ${error}`);
765
+ return null;
766
+ }
767
+ }
768
+
769
+ private generateEpisodeTitle(
770
+ alert: Alert,
771
+ template: string | undefined,
772
+ alertCount: number = 1,
773
+ ): string {
774
+ if (!template) {
775
+ // Default title based on alert
776
+ if (alert.monitor?.name) {
777
+ return `Alert Episode: ${alert.monitor.name}`;
778
+ }
779
+ if (alert.title) {
780
+ return `Alert Episode: ${alert.title.substring(0, 50)}`;
781
+ }
782
+ return "Alert Episode";
783
+ }
784
+
785
+ return (
786
+ this.replaceTemplatePlaceholders(alert, template, alertCount) ||
787
+ "Alert Episode"
788
+ );
789
+ }
790
+
791
+ private generateEpisodeDescription(
792
+ alert: Alert,
793
+ template: string | undefined,
794
+ alertCount: number = 1,
795
+ ): string | undefined {
796
+ if (!template) {
797
+ return undefined;
798
+ }
799
+
800
+ return (
801
+ this.replaceTemplatePlaceholders(alert, template, alertCount) || undefined
802
+ );
803
+ }
804
+
805
+ private replaceTemplatePlaceholders(
806
+ alert: Alert,
807
+ template: string,
808
+ alertCount: number = 1,
809
+ ): string {
810
+ let result: string = template;
811
+
812
+ /*
813
+ * Static variables (from first alert)
814
+ * {{alertTitle}}
815
+ */
816
+ if (alert.title) {
817
+ result = result.replace(/\{\{alertTitle\}\}/g, alert.title);
818
+ }
819
+
820
+ // {{alertDescription}}
821
+ if (alert.description) {
822
+ result = result.replace(/\{\{alertDescription\}\}/g, alert.description);
823
+ }
824
+
825
+ // {{monitorName}}
826
+ if (alert.monitor?.name) {
827
+ result = result.replace(/\{\{monitorName\}\}/g, alert.monitor.name);
828
+ }
829
+
830
+ // {{alertSeverity}}
831
+ if (alert.alertSeverity?.name) {
832
+ result = result.replace(
833
+ /\{\{alertSeverity\}\}/g,
834
+ alert.alertSeverity.name,
835
+ );
836
+ }
837
+
838
+ /*
839
+ * Dynamic variables (updated when alerts are added/removed)
840
+ * {{alertCount}}
841
+ */
842
+ result = result.replace(/\{\{alertCount\}\}/g, alertCount.toString());
843
+
844
+ // Clean up any remaining unknown placeholders
845
+ result = result.replace(/\{\{[^}]+\}\}/g, "");
846
+
847
+ return result;
848
+ }
849
+
850
+ /*
851
+ * Preprocess template: replace static variables but keep dynamic ones as placeholders
852
+ * This is stored on the episode so we can re-render with updated dynamic values later
853
+ */
854
+ private preprocessTemplate(alert: Alert, template: string): string {
855
+ let result: string = template;
856
+
857
+ /*
858
+ * Replace static variables (from first alert)
859
+ * {{alertTitle}}
860
+ */
861
+ if (alert.title) {
862
+ result = result.replace(/\{\{alertTitle\}\}/g, alert.title);
863
+ }
864
+
865
+ // {{alertDescription}}
866
+ if (alert.description) {
867
+ result = result.replace(/\{\{alertDescription\}\}/g, alert.description);
868
+ }
869
+
870
+ // {{monitorName}}
871
+ if (alert.monitor?.name) {
872
+ result = result.replace(/\{\{monitorName\}\}/g, alert.monitor.name);
873
+ }
874
+
875
+ // {{alertSeverity}}
876
+ if (alert.alertSeverity?.name) {
877
+ result = result.replace(
878
+ /\{\{alertSeverity\}\}/g,
879
+ alert.alertSeverity.name,
880
+ );
881
+ }
882
+
883
+ /*
884
+ * Keep dynamic variables as placeholders (e.g., {{alertCount}})
885
+ * They will be replaced when title/description is re-rendered
886
+ */
887
+
888
+ return result;
889
+ }
890
+
891
+ @CaptureSpan()
892
+ private async addAlertToEpisode(
893
+ alert: Alert,
894
+ episodeId: ObjectID,
895
+ addedBy: AlertEpisodeMemberAddedBy,
896
+ ruleId?: ObjectID,
897
+ rule?: AlertGroupingRule,
898
+ isNewEpisode?: boolean,
899
+ wasReopened?: boolean,
900
+ ): Promise<void> {
901
+ const member: AlertEpisodeMember = new AlertEpisodeMember();
902
+ member.projectId = alert.projectId!;
903
+ member.alertEpisodeId = episodeId;
904
+ member.alertId = alert.id!;
905
+ member.addedBy = addedBy;
906
+
907
+ if (ruleId) {
908
+ member.matchedRuleId = ruleId;
909
+ }
910
+
911
+ try {
912
+ await AlertEpisodeMemberService.create({
913
+ data: member,
914
+ props: {
915
+ isRoot: true,
916
+ },
917
+ });
918
+
919
+ // Create feed entries for alert being added to episode
920
+ await this.createAlertAddedFeedEntries({
921
+ alert,
922
+ episodeId,
923
+ addedBy,
924
+ rule,
925
+ isNewEpisode,
926
+ wasReopened,
927
+ });
928
+ } catch (error) {
929
+ // Check if it's a duplicate error (alert already in episode)
930
+ if (
931
+ error instanceof Error &&
932
+ error.message.includes("already a member")
933
+ ) {
934
+ logger.debug(`Alert ${alert.id} is already in episode ${episodeId}`);
935
+ return;
936
+ }
937
+ throw error;
938
+ }
939
+ }
940
+
941
+ @CaptureSpan()
942
+ private async createAlertAddedFeedEntries(data: {
943
+ alert: Alert;
944
+ episodeId: ObjectID;
945
+ addedBy: AlertEpisodeMemberAddedBy;
946
+ rule?: AlertGroupingRule | undefined;
947
+ isNewEpisode?: boolean | undefined;
948
+ wasReopened?: boolean | undefined;
949
+ addedByUserId?: ObjectID | undefined;
950
+ }): Promise<void> {
951
+ const {
952
+ alert,
953
+ episodeId,
954
+ addedBy,
955
+ rule,
956
+ isNewEpisode,
957
+ wasReopened,
958
+ addedByUserId,
959
+ } = data;
960
+
961
+ // Fetch episode number for feed entry
962
+ const episode: AlertEpisode | null = await AlertEpisodeService.findOneById({
963
+ id: episodeId,
964
+ select: {
965
+ episodeNumber: true,
966
+ },
967
+ props: {
968
+ isRoot: true,
969
+ },
970
+ });
971
+
972
+ const episodeNumber: number = episode?.episodeNumber || 0;
973
+ const alertNumber: number = alert.alertNumber || 0;
974
+
975
+ // Build explanation of why the alert was added
976
+ let matchReason: string = "";
977
+ let alertFeedMessage: string = "";
978
+ let episodeFeedMessage: string = "";
979
+
980
+ if (addedBy === AlertEpisodeMemberAddedBy.Rule && rule) {
981
+ const matchCriteria: Array<string> = [];
982
+
983
+ if (rule.monitors && rule.monitors.length > 0) {
984
+ matchCriteria.push("monitor matches rule criteria");
985
+ }
986
+ if (rule.alertSeverities && rule.alertSeverities.length > 0) {
987
+ matchCriteria.push("severity matches rule criteria");
988
+ }
989
+ if (rule.alertLabels && rule.alertLabels.length > 0) {
990
+ matchCriteria.push("alert labels match rule criteria");
991
+ }
992
+ if (rule.monitorLabels && rule.monitorLabels.length > 0) {
993
+ matchCriteria.push("monitor labels match rule criteria");
994
+ }
995
+ if (rule.alertTitlePattern) {
996
+ matchCriteria.push(
997
+ `alert title matches pattern \`${rule.alertTitlePattern}\``,
998
+ );
999
+ }
1000
+ if (rule.alertDescriptionPattern) {
1001
+ matchCriteria.push(
1002
+ `alert description matches pattern \`${rule.alertDescriptionPattern}\``,
1003
+ );
1004
+ }
1005
+ if (rule.monitorNamePattern) {
1006
+ matchCriteria.push(
1007
+ `monitor name matches pattern \`${rule.monitorNamePattern}\``,
1008
+ );
1009
+ }
1010
+ if (rule.monitorDescriptionPattern) {
1011
+ matchCriteria.push(
1012
+ `monitor description matches pattern \`${rule.monitorDescriptionPattern}\``,
1013
+ );
1014
+ }
1015
+
1016
+ if (matchCriteria.length === 0) {
1017
+ matchCriteria.push("all criteria matched (rule matches all alerts)");
1018
+ }
1019
+
1020
+ matchReason = `**Match Criteria:**\n- ${matchCriteria.join("\n- ")}`;
1021
+
1022
+ if (wasReopened) {
1023
+ alertFeedMessage = `➕ **Added to Episode #${episodeNumber}** (reopened) by rule **${rule.name || "Unnamed Rule"}**`;
1024
+ episodeFeedMessage = `➕ **Alert #${alertNumber}** added (episode reopened) by rule **${rule.name || "Unnamed Rule"}**`;
1025
+ } else if (isNewEpisode) {
1026
+ alertFeedMessage = `➕ **Added to new Episode #${episodeNumber}** by rule **${rule.name || "Unnamed Rule"}**`;
1027
+ episodeFeedMessage = `➕ **Alert #${alertNumber}** added (initial alert) by rule **${rule.name || "Unnamed Rule"}**`;
1028
+ } else {
1029
+ alertFeedMessage = `➕ **Added to Episode #${episodeNumber}** by rule **${rule.name || "Unnamed Rule"}**`;
1030
+ episodeFeedMessage = `➕ **Alert #${alertNumber}** added by rule **${rule.name || "Unnamed Rule"}**`;
1031
+ }
1032
+ } else {
1033
+ // Manual addition
1034
+ alertFeedMessage = `➕ **Manually added to Episode #${episodeNumber}**`;
1035
+ episodeFeedMessage = `➕ **Alert #${alertNumber}** manually added`;
1036
+ matchReason = "**Reason:** Manually added by user";
1037
+ }
1038
+
1039
+ // Create alert feed entry
1040
+ try {
1041
+ await AlertFeedService.createAlertFeedItem({
1042
+ alertId: alert.id!,
1043
+ projectId: alert.projectId!,
1044
+ alertFeedEventType: AlertFeedEventType.AddedToEpisode,
1045
+ displayColor: Purple500,
1046
+ feedInfoInMarkdown: alertFeedMessage,
1047
+ moreInformationInMarkdown: matchReason,
1048
+ userId: addedByUserId,
1049
+ });
1050
+ } catch (feedError) {
1051
+ logger.error(
1052
+ `Error creating alert feed for alert added to episode: ${feedError}`,
1053
+ );
1054
+ }
1055
+
1056
+ // Create episode feed entry
1057
+ try {
1058
+ let moreInfo: string = `**Alert:** #${alertNumber}`;
1059
+ if (alert.title) {
1060
+ moreInfo += ` - ${alert.title}`;
1061
+ }
1062
+ moreInfo += `\n\n${matchReason}`;
1063
+
1064
+ await AlertEpisodeFeedService.createAlertEpisodeFeedItem({
1065
+ alertEpisodeId: episodeId,
1066
+ projectId: alert.projectId!,
1067
+ alertEpisodeFeedEventType: AlertEpisodeFeedEventType.AlertAdded,
1068
+ displayColor: Purple500,
1069
+ feedInfoInMarkdown: episodeFeedMessage,
1070
+ moreInformationInMarkdown: moreInfo,
1071
+ userId: addedByUserId,
1072
+ });
1073
+ } catch (feedError) {
1074
+ logger.error(`Error creating episode feed for alert added: ${feedError}`);
1075
+ }
1076
+ }
1077
+
1078
+ @CaptureSpan()
1079
+ public async addAlertToEpisodeManually(
1080
+ alert: Alert,
1081
+ episodeId: ObjectID,
1082
+ addedByUserId?: ObjectID,
1083
+ ): Promise<void> {
1084
+ const member: AlertEpisodeMember = new AlertEpisodeMember();
1085
+ member.projectId = alert.projectId!;
1086
+ member.alertEpisodeId = episodeId;
1087
+ member.alertId = alert.id!;
1088
+ member.addedBy = AlertEpisodeMemberAddedBy.Manual;
1089
+
1090
+ if (addedByUserId) {
1091
+ member.addedByUserId = addedByUserId;
1092
+ }
1093
+
1094
+ await AlertEpisodeMemberService.create({
1095
+ data: member,
1096
+ props: {
1097
+ isRoot: true,
1098
+ },
1099
+ });
1100
+
1101
+ // Create feed entries for manual addition
1102
+ await this.createAlertAddedFeedEntries({
1103
+ alert,
1104
+ episodeId,
1105
+ addedBy: AlertEpisodeMemberAddedBy.Manual,
1106
+ addedByUserId,
1107
+ });
1108
+
1109
+ // Update episode severity if needed
1110
+ if (alert.alertSeverityId) {
1111
+ await AlertEpisodeService.updateEpisodeSeverity({
1112
+ episodeId: episodeId,
1113
+ severityId: alert.alertSeverityId,
1114
+ onlyIfHigher: true,
1115
+ });
1116
+ }
1117
+ }
1118
+ }
1119
+
1120
+ export default new AlertGroupingEngineServiceClass();