@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,2803 @@
|
|
|
1
|
+
import AlertSeverityService from "./AlertSeverityService";
|
|
2
|
+
import IncidentSeverityService from "./IncidentSeverityService";
|
|
3
|
+
import OnCallDutyPolicyEscalationRuleScheduleService from "./OnCallDutyPolicyEscalationRuleScheduleService";
|
|
4
|
+
import OnCallDutyPolicyEscalationRuleTeamService from "./OnCallDutyPolicyEscalationRuleTeamService";
|
|
5
|
+
import OnCallDutyPolicyEscalationRuleUserService from "./OnCallDutyPolicyEscalationRuleUserService";
|
|
6
|
+
import OnCallDutyPolicyScheduleLayerUserService from "./OnCallDutyPolicyScheduleLayerUserService";
|
|
7
|
+
import OnCallDutyPolicyService from "./OnCallDutyPolicyService";
|
|
8
|
+
import OnCallDutyPolicyUserOverrideService from "./OnCallDutyPolicyUserOverrideService";
|
|
9
|
+
import ProjectService from "./ProjectService";
|
|
10
|
+
import TeamMemberService from "./TeamMemberService";
|
|
11
|
+
import TeamService from "./TeamService";
|
|
12
|
+
import UserCallService from "./UserCallService";
|
|
13
|
+
import UserEmailService from "./UserEmailService";
|
|
14
|
+
import UserNotificationRuleService from "./UserNotificationRuleService";
|
|
15
|
+
import UserPushService from "./UserPushService";
|
|
16
|
+
import UserService from "./UserService";
|
|
17
|
+
import UserSmsService from "./UserSmsService";
|
|
18
|
+
import UserTelegramService from "./UserTelegramService";
|
|
19
|
+
import UserWebhookService from "./UserWebhookService";
|
|
20
|
+
import UserWhatsAppService from "./UserWhatsAppService";
|
|
21
|
+
import InMemoryTTLCache from "../Infrastructure/InMemoryTTLCache";
|
|
22
|
+
import FindBy from "../Types/Database/FindBy";
|
|
23
|
+
import Query from "../Types/Database/Query";
|
|
24
|
+
import QueryHelper from "../Types/Database/QueryHelper";
|
|
25
|
+
import Select from "../Types/Database/Select";
|
|
26
|
+
import Sort from "../Types/Database/Sort";
|
|
27
|
+
import logger from "../Utils/Logger";
|
|
28
|
+
import Includes from "../../Types/BaseDatabase/Includes";
|
|
29
|
+
import SortOrder from "../../Types/BaseDatabase/SortOrder";
|
|
30
|
+
import { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
|
|
31
|
+
import OneUptimeDate from "../../Types/Date";
|
|
32
|
+
import BadDataException from "../../Types/Exception/BadDataException";
|
|
33
|
+
import NotificationRuleType from "../../Types/NotificationRule/NotificationRuleType";
|
|
34
|
+
import ObjectID from "../../Types/ObjectID";
|
|
35
|
+
import AlertSeverity from "../../Models/DatabaseModels/AlertSeverity";
|
|
36
|
+
import DatabaseBaseModel from "../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
|
|
37
|
+
import IncidentSeverity from "../../Models/DatabaseModels/IncidentSeverity";
|
|
38
|
+
import OnCallDutyPolicy from "../../Models/DatabaseModels/OnCallDutyPolicy";
|
|
39
|
+
import OnCallDutyPolicyEscalationRuleSchedule from "../../Models/DatabaseModels/OnCallDutyPolicyEscalationRuleSchedule";
|
|
40
|
+
import OnCallDutyPolicyEscalationRuleTeam from "../../Models/DatabaseModels/OnCallDutyPolicyEscalationRuleTeam";
|
|
41
|
+
import OnCallDutyPolicyEscalationRuleUser from "../../Models/DatabaseModels/OnCallDutyPolicyEscalationRuleUser";
|
|
42
|
+
import OnCallDutyPolicyScheduleLayerUser from "../../Models/DatabaseModels/OnCallDutyPolicyScheduleLayerUser";
|
|
43
|
+
import OnCallDutyPolicyUserOverride from "../../Models/DatabaseModels/OnCallDutyPolicyUserOverride";
|
|
44
|
+
import Project from "../../Models/DatabaseModels/Project";
|
|
45
|
+
import Team from "../../Models/DatabaseModels/Team";
|
|
46
|
+
import TeamMember from "../../Models/DatabaseModels/TeamMember";
|
|
47
|
+
import User from "../../Models/DatabaseModels/User";
|
|
48
|
+
import UserCall from "../../Models/DatabaseModels/UserCall";
|
|
49
|
+
import UserEmail from "../../Models/DatabaseModels/UserEmail";
|
|
50
|
+
import UserNotificationRule from "../../Models/DatabaseModels/UserNotificationRule";
|
|
51
|
+
import UserPush from "../../Models/DatabaseModels/UserPush";
|
|
52
|
+
import UserSMS from "../../Models/DatabaseModels/UserSMS";
|
|
53
|
+
import UserTelegram from "../../Models/DatabaseModels/UserTelegram";
|
|
54
|
+
import UserWebhook from "../../Models/DatabaseModels/UserWebhook";
|
|
55
|
+
import UserWhatsApp from "../../Models/DatabaseModels/UserWhatsApp";
|
|
56
|
+
|
|
57
|
+
/*
|
|
58
|
+
* "Can this responder actually be paged?" — computed, not configured.
|
|
59
|
+
*
|
|
60
|
+
* This is the single place that answers that question, and every readiness surface in
|
|
61
|
+
* the product renders what it returns. It exists because the question was previously
|
|
62
|
+
* answered by TeamComplianceService, which answered it wrongly in seven separate ways —
|
|
63
|
+
* each of which is a real defect with a real missed page behind it, and each of which is
|
|
64
|
+
* fixed here deliberately rather than incidentally:
|
|
65
|
+
*
|
|
66
|
+
* 1. It was opt-in per team and OFF by default, so the common case was no answer at
|
|
67
|
+
* all. Readiness here is always computed; there is nothing to switch on.
|
|
68
|
+
* 2. It was TEAM-SCOPED. A user attached directly to an escalation rule, reached
|
|
69
|
+
* through a schedule layer, or substituted in by a user override was never checked —
|
|
70
|
+
* which is to say the three ways a responder most often gets paged were invisible.
|
|
71
|
+
* resolveResponders below is the union of all four, and the union is the point.
|
|
72
|
+
* 3. It capped members and users at a bare `limit: 100`, silently truncating a large
|
|
73
|
+
* project into a comfortable lie. NOTHING here is capped: every read goes through
|
|
74
|
+
* readEveryPage, which pages until the table is exhausted. See the essay on that
|
|
75
|
+
* method for why raising the cap instead would have been the same bug with a bigger
|
|
76
|
+
* number — a responder who falls off the end of a page is reported in no count, no
|
|
77
|
+
* list and no summary, which is indistinguishable from a responder who is fine.
|
|
78
|
+
* 4. It ignored `ruleType`, so a "when I go off call" rule counted as incident
|
|
79
|
+
* coverage. Coverage below is keyed on (userId, ruleType, severityId); a rule for
|
|
80
|
+
* the wrong ruleType covers nothing.
|
|
81
|
+
* 5. It counted only call/SMS/email/push, so a responder whose only method was
|
|
82
|
+
* Telegram, WhatsApp or Webhook was reported non-compliant while being perfectly
|
|
83
|
+
* reachable. All seven channels count here.
|
|
84
|
+
* 6. It ran one findBy per severity per user. Every read below is batched with
|
|
85
|
+
* Includes(userIds); the query count is constant in the number of responders and
|
|
86
|
+
* in the number of severities. It grows only with the number of PAGES of rows that
|
|
87
|
+
* come back, which is the unavoidable price of not truncating.
|
|
88
|
+
*
|
|
89
|
+
* The seventh defect was that it was read-only prose. This service does not fix that on
|
|
90
|
+
* its own, but everything it returns is shaped to be acted on: `reasons` are sentences
|
|
91
|
+
* naming a specific missing thing, and `coverage` is a grid an admin can fix cell by
|
|
92
|
+
* cell.
|
|
93
|
+
*/
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Ready — every coverage cell either has a rule or is explicitly muted.
|
|
97
|
+
* PartiallyReady — reachable, but at least one cell falls back.
|
|
98
|
+
* NotReachable — zero USABLE notification methods; nothing will reach this person.
|
|
99
|
+
*/
|
|
100
|
+
export enum ReadinessStatus {
|
|
101
|
+
Ready = "Ready",
|
|
102
|
+
PartiallyReady = "PartiallyReady",
|
|
103
|
+
NotReachable = "NotReachable",
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* WHY a user is on this policy. A user reached two ways carries both sources, because
|
|
108
|
+
* removing them from one attachment does not stop them being paged through the other —
|
|
109
|
+
* an admin looking at an unreachable responder needs to know every door they came in by.
|
|
110
|
+
*/
|
|
111
|
+
export enum ResponderSource {
|
|
112
|
+
Direct = "Direct",
|
|
113
|
+
Team = "Team",
|
|
114
|
+
Schedule = "Schedule",
|
|
115
|
+
Override = "Override",
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The seven channels a page can be delivered on. These strings are the same literals the
|
|
120
|
+
* fallback uses for `channelsUsed` (UserNotificationRuleService.chooseFallbackChannels),
|
|
121
|
+
* deliberately: an operator reading "notified via fallback (Push, Email)" in an execution
|
|
122
|
+
* log and "Push, Email" in the readiness table must not have to translate between two
|
|
123
|
+
* vocabularies for the same thing.
|
|
124
|
+
*/
|
|
125
|
+
export enum ReadinessMethodType {
|
|
126
|
+
Push = "Push",
|
|
127
|
+
Email = "Email",
|
|
128
|
+
SMS = "SMS",
|
|
129
|
+
Call = "Call",
|
|
130
|
+
WhatsApp = "WhatsApp",
|
|
131
|
+
Telegram = "Telegram",
|
|
132
|
+
Webhook = "Webhook",
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface ReadinessMethod {
|
|
136
|
+
/**
|
|
137
|
+
* The id of the METHOD ROW itself — UserSMS._id, UserEmail._id and so on — which is
|
|
138
|
+
* exactly what UserNotificationRule.userSmsId / userEmailId / ... reference.
|
|
139
|
+
*
|
|
140
|
+
* It is here so that an administrator can POINT A RULE AT a method without READING that
|
|
141
|
+
* method's row, which is the whole difficulty. The seven method models are scoped to
|
|
142
|
+
* their owner: nobody but the owner may read a UserSMS, and that is deliberate, because
|
|
143
|
+
* the columns behind it are the raw phone number, the webhook bearer url, the push
|
|
144
|
+
* device token, the telegram chat id and the verification code. Widening that scope so
|
|
145
|
+
* an admin could populate a dropdown was tried, and the exposure it opened could not be
|
|
146
|
+
* contained; this field is what replaces it.
|
|
147
|
+
*
|
|
148
|
+
* A foreign key is not a secret. It is already stored in plain sight on every rule its
|
|
149
|
+
* owner has created, and it addresses nothing on its own — you cannot page a uuid.
|
|
150
|
+
* Carrying it alongside the mask is therefore the entire trick: the rule form renders
|
|
151
|
+
* "SMS ending 4821" and submits userSmsId, and the number itself never leaves the
|
|
152
|
+
* server for a caller who is not its owner.
|
|
153
|
+
*
|
|
154
|
+
* Which is also why NOTHING ELSE about the row belongs on this interface. Every field
|
|
155
|
+
* added here is a field that ships to every administrator of the project.
|
|
156
|
+
*/
|
|
157
|
+
methodId: ObjectID;
|
|
158
|
+
methodType: string;
|
|
159
|
+
/**
|
|
160
|
+
* ALWAYS masked, by construction — see maskIdentifier. Never the raw value.
|
|
161
|
+
*/
|
|
162
|
+
maskedIdentifier: string;
|
|
163
|
+
isVerified: boolean;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export interface ReadinessCoverageCell {
|
|
167
|
+
ruleType: NotificationRuleType;
|
|
168
|
+
/**
|
|
169
|
+
* Undefined for the two go-on/off-call rule types, which are about the user's shift
|
|
170
|
+
* rather than about anything that fired and so legitimately carry no severity. This
|
|
171
|
+
* service currently emits cells only for the four ON_CALL_EXECUTED_* types, all of
|
|
172
|
+
* which are severity-scoped, so in practice this is always set — the optionality is
|
|
173
|
+
* kept so a later surface can add handoff cells without changing the contract.
|
|
174
|
+
*/
|
|
175
|
+
severityId?: ObjectID | undefined;
|
|
176
|
+
severityName?: string | undefined;
|
|
177
|
+
hasRule: boolean;
|
|
178
|
+
isOptOut: boolean;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* One team that routes pages to a responder.
|
|
183
|
+
*
|
|
184
|
+
* "Routes pages to" is narrower than "is a member of", and the difference is the whole
|
|
185
|
+
* point. A project team with no escalation rule attached to it does not page anybody, so
|
|
186
|
+
* naming it here would let an admin filter the readiness table down to a team and read a
|
|
187
|
+
* clean answer about people that team cannot actually reach. Every team named on a
|
|
188
|
+
* responder is a team that (a) they belong to and (b) is attached to an escalation rule in
|
|
189
|
+
* the scope being computed — which is exactly the set that makes `reachedVia` contain
|
|
190
|
+
* `Team`.
|
|
191
|
+
*/
|
|
192
|
+
export interface ReadinessTeam {
|
|
193
|
+
_id: ObjectID;
|
|
194
|
+
name: string;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export interface UserReadiness {
|
|
198
|
+
userId: ObjectID;
|
|
199
|
+
userName: string;
|
|
200
|
+
/**
|
|
201
|
+
* The LOGIN email, not a notification method, and deliberately not masked: it is
|
|
202
|
+
* already admin-readable everywhere a user is listed in the product, and masking it
|
|
203
|
+
* here would make the readiness table the one place an admin cannot tell two people
|
|
204
|
+
* called "J. Smith" apart. The notification email in `methods` IS masked — those are
|
|
205
|
+
* different values with different exposure, even when they happen to be equal.
|
|
206
|
+
*/
|
|
207
|
+
userEmail: string;
|
|
208
|
+
userProfilePictureId?: ObjectID | undefined;
|
|
209
|
+
status: ReadinessStatus;
|
|
210
|
+
methods: Array<ReadinessMethod>;
|
|
211
|
+
coverage: Array<ReadinessCoverageCell>;
|
|
212
|
+
reasons: Array<string>;
|
|
213
|
+
reachedVia: Array<ResponderSource>;
|
|
214
|
+
/**
|
|
215
|
+
* The teams that page this responder, in name order. Empty whenever `reachedVia` does
|
|
216
|
+
* not contain `Team` — a responder attached directly or through a schedule is reached
|
|
217
|
+
* without a team being involved, and saying otherwise would put them under a team
|
|
218
|
+
* filter they are not answerable to.
|
|
219
|
+
*/
|
|
220
|
+
teams: Array<ReadinessTeam>;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export interface ReadinessSummary {
|
|
224
|
+
projectId: ObjectID;
|
|
225
|
+
onCallDutyPolicyId?: ObjectID | undefined;
|
|
226
|
+
readyCount: number;
|
|
227
|
+
partiallyReadyCount: number;
|
|
228
|
+
notReachableCount: number;
|
|
229
|
+
/**
|
|
230
|
+
* Whether a page with no matching rule falls back to the responder's verified methods,
|
|
231
|
+
* or is dropped on the floor.
|
|
232
|
+
*
|
|
233
|
+
* On the wire because the UI has to stop promising "those pages still reach them
|
|
234
|
+
* through the fallback, so nothing is dropped" for a project that has
|
|
235
|
+
* `disableOnCallNotificationFallback` set. That sentence is comforting and, for such a
|
|
236
|
+
* project, false — and it is shown at the exact moment an admin is deciding whether a
|
|
237
|
+
* PartiallyReady responder is worth chasing. The per-user `reasons` already say it in
|
|
238
|
+
* prose; this is the same fact as a boolean, so a chip or a banner can render it
|
|
239
|
+
* without parsing English.
|
|
240
|
+
*/
|
|
241
|
+
isFallbackEnabled: boolean;
|
|
242
|
+
/**
|
|
243
|
+
* TRUE means this summary is INCOMPLETE — at least one read behind it hit its page
|
|
244
|
+
* ceiling, so responders, methods or rules may be missing from it.
|
|
245
|
+
*
|
|
246
|
+
* It exists because "we truncated" must never be indistinguishable from "everyone is
|
|
247
|
+
* fine". A responder dropped by a truncated read appears in no count, no list and no
|
|
248
|
+
* "needs attention" section; without this flag the summary would state, with total
|
|
249
|
+
* confidence, that a project is healthier than it is. Every surface that renders a
|
|
250
|
+
* count should say so out loud when this is set.
|
|
251
|
+
*/
|
|
252
|
+
isTruncated: boolean;
|
|
253
|
+
users: Array<UserReadiness>;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/*
|
|
257
|
+
* The bullet used for every redaction. A single shared constant so a test can assert on
|
|
258
|
+
* the mask without hard-coding a character that is easy to typo into a look-alike (there
|
|
259
|
+
* are several bullet-ish code points and they are indistinguishable on screen).
|
|
260
|
+
*/
|
|
261
|
+
export const IDENTIFIER_MASK: string = "•••";
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* What SHAPE an identifier has, which is all masking needs to know. Kept separate from
|
|
265
|
+
* ReadinessMethodType because five of the seven channels mask identically — a phone is a
|
|
266
|
+
* phone whether it rings, texts or WhatsApps — and collapsing them here means a new
|
|
267
|
+
* channel cannot arrive with no masking rule at all.
|
|
268
|
+
*/
|
|
269
|
+
export enum MaskedIdentifierKind {
|
|
270
|
+
Email = "Email",
|
|
271
|
+
Phone = "Phone",
|
|
272
|
+
Handle = "Handle",
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export type MaskIdentifierFunction = (
|
|
276
|
+
value: string | undefined | null,
|
|
277
|
+
kind: MaskedIdentifierKind,
|
|
278
|
+
) => string;
|
|
279
|
+
|
|
280
|
+
/*
|
|
281
|
+
* How many trailing digits of a phone number are revealed, and the shortest value that
|
|
282
|
+
* may have them revealed. The two are deliberately different numbers: revealing the last
|
|
283
|
+
* four digits of a four-digit value reveals the value, and revealing the last four of a
|
|
284
|
+
* FIVE-digit value would be a mask in name only. A number has to be longer than what the
|
|
285
|
+
* mask keeps for the mask to be hiding anything at all, so the floor is "more digits than
|
|
286
|
+
* we reveal".
|
|
287
|
+
*/
|
|
288
|
+
const REVEALED_PHONE_DIGITS: number = 4;
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Redact an identifier down to just enough for its owner to recognise it.
|
|
292
|
+
*
|
|
293
|
+
* Exported as a free function, and used by this service for every single identifier it
|
|
294
|
+
* emits, for two reasons. The first is that masking is the one rule in this file that
|
|
295
|
+
* must never be got wrong even slightly, and a pure function of (string, kind) is
|
|
296
|
+
* directly unit-testable in a way that "call the service and inspect the summary" is
|
|
297
|
+
* not. The second is structural: because the ONLY way an identifier reaches a
|
|
298
|
+
* ReadinessMethod is through this function, the API layer has no unmasked value
|
|
299
|
+
* available to leak by accident. Bypassing the masking would require deliberately
|
|
300
|
+
* writing a second query, not merely forgetting a call.
|
|
301
|
+
*
|
|
302
|
+
* Email jane@example.com -> j•••@example.com
|
|
303
|
+
* Phone +14155554821 -> +1 ••• ••• 4821
|
|
304
|
+
* Handle @jamesbond -> @ja•••
|
|
305
|
+
*
|
|
306
|
+
* The phone rule keeps everything before the last ten digits as the country code, which
|
|
307
|
+
* is a heuristic rather than a parse — national numbers are ~10 digits nearly
|
|
308
|
+
* everywhere, so "+1" and "+44" both come out right, and a country whose numbering plan
|
|
309
|
+
* disagrees loses a cosmetic digit and leaks nothing. Correctness here is measured in
|
|
310
|
+
* what is HIDDEN, and the last four digits plus the country code is the same disclosure
|
|
311
|
+
* every bank confirmation screen makes.
|
|
312
|
+
*/
|
|
313
|
+
export const maskIdentifier: MaskIdentifierFunction = (
|
|
314
|
+
value: string | undefined | null,
|
|
315
|
+
kind: MaskedIdentifierKind,
|
|
316
|
+
): string => {
|
|
317
|
+
const trimmed: string = (value || "").trim();
|
|
318
|
+
|
|
319
|
+
if (!trimmed) {
|
|
320
|
+
/*
|
|
321
|
+
* Nothing to mask and nothing to reveal. Returning the bare mask rather than an
|
|
322
|
+
* empty string keeps the UI cell from collapsing into blank space that reads as
|
|
323
|
+
* "no method" when a method demonstrably exists.
|
|
324
|
+
*/
|
|
325
|
+
return IDENTIFIER_MASK;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (kind === MaskedIdentifierKind.Email) {
|
|
329
|
+
const atIndex: number = trimmed.lastIndexOf("@");
|
|
330
|
+
|
|
331
|
+
/*
|
|
332
|
+
* An address with no "@" is not an address. Rather than guess, fall through to the
|
|
333
|
+
* handle rule, which is strictly more conservative than the email rule (it reveals
|
|
334
|
+
* two characters and no domain).
|
|
335
|
+
*/
|
|
336
|
+
if (atIndex <= 0) {
|
|
337
|
+
return maskIdentifier(trimmed, MaskedIdentifierKind.Handle);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const localPart: string = trimmed.substring(0, atIndex);
|
|
341
|
+
const domain: string = trimmed.substring(atIndex + 1);
|
|
342
|
+
|
|
343
|
+
return `${localPart.substring(0, 1)}${IDENTIFIER_MASK}@${domain}`;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (kind === MaskedIdentifierKind.Phone) {
|
|
347
|
+
const digits: string = trimmed.replace(/\D/g, "");
|
|
348
|
+
|
|
349
|
+
/*
|
|
350
|
+
* Note the <=, not <. At exactly four digits the "last four" IS the whole value, so
|
|
351
|
+
* the old strict comparison handed the number back in full while looking, on the
|
|
352
|
+
* screen and in a code review, exactly like a mask. Anything at or below the number
|
|
353
|
+
* of digits we reveal is therefore masked entirely: a value we cannot hide half of
|
|
354
|
+
* is a value we do not show.
|
|
355
|
+
*/
|
|
356
|
+
if (digits.length <= REVEALED_PHONE_DIGITS) {
|
|
357
|
+
return IDENTIFIER_MASK;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const lastFour: string = digits.substring(
|
|
361
|
+
digits.length - REVEALED_PHONE_DIGITS,
|
|
362
|
+
);
|
|
363
|
+
const countryCode: string =
|
|
364
|
+
digits.length > 10 ? digits.substring(0, digits.length - 10) : "";
|
|
365
|
+
const prefix: string = countryCode ? `+${countryCode} ` : "";
|
|
366
|
+
|
|
367
|
+
return `${prefix}${IDENTIFIER_MASK} ${IDENTIFIER_MASK} ${lastFour}`;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/*
|
|
371
|
+
* Handles cover Telegram handles, push device names and webhook names. Two characters
|
|
372
|
+
* is enough for the owner to say "yes, that is my phone" and not enough for anyone
|
|
373
|
+
* else to say whose phone it is.
|
|
374
|
+
*/
|
|
375
|
+
const hasLeadingAt: boolean = trimmed.startsWith("@");
|
|
376
|
+
const body: string = hasLeadingAt ? trimmed.substring(1) : trimmed;
|
|
377
|
+
|
|
378
|
+
return `${hasLeadingAt ? "@" : ""}${body.substring(0, 2)}${IDENTIFIER_MASK}`;
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
/*
|
|
382
|
+
* Which severity list scopes which rule type, and the noun to use when telling an admin
|
|
383
|
+
* about it. Incident and incident-episode pages are severity-scoped by IncidentSeverity;
|
|
384
|
+
* alert and alert-episode by AlertSeverity. Getting this pairing wrong is not a cosmetic
|
|
385
|
+
* error — an alert rule matched against an incident severity id matches nothing at all,
|
|
386
|
+
* which is exactly the shape of Gap G, where episode default rules were written with a
|
|
387
|
+
* NULL severity and were therefore invisible and unreachable at the same time.
|
|
388
|
+
*/
|
|
389
|
+
enum SeverityKind {
|
|
390
|
+
Incident = "Incident",
|
|
391
|
+
Alert = "Alert",
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
interface RuleTypeScope {
|
|
395
|
+
ruleType: NotificationRuleType;
|
|
396
|
+
severityKind: SeverityKind;
|
|
397
|
+
/** Plural noun for the reason sentence: "No rules for Sev4 incidents - ...". */
|
|
398
|
+
subjectNoun: string;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/*
|
|
402
|
+
* The four rule types a PAGE can arrive under. The two handoff types
|
|
403
|
+
* (WHEN_USER_GOES_ON_CALL / WHEN_USER_GOES_OFF_CALL) are deliberately absent: they are
|
|
404
|
+
* courtesy notifications about a shift change, nobody is waiting on them, and counting a
|
|
405
|
+
* missing one as "not ready" would flood the amber state with users whose paging is
|
|
406
|
+
* perfectly healthy. Readiness is about pages.
|
|
407
|
+
*/
|
|
408
|
+
const RULE_TYPE_SCOPES: Array<RuleTypeScope> = [
|
|
409
|
+
{
|
|
410
|
+
ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
|
|
411
|
+
severityKind: SeverityKind.Incident,
|
|
412
|
+
subjectNoun: "incidents",
|
|
413
|
+
},
|
|
414
|
+
{
|
|
415
|
+
ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT_EPISODE,
|
|
416
|
+
severityKind: SeverityKind.Incident,
|
|
417
|
+
subjectNoun: "incident episodes",
|
|
418
|
+
},
|
|
419
|
+
{
|
|
420
|
+
ruleType: NotificationRuleType.ON_CALL_EXECUTED_ALERT,
|
|
421
|
+
severityKind: SeverityKind.Alert,
|
|
422
|
+
subjectNoun: "alerts",
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
ruleType: NotificationRuleType.ON_CALL_EXECUTED_ALERT_EPISODE,
|
|
426
|
+
severityKind: SeverityKind.Alert,
|
|
427
|
+
subjectNoun: "alert episodes",
|
|
428
|
+
},
|
|
429
|
+
];
|
|
430
|
+
|
|
431
|
+
/*
|
|
432
|
+
* Display order for `methods`, and simultaneously the order the fallback would try them
|
|
433
|
+
* in. Sharing one order means the first entry in the list an admin looks at is the
|
|
434
|
+
* channel a fallback page would actually arrive on.
|
|
435
|
+
*/
|
|
436
|
+
const METHOD_DISPLAY_ORDER: Array<ReadinessMethodType> = [
|
|
437
|
+
ReadinessMethodType.Push,
|
|
438
|
+
ReadinessMethodType.Email,
|
|
439
|
+
ReadinessMethodType.SMS,
|
|
440
|
+
ReadinessMethodType.Call,
|
|
441
|
+
ReadinessMethodType.WhatsApp,
|
|
442
|
+
ReadinessMethodType.Telegram,
|
|
443
|
+
ReadinessMethodType.Webhook,
|
|
444
|
+
];
|
|
445
|
+
|
|
446
|
+
const RESPONDER_SOURCE_ORDER: Array<ResponderSource> = [
|
|
447
|
+
ResponderSource.Direct,
|
|
448
|
+
ResponderSource.Team,
|
|
449
|
+
ResponderSource.Schedule,
|
|
450
|
+
ResponderSource.Override,
|
|
451
|
+
];
|
|
452
|
+
|
|
453
|
+
/*
|
|
454
|
+
* Most-broken-first. The readiness table is read by somebody looking for a problem, so
|
|
455
|
+
* the problems sort to the top and the healthy majority sorts out of the way.
|
|
456
|
+
*/
|
|
457
|
+
const STATUS_SORT_RANK: Record<ReadinessStatus, number> = {
|
|
458
|
+
[ReadinessStatus.NotReachable]: 0,
|
|
459
|
+
[ReadinessStatus.PartiallyReady]: 1,
|
|
460
|
+
[ReadinessStatus.Ready]: 2,
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
/*
|
|
464
|
+
* 60 seconds, the same window ProjectService.currentPlanCache uses and for the same
|
|
465
|
+
* reason: this is computed on page load and on every responder chip render, the inputs
|
|
466
|
+
* (escalation rules, notification rules, verified methods) change on a human timescale,
|
|
467
|
+
* and a minute of staleness on "this person has no SMS rule" costs nothing. There is no
|
|
468
|
+
* cross-process invalidation — each replica holds its own copy — so the TTL is the only
|
|
469
|
+
* guarantee, which is why it is short.
|
|
470
|
+
*/
|
|
471
|
+
const READINESS_CACHE_TTL_IN_MS: number = 60 * 1000;
|
|
472
|
+
|
|
473
|
+
/*
|
|
474
|
+
* Rows per page. LIMIT_PER_PROJECT is the largest read the database layer will serve
|
|
475
|
+
* (DatabaseService clamps anything above it), so it is the biggest page that survives a
|
|
476
|
+
* round trip, and a bigger page means fewer round trips for the same total.
|
|
477
|
+
*/
|
|
478
|
+
const READ_PAGE_SIZE: number = LIMIT_PER_PROJECT;
|
|
479
|
+
|
|
480
|
+
/*
|
|
481
|
+
* A ceiling on the number of pages ONE read may take, so a fetcher that keeps returning
|
|
482
|
+
* full pages — a paging bug, a query whose sort is not total, a table that is genuinely
|
|
483
|
+
* growing faster than we can read it — cannot spin this service forever holding a
|
|
484
|
+
* connection. Five million rows is far past any real project; a read that hits it is a
|
|
485
|
+
* bug report, not a big customer, which is why hitting it is logged as an error AND
|
|
486
|
+
* reported as isTruncated rather than quietly stopping.
|
|
487
|
+
*/
|
|
488
|
+
const MAX_PAGES_PER_READ: number = 500;
|
|
489
|
+
|
|
490
|
+
/** The state of one (user, ruleType, severity) cell while it is being accumulated. */
|
|
491
|
+
interface CoverageState {
|
|
492
|
+
hasRule: boolean;
|
|
493
|
+
isOptOut: boolean;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/** A severity reduced to what coverage needs, in display order. */
|
|
497
|
+
interface SeverityRef {
|
|
498
|
+
id: ObjectID;
|
|
499
|
+
name: string;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/*
|
|
503
|
+
* Whether every read behind one answer actually reached the end of its table. Threaded
|
|
504
|
+
* through every loader as one mutable object rather than returned by each of them,
|
|
505
|
+
* because a truncation anywhere — responders, methods, rules — invalidates the whole
|
|
506
|
+
* answer equally, and an accumulator that any layer can set is much harder to forget to
|
|
507
|
+
* propagate than a boolean that has to be OR-ed at fifteen call sites.
|
|
508
|
+
*/
|
|
509
|
+
interface ReadCompleteness {
|
|
510
|
+
isTruncated: boolean;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/*
|
|
514
|
+
* Everything the resolution pass learns about ONE responder, and the only channel between
|
|
515
|
+
* "who is on this policy" and buildUserReadiness.
|
|
516
|
+
*
|
|
517
|
+
* It used to be a bare `Set<ResponderSource>`, which was enough while the answer to "how
|
|
518
|
+
* is this person reached?" was a four-value enum. It stopped being enough the moment the
|
|
519
|
+
* readiness table needed to be filtered by team: the resolution pass reads the exact
|
|
520
|
+
* TeamMember rows that would answer that, then threw the team id away and kept only the
|
|
521
|
+
* fact that SOME team was involved. Carrying the ids costs one extra selected column on a
|
|
522
|
+
* read that already happens, and keeps the "reached via Team" chip and the team filter
|
|
523
|
+
* derived from the same pass rather than from two that can disagree.
|
|
524
|
+
*
|
|
525
|
+
* Ids, not names. Names are resolved once per scope in loadTeamNames, because a team on an
|
|
526
|
+
* escalation rule is shared by every one of its members and reading its name per member is
|
|
527
|
+
* the N+1 this service was written to eliminate.
|
|
528
|
+
*/
|
|
529
|
+
interface ResponderAttachment {
|
|
530
|
+
sources: Set<ResponderSource>;
|
|
531
|
+
/** Team ids as strings, so a user on the same team twice dedupes for free. */
|
|
532
|
+
teamIds: Set<string>;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/*
|
|
536
|
+
* The project-level switches that decide what a page with no matching rule actually
|
|
537
|
+
* does, AND which channels can carry a page at all. Read once per summary, never per
|
|
538
|
+
* user.
|
|
539
|
+
*/
|
|
540
|
+
interface ProjectNotificationSettings {
|
|
541
|
+
isFallbackDisabled: boolean;
|
|
542
|
+
enableSmsNotifications: boolean;
|
|
543
|
+
enableCallNotifications: boolean;
|
|
544
|
+
enableWhatsAppNotifications: boolean;
|
|
545
|
+
enableTelegramNotifications: boolean;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/** Everything the per-user pass needs, loaded once for the whole responder set. */
|
|
549
|
+
interface ReadinessInputs {
|
|
550
|
+
users: Array<User>;
|
|
551
|
+
methodsByUserId: Map<string, Array<ReadinessMethod>>;
|
|
552
|
+
coverageByKey: Map<string, CoverageState>;
|
|
553
|
+
incidentSeverities: Array<SeverityRef>;
|
|
554
|
+
alertSeverities: Array<SeverityRef>;
|
|
555
|
+
projectSettings: ProjectNotificationSettings;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* The result of the batch entry point, plus the one fact a single-user caller needs that
|
|
560
|
+
* the list itself cannot carry: WHICH of the requested users turned out to be members of
|
|
561
|
+
* the project. "Not a member" and "member with no User row" are different errors with
|
|
562
|
+
* different fixes, and an empty array cannot tell them apart.
|
|
563
|
+
*/
|
|
564
|
+
interface UserReadinessBatch {
|
|
565
|
+
readiness: Array<UserReadiness>;
|
|
566
|
+
memberUserIds: Set<string>;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* The shape of a service this file can page over. Structural rather than
|
|
571
|
+
* `DatabaseService<TModel>` so that readEveryPage does not drag the whole base class
|
|
572
|
+
* into its signature, and so a test double is assignable without pretending to be one.
|
|
573
|
+
*/
|
|
574
|
+
interface PagedReadService<TModel extends DatabaseBaseModel> {
|
|
575
|
+
findBy(findBy: FindBy<TModel>): Promise<Array<TModel>>;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* The two ids every notification-method row has to hand over: its OWN, which is what a
|
|
580
|
+
* rule points at, and its OWNER's, which is whose readiness it counts towards.
|
|
581
|
+
*
|
|
582
|
+
* Structural rather than a union of the seven models so that loadMethods can take both
|
|
583
|
+
* out of any of them without seven overloads — and so that an eighth channel cannot be
|
|
584
|
+
* added without supplying both, which is the pair that makes a method both listable and
|
|
585
|
+
* selectable.
|
|
586
|
+
*/
|
|
587
|
+
interface MethodRowRef {
|
|
588
|
+
id: ObjectID | null;
|
|
589
|
+
userId?: ObjectID | undefined;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
type CoverageKeyFunction = (
|
|
593
|
+
userId: string,
|
|
594
|
+
ruleType: NotificationRuleType,
|
|
595
|
+
severityId: string,
|
|
596
|
+
) => string;
|
|
597
|
+
|
|
598
|
+
/*
|
|
599
|
+
* Pipe-separated because none of the three components can contain a pipe: user and
|
|
600
|
+
* severity ids are uuids, and NotificationRuleType's values are fixed English sentences.
|
|
601
|
+
*/
|
|
602
|
+
const buildCoverageKey: CoverageKeyFunction = (
|
|
603
|
+
userId: string,
|
|
604
|
+
ruleType: NotificationRuleType,
|
|
605
|
+
severityId: string,
|
|
606
|
+
): string => {
|
|
607
|
+
return `${userId}|${ruleType}|${severityId}`;
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
export default class OnCallReadinessService {
|
|
611
|
+
private static summaryCache: InMemoryTTLCache<ReadinessSummary> =
|
|
612
|
+
new InMemoryTTLCache(10_000);
|
|
613
|
+
|
|
614
|
+
private static userCache: InMemoryTTLCache<UserReadiness> =
|
|
615
|
+
new InMemoryTTLCache(10_000);
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Readiness for every responder a single policy can reach — the union of its direct
|
|
619
|
+
* users, its teams' members, its schedules' layer users, and anyone an override routes
|
|
620
|
+
* pages to.
|
|
621
|
+
*/
|
|
622
|
+
public static async getReadinessForPolicy(
|
|
623
|
+
policyId: ObjectID,
|
|
624
|
+
projectId: ObjectID,
|
|
625
|
+
): Promise<ReadinessSummary> {
|
|
626
|
+
const cacheKey: string = `${projectId.toString()}:policy:${policyId.toString()}`;
|
|
627
|
+
const cached: ReadinessSummary | undefined =
|
|
628
|
+
this.summaryCache.get(cacheKey);
|
|
629
|
+
|
|
630
|
+
if (cached) {
|
|
631
|
+
return cached;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/*
|
|
635
|
+
* Look the policy up rather than letting a bad id fall through. Every query below is
|
|
636
|
+
* scoped by projectId, so a policy id from another project would return zero
|
|
637
|
+
* responders — and a summary that says "0 responders, nothing wrong" is the most
|
|
638
|
+
* dangerous possible answer to "is this policy safe to rely on?".
|
|
639
|
+
*/
|
|
640
|
+
const policy: OnCallDutyPolicy | null =
|
|
641
|
+
await OnCallDutyPolicyService.findOneById({
|
|
642
|
+
id: policyId,
|
|
643
|
+
select: {
|
|
644
|
+
_id: true,
|
|
645
|
+
projectId: true,
|
|
646
|
+
},
|
|
647
|
+
props: {
|
|
648
|
+
isRoot: true,
|
|
649
|
+
},
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
if (!policy || policy.projectId?.toString() !== projectId.toString()) {
|
|
653
|
+
throw new BadDataException("On-call duty policy not found");
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
const summary: ReadinessSummary = await this.computeSummary(
|
|
657
|
+
projectId,
|
|
658
|
+
policyId,
|
|
659
|
+
);
|
|
660
|
+
|
|
661
|
+
this.summaryCache.set(cacheKey, summary, READINESS_CACHE_TTL_IN_MS);
|
|
662
|
+
|
|
663
|
+
return summary;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Readiness for every responder reachable through ANY policy in the project. Same
|
|
668
|
+
* resolution as the per-policy call with the policy filter dropped, so a user who is
|
|
669
|
+
* ready on one policy and unreachable on another appears exactly once, with the union
|
|
670
|
+
* of their sources.
|
|
671
|
+
*/
|
|
672
|
+
public static async getReadinessForProject(
|
|
673
|
+
projectId: ObjectID,
|
|
674
|
+
): Promise<ReadinessSummary> {
|
|
675
|
+
const cacheKey: string = `${projectId.toString()}:project`;
|
|
676
|
+
const cached: ReadinessSummary | undefined =
|
|
677
|
+
this.summaryCache.get(cacheKey);
|
|
678
|
+
|
|
679
|
+
if (cached) {
|
|
680
|
+
return cached;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
const summary: ReadinessSummary = await this.computeSummary(
|
|
684
|
+
projectId,
|
|
685
|
+
undefined,
|
|
686
|
+
);
|
|
687
|
+
|
|
688
|
+
this.summaryCache.set(cacheKey, summary, READINESS_CACHE_TTL_IN_MS);
|
|
689
|
+
|
|
690
|
+
return summary;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* Readiness for a SET of users, at the cost of one user.
|
|
695
|
+
*
|
|
696
|
+
* This is the entry point every list-shaped caller must use — a team roster, a
|
|
697
|
+
* responder table, a page of chips. It exists because the obvious alternative,
|
|
698
|
+
* `Promise.all(userIds.map(getReadinessForUser))`, is not a small inefficiency but a
|
|
699
|
+
* different order of cost: each of those calls used to resolve the ENTIRE project's
|
|
700
|
+
* responder set (six heavy reads) purely to work out one user's `reachedVia`, so a
|
|
701
|
+
* forty-member team issued several hundred queries where a handful would do, and one
|
|
702
|
+
* rejected promise threw the whole page away.
|
|
703
|
+
*
|
|
704
|
+
* The query count here is constant in the number of users asked about: every read is
|
|
705
|
+
* `Includes(userIds)` over the whole set, and the membership resolution is targeted at
|
|
706
|
+
* exactly those users rather than at the project. One user and five hundred users cost
|
|
707
|
+
* the same round trips — the only thing that grows is the number of PAGES each read
|
|
708
|
+
* takes, which is a function of how many rows exist, not of how many users were asked
|
|
709
|
+
* about, and is the price of never truncating.
|
|
710
|
+
*
|
|
711
|
+
* Users that are not members of the project are OMITTED rather than thrown for: a
|
|
712
|
+
* roster read races with somebody being removed from a team, and one departed member
|
|
713
|
+
* must not blank out the readiness of the other thirty-nine. A caller that needs to
|
|
714
|
+
* know about the omission should compare the returned userIds with the ones it asked
|
|
715
|
+
* for.
|
|
716
|
+
*/
|
|
717
|
+
public static async getReadinessForUsers(
|
|
718
|
+
userIds: Array<ObjectID>,
|
|
719
|
+
projectId: ObjectID,
|
|
720
|
+
): Promise<Array<UserReadiness>> {
|
|
721
|
+
if (userIds.length === 0) {
|
|
722
|
+
return [];
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
const batch: UserReadinessBatch = await this.computeReadinessForUsers(
|
|
726
|
+
userIds,
|
|
727
|
+
projectId,
|
|
728
|
+
);
|
|
729
|
+
|
|
730
|
+
return batch.readiness;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* Readiness for one user, whether or not they are on a policy at all.
|
|
735
|
+
*
|
|
736
|
+
* The "whether or not" matters: this is what the add-responder modal calls BEFORE the
|
|
737
|
+
* user has been attached to anything, which is the only moment at which the mistake is
|
|
738
|
+
* cheap to fix. Such a user has an empty `reachedVia` and a status computed exactly as
|
|
739
|
+
* it would be once they are attached.
|
|
740
|
+
*
|
|
741
|
+
* A thin wrapper over getReadinessForUsers, deliberately: one code path computes
|
|
742
|
+
* readiness for a set, and "one" is a set of size one. The only thing this adds is
|
|
743
|
+
* turning the two ways of getting nothing back — not a member of the project, member
|
|
744
|
+
* with no User row — into the two different exceptions callers have always seen.
|
|
745
|
+
*/
|
|
746
|
+
public static async getReadinessForUser(
|
|
747
|
+
userId: ObjectID,
|
|
748
|
+
projectId: ObjectID,
|
|
749
|
+
): Promise<UserReadiness> {
|
|
750
|
+
const batch: UserReadinessBatch = await this.computeReadinessForUsers(
|
|
751
|
+
[userId],
|
|
752
|
+
projectId,
|
|
753
|
+
);
|
|
754
|
+
|
|
755
|
+
/*
|
|
756
|
+
* User is a GLOBAL model — it is not scoped by project — so without this check any
|
|
757
|
+
* caller holding a project's credentials could ask for the readiness of an arbitrary
|
|
758
|
+
* user id and get their name and login email back. Team membership is what "in this
|
|
759
|
+
* project" means, so that is what is checked, and it is checked BEFORE any other
|
|
760
|
+
* read is issued (see computeReadinessForUsers), so a caller probing arbitrary user
|
|
761
|
+
* ids learns nothing at all.
|
|
762
|
+
*/
|
|
763
|
+
if (!batch.memberUserIds.has(userId.toString())) {
|
|
764
|
+
throw new BadDataException("User is not a member of this project");
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
const readiness: UserReadiness | undefined = batch.readiness[0];
|
|
768
|
+
|
|
769
|
+
if (!readiness) {
|
|
770
|
+
throw new BadDataException("User not found");
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
return readiness;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* Throw away every cached answer in this process.
|
|
778
|
+
*
|
|
779
|
+
* Deliberately coarse — it does not take a projectId — because the cache is keyed
|
|
780
|
+
* three different ways (project, policy, user) and a write to one notification rule
|
|
781
|
+
* can invalidate all three at once: the rule's owner, every policy that reaches them,
|
|
782
|
+
* and the project roll-up. Working out the affected key set would be more code than
|
|
783
|
+
* the saving is worth when the entries expire in sixty seconds anyway, and a
|
|
784
|
+
* too-clever invalidation that misses a key is indistinguishable from the stale
|
|
785
|
+
* readiness this whole service exists to eliminate.
|
|
786
|
+
*
|
|
787
|
+
* This is PUBLIC and must actually be CALLED. Until it is, a "Recheck" button re-reads
|
|
788
|
+
* the same sixty-second-old answer and redraws it unchanged, which reads to an admin
|
|
789
|
+
* who has just fixed something as "my fix did not work". The exact call sites, all of
|
|
790
|
+
* which currently do not call it:
|
|
791
|
+
*
|
|
792
|
+
* - UserNotificationRuleService — on create and on delete of a rule (this is the
|
|
793
|
+
* write the Recheck button is nearly always chasing).
|
|
794
|
+
* - The seven notification-method services (UserEmailService, UserSmsService,
|
|
795
|
+
* UserCallService, UserPushService, UserWhatsAppService, UserTelegramService,
|
|
796
|
+
* UserWebhookService) — on create, on delete, and on VERIFICATION, which is the
|
|
797
|
+
* write that flips a responder from NotReachable to Ready.
|
|
798
|
+
* - OnCallDutyPolicyEscalationRuleUserService / ...TeamService / ...ScheduleService
|
|
799
|
+
* and OnCallDutyPolicyScheduleLayerUserService — on create and delete, because
|
|
800
|
+
* they change who the responder set even contains.
|
|
801
|
+
* - OnCallDutyPolicyUserOverrideService — on create, update and delete.
|
|
802
|
+
* - TeamMemberService — on create and delete, for the same reason.
|
|
803
|
+
* - ProjectService — on any update that touches disableOnCallNotificationFallback or
|
|
804
|
+
* the four enable*Notifications switches, since those change every user's status
|
|
805
|
+
* at once.
|
|
806
|
+
* - IncidentSeverityService / AlertSeverityService — on create and delete, which add
|
|
807
|
+
* and remove whole columns of the coverage grid.
|
|
808
|
+
* - OnCallReadinessAPI — on an explicit refresh request, so "Recheck" means recheck.
|
|
809
|
+
*
|
|
810
|
+
* Note that each replica holds its own copy and clears only its own; the TTL remains
|
|
811
|
+
* the only cross-process guarantee.
|
|
812
|
+
*/
|
|
813
|
+
public static clearCache(): void {
|
|
814
|
+
this.summaryCache.clear();
|
|
815
|
+
this.userCache.clear();
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
private static async computeSummary(
|
|
819
|
+
projectId: ObjectID,
|
|
820
|
+
onCallDutyPolicyId: ObjectID | undefined,
|
|
821
|
+
): Promise<ReadinessSummary> {
|
|
822
|
+
const completeness: ReadCompleteness = this.newReadCompleteness();
|
|
823
|
+
|
|
824
|
+
/*
|
|
825
|
+
* Loaded here rather than inside the per-user pass because the summary itself has to
|
|
826
|
+
* report isFallbackEnabled even when the responder set is empty — a project with no
|
|
827
|
+
* responders and the fallback switched off is exactly the project somebody is about
|
|
828
|
+
* to attach a responder to.
|
|
829
|
+
*/
|
|
830
|
+
const projectSettings: ProjectNotificationSettings =
|
|
831
|
+
await this.loadProjectSettings(projectId);
|
|
832
|
+
|
|
833
|
+
const responders: Map<string, ResponderAttachment> =
|
|
834
|
+
await this.resolveResponders(projectId, onCallDutyPolicyId, completeness);
|
|
835
|
+
|
|
836
|
+
const users: Array<UserReadiness> = await this.buildReadiness({
|
|
837
|
+
projectId: projectId,
|
|
838
|
+
responders: responders,
|
|
839
|
+
projectSettings: projectSettings,
|
|
840
|
+
completeness: completeness,
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
let readyCount: number = 0;
|
|
844
|
+
let partiallyReadyCount: number = 0;
|
|
845
|
+
let notReachableCount: number = 0;
|
|
846
|
+
|
|
847
|
+
for (const user of users) {
|
|
848
|
+
if (user.status === ReadinessStatus.Ready) {
|
|
849
|
+
readyCount++;
|
|
850
|
+
} else if (user.status === ReadinessStatus.PartiallyReady) {
|
|
851
|
+
partiallyReadyCount++;
|
|
852
|
+
} else {
|
|
853
|
+
notReachableCount++;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
return {
|
|
858
|
+
projectId: projectId,
|
|
859
|
+
onCallDutyPolicyId: onCallDutyPolicyId,
|
|
860
|
+
readyCount: readyCount,
|
|
861
|
+
partiallyReadyCount: partiallyReadyCount,
|
|
862
|
+
notReachableCount: notReachableCount,
|
|
863
|
+
isFallbackEnabled: !projectSettings.isFallbackDisabled,
|
|
864
|
+
isTruncated: completeness.isTruncated,
|
|
865
|
+
users: users,
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
/**
|
|
870
|
+
* The batch computation both public user-shaped entry points share.
|
|
871
|
+
*
|
|
872
|
+
* Membership is resolved FIRST and everything else is filtered to the members it
|
|
873
|
+
* found, which does three jobs with one read: it is the cross-project guard, it is the
|
|
874
|
+
* source of each user's team list (which is how the Team source is resolved without
|
|
875
|
+
* expanding the whole project), and it is what makes "user removed mid-request" a
|
|
876
|
+
* quiet omission rather than an exception.
|
|
877
|
+
*/
|
|
878
|
+
private static async computeReadinessForUsers(
|
|
879
|
+
userIds: Array<ObjectID>,
|
|
880
|
+
projectId: ObjectID,
|
|
881
|
+
): Promise<UserReadinessBatch> {
|
|
882
|
+
const requestedUserIds: Array<ObjectID> = this.distinctIds(userIds);
|
|
883
|
+
|
|
884
|
+
const readiness: Array<UserReadiness> = [];
|
|
885
|
+
const memberUserIds: Set<string> = new Set<string>();
|
|
886
|
+
const uncachedUserIds: Array<ObjectID> = [];
|
|
887
|
+
|
|
888
|
+
for (const userId of requestedUserIds) {
|
|
889
|
+
const cached: UserReadiness | undefined = this.userCache.get(
|
|
890
|
+
this.userCacheKey(projectId, userId),
|
|
891
|
+
);
|
|
892
|
+
|
|
893
|
+
if (cached) {
|
|
894
|
+
/*
|
|
895
|
+
* Only members are ever written to this cache, so a hit answers the membership
|
|
896
|
+
* question too. That matters: re-issuing the membership read for a cached user
|
|
897
|
+
* would make a cache hit cost a round trip, which is most of what the cache is
|
|
898
|
+
* for.
|
|
899
|
+
*/
|
|
900
|
+
memberUserIds.add(userId.toString());
|
|
901
|
+
readiness.push(cached);
|
|
902
|
+
continue;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
uncachedUserIds.push(userId);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
if (uncachedUserIds.length === 0) {
|
|
909
|
+
return {
|
|
910
|
+
readiness: this.sortReadiness(readiness),
|
|
911
|
+
memberUserIds: memberUserIds,
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
const completeness: ReadCompleteness = this.newReadCompleteness();
|
|
916
|
+
|
|
917
|
+
const teamIdsByUserId: Map<
|
|
918
|
+
string,
|
|
919
|
+
Array<ObjectID>
|
|
920
|
+
> = await this.loadProjectMembership(
|
|
921
|
+
projectId,
|
|
922
|
+
uncachedUserIds,
|
|
923
|
+
completeness,
|
|
924
|
+
);
|
|
925
|
+
|
|
926
|
+
const memberIds: Array<ObjectID> = uncachedUserIds.filter(
|
|
927
|
+
(userId: ObjectID): boolean => {
|
|
928
|
+
return teamIdsByUserId.has(userId.toString());
|
|
929
|
+
},
|
|
930
|
+
);
|
|
931
|
+
|
|
932
|
+
for (const memberId of memberIds) {
|
|
933
|
+
memberUserIds.add(memberId.toString());
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
if (memberIds.length === 0) {
|
|
937
|
+
return {
|
|
938
|
+
readiness: this.sortReadiness(readiness),
|
|
939
|
+
memberUserIds: memberUserIds,
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
const projectSettings: ProjectNotificationSettings =
|
|
944
|
+
await this.loadProjectSettings(projectId);
|
|
945
|
+
|
|
946
|
+
const responders: Map<string, ResponderAttachment> =
|
|
947
|
+
await this.resolveRespondersForUsers({
|
|
948
|
+
projectId: projectId,
|
|
949
|
+
userIds: memberIds,
|
|
950
|
+
teamIdsByUserId: teamIdsByUserId,
|
|
951
|
+
completeness: completeness,
|
|
952
|
+
});
|
|
953
|
+
|
|
954
|
+
const computed: Array<UserReadiness> = await this.buildReadiness({
|
|
955
|
+
projectId: projectId,
|
|
956
|
+
responders: responders,
|
|
957
|
+
projectSettings: projectSettings,
|
|
958
|
+
completeness: completeness,
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
for (const one of computed) {
|
|
962
|
+
/*
|
|
963
|
+
* A truncated read can only make a user look WORSE here — a rule that did not
|
|
964
|
+
* arrive reads as a missing rule, a method that did not arrive as a missing method
|
|
965
|
+
* — so the answer is safe to RETURN. It is not safe to cache for a minute, and the
|
|
966
|
+
* reason is the asymmetry with the summary: ReadinessSummary carries isTruncated,
|
|
967
|
+
* so a cached truncated summary is still telling the truth about itself, whereas
|
|
968
|
+
* UserReadiness carries no such field and a cached one would be an unlabelled false
|
|
969
|
+
* amber sitting on somebody's card for sixty seconds. Recomputing is cheap next to
|
|
970
|
+
* teaching admins that amber means nothing.
|
|
971
|
+
*/
|
|
972
|
+
if (!completeness.isTruncated) {
|
|
973
|
+
this.userCache.set(
|
|
974
|
+
this.userCacheKey(projectId, one.userId),
|
|
975
|
+
one,
|
|
976
|
+
READINESS_CACHE_TTL_IN_MS,
|
|
977
|
+
);
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
readiness.push(one);
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
return {
|
|
984
|
+
readiness: this.sortReadiness(readiness),
|
|
985
|
+
memberUserIds: memberUserIds,
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
private static userCacheKey(projectId: ObjectID, userId: ObjectID): string {
|
|
990
|
+
return `${projectId.toString()}:user:${userId.toString()}`;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
private static newReadCompleteness(): ReadCompleteness {
|
|
994
|
+
return {
|
|
995
|
+
isTruncated: false,
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
/**
|
|
1000
|
+
* Read a whole table, one page at a time, folding each page as it arrives.
|
|
1001
|
+
*
|
|
1002
|
+
* This is the single most important method in the file, because the alternative it
|
|
1003
|
+
* replaces — one findBy capped at LIMIT_PER_PROJECT — is not a performance choice but
|
|
1004
|
+
* a correctness one, and it fails in the worst available direction. UserNotificationRule
|
|
1005
|
+
* rows grow as users x (2 x incidentSeverities + 2 x alertSeverities) x verified
|
|
1006
|
+
* methods, so a five thousand responder project with eight severities of each kind and
|
|
1007
|
+
* two methods each holds around 340,000 of them. A single capped read returns the first
|
|
1008
|
+
* 10,000 — roughly a hundred and fifty users' worth, in whatever order the database
|
|
1009
|
+
* felt like — and every other responder is then scored against ZERO rules. The same
|
|
1010
|
+
* shape on a responder-producing read (team member expansion, schedule layers) is worse
|
|
1011
|
+
* still: those users never enter the map at all, so they appear in no count, no list and
|
|
1012
|
+
* no "needs attention" section. The feature reports them as though they do not exist.
|
|
1013
|
+
*
|
|
1014
|
+
* Raising the cap does not fix that; it moves it to a slightly larger project and makes
|
|
1015
|
+
* it harder to notice. Paging does fix it, at the cost of one round trip per full page,
|
|
1016
|
+
* which is a price worth paying to never quietly lie about who can be paged.
|
|
1017
|
+
*
|
|
1018
|
+
* Three details that are load-bearing rather than incidental:
|
|
1019
|
+
*
|
|
1020
|
+
* - Pages are folded by the CALLER as they arrive, not accumulated and returned. The
|
|
1021
|
+
* coverage read would otherwise materialise all 340,000 rows at once purely to
|
|
1022
|
+
* collapse them into a map of a few thousand entries.
|
|
1023
|
+
* - Every read is sorted, with `_id` ascending as the final tiebreak. OFFSET paging
|
|
1024
|
+
* over an unordered — or non-totally-ordered — query may return the same row twice
|
|
1025
|
+
* and skip another, and the default sort here would be `createdAt DESC`, which is
|
|
1026
|
+
* emphatically not unique when a migration wrote a project's default rules in one
|
|
1027
|
+
* transaction.
|
|
1028
|
+
* - Hitting MAX_PAGES_PER_READ sets isTruncated AND logs an error. A truncation that
|
|
1029
|
+
* is merely logged is invisible to the person reading the readiness table, and a
|
|
1030
|
+
* truncation that is merely flagged is invisible to whoever has to work out why.
|
|
1031
|
+
*/
|
|
1032
|
+
private static async readEveryPage<TModel extends DatabaseBaseModel>(data: {
|
|
1033
|
+
description: string;
|
|
1034
|
+
projectId: ObjectID;
|
|
1035
|
+
completeness: ReadCompleteness;
|
|
1036
|
+
service: PagedReadService<TModel>;
|
|
1037
|
+
query: Query<TModel>;
|
|
1038
|
+
select: Select<TModel>;
|
|
1039
|
+
sort?: Sort<TModel> | undefined;
|
|
1040
|
+
consumePage: (rows: Array<TModel>) => void;
|
|
1041
|
+
}): Promise<void> {
|
|
1042
|
+
const sort: Sort<TModel> = {
|
|
1043
|
+
...(data.sort || {}),
|
|
1044
|
+
_id: SortOrder.Ascending,
|
|
1045
|
+
} as Sort<TModel>;
|
|
1046
|
+
|
|
1047
|
+
let skip: number = 0;
|
|
1048
|
+
|
|
1049
|
+
for (let page: number = 0; page < MAX_PAGES_PER_READ; page++) {
|
|
1050
|
+
const rows: Array<TModel> = await data.service.findBy({
|
|
1051
|
+
/*
|
|
1052
|
+
* A fresh shallow copy per page: findBy hands the query object to the permission
|
|
1053
|
+
* layer, which is free to add its own predicates to it, and a query that
|
|
1054
|
+
* accumulated them across pages would silently narrow as it went.
|
|
1055
|
+
*/
|
|
1056
|
+
query: { ...data.query },
|
|
1057
|
+
select: data.select,
|
|
1058
|
+
sort: sort,
|
|
1059
|
+
limit: READ_PAGE_SIZE,
|
|
1060
|
+
skip: skip,
|
|
1061
|
+
props: {
|
|
1062
|
+
isRoot: true,
|
|
1063
|
+
},
|
|
1064
|
+
});
|
|
1065
|
+
|
|
1066
|
+
data.consumePage(rows);
|
|
1067
|
+
|
|
1068
|
+
if (rows.length < READ_PAGE_SIZE) {
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
skip += rows.length;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
data.completeness.isTruncated = true;
|
|
1076
|
+
|
|
1077
|
+
logger.error(
|
|
1078
|
+
`OnCallReadinessService stopped reading ${data.description} for project ${data.projectId.toString()} after ${MAX_PAGES_PER_READ} pages of ${READ_PAGE_SIZE} rows. The readiness answer for this project is INCOMPLETE: responders may be missing from it entirely, and its counts understate the number of people who cannot be paged.`,
|
|
1079
|
+
);
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
/**
|
|
1083
|
+
* The effective responder set, deduped on userId, with every source a user was reached
|
|
1084
|
+
* by.
|
|
1085
|
+
*
|
|
1086
|
+
* This mirrors OnCallDutyPolicyEscalationRuleService.startRuleExecution, which is the
|
|
1087
|
+
* runtime's own answer to "who does this page". Any divergence between the two is a
|
|
1088
|
+
* lie in the UI: a readiness table that omits a user the runtime pages is worse than
|
|
1089
|
+
* no table, because it actively certifies a gap as covered. Three deliberate
|
|
1090
|
+
* alignments with the runtime:
|
|
1091
|
+
*
|
|
1092
|
+
* - Team members are NOT filtered by hasAcceptedInvitation, because
|
|
1093
|
+
* TeamMemberService.getUsersInTeam does not filter either. A member who never
|
|
1094
|
+
* accepted their invite still gets paged, so they still have to be checked.
|
|
1095
|
+
* - Schedule layer users are taken WHOLE, not sampled for who is on call right now.
|
|
1096
|
+
* Readiness is a property of the roster, not of this instant; a user in next
|
|
1097
|
+
* week's rotation with no notification rule is a page that will be missed next
|
|
1098
|
+
* week, and that is precisely what this is for.
|
|
1099
|
+
* - Overrides contribute the user pages are ROUTED TO, not the user being covered
|
|
1100
|
+
* for. During an override the covered user is not paged at all, while the
|
|
1101
|
+
* substitute is — and the substitute may not be attached to the policy by any
|
|
1102
|
+
* other means, which makes them the single most likely responder to be silently
|
|
1103
|
+
* unreachable.
|
|
1104
|
+
*/
|
|
1105
|
+
private static async resolveResponders(
|
|
1106
|
+
projectId: ObjectID,
|
|
1107
|
+
onCallDutyPolicyId: ObjectID | undefined,
|
|
1108
|
+
completeness: ReadCompleteness,
|
|
1109
|
+
): Promise<Map<string, ResponderAttachment>> {
|
|
1110
|
+
const responders: Map<string, ResponderAttachment> = new Map<
|
|
1111
|
+
string,
|
|
1112
|
+
ResponderAttachment
|
|
1113
|
+
>();
|
|
1114
|
+
|
|
1115
|
+
type AddResponderFunction = (
|
|
1116
|
+
userId: ObjectID | undefined,
|
|
1117
|
+
source: ResponderSource,
|
|
1118
|
+
teamId?: ObjectID | undefined,
|
|
1119
|
+
) => void;
|
|
1120
|
+
|
|
1121
|
+
const addResponder: AddResponderFunction = (
|
|
1122
|
+
userId: ObjectID | undefined,
|
|
1123
|
+
source: ResponderSource,
|
|
1124
|
+
teamId?: ObjectID | undefined,
|
|
1125
|
+
): void => {
|
|
1126
|
+
if (!userId) {
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
const key: string = userId.toString();
|
|
1131
|
+
let existing: ResponderAttachment | undefined = responders.get(key);
|
|
1132
|
+
|
|
1133
|
+
if (!existing) {
|
|
1134
|
+
existing = {
|
|
1135
|
+
sources: new Set<ResponderSource>(),
|
|
1136
|
+
teamIds: new Set<string>(),
|
|
1137
|
+
};
|
|
1138
|
+
responders.set(key, existing);
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
existing.sources.add(source);
|
|
1142
|
+
|
|
1143
|
+
if (teamId) {
|
|
1144
|
+
existing.teamIds.add(teamId.toString());
|
|
1145
|
+
}
|
|
1146
|
+
};
|
|
1147
|
+
|
|
1148
|
+
// 1. Users attached directly to an escalation rule.
|
|
1149
|
+
|
|
1150
|
+
const directQuery: Query<OnCallDutyPolicyEscalationRuleUser> = {
|
|
1151
|
+
projectId: projectId,
|
|
1152
|
+
};
|
|
1153
|
+
|
|
1154
|
+
if (onCallDutyPolicyId) {
|
|
1155
|
+
directQuery.onCallDutyPolicyId = onCallDutyPolicyId;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
await this.readEveryPage<OnCallDutyPolicyEscalationRuleUser>({
|
|
1159
|
+
description: "escalation rule users",
|
|
1160
|
+
projectId: projectId,
|
|
1161
|
+
completeness: completeness,
|
|
1162
|
+
service: OnCallDutyPolicyEscalationRuleUserService,
|
|
1163
|
+
query: directQuery,
|
|
1164
|
+
select: {
|
|
1165
|
+
_id: true,
|
|
1166
|
+
userId: true,
|
|
1167
|
+
},
|
|
1168
|
+
consumePage: (rows: Array<OnCallDutyPolicyEscalationRuleUser>): void => {
|
|
1169
|
+
for (const row of rows) {
|
|
1170
|
+
addResponder(row.userId, ResponderSource.Direct);
|
|
1171
|
+
}
|
|
1172
|
+
},
|
|
1173
|
+
});
|
|
1174
|
+
|
|
1175
|
+
// 2. Teams attached to an escalation rule, expanded to their members.
|
|
1176
|
+
|
|
1177
|
+
const teamQuery: Query<OnCallDutyPolicyEscalationRuleTeam> = {
|
|
1178
|
+
projectId: projectId,
|
|
1179
|
+
};
|
|
1180
|
+
|
|
1181
|
+
if (onCallDutyPolicyId) {
|
|
1182
|
+
teamQuery.onCallDutyPolicyId = onCallDutyPolicyId;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
const escalationTeams: Array<OnCallDutyPolicyEscalationRuleTeam> = [];
|
|
1186
|
+
|
|
1187
|
+
await this.readEveryPage<OnCallDutyPolicyEscalationRuleTeam>({
|
|
1188
|
+
description: "escalation rule teams",
|
|
1189
|
+
projectId: projectId,
|
|
1190
|
+
completeness: completeness,
|
|
1191
|
+
service: OnCallDutyPolicyEscalationRuleTeamService,
|
|
1192
|
+
query: teamQuery,
|
|
1193
|
+
select: {
|
|
1194
|
+
_id: true,
|
|
1195
|
+
teamId: true,
|
|
1196
|
+
},
|
|
1197
|
+
consumePage: (rows: Array<OnCallDutyPolicyEscalationRuleTeam>): void => {
|
|
1198
|
+
escalationTeams.push(...rows);
|
|
1199
|
+
},
|
|
1200
|
+
});
|
|
1201
|
+
|
|
1202
|
+
const teamIds: Array<ObjectID> = this.distinctIds(
|
|
1203
|
+
escalationTeams.map(
|
|
1204
|
+
(team: OnCallDutyPolicyEscalationRuleTeam): ObjectID | undefined => {
|
|
1205
|
+
return team.teamId;
|
|
1206
|
+
},
|
|
1207
|
+
),
|
|
1208
|
+
);
|
|
1209
|
+
|
|
1210
|
+
if (teamIds.length > 0) {
|
|
1211
|
+
/*
|
|
1212
|
+
* ONE paged read for every team on the policy, not one per team. This is the N+1
|
|
1213
|
+
* that made TeamComplianceService unusable on a project of any size — and the page
|
|
1214
|
+
* loop is what keeps the fix from re-introducing the truncation it replaced, since
|
|
1215
|
+
* five thousand users across three teams is fifteen thousand membership rows and
|
|
1216
|
+
* the old single read returned ten thousand of them.
|
|
1217
|
+
*/
|
|
1218
|
+
await this.readEveryPage<TeamMember>({
|
|
1219
|
+
description: "team members of escalation rule teams",
|
|
1220
|
+
projectId: projectId,
|
|
1221
|
+
completeness: completeness,
|
|
1222
|
+
service: TeamMemberService,
|
|
1223
|
+
query: {
|
|
1224
|
+
projectId: projectId,
|
|
1225
|
+
teamId: new Includes(teamIds),
|
|
1226
|
+
},
|
|
1227
|
+
select: {
|
|
1228
|
+
_id: true,
|
|
1229
|
+
userId: true,
|
|
1230
|
+
/*
|
|
1231
|
+
* WHICH team, not merely that a team was involved. One extra column on a read
|
|
1232
|
+
* that already runs, and it is what lets the readiness table be filtered down
|
|
1233
|
+
* to a team without a second pass that could disagree with this one about who
|
|
1234
|
+
* is on it.
|
|
1235
|
+
*/
|
|
1236
|
+
teamId: true,
|
|
1237
|
+
},
|
|
1238
|
+
consumePage: (rows: Array<TeamMember>): void => {
|
|
1239
|
+
for (const row of rows) {
|
|
1240
|
+
addResponder(row.userId, ResponderSource.Team, row.teamId);
|
|
1241
|
+
}
|
|
1242
|
+
},
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
// 3. Schedules attached to an escalation rule, expanded to their layer users.
|
|
1247
|
+
|
|
1248
|
+
const scheduleQuery: Query<OnCallDutyPolicyEscalationRuleSchedule> = {
|
|
1249
|
+
projectId: projectId,
|
|
1250
|
+
};
|
|
1251
|
+
|
|
1252
|
+
if (onCallDutyPolicyId) {
|
|
1253
|
+
scheduleQuery.onCallDutyPolicyId = onCallDutyPolicyId;
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
const escalationSchedules: Array<OnCallDutyPolicyEscalationRuleSchedule> =
|
|
1257
|
+
[];
|
|
1258
|
+
|
|
1259
|
+
await this.readEveryPage<OnCallDutyPolicyEscalationRuleSchedule>({
|
|
1260
|
+
description: "escalation rule schedules",
|
|
1261
|
+
projectId: projectId,
|
|
1262
|
+
completeness: completeness,
|
|
1263
|
+
service: OnCallDutyPolicyEscalationRuleScheduleService,
|
|
1264
|
+
query: scheduleQuery,
|
|
1265
|
+
select: {
|
|
1266
|
+
_id: true,
|
|
1267
|
+
onCallDutyPolicyScheduleId: true,
|
|
1268
|
+
},
|
|
1269
|
+
consumePage: (
|
|
1270
|
+
rows: Array<OnCallDutyPolicyEscalationRuleSchedule>,
|
|
1271
|
+
): void => {
|
|
1272
|
+
escalationSchedules.push(...rows);
|
|
1273
|
+
},
|
|
1274
|
+
});
|
|
1275
|
+
|
|
1276
|
+
const scheduleIds: Array<ObjectID> = this.distinctIds(
|
|
1277
|
+
escalationSchedules.map(
|
|
1278
|
+
(
|
|
1279
|
+
schedule: OnCallDutyPolicyEscalationRuleSchedule,
|
|
1280
|
+
): ObjectID | undefined => {
|
|
1281
|
+
return schedule.onCallDutyPolicyScheduleId;
|
|
1282
|
+
},
|
|
1283
|
+
),
|
|
1284
|
+
);
|
|
1285
|
+
|
|
1286
|
+
if (scheduleIds.length > 0) {
|
|
1287
|
+
/*
|
|
1288
|
+
* Going through the escalation-rule join even for the project-wide scope is
|
|
1289
|
+
* deliberate: a schedule that is not attached to any policy pages nobody, and
|
|
1290
|
+
* listing its members as unready responders would be noise an admin cannot act on.
|
|
1291
|
+
*/
|
|
1292
|
+
await this.readEveryPage<OnCallDutyPolicyScheduleLayerUser>({
|
|
1293
|
+
description: "schedule layer users",
|
|
1294
|
+
projectId: projectId,
|
|
1295
|
+
completeness: completeness,
|
|
1296
|
+
service: OnCallDutyPolicyScheduleLayerUserService,
|
|
1297
|
+
query: {
|
|
1298
|
+
projectId: projectId,
|
|
1299
|
+
onCallDutyPolicyScheduleId: new Includes(scheduleIds),
|
|
1300
|
+
},
|
|
1301
|
+
select: {
|
|
1302
|
+
_id: true,
|
|
1303
|
+
userId: true,
|
|
1304
|
+
},
|
|
1305
|
+
consumePage: (rows: Array<OnCallDutyPolicyScheduleLayerUser>): void => {
|
|
1306
|
+
for (const row of rows) {
|
|
1307
|
+
addResponder(row.userId, ResponderSource.Schedule);
|
|
1308
|
+
}
|
|
1309
|
+
},
|
|
1310
|
+
});
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
// 4. Users that overrides route pages to.
|
|
1314
|
+
|
|
1315
|
+
const overrideQuery: Query<OnCallDutyPolicyUserOverride> = {
|
|
1316
|
+
projectId: projectId,
|
|
1317
|
+
/*
|
|
1318
|
+
* An override that has already ended routes nothing, so it is not a reason anyone
|
|
1319
|
+
* is a responder. Future overrides ARE included: the substitute needs to be
|
|
1320
|
+
* reachable before their window opens, not discovered to be unreachable during it.
|
|
1321
|
+
*/
|
|
1322
|
+
endsAt: QueryHelper.greaterThanEqualTo(OneUptimeDate.getCurrentDate()),
|
|
1323
|
+
};
|
|
1324
|
+
|
|
1325
|
+
if (onCallDutyPolicyId) {
|
|
1326
|
+
/*
|
|
1327
|
+
* equalToOrNull, matching getRouteAlertToUserId exactly: an override with a NULL
|
|
1328
|
+
* policy id is a GLOBAL override and applies to this policy too. Filtering on
|
|
1329
|
+
* equality alone would drop every global override, which is the single most
|
|
1330
|
+
* commonly configured kind.
|
|
1331
|
+
*/
|
|
1332
|
+
overrideQuery.onCallDutyPolicyId =
|
|
1333
|
+
QueryHelper.equalToOrNull(onCallDutyPolicyId);
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
await this.readEveryPage<OnCallDutyPolicyUserOverride>({
|
|
1337
|
+
description: "on-call user overrides",
|
|
1338
|
+
projectId: projectId,
|
|
1339
|
+
completeness: completeness,
|
|
1340
|
+
service: OnCallDutyPolicyUserOverrideService,
|
|
1341
|
+
query: overrideQuery,
|
|
1342
|
+
select: {
|
|
1343
|
+
_id: true,
|
|
1344
|
+
routeAlertsToUserId: true,
|
|
1345
|
+
},
|
|
1346
|
+
consumePage: (rows: Array<OnCallDutyPolicyUserOverride>): void => {
|
|
1347
|
+
for (const row of rows) {
|
|
1348
|
+
addResponder(row.routeAlertsToUserId, ResponderSource.Override);
|
|
1349
|
+
}
|
|
1350
|
+
},
|
|
1351
|
+
});
|
|
1352
|
+
|
|
1353
|
+
return responders;
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
/**
|
|
1357
|
+
* The same four sources as resolveResponders, but asked the other way round: not "who
|
|
1358
|
+
* does this project page" but "how, if at all, does this project page THESE people".
|
|
1359
|
+
*
|
|
1360
|
+
* The difference is the entire fix for the amplified N+1. Answering `reachedVia` for a
|
|
1361
|
+
* forty-member team by resolving the whole project's responder set means reading every
|
|
1362
|
+
* escalation rule, every team's full membership and every schedule's full layer roster
|
|
1363
|
+
* — work proportional to the project, repeated per caller, to produce four booleans per
|
|
1364
|
+
* user. Every read here is keyed on the userIds actually asked about instead, and the
|
|
1365
|
+
* two that cannot be (which teams and which schedules are attached to a policy) are
|
|
1366
|
+
* keyed on just those users' teams and schedules.
|
|
1367
|
+
*
|
|
1368
|
+
* Users with no source at all stay in the map with an empty set, because a user who is
|
|
1369
|
+
* on no policy yet is exactly who the add-responder modal is asking about.
|
|
1370
|
+
*/
|
|
1371
|
+
private static async resolveRespondersForUsers(data: {
|
|
1372
|
+
projectId: ObjectID;
|
|
1373
|
+
userIds: Array<ObjectID>;
|
|
1374
|
+
teamIdsByUserId: Map<string, Array<ObjectID>>;
|
|
1375
|
+
completeness: ReadCompleteness;
|
|
1376
|
+
}): Promise<Map<string, ResponderAttachment>> {
|
|
1377
|
+
const responders: Map<string, ResponderAttachment> = new Map<
|
|
1378
|
+
string,
|
|
1379
|
+
ResponderAttachment
|
|
1380
|
+
>();
|
|
1381
|
+
|
|
1382
|
+
for (const userId of data.userIds) {
|
|
1383
|
+
responders.set(userId.toString(), {
|
|
1384
|
+
sources: new Set<ResponderSource>(),
|
|
1385
|
+
teamIds: new Set<string>(),
|
|
1386
|
+
});
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
type AddSourceFunction = (
|
|
1390
|
+
userId: ObjectID | undefined,
|
|
1391
|
+
source: ResponderSource,
|
|
1392
|
+
teamId?: ObjectID | undefined,
|
|
1393
|
+
) => void;
|
|
1394
|
+
|
|
1395
|
+
const addSource: AddSourceFunction = (
|
|
1396
|
+
userId: ObjectID | undefined,
|
|
1397
|
+
source: ResponderSource,
|
|
1398
|
+
teamId?: ObjectID | undefined,
|
|
1399
|
+
): void => {
|
|
1400
|
+
if (!userId) {
|
|
1401
|
+
return;
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
/*
|
|
1405
|
+
* A row for somebody we were not asked about is dropped rather than added. The
|
|
1406
|
+
* reads are all filtered on the user set already; this is the guard that keeps a
|
|
1407
|
+
* future unfiltered read from silently widening the answer.
|
|
1408
|
+
*/
|
|
1409
|
+
const attachment: ResponderAttachment | undefined = responders.get(
|
|
1410
|
+
userId.toString(),
|
|
1411
|
+
);
|
|
1412
|
+
|
|
1413
|
+
if (!attachment) {
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
attachment.sources.add(source);
|
|
1418
|
+
|
|
1419
|
+
if (teamId) {
|
|
1420
|
+
attachment.teamIds.add(teamId.toString());
|
|
1421
|
+
}
|
|
1422
|
+
};
|
|
1423
|
+
|
|
1424
|
+
// 1. Attached directly to an escalation rule.
|
|
1425
|
+
|
|
1426
|
+
await this.readEveryPage<OnCallDutyPolicyEscalationRuleUser>({
|
|
1427
|
+
description: "escalation rule users for a user set",
|
|
1428
|
+
projectId: data.projectId,
|
|
1429
|
+
completeness: data.completeness,
|
|
1430
|
+
service: OnCallDutyPolicyEscalationRuleUserService,
|
|
1431
|
+
query: {
|
|
1432
|
+
projectId: data.projectId,
|
|
1433
|
+
userId: new Includes(data.userIds),
|
|
1434
|
+
},
|
|
1435
|
+
select: {
|
|
1436
|
+
_id: true,
|
|
1437
|
+
userId: true,
|
|
1438
|
+
},
|
|
1439
|
+
consumePage: (rows: Array<OnCallDutyPolicyEscalationRuleUser>): void => {
|
|
1440
|
+
for (const row of rows) {
|
|
1441
|
+
addSource(row.userId, ResponderSource.Direct);
|
|
1442
|
+
}
|
|
1443
|
+
},
|
|
1444
|
+
});
|
|
1445
|
+
|
|
1446
|
+
// 2. In a team that is attached to an escalation rule.
|
|
1447
|
+
|
|
1448
|
+
const memberTeamIds: Array<ObjectID> = this.distinctIds(
|
|
1449
|
+
Array.from(data.teamIdsByUserId.values()).flat(),
|
|
1450
|
+
);
|
|
1451
|
+
|
|
1452
|
+
const attachedTeamIds: Set<string> = new Set<string>();
|
|
1453
|
+
|
|
1454
|
+
if (memberTeamIds.length > 0) {
|
|
1455
|
+
await this.readEveryPage<OnCallDutyPolicyEscalationRuleTeam>({
|
|
1456
|
+
description: "escalation rule teams for a user set",
|
|
1457
|
+
projectId: data.projectId,
|
|
1458
|
+
completeness: data.completeness,
|
|
1459
|
+
service: OnCallDutyPolicyEscalationRuleTeamService,
|
|
1460
|
+
query: {
|
|
1461
|
+
projectId: data.projectId,
|
|
1462
|
+
teamId: new Includes(memberTeamIds),
|
|
1463
|
+
},
|
|
1464
|
+
select: {
|
|
1465
|
+
_id: true,
|
|
1466
|
+
teamId: true,
|
|
1467
|
+
},
|
|
1468
|
+
consumePage: (
|
|
1469
|
+
rows: Array<OnCallDutyPolicyEscalationRuleTeam>,
|
|
1470
|
+
): void => {
|
|
1471
|
+
for (const row of rows) {
|
|
1472
|
+
if (row.teamId) {
|
|
1473
|
+
attachedTeamIds.add(row.teamId.toString());
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
},
|
|
1477
|
+
});
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
if (attachedTeamIds.size > 0) {
|
|
1481
|
+
for (const userId of data.userIds) {
|
|
1482
|
+
const teamIds: Array<ObjectID> =
|
|
1483
|
+
data.teamIdsByUserId.get(userId.toString()) || [];
|
|
1484
|
+
|
|
1485
|
+
/*
|
|
1486
|
+
* Every matching team, not the first one and not a boolean. A user on two
|
|
1487
|
+
* attached teams is paged by both, so both belong on the row and both have to
|
|
1488
|
+
* match a team filter — the boolean this replaced could only ever have said
|
|
1489
|
+
* "some team", which is precisely the answer that made the team filter
|
|
1490
|
+
* impossible to build.
|
|
1491
|
+
*/
|
|
1492
|
+
for (const teamId of teamIds) {
|
|
1493
|
+
if (attachedTeamIds.has(teamId.toString())) {
|
|
1494
|
+
addSource(userId, ResponderSource.Team, teamId);
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
// 3. On a layer of a schedule that is attached to an escalation rule.
|
|
1501
|
+
|
|
1502
|
+
const scheduleIdsByUserId: Map<string, Array<ObjectID>> = new Map<
|
|
1503
|
+
string,
|
|
1504
|
+
Array<ObjectID>
|
|
1505
|
+
>();
|
|
1506
|
+
|
|
1507
|
+
await this.readEveryPage<OnCallDutyPolicyScheduleLayerUser>({
|
|
1508
|
+
description: "schedule layer users for a user set",
|
|
1509
|
+
projectId: data.projectId,
|
|
1510
|
+
completeness: data.completeness,
|
|
1511
|
+
service: OnCallDutyPolicyScheduleLayerUserService,
|
|
1512
|
+
query: {
|
|
1513
|
+
projectId: data.projectId,
|
|
1514
|
+
userId: new Includes(data.userIds),
|
|
1515
|
+
},
|
|
1516
|
+
select: {
|
|
1517
|
+
_id: true,
|
|
1518
|
+
userId: true,
|
|
1519
|
+
onCallDutyPolicyScheduleId: true,
|
|
1520
|
+
},
|
|
1521
|
+
consumePage: (rows: Array<OnCallDutyPolicyScheduleLayerUser>): void => {
|
|
1522
|
+
for (const row of rows) {
|
|
1523
|
+
if (!row.userId || !row.onCallDutyPolicyScheduleId) {
|
|
1524
|
+
continue;
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
const key: string = row.userId.toString();
|
|
1528
|
+
const existing: Array<ObjectID> | undefined =
|
|
1529
|
+
scheduleIdsByUserId.get(key);
|
|
1530
|
+
|
|
1531
|
+
if (existing) {
|
|
1532
|
+
existing.push(row.onCallDutyPolicyScheduleId);
|
|
1533
|
+
continue;
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
scheduleIdsByUserId.set(key, [row.onCallDutyPolicyScheduleId]);
|
|
1537
|
+
}
|
|
1538
|
+
},
|
|
1539
|
+
});
|
|
1540
|
+
|
|
1541
|
+
const memberScheduleIds: Array<ObjectID> = this.distinctIds(
|
|
1542
|
+
Array.from(scheduleIdsByUserId.values()).flat(),
|
|
1543
|
+
);
|
|
1544
|
+
|
|
1545
|
+
const attachedScheduleIds: Set<string> = new Set<string>();
|
|
1546
|
+
|
|
1547
|
+
if (memberScheduleIds.length > 0) {
|
|
1548
|
+
await this.readEveryPage<OnCallDutyPolicyEscalationRuleSchedule>({
|
|
1549
|
+
description: "escalation rule schedules for a user set",
|
|
1550
|
+
projectId: data.projectId,
|
|
1551
|
+
completeness: data.completeness,
|
|
1552
|
+
service: OnCallDutyPolicyEscalationRuleScheduleService,
|
|
1553
|
+
query: {
|
|
1554
|
+
projectId: data.projectId,
|
|
1555
|
+
onCallDutyPolicyScheduleId: new Includes(memberScheduleIds),
|
|
1556
|
+
},
|
|
1557
|
+
select: {
|
|
1558
|
+
_id: true,
|
|
1559
|
+
onCallDutyPolicyScheduleId: true,
|
|
1560
|
+
},
|
|
1561
|
+
consumePage: (
|
|
1562
|
+
rows: Array<OnCallDutyPolicyEscalationRuleSchedule>,
|
|
1563
|
+
): void => {
|
|
1564
|
+
for (const row of rows) {
|
|
1565
|
+
if (row.onCallDutyPolicyScheduleId) {
|
|
1566
|
+
attachedScheduleIds.add(
|
|
1567
|
+
row.onCallDutyPolicyScheduleId.toString(),
|
|
1568
|
+
);
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
},
|
|
1572
|
+
});
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
if (attachedScheduleIds.size > 0) {
|
|
1576
|
+
for (const userId of data.userIds) {
|
|
1577
|
+
const scheduleIds: Array<ObjectID> =
|
|
1578
|
+
scheduleIdsByUserId.get(userId.toString()) || [];
|
|
1579
|
+
|
|
1580
|
+
const isOnAnAttachedSchedule: boolean = scheduleIds.some(
|
|
1581
|
+
(scheduleId: ObjectID): boolean => {
|
|
1582
|
+
return attachedScheduleIds.has(scheduleId.toString());
|
|
1583
|
+
},
|
|
1584
|
+
);
|
|
1585
|
+
|
|
1586
|
+
if (isOnAnAttachedSchedule) {
|
|
1587
|
+
addSource(userId, ResponderSource.Schedule);
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
// 4. Substituted in by an override that has not ended.
|
|
1593
|
+
|
|
1594
|
+
await this.readEveryPage<OnCallDutyPolicyUserOverride>({
|
|
1595
|
+
description: "on-call user overrides for a user set",
|
|
1596
|
+
projectId: data.projectId,
|
|
1597
|
+
completeness: data.completeness,
|
|
1598
|
+
service: OnCallDutyPolicyUserOverrideService,
|
|
1599
|
+
query: {
|
|
1600
|
+
projectId: data.projectId,
|
|
1601
|
+
routeAlertsToUserId: new Includes(data.userIds),
|
|
1602
|
+
endsAt: QueryHelper.greaterThanEqualTo(OneUptimeDate.getCurrentDate()),
|
|
1603
|
+
},
|
|
1604
|
+
select: {
|
|
1605
|
+
_id: true,
|
|
1606
|
+
routeAlertsToUserId: true,
|
|
1607
|
+
},
|
|
1608
|
+
consumePage: (rows: Array<OnCallDutyPolicyUserOverride>): void => {
|
|
1609
|
+
for (const row of rows) {
|
|
1610
|
+
addSource(row.routeAlertsToUserId, ResponderSource.Override);
|
|
1611
|
+
}
|
|
1612
|
+
},
|
|
1613
|
+
});
|
|
1614
|
+
|
|
1615
|
+
return responders;
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
/**
|
|
1619
|
+
* Which teams each of these users belongs to in this project — and, by existing at
|
|
1620
|
+
* all, whether they belong to the project.
|
|
1621
|
+
*
|
|
1622
|
+
* One read doing both jobs is not a trick: team membership IS what "in this project"
|
|
1623
|
+
* means for a User, which is a global model. Reading the teamIds at the same time is
|
|
1624
|
+
* free and is what lets the Team responder source be resolved without expanding every
|
|
1625
|
+
* team in the project into its full membership.
|
|
1626
|
+
*/
|
|
1627
|
+
private static async loadProjectMembership(
|
|
1628
|
+
projectId: ObjectID,
|
|
1629
|
+
userIds: Array<ObjectID>,
|
|
1630
|
+
completeness: ReadCompleteness,
|
|
1631
|
+
): Promise<Map<string, Array<ObjectID>>> {
|
|
1632
|
+
const teamIdsByUserId: Map<string, Array<ObjectID>> = new Map<
|
|
1633
|
+
string,
|
|
1634
|
+
Array<ObjectID>
|
|
1635
|
+
>();
|
|
1636
|
+
|
|
1637
|
+
await this.readEveryPage<TeamMember>({
|
|
1638
|
+
description: "project membership for a user set",
|
|
1639
|
+
projectId: projectId,
|
|
1640
|
+
completeness: completeness,
|
|
1641
|
+
service: TeamMemberService,
|
|
1642
|
+
query: {
|
|
1643
|
+
projectId: projectId,
|
|
1644
|
+
userId: new Includes(userIds),
|
|
1645
|
+
},
|
|
1646
|
+
select: {
|
|
1647
|
+
_id: true,
|
|
1648
|
+
userId: true,
|
|
1649
|
+
teamId: true,
|
|
1650
|
+
},
|
|
1651
|
+
consumePage: (rows: Array<TeamMember>): void => {
|
|
1652
|
+
for (const row of rows) {
|
|
1653
|
+
if (!row.userId) {
|
|
1654
|
+
continue;
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
const key: string = row.userId.toString();
|
|
1658
|
+
const existing: Array<ObjectID> | undefined =
|
|
1659
|
+
teamIdsByUserId.get(key);
|
|
1660
|
+
|
|
1661
|
+
if (existing) {
|
|
1662
|
+
if (row.teamId) {
|
|
1663
|
+
existing.push(row.teamId);
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
continue;
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
teamIdsByUserId.set(key, row.teamId ? [row.teamId] : []);
|
|
1670
|
+
}
|
|
1671
|
+
},
|
|
1672
|
+
});
|
|
1673
|
+
|
|
1674
|
+
return teamIdsByUserId;
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
/**
|
|
1678
|
+
* Turn a resolved responder set into per-user readiness. Every read here is batched
|
|
1679
|
+
* over the whole set; the number of queries does not grow with the number of users or
|
|
1680
|
+
* the number of severities.
|
|
1681
|
+
*/
|
|
1682
|
+
private static async buildReadiness(data: {
|
|
1683
|
+
projectId: ObjectID;
|
|
1684
|
+
responders: Map<string, ResponderAttachment>;
|
|
1685
|
+
projectSettings: ProjectNotificationSettings;
|
|
1686
|
+
completeness: ReadCompleteness;
|
|
1687
|
+
}): Promise<Array<UserReadiness>> {
|
|
1688
|
+
const userIds: Array<ObjectID> = Array.from(data.responders.keys()).map(
|
|
1689
|
+
(userId: string): ObjectID => {
|
|
1690
|
+
return new ObjectID(userId);
|
|
1691
|
+
},
|
|
1692
|
+
);
|
|
1693
|
+
|
|
1694
|
+
if (userIds.length === 0) {
|
|
1695
|
+
return [];
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
const inputs: ReadinessInputs = await this.loadInputs({
|
|
1699
|
+
projectId: data.projectId,
|
|
1700
|
+
userIds: userIds,
|
|
1701
|
+
projectSettings: data.projectSettings,
|
|
1702
|
+
completeness: data.completeness,
|
|
1703
|
+
});
|
|
1704
|
+
|
|
1705
|
+
/*
|
|
1706
|
+
* ONE read for every team named anywhere in the responder set, rather than one per
|
|
1707
|
+
* responder. A team on an escalation rule is shared by every one of its members, so
|
|
1708
|
+
* the per-member read would be the same N+1 that made the report this service
|
|
1709
|
+
* replaced unusable — see readEveryPage.
|
|
1710
|
+
*/
|
|
1711
|
+
const teamNamesById: Map<string, string> = await this.loadTeamNames({
|
|
1712
|
+
projectId: data.projectId,
|
|
1713
|
+
teamIds: this.distinctIds(
|
|
1714
|
+
Array.from(data.responders.values())
|
|
1715
|
+
.flatMap((attachment: ResponderAttachment): Array<string> => {
|
|
1716
|
+
return Array.from(attachment.teamIds);
|
|
1717
|
+
})
|
|
1718
|
+
.map((teamId: string): ObjectID => {
|
|
1719
|
+
return new ObjectID(teamId);
|
|
1720
|
+
}),
|
|
1721
|
+
),
|
|
1722
|
+
completeness: data.completeness,
|
|
1723
|
+
});
|
|
1724
|
+
|
|
1725
|
+
const readiness: Array<UserReadiness> = [];
|
|
1726
|
+
|
|
1727
|
+
for (const user of inputs.users) {
|
|
1728
|
+
const userIdString: string = user.id?.toString() || "";
|
|
1729
|
+
const attachment: ResponderAttachment = data.responders.get(
|
|
1730
|
+
userIdString,
|
|
1731
|
+
) || {
|
|
1732
|
+
sources: new Set<ResponderSource>(),
|
|
1733
|
+
teamIds: new Set<string>(),
|
|
1734
|
+
};
|
|
1735
|
+
|
|
1736
|
+
readiness.push(
|
|
1737
|
+
this.buildUserReadiness(
|
|
1738
|
+
user,
|
|
1739
|
+
attachment,
|
|
1740
|
+
userIdString,
|
|
1741
|
+
inputs,
|
|
1742
|
+
teamNamesById,
|
|
1743
|
+
),
|
|
1744
|
+
);
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
return this.sortReadiness(readiness);
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
/**
|
|
1751
|
+
* Team id -> team name, for every team that pages somebody in the responder set.
|
|
1752
|
+
*
|
|
1753
|
+
* A team whose row did not come back is simply absent from the map, and
|
|
1754
|
+
* buildUserReadiness then drops it from the responder's `teams` rather than rendering an
|
|
1755
|
+
* id or an empty chip. That is the right direction to fail in for a filter: an option
|
|
1756
|
+
* that cannot be labelled is an option nobody can choose deliberately, whereas a chip
|
|
1757
|
+
* reading a bare uuid is one an admin might act on.
|
|
1758
|
+
*/
|
|
1759
|
+
private static async loadTeamNames(data: {
|
|
1760
|
+
projectId: ObjectID;
|
|
1761
|
+
teamIds: Array<ObjectID>;
|
|
1762
|
+
completeness: ReadCompleteness;
|
|
1763
|
+
}): Promise<Map<string, string>> {
|
|
1764
|
+
const teamNamesById: Map<string, string> = new Map<string, string>();
|
|
1765
|
+
|
|
1766
|
+
if (data.teamIds.length === 0) {
|
|
1767
|
+
return teamNamesById;
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
await this.readEveryPage<Team>({
|
|
1771
|
+
description: "teams that page a responder",
|
|
1772
|
+
projectId: data.projectId,
|
|
1773
|
+
completeness: data.completeness,
|
|
1774
|
+
service: TeamService,
|
|
1775
|
+
query: {
|
|
1776
|
+
projectId: data.projectId,
|
|
1777
|
+
_id: new Includes(data.teamIds),
|
|
1778
|
+
},
|
|
1779
|
+
select: {
|
|
1780
|
+
_id: true,
|
|
1781
|
+
name: true,
|
|
1782
|
+
},
|
|
1783
|
+
consumePage: (rows: Array<Team>): void => {
|
|
1784
|
+
for (const row of rows) {
|
|
1785
|
+
if (row.id && row.name) {
|
|
1786
|
+
teamNamesById.set(row.id.toString(), row.name);
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
},
|
|
1790
|
+
});
|
|
1791
|
+
|
|
1792
|
+
return teamNamesById;
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
private static sortReadiness(
|
|
1796
|
+
readiness: Array<UserReadiness>,
|
|
1797
|
+
): Array<UserReadiness> {
|
|
1798
|
+
readiness.sort((a: UserReadiness, b: UserReadiness): number => {
|
|
1799
|
+
const rankDifference: number =
|
|
1800
|
+
STATUS_SORT_RANK[a.status] - STATUS_SORT_RANK[b.status];
|
|
1801
|
+
|
|
1802
|
+
if (rankDifference !== 0) {
|
|
1803
|
+
return rankDifference;
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
return a.userName.localeCompare(b.userName);
|
|
1807
|
+
});
|
|
1808
|
+
|
|
1809
|
+
return readiness;
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
/*
|
|
1813
|
+
* Read the project's switches once. They are what decides whether "no rule for Sev4"
|
|
1814
|
+
* means "falls back to email" or "is dropped on the floor", and whether a verified SMS
|
|
1815
|
+
* number is a way to reach somebody or a decoration. A readiness surface that cannot
|
|
1816
|
+
* tell those apart is not diagnosing anything.
|
|
1817
|
+
*/
|
|
1818
|
+
private static async loadProjectSettings(
|
|
1819
|
+
projectId: ObjectID,
|
|
1820
|
+
): Promise<ProjectNotificationSettings> {
|
|
1821
|
+
const project: Project | null = await ProjectService.findOneById({
|
|
1822
|
+
id: projectId,
|
|
1823
|
+
select: {
|
|
1824
|
+
_id: true,
|
|
1825
|
+
disableOnCallNotificationFallback: true,
|
|
1826
|
+
enableSmsNotifications: true,
|
|
1827
|
+
enableCallNotifications: true,
|
|
1828
|
+
enableWhatsAppNotifications: true,
|
|
1829
|
+
enableTelegramNotifications: true,
|
|
1830
|
+
},
|
|
1831
|
+
props: {
|
|
1832
|
+
isRoot: true,
|
|
1833
|
+
},
|
|
1834
|
+
});
|
|
1835
|
+
|
|
1836
|
+
/*
|
|
1837
|
+
* A missing project row reads as every paid channel OFF and the fallback ON, which
|
|
1838
|
+
* is the pairing that produces the loudest answer rather than the most convenient
|
|
1839
|
+
* one. Defaulting a switch to "on" would let a project we could not read certify
|
|
1840
|
+
* responders as reachable on channels that may be switched off.
|
|
1841
|
+
*/
|
|
1842
|
+
return {
|
|
1843
|
+
isFallbackDisabled: Boolean(project?.disableOnCallNotificationFallback),
|
|
1844
|
+
enableSmsNotifications: Boolean(project?.enableSmsNotifications),
|
|
1845
|
+
enableCallNotifications: Boolean(project?.enableCallNotifications),
|
|
1846
|
+
enableWhatsAppNotifications: Boolean(
|
|
1847
|
+
project?.enableWhatsAppNotifications,
|
|
1848
|
+
),
|
|
1849
|
+
enableTelegramNotifications: Boolean(
|
|
1850
|
+
project?.enableTelegramNotifications,
|
|
1851
|
+
),
|
|
1852
|
+
};
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
private static async loadInputs(data: {
|
|
1856
|
+
projectId: ObjectID;
|
|
1857
|
+
userIds: Array<ObjectID>;
|
|
1858
|
+
projectSettings: ProjectNotificationSettings;
|
|
1859
|
+
completeness: ReadCompleteness;
|
|
1860
|
+
}): Promise<ReadinessInputs> {
|
|
1861
|
+
const users: Array<User> = [];
|
|
1862
|
+
|
|
1863
|
+
await this.readEveryPage<User>({
|
|
1864
|
+
description: "responder user records",
|
|
1865
|
+
projectId: data.projectId,
|
|
1866
|
+
completeness: data.completeness,
|
|
1867
|
+
service: UserService,
|
|
1868
|
+
query: {
|
|
1869
|
+
_id: new Includes(data.userIds),
|
|
1870
|
+
},
|
|
1871
|
+
select: {
|
|
1872
|
+
_id: true,
|
|
1873
|
+
name: true,
|
|
1874
|
+
email: true,
|
|
1875
|
+
profilePictureId: true,
|
|
1876
|
+
},
|
|
1877
|
+
consumePage: (rows: Array<User>): void => {
|
|
1878
|
+
users.push(...rows);
|
|
1879
|
+
},
|
|
1880
|
+
});
|
|
1881
|
+
|
|
1882
|
+
const methodsByUserId: Map<
|
|
1883
|
+
string,
|
|
1884
|
+
Array<ReadinessMethod>
|
|
1885
|
+
> = await this.loadMethods(data.projectId, data.userIds, data.completeness);
|
|
1886
|
+
|
|
1887
|
+
const coverageByKey: Map<string, CoverageState> =
|
|
1888
|
+
await this.loadCoverageIndex(
|
|
1889
|
+
data.projectId,
|
|
1890
|
+
data.userIds,
|
|
1891
|
+
data.completeness,
|
|
1892
|
+
);
|
|
1893
|
+
|
|
1894
|
+
const incidentSeverityModels: Array<IncidentSeverity> = [];
|
|
1895
|
+
|
|
1896
|
+
await this.readEveryPage<IncidentSeverity>({
|
|
1897
|
+
description: "incident severities",
|
|
1898
|
+
projectId: data.projectId,
|
|
1899
|
+
completeness: data.completeness,
|
|
1900
|
+
service: IncidentSeverityService,
|
|
1901
|
+
query: {
|
|
1902
|
+
projectId: data.projectId,
|
|
1903
|
+
},
|
|
1904
|
+
select: {
|
|
1905
|
+
_id: true,
|
|
1906
|
+
name: true,
|
|
1907
|
+
},
|
|
1908
|
+
sort: {
|
|
1909
|
+
order: SortOrder.Ascending,
|
|
1910
|
+
},
|
|
1911
|
+
consumePage: (rows: Array<IncidentSeverity>): void => {
|
|
1912
|
+
incidentSeverityModels.push(...rows);
|
|
1913
|
+
},
|
|
1914
|
+
});
|
|
1915
|
+
|
|
1916
|
+
const alertSeverityModels: Array<AlertSeverity> = [];
|
|
1917
|
+
|
|
1918
|
+
await this.readEveryPage<AlertSeverity>({
|
|
1919
|
+
description: "alert severities",
|
|
1920
|
+
projectId: data.projectId,
|
|
1921
|
+
completeness: data.completeness,
|
|
1922
|
+
service: AlertSeverityService,
|
|
1923
|
+
query: {
|
|
1924
|
+
projectId: data.projectId,
|
|
1925
|
+
},
|
|
1926
|
+
select: {
|
|
1927
|
+
_id: true,
|
|
1928
|
+
name: true,
|
|
1929
|
+
},
|
|
1930
|
+
sort: {
|
|
1931
|
+
order: SortOrder.Ascending,
|
|
1932
|
+
},
|
|
1933
|
+
consumePage: (rows: Array<AlertSeverity>): void => {
|
|
1934
|
+
alertSeverityModels.push(...rows);
|
|
1935
|
+
},
|
|
1936
|
+
});
|
|
1937
|
+
|
|
1938
|
+
return {
|
|
1939
|
+
users: users,
|
|
1940
|
+
methodsByUserId: methodsByUserId,
|
|
1941
|
+
coverageByKey: coverageByKey,
|
|
1942
|
+
incidentSeverities: this.toSeverityRefs(incidentSeverityModels),
|
|
1943
|
+
alertSeverities: this.toSeverityRefs(alertSeverityModels),
|
|
1944
|
+
projectSettings: data.projectSettings,
|
|
1945
|
+
};
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
/**
|
|
1949
|
+
* One paged read per method model, each over the whole responder set.
|
|
1950
|
+
*
|
|
1951
|
+
* All SEVEN channels are here. TeamComplianceService looked at four and therefore told
|
|
1952
|
+
* a responder whose only method was Telegram, WhatsApp or Webhook that they were
|
|
1953
|
+
* non-compliant while the runtime was quite happily paging them — a false alarm that
|
|
1954
|
+
* teaches admins to ignore the table, which is worse than the table not existing.
|
|
1955
|
+
*/
|
|
1956
|
+
private static async loadMethods(
|
|
1957
|
+
projectId: ObjectID,
|
|
1958
|
+
userIds: Array<ObjectID>,
|
|
1959
|
+
completeness: ReadCompleteness,
|
|
1960
|
+
): Promise<Map<string, Array<ReadinessMethod>>> {
|
|
1961
|
+
const methodsByUserId: Map<string, Array<ReadinessMethod>> = new Map<
|
|
1962
|
+
string,
|
|
1963
|
+
Array<ReadinessMethod>
|
|
1964
|
+
>();
|
|
1965
|
+
|
|
1966
|
+
/*
|
|
1967
|
+
* The row goes in whole rather than as a userId, so that the id a rule will point at
|
|
1968
|
+
* and the identifier that gets masked provably come off the SAME row. Handing the two
|
|
1969
|
+
* in separately is how a caller ends up attaching one person's method id to another
|
|
1970
|
+
* person's mask, and a rule pointed at the wrong row pages the wrong human.
|
|
1971
|
+
*/
|
|
1972
|
+
type AddMethodFunction = (
|
|
1973
|
+
row: MethodRowRef,
|
|
1974
|
+
method: Omit<ReadinessMethod, "methodId">,
|
|
1975
|
+
) => void;
|
|
1976
|
+
|
|
1977
|
+
const addMethod: AddMethodFunction = (
|
|
1978
|
+
row: MethodRowRef,
|
|
1979
|
+
method: Omit<ReadinessMethod, "methodId">,
|
|
1980
|
+
): void => {
|
|
1981
|
+
const methodId: ObjectID | null = row.id;
|
|
1982
|
+
|
|
1983
|
+
/*
|
|
1984
|
+
* A row with no owner, or no id of its own, is dropped. Neither is reachable while
|
|
1985
|
+
* every select below asks for `_id` and every method row is owned — a primary key
|
|
1986
|
+
* is not optional in the database — so this is a guard against a future select
|
|
1987
|
+
* being trimmed rather than a case that happens.
|
|
1988
|
+
*
|
|
1989
|
+
* It is a drop rather than a partial emit because the alternatives are both worse.
|
|
1990
|
+
* Emitting the method without an id would mean typing methodId as optional, which
|
|
1991
|
+
* pushes this impossible case out to every caller and gives the rule form an option
|
|
1992
|
+
* it cannot submit. Dropping errs towards reporting the responder as LESS reachable
|
|
1993
|
+
* than they are, which is the direction this service always errs in: a false amber
|
|
1994
|
+
* gets investigated, and a false green does not.
|
|
1995
|
+
*/
|
|
1996
|
+
if (!row.userId || !methodId) {
|
|
1997
|
+
return;
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
const readinessMethod: ReadinessMethod = {
|
|
2001
|
+
methodId: methodId,
|
|
2002
|
+
...method,
|
|
2003
|
+
};
|
|
2004
|
+
|
|
2005
|
+
const key: string = row.userId.toString();
|
|
2006
|
+
const existing: Array<ReadinessMethod> | undefined =
|
|
2007
|
+
methodsByUserId.get(key);
|
|
2008
|
+
|
|
2009
|
+
if (existing) {
|
|
2010
|
+
existing.push(readinessMethod);
|
|
2011
|
+
|
|
2012
|
+
return;
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
methodsByUserId.set(key, [readinessMethod]);
|
|
2016
|
+
};
|
|
2017
|
+
|
|
2018
|
+
await this.readEveryPage<UserPush>({
|
|
2019
|
+
description: "push notification methods",
|
|
2020
|
+
projectId: projectId,
|
|
2021
|
+
completeness: completeness,
|
|
2022
|
+
service: UserPushService,
|
|
2023
|
+
query: {
|
|
2024
|
+
projectId: projectId,
|
|
2025
|
+
userId: new Includes(userIds),
|
|
2026
|
+
},
|
|
2027
|
+
select: {
|
|
2028
|
+
_id: true,
|
|
2029
|
+
userId: true,
|
|
2030
|
+
deviceName: true,
|
|
2031
|
+
isVerified: true,
|
|
2032
|
+
},
|
|
2033
|
+
consumePage: (rows: Array<UserPush>): void => {
|
|
2034
|
+
for (const row of rows) {
|
|
2035
|
+
addMethod(row, {
|
|
2036
|
+
methodType: ReadinessMethodType.Push,
|
|
2037
|
+
maskedIdentifier: maskIdentifier(
|
|
2038
|
+
row.deviceName,
|
|
2039
|
+
MaskedIdentifierKind.Handle,
|
|
2040
|
+
),
|
|
2041
|
+
isVerified: Boolean(row.isVerified),
|
|
2042
|
+
});
|
|
2043
|
+
}
|
|
2044
|
+
},
|
|
2045
|
+
});
|
|
2046
|
+
|
|
2047
|
+
await this.readEveryPage<UserEmail>({
|
|
2048
|
+
description: "email notification methods",
|
|
2049
|
+
projectId: projectId,
|
|
2050
|
+
completeness: completeness,
|
|
2051
|
+
service: UserEmailService,
|
|
2052
|
+
query: {
|
|
2053
|
+
projectId: projectId,
|
|
2054
|
+
userId: new Includes(userIds),
|
|
2055
|
+
},
|
|
2056
|
+
select: {
|
|
2057
|
+
_id: true,
|
|
2058
|
+
userId: true,
|
|
2059
|
+
email: true,
|
|
2060
|
+
isVerified: true,
|
|
2061
|
+
},
|
|
2062
|
+
consumePage: (rows: Array<UserEmail>): void => {
|
|
2063
|
+
for (const row of rows) {
|
|
2064
|
+
addMethod(row, {
|
|
2065
|
+
methodType: ReadinessMethodType.Email,
|
|
2066
|
+
maskedIdentifier: maskIdentifier(
|
|
2067
|
+
row.email?.toString(),
|
|
2068
|
+
MaskedIdentifierKind.Email,
|
|
2069
|
+
),
|
|
2070
|
+
isVerified: Boolean(row.isVerified),
|
|
2071
|
+
});
|
|
2072
|
+
}
|
|
2073
|
+
},
|
|
2074
|
+
});
|
|
2075
|
+
|
|
2076
|
+
await this.readEveryPage<UserSMS>({
|
|
2077
|
+
description: "SMS notification methods",
|
|
2078
|
+
projectId: projectId,
|
|
2079
|
+
completeness: completeness,
|
|
2080
|
+
service: UserSmsService,
|
|
2081
|
+
query: {
|
|
2082
|
+
projectId: projectId,
|
|
2083
|
+
userId: new Includes(userIds),
|
|
2084
|
+
},
|
|
2085
|
+
select: {
|
|
2086
|
+
_id: true,
|
|
2087
|
+
userId: true,
|
|
2088
|
+
phone: true,
|
|
2089
|
+
isVerified: true,
|
|
2090
|
+
},
|
|
2091
|
+
consumePage: (rows: Array<UserSMS>): void => {
|
|
2092
|
+
for (const row of rows) {
|
|
2093
|
+
addMethod(row, {
|
|
2094
|
+
methodType: ReadinessMethodType.SMS,
|
|
2095
|
+
maskedIdentifier: maskIdentifier(
|
|
2096
|
+
row.phone?.toString(),
|
|
2097
|
+
MaskedIdentifierKind.Phone,
|
|
2098
|
+
),
|
|
2099
|
+
isVerified: Boolean(row.isVerified),
|
|
2100
|
+
});
|
|
2101
|
+
}
|
|
2102
|
+
},
|
|
2103
|
+
});
|
|
2104
|
+
|
|
2105
|
+
await this.readEveryPage<UserCall>({
|
|
2106
|
+
description: "call notification methods",
|
|
2107
|
+
projectId: projectId,
|
|
2108
|
+
completeness: completeness,
|
|
2109
|
+
service: UserCallService,
|
|
2110
|
+
query: {
|
|
2111
|
+
projectId: projectId,
|
|
2112
|
+
userId: new Includes(userIds),
|
|
2113
|
+
},
|
|
2114
|
+
select: {
|
|
2115
|
+
_id: true,
|
|
2116
|
+
userId: true,
|
|
2117
|
+
phone: true,
|
|
2118
|
+
isVerified: true,
|
|
2119
|
+
},
|
|
2120
|
+
consumePage: (rows: Array<UserCall>): void => {
|
|
2121
|
+
for (const row of rows) {
|
|
2122
|
+
addMethod(row, {
|
|
2123
|
+
methodType: ReadinessMethodType.Call,
|
|
2124
|
+
maskedIdentifier: maskIdentifier(
|
|
2125
|
+
row.phone?.toString(),
|
|
2126
|
+
MaskedIdentifierKind.Phone,
|
|
2127
|
+
),
|
|
2128
|
+
isVerified: Boolean(row.isVerified),
|
|
2129
|
+
});
|
|
2130
|
+
}
|
|
2131
|
+
},
|
|
2132
|
+
});
|
|
2133
|
+
|
|
2134
|
+
await this.readEveryPage<UserWhatsApp>({
|
|
2135
|
+
description: "WhatsApp notification methods",
|
|
2136
|
+
projectId: projectId,
|
|
2137
|
+
completeness: completeness,
|
|
2138
|
+
service: UserWhatsAppService,
|
|
2139
|
+
query: {
|
|
2140
|
+
projectId: projectId,
|
|
2141
|
+
userId: new Includes(userIds),
|
|
2142
|
+
},
|
|
2143
|
+
select: {
|
|
2144
|
+
_id: true,
|
|
2145
|
+
userId: true,
|
|
2146
|
+
phone: true,
|
|
2147
|
+
isVerified: true,
|
|
2148
|
+
},
|
|
2149
|
+
consumePage: (rows: Array<UserWhatsApp>): void => {
|
|
2150
|
+
for (const row of rows) {
|
|
2151
|
+
addMethod(row, {
|
|
2152
|
+
methodType: ReadinessMethodType.WhatsApp,
|
|
2153
|
+
maskedIdentifier: maskIdentifier(
|
|
2154
|
+
row.phone?.toString(),
|
|
2155
|
+
MaskedIdentifierKind.Phone,
|
|
2156
|
+
),
|
|
2157
|
+
isVerified: Boolean(row.isVerified),
|
|
2158
|
+
});
|
|
2159
|
+
}
|
|
2160
|
+
},
|
|
2161
|
+
});
|
|
2162
|
+
|
|
2163
|
+
await this.readEveryPage<UserTelegram>({
|
|
2164
|
+
description: "Telegram notification methods",
|
|
2165
|
+
projectId: projectId,
|
|
2166
|
+
completeness: completeness,
|
|
2167
|
+
service: UserTelegramService,
|
|
2168
|
+
query: {
|
|
2169
|
+
projectId: projectId,
|
|
2170
|
+
userId: new Includes(userIds),
|
|
2171
|
+
},
|
|
2172
|
+
/*
|
|
2173
|
+
* The handle only — never telegramChatId. The chat id is the addressable target a
|
|
2174
|
+
* bot sends to; the handle is the human-facing label, and it is the one a user can
|
|
2175
|
+
* recognise as theirs.
|
|
2176
|
+
*/
|
|
2177
|
+
select: {
|
|
2178
|
+
_id: true,
|
|
2179
|
+
userId: true,
|
|
2180
|
+
telegramUserHandle: true,
|
|
2181
|
+
isVerified: true,
|
|
2182
|
+
},
|
|
2183
|
+
consumePage: (rows: Array<UserTelegram>): void => {
|
|
2184
|
+
for (const row of rows) {
|
|
2185
|
+
addMethod(row, {
|
|
2186
|
+
methodType: ReadinessMethodType.Telegram,
|
|
2187
|
+
maskedIdentifier: maskIdentifier(
|
|
2188
|
+
row.telegramUserHandle,
|
|
2189
|
+
MaskedIdentifierKind.Handle,
|
|
2190
|
+
),
|
|
2191
|
+
isVerified: Boolean(row.isVerified),
|
|
2192
|
+
});
|
|
2193
|
+
}
|
|
2194
|
+
},
|
|
2195
|
+
});
|
|
2196
|
+
|
|
2197
|
+
await this.readEveryPage<UserWebhook>({
|
|
2198
|
+
description: "webhook notification methods",
|
|
2199
|
+
projectId: projectId,
|
|
2200
|
+
completeness: completeness,
|
|
2201
|
+
service: UserWebhookService,
|
|
2202
|
+
query: {
|
|
2203
|
+
projectId: projectId,
|
|
2204
|
+
userId: new Includes(userIds),
|
|
2205
|
+
},
|
|
2206
|
+
/*
|
|
2207
|
+
* `name` ONLY. UserWebhook.webhookUrl is a bearer credential — anyone holding a
|
|
2208
|
+
* Slack/Discord/Teams hook URL can post as the integration — so it is never
|
|
2209
|
+
* selected here and never leaves the server on this path.
|
|
2210
|
+
* Common/UI/Utils/NotificationMethodUtil.ts documents the same rule for the rule
|
|
2211
|
+
* tables and reads only `name`; this follows it. The masked name is enough to
|
|
2212
|
+
* answer the only question readiness asks, which is whether a webhook exists.
|
|
2213
|
+
*/
|
|
2214
|
+
select: {
|
|
2215
|
+
_id: true,
|
|
2216
|
+
userId: true,
|
|
2217
|
+
name: true,
|
|
2218
|
+
},
|
|
2219
|
+
consumePage: (rows: Array<UserWebhook>): void => {
|
|
2220
|
+
for (const row of rows) {
|
|
2221
|
+
/*
|
|
2222
|
+
* isVerified: true, with no isVerified column behind it. UserWebhook has no
|
|
2223
|
+
* verification concept at all — its presence IS the whole test, which is
|
|
2224
|
+
* exactly how the runtime fallback treats it. Reporting it as unverified would
|
|
2225
|
+
* paint the one channel that is guaranteed to work as the one channel that
|
|
2226
|
+
* will not.
|
|
2227
|
+
*/
|
|
2228
|
+
addMethod(row, {
|
|
2229
|
+
methodType: ReadinessMethodType.Webhook,
|
|
2230
|
+
maskedIdentifier: maskIdentifier(
|
|
2231
|
+
row.name,
|
|
2232
|
+
MaskedIdentifierKind.Handle,
|
|
2233
|
+
),
|
|
2234
|
+
isVerified: true,
|
|
2235
|
+
});
|
|
2236
|
+
}
|
|
2237
|
+
},
|
|
2238
|
+
});
|
|
2239
|
+
|
|
2240
|
+
for (const methods of methodsByUserId.values()) {
|
|
2241
|
+
methods.sort((a: ReadinessMethod, b: ReadinessMethod): number => {
|
|
2242
|
+
return (
|
|
2243
|
+
METHOD_DISPLAY_ORDER.indexOf(a.methodType as ReadinessMethodType) -
|
|
2244
|
+
METHOD_DISPLAY_ORDER.indexOf(b.methodType as ReadinessMethodType)
|
|
2245
|
+
);
|
|
2246
|
+
});
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
return methodsByUserId;
|
|
2250
|
+
}
|
|
2251
|
+
|
|
2252
|
+
/**
|
|
2253
|
+
* ONE paged read over UserNotificationRule for the whole responder set, folded into a
|
|
2254
|
+
* map keyed by (userId, ruleType, severityId).
|
|
2255
|
+
*
|
|
2256
|
+
* The folding happens per page rather than after the read for a reason that is not
|
|
2257
|
+
* about tidiness: this is by far the largest table this service touches — users x rule
|
|
2258
|
+
* types x severities x methods — so a project of a few thousand responders holds
|
|
2259
|
+
* hundreds of thousands of rows here, all of which collapse into at most a few cells
|
|
2260
|
+
* per user. Accumulating them first and folding second would hold the whole table in
|
|
2261
|
+
* memory to produce a map a thousand times smaller.
|
|
2262
|
+
*
|
|
2263
|
+
* Opt-out rows are read alongside real rules rather than filtered out in SQL, because
|
|
2264
|
+
* both halves are needed: a cell with an opt-out is Ready (deliberate silence) while a
|
|
2265
|
+
* cell with nothing at all is PartiallyReady (silence nobody chose), and telling those
|
|
2266
|
+
* apart is the entire reason the isOptOut column exists.
|
|
2267
|
+
*
|
|
2268
|
+
* The in-memory split is `isOptOut === true`, which is the exact dual of the
|
|
2269
|
+
* notOptOutRuleQuery predicate the paging path uses — deliberately NOT `isOptOut ===
|
|
2270
|
+
* false`. The column is nullable and was added long after these rows started existing,
|
|
2271
|
+
* so it is NULL on every rule in every existing install. Testing for false would
|
|
2272
|
+
* classify all of them as neither rules nor opt-outs, and this service would report a
|
|
2273
|
+
* fully-configured project as entirely unready.
|
|
2274
|
+
*/
|
|
2275
|
+
private static async loadCoverageIndex(
|
|
2276
|
+
projectId: ObjectID,
|
|
2277
|
+
userIds: Array<ObjectID>,
|
|
2278
|
+
completeness: ReadCompleteness,
|
|
2279
|
+
): Promise<Map<string, CoverageState>> {
|
|
2280
|
+
const coverageByKey: Map<string, CoverageState> = new Map<
|
|
2281
|
+
string,
|
|
2282
|
+
CoverageState
|
|
2283
|
+
>();
|
|
2284
|
+
|
|
2285
|
+
await this.readEveryPage<UserNotificationRule>({
|
|
2286
|
+
description: "user notification rules",
|
|
2287
|
+
projectId: projectId,
|
|
2288
|
+
completeness: completeness,
|
|
2289
|
+
service: UserNotificationRuleService,
|
|
2290
|
+
query: {
|
|
2291
|
+
projectId: projectId,
|
|
2292
|
+
userId: new Includes(userIds),
|
|
2293
|
+
},
|
|
2294
|
+
select: {
|
|
2295
|
+
_id: true,
|
|
2296
|
+
userId: true,
|
|
2297
|
+
ruleType: true,
|
|
2298
|
+
incidentSeverityId: true,
|
|
2299
|
+
alertSeverityId: true,
|
|
2300
|
+
isOptOut: true,
|
|
2301
|
+
},
|
|
2302
|
+
consumePage: (rows: Array<UserNotificationRule>): void => {
|
|
2303
|
+
for (const rule of rows) {
|
|
2304
|
+
this.foldRuleIntoCoverage(rule, coverageByKey);
|
|
2305
|
+
}
|
|
2306
|
+
},
|
|
2307
|
+
});
|
|
2308
|
+
|
|
2309
|
+
return coverageByKey;
|
|
2310
|
+
}
|
|
2311
|
+
|
|
2312
|
+
private static foldRuleIntoCoverage(
|
|
2313
|
+
rule: UserNotificationRule,
|
|
2314
|
+
coverageByKey: Map<string, CoverageState>,
|
|
2315
|
+
): void {
|
|
2316
|
+
const scope: RuleTypeScope | undefined = RULE_TYPE_SCOPES.find(
|
|
2317
|
+
(candidate: RuleTypeScope): boolean => {
|
|
2318
|
+
return candidate.ruleType === rule.ruleType;
|
|
2319
|
+
},
|
|
2320
|
+
);
|
|
2321
|
+
|
|
2322
|
+
/*
|
|
2323
|
+
* Not one of the four paging rule types — a handoff rule, or something added later.
|
|
2324
|
+
* It covers no page, so it covers no cell. This is defect 4 of
|
|
2325
|
+
* TeamComplianceService, which matched on severity alone and let a
|
|
2326
|
+
* WHEN_USER_GOES_OFF_CALL rule certify incident coverage.
|
|
2327
|
+
*/
|
|
2328
|
+
if (!scope) {
|
|
2329
|
+
return;
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2332
|
+
/*
|
|
2333
|
+
* Take the severity from the column the RULE TYPE dictates, never from whichever one
|
|
2334
|
+
* happens to be populated. An alert rule carrying a stray incidentSeverityId matches
|
|
2335
|
+
* no page at runtime, so it must not be allowed to satisfy a cell here.
|
|
2336
|
+
*/
|
|
2337
|
+
const severityId: ObjectID | undefined =
|
|
2338
|
+
scope.severityKind === SeverityKind.Incident
|
|
2339
|
+
? rule.incidentSeverityId
|
|
2340
|
+
: rule.alertSeverityId;
|
|
2341
|
+
|
|
2342
|
+
/*
|
|
2343
|
+
* A severity-scoped rule with a NULL severity is the Gap G corpse: the paging path
|
|
2344
|
+
* counts episode rules filtered by a concrete severity id, so NULL matches nothing
|
|
2345
|
+
* and the rule is unreachable. Counting it as coverage would report exactly the
|
|
2346
|
+
* users worst affected by that bug as fully ready.
|
|
2347
|
+
*/
|
|
2348
|
+
if (!severityId) {
|
|
2349
|
+
return;
|
|
2350
|
+
}
|
|
2351
|
+
|
|
2352
|
+
const key: string = buildCoverageKey(
|
|
2353
|
+
rule.userId?.toString() || "",
|
|
2354
|
+
scope.ruleType,
|
|
2355
|
+
severityId.toString(),
|
|
2356
|
+
);
|
|
2357
|
+
|
|
2358
|
+
const state: CoverageState = coverageByKey.get(key) || {
|
|
2359
|
+
hasRule: false,
|
|
2360
|
+
isOptOut: false,
|
|
2361
|
+
};
|
|
2362
|
+
|
|
2363
|
+
if (rule.isOptOut === true) {
|
|
2364
|
+
state.isOptOut = true;
|
|
2365
|
+
} else {
|
|
2366
|
+
state.hasRule = true;
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2369
|
+
coverageByKey.set(key, state);
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2372
|
+
private static buildUserReadiness(
|
|
2373
|
+
user: User,
|
|
2374
|
+
attachment: ResponderAttachment,
|
|
2375
|
+
userIdString: string,
|
|
2376
|
+
inputs: ReadinessInputs,
|
|
2377
|
+
teamNamesById: Map<string, string>,
|
|
2378
|
+
): UserReadiness {
|
|
2379
|
+
const sources: Set<ResponderSource> = attachment.sources;
|
|
2380
|
+
const methods: Array<ReadinessMethod> =
|
|
2381
|
+
inputs.methodsByUserId.get(userIdString) || [];
|
|
2382
|
+
|
|
2383
|
+
const coverage: Array<ReadinessCoverageCell> = [];
|
|
2384
|
+
|
|
2385
|
+
for (const scope of RULE_TYPE_SCOPES) {
|
|
2386
|
+
const severities: Array<SeverityRef> =
|
|
2387
|
+
scope.severityKind === SeverityKind.Incident
|
|
2388
|
+
? inputs.incidentSeverities
|
|
2389
|
+
: inputs.alertSeverities;
|
|
2390
|
+
|
|
2391
|
+
for (const severity of severities) {
|
|
2392
|
+
const state: CoverageState | undefined = inputs.coverageByKey.get(
|
|
2393
|
+
buildCoverageKey(
|
|
2394
|
+
userIdString,
|
|
2395
|
+
scope.ruleType,
|
|
2396
|
+
severity.id.toString(),
|
|
2397
|
+
),
|
|
2398
|
+
);
|
|
2399
|
+
|
|
2400
|
+
coverage.push({
|
|
2401
|
+
ruleType: scope.ruleType,
|
|
2402
|
+
severityId: severity.id,
|
|
2403
|
+
severityName: severity.name,
|
|
2404
|
+
hasRule: Boolean(state?.hasRule),
|
|
2405
|
+
isOptOut: Boolean(state?.isOptOut),
|
|
2406
|
+
});
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
|
|
2410
|
+
/*
|
|
2411
|
+
* Verified is necessary and not sufficient — webhooks are reported verified by
|
|
2412
|
+
* construction above, so this one predicate covers the "webhook counts without
|
|
2413
|
+
* verification" rule without a special case leaking into the status logic.
|
|
2414
|
+
*/
|
|
2415
|
+
const verifiedMethods: Array<ReadinessMethod> = methods.filter(
|
|
2416
|
+
(method: ReadinessMethod): boolean => {
|
|
2417
|
+
return method.isVerified;
|
|
2418
|
+
},
|
|
2419
|
+
);
|
|
2420
|
+
|
|
2421
|
+
/*
|
|
2422
|
+
* USABLE is verified AND on a channel the project has switched on. Status used to be
|
|
2423
|
+
* computed from verification alone, which meant a responder whose only verified
|
|
2424
|
+
* methods were SMS and Call, in a project with SMS and Call switched off, rendered
|
|
2425
|
+
* Ready and green while being completely unpageable — the exact false green this
|
|
2426
|
+
* service exists to make impossible. The project switches are already loaded, sit
|
|
2427
|
+
* two fields away, and the runtime consults them on every send, so ignoring them here
|
|
2428
|
+
* was never a judgement call.
|
|
2429
|
+
*/
|
|
2430
|
+
const usableMethods: Array<ReadinessMethod> = verifiedMethods.filter(
|
|
2431
|
+
(method: ReadinessMethod): boolean => {
|
|
2432
|
+
return this.isChannelEnabled(method.methodType, inputs.projectSettings);
|
|
2433
|
+
},
|
|
2434
|
+
);
|
|
2435
|
+
|
|
2436
|
+
const disabledChannels: Array<string> = this.distinctStrings(
|
|
2437
|
+
verifiedMethods
|
|
2438
|
+
.filter((method: ReadinessMethod): boolean => {
|
|
2439
|
+
return !this.isChannelEnabled(
|
|
2440
|
+
method.methodType,
|
|
2441
|
+
inputs.projectSettings,
|
|
2442
|
+
);
|
|
2443
|
+
})
|
|
2444
|
+
.map((method: ReadinessMethod): string => {
|
|
2445
|
+
return method.methodType;
|
|
2446
|
+
}),
|
|
2447
|
+
);
|
|
2448
|
+
|
|
2449
|
+
const uncoveredCells: Array<ReadinessCoverageCell> = coverage.filter(
|
|
2450
|
+
(cell: ReadinessCoverageCell): boolean => {
|
|
2451
|
+
return !cell.hasRule && !cell.isOptOut;
|
|
2452
|
+
},
|
|
2453
|
+
);
|
|
2454
|
+
|
|
2455
|
+
let status: ReadinessStatus = ReadinessStatus.Ready;
|
|
2456
|
+
|
|
2457
|
+
if (usableMethods.length === 0) {
|
|
2458
|
+
status = ReadinessStatus.NotReachable;
|
|
2459
|
+
} else if (uncoveredCells.length > 0) {
|
|
2460
|
+
status = ReadinessStatus.PartiallyReady;
|
|
2461
|
+
}
|
|
2462
|
+
|
|
2463
|
+
const reasons: Array<string> = this.buildReasons({
|
|
2464
|
+
status: status,
|
|
2465
|
+
methods: methods,
|
|
2466
|
+
verifiedMethods: verifiedMethods,
|
|
2467
|
+
usableMethods: usableMethods,
|
|
2468
|
+
disabledChannels: disabledChannels,
|
|
2469
|
+
uncoveredCells: uncoveredCells,
|
|
2470
|
+
projectSettings: inputs.projectSettings,
|
|
2471
|
+
});
|
|
2472
|
+
|
|
2473
|
+
return {
|
|
2474
|
+
userId: user.id!,
|
|
2475
|
+
userName:
|
|
2476
|
+
user.name?.toString() || user.email?.toString() || "Unknown User",
|
|
2477
|
+
userEmail: user.email?.toString() || "",
|
|
2478
|
+
userProfilePictureId: user.profilePictureId,
|
|
2479
|
+
status: status,
|
|
2480
|
+
methods: methods,
|
|
2481
|
+
coverage: coverage,
|
|
2482
|
+
reasons: reasons,
|
|
2483
|
+
reachedVia: RESPONDER_SOURCE_ORDER.filter(
|
|
2484
|
+
(source: ResponderSource): boolean => {
|
|
2485
|
+
return sources.has(source);
|
|
2486
|
+
},
|
|
2487
|
+
),
|
|
2488
|
+
/*
|
|
2489
|
+
* A fresh array per user, never a shared one. InMemoryTTLCache stores by reference
|
|
2490
|
+
* and hands the same object graph to every caller inside its TTL, so a list shared
|
|
2491
|
+
* between two responders would let a mutation anywhere downstream rewrite the
|
|
2492
|
+
* cached answer for both.
|
|
2493
|
+
*
|
|
2494
|
+
* Sorted by name so the column and the filter chip read the same way for every
|
|
2495
|
+
* responder, and so the order does not depend on which membership row the database
|
|
2496
|
+
* happened to return first.
|
|
2497
|
+
*/
|
|
2498
|
+
teams: Array.from(attachment.teamIds)
|
|
2499
|
+
.map((teamId: string): ReadinessTeam | null => {
|
|
2500
|
+
const name: string | undefined = teamNamesById.get(teamId);
|
|
2501
|
+
|
|
2502
|
+
return name ? { _id: new ObjectID(teamId), name: name } : null;
|
|
2503
|
+
})
|
|
2504
|
+
.filter((team: ReadinessTeam | null): team is ReadinessTeam => {
|
|
2505
|
+
return team !== null;
|
|
2506
|
+
})
|
|
2507
|
+
.sort((a: ReadinessTeam, b: ReadinessTeam): number => {
|
|
2508
|
+
return a.name.localeCompare(b.name);
|
|
2509
|
+
}),
|
|
2510
|
+
};
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
/**
|
|
2514
|
+
* Whether this project can send on this channel at all.
|
|
2515
|
+
*
|
|
2516
|
+
* Push, Email and Webhook have no project switch: the first two are zero-cost and the
|
|
2517
|
+
* third is somebody else's endpoint, so nothing gates them and they are always
|
|
2518
|
+
* available. The four paid channels do, and the runtime honours them — SmsService and
|
|
2519
|
+
* CallService refuse at send time, and the fallback checks all four before it picks a
|
|
2520
|
+
* channel to spend money on. Treating all four alike here is the conservative reading:
|
|
2521
|
+
* a responder marked unreachable because their project switched their only channel off
|
|
2522
|
+
* is an alarm somebody can act on, while the reverse mistake is a page nobody hears.
|
|
2523
|
+
*/
|
|
2524
|
+
private static isChannelEnabled(
|
|
2525
|
+
methodType: string,
|
|
2526
|
+
settings: ProjectNotificationSettings,
|
|
2527
|
+
): boolean {
|
|
2528
|
+
if (methodType === ReadinessMethodType.SMS) {
|
|
2529
|
+
return settings.enableSmsNotifications;
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2532
|
+
if (methodType === ReadinessMethodType.Call) {
|
|
2533
|
+
return settings.enableCallNotifications;
|
|
2534
|
+
}
|
|
2535
|
+
|
|
2536
|
+
if (methodType === ReadinessMethodType.WhatsApp) {
|
|
2537
|
+
return settings.enableWhatsAppNotifications;
|
|
2538
|
+
}
|
|
2539
|
+
|
|
2540
|
+
if (methodType === ReadinessMethodType.Telegram) {
|
|
2541
|
+
return settings.enableTelegramNotifications;
|
|
2542
|
+
}
|
|
2543
|
+
|
|
2544
|
+
return true;
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2547
|
+
/**
|
|
2548
|
+
* Sentences an admin can act on, in the order they should act on them.
|
|
2549
|
+
*
|
|
2550
|
+
* Not status names, not rule-type enum values, not "non-compliant" — every line names
|
|
2551
|
+
* a specific missing thing and what happens because of it. The consequence clause is
|
|
2552
|
+
* the part that matters: "no rule for Sev4" is a shrug, "no rule for Sev4, pages are
|
|
2553
|
+
* dropped" is a ticket.
|
|
2554
|
+
*/
|
|
2555
|
+
private static buildReasons(data: {
|
|
2556
|
+
status: ReadinessStatus;
|
|
2557
|
+
methods: Array<ReadinessMethod>;
|
|
2558
|
+
verifiedMethods: Array<ReadinessMethod>;
|
|
2559
|
+
usableMethods: Array<ReadinessMethod>;
|
|
2560
|
+
disabledChannels: Array<string>;
|
|
2561
|
+
uncoveredCells: Array<ReadinessCoverageCell>;
|
|
2562
|
+
projectSettings: ProjectNotificationSettings;
|
|
2563
|
+
}): Array<string> {
|
|
2564
|
+
const reasons: Array<string> = [];
|
|
2565
|
+
|
|
2566
|
+
if (data.status === ReadinessStatus.NotReachable) {
|
|
2567
|
+
/*
|
|
2568
|
+
* Three ways to be unreachable, and they need three different sentences because
|
|
2569
|
+
* they need three different people to fix them: the user adds a method, the user
|
|
2570
|
+
* verifies a method, or an admin turns a channel back on. A single "cannot be
|
|
2571
|
+
* paged" line sends all three to the wrong place.
|
|
2572
|
+
*/
|
|
2573
|
+
if (data.verifiedMethods.length > 0) {
|
|
2574
|
+
reasons.push("No usable notification method - cannot be paged");
|
|
2575
|
+
reasons.push(
|
|
2576
|
+
`Every method they have verified is on ${data.disabledChannels.join(", ")}, and this project has ${data.disabledChannels.length === 1 ? "that channel" : "those channels"} switched off - that is a project setting, not something this user can fix`,
|
|
2577
|
+
);
|
|
2578
|
+
|
|
2579
|
+
return reasons;
|
|
2580
|
+
}
|
|
2581
|
+
|
|
2582
|
+
reasons.push("No verified notification method - cannot be paged");
|
|
2583
|
+
|
|
2584
|
+
/*
|
|
2585
|
+
* "They have nothing" and "they have something they never verified" look identical
|
|
2586
|
+
* in a status chip and could not be more different to fix: the second needs one
|
|
2587
|
+
* click from the user, not a conversation about how on-call works.
|
|
2588
|
+
*/
|
|
2589
|
+
if (data.methods.length > 0) {
|
|
2590
|
+
const unverified: Array<string> = this.distinctStrings(
|
|
2591
|
+
data.methods.map((method: ReadinessMethod): string => {
|
|
2592
|
+
return method.methodType;
|
|
2593
|
+
}),
|
|
2594
|
+
);
|
|
2595
|
+
|
|
2596
|
+
reasons.push(
|
|
2597
|
+
`Added ${unverified.join(", ")} but never verified - unverified methods are never used`,
|
|
2598
|
+
);
|
|
2599
|
+
} else {
|
|
2600
|
+
reasons.push(
|
|
2601
|
+
"Ask this user to add and verify a notification method in User Settings > Notification Methods",
|
|
2602
|
+
);
|
|
2603
|
+
}
|
|
2604
|
+
|
|
2605
|
+
/*
|
|
2606
|
+
* Coverage is meaningless for someone nothing can reach. Listing their missing
|
|
2607
|
+
* rules underneath would bury the one sentence that matters under a dozen that do
|
|
2608
|
+
* not.
|
|
2609
|
+
*/
|
|
2610
|
+
return reasons;
|
|
2611
|
+
}
|
|
2612
|
+
|
|
2613
|
+
/*
|
|
2614
|
+
* Note what is deliberately NOT said here. A responder who still has one working
|
|
2615
|
+
* channel but also has, say, an SMS number the project has switched off is reachable,
|
|
2616
|
+
* and saying so on their row would put a warning sentence on every single user in a
|
|
2617
|
+
* project that has switched SMS off — a line nobody can act on, attached to people
|
|
2618
|
+
* who have nothing wrong with them, which is how a readiness surface teaches admins
|
|
2619
|
+
* to stop reading it. The stranded channel matters only when it is the reason nobody
|
|
2620
|
+
* can be reached, and that case is handled above.
|
|
2621
|
+
*/
|
|
2622
|
+
if (data.uncoveredCells.length === 0) {
|
|
2623
|
+
return reasons;
|
|
2624
|
+
}
|
|
2625
|
+
|
|
2626
|
+
const fallbackChannels: Array<string> = this.describeFallbackChannels(
|
|
2627
|
+
data.usableMethods,
|
|
2628
|
+
data.projectSettings,
|
|
2629
|
+
);
|
|
2630
|
+
|
|
2631
|
+
/*
|
|
2632
|
+
* One sentence per rule type, listing its missing severities, rather than one per
|
|
2633
|
+
* cell. A project with four severities and no rules at all would otherwise produce
|
|
2634
|
+
* sixteen near-identical lines that nobody reads to the end of.
|
|
2635
|
+
*/
|
|
2636
|
+
for (const scope of RULE_TYPE_SCOPES) {
|
|
2637
|
+
const severityNames: Array<string> = this.distinctStrings(
|
|
2638
|
+
data.uncoveredCells
|
|
2639
|
+
.filter((cell: ReadinessCoverageCell): boolean => {
|
|
2640
|
+
return cell.ruleType === scope.ruleType;
|
|
2641
|
+
})
|
|
2642
|
+
.map((cell: ReadinessCoverageCell): string => {
|
|
2643
|
+
return cell.severityName || "this severity";
|
|
2644
|
+
}),
|
|
2645
|
+
);
|
|
2646
|
+
|
|
2647
|
+
if (severityNames.length === 0) {
|
|
2648
|
+
continue;
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2651
|
+
const subject: string = `No rules for ${severityNames.join(", ")} ${scope.subjectNoun}`;
|
|
2652
|
+
|
|
2653
|
+
if (data.projectSettings.isFallbackDisabled) {
|
|
2654
|
+
reasons.push(
|
|
2655
|
+
`${subject} - pages are dropped because on-call fallback is disabled for this project`,
|
|
2656
|
+
);
|
|
2657
|
+
} else if (fallbackChannels.length === 0) {
|
|
2658
|
+
/*
|
|
2659
|
+
* Not reachable from here today — a usable method always yields a fallback
|
|
2660
|
+
* channel — but kept so that a future channel with no fallback path degrades
|
|
2661
|
+
* into an honest sentence instead of "pages fall back to " with nothing after
|
|
2662
|
+
* it.
|
|
2663
|
+
*/
|
|
2664
|
+
reasons.push(`${subject} - pages cannot be delivered`);
|
|
2665
|
+
} else {
|
|
2666
|
+
reasons.push(
|
|
2667
|
+
`${subject} - pages fall back to ${fallbackChannels.join(", ")}`,
|
|
2668
|
+
);
|
|
2669
|
+
}
|
|
2670
|
+
}
|
|
2671
|
+
|
|
2672
|
+
return reasons;
|
|
2673
|
+
}
|
|
2674
|
+
|
|
2675
|
+
/**
|
|
2676
|
+
* The channels a fallback page would actually arrive on, so the reason sentences name
|
|
2677
|
+
* the real thing rather than a hopeful "some verified method".
|
|
2678
|
+
*
|
|
2679
|
+
* This mirrors UserNotificationRuleService.chooseFallbackChannels exactly, including
|
|
2680
|
+
* the project switches: zero-cost channels first and BOTH of them if present, then one
|
|
2681
|
+
* paid channel in escalating-intrusiveness order, then webhook. If that function's
|
|
2682
|
+
* order ever changes, this one has to change with it — a readiness surface promising
|
|
2683
|
+
* "falls back to SMS" for a project that has SMS switched off is worse than saying
|
|
2684
|
+
* nothing. The switch checks are redundant with the usable-method filter upstream and
|
|
2685
|
+
* are kept anyway, because this function's whole value is being a line-by-line mirror
|
|
2686
|
+
* of the one that does the real thing.
|
|
2687
|
+
*/
|
|
2688
|
+
private static describeFallbackChannels(
|
|
2689
|
+
usableMethods: Array<ReadinessMethod>,
|
|
2690
|
+
settings: ProjectNotificationSettings,
|
|
2691
|
+
): Array<string> {
|
|
2692
|
+
type HasMethodFunction = (methodType: ReadinessMethodType) => boolean;
|
|
2693
|
+
|
|
2694
|
+
const has: HasMethodFunction = (
|
|
2695
|
+
methodType: ReadinessMethodType,
|
|
2696
|
+
): boolean => {
|
|
2697
|
+
return usableMethods.some((method: ReadinessMethod): boolean => {
|
|
2698
|
+
return method.methodType === methodType;
|
|
2699
|
+
});
|
|
2700
|
+
};
|
|
2701
|
+
|
|
2702
|
+
const zeroCost: Array<string> = [];
|
|
2703
|
+
|
|
2704
|
+
if (has(ReadinessMethodType.Push)) {
|
|
2705
|
+
zeroCost.push(ReadinessMethodType.Push);
|
|
2706
|
+
}
|
|
2707
|
+
|
|
2708
|
+
if (has(ReadinessMethodType.Email)) {
|
|
2709
|
+
zeroCost.push(ReadinessMethodType.Email);
|
|
2710
|
+
}
|
|
2711
|
+
|
|
2712
|
+
if (zeroCost.length > 0) {
|
|
2713
|
+
return zeroCost;
|
|
2714
|
+
}
|
|
2715
|
+
|
|
2716
|
+
if (settings.enableSmsNotifications && has(ReadinessMethodType.SMS)) {
|
|
2717
|
+
return [ReadinessMethodType.SMS];
|
|
2718
|
+
}
|
|
2719
|
+
|
|
2720
|
+
if (settings.enableCallNotifications && has(ReadinessMethodType.Call)) {
|
|
2721
|
+
return [ReadinessMethodType.Call];
|
|
2722
|
+
}
|
|
2723
|
+
|
|
2724
|
+
if (
|
|
2725
|
+
settings.enableWhatsAppNotifications &&
|
|
2726
|
+
has(ReadinessMethodType.WhatsApp)
|
|
2727
|
+
) {
|
|
2728
|
+
return [ReadinessMethodType.WhatsApp];
|
|
2729
|
+
}
|
|
2730
|
+
|
|
2731
|
+
if (
|
|
2732
|
+
settings.enableTelegramNotifications &&
|
|
2733
|
+
has(ReadinessMethodType.Telegram)
|
|
2734
|
+
) {
|
|
2735
|
+
return [ReadinessMethodType.Telegram];
|
|
2736
|
+
}
|
|
2737
|
+
|
|
2738
|
+
if (has(ReadinessMethodType.Webhook)) {
|
|
2739
|
+
return [ReadinessMethodType.Webhook];
|
|
2740
|
+
}
|
|
2741
|
+
|
|
2742
|
+
return [];
|
|
2743
|
+
}
|
|
2744
|
+
|
|
2745
|
+
private static toSeverityRefs(
|
|
2746
|
+
severities: Array<IncidentSeverity | AlertSeverity>,
|
|
2747
|
+
): Array<SeverityRef> {
|
|
2748
|
+
const refs: Array<SeverityRef> = [];
|
|
2749
|
+
|
|
2750
|
+
for (const severity of severities) {
|
|
2751
|
+
if (!severity.id) {
|
|
2752
|
+
continue;
|
|
2753
|
+
}
|
|
2754
|
+
|
|
2755
|
+
refs.push({
|
|
2756
|
+
id: severity.id,
|
|
2757
|
+
name: severity.name || "Unnamed Severity",
|
|
2758
|
+
});
|
|
2759
|
+
}
|
|
2760
|
+
|
|
2761
|
+
return refs;
|
|
2762
|
+
}
|
|
2763
|
+
|
|
2764
|
+
private static distinctIds(
|
|
2765
|
+
ids: Array<ObjectID | undefined>,
|
|
2766
|
+
): Array<ObjectID> {
|
|
2767
|
+
const seen: Set<string> = new Set<string>();
|
|
2768
|
+
const distinct: Array<ObjectID> = [];
|
|
2769
|
+
|
|
2770
|
+
for (const id of ids) {
|
|
2771
|
+
if (!id) {
|
|
2772
|
+
continue;
|
|
2773
|
+
}
|
|
2774
|
+
|
|
2775
|
+
const key: string = id.toString();
|
|
2776
|
+
|
|
2777
|
+
if (seen.has(key)) {
|
|
2778
|
+
continue;
|
|
2779
|
+
}
|
|
2780
|
+
|
|
2781
|
+
seen.add(key);
|
|
2782
|
+
distinct.push(id);
|
|
2783
|
+
}
|
|
2784
|
+
|
|
2785
|
+
return distinct;
|
|
2786
|
+
}
|
|
2787
|
+
|
|
2788
|
+
private static distinctStrings(values: Array<string>): Array<string> {
|
|
2789
|
+
const seen: Set<string> = new Set<string>();
|
|
2790
|
+
const distinct: Array<string> = [];
|
|
2791
|
+
|
|
2792
|
+
for (const value of values) {
|
|
2793
|
+
if (seen.has(value)) {
|
|
2794
|
+
continue;
|
|
2795
|
+
}
|
|
2796
|
+
|
|
2797
|
+
seen.add(value);
|
|
2798
|
+
distinct.push(value);
|
|
2799
|
+
}
|
|
2800
|
+
|
|
2801
|
+
return distinct;
|
|
2802
|
+
}
|
|
2803
|
+
}
|