@oneuptime/common 12.0.32 → 12.0.34
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 +117 -1
- package/Models/AnalyticsModels/RumSessionChunk.ts +13 -0
- package/Models/AnalyticsModels/Span.ts +14 -0
- package/Models/DatabaseModels/AlertCustomField.ts +77 -3
- package/Models/DatabaseModels/ApiKeyPermission.ts +3 -27
- package/Models/DatabaseModels/EnterpriseLicense.ts +56 -0
- package/Models/DatabaseModels/IncidentCustomField.ts +77 -3
- package/Models/DatabaseModels/Index.ts +10 -0
- package/Models/DatabaseModels/InventoryItemCustomField.ts +77 -3
- package/Models/DatabaseModels/Monitor.ts +76 -0
- package/Models/DatabaseModels/MonitorCustomField.ts +77 -3
- 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/OnCallDutyPolicyCustomField.ts +77 -3
- package/Models/DatabaseModels/RumApplication.ts +6 -6
- package/Models/DatabaseModels/ScheduledMaintenanceCustomField.ts +77 -3
- package/Models/DatabaseModels/StatusPageCustomField.ts +77 -3
- package/Models/DatabaseModels/TeamCustomField.ts +77 -3
- package/Models/DatabaseModels/TeamMember.ts +0 -9
- package/Models/DatabaseModels/TeamMemberCustomField.ts +77 -3
- package/Models/DatabaseModels/TeamPermission.ts +3 -24
- package/Models/DatabaseModels/TelemetryException.ts +119 -0
- package/Models/DatabaseModels/TelemetryIngestionKey.ts +260 -0
- package/Models/DatabaseModels/UserNotificationEmailRollupBatch.ts +334 -0
- package/Models/DatabaseModels/UserNotificationEmailRollupItem.ts +370 -0
- package/Models/DatabaseModels/UserNotificationEmailRollupSetting.ts +282 -0
- package/Models/DatabaseModels/UserTelegram.ts +0 -4
- package/Scripts/benchmark-fanin-capacity.js +181 -0
- package/Server/API/EnterpriseLicenseAPI.ts +391 -64
- package/Server/API/LlmProviderAPI.ts +137 -22
- package/Server/API/TelemetryAPI.ts +1284 -164
- package/Server/API/UserNotificationSettingAPI.ts +55 -0
- package/Server/API/UserTelegramAPI.ts +48 -4
- package/Server/EnvironmentConfig.ts +49 -2
- 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/1791300000000-AddTelemetryIngestionKeyType.ts +91 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1791400000000-SessionReplayRecordEverySessionByDefault.ts +67 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1791500000000-WidenCustomFieldDropdownOptions.ts +139 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1791600000000-AddCustomFieldValueMapping.ts +121 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +20 -0
- package/Server/Infrastructure/QueueWorker.ts +59 -23
- package/Server/Infrastructure/Semaphore.ts +2 -0
- 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 +464 -7
- package/Server/Services/AIBillingService.ts +101 -27
- package/Server/Services/AccessTokenService.ts +1 -0
- package/Server/Services/AlertCustomFieldService.ts +98 -0
- package/Server/Services/AlertService.ts +37 -0
- package/Server/Services/ApiKeyPermissionService.ts +461 -29
- package/Server/Services/CustomFieldMappingService.ts +879 -0
- package/Server/Services/EnterpriseLicenseInstanceService.ts +197 -0
- package/Server/Services/EnterpriseLicenseService.ts +55 -0
- package/Server/Services/GlobalConfigService.ts +194 -0
- package/Server/Services/IncidentCustomFieldService.ts +98 -0
- package/Server/Services/IncidentService.ts +35 -0
- package/Server/Services/Index.ts +10 -0
- package/Server/Services/MetricService.ts +194 -0
- package/Server/Services/MonitorService.ts +175 -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 +87 -35
- 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 +127 -48
- package/Server/Services/RoutineEmailSettingsService.ts +73 -0
- package/Server/Services/RumSessionReplayViewService.ts +104 -24
- package/Server/Services/ScheduledMaintenanceCustomFieldService.ts +98 -0
- package/Server/Services/ScheduledMaintenanceService.ts +32 -0
- package/Server/Services/StatusPagePrivateUserService.ts +103 -3
- package/Server/Services/TeamMemberService.ts +78 -0
- package/Server/Services/TeamPermissionService.ts +235 -3
- package/Server/Services/TelemetryIngestionKeyService.ts +722 -19
- 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 +90 -25
- package/Server/Services/UserService.ts +95 -0
- package/Server/Services/UserTelegramService.ts +326 -5
- package/Server/Types/Database/QueryHelper.ts +4 -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/APIKey/AccessPermission.ts +5 -2
- package/Server/Utils/Billing/BillingFailureNoticeThrottle.ts +80 -0
- package/Server/Utils/CustomField/CustomFieldDefinitionMappingHooks.ts +54 -0
- package/Server/Utils/CustomField/CustomFieldMappingRegistry.ts +415 -0
- package/Server/Utils/CustomField/CustomFieldMappingValidator.ts +335 -0
- package/Server/Utils/DataSource/EgressGuard.ts +177 -8
- package/Server/Utils/DataSource/HttpFetch.ts +9 -2
- package/Server/Utils/EmailRollup/EmailRollupConstants.ts +131 -0
- package/Server/Utils/EmailRollup/EmailRollupFlushRunner.ts +925 -0
- package/Server/Utils/EmailRollup/EmailRollupRenderer.ts +781 -0
- package/Server/Utils/EmailRollup/EmailRollupWriter.ts +368 -0
- package/Server/Utils/FrontendEnvironment.ts +40 -0
- package/Server/Utils/LLM/LLMService.ts +78 -0
- package/Server/Utils/LogRedaction.ts +28 -0
- package/Server/Utils/Logger.ts +90 -1
- package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +338 -77
- package/Server/Utils/Monitor/Criteria/DatabaseMonitorCriteria.ts +190 -0
- package/Server/Utils/Monitor/Criteria/EvaluateOverTime.ts +9 -2
- package/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.ts +30 -4
- package/Server/Utils/Monitor/Criteria/SnmpMonitorCriteria.ts +61 -1
- package/Server/Utils/Monitor/MonitorAlert.ts +29 -6
- package/Server/Utils/Monitor/MonitorCriteriaDataExtractor.ts +14 -0
- package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +159 -15
- package/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.ts +255 -4
- package/Server/Utils/Monitor/MonitorIncident.ts +22 -6
- package/Server/Utils/Monitor/MonitorMetricUtil.ts +41 -0
- package/Server/Utils/Monitor/MonitorTemplateUtil.ts +72 -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/Monitor/SeriesContextEnricher.ts +304 -0
- 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/SSRFProtection.ts +217 -10
- package/Server/Utils/SessionReplay/SessionReplayGateCache.ts +239 -138
- package/Server/Utils/SessionReplay/SessionReplayHealthCounters.ts +305 -0
- package/Server/Utils/SessionReplay/SessionReplayReadService.ts +1430 -289
- package/Server/Utils/StartServer.ts +53 -36
- package/Server/Utils/TelegramVerificationToken.ts +185 -0
- package/Server/Utils/Telemetry/CaptureSpan.ts +32 -8
- package/Server/Utils/Telemetry/ErrorClassResolver.ts +120 -0
- package/Server/Utils/Telemetry/PinServiceName.ts +197 -0
- package/Server/Utils/Telemetry/SpanUtil.ts +33 -0
- package/Server/Utils/Telemetry/TelemetryFanInWriter.ts +38 -16
- package/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.ts +92 -0
- package/Server/Utils/Telemetry/TelemetryIngestionKeyRateLimiter.ts +217 -0
- package/Server/Utils/Telemetry.ts +198 -51
- package/Server/Utils/VM/VMAPI.ts +1 -0
- package/Server/Utils/VM/VMRunner.ts +975 -95
- package/Server/Views/Partials/SensitiveUrlToken.ejs +140 -0
- package/Tests/App/AdminDashboard/AdminHeaderSmallScreens.test.tsx +202 -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/DashboardHeaderSmallScreens.test.tsx +404 -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 +486 -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/NotificationEmailPreferences.test.tsx +399 -0
- 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/StatusPageLastUpdated.test.tsx +263 -0
- package/Tests/App/StatusPage/StatusPageLoginCodeBootstrap.test.ts +9 -4
- package/Tests/App/StatusPage/StatusPageOverviewLiveAndSearch.test.tsx +753 -0
- package/Tests/App/StatusPage/StatusPageResourceSearchBox.test.tsx +224 -0
- package/Tests/Models/AnalyticsModels/RumSessionReplayColumns.test.ts +262 -0
- package/Tests/Models/CustomFieldMappingColumns.test.ts +166 -0
- package/Tests/Models/DatabaseModels/SessionReplayModels.test.ts +20 -6
- package/Tests/Models/NetworkSiteHierarchy.test.ts +41 -4
- package/Tests/ResponsiveVisibility.test.ts +115 -0
- package/Tests/ResponsiveVisibility.ts +183 -0
- package/Tests/Server/API/EnterpriseLicenseReportUserCount.test.ts +951 -7
- package/Tests/Server/API/LlmProviderConnectionTest.test.ts +522 -0
- package/Tests/Server/API/SessionReplayAPI.test.ts +1940 -216
- package/Tests/Server/API/UserNotificationSettingAPI.test.ts +186 -0
- package/Tests/Server/API/UserTelegramAPISecurity.test.ts +231 -0
- package/Tests/Server/EnvironmentConfigFrontendSecurity.test.ts +234 -0
- package/Tests/Server/Infrastructure/Postgres/CustomFieldValueMappingMigration.test.ts +202 -0
- package/Tests/Server/Infrastructure/Postgres/InventoryItemArchiveMigration.test.ts +41 -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/SessionReplayRecordEverySessionByDefaultMigration.test.ts +169 -0
- package/Tests/Server/Infrastructure/Postgres/UserNotificationEmailRollupTableMigration.test.ts +535 -0
- package/Tests/Server/Infrastructure/QueueWorkerTimeout.test.ts +210 -0
- package/Tests/Server/Infrastructure/TelemetryExporterDeploymentConfig.test.ts +130 -0
- package/Tests/Server/Middleware/ProjectAuthorizationApiKeyMiddleware.test.ts +4 -1
- package/Tests/Server/Middleware/TelemetryIngestBrowserKey.test.ts +1163 -0
- package/Tests/Server/Middleware/TelemetryIngestTokenLog.test.ts +5 -3
- package/Tests/Server/Services/AIChatPrivacyPin.test.ts +560 -0
- package/Tests/Server/Services/AddTelemetryIngestionKeyTypeMigration.test.ts +555 -0
- package/Tests/Server/Services/ApiKeyPermissionSecurity.test.ts +678 -0
- package/Tests/Server/Services/ApiKeyPermissionService.test.ts +276 -28
- package/Tests/Server/Services/BillingRechargeOwnerEmailGuards.test.ts +783 -0
- package/Tests/Server/Services/CustomFieldDropdownOptionsColumnWidth.test.ts +431 -0
- package/Tests/Server/Services/CustomFieldMappingService.test.ts +850 -0
- package/Tests/Server/Services/DiscoveryScanClaimHookFreeSafety.test.ts +32 -0
- package/Tests/Server/Services/EnterpriseLicenseInstanceServiceDeletionUsage.test.ts +707 -0
- package/Tests/Server/Services/EnterpriseLicenseServiceUsageAggregationLock.test.ts +329 -0
- package/Tests/Server/Services/GlobalConfigService.test.ts +414 -1
- package/Tests/Server/Services/MetricRawEntityKeyPrune.test.ts +592 -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 +273 -5
- 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/RoutineEmailSettingsPostgres.test.ts +329 -0
- package/Tests/Server/Services/RumSessionReplayViewService.test.ts +267 -0
- package/Tests/Server/Services/StatusPagePrivateUserEmailChangePasswordResetExpiry.test.ts +269 -0
- package/Tests/Server/Services/TeamMemberAutoAcceptInvitation.test.ts +9 -0
- package/Tests/Server/Services/TeamMemberInviteRegistrationToken.test.ts +9 -0
- package/Tests/Server/Services/TeamPrivilegeEscalation.test.ts +834 -0
- package/Tests/Server/Services/TelemetryIngestionKeyPolicyResolution.test.ts +814 -0
- package/Tests/Server/Services/TelemetryIngestionKeyValidation.test.ts +998 -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 +997 -0
- package/Tests/Server/Services/UserNotificationSettingWorkspaceChannels.test.ts +110 -1
- package/Tests/Server/Services/UserTelegramVerificationSecurity.test.ts +688 -0
- 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/AI/SRE/Insights/FixRouting.test.ts +497 -0
- package/Tests/Server/Utils/AI/Toolbox/Serializer.test.ts +383 -0
- package/Tests/Server/Utils/APIKey/AccessPermission.test.ts +7 -3
- package/Tests/Server/Utils/AnalyticsDatabase/ClusterConfig.test.ts +315 -0
- package/Tests/Server/Utils/Captcha.test.ts +422 -0
- package/Tests/Server/Utils/CustomField/CustomFieldMappingValidator.test.ts +437 -0
- package/Tests/Server/Utils/DataSource/EgressGuard.test.ts +300 -11
- 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/EmailRollupBurstWindow.test.ts +83 -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/EmailRollupFlushRunnerPreferences.test.ts +362 -0
- package/Tests/Server/Utils/EmailRollup/EmailRollupRenderer.test.ts +1428 -0
- package/Tests/Server/Utils/EmailRollup/EmailRollupTestHarness.ts +828 -0
- package/Tests/Server/Utils/EmailRollup/EmailRollupWriter.test.ts +864 -0
- package/Tests/Server/Utils/FrontendEnvironment.test.ts +190 -0
- package/Tests/Server/Utils/LogRedaction.test.ts +26 -0
- package/Tests/Server/Utils/LoggerCredentialLeak.test.ts +23 -9
- package/Tests/Server/Utils/LoggerFaultDemotion.test.ts +346 -0
- package/Tests/Server/Utils/Monitor/Criteria/CompareCriteriaAggregation.test.ts +584 -0
- package/Tests/Server/Utils/Monitor/Criteria/DatabaseMonitorCriteria.test.ts +633 -0
- package/Tests/Server/Utils/Monitor/Criteria/IncomingRequestHeaderCriteria.test.ts +236 -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/MonitorCriteriaEvaluator.test.ts +437 -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/MonitorTemplateUtilSeriesContext.test.ts +176 -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/Monitor/SeriesContextEnricher.test.ts +356 -0
- 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/SSRFProtectionCloudServiceAddresses.test.ts +595 -0
- package/Tests/Server/Utils/SecurityEvent/ThreatIntel/TaxiiClientUserAgent.test.ts +141 -0
- package/Tests/Server/Utils/SessionReplay/SessionReplayGateCachePolicy.test.ts +198 -1
- package/Tests/Server/Utils/SessionReplay/SessionReplayHealthCounters.test.ts +453 -0
- package/Tests/Server/Utils/SessionReplay/SessionReplayReadServiceQueries.test.ts +1378 -0
- package/Tests/Server/Utils/SessionReplayOriginAllowListRefactor.test.ts +420 -0
- package/Tests/Server/Utils/TelegramVerificationToken.test.ts +253 -0
- package/Tests/Server/Utils/Telemetry/ErrorClassResolver.test.ts +298 -0
- package/Tests/Server/Utils/Telemetry/PinServiceName.test.ts +666 -0
- package/Tests/Server/Utils/Telemetry/TelemetryFanInWriterCapacity.test.ts +458 -0
- package/Tests/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.test.ts +424 -0
- package/Tests/Server/Utils/Telemetry/TelemetryIngestionKeyRateLimiter.test.ts +528 -0
- package/Tests/Server/Utils/Telemetry.test.ts +456 -66
- package/Tests/Server/Utils/TelemetryExporterEnvironment.test.ts +56 -0
- package/Tests/Server/Utils/VM/VMRunnerHostBridgeLatency.test.ts +74 -0
- package/Tests/Server/Utils/VM/VMRunnerPrivateNetworkWiring.test.ts +100 -7
- package/Tests/Server/Utils/VM/VMRunnerSsrf.test.ts +1176 -6
- package/Tests/Types/CustomField/CustomFieldMappingCatalog.test.ts +220 -0
- package/Tests/Types/CustomField/CustomFieldValueMapping.test.ts +348 -0
- package/Tests/Types/JSONFunctions.test.ts +260 -0
- package/Tests/Types/Monitor/CephAlertTemplates.test.ts +422 -53
- package/Tests/Types/Monitor/DatabaseMetricCatalog.test.ts +388 -0
- package/Tests/Types/Monitor/DockerAlertTemplates.test.ts +495 -15
- package/Tests/Types/Monitor/DockerSwarmAlertTemplates.test.ts +191 -45
- package/Tests/Types/Monitor/HostAlertTemplates.test.ts +370 -42
- package/Tests/Types/Monitor/IotAlertTemplates.test.ts +375 -20
- package/Tests/Types/Monitor/KubernetesAlertTemplates.test.ts +636 -42
- package/Tests/Types/Monitor/KubernetesMetricCatalog.test.ts +189 -0
- package/Tests/Types/Monitor/KubernetesTemplateGroupByKeys.test.ts +95 -14
- 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/Monitor/PodmanAlertTemplates.test.ts +190 -21
- package/Tests/Types/Monitor/ProxmoxAlertTemplates.test.ts +352 -22
- package/Tests/Types/Monitor/Recommendation/MonitorRecommendationAlertDebuggability.test.ts +363 -0
- package/Tests/Types/Monitor/Recommendation/MonitorRecommendationNotificationMode.test.ts +93 -18
- package/Tests/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.test.ts +259 -0
- package/Tests/Types/Monitor/Recommendation/RecommendationCriteriaBuilder.test.ts +476 -0
- package/Tests/Types/Monitor/RumAlertTemplates.test.ts +136 -0
- package/Tests/Types/Monitor/SeriesContext/SeriesDebugHints.test.ts +661 -0
- package/Tests/Types/Monitor/SeriesContext/SeriesLabelDisplay.test.ts +507 -0
- package/Tests/Types/Monitor/ServiceAlertTemplates.test.ts +270 -0
- package/Tests/Types/Monitor/TemplateGroupByKeys.test.ts +38 -10
- package/Tests/Types/Monitor/Utils/RecommendationCriteriaAssertions.ts +102 -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/NotificationSetting/RoutineEmailEvents.test.ts +47 -0
- package/Tests/Types/Rum/SessionReplayApiContracts.test.ts +608 -0
- package/Tests/Types/Rum/SessionReplayCustomEvents.test.ts +434 -0
- package/Tests/Types/Telemetry/ErrorClass.test.ts +483 -0
- package/Tests/Types/WebsiteRequest.test.ts +285 -3
- package/Tests/UI/Components/BasicRadioButtons.test.tsx +336 -0
- package/Tests/UI/Components/Charts/ChartBucketIdentity.test.ts +16 -3
- package/Tests/UI/Components/Charts/ChartTrailingBucketGap.test.ts +691 -0
- package/Tests/UI/Components/ComponentsModal.test.tsx +12 -4
- package/Tests/UI/Components/ComponentsModalUsability.test.tsx +518 -0
- package/Tests/UI/Components/CustomFields/CustomFieldsDetailMapping.test.tsx +324 -0
- package/Tests/UI/Components/CustomFields/MapFromCustomFieldInput.test.tsx +176 -0
- package/Tests/UI/Components/Graphs/DayUptimeGraph.test.tsx +484 -0
- package/Tests/UI/Components/HeaderRightRail.test.tsx +157 -0
- package/Tests/UI/Components/IconDropdownItem.test.tsx +102 -0
- package/Tests/UI/Components/JSONTablePrototypePollution.test.tsx +117 -0
- package/Tests/UI/Components/KeyboardShortcutsModal.test.tsx +214 -0
- package/Tests/UI/Components/ModelTable/ModelTableWrapContent.test.tsx +561 -0
- package/Tests/UI/Components/MonitorGraphs/UptimeBarDayModal.test.tsx +302 -0
- package/Tests/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.test.ts +43 -12
- package/Tests/UI/Components/ProgressBar.test.tsx +38 -2
- package/Tests/UI/Components/ShortcutDialogGuard.test.tsx +74 -0
- 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/Components/StatusPage/ResourceGroupSectionAutoExpand.test.tsx +248 -0
- package/Tests/UI/Components/TableCellWrapping.test.tsx +625 -0
- package/Tests/UI/Components/TableLoadingStates.test.tsx +205 -0
- package/Tests/UI/Components/Workflow/NodePlacement.test.ts +148 -0
- package/Tests/UI/Components/Workflow/Workflow.test.tsx +981 -0
- package/Tests/UI/ConfigBrowserTelemetry.test.ts +70 -0
- package/Tests/UI/Rum/ChunkLoader.test.ts +1036 -2
- package/Tests/UI/Rum/FidelityNotices.test.ts +187 -0
- package/Tests/UI/Rum/InactivityMap.test.ts +341 -0
- package/Tests/UI/Rum/PrivacySummaryCard.test.tsx +259 -0
- package/Tests/UI/Rum/RecordingHealthCard.test.tsx +855 -0
- package/Tests/UI/Rum/RecordingHealthStrip.test.tsx +594 -0
- package/Tests/UI/Rum/ReplayCard.test.tsx +451 -0
- package/Tests/UI/Rum/ReplayCorrelationPanel.test.tsx +536 -0
- package/Tests/UI/Rum/ReplayEngine.test.ts +2503 -0
- package/Tests/UI/Rum/ReplayEngineTypes.test.ts +171 -0
- package/Tests/UI/Rum/ReplayHeader.test.tsx +711 -0
- package/Tests/UI/Rum/ReplayLink.test.tsx +119 -0
- package/Tests/UI/Rum/ReplayPinControl.test.tsx +408 -0
- package/Tests/UI/Rum/ReplayPlaybackIntent.test.ts +152 -0
- package/Tests/UI/Rum/ReplayRail.test.tsx +1296 -0
- package/Tests/UI/Rum/ReplayRailDetail.test.tsx +900 -0
- package/Tests/UI/Rum/ReplayScrubber.test.tsx +763 -147
- package/Tests/UI/Rum/ReplaySignalTypes.test.ts +189 -0
- package/Tests/UI/Rum/ReplaySignals.test.ts +1579 -0
- package/Tests/UI/Rum/ReplayStage.test.tsx +396 -473
- package/Tests/UI/Rum/ReplayStageOverlays.test.tsx +918 -0
- package/Tests/UI/Rum/ReplayTimeline.test.tsx +696 -0
- package/Tests/UI/Rum/SessionReplayEmptyState.test.tsx +499 -0
- package/Tests/UI/Rum/SessionReplaySearchBar.test.tsx +346 -0
- package/Tests/UI/Rum/SessionReplaySetupGuide.test.tsx +545 -0
- package/Tests/UI/Rum/SessionReplayTable.test.tsx +991 -0
- package/Tests/UI/Rum/TargetedCapturePanel.test.tsx +254 -0
- package/Tests/UI/Telemetry/BrowserExporterIsolation.test.ts +71 -0
- package/Tests/UI/Utils/GlobalKeyboardShortcut.test.ts +344 -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/API.test.ts +308 -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/HTTPResponseBodyReader.test.ts +323 -0
- 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/DiscoveryScanStatus.test.ts +117 -0
- 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/Rum/ChunkMath.test.ts +83 -0
- package/Tests/Utils/Rum/SessionReplayHealthDiagnosis.test.ts +993 -0
- package/Tests/Utils/Rum/SessionReplayStringMap.test.ts +245 -0
- package/Tests/Utils/StatusPage/ResourceSearch.test.ts +631 -0
- package/Tests/Utils/Telemetry/CaptureSpanExportGating.test.ts +145 -0
- package/Tests/Utils/Telemetry/OriginAllowList.test.ts +633 -0
- package/Tests/Utils/Uptime/DayUptimeGraphUtil.test.ts +461 -0
- package/Tests/Utils/ValueFormatter.test.ts +148 -0
- package/Types/AI/ExceptionAIClassification.ts +10 -24
- package/Types/CustomField/CustomFieldMappingCatalog.ts +161 -0
- package/Types/CustomField/CustomFieldMappingSourceResource.ts +19 -0
- package/Types/CustomField/CustomFieldValueMapping.ts +288 -0
- 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/Icon/IconProp.ts +1 -0
- package/Types/JSONFunctions.ts +189 -43
- package/Types/Monitor/CephAlertTemplates.ts +157 -195
- 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/DockerAlertTemplates.ts +338 -128
- package/Types/Monitor/DockerSwarmAlertTemplates.ts +95 -112
- package/Types/Monitor/HostAlertTemplates.ts +302 -130
- package/Types/Monitor/IotAlertTemplates.ts +112 -104
- package/Types/Monitor/KubernetesAlertTemplates.ts +415 -224
- package/Types/Monitor/KubernetesMetricCatalog.ts +23 -19
- 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/PodmanAlertTemplates.ts +116 -163
- package/Types/Monitor/ProxmoxAlertTemplates.ts +217 -99
- package/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.ts +98 -6
- package/Types/Monitor/Recommendation/MonitorRecommendationUtil.ts +44 -7
- package/Types/Monitor/Recommendation/RecommendationCriteriaBuilder.ts +383 -0
- package/Types/Monitor/RumAlertTemplates.ts +46 -7
- package/Types/Monitor/SeriesContext/SeriesDebugHints.ts +596 -0
- package/Types/Monitor/SeriesContext/SeriesLabelDisplay.ts +554 -0
- package/Types/Monitor/ServiceAlertTemplates.ts +98 -11
- package/Types/Monitor/SnmpMonitor/NetworkDeviceAlertPack.ts +22 -1
- package/Types/Monitor/SqlConnectionConfig.ts +31 -0
- package/Types/Monitor/UptimeHistoryLabels.ts +56 -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/NotificationSetting/RoutineEmailEvents.ts +30 -0
- package/Types/Permission.ts +90 -0
- package/Types/Probe/ProbeMonitorResponse.ts +16 -0
- package/Types/Rum/SessionReplay.ts +209 -0
- package/Types/Rum/SessionReplayApi.ts +722 -0
- package/Types/Rum/SessionReplayCaptureTrigger.ts +21 -12
- package/Types/Rum/SessionReplayConsentMode.ts +11 -7
- package/Types/Rum/SessionReplayCustomEvents.ts +549 -0
- package/Types/Rum/SessionReplayHealth.ts +238 -0
- package/Types/Telemetry/ErrorClass.ts +274 -0
- package/Types/Telemetry/TelemetryIngestSurface.ts +95 -0
- package/Types/Telemetry/TelemetryIngestionKeyPolicy.ts +75 -0
- package/Types/Telemetry/TelemetryIngestionKeyType.ts +29 -0
- package/Types/Telemetry/UnitOfWork.ts +59 -0
- package/Types/WebsiteRequest.ts +85 -3
- package/UI/Components/Charts/Area/AreaChart.tsx +13 -2
- package/UI/Components/Charts/Bar/BarChart.tsx +6 -2
- package/UI/Components/Charts/Line/LineChart.tsx +13 -2
- package/UI/Components/Charts/Types/XAxis/XAxis.ts +29 -0
- package/UI/Components/Charts/Utils/DataPoint.ts +22 -3
- package/UI/Components/Charts/Utils/TimeAnnotation.ts +76 -9
- package/UI/Components/Charts/Utils/XAxis.ts +217 -0
- package/UI/Components/CommandPalette/CommandPalette.tsx +2 -8
- package/UI/Components/CustomFields/CustomFieldsDetail.tsx +143 -25
- package/UI/Components/CustomFields/MapFromCustomFieldInput.tsx +216 -0
- package/UI/Components/EditionLabel/EditionLabel.tsx +4 -0
- package/UI/Components/Graphs/DayUptimeGraph.tsx +173 -8
- package/UI/Components/Graphs/UptimeBarTooltip.tsx +22 -278
- package/UI/Components/Graphs/UptimeDaySummary.tsx +327 -0
- package/UI/Components/Header/Header.tsx +25 -9
- package/UI/Components/Header/IconDropdown/IconDropdownItem.tsx +10 -1
- package/UI/Components/Header/IconDropdown/IconDropdownMenu.tsx +6 -1
- package/UI/Components/Header/ProjectPicker/ProjectPicker.tsx +6 -1
- package/UI/Components/HeaderAlert/NotificationBell/NotificationBellDropdown.tsx +6 -1
- package/UI/Components/Icon/Icon.tsx +11 -0
- package/UI/Components/JSONTable/JSONTable.tsx +2 -2
- package/UI/Components/KeyboardShortcut/KeyboardShortcutsModal.tsx +125 -0
- package/UI/Components/KeyboardShortcut/Screenshots/README.md +19 -0
- package/UI/Components/KeyboardShortcut/Screenshots/keyboard-shortcuts-command-palette.png +0 -0
- package/UI/Components/KeyboardShortcut/Screenshots/keyboard-shortcuts-dialog-dark.png +0 -0
- package/UI/Components/KeyboardShortcut/Screenshots/keyboard-shortcuts-dialog.png +0 -0
- package/UI/Components/KeyboardShortcut/Screenshots/keyboard-shortcuts-help-menu.png +0 -0
- package/UI/Components/KeyboardShortcut/Screenshots/keyboard-shortcuts-narrow.png +0 -0
- package/UI/Components/Modal/Modal.tsx +2 -8
- package/UI/Components/ModelTable/Column.ts +15 -0
- package/UI/Components/Monitor/SeriesDebugCommandsViewer.tsx +64 -0
- package/UI/Components/Monitor/SeriesLabelsViewer.tsx +93 -0
- package/UI/Components/MonitorGraphs/Uptime.tsx +14 -2
- package/UI/Components/MonitorGraphs/UptimeBarDayModal.tsx +63 -10
- package/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.ts +88 -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/Components/StatusPage/ResourceGroupSection.tsx +40 -0
- package/UI/Components/Table/CellClassName.ts +81 -0
- package/UI/Components/Table/TableRow.tsx +20 -14
- package/UI/Components/Table/TableSkeletonRows.tsx +9 -9
- package/UI/Components/Table/Types/Column.ts +36 -0
- package/UI/Components/Tooltip/Tooltip.tsx +11 -1
- package/UI/Components/Workflow/ComponentsModal.tsx +60 -39
- package/UI/Components/Workflow/NodePlacement.ts +57 -0
- package/UI/Components/Workflow/Workflow.tsx +47 -18
- package/UI/Config.ts +15 -33
- package/UI/Utils/GlobalKeyboardShortcut.ts +208 -0
- package/UI/Utils/PageScrollLock.ts +79 -3
- package/UI/Utils/PermissionGate.ts +61 -0
- package/UI/Utils/SensitiveUrlToken.ts +112 -0
- package/UI/Utils/Telemetry/BrowserTelemetryConfig.ts +28 -0
- package/UI/Utils/Telemetry/Telemetry.ts +10 -8
- package/UI/Utils/TestLLMProvider.ts +11 -0
- package/Utils/API.ts +129 -3
- package/Utils/EnterpriseLicense/EnterpriseLicenseSeats.ts +16 -0
- package/Utils/EnterpriseLicense/EnterpriseLicenseUsage.ts +191 -11
- package/Utils/HTTPResponseBodyReader.ts +221 -0
- 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/DiscoveryScanStatus.ts +77 -0
- package/Utils/NetworkDiscovery/PingMonitorBuilder.ts +115 -13
- package/Utils/NetworkSite/SiteStatusRollupUtil.ts +66 -20
- package/Utils/NetworkSite/TypeHierarchyUtil.ts +313 -0
- package/Utils/Rum/ChunkMath.ts +106 -0
- package/Utils/Rum/SessionReplayHealth.ts +732 -0
- package/Utils/Rum/SessionReplayStringMap.ts +226 -0
- package/Utils/Schema/ModelSchema.ts +1 -0
- package/Utils/StatusPage/ResourceSearch.ts +290 -0
- package/Utils/Telemetry/OriginAllowList.ts +355 -0
- package/Utils/Uptime/DayUptimeGraphUtil.ts +205 -0
- package/Utils/ValueFormatter.ts +35 -1
- package/build/dist/Models/AnalyticsModels/RumSession.js +102 -19
- package/build/dist/Models/AnalyticsModels/RumSession.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/RumSessionChunk.js +11 -9
- package/build/dist/Models/AnalyticsModels/RumSessionChunk.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/Span.js +14 -0
- package/build/dist/Models/AnalyticsModels/Span.js.map +1 -1
- package/build/dist/Models/DatabaseModels/AlertCustomField.js +79 -3
- package/build/dist/Models/DatabaseModels/AlertCustomField.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ApiKeyPermission.js +3 -27
- package/build/dist/Models/DatabaseModels/ApiKeyPermission.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/IncidentCustomField.js +79 -3
- package/build/dist/Models/DatabaseModels/IncidentCustomField.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/InventoryItemCustomField.js +79 -3
- package/build/dist/Models/DatabaseModels/InventoryItemCustomField.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/MonitorCustomField.js +79 -3
- package/build/dist/Models/DatabaseModels/MonitorCustomField.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/OnCallDutyPolicyCustomField.js +79 -3
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyCustomField.js.map +1 -1
- package/build/dist/Models/DatabaseModels/RumApplication.js +6 -6
- package/build/dist/Models/DatabaseModels/RumApplication.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ScheduledMaintenanceCustomField.js +79 -3
- package/build/dist/Models/DatabaseModels/ScheduledMaintenanceCustomField.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageCustomField.js +79 -3
- package/build/dist/Models/DatabaseModels/StatusPageCustomField.js.map +1 -1
- package/build/dist/Models/DatabaseModels/TeamCustomField.js +79 -3
- package/build/dist/Models/DatabaseModels/TeamCustomField.js.map +1 -1
- package/build/dist/Models/DatabaseModels/TeamMember.js +0 -9
- package/build/dist/Models/DatabaseModels/TeamMember.js.map +1 -1
- package/build/dist/Models/DatabaseModels/TeamMemberCustomField.js +79 -3
- package/build/dist/Models/DatabaseModels/TeamMemberCustomField.js.map +1 -1
- package/build/dist/Models/DatabaseModels/TeamPermission.js +3 -24
- package/build/dist/Models/DatabaseModels/TeamPermission.js.map +1 -1
- package/build/dist/Models/DatabaseModels/TelemetryException.js +123 -0
- package/build/dist/Models/DatabaseModels/TelemetryException.js.map +1 -1
- package/build/dist/Models/DatabaseModels/TelemetryIngestionKey.js +267 -0
- package/build/dist/Models/DatabaseModels/TelemetryIngestionKey.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/Models/DatabaseModels/UserTelegram.js +0 -4
- package/build/dist/Models/DatabaseModels/UserTelegram.js.map +1 -1
- 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/API/TelemetryAPI.js +769 -109
- package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
- package/build/dist/Server/API/UserNotificationSettingAPI.js +35 -0
- package/build/dist/Server/API/UserNotificationSettingAPI.js.map +1 -0
- package/build/dist/Server/API/UserTelegramAPI.js +28 -6
- package/build/dist/Server/API/UserTelegramAPI.js.map +1 -1
- package/build/dist/Server/EnvironmentConfig.js +41 -2
- 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/1791300000000-AddTelemetryIngestionKeyType.js +60 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791300000000-AddTelemetryIngestionKeyType.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791400000000-SessionReplayRecordEverySessionByDefault.js +53 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791400000000-SessionReplayRecordEverySessionByDefault.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791500000000-WidenCustomFieldDropdownOptions.js +82 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791500000000-WidenCustomFieldDropdownOptions.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791600000000-AddCustomFieldValueMapping.js +46 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791600000000-AddCustomFieldValueMapping.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +20 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Infrastructure/QueueWorker.js +52 -12
- package/build/dist/Server/Infrastructure/QueueWorker.js.map +1 -1
- package/build/dist/Server/Infrastructure/Semaphore.js +1 -0
- package/build/dist/Server/Infrastructure/Semaphore.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 +326 -8
- 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/AccessTokenService.js +1 -0
- package/build/dist/Server/Services/AccessTokenService.js.map +1 -1
- package/build/dist/Server/Services/AlertCustomFieldService.js +93 -0
- package/build/dist/Server/Services/AlertCustomFieldService.js.map +1 -1
- package/build/dist/Server/Services/AlertService.js +34 -0
- package/build/dist/Server/Services/AlertService.js.map +1 -1
- package/build/dist/Server/Services/ApiKeyPermissionService.js +331 -28
- package/build/dist/Server/Services/ApiKeyPermissionService.js.map +1 -1
- package/build/dist/Server/Services/CustomFieldMappingService.js +614 -0
- package/build/dist/Server/Services/CustomFieldMappingService.js.map +1 -0
- 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/GlobalConfigService.js +133 -0
- package/build/dist/Server/Services/GlobalConfigService.js.map +1 -1
- package/build/dist/Server/Services/IncidentCustomFieldService.js +93 -0
- package/build/dist/Server/Services/IncidentCustomFieldService.js.map +1 -1
- package/build/dist/Server/Services/IncidentService.js +32 -0
- package/build/dist/Server/Services/IncidentService.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/MetricService.js +163 -0
- package/build/dist/Server/Services/MetricService.js.map +1 -1
- package/build/dist/Server/Services/MonitorService.js +148 -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 +89 -36
- 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 +88 -30
- package/build/dist/Server/Services/ProjectService.js.map +1 -1
- package/build/dist/Server/Services/RoutineEmailSettingsService.js +69 -0
- package/build/dist/Server/Services/RoutineEmailSettingsService.js.map +1 -0
- package/build/dist/Server/Services/RumSessionReplayViewService.js +86 -24
- package/build/dist/Server/Services/RumSessionReplayViewService.js.map +1 -1
- package/build/dist/Server/Services/ScheduledMaintenanceCustomFieldService.js +93 -0
- package/build/dist/Server/Services/ScheduledMaintenanceCustomFieldService.js.map +1 -1
- package/build/dist/Server/Services/ScheduledMaintenanceService.js +29 -0
- package/build/dist/Server/Services/ScheduledMaintenanceService.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/TeamMemberService.js +62 -2
- package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
- package/build/dist/Server/Services/TeamPermissionService.js +166 -3
- package/build/dist/Server/Services/TeamPermissionService.js.map +1 -1
- package/build/dist/Server/Services/TelemetryIngestionKeyService.js +562 -18
- package/build/dist/Server/Services/TelemetryIngestionKeyService.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 +66 -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/Services/UserTelegramService.js +257 -4
- package/build/dist/Server/Services/UserTelegramService.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/APIKey/AccessPermission.js +2 -2
- package/build/dist/Server/Utils/APIKey/AccessPermission.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/CustomField/CustomFieldDefinitionMappingHooks.js +27 -0
- package/build/dist/Server/Utils/CustomField/CustomFieldDefinitionMappingHooks.js.map +1 -0
- package/build/dist/Server/Utils/CustomField/CustomFieldMappingRegistry.js +226 -0
- package/build/dist/Server/Utils/CustomField/CustomFieldMappingRegistry.js.map +1 -0
- package/build/dist/Server/Utils/CustomField/CustomFieldMappingValidator.js +189 -0
- package/build/dist/Server/Utils/CustomField/CustomFieldMappingValidator.js.map +1 -0
- package/build/dist/Server/Utils/DataSource/EgressGuard.js +121 -7
- package/build/dist/Server/Utils/DataSource/EgressGuard.js.map +1 -1
- 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 +124 -0
- package/build/dist/Server/Utils/EmailRollup/EmailRollupConstants.js.map +1 -0
- package/build/dist/Server/Utils/EmailRollup/EmailRollupFlushRunner.js +733 -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/FrontendEnvironment.js +33 -0
- package/build/dist/Server/Utils/FrontendEnvironment.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 +28 -0
- package/build/dist/Server/Utils/LogRedaction.js.map +1 -1
- package/build/dist/Server/Utils/Logger.js +73 -1
- package/build/dist/Server/Utils/Logger.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +287 -68
- package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.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/IncomingRequestCriteria.js +20 -4
- package/build/dist/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.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/MonitorAlert.js +29 -6
- package/build/dist/Server/Utils/Monitor/MonitorAlert.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 +125 -13
- 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/MonitorIncident.js +22 -6
- package/build/dist/Server/Utils/Monitor/MonitorIncident.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 +54 -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/Monitor/SeriesContextEnricher.js +237 -0
- package/build/dist/Server/Utils/Monitor/SeriesContextEnricher.js.map +1 -0
- 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/SSRFProtection.js +136 -9
- package/build/dist/Server/Utils/SSRFProtection.js.map +1 -1
- package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCache.js +174 -89
- package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCache.js.map +1 -1
- package/build/dist/Server/Utils/SessionReplay/SessionReplayHealthCounters.js +223 -0
- package/build/dist/Server/Utils/SessionReplay/SessionReplayHealthCounters.js.map +1 -0
- package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js +945 -164
- package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js.map +1 -1
- package/build/dist/Server/Utils/StartServer.js +41 -28
- package/build/dist/Server/Utils/StartServer.js.map +1 -1
- package/build/dist/Server/Utils/TelegramVerificationToken.js +129 -0
- package/build/dist/Server/Utils/TelegramVerificationToken.js.map +1 -0
- 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/PinServiceName.js +163 -0
- package/build/dist/Server/Utils/Telemetry/PinServiceName.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/TelemetryFanInWriter.js +28 -15
- package/build/dist/Server/Utils/Telemetry/TelemetryFanInWriter.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.js +64 -0
- package/build/dist/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/TelemetryIngestionKeyRateLimiter.js +171 -0
- package/build/dist/Server/Utils/Telemetry/TelemetryIngestionKeyRateLimiter.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry.js +162 -46
- package/build/dist/Server/Utils/Telemetry.js.map +1 -1
- package/build/dist/Server/Utils/VM/VMAPI.js.map +1 -1
- package/build/dist/Server/Utils/VM/VMRunner.js +780 -69
- package/build/dist/Server/Utils/VM/VMRunner.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/CustomField/CustomFieldMappingCatalog.js +60 -0
- package/build/dist/Types/CustomField/CustomFieldMappingCatalog.js.map +1 -0
- package/build/dist/Types/CustomField/CustomFieldMappingSourceResource.js +20 -0
- package/build/dist/Types/CustomField/CustomFieldMappingSourceResource.js.map +1 -0
- package/build/dist/Types/CustomField/CustomFieldValueMapping.js +141 -0
- package/build/dist/Types/CustomField/CustomFieldValueMapping.js.map +1 -0
- 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/Icon/IconProp.js +1 -0
- package/build/dist/Types/Icon/IconProp.js.map +1 -1
- package/build/dist/Types/JSONFunctions.js +118 -37
- package/build/dist/Types/JSONFunctions.js.map +1 -1
- package/build/dist/Types/Monitor/CephAlertTemplates.js +130 -171
- package/build/dist/Types/Monitor/CephAlertTemplates.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/DockerAlertTemplates.js +286 -121
- package/build/dist/Types/Monitor/DockerAlertTemplates.js.map +1 -1
- package/build/dist/Types/Monitor/DockerSwarmAlertTemplates.js +84 -105
- package/build/dist/Types/Monitor/DockerSwarmAlertTemplates.js.map +1 -1
- package/build/dist/Types/Monitor/HostAlertTemplates.js +250 -122
- package/build/dist/Types/Monitor/HostAlertTemplates.js.map +1 -1
- package/build/dist/Types/Monitor/IotAlertTemplates.js +81 -81
- package/build/dist/Types/Monitor/IotAlertTemplates.js.map +1 -1
- package/build/dist/Types/Monitor/KubernetesAlertTemplates.js +375 -215
- package/build/dist/Types/Monitor/KubernetesAlertTemplates.js.map +1 -1
- package/build/dist/Types/Monitor/KubernetesMetricCatalog.js +19 -19
- package/build/dist/Types/Monitor/KubernetesMetricCatalog.js.map +1 -1
- 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/PodmanAlertTemplates.js +104 -153
- package/build/dist/Types/Monitor/PodmanAlertTemplates.js.map +1 -1
- package/build/dist/Types/Monitor/ProxmoxAlertTemplates.js +156 -91
- package/build/dist/Types/Monitor/ProxmoxAlertTemplates.js.map +1 -1
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.js +67 -6
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.js.map +1 -1
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationUtil.js +38 -7
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationUtil.js.map +1 -1
- package/build/dist/Types/Monitor/Recommendation/RecommendationCriteriaBuilder.js +261 -0
- package/build/dist/Types/Monitor/Recommendation/RecommendationCriteriaBuilder.js.map +1 -0
- package/build/dist/Types/Monitor/RumAlertTemplates.js +46 -7
- package/build/dist/Types/Monitor/RumAlertTemplates.js.map +1 -1
- package/build/dist/Types/Monitor/SeriesContext/SeriesDebugHints.js +422 -0
- package/build/dist/Types/Monitor/SeriesContext/SeriesDebugHints.js.map +1 -0
- package/build/dist/Types/Monitor/SeriesContext/SeriesLabelDisplay.js +441 -0
- package/build/dist/Types/Monitor/SeriesContext/SeriesLabelDisplay.js.map +1 -0
- package/build/dist/Types/Monitor/ServiceAlertTemplates.js +79 -15
- package/build/dist/Types/Monitor/ServiceAlertTemplates.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/Monitor/UptimeHistoryLabels.js +17 -0
- package/build/dist/Types/Monitor/UptimeHistoryLabels.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/NotificationSetting/RoutineEmailEvents.js +29 -0
- package/build/dist/Types/NotificationSetting/RoutineEmailEvents.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/Rum/SessionReplay.js +117 -0
- package/build/dist/Types/Rum/SessionReplay.js.map +1 -1
- package/build/dist/Types/Rum/SessionReplayApi.js +181 -0
- package/build/dist/Types/Rum/SessionReplayApi.js.map +1 -0
- package/build/dist/Types/Rum/SessionReplayCaptureTrigger.js +21 -12
- package/build/dist/Types/Rum/SessionReplayCaptureTrigger.js.map +1 -1
- package/build/dist/Types/Rum/SessionReplayConsentMode.js +11 -7
- package/build/dist/Types/Rum/SessionReplayConsentMode.js.map +1 -1
- package/build/dist/Types/Rum/SessionReplayCustomEvents.js +172 -0
- package/build/dist/Types/Rum/SessionReplayCustomEvents.js.map +1 -0
- package/build/dist/Types/Rum/SessionReplayHealth.js +37 -0
- package/build/dist/Types/Rum/SessionReplayHealth.js.map +1 -0
- package/build/dist/Types/Telemetry/ErrorClass.js +216 -0
- package/build/dist/Types/Telemetry/ErrorClass.js.map +1 -0
- package/build/dist/Types/Telemetry/TelemetryIngestSurface.js +85 -0
- package/build/dist/Types/Telemetry/TelemetryIngestSurface.js.map +1 -0
- package/build/dist/Types/Telemetry/TelemetryIngestionKeyPolicy.js +15 -0
- package/build/dist/Types/Telemetry/TelemetryIngestionKeyPolicy.js.map +1 -0
- package/build/dist/Types/Telemetry/TelemetryIngestionKeyType.js +30 -0
- package/build/dist/Types/Telemetry/TelemetryIngestionKeyType.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/Types/WebsiteRequest.js +55 -3
- package/build/dist/Types/WebsiteRequest.js.map +1 -1
- package/build/dist/UI/Components/Charts/Area/AreaChart.js +13 -2
- package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.js +6 -2
- package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/Line/LineChart.js +13 -2
- package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/Types/XAxis/XAxis.js.map +1 -1
- package/build/dist/UI/Components/Charts/Utils/DataPoint.js +15 -3
- package/build/dist/UI/Components/Charts/Utils/DataPoint.js.map +1 -1
- package/build/dist/UI/Components/Charts/Utils/TimeAnnotation.js +40 -9
- package/build/dist/UI/Components/Charts/Utils/TimeAnnotation.js.map +1 -1
- package/build/dist/UI/Components/Charts/Utils/XAxis.js +186 -0
- package/build/dist/UI/Components/Charts/Utils/XAxis.js.map +1 -1
- 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/CustomFields/CustomFieldsDetail.js +80 -6
- package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js.map +1 -1
- package/build/dist/UI/Components/CustomFields/MapFromCustomFieldInput.js +118 -0
- package/build/dist/UI/Components/CustomFields/MapFromCustomFieldInput.js.map +1 -0
- 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/Graphs/DayUptimeGraph.js +112 -7
- package/build/dist/UI/Components/Graphs/DayUptimeGraph.js.map +1 -1
- package/build/dist/UI/Components/Graphs/UptimeBarTooltip.js +5 -162
- package/build/dist/UI/Components/Graphs/UptimeBarTooltip.js.map +1 -1
- package/build/dist/UI/Components/Graphs/UptimeDaySummary.js +181 -0
- package/build/dist/UI/Components/Graphs/UptimeDaySummary.js.map +1 -0
- package/build/dist/UI/Components/Header/Header.js +2 -3
- package/build/dist/UI/Components/Header/Header.js.map +1 -1
- package/build/dist/UI/Components/Header/IconDropdown/IconDropdownItem.js +2 -1
- package/build/dist/UI/Components/Header/IconDropdown/IconDropdownItem.js.map +1 -1
- package/build/dist/UI/Components/Header/IconDropdown/IconDropdownMenu.js +7 -1
- package/build/dist/UI/Components/Header/IconDropdown/IconDropdownMenu.js.map +1 -1
- package/build/dist/UI/Components/Header/ProjectPicker/ProjectPicker.js +7 -1
- package/build/dist/UI/Components/Header/ProjectPicker/ProjectPicker.js.map +1 -1
- package/build/dist/UI/Components/HeaderAlert/NotificationBell/NotificationBellDropdown.js +7 -1
- package/build/dist/UI/Components/HeaderAlert/NotificationBell/NotificationBellDropdown.js.map +1 -1
- package/build/dist/UI/Components/Icon/Icon.js +5 -0
- package/build/dist/UI/Components/Icon/Icon.js.map +1 -1
- package/build/dist/UI/Components/JSONTable/JSONTable.js +2 -2
- package/build/dist/UI/Components/JSONTable/JSONTable.js.map +1 -1
- package/build/dist/UI/Components/KeyboardShortcut/KeyboardShortcutsModal.js +35 -0
- package/build/dist/UI/Components/KeyboardShortcut/KeyboardShortcutsModal.js.map +1 -0
- 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/Monitor/SeriesDebugCommandsViewer.js +34 -0
- package/build/dist/UI/Components/Monitor/SeriesDebugCommandsViewer.js.map +1 -0
- package/build/dist/UI/Components/Monitor/SeriesLabelsViewer.js +49 -0
- package/build/dist/UI/Components/Monitor/SeriesLabelsViewer.js.map +1 -0
- package/build/dist/UI/Components/MonitorGraphs/Uptime.js +1 -1
- package/build/dist/UI/Components/MonitorGraphs/Uptime.js.map +1 -1
- package/build/dist/UI/Components/MonitorGraphs/UptimeBarDayModal.js +32 -6
- package/build/dist/UI/Components/MonitorGraphs/UptimeBarDayModal.js.map +1 -1
- package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.js +77 -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/Components/StatusPage/ResourceGroupSection.js +22 -1
- package/build/dist/UI/Components/StatusPage/ResourceGroupSection.js.map +1 -1
- package/build/dist/UI/Components/Table/CellClassName.js +55 -0
- package/build/dist/UI/Components/Table/CellClassName.js.map +1 -0
- package/build/dist/UI/Components/Table/TableRow.js +16 -13
- package/build/dist/UI/Components/Table/TableRow.js.map +1 -1
- package/build/dist/UI/Components/Table/TableSkeletonRows.js +9 -8
- package/build/dist/UI/Components/Table/TableSkeletonRows.js.map +1 -1
- package/build/dist/UI/Components/Tooltip/Tooltip.js +25 -1
- package/build/dist/UI/Components/Tooltip/Tooltip.js.map +1 -1
- package/build/dist/UI/Components/Workflow/ComponentsModal.js +47 -30
- package/build/dist/UI/Components/Workflow/ComponentsModal.js.map +1 -1
- package/build/dist/UI/Components/Workflow/NodePlacement.js +37 -0
- package/build/dist/UI/Components/Workflow/NodePlacement.js.map +1 -0
- package/build/dist/UI/Components/Workflow/Workflow.js +35 -18
- package/build/dist/UI/Components/Workflow/Workflow.js.map +1 -1
- package/build/dist/UI/Config.js +10 -20
- package/build/dist/UI/Config.js.map +1 -1
- package/build/dist/UI/Utils/GlobalKeyboardShortcut.js +138 -0
- package/build/dist/UI/Utils/GlobalKeyboardShortcut.js.map +1 -0
- 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/Telemetry/BrowserTelemetryConfig.js +14 -0
- package/build/dist/UI/Utils/Telemetry/BrowserTelemetryConfig.js.map +1 -0
- package/build/dist/UI/Utils/Telemetry/Telemetry.js +7 -5
- package/build/dist/UI/Utils/Telemetry/Telemetry.js.map +1 -1
- package/build/dist/UI/Utils/TestLLMProvider.js +4 -5
- package/build/dist/UI/Utils/TestLLMProvider.js.map +1 -1
- package/build/dist/Utils/API.js +67 -5
- package/build/dist/Utils/API.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/HTTPResponseBodyReader.js +157 -0
- package/build/dist/Utils/HTTPResponseBodyReader.js.map +1 -0
- 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/DiscoveryScanStatus.js +69 -0
- package/build/dist/Utils/NetworkDiscovery/DiscoveryScanStatus.js.map +1 -0
- 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/build/dist/Utils/Rum/ChunkMath.js +76 -0
- package/build/dist/Utils/Rum/ChunkMath.js.map +1 -1
- package/build/dist/Utils/Rum/SessionReplayHealth.js +534 -0
- package/build/dist/Utils/Rum/SessionReplayHealth.js.map +1 -0
- package/build/dist/Utils/Rum/SessionReplayStringMap.js +165 -0
- package/build/dist/Utils/Rum/SessionReplayStringMap.js.map +1 -0
- package/build/dist/Utils/Schema/ModelSchema.js +1 -0
- package/build/dist/Utils/Schema/ModelSchema.js.map +1 -1
- package/build/dist/Utils/StatusPage/ResourceSearch.js +200 -0
- package/build/dist/Utils/StatusPage/ResourceSearch.js.map +1 -0
- package/build/dist/Utils/Telemetry/OriginAllowList.js +299 -0
- package/build/dist/Utils/Telemetry/OriginAllowList.js.map +1 -0
- package/build/dist/Utils/Uptime/DayUptimeGraphUtil.js +147 -0
- package/build/dist/Utils/Uptime/DayUptimeGraphUtil.js.map +1 -0
- package/build/dist/Utils/ValueFormatter.js +32 -1
- package/build/dist/Utils/ValueFormatter.js.map +1 -1
- package/package.json +2 -2
|
@@ -6,8 +6,17 @@ import RumApplicationService from "../../../Server/Services/RumApplicationServic
|
|
|
6
6
|
import RumSessionReplayViewService from "../../../Server/Services/RumSessionReplayViewService";
|
|
7
7
|
import RumSessionService from "../../../Server/Services/RumSessionService";
|
|
8
8
|
import RumSessionChunkService from "../../../Server/Services/RumSessionChunkService";
|
|
9
|
+
import ExceptionInstanceService from "../../../Server/Services/ExceptionInstanceService";
|
|
9
10
|
import { Statement } from "../../../Server/Utils/AnalyticsDatabase/Statement";
|
|
10
11
|
import SessionReplayIdentity from "../../../Server/Utils/SessionReplay/SessionReplayIdentity";
|
|
12
|
+
import SessionReplayReadService from "../../../Server/Utils/SessionReplay/SessionReplayReadService";
|
|
13
|
+
import NotFoundException from "../../../Types/Exception/NotFoundException";
|
|
14
|
+
import {
|
|
15
|
+
MAX_SESSION_REPLAY_CHUNKS_PER_READ,
|
|
16
|
+
SESSION_REPLAY_LIST_SEARCH_MAX_LENGTH,
|
|
17
|
+
SESSION_REPLAY_LIST_SEARCH_MAX_WINDOW_DAYS,
|
|
18
|
+
SESSION_REPLAY_MAX_SESSION_MS,
|
|
19
|
+
} from "../../../Types/Rum/SessionReplay";
|
|
11
20
|
import RumApplication from "../../../Models/DatabaseModels/RumApplication";
|
|
12
21
|
import RumSessionReplayView from "../../../Models/DatabaseModels/RumSessionReplayView";
|
|
13
22
|
import Label from "../../../Models/DatabaseModels/Label";
|
|
@@ -30,7 +39,6 @@ import Permission, {
|
|
|
30
39
|
} from "../../../Types/Permission";
|
|
31
40
|
import PermissionScope from "../../../Types/Database/AccessControl/PermissionScope";
|
|
32
41
|
import UserType from "../../../Types/UserType";
|
|
33
|
-
import { MAX_SESSION_REPLAY_CHUNKS_PER_READ } from "../../../Types/Rum/SessionReplay";
|
|
34
42
|
import {
|
|
35
43
|
afterEach,
|
|
36
44
|
beforeAll,
|
|
@@ -124,6 +132,7 @@ const FOR_EXCEPTION_ROUTE: string =
|
|
|
124
132
|
"/telemetry/rum/session-replay/for-exception";
|
|
125
133
|
const INGEST_STATUS_ROUTE: string =
|
|
126
134
|
"/telemetry/rum/session-replay/ingest-status";
|
|
135
|
+
const VIEWS_ROUTE: string = "/telemetry/rum/session-replay/views";
|
|
127
136
|
|
|
128
137
|
function findRoute(uri: string): RecordedRoute {
|
|
129
138
|
const route: RecordedRoute | undefined = recordedRoutes.find(
|
|
@@ -414,6 +423,7 @@ describe("Session replay playback API", () => {
|
|
|
414
423
|
|
|
415
424
|
let headerQuerySpy: jest.SpyInstance;
|
|
416
425
|
let chunkQuerySpy: jest.SpyInstance;
|
|
426
|
+
let exceptionQuerySpy: jest.SpyInstance;
|
|
417
427
|
let findOneBySpy: jest.SpyInstance;
|
|
418
428
|
let findBySpy: jest.SpyInstance;
|
|
419
429
|
let recordViewSpy: jest.SpyInstance;
|
|
@@ -450,6 +460,18 @@ describe("Session replay playback API", () => {
|
|
|
450
460
|
.spyOn(RumSessionChunkService, "executeQuery")
|
|
451
461
|
.mockResolvedValue(fakeResultSet([]) as never);
|
|
452
462
|
|
|
463
|
+
/*
|
|
464
|
+
* The exception -> replay lookup consults the exception instance
|
|
465
|
+
* table for live sessions before it reads headers. Empty by default;
|
|
466
|
+
* the for-exception suite overrides it where the side index matters.
|
|
467
|
+
*/
|
|
468
|
+
exceptionQuerySpy = jest
|
|
469
|
+
.spyOn(ExceptionInstanceService, "executeQuery")
|
|
470
|
+
.mockResolvedValue(fakeResultSet([]) as never);
|
|
471
|
+
|
|
472
|
+
SessionReplayReadService.clearActivitySummaryCache();
|
|
473
|
+
SessionReplayReadService.setPublishedRecorderVersionProvider(null);
|
|
474
|
+
|
|
453
475
|
findOneBySpy = jest.spyOn(RumApplicationService, "findOneBy");
|
|
454
476
|
findBySpy = jest.spyOn(RumApplicationService, "findBy");
|
|
455
477
|
|
|
@@ -527,12 +549,13 @@ describe("Session replay playback API", () => {
|
|
|
527
549
|
}
|
|
528
550
|
|
|
529
551
|
describe("guard shape", () => {
|
|
530
|
-
test("all
|
|
552
|
+
test("all seven routes are registered and every one carries the three-middleware guard", () => {
|
|
531
553
|
for (const uri of [
|
|
532
554
|
LIST_ROUTE,
|
|
533
555
|
MANIFEST_ROUTE,
|
|
534
556
|
CHUNKS_ROUTE,
|
|
535
557
|
HEARTBEAT_ROUTE,
|
|
558
|
+
VIEWS_ROUTE,
|
|
536
559
|
FOR_EXCEPTION_ROUTE,
|
|
537
560
|
INGEST_STATUS_ROUTE,
|
|
538
561
|
]) {
|
|
@@ -563,7 +586,12 @@ describe("Session replay playback API", () => {
|
|
|
563
586
|
expect(listGuard).toBeDefined();
|
|
564
587
|
expect(payloadGuard).not.toBe(listGuard);
|
|
565
588
|
|
|
566
|
-
for (const uri of [
|
|
589
|
+
for (const uri of [
|
|
590
|
+
MANIFEST_ROUTE,
|
|
591
|
+
CHUNKS_ROUTE,
|
|
592
|
+
HEARTBEAT_ROUTE,
|
|
593
|
+
VIEWS_ROUTE,
|
|
594
|
+
]) {
|
|
567
595
|
expect(findRoute(uri).handlers[2]).toBe(payloadGuard);
|
|
568
596
|
}
|
|
569
597
|
|
|
@@ -608,7 +636,7 @@ describe("Session replay playback API", () => {
|
|
|
608
636
|
);
|
|
609
637
|
});
|
|
610
638
|
|
|
611
|
-
test("
|
|
639
|
+
test("the chunk query filters on the application resolved from the header", async () => {
|
|
612
640
|
const principal: {
|
|
613
641
|
request: JSONObject;
|
|
614
642
|
databaseProps: DatabaseCommonInteractionProps;
|
|
@@ -622,13 +650,11 @@ describe("Session replay playback API", () => {
|
|
|
622
650
|
mockSessionHeader(applicationAId);
|
|
623
651
|
mockApplication({ id: applicationAId, labelIds: [] });
|
|
624
652
|
|
|
625
|
-
chunkQuerySpy
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
)
|
|
629
|
-
|
|
630
|
-
fakeResultSet([{ chunkIndex: 0, payload: "[1]" }]) as never,
|
|
631
|
-
);
|
|
653
|
+
chunkQuerySpy.mockResolvedValue(
|
|
654
|
+
fakeResultSet([
|
|
655
|
+
{ chunkIndex: 0, servedPayload: "[1]", isServed: 1 },
|
|
656
|
+
]) as never,
|
|
657
|
+
);
|
|
632
658
|
|
|
633
659
|
await callRoute({
|
|
634
660
|
uri: CHUNKS_ROUTE,
|
|
@@ -637,23 +663,85 @@ describe("Session replay playback API", () => {
|
|
|
637
663
|
sessionId: "session-1",
|
|
638
664
|
tabId: "tab-1",
|
|
639
665
|
chunkIndexes: [0],
|
|
640
|
-
|
|
666
|
+
},
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
expect(chunkQuerySpy).toHaveBeenCalledTimes(1);
|
|
670
|
+
|
|
671
|
+
const statement: Statement = chunkQuerySpy.mock.calls[0]![0] as Statement;
|
|
672
|
+
expect(statement.query).toContain("rumApplicationId = ");
|
|
673
|
+
expect(Object.values(statement.query_params)).toContain(
|
|
674
|
+
applicationAId.toString(),
|
|
675
|
+
);
|
|
676
|
+
expect(Object.values(statement.query_params)).not.toContain(
|
|
677
|
+
applicationBId.toString(),
|
|
678
|
+
);
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
/*
|
|
682
|
+
* A caller-supplied rumApplicationId is a DISAMBIGUATOR: it narrows
|
|
683
|
+
* which header row is read, and the application THAT row names is
|
|
684
|
+
* what gets authorized. Naming an application the session was never
|
|
685
|
+
* recorded under finds no header, so it cannot widen access.
|
|
686
|
+
*/
|
|
687
|
+
test("a caller-supplied rumApplicationId only narrows the header read and is authorized on its own merits", async () => {
|
|
688
|
+
const principal: {
|
|
689
|
+
request: JSONObject;
|
|
690
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
691
|
+
} = buildPrincipal({
|
|
692
|
+
projectId: projectId,
|
|
693
|
+
userId: userId,
|
|
694
|
+
permissions: [Permission.ReadRumSessionReplayPayload],
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
mockProps(principal.databaseProps);
|
|
698
|
+
/* No header exists under application B, whatever the body says. */
|
|
699
|
+
headerQuerySpy.mockResolvedValue(fakeResultSet([]) as never);
|
|
700
|
+
mockApplication({ id: applicationAId, labelIds: [] });
|
|
701
|
+
|
|
702
|
+
const result: CallResult = await callRoute({
|
|
703
|
+
uri: CHUNKS_ROUTE,
|
|
704
|
+
request: principal.request,
|
|
705
|
+
body: {
|
|
706
|
+
sessionId: "session-1",
|
|
707
|
+
tabId: "tab-1",
|
|
708
|
+
chunkIndexes: [0],
|
|
641
709
|
rumApplicationId: applicationBId.toString(),
|
|
642
710
|
},
|
|
643
711
|
});
|
|
644
712
|
|
|
645
|
-
|
|
713
|
+
const headerStatement: Statement = headerQuerySpy.mock
|
|
714
|
+
.calls[0]![0] as Statement;
|
|
715
|
+
expect(headerStatement.query).toContain("rumApplicationId = ");
|
|
716
|
+
expect(Object.values(headerStatement.query_params)).toContain(
|
|
717
|
+
applicationBId.toString(),
|
|
718
|
+
);
|
|
719
|
+
|
|
720
|
+
expect(result.thrownToNext).toBeInstanceOf(NotFoundException);
|
|
721
|
+
expect(chunkQuerySpy).not.toHaveBeenCalled();
|
|
722
|
+
});
|
|
646
723
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
724
|
+
test("a malformed disambiguator is a bad request", async () => {
|
|
725
|
+
const principal: {
|
|
726
|
+
request: JSONObject;
|
|
727
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
728
|
+
} = buildPrincipal({
|
|
729
|
+
projectId: projectId,
|
|
730
|
+
userId: userId,
|
|
731
|
+
permissions: [Permission.ReadRumSessionReplayPayload],
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
mockProps(principal.databaseProps);
|
|
735
|
+
|
|
736
|
+
const result: CallResult = await callRoute({
|
|
737
|
+
uri: MANIFEST_ROUTE,
|
|
738
|
+
request: principal.request,
|
|
739
|
+
body: { sessionId: "session-1", rumApplicationId: "nope" },
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
expect(result.thrownToNext).toBeInstanceOf(BadDataException);
|
|
743
|
+
expect(headerQuerySpy).not.toHaveBeenCalled();
|
|
744
|
+
expect(recordViewSpy).not.toHaveBeenCalled();
|
|
657
745
|
});
|
|
658
746
|
|
|
659
747
|
test("a sessionId that exists under two applications is refused rather than resolved to the newest", async () => {
|
|
@@ -1245,6 +1333,132 @@ describe("Session replay playback API", () => {
|
|
|
1245
1333
|
expect(statement.query).not.toContain("aggIdentifiedUserKey =");
|
|
1246
1334
|
});
|
|
1247
1335
|
|
|
1336
|
+
/*
|
|
1337
|
+
* Dropping the filter silently is the dangerous half of that gate. The
|
|
1338
|
+
* request "show me jane@example.com's sessions" then answers 200 with
|
|
1339
|
+
* every session in the application and the caller has no way to tell -
|
|
1340
|
+
* least of all when the application has no sessions at all, where even
|
|
1341
|
+
* inspecting the rows cannot reveal it. A support engineer opens the
|
|
1342
|
+
* wrong recording, and each opening writes an audit row against a real
|
|
1343
|
+
* end user. The Dashboard already reads this field and shows a notice.
|
|
1344
|
+
*/
|
|
1345
|
+
test("a dropped identity filter is NAMED in the response, not silently ignored", async () => {
|
|
1346
|
+
const principal: {
|
|
1347
|
+
request: JSONObject;
|
|
1348
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
1349
|
+
} = buildPrincipal({
|
|
1350
|
+
projectId: projectId,
|
|
1351
|
+
userId: userId,
|
|
1352
|
+
permissions: [Permission.TelemetryAdmin],
|
|
1353
|
+
});
|
|
1354
|
+
|
|
1355
|
+
mockProps(principal.databaseProps);
|
|
1356
|
+
mockApplication({ id: applicationAId, labelIds: [] });
|
|
1357
|
+
|
|
1358
|
+
const result: CallResult = await callRoute({
|
|
1359
|
+
uri: LIST_ROUTE,
|
|
1360
|
+
request: principal.request,
|
|
1361
|
+
body: {
|
|
1362
|
+
rumApplicationId: applicationAId.toString(),
|
|
1363
|
+
filters: { identifiedUserRef: "jane@example.com" },
|
|
1364
|
+
},
|
|
1365
|
+
});
|
|
1366
|
+
|
|
1367
|
+
expect(result.jsonBody?.["ignoredFilters"]).toEqual([
|
|
1368
|
+
"identifiedUserRef",
|
|
1369
|
+
]);
|
|
1370
|
+
});
|
|
1371
|
+
|
|
1372
|
+
test("a list that honoured every filter says so with an empty ignoredFilters", async () => {
|
|
1373
|
+
const principal: {
|
|
1374
|
+
request: JSONObject;
|
|
1375
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
1376
|
+
} = buildPrincipal({
|
|
1377
|
+
projectId: projectId,
|
|
1378
|
+
userId: userId,
|
|
1379
|
+
permissions: [Permission.ProjectOwner],
|
|
1380
|
+
});
|
|
1381
|
+
|
|
1382
|
+
mockProps(principal.databaseProps);
|
|
1383
|
+
mockApplication({ id: applicationAId, labelIds: [] });
|
|
1384
|
+
|
|
1385
|
+
const result: CallResult = await callRoute({
|
|
1386
|
+
uri: LIST_ROUTE,
|
|
1387
|
+
request: principal.request,
|
|
1388
|
+
body: {
|
|
1389
|
+
rumApplicationId: applicationAId.toString(),
|
|
1390
|
+
filters: { identifiedUserRef: "jane@example.com" },
|
|
1391
|
+
},
|
|
1392
|
+
});
|
|
1393
|
+
|
|
1394
|
+
/*
|
|
1395
|
+
* Always present, so a client never has to tell "nothing was ignored"
|
|
1396
|
+
* apart from "an older server that never said".
|
|
1397
|
+
*/
|
|
1398
|
+
expect(result.jsonBody?.["ignoredFilters"]).toEqual([]);
|
|
1399
|
+
});
|
|
1400
|
+
|
|
1401
|
+
/*
|
|
1402
|
+
* Every array filter becomes an `IN (...)` inside a HAVING that
|
|
1403
|
+
* ClickHouse evaluates per group over the whole window, so an uncapped
|
|
1404
|
+
* array is the caller's length times the sessions in range - a cheap
|
|
1405
|
+
* denial of service for anyone holding the list permission.
|
|
1406
|
+
*/
|
|
1407
|
+
test("filter arrays and free-text filters are capped before they reach the query", async () => {
|
|
1408
|
+
const principal: {
|
|
1409
|
+
request: JSONObject;
|
|
1410
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
1411
|
+
} = buildPrincipal({
|
|
1412
|
+
projectId: projectId,
|
|
1413
|
+
userId: userId,
|
|
1414
|
+
permissions: [Permission.ProjectOwner],
|
|
1415
|
+
});
|
|
1416
|
+
|
|
1417
|
+
mockProps(principal.databaseProps);
|
|
1418
|
+
mockApplication({ id: applicationAId, labelIds: [] });
|
|
1419
|
+
|
|
1420
|
+
const manyBrowsers: Array<string> = [];
|
|
1421
|
+
|
|
1422
|
+
for (let index: number = 0; index < 5000; index++) {
|
|
1423
|
+
manyBrowsers.push(`browser-${index}`);
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
await callRoute({
|
|
1427
|
+
uri: LIST_ROUTE,
|
|
1428
|
+
request: principal.request,
|
|
1429
|
+
body: {
|
|
1430
|
+
rumApplicationId: applicationAId.toString(),
|
|
1431
|
+
filters: {
|
|
1432
|
+
browserNames: manyBrowsers,
|
|
1433
|
+
route: "r".repeat(10_000),
|
|
1434
|
+
},
|
|
1435
|
+
},
|
|
1436
|
+
});
|
|
1437
|
+
|
|
1438
|
+
const statement: Statement = headerQuerySpy.mock
|
|
1439
|
+
.calls[0]![0] as Statement;
|
|
1440
|
+
|
|
1441
|
+
const bound: Array<unknown> = Object.values(statement.query_params);
|
|
1442
|
+
|
|
1443
|
+
const browserArray: Array<string> | undefined = bound.find(
|
|
1444
|
+
(value: unknown): boolean => {
|
|
1445
|
+
return Array.isArray(value) && (value as Array<string>).length > 1;
|
|
1446
|
+
},
|
|
1447
|
+
) as Array<string> | undefined;
|
|
1448
|
+
|
|
1449
|
+
expect(browserArray).toBeDefined();
|
|
1450
|
+
expect(browserArray!.length).toBeLessThanOrEqual(50);
|
|
1451
|
+
|
|
1452
|
+
const routeValue: string | undefined = bound.find(
|
|
1453
|
+
(value: unknown): boolean => {
|
|
1454
|
+
return typeof value === "string" && value.startsWith("rrr");
|
|
1455
|
+
},
|
|
1456
|
+
) as string | undefined;
|
|
1457
|
+
|
|
1458
|
+
expect(routeValue).toBeDefined();
|
|
1459
|
+
expect(routeValue!.length).toBeLessThanOrEqual(256);
|
|
1460
|
+
});
|
|
1461
|
+
|
|
1248
1462
|
/*
|
|
1249
1463
|
* A filter the server cannot honour must be refused, not dropped. The
|
|
1250
1464
|
* silent-drop shape returns the WHOLE project's sessions with a 200 and
|
|
@@ -1495,162 +1709,1143 @@ describe("Session replay playback API", () => {
|
|
|
1495
1709
|
"(aggRageClickCount + aggDeadClickCount + aggErrorClickCount + aggRefreshRageCount) > 0",
|
|
1496
1710
|
);
|
|
1497
1711
|
});
|
|
1498
|
-
});
|
|
1499
1712
|
|
|
1500
|
-
|
|
1501
|
-
|
|
1713
|
+
function ownerPrincipal(): {
|
|
1714
|
+
request: JSONObject;
|
|
1715
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
1716
|
+
} {
|
|
1502
1717
|
const principal: {
|
|
1503
1718
|
request: JSONObject;
|
|
1504
1719
|
databaseProps: DatabaseCommonInteractionProps;
|
|
1505
1720
|
} = buildPrincipal({
|
|
1506
1721
|
projectId: projectId,
|
|
1507
1722
|
userId: userId,
|
|
1508
|
-
permissions: [Permission.
|
|
1723
|
+
permissions: [Permission.ProjectOwner],
|
|
1509
1724
|
});
|
|
1510
1725
|
|
|
1511
1726
|
mockProps(principal.databaseProps);
|
|
1512
|
-
mockSessionHeader(applicationAId);
|
|
1513
1727
|
mockApplication({ id: applicationAId, labelIds: [] });
|
|
1514
|
-
const viewId: ObjectID = mockRecordedView();
|
|
1515
1728
|
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
{
|
|
1519
|
-
tabId: "tab-1",
|
|
1520
|
-
chunkIndex: 0,
|
|
1521
|
-
chunkStartOffsetMs: 0,
|
|
1522
|
-
chunkEndOffsetMs: 15000,
|
|
1523
|
-
eventCount: 100,
|
|
1524
|
-
hasFullSnapshot: true,
|
|
1525
|
-
chunkPayloadBytes: 1024,
|
|
1526
|
-
errorCount: 2,
|
|
1527
|
-
rageClickCount: 1,
|
|
1528
|
-
deadClickCount: 4,
|
|
1529
|
-
errorClickCount: 6,
|
|
1530
|
-
refreshRageCount: 5,
|
|
1531
|
-
routeCount: 3,
|
|
1532
|
-
},
|
|
1533
|
-
{
|
|
1534
|
-
tabId: "tab-1",
|
|
1535
|
-
chunkIndex: 3,
|
|
1536
|
-
chunkStartOffsetMs: 45000,
|
|
1537
|
-
chunkEndOffsetMs: 60000,
|
|
1538
|
-
eventCount: 100,
|
|
1539
|
-
hasFullSnapshot: false,
|
|
1540
|
-
chunkPayloadBytes: 1024,
|
|
1541
|
-
errorCount: 0,
|
|
1542
|
-
rageClickCount: 0,
|
|
1543
|
-
deadClickCount: 0,
|
|
1544
|
-
errorClickCount: 0,
|
|
1545
|
-
refreshRageCount: 0,
|
|
1546
|
-
routeCount: 0,
|
|
1547
|
-
},
|
|
1548
|
-
]) as never,
|
|
1549
|
-
);
|
|
1729
|
+
return principal;
|
|
1730
|
+
}
|
|
1550
1731
|
|
|
1551
|
-
|
|
1552
|
-
|
|
1732
|
+
test("the new quick filters, url prefix and tags reach the query as HAVING predicates", async () => {
|
|
1733
|
+
const principal: {
|
|
1734
|
+
request: JSONObject;
|
|
1735
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
1736
|
+
} = ownerPrincipal();
|
|
1737
|
+
|
|
1738
|
+
await callRoute({
|
|
1739
|
+
uri: LIST_ROUTE,
|
|
1553
1740
|
request: principal.request,
|
|
1554
|
-
body: {
|
|
1741
|
+
body: {
|
|
1742
|
+
rumApplicationId: applicationAId.toString(),
|
|
1743
|
+
filters: {
|
|
1744
|
+
hasIdentifiedUser: true,
|
|
1745
|
+
isPlayable: true,
|
|
1746
|
+
hasTraces: true,
|
|
1747
|
+
urlPrefix: "/checkout",
|
|
1748
|
+
tags: { build: "1.2.3", ignored: 4 },
|
|
1749
|
+
},
|
|
1750
|
+
},
|
|
1555
1751
|
});
|
|
1556
1752
|
|
|
1557
|
-
const statement: Statement =
|
|
1753
|
+
const statement: Statement = headerQuerySpy.mock
|
|
1754
|
+
.calls[0]![0] as Statement;
|
|
1755
|
+
const having: string = statement.query.substring(
|
|
1756
|
+
statement.query.indexOf("HAVING 1 = 1"),
|
|
1757
|
+
);
|
|
1558
1758
|
|
|
1559
|
-
expect(
|
|
1560
|
-
expect(
|
|
1561
|
-
expect(
|
|
1759
|
+
expect(having).toContain("aggIdentifiedUserKey != ''");
|
|
1760
|
+
expect(having).toContain("aggIsFinalized = 0 OR aggChunkCount > 0");
|
|
1761
|
+
expect(having).toContain("aggTraceCount > 0");
|
|
1762
|
+
expect(having).toContain("arrayExists(r -> startsWith(r, ");
|
|
1763
|
+
expect(having).toContain("mapContains(aggTags, ");
|
|
1764
|
+
|
|
1765
|
+
const bound: Array<unknown> = Object.values(statement.query_params);
|
|
1766
|
+
expect(bound).toContain("/checkout");
|
|
1767
|
+
expect(bound).toContain("build");
|
|
1768
|
+
expect(bound).toContain("1.2.3");
|
|
1769
|
+
/* A non-string tag value is not a filter. */
|
|
1770
|
+
expect(bound).not.toContain("ignored");
|
|
1771
|
+
});
|
|
1562
1772
|
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
*/
|
|
1569
|
-
for (const counterColumn of [
|
|
1570
|
-
"errorCount",
|
|
1571
|
-
"rageClickCount",
|
|
1572
|
-
"deadClickCount",
|
|
1573
|
-
"errorClickCount",
|
|
1574
|
-
"refreshRageCount",
|
|
1575
|
-
"routeCount",
|
|
1576
|
-
]) {
|
|
1577
|
-
expect(statement.query).toContain(counterColumn);
|
|
1578
|
-
}
|
|
1773
|
+
test("search is bound into the query, and names the label only for an identity-permitted caller", async () => {
|
|
1774
|
+
const owner: {
|
|
1775
|
+
request: JSONObject;
|
|
1776
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
1777
|
+
} = ownerPrincipal();
|
|
1579
1778
|
|
|
1580
|
-
|
|
1581
|
-
|
|
1779
|
+
await callRoute({
|
|
1780
|
+
uri: LIST_ROUTE,
|
|
1781
|
+
request: owner.request,
|
|
1782
|
+
body: {
|
|
1783
|
+
rumApplicationId: applicationAId.toString(),
|
|
1784
|
+
startTime: "2026-08-01T00:00:00.000Z",
|
|
1785
|
+
endTime: "2026-08-08T00:00:00.000Z",
|
|
1786
|
+
filters: { search: "jane" },
|
|
1787
|
+
},
|
|
1788
|
+
});
|
|
1789
|
+
|
|
1790
|
+
const permitted: Statement = headerQuerySpy.mock
|
|
1791
|
+
.calls[0]![0] as Statement;
|
|
1792
|
+
expect(permitted.query).toContain(
|
|
1793
|
+
"positionCaseInsensitiveUTF8(aggIdentifiedUserLabel, ",
|
|
1794
|
+
);
|
|
1795
|
+
expect(permitted.query).not.toContain("'jane'");
|
|
1796
|
+
expect(Object.values(permitted.query_params)).toContain("jane");
|
|
1797
|
+
|
|
1798
|
+
jest.clearAllMocks();
|
|
1799
|
+
|
|
1800
|
+
const telemetryAdmin: {
|
|
1801
|
+
request: JSONObject;
|
|
1802
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
1803
|
+
} = buildPrincipal({
|
|
1804
|
+
projectId: projectId,
|
|
1805
|
+
userId: userId,
|
|
1806
|
+
permissions: [Permission.TelemetryAdmin],
|
|
1807
|
+
});
|
|
1808
|
+
|
|
1809
|
+
mockProps(telemetryAdmin.databaseProps);
|
|
1810
|
+
mockApplication({ id: applicationAId, labelIds: [] });
|
|
1811
|
+
|
|
1812
|
+
await callRoute({
|
|
1813
|
+
uri: LIST_ROUTE,
|
|
1814
|
+
request: telemetryAdmin.request,
|
|
1815
|
+
body: {
|
|
1816
|
+
rumApplicationId: applicationAId.toString(),
|
|
1817
|
+
startTime: "2026-08-01T00:00:00.000Z",
|
|
1818
|
+
endTime: "2026-08-08T00:00:00.000Z",
|
|
1819
|
+
filters: { search: "jane" },
|
|
1820
|
+
},
|
|
1821
|
+
});
|
|
1822
|
+
|
|
1823
|
+
const gated: Statement = headerQuerySpy.mock.calls[0]![0] as Statement;
|
|
1824
|
+
expect(gated.query).toContain("startsWith(sessionId, ");
|
|
1825
|
+
expect(gated.query).not.toContain("identifiedUserLabel");
|
|
1826
|
+
});
|
|
1827
|
+
|
|
1828
|
+
test("a search longer than the cap is refused before any query", async () => {
|
|
1829
|
+
const principal: {
|
|
1830
|
+
request: JSONObject;
|
|
1831
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
1832
|
+
} = ownerPrincipal();
|
|
1833
|
+
|
|
1834
|
+
const result: CallResult = await callRoute({
|
|
1835
|
+
uri: LIST_ROUTE,
|
|
1836
|
+
request: principal.request,
|
|
1837
|
+
body: {
|
|
1838
|
+
rumApplicationId: applicationAId.toString(),
|
|
1839
|
+
filters: {
|
|
1840
|
+
search: "x".repeat(SESSION_REPLAY_LIST_SEARCH_MAX_LENGTH + 1),
|
|
1841
|
+
},
|
|
1842
|
+
},
|
|
1843
|
+
});
|
|
1844
|
+
|
|
1845
|
+
expect(result.deniedWith).toBeInstanceOf(BadDataException);
|
|
1846
|
+
expect(result.deniedWith?.message).toContain(
|
|
1847
|
+
`${SESSION_REPLAY_LIST_SEARCH_MAX_LENGTH}`,
|
|
1848
|
+
);
|
|
1849
|
+
expect(headerQuerySpy).not.toHaveBeenCalled();
|
|
1850
|
+
});
|
|
1851
|
+
|
|
1852
|
+
test("a search over a window wider than the cap says to narrow the range, distinctly", async () => {
|
|
1853
|
+
const principal: {
|
|
1854
|
+
request: JSONObject;
|
|
1855
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
1856
|
+
} = ownerPrincipal();
|
|
1857
|
+
|
|
1858
|
+
const endTime: Date = new Date("2026-08-08T00:00:00.000Z");
|
|
1859
|
+
const startTime: Date = new Date(
|
|
1860
|
+
endTime.getTime() -
|
|
1861
|
+
(SESSION_REPLAY_LIST_SEARCH_MAX_WINDOW_DAYS + 1) *
|
|
1862
|
+
24 *
|
|
1863
|
+
60 *
|
|
1864
|
+
60 *
|
|
1865
|
+
1000,
|
|
1866
|
+
);
|
|
1867
|
+
|
|
1868
|
+
const result: CallResult = await callRoute({
|
|
1869
|
+
uri: LIST_ROUTE,
|
|
1870
|
+
request: principal.request,
|
|
1871
|
+
body: {
|
|
1872
|
+
rumApplicationId: applicationAId.toString(),
|
|
1873
|
+
startTime: startTime.toISOString(),
|
|
1874
|
+
endTime: endTime.toISOString(),
|
|
1875
|
+
filters: { search: "acme" },
|
|
1876
|
+
},
|
|
1877
|
+
});
|
|
1878
|
+
|
|
1879
|
+
expect(result.deniedWith).toBeInstanceOf(BadDataException);
|
|
1880
|
+
expect(result.deniedWith?.message).toMatch(/narrow the range/i);
|
|
1881
|
+
expect(result.deniedWith?.message).toContain(
|
|
1882
|
+
`${SESSION_REPLAY_LIST_SEARCH_MAX_WINDOW_DAYS} days`,
|
|
1883
|
+
);
|
|
1884
|
+
expect(headerQuerySpy).not.toHaveBeenCalled();
|
|
1885
|
+
|
|
1886
|
+
/* The same window WITHOUT a search is fine. */
|
|
1887
|
+
jest.clearAllMocks();
|
|
1888
|
+
mockProps(principal.databaseProps);
|
|
1889
|
+
mockApplication({ id: applicationAId, labelIds: [] });
|
|
1890
|
+
|
|
1891
|
+
const unsearched: CallResult = await callRoute({
|
|
1892
|
+
uri: LIST_ROUTE,
|
|
1893
|
+
request: principal.request,
|
|
1894
|
+
body: {
|
|
1895
|
+
rumApplicationId: applicationAId.toString(),
|
|
1896
|
+
startTime: startTime.toISOString(),
|
|
1897
|
+
endTime: endTime.toISOString(),
|
|
1898
|
+
},
|
|
1899
|
+
});
|
|
1900
|
+
|
|
1901
|
+
expect(unsearched.deniedWith).toBeUndefined();
|
|
1902
|
+
expect(headerQuerySpy).toHaveBeenCalledTimes(1);
|
|
1903
|
+
});
|
|
1904
|
+
|
|
1905
|
+
test("an unknown sortBy is a bad request", async () => {
|
|
1906
|
+
const principal: {
|
|
1907
|
+
request: JSONObject;
|
|
1908
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
1909
|
+
} = ownerPrincipal();
|
|
1910
|
+
|
|
1911
|
+
const result: CallResult = await callRoute({
|
|
1912
|
+
uri: LIST_ROUTE,
|
|
1913
|
+
request: principal.request,
|
|
1914
|
+
body: {
|
|
1915
|
+
rumApplicationId: applicationAId.toString(),
|
|
1916
|
+
sortBy: "payloadBytes",
|
|
1917
|
+
},
|
|
1918
|
+
});
|
|
1919
|
+
|
|
1920
|
+
expect(result.deniedWith).toBeInstanceOf(BadDataException);
|
|
1921
|
+
expect(headerQuerySpy).not.toHaveBeenCalled();
|
|
1922
|
+
});
|
|
1923
|
+
|
|
1924
|
+
test("sortBy orders the query and the next cursor carries the sort key", async () => {
|
|
1925
|
+
const principal: {
|
|
1926
|
+
request: JSONObject;
|
|
1927
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
1928
|
+
} = ownerPrincipal();
|
|
1929
|
+
|
|
1930
|
+
headerQuerySpy.mockResolvedValue(
|
|
1931
|
+
fakeResultSet([
|
|
1932
|
+
{ sessionId: "a", aggErrorCount: 9, aggStartTime: 3 },
|
|
1933
|
+
{ sessionId: "b", aggErrorCount: 4, aggStartTime: 2 },
|
|
1934
|
+
]) as never,
|
|
1935
|
+
);
|
|
1936
|
+
|
|
1937
|
+
const result: CallResult = await callRoute({
|
|
1938
|
+
uri: LIST_ROUTE,
|
|
1939
|
+
request: principal.request,
|
|
1940
|
+
body: {
|
|
1941
|
+
rumApplicationId: applicationAId.toString(),
|
|
1942
|
+
sortBy: "errorCount",
|
|
1943
|
+
limit: 1,
|
|
1944
|
+
},
|
|
1945
|
+
});
|
|
1946
|
+
|
|
1947
|
+
const statement: Statement = headerQuerySpy.mock
|
|
1948
|
+
.calls[0]![0] as Statement;
|
|
1949
|
+
expect(statement.query).toContain(
|
|
1950
|
+
"ORDER BY aggErrorCount DESC, sessionId DESC",
|
|
1951
|
+
);
|
|
1952
|
+
|
|
1953
|
+
expect((result.jsonBody as JSONObject)["nextCursor"]).toEqual({
|
|
1954
|
+
sortBy: "errorCount",
|
|
1955
|
+
sortValue: 9,
|
|
1956
|
+
sessionId: "a",
|
|
1957
|
+
});
|
|
1958
|
+
});
|
|
1959
|
+
|
|
1960
|
+
test("the newest-first list still emits and accepts the legacy cursor shape", async () => {
|
|
1961
|
+
const principal: {
|
|
1962
|
+
request: JSONObject;
|
|
1963
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
1964
|
+
} = ownerPrincipal();
|
|
1965
|
+
|
|
1966
|
+
headerQuerySpy.mockResolvedValue(
|
|
1967
|
+
fakeResultSet([
|
|
1968
|
+
{ sessionId: "a", aggStartTime: 1700000002000 },
|
|
1969
|
+
{ sessionId: "b", aggStartTime: 1700000001000 },
|
|
1970
|
+
]) as never,
|
|
1971
|
+
);
|
|
1972
|
+
|
|
1973
|
+
const firstPage: CallResult = await callRoute({
|
|
1974
|
+
uri: LIST_ROUTE,
|
|
1975
|
+
request: principal.request,
|
|
1976
|
+
body: { rumApplicationId: applicationAId.toString(), limit: 1 },
|
|
1977
|
+
});
|
|
1978
|
+
|
|
1979
|
+
expect((firstPage.jsonBody as JSONObject)["nextCursor"]).toEqual({
|
|
1980
|
+
startTimeUnixMs: 1700000002000,
|
|
1981
|
+
sessionId: "a",
|
|
1982
|
+
});
|
|
1983
|
+
|
|
1984
|
+
jest.clearAllMocks();
|
|
1985
|
+
mockProps(principal.databaseProps);
|
|
1986
|
+
mockApplication({ id: applicationAId, labelIds: [] });
|
|
1987
|
+
|
|
1988
|
+
await callRoute({
|
|
1989
|
+
uri: LIST_ROUTE,
|
|
1990
|
+
request: principal.request,
|
|
1991
|
+
body: {
|
|
1992
|
+
rumApplicationId: applicationAId.toString(),
|
|
1993
|
+
limit: 1,
|
|
1994
|
+
cursor: { startTimeUnixMs: 1700000002000, sessionId: "a" },
|
|
1995
|
+
},
|
|
1996
|
+
});
|
|
1997
|
+
|
|
1998
|
+
const statement: Statement = headerQuerySpy.mock
|
|
1999
|
+
.calls[0]![0] as Statement;
|
|
2000
|
+
expect(statement.query).toMatch(
|
|
2001
|
+
/AND \(aggStartTime < \{p\d+:Double\} OR \(aggStartTime = \{p\d+:Double\} AND sessionId < \{p\d+:String\}\)\)/,
|
|
2002
|
+
);
|
|
2003
|
+
expect(Object.values(statement.query_params)).toContain("a");
|
|
2004
|
+
});
|
|
2005
|
+
|
|
2006
|
+
test("a cursor from another ordering, or a malformed one, is refused", async () => {
|
|
2007
|
+
const principal: {
|
|
2008
|
+
request: JSONObject;
|
|
2009
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
2010
|
+
} = ownerPrincipal();
|
|
2011
|
+
|
|
2012
|
+
const mismatched: CallResult = await callRoute({
|
|
2013
|
+
uri: LIST_ROUTE,
|
|
2014
|
+
request: principal.request,
|
|
2015
|
+
body: {
|
|
2016
|
+
rumApplicationId: applicationAId.toString(),
|
|
2017
|
+
sortBy: "durationMs",
|
|
2018
|
+
cursor: { sortBy: "errorCount", sortValue: 3, sessionId: "a" },
|
|
2019
|
+
},
|
|
2020
|
+
});
|
|
2021
|
+
|
|
2022
|
+
expect(mismatched.thrownToNext).toBeInstanceOf(BadDataException);
|
|
2023
|
+
|
|
2024
|
+
jest.clearAllMocks();
|
|
2025
|
+
mockProps(principal.databaseProps);
|
|
2026
|
+
mockApplication({ id: applicationAId, labelIds: [] });
|
|
2027
|
+
|
|
2028
|
+
const malformed: CallResult = await callRoute({
|
|
2029
|
+
uri: LIST_ROUTE,
|
|
2030
|
+
request: principal.request,
|
|
2031
|
+
body: {
|
|
2032
|
+
rumApplicationId: applicationAId.toString(),
|
|
2033
|
+
cursor: { nonsense: true },
|
|
2034
|
+
},
|
|
2035
|
+
});
|
|
2036
|
+
|
|
2037
|
+
expect(malformed.deniedWith).toBeInstanceOf(BadDataException);
|
|
2038
|
+
expect(headerQuerySpy).not.toHaveBeenCalled();
|
|
2039
|
+
});
|
|
2040
|
+
|
|
2041
|
+
/*
|
|
2042
|
+
* parseSessionReplayListCursor only checks Number.isFinite, so 1e300
|
|
2043
|
+
* passes it - and for the startTime ordering it becomes
|
|
2044
|
+
* `new Date(1e300)`, an Invalid Date that renders as NaN text and is
|
|
2045
|
+
* rejected by the ClickHouse driver. A crafted or corrupted cursor
|
|
2046
|
+
* answered 500 instead of the 400 the handler promises.
|
|
2047
|
+
*/
|
|
2048
|
+
test("a cursor sortValue that cannot be bound is a 400, never a driver error", async () => {
|
|
2049
|
+
const principal: {
|
|
2050
|
+
request: JSONObject;
|
|
2051
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
2052
|
+
} = ownerPrincipal();
|
|
2053
|
+
|
|
2054
|
+
const absurd: CallResult = await callRoute({
|
|
2055
|
+
uri: LIST_ROUTE,
|
|
2056
|
+
request: principal.request,
|
|
2057
|
+
body: {
|
|
2058
|
+
rumApplicationId: applicationAId.toString(),
|
|
2059
|
+
cursor: { startTimeUnixMs: 1e300, sessionId: "a" },
|
|
2060
|
+
},
|
|
2061
|
+
});
|
|
2062
|
+
|
|
2063
|
+
expect(absurd.deniedWith).toBeInstanceOf(BadDataException);
|
|
2064
|
+
expect(headerQuerySpy).not.toHaveBeenCalled();
|
|
2065
|
+
|
|
2066
|
+
jest.clearAllMocks();
|
|
2067
|
+
mockProps(principal.databaseProps);
|
|
2068
|
+
mockApplication({ id: applicationAId, labelIds: [] });
|
|
2069
|
+
|
|
2070
|
+
/* A count cannot be negative, so no page begins there either. */
|
|
2071
|
+
const negative: CallResult = await callRoute({
|
|
2072
|
+
uri: LIST_ROUTE,
|
|
2073
|
+
request: principal.request,
|
|
2074
|
+
body: {
|
|
2075
|
+
rumApplicationId: applicationAId.toString(),
|
|
2076
|
+
sortBy: "errorCount",
|
|
2077
|
+
cursor: { sortBy: "errorCount", sortValue: -1, sessionId: "a" },
|
|
2078
|
+
},
|
|
2079
|
+
});
|
|
2080
|
+
|
|
2081
|
+
expect(negative.deniedWith).toBeInstanceOf(BadDataException);
|
|
2082
|
+
expect(headerQuerySpy).not.toHaveBeenCalled();
|
|
2083
|
+
});
|
|
2084
|
+
|
|
2085
|
+
test.each([2.5, 0, -1, "20"])(
|
|
2086
|
+
"limit %p is a bad request rather than a ClickHouse error",
|
|
2087
|
+
async (limit: unknown) => {
|
|
2088
|
+
const principal: {
|
|
2089
|
+
request: JSONObject;
|
|
2090
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
2091
|
+
} = ownerPrincipal();
|
|
2092
|
+
|
|
2093
|
+
const result: CallResult = await callRoute({
|
|
2094
|
+
uri: LIST_ROUTE,
|
|
2095
|
+
request: principal.request,
|
|
2096
|
+
body: {
|
|
2097
|
+
rumApplicationId: applicationAId.toString(),
|
|
2098
|
+
limit: limit as number,
|
|
2099
|
+
},
|
|
2100
|
+
});
|
|
2101
|
+
|
|
2102
|
+
expect(result.thrownToNext).toBeInstanceOf(BadDataException);
|
|
2103
|
+
expect((result.thrownToNext as Exception).message).toContain("limit");
|
|
2104
|
+
expect(headerQuerySpy).not.toHaveBeenCalled();
|
|
2105
|
+
},
|
|
2106
|
+
);
|
|
2107
|
+
|
|
2108
|
+
test("an unparseable startTime, or a window that ends before it starts, is a bad request", async () => {
|
|
2109
|
+
const principal: {
|
|
2110
|
+
request: JSONObject;
|
|
2111
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
2112
|
+
} = ownerPrincipal();
|
|
2113
|
+
|
|
2114
|
+
const garbage: CallResult = await callRoute({
|
|
2115
|
+
uri: LIST_ROUTE,
|
|
2116
|
+
request: principal.request,
|
|
2117
|
+
body: {
|
|
2118
|
+
rumApplicationId: applicationAId.toString(),
|
|
2119
|
+
startTime: "yesterday-ish",
|
|
2120
|
+
},
|
|
2121
|
+
});
|
|
2122
|
+
|
|
2123
|
+
expect(garbage.thrownToNext).toBeInstanceOf(BadDataException);
|
|
2124
|
+
expect(headerQuerySpy).not.toHaveBeenCalled();
|
|
2125
|
+
|
|
2126
|
+
jest.clearAllMocks();
|
|
2127
|
+
mockProps(principal.databaseProps);
|
|
2128
|
+
mockApplication({ id: applicationAId, labelIds: [] });
|
|
2129
|
+
|
|
2130
|
+
const inverted: CallResult = await callRoute({
|
|
2131
|
+
uri: LIST_ROUTE,
|
|
2132
|
+
request: principal.request,
|
|
2133
|
+
body: {
|
|
2134
|
+
rumApplicationId: applicationAId.toString(),
|
|
2135
|
+
startTime: "2026-08-08T00:00:00.000Z",
|
|
2136
|
+
endTime: "2026-08-01T00:00:00.000Z",
|
|
2137
|
+
},
|
|
2138
|
+
});
|
|
2139
|
+
|
|
2140
|
+
expect(inverted.deniedWith).toBeInstanceOf(BadDataException);
|
|
2141
|
+
expect(inverted.deniedWith?.message).toContain("startTime");
|
|
2142
|
+
expect(headerQuerySpy).not.toHaveBeenCalled();
|
|
2143
|
+
});
|
|
2144
|
+
|
|
2145
|
+
test("projects the new list columns, with traits only for an identity-permitted caller", async () => {
|
|
2146
|
+
const principal: {
|
|
2147
|
+
request: JSONObject;
|
|
2148
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
2149
|
+
} = ownerPrincipal();
|
|
2150
|
+
|
|
2151
|
+
headerQuerySpy.mockResolvedValue(
|
|
2152
|
+
fakeResultSet([
|
|
2153
|
+
{
|
|
2154
|
+
sessionId: "a",
|
|
2155
|
+
applicationId: applicationAId.toString(),
|
|
2156
|
+
aggStartTime: 1700000000000,
|
|
2157
|
+
aggEndTime: 1700000090000,
|
|
2158
|
+
aggRoutes: ["/a", "/b"],
|
|
2159
|
+
aggTraceCount: 2,
|
|
2160
|
+
aggExceptionGroupCount: 1,
|
|
2161
|
+
aggClickCount: 12,
|
|
2162
|
+
aggActiveMs: 50000,
|
|
2163
|
+
aggFirstErrorOffsetMs: 3000,
|
|
2164
|
+
aggExpiresAt: 1700604800000,
|
|
2165
|
+
aggTags: { env: "prod" },
|
|
2166
|
+
aggIdentifiedUserLabel: "jane@example.com",
|
|
2167
|
+
aggIdentifiedUserTraits: { plan: "pro" },
|
|
2168
|
+
},
|
|
2169
|
+
]) as never,
|
|
2170
|
+
);
|
|
2171
|
+
|
|
2172
|
+
const result: CallResult = await callRoute({
|
|
2173
|
+
uri: LIST_ROUTE,
|
|
2174
|
+
request: principal.request,
|
|
2175
|
+
body: { rumApplicationId: applicationAId.toString() },
|
|
2176
|
+
});
|
|
2177
|
+
|
|
2178
|
+
const sessions: Array<JSONObject> = (result.jsonBody as JSONObject)[
|
|
2179
|
+
"sessions"
|
|
2180
|
+
] as unknown as Array<JSONObject>;
|
|
2181
|
+
const row: JSONObject = sessions[0]!;
|
|
2182
|
+
|
|
2183
|
+
expect(row["routes"]).toEqual(["/a", "/b"]);
|
|
2184
|
+
expect(row["traceCount"]).toBe(2);
|
|
2185
|
+
expect(row["exceptionGroupCount"]).toBe(1);
|
|
2186
|
+
expect(row["clickCount"]).toBe(12);
|
|
2187
|
+
expect(row["activeMs"]).toBe(50000);
|
|
2188
|
+
expect(row["firstErrorOffsetMs"]).toBe(3000);
|
|
2189
|
+
expect(row["expiresAtUnixMs"]).toBe(1700604800000);
|
|
2190
|
+
expect(row["tags"]).toEqual({ env: "prod" });
|
|
2191
|
+
expect(row["startTimeUnixMs"]).toBe(1700000000000);
|
|
2192
|
+
expect(row["endTimeUnixMs"]).toBe(1700000090000);
|
|
2193
|
+
expect(row["identifiedUserLabel"]).toBe("jane@example.com");
|
|
2194
|
+
expect(row["identifiedUserTraits"]).toEqual({ plan: "pro" });
|
|
2195
|
+
});
|
|
2196
|
+
});
|
|
2197
|
+
|
|
2198
|
+
describe("manifest", () => {
|
|
2199
|
+
test("never names the payload column and writes the read audit row", async () => {
|
|
2200
|
+
const principal: {
|
|
2201
|
+
request: JSONObject;
|
|
2202
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
2203
|
+
} = buildPrincipal({
|
|
2204
|
+
projectId: projectId,
|
|
2205
|
+
userId: userId,
|
|
2206
|
+
permissions: [Permission.ReadRumSessionReplayPayload],
|
|
2207
|
+
});
|
|
2208
|
+
|
|
2209
|
+
mockProps(principal.databaseProps);
|
|
2210
|
+
mockSessionHeader(applicationAId);
|
|
2211
|
+
mockApplication({ id: applicationAId, labelIds: [] });
|
|
2212
|
+
const viewId: ObjectID = mockRecordedView();
|
|
2213
|
+
|
|
2214
|
+
chunkQuerySpy.mockResolvedValue(
|
|
2215
|
+
fakeResultSet([
|
|
2216
|
+
{
|
|
2217
|
+
tabId: "tab-1",
|
|
2218
|
+
chunkIndex: 0,
|
|
2219
|
+
chunkStartOffsetMs: 0,
|
|
2220
|
+
chunkEndOffsetMs: 15000,
|
|
2221
|
+
eventCount: 100,
|
|
2222
|
+
hasFullSnapshot: true,
|
|
2223
|
+
chunkPayloadBytes: 1024,
|
|
2224
|
+
errorCount: 2,
|
|
2225
|
+
rageClickCount: 1,
|
|
2226
|
+
deadClickCount: 4,
|
|
2227
|
+
errorClickCount: 6,
|
|
2228
|
+
refreshRageCount: 5,
|
|
2229
|
+
routeCount: 3,
|
|
2230
|
+
},
|
|
2231
|
+
{
|
|
2232
|
+
tabId: "tab-1",
|
|
2233
|
+
chunkIndex: 3,
|
|
2234
|
+
chunkStartOffsetMs: 45000,
|
|
2235
|
+
chunkEndOffsetMs: 60000,
|
|
2236
|
+
eventCount: 100,
|
|
2237
|
+
hasFullSnapshot: false,
|
|
2238
|
+
chunkPayloadBytes: 1024,
|
|
2239
|
+
errorCount: 0,
|
|
2240
|
+
rageClickCount: 0,
|
|
2241
|
+
deadClickCount: 0,
|
|
2242
|
+
errorClickCount: 0,
|
|
2243
|
+
refreshRageCount: 0,
|
|
2244
|
+
routeCount: 0,
|
|
2245
|
+
},
|
|
2246
|
+
]) as never,
|
|
2247
|
+
);
|
|
2248
|
+
|
|
2249
|
+
const result: CallResult = await callRoute({
|
|
2250
|
+
uri: MANIFEST_ROUTE,
|
|
2251
|
+
request: principal.request,
|
|
2252
|
+
body: { sessionId: "session-1", accessReason: "incident triage" },
|
|
2253
|
+
});
|
|
2254
|
+
|
|
2255
|
+
const statement: Statement = chunkQuerySpy.mock.calls[0]![0] as Statement;
|
|
2256
|
+
|
|
2257
|
+
expect(statement.query).not.toContain("payload,");
|
|
2258
|
+
expect(statement.query).not.toMatch(/\bpayload\b(?!Bytes)/);
|
|
2259
|
+
expect(statement.query).toContain("LIMIT 1 BY tabId, chunkIndex");
|
|
2260
|
+
|
|
2261
|
+
/*
|
|
2262
|
+
* The signal counters must be projected: the player's error,
|
|
2263
|
+
* frustration and route timeline lanes and the "next error" jump are
|
|
2264
|
+
* fed exclusively by these per-chunk columns. Dropping one from the
|
|
2265
|
+
* SELECT silently blanks a lane — it parses as 0 client-side.
|
|
2266
|
+
*/
|
|
2267
|
+
for (const counterColumn of [
|
|
2268
|
+
"errorCount",
|
|
2269
|
+
"rageClickCount",
|
|
2270
|
+
"deadClickCount",
|
|
2271
|
+
"errorClickCount",
|
|
2272
|
+
"refreshRageCount",
|
|
2273
|
+
"routeCount",
|
|
2274
|
+
]) {
|
|
2275
|
+
expect(statement.query).toContain(counterColumn);
|
|
2276
|
+
}
|
|
2277
|
+
|
|
2278
|
+
expect(recordViewSpy).toHaveBeenCalledTimes(1);
|
|
2279
|
+
const auditArgs: JSONObject = recordViewSpy.mock
|
|
1582
2280
|
.calls[0]![0] as JSONObject;
|
|
1583
2281
|
expect((auditArgs["projectId"] as ObjectID).toString()).toBe(
|
|
1584
2282
|
projectId.toString(),
|
|
1585
2283
|
);
|
|
1586
|
-
expect((auditArgs["rumApplicationId"] as ObjectID).toString()).toBe(
|
|
1587
|
-
applicationAId.toString(),
|
|
2284
|
+
expect((auditArgs["rumApplicationId"] as ObjectID).toString()).toBe(
|
|
2285
|
+
applicationAId.toString(),
|
|
2286
|
+
);
|
|
2287
|
+
expect(auditArgs["sessionId"]).toBe("session-1");
|
|
2288
|
+
expect(auditArgs["accessReason"]).toBe("incident triage");
|
|
2289
|
+
expect(auditArgs["ipAddress"]).toBe("203.0.113.7");
|
|
2290
|
+
|
|
2291
|
+
const body: JSONObject = result.jsonBody as JSONObject;
|
|
2292
|
+
expect(body["viewId"]).toBe(viewId.toString());
|
|
2293
|
+
|
|
2294
|
+
const tabs: Array<JSONObject> = body[
|
|
2295
|
+
"tabs"
|
|
2296
|
+
] as unknown as Array<JSONObject>;
|
|
2297
|
+
expect(tabs).toHaveLength(1);
|
|
2298
|
+
expect(tabs[0]!["chunkIndexes"]).toEqual([0, 3]);
|
|
2299
|
+
expect(tabs[0]!["fullSnapshotChunkIndexes"]).toEqual([0]);
|
|
2300
|
+
|
|
2301
|
+
/*
|
|
2302
|
+
* Every counter gets a DISTINCT nonzero value, asserted individually:
|
|
2303
|
+
* the client parses missing keys to 0, so a counter dropped from the
|
|
2304
|
+
* SELECT (or two counters cross-wired in the row mapping) fails
|
|
2305
|
+
* loudly here instead of silently blanking a timeline lane.
|
|
2306
|
+
*/
|
|
2307
|
+
const manifestChunks: Array<JSONObject> = tabs[0]![
|
|
2308
|
+
"chunks"
|
|
2309
|
+
] as unknown as Array<JSONObject>;
|
|
2310
|
+
expect(manifestChunks[0]!["errorCount"]).toBe(2);
|
|
2311
|
+
expect(manifestChunks[0]!["rageClickCount"]).toBe(1);
|
|
2312
|
+
expect(manifestChunks[0]!["deadClickCount"]).toBe(4);
|
|
2313
|
+
expect(manifestChunks[0]!["errorClickCount"]).toBe(6);
|
|
2314
|
+
expect(manifestChunks[0]!["refreshRageCount"]).toBe(5);
|
|
2315
|
+
expect(manifestChunks[0]!["routeCount"]).toBe(3);
|
|
2316
|
+
expect(manifestChunks[1]!["errorCount"]).toBe(0);
|
|
2317
|
+
|
|
2318
|
+
/*
|
|
2319
|
+
* The hole between chunk 0 and chunk 3 must be reported. Playback
|
|
2320
|
+
* that silently crosses a gap renders a DOM the user never saw.
|
|
2321
|
+
*/
|
|
2322
|
+
const gaps: Array<JSONObject> = tabs[0]![
|
|
2323
|
+
"gaps"
|
|
2324
|
+
] as unknown as Array<JSONObject>;
|
|
2325
|
+
expect(gaps).toHaveLength(1);
|
|
2326
|
+
expect(gaps[0]!["fromIndex"]).toBe(0);
|
|
2327
|
+
expect(gaps[0]!["toIndex"]).toBe(3);
|
|
2328
|
+
expect(gaps[0]!["missingMs"]).toBe(30000);
|
|
2329
|
+
});
|
|
2330
|
+
|
|
2331
|
+
test("a session that does not exist in this tenant is refused before any audit row", async () => {
|
|
2332
|
+
const principal: {
|
|
2333
|
+
request: JSONObject;
|
|
2334
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
2335
|
+
} = buildPrincipal({
|
|
2336
|
+
projectId: projectId,
|
|
2337
|
+
userId: userId,
|
|
2338
|
+
permissions: [Permission.ProjectOwner],
|
|
2339
|
+
});
|
|
2340
|
+
|
|
2341
|
+
mockProps(principal.databaseProps);
|
|
2342
|
+
headerQuerySpy.mockResolvedValue(fakeResultSet([]) as never);
|
|
2343
|
+
|
|
2344
|
+
const result: CallResult = await callRoute({
|
|
2345
|
+
uri: MANIFEST_ROUTE,
|
|
2346
|
+
request: principal.request,
|
|
2347
|
+
body: { sessionId: "does-not-exist" },
|
|
2348
|
+
});
|
|
2349
|
+
|
|
2350
|
+
expect(result.thrownToNext).toBeInstanceOf(NotFoundException);
|
|
2351
|
+
expect((result.thrownToNext as Exception).message).toMatch(/^not-found:/);
|
|
2352
|
+
expect(recordViewSpy).not.toHaveBeenCalled();
|
|
2353
|
+
});
|
|
2354
|
+
|
|
2355
|
+
/*
|
|
2356
|
+
* A link from an incident a week later must say "expired on <date>",
|
|
2357
|
+
* not "not found": the two need different actions from the reader.
|
|
2358
|
+
* The retention-filtered header read finds nothing, so a second read
|
|
2359
|
+
* without the filter answers when the row aged out - and only dates
|
|
2360
|
+
* and the application id, never a row's content.
|
|
2361
|
+
*/
|
|
2362
|
+
test("an expired session is a distinguishable 404 that names the expiry", async () => {
|
|
2363
|
+
const principal: {
|
|
2364
|
+
request: JSONObject;
|
|
2365
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
2366
|
+
} = buildPrincipal({
|
|
2367
|
+
projectId: projectId,
|
|
2368
|
+
userId: userId,
|
|
2369
|
+
permissions: [Permission.ProjectOwner],
|
|
2370
|
+
});
|
|
2371
|
+
|
|
2372
|
+
mockProps(principal.databaseProps);
|
|
2373
|
+
headerQuerySpy
|
|
2374
|
+
.mockResolvedValueOnce(fakeResultSet([]) as never)
|
|
2375
|
+
.mockResolvedValueOnce(
|
|
2376
|
+
fakeResultSet([
|
|
2377
|
+
{
|
|
2378
|
+
applicationId: applicationAId.toString(),
|
|
2379
|
+
expiresAtUnixMs: Date.UTC(2026, 7, 8),
|
|
2380
|
+
startTimeUnixMs: Date.UTC(2026, 7, 1),
|
|
2381
|
+
},
|
|
2382
|
+
]) as never,
|
|
2383
|
+
);
|
|
2384
|
+
|
|
2385
|
+
const result: CallResult = await callRoute({
|
|
2386
|
+
uri: MANIFEST_ROUTE,
|
|
2387
|
+
request: principal.request,
|
|
2388
|
+
body: { sessionId: "session-old" },
|
|
2389
|
+
});
|
|
2390
|
+
|
|
2391
|
+
expect(result.thrownToNext).toBeInstanceOf(NotFoundException);
|
|
2392
|
+
const message: string = (result.thrownToNext as Exception).message;
|
|
2393
|
+
expect(message).toMatch(/^expired:/);
|
|
2394
|
+
expect(message).toContain("2026-08-08");
|
|
2395
|
+
expect(message).toContain("7-day retention");
|
|
2396
|
+
|
|
2397
|
+
const expiryStatement: Statement = headerQuerySpy.mock
|
|
2398
|
+
.calls[1]![0] as Statement;
|
|
2399
|
+
expect(expiryStatement.query).not.toContain("retentionDate >= now()");
|
|
2400
|
+
expect(recordViewSpy).not.toHaveBeenCalled();
|
|
2401
|
+
});
|
|
2402
|
+
|
|
2403
|
+
/*
|
|
2404
|
+
* The expiry answer discloses that a recording existed, when it
|
|
2405
|
+
* started, and what retention the owning application runs - and it is
|
|
2406
|
+
* produced on the path where there is no header left to authorize
|
|
2407
|
+
* against, so it used to be answered BEFORE any access check. A
|
|
2408
|
+
* label-scoped reviewer could therefore probe session ids and learn all
|
|
2409
|
+
* three for applications outside their scope: exactly the existence
|
|
2410
|
+
* probing assertSessionReplayApplicationAccess refuses "the same way"
|
|
2411
|
+
* for a session that does exist.
|
|
2412
|
+
*/
|
|
2413
|
+
test("the expiry answer is withheld from a caller outside the owning application's scope", async () => {
|
|
2414
|
+
const principal: {
|
|
2415
|
+
request: JSONObject;
|
|
2416
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
2417
|
+
} = buildPrincipal({
|
|
2418
|
+
projectId: projectId,
|
|
2419
|
+
userId: userId,
|
|
2420
|
+
permissions: [Permission.ReadRumSessionReplayPayload],
|
|
2421
|
+
/* Scoped to application B's label; the expired recording is A's. */
|
|
2422
|
+
labelIds: [labelBId],
|
|
2423
|
+
});
|
|
2424
|
+
|
|
2425
|
+
mockProps(principal.databaseProps);
|
|
2426
|
+
mockApplication({ id: applicationAId, labelIds: [labelAId] });
|
|
2427
|
+
|
|
2428
|
+
headerQuerySpy
|
|
2429
|
+
.mockResolvedValueOnce(fakeResultSet([]) as never)
|
|
2430
|
+
.mockResolvedValueOnce(
|
|
2431
|
+
fakeResultSet([
|
|
2432
|
+
{
|
|
2433
|
+
applicationId: applicationAId.toString(),
|
|
2434
|
+
expiresAtUnixMs: Date.UTC(2026, 7, 8),
|
|
2435
|
+
startTimeUnixMs: Date.UTC(2026, 7, 1),
|
|
2436
|
+
},
|
|
2437
|
+
]) as never,
|
|
2438
|
+
);
|
|
2439
|
+
|
|
2440
|
+
const result: CallResult = await callRoute({
|
|
2441
|
+
uri: MANIFEST_ROUTE,
|
|
2442
|
+
request: principal.request,
|
|
2443
|
+
body: { sessionId: "session-old" },
|
|
2444
|
+
});
|
|
2445
|
+
|
|
2446
|
+
expect(result.thrownToNext).toBeInstanceOf(NotFoundException);
|
|
2447
|
+
|
|
2448
|
+
const message: string = (result.thrownToNext as Exception).message;
|
|
2449
|
+
|
|
2450
|
+
/* The generic answer: no date, no retention, no admission it existed. */
|
|
2451
|
+
expect(message).toMatch(/^not-found:/);
|
|
2452
|
+
expect(message).not.toContain("2026-08-08");
|
|
2453
|
+
expect(message).not.toContain("retention");
|
|
2454
|
+
});
|
|
2455
|
+
|
|
2456
|
+
test("projects the clock, expiry, tags and engagement counters on the header, and the per-tab first offset", async () => {
|
|
2457
|
+
const principal: {
|
|
2458
|
+
request: JSONObject;
|
|
2459
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
2460
|
+
} = buildPrincipal({
|
|
2461
|
+
projectId: projectId,
|
|
2462
|
+
userId: userId,
|
|
2463
|
+
permissions: [Permission.TelemetryAdmin],
|
|
2464
|
+
});
|
|
2465
|
+
|
|
2466
|
+
mockProps(principal.databaseProps);
|
|
2467
|
+
headerQuerySpy.mockResolvedValue(
|
|
2468
|
+
fakeResultSet([
|
|
2469
|
+
{
|
|
2470
|
+
...buildHeaderRow({
|
|
2471
|
+
sessionId: "session-1",
|
|
2472
|
+
projectId: projectId,
|
|
2473
|
+
rumApplicationId: applicationAId,
|
|
2474
|
+
}),
|
|
2475
|
+
aggClientReportedStart: 1699999999500,
|
|
2476
|
+
aggTags: { build: "1.2.3" },
|
|
2477
|
+
aggExpiresAt: 1700604800000,
|
|
2478
|
+
aggClickCount: 41,
|
|
2479
|
+
aggCustomEventCount: 3,
|
|
2480
|
+
aggActiveMs: 36000,
|
|
2481
|
+
aggFirstErrorOffsetMs: 12000,
|
|
2482
|
+
aggAttributes: { "recorder.capabilities": "click-events,tags" },
|
|
2483
|
+
},
|
|
2484
|
+
]) as never,
|
|
2485
|
+
);
|
|
2486
|
+
mockApplication({ id: applicationAId, labelIds: [] });
|
|
2487
|
+
mockRecordedView();
|
|
2488
|
+
|
|
2489
|
+
chunkQuerySpy.mockResolvedValue(
|
|
2490
|
+
fakeResultSet([
|
|
2491
|
+
{
|
|
2492
|
+
tabId: "tab-2",
|
|
2493
|
+
chunkIndex: 0,
|
|
2494
|
+
chunkStartOffsetMs: 134000,
|
|
2495
|
+
chunkEndOffsetMs: 150000,
|
|
2496
|
+
eventCount: 10,
|
|
2497
|
+
hasFullSnapshot: 1,
|
|
2498
|
+
chunkPayloadBytes: 10,
|
|
2499
|
+
clickCount: 2,
|
|
2500
|
+
url: "https://example.com/help",
|
|
2501
|
+
},
|
|
2502
|
+
]) as never,
|
|
1588
2503
|
);
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
2504
|
+
|
|
2505
|
+
const result: CallResult = await callRoute({
|
|
2506
|
+
uri: MANIFEST_ROUTE,
|
|
2507
|
+
request: principal.request,
|
|
2508
|
+
body: { sessionId: "session-1" },
|
|
2509
|
+
});
|
|
1592
2510
|
|
|
1593
2511
|
const body: JSONObject = result.jsonBody as JSONObject;
|
|
1594
|
-
|
|
2512
|
+
const header: JSONObject = body["header"] as JSONObject;
|
|
2513
|
+
|
|
2514
|
+
expect(header["startTimeUnixMs"]).toBe(1700000000000);
|
|
2515
|
+
expect(header["endTimeUnixMs"]).toBe(1700000060000);
|
|
2516
|
+
expect(header["clientReportedStartUnixMs"]).toBe(1699999999500);
|
|
2517
|
+
expect(header["tags"]).toEqual({ build: "1.2.3" });
|
|
2518
|
+
expect(header["expiresAtUnixMs"]).toBe(1700604800000);
|
|
2519
|
+
expect(header["clickCount"]).toBe(41);
|
|
2520
|
+
expect(header["customEventCount"]).toBe(3);
|
|
2521
|
+
expect(header["activeMs"]).toBe(36000);
|
|
2522
|
+
expect(header["firstErrorOffsetMs"]).toBe(12000);
|
|
2523
|
+
expect(header["recorderCapabilities"]).toEqual(["click-events", "tags"]);
|
|
1595
2524
|
|
|
1596
2525
|
const tabs: Array<JSONObject> = body[
|
|
1597
2526
|
"tabs"
|
|
1598
2527
|
] as unknown as Array<JSONObject>;
|
|
1599
|
-
expect(tabs).
|
|
1600
|
-
|
|
1601
|
-
expect(tabs[0]!["fullSnapshotChunkIndexes"]).toEqual([0]);
|
|
1602
|
-
|
|
1603
|
-
/*
|
|
1604
|
-
* Every counter gets a DISTINCT nonzero value, asserted individually:
|
|
1605
|
-
* the client parses missing keys to 0, so a counter dropped from the
|
|
1606
|
-
* SELECT (or two counters cross-wired in the row mapping) fails
|
|
1607
|
-
* loudly here instead of silently blanking a timeline lane.
|
|
1608
|
-
*/
|
|
1609
|
-
const manifestChunks: Array<JSONObject> = tabs[0]![
|
|
2528
|
+
expect(tabs[0]!["firstChunkStartOffsetMs"]).toBe(134000);
|
|
2529
|
+
const chunks: Array<JSONObject> = tabs[0]![
|
|
1610
2530
|
"chunks"
|
|
1611
2531
|
] as unknown as Array<JSONObject>;
|
|
1612
|
-
expect(
|
|
1613
|
-
expect(
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
2532
|
+
expect(chunks[0]!["clickCount"]).toBe(2);
|
|
2533
|
+
expect(chunks[0]!["url"]).toBe("https://example.com/help");
|
|
2534
|
+
});
|
|
2535
|
+
|
|
2536
|
+
/*
|
|
2537
|
+
* The identity columns carry the narrowest ACL in the schema. A
|
|
2538
|
+
* TelemetryAdmin can watch a recording but is deliberately excluded
|
|
2539
|
+
* from SESSION_REPLAY_IDENTITY_PERMISSIONS, so no statement issued on
|
|
2540
|
+
* their behalf may even NAME identifiedUserLabel or the traits map.
|
|
2541
|
+
*/
|
|
2542
|
+
test("omits identifiedUserLabel and traits, and never names them, without the identity permission", async () => {
|
|
2543
|
+
const principal: {
|
|
2544
|
+
request: JSONObject;
|
|
2545
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
2546
|
+
} = buildPrincipal({
|
|
2547
|
+
projectId: projectId,
|
|
2548
|
+
userId: userId,
|
|
2549
|
+
permissions: [Permission.TelemetryAdmin],
|
|
2550
|
+
});
|
|
2551
|
+
|
|
2552
|
+
mockProps(principal.databaseProps);
|
|
2553
|
+
mockSessionHeader(applicationAId);
|
|
2554
|
+
mockApplication({ id: applicationAId, labelIds: [] });
|
|
2555
|
+
mockRecordedView();
|
|
2556
|
+
|
|
2557
|
+
const result: CallResult = await callRoute({
|
|
2558
|
+
uri: MANIFEST_ROUTE,
|
|
2559
|
+
request: principal.request,
|
|
2560
|
+
body: { sessionId: "session-1" },
|
|
2561
|
+
});
|
|
2562
|
+
|
|
2563
|
+
for (const call of headerQuerySpy.mock.calls) {
|
|
2564
|
+
const statement: Statement = call[0] as Statement;
|
|
2565
|
+
expect(statement.query).not.toContain("identifiedUserLabel");
|
|
2566
|
+
expect(statement.query).not.toContain("identifiedUserTraits");
|
|
2567
|
+
}
|
|
2568
|
+
|
|
2569
|
+
const header: JSONObject = (result.jsonBody as JSONObject)[
|
|
2570
|
+
"header"
|
|
2571
|
+
] as JSONObject;
|
|
2572
|
+
expect(header["identifiedUserLabel"]).toBeUndefined();
|
|
2573
|
+
expect(header["identifiedUserTraits"]).toBeUndefined();
|
|
2574
|
+
});
|
|
2575
|
+
|
|
2576
|
+
test("includes identifiedUserLabel and traits, read by a separate application-pinned statement, with the identity permission", async () => {
|
|
2577
|
+
const principal: {
|
|
2578
|
+
request: JSONObject;
|
|
2579
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
2580
|
+
} = buildPrincipal({
|
|
2581
|
+
projectId: projectId,
|
|
2582
|
+
userId: userId,
|
|
2583
|
+
permissions: [Permission.ReadRumSessionReplayPayload],
|
|
2584
|
+
labelIds: [labelAId],
|
|
2585
|
+
});
|
|
2586
|
+
|
|
2587
|
+
mockProps(principal.databaseProps);
|
|
2588
|
+
headerQuerySpy.mockImplementation(async (statement: Statement) => {
|
|
2589
|
+
if (statement.query.includes("identifiedUserTraits")) {
|
|
2590
|
+
return fakeResultSet([
|
|
2591
|
+
{
|
|
2592
|
+
aggIdentifiedUserLabel: "jane@example.com",
|
|
2593
|
+
aggIdentifiedUserTraits: { plan: "pro" },
|
|
2594
|
+
},
|
|
2595
|
+
]);
|
|
2596
|
+
}
|
|
2597
|
+
|
|
2598
|
+
return fakeResultSet([
|
|
2599
|
+
buildHeaderRow({
|
|
2600
|
+
sessionId: "session-1",
|
|
2601
|
+
projectId: projectId,
|
|
2602
|
+
rumApplicationId: applicationAId,
|
|
2603
|
+
}),
|
|
2604
|
+
]);
|
|
2605
|
+
});
|
|
2606
|
+
mockApplication({ id: applicationAId, labelIds: [labelAId] });
|
|
2607
|
+
mockRecordedView();
|
|
2608
|
+
|
|
2609
|
+
const result: CallResult = await callRoute({
|
|
2610
|
+
uri: MANIFEST_ROUTE,
|
|
2611
|
+
request: principal.request,
|
|
2612
|
+
body: { sessionId: "session-1" },
|
|
2613
|
+
});
|
|
2614
|
+
|
|
2615
|
+
const identityStatements: Array<Statement> = headerQuerySpy.mock.calls
|
|
2616
|
+
.map((call: Array<unknown>): Statement => {
|
|
2617
|
+
return call[0] as Statement;
|
|
2618
|
+
})
|
|
2619
|
+
.filter((statement: Statement): boolean => {
|
|
2620
|
+
return statement.query.includes("identifiedUserLabel");
|
|
2621
|
+
});
|
|
2622
|
+
|
|
2623
|
+
expect(identityStatements).toHaveLength(1);
|
|
2624
|
+
expect(identityStatements[0]!.query).toContain(
|
|
2625
|
+
"argMax(identifiedUserTraits, version) AS aggIdentifiedUserTraits",
|
|
2626
|
+
);
|
|
2627
|
+
expect(identityStatements[0]!.query).toContain("rumApplicationId = ");
|
|
2628
|
+
expect(Object.values(identityStatements[0]!.query_params)).toContain(
|
|
2629
|
+
applicationAId.toString(),
|
|
2630
|
+
);
|
|
2631
|
+
|
|
2632
|
+
/* The header read itself still never names them. */
|
|
2633
|
+
const headerStatement: Statement = headerQuerySpy.mock
|
|
2634
|
+
.calls[0]![0] as Statement;
|
|
2635
|
+
expect(headerStatement.query).not.toContain("identifiedUserLabel");
|
|
2636
|
+
|
|
2637
|
+
const header: JSONObject = (result.jsonBody as JSONObject)[
|
|
2638
|
+
"header"
|
|
2639
|
+
] as JSONObject;
|
|
2640
|
+
expect(header["identifiedUserLabel"]).toBe("jane@example.com");
|
|
2641
|
+
expect(header["identifiedUserTraits"]).toEqual({ plan: "pro" });
|
|
2642
|
+
});
|
|
2643
|
+
|
|
2644
|
+
test("omits the identity columns when the identity grant is scoped to a different application", async () => {
|
|
2645
|
+
const principal: {
|
|
2646
|
+
request: JSONObject;
|
|
2647
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
2648
|
+
} = buildPrincipal({
|
|
2649
|
+
projectId: projectId,
|
|
2650
|
+
userId: userId,
|
|
2651
|
+
permissions: [Permission.ReadRumSessionReplayPayload],
|
|
2652
|
+
labelIds: [labelAId],
|
|
2653
|
+
});
|
|
2654
|
+
|
|
2655
|
+
mockProps(principal.databaseProps);
|
|
2656
|
+
mockSessionHeader(applicationBId);
|
|
2657
|
+
/* B carries label A too so the payload read is allowed... */
|
|
2658
|
+
mockApplication({ id: applicationBId, labelIds: [labelAId] });
|
|
2659
|
+
mockRecordedView();
|
|
2660
|
+
|
|
2661
|
+
const allowed: CallResult = await callRoute({
|
|
2662
|
+
uri: MANIFEST_ROUTE,
|
|
2663
|
+
request: principal.request,
|
|
2664
|
+
body: { sessionId: "session-1" },
|
|
2665
|
+
});
|
|
2666
|
+
expect(allowed.thrownToNext).toBeUndefined();
|
|
2667
|
+
|
|
2668
|
+
/* ...whereas with only a foreign label the whole read is refused. */
|
|
2669
|
+
jest.clearAllMocks();
|
|
2670
|
+
mockProps(principal.databaseProps);
|
|
2671
|
+
mockSessionHeader(applicationBId);
|
|
2672
|
+
mockApplication({ id: applicationBId, labelIds: [labelBId] });
|
|
2673
|
+
|
|
2674
|
+
const refused: CallResult = await callRoute({
|
|
2675
|
+
uri: MANIFEST_ROUTE,
|
|
2676
|
+
request: principal.request,
|
|
2677
|
+
body: { sessionId: "session-1" },
|
|
2678
|
+
});
|
|
2679
|
+
|
|
2680
|
+
expect(refused.thrownToNext).toBeInstanceOf(NotAuthorizedException);
|
|
2681
|
+
for (const call of headerQuerySpy.mock.calls) {
|
|
2682
|
+
expect((call[0] as Statement).query).not.toContain(
|
|
2683
|
+
"identifiedUserLabel",
|
|
2684
|
+
);
|
|
2685
|
+
}
|
|
2686
|
+
});
|
|
2687
|
+
|
|
2688
|
+
/*
|
|
2689
|
+
* Live polling. The player re-fetches the manifest every 30s while a
|
|
2690
|
+
* session is recording; each poll is the same viewing, and one audit
|
|
2691
|
+
* row per VIEW is the contract the E2E pins.
|
|
2692
|
+
*/
|
|
2693
|
+
test("isRefresh with the caller's own viewId for this session skips the audit write and echoes the id", async () => {
|
|
2694
|
+
const principal: {
|
|
2695
|
+
request: JSONObject;
|
|
2696
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
2697
|
+
} = buildPrincipal({
|
|
2698
|
+
projectId: projectId,
|
|
2699
|
+
userId: userId,
|
|
2700
|
+
permissions: [Permission.ReadRumSessionReplayPayload],
|
|
2701
|
+
});
|
|
2702
|
+
|
|
2703
|
+
mockProps(principal.databaseProps);
|
|
2704
|
+
mockSessionHeader(applicationAId);
|
|
2705
|
+
mockApplication({ id: applicationAId, labelIds: [] });
|
|
2706
|
+
|
|
2707
|
+
const viewId: ObjectID = ObjectID.generate();
|
|
2708
|
+
mockOwnView({ viewId: viewId, rumApplicationId: applicationAId });
|
|
2709
|
+
|
|
2710
|
+
const result: CallResult = await callRoute({
|
|
2711
|
+
uri: MANIFEST_ROUTE,
|
|
2712
|
+
request: principal.request,
|
|
2713
|
+
body: {
|
|
2714
|
+
sessionId: "session-1",
|
|
2715
|
+
isRefresh: true,
|
|
2716
|
+
viewId: viewId.toString(),
|
|
2717
|
+
},
|
|
2718
|
+
});
|
|
2719
|
+
|
|
2720
|
+
expect(recordViewSpy).not.toHaveBeenCalled();
|
|
2721
|
+
expect((result.jsonBody as JSONObject)["viewId"]).toBe(viewId.toString());
|
|
2722
|
+
|
|
2723
|
+
/* Ownership AND the session are in the lookup's own predicate. */
|
|
2724
|
+
const lookupArgs: JSONObject = viewFindOneBySpy.mock
|
|
2725
|
+
.calls[0]![0] as JSONObject;
|
|
2726
|
+
const query: JSONObject = lookupArgs["query"] as JSONObject;
|
|
2727
|
+
expect((query["viewedByUserId"] as ObjectID).toString()).toBe(
|
|
2728
|
+
userId.toString(),
|
|
2729
|
+
);
|
|
2730
|
+
expect(query["sessionId"]).toBe("session-1");
|
|
2731
|
+
expect(query["_id"]).toBe(viewId.toString());
|
|
2732
|
+
});
|
|
2733
|
+
|
|
2734
|
+
test("isRefresh with a viewId that is not the caller's own for this session records a fresh view", async () => {
|
|
2735
|
+
const principal: {
|
|
2736
|
+
request: JSONObject;
|
|
2737
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
2738
|
+
} = buildPrincipal({
|
|
2739
|
+
projectId: projectId,
|
|
2740
|
+
userId: userId,
|
|
2741
|
+
permissions: [Permission.ReadRumSessionReplayPayload],
|
|
2742
|
+
});
|
|
2743
|
+
|
|
2744
|
+
mockProps(principal.databaseProps);
|
|
2745
|
+
mockSessionHeader(applicationAId);
|
|
2746
|
+
mockApplication({ id: applicationAId, labelIds: [] });
|
|
2747
|
+
const freshViewId: ObjectID = mockRecordedView();
|
|
2748
|
+
|
|
2749
|
+
/* The row is somebody else's, or for another session: no match. */
|
|
2750
|
+
viewFindOneBySpy.mockResolvedValue(null);
|
|
2751
|
+
|
|
2752
|
+
const result: CallResult = await callRoute({
|
|
2753
|
+
uri: MANIFEST_ROUTE,
|
|
2754
|
+
request: principal.request,
|
|
2755
|
+
body: {
|
|
2756
|
+
sessionId: "session-1",
|
|
2757
|
+
isRefresh: true,
|
|
2758
|
+
viewId: ObjectID.generate().toString(),
|
|
2759
|
+
},
|
|
2760
|
+
});
|
|
2761
|
+
|
|
2762
|
+
expect(recordViewSpy).toHaveBeenCalledTimes(1);
|
|
2763
|
+
expect((result.jsonBody as JSONObject)["viewId"]).toBe(
|
|
2764
|
+
freshViewId.toString(),
|
|
2765
|
+
);
|
|
2766
|
+
});
|
|
2767
|
+
|
|
2768
|
+
test("isRefresh with a view row for a different application than the one authorized records a fresh view", async () => {
|
|
2769
|
+
const principal: {
|
|
2770
|
+
request: JSONObject;
|
|
2771
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
2772
|
+
} = buildPrincipal({
|
|
2773
|
+
projectId: projectId,
|
|
2774
|
+
userId: userId,
|
|
2775
|
+
permissions: [Permission.ReadRumSessionReplayPayload],
|
|
2776
|
+
});
|
|
2777
|
+
|
|
2778
|
+
mockProps(principal.databaseProps);
|
|
2779
|
+
mockSessionHeader(applicationAId);
|
|
2780
|
+
mockApplication({ id: applicationAId, labelIds: [] });
|
|
2781
|
+
mockRecordedView();
|
|
2782
|
+
|
|
2783
|
+
const viewId: ObjectID = ObjectID.generate();
|
|
2784
|
+
mockOwnView({ viewId: viewId, rumApplicationId: applicationBId });
|
|
1619
2785
|
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
expect(
|
|
2786
|
+
await callRoute({
|
|
2787
|
+
uri: MANIFEST_ROUTE,
|
|
2788
|
+
request: principal.request,
|
|
2789
|
+
body: {
|
|
2790
|
+
sessionId: "session-1",
|
|
2791
|
+
isRefresh: true,
|
|
2792
|
+
viewId: viewId.toString(),
|
|
2793
|
+
},
|
|
2794
|
+
});
|
|
2795
|
+
|
|
2796
|
+
expect(recordViewSpy).toHaveBeenCalledTimes(1);
|
|
1631
2797
|
});
|
|
1632
2798
|
|
|
1633
|
-
test("a
|
|
2799
|
+
test("a malformed linkedIncidentId is dropped from the audit row rather than failing the read", async () => {
|
|
1634
2800
|
const principal: {
|
|
1635
2801
|
request: JSONObject;
|
|
1636
2802
|
databaseProps: DatabaseCommonInteractionProps;
|
|
1637
2803
|
} = buildPrincipal({
|
|
1638
2804
|
projectId: projectId,
|
|
1639
2805
|
userId: userId,
|
|
1640
|
-
permissions: [Permission.
|
|
2806
|
+
permissions: [Permission.ReadRumSessionReplayPayload],
|
|
1641
2807
|
});
|
|
1642
2808
|
|
|
1643
2809
|
mockProps(principal.databaseProps);
|
|
1644
|
-
|
|
2810
|
+
mockSessionHeader(applicationAId);
|
|
2811
|
+
mockApplication({ id: applicationAId, labelIds: [] });
|
|
2812
|
+
mockRecordedView();
|
|
1645
2813
|
|
|
1646
|
-
const
|
|
2814
|
+
const incidentId: ObjectID = ObjectID.generate();
|
|
2815
|
+
|
|
2816
|
+
await callRoute({
|
|
1647
2817
|
uri: MANIFEST_ROUTE,
|
|
1648
2818
|
request: principal.request,
|
|
1649
|
-
body: {
|
|
2819
|
+
body: {
|
|
2820
|
+
sessionId: "session-1",
|
|
2821
|
+
linkedIncidentId: "not-a-uuid",
|
|
2822
|
+
linkedExceptionFingerprint: "fp-1",
|
|
2823
|
+
},
|
|
1650
2824
|
});
|
|
1651
2825
|
|
|
1652
|
-
|
|
1653
|
-
expect(
|
|
2826
|
+
const dropped: JSONObject = recordViewSpy.mock.calls[0]![0] as JSONObject;
|
|
2827
|
+
expect(dropped["linkedIncidentId"]).toBeUndefined();
|
|
2828
|
+
expect(dropped["linkedExceptionFingerprint"]).toBe("fp-1");
|
|
2829
|
+
|
|
2830
|
+
jest.clearAllMocks();
|
|
2831
|
+
mockProps(principal.databaseProps);
|
|
2832
|
+
mockSessionHeader(applicationAId);
|
|
2833
|
+
mockApplication({ id: applicationAId, labelIds: [] });
|
|
2834
|
+
mockRecordedView();
|
|
2835
|
+
|
|
2836
|
+
await callRoute({
|
|
2837
|
+
uri: MANIFEST_ROUTE,
|
|
2838
|
+
request: principal.request,
|
|
2839
|
+
body: {
|
|
2840
|
+
sessionId: "session-1",
|
|
2841
|
+
linkedIncidentId: incidentId.toString(),
|
|
2842
|
+
},
|
|
2843
|
+
});
|
|
2844
|
+
|
|
2845
|
+
const kept: JSONObject = recordViewSpy.mock.calls[0]![0] as JSONObject;
|
|
2846
|
+
expect((kept["linkedIncidentId"] as ObjectID).toString()).toBe(
|
|
2847
|
+
incidentId.toString(),
|
|
2848
|
+
);
|
|
1654
2849
|
});
|
|
1655
2850
|
});
|
|
1656
2851
|
|
|
@@ -1702,7 +2897,17 @@ describe("Session replay playback API", () => {
|
|
|
1702
2897
|
expect(headerQuerySpy).not.toHaveBeenCalled();
|
|
1703
2898
|
});
|
|
1704
2899
|
|
|
1705
|
-
|
|
2900
|
+
/*
|
|
2901
|
+
* The cap has to bound the bytes this endpoint actually returns.
|
|
2902
|
+
* `payloadBytes` is the POST-GZIP wire size the recorder uploaded,
|
|
2903
|
+
* while the `payload` column holds the DECOMPRESSED JSON that is
|
|
2904
|
+
* served. Measuring the former let 8 chunks that pass a 8 MiB check
|
|
2905
|
+
* decompress into tens of megabytes of response. It is measured in
|
|
2906
|
+
* the SAME statement that ships the bytes, so the column is
|
|
2907
|
+
* decompressed once per page rather than once for a pre-check and
|
|
2908
|
+
* again for the read.
|
|
2909
|
+
*/
|
|
2910
|
+
test("measures the stored (decompressed) size in the one statement that ships the bytes", async () => {
|
|
1706
2911
|
const principal: {
|
|
1707
2912
|
request: JSONObject;
|
|
1708
2913
|
databaseProps: DatabaseCommonInteractionProps;
|
|
@@ -1714,34 +2919,36 @@ describe("Session replay playback API", () => {
|
|
|
1714
2919
|
|
|
1715
2920
|
chunkQuerySpy.mockResolvedValue(
|
|
1716
2921
|
fakeResultSet([
|
|
1717
|
-
{ chunkIndex: 0,
|
|
1718
|
-
{ chunkIndex: 1, chunkStoredBytes: 5 * 1024 * 1024 },
|
|
2922
|
+
{ chunkIndex: 0, servedPayload: "[1]", isServed: 1 },
|
|
1719
2923
|
]) as never,
|
|
1720
2924
|
);
|
|
1721
2925
|
|
|
1722
|
-
|
|
2926
|
+
await callRoute({
|
|
1723
2927
|
uri: CHUNKS_ROUTE,
|
|
1724
2928
|
request: principal.request,
|
|
1725
2929
|
body: {
|
|
1726
2930
|
sessionId: "session-1",
|
|
1727
2931
|
tabId: "tab-1",
|
|
1728
|
-
chunkIndexes: [0
|
|
2932
|
+
chunkIndexes: [0],
|
|
1729
2933
|
},
|
|
1730
2934
|
});
|
|
1731
2935
|
|
|
1732
|
-
expect(result.thrownToNext).toBeInstanceOf(BadDataException);
|
|
1733
|
-
/* Refused on the pre-check: the payload read never ran. */
|
|
1734
2936
|
expect(chunkQuerySpy).toHaveBeenCalledTimes(1);
|
|
2937
|
+
|
|
2938
|
+
const statement: Statement = chunkQuerySpy.mock.calls[0]![0] as Statement;
|
|
2939
|
+
|
|
2940
|
+
expect(statement.query).toContain("length(payload)");
|
|
2941
|
+
expect(statement.query).not.toContain("toFloat64(payloadBytes)");
|
|
1735
2942
|
});
|
|
1736
2943
|
|
|
1737
2944
|
/*
|
|
1738
|
-
*
|
|
1739
|
-
*
|
|
1740
|
-
*
|
|
1741
|
-
*
|
|
1742
|
-
*
|
|
2945
|
+
* A page that does not fit is answered with the prefix that does -
|
|
2946
|
+
* never with a refusal. The old 400 ("Request fewer chunks") was
|
|
2947
|
+
* reachable by the player's own page plan, because it plans against
|
|
2948
|
+
* the wire size the manifest exposes while the cap is on decompressed
|
|
2949
|
+
* bytes, and the player had no recovery for it.
|
|
1743
2950
|
*/
|
|
1744
|
-
test("the
|
|
2951
|
+
test("serves the prefix of whole chunks that fits, names the rest, and never refuses", async () => {
|
|
1745
2952
|
const principal: {
|
|
1746
2953
|
request: JSONObject;
|
|
1747
2954
|
databaseProps: DatabaseCommonInteractionProps;
|
|
@@ -1751,31 +2958,43 @@ describe("Session replay playback API", () => {
|
|
|
1751
2958
|
mockSessionHeader(applicationAId);
|
|
1752
2959
|
mockApplication({ id: applicationAId, labelIds: [] });
|
|
1753
2960
|
|
|
1754
|
-
|
|
1755
|
-
.mockResolvedValueOnce(
|
|
1756
|
-
fakeResultSet([{ chunkIndex: 0, chunkStoredBytes: 4 }]) as never,
|
|
1757
|
-
)
|
|
1758
|
-
.mockResolvedValueOnce(
|
|
1759
|
-
fakeResultSet([{ chunkIndex: 0, payload: "[1]" }]) as never,
|
|
1760
|
-
);
|
|
2961
|
+
const fatPayload: string = "a".repeat(5 * 1024 * 1024);
|
|
1761
2962
|
|
|
1762
|
-
|
|
2963
|
+
chunkQuerySpy.mockResolvedValue(
|
|
2964
|
+
fakeResultSet([
|
|
2965
|
+
{ chunkIndex: 0, servedPayload: fatPayload, isServed: 1 },
|
|
2966
|
+
{ chunkIndex: 1, servedPayload: fatPayload, isServed: 1 },
|
|
2967
|
+
{ chunkIndex: 2, servedPayload: "", isServed: 0 },
|
|
2968
|
+
]) as never,
|
|
2969
|
+
);
|
|
2970
|
+
|
|
2971
|
+
const result: CallResult = await callRoute({
|
|
1763
2972
|
uri: CHUNKS_ROUTE,
|
|
1764
2973
|
request: principal.request,
|
|
1765
2974
|
body: {
|
|
1766
2975
|
sessionId: "session-1",
|
|
1767
2976
|
tabId: "tab-1",
|
|
1768
|
-
chunkIndexes: [0],
|
|
2977
|
+
chunkIndexes: [0, 1, 2],
|
|
1769
2978
|
},
|
|
1770
2979
|
});
|
|
1771
2980
|
|
|
1772
|
-
|
|
2981
|
+
expect(result.thrownToNext).toBeUndefined();
|
|
2982
|
+
expect(result.deniedWith).toBeUndefined();
|
|
1773
2983
|
|
|
1774
|
-
|
|
1775
|
-
expect(
|
|
2984
|
+
const buffer: Buffer = result.sentBuffer as unknown as Buffer;
|
|
2985
|
+
expect(buffer.readUInt32LE(0)).toBe(0);
|
|
2986
|
+
expect(buffer.readUInt32LE(4)).toBe(fatPayload.length);
|
|
2987
|
+
expect(buffer.length).toBe(8 + fatPayload.length);
|
|
2988
|
+
expect(result.headers["X-OneUptime-Replay-Omitted-Chunks"]).toBe("1,2");
|
|
1776
2989
|
});
|
|
1777
2990
|
|
|
1778
|
-
|
|
2991
|
+
/*
|
|
2992
|
+
* A full snapshot between (cap - 8 bytes) and the cap - or one that
|
|
2993
|
+
* re-serialised slightly larger than the ingest inflate cap - used
|
|
2994
|
+
* to fail EVERY fetch, so playback of that tab dead-ended at it. A
|
|
2995
|
+
* lone chunk is bounded by the ingest cap already.
|
|
2996
|
+
*/
|
|
2997
|
+
test("a single chunk at or over the read cap is still served", async () => {
|
|
1779
2998
|
const principal: {
|
|
1780
2999
|
request: JSONObject;
|
|
1781
3000
|
databaseProps: DatabaseCommonInteractionProps;
|
|
@@ -1785,22 +3004,13 @@ describe("Session replay playback API", () => {
|
|
|
1785
3004
|
mockSessionHeader(applicationAId);
|
|
1786
3005
|
mockApplication({ id: applicationAId, labelIds: [] });
|
|
1787
3006
|
|
|
1788
|
-
|
|
1789
|
-
const fatPayload: string = "a".repeat(5 * 1024 * 1024);
|
|
3007
|
+
const edgePayload: string = "a".repeat(8 * 1024 * 1024);
|
|
1790
3008
|
|
|
1791
|
-
chunkQuerySpy
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
]) as never,
|
|
1797
|
-
)
|
|
1798
|
-
.mockResolvedValueOnce(
|
|
1799
|
-
fakeResultSet([
|
|
1800
|
-
{ chunkIndex: 0, payload: fatPayload },
|
|
1801
|
-
{ chunkIndex: 1, payload: fatPayload },
|
|
1802
|
-
]) as never,
|
|
1803
|
-
);
|
|
3009
|
+
chunkQuerySpy.mockResolvedValue(
|
|
3010
|
+
fakeResultSet([
|
|
3011
|
+
{ chunkIndex: 0, servedPayload: edgePayload, isServed: 1 },
|
|
3012
|
+
]) as never,
|
|
3013
|
+
);
|
|
1804
3014
|
|
|
1805
3015
|
const result: CallResult = await callRoute({
|
|
1806
3016
|
uri: CHUNKS_ROUTE,
|
|
@@ -1808,13 +3018,85 @@ describe("Session replay playback API", () => {
|
|
|
1808
3018
|
body: {
|
|
1809
3019
|
sessionId: "session-1",
|
|
1810
3020
|
tabId: "tab-1",
|
|
1811
|
-
chunkIndexes: [0
|
|
3021
|
+
chunkIndexes: [0],
|
|
1812
3022
|
},
|
|
1813
3023
|
});
|
|
1814
3024
|
|
|
1815
|
-
expect(result.thrownToNext).
|
|
1816
|
-
|
|
1817
|
-
expect(
|
|
3025
|
+
expect(result.thrownToNext).toBeUndefined();
|
|
3026
|
+
const buffer: Buffer = result.sentBuffer as unknown as Buffer;
|
|
3027
|
+
expect(buffer.length).toBe(8 + edgePayload.length);
|
|
3028
|
+
expect(
|
|
3029
|
+
result.headers["X-OneUptime-Replay-Omitted-Chunks"],
|
|
3030
|
+
).toBeUndefined();
|
|
3031
|
+
});
|
|
3032
|
+
|
|
3033
|
+
/*
|
|
3034
|
+
* A 480-chunk session is 60 chunk pages. Each used to re-run the
|
|
3035
|
+
* header GROUP BY and re-load the application's labels; now the
|
|
3036
|
+
* chunk route serves both from a 30s cache the manifest refreshes.
|
|
3037
|
+
* The label DECISION is still made per request against the caller's
|
|
3038
|
+
* permissions.
|
|
3039
|
+
*/
|
|
3040
|
+
test("consecutive chunk pages reuse the resolved header and application", async () => {
|
|
3041
|
+
const principal: {
|
|
3042
|
+
request: JSONObject;
|
|
3043
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
3044
|
+
} = payloadPrincipal();
|
|
3045
|
+
|
|
3046
|
+
mockProps(principal.databaseProps);
|
|
3047
|
+
mockSessionHeader(applicationAId);
|
|
3048
|
+
mockApplication({ id: applicationAId, labelIds: [] });
|
|
3049
|
+
|
|
3050
|
+
chunkQuerySpy.mockResolvedValue(
|
|
3051
|
+
fakeResultSet([
|
|
3052
|
+
{ chunkIndex: 0, servedPayload: "[1]", isServed: 1 },
|
|
3053
|
+
]) as never,
|
|
3054
|
+
);
|
|
3055
|
+
|
|
3056
|
+
for (const chunkIndex of [0, 1, 2]) {
|
|
3057
|
+
await callRoute({
|
|
3058
|
+
uri: CHUNKS_ROUTE,
|
|
3059
|
+
request: principal.request,
|
|
3060
|
+
body: {
|
|
3061
|
+
sessionId: "session-1",
|
|
3062
|
+
tabId: "tab-1",
|
|
3063
|
+
chunkIndexes: [chunkIndex],
|
|
3064
|
+
},
|
|
3065
|
+
});
|
|
3066
|
+
}
|
|
3067
|
+
|
|
3068
|
+
expect(chunkQuerySpy).toHaveBeenCalledTimes(3);
|
|
3069
|
+
expect(headerQuerySpy).toHaveBeenCalledTimes(1);
|
|
3070
|
+
expect(findOneBySpy).toHaveBeenCalledTimes(1);
|
|
3071
|
+
|
|
3072
|
+
/*
|
|
3073
|
+
* A revoked grant takes effect on the very next page: the cached
|
|
3074
|
+
* data is re-checked against the caller's current permissions.
|
|
3075
|
+
*/
|
|
3076
|
+
const viewer: {
|
|
3077
|
+
request: JSONObject;
|
|
3078
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
3079
|
+
} = buildPrincipal({
|
|
3080
|
+
projectId: projectId,
|
|
3081
|
+
userId: userId,
|
|
3082
|
+
permissions: [Permission.ReadRumSessionReplayPayload],
|
|
3083
|
+
labelIds: [labelBId],
|
|
3084
|
+
});
|
|
3085
|
+
|
|
3086
|
+
mockProps(viewer.databaseProps);
|
|
3087
|
+
|
|
3088
|
+
const refused: CallResult = await callRoute({
|
|
3089
|
+
uri: CHUNKS_ROUTE,
|
|
3090
|
+
request: viewer.request,
|
|
3091
|
+
body: {
|
|
3092
|
+
sessionId: "session-1",
|
|
3093
|
+
tabId: "tab-1",
|
|
3094
|
+
chunkIndexes: [3],
|
|
3095
|
+
},
|
|
3096
|
+
});
|
|
3097
|
+
|
|
3098
|
+
expect(refused.thrownToNext).toBeInstanceOf(NotAuthorizedException);
|
|
3099
|
+
expect(chunkQuerySpy).toHaveBeenCalledTimes(3);
|
|
1818
3100
|
});
|
|
1819
3101
|
|
|
1820
3102
|
test("returns length-prefixed binary frames and de-duplicates by version", async () => {
|
|
@@ -1827,19 +3109,12 @@ describe("Session replay playback API", () => {
|
|
|
1827
3109
|
mockSessionHeader(applicationAId);
|
|
1828
3110
|
mockApplication({ id: applicationAId, labelIds: [] });
|
|
1829
3111
|
|
|
1830
|
-
chunkQuerySpy
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
)
|
|
1837
|
-
.mockResolvedValueOnce(
|
|
1838
|
-
fakeResultSet([
|
|
1839
|
-
{ chunkIndex: 0, payload: "[1]" },
|
|
1840
|
-
{ chunkIndex: 1, payload: "[22]" },
|
|
1841
|
-
]) as never,
|
|
1842
|
-
);
|
|
3112
|
+
chunkQuerySpy.mockResolvedValue(
|
|
3113
|
+
fakeResultSet([
|
|
3114
|
+
{ chunkIndex: 0, servedPayload: "[1]", isServed: 1 },
|
|
3115
|
+
{ chunkIndex: 1, servedPayload: "[22]", isServed: 1 },
|
|
3116
|
+
]) as never,
|
|
3117
|
+
);
|
|
1843
3118
|
|
|
1844
3119
|
const result: CallResult = await callRoute({
|
|
1845
3120
|
uri: CHUNKS_ROUTE,
|
|
@@ -1866,7 +3141,7 @@ describe("Session replay playback API", () => {
|
|
|
1866
3141
|
expect(result.headers["Cache-Control"]).toBe("no-store");
|
|
1867
3142
|
|
|
1868
3143
|
const payloadStatement: Statement = chunkQuerySpy.mock
|
|
1869
|
-
.calls[
|
|
3144
|
+
.calls[0]![0] as Statement;
|
|
1870
3145
|
expect(payloadStatement.query).toContain(
|
|
1871
3146
|
"ORDER BY chunkIndex ASC, version DESC LIMIT 1 BY chunkIndex",
|
|
1872
3147
|
);
|
|
@@ -1986,22 +3261,121 @@ describe("Session replay playback API", () => {
|
|
|
1986
3261
|
},
|
|
1987
3262
|
});
|
|
1988
3263
|
|
|
1989
|
-
expect(result.deniedWith).toBeInstanceOf(NotAuthorizedException);
|
|
1990
|
-
expect(recordSecondsWatchedSpy).not.toHaveBeenCalled();
|
|
3264
|
+
expect(result.deniedWith).toBeInstanceOf(NotAuthorizedException);
|
|
3265
|
+
expect(recordSecondsWatchedSpy).not.toHaveBeenCalled();
|
|
3266
|
+
|
|
3267
|
+
/* The ownership predicate is the thing being pinned. */
|
|
3268
|
+
const lookupArgs: JSONObject = viewFindOneBySpy.mock
|
|
3269
|
+
.calls[0]![0] as JSONObject;
|
|
3270
|
+
const query: JSONObject = lookupArgs["query"] as JSONObject;
|
|
3271
|
+
expect((query["viewedByUserId"] as ObjectID).toString()).toBe(
|
|
3272
|
+
userId.toString(),
|
|
3273
|
+
);
|
|
3274
|
+
expect((query["projectId"] as ObjectID).toString()).toBe(
|
|
3275
|
+
projectId.toString(),
|
|
3276
|
+
);
|
|
3277
|
+
});
|
|
3278
|
+
|
|
3279
|
+
test("a caller outside the view's application label scope cannot advance it", async () => {
|
|
3280
|
+
const principal: {
|
|
3281
|
+
request: JSONObject;
|
|
3282
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
3283
|
+
} = buildPrincipal({
|
|
3284
|
+
projectId: projectId,
|
|
3285
|
+
userId: userId,
|
|
3286
|
+
permissions: [Permission.ReadRumSessionReplayPayload],
|
|
3287
|
+
labelIds: [labelAId],
|
|
3288
|
+
});
|
|
3289
|
+
|
|
3290
|
+
mockProps(principal.databaseProps);
|
|
3291
|
+
/* The view points at application B, which carries a foreign label. */
|
|
3292
|
+
mockApplication({ id: applicationBId, labelIds: [labelBId] });
|
|
3293
|
+
|
|
3294
|
+
const viewId: ObjectID = ObjectID.generate();
|
|
3295
|
+
mockOwnView({ viewId: viewId, rumApplicationId: applicationBId });
|
|
3296
|
+
|
|
3297
|
+
const result: CallResult = await callRoute({
|
|
3298
|
+
uri: HEARTBEAT_ROUTE,
|
|
3299
|
+
request: principal.request,
|
|
3300
|
+
body: { viewId: viewId.toString(), secondsWatched: 30 },
|
|
3301
|
+
});
|
|
3302
|
+
|
|
3303
|
+
expect(result.thrownToNext).toBeInstanceOf(NotAuthorizedException);
|
|
3304
|
+
expect(recordSecondsWatchedSpy).not.toHaveBeenCalled();
|
|
3305
|
+
});
|
|
3306
|
+
|
|
3307
|
+
test("a malformed viewId is a bad request, not a database error", async () => {
|
|
3308
|
+
const principal: {
|
|
3309
|
+
request: JSONObject;
|
|
3310
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
3311
|
+
} = buildPrincipal({
|
|
3312
|
+
projectId: projectId,
|
|
3313
|
+
userId: userId,
|
|
3314
|
+
permissions: [Permission.ReadRumSessionReplayPayload],
|
|
3315
|
+
});
|
|
3316
|
+
|
|
3317
|
+
mockProps(principal.databaseProps);
|
|
3318
|
+
|
|
3319
|
+
const result: CallResult = await callRoute({
|
|
3320
|
+
uri: HEARTBEAT_ROUTE,
|
|
3321
|
+
request: principal.request,
|
|
3322
|
+
body: { viewId: "nope", secondsWatched: 30 },
|
|
3323
|
+
});
|
|
3324
|
+
|
|
3325
|
+
expect(result.thrownToNext).toBeInstanceOf(BadDataException);
|
|
3326
|
+
expect(viewFindOneBySpy).not.toHaveBeenCalled();
|
|
3327
|
+
expect(recordSecondsWatchedSpy).not.toHaveBeenCalled();
|
|
3328
|
+
});
|
|
3329
|
+
|
|
3330
|
+
/*
|
|
3331
|
+
* secondsWatched is time WATCHED, cumulative and monotonic. The one
|
|
3332
|
+
* ownership lookup also carries the row's current figure, so the
|
|
3333
|
+
* service is told what it already holds and a heartbeat that does not
|
|
3334
|
+
* advance costs no write; the response echoes the figure on record.
|
|
3335
|
+
*/
|
|
3336
|
+
test("hands the service the row's current figure and never reports a lower one", async () => {
|
|
3337
|
+
const principal: {
|
|
3338
|
+
request: JSONObject;
|
|
3339
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
3340
|
+
} = buildPrincipal({
|
|
3341
|
+
projectId: projectId,
|
|
3342
|
+
userId: userId,
|
|
3343
|
+
permissions: [Permission.ReadRumSessionReplayPayload],
|
|
3344
|
+
});
|
|
3345
|
+
|
|
3346
|
+
mockProps(principal.databaseProps);
|
|
3347
|
+
mockApplication({ id: applicationAId, labelIds: [] });
|
|
3348
|
+
|
|
3349
|
+
const viewId: ObjectID = ObjectID.generate();
|
|
3350
|
+
const view: RumSessionReplayView = new RumSessionReplayView();
|
|
3351
|
+
view.id = viewId;
|
|
3352
|
+
view.projectId = projectId;
|
|
3353
|
+
view.rumApplicationId = applicationAId;
|
|
3354
|
+
view.viewedByUserId = userId;
|
|
3355
|
+
view.secondsWatched = 90;
|
|
3356
|
+
viewFindOneBySpy.mockResolvedValue(view);
|
|
3357
|
+
|
|
3358
|
+
const result: CallResult = await callRoute({
|
|
3359
|
+
uri: HEARTBEAT_ROUTE,
|
|
3360
|
+
request: principal.request,
|
|
3361
|
+
body: { viewId: viewId.toString(), secondsWatched: 44 },
|
|
3362
|
+
});
|
|
3363
|
+
|
|
3364
|
+
const args: JSONObject = recordSecondsWatchedSpy.mock
|
|
3365
|
+
.calls[0]![0] as JSONObject;
|
|
3366
|
+
expect(args["secondsWatched"]).toBe(30);
|
|
3367
|
+
expect(args["currentSecondsWatched"]).toBe(90);
|
|
3368
|
+
expect((result.jsonBody as JSONObject)["secondsWatched"]).toBe(90);
|
|
1991
3369
|
|
|
1992
|
-
/* The
|
|
3370
|
+
/* The lookup selects the figure so no second read is needed. */
|
|
1993
3371
|
const lookupArgs: JSONObject = viewFindOneBySpy.mock
|
|
1994
3372
|
.calls[0]![0] as JSONObject;
|
|
1995
|
-
|
|
1996
|
-
expect((query["viewedByUserId"] as ObjectID).toString()).toBe(
|
|
1997
|
-
userId.toString(),
|
|
1998
|
-
);
|
|
1999
|
-
expect((query["projectId"] as ObjectID).toString()).toBe(
|
|
2000
|
-
projectId.toString(),
|
|
2001
|
-
);
|
|
3373
|
+
expect((lookupArgs["select"] as JSONObject)["secondsWatched"]).toBe(true);
|
|
2002
3374
|
});
|
|
3375
|
+
});
|
|
2003
3376
|
|
|
2004
|
-
|
|
3377
|
+
describe("views", () => {
|
|
3378
|
+
test("lists who watched, pinned to the authorized application, for a payload-permitted caller", async () => {
|
|
2005
3379
|
const principal: {
|
|
2006
3380
|
request: JSONObject;
|
|
2007
3381
|
databaseProps: DatabaseCommonInteractionProps;
|
|
@@ -2009,47 +3383,67 @@ describe("Session replay playback API", () => {
|
|
|
2009
3383
|
projectId: projectId,
|
|
2010
3384
|
userId: userId,
|
|
2011
3385
|
permissions: [Permission.ReadRumSessionReplayPayload],
|
|
2012
|
-
labelIds: [labelAId],
|
|
2013
3386
|
});
|
|
2014
3387
|
|
|
2015
3388
|
mockProps(principal.databaseProps);
|
|
2016
|
-
|
|
2017
|
-
mockApplication({ id:
|
|
3389
|
+
mockSessionHeader(applicationAId);
|
|
3390
|
+
mockApplication({ id: applicationAId, labelIds: [] });
|
|
2018
3391
|
|
|
2019
|
-
const
|
|
2020
|
-
|
|
3392
|
+
const view: RumSessionReplayView = new RumSessionReplayView();
|
|
3393
|
+
view.id = ObjectID.generate();
|
|
3394
|
+
view.viewedAt = new Date("2026-08-01T10:00:00.000Z");
|
|
3395
|
+
view.secondsWatched = 45;
|
|
3396
|
+
view.accessReason = "incident triage";
|
|
3397
|
+
view.viewedByUserId = userId;
|
|
3398
|
+
|
|
3399
|
+
const getViewsSpy: jest.SpyInstance = jest
|
|
3400
|
+
.spyOn(RumSessionReplayViewService, "getViewsForSession")
|
|
3401
|
+
.mockResolvedValue([view]);
|
|
2021
3402
|
|
|
2022
3403
|
const result: CallResult = await callRoute({
|
|
2023
|
-
uri:
|
|
3404
|
+
uri: VIEWS_ROUTE,
|
|
2024
3405
|
request: principal.request,
|
|
2025
|
-
body: {
|
|
3406
|
+
body: { sessionId: "session-1" },
|
|
2026
3407
|
});
|
|
2027
3408
|
|
|
2028
|
-
|
|
2029
|
-
expect(
|
|
3409
|
+
const args: JSONObject = getViewsSpy.mock.calls[0]![0] as JSONObject;
|
|
3410
|
+
expect((args["rumApplicationId"] as ObjectID).toString()).toBe(
|
|
3411
|
+
applicationAId.toString(),
|
|
3412
|
+
);
|
|
3413
|
+
expect(args["sessionId"]).toBe("session-1");
|
|
3414
|
+
|
|
3415
|
+
const views: Array<JSONObject> = (result.jsonBody as JSONObject)[
|
|
3416
|
+
"views"
|
|
3417
|
+
] as unknown as Array<JSONObject>;
|
|
3418
|
+
expect(views).toHaveLength(1);
|
|
3419
|
+
expect(views[0]!["secondsWatched"]).toBe(45);
|
|
3420
|
+
expect(views[0]!["accessReason"]).toBe("incident triage");
|
|
3421
|
+
expect(views[0]!["viewedAt"]).toBe("2026-08-01T10:00:00.000Z");
|
|
3422
|
+
expect(views[0]!["viewedByUserId"]).toBe(userId.toString());
|
|
3423
|
+
/* Listing who watched is not itself a view. */
|
|
3424
|
+
expect(recordViewSpy).not.toHaveBeenCalled();
|
|
2030
3425
|
});
|
|
2031
3426
|
|
|
2032
|
-
test("a
|
|
3427
|
+
test("a caller with only the list permission cannot see who watched", async () => {
|
|
2033
3428
|
const principal: {
|
|
2034
3429
|
request: JSONObject;
|
|
2035
3430
|
databaseProps: DatabaseCommonInteractionProps;
|
|
2036
3431
|
} = buildPrincipal({
|
|
2037
3432
|
projectId: projectId,
|
|
2038
3433
|
userId: userId,
|
|
2039
|
-
permissions: [Permission.
|
|
3434
|
+
permissions: [Permission.ReadRumSessionReplay],
|
|
2040
3435
|
});
|
|
2041
3436
|
|
|
2042
3437
|
mockProps(principal.databaseProps);
|
|
2043
3438
|
|
|
2044
3439
|
const result: CallResult = await callRoute({
|
|
2045
|
-
uri:
|
|
3440
|
+
uri: VIEWS_ROUTE,
|
|
2046
3441
|
request: principal.request,
|
|
2047
|
-
body: {
|
|
3442
|
+
body: { sessionId: "session-1" },
|
|
2048
3443
|
});
|
|
2049
3444
|
|
|
2050
|
-
expect(result.
|
|
2051
|
-
expect(
|
|
2052
|
-
expect(recordSecondsWatchedSpy).not.toHaveBeenCalled();
|
|
3445
|
+
expect(result.deniedWith).toBeInstanceOf(NotAuthorizedException);
|
|
3446
|
+
expect(headerQuerySpy).not.toHaveBeenCalled();
|
|
2053
3447
|
});
|
|
2054
3448
|
});
|
|
2055
3449
|
|
|
@@ -2207,6 +3601,174 @@ describe("Session replay playback API", () => {
|
|
|
2207
3601
|
expect(sessions[0]!["refreshRageCount"]).toBe(5);
|
|
2208
3602
|
expect(sessions[0]!["maskingMode"]).toBe("MaskAllText");
|
|
2209
3603
|
});
|
|
3604
|
+
|
|
3605
|
+
/*
|
|
3606
|
+
* RumSession is partitioned by day. Without a window the lookup
|
|
3607
|
+
* scanned every partition the project ever wrote, on every exception
|
|
3608
|
+
* page load; a 30-day default covers every retention tier a recording
|
|
3609
|
+
* can still be played under.
|
|
3610
|
+
*/
|
|
3611
|
+
test("applies a default window when the caller gives none", async () => {
|
|
3612
|
+
const principal: {
|
|
3613
|
+
request: JSONObject;
|
|
3614
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
3615
|
+
} = buildPrincipal({
|
|
3616
|
+
projectId: projectId,
|
|
3617
|
+
userId: userId,
|
|
3618
|
+
permissions: [Permission.ProjectOwner],
|
|
3619
|
+
});
|
|
3620
|
+
|
|
3621
|
+
mockProps(principal.databaseProps);
|
|
3622
|
+
|
|
3623
|
+
await callRoute({
|
|
3624
|
+
uri: FOR_EXCEPTION_ROUTE,
|
|
3625
|
+
request: principal.request,
|
|
3626
|
+
body: { fingerprint: "fp-1" },
|
|
3627
|
+
});
|
|
3628
|
+
|
|
3629
|
+
const statement: Statement = headerQuerySpy.mock
|
|
3630
|
+
.calls[0]![0] as Statement;
|
|
3631
|
+
expect(statement.query).toContain("startTime >= ");
|
|
3632
|
+
expect(statement.query).toContain("startTime <= ");
|
|
3633
|
+
});
|
|
3634
|
+
|
|
3635
|
+
test("derives the window from the error's own time when the caller sends it", async () => {
|
|
3636
|
+
const principal: {
|
|
3637
|
+
request: JSONObject;
|
|
3638
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
3639
|
+
} = buildPrincipal({
|
|
3640
|
+
projectId: projectId,
|
|
3641
|
+
userId: userId,
|
|
3642
|
+
permissions: [Permission.ProjectOwner],
|
|
3643
|
+
});
|
|
3644
|
+
|
|
3645
|
+
mockProps(principal.databaseProps);
|
|
3646
|
+
|
|
3647
|
+
const errorTimeUnixMs: number = Date.UTC(2026, 7, 5, 12, 0, 0);
|
|
3648
|
+
|
|
3649
|
+
await callRoute({
|
|
3650
|
+
uri: FOR_EXCEPTION_ROUTE,
|
|
3651
|
+
request: principal.request,
|
|
3652
|
+
body: { fingerprint: "fp-1", errorTimeUnixMs: errorTimeUnixMs },
|
|
3653
|
+
});
|
|
3654
|
+
|
|
3655
|
+
const statement: Statement = headerQuerySpy.mock
|
|
3656
|
+
.calls[0]![0] as Statement;
|
|
3657
|
+
const bound: Array<unknown> = Object.values(statement.query_params);
|
|
3658
|
+
|
|
3659
|
+
/*
|
|
3660
|
+
* The bound DateTime64 strings carry the window: the start sits one
|
|
3661
|
+
* maximum session length (plus padding) before the error.
|
|
3662
|
+
*/
|
|
3663
|
+
const earliestStart: Date = new Date(
|
|
3664
|
+
errorTimeUnixMs - SESSION_REPLAY_MAX_SESSION_MS - 5 * 60 * 1000,
|
|
3665
|
+
);
|
|
3666
|
+
const boundStrings: Array<string> = bound.filter(
|
|
3667
|
+
(value: unknown): value is string => {
|
|
3668
|
+
return typeof value === "string";
|
|
3669
|
+
},
|
|
3670
|
+
);
|
|
3671
|
+
expect(
|
|
3672
|
+
boundStrings.some((value: string): boolean => {
|
|
3673
|
+
return value.startsWith(
|
|
3674
|
+
earliestStart.toISOString().substring(0, 19).replace("T", " "),
|
|
3675
|
+
);
|
|
3676
|
+
}),
|
|
3677
|
+
).toBe(true);
|
|
3678
|
+
});
|
|
3679
|
+
|
|
3680
|
+
/*
|
|
3681
|
+
* The header's fingerprint list is written by the finalizer, 10+
|
|
3682
|
+
* minutes after the session goes quiet - which is the whole incident
|
|
3683
|
+
* from the reporter's point of view. The exception instance table
|
|
3684
|
+
* knows the session id from the moment the error is ingested, so a
|
|
3685
|
+
* live session is found through it.
|
|
3686
|
+
*/
|
|
3687
|
+
test("finds a live session through the exception instances before the finalizer has run", async () => {
|
|
3688
|
+
const principal: {
|
|
3689
|
+
request: JSONObject;
|
|
3690
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
3691
|
+
} = buildPrincipal({
|
|
3692
|
+
projectId: projectId,
|
|
3693
|
+
userId: userId,
|
|
3694
|
+
permissions: [Permission.ProjectOwner],
|
|
3695
|
+
});
|
|
3696
|
+
|
|
3697
|
+
mockProps(principal.databaseProps);
|
|
3698
|
+
exceptionQuerySpy.mockResolvedValue(
|
|
3699
|
+
fakeResultSet([{ sessionId: "live-session" }]) as never,
|
|
3700
|
+
);
|
|
3701
|
+
|
|
3702
|
+
await callRoute({
|
|
3703
|
+
uri: FOR_EXCEPTION_ROUTE,
|
|
3704
|
+
request: principal.request,
|
|
3705
|
+
body: { fingerprint: "fp-1" },
|
|
3706
|
+
});
|
|
3707
|
+
|
|
3708
|
+
const instanceStatement: Statement = exceptionQuerySpy.mock
|
|
3709
|
+
.calls[0]![0] as Statement;
|
|
3710
|
+
expect(instanceStatement.query).toContain("fingerprint = ");
|
|
3711
|
+
expect(instanceStatement.query).toContain("sessionId != ''");
|
|
3712
|
+
|
|
3713
|
+
const headerStatement: Statement = headerQuerySpy.mock
|
|
3714
|
+
.calls[0]![0] as Statement;
|
|
3715
|
+
expect(headerStatement.query).toContain("OR sessionId IN (");
|
|
3716
|
+
expect(Object.values(headerStatement.query_params)).toContainEqual([
|
|
3717
|
+
"live-session",
|
|
3718
|
+
]);
|
|
3719
|
+
});
|
|
3720
|
+
|
|
3721
|
+
test("a pinned sessionId and a non-integer limit are handled before the query", async () => {
|
|
3722
|
+
const principal: {
|
|
3723
|
+
request: JSONObject;
|
|
3724
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
3725
|
+
} = buildPrincipal({
|
|
3726
|
+
projectId: projectId,
|
|
3727
|
+
userId: userId,
|
|
3728
|
+
permissions: [Permission.ProjectOwner],
|
|
3729
|
+
});
|
|
3730
|
+
|
|
3731
|
+
mockProps(principal.databaseProps);
|
|
3732
|
+
exceptionQuerySpy.mockResolvedValue(
|
|
3733
|
+
fakeResultSet([{ sessionId: "s-9" }]) as never,
|
|
3734
|
+
);
|
|
3735
|
+
|
|
3736
|
+
await callRoute({
|
|
3737
|
+
uri: FOR_EXCEPTION_ROUTE,
|
|
3738
|
+
request: principal.request,
|
|
3739
|
+
body: { fingerprint: "fp-1", sessionId: "s-9" },
|
|
3740
|
+
});
|
|
3741
|
+
|
|
3742
|
+
/*
|
|
3743
|
+
* The pin NARROWS the instance lookup rather than replacing it.
|
|
3744
|
+
* Returning the pinned id unchecked reduced the header statement to
|
|
3745
|
+
* `sessionId = X AND (hasAny(fingerprints, [f]) OR sessionId IN (X))`,
|
|
3746
|
+
* whose second arm is trivially true - so the card would claim any
|
|
3747
|
+
* accessible session had observed this exception.
|
|
3748
|
+
*/
|
|
3749
|
+
const instanceStatement: Statement = exceptionQuerySpy.mock
|
|
3750
|
+
.calls[0]![0] as Statement;
|
|
3751
|
+
expect(instanceStatement.query).toContain("fingerprint = ");
|
|
3752
|
+
expect(instanceStatement.query).toContain("AND sessionId = ");
|
|
3753
|
+
expect(Object.values(instanceStatement.query_params)).toContain("s-9");
|
|
3754
|
+
|
|
3755
|
+
const statement: Statement = headerQuerySpy.mock
|
|
3756
|
+
.calls[0]![0] as Statement;
|
|
3757
|
+
expect(statement.query).toContain("AND sessionId = ");
|
|
3758
|
+
expect(Object.values(statement.query_params)).toContain("s-9");
|
|
3759
|
+
|
|
3760
|
+
jest.clearAllMocks();
|
|
3761
|
+
mockProps(principal.databaseProps);
|
|
3762
|
+
|
|
3763
|
+
const bad: CallResult = await callRoute({
|
|
3764
|
+
uri: FOR_EXCEPTION_ROUTE,
|
|
3765
|
+
request: principal.request,
|
|
3766
|
+
body: { fingerprint: "fp-1", limit: 2.5 },
|
|
3767
|
+
});
|
|
3768
|
+
|
|
3769
|
+
expect(bad.thrownToNext).toBeInstanceOf(BadDataException);
|
|
3770
|
+
expect(headerQuerySpy).not.toHaveBeenCalled();
|
|
3771
|
+
});
|
|
2210
3772
|
});
|
|
2211
3773
|
|
|
2212
3774
|
describe("ingest-status", () => {
|
|
@@ -2321,5 +3883,167 @@ describe("Session replay playback API", () => {
|
|
|
2321
3883
|
expect(body["applicationBytesUsedThisMonth"]).toBeNull();
|
|
2322
3884
|
expect(body["dailyByteLimit"]).toBeGreaterThan(0);
|
|
2323
3885
|
});
|
|
3886
|
+
|
|
3887
|
+
/*
|
|
3888
|
+
* The full RecordingHealthStatus. Every counter that could not be
|
|
3889
|
+
* read is null - the diagnosis renders "unknown" - and every
|
|
3890
|
+
* timestamp is ISO or null. Jest runs without Redis, so the refusal
|
|
3891
|
+
* and drop counters are the null case here; the activity summary is
|
|
3892
|
+
* ClickHouse and is mocked.
|
|
3893
|
+
*/
|
|
3894
|
+
test("carries the policy, liveness stamps and activity summary, with unreadable counters as null", async () => {
|
|
3895
|
+
const principal: {
|
|
3896
|
+
request: JSONObject;
|
|
3897
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
3898
|
+
} = buildPrincipal({
|
|
3899
|
+
projectId: projectId,
|
|
3900
|
+
userId: userId,
|
|
3901
|
+
permissions: [Permission.ProjectOwner],
|
|
3902
|
+
});
|
|
3903
|
+
|
|
3904
|
+
mockProps(principal.databaseProps);
|
|
3905
|
+
|
|
3906
|
+
const application: RumApplication = new RumApplication();
|
|
3907
|
+
application.id = applicationAId;
|
|
3908
|
+
application.projectId = projectId;
|
|
3909
|
+
application.labels = [];
|
|
3910
|
+
application.appIdentifier = "checkout-web";
|
|
3911
|
+
application.isSessionReplayEnabled = true;
|
|
3912
|
+
application.sessionReplayAllowedOrigins = [];
|
|
3913
|
+
application.sessionReplaySamplePercentage = 100;
|
|
3914
|
+
application.sessionReplayRetentionInDays = 14;
|
|
3915
|
+
application.lastSeenAt = new Date("2026-08-05T09:14:00.000Z");
|
|
3916
|
+
(application as unknown as JSONObject)["sessionReplayConsentMode"] =
|
|
3917
|
+
"NotRequired";
|
|
3918
|
+
(application as unknown as JSONObject)["sessionReplayMaskingMode"] =
|
|
3919
|
+
"MaskAllText";
|
|
3920
|
+
findOneBySpy.mockResolvedValue(application);
|
|
3921
|
+
mockProject(true);
|
|
3922
|
+
|
|
3923
|
+
headerQuerySpy
|
|
3924
|
+
.mockResolvedValueOnce(
|
|
3925
|
+
fakeResultSet([{ sessionCount: 143, unplayableCount: 3 }]) as never,
|
|
3926
|
+
)
|
|
3927
|
+
.mockResolvedValueOnce(
|
|
3928
|
+
fakeResultSet([
|
|
3929
|
+
{ lastStartUnixMs: Date.UTC(2026, 7, 5, 9, 0) },
|
|
3930
|
+
]) as never,
|
|
3931
|
+
);
|
|
3932
|
+
|
|
3933
|
+
const result: CallResult = await callRoute({
|
|
3934
|
+
uri: INGEST_STATUS_ROUTE,
|
|
3935
|
+
request: principal.request,
|
|
3936
|
+
body: { rumApplicationId: applicationAId.toString() },
|
|
3937
|
+
});
|
|
3938
|
+
|
|
3939
|
+
const body: JSONObject = result.jsonBody as JSONObject;
|
|
3940
|
+
|
|
3941
|
+
expect(body["consentMode"]).toBe("NotRequired");
|
|
3942
|
+
expect(body["maskingMode"]).toBe("MaskAllText");
|
|
3943
|
+
expect(body["retentionInDays"]).toBe(14);
|
|
3944
|
+
expect(body["lastConfigFetchAt"]).toBe("2026-08-05T09:14:00.000Z");
|
|
3945
|
+
expect(body["lastSessionStartedAt"]).toBe("2026-08-05T09:00:00.000Z");
|
|
3946
|
+
expect(body["sessionsLast24h"]).toBe(143);
|
|
3947
|
+
expect(body["playableSessionsLast24h"]).toBe(140);
|
|
3948
|
+
/* No recorder manifest reader registered, no Redis: unknown, not 0. */
|
|
3949
|
+
expect(body["publishedRecorderVersion"]).toBeNull();
|
|
3950
|
+
expect(body["refusalsLast24h"]).toBeNull();
|
|
3951
|
+
expect(body["dropsLast24h"]).toBeNull();
|
|
3952
|
+
/* The pre-existing fields are untouched. */
|
|
3953
|
+
expect(body["isProjectAllowed"]).toBe(true);
|
|
3954
|
+
expect(body["appIdentifier"]).toBe("checkout-web");
|
|
3955
|
+
});
|
|
3956
|
+
|
|
3957
|
+
test("reports the published recorder version from the registered reader, and never-seen stamps as null", async () => {
|
|
3958
|
+
const principal: {
|
|
3959
|
+
request: JSONObject;
|
|
3960
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
3961
|
+
} = buildPrincipal({
|
|
3962
|
+
projectId: projectId,
|
|
3963
|
+
userId: userId,
|
|
3964
|
+
permissions: [Permission.ProjectOwner],
|
|
3965
|
+
});
|
|
3966
|
+
|
|
3967
|
+
mockProps(principal.databaseProps);
|
|
3968
|
+
mockConfiguredApplication();
|
|
3969
|
+
mockProject(true);
|
|
3970
|
+
|
|
3971
|
+
SessionReplayReadService.setPublishedRecorderVersionProvider(
|
|
3972
|
+
(): string | null => {
|
|
3973
|
+
return "3.1.0";
|
|
3974
|
+
},
|
|
3975
|
+
);
|
|
3976
|
+
|
|
3977
|
+
/* The activity summary read fails: unknown, never zero. */
|
|
3978
|
+
headerQuerySpy.mockRejectedValue(
|
|
3979
|
+
new Error("clickhouse timeout") as never,
|
|
3980
|
+
);
|
|
3981
|
+
|
|
3982
|
+
const result: CallResult = await callRoute({
|
|
3983
|
+
uri: INGEST_STATUS_ROUTE,
|
|
3984
|
+
request: principal.request,
|
|
3985
|
+
body: { rumApplicationId: applicationAId.toString() },
|
|
3986
|
+
});
|
|
3987
|
+
|
|
3988
|
+
const body: JSONObject = result.jsonBody as JSONObject;
|
|
3989
|
+
|
|
3990
|
+
expect(body["publishedRecorderVersion"]).toBe("3.1.0");
|
|
3991
|
+
expect(body["lastConfigFetchAt"]).toBeNull();
|
|
3992
|
+
expect(body["lastSessionStartedAt"]).toBeNull();
|
|
3993
|
+
expect(body["sessionsLast24h"]).toBeNull();
|
|
3994
|
+
expect(body["playableSessionsLast24h"]).toBeNull();
|
|
3995
|
+
expect(body["retentionInDays"]).toBeNull();
|
|
3996
|
+
expect(body["recorderCapabilities"]).toBeNull();
|
|
3997
|
+
});
|
|
3998
|
+
|
|
3999
|
+
/*
|
|
4000
|
+
* The health card and the installation test both point an operator at
|
|
4001
|
+
* "the capabilities of the newest recorder that reported" - the one
|
|
4002
|
+
* way to spot a stale cached artifact ("click labels: no") without
|
|
4003
|
+
* opening a recording, which writes an audit row. The route never sent
|
|
4004
|
+
* the field, so both surfaces said "not reported yet" for every
|
|
4005
|
+
* application forever, which reads as a bug rather than as
|
|
4006
|
+
* information.
|
|
4007
|
+
*/
|
|
4008
|
+
test("reports the newest session's recorder capabilities, filtered to the known vocabulary", async () => {
|
|
4009
|
+
const principal: {
|
|
4010
|
+
request: JSONObject;
|
|
4011
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
4012
|
+
} = buildPrincipal({
|
|
4013
|
+
projectId: projectId,
|
|
4014
|
+
userId: userId,
|
|
4015
|
+
permissions: [Permission.ProjectOwner],
|
|
4016
|
+
});
|
|
4017
|
+
|
|
4018
|
+
mockProps(principal.databaseProps);
|
|
4019
|
+
mockConfiguredApplication();
|
|
4020
|
+
mockProject(true);
|
|
4021
|
+
|
|
4022
|
+
headerQuerySpy
|
|
4023
|
+
.mockResolvedValueOnce(
|
|
4024
|
+
fakeResultSet([{ sessionCount: 2, unplayableCount: 0 }]) as never,
|
|
4025
|
+
)
|
|
4026
|
+
.mockResolvedValueOnce(
|
|
4027
|
+
fakeResultSet([
|
|
4028
|
+
{
|
|
4029
|
+
lastStartUnixMs: Date.UTC(2026, 7, 5, 9, 0),
|
|
4030
|
+
aggAttributes: {
|
|
4031
|
+
"recorder.capabilities": "click-events,tags,made-up",
|
|
4032
|
+
},
|
|
4033
|
+
},
|
|
4034
|
+
]) as never,
|
|
4035
|
+
);
|
|
4036
|
+
|
|
4037
|
+
const result: CallResult = await callRoute({
|
|
4038
|
+
uri: INGEST_STATUS_ROUTE,
|
|
4039
|
+
request: principal.request,
|
|
4040
|
+
body: { rumApplicationId: applicationAId.toString() },
|
|
4041
|
+
});
|
|
4042
|
+
|
|
4043
|
+
expect((result.jsonBody as JSONObject)["recorderCapabilities"]).toEqual([
|
|
4044
|
+
"click-events",
|
|
4045
|
+
"tags",
|
|
4046
|
+
]);
|
|
4047
|
+
});
|
|
2324
4048
|
});
|
|
2325
4049
|
});
|