@oneuptime/common 12.0.32 → 12.0.33
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/Span.ts +14 -0
- package/Models/DatabaseModels/EnterpriseLicense.ts +56 -0
- package/Models/DatabaseModels/Index.ts +10 -0
- package/Models/DatabaseModels/Monitor.ts +76 -0
- package/Models/DatabaseModels/NetworkAlertPolicy.ts +690 -0
- package/Models/DatabaseModels/NetworkDevice.ts +176 -1
- package/Models/DatabaseModels/NetworkSite.ts +201 -2
- package/Models/DatabaseModels/NetworkSiteType.ts +102 -3
- package/Models/DatabaseModels/NetworkSnmpCredentialProfile.ts +841 -0
- package/Models/DatabaseModels/TelemetryException.ts +119 -0
- package/Models/DatabaseModels/UserNotificationEmailRollupBatch.ts +334 -0
- package/Models/DatabaseModels/UserNotificationEmailRollupItem.ts +370 -0
- package/Models/DatabaseModels/UserNotificationEmailRollupSetting.ts +282 -0
- package/Server/API/EnterpriseLicenseAPI.ts +391 -64
- package/Server/API/LlmProviderAPI.ts +137 -22
- package/Server/EnvironmentConfig.ts +14 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1790700000000-AddNetworkSiteTypeParentHierarchy.ts +55 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1790900000000-AddEnterpriseLicenseUsageProvenance.ts +31 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1790900000000-AddTelemetryExceptionErrorClass.ts +124 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1791000000000-AddUserNotificationEmailRollup.ts +132 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1791100000000-AddUserNotificationEmailRollupSetting.ts +79 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1791200000000-AddNetworkSnmpCredentialProfilesAndAlertPolicies.ts +191 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +12 -0
- package/Server/Infrastructure/QueueWorker.ts +38 -8
- package/Server/Middleware/BearerTokenAuthorization.ts +12 -0
- package/Server/Middleware/ProjectAuthorization.ts +11 -0
- package/Server/Middleware/SCIMAuthorization.ts +12 -0
- package/Server/Middleware/TelemetryIngest.ts +11 -0
- package/Server/Services/AIBillingService.ts +101 -27
- package/Server/Services/EnterpriseLicenseInstanceService.ts +197 -0
- package/Server/Services/EnterpriseLicenseService.ts +55 -0
- package/Server/Services/Index.ts +10 -0
- package/Server/Services/MonitorService.ts +143 -21
- package/Server/Services/MonitorStatusTimelineService.ts +150 -8
- package/Server/Services/MonitorTemplateService.ts +217 -1
- package/Server/Services/NetworkAlertPolicyEngineService.ts +2159 -0
- package/Server/Services/NetworkAlertPolicyService.ts +944 -0
- package/Server/Services/NetworkDeviceAutoImportRuleEngineService.ts +9 -10
- package/Server/Services/NetworkDeviceAutoImportRuleService.ts +60 -15
- package/Server/Services/NetworkDeviceService.ts +1032 -73
- package/Server/Services/NetworkSiteService.ts +1502 -129
- package/Server/Services/NetworkSiteTypeService.ts +1216 -0
- package/Server/Services/NetworkSnmpCredentialProfileService.ts +238 -0
- package/Server/Services/NotificationService.ts +118 -28
- package/Server/Services/ProjectService.ts +105 -48
- package/Server/Services/StatusPagePrivateUserService.ts +103 -3
- package/Server/Services/UserNotificationEmailRollupBatchService.ts +78 -0
- package/Server/Services/UserNotificationEmailRollupItemService.ts +79 -0
- package/Server/Services/UserNotificationEmailRollupSettingService.ts +113 -0
- package/Server/Services/UserNotificationSettingService.ts +62 -25
- package/Server/Services/UserService.ts +95 -0
- package/Server/Types/Database/QueryHelper.ts +2 -2
- package/Server/Utils/AI/Chat/ObservabilityChatPrompt.ts +14 -3
- package/Server/Utils/AI/SRE/Insights/Detectors/ExceptionSpikeDetector.ts +10 -0
- package/Server/Utils/AI/SRE/Insights/Detectors/NewExceptionDetector.ts +10 -0
- package/Server/Utils/AI/SRE/Insights/InsightTriageRunner.ts +62 -1
- package/Server/Utils/AI/Toolbox/AlertTools.ts +15 -1
- package/Server/Utils/Billing/BillingFailureNoticeThrottle.ts +80 -0
- package/Server/Utils/DataSource/HttpFetch.ts +9 -2
- package/Server/Utils/EmailRollup/EmailRollupConstants.ts +98 -0
- package/Server/Utils/EmailRollup/EmailRollupFlushRunner.ts +872 -0
- package/Server/Utils/EmailRollup/EmailRollupRenderer.ts +781 -0
- package/Server/Utils/EmailRollup/EmailRollupWriter.ts +368 -0
- package/Server/Utils/LLM/LLMService.ts +78 -0
- package/Server/Utils/LogRedaction.ts +8 -0
- package/Server/Utils/Logger.ts +79 -1
- package/Server/Utils/Monitor/Criteria/DatabaseMonitorCriteria.ts +190 -0
- package/Server/Utils/Monitor/Criteria/EvaluateOverTime.ts +9 -2
- package/Server/Utils/Monitor/Criteria/SnmpMonitorCriteria.ts +61 -1
- package/Server/Utils/Monitor/MonitorCriteriaDataExtractor.ts +14 -0
- package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +49 -0
- package/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.ts +255 -4
- package/Server/Utils/Monitor/MonitorMetricUtil.ts +41 -0
- package/Server/Utils/Monitor/MonitorTemplateUtil.ts +43 -0
- package/Server/Utils/Monitor/NetworkDeviceHydrationUtil.ts +499 -19
- package/Server/Utils/Monitor/NetworkDeviceMetricUtil.ts +66 -10
- package/Server/Utils/Monitor/NetworkDeviceWalkUtil.ts +238 -44
- package/Server/Utils/Monitor/NetworkInventoryUtil.ts +146 -39
- package/Server/Utils/NetworkDevice/DeviceHealthAggregation.ts +31 -18
- package/Server/Utils/NetworkSite/NetworkSiteHierarchyLock.ts +213 -0
- package/Server/Utils/OutboundUserAgent.ts +152 -0
- package/Server/Utils/StartServer.ts +51 -19
- package/Server/Utils/Telemetry/CaptureSpan.ts +32 -8
- package/Server/Utils/Telemetry/ErrorClassResolver.ts +120 -0
- package/Server/Utils/Telemetry/SpanUtil.ts +33 -0
- package/Server/Utils/Telemetry.ts +198 -51
- package/Server/Views/Partials/SensitiveUrlToken.ejs +140 -0
- package/Tests/App/Dashboard/AddNeighborToMonitoringModal.test.tsx +134 -33
- package/Tests/App/Dashboard/AskAiSuggestedPrompts.test.ts +279 -0
- package/Tests/App/Dashboard/DashboardEditPermissions.test.tsx +517 -0
- package/Tests/App/Dashboard/DashboardVariablesDiscard.test.tsx +26 -0
- package/Tests/App/Dashboard/DeviceStatusHero.test.tsx +588 -0
- package/Tests/App/Dashboard/DiscoveryScanWizardValidation.test.tsx +18 -9
- package/Tests/App/Dashboard/MonitorCreateFromMonitorBackedDevice.test.tsx +632 -0
- package/Tests/App/Dashboard/MonitorRecommendationCreateProgress.test.tsx +682 -0
- package/Tests/App/Dashboard/MonitorTypePicker.test.tsx +4 -2
- package/Tests/App/Dashboard/NetworkDeviceCreateFormMonitorBinding.test.tsx +676 -0
- package/Tests/App/Dashboard/NetworkDeviceCreateFormPingMonitor.test.tsx +791 -0
- package/Tests/App/Dashboard/NetworkDeviceCreateFormProbeAndSnmp.test.tsx +833 -0
- package/Tests/App/Dashboard/NetworkDeviceCriteriaFilter.test.ts +161 -0
- package/Tests/App/Dashboard/NetworkDeviceSettingsMonitoringSection.test.tsx +458 -0
- package/Tests/App/Dashboard/NetworkSideMenu.test.tsx +41 -5
- package/Tests/App/Dashboard/PingMonitorProvisioning.test.ts +368 -0
- package/Tests/App/Dashboard/RecommendationCard.test.tsx +796 -0
- package/Tests/App/Dashboard/RecommendationToolbar.test.tsx +519 -0
- package/Tests/App/Dashboard/RecommendationsList.test.tsx +661 -0
- package/Tests/App/Dashboard/UseBulkCreatePingMonitors.test.tsx +899 -0
- package/Tests/App/Dashboard/UseBulkSnmpCredentialProfileActions.test.tsx +732 -0
- package/Tests/App/SensitiveUrlTokenBootstrap.test.ts +256 -0
- package/Tests/App/StatusPage/StatusPageIndexPageRobotsMeta.test.ts +11 -1
- package/Tests/App/StatusPage/StatusPageLoginCodeBootstrap.test.ts +9 -4
- package/Tests/Models/NetworkSiteHierarchy.test.ts +41 -4
- package/Tests/Server/API/EnterpriseLicenseReportUserCount.test.ts +951 -7
- package/Tests/Server/API/LlmProviderConnectionTest.test.ts +522 -0
- package/Tests/Server/Infrastructure/Postgres/NetworkSnmpCredentialProfilesAndAlertPoliciesMigration.test.ts +854 -0
- package/Tests/Server/Infrastructure/Postgres/SchemaMigrationsOrdering.test.ts +18 -0
- package/Tests/Server/Infrastructure/Postgres/UserNotificationEmailRollupTableMigration.test.ts +535 -0
- package/Tests/Server/Services/AIChatPrivacyPin.test.ts +560 -0
- package/Tests/Server/Services/BillingRechargeOwnerEmailGuards.test.ts +783 -0
- package/Tests/Server/Services/EnterpriseLicenseInstanceServiceDeletionUsage.test.ts +707 -0
- package/Tests/Server/Services/EnterpriseLicenseServiceUsageAggregationLock.test.ts +329 -0
- package/Tests/Server/Services/MonitorServiceDeleteNetworkDeviceCleanup.test.ts +294 -0
- package/Tests/Server/Services/MonitorStatusBridgeProbePath.test.ts +802 -0
- package/Tests/Server/Services/MonitorStatusTimelineService.test.ts +72 -2
- package/Tests/Server/Services/NetworkAlertPolicyEngineHooks.test.ts +752 -0
- package/Tests/Server/Services/NetworkAlertPolicyEngineService.test.ts +1849 -0
- package/Tests/Server/Services/NetworkAlertPolicyModel.test.ts +1793 -0
- package/Tests/Server/Services/NetworkDeviceAutoImportRuleEngineService.test.ts +47 -4
- package/Tests/Server/Services/NetworkDeviceAutoImportRuleService.test.ts +97 -12
- package/Tests/Server/Services/NetworkDeviceAutoMonitorLifecycle.test.ts +148 -3
- package/Tests/Server/Services/NetworkDeviceMonitorBindingUpdateGuard.test.ts +514 -0
- package/Tests/Server/Services/NetworkDeviceMonitorStatusStamp.test.ts +447 -58
- package/Tests/Server/Services/NetworkDeviceMonitoringMethodTransition.test.ts +739 -0
- package/Tests/Server/Services/NetworkDevicePollingTenancy.test.ts +501 -0
- package/Tests/Server/Services/NetworkDeviceSiteDefaultProbe.test.ts +432 -0
- package/Tests/Server/Services/NetworkSiteMonitoringDefaults.test.ts +498 -0
- package/Tests/Server/Services/NetworkSiteRollupPolicyAndMaintenance.test.ts +337 -55
- package/Tests/Server/Services/NetworkSiteService.test.ts +1868 -131
- package/Tests/Server/Services/NetworkSiteTypeService.test.ts +1109 -0
- package/Tests/Server/Services/NetworkSnmpCredentialProfileModel.test.ts +926 -0
- package/Tests/Server/Services/NetworkSnmpCredentialProfileService.test.ts +796 -0
- package/Tests/Server/Services/ProjectServiceNetworkSiteTypeDefaults.test.ts +214 -0
- package/Tests/Server/Services/StatusPagePrivateUserEmailChangePasswordResetExpiry.test.ts +269 -0
- package/Tests/Server/Services/UserEmailChangePasswordResetExpiry.test.ts +472 -0
- package/Tests/Server/Services/UserNotificationEmailRollupModels.test.ts +569 -0
- package/Tests/Server/Services/UserNotificationEmailRollupSettingModel.test.ts +314 -0
- package/Tests/Server/Services/UserNotificationEmailRollupSettingService.test.ts +409 -0
- package/Tests/Server/Services/UserNotificationSettingRollupRouting.test.ts +856 -0
- package/Tests/Server/Services/UserNotificationSettingWorkspaceChannels.test.ts +102 -1
- package/Tests/Server/Types/Database/Permissions/DashboardAccessPermission.test.ts +321 -0
- package/Tests/Server/Types/Database/QueryHelperOperators.test.ts +17 -0
- package/Tests/Server/Utils/AI/AlertMonitorFilters.test.ts +124 -0
- package/Tests/Server/Utils/AI/Insights/InsightTriageRunner.test.ts +208 -0
- package/Tests/Server/Utils/AI/LLMServiceToolCalling.test.ts +235 -0
- package/Tests/Server/Utils/AI/ObservabilityChatPrompt.test.ts +253 -0
- package/Tests/Server/Utils/Captcha.test.ts +422 -0
- package/Tests/Server/Utils/DataSource/HttpFetch.test.ts +96 -2
- package/Tests/Server/Utils/Database/MigrationFailureLog.test.ts +466 -0
- package/Tests/Server/Utils/EmailRollup/EmailRollupFlushRunnerBehaviour.test.ts +1096 -0
- package/Tests/Server/Utils/EmailRollup/EmailRollupFlushRunnerClaim.test.ts +466 -0
- package/Tests/Server/Utils/EmailRollup/EmailRollupRenderer.test.ts +1428 -0
- package/Tests/Server/Utils/EmailRollup/EmailRollupTestHarness.ts +752 -0
- package/Tests/Server/Utils/EmailRollup/EmailRollupWriter.test.ts +864 -0
- package/Tests/Server/Utils/LoggerFaultDemotion.test.ts +346 -0
- package/Tests/Server/Utils/Monitor/Criteria/DatabaseMonitorCriteria.test.ts +633 -0
- package/Tests/Server/Utils/Monitor/Criteria/SnmpMonitorCriteria.test.ts +196 -0
- package/Tests/Server/Utils/Monitor/DatabaseMetricWrite.test.ts +367 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaObservationBuilderUnits.test.ts +100 -0
- package/Tests/Server/Utils/Monitor/MonitorStepResourceIdentity.test.ts +5 -0
- package/Tests/Server/Utils/Monitor/NetworkDeviceHydrationUtil.test.ts +512 -31
- package/Tests/Server/Utils/Monitor/NetworkDeviceMetricUtil.test.ts +252 -0
- package/Tests/Server/Utils/Monitor/NetworkDeviceWalkUtil.test.ts +479 -42
- package/Tests/Server/Utils/Monitor/NetworkInventoryUtil.test.ts +351 -5
- package/Tests/Server/Utils/NetworkDevice/DeviceHealthAggregation.test.ts +136 -12
- package/Tests/Server/Utils/NetworkSite/NetworkSiteHierarchyLock.test.ts +306 -0
- package/Tests/Server/Utils/OutboundUserAgent.test.ts +266 -0
- package/Tests/Server/Utils/SecurityEvent/ThreatIntel/TaxiiClientUserAgent.test.ts +141 -0
- package/Tests/Server/Utils/Telemetry/ErrorClassResolver.test.ts +298 -0
- package/Tests/Server/Utils/Telemetry.test.ts +456 -66
- package/Tests/Types/Monitor/DatabaseMetricCatalog.test.ts +388 -0
- package/Tests/Types/Monitor/MonitorStepDatabaseMonitor.test.ts +243 -0
- package/Tests/Types/Monitor/MonitorStepDefaultTelemetryConfig.test.ts +1 -0
- package/Tests/Types/Monitor/MonitorType.test.ts +1 -0
- package/Tests/Types/Monitor/MonitorTypeKeywords.test.ts +16 -7
- package/Tests/Types/Monitor/NetworkDeviceAlertPack.test.ts +60 -0
- package/Tests/Types/NetworkDevice/NetworkAlertPolicyScope.test.ts +655 -0
- package/Tests/Types/NetworkDevice/NetworkDeviceMonitoringMethod.test.ts +115 -26
- package/Tests/Types/NetworkSite/DefaultNetworkSiteType.test.ts +39 -0
- package/Tests/Types/NotificationSetting/NotificationEmailRollupPolicy.test.ts +299 -0
- package/Tests/Types/Telemetry/ErrorClass.test.ts +483 -0
- package/Tests/UI/Components/BasicRadioButtons.test.tsx +336 -0
- package/Tests/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.test.ts +15 -0
- package/Tests/UI/Components/ProgressBar.test.tsx +38 -2
- package/Tests/UI/Components/SideMenuItem.test.tsx +226 -4
- package/Tests/UI/Components/SideMenuSection.test.tsx +310 -0
- package/Tests/UI/Components/SideOverSubmitState.test.tsx +335 -0
- package/Tests/UI/Utils/PageScrollLock.test.tsx +377 -0
- package/Tests/UI/Utils/PermissionGate.test.ts +78 -0
- package/Tests/UI/Utils/SensitiveUrlToken.test.ts +140 -0
- package/Tests/Utils/EnterpriseLicenseSeats.test.ts +69 -0
- package/Tests/Utils/EnterpriseLicenseSync.test.ts +7 -1
- package/Tests/Utils/EnterpriseLicenseUsage.test.ts +433 -2
- package/Tests/Utils/Monitor/MonitorMetricType.test.ts +280 -1
- package/Tests/Utils/Monitor/MonitorSummarySnapshotUtil.test.ts +1 -0
- package/Tests/Utils/Monitor/NetworkTopologyUtil.test.ts +38 -0
- package/Tests/Utils/NetworkDevice/DeviceHealthStateUtil.test.ts +284 -23
- package/Tests/Utils/NetworkDevice/DeviceReachabilityUtil.test.ts +177 -0
- package/Tests/Utils/NetworkDevice/MonitoringMethodThreadedClassifiers.test.ts +992 -0
- package/Tests/Utils/NetworkDevice/NetworkAlertPolicyBootstrapUtil.test.ts +427 -0
- package/Tests/Utils/NetworkDevice/ReachabilityStampConsistency.test.ts +185 -0
- package/Tests/Utils/NetworkDevice/SnmpCredentialUtil.test.ts +268 -0
- package/Tests/Utils/NetworkDiscovery/AutoImportRuleMatcher.test.ts +55 -5
- package/Tests/Utils/NetworkDiscovery/DiscoveredDeviceBuilder.test.ts +98 -21
- package/Tests/Utils/NetworkDiscovery/DiscoveryImportEligibility.test.ts +98 -20
- package/Tests/Utils/NetworkDiscovery/DiscoveryReverseDnsChain.test.ts +8 -7
- package/Tests/Utils/NetworkDiscovery/PingMonitorBuilderForAddress.test.ts +274 -0
- package/Tests/Utils/NetworkSite/SiteStatusRollupUtil.test.ts +230 -51
- package/Tests/Utils/NetworkSite/TypeHierarchyUtil.test.ts +209 -0
- package/Tests/Utils/Telemetry/CaptureSpanExportGating.test.ts +145 -0
- package/Types/AI/ExceptionAIClassification.ts +10 -24
- package/Types/Email/EmailTemplateType.ts +2 -0
- package/Types/EnterpriseLicense/EnterpriseLicenseInstanceSummary.ts +5 -0
- package/Types/EnterpriseLicense/EnterpriseLicenseUsageSnapshot.ts +13 -0
- package/Types/EnterpriseLicense/EnterpriseLicenseUserCountSource.ts +6 -0
- package/Types/Exception/ApiException.ts +10 -0
- package/Types/Exception/BadDataException.ts +11 -0
- package/Types/Exception/BadRequestException.ts +11 -0
- package/Types/Exception/Exception.ts +65 -0
- package/Types/Exception/ForbiddenException.ts +10 -0
- package/Types/Exception/NotAuthenticatedException.ts +10 -0
- package/Types/Exception/NotAuthorizedException.ts +10 -0
- package/Types/Exception/NotFoundException.ts +11 -0
- package/Types/Exception/PayloadTooLargeException.ts +11 -0
- package/Types/Exception/PaymentRequiredException.ts +10 -0
- package/Types/Exception/ServiceUnavailableException.ts +10 -0
- package/Types/Exception/SsoAuthorizationException.ts +10 -0
- package/Types/Exception/TenantNotFoundException.ts +11 -0
- package/Types/Exception/TimeoutException.ts +10 -0
- package/Types/Exception/TooManyRequestsException.ts +10 -0
- package/Types/Exception/UnableToReachServer.ts +10 -0
- package/Types/Exception/WebsiteRequestException.ts +10 -0
- package/Types/Monitor/CriteriaFilter.ts +60 -2
- package/Types/Monitor/DatabaseMetricCatalog.ts +693 -0
- package/Types/Monitor/DatabaseMonitor/DatabaseMonitorResponse.ts +83 -0
- package/Types/Monitor/MonitorCriteriaInstance.ts +76 -0
- package/Types/Monitor/MonitorMetricType.ts +71 -0
- package/Types/Monitor/MonitorStep.ts +76 -0
- package/Types/Monitor/MonitorStepDatabaseMonitor.ts +171 -0
- package/Types/Monitor/MonitorStepSqlMonitor.ts +2 -20
- package/Types/Monitor/MonitorType.ts +47 -1
- package/Types/Monitor/SnmpMonitor/NetworkDeviceAlertPack.ts +22 -1
- package/Types/Monitor/SqlConnectionConfig.ts +31 -0
- package/Types/NetworkDevice/NetworkAlertPolicyScope.ts +343 -0
- package/Types/NetworkDevice/NetworkDeviceMonitoringMethod.ts +42 -22
- package/Types/NetworkSite/DefaultNetworkSiteTypeHierarchy.ts +30 -0
- package/Types/NotificationSetting/NotificationEmailRollupCategory.ts +255 -0
- package/Types/NotificationSetting/NotificationEmailRollupPolicy.ts +104 -0
- package/Types/Permission.ts +90 -0
- package/Types/Probe/ProbeMonitorResponse.ts +16 -0
- package/Types/Telemetry/ErrorClass.ts +274 -0
- package/Types/Telemetry/UnitOfWork.ts +59 -0
- package/UI/Components/CommandPalette/CommandPalette.tsx +2 -8
- package/UI/Components/EditionLabel/EditionLabel.tsx +4 -0
- package/UI/Components/Modal/Modal.tsx +2 -8
- package/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.ts +44 -0
- package/UI/Components/ProgressBar/ProgressBar.tsx +12 -1
- package/UI/Components/RadioButtons/BasicRadioButtons.tsx +37 -7
- package/UI/Components/SideMenu/SideMenu.tsx +123 -5
- package/UI/Components/SideMenu/SideMenuItem.tsx +76 -84
- package/UI/Components/SideMenu/SideMenuSection.tsx +55 -23
- package/UI/Components/SideOver/SideOver.tsx +21 -20
- package/UI/Utils/PageScrollLock.ts +79 -3
- package/UI/Utils/PermissionGate.ts +61 -0
- package/UI/Utils/SensitiveUrlToken.ts +112 -0
- package/UI/Utils/TestLLMProvider.ts +11 -0
- package/Utils/EnterpriseLicense/EnterpriseLicenseSeats.ts +16 -0
- package/Utils/EnterpriseLicense/EnterpriseLicenseUsage.ts +191 -11
- package/Utils/Monitor/MonitorMetricType.ts +153 -1
- package/Utils/Monitor/NetworkTopologyUtil.ts +23 -3
- package/Utils/NetworkDevice/DeviceHealthStateUtil.ts +65 -14
- package/Utils/NetworkDevice/DeviceReachabilityUtil.ts +43 -14
- package/Utils/NetworkDevice/NetworkAlertPolicyBootstrapUtil.ts +247 -0
- package/Utils/NetworkDevice/SnmpCredentialUtil.ts +68 -0
- package/Utils/NetworkDiscovery/AutoImportRuleMatcher.ts +15 -7
- package/Utils/NetworkDiscovery/DiscoveredDeviceBuilder.ts +52 -17
- package/Utils/NetworkDiscovery/DiscoveryImportEligibility.ts +55 -23
- package/Utils/NetworkDiscovery/PingMonitorBuilder.ts +115 -13
- package/Utils/NetworkSite/SiteStatusRollupUtil.ts +66 -20
- package/Utils/NetworkSite/TypeHierarchyUtil.ts +313 -0
- package/build/dist/Models/AnalyticsModels/Span.js +14 -0
- package/build/dist/Models/AnalyticsModels/Span.js.map +1 -1
- package/build/dist/Models/DatabaseModels/EnterpriseLicense.js +59 -0
- package/build/dist/Models/DatabaseModels/EnterpriseLicense.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +10 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Monitor.js +75 -0
- package/build/dist/Models/DatabaseModels/Monitor.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkAlertPolicy.js +713 -0
- package/build/dist/Models/DatabaseModels/NetworkAlertPolicy.js.map +1 -0
- package/build/dist/Models/DatabaseModels/NetworkDevice.js +180 -1
- package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkSite.js +200 -2
- package/build/dist/Models/DatabaseModels/NetworkSite.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkSiteType.js +102 -3
- package/build/dist/Models/DatabaseModels/NetworkSiteType.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkSnmpCredentialProfile.js +878 -0
- package/build/dist/Models/DatabaseModels/NetworkSnmpCredentialProfile.js.map +1 -0
- package/build/dist/Models/DatabaseModels/TelemetryException.js +123 -0
- package/build/dist/Models/DatabaseModels/TelemetryException.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserNotificationEmailRollupBatch.js +363 -0
- package/build/dist/Models/DatabaseModels/UserNotificationEmailRollupBatch.js.map +1 -0
- package/build/dist/Models/DatabaseModels/UserNotificationEmailRollupItem.js +402 -0
- package/build/dist/Models/DatabaseModels/UserNotificationEmailRollupItem.js.map +1 -0
- package/build/dist/Models/DatabaseModels/UserNotificationEmailRollupSetting.js +296 -0
- package/build/dist/Models/DatabaseModels/UserNotificationEmailRollupSetting.js.map +1 -0
- package/build/dist/Server/API/EnterpriseLicenseAPI.js +273 -60
- package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +1 -1
- package/build/dist/Server/API/LlmProviderAPI.js +108 -13
- package/build/dist/Server/API/LlmProviderAPI.js.map +1 -1
- package/build/dist/Server/EnvironmentConfig.js +12 -0
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790700000000-AddNetworkSiteTypeParentHierarchy.js +24 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790700000000-AddNetworkSiteTypeParentHierarchy.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790900000000-AddEnterpriseLicenseUsageProvenance.js +16 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790900000000-AddEnterpriseLicenseUsageProvenance.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790900000000-AddTelemetryExceptionErrorClass.js +111 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790900000000-AddTelemetryExceptionErrorClass.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791000000000-AddUserNotificationEmailRollup.js +77 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791000000000-AddUserNotificationEmailRollup.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791100000000-AddUserNotificationEmailRollupSetting.js +50 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791100000000-AddUserNotificationEmailRollupSetting.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791200000000-AddNetworkSnmpCredentialProfilesAndAlertPolicies.js +83 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791200000000-AddNetworkSnmpCredentialProfilesAndAlertPolicies.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +12 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Infrastructure/QueueWorker.js +33 -5
- package/build/dist/Server/Infrastructure/QueueWorker.js.map +1 -1
- package/build/dist/Server/Middleware/BearerTokenAuthorization.js +12 -0
- package/build/dist/Server/Middleware/BearerTokenAuthorization.js.map +1 -1
- package/build/dist/Server/Middleware/ProjectAuthorization.js +11 -0
- package/build/dist/Server/Middleware/ProjectAuthorization.js.map +1 -1
- package/build/dist/Server/Middleware/SCIMAuthorization.js +12 -0
- package/build/dist/Server/Middleware/SCIMAuthorization.js.map +1 -1
- package/build/dist/Server/Middleware/TelemetryIngest.js +11 -0
- package/build/dist/Server/Middleware/TelemetryIngest.js.map +1 -1
- package/build/dist/Server/Services/AIBillingService.js +70 -16
- package/build/dist/Server/Services/AIBillingService.js.map +1 -1
- package/build/dist/Server/Services/EnterpriseLicenseInstanceService.js +167 -0
- package/build/dist/Server/Services/EnterpriseLicenseInstanceService.js.map +1 -1
- package/build/dist/Server/Services/EnterpriseLicenseService.js +39 -0
- package/build/dist/Server/Services/EnterpriseLicenseService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +10 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/MonitorService.js +119 -21
- package/build/dist/Server/Services/MonitorService.js.map +1 -1
- package/build/dist/Server/Services/MonitorStatusTimelineService.js +127 -8
- package/build/dist/Server/Services/MonitorStatusTimelineService.js.map +1 -1
- package/build/dist/Server/Services/MonitorTemplateService.js +162 -0
- package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
- package/build/dist/Server/Services/NetworkAlertPolicyEngineService.js +1683 -0
- package/build/dist/Server/Services/NetworkAlertPolicyEngineService.js.map +1 -0
- package/build/dist/Server/Services/NetworkAlertPolicyService.js +732 -0
- package/build/dist/Server/Services/NetworkAlertPolicyService.js.map +1 -0
- package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js +10 -9
- package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceAutoImportRuleService.js +54 -11
- package/build/dist/Server/Services/NetworkDeviceAutoImportRuleService.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceService.js +841 -70
- package/build/dist/Server/Services/NetworkDeviceService.js.map +1 -1
- package/build/dist/Server/Services/NetworkSiteService.js +1145 -107
- package/build/dist/Server/Services/NetworkSiteService.js.map +1 -1
- package/build/dist/Server/Services/NetworkSiteTypeService.js +863 -0
- package/build/dist/Server/Services/NetworkSiteTypeService.js.map +1 -1
- package/build/dist/Server/Services/NetworkSnmpCredentialProfileService.js +217 -0
- package/build/dist/Server/Services/NetworkSnmpCredentialProfileService.js.map +1 -0
- package/build/dist/Server/Services/NotificationService.js +80 -17
- package/build/dist/Server/Services/NotificationService.js.map +1 -1
- package/build/dist/Server/Services/ProjectService.js +66 -30
- package/build/dist/Server/Services/ProjectService.js.map +1 -1
- package/build/dist/Server/Services/StatusPagePrivateUserService.js +92 -2
- package/build/dist/Server/Services/StatusPagePrivateUserService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationEmailRollupBatchService.js +76 -0
- package/build/dist/Server/Services/UserNotificationEmailRollupBatchService.js.map +1 -0
- package/build/dist/Server/Services/UserNotificationEmailRollupItemService.js +77 -0
- package/build/dist/Server/Services/UserNotificationEmailRollupItemService.js.map +1 -0
- package/build/dist/Server/Services/UserNotificationEmailRollupSettingService.js +111 -0
- package/build/dist/Server/Services/UserNotificationEmailRollupSettingService.js.map +1 -0
- package/build/dist/Server/Services/UserNotificationSettingService.js +46 -20
- package/build/dist/Server/Services/UserNotificationSettingService.js.map +1 -1
- package/build/dist/Server/Services/UserService.js +90 -0
- package/build/dist/Server/Services/UserService.js.map +1 -1
- package/build/dist/Server/Types/Database/QueryHelper.js +1 -1
- package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
- package/build/dist/Server/Utils/AI/Chat/ObservabilityChatPrompt.js +14 -3
- package/build/dist/Server/Utils/AI/Chat/ObservabilityChatPrompt.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/Insights/Detectors/ExceptionSpikeDetector.js +10 -0
- package/build/dist/Server/Utils/AI/SRE/Insights/Detectors/ExceptionSpikeDetector.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/Insights/Detectors/NewExceptionDetector.js +10 -0
- package/build/dist/Server/Utils/AI/SRE/Insights/Detectors/NewExceptionDetector.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/Insights/InsightTriageRunner.js +54 -1
- package/build/dist/Server/Utils/AI/SRE/Insights/InsightTriageRunner.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/AlertTools.js +18 -4
- package/build/dist/Server/Utils/AI/Toolbox/AlertTools.js.map +1 -1
- package/build/dist/Server/Utils/Billing/BillingFailureNoticeThrottle.js +59 -0
- package/build/dist/Server/Utils/Billing/BillingFailureNoticeThrottle.js.map +1 -0
- package/build/dist/Server/Utils/DataSource/HttpFetch.js +2 -1
- package/build/dist/Server/Utils/DataSource/HttpFetch.js.map +1 -1
- package/build/dist/Server/Utils/EmailRollup/EmailRollupConstants.js +91 -0
- package/build/dist/Server/Utils/EmailRollup/EmailRollupConstants.js.map +1 -0
- package/build/dist/Server/Utils/EmailRollup/EmailRollupFlushRunner.js +691 -0
- package/build/dist/Server/Utils/EmailRollup/EmailRollupFlushRunner.js.map +1 -0
- package/build/dist/Server/Utils/EmailRollup/EmailRollupRenderer.js +497 -0
- package/build/dist/Server/Utils/EmailRollup/EmailRollupRenderer.js.map +1 -0
- package/build/dist/Server/Utils/EmailRollup/EmailRollupWriter.js +257 -0
- package/build/dist/Server/Utils/EmailRollup/EmailRollupWriter.js.map +1 -0
- package/build/dist/Server/Utils/LLM/LLMService.js +64 -1
- package/build/dist/Server/Utils/LLM/LLMService.js.map +1 -1
- package/build/dist/Server/Utils/LogRedaction.js +8 -0
- package/build/dist/Server/Utils/LogRedaction.js.map +1 -1
- package/build/dist/Server/Utils/Logger.js +63 -1
- package/build/dist/Server/Utils/Logger.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/DatabaseMonitorCriteria.js +141 -0
- package/build/dist/Server/Utils/Monitor/Criteria/DatabaseMonitorCriteria.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/Criteria/EvaluateOverTime.js +8 -1
- package/build/dist/Server/Utils/Monitor/Criteria/EvaluateOverTime.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/SnmpMonitorCriteria.js +51 -1
- package/build/dist/Server/Utils/Monitor/Criteria/SnmpMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaDataExtractor.js +7 -0
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaDataExtractor.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +37 -0
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js +158 -6
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorMetricUtil.js +33 -0
- package/build/dist/Server/Utils/Monitor/MonitorMetricUtil.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorTemplateUtil.js +32 -0
- package/build/dist/Server/Utils/Monitor/MonitorTemplateUtil.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/NetworkDeviceHydrationUtil.js +330 -21
- package/build/dist/Server/Utils/Monitor/NetworkDeviceHydrationUtil.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/NetworkDeviceMetricUtil.js +55 -10
- package/build/dist/Server/Utils/Monitor/NetworkDeviceMetricUtil.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/NetworkDeviceWalkUtil.js +178 -49
- package/build/dist/Server/Utils/Monitor/NetworkDeviceWalkUtil.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js +119 -29
- package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js.map +1 -1
- package/build/dist/Server/Utils/NetworkDevice/DeviceHealthAggregation.js +30 -17
- package/build/dist/Server/Utils/NetworkDevice/DeviceHealthAggregation.js.map +1 -1
- package/build/dist/Server/Utils/NetworkSite/NetworkSiteHierarchyLock.js +158 -0
- package/build/dist/Server/Utils/NetworkSite/NetworkSiteHierarchyLock.js.map +1 -0
- package/build/dist/Server/Utils/OutboundUserAgent.js +123 -0
- package/build/dist/Server/Utils/OutboundUserAgent.js.map +1 -0
- package/build/dist/Server/Utils/StartServer.js +38 -14
- package/build/dist/Server/Utils/StartServer.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/CaptureSpan.js +28 -8
- package/build/dist/Server/Utils/Telemetry/CaptureSpan.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/ErrorClassResolver.js +95 -0
- package/build/dist/Server/Utils/Telemetry/ErrorClassResolver.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/SpanUtil.js +29 -0
- package/build/dist/Server/Utils/Telemetry/SpanUtil.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry.js +162 -46
- package/build/dist/Server/Utils/Telemetry.js.map +1 -1
- package/build/dist/Types/AI/ExceptionAIClassification.js +10 -24
- package/build/dist/Types/AI/ExceptionAIClassification.js.map +1 -1
- package/build/dist/Types/Email/EmailTemplateType.js +1 -0
- package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
- package/build/dist/Types/EnterpriseLicense/EnterpriseLicenseUsageSnapshot.js +2 -0
- package/build/dist/Types/EnterpriseLicense/EnterpriseLicenseUsageSnapshot.js.map +1 -0
- package/build/dist/Types/EnterpriseLicense/EnterpriseLicenseUserCountSource.js +7 -0
- package/build/dist/Types/EnterpriseLicense/EnterpriseLicenseUserCountSource.js.map +1 -0
- package/build/dist/Types/Exception/ApiException.js +9 -0
- package/build/dist/Types/Exception/ApiException.js.map +1 -1
- package/build/dist/Types/Exception/BadDataException.js +10 -0
- package/build/dist/Types/Exception/BadDataException.js.map +1 -1
- package/build/dist/Types/Exception/BadRequestException.js +10 -0
- package/build/dist/Types/Exception/BadRequestException.js.map +1 -1
- package/build/dist/Types/Exception/Exception.js +56 -0
- package/build/dist/Types/Exception/Exception.js.map +1 -1
- package/build/dist/Types/Exception/ForbiddenException.js +9 -0
- package/build/dist/Types/Exception/ForbiddenException.js.map +1 -1
- package/build/dist/Types/Exception/NotAuthenticatedException.js +9 -0
- package/build/dist/Types/Exception/NotAuthenticatedException.js.map +1 -1
- package/build/dist/Types/Exception/NotAuthorizedException.js +9 -0
- package/build/dist/Types/Exception/NotAuthorizedException.js.map +1 -1
- package/build/dist/Types/Exception/NotFoundException.js +10 -0
- package/build/dist/Types/Exception/NotFoundException.js.map +1 -1
- package/build/dist/Types/Exception/PayloadTooLargeException.js +10 -0
- package/build/dist/Types/Exception/PayloadTooLargeException.js.map +1 -1
- package/build/dist/Types/Exception/PaymentRequiredException.js +9 -0
- package/build/dist/Types/Exception/PaymentRequiredException.js.map +1 -1
- package/build/dist/Types/Exception/ServiceUnavailableException.js +9 -0
- package/build/dist/Types/Exception/ServiceUnavailableException.js.map +1 -1
- package/build/dist/Types/Exception/SsoAuthorizationException.js +9 -0
- package/build/dist/Types/Exception/SsoAuthorizationException.js.map +1 -1
- package/build/dist/Types/Exception/TenantNotFoundException.js +10 -0
- package/build/dist/Types/Exception/TenantNotFoundException.js.map +1 -1
- package/build/dist/Types/Exception/TimeoutException.js +9 -0
- package/build/dist/Types/Exception/TimeoutException.js.map +1 -1
- package/build/dist/Types/Exception/TooManyRequestsException.js +9 -0
- package/build/dist/Types/Exception/TooManyRequestsException.js.map +1 -1
- package/build/dist/Types/Exception/UnableToReachServer.js +9 -0
- package/build/dist/Types/Exception/UnableToReachServer.js.map +1 -1
- package/build/dist/Types/Exception/WebsiteRequestException.js +9 -0
- package/build/dist/Types/Exception/WebsiteRequestException.js.map +1 -1
- package/build/dist/Types/Monitor/CriteriaFilter.js +45 -2
- package/build/dist/Types/Monitor/CriteriaFilter.js.map +1 -1
- package/build/dist/Types/Monitor/DatabaseMetricCatalog.js +594 -0
- package/build/dist/Types/Monitor/DatabaseMetricCatalog.js.map +1 -0
- package/build/dist/Types/Monitor/DatabaseMonitor/DatabaseMonitorResponse.js +21 -0
- package/build/dist/Types/Monitor/DatabaseMonitor/DatabaseMonitorResponse.js.map +1 -0
- package/build/dist/Types/Monitor/MonitorCriteriaInstance.js +70 -0
- package/build/dist/Types/Monitor/MonitorCriteriaInstance.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorMetricType.js +61 -0
- package/build/dist/Types/Monitor/MonitorMetricType.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorStep.js +54 -0
- package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorStepDatabaseMonitor.js +122 -0
- package/build/dist/Types/Monitor/MonitorStepDatabaseMonitor.js.map +1 -0
- package/build/dist/Types/Monitor/MonitorStepSqlMonitor.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorType.js +45 -1
- package/build/dist/Types/Monitor/MonitorType.js.map +1 -1
- package/build/dist/Types/Monitor/SnmpMonitor/NetworkDeviceAlertPack.js +21 -1
- package/build/dist/Types/Monitor/SnmpMonitor/NetworkDeviceAlertPack.js.map +1 -1
- package/build/dist/Types/Monitor/SqlConnectionConfig.js +2 -0
- package/build/dist/Types/Monitor/SqlConnectionConfig.js.map +1 -0
- package/build/dist/Types/NetworkDevice/NetworkAlertPolicyScope.js +172 -0
- package/build/dist/Types/NetworkDevice/NetworkAlertPolicyScope.js.map +1 -0
- package/build/dist/Types/NetworkDevice/NetworkDeviceMonitoringMethod.js +40 -22
- package/build/dist/Types/NetworkDevice/NetworkDeviceMonitoringMethod.js.map +1 -1
- package/build/dist/Types/NetworkSite/DefaultNetworkSiteTypeHierarchy.js +26 -0
- package/build/dist/Types/NetworkSite/DefaultNetworkSiteTypeHierarchy.js.map +1 -0
- package/build/dist/Types/NotificationSetting/NotificationEmailRollupCategory.js +186 -0
- package/build/dist/Types/NotificationSetting/NotificationEmailRollupCategory.js.map +1 -0
- package/build/dist/Types/NotificationSetting/NotificationEmailRollupPolicy.js +87 -0
- package/build/dist/Types/NotificationSetting/NotificationEmailRollupPolicy.js.map +1 -0
- package/build/dist/Types/Permission.js +80 -0
- package/build/dist/Types/Permission.js.map +1 -1
- package/build/dist/Types/Telemetry/ErrorClass.js +216 -0
- package/build/dist/Types/Telemetry/ErrorClass.js.map +1 -0
- package/build/dist/Types/Telemetry/UnitOfWork.js +59 -0
- package/build/dist/Types/Telemetry/UnitOfWork.js.map +1 -0
- package/build/dist/UI/Components/CommandPalette/CommandPalette.js +2 -7
- package/build/dist/UI/Components/CommandPalette/CommandPalette.js.map +1 -1
- package/build/dist/UI/Components/EditionLabel/EditionLabel.js +3 -0
- package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
- package/build/dist/UI/Components/Modal/Modal.js +2 -7
- package/build/dist/UI/Components/Modal/Modal.js.map +1 -1
- package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.js +39 -0
- package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.js.map +1 -1
- package/build/dist/UI/Components/ProgressBar/ProgressBar.js +12 -1
- package/build/dist/UI/Components/ProgressBar/ProgressBar.js.map +1 -1
- package/build/dist/UI/Components/RadioButtons/BasicRadioButtons.js +6 -6
- package/build/dist/UI/Components/RadioButtons/BasicRadioButtons.js.map +1 -1
- package/build/dist/UI/Components/SideMenu/SideMenu.js +67 -5
- package/build/dist/UI/Components/SideMenu/SideMenu.js.map +1 -1
- package/build/dist/UI/Components/SideMenu/SideMenuItem.js +49 -53
- package/build/dist/UI/Components/SideMenu/SideMenuItem.js.map +1 -1
- package/build/dist/UI/Components/SideMenu/SideMenuSection.js +25 -7
- package/build/dist/UI/Components/SideMenu/SideMenuSection.js.map +1 -1
- package/build/dist/UI/Components/SideOver/SideOver.js +5 -20
- package/build/dist/UI/Components/SideOver/SideOver.js.map +1 -1
- package/build/dist/UI/Utils/PageScrollLock.js +31 -3
- package/build/dist/UI/Utils/PageScrollLock.js.map +1 -1
- package/build/dist/UI/Utils/PermissionGate.js +42 -1
- package/build/dist/UI/Utils/PermissionGate.js.map +1 -1
- package/build/dist/UI/Utils/SensitiveUrlToken.js +97 -0
- package/build/dist/UI/Utils/SensitiveUrlToken.js.map +1 -0
- package/build/dist/UI/Utils/TestLLMProvider.js +4 -5
- package/build/dist/UI/Utils/TestLLMProvider.js.map +1 -1
- package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSeats.js +15 -0
- package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSeats.js.map +1 -1
- package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseUsage.js +111 -9
- package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseUsage.js.map +1 -1
- package/build/dist/Utils/Monitor/MonitorMetricType.js +109 -1
- package/build/dist/Utils/Monitor/MonitorMetricType.js.map +1 -1
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +8 -3
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
- package/build/dist/Utils/NetworkDevice/DeviceHealthStateUtil.js +27 -7
- package/build/dist/Utils/NetworkDevice/DeviceHealthStateUtil.js.map +1 -1
- package/build/dist/Utils/NetworkDevice/DeviceReachabilityUtil.js +7 -5
- package/build/dist/Utils/NetworkDevice/DeviceReachabilityUtil.js.map +1 -1
- package/build/dist/Utils/NetworkDevice/NetworkAlertPolicyBootstrapUtil.js +180 -0
- package/build/dist/Utils/NetworkDevice/NetworkAlertPolicyBootstrapUtil.js.map +1 -0
- package/build/dist/Utils/NetworkDevice/SnmpCredentialUtil.js +12 -0
- package/build/dist/Utils/NetworkDevice/SnmpCredentialUtil.js.map +1 -0
- package/build/dist/Utils/NetworkDiscovery/AutoImportRuleMatcher.js +15 -5
- package/build/dist/Utils/NetworkDiscovery/AutoImportRuleMatcher.js.map +1 -1
- package/build/dist/Utils/NetworkDiscovery/DiscoveredDeviceBuilder.js +44 -15
- package/build/dist/Utils/NetworkDiscovery/DiscoveredDeviceBuilder.js.map +1 -1
- package/build/dist/Utils/NetworkDiscovery/DiscoveryImportEligibility.js +52 -23
- package/build/dist/Utils/NetworkDiscovery/DiscoveryImportEligibility.js.map +1 -1
- package/build/dist/Utils/NetworkDiscovery/PingMonitorBuilder.js +85 -13
- package/build/dist/Utils/NetworkDiscovery/PingMonitorBuilder.js.map +1 -1
- package/build/dist/Utils/NetworkSite/SiteStatusRollupUtil.js +30 -15
- package/build/dist/Utils/NetworkSite/SiteStatusRollupUtil.js.map +1 -1
- package/build/dist/Utils/NetworkSite/TypeHierarchyUtil.js +191 -0
- package/build/dist/Utils/NetworkSite/TypeHierarchyUtil.js.map +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,1096 @@
|
|
|
1
|
+
import MailService from "../../../../Server/Services/MailService";
|
|
2
|
+
import ProjectService from "../../../../Server/Services/ProjectService";
|
|
3
|
+
import UserEmailService from "../../../../Server/Services/UserEmailService";
|
|
4
|
+
import UserNotificationEmailRollupItemService from "../../../../Server/Services/UserNotificationEmailRollupItemService";
|
|
5
|
+
import EmailRollupFlushRunner, {
|
|
6
|
+
RollupSweepStats,
|
|
7
|
+
} from "../../../../Server/Utils/EmailRollup/EmailRollupFlushRunner";
|
|
8
|
+
import * as EmailRollupRenderer from "../../../../Server/Utils/EmailRollup/EmailRollupRenderer";
|
|
9
|
+
import {
|
|
10
|
+
FLUSH_AFTER_MINUTES,
|
|
11
|
+
MAX_BUCKETS_PER_TICK,
|
|
12
|
+
MAX_ITEMS_PER_ROLLUP,
|
|
13
|
+
MAX_ITEMS_SCANNED_PER_TICK,
|
|
14
|
+
ROLLUP_SEND_TIMEOUT_MS,
|
|
15
|
+
ROLLUP_SWEEP_BUDGET_MS,
|
|
16
|
+
} from "../../../../Server/Utils/EmailRollup/EmailRollupConstants";
|
|
17
|
+
import { RollupBatchStatus } from "../../../../Models/DatabaseModels/UserNotificationEmailRollupBatch";
|
|
18
|
+
import OneUptimeDate from "../../../../Types/Date";
|
|
19
|
+
import Email from "../../../../Types/Email";
|
|
20
|
+
import EmailTemplateType from "../../../../Types/Email/EmailTemplateType";
|
|
21
|
+
import RollupCategory from "../../../../Types/NotificationSetting/NotificationEmailRollupCategory";
|
|
22
|
+
import ObjectID from "../../../../Types/ObjectID";
|
|
23
|
+
import {
|
|
24
|
+
FakeBatchRow,
|
|
25
|
+
FakeItemRow,
|
|
26
|
+
HARNESS_DASHBOARD_URL,
|
|
27
|
+
RollupHarness,
|
|
28
|
+
SentRollupMail,
|
|
29
|
+
batchesOfStatus,
|
|
30
|
+
emptyRollupHarness,
|
|
31
|
+
installRollupHarness,
|
|
32
|
+
pendingItems,
|
|
33
|
+
seedItem,
|
|
34
|
+
seedProject,
|
|
35
|
+
seedVerifiedEmail,
|
|
36
|
+
} from "./EmailRollupTestHarness";
|
|
37
|
+
import { afterEach, beforeEach, describe, expect, test } from "@jest/globals";
|
|
38
|
+
|
|
39
|
+
/*
|
|
40
|
+
* WHAT ONE FLUSH ACTUALLY DOES. What breaks in production if any of the
|
|
41
|
+
* behaviour below regresses:
|
|
42
|
+
*
|
|
43
|
+
* 1. COALESCING IS THE FEATURE. N pending notifications for one address must
|
|
44
|
+
* become ONE email. If the bucketing ever leaks - keyed on the wrong
|
|
45
|
+
* columns, or re-claimed per item - the recipient gets N emails a few
|
|
46
|
+
* minutes late, which is strictly worse than the problem this replaced.
|
|
47
|
+
*
|
|
48
|
+
* 2. STAMP BEFORE SEND. The items are marked sent BEFORE MailService is
|
|
49
|
+
* called. Reverse it and a permanently broken SMTP server produces a
|
|
50
|
+
* rollup that grows every epoch and is re-sent every epoch forever -
|
|
51
|
+
* exactly the storm the feature exists to stop. The cost is a bounded,
|
|
52
|
+
* recorded loss on a hard send failure, and that trade is pinned here so
|
|
53
|
+
* nobody "fixes" it without reading why.
|
|
54
|
+
*
|
|
55
|
+
* 3. THE RECIPIENT IS RE-VALIDATED AT SEND TIME. The address was captured
|
|
56
|
+
* minutes ago; the individual sends this replaces each re-read the
|
|
57
|
+
* verified set. An address unverified or removed in between must not be
|
|
58
|
+
* handed a list of this project's incident subjects.
|
|
59
|
+
*
|
|
60
|
+
* 4. EVERY LOOP IS BOUNDED. Items per batch, buckets per tick, and a
|
|
61
|
+
* wall-clock budget. Without all three, one tenant's backlog spike blows
|
|
62
|
+
* the job timeout part way through the loop, AFTER rows are stamped and
|
|
63
|
+
* before they are sent - which loses notifications silently.
|
|
64
|
+
*
|
|
65
|
+
* 5. THE CORRELATION IDS. MailService writes an EmailLog row only when
|
|
66
|
+
* projectId is present, so dropping it makes every rollup invisible in
|
|
67
|
+
* Notification Logs. And a rollup spans many resources, so attaching any
|
|
68
|
+
* single incidentId / alertId / monitorId would be a lie recorded in the
|
|
69
|
+
* log.
|
|
70
|
+
*
|
|
71
|
+
* 6. THE DISCOVERY PREDICATES ARE THE SCHEDULE. "Five minutes late, at
|
|
72
|
+
* most" is nothing but `createdAt <= now - FLUSH_AFTER_MINUTES`, and
|
|
73
|
+
* "each row rides in exactly one rollup" is nothing but
|
|
74
|
+
* `sentAt IS NULL`. Drop either and the feature still passes every test
|
|
75
|
+
* about what an email CONTAINS while being a different feature.
|
|
76
|
+
*
|
|
77
|
+
* 7. THE DRAIN IS FIFO. updateBy resolves the rows it writes with an
|
|
78
|
+
* internal find that defaults to createdAt DESCENDING, so an over-full
|
|
79
|
+
* bucket stamped by a bare limited updateBy is a LIFO queue: its oldest
|
|
80
|
+
* notifications are never sent and are hard-deleted unsent at
|
|
81
|
+
* ROLLUP_ITEM_RETENTION_DAYS.
|
|
82
|
+
*
|
|
83
|
+
* 8. NOTHING BETWEEN THE STAMP AND THE SEND MAY THROW ITS WAY OUT. Past the
|
|
84
|
+
* stamp, those rows exist only as an obligation to send this one email.
|
|
85
|
+
* An escaping exception makes that obligation vanish with no trace but a
|
|
86
|
+
* log line and a batch row still reading Claimed.
|
|
87
|
+
*
|
|
88
|
+
* 9. THE SEND CANNOT HANG. The sweep holds a Redis mutex whose lock
|
|
89
|
+
* auto-refreshes while held, so one wedged HTTP POST would stop every
|
|
90
|
+
* rollup on the install, on every replica, forever.
|
|
91
|
+
*/
|
|
92
|
+
|
|
93
|
+
const PROJECT_ID: ObjectID = new ObjectID(
|
|
94
|
+
"11111111-1111-4111-8111-111111111111",
|
|
95
|
+
);
|
|
96
|
+
const USER_ID: ObjectID = new ObjectID("22222222-2222-4222-8222-222222222222");
|
|
97
|
+
const TO_EMAIL: Email = new Email("owner@example.com");
|
|
98
|
+
|
|
99
|
+
const NOW: Date = OneUptimeDate.fromString("2026-09-03T17:07:30.000Z");
|
|
100
|
+
|
|
101
|
+
describe("EmailRollupFlushRunner - one flush", () => {
|
|
102
|
+
let harness: RollupHarness;
|
|
103
|
+
|
|
104
|
+
beforeEach(() => {
|
|
105
|
+
harness = emptyRollupHarness();
|
|
106
|
+
installRollupHarness(harness);
|
|
107
|
+
|
|
108
|
+
seedProject(harness, { projectId: PROJECT_ID, name: "Acme" });
|
|
109
|
+
seedVerifiedEmail(harness, {
|
|
110
|
+
projectId: PROJECT_ID,
|
|
111
|
+
userId: USER_ID,
|
|
112
|
+
email: TO_EMAIL,
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
afterEach(() => {
|
|
117
|
+
/*
|
|
118
|
+
* Unconditional: only the send-timeout tests install fake timers, and
|
|
119
|
+
* leaving them installed would make every later suite's setTimeout hang.
|
|
120
|
+
*/
|
|
121
|
+
jest.useRealTimers();
|
|
122
|
+
jest.restoreAllMocks();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
function seedDue(data: {
|
|
126
|
+
minutesAgo: number;
|
|
127
|
+
subject?: string | undefined;
|
|
128
|
+
viewLink?: string | undefined;
|
|
129
|
+
rollupCategory?: RollupCategory | undefined;
|
|
130
|
+
userId?: ObjectID | undefined;
|
|
131
|
+
toEmail?: Email | undefined;
|
|
132
|
+
}): FakeItemRow {
|
|
133
|
+
return seedItem(harness, {
|
|
134
|
+
projectId: PROJECT_ID,
|
|
135
|
+
userId: data.userId ?? USER_ID,
|
|
136
|
+
toEmail: data.toEmail ?? TO_EMAIL,
|
|
137
|
+
createdAt: OneUptimeDate.addRemoveMinutes(NOW, data.minutesAgo * -1),
|
|
138
|
+
subject: data.subject,
|
|
139
|
+
viewLink: data.viewLink,
|
|
140
|
+
rollupCategory: data.rollupCategory,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/*
|
|
145
|
+
* ----------------------------------------------------------------------- *
|
|
146
|
+
* (A) Coalescing.
|
|
147
|
+
* -----------------------------------------------------------------------
|
|
148
|
+
*/
|
|
149
|
+
|
|
150
|
+
describe("coalescing", () => {
|
|
151
|
+
test("two pending items for one address become exactly one email", async () => {
|
|
152
|
+
seedDue({ minutesAgo: 12, subject: "Checkout is down" });
|
|
153
|
+
seedDue({ minutesAgo: 11, subject: "Checkout acknowledged" });
|
|
154
|
+
|
|
155
|
+
const stats: RollupSweepStats = await EmailRollupFlushRunner.runSweep({
|
|
156
|
+
now: NOW,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
expect(harness.sent).toHaveLength(1);
|
|
160
|
+
expect(stats.sent).toBe(1);
|
|
161
|
+
expect(stats.bucketsDue).toBe(1);
|
|
162
|
+
|
|
163
|
+
const mail: SentRollupMail = harness.sent[0]!;
|
|
164
|
+
|
|
165
|
+
expect(mail.toEmail).toBe(TO_EMAIL.toString());
|
|
166
|
+
expect(mail.templateType).toBe(EmailTemplateType.NotificationRollup);
|
|
167
|
+
expect(mail.subject).toBe("[Acme] 2 notifications: 2 Incidents");
|
|
168
|
+
expect(harness.batches[0]!.itemCount).toBe(2);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("builds the project home and preferences links from the dashboard URL", async () => {
|
|
172
|
+
seedDue({ minutesAgo: 12 });
|
|
173
|
+
|
|
174
|
+
await EmailRollupFlushRunner.runSweep({ now: NOW });
|
|
175
|
+
|
|
176
|
+
const vars: Record<string, unknown> = harness.sent[0]!.vars as Record<
|
|
177
|
+
string,
|
|
178
|
+
unknown
|
|
179
|
+
>;
|
|
180
|
+
|
|
181
|
+
expect(vars["projectHomeLink"]).toBe(
|
|
182
|
+
`${HARNESS_DASHBOARD_URL}/${PROJECT_ID.toString()}/home`,
|
|
183
|
+
);
|
|
184
|
+
expect(vars["preferencesLink"]).toBe(
|
|
185
|
+
`${HARNESS_DASHBOARD_URL}/${PROJECT_ID.toString()}/user-settings/notification-settings`,
|
|
186
|
+
);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("reads the project name at flush time rather than replaying a snapshot", async () => {
|
|
190
|
+
seedDue({ minutesAgo: 12 });
|
|
191
|
+
|
|
192
|
+
// Renamed after the items were queued.
|
|
193
|
+
harness.projects[0]!["name"] = "Acme Renamed";
|
|
194
|
+
|
|
195
|
+
await EmailRollupFlushRunner.runSweep({ now: NOW });
|
|
196
|
+
|
|
197
|
+
expect(harness.sent[0]!.subject).toContain("[Acme Renamed]");
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("a bucket whose items were all stamped between discovery and claim ends Empty and says nothing", async () => {
|
|
201
|
+
seedDue({ minutesAgo: 12 });
|
|
202
|
+
seedDue({ minutesAgo: 11 });
|
|
203
|
+
|
|
204
|
+
/*
|
|
205
|
+
* Another replica finished its own flush of this bucket in the window
|
|
206
|
+
* between our discovery and our claim. Silence is the only correct
|
|
207
|
+
* output: everything we would have described has already been mailed.
|
|
208
|
+
*/
|
|
209
|
+
harness.beforeBatchCreate = (): void => {
|
|
210
|
+
for (const item of harness.items) {
|
|
211
|
+
item.sentAt = NOW;
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const stats: RollupSweepStats = await EmailRollupFlushRunner.runSweep({
|
|
216
|
+
now: NOW,
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
expect(harness.sendAttempts).toHaveLength(0);
|
|
220
|
+
expect(stats.empty).toBe(1);
|
|
221
|
+
expect(harness.batches[0]!.status).toBe(RollupBatchStatus.Empty);
|
|
222
|
+
expect(harness.batches[0]!.itemCount).toBe(0);
|
|
223
|
+
expect(harness.batches[0]!.sentAt).toBeNull();
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
/*
|
|
228
|
+
* ----------------------------------------------------------------------- *
|
|
229
|
+
* (B) Stamp before send.
|
|
230
|
+
* -----------------------------------------------------------------------
|
|
231
|
+
*/
|
|
232
|
+
|
|
233
|
+
describe("stamp before send", () => {
|
|
234
|
+
test("the items are stamped before MailService is called", async () => {
|
|
235
|
+
seedDue({ minutesAgo: 12 });
|
|
236
|
+
|
|
237
|
+
await EmailRollupFlushRunner.runSweep({ now: NOW });
|
|
238
|
+
|
|
239
|
+
expect(harness.callLog).toEqual(["stamp", "send"]);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("a throwing send leaves the items stamped, so a broken mailer cannot re-spam every epoch", async () => {
|
|
243
|
+
seedDue({ minutesAgo: 12 });
|
|
244
|
+
seedDue({ minutesAgo: 11 });
|
|
245
|
+
|
|
246
|
+
const boom: Error = new Error("SMTP connection refused");
|
|
247
|
+
|
|
248
|
+
harness.failSend = (): Error => {
|
|
249
|
+
return boom;
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const stats: RollupSweepStats = await EmailRollupFlushRunner.runSweep({
|
|
253
|
+
now: NOW,
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
expect(stats.failed).toBe(1);
|
|
257
|
+
// Not rethrown: one dead mailer must not abort the whole tick.
|
|
258
|
+
expect(stats.errors).toBe(0);
|
|
259
|
+
expect(pendingItems(harness)).toHaveLength(0);
|
|
260
|
+
|
|
261
|
+
const batch: FakeBatchRow = harness.batches[0]!;
|
|
262
|
+
|
|
263
|
+
expect(batch.status).toBe(RollupBatchStatus.Failed);
|
|
264
|
+
expect(batch.itemCount).toBe(2);
|
|
265
|
+
expect(batch.sentAt).toBeNull();
|
|
266
|
+
expect(batch.statusMessage).toBe("SMTP connection refused");
|
|
267
|
+
|
|
268
|
+
/*
|
|
269
|
+
* Logged TWICE, and both are wanted. The first is the flush recording
|
|
270
|
+
* why this batch failed. The second is the guard withSendTimeout
|
|
271
|
+
* attaches to the losing side of its Promise.race: the send's rejection
|
|
272
|
+
* is still unhandled once the race has settled, and an unhandled
|
|
273
|
+
* rejection can take the worker down.
|
|
274
|
+
*/
|
|
275
|
+
expect(harness.errors).toEqual([boom, boom]);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test("a hostile error message is truncated to the statusMessage column length", async () => {
|
|
279
|
+
seedDue({ minutesAgo: 12 });
|
|
280
|
+
|
|
281
|
+
harness.failSend = (): Error => {
|
|
282
|
+
return new Error("x".repeat(5000));
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
await EmailRollupFlushRunner.runSweep({ now: NOW });
|
|
286
|
+
|
|
287
|
+
const message: string = harness.batches[0]!.statusMessage ?? "";
|
|
288
|
+
|
|
289
|
+
expect(message).toHaveLength(500);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test("an error with no message still records something rather than nothing", async () => {
|
|
293
|
+
seedDue({ minutesAgo: 12 });
|
|
294
|
+
|
|
295
|
+
harness.failSend = (): Error => {
|
|
296
|
+
return new Error("");
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
await EmailRollupFlushRunner.runSweep({ now: NOW });
|
|
300
|
+
|
|
301
|
+
expect(harness.batches[0]!.status).toBe(RollupBatchStatus.Failed);
|
|
302
|
+
expect(harness.batches[0]!.statusMessage).toBe("unknown error");
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
/*
|
|
307
|
+
* ----------------------------------------------------------------------- *
|
|
308
|
+
* (C) Recipient and project re-validation.
|
|
309
|
+
* -----------------------------------------------------------------------
|
|
310
|
+
*/
|
|
311
|
+
|
|
312
|
+
describe("re-validation at send time", () => {
|
|
313
|
+
test("an address that is no longer verified is Skipped, not mailed", async () => {
|
|
314
|
+
seedDue({ minutesAgo: 12 });
|
|
315
|
+
|
|
316
|
+
// Unverified between enqueue and flush.
|
|
317
|
+
harness.userEmails[0]!["isVerified"] = false;
|
|
318
|
+
|
|
319
|
+
const stats: RollupSweepStats = await EmailRollupFlushRunner.runSweep({
|
|
320
|
+
now: NOW,
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
expect(harness.sendAttempts).toHaveLength(0);
|
|
324
|
+
expect(stats.skipped).toBe(1);
|
|
325
|
+
expect(harness.batches[0]!.status).toBe(RollupBatchStatus.Skipped);
|
|
326
|
+
expect(harness.batches[0]!.statusMessage).toContain("verified");
|
|
327
|
+
/*
|
|
328
|
+
* The items stay stamped. The claim is what marks this epoch used, and
|
|
329
|
+
* un-stamping would just re-offer the same suppressed rollup next tick.
|
|
330
|
+
*/
|
|
331
|
+
expect(pendingItems(harness)).toHaveLength(0);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
test("an address removed from the user entirely is Skipped", async () => {
|
|
335
|
+
seedDue({ minutesAgo: 12 });
|
|
336
|
+
|
|
337
|
+
harness.userEmails = [];
|
|
338
|
+
|
|
339
|
+
await EmailRollupFlushRunner.runSweep({ now: NOW });
|
|
340
|
+
|
|
341
|
+
expect(harness.sendAttempts).toHaveLength(0);
|
|
342
|
+
expect(harness.batches[0]!.status).toBe(RollupBatchStatus.Skipped);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test("a project deleted while items were queued is Skipped", async () => {
|
|
346
|
+
seedDue({ minutesAgo: 12 });
|
|
347
|
+
|
|
348
|
+
harness.projects = [];
|
|
349
|
+
|
|
350
|
+
const stats: RollupSweepStats = await EmailRollupFlushRunner.runSweep({
|
|
351
|
+
now: NOW,
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
expect(harness.sendAttempts).toHaveLength(0);
|
|
355
|
+
expect(stats.skipped).toBe(1);
|
|
356
|
+
expect(harness.batches[0]!.status).toBe(RollupBatchStatus.Skipped);
|
|
357
|
+
expect(harness.batches[0]!.statusMessage).toBe("project not found");
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
/*
|
|
362
|
+
* ----------------------------------------------------------------------- *
|
|
363
|
+
* (D) Every loop is bounded.
|
|
364
|
+
* -----------------------------------------------------------------------
|
|
365
|
+
*/
|
|
366
|
+
|
|
367
|
+
describe("bounds", () => {
|
|
368
|
+
test("discovery is a bounded findBy, never findAllBy", async () => {
|
|
369
|
+
const findAllBy: jest.SpyInstance = jest
|
|
370
|
+
.spyOn(UserNotificationEmailRollupItemService, "findAllBy")
|
|
371
|
+
.mockResolvedValue([] as never);
|
|
372
|
+
|
|
373
|
+
seedDue({ minutesAgo: 12 });
|
|
374
|
+
|
|
375
|
+
await EmailRollupFlushRunner.runSweep({ now: NOW });
|
|
376
|
+
|
|
377
|
+
/*
|
|
378
|
+
* findAllBy pages to exhaustion. Using it here is exactly how a backlog
|
|
379
|
+
* spike blows the job timeout mid-loop, after some rows are stamped and
|
|
380
|
+
* before they are sent.
|
|
381
|
+
*/
|
|
382
|
+
expect(findAllBy).not.toHaveBeenCalled();
|
|
383
|
+
expect(harness.itemFindByArgs[0]!["limit"]).toBe(
|
|
384
|
+
MAX_ITEMS_SCANNED_PER_TICK,
|
|
385
|
+
);
|
|
386
|
+
expect(harness.itemFindByArgs[0]!["skip"]).toBe(0);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
test("the claim takes at most MAX_ITEMS_PER_ROLLUP and the remainder waits for the next epoch", async () => {
|
|
390
|
+
const total: number = MAX_ITEMS_PER_ROLLUP + 25;
|
|
391
|
+
|
|
392
|
+
for (let index: number = 0; index < total; index++) {
|
|
393
|
+
seedDue({ minutesAgo: 12, subject: `Incident ${index}` });
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
await EmailRollupFlushRunner.runSweep({ now: NOW });
|
|
397
|
+
|
|
398
|
+
expect(harness.batches[0]!.itemCount).toBe(MAX_ITEMS_PER_ROLLUP);
|
|
399
|
+
expect(pendingItems(harness)).toHaveLength(25);
|
|
400
|
+
|
|
401
|
+
// A later epoch picks up exactly the remainder.
|
|
402
|
+
await EmailRollupFlushRunner.runSweep({
|
|
403
|
+
now: OneUptimeDate.addRemoveMinutes(NOW, 6),
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
expect(harness.sent).toHaveLength(2);
|
|
407
|
+
expect(harness.batches[1]!.itemCount).toBe(25);
|
|
408
|
+
expect(pendingItems(harness)).toHaveLength(0);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
test("the claim ignores the cutoff, so a ten-second-old item rides along with an older one", async () => {
|
|
412
|
+
seedDue({ minutesAgo: 12, subject: "the item that made it due" });
|
|
413
|
+
|
|
414
|
+
seedItem(harness, {
|
|
415
|
+
projectId: PROJECT_ID,
|
|
416
|
+
userId: USER_ID,
|
|
417
|
+
toEmail: TO_EMAIL,
|
|
418
|
+
createdAt: new Date(NOW.getTime() - 10 * 1000),
|
|
419
|
+
subject: "arrived ten seconds ago",
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
await EmailRollupFlushRunner.runSweep({ now: NOW });
|
|
423
|
+
|
|
424
|
+
/*
|
|
425
|
+
* Coalescing more can only ever shorten an item's latency, never lengthen
|
|
426
|
+
* it, so the claim deliberately takes everything pending for the bucket.
|
|
427
|
+
*/
|
|
428
|
+
expect(harness.batches[0]!.itemCount).toBe(2);
|
|
429
|
+
expect(pendingItems(harness)).toHaveLength(0);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
test("buckets are served oldest-first, so a backlog drains fairly", async () => {
|
|
433
|
+
const middle: ObjectID = new ObjectID("user-middle");
|
|
434
|
+
const newest: ObjectID = new ObjectID("user-newest");
|
|
435
|
+
const oldest: ObjectID = new ObjectID("user-oldest");
|
|
436
|
+
|
|
437
|
+
const middleEmail: Email = new Email("middle@example.com");
|
|
438
|
+
const newestEmail: Email = new Email("newest@example.com");
|
|
439
|
+
const oldestEmail: Email = new Email("oldest@example.com");
|
|
440
|
+
|
|
441
|
+
for (const pair of [
|
|
442
|
+
{ userId: middle, email: middleEmail },
|
|
443
|
+
{ userId: newest, email: newestEmail },
|
|
444
|
+
{ userId: oldest, email: oldestEmail },
|
|
445
|
+
]) {
|
|
446
|
+
seedVerifiedEmail(harness, {
|
|
447
|
+
projectId: PROJECT_ID,
|
|
448
|
+
userId: pair.userId,
|
|
449
|
+
email: pair.email,
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Inserted newest-first on purpose: the order must come from createdAt.
|
|
454
|
+
seedDue({ minutesAgo: 6, userId: newest, toEmail: newestEmail });
|
|
455
|
+
seedDue({ minutesAgo: 30, userId: oldest, toEmail: oldestEmail });
|
|
456
|
+
seedDue({ minutesAgo: 12, userId: middle, toEmail: middleEmail });
|
|
457
|
+
|
|
458
|
+
await EmailRollupFlushRunner.runSweep({ now: NOW });
|
|
459
|
+
|
|
460
|
+
expect(
|
|
461
|
+
harness.sent.map((mail: SentRollupMail): string => {
|
|
462
|
+
return mail.toEmail;
|
|
463
|
+
}),
|
|
464
|
+
).toEqual([
|
|
465
|
+
oldestEmail.toString(),
|
|
466
|
+
middleEmail.toString(),
|
|
467
|
+
newestEmail.toString(),
|
|
468
|
+
]);
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
test("at most MAX_BUCKETS_PER_TICK buckets are flushed in one tick", async () => {
|
|
472
|
+
const total: number = MAX_BUCKETS_PER_TICK + 5;
|
|
473
|
+
|
|
474
|
+
for (let index: number = 0; index < total; index++) {
|
|
475
|
+
const userId: ObjectID = new ObjectID(`bulk-user-${index}`);
|
|
476
|
+
const email: Email = new Email(`bulk-${index}@example.com`);
|
|
477
|
+
|
|
478
|
+
seedVerifiedEmail(harness, {
|
|
479
|
+
projectId: PROJECT_ID,
|
|
480
|
+
userId: userId,
|
|
481
|
+
email: email,
|
|
482
|
+
});
|
|
483
|
+
// Strictly decreasing age, so "oldest first" picks indices 0..49.
|
|
484
|
+
seedDue({
|
|
485
|
+
minutesAgo: 60 - index,
|
|
486
|
+
userId: userId,
|
|
487
|
+
toEmail: email,
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const stats: RollupSweepStats = await EmailRollupFlushRunner.runSweep({
|
|
492
|
+
now: NOW,
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
expect(stats.bucketsDue).toBe(total);
|
|
496
|
+
expect(stats.bucketsProcessed).toBe(MAX_BUCKETS_PER_TICK);
|
|
497
|
+
expect(harness.sent).toHaveLength(MAX_BUCKETS_PER_TICK);
|
|
498
|
+
expect(pendingItems(harness)).toHaveLength(5);
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
test("the wall-clock budget stops the loop mid-tick and warns", async () => {
|
|
502
|
+
for (let index: number = 0; index < 3; index++) {
|
|
503
|
+
const userId: ObjectID = new ObjectID(`slow-user-${index}`);
|
|
504
|
+
const email: Email = new Email(`slow-${index}@example.com`);
|
|
505
|
+
|
|
506
|
+
seedVerifiedEmail(harness, {
|
|
507
|
+
projectId: PROJECT_ID,
|
|
508
|
+
userId: userId,
|
|
509
|
+
email: email,
|
|
510
|
+
});
|
|
511
|
+
seedDue({
|
|
512
|
+
minutesAgo: 30 - index,
|
|
513
|
+
userId: userId,
|
|
514
|
+
toEmail: email,
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// The first send burns the entire budget.
|
|
519
|
+
harness.failSend = (): null => {
|
|
520
|
+
harness.clockOffsetMs =
|
|
521
|
+
harness.clockOffsetMs + ROLLUP_SWEEP_BUDGET_MS + 1000;
|
|
522
|
+
return null;
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
const stats: RollupSweepStats = await EmailRollupFlushRunner.runSweep({
|
|
526
|
+
now: NOW,
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
expect(stats.budgetExhausted).toBe(true);
|
|
530
|
+
expect(stats.bucketsProcessed).toBe(1);
|
|
531
|
+
expect(harness.sent).toHaveLength(1);
|
|
532
|
+
expect(pendingItems(harness)).toHaveLength(2);
|
|
533
|
+
expect(harness.warnings.join(" ")).toContain("budget exhausted");
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
test("a claim that fails for a reason that is not a duplicate does not stop the others", async () => {
|
|
537
|
+
const secondUser: ObjectID = new ObjectID("second-user");
|
|
538
|
+
const secondEmail: Email = new Email("second@example.com");
|
|
539
|
+
|
|
540
|
+
seedVerifiedEmail(harness, {
|
|
541
|
+
projectId: PROJECT_ID,
|
|
542
|
+
userId: secondUser,
|
|
543
|
+
email: secondEmail,
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
seedDue({ minutesAgo: 30 });
|
|
547
|
+
seedDue({ minutesAgo: 12, userId: secondUser, toEmail: secondEmail });
|
|
548
|
+
|
|
549
|
+
const boom: Error = new Error("claim insert deadlocked");
|
|
550
|
+
let claims: number = 0;
|
|
551
|
+
|
|
552
|
+
/*
|
|
553
|
+
* BEFORE the claim, so nothing has been stamped: this is the one class
|
|
554
|
+
* of per-bucket failure that legitimately escapes to the sweep's own
|
|
555
|
+
* handler, and the point here is that it costs exactly one bucket.
|
|
556
|
+
*/
|
|
557
|
+
harness.failBatchCreate = (): Error | null => {
|
|
558
|
+
claims = claims + 1;
|
|
559
|
+
return claims === 1 ? boom : null;
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
const stats: RollupSweepStats = await EmailRollupFlushRunner.runSweep({
|
|
563
|
+
now: NOW,
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
expect(stats.errors).toBe(1);
|
|
567
|
+
expect(harness.errors).toEqual([boom]);
|
|
568
|
+
expect(harness.sent).toHaveLength(1);
|
|
569
|
+
expect(harness.sent[0]!.toEmail).toBe(secondEmail.toString());
|
|
570
|
+
// Untouched, so the next tick can try again.
|
|
571
|
+
expect(pendingItems(harness)).toHaveLength(1);
|
|
572
|
+
});
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
/*
|
|
576
|
+
* ----------------------------------------------------------------------- *
|
|
577
|
+
* (E) The correlation ids.
|
|
578
|
+
* -----------------------------------------------------------------------
|
|
579
|
+
*/
|
|
580
|
+
|
|
581
|
+
describe("the send options", () => {
|
|
582
|
+
test("carry projectId and userId and nothing else", async () => {
|
|
583
|
+
seedDue({ minutesAgo: 12, rollupCategory: RollupCategory.Incidents });
|
|
584
|
+
seedDue({ minutesAgo: 11, rollupCategory: RollupCategory.Monitors });
|
|
585
|
+
|
|
586
|
+
await EmailRollupFlushRunner.runSweep({ now: NOW });
|
|
587
|
+
|
|
588
|
+
const options: Record<string, unknown> = harness.sent[0]!.options ?? {};
|
|
589
|
+
|
|
590
|
+
/*
|
|
591
|
+
* projectId is required: MailService only writes an EmailLog row when it
|
|
592
|
+
* is present, so without it every rollup would be invisible in
|
|
593
|
+
* Notification Logs. Everything else is absent because a rollup spans
|
|
594
|
+
* many resources and no single-valued id would be true of it.
|
|
595
|
+
*/
|
|
596
|
+
expect(Object.keys(options).sort()).toEqual(["projectId", "userId"]);
|
|
597
|
+
expect(String(options["projectId"])).toBe(PROJECT_ID.toString());
|
|
598
|
+
expect(String(options["userId"])).toBe(USER_ID.toString());
|
|
599
|
+
});
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
/*
|
|
603
|
+
* ----------------------------------------------------------------------- *
|
|
604
|
+
* (F) What discovery considers due.
|
|
605
|
+
*
|
|
606
|
+
* The two predicates in the discovery query ARE the schedule and the
|
|
607
|
+
* exactly-once promise, and neither is visible in any assertion about what
|
|
608
|
+
* an email contains. Delete `createdAt <= cutoff` and every deferred owner
|
|
609
|
+
* email goes out on the next minute's tick instead of after five, which
|
|
610
|
+
* defeats the coalescing the feature exists for - a burst of five would
|
|
611
|
+
* become one immediate email plus one a minute later, then another, then
|
|
612
|
+
* another. Delete `sentAt IS NULL` and every address that has EVER been
|
|
613
|
+
* rolled up looks permanently due, burning a claim every epoch forever.
|
|
614
|
+
* -----------------------------------------------------------------------
|
|
615
|
+
*/
|
|
616
|
+
|
|
617
|
+
describe("what discovery considers due", () => {
|
|
618
|
+
test("a bucket whose only pending item is seconds old is left alone this tick", async () => {
|
|
619
|
+
seedItem(harness, {
|
|
620
|
+
projectId: PROJECT_ID,
|
|
621
|
+
userId: USER_ID,
|
|
622
|
+
toEmail: TO_EMAIL,
|
|
623
|
+
createdAt: new Date(NOW.getTime() - 30 * 1000),
|
|
624
|
+
subject: "arrived thirty seconds ago",
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
const stats: RollupSweepStats = await EmailRollupFlushRunner.runSweep({
|
|
628
|
+
now: NOW,
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
expect(stats.itemsScanned).toBe(0);
|
|
632
|
+
expect(stats.bucketsDue).toBe(0);
|
|
633
|
+
|
|
634
|
+
/*
|
|
635
|
+
* Not merely "no email was sent": NO CLAIM MAY BE MINTED EITHER. A claim
|
|
636
|
+
* consumes this address's epoch under the batch table's unique index, so
|
|
637
|
+
* a bucket picked up too early would also lock out the legitimate flush
|
|
638
|
+
* that comes due later inside the same five minutes.
|
|
639
|
+
*/
|
|
640
|
+
expect(harness.batchCreateCalls).toBe(0);
|
|
641
|
+
expect(harness.batches).toHaveLength(0);
|
|
642
|
+
expect(harness.sendAttempts).toHaveLength(0);
|
|
643
|
+
expect(pendingItems(harness)).toHaveLength(1);
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
test("the cutoff is inclusive: exactly FLUSH_AFTER_MINUTES old is due, one second short is not", async () => {
|
|
647
|
+
const cutoff: Date = OneUptimeDate.addRemoveMinutes(
|
|
648
|
+
NOW,
|
|
649
|
+
FLUSH_AFTER_MINUTES * -1,
|
|
650
|
+
);
|
|
651
|
+
|
|
652
|
+
const dueUser: ObjectID = new ObjectID("cutoff-due-user");
|
|
653
|
+
const dueEmail: Email = new Email("due@example.com");
|
|
654
|
+
const youngUser: ObjectID = new ObjectID("cutoff-young-user");
|
|
655
|
+
const youngEmail: Email = new Email("young@example.com");
|
|
656
|
+
|
|
657
|
+
for (const pair of [
|
|
658
|
+
{ userId: dueUser, email: dueEmail },
|
|
659
|
+
{ userId: youngUser, email: youngEmail },
|
|
660
|
+
]) {
|
|
661
|
+
seedVerifiedEmail(harness, {
|
|
662
|
+
projectId: PROJECT_ID,
|
|
663
|
+
userId: pair.userId,
|
|
664
|
+
email: pair.email,
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
seedItem(harness, {
|
|
669
|
+
projectId: PROJECT_ID,
|
|
670
|
+
userId: dueUser,
|
|
671
|
+
toEmail: dueEmail,
|
|
672
|
+
createdAt: cutoff,
|
|
673
|
+
subject: "exactly five minutes old",
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
seedItem(harness, {
|
|
677
|
+
projectId: PROJECT_ID,
|
|
678
|
+
userId: youngUser,
|
|
679
|
+
toEmail: youngEmail,
|
|
680
|
+
createdAt: new Date(cutoff.getTime() + 1000),
|
|
681
|
+
subject: "one second short of due",
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
const stats: RollupSweepStats = await EmailRollupFlushRunner.runSweep({
|
|
685
|
+
now: NOW,
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
// The cutoff is now minus FLUSH_AFTER_MINUTES, not plus, and not zero.
|
|
689
|
+
expect(stats.cutoff.getTime()).toBe(cutoff.getTime());
|
|
690
|
+
expect(stats.itemsScanned).toBe(1);
|
|
691
|
+
expect(stats.bucketsDue).toBe(1);
|
|
692
|
+
expect(
|
|
693
|
+
harness.sent.map((mail: SentRollupMail): string => {
|
|
694
|
+
return mail.toEmail;
|
|
695
|
+
}),
|
|
696
|
+
).toEqual([dueEmail.toString()]);
|
|
697
|
+
expect(
|
|
698
|
+
pendingItems(harness).map((row: FakeItemRow): string => {
|
|
699
|
+
return row.subject;
|
|
700
|
+
}),
|
|
701
|
+
).toEqual(["one second short of due"]);
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
test("an already-stamped row does not make its bucket look due", async () => {
|
|
705
|
+
const earlierBatchId: ObjectID = new ObjectID("earlier-batch");
|
|
706
|
+
|
|
707
|
+
/*
|
|
708
|
+
* Old enough to be due on age alone; already carried by a rollup that
|
|
709
|
+
* went out fifteen minutes ago.
|
|
710
|
+
*/
|
|
711
|
+
seedItem(harness, {
|
|
712
|
+
projectId: PROJECT_ID,
|
|
713
|
+
userId: USER_ID,
|
|
714
|
+
toEmail: TO_EMAIL,
|
|
715
|
+
createdAt: OneUptimeDate.addRemoveMinutes(NOW, -20),
|
|
716
|
+
subject: "already sent in an earlier rollup",
|
|
717
|
+
sentAt: OneUptimeDate.addRemoveMinutes(NOW, -15),
|
|
718
|
+
rollupBatchId: earlierBatchId,
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
const stats: RollupSweepStats = await EmailRollupFlushRunner.runSweep({
|
|
722
|
+
now: NOW,
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
expect(stats.itemsScanned).toBe(0);
|
|
726
|
+
expect(stats.bucketsDue).toBe(0);
|
|
727
|
+
expect(harness.batchCreateCalls).toBe(0);
|
|
728
|
+
expect(harness.batches).toHaveLength(0);
|
|
729
|
+
expect(harness.sendAttempts).toHaveLength(0);
|
|
730
|
+
// Still pointing at the batch that actually carried it.
|
|
731
|
+
expect(harness.items[0]!.rollupBatchId).toBe(earlierBatchId);
|
|
732
|
+
});
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
/*
|
|
736
|
+
* ----------------------------------------------------------------------- *
|
|
737
|
+
* (G) The drain is FIFO.
|
|
738
|
+
* -----------------------------------------------------------------------
|
|
739
|
+
*/
|
|
740
|
+
|
|
741
|
+
describe("an over-full bucket", () => {
|
|
742
|
+
test("drains oldest-first: the oldest MAX_ITEMS_PER_ROLLUP are sent and the newest are the ones left waiting", async () => {
|
|
743
|
+
const overflow: number = 3;
|
|
744
|
+
const total: number = MAX_ITEMS_PER_ROLLUP + overflow;
|
|
745
|
+
|
|
746
|
+
/*
|
|
747
|
+
* A distinct deep link per item so the renderer folds nothing: every
|
|
748
|
+
* item is its own row, which makes the rendered table a direct readout
|
|
749
|
+
* of which items this batch claimed.
|
|
750
|
+
*/
|
|
751
|
+
for (let index: number = 0; index < total; index++) {
|
|
752
|
+
seedDue({
|
|
753
|
+
minutesAgo: total - index,
|
|
754
|
+
subject: `Incident ${index}`,
|
|
755
|
+
viewLink: `https://oneuptime.example.com/incident/${index}`,
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
await EmailRollupFlushRunner.runSweep({ now: NOW });
|
|
760
|
+
|
|
761
|
+
expect(harness.sent).toHaveLength(1);
|
|
762
|
+
expect(harness.batches[0]!.itemCount).toBe(MAX_ITEMS_PER_ROLLUP);
|
|
763
|
+
expect(
|
|
764
|
+
harness.items.filter((row: FakeItemRow): boolean => {
|
|
765
|
+
return row.sentAt !== null;
|
|
766
|
+
}),
|
|
767
|
+
).toHaveLength(MAX_ITEMS_PER_ROLLUP);
|
|
768
|
+
|
|
769
|
+
const expectedPending: Array<string> = [];
|
|
770
|
+
|
|
771
|
+
for (let index: number = MAX_ITEMS_PER_ROLLUP; index < total; index++) {
|
|
772
|
+
expectedPending.push(`Incident ${index}`);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/*
|
|
776
|
+
* THE WHOLE POINT. updateBy resolves the rows it writes with an internal
|
|
777
|
+
* find that takes no sort from the caller and therefore defaults to
|
|
778
|
+
* createdAt DESCENDING, so a bare `updateBy(..., limit: 500)` here would
|
|
779
|
+
* stamp the NEWEST five hundred and leave THESE three - the oldest - to
|
|
780
|
+
* wait behind every future arrival, until retention hard-deletes them
|
|
781
|
+
* unsent. The flush selects the oldest ids explicitly to stop that.
|
|
782
|
+
*/
|
|
783
|
+
expect(
|
|
784
|
+
pendingItems(harness).map((row: FakeItemRow): string => {
|
|
785
|
+
return row.subject;
|
|
786
|
+
}),
|
|
787
|
+
).toEqual(expectedPending);
|
|
788
|
+
|
|
789
|
+
const rows: Array<Record<string, unknown>> = harness.sent[0]!.vars[
|
|
790
|
+
"rows"
|
|
791
|
+
] as unknown as Array<Record<string, unknown>>;
|
|
792
|
+
const titles: Array<string> = rows.map(
|
|
793
|
+
(row: Record<string, unknown>): string => {
|
|
794
|
+
return String(row["title"]);
|
|
795
|
+
},
|
|
796
|
+
);
|
|
797
|
+
|
|
798
|
+
/*
|
|
799
|
+
* The renderer lists the newest rows first, so the email's lead row is
|
|
800
|
+
* the newest item INSIDE the claimed window - and nothing from outside
|
|
801
|
+
* it appears anywhere in the message.
|
|
802
|
+
*/
|
|
803
|
+
expect(titles[0]).toBe(`Incident ${MAX_ITEMS_PER_ROLLUP - 1}`);
|
|
804
|
+
|
|
805
|
+
for (const subject of expectedPending) {
|
|
806
|
+
expect(titles).not.toContain(subject);
|
|
807
|
+
}
|
|
808
|
+
});
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
/*
|
|
812
|
+
* ----------------------------------------------------------------------- *
|
|
813
|
+
* (H) A throw between the stamp and the send.
|
|
814
|
+
*
|
|
815
|
+
* Past the stamp, up to MAX_ITEMS_PER_ROLLUP notifications exist ONLY as an
|
|
816
|
+
* obligation to send this one email. Every one of the three collaborators
|
|
817
|
+
* below sits in that window. If one of them throws and the exception merely
|
|
818
|
+
* escapes to the sweep's per-bucket handler, those notifications are gone:
|
|
819
|
+
* stamped, so nothing will ever pick them up again, with a batch row still
|
|
820
|
+
* saying Claimed and nothing anywhere saying what happened.
|
|
821
|
+
* -----------------------------------------------------------------------
|
|
822
|
+
*/
|
|
823
|
+
|
|
824
|
+
describe("a throw between the stamp and the send", () => {
|
|
825
|
+
function expectTheBatchRecordedTheFailure(data: {
|
|
826
|
+
stats: RollupSweepStats;
|
|
827
|
+
reason: string;
|
|
828
|
+
}): void {
|
|
829
|
+
// Recorded and counted, not rethrown.
|
|
830
|
+
expect(data.stats.failed).toBe(1);
|
|
831
|
+
expect(data.stats.errors).toBe(0);
|
|
832
|
+
expect(data.stats.bucketsProcessed).toBe(1);
|
|
833
|
+
expect(harness.sendAttempts).toHaveLength(0);
|
|
834
|
+
|
|
835
|
+
const batch: FakeBatchRow = harness.batches[0]!;
|
|
836
|
+
|
|
837
|
+
expect(batch.status).toBe(RollupBatchStatus.Failed);
|
|
838
|
+
expect(batch.sentAt).toBeNull();
|
|
839
|
+
expect(batch.statusMessage).toBe(data.reason);
|
|
840
|
+
expect((batch.statusMessage ?? "").length).toBeGreaterThan(0);
|
|
841
|
+
|
|
842
|
+
/*
|
|
843
|
+
* The rows stay stamped into THIS batch. Un-stamping them looks kinder
|
|
844
|
+
* and is the trap: the same failure recurs every epoch and the pile
|
|
845
|
+
* grows, which is the storm the whole feature exists to stop. The loss
|
|
846
|
+
* is bounded, attributed to a batch id, and explained.
|
|
847
|
+
*/
|
|
848
|
+
expect(pendingItems(harness)).toHaveLength(0);
|
|
849
|
+
expect(
|
|
850
|
+
harness.items.every((row: FakeItemRow): boolean => {
|
|
851
|
+
return row.rollupBatchId?.toString() === batch.id.toString();
|
|
852
|
+
}),
|
|
853
|
+
).toBe(true);
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
test("the recipient re-validation throwing is recorded on the batch", async () => {
|
|
857
|
+
seedDue({ minutesAgo: 12 });
|
|
858
|
+
seedDue({ minutesAgo: 11 });
|
|
859
|
+
|
|
860
|
+
jest.spyOn(UserEmailService, "findBy").mockImplementation((): never => {
|
|
861
|
+
throw new Error("user email read timed out");
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
const stats: RollupSweepStats = await EmailRollupFlushRunner.runSweep({
|
|
865
|
+
now: NOW,
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
expectTheBatchRecordedTheFailure({
|
|
869
|
+
stats: stats,
|
|
870
|
+
reason: "user email read timed out",
|
|
871
|
+
});
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
test("the project lookup throwing is recorded on the batch", async () => {
|
|
875
|
+
seedDue({ minutesAgo: 12 });
|
|
876
|
+
seedDue({ minutesAgo: 11 });
|
|
877
|
+
|
|
878
|
+
jest
|
|
879
|
+
.spyOn(ProjectService, "findOneById")
|
|
880
|
+
.mockImplementation((): never => {
|
|
881
|
+
throw new Error("project read timed out");
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
const stats: RollupSweepStats = await EmailRollupFlushRunner.runSweep({
|
|
885
|
+
now: NOW,
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
expectTheBatchRecordedTheFailure({
|
|
889
|
+
stats: stats,
|
|
890
|
+
reason: "project read timed out",
|
|
891
|
+
});
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
test("the renderer throwing is recorded on the batch", async () => {
|
|
895
|
+
seedDue({ minutesAgo: 12 });
|
|
896
|
+
seedDue({ minutesAgo: 11 });
|
|
897
|
+
|
|
898
|
+
/*
|
|
899
|
+
* A renderer that throws on some row shape nobody anticipated is the
|
|
900
|
+
* most likely of the three to be introduced by a later edit, and it is
|
|
901
|
+
* the one furthest from anything the flush itself owns.
|
|
902
|
+
*/
|
|
903
|
+
jest
|
|
904
|
+
.spyOn(EmailRollupRenderer, "buildRollupEmail")
|
|
905
|
+
.mockImplementation((): never => {
|
|
906
|
+
throw new Error("cannot read properties of undefined");
|
|
907
|
+
});
|
|
908
|
+
|
|
909
|
+
const stats: RollupSweepStats = await EmailRollupFlushRunner.runSweep({
|
|
910
|
+
now: NOW,
|
|
911
|
+
});
|
|
912
|
+
|
|
913
|
+
expectTheBatchRecordedTheFailure({
|
|
914
|
+
stats: stats,
|
|
915
|
+
reason: "cannot read properties of undefined",
|
|
916
|
+
});
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
test("the other buckets on the same tick still get their rollup", async () => {
|
|
920
|
+
const secondUser: ObjectID = new ObjectID("survivor-user");
|
|
921
|
+
const secondEmail: Email = new Email("survivor@example.com");
|
|
922
|
+
|
|
923
|
+
seedVerifiedEmail(harness, {
|
|
924
|
+
projectId: PROJECT_ID,
|
|
925
|
+
userId: secondUser,
|
|
926
|
+
email: secondEmail,
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
// The older bucket is served first, so it is the one that fails.
|
|
930
|
+
seedDue({ minutesAgo: 30 });
|
|
931
|
+
seedDue({ minutesAgo: 12, userId: secondUser, toEmail: secondEmail });
|
|
932
|
+
|
|
933
|
+
jest
|
|
934
|
+
.spyOn(ProjectService, "findOneById")
|
|
935
|
+
.mockImplementationOnce((): never => {
|
|
936
|
+
throw new Error("project read timed out");
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
const stats: RollupSweepStats = await EmailRollupFlushRunner.runSweep({
|
|
940
|
+
now: NOW,
|
|
941
|
+
});
|
|
942
|
+
|
|
943
|
+
expect(stats.failed).toBe(1);
|
|
944
|
+
expect(stats.sent).toBe(1);
|
|
945
|
+
expect(stats.errors).toBe(0);
|
|
946
|
+
expect(stats.bucketsProcessed).toBe(2);
|
|
947
|
+
expect(harness.sent).toHaveLength(1);
|
|
948
|
+
expect(harness.sent[0]!.toEmail).toBe(secondEmail.toString());
|
|
949
|
+
expect(batchesOfStatus(harness, RollupBatchStatus.Failed)).toHaveLength(
|
|
950
|
+
1,
|
|
951
|
+
);
|
|
952
|
+
expect(batchesOfStatus(harness, RollupBatchStatus.Sent)).toHaveLength(1);
|
|
953
|
+
});
|
|
954
|
+
|
|
955
|
+
test("the failed bucket's rows are not resurrected by a later tick", async () => {
|
|
956
|
+
seedDue({ minutesAgo: 12 });
|
|
957
|
+
seedDue({ minutesAgo: 11 });
|
|
958
|
+
|
|
959
|
+
jest
|
|
960
|
+
.spyOn(ProjectService, "findOneById")
|
|
961
|
+
.mockImplementationOnce((): never => {
|
|
962
|
+
throw new Error("project read timed out");
|
|
963
|
+
});
|
|
964
|
+
|
|
965
|
+
await EmailRollupFlushRunner.runSweep({ now: NOW });
|
|
966
|
+
|
|
967
|
+
/*
|
|
968
|
+
* The project reads fine again, and the epoch has moved on, so nothing
|
|
969
|
+
* but the stamp is stopping a second rollup of the same notifications.
|
|
970
|
+
*/
|
|
971
|
+
const later: RollupSweepStats = await EmailRollupFlushRunner.runSweep({
|
|
972
|
+
now: OneUptimeDate.addRemoveMinutes(NOW, 6),
|
|
973
|
+
});
|
|
974
|
+
|
|
975
|
+
expect(later.bucketsDue).toBe(0);
|
|
976
|
+
expect(harness.sendAttempts).toHaveLength(0);
|
|
977
|
+
expect(harness.batches).toHaveLength(1);
|
|
978
|
+
expect(harness.batches[0]!.status).toBe(RollupBatchStatus.Failed);
|
|
979
|
+
});
|
|
980
|
+
});
|
|
981
|
+
|
|
982
|
+
/*
|
|
983
|
+
* ----------------------------------------------------------------------- *
|
|
984
|
+
* (I) The send cannot hang.
|
|
985
|
+
*
|
|
986
|
+
* MailService.sendMail is an HTTP POST to the notification service with no
|
|
987
|
+
* timeout of its own, and the sweep holds a Redis mutex whose lock is
|
|
988
|
+
* AUTO-REFRESHED while it is held. One wedged send would therefore stop
|
|
989
|
+
* every replica from flushing any recipient's rollup, indefinitely, while
|
|
990
|
+
* the queue behind it grew and its oldest rows aged out of retention unsent.
|
|
991
|
+
* -----------------------------------------------------------------------
|
|
992
|
+
*/
|
|
993
|
+
|
|
994
|
+
describe("the send timeout", () => {
|
|
995
|
+
function fakeOnlyTheSendTimer(): void {
|
|
996
|
+
/*
|
|
997
|
+
* setTimeout / clearTimeout and nothing else. Date stays real because
|
|
998
|
+
* the harness spies Date.now to drive the sweep's wall-clock budget, and
|
|
999
|
+
* nextTick / queueMicrotask stay real so awaiting the sweep still makes
|
|
1000
|
+
* progress while the clock is frozen.
|
|
1001
|
+
*/
|
|
1002
|
+
jest.useFakeTimers({
|
|
1003
|
+
doNotFake: [
|
|
1004
|
+
"Date",
|
|
1005
|
+
"hrtime",
|
|
1006
|
+
"nextTick",
|
|
1007
|
+
"performance",
|
|
1008
|
+
"queueMicrotask",
|
|
1009
|
+
"requestAnimationFrame",
|
|
1010
|
+
"cancelAnimationFrame",
|
|
1011
|
+
"requestIdleCallback",
|
|
1012
|
+
"cancelIdleCallback",
|
|
1013
|
+
"setImmediate",
|
|
1014
|
+
"clearImmediate",
|
|
1015
|
+
],
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
test("a send that never settles ends the batch Failed and lets the sweep return", async () => {
|
|
1020
|
+
fakeOnlyTheSendTimer();
|
|
1021
|
+
|
|
1022
|
+
seedDue({ minutesAgo: 12 });
|
|
1023
|
+
seedDue({ minutesAgo: 11 });
|
|
1024
|
+
|
|
1025
|
+
const sendMail: jest.SpyInstance = jest
|
|
1026
|
+
.spyOn(MailService, "sendMail")
|
|
1027
|
+
.mockImplementation(((): Promise<void> => {
|
|
1028
|
+
return new Promise<void>((): void => {
|
|
1029
|
+
// Wedged: the socket was accepted and nothing ever came back.
|
|
1030
|
+
});
|
|
1031
|
+
}) as never);
|
|
1032
|
+
|
|
1033
|
+
const sweep: Promise<RollupSweepStats> = EmailRollupFlushRunner.runSweep({
|
|
1034
|
+
now: NOW,
|
|
1035
|
+
});
|
|
1036
|
+
|
|
1037
|
+
// Let the sweep run as far as arming the timeout, and no further.
|
|
1038
|
+
for (
|
|
1039
|
+
let turn: number = 0;
|
|
1040
|
+
turn < 200 && jest.getTimerCount() === 0;
|
|
1041
|
+
turn++
|
|
1042
|
+
) {
|
|
1043
|
+
await Promise.resolve();
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
expect(sendMail).toHaveBeenCalledTimes(1);
|
|
1047
|
+
expect(jest.getTimerCount()).toBe(1);
|
|
1048
|
+
|
|
1049
|
+
jest.advanceTimersByTime(ROLLUP_SEND_TIMEOUT_MS);
|
|
1050
|
+
|
|
1051
|
+
// Returns at all - this is the assertion the whole timeout exists for.
|
|
1052
|
+
const stats: RollupSweepStats = await sweep;
|
|
1053
|
+
|
|
1054
|
+
expect(stats.failed).toBe(1);
|
|
1055
|
+
expect(stats.errors).toBe(0);
|
|
1056
|
+
|
|
1057
|
+
const batch: FakeBatchRow = harness.batches[0]!;
|
|
1058
|
+
|
|
1059
|
+
expect(batch.status).toBe(RollupBatchStatus.Failed);
|
|
1060
|
+
expect(batch.sentAt).toBeNull();
|
|
1061
|
+
expect(batch.itemCount).toBe(2);
|
|
1062
|
+
expect(batch.statusMessage).toContain(
|
|
1063
|
+
`did not complete within ${ROLLUP_SEND_TIMEOUT_MS}ms`,
|
|
1064
|
+
);
|
|
1065
|
+
|
|
1066
|
+
/*
|
|
1067
|
+
* Stamped, so if the wedged request does eventually deliver, the worst
|
|
1068
|
+
* case is one duplicate for one recipient rather than a second rollup
|
|
1069
|
+
* built from the same rows next epoch.
|
|
1070
|
+
*/
|
|
1071
|
+
expect(pendingItems(harness)).toHaveLength(0);
|
|
1072
|
+
|
|
1073
|
+
// Nothing left armed, even though the send itself is still out there.
|
|
1074
|
+
expect(jest.getTimerCount()).toBe(0);
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
test("a normal send leaves no timer armed behind it", async () => {
|
|
1078
|
+
fakeOnlyTheSendTimer();
|
|
1079
|
+
|
|
1080
|
+
seedDue({ minutesAgo: 12 });
|
|
1081
|
+
|
|
1082
|
+
const stats: RollupSweepStats = await EmailRollupFlushRunner.runSweep({
|
|
1083
|
+
now: NOW,
|
|
1084
|
+
});
|
|
1085
|
+
|
|
1086
|
+
expect(stats.sent).toBe(1);
|
|
1087
|
+
|
|
1088
|
+
/*
|
|
1089
|
+
* The timeout is cleared in a finally, on the happy path too. One leaked
|
|
1090
|
+
* sixty-second timer per rollup would hold the worker's event loop open
|
|
1091
|
+
* at shutdown for as long as the last flush was ago.
|
|
1092
|
+
*/
|
|
1093
|
+
expect(jest.getTimerCount()).toBe(0);
|
|
1094
|
+
});
|
|
1095
|
+
});
|
|
1096
|
+
});
|