@oneuptime/common 12.0.33 → 13.0.0

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 (877) hide show
  1. package/Models/AnalyticsModels/RumSession.ts +117 -1
  2. package/Models/AnalyticsModels/RumSessionChunk.ts +13 -0
  3. package/Models/DatabaseModels/AlertCustomField.ts +77 -3
  4. package/Models/DatabaseModels/ApiKeyPermission.ts +3 -27
  5. package/Models/DatabaseModels/GlobalConfig.ts +12 -12
  6. package/Models/DatabaseModels/IncidentCustomField.ts +77 -3
  7. package/Models/DatabaseModels/InventoryItem.ts +44 -1
  8. package/Models/DatabaseModels/InventoryItemCustomField.ts +77 -3
  9. package/Models/DatabaseModels/MonitorCustomField.ts +77 -3
  10. package/Models/DatabaseModels/NetworkDevice.ts +126 -0
  11. package/Models/DatabaseModels/OnCallDutyPolicyCustomField.ts +77 -3
  12. package/Models/DatabaseModels/RumApplication.ts +6 -6
  13. package/Models/DatabaseModels/ScheduledMaintenanceCustomField.ts +77 -3
  14. package/Models/DatabaseModels/StatusPageCustomField.ts +77 -3
  15. package/Models/DatabaseModels/TeamCustomField.ts +77 -3
  16. package/Models/DatabaseModels/TeamMember.ts +0 -9
  17. package/Models/DatabaseModels/TeamMemberCustomField.ts +77 -3
  18. package/Models/DatabaseModels/TeamPermission.ts +3 -24
  19. package/Models/DatabaseModels/TelemetryIngestionKey.ts +260 -0
  20. package/Models/DatabaseModels/UserTelegram.ts +0 -4
  21. package/Scripts/benchmark-fanin-capacity.js +181 -0
  22. package/Server/API/TelemetryAPI.ts +1284 -164
  23. package/Server/API/UserNotificationSettingAPI.ts +55 -0
  24. package/Server/API/UserTelegramAPI.ts +48 -4
  25. package/Server/EnvironmentConfig.ts +82 -18
  26. package/Server/Infrastructure/Postgres/SchemaMigrations/1791300000000-AddTelemetryIngestionKeyType.ts +91 -0
  27. package/Server/Infrastructure/Postgres/SchemaMigrations/1791400000000-SessionReplayRecordEverySessionByDefault.ts +67 -0
  28. package/Server/Infrastructure/Postgres/SchemaMigrations/1791500000000-WidenCustomFieldDropdownOptions.ts +139 -0
  29. package/Server/Infrastructure/Postgres/SchemaMigrations/1791600000000-AddCustomFieldValueMapping.ts +121 -0
  30. package/Server/Infrastructure/Postgres/SchemaMigrations/1791700000000-AddMacAddressToNetworkDevice.ts +25 -0
  31. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +10 -0
  32. package/Server/Infrastructure/QueueWorker.ts +20 -14
  33. package/Server/Infrastructure/Redis.ts +8 -8
  34. package/Server/Infrastructure/Semaphore.ts +2 -0
  35. package/Server/Infrastructure/Status.ts +10 -4
  36. package/Server/Middleware/TelemetryIngest.ts +453 -7
  37. package/Server/Services/AccessTokenService.ts +1 -0
  38. package/Server/Services/AlertCustomFieldService.ts +98 -0
  39. package/Server/Services/AlertService.ts +37 -0
  40. package/Server/Services/ApiKeyPermissionService.ts +461 -29
  41. package/Server/Services/CustomFieldMappingService.ts +879 -0
  42. package/Server/Services/DatabaseService.ts +65 -13
  43. package/Server/Services/GlobalConfigService.ts +194 -0
  44. package/Server/Services/IncidentCustomFieldService.ts +98 -0
  45. package/Server/Services/IncidentService.ts +35 -0
  46. package/Server/Services/InventoryItemService.ts +31 -1
  47. package/Server/Services/MetricService.ts +194 -0
  48. package/Server/Services/MonitorService.ts +32 -0
  49. package/Server/Services/MonitorTemplateService.ts +140 -19
  50. package/Server/Services/NetworkDeviceAutoImportRuleEngineService.ts +427 -66
  51. package/Server/Services/NetworkDeviceDiscoveryScanService.ts +87 -2
  52. package/Server/Services/NetworkDeviceService.ts +69 -0
  53. package/Server/Services/ProjectService.ts +22 -0
  54. package/Server/Services/RoutineEmailSettingsService.ts +73 -0
  55. package/Server/Services/RumSessionReplayViewService.ts +104 -24
  56. package/Server/Services/ScheduledMaintenanceCustomFieldService.ts +98 -0
  57. package/Server/Services/ScheduledMaintenanceService.ts +32 -0
  58. package/Server/Services/TeamMemberService.ts +78 -0
  59. package/Server/Services/TeamPermissionService.ts +235 -3
  60. package/Server/Services/TelemetryIngestionKeyService.ts +722 -19
  61. package/Server/Services/UserNotificationRuleService.ts +76 -25
  62. package/Server/Services/UserNotificationSettingService.ts +34 -6
  63. package/Server/Services/UserTelegramService.ts +326 -5
  64. package/Server/Types/Database/Permissions/ReadPermission.ts +45 -5
  65. package/Server/Types/Database/QueryHelper.ts +2 -0
  66. package/Server/Types/Database/QueryUtil.ts +56 -0
  67. package/Server/Types/Markdown.ts +93 -1
  68. package/Server/Utils/APIKey/AccessPermission.ts +5 -2
  69. package/Server/Utils/CustomField/CustomFieldDefinitionMappingHooks.ts +54 -0
  70. package/Server/Utils/CustomField/CustomFieldMappingRegistry.ts +415 -0
  71. package/Server/Utils/CustomField/CustomFieldMappingValidator.ts +335 -0
  72. package/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.ts +82 -17
  73. package/Server/Utils/DataSource/EgressGuard.ts +177 -8
  74. package/Server/Utils/EmailRollup/EmailRollupConstants.ts +38 -5
  75. package/Server/Utils/EmailRollup/EmailRollupFlushRunner.ts +67 -5
  76. package/Server/Utils/EmailRollup/EmailRollupRenderer.ts +2 -2
  77. package/Server/Utils/FrontendEnvironment.ts +40 -0
  78. package/Server/Utils/LogRedaction.ts +20 -0
  79. package/Server/Utils/Logger.ts +11 -0
  80. package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +394 -86
  81. package/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.ts +30 -4
  82. package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +31 -5
  83. package/Server/Utils/Monitor/MonitorAlert.ts +29 -6
  84. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +312 -52
  85. package/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.ts +82 -9
  86. package/Server/Utils/Monitor/MonitorCriteriaMessageBuilder.ts +16 -7
  87. package/Server/Utils/Monitor/MonitorCriteriaMessageFormatter.ts +29 -17
  88. package/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.ts +107 -48
  89. package/Server/Utils/Monitor/MonitorIncident.ts +22 -6
  90. package/Server/Utils/Monitor/MonitorTemplateUtil.ts +29 -0
  91. package/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.ts +158 -0
  92. package/Server/Utils/Monitor/NetworkInventoryUtil.ts +27 -0
  93. package/Server/Utils/Monitor/SeriesContextEnricher.ts +304 -0
  94. package/Server/Utils/SSRFProtection.ts +217 -10
  95. package/Server/Utils/SessionReplay/SessionReplayGateCache.ts +239 -138
  96. package/Server/Utils/SessionReplay/SessionReplayHealthCounters.ts +305 -0
  97. package/Server/Utils/SessionReplay/SessionReplayReadService.ts +1430 -289
  98. package/Server/Utils/StartServer.ts +2 -17
  99. package/Server/Utils/TelegramVerificationToken.ts +185 -0
  100. package/Server/Utils/Telemetry/EntityRegistry.ts +11 -1
  101. package/Server/Utils/Telemetry/PinServiceName.ts +197 -0
  102. package/Server/Utils/Telemetry/TelemetryEntity.ts +83 -9
  103. package/Server/Utils/Telemetry/TelemetryFanInWriter.ts +38 -16
  104. package/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.ts +92 -0
  105. package/Server/Utils/Telemetry/TelemetryIngestionKeyRateLimiter.ts +217 -0
  106. package/Server/Utils/VM/VMAPI.ts +1 -0
  107. package/Server/Utils/VM/VMRunner.ts +975 -95
  108. package/Tests/App/AdminDashboard/AdminHeaderSmallScreens.test.tsx +202 -0
  109. package/Tests/App/Dashboard/AddNeighborToMonitoringModal.test.tsx +32 -0
  110. package/Tests/App/Dashboard/AdoptedDeviceDrawer.test.tsx +374 -0
  111. package/Tests/App/Dashboard/DashboardHeaderSmallScreens.test.tsx +404 -0
  112. package/Tests/App/Dashboard/DashboardLabelVariables.test.tsx +580 -0
  113. package/Tests/App/Dashboard/DashboardMonitorLabelVariable.test.tsx +438 -0
  114. package/Tests/App/Dashboard/DashboardVariableAllContract.test.tsx +5 -2
  115. package/Tests/App/Dashboard/DeviceAttachmentCard.test.tsx +273 -0
  116. package/Tests/App/Dashboard/DiscoveryReviewInventoryRefresh.test.tsx +505 -0
  117. package/Tests/App/Dashboard/DiscoveryScanWizardValidation.test.tsx +468 -0
  118. package/Tests/App/Dashboard/EntityDetailPanel.test.tsx +163 -0
  119. package/Tests/App/Dashboard/InfrastructureExplorer.test.tsx +401 -0
  120. package/Tests/App/Dashboard/InfrastructureGraph.test.tsx +232 -0
  121. package/Tests/App/Dashboard/InventoryItems.test.tsx +201 -0
  122. package/Tests/App/Dashboard/InventoryTypeAndStatusFacets.test.tsx +798 -0
  123. package/Tests/App/Dashboard/NetworkTopologyLiveView.test.tsx +320 -0
  124. package/Tests/App/Dashboard/NetworkTopologyToolbar.test.tsx +275 -0
  125. package/Tests/App/Dashboard/OnCallCalendarFeedEmptyState.test.tsx +975 -0
  126. package/Tests/App/Dashboard/OnCallCalendarFeedPlanGate.test.tsx +63 -0
  127. package/Tests/App/Dashboard/ServiceMapGraph.test.tsx +626 -0
  128. package/Tests/App/Dashboard/TopologyDataLoading.test.tsx +348 -0
  129. package/Tests/App/Dashboard/TopologyPageNavigation.test.tsx +331 -0
  130. package/Tests/App/Dashboard/UserSettingsEmailPreferences.test.tsx +603 -0
  131. package/Tests/App/Dashboard/UserSettingsNotificationSettings.test.tsx +324 -0
  132. package/Tests/App/PublicDashboard/DashboardVariableSelector.test.tsx +13 -4
  133. package/Tests/App/StatusPage/StatusPageLastUpdated.test.tsx +263 -0
  134. package/Tests/App/StatusPage/StatusPageOidcOrigin.test.tsx +223 -0
  135. package/Tests/App/StatusPage/StatusPageOverviewLiveAndSearch.test.tsx +753 -0
  136. package/Tests/App/StatusPage/StatusPageResourceSearchBox.test.tsx +224 -0
  137. package/Tests/Models/AnalyticsModels/RumSessionReplayColumns.test.ts +262 -0
  138. package/Tests/Models/CustomFieldMappingColumns.test.ts +166 -0
  139. package/Tests/Models/DatabaseModels/SessionReplayModels.test.ts +20 -6
  140. package/Tests/Models/InventoryItemStatus.test.ts +147 -0
  141. package/Tests/Models/NetworkDeviceMacAddressColumn.test.ts +294 -0
  142. package/Tests/ResponsiveVisibility.test.ts +115 -0
  143. package/Tests/ResponsiveVisibility.ts +183 -0
  144. package/Tests/Server/API/DashboardPublicResourceListAPI.test.ts +129 -0
  145. package/Tests/Server/API/SessionReplayAPI.test.ts +1940 -216
  146. package/Tests/Server/API/UserNotificationSettingAPI.test.ts +186 -0
  147. package/Tests/Server/API/UserTelegramAPISecurity.test.ts +231 -0
  148. package/Tests/Server/EnvironmentConfigFrontendSecurity.test.ts +234 -0
  149. package/Tests/Server/EnvironmentConfigValkey.test.ts +227 -0
  150. package/Tests/Server/Infrastructure/Postgres/AddMacAddressToNetworkDeviceMigration.test.ts +365 -0
  151. package/Tests/Server/Infrastructure/Postgres/CustomFieldValueMappingMigration.test.ts +241 -0
  152. package/Tests/Server/Infrastructure/Postgres/InventoryItemArchiveMigration.test.ts +41 -0
  153. package/Tests/Server/Infrastructure/Postgres/SessionReplayRecordEverySessionByDefaultMigration.test.ts +169 -0
  154. package/Tests/Server/Infrastructure/QueueWorkerTimeout.test.ts +210 -0
  155. package/Tests/Server/Infrastructure/TelemetryExporterDeploymentConfig.test.ts +130 -0
  156. package/Tests/Server/Infrastructure/ValkeyDeploymentConfig.test.ts +265 -0
  157. package/Tests/Server/Middleware/HttpMetricsMiddleware.test.ts +382 -0
  158. package/Tests/Server/Middleware/ProjectAuthorizationApiKeyMiddleware.test.ts +4 -1
  159. package/Tests/Server/Middleware/SCIMAuthorization.test.ts +525 -0
  160. package/Tests/Server/Middleware/TelemetryIngestBrowserKey.test.ts +1163 -0
  161. package/Tests/Server/Middleware/TelemetryIngestTokenLog.test.ts +5 -3
  162. package/Tests/Server/Services/AddTelemetryIngestionKeyTypeMigration.test.ts +555 -0
  163. package/Tests/Server/Services/ApiKeyPermissionSecurity.test.ts +678 -0
  164. package/Tests/Server/Services/ApiKeyPermissionService.test.ts +276 -28
  165. package/Tests/Server/Services/CustomFieldDropdownOptionsColumnWidth.test.ts +431 -0
  166. package/Tests/Server/Services/CustomFieldMappingService.test.ts +850 -0
  167. package/Tests/Server/Services/DatabaseServiceAggregateBy.test.ts +291 -56
  168. package/Tests/Server/Services/DatabaseServiceSortTiebreaker.test.ts +151 -0
  169. package/Tests/Server/Services/DiscoveryScanClaimHookFreeSafety.test.ts +32 -0
  170. package/Tests/Server/Services/GlobalConfigService.test.ts +414 -1
  171. package/Tests/Server/Services/InventoryItemManualCreate.test.ts +105 -0
  172. package/Tests/Server/Services/InventoryItemPromotionGate.test.ts +178 -0
  173. package/Tests/Server/Services/MetricRawEntityKeyPrune.test.ts +592 -0
  174. package/Tests/Server/Services/MonitorTemplateServiceCustomFieldSync.test.ts +502 -0
  175. package/Tests/Server/Services/NetworkDeviceAutoImportRuleEngineService.test.ts +1351 -74
  176. package/Tests/Server/Services/NetworkDeviceDiscoveryScanRegistration.test.ts +520 -0
  177. package/Tests/Server/Services/NetworkDeviceMacAddressNormalization.test.ts +572 -0
  178. package/Tests/Server/Services/RoutineEmailSettingsPostgres.test.ts +329 -0
  179. package/Tests/Server/Services/RumSessionReplayViewService.test.ts +267 -0
  180. package/Tests/Server/Services/TeamMemberAutoAcceptInvitation.test.ts +9 -0
  181. package/Tests/Server/Services/TeamMemberInviteRegistrationToken.test.ts +9 -0
  182. package/Tests/Server/Services/TeamPrivilegeEscalation.test.ts +834 -0
  183. package/Tests/Server/Services/TelemetryIngestionKeyPolicyResolution.test.ts +814 -0
  184. package/Tests/Server/Services/TelemetryIngestionKeyValidation.test.ts +998 -0
  185. package/Tests/Server/Services/UserNotificationSettingRollupRouting.test.ts +141 -0
  186. package/Tests/Server/Services/UserNotificationSettingWorkspaceChannels.test.ts +18 -10
  187. package/Tests/Server/Services/UserTelegramVerificationSecurity.test.ts +688 -0
  188. package/Tests/Server/Types/Database/InventoryStatusPostgres.test.ts +368 -0
  189. package/Tests/Server/Types/Database/Permissions/ReadBlockPermission.test.ts +261 -14
  190. package/Tests/Server/Types/Database/QueryUtilIncludesAnyOfGroups.test.ts +169 -0
  191. package/Tests/Server/Types/Database/QueryUtilIncludesAnyOfGroupsPostgres.test.ts +297 -0
  192. package/Tests/Server/Types/Markdown.test.ts +194 -1
  193. package/Tests/Server/Types/Workflow/Components/IncomingWebhookUtils.test.ts +279 -0
  194. package/Tests/Server/Types/Workflow/Components/TextToJson.test.ts +212 -0
  195. package/Tests/Server/Utils/AI/SRE/Insights/FixRouting.test.ts +497 -0
  196. package/Tests/Server/Utils/AI/Toolbox/Serializer.test.ts +383 -0
  197. package/Tests/Server/Utils/APIKey/AccessPermission.test.ts +7 -3
  198. package/Tests/Server/Utils/AnalyticsDatabase/ClusterConfig.test.ts +315 -0
  199. package/Tests/Server/Utils/CustomField/CustomFieldMappingRegistry.test.ts +436 -0
  200. package/Tests/Server/Utils/CustomField/CustomFieldMappingValidator.test.ts +437 -0
  201. package/Tests/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.test.ts +214 -0
  202. package/Tests/Server/Utils/DataSource/EgressGuard.test.ts +300 -11
  203. package/Tests/Server/Utils/EmailRollup/EmailRollupBurstWindow.test.ts +83 -0
  204. package/Tests/Server/Utils/EmailRollup/EmailRollupFlushRunnerBehaviour.test.ts +2 -2
  205. package/Tests/Server/Utils/EmailRollup/EmailRollupFlushRunnerPreferences.test.ts +362 -0
  206. package/Tests/Server/Utils/EmailRollup/EmailRollupRenderer.test.ts +10 -3
  207. package/Tests/Server/Utils/EmailRollup/EmailRollupTestHarness.ts +77 -1
  208. package/Tests/Server/Utils/EventLoop.test.ts +359 -0
  209. package/Tests/Server/Utils/FrontendEnvironment.test.ts +190 -0
  210. package/Tests/Server/Utils/JsonWebToken.test.ts +30 -0
  211. package/Tests/Server/Utils/LogRedaction.test.ts +26 -0
  212. package/Tests/Server/Utils/LoggerCredentialLeak.test.ts +23 -9
  213. package/Tests/Server/Utils/Monitor/Criteria/CompareCriteria.test.ts +242 -0
  214. package/Tests/Server/Utils/Monitor/Criteria/CompareCriteriaAggregation.test.ts +590 -0
  215. package/Tests/Server/Utils/Monitor/Criteria/DatabaseMonitorCriteria.test.ts +29 -0
  216. package/Tests/Server/Utils/Monitor/Criteria/IncomingRequestHeaderCriteria.test.ts +236 -0
  217. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.test.ts +30 -6
  218. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteriaAnomalyUnits.test.ts +246 -0
  219. package/Tests/Server/Utils/Monitor/MonitorCriteriaEmailEvalLogAgreement.test.ts +280 -0
  220. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluator.test.ts +659 -8
  221. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorMetricUnits.test.ts +570 -0
  222. package/Tests/Server/Utils/Monitor/MonitorCriteriaExpectationBuilderUnits.test.ts +30 -5
  223. package/Tests/Server/Utils/Monitor/MonitorCriteriaMessageBuilderUnits.test.ts +68 -21
  224. package/Tests/Server/Utils/Monitor/MonitorCriteriaObservationBuilderUnits.test.ts +36 -19
  225. package/Tests/Server/Utils/Monitor/MonitorTemplateUtilSeriesContext.test.ts +176 -0
  226. package/Tests/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.test.ts +573 -0
  227. package/Tests/Server/Utils/Monitor/NetworkInventoryUtil.test.ts +317 -0
  228. package/Tests/Server/Utils/Monitor/SeriesContextEnricher.test.ts +356 -0
  229. package/Tests/Server/Utils/SSRFProtectionCloudServiceAddresses.test.ts +595 -0
  230. package/Tests/Server/Utils/SessionReplay/SessionReplayGateCachePolicy.test.ts +198 -1
  231. package/Tests/Server/Utils/SessionReplay/SessionReplayHealthCounters.test.ts +453 -0
  232. package/Tests/Server/Utils/SessionReplay/SessionReplayReadServiceQueries.test.ts +1378 -0
  233. package/Tests/Server/Utils/SessionReplayOriginAllowListRefactor.test.ts +420 -0
  234. package/Tests/Server/Utils/TelegramVerificationToken.test.ts +253 -0
  235. package/Tests/Server/Utils/Telemetry/ContextSpanProcessor.test.ts +281 -0
  236. package/Tests/Server/Utils/Telemetry/InventoryDuplicateIdentity.test.ts +316 -0
  237. package/Tests/Server/Utils/Telemetry/PinServiceName.test.ts +666 -0
  238. package/Tests/Server/Utils/Telemetry/TelemetryEntity.test.ts +111 -8
  239. package/Tests/Server/Utils/Telemetry/TelemetryFanInWriterCapacity.test.ts +458 -0
  240. package/Tests/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.test.ts +424 -0
  241. package/Tests/Server/Utils/Telemetry/TelemetryIngestionKeyRateLimiter.test.ts +528 -0
  242. package/Tests/Server/Utils/TelemetryExporterEnvironment.test.ts +56 -0
  243. package/Tests/Server/Utils/VM/VMRunnerHostBridgeLatency.test.ts +74 -0
  244. package/Tests/Server/Utils/VM/VMRunnerPrivateNetworkWiring.test.ts +100 -7
  245. package/Tests/Server/Utils/VM/VMRunnerSsrf.test.ts +1176 -6
  246. package/Tests/Types/CustomField/CustomFieldMappingCatalog.test.ts +220 -0
  247. package/Tests/Types/CustomField/CustomFieldValueMapping.test.ts +348 -0
  248. package/Tests/Types/Database/IncludesAnyOfGroups.test.ts +106 -0
  249. package/Tests/Types/JSONFunctions.test.ts +300 -0
  250. package/Tests/Types/Monitor/CephAlertTemplates.test.ts +422 -53
  251. package/Tests/Types/Monitor/DockerAlertTemplates.test.ts +495 -15
  252. package/Tests/Types/Monitor/DockerSwarmAlertTemplates.test.ts +191 -45
  253. package/Tests/Types/Monitor/HostAlertTemplates.test.ts +370 -42
  254. package/Tests/Types/Monitor/IotAlertTemplates.test.ts +375 -20
  255. package/Tests/Types/Monitor/KubernetesAlertTemplates.test.ts +636 -42
  256. package/Tests/Types/Monitor/KubernetesMetricCatalog.test.ts +189 -0
  257. package/Tests/Types/Monitor/KubernetesTemplateGroupByKeys.test.ts +95 -14
  258. package/Tests/Types/Monitor/PodmanAlertTemplates.test.ts +190 -21
  259. package/Tests/Types/Monitor/ProxmoxAlertTemplates.test.ts +352 -22
  260. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationAlertDebuggability.test.ts +363 -0
  261. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationNotificationMode.test.ts +93 -18
  262. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.test.ts +259 -0
  263. package/Tests/Types/Monitor/Recommendation/RecommendationCriteriaBuilder.test.ts +476 -0
  264. package/Tests/Types/Monitor/RumAlertTemplates.test.ts +136 -0
  265. package/Tests/Types/Monitor/SeriesContext/SeriesDebugHints.test.ts +661 -0
  266. package/Tests/Types/Monitor/SeriesContext/SeriesLabelDisplay.test.ts +507 -0
  267. package/Tests/Types/Monitor/ServiceAlertTemplates.test.ts +270 -0
  268. package/Tests/Types/Monitor/TemplateGroupByKeys.test.ts +38 -10
  269. package/Tests/Types/Monitor/Utils/RecommendationCriteriaAssertions.ts +102 -0
  270. package/Tests/Types/NetworkAutomation/AutoImportRunMerge.test.ts +235 -0
  271. package/Tests/Types/NetworkAutomation/RuleRunResult.test.ts +66 -0
  272. package/Tests/Types/NetworkAutomation/RuleRunResultDescribe.test.ts +309 -1
  273. package/Tests/Types/NotificationSetting/RoutineEmailEvents.test.ts +47 -0
  274. package/Tests/Types/Rum/SessionReplayApiContracts.test.ts +608 -0
  275. package/Tests/Types/Rum/SessionReplayCustomEvents.test.ts +434 -0
  276. package/Tests/Types/Telemetry/InventoryLiveness.test.ts +99 -0
  277. package/Tests/Types/WebsiteRequest.test.ts +285 -3
  278. package/Tests/UI/Components/Charts/ChartBucketIdentity.test.ts +16 -3
  279. package/Tests/UI/Components/Charts/ChartTrailingBucketGap.test.ts +691 -0
  280. package/Tests/UI/Components/ComponentsModal.test.tsx +12 -4
  281. package/Tests/UI/Components/ComponentsModalUsability.test.tsx +518 -0
  282. package/Tests/UI/Components/CustomFields/CustomFieldsDetail.test.tsx +160 -0
  283. package/Tests/UI/Components/CustomFields/CustomFieldsDetailMapping.test.tsx +324 -0
  284. package/Tests/UI/Components/CustomFields/MapFromCustomFieldInput.test.tsx +176 -0
  285. package/Tests/UI/Components/Graphs/DayUptimeGraph.test.tsx +484 -0
  286. package/Tests/UI/Components/HeaderRightRail.test.tsx +157 -0
  287. package/Tests/UI/Components/IconDropdownItem.test.tsx +102 -0
  288. package/Tests/UI/Components/JSONTablePrototypePollution.test.tsx +117 -0
  289. package/Tests/UI/Components/KeyboardShortcutsModal.test.tsx +214 -0
  290. package/Tests/UI/Components/ModelTable/ModelTableWrapContent.test.tsx +561 -0
  291. package/Tests/UI/Components/MonitorGraphs/UptimeBarDayModal.test.tsx +302 -0
  292. package/Tests/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.test.ts +28 -12
  293. package/Tests/UI/Components/ShortcutDialogGuard.test.tsx +74 -0
  294. package/Tests/UI/Components/StatusPage/ResourceGroupSectionAutoExpand.test.tsx +248 -0
  295. package/Tests/UI/Components/TableCellWrapping.test.tsx +625 -0
  296. package/Tests/UI/Components/TableLoadingStates.test.tsx +205 -0
  297. package/Tests/UI/Components/Workflow/NodePlacement.test.ts +148 -0
  298. package/Tests/UI/Components/Workflow/Workflow.test.tsx +981 -0
  299. package/Tests/UI/ConfigBrowserTelemetry.test.ts +70 -0
  300. package/Tests/UI/ReactRouterSingletonBuild.test.ts +133 -0
  301. package/Tests/UI/Rum/ChunkLoader.test.ts +1036 -2
  302. package/Tests/UI/Rum/FidelityNotices.test.ts +187 -0
  303. package/Tests/UI/Rum/InactivityMap.test.ts +341 -0
  304. package/Tests/UI/Rum/PrivacySummaryCard.test.tsx +259 -0
  305. package/Tests/UI/Rum/RecordingHealthCard.test.tsx +855 -0
  306. package/Tests/UI/Rum/RecordingHealthStrip.test.tsx +594 -0
  307. package/Tests/UI/Rum/ReplayCard.test.tsx +451 -0
  308. package/Tests/UI/Rum/ReplayCorrelationPanel.test.tsx +536 -0
  309. package/Tests/UI/Rum/ReplayEngine.test.ts +2503 -0
  310. package/Tests/UI/Rum/ReplayEngineTypes.test.ts +171 -0
  311. package/Tests/UI/Rum/ReplayHeader.test.tsx +719 -0
  312. package/Tests/UI/Rum/ReplayLink.test.tsx +119 -0
  313. package/Tests/UI/Rum/ReplayPinControl.test.tsx +408 -0
  314. package/Tests/UI/Rum/ReplayPlaybackIntent.test.ts +152 -0
  315. package/Tests/UI/Rum/ReplayPlayerChrome.test.tsx +514 -0
  316. package/Tests/UI/Rum/ReplayRail.test.tsx +1296 -0
  317. package/Tests/UI/Rum/ReplayRailDetail.test.tsx +900 -0
  318. package/Tests/UI/Rum/ReplayScrubber.test.tsx +763 -147
  319. package/Tests/UI/Rum/ReplaySignalTypes.test.ts +189 -0
  320. package/Tests/UI/Rum/ReplaySignals.test.ts +1579 -0
  321. package/Tests/UI/Rum/ReplayStage.test.tsx +396 -473
  322. package/Tests/UI/Rum/ReplayStageOverlays.test.tsx +918 -0
  323. package/Tests/UI/Rum/ReplayTimeFormat.test.ts +94 -0
  324. package/Tests/UI/Rum/ReplayTimeline.test.tsx +696 -0
  325. package/Tests/UI/Rum/ReplayUi.test.tsx +559 -0
  326. package/Tests/UI/Rum/SessionReplayEmptyState.test.tsx +499 -0
  327. package/Tests/UI/Rum/SessionReplaySearchBar.test.tsx +346 -0
  328. package/Tests/UI/Rum/SessionReplaySetupGuide.test.tsx +545 -0
  329. package/Tests/UI/Rum/SessionReplayTable.test.tsx +991 -0
  330. package/Tests/UI/Rum/TargetedCapturePanel.test.tsx +254 -0
  331. package/Tests/UI/Telemetry/BrowserExporterIsolation.test.ts +71 -0
  332. package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +5 -0
  333. package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +1 -0
  334. package/Tests/UI/Utils/GlobalKeyboardShortcut.test.ts +344 -0
  335. package/Tests/Utils/API.test.ts +308 -0
  336. package/Tests/Utils/Dashboard/Components/DashboardMonitorListComponent.test.ts +11 -1
  337. package/Tests/Utils/Dashboard/LabelVariable.test.ts +257 -0
  338. package/Tests/Utils/Dashboard/VariableUrlState.test.ts +144 -14
  339. package/Tests/Utils/HTTPResponseBodyReader.test.ts +323 -0
  340. package/Tests/Utils/MetricUnitUtil.test.ts +81 -0
  341. package/Tests/Utils/Monitor/DeviceMacLearningUtil.test.ts +497 -0
  342. package/Tests/Utils/Monitor/MetricValueFormatter.test.ts +546 -0
  343. package/Tests/Utils/Monitor/MonitorTemplateCustomFieldUtil.test.ts +218 -0
  344. package/Tests/Utils/Monitor/NetworkTopologyEndpointAdoption.test.ts +1755 -0
  345. package/Tests/Utils/NetworkAutomation/AutoImportRunChain.test.ts +320 -0
  346. package/Tests/Utils/NetworkDiscovery/DiscoveryScanStatus.test.ts +117 -0
  347. package/Tests/Utils/Rum/ChunkMath.test.ts +83 -0
  348. package/Tests/Utils/Rum/SessionReplayHealthDiagnosis.test.ts +993 -0
  349. package/Tests/Utils/Rum/SessionReplayStringMap.test.ts +245 -0
  350. package/Tests/Utils/StatusPage/ResourceSearch.test.ts +631 -0
  351. package/Tests/Utils/Telemetry/OriginAllowList.test.ts +633 -0
  352. package/Tests/Utils/Uptime/DayUptimeGraphUtil.test.ts +461 -0
  353. package/Tests/Utils/ValueFormatter.test.ts +148 -0
  354. package/Types/BaseDatabase/IncludesAnyOfGroups.ts +77 -0
  355. package/Types/CustomField/CustomFieldMappingCatalog.ts +161 -0
  356. package/Types/CustomField/CustomFieldMappingSourceResource.ts +19 -0
  357. package/Types/CustomField/CustomFieldValueMapping.ts +288 -0
  358. package/Types/Dashboard/DashboardComponents/ComponentArgument.ts +1 -0
  359. package/Types/Dashboard/DashboardComponents/DashboardMonitorListComponent.ts +1 -0
  360. package/Types/Dashboard/DashboardVariable.ts +8 -0
  361. package/Types/Icon/IconProp.ts +1 -0
  362. package/Types/JSON.ts +4 -0
  363. package/Types/JSONFunctions.ts +205 -43
  364. package/Types/Monitor/CephAlertTemplates.ts +157 -195
  365. package/Types/Monitor/DockerAlertTemplates.ts +338 -128
  366. package/Types/Monitor/DockerSwarmAlertTemplates.ts +95 -112
  367. package/Types/Monitor/HostAlertTemplates.ts +302 -130
  368. package/Types/Monitor/IotAlertTemplates.ts +112 -104
  369. package/Types/Monitor/KubernetesAlertTemplates.ts +415 -224
  370. package/Types/Monitor/KubernetesMetricCatalog.ts +23 -19
  371. package/Types/Monitor/PodmanAlertTemplates.ts +116 -163
  372. package/Types/Monitor/ProxmoxAlertTemplates.ts +217 -99
  373. package/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.ts +98 -6
  374. package/Types/Monitor/Recommendation/MonitorRecommendationUtil.ts +44 -7
  375. package/Types/Monitor/Recommendation/RecommendationCriteriaBuilder.ts +383 -0
  376. package/Types/Monitor/RumAlertTemplates.ts +46 -7
  377. package/Types/Monitor/SeriesContext/SeriesDebugHints.ts +596 -0
  378. package/Types/Monitor/SeriesContext/SeriesLabelDisplay.ts +554 -0
  379. package/Types/Monitor/ServiceAlertTemplates.ts +98 -11
  380. package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +13 -0
  381. package/Types/Monitor/UptimeHistoryLabels.ts +56 -0
  382. package/Types/NetworkAutomation/RuleRunResult.ts +402 -13
  383. package/Types/NotificationSetting/RoutineEmailEvents.ts +30 -0
  384. package/Types/Rum/SessionReplay.ts +209 -0
  385. package/Types/Rum/SessionReplayApi.ts +722 -0
  386. package/Types/Rum/SessionReplayCaptureTrigger.ts +21 -12
  387. package/Types/Rum/SessionReplayConsentMode.ts +11 -7
  388. package/Types/Rum/SessionReplayCustomEvents.ts +549 -0
  389. package/Types/Rum/SessionReplayHealth.ts +238 -0
  390. package/Types/SerializableObjectDictionary.ts +4 -0
  391. package/Types/Telemetry/InventoryLiveness.ts +56 -0
  392. package/Types/Telemetry/TelemetryIngestSurface.ts +95 -0
  393. package/Types/Telemetry/TelemetryIngestionKeyPolicy.ts +75 -0
  394. package/Types/Telemetry/TelemetryIngestionKeyType.ts +29 -0
  395. package/Types/WebsiteRequest.ts +85 -3
  396. package/UI/Components/CSVFileUpload/CSVFileUpload.tsx +9 -2
  397. package/UI/Components/Charts/Area/AreaChart.tsx +13 -2
  398. package/UI/Components/Charts/Bar/BarChart.tsx +6 -2
  399. package/UI/Components/Charts/Line/LineChart.tsx +13 -2
  400. package/UI/Components/Charts/Types/XAxis/XAxis.ts +29 -0
  401. package/UI/Components/Charts/Utils/DataPoint.ts +22 -3
  402. package/UI/Components/Charts/Utils/TimeAnnotation.ts +76 -9
  403. package/UI/Components/Charts/Utils/XAxis.ts +217 -0
  404. package/UI/Components/CustomFields/CustomFieldsDetail.tsx +189 -46
  405. package/UI/Components/CustomFields/MapFromCustomFieldInput.tsx +216 -0
  406. package/UI/Components/Dashboard/DashboardVariableControl.tsx +285 -0
  407. package/UI/Components/Graphs/DayUptimeGraph.tsx +173 -8
  408. package/UI/Components/Graphs/UptimeBarTooltip.tsx +22 -278
  409. package/UI/Components/Graphs/UptimeDaySummary.tsx +327 -0
  410. package/UI/Components/Header/Header.tsx +25 -9
  411. package/UI/Components/Header/IconDropdown/IconDropdownItem.tsx +10 -1
  412. package/UI/Components/Header/IconDropdown/IconDropdownMenu.tsx +6 -1
  413. package/UI/Components/Header/ProjectPicker/ProjectPicker.tsx +6 -1
  414. package/UI/Components/HeaderAlert/NotificationBell/NotificationBellDropdown.tsx +6 -1
  415. package/UI/Components/Icon/Icon.tsx +11 -0
  416. package/UI/Components/JSONTable/JSONTable.tsx +2 -2
  417. package/UI/Components/KeyboardShortcut/KeyboardShortcutsModal.tsx +125 -0
  418. package/UI/Components/KeyboardShortcut/Screenshots/README.md +19 -0
  419. package/UI/Components/KeyboardShortcut/Screenshots/keyboard-shortcuts-command-palette.png +0 -0
  420. package/UI/Components/KeyboardShortcut/Screenshots/keyboard-shortcuts-dialog-dark.png +0 -0
  421. package/UI/Components/KeyboardShortcut/Screenshots/keyboard-shortcuts-dialog.png +0 -0
  422. package/UI/Components/KeyboardShortcut/Screenshots/keyboard-shortcuts-help-menu.png +0 -0
  423. package/UI/Components/KeyboardShortcut/Screenshots/keyboard-shortcuts-narrow.png +0 -0
  424. package/UI/Components/ModelTable/Column.ts +15 -0
  425. package/UI/Components/Monitor/SeriesDebugCommandsViewer.tsx +64 -0
  426. package/UI/Components/Monitor/SeriesLabelsViewer.tsx +93 -0
  427. package/UI/Components/MonitorGraphs/Uptime.tsx +14 -2
  428. package/UI/Components/MonitorGraphs/UptimeBarDayModal.tsx +63 -10
  429. package/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.ts +44 -0
  430. package/UI/Components/StatusPage/ResourceGroupSection.tsx +40 -0
  431. package/UI/Components/Table/CellClassName.ts +81 -0
  432. package/UI/Components/Table/TableRow.tsx +20 -14
  433. package/UI/Components/Table/TableSkeletonRows.tsx +9 -9
  434. package/UI/Components/Table/Types/Column.ts +36 -0
  435. package/UI/Components/Tooltip/Tooltip.tsx +11 -1
  436. package/UI/Components/Workflow/ComponentsModal.tsx +60 -39
  437. package/UI/Components/Workflow/NodePlacement.ts +57 -0
  438. package/UI/Components/Workflow/Workflow.tsx +47 -18
  439. package/UI/Config.ts +15 -33
  440. package/UI/Utils/GlobalKeyboardShortcut.ts +208 -0
  441. package/UI/Utils/Telemetry/BrowserTelemetryConfig.ts +28 -0
  442. package/UI/Utils/Telemetry/Telemetry.ts +10 -8
  443. package/UI/esbuild-config.js +6 -0
  444. package/Utils/API.ts +129 -3
  445. package/Utils/Dashboard/Components/DashboardMonitorListComponent.ts +12 -1
  446. package/Utils/Dashboard/LabelVariable.ts +152 -0
  447. package/Utils/Dashboard/VariableUrlState.ts +27 -5
  448. package/Utils/HTTPResponseBodyReader.ts +221 -0
  449. package/Utils/MetricUnitUtil.ts +34 -0
  450. package/Utils/Monitor/DeviceMacLearningUtil.ts +177 -0
  451. package/Utils/Monitor/MetricValueFormatter.ts +342 -0
  452. package/Utils/Monitor/MonitorTemplateCustomFieldUtil.ts +124 -0
  453. package/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.ts +7 -2
  454. package/Utils/Monitor/NetworkTopologyUtil.ts +693 -0
  455. package/Utils/NetworkAutomation/AutoImportRunChain.ts +138 -0
  456. package/Utils/NetworkDiscovery/DiscoveryScanStatus.ts +77 -0
  457. package/Utils/Rum/ChunkMath.ts +106 -0
  458. package/Utils/Rum/SessionReplayHealth.ts +732 -0
  459. package/Utils/Rum/SessionReplayStringMap.ts +226 -0
  460. package/Utils/Schema/ModelSchema.ts +1 -0
  461. package/Utils/StatusPage/ResourceSearch.ts +290 -0
  462. package/Utils/Telemetry/OriginAllowList.ts +355 -0
  463. package/Utils/Uptime/DayUptimeGraphUtil.ts +205 -0
  464. package/Utils/ValueFormatter.ts +35 -1
  465. package/build/dist/Models/AnalyticsModels/RumSession.js +102 -19
  466. package/build/dist/Models/AnalyticsModels/RumSession.js.map +1 -1
  467. package/build/dist/Models/AnalyticsModels/RumSessionChunk.js +11 -9
  468. package/build/dist/Models/AnalyticsModels/RumSessionChunk.js.map +1 -1
  469. package/build/dist/Models/DatabaseModels/AlertCustomField.js +79 -3
  470. package/build/dist/Models/DatabaseModels/AlertCustomField.js.map +1 -1
  471. package/build/dist/Models/DatabaseModels/ApiKeyPermission.js +3 -27
  472. package/build/dist/Models/DatabaseModels/ApiKeyPermission.js.map +1 -1
  473. package/build/dist/Models/DatabaseModels/GlobalConfig.js +12 -12
  474. package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
  475. package/build/dist/Models/DatabaseModels/IncidentCustomField.js +79 -3
  476. package/build/dist/Models/DatabaseModels/IncidentCustomField.js.map +1 -1
  477. package/build/dist/Models/DatabaseModels/InventoryItem.js +34 -1
  478. package/build/dist/Models/DatabaseModels/InventoryItem.js.map +1 -1
  479. package/build/dist/Models/DatabaseModels/InventoryItemCustomField.js +79 -3
  480. package/build/dist/Models/DatabaseModels/InventoryItemCustomField.js.map +1 -1
  481. package/build/dist/Models/DatabaseModels/MonitorCustomField.js +79 -3
  482. package/build/dist/Models/DatabaseModels/MonitorCustomField.js.map +1 -1
  483. package/build/dist/Models/DatabaseModels/NetworkDevice.js +128 -0
  484. package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
  485. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyCustomField.js +79 -3
  486. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyCustomField.js.map +1 -1
  487. package/build/dist/Models/DatabaseModels/RumApplication.js +6 -6
  488. package/build/dist/Models/DatabaseModels/RumApplication.js.map +1 -1
  489. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceCustomField.js +79 -3
  490. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceCustomField.js.map +1 -1
  491. package/build/dist/Models/DatabaseModels/StatusPageCustomField.js +79 -3
  492. package/build/dist/Models/DatabaseModels/StatusPageCustomField.js.map +1 -1
  493. package/build/dist/Models/DatabaseModels/TeamCustomField.js +79 -3
  494. package/build/dist/Models/DatabaseModels/TeamCustomField.js.map +1 -1
  495. package/build/dist/Models/DatabaseModels/TeamMember.js +0 -9
  496. package/build/dist/Models/DatabaseModels/TeamMember.js.map +1 -1
  497. package/build/dist/Models/DatabaseModels/TeamMemberCustomField.js +79 -3
  498. package/build/dist/Models/DatabaseModels/TeamMemberCustomField.js.map +1 -1
  499. package/build/dist/Models/DatabaseModels/TeamPermission.js +3 -24
  500. package/build/dist/Models/DatabaseModels/TeamPermission.js.map +1 -1
  501. package/build/dist/Models/DatabaseModels/TelemetryIngestionKey.js +267 -0
  502. package/build/dist/Models/DatabaseModels/TelemetryIngestionKey.js.map +1 -1
  503. package/build/dist/Models/DatabaseModels/UserTelegram.js +0 -4
  504. package/build/dist/Models/DatabaseModels/UserTelegram.js.map +1 -1
  505. package/build/dist/Server/API/TelemetryAPI.js +769 -109
  506. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  507. package/build/dist/Server/API/UserNotificationSettingAPI.js +35 -0
  508. package/build/dist/Server/API/UserNotificationSettingAPI.js.map +1 -0
  509. package/build/dist/Server/API/UserTelegramAPI.js +28 -6
  510. package/build/dist/Server/API/UserTelegramAPI.js.map +1 -1
  511. package/build/dist/Server/EnvironmentConfig.js +73 -14
  512. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  513. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791300000000-AddTelemetryIngestionKeyType.js +60 -0
  514. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791300000000-AddTelemetryIngestionKeyType.js.map +1 -0
  515. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791400000000-SessionReplayRecordEverySessionByDefault.js +53 -0
  516. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791400000000-SessionReplayRecordEverySessionByDefault.js.map +1 -0
  517. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791500000000-WidenCustomFieldDropdownOptions.js +82 -0
  518. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791500000000-WidenCustomFieldDropdownOptions.js.map +1 -0
  519. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791600000000-AddCustomFieldValueMapping.js +46 -0
  520. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791600000000-AddCustomFieldValueMapping.js.map +1 -0
  521. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791700000000-AddMacAddressToNetworkDevice.js +14 -0
  522. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791700000000-AddMacAddressToNetworkDevice.js.map +1 -0
  523. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +10 -0
  524. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  525. package/build/dist/Server/Infrastructure/QueueWorker.js +19 -7
  526. package/build/dist/Server/Infrastructure/QueueWorker.js.map +1 -1
  527. package/build/dist/Server/Infrastructure/Redis.js +8 -8
  528. package/build/dist/Server/Infrastructure/Redis.js.map +1 -1
  529. package/build/dist/Server/Infrastructure/Semaphore.js +1 -0
  530. package/build/dist/Server/Infrastructure/Semaphore.js.map +1 -1
  531. package/build/dist/Server/Infrastructure/Status.js +10 -4
  532. package/build/dist/Server/Infrastructure/Status.js.map +1 -1
  533. package/build/dist/Server/Middleware/TelemetryIngest.js +315 -8
  534. package/build/dist/Server/Middleware/TelemetryIngest.js.map +1 -1
  535. package/build/dist/Server/Services/AccessTokenService.js +1 -0
  536. package/build/dist/Server/Services/AccessTokenService.js.map +1 -1
  537. package/build/dist/Server/Services/AlertCustomFieldService.js +93 -0
  538. package/build/dist/Server/Services/AlertCustomFieldService.js.map +1 -1
  539. package/build/dist/Server/Services/AlertService.js +34 -0
  540. package/build/dist/Server/Services/AlertService.js.map +1 -1
  541. package/build/dist/Server/Services/ApiKeyPermissionService.js +331 -28
  542. package/build/dist/Server/Services/ApiKeyPermissionService.js.map +1 -1
  543. package/build/dist/Server/Services/CustomFieldMappingService.js +614 -0
  544. package/build/dist/Server/Services/CustomFieldMappingService.js.map +1 -0
  545. package/build/dist/Server/Services/DatabaseService.js +58 -13
  546. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  547. package/build/dist/Server/Services/GlobalConfigService.js +133 -0
  548. package/build/dist/Server/Services/GlobalConfigService.js.map +1 -1
  549. package/build/dist/Server/Services/IncidentCustomFieldService.js +93 -0
  550. package/build/dist/Server/Services/IncidentCustomFieldService.js.map +1 -1
  551. package/build/dist/Server/Services/IncidentService.js +32 -0
  552. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  553. package/build/dist/Server/Services/InventoryItemService.js +28 -1
  554. package/build/dist/Server/Services/InventoryItemService.js.map +1 -1
  555. package/build/dist/Server/Services/MetricService.js +163 -0
  556. package/build/dist/Server/Services/MetricService.js.map +1 -1
  557. package/build/dist/Server/Services/MonitorService.js +29 -0
  558. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  559. package/build/dist/Server/Services/MonitorTemplateService.js +116 -17
  560. package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
  561. package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js +349 -64
  562. package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js.map +1 -1
  563. package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js +65 -0
  564. package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js.map +1 -1
  565. package/build/dist/Server/Services/NetworkDeviceService.js +54 -0
  566. package/build/dist/Server/Services/NetworkDeviceService.js.map +1 -1
  567. package/build/dist/Server/Services/ProjectService.js +22 -0
  568. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  569. package/build/dist/Server/Services/RoutineEmailSettingsService.js +69 -0
  570. package/build/dist/Server/Services/RoutineEmailSettingsService.js.map +1 -0
  571. package/build/dist/Server/Services/RumSessionReplayViewService.js +86 -24
  572. package/build/dist/Server/Services/RumSessionReplayViewService.js.map +1 -1
  573. package/build/dist/Server/Services/ScheduledMaintenanceCustomFieldService.js +93 -0
  574. package/build/dist/Server/Services/ScheduledMaintenanceCustomFieldService.js.map +1 -1
  575. package/build/dist/Server/Services/ScheduledMaintenanceService.js +29 -0
  576. package/build/dist/Server/Services/ScheduledMaintenanceService.js.map +1 -1
  577. package/build/dist/Server/Services/TeamMemberService.js +62 -2
  578. package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
  579. package/build/dist/Server/Services/TeamPermissionService.js +166 -3
  580. package/build/dist/Server/Services/TeamPermissionService.js.map +1 -1
  581. package/build/dist/Server/Services/TelemetryIngestionKeyService.js +562 -18
  582. package/build/dist/Server/Services/TelemetryIngestionKeyService.js.map +1 -1
  583. package/build/dist/Server/Services/UserNotificationRuleService.js +60 -25
  584. package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
  585. package/build/dist/Server/Services/UserNotificationSettingService.js +26 -6
  586. package/build/dist/Server/Services/UserNotificationSettingService.js.map +1 -1
  587. package/build/dist/Server/Services/UserTelegramService.js +257 -4
  588. package/build/dist/Server/Services/UserTelegramService.js.map +1 -1
  589. package/build/dist/Server/Types/Database/Permissions/ReadPermission.js +29 -4
  590. package/build/dist/Server/Types/Database/Permissions/ReadPermission.js.map +1 -1
  591. package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
  592. package/build/dist/Server/Types/Database/QueryUtil.js +36 -0
  593. package/build/dist/Server/Types/Database/QueryUtil.js.map +1 -1
  594. package/build/dist/Server/Types/Markdown.js +75 -0
  595. package/build/dist/Server/Types/Markdown.js.map +1 -1
  596. package/build/dist/Server/Utils/APIKey/AccessPermission.js +2 -2
  597. package/build/dist/Server/Utils/APIKey/AccessPermission.js.map +1 -1
  598. package/build/dist/Server/Utils/CustomField/CustomFieldDefinitionMappingHooks.js +27 -0
  599. package/build/dist/Server/Utils/CustomField/CustomFieldDefinitionMappingHooks.js.map +1 -0
  600. package/build/dist/Server/Utils/CustomField/CustomFieldMappingRegistry.js +226 -0
  601. package/build/dist/Server/Utils/CustomField/CustomFieldMappingRegistry.js.map +1 -0
  602. package/build/dist/Server/Utils/CustomField/CustomFieldMappingValidator.js +189 -0
  603. package/build/dist/Server/Utils/CustomField/CustomFieldMappingValidator.js.map +1 -0
  604. package/build/dist/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.js +53 -18
  605. package/build/dist/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.js.map +1 -1
  606. package/build/dist/Server/Utils/DataSource/EgressGuard.js +121 -7
  607. package/build/dist/Server/Utils/DataSource/EgressGuard.js.map +1 -1
  608. package/build/dist/Server/Utils/EmailRollup/EmailRollupConstants.js +38 -5
  609. package/build/dist/Server/Utils/EmailRollup/EmailRollupConstants.js.map +1 -1
  610. package/build/dist/Server/Utils/EmailRollup/EmailRollupFlushRunner.js +54 -3
  611. package/build/dist/Server/Utils/EmailRollup/EmailRollupFlushRunner.js.map +1 -1
  612. package/build/dist/Server/Utils/EmailRollup/EmailRollupRenderer.js +2 -2
  613. package/build/dist/Server/Utils/EmailRollup/EmailRollupRenderer.js.map +1 -1
  614. package/build/dist/Server/Utils/FrontendEnvironment.js +33 -0
  615. package/build/dist/Server/Utils/FrontendEnvironment.js.map +1 -0
  616. package/build/dist/Server/Utils/LogRedaction.js +20 -0
  617. package/build/dist/Server/Utils/LogRedaction.js.map +1 -1
  618. package/build/dist/Server/Utils/Logger.js +10 -0
  619. package/build/dist/Server/Utils/Logger.js.map +1 -1
  620. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +323 -81
  621. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js.map +1 -1
  622. package/build/dist/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.js +20 -4
  623. package/build/dist/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.js.map +1 -1
  624. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +29 -5
  625. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js.map +1 -1
  626. package/build/dist/Server/Utils/Monitor/MonitorAlert.js +29 -6
  627. package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
  628. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +254 -48
  629. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  630. package/build/dist/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.js +58 -9
  631. package/build/dist/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.js.map +1 -1
  632. package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageBuilder.js +9 -3
  633. package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageBuilder.js.map +1 -1
  634. package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageFormatter.js +26 -12
  635. package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageFormatter.js.map +1 -1
  636. package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js +74 -33
  637. package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js.map +1 -1
  638. package/build/dist/Server/Utils/Monitor/MonitorIncident.js +22 -6
  639. package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
  640. package/build/dist/Server/Utils/Monitor/MonitorTemplateUtil.js +22 -0
  641. package/build/dist/Server/Utils/Monitor/MonitorTemplateUtil.js.map +1 -1
  642. package/build/dist/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.js +114 -0
  643. package/build/dist/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.js.map +1 -0
  644. package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js +25 -0
  645. package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js.map +1 -1
  646. package/build/dist/Server/Utils/Monitor/SeriesContextEnricher.js +237 -0
  647. package/build/dist/Server/Utils/Monitor/SeriesContextEnricher.js.map +1 -0
  648. package/build/dist/Server/Utils/SSRFProtection.js +136 -9
  649. package/build/dist/Server/Utils/SSRFProtection.js.map +1 -1
  650. package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCache.js +174 -89
  651. package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCache.js.map +1 -1
  652. package/build/dist/Server/Utils/SessionReplay/SessionReplayHealthCounters.js +223 -0
  653. package/build/dist/Server/Utils/SessionReplay/SessionReplayHealthCounters.js.map +1 -0
  654. package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js +945 -164
  655. package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js.map +1 -1
  656. package/build/dist/Server/Utils/StartServer.js +3 -14
  657. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  658. package/build/dist/Server/Utils/TelegramVerificationToken.js +129 -0
  659. package/build/dist/Server/Utils/TelegramVerificationToken.js.map +1 -0
  660. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +9 -1
  661. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
  662. package/build/dist/Server/Utils/Telemetry/PinServiceName.js +163 -0
  663. package/build/dist/Server/Utils/Telemetry/PinServiceName.js.map +1 -0
  664. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js +70 -13
  665. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js.map +1 -1
  666. package/build/dist/Server/Utils/Telemetry/TelemetryFanInWriter.js +28 -15
  667. package/build/dist/Server/Utils/Telemetry/TelemetryFanInWriter.js.map +1 -1
  668. package/build/dist/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.js +64 -0
  669. package/build/dist/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.js.map +1 -0
  670. package/build/dist/Server/Utils/Telemetry/TelemetryIngestionKeyRateLimiter.js +171 -0
  671. package/build/dist/Server/Utils/Telemetry/TelemetryIngestionKeyRateLimiter.js.map +1 -0
  672. package/build/dist/Server/Utils/VM/VMAPI.js.map +1 -1
  673. package/build/dist/Server/Utils/VM/VMRunner.js +780 -69
  674. package/build/dist/Server/Utils/VM/VMRunner.js.map +1 -1
  675. package/build/dist/Types/BaseDatabase/IncludesAnyOfGroups.js +54 -0
  676. package/build/dist/Types/BaseDatabase/IncludesAnyOfGroups.js.map +1 -0
  677. package/build/dist/Types/CustomField/CustomFieldMappingCatalog.js +60 -0
  678. package/build/dist/Types/CustomField/CustomFieldMappingCatalog.js.map +1 -0
  679. package/build/dist/Types/CustomField/CustomFieldMappingSourceResource.js +20 -0
  680. package/build/dist/Types/CustomField/CustomFieldMappingSourceResource.js.map +1 -0
  681. package/build/dist/Types/CustomField/CustomFieldValueMapping.js +141 -0
  682. package/build/dist/Types/CustomField/CustomFieldValueMapping.js.map +1 -0
  683. package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js +1 -0
  684. package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js.map +1 -1
  685. package/build/dist/Types/Dashboard/DashboardVariable.js +1 -0
  686. package/build/dist/Types/Dashboard/DashboardVariable.js.map +1 -1
  687. package/build/dist/Types/Icon/IconProp.js +1 -0
  688. package/build/dist/Types/Icon/IconProp.js.map +1 -1
  689. package/build/dist/Types/JSON.js +1 -0
  690. package/build/dist/Types/JSON.js.map +1 -1
  691. package/build/dist/Types/JSONFunctions.js +131 -37
  692. package/build/dist/Types/JSONFunctions.js.map +1 -1
  693. package/build/dist/Types/Monitor/CephAlertTemplates.js +130 -171
  694. package/build/dist/Types/Monitor/CephAlertTemplates.js.map +1 -1
  695. package/build/dist/Types/Monitor/DockerAlertTemplates.js +286 -121
  696. package/build/dist/Types/Monitor/DockerAlertTemplates.js.map +1 -1
  697. package/build/dist/Types/Monitor/DockerSwarmAlertTemplates.js +84 -105
  698. package/build/dist/Types/Monitor/DockerSwarmAlertTemplates.js.map +1 -1
  699. package/build/dist/Types/Monitor/HostAlertTemplates.js +250 -122
  700. package/build/dist/Types/Monitor/HostAlertTemplates.js.map +1 -1
  701. package/build/dist/Types/Monitor/IotAlertTemplates.js +81 -81
  702. package/build/dist/Types/Monitor/IotAlertTemplates.js.map +1 -1
  703. package/build/dist/Types/Monitor/KubernetesAlertTemplates.js +375 -215
  704. package/build/dist/Types/Monitor/KubernetesAlertTemplates.js.map +1 -1
  705. package/build/dist/Types/Monitor/KubernetesMetricCatalog.js +19 -19
  706. package/build/dist/Types/Monitor/KubernetesMetricCatalog.js.map +1 -1
  707. package/build/dist/Types/Monitor/PodmanAlertTemplates.js +104 -153
  708. package/build/dist/Types/Monitor/PodmanAlertTemplates.js.map +1 -1
  709. package/build/dist/Types/Monitor/ProxmoxAlertTemplates.js +156 -91
  710. package/build/dist/Types/Monitor/ProxmoxAlertTemplates.js.map +1 -1
  711. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.js +67 -6
  712. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.js.map +1 -1
  713. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationUtil.js +38 -7
  714. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationUtil.js.map +1 -1
  715. package/build/dist/Types/Monitor/Recommendation/RecommendationCriteriaBuilder.js +261 -0
  716. package/build/dist/Types/Monitor/Recommendation/RecommendationCriteriaBuilder.js.map +1 -0
  717. package/build/dist/Types/Monitor/RumAlertTemplates.js +46 -7
  718. package/build/dist/Types/Monitor/RumAlertTemplates.js.map +1 -1
  719. package/build/dist/Types/Monitor/SeriesContext/SeriesDebugHints.js +422 -0
  720. package/build/dist/Types/Monitor/SeriesContext/SeriesDebugHints.js.map +1 -0
  721. package/build/dist/Types/Monitor/SeriesContext/SeriesLabelDisplay.js +441 -0
  722. package/build/dist/Types/Monitor/SeriesContext/SeriesLabelDisplay.js.map +1 -0
  723. package/build/dist/Types/Monitor/ServiceAlertTemplates.js +79 -15
  724. package/build/dist/Types/Monitor/ServiceAlertTemplates.js.map +1 -1
  725. package/build/dist/Types/Monitor/UptimeHistoryLabels.js +17 -0
  726. package/build/dist/Types/Monitor/UptimeHistoryLabels.js.map +1 -0
  727. package/build/dist/Types/NetworkAutomation/RuleRunResult.js +291 -6
  728. package/build/dist/Types/NetworkAutomation/RuleRunResult.js.map +1 -1
  729. package/build/dist/Types/NotificationSetting/RoutineEmailEvents.js +29 -0
  730. package/build/dist/Types/NotificationSetting/RoutineEmailEvents.js.map +1 -0
  731. package/build/dist/Types/Rum/SessionReplay.js +117 -0
  732. package/build/dist/Types/Rum/SessionReplay.js.map +1 -1
  733. package/build/dist/Types/Rum/SessionReplayApi.js +181 -0
  734. package/build/dist/Types/Rum/SessionReplayApi.js.map +1 -0
  735. package/build/dist/Types/Rum/SessionReplayCaptureTrigger.js +21 -12
  736. package/build/dist/Types/Rum/SessionReplayCaptureTrigger.js.map +1 -1
  737. package/build/dist/Types/Rum/SessionReplayConsentMode.js +11 -7
  738. package/build/dist/Types/Rum/SessionReplayConsentMode.js.map +1 -1
  739. package/build/dist/Types/Rum/SessionReplayCustomEvents.js +172 -0
  740. package/build/dist/Types/Rum/SessionReplayCustomEvents.js.map +1 -0
  741. package/build/dist/Types/Rum/SessionReplayHealth.js +37 -0
  742. package/build/dist/Types/Rum/SessionReplayHealth.js.map +1 -0
  743. package/build/dist/Types/SerializableObjectDictionary.js +4 -0
  744. package/build/dist/Types/SerializableObjectDictionary.js.map +1 -1
  745. package/build/dist/Types/Telemetry/InventoryLiveness.js +37 -0
  746. package/build/dist/Types/Telemetry/InventoryLiveness.js.map +1 -0
  747. package/build/dist/Types/Telemetry/TelemetryIngestSurface.js +85 -0
  748. package/build/dist/Types/Telemetry/TelemetryIngestSurface.js.map +1 -0
  749. package/build/dist/Types/Telemetry/TelemetryIngestionKeyPolicy.js +15 -0
  750. package/build/dist/Types/Telemetry/TelemetryIngestionKeyPolicy.js.map +1 -0
  751. package/build/dist/Types/Telemetry/TelemetryIngestionKeyType.js +30 -0
  752. package/build/dist/Types/Telemetry/TelemetryIngestionKeyType.js.map +1 -0
  753. package/build/dist/Types/WebsiteRequest.js +55 -3
  754. package/build/dist/Types/WebsiteRequest.js.map +1 -1
  755. package/build/dist/UI/Components/CSVFileUpload/CSVFileUpload.js +7 -0
  756. package/build/dist/UI/Components/CSVFileUpload/CSVFileUpload.js.map +1 -1
  757. package/build/dist/UI/Components/Charts/Area/AreaChart.js +13 -2
  758. package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
  759. package/build/dist/UI/Components/Charts/Bar/BarChart.js +6 -2
  760. package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
  761. package/build/dist/UI/Components/Charts/Line/LineChart.js +13 -2
  762. package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
  763. package/build/dist/UI/Components/Charts/Types/XAxis/XAxis.js.map +1 -1
  764. package/build/dist/UI/Components/Charts/Utils/DataPoint.js +15 -3
  765. package/build/dist/UI/Components/Charts/Utils/DataPoint.js.map +1 -1
  766. package/build/dist/UI/Components/Charts/Utils/TimeAnnotation.js +40 -9
  767. package/build/dist/UI/Components/Charts/Utils/TimeAnnotation.js.map +1 -1
  768. package/build/dist/UI/Components/Charts/Utils/XAxis.js +186 -0
  769. package/build/dist/UI/Components/Charts/Utils/XAxis.js.map +1 -1
  770. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js +89 -8
  771. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js.map +1 -1
  772. package/build/dist/UI/Components/CustomFields/MapFromCustomFieldInput.js +118 -0
  773. package/build/dist/UI/Components/CustomFields/MapFromCustomFieldInput.js.map +1 -0
  774. package/build/dist/UI/Components/Dashboard/DashboardVariableControl.js +137 -0
  775. package/build/dist/UI/Components/Dashboard/DashboardVariableControl.js.map +1 -0
  776. package/build/dist/UI/Components/Graphs/DayUptimeGraph.js +112 -7
  777. package/build/dist/UI/Components/Graphs/DayUptimeGraph.js.map +1 -1
  778. package/build/dist/UI/Components/Graphs/UptimeBarTooltip.js +5 -162
  779. package/build/dist/UI/Components/Graphs/UptimeBarTooltip.js.map +1 -1
  780. package/build/dist/UI/Components/Graphs/UptimeDaySummary.js +181 -0
  781. package/build/dist/UI/Components/Graphs/UptimeDaySummary.js.map +1 -0
  782. package/build/dist/UI/Components/Header/Header.js +2 -3
  783. package/build/dist/UI/Components/Header/Header.js.map +1 -1
  784. package/build/dist/UI/Components/Header/IconDropdown/IconDropdownItem.js +2 -1
  785. package/build/dist/UI/Components/Header/IconDropdown/IconDropdownItem.js.map +1 -1
  786. package/build/dist/UI/Components/Header/IconDropdown/IconDropdownMenu.js +7 -1
  787. package/build/dist/UI/Components/Header/IconDropdown/IconDropdownMenu.js.map +1 -1
  788. package/build/dist/UI/Components/Header/ProjectPicker/ProjectPicker.js +7 -1
  789. package/build/dist/UI/Components/Header/ProjectPicker/ProjectPicker.js.map +1 -1
  790. package/build/dist/UI/Components/HeaderAlert/NotificationBell/NotificationBellDropdown.js +7 -1
  791. package/build/dist/UI/Components/HeaderAlert/NotificationBell/NotificationBellDropdown.js.map +1 -1
  792. package/build/dist/UI/Components/Icon/Icon.js +5 -0
  793. package/build/dist/UI/Components/Icon/Icon.js.map +1 -1
  794. package/build/dist/UI/Components/JSONTable/JSONTable.js +2 -2
  795. package/build/dist/UI/Components/JSONTable/JSONTable.js.map +1 -1
  796. package/build/dist/UI/Components/KeyboardShortcut/KeyboardShortcutsModal.js +35 -0
  797. package/build/dist/UI/Components/KeyboardShortcut/KeyboardShortcutsModal.js.map +1 -0
  798. package/build/dist/UI/Components/Monitor/SeriesDebugCommandsViewer.js +34 -0
  799. package/build/dist/UI/Components/Monitor/SeriesDebugCommandsViewer.js.map +1 -0
  800. package/build/dist/UI/Components/Monitor/SeriesLabelsViewer.js +49 -0
  801. package/build/dist/UI/Components/Monitor/SeriesLabelsViewer.js.map +1 -0
  802. package/build/dist/UI/Components/MonitorGraphs/Uptime.js +1 -1
  803. package/build/dist/UI/Components/MonitorGraphs/Uptime.js.map +1 -1
  804. package/build/dist/UI/Components/MonitorGraphs/UptimeBarDayModal.js +32 -6
  805. package/build/dist/UI/Components/MonitorGraphs/UptimeBarDayModal.js.map +1 -1
  806. package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.js +38 -0
  807. package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.js.map +1 -1
  808. package/build/dist/UI/Components/StatusPage/ResourceGroupSection.js +22 -1
  809. package/build/dist/UI/Components/StatusPage/ResourceGroupSection.js.map +1 -1
  810. package/build/dist/UI/Components/Table/CellClassName.js +55 -0
  811. package/build/dist/UI/Components/Table/CellClassName.js.map +1 -0
  812. package/build/dist/UI/Components/Table/TableRow.js +16 -13
  813. package/build/dist/UI/Components/Table/TableRow.js.map +1 -1
  814. package/build/dist/UI/Components/Table/TableSkeletonRows.js +9 -8
  815. package/build/dist/UI/Components/Table/TableSkeletonRows.js.map +1 -1
  816. package/build/dist/UI/Components/Tooltip/Tooltip.js +25 -1
  817. package/build/dist/UI/Components/Tooltip/Tooltip.js.map +1 -1
  818. package/build/dist/UI/Components/Workflow/ComponentsModal.js +47 -30
  819. package/build/dist/UI/Components/Workflow/ComponentsModal.js.map +1 -1
  820. package/build/dist/UI/Components/Workflow/NodePlacement.js +37 -0
  821. package/build/dist/UI/Components/Workflow/NodePlacement.js.map +1 -0
  822. package/build/dist/UI/Components/Workflow/Workflow.js +35 -18
  823. package/build/dist/UI/Components/Workflow/Workflow.js.map +1 -1
  824. package/build/dist/UI/Config.js +10 -20
  825. package/build/dist/UI/Config.js.map +1 -1
  826. package/build/dist/UI/Utils/GlobalKeyboardShortcut.js +138 -0
  827. package/build/dist/UI/Utils/GlobalKeyboardShortcut.js.map +1 -0
  828. package/build/dist/UI/Utils/Telemetry/BrowserTelemetryConfig.js +14 -0
  829. package/build/dist/UI/Utils/Telemetry/BrowserTelemetryConfig.js.map +1 -0
  830. package/build/dist/UI/Utils/Telemetry/Telemetry.js +7 -5
  831. package/build/dist/UI/Utils/Telemetry/Telemetry.js.map +1 -1
  832. package/build/dist/Utils/API.js +67 -5
  833. package/build/dist/Utils/API.js.map +1 -1
  834. package/build/dist/Utils/Dashboard/Components/DashboardMonitorListComponent.js +9 -1
  835. package/build/dist/Utils/Dashboard/Components/DashboardMonitorListComponent.js.map +1 -1
  836. package/build/dist/Utils/Dashboard/LabelVariable.js +96 -0
  837. package/build/dist/Utils/Dashboard/LabelVariable.js.map +1 -0
  838. package/build/dist/Utils/Dashboard/VariableUrlState.js +19 -4
  839. package/build/dist/Utils/Dashboard/VariableUrlState.js.map +1 -1
  840. package/build/dist/Utils/HTTPResponseBodyReader.js +157 -0
  841. package/build/dist/Utils/HTTPResponseBodyReader.js.map +1 -0
  842. package/build/dist/Utils/MetricUnitUtil.js +26 -0
  843. package/build/dist/Utils/MetricUnitUtil.js.map +1 -1
  844. package/build/dist/Utils/Monitor/DeviceMacLearningUtil.js +112 -0
  845. package/build/dist/Utils/Monitor/DeviceMacLearningUtil.js.map +1 -0
  846. package/build/dist/Utils/Monitor/MetricValueFormatter.js +276 -0
  847. package/build/dist/Utils/Monitor/MetricValueFormatter.js.map +1 -0
  848. package/build/dist/Utils/Monitor/MonitorTemplateCustomFieldUtil.js +101 -0
  849. package/build/dist/Utils/Monitor/MonitorTemplateCustomFieldUtil.js.map +1 -0
  850. package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js +7 -2
  851. package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js.map +1 -1
  852. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +484 -0
  853. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
  854. package/build/dist/Utils/NetworkAutomation/AutoImportRunChain.js +59 -0
  855. package/build/dist/Utils/NetworkAutomation/AutoImportRunChain.js.map +1 -0
  856. package/build/dist/Utils/NetworkDiscovery/DiscoveryScanStatus.js +69 -0
  857. package/build/dist/Utils/NetworkDiscovery/DiscoveryScanStatus.js.map +1 -0
  858. package/build/dist/Utils/Rum/ChunkMath.js +76 -0
  859. package/build/dist/Utils/Rum/ChunkMath.js.map +1 -1
  860. package/build/dist/Utils/Rum/SessionReplayHealth.js +534 -0
  861. package/build/dist/Utils/Rum/SessionReplayHealth.js.map +1 -0
  862. package/build/dist/Utils/Rum/SessionReplayStringMap.js +165 -0
  863. package/build/dist/Utils/Rum/SessionReplayStringMap.js.map +1 -0
  864. package/build/dist/Utils/Schema/ModelSchema.js +1 -0
  865. package/build/dist/Utils/Schema/ModelSchema.js.map +1 -1
  866. package/build/dist/Utils/StatusPage/ResourceSearch.js +200 -0
  867. package/build/dist/Utils/StatusPage/ResourceSearch.js.map +1 -0
  868. package/build/dist/Utils/Telemetry/OriginAllowList.js +299 -0
  869. package/build/dist/Utils/Telemetry/OriginAllowList.js.map +1 -0
  870. package/build/dist/Utils/Uptime/DayUptimeGraphUtil.js +147 -0
  871. package/build/dist/Utils/Uptime/DayUptimeGraphUtil.js.map +1 -0
  872. package/build/dist/Utils/ValueFormatter.js +32 -1
  873. package/build/dist/Utils/ValueFormatter.js.map +1 -1
  874. package/jest.config.json +2 -0
  875. package/package.json +1 -1
  876. package/test-setup.sh +9 -9
  877. package/tsconfig.json +51 -11
