@oneuptime/common 12.0.8 → 12.0.9
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/AnalyticsModels/MetricItemAggMV1mByK8sCluster.ts +1 -1
- package/Models/AnalyticsModels/MetricItemAggMV1mByService.ts +1 -1
- package/Models/DatabaseModels/AIConversation.ts +32 -0
- package/Models/DatabaseModels/AIConversationMessage.ts +41 -0
- package/Models/DatabaseModels/AlertEpisodeMember.ts +27 -0
- package/Models/DatabaseModels/IncidentEpisodeMember.ts +28 -0
- package/Models/DatabaseModels/Index.ts +12 -4
- package/Models/DatabaseModels/{TelemetryEntity.ts → InventoryItem.ts} +139 -9
- package/Models/DatabaseModels/InventoryItemCustomField.ts +434 -0
- package/Models/DatabaseModels/{TelemetryEntityRelationship.ts → InventoryItemRelationship.ts} +8 -8
- package/Models/DatabaseModels/NetworkDevice.ts +141 -0
- package/Models/DatabaseModels/NetworkDeviceLink.ts +699 -0
- package/Models/DatabaseModels/NetworkDeviceLinkRule.ts +467 -0
- package/Models/DatabaseModels/NetworkTopologySuppression.ts +429 -0
- package/Models/DatabaseModels/OnCallDutyPolicyFeed.ts +9 -0
- package/Models/DatabaseModels/Project.ts +78 -0
- package/Models/DatabaseModels/UserCall.ts +42 -0
- package/Models/DatabaseModels/UserEmail.ts +44 -0
- package/Models/DatabaseModels/UserNotificationRule.ts +425 -55
- package/Models/DatabaseModels/UserOnCallLogTimeline.ts +24 -2
- package/Models/DatabaseModels/UserPush.ts +49 -0
- package/Models/DatabaseModels/UserSMS.ts +43 -0
- package/Models/DatabaseModels/UserTelegram.ts +59 -0
- package/Models/DatabaseModels/UserWebhook.ts +52 -0
- package/Models/DatabaseModels/UserWhatsApp.ts +41 -0
- package/Models/DatabaseModels/WorkflowVariable.ts +12 -0
- package/Server/API/AIChatAPI.ts +315 -1
- package/Server/API/DashboardAPI.ts +217 -1
- package/Server/API/OnCallReadinessAPI.ts +841 -0
- package/Server/API/TeamComplianceAPI.ts +69 -17
- package/Server/API/TelemetryAPI.ts +220 -12
- package/Server/EnvironmentConfig.ts +52 -0
- package/Server/Infrastructure/Postgres/DataSourceOptions.ts +22 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1786100000000-RestoreServiceLowerNameIndex.ts +4 -4
- package/Server/Infrastructure/Postgres/SchemaMigrations/1786200000000-RestoreDroppedUniqueIndexes.ts +5 -5
- package/Server/Infrastructure/Postgres/SchemaMigrations/1786625176831-AddMonitoringMethodToNetworkDevice.ts +35 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1786634985763-AddNetworkDeviceLink.ts +82 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1786639512056-AddNetworkDeviceLinkRule.ts +91 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1786639972982-AddNetworkTopologySuppression.ts +47 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1786800000000-RenameTelemetryEntityToInventoryItem.ts +255 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1786900000000-AddInventoryItemArchiveAndCustomFields.ts +107 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1787000000000-AddOnCallNotificationFallbackColumns.ts +90 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1787100000000-AddAIConversationPageContext.ts +39 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1787200000000-AddAIChatMessageFeedback.ts +33 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1787300000000-AddEpisodeMemberNotifyIndexes.ts +59 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +20 -0
- package/Server/Infrastructure/Queue.ts +78 -13
- package/Server/Middleware/PublicDashboardRateLimit.ts +593 -0
- package/Server/Services/AIService.ts +7 -0
- package/Server/Services/AlertEpisodeStateTimelineService.ts +29 -0
- package/Server/Services/AlertSeverityService.ts +63 -0
- package/Server/Services/DashboardService.ts +9 -10
- package/Server/Services/DatabaseService.ts +32 -2
- package/Server/Services/IncidentEpisodeStateTimelineService.ts +29 -0
- package/Server/Services/IncidentSeverityService.ts +76 -0
- package/Server/Services/Index.ts +12 -4
- package/Server/Services/InventoryItemCustomFieldService.ts +9 -0
- package/Server/Services/{TelemetryEntityRelationshipService.ts → InventoryItemRelationshipService.ts} +4 -4
- package/Server/Services/{TelemetryEntityService.ts → InventoryItemService.ts} +89 -20
- package/Server/Services/LogAggregationService.ts +45 -8
- package/Server/Services/MetricAggregationService.ts +121 -0
- package/Server/Services/MetricService.ts +7 -7
- package/Server/Services/NetworkDeviceLinkRuleService.ts +10 -0
- package/Server/Services/NetworkDeviceLinkService.ts +84 -0
- package/Server/Services/NetworkDeviceService.ts +140 -0
- package/Server/Services/NetworkSiteService.ts +77 -25
- package/Server/Services/NetworkTopologySuppressionService.ts +84 -0
- package/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.ts +41 -29
- package/Server/Services/OnCallDutyPolicyExecutionLogService.ts +8 -0
- package/Server/Services/OnCallDutyPolicyExecutionLogTimelineService.ts +62 -13
- package/Server/Services/OnCallDutyPolicyScheduleService.ts +61 -1
- package/Server/Services/OnCallNotificationAlertingService.ts +742 -0
- package/Server/Services/OnCallReadinessService.ts +2803 -0
- package/Server/Services/OnCallSetupReminderService.ts +955 -0
- package/Server/Services/ProfileAggregationService.ts +123 -0
- package/Server/Services/StatusPageService.ts +9 -10
- package/Server/Services/TeamComplianceService.ts +429 -252
- package/Server/Services/UserCallService.ts +26 -1
- package/Server/Services/UserEmailService.ts +26 -1
- package/Server/Services/UserNotificationRuleAdminService.ts +1183 -0
- package/Server/Services/UserNotificationRuleService.ts +3812 -333
- package/Server/Services/UserOnCallLogService.ts +561 -48
- package/Server/Services/UserPushService.ts +29 -0
- package/Server/Services/UserService.ts +11 -0
- package/Server/Services/UserSmsService.ts +26 -1
- package/Server/Services/UserTelegramService.ts +24 -1
- package/Server/Services/UserWebhookService.ts +28 -1
- package/Server/Services/UserWhatsAppService.ts +24 -1
- package/Server/Types/Database/Permissions/BasePermission.ts +19 -0
- package/Server/Types/Database/Permissions/CreatePermission.ts +164 -0
- package/Server/Types/Database/Permissions/OwnerOnlyColumnPermission.ts +340 -0
- package/Server/Types/Database/Permissions/QueryPermission.ts +48 -0
- package/Server/Types/Database/Permissions/TenantPermission.ts +8 -1
- package/Server/Types/Workflow/Components/API/Delete.ts +1 -1
- package/Server/Types/Workflow/Components/API/Get.ts +1 -1
- package/Server/Types/Workflow/Components/API/Patch.ts +1 -1
- package/Server/Types/Workflow/Components/API/Post.ts +1 -1
- package/Server/Types/Workflow/Components/API/Put.ts +1 -1
- package/Server/Types/Workflow/Components/BaseModel/CreateManyBaseModel.ts +29 -5
- package/Server/Types/Workflow/Components/BaseModel/CreateOneBaseModel.ts +18 -10
- package/Server/Types/Workflow/Components/BaseModel/ModelArguments.ts +55 -0
- package/Server/Types/Workflow/Components/Conditions/IfElse.ts +3 -17
- package/Server/Types/Workflow/Components/Email.ts +25 -7
- package/Server/Types/Workflow/Components/JavaScript.ts +10 -3
- package/Server/Types/Workflow/Components/MicrosoftTeams/SendMessageToChannel.ts +1 -1
- package/Server/Utils/AI/Chat/ChatAgentRunner.ts +643 -48
- package/Server/Utils/AI/Chat/ObservabilityAssistant.ts +32 -3
- package/Server/Utils/AI/Chat/ObservabilityChatPrompt.ts +20 -6
- package/Server/Utils/AI/SRE/AIInvestigationEngine.ts +7 -0
- package/Server/Utils/AI/Toolbox/AIActionTools.ts +2 -2
- package/Server/Utils/AI/Toolbox/AIMetaTools.ts +863 -0
- package/Server/Utils/AI/Toolbox/AlertTools.ts +177 -15
- package/Server/Utils/AI/Toolbox/IncidentTools.ts +191 -10
- package/Server/Utils/AI/Toolbox/Index.ts +48 -0
- package/Server/Utils/AI/Toolbox/MonitorTools.ts +298 -11
- package/Server/Utils/AI/Toolbox/NoteWriteTools.ts +295 -0
- package/Server/Utils/AI/Toolbox/OnCallTools.ts +1246 -0
- package/Server/Utils/AI/Toolbox/RunbookTools.ts +424 -0
- package/Server/Utils/AI/Toolbox/SloTools.ts +456 -0
- package/Server/Utils/AI/Toolbox/StatusPageTools.ts +559 -0
- package/Server/Utils/AI/Toolbox/TeamTools.ts +327 -0
- package/Server/Utils/AI/Toolbox/TimelineTools.ts +615 -0
- package/Server/Utils/AI/Toolbox/WorkflowProbeTools.ts +664 -0
- package/Server/Utils/ClientIp.ts +221 -0
- package/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.ts +47 -0
- package/Server/Utils/Dashboard/PublicDashboardSloHistoryPolicy.ts +163 -0
- package/Server/Utils/Dashboard/PublicDashboardSloWidget.ts +147 -0
- package/Server/Utils/Express.ts +12 -17
- package/Server/Utils/LLM/LLMService.ts +85 -8
- package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +204 -10
- package/Server/Utils/SSRFProtection.ts +98 -23
- package/Server/Utils/StartServer.ts +12 -3
- package/Server/Utils/Telemetry/EntityRegistry.ts +205 -18
- package/Server/Utils/Telemetry/InventoryEntityRegistry.ts +25 -25
- package/Server/Utils/Telemetry/TelemetryEntity.ts +160 -52
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +7 -3
- package/Tests/App/Dashboard/AdminNotificationRulesPage.test.tsx +2146 -0
- package/Tests/App/Dashboard/CreateWorkflowModal.test.tsx +561 -0
- package/Tests/App/Dashboard/EscalationRuleReadiness.test.tsx +2470 -0
- package/Tests/App/Dashboard/OnCallPreventionGuards.test.tsx +1897 -0
- package/Tests/App/Dashboard/OnCallReadinessSurfaces.test.tsx +3606 -0
- package/Tests/App/Dashboard/OnCallRulesDeleteGuard.test.tsx +784 -0
- package/Tests/App/Dashboard/OnCallRulesTable.test.tsx +1119 -0
- package/Tests/App/Dashboard/SloWidgetFetching.test.tsx +531 -0
- package/Tests/App/Dashboard/UserSettingsSetupChecklistModel.test.ts +1312 -0
- package/Tests/App/Dashboard/UserSettingsSetupChecklistPage.test.tsx +1390 -0
- package/Tests/Models/InventoryItemModel.test.ts +174 -0
- package/Tests/Models/InventoryItemNaming.test.ts +302 -0
- package/Tests/Server/API/AIChatCancelAndFeedback.test.ts +437 -0
- package/Tests/Server/API/DashboardPublicRateLimit.test.ts +659 -0
- package/Tests/Server/API/DashboardPublicResourceListAPI.test.ts +18 -0
- package/Tests/Server/API/DashboardPublicSloAPI.test.ts +880 -0
- package/Tests/Server/API/Helpers.ts +24 -15
- package/Tests/Server/API/OnCallReadinessAPI.test.ts +2680 -0
- package/Tests/Server/API/OnCallSetupReminderAPI.test.ts +915 -0
- package/Tests/Server/Infrastructure/Postgres/EpisodeMemberNotifyIndexesMigration.test.ts +533 -0
- package/Tests/Server/Infrastructure/Postgres/InventoryItemArchiveMigration.test.ts +213 -0
- package/Tests/Server/Infrastructure/Postgres/RenameInventoryItemMigration.test.ts +432 -0
- package/Tests/Server/Infrastructure/Queue.test.ts +293 -0
- package/Tests/Server/Middleware/PublicDashboardRateLimit.test.ts +1645 -0
- package/Tests/Server/Services/AdminRuleEditGuards.test.ts +2848 -0
- package/Tests/Server/Services/DeliverNotificationForRuleExtraction.test.ts +1393 -0
- package/Tests/Server/Services/EpisodeRuleSeverityRepair.test.ts +1802 -0
- package/Tests/Server/Services/EpisodeStateTimelineNote.test.ts +304 -0
- package/Tests/Server/Services/InventoryItemDisplayName.test.ts +339 -0
- package/Tests/Server/Services/{TelemetryEntityManualCreate.test.ts → InventoryItemManualCreate.test.ts} +27 -27
- package/Tests/Server/Services/IpAllowlistSpoofing.test.ts +450 -0
- package/Tests/Server/Services/LogAggregationService.test.ts +235 -1
- package/Tests/Server/Services/MetricAggregationService.test.ts +231 -0
- package/Tests/Server/Services/MetricEntityMVKeyParity.test.ts +80 -29
- package/Tests/Server/Services/MetricServiceAggregate.test.ts +30 -30
- package/Tests/Server/Services/NetworkSiteService.test.ts +18 -3
- package/Tests/Server/Services/NotificationChannelEventCoverage.test.ts +1728 -0
- package/Tests/Server/Services/NotificationDeletionImpact.test.ts +2402 -0
- package/Tests/Server/Services/OnCallDutyPolicyExecutionLogTimelineGapFeed.test.ts +394 -0
- package/Tests/Server/Services/OnCallNotificationFallback.test.ts +1744 -0
- package/Tests/Server/Services/OnCallReadinessService.test.ts +4295 -0
- package/Tests/Server/Services/OnCallSetupReminder.test.ts +1272 -0
- package/Tests/Server/Services/OnCallWeeklyReadinessDigest.test.ts +1021 -0
- package/Tests/Server/Services/ProfileAggregationService.test.ts +296 -0
- package/Tests/Server/Services/SeverityCreationRuleBackfill.test.ts +1536 -0
- package/Tests/Server/Services/SeverityRuleBackfill.test.ts +1818 -0
- package/Tests/Server/Services/TeamComplianceServiceBehaviour.test.ts +1845 -0
- package/Tests/Server/Services/UserNotificationRuleAdminGuards.test.ts +1394 -0
- package/Tests/Server/Services/UserNotificationRuleDefaultCreation.test.ts +1166 -0
- package/Tests/Server/Services/UserNotificationRuleExecuteItem.test.ts +1468 -0
- package/Tests/Server/Services/UserOnCallLogNoNotificationRules.test.ts +1457 -0
- package/Tests/Server/Types/Database/Permissions/AdminNotificationRuleAccess.test.ts +1546 -0
- package/Tests/Server/Types/Database/Permissions/CreateOwnershipScoping.test.ts +529 -0
- package/Tests/Server/Types/Database/Permissions/OwnerOnlyColumns.test.ts +1219 -0
- package/Tests/Server/Types/Database/Permissions/UserNotificationRuleScoping.test.ts +1089 -0
- package/Tests/Server/Types/Workflow/Components/ApiComponentErrorPort.test.ts +2 -1
- package/Tests/Server/Types/Workflow/Components/BaseModelDatabaseComponents.test.ts +190 -0
- package/Tests/Server/Types/Workflow/Components/ChatWebhookComponents.test.ts +44 -14
- package/Tests/Server/Types/Workflow/Components/Email.test.ts +151 -0
- package/Tests/Server/Types/Workflow/Components/IfElse.test.ts +98 -0
- package/Tests/Server/Types/Workflow/Components/JavaScript.test.ts +51 -0
- package/Tests/Server/Utils/AI/AIMetaTools.test.ts +586 -0
- package/Tests/Server/Utils/AI/AlertMonitorFilters.test.ts +582 -0
- package/Tests/Server/Utils/AI/ChatAgentRunner.test.ts +726 -0
- package/Tests/Server/Utils/AI/IncidentToolsFilters.test.ts +315 -0
- package/Tests/Server/Utils/AI/LLMServiceStopReason.test.ts +314 -0
- package/Tests/Server/Utils/AI/LLMServiceToolCalling.test.ts +26 -3
- package/Tests/Server/Utils/AI/NoteWriteTools.test.ts +268 -0
- package/Tests/Server/Utils/AI/ObservabilityChatPrompt.test.ts +169 -0
- package/Tests/Server/Utils/AI/OnCallTools.test.ts +664 -0
- package/Tests/Server/Utils/AI/RunbookTools.test.ts +325 -0
- package/Tests/Server/Utils/AI/SloTools.test.ts +306 -0
- package/Tests/Server/Utils/AI/StatusPageTools.test.ts +391 -0
- package/Tests/Server/Utils/AI/TeamTools.test.ts +257 -0
- package/Tests/Server/Utils/AI/TimelineTools.test.ts +472 -0
- package/Tests/Server/Utils/AI/WorkflowProbeTools.test.ts +428 -0
- package/Tests/Server/Utils/ClientIp.test.ts +438 -0
- package/Tests/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.test.ts +171 -0
- package/Tests/Server/Utils/Dashboard/PublicDashboardSloHistoryPolicy.test.ts +383 -0
- package/Tests/Server/Utils/EntityRegistryRowFence.test.ts +23 -25
- package/Tests/Server/Utils/MicrosoftTeamsWebhookUrlValidation.test.ts +6 -0
- package/Tests/Server/Utils/Monitor/Criteria/DnssecMonitorCriteria.test.ts +307 -0
- package/Tests/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.test.ts +468 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorTelemetryDeepLinks.test.ts +460 -0
- package/Tests/Server/Utils/ResponseRateLimitStatusCodes.test.ts +137 -0
- package/Tests/Server/Utils/SSRFProtectionBypasses.test.ts +40 -8
- package/Tests/Server/Utils/SSRFProtectionUserInfo.test.ts +351 -0
- package/Tests/Server/Utils/Telemetry/EntityRegistryRetirement.test.ts +531 -0
- package/Tests/Server/Utils/Telemetry/InventoryEntityRegistry.test.ts +15 -15
- package/Tests/Server/Utils/Telemetry/TelemetryEntity.test.ts +356 -39
- package/Tests/Types/IP/IP.test.ts +263 -0
- package/Tests/Types/IP/IPWhitelist.test.ts +197 -0
- package/Tests/Types/IP/IPv6.test.ts +12 -1
- package/Tests/Types/Monitor/SnmpOid.test.ts +64 -0
- package/Tests/Types/NetworkDevice/NetworkDeviceMonitoringMethod.test.ts +110 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilMergeAudit.test.ts +106 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilMergeDifferential.test.ts +511 -0
- package/Tests/Types/OnCallDutyPolicy/ScheduleCoverageEndToEnd.test.ts +489 -0
- package/Tests/Types/OnCallDutyPolicy/ScheduleCoverageGapTolerance.test.ts +479 -0
- package/Tests/Types/OnCallDutyPolicy/ScheduleCoverageState.test.ts +822 -0
- package/Tests/Types/Telemetry/EntityTypeGroups.test.ts +6 -6
- package/Tests/Types/Workflow/BaseModelComponents.test.ts +429 -0
- package/Tests/Types/Workflow/Components/BaseModel.test.ts +225 -0
- package/Tests/Types/Workflow/IntegrationCredentialMetadata.test.ts +100 -0
- package/Tests/Types/Workflow/TemplateSyntax.test.ts +112 -0
- package/Tests/Types/Workflow/Templates.test.ts +916 -101
- package/Tests/UI/Components/ActiveFilterChipsOpenRoute.test.tsx +82 -0
- package/Tests/UI/Components/ComponentsModal.test.tsx +564 -7
- package/Tests/UI/Components/KeyboardShortcut.test.tsx +95 -0
- package/Tests/UI/Components/LogDetailsPanelCrossSignal.test.tsx +448 -0
- package/Tests/UI/Components/LogsTableCrossLinks.test.tsx +263 -0
- package/Tests/UI/Components/PendingProjectInvitations.test.tsx +913 -0
- package/Tests/UI/Components/SimpleLogViewer.test.tsx +228 -0
- package/Tests/UI/Components/TableRowSelectability.test.tsx +312 -0
- package/Tests/UI/Components/Workflow/GraphLint.test.ts +72 -0
- package/Tests/UI/Components/Workflow/GraphLintSummary.test.ts +755 -0
- package/Tests/UI/Components/Workflow/ModelColumnEditor.test.ts +242 -7
- package/Tests/UI/Components/Workflow/ModelColumnEditorServerContract.test.ts +15 -7
- package/Tests/UI/Components/Workflow/ModelSchema.test.ts +242 -28
- package/Tests/UI/Components/Workflow/RunStatusWatcher.test.ts +59 -1
- package/Tests/UI/Components/Workflow/StepTraceViewer.test.tsx +256 -0
- package/Tests/UI/Components/Workflow/UseRunWatch.test.tsx +665 -0
- package/Tests/UI/Components/Workflow/WorkflowIssuesModal.test.tsx +485 -0
- package/Tests/UI/Components/Workflow/WorkflowLogModal.test.tsx +478 -0
- package/Tests/UI/Components/Workflow/WorkflowStatusBar.test.tsx +379 -0
- package/Tests/UI/EsbuildConfig.test.ts +607 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +5 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +11 -4
- package/Tests/UI/Utils/ModelAPICreateMiscData.test.ts +94 -0
- package/Tests/UI/Utils/Platform.test.ts +147 -0
- package/Tests/UI/Utils/ProjectInvitationDisplay.test.ts +357 -0
- package/Tests/Utils/Monitor/NetworkDeviceLinkRuleUtil.test.ts +198 -0
- package/Tests/Utils/Monitor/NetworkTopologyUtil.test.ts +487 -0
- package/Tests/Utils/Telemetry/CrossSignalScope.test.ts +698 -0
- package/Tests/__mocks__/bullmq.js +55 -0
- package/Types/AI/AIChatMessageStatus.ts +8 -1
- package/Types/AI/AIChatTypes.ts +12 -0
- package/Types/Database/AccessControl/OwnerOnlyColumn.ts +88 -0
- package/Types/Exception/ExceptionCode.ts +2 -0
- package/Types/Exception/ServiceUnavailableException.ts +8 -0
- package/Types/Exception/TooManyRequestsException.ts +8 -0
- package/Types/IP/IP.ts +93 -47
- package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +49 -3
- package/Types/NetworkDevice/NetworkDeviceMonitoringMethod.ts +55 -0
- package/Types/OnCallDutyPolicy/Layer.ts +203 -149
- package/Types/OnCallDutyPolicy/OnCallDutyPolicyStatus.ts +13 -0
- package/Types/OnCallDutyPolicy/ScheduleShiftUtil.ts +155 -10
- package/Types/Permission.ts +193 -0
- package/Types/Telemetry/EntityRelationshipType.ts +1 -1
- package/Types/Telemetry/EntitySource.ts +1 -1
- package/Types/Telemetry/EntityType.ts +1 -1
- package/Types/Telemetry/EntityTypeGroups.ts +1 -1
- package/Types/Workflow/Components/BaseModel.ts +75 -29
- package/Types/Workflow/Components/Discord.ts +1 -0
- package/Types/Workflow/Components/Email.ts +12 -3
- package/Types/Workflow/Components/JavaScript.ts +7 -0
- package/Types/Workflow/Components/MicrosoftTeams.ts +3 -2
- package/Types/Workflow/Components/Slack.ts +1 -0
- package/Types/Workflow/Components/Telegram.ts +1 -0
- package/Types/Workflow/TemplateSyntax.ts +44 -0
- package/Types/Workflow/Templates.ts +2097 -45
- package/UI/Components/Calendar/Calendar.css +43 -0
- package/UI/Components/Calendar/Calendar.tsx +8 -0
- package/UI/Components/Card/Card.tsx +2 -2
- package/UI/Components/Checkbox/Checkbox.tsx +16 -0
- package/UI/Components/Dictionary/Dictionary.tsx +48 -9
- package/UI/Components/FormModal/BasicFormModal.tsx +2 -1
- package/UI/Components/Header/HeaderIconDropdownButton.tsx +53 -3
- package/UI/Components/Input/Input.tsx +1 -0
- package/UI/Components/KeyboardShortcut/KeyboardKey.ts +185 -0
- package/UI/Components/KeyboardShortcut/KeyboardShortcut.tsx +87 -0
- package/UI/Components/LogsViewer/LogsViewer.tsx +28 -0
- package/UI/Components/LogsViewer/components/ActiveFilterChips.tsx +31 -0
- package/UI/Components/LogsViewer/components/KeyboardShortcutsHelp.tsx +18 -18
- package/UI/Components/LogsViewer/components/LogDetailsPanel.tsx +363 -14
- package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +11 -0
- package/UI/Components/LogsViewer/components/LogsTable.tsx +155 -12
- package/UI/Components/LogsViewer/components/LogsViewerToolbar.tsx +29 -0
- package/UI/Components/LogsViewer/types.ts +23 -0
- package/UI/Components/Markdown.tsx/MarkdownEditor.tsx +9 -2
- package/UI/Components/Navbar/NavBarMenuModal.tsx +15 -30
- package/UI/Components/ProjectInvitations/PendingProjectInvitations.tsx +442 -0
- package/UI/Components/SimpleLogViewer/SimpleLogViewer.tsx +23 -1
- package/UI/Components/Table/Table.tsx +49 -16
- package/UI/Components/Table/TableBody.tsx +53 -28
- package/UI/Components/Table/TableHeader.tsx +13 -0
- package/UI/Components/Table/TableRow.tsx +58 -26
- package/UI/Components/Workflow/ArgumentsForm.tsx +155 -9
- package/UI/Components/Workflow/ComponentReturnValueViewer.tsx +26 -0
- package/UI/Components/Workflow/ComponentSettingsModal.tsx +10 -1
- package/UI/Components/Workflow/ComponentValuePickerModal.tsx +135 -7
- package/UI/Components/Workflow/ComponentsModal.tsx +116 -22
- package/UI/Components/Workflow/DocumentationViewer.tsx +59 -9
- package/UI/Components/Workflow/GraphLint.ts +42 -5
- package/UI/Components/Workflow/GraphLintSummary.ts +390 -0
- package/UI/Components/Workflow/ModelColumnEditor.tsx +154 -22
- package/UI/Components/Workflow/ModelSchema.ts +115 -33
- package/UI/Components/Workflow/RunStatusWatcher.ts +1 -1
- package/UI/Components/Workflow/StepTraceViewer.tsx +1 -1
- package/UI/Components/Workflow/UseRunWatch.ts +212 -0
- package/UI/Components/Workflow/VariableModal.tsx +6 -2
- package/UI/Components/Workflow/Workflow.tsx +49 -3
- package/UI/Components/Workflow/WorkflowIssuesModal.tsx +255 -0
- package/UI/Components/Workflow/WorkflowLogModal.tsx +128 -0
- package/UI/Components/Workflow/WorkflowStatusBar.tsx +224 -0
- package/UI/Utils/AIChatExport/ConversationMarkdown.ts +10 -0
- package/UI/Utils/ModelAPI/ModelAPI.ts +7 -1
- package/UI/Utils/Platform.ts +149 -0
- package/UI/Utils/ProjectInvitationDisplay.ts +118 -0
- package/UI/esbuild-config.js +22 -1
- package/Utils/Monitor/NetworkDeviceLinkRuleUtil.ts +187 -0
- package/Utils/Monitor/NetworkTopologyUtil.ts +888 -164
- package/Utils/Telemetry/CrossSignalScope.ts +502 -0
- package/Utils/Telemetry/EntityKey.ts +5 -5
- package/Utils/Telemetry/EntityRelationship.ts +1 -1
- package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByK8sCluster.js +1 -1
- package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByService.js +1 -1
- package/build/dist/Models/DatabaseModels/AIConversation.js +32 -0
- package/build/dist/Models/DatabaseModels/AIConversation.js.map +1 -1
- package/build/dist/Models/DatabaseModels/AIConversationMessage.js +42 -0
- package/build/dist/Models/DatabaseModels/AIConversationMessage.js.map +1 -1
- package/build/dist/Models/DatabaseModels/AlertEpisodeMember.js +28 -0
- package/build/dist/Models/DatabaseModels/AlertEpisodeMember.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentEpisodeMember.js +29 -0
- package/build/dist/Models/DatabaseModels/IncidentEpisodeMember.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +12 -4
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/{TelemetryEntity.js → InventoryItem.js} +167 -31
- package/build/dist/Models/DatabaseModels/InventoryItem.js.map +1 -0
- package/build/dist/Models/DatabaseModels/InventoryItemCustomField.js +454 -0
- package/build/dist/Models/DatabaseModels/InventoryItemCustomField.js.map +1 -0
- package/build/dist/Models/DatabaseModels/{TelemetryEntityRelationship.js → InventoryItemRelationship.js} +27 -27
- package/build/dist/Models/DatabaseModels/InventoryItemRelationship.js.map +1 -0
- package/build/dist/Models/DatabaseModels/NetworkDevice.js +141 -0
- package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDeviceLink.js +719 -0
- package/build/dist/Models/DatabaseModels/NetworkDeviceLink.js.map +1 -0
- package/build/dist/Models/DatabaseModels/NetworkDeviceLinkRule.js +475 -0
- package/build/dist/Models/DatabaseModels/NetworkDeviceLinkRule.js.map +1 -0
- package/build/dist/Models/DatabaseModels/NetworkTopologySuppression.js +446 -0
- package/build/dist/Models/DatabaseModels/NetworkTopologySuppression.js.map +1 -0
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyFeed.js +9 -0
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyFeed.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Project.js +80 -0
- package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserCall.js +46 -2
- package/build/dist/Models/DatabaseModels/UserCall.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserEmail.js +48 -2
- package/build/dist/Models/DatabaseModels/UserEmail.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserNotificationRule.js +424 -55
- package/build/dist/Models/DatabaseModels/UserNotificationRule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserOnCallLogTimeline.js +24 -2
- package/build/dist/Models/DatabaseModels/UserOnCallLogTimeline.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserPush.js +51 -1
- package/build/dist/Models/DatabaseModels/UserPush.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserSMS.js +47 -2
- package/build/dist/Models/DatabaseModels/UserSMS.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserTelegram.js +65 -3
- package/build/dist/Models/DatabaseModels/UserTelegram.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserWebhook.js +56 -2
- package/build/dist/Models/DatabaseModels/UserWebhook.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserWhatsApp.js +45 -2
- package/build/dist/Models/DatabaseModels/UserWhatsApp.js.map +1 -1
- package/build/dist/Models/DatabaseModels/WorkflowVariable.js +12 -0
- package/build/dist/Models/DatabaseModels/WorkflowVariable.js.map +1 -1
- package/build/dist/Server/API/AIChatAPI.js +237 -1
- package/build/dist/Server/API/AIChatAPI.js.map +1 -1
- package/build/dist/Server/API/DashboardAPI.js +165 -13
- package/build/dist/Server/API/DashboardAPI.js.map +1 -1
- package/build/dist/Server/API/OnCallReadinessAPI.js +599 -0
- package/build/dist/Server/API/OnCallReadinessAPI.js.map +1 -0
- package/build/dist/Server/API/TeamComplianceAPI.js +68 -9
- package/build/dist/Server/API/TeamComplianceAPI.js.map +1 -1
- package/build/dist/Server/API/TelemetryAPI.js +129 -18
- package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
- package/build/dist/Server/EnvironmentConfig.js +45 -0
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js +22 -0
- package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786100000000-RestoreServiceLowerNameIndex.js +4 -4
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786625176831-AddMonitoringMethodToNetworkDevice.js +18 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786625176831-AddMonitoringMethodToNetworkDevice.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786634985763-AddNetworkDeviceLink.js +39 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786634985763-AddNetworkDeviceLink.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786639512056-AddNetworkDeviceLinkRule.js +38 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786639512056-AddNetworkDeviceLinkRule.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786639972982-AddNetworkTopologySuppression.js +22 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786639972982-AddNetworkTopologySuppression.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786800000000-RenameTelemetryEntityToInventoryItem.js +150 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786800000000-RenameTelemetryEntityToInventoryItem.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786900000000-AddInventoryItemArchiveAndCustomFields.js +57 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786900000000-AddInventoryItemArchiveAndCustomFields.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787000000000-AddOnCallNotificationFallbackColumns.js +69 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787000000000-AddOnCallNotificationFallbackColumns.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787100000000-AddAIConversationPageContext.js +32 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787100000000-AddAIConversationPageContext.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787200000000-AddAIChatMessageFeedback.js +26 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787200000000-AddAIChatMessageFeedback.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787300000000-AddEpisodeMemberNotifyIndexes.js +48 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787300000000-AddEpisodeMemberNotifyIndexes.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/Infrastructure/Queue.js +72 -13
- package/build/dist/Server/Infrastructure/Queue.js.map +1 -1
- package/build/dist/Server/Middleware/PublicDashboardRateLimit.js +399 -0
- package/build/dist/Server/Middleware/PublicDashboardRateLimit.js.map +1 -0
- package/build/dist/Server/Services/AIService.js +1 -0
- package/build/dist/Server/Services/AIService.js.map +1 -1
- package/build/dist/Server/Services/AlertEpisodeStateTimelineService.js +24 -3
- package/build/dist/Server/Services/AlertEpisodeStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/AlertSeverityService.js +54 -0
- package/build/dist/Server/Services/AlertSeverityService.js.map +1 -1
- package/build/dist/Server/Services/DashboardService.js +10 -9
- package/build/dist/Server/Services/DashboardService.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +24 -2
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/IncidentEpisodeStateTimelineService.js +24 -3
- package/build/dist/Server/Services/IncidentEpisodeStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/IncidentSeverityService.js +67 -0
- package/build/dist/Server/Services/IncidentSeverityService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +12 -4
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/InventoryItemCustomFieldService.js +9 -0
- package/build/dist/Server/Services/InventoryItemCustomFieldService.js.map +1 -0
- package/build/dist/Server/Services/{TelemetryEntityRelationshipService.js → InventoryItemRelationshipService.js} +6 -6
- package/build/dist/Server/Services/InventoryItemRelationshipService.js.map +1 -0
- package/build/dist/Server/Services/{TelemetryEntityService.js → InventoryItemService.js} +68 -23
- package/build/dist/Server/Services/InventoryItemService.js.map +1 -0
- package/build/dist/Server/Services/LogAggregationService.js +27 -8
- package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
- package/build/dist/Server/Services/MetricAggregationService.js +80 -0
- package/build/dist/Server/Services/MetricAggregationService.js.map +1 -1
- package/build/dist/Server/Services/MetricService.js +6 -6
- package/build/dist/Server/Services/MetricService.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceLinkRuleService.js +9 -0
- package/build/dist/Server/Services/NetworkDeviceLinkRuleService.js.map +1 -0
- package/build/dist/Server/Services/NetworkDeviceLinkService.js +71 -0
- package/build/dist/Server/Services/NetworkDeviceLinkService.js.map +1 -0
- package/build/dist/Server/Services/NetworkDeviceService.js +113 -0
- package/build/dist/Server/Services/NetworkDeviceService.js.map +1 -1
- package/build/dist/Server/Services/NetworkSiteService.js +53 -13
- package/build/dist/Server/Services/NetworkSiteService.js.map +1 -1
- package/build/dist/Server/Services/NetworkTopologySuppressionService.js +85 -0
- package/build/dist/Server/Services/NetworkTopologySuppressionService.js.map +1 -0
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js +48 -32
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyExecutionLogService.js +8 -0
- package/build/dist/Server/Services/OnCallDutyPolicyExecutionLogService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyExecutionLogTimelineService.js +51 -12
- package/build/dist/Server/Services/OnCallDutyPolicyExecutionLogTimelineService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js +57 -13
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js.map +1 -1
- package/build/dist/Server/Services/OnCallNotificationAlertingService.js +548 -0
- package/build/dist/Server/Services/OnCallNotificationAlertingService.js.map +1 -0
- package/build/dist/Server/Services/OnCallReadinessService.js +1961 -0
- package/build/dist/Server/Services/OnCallReadinessService.js.map +1 -0
- package/build/dist/Server/Services/OnCallSetupReminderService.js +738 -0
- package/build/dist/Server/Services/OnCallSetupReminderService.js.map +1 -0
- package/build/dist/Server/Services/ProfileAggregationService.js +68 -4
- package/build/dist/Server/Services/ProfileAggregationService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageService.js +11 -10
- package/build/dist/Server/Services/StatusPageService.js.map +1 -1
- package/build/dist/Server/Services/TeamComplianceService.js +312 -160
- package/build/dist/Server/Services/TeamComplianceService.js.map +1 -1
- package/build/dist/Server/Services/UserCallService.js +24 -1
- package/build/dist/Server/Services/UserCallService.js.map +1 -1
- package/build/dist/Server/Services/UserEmailService.js +24 -1
- package/build/dist/Server/Services/UserEmailService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationRuleAdminService.js +858 -0
- package/build/dist/Server/Services/UserNotificationRuleAdminService.js.map +1 -0
- package/build/dist/Server/Services/UserNotificationRuleService.js +2830 -175
- package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
- package/build/dist/Server/Services/UserOnCallLogService.js +488 -43
- package/build/dist/Server/Services/UserOnCallLogService.js.map +1 -1
- package/build/dist/Server/Services/UserPushService.js +26 -0
- package/build/dist/Server/Services/UserPushService.js.map +1 -1
- package/build/dist/Server/Services/UserService.js +10 -0
- package/build/dist/Server/Services/UserService.js.map +1 -1
- package/build/dist/Server/Services/UserSmsService.js +24 -1
- package/build/dist/Server/Services/UserSmsService.js.map +1 -1
- package/build/dist/Server/Services/UserTelegramService.js +22 -1
- package/build/dist/Server/Services/UserTelegramService.js.map +1 -1
- package/build/dist/Server/Services/UserWebhookService.js +25 -1
- package/build/dist/Server/Services/UserWebhookService.js.map +1 -1
- package/build/dist/Server/Services/UserWhatsAppService.js +22 -1
- package/build/dist/Server/Services/UserWhatsAppService.js.map +1 -1
- package/build/dist/Server/Types/Database/Permissions/BasePermission.js +12 -1
- package/build/dist/Server/Types/Database/Permissions/BasePermission.js.map +1 -1
- package/build/dist/Server/Types/Database/Permissions/CreatePermission.js +126 -0
- package/build/dist/Server/Types/Database/Permissions/CreatePermission.js.map +1 -1
- package/build/dist/Server/Types/Database/Permissions/OwnerOnlyColumnPermission.js +254 -0
- package/build/dist/Server/Types/Database/Permissions/OwnerOnlyColumnPermission.js.map +1 -0
- package/build/dist/Server/Types/Database/Permissions/QueryPermission.js +47 -2
- package/build/dist/Server/Types/Database/Permissions/QueryPermission.js.map +1 -1
- package/build/dist/Server/Types/Database/Permissions/TenantPermission.js +7 -0
- package/build/dist/Server/Types/Database/Permissions/TenantPermission.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/API/Delete.js +1 -1
- package/build/dist/Server/Types/Workflow/Components/API/Delete.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/API/Get.js +1 -1
- package/build/dist/Server/Types/Workflow/Components/API/Get.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/API/Patch.js +1 -1
- package/build/dist/Server/Types/Workflow/Components/API/Patch.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/API/Post.js +1 -1
- package/build/dist/Server/Types/Workflow/Components/API/Post.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/API/Put.js +1 -1
- package/build/dist/Server/Types/Workflow/Components/API/Put.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/BaseModel/CreateManyBaseModel.js +21 -5
- package/build/dist/Server/Types/Workflow/Components/BaseModel/CreateManyBaseModel.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/BaseModel/CreateOneBaseModel.js +14 -10
- package/build/dist/Server/Types/Workflow/Components/BaseModel/CreateOneBaseModel.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/BaseModel/ModelArguments.js +31 -0
- package/build/dist/Server/Types/Workflow/Components/BaseModel/ModelArguments.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/Conditions/IfElse.js +3 -9
- package/build/dist/Server/Types/Workflow/Components/Conditions/IfElse.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/Email.js +15 -4
- package/build/dist/Server/Types/Workflow/Components/Email.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/JavaScript.js +7 -2
- package/build/dist/Server/Types/Workflow/Components/JavaScript.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/MicrosoftTeams/SendMessageToChannel.js +1 -1
- package/build/dist/Server/Types/Workflow/Components/MicrosoftTeams/SendMessageToChannel.js.map +1 -1
- package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js +514 -56
- package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js.map +1 -1
- package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js +20 -3
- package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js.map +1 -1
- package/build/dist/Server/Utils/AI/Chat/ObservabilityChatPrompt.js +19 -6
- package/build/dist/Server/Utils/AI/Chat/ObservabilityChatPrompt.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js +7 -0
- package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/AIActionTools.js +2 -2
- package/build/dist/Server/Utils/AI/Toolbox/AIActionTools.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/AIMetaTools.js +692 -0
- package/build/dist/Server/Utils/AI/Toolbox/AIMetaTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/AlertTools.js +148 -12
- package/build/dist/Server/Utils/AI/Toolbox/AlertTools.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/IncidentTools.js +157 -10
- package/build/dist/Server/Utils/AI/Toolbox/IncidentTools.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/Index.js +37 -0
- package/build/dist/Server/Utils/AI/Toolbox/Index.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/MonitorTools.js +259 -14
- package/build/dist/Server/Utils/AI/Toolbox/MonitorTools.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/NoteWriteTools.js +235 -0
- package/build/dist/Server/Utils/AI/Toolbox/NoteWriteTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/OnCallTools.js +1000 -0
- package/build/dist/Server/Utils/AI/Toolbox/OnCallTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/RunbookTools.js +356 -0
- package/build/dist/Server/Utils/AI/Toolbox/RunbookTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/SloTools.js +394 -0
- package/build/dist/Server/Utils/AI/Toolbox/SloTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/StatusPageTools.js +465 -0
- package/build/dist/Server/Utils/AI/Toolbox/StatusPageTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/TeamTools.js +280 -0
- package/build/dist/Server/Utils/AI/Toolbox/TeamTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/TimelineTools.js +527 -0
- package/build/dist/Server/Utils/AI/Toolbox/TimelineTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/WorkflowProbeTools.js +548 -0
- package/build/dist/Server/Utils/AI/Toolbox/WorkflowProbeTools.js.map +1 -0
- package/build/dist/Server/Utils/ClientIp.js +137 -0
- package/build/dist/Server/Utils/ClientIp.js.map +1 -0
- package/build/dist/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.js +38 -0
- package/build/dist/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.js.map +1 -1
- package/build/dist/Server/Utils/Dashboard/PublicDashboardSloHistoryPolicy.js +89 -0
- package/build/dist/Server/Utils/Dashboard/PublicDashboardSloHistoryPolicy.js.map +1 -0
- package/build/dist/Server/Utils/Dashboard/PublicDashboardSloWidget.js +77 -0
- package/build/dist/Server/Utils/Dashboard/PublicDashboardSloWidget.js.map +1 -0
- package/build/dist/Server/Utils/Express.js +12 -12
- package/build/dist/Server/Utils/Express.js.map +1 -1
- package/build/dist/Server/Utils/LLM/LLMService.js +70 -7
- package/build/dist/Server/Utils/LLM/LLMService.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +121 -11
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
- package/build/dist/Server/Utils/SSRFProtection.js +82 -21
- package/build/dist/Server/Utils/SSRFProtection.js.map +1 -1
- package/build/dist/Server/Utils/StartServer.js +12 -4
- package/build/dist/Server/Utils/StartServer.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +165 -18
- package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js +11 -11
- package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js +122 -47
- package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +7 -3
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
- package/build/dist/Types/AI/AIChatMessageStatus.js +8 -1
- package/build/dist/Types/AI/AIChatMessageStatus.js.map +1 -1
- package/build/dist/Types/AI/AIChatTypes.js +12 -0
- package/build/dist/Types/AI/AIChatTypes.js.map +1 -1
- package/build/dist/Types/Database/AccessControl/OwnerOnlyColumn.js +60 -0
- package/build/dist/Types/Database/AccessControl/OwnerOnlyColumn.js.map +1 -0
- package/build/dist/Types/Exception/ExceptionCode.js +2 -0
- package/build/dist/Types/Exception/ExceptionCode.js.map +1 -1
- package/build/dist/Types/Exception/ServiceUnavailableException.js +8 -0
- package/build/dist/Types/Exception/ServiceUnavailableException.js.map +1 -0
- package/build/dist/Types/Exception/TooManyRequestsException.js +8 -0
- package/build/dist/Types/Exception/TooManyRequestsException.js.map +1 -0
- package/build/dist/Types/IP/IP.js +87 -43
- package/build/dist/Types/IP/IP.js.map +1 -1
- package/build/dist/Types/NetworkDevice/NetworkDeviceMonitoringMethod.js +50 -0
- package/build/dist/Types/NetworkDevice/NetworkDeviceMonitoringMethod.js.map +1 -0
- package/build/dist/Types/OnCallDutyPolicy/Layer.js +186 -123
- package/build/dist/Types/OnCallDutyPolicy/Layer.js.map +1 -1
- package/build/dist/Types/OnCallDutyPolicy/OnCallDutyPolicyStatus.js +13 -0
- package/build/dist/Types/OnCallDutyPolicy/OnCallDutyPolicyStatus.js.map +1 -1
- package/build/dist/Types/OnCallDutyPolicy/ScheduleShiftUtil.js +105 -11
- package/build/dist/Types/OnCallDutyPolicy/ScheduleShiftUtil.js.map +1 -1
- package/build/dist/Types/Permission.js +174 -0
- package/build/dist/Types/Permission.js.map +1 -1
- package/build/dist/Types/Telemetry/EntityRelationshipType.js +1 -1
- package/build/dist/Types/Telemetry/EntitySource.js +1 -1
- package/build/dist/Types/Telemetry/EntityType.js +1 -1
- package/build/dist/Types/Telemetry/EntityTypeGroups.js +1 -1
- package/build/dist/Types/Telemetry/EntityTypeGroups.js.map +1 -1
- package/build/dist/Types/Workflow/Components/BaseModel.js +66 -28
- package/build/dist/Types/Workflow/Components/BaseModel.js.map +1 -1
- package/build/dist/Types/Workflow/Components/Discord.js +1 -0
- package/build/dist/Types/Workflow/Components/Discord.js.map +1 -1
- package/build/dist/Types/Workflow/Components/Email.js +12 -3
- package/build/dist/Types/Workflow/Components/Email.js.map +1 -1
- package/build/dist/Types/Workflow/Components/JavaScript.js +7 -0
- package/build/dist/Types/Workflow/Components/JavaScript.js.map +1 -1
- package/build/dist/Types/Workflow/Components/MicrosoftTeams.js +3 -2
- package/build/dist/Types/Workflow/Components/MicrosoftTeams.js.map +1 -1
- package/build/dist/Types/Workflow/Components/Slack.js +1 -0
- package/build/dist/Types/Workflow/Components/Slack.js.map +1 -1
- package/build/dist/Types/Workflow/Components/Telegram.js +1 -0
- package/build/dist/Types/Workflow/Components/Telegram.js.map +1 -1
- package/build/dist/Types/Workflow/TemplateSyntax.js +12 -0
- package/build/dist/Types/Workflow/TemplateSyntax.js.map +1 -1
- package/build/dist/Types/Workflow/Templates.js +1931 -38
- package/build/dist/Types/Workflow/Templates.js.map +1 -1
- package/build/dist/UI/Components/Calendar/Calendar.js +1 -1
- package/build/dist/UI/Components/Calendar/Calendar.js.map +1 -1
- package/build/dist/UI/Components/Checkbox/Checkbox.js +1 -1
- package/build/dist/UI/Components/Checkbox/Checkbox.js.map +1 -1
- package/build/dist/UI/Components/Dictionary/Dictionary.js +25 -11
- package/build/dist/UI/Components/Dictionary/Dictionary.js.map +1 -1
- package/build/dist/UI/Components/FormModal/BasicFormModal.js.map +1 -1
- package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js +27 -4
- package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js.map +1 -1
- package/build/dist/UI/Components/Input/Input.js +1 -0
- package/build/dist/UI/Components/Input/Input.js.map +1 -1
- package/build/dist/UI/Components/KeyboardShortcut/KeyboardKey.js +163 -0
- package/build/dist/UI/Components/KeyboardShortcut/KeyboardKey.js.map +1 -0
- package/build/dist/UI/Components/KeyboardShortcut/KeyboardShortcut.js +47 -0
- package/build/dist/UI/Components/KeyboardShortcut/KeyboardShortcut.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js +4 -4
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/ActiveFilterChips.js +11 -1
- package/build/dist/UI/Components/LogsViewer/components/ActiveFilterChips.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/KeyboardShortcutsHelp.js +10 -8
- package/build/dist/UI/Components/LogsViewer/components/KeyboardShortcutsHelp.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogDetailsPanel.js +227 -14
- package/build/dist/UI/Components/LogsViewer/components/LogDetailsPanel.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +5 -0
- package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsTable.js +69 -5
- package/build/dist/UI/Components/LogsViewer/components/LogsTable.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js +8 -0
- package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/types.js.map +1 -1
- package/build/dist/UI/Components/Markdown.tsx/MarkdownEditor.js +9 -2
- package/build/dist/UI/Components/Markdown.tsx/MarkdownEditor.js.map +1 -1
- package/build/dist/UI/Components/Navbar/NavBarMenuModal.js +8 -21
- package/build/dist/UI/Components/Navbar/NavBarMenuModal.js.map +1 -1
- package/build/dist/UI/Components/ProjectInvitations/PendingProjectInvitations.js +251 -0
- package/build/dist/UI/Components/ProjectInvitations/PendingProjectInvitations.js.map +1 -0
- package/build/dist/UI/Components/SimpleLogViewer/SimpleLogViewer.js +9 -2
- package/build/dist/UI/Components/SimpleLogViewer/SimpleLogViewer.js.map +1 -1
- package/build/dist/UI/Components/Table/Table.js +27 -15
- package/build/dist/UI/Components/Table/Table.js.map +1 -1
- package/build/dist/UI/Components/Table/TableBody.js +24 -18
- package/build/dist/UI/Components/Table/TableBody.js.map +1 -1
- package/build/dist/UI/Components/Table/TableHeader.js +9 -1
- package/build/dist/UI/Components/Table/TableHeader.js.map +1 -1
- package/build/dist/UI/Components/Table/TableRow.js +29 -21
- package/build/dist/UI/Components/Table/TableRow.js.map +1 -1
- package/build/dist/UI/Components/Workflow/ArgumentsForm.js +130 -12
- package/build/dist/UI/Components/Workflow/ArgumentsForm.js.map +1 -1
- package/build/dist/UI/Components/Workflow/ComponentReturnValueViewer.js +10 -1
- package/build/dist/UI/Components/Workflow/ComponentReturnValueViewer.js.map +1 -1
- package/build/dist/UI/Components/Workflow/ComponentSettingsModal.js +4 -4
- package/build/dist/UI/Components/Workflow/ComponentSettingsModal.js.map +1 -1
- package/build/dist/UI/Components/Workflow/ComponentValuePickerModal.js +57 -7
- package/build/dist/UI/Components/Workflow/ComponentValuePickerModal.js.map +1 -1
- package/build/dist/UI/Components/Workflow/ComponentsModal.js +53 -18
- package/build/dist/UI/Components/Workflow/ComponentsModal.js.map +1 -1
- package/build/dist/UI/Components/Workflow/DocumentationViewer.js +19 -6
- package/build/dist/UI/Components/Workflow/DocumentationViewer.js.map +1 -1
- package/build/dist/UI/Components/Workflow/GraphLint.js +33 -4
- package/build/dist/UI/Components/Workflow/GraphLint.js.map +1 -1
- package/build/dist/UI/Components/Workflow/GraphLintSummary.js +231 -0
- package/build/dist/UI/Components/Workflow/GraphLintSummary.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ModelColumnEditor.js +116 -21
- package/build/dist/UI/Components/Workflow/ModelColumnEditor.js.map +1 -1
- package/build/dist/UI/Components/Workflow/ModelSchema.js +72 -34
- package/build/dist/UI/Components/Workflow/ModelSchema.js.map +1 -1
- package/build/dist/UI/Components/Workflow/RunStatusWatcher.js +1 -1
- package/build/dist/UI/Components/Workflow/RunStatusWatcher.js.map +1 -1
- package/build/dist/UI/Components/Workflow/StepTraceViewer.js +1 -1
- package/build/dist/UI/Components/Workflow/StepTraceViewer.js.map +1 -1
- package/build/dist/UI/Components/Workflow/UseRunWatch.js +123 -0
- package/build/dist/UI/Components/Workflow/UseRunWatch.js.map +1 -0
- package/build/dist/UI/Components/Workflow/VariableModal.js +3 -2
- package/build/dist/UI/Components/Workflow/VariableModal.js.map +1 -1
- package/build/dist/UI/Components/Workflow/Workflow.js +41 -1
- package/build/dist/UI/Components/Workflow/Workflow.js.map +1 -1
- package/build/dist/UI/Components/Workflow/WorkflowIssuesModal.js +99 -0
- package/build/dist/UI/Components/Workflow/WorkflowIssuesModal.js.map +1 -0
- package/build/dist/UI/Components/Workflow/WorkflowLogModal.js +56 -0
- package/build/dist/UI/Components/Workflow/WorkflowLogModal.js.map +1 -0
- package/build/dist/UI/Components/Workflow/WorkflowStatusBar.js +92 -0
- package/build/dist/UI/Components/Workflow/WorkflowStatusBar.js.map +1 -0
- package/build/dist/UI/Utils/AIChatExport/ConversationMarkdown.js +9 -0
- package/build/dist/UI/Utils/AIChatExport/ConversationMarkdown.js.map +1 -1
- package/build/dist/UI/Utils/ModelAPI/ModelAPI.js +1 -1
- package/build/dist/UI/Utils/ModelAPI/ModelAPI.js.map +1 -1
- package/build/dist/UI/Utils/Platform.js +118 -0
- package/build/dist/UI/Utils/Platform.js.map +1 -0
- package/build/dist/UI/Utils/ProjectInvitationDisplay.js +106 -0
- package/build/dist/UI/Utils/ProjectInvitationDisplay.js.map +1 -0
- package/build/dist/Utils/Monitor/NetworkDeviceLinkRuleUtil.js +108 -0
- package/build/dist/Utils/Monitor/NetworkDeviceLinkRuleUtil.js.map +1 -0
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +642 -136
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
- package/build/dist/Utils/Telemetry/CrossSignalScope.js +328 -0
- package/build/dist/Utils/Telemetry/CrossSignalScope.js.map +1 -0
- package/build/dist/Utils/Telemetry/EntityKey.js +5 -5
- package/build/dist/Utils/Telemetry/EntityRelationship.js +1 -1
- package/jest.config.json +1 -0
- package/package.json +1 -1
- package/build/dist/Models/DatabaseModels/TelemetryEntity.js.map +0 -1
- package/build/dist/Models/DatabaseModels/TelemetryEntityRelationship.js.map +0 -1
- package/build/dist/Server/Services/TelemetryEntityRelationshipService.js.map +0 -1
- package/build/dist/Server/Services/TelemetryEntityService.js.map +0 -1
|
@@ -0,0 +1,1818 @@
|
|
|
1
|
+
import AlertSeverityService from "../../../Server/Services/AlertSeverityService";
|
|
2
|
+
import IncidentSeverityService from "../../../Server/Services/IncidentSeverityService";
|
|
3
|
+
import UserEmailService from "../../../Server/Services/UserEmailService";
|
|
4
|
+
import UserNotificationRuleService from "../../../Server/Services/UserNotificationRuleService";
|
|
5
|
+
import logger from "../../../Server/Utils/Logger";
|
|
6
|
+
import AlertSeverity from "../../../Models/DatabaseModels/AlertSeverity";
|
|
7
|
+
import IncidentSeverity from "../../../Models/DatabaseModels/IncidentSeverity";
|
|
8
|
+
import UserEmail from "../../../Models/DatabaseModels/UserEmail";
|
|
9
|
+
import UserNotificationRule from "../../../Models/DatabaseModels/UserNotificationRule";
|
|
10
|
+
import LIMIT_MAX from "../../../Types/Database/LimitMax";
|
|
11
|
+
import NotificationRuleType from "../../../Types/NotificationRule/NotificationRuleType";
|
|
12
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
13
|
+
import { EVERY_FIVE_MINUTE } from "../../../Utils/CronTime";
|
|
14
|
+
import { afterEach, beforeEach, describe, expect, test } from "@jest/globals";
|
|
15
|
+
|
|
16
|
+
/*
|
|
17
|
+
* GAP A, the worker half.
|
|
18
|
+
*
|
|
19
|
+
* A severity created after a responder joined was a severity that responder had
|
|
20
|
+
* no notification rule for. Default rules are written exactly twice in a
|
|
21
|
+
* responder's life - when they join a project and when they verify a
|
|
22
|
+
* notification method - and both paths iterate the severities that exist AT
|
|
23
|
+
* THAT MOMENT. Nothing revisited the question, so the "Sev4" somebody adds a
|
|
24
|
+
* year in paged precisely nobody: every incident on it counted zero matching
|
|
25
|
+
* rules and dropped into an execution log no one reads.
|
|
26
|
+
*
|
|
27
|
+
* Phase 1 closed that with a worker job,
|
|
28
|
+
* App/FeatureSet/Workers/Jobs/OnCallDutyPolicy/BackfillNotificationRulesForNewSeverities.ts,
|
|
29
|
+
* enqueued by name from IncidentSeverityService.onCreateSuccess and
|
|
30
|
+
* AlertSeverityService.onCreateSuccess and also swept on a five-minute
|
|
31
|
+
* schedule over severities created in the last hour.
|
|
32
|
+
*
|
|
33
|
+
* The neighbouring SeverityCreationRuleBackfill.test.ts covers the ENQUEUE:
|
|
34
|
+
* that creating a severity puts the job on the Worker queue and writes nothing
|
|
35
|
+
* inline. This file covers THE JOB ITSELF, through the three functions it
|
|
36
|
+
* exports for exactly this purpose - findRecentlyCreatedSeverities,
|
|
37
|
+
* buildResponderIntents and backfillSeverity - rather than through RunCron.
|
|
38
|
+
*
|
|
39
|
+
* Four properties are load-bearing, and everything here serves one of them:
|
|
40
|
+
*
|
|
41
|
+
* 1. IT MIRRORS INTENT, IT DOES NOT IMPOSE A DEFAULT. Someone who set
|
|
42
|
+
* "Sev1 -> call me immediately, Sev3 -> email me after fifteen minutes"
|
|
43
|
+
* gets one new rule per distinct (method, delay) pair they already chose
|
|
44
|
+
* for that rule type. Not a hardcoded email-at-zero, which would page
|
|
45
|
+
* instantly a responder who deliberately built a delay into every rule
|
|
46
|
+
* they own; and above all not a phone call for someone who has only ever
|
|
47
|
+
* used email. Mirroring can only ever hand a responder a channel they
|
|
48
|
+
* already opted into. A responder who muted the rule type entirely gets
|
|
49
|
+
* the mute mirrored, not a page.
|
|
50
|
+
*
|
|
51
|
+
* 2. IT IS IDEMPOTENT. The job runs from two routes at once - the by-name
|
|
52
|
+
* enqueue fires seconds after a severity is created, and a scheduled sweep
|
|
53
|
+
* may already be part-way through the same severity - and
|
|
54
|
+
* UserNotificationRule carries no unique index over
|
|
55
|
+
* (project, user, ruleType, severity, method). A duplicated rule is a
|
|
56
|
+
* duplicated page. Two guards are pinned separately below: the per-run
|
|
57
|
+
* snapshot, and the per-row read taken immediately before each write that
|
|
58
|
+
* catches the case where the snapshot was already stale.
|
|
59
|
+
*
|
|
60
|
+
* 3. IT DOES NOT LOAD A PROJECT PER RESPONDER. Reads are one project-wide
|
|
61
|
+
* findAllBy per rule type - which pages in LIMIT_MAX batches under the
|
|
62
|
+
* hood - not one query per responder. A thousand-responder project must
|
|
63
|
+
* not issue a thousand queries per severity.
|
|
64
|
+
*
|
|
65
|
+
* 4. ONE RESPONDER'S FAILURE DOES NOT TAKE THE REST DOWN. A method row that
|
|
66
|
+
* vanishes mid-run is a log line, not an aborted backfill for everybody
|
|
67
|
+
* else in the project.
|
|
68
|
+
*
|
|
69
|
+
* No database is touched. UserNotificationRule is backed by an in-memory table
|
|
70
|
+
* so that rows written by one run are genuinely visible to the next - the
|
|
71
|
+
* idempotence assertions are only worth anything if the reader really reads
|
|
72
|
+
* what the writer really wrote.
|
|
73
|
+
*/
|
|
74
|
+
|
|
75
|
+
/*
|
|
76
|
+
* The job registers itself with RunCron at import time, which would otherwise
|
|
77
|
+
* reach for the Worker queue (and therefore Redis) as a side effect of
|
|
78
|
+
* importing this file. Mocked before the job module is imported, both to keep
|
|
79
|
+
* the import inert and to capture the registration itself.
|
|
80
|
+
*/
|
|
81
|
+
jest.mock("../../../../App/FeatureSet/Workers/Utils/Cron", () => {
|
|
82
|
+
return {
|
|
83
|
+
__esModule: true,
|
|
84
|
+
default: jest.fn(),
|
|
85
|
+
};
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Imported AFTER the mock above so the registration lands in it.
|
|
89
|
+
import RunCron from "../../../../App/FeatureSet/Workers/Utils/Cron";
|
|
90
|
+
import {
|
|
91
|
+
JOB_NAME,
|
|
92
|
+
backfillSeverity,
|
|
93
|
+
buildResponderIntents,
|
|
94
|
+
findRecentlyCreatedSeverities,
|
|
95
|
+
MirroredRule,
|
|
96
|
+
NewSeverity,
|
|
97
|
+
ResponderIntent,
|
|
98
|
+
} from "../../../../App/FeatureSet/Workers/Jobs/OnCallDutyPolicy/BackfillNotificationRulesForNewSeverities";
|
|
99
|
+
|
|
100
|
+
/*
|
|
101
|
+
* The third copy of the job name. IncidentSeverityService and
|
|
102
|
+
* AlertSeverityService each duplicate this string deliberately (Common cannot
|
|
103
|
+
* import from App), and SeverityCreationRuleBackfill.test.ts pins their copies.
|
|
104
|
+
* This is the job's own, so a rename that misses any of the three surfaces.
|
|
105
|
+
*/
|
|
106
|
+
const EXPECTED_JOB_NAME: string =
|
|
107
|
+
"OnCallDutyPolicy:BackfillNotificationRulesForNewSeverities";
|
|
108
|
+
|
|
109
|
+
const PROJECT_ID: ObjectID = new ObjectID("project-1");
|
|
110
|
+
|
|
111
|
+
const USER_A: ObjectID = new ObjectID("user-a");
|
|
112
|
+
const USER_B: ObjectID = new ObjectID("user-b");
|
|
113
|
+
const USER_C: ObjectID = new ObjectID("user-c");
|
|
114
|
+
|
|
115
|
+
const EMAIL_A: ObjectID = new ObjectID("user-email-a");
|
|
116
|
+
const EMAIL_A_SECOND: ObjectID = new ObjectID("user-email-a-second");
|
|
117
|
+
const EMAIL_B: ObjectID = new ObjectID("user-email-b");
|
|
118
|
+
const EMAIL_C: ObjectID = new ObjectID("user-email-c");
|
|
119
|
+
|
|
120
|
+
const CALL_A: ObjectID = new ObjectID("user-call-a");
|
|
121
|
+
const SMS_A: ObjectID = new ObjectID("user-sms-a");
|
|
122
|
+
const PUSH_A: ObjectID = new ObjectID("user-push-a");
|
|
123
|
+
|
|
124
|
+
// The severities that existed when the responder configured their rules...
|
|
125
|
+
const SEV_1: ObjectID = new ObjectID("incident-severity-1");
|
|
126
|
+
const SEV_2: ObjectID = new ObjectID("incident-severity-2");
|
|
127
|
+
const SEV_3: ObjectID = new ObjectID("incident-severity-3");
|
|
128
|
+
// ...and the one added a year later, which nothing covered.
|
|
129
|
+
const SEV_NEW: ObjectID = new ObjectID("incident-severity-4-added-later");
|
|
130
|
+
|
|
131
|
+
const ALERT_SEV_1: ObjectID = new ObjectID("alert-severity-1");
|
|
132
|
+
const ALERT_SEV_NEW: ObjectID = new ObjectID("alert-severity-4-added-later");
|
|
133
|
+
|
|
134
|
+
const INCIDENT_RULE_TYPES: Array<NotificationRuleType> = [
|
|
135
|
+
NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
|
|
136
|
+
NotificationRuleType.ON_CALL_EXECUTED_INCIDENT_EPISODE,
|
|
137
|
+
];
|
|
138
|
+
|
|
139
|
+
const ALERT_RULE_TYPES: Array<NotificationRuleType> = [
|
|
140
|
+
NotificationRuleType.ON_CALL_EXECUTED_ALERT,
|
|
141
|
+
NotificationRuleType.ON_CALL_EXECUTED_ALERT_EPISODE,
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
type RuleMethodColumn =
|
|
145
|
+
| "userEmailId"
|
|
146
|
+
| "userSmsId"
|
|
147
|
+
| "userCallId"
|
|
148
|
+
| "userPushId"
|
|
149
|
+
| "userWhatsAppId"
|
|
150
|
+
| "userTelegramId"
|
|
151
|
+
| "userWebhookId";
|
|
152
|
+
|
|
153
|
+
type RuleIdColumn =
|
|
154
|
+
| RuleMethodColumn
|
|
155
|
+
| "projectId"
|
|
156
|
+
| "userId"
|
|
157
|
+
| "incidentSeverityId"
|
|
158
|
+
| "alertSeverityId";
|
|
159
|
+
|
|
160
|
+
const METHOD_COLUMNS: Array<RuleMethodColumn> = [
|
|
161
|
+
"userEmailId",
|
|
162
|
+
"userSmsId",
|
|
163
|
+
"userCallId",
|
|
164
|
+
"userPushId",
|
|
165
|
+
"userWhatsAppId",
|
|
166
|
+
"userTelegramId",
|
|
167
|
+
"userWebhookId",
|
|
168
|
+
];
|
|
169
|
+
|
|
170
|
+
const ID_COLUMNS: Array<RuleIdColumn> = [
|
|
171
|
+
"projectId",
|
|
172
|
+
"userId",
|
|
173
|
+
"incidentSeverityId",
|
|
174
|
+
"alertSeverityId",
|
|
175
|
+
...METHOD_COLUMNS,
|
|
176
|
+
];
|
|
177
|
+
|
|
178
|
+
/*
|
|
179
|
+
* ------------------------------------------------------------------------- *
|
|
180
|
+
* An in-memory stand-in for the UserNotificationRule table.
|
|
181
|
+
*
|
|
182
|
+
* Rows are real model instances, because that is what the job reads: it asks
|
|
183
|
+
* rules for `.userId`, `.isOptOut`, `.notifyAfterMinutes` and the seven method
|
|
184
|
+
* foreign keys, and hands `create` a model it built itself.
|
|
185
|
+
* -------------------------------------------------------------------------
|
|
186
|
+
*/
|
|
187
|
+
|
|
188
|
+
let ruleStore: Array<UserNotificationRule> = [];
|
|
189
|
+
|
|
190
|
+
// Only the rows THIS run wrote - the whole point of the idempotence assertions.
|
|
191
|
+
let createdRules: Array<UserNotificationRule> = [];
|
|
192
|
+
|
|
193
|
+
/*
|
|
194
|
+
* When set, findAllBy reports this instead of the live table.
|
|
195
|
+
*
|
|
196
|
+
* That is how an OVERLAPPING run is expressed: the job's per-rule-type snapshot
|
|
197
|
+
* is taken once at the top of a run, so a second run that started before the
|
|
198
|
+
* first one wrote anything is a run whose snapshot no longer matches the table.
|
|
199
|
+
* findOneBy always reads the live table, which is precisely the guard under
|
|
200
|
+
* test.
|
|
201
|
+
*/
|
|
202
|
+
let frozenSnapshot: Array<UserNotificationRule> | null = null;
|
|
203
|
+
|
|
204
|
+
// The verified email rows the project has, in the order the query returns them.
|
|
205
|
+
let verifiedEmails: Array<{ userId: ObjectID; emailId: ObjectID }> = [];
|
|
206
|
+
|
|
207
|
+
let ruleFindAllBySpy: jest.SpyInstance;
|
|
208
|
+
let ruleFindOneBySpy: jest.SpyInstance;
|
|
209
|
+
let ruleCreateSpy: jest.SpyInstance;
|
|
210
|
+
let userEmailFindAllBySpy: jest.SpyInstance;
|
|
211
|
+
let loggerErrorSpy: jest.SpyInstance;
|
|
212
|
+
|
|
213
|
+
function idOf(value: unknown): string | undefined {
|
|
214
|
+
if (value === undefined || value === null) {
|
|
215
|
+
return undefined;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return String(value);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
interface RuleSpec {
|
|
222
|
+
userId: ObjectID;
|
|
223
|
+
ruleType: NotificationRuleType;
|
|
224
|
+
projectId?: ObjectID | undefined;
|
|
225
|
+
incidentSeverityId?: ObjectID | undefined;
|
|
226
|
+
alertSeverityId?: ObjectID | undefined;
|
|
227
|
+
notifyAfterMinutes?: number | undefined;
|
|
228
|
+
userEmailId?: ObjectID | undefined;
|
|
229
|
+
userSmsId?: ObjectID | undefined;
|
|
230
|
+
userCallId?: ObjectID | undefined;
|
|
231
|
+
userPushId?: ObjectID | undefined;
|
|
232
|
+
isOptOut?: boolean | undefined;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
let seedCounter: number = 0;
|
|
236
|
+
|
|
237
|
+
/*
|
|
238
|
+
* A rule row as the responder (or an earlier default-rule pass) left it.
|
|
239
|
+
* Columns are only set when the spec names them, so "no severity id at all" and
|
|
240
|
+
* "no delay at all" - both of which really occur in this table - stay
|
|
241
|
+
* expressible.
|
|
242
|
+
*/
|
|
243
|
+
function makeRule(spec: RuleSpec): UserNotificationRule {
|
|
244
|
+
seedCounter++;
|
|
245
|
+
|
|
246
|
+
const rule: UserNotificationRule = new UserNotificationRule();
|
|
247
|
+
rule._id = `seeded-rule-${seedCounter}`;
|
|
248
|
+
rule.projectId = spec.projectId || PROJECT_ID;
|
|
249
|
+
rule.userId = spec.userId;
|
|
250
|
+
rule.ruleType = spec.ruleType;
|
|
251
|
+
|
|
252
|
+
if (spec.incidentSeverityId) {
|
|
253
|
+
rule.incidentSeverityId = spec.incidentSeverityId;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (spec.alertSeverityId) {
|
|
257
|
+
rule.alertSeverityId = spec.alertSeverityId;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (spec.notifyAfterMinutes !== undefined) {
|
|
261
|
+
rule.notifyAfterMinutes = spec.notifyAfterMinutes;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (spec.userEmailId) {
|
|
265
|
+
rule.userEmailId = spec.userEmailId;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (spec.userSmsId) {
|
|
269
|
+
rule.userSmsId = spec.userSmsId;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (spec.userCallId) {
|
|
273
|
+
rule.userCallId = spec.userCallId;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (spec.userPushId) {
|
|
277
|
+
rule.userPushId = spec.userPushId;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (spec.isOptOut !== undefined) {
|
|
281
|
+
rule.isOptOut = spec.isOptOut;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return rule;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function seedRule(spec: RuleSpec): UserNotificationRule {
|
|
288
|
+
const rule: UserNotificationRule = makeRule(spec);
|
|
289
|
+
ruleStore.push(rule);
|
|
290
|
+
|
|
291
|
+
return rule;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/*
|
|
295
|
+
* Match a stored row against a service query. A column the query does not carry
|
|
296
|
+
* is not a constraint, which mirrors how the real queries are built: the
|
|
297
|
+
* per-rule-type read names only projectId and ruleType, and the pre-write
|
|
298
|
+
* existence check names exactly one method column.
|
|
299
|
+
*/
|
|
300
|
+
function ruleMatchesQuery(
|
|
301
|
+
rule: UserNotificationRule,
|
|
302
|
+
query: Record<string, unknown>,
|
|
303
|
+
): boolean {
|
|
304
|
+
for (const column of ID_COLUMNS) {
|
|
305
|
+
const expected: string | undefined = idOf(query[column]);
|
|
306
|
+
|
|
307
|
+
if (expected !== undefined && idOf(rule[column]) !== expected) {
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const expectedRuleType: unknown = query["ruleType"];
|
|
313
|
+
|
|
314
|
+
if (expectedRuleType !== undefined && rule.ruleType !== expectedRuleType) {
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const expectedDelay: unknown = query["notifyAfterMinutes"];
|
|
319
|
+
|
|
320
|
+
if (
|
|
321
|
+
expectedDelay !== undefined &&
|
|
322
|
+
rule.notifyAfterMinutes !== expectedDelay
|
|
323
|
+
) {
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/*
|
|
328
|
+
* Compared as a boolean because an ordinary rule leaves the column undefined
|
|
329
|
+
* rather than false, while the opt-out existence check asks for `true`.
|
|
330
|
+
*/
|
|
331
|
+
const expectedOptOut: unknown = query["isOptOut"];
|
|
332
|
+
|
|
333
|
+
if (
|
|
334
|
+
expectedOptOut !== undefined &&
|
|
335
|
+
Boolean(rule.isOptOut) !== expectedOptOut
|
|
336
|
+
) {
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return true;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function matchingRules(
|
|
344
|
+
source: Array<UserNotificationRule>,
|
|
345
|
+
query: Record<string, unknown>,
|
|
346
|
+
): Array<UserNotificationRule> {
|
|
347
|
+
return source.filter((rule: UserNotificationRule): boolean => {
|
|
348
|
+
return ruleMatchesQuery(rule, query);
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/* "userCallId:user-call-a", or "none" for an opt-out row. */
|
|
353
|
+
function methodOf(rule: UserNotificationRule): string {
|
|
354
|
+
for (const column of METHOD_COLUMNS) {
|
|
355
|
+
const value: ObjectID | undefined = rule[column];
|
|
356
|
+
|
|
357
|
+
if (value) {
|
|
358
|
+
return `${column}:${value.toString()}`;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return "none";
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/* The whole of what a rule says, in one comparable string: "how@when". */
|
|
366
|
+
function shapeOf(rule: UserNotificationRule): string {
|
|
367
|
+
return `${methodOf(rule)}@${String(rule.notifyAfterMinutes)}`;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function createdOfType(
|
|
371
|
+
ruleType: NotificationRuleType,
|
|
372
|
+
): Array<UserNotificationRule> {
|
|
373
|
+
return createdRules.filter((rule: UserNotificationRule): boolean => {
|
|
374
|
+
return rule.ruleType === ruleType;
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function shapesCreatedFor(ruleType: NotificationRuleType): Array<string> {
|
|
379
|
+
return createdOfType(ruleType).map((rule: UserNotificationRule): string => {
|
|
380
|
+
return shapeOf(rule);
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function incidentSeverity(id: ObjectID): NewSeverity {
|
|
385
|
+
return {
|
|
386
|
+
id: id,
|
|
387
|
+
projectId: PROJECT_ID,
|
|
388
|
+
kind: "incident",
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function alertSeverity(id: ObjectID): NewSeverity {
|
|
393
|
+
return {
|
|
394
|
+
id: id,
|
|
395
|
+
projectId: PROJECT_ID,
|
|
396
|
+
kind: "alert",
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function incidentSeverityRow(
|
|
401
|
+
id: ObjectID | null,
|
|
402
|
+
projectId: ObjectID | null,
|
|
403
|
+
): IncidentSeverity {
|
|
404
|
+
const severity: IncidentSeverity = new IncidentSeverity();
|
|
405
|
+
|
|
406
|
+
if (id) {
|
|
407
|
+
severity._id = id.toString();
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (projectId) {
|
|
411
|
+
severity.projectId = projectId;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return severity;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function alertSeverityRow(
|
|
418
|
+
id: ObjectID | null,
|
|
419
|
+
projectId: ObjectID | null,
|
|
420
|
+
): AlertSeverity {
|
|
421
|
+
const severity: AlertSeverity = new AlertSeverity();
|
|
422
|
+
|
|
423
|
+
if (id) {
|
|
424
|
+
severity._id = id.toString();
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (projectId) {
|
|
428
|
+
severity.projectId = projectId;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return severity;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function userEmailRow(userId: ObjectID, emailId: ObjectID): UserEmail {
|
|
435
|
+
const model: UserEmail = new UserEmail();
|
|
436
|
+
model._id = emailId.toString();
|
|
437
|
+
model.userId = userId;
|
|
438
|
+
|
|
439
|
+
return model;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function queryOf(
|
|
443
|
+
spy: jest.SpyInstance,
|
|
444
|
+
callIndex: number,
|
|
445
|
+
): Record<string, unknown> {
|
|
446
|
+
return (spy.mock.calls[callIndex]![0] as { query: Record<string, unknown> })
|
|
447
|
+
.query;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function propsOf(
|
|
451
|
+
spy: jest.SpyInstance,
|
|
452
|
+
callIndex: number,
|
|
453
|
+
): { isRoot?: boolean | undefined } {
|
|
454
|
+
return (
|
|
455
|
+
spy.mock.calls[callIndex]![0] as {
|
|
456
|
+
props: { isRoot?: boolean | undefined };
|
|
457
|
+
}
|
|
458
|
+
).props;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
beforeEach(() => {
|
|
462
|
+
ruleStore = [];
|
|
463
|
+
createdRules = [];
|
|
464
|
+
frozenSnapshot = null;
|
|
465
|
+
verifiedEmails = [];
|
|
466
|
+
seedCounter = 0;
|
|
467
|
+
|
|
468
|
+
ruleFindAllBySpy = jest
|
|
469
|
+
.spyOn(UserNotificationRuleService, "findAllBy")
|
|
470
|
+
.mockImplementation(((data: {
|
|
471
|
+
query: Record<string, unknown>;
|
|
472
|
+
}): Promise<Array<UserNotificationRule>> => {
|
|
473
|
+
return Promise.resolve(
|
|
474
|
+
matchingRules(frozenSnapshot || ruleStore, data.query),
|
|
475
|
+
);
|
|
476
|
+
}) as never);
|
|
477
|
+
|
|
478
|
+
ruleFindOneBySpy = jest
|
|
479
|
+
.spyOn(UserNotificationRuleService, "findOneBy")
|
|
480
|
+
.mockImplementation(((data: {
|
|
481
|
+
query: Record<string, unknown>;
|
|
482
|
+
}): Promise<UserNotificationRule | null> => {
|
|
483
|
+
return Promise.resolve(matchingRules(ruleStore, data.query)[0] || null);
|
|
484
|
+
}) as never);
|
|
485
|
+
|
|
486
|
+
ruleCreateSpy = jest
|
|
487
|
+
.spyOn(UserNotificationRuleService, "create")
|
|
488
|
+
.mockImplementation(((data: {
|
|
489
|
+
data: UserNotificationRule;
|
|
490
|
+
}): Promise<UserNotificationRule> => {
|
|
491
|
+
const rule: UserNotificationRule = data.data;
|
|
492
|
+
rule._id = `created-rule-${createdRules.length + 1}`;
|
|
493
|
+
|
|
494
|
+
ruleStore.push(rule);
|
|
495
|
+
createdRules.push(rule);
|
|
496
|
+
|
|
497
|
+
return Promise.resolve(rule);
|
|
498
|
+
}) as never);
|
|
499
|
+
|
|
500
|
+
userEmailFindAllBySpy = jest
|
|
501
|
+
.spyOn(UserEmailService, "findAllBy")
|
|
502
|
+
.mockImplementation(((): Promise<Array<UserEmail>> => {
|
|
503
|
+
return Promise.resolve(
|
|
504
|
+
verifiedEmails.map(
|
|
505
|
+
(entry: { userId: ObjectID; emailId: ObjectID }): UserEmail => {
|
|
506
|
+
return userEmailRow(entry.userId, entry.emailId);
|
|
507
|
+
},
|
|
508
|
+
),
|
|
509
|
+
);
|
|
510
|
+
}) as never);
|
|
511
|
+
|
|
512
|
+
// The job logs a failed row rather than throwing; keep the output clean.
|
|
513
|
+
loggerErrorSpy = jest.spyOn(logger, "error").mockImplementation((): void => {
|
|
514
|
+
return undefined;
|
|
515
|
+
});
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
afterEach(() => {
|
|
519
|
+
jest.restoreAllMocks();
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
/*
|
|
523
|
+
* ========================================================================= *
|
|
524
|
+
* The registration itself.
|
|
525
|
+
* =========================================================================
|
|
526
|
+
*/
|
|
527
|
+
|
|
528
|
+
describe("the sweep is registered under the name both severity services enqueue", () => {
|
|
529
|
+
test("the job name is the exact string IncidentSeverityService and AlertSeverityService duplicate", () => {
|
|
530
|
+
expect(JOB_NAME).toBe(EXPECTED_JOB_NAME);
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
/*
|
|
534
|
+
* The by-name enqueue is best-effort: if Redis is unreachable when a severity
|
|
535
|
+
* is created, the enqueue is swallowed and coverage depends entirely on this
|
|
536
|
+
* schedule existing. runOnStartup stays false because a worker restart loop
|
|
537
|
+
* must not re-run the sweep on every boot.
|
|
538
|
+
*/
|
|
539
|
+
test("it registers a five-minute schedule that does not run on startup", () => {
|
|
540
|
+
const runCronMock: jest.Mock = RunCron as unknown as jest.Mock;
|
|
541
|
+
|
|
542
|
+
expect(runCronMock).toHaveBeenCalledTimes(1);
|
|
543
|
+
|
|
544
|
+
const call: Array<unknown> = runCronMock.mock.calls[0]!;
|
|
545
|
+
expect(call[0]).toBe(EXPECTED_JOB_NAME);
|
|
546
|
+
|
|
547
|
+
const options: { schedule: string; runOnStartup: boolean } = call[1] as {
|
|
548
|
+
schedule: string;
|
|
549
|
+
runOnStartup: boolean;
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
expect(options.schedule).toBe(EVERY_FIVE_MINUTE);
|
|
553
|
+
expect(options.runOnStartup).toBe(false);
|
|
554
|
+
});
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
/*
|
|
558
|
+
* ========================================================================= *
|
|
559
|
+
* findRecentlyCreatedSeverities - the sweep re-derives its own work set.
|
|
560
|
+
*
|
|
561
|
+
* The Worker queue dispatches on job NAME and hands the job function no
|
|
562
|
+
* payload, so the job cannot be told which severity to cover; it has to ask.
|
|
563
|
+
* Asking over a window wider than the schedule is what makes a lost enqueue
|
|
564
|
+
* cost minutes of latency instead of a permanently uncovered severity.
|
|
565
|
+
* =========================================================================
|
|
566
|
+
*/
|
|
567
|
+
|
|
568
|
+
describe("findRecentlyCreatedSeverities", () => {
|
|
569
|
+
let incidentFindAllBySpy: jest.SpyInstance;
|
|
570
|
+
let alertFindAllBySpy: jest.SpyInstance;
|
|
571
|
+
|
|
572
|
+
function stubSeverityTables(
|
|
573
|
+
incidentRows: Array<IncidentSeverity>,
|
|
574
|
+
alertRows: Array<AlertSeverity>,
|
|
575
|
+
): void {
|
|
576
|
+
incidentFindAllBySpy = jest
|
|
577
|
+
.spyOn(IncidentSeverityService, "findAllBy")
|
|
578
|
+
.mockResolvedValue(incidentRows as never);
|
|
579
|
+
alertFindAllBySpy = jest
|
|
580
|
+
.spyOn(AlertSeverityService, "findAllBy")
|
|
581
|
+
.mockResolvedValue(alertRows as never);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/*
|
|
585
|
+
* QueryHelper.greaterThanEqualTo builds a TypeORM Raw operator carrying its
|
|
586
|
+
* bound as a bound parameter. Reading it back is the only way to assert the
|
|
587
|
+
* window is a window at all rather than "every severity ever created".
|
|
588
|
+
*/
|
|
589
|
+
function lookbackInMinutes(query: Record<string, unknown>): number {
|
|
590
|
+
const operator: { objectLiteralParameters?: Record<string, unknown> } =
|
|
591
|
+
query["createdAt"] as unknown as {
|
|
592
|
+
objectLiteralParameters?: Record<string, unknown>;
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
const parameters: Record<string, unknown> =
|
|
596
|
+
operator.objectLiteralParameters || {};
|
|
597
|
+
const values: Array<unknown> = Object.values(parameters);
|
|
598
|
+
|
|
599
|
+
expect(values).toHaveLength(1);
|
|
600
|
+
|
|
601
|
+
const bound: Date = values[0] as Date;
|
|
602
|
+
|
|
603
|
+
return (Date.now() - bound.getTime()) / (60 * 1000);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
test("it reads both severity tables, root-privileged, selecting only what it needs", async () => {
|
|
607
|
+
stubSeverityTables([], []);
|
|
608
|
+
|
|
609
|
+
await findRecentlyCreatedSeverities();
|
|
610
|
+
|
|
611
|
+
expect(incidentFindAllBySpy).toHaveBeenCalledTimes(1);
|
|
612
|
+
expect(alertFindAllBySpy).toHaveBeenCalledTimes(1);
|
|
613
|
+
|
|
614
|
+
expect(propsOf(incidentFindAllBySpy, 0).isRoot).toBe(true);
|
|
615
|
+
expect(propsOf(alertFindAllBySpy, 0).isRoot).toBe(true);
|
|
616
|
+
|
|
617
|
+
const select: Record<string, unknown> = (
|
|
618
|
+
incidentFindAllBySpy.mock.calls[0]![0] as {
|
|
619
|
+
select: Record<string, unknown>;
|
|
620
|
+
}
|
|
621
|
+
).select;
|
|
622
|
+
|
|
623
|
+
expect(select["_id"]).toBe(true);
|
|
624
|
+
expect(select["projectId"]).toBe(true);
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
test("the window is an hour wide - comfortably longer than the five-minute schedule", async () => {
|
|
628
|
+
stubSeverityTables([], []);
|
|
629
|
+
|
|
630
|
+
await findRecentlyCreatedSeverities();
|
|
631
|
+
|
|
632
|
+
expect(lookbackInMinutes(queryOf(incidentFindAllBySpy, 0))).toBeGreaterThan(
|
|
633
|
+
59,
|
|
634
|
+
);
|
|
635
|
+
expect(lookbackInMinutes(queryOf(incidentFindAllBySpy, 0))).toBeLessThan(
|
|
636
|
+
61,
|
|
637
|
+
);
|
|
638
|
+
expect(lookbackInMinutes(queryOf(alertFindAllBySpy, 0))).toBeGreaterThan(
|
|
639
|
+
59,
|
|
640
|
+
);
|
|
641
|
+
expect(lookbackInMinutes(queryOf(alertFindAllBySpy, 0))).toBeLessThan(61);
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
test("a quiet project yields no work at all - severity creation is rare", async () => {
|
|
645
|
+
stubSeverityTables([], []);
|
|
646
|
+
|
|
647
|
+
await expect(findRecentlyCreatedSeverities()).resolves.toEqual([]);
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
/*
|
|
651
|
+
* The kind tag is what tells backfillSeverity which of the two severity
|
|
652
|
+
* columns to write and which pair of rule types to cover, so a row from the
|
|
653
|
+
* wrong table would silently write a rule that can never match.
|
|
654
|
+
*/
|
|
655
|
+
test("each severity is tagged with the table it came from", async () => {
|
|
656
|
+
stubSeverityTables(
|
|
657
|
+
[incidentSeverityRow(SEV_NEW, PROJECT_ID)],
|
|
658
|
+
[alertSeverityRow(ALERT_SEV_NEW, PROJECT_ID)],
|
|
659
|
+
);
|
|
660
|
+
|
|
661
|
+
const severities: Array<NewSeverity> =
|
|
662
|
+
await findRecentlyCreatedSeverities();
|
|
663
|
+
|
|
664
|
+
expect(severities).toHaveLength(2);
|
|
665
|
+
|
|
666
|
+
const incident: NewSeverity | undefined = severities.find(
|
|
667
|
+
(severity: NewSeverity): boolean => {
|
|
668
|
+
return severity.kind === "incident";
|
|
669
|
+
},
|
|
670
|
+
);
|
|
671
|
+
const alert: NewSeverity | undefined = severities.find(
|
|
672
|
+
(severity: NewSeverity): boolean => {
|
|
673
|
+
return severity.kind === "alert";
|
|
674
|
+
},
|
|
675
|
+
);
|
|
676
|
+
|
|
677
|
+
expect(incident!.id.toString()).toBe(SEV_NEW.toString());
|
|
678
|
+
expect(incident!.projectId.toString()).toBe(PROJECT_ID.toString());
|
|
679
|
+
expect(alert!.id.toString()).toBe(ALERT_SEV_NEW.toString());
|
|
680
|
+
expect(alert!.projectId.toString()).toBe(PROJECT_ID.toString());
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
test("a row with no id or no project is skipped rather than backfilled against nothing", async () => {
|
|
684
|
+
stubSeverityTables(
|
|
685
|
+
[
|
|
686
|
+
incidentSeverityRow(null, PROJECT_ID),
|
|
687
|
+
incidentSeverityRow(SEV_NEW, null),
|
|
688
|
+
incidentSeverityRow(SEV_1, PROJECT_ID),
|
|
689
|
+
],
|
|
690
|
+
[alertSeverityRow(null, null)],
|
|
691
|
+
);
|
|
692
|
+
|
|
693
|
+
const severities: Array<NewSeverity> =
|
|
694
|
+
await findRecentlyCreatedSeverities();
|
|
695
|
+
|
|
696
|
+
expect(severities).toHaveLength(1);
|
|
697
|
+
expect(severities[0]!.id.toString()).toBe(SEV_1.toString());
|
|
698
|
+
});
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
/*
|
|
702
|
+
* ========================================================================= *
|
|
703
|
+
* buildResponderIntents - "what would this person want for a new severity?"
|
|
704
|
+
*
|
|
705
|
+
* The answer is: the same channels, at the same delays, that they already chose
|
|
706
|
+
* for this rule type on the severities they did configure.
|
|
707
|
+
* =========================================================================
|
|
708
|
+
*/
|
|
709
|
+
|
|
710
|
+
describe("buildResponderIntents", () => {
|
|
711
|
+
function intentsFor(
|
|
712
|
+
rules: Array<UserNotificationRule>,
|
|
713
|
+
severity: NewSeverity,
|
|
714
|
+
): Map<string, ResponderIntent> {
|
|
715
|
+
return buildResponderIntents(rules, severity);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function mirroredOf(intent: ResponderIntent): Array<string> {
|
|
719
|
+
return Array.from(intent.mirroredRules.values()).map(
|
|
720
|
+
(mirrored: MirroredRule): string => {
|
|
721
|
+
return `${mirrored.methodColumn}:${mirrored.methodId.toString()}@${String(
|
|
722
|
+
mirrored.notifyAfterMinutes,
|
|
723
|
+
)}`;
|
|
724
|
+
},
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
test("the same (method, delay) used on three severities is one distinct pair", () => {
|
|
729
|
+
const intents: Map<string, ResponderIntent> = intentsFor(
|
|
730
|
+
[
|
|
731
|
+
makeRule({
|
|
732
|
+
userId: USER_A,
|
|
733
|
+
ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
|
|
734
|
+
incidentSeverityId: SEV_1,
|
|
735
|
+
userEmailId: EMAIL_A,
|
|
736
|
+
notifyAfterMinutes: 0,
|
|
737
|
+
}),
|
|
738
|
+
makeRule({
|
|
739
|
+
userId: USER_A,
|
|
740
|
+
ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
|
|
741
|
+
incidentSeverityId: SEV_2,
|
|
742
|
+
userEmailId: EMAIL_A,
|
|
743
|
+
notifyAfterMinutes: 0,
|
|
744
|
+
}),
|
|
745
|
+
makeRule({
|
|
746
|
+
userId: USER_A,
|
|
747
|
+
ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
|
|
748
|
+
incidentSeverityId: SEV_3,
|
|
749
|
+
userEmailId: EMAIL_A,
|
|
750
|
+
notifyAfterMinutes: 0,
|
|
751
|
+
}),
|
|
752
|
+
],
|
|
753
|
+
incidentSeverity(SEV_NEW),
|
|
754
|
+
);
|
|
755
|
+
|
|
756
|
+
expect(mirroredOf(intents.get(USER_A.toString())!)).toEqual([
|
|
757
|
+
`userEmailId:${EMAIL_A.toString()}@0`,
|
|
758
|
+
]);
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
/*
|
|
762
|
+
* The delay is part of the key on purpose. "Email me now" and "email me in
|
|
763
|
+
* fifteen minutes" are two different requests, and a key that ignored the
|
|
764
|
+
* delay would collapse them into one.
|
|
765
|
+
*/
|
|
766
|
+
test("the same method at two delays is two distinct pairs", () => {
|
|
767
|
+
const intents: Map<string, ResponderIntent> = intentsFor(
|
|
768
|
+
[
|
|
769
|
+
makeRule({
|
|
770
|
+
userId: USER_A,
|
|
771
|
+
ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
|
|
772
|
+
incidentSeverityId: SEV_1,
|
|
773
|
+
userEmailId: EMAIL_A,
|
|
774
|
+
notifyAfterMinutes: 0,
|
|
775
|
+
}),
|
|
776
|
+
makeRule({
|
|
777
|
+
userId: USER_A,
|
|
778
|
+
ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
|
|
779
|
+
incidentSeverityId: SEV_1,
|
|
780
|
+
userEmailId: EMAIL_A,
|
|
781
|
+
notifyAfterMinutes: 15,
|
|
782
|
+
}),
|
|
783
|
+
],
|
|
784
|
+
incidentSeverity(SEV_NEW),
|
|
785
|
+
);
|
|
786
|
+
|
|
787
|
+
expect(mirroredOf(intents.get(USER_A.toString())!).sort()).toEqual([
|
|
788
|
+
`userEmailId:${EMAIL_A.toString()}@0`,
|
|
789
|
+
`userEmailId:${EMAIL_A.toString()}@15`,
|
|
790
|
+
]);
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
test("two different channels at the same delay do not collide", () => {
|
|
794
|
+
const intents: Map<string, ResponderIntent> = intentsFor(
|
|
795
|
+
[
|
|
796
|
+
makeRule({
|
|
797
|
+
userId: USER_A,
|
|
798
|
+
ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
|
|
799
|
+
incidentSeverityId: SEV_1,
|
|
800
|
+
userEmailId: EMAIL_A,
|
|
801
|
+
notifyAfterMinutes: 0,
|
|
802
|
+
}),
|
|
803
|
+
makeRule({
|
|
804
|
+
userId: USER_A,
|
|
805
|
+
ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
|
|
806
|
+
incidentSeverityId: SEV_1,
|
|
807
|
+
userCallId: CALL_A,
|
|
808
|
+
notifyAfterMinutes: 0,
|
|
809
|
+
}),
|
|
810
|
+
],
|
|
811
|
+
incidentSeverity(SEV_NEW),
|
|
812
|
+
);
|
|
813
|
+
|
|
814
|
+
expect(mirroredOf(intents.get(USER_A.toString())!)).toHaveLength(2);
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
test("responders are kept apart - one person's channels never leak into another's", () => {
|
|
818
|
+
const intents: Map<string, ResponderIntent> = intentsFor(
|
|
819
|
+
[
|
|
820
|
+
makeRule({
|
|
821
|
+
userId: USER_A,
|
|
822
|
+
ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
|
|
823
|
+
incidentSeverityId: SEV_1,
|
|
824
|
+
userCallId: CALL_A,
|
|
825
|
+
notifyAfterMinutes: 0,
|
|
826
|
+
}),
|
|
827
|
+
makeRule({
|
|
828
|
+
userId: USER_B,
|
|
829
|
+
ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
|
|
830
|
+
incidentSeverityId: SEV_1,
|
|
831
|
+
userEmailId: EMAIL_B,
|
|
832
|
+
notifyAfterMinutes: 30,
|
|
833
|
+
}),
|
|
834
|
+
],
|
|
835
|
+
incidentSeverity(SEV_NEW),
|
|
836
|
+
);
|
|
837
|
+
|
|
838
|
+
expect(intents.size).toBe(2);
|
|
839
|
+
expect(mirroredOf(intents.get(USER_A.toString())!)).toEqual([
|
|
840
|
+
`userCallId:${CALL_A.toString()}@0`,
|
|
841
|
+
]);
|
|
842
|
+
expect(mirroredOf(intents.get(USER_B.toString())!)).toEqual([
|
|
843
|
+
`userEmailId:${EMAIL_B.toString()}@30`,
|
|
844
|
+
]);
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
/*
|
|
848
|
+
* A row for the new severity is the idempotency guard, not input: it means an
|
|
849
|
+
* earlier run - or the responder - already covered this cell, and mirroring
|
|
850
|
+
* it back onto itself would be the duplicate the whole design exists to
|
|
851
|
+
* avoid.
|
|
852
|
+
*/
|
|
853
|
+
test("a row for the new severity marks the cell covered and is not itself mirrored", () => {
|
|
854
|
+
const intents: Map<string, ResponderIntent> = intentsFor(
|
|
855
|
+
[
|
|
856
|
+
makeRule({
|
|
857
|
+
userId: USER_A,
|
|
858
|
+
ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
|
|
859
|
+
incidentSeverityId: SEV_NEW,
|
|
860
|
+
userEmailId: EMAIL_A,
|
|
861
|
+
notifyAfterMinutes: 0,
|
|
862
|
+
}),
|
|
863
|
+
],
|
|
864
|
+
incidentSeverity(SEV_NEW),
|
|
865
|
+
);
|
|
866
|
+
|
|
867
|
+
const intent: ResponderIntent = intents.get(USER_A.toString())!;
|
|
868
|
+
|
|
869
|
+
expect(intent.hasRuleForNewSeverity).toBe(true);
|
|
870
|
+
expect(mirroredOf(intent)).toEqual([]);
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
test("an opt-out row is recorded as an opt-out and contributes no channel", () => {
|
|
874
|
+
const intents: Map<string, ResponderIntent> = intentsFor(
|
|
875
|
+
[
|
|
876
|
+
makeRule({
|
|
877
|
+
userId: USER_A,
|
|
878
|
+
ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
|
|
879
|
+
incidentSeverityId: SEV_1,
|
|
880
|
+
isOptOut: true,
|
|
881
|
+
notifyAfterMinutes: 0,
|
|
882
|
+
}),
|
|
883
|
+
],
|
|
884
|
+
incidentSeverity(SEV_NEW),
|
|
885
|
+
);
|
|
886
|
+
|
|
887
|
+
const intent: ResponderIntent = intents.get(USER_A.toString())!;
|
|
888
|
+
|
|
889
|
+
expect(intent.hasOptOut).toBe(true);
|
|
890
|
+
expect(intent.hasRuleForNewSeverity).toBe(false);
|
|
891
|
+
expect(mirroredOf(intent)).toEqual([]);
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
/*
|
|
895
|
+
* Episode default rules were written without a severity id for a long time
|
|
896
|
+
* (Gap G). Those rows are just as much a statement of "this is how I want to
|
|
897
|
+
* be told" as a severity-scoped one.
|
|
898
|
+
*/
|
|
899
|
+
test("a row with no severity at all still counts as intent", () => {
|
|
900
|
+
const intents: Map<string, ResponderIntent> = intentsFor(
|
|
901
|
+
[
|
|
902
|
+
makeRule({
|
|
903
|
+
userId: USER_A,
|
|
904
|
+
ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT_EPISODE,
|
|
905
|
+
userSmsId: SMS_A,
|
|
906
|
+
notifyAfterMinutes: 5,
|
|
907
|
+
}),
|
|
908
|
+
],
|
|
909
|
+
incidentSeverity(SEV_NEW),
|
|
910
|
+
);
|
|
911
|
+
|
|
912
|
+
const intent: ResponderIntent = intents.get(USER_A.toString())!;
|
|
913
|
+
|
|
914
|
+
expect(intent.hasRuleForNewSeverity).toBe(false);
|
|
915
|
+
expect(mirroredOf(intent)).toEqual([`userSmsId:${SMS_A.toString()}@5`]);
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
test("a missing delay is read as immediate, not as undefined", () => {
|
|
919
|
+
const intents: Map<string, ResponderIntent> = intentsFor(
|
|
920
|
+
[
|
|
921
|
+
makeRule({
|
|
922
|
+
userId: USER_A,
|
|
923
|
+
ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
|
|
924
|
+
incidentSeverityId: SEV_1,
|
|
925
|
+
userEmailId: EMAIL_A,
|
|
926
|
+
}),
|
|
927
|
+
],
|
|
928
|
+
incidentSeverity(SEV_NEW),
|
|
929
|
+
);
|
|
930
|
+
|
|
931
|
+
const mirrored: Array<MirroredRule> = Array.from(
|
|
932
|
+
intents.get(USER_A.toString())!.mirroredRules.values(),
|
|
933
|
+
);
|
|
934
|
+
|
|
935
|
+
expect(mirrored[0]!.notifyAfterMinutes).toBe(0);
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
test("a row with no user is ignored entirely", () => {
|
|
939
|
+
const orphan: UserNotificationRule = new UserNotificationRule();
|
|
940
|
+
orphan._id = "orphan-rule";
|
|
941
|
+
orphan.ruleType = NotificationRuleType.ON_CALL_EXECUTED_INCIDENT;
|
|
942
|
+
orphan.userEmailId = EMAIL_A;
|
|
943
|
+
|
|
944
|
+
expect(intentsFor([orphan], incidentSeverity(SEV_NEW)).size).toBe(0);
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
/*
|
|
948
|
+
* The two severity columns are read according to the KIND being backfilled.
|
|
949
|
+
* A rule carrying an incidentSeverityId cannot mark an alert severity
|
|
950
|
+
* covered, however equal the ids happen to look.
|
|
951
|
+
*/
|
|
952
|
+
test("an alert backfill reads alertSeverityId, never incidentSeverityId", () => {
|
|
953
|
+
const intents: Map<string, ResponderIntent> = intentsFor(
|
|
954
|
+
[
|
|
955
|
+
makeRule({
|
|
956
|
+
userId: USER_A,
|
|
957
|
+
ruleType: NotificationRuleType.ON_CALL_EXECUTED_ALERT,
|
|
958
|
+
incidentSeverityId: ALERT_SEV_NEW,
|
|
959
|
+
userEmailId: EMAIL_A,
|
|
960
|
+
notifyAfterMinutes: 0,
|
|
961
|
+
}),
|
|
962
|
+
],
|
|
963
|
+
alertSeverity(ALERT_SEV_NEW),
|
|
964
|
+
);
|
|
965
|
+
|
|
966
|
+
const intent: ResponderIntent = intents.get(USER_A.toString())!;
|
|
967
|
+
|
|
968
|
+
expect(intent.hasRuleForNewSeverity).toBe(false);
|
|
969
|
+
expect(mirroredOf(intent)).toEqual([`userEmailId:${EMAIL_A.toString()}@0`]);
|
|
970
|
+
});
|
|
971
|
+
});
|
|
972
|
+
|
|
973
|
+
/*
|
|
974
|
+
* ========================================================================= *
|
|
975
|
+
* backfillSeverity - mirroring existing intent.
|
|
976
|
+
*
|
|
977
|
+
* Note throughout: one severity covers TWO rule types (incident and incident
|
|
978
|
+
* episode, or alert and alert episode), so a responder configured only for the
|
|
979
|
+
* non-episode type is mirrored there and falls back to their verified email for
|
|
980
|
+
* the episode one. The assertions are per rule type for exactly that reason.
|
|
981
|
+
* =========================================================================
|
|
982
|
+
*/
|
|
983
|
+
|
|
984
|
+
describe("backfillSeverity mirrors what the responder already asked for", () => {
|
|
985
|
+
/*
|
|
986
|
+
* The headline case. "Sev1 -> call me at once, Sev3 -> email me after fifteen
|
|
987
|
+
* minutes" must become one new rule per pair - not a surprise phone call at a
|
|
988
|
+
* delay they never chose, and not a flattened email-at-zero default.
|
|
989
|
+
*/
|
|
990
|
+
test("a responder with two different (method, delay) pairs gets both of them", async () => {
|
|
991
|
+
verifiedEmails = [{ userId: USER_A, emailId: EMAIL_A }];
|
|
992
|
+
|
|
993
|
+
seedRule({
|
|
994
|
+
userId: USER_A,
|
|
995
|
+
ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
|
|
996
|
+
incidentSeverityId: SEV_1,
|
|
997
|
+
userCallId: CALL_A,
|
|
998
|
+
notifyAfterMinutes: 0,
|
|
999
|
+
});
|
|
1000
|
+
seedRule({
|
|
1001
|
+
userId: USER_A,
|
|
1002
|
+
ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
|
|
1003
|
+
incidentSeverityId: SEV_3,
|
|
1004
|
+
userEmailId: EMAIL_A,
|
|
1005
|
+
notifyAfterMinutes: 15,
|
|
1006
|
+
});
|
|
1007
|
+
|
|
1008
|
+
await backfillSeverity(incidentSeverity(SEV_NEW));
|
|
1009
|
+
|
|
1010
|
+
const shapes: Array<string> = shapesCreatedFor(
|
|
1011
|
+
NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
|
|
1012
|
+
);
|
|
1013
|
+
|
|
1014
|
+
expect(shapes).toHaveLength(2);
|
|
1015
|
+
expect(shapes).toContain(`userCallId:${CALL_A.toString()}@0`);
|
|
1016
|
+
expect(shapes).toContain(`userEmailId:${EMAIL_A.toString()}@15`);
|
|
1017
|
+
|
|
1018
|
+
// The flattened default they must NOT get: their email, immediately.
|
|
1019
|
+
expect(shapes).not.toContain(`userEmailId:${EMAIL_A.toString()}@0`);
|
|
1020
|
+
});
|
|
1021
|
+
|
|
1022
|
+
test("every new rule is bound to the new severity and to no other", async () => {
|
|
1023
|
+
verifiedEmails = [{ userId: USER_A, emailId: EMAIL_A }];
|
|
1024
|
+
|
|
1025
|
+
seedRule({
|
|
1026
|
+
userId: USER_A,
|
|
1027
|
+
ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
|
|
1028
|
+
incidentSeverityId: SEV_1,
|
|
1029
|
+
userCallId: CALL_A,
|
|
1030
|
+
notifyAfterMinutes: 0,
|
|
1031
|
+
});
|
|
1032
|
+
|
|
1033
|
+
await backfillSeverity(incidentSeverity(SEV_NEW));
|
|
1034
|
+
|
|
1035
|
+
expect(createdRules.length).toBeGreaterThan(0);
|
|
1036
|
+
|
|
1037
|
+
for (const rule of createdRules) {
|
|
1038
|
+
expect(rule.projectId!.toString()).toBe(PROJECT_ID.toString());
|
|
1039
|
+
expect(rule.userId!.toString()).toBe(USER_A.toString());
|
|
1040
|
+
expect(rule.incidentSeverityId!.toString()).toBe(SEV_NEW.toString());
|
|
1041
|
+
expect(rule.alertSeverityId).toBeUndefined();
|
|
1042
|
+
}
|
|
1043
|
+
});
|
|
1044
|
+
|
|
1045
|
+
/*
|
|
1046
|
+
* The failure mode a hardcoded default would produce: a responder who has
|
|
1047
|
+
* only ever used email waking up to a phone call.
|
|
1048
|
+
*/
|
|
1049
|
+
test("an email-only responder is never given a phone call", async () => {
|
|
1050
|
+
verifiedEmails = [{ userId: USER_A, emailId: EMAIL_A }];
|
|
1051
|
+
|
|
1052
|
+
seedRule({
|
|
1053
|
+
userId: USER_A,
|
|
1054
|
+
ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
|
|
1055
|
+
incidentSeverityId: SEV_1,
|
|
1056
|
+
userEmailId: EMAIL_A,
|
|
1057
|
+
notifyAfterMinutes: 10,
|
|
1058
|
+
});
|
|
1059
|
+
|
|
1060
|
+
await backfillSeverity(incidentSeverity(SEV_NEW));
|
|
1061
|
+
|
|
1062
|
+
for (const rule of createdOfType(
|
|
1063
|
+
NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
|
|
1064
|
+
)) {
|
|
1065
|
+
expect(rule.userCallId).toBeUndefined();
|
|
1066
|
+
expect(rule.userSmsId).toBeUndefined();
|
|
1067
|
+
expect(rule.notifyAfterMinutes).toBe(10);
|
|
1068
|
+
}
|
|
1069
|
+
});
|
|
1070
|
+
|
|
1071
|
+
test("the same pair repeated across severities produces exactly one new rule", async () => {
|
|
1072
|
+
verifiedEmails = [{ userId: USER_A, emailId: EMAIL_A }];
|
|
1073
|
+
|
|
1074
|
+
for (const severityId of [SEV_1, SEV_2, SEV_3]) {
|
|
1075
|
+
seedRule({
|
|
1076
|
+
userId: USER_A,
|
|
1077
|
+
ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
|
|
1078
|
+
incidentSeverityId: severityId,
|
|
1079
|
+
userPushId: PUSH_A,
|
|
1080
|
+
notifyAfterMinutes: 0,
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
await backfillSeverity(incidentSeverity(SEV_NEW));
|
|
1085
|
+
|
|
1086
|
+
expect(
|
|
1087
|
+
shapesCreatedFor(NotificationRuleType.ON_CALL_EXECUTED_INCIDENT),
|
|
1088
|
+
).toEqual([`userPushId:${PUSH_A.toString()}@0`]);
|
|
1089
|
+
});
|
|
1090
|
+
|
|
1091
|
+
/*
|
|
1092
|
+
* A responder who deliberately muted this rule type gets the mute mirrored.
|
|
1093
|
+
* Leaving the cell empty would hand them to the verified-method fallback in
|
|
1094
|
+
* UserOnCallLogService, which is to say it would page someone who explicitly
|
|
1095
|
+
* asked not to be paged.
|
|
1096
|
+
*/
|
|
1097
|
+
test("a responder who opted out is given an opt-out row, not a page", async () => {
|
|
1098
|
+
verifiedEmails = [{ userId: USER_A, emailId: EMAIL_A }];
|
|
1099
|
+
|
|
1100
|
+
seedRule({
|
|
1101
|
+
userId: USER_A,
|
|
1102
|
+
ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
|
|
1103
|
+
incidentSeverityId: SEV_1,
|
|
1104
|
+
isOptOut: true,
|
|
1105
|
+
notifyAfterMinutes: 0,
|
|
1106
|
+
});
|
|
1107
|
+
|
|
1108
|
+
await backfillSeverity(incidentSeverity(SEV_NEW));
|
|
1109
|
+
|
|
1110
|
+
const created: Array<UserNotificationRule> = createdOfType(
|
|
1111
|
+
NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
|
|
1112
|
+
);
|
|
1113
|
+
|
|
1114
|
+
expect(created).toHaveLength(1);
|
|
1115
|
+
expect(created[0]!.isOptOut).toBe(true);
|
|
1116
|
+
expect(methodOf(created[0]!)).toBe("none");
|
|
1117
|
+
expect(created[0]!.notifyAfterMinutes).toBe(0);
|
|
1118
|
+
expect(created[0]!.incidentSeverityId!.toString()).toBe(SEV_NEW.toString());
|
|
1119
|
+
});
|
|
1120
|
+
|
|
1121
|
+
test("an opt-out on one severity does not silence a responder who configured another", async () => {
|
|
1122
|
+
verifiedEmails = [{ userId: USER_A, emailId: EMAIL_A }];
|
|
1123
|
+
|
|
1124
|
+
seedRule({
|
|
1125
|
+
userId: USER_A,
|
|
1126
|
+
ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
|
|
1127
|
+
incidentSeverityId: SEV_1,
|
|
1128
|
+
isOptOut: true,
|
|
1129
|
+
notifyAfterMinutes: 0,
|
|
1130
|
+
});
|
|
1131
|
+
seedRule({
|
|
1132
|
+
userId: USER_A,
|
|
1133
|
+
ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
|
|
1134
|
+
incidentSeverityId: SEV_3,
|
|
1135
|
+
userEmailId: EMAIL_A,
|
|
1136
|
+
notifyAfterMinutes: 0,
|
|
1137
|
+
});
|
|
1138
|
+
|
|
1139
|
+
await backfillSeverity(incidentSeverity(SEV_NEW));
|
|
1140
|
+
|
|
1141
|
+
const created: Array<UserNotificationRule> = createdOfType(
|
|
1142
|
+
NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
|
|
1143
|
+
);
|
|
1144
|
+
|
|
1145
|
+
expect(created).toHaveLength(1);
|
|
1146
|
+
expect(created[0]!.isOptOut).toBeUndefined();
|
|
1147
|
+
expect(methodOf(created[0]!)).toBe(`userEmailId:${EMAIL_A.toString()}`);
|
|
1148
|
+
});
|
|
1149
|
+
|
|
1150
|
+
/*
|
|
1151
|
+
* Mirroring is per rule type. An incident-only responder's phone-call rule
|
|
1152
|
+
* must not become an alert rule, and vice versa - the two lists are
|
|
1153
|
+
* configured independently in the UI and mean different things.
|
|
1154
|
+
*/
|
|
1155
|
+
test("an alert backfill ignores the responder's incident rules", async () => {
|
|
1156
|
+
verifiedEmails = [{ userId: USER_A, emailId: EMAIL_A }];
|
|
1157
|
+
|
|
1158
|
+
seedRule({
|
|
1159
|
+
userId: USER_A,
|
|
1160
|
+
ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
|
|
1161
|
+
incidentSeverityId: SEV_1,
|
|
1162
|
+
userCallId: CALL_A,
|
|
1163
|
+
notifyAfterMinutes: 0,
|
|
1164
|
+
});
|
|
1165
|
+
|
|
1166
|
+
await backfillSeverity(alertSeverity(ALERT_SEV_NEW));
|
|
1167
|
+
|
|
1168
|
+
const shapes: Array<string> = shapesCreatedFor(
|
|
1169
|
+
NotificationRuleType.ON_CALL_EXECUTED_ALERT,
|
|
1170
|
+
);
|
|
1171
|
+
|
|
1172
|
+
expect(shapes).toEqual([`userEmailId:${EMAIL_A.toString()}@0`]);
|
|
1173
|
+
expect(shapes).not.toContain(`userCallId:${CALL_A.toString()}@0`);
|
|
1174
|
+
});
|
|
1175
|
+
|
|
1176
|
+
test("both rule types of a severity kind are covered, and only those two", async () => {
|
|
1177
|
+
verifiedEmails = [{ userId: USER_A, emailId: EMAIL_A }];
|
|
1178
|
+
|
|
1179
|
+
await backfillSeverity(incidentSeverity(SEV_NEW));
|
|
1180
|
+
|
|
1181
|
+
const typesCovered: Array<NotificationRuleType | undefined> =
|
|
1182
|
+
createdRules.map(
|
|
1183
|
+
(rule: UserNotificationRule): NotificationRuleType | undefined => {
|
|
1184
|
+
return rule.ruleType;
|
|
1185
|
+
},
|
|
1186
|
+
);
|
|
1187
|
+
|
|
1188
|
+
expect(typesCovered.sort()).toEqual([...INCIDENT_RULE_TYPES].sort());
|
|
1189
|
+
});
|
|
1190
|
+
|
|
1191
|
+
test("an episode rule written without a severity is mirrored onto the new severity", async () => {
|
|
1192
|
+
verifiedEmails = [{ userId: USER_A, emailId: EMAIL_A }];
|
|
1193
|
+
|
|
1194
|
+
seedRule({
|
|
1195
|
+
userId: USER_A,
|
|
1196
|
+
ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT_EPISODE,
|
|
1197
|
+
userSmsId: SMS_A,
|
|
1198
|
+
notifyAfterMinutes: 5,
|
|
1199
|
+
});
|
|
1200
|
+
|
|
1201
|
+
await backfillSeverity(incidentSeverity(SEV_NEW));
|
|
1202
|
+
|
|
1203
|
+
// Their stated channel, not the email default they never asked for.
|
|
1204
|
+
expect(
|
|
1205
|
+
shapesCreatedFor(NotificationRuleType.ON_CALL_EXECUTED_INCIDENT_EPISODE),
|
|
1206
|
+
).toEqual([`userSmsId:${SMS_A.toString()}@5`]);
|
|
1207
|
+
});
|
|
1208
|
+
});
|
|
1209
|
+
|
|
1210
|
+
/*
|
|
1211
|
+
* ========================================================================= *
|
|
1212
|
+
* backfillSeverity - the verified-email fallback.
|
|
1213
|
+
*
|
|
1214
|
+
* A responder who has said nothing at all about a rule type gets exactly what
|
|
1215
|
+
* addDefaultNotificationRuleForUser would have written for them had the
|
|
1216
|
+
* severity existed when they joined.
|
|
1217
|
+
* =========================================================================
|
|
1218
|
+
*/
|
|
1219
|
+
|
|
1220
|
+
describe("backfillSeverity falls back to the responder's verified email", () => {
|
|
1221
|
+
test("a responder with no rules of that type gets their verified email, immediately", async () => {
|
|
1222
|
+
verifiedEmails = [{ userId: USER_A, emailId: EMAIL_A }];
|
|
1223
|
+
|
|
1224
|
+
await backfillSeverity(incidentSeverity(SEV_NEW));
|
|
1225
|
+
|
|
1226
|
+
expect(createdRules).toHaveLength(2);
|
|
1227
|
+
|
|
1228
|
+
for (const ruleType of INCIDENT_RULE_TYPES) {
|
|
1229
|
+
expect(shapesCreatedFor(ruleType)).toEqual([
|
|
1230
|
+
`userEmailId:${EMAIL_A.toString()}@0`,
|
|
1231
|
+
]);
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
for (const rule of createdRules) {
|
|
1235
|
+
expect(rule.isOptOut).toBeUndefined();
|
|
1236
|
+
}
|
|
1237
|
+
});
|
|
1238
|
+
|
|
1239
|
+
/*
|
|
1240
|
+
* The read that decides who is reachable at all. Unverified addresses are
|
|
1241
|
+
* excluded by the query itself, which is what makes "no verified method =>
|
|
1242
|
+
* nothing written" true rather than accidental.
|
|
1243
|
+
*/
|
|
1244
|
+
test("only verified addresses in this project are considered", async () => {
|
|
1245
|
+
verifiedEmails = [{ userId: USER_A, emailId: EMAIL_A }];
|
|
1246
|
+
|
|
1247
|
+
await backfillSeverity(incidentSeverity(SEV_NEW));
|
|
1248
|
+
|
|
1249
|
+
const query: Record<string, unknown> = queryOf(userEmailFindAllBySpy, 0);
|
|
1250
|
+
|
|
1251
|
+
expect(idOf(query["projectId"])).toBe(PROJECT_ID.toString());
|
|
1252
|
+
expect(query["isVerified"]).toBe(true);
|
|
1253
|
+
expect(propsOf(userEmailFindAllBySpy, 0).isRoot).toBe(true);
|
|
1254
|
+
});
|
|
1255
|
+
|
|
1256
|
+
test("a responder with no verified method gets nothing, and the run still covers everyone else", async () => {
|
|
1257
|
+
// USER_B has no verified email row at all, so the project cannot reach them.
|
|
1258
|
+
verifiedEmails = [
|
|
1259
|
+
{ userId: USER_A, emailId: EMAIL_A },
|
|
1260
|
+
{ userId: USER_C, emailId: EMAIL_C },
|
|
1261
|
+
];
|
|
1262
|
+
|
|
1263
|
+
await expect(
|
|
1264
|
+
backfillSeverity(incidentSeverity(SEV_NEW)),
|
|
1265
|
+
).resolves.toBeUndefined();
|
|
1266
|
+
|
|
1267
|
+
const usersCovered: Array<string> = createdRules.map(
|
|
1268
|
+
(rule: UserNotificationRule): string => {
|
|
1269
|
+
return rule.userId!.toString();
|
|
1270
|
+
},
|
|
1271
|
+
);
|
|
1272
|
+
|
|
1273
|
+
expect(usersCovered).not.toContain(USER_B.toString());
|
|
1274
|
+
expect(createdRules).toHaveLength(4);
|
|
1275
|
+
});
|
|
1276
|
+
|
|
1277
|
+
test("a responder with several verified addresses gets one rule, on the first", async () => {
|
|
1278
|
+
verifiedEmails = [
|
|
1279
|
+
{ userId: USER_A, emailId: EMAIL_A },
|
|
1280
|
+
{ userId: USER_A, emailId: EMAIL_A_SECOND },
|
|
1281
|
+
];
|
|
1282
|
+
|
|
1283
|
+
await backfillSeverity(incidentSeverity(SEV_NEW));
|
|
1284
|
+
|
|
1285
|
+
expect(createdRules).toHaveLength(2);
|
|
1286
|
+
expect(
|
|
1287
|
+
shapesCreatedFor(NotificationRuleType.ON_CALL_EXECUTED_INCIDENT),
|
|
1288
|
+
).toEqual([`userEmailId:${EMAIL_A.toString()}@0`]);
|
|
1289
|
+
});
|
|
1290
|
+
|
|
1291
|
+
/*
|
|
1292
|
+
* The fallback is only for responders who said NOTHING. Someone whose stated
|
|
1293
|
+
* channel is SMS keeps SMS even though the email map is what enumerates the
|
|
1294
|
+
* project.
|
|
1295
|
+
*/
|
|
1296
|
+
test("a responder who stated a channel keeps it instead of the email default", async () => {
|
|
1297
|
+
verifiedEmails = [{ userId: USER_A, emailId: EMAIL_A }];
|
|
1298
|
+
|
|
1299
|
+
seedRule({
|
|
1300
|
+
userId: USER_A,
|
|
1301
|
+
ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
|
|
1302
|
+
incidentSeverityId: SEV_1,
|
|
1303
|
+
userSmsId: SMS_A,
|
|
1304
|
+
notifyAfterMinutes: 0,
|
|
1305
|
+
});
|
|
1306
|
+
|
|
1307
|
+
await backfillSeverity(incidentSeverity(SEV_NEW));
|
|
1308
|
+
|
|
1309
|
+
expect(
|
|
1310
|
+
shapesCreatedFor(NotificationRuleType.ON_CALL_EXECUTED_INCIDENT),
|
|
1311
|
+
).toEqual([`userSmsId:${SMS_A.toString()}@0`]);
|
|
1312
|
+
});
|
|
1313
|
+
});
|
|
1314
|
+
|
|
1315
|
+
/*
|
|
1316
|
+
* ========================================================================= *
|
|
1317
|
+
* backfillSeverity - idempotence.
|
|
1318
|
+
*
|
|
1319
|
+
* A duplicated rule is a duplicated page. There is no unique index over
|
|
1320
|
+
* (project, user, ruleType, severity, method), so two guards do this work and
|
|
1321
|
+
* they are pinned separately: the per-run snapshot, and the per-row read taken
|
|
1322
|
+
* immediately before each write.
|
|
1323
|
+
* =========================================================================
|
|
1324
|
+
*/
|
|
1325
|
+
|
|
1326
|
+
describe("backfillSeverity is idempotent", () => {
|
|
1327
|
+
test("running the same severity a second time writes nothing", async () => {
|
|
1328
|
+
verifiedEmails = [{ userId: USER_A, emailId: EMAIL_A }];
|
|
1329
|
+
|
|
1330
|
+
seedRule({
|
|
1331
|
+
userId: USER_A,
|
|
1332
|
+
ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
|
|
1333
|
+
incidentSeverityId: SEV_1,
|
|
1334
|
+
userCallId: CALL_A,
|
|
1335
|
+
notifyAfterMinutes: 0,
|
|
1336
|
+
});
|
|
1337
|
+
|
|
1338
|
+
await backfillSeverity(incidentSeverity(SEV_NEW));
|
|
1339
|
+
const afterFirstRun: number = createdRules.length;
|
|
1340
|
+
expect(afterFirstRun).toBeGreaterThan(0);
|
|
1341
|
+
|
|
1342
|
+
await backfillSeverity(incidentSeverity(SEV_NEW));
|
|
1343
|
+
|
|
1344
|
+
expect(createdRules).toHaveLength(afterFirstRun);
|
|
1345
|
+
});
|
|
1346
|
+
|
|
1347
|
+
test("a third and fourth run still write nothing", async () => {
|
|
1348
|
+
verifiedEmails = [
|
|
1349
|
+
{ userId: USER_A, emailId: EMAIL_A },
|
|
1350
|
+
{ userId: USER_B, emailId: EMAIL_B },
|
|
1351
|
+
];
|
|
1352
|
+
|
|
1353
|
+
await backfillSeverity(incidentSeverity(SEV_NEW));
|
|
1354
|
+
const afterFirstRun: number = createdRules.length;
|
|
1355
|
+
|
|
1356
|
+
await backfillSeverity(incidentSeverity(SEV_NEW));
|
|
1357
|
+
await backfillSeverity(incidentSeverity(SEV_NEW));
|
|
1358
|
+
await backfillSeverity(incidentSeverity(SEV_NEW));
|
|
1359
|
+
|
|
1360
|
+
expect(createdRules).toHaveLength(afterFirstRun);
|
|
1361
|
+
expect(createdRules).toHaveLength(4);
|
|
1362
|
+
});
|
|
1363
|
+
|
|
1364
|
+
/*
|
|
1365
|
+
* The second guard, in isolation. Both runs took their snapshot before either
|
|
1366
|
+
* wrote, so the snapshot cannot save the loser - only the read taken
|
|
1367
|
+
* immediately before the write can, and it is the one that must fire here.
|
|
1368
|
+
* That overlap is likeliest at exactly the worst moment: a new severity
|
|
1369
|
+
* arrives by two routes at once, the by-name enqueue seconds after creation
|
|
1370
|
+
* and a scheduled sweep already part-way through the same severity.
|
|
1371
|
+
*/
|
|
1372
|
+
test("a row written by an overlapping run between the snapshot and the write is not duplicated", async () => {
|
|
1373
|
+
verifiedEmails = [{ userId: USER_A, emailId: EMAIL_A }];
|
|
1374
|
+
|
|
1375
|
+
const existing: UserNotificationRule = seedRule({
|
|
1376
|
+
userId: USER_A,
|
|
1377
|
+
ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
|
|
1378
|
+
incidentSeverityId: SEV_1,
|
|
1379
|
+
userCallId: CALL_A,
|
|
1380
|
+
notifyAfterMinutes: 0,
|
|
1381
|
+
});
|
|
1382
|
+
|
|
1383
|
+
// The snapshot BOTH overlapping runs took: nothing for the new severity yet.
|
|
1384
|
+
frozenSnapshot = [existing];
|
|
1385
|
+
|
|
1386
|
+
// One run wins the race and writes its rows...
|
|
1387
|
+
await backfillSeverity(incidentSeverity(SEV_NEW));
|
|
1388
|
+
const afterWinner: number = createdRules.length;
|
|
1389
|
+
expect(afterWinner).toBeGreaterThan(0);
|
|
1390
|
+
|
|
1391
|
+
const readsBefore: number = ruleFindOneBySpy.mock.calls.length;
|
|
1392
|
+
|
|
1393
|
+
// ...and the loser is still working from the snapshot it took earlier.
|
|
1394
|
+
await backfillSeverity(incidentSeverity(SEV_NEW));
|
|
1395
|
+
|
|
1396
|
+
expect(createdRules).toHaveLength(afterWinner);
|
|
1397
|
+
|
|
1398
|
+
/*
|
|
1399
|
+
* It really did reach the write path and get stopped there - had the stale
|
|
1400
|
+
* snapshot been what stopped it, no existence check would have been issued.
|
|
1401
|
+
*/
|
|
1402
|
+
expect(ruleFindOneBySpy.mock.calls.length).toBeGreaterThan(readsBefore);
|
|
1403
|
+
});
|
|
1404
|
+
|
|
1405
|
+
/*
|
|
1406
|
+
* The existence check keys on the delay as well as the method. A responder
|
|
1407
|
+
* who asked for "email me now" AND "email me in fifteen minutes" must get
|
|
1408
|
+
* both rows on the new severity; a key that ignored the delay would drop the
|
|
1409
|
+
* second one during the same pass that wrote the first.
|
|
1410
|
+
*/
|
|
1411
|
+
test("two rules that differ only by delay both survive the existence check", async () => {
|
|
1412
|
+
verifiedEmails = [{ userId: USER_A, emailId: EMAIL_A }];
|
|
1413
|
+
|
|
1414
|
+
seedRule({
|
|
1415
|
+
userId: USER_A,
|
|
1416
|
+
ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
|
|
1417
|
+
incidentSeverityId: SEV_1,
|
|
1418
|
+
userEmailId: EMAIL_A,
|
|
1419
|
+
notifyAfterMinutes: 0,
|
|
1420
|
+
});
|
|
1421
|
+
seedRule({
|
|
1422
|
+
userId: USER_A,
|
|
1423
|
+
ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
|
|
1424
|
+
incidentSeverityId: SEV_1,
|
|
1425
|
+
userEmailId: EMAIL_A,
|
|
1426
|
+
notifyAfterMinutes: 15,
|
|
1427
|
+
});
|
|
1428
|
+
|
|
1429
|
+
await backfillSeverity(incidentSeverity(SEV_NEW));
|
|
1430
|
+
|
|
1431
|
+
const shapes: Array<string> = shapesCreatedFor(
|
|
1432
|
+
NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
|
|
1433
|
+
);
|
|
1434
|
+
|
|
1435
|
+
expect(shapes.sort()).toEqual([
|
|
1436
|
+
`userEmailId:${EMAIL_A.toString()}@0`,
|
|
1437
|
+
`userEmailId:${EMAIL_A.toString()}@15`,
|
|
1438
|
+
]);
|
|
1439
|
+
});
|
|
1440
|
+
|
|
1441
|
+
test("a responder already covered by hand is left completely alone", async () => {
|
|
1442
|
+
verifiedEmails = [{ userId: USER_A, emailId: EMAIL_A }];
|
|
1443
|
+
|
|
1444
|
+
seedRule({
|
|
1445
|
+
userId: USER_A,
|
|
1446
|
+
ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
|
|
1447
|
+
incidentSeverityId: SEV_1,
|
|
1448
|
+
userCallId: CALL_A,
|
|
1449
|
+
notifyAfterMinutes: 0,
|
|
1450
|
+
});
|
|
1451
|
+
// They already wrote their own rule for the new severity.
|
|
1452
|
+
seedRule({
|
|
1453
|
+
userId: USER_A,
|
|
1454
|
+
ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
|
|
1455
|
+
incidentSeverityId: SEV_NEW,
|
|
1456
|
+
userEmailId: EMAIL_A,
|
|
1457
|
+
notifyAfterMinutes: 45,
|
|
1458
|
+
});
|
|
1459
|
+
|
|
1460
|
+
await backfillSeverity(incidentSeverity(SEV_NEW));
|
|
1461
|
+
|
|
1462
|
+
expect(
|
|
1463
|
+
createdOfType(NotificationRuleType.ON_CALL_EXECUTED_INCIDENT),
|
|
1464
|
+
).toHaveLength(0);
|
|
1465
|
+
});
|
|
1466
|
+
|
|
1467
|
+
/*
|
|
1468
|
+
* The existence check has to name the new severity, or a rule for Sev1 would
|
|
1469
|
+
* satisfy it and the new severity would stay uncovered forever - which is the
|
|
1470
|
+
* original bug wearing a different hat.
|
|
1471
|
+
*/
|
|
1472
|
+
test("the pre-write check names the whole row: project, user, type, severity, method and delay", async () => {
|
|
1473
|
+
verifiedEmails = [];
|
|
1474
|
+
|
|
1475
|
+
seedRule({
|
|
1476
|
+
userId: USER_A,
|
|
1477
|
+
ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
|
|
1478
|
+
incidentSeverityId: SEV_1,
|
|
1479
|
+
userCallId: CALL_A,
|
|
1480
|
+
notifyAfterMinutes: 20,
|
|
1481
|
+
});
|
|
1482
|
+
|
|
1483
|
+
await backfillSeverity(incidentSeverity(SEV_NEW));
|
|
1484
|
+
|
|
1485
|
+
const query: Record<string, unknown> = queryOf(ruleFindOneBySpy, 0);
|
|
1486
|
+
|
|
1487
|
+
expect(idOf(query["projectId"])).toBe(PROJECT_ID.toString());
|
|
1488
|
+
expect(idOf(query["userId"])).toBe(USER_A.toString());
|
|
1489
|
+
expect(query["ruleType"]).toBe(
|
|
1490
|
+
NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
|
|
1491
|
+
);
|
|
1492
|
+
expect(idOf(query["incidentSeverityId"])).toBe(SEV_NEW.toString());
|
|
1493
|
+
expect(idOf(query["userCallId"])).toBe(CALL_A.toString());
|
|
1494
|
+
expect(query["notifyAfterMinutes"]).toBe(20);
|
|
1495
|
+
expect(query["alertSeverityId"]).toBeUndefined();
|
|
1496
|
+
expect(propsOf(ruleFindOneBySpy, 0).isRoot).toBe(true);
|
|
1497
|
+
});
|
|
1498
|
+
|
|
1499
|
+
test("an alert backfill's existence check names alertSeverityId instead", async () => {
|
|
1500
|
+
verifiedEmails = [{ userId: USER_A, emailId: EMAIL_A }];
|
|
1501
|
+
|
|
1502
|
+
await backfillSeverity(alertSeverity(ALERT_SEV_NEW));
|
|
1503
|
+
|
|
1504
|
+
const query: Record<string, unknown> = queryOf(ruleFindOneBySpy, 0);
|
|
1505
|
+
|
|
1506
|
+
expect(idOf(query["alertSeverityId"])).toBe(ALERT_SEV_NEW.toString());
|
|
1507
|
+
expect(query["incidentSeverityId"]).toBeUndefined();
|
|
1508
|
+
});
|
|
1509
|
+
|
|
1510
|
+
test("an opt-out row is not duplicated on a second run either", async () => {
|
|
1511
|
+
verifiedEmails = [{ userId: USER_A, emailId: EMAIL_A }];
|
|
1512
|
+
|
|
1513
|
+
seedRule({
|
|
1514
|
+
userId: USER_A,
|
|
1515
|
+
ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
|
|
1516
|
+
incidentSeverityId: SEV_1,
|
|
1517
|
+
isOptOut: true,
|
|
1518
|
+
notifyAfterMinutes: 0,
|
|
1519
|
+
});
|
|
1520
|
+
|
|
1521
|
+
await backfillSeverity(incidentSeverity(SEV_NEW));
|
|
1522
|
+
const afterFirstRun: number = createdRules.length;
|
|
1523
|
+
|
|
1524
|
+
await backfillSeverity(incidentSeverity(SEV_NEW));
|
|
1525
|
+
|
|
1526
|
+
expect(createdRules).toHaveLength(afterFirstRun);
|
|
1527
|
+
expect(
|
|
1528
|
+
createdOfType(NotificationRuleType.ON_CALL_EXECUTED_INCIDENT),
|
|
1529
|
+
).toHaveLength(1);
|
|
1530
|
+
});
|
|
1531
|
+
});
|
|
1532
|
+
|
|
1533
|
+
/*
|
|
1534
|
+
* ========================================================================= *
|
|
1535
|
+
* Alert severities take the identical route.
|
|
1536
|
+
* =========================================================================
|
|
1537
|
+
*/
|
|
1538
|
+
|
|
1539
|
+
describe("backfillSeverity covers alert severities the same way", () => {
|
|
1540
|
+
test("an alert severity mirrors the responder's alert rules onto alertSeverityId", async () => {
|
|
1541
|
+
verifiedEmails = [{ userId: USER_A, emailId: EMAIL_A }];
|
|
1542
|
+
|
|
1543
|
+
seedRule({
|
|
1544
|
+
userId: USER_A,
|
|
1545
|
+
ruleType: NotificationRuleType.ON_CALL_EXECUTED_ALERT,
|
|
1546
|
+
alertSeverityId: ALERT_SEV_1,
|
|
1547
|
+
userPushId: PUSH_A,
|
|
1548
|
+
notifyAfterMinutes: 0,
|
|
1549
|
+
});
|
|
1550
|
+
|
|
1551
|
+
await backfillSeverity(alertSeverity(ALERT_SEV_NEW));
|
|
1552
|
+
|
|
1553
|
+
const created: Array<UserNotificationRule> = createdOfType(
|
|
1554
|
+
NotificationRuleType.ON_CALL_EXECUTED_ALERT,
|
|
1555
|
+
);
|
|
1556
|
+
|
|
1557
|
+
expect(created).toHaveLength(1);
|
|
1558
|
+
expect(created[0]!.alertSeverityId!.toString()).toBe(
|
|
1559
|
+
ALERT_SEV_NEW.toString(),
|
|
1560
|
+
);
|
|
1561
|
+
expect(created[0]!.incidentSeverityId).toBeUndefined();
|
|
1562
|
+
expect(methodOf(created[0]!)).toBe(`userPushId:${PUSH_A.toString()}`);
|
|
1563
|
+
});
|
|
1564
|
+
|
|
1565
|
+
test("an alert severity covers both alert rule types and no incident one", async () => {
|
|
1566
|
+
verifiedEmails = [{ userId: USER_A, emailId: EMAIL_A }];
|
|
1567
|
+
|
|
1568
|
+
await backfillSeverity(alertSeverity(ALERT_SEV_NEW));
|
|
1569
|
+
|
|
1570
|
+
const typesCovered: Array<NotificationRuleType | undefined> =
|
|
1571
|
+
createdRules.map(
|
|
1572
|
+
(rule: UserNotificationRule): NotificationRuleType | undefined => {
|
|
1573
|
+
return rule.ruleType;
|
|
1574
|
+
},
|
|
1575
|
+
);
|
|
1576
|
+
|
|
1577
|
+
expect(typesCovered.sort()).toEqual([...ALERT_RULE_TYPES].sort());
|
|
1578
|
+
});
|
|
1579
|
+
|
|
1580
|
+
test("an incident backfill never writes an alert rule type", async () => {
|
|
1581
|
+
verifiedEmails = [{ userId: USER_A, emailId: EMAIL_A }];
|
|
1582
|
+
|
|
1583
|
+
seedRule({
|
|
1584
|
+
userId: USER_A,
|
|
1585
|
+
ruleType: NotificationRuleType.ON_CALL_EXECUTED_ALERT,
|
|
1586
|
+
alertSeverityId: ALERT_SEV_1,
|
|
1587
|
+
userPushId: PUSH_A,
|
|
1588
|
+
notifyAfterMinutes: 0,
|
|
1589
|
+
});
|
|
1590
|
+
|
|
1591
|
+
await backfillSeverity(incidentSeverity(SEV_NEW));
|
|
1592
|
+
|
|
1593
|
+
for (const ruleType of ALERT_RULE_TYPES) {
|
|
1594
|
+
expect(createdOfType(ruleType)).toHaveLength(0);
|
|
1595
|
+
}
|
|
1596
|
+
});
|
|
1597
|
+
|
|
1598
|
+
test("alert severities are idempotent on a second run too", async () => {
|
|
1599
|
+
verifiedEmails = [{ userId: USER_A, emailId: EMAIL_A }];
|
|
1600
|
+
|
|
1601
|
+
await backfillSeverity(alertSeverity(ALERT_SEV_NEW));
|
|
1602
|
+
const afterFirstRun: number = createdRules.length;
|
|
1603
|
+
|
|
1604
|
+
await backfillSeverity(alertSeverity(ALERT_SEV_NEW));
|
|
1605
|
+
|
|
1606
|
+
expect(createdRules).toHaveLength(afterFirstRun);
|
|
1607
|
+
});
|
|
1608
|
+
});
|
|
1609
|
+
|
|
1610
|
+
/*
|
|
1611
|
+
* ========================================================================= *
|
|
1612
|
+
* The fan-out: bounded reads, and failure isolation.
|
|
1613
|
+
* =========================================================================
|
|
1614
|
+
*/
|
|
1615
|
+
|
|
1616
|
+
describe("the fan-out is bounded and does not collapse on one responder", () => {
|
|
1617
|
+
function seedProjectWithUsers(count: number): void {
|
|
1618
|
+
verifiedEmails = [];
|
|
1619
|
+
|
|
1620
|
+
for (let index: number = 0; index < count; index++) {
|
|
1621
|
+
verifiedEmails.push({
|
|
1622
|
+
userId: new ObjectID(`bulk-user-${index}`),
|
|
1623
|
+
emailId: new ObjectID(`bulk-user-email-${index}`),
|
|
1624
|
+
});
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
/*
|
|
1629
|
+
* The N+1 this job was written to avoid: looping the project's responders and
|
|
1630
|
+
* asking the database once per responder per severity. Reads must be a
|
|
1631
|
+
* function of the RULE TYPES, not of the head-count.
|
|
1632
|
+
*/
|
|
1633
|
+
test("reads do not grow with the number of responders", async () => {
|
|
1634
|
+
seedProjectWithUsers(25);
|
|
1635
|
+
|
|
1636
|
+
await backfillSeverity(incidentSeverity(SEV_NEW));
|
|
1637
|
+
|
|
1638
|
+
expect(createdRules).toHaveLength(50);
|
|
1639
|
+
|
|
1640
|
+
// One responder read for the severity, one rule read per rule type.
|
|
1641
|
+
expect(userEmailFindAllBySpy).toHaveBeenCalledTimes(1);
|
|
1642
|
+
expect(ruleFindAllBySpy).toHaveBeenCalledTimes(INCIDENT_RULE_TYPES.length);
|
|
1643
|
+
});
|
|
1644
|
+
|
|
1645
|
+
test("the project-wide rule read is scoped to the project and one rule type", async () => {
|
|
1646
|
+
seedProjectWithUsers(3);
|
|
1647
|
+
|
|
1648
|
+
await backfillSeverity(incidentSeverity(SEV_NEW));
|
|
1649
|
+
|
|
1650
|
+
const ruleTypesRead: Array<unknown> = ruleFindAllBySpy.mock.calls.map(
|
|
1651
|
+
(call: Array<unknown>): unknown => {
|
|
1652
|
+
return (call[0] as { query: Record<string, unknown> }).query[
|
|
1653
|
+
"ruleType"
|
|
1654
|
+
];
|
|
1655
|
+
},
|
|
1656
|
+
);
|
|
1657
|
+
|
|
1658
|
+
expect(ruleTypesRead.sort()).toEqual([...INCIDENT_RULE_TYPES].sort());
|
|
1659
|
+
expect(idOf(queryOf(ruleFindAllBySpy, 0)["projectId"])).toBe(
|
|
1660
|
+
PROJECT_ID.toString(),
|
|
1661
|
+
);
|
|
1662
|
+
expect(propsOf(ruleFindAllBySpy, 0).isRoot).toBe(true);
|
|
1663
|
+
});
|
|
1664
|
+
|
|
1665
|
+
/*
|
|
1666
|
+
* findAllBy is the paging reader: it walks the table in LIMIT_MAX batches
|
|
1667
|
+
* rather than issuing one unbounded query, which is what keeps a project with
|
|
1668
|
+
* more responders than fit in a single page from being read in one gulp. The
|
|
1669
|
+
* real findAllBy runs here; only the underlying page read is stubbed.
|
|
1670
|
+
*/
|
|
1671
|
+
test("responders are read in bounded pages, not one unbounded query", async () => {
|
|
1672
|
+
userEmailFindAllBySpy.mockRestore();
|
|
1673
|
+
|
|
1674
|
+
const singleRow: UserEmail = userEmailRow(USER_A, EMAIL_A);
|
|
1675
|
+
let pageIndex: number = 0;
|
|
1676
|
+
|
|
1677
|
+
const emailFindBySpy: jest.SpyInstance = jest
|
|
1678
|
+
.spyOn(UserEmailService, "findBy")
|
|
1679
|
+
.mockImplementation(((): Promise<Array<UserEmail>> => {
|
|
1680
|
+
pageIndex++;
|
|
1681
|
+
|
|
1682
|
+
if (pageIndex === 1) {
|
|
1683
|
+
// A full page, which is what tells findAllBy to ask for another.
|
|
1684
|
+
return Promise.resolve(
|
|
1685
|
+
new Array<UserEmail>(LIMIT_MAX).fill(singleRow),
|
|
1686
|
+
);
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
return Promise.resolve([]);
|
|
1690
|
+
}) as never);
|
|
1691
|
+
|
|
1692
|
+
await backfillSeverity(incidentSeverity(SEV_NEW));
|
|
1693
|
+
|
|
1694
|
+
expect(emailFindBySpy).toHaveBeenCalledTimes(2);
|
|
1695
|
+
|
|
1696
|
+
const firstPage: { skip: number; limit: number } = emailFindBySpy.mock
|
|
1697
|
+
.calls[0]![0] as { skip: number; limit: number };
|
|
1698
|
+
const secondPage: { skip: number; limit: number } = emailFindBySpy.mock
|
|
1699
|
+
.calls[1]![0] as { skip: number; limit: number };
|
|
1700
|
+
|
|
1701
|
+
expect(firstPage.skip).toBe(0);
|
|
1702
|
+
expect(firstPage.limit).toBe(LIMIT_MAX);
|
|
1703
|
+
expect(secondPage.skip).toBe(LIMIT_MAX);
|
|
1704
|
+
expect(secondPage.limit).toBe(LIMIT_MAX);
|
|
1705
|
+
|
|
1706
|
+
// The page was one responder repeated, so one responder is covered.
|
|
1707
|
+
expect(createdRules).toHaveLength(INCIDENT_RULE_TYPES.length);
|
|
1708
|
+
});
|
|
1709
|
+
|
|
1710
|
+
/*
|
|
1711
|
+
* The likeliest real failure: a method row deleted between the existence
|
|
1712
|
+
* check and the write, which cascades the foreign key away. That is worth a
|
|
1713
|
+
* log line for one person, not an aborted backfill for the whole project.
|
|
1714
|
+
*/
|
|
1715
|
+
test("a write that fails for one responder does not stop the others", async () => {
|
|
1716
|
+
verifiedEmails = [
|
|
1717
|
+
{ userId: USER_A, emailId: EMAIL_A },
|
|
1718
|
+
{ userId: USER_B, emailId: EMAIL_B },
|
|
1719
|
+
{ userId: USER_C, emailId: EMAIL_C },
|
|
1720
|
+
];
|
|
1721
|
+
|
|
1722
|
+
ruleCreateSpy.mockImplementation(((data: {
|
|
1723
|
+
data: UserNotificationRule;
|
|
1724
|
+
}): Promise<UserNotificationRule> => {
|
|
1725
|
+
const rule: UserNotificationRule = data.data;
|
|
1726
|
+
|
|
1727
|
+
if (rule.userId && rule.userId.toString() === USER_B.toString()) {
|
|
1728
|
+
return Promise.reject(
|
|
1729
|
+
new Error("insert or update violates foreign key constraint"),
|
|
1730
|
+
);
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
rule._id = `created-rule-${createdRules.length + 1}`;
|
|
1734
|
+
ruleStore.push(rule);
|
|
1735
|
+
createdRules.push(rule);
|
|
1736
|
+
|
|
1737
|
+
return Promise.resolve(rule);
|
|
1738
|
+
}) as never);
|
|
1739
|
+
|
|
1740
|
+
await expect(
|
|
1741
|
+
backfillSeverity(incidentSeverity(SEV_NEW)),
|
|
1742
|
+
).resolves.toBeUndefined();
|
|
1743
|
+
|
|
1744
|
+
const usersCovered: Array<string> = createdRules.map(
|
|
1745
|
+
(rule: UserNotificationRule): string => {
|
|
1746
|
+
return rule.userId!.toString();
|
|
1747
|
+
},
|
|
1748
|
+
);
|
|
1749
|
+
|
|
1750
|
+
expect(usersCovered).toContain(USER_A.toString());
|
|
1751
|
+
expect(usersCovered).toContain(USER_C.toString());
|
|
1752
|
+
expect(usersCovered).not.toContain(USER_B.toString());
|
|
1753
|
+
expect(createdRules).toHaveLength(4);
|
|
1754
|
+
|
|
1755
|
+
// And the responder who was missed is named, not silently dropped.
|
|
1756
|
+
const logged: Array<string> = loggerErrorSpy.mock.calls.map(
|
|
1757
|
+
(call: Array<unknown>): string => {
|
|
1758
|
+
return String(call[0]);
|
|
1759
|
+
},
|
|
1760
|
+
);
|
|
1761
|
+
|
|
1762
|
+
expect(
|
|
1763
|
+
logged.some((message: string): boolean => {
|
|
1764
|
+
return message.includes(USER_B.toString());
|
|
1765
|
+
}),
|
|
1766
|
+
).toBe(true);
|
|
1767
|
+
});
|
|
1768
|
+
|
|
1769
|
+
/*
|
|
1770
|
+
* The boundary of that isolation, pinned so a future reader knows where it
|
|
1771
|
+
* is: only the WRITE is wrapped. A failing existence READ propagates and ends
|
|
1772
|
+
* this severity's pass. It is not a lost page - the sweep re-derives its work
|
|
1773
|
+
* set from an hour-wide window every five minutes, so the responders after
|
|
1774
|
+
* the failure are picked up by a later tick - but the rest of THIS pass does
|
|
1775
|
+
* not happen.
|
|
1776
|
+
*/
|
|
1777
|
+
test("a failing existence read ends this pass, leaving the rest to the next sweep", async () => {
|
|
1778
|
+
verifiedEmails = [
|
|
1779
|
+
{ userId: USER_A, emailId: EMAIL_A },
|
|
1780
|
+
{ userId: USER_B, emailId: EMAIL_B },
|
|
1781
|
+
{ userId: USER_C, emailId: EMAIL_C },
|
|
1782
|
+
];
|
|
1783
|
+
|
|
1784
|
+
ruleFindOneBySpy.mockImplementation(((data: {
|
|
1785
|
+
query: Record<string, unknown>;
|
|
1786
|
+
}): Promise<UserNotificationRule | null> => {
|
|
1787
|
+
if (idOf(data.query["userId"]) === USER_B.toString()) {
|
|
1788
|
+
return Promise.reject(new Error("connection terminated unexpectedly"));
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
return Promise.resolve(matchingRules(ruleStore, data.query)[0] || null);
|
|
1792
|
+
}) as never);
|
|
1793
|
+
|
|
1794
|
+
await expect(backfillSeverity(incidentSeverity(SEV_NEW))).rejects.toThrow(
|
|
1795
|
+
"connection terminated unexpectedly",
|
|
1796
|
+
);
|
|
1797
|
+
|
|
1798
|
+
const usersCovered: Array<string> = createdRules.map(
|
|
1799
|
+
(rule: UserNotificationRule): string => {
|
|
1800
|
+
return rule.userId!.toString();
|
|
1801
|
+
},
|
|
1802
|
+
);
|
|
1803
|
+
|
|
1804
|
+
expect(usersCovered).toEqual([USER_A.toString()]);
|
|
1805
|
+
});
|
|
1806
|
+
|
|
1807
|
+
test("a project with nobody in it is a pair of reads and no writes", async () => {
|
|
1808
|
+
verifiedEmails = [];
|
|
1809
|
+
|
|
1810
|
+
await expect(
|
|
1811
|
+
backfillSeverity(incidentSeverity(SEV_NEW)),
|
|
1812
|
+
).resolves.toBeUndefined();
|
|
1813
|
+
|
|
1814
|
+
expect(createdRules).toHaveLength(0);
|
|
1815
|
+
expect(ruleCreateSpy).not.toHaveBeenCalled();
|
|
1816
|
+
expect(ruleFindOneBySpy).not.toHaveBeenCalled();
|
|
1817
|
+
});
|
|
1818
|
+
});
|