@oneuptime/common 12.0.33 → 12.0.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (652) 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/IncidentCustomField.ts +77 -3
  6. package/Models/DatabaseModels/InventoryItemCustomField.ts +77 -3
  7. package/Models/DatabaseModels/MonitorCustomField.ts +77 -3
  8. package/Models/DatabaseModels/OnCallDutyPolicyCustomField.ts +77 -3
  9. package/Models/DatabaseModels/RumApplication.ts +6 -6
  10. package/Models/DatabaseModels/ScheduledMaintenanceCustomField.ts +77 -3
  11. package/Models/DatabaseModels/StatusPageCustomField.ts +77 -3
  12. package/Models/DatabaseModels/TeamCustomField.ts +77 -3
  13. package/Models/DatabaseModels/TeamMember.ts +0 -9
  14. package/Models/DatabaseModels/TeamMemberCustomField.ts +77 -3
  15. package/Models/DatabaseModels/TeamPermission.ts +3 -24
  16. package/Models/DatabaseModels/TelemetryIngestionKey.ts +260 -0
  17. package/Models/DatabaseModels/UserTelegram.ts +0 -4
  18. package/Scripts/benchmark-fanin-capacity.js +181 -0
  19. package/Server/API/TelemetryAPI.ts +1284 -164
  20. package/Server/API/UserNotificationSettingAPI.ts +55 -0
  21. package/Server/API/UserTelegramAPI.ts +48 -4
  22. package/Server/EnvironmentConfig.ts +35 -2
  23. package/Server/Infrastructure/Postgres/SchemaMigrations/1791300000000-AddTelemetryIngestionKeyType.ts +91 -0
  24. package/Server/Infrastructure/Postgres/SchemaMigrations/1791400000000-SessionReplayRecordEverySessionByDefault.ts +67 -0
  25. package/Server/Infrastructure/Postgres/SchemaMigrations/1791500000000-WidenCustomFieldDropdownOptions.ts +139 -0
  26. package/Server/Infrastructure/Postgres/SchemaMigrations/1791600000000-AddCustomFieldValueMapping.ts +121 -0
  27. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
  28. package/Server/Infrastructure/QueueWorker.ts +20 -14
  29. package/Server/Infrastructure/Semaphore.ts +2 -0
  30. package/Server/Middleware/TelemetryIngest.ts +453 -7
  31. package/Server/Services/AccessTokenService.ts +1 -0
  32. package/Server/Services/AlertCustomFieldService.ts +98 -0
  33. package/Server/Services/AlertService.ts +37 -0
  34. package/Server/Services/ApiKeyPermissionService.ts +461 -29
  35. package/Server/Services/CustomFieldMappingService.ts +879 -0
  36. package/Server/Services/GlobalConfigService.ts +194 -0
  37. package/Server/Services/IncidentCustomFieldService.ts +98 -0
  38. package/Server/Services/IncidentService.ts +35 -0
  39. package/Server/Services/MetricService.ts +194 -0
  40. package/Server/Services/MonitorService.ts +32 -0
  41. package/Server/Services/NetworkDeviceAutoImportRuleEngineService.ts +78 -25
  42. package/Server/Services/ProjectService.ts +22 -0
  43. package/Server/Services/RoutineEmailSettingsService.ts +73 -0
  44. package/Server/Services/RumSessionReplayViewService.ts +104 -24
  45. package/Server/Services/ScheduledMaintenanceCustomFieldService.ts +98 -0
  46. package/Server/Services/ScheduledMaintenanceService.ts +32 -0
  47. package/Server/Services/TeamMemberService.ts +78 -0
  48. package/Server/Services/TeamPermissionService.ts +235 -3
  49. package/Server/Services/TelemetryIngestionKeyService.ts +722 -19
  50. package/Server/Services/UserNotificationSettingService.ts +34 -6
  51. package/Server/Services/UserTelegramService.ts +326 -5
  52. package/Server/Types/Database/QueryHelper.ts +2 -0
  53. package/Server/Utils/APIKey/AccessPermission.ts +5 -2
  54. package/Server/Utils/CustomField/CustomFieldDefinitionMappingHooks.ts +54 -0
  55. package/Server/Utils/CustomField/CustomFieldMappingRegistry.ts +415 -0
  56. package/Server/Utils/CustomField/CustomFieldMappingValidator.ts +335 -0
  57. package/Server/Utils/DataSource/EgressGuard.ts +177 -8
  58. package/Server/Utils/EmailRollup/EmailRollupConstants.ts +38 -5
  59. package/Server/Utils/EmailRollup/EmailRollupFlushRunner.ts +57 -4
  60. package/Server/Utils/FrontendEnvironment.ts +40 -0
  61. package/Server/Utils/LogRedaction.ts +20 -0
  62. package/Server/Utils/Logger.ts +11 -0
  63. package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +338 -77
  64. package/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.ts +30 -4
  65. package/Server/Utils/Monitor/MonitorAlert.ts +29 -6
  66. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +110 -15
  67. package/Server/Utils/Monitor/MonitorIncident.ts +22 -6
  68. package/Server/Utils/Monitor/MonitorTemplateUtil.ts +29 -0
  69. package/Server/Utils/Monitor/SeriesContextEnricher.ts +304 -0
  70. package/Server/Utils/SSRFProtection.ts +217 -10
  71. package/Server/Utils/SessionReplay/SessionReplayGateCache.ts +239 -138
  72. package/Server/Utils/SessionReplay/SessionReplayHealthCounters.ts +305 -0
  73. package/Server/Utils/SessionReplay/SessionReplayReadService.ts +1430 -289
  74. package/Server/Utils/StartServer.ts +2 -17
  75. package/Server/Utils/TelegramVerificationToken.ts +185 -0
  76. package/Server/Utils/Telemetry/PinServiceName.ts +197 -0
  77. package/Server/Utils/Telemetry/TelemetryFanInWriter.ts +38 -16
  78. package/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.ts +92 -0
  79. package/Server/Utils/Telemetry/TelemetryIngestionKeyRateLimiter.ts +217 -0
  80. package/Server/Utils/VM/VMAPI.ts +1 -0
  81. package/Server/Utils/VM/VMRunner.ts +975 -95
  82. package/Tests/App/AdminDashboard/AdminHeaderSmallScreens.test.tsx +202 -0
  83. package/Tests/App/Dashboard/DashboardHeaderSmallScreens.test.tsx +404 -0
  84. package/Tests/App/Dashboard/DiscoveryScanWizardValidation.test.tsx +468 -0
  85. package/Tests/App/Dashboard/NotificationEmailPreferences.test.tsx +399 -0
  86. package/Tests/App/StatusPage/StatusPageLastUpdated.test.tsx +263 -0
  87. package/Tests/App/StatusPage/StatusPageOverviewLiveAndSearch.test.tsx +753 -0
  88. package/Tests/App/StatusPage/StatusPageResourceSearchBox.test.tsx +224 -0
  89. package/Tests/Models/AnalyticsModels/RumSessionReplayColumns.test.ts +262 -0
  90. package/Tests/Models/CustomFieldMappingColumns.test.ts +166 -0
  91. package/Tests/Models/DatabaseModels/SessionReplayModels.test.ts +20 -6
  92. package/Tests/ResponsiveVisibility.test.ts +115 -0
  93. package/Tests/ResponsiveVisibility.ts +183 -0
  94. package/Tests/Server/API/SessionReplayAPI.test.ts +1940 -216
  95. package/Tests/Server/API/UserNotificationSettingAPI.test.ts +186 -0
  96. package/Tests/Server/API/UserTelegramAPISecurity.test.ts +231 -0
  97. package/Tests/Server/EnvironmentConfigFrontendSecurity.test.ts +234 -0
  98. package/Tests/Server/Infrastructure/Postgres/CustomFieldValueMappingMigration.test.ts +202 -0
  99. package/Tests/Server/Infrastructure/Postgres/InventoryItemArchiveMigration.test.ts +41 -0
  100. package/Tests/Server/Infrastructure/Postgres/SessionReplayRecordEverySessionByDefaultMigration.test.ts +169 -0
  101. package/Tests/Server/Infrastructure/QueueWorkerTimeout.test.ts +210 -0
  102. package/Tests/Server/Infrastructure/TelemetryExporterDeploymentConfig.test.ts +130 -0
  103. package/Tests/Server/Middleware/ProjectAuthorizationApiKeyMiddleware.test.ts +4 -1
  104. package/Tests/Server/Middleware/TelemetryIngestBrowserKey.test.ts +1163 -0
  105. package/Tests/Server/Middleware/TelemetryIngestTokenLog.test.ts +5 -3
  106. package/Tests/Server/Services/AddTelemetryIngestionKeyTypeMigration.test.ts +555 -0
  107. package/Tests/Server/Services/ApiKeyPermissionSecurity.test.ts +678 -0
  108. package/Tests/Server/Services/ApiKeyPermissionService.test.ts +276 -28
  109. package/Tests/Server/Services/CustomFieldDropdownOptionsColumnWidth.test.ts +431 -0
  110. package/Tests/Server/Services/CustomFieldMappingService.test.ts +850 -0
  111. package/Tests/Server/Services/DiscoveryScanClaimHookFreeSafety.test.ts +32 -0
  112. package/Tests/Server/Services/GlobalConfigService.test.ts +414 -1
  113. package/Tests/Server/Services/MetricRawEntityKeyPrune.test.ts +592 -0
  114. package/Tests/Server/Services/NetworkDeviceAutoImportRuleEngineService.test.ts +226 -1
  115. package/Tests/Server/Services/RoutineEmailSettingsPostgres.test.ts +329 -0
  116. package/Tests/Server/Services/RumSessionReplayViewService.test.ts +267 -0
  117. package/Tests/Server/Services/TeamMemberAutoAcceptInvitation.test.ts +9 -0
  118. package/Tests/Server/Services/TeamMemberInviteRegistrationToken.test.ts +9 -0
  119. package/Tests/Server/Services/TeamPrivilegeEscalation.test.ts +834 -0
  120. package/Tests/Server/Services/TelemetryIngestionKeyPolicyResolution.test.ts +814 -0
  121. package/Tests/Server/Services/TelemetryIngestionKeyValidation.test.ts +998 -0
  122. package/Tests/Server/Services/UserNotificationSettingRollupRouting.test.ts +141 -0
  123. package/Tests/Server/Services/UserNotificationSettingWorkspaceChannels.test.ts +18 -10
  124. package/Tests/Server/Services/UserTelegramVerificationSecurity.test.ts +688 -0
  125. package/Tests/Server/Utils/AI/SRE/Insights/FixRouting.test.ts +497 -0
  126. package/Tests/Server/Utils/AI/Toolbox/Serializer.test.ts +383 -0
  127. package/Tests/Server/Utils/APIKey/AccessPermission.test.ts +7 -3
  128. package/Tests/Server/Utils/AnalyticsDatabase/ClusterConfig.test.ts +315 -0
  129. package/Tests/Server/Utils/CustomField/CustomFieldMappingValidator.test.ts +437 -0
  130. package/Tests/Server/Utils/DataSource/EgressGuard.test.ts +300 -11
  131. package/Tests/Server/Utils/EmailRollup/EmailRollupBurstWindow.test.ts +83 -0
  132. package/Tests/Server/Utils/EmailRollup/EmailRollupFlushRunnerBehaviour.test.ts +1 -1
  133. package/Tests/Server/Utils/EmailRollup/EmailRollupFlushRunnerPreferences.test.ts +362 -0
  134. package/Tests/Server/Utils/EmailRollup/EmailRollupTestHarness.ts +77 -1
  135. package/Tests/Server/Utils/FrontendEnvironment.test.ts +190 -0
  136. package/Tests/Server/Utils/LogRedaction.test.ts +26 -0
  137. package/Tests/Server/Utils/LoggerCredentialLeak.test.ts +23 -9
  138. package/Tests/Server/Utils/Monitor/Criteria/CompareCriteriaAggregation.test.ts +584 -0
  139. package/Tests/Server/Utils/Monitor/Criteria/IncomingRequestHeaderCriteria.test.ts +236 -0
  140. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluator.test.ts +437 -0
  141. package/Tests/Server/Utils/Monitor/MonitorTemplateUtilSeriesContext.test.ts +176 -0
  142. package/Tests/Server/Utils/Monitor/SeriesContextEnricher.test.ts +356 -0
  143. package/Tests/Server/Utils/SSRFProtectionCloudServiceAddresses.test.ts +595 -0
  144. package/Tests/Server/Utils/SessionReplay/SessionReplayGateCachePolicy.test.ts +198 -1
  145. package/Tests/Server/Utils/SessionReplay/SessionReplayHealthCounters.test.ts +453 -0
  146. package/Tests/Server/Utils/SessionReplay/SessionReplayReadServiceQueries.test.ts +1378 -0
  147. package/Tests/Server/Utils/SessionReplayOriginAllowListRefactor.test.ts +420 -0
  148. package/Tests/Server/Utils/TelegramVerificationToken.test.ts +253 -0
  149. package/Tests/Server/Utils/Telemetry/PinServiceName.test.ts +666 -0
  150. package/Tests/Server/Utils/Telemetry/TelemetryFanInWriterCapacity.test.ts +458 -0
  151. package/Tests/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.test.ts +424 -0
  152. package/Tests/Server/Utils/Telemetry/TelemetryIngestionKeyRateLimiter.test.ts +528 -0
  153. package/Tests/Server/Utils/TelemetryExporterEnvironment.test.ts +56 -0
  154. package/Tests/Server/Utils/VM/VMRunnerHostBridgeLatency.test.ts +74 -0
  155. package/Tests/Server/Utils/VM/VMRunnerPrivateNetworkWiring.test.ts +100 -7
  156. package/Tests/Server/Utils/VM/VMRunnerSsrf.test.ts +1176 -6
  157. package/Tests/Types/CustomField/CustomFieldMappingCatalog.test.ts +220 -0
  158. package/Tests/Types/CustomField/CustomFieldValueMapping.test.ts +348 -0
  159. package/Tests/Types/JSONFunctions.test.ts +260 -0
  160. package/Tests/Types/Monitor/CephAlertTemplates.test.ts +422 -53
  161. package/Tests/Types/Monitor/DockerAlertTemplates.test.ts +495 -15
  162. package/Tests/Types/Monitor/DockerSwarmAlertTemplates.test.ts +191 -45
  163. package/Tests/Types/Monitor/HostAlertTemplates.test.ts +370 -42
  164. package/Tests/Types/Monitor/IotAlertTemplates.test.ts +375 -20
  165. package/Tests/Types/Monitor/KubernetesAlertTemplates.test.ts +636 -42
  166. package/Tests/Types/Monitor/KubernetesMetricCatalog.test.ts +189 -0
  167. package/Tests/Types/Monitor/KubernetesTemplateGroupByKeys.test.ts +95 -14
  168. package/Tests/Types/Monitor/PodmanAlertTemplates.test.ts +190 -21
  169. package/Tests/Types/Monitor/ProxmoxAlertTemplates.test.ts +352 -22
  170. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationAlertDebuggability.test.ts +363 -0
  171. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationNotificationMode.test.ts +93 -18
  172. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.test.ts +259 -0
  173. package/Tests/Types/Monitor/Recommendation/RecommendationCriteriaBuilder.test.ts +476 -0
  174. package/Tests/Types/Monitor/RumAlertTemplates.test.ts +136 -0
  175. package/Tests/Types/Monitor/SeriesContext/SeriesDebugHints.test.ts +661 -0
  176. package/Tests/Types/Monitor/SeriesContext/SeriesLabelDisplay.test.ts +507 -0
  177. package/Tests/Types/Monitor/ServiceAlertTemplates.test.ts +270 -0
  178. package/Tests/Types/Monitor/TemplateGroupByKeys.test.ts +38 -10
  179. package/Tests/Types/Monitor/Utils/RecommendationCriteriaAssertions.ts +102 -0
  180. package/Tests/Types/NotificationSetting/RoutineEmailEvents.test.ts +47 -0
  181. package/Tests/Types/Rum/SessionReplayApiContracts.test.ts +608 -0
  182. package/Tests/Types/Rum/SessionReplayCustomEvents.test.ts +434 -0
  183. package/Tests/Types/WebsiteRequest.test.ts +285 -3
  184. package/Tests/UI/Components/Charts/ChartBucketIdentity.test.ts +16 -3
  185. package/Tests/UI/Components/Charts/ChartTrailingBucketGap.test.ts +691 -0
  186. package/Tests/UI/Components/ComponentsModal.test.tsx +12 -4
  187. package/Tests/UI/Components/ComponentsModalUsability.test.tsx +518 -0
  188. package/Tests/UI/Components/CustomFields/CustomFieldsDetailMapping.test.tsx +324 -0
  189. package/Tests/UI/Components/CustomFields/MapFromCustomFieldInput.test.tsx +176 -0
  190. package/Tests/UI/Components/Graphs/DayUptimeGraph.test.tsx +484 -0
  191. package/Tests/UI/Components/HeaderRightRail.test.tsx +157 -0
  192. package/Tests/UI/Components/IconDropdownItem.test.tsx +102 -0
  193. package/Tests/UI/Components/JSONTablePrototypePollution.test.tsx +117 -0
  194. package/Tests/UI/Components/KeyboardShortcutsModal.test.tsx +214 -0
  195. package/Tests/UI/Components/ModelTable/ModelTableWrapContent.test.tsx +561 -0
  196. package/Tests/UI/Components/MonitorGraphs/UptimeBarDayModal.test.tsx +302 -0
  197. package/Tests/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.test.ts +28 -12
  198. package/Tests/UI/Components/ShortcutDialogGuard.test.tsx +74 -0
  199. package/Tests/UI/Components/StatusPage/ResourceGroupSectionAutoExpand.test.tsx +248 -0
  200. package/Tests/UI/Components/TableCellWrapping.test.tsx +625 -0
  201. package/Tests/UI/Components/TableLoadingStates.test.tsx +205 -0
  202. package/Tests/UI/Components/Workflow/NodePlacement.test.ts +148 -0
  203. package/Tests/UI/Components/Workflow/Workflow.test.tsx +981 -0
  204. package/Tests/UI/ConfigBrowserTelemetry.test.ts +70 -0
  205. package/Tests/UI/Rum/ChunkLoader.test.ts +1036 -2
  206. package/Tests/UI/Rum/FidelityNotices.test.ts +187 -0
  207. package/Tests/UI/Rum/InactivityMap.test.ts +341 -0
  208. package/Tests/UI/Rum/PrivacySummaryCard.test.tsx +259 -0
  209. package/Tests/UI/Rum/RecordingHealthCard.test.tsx +855 -0
  210. package/Tests/UI/Rum/RecordingHealthStrip.test.tsx +594 -0
  211. package/Tests/UI/Rum/ReplayCard.test.tsx +451 -0
  212. package/Tests/UI/Rum/ReplayCorrelationPanel.test.tsx +536 -0
  213. package/Tests/UI/Rum/ReplayEngine.test.ts +2503 -0
  214. package/Tests/UI/Rum/ReplayEngineTypes.test.ts +171 -0
  215. package/Tests/UI/Rum/ReplayHeader.test.tsx +711 -0
  216. package/Tests/UI/Rum/ReplayLink.test.tsx +119 -0
  217. package/Tests/UI/Rum/ReplayPinControl.test.tsx +408 -0
  218. package/Tests/UI/Rum/ReplayPlaybackIntent.test.ts +152 -0
  219. package/Tests/UI/Rum/ReplayRail.test.tsx +1296 -0
  220. package/Tests/UI/Rum/ReplayRailDetail.test.tsx +900 -0
  221. package/Tests/UI/Rum/ReplayScrubber.test.tsx +763 -147
  222. package/Tests/UI/Rum/ReplaySignalTypes.test.ts +189 -0
  223. package/Tests/UI/Rum/ReplaySignals.test.ts +1579 -0
  224. package/Tests/UI/Rum/ReplayStage.test.tsx +396 -473
  225. package/Tests/UI/Rum/ReplayStageOverlays.test.tsx +918 -0
  226. package/Tests/UI/Rum/ReplayTimeline.test.tsx +696 -0
  227. package/Tests/UI/Rum/SessionReplayEmptyState.test.tsx +499 -0
  228. package/Tests/UI/Rum/SessionReplaySearchBar.test.tsx +346 -0
  229. package/Tests/UI/Rum/SessionReplaySetupGuide.test.tsx +545 -0
  230. package/Tests/UI/Rum/SessionReplayTable.test.tsx +991 -0
  231. package/Tests/UI/Rum/TargetedCapturePanel.test.tsx +254 -0
  232. package/Tests/UI/Telemetry/BrowserExporterIsolation.test.ts +71 -0
  233. package/Tests/UI/Utils/GlobalKeyboardShortcut.test.ts +344 -0
  234. package/Tests/Utils/API.test.ts +308 -0
  235. package/Tests/Utils/HTTPResponseBodyReader.test.ts +323 -0
  236. package/Tests/Utils/NetworkDiscovery/DiscoveryScanStatus.test.ts +117 -0
  237. package/Tests/Utils/Rum/ChunkMath.test.ts +83 -0
  238. package/Tests/Utils/Rum/SessionReplayHealthDiagnosis.test.ts +993 -0
  239. package/Tests/Utils/Rum/SessionReplayStringMap.test.ts +245 -0
  240. package/Tests/Utils/StatusPage/ResourceSearch.test.ts +631 -0
  241. package/Tests/Utils/Telemetry/OriginAllowList.test.ts +633 -0
  242. package/Tests/Utils/Uptime/DayUptimeGraphUtil.test.ts +461 -0
  243. package/Tests/Utils/ValueFormatter.test.ts +148 -0
  244. package/Types/CustomField/CustomFieldMappingCatalog.ts +161 -0
  245. package/Types/CustomField/CustomFieldMappingSourceResource.ts +19 -0
  246. package/Types/CustomField/CustomFieldValueMapping.ts +288 -0
  247. package/Types/Icon/IconProp.ts +1 -0
  248. package/Types/JSONFunctions.ts +189 -43
  249. package/Types/Monitor/CephAlertTemplates.ts +157 -195
  250. package/Types/Monitor/DockerAlertTemplates.ts +338 -128
  251. package/Types/Monitor/DockerSwarmAlertTemplates.ts +95 -112
  252. package/Types/Monitor/HostAlertTemplates.ts +302 -130
  253. package/Types/Monitor/IotAlertTemplates.ts +112 -104
  254. package/Types/Monitor/KubernetesAlertTemplates.ts +415 -224
  255. package/Types/Monitor/KubernetesMetricCatalog.ts +23 -19
  256. package/Types/Monitor/PodmanAlertTemplates.ts +116 -163
  257. package/Types/Monitor/ProxmoxAlertTemplates.ts +217 -99
  258. package/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.ts +98 -6
  259. package/Types/Monitor/Recommendation/MonitorRecommendationUtil.ts +44 -7
  260. package/Types/Monitor/Recommendation/RecommendationCriteriaBuilder.ts +383 -0
  261. package/Types/Monitor/RumAlertTemplates.ts +46 -7
  262. package/Types/Monitor/SeriesContext/SeriesDebugHints.ts +596 -0
  263. package/Types/Monitor/SeriesContext/SeriesLabelDisplay.ts +554 -0
  264. package/Types/Monitor/ServiceAlertTemplates.ts +98 -11
  265. package/Types/Monitor/UptimeHistoryLabels.ts +56 -0
  266. package/Types/NotificationSetting/RoutineEmailEvents.ts +30 -0
  267. package/Types/Rum/SessionReplay.ts +209 -0
  268. package/Types/Rum/SessionReplayApi.ts +722 -0
  269. package/Types/Rum/SessionReplayCaptureTrigger.ts +21 -12
  270. package/Types/Rum/SessionReplayConsentMode.ts +11 -7
  271. package/Types/Rum/SessionReplayCustomEvents.ts +549 -0
  272. package/Types/Rum/SessionReplayHealth.ts +238 -0
  273. package/Types/Telemetry/TelemetryIngestSurface.ts +95 -0
  274. package/Types/Telemetry/TelemetryIngestionKeyPolicy.ts +75 -0
  275. package/Types/Telemetry/TelemetryIngestionKeyType.ts +29 -0
  276. package/Types/WebsiteRequest.ts +85 -3
  277. package/UI/Components/Charts/Area/AreaChart.tsx +13 -2
  278. package/UI/Components/Charts/Bar/BarChart.tsx +6 -2
  279. package/UI/Components/Charts/Line/LineChart.tsx +13 -2
  280. package/UI/Components/Charts/Types/XAxis/XAxis.ts +29 -0
  281. package/UI/Components/Charts/Utils/DataPoint.ts +22 -3
  282. package/UI/Components/Charts/Utils/TimeAnnotation.ts +76 -9
  283. package/UI/Components/Charts/Utils/XAxis.ts +217 -0
  284. package/UI/Components/CustomFields/CustomFieldsDetail.tsx +143 -25
  285. package/UI/Components/CustomFields/MapFromCustomFieldInput.tsx +216 -0
  286. package/UI/Components/Graphs/DayUptimeGraph.tsx +173 -8
  287. package/UI/Components/Graphs/UptimeBarTooltip.tsx +22 -278
  288. package/UI/Components/Graphs/UptimeDaySummary.tsx +327 -0
  289. package/UI/Components/Header/Header.tsx +25 -9
  290. package/UI/Components/Header/IconDropdown/IconDropdownItem.tsx +10 -1
  291. package/UI/Components/Header/IconDropdown/IconDropdownMenu.tsx +6 -1
  292. package/UI/Components/Header/ProjectPicker/ProjectPicker.tsx +6 -1
  293. package/UI/Components/HeaderAlert/NotificationBell/NotificationBellDropdown.tsx +6 -1
  294. package/UI/Components/Icon/Icon.tsx +11 -0
  295. package/UI/Components/JSONTable/JSONTable.tsx +2 -2
  296. package/UI/Components/KeyboardShortcut/KeyboardShortcutsModal.tsx +125 -0
  297. package/UI/Components/KeyboardShortcut/Screenshots/README.md +19 -0
  298. package/UI/Components/KeyboardShortcut/Screenshots/keyboard-shortcuts-command-palette.png +0 -0
  299. package/UI/Components/KeyboardShortcut/Screenshots/keyboard-shortcuts-dialog-dark.png +0 -0
  300. package/UI/Components/KeyboardShortcut/Screenshots/keyboard-shortcuts-dialog.png +0 -0
  301. package/UI/Components/KeyboardShortcut/Screenshots/keyboard-shortcuts-help-menu.png +0 -0
  302. package/UI/Components/KeyboardShortcut/Screenshots/keyboard-shortcuts-narrow.png +0 -0
  303. package/UI/Components/ModelTable/Column.ts +15 -0
  304. package/UI/Components/Monitor/SeriesDebugCommandsViewer.tsx +64 -0
  305. package/UI/Components/Monitor/SeriesLabelsViewer.tsx +93 -0
  306. package/UI/Components/MonitorGraphs/Uptime.tsx +14 -2
  307. package/UI/Components/MonitorGraphs/UptimeBarDayModal.tsx +63 -10
  308. package/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.ts +44 -0
  309. package/UI/Components/StatusPage/ResourceGroupSection.tsx +40 -0
  310. package/UI/Components/Table/CellClassName.ts +81 -0
  311. package/UI/Components/Table/TableRow.tsx +20 -14
  312. package/UI/Components/Table/TableSkeletonRows.tsx +9 -9
  313. package/UI/Components/Table/Types/Column.ts +36 -0
  314. package/UI/Components/Tooltip/Tooltip.tsx +11 -1
  315. package/UI/Components/Workflow/ComponentsModal.tsx +60 -39
  316. package/UI/Components/Workflow/NodePlacement.ts +57 -0
  317. package/UI/Components/Workflow/Workflow.tsx +47 -18
  318. package/UI/Config.ts +15 -33
  319. package/UI/Utils/GlobalKeyboardShortcut.ts +208 -0
  320. package/UI/Utils/Telemetry/BrowserTelemetryConfig.ts +28 -0
  321. package/UI/Utils/Telemetry/Telemetry.ts +10 -8
  322. package/Utils/API.ts +129 -3
  323. package/Utils/HTTPResponseBodyReader.ts +221 -0
  324. package/Utils/NetworkDiscovery/DiscoveryScanStatus.ts +77 -0
  325. package/Utils/Rum/ChunkMath.ts +106 -0
  326. package/Utils/Rum/SessionReplayHealth.ts +732 -0
  327. package/Utils/Rum/SessionReplayStringMap.ts +226 -0
  328. package/Utils/Schema/ModelSchema.ts +1 -0
  329. package/Utils/StatusPage/ResourceSearch.ts +290 -0
  330. package/Utils/Telemetry/OriginAllowList.ts +355 -0
  331. package/Utils/Uptime/DayUptimeGraphUtil.ts +205 -0
  332. package/Utils/ValueFormatter.ts +35 -1
  333. package/build/dist/Models/AnalyticsModels/RumSession.js +102 -19
  334. package/build/dist/Models/AnalyticsModels/RumSession.js.map +1 -1
  335. package/build/dist/Models/AnalyticsModels/RumSessionChunk.js +11 -9
  336. package/build/dist/Models/AnalyticsModels/RumSessionChunk.js.map +1 -1
  337. package/build/dist/Models/DatabaseModels/AlertCustomField.js +79 -3
  338. package/build/dist/Models/DatabaseModels/AlertCustomField.js.map +1 -1
  339. package/build/dist/Models/DatabaseModels/ApiKeyPermission.js +3 -27
  340. package/build/dist/Models/DatabaseModels/ApiKeyPermission.js.map +1 -1
  341. package/build/dist/Models/DatabaseModels/IncidentCustomField.js +79 -3
  342. package/build/dist/Models/DatabaseModels/IncidentCustomField.js.map +1 -1
  343. package/build/dist/Models/DatabaseModels/InventoryItemCustomField.js +79 -3
  344. package/build/dist/Models/DatabaseModels/InventoryItemCustomField.js.map +1 -1
  345. package/build/dist/Models/DatabaseModels/MonitorCustomField.js +79 -3
  346. package/build/dist/Models/DatabaseModels/MonitorCustomField.js.map +1 -1
  347. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyCustomField.js +79 -3
  348. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyCustomField.js.map +1 -1
  349. package/build/dist/Models/DatabaseModels/RumApplication.js +6 -6
  350. package/build/dist/Models/DatabaseModels/RumApplication.js.map +1 -1
  351. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceCustomField.js +79 -3
  352. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceCustomField.js.map +1 -1
  353. package/build/dist/Models/DatabaseModels/StatusPageCustomField.js +79 -3
  354. package/build/dist/Models/DatabaseModels/StatusPageCustomField.js.map +1 -1
  355. package/build/dist/Models/DatabaseModels/TeamCustomField.js +79 -3
  356. package/build/dist/Models/DatabaseModels/TeamCustomField.js.map +1 -1
  357. package/build/dist/Models/DatabaseModels/TeamMember.js +0 -9
  358. package/build/dist/Models/DatabaseModels/TeamMember.js.map +1 -1
  359. package/build/dist/Models/DatabaseModels/TeamMemberCustomField.js +79 -3
  360. package/build/dist/Models/DatabaseModels/TeamMemberCustomField.js.map +1 -1
  361. package/build/dist/Models/DatabaseModels/TeamPermission.js +3 -24
  362. package/build/dist/Models/DatabaseModels/TeamPermission.js.map +1 -1
  363. package/build/dist/Models/DatabaseModels/TelemetryIngestionKey.js +267 -0
  364. package/build/dist/Models/DatabaseModels/TelemetryIngestionKey.js.map +1 -1
  365. package/build/dist/Models/DatabaseModels/UserTelegram.js +0 -4
  366. package/build/dist/Models/DatabaseModels/UserTelegram.js.map +1 -1
  367. package/build/dist/Server/API/TelemetryAPI.js +769 -109
  368. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  369. package/build/dist/Server/API/UserNotificationSettingAPI.js +35 -0
  370. package/build/dist/Server/API/UserNotificationSettingAPI.js.map +1 -0
  371. package/build/dist/Server/API/UserTelegramAPI.js +28 -6
  372. package/build/dist/Server/API/UserTelegramAPI.js.map +1 -1
  373. package/build/dist/Server/EnvironmentConfig.js +29 -2
  374. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  375. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791300000000-AddTelemetryIngestionKeyType.js +60 -0
  376. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791300000000-AddTelemetryIngestionKeyType.js.map +1 -0
  377. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791400000000-SessionReplayRecordEverySessionByDefault.js +53 -0
  378. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791400000000-SessionReplayRecordEverySessionByDefault.js.map +1 -0
  379. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791500000000-WidenCustomFieldDropdownOptions.js +82 -0
  380. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791500000000-WidenCustomFieldDropdownOptions.js.map +1 -0
  381. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791600000000-AddCustomFieldValueMapping.js +46 -0
  382. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791600000000-AddCustomFieldValueMapping.js.map +1 -0
  383. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
  384. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  385. package/build/dist/Server/Infrastructure/QueueWorker.js +19 -7
  386. package/build/dist/Server/Infrastructure/QueueWorker.js.map +1 -1
  387. package/build/dist/Server/Infrastructure/Semaphore.js +1 -0
  388. package/build/dist/Server/Infrastructure/Semaphore.js.map +1 -1
  389. package/build/dist/Server/Middleware/TelemetryIngest.js +315 -8
  390. package/build/dist/Server/Middleware/TelemetryIngest.js.map +1 -1
  391. package/build/dist/Server/Services/AccessTokenService.js +1 -0
  392. package/build/dist/Server/Services/AccessTokenService.js.map +1 -1
  393. package/build/dist/Server/Services/AlertCustomFieldService.js +93 -0
  394. package/build/dist/Server/Services/AlertCustomFieldService.js.map +1 -1
  395. package/build/dist/Server/Services/AlertService.js +34 -0
  396. package/build/dist/Server/Services/AlertService.js.map +1 -1
  397. package/build/dist/Server/Services/ApiKeyPermissionService.js +331 -28
  398. package/build/dist/Server/Services/ApiKeyPermissionService.js.map +1 -1
  399. package/build/dist/Server/Services/CustomFieldMappingService.js +614 -0
  400. package/build/dist/Server/Services/CustomFieldMappingService.js.map +1 -0
  401. package/build/dist/Server/Services/GlobalConfigService.js +133 -0
  402. package/build/dist/Server/Services/GlobalConfigService.js.map +1 -1
  403. package/build/dist/Server/Services/IncidentCustomFieldService.js +93 -0
  404. package/build/dist/Server/Services/IncidentCustomFieldService.js.map +1 -1
  405. package/build/dist/Server/Services/IncidentService.js +32 -0
  406. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  407. package/build/dist/Server/Services/MetricService.js +163 -0
  408. package/build/dist/Server/Services/MetricService.js.map +1 -1
  409. package/build/dist/Server/Services/MonitorService.js +29 -0
  410. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  411. package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js +79 -27
  412. package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js.map +1 -1
  413. package/build/dist/Server/Services/ProjectService.js +22 -0
  414. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  415. package/build/dist/Server/Services/RoutineEmailSettingsService.js +69 -0
  416. package/build/dist/Server/Services/RoutineEmailSettingsService.js.map +1 -0
  417. package/build/dist/Server/Services/RumSessionReplayViewService.js +86 -24
  418. package/build/dist/Server/Services/RumSessionReplayViewService.js.map +1 -1
  419. package/build/dist/Server/Services/ScheduledMaintenanceCustomFieldService.js +93 -0
  420. package/build/dist/Server/Services/ScheduledMaintenanceCustomFieldService.js.map +1 -1
  421. package/build/dist/Server/Services/ScheduledMaintenanceService.js +29 -0
  422. package/build/dist/Server/Services/ScheduledMaintenanceService.js.map +1 -1
  423. package/build/dist/Server/Services/TeamMemberService.js +62 -2
  424. package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
  425. package/build/dist/Server/Services/TeamPermissionService.js +166 -3
  426. package/build/dist/Server/Services/TeamPermissionService.js.map +1 -1
  427. package/build/dist/Server/Services/TelemetryIngestionKeyService.js +562 -18
  428. package/build/dist/Server/Services/TelemetryIngestionKeyService.js.map +1 -1
  429. package/build/dist/Server/Services/UserNotificationSettingService.js +26 -6
  430. package/build/dist/Server/Services/UserNotificationSettingService.js.map +1 -1
  431. package/build/dist/Server/Services/UserTelegramService.js +257 -4
  432. package/build/dist/Server/Services/UserTelegramService.js.map +1 -1
  433. package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
  434. package/build/dist/Server/Utils/APIKey/AccessPermission.js +2 -2
  435. package/build/dist/Server/Utils/APIKey/AccessPermission.js.map +1 -1
  436. package/build/dist/Server/Utils/CustomField/CustomFieldDefinitionMappingHooks.js +27 -0
  437. package/build/dist/Server/Utils/CustomField/CustomFieldDefinitionMappingHooks.js.map +1 -0
  438. package/build/dist/Server/Utils/CustomField/CustomFieldMappingRegistry.js +226 -0
  439. package/build/dist/Server/Utils/CustomField/CustomFieldMappingRegistry.js.map +1 -0
  440. package/build/dist/Server/Utils/CustomField/CustomFieldMappingValidator.js +189 -0
  441. package/build/dist/Server/Utils/CustomField/CustomFieldMappingValidator.js.map +1 -0
  442. package/build/dist/Server/Utils/DataSource/EgressGuard.js +121 -7
  443. package/build/dist/Server/Utils/DataSource/EgressGuard.js.map +1 -1
  444. package/build/dist/Server/Utils/EmailRollup/EmailRollupConstants.js +38 -5
  445. package/build/dist/Server/Utils/EmailRollup/EmailRollupConstants.js.map +1 -1
  446. package/build/dist/Server/Utils/EmailRollup/EmailRollupFlushRunner.js +44 -2
  447. package/build/dist/Server/Utils/EmailRollup/EmailRollupFlushRunner.js.map +1 -1
  448. package/build/dist/Server/Utils/FrontendEnvironment.js +33 -0
  449. package/build/dist/Server/Utils/FrontendEnvironment.js.map +1 -0
  450. package/build/dist/Server/Utils/LogRedaction.js +20 -0
  451. package/build/dist/Server/Utils/LogRedaction.js.map +1 -1
  452. package/build/dist/Server/Utils/Logger.js +10 -0
  453. package/build/dist/Server/Utils/Logger.js.map +1 -1
  454. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +287 -68
  455. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js.map +1 -1
  456. package/build/dist/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.js +20 -4
  457. package/build/dist/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.js.map +1 -1
  458. package/build/dist/Server/Utils/Monitor/MonitorAlert.js +29 -6
  459. package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
  460. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +88 -13
  461. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  462. package/build/dist/Server/Utils/Monitor/MonitorIncident.js +22 -6
  463. package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
  464. package/build/dist/Server/Utils/Monitor/MonitorTemplateUtil.js +22 -0
  465. package/build/dist/Server/Utils/Monitor/MonitorTemplateUtil.js.map +1 -1
  466. package/build/dist/Server/Utils/Monitor/SeriesContextEnricher.js +237 -0
  467. package/build/dist/Server/Utils/Monitor/SeriesContextEnricher.js.map +1 -0
  468. package/build/dist/Server/Utils/SSRFProtection.js +136 -9
  469. package/build/dist/Server/Utils/SSRFProtection.js.map +1 -1
  470. package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCache.js +174 -89
  471. package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCache.js.map +1 -1
  472. package/build/dist/Server/Utils/SessionReplay/SessionReplayHealthCounters.js +223 -0
  473. package/build/dist/Server/Utils/SessionReplay/SessionReplayHealthCounters.js.map +1 -0
  474. package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js +945 -164
  475. package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js.map +1 -1
  476. package/build/dist/Server/Utils/StartServer.js +3 -14
  477. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  478. package/build/dist/Server/Utils/TelegramVerificationToken.js +129 -0
  479. package/build/dist/Server/Utils/TelegramVerificationToken.js.map +1 -0
  480. package/build/dist/Server/Utils/Telemetry/PinServiceName.js +163 -0
  481. package/build/dist/Server/Utils/Telemetry/PinServiceName.js.map +1 -0
  482. package/build/dist/Server/Utils/Telemetry/TelemetryFanInWriter.js +28 -15
  483. package/build/dist/Server/Utils/Telemetry/TelemetryFanInWriter.js.map +1 -1
  484. package/build/dist/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.js +64 -0
  485. package/build/dist/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.js.map +1 -0
  486. package/build/dist/Server/Utils/Telemetry/TelemetryIngestionKeyRateLimiter.js +171 -0
  487. package/build/dist/Server/Utils/Telemetry/TelemetryIngestionKeyRateLimiter.js.map +1 -0
  488. package/build/dist/Server/Utils/VM/VMAPI.js.map +1 -1
  489. package/build/dist/Server/Utils/VM/VMRunner.js +780 -69
  490. package/build/dist/Server/Utils/VM/VMRunner.js.map +1 -1
  491. package/build/dist/Types/CustomField/CustomFieldMappingCatalog.js +60 -0
  492. package/build/dist/Types/CustomField/CustomFieldMappingCatalog.js.map +1 -0
  493. package/build/dist/Types/CustomField/CustomFieldMappingSourceResource.js +20 -0
  494. package/build/dist/Types/CustomField/CustomFieldMappingSourceResource.js.map +1 -0
  495. package/build/dist/Types/CustomField/CustomFieldValueMapping.js +141 -0
  496. package/build/dist/Types/CustomField/CustomFieldValueMapping.js.map +1 -0
  497. package/build/dist/Types/Icon/IconProp.js +1 -0
  498. package/build/dist/Types/Icon/IconProp.js.map +1 -1
  499. package/build/dist/Types/JSONFunctions.js +118 -37
  500. package/build/dist/Types/JSONFunctions.js.map +1 -1
  501. package/build/dist/Types/Monitor/CephAlertTemplates.js +130 -171
  502. package/build/dist/Types/Monitor/CephAlertTemplates.js.map +1 -1
  503. package/build/dist/Types/Monitor/DockerAlertTemplates.js +286 -121
  504. package/build/dist/Types/Monitor/DockerAlertTemplates.js.map +1 -1
  505. package/build/dist/Types/Monitor/DockerSwarmAlertTemplates.js +84 -105
  506. package/build/dist/Types/Monitor/DockerSwarmAlertTemplates.js.map +1 -1
  507. package/build/dist/Types/Monitor/HostAlertTemplates.js +250 -122
  508. package/build/dist/Types/Monitor/HostAlertTemplates.js.map +1 -1
  509. package/build/dist/Types/Monitor/IotAlertTemplates.js +81 -81
  510. package/build/dist/Types/Monitor/IotAlertTemplates.js.map +1 -1
  511. package/build/dist/Types/Monitor/KubernetesAlertTemplates.js +375 -215
  512. package/build/dist/Types/Monitor/KubernetesAlertTemplates.js.map +1 -1
  513. package/build/dist/Types/Monitor/KubernetesMetricCatalog.js +19 -19
  514. package/build/dist/Types/Monitor/KubernetesMetricCatalog.js.map +1 -1
  515. package/build/dist/Types/Monitor/PodmanAlertTemplates.js +104 -153
  516. package/build/dist/Types/Monitor/PodmanAlertTemplates.js.map +1 -1
  517. package/build/dist/Types/Monitor/ProxmoxAlertTemplates.js +156 -91
  518. package/build/dist/Types/Monitor/ProxmoxAlertTemplates.js.map +1 -1
  519. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.js +67 -6
  520. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.js.map +1 -1
  521. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationUtil.js +38 -7
  522. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationUtil.js.map +1 -1
  523. package/build/dist/Types/Monitor/Recommendation/RecommendationCriteriaBuilder.js +261 -0
  524. package/build/dist/Types/Monitor/Recommendation/RecommendationCriteriaBuilder.js.map +1 -0
  525. package/build/dist/Types/Monitor/RumAlertTemplates.js +46 -7
  526. package/build/dist/Types/Monitor/RumAlertTemplates.js.map +1 -1
  527. package/build/dist/Types/Monitor/SeriesContext/SeriesDebugHints.js +422 -0
  528. package/build/dist/Types/Monitor/SeriesContext/SeriesDebugHints.js.map +1 -0
  529. package/build/dist/Types/Monitor/SeriesContext/SeriesLabelDisplay.js +441 -0
  530. package/build/dist/Types/Monitor/SeriesContext/SeriesLabelDisplay.js.map +1 -0
  531. package/build/dist/Types/Monitor/ServiceAlertTemplates.js +79 -15
  532. package/build/dist/Types/Monitor/ServiceAlertTemplates.js.map +1 -1
  533. package/build/dist/Types/Monitor/UptimeHistoryLabels.js +17 -0
  534. package/build/dist/Types/Monitor/UptimeHistoryLabels.js.map +1 -0
  535. package/build/dist/Types/NotificationSetting/RoutineEmailEvents.js +29 -0
  536. package/build/dist/Types/NotificationSetting/RoutineEmailEvents.js.map +1 -0
  537. package/build/dist/Types/Rum/SessionReplay.js +117 -0
  538. package/build/dist/Types/Rum/SessionReplay.js.map +1 -1
  539. package/build/dist/Types/Rum/SessionReplayApi.js +181 -0
  540. package/build/dist/Types/Rum/SessionReplayApi.js.map +1 -0
  541. package/build/dist/Types/Rum/SessionReplayCaptureTrigger.js +21 -12
  542. package/build/dist/Types/Rum/SessionReplayCaptureTrigger.js.map +1 -1
  543. package/build/dist/Types/Rum/SessionReplayConsentMode.js +11 -7
  544. package/build/dist/Types/Rum/SessionReplayConsentMode.js.map +1 -1
  545. package/build/dist/Types/Rum/SessionReplayCustomEvents.js +172 -0
  546. package/build/dist/Types/Rum/SessionReplayCustomEvents.js.map +1 -0
  547. package/build/dist/Types/Rum/SessionReplayHealth.js +37 -0
  548. package/build/dist/Types/Rum/SessionReplayHealth.js.map +1 -0
  549. package/build/dist/Types/Telemetry/TelemetryIngestSurface.js +85 -0
  550. package/build/dist/Types/Telemetry/TelemetryIngestSurface.js.map +1 -0
  551. package/build/dist/Types/Telemetry/TelemetryIngestionKeyPolicy.js +15 -0
  552. package/build/dist/Types/Telemetry/TelemetryIngestionKeyPolicy.js.map +1 -0
  553. package/build/dist/Types/Telemetry/TelemetryIngestionKeyType.js +30 -0
  554. package/build/dist/Types/Telemetry/TelemetryIngestionKeyType.js.map +1 -0
  555. package/build/dist/Types/WebsiteRequest.js +55 -3
  556. package/build/dist/Types/WebsiteRequest.js.map +1 -1
  557. package/build/dist/UI/Components/Charts/Area/AreaChart.js +13 -2
  558. package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
  559. package/build/dist/UI/Components/Charts/Bar/BarChart.js +6 -2
  560. package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
  561. package/build/dist/UI/Components/Charts/Line/LineChart.js +13 -2
  562. package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
  563. package/build/dist/UI/Components/Charts/Types/XAxis/XAxis.js.map +1 -1
  564. package/build/dist/UI/Components/Charts/Utils/DataPoint.js +15 -3
  565. package/build/dist/UI/Components/Charts/Utils/DataPoint.js.map +1 -1
  566. package/build/dist/UI/Components/Charts/Utils/TimeAnnotation.js +40 -9
  567. package/build/dist/UI/Components/Charts/Utils/TimeAnnotation.js.map +1 -1
  568. package/build/dist/UI/Components/Charts/Utils/XAxis.js +186 -0
  569. package/build/dist/UI/Components/Charts/Utils/XAxis.js.map +1 -1
  570. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js +80 -6
  571. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js.map +1 -1
  572. package/build/dist/UI/Components/CustomFields/MapFromCustomFieldInput.js +118 -0
  573. package/build/dist/UI/Components/CustomFields/MapFromCustomFieldInput.js.map +1 -0
  574. package/build/dist/UI/Components/Graphs/DayUptimeGraph.js +112 -7
  575. package/build/dist/UI/Components/Graphs/DayUptimeGraph.js.map +1 -1
  576. package/build/dist/UI/Components/Graphs/UptimeBarTooltip.js +5 -162
  577. package/build/dist/UI/Components/Graphs/UptimeBarTooltip.js.map +1 -1
  578. package/build/dist/UI/Components/Graphs/UptimeDaySummary.js +181 -0
  579. package/build/dist/UI/Components/Graphs/UptimeDaySummary.js.map +1 -0
  580. package/build/dist/UI/Components/Header/Header.js +2 -3
  581. package/build/dist/UI/Components/Header/Header.js.map +1 -1
  582. package/build/dist/UI/Components/Header/IconDropdown/IconDropdownItem.js +2 -1
  583. package/build/dist/UI/Components/Header/IconDropdown/IconDropdownItem.js.map +1 -1
  584. package/build/dist/UI/Components/Header/IconDropdown/IconDropdownMenu.js +7 -1
  585. package/build/dist/UI/Components/Header/IconDropdown/IconDropdownMenu.js.map +1 -1
  586. package/build/dist/UI/Components/Header/ProjectPicker/ProjectPicker.js +7 -1
  587. package/build/dist/UI/Components/Header/ProjectPicker/ProjectPicker.js.map +1 -1
  588. package/build/dist/UI/Components/HeaderAlert/NotificationBell/NotificationBellDropdown.js +7 -1
  589. package/build/dist/UI/Components/HeaderAlert/NotificationBell/NotificationBellDropdown.js.map +1 -1
  590. package/build/dist/UI/Components/Icon/Icon.js +5 -0
  591. package/build/dist/UI/Components/Icon/Icon.js.map +1 -1
  592. package/build/dist/UI/Components/JSONTable/JSONTable.js +2 -2
  593. package/build/dist/UI/Components/JSONTable/JSONTable.js.map +1 -1
  594. package/build/dist/UI/Components/KeyboardShortcut/KeyboardShortcutsModal.js +35 -0
  595. package/build/dist/UI/Components/KeyboardShortcut/KeyboardShortcutsModal.js.map +1 -0
  596. package/build/dist/UI/Components/Monitor/SeriesDebugCommandsViewer.js +34 -0
  597. package/build/dist/UI/Components/Monitor/SeriesDebugCommandsViewer.js.map +1 -0
  598. package/build/dist/UI/Components/Monitor/SeriesLabelsViewer.js +49 -0
  599. package/build/dist/UI/Components/Monitor/SeriesLabelsViewer.js.map +1 -0
  600. package/build/dist/UI/Components/MonitorGraphs/Uptime.js +1 -1
  601. package/build/dist/UI/Components/MonitorGraphs/Uptime.js.map +1 -1
  602. package/build/dist/UI/Components/MonitorGraphs/UptimeBarDayModal.js +32 -6
  603. package/build/dist/UI/Components/MonitorGraphs/UptimeBarDayModal.js.map +1 -1
  604. package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.js +38 -0
  605. package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.js.map +1 -1
  606. package/build/dist/UI/Components/StatusPage/ResourceGroupSection.js +22 -1
  607. package/build/dist/UI/Components/StatusPage/ResourceGroupSection.js.map +1 -1
  608. package/build/dist/UI/Components/Table/CellClassName.js +55 -0
  609. package/build/dist/UI/Components/Table/CellClassName.js.map +1 -0
  610. package/build/dist/UI/Components/Table/TableRow.js +16 -13
  611. package/build/dist/UI/Components/Table/TableRow.js.map +1 -1
  612. package/build/dist/UI/Components/Table/TableSkeletonRows.js +9 -8
  613. package/build/dist/UI/Components/Table/TableSkeletonRows.js.map +1 -1
  614. package/build/dist/UI/Components/Tooltip/Tooltip.js +25 -1
  615. package/build/dist/UI/Components/Tooltip/Tooltip.js.map +1 -1
  616. package/build/dist/UI/Components/Workflow/ComponentsModal.js +47 -30
  617. package/build/dist/UI/Components/Workflow/ComponentsModal.js.map +1 -1
  618. package/build/dist/UI/Components/Workflow/NodePlacement.js +37 -0
  619. package/build/dist/UI/Components/Workflow/NodePlacement.js.map +1 -0
  620. package/build/dist/UI/Components/Workflow/Workflow.js +35 -18
  621. package/build/dist/UI/Components/Workflow/Workflow.js.map +1 -1
  622. package/build/dist/UI/Config.js +10 -20
  623. package/build/dist/UI/Config.js.map +1 -1
  624. package/build/dist/UI/Utils/GlobalKeyboardShortcut.js +138 -0
  625. package/build/dist/UI/Utils/GlobalKeyboardShortcut.js.map +1 -0
  626. package/build/dist/UI/Utils/Telemetry/BrowserTelemetryConfig.js +14 -0
  627. package/build/dist/UI/Utils/Telemetry/BrowserTelemetryConfig.js.map +1 -0
  628. package/build/dist/UI/Utils/Telemetry/Telemetry.js +7 -5
  629. package/build/dist/UI/Utils/Telemetry/Telemetry.js.map +1 -1
  630. package/build/dist/Utils/API.js +67 -5
  631. package/build/dist/Utils/API.js.map +1 -1
  632. package/build/dist/Utils/HTTPResponseBodyReader.js +157 -0
  633. package/build/dist/Utils/HTTPResponseBodyReader.js.map +1 -0
  634. package/build/dist/Utils/NetworkDiscovery/DiscoveryScanStatus.js +69 -0
  635. package/build/dist/Utils/NetworkDiscovery/DiscoveryScanStatus.js.map +1 -0
  636. package/build/dist/Utils/Rum/ChunkMath.js +76 -0
  637. package/build/dist/Utils/Rum/ChunkMath.js.map +1 -1
  638. package/build/dist/Utils/Rum/SessionReplayHealth.js +534 -0
  639. package/build/dist/Utils/Rum/SessionReplayHealth.js.map +1 -0
  640. package/build/dist/Utils/Rum/SessionReplayStringMap.js +165 -0
  641. package/build/dist/Utils/Rum/SessionReplayStringMap.js.map +1 -0
  642. package/build/dist/Utils/Schema/ModelSchema.js +1 -0
  643. package/build/dist/Utils/Schema/ModelSchema.js.map +1 -1
  644. package/build/dist/Utils/StatusPage/ResourceSearch.js +200 -0
  645. package/build/dist/Utils/StatusPage/ResourceSearch.js.map +1 -0
  646. package/build/dist/Utils/Telemetry/OriginAllowList.js +299 -0
  647. package/build/dist/Utils/Telemetry/OriginAllowList.js.map +1 -0
  648. package/build/dist/Utils/Uptime/DayUptimeGraphUtil.js +147 -0
  649. package/build/dist/Utils/Uptime/DayUptimeGraphUtil.js.map +1 -0
  650. package/build/dist/Utils/ValueFormatter.js +32 -1
  651. package/build/dist/Utils/ValueFormatter.js.map +1 -1
  652. package/package.json +1 -1