@@ -2,22 +2,35 @@ import { SQL, Statement } from "../AnalyticsDatabase/Statement";
2
2
  import { getQuerySettings } from "../AnalyticsDatabase/QuerySettingsHelper";
3
3
  import RumSessionService from "../../Services/RumSessionService";
4
4
  import RumSessionChunkService from "../../Services/RumSessionChunkService";
5
+ import ExceptionInstanceService from "../../Services/ExceptionInstanceService";
5
6
  import {
6
7
  DbJSONResponse,
7
8
  Results,
8
9
  } from "../../Services/AnalyticsDatabaseService";
10
+ import logger from "../Logger";
9
11
  import AnalyticsTableName from "../../../Types/AnalyticsDatabase/AnalyticsTableName";
10
12
  import TableColumnType from "../../../Types/AnalyticsDatabase/TableColumnType";
11
13
  import Includes from "../../../Types/BaseDatabase/Includes";
12
14
  import { JSONObject } from "../../../Types/JSON";
13
15
  import ObjectID from "../../../Types/ObjectID";
16
+ import OneUptimeDate from "../../../Types/Date";
14
17
  import ChunkMath from "../../../Utils/Rum/ChunkMath";
15
18
  import {
16
19
  MAX_SESSION_REPLAY_CHUNKS_PER_READ,
17
20
  MAX_SESSION_REPLAY_READ_BYTES,
21
+ SESSION_REPLAY_LIST_SEARCH_MAX_LENGTH,
22
+ SESSION_REPLAY_MAX_SESSION_MS,
23
+ SESSION_REPLAY_MAX_TAG_KEYS,
24
+ SESSION_REPLAY_RECORDER_CAPABILITIES,
18
25
  SessionReplayChunkManifestEntry,
19
26
  SessionReplayGap,
27
+ SessionReplaySealedReason,
20
28
  } from "../../../Types/Rum/SessionReplay";
