@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.
- package/Models/DatabaseModels/Alert.ts +76 -0
- package/Models/DatabaseModels/AlertEpisode.ts +1201 -0
- package/Models/DatabaseModels/AlertEpisodeFeed.ts +529 -0
- package/Models/DatabaseModels/AlertEpisodeInternalNote.ts +455 -0
- package/Models/DatabaseModels/AlertEpisodeMember.ts +586 -0
- package/Models/DatabaseModels/AlertEpisodeOwnerTeam.ts +421 -0
- package/Models/DatabaseModels/AlertEpisodeOwnerUser.ts +419 -0
- package/Models/DatabaseModels/AlertEpisodeStateTimeline.ts +523 -0
- package/Models/DatabaseModels/AlertFeed.ts +1 -0
- package/Models/DatabaseModels/AlertGroupingRule.ts +1432 -0
- package/Models/DatabaseModels/Index.ts +18 -0
- package/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.ts +70 -0
- package/Models/DatabaseModels/StatusPageDomain.ts +2 -0
- package/Models/DatabaseModels/WorkspaceNotificationLog.ts +57 -0
- package/Server/API/SlackAPI.ts +21 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1768938069147-MigrationName.ts +751 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1769125561322-MigrationName.ts +41 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1769170578688-MigrationName.ts +29 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1769172358833-MigrationName.ts +177 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1769176450526-MigrationName.ts +71 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1769190495840-MigrationName.ts +35 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1769199303656-MigrationName.ts +29 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1769202898645-MigrationName.ts +29 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1769428619414-MigrationName.ts +35 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1769428821686-MigrationName.ts +47 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +20 -0
- package/Server/Services/AlertEpisodeFeedService.ts +94 -0
- package/Server/Services/AlertEpisodeInternalNoteService.ts +71 -0
- package/Server/Services/AlertEpisodeMemberService.ts +267 -0
- package/Server/Services/AlertEpisodeOwnerTeamService.ts +10 -0
- package/Server/Services/AlertEpisodeOwnerUserService.ts +10 -0
- package/Server/Services/AlertEpisodeService.ts +988 -0
- package/Server/Services/AlertEpisodeStateTimelineService.ts +557 -0
- package/Server/Services/AlertGroupingEngineService.ts +1120 -0
- package/Server/Services/AlertGroupingRuleService.ts +14 -0
- package/Server/Services/AlertService.ts +12 -0
- package/Server/Services/CallService.ts +2 -0
- package/Server/Services/Index.ts +21 -0
- package/Server/Services/MailService.ts +5 -0
- package/Server/Services/OnCallDutyPolicyService.ts +5 -0
- package/Server/Services/SmsService.ts +2 -0
- package/Server/Services/UserNotificationSettingService.ts +23 -0
- package/Server/Services/WhatsAppService.ts +5 -0
- package/Server/Services/WorkspaceNotificationRuleService.ts +26 -0
- package/Server/Utils/AnalyticsDatabase/Statement.ts +6 -2
- package/Server/Utils/WhatsAppTemplateUtil.ts +13 -0
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/ActionTypes.ts +18 -0
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/AlertEpisode.ts +689 -0
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +16 -0
- package/Server/Utils/Workspace/Slack/Actions/ActionTypes.ts +11 -0
- package/Server/Utils/Workspace/Slack/Actions/AlertEpisode.ts +915 -0
- package/Server/Utils/Workspace/Slack/Messages/AlertEpisode.ts +120 -0
- package/Server/Utils/Workspace/WorkspaceMessages/AlertEpisode.ts +74 -0
- package/Tests/Server/Services/AlertEpisodeMemberService.test.ts +200 -0
- package/Tests/Server/Services/AlertEpisodeService.test.ts +240 -0
- package/Tests/Server/Services/AlertGroupingEngineService.test.ts +542 -0
- package/Tests/Server/Services/AlertGroupingRuleService.test.ts +383 -0
- package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +1 -1
- package/Tests/UI/Components/Input.test.tsx +1 -1
- package/Tests/UI/Components/TextArea.test.tsx +2 -2
- package/Types/BaseDatabase/SortOrder.ts +9 -0
- package/Types/Email/EmailTemplateType.ts +5 -0
- package/Types/NotificationRule/NotificationRuleType.ts +1 -0
- package/Types/NotificationSetting/NotificationSettingEventType.ts +7 -0
- package/Types/Permission.ts +309 -0
- package/Types/UserNotification/UserNotificationEventType.ts +1 -0
- package/Types/WhatsApp/WhatsAppTemplates.ts +20 -0
- package/Types/Workspace/NotificationRules/EventType.ts +1 -0
- package/Types/Workspace/NotificationRules/NotificationRuleCondition.ts +32 -3
- package/UI/Components/Accordion/Accordion.tsx +20 -2
- package/UI/Components/Alerts/Alert.tsx +1 -0
- package/UI/Components/Button/Button.tsx +29 -0
- package/UI/Components/CardSelect/CardSelect.tsx +5 -1
- package/UI/Components/Checkbox/Checkbox.tsx +7 -3
- package/UI/Components/ColorCircle/ColorCircle.tsx +2 -0
- package/UI/Components/ColorViewer/ColorViewer.tsx +19 -3
- package/UI/Components/CopyableButton/CopyableButton.tsx +22 -5
- package/UI/Components/Detail/Detail.tsx +1 -1
- package/UI/Components/Dropdown/Dropdown.tsx +14 -1
- package/UI/Components/Forms/Fields/FormField.tsx +28 -0
- package/UI/Components/FullPageModal/FullPageModal.tsx +35 -4
- package/UI/Components/Input/Input.tsx +14 -2
- package/UI/Components/Link/Link.tsx +1 -0
- package/UI/Components/Loader/Loader.tsx +8 -2
- package/UI/Components/Markdown.tsx/MarkdownViewer.tsx +76 -1
- package/UI/Components/Modal/Modal.tsx +47 -3
- package/UI/Components/ModelTable/BaseModelTable.tsx +42 -1
- package/UI/Components/MoreMenu/MoreMenu.tsx +84 -2
- package/UI/Components/OrderedStatesList/OrderedStatesList.tsx +30 -8
- package/UI/Components/Pagination/Pagination.tsx +113 -8
- package/UI/Components/ProgressBar/ProgressBar.tsx +12 -2
- package/UI/Components/Radio/Radio.tsx +21 -3
- package/UI/Components/SideMenu/CountModelSideMenuItem.tsx +54 -27
- package/UI/Components/StatusBubble/StatusBubble.tsx +7 -2
- package/UI/Components/Table/TableHeader.tsx +20 -3
- package/UI/Components/Tabs/Tab.tsx +16 -1
- package/UI/Components/Tabs/Tabs.tsx +12 -1
- package/UI/Components/TextArea/TextArea.tsx +12 -2
- package/UI/Components/Toggle/Toggle.tsx +14 -3
- package/UI/Components/Tooltip/Tooltip.tsx +11 -1
- package/UI/Components/TopAlert/TopAlert.tsx +2 -0
- package/build/dist/Models/DatabaseModels/Alert.js +77 -0
- package/build/dist/Models/DatabaseModels/Alert.js.map +1 -1
- package/build/dist/Models/DatabaseModels/AlertEpisode.js +1225 -0
- package/build/dist/Models/DatabaseModels/AlertEpisode.js.map +1 -0
- package/build/dist/Models/DatabaseModels/AlertEpisodeFeed.js +553 -0
- package/build/dist/Models/DatabaseModels/AlertEpisodeFeed.js.map +1 -0
- package/build/dist/Models/DatabaseModels/AlertEpisodeInternalNote.js +467 -0
- package/build/dist/Models/DatabaseModels/AlertEpisodeInternalNote.js.map +1 -0
- package/build/dist/Models/DatabaseModels/AlertEpisodeMember.js +607 -0
- package/build/dist/Models/DatabaseModels/AlertEpisodeMember.js.map +1 -0
- package/build/dist/Models/DatabaseModels/AlertEpisodeOwnerTeam.js +437 -0
- package/build/dist/Models/DatabaseModels/AlertEpisodeOwnerTeam.js.map +1 -0
- package/build/dist/Models/DatabaseModels/AlertEpisodeOwnerUser.js +436 -0
- package/build/dist/Models/DatabaseModels/AlertEpisodeOwnerUser.js.map +1 -0
- package/build/dist/Models/DatabaseModels/AlertEpisodeStateTimeline.js +546 -0
- package/build/dist/Models/DatabaseModels/AlertEpisodeStateTimeline.js.map +1 -0
- package/build/dist/Models/DatabaseModels/AlertFeed.js +1 -0
- package/build/dist/Models/DatabaseModels/AlertFeed.js.map +1 -1
- package/build/dist/Models/DatabaseModels/AlertGroupingRule.js +1437 -0
- package/build/dist/Models/DatabaseModels/AlertGroupingRule.js.map +1 -0
- package/build/dist/Models/DatabaseModels/Index.js +16 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.js +69 -0
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageDomain.js +2 -0
- package/build/dist/Models/DatabaseModels/StatusPageDomain.js.map +1 -1
- package/build/dist/Models/DatabaseModels/WorkspaceNotificationLog.js +58 -0
- package/build/dist/Models/DatabaseModels/WorkspaceNotificationLog.js.map +1 -1
- package/build/dist/Server/API/SlackAPI.js +18 -0
- package/build/dist/Server/API/SlackAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1768938069147-MigrationName.js +266 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1768938069147-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1769125561322-MigrationName.js +20 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1769125561322-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1769170578688-MigrationName.js +16 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1769170578688-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1769172358833-MigrationName.js +68 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1769172358833-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1769176450526-MigrationName.js +30 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1769176450526-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1769190495840-MigrationName.js +18 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1769190495840-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1769199303656-MigrationName.js +16 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1769199303656-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1769202898645-MigrationName.js +16 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1769202898645-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1769428619414-MigrationName.js +18 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1769428619414-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1769428821686-MigrationName.js +22 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1769428821686-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +20 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/AlertEpisodeFeedService.js +83 -0
- package/build/dist/Server/Services/AlertEpisodeFeedService.js.map +1 -0
- package/build/dist/Server/Services/AlertEpisodeInternalNoteService.js +70 -0
- package/build/dist/Server/Services/AlertEpisodeInternalNoteService.js.map +1 -0
- package/build/dist/Server/Services/AlertEpisodeMemberService.js +256 -0
- package/build/dist/Server/Services/AlertEpisodeMemberService.js.map +1 -0
- package/build/dist/Server/Services/AlertEpisodeOwnerTeamService.js +9 -0
- package/build/dist/Server/Services/AlertEpisodeOwnerTeamService.js.map +1 -0
- package/build/dist/Server/Services/AlertEpisodeOwnerUserService.js +9 -0
- package/build/dist/Server/Services/AlertEpisodeOwnerUserService.js.map +1 -0
- package/build/dist/Server/Services/AlertEpisodeService.js +885 -0
- package/build/dist/Server/Services/AlertEpisodeService.js.map +1 -0
- package/build/dist/Server/Services/AlertEpisodeStateTimelineService.js +494 -0
- package/build/dist/Server/Services/AlertEpisodeStateTimelineService.js.map +1 -0
- package/build/dist/Server/Services/AlertGroupingEngineService.js +893 -0
- package/build/dist/Server/Services/AlertGroupingEngineService.js.map +1 -0
- package/build/dist/Server/Services/AlertGroupingRuleService.js +13 -0
- package/build/dist/Server/Services/AlertGroupingRuleService.js.map +1 -0
- package/build/dist/Server/Services/AlertService.js +11 -0
- package/build/dist/Server/Services/AlertService.js.map +1 -1
- package/build/dist/Server/Services/CallService.js +11 -10
- package/build/dist/Server/Services/CallService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +18 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/MailService.js +3 -0
- package/build/dist/Server/Services/MailService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyService.js +3 -0
- package/build/dist/Server/Services/OnCallDutyPolicyService.js.map +1 -1
- package/build/dist/Server/Services/SmsService.js +11 -10
- package/build/dist/Server/Services/SmsService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationSettingService.js +9 -0
- package/build/dist/Server/Services/UserNotificationSettingService.js.map +1 -1
- package/build/dist/Server/Services/WhatsAppService.js +3 -0
- package/build/dist/Server/Services/WhatsAppService.js.map +1 -1
- package/build/dist/Server/Services/WorkspaceNotificationRuleService.js +25 -0
- package/build/dist/Server/Services/WorkspaceNotificationRuleService.js.map +1 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js +4 -2
- package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js.map +1 -1
- package/build/dist/Server/Utils/WhatsAppTemplateUtil.js +8 -0
- package/build/dist/Server/Utils/WhatsAppTemplateUtil.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/ActionTypes.js +17 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/ActionTypes.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/AlertEpisode.js +545 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/AlertEpisode.js.map +1 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +13 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/Slack/Actions/ActionTypes.js +10 -0
- package/build/dist/Server/Utils/Workspace/Slack/Actions/ActionTypes.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/Slack/Actions/AlertEpisode.js +651 -0
- package/build/dist/Server/Utils/Workspace/Slack/Actions/AlertEpisode.js.map +1 -0
- package/build/dist/Server/Utils/Workspace/Slack/Messages/AlertEpisode.js +100 -0
- package/build/dist/Server/Utils/Workspace/Slack/Messages/AlertEpisode.js.map +1 -0
- package/build/dist/Server/Utils/Workspace/WorkspaceMessages/AlertEpisode.js +70 -0
- package/build/dist/Server/Utils/Workspace/WorkspaceMessages/AlertEpisode.js.map +1 -0
- package/build/dist/Tests/Server/Services/AlertEpisodeMemberService.test.js +165 -0
- package/build/dist/Tests/Server/Services/AlertEpisodeMemberService.test.js.map +1 -0
- package/build/dist/Tests/Server/Services/AlertEpisodeService.test.js +193 -0
- package/build/dist/Tests/Server/Services/AlertEpisodeService.test.js.map +1 -0
- package/build/dist/Tests/Server/Services/AlertGroupingEngineService.test.js +412 -0
- package/build/dist/Tests/Server/Services/AlertGroupingEngineService.test.js.map +1 -0
- package/build/dist/Tests/Server/Services/AlertGroupingRuleService.test.js +308 -0
- package/build/dist/Tests/Server/Services/AlertGroupingRuleService.test.js.map +1 -0
- package/build/dist/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.js +1 -1
- package/build/dist/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.js.map +1 -1
- package/build/dist/Tests/UI/Components/Input.test.js +1 -1
- package/build/dist/Tests/UI/Components/Input.test.js.map +1 -1
- package/build/dist/Tests/UI/Components/TextArea.test.js +2 -2
- package/build/dist/Tests/UI/Components/TextArea.test.js.map +1 -1
- package/build/dist/Types/BaseDatabase/SortOrder.js +5 -0
- package/build/dist/Types/BaseDatabase/SortOrder.js.map +1 -1
- package/build/dist/Types/Email/EmailTemplateType.js +4 -0
- package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
- package/build/dist/Types/NotificationRule/NotificationRuleType.js +1 -0
- package/build/dist/Types/NotificationRule/NotificationRuleType.js.map +1 -1
- package/build/dist/Types/NotificationSetting/NotificationSettingEventType.js +5 -0
- package/build/dist/Types/NotificationSetting/NotificationSettingEventType.js.map +1 -1
- package/build/dist/Types/Permission.js +264 -0
- package/build/dist/Types/Permission.js.map +1 -1
- package/build/dist/Types/UserNotification/UserNotificationEventType.js +1 -0
- package/build/dist/Types/UserNotification/UserNotificationEventType.js.map +1 -1
- package/build/dist/Types/WhatsApp/WhatsAppTemplates.js +12 -0
- package/build/dist/Types/WhatsApp/WhatsAppTemplates.js.map +1 -1
- package/build/dist/Types/Workspace/NotificationRules/EventType.js +1 -0
- package/build/dist/Types/Workspace/NotificationRules/EventType.js.map +1 -1
- package/build/dist/Types/Workspace/NotificationRules/NotificationRuleCondition.js +28 -3
- package/build/dist/Types/Workspace/NotificationRules/NotificationRuleCondition.js.map +1 -1
- package/build/dist/UI/Components/Accordion/Accordion.js +10 -3
- package/build/dist/UI/Components/Accordion/Accordion.js.map +1 -1
- package/build/dist/UI/Components/Alerts/Alert.js +1 -1
- package/build/dist/UI/Components/Alerts/Alert.js.map +1 -1
- package/build/dist/UI/Components/Button/Button.js +8 -2
- package/build/dist/UI/Components/Button/Button.js.map +1 -1
- package/build/dist/UI/Components/CardSelect/CardSelect.js +1 -1
- package/build/dist/UI/Components/CardSelect/CardSelect.js.map +1 -1
- package/build/dist/UI/Components/Checkbox/Checkbox.js +2 -2
- package/build/dist/UI/Components/Checkbox/Checkbox.js.map +1 -1
- package/build/dist/UI/Components/ColorCircle/ColorCircle.js +1 -1
- package/build/dist/UI/Components/ColorCircle/ColorCircle.js.map +1 -1
- package/build/dist/UI/Components/ColorViewer/ColorViewer.js +12 -3
- package/build/dist/UI/Components/ColorViewer/ColorViewer.js.map +1 -1
- package/build/dist/UI/Components/CopyableButton/CopyableButton.js +12 -5
- package/build/dist/UI/Components/CopyableButton/CopyableButton.js.map +1 -1
- package/build/dist/UI/Components/Detail/Detail.js +1 -1
- package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
- package/build/dist/UI/Components/Dropdown/Dropdown.js +5 -3
- package/build/dist/UI/Components/Dropdown/Dropdown.js.map +1 -1
- package/build/dist/UI/Components/Forms/Fields/FormField.js +19 -1
- package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
- package/build/dist/UI/Components/FullPageModal/FullPageModal.js +24 -5
- package/build/dist/UI/Components/FullPageModal/FullPageModal.js.map +1 -1
- package/build/dist/UI/Components/Input/Input.js +3 -3
- package/build/dist/UI/Components/Input/Input.js.map +1 -1
- package/build/dist/UI/Components/Link/Link.js +1 -1
- package/build/dist/UI/Components/Link/Link.js.map +1 -1
- package/build/dist/UI/Components/Loader/Loader.js +6 -4
- package/build/dist/UI/Components/Loader/Loader.js.map +1 -1
- package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js +56 -3
- package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js.map +1 -1
- package/build/dist/UI/Components/Modal/Modal.js +28 -3
- package/build/dist/UI/Components/Modal/Modal.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +23 -1
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/build/dist/UI/Components/MoreMenu/MoreMenu.js +67 -6
- package/build/dist/UI/Components/MoreMenu/MoreMenu.js.map +1 -1
- package/build/dist/UI/Components/OrderedStatesList/OrderedStatesList.js +14 -3
- package/build/dist/UI/Components/OrderedStatesList/OrderedStatesList.js.map +1 -1
- package/build/dist/UI/Components/Pagination/Pagination.js +69 -13
- package/build/dist/UI/Components/Pagination/Pagination.js.map +1 -1
- package/build/dist/UI/Components/ProgressBar/ProgressBar.js +2 -2
- package/build/dist/UI/Components/ProgressBar/ProgressBar.js.map +1 -1
- package/build/dist/UI/Components/Radio/Radio.js +8 -5
- package/build/dist/UI/Components/Radio/Radio.js.map +1 -1
- package/build/dist/UI/Components/SideMenu/CountModelSideMenuItem.js +23 -4
- package/build/dist/UI/Components/SideMenu/CountModelSideMenuItem.js.map +1 -1
- package/build/dist/UI/Components/StatusBubble/StatusBubble.js +2 -2
- package/build/dist/UI/Components/StatusBubble/StatusBubble.js.map +1 -1
- package/build/dist/UI/Components/Table/TableHeader.js +12 -4
- package/build/dist/UI/Components/Table/TableHeader.js.map +1 -1
- package/build/dist/UI/Components/Tabs/Tab.js +8 -1
- package/build/dist/UI/Components/Tabs/Tab.js.map +1 -1
- package/build/dist/UI/Components/Tabs/Tabs.js +4 -3
- package/build/dist/UI/Components/Tabs/Tabs.js.map +1 -1
- package/build/dist/UI/Components/TextArea/TextArea.js +3 -3
- package/build/dist/UI/Components/TextArea/TextArea.js.map +1 -1
- package/build/dist/UI/Components/Toggle/Toggle.js +7 -4
- package/build/dist/UI/Components/Toggle/Toggle.js.map +1 -1
- package/build/dist/UI/Components/Tooltip/Tooltip.js +4 -1
- package/build/dist/UI/Components/Tooltip/Tooltip.js.map +1 -1
- package/build/dist/UI/Components/TopAlert/TopAlert.js +1 -1
- package/build/dist/UI/Components/TopAlert/TopAlert.js.map +1 -1
- 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();
|