@@ -0,0 +1,1378 @@
1
+ import RumSessionService from "../../../../Server/Services/RumSessionService";
2
+ import RumSessionChunkService from "../../../../Server/Services/RumSessionChunkService";
3
+ import ExceptionInstanceService from "../../../../Server/Services/ExceptionInstanceService";
4
+ import { Statement } from "../../../../Server/Utils/AnalyticsDatabase/Statement";
5
+ import SessionReplayReadService, {
6
+ MAX_LIST_ROUTES,
7
+ SESSION_REPLAY_ACTIVITY_SUMMARY_CACHE_TTL_MS,
8
+ SessionReplayApplicationActivitySummary,
9
+ SessionReplayChunkReadResult,
10
+ SessionReplayExceptionSession,
11
+ SessionReplayExpiredSessionInfo,
12
+ SessionReplayListFilters,
13
+ SessionReplayListItem,
14
+ SessionReplayListRequest,
15
+ SessionReplayListResult,
16
+ SessionReplayManifest,
17
+ SessionReplaySessionHeader,
18
+ SessionReplaySessionIdentity,
19
+ } from "../../../../Server/Utils/SessionReplay/SessionReplayReadService";
20
+ import BadDataException from "../../../../Types/Exception/BadDataException";
21
+ import { JSONObject } from "../../../../Types/JSON";
22
+ import ObjectID from "../../../../Types/ObjectID";
23
+ import { MAX_SESSION_REPLAY_READ_BYTES } from "../../../../Types/Rum/SessionReplay";
24
+ import { SessionReplaySortBy } from "../../../../Types/Rum/SessionReplayApi";
25
+ import { afterEach, beforeEach, describe, expect, test } from "@jest/globals";
26
+
27
+ /*
28
+ * Statement-text tests for the bespoke ClickHouse reads. Nothing here
29
+ * talks to a database: executeQuery is spied and the SQL it would have
30
+ * run is asserted. What is pinned is the shape that keeps the reads
31
+ * correct on a ReplacingMergeTree and cheap under the sort key: every
32
+ * list predicate is a HAVING clause over an argMax alias (never a raw
33
+ * column, which would match a superseded header version), the WHERE
34
+ * stays the (projectId, rumApplicationId, startTime) prefix, the
35
+ * payload column is named by exactly one read, and the identity columns
36
+ * are named only when the caller asked for them.
37
+ */
38
+
39
+ function fakeResultSet(rows: Array<JSONObject>): unknown {
40
+ return {
41
+ json: async (): Promise<JSONObject> => {
42
+ return { data: rows } as unknown as JSONObject;
43
+ },
44
+ };
45
+ }
46
+
47
+ function statementOf(spy: jest.SpyInstance, call: number = 0): Statement {
48
+ const statement: Statement | undefined = spy.mock.calls[call]?.[0] as
49
+ | Statement
50
+ | undefined;
51
+
52
+ if (!statement) {
53
+ throw new Error(`executeQuery call ${call} was not made`);
54
+ }
55
+
56
+ return statement;
57
+ }
58
+
59
+ function boundValues(statement: Statement): Array<unknown> {
60
+ return Object.values(statement.query_params);
61
+ }
62
+
63
+ /* The HAVING section of a list statement, so WHERE-level leaks fail. */
64
+ function havingSection(query: string): string {
65
+ const index: number = query.indexOf("HAVING 1 = 1");
66
+
67
+ if (index < 0) {
68
+ throw new Error("Statement has no HAVING section");
69
+ }
70
+
71
+ return query.substring(index);
72
+ }
73
+
74
+ function whereSection(query: string): string {
75
+ const start: number = query.indexOf("WHERE");
76
+ const end: number = query.indexOf("GROUP BY");
77
+
78
+ return query.substring(start, end > 0 ? end : undefined);
79
+ }
80
+
81
+ describe("SessionReplayReadService statements", () => {
82
+ const projectId: ObjectID = ObjectID.generate();
83
+ const rumApplicationId: ObjectID = ObjectID.generate();
84
+
85
+ let headerQuerySpy: jest.SpyInstance;
86
+ let chunkQuerySpy: jest.SpyInstance;
87
+ let exceptionQuerySpy: jest.SpyInstance;
88
+
89
+ beforeEach(() => {
90
+ jest.clearAllMocks();
91
+ SessionReplayReadService.clearActivitySummaryCache();
92
+ SessionReplayReadService.setPublishedRecorderVersionProvider(null);
93
+
94
+ headerQuerySpy = jest
95
+ .spyOn(RumSessionService, "executeQuery")
96
+ .mockResolvedValue(fakeResultSet([]) as never);
97
+ chunkQuerySpy = jest
98
+ .spyOn(RumSessionChunkService, "executeQuery")
99
+ .mockResolvedValue(fakeResultSet([]) as never);
100
+ exceptionQuerySpy = jest
101
+ .spyOn(ExceptionInstanceService, "executeQuery")
102
+ .mockResolvedValue(fakeResultSet([]) as never);
103
+ });
104
+
105
+ afterEach(() => {
106
+ jest.restoreAllMocks();
107
+ });
108
+
109
+ function listRequest(
110
+ overrides: Partial<SessionReplayListRequest> = {},
111
+ ): SessionReplayListRequest {
112
+ return {
113
+ projectId: projectId,
114
+ rumApplicationId: rumApplicationId,
115
+ startTime: new Date("2026-08-01T00:00:00.000Z"),
116
+ endTime: new Date("2026-08-08T00:00:00.000Z"),
117
+ filters: {},
118
+ limit: 20,
119
+ includeIdentifiedUserLabel: false,
120
+ ...overrides,
121
+ };
122
+ }
123
+
124
+ async function listQuery(
125
+ filters: SessionReplayListFilters,
126
+ overrides: Partial<SessionReplayListRequest> = {},
127
+ ): Promise<string> {
128
+ await SessionReplayReadService.listSessions(
129
+ listRequest({ filters: filters, ...overrides }),
130
+ );
131
+
132
+ return statementOf(headerQuerySpy).query;
133
+ }
134
+
135
+ describe("list predicates are HAVING clauses over argMax aliases", () => {
136
+ test("hasIdentifiedUser tests the digest alias, never the label", async () => {
137
+ const positive: string = await listQuery({ hasIdentifiedUser: true });
138
+ expect(havingSection(positive)).toContain(
139
+ "AND aggIdentifiedUserKey != ''",
140
+ );
141
+ expect(whereSection(positive)).not.toContain("identifiedUserKey");
142
+ expect(positive).not.toContain("identifiedUserLabel");
143
+
144
+ headerQuerySpy.mockClear();
145
+
146
+ const negative: string = await listQuery({ hasIdentifiedUser: false });
147
+ expect(havingSection(negative)).toContain(
148
+ "AND aggIdentifiedUserKey = ''",
149
+ );
150
+ });
151
+
152
+ test("isPlayable combines finalization, chunk count and the lost seal", async () => {
153
+ const playable: string = await listQuery({ isPlayable: true });
154
+ expect(havingSection(playable)).toContain(
155
+ "AND ((aggIsFinalized = 0 OR aggChunkCount > 0) AND aggSealedReason != 'recording-lost')",
156
+ );
157
+
158
+ headerQuerySpy.mockClear();
159
+
160
+ const unplayable: string = await listQuery({ isPlayable: false });
161
+ expect(havingSection(unplayable)).toContain(
162
+ "AND NOT ((aggIsFinalized = 0 OR aggChunkCount > 0) AND aggSealedReason != 'recording-lost')",
163
+ );
164
+ });
165
+
166
+ test("hasTraces tests the trace-count alias", async () => {
167
+ const query: string = await listQuery({ hasTraces: true });
168
+ expect(havingSection(query)).toContain("AND aggTraceCount > 0");
169
+ expect(query).toContain(
170
+ "toFloat64(length(argMax(traceIds, version))) AS aggTraceCount",
171
+ );
172
+
173
+ headerQuerySpy.mockClear();
174
+
175
+ const none: string = await listQuery({ hasTraces: false });
176
+ expect(havingSection(none)).toContain("AND aggTraceCount = 0");
177
+ });
178
+
179
+ /*
180
+ * The routes and entry URL stored on a header are scrubbed ABSOLUTE
181
+ * urls (https://host/path), but the filter a person types is a PATH -
182
+ * the search box routes anything beginning with "/" to this filter, and
183
+ * the docs promise `url:/checkout` outright. Matching the whole string
184
+ * only meant that documented search returned an empty list in every
185
+ * project, silently. Both arms are needed: the whole-URL one for a
186
+ * caller that pastes an absolute URL, the path() one for the path.
187
+ */
188
+ test("urlPrefix matches the PATH of a route and of the entry URL, as well as the whole URL", async () => {
189
+ const query: string = await listQuery({ urlPrefix: "/checkout" });
190
+ const having: string = havingSection(query);
191
+
192
+ expect(having).toMatch(
193
+ /AND \(arrayExists\(r -> startsWith\(r, \{p\d+:String\}\) OR startsWith\(path\(r\), \{p\d+:String\}\), aggRoutes\) OR startsWith\(aggEntryUrl, \{p\d+:String\}\) OR startsWith\(path\(aggEntryUrl\), \{p\d+:String\}\)\)/,
194
+ );
195
+ expect(query).toContain("argMax(routes, version) AS aggRoutes");
196
+ /* Bound four times, never interpolated. */
197
+ expect(query).not.toContain("'/checkout'");
198
+ expect(
199
+ boundValues(statementOf(headerQuerySpy)).filter(
200
+ (value: unknown): boolean => {
201
+ return value === "/checkout";
202
+ },
203
+ ),
204
+ ).toHaveLength(4);
205
+ });
206
+
207
+ test("tags require every pair through mapContains over the argMax'd map", async () => {
208
+ const query: string = await listQuery({
209
+ tags: { build: "1.2.3", tier: "enterprise" },
210
+ });
211
+ const having: string = havingSection(query);
212
+
213
+ expect(having).toMatch(
214
+ /AND mapContains\(aggTags, \{p\d+:String\}\) AND aggTags\[\{p\d+:String\}\] = \{p\d+:String\}/,
215
+ );
216
+ expect(having.match(/mapContains\(aggTags/g)).toHaveLength(2);
217
+ expect(query).toContain("argMax(tags, version) AS aggTags");
218
+
219
+ const bound: Array<unknown> = boundValues(statementOf(headerQuerySpy));
220
+ expect(bound).toContain("build");
221
+ expect(bound).toContain("1.2.3");
222
+ expect(bound).toContain("tier");
223
+ expect(bound).toContain("enterprise");
224
+ });
225
+
226
+ test("search covers sessionId, both URLs, routes and trace ids, binds the term, and omits the label by default", async () => {
227
+ const query: string = await listQuery({ search: "acme" });
228
+ const having: string = havingSection(query);
229
+
230
+ expect(having).toMatch(/startsWith\(sessionId, \{p\d+:String\}\)/);
231
+ expect(having).toMatch(
232
+ /positionCaseInsensitiveUTF8\(aggEntryUrl, \{p\d+:String\}\) > 0/,
233
+ );
234
+ expect(having).toMatch(
235
+ /positionCaseInsensitiveUTF8\(aggExitUrl, \{p\d+:String\}\) > 0/,
236
+ );
237
+ expect(having).toMatch(
238
+ /arrayExists\(r -> positionCaseInsensitiveUTF8\(r, \{p\d+:String\}\) > 0, aggRoutes\)/,
239
+ );
240
+ expect(having).toMatch(
241
+ /has\(argMax\(traceIds, version\), \{p\d+:String\}\)/,
242
+ );
243
+ expect(query).not.toContain("identifiedUserLabel");
244
+ expect(query).not.toContain("'acme'");
245
+ expect(boundValues(statementOf(headerQuerySpy))).toContain("acme");
246
+ });
247
+
248
+ test("search names the identified user label only when the caller may read it", async () => {
249
+ const query: string = await listQuery(
250
+ { search: "jane" },
251
+ { includeIdentifiedUserLabel: true },
252
+ );
253
+
254
+ expect(havingSection(query)).toMatch(
255
+ /positionCaseInsensitiveUTF8\(aggIdentifiedUserLabel, \{p\d+:String\}\) > 0/,
256
+ );
257
+ expect(query).toContain(
258
+ "argMax(identifiedUserLabel, version) AS aggIdentifiedUserLabel",
259
+ );
260
+ });
261
+
262
+ test("search is appended after the cheap boolean predicates", async () => {
263
+ const query: string = await listQuery({
264
+ search: "acme",
265
+ hasError: true,
266
+ hasTraces: true,
267
+ isPlayable: true,
268
+ });
269
+ const having: string = havingSection(query);
270
+
271
+ const searchAt: number = having.indexOf("startsWith(sessionId");
272
+ expect(searchAt).toBeGreaterThan(having.indexOf("aggHasError ="));
273
+ expect(searchAt).toBeGreaterThan(having.indexOf("aggTraceCount > 0"));
274
+ expect(searchAt).toBeGreaterThan(having.indexOf("aggIsFinalized = 0"));
275
+ });
276
+
277
+ test("the WHERE stays the sort-key prefix whatever filters are set", async () => {
278
+ const query: string = await listQuery({
279
+ search: "acme",
280
+ urlPrefix: "/x",
281
+ tags: { a: "b" },
282
+ hasTraces: true,
283
+ isPlayable: true,
284
+ hasIdentifiedUser: true,
285
+ route: "/y",
286
+ browserNames: ["Chrome"],
287
+ });
288
+ const where: string = whereSection(query);
289
+
290
+ expect(where).toContain("projectId = ");
291
+ expect(where).toContain("rumApplicationId = ");
292
+ expect(where).toContain("startTime >= ");
293
+ expect(where).toContain("startTime <= ");
294
+ expect(where).toContain("retentionDate >= now()");
295
+
296
+ for (const forbidden of [
297
+ "routes",
298
+ "tags",
299
+ "traceIds",
300
+ "entryUrl",
301
+ "browserName",
302
+ "identifiedUserKey",
303
+ "sealedReason",
304
+ ]) {
305
+ expect(where).not.toContain(forbidden);
306
+ }
307
+ });
308
+
309
+ test("the exact route filter runs over the routes alias", async () => {
310
+ const query: string = await listQuery({ route: "https://a/b" });
311
+ expect(havingSection(query)).toMatch(
312
+ /AND has\(aggRoutes, \{p\d+:String\}\)/,
313
+ );
314
+ });
315
+ });
316
+
317
+ describe("list sort and cursor", () => {
318
+ test("defaults to newest first with a sessionId tiebreak", async () => {
319
+ const query: string = await listQuery({});
320
+ expect(query).toContain("ORDER BY aggStartTime DESC, sessionId DESC");
321
+ });
322
+
323
+ test.each([
324
+ ["durationMs", "aggDurationMs"],
325
+ ["errorCount", "aggErrorCount"],
326
+ [
327
+ "frustration",
328
+ "(aggRageClickCount + aggDeadClickCount + aggErrorClickCount + aggRefreshRageCount)",
329
+ ],
330
+ ] as Array<[SessionReplaySortBy, string]>)(
331
+ "sortBy %s orders by its alias with a sessionId tiebreak",
332
+ async (sortBy: SessionReplaySortBy, expression: string) => {
333
+ const query: string = await listQuery({}, { sortBy: sortBy });
334
+ expect(query).toContain(`ORDER BY ${expression} DESC, sessionId DESC`);
335
+ },
336
+ );
337
+
338
+ test("an unknown sortBy is refused before any query", async () => {
339
+ await expect(
340
+ SessionReplayReadService.listSessions(
341
+ listRequest({ sortBy: "payloadBytes" as SessionReplaySortBy }),
342
+ ),
343
+ ).rejects.toBeInstanceOf(BadDataException);
344
+ expect(headerQuerySpy).not.toHaveBeenCalled();
345
+ });
346
+
347
+ test("a newest-first cursor bounds startTime in the WHERE and tiebreaks in HAVING", async () => {
348
+ const query: string = await listQuery(
349
+ {},
350
+ {
351
+ cursor: {
352
+ sortBy: "startTime",
353
+ sortValue: 1700000000000,
354
+ sessionId: "s-9",
355
+ },
356
+ },
357
+ );
358
+
359
+ /* The window's own upper bound plus the cursor's. */
360
+ expect(whereSection(query).match(/startTime <= /g)).toHaveLength(2);
361
+ expect(havingSection(query)).toMatch(
362
+ /AND \(aggStartTime < \{p\d+:Double\} OR \(aggStartTime = \{p\d+:Double\} AND sessionId < \{p\d+:String\}\)\)/,
363
+ );
364
+ });
365
+
366
+ test("a cursor on any other sort never touches the WHERE", async () => {
367
+ const query: string = await listQuery(
368
+ {},
369
+ {
370
+ sortBy: "errorCount",
371
+ cursor: { sortBy: "errorCount", sortValue: 4, sessionId: "s-9" },
372
+ },
373
+ );
374
+
375
+ expect(whereSection(query).match(/startTime <= /g)).toHaveLength(1);
376
+ expect(havingSection(query)).toMatch(
377
+ /AND \(aggErrorCount < \{p\d+:Double\} OR \(aggErrorCount = \{p\d+:Double\} AND sessionId < \{p\d+:String\}\)\)/,
378
+ );
379
+ });
380
+
381
+ test("a cursor from a different ordering is refused", async () => {
382
+ await expect(
383
+ SessionReplayReadService.listSessions(
384
+ listRequest({
385
+ sortBy: "durationMs",
386
+ cursor: { sortBy: "errorCount", sortValue: 4, sessionId: "s" },
387
+ }),
388
+ ),
389
+ ).rejects.toBeInstanceOf(BadDataException);
390
+ expect(headerQuerySpy).not.toHaveBeenCalled();
391
+ });
392
+
393
+ test("nextCursor carries the sort key of the last row, and only when a page follows", async () => {
394
+ const rows: Array<JSONObject> = [
395
+ { sessionId: "a", aggErrorCount: 9, aggStartTime: 3 },
396
+ { sessionId: "b", aggErrorCount: 4, aggStartTime: 2 },
397
+ { sessionId: "c", aggErrorCount: 1, aggStartTime: 1 },
398
+ ];
399
+ headerQuerySpy.mockResolvedValue(fakeResultSet(rows) as never);
400
+
401
+ const result: SessionReplayListResult =
402
+ await SessionReplayReadService.listSessions(
403
+ listRequest({ limit: 2, sortBy: "errorCount" }),
404
+ );
405
+
406
+ expect(result.sessions).toHaveLength(2);
407
+ expect(result.nextCursor).toEqual({
408
+ sortBy: "errorCount",
409
+ sortValue: 4,
410
+ sessionId: "b",
411
+ });
412
+
413
+ headerQuerySpy.mockResolvedValue(fakeResultSet(rows) as never);
414
+
415
+ const lastPage: SessionReplayListResult =
416
+ await SessionReplayReadService.listSessions(
417
+ listRequest({ limit: 3, sortBy: "errorCount" }),
418
+ );
419
+ expect(lastPage.nextCursor).toBeNull();
420
+ });
421
+ });
422
+
423
+ describe("list projections", () => {
424
+ test("selects the engagement, correlation and expiry columns and the live duration", async () => {
425
+ const query: string = await listQuery({});
426
+
427
+ for (const projection of [
428
+ "toFloat64(length(argMax(exceptionFingerprints, version))) AS aggExceptionGroupCount",
429
+ "toFloat64(argMax(clickCount, version)) AS aggClickCount",
430
+ "toFloat64(argMax(activeMs, version)) AS aggActiveMs",
431
+ "toFloat64(argMax(firstErrorOffsetMs, version)) AS aggFirstErrorOffsetMs",
432
+ "toFloat64(toUnixTimestamp(argMax(retentionDate, version))) * 1000 AS aggExpiresAt",
433
+ "argMax(tags, version) AS aggTags",
434
+ "argMax(routes, version) AS aggRoutes",
435
+ ]) {
436
+ expect(query).toContain(projection);
437
+ }
438
+
439
+ /*
440
+ * A provisional header says durationMs 0 for ten minutes; the span
441
+ * it asserts itself is the honest lower bound until then.
442
+ */
443
+ expect(query).toContain(
444
+ "toFloat64(if(argMax(isFinalized, version), toInt64(argMax(durationMs, version)), greatest(toInt64(argMax(durationMs, version)), toUnixTimestamp64Milli(argMax(endTime, version)) - toUnixTimestamp64Milli(argMax(startTime, version))))) AS aggDurationMs",
445
+ );
446
+
447
+ expect(query).not.toContain("identifiedUserTraits");
448
+ expect(query).not.toMatch(/\bpayload\b(?!Bytes)/);
449
+ expect(query).toContain("retentionDate >= now()");
450
+ });
451
+
452
+ test("maps the new projections, slicing routes and keeping the clock as numbers", async () => {
453
+ headerQuerySpy.mockResolvedValue(
454
+ fakeResultSet([
455
+ {
456
+ sessionId: "s-1",
457
+ applicationId: rumApplicationId.toString(),
458
+ aggStartTime: 1700000000000,
459
+ aggEndTime: 1700000090000,
460
+ aggRoutes: ["/a", "/b", "/c", "/d", "/e", "/f", "/g"],
461
+ aggTraceCount: "3",
462
+ aggExceptionGroupCount: 2,
463
+ aggClickCount: 41,
464
+ aggActiveMs: "54000",
465
+ aggFirstErrorOffsetMs: "12000",
466
+ aggExpiresAt: 1700604800000,
467
+ aggTags: { build: "1.2.3" },
468
+ },
469
+ ]) as never,
470
+ );
471
+
472
+ const result: SessionReplayListResult =
473
+ await SessionReplayReadService.listSessions(listRequest());
474
+ const item: SessionReplayListItem = result.sessions[0]!;
475
+
476
+ expect(item.routes).toHaveLength(MAX_LIST_ROUTES);
477
+ expect(item.routes[0]).toBe("/a");
478
+ expect(item.traceCount).toBe(3);
479
+ expect(item.exceptionGroupCount).toBe(2);
480
+ expect(item.clickCount).toBe(41);
481
+ expect(item.activeMs).toBe(54000);
482
+ expect(item.firstErrorOffsetMs).toBe(12000);
483
+ expect(item.expiresAtUnixMs).toBe(1700604800000);
484
+ expect(item.tags).toEqual({ build: "1.2.3" });
485
+ expect(item.startTimeUnixMs).toBe(1700000000000);
486
+ expect(item.endTimeUnixMs).toBe(1700000090000);
487
+ expect(item.identifiedUserTraits).toBeUndefined();
488
+ expect(item.identifiedUserLabel).toBeUndefined();
489
+ });
490
+
491
+ /*
492
+ * The list's "3 errors" badge has nowhere to link without a
493
+ * fingerprint: the Exceptions page can only be opened unfiltered.
494
+ * Projected from the same argMax'd array the group count is measured
495
+ * over, so the two can never disagree about which session errored.
496
+ */
497
+ test("the first exception fingerprint is projected so the errors badge can link", async () => {
498
+ const query: string = await listQuery({});
499
+
500
+ expect(query).toContain(
501
+ "arrayElement(argMax(exceptionFingerprints, version), 1) AS aggTopExceptionFingerprint",
502
+ );
503
+
504
+ headerQuerySpy.mockResolvedValue(
505
+ fakeResultSet([
506
+ {
507
+ sessionId: "s-1",
508
+ aggTopExceptionFingerprint: "fp-abc",
509
+ },
510
+ ]) as never,
511
+ );
512
+
513
+ const withFingerprint: SessionReplayListResult =
514
+ await SessionReplayReadService.listSessions(listRequest());
515
+
516
+ expect(withFingerprint.sessions[0]!.topExceptionFingerprint).toBe(
517
+ "fp-abc",
518
+ );
519
+
520
+ /* A clean session reports "", never undefined. */
521
+ headerQuerySpy.mockResolvedValue(
522
+ fakeResultSet([{ sessionId: "s-2" }]) as never,
523
+ );
524
+
525
+ const clean: SessionReplayListResult =
526
+ await SessionReplayReadService.listSessions(listRequest());
527
+
528
+ expect(clean.sessions[0]!.topExceptionFingerprint).toBe("");
529
+ });
530
+
531
+ test("names and maps the identity columns only when asked", async () => {
532
+ headerQuerySpy.mockResolvedValue(
533
+ fakeResultSet([
534
+ {
535
+ sessionId: "s-1",
536
+ aggIdentifiedUserLabel: "jane@example.com",
537
+ aggIdentifiedUserTraits: { plan: "pro" },
538
+ },
539
+ ]) as never,
540
+ );
541
+
542
+ const result: SessionReplayListResult =
543
+ await SessionReplayReadService.listSessions(
544
+ listRequest({ includeIdentifiedUserLabel: true }),
545
+ );
546
+
547
+ const query: string = statementOf(headerQuerySpy).query;
548
+ expect(query).toContain(
549
+ "argMax(identifiedUserTraits, version) AS aggIdentifiedUserTraits",
550
+ );
551
+ expect(result.sessions[0]!.identifiedUserLabel).toBe("jane@example.com");
552
+ expect(result.sessions[0]!.identifiedUserTraits).toEqual({
553
+ plan: "pro",
554
+ });
555
+ });
556
+ });
557
+
558
+ describe("getSessionHeader", () => {
559
+ const headerRow: JSONObject = {
560
+ sessionId: "s-1",
561
+ headerProjectId: "p",
562
+ applicationId: "a",
563
+ aggStartTime: 1700000000000,
564
+ aggEndTime: 1700000060000,
565
+ aggIsFinalized: 1,
566
+ aggClientReportedStart: 1699999999000,
567
+ aggTags: { env: "prod" },
568
+ aggExpiresAt: 1700604800000,
569
+ aggClickCount: 7,
570
+ aggCustomEventCount: 2,
571
+ aggActiveMs: 30000,
572
+ aggFirstErrorOffsetMs: 5000,
573
+ aggAttributes: {
574
+ "recorder.capabilities": "click-events,web-vitals,made-up",
575
+ },
576
+ };
577
+
578
+ test("never names the identity columns and pins the application only when given one", async () => {
579
+ headerQuerySpy.mockResolvedValue(fakeResultSet([headerRow]) as never);
580
+
581
+ await SessionReplayReadService.getSessionHeader({
582
+ projectId: projectId,
583
+ sessionId: "s-1",
584
+ });
585
+
586
+ const plain: string = statementOf(headerQuerySpy).query;
587
+ expect(plain).not.toContain("identifiedUserLabel");
588
+ expect(plain).not.toContain("identifiedUserTraits");
589
+ expect(plain).not.toContain("rumApplicationId = ");
590
+ expect(plain).toContain("LIMIT 2");
591
+ expect(plain).toContain("retentionDate >= now()");
592
+
593
+ headerQuerySpy.mockClear();
594
+
595
+ await SessionReplayReadService.getSessionHeader({
596
+ projectId: projectId,
597
+ sessionId: "s-1",
598
+ rumApplicationId: rumApplicationId,
599
+ });
600
+
601
+ const pinned: Statement = statementOf(headerQuerySpy);
602
+ expect(whereSection(pinned.query)).toContain("rumApplicationId = ");
603
+ expect(boundValues(pinned)).toContain(rumApplicationId.toString());
604
+ });
605
+
606
+ test("maps the clock, tags, expiry, counters and the known recorder capabilities", async () => {
607
+ headerQuerySpy.mockResolvedValue(fakeResultSet([headerRow]) as never);
608
+
609
+ const header: SessionReplaySessionHeader | null =
610
+ await SessionReplayReadService.getSessionHeader({
611
+ projectId: projectId,
612
+ sessionId: "s-1",
613
+ });
614
+
615
+ expect(header).not.toBeNull();
616
+ expect(header!.startTimeUnixMs).toBe(1700000000000);
617
+ expect(header!.endTimeUnixMs).toBe(1700000060000);
618
+ expect(header!.clientReportedStartUnixMs).toBe(1699999999000);
619
+ expect(header!.tags).toEqual({ env: "prod" });
620
+ expect(header!.expiresAtUnixMs).toBe(1700604800000);
621
+ expect(header!.clickCount).toBe(7);
622
+ expect(header!.customEventCount).toBe(2);
623
+ expect(header!.activeMs).toBe(30000);
624
+ expect(header!.firstErrorOffsetMs).toBe(5000);
625
+ /* A stored value outside the vocabulary never reaches the player. */
626
+ expect(header!.recorderCapabilities).toEqual([
627
+ "click-events",
628
+ "web-vitals",
629
+ ]);
630
+ expect(header!.identifiedUserLabel).toBeUndefined();
631
+ expect(header!.identifiedUserTraits).toBeUndefined();
632
+ });
633
+
634
+ test("an ambiguous session id is refused with an actionable message", async () => {
635
+ headerQuerySpy.mockResolvedValue(
636
+ fakeResultSet([
637
+ headerRow,
638
+ { ...headerRow, applicationId: "b" },
639
+ ]) as never,
640
+ );
641
+
642
+ await expect(
643
+ SessionReplayReadService.getSessionHeader({
644
+ projectId: projectId,
645
+ sessionId: "s-1",
646
+ }),
647
+ ).rejects.toThrow(/rumApplicationId/);
648
+ });
649
+ });
650
+
651
+ describe("getSessionIdentity", () => {
652
+ test("names both identity columns, pinned to the application", async () => {
653
+ headerQuerySpy.mockResolvedValue(
654
+ fakeResultSet([
655
+ {
656
+ aggIdentifiedUserLabel: "jane@example.com",
657
+ aggIdentifiedUserTraits: { plan: "pro", seats: 4 },
658
+ },
659
+ ]) as never,
660
+ );
661
+
662
+ const identity: SessionReplaySessionIdentity =
663
+ await SessionReplayReadService.getSessionIdentity({
664
+ projectId: projectId,
665
+ rumApplicationId: rumApplicationId,
666
+ sessionId: "s-1",
667
+ });
668
+
669
+ const statement: Statement = statementOf(headerQuerySpy);
670
+ expect(statement.query).toContain(
671
+ "argMax(identifiedUserLabel, version) AS aggIdentifiedUserLabel",
672
+ );
673
+ expect(statement.query).toContain(
674
+ "argMax(identifiedUserTraits, version) AS aggIdentifiedUserTraits",
675
+ );
676
+ expect(whereSection(statement.query)).toContain("rumApplicationId = ");
677
+ expect(statement.query).toContain("retentionDate >= now()");
678
+
679
+ expect(identity.identifiedUserLabel).toBe("jane@example.com");
680
+ /* A numeric-looking trait still renders. */
681
+ expect(identity.identifiedUserTraits).toEqual({
682
+ plan: "pro",
683
+ seats: "4",
684
+ });
685
+ });
686
+
687
+ test("answers empty rather than throwing when no row survives retention", async () => {
688
+ const identity: SessionReplaySessionIdentity =
689
+ await SessionReplayReadService.getSessionIdentity({
690
+ projectId: projectId,
691
+ rumApplicationId: rumApplicationId,
692
+ sessionId: "s-1",
693
+ });
694
+
695
+ expect(identity).toEqual({
696
+ identifiedUserLabel: "",
697
+ identifiedUserTraits: {},
698
+ });
699
+ });
700
+ });
701
+
702
+ describe("getExpiredSessionInfo", () => {
703
+ test("reads past retention, returning only dates and the application", async () => {
704
+ headerQuerySpy.mockResolvedValue(
705
+ fakeResultSet([
706
+ {
707
+ applicationId: rumApplicationId.toString(),
708
+ expiresAtUnixMs: 1700604800000,
709
+ startTimeUnixMs: 1700000000000,
710
+ },
711
+ ]) as never,
712
+ );
713
+
714
+ const info: SessionReplayExpiredSessionInfo | null =
715
+ await SessionReplayReadService.getExpiredSessionInfo({
716
+ projectId: projectId,
717
+ sessionId: "s-1",
718
+ });
719
+
720
+ const query: string = statementOf(headerQuerySpy).query;
721
+ expect(query).not.toContain("retentionDate >= now()");
722
+ expect(query).toContain("max(retentionDate)");
723
+ expect(query).not.toMatch(/\bpayload\b/);
724
+ expect(query).not.toContain("identifiedUser");
725
+
726
+ expect(info).not.toBeNull();
727
+ expect(info!.rumApplicationId).toBe(rumApplicationId.toString());
728
+ expect(info!.expiresAt.getTime()).toBe(1700604800000);
729
+ expect(info!.startTime.getTime()).toBe(1700000000000);
730
+ });
731
+
732
+ test("is null when no header ever existed", async () => {
733
+ expect(
734
+ await SessionReplayReadService.getExpiredSessionInfo({
735
+ projectId: projectId,
736
+ sessionId: "never",
737
+ }),
738
+ ).toBeNull();
739
+ });
740
+ });
741
+
742
+ describe("getManifest", () => {
743
+ function header(
744
+ overrides: Partial<SessionReplaySessionHeader>,
745
+ ): SessionReplaySessionHeader {
746
+ return {
747
+ sessionId: "s-1",
748
+ projectId: projectId.toString(),
749
+ rumApplicationId: rumApplicationId.toString(),
750
+ startTime: new Date(1700000000000),
751
+ endTime: new Date(1700000015000),
752
+ durationMs: 0,
753
+ isFinalized: false,
754
+ sealedReason: "",
755
+ chunkCount: 0,
756
+ maxChunkIndex: 0,
757
+ missingChunkCount: 0,
758
+ eventCount: 0,
759
+ payloadBytes: 0,
760
+ hasError: false,
761
+ errorCount: 0,
762
+ rageClickCount: 0,
763
+ deadClickCount: 0,
764
+ errorClickCount: 0,
765
+ refreshRageCount: 0,
766
+ pageCount: 1,
767
+ triggerReason: "always",
768
+ maskingMode: "MaskAllText",
769
+ consentState: "NotRequired",
770
+ recorderKind: "dom",
771
+ recorderVersion: "1.0.0",
772
+ rrwebVersion: "2.1.1",
773
+ schemaVersion: 1,
774
+ wireVersion: 1,
775
+ entryUrl: "https://a/",
776
+ exitUrl: "https://a/",
777
+ routes: ["https://a/"],
778
+ browserName: "Chrome",
779
+ browserVersion: "1",
780
+ osName: "macOS",
781
+ deviceType: "desktop",
782
+ countryCode: "GB",
783
+ viewportWidth: 1,
784
+ viewportHeight: 1,
785
+ fidelityNotices: [],
786
+ fullSnapshotChunkIndexes: [],
787
+ traceIds: [],
788
+ exceptionFingerprints: [],
789
+ clockSkewMs: 0,
790
+ startTimeUnixMs: 1700000000000,
791
+ endTimeUnixMs: 1700000015000,
792
+ clientReportedStartUnixMs: 1700000000000,
793
+ tags: {},
794
+ expiresAtUnixMs: 1700604800000,
795
+ clickCount: 0,
796
+ customEventCount: 0,
797
+ activeMs: 0,
798
+ firstErrorOffsetMs: 0,
799
+ recorderCapabilities: [],
800
+ ...overrides,
801
+ };
802
+ }
803
+
804
+ const chunkRows: Array<JSONObject> = [
805
+ {
806
+ tabId: "tab-1",
807
+ chunkIndex: 0,
808
+ chunkStartOffsetMs: 0,
809
+ chunkEndOffsetMs: 15000,
810
+ eventCount: 100,
811
+ hasFullSnapshot: 1,
812
+ chunkPayloadBytes: 1024,
813
+ clickCount: 3,
814
+ url: "https://a/",
815
+ },
816
+ {
817
+ tabId: "tab-1",
818
+ chunkIndex: 1,
819
+ chunkStartOffsetMs: 15000,
820
+ chunkEndOffsetMs: 30000,
821
+ eventCount: 50,
822
+ hasFullSnapshot: 0,
823
+ chunkPayloadBytes: 512,
824
+ clickCount: 1,
825
+ url: "https://a/checkout",
826
+ },
827
+ {
828
+ tabId: "tab-2",
829
+ chunkIndex: 0,
830
+ chunkStartOffsetMs: 134000,
831
+ chunkEndOffsetMs: 150000,
832
+ eventCount: 20,
833
+ hasFullSnapshot: 1,
834
+ chunkPayloadBytes: 256,
835
+ clickCount: 0,
836
+ url: "https://a/help",
837
+ },
838
+ ];
839
+
840
+ test("projects clickCount and url per chunk, never the payload, and derives each tab's first offset", async () => {
841
+ chunkQuerySpy.mockResolvedValue(fakeResultSet(chunkRows) as never);
842
+
843
+ const manifest: SessionReplayManifest =
844
+ await SessionReplayReadService.getManifest({
845
+ header: header({ isFinalized: true, durationMs: 150000 }),
846
+ projectId: projectId,
847
+ rumApplicationId: rumApplicationId,
848
+ sessionId: "s-1",
849
+ });
850
+
851
+ const query: string = statementOf(chunkQuerySpy).query;
852
+ expect(query).toContain("clickCount");
853
+ expect(query).toContain("url");
854
+ expect(query).not.toMatch(/\bpayload\b(?!Bytes)/);
855
+ expect(query).not.toContain("length(payload)");
856
+ expect(query).toContain("retentionDate >= now()");
857
+
858
+ expect(manifest.tabs).toHaveLength(2);
859
+ expect(manifest.tabs[0]!.firstChunkStartOffsetMs).toBe(0);
860
+ expect(manifest.tabs[1]!.firstChunkStartOffsetMs).toBe(134000);
861
+ expect(manifest.tabs[0]!.chunks[1]!.clickCount).toBe(1);
862
+ expect(manifest.tabs[0]!.chunks[1]!.url).toBe("https://a/checkout");
863
+ });
864
+
865
+ test("a finalized header is returned untouched", async () => {
866
+ chunkQuerySpy.mockResolvedValue(fakeResultSet(chunkRows) as never);
867
+
868
+ const finalized: SessionReplaySessionHeader = header({
869
+ isFinalized: true,
870
+ durationMs: 90000,
871
+ chunkCount: 9,
872
+ eventCount: 9,
873
+ });
874
+
875
+ const manifest: SessionReplayManifest =
876
+ await SessionReplayReadService.getManifest({
877
+ header: finalized,
878
+ projectId: projectId,
879
+ rumApplicationId: rumApplicationId,
880
+ sessionId: "s-1",
881
+ });
882
+
883
+ expect(manifest.header).toBe(finalized);
884
+ });
885
+
886
+ test("a provisional header reports what its chunk rows prove instead of zeros", async () => {
887
+ chunkQuerySpy.mockResolvedValue(fakeResultSet(chunkRows) as never);
888
+
889
+ const manifest: SessionReplayManifest =
890
+ await SessionReplayReadService.getManifest({
891
+ header: header({ isFinalized: false }),
892
+ projectId: projectId,
893
+ rumApplicationId: rumApplicationId,
894
+ sessionId: "s-1",
895
+ });
896
+
897
+ expect(manifest.header.isFinalized).toBe(false);
898
+ expect(manifest.header.durationMs).toBe(150000);
899
+ expect(manifest.header.chunkCount).toBe(3);
900
+ expect(manifest.header.eventCount).toBe(170);
901
+ expect(manifest.header.maxChunkIndex).toBe(1);
902
+ expect(manifest.header.endTimeUnixMs).toBe(1700000150000);
903
+ expect(manifest.header.endTime.getTime()).toBe(1700000150000);
904
+ });
905
+ });
906
+
907
+ describe("getChunks", () => {
908
+ const chunkRequest: {
909
+ projectId: ObjectID;
910
+ rumApplicationId: ObjectID;
911
+ sessionId: string;
912
+ tabId: string;
913
+ chunkIndexes: Array<number>;
914
+ } = {
915
+ projectId: projectId,
916
+ rumApplicationId: rumApplicationId,
917
+ sessionId: "s-1",
918
+ tabId: "tab-1",
919
+ chunkIndexes: [0, 1, 2],
920
+ };
921
+
922
+ test("measures the stored size in the one statement that ships the bytes", async () => {
923
+ chunkQuerySpy.mockResolvedValue(
924
+ fakeResultSet([
925
+ { chunkIndex: 0, servedPayload: "[1]", isServed: 1 },
926
+ { chunkIndex: 1, servedPayload: "[22]", isServed: 1 },
927
+ ]) as never,
928
+ );
929
+
930
+ const result: SessionReplayChunkReadResult =
931
+ await SessionReplayReadService.getChunks(chunkRequest);
932
+
933
+ expect(chunkQuerySpy).toHaveBeenCalledTimes(1);
934
+
935
+ const query: string = statementOf(chunkQuerySpy).query;
936
+ expect(query).toContain("length(payload)");
937
+ expect(query).not.toContain("toFloat64(payloadBytes)");
938
+ expect(query).toContain(
939
+ "ORDER BY chunkIndex ASC, version DESC LIMIT 1 BY chunkIndex",
940
+ );
941
+ expect(query).toContain(
942
+ "row_number() OVER (ORDER BY chunkIndex ASC) = 1",
943
+ );
944
+ expect(query).toContain("retentionDate >= now()");
945
+ expect(boundValues(statementOf(chunkQuerySpy))).toContain(
946
+ MAX_SESSION_REPLAY_READ_BYTES,
947
+ );
948
+
949
+ expect(
950
+ result.chunks.map((c: { chunkIndex: number }): number => {
951
+ return c.chunkIndex;
952
+ }),
953
+ ).toEqual([0, 1]);
954
+ expect(result.omittedChunkIndexes).toEqual([]);
955
+ });
956
+
957
+ test("serves the prefix that fits and names what was left out, never refusing outright", async () => {
958
+ chunkQuerySpy.mockResolvedValue(
959
+ fakeResultSet([
960
+ { chunkIndex: 0, servedPayload: "[1]", isServed: 1 },
961
+ { chunkIndex: 1, servedPayload: "", isServed: 0 },
962
+ { chunkIndex: 2, servedPayload: "[3]", isServed: 1 },
963
+ ]) as never,
964
+ );
965
+
966
+ const result: SessionReplayChunkReadResult =
967
+ await SessionReplayReadService.getChunks(chunkRequest);
968
+
969
+ expect(
970
+ result.chunks.map((c: { chunkIndex: number }): number => {
971
+ return c.chunkIndex;
972
+ }),
973
+ ).toEqual([0]);
974
+ /* Chunk 2 fit, but a hole before it would be unplayable. */
975
+ expect(result.omittedChunkIndexes).toEqual([1, 2]);
976
+ });
977
+
978
+ test("a single oversized chunk is still served: the ingest cap already bounded it", async () => {
979
+ const fat: string = "a".repeat(MAX_SESSION_REPLAY_READ_BYTES + 10);
980
+
981
+ chunkQuerySpy.mockResolvedValue(
982
+ fakeResultSet([
983
+ { chunkIndex: 0, servedPayload: fat, isServed: 1 },
984
+ ]) as never,
985
+ );
986
+
987
+ const result: SessionReplayChunkReadResult =
988
+ await SessionReplayReadService.getChunks({
989
+ ...chunkRequest,
990
+ chunkIndexes: [0],
991
+ });
992
+
993
+ expect(result.chunks).toHaveLength(1);
994
+ expect(result.omittedChunkIndexes).toEqual([]);
995
+ });
996
+
997
+ test("re-applies the cap to the bytes actually returned", async () => {
998
+ const half: string = "a".repeat(5 * 1024 * 1024);
999
+
1000
+ chunkQuerySpy.mockResolvedValue(
1001
+ fakeResultSet([
1002
+ { chunkIndex: 0, servedPayload: half, isServed: 1 },
1003
+ { chunkIndex: 1, servedPayload: half, isServed: 1 },
1004
+ ]) as never,
1005
+ );
1006
+
1007
+ const result: SessionReplayChunkReadResult =
1008
+ await SessionReplayReadService.getChunks({
1009
+ ...chunkRequest,
1010
+ chunkIndexes: [0, 1],
1011
+ });
1012
+
1013
+ expect(
1014
+ result.chunks.map((c: { chunkIndex: number }): number => {
1015
+ return c.chunkIndex;
1016
+ }),
1017
+ ).toEqual([0]);
1018
+ expect(result.omittedChunkIndexes).toEqual([1]);
1019
+ });
1020
+
1021
+ test("refuses more than the per-read chunk cap before querying", async () => {
1022
+ await expect(
1023
+ SessionReplayReadService.getChunks({
1024
+ ...chunkRequest,
1025
+ chunkIndexes: [0, 1, 2, 3, 4, 5, 6, 7, 8],
1026
+ }),
1027
+ ).rejects.toBeInstanceOf(BadDataException);
1028
+ expect(chunkQuerySpy).not.toHaveBeenCalled();
1029
+ });
1030
+ });
1031
+
1032
+ describe("getSessionsForException", () => {
1033
+ test("always bounds the window and consults the exception instances for live sessions", async () => {
1034
+ exceptionQuerySpy.mockResolvedValue(
1035
+ fakeResultSet([
1036
+ { sessionId: "live-1" },
1037
+ { sessionId: "live-2" },
1038
+ ]) as never,
1039
+ );
1040
+
1041
+ await SessionReplayReadService.getSessionsForException({
1042
+ projectId: projectId,
1043
+ exceptionFingerprint: "fp-1",
1044
+ accessibleRumApplicationIds: null,
1045
+ limit: 5,
1046
+ });
1047
+
1048
+ const instances: Statement = statementOf(exceptionQuerySpy);
1049
+ expect(instances.query).toContain("SELECT DISTINCT sessionId");
1050
+ expect(instances.query).toContain("fingerprint = ");
1051
+ expect(instances.query).toContain("sessionId != ''");
1052
+ expect(instances.query).toContain("time >= ");
1053
+ expect(instances.query).toContain("time <= ");
1054
+ expect(boundValues(instances)).toContain("fp-1");
1055
+
1056
+ const headers: Statement = statementOf(headerQuerySpy);
1057
+ const where: string = whereSection(headers.query);
1058
+ expect(where).toContain("startTime >= ");
1059
+ expect(where).toContain("startTime <= ");
1060
+ expect(where).toContain("retentionDate >= now()");
1061
+ expect(where).toMatch(
1062
+ /AND \(hasAny\(exceptionFingerprints, \[\{p\d+:String\}\]\) OR sessionId IN \(\{p\d+:Array\(String\)\}\)\)/,
1063
+ );
1064
+ expect(
1065
+ havingSection(headers.query.replace("HAVING (", "HAVING 1 = 1 AND (")),
1066
+ ).toMatch(
1067
+ /hasAny\(aggExceptionFingerprints, \[\{p\d+:String\}\]\) OR sessionId IN \(\{p\d+:Array\(String\)\}\)/,
1068
+ );
1069
+ expect(boundValues(headers)).toContainEqual(["live-1", "live-2"]);
1070
+ });
1071
+
1072
+ /*
1073
+ * A pinned sessionId NARROWS the instance lookup; it does not replace
1074
+ * it. Returning the pin unchecked reduced the statement to
1075
+ * `sessionId = X AND (hasAny(fingerprints, [f]) OR sessionId IN (X))`,
1076
+ * whose second arm is trivially true - so the fingerprint constrained
1077
+ * nothing and the "Watch what the user saw" card would present any
1078
+ * accessible session as having observed the exception, on nothing but a
1079
+ * stale occurrence row.
1080
+ */
1081
+ test("a pinned session id still has to be confirmed by the instance table", async () => {
1082
+ exceptionQuerySpy.mockResolvedValue(
1083
+ fakeResultSet([{ sessionId: "s-9" }]) as never,
1084
+ );
1085
+
1086
+ await SessionReplayReadService.getSessionsForException({
1087
+ projectId: projectId,
1088
+ exceptionFingerprint: "fp-1",
1089
+ accessibleRumApplicationIds: null,
1090
+ sessionId: "s-9",
1091
+ limit: 5,
1092
+ });
1093
+
1094
+ const instances: Statement = statementOf(exceptionQuerySpy);
1095
+ expect(instances.query).toContain("fingerprint = ");
1096
+ expect(instances.query).toContain("AND sessionId = ");
1097
+ expect(boundValues(instances)).toContain("fp-1");
1098
+ expect(boundValues(instances)).toContain("s-9");
1099
+
1100
+ const headers: Statement = statementOf(headerQuerySpy);
1101
+ expect(whereSection(headers.query)).toContain("AND sessionId = ");
1102
+ expect(boundValues(headers)).toContain("s-9");
1103
+ });
1104
+
1105
+ test("a pinned session the instance table has never seen falls back to the fingerprint alone", async () => {
1106
+ /* The session exists, but it never threw this exception. */
1107
+ exceptionQuerySpy.mockResolvedValue(fakeResultSet([]) as never);
1108
+
1109
+ await SessionReplayReadService.getSessionsForException({
1110
+ projectId: projectId,
1111
+ exceptionFingerprint: "fp-1",
1112
+ accessibleRumApplicationIds: null,
1113
+ sessionId: "s-9",
1114
+ limit: 5,
1115
+ });
1116
+
1117
+ const headers: Statement = statementOf(headerQuerySpy);
1118
+
1119
+ /*
1120
+ * No `OR sessionId IN (...)` escape hatch: the header's own
1121
+ * fingerprint list is the only thing that can admit the row.
1122
+ */
1123
+ expect(headers.query).not.toContain("OR sessionId IN (");
1124
+ expect(headers.query).toContain("hasAny(exceptionFingerprints");
1125
+ });
1126
+
1127
+ test("a failed instance lookup degrades to the finalized headers", async () => {
1128
+ exceptionQuerySpy.mockRejectedValue(
1129
+ new Error("clickhouse down") as never,
1130
+ );
1131
+ headerQuerySpy.mockResolvedValue(
1132
+ fakeResultSet([
1133
+ {
1134
+ sessionId: "s-1",
1135
+ applicationId: rumApplicationId.toString(),
1136
+ aggStartTime: 1,
1137
+ aggEndTime: 2,
1138
+ aggIsFinalized: 1,
1139
+ },
1140
+ ]) as never,
1141
+ );
1142
+
1143
+ const sessions: Array<SessionReplayExceptionSession> =
1144
+ await SessionReplayReadService.getSessionsForException({
1145
+ projectId: projectId,
1146
+ exceptionFingerprint: "fp-1",
1147
+ accessibleRumApplicationIds: null,
1148
+ limit: 5,
1149
+ });
1150
+
1151
+ expect(sessions).toHaveLength(1);
1152
+ expect(statementOf(headerQuerySpy).query).not.toContain(
1153
+ "OR sessionId IN (",
1154
+ );
1155
+ });
1156
+
1157
+ test("a caller who reaches no application gets no rows and no query", async () => {
1158
+ const sessions: Array<SessionReplayExceptionSession> =
1159
+ await SessionReplayReadService.getSessionsForException({
1160
+ projectId: projectId,
1161
+ exceptionFingerprint: "fp-1",
1162
+ accessibleRumApplicationIds: [],
1163
+ limit: 5,
1164
+ });
1165
+
1166
+ expect(sessions).toEqual([]);
1167
+ expect(headerQuerySpy).not.toHaveBeenCalled();
1168
+ expect(exceptionQuerySpy).not.toHaveBeenCalled();
1169
+ });
1170
+ });
1171
+
1172
+ describe("getApplicationActivitySummary", () => {
1173
+ const summaryRows: Array<Array<JSONObject>> = [
1174
+ [{ sessionCount: 143, unplayableCount: 3 }],
1175
+ [{ lastStartUnixMs: 1700000000000 }],
1176
+ ];
1177
+
1178
+ function mockSummaryRows(): void {
1179
+ headerQuerySpy
1180
+ .mockResolvedValueOnce(fakeResultSet(summaryRows[0]!) as never)
1181
+ .mockResolvedValueOnce(fakeResultSet(summaryRows[1]!) as never);
1182
+ }
1183
+
1184
+ test("counts without a GROUP BY and reads the latest start in sort-key order", async () => {
1185
+ mockSummaryRows();
1186
+
1187
+ const summary: SessionReplayApplicationActivitySummary =
1188
+ await SessionReplayReadService.getApplicationActivitySummary({
1189
+ projectId: projectId,
1190
+ rumApplicationId: rumApplicationId,
1191
+ nowUnixMs: 1700000000000,
1192
+ });
1193
+
1194
+ expect(headerQuerySpy).toHaveBeenCalledTimes(2);
1195
+
1196
+ const counts: Statement = statementOf(headerQuerySpy, 0);
1197
+ expect(counts.query).toContain("uniqExact(sessionId)");
1198
+ expect(counts.query).toContain(
1199
+ "uniqExactIf(sessionId, isFinalized AND (chunkCount = 0 OR sealedReason = ",
1200
+ );
1201
+ expect(counts.query).not.toContain("GROUP BY");
1202
+ expect(counts.query).toContain("startTime >= ");
1203
+ expect(counts.query).toContain("retentionDate >= now()");
1204
+ expect(counts.query).not.toMatch(/\bpayload\b/);
1205
+ expect(boundValues(counts)).toContain("recording-lost");
1206
+
1207
+ const latest: Statement = statementOf(headerQuerySpy, 1);
1208
+ expect(latest.query).toContain("ORDER BY startTime DESC LIMIT 1");
1209
+ expect(latest.query).not.toContain("GROUP BY");
1210
+ expect(latest.query).toContain("retentionDate >= now()");
1211
+
1212
+ expect(summary.sessionsLast24h).toBe(143);
1213
+ expect(summary.playableSessionsLast24h).toBe(140);
1214
+ expect(summary.lastSessionStartedAt?.getTime()).toBe(1700000000000);
1215
+ });
1216
+
1217
+ test("is served from memory within the cache window and re-read after it", async () => {
1218
+ mockSummaryRows();
1219
+
1220
+ await SessionReplayReadService.getApplicationActivitySummary({
1221
+ projectId: projectId,
1222
+ rumApplicationId: rumApplicationId,
1223
+ nowUnixMs: 1700000000000,
1224
+ });
1225
+ await SessionReplayReadService.getApplicationActivitySummary({
1226
+ projectId: projectId,
1227
+ rumApplicationId: rumApplicationId,
1228
+ nowUnixMs:
1229
+ 1700000000000 + SESSION_REPLAY_ACTIVITY_SUMMARY_CACHE_TTL_MS - 1,
1230
+ });
1231
+
1232
+ expect(headerQuerySpy).toHaveBeenCalledTimes(2);
1233
+
1234
+ mockSummaryRows();
1235
+
1236
+ await SessionReplayReadService.getApplicationActivitySummary({
1237
+ projectId: projectId,
1238
+ rumApplicationId: rumApplicationId,
1239
+ nowUnixMs:
1240
+ 1700000000000 + SESSION_REPLAY_ACTIVITY_SUMMARY_CACHE_TTL_MS + 1,
1241
+ });
1242
+
1243
+ expect(headerQuerySpy).toHaveBeenCalledTimes(4);
1244
+ });
1245
+
1246
+ test("answers null counts, never zero, when ClickHouse cannot be read", async () => {
1247
+ headerQuerySpy.mockRejectedValue(new Error("timeout") as never);
1248
+
1249
+ const summary: SessionReplayApplicationActivitySummary =
1250
+ await SessionReplayReadService.getApplicationActivitySummary({
1251
+ projectId: projectId,
1252
+ rumApplicationId: rumApplicationId,
1253
+ nowUnixMs: 1700000000000,
1254
+ });
1255
+
1256
+ expect(summary).toEqual({
1257
+ sessionsLast24h: null,
1258
+ playableSessionsLast24h: null,
1259
+ lastSessionStartedAt: null,
1260
+ recorderCapabilities: null,
1261
+ });
1262
+ });
1263
+
1264
+ /*
1265
+ * The health card and the installation test both promise "the
1266
+ * capabilities of the newest recorder that reported" - the one place an
1267
+ * operator can spot a stale cached artifact ("click labels: no")
1268
+ * without opening a recording, which writes an audit row. The route
1269
+ * never sent them, so the row said "not reported yet" for every
1270
+ * application forever. They ride on the last-session query rather than
1271
+ * costing a query of their own.
1272
+ */
1273
+ test("the newest session's recorder capabilities ride on the last-start read", async () => {
1274
+ headerQuerySpy
1275
+ .mockResolvedValueOnce(
1276
+ fakeResultSet([{ sessionCount: 4, unplayableCount: 0 }]) as never,
1277
+ )
1278
+ .mockResolvedValueOnce(
1279
+ fakeResultSet([
1280
+ {
1281
+ lastStartUnixMs: 1700000000000,
1282
+ aggAttributes: {
1283
+ "recorder.capabilities":
1284
+ "click-events,web-vitals,not-a-capability",
1285
+ },
1286
+ },
1287
+ ]) as never,
1288
+ );
1289
+
1290
+ const summary: SessionReplayApplicationActivitySummary =
1291
+ await SessionReplayReadService.getApplicationActivitySummary({
1292
+ projectId: projectId,
1293
+ rumApplicationId: rumApplicationId,
1294
+ nowUnixMs: 1700000000000,
1295
+ });
1296
+
1297
+ const latest: Statement = statementOf(headerQuerySpy, 1);
1298
+ expect(latest.query).toContain("attributes AS aggAttributes");
1299
+
1300
+ /* Filtered to the vocabulary this build knows. */
1301
+ expect(summary.recorderCapabilities).toEqual([
1302
+ "click-events",
1303
+ "web-vitals",
1304
+ ]);
1305
+ });
1306
+
1307
+ test("a session that declared no capabilities answers null, never an empty list", async () => {
1308
+ headerQuerySpy
1309
+ .mockResolvedValueOnce(
1310
+ fakeResultSet([{ sessionCount: 1, unplayableCount: 0 }]) as never,
1311
+ )
1312
+ .mockResolvedValueOnce(
1313
+ fakeResultSet([{ lastStartUnixMs: 1700000000000 }]) as never,
1314
+ );
1315
+
1316
+ const summary: SessionReplayApplicationActivitySummary =
1317
+ await SessionReplayReadService.getApplicationActivitySummary({
1318
+ projectId: projectId,
1319
+ rumApplicationId: rumApplicationId,
1320
+ nowUnixMs: 1700000000000,
1321
+ });
1322
+
1323
+ /*
1324
+ * "An old recorder declared nothing" and "we could not tell" are both
1325
+ * rendered as "not reported yet"; claiming the recorder can do
1326
+ * NOTHING would be a stronger statement than the row supports.
1327
+ */
1328
+ expect(summary.recorderCapabilities).toBeNull();
1329
+ });
1330
+
1331
+ test("an application with no session in retention has no last start", async () => {
1332
+ headerQuerySpy
1333
+ .mockResolvedValueOnce(
1334
+ fakeResultSet([{ sessionCount: 0, unplayableCount: 0 }]) as never,
1335
+ )
1336
+ .mockResolvedValueOnce(fakeResultSet([]) as never);
1337
+
1338
+ const summary: SessionReplayApplicationActivitySummary =
1339
+ await SessionReplayReadService.getApplicationActivitySummary({
1340
+ projectId: projectId,
1341
+ rumApplicationId: rumApplicationId,
1342
+ nowUnixMs: 1700000000000,
1343
+ });
1344
+
1345
+ expect(summary.sessionsLast24h).toBe(0);
1346
+ expect(summary.lastSessionStartedAt).toBeNull();
1347
+ });
1348
+ });
1349
+
1350
+ describe("published recorder version", () => {
1351
+ test("is unknown until a provider is registered, and survives a throwing provider", () => {
1352
+ expect(SessionReplayReadService.getPublishedRecorderVersion()).toBeNull();
1353
+
1354
+ SessionReplayReadService.setPublishedRecorderVersionProvider(
1355
+ (): string | null => {
1356
+ return "2.3.4";
1357
+ },
1358
+ );
1359
+ expect(SessionReplayReadService.getPublishedRecorderVersion()).toBe(
1360
+ "2.3.4",
1361
+ );
1362
+
1363
+ SessionReplayReadService.setPublishedRecorderVersionProvider(
1364
+ (): string | null => {
1365
+ throw new Error("manifest unreadable");
1366
+ },
1367
+ );
1368
+ expect(SessionReplayReadService.getPublishedRecorderVersion()).toBeNull();
1369
+
1370
+ SessionReplayReadService.setPublishedRecorderVersionProvider(
1371
+ (): string | null => {
1372
+ return "";
1373
+ },
1374
+ );
1375
+ expect(SessionReplayReadService.getPublishedRecorderVersion()).toBeNull();
1376
+ });
1377
+ });
1378
+ });