29
+ import {
30
+ SESSION_REPLAY_SORT_BY_VALUES,
31
+ SessionReplaySortBy,
32
+ SessionReplaySortedListCursorDto,
33
+ } from "../../../Types/Rum/SessionReplayApi";
21
34
  import BadDataException from "../../../Types/Exception/BadDataException";
22
35
  import CaptureSpan from "../Telemetry/CaptureSpan";
23
36
 
@@ -50,10 +63,9 @@ import CaptureSpan from "../Telemetry/CaptureSpan";
50
63
  * 3. The manifest read must never name the `payload` column, so
51
64
  * ClickHouse never touches (and never decompresses) the only column
52
65
  * in the system that holds a recording of a real person's screen.
53
- * The byte-cap pre-check is the one exception, and it measures
54
- * `length(payload)` inside ClickHouse without ever shipping the
55
- * bytes: the cap has to bound the size of what is actually returned,
56
- * and the only honest measure of that is the stored column itself.
66
+ * getChunks is the one read that names it, and it measures
67
+ * `length(payload)` in the same statement that ships the bytes, so
68
+ * the column is decompressed exactly once per page.
57
69
  *
58
70
  * NOTE on aliases: ClickHouse substitutes SELECT aliases into same-level
59
71
  * unqualified WHERE references, and an aggregate alias there is an
