@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
|
@@ -0,0 +1,2503 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from "@jest/globals";
|
|
2
|
+
import { SessionReplayChunkManifestEntry } from "../../../Types/Rum/SessionReplay";
|
|
3
|
+
import ChunkLoader, {
|
|
4
|
+
RRWEB_EVENT_TYPE_FULL_SNAPSHOT,
|
|
5
|
+
RRWEB_EVENT_TYPE_INCREMENTAL,
|
|
6
|
+
RRWEB_EVENT_TYPE_META,
|
|
7
|
+
SessionReplayChunkFetchRequest,
|
|
8
|
+
SessionReplayRecordedEvent,
|
|
9
|
+
} from "../../../../App/FeatureSet/Dashboard/src/Components/SessionReplay/ChunkLoader";
|
|
10
|
+
import { createReplayEngine } from "../../../../App/FeatureSet/Dashboard/src/Components/SessionReplay/Engine/ReplayEngine";
|
|
11
|
+
import {
|
|
12
|
+
REPLAY_FEED_AHEAD_MIN_MS,
|
|
13
|
+
ReplayEngine,
|
|
14
|
+
ReplayEngineSnapshot,
|
|
15
|
+
ReplayScheduleHandle,
|
|
16
|
+
ReplayerLike,
|
|
17
|
+
} from "../../../../App/FeatureSet/Dashboard/src/Components/SessionReplay/Engine/ReplayEngineTypes";
|
|
18
|
+
|
|
19
|
+
/*
|
|
20
|
+
* The engine is the piece whose failure mode is a lie rather than a crash:
|
|
21
|
+
* feed rrweb a chunk whose predecessor never arrived and it resolves those
|
|
22
|
+
* mutations against stale node ids, rendering a DOM the end user never
|
|
23
|
+
* saw; stop on a Finish that was only a stall and Play does nothing.
|
|
24
|
+
*
|
|
25
|
+
* These tests drive it with a real ChunkLoader over fixture bytes, a fake
|
|
26
|
+
* Replayer, an injected clock and a recording scheduler, so every state
|
|
27
|
+
* transition is pinned without rrweb, a network, React or a browser.
|
|
28
|
+
* Every scenario the old ReplayStage.test.tsx covered is ported here
|
|
29
|
+
* one-for-one, followed by the invariants (a)-(g) from the design.
|
|
30
|
+
*
|
|
31
|
+
* It lives in Common for the same reason ChunkLoader.test.ts does: the
|
|
32
|
+
* logic is Dashboard code, but nothing about it needs the Dashboard build.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
const CHUNK_MS: number = 15000;
|
|
36
|
+
|
|
37
|
+
/* Deliberately nothing like a session offset: these are raw client clocks. */
|
|
38
|
+
const CLIENT_CLOCK_BASE_MS: number = 1700000000000;
|
|
39
|
+
|
|
40
|
+
function makeEntry(
|
|
41
|
+
chunkIndex: number,
|
|
42
|
+
options?: {
|
|
43
|
+
hasFullSnapshot?: boolean;
|
|
44
|
+
eventCount?: number;
|
|
45
|
+
payloadBytes?: number;
|
|
46
|
+
},
|
|
47
|
+
): SessionReplayChunkManifestEntry {
|
|
48
|
+
return {
|
|
49
|
+
chunkIndex: chunkIndex,
|
|
50
|
+
tabId: "tab-1",
|
|
51
|
+
chunkStartOffsetMs: chunkIndex * CHUNK_MS,
|
|
52
|
+
chunkEndOffsetMs: (chunkIndex + 1) * CHUNK_MS,
|
|
53
|
+
eventCount: options?.eventCount ?? 10,
|
|
54
|
+
hasFullSnapshot: options?.hasFullSnapshot ?? false,
|
|
55
|
+
payloadBytes: options?.payloadBytes ?? 4096,
|
|
56
|
+
errorCount: 0,
|
|
57
|
+
rageClickCount: 0,
|
|
58
|
+
deadClickCount: 0,
|
|
59
|
+
errorClickCount: 0,
|
|
60
|
+
refreshRageCount: 0,
|
|
61
|
+
routeCount: 0,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function eventsFor(
|
|
66
|
+
chunkIndex: number,
|
|
67
|
+
count: number,
|
|
68
|
+
): Array<SessionReplayRecordedEvent> {
|
|
69
|
+
const events: Array<SessionReplayRecordedEvent> = [];
|
|
70
|
+
|
|
71
|
+
for (let i: number = 0; i < count; i++) {
|
|
72
|
+
events.push({
|
|
73
|
+
type:
|
|
74
|
+
chunkIndex === 0 && i === 0
|
|
75
|
+
? RRWEB_EVENT_TYPE_META
|
|
76
|
+
: chunkIndex === 0 && i === 1
|
|
77
|
+
? RRWEB_EVENT_TYPE_FULL_SNAPSHOT
|
|
78
|
+
: RRWEB_EVENT_TYPE_INCREMENTAL,
|
|
79
|
+
timestamp: CLIENT_CLOCK_BASE_MS + chunkIndex * CHUNK_MS + i,
|
|
80
|
+
data:
|
|
81
|
+
chunkIndex === 0 && i === 0
|
|
82
|
+
? { href: "https://app.example.com/", width: 1440, height: 900 }
|
|
83
|
+
: { chunkIndex: chunkIndex, sequence: i },
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return events;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/* [u32 chunkIndex][u32 length][payload], little-endian, as the server writes. */
|
|
91
|
+
function encodeFrames(
|
|
92
|
+
frames: Array<{
|
|
93
|
+
chunkIndex: number;
|
|
94
|
+
events: Array<SessionReplayRecordedEvent>;
|
|
95
|
+
}>,
|
|
96
|
+
): ArrayBuffer {
|
|
97
|
+
const encoder: TextEncoder = new TextEncoder();
|
|
98
|
+
const encoded: Array<{ chunkIndex: number; bytes: Uint8Array }> = frames.map(
|
|
99
|
+
(frame: {
|
|
100
|
+
chunkIndex: number;
|
|
101
|
+
events: Array<SessionReplayRecordedEvent>;
|
|
102
|
+
}): { chunkIndex: number; bytes: Uint8Array } => {
|
|
103
|
+
return {
|
|
104
|
+
chunkIndex: frame.chunkIndex,
|
|
105
|
+
bytes: encoder.encode(JSON.stringify(frame.events)),
|
|
106
|
+
};
|
|
107
|
+
},
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const totalBytes: number = encoded.reduce(
|
|
111
|
+
(total: number, frame: { bytes: Uint8Array }): number => {
|
|
112
|
+
return total + 8 + frame.bytes.length;
|
|
113
|
+
},
|
|
114
|
+
0,
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const buffer: ArrayBuffer = new ArrayBuffer(totalBytes);
|
|
118
|
+
const view: DataView = new DataView(buffer);
|
|
119
|
+
const bytes: Uint8Array = new Uint8Array(buffer);
|
|
120
|
+
let offset: number = 0;
|
|
121
|
+
|
|
122
|
+
for (const frame of encoded) {
|
|
123
|
+
view.setUint32(offset, frame.chunkIndex, true);
|
|
124
|
+
view.setUint32(offset + 4, frame.bytes.length, true);
|
|
125
|
+
bytes.set(frame.bytes, offset + 8);
|
|
126
|
+
offset += 8 + frame.bytes.length;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return buffer;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/*
|
|
133
|
+
* Stand-in for rrweb's Replayer. Records everything the engine does to it,
|
|
134
|
+
* and mimics the two behaviours the engine depends on: the clock advances
|
|
135
|
+
* only while playing, and the first play/pause rebuilds the snapshot
|
|
136
|
+
* (rrweb casts the FullSnapshot synchronously on the first transport call).
|
|
137
|
+
*/
|
|
138
|
+
class FakeReplayer implements ReplayerLike {
|
|
139
|
+
public readonly iframe: HTMLIFrameElement;
|
|
140
|
+
public readonly wrapper: HTMLElement;
|
|
141
|
+
public readonly initialEvents: Array<SessionReplayRecordedEvent>;
|
|
142
|
+
public readonly constructorConfig: Record<string, unknown>;
|
|
143
|
+
public readonly added: Array<SessionReplayRecordedEvent> = [];
|
|
144
|
+
public readonly playOffsets: Array<number | undefined> = [];
|
|
145
|
+
public readonly pauseOffsets: Array<number | undefined> = [];
|
|
146
|
+
public readonly configs: Array<Record<string, unknown>> = [];
|
|
147
|
+
public readonly handlers: Map<string, Array<(payload: unknown) => void>> =
|
|
148
|
+
new Map<string, Array<(payload: unknown) => void>>();
|
|
149
|
+
public isDestroyed: boolean = false;
|
|
150
|
+
public isPlaying: boolean = false;
|
|
151
|
+
public currentTimeMs: number = 0;
|
|
152
|
+
public autoRebuild: boolean = true;
|
|
153
|
+
public addEventError: Error | null = null;
|
|
154
|
+
private hasRebuilt: boolean = false;
|
|
155
|
+
|
|
156
|
+
public constructor(
|
|
157
|
+
events: Array<SessionReplayRecordedEvent>,
|
|
158
|
+
config?: Record<string, unknown>,
|
|
159
|
+
) {
|
|
160
|
+
this.initialEvents = events;
|
|
161
|
+
this.constructorConfig = config ?? {};
|
|
162
|
+
this.iframe = document.createElement("iframe");
|
|
163
|
+
this.wrapper = document.createElement("div");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
public play(timeOffsetMs?: number): void {
|
|
167
|
+
this.playOffsets.push(timeOffsetMs);
|
|
168
|
+
|
|
169
|
+
if (timeOffsetMs !== undefined) {
|
|
170
|
+
this.currentTimeMs = timeOffsetMs;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
this.isPlaying = true;
|
|
174
|
+
this.rebuildOnce();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
public pause(timeOffsetMs?: number): void {
|
|
178
|
+
this.pauseOffsets.push(timeOffsetMs);
|
|
179
|
+
|
|
180
|
+
if (timeOffsetMs !== undefined) {
|
|
181
|
+
this.currentTimeMs = timeOffsetMs;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
this.isPlaying = false;
|
|
185
|
+
this.rebuildOnce();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
public destroy(): void {
|
|
189
|
+
this.isDestroyed = true;
|
|
190
|
+
this.isPlaying = false;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
public addEvent(event: SessionReplayRecordedEvent): void {
|
|
194
|
+
if (this.addEventError) {
|
|
195
|
+
throw this.addEventError;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
this.added.push(event);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
public getCurrentTime(): number {
|
|
202
|
+
return this.currentTimeMs;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
public setConfig(config: Record<string, unknown>): void {
|
|
206
|
+
this.configs.push(config);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
public on(event: string, handler: (payload: unknown) => void): unknown {
|
|
210
|
+
const list: Array<(payload: unknown) => void> =
|
|
211
|
+
this.handlers.get(event) ?? [];
|
|
212
|
+
list.push(handler);
|
|
213
|
+
this.handlers.set(event, list);
|
|
214
|
+
return this;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
public emit(event: string, payload?: unknown): void {
|
|
218
|
+
for (const handler of this.handlers.get(event) ?? []) {
|
|
219
|
+
handler(payload);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/* Wall clock passed; rrweb's clock follows only while playing. */
|
|
224
|
+
public advance(ms: number, speed: number): void {
|
|
225
|
+
if (this.isPlaying && !this.isDestroyed) {
|
|
226
|
+
this.currentTimeMs += ms * speed;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
private rebuildOnce(): void {
|
|
231
|
+
if (this.autoRebuild && !this.hasRebuilt) {
|
|
232
|
+
this.hasRebuilt = true;
|
|
233
|
+
this.emit("fullsnapshot-rebuilded", undefined);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
interface HarnessOptions {
|
|
239
|
+
entries: Array<SessionReplayChunkManifestEntry>;
|
|
240
|
+
/*
|
|
241
|
+
* Skip the attach() a real stage performs, so a test can exercise what
|
|
242
|
+
* the engine does before its host is on the page.
|
|
243
|
+
*/
|
|
244
|
+
leaveHostDetached?: boolean;
|
|
245
|
+
eventsPerChunk?: number;
|
|
246
|
+
/* Per-chunk override of the events the "server" returns. */
|
|
247
|
+
eventsForChunk?: (chunkIndex: number) => Array<SessionReplayRecordedEvent>;
|
|
248
|
+
/* Chunk indexes the server "loses" - present in the manifest, absent from the response. */
|
|
249
|
+
omitChunkIndexes?: Array<number>;
|
|
250
|
+
deferFetch?: boolean;
|
|
251
|
+
/* Requests containing any of these indexes reject (until `healFetch`). */
|
|
252
|
+
failChunkIndexes?: Array<number>;
|
|
253
|
+
/* Requests containing any of these indexes never resolve. */
|
|
254
|
+
hangChunkIndexes?: Array<number>;
|
|
255
|
+
autoRebuild?: boolean;
|
|
256
|
+
fetchTimeoutMs?: number;
|
|
257
|
+
retryDelaysMs?: Array<number>;
|
|
258
|
+
headerViewport?: { width: number; height: number } | null;
|
|
259
|
+
/*
|
|
260
|
+
* document.hidden. While it answers true the fake Replayers' clocks are
|
|
261
|
+
* frozen too, because rrweb only advances its timer inside a
|
|
262
|
+
* requestAnimationFrame callback and browsers suspend those in a
|
|
263
|
+
* background tab.
|
|
264
|
+
*/
|
|
265
|
+
isDocumentHidden?: () => boolean;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
interface Harness {
|
|
269
|
+
loader: ChunkLoader;
|
|
270
|
+
engine: ReplayEngine;
|
|
271
|
+
replayers: Array<FakeReplayer>;
|
|
272
|
+
requests: Array<Array<number>>;
|
|
273
|
+
signals: Array<AbortSignal>;
|
|
274
|
+
snapshots: Array<ReplayEngineSnapshot>;
|
|
275
|
+
scheduled: Array<{ callback: () => void; delayMs: number }>;
|
|
276
|
+
resolveFetch: () => void;
|
|
277
|
+
/*
|
|
278
|
+
* Release only the deferred request that carries this chunk. Ordering
|
|
279
|
+
* two in-flight fetches against each other is the only way to reproduce
|
|
280
|
+
* one feeding loop being retired while another is mid-await.
|
|
281
|
+
*/
|
|
282
|
+
releaseFetch: (chunkIndex: number) => void;
|
|
283
|
+
healFetch: () => void;
|
|
284
|
+
now: () => number;
|
|
285
|
+
/* Advance the wall clock AND every playing fake Replayer, then TICK. */
|
|
286
|
+
tick: (ms: number) => Promise<void>;
|
|
287
|
+
live: () => FakeReplayer;
|
|
288
|
+
snapshot: () => ReplayEngineSnapshot;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const harnesses: Array<Harness> = [];
|
|
292
|
+
/* Stage containers appended to the document, removed in afterEach. */
|
|
293
|
+
const containers: Array<HTMLElement> = [];
|
|
294
|
+
|
|
295
|
+
function makeHarness(options: HarnessOptions): Harness {
|
|
296
|
+
const replayers: Array<FakeReplayer> = [];
|
|
297
|
+
const requests: Array<Array<number>> = [];
|
|
298
|
+
const signals: Array<AbortSignal> = [];
|
|
299
|
+
const snapshots: Array<ReplayEngineSnapshot> = [];
|
|
300
|
+
const scheduled: Array<{ callback: () => void; delayMs: number }> = [];
|
|
301
|
+
const pending: Array<{ chunkIndexes: Array<number>; resolve: () => void }> =
|
|
302
|
+
[];
|
|
303
|
+
const omitted: Set<number> = new Set<number>(options.omitChunkIndexes ?? []);
|
|
304
|
+
const failing: Set<number> = new Set<number>(options.failChunkIndexes ?? []);
|
|
305
|
+
const hanging: Set<number> = new Set<number>(options.hangChunkIndexes ?? []);
|
|
306
|
+
let clock: number = 10_000;
|
|
307
|
+
|
|
308
|
+
const loader: ChunkLoader = new ChunkLoader({
|
|
309
|
+
sessionId: "sess-1",
|
|
310
|
+
tabId: "tab-1",
|
|
311
|
+
entries: options.entries,
|
|
312
|
+
fetchTimeoutMs: options.fetchTimeoutMs ?? 2000,
|
|
313
|
+
retryDelaysMs: options.retryDelaysMs ?? [1, 1],
|
|
314
|
+
fetcher: (
|
|
315
|
+
request: SessionReplayChunkFetchRequest,
|
|
316
|
+
): Promise<ArrayBuffer> => {
|
|
317
|
+
requests.push([...request.chunkIndexes]);
|
|
318
|
+
|
|
319
|
+
if (request.signal) {
|
|
320
|
+
signals.push(request.signal);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (
|
|
324
|
+
request.chunkIndexes.some((chunkIndex: number): boolean => {
|
|
325
|
+
return hanging.has(chunkIndex);
|
|
326
|
+
})
|
|
327
|
+
) {
|
|
328
|
+
return new Promise<ArrayBuffer>((): void => {
|
|
329
|
+
// Never resolves: the timeout is what ends it.
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (
|
|
334
|
+
request.chunkIndexes.some((chunkIndex: number): boolean => {
|
|
335
|
+
return failing.has(chunkIndex);
|
|
336
|
+
})
|
|
337
|
+
) {
|
|
338
|
+
return Promise.reject(
|
|
339
|
+
new Error("Could not load recording data (HTTP 503)."),
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const buffer: ArrayBuffer = encodeFrames(
|
|
344
|
+
request.chunkIndexes
|
|
345
|
+
.filter((chunkIndex: number): boolean => {
|
|
346
|
+
return !omitted.has(chunkIndex);
|
|
347
|
+
})
|
|
348
|
+
.map(
|
|
349
|
+
(
|
|
350
|
+
chunkIndex: number,
|
|
351
|
+
): {
|
|
352
|
+
chunkIndex: number;
|
|
353
|
+
events: Array<SessionReplayRecordedEvent>;
|
|
354
|
+
} => {
|
|
355
|
+
return {
|
|
356
|
+
chunkIndex: chunkIndex,
|
|
357
|
+
events: options.eventsForChunk
|
|
358
|
+
? options.eventsForChunk(chunkIndex)
|
|
359
|
+
: eventsFor(chunkIndex, options.eventsPerChunk ?? 2),
|
|
360
|
+
};
|
|
361
|
+
},
|
|
362
|
+
),
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
if (!options.deferFetch) {
|
|
366
|
+
return Promise.resolve(buffer);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const requested: Array<number> = [...request.chunkIndexes];
|
|
370
|
+
|
|
371
|
+
return new Promise<ArrayBuffer>((resolve: (b: ArrayBuffer) => void) => {
|
|
372
|
+
pending.push({
|
|
373
|
+
chunkIndexes: requested,
|
|
374
|
+
resolve: (): void => {
|
|
375
|
+
resolve(buffer);
|
|
376
|
+
},
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
},
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
const engine: ReplayEngine = createReplayEngine(
|
|
383
|
+
{
|
|
384
|
+
loader: loader,
|
|
385
|
+
createReplayer: (
|
|
386
|
+
events: Array<SessionReplayRecordedEvent>,
|
|
387
|
+
config: Record<string, unknown>,
|
|
388
|
+
): ReplayerLike => {
|
|
389
|
+
const replayer: FakeReplayer = new FakeReplayer(events, config);
|
|
390
|
+
replayer.autoRebuild = options.autoRebuild ?? true;
|
|
391
|
+
replayers.push(replayer);
|
|
392
|
+
return replayer;
|
|
393
|
+
},
|
|
394
|
+
now: (): number => {
|
|
395
|
+
return clock;
|
|
396
|
+
},
|
|
397
|
+
schedule: (
|
|
398
|
+
callback: () => void,
|
|
399
|
+
delayMs: number,
|
|
400
|
+
): ReplayScheduleHandle => {
|
|
401
|
+
scheduled.push({ callback: callback, delayMs: delayMs });
|
|
402
|
+
return scheduled.length;
|
|
403
|
+
},
|
|
404
|
+
cancel: (): void => {
|
|
405
|
+
// Recorded callbacks are never run; tests drive TICK themselves.
|
|
406
|
+
},
|
|
407
|
+
isDocumentHidden: options.isDocumentHidden,
|
|
408
|
+
},
|
|
409
|
+
{
|
|
410
|
+
tabId: "tab-1",
|
|
411
|
+
headerViewport: options.headerViewport ?? null,
|
|
412
|
+
},
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
engine.subscribe((snapshot: ReplayEngineSnapshot): void => {
|
|
416
|
+
snapshots.push(snapshot);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
const harness: Harness = {
|
|
420
|
+
loader: loader,
|
|
421
|
+
engine: engine,
|
|
422
|
+
replayers: replayers,
|
|
423
|
+
requests: requests,
|
|
424
|
+
signals: signals,
|
|
425
|
+
snapshots: snapshots,
|
|
426
|
+
scheduled: scheduled,
|
|
427
|
+
resolveFetch: (): void => {
|
|
428
|
+
const waiting: Array<{
|
|
429
|
+
chunkIndexes: Array<number>;
|
|
430
|
+
resolve: () => void;
|
|
431
|
+
}> = [...pending];
|
|
432
|
+
pending.length = 0;
|
|
433
|
+
|
|
434
|
+
for (const request of waiting) {
|
|
435
|
+
request.resolve();
|
|
436
|
+
}
|
|
437
|
+
},
|
|
438
|
+
releaseFetch: (chunkIndex: number): void => {
|
|
439
|
+
const matching: Array<{
|
|
440
|
+
chunkIndexes: Array<number>;
|
|
441
|
+
resolve: () => void;
|
|
442
|
+
}> = pending.filter(
|
|
443
|
+
(request: {
|
|
444
|
+
chunkIndexes: Array<number>;
|
|
445
|
+
resolve: () => void;
|
|
446
|
+
}): boolean => {
|
|
447
|
+
return request.chunkIndexes.includes(chunkIndex);
|
|
448
|
+
},
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
for (const request of matching) {
|
|
452
|
+
pending.splice(pending.indexOf(request), 1);
|
|
453
|
+
request.resolve();
|
|
454
|
+
}
|
|
455
|
+
},
|
|
456
|
+
healFetch: (): void => {
|
|
457
|
+
failing.clear();
|
|
458
|
+
hanging.clear();
|
|
459
|
+
},
|
|
460
|
+
now: (): number => {
|
|
461
|
+
return clock;
|
|
462
|
+
},
|
|
463
|
+
tick: async (ms: number): Promise<void> => {
|
|
464
|
+
clock += ms;
|
|
465
|
+
const speed: number = engine.getSnapshot().speed;
|
|
466
|
+
/* rrweb's clock rides requestAnimationFrame, which a hidden tab suspends. */
|
|
467
|
+
const isHidden: boolean = options.isDocumentHidden?.() ?? false;
|
|
468
|
+
|
|
469
|
+
for (const replayer of replayers) {
|
|
470
|
+
replayer.advance(isHidden ? 0 : ms, speed);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
engine.dispatch({ type: "TICK", nowMs: clock });
|
|
474
|
+
await flush();
|
|
475
|
+
},
|
|
476
|
+
live: (): FakeReplayer => {
|
|
477
|
+
const candidates: Array<FakeReplayer> = replayers.filter(
|
|
478
|
+
(replayer: FakeReplayer): boolean => {
|
|
479
|
+
return !replayer.isDestroyed;
|
|
480
|
+
},
|
|
481
|
+
);
|
|
482
|
+
const last: FakeReplayer | undefined = candidates[candidates.length - 1];
|
|
483
|
+
|
|
484
|
+
if (!last) {
|
|
485
|
+
throw new Error("No live Replayer");
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return last;
|
|
489
|
+
},
|
|
490
|
+
snapshot: (): ReplayEngineSnapshot => {
|
|
491
|
+
return engine.getSnapshot();
|
|
492
|
+
},
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
/*
|
|
496
|
+
* Mount the engine's host, the way ReplayStage does.
|
|
497
|
+
*
|
|
498
|
+
* rrweb's stage lives in a sandboxed iframe, and creating one throws
|
|
499
|
+
* unless its root is in a document - so the engine waits for attach()
|
|
500
|
+
* before building a Replayer. A harness that never attached would model
|
|
501
|
+
* a player nobody can see, and every build in this file would sit
|
|
502
|
+
* deferred forever.
|
|
503
|
+
*/
|
|
504
|
+
if (!options.leaveHostDetached) {
|
|
505
|
+
const container: HTMLDivElement = document.createElement("div");
|
|
506
|
+
|
|
507
|
+
document.body.appendChild(container);
|
|
508
|
+
containers.push(container);
|
|
509
|
+
engine.attach(container);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
harnesses.push(harness);
|
|
513
|
+
|
|
514
|
+
return harness;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/* Lets every queued microtask and zero-delay timer (the awaits inside the engine and loader) run. */
|
|
518
|
+
async function flush(): Promise<void> {
|
|
519
|
+
for (let i: number = 0; i < 6; i++) {
|
|
520
|
+
await new Promise<void>((resolve: () => void) => {
|
|
521
|
+
setTimeout(resolve, 0);
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/*
|
|
527
|
+
* Waits for a condition the engine reaches through REAL timers (the chunk
|
|
528
|
+
* fetch timeout and its retry backoffs are setTimeout-driven).
|
|
529
|
+
*
|
|
530
|
+
* Sleeping for a fixed span instead - which this suite used to do - races
|
|
531
|
+
* the machine: eight jest workers on a loaded box can leave the event loop
|
|
532
|
+
* starved past any budget picked to look generous, so the assertion fails
|
|
533
|
+
* on CI and passes on a quiet laptop, which is the worst kind of test.
|
|
534
|
+
* Polling for the outcome keeps the timing real while making the wait
|
|
535
|
+
* depend on the engine's progress rather than on the wall clock.
|
|
536
|
+
*/
|
|
537
|
+
async function waitUntil(
|
|
538
|
+
predicate: () => boolean,
|
|
539
|
+
timeoutMs: number = 5000,
|
|
540
|
+
): Promise<void> {
|
|
541
|
+
const deadline: number = Date.now() + timeoutMs;
|
|
542
|
+
|
|
543
|
+
while (!predicate()) {
|
|
544
|
+
if (Date.now() > deadline) {
|
|
545
|
+
throw new Error(
|
|
546
|
+
`waitUntil: condition still false after ${timeoutMs}ms of real time`,
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
await new Promise<void>((resolve: () => void) => {
|
|
551
|
+
setTimeout(resolve, 5);
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
await flush();
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
async function loadAndFlush(
|
|
559
|
+
harness: Harness,
|
|
560
|
+
anchor: number,
|
|
561
|
+
targetMs: number,
|
|
562
|
+
): Promise<void> {
|
|
563
|
+
harness.engine.dispatch({
|
|
564
|
+
type: "LOAD",
|
|
565
|
+
anchorChunkIndex: anchor,
|
|
566
|
+
targetMs: targetMs,
|
|
567
|
+
});
|
|
568
|
+
await flush();
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function dataOf(events: Array<SessionReplayRecordedEvent>): Array<unknown> {
|
|
572
|
+
return events.map((event: SessionReplayRecordedEvent): unknown => {
|
|
573
|
+
return event.data;
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
afterEach(() => {
|
|
578
|
+
/*
|
|
579
|
+
* (g) The watchdog is a backstop, not a mechanism: if any scenario in
|
|
580
|
+
* this file needed it, the state machine has a hole the scenario should
|
|
581
|
+
* have pinned directly.
|
|
582
|
+
*/
|
|
583
|
+
const finished: Array<Harness> = [...harnesses];
|
|
584
|
+
/*
|
|
585
|
+
* Cleared before the assertion, not after: a harness left in the list by
|
|
586
|
+
* a throwing expectation would be re-asserted (and re-disposed) in the
|
|
587
|
+
* NEXT test's afterEach, reporting one failure as two.
|
|
588
|
+
*/
|
|
589
|
+
harnesses.length = 0;
|
|
590
|
+
|
|
591
|
+
for (const harness of finished) {
|
|
592
|
+
harness.engine.dispose();
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
for (const container of containers) {
|
|
596
|
+
container.remove();
|
|
597
|
+
}
|
|
598
|
+
containers.length = 0;
|
|
599
|
+
|
|
600
|
+
for (const harness of finished) {
|
|
601
|
+
expect(harness.engine.getDiagnostics().watchdogFireCount).toBe(0);
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
/*
|
|
606
|
+
* rrweb's stage is a sandboxed iframe, and creating one throws unless its
|
|
607
|
+
* root is already in a document. The engine's host div is created in the
|
|
608
|
+
* constructor and only enters the page when ReplayStage's mount effect calls
|
|
609
|
+
* attach() - while loading starts as soon as the manifest lands. Whether the
|
|
610
|
+
* host is on the page by the time the anchor decodes is therefore a race
|
|
611
|
+
* between the network and React's commit, and on a fast local read the
|
|
612
|
+
* network wins.
|
|
613
|
+
*
|
|
614
|
+
* It lost that race in CI: rrweb threw, the throw landed in build()'s catch
|
|
615
|
+
* as a load failure, and a perfectly good recording rendered "Playback
|
|
616
|
+
* stopped - the recording could not be loaded" with an rrweb-internal
|
|
617
|
+
* sentence under it. Nothing had gone wrong; the stage simply was not on
|
|
618
|
+
* screen yet.
|
|
619
|
+
*/
|
|
620
|
+
describe("ReplayEngine builds only once its host is on the page", () => {
|
|
621
|
+
it("waits instead of failing when the stage has not mounted yet", async () => {
|
|
622
|
+
const harness: Harness = makeHarness({
|
|
623
|
+
leaveHostDetached: true,
|
|
624
|
+
entries: [makeEntry(0, { hasFullSnapshot: true }), makeEntry(1)],
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
await loadAndFlush(harness, 0, 0);
|
|
628
|
+
|
|
629
|
+
/* No Replayer, and - the point - no error and no halt. */
|
|
630
|
+
expect(harness.replayers.length).toBe(0);
|
|
631
|
+
expect(harness.snapshot().error).toBeNull();
|
|
632
|
+
expect(harness.snapshot().phase).not.toBe("error");
|
|
633
|
+
|
|
634
|
+
/*
|
|
635
|
+
* "Still loading" is the honest word for it, and it is what a viewer
|
|
636
|
+
* looking at an unmounted stage is actually seeing.
|
|
637
|
+
*/
|
|
638
|
+
expect(["loading", "seeking", "buffering"]).toContain(
|
|
639
|
+
harness.snapshot().phase,
|
|
640
|
+
);
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
it("resumes the same build the moment the stage mounts", async () => {
|
|
644
|
+
const harness: Harness = makeHarness({
|
|
645
|
+
leaveHostDetached: true,
|
|
646
|
+
entries: [makeEntry(0, { hasFullSnapshot: true }), makeEntry(1)],
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
await loadAndFlush(harness, 0, 0);
|
|
650
|
+
|
|
651
|
+
expect(harness.replayers.length).toBe(0);
|
|
652
|
+
|
|
653
|
+
const container: HTMLDivElement = document.createElement("div");
|
|
654
|
+
|
|
655
|
+
document.body.appendChild(container);
|
|
656
|
+
|
|
657
|
+
try {
|
|
658
|
+
harness.engine.attach(container);
|
|
659
|
+
await flush();
|
|
660
|
+
|
|
661
|
+
expect(harness.replayers.length).toBe(1);
|
|
662
|
+
expect(harness.snapshot().error).toBeNull();
|
|
663
|
+
expect(harness.snapshot().loadedChunkIndexes).toEqual([0, 1]);
|
|
664
|
+
} finally {
|
|
665
|
+
container.remove();
|
|
666
|
+
}
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
it("does not resume against a container that is not in the document either", async () => {
|
|
670
|
+
const harness: Harness = makeHarness({
|
|
671
|
+
leaveHostDetached: true,
|
|
672
|
+
entries: [makeEntry(0, { hasFullSnapshot: true }), makeEntry(1)],
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
await loadAndFlush(harness, 0, 0);
|
|
676
|
+
|
|
677
|
+
/* Attaching to a detached container leaves the host just as detached. */
|
|
678
|
+
harness.engine.attach(document.createElement("div"));
|
|
679
|
+
await flush();
|
|
680
|
+
|
|
681
|
+
expect(harness.replayers.length).toBe(0);
|
|
682
|
+
expect(harness.snapshot().error).toBeNull();
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
it("a newer load supersedes the deferred one, so mounting builds the newer anchor", async () => {
|
|
686
|
+
const harness: Harness = makeHarness({
|
|
687
|
+
leaveHostDetached: true,
|
|
688
|
+
entries: [
|
|
689
|
+
makeEntry(0, { hasFullSnapshot: true }),
|
|
690
|
+
makeEntry(1),
|
|
691
|
+
makeEntry(2, { hasFullSnapshot: true }),
|
|
692
|
+
makeEntry(3),
|
|
693
|
+
],
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
await loadAndFlush(harness, 0, 0);
|
|
697
|
+
await loadAndFlush(harness, 2, 2 * CHUNK_MS);
|
|
698
|
+
|
|
699
|
+
expect(harness.replayers.length).toBe(0);
|
|
700
|
+
|
|
701
|
+
const container: HTMLDivElement = document.createElement("div");
|
|
702
|
+
|
|
703
|
+
document.body.appendChild(container);
|
|
704
|
+
|
|
705
|
+
try {
|
|
706
|
+
harness.engine.attach(container);
|
|
707
|
+
await flush();
|
|
708
|
+
|
|
709
|
+
/* One Replayer, anchored where the SECOND load asked - not the first. */
|
|
710
|
+
expect(harness.replayers.length).toBe(1);
|
|
711
|
+
expect(harness.snapshot().loadedChunkIndexes).toEqual([2, 3]);
|
|
712
|
+
expect(harness.snapshot().error).toBeNull();
|
|
713
|
+
} finally {
|
|
714
|
+
container.remove();
|
|
715
|
+
}
|
|
716
|
+
});
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
describe("ReplayEngine chunk feeding", () => {
|
|
720
|
+
it("feeds the contiguous next chunk into the live Replayer", async () => {
|
|
721
|
+
const harness: Harness = makeHarness({
|
|
722
|
+
entries: [
|
|
723
|
+
makeEntry(0, { hasFullSnapshot: true }),
|
|
724
|
+
makeEntry(1),
|
|
725
|
+
makeEntry(2),
|
|
726
|
+
makeEntry(3),
|
|
727
|
+
],
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
await loadAndFlush(harness, 0, 0);
|
|
731
|
+
|
|
732
|
+
expect(harness.replayers.length).toBe(1);
|
|
733
|
+
|
|
734
|
+
/*
|
|
735
|
+
* The priority pair (0 and 1) is decoded before the Replayer exists,
|
|
736
|
+
* so chunk 1 rides in the constructor; the fed range says so.
|
|
737
|
+
*/
|
|
738
|
+
expect(harness.snapshot().fedRange).toEqual({
|
|
739
|
+
fromMs: 0,
|
|
740
|
+
toMs: 2 * CHUNK_MS,
|
|
741
|
+
});
|
|
742
|
+
expect(harness.snapshot().loadedChunkIndexes).toEqual([0, 1]);
|
|
743
|
+
|
|
744
|
+
/* The feed-ahead window (30s) is now exactly used up: the tick extends. */
|
|
745
|
+
await harness.tick(16);
|
|
746
|
+
|
|
747
|
+
expect(dataOf(harness.replayers[0]!.added)).toEqual([
|
|
748
|
+
{ chunkIndex: 2, sequence: 0 },
|
|
749
|
+
{ chunkIndex: 2, sequence: 1 },
|
|
750
|
+
]);
|
|
751
|
+
expect(harness.snapshot().loadedChunkIndexes).toEqual([0, 1, 2]);
|
|
752
|
+
expect(harness.snapshot().lastGap).toBeNull();
|
|
753
|
+
expect(harness.snapshot().error).toBeNull();
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
it("never treats a chunk that failed to decode as fed", async () => {
|
|
757
|
+
/*
|
|
758
|
+
* The regression this whole file exists for. Chunk 1 is in the manifest
|
|
759
|
+
* but the response does not carry it. Advancing lastFedChunkIndex anyway
|
|
760
|
+
* would make the next tick feed chunk 2 into a Replayer that never
|
|
761
|
+
* received 1 - a silent jump rendered as though it were continuous
|
|
762
|
+
* footage, which is the one failure the design forbids outright.
|
|
763
|
+
*/
|
|
764
|
+
const harness: Harness = makeHarness({
|
|
765
|
+
entries: [
|
|
766
|
+
makeEntry(0, { hasFullSnapshot: true }),
|
|
767
|
+
makeEntry(1),
|
|
768
|
+
makeEntry(2, { hasFullSnapshot: true }),
|
|
769
|
+
makeEntry(3),
|
|
770
|
+
],
|
|
771
|
+
omitChunkIndexes: [1],
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
await loadAndFlush(harness, 0, 0);
|
|
775
|
+
harness.engine.dispatch({ type: "PLAY" });
|
|
776
|
+
|
|
777
|
+
expect(harness.replayers.length).toBe(1);
|
|
778
|
+
|
|
779
|
+
// Discover the failure.
|
|
780
|
+
await harness.tick(16);
|
|
781
|
+
|
|
782
|
+
// Nothing from chunk 2 may have reached the first Replayer.
|
|
783
|
+
expect(harness.replayers[0]!.added).toEqual([]);
|
|
784
|
+
expect(harness.snapshot().buffer).toBe("gap-pending");
|
|
785
|
+
expect(harness.snapshot().phase).toBe("playing");
|
|
786
|
+
|
|
787
|
+
// Play out the footage we do have, which is what releases the jump.
|
|
788
|
+
harness.replayers[0]!.currentTimeMs = CHUNK_MS;
|
|
789
|
+
await harness.tick(16);
|
|
790
|
+
|
|
791
|
+
expect(harness.snapshot().lastGap).toEqual({
|
|
792
|
+
fromIndex: 0,
|
|
793
|
+
toIndex: 2,
|
|
794
|
+
missingMs: CHUNK_MS,
|
|
795
|
+
});
|
|
796
|
+
expect(harness.replayers[0]!.added).toEqual([]);
|
|
797
|
+
expect(harness.replayers[0]!.isDestroyed).toBe(true);
|
|
798
|
+
|
|
799
|
+
// Playback resumed by re-anchoring on the next full snapshot, not by guessing.
|
|
800
|
+
expect(harness.replayers.length).toBe(2);
|
|
801
|
+
expect(dataOf(harness.replayers[1]!.initialEvents)).toEqual(
|
|
802
|
+
expect.arrayContaining([
|
|
803
|
+
{ chunkIndex: 2, sequence: 0 },
|
|
804
|
+
{ chunkIndex: 2, sequence: 1 },
|
|
805
|
+
]),
|
|
806
|
+
);
|
|
807
|
+
/* The viewer pressed Play; the jump must not land paused. */
|
|
808
|
+
expect(harness.snapshot().intent).toBe("playing");
|
|
809
|
+
expect(harness.replayers[1]!.playOffsets.length).toBeGreaterThan(0);
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
it("halts, retryable, rather than skipping when nothing can anchor after a failed chunk", async () => {
|
|
813
|
+
const harness: Harness = makeHarness({
|
|
814
|
+
entries: [makeEntry(0, { hasFullSnapshot: true }), makeEntry(1)],
|
|
815
|
+
omitChunkIndexes: [1],
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
await loadAndFlush(harness, 0, 0);
|
|
819
|
+
await harness.tick(16);
|
|
820
|
+
|
|
821
|
+
expect(harness.replayers[0]!.added).toEqual([]);
|
|
822
|
+
expect(harness.snapshot().phase).toBe("error");
|
|
823
|
+
expect(harness.snapshot().error?.message).toContain("no later snapshot");
|
|
824
|
+
expect(harness.snapshot().error?.retryable).toBe(true);
|
|
825
|
+
|
|
826
|
+
/*
|
|
827
|
+
* And it stops trying. The tick runs many times a second and the fed
|
|
828
|
+
* range can never advance past this, so retrying would re-POST /chunks
|
|
829
|
+
* for footage that is not coming back.
|
|
830
|
+
*/
|
|
831
|
+
const requestsAfterFailure: number = harness.requests.length;
|
|
832
|
+
await harness.tick(250);
|
|
833
|
+
await harness.tick(250);
|
|
834
|
+
await harness.tick(250);
|
|
835
|
+
|
|
836
|
+
expect(harness.snapshot().phase).toBe("error");
|
|
837
|
+
expect(harness.requests.length).toBe(requestsAfterFailure);
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
it("(a) advances lastFedChunkIndex only after addEvent took the events", async () => {
|
|
841
|
+
const harness: Harness = makeHarness({
|
|
842
|
+
entries: [
|
|
843
|
+
makeEntry(0, { hasFullSnapshot: true }),
|
|
844
|
+
makeEntry(1),
|
|
845
|
+
makeEntry(2),
|
|
846
|
+
makeEntry(3),
|
|
847
|
+
],
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
await loadAndFlush(harness, 0, 0);
|
|
851
|
+
expect(harness.engine.getDiagnostics().lastFedChunkIndex).toBe(1);
|
|
852
|
+
|
|
853
|
+
harness.replayers[0]!.addEventError = new Error("node 42 not found");
|
|
854
|
+
await harness.tick(16);
|
|
855
|
+
|
|
856
|
+
expect(harness.engine.getDiagnostics().lastFedChunkIndex).toBe(1);
|
|
857
|
+
expect(harness.snapshot().fedRange?.toMs).toBe(2 * CHUNK_MS);
|
|
858
|
+
expect(harness.snapshot().phase).toBe("error");
|
|
859
|
+
expect(harness.snapshot().error?.retryable).toBe(true);
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
it("treats a zero-event closing chunk as the end, not as a broken chunk", async () => {
|
|
863
|
+
/*
|
|
864
|
+
* The recorder closes a tab with an empty chunk that carries the close
|
|
865
|
+
* time. It is a terminator, not footage: playing past chunk 1 ends the
|
|
866
|
+
* recording cleanly instead of raising "could not be loaded".
|
|
867
|
+
*/
|
|
868
|
+
const harness: Harness = makeHarness({
|
|
869
|
+
entries: [
|
|
870
|
+
makeEntry(0, { hasFullSnapshot: true }),
|
|
871
|
+
makeEntry(1),
|
|
872
|
+
makeEntry(2, { eventCount: 0 }),
|
|
873
|
+
],
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
await loadAndFlush(harness, 0, 0);
|
|
877
|
+
harness.engine.dispatch({ type: "PLAY" });
|
|
878
|
+
|
|
879
|
+
expect(harness.snapshot().durationMs).toBe(2 * CHUNK_MS);
|
|
880
|
+
|
|
881
|
+
await harness.tick(16);
|
|
882
|
+
harness.live().emit("finish");
|
|
883
|
+
|
|
884
|
+
expect(harness.snapshot().phase).toBe("ended");
|
|
885
|
+
expect(harness.snapshot().error).toBeNull();
|
|
886
|
+
expect(harness.snapshot().currentTimeMs).toBe(2 * CHUNK_MS);
|
|
887
|
+
});
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
describe("ReplayEngine Replayer construction", () => {
|
|
891
|
+
it("borrows the next contiguous chunk when the anchor has a single event", async () => {
|
|
892
|
+
/*
|
|
893
|
+
* rrweb 2.1.1 throws "Replayer need at least 2 events." out of its
|
|
894
|
+
* constructor whenever liveMode is false and fewer than two events are
|
|
895
|
+
* passed. A one-event anchor is exactly what splitting an oversized
|
|
896
|
+
* FullSnapshot produces for its final part - the part that carries
|
|
897
|
+
* hasFullSnapshot.
|
|
898
|
+
*/
|
|
899
|
+
const harness: Harness = makeHarness({
|
|
900
|
+
entries: [makeEntry(0, { hasFullSnapshot: true }), makeEntry(1)],
|
|
901
|
+
eventsPerChunk: 1,
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
await loadAndFlush(harness, 0, 0);
|
|
905
|
+
|
|
906
|
+
expect(harness.snapshot().error).toBeNull();
|
|
907
|
+
expect(harness.replayers.length).toBe(1);
|
|
908
|
+
expect(harness.replayers[0]!.initialEvents.length).toBeGreaterThanOrEqual(
|
|
909
|
+
2,
|
|
910
|
+
);
|
|
911
|
+
});
|
|
912
|
+
|
|
913
|
+
it("gives a domain message when a single event is all there is", async () => {
|
|
914
|
+
const harness: Harness = makeHarness({
|
|
915
|
+
entries: [makeEntry(0, { hasFullSnapshot: true })],
|
|
916
|
+
eventsPerChunk: 1,
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
await loadAndFlush(harness, 0, 0);
|
|
920
|
+
|
|
921
|
+
expect(harness.replayers.length).toBe(0);
|
|
922
|
+
expect(harness.snapshot().phase).toBe("error");
|
|
923
|
+
expect(harness.snapshot().error?.message).toContain("too short to play");
|
|
924
|
+
// Never rrweb's own string.
|
|
925
|
+
expect(harness.snapshot().error?.message).not.toContain(
|
|
926
|
+
"Replayer need at least",
|
|
927
|
+
);
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
it("synthesises a Meta event for an anchor that is a lone FullSnapshot", async () => {
|
|
931
|
+
/*
|
|
932
|
+
* rrweb creates its iframe hidden and the ONLY thing that shows it is
|
|
933
|
+
* a Meta event. The recorder puts an oversized FullSnapshot alone in
|
|
934
|
+
* its chunk, with the Meta in the previous one, so every anchor on a
|
|
935
|
+
* large-DOM page used to play as an empty grey box with a moving clock.
|
|
936
|
+
*/
|
|
937
|
+
const harness: Harness = makeHarness({
|
|
938
|
+
entries: [makeEntry(0), makeEntry(1, { hasFullSnapshot: true })],
|
|
939
|
+
eventsForChunk: (
|
|
940
|
+
chunkIndex: number,
|
|
941
|
+
): Array<SessionReplayRecordedEvent> => {
|
|
942
|
+
if (chunkIndex === 0) {
|
|
943
|
+
return [
|
|
944
|
+
{
|
|
945
|
+
type: RRWEB_EVENT_TYPE_META,
|
|
946
|
+
timestamp: CLIENT_CLOCK_BASE_MS,
|
|
947
|
+
data: {
|
|
948
|
+
href: "https://app.example.com/big",
|
|
949
|
+
width: 1920,
|
|
950
|
+
height: 1080,
|
|
951
|
+
},
|
|
952
|
+
},
|
|
953
|
+
{
|
|
954
|
+
type: RRWEB_EVENT_TYPE_INCREMENTAL,
|
|
955
|
+
timestamp: CLIENT_CLOCK_BASE_MS + 10,
|
|
956
|
+
data: { source: 1 },
|
|
957
|
+
},
|
|
958
|
+
];
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
return [
|
|
962
|
+
{
|
|
963
|
+
type: RRWEB_EVENT_TYPE_FULL_SNAPSHOT,
|
|
964
|
+
timestamp: CLIENT_CLOCK_BASE_MS + CHUNK_MS,
|
|
965
|
+
data: { node: {} },
|
|
966
|
+
},
|
|
967
|
+
];
|
|
968
|
+
},
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
await loadAndFlush(harness, 1, CHUNK_MS);
|
|
972
|
+
|
|
973
|
+
expect(harness.snapshot().error).toBeNull();
|
|
974
|
+
expect(harness.replayers.length).toBe(1);
|
|
975
|
+
|
|
976
|
+
const first: SessionReplayRecordedEvent | undefined =
|
|
977
|
+
harness.replayers[0]!.initialEvents[0];
|
|
978
|
+
|
|
979
|
+
expect(first?.type).toBe(RRWEB_EVENT_TYPE_META);
|
|
980
|
+
/* Dimensions come from the previous chunk's real Meta, not a guess. */
|
|
981
|
+
expect(first?.data).toEqual(
|
|
982
|
+
expect.objectContaining({ width: 1920, height: 1080 }),
|
|
983
|
+
);
|
|
984
|
+
expect(first?.timestamp).toBeLessThanOrEqual(
|
|
985
|
+
harness.replayers[0]!.initialEvents[1]!.timestamp,
|
|
986
|
+
);
|
|
987
|
+
});
|
|
988
|
+
|
|
989
|
+
it("falls back to the header viewport for the synthesised Meta", async () => {
|
|
990
|
+
const harness: Harness = makeHarness({
|
|
991
|
+
entries: [makeEntry(3, { hasFullSnapshot: true }), makeEntry(4)],
|
|
992
|
+
headerViewport: { width: 390, height: 844 },
|
|
993
|
+
eventsForChunk: (
|
|
994
|
+
chunkIndex: number,
|
|
995
|
+
): Array<SessionReplayRecordedEvent> => {
|
|
996
|
+
return [
|
|
997
|
+
{
|
|
998
|
+
type:
|
|
999
|
+
chunkIndex === 3
|
|
1000
|
+
? RRWEB_EVENT_TYPE_FULL_SNAPSHOT
|
|
1001
|
+
: RRWEB_EVENT_TYPE_INCREMENTAL,
|
|
1002
|
+
timestamp: CLIENT_CLOCK_BASE_MS + chunkIndex * CHUNK_MS,
|
|
1003
|
+
data: {},
|
|
1004
|
+
},
|
|
1005
|
+
];
|
|
1006
|
+
},
|
|
1007
|
+
});
|
|
1008
|
+
|
|
1009
|
+
await loadAndFlush(harness, 3, 3 * CHUNK_MS);
|
|
1010
|
+
|
|
1011
|
+
const first: SessionReplayRecordedEvent | undefined =
|
|
1012
|
+
harness.replayers[0]!.initialEvents[0];
|
|
1013
|
+
|
|
1014
|
+
expect(first?.type).toBe(RRWEB_EVENT_TYPE_META);
|
|
1015
|
+
expect(first?.data).toEqual(
|
|
1016
|
+
expect.objectContaining({ width: 390, height: 844 }),
|
|
1017
|
+
);
|
|
1018
|
+
});
|
|
1019
|
+
});
|
|
1020
|
+
|
|
1021
|
+
describe("ReplayEngine playback clock", () => {
|
|
1022
|
+
it("reports offsets from the manifest, not from end-user event timestamps", async () => {
|
|
1023
|
+
/*
|
|
1024
|
+
* Event timestamps are raw Date.now() values from the recorded machine.
|
|
1025
|
+
* Deriving the segment base from them puts the playhead clockSkewMs away
|
|
1026
|
+
* from the bands and markers, which come from the manifest.
|
|
1027
|
+
*/
|
|
1028
|
+
const harness: Harness = makeHarness({
|
|
1029
|
+
entries: [
|
|
1030
|
+
makeEntry(0),
|
|
1031
|
+
makeEntry(1),
|
|
1032
|
+
makeEntry(2, { hasFullSnapshot: true }),
|
|
1033
|
+
],
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
await loadAndFlush(harness, 2, 2 * CHUNK_MS);
|
|
1037
|
+
|
|
1038
|
+
harness.replayers[0]!.currentTimeMs = 5000;
|
|
1039
|
+
await harness.tick(0);
|
|
1040
|
+
|
|
1041
|
+
// Chunk 2 starts at 30000ms into the session.
|
|
1042
|
+
expect(harness.snapshot().currentTimeMs).toBe(35000);
|
|
1043
|
+
});
|
|
1044
|
+
|
|
1045
|
+
it("(b) never moves the clock backwards except on SEEK", async () => {
|
|
1046
|
+
const harness: Harness = makeHarness({
|
|
1047
|
+
entries: [
|
|
1048
|
+
makeEntry(0, { hasFullSnapshot: true }),
|
|
1049
|
+
makeEntry(1),
|
|
1050
|
+
makeEntry(2),
|
|
1051
|
+
makeEntry(3),
|
|
1052
|
+
],
|
|
1053
|
+
});
|
|
1054
|
+
|
|
1055
|
+
await loadAndFlush(harness, 0, 0);
|
|
1056
|
+
harness.engine.dispatch({ type: "PLAY" });
|
|
1057
|
+
|
|
1058
|
+
for (let i: number = 0; i < 20; i++) {
|
|
1059
|
+
await harness.tick(500);
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
/* A stall in the middle: rrweb drains, the engine extends, playback resumes. */
|
|
1063
|
+
harness.live().emit("finish");
|
|
1064
|
+
await flush();
|
|
1065
|
+
|
|
1066
|
+
for (let i: number = 0; i < 10; i++) {
|
|
1067
|
+
await harness.tick(500);
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
let previous: number = -1;
|
|
1071
|
+
|
|
1072
|
+
for (const snapshot of harness.snapshots) {
|
|
1073
|
+
expect(snapshot.currentTimeMs).toBeGreaterThanOrEqual(previous);
|
|
1074
|
+
previous = snapshot.currentTimeMs;
|
|
1075
|
+
}
|
|
1076
|
+
});
|
|
1077
|
+
|
|
1078
|
+
it("(c) reports the seek target during a rebuild, never 0", async () => {
|
|
1079
|
+
const harness: Harness = makeHarness({
|
|
1080
|
+
entries: [
|
|
1081
|
+
makeEntry(0, { hasFullSnapshot: true }),
|
|
1082
|
+
makeEntry(1),
|
|
1083
|
+
makeEntry(2, { hasFullSnapshot: true }),
|
|
1084
|
+
makeEntry(3),
|
|
1085
|
+
],
|
|
1086
|
+
deferFetch: true,
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
harness.engine.dispatch({ type: "LOAD", anchorChunkIndex: 0, targetMs: 0 });
|
|
1090
|
+
harness.resolveFetch();
|
|
1091
|
+
await flush();
|
|
1092
|
+
harness.resolveFetch();
|
|
1093
|
+
await flush();
|
|
1094
|
+
|
|
1095
|
+
expect(harness.replayers.length).toBe(1);
|
|
1096
|
+
|
|
1097
|
+
const target: number = 2 * CHUNK_MS + 4000;
|
|
1098
|
+
const publishedBeforeSeek: number = harness.snapshots.length;
|
|
1099
|
+
harness.engine.dispatch({ type: "SEEK", offsetMs: target, token: 1 });
|
|
1100
|
+
|
|
1101
|
+
/* The fetch for chunk 2 is still in flight: this is the rebuild window. */
|
|
1102
|
+
expect(harness.snapshot().phase).toBe("seeking");
|
|
1103
|
+
expect(harness.snapshot().currentTimeMs).toBe(target);
|
|
1104
|
+
expect(harness.snapshot().pendingSeekMs).toBe(target);
|
|
1105
|
+
|
|
1106
|
+
await harness.tick(250);
|
|
1107
|
+
expect(harness.snapshot().currentTimeMs).toBe(target);
|
|
1108
|
+
|
|
1109
|
+
harness.resolveFetch();
|
|
1110
|
+
await flush();
|
|
1111
|
+
harness.resolveFetch();
|
|
1112
|
+
await flush();
|
|
1113
|
+
|
|
1114
|
+
expect(harness.snapshot().phase).toBe("paused");
|
|
1115
|
+
expect(harness.snapshot().currentTimeMs).toBe(target);
|
|
1116
|
+
expect(harness.snapshot().pendingSeekMs).toBeNull();
|
|
1117
|
+
expect(harness.replayers[1]!.pauseOffsets).toEqual([4000]);
|
|
1118
|
+
|
|
1119
|
+
/* Every snapshot published from the seek onward carries the target. */
|
|
1120
|
+
for (const snapshot of harness.snapshots.slice(publishedBeforeSeek)) {
|
|
1121
|
+
expect(snapshot.currentTimeMs).toBe(target);
|
|
1122
|
+
}
|
|
1123
|
+
});
|
|
1124
|
+
});
|
|
1125
|
+
|
|
1126
|
+
describe("ReplayEngine transport state", () => {
|
|
1127
|
+
it("honours a Play pressed while the first chunk is still loading", async () => {
|
|
1128
|
+
const harness: Harness = makeHarness({
|
|
1129
|
+
entries: [makeEntry(0, { hasFullSnapshot: true }), makeEntry(1)],
|
|
1130
|
+
deferFetch: true,
|
|
1131
|
+
});
|
|
1132
|
+
|
|
1133
|
+
harness.engine.dispatch({ type: "LOAD", anchorChunkIndex: 0, targetMs: 0 });
|
|
1134
|
+
await flush();
|
|
1135
|
+
expect(harness.replayers.length).toBe(0);
|
|
1136
|
+
expect(harness.snapshot().phase).toBe("seeking");
|
|
1137
|
+
|
|
1138
|
+
// The viewer presses Play while the fetch is still in flight.
|
|
1139
|
+
harness.engine.dispatch({ type: "PLAY" });
|
|
1140
|
+
expect(harness.snapshot().phase).toBe("buffering");
|
|
1141
|
+
|
|
1142
|
+
harness.resolveFetch();
|
|
1143
|
+
await flush();
|
|
1144
|
+
|
|
1145
|
+
expect(harness.replayers.length).toBe(1);
|
|
1146
|
+
expect(harness.replayers[0]!.playOffsets).toEqual([0]);
|
|
1147
|
+
expect(harness.replayers[0]!.pauseOffsets).toEqual([]);
|
|
1148
|
+
expect(harness.snapshot().phase).toBe("playing");
|
|
1149
|
+
});
|
|
1150
|
+
|
|
1151
|
+
it("applies a Play pressed after the Replayer already exists", async () => {
|
|
1152
|
+
/*
|
|
1153
|
+
* The plain case, and the one the reported "Play does nothing" was
|
|
1154
|
+
* about. Anything that leaves the intent and the applied state out of
|
|
1155
|
+
* step - a rebuild, an error, a Replayer swapped between the press and
|
|
1156
|
+
* the apply - used to leave the button saying "playing" over a stage
|
|
1157
|
+
* that was not. Here the phase is DERIVED from the intent, so the
|
|
1158
|
+
* button and the stage cannot disagree.
|
|
1159
|
+
*/
|
|
1160
|
+
const harness: Harness = makeHarness({
|
|
1161
|
+
entries: [makeEntry(0, { hasFullSnapshot: true }), makeEntry(1)],
|
|
1162
|
+
});
|
|
1163
|
+
|
|
1164
|
+
await loadAndFlush(harness, 0, 0);
|
|
1165
|
+
|
|
1166
|
+
expect(harness.replayers.length).toBe(1);
|
|
1167
|
+
expect(harness.replayers[0]!.playOffsets).toEqual([]);
|
|
1168
|
+
expect(harness.snapshot().phase).toBe("paused");
|
|
1169
|
+
|
|
1170
|
+
harness.engine.dispatch({ type: "PLAY" });
|
|
1171
|
+
|
|
1172
|
+
expect(harness.replayers[0]!.playOffsets).toEqual([0]);
|
|
1173
|
+
expect(harness.snapshot().phase).toBe("playing");
|
|
1174
|
+
|
|
1175
|
+
/* Idempotent: a second PLAY does not restart rrweb's timer. */
|
|
1176
|
+
harness.engine.dispatch({ type: "PLAY" });
|
|
1177
|
+
expect(harness.replayers[0]!.playOffsets).toEqual([0]);
|
|
1178
|
+
|
|
1179
|
+
harness.engine.dispatch({ type: "PAUSE" });
|
|
1180
|
+
|
|
1181
|
+
/*
|
|
1182
|
+
* Pausing is applied to the SAME Replayer, not by rebuilding one. The
|
|
1183
|
+
* leading 0 is the build's own pause(withinSegment); the undefined is
|
|
1184
|
+
* this press, which pauses in place rather than seeking.
|
|
1185
|
+
*/
|
|
1186
|
+
expect(harness.replayers.length).toBe(1);
|
|
1187
|
+
expect(harness.replayers[0]!.pauseOffsets).toEqual([0, undefined]);
|
|
1188
|
+
expect(harness.snapshot().phase).toBe("paused");
|
|
1189
|
+
});
|
|
1190
|
+
|
|
1191
|
+
it("does not re-issue play on a rebuild that already applied the intent", async () => {
|
|
1192
|
+
/*
|
|
1193
|
+
* A rebuild - a seek across a snapshot anchor, or a gap jump - applies
|
|
1194
|
+
* the current transport state itself. Re-asserting it afterwards would
|
|
1195
|
+
* restart rrweb's timer at the same offset on every one of them.
|
|
1196
|
+
*/
|
|
1197
|
+
const harness: Harness = makeHarness({
|
|
1198
|
+
entries: [
|
|
1199
|
+
makeEntry(0, { hasFullSnapshot: true }),
|
|
1200
|
+
makeEntry(1),
|
|
1201
|
+
makeEntry(2, { hasFullSnapshot: true }),
|
|
1202
|
+
makeEntry(3),
|
|
1203
|
+
],
|
|
1204
|
+
});
|
|
1205
|
+
|
|
1206
|
+
await loadAndFlush(harness, 0, 0);
|
|
1207
|
+
harness.engine.dispatch({ type: "PLAY" });
|
|
1208
|
+
|
|
1209
|
+
expect(harness.replayers.length).toBe(1);
|
|
1210
|
+
expect(harness.replayers[0]!.playOffsets).toEqual([0]);
|
|
1211
|
+
|
|
1212
|
+
/* Seek into chunk 2, which anchors a new segment. */
|
|
1213
|
+
harness.engine.dispatch({
|
|
1214
|
+
type: "SEEK",
|
|
1215
|
+
offsetMs: 2 * CHUNK_MS + 1000,
|
|
1216
|
+
token: 1,
|
|
1217
|
+
});
|
|
1218
|
+
await flush();
|
|
1219
|
+
|
|
1220
|
+
expect(harness.replayers.length).toBe(2);
|
|
1221
|
+
expect(harness.replayers[1]!.playOffsets).toEqual([1000]);
|
|
1222
|
+
});
|
|
1223
|
+
|
|
1224
|
+
it("(d) turns a Finish with footage left into a stall and extends immediately", async () => {
|
|
1225
|
+
/*
|
|
1226
|
+
* rrweb emits Finish whenever it drains what it has been given, which
|
|
1227
|
+
* with chunk streaming is a stall rather than the end. The old stage
|
|
1228
|
+
* waited for the next 200ms tick (or the next press of Play) to fetch;
|
|
1229
|
+
* the engine fetches NOW, and the phase says "buffering" meanwhile so
|
|
1230
|
+
* the frozen picture is explained.
|
|
1231
|
+
*/
|
|
1232
|
+
const harness: Harness = makeHarness({
|
|
1233
|
+
entries: [
|
|
1234
|
+
makeEntry(0, { hasFullSnapshot: true }),
|
|
1235
|
+
makeEntry(1),
|
|
1236
|
+
makeEntry(2),
|
|
1237
|
+
],
|
|
1238
|
+
deferFetch: true,
|
|
1239
|
+
});
|
|
1240
|
+
|
|
1241
|
+
harness.engine.dispatch({ type: "LOAD", anchorChunkIndex: 0, targetMs: 0 });
|
|
1242
|
+
harness.engine.dispatch({ type: "PLAY" });
|
|
1243
|
+
harness.resolveFetch();
|
|
1244
|
+
await flush();
|
|
1245
|
+
|
|
1246
|
+
expect(harness.replayers.length).toBe(1);
|
|
1247
|
+
expect(harness.replayers[0]!.added).toEqual([]);
|
|
1248
|
+
|
|
1249
|
+
/* The stall: rrweb ran out of events while more chunks exist. */
|
|
1250
|
+
harness.replayers[0]!.emit("finish");
|
|
1251
|
+
|
|
1252
|
+
expect(harness.snapshot().intent).toBe("playing");
|
|
1253
|
+
expect(harness.snapshot().buffer).toBe("stalled");
|
|
1254
|
+
expect(harness.snapshot().phase).toBe("buffering");
|
|
1255
|
+
expect(harness.snapshot().bufferingSinceMs).toBe(harness.now());
|
|
1256
|
+
|
|
1257
|
+
/* The page fetch that loadFirst queued lands. */
|
|
1258
|
+
harness.resolveFetch();
|
|
1259
|
+
await flush();
|
|
1260
|
+
|
|
1261
|
+
expect(harness.replayers[0]!.added.length).toBeGreaterThan(0);
|
|
1262
|
+
/* And it resumed the cast rather than only topping the buffer up. */
|
|
1263
|
+
expect(harness.replayers[0]!.playOffsets.length).toBeGreaterThan(1);
|
|
1264
|
+
expect(harness.snapshot().phase).toBe("playing");
|
|
1265
|
+
expect(harness.snapshot().bufferingSinceMs).toBeNull();
|
|
1266
|
+
});
|
|
1267
|
+
|
|
1268
|
+
it("kicks a fetch immediately when Play resumes a paused stall", async () => {
|
|
1269
|
+
const harness: Harness = makeHarness({
|
|
1270
|
+
entries: [
|
|
1271
|
+
makeEntry(0, { hasFullSnapshot: true }),
|
|
1272
|
+
makeEntry(1),
|
|
1273
|
+
makeEntry(2),
|
|
1274
|
+
],
|
|
1275
|
+
deferFetch: true,
|
|
1276
|
+
});
|
|
1277
|
+
|
|
1278
|
+
harness.engine.dispatch({ type: "LOAD", anchorChunkIndex: 0, targetMs: 0 });
|
|
1279
|
+
harness.engine.dispatch({ type: "PLAY" });
|
|
1280
|
+
harness.resolveFetch();
|
|
1281
|
+
await flush();
|
|
1282
|
+
|
|
1283
|
+
harness.replayers[0]!.emit("finish");
|
|
1284
|
+
harness.engine.dispatch({ type: "PAUSE" });
|
|
1285
|
+
|
|
1286
|
+
expect(harness.snapshot().phase).toBe("paused");
|
|
1287
|
+
expect(harness.replayers[0]!.added).toEqual([]);
|
|
1288
|
+
|
|
1289
|
+
const requestsBefore: number = harness.requests.length;
|
|
1290
|
+
harness.engine.dispatch({ type: "PLAY" });
|
|
1291
|
+
|
|
1292
|
+
expect(harness.snapshot().phase).toBe("buffering");
|
|
1293
|
+
expect(harness.requests.length).toBeGreaterThanOrEqual(requestsBefore);
|
|
1294
|
+
|
|
1295
|
+
harness.resolveFetch();
|
|
1296
|
+
await flush();
|
|
1297
|
+
|
|
1298
|
+
expect(harness.replayers[0]!.added.length).toBeGreaterThan(0);
|
|
1299
|
+
expect(harness.replayers[0]!.playOffsets.length).toBeGreaterThan(1);
|
|
1300
|
+
expect(harness.snapshot().phase).toBe("playing");
|
|
1301
|
+
});
|
|
1302
|
+
|
|
1303
|
+
it("rewinds to the first playable moment when Play is pressed at the end", async () => {
|
|
1304
|
+
const harness: Harness = makeHarness({
|
|
1305
|
+
entries: [makeEntry(0, { hasFullSnapshot: true }), makeEntry(1)],
|
|
1306
|
+
});
|
|
1307
|
+
|
|
1308
|
+
await loadAndFlush(harness, 0, 0);
|
|
1309
|
+
harness.engine.dispatch({ type: "PLAY" });
|
|
1310
|
+
harness.replayers[0]!.currentTimeMs = 2 * CHUNK_MS;
|
|
1311
|
+
await harness.tick(0);
|
|
1312
|
+
harness.replayers[0]!.emit("finish");
|
|
1313
|
+
|
|
1314
|
+
expect(harness.snapshot().phase).toBe("ended");
|
|
1315
|
+
expect(harness.snapshot().intent).toBe("paused");
|
|
1316
|
+
|
|
1317
|
+
harness.engine.dispatch({ type: "PLAY" });
|
|
1318
|
+
await flush();
|
|
1319
|
+
|
|
1320
|
+
/* Same anchor, inside the fed range: no rebuild, play(0). */
|
|
1321
|
+
expect(harness.replayers.length).toBe(1);
|
|
1322
|
+
expect(
|
|
1323
|
+
harness.replayers[0]!.playOffsets[
|
|
1324
|
+
harness.replayers[0]!.playOffsets.length - 1
|
|
1325
|
+
],
|
|
1326
|
+
).toBe(0);
|
|
1327
|
+
expect(harness.snapshot().phase).toBe("playing");
|
|
1328
|
+
expect(harness.snapshot().currentTimeMs).toBe(0);
|
|
1329
|
+
});
|
|
1330
|
+
});
|
|
1331
|
+
|
|
1332
|
+
describe("ReplayEngine Replayer configuration", () => {
|
|
1333
|
+
it("draws the pointer trail, so recorded mouse movement is visible", async () => {
|
|
1334
|
+
const harness: Harness = makeHarness({
|
|
1335
|
+
entries: [makeEntry(0, { hasFullSnapshot: true })],
|
|
1336
|
+
});
|
|
1337
|
+
|
|
1338
|
+
await loadAndFlush(harness, 0, 0);
|
|
1339
|
+
|
|
1340
|
+
const config: Record<string, unknown> =
|
|
1341
|
+
harness.replayers[0]!.constructorConfig;
|
|
1342
|
+
|
|
1343
|
+
expect(config["mouseTail"]).not.toBe(false);
|
|
1344
|
+
expect(config["mouseTail"]).toEqual(
|
|
1345
|
+
expect.objectContaining({ lineWidth: expect.any(Number) }),
|
|
1346
|
+
);
|
|
1347
|
+
});
|
|
1348
|
+
|
|
1349
|
+
it("bounds how fast rrweb may fast-forward and uses the virtual DOM", async () => {
|
|
1350
|
+
const harness: Harness = makeHarness({
|
|
1351
|
+
entries: [makeEntry(0, { hasFullSnapshot: true })],
|
|
1352
|
+
});
|
|
1353
|
+
|
|
1354
|
+
await loadAndFlush(harness, 0, 0);
|
|
1355
|
+
|
|
1356
|
+
const config: Record<string, unknown> =
|
|
1357
|
+
harness.replayers[0]!.constructorConfig;
|
|
1358
|
+
|
|
1359
|
+
expect(config["maxSpeed"]).toBe(8);
|
|
1360
|
+
expect(config["useVirtualDom"]).toBe(true);
|
|
1361
|
+
});
|
|
1362
|
+
|
|
1363
|
+
it("never enables canvas replay, which would drop the iframe sandbox", async () => {
|
|
1364
|
+
const harness: Harness = makeHarness({
|
|
1365
|
+
entries: [makeEntry(0, { hasFullSnapshot: true })],
|
|
1366
|
+
});
|
|
1367
|
+
|
|
1368
|
+
await loadAndFlush(harness, 0, 0);
|
|
1369
|
+
|
|
1370
|
+
expect(harness.replayers[0]!.constructorConfig["UNSAFE_replayCanvas"]).toBe(
|
|
1371
|
+
false,
|
|
1372
|
+
);
|
|
1373
|
+
});
|
|
1374
|
+
|
|
1375
|
+
it("(f) never sets rrweb's own skipInactive, even when the viewer turns skip-idle on", async () => {
|
|
1376
|
+
const harness: Harness = makeHarness({
|
|
1377
|
+
entries: [makeEntry(0, { hasFullSnapshot: true }), makeEntry(1)],
|
|
1378
|
+
});
|
|
1379
|
+
|
|
1380
|
+
harness.engine.dispatch({ type: "SET_SKIP_INACTIVE", enabled: true });
|
|
1381
|
+
await loadAndFlush(harness, 0, 0);
|
|
1382
|
+
harness.engine.dispatch({ type: "SET_SPEED", speed: 4 });
|
|
1383
|
+
|
|
1384
|
+
expect(harness.snapshot().skipInactive).toBe(true);
|
|
1385
|
+
expect(harness.replayers[0]!.constructorConfig["skipInactive"]).toBe(false);
|
|
1386
|
+
|
|
1387
|
+
for (const config of harness.replayers[0]!.configs) {
|
|
1388
|
+
expect(config["skipInactive"]).not.toBe(true);
|
|
1389
|
+
}
|
|
1390
|
+
});
|
|
1391
|
+
|
|
1392
|
+
it("raises the feed-ahead window with the speed", async () => {
|
|
1393
|
+
const harness: Harness = makeHarness({
|
|
1394
|
+
entries: [makeEntry(0, { hasFullSnapshot: true })],
|
|
1395
|
+
});
|
|
1396
|
+
|
|
1397
|
+
expect(harness.snapshot().feedAheadMs).toBe(REPLAY_FEED_AHEAD_MIN_MS);
|
|
1398
|
+
|
|
1399
|
+
harness.engine.dispatch({ type: "SET_SPEED", speed: 8 });
|
|
1400
|
+
expect(harness.snapshot().feedAheadMs).toBe(160000);
|
|
1401
|
+
|
|
1402
|
+
harness.engine.dispatch({ type: "SET_SPEED", speed: 1 });
|
|
1403
|
+
expect(harness.snapshot().feedAheadMs).toBe(REPLAY_FEED_AHEAD_MIN_MS);
|
|
1404
|
+
|
|
1405
|
+
await loadAndFlush(harness, 0, 0);
|
|
1406
|
+
harness.engine.dispatch({ type: "SET_SPEED", speed: 2 });
|
|
1407
|
+
|
|
1408
|
+
expect(harness.replayers[0]!.configs).toContainEqual({ speed: 2 });
|
|
1409
|
+
});
|
|
1410
|
+
});
|
|
1411
|
+
|
|
1412
|
+
describe("ReplayEngine finish handling", () => {
|
|
1413
|
+
it("does not stop playback when rrweb drains its buffer mid-recording", async () => {
|
|
1414
|
+
const harness: Harness = makeHarness({
|
|
1415
|
+
entries: [
|
|
1416
|
+
makeEntry(0, { hasFullSnapshot: true }),
|
|
1417
|
+
makeEntry(1),
|
|
1418
|
+
makeEntry(2),
|
|
1419
|
+
],
|
|
1420
|
+
});
|
|
1421
|
+
|
|
1422
|
+
await loadAndFlush(harness, 0, 0);
|
|
1423
|
+
harness.engine.dispatch({ type: "PLAY" });
|
|
1424
|
+
|
|
1425
|
+
const finish: Array<(payload: unknown) => void> | undefined =
|
|
1426
|
+
harness.replayers[0]!.handlers.get("finish");
|
|
1427
|
+
|
|
1428
|
+
expect(finish).toBeDefined();
|
|
1429
|
+
|
|
1430
|
+
harness.replayers[0]!.emit("finish");
|
|
1431
|
+
|
|
1432
|
+
/*
|
|
1433
|
+
* There is still footage to feed, so this Finish is a stall waiting on
|
|
1434
|
+
* /chunks. Reporting it as "not playing" would stop the session for good:
|
|
1435
|
+
* later addEvent calls append to a machine that has already ended.
|
|
1436
|
+
*/
|
|
1437
|
+
expect(harness.snapshot().intent).toBe("playing");
|
|
1438
|
+
expect(harness.snapshot().phase).not.toBe("ended");
|
|
1439
|
+
});
|
|
1440
|
+
|
|
1441
|
+
it("stops playback when the recording genuinely ends", async () => {
|
|
1442
|
+
const harness: Harness = makeHarness({
|
|
1443
|
+
entries: [makeEntry(0, { hasFullSnapshot: true })],
|
|
1444
|
+
});
|
|
1445
|
+
|
|
1446
|
+
await loadAndFlush(harness, 0, 0);
|
|
1447
|
+
harness.engine.dispatch({ type: "PLAY" });
|
|
1448
|
+
harness.replayers[0]!.emit("finish");
|
|
1449
|
+
|
|
1450
|
+
expect(harness.snapshot().phase).toBe("ended");
|
|
1451
|
+
expect(harness.snapshot().intent).toBe("paused");
|
|
1452
|
+
});
|
|
1453
|
+
|
|
1454
|
+
it("ignores a Finish from a Replayer that has been retired", async () => {
|
|
1455
|
+
/*
|
|
1456
|
+
* rrweb schedules Finish 50ms after the last cast and destroy() does
|
|
1457
|
+
* not cancel it. Without the identity guard a retired Replayer's
|
|
1458
|
+
* Finish landed in the NEW segment and paused it, so a quarter of gap
|
|
1459
|
+
* jumps used to land with the button reading Play.
|
|
1460
|
+
*/
|
|
1461
|
+
const harness: Harness = makeHarness({
|
|
1462
|
+
entries: [
|
|
1463
|
+
makeEntry(0, { hasFullSnapshot: true }),
|
|
1464
|
+
makeEntry(1),
|
|
1465
|
+
makeEntry(2, { hasFullSnapshot: true }),
|
|
1466
|
+
],
|
|
1467
|
+
});
|
|
1468
|
+
|
|
1469
|
+
await loadAndFlush(harness, 0, 0);
|
|
1470
|
+
harness.engine.dispatch({ type: "PLAY" });
|
|
1471
|
+
harness.engine.dispatch({ type: "SEEK", offsetMs: 2 * CHUNK_MS, token: 1 });
|
|
1472
|
+
await flush();
|
|
1473
|
+
|
|
1474
|
+
expect(harness.replayers.length).toBe(2);
|
|
1475
|
+
|
|
1476
|
+
const before: ReplayEngineSnapshot = harness.snapshot();
|
|
1477
|
+
harness.replayers[0]!.emit("finish");
|
|
1478
|
+
|
|
1479
|
+
expect(harness.snapshot().intent).toBe(before.intent);
|
|
1480
|
+
expect(harness.snapshot().buffer).toBe(before.buffer);
|
|
1481
|
+
expect(harness.snapshot().phase).toBe("playing");
|
|
1482
|
+
});
|
|
1483
|
+
});
|
|
1484
|
+
|
|
1485
|
+
describe("ReplayEngine seeks", () => {
|
|
1486
|
+
it("feeds forward into the live Replayer for a seek ahead of the fed range (case 2)", async () => {
|
|
1487
|
+
/*
|
|
1488
|
+
* ArrowRight three times, or a marker 40s ahead inside a 60s checkout
|
|
1489
|
+
* interval, used to destroy the Replayer, drop the decoded LRU,
|
|
1490
|
+
* re-fetch and rebuild from the snapshot - a blank stage for a small
|
|
1491
|
+
* forward seek that a video player would treat as instantaneous.
|
|
1492
|
+
*/
|
|
1493
|
+
const harness: Harness = makeHarness({
|
|
1494
|
+
entries: Array.from(
|
|
1495
|
+
{ length: 8 },
|
|
1496
|
+
(_unused: unknown, index: number): SessionReplayChunkManifestEntry => {
|
|
1497
|
+
return makeEntry(index, { hasFullSnapshot: index === 0 });
|
|
1498
|
+
},
|
|
1499
|
+
),
|
|
1500
|
+
});
|
|
1501
|
+
|
|
1502
|
+
await loadAndFlush(harness, 0, 0);
|
|
1503
|
+
expect(harness.snapshot().fedRange?.toMs).toBe(2 * CHUNK_MS);
|
|
1504
|
+
|
|
1505
|
+
const target: number = 4 * CHUNK_MS + 1000;
|
|
1506
|
+
harness.engine.dispatch({ type: "SEEK", offsetMs: target, token: 1 });
|
|
1507
|
+
|
|
1508
|
+
expect(harness.snapshot().phase).toBe("seeking");
|
|
1509
|
+
expect(harness.snapshot().currentTimeMs).toBe(target);
|
|
1510
|
+
|
|
1511
|
+
await flush();
|
|
1512
|
+
|
|
1513
|
+
expect(harness.replayers.length).toBe(1);
|
|
1514
|
+
|
|
1515
|
+
const fedFirstEvents: Array<unknown> = dataOf(
|
|
1516
|
+
harness.replayers[0]!.added,
|
|
1517
|
+
).filter((data: unknown): boolean => {
|
|
1518
|
+
return (data as { sequence: number }).sequence === 0;
|
|
1519
|
+
});
|
|
1520
|
+
|
|
1521
|
+
/* Chunks 2, 3 and 4 in order reached the target... */
|
|
1522
|
+
expect(fedFirstEvents.slice(0, 3)).toEqual([
|
|
1523
|
+
{ chunkIndex: 2, sequence: 0 },
|
|
1524
|
+
{ chunkIndex: 3, sequence: 0 },
|
|
1525
|
+
{ chunkIndex: 4, sequence: 0 },
|
|
1526
|
+
]);
|
|
1527
|
+
expect(harness.replayers[0]!.pauseOffsets).toEqual([0, target]);
|
|
1528
|
+
expect(harness.snapshot().phase).toBe("paused");
|
|
1529
|
+
/* ...and the feeder kept its 30s of headroom past the new playhead. */
|
|
1530
|
+
expect(harness.snapshot().fedRange?.toMs).toBeGreaterThanOrEqual(
|
|
1531
|
+
5 * CHUNK_MS,
|
|
1532
|
+
);
|
|
1533
|
+
expect(harness.snapshot().fedRange?.toMs).toBeGreaterThanOrEqual(
|
|
1534
|
+
target + REPLAY_FEED_AHEAD_MIN_MS,
|
|
1535
|
+
);
|
|
1536
|
+
});
|
|
1537
|
+
|
|
1538
|
+
it("rebuilds instead of feeding when the seek is more than 90s past the fed range", async () => {
|
|
1539
|
+
const harness: Harness = makeHarness({
|
|
1540
|
+
entries: Array.from(
|
|
1541
|
+
{ length: 12 },
|
|
1542
|
+
(_unused: unknown, index: number): SessionReplayChunkManifestEntry => {
|
|
1543
|
+
return makeEntry(index, { hasFullSnapshot: index === 0 });
|
|
1544
|
+
},
|
|
1545
|
+
),
|
|
1546
|
+
});
|
|
1547
|
+
|
|
1548
|
+
await loadAndFlush(harness, 0, 0);
|
|
1549
|
+
|
|
1550
|
+
/* fedUntil is 30s; 9 * 15s = 135s is 105s past it. */
|
|
1551
|
+
harness.engine.dispatch({ type: "SEEK", offsetMs: 9 * CHUNK_MS, token: 1 });
|
|
1552
|
+
await flush();
|
|
1553
|
+
|
|
1554
|
+
expect(harness.replayers.length).toBe(2);
|
|
1555
|
+
expect(harness.snapshot().currentTimeMs).toBe(9 * CHUNK_MS);
|
|
1556
|
+
});
|
|
1557
|
+
|
|
1558
|
+
it("seeks within the fed range without a rebuild and clears a stall", async () => {
|
|
1559
|
+
const harness: Harness = makeHarness({
|
|
1560
|
+
entries: [
|
|
1561
|
+
makeEntry(0, { hasFullSnapshot: true }),
|
|
1562
|
+
makeEntry(1),
|
|
1563
|
+
makeEntry(2),
|
|
1564
|
+
],
|
|
1565
|
+
deferFetch: true,
|
|
1566
|
+
});
|
|
1567
|
+
|
|
1568
|
+
harness.engine.dispatch({ type: "LOAD", anchorChunkIndex: 0, targetMs: 0 });
|
|
1569
|
+
harness.engine.dispatch({ type: "PLAY" });
|
|
1570
|
+
harness.resolveFetch();
|
|
1571
|
+
await flush();
|
|
1572
|
+
|
|
1573
|
+
harness.replayers[0]!.emit("finish");
|
|
1574
|
+
expect(harness.snapshot().buffer).toBe("stalled");
|
|
1575
|
+
|
|
1576
|
+
/* Back into footage rrweb already holds. */
|
|
1577
|
+
harness.engine.dispatch({ type: "SEEK", offsetMs: 5000, token: 1 });
|
|
1578
|
+
|
|
1579
|
+
expect(harness.replayers.length).toBe(1);
|
|
1580
|
+
expect(harness.replayers[0]!.playOffsets).toContain(5000);
|
|
1581
|
+
expect(harness.snapshot().buffer).toBe("ok");
|
|
1582
|
+
expect(harness.snapshot().phase).toBe("playing");
|
|
1583
|
+
expect(harness.snapshot().currentTimeMs).toBe(5000);
|
|
1584
|
+
});
|
|
1585
|
+
|
|
1586
|
+
it("clamps a seek before the first snapshot onto the earliest playable moment with a notice", async () => {
|
|
1587
|
+
/*
|
|
1588
|
+
* Whenever chunk 0 cannot anchor - the oversized-snapshot layout, or a
|
|
1589
|
+
* session whose opening chunks were lost - pressing Play at the end,
|
|
1590
|
+
* ArrowLeft to the start, or a ?t=0 link used to show a red "no full
|
|
1591
|
+
* snapshot before that point" banner and nothing played.
|
|
1592
|
+
*/
|
|
1593
|
+
const harness: Harness = makeHarness({
|
|
1594
|
+
entries: [
|
|
1595
|
+
makeEntry(0),
|
|
1596
|
+
makeEntry(1, { hasFullSnapshot: true }),
|
|
1597
|
+
makeEntry(2),
|
|
1598
|
+
],
|
|
1599
|
+
});
|
|
1600
|
+
|
|
1601
|
+
await loadAndFlush(harness, 1, CHUNK_MS);
|
|
1602
|
+
harness.engine.dispatch({ type: "SEEK", offsetMs: 0, token: 1 });
|
|
1603
|
+
await flush();
|
|
1604
|
+
|
|
1605
|
+
expect(harness.snapshot().error).toBeNull();
|
|
1606
|
+
expect(harness.snapshot().phase).toBe("paused");
|
|
1607
|
+
expect(harness.snapshot().currentTimeMs).toBe(CHUNK_MS);
|
|
1608
|
+
expect(harness.snapshot().notice?.kind).toBe("seek-clamped");
|
|
1609
|
+
expect(harness.snapshot().notice?.message).toContain("0:15");
|
|
1610
|
+
expect(harness.snapshot().notice?.landedAtMs).toBe(CHUNK_MS);
|
|
1611
|
+
expect(harness.replayers.length).toBe(1);
|
|
1612
|
+
expect(harness.snapshot().earliestPlayableMs).toBe(CHUNK_MS);
|
|
1613
|
+
});
|
|
1614
|
+
|
|
1615
|
+
it("re-seeks when the same offset arrives with a new token, and ignores a repeated token", async () => {
|
|
1616
|
+
const harness: Harness = makeHarness({
|
|
1617
|
+
entries: [makeEntry(0, { hasFullSnapshot: true }), makeEntry(1)],
|
|
1618
|
+
});
|
|
1619
|
+
|
|
1620
|
+
await loadAndFlush(harness, 0, 0);
|
|
1621
|
+
|
|
1622
|
+
harness.engine.dispatch({ type: "SEEK", offsetMs: 3000, token: 7 });
|
|
1623
|
+
harness.engine.dispatch({ type: "SEEK", offsetMs: 3000, token: 7 });
|
|
1624
|
+
harness.engine.dispatch({ type: "SEEK", offsetMs: 3000, token: 8 });
|
|
1625
|
+
|
|
1626
|
+
expect(harness.replayers[0]!.pauseOffsets).toEqual([0, 3000, 3000]);
|
|
1627
|
+
});
|
|
1628
|
+
});
|
|
1629
|
+
|
|
1630
|
+
describe("ReplayEngine idle skipping", () => {
|
|
1631
|
+
const idleEntries: Array<SessionReplayChunkManifestEntry> = Array.from(
|
|
1632
|
+
{ length: 8 },
|
|
1633
|
+
(_unused: unknown, index: number): SessionReplayChunkManifestEntry => {
|
|
1634
|
+
return makeEntry(index, {
|
|
1635
|
+
hasFullSnapshot: index === 0,
|
|
1636
|
+
eventCount: index >= 2 && index <= 5 ? 1 : 10,
|
|
1637
|
+
});
|
|
1638
|
+
},
|
|
1639
|
+
);
|
|
1640
|
+
|
|
1641
|
+
it("publishes coarse idle bands from the manifest before any chunk is decoded", () => {
|
|
1642
|
+
const harness: Harness = makeHarness({ entries: idleEntries });
|
|
1643
|
+
|
|
1644
|
+
expect(harness.snapshot().idleBands).toEqual([
|
|
1645
|
+
{
|
|
1646
|
+
startMs: 2 * CHUNK_MS,
|
|
1647
|
+
endMs: 6 * CHUNK_MS,
|
|
1648
|
+
kind: "idle",
|
|
1649
|
+
fidelity: "coarse",
|
|
1650
|
+
},
|
|
1651
|
+
]);
|
|
1652
|
+
});
|
|
1653
|
+
|
|
1654
|
+
it("jumps to the end of an idle band, feeding until it is fed, and never touches rrweb's skipInactive", async () => {
|
|
1655
|
+
const harness: Harness = makeHarness({
|
|
1656
|
+
entries: idleEntries,
|
|
1657
|
+
eventsForChunk: (
|
|
1658
|
+
chunkIndex: number,
|
|
1659
|
+
): Array<SessionReplayRecordedEvent> => {
|
|
1660
|
+
/* Idle chunks carry only a heartbeat mutation; active ones a mousemove. */
|
|
1661
|
+
return [
|
|
1662
|
+
...eventsFor(chunkIndex, 1),
|
|
1663
|
+
{
|
|
1664
|
+
type: RRWEB_EVENT_TYPE_INCREMENTAL,
|
|
1665
|
+
timestamp: CLIENT_CLOCK_BASE_MS + chunkIndex * CHUNK_MS + 500,
|
|
1666
|
+
data: { source: chunkIndex >= 2 && chunkIndex <= 5 ? 0 : 1 },
|
|
1667
|
+
},
|
|
1668
|
+
];
|
|
1669
|
+
},
|
|
1670
|
+
});
|
|
1671
|
+
|
|
1672
|
+
harness.engine.dispatch({ type: "SET_SKIP_INACTIVE", enabled: true });
|
|
1673
|
+
await loadAndFlush(harness, 0, 0);
|
|
1674
|
+
harness.engine.dispatch({ type: "PLAY" });
|
|
1675
|
+
|
|
1676
|
+
/* Play into the band. */
|
|
1677
|
+
harness.live().currentTimeMs = 2 * CHUNK_MS + 1000;
|
|
1678
|
+
await harness.tick(16);
|
|
1679
|
+
await flush();
|
|
1680
|
+
|
|
1681
|
+
const skip: ReplayEngineSnapshot = harness.snapshot();
|
|
1682
|
+
|
|
1683
|
+
expect(skip.lastIdleSkip?.kind).toBe("idle");
|
|
1684
|
+
expect(skip.lastIdleSkip?.startMs).toBeLessThanOrEqual(2 * CHUNK_MS + 1000);
|
|
1685
|
+
/*
|
|
1686
|
+
* Landed one second before the band ends, on the live Replayer.
|
|
1687
|
+
*
|
|
1688
|
+
* The band ends where activity actually RESUMES - chunk 6's mousemove
|
|
1689
|
+
* at +500ms, from that chunk's decoded events - not at the chunk
|
|
1690
|
+
* boundary the manifest guesses. The loader had already decoded it in
|
|
1691
|
+
* the first page, so the map is exact here even though playback has
|
|
1692
|
+
* not fed that far.
|
|
1693
|
+
*/
|
|
1694
|
+
expect(harness.replayers.length).toBe(1);
|
|
1695
|
+
expect(skip.lastIdleSkip?.endMs).toBe(6 * CHUNK_MS + 500);
|
|
1696
|
+
expect(skip.lastIdleSkip?.fidelity).toBe("exact");
|
|
1697
|
+
expect(harness.replayers[0]!.playOffsets).toContain(
|
|
1698
|
+
6 * CHUNK_MS + 500 - 1000,
|
|
1699
|
+
);
|
|
1700
|
+
expect(skip.currentTimeMs).toBe(6 * CHUNK_MS + 500 - 1000);
|
|
1701
|
+
expect(skip.fedRange?.toMs).toBeGreaterThanOrEqual(6 * CHUNK_MS);
|
|
1702
|
+
|
|
1703
|
+
for (const config of harness.replayers[0]!.configs) {
|
|
1704
|
+
expect(config["skipInactive"]).not.toBe(true);
|
|
1705
|
+
}
|
|
1706
|
+
expect(harness.replayers[0]!.constructorConfig["skipInactive"]).toBe(false);
|
|
1707
|
+
});
|
|
1708
|
+
|
|
1709
|
+
it("does not skip while the toggle is off", async () => {
|
|
1710
|
+
const harness: Harness = makeHarness({ entries: idleEntries });
|
|
1711
|
+
|
|
1712
|
+
await loadAndFlush(harness, 0, 0);
|
|
1713
|
+
harness.engine.dispatch({ type: "PLAY" });
|
|
1714
|
+
harness.live().currentTimeMs = 2 * CHUNK_MS + 1000;
|
|
1715
|
+
await harness.tick(16);
|
|
1716
|
+
|
|
1717
|
+
expect(harness.snapshot().lastIdleSkip).toBeNull();
|
|
1718
|
+
/* Still inside the band: the 16ms tick moved the clock, nothing jumped. */
|
|
1719
|
+
expect(harness.snapshot().currentTimeMs).toBeGreaterThanOrEqual(
|
|
1720
|
+
2 * CHUNK_MS + 1000,
|
|
1721
|
+
);
|
|
1722
|
+
expect(harness.snapshot().currentTimeMs).toBeLessThan(2 * CHUNK_MS + 2000);
|
|
1723
|
+
});
|
|
1724
|
+
});
|
|
1725
|
+
|
|
1726
|
+
describe("ReplayEngine tabs and live sessions", () => {
|
|
1727
|
+
it("preserves the session-clock playhead across a tab switch when the target tab covers it", async () => {
|
|
1728
|
+
const harness: Harness = makeHarness({
|
|
1729
|
+
entries: [
|
|
1730
|
+
makeEntry(0, { hasFullSnapshot: true }),
|
|
1731
|
+
makeEntry(1),
|
|
1732
|
+
makeEntry(2),
|
|
1733
|
+
],
|
|
1734
|
+
});
|
|
1735
|
+
|
|
1736
|
+
await loadAndFlush(harness, 0, 0);
|
|
1737
|
+
harness.replayers[0]!.currentTimeMs = 20000;
|
|
1738
|
+
await harness.tick(0);
|
|
1739
|
+
expect(harness.snapshot().currentTimeMs).toBe(20000);
|
|
1740
|
+
|
|
1741
|
+
const second: ChunkLoader = new ChunkLoader({
|
|
1742
|
+
sessionId: "sess-1",
|
|
1743
|
+
tabId: "tab-2",
|
|
1744
|
+
entries: [
|
|
1745
|
+
{ ...makeEntry(0, { hasFullSnapshot: true }), tabId: "tab-2" },
|
|
1746
|
+
{ ...makeEntry(1), tabId: "tab-2" },
|
|
1747
|
+
{ ...makeEntry(2), tabId: "tab-2" },
|
|
1748
|
+
],
|
|
1749
|
+
fetcher: (
|
|
1750
|
+
request: SessionReplayChunkFetchRequest,
|
|
1751
|
+
): Promise<ArrayBuffer> => {
|
|
1752
|
+
return Promise.resolve(
|
|
1753
|
+
encodeFrames(
|
|
1754
|
+
request.chunkIndexes.map(
|
|
1755
|
+
(
|
|
1756
|
+
chunkIndex: number,
|
|
1757
|
+
): {
|
|
1758
|
+
chunkIndex: number;
|
|
1759
|
+
events: Array<SessionReplayRecordedEvent>;
|
|
1760
|
+
} => {
|
|
1761
|
+
return {
|
|
1762
|
+
chunkIndex: chunkIndex,
|
|
1763
|
+
events: eventsFor(chunkIndex, 2),
|
|
1764
|
+
};
|
|
1765
|
+
},
|
|
1766
|
+
),
|
|
1767
|
+
),
|
|
1768
|
+
);
|
|
1769
|
+
},
|
|
1770
|
+
});
|
|
1771
|
+
|
|
1772
|
+
harness.engine.dispatch({
|
|
1773
|
+
type: "TAB_SWITCH",
|
|
1774
|
+
tabId: "tab-2",
|
|
1775
|
+
loader: second,
|
|
1776
|
+
});
|
|
1777
|
+
await flush();
|
|
1778
|
+
|
|
1779
|
+
expect(harness.replayers.length).toBe(2);
|
|
1780
|
+
expect(harness.replayers[0]!.isDestroyed).toBe(true);
|
|
1781
|
+
expect(harness.replayers[1]!.pauseOffsets).toEqual([20000]);
|
|
1782
|
+
expect(harness.snapshot().activeTabId).toBe("tab-2");
|
|
1783
|
+
expect(harness.snapshot().currentTimeMs).toBe(20000);
|
|
1784
|
+
});
|
|
1785
|
+
|
|
1786
|
+
it("turns an ended tab back into a stall when new manifest rows arrive", async () => {
|
|
1787
|
+
const harness: Harness = makeHarness({
|
|
1788
|
+
entries: [makeEntry(0, { hasFullSnapshot: true }), makeEntry(1)],
|
|
1789
|
+
});
|
|
1790
|
+
|
|
1791
|
+
await loadAndFlush(harness, 0, 0);
|
|
1792
|
+
harness.engine.dispatch({ type: "PLAY" });
|
|
1793
|
+
harness.replayers[0]!.emit("finish");
|
|
1794
|
+
|
|
1795
|
+
expect(harness.snapshot().phase).toBe("ended");
|
|
1796
|
+
|
|
1797
|
+
harness.engine.dispatch({
|
|
1798
|
+
type: "APPEND_ENTRIES",
|
|
1799
|
+
entries: [makeEntry(2)],
|
|
1800
|
+
});
|
|
1801
|
+
|
|
1802
|
+
expect(harness.snapshot().durationMs).toBe(3 * CHUNK_MS);
|
|
1803
|
+
expect(harness.snapshot().buffer).toBe("stalled");
|
|
1804
|
+
expect(harness.replayers.length).toBe(1);
|
|
1805
|
+
|
|
1806
|
+
harness.engine.dispatch({ type: "PLAY" });
|
|
1807
|
+
await flush();
|
|
1808
|
+
|
|
1809
|
+
/* Continues, no rewind, no rebuild. */
|
|
1810
|
+
expect(harness.replayers.length).toBe(1);
|
|
1811
|
+
expect(dataOf(harness.replayers[0]!.added)).toContainEqual({
|
|
1812
|
+
chunkIndex: 2,
|
|
1813
|
+
sequence: 0,
|
|
1814
|
+
});
|
|
1815
|
+
expect(harness.snapshot().phase).toBe("playing");
|
|
1816
|
+
});
|
|
1817
|
+
});
|
|
1818
|
+
|
|
1819
|
+
describe("ReplayEngine failure handling", () => {
|
|
1820
|
+
it("(e) gives up after the retries, halts retryable, and does not hammer the API", async () => {
|
|
1821
|
+
const harness: Harness = makeHarness({
|
|
1822
|
+
entries: [
|
|
1823
|
+
makeEntry(0, { hasFullSnapshot: true }),
|
|
1824
|
+
makeEntry(1),
|
|
1825
|
+
makeEntry(2),
|
|
1826
|
+
makeEntry(3),
|
|
1827
|
+
],
|
|
1828
|
+
failChunkIndexes: [2],
|
|
1829
|
+
});
|
|
1830
|
+
|
|
1831
|
+
await loadAndFlush(harness, 0, 0);
|
|
1832
|
+
expect(harness.replayers.length).toBe(1);
|
|
1833
|
+
|
|
1834
|
+
/* Chunk 2 is needed: the tick asks for it, three attempts fail. */
|
|
1835
|
+
await harness.tick(16);
|
|
1836
|
+
await flush();
|
|
1837
|
+
|
|
1838
|
+
expect(harness.snapshot().phase).toBe("error");
|
|
1839
|
+
expect(harness.snapshot().error?.retryable).toBe(true);
|
|
1840
|
+
expect(harness.snapshot().error?.message).toContain("0:30");
|
|
1841
|
+
expect(harness.snapshot().error?.message).toContain("3 attempts");
|
|
1842
|
+
|
|
1843
|
+
const requestsAfterHalt: number = harness.requests.length;
|
|
1844
|
+
|
|
1845
|
+
for (let i: number = 0; i < 5; i++) {
|
|
1846
|
+
await harness.tick(250);
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
expect(harness.requests.length).toBe(requestsAfterHalt);
|
|
1850
|
+
});
|
|
1851
|
+
|
|
1852
|
+
it("times out a hung fetch, retries twice, and RETRY rebuilds at the same anchor", async () => {
|
|
1853
|
+
const harness: Harness = makeHarness({
|
|
1854
|
+
entries: [makeEntry(0, { hasFullSnapshot: true }), makeEntry(1)],
|
|
1855
|
+
hangChunkIndexes: [0],
|
|
1856
|
+
fetchTimeoutMs: 5,
|
|
1857
|
+
});
|
|
1858
|
+
|
|
1859
|
+
harness.engine.dispatch({ type: "LOAD", anchorChunkIndex: 0, targetMs: 0 });
|
|
1860
|
+
harness.engine.dispatch({ type: "PLAY" });
|
|
1861
|
+
|
|
1862
|
+
/*
|
|
1863
|
+
* Three attempts have to time out (5ms each) with their two retry
|
|
1864
|
+
* backoffs before the engine halts; wait for the halt itself rather
|
|
1865
|
+
* than for a span of wall clock.
|
|
1866
|
+
*/
|
|
1867
|
+
await waitUntil((): boolean => {
|
|
1868
|
+
return harness.snapshot().phase === "error";
|
|
1869
|
+
});
|
|
1870
|
+
|
|
1871
|
+
expect(harness.snapshot().phase).toBe("error");
|
|
1872
|
+
expect(harness.snapshot().error?.retryable).toBe(true);
|
|
1873
|
+
expect(harness.snapshot().error?.message).toContain("did not respond");
|
|
1874
|
+
expect(harness.requests.length).toBe(3);
|
|
1875
|
+
expect(harness.replayers.length).toBe(0);
|
|
1876
|
+
|
|
1877
|
+
harness.healFetch();
|
|
1878
|
+
harness.engine.dispatch({ type: "RETRY" });
|
|
1879
|
+
await flush();
|
|
1880
|
+
|
|
1881
|
+
expect(harness.replayers.length).toBe(1);
|
|
1882
|
+
expect(harness.snapshot().error).toBeNull();
|
|
1883
|
+
/* The intent survived the failure. */
|
|
1884
|
+
expect(harness.snapshot().phase).toBe("playing");
|
|
1885
|
+
expect(harness.replayers[0]!.playOffsets).toEqual([0]);
|
|
1886
|
+
});
|
|
1887
|
+
|
|
1888
|
+
it("(e) aborts in-flight fetches on DISPOSE and drops a late response", async () => {
|
|
1889
|
+
const harness: Harness = makeHarness({
|
|
1890
|
+
entries: [makeEntry(0, { hasFullSnapshot: true }), makeEntry(1)],
|
|
1891
|
+
deferFetch: true,
|
|
1892
|
+
});
|
|
1893
|
+
|
|
1894
|
+
harness.engine.dispatch({ type: "LOAD", anchorChunkIndex: 0, targetMs: 0 });
|
|
1895
|
+
await flush();
|
|
1896
|
+
|
|
1897
|
+
expect(harness.signals.length).toBeGreaterThan(0);
|
|
1898
|
+
expect(harness.signals[0]!.aborted).toBe(false);
|
|
1899
|
+
|
|
1900
|
+
harness.engine.dispose();
|
|
1901
|
+
|
|
1902
|
+
expect(harness.signals[0]!.aborted).toBe(true);
|
|
1903
|
+
|
|
1904
|
+
harness.resolveFetch();
|
|
1905
|
+
await flush();
|
|
1906
|
+
|
|
1907
|
+
expect(harness.replayers.length).toBe(0);
|
|
1908
|
+
expect(harness.loader.getDecodedChunkIndexes()).toEqual([]);
|
|
1909
|
+
});
|
|
1910
|
+
|
|
1911
|
+
it("halts without retry when the tab has no snapshot at all", async () => {
|
|
1912
|
+
const harness: Harness = makeHarness({
|
|
1913
|
+
entries: [makeEntry(0), makeEntry(1)],
|
|
1914
|
+
});
|
|
1915
|
+
|
|
1916
|
+
harness.engine.dispatch({ type: "SEEK", offsetMs: 0, token: 1 });
|
|
1917
|
+
|
|
1918
|
+
expect(harness.snapshot().phase).toBe("error");
|
|
1919
|
+
expect(harness.snapshot().error?.retryable).toBe(false);
|
|
1920
|
+
expect(harness.snapshot().error?.message).toContain("no full DOM snapshot");
|
|
1921
|
+
});
|
|
1922
|
+
});
|
|
1923
|
+
|
|
1924
|
+
describe("ReplayEngine hold-last-frame rebuilds", () => {
|
|
1925
|
+
const entries: Array<SessionReplayChunkManifestEntry> = [
|
|
1926
|
+
makeEntry(0, { hasFullSnapshot: true }),
|
|
1927
|
+
makeEntry(1),
|
|
1928
|
+
makeEntry(2, { hasFullSnapshot: true }),
|
|
1929
|
+
makeEntry(3),
|
|
1930
|
+
];
|
|
1931
|
+
|
|
1932
|
+
it("keeps the old Replayer visible until the new one has rebuilt its snapshot, then destroys exactly one", async () => {
|
|
1933
|
+
const harness: Harness = makeHarness({
|
|
1934
|
+
entries: entries,
|
|
1935
|
+
autoRebuild: false,
|
|
1936
|
+
});
|
|
1937
|
+
|
|
1938
|
+
await loadAndFlush(harness, 0, 0);
|
|
1939
|
+
harness.engine.dispatch({ type: "SEEK", offsetMs: 2 * CHUNK_MS, token: 1 });
|
|
1940
|
+
await flush();
|
|
1941
|
+
|
|
1942
|
+
expect(harness.replayers.length).toBe(2);
|
|
1943
|
+
expect(harness.replayers[0]!.isDestroyed).toBe(false);
|
|
1944
|
+
expect(harness.replayers[0]!.wrapper.style.pointerEvents).toBe("none");
|
|
1945
|
+
expect(harness.engine.getDiagnostics().isHoldingLastFrame).toBe(true);
|
|
1946
|
+
|
|
1947
|
+
harness.replayers[1]!.emit("fullsnapshot-rebuilded");
|
|
1948
|
+
|
|
1949
|
+
expect(harness.replayers[0]!.isDestroyed).toBe(true);
|
|
1950
|
+
expect(harness.replayers[1]!.isDestroyed).toBe(false);
|
|
1951
|
+
expect(harness.engine.getDiagnostics().isHoldingLastFrame).toBe(false);
|
|
1952
|
+
expect(harness.engine.getDiagnostics().replayersDestroyed).toBe(1);
|
|
1953
|
+
});
|
|
1954
|
+
|
|
1955
|
+
it("destroys the old Replayer eagerly when the anchor payload is over 4MB", async () => {
|
|
1956
|
+
const heavy: Array<SessionReplayChunkManifestEntry> = entries.map(
|
|
1957
|
+
(
|
|
1958
|
+
entry: SessionReplayChunkManifestEntry,
|
|
1959
|
+
): SessionReplayChunkManifestEntry => {
|
|
1960
|
+
return entry.chunkIndex === 2
|
|
1961
|
+
? { ...entry, payloadBytes: 5 * 1024 * 1024 }
|
|
1962
|
+
: entry;
|
|
1963
|
+
},
|
|
1964
|
+
);
|
|
1965
|
+
const harness: Harness = makeHarness({
|
|
1966
|
+
entries: heavy,
|
|
1967
|
+
autoRebuild: false,
|
|
1968
|
+
});
|
|
1969
|
+
|
|
1970
|
+
await loadAndFlush(harness, 0, 0);
|
|
1971
|
+
harness.engine.dispatch({ type: "SEEK", offsetMs: 2 * CHUNK_MS, token: 1 });
|
|
1972
|
+
|
|
1973
|
+
expect(harness.replayers[0]!.isDestroyed).toBe(true);
|
|
1974
|
+
expect(harness.engine.getDiagnostics().isHoldingLastFrame).toBe(false);
|
|
1975
|
+
});
|
|
1976
|
+
|
|
1977
|
+
it("drops the held frame on the scheduler's fallback if no rebuild is ever reported", async () => {
|
|
1978
|
+
const harness: Harness = makeHarness({
|
|
1979
|
+
entries: entries,
|
|
1980
|
+
autoRebuild: false,
|
|
1981
|
+
});
|
|
1982
|
+
|
|
1983
|
+
await loadAndFlush(harness, 0, 0);
|
|
1984
|
+
harness.engine.dispatch({ type: "SEEK", offsetMs: 2 * CHUNK_MS, token: 1 });
|
|
1985
|
+
await flush();
|
|
1986
|
+
|
|
1987
|
+
const fallback: { callback: () => void; delayMs: number } | undefined =
|
|
1988
|
+
harness.scheduled.find((entry: { delayMs: number }): boolean => {
|
|
1989
|
+
return entry.delayMs === 2000;
|
|
1990
|
+
});
|
|
1991
|
+
|
|
1992
|
+
expect(fallback).toBeDefined();
|
|
1993
|
+
fallback!.callback();
|
|
1994
|
+
|
|
1995
|
+
expect(harness.replayers[0]!.isDestroyed).toBe(true);
|
|
1996
|
+
});
|
|
1997
|
+
});
|
|
1998
|
+
|
|
1999
|
+
describe("ReplayEngine gaps", () => {
|
|
2000
|
+
it("reports the hole it crossed and keeps playing after the jump", async () => {
|
|
2001
|
+
const harness: Harness = makeHarness({
|
|
2002
|
+
entries: [
|
|
2003
|
+
makeEntry(0, { hasFullSnapshot: true }),
|
|
2004
|
+
makeEntry(1),
|
|
2005
|
+
makeEntry(4, { hasFullSnapshot: true }),
|
|
2006
|
+
makeEntry(5),
|
|
2007
|
+
],
|
|
2008
|
+
});
|
|
2009
|
+
|
|
2010
|
+
await loadAndFlush(harness, 0, 0);
|
|
2011
|
+
harness.engine.dispatch({ type: "PLAY" });
|
|
2012
|
+
|
|
2013
|
+
await harness.tick(16);
|
|
2014
|
+
expect(harness.snapshot().buffer).toBe("gap-pending");
|
|
2015
|
+
expect(harness.snapshot().phase).toBe("playing");
|
|
2016
|
+
|
|
2017
|
+
/* Footage runs out: rrweb finishes, and the jump happens now. */
|
|
2018
|
+
harness.live().emit("finish");
|
|
2019
|
+
await flush();
|
|
2020
|
+
|
|
2021
|
+
expect(harness.snapshot().lastGap).toEqual({
|
|
2022
|
+
fromIndex: 1,
|
|
2023
|
+
toIndex: 4,
|
|
2024
|
+
missingMs: 2 * CHUNK_MS,
|
|
2025
|
+
});
|
|
2026
|
+
expect(harness.replayers.length).toBe(2);
|
|
2027
|
+
expect(harness.snapshot().phase).toBe("playing");
|
|
2028
|
+
expect(harness.snapshot().currentTimeMs).toBe(4 * CHUNK_MS);
|
|
2029
|
+
expect(harness.snapshot().loadedChunkIndexes).toEqual([4, 5]);
|
|
2030
|
+
});
|
|
2031
|
+
|
|
2032
|
+
it("holds the last frame before a hole while paused, and jumps only once Play resumes", async () => {
|
|
2033
|
+
const harness: Harness = makeHarness({
|
|
2034
|
+
entries: [
|
|
2035
|
+
makeEntry(0, { hasFullSnapshot: true }),
|
|
2036
|
+
makeEntry(1),
|
|
2037
|
+
makeEntry(4, { hasFullSnapshot: true }),
|
|
2038
|
+
makeEntry(5),
|
|
2039
|
+
],
|
|
2040
|
+
});
|
|
2041
|
+
|
|
2042
|
+
await loadAndFlush(harness, 0, 0);
|
|
2043
|
+
harness.engine.dispatch({ type: "PLAY" });
|
|
2044
|
+
await harness.tick(16);
|
|
2045
|
+
expect(harness.snapshot().buffer).toBe("gap-pending");
|
|
2046
|
+
|
|
2047
|
+
/* The viewer pauses on the very last frame before the hole. */
|
|
2048
|
+
harness.engine.dispatch({ type: "PAUSE" });
|
|
2049
|
+
harness.engine.dispatch({ type: "SEEK", offsetMs: 2 * CHUNK_MS, token: 1 });
|
|
2050
|
+
await flush();
|
|
2051
|
+
|
|
2052
|
+
/* Paused ticks must not swap the picture from under them. */
|
|
2053
|
+
await harness.tick(250);
|
|
2054
|
+
await harness.tick(250);
|
|
2055
|
+
|
|
2056
|
+
expect(harness.replayers.length).toBe(1);
|
|
2057
|
+
expect(harness.snapshot().phase).toBe("paused");
|
|
2058
|
+
expect(harness.snapshot().lastGap).toBeNull();
|
|
2059
|
+
expect(harness.snapshot().currentTimeMs).toBe(2 * CHUNK_MS);
|
|
2060
|
+
|
|
2061
|
+
/* Play resumes the cast; rrweb drains and finishes; NOW the jump. */
|
|
2062
|
+
harness.engine.dispatch({ type: "PLAY" });
|
|
2063
|
+
harness.live().emit("finish");
|
|
2064
|
+
await flush();
|
|
2065
|
+
|
|
2066
|
+
expect(harness.replayers.length).toBe(2);
|
|
2067
|
+
expect(harness.snapshot().lastGap?.toIndex).toBe(4);
|
|
2068
|
+
expect(harness.snapshot().phase).toBe("playing");
|
|
2069
|
+
expect(harness.snapshot().currentTimeMs).toBe(4 * CHUNK_MS);
|
|
2070
|
+
});
|
|
2071
|
+
|
|
2072
|
+
it("publishes the fed range as the seekable band, not the decoded cache", async () => {
|
|
2073
|
+
const harness: Harness = makeHarness({
|
|
2074
|
+
entries: [
|
|
2075
|
+
makeEntry(0, { hasFullSnapshot: true }),
|
|
2076
|
+
makeEntry(1),
|
|
2077
|
+
makeEntry(2),
|
|
2078
|
+
],
|
|
2079
|
+
});
|
|
2080
|
+
|
|
2081
|
+
await loadAndFlush(harness, 0, 0);
|
|
2082
|
+
harness.loader.evictOutsideWindow(99, 0);
|
|
2083
|
+
|
|
2084
|
+
expect(harness.loader.getDecodedChunkIndexes()).toEqual([]);
|
|
2085
|
+
/* rrweb still holds 0 and 1; the band must say so. */
|
|
2086
|
+
expect(harness.snapshot().loadedChunkIndexes).toEqual([0, 1]);
|
|
2087
|
+
});
|
|
2088
|
+
});
|
|
2089
|
+
|
|
2090
|
+
describe("ReplayEngine transport economy", () => {
|
|
2091
|
+
it("plays a session longer than the decoded cache without fetching anything twice", async () => {
|
|
2092
|
+
/*
|
|
2093
|
+
* The engine half of the 12x amplification: prefetching two pages past
|
|
2094
|
+
* every fed chunk asked for more footage than the cache could hold, so
|
|
2095
|
+
* each admit evicted a chunk that had been downloaded and not yet fed
|
|
2096
|
+
* and the feed loop fetched it back. Over a 60-chunk session that was
|
|
2097
|
+
* 95 page requests instead of nine, and the refetch on the critical
|
|
2098
|
+
* path is exactly the periodic stall this overhaul set out to remove.
|
|
2099
|
+
*/
|
|
2100
|
+
const entries: Array<SessionReplayChunkManifestEntry> = Array.from(
|
|
2101
|
+
{ length: 60 },
|
|
2102
|
+
(_unused: unknown, index: number): SessionReplayChunkManifestEntry => {
|
|
2103
|
+
return makeEntry(index, { hasFullSnapshot: index === 0 });
|
|
2104
|
+
},
|
|
2105
|
+
);
|
|
2106
|
+
|
|
2107
|
+
const harness: Harness = makeHarness({ entries: entries });
|
|
2108
|
+
|
|
2109
|
+
await loadAndFlush(harness, 0, 0);
|
|
2110
|
+
harness.engine.dispatch({ type: "PLAY" });
|
|
2111
|
+
|
|
2112
|
+
/* 15 minutes of playback, five seconds of wall clock at a time. */
|
|
2113
|
+
for (let step: number = 0; step < 185; step++) {
|
|
2114
|
+
await harness.tick(5000);
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
expect(harness.snapshot().error).toBeNull();
|
|
2118
|
+
expect(harness.snapshot().currentTimeMs).toBeGreaterThan(50 * CHUNK_MS);
|
|
2119
|
+
|
|
2120
|
+
const timesRequested: Map<number, number> = new Map<number, number>();
|
|
2121
|
+
|
|
2122
|
+
for (const request of harness.requests) {
|
|
2123
|
+
for (const chunkIndex of request) {
|
|
2124
|
+
timesRequested.set(
|
|
2125
|
+
chunkIndex,
|
|
2126
|
+
(timesRequested.get(chunkIndex) ?? 0) + 1,
|
|
2127
|
+
);
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
const refetched: Array<number> = [...timesRequested.entries()]
|
|
2132
|
+
.filter((pair: [number, number]): boolean => {
|
|
2133
|
+
return pair[1] > 1;
|
|
2134
|
+
})
|
|
2135
|
+
.map((pair: [number, number]): number => {
|
|
2136
|
+
return pair[0];
|
|
2137
|
+
});
|
|
2138
|
+
|
|
2139
|
+
expect(refetched).toEqual([]);
|
|
2140
|
+
/* Nine page-aligned reads cover 60 chunks; anything more is churn. */
|
|
2141
|
+
expect(harness.requests.length).toBeLessThanOrEqual(10);
|
|
2142
|
+
});
|
|
2143
|
+
|
|
2144
|
+
it("feeds every chunk into rrweb exactly once when a seek retires a feeding loop mid-fetch", async () => {
|
|
2145
|
+
/*
|
|
2146
|
+
* A retired loop still runs its finally when the fetch it was awaiting
|
|
2147
|
+
* lands. Clearing isExtending there cleared it for the loop that had
|
|
2148
|
+
* REPLACED it, so the next tick started a second loop on the same
|
|
2149
|
+
* segment and both fed every chunk from then on: duplicate mutations,
|
|
2150
|
+
* inputs and clicks cast into rrweb, and its event array growing at
|
|
2151
|
+
* twice the rate. Nothing reported it, because lastFedChunkIndex
|
|
2152
|
+
* advanced normally.
|
|
2153
|
+
*/
|
|
2154
|
+
const entries: Array<SessionReplayChunkManifestEntry> = Array.from(
|
|
2155
|
+
{ length: 40 },
|
|
2156
|
+
(_unused: unknown, index: number): SessionReplayChunkManifestEntry => {
|
|
2157
|
+
return makeEntry(index, {
|
|
2158
|
+
hasFullSnapshot: index === 0 || index === 32,
|
|
2159
|
+
});
|
|
2160
|
+
},
|
|
2161
|
+
);
|
|
2162
|
+
|
|
2163
|
+
const harness: Harness = makeHarness({
|
|
2164
|
+
entries: entries,
|
|
2165
|
+
deferFetch: true,
|
|
2166
|
+
});
|
|
2167
|
+
|
|
2168
|
+
harness.engine.dispatch({ type: "LOAD", anchorChunkIndex: 0, targetMs: 0 });
|
|
2169
|
+
await flush();
|
|
2170
|
+
|
|
2171
|
+
/* The priority pair lands; the rest of its page goes on the wire. */
|
|
2172
|
+
harness.releaseFetch(0);
|
|
2173
|
+
await flush();
|
|
2174
|
+
|
|
2175
|
+
harness.engine.dispatch({ type: "PLAY" });
|
|
2176
|
+
await harness.tick(16);
|
|
2177
|
+
|
|
2178
|
+
/* Loop A is now awaiting [2..7]. */
|
|
2179
|
+
expect(harness.loader.isChunkInFlight(2)).toBe(true);
|
|
2180
|
+
|
|
2181
|
+
harness.engine.dispatch({
|
|
2182
|
+
type: "SEEK",
|
|
2183
|
+
offsetMs: 32 * CHUNK_MS + 1000,
|
|
2184
|
+
token: 1,
|
|
2185
|
+
});
|
|
2186
|
+
await flush();
|
|
2187
|
+
|
|
2188
|
+
/* The rebuild's own priority pair, which loop B will feed from. */
|
|
2189
|
+
harness.releaseFetch(32);
|
|
2190
|
+
await flush();
|
|
2191
|
+
|
|
2192
|
+
expect(harness.replayers.length).toBe(2);
|
|
2193
|
+
|
|
2194
|
+
/* Loop B starts and blocks on the next page. */
|
|
2195
|
+
await harness.tick(16);
|
|
2196
|
+
expect(harness.loader.isChunkInFlight(34)).toBe(true);
|
|
2197
|
+
|
|
2198
|
+
/*
|
|
2199
|
+
* Only NOW does the retired loop's await resolve. It is waiting on the
|
|
2200
|
+
* page it asked for and on the page the first prefetch put on the wire.
|
|
2201
|
+
*/
|
|
2202
|
+
harness.releaseFetch(2);
|
|
2203
|
+
harness.releaseFetch(8);
|
|
2204
|
+
await flush();
|
|
2205
|
+
|
|
2206
|
+
/* The tick that used to start a second loop on the live segment. */
|
|
2207
|
+
await harness.tick(16);
|
|
2208
|
+
harness.releaseFetch(34);
|
|
2209
|
+
await flush();
|
|
2210
|
+
await harness.tick(16);
|
|
2211
|
+
await flush();
|
|
2212
|
+
|
|
2213
|
+
const live: FakeReplayer = harness.live();
|
|
2214
|
+
const seen: Map<string, number> = new Map<string, number>();
|
|
2215
|
+
|
|
2216
|
+
for (const event of live.added) {
|
|
2217
|
+
const key: string = JSON.stringify(event.data);
|
|
2218
|
+
seen.set(key, (seen.get(key) ?? 0) + 1);
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
const duplicated: Array<string> = [...seen.entries()]
|
|
2222
|
+
.filter((pair: [string, number]): boolean => {
|
|
2223
|
+
return pair[1] > 1;
|
|
2224
|
+
})
|
|
2225
|
+
.map((pair: [string, number]): string => {
|
|
2226
|
+
return pair[0];
|
|
2227
|
+
});
|
|
2228
|
+
|
|
2229
|
+
expect(duplicated).toEqual([]);
|
|
2230
|
+
expect(live.added.length).toBeGreaterThan(0);
|
|
2231
|
+
});
|
|
2232
|
+
});
|
|
2233
|
+
|
|
2234
|
+
describe("ReplayEngine seeking past a hole", () => {
|
|
2235
|
+
it("recovers from a seek made while a gap jump is queued instead of buffering for ever", async () => {
|
|
2236
|
+
/*
|
|
2237
|
+
* Chunk 3 is in the manifest but comes back empty, so a gap jump is
|
|
2238
|
+
* queued. A seek past it looked reachable - isContiguousRange answers
|
|
2239
|
+
* from the manifest, which still lists chunk 3 - so the engine went to
|
|
2240
|
+
* "building" and asked the feeding loop to reach it, and the feeding
|
|
2241
|
+
* loop refuses to run while a gap is pending. The result was an
|
|
2242
|
+
* endless buffering pill with error null, a Retry that does nothing
|
|
2243
|
+
* (RETRY only acts on "halted") and a Play that moves nothing.
|
|
2244
|
+
*/
|
|
2245
|
+
const entries: Array<SessionReplayChunkManifestEntry> = Array.from(
|
|
2246
|
+
{ length: 20 },
|
|
2247
|
+
(_unused: unknown, index: number): SessionReplayChunkManifestEntry => {
|
|
2248
|
+
return makeEntry(index, {
|
|
2249
|
+
hasFullSnapshot: index === 0 || index === 5,
|
|
2250
|
+
});
|
|
2251
|
+
},
|
|
2252
|
+
);
|
|
2253
|
+
|
|
2254
|
+
const harness: Harness = makeHarness({
|
|
2255
|
+
entries: entries,
|
|
2256
|
+
omitChunkIndexes: [3],
|
|
2257
|
+
});
|
|
2258
|
+
|
|
2259
|
+
await loadAndFlush(harness, 0, 0);
|
|
2260
|
+
harness.engine.dispatch({ type: "PLAY" });
|
|
2261
|
+
|
|
2262
|
+
/* Feed forward until chunk 3 comes back empty. */
|
|
2263
|
+
for (let step: number = 0; step < 4; step++) {
|
|
2264
|
+
await harness.tick(5000);
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2267
|
+
expect(harness.snapshot().buffer).toBe("gap-pending");
|
|
2268
|
+
|
|
2269
|
+
harness.engine.dispatch({
|
|
2270
|
+
type: "SEEK",
|
|
2271
|
+
offsetMs: 4 * CHUNK_MS + 500,
|
|
2272
|
+
token: 1,
|
|
2273
|
+
});
|
|
2274
|
+
await flush();
|
|
2275
|
+
|
|
2276
|
+
const afterSeek: ReplayEngineSnapshot = harness.snapshot();
|
|
2277
|
+
|
|
2278
|
+
expect(afterSeek.buffer).not.toBe("building");
|
|
2279
|
+
expect(afterSeek.pendingSeekMs).toBeNull();
|
|
2280
|
+
expect(afterSeek.error).toBeNull();
|
|
2281
|
+
expect(afterSeek.phase).not.toBe("buffering");
|
|
2282
|
+
|
|
2283
|
+
/*
|
|
2284
|
+
* And it keeps its promise: the footage before the hole plays out and
|
|
2285
|
+
* the jump lands on the next snapshot, rather than sitting still.
|
|
2286
|
+
*/
|
|
2287
|
+
for (let step: number = 0; step < 4; step++) {
|
|
2288
|
+
await harness.tick(5000);
|
|
2289
|
+
await flush();
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
expect(harness.snapshot().lastGap?.toIndex).toBe(5);
|
|
2293
|
+
expect(harness.snapshot().currentTimeMs).toBeGreaterThanOrEqual(
|
|
2294
|
+
5 * CHUNK_MS,
|
|
2295
|
+
);
|
|
2296
|
+
expect(harness.snapshot().phase).toBe("playing");
|
|
2297
|
+
});
|
|
2298
|
+
});
|
|
2299
|
+
|
|
2300
|
+
describe("ReplayEngine in a hidden tab", () => {
|
|
2301
|
+
it("does not run the watchdog while the document is hidden", async () => {
|
|
2302
|
+
/*
|
|
2303
|
+
* rrweb's clock only advances inside a requestAnimationFrame callback
|
|
2304
|
+
* and browsers suspend those in a background tab, so getCurrentTime()
|
|
2305
|
+
* CANNOT move while the tab is hidden. Reading that as a stall fired
|
|
2306
|
+
* the watchdog every 1.5s for as long as the viewer was away, each
|
|
2307
|
+
* time re-applying every event since the last snapshot synchronously,
|
|
2308
|
+
* and left watchdogFireCount - which the diagnostic reports as a
|
|
2309
|
+
* number that should be zero - in the hundreds.
|
|
2310
|
+
*/
|
|
2311
|
+
let hidden: boolean = false;
|
|
2312
|
+
|
|
2313
|
+
const harness: Harness = makeHarness({
|
|
2314
|
+
entries: [
|
|
2315
|
+
makeEntry(0, { hasFullSnapshot: true }),
|
|
2316
|
+
makeEntry(1),
|
|
2317
|
+
makeEntry(2),
|
|
2318
|
+
makeEntry(3),
|
|
2319
|
+
makeEntry(4),
|
|
2320
|
+
],
|
|
2321
|
+
isDocumentHidden: (): boolean => {
|
|
2322
|
+
return hidden;
|
|
2323
|
+
},
|
|
2324
|
+
});
|
|
2325
|
+
|
|
2326
|
+
await loadAndFlush(harness, 0, 0);
|
|
2327
|
+
harness.engine.dispatch({ type: "PLAY" });
|
|
2328
|
+
await harness.tick(16);
|
|
2329
|
+
|
|
2330
|
+
const live: FakeReplayer = harness.live();
|
|
2331
|
+
const playsBeforeHiding: number = live.playOffsets.length;
|
|
2332
|
+
|
|
2333
|
+
hidden = true;
|
|
2334
|
+
|
|
2335
|
+
/* Six seconds away: four watchdog windows with a frozen clock. */
|
|
2336
|
+
for (let step: number = 0; step < 30; step++) {
|
|
2337
|
+
await harness.tick(200);
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2340
|
+
expect(harness.engine.getDiagnostics().watchdogFireCount).toBe(0);
|
|
2341
|
+
expect(live.playOffsets.length).toBe(playsBeforeHiding);
|
|
2342
|
+
expect(harness.snapshot().phase).toBe("playing");
|
|
2343
|
+
|
|
2344
|
+
/* Coming back does not fire it either: the window starts again. */
|
|
2345
|
+
hidden = false;
|
|
2346
|
+
await harness.tick(200);
|
|
2347
|
+
|
|
2348
|
+
expect(harness.engine.getDiagnostics().watchdogFireCount).toBe(0);
|
|
2349
|
+
});
|
|
2350
|
+
});
|
|
2351
|
+
|
|
2352
|
+
describe("ReplayEngine idle skipping over decoded footage", () => {
|
|
2353
|
+
it("stops at a click in a chunk the manifest calls idle", async () => {
|
|
2354
|
+
/*
|
|
2355
|
+
* A chunk carrying one click, one mutation and one mousemove batch is
|
|
2356
|
+
* below the "active" event count, so the manifest guess folded it into
|
|
2357
|
+
* the surrounding silence and the skip jumped a whole minute past the
|
|
2358
|
+
* click - the exact moment the viewer turned the toggle on to reach.
|
|
2359
|
+
* The loader had already decoded that chunk in its first page; the
|
|
2360
|
+
* engine only admitted evidence for chunks playback had FED, so it
|
|
2361
|
+
* never looked.
|
|
2362
|
+
*/
|
|
2363
|
+
const entries: Array<SessionReplayChunkManifestEntry> = Array.from(
|
|
2364
|
+
{ length: 12 },
|
|
2365
|
+
(_unused: unknown, index: number): SessionReplayChunkManifestEntry => {
|
|
2366
|
+
return makeEntry(index, {
|
|
2367
|
+
hasFullSnapshot: index === 0,
|
|
2368
|
+
/* Chunk 6 is the one with the click, and still looks idle. */
|
|
2369
|
+
eventCount: index >= 2 && index <= 9 ? (index === 6 ? 3 : 1) : 10,
|
|
2370
|
+
});
|
|
2371
|
+
},
|
|
2372
|
+
);
|
|
2373
|
+
|
|
2374
|
+
const harness: Harness = makeHarness({
|
|
2375
|
+
entries: entries,
|
|
2376
|
+
eventsForChunk: (
|
|
2377
|
+
chunkIndex: number,
|
|
2378
|
+
): Array<SessionReplayRecordedEvent> => {
|
|
2379
|
+
const events: Array<SessionReplayRecordedEvent> = eventsFor(
|
|
2380
|
+
chunkIndex,
|
|
2381
|
+
1,
|
|
2382
|
+
);
|
|
2383
|
+
|
|
2384
|
+
if (chunkIndex === 6) {
|
|
2385
|
+
events.push({
|
|
2386
|
+
type: RRWEB_EVENT_TYPE_INCREMENTAL,
|
|
2387
|
+
timestamp: CLIENT_CLOCK_BASE_MS + 6 * CHUNK_MS + 500,
|
|
2388
|
+
data: { source: 2, type: 2, x: 40, y: 60 },
|
|
2389
|
+
});
|
|
2390
|
+
}
|
|
2391
|
+
|
|
2392
|
+
return events;
|
|
2393
|
+
},
|
|
2394
|
+
});
|
|
2395
|
+
|
|
2396
|
+
harness.engine.dispatch({ type: "SET_SKIP_INACTIVE", enabled: true });
|
|
2397
|
+
await loadAndFlush(harness, 0, 0);
|
|
2398
|
+
harness.engine.dispatch({ type: "PLAY" });
|
|
2399
|
+
|
|
2400
|
+
harness.live().currentTimeMs = 2 * CHUNK_MS + 1000;
|
|
2401
|
+
await harness.tick(16);
|
|
2402
|
+
await flush();
|
|
2403
|
+
|
|
2404
|
+
const skip: ReplayEngineSnapshot = harness.snapshot();
|
|
2405
|
+
|
|
2406
|
+
/* Landed one second before the click, never past it. */
|
|
2407
|
+
expect(skip.currentTimeMs).toBe(6 * CHUNK_MS + 500 - 1000);
|
|
2408
|
+
expect(skip.currentTimeMs).toBeLessThan(6 * CHUNK_MS + 500);
|
|
2409
|
+
expect(skip.lastIdleSkip?.endMs).toBe(6 * CHUNK_MS + 500);
|
|
2410
|
+
expect(skip.error).toBeNull();
|
|
2411
|
+
});
|
|
2412
|
+
});
|
|
2413
|
+
|
|
2414
|
+
describe("ReplayEngine long playthroughs", () => {
|
|
2415
|
+
/* 60-second chunks, each its own snapshot: the recorder's checkout cadence. */
|
|
2416
|
+
const WIDE_CHUNK_MS: number = 60 * 1000;
|
|
2417
|
+
|
|
2418
|
+
function wideEntry(chunkIndex: number): SessionReplayChunkManifestEntry {
|
|
2419
|
+
return {
|
|
2420
|
+
...makeEntry(chunkIndex, { hasFullSnapshot: true }),
|
|
2421
|
+
chunkStartOffsetMs: chunkIndex * WIDE_CHUNK_MS,
|
|
2422
|
+
chunkEndOffsetMs: (chunkIndex + 1) * WIDE_CHUNK_MS,
|
|
2423
|
+
};
|
|
2424
|
+
}
|
|
2425
|
+
|
|
2426
|
+
it("re-anchors on a snapshot once one Replayer holds too much footage", async () => {
|
|
2427
|
+
/*
|
|
2428
|
+
* rrweb never drops an event it has been given, so a viewer who lets a
|
|
2429
|
+
* long session play straight through held every parsed event of it and
|
|
2430
|
+
* every stall resume re-applied all of them synchronously. Past the
|
|
2431
|
+
* bound, the next snapshot the PLAYHEAD reaches is a free place to
|
|
2432
|
+
* start again: nothing is skipped, and the old Replayer goes with its
|
|
2433
|
+
* events.
|
|
2434
|
+
*/
|
|
2435
|
+
const harness: Harness = makeHarness({
|
|
2436
|
+
entries: Array.from(
|
|
2437
|
+
{ length: 14 },
|
|
2438
|
+
(_unused: unknown, index: number): SessionReplayChunkManifestEntry => {
|
|
2439
|
+
return wideEntry(index);
|
|
2440
|
+
},
|
|
2441
|
+
),
|
|
2442
|
+
});
|
|
2443
|
+
|
|
2444
|
+
await loadAndFlush(harness, 0, 0);
|
|
2445
|
+
harness.engine.dispatch({ type: "PLAY" });
|
|
2446
|
+
|
|
2447
|
+
let beforeMs: number = 0;
|
|
2448
|
+
|
|
2449
|
+
for (
|
|
2450
|
+
let step: number = 0;
|
|
2451
|
+
step < 80 && harness.engine.getDiagnostics().replayersCreated < 2;
|
|
2452
|
+
step++
|
|
2453
|
+
) {
|
|
2454
|
+
beforeMs = harness.snapshot().currentTimeMs;
|
|
2455
|
+
await harness.tick(10000);
|
|
2456
|
+
}
|
|
2457
|
+
|
|
2458
|
+
expect(harness.engine.getDiagnostics().replayersCreated).toBe(2);
|
|
2459
|
+
|
|
2460
|
+
const after: ReplayEngineSnapshot = harness.snapshot();
|
|
2461
|
+
|
|
2462
|
+
/* Nothing was skipped, and nothing was reported as missing. */
|
|
2463
|
+
expect(after.currentTimeMs).toBeGreaterThanOrEqual(beforeMs);
|
|
2464
|
+
expect(after.lastGap).toBeNull();
|
|
2465
|
+
expect(after.error).toBeNull();
|
|
2466
|
+
expect(after.notice ?? null).toBeNull();
|
|
2467
|
+
|
|
2468
|
+
/* The new segment starts at a snapshot at or before the playhead. */
|
|
2469
|
+
expect(after.fedRange?.fromMs).toBeGreaterThan(0);
|
|
2470
|
+
expect(after.fedRange?.fromMs).toBeLessThanOrEqual(after.currentTimeMs);
|
|
2471
|
+
expect(after.currentTimeMs - (after.fedRange?.fromMs ?? 0)).toBeLessThan(
|
|
2472
|
+
2 * WIDE_CHUNK_MS,
|
|
2473
|
+
);
|
|
2474
|
+
|
|
2475
|
+
/* And the old one, with everything it was holding, is gone. */
|
|
2476
|
+
expect(harness.replayers[0]!.isDestroyed).toBe(true);
|
|
2477
|
+
|
|
2478
|
+
await harness.tick(10000);
|
|
2479
|
+
await flush();
|
|
2480
|
+
|
|
2481
|
+
expect(harness.snapshot().phase).toBe("playing");
|
|
2482
|
+
});
|
|
2483
|
+
|
|
2484
|
+
it("leaves a short session on one Replayer", async () => {
|
|
2485
|
+
const harness: Harness = makeHarness({
|
|
2486
|
+
entries: Array.from(
|
|
2487
|
+
{ length: 4 },
|
|
2488
|
+
(_unused: unknown, index: number): SessionReplayChunkManifestEntry => {
|
|
2489
|
+
return wideEntry(index);
|
|
2490
|
+
},
|
|
2491
|
+
),
|
|
2492
|
+
});
|
|
2493
|
+
|
|
2494
|
+
await loadAndFlush(harness, 0, 0);
|
|
2495
|
+
harness.engine.dispatch({ type: "PLAY" });
|
|
2496
|
+
|
|
2497
|
+
for (let step: number = 0; step < 20; step++) {
|
|
2498
|
+
await harness.tick(10000);
|
|
2499
|
+
}
|
|
2500
|
+
|
|
2501
|
+
expect(harness.engine.getDiagnostics().replayersCreated).toBe(1);
|
|
2502
|
+
});
|
|
2503
|
+
});
|