@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.
Files changed (687) hide show
  1. package/Models/DatabaseModels/Index.ts +4 -0
  2. package/Models/DatabaseModels/RumApplication.ts +1 -1
  3. package/Models/DatabaseModels/SecurityEventConnection.ts +457 -0
  4. package/Models/DatabaseModels/SecurityEventConnectionRun.ts +263 -0
  5. package/Models/DatabaseModels/TelemetryIngestionKey.ts +2 -2
  6. package/Server/API/AIInvestigationAPI.ts +484 -24
  7. package/Server/API/GoogleSecOpsConnectionAPI.ts +249 -8
  8. package/Server/API/SecurityEventConnectionAPI.ts +250 -0
  9. package/Server/API/TelemetryAPI.ts +402 -265
  10. package/Server/Infrastructure/Postgres/SchemaMigrations/1792700000000-AddSecurityEventConnections.ts +99 -0
  11. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  12. package/Server/Middleware/TelemetryIngest.ts +101 -6
  13. package/Server/Services/ExceptionAggregationService.ts +7 -16
  14. package/Server/Services/GoogleSecOpsConnectionService.ts +15 -0
  15. package/Server/Services/Index.ts +4 -0
  16. package/Server/Services/InventoryItemService.ts +28 -0
  17. package/Server/Services/LogAggregationService.ts +17 -23
  18. package/Server/Services/MetricAggregationService.ts +4 -14
  19. package/Server/Services/SecurityEventConnectionRunService.ts +10 -0
  20. package/Server/Services/SecurityEventConnectionService.ts +538 -0
  21. package/Server/Services/TraceAggregationService.ts +77 -28
  22. package/Server/Types/AnalyticsDatabase/ModelPermission.ts +9 -0
  23. package/Server/Utils/AI/Chat/ObservabilityAssistant.ts +13 -1
  24. package/Server/Utils/AI/SRE/AIInvestigationEngine.ts +15 -1
  25. package/Server/Utils/AI/SRE/InvestigationEvidence.ts +658 -0
  26. package/Server/Utils/AI/SRE/InvestigationReferences.ts +324 -0
  27. package/Server/Utils/AI/Toolbox/Index.ts +38 -0
  28. package/Server/Utils/AI/Toolbox/TimelineTools.ts +9 -2
  29. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +80 -0
  30. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +5 -3
  31. package/Server/Utils/SecurityEvent/Connectors/AwsSecurityHub/AwsSecurityHubClient.ts +1108 -0
  32. package/Server/Utils/SecurityEvent/Connectors/AwsSecurityHub/AwsSecurityHubConnector.ts +732 -0
  33. package/Server/Utils/SecurityEvent/Connectors/ConnectorPlatformHealth.ts +495 -0
  34. package/Server/Utils/SecurityEvent/Connectors/CrowdStrikeFalcon/CrowdStrikeFalconClient.ts +689 -0
  35. package/Server/Utils/SecurityEvent/Connectors/CrowdStrikeFalcon/CrowdStrikeFalconConnector.ts +767 -0
  36. package/Server/Utils/SecurityEvent/Connectors/ElasticSecurity/ElasticSecurityClient.ts +571 -0
  37. package/Server/Utils/SecurityEvent/Connectors/ElasticSecurity/ElasticSecurityConnector.ts +827 -0
  38. package/Server/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdr/MicrosoftDefenderXdrClient.ts +610 -0
  39. package/Server/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdr/MicrosoftDefenderXdrConnector.ts +549 -0
  40. package/Server/Utils/SecurityEvent/Connectors/MicrosoftSentinel/MicrosoftSentinelClient.ts +830 -0
  41. package/Server/Utils/SecurityEvent/Connectors/MicrosoftSentinel/MicrosoftSentinelConnector.ts +684 -0
  42. package/Server/Utils/SecurityEvent/Connectors/Okta/OktaClient.ts +487 -0
  43. package/Server/Utils/SecurityEvent/Connectors/Okta/OktaConnector.ts +778 -0
  44. package/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionPoller.ts +1002 -0
  45. package/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionRunExecutor.ts +556 -0
  46. package/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionTester.ts +275 -0
  47. package/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectorRegistry.ts +53 -0
  48. package/Server/Utils/SecurityEvent/Connectors/Splunk/SplunkClient.ts +709 -0
  49. package/Server/Utils/SecurityEvent/Connectors/Splunk/SplunkConnector.ts +596 -0
  50. package/Server/Utils/SecurityEvent/Connectors/Types.ts +191 -0
  51. package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.ts +353 -65
  52. package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsConnectionTester.ts +777 -0
  53. package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.ts +747 -148
  54. package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsRunExecutor.ts +48 -2
  55. package/Server/Utils/SecurityEvent/SecurityEventDedupe.ts +81 -0
  56. package/Server/Utils/SessionReplay/SessionReplayReadService.ts +741 -40
  57. package/Server/Utils/StartServer.ts +10 -0
  58. package/Server/Utils/Telemetry/EntityRegistry.ts +8 -0
  59. package/Server/Utils/Telemetry/KubernetesResourceAttributes.ts +108 -0
  60. package/Server/Utils/Telemetry/ResourceEntityFilter.ts +174 -93
  61. package/Server/Utils/Telemetry/ResourceFacetPlanner.ts +251 -0
  62. package/Server/Utils/Telemetry/ResourceFacetResolver.ts +248 -413
  63. package/Server/Utils/Telemetry/ServiceDependencyDiscovery.ts +668 -0
  64. package/Server/Utils/Telemetry/TraceContextPropagation.ts +348 -0
  65. package/Server/Utils/Telemetry.ts +18 -0
  66. package/Tests/App/Dashboard/AIInvestigationHeaderStatus.test.tsx +413 -0
  67. package/Tests/App/Dashboard/AIRootCauseFeedItem.test.tsx +285 -0
  68. package/Tests/App/Dashboard/AffectedResourcesDisplay.test.tsx +242 -0
  69. package/Tests/App/Dashboard/AlertEpisodeViewFields.test.tsx +601 -15
  70. package/Tests/App/Dashboard/ChangeAlertStateAIHeader.test.tsx +835 -0
  71. package/Tests/App/Dashboard/CloudResourceTelemetryScope.test.ts +41 -0
  72. package/Tests/App/Dashboard/ConnectionTestModal.test.tsx +604 -0
  73. package/Tests/App/Dashboard/ConnectorTestReportView.test.tsx +738 -0
  74. package/Tests/App/Dashboard/DashboardLazyPages.test.tsx +153 -6
  75. package/Tests/App/Dashboard/EntityDetailPanel.test.tsx +165 -0
  76. package/Tests/App/Dashboard/EpisodeChangeState.test.tsx +1069 -0
  77. package/Tests/App/Dashboard/EpisodeMembersCard.test.tsx +772 -0
  78. package/Tests/App/Dashboard/EventOverviewPages.test.tsx +2354 -0
  79. package/Tests/App/Dashboard/EventOverviewSkeleton.test.tsx +210 -0
  80. package/Tests/App/Dashboard/EventStatBar.test.tsx +392 -0
  81. package/Tests/App/Dashboard/EventStatusPanel.test.tsx +247 -0
  82. package/Tests/App/Dashboard/ExceptionAIAssistance.test.tsx +606 -0
  83. package/Tests/App/Dashboard/ExceptionBreadcrumbTimeline.test.tsx +273 -0
  84. package/Tests/App/Dashboard/ExceptionDetailSideMenu.test.tsx +176 -0
  85. package/Tests/App/Dashboard/ExceptionOverviewCards.test.tsx +366 -0
  86. package/Tests/App/Dashboard/ExceptionSignalPages.test.tsx +371 -0
  87. package/Tests/App/Dashboard/ExceptionStackFrameViewer.test.tsx +440 -0
  88. package/Tests/App/Dashboard/ExceptionSummary.test.tsx +381 -0
  89. package/Tests/App/Dashboard/ExceptionTriage.test.tsx +306 -0
  90. package/Tests/App/Dashboard/ExceptionsResourceFacets.test.tsx +604 -0
  91. package/Tests/App/Dashboard/ExceptionsViewerEntityChips.test.tsx +706 -0
  92. package/Tests/App/Dashboard/FeedOrdering.test.ts +32 -0
  93. package/Tests/App/Dashboard/GoogleSecOpsConnectionForm.test.tsx +838 -0
  94. package/Tests/App/Dashboard/GoogleSecOpsConnectionsErrors.test.tsx +18 -0
  95. package/Tests/App/Dashboard/GoogleSecOpsDiagnostics.test.tsx +297 -20
  96. package/Tests/App/Dashboard/IncidentEpisodeViewFields.test.tsx +618 -13
  97. package/Tests/App/Dashboard/InfrastructureExplorer.test.tsx +254 -207
  98. package/Tests/App/Dashboard/InfrastructureGraph.test.tsx +303 -135
  99. package/Tests/App/Dashboard/InvestigationEvidenceList.test.tsx +1251 -0
  100. package/Tests/App/Dashboard/InvestigationFeedRefresh.test.tsx +303 -0
  101. package/Tests/App/Dashboard/InvestigationPanel.test.tsx +876 -8
  102. package/Tests/App/Dashboard/InvestigationPanelStatus.test.tsx +35 -0
  103. package/Tests/App/Dashboard/InvestigationReferenceLink.test.tsx +177 -0
  104. package/Tests/App/Dashboard/InvestigationReportView.test.tsx +1295 -0
  105. package/Tests/App/Dashboard/KubernetesImageReferenceView.test.tsx +114 -0
  106. package/Tests/App/Dashboard/KubernetesLogsTabChips.test.tsx +285 -0
  107. package/Tests/App/Dashboard/LogsEntityChipNames.test.tsx +316 -0
  108. package/Tests/App/Dashboard/LogsResourceFacetChips.test.tsx +361 -0
  109. package/Tests/App/Dashboard/ProfileTableEntityNames.test.tsx +432 -0
  110. package/Tests/App/Dashboard/ProfileViewEntityName.test.tsx +250 -0
  111. package/Tests/App/Dashboard/ResourceArchiveSettings.test.tsx +10 -0
  112. package/Tests/App/Dashboard/ResourceLogsTabsBareLayout.test.tsx +700 -0
  113. package/Tests/App/Dashboard/ResourcePageEntityNameProps.test.tsx +803 -0
  114. package/Tests/App/Dashboard/RumSessionReplaySideMenu.test.tsx +11 -1
  115. package/Tests/App/Dashboard/ScheduledMaintenanceChangeState.test.tsx +1026 -0
  116. package/Tests/App/Dashboard/ScheduledMaintenanceFeedRefresh.test.tsx +436 -0
  117. package/Tests/App/Dashboard/ScheduledMaintenanceOverviewPage.test.tsx +1195 -0
  118. package/Tests/App/Dashboard/SecurityEventConnectionFormModal.test.tsx +1022 -0
  119. package/Tests/App/Dashboard/SecurityEventConnectionsTable.test.tsx +905 -0
  120. package/Tests/App/Dashboard/SecurityEventsSideMenu.test.tsx +247 -0
  121. package/Tests/App/Dashboard/ServiceMapGraph.test.tsx +375 -274
  122. package/Tests/App/Dashboard/SessionReplayAuditTable.test.tsx +410 -0
  123. package/Tests/App/Dashboard/TelemetryResourceRetentionSettings.test.tsx +774 -0
  124. package/Tests/App/Dashboard/TopErrorsPanelResourceNames.test.tsx +316 -0
  125. package/Tests/App/Dashboard/TopologyDataLoading.test.tsx +30 -0
  126. package/Tests/App/Dashboard/TopologyPageNavigation.test.tsx +88 -24
  127. package/Tests/App/Dashboard/TraceExplorer.test.tsx +1590 -0
  128. package/Tests/App/Dashboard/TraceWaterfall.test.tsx +488 -0
  129. package/Tests/App/Dashboard/TracesAnalyticsSeriesLabels.test.tsx +367 -0
  130. package/Tests/App/Dashboard/TracesEntityNames.test.tsx +585 -0
  131. package/Tests/App/Dashboard/TracesResourceFacets.test.tsx +613 -0
  132. package/Tests/App/Dashboard/UseServiceNames.test.tsx +359 -0
  133. package/Tests/Models/DatabaseModels/DomainRoleTierCoverage.test.ts +7 -0
  134. package/Tests/Models/InventoryItemNaming.test.ts +132 -16
  135. package/Tests/Models/SecurityDomainAccessControl.test.ts +61 -17
  136. package/Tests/Server/API/AIInvestigationAPI.test.ts +611 -2
  137. package/Tests/Server/API/AIInvestigationEvidenceAPI.test.ts +1098 -0
  138. package/Tests/Server/API/GoogleSecOpsConnectionAPI.test.ts +409 -0
  139. package/Tests/Server/API/SecurityEventConnectionAPI.test.ts +856 -0
  140. package/Tests/Server/API/SessionReplayAPI.test.ts +528 -1
  141. package/Tests/Server/API/TelemetryFacetsAPI.test.ts +810 -0
  142. package/Tests/Server/Infrastructure/E2EClickHouseFixtureAccess.test.ts +240 -0
  143. package/Tests/Server/Middleware/TelemetryIngestBrowserKey.test.ts +229 -0
  144. package/Tests/Server/Services/InventoryItemDisplayName.test.ts +66 -0
  145. package/Tests/Server/Services/OpenTelemetryResourceRetentionMemo.test.ts +28 -1
  146. package/Tests/Server/Services/SecurityEventConnectionService.test.ts +1233 -0
  147. package/Tests/Server/Services/TelemetryIngestionKeyValidation.test.ts +53 -0
  148. package/Tests/Server/Services/TelemetryResourceFacetCounting.test.ts +218 -0
  149. package/Tests/Server/Services/TelemetryResourceFacetFilters.test.ts +155 -1
  150. package/Tests/Server/Services/TraceAggregationExceptionScope.test.ts +142 -0
  151. package/Tests/Server/Services/TraceAnalyticsResourceDisplayNames.test.ts +85 -24
  152. package/Tests/Server/Types/Database/Permissions/SecurityRoleAccess.test.ts +33 -0
  153. package/Tests/Server/Utils/AI/InvestigationEvidence.test.ts +1395 -0
  154. package/Tests/Server/Utils/AI/InvestigationReferences.test.ts +616 -0
  155. package/Tests/Server/Utils/AI/TimelineTools.test.ts +119 -0
  156. package/Tests/Server/Utils/AI/Toolbox/AIToolbox.test.ts +151 -0
  157. package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +65 -0
  158. package/Tests/Server/Utils/AnalyticsDatabase/StatementGeneratorExceptionScope.test.ts +224 -0
  159. package/Tests/Server/Utils/LoggerCore.test.ts +597 -0
  160. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorPerSeriesFanout.test.ts +55 -2
  161. package/Tests/Server/Utils/Monitor/MonitorTemplateUtil.test.ts +1109 -0
  162. package/Tests/Server/Utils/SecurityEvent/Connectors/AwsSecurityHub/AwsSecurityHubClient.test.ts +1240 -0
  163. package/Tests/Server/Utils/SecurityEvent/Connectors/AwsSecurityHub/AwsSecurityHubConnector.test.ts +1853 -0
  164. package/Tests/Server/Utils/SecurityEvent/Connectors/ConnectorPlatformHealth.test.ts +1129 -0
  165. package/Tests/Server/Utils/SecurityEvent/Connectors/CrowdStrikeFalcon/CrowdStrikeFalconClient.test.ts +1066 -0
  166. package/Tests/Server/Utils/SecurityEvent/Connectors/CrowdStrikeFalcon/CrowdStrikeFalconConnector.test.ts +1568 -0
  167. package/Tests/Server/Utils/SecurityEvent/Connectors/ElasticSecurity/ElasticSecurityClient.test.ts +930 -0
  168. package/Tests/Server/Utils/SecurityEvent/Connectors/ElasticSecurity/ElasticSecurityConnector.test.ts +1409 -0
  169. package/Tests/Server/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdr/MicrosoftDefenderXdrClient.test.ts +763 -0
  170. package/Tests/Server/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdr/MicrosoftDefenderXdrConnector.test.ts +1296 -0
  171. package/Tests/Server/Utils/SecurityEvent/Connectors/MicrosoftSentinel/MicrosoftSentinelClient.test.ts +1102 -0
  172. package/Tests/Server/Utils/SecurityEvent/Connectors/MicrosoftSentinel/MicrosoftSentinelConnector.test.ts +1174 -0
  173. package/Tests/Server/Utils/SecurityEvent/Connectors/Okta/OktaClient.test.ts +1032 -0
  174. package/Tests/Server/Utils/SecurityEvent/Connectors/Okta/OktaConnector.test.ts +1558 -0
  175. package/Tests/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionPoller.test.ts +3041 -0
  176. package/Tests/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionRunExecutor.test.ts +1168 -0
  177. package/Tests/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionTester.test.ts +906 -0
  178. package/Tests/Server/Utils/SecurityEvent/Connectors/Splunk/SplunkClient.test.ts +1068 -0
  179. package/Tests/Server/Utils/SecurityEvent/Connectors/Splunk/SplunkConnector.test.ts +1004 -0
  180. package/Tests/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsConnectionTester.test.ts +784 -0
  181. package/Tests/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsRunExecutor.test.ts +113 -1
  182. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsClient.test.ts +5 -1
  183. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsDiagnostics.test.ts +93 -13
  184. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsHttpIntegration.test.ts +23 -1
  185. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPoller.test.ts +25 -3
  186. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerFailureTaxonomy.test.ts +22 -8
  187. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerHardening.test.ts +82 -16
  188. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsSearchDetections.test.ts +730 -0
  189. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsThreePassPoller.test.ts +1577 -0
  190. package/Tests/Server/Utils/SessionReplay/SessionReplayOriginAllowlist.test.ts +41 -0
  191. package/Tests/Server/Utils/SessionReplay/SessionReplayReadServiceQueries.test.ts +1453 -18
  192. package/Tests/Server/Utils/Telemetry/EntityRegistry.test.ts +711 -0
  193. package/Tests/Server/Utils/Telemetry/KubernetesResourceAttributes.test.ts +162 -0
  194. package/Tests/Server/Utils/Telemetry/ResourceEntityFilter.test.ts +460 -13
  195. package/Tests/Server/Utils/Telemetry/ResourceFacetPlanner.test.ts +678 -0
  196. package/Tests/Server/Utils/Telemetry/ResourceFacetResolver.test.ts +868 -0
  197. package/Tests/Server/Utils/Telemetry/ServiceDependencyDiscovery.test.ts +525 -0
  198. package/Tests/Server/Utils/Telemetry/TraceContextPropagation.test.ts +420 -0
  199. package/Tests/Types/OnCallDutyPolicy/LayerUtilCorePaths.test.ts +1014 -0
  200. package/Tests/Types/OnCallDutyPolicy/LayerUtilExhaustiveRotation.test.ts +9 -6
  201. package/Tests/Types/Rules/RuleCriteriaFieldRegistry.test.ts +276 -0
  202. package/Tests/Types/Rum/SessionReplayApiContracts.test.ts +73 -0
  203. package/Tests/Types/Rum/SessionReplayTransportContracts.test.ts +95 -0
  204. package/Tests/Types/SecurityEvent/Connectors/ConnectorDiagnostics.test.ts +82 -0
  205. package/Tests/Types/SecurityEvent/Connectors/SecurityEventConnectorCatalog.test.ts +314 -0
  206. package/Tests/Types/Telemetry/ExceptionSpanScope.test.ts +89 -0
  207. package/Tests/Types/Telemetry/LlmConventions.test.ts +495 -0
  208. package/Tests/Types/Telemetry/ResourceEntityFacet.test.ts +96 -0
  209. package/Tests/Types/Telemetry/ResourceFacetCatalog.test.ts +304 -0
  210. package/Tests/UI/Components/ActiveFilterChipsLockedFilters.test.tsx +357 -0
  211. package/Tests/UI/Components/Card.test.tsx +312 -1
  212. package/Tests/UI/Components/CardModelDetailHeaderLayout.test.tsx +253 -0
  213. package/Tests/UI/Components/CardModelDetailRefresher.test.tsx +342 -0
  214. package/Tests/UI/Components/Checkbox.test.tsx +79 -0
  215. package/Tests/UI/Components/CopyTextButton.test.tsx +142 -0
  216. package/Tests/UI/Components/DetailCompactStyle.test.tsx +621 -0
  217. package/Tests/UI/Components/ErrorBoundary.test.tsx +60 -0
  218. package/Tests/UI/Components/FacetSections.test.tsx +581 -0
  219. package/Tests/UI/Components/FacetSidebarControls.test.tsx +812 -0
  220. package/Tests/UI/Components/FacetVisibility.test.ts +429 -0
  221. package/Tests/UI/Components/LockedFilterActions.test.tsx +396 -0
  222. package/Tests/UI/Components/LockedFilterChip.test.tsx +651 -0
  223. package/Tests/UI/Components/LogsEntityNames.test.ts +1769 -0
  224. package/Tests/UI/Components/LogsEntityNamesComponents.test.tsx +696 -0
  225. package/Tests/UI/Components/LogsFacetSidebarHiddenFacets.test.tsx +600 -0
  226. package/Tests/UI/Components/LogsViewerEntityNames.test.tsx +713 -0
  227. package/Tests/UI/Components/MarkdownInlineReferences.test.tsx +2079 -0
  228. package/Tests/UI/Components/MemberRoleAssignment.test.tsx +446 -0
  229. package/Tests/UI/Components/ModelDetailRefetch.test.tsx +397 -0
  230. package/Tests/UI/Components/ModelListRefetch.test.tsx +233 -0
  231. package/Tests/UI/Components/ModelPageModelChange.test.tsx +409 -0
  232. package/Tests/UI/Components/PaginationIntegration.test.tsx +25 -0
  233. package/Tests/UI/Components/ResourceFacetConfigs.test.ts +172 -0
  234. package/Tests/UI/Components/SavedViewsSidebarIntegration.test.tsx +32 -14
  235. package/Tests/UI/Components/TableSortAndMobileColumns.test.tsx +44 -1
  236. package/Tests/UI/Components/TelemetryActiveFilterChipsLockedFilters.test.tsx +304 -0
  237. package/Tests/UI/Components/TelemetryFacetSidebarHiddenFacets.test.tsx +718 -0
  238. package/Tests/UI/Monitor/EvaluationLogList.test.tsx +303 -0
  239. package/Tests/UI/Monitor/MonitorSummarySnapshotRender.test.tsx +55 -1
  240. package/Tests/UI/Rum/FidelityNotices.test.ts +22 -0
  241. package/Tests/UI/Rum/RecordingHealthCard.test.tsx +120 -136
  242. package/Tests/UI/Rum/RecordingHealthDashboard.test.tsx +988 -0
  243. package/Tests/UI/Rum/ReplayCard.test.tsx +32 -0
  244. package/Tests/UI/Rum/ReplayCorrelationPanel.test.tsx +709 -12
  245. package/Tests/UI/Rum/ReplayHeader.test.tsx +50 -1
  246. package/Tests/UI/Rum/ReplayPinControl.test.tsx +67 -10
  247. package/Tests/UI/Rum/ReplayPlayerChrome.test.tsx +41 -12
  248. package/Tests/UI/Rum/ReplayStage.test.tsx +663 -1
  249. package/Tests/UI/Rum/ReplayStageOverlays.test.tsx +173 -0
  250. package/Tests/UI/Rum/ReplayUi.test.tsx +189 -2
  251. package/Tests/UI/Rum/ReplayUserSessionsMenu.test.tsx +91 -0
  252. package/Tests/UI/Rum/SessionReplayEmptyState.test.tsx +1 -1
  253. package/Tests/UI/Rum/SessionReplaySettingsPolicyLoading.test.tsx +585 -0
  254. package/Tests/UI/Rum/SessionReplayTable.test.tsx +844 -51
  255. package/Tests/UI/Rum/SessionReplayUsersTable.test.tsx +29 -1
  256. package/Tests/UI/Telemetry/TelemetryDetailPanel.test.tsx +288 -0
  257. package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +31 -1
  258. package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +6 -0
  259. package/Tests/UI/Utils/Clipboard.test.ts +154 -0
  260. package/Tests/UI/Utils/Navigation.test.ts +24 -0
  261. package/Tests/UI/Utils/Telemetry/TelemetryEntityNames.test.ts +2205 -0
  262. package/Tests/UI/Utils/Telemetry/UseTelemetryEntityNames.test.tsx +844 -0
  263. package/Tests/Utils/AI/InvestigationReport.test.ts +2430 -0
  264. package/Tests/Utils/APIOutgoingRequestTracer.test.ts +137 -0
  265. package/Tests/Utils/Rum/SessionReplayHealth.test.ts +884 -0
  266. package/Tests/Utils/Rum/SessionReplayHealthDiagnosis.test.ts +40 -0
  267. package/Tests/Utils/Rum/SessionReplayRecordingEnded.test.ts +619 -0
  268. package/Tests/Utils/SecurityEvent/Connectors/AwsSecurityHubNormalizer.test.ts +631 -0
  269. package/Tests/Utils/SecurityEvent/Connectors/CrowdStrikeFalconNormalizer.test.ts +479 -0
  270. package/Tests/Utils/SecurityEvent/Connectors/ElasticSecurityNormalizer.test.ts +507 -0
  271. package/Tests/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdrNormalizer.test.ts +686 -0
  272. package/Tests/Utils/SecurityEvent/Connectors/MicrosoftSentinelNormalizer.test.ts +474 -0
  273. package/Tests/Utils/SecurityEvent/Connectors/OktaNormalizer.test.ts +736 -0
  274. package/Tests/Utils/SecurityEvent/Connectors/SplunkNormalizer.test.ts +407 -0
  275. package/Tests/Utils/SecurityEvent/GoogleSecOpsAlertNormalizer.test.ts +129 -0
  276. package/Tests/Utils/Telemetry/CrossSignalScope.test.ts +57 -1
  277. package/Tests/Utils/Telemetry/EntityRelationship.test.ts +36 -0
  278. package/Tests/Utils/Telemetry/LockedFilterSearch.test.ts +600 -0
  279. package/Tests/Utils/Telemetry/NetworkHost.test.ts +48 -0
  280. package/Tests/Utils/Telemetry/OriginAllowList.test.ts +154 -0
  281. package/Tests/Utils/Traces/CriticalPath.test.ts +25 -0
  282. package/Types/AI/AIChatTypes.ts +8 -0
  283. package/Types/AI/InvestigationEvidence.ts +96 -0
  284. package/Types/Monitor/MonitorEvaluationSummary.ts +5 -0
  285. package/Types/OnCallDutyPolicy/Layer.ts +12 -0
  286. package/Types/Permission.ts +2 -2
  287. package/Types/Rules/RuleCriteriaFieldRegistry.ts +12 -0
  288. package/Types/Rum/SessionReplay.ts +109 -0
  289. package/Types/Rum/SessionReplayApi.ts +31 -0
  290. package/Types/SecurityEvent/Connectors/ConnectorDiagnostics.ts +111 -0
  291. package/Types/SecurityEvent/Connectors/SecurityEventConnectionDiagnostics.ts +87 -0
  292. package/Types/SecurityEvent/Connectors/SecurityEventConnectorCatalog.ts +492 -0
  293. package/Types/SecurityEvent/Connectors/SecurityEventConnectorProvider.ts +41 -0
  294. package/Types/SecurityEvent/GoogleSecOpsDiagnostics.ts +56 -1
  295. package/Types/SecurityEvent/GoogleSecOpsRegion.ts +34 -0
  296. package/Types/Telemetry/EntityType.ts +14 -0
  297. package/Types/Telemetry/ExceptionSpanScope.ts +83 -0
  298. package/Types/Telemetry/LockedFilterDetail.ts +46 -0
  299. package/Types/Telemetry/ResourceEntityFacet.ts +17 -10
  300. package/Types/Telemetry/ResourceFacetCatalog.ts +171 -0
  301. package/UI/Components/Card/Card.tsx +118 -83
  302. package/UI/Components/Checkbox/Checkbox.tsx +8 -5
  303. package/UI/Components/CopyTextButton/CopyTextButton.tsx +7 -1
  304. package/UI/Components/CustomFields/CustomFieldsDetail.tsx +4 -1
  305. package/UI/Components/Detail/Detail.tsx +137 -22
  306. package/UI/Components/ErrorBoundary.tsx +38 -0
  307. package/UI/Components/LogsViewer/LogsEntityNames.ts +912 -0
  308. package/UI/Components/LogsViewer/LogsViewer.tsx +156 -77
  309. package/UI/Components/LogsViewer/components/ActiveFilterChips.tsx +28 -11
  310. package/UI/Components/LogsViewer/components/FacetSection.tsx +66 -79
  311. package/UI/Components/LogsViewer/components/FacetValueRow.tsx +17 -2
  312. package/UI/Components/LogsViewer/components/LogDetailsPanel.tsx +45 -5
  313. package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +100 -19
  314. package/UI/Components/LogsViewer/components/LogsFacetSidebar.tsx +156 -39
  315. package/UI/Components/LogsViewer/components/LogsTable.tsx +30 -8
  316. package/UI/Components/LogsViewer/types.ts +6 -0
  317. package/UI/Components/Markdown.tsx/InlineReferences.tsx +628 -0
  318. package/UI/Components/Markdown.tsx/MarkdownViewer.tsx +385 -312
  319. package/UI/Components/MemberRoleAssignment/MemberRoleAssignment.tsx +50 -22
  320. package/UI/Components/ModelDetail/CardModelDetail.tsx +15 -0
  321. package/UI/Components/ModelDetail/ModelDetail.tsx +64 -19
  322. package/UI/Components/ModelList/ModelList.tsx +33 -8
  323. package/UI/Components/Page/ModelPage.tsx +119 -54
  324. package/UI/Components/Table/Table.tsx +8 -0
  325. package/UI/Components/Table/TableRow.tsx +13 -5
  326. package/UI/Components/TelemetryViewer/FacetVisibility.ts +229 -0
  327. package/UI/Components/TelemetryViewer/ResourceFacetConfigs.ts +46 -0
  328. package/UI/Components/TelemetryViewer/TelemetryViewer.tsx +11 -0
  329. package/UI/Components/TelemetryViewer/components/FacetSearchInput.tsx +79 -0
  330. package/UI/Components/TelemetryViewer/components/FacetSectionHeader.tsx +69 -0
  331. package/UI/Components/TelemetryViewer/components/FacetShowMoreButton.tsx +36 -0
  332. package/UI/Components/TelemetryViewer/components/HiddenFacetsFooter.tsx +88 -0
  333. package/UI/Components/TelemetryViewer/components/LockedFilterActions.tsx +205 -0
  334. package/UI/Components/TelemetryViewer/components/LockedFilterChip.tsx +496 -0
  335. package/UI/Components/TelemetryViewer/components/TelemetryActiveFilterChips.tsx +30 -10
  336. package/UI/Components/TelemetryViewer/components/TelemetryDetailPanel.tsx +203 -10
  337. package/UI/Components/TelemetryViewer/components/TelemetryFacetSection.tsx +60 -73
  338. package/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.tsx +94 -7
  339. package/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.tsx +17 -2
  340. package/UI/Components/TelemetryViewer/types.ts +19 -0
  341. package/UI/Components/TelemetryViewer/useFacetSearchExemptions.ts +158 -0
  342. package/UI/Components/TelemetryViewer/useFacetSectionSearch.ts +102 -0
  343. package/UI/Utils/Clipboard.ts +68 -2
  344. package/UI/Utils/Navigation.ts +17 -2
  345. package/UI/Utils/Telemetry/TelemetryEntityNames.ts +620 -0
  346. package/UI/Utils/Telemetry/UseTelemetryEntityNames.ts +151 -0
  347. package/Utils/AI/InvestigationReport.ts +1789 -0
  348. package/Utils/API.ts +90 -1
  349. package/Utils/Rum/SessionReplayHealth.ts +11 -0
  350. package/Utils/Rum/SessionReplayRecordingEnded.ts +134 -0
  351. package/Utils/SecurityEvent/Connectors/AwsSecurityHubNormalizer.ts +427 -0
  352. package/Utils/SecurityEvent/Connectors/CrowdStrikeFalconNormalizer.ts +197 -0
  353. package/Utils/SecurityEvent/Connectors/ElasticSecurityNormalizer.ts +464 -0
  354. package/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdrNormalizer.ts +567 -0
  355. package/Utils/SecurityEvent/Connectors/MicrosoftSentinelNormalizer.ts +299 -0
  356. package/Utils/SecurityEvent/Connectors/OktaNormalizer.ts +467 -0
  357. package/Utils/SecurityEvent/Connectors/SplunkNormalizer.ts +439 -0
  358. package/Utils/SecurityEvent/GoogleSecOpsAlertNormalizer.ts +124 -12
  359. package/Utils/Telemetry/CrossSignalScope.ts +22 -8
  360. package/Utils/Telemetry/EntityRelationship.ts +8 -0
  361. package/Utils/Telemetry/LockedFilterSearch.ts +412 -0
  362. package/Utils/Telemetry/NetworkHost.ts +43 -0
  363. package/Utils/Telemetry/OriginAllowList.ts +139 -4
  364. package/Utils/Traces/CriticalPath.ts +10 -4
  365. package/build/dist/Models/DatabaseModels/Index.js +4 -0
  366. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  367. package/build/dist/Models/DatabaseModels/RumApplication.js +1 -1
  368. package/build/dist/Models/DatabaseModels/RumApplication.js.map +1 -1
  369. package/build/dist/Models/DatabaseModels/SecurityEventConnection.js +490 -0
  370. package/build/dist/Models/DatabaseModels/SecurityEventConnection.js.map +1 -0
  371. package/build/dist/Models/DatabaseModels/SecurityEventConnectionRun.js +287 -0
  372. package/build/dist/Models/DatabaseModels/SecurityEventConnectionRun.js.map +1 -0
  373. package/build/dist/Models/DatabaseModels/TelemetryIngestionKey.js +2 -2
  374. package/build/dist/Models/DatabaseModels/TelemetryIngestionKey.js.map +1 -1
  375. package/build/dist/Server/API/AIInvestigationAPI.js +336 -27
  376. package/build/dist/Server/API/AIInvestigationAPI.js.map +1 -1
  377. package/build/dist/Server/API/GoogleSecOpsConnectionAPI.js +182 -7
  378. package/build/dist/Server/API/GoogleSecOpsConnectionAPI.js.map +1 -1
  379. package/build/dist/Server/API/SecurityEventConnectionAPI.js +171 -0
  380. package/build/dist/Server/API/SecurityEventConnectionAPI.js.map +1 -0
  381. package/build/dist/Server/API/TelemetryAPI.js +248 -170
  382. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  383. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1792700000000-AddSecurityEventConnections.js +40 -0
  384. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1792700000000-AddSecurityEventConnections.js.map +1 -0
  385. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  386. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  387. package/build/dist/Server/Middleware/TelemetryIngest.js +57 -4
  388. package/build/dist/Server/Middleware/TelemetryIngest.js.map +1 -1
  389. package/build/dist/Server/Services/ExceptionAggregationService.js +7 -16
  390. package/build/dist/Server/Services/ExceptionAggregationService.js.map +1 -1
  391. package/build/dist/Server/Services/GoogleSecOpsConnectionService.js +8 -0
  392. package/build/dist/Server/Services/GoogleSecOpsConnectionService.js.map +1 -1
  393. package/build/dist/Server/Services/Index.js +4 -0
  394. package/build/dist/Server/Services/Index.js.map +1 -1
  395. package/build/dist/Server/Services/InventoryItemService.js +22 -0
  396. package/build/dist/Server/Services/InventoryItemService.js.map +1 -1
  397. package/build/dist/Server/Services/LogAggregationService.js +8 -17
  398. package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
  399. package/build/dist/Server/Services/MetricAggregationService.js +4 -14
  400. package/build/dist/Server/Services/MetricAggregationService.js.map +1 -1
  401. package/build/dist/Server/Services/SecurityEventConnectionRunService.js +9 -0
  402. package/build/dist/Server/Services/SecurityEventConnectionRunService.js.map +1 -0
  403. package/build/dist/Server/Services/SecurityEventConnectionService.js +356 -0
  404. package/build/dist/Server/Services/SecurityEventConnectionService.js.map +1 -0
  405. package/build/dist/Server/Services/TraceAggregationService.js +51 -25
  406. package/build/dist/Server/Services/TraceAggregationService.js.map +1 -1
  407. package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js +9 -0
  408. package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js.map +1 -1
  409. package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js +3 -0
  410. package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js.map +1 -1
  411. package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js +14 -7
  412. package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js.map +1 -1
  413. package/build/dist/Server/Utils/AI/SRE/InvestigationEvidence.js +468 -0
  414. package/build/dist/Server/Utils/AI/SRE/InvestigationEvidence.js.map +1 -0
  415. package/build/dist/Server/Utils/AI/SRE/InvestigationReferences.js +230 -0
  416. package/build/dist/Server/Utils/AI/SRE/InvestigationReferences.js.map +1 -0
  417. package/build/dist/Server/Utils/AI/Toolbox/Index.js +32 -0
  418. package/build/dist/Server/Utils/AI/Toolbox/Index.js.map +1 -1
  419. package/build/dist/Server/Utils/AI/Toolbox/TimelineTools.js +6 -2
  420. package/build/dist/Server/Utils/AI/Toolbox/TimelineTools.js.map +1 -1
  421. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +59 -0
  422. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
  423. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +5 -3
  424. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  425. package/build/dist/Server/Utils/SecurityEvent/Connectors/AwsSecurityHub/AwsSecurityHubClient.js +752 -0
  426. package/build/dist/Server/Utils/SecurityEvent/Connectors/AwsSecurityHub/AwsSecurityHubClient.js.map +1 -0
  427. package/build/dist/Server/Utils/SecurityEvent/Connectors/AwsSecurityHub/AwsSecurityHubConnector.js +493 -0
  428. package/build/dist/Server/Utils/SecurityEvent/Connectors/AwsSecurityHub/AwsSecurityHubConnector.js.map +1 -0
  429. package/build/dist/Server/Utils/SecurityEvent/Connectors/ConnectorPlatformHealth.js +352 -0
  430. package/build/dist/Server/Utils/SecurityEvent/Connectors/ConnectorPlatformHealth.js.map +1 -0
  431. package/build/dist/Server/Utils/SecurityEvent/Connectors/CrowdStrikeFalcon/CrowdStrikeFalconClient.js +405 -0
  432. package/build/dist/Server/Utils/SecurityEvent/Connectors/CrowdStrikeFalcon/CrowdStrikeFalconClient.js.map +1 -0
  433. package/build/dist/Server/Utils/SecurityEvent/Connectors/CrowdStrikeFalcon/CrowdStrikeFalconConnector.js +466 -0
  434. package/build/dist/Server/Utils/SecurityEvent/Connectors/CrowdStrikeFalcon/CrowdStrikeFalconConnector.js.map +1 -0
  435. package/build/dist/Server/Utils/SecurityEvent/Connectors/ElasticSecurity/ElasticSecurityClient.js +344 -0
  436. package/build/dist/Server/Utils/SecurityEvent/Connectors/ElasticSecurity/ElasticSecurityClient.js.map +1 -0
  437. package/build/dist/Server/Utils/SecurityEvent/Connectors/ElasticSecurity/ElasticSecurityConnector.js +511 -0
  438. package/build/dist/Server/Utils/SecurityEvent/Connectors/ElasticSecurity/ElasticSecurityConnector.js.map +1 -0
  439. package/build/dist/Server/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdr/MicrosoftDefenderXdrClient.js +406 -0
  440. package/build/dist/Server/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdr/MicrosoftDefenderXdrClient.js.map +1 -0
  441. package/build/dist/Server/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdr/MicrosoftDefenderXdrConnector.js +336 -0
  442. package/build/dist/Server/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdr/MicrosoftDefenderXdrConnector.js.map +1 -0
  443. package/build/dist/Server/Utils/SecurityEvent/Connectors/MicrosoftSentinel/MicrosoftSentinelClient.js +474 -0
  444. package/build/dist/Server/Utils/SecurityEvent/Connectors/MicrosoftSentinel/MicrosoftSentinelClient.js.map +1 -0
  445. package/build/dist/Server/Utils/SecurityEvent/Connectors/MicrosoftSentinel/MicrosoftSentinelConnector.js +417 -0
  446. package/build/dist/Server/Utils/SecurityEvent/Connectors/MicrosoftSentinel/MicrosoftSentinelConnector.js.map +1 -0
  447. package/build/dist/Server/Utils/SecurityEvent/Connectors/Okta/OktaClient.js +314 -0
  448. package/build/dist/Server/Utils/SecurityEvent/Connectors/Okta/OktaClient.js.map +1 -0
  449. package/build/dist/Server/Utils/SecurityEvent/Connectors/Okta/OktaConnector.js +494 -0
  450. package/build/dist/Server/Utils/SecurityEvent/Connectors/Okta/OktaConnector.js.map +1 -0
  451. package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionPoller.js +719 -0
  452. package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionPoller.js.map +1 -0
  453. package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionRunExecutor.js +404 -0
  454. package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionRunExecutor.js.map +1 -0
  455. package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionTester.js +209 -0
  456. package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionTester.js.map +1 -0
  457. package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectorRegistry.js +40 -0
  458. package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectorRegistry.js.map +1 -0
  459. package/build/dist/Server/Utils/SecurityEvent/Connectors/Splunk/SplunkClient.js +494 -0
  460. package/build/dist/Server/Utils/SecurityEvent/Connectors/Splunk/SplunkClient.js.map +1 -0
  461. package/build/dist/Server/Utils/SecurityEvent/Connectors/Splunk/SplunkConnector.js +379 -0
  462. package/build/dist/Server/Utils/SecurityEvent/Connectors/Splunk/SplunkConnector.js.map +1 -0
  463. package/build/dist/Server/Utils/SecurityEvent/Connectors/Types.js +27 -0
  464. package/build/dist/Server/Utils/SecurityEvent/Connectors/Types.js.map +1 -0
  465. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js +234 -61
  466. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js.map +1 -1
  467. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsConnectionTester.js +537 -0
  468. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsConnectionTester.js.map +1 -0
  469. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js +572 -115
  470. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js.map +1 -1
  471. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsRunExecutor.js +35 -1
  472. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsRunExecutor.js.map +1 -1
  473. package/build/dist/Server/Utils/SecurityEvent/SecurityEventDedupe.js +52 -0
  474. package/build/dist/Server/Utils/SecurityEvent/SecurityEventDedupe.js.map +1 -0
  475. package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js +521 -32
  476. package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js.map +1 -1
  477. package/build/dist/Server/Utils/StartServer.js +10 -0
  478. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  479. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +8 -0
  480. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
  481. package/build/dist/Server/Utils/Telemetry/KubernetesResourceAttributes.js +80 -0
  482. package/build/dist/Server/Utils/Telemetry/KubernetesResourceAttributes.js.map +1 -0
  483. package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js +122 -67
  484. package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js.map +1 -1
  485. package/build/dist/Server/Utils/Telemetry/ResourceFacetPlanner.js +146 -0
  486. package/build/dist/Server/Utils/Telemetry/ResourceFacetPlanner.js.map +1 -0
  487. package/build/dist/Server/Utils/Telemetry/ResourceFacetResolver.js +174 -248
  488. package/build/dist/Server/Utils/Telemetry/ResourceFacetResolver.js.map +1 -1
  489. package/build/dist/Server/Utils/Telemetry/ServiceDependencyDiscovery.js +492 -0
  490. package/build/dist/Server/Utils/Telemetry/ServiceDependencyDiscovery.js.map +1 -0
  491. package/build/dist/Server/Utils/Telemetry/TraceContextPropagation.js +259 -0
  492. package/build/dist/Server/Utils/Telemetry/TraceContextPropagation.js.map +1 -0
  493. package/build/dist/Server/Utils/Telemetry.js +11 -5
  494. package/build/dist/Server/Utils/Telemetry.js.map +1 -1
  495. package/build/dist/Types/AI/InvestigationEvidence.js +2 -0
  496. package/build/dist/Types/AI/InvestigationEvidence.js.map +1 -0
  497. package/build/dist/Types/OnCallDutyPolicy/Layer.js +11 -0
  498. package/build/dist/Types/OnCallDutyPolicy/Layer.js.map +1 -1
  499. package/build/dist/Types/Permission.js +2 -2
  500. package/build/dist/Types/Permission.js.map +1 -1
  501. package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js +9 -0
  502. package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js.map +1 -1
  503. package/build/dist/Types/Rum/SessionReplay.js +95 -0
  504. package/build/dist/Types/Rum/SessionReplay.js.map +1 -1
  505. package/build/dist/Types/Rum/SessionReplayApi.js.map +1 -1
  506. package/build/dist/Types/SecurityEvent/Connectors/ConnectorDiagnostics.js +18 -0
  507. package/build/dist/Types/SecurityEvent/Connectors/ConnectorDiagnostics.js.map +1 -0
  508. package/build/dist/Types/SecurityEvent/Connectors/SecurityEventConnectionDiagnostics.js +2 -0
  509. package/build/dist/Types/SecurityEvent/Connectors/SecurityEventConnectionDiagnostics.js.map +1 -0
  510. package/build/dist/Types/SecurityEvent/Connectors/SecurityEventConnectorCatalog.js +374 -0
  511. package/build/dist/Types/SecurityEvent/Connectors/SecurityEventConnectorCatalog.js.map +1 -0
  512. package/build/dist/Types/SecurityEvent/Connectors/SecurityEventConnectorProvider.js +35 -0
  513. package/build/dist/Types/SecurityEvent/Connectors/SecurityEventConnectorProvider.js.map +1 -0
  514. package/build/dist/Types/SecurityEvent/GoogleSecOpsRegion.js +35 -0
  515. package/build/dist/Types/SecurityEvent/GoogleSecOpsRegion.js.map +1 -0
  516. package/build/dist/Types/Telemetry/EntityType.js +14 -0
  517. package/build/dist/Types/Telemetry/EntityType.js.map +1 -1
  518. package/build/dist/Types/Telemetry/ExceptionSpanScope.js +59 -0
  519. package/build/dist/Types/Telemetry/ExceptionSpanScope.js.map +1 -0
  520. package/build/dist/Types/Telemetry/LockedFilterDetail.js +16 -0
  521. package/build/dist/Types/Telemetry/LockedFilterDetail.js.map +1 -0
  522. package/build/dist/Types/Telemetry/ResourceEntityFacet.js +16 -10
  523. package/build/dist/Types/Telemetry/ResourceEntityFacet.js.map +1 -1
  524. package/build/dist/Types/Telemetry/ResourceFacetCatalog.js +119 -0
  525. package/build/dist/Types/Telemetry/ResourceFacetCatalog.js.map +1 -0
  526. package/build/dist/UI/Components/Card/Card.js +34 -17
  527. package/build/dist/UI/Components/Card/Card.js.map +1 -1
  528. package/build/dist/UI/Components/Checkbox/Checkbox.js +6 -4
  529. package/build/dist/UI/Components/Checkbox/Checkbox.js.map +1 -1
  530. package/build/dist/UI/Components/CopyTextButton/CopyTextButton.js +5 -1
  531. package/build/dist/UI/Components/CopyTextButton/CopyTextButton.js.map +1 -1
  532. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js +1 -1
  533. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js.map +1 -1
  534. package/build/dist/UI/Components/Detail/Detail.js +63 -13
  535. package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
  536. package/build/dist/UI/Components/ErrorBoundary.js +34 -0
  537. package/build/dist/UI/Components/ErrorBoundary.js.map +1 -1
  538. package/build/dist/UI/Components/LogsViewer/LogsEntityNames.js +535 -0
  539. package/build/dist/UI/Components/LogsViewer/LogsEntityNames.js.map +1 -0
  540. package/build/dist/UI/Components/LogsViewer/LogsViewer.js +117 -57
  541. package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
  542. package/build/dist/UI/Components/LogsViewer/components/ActiveFilterChips.js +5 -7
  543. package/build/dist/UI/Components/LogsViewer/components/ActiveFilterChips.js.map +1 -1
  544. package/build/dist/UI/Components/LogsViewer/components/FacetSection.js +31 -44
  545. package/build/dist/UI/Components/LogsViewer/components/FacetSection.js.map +1 -1
  546. package/build/dist/UI/Components/LogsViewer/components/FacetValueRow.js +17 -3
  547. package/build/dist/UI/Components/LogsViewer/components/FacetValueRow.js.map +1 -1
  548. package/build/dist/UI/Components/LogsViewer/components/LogDetailsPanel.js +19 -3
  549. package/build/dist/UI/Components/LogsViewer/components/LogDetailsPanel.js.map +1 -1
  550. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +64 -18
  551. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
  552. package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js +100 -41
  553. package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js.map +1 -1
  554. package/build/dist/UI/Components/LogsViewer/components/LogsTable.js +13 -5
  555. package/build/dist/UI/Components/LogsViewer/components/LogsTable.js.map +1 -1
  556. package/build/dist/UI/Components/LogsViewer/types.js.map +1 -1
  557. package/build/dist/UI/Components/Markdown.tsx/InlineReferences.js +403 -0
  558. package/build/dist/UI/Components/Markdown.tsx/InlineReferences.js.map +1 -0
  559. package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js +197 -179
  560. package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js.map +1 -1
  561. package/build/dist/UI/Components/MemberRoleAssignment/MemberRoleAssignment.js +30 -21
  562. package/build/dist/UI/Components/MemberRoleAssignment/MemberRoleAssignment.js.map +1 -1
  563. package/build/dist/UI/Components/ModelDetail/CardModelDetail.js +12 -0
  564. package/build/dist/UI/Components/ModelDetail/CardModelDetail.js.map +1 -1
  565. package/build/dist/UI/Components/ModelDetail/ModelDetail.js +46 -12
  566. package/build/dist/UI/Components/ModelDetail/ModelDetail.js.map +1 -1
  567. package/build/dist/UI/Components/ModelList/ModelList.js +26 -7
  568. package/build/dist/UI/Components/ModelList/ModelList.js.map +1 -1
  569. package/build/dist/UI/Components/Page/ModelPage.js +62 -34
  570. package/build/dist/UI/Components/Page/ModelPage.js.map +1 -1
  571. package/build/dist/UI/Components/Table/Table.js +3 -1
  572. package/build/dist/UI/Components/Table/Table.js.map +1 -1
  573. package/build/dist/UI/Components/Table/TableRow.js +8 -1
  574. package/build/dist/UI/Components/Table/TableRow.js.map +1 -1
  575. package/build/dist/UI/Components/TelemetryViewer/FacetVisibility.js +111 -0
  576. package/build/dist/UI/Components/TelemetryViewer/FacetVisibility.js.map +1 -0
  577. package/build/dist/UI/Components/TelemetryViewer/ResourceFacetConfigs.js +22 -0
  578. package/build/dist/UI/Components/TelemetryViewer/ResourceFacetConfigs.js.map +1 -0
  579. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js +1 -1
  580. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js.map +1 -1
  581. package/build/dist/UI/Components/TelemetryViewer/components/FacetSearchInput.js +36 -0
  582. package/build/dist/UI/Components/TelemetryViewer/components/FacetSearchInput.js.map +1 -0
  583. package/build/dist/UI/Components/TelemetryViewer/components/FacetSectionHeader.js +21 -0
  584. package/build/dist/UI/Components/TelemetryViewer/components/FacetSectionHeader.js.map +1 -0
  585. package/build/dist/UI/Components/TelemetryViewer/components/FacetShowMoreButton.js +14 -0
  586. package/build/dist/UI/Components/TelemetryViewer/components/FacetShowMoreButton.js.map +1 -0
  587. package/build/dist/UI/Components/TelemetryViewer/components/HiddenFacetsFooter.js +27 -0
  588. package/build/dist/UI/Components/TelemetryViewer/components/HiddenFacetsFooter.js.map +1 -0
  589. package/build/dist/UI/Components/TelemetryViewer/components/LockedFilterActions.js +88 -0
  590. package/build/dist/UI/Components/TelemetryViewer/components/LockedFilterActions.js.map +1 -0
  591. package/build/dist/UI/Components/TelemetryViewer/components/LockedFilterChip.js +228 -0
  592. package/build/dist/UI/Components/TelemetryViewer/components/LockedFilterChip.js.map +1 -0
  593. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryActiveFilterChips.js +6 -6
  594. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryActiveFilterChips.js.map +1 -1
  595. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryDetailPanel.js +111 -12
  596. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryDetailPanel.js.map +1 -1
  597. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSection.js +27 -40
  598. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSection.js.map +1 -1
  599. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.js +70 -10
  600. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.js.map +1 -1
  601. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.js +17 -3
  602. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.js.map +1 -1
  603. package/build/dist/UI/Components/TelemetryViewer/types.js +0 -4
  604. package/build/dist/UI/Components/TelemetryViewer/types.js.map +1 -1
  605. package/build/dist/UI/Components/TelemetryViewer/useFacetSearchExemptions.js +70 -0
  606. package/build/dist/UI/Components/TelemetryViewer/useFacetSearchExemptions.js.map +1 -0
  607. package/build/dist/UI/Components/TelemetryViewer/useFacetSectionSearch.js +52 -0
  608. package/build/dist/UI/Components/TelemetryViewer/useFacetSectionSearch.js.map +1 -0
  609. package/build/dist/UI/Utils/Clipboard.js +59 -2
  610. package/build/dist/UI/Utils/Clipboard.js.map +1 -1
  611. package/build/dist/UI/Utils/Navigation.js +11 -2
  612. package/build/dist/UI/Utils/Navigation.js.map +1 -1
  613. package/build/dist/UI/Utils/Telemetry/TelemetryEntityNames.js +471 -0
  614. package/build/dist/UI/Utils/Telemetry/TelemetryEntityNames.js.map +1 -0
  615. package/build/dist/UI/Utils/Telemetry/UseTelemetryEntityNames.js +113 -0
  616. package/build/dist/UI/Utils/Telemetry/UseTelemetryEntityNames.js.map +1 -0
  617. package/build/dist/Utils/AI/InvestigationReport.js +1241 -0
  618. package/build/dist/Utils/AI/InvestigationReport.js.map +1 -0
  619. package/build/dist/Utils/API.js +25 -0
  620. package/build/dist/Utils/API.js.map +1 -1
  621. package/build/dist/Utils/Rum/SessionReplayHealth.js +10 -0
  622. package/build/dist/Utils/Rum/SessionReplayHealth.js.map +1 -1
  623. package/build/dist/Utils/Rum/SessionReplayRecordingEnded.js +54 -0
  624. package/build/dist/Utils/Rum/SessionReplayRecordingEnded.js.map +1 -0
  625. package/build/dist/Utils/SecurityEvent/Connectors/AwsSecurityHubNormalizer.js +311 -0
  626. package/build/dist/Utils/SecurityEvent/Connectors/AwsSecurityHubNormalizer.js.map +1 -0
  627. package/build/dist/Utils/SecurityEvent/Connectors/CrowdStrikeFalconNormalizer.js +150 -0
  628. package/build/dist/Utils/SecurityEvent/Connectors/CrowdStrikeFalconNormalizer.js.map +1 -0
  629. package/build/dist/Utils/SecurityEvent/Connectors/ElasticSecurityNormalizer.js +328 -0
  630. package/build/dist/Utils/SecurityEvent/Connectors/ElasticSecurityNormalizer.js.map +1 -0
  631. package/build/dist/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdrNormalizer.js +405 -0
  632. package/build/dist/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdrNormalizer.js.map +1 -0
  633. package/build/dist/Utils/SecurityEvent/Connectors/MicrosoftSentinelNormalizer.js +212 -0
  634. package/build/dist/Utils/SecurityEvent/Connectors/MicrosoftSentinelNormalizer.js.map +1 -0
  635. package/build/dist/Utils/SecurityEvent/Connectors/OktaNormalizer.js +351 -0
  636. package/build/dist/Utils/SecurityEvent/Connectors/OktaNormalizer.js.map +1 -0
  637. package/build/dist/Utils/SecurityEvent/Connectors/SplunkNormalizer.js +348 -0
  638. package/build/dist/Utils/SecurityEvent/Connectors/SplunkNormalizer.js.map +1 -0
  639. package/build/dist/Utils/SecurityEvent/GoogleSecOpsAlertNormalizer.js +103 -10
  640. package/build/dist/Utils/SecurityEvent/GoogleSecOpsAlertNormalizer.js.map +1 -1
  641. package/build/dist/Utils/Telemetry/CrossSignalScope.js +16 -5
  642. package/build/dist/Utils/Telemetry/CrossSignalScope.js.map +1 -1
  643. package/build/dist/Utils/Telemetry/EntityRelationship.js +7 -0
  644. package/build/dist/Utils/Telemetry/EntityRelationship.js.map +1 -1
  645. package/build/dist/Utils/Telemetry/LockedFilterSearch.js +277 -0
  646. package/build/dist/Utils/Telemetry/LockedFilterSearch.js.map +1 -0
  647. package/build/dist/Utils/Telemetry/NetworkHost.js +39 -0
  648. package/build/dist/Utils/Telemetry/NetworkHost.js.map +1 -0
  649. package/build/dist/Utils/Telemetry/OriginAllowList.js +105 -4
  650. package/build/dist/Utils/Telemetry/OriginAllowList.js.map +1 -1
  651. package/build/dist/Utils/Traces/CriticalPath.js +9 -4
  652. package/build/dist/Utils/Traces/CriticalPath.js.map +1 -1
  653. package/package.json +1 -1
  654. package/Tests/UI/Rum/RecordingHealthStrip.test.tsx +0 -600
  655. package/UI/Components/GanttChart/Bar/BarLabel.tsx +0 -17
  656. package/UI/Components/GanttChart/Bar/Index.tsx +0 -149
  657. package/UI/Components/GanttChart/ChartContainer.tsx +0 -50
  658. package/UI/Components/GanttChart/Index.tsx +0 -90
  659. package/UI/Components/GanttChart/Row/Index.tsx +0 -22
  660. package/UI/Components/GanttChart/Row/Row.tsx +0 -231
  661. package/UI/Components/GanttChart/Row/RowLabel.tsx +0 -30
  662. package/UI/Components/GanttChart/Rows.tsx +0 -59
  663. package/UI/Components/GanttChart/Timeline/Index.tsx +0 -46
  664. package/UI/Components/GanttChart/Timeline/TimelineInterval.tsx +0 -33
  665. package/UI/Components/GanttChart/Timeline/TimelineIntervalMarks.tsx +0 -43
  666. package/build/dist/UI/Components/GanttChart/Bar/BarLabel.js +0 -8
  667. package/build/dist/UI/Components/GanttChart/Bar/BarLabel.js.map +0 -1
  668. package/build/dist/UI/Components/GanttChart/Bar/Index.js +0 -77
  669. package/build/dist/UI/Components/GanttChart/Bar/Index.js.map +0 -1
  670. package/build/dist/UI/Components/GanttChart/ChartContainer.js +0 -33
  671. package/build/dist/UI/Components/GanttChart/ChartContainer.js.map +0 -1
  672. package/build/dist/UI/Components/GanttChart/Index.js +0 -39
  673. package/build/dist/UI/Components/GanttChart/Index.js.map +0 -1
  674. package/build/dist/UI/Components/GanttChart/Row/Index.js +0 -7
  675. package/build/dist/UI/Components/GanttChart/Row/Index.js.map +0 -1
  676. package/build/dist/UI/Components/GanttChart/Row/Row.js +0 -106
  677. package/build/dist/UI/Components/GanttChart/Row/Row.js.map +0 -1
  678. package/build/dist/UI/Components/GanttChart/Row/RowLabel.js +0 -10
  679. package/build/dist/UI/Components/GanttChart/Row/RowLabel.js.map +0 -1
  680. package/build/dist/UI/Components/GanttChart/Rows.js +0 -19
  681. package/build/dist/UI/Components/GanttChart/Rows.js.map +0 -1
  682. package/build/dist/UI/Components/GanttChart/Timeline/Index.js +0 -16
  683. package/build/dist/UI/Components/GanttChart/Timeline/Index.js.map +0 -1
  684. package/build/dist/UI/Components/GanttChart/Timeline/TimelineInterval.js +0 -13
  685. package/build/dist/UI/Components/GanttChart/Timeline/TimelineInterval.js.map +0 -1
  686. package/build/dist/UI/Components/GanttChart/Timeline/TimelineIntervalMarks.js +0 -13
  687. 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 { MAX_SESSION_REPLAY_READ_BYTES } from "../../../../Types/Rum/SessionReplay";
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(listRequest());
1397
+ await SessionReplayReadService.listSessions(request());
615
1398
 
616
- const query: string = statementOf(headerQuerySpy).query;
617
- expect(query).toContain("argMax(visitorId, version) AS aggVisitorId");
618
- expect(query).not.toContain("identifiedUserLabel");
1399
+ expect(endedBySessionId(result)).toEqual({
1400
+ "s-no-index": false,
1401
+ "s-garbled": false,
1402
+ });
1403
+ });
619
1404
 
620
- expect(result.sessions[0]!.identifiedUserKey).toBe("hmac-1");
621
- expect(result.sessions[0]!.visitorId).toBe(visitorId);
622
- expect(result.sessions[1]!.visitorId).toBe("");
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);