@@ -84,6 +96,29 @@ export const MAX_SESSION_REPLAY_LIST_LIMIT: number = 200;
84
96
  /* Sessions returned by the exception -> replay lookup. */
85
97
  export const MAX_SESSION_REPLAY_FOR_EXCEPTION_LIMIT: number = 20;
86
98
 
99
+ /*
100
+ * Default window for the exception -> replay lookup when the caller gives
101
+ * none. RumSession is partitioned by day, so an unbounded lookup scans
102
+ * every partition the project has ever written; 30 days covers every
103
+ * retention tier a recording can still be played under.
104
+ */
105
+ export const DEFAULT_SESSION_REPLAY_FOR_EXCEPTION_WINDOW_DAYS: number = 30;
106
+
107
+ /*
108
+ * Sessions the exception-instance side index may name. The instance table
109
+ * carries the session id of the page that threw, which is how a session
110
+ * is found BEFORE the finalizer has written its fingerprint list.
111
+ */
112
+ const MAX_EXCEPTION_INSTANCE_SESSION_IDS: number = 100;
113
+
114
+ /*
115
+ * Padding around an exception's own timestamp when the caller pins the
116
+ * lookup to a moment: a session that contains the error started at most
117
+ * SESSION_REPLAY_MAX_SESSION_MS before it, and clock skew between the
118
+ * browser and the server is bounded far below this.
119
+ */
120
+ export const SESSION_REPLAY_EXCEPTION_WINDOW_PADDING_MS: number = 5 * 60 * 1000;
121
+
87
122
  /*
88
123
  * Row ceiling on one manifest. A session is capped at
89
124
  * MAX_SESSION_REPLAY_CHUNKS_PER_SESSION (480) chunks PER TAB, and a
@@ -93,11 +128,24 @@ export const MAX_SESSION_REPLAY_FOR_EXCEPTION_LIMIT: number = 20;
93
128
  */
94
129
  const MAX_MANIFEST_ROWS: number = 4096;
95
130
 
96
- export interface SessionReplayListCursor {
97
- /* Server-clamped session start of the last row of the previous page. */
98
- startTimeUnixMs: number;
99
- sessionId: string;
100
- }
131
+ /*
132
+ * How long one application's activity summary is served from memory. The
133
+ * health card polls every 10-60s per viewer and the summary is a small
134
+ * aggregate over a day of headers, so a 30s cache turns N viewers into
135
+ * one ClickHouse query per pod per half minute.
136
+ */
137
+ export const SESSION_REPLAY_ACTIVITY_SUMMARY_CACHE_TTL_MS: number = 30 * 1000;
138
+ const MAX_ACTIVITY_SUMMARY_CACHE_ENTRIES: number = 1000;
139
+
140
+ /* The header attribute the ingest writes chunk 0's capability list into. */
141
+ export const RECORDER_CAPABILITIES_ATTRIBUTE: string = "recorder.capabilities";
142
+
143
+ /*
144
+ * The keyset cursor the list accepts and emits. The legacy
145
+ * {startTimeUnixMs, sessionId} shape is normalised to this by
146
+ * parseSessionReplayListCursor before it reaches the service.
147
+ */
148
+ export type SessionReplayListCursor = SessionReplaySortedListCursorDto;
101
149
 
