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