@oneuptime/common 13.0.3 → 13.0.5
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/DatabaseModels/Index.ts +4 -0
- package/Models/DatabaseModels/RumApplication.ts +1 -1
- package/Models/DatabaseModels/SecurityEventConnection.ts +457 -0
- package/Models/DatabaseModels/SecurityEventConnectionRun.ts +263 -0
- package/Models/DatabaseModels/TelemetryIngestionKey.ts +2 -2
- package/Server/API/AIInvestigationAPI.ts +484 -24
- package/Server/API/GoogleSecOpsConnectionAPI.ts +249 -8
- package/Server/API/SecurityEventConnectionAPI.ts +250 -0
- package/Server/API/TelemetryAPI.ts +402 -265
- package/Server/Infrastructure/Postgres/SchemaMigrations/1792700000000-AddSecurityEventConnections.ts +99 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Server/Middleware/TelemetryIngest.ts +101 -6
- package/Server/Services/ExceptionAggregationService.ts +7 -16
- package/Server/Services/GoogleSecOpsConnectionService.ts +15 -0
- package/Server/Services/Index.ts +4 -0
- package/Server/Services/InventoryItemService.ts +28 -0
- package/Server/Services/LogAggregationService.ts +17 -23
- package/Server/Services/MetricAggregationService.ts +4 -14
- package/Server/Services/SecurityEventConnectionRunService.ts +10 -0
- package/Server/Services/SecurityEventConnectionService.ts +538 -0
- package/Server/Services/TraceAggregationService.ts +77 -28
- package/Server/Types/AnalyticsDatabase/ModelPermission.ts +9 -0
- package/Server/Utils/AI/Chat/ObservabilityAssistant.ts +13 -1
- package/Server/Utils/AI/SRE/AIInvestigationEngine.ts +15 -1
- package/Server/Utils/AI/SRE/InvestigationEvidence.ts +658 -0
- package/Server/Utils/AI/SRE/InvestigationReferences.ts +324 -0
- package/Server/Utils/AI/Toolbox/Index.ts +38 -0
- package/Server/Utils/AI/Toolbox/TimelineTools.ts +9 -2
- package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +80 -0
- package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +5 -3
- package/Server/Utils/SecurityEvent/Connectors/AwsSecurityHub/AwsSecurityHubClient.ts +1108 -0
- package/Server/Utils/SecurityEvent/Connectors/AwsSecurityHub/AwsSecurityHubConnector.ts +732 -0
- package/Server/Utils/SecurityEvent/Connectors/ConnectorPlatformHealth.ts +495 -0
- package/Server/Utils/SecurityEvent/Connectors/CrowdStrikeFalcon/CrowdStrikeFalconClient.ts +689 -0
- package/Server/Utils/SecurityEvent/Connectors/CrowdStrikeFalcon/CrowdStrikeFalconConnector.ts +767 -0
- package/Server/Utils/SecurityEvent/Connectors/ElasticSecurity/ElasticSecurityClient.ts +571 -0
- package/Server/Utils/SecurityEvent/Connectors/ElasticSecurity/ElasticSecurityConnector.ts +827 -0
- package/Server/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdr/MicrosoftDefenderXdrClient.ts +610 -0
- package/Server/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdr/MicrosoftDefenderXdrConnector.ts +549 -0
- package/Server/Utils/SecurityEvent/Connectors/MicrosoftSentinel/MicrosoftSentinelClient.ts +830 -0
- package/Server/Utils/SecurityEvent/Connectors/MicrosoftSentinel/MicrosoftSentinelConnector.ts +684 -0
- package/Server/Utils/SecurityEvent/Connectors/Okta/OktaClient.ts +487 -0
- package/Server/Utils/SecurityEvent/Connectors/Okta/OktaConnector.ts +778 -0
- package/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionPoller.ts +1002 -0
- package/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionRunExecutor.ts +556 -0
- package/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionTester.ts +275 -0
- package/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectorRegistry.ts +53 -0
- package/Server/Utils/SecurityEvent/Connectors/Splunk/SplunkClient.ts +709 -0
- package/Server/Utils/SecurityEvent/Connectors/Splunk/SplunkConnector.ts +596 -0
- package/Server/Utils/SecurityEvent/Connectors/Types.ts +191 -0
- package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.ts +353 -65
- package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsConnectionTester.ts +777 -0
- package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.ts +747 -148
- package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsRunExecutor.ts +48 -2
- package/Server/Utils/SecurityEvent/SecurityEventDedupe.ts +81 -0
- package/Server/Utils/SessionReplay/SessionReplayReadService.ts +741 -40
- package/Server/Utils/StartServer.ts +10 -0
- package/Server/Utils/Telemetry/EntityRegistry.ts +8 -0
- package/Server/Utils/Telemetry/KubernetesResourceAttributes.ts +108 -0
- package/Server/Utils/Telemetry/ResourceEntityFilter.ts +174 -93
- package/Server/Utils/Telemetry/ResourceFacetPlanner.ts +251 -0
- package/Server/Utils/Telemetry/ResourceFacetResolver.ts +248 -413
- package/Server/Utils/Telemetry/ServiceDependencyDiscovery.ts +668 -0
- package/Server/Utils/Telemetry/TraceContextPropagation.ts +348 -0
- package/Server/Utils/Telemetry.ts +18 -0
- package/Tests/App/Dashboard/AIInvestigationHeaderStatus.test.tsx +413 -0
- package/Tests/App/Dashboard/AIRootCauseFeedItem.test.tsx +285 -0
- package/Tests/App/Dashboard/AffectedResourcesDisplay.test.tsx +242 -0
- package/Tests/App/Dashboard/AlertEpisodeViewFields.test.tsx +601 -15
- package/Tests/App/Dashboard/ChangeAlertStateAIHeader.test.tsx +835 -0
- package/Tests/App/Dashboard/CloudResourceTelemetryScope.test.ts +41 -0
- package/Tests/App/Dashboard/ConnectionTestModal.test.tsx +604 -0
- package/Tests/App/Dashboard/ConnectorTestReportView.test.tsx +738 -0
- package/Tests/App/Dashboard/DashboardLazyPages.test.tsx +153 -6
- package/Tests/App/Dashboard/EntityDetailPanel.test.tsx +165 -0
- package/Tests/App/Dashboard/EpisodeChangeState.test.tsx +1069 -0
- package/Tests/App/Dashboard/EpisodeMembersCard.test.tsx +772 -0
- package/Tests/App/Dashboard/EventOverviewPages.test.tsx +2354 -0
- package/Tests/App/Dashboard/EventOverviewSkeleton.test.tsx +210 -0
- package/Tests/App/Dashboard/EventStatBar.test.tsx +392 -0
- package/Tests/App/Dashboard/EventStatusPanel.test.tsx +247 -0
- package/Tests/App/Dashboard/ExceptionAIAssistance.test.tsx +606 -0
- package/Tests/App/Dashboard/ExceptionBreadcrumbTimeline.test.tsx +273 -0
- package/Tests/App/Dashboard/ExceptionDetailSideMenu.test.tsx +176 -0
- package/Tests/App/Dashboard/ExceptionOverviewCards.test.tsx +366 -0
- package/Tests/App/Dashboard/ExceptionSignalPages.test.tsx +371 -0
- package/Tests/App/Dashboard/ExceptionStackFrameViewer.test.tsx +440 -0
- package/Tests/App/Dashboard/ExceptionSummary.test.tsx +381 -0
- package/Tests/App/Dashboard/ExceptionTriage.test.tsx +306 -0
- package/Tests/App/Dashboard/ExceptionsResourceFacets.test.tsx +604 -0
- package/Tests/App/Dashboard/ExceptionsViewerEntityChips.test.tsx +706 -0
- package/Tests/App/Dashboard/FeedOrdering.test.ts +32 -0
- package/Tests/App/Dashboard/GoogleSecOpsConnectionForm.test.tsx +838 -0
- package/Tests/App/Dashboard/GoogleSecOpsConnectionsErrors.test.tsx +18 -0
- package/Tests/App/Dashboard/GoogleSecOpsDiagnostics.test.tsx +297 -20
- package/Tests/App/Dashboard/IncidentEpisodeViewFields.test.tsx +618 -13
- package/Tests/App/Dashboard/InfrastructureExplorer.test.tsx +254 -207
- package/Tests/App/Dashboard/InfrastructureGraph.test.tsx +303 -135
- package/Tests/App/Dashboard/InvestigationEvidenceList.test.tsx +1251 -0
- package/Tests/App/Dashboard/InvestigationFeedRefresh.test.tsx +303 -0
- package/Tests/App/Dashboard/InvestigationPanel.test.tsx +876 -8
- package/Tests/App/Dashboard/InvestigationPanelStatus.test.tsx +35 -0
- package/Tests/App/Dashboard/InvestigationReferenceLink.test.tsx +177 -0
- package/Tests/App/Dashboard/InvestigationReportView.test.tsx +1295 -0
- package/Tests/App/Dashboard/KubernetesImageReferenceView.test.tsx +114 -0
- package/Tests/App/Dashboard/KubernetesLogsTabChips.test.tsx +285 -0
- package/Tests/App/Dashboard/LogsEntityChipNames.test.tsx +316 -0
- package/Tests/App/Dashboard/LogsResourceFacetChips.test.tsx +361 -0
- package/Tests/App/Dashboard/ProfileTableEntityNames.test.tsx +432 -0
- package/Tests/App/Dashboard/ProfileViewEntityName.test.tsx +250 -0
- package/Tests/App/Dashboard/ResourceArchiveSettings.test.tsx +10 -0
- package/Tests/App/Dashboard/ResourceLogsTabsBareLayout.test.tsx +700 -0
- package/Tests/App/Dashboard/ResourcePageEntityNameProps.test.tsx +803 -0
- package/Tests/App/Dashboard/RumSessionReplaySideMenu.test.tsx +11 -1
- package/Tests/App/Dashboard/ScheduledMaintenanceChangeState.test.tsx +1026 -0
- package/Tests/App/Dashboard/ScheduledMaintenanceFeedRefresh.test.tsx +436 -0
- package/Tests/App/Dashboard/ScheduledMaintenanceOverviewPage.test.tsx +1195 -0
- package/Tests/App/Dashboard/SecurityEventConnectionFormModal.test.tsx +1022 -0
- package/Tests/App/Dashboard/SecurityEventConnectionsTable.test.tsx +905 -0
- package/Tests/App/Dashboard/SecurityEventsSideMenu.test.tsx +247 -0
- package/Tests/App/Dashboard/ServiceMapGraph.test.tsx +375 -274
- package/Tests/App/Dashboard/SessionReplayAuditTable.test.tsx +410 -0
- package/Tests/App/Dashboard/TelemetryResourceRetentionSettings.test.tsx +774 -0
- package/Tests/App/Dashboard/TopErrorsPanelResourceNames.test.tsx +316 -0
- package/Tests/App/Dashboard/TopologyDataLoading.test.tsx +30 -0
- package/Tests/App/Dashboard/TopologyPageNavigation.test.tsx +88 -24
- package/Tests/App/Dashboard/TraceExplorer.test.tsx +1590 -0
- package/Tests/App/Dashboard/TraceWaterfall.test.tsx +488 -0
- package/Tests/App/Dashboard/TracesAnalyticsSeriesLabels.test.tsx +367 -0
- package/Tests/App/Dashboard/TracesEntityNames.test.tsx +585 -0
- package/Tests/App/Dashboard/TracesResourceFacets.test.tsx +613 -0
- package/Tests/App/Dashboard/UseServiceNames.test.tsx +359 -0
- package/Tests/Models/DatabaseModels/DomainRoleTierCoverage.test.ts +7 -0
- package/Tests/Models/InventoryItemNaming.test.ts +132 -16
- package/Tests/Models/SecurityDomainAccessControl.test.ts +61 -17
- package/Tests/Server/API/AIInvestigationAPI.test.ts +611 -2
- package/Tests/Server/API/AIInvestigationEvidenceAPI.test.ts +1098 -0
- package/Tests/Server/API/GoogleSecOpsConnectionAPI.test.ts +409 -0
- package/Tests/Server/API/SecurityEventConnectionAPI.test.ts +856 -0
- package/Tests/Server/API/SessionReplayAPI.test.ts +528 -1
- package/Tests/Server/API/TelemetryFacetsAPI.test.ts +810 -0
- package/Tests/Server/Infrastructure/E2EClickHouseFixtureAccess.test.ts +240 -0
- package/Tests/Server/Middleware/TelemetryIngestBrowserKey.test.ts +229 -0
- package/Tests/Server/Services/InventoryItemDisplayName.test.ts +66 -0
- package/Tests/Server/Services/OpenTelemetryResourceRetentionMemo.test.ts +28 -1
- package/Tests/Server/Services/SecurityEventConnectionService.test.ts +1233 -0
- package/Tests/Server/Services/TelemetryIngestionKeyValidation.test.ts +53 -0
- package/Tests/Server/Services/TelemetryResourceFacetCounting.test.ts +218 -0
- package/Tests/Server/Services/TelemetryResourceFacetFilters.test.ts +155 -1
- package/Tests/Server/Services/TraceAggregationExceptionScope.test.ts +142 -0
- package/Tests/Server/Services/TraceAnalyticsResourceDisplayNames.test.ts +85 -24
- package/Tests/Server/Types/Database/Permissions/SecurityRoleAccess.test.ts +33 -0
- package/Tests/Server/Utils/AI/InvestigationEvidence.test.ts +1395 -0
- package/Tests/Server/Utils/AI/InvestigationReferences.test.ts +616 -0
- package/Tests/Server/Utils/AI/TimelineTools.test.ts +119 -0
- package/Tests/Server/Utils/AI/Toolbox/AIToolbox.test.ts +151 -0
- package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +65 -0
- package/Tests/Server/Utils/AnalyticsDatabase/StatementGeneratorExceptionScope.test.ts +224 -0
- package/Tests/Server/Utils/LoggerCore.test.ts +597 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorPerSeriesFanout.test.ts +55 -2
- package/Tests/Server/Utils/Monitor/MonitorTemplateUtil.test.ts +1109 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/AwsSecurityHub/AwsSecurityHubClient.test.ts +1240 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/AwsSecurityHub/AwsSecurityHubConnector.test.ts +1853 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/ConnectorPlatformHealth.test.ts +1129 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/CrowdStrikeFalcon/CrowdStrikeFalconClient.test.ts +1066 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/CrowdStrikeFalcon/CrowdStrikeFalconConnector.test.ts +1568 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/ElasticSecurity/ElasticSecurityClient.test.ts +930 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/ElasticSecurity/ElasticSecurityConnector.test.ts +1409 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdr/MicrosoftDefenderXdrClient.test.ts +763 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdr/MicrosoftDefenderXdrConnector.test.ts +1296 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/MicrosoftSentinel/MicrosoftSentinelClient.test.ts +1102 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/MicrosoftSentinel/MicrosoftSentinelConnector.test.ts +1174 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/Okta/OktaClient.test.ts +1032 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/Okta/OktaConnector.test.ts +1558 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionPoller.test.ts +3041 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionRunExecutor.test.ts +1168 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionTester.test.ts +906 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/Splunk/SplunkClient.test.ts +1068 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/Splunk/SplunkConnector.test.ts +1004 -0
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsConnectionTester.test.ts +784 -0
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsRunExecutor.test.ts +113 -1
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsClient.test.ts +5 -1
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsDiagnostics.test.ts +93 -13
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsHttpIntegration.test.ts +23 -1
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPoller.test.ts +25 -3
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerFailureTaxonomy.test.ts +22 -8
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerHardening.test.ts +82 -16
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsSearchDetections.test.ts +730 -0
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsThreePassPoller.test.ts +1577 -0
- package/Tests/Server/Utils/SessionReplay/SessionReplayOriginAllowlist.test.ts +41 -0
- package/Tests/Server/Utils/SessionReplay/SessionReplayReadServiceQueries.test.ts +1453 -18
- package/Tests/Server/Utils/Telemetry/EntityRegistry.test.ts +711 -0
- package/Tests/Server/Utils/Telemetry/KubernetesResourceAttributes.test.ts +162 -0
- package/Tests/Server/Utils/Telemetry/ResourceEntityFilter.test.ts +460 -13
- package/Tests/Server/Utils/Telemetry/ResourceFacetPlanner.test.ts +678 -0
- package/Tests/Server/Utils/Telemetry/ResourceFacetResolver.test.ts +868 -0
- package/Tests/Server/Utils/Telemetry/ServiceDependencyDiscovery.test.ts +525 -0
- package/Tests/Server/Utils/Telemetry/TraceContextPropagation.test.ts +420 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilCorePaths.test.ts +1014 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilExhaustiveRotation.test.ts +9 -6
- package/Tests/Types/Rules/RuleCriteriaFieldRegistry.test.ts +276 -0
- package/Tests/Types/Rum/SessionReplayApiContracts.test.ts +73 -0
- package/Tests/Types/Rum/SessionReplayTransportContracts.test.ts +95 -0
- package/Tests/Types/SecurityEvent/Connectors/ConnectorDiagnostics.test.ts +82 -0
- package/Tests/Types/SecurityEvent/Connectors/SecurityEventConnectorCatalog.test.ts +314 -0
- package/Tests/Types/Telemetry/ExceptionSpanScope.test.ts +89 -0
- package/Tests/Types/Telemetry/LlmConventions.test.ts +495 -0
- package/Tests/Types/Telemetry/ResourceEntityFacet.test.ts +96 -0
- package/Tests/Types/Telemetry/ResourceFacetCatalog.test.ts +304 -0
- package/Tests/UI/Components/ActiveFilterChipsLockedFilters.test.tsx +357 -0
- package/Tests/UI/Components/Card.test.tsx +312 -1
- package/Tests/UI/Components/CardModelDetailHeaderLayout.test.tsx +253 -0
- package/Tests/UI/Components/CardModelDetailRefresher.test.tsx +342 -0
- package/Tests/UI/Components/Checkbox.test.tsx +79 -0
- package/Tests/UI/Components/CopyTextButton.test.tsx +142 -0
- package/Tests/UI/Components/DetailCompactStyle.test.tsx +621 -0
- package/Tests/UI/Components/ErrorBoundary.test.tsx +60 -0
- package/Tests/UI/Components/FacetSections.test.tsx +581 -0
- package/Tests/UI/Components/FacetSidebarControls.test.tsx +812 -0
- package/Tests/UI/Components/FacetVisibility.test.ts +429 -0
- package/Tests/UI/Components/LockedFilterActions.test.tsx +396 -0
- package/Tests/UI/Components/LockedFilterChip.test.tsx +651 -0
- package/Tests/UI/Components/LogsEntityNames.test.ts +1769 -0
- package/Tests/UI/Components/LogsEntityNamesComponents.test.tsx +696 -0
- package/Tests/UI/Components/LogsFacetSidebarHiddenFacets.test.tsx +600 -0
- package/Tests/UI/Components/LogsViewerEntityNames.test.tsx +713 -0
- package/Tests/UI/Components/MarkdownInlineReferences.test.tsx +2079 -0
- package/Tests/UI/Components/MemberRoleAssignment.test.tsx +446 -0
- package/Tests/UI/Components/ModelDetailRefetch.test.tsx +397 -0
- package/Tests/UI/Components/ModelListRefetch.test.tsx +233 -0
- package/Tests/UI/Components/ModelPageModelChange.test.tsx +409 -0
- package/Tests/UI/Components/PaginationIntegration.test.tsx +25 -0
- package/Tests/UI/Components/ResourceFacetConfigs.test.ts +172 -0
- package/Tests/UI/Components/SavedViewsSidebarIntegration.test.tsx +32 -14
- package/Tests/UI/Components/TableSortAndMobileColumns.test.tsx +44 -1
- package/Tests/UI/Components/TelemetryActiveFilterChipsLockedFilters.test.tsx +304 -0
- package/Tests/UI/Components/TelemetryFacetSidebarHiddenFacets.test.tsx +718 -0
- package/Tests/UI/Monitor/EvaluationLogList.test.tsx +303 -0
- package/Tests/UI/Monitor/MonitorSummarySnapshotRender.test.tsx +55 -1
- package/Tests/UI/Rum/FidelityNotices.test.ts +22 -0
- package/Tests/UI/Rum/RecordingHealthCard.test.tsx +120 -136
- package/Tests/UI/Rum/RecordingHealthDashboard.test.tsx +988 -0
- package/Tests/UI/Rum/ReplayCard.test.tsx +32 -0
- package/Tests/UI/Rum/ReplayCorrelationPanel.test.tsx +709 -12
- package/Tests/UI/Rum/ReplayHeader.test.tsx +50 -1
- package/Tests/UI/Rum/ReplayPinControl.test.tsx +67 -10
- package/Tests/UI/Rum/ReplayPlayerChrome.test.tsx +41 -12
- package/Tests/UI/Rum/ReplayStage.test.tsx +663 -1
- package/Tests/UI/Rum/ReplayStageOverlays.test.tsx +173 -0
- package/Tests/UI/Rum/ReplayUi.test.tsx +189 -2
- package/Tests/UI/Rum/ReplayUserSessionsMenu.test.tsx +91 -0
- package/Tests/UI/Rum/SessionReplayEmptyState.test.tsx +1 -1
- package/Tests/UI/Rum/SessionReplaySettingsPolicyLoading.test.tsx +585 -0
- package/Tests/UI/Rum/SessionReplayTable.test.tsx +844 -51
- package/Tests/UI/Rum/SessionReplayUsersTable.test.tsx +29 -1
- package/Tests/UI/Telemetry/TelemetryDetailPanel.test.tsx +288 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +31 -1
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +6 -0
- package/Tests/UI/Utils/Clipboard.test.ts +154 -0
- package/Tests/UI/Utils/Navigation.test.ts +24 -0
- package/Tests/UI/Utils/Telemetry/TelemetryEntityNames.test.ts +2205 -0
- package/Tests/UI/Utils/Telemetry/UseTelemetryEntityNames.test.tsx +844 -0
- package/Tests/Utils/AI/InvestigationReport.test.ts +2430 -0
- package/Tests/Utils/APIOutgoingRequestTracer.test.ts +137 -0
- package/Tests/Utils/Rum/SessionReplayHealth.test.ts +884 -0
- package/Tests/Utils/Rum/SessionReplayHealthDiagnosis.test.ts +40 -0
- package/Tests/Utils/Rum/SessionReplayRecordingEnded.test.ts +619 -0
- package/Tests/Utils/SecurityEvent/Connectors/AwsSecurityHubNormalizer.test.ts +631 -0
- package/Tests/Utils/SecurityEvent/Connectors/CrowdStrikeFalconNormalizer.test.ts +479 -0
- package/Tests/Utils/SecurityEvent/Connectors/ElasticSecurityNormalizer.test.ts +507 -0
- package/Tests/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdrNormalizer.test.ts +686 -0
- package/Tests/Utils/SecurityEvent/Connectors/MicrosoftSentinelNormalizer.test.ts +474 -0
- package/Tests/Utils/SecurityEvent/Connectors/OktaNormalizer.test.ts +736 -0
- package/Tests/Utils/SecurityEvent/Connectors/SplunkNormalizer.test.ts +407 -0
- package/Tests/Utils/SecurityEvent/GoogleSecOpsAlertNormalizer.test.ts +129 -0
- package/Tests/Utils/Telemetry/CrossSignalScope.test.ts +57 -1
- package/Tests/Utils/Telemetry/EntityRelationship.test.ts +36 -0
- package/Tests/Utils/Telemetry/LockedFilterSearch.test.ts +600 -0
- package/Tests/Utils/Telemetry/NetworkHost.test.ts +48 -0
- package/Tests/Utils/Telemetry/OriginAllowList.test.ts +154 -0
- package/Tests/Utils/Traces/CriticalPath.test.ts +25 -0
- package/Types/AI/AIChatTypes.ts +8 -0
- package/Types/AI/InvestigationEvidence.ts +96 -0
- package/Types/Monitor/MonitorEvaluationSummary.ts +5 -0
- package/Types/OnCallDutyPolicy/Layer.ts +12 -0
- package/Types/Permission.ts +2 -2
- package/Types/Rules/RuleCriteriaFieldRegistry.ts +12 -0
- package/Types/Rum/SessionReplay.ts +109 -0
- package/Types/Rum/SessionReplayApi.ts +31 -0
- package/Types/SecurityEvent/Connectors/ConnectorDiagnostics.ts +111 -0
- package/Types/SecurityEvent/Connectors/SecurityEventConnectionDiagnostics.ts +87 -0
- package/Types/SecurityEvent/Connectors/SecurityEventConnectorCatalog.ts +492 -0
- package/Types/SecurityEvent/Connectors/SecurityEventConnectorProvider.ts +41 -0
- package/Types/SecurityEvent/GoogleSecOpsDiagnostics.ts +56 -1
- package/Types/SecurityEvent/GoogleSecOpsRegion.ts +34 -0
- package/Types/Telemetry/EntityType.ts +14 -0
- package/Types/Telemetry/ExceptionSpanScope.ts +83 -0
- package/Types/Telemetry/LockedFilterDetail.ts +46 -0
- package/Types/Telemetry/ResourceEntityFacet.ts +17 -10
- package/Types/Telemetry/ResourceFacetCatalog.ts +171 -0
- package/UI/Components/Card/Card.tsx +118 -83
- package/UI/Components/Checkbox/Checkbox.tsx +8 -5
- package/UI/Components/CopyTextButton/CopyTextButton.tsx +7 -1
- package/UI/Components/CustomFields/CustomFieldsDetail.tsx +4 -1
- package/UI/Components/Detail/Detail.tsx +137 -22
- package/UI/Components/ErrorBoundary.tsx +38 -0
- package/UI/Components/LogsViewer/LogsEntityNames.ts +912 -0
- package/UI/Components/LogsViewer/LogsViewer.tsx +156 -77
- package/UI/Components/LogsViewer/components/ActiveFilterChips.tsx +28 -11
- package/UI/Components/LogsViewer/components/FacetSection.tsx +66 -79
- package/UI/Components/LogsViewer/components/FacetValueRow.tsx +17 -2
- package/UI/Components/LogsViewer/components/LogDetailsPanel.tsx +45 -5
- package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +100 -19
- package/UI/Components/LogsViewer/components/LogsFacetSidebar.tsx +156 -39
- package/UI/Components/LogsViewer/components/LogsTable.tsx +30 -8
- package/UI/Components/LogsViewer/types.ts +6 -0
- package/UI/Components/Markdown.tsx/InlineReferences.tsx +628 -0
- package/UI/Components/Markdown.tsx/MarkdownViewer.tsx +385 -312
- package/UI/Components/MemberRoleAssignment/MemberRoleAssignment.tsx +50 -22
- package/UI/Components/ModelDetail/CardModelDetail.tsx +15 -0
- package/UI/Components/ModelDetail/ModelDetail.tsx +64 -19
- package/UI/Components/ModelList/ModelList.tsx +33 -8
- package/UI/Components/Page/ModelPage.tsx +119 -54
- package/UI/Components/Table/Table.tsx +8 -0
- package/UI/Components/Table/TableRow.tsx +13 -5
- package/UI/Components/TelemetryViewer/FacetVisibility.ts +229 -0
- package/UI/Components/TelemetryViewer/ResourceFacetConfigs.ts +46 -0
- package/UI/Components/TelemetryViewer/TelemetryViewer.tsx +11 -0
- package/UI/Components/TelemetryViewer/components/FacetSearchInput.tsx +79 -0
- package/UI/Components/TelemetryViewer/components/FacetSectionHeader.tsx +69 -0
- package/UI/Components/TelemetryViewer/components/FacetShowMoreButton.tsx +36 -0
- package/UI/Components/TelemetryViewer/components/HiddenFacetsFooter.tsx +88 -0
- package/UI/Components/TelemetryViewer/components/LockedFilterActions.tsx +205 -0
- package/UI/Components/TelemetryViewer/components/LockedFilterChip.tsx +496 -0
- package/UI/Components/TelemetryViewer/components/TelemetryActiveFilterChips.tsx +30 -10
- package/UI/Components/TelemetryViewer/components/TelemetryDetailPanel.tsx +203 -10
- package/UI/Components/TelemetryViewer/components/TelemetryFacetSection.tsx +60 -73
- package/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.tsx +94 -7
- package/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.tsx +17 -2
- package/UI/Components/TelemetryViewer/types.ts +19 -0
- package/UI/Components/TelemetryViewer/useFacetSearchExemptions.ts +158 -0
- package/UI/Components/TelemetryViewer/useFacetSectionSearch.ts +102 -0
- package/UI/Utils/Clipboard.ts +68 -2
- package/UI/Utils/Navigation.ts +17 -2
- package/UI/Utils/Telemetry/TelemetryEntityNames.ts +620 -0
- package/UI/Utils/Telemetry/UseTelemetryEntityNames.ts +151 -0
- package/Utils/AI/InvestigationReport.ts +1789 -0
- package/Utils/API.ts +90 -1
- package/Utils/Rum/SessionReplayHealth.ts +11 -0
- package/Utils/Rum/SessionReplayRecordingEnded.ts +134 -0
- package/Utils/SecurityEvent/Connectors/AwsSecurityHubNormalizer.ts +427 -0
- package/Utils/SecurityEvent/Connectors/CrowdStrikeFalconNormalizer.ts +197 -0
- package/Utils/SecurityEvent/Connectors/ElasticSecurityNormalizer.ts +464 -0
- package/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdrNormalizer.ts +567 -0
- package/Utils/SecurityEvent/Connectors/MicrosoftSentinelNormalizer.ts +299 -0
- package/Utils/SecurityEvent/Connectors/OktaNormalizer.ts +467 -0
- package/Utils/SecurityEvent/Connectors/SplunkNormalizer.ts +439 -0
- package/Utils/SecurityEvent/GoogleSecOpsAlertNormalizer.ts +124 -12
- package/Utils/Telemetry/CrossSignalScope.ts +22 -8
- package/Utils/Telemetry/EntityRelationship.ts +8 -0
- package/Utils/Telemetry/LockedFilterSearch.ts +412 -0
- package/Utils/Telemetry/NetworkHost.ts +43 -0
- package/Utils/Telemetry/OriginAllowList.ts +139 -4
- package/Utils/Traces/CriticalPath.ts +10 -4
- package/build/dist/Models/DatabaseModels/Index.js +4 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/RumApplication.js +1 -1
- package/build/dist/Models/DatabaseModels/RumApplication.js.map +1 -1
- package/build/dist/Models/DatabaseModels/SecurityEventConnection.js +490 -0
- package/build/dist/Models/DatabaseModels/SecurityEventConnection.js.map +1 -0
- package/build/dist/Models/DatabaseModels/SecurityEventConnectionRun.js +287 -0
- package/build/dist/Models/DatabaseModels/SecurityEventConnectionRun.js.map +1 -0
- package/build/dist/Models/DatabaseModels/TelemetryIngestionKey.js +2 -2
- package/build/dist/Models/DatabaseModels/TelemetryIngestionKey.js.map +1 -1
- package/build/dist/Server/API/AIInvestigationAPI.js +336 -27
- package/build/dist/Server/API/AIInvestigationAPI.js.map +1 -1
- package/build/dist/Server/API/GoogleSecOpsConnectionAPI.js +182 -7
- package/build/dist/Server/API/GoogleSecOpsConnectionAPI.js.map +1 -1
- package/build/dist/Server/API/SecurityEventConnectionAPI.js +171 -0
- package/build/dist/Server/API/SecurityEventConnectionAPI.js.map +1 -0
- package/build/dist/Server/API/TelemetryAPI.js +248 -170
- package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1792700000000-AddSecurityEventConnections.js +40 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1792700000000-AddSecurityEventConnections.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Middleware/TelemetryIngest.js +57 -4
- package/build/dist/Server/Middleware/TelemetryIngest.js.map +1 -1
- package/build/dist/Server/Services/ExceptionAggregationService.js +7 -16
- package/build/dist/Server/Services/ExceptionAggregationService.js.map +1 -1
- package/build/dist/Server/Services/GoogleSecOpsConnectionService.js +8 -0
- package/build/dist/Server/Services/GoogleSecOpsConnectionService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +4 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/InventoryItemService.js +22 -0
- package/build/dist/Server/Services/InventoryItemService.js.map +1 -1
- package/build/dist/Server/Services/LogAggregationService.js +8 -17
- package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
- package/build/dist/Server/Services/MetricAggregationService.js +4 -14
- package/build/dist/Server/Services/MetricAggregationService.js.map +1 -1
- package/build/dist/Server/Services/SecurityEventConnectionRunService.js +9 -0
- package/build/dist/Server/Services/SecurityEventConnectionRunService.js.map +1 -0
- package/build/dist/Server/Services/SecurityEventConnectionService.js +356 -0
- package/build/dist/Server/Services/SecurityEventConnectionService.js.map +1 -0
- package/build/dist/Server/Services/TraceAggregationService.js +51 -25
- package/build/dist/Server/Services/TraceAggregationService.js.map +1 -1
- package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js +9 -0
- package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js.map +1 -1
- package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js +3 -0
- package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js +14 -7
- package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/InvestigationEvidence.js +468 -0
- package/build/dist/Server/Utils/AI/SRE/InvestigationEvidence.js.map +1 -0
- package/build/dist/Server/Utils/AI/SRE/InvestigationReferences.js +230 -0
- package/build/dist/Server/Utils/AI/SRE/InvestigationReferences.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/Index.js +32 -0
- package/build/dist/Server/Utils/AI/Toolbox/Index.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/TimelineTools.js +6 -2
- package/build/dist/Server/Utils/AI/Toolbox/TimelineTools.js.map +1 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +59 -0
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +5 -3
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
- package/build/dist/Server/Utils/SecurityEvent/Connectors/AwsSecurityHub/AwsSecurityHubClient.js +752 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/AwsSecurityHub/AwsSecurityHubClient.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/AwsSecurityHub/AwsSecurityHubConnector.js +493 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/AwsSecurityHub/AwsSecurityHubConnector.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/ConnectorPlatformHealth.js +352 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/ConnectorPlatformHealth.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/CrowdStrikeFalcon/CrowdStrikeFalconClient.js +405 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/CrowdStrikeFalcon/CrowdStrikeFalconClient.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/CrowdStrikeFalcon/CrowdStrikeFalconConnector.js +466 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/CrowdStrikeFalcon/CrowdStrikeFalconConnector.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/ElasticSecurity/ElasticSecurityClient.js +344 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/ElasticSecurity/ElasticSecurityClient.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/ElasticSecurity/ElasticSecurityConnector.js +511 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/ElasticSecurity/ElasticSecurityConnector.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdr/MicrosoftDefenderXdrClient.js +406 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdr/MicrosoftDefenderXdrClient.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdr/MicrosoftDefenderXdrConnector.js +336 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdr/MicrosoftDefenderXdrConnector.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/MicrosoftSentinel/MicrosoftSentinelClient.js +474 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/MicrosoftSentinel/MicrosoftSentinelClient.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/MicrosoftSentinel/MicrosoftSentinelConnector.js +417 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/MicrosoftSentinel/MicrosoftSentinelConnector.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/Okta/OktaClient.js +314 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/Okta/OktaClient.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/Okta/OktaConnector.js +494 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/Okta/OktaConnector.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionPoller.js +719 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionPoller.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionRunExecutor.js +404 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionRunExecutor.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionTester.js +209 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionTester.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectorRegistry.js +40 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectorRegistry.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/Splunk/SplunkClient.js +494 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/Splunk/SplunkClient.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/Splunk/SplunkConnector.js +379 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/Splunk/SplunkConnector.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/Types.js +27 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/Types.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js +234 -61
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js.map +1 -1
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsConnectionTester.js +537 -0
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsConnectionTester.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js +572 -115
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js.map +1 -1
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsRunExecutor.js +35 -1
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsRunExecutor.js.map +1 -1
- package/build/dist/Server/Utils/SecurityEvent/SecurityEventDedupe.js +52 -0
- package/build/dist/Server/Utils/SecurityEvent/SecurityEventDedupe.js.map +1 -0
- package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js +521 -32
- package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js.map +1 -1
- package/build/dist/Server/Utils/StartServer.js +10 -0
- package/build/dist/Server/Utils/StartServer.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +8 -0
- package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/KubernetesResourceAttributes.js +80 -0
- package/build/dist/Server/Utils/Telemetry/KubernetesResourceAttributes.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js +122 -67
- package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/ResourceFacetPlanner.js +146 -0
- package/build/dist/Server/Utils/Telemetry/ResourceFacetPlanner.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/ResourceFacetResolver.js +174 -248
- package/build/dist/Server/Utils/Telemetry/ResourceFacetResolver.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/ServiceDependencyDiscovery.js +492 -0
- package/build/dist/Server/Utils/Telemetry/ServiceDependencyDiscovery.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/TraceContextPropagation.js +259 -0
- package/build/dist/Server/Utils/Telemetry/TraceContextPropagation.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry.js +11 -5
- package/build/dist/Server/Utils/Telemetry.js.map +1 -1
- package/build/dist/Types/AI/InvestigationEvidence.js +2 -0
- package/build/dist/Types/AI/InvestigationEvidence.js.map +1 -0
- package/build/dist/Types/OnCallDutyPolicy/Layer.js +11 -0
- package/build/dist/Types/OnCallDutyPolicy/Layer.js.map +1 -1
- package/build/dist/Types/Permission.js +2 -2
- package/build/dist/Types/Permission.js.map +1 -1
- package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js +9 -0
- package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js.map +1 -1
- package/build/dist/Types/Rum/SessionReplay.js +95 -0
- package/build/dist/Types/Rum/SessionReplay.js.map +1 -1
- package/build/dist/Types/Rum/SessionReplayApi.js.map +1 -1
- package/build/dist/Types/SecurityEvent/Connectors/ConnectorDiagnostics.js +18 -0
- package/build/dist/Types/SecurityEvent/Connectors/ConnectorDiagnostics.js.map +1 -0
- package/build/dist/Types/SecurityEvent/Connectors/SecurityEventConnectionDiagnostics.js +2 -0
- package/build/dist/Types/SecurityEvent/Connectors/SecurityEventConnectionDiagnostics.js.map +1 -0
- package/build/dist/Types/SecurityEvent/Connectors/SecurityEventConnectorCatalog.js +374 -0
- package/build/dist/Types/SecurityEvent/Connectors/SecurityEventConnectorCatalog.js.map +1 -0
- package/build/dist/Types/SecurityEvent/Connectors/SecurityEventConnectorProvider.js +35 -0
- package/build/dist/Types/SecurityEvent/Connectors/SecurityEventConnectorProvider.js.map +1 -0
- package/build/dist/Types/SecurityEvent/GoogleSecOpsRegion.js +35 -0
- package/build/dist/Types/SecurityEvent/GoogleSecOpsRegion.js.map +1 -0
- package/build/dist/Types/Telemetry/EntityType.js +14 -0
- package/build/dist/Types/Telemetry/EntityType.js.map +1 -1
- package/build/dist/Types/Telemetry/ExceptionSpanScope.js +59 -0
- package/build/dist/Types/Telemetry/ExceptionSpanScope.js.map +1 -0
- package/build/dist/Types/Telemetry/LockedFilterDetail.js +16 -0
- package/build/dist/Types/Telemetry/LockedFilterDetail.js.map +1 -0
- package/build/dist/Types/Telemetry/ResourceEntityFacet.js +16 -10
- package/build/dist/Types/Telemetry/ResourceEntityFacet.js.map +1 -1
- package/build/dist/Types/Telemetry/ResourceFacetCatalog.js +119 -0
- package/build/dist/Types/Telemetry/ResourceFacetCatalog.js.map +1 -0
- package/build/dist/UI/Components/Card/Card.js +34 -17
- package/build/dist/UI/Components/Card/Card.js.map +1 -1
- package/build/dist/UI/Components/Checkbox/Checkbox.js +6 -4
- package/build/dist/UI/Components/Checkbox/Checkbox.js.map +1 -1
- package/build/dist/UI/Components/CopyTextButton/CopyTextButton.js +5 -1
- package/build/dist/UI/Components/CopyTextButton/CopyTextButton.js.map +1 -1
- package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js +1 -1
- package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js.map +1 -1
- package/build/dist/UI/Components/Detail/Detail.js +63 -13
- package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
- package/build/dist/UI/Components/ErrorBoundary.js +34 -0
- package/build/dist/UI/Components/ErrorBoundary.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/LogsEntityNames.js +535 -0
- package/build/dist/UI/Components/LogsViewer/LogsEntityNames.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js +117 -57
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/ActiveFilterChips.js +5 -7
- package/build/dist/UI/Components/LogsViewer/components/ActiveFilterChips.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/FacetSection.js +31 -44
- package/build/dist/UI/Components/LogsViewer/components/FacetSection.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/FacetValueRow.js +17 -3
- package/build/dist/UI/Components/LogsViewer/components/FacetValueRow.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogDetailsPanel.js +19 -3
- package/build/dist/UI/Components/LogsViewer/components/LogDetailsPanel.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +64 -18
- package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js +100 -41
- package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsTable.js +13 -5
- package/build/dist/UI/Components/LogsViewer/components/LogsTable.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/types.js.map +1 -1
- package/build/dist/UI/Components/Markdown.tsx/InlineReferences.js +403 -0
- package/build/dist/UI/Components/Markdown.tsx/InlineReferences.js.map +1 -0
- package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js +197 -179
- package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js.map +1 -1
- package/build/dist/UI/Components/MemberRoleAssignment/MemberRoleAssignment.js +30 -21
- package/build/dist/UI/Components/MemberRoleAssignment/MemberRoleAssignment.js.map +1 -1
- package/build/dist/UI/Components/ModelDetail/CardModelDetail.js +12 -0
- package/build/dist/UI/Components/ModelDetail/CardModelDetail.js.map +1 -1
- package/build/dist/UI/Components/ModelDetail/ModelDetail.js +46 -12
- package/build/dist/UI/Components/ModelDetail/ModelDetail.js.map +1 -1
- package/build/dist/UI/Components/ModelList/ModelList.js +26 -7
- package/build/dist/UI/Components/ModelList/ModelList.js.map +1 -1
- package/build/dist/UI/Components/Page/ModelPage.js +62 -34
- package/build/dist/UI/Components/Page/ModelPage.js.map +1 -1
- package/build/dist/UI/Components/Table/Table.js +3 -1
- package/build/dist/UI/Components/Table/Table.js.map +1 -1
- package/build/dist/UI/Components/Table/TableRow.js +8 -1
- package/build/dist/UI/Components/Table/TableRow.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/FacetVisibility.js +111 -0
- package/build/dist/UI/Components/TelemetryViewer/FacetVisibility.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/ResourceFacetConfigs.js +22 -0
- package/build/dist/UI/Components/TelemetryViewer/ResourceFacetConfigs.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js +1 -1
- package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/FacetSearchInput.js +36 -0
- package/build/dist/UI/Components/TelemetryViewer/components/FacetSearchInput.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/components/FacetSectionHeader.js +21 -0
- package/build/dist/UI/Components/TelemetryViewer/components/FacetSectionHeader.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/components/FacetShowMoreButton.js +14 -0
- package/build/dist/UI/Components/TelemetryViewer/components/FacetShowMoreButton.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/components/HiddenFacetsFooter.js +27 -0
- package/build/dist/UI/Components/TelemetryViewer/components/HiddenFacetsFooter.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/components/LockedFilterActions.js +88 -0
- package/build/dist/UI/Components/TelemetryViewer/components/LockedFilterActions.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/components/LockedFilterChip.js +228 -0
- package/build/dist/UI/Components/TelemetryViewer/components/LockedFilterChip.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryActiveFilterChips.js +6 -6
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryActiveFilterChips.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryDetailPanel.js +111 -12
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryDetailPanel.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSection.js +27 -40
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSection.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.js +70 -10
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.js +17 -3
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/types.js +0 -4
- package/build/dist/UI/Components/TelemetryViewer/types.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/useFacetSearchExemptions.js +70 -0
- package/build/dist/UI/Components/TelemetryViewer/useFacetSearchExemptions.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/useFacetSectionSearch.js +52 -0
- package/build/dist/UI/Components/TelemetryViewer/useFacetSectionSearch.js.map +1 -0
- package/build/dist/UI/Utils/Clipboard.js +59 -2
- package/build/dist/UI/Utils/Clipboard.js.map +1 -1
- package/build/dist/UI/Utils/Navigation.js +11 -2
- package/build/dist/UI/Utils/Navigation.js.map +1 -1
- package/build/dist/UI/Utils/Telemetry/TelemetryEntityNames.js +471 -0
- package/build/dist/UI/Utils/Telemetry/TelemetryEntityNames.js.map +1 -0
- package/build/dist/UI/Utils/Telemetry/UseTelemetryEntityNames.js +113 -0
- package/build/dist/UI/Utils/Telemetry/UseTelemetryEntityNames.js.map +1 -0
- package/build/dist/Utils/AI/InvestigationReport.js +1241 -0
- package/build/dist/Utils/AI/InvestigationReport.js.map +1 -0
- package/build/dist/Utils/API.js +25 -0
- package/build/dist/Utils/API.js.map +1 -1
- package/build/dist/Utils/Rum/SessionReplayHealth.js +10 -0
- package/build/dist/Utils/Rum/SessionReplayHealth.js.map +1 -1
- package/build/dist/Utils/Rum/SessionReplayRecordingEnded.js +54 -0
- package/build/dist/Utils/Rum/SessionReplayRecordingEnded.js.map +1 -0
- package/build/dist/Utils/SecurityEvent/Connectors/AwsSecurityHubNormalizer.js +311 -0
- package/build/dist/Utils/SecurityEvent/Connectors/AwsSecurityHubNormalizer.js.map +1 -0
- package/build/dist/Utils/SecurityEvent/Connectors/CrowdStrikeFalconNormalizer.js +150 -0
- package/build/dist/Utils/SecurityEvent/Connectors/CrowdStrikeFalconNormalizer.js.map +1 -0
- package/build/dist/Utils/SecurityEvent/Connectors/ElasticSecurityNormalizer.js +328 -0
- package/build/dist/Utils/SecurityEvent/Connectors/ElasticSecurityNormalizer.js.map +1 -0
- package/build/dist/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdrNormalizer.js +405 -0
- package/build/dist/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdrNormalizer.js.map +1 -0
- package/build/dist/Utils/SecurityEvent/Connectors/MicrosoftSentinelNormalizer.js +212 -0
- package/build/dist/Utils/SecurityEvent/Connectors/MicrosoftSentinelNormalizer.js.map +1 -0
- package/build/dist/Utils/SecurityEvent/Connectors/OktaNormalizer.js +351 -0
- package/build/dist/Utils/SecurityEvent/Connectors/OktaNormalizer.js.map +1 -0
- package/build/dist/Utils/SecurityEvent/Connectors/SplunkNormalizer.js +348 -0
- package/build/dist/Utils/SecurityEvent/Connectors/SplunkNormalizer.js.map +1 -0
- package/build/dist/Utils/SecurityEvent/GoogleSecOpsAlertNormalizer.js +103 -10
- package/build/dist/Utils/SecurityEvent/GoogleSecOpsAlertNormalizer.js.map +1 -1
- package/build/dist/Utils/Telemetry/CrossSignalScope.js +16 -5
- package/build/dist/Utils/Telemetry/CrossSignalScope.js.map +1 -1
- package/build/dist/Utils/Telemetry/EntityRelationship.js +7 -0
- package/build/dist/Utils/Telemetry/EntityRelationship.js.map +1 -1
- package/build/dist/Utils/Telemetry/LockedFilterSearch.js +277 -0
- package/build/dist/Utils/Telemetry/LockedFilterSearch.js.map +1 -0
- package/build/dist/Utils/Telemetry/NetworkHost.js +39 -0
- package/build/dist/Utils/Telemetry/NetworkHost.js.map +1 -0
- package/build/dist/Utils/Telemetry/OriginAllowList.js +105 -4
- package/build/dist/Utils/Telemetry/OriginAllowList.js.map +1 -1
- package/build/dist/Utils/Traces/CriticalPath.js +9 -4
- package/build/dist/Utils/Traces/CriticalPath.js.map +1 -1
- package/package.json +1 -1
- package/Tests/UI/Rum/RecordingHealthStrip.test.tsx +0 -600
- package/UI/Components/GanttChart/Bar/BarLabel.tsx +0 -17
- package/UI/Components/GanttChart/Bar/Index.tsx +0 -149
- package/UI/Components/GanttChart/ChartContainer.tsx +0 -50
- package/UI/Components/GanttChart/Index.tsx +0 -90
- package/UI/Components/GanttChart/Row/Index.tsx +0 -22
- package/UI/Components/GanttChart/Row/Row.tsx +0 -231
- package/UI/Components/GanttChart/Row/RowLabel.tsx +0 -30
- package/UI/Components/GanttChart/Rows.tsx +0 -59
- package/UI/Components/GanttChart/Timeline/Index.tsx +0 -46
- package/UI/Components/GanttChart/Timeline/TimelineInterval.tsx +0 -33
- package/UI/Components/GanttChart/Timeline/TimelineIntervalMarks.tsx +0 -43
- package/build/dist/UI/Components/GanttChart/Bar/BarLabel.js +0 -8
- package/build/dist/UI/Components/GanttChart/Bar/BarLabel.js.map +0 -1
- package/build/dist/UI/Components/GanttChart/Bar/Index.js +0 -77
- package/build/dist/UI/Components/GanttChart/Bar/Index.js.map +0 -1
- package/build/dist/UI/Components/GanttChart/ChartContainer.js +0 -33
- package/build/dist/UI/Components/GanttChart/ChartContainer.js.map +0 -1
- package/build/dist/UI/Components/GanttChart/Index.js +0 -39
- package/build/dist/UI/Components/GanttChart/Index.js.map +0 -1
- package/build/dist/UI/Components/GanttChart/Row/Index.js +0 -7
- package/build/dist/UI/Components/GanttChart/Row/Index.js.map +0 -1
- package/build/dist/UI/Components/GanttChart/Row/Row.js +0 -106
- package/build/dist/UI/Components/GanttChart/Row/Row.js.map +0 -1
- package/build/dist/UI/Components/GanttChart/Row/RowLabel.js +0 -10
- package/build/dist/UI/Components/GanttChart/Row/RowLabel.js.map +0 -1
- package/build/dist/UI/Components/GanttChart/Rows.js +0 -19
- package/build/dist/UI/Components/GanttChart/Rows.js.map +0 -1
- package/build/dist/UI/Components/GanttChart/Timeline/Index.js +0 -16
- package/build/dist/UI/Components/GanttChart/Timeline/Index.js.map +0 -1
- package/build/dist/UI/Components/GanttChart/Timeline/TimelineInterval.js +0 -13
- package/build/dist/UI/Components/GanttChart/Timeline/TimelineInterval.js.map +0 -1
- package/build/dist/UI/Components/GanttChart/Timeline/TimelineIntervalMarks.js +0 -13
- package/build/dist/UI/Components/GanttChart/Timeline/TimelineIntervalMarks.js.map +0 -1
|
@@ -4,6 +4,8 @@ import ExceptionInstanceService from "../../../../Server/Services/ExceptionInsta
|
|
|
4
4
|
import { Statement } from "../../../../Server/Utils/AnalyticsDatabase/Statement";
|
|
5
5
|
import SessionReplayReadService, {
|
|
6
6
|
MAX_LIST_ROUTES,
|
|
7
|
+
MAX_SESSION_REPLAY_SESSION_ID_LENGTH,
|
|
8
|
+
MAX_SESSION_REPLAY_SUMMARIES_BATCH_SIZE,
|
|
7
9
|
MAX_SESSION_REPLAY_USERS_LIMIT,
|
|
8
10
|
SESSION_REPLAY_ACTIVITY_SUMMARY_CACHE_TTL_MS,
|
|
9
11
|
SessionReplayApplicationActivitySummary,
|
|
@@ -17,15 +19,25 @@ import SessionReplayReadService, {
|
|
|
17
19
|
SessionReplayManifest,
|
|
18
20
|
SessionReplaySessionHeader,
|
|
19
21
|
SessionReplaySessionIdentity,
|
|
22
|
+
SessionReplaySummariesRequest,
|
|
23
|
+
SessionReplaySummary,
|
|
20
24
|
SessionReplayUserRollup,
|
|
21
25
|
SessionReplayUsersRequest,
|
|
22
26
|
SessionReplayUsersResult,
|
|
23
27
|
} from "../../../../Server/Utils/SessionReplay/SessionReplayReadService";
|
|
28
|
+
import logger from "../../../../Server/Utils/Logger";
|
|
24
29
|
import BadDataException from "../../../../Types/Exception/BadDataException";
|
|
25
30
|
import { JSONObject } from "../../../../Types/JSON";
|
|
26
31
|
import ObjectID from "../../../../Types/ObjectID";
|
|
27
|
-
import
|
|
32
|
+
import AnalyticsTableName from "../../../../Types/AnalyticsDatabase/AnalyticsTableName";
|
|
33
|
+
import {
|
|
34
|
+
MAX_SESSION_REPLAY_CHUNKS_PER_SESSION,
|
|
35
|
+
MAX_SESSION_REPLAY_READ_BYTES,
|
|
36
|
+
SESSION_REPLAY_ENDED_FINALIZE_GRACE_MS,
|
|
37
|
+
SESSION_REPLAY_ENDED_TRAILING_CHUNK_TOLERANCE_MS,
|
|
38
|
+
} from "../../../../Types/Rum/SessionReplay";
|
|
28
39
|
import { SessionReplaySortBy } from "../../../../Types/Rum/SessionReplayApi";
|
|
40
|
+
import ServiceType from "../../../../Types/Telemetry/ServiceType";
|
|
29
41
|
import { afterEach, beforeEach, describe, expect, test } from "@jest/globals";
|
|
30
42
|
|
|
31
43
|
/*
|
|
@@ -136,6 +148,173 @@ describe("SessionReplayReadService statements", () => {
|
|
|
136
148
|
return statementOf(headerQuerySpy).query;
|
|
137
149
|
}
|
|
138
150
|
|
|
151
|
+
function summariesRequest(
|
|
152
|
+
overrides: Partial<SessionReplaySummariesRequest> = {},
|
|
153
|
+
): SessionReplaySummariesRequest {
|
|
154
|
+
return {
|
|
155
|
+
projectId: projectId,
|
|
156
|
+
rumApplicationId: rumApplicationId,
|
|
157
|
+
sessionIds: ["session-a", "session-b"],
|
|
158
|
+
...overrides,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
describe("session summary batch", () => {
|
|
163
|
+
test("uses one application-scoped argMax query with only non-identity summary columns", async () => {
|
|
164
|
+
await SessionReplayReadService.getSessionSummaries(summariesRequest());
|
|
165
|
+
|
|
166
|
+
expect(headerQuerySpy).toHaveBeenCalledTimes(1);
|
|
167
|
+
expect(chunkQuerySpy).not.toHaveBeenCalled();
|
|
168
|
+
|
|
169
|
+
const statement: Statement = statementOf(headerQuerySpy);
|
|
170
|
+
const query: string = statement.query;
|
|
171
|
+
|
|
172
|
+
expect(query).toMatch(/FROM \{p\d+:Identifier\}/);
|
|
173
|
+
expect(query).toContain("projectId = ");
|
|
174
|
+
expect(query).toContain("rumApplicationId = ");
|
|
175
|
+
expect(query).toContain("sessionId IN (");
|
|
176
|
+
expect(query).toContain(
|
|
177
|
+
"GROUP BY projectId, rumApplicationId, sessionId",
|
|
178
|
+
);
|
|
179
|
+
expect(query).toContain("argMax(startTime, version)");
|
|
180
|
+
expect(query).toContain("argMax(entryUrl, version)");
|
|
181
|
+
expect(query).toContain("argMax(browserName, version)");
|
|
182
|
+
expect(query).toContain("argMax(browserVersion, version)");
|
|
183
|
+
expect(query).toContain("argMax(osName, version)");
|
|
184
|
+
expect(query).toContain("argMax(deviceType, version)");
|
|
185
|
+
expect(query).toContain("retentionDate >= now()");
|
|
186
|
+
expect(query).toContain("timeout_overflow_mode = 'throw'");
|
|
187
|
+
expect(query).not.toContain(" FINAL");
|
|
188
|
+
|
|
189
|
+
for (const forbiddenColumn of [
|
|
190
|
+
"identifiedUserKey",
|
|
191
|
+
"identifiedUserLabel",
|
|
192
|
+
"identifiedUserTraits",
|
|
193
|
+
"visitorId",
|
|
194
|
+
"payload",
|
|
195
|
+
"exitUrl",
|
|
196
|
+
"countryCode",
|
|
197
|
+
]) {
|
|
198
|
+
expect(query).not.toContain(forbiddenColumn);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const values: Array<unknown> = boundValues(statement);
|
|
202
|
+
expect(values).toContain(AnalyticsTableName.RumSession);
|
|
203
|
+
expect(values).toContain(projectId.toString());
|
|
204
|
+
expect(values).toContain(rumApplicationId.toString());
|
|
205
|
+
expect(values).toContainEqual(["session-a", "session-b"]);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("deduplicates ids, returns stable request order and omits missing or unexpected rows", async () => {
|
|
209
|
+
headerQuerySpy.mockResolvedValue(
|
|
210
|
+
fakeResultSet([
|
|
211
|
+
{
|
|
212
|
+
sessionId: "not-requested",
|
|
213
|
+
aggStartTime: 1,
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
sessionId: "session-b",
|
|
217
|
+
aggStartTime: 1700000200000,
|
|
218
|
+
aggDurationMs: "45000",
|
|
219
|
+
aggEntryUrl: "https://example.com/checkout",
|
|
220
|
+
aggBrowserName: "Firefox",
|
|
221
|
+
aggBrowserVersion: "130",
|
|
222
|
+
aggOsName: "Linux",
|
|
223
|
+
aggDeviceType: "desktop",
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
sessionId: "session-a",
|
|
227
|
+
aggStartTime: 1700000100000,
|
|
228
|
+
aggDurationMs: 15000,
|
|
229
|
+
aggEntryUrl: "https://example.com/",
|
|
230
|
+
aggBrowserName: "Safari",
|
|
231
|
+
aggBrowserVersion: "18",
|
|
232
|
+
aggOsName: "iOS",
|
|
233
|
+
aggDeviceType: "mobile",
|
|
234
|
+
},
|
|
235
|
+
/* Defensive duplicate driver rows never duplicate the response. */
|
|
236
|
+
{
|
|
237
|
+
sessionId: "session-a",
|
|
238
|
+
aggStartTime: 2,
|
|
239
|
+
},
|
|
240
|
+
]) as never,
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
const summaries: Array<SessionReplaySummary> =
|
|
244
|
+
await SessionReplayReadService.getSessionSummaries(
|
|
245
|
+
summariesRequest({
|
|
246
|
+
sessionIds: [
|
|
247
|
+
"session-a",
|
|
248
|
+
"session-b",
|
|
249
|
+
"session-a",
|
|
250
|
+
"missing-session",
|
|
251
|
+
],
|
|
252
|
+
}),
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
expect(summaries).toEqual([
|
|
256
|
+
{
|
|
257
|
+
sessionId: "session-a",
|
|
258
|
+
startTime: new Date(1700000100000),
|
|
259
|
+
startTimeUnixMs: 1700000100000,
|
|
260
|
+
durationMs: 15000,
|
|
261
|
+
entryUrl: "https://example.com/",
|
|
262
|
+
browserName: "Safari",
|
|
263
|
+
browserVersion: "18",
|
|
264
|
+
osName: "iOS",
|
|
265
|
+
deviceType: "mobile",
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
sessionId: "session-b",
|
|
269
|
+
startTime: new Date(1700000200000),
|
|
270
|
+
startTimeUnixMs: 1700000200000,
|
|
271
|
+
durationMs: 45000,
|
|
272
|
+
entryUrl: "https://example.com/checkout",
|
|
273
|
+
browserName: "Firefox",
|
|
274
|
+
browserVersion: "130",
|
|
275
|
+
osName: "Linux",
|
|
276
|
+
deviceType: "desktop",
|
|
277
|
+
},
|
|
278
|
+
]);
|
|
279
|
+
|
|
280
|
+
const values: Array<unknown> = boundValues(statementOf(headerQuerySpy));
|
|
281
|
+
expect(values).toContainEqual([
|
|
282
|
+
"session-a",
|
|
283
|
+
"session-b",
|
|
284
|
+
"missing-session",
|
|
285
|
+
]);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test.each([
|
|
289
|
+
{ name: "an empty batch", sessionIds: [] },
|
|
290
|
+
{ name: "an empty id", sessionIds: [""] },
|
|
291
|
+
{
|
|
292
|
+
name: "an overlong id",
|
|
293
|
+
sessionIds: ["x".repeat(MAX_SESSION_REPLAY_SESSION_ID_LENGTH + 1)],
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
name: "an oversized batch",
|
|
297
|
+
sessionIds: Array.from(
|
|
298
|
+
{ length: MAX_SESSION_REPLAY_SUMMARIES_BATCH_SIZE + 1 },
|
|
299
|
+
(_value: unknown, index: number): string => {
|
|
300
|
+
return `session-${index}`;
|
|
301
|
+
},
|
|
302
|
+
),
|
|
303
|
+
},
|
|
304
|
+
])(
|
|
305
|
+
"rejects $name before querying",
|
|
306
|
+
async ({ sessionIds }: { sessionIds: Array<string> }) => {
|
|
307
|
+
await expect(
|
|
308
|
+
SessionReplayReadService.getSessionSummaries(
|
|
309
|
+
summariesRequest({ sessionIds: sessionIds }),
|
|
310
|
+
),
|
|
311
|
+
).rejects.toBeInstanceOf(BadDataException);
|
|
312
|
+
|
|
313
|
+
expect(headerQuerySpy).not.toHaveBeenCalled();
|
|
314
|
+
},
|
|
315
|
+
);
|
|
316
|
+
});
|
|
317
|
+
|
|
139
318
|
describe("list predicates are HAVING clauses over argMax aliases", () => {
|
|
140
319
|
test("hasIdentifiedUser tests the digest alias, never the label", async () => {
|
|
141
320
|
const positive: string = await listQuery({ hasIdentifiedUser: true });
|
|
@@ -599,27 +778,654 @@ describe("SessionReplayReadService statements", () => {
|
|
|
599
778
|
test("projects and maps the visitor id for every caller, '' from an older recorder", async () => {
|
|
600
779
|
const visitorId: string = "0123456789abcdef0123456789abcdef";
|
|
601
780
|
|
|
602
|
-
headerQuerySpy.mockResolvedValue(
|
|
603
|
-
fakeResultSet([
|
|
604
|
-
{
|
|
605
|
-
sessionId: "s-1",
|
|
606
|
-
aggIdentifiedUserKey: "hmac-1",
|
|
607
|
-
aggVisitorId: visitorId,
|
|
608
|
-
},
|
|
609
|
-
{ sessionId: "s-2" },
|
|
610
|
-
]) as never,
|
|
611
|
-
);
|
|
781
|
+
headerQuerySpy.mockResolvedValue(
|
|
782
|
+
fakeResultSet([
|
|
783
|
+
{
|
|
784
|
+
sessionId: "s-1",
|
|
785
|
+
aggIdentifiedUserKey: "hmac-1",
|
|
786
|
+
aggVisitorId: visitorId,
|
|
787
|
+
},
|
|
788
|
+
{ sessionId: "s-2" },
|
|
789
|
+
]) as never,
|
|
790
|
+
);
|
|
791
|
+
|
|
792
|
+
const result: SessionReplayListResult =
|
|
793
|
+
await SessionReplayReadService.listSessions(listRequest());
|
|
794
|
+
|
|
795
|
+
const query: string = statementOf(headerQuerySpy).query;
|
|
796
|
+
expect(query).toContain("argMax(visitorId, version) AS aggVisitorId");
|
|
797
|
+
expect(query).not.toContain("identifiedUserLabel");
|
|
798
|
+
|
|
799
|
+
expect(result.sessions[0]!.identifiedUserKey).toBe("hmac-1");
|
|
800
|
+
expect(result.sessions[0]!.visitorId).toBe(visitorId);
|
|
801
|
+
expect(result.sessions[1]!.visitorId).toBe("");
|
|
802
|
+
});
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
/*
|
|
806
|
+
* The bug: a user closed the tab, the recorder sent its final chunk, and
|
|
807
|
+
* the sessions table kept saying "Recording now" for 10-15 minutes -
|
|
808
|
+
* because the only fact the list returned was isFinalized, and the
|
|
809
|
+
* finalizer only runs once a session has been idle for ten minutes. The
|
|
810
|
+
* list now also says whether every tab of an unfinalized session has
|
|
811
|
+
* ended, read from the chunk rows by the shared rule, once the same
|
|
812
|
+
* grace the finalizer waits on has passed.
|
|
813
|
+
*/
|
|
814
|
+
describe("hasRecordingEnded on the session list", () => {
|
|
815
|
+
/* An arbitrary fixed device clock: the end of a tab's final chunk. */
|
|
816
|
+
const FINAL_END: number = 1757000000000;
|
|
817
|
+
/*
|
|
818
|
+
* The SERVER unix ms the tab's newest chunk row was written (its
|
|
819
|
+
* version). Deliberately not FINAL_END: the grace is measured on the
|
|
820
|
+
* server clock, never on the device's.
|
|
821
|
+
*/
|
|
822
|
+
const STORED_AT: number = FINAL_END + 2345;
|
|
823
|
+
/* The first server "now" at which STORED_AT is past the grace. */
|
|
824
|
+
const GRACE_PASSED: number =
|
|
825
|
+
STORED_AT + SESSION_REPLAY_ENDED_FINALIZE_GRACE_MS;
|
|
826
|
+
|
|
827
|
+
function request(
|
|
828
|
+
overrides: Partial<SessionReplayListRequest> = {},
|
|
829
|
+
): SessionReplayListRequest {
|
|
830
|
+
return listRequest({ nowUnixMs: GRACE_PASSED, ...overrides });
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
function listRow(sessionId: string, isFinalized: boolean): JSONObject {
|
|
834
|
+
return {
|
|
835
|
+
sessionId: sessionId,
|
|
836
|
+
applicationId: rumApplicationId.toString(),
|
|
837
|
+
aggStartTime: FINAL_END - 60000,
|
|
838
|
+
aggEndTime: FINAL_END,
|
|
839
|
+
aggIsFinalized: isFinalized ? 1 : 0,
|
|
840
|
+
aggSealedReason: isFinalized ? "final-chunk" : "",
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
/* A tab whose final chunk ended at FINAL_END and nothing started after. */
|
|
845
|
+
function endedTabRow(
|
|
846
|
+
sessionId: string,
|
|
847
|
+
tabId: string,
|
|
848
|
+
overrides: JSONObject = {},
|
|
849
|
+
): JSONObject {
|
|
850
|
+
return {
|
|
851
|
+
sessionId: sessionId,
|
|
852
|
+
tabId: tabId,
|
|
853
|
+
tabHasFinalChunk: 1,
|
|
854
|
+
tabFinalChunkEndUnixMs: FINAL_END,
|
|
855
|
+
tabLastChunkStartUnixMs: FINAL_END - 15000,
|
|
856
|
+
tabMaxChunkIndex: 4,
|
|
857
|
+
tabLastChunkStoredAtUnixMs: STORED_AT,
|
|
858
|
+
...overrides,
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
/* A tab that has not sent a final chunk: maxIf's default end is epoch. */
|
|
863
|
+
function liveTabRow(
|
|
864
|
+
sessionId: string,
|
|
865
|
+
tabId: string,
|
|
866
|
+
overrides: JSONObject = {},
|
|
867
|
+
): JSONObject {
|
|
868
|
+
return {
|
|
869
|
+
sessionId: sessionId,
|
|
870
|
+
tabId: tabId,
|
|
871
|
+
tabHasFinalChunk: 0,
|
|
872
|
+
tabFinalChunkEndUnixMs: 0,
|
|
873
|
+
tabLastChunkStartUnixMs: FINAL_END,
|
|
874
|
+
tabMaxChunkIndex: 4,
|
|
875
|
+
tabLastChunkStoredAtUnixMs: STORED_AT,
|
|
876
|
+
...overrides,
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
function mockPage(rows: Array<JSONObject>): void {
|
|
881
|
+
headerQuerySpy.mockResolvedValue(fakeResultSet(rows) as never);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
function mockChunkFacts(rows: Array<JSONObject>): void {
|
|
885
|
+
chunkQuerySpy.mockResolvedValue(fakeResultSet(rows) as never);
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
function endedBySessionId(
|
|
889
|
+
result: SessionReplayListResult,
|
|
890
|
+
): Record<string, boolean> {
|
|
891
|
+
const ended: Record<string, boolean> = {};
|
|
892
|
+
|
|
893
|
+
for (const session of result.sessions) {
|
|
894
|
+
ended[session.sessionId] = session.hasRecordingEnded;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
return ended;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
test("the follow-up is one parameterised, application-pinned, per-tab key-range read over the chunk table", async () => {
|
|
901
|
+
mockPage([
|
|
902
|
+
listRow("s-1", false),
|
|
903
|
+
listRow("s-2", true),
|
|
904
|
+
listRow("s-3", false),
|
|
905
|
+
]);
|
|
906
|
+
|
|
907
|
+
await SessionReplayReadService.listSessions(request());
|
|
908
|
+
|
|
909
|
+
expect(headerQuerySpy).toHaveBeenCalledTimes(1);
|
|
910
|
+
expect(chunkQuerySpy).toHaveBeenCalledTimes(1);
|
|
911
|
+
|
|
912
|
+
const statement: Statement = statementOf(chunkQuerySpy);
|
|
913
|
+
const query: string = statement.query;
|
|
914
|
+
const bound: Array<unknown> = boundValues(statement);
|
|
915
|
+
|
|
916
|
+
/* The chunk table, bound as an identifier like every other read. */
|
|
917
|
+
expect(bound).toContain(AnalyticsTableName.RumSessionChunk);
|
|
918
|
+
|
|
919
|
+
/* The sort-key prefix (projectId, sessionId) plus the app pin. */
|
|
920
|
+
expect(whereSection(query)).toMatch(/projectId = \{p\d+:String\}/);
|
|
921
|
+
expect(whereSection(query)).toMatch(
|
|
922
|
+
/AND sessionId IN \(\{p\d+:Array\(String\)\}\)/,
|
|
923
|
+
);
|
|
924
|
+
expect(whereSection(query)).toMatch(
|
|
925
|
+
/AND rumApplicationId = \{p\d+:String\}/,
|
|
926
|
+
);
|
|
927
|
+
expect(whereSection(query)).toContain("retentionDate >= now()");
|
|
928
|
+
expect(bound).toContain(projectId.toString());
|
|
929
|
+
expect(bound).toContain(rumApplicationId.toString());
|
|
930
|
+
|
|
931
|
+
/* Only the unfinalized sessions of the page, bound, never spelled. */
|
|
932
|
+
expect(bound).toContainEqual(["s-1", "s-3"]);
|
|
933
|
+
expect(query).not.toContain("'s-1'");
|
|
934
|
+
expect(query).not.toContain("'s-3'");
|
|
935
|
+
for (const value of bound) {
|
|
936
|
+
if (Array.isArray(value)) {
|
|
937
|
+
expect(value).not.toContain("s-2");
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
/* Per tab, with exactly the five documented facts. */
|
|
942
|
+
expect(query).toContain("GROUP BY sessionId, tabId");
|
|
943
|
+
expect(query).toContain("max(toUInt8(isFinal)) AS tabHasFinalChunk");
|
|
944
|
+
expect(query).toContain(
|
|
945
|
+
"toFloat64(toUnixTimestamp64Milli(maxIf(chunkEndTime, isFinal))) AS tabFinalChunkEndUnixMs",
|
|
946
|
+
);
|
|
947
|
+
expect(query).toContain(
|
|
948
|
+
"toFloat64(toUnixTimestamp64Milli(max(chunkStartTime))) AS tabLastChunkStartUnixMs",
|
|
949
|
+
);
|
|
950
|
+
expect(query).toContain("toFloat64(max(chunkIndex)) AS tabMaxChunkIndex");
|
|
951
|
+
expect(query).toContain(
|
|
952
|
+
"toFloat64(max(version)) AS tabLastChunkStoredAtUnixMs",
|
|
953
|
+
);
|
|
954
|
+
|
|
955
|
+
/*
|
|
956
|
+
* The grace is judged in the process against the rows' server write
|
|
957
|
+
* time, not in SQL against the device clock or now64().
|
|
958
|
+
*/
|
|
959
|
+
expect(query).not.toContain("now64(");
|
|
960
|
+
|
|
961
|
+
/* Maxima need no de-duplication, and the payload is never named. */
|
|
962
|
+
expect(query).not.toContain("LIMIT 1 BY");
|
|
963
|
+
expect(query).not.toMatch(/\bpayload\b(?!Bytes)/);
|
|
964
|
+
expect(query).toContain("timeout_overflow_mode = 'throw'");
|
|
965
|
+
});
|
|
966
|
+
|
|
967
|
+
test("an unfinalized session whose single tab sent its final chunk reads as ended once the grace has passed (the closed-tab regression)", async () => {
|
|
968
|
+
mockPage([listRow("s-1", false)]);
|
|
969
|
+
mockChunkFacts([endedTabRow("s-1", "tab-1")]);
|
|
970
|
+
|
|
971
|
+
const result: SessionReplayListResult =
|
|
972
|
+
await SessionReplayReadService.listSessions(request());
|
|
973
|
+
|
|
974
|
+
expect(result.sessions).toHaveLength(1);
|
|
975
|
+
expect(result.sessions[0]!.isFinalized).toBe(false);
|
|
976
|
+
expect(result.sessions[0]!.hasRecordingEnded).toBe(true);
|
|
977
|
+
});
|
|
978
|
+
|
|
979
|
+
/*
|
|
980
|
+
* The flap this grace exists for: in a multi-page app, page A's final
|
|
981
|
+
* chunk lands while page B (a new tab id under the same session) has
|
|
982
|
+
* not stored its first chunk yet. For that moment the only tab the
|
|
983
|
+
* rows know about has ended, and without a grace the list would say
|
|
984
|
+
* "Recording ended" for a session that is still recording, then flip
|
|
985
|
+
* back on the next poll.
|
|
986
|
+
*/
|
|
987
|
+
test.each([
|
|
988
|
+
["stored just now", 0, false],
|
|
989
|
+
["stored 20 s ago (page B's first chunk still on its way)", 20000, false],
|
|
990
|
+
[
|
|
991
|
+
"stored 1 ms short of the grace",
|
|
992
|
+
SESSION_REPLAY_ENDED_FINALIZE_GRACE_MS - 1,
|
|
993
|
+
false,
|
|
994
|
+
],
|
|
995
|
+
[
|
|
996
|
+
"stored exactly the grace ago",
|
|
997
|
+
SESSION_REPLAY_ENDED_FINALIZE_GRACE_MS,
|
|
998
|
+
true,
|
|
999
|
+
],
|
|
1000
|
+
[
|
|
1001
|
+
"stored well past the grace",
|
|
1002
|
+
SESSION_REPLAY_ENDED_FINALIZE_GRACE_MS + 5 * 60 * 1000,
|
|
1003
|
+
true,
|
|
1004
|
+
],
|
|
1005
|
+
] as Array<[string, number, boolean]>)(
|
|
1006
|
+
"a single sealed tab %s (%i ms old): hasRecordingEnded %s",
|
|
1007
|
+
async (_label: string, ageMs: number, expected: boolean) => {
|
|
1008
|
+
mockPage([listRow("s-1", false)]);
|
|
1009
|
+
mockChunkFacts([endedTabRow("s-1", "tab-1")]);
|
|
1010
|
+
|
|
1011
|
+
const result: SessionReplayListResult =
|
|
1012
|
+
await SessionReplayReadService.listSessions(
|
|
1013
|
+
request({ nowUnixMs: STORED_AT + ageMs }),
|
|
1014
|
+
);
|
|
1015
|
+
|
|
1016
|
+
expect(result.sessions[0]!.hasRecordingEnded).toBe(expected);
|
|
1017
|
+
},
|
|
1018
|
+
);
|
|
1019
|
+
|
|
1020
|
+
test("the grace is measured from the NEWEST stored chunk of any tab, not from the oldest seal", async () => {
|
|
1021
|
+
/*
|
|
1022
|
+
* Page A sealed and was stored long ago; page B sealed moments ago.
|
|
1023
|
+
* Page C may still be about to register, so the session is not over.
|
|
1024
|
+
*/
|
|
1025
|
+
mockPage([listRow("s-1", false)]);
|
|
1026
|
+
mockChunkFacts([
|
|
1027
|
+
endedTabRow("s-1", "tab-page-a", {
|
|
1028
|
+
tabLastChunkStoredAtUnixMs: STORED_AT - 10 * 60 * 1000,
|
|
1029
|
+
}),
|
|
1030
|
+
endedTabRow("s-1", "tab-page-b", {
|
|
1031
|
+
tabLastChunkStoredAtUnixMs: STORED_AT,
|
|
1032
|
+
}),
|
|
1033
|
+
]);
|
|
1034
|
+
|
|
1035
|
+
const early: SessionReplayListResult =
|
|
1036
|
+
await SessionReplayReadService.listSessions(
|
|
1037
|
+
request({ nowUnixMs: GRACE_PASSED - 1 }),
|
|
1038
|
+
);
|
|
1039
|
+
expect(early.sessions[0]!.hasRecordingEnded).toBe(false);
|
|
1040
|
+
|
|
1041
|
+
const late: SessionReplayListResult =
|
|
1042
|
+
await SessionReplayReadService.listSessions(
|
|
1043
|
+
request({ nowUnixMs: GRACE_PASSED }),
|
|
1044
|
+
);
|
|
1045
|
+
expect(late.sessions[0]!.hasRecordingEnded).toBe(true);
|
|
1046
|
+
});
|
|
1047
|
+
|
|
1048
|
+
test("the grace reads the rows' server write time, not the device's chunk clock", async () => {
|
|
1049
|
+
/*
|
|
1050
|
+
* A device clock hours behind the server: its final chunk "ended"
|
|
1051
|
+
* long before now, but the row was stored seconds ago.
|
|
1052
|
+
*/
|
|
1053
|
+
mockPage([listRow("s-1", false)]);
|
|
1054
|
+
mockChunkFacts([
|
|
1055
|
+
endedTabRow("s-1", "tab-1", {
|
|
1056
|
+
tabFinalChunkEndUnixMs: FINAL_END - 3 * 60 * 60 * 1000,
|
|
1057
|
+
tabLastChunkStartUnixMs: FINAL_END - 3 * 60 * 60 * 1000 - 15000,
|
|
1058
|
+
}),
|
|
1059
|
+
]);
|
|
1060
|
+
|
|
1061
|
+
const result: SessionReplayListResult =
|
|
1062
|
+
await SessionReplayReadService.listSessions(
|
|
1063
|
+
request({ nowUnixMs: STORED_AT + 5000 }),
|
|
1064
|
+
);
|
|
1065
|
+
|
|
1066
|
+
expect(result.sessions[0]!.hasRecordingEnded).toBe(false);
|
|
1067
|
+
});
|
|
1068
|
+
|
|
1069
|
+
test("without an injected clock the list measures the grace against Date.now()", async () => {
|
|
1070
|
+
mockPage([listRow("s-1", false)]);
|
|
1071
|
+
mockChunkFacts([endedTabRow("s-1", "tab-1")]);
|
|
1072
|
+
|
|
1073
|
+
const nowSpy: jest.SpyInstance = jest
|
|
1074
|
+
.spyOn(Date, "now")
|
|
1075
|
+
.mockReturnValue(GRACE_PASSED - 1);
|
|
1076
|
+
|
|
1077
|
+
const early: SessionReplayListResult =
|
|
1078
|
+
await SessionReplayReadService.listSessions(listRequest());
|
|
1079
|
+
expect(early.sessions[0]!.hasRecordingEnded).toBe(false);
|
|
1080
|
+
|
|
1081
|
+
nowSpy.mockReturnValue(GRACE_PASSED);
|
|
1082
|
+
|
|
1083
|
+
const late: SessionReplayListResult =
|
|
1084
|
+
await SessionReplayReadService.listSessions(listRequest());
|
|
1085
|
+
expect(late.sessions[0]!.hasRecordingEnded).toBe(true);
|
|
1086
|
+
});
|
|
1087
|
+
|
|
1088
|
+
/*
|
|
1089
|
+
* A tab that reached the per-session chunk cap never gets a final
|
|
1090
|
+
* chunk: the ingest gate refuses every index past the last one, the
|
|
1091
|
+
* recorder's own truncation seal included. It has ended all the same.
|
|
1092
|
+
*/
|
|
1093
|
+
test.each([
|
|
1094
|
+
[
|
|
1095
|
+
"at the last permitted index, grace passed",
|
|
1096
|
+
MAX_SESSION_REPLAY_CHUNKS_PER_SESSION - 1,
|
|
1097
|
+
GRACE_PASSED,
|
|
1098
|
+
true,
|
|
1099
|
+
],
|
|
1100
|
+
[
|
|
1101
|
+
"at the last permitted index, inside the grace",
|
|
1102
|
+
MAX_SESSION_REPLAY_CHUNKS_PER_SESSION - 1,
|
|
1103
|
+
GRACE_PASSED - 1,
|
|
1104
|
+
false,
|
|
1105
|
+
],
|
|
1106
|
+
[
|
|
1107
|
+
"one index short of the cap, grace passed",
|
|
1108
|
+
MAX_SESSION_REPLAY_CHUNKS_PER_SESSION - 2,
|
|
1109
|
+
GRACE_PASSED,
|
|
1110
|
+
false,
|
|
1111
|
+
],
|
|
1112
|
+
] as Array<[string, number, number, boolean]>)(
|
|
1113
|
+
"a capped tab with no final chunk %s (index %i, now %i): hasRecordingEnded %s",
|
|
1114
|
+
async (
|
|
1115
|
+
_label: string,
|
|
1116
|
+
maxChunkIndex: number,
|
|
1117
|
+
nowUnixMs: number,
|
|
1118
|
+
expected: boolean,
|
|
1119
|
+
) => {
|
|
1120
|
+
mockPage([listRow("s-1", false)]);
|
|
1121
|
+
mockChunkFacts([
|
|
1122
|
+
liveTabRow("s-1", "tab-1", { tabMaxChunkIndex: maxChunkIndex }),
|
|
1123
|
+
]);
|
|
1124
|
+
|
|
1125
|
+
const result: SessionReplayListResult =
|
|
1126
|
+
await SessionReplayReadService.listSessions(
|
|
1127
|
+
request({ nowUnixMs: nowUnixMs }),
|
|
1128
|
+
);
|
|
1129
|
+
|
|
1130
|
+
expect(result.sessions[0]!.hasRecordingEnded).toBe(expected);
|
|
1131
|
+
},
|
|
1132
|
+
);
|
|
1133
|
+
|
|
1134
|
+
test("one live tab among ended ones keeps the whole session recording", async () => {
|
|
1135
|
+
mockPage([listRow("s-1", false)]);
|
|
1136
|
+
mockChunkFacts([
|
|
1137
|
+
endedTabRow("s-1", "tab-1"),
|
|
1138
|
+
liveTabRow("s-1", "tab-2"),
|
|
1139
|
+
endedTabRow("s-1", "tab-3"),
|
|
1140
|
+
]);
|
|
1141
|
+
|
|
1142
|
+
const result: SessionReplayListResult =
|
|
1143
|
+
await SessionReplayReadService.listSessions(request());
|
|
1144
|
+
|
|
1145
|
+
expect(result.sessions[0]!.hasRecordingEnded).toBe(false);
|
|
1146
|
+
});
|
|
1147
|
+
|
|
1148
|
+
test("an older recorder's trailing chunk inside the tolerance still ends the tab; one past it does not", async () => {
|
|
1149
|
+
mockPage([listRow("s-trailing", false), listRow("s-resumed", false)]);
|
|
1150
|
+
mockChunkFacts([
|
|
1151
|
+
endedTabRow("s-trailing", "tab-1", {
|
|
1152
|
+
tabLastChunkStartUnixMs:
|
|
1153
|
+
FINAL_END + SESSION_REPLAY_ENDED_TRAILING_CHUNK_TOLERANCE_MS,
|
|
1154
|
+
}),
|
|
1155
|
+
endedTabRow("s-resumed", "tab-1", {
|
|
1156
|
+
tabLastChunkStartUnixMs:
|
|
1157
|
+
FINAL_END + SESSION_REPLAY_ENDED_TRAILING_CHUNK_TOLERANCE_MS + 1,
|
|
1158
|
+
}),
|
|
1159
|
+
]);
|
|
1160
|
+
|
|
1161
|
+
const result: SessionReplayListResult =
|
|
1162
|
+
await SessionReplayReadService.listSessions(request());
|
|
1163
|
+
|
|
1164
|
+
expect(endedBySessionId(result)).toEqual({
|
|
1165
|
+
"s-trailing": true,
|
|
1166
|
+
"s-resumed": false,
|
|
1167
|
+
});
|
|
1168
|
+
});
|
|
1169
|
+
|
|
1170
|
+
test("each session is judged on its own tabs and its own stored times", async () => {
|
|
1171
|
+
mockPage([
|
|
1172
|
+
listRow("s-ended", false),
|
|
1173
|
+
listRow("s-live", false),
|
|
1174
|
+
listRow("s-mixed", false),
|
|
1175
|
+
listRow("s-recent", false),
|
|
1176
|
+
]);
|
|
1177
|
+
mockChunkFacts([
|
|
1178
|
+
endedTabRow("s-ended", "tab-1"),
|
|
1179
|
+
endedTabRow("s-ended", "tab-2"),
|
|
1180
|
+
liveTabRow("s-live", "tab-1"),
|
|
1181
|
+
endedTabRow("s-mixed", "tab-1"),
|
|
1182
|
+
liveTabRow("s-mixed", "tab-2"),
|
|
1183
|
+
endedTabRow("s-recent", "tab-1", {
|
|
1184
|
+
tabLastChunkStoredAtUnixMs: GRACE_PASSED - 1000,
|
|
1185
|
+
}),
|
|
1186
|
+
]);
|
|
1187
|
+
|
|
1188
|
+
const result: SessionReplayListResult =
|
|
1189
|
+
await SessionReplayReadService.listSessions(request());
|
|
1190
|
+
|
|
1191
|
+
expect(endedBySessionId(result)).toEqual({
|
|
1192
|
+
"s-ended": true,
|
|
1193
|
+
"s-live": false,
|
|
1194
|
+
"s-mixed": false,
|
|
1195
|
+
"s-recent": false,
|
|
1196
|
+
});
|
|
1197
|
+
});
|
|
1198
|
+
|
|
1199
|
+
test("a finalized session is never 'ended', even when chunk facts for it come back", async () => {
|
|
1200
|
+
mockPage([listRow("s-final", true), listRow("s-open", false)]);
|
|
1201
|
+
mockChunkFacts([
|
|
1202
|
+
endedTabRow("s-final", "tab-1"),
|
|
1203
|
+
endedTabRow("s-open", "tab-1"),
|
|
1204
|
+
]);
|
|
1205
|
+
|
|
1206
|
+
const result: SessionReplayListResult =
|
|
1207
|
+
await SessionReplayReadService.listSessions(request());
|
|
1208
|
+
|
|
1209
|
+
expect(endedBySessionId(result)).toEqual({
|
|
1210
|
+
"s-final": false,
|
|
1211
|
+
"s-open": true,
|
|
1212
|
+
});
|
|
1213
|
+
expect(boundValues(statementOf(chunkQuerySpy))).toContainEqual([
|
|
1214
|
+
"s-open",
|
|
1215
|
+
]);
|
|
1216
|
+
});
|
|
1217
|
+
|
|
1218
|
+
test("a page of only finalized sessions runs no follow-up query", async () => {
|
|
1219
|
+
mockPage([listRow("s-1", true), listRow("s-2", true)]);
|
|
1220
|
+
|
|
1221
|
+
const result: SessionReplayListResult =
|
|
1222
|
+
await SessionReplayReadService.listSessions(request());
|
|
1223
|
+
|
|
1224
|
+
expect(chunkQuerySpy).not.toHaveBeenCalled();
|
|
1225
|
+
expect(endedBySessionId(result)).toEqual({ "s-1": false, "s-2": false });
|
|
1226
|
+
});
|
|
1227
|
+
|
|
1228
|
+
test("an empty page runs no follow-up query", async () => {
|
|
1229
|
+
mockPage([]);
|
|
1230
|
+
|
|
1231
|
+
const result: SessionReplayListResult =
|
|
1232
|
+
await SessionReplayReadService.listSessions(request());
|
|
1233
|
+
|
|
1234
|
+
expect(result.sessions).toHaveLength(0);
|
|
1235
|
+
expect(chunkQuerySpy).not.toHaveBeenCalled();
|
|
1236
|
+
});
|
|
1237
|
+
|
|
1238
|
+
test("the look-ahead row past the page size is not asked about", async () => {
|
|
1239
|
+
mockPage([listRow("s-1", false), listRow("s-2", false)]);
|
|
1240
|
+
|
|
1241
|
+
const result: SessionReplayListResult =
|
|
1242
|
+
await SessionReplayReadService.listSessions(request({ limit: 1 }));
|
|
1243
|
+
|
|
1244
|
+
expect(result.sessions).toHaveLength(1);
|
|
1245
|
+
expect(result.nextCursor).not.toBeNull();
|
|
1246
|
+
expect(boundValues(statementOf(chunkQuerySpy))).toContainEqual(["s-1"]);
|
|
1247
|
+
});
|
|
1248
|
+
|
|
1249
|
+
test("an unfinalized session with no chunk rows yet is not 'ended'", async () => {
|
|
1250
|
+
mockPage([listRow("s-1", false), listRow("s-2", false)]);
|
|
1251
|
+
mockChunkFacts([endedTabRow("s-2", "tab-1")]);
|
|
1252
|
+
|
|
1253
|
+
const result: SessionReplayListResult =
|
|
1254
|
+
await SessionReplayReadService.listSessions(request());
|
|
1255
|
+
|
|
1256
|
+
expect(endedBySessionId(result)).toEqual({ "s-1": false, "s-2": true });
|
|
1257
|
+
});
|
|
1258
|
+
|
|
1259
|
+
test("a thrown follow-up leaves every row false and still returns the list", async () => {
|
|
1260
|
+
const warnSpy: jest.SpyInstance = jest
|
|
1261
|
+
.spyOn(logger, "warn")
|
|
1262
|
+
.mockImplementation((): void => {
|
|
1263
|
+
return;
|
|
1264
|
+
});
|
|
1265
|
+
|
|
1266
|
+
mockPage([listRow("s-1", false), listRow("s-2", true)]);
|
|
1267
|
+
chunkQuerySpy.mockRejectedValue(new Error("clickhouse down") as never);
|
|
1268
|
+
|
|
1269
|
+
const result: SessionReplayListResult =
|
|
1270
|
+
await SessionReplayReadService.listSessions(request());
|
|
1271
|
+
|
|
1272
|
+
expect(result.sessions).toHaveLength(2);
|
|
1273
|
+
expect(endedBySessionId(result)).toEqual({ "s-1": false, "s-2": false });
|
|
1274
|
+
expect(warnSpy).toHaveBeenCalled();
|
|
1275
|
+
});
|
|
1276
|
+
|
|
1277
|
+
test("a follow-up whose body cannot be parsed degrades the same way", async () => {
|
|
1278
|
+
jest.spyOn(logger, "warn").mockImplementation((): void => {
|
|
1279
|
+
return;
|
|
1280
|
+
});
|
|
1281
|
+
|
|
1282
|
+
mockPage([listRow("s-1", false)]);
|
|
1283
|
+
chunkQuerySpy.mockResolvedValue({
|
|
1284
|
+
json: async (): Promise<JSONObject> => {
|
|
1285
|
+
throw new Error("truncated body");
|
|
1286
|
+
},
|
|
1287
|
+
} as never);
|
|
1288
|
+
|
|
1289
|
+
const result: SessionReplayListResult =
|
|
1290
|
+
await SessionReplayReadService.listSessions(request());
|
|
1291
|
+
|
|
1292
|
+
expect(result.sessions[0]!.hasRecordingEnded).toBe(false);
|
|
1293
|
+
});
|
|
1294
|
+
|
|
1295
|
+
test("ClickHouse's quoted numbers parse, and a missing clock is never read as ended", async () => {
|
|
1296
|
+
mockPage([
|
|
1297
|
+
listRow("s-quoted", false),
|
|
1298
|
+
listRow("s-no-start", false),
|
|
1299
|
+
listRow("s-no-end", false),
|
|
1300
|
+
listRow("s-garbled", false),
|
|
1301
|
+
listRow("s-no-stored-at", false),
|
|
1302
|
+
listRow("s-blank-stored-at", false),
|
|
1303
|
+
listRow("s-no-index", false),
|
|
1304
|
+
]);
|
|
1305
|
+
mockChunkFacts([
|
|
1306
|
+
{
|
|
1307
|
+
sessionId: "s-quoted",
|
|
1308
|
+
tabId: "tab-1",
|
|
1309
|
+
tabHasFinalChunk: "1",
|
|
1310
|
+
tabFinalChunkEndUnixMs: String(FINAL_END),
|
|
1311
|
+
tabLastChunkStartUnixMs: String(FINAL_END - 15000),
|
|
1312
|
+
tabMaxChunkIndex: "4",
|
|
1313
|
+
tabLastChunkStoredAtUnixMs: String(STORED_AT),
|
|
1314
|
+
},
|
|
1315
|
+
{
|
|
1316
|
+
sessionId: "s-no-start",
|
|
1317
|
+
tabId: "tab-1",
|
|
1318
|
+
tabHasFinalChunk: 1,
|
|
1319
|
+
tabFinalChunkEndUnixMs: FINAL_END,
|
|
1320
|
+
tabMaxChunkIndex: 4,
|
|
1321
|
+
tabLastChunkStoredAtUnixMs: STORED_AT,
|
|
1322
|
+
},
|
|
1323
|
+
{
|
|
1324
|
+
sessionId: "s-no-end",
|
|
1325
|
+
tabId: "tab-1",
|
|
1326
|
+
tabHasFinalChunk: 1,
|
|
1327
|
+
tabLastChunkStartUnixMs: FINAL_END - 15000,
|
|
1328
|
+
tabMaxChunkIndex: 4,
|
|
1329
|
+
tabLastChunkStoredAtUnixMs: STORED_AT,
|
|
1330
|
+
},
|
|
1331
|
+
{
|
|
1332
|
+
sessionId: "s-garbled",
|
|
1333
|
+
tabId: "tab-1",
|
|
1334
|
+
tabHasFinalChunk: 1,
|
|
1335
|
+
tabFinalChunkEndUnixMs: "not-a-number",
|
|
1336
|
+
tabLastChunkStartUnixMs: FINAL_END - 15000,
|
|
1337
|
+
tabMaxChunkIndex: 4,
|
|
1338
|
+
tabLastChunkStoredAtUnixMs: STORED_AT,
|
|
1339
|
+
},
|
|
1340
|
+
/*
|
|
1341
|
+
* A missing write time must not read as 0 - epoch is older than
|
|
1342
|
+
* any grace, which would skip it outright.
|
|
1343
|
+
*/
|
|
1344
|
+
{
|
|
1345
|
+
sessionId: "s-no-stored-at",
|
|
1346
|
+
tabId: "tab-1",
|
|
1347
|
+
tabHasFinalChunk: 1,
|
|
1348
|
+
tabFinalChunkEndUnixMs: FINAL_END,
|
|
1349
|
+
tabLastChunkStartUnixMs: FINAL_END - 15000,
|
|
1350
|
+
tabMaxChunkIndex: 4,
|
|
1351
|
+
},
|
|
1352
|
+
{
|
|
1353
|
+
sessionId: "s-blank-stored-at",
|
|
1354
|
+
tabId: "tab-1",
|
|
1355
|
+
tabHasFinalChunk: 1,
|
|
1356
|
+
tabFinalChunkEndUnixMs: FINAL_END,
|
|
1357
|
+
tabLastChunkStartUnixMs: FINAL_END - 15000,
|
|
1358
|
+
tabMaxChunkIndex: 4,
|
|
1359
|
+
tabLastChunkStoredAtUnixMs: " ",
|
|
1360
|
+
},
|
|
1361
|
+
/*
|
|
1362
|
+
* A missing chunk index only means the cap rule cannot apply; a
|
|
1363
|
+
* sealed tab is still judged by its seal.
|
|
1364
|
+
*/
|
|
1365
|
+
{
|
|
1366
|
+
sessionId: "s-no-index",
|
|
1367
|
+
tabId: "tab-1",
|
|
1368
|
+
tabHasFinalChunk: 1,
|
|
1369
|
+
tabFinalChunkEndUnixMs: FINAL_END,
|
|
1370
|
+
tabLastChunkStartUnixMs: FINAL_END - 15000,
|
|
1371
|
+
tabLastChunkStoredAtUnixMs: STORED_AT,
|
|
1372
|
+
},
|
|
1373
|
+
]);
|
|
1374
|
+
|
|
1375
|
+
const result: SessionReplayListResult =
|
|
1376
|
+
await SessionReplayReadService.listSessions(request());
|
|
1377
|
+
|
|
1378
|
+
expect(endedBySessionId(result)).toEqual({
|
|
1379
|
+
"s-quoted": true,
|
|
1380
|
+
"s-no-start": false,
|
|
1381
|
+
"s-no-end": false,
|
|
1382
|
+
"s-garbled": false,
|
|
1383
|
+
"s-no-stored-at": false,
|
|
1384
|
+
"s-blank-stored-at": false,
|
|
1385
|
+
"s-no-index": true,
|
|
1386
|
+
});
|
|
1387
|
+
});
|
|
1388
|
+
|
|
1389
|
+
test("a missing or garbled chunk index never reads as capped", async () => {
|
|
1390
|
+
mockPage([listRow("s-no-index", false), listRow("s-garbled", false)]);
|
|
1391
|
+
mockChunkFacts([
|
|
1392
|
+
liveTabRow("s-no-index", "tab-1", { tabMaxChunkIndex: undefined }),
|
|
1393
|
+
liveTabRow("s-garbled", "tab-1", { tabMaxChunkIndex: "Infinity" }),
|
|
1394
|
+
]);
|
|
612
1395
|
|
|
613
1396
|
const result: SessionReplayListResult =
|
|
614
|
-
await SessionReplayReadService.listSessions(
|
|
1397
|
+
await SessionReplayReadService.listSessions(request());
|
|
615
1398
|
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
1399
|
+
expect(endedBySessionId(result)).toEqual({
|
|
1400
|
+
"s-no-index": false,
|
|
1401
|
+
"s-garbled": false,
|
|
1402
|
+
});
|
|
1403
|
+
});
|
|
619
1404
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
1405
|
+
test("a chunk row for a session that was not asked about decides nothing", async () => {
|
|
1406
|
+
mockPage([listRow("s-1", false)]);
|
|
1407
|
+
mockChunkFacts([
|
|
1408
|
+
endedTabRow("s-1", "tab-1"),
|
|
1409
|
+
liveTabRow("someone-else", "tab-1"),
|
|
1410
|
+
endedTabRow("", "tab-1"),
|
|
1411
|
+
]);
|
|
1412
|
+
|
|
1413
|
+
const result: SessionReplayListResult =
|
|
1414
|
+
await SessionReplayReadService.listSessions(request());
|
|
1415
|
+
|
|
1416
|
+
expect(result.sessions[0]!.hasRecordingEnded).toBe(true);
|
|
1417
|
+
});
|
|
1418
|
+
|
|
1419
|
+
test("the follow-up does not change the list statement or its filters", async () => {
|
|
1420
|
+
mockPage([listRow("s-1", false)]);
|
|
1421
|
+
|
|
1422
|
+
const query: string = await listQuery({ isFinalized: false });
|
|
1423
|
+
|
|
1424
|
+
/* "Live" stays "not finalized"; the chunk facts never reach HAVING. */
|
|
1425
|
+
expect(havingSection(query)).toMatch(/aggIsFinalized = \{p\d+:Bool\}/);
|
|
1426
|
+
expect(query).not.toContain("isFinal)");
|
|
1427
|
+
expect(query).not.toContain("tabHasFinalChunk");
|
|
1428
|
+
expect(query).not.toContain("tabLastChunkStoredAtUnixMs");
|
|
623
1429
|
});
|
|
624
1430
|
});
|
|
625
1431
|
|
|
@@ -743,6 +1549,30 @@ describe("SessionReplayReadService statements", () => {
|
|
|
743
1549
|
expect(older!.visitorId).toBe("");
|
|
744
1550
|
});
|
|
745
1551
|
|
|
1552
|
+
/*
|
|
1553
|
+
* The header read is also the authorization lookup behind every chunk
|
|
1554
|
+
* and heartbeat request (cached 30s), so it must not grow a chunk-table
|
|
1555
|
+
* read. It answers false; getManifest judges the field.
|
|
1556
|
+
*/
|
|
1557
|
+
test("never reads the chunk table and leaves hasRecordingEnded to the manifest", async () => {
|
|
1558
|
+
headerQuerySpy.mockResolvedValue(
|
|
1559
|
+
fakeResultSet([
|
|
1560
|
+
{ ...headerRow, aggIsFinalized: 0, aggSealedReason: "final-chunk" },
|
|
1561
|
+
]) as never,
|
|
1562
|
+
);
|
|
1563
|
+
|
|
1564
|
+
const header: SessionReplaySessionHeader | null =
|
|
1565
|
+
await SessionReplayReadService.getSessionHeader({
|
|
1566
|
+
projectId: projectId,
|
|
1567
|
+
sessionId: "s-1",
|
|
1568
|
+
rumApplicationId: rumApplicationId,
|
|
1569
|
+
});
|
|
1570
|
+
|
|
1571
|
+
expect(header!.isFinalized).toBe(false);
|
|
1572
|
+
expect(header!.hasRecordingEnded).toBe(false);
|
|
1573
|
+
expect(chunkQuerySpy).not.toHaveBeenCalled();
|
|
1574
|
+
});
|
|
1575
|
+
|
|
746
1576
|
test("an ambiguous session id is refused with an actionable message", async () => {
|
|
747
1577
|
headerQuerySpy.mockResolvedValue(
|
|
748
1578
|
fakeResultSet([
|
|
@@ -1355,6 +2185,7 @@ describe("SessionReplayReadService statements", () => {
|
|
|
1355
2185
|
recorderCapabilities: [],
|
|
1356
2186
|
identifiedUserKey: "",
|
|
1357
2187
|
visitorId: "",
|
|
2188
|
+
hasRecordingEnded: false,
|
|
1358
2189
|
...overrides,
|
|
1359
2190
|
};
|
|
1360
2191
|
}
|
|
@@ -1460,6 +2291,382 @@ describe("SessionReplayReadService statements", () => {
|
|
|
1460
2291
|
expect(manifest.header.endTimeUnixMs).toBe(1700000150000);
|
|
1461
2292
|
expect(manifest.header.endTime.getTime()).toBe(1700000150000);
|
|
1462
2293
|
});
|
|
2294
|
+
|
|
2295
|
+
/*
|
|
2296
|
+
* The player's Live pill reads the manifest header. The header read
|
|
2297
|
+
* itself never judges hasRecordingEnded (it is also the cached
|
|
2298
|
+
* authorization lookup), so the manifest does, with the same helper
|
|
2299
|
+
* and the same statement as the list.
|
|
2300
|
+
*/
|
|
2301
|
+
describe("hasRecordingEnded on the manifest header", () => {
|
|
2302
|
+
const FINAL_END: number = 1700000150000;
|
|
2303
|
+
/* Server write time of the newest chunk row (its version). */
|
|
2304
|
+
const STORED_AT: number = FINAL_END + 1500;
|
|
2305
|
+
const GRACE_PASSED: number =
|
|
2306
|
+
STORED_AT + SESSION_REPLAY_ENDED_FINALIZE_GRACE_MS;
|
|
2307
|
+
|
|
2308
|
+
function endedTabRow(
|
|
2309
|
+
tabId: string,
|
|
2310
|
+
overrides: JSONObject = {},
|
|
2311
|
+
): JSONObject {
|
|
2312
|
+
return {
|
|
2313
|
+
sessionId: "s-1",
|
|
2314
|
+
tabId: tabId,
|
|
2315
|
+
tabHasFinalChunk: 1,
|
|
2316
|
+
tabFinalChunkEndUnixMs: FINAL_END,
|
|
2317
|
+
tabLastChunkStartUnixMs: FINAL_END - 16000,
|
|
2318
|
+
tabMaxChunkIndex: 1,
|
|
2319
|
+
tabLastChunkStoredAtUnixMs: STORED_AT,
|
|
2320
|
+
...overrides,
|
|
2321
|
+
};
|
|
2322
|
+
}
|
|
2323
|
+
|
|
2324
|
+
function liveTabRow(
|
|
2325
|
+
tabId: string,
|
|
2326
|
+
overrides: JSONObject = {},
|
|
2327
|
+
): JSONObject {
|
|
2328
|
+
return {
|
|
2329
|
+
sessionId: "s-1",
|
|
2330
|
+
tabId: tabId,
|
|
2331
|
+
tabHasFinalChunk: 0,
|
|
2332
|
+
tabFinalChunkEndUnixMs: 0,
|
|
2333
|
+
tabLastChunkStartUnixMs: FINAL_END,
|
|
2334
|
+
tabMaxChunkIndex: 1,
|
|
2335
|
+
tabLastChunkStoredAtUnixMs: STORED_AT,
|
|
2336
|
+
...overrides,
|
|
2337
|
+
};
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2340
|
+
async function manifestAt(
|
|
2341
|
+
nowUnixMs: number | undefined,
|
|
2342
|
+
): Promise<SessionReplayManifest> {
|
|
2343
|
+
return SessionReplayReadService.getManifest({
|
|
2344
|
+
header: header({ isFinalized: false }),
|
|
2345
|
+
projectId: projectId,
|
|
2346
|
+
rumApplicationId: rumApplicationId,
|
|
2347
|
+
sessionId: "s-1",
|
|
2348
|
+
...(nowUnixMs !== undefined && { nowUnixMs: nowUnixMs }),
|
|
2349
|
+
});
|
|
2350
|
+
}
|
|
2351
|
+
|
|
2352
|
+
function mockManifestThenFacts(facts: Array<JSONObject>): void {
|
|
2353
|
+
chunkQuerySpy
|
|
2354
|
+
.mockResolvedValueOnce(fakeResultSet(chunkRows) as never)
|
|
2355
|
+
.mockResolvedValueOnce(fakeResultSet(facts) as never);
|
|
2356
|
+
}
|
|
2357
|
+
|
|
2358
|
+
test("an unfinalized session whose every tab sent its final chunk is ended once the grace has passed, read pinned to the authorized application", async () => {
|
|
2359
|
+
mockManifestThenFacts([
|
|
2360
|
+
endedTabRow("tab-1"),
|
|
2361
|
+
endedTabRow("tab-2", {
|
|
2362
|
+
tabLastChunkStartUnixMs: FINAL_END + 4,
|
|
2363
|
+
}),
|
|
2364
|
+
]);
|
|
2365
|
+
|
|
2366
|
+
const manifest: SessionReplayManifest = await manifestAt(GRACE_PASSED);
|
|
2367
|
+
|
|
2368
|
+
expect(chunkQuerySpy).toHaveBeenCalledTimes(2);
|
|
2369
|
+
|
|
2370
|
+
/* The manifest statement goes first and is unchanged. */
|
|
2371
|
+
const manifestQuery: string = statementOf(chunkQuerySpy, 0).query;
|
|
2372
|
+
expect(manifestQuery).toContain("LIMIT 1 BY tabId, chunkIndex");
|
|
2373
|
+
expect(manifestQuery).not.toContain("tabHasFinalChunk");
|
|
2374
|
+
|
|
2375
|
+
const ended: Statement = statementOf(chunkQuerySpy, 1);
|
|
2376
|
+
expect(ended.query).toContain("GROUP BY sessionId, tabId");
|
|
2377
|
+
expect(ended.query).toMatch(
|
|
2378
|
+
/AND sessionId IN \(\{p\d+:Array\(String\)\}\)/,
|
|
2379
|
+
);
|
|
2380
|
+
expect(ended.query).toMatch(/AND rumApplicationId = \{p\d+:String\}/);
|
|
2381
|
+
expect(ended.query).toContain(
|
|
2382
|
+
"toFloat64(max(chunkIndex)) AS tabMaxChunkIndex",
|
|
2383
|
+
);
|
|
2384
|
+
expect(ended.query).toContain(
|
|
2385
|
+
"toFloat64(max(version)) AS tabLastChunkStoredAtUnixMs",
|
|
2386
|
+
);
|
|
2387
|
+
expect(boundValues(ended)).toContainEqual(["s-1"]);
|
|
2388
|
+
expect(boundValues(ended)).toContain(rumApplicationId.toString());
|
|
2389
|
+
expect(boundValues(ended)).toContain(projectId.toString());
|
|
2390
|
+
|
|
2391
|
+
expect(manifest.header.isFinalized).toBe(false);
|
|
2392
|
+
expect(manifest.header.hasRecordingEnded).toBe(true);
|
|
2393
|
+
/* The live reconciliation still happens alongside it. */
|
|
2394
|
+
expect(manifest.header.durationMs).toBe(150000);
|
|
2395
|
+
expect(manifest.header.chunkCount).toBe(3);
|
|
2396
|
+
});
|
|
2397
|
+
|
|
2398
|
+
/*
|
|
2399
|
+
* The player's Live pill must not switch off while a multi-page
|
|
2400
|
+
* app's next page is still registering its first chunk.
|
|
2401
|
+
*/
|
|
2402
|
+
test.each([
|
|
2403
|
+
["stored just now", 0, false],
|
|
2404
|
+
[
|
|
2405
|
+
"stored 1 ms short of the grace",
|
|
2406
|
+
SESSION_REPLAY_ENDED_FINALIZE_GRACE_MS - 1,
|
|
2407
|
+
false,
|
|
2408
|
+
],
|
|
2409
|
+
[
|
|
2410
|
+
"stored exactly the grace ago",
|
|
2411
|
+
SESSION_REPLAY_ENDED_FINALIZE_GRACE_MS,
|
|
2412
|
+
true,
|
|
2413
|
+
],
|
|
2414
|
+
] as Array<[string, number, boolean]>)(
|
|
2415
|
+
"a single sealed tab %s (%i ms old): hasRecordingEnded %s",
|
|
2416
|
+
async (_label: string, ageMs: number, expected: boolean) => {
|
|
2417
|
+
mockManifestThenFacts([endedTabRow("tab-1")]);
|
|
2418
|
+
|
|
2419
|
+
const manifest: SessionReplayManifest = await manifestAt(
|
|
2420
|
+
STORED_AT + ageMs,
|
|
2421
|
+
);
|
|
2422
|
+
|
|
2423
|
+
expect(manifest.header.hasRecordingEnded).toBe(expected);
|
|
2424
|
+
},
|
|
2425
|
+
);
|
|
2426
|
+
|
|
2427
|
+
test("a capped tab with no final chunk is ended after the grace, not before", async () => {
|
|
2428
|
+
const capped: JSONObject = liveTabRow("tab-1", {
|
|
2429
|
+
tabMaxChunkIndex: MAX_SESSION_REPLAY_CHUNKS_PER_SESSION - 1,
|
|
2430
|
+
});
|
|
2431
|
+
|
|
2432
|
+
mockManifestThenFacts([capped]);
|
|
2433
|
+
expect(
|
|
2434
|
+
(await manifestAt(GRACE_PASSED - 1)).header.hasRecordingEnded,
|
|
2435
|
+
).toBe(false);
|
|
2436
|
+
|
|
2437
|
+
mockManifestThenFacts([capped]);
|
|
2438
|
+
expect((await manifestAt(GRACE_PASSED)).header.hasRecordingEnded).toBe(
|
|
2439
|
+
true,
|
|
2440
|
+
);
|
|
2441
|
+
});
|
|
2442
|
+
|
|
2443
|
+
test("without an injected clock the manifest measures the grace against Date.now()", async () => {
|
|
2444
|
+
const nowSpy: jest.SpyInstance = jest
|
|
2445
|
+
.spyOn(Date, "now")
|
|
2446
|
+
.mockReturnValue(GRACE_PASSED - 1);
|
|
2447
|
+
|
|
2448
|
+
mockManifestThenFacts([endedTabRow("tab-1")]);
|
|
2449
|
+
expect((await manifestAt(undefined)).header.hasRecordingEnded).toBe(
|
|
2450
|
+
false,
|
|
2451
|
+
);
|
|
2452
|
+
|
|
2453
|
+
nowSpy.mockReturnValue(GRACE_PASSED);
|
|
2454
|
+
|
|
2455
|
+
mockManifestThenFacts([endedTabRow("tab-1")]);
|
|
2456
|
+
expect((await manifestAt(undefined)).header.hasRecordingEnded).toBe(
|
|
2457
|
+
true,
|
|
2458
|
+
);
|
|
2459
|
+
});
|
|
2460
|
+
|
|
2461
|
+
test("one tab still recording keeps the header live", async () => {
|
|
2462
|
+
mockManifestThenFacts([endedTabRow("tab-1"), liveTabRow("tab-2")]);
|
|
2463
|
+
|
|
2464
|
+
const manifest: SessionReplayManifest = await manifestAt(GRACE_PASSED);
|
|
2465
|
+
|
|
2466
|
+
expect(manifest.header.hasRecordingEnded).toBe(false);
|
|
2467
|
+
});
|
|
2468
|
+
|
|
2469
|
+
test("a tab that kept recording past the tolerance after its final chunk keeps the header live", async () => {
|
|
2470
|
+
mockManifestThenFacts([
|
|
2471
|
+
endedTabRow("tab-1", {
|
|
2472
|
+
tabLastChunkStartUnixMs:
|
|
2473
|
+
FINAL_END + SESSION_REPLAY_ENDED_TRAILING_CHUNK_TOLERANCE_MS + 1,
|
|
2474
|
+
}),
|
|
2475
|
+
]);
|
|
2476
|
+
|
|
2477
|
+
const manifest: SessionReplayManifest = await manifestAt(GRACE_PASSED);
|
|
2478
|
+
|
|
2479
|
+
expect(manifest.header.hasRecordingEnded).toBe(false);
|
|
2480
|
+
});
|
|
2481
|
+
|
|
2482
|
+
test("a finalized header runs no second read and is still returned untouched", async () => {
|
|
2483
|
+
chunkQuerySpy.mockResolvedValue(fakeResultSet(chunkRows) as never);
|
|
2484
|
+
|
|
2485
|
+
const finalized: SessionReplaySessionHeader = header({
|
|
2486
|
+
isFinalized: true,
|
|
2487
|
+
durationMs: 150000,
|
|
2488
|
+
});
|
|
2489
|
+
|
|
2490
|
+
const manifest: SessionReplayManifest =
|
|
2491
|
+
await SessionReplayReadService.getManifest({
|
|
2492
|
+
header: finalized,
|
|
2493
|
+
projectId: projectId,
|
|
2494
|
+
rumApplicationId: rumApplicationId,
|
|
2495
|
+
sessionId: "s-1",
|
|
2496
|
+
nowUnixMs: GRACE_PASSED,
|
|
2497
|
+
});
|
|
2498
|
+
|
|
2499
|
+
expect(chunkQuerySpy).toHaveBeenCalledTimes(1);
|
|
2500
|
+
expect(manifest.header).toBe(finalized);
|
|
2501
|
+
expect(manifest.header.hasRecordingEnded).toBe(false);
|
|
2502
|
+
});
|
|
2503
|
+
|
|
2504
|
+
test("a failed ended read answers 'not ended' and never fails the manifest", async () => {
|
|
2505
|
+
const warnSpy: jest.SpyInstance = jest
|
|
2506
|
+
.spyOn(logger, "warn")
|
|
2507
|
+
.mockImplementation((): void => {
|
|
2508
|
+
return;
|
|
2509
|
+
});
|
|
2510
|
+
|
|
2511
|
+
chunkQuerySpy
|
|
2512
|
+
.mockResolvedValueOnce(fakeResultSet(chunkRows) as never)
|
|
2513
|
+
.mockRejectedValueOnce(new Error("clickhouse down") as never);
|
|
2514
|
+
|
|
2515
|
+
const manifest: SessionReplayManifest = await manifestAt(GRACE_PASSED);
|
|
2516
|
+
|
|
2517
|
+
expect(manifest.tabs).toHaveLength(2);
|
|
2518
|
+
expect(manifest.header.hasRecordingEnded).toBe(false);
|
|
2519
|
+
expect(manifest.header.chunkCount).toBe(3);
|
|
2520
|
+
expect(warnSpy).toHaveBeenCalled();
|
|
2521
|
+
});
|
|
2522
|
+
|
|
2523
|
+
test("a failed manifest read still fails, whatever the ended read answered", async () => {
|
|
2524
|
+
chunkQuerySpy
|
|
2525
|
+
.mockRejectedValueOnce(new Error("manifest down") as never)
|
|
2526
|
+
.mockResolvedValueOnce(
|
|
2527
|
+
fakeResultSet([endedTabRow("tab-1")]) as never,
|
|
2528
|
+
);
|
|
2529
|
+
|
|
2530
|
+
await expect(manifestAt(GRACE_PASSED)).rejects.toThrow("manifest down");
|
|
2531
|
+
});
|
|
2532
|
+
|
|
2533
|
+
test("an unfinalized header with no chunk rows is not ended and keeps its fields", async () => {
|
|
2534
|
+
const provisional: SessionReplaySessionHeader = header({
|
|
2535
|
+
isFinalized: false,
|
|
2536
|
+
});
|
|
2537
|
+
|
|
2538
|
+
const manifest: SessionReplayManifest =
|
|
2539
|
+
await SessionReplayReadService.getManifest({
|
|
2540
|
+
header: provisional,
|
|
2541
|
+
projectId: projectId,
|
|
2542
|
+
rumApplicationId: rumApplicationId,
|
|
2543
|
+
sessionId: "s-1",
|
|
2544
|
+
nowUnixMs: GRACE_PASSED,
|
|
2545
|
+
});
|
|
2546
|
+
|
|
2547
|
+
expect(manifest.tabs).toHaveLength(0);
|
|
2548
|
+
expect(manifest.header).toEqual({
|
|
2549
|
+
...provisional,
|
|
2550
|
+
hasRecordingEnded: false,
|
|
2551
|
+
});
|
|
2552
|
+
});
|
|
2553
|
+
|
|
2554
|
+
/*
|
|
2555
|
+
* The list badge and the player's Live pill are two reads of one
|
|
2556
|
+
* helper. Fed the same chunk facts at the same server "now", they
|
|
2557
|
+
* must give the same answer, at every edge of the rule.
|
|
2558
|
+
*/
|
|
2559
|
+
describe("the list and the manifest agree", () => {
|
|
2560
|
+
const scenarios: Array<[string, Array<JSONObject>, number, boolean]> = [
|
|
2561
|
+
[
|
|
2562
|
+
"a sealed tab inside the grace",
|
|
2563
|
+
[endedTabRow("tab-1")],
|
|
2564
|
+
GRACE_PASSED - 1,
|
|
2565
|
+
false,
|
|
2566
|
+
],
|
|
2567
|
+
[
|
|
2568
|
+
"a sealed tab at the grace",
|
|
2569
|
+
[endedTabRow("tab-1")],
|
|
2570
|
+
GRACE_PASSED,
|
|
2571
|
+
true,
|
|
2572
|
+
],
|
|
2573
|
+
[
|
|
2574
|
+
"a capped non-final tab inside the grace",
|
|
2575
|
+
[
|
|
2576
|
+
liveTabRow("tab-1", {
|
|
2577
|
+
tabMaxChunkIndex: MAX_SESSION_REPLAY_CHUNKS_PER_SESSION - 1,
|
|
2578
|
+
}),
|
|
2579
|
+
],
|
|
2580
|
+
GRACE_PASSED - 1,
|
|
2581
|
+
false,
|
|
2582
|
+
],
|
|
2583
|
+
[
|
|
2584
|
+
"a capped non-final tab at the grace",
|
|
2585
|
+
[
|
|
2586
|
+
liveTabRow("tab-1", {
|
|
2587
|
+
tabMaxChunkIndex: MAX_SESSION_REPLAY_CHUNKS_PER_SESSION - 1,
|
|
2588
|
+
}),
|
|
2589
|
+
],
|
|
2590
|
+
GRACE_PASSED,
|
|
2591
|
+
true,
|
|
2592
|
+
],
|
|
2593
|
+
[
|
|
2594
|
+
"a non-final tab one index short of the cap",
|
|
2595
|
+
[
|
|
2596
|
+
liveTabRow("tab-1", {
|
|
2597
|
+
tabMaxChunkIndex: MAX_SESSION_REPLAY_CHUNKS_PER_SESSION - 2,
|
|
2598
|
+
}),
|
|
2599
|
+
],
|
|
2600
|
+
GRACE_PASSED,
|
|
2601
|
+
false,
|
|
2602
|
+
],
|
|
2603
|
+
[
|
|
2604
|
+
"an old sealed tab next to a freshly sealed one",
|
|
2605
|
+
[
|
|
2606
|
+
endedTabRow("tab-1", {
|
|
2607
|
+
tabLastChunkStoredAtUnixMs: STORED_AT - 10 * 60 * 1000,
|
|
2608
|
+
}),
|
|
2609
|
+
endedTabRow("tab-2"),
|
|
2610
|
+
],
|
|
2611
|
+
GRACE_PASSED - 1,
|
|
2612
|
+
false,
|
|
2613
|
+
],
|
|
2614
|
+
[
|
|
2615
|
+
"a sealed tab next to a live one",
|
|
2616
|
+
[endedTabRow("tab-1"), liveTabRow("tab-2")],
|
|
2617
|
+
GRACE_PASSED,
|
|
2618
|
+
false,
|
|
2619
|
+
],
|
|
2620
|
+
];
|
|
2621
|
+
|
|
2622
|
+
test.each(scenarios)(
|
|
2623
|
+
"%s",
|
|
2624
|
+
async (
|
|
2625
|
+
_label: string,
|
|
2626
|
+
facts: Array<JSONObject>,
|
|
2627
|
+
nowUnixMs: number,
|
|
2628
|
+
expected: boolean,
|
|
2629
|
+
) => {
|
|
2630
|
+
/*
|
|
2631
|
+
* Answer each chunk-table statement by what it is, so the
|
|
2632
|
+
* list's single follow-up and the manifest's pair can share
|
|
2633
|
+
* one mock regardless of order.
|
|
2634
|
+
*/
|
|
2635
|
+
chunkQuerySpy.mockImplementation(
|
|
2636
|
+
async (statement: Statement): Promise<unknown> => {
|
|
2637
|
+
return fakeResultSet(
|
|
2638
|
+
statement.query.includes("tabHasFinalChunk")
|
|
2639
|
+
? facts
|
|
2640
|
+
: chunkRows,
|
|
2641
|
+
);
|
|
2642
|
+
},
|
|
2643
|
+
);
|
|
2644
|
+
headerQuerySpy.mockResolvedValue(
|
|
2645
|
+
fakeResultSet([
|
|
2646
|
+
{
|
|
2647
|
+
sessionId: "s-1",
|
|
2648
|
+
applicationId: rumApplicationId.toString(),
|
|
2649
|
+
aggStartTime: FINAL_END - 60000,
|
|
2650
|
+
aggEndTime: FINAL_END,
|
|
2651
|
+
aggIsFinalized: 0,
|
|
2652
|
+
aggSealedReason: "",
|
|
2653
|
+
},
|
|
2654
|
+
]) as never,
|
|
2655
|
+
);
|
|
2656
|
+
|
|
2657
|
+
const list: SessionReplayListResult =
|
|
2658
|
+
await SessionReplayReadService.listSessions(
|
|
2659
|
+
listRequest({ nowUnixMs: nowUnixMs }),
|
|
2660
|
+
);
|
|
2661
|
+
const manifest: SessionReplayManifest = await manifestAt(nowUnixMs);
|
|
2662
|
+
|
|
2663
|
+
expect(list.sessions).toHaveLength(1);
|
|
2664
|
+
expect(list.sessions[0]!.hasRecordingEnded).toBe(expected);
|
|
2665
|
+
expect(manifest.header.hasRecordingEnded).toBe(expected);
|
|
2666
|
+
},
|
|
2667
|
+
);
|
|
2668
|
+
});
|
|
2669
|
+
});
|
|
1463
2670
|
});
|
|
1464
2671
|
|
|
1465
2672
|
describe("getChunks", () => {
|
|
@@ -1660,6 +2867,234 @@ describe("SessionReplayReadService statements", () => {
|
|
|
1660
2867
|
expect(boundValues(headers)).toContain("s-9");
|
|
1661
2868
|
});
|
|
1662
2869
|
|
|
2870
|
+
test("binds both live-instance and finalized-header RUM matches to the exception application", async () => {
|
|
2871
|
+
const primaryEntityId: ObjectID = ObjectID.generate();
|
|
2872
|
+
|
|
2873
|
+
exceptionQuerySpy.mockResolvedValue(
|
|
2874
|
+
fakeResultSet([{ sessionId: "scoped-live-session" }]) as never,
|
|
2875
|
+
);
|
|
2876
|
+
|
|
2877
|
+
await SessionReplayReadService.getSessionsForException({
|
|
2878
|
+
projectId: projectId,
|
|
2879
|
+
exceptionFingerprint: "shared-fingerprint",
|
|
2880
|
+
primaryEntityId: primaryEntityId,
|
|
2881
|
+
primaryEntityType: ServiceType.RealUserMonitor,
|
|
2882
|
+
accessibleRumApplicationIds: null,
|
|
2883
|
+
limit: 5,
|
|
2884
|
+
});
|
|
2885
|
+
|
|
2886
|
+
const instances: Statement = statementOf(exceptionQuerySpy);
|
|
2887
|
+
expect(instances.query).toContain("AND primaryEntityId = ");
|
|
2888
|
+
expect(instances.query).toContain("AND primaryEntityType = ");
|
|
2889
|
+
expect(boundValues(instances)).toContain(primaryEntityId.toString());
|
|
2890
|
+
expect(boundValues(instances)).toContain(ServiceType.RealUserMonitor);
|
|
2891
|
+
|
|
2892
|
+
const headers: Statement = statementOf(headerQuerySpy);
|
|
2893
|
+
const where: string = whereSection(headers.query);
|
|
2894
|
+
expect(where).toMatch(/AND rumApplicationId = \{p\d+:String\}/);
|
|
2895
|
+
expect(where).toContain("hasAny(exceptionFingerprints");
|
|
2896
|
+
expect(where).toContain("OR sessionId IN (");
|
|
2897
|
+
expect(headers.query).toContain(
|
|
2898
|
+
"HAVING (hasAny(aggExceptionFingerprints",
|
|
2899
|
+
);
|
|
2900
|
+
expect(boundValues(headers)).toContain(primaryEntityId.toString());
|
|
2901
|
+
expect(boundValues(headers)).toContainEqual(["scoped-live-session"]);
|
|
2902
|
+
});
|
|
2903
|
+
|
|
2904
|
+
test("a scoped backend exception admits only instance-proven sessions", async () => {
|
|
2905
|
+
const primaryEntityId: ObjectID = ObjectID.generate();
|
|
2906
|
+
|
|
2907
|
+
exceptionQuerySpy.mockResolvedValue(
|
|
2908
|
+
fakeResultSet([{ sessionId: "backend-session" }]) as never,
|
|
2909
|
+
);
|
|
2910
|
+
headerQuerySpy.mockResolvedValue(
|
|
2911
|
+
fakeResultSet([
|
|
2912
|
+
{
|
|
2913
|
+
sessionId: "backend-session",
|
|
2914
|
+
applicationId: rumApplicationId.toString(),
|
|
2915
|
+
aggStartTime: "2026-08-14 10:00:00.000",
|
|
2916
|
+
aggEndTime: "2026-08-14 10:10:00.000",
|
|
2917
|
+
matchedApplicationCount: 1,
|
|
2918
|
+
},
|
|
2919
|
+
]) as never,
|
|
2920
|
+
);
|
|
2921
|
+
|
|
2922
|
+
const sessions: Array<SessionReplayExceptionSession> =
|
|
2923
|
+
await SessionReplayReadService.getSessionsForException({
|
|
2924
|
+
projectId: projectId,
|
|
2925
|
+
exceptionFingerprint: "shared-fingerprint",
|
|
2926
|
+
primaryEntityId: primaryEntityId,
|
|
2927
|
+
primaryEntityType: ServiceType.OpenTelemetry,
|
|
2928
|
+
accessibleRumApplicationIds: null,
|
|
2929
|
+
limit: 5,
|
|
2930
|
+
});
|
|
2931
|
+
|
|
2932
|
+
expect(sessions).toHaveLength(1);
|
|
2933
|
+
expect(sessions[0]?.sessionId).toBe("backend-session");
|
|
2934
|
+
expect(sessions[0]?.rumApplicationId).toBe(rumApplicationId.toString());
|
|
2935
|
+
|
|
2936
|
+
const instances: Statement = statementOf(exceptionQuerySpy);
|
|
2937
|
+
expect(instances.query).toContain("AND primaryEntityId = ");
|
|
2938
|
+
expect(instances.query).toContain(
|
|
2939
|
+
"ifNull(primaryEntityType, '') = '' OR primaryEntityType = ",
|
|
2940
|
+
);
|
|
2941
|
+
expect(boundValues(instances)).toContain(ServiceType.OpenTelemetry);
|
|
2942
|
+
|
|
2943
|
+
const headers: Statement = statementOf(headerQuerySpy);
|
|
2944
|
+
expect(whereSection(headers.query)).toContain("sessionId IN (");
|
|
2945
|
+
expect(headers.query).not.toContain("hasAny(exceptionFingerprints");
|
|
2946
|
+
expect(headers.query).not.toContain("hasAny(aggExceptionFingerprints");
|
|
2947
|
+
expect(boundValues(headers)).toContainEqual(["backend-session"]);
|
|
2948
|
+
});
|
|
2949
|
+
|
|
2950
|
+
test("an id-only legacy scope uses the same conservative instance and ambiguity guards", async () => {
|
|
2951
|
+
const primaryEntityId: ObjectID = ObjectID.generate();
|
|
2952
|
+
|
|
2953
|
+
exceptionQuerySpy.mockResolvedValue(
|
|
2954
|
+
fakeResultSet([{ sessionId: "legacy-session" }]) as never,
|
|
2955
|
+
);
|
|
2956
|
+
headerQuerySpy.mockResolvedValue(
|
|
2957
|
+
fakeResultSet([
|
|
2958
|
+
{
|
|
2959
|
+
sessionId: "legacy-session",
|
|
2960
|
+
applicationId: rumApplicationId.toString(),
|
|
2961
|
+
aggStartTime: "2026-08-14 10:00:00.000",
|
|
2962
|
+
aggEndTime: "2026-08-14 10:10:00.000",
|
|
2963
|
+
matchedApplicationCount: 1,
|
|
2964
|
+
},
|
|
2965
|
+
]) as never,
|
|
2966
|
+
);
|
|
2967
|
+
|
|
2968
|
+
const sessions: Array<SessionReplayExceptionSession> =
|
|
2969
|
+
await SessionReplayReadService.getSessionsForException({
|
|
2970
|
+
projectId: projectId,
|
|
2971
|
+
exceptionFingerprint: "shared-fingerprint",
|
|
2972
|
+
primaryEntityId: primaryEntityId,
|
|
2973
|
+
accessibleRumApplicationIds: null,
|
|
2974
|
+
limit: 5,
|
|
2975
|
+
});
|
|
2976
|
+
|
|
2977
|
+
expect(sessions).toHaveLength(1);
|
|
2978
|
+
|
|
2979
|
+
const instances: Statement = statementOf(exceptionQuerySpy);
|
|
2980
|
+
expect(instances.query).toContain("AND primaryEntityId = ");
|
|
2981
|
+
expect(instances.query).not.toContain("AND primaryEntityType = ");
|
|
2982
|
+
|
|
2983
|
+
const headers: Statement = statementOf(headerQuerySpy);
|
|
2984
|
+
expect(headers.query).not.toContain("hasAny(exceptionFingerprints");
|
|
2985
|
+
expect(headers.query).toContain(
|
|
2986
|
+
"count() OVER (PARTITION BY sessionId) AS matchedApplicationCount",
|
|
2987
|
+
);
|
|
2988
|
+
expect(headers.query).toContain("QUALIFY matchedApplicationCount = 1");
|
|
2989
|
+
});
|
|
2990
|
+
|
|
2991
|
+
test("rejects a scoped backend session id shared by two applications before authorization and limit", async () => {
|
|
2992
|
+
const primaryEntityId: ObjectID = ObjectID.generate();
|
|
2993
|
+
const accessibleApplicationId: ObjectID = ObjectID.generate();
|
|
2994
|
+
const startTime: Date = new Date("2026-08-14T10:00:00.000Z");
|
|
2995
|
+
const endTime: Date = new Date("2026-08-14T11:00:00.000Z");
|
|
2996
|
+
|
|
2997
|
+
exceptionQuerySpy.mockResolvedValue(
|
|
2998
|
+
fakeResultSet([{ sessionId: "colliding-session" }]) as never,
|
|
2999
|
+
);
|
|
3000
|
+
/* The selected pre-limit window count also makes the mapper fail closed. */
|
|
3001
|
+
headerQuerySpy.mockResolvedValue(
|
|
3002
|
+
fakeResultSet([
|
|
3003
|
+
{
|
|
3004
|
+
sessionId: "colliding-session",
|
|
3005
|
+
applicationId: accessibleApplicationId.toString(),
|
|
3006
|
+
aggStartTime: startTime.getTime(),
|
|
3007
|
+
matchedApplicationCount: 2,
|
|
3008
|
+
},
|
|
3009
|
+
]) as never,
|
|
3010
|
+
);
|
|
3011
|
+
|
|
3012
|
+
const sessions: Array<SessionReplayExceptionSession> =
|
|
3013
|
+
await SessionReplayReadService.getSessionsForException({
|
|
3014
|
+
projectId: projectId,
|
|
3015
|
+
exceptionFingerprint: "shared-fingerprint",
|
|
3016
|
+
primaryEntityId: primaryEntityId,
|
|
3017
|
+
primaryEntityType: ServiceType.OpenTelemetry,
|
|
3018
|
+
accessibleRumApplicationIds: [accessibleApplicationId],
|
|
3019
|
+
startTime: startTime,
|
|
3020
|
+
endTime: endTime,
|
|
3021
|
+
limit: 1,
|
|
3022
|
+
});
|
|
3023
|
+
|
|
3024
|
+
expect(sessions).toEqual([]);
|
|
3025
|
+
|
|
3026
|
+
const headers: Statement = statementOf(headerQuerySpy);
|
|
3027
|
+
const where: string = whereSection(headers.query);
|
|
3028
|
+
const qualifyIndex: number = headers.query.indexOf(
|
|
3029
|
+
"QUALIFY matchedApplicationCount = 1",
|
|
3030
|
+
);
|
|
3031
|
+
const authorizationIndex: number = headers.query.indexOf(
|
|
3032
|
+
"rumApplicationId IN (",
|
|
3033
|
+
);
|
|
3034
|
+
const orderIndex: number = headers.query.indexOf("ORDER BY");
|
|
3035
|
+
const limitIndex: number = headers.query.indexOf("LIMIT");
|
|
3036
|
+
|
|
3037
|
+
expect(headers.query).toContain(
|
|
3038
|
+
"count() OVER (PARTITION BY sessionId) AS matchedApplicationCount",
|
|
3039
|
+
);
|
|
3040
|
+
expect(where).not.toContain("rumApplicationId IN (");
|
|
3041
|
+
expect(where).not.toContain("startTime >= ");
|
|
3042
|
+
expect(headers.query).toMatch(/aggStartTime >= \{p\d+:Int64\}/);
|
|
3043
|
+
expect(headers.query).toMatch(/aggStartTime <= \{p\d+:Int64\}/);
|
|
3044
|
+
expect(qualifyIndex).toBeGreaterThan(headers.query.indexOf("GROUP BY"));
|
|
3045
|
+
expect(authorizationIndex).toBeGreaterThan(qualifyIndex);
|
|
3046
|
+
expect(orderIndex).toBeGreaterThan(authorizationIndex);
|
|
3047
|
+
expect(limitIndex).toBeGreaterThan(orderIndex);
|
|
3048
|
+
expect(boundValues(headers)).toContainEqual([
|
|
3049
|
+
accessibleApplicationId.toString(),
|
|
3050
|
+
]);
|
|
3051
|
+
expect(boundValues(headers)).toContain(startTime.getTime());
|
|
3052
|
+
expect(boundValues(headers)).toContain(endTime.getTime());
|
|
3053
|
+
});
|
|
3054
|
+
|
|
3055
|
+
test("a scoped backend exception with no proven session fails closed", async () => {
|
|
3056
|
+
exceptionQuerySpy.mockResolvedValue(fakeResultSet([]) as never);
|
|
3057
|
+
|
|
3058
|
+
const sessions: Array<SessionReplayExceptionSession> =
|
|
3059
|
+
await SessionReplayReadService.getSessionsForException({
|
|
3060
|
+
projectId: projectId,
|
|
3061
|
+
exceptionFingerprint: "fp-1",
|
|
3062
|
+
primaryEntityId: ObjectID.generate(),
|
|
3063
|
+
primaryEntityType: ServiceType.OpenTelemetry,
|
|
3064
|
+
accessibleRumApplicationIds: null,
|
|
3065
|
+
limit: 5,
|
|
3066
|
+
});
|
|
3067
|
+
|
|
3068
|
+
expect(sessions).toEqual([]);
|
|
3069
|
+
expect(headerQuerySpy).not.toHaveBeenCalled();
|
|
3070
|
+
});
|
|
3071
|
+
|
|
3072
|
+
test("rejects a type without an id or an invalid entity type before querying", async () => {
|
|
3073
|
+
await expect(
|
|
3074
|
+
SessionReplayReadService.getSessionsForException({
|
|
3075
|
+
projectId: projectId,
|
|
3076
|
+
exceptionFingerprint: "fp-1",
|
|
3077
|
+
primaryEntityType: ServiceType.OpenTelemetry,
|
|
3078
|
+
accessibleRumApplicationIds: null,
|
|
3079
|
+
limit: 5,
|
|
3080
|
+
}),
|
|
3081
|
+
).rejects.toBeInstanceOf(BadDataException);
|
|
3082
|
+
|
|
3083
|
+
await expect(
|
|
3084
|
+
SessionReplayReadService.getSessionsForException({
|
|
3085
|
+
projectId: projectId,
|
|
3086
|
+
exceptionFingerprint: "fp-1",
|
|
3087
|
+
primaryEntityId: ObjectID.generate(),
|
|
3088
|
+
primaryEntityType: "invalid" as ServiceType,
|
|
3089
|
+
accessibleRumApplicationIds: null,
|
|
3090
|
+
limit: 5,
|
|
3091
|
+
}),
|
|
3092
|
+
).rejects.toBeInstanceOf(BadDataException);
|
|
3093
|
+
|
|
3094
|
+
expect(exceptionQuerySpy).not.toHaveBeenCalled();
|
|
3095
|
+
expect(headerQuerySpy).not.toHaveBeenCalled();
|
|
3096
|
+
});
|
|
3097
|
+
|
|
1663
3098
|
test("a pinned session the instance table has never seen falls back to the fingerprint alone", async () => {
|
|
1664
3099
|
/* The session exists, but it never threw this exception. */
|
|
1665
3100
|
exceptionQuerySpy.mockResolvedValue(fakeResultSet([]) as never);
|