102
150
  export interface SessionReplayListFilters {
103
151
  hasError?: boolean | undefined;
@@ -118,6 +166,20 @@ export interface SessionReplayListFilters {
118
166
  /* "sessions that hit /checkout" - matches the routes array. */
119
167
  route?: string | undefined;
120
168
  minDurationMs?: number | undefined;
169
+ /*
170
+ * Free text: sessionId prefix, entry/exit URL and routes substring, exact
171
+ * trace id, and the identified user label when the caller may read it.
172
+ * Capped at SESSION_REPLAY_LIST_SEARCH_MAX_LENGTH by the handler.
173
+ */
174
+ search?: string | undefined;
175
+ /* startsWith over the routes array and the entry URL. */
176
+ urlPrefix?: string | undefined;
177
+ /* Every pair must match the session's tag map. */
178
+ tags?: Record<string, string> | undefined;
179
+ hasIdentifiedUser?: boolean | undefined;
180
+ /* (not finalized OR has chunks) AND not recording-lost. */
181
+ isPlayable?: boolean | undefined;
182
+ hasTraces?: boolean | undefined;
121
183
  }
122
184
 
123
185
  export interface SessionReplayListRequest {
@@ -128,11 +190,15 @@ export interface SessionReplayListRequest {
128
190
  filters: SessionReplayListFilters;
129
191
  limit: number;
130
192
  cursor?: SessionReplayListCursor | undefined;
193
+ /* Absent means "startTime", which is what the list always did. */
194
+ sortBy?: SessionReplaySortBy | undefined;
131
195
  /*
132
196
  * The raw end-user identifier has its own, narrower column ACL than the
133
197
  * rest of the header row. This raw-SQL path never invokes
134
198
  * ModelPermission, so the caller decides column-by-column and the
135
199
  * column is simply not named in the SELECT when it is not permitted.
200
+ * Gates the traits column and the label half of the search predicate
201
+ * as well.
136
202
  */
137
203
  includeIdentifiedUserLabel: boolean;
138
204
  }
@@ -170,7 +236,24 @@ export interface SessionReplayListItem {
170
236
  identifiedUserKey: string;
171
237
  /* Present only when the caller holds the narrower identity permission. */
172
238
  identifiedUserLabel?: string | undefined;
239
+ identifiedUserTraits?: Record<string, string> | undefined;
173
240
  samplePercentageAtCapture: number;
241
+ /* First MAX_LIST_ROUTES routes, in order. */
242
+ routes: Array<string>;
243
+ traceCount: number;
244
+ exceptionGroupCount: number;
245
+ /*
246
+ * The first exception fingerprint of the session, "" when there is none.
247
+ * The list's errors badge links at the exception group with it.
248
+ */
249
+ topExceptionFingerprint: string;
250
+ clickCount: number;
251
+ activeMs: number;
252
+ firstErrorOffsetMs: number;
253
+ expiresAtUnixMs: number;
254
+ tags: Record<string, string>;
255
+ startTimeUnixMs: number;
256
+ endTimeUnixMs: number;
174
257
  }
175
258
 
176
259
  export interface SessionReplayListResult {
@@ -178,6 +261,9 @@ export interface SessionReplayListResult {
178
261
  nextCursor: SessionReplayListCursor | null;
179
262
  }
180
263
 
264
+ /* Routes projected onto a list row; the table shows three and says "(N pages)". */
265
+ export const MAX_LIST_ROUTES: number = 5;
266
+
181
267
  export interface SessionReplaySessionHeader {
182
268
  sessionId: string;
183
269
  projectId: string;
@@ -222,6 +308,50 @@ export interface SessionReplaySessionHeader {
222
308
  traceIds: Array<string>;
223
309
  exceptionFingerprints: Array<string>;
224
310
  clockSkewMs: number;
311
+ /*
312
+ * The session clock as numbers, so the player places every telemetry
313
+ * row at rowUnixMs - startTimeUnixMs without re-parsing an ISO string.
314
+ */
315
+ startTimeUnixMs: number;
316
+ endTimeUnixMs: number;
317
+ /* The recorder's own start clock, before the server clamped it. */
318
+ clientReportedStartUnixMs: number;
319
+ tags: Record<string, string>;
320
+ expiresAtUnixMs: number;
321
+ clickCount: number;
322
+ customEventCount: number;
323
+ activeMs: number;
324
+ firstErrorOffsetMs: number;
325
+ /*
326
+ * From chunk 0's envelope (attributes["recorder.capabilities"]); empty
327
+ * for recordings that predate the field.
328
+ */
329
+ recorderCapabilities: Array<string>;
330
+ /*
331
+ * Never populated by getSessionHeader. The manifest handler fills them
332
+ * from getSessionIdentity ONLY after canReadIdentifiedUserLabel passes,
333
+ * so no statement names the identity columns for a caller who may not
334
+ * read them.
335
+ */
336
+ identifiedUserLabel?: string | undefined;
337
+ identifiedUserTraits?: Record<string, string> | undefined;
338
+ }
339
+
340
+ /* The two identity columns, read separately behind the identity ACL. */
341
+ export interface SessionReplaySessionIdentity {
342
+ identifiedUserLabel: string;
343
+ identifiedUserTraits: Record<string, string>;
344
+ }
345
+
346
+ /*
347
+ * What is still knowable about a session whose header has aged out of
348
+ * retention (or was never finalized), for the "this recording expired on
349
+ * <date>" answer instead of a bare "not found".
350
+ */
351
+ export interface SessionReplayExpiredSessionInfo {
352
+ rumApplicationId: string;
353
+ startTime: Date;
354
+ expiresAt: Date;
225
355
  }
226
356
 
227
357
  /*
@@ -238,6 +368,8 @@ export interface SessionReplayManifestTab {
238
368
  gaps: Array<SessionReplayGap>;
239
369
  maxChunkIndex: number;
240
370
  totalPayloadBytes: number;
371
+ /* Where this tab's footage begins on the session clock. */
372
+ firstChunkStartOffsetMs: number;
241
373
  }
242
374
 
243
375
  export interface SessionReplayManifest {
@@ -256,6 +388,17 @@ export interface SessionReplayChunkPayload {
256
388
  payload: string;
257
389
  }
258
390
 
391
+ export interface SessionReplayChunkReadResult {
392
+ /* The longest contiguous prefix of the requested chunks under the cap. */
393
+ chunks: Array<SessionReplayChunkPayload>;
394
+ /*
395
+ * Chunks that exist and were requested but did not fit under
396
+ * MAX_SESSION_REPLAY_READ_BYTES behind the ones served. A chunk absent
397
+ * from storage is NOT listed here: that is a gap, not an omission.
398
+ */
399
+ omittedChunkIndexes: Array<number>;
400
+ }
401
+
259
402
  export interface SessionReplayExceptionSession {
260
403
  sessionId: string;
261
404
  rumApplicationId: string;
@@ -277,6 +420,26 @@ export interface SessionReplayExceptionSession {
277
420
  isFinalized: boolean;
278
421
  }
279
422
 
423
+ export interface SessionReplayApplicationActivitySummary {
424
+ /* null when ClickHouse could not answer; the UI renders "unknown". */
425
+ sessionsLast24h: number | null;
426
+ playableSessionsLast24h: number | null;
427
+ /* null when the application has no session in retention. */
428
+ lastSessionStartedAt: Date | null;
429
+ /*
430
+ * What the NEWEST session's recorder said it could capture, filtered to
431
+ * the known vocabulary. null when there is no session in retention, when
432
+ * that session predates the attribute, or when the query failed - all
433
+ * three render as "not reported yet", which is the honest answer.
434
+ *
435
+ * This is how an operator spots a stale cached recorder artifact
436
+ * ("click labels: no") without opening a recording, which would write an
437
+ * audit row. It rides on the last-session query that is already run for
438
+ * lastSessionStartedAt, so it costs no extra round trip.
439
+ */
440
+ recorderCapabilities: Array<string> | null;
441
+ }
442
+
280
443
  /*
281
444
  * Header columns that are aggregated with argMax and their SELECT alias.
282
445
  * Kept as data rather than a hand-written SELECT list so the list, header
@@ -305,6 +468,44 @@ function argMaxDateTime(column: string): string {
305
468
  return `toFloat64(toUnixTimestamp64Milli(${argMaxColumn(column)}))`;
306
469
  }
307
470
 
471
+ /* A Date column (retentionDate) as unix milliseconds. */
472
+ function argMaxDate(column: string): string {
473
+ return `toFloat64(toUnixTimestamp(${argMaxColumn(column)})) * 1000`;
474
+ }
475
+
476
+ /*
477
+ * Duration that stays honest for a session the finalizer has not reached.
478
+ *
479
+ * The provisional header is written on chunk 0 with durationMs 0 and
480
+ * endTime = chunk 0's end, and it stays that way for the 10+ minutes of
481
+ * idleness the finalizer waits for. Reported verbatim, every live or
482
+ * recently finished session read "0s" in the list and a "longer than"
483
+ * filter hid all of them - the first thing a person testing their install
484
+ * sees is a session that claims to be empty. Until the finalized row
485
+ * exists, the span the header itself asserts (endTime - startTime) is the
486
+ * best lower bound there is, so the live value is the larger of the two.
487
+ * The finalized row's durationMs is authoritative and is used as-is.
488
+ */
489
+ /*
490
+ * durationMs is Int128 on disk while the clock arithmetic is Int64; both
491
+ * branches are cast to Int64 (a session is capped at four hours, so the
492
+ * cast cannot overflow) so `if` and `greatest` see one type.
493
+ */
494
+ const LIVE_DURATION_EXPRESSION: string = `toFloat64(if(${argMaxColumn(
495
+ "isFinalized",
496
+ )}, toInt64(${argMaxColumn("durationMs")}), greatest(toInt64(${argMaxColumn(
497
+ "durationMs",
498
+ )}), toUnixTimestamp64Milli(${argMaxColumn(
499
+ "endTime",
500
+ )}) - toUnixTimestamp64Milli(${argMaxColumn("startTime")}))))`;
501
+
502
+ /*
503
+ * The frustration total, shared by the hasFrustration predicate and the
504
+ * "frustration" sort so the two can never disagree about what counts.
505
+ */
506
+ const FRUSTRATION_TOTAL_EXPRESSION: string =
507
+ "(aggRageClickCount + aggDeadClickCount + aggErrorClickCount + aggRefreshRageCount)";
508
+
308
509
  /*
309
510
  * Aliases deliberately differ from the physical column names. See the
310
511
  * ILLEGAL_AGGREGATION note in the file header.
@@ -312,7 +513,7 @@ function argMaxDateTime(column: string): string {
312
513
  const HEADER_AGGREGATES: Array<AggregatedColumn> = [
313
514
  { alias: "aggStartTime", expression: argMaxDateTime("startTime") },
314
515
  { alias: "aggEndTime", expression: argMaxDateTime("endTime") },
315
- { alias: "aggDurationMs", expression: argMaxNumeric("durationMs") },
516
+ { alias: "aggDurationMs", expression: LIVE_DURATION_EXPRESSION },
316
517
  { alias: "aggIsFinalized", expression: argMaxColumn("isFinalized") },
317
518
  { alias: "aggSealedReason", expression: argMaxColumn("sealedReason") },
318
519
  { alias: "aggChunkCount", expression: argMaxNumeric("chunkCount") },
@@ -336,6 +537,12 @@ const HEADER_AGGREGATES: Array<AggregatedColumn> = [
336
537
  { alias: "aggTriggerReason", expression: argMaxColumn("triggerReason") },
337
538
  { alias: "aggEntryUrl", expression: argMaxColumn("entryUrl") },
338
539
  { alias: "aggExitUrl", expression: argMaxColumn("exitUrl") },
540
+ /*
541
+ * The full routes array: the list projects the first MAX_LIST_ROUTES and
542
+ * the urlPrefix / search predicates run over the argMax'd whole, never
543
+ * the raw column (which would match a superseded header version).
544
+ */
545
+ { alias: "aggRoutes", expression: argMaxColumn("routes") },
339
546
  { alias: "aggBrowserName", expression: argMaxColumn("browserName") },
340
547
  { alias: "aggBrowserVersion", expression: argMaxColumn("browserVersion") },
341
548
  { alias: "aggOsName", expression: argMaxColumn("osName") },
@@ -351,6 +558,36 @@ const HEADER_AGGREGATES: Array<AggregatedColumn> = [
351
558
  alias: "aggSamplePercentage",
352
559
  expression: argMaxNumeric("samplePercentageAtCapture"),
353
560
  },
561
+ /*
562
+ * Counts rather than the arrays themselves: the list only says "3 traces"
563
+ * and "2 exception groups", and the hasTraces predicate needs a number.
564
+ */
565
+ {
566
+ alias: "aggTraceCount",
567
+ expression: `toFloat64(length(${argMaxColumn("traceIds")}))`,
568
+ },
569
+ {
570
+ alias: "aggExceptionGroupCount",
571
+ expression: `toFloat64(length(${argMaxColumn("exceptionFingerprints")}))`,
572
+ },
573
+ /*
574
+ * The first fingerprint, so the list's "3 errors" badge can link at the
575
+ * exception group instead of at an unfiltered Exceptions page. Empty
576
+ * string when the session recorded no exception group; arrayElement on
577
+ * an empty array returns the type's default, which for String is ''.
578
+ */
579
+ {
580
+ alias: "aggTopExceptionFingerprint",
581
+ expression: `arrayElement(${argMaxColumn("exceptionFingerprints")}, 1)`,
582
+ },
583
+ { alias: "aggClickCount", expression: argMaxNumeric("clickCount") },
584
+ { alias: "aggActiveMs", expression: argMaxNumeric("activeMs") },
585
+ {
586
+ alias: "aggFirstErrorOffsetMs",
587
+ expression: argMaxNumeric("firstErrorOffsetMs"),
588
+ },
589
+ { alias: "aggExpiresAt", expression: argMaxDate("retentionDate") },
590
+ { alias: "aggTags", expression: argMaxColumn("tags") },
354
591
  ];
355
592
 
356
593
  /* Only the manifest needs these; the list never renders them. */
@@ -362,7 +599,6 @@ const HEADER_DETAIL_AGGREGATES: Array<AggregatedColumn> = [
362
599
  { alias: "aggRrwebVersion", expression: argMaxColumn("rrwebVersion") },
363
600
  { alias: "aggSchemaVersion", expression: argMaxNumeric("schemaVersion") },
364
601
  { alias: "aggWireVersion", expression: argMaxNumeric("wireVersion") },
365
- { alias: "aggRoutes", expression: argMaxColumn("routes") },
366
602
  { alias: "aggFidelityNotices", expression: argMaxColumn("fidelityNotices") },
367
603
  {
368
604
  alias: "aggFullSnapshotChunkIndexes",
@@ -374,6 +610,31 @@ const HEADER_DETAIL_AGGREGATES: Array<AggregatedColumn> = [
374
610
  expression: argMaxColumn("exceptionFingerprints"),
375
611
  },
376
612
  { alias: "aggClockSkewMs", expression: argMaxNumeric("clockSkewMs") },
613
+ {
614
+ alias: "aggClientReportedStart",
615
+ expression: argMaxDateTime("clientReportedStartTime"),
616
+ },
617
+ {
618
+ alias: "aggCustomEventCount",
619
+ expression: argMaxNumeric("customEventCount"),
620
+ },
621
+ { alias: "aggAttributes", expression: argMaxColumn("attributes") },
622
+ ];
623
+
624
+ /*
625
+ * The two columns under the identity ACL. Named in a statement ONLY when
626
+ * the caller has already passed canReadIdentifiedUserLabel for the
627
+ * application the statement is pinned to.
628
+ */
629
+ const IDENTITY_AGGREGATES: Array<AggregatedColumn> = [
630
+ {
631
+ alias: "aggIdentifiedUserLabel",
632
+ expression: argMaxColumn("identifiedUserLabel"),
633
+ },
634
+ {
635
+ alias: "aggIdentifiedUserTraits",
636
+ expression: argMaxColumn("identifiedUserTraits"),
637
+ },
377
638
  ];
378
639
 
379
640
  function toSelectList(columns: Array<AggregatedColumn>): string {
@@ -456,6 +717,32 @@ function readNumberArray(row: JSONObject, key: string): Array<number> {
456
717
  return numbers;
457
718
  }
458
719
 
720
+ /*
721
+ * A Map(String, String) column. ClickHouse serialises it as a JSON object;
722
+ * anything else (including an array, or a row that predates the column)
723
+ * reads as an empty map.
724
+ */
725
+ function readStringMap(row: JSONObject, key: string): Record<string, string> {
726
+ const value: unknown = row[key];
727
+ const result: Record<string, string> = {};
728
+
729
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
730
+ return result;
731
+ }
732
+
733
+ for (const entryKey of Object.keys(value as Record<string, unknown>)) {
734
+ const entry: unknown = (value as Record<string, unknown>)[entryKey];
735
+
736
+ if (typeof entry === "string") {
737
+ result[entryKey] = entry;
738
+ } else if (typeof entry === "number" || typeof entry === "boolean") {
739
+ result[entryKey] = String(entry);
740
+ }
741
+ }
742
+
743
+ return result;
744
+ }
745
+
459
746
  /*
460
747
  * Unix millis -> Date. The queries return epoch milliseconds as a Float64
461
748
  * precisely so no ClickHouse datetime string ever has to be re-parsed
@@ -466,7 +753,79 @@ function readDate(row: JSONObject, key: string): Date {
466
753
  return new Date(readNumber(row, key));
467
754
  }
468
755
 
756
+ /*
757
+ * The capability list chunk 0 declared, filtered to the vocabulary this
758
+ * build knows so a stored typo never reaches the player as a capability.
759
+ */
760
+ function readRecorderCapabilities(row: JSONObject): Array<string> {
761
+ const attributes: Record<string, string> = readStringMap(
762
+ row,
763
+ "aggAttributes",
764
+ );
765
+ const raw: string | undefined = attributes[RECORDER_CAPABILITIES_ATTRIBUTE];
766
+
767
+ if (!raw) {
768
+ return [];
769
+ }
770
+
771
+ return raw
772
+ .split(",")
773
+ .map((capability: string): string => {
774
+ return capability.trim();
775
+ })
776
+ .filter((capability: string): boolean => {
777
+ return SESSION_REPLAY_RECORDER_CAPABILITIES.includes(capability);
778
+ });
779
+ }
780
+
781
+ interface ActivitySummaryCacheEntry {
782
+ summary: SessionReplayApplicationActivitySummary;
783
+ expiresAt: number;
784
+ }
785
+
786
+ const activitySummaryCache: Map<string, ActivitySummaryCacheEntry> = new Map<
787
+ string,
788
+ ActivitySummaryCacheEntry
789
+ >();
790
+
791
+ /*
792
+ * Where the published recorder version comes from. The recorder manifest
793
+ * is read by App/FeatureSet/BrowserRecorder/Manifest.ts, which lives in
794
+ * the App tree and cannot be imported from Common; the feature set that
795
+ * mounts the read routes registers the reader at boot. Until it does, the
796
+ * ingest-status route answers null - "unknown", never a guessed version.
797
+ */
798
+ type PublishedRecorderVersionProvider = () => string | null;
799
+
800
+ let publishedRecorderVersionProvider: PublishedRecorderVersionProvider | null =
801
+ null;
802
+
469
803
  export default class SessionReplayReadService {
804
+ public static setPublishedRecorderVersionProvider(
805
+ provider: PublishedRecorderVersionProvider | null,
806
+ ): void {
807
+ publishedRecorderVersionProvider = provider;
808
+ }
809
+
810
+ public static getPublishedRecorderVersion(): string | null {
811
+ if (!publishedRecorderVersionProvider) {
812
+ return null;
813
+ }
814
+
815
+ try {
816
+ const version: string | null = publishedRecorderVersionProvider();
817
+
818
+ return typeof version === "string" && version.length > 0 ? version : null;
819
+ } catch {
820
+ return null;
821
+ }
822
+ }
823
+
824
+ /* Test seam: the summary cache is process-local. */
825
+ public static clearActivitySummaryCache(): void {
826
+ activitySummaryCache.clear();
827
+ }
828
+
470
829
  /*
471
830
  * Session list.
472
831
  *
@@ -474,7 +833,10 @@ export default class SessionReplayReadService {
474
833
  * are the first three elements of the sort key AND they are part of the
475
834
  * ReplacingMergeTree replace key, so they are byte-identical on every
476
835
  * duplicate row of a session. Filtering on them before the GROUP BY is
477
- * therefore both index-friendly and safe.
836
+ * therefore both index-friendly and safe. Nothing else is ever added to
837
+ * the WHERE: any other predicate would have to run over raw rows and
838
+ * would either match a superseded header version or force a scan that
839
+ * the (projectId, rumApplicationId, startTime) prefix cannot prune.
478
840
  *
479
841
  * Everything else is filtered in HAVING against the argMax'd value.
480
842
  * That is not a style choice: a provisional header (written on chunk 0,
@@ -492,6 +854,24 @@ export default class SessionReplayReadService {
492
854
  Math.min(request.limit, MAX_SESSION_REPLAY_LIST_LIMIT),
493
855
  );
494
856
 
857
+ const sortBy: SessionReplaySortBy = request.sortBy || "startTime";
858
+
859
+ if (!SESSION_REPLAY_SORT_BY_VALUES.includes(sortBy)) {
860
+ throw new BadDataException(
861
+ `sortBy must be one of ${SESSION_REPLAY_SORT_BY_VALUES.join(", ")}.`,
862
+ );
863
+ }
864
+
865
+ if (request.cursor && request.cursor.sortBy !== sortBy) {
866
+ /*
867
+ * A cursor is a position in ONE ordering. Applying a "most errors"
868
+ * cursor to a "newest" list would silently skip or repeat sessions.
869
+ */
870
+ throw new BadDataException(
871
+ `The cursor belongs to a list sorted by ${request.cursor.sortBy}, not ${sortBy}. Start from the first page.`,
872
+ );
873
+ }
874
+
495
875
  const selectList: string = toSelectList(HEADER_AGGREGATES);
496
876
 
497
877
  const statement: Statement = SQL`
@@ -503,9 +883,7 @@ export default class SessionReplayReadService {
503
883
  statement.append(` ${selectList}`);
504
884
 
505
885
  if (request.includeIdentifiedUserLabel) {
506
- statement.append(
507
- `,\n ${argMaxColumn("identifiedUserLabel")} AS aggIdentifiedUserLabel`,
508
- );
886
+ statement.append(`,\n ${toSelectList(IDENTITY_AGGREGATES)}`);
509
887
  }
510
888
 
511
889
  statement.append(SQL`
@@ -532,19 +910,24 @@ export default class SessionReplayReadService {
532
910
 
533
911
  /*
534
912
  * Keyset cursor. The sort key is (projectId, rumApplicationId,
535
- * startTime, sessionId) and the list is ordered by the same tuple
536
- * descending, so the previous page's last startTime is a valid
537
- * WHERE-level upper bound: it prunes granules instead of paging with
538
- * OFFSET, which on a wide time window would re-read and re-aggregate
539
- * everything already returned. The exact ties are removed by the
540
- * HAVING tiebreak below - the WHERE bound is deliberately inclusive
541
- * so a row sharing the boundary timestamp is not skipped.
913
+ * startTime, sessionId) and the newest-first list is ordered by the
914
+ * same tuple descending, so the previous page's last startTime is a
915
+ * valid WHERE-level upper bound: it prunes granules instead of paging
916
+ * with OFFSET, which on a wide time window would re-read and
917
+ * re-aggregate everything already returned. The exact ties are
918
+ * removed by the HAVING tiebreak below - the WHERE bound is
919
+ * deliberately inclusive so a row sharing the boundary timestamp is
920
+ * not skipped.
921
+ *
922
+ * Only for the startTime sort: for any other key the cursor value is
923
+ * an aggregate, and a WHERE on startTime would drop sessions that
924
+ * belong on later pages.
542
925
  */
543
- if (request.cursor) {
926
+ if (request.cursor && sortBy === "startTime") {
544
927
  statement.append(
545
928
  SQL` AND startTime <= ${{
546
929
  type: TableColumnType.DateTime64,
547
- value: new Date(request.cursor.startTimeUnixMs),
930
+ value: new Date(request.cursor.sortValue),
548
931
  }}`,
549
932
  );
550
933
  }
@@ -556,16 +939,25 @@ export default class SessionReplayReadService {
556
939
  SessionReplayReadService.appendListHavingFilters(
557
940
  statement,
558
941
  request.filters,
942
+ request.includeIdentifiedUserLabel,
559
943
  );
560
944
 
945
+ const sortExpression: string =
946
+ SessionReplayReadService.getSortExpression(sortBy);
947
+
561
948
  if (request.cursor) {
949
+ statement.append(` AND (${sortExpression} < `);
562
950
  statement.append(
563
- SQL` AND (aggStartTime < ${{
951
+ SQL`${{
564
952
  type: TableColumnType.Decimal,
565
- value: request.cursor.startTimeUnixMs,
566
- }} OR (aggStartTime = ${{
953
+ value: request.cursor.sortValue,
954
+ }}`,
955
+ );
956
+ statement.append(` OR (${sortExpression} = `);
957
+ statement.append(
958
+ SQL`${{
567
959
  type: TableColumnType.Decimal,
568
- value: request.cursor.startTimeUnixMs,
960
+ value: request.cursor.sortValue,
569
961
  }} AND sessionId < ${{
570
962
  type: TableColumnType.Text,
571
963
  value: request.cursor.sessionId,
@@ -573,12 +965,12 @@ export default class SessionReplayReadService {
573
965
  );
574
966
  }
575
967
 
968
+ statement.append(` ORDER BY ${sortExpression} DESC, sessionId DESC`);
576
969
  statement.append(
577
- SQL` ORDER BY aggStartTime DESC, sessionId DESC
578
- LIMIT ${{
579
- type: TableColumnType.Number,
580
- value: limit + 1,
581
- }}`,
970
+ SQL` LIMIT ${{
971
+ type: TableColumnType.Number,
972
+ value: limit + 1,
973
+ }}`,
582
974
  );
583
975
 
584
976
  statement.append(READ_QUERY_SETTINGS);
@@ -600,11 +992,14 @@ export default class SessionReplayReadService {
600
992
 
601
993
  const sessions: Array<SessionReplayListItem> = pageRows.map(
602
994
  (row: JSONObject): SessionReplayListItem => {
995
+ const startTime: Date = readDate(row, "aggStartTime");
996
+ const endTime: Date = readDate(row, "aggEndTime");
997
+
603
998
  const item: SessionReplayListItem = {
604
999
  sessionId: readString(row, "sessionId"),
605
1000
  rumApplicationId: readString(row, "applicationId"),
606
- startTime: readDate(row, "aggStartTime"),
607
- endTime: readDate(row, "aggEndTime"),
1001
+ startTime: startTime,
1002
+ endTime: endTime,
608
1003
  durationMs: readNumber(row, "aggDurationMs"),
609
1004
  isFinalized: readBoolean(row, "aggIsFinalized"),
610
1005
  sealedReason: readString(row, "aggSealedReason"),
@@ -632,10 +1027,28 @@ export default class SessionReplayReadService {
632
1027
  viewportHeight: readNumber(row, "aggViewportHeight"),
633
1028
  identifiedUserKey: readString(row, "aggIdentifiedUserKey"),
634
1029
  samplePercentageAtCapture: readNumber(row, "aggSamplePercentage"),
1030
+ routes: readStringArray(row, "aggRoutes").slice(0, MAX_LIST_ROUTES),
1031
+ traceCount: readNumber(row, "aggTraceCount"),
1032
+ exceptionGroupCount: readNumber(row, "aggExceptionGroupCount"),
1033
+ topExceptionFingerprint: readString(
1034
+ row,
1035
+ "aggTopExceptionFingerprint",
1036
+ ),
1037
+ clickCount: readNumber(row, "aggClickCount"),
1038
+ activeMs: readNumber(row, "aggActiveMs"),
1039
+ firstErrorOffsetMs: readNumber(row, "aggFirstErrorOffsetMs"),
1040
+ expiresAtUnixMs: readNumber(row, "aggExpiresAt"),
1041
+ tags: readStringMap(row, "aggTags"),
1042
+ startTimeUnixMs: startTime.getTime(),
1043
+ endTimeUnixMs: endTime.getTime(),
635
1044
  };
636
1045
 
637
1046
  if (request.includeIdentifiedUserLabel) {
638
1047
  item.identifiedUserLabel = readString(row, "aggIdentifiedUserLabel");
1048
+ item.identifiedUserTraits = readStringMap(
1049
+ row,
1050
+ "aggIdentifiedUserTraits",
1051
+ );
639
1052
  }
640
1053
 
641
1054
  return item;
@@ -650,7 +1063,11 @@ export default class SessionReplayReadService {
650
1063
  nextCursor:
651
1064
  hasMore && lastSession
652
1065
  ? {
653
- startTimeUnixMs: lastSession.startTime.getTime(),
1066
+ sortBy: sortBy,
1067
+ sortValue: SessionReplayReadService.getSortValue(
1068
+ sortBy,
1069
+ lastSession,
1070
+ ),
654
1071
  sessionId: lastSession.sessionId,
655
1072
  }
656
1073
  : null,
@@ -663,14 +1080,22 @@ export default class SessionReplayReadService {
663
1080
  *
664
1081
  * This is also what resolves a sessionId to its owning RUM application
665
1082
  * for the handler-level authorization check, which is why it is keyed
666
- * on (projectId, sessionId) only and never accepts an application id
667
- * from the caller: an application id supplied in the request body would
668
- * make the check circular.
1083
+ * on (projectId, sessionId) and the optional rumApplicationId is a
1084
+ * DISAMBIGUATOR, never a substitute for the check: a supplied id only
1085
+ * narrows which header row is read, and the handler still authorizes
1086
+ * the application that row names.
669
1087
  */
670
1088
  @CaptureSpan()
671
1089
  public static async getSessionHeader(data: {
672
1090
  projectId: ObjectID;
673
1091
  sessionId: string;
1092
+ /*
1093
+ * Which application's recording to read when the same browser-minted
1094
+ * sessionId was recorded under more than one application (an
1095
+ * appIdentifier rename, two apps on one origin). Without it an
1096
+ * ambiguous id is refused.
1097
+ */
1098
+ rumApplicationId?: ObjectID | undefined;
674
1099
  }): Promise<SessionReplaySessionHeader | null> {
675
1100
  const selectList: string = toSelectList([
676
1101
  ...HEADER_AGGREGATES,
@@ -698,6 +1123,15 @@ export default class SessionReplayReadService {
698
1123
  }}
699
1124
  `);
700
1125
 
1126
+ if (data.rumApplicationId) {
1127
+ statement.append(
1128
+ SQL` AND rumApplicationId = ${{
1129
+ type: TableColumnType.ObjectID,
1130
+ value: data.rumApplicationId,
1131
+ }}`,
1132
+ );
1133
+ }
1134
+
701
1135
  statement.append(RETENTION_FILTER);
702
1136
 
703
1137
  /*
@@ -711,9 +1145,10 @@ export default class SessionReplayReadService {
711
1145
  * sessionId share a key space. Picking the newest group would let
712
1146
  * anyone who can write to application A resolve a sessionId belonging
713
1147
  * to application B onto their own application and pass the label
714
- * check. An ambiguous sessionId is refused outright instead: it is
715
- * either an attack or a collision, and neither has a correct
716
- * recording to return.
1148
+ * check. An ambiguous sessionId is refused outright unless the caller
1149
+ * named the application it wants (which is then authorized on its own
1150
+ * merits): it is either an attack or a collision, and neither has a
1151
+ * single correct recording to return.
717
1152
  */
718
1153
  statement.append(
719
1154
  " GROUP BY projectId, rumApplicationId, sessionId ORDER BY aggStartTime DESC LIMIT 2",
@@ -730,7 +1165,7 @@ export default class SessionReplayReadService {
730
1165
 
731
1166
  if (rows.length > 1) {
732
1167
  throw new BadDataException(
733
- "This session id resolves to more than one RUM application and cannot be played back.",
1168
+ "This session id was recorded under more than one application in this project. Open it from the session list of the application you want to watch, which passes rumApplicationId to choose the recording.",
734
1169
  );
735
1170
  }
736
1171
 
@@ -740,12 +1175,15 @@ export default class SessionReplayReadService {
740
1175
  return null;
741
1176
  }
742
1177
 
1178
+ const startTime: Date = readDate(row, "aggStartTime");
1179
+ const endTime: Date = readDate(row, "aggEndTime");
1180
+
743
1181
  return {
744
1182
  sessionId: readString(row, "sessionId"),
745
1183
  projectId: readString(row, "headerProjectId"),
746
1184
  rumApplicationId: readString(row, "applicationId"),
747
- startTime: readDate(row, "aggStartTime"),
748
- endTime: readDate(row, "aggEndTime"),
1185
+ startTime: startTime,
1186
+ endTime: endTime,
749
1187
  durationMs: readNumber(row, "aggDurationMs"),
750
1188
  isFinalized: readBoolean(row, "aggIsFinalized"),
751
1189
  sealedReason: readString(row, "aggSealedReason"),
@@ -787,57 +1225,43 @@ export default class SessionReplayReadService {
787
1225
  traceIds: readStringArray(row, "aggTraceIds"),
788
1226
  exceptionFingerprints: readStringArray(row, "aggExceptionFingerprints"),
789
1227
  clockSkewMs: readNumber(row, "aggClockSkewMs"),
1228
+ startTimeUnixMs: startTime.getTime(),
1229
+ endTimeUnixMs: endTime.getTime(),
1230
+ clientReportedStartUnixMs: readNumber(row, "aggClientReportedStart"),
1231
+ tags: readStringMap(row, "aggTags"),
1232
+ expiresAtUnixMs: readNumber(row, "aggExpiresAt"),
1233
+ clickCount: readNumber(row, "aggClickCount"),
1234
+ customEventCount: readNumber(row, "aggCustomEventCount"),
1235
+ activeMs: readNumber(row, "aggActiveMs"),
1236
+ firstErrorOffsetMs: readNumber(row, "aggFirstErrorOffsetMs"),
1237
+ recorderCapabilities: readRecorderCapabilities(row),
790
1238
  };
791
1239
  }
792
1240
 
793
1241
  /*
794
- * Playback manifest: everything the player needs to draw a complete,
795
- * honest timeline without fetching one payload byte.
796
- *
797
- * The `payload` column is deliberately absent from this SELECT. That is
798
- * the entire performance story of the feature: a 14-chunk session is
799
- * one 128-row granule of a handful of narrow columns (~2 KB) instead of
800
- * megabytes of decompressed recording.
1242
+ * The identity columns for one session, pinned to the application the
1243
+ * caller was authorized against. This is the ONLY statement outside the
1244
+ * identity-gated list projection that names identifiedUserLabel or
1245
+ * identifiedUserTraits, and the manifest handler calls it strictly
1246
+ * after canReadIdentifiedUserLabel has passed for this application. It
1247
+ * is a separate, tiny read rather than two more columns on
1248
+ * getSessionHeader because the header is resolved BEFORE the
1249
+ * application (and therefore the identity decision) is known.
801
1250
  */
802
1251
  @CaptureSpan()
803
- public static async getManifest(data: {
804
- header: SessionReplaySessionHeader;
1252
+ public static async getSessionIdentity(data: {
805
1253
  projectId: ObjectID;
806
- /*
807
- * The application the caller was actually authorized against, always
808
- * the one resolved from the session header server-side. Every chunk
809
- * read is pinned to it: the chunk table's replace key does not
810
- * include rumApplicationId, so (projectId, sessionId) alone is not a
811
- * tenant-safe key once a sessionId can be reused across
812
- * applications.
813
- */
814
1254
  rumApplicationId: ObjectID;
815
1255
  sessionId: string;
816
- }): Promise<SessionReplayManifest> {
817
- /*
818
- * LIMIT 1 BY (tabId, chunkIndex) after ORDER BY ... version DESC
819
- * keeps exactly the highest-version row per chunk. tabId is part of
820
- * the group because chunkIndex is minted per tab. Ordering by
821
- * tabId/chunkIndex first (rather than by version alone) leaves the
822
- * output already sorted for the caller - LIMIT BY runs after ORDER
823
- * BY, so the version DESC tiebreak still selects the right row.
824
- */
1256
+ }): Promise<SessionReplaySessionIdentity> {
825
1257
  const statement: Statement = SQL`
826
1258
  SELECT
827
- tabId,
828
- chunkIndex,
829
- chunkStartOffsetMs,
830
- chunkEndOffsetMs,
831
- eventCount,
832
- hasFullSnapshot,
833
- toFloat64(payloadBytes) AS chunkPayloadBytes,
834
- errorCount,
835
- rageClickCount,
836
- deadClickCount,
837
- errorClickCount,
838
- refreshRageCount,
839
- routeCount
840
- FROM ${AnalyticsTableName.RumSessionChunk}
1259
+ `;
1260
+
1261
+ statement.append(` ${toSelectList(IDENTITY_AGGREGATES)}`);
1262
+
1263
+ statement.append(SQL`
1264
+ FROM ${AnalyticsTableName.RumSession}
841
1265
  WHERE projectId = ${{
842
1266
  type: TableColumnType.ObjectID,
843
1267
  value: data.projectId,
@@ -850,55 +1274,221 @@ export default class SessionReplayReadService {
850
1274
  type: TableColumnType.Text,
851
1275
  value: data.sessionId,
852
1276
  }}
853
- `;
1277
+ `);
854
1278
 
855
1279
  statement.append(RETENTION_FILTER);
856
-
857
1280
  statement.append(
858
- SQL` ORDER BY tabId ASC, chunkIndex ASC, version DESC
859
- LIMIT 1 BY tabId, chunkIndex
860
- LIMIT ${{
861
- type: TableColumnType.Number,
862
- value: MAX_MANIFEST_ROWS,
863
- }}`,
1281
+ " GROUP BY projectId, rumApplicationId, sessionId LIMIT 1",
864
1282
  );
865
-
866
1283
  statement.append(READ_QUERY_SETTINGS);
867
1284
 
868
- const dbResult: Results =
869
- await RumSessionChunkService.executeQuery(statement);
1285
+ const dbResult: Results = await RumSessionService.executeQuery(statement);
870
1286
  const response: DbJSONResponse = await dbResult.json<{
871
1287
  data?: Array<JSONObject>;
872
1288
  }>();
873
1289
 
874
- const rows: Array<JSONObject> = response.data || [];
875
-
876
- const tabsById: Map<
877
- string,
878
- Array<SessionReplayChunkManifestEntry>
879
- > = new Map<string, Array<SessionReplayChunkManifestEntry>>();
880
-
881
- for (const row of rows) {
882
- const tabId: string = readString(row, "tabId");
1290
+ const row: JSONObject | undefined = (response.data || [])[0];
883
1291
 
884
- const entry: SessionReplayChunkManifestEntry = {
885
- chunkIndex: readNumber(row, "chunkIndex"),
886
- tabId: tabId,
887
- chunkStartOffsetMs: readNumber(row, "chunkStartOffsetMs"),
888
- chunkEndOffsetMs: readNumber(row, "chunkEndOffsetMs"),
889
- eventCount: readNumber(row, "eventCount"),
890
- hasFullSnapshot: readBoolean(row, "hasFullSnapshot"),
891
- payloadBytes: readNumber(row, "chunkPayloadBytes"),
892
- errorCount: readNumber(row, "errorCount"),
893
- rageClickCount: readNumber(row, "rageClickCount"),
894
- deadClickCount: readNumber(row, "deadClickCount"),
895
- errorClickCount: readNumber(row, "errorClickCount"),
896
- refreshRageCount: readNumber(row, "refreshRageCount"),
897
- routeCount: readNumber(row, "routeCount"),
898
- };
1292
+ if (!row) {
1293
+ return { identifiedUserLabel: "", identifiedUserTraits: {} };
1294
+ }
899
1295
 
900
- const existing: Array<SessionReplayChunkManifestEntry> | undefined =
901
- tabsById.get(tabId);
1296
+ return {
1297
+ identifiedUserLabel: readString(row, "aggIdentifiedUserLabel"),
1298
+ identifiedUserTraits: readStringMap(row, "aggIdentifiedUserTraits"),
1299
+ };
1300
+ }
1301
+
1302
+ /*
1303
+ * For a sessionId that getSessionHeader could not find: did a header
1304
+ * ever exist, and when did (or does) it expire? Runs WITHOUT the
1305
+ * retention filter, which is safe only because it returns dates and an
1306
+ * application id and never a row's content - it lets the handler say
1307
+ * "this recording expired on <date>" instead of "not found".
1308
+ *
1309
+ * null when no row exists at all (never recorded, or already dropped by
1310
+ * the ClickHouse TTL, or erased).
1311
+ */
1312
+ @CaptureSpan()
1313
+ public static async getExpiredSessionInfo(data: {
1314
+ projectId: ObjectID;
1315
+ sessionId: string;
1316
+ rumApplicationId?: ObjectID | undefined;
1317
+ }): Promise<SessionReplayExpiredSessionInfo | null> {
1318
+ const statement: Statement = SQL`
1319
+ SELECT
1320
+ toString(rumApplicationId) AS applicationId,
1321
+ toFloat64(toUnixTimestamp(max(retentionDate))) * 1000 AS expiresAtUnixMs,
1322
+ toFloat64(toUnixTimestamp64Milli(min(startTime))) AS startTimeUnixMs
1323
+ FROM ${AnalyticsTableName.RumSession}
1324
+ WHERE projectId = ${{
1325
+ type: TableColumnType.ObjectID,
1326
+ value: data.projectId,
1327
+ }}
1328
+ AND sessionId = ${{
1329
+ type: TableColumnType.Text,
1330
+ value: data.sessionId,
1331
+ }}
1332
+ `;
1333
+
1334
+ if (data.rumApplicationId) {
1335
+ statement.append(
1336
+ SQL` AND rumApplicationId = ${{
1337
+ type: TableColumnType.ObjectID,
1338
+ value: data.rumApplicationId,
1339
+ }}`,
1340
+ );
1341
+ }
1342
+
1343
+ statement.append(
1344
+ " GROUP BY rumApplicationId ORDER BY expiresAtUnixMs DESC LIMIT 1",
1345
+ );
1346
+ statement.append(READ_QUERY_SETTINGS);
1347
+
1348
+ const dbResult: Results = await RumSessionService.executeQuery(statement);
1349
+ const response: DbJSONResponse = await dbResult.json<{
1350
+ data?: Array<JSONObject>;
1351
+ }>();
1352
+
1353
+ const row: JSONObject | undefined = (response.data || [])[0];
1354
+
1355
+ if (!row) {
1356
+ return null;
1357
+ }
1358
+
1359
+ return {
1360
+ rumApplicationId: readString(row, "applicationId"),
1361
+ startTime: readDate(row, "startTimeUnixMs"),
1362
+ expiresAt: readDate(row, "expiresAtUnixMs"),
1363
+ };
1364
+ }
1365
+
1366
+ /*
1367
+ * Playback manifest: everything the player needs to draw a complete,
1368
+ * honest timeline without fetching one payload byte.
1369
+ *
1370
+ * The `payload` column is deliberately absent from this SELECT. That is
1371
+ * the entire performance story of the feature: a 14-chunk session is
1372
+ * one 128-row granule of a handful of narrow columns (~2 KB) instead of
1373
+ * megabytes of decompressed recording.
1374
+ */
1375
+ @CaptureSpan()
1376
+ public static async getManifest(data: {
1377
+ header: SessionReplaySessionHeader;
1378
+ projectId: ObjectID;
1379
+ /*
1380
+ * The application the caller was actually authorized against, always
1381
+ * the one resolved from the session header server-side. Every chunk
1382
+ * read is pinned to it: the chunk table's replace key does not
1383
+ * include rumApplicationId, so (projectId, sessionId) alone is not a
1384
+ * tenant-safe key once a sessionId can be reused across
1385
+ * applications.
1386
+ */
1387
+ rumApplicationId: ObjectID;
1388
+ sessionId: string;
1389
+ }): Promise<SessionReplayManifest> {
1390
+ /*
1391
+ * LIMIT 1 BY (tabId, chunkIndex) after ORDER BY ... version DESC
1392
+ * keeps exactly the highest-version row per chunk. tabId is part of
1393
+ * the group because chunkIndex is minted per tab. Ordering by
1394
+ * tabId/chunkIndex first (rather than by version alone) leaves the
1395
+ * output already sorted for the caller - LIMIT BY runs after ORDER
1396
+ * BY, so the version DESC tiebreak still selects the right row.
1397
+ *
1398
+ * clickCount and url are narrow columns: the activity lane and the
1399
+ * URL bar read them before any chunk is decoded. payloadBytes stays
1400
+ * the WIRE size the recorder posted; the stored size is only ever
1401
+ * measured by getChunks, which is the read the cap actually bounds.
1402
+ */
1403
+ const statement: Statement = SQL`
1404
+ SELECT
1405
+ tabId,
1406
+ chunkIndex,
1407
+ chunkStartOffsetMs,
1408
+ chunkEndOffsetMs,
1409
+ eventCount,
1410
+ hasFullSnapshot,
1411
+ toFloat64(payloadBytes) AS chunkPayloadBytes,
1412
+ errorCount,
1413
+ rageClickCount,
1414
+ deadClickCount,
1415
+ errorClickCount,
1416
+ refreshRageCount,
1417
+ routeCount,
1418
+ clickCount,
1419
+ url
1420
+ FROM ${AnalyticsTableName.RumSessionChunk}
1421
+ WHERE projectId = ${{
1422
+ type: TableColumnType.ObjectID,
1423
+ value: data.projectId,
1424
+ }}
1425
+ AND rumApplicationId = ${{
1426
+ type: TableColumnType.ObjectID,
1427
+ value: data.rumApplicationId,
1428
+ }}
1429
+ AND sessionId = ${{
1430
+ type: TableColumnType.Text,
1431
+ value: data.sessionId,
1432
+ }}
1433
+ `;
1434
+
1435
+ statement.append(RETENTION_FILTER);
1436
+
1437
+ statement.append(
1438
+ SQL` ORDER BY tabId ASC, chunkIndex ASC, version DESC
1439
+ LIMIT 1 BY tabId, chunkIndex
1440
+ LIMIT ${{
1441
+ type: TableColumnType.Number,
1442
+ value: MAX_MANIFEST_ROWS,
1443
+ }}`,
1444
+ );
1445
+
1446
+ statement.append(READ_QUERY_SETTINGS);
1447
+
1448
+ const dbResult: Results =
1449
+ await RumSessionChunkService.executeQuery(statement);
1450
+ const response: DbJSONResponse = await dbResult.json<{
1451
+ data?: Array<JSONObject>;
1452
+ }>();
1453
+
1454
+ const rows: Array<JSONObject> = response.data || [];
1455
+
1456
+ const tabsById: Map<
1457
+ string,
1458
+ Array<SessionReplayChunkManifestEntry>
1459
+ > = new Map<string, Array<SessionReplayChunkManifestEntry>>();
1460
+
1461
+ let liveDurationMs: number = 0;
1462
+ let liveEventCount: number = 0;
1463
+ let liveMaxChunkIndex: number = 0;
1464
+
1465
+ for (const row of rows) {
1466
+ const tabId: string = readString(row, "tabId");
1467
+
1468
+ const entry: SessionReplayChunkManifestEntry = {
1469
+ chunkIndex: readNumber(row, "chunkIndex"),
1470
+ tabId: tabId,
1471
+ chunkStartOffsetMs: readNumber(row, "chunkStartOffsetMs"),
1472
+ chunkEndOffsetMs: readNumber(row, "chunkEndOffsetMs"),
1473
+ eventCount: readNumber(row, "eventCount"),
1474
+ hasFullSnapshot: readBoolean(row, "hasFullSnapshot"),
1475
+ payloadBytes: readNumber(row, "chunkPayloadBytes"),
1476
+ errorCount: readNumber(row, "errorCount"),
1477
+ rageClickCount: readNumber(row, "rageClickCount"),
1478
+ deadClickCount: readNumber(row, "deadClickCount"),
1479
+ errorClickCount: readNumber(row, "errorClickCount"),
1480
+ refreshRageCount: readNumber(row, "refreshRageCount"),
1481
+ routeCount: readNumber(row, "routeCount"),
1482
+ clickCount: readNumber(row, "clickCount"),
1483
+ url: readString(row, "url"),
1484
+ };
1485
+
1486
+ liveDurationMs = Math.max(liveDurationMs, entry.chunkEndOffsetMs);
1487
+ liveEventCount += entry.eventCount;
1488
+ liveMaxChunkIndex = Math.max(liveMaxChunkIndex, entry.chunkIndex);
1489
+
1490
+ const existing: Array<SessionReplayChunkManifestEntry> | undefined =
1491
+ tabsById.get(tabId);
902
1492
 
903
1493
  if (existing) {
904
1494
  existing.push(entry);
@@ -944,107 +1534,95 @@ export default class SessionReplayReadService {
944
1534
  },
945
1535
  0,
946
1536
  ),
1537
+ firstChunkStartOffsetMs: entries.reduce(
1538
+ (min: number, entry: SessionReplayChunkManifestEntry): number => {
1539
+ return Math.min(min, entry.chunkStartOffsetMs);
1540
+ },
1541
+ Number.POSITIVE_INFINITY,
1542
+ ),
947
1543
  });
948
1544
  }
949
1545
 
1546
+ for (const tab of tabs) {
1547
+ if (!Number.isFinite(tab.firstChunkStartOffsetMs)) {
1548
+ tab.firstChunkStartOffsetMs = 0;
1549
+ }
1550
+ }
1551
+
950
1552
  return {
951
- header: data.header,
1553
+ header: SessionReplayReadService.reconcileLiveHeader({
1554
+ header: data.header,
1555
+ chunkRowCount: rows.length,
1556
+ liveDurationMs: liveDurationMs,
1557
+ liveEventCount: liveEventCount,
1558
+ liveMaxChunkIndex: liveMaxChunkIndex,
1559
+ }),
952
1560
  tabs: tabs,
953
1561
  isChunkIndexTruncated: rows.length >= MAX_MANIFEST_ROWS,
954
1562
  };
955
1563
  }
956
1564
 
957
1565
  /*
958
- * Total STORED bytes for a specific set of chunks, without shipping the
959
- * payload column to the application.
960
- *
961
- * `length(payload)`, deliberately NOT `payloadBytes`. Those are two
962
- * different quantities: payloadBytes is the post-gzip WIRE size the
963
- * recorder uploaded (the metering signal), while the payload column
964
- * holds the DECOMPRESSED JSON and is what this endpoint actually
965
- * returns. rrweb JSON gzips 10-20x, so a cap applied to payloadBytes
966
- * bounds a number an order of magnitude smaller than the response and
967
- * therefore bounds nothing useful.
968
- *
969
- * The cost is that ClickHouse has to decompress the column to measure
970
- * it, which is exactly what naming `payload` was meant to avoid. It is
971
- * still worth doing before the read rather than after: the bytes are
972
- * measured inside ClickHouse and never cross the wire, and the marks
973
- * the following read needs are warm by the time it runs.
1566
+ * A provisional header (isFinalized false) says durationMs 0, chunkCount
1567
+ * 0 and eventCount 0 while its chunk rows say otherwise; the manifest
1568
+ * has just read every chunk row, so it reports what the rows prove. A
1569
+ * finalized header is authoritative and returned untouched.
974
1570
  */
975
- @CaptureSpan()
976
- public static async getChunkStoredBytes(data: {
977
- projectId: ObjectID;
978
- rumApplicationId: ObjectID;
979
- sessionId: string;
980
- tabId: string;
981
- chunkIndexes: Array<number>;
982
- }): Promise<number> {
983
- if (data.chunkIndexes.length === 0) {
984
- return 0;
1571
+ private static reconcileLiveHeader(data: {
1572
+ header: SessionReplaySessionHeader;
1573
+ chunkRowCount: number;
1574
+ liveDurationMs: number;
1575
+ liveEventCount: number;
1576
+ liveMaxChunkIndex: number;
1577
+ }): SessionReplaySessionHeader {
1578
+ if (data.header.isFinalized || data.chunkRowCount === 0) {
1579
+ return data.header;
985
1580
  }
986
1581
 
987
- const statement: Statement = SQL`
988
- SELECT
989
- chunkIndex,
990
- toFloat64(length(payload)) AS chunkStoredBytes
991
- FROM ${AnalyticsTableName.RumSessionChunk}
992
- WHERE projectId = ${{
993
- type: TableColumnType.ObjectID,
994
- value: data.projectId,
995
- }}
996
- AND rumApplicationId = ${{
997
- type: TableColumnType.ObjectID,
998
- value: data.rumApplicationId,
999
- }}
1000
- AND sessionId = ${{
1001
- type: TableColumnType.Text,
1002
- value: data.sessionId,
1003
- }}
1004
- AND tabId = ${{
1005
- type: TableColumnType.Text,
1006
- value: data.tabId,
1007
- }}
1008
- AND chunkIndex IN (${{
1009
- type: TableColumnType.Number,
1010
- value: new Includes(data.chunkIndexes),
1011
- }})
1012
- `;
1013
-
1014
- statement.append(RETENTION_FILTER);
1015
-
1016
- /*
1017
- * Deduplicated exactly like the payload read, so the pre-check and
1018
- * the read it guards can never disagree about which rows count.
1019
- * Summed in TypeScript rather than in SQL because the row count is
1020
- * bounded by MAX_SESSION_REPLAY_CHUNKS_PER_READ, and a wrapping
1021
- * aggregate would need a subquery whose LIMIT BY semantics are
1022
- * easier to get subtly wrong than to read.
1023
- */
1024
- statement.append(
1025
- " ORDER BY chunkIndex ASC, version DESC LIMIT 1 BY chunkIndex",
1582
+ const durationMs: number = Math.max(
1583
+ data.header.durationMs,
1584
+ data.liveDurationMs,
1026
1585
  );
1027
-
1028
- statement.append(READ_QUERY_SETTINGS);
1029
-
1030
- const dbResult: Results =
1031
- await RumSessionChunkService.executeQuery(statement);
1032
- const response: DbJSONResponse = await dbResult.json<{
1033
- data?: Array<JSONObject>;
1034
- }>();
1035
-
1036
- return (response.data || []).reduce(
1037
- (total: number, row: JSONObject): number => {
1038
- return total + readNumber(row, "chunkStoredBytes");
1039
- },
1040
- 0,
1586
+ const endTimeUnixMs: number = Math.max(
1587
+ data.header.endTimeUnixMs,
1588
+ data.header.startTimeUnixMs + durationMs,
1041
1589
  );
1590
+
1591
+ return {
1592
+ ...data.header,
1593
+ durationMs: durationMs,
1594
+ endTime: new Date(endTimeUnixMs),
1595
+ endTimeUnixMs: endTimeUnixMs,
1596
+ chunkCount: Math.max(data.header.chunkCount, data.chunkRowCount),
1597
+ eventCount: Math.max(data.header.eventCount, data.liveEventCount),
1598
+ maxChunkIndex: Math.max(
1599
+ data.header.maxChunkIndex,
1600
+ data.liveMaxChunkIndex,
1601
+ ),
1602
+ };
1042
1603
  }
1043
1604
 
1044
1605
  /*
1045
1606
  * The payload read. The only query in the system that names the
1046
1607
  * `payload` column.
1047
1608
  *
1609
+ * The byte cap is measured on `length(payload)` - the DECOMPRESSED
1610
+ * stored JSON that is actually returned - in the SAME statement that
1611
+ * ships the bytes, so the column is decompressed once per page rather
1612
+ * than once for a pre-check and again for the read. `payloadBytes` is
1613
+ * the post-gzip WIRE size the recorder uploaded; rrweb JSON gzips
1614
+ * 10-20x, so a cap on it bounds a number an order of magnitude smaller
1615
+ * than the response and therefore bounds nothing useful.
1616
+ *
1617
+ * Prefix semantics rather than refusal. A page that does not fit is
1618
+ * answered with the longest prefix of whole chunks that does, and ALWAYS
1619
+ * with at least the first chunk: the ingest cap
1620
+ * (SESSION_REPLAY_MAX_DECOMPRESSED_FRAME_BYTES) already bounds a single
1621
+ * frame, so a lone chunk can never exceed what the ingest let in, and a
1622
+ * single oversized snapshot that could never be served would dead-end
1623
+ * playback at that chunk forever. The player plans pages against the
1624
+ * wire size it has, requests, and reads back whichever chunks arrived.
1625
+ *
1048
1626
  * Both caps are enforced here rather than only at the route so no
1049
1627
  * future caller can reach the payload column without them.
1050
1628
  */
@@ -1055,9 +1633,9 @@ export default class SessionReplayReadService {
1055
1633
  sessionId: string;
1056
1634
  tabId: string;
1057
1635
  chunkIndexes: Array<number>;
1058
- }): Promise<Array<SessionReplayChunkPayload>> {
1636
+ }): Promise<SessionReplayChunkReadResult> {
1059
1637
  if (data.chunkIndexes.length === 0) {
1060
- return [];
1638
+ return { chunks: [], omittedChunkIndexes: [] };
1061
1639
  }
1062
1640
 
1063
1641
  if (data.chunkIndexes.length > MAX_SESSION_REPLAY_CHUNKS_PER_READ) {
@@ -1066,52 +1644,69 @@ export default class SessionReplayReadService {
1066
1644
  );
1067
1645
  }
1068
1646
 
1069
- const totalBytes: number =
1070
- await SessionReplayReadService.getChunkStoredBytes(data);
1071
-
1072
- if (totalBytes > MAX_SESSION_REPLAY_READ_BYTES) {
1073
- throw new BadDataException(
1074
- `The requested chunks total ${totalBytes} bytes, which exceeds the ${MAX_SESSION_REPLAY_READ_BYTES} byte limit for a single read. Request fewer chunks.`,
1075
- );
1076
- }
1077
-
1647
+ /*
1648
+ * Innermost: the de-duplicated rows. A retried delivery is two
1649
+ * physically present rows on a ReplacingMergeTree until a merge runs.
1650
+ * Feeding both to the player would replay the same mutations twice,
1651
+ * which rrweb resolves against node ids and would either throw or
1652
+ * render a DOM that never existed.
1653
+ *
1654
+ * Middle: a running total of stored bytes in chunk order. Outermost:
1655
+ * a row is SERVED when the total up to and including it is under the
1656
+ * cap, or when it is the first row; a row that is not served keeps
1657
+ * its index (so the caller can name what was omitted) but ships an
1658
+ * empty payload, so the bytes crossing the wire are bounded inside
1659
+ * ClickHouse and never in the application.
1660
+ *
1661
+ * The outer projection is aliased servedPayload rather than payload:
1662
+ * an alias that names the column its own expression reads is a
1663
+ * cyclic alias to ClickHouse.
1664
+ */
1078
1665
  const statement: Statement = SQL`
1079
1666
  SELECT
1080
1667
  chunkIndex,
1081
- payload
1082
- FROM ${AnalyticsTableName.RumSessionChunk}
1083
- WHERE projectId = ${{
1084
- type: TableColumnType.ObjectID,
1085
- value: data.projectId,
1086
- }}
1087
- AND rumApplicationId = ${{
1088
- type: TableColumnType.ObjectID,
1089
- value: data.rumApplicationId,
1090
- }}
1091
- AND sessionId = ${{
1092
- type: TableColumnType.Text,
1093
- value: data.sessionId,
1094
- }}
1095
- AND tabId = ${{
1096
- type: TableColumnType.Text,
1097
- value: data.tabId,
1098
- }}
1099
- AND chunkIndex IN (${{
1100
- type: TableColumnType.Number,
1101
- value: new Includes(data.chunkIndexes),
1102
- }})
1668
+ if(isServed, payload, '') AS servedPayload,
1669
+ isServed
1670
+ FROM (
1671
+ SELECT
1672
+ chunkIndex,
1673
+ payload,
1674
+ (sum(length(payload)) OVER (ORDER BY chunkIndex ASC ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) <= ${{
1675
+ type: TableColumnType.Decimal,
1676
+ value: MAX_SESSION_REPLAY_READ_BYTES,
1677
+ }}
1678
+ OR row_number() OVER (ORDER BY chunkIndex ASC) = 1) AS isServed
1679
+ FROM (
1680
+ SELECT
1681
+ chunkIndex,
1682
+ payload
1683
+ FROM ${AnalyticsTableName.RumSessionChunk}
1684
+ WHERE projectId = ${{
1685
+ type: TableColumnType.ObjectID,
1686
+ value: data.projectId,
1687
+ }}
1688
+ AND rumApplicationId = ${{
1689
+ type: TableColumnType.ObjectID,
1690
+ value: data.rumApplicationId,
1691
+ }}
1692
+ AND sessionId = ${{
1693
+ type: TableColumnType.Text,
1694
+ value: data.sessionId,
1695
+ }}
1696
+ AND tabId = ${{
1697
+ type: TableColumnType.Text,
1698
+ value: data.tabId,
1699
+ }}
1700
+ AND chunkIndex IN (${{
1701
+ type: TableColumnType.Number,
1702
+ value: new Includes(data.chunkIndexes),
1703
+ }})
1103
1704
  `;
1104
1705
 
1105
1706
  statement.append(RETENTION_FILTER);
1106
1707
 
1107
- /*
1108
- * A retried delivery is two physically present rows on a
1109
- * ReplacingMergeTree until a merge runs. Feeding both to the player
1110
- * would replay the same mutations twice, which rrweb resolves against
1111
- * node ids and would either throw or render a DOM that never existed.
1112
- */
1113
1708
  statement.append(
1114
- " ORDER BY chunkIndex ASC, version DESC LIMIT 1 BY chunkIndex",
1709
+ " ORDER BY chunkIndex ASC, version DESC LIMIT 1 BY chunkIndex\n )\n )\n ORDER BY chunkIndex ASC",
1115
1710
  );
1116
1711
 
1117
1712
  statement.append(READ_QUERY_SETTINGS);
@@ -1124,39 +1719,58 @@ export default class SessionReplayReadService {
1124
1719
 
1125
1720
  /*
1126
1721
  * The cap is re-applied to the bytes actually being handed back, not
1127
- * only to what the pre-check believed. The pre-check reads a
1128
- * different snapshot of a ReplacingMergeTree than the read it guards,
1129
- * and any future change to how stored size is derived would otherwise
1130
- * silently unbound the response. Accumulated as the rows are mapped
1131
- * so an oversized read fails before the caller ever holds the whole
1132
- * set.
1722
+ * only to what ClickHouse computed: any future change to how stored
1723
+ * size is derived would otherwise silently unbound the response. The
1724
+ * prefix is also re-established here - a served row behind an
1725
+ * unserved one would be a hole the player cannot play across, so the
1726
+ * served set stops at the first omission.
1133
1727
  */
1134
1728
  let totalReturnedBytes: number = 0;
1135
1729
  const chunks: Array<SessionReplayChunkPayload> = [];
1730
+ const omittedChunkIndexes: Array<number> = [];
1136
1731
 
1137
- for (const row of response.data || []) {
1138
- const payload: string = readString(row, "payload");
1732
+ const chunkRows: Array<JSONObject> = response.data || [];
1139
1733
 
1140
- totalReturnedBytes += Buffer.byteLength(payload, "utf8");
1734
+ for (const row of chunkRows) {
1735
+ const chunkIndex: number = readNumber(row, "chunkIndex");
1736
+ const payload: string = readString(row, "servedPayload");
1737
+ const isServed: boolean =
1738
+ row["isServed"] === undefined ? true : readBoolean(row, "isServed");
1141
1739
 
1142
- if (totalReturnedBytes > MAX_SESSION_REPLAY_READ_BYTES) {
1143
- throw new BadDataException(
1144
- `The requested chunks exceed the ${MAX_SESSION_REPLAY_READ_BYTES} byte limit for a single read. Request fewer chunks.`,
1145
- );
1740
+ const payloadBytes: number = Buffer.byteLength(payload, "utf8");
1741
+
1742
+ const fits: boolean =
1743
+ chunks.length === 0 ||
1744
+ totalReturnedBytes + payloadBytes <= MAX_SESSION_REPLAY_READ_BYTES;
1745
+
1746
+ if (!isServed || !fits || omittedChunkIndexes.length > 0) {
1747
+ omittedChunkIndexes.push(chunkIndex);
1748
+ continue;
1146
1749
  }
1147
1750
 
1751
+ totalReturnedBytes += payloadBytes;
1752
+
1148
1753
  chunks.push({
1149
- chunkIndex: readNumber(row, "chunkIndex"),
1754
+ chunkIndex: chunkIndex,
1150
1755
  payload: payload,
1151
1756
  });
1152
1757
  }
1153
1758
 
1154
- return chunks;
1759
+ return { chunks: chunks, omittedChunkIndexes: omittedChunkIndexes };
1155
1760
  }
1156
1761
 
1157
1762
  /*
1158
1763
  * Sessions that observed a given exception fingerprint.
1159
1764
  *
1765
+ * Two sources, one header query. The header's exceptionFingerprints
1766
+ * array is written by the finalizer, so for the first 10+ minutes after
1767
+ * the error - the whole incident, from the reporter's point of view -
1768
+ * the session's header knows nothing about it. The exception instance
1769
+ * table, however, carries the session id of the page that threw, from
1770
+ * the moment the exception is ingested. Those ids are looked up first
1771
+ * (cheap: bloom-indexed fingerprint, bounded window) and OR-ed into the
1772
+ * header predicate, so a live session is found as soon as its error is.
1773
+ *
1160
1774
  * hasAny() appears twice on purpose. In the WHERE it is a bloom-pruned
1161
1775
  * pre-filter over physical rows; a group survives it if ANY of its rows
1162
1776
  * carries the fingerprint, which necessarily includes the case where
@@ -1164,6 +1778,10 @@ export default class SessionReplayReadService {
1164
1778
  * drop a true match. The HAVING then re-checks the argMax'd array so a
1165
1779
  * fingerprint present only on a superseded row does not produce a false
1166
1780
  * positive.
1781
+ *
1782
+ * Always windowed. RumSession is partitioned by day, so without a
1783
+ * window this scanned every partition the project ever wrote on every
1784
+ * exception page load.
1167
1785
  */
1168
1786
  @CaptureSpan()
1169
1787
  public static async getSessionsForException(data: {
@@ -1178,6 +1796,8 @@ export default class SessionReplayReadService {
1178
1796
  accessibleRumApplicationIds: Array<ObjectID> | null;
1179
1797
  startTime?: Date | undefined;
1180
1798
  endTime?: Date | undefined;
1799
+ /* Pin to the one session the caller already knows threw. */
1800
+ sessionId?: string | undefined;
1181
1801
  limit: number;
1182
1802
  }): Promise<Array<SessionReplayExceptionSession>> {
1183
1803
  if (
@@ -1192,10 +1812,27 @@ export default class SessionReplayReadService {
1192
1812
  Math.min(data.limit, MAX_SESSION_REPLAY_FOR_EXCEPTION_LIMIT),
1193
1813
  );
1194
1814
 
1815
+ const endTime: Date = data.endTime || OneUptimeDate.getCurrentDate();
1816
+ const startTime: Date =
1817
+ data.startTime ||
1818
+ OneUptimeDate.addRemoveDays(
1819
+ endTime,
1820
+ -DEFAULT_SESSION_REPLAY_FOR_EXCEPTION_WINDOW_DAYS,
1821
+ );
1822
+
1823
+ const instanceSessionIds: Array<string> =
1824
+ await SessionReplayReadService.getSessionIdsForExceptionInstances({
1825
+ projectId: data.projectId,
1826
+ exceptionFingerprint: data.exceptionFingerprint,
1827
+ startTime: startTime,
1828
+ endTime: endTime,
1829
+ sessionId: data.sessionId,
1830
+ });
1831
+
1195
1832
  const selectList: string = toSelectList([
1196
1833
  { alias: "aggStartTime", expression: argMaxDateTime("startTime") },
1197
1834
  { alias: "aggEndTime", expression: argMaxDateTime("endTime") },
1198
- { alias: "aggDurationMs", expression: argMaxNumeric("durationMs") },
1835
+ { alias: "aggDurationMs", expression: LIVE_DURATION_EXPRESSION },
1199
1836
  { alias: "aggHasError", expression: argMaxColumn("hasError") },
1200
1837
  { alias: "aggErrorCount", expression: argMaxNumeric("errorCount") },
1201
1838
  /*
@@ -1247,14 +1884,8 @@ export default class SessionReplayReadService {
1247
1884
  type: TableColumnType.ObjectID,
1248
1885
  value: data.projectId,
1249
1886
  }}
1250
- AND hasAny(exceptionFingerprints, [${{
1251
- type: TableColumnType.Text,
1252
- value: data.exceptionFingerprint,
1253
- }}])
1254
1887
  `);
1255
1888
 
1256
- statement.append(RETENTION_FILTER);
1257
-
1258
1889
  if (data.accessibleRumApplicationIds) {
1259
1890
  statement.append(
1260
1891
  SQL` AND rumApplicationId IN (${{
@@ -1264,31 +1895,66 @@ export default class SessionReplayReadService {
1264
1895
  );
1265
1896
  }
1266
1897
 
1267
- if (data.startTime) {
1898
+ statement.append(
1899
+ SQL` AND startTime >= ${{
1900
+ type: TableColumnType.DateTime64,
1901
+ value: startTime,
1902
+ }} AND startTime <= ${{
1903
+ type: TableColumnType.DateTime64,
1904
+ value: endTime,
1905
+ }}`,
1906
+ );
1907
+
1908
+ statement.append(RETENTION_FILTER);
1909
+
1910
+ if (data.sessionId) {
1268
1911
  statement.append(
1269
- SQL` AND startTime >= ${{
1270
- type: TableColumnType.DateTime64,
1271
- value: data.startTime,
1912
+ SQL` AND sessionId = ${{
1913
+ type: TableColumnType.Text,
1914
+ value: data.sessionId,
1272
1915
  }}`,
1273
1916
  );
1274
1917
  }
1275
1918
 
1276
- if (data.endTime) {
1919
+ statement.append(
1920
+ SQL` AND (hasAny(exceptionFingerprints, [${{
1921
+ type: TableColumnType.Text,
1922
+ value: data.exceptionFingerprint,
1923
+ }}])`,
1924
+ );
1925
+
1926
+ if (instanceSessionIds.length > 0) {
1277
1927
  statement.append(
1278
- SQL` AND startTime <= ${{
1279
- type: TableColumnType.DateTime64,
1280
- value: data.endTime,
1281
- }}`,
1928
+ SQL` OR sessionId IN (${{
1929
+ type: TableColumnType.Text,
1930
+ value: new Includes(instanceSessionIds),
1931
+ }})`,
1282
1932
  );
1283
1933
  }
1284
1934
 
1935
+ statement.append(")");
1936
+
1285
1937
  statement.append(
1286
1938
  SQL` GROUP BY projectId, rumApplicationId, sessionId
1287
- HAVING hasAny(aggExceptionFingerprints, [${{
1939
+ HAVING (hasAny(aggExceptionFingerprints, [${{
1288
1940
  type: TableColumnType.Text,
1289
1941
  value: data.exceptionFingerprint,
1290
- }}])
1291
- ORDER BY aggStartTime DESC
1942
+ }}])`,
1943
+ );
1944
+
1945
+ if (instanceSessionIds.length > 0) {
1946
+ statement.append(
1947
+ SQL` OR sessionId IN (${{
1948
+ type: TableColumnType.Text,
1949
+ value: new Includes(instanceSessionIds),
1950
+ }})`,
1951
+ );
1952
+ }
1953
+
1954
+ statement.append(")");
1955
+
1956
+ statement.append(
1957
+ SQL` ORDER BY aggStartTime DESC
1292
1958
  LIMIT ${{
1293
1959
  type: TableColumnType.Number,
1294
1960
  value: limit,
@@ -1328,9 +1994,344 @@ export default class SessionReplayReadService {
1328
1994
  );
1329
1995
  }
1330
1996
 
1997
+ /*
1998
+ * Session ids of the pages that threw this exception, from the
1999
+ * exception instance table. The instance's `time` sits inside its
2000
+ * session, so a session that started inside the window threw inside
2001
+ * [startTime, endTime + max session length].
2002
+ *
2003
+ * Best-effort: the side index only ADDS live sessions to the answer, so
2004
+ * a failure here degrades to the finalized-only lookup with a warning
2005
+ * rather than failing the exception page's replay card.
2006
+ *
2007
+ * A caller-pinned sessionId narrows the lookup rather than bypassing it,
2008
+ * so the pin can never assert that a session threw something the
2009
+ * instance table has no record of it throwing.
2010
+ */
2011
+ private static async getSessionIdsForExceptionInstances(data: {
2012
+ projectId: ObjectID;
2013
+ exceptionFingerprint: string;
2014
+ startTime: Date;
2015
+ endTime: Date;
2016
+ sessionId?: string | undefined;
2017
+ }): Promise<Array<string>> {
2018
+ const statement: Statement = SQL`
2019
+ SELECT DISTINCT sessionId
2020
+ FROM ${AnalyticsTableName.ExceptionInstance}
2021
+ WHERE projectId = ${{
2022
+ type: TableColumnType.ObjectID,
2023
+ value: data.projectId,
2024
+ }}
2025
+ AND fingerprint = ${{
2026
+ type: TableColumnType.Text,
2027
+ value: data.exceptionFingerprint,
2028
+ }}
2029
+ AND sessionId != ''
2030
+ AND time >= ${{
2031
+ type: TableColumnType.DateTime64,
2032
+ value: data.startTime,
2033
+ }}
2034
+ AND time <= ${{
2035
+ type: TableColumnType.DateTime64,
2036
+ value: new Date(
2037
+ data.endTime.getTime() + SESSION_REPLAY_MAX_SESSION_MS,
2038
+ ),
2039
+ }}
2040
+ `;
2041
+
2042
+ /*
2043
+ * A pinned sessionId narrows this lookup; it does NOT replace it.
2044
+ *
2045
+ * Returning the pinned id unchecked made the caller's statement read
2046
+ * `sessionId = X AND (hasAny(fingerprints, [f]) OR sessionId IN (X))`,
2047
+ * whose second arm is trivially true - so the fingerprint constrained
2048
+ * nothing and the "Watch what the user saw" card would present any
2049
+ * accessible session as having observed this exception, on nothing but
2050
+ * a stale occurrence row. Asking the instance table whether THAT
2051
+ * session threw THIS fingerprint keeps the pin's real purpose (a live
2052
+ * session whose header has no fingerprints yet) while keeping the
2053
+ * claim true. A failure here answers [] and the header's mandatory
2054
+ * hasAny() predicate decides alone - fail closed.
2055
+ */
2056
+ if (data.sessionId) {
2057
+ statement.append(
2058
+ SQL` AND sessionId = ${{
2059
+ type: TableColumnType.Text,
2060
+ value: data.sessionId,
2061
+ }}`,
2062
+ );
2063
+ }
2064
+
2065
+ statement.append(SQL`
2066
+ ORDER BY sessionId ASC
2067
+ LIMIT ${{
2068
+ type: TableColumnType.Number,
2069
+ value: MAX_EXCEPTION_INSTANCE_SESSION_IDS,
2070
+ }}
2071
+ `);
2072
+
2073
+ statement.append(READ_QUERY_SETTINGS);
2074
+
2075
+ try {
2076
+ const dbResult: Results =
2077
+ await ExceptionInstanceService.executeQuery(statement);
2078
+ const response: DbJSONResponse = await dbResult.json<{
2079
+ data?: Array<JSONObject>;
2080
+ }>();
2081
+
2082
+ return (response.data || [])
2083
+ .map((row: JSONObject): string => {
2084
+ return readString(row, "sessionId");
2085
+ })
2086
+ .filter((sessionId: string): boolean => {
2087
+ return sessionId.length > 0;
2088
+ });
2089
+ } catch (err: unknown) {
2090
+ logger.warn(
2091
+ "SessionReplayReadService: could not look up exception instances by session; answering from finalized headers only",
2092
+ );
2093
+ logger.warn(err);
2094
+
2095
+ return [];
2096
+ }
2097
+ }
2098
+
2099
+ /*
2100
+ * Recording activity for one application over the last 24 hours, for
2101
+ * the health surface. No GROUP BY and no payload: uniqExact over the
2102
+ * sort-key range for the counts, and an ORDER BY startTime DESC LIMIT 1
2103
+ * (read in sort-key order, stops after one granule) for the most recent
2104
+ * start, which is NOT bounded to 24h so "the most recent was 3 days
2105
+ * ago" can be said when today is quiet.
2106
+ *
2107
+ * "Playable" is counted by subtraction: a session is unplayable only
2108
+ * when its FINALIZED row says it holds no chunks or was sealed as
2109
+ * recording-lost. Every other session - live, or finalized with footage
2110
+ * - can be watched. Counted that way because a finalized session still
2111
+ * has its provisional row on disk until a merge runs, and that row
2112
+ * would otherwise count a lost recording as live.
2113
+ *
2114
+ * ClickHouse trouble answers null (the UI says "unknown"), never 0:
2115
+ * "no sessions" and "could not count" are different diagnoses.
2116
+ */
2117
+ @CaptureSpan()
2118
+ public static async getApplicationActivitySummary(data: {
2119
+ projectId: ObjectID;
2120
+ rumApplicationId: ObjectID;
2121
+ nowUnixMs?: number | undefined;
2122
+ }): Promise<SessionReplayApplicationActivitySummary> {
2123
+ const nowUnixMs: number = data.nowUnixMs ?? Date.now();
2124
+ const cacheKey: string = `${data.projectId.toString()}:${data.rumApplicationId.toString()}`;
2125
+
2126
+ const cached: ActivitySummaryCacheEntry | undefined =
2127
+ activitySummaryCache.get(cacheKey);
2128
+
2129
+ if (cached && cached.expiresAt > nowUnixMs) {
2130
+ return cached.summary;
2131
+ }
2132
+
2133
+ const summary: SessionReplayApplicationActivitySummary =
2134
+ await SessionReplayReadService.readApplicationActivitySummary({
2135
+ projectId: data.projectId,
2136
+ rumApplicationId: data.rumApplicationId,
2137
+ nowUnixMs: nowUnixMs,
2138
+ });
2139
+
2140
+ /*
2141
+ * Coarse LRU: evict the oldest entry when full and the key is new, so
2142
+ * a burst of distinct applications cannot grow the map without bound.
2143
+ */
2144
+ if (
2145
+ activitySummaryCache.size >= MAX_ACTIVITY_SUMMARY_CACHE_ENTRIES &&
2146
+ !activitySummaryCache.has(cacheKey)
2147
+ ) {
2148
+ const oldest: string | undefined = activitySummaryCache
2149
+ .keys()
2150
+ .next().value;
2151
+
2152
+ if (oldest !== undefined) {
2153
+ activitySummaryCache.delete(oldest);
2154
+ }
2155
+ }
2156
+
2157
+ activitySummaryCache.delete(cacheKey);
2158
+ activitySummaryCache.set(cacheKey, {
2159
+ summary: summary,
2160
+ expiresAt: nowUnixMs + SESSION_REPLAY_ACTIVITY_SUMMARY_CACHE_TTL_MS,
2161
+ });
2162
+
2163
+ return summary;
2164
+ }
2165
+
2166
+ private static async readApplicationActivitySummary(data: {
2167
+ projectId: ObjectID;
2168
+ rumApplicationId: ObjectID;
2169
+ nowUnixMs: number;
2170
+ }): Promise<SessionReplayApplicationActivitySummary> {
2171
+ const countsStatement: Statement = SQL`
2172
+ SELECT
2173
+ toFloat64(uniqExact(sessionId)) AS sessionCount,
2174
+ toFloat64(uniqExactIf(sessionId, isFinalized AND (chunkCount = 0 OR sealedReason = ${{
2175
+ type: TableColumnType.Text,
2176
+ value: SessionReplaySealedReason.RecordingLost,
2177
+ }}))) AS unplayableCount
2178
+ FROM ${AnalyticsTableName.RumSession}
2179
+ WHERE projectId = ${{
2180
+ type: TableColumnType.ObjectID,
2181
+ value: data.projectId,
2182
+ }}
2183
+ AND rumApplicationId = ${{
2184
+ type: TableColumnType.ObjectID,
2185
+ value: data.rumApplicationId,
2186
+ }}
2187
+ AND startTime >= ${{
2188
+ type: TableColumnType.DateTime64,
2189
+ value: new Date(data.nowUnixMs - 24 * 60 * 60 * 1000),
2190
+ }}
2191
+ `;
2192
+
2193
+ countsStatement.append(RETENTION_FILTER);
2194
+ countsStatement.append(READ_QUERY_SETTINGS);
2195
+
2196
+ const lastStartStatement: Statement = SQL`
2197
+ SELECT
2198
+ toFloat64(toUnixTimestamp64Milli(startTime)) AS lastStartUnixMs,
2199
+ /*
2200
+ * The newest session's recorder capabilities, read off the same row
2201
+ * that answers "when did recording last start". Named directly (not
2202
+ * through the argMax alias set) because this statement has no GROUP
2203
+ * BY: it is one row, read in sort-key order, LIMIT 1.
2204
+ */
2205
+ attributes AS aggAttributes
2206
+ FROM ${AnalyticsTableName.RumSession}
2207
+ WHERE projectId = ${{
2208
+ type: TableColumnType.ObjectID,
2209
+ value: data.projectId,
2210
+ }}
2211
+ AND rumApplicationId = ${{
2212
+ type: TableColumnType.ObjectID,
2213
+ value: data.rumApplicationId,
2214
+ }}
2215
+ `;
2216
+
2217
+ lastStartStatement.append(RETENTION_FILTER);
2218
+ lastStartStatement.append(" ORDER BY startTime DESC LIMIT 1");
2219
+ lastStartStatement.append(READ_QUERY_SETTINGS);
2220
+
2221
+ try {
2222
+ const [countsResult, lastStartResult]: [Results, Results] =
2223
+ await Promise.all([
2224
+ RumSessionService.executeQuery(countsStatement),
2225
+ RumSessionService.executeQuery(lastStartStatement),
2226
+ ]);
2227
+
2228
+ const countsResponse: DbJSONResponse = await countsResult.json<{
2229
+ data?: Array<JSONObject>;
2230
+ }>();
2231
+ const lastStartResponse: DbJSONResponse = await lastStartResult.json<{
2232
+ data?: Array<JSONObject>;
2233
+ }>();
2234
+
2235
+ const countsRow: JSONObject | undefined = (countsResponse.data || [])[0];
2236
+ const lastStartRow: JSONObject | undefined = (lastStartResponse.data ||
2237
+ [])[0];
2238
+
2239
+ const sessionCount: number = countsRow
2240
+ ? readNumber(countsRow, "sessionCount")
2241
+ : 0;
2242
+ const unplayableCount: number = countsRow
2243
+ ? readNumber(countsRow, "unplayableCount")
2244
+ : 0;
2245
+
2246
+ const lastStartUnixMs: number = lastStartRow
2247
+ ? readNumber(lastStartRow, "lastStartUnixMs")
2248
+ : 0;
2249
+
2250
+ /*
2251
+ * An empty list means "the newest session declared none" (an old
2252
+ * recorder artifact), which is not the same as "we could not tell" -
2253
+ * but the health copy renders both as "not reported yet", and
2254
+ * claiming a recorder has NO capabilities would be a stronger
2255
+ * statement than the row supports. So an empty list answers null and
2256
+ * only a non-empty one is reported.
2257
+ */
2258
+ const recorderCapabilities: Array<string> = lastStartRow
2259
+ ? readRecorderCapabilities(lastStartRow)
2260
+ : [];
2261
+
2262
+ return {
2263
+ sessionsLast24h: sessionCount,
2264
+ playableSessionsLast24h: Math.max(0, sessionCount - unplayableCount),
2265
+ lastSessionStartedAt:
2266
+ lastStartUnixMs > 0 ? new Date(lastStartUnixMs) : null,
2267
+ recorderCapabilities:
2268
+ recorderCapabilities.length > 0 ? recorderCapabilities : null,
2269
+ };
2270
+ } catch (err: unknown) {
2271
+ logger.warn(
2272
+ "SessionReplayReadService: could not read the application activity summary",
2273
+ );
2274
+ logger.warn(err);
2275
+
2276
+ return {
2277
+ sessionsLast24h: null,
2278
+ playableSessionsLast24h: null,
2279
+ lastSessionStartedAt: null,
2280
+ recorderCapabilities: null,
2281
+ };
2282
+ }
2283
+ }
2284
+
2285
+ /* The HAVING/ORDER BY expression for a sort key. */
2286
+ private static getSortExpression(sortBy: SessionReplaySortBy): string {
2287
+ switch (sortBy) {
2288
+ case "durationMs":
2289
+ return "aggDurationMs";
2290
+ case "errorCount":
2291
+ return "aggErrorCount";
2292
+ case "frustration":
2293
+ return FRUSTRATION_TOTAL_EXPRESSION;
2294
+ case "startTime":
2295
+ default:
2296
+ return "aggStartTime";
2297
+ }
2298
+ }
2299
+
2300
+ /* The cursor value of a row under a sort key: what the expression above yields. */
2301
+ private static getSortValue(
2302
+ sortBy: SessionReplaySortBy,
2303
+ item: SessionReplayListItem,
2304
+ ): number {
2305
+ switch (sortBy) {
2306
+ case "durationMs":
2307
+ return item.durationMs;
2308
+ case "errorCount":
2309
+ return item.errorCount;
2310
+ case "frustration":
2311
+ return (
2312
+ item.rageClickCount +
2313
+ item.deadClickCount +
2314
+ item.errorClickCount +
2315
+ item.refreshRageCount
2316
+ );
2317
+ case "startTime":
2318
+ default:
2319
+ return item.startTime.getTime();
2320
+ }
2321
+ }
2322
+
2323
+ /*
2324
+ * Every list predicate, in cost order: booleans and equality over
2325
+ * aliases first, IN lists next, array membership after, and the
2326
+ * substring predicates (tags, urlPrefix, search) LAST. ClickHouse
2327
+ * evaluates HAVING per group after aggregation, so the order does not
2328
+ * change what is scanned, but a cheap predicate that fails first spares
2329
+ * the string work for every group it eliminates.
2330
+ */
1331
2331
  private static appendListHavingFilters(
1332
2332
  statement: Statement,
1333
2333
  filters: SessionReplayListFilters,
2334
+ includeIdentifiedUserLabel: boolean,
1334
2335
  ): void {
1335
2336
  if (filters.hasError !== undefined) {
1336
2337
  statement.append(
@@ -1352,11 +2353,10 @@ export default class SessionReplayReadService {
1352
2353
  * honour false, so accepting the value and ignoring it returned the
1353
2354
  * whole unfiltered list with a 200 and no indication why.
1354
2355
  */
1355
- const total: string =
1356
- "(aggRageClickCount + aggDeadClickCount + aggErrorClickCount + aggRefreshRageCount)";
1357
-
1358
2356
  statement.append(
1359
- filters.hasFrustration ? ` AND ${total} > 0` : ` AND ${total} = 0`,
2357
+ filters.hasFrustration
2358
+ ? ` AND ${FRUSTRATION_TOTAL_EXPRESSION} > 0`
2359
+ : ` AND ${FRUSTRATION_TOTAL_EXPRESSION} = 0`,
1360
2360
  );
1361
2361
  }
1362
2362
 
@@ -1369,6 +2369,37 @@ export default class SessionReplayReadService {
1369
2369
  );
1370
2370
  }
1371
2371
 
2372
+ if (filters.hasIdentifiedUser !== undefined) {
2373
+ /*
2374
+ * The digest column, not the label: it is under the ordinary session
2375
+ * ACL, and "did somebody identify" discloses nothing about who.
2376
+ */
2377
+ statement.append(
2378
+ filters.hasIdentifiedUser
2379
+ ? " AND aggIdentifiedUserKey != ''"
2380
+ : " AND aggIdentifiedUserKey = ''",
2381
+ );
2382
+ }
2383
+
2384
+ if (filters.isPlayable !== undefined) {
2385
+ /*
2386
+ * A live session is playable (its chunks are being written); a
2387
+ * finalized one only when the finalizer counted chunks and did not
2388
+ * seal it as lost.
2389
+ */
2390
+ const playable: string = `((aggIsFinalized = 0 OR aggChunkCount > 0) AND aggSealedReason != '${SessionReplaySealedReason.RecordingLost}')`;
2391
+
2392
+ statement.append(
2393
+ filters.isPlayable ? ` AND ${playable}` : ` AND NOT ${playable}`,
2394
+ );
2395
+ }
2396
+
2397
+ if (filters.hasTraces !== undefined) {
2398
+ statement.append(
2399
+ filters.hasTraces ? " AND aggTraceCount > 0" : " AND aggTraceCount = 0",
2400
+ );
2401
+ }
2402
+
1372
2403
  if (filters.triggerReasons && filters.triggerReasons.length > 0) {
1373
2404
  statement.append(
1374
2405
  SQL` AND aggTriggerReason IN (${{
@@ -1425,16 +2456,10 @@ export default class SessionReplayReadService {
1425
2456
 
1426
2457
  if (filters.route) {
1427
2458
  /*
1428
- * `has`, not a Search/LIKE. StatementGenerator's own comment calls
1429
- * restoring the exact-match array fast path "the single biggest
1430
- * performance fix" - a lowerUTF8 arrayExists has no bloom
1431
- * pre-filter and full-scans the table.
1432
- *
1433
- * The argMax expression is appended as raw SQL rather than
1434
- * interpolated into the template: a plain string substituted into
1435
- * an SQL`` literal is bound as an Identifier and would be quoted.
2459
+ * `has`, not a Search/LIKE: exact membership is the cheap array
2460
+ * path. Over the argMax alias, never the raw column.
1436
2461
  */
1437
- statement.append(` AND has(${argMaxColumn("routes")}, `);
2462
+ statement.append(" AND has(aggRoutes, ");
1438
2463
  statement.append(
1439
2464
  SQL`${{
1440
2465
  type: TableColumnType.Text,
@@ -1454,5 +2479,121 @@ export default class SessionReplayReadService {
1454
2479
  }}`,
1455
2480
  );
1456
2481
  }
2482
+
2483
+ if (filters.tags) {
2484
+ /*
2485
+ * Every pair must match. mapContains first so an absent key never
2486
+ * matches the empty string a Map subscript returns for it. Bounded
2487
+ * by the number of tags a session can even carry.
2488
+ */
2489
+ const pairs: Array<[string, string]> = Object.entries(filters.tags)
2490
+ .filter(([key, value]: [string, string]): boolean => {
2491
+ return key.length > 0 && typeof value === "string";
2492
+ })
2493
+ .slice(0, SESSION_REPLAY_MAX_TAG_KEYS);
2494
+
2495
+ for (const [key, value] of pairs) {
2496
+ statement.append(" AND mapContains(aggTags, ");
2497
+ statement.append(
2498
+ SQL`${{
2499
+ type: TableColumnType.Text,
2500
+ value: key,
2501
+ }}) AND aggTags[${{
2502
+ type: TableColumnType.Text,
2503
+ value: key,
2504
+ }}] = ${{
2505
+ type: TableColumnType.Text,
2506
+ value: value,
2507
+ }}`,
2508
+ );
2509
+ }
2510
+ }
2511
+
2512
+ if (filters.urlPrefix) {
2513
+ /*
2514
+ * "sessions that touched /checkout/*": a prefix over every route the
2515
+ * session visited and over the entry URL, which for a pre-migration
2516
+ * session is the only URL the header holds.
2517
+ *
2518
+ * The stored values are scrubbed ABSOLUTE urls (https://host/path),
2519
+ * but the filter a human types is a PATH - the search box routes any
2520
+ * value beginning with "/" here, and the docs promise `url:/checkout`
2521
+ * outright. Matching only the full string meant that documented
2522
+ * search never matched anything, in any project, with no error to
2523
+ * say so. So the path of each route and of the entry URL is matched
2524
+ * as well as the whole URL: an absolute prefix still matches on the
2525
+ * first arm, a path prefix on the second. ClickHouse's path() returns
2526
+ * the path component without host or query, which is exactly the
2527
+ * shape the recorder's route list is scrubbed down to.
2528
+ */
2529
+ const prefixParameter: { type: TableColumnType; value: string } = {
2530
+ type: TableColumnType.Text,
2531
+ value: filters.urlPrefix,
2532
+ };
2533
+
2534
+ statement.append(" AND (arrayExists(r -> startsWith(r, ");
2535
+ statement.append(SQL`${prefixParameter}`);
2536
+ statement.append(") OR startsWith(path(r), ");
2537
+ statement.append(SQL`${prefixParameter}`);
2538
+ statement.append("), aggRoutes) OR startsWith(aggEntryUrl, ");
2539
+ statement.append(SQL`${prefixParameter}`);
2540
+ statement.append(") OR startsWith(path(aggEntryUrl), ");
2541
+ statement.append(SQL`${prefixParameter}`);
2542
+ statement.append("))");
2543
+ }
2544
+
2545
+ if (filters.search) {
2546
+ SessionReplayReadService.appendSearchPredicate(
2547
+ statement,
2548
+ filters.search,
2549
+ includeIdentifiedUserLabel,
2550
+ );
2551
+ }
2552
+ }
2553
+
2554
+ /*
2555
+ * Free-text search, last of the predicates because it is the only one
2556
+ * that does substring work per group. The identified user label is
2557
+ * searched ONLY when the caller may read it: without that gate a caller
2558
+ * denied the label could ask "is jane@example.com here" and read every
2559
+ * other field of the answer.
2560
+ */
2561
+ private static appendSearchPredicate(
2562
+ statement: Statement,
2563
+ search: string,
2564
+ includeIdentifiedUserLabel: boolean,
2565
+ ): void {
2566
+ const term: string = search
2567
+ .trim()
2568
+ .substring(0, SESSION_REPLAY_LIST_SEARCH_MAX_LENGTH);
2569
+
2570
+ if (!term) {
2571
+ return;
2572
+ }
2573
+
2574
+ const textParameter: { type: TableColumnType; value: string } = {
2575
+ type: TableColumnType.Text,
2576
+ value: term,
2577
+ };
2578
+
2579
+ statement.append(" AND (startsWith(sessionId, ");
2580
+ statement.append(SQL`${textParameter})`);
2581
+ statement.append(" OR positionCaseInsensitiveUTF8(aggEntryUrl, ");
2582
+ statement.append(SQL`${textParameter}) > 0`);
2583
+ statement.append(" OR positionCaseInsensitiveUTF8(aggExitUrl, ");
2584
+ statement.append(SQL`${textParameter}) > 0`);
2585
+ statement.append(" OR arrayExists(r -> positionCaseInsensitiveUTF8(r, ");
2586
+ statement.append(SQL`${textParameter}) > 0, aggRoutes)`);
2587
+ statement.append(` OR has(${argMaxColumn("traceIds")}, `);
2588
+ statement.append(SQL`${textParameter})`);
2589
+
2590
+ if (includeIdentifiedUserLabel) {
2591
+ statement.append(
2592
+ " OR positionCaseInsensitiveUTF8(aggIdentifiedUserLabel, ",
2593
+ );
2594
+ statement.append(SQL`${textParameter}) > 0`);
2595
+ }
2596
+
2597
+ statement.append(")");
1457
2598
  }
1458
2599
  }