@oneuptime/common 12.0.33 → 13.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (877) hide show
  1. package/Models/AnalyticsModels/RumSession.ts +117 -1
  2. package/Models/AnalyticsModels/RumSessionChunk.ts +13 -0
  3. package/Models/DatabaseModels/AlertCustomField.ts +77 -3
  4. package/Models/DatabaseModels/ApiKeyPermission.ts +3 -27
  5. package/Models/DatabaseModels/GlobalConfig.ts +12 -12
  6. package/Models/DatabaseModels/IncidentCustomField.ts +77 -3
  7. package/Models/DatabaseModels/InventoryItem.ts +44 -1
  8. package/Models/DatabaseModels/InventoryItemCustomField.ts +77 -3
  9. package/Models/DatabaseModels/MonitorCustomField.ts +77 -3
  10. package/Models/DatabaseModels/NetworkDevice.ts +126 -0
  11. package/Models/DatabaseModels/OnCallDutyPolicyCustomField.ts +77 -3
  12. package/Models/DatabaseModels/RumApplication.ts +6 -6
  13. package/Models/DatabaseModels/ScheduledMaintenanceCustomField.ts +77 -3
  14. package/Models/DatabaseModels/StatusPageCustomField.ts +77 -3
  15. package/Models/DatabaseModels/TeamCustomField.ts +77 -3
  16. package/Models/DatabaseModels/TeamMember.ts +0 -9
  17. package/Models/DatabaseModels/TeamMemberCustomField.ts +77 -3
  18. package/Models/DatabaseModels/TeamPermission.ts +3 -24
  19. package/Models/DatabaseModels/TelemetryIngestionKey.ts +260 -0
  20. package/Models/DatabaseModels/UserTelegram.ts +0 -4
  21. package/Scripts/benchmark-fanin-capacity.js +181 -0
  22. package/Server/API/TelemetryAPI.ts +1284 -164
  23. package/Server/API/UserNotificationSettingAPI.ts +55 -0
  24. package/Server/API/UserTelegramAPI.ts +48 -4
  25. package/Server/EnvironmentConfig.ts +82 -18
  26. package/Server/Infrastructure/Postgres/SchemaMigrations/1791300000000-AddTelemetryIngestionKeyType.ts +91 -0
  27. package/Server/Infrastructure/Postgres/SchemaMigrations/1791400000000-SessionReplayRecordEverySessionByDefault.ts +67 -0
  28. package/Server/Infrastructure/Postgres/SchemaMigrations/1791500000000-WidenCustomFieldDropdownOptions.ts +139 -0
  29. package/Server/Infrastructure/Postgres/SchemaMigrations/1791600000000-AddCustomFieldValueMapping.ts +121 -0
  30. package/Server/Infrastructure/Postgres/SchemaMigrations/1791700000000-AddMacAddressToNetworkDevice.ts +25 -0
  31. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +10 -0
  32. package/Server/Infrastructure/QueueWorker.ts +20 -14
  33. package/Server/Infrastructure/Redis.ts +8 -8
  34. package/Server/Infrastructure/Semaphore.ts +2 -0
  35. package/Server/Infrastructure/Status.ts +10 -4
  36. package/Server/Middleware/TelemetryIngest.ts +453 -7
  37. package/Server/Services/AccessTokenService.ts +1 -0
  38. package/Server/Services/AlertCustomFieldService.ts +98 -0
  39. package/Server/Services/AlertService.ts +37 -0
  40. package/Server/Services/ApiKeyPermissionService.ts +461 -29
  41. package/Server/Services/CustomFieldMappingService.ts +879 -0
  42. package/Server/Services/DatabaseService.ts +65 -13
  43. package/Server/Services/GlobalConfigService.ts +194 -0
  44. package/Server/Services/IncidentCustomFieldService.ts +98 -0
  45. package/Server/Services/IncidentService.ts +35 -0
  46. package/Server/Services/InventoryItemService.ts +31 -1
  47. package/Server/Services/MetricService.ts +194 -0
  48. package/Server/Services/MonitorService.ts +32 -0
  49. package/Server/Services/MonitorTemplateService.ts +140 -19
  50. package/Server/Services/NetworkDeviceAutoImportRuleEngineService.ts +427 -66
  51. package/Server/Services/NetworkDeviceDiscoveryScanService.ts +87 -2
  52. package/Server/Services/NetworkDeviceService.ts +69 -0
  53. package/Server/Services/ProjectService.ts +22 -0
  54. package/Server/Services/RoutineEmailSettingsService.ts +73 -0
  55. package/Server/Services/RumSessionReplayViewService.ts +104 -24
  56. package/Server/Services/ScheduledMaintenanceCustomFieldService.ts +98 -0
  57. package/Server/Services/ScheduledMaintenanceService.ts +32 -0
  58. package/Server/Services/TeamMemberService.ts +78 -0
  59. package/Server/Services/TeamPermissionService.ts +235 -3
  60. package/Server/Services/TelemetryIngestionKeyService.ts +722 -19
  61. package/Server/Services/UserNotificationRuleService.ts +76 -25
  62. package/Server/Services/UserNotificationSettingService.ts +34 -6
  63. package/Server/Services/UserTelegramService.ts +326 -5
  64. package/Server/Types/Database/Permissions/ReadPermission.ts +45 -5
  65. package/Server/Types/Database/QueryHelper.ts +2 -0
  66. package/Server/Types/Database/QueryUtil.ts +56 -0
  67. package/Server/Types/Markdown.ts +93 -1
  68. package/Server/Utils/APIKey/AccessPermission.ts +5 -2
  69. package/Server/Utils/CustomField/CustomFieldDefinitionMappingHooks.ts +54 -0
  70. package/Server/Utils/CustomField/CustomFieldMappingRegistry.ts +415 -0
  71. package/Server/Utils/CustomField/CustomFieldMappingValidator.ts +335 -0
  72. package/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.ts +82 -17
  73. package/Server/Utils/DataSource/EgressGuard.ts +177 -8
  74. package/Server/Utils/EmailRollup/EmailRollupConstants.ts +38 -5
  75. package/Server/Utils/EmailRollup/EmailRollupFlushRunner.ts +67 -5
  76. package/Server/Utils/EmailRollup/EmailRollupRenderer.ts +2 -2
  77. package/Server/Utils/FrontendEnvironment.ts +40 -0
  78. package/Server/Utils/LogRedaction.ts +20 -0
  79. package/Server/Utils/Logger.ts +11 -0
  80. package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +394 -86
  81. package/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.ts +30 -4
  82. package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +31 -5
  83. package/Server/Utils/Monitor/MonitorAlert.ts +29 -6
  84. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +312 -52
  85. package/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.ts +82 -9
  86. package/Server/Utils/Monitor/MonitorCriteriaMessageBuilder.ts +16 -7
  87. package/Server/Utils/Monitor/MonitorCriteriaMessageFormatter.ts +29 -17
  88. package/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.ts +107 -48
  89. package/Server/Utils/Monitor/MonitorIncident.ts +22 -6
  90. package/Server/Utils/Monitor/MonitorTemplateUtil.ts +29 -0
  91. package/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.ts +158 -0
  92. package/Server/Utils/Monitor/NetworkInventoryUtil.ts +27 -0
  93. package/Server/Utils/Monitor/SeriesContextEnricher.ts +304 -0
  94. package/Server/Utils/SSRFProtection.ts +217 -10
  95. package/Server/Utils/SessionReplay/SessionReplayGateCache.ts +239 -138
  96. package/Server/Utils/SessionReplay/SessionReplayHealthCounters.ts +305 -0
  97. package/Server/Utils/SessionReplay/SessionReplayReadService.ts +1430 -289
  98. package/Server/Utils/StartServer.ts +2 -17
  99. package/Server/Utils/TelegramVerificationToken.ts +185 -0
  100. package/Server/Utils/Telemetry/EntityRegistry.ts +11 -1
  101. package/Server/Utils/Telemetry/PinServiceName.ts +197 -0
  102. package/Server/Utils/Telemetry/TelemetryEntity.ts +83 -9
  103. package/Server/Utils/Telemetry/TelemetryFanInWriter.ts +38 -16
  104. package/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.ts +92 -0
  105. package/Server/Utils/Telemetry/TelemetryIngestionKeyRateLimiter.ts +217 -0
  106. package/Server/Utils/VM/VMAPI.ts +1 -0
  107. package/Server/Utils/VM/VMRunner.ts +975 -95
  108. package/Tests/App/AdminDashboard/AdminHeaderSmallScreens.test.tsx +202 -0
  109. package/Tests/App/Dashboard/AddNeighborToMonitoringModal.test.tsx +32 -0
  110. package/Tests/App/Dashboard/AdoptedDeviceDrawer.test.tsx +374 -0
  111. package/Tests/App/Dashboard/DashboardHeaderSmallScreens.test.tsx +404 -0
  112. package/Tests/App/Dashboard/DashboardLabelVariables.test.tsx +580 -0
  113. package/Tests/App/Dashboard/DashboardMonitorLabelVariable.test.tsx +438 -0
  114. package/Tests/App/Dashboard/DashboardVariableAllContract.test.tsx +5 -2
  115. package/Tests/App/Dashboard/DeviceAttachmentCard.test.tsx +273 -0
  116. package/Tests/App/Dashboard/DiscoveryReviewInventoryRefresh.test.tsx +505 -0
  117. package/Tests/App/Dashboard/DiscoveryScanWizardValidation.test.tsx +468 -0
  118. package/Tests/App/Dashboard/EntityDetailPanel.test.tsx +163 -0
  119. package/Tests/App/Dashboard/InfrastructureExplorer.test.tsx +401 -0
  120. package/Tests/App/Dashboard/InfrastructureGraph.test.tsx +232 -0
  121. package/Tests/App/Dashboard/InventoryItems.test.tsx +201 -0
  122. package/Tests/App/Dashboard/InventoryTypeAndStatusFacets.test.tsx +798 -0
  123. package/Tests/App/Dashboard/NetworkTopologyLiveView.test.tsx +320 -0
  124. package/Tests/App/Dashboard/NetworkTopologyToolbar.test.tsx +275 -0
  125. package/Tests/App/Dashboard/OnCallCalendarFeedEmptyState.test.tsx +975 -0
  126. package/Tests/App/Dashboard/OnCallCalendarFeedPlanGate.test.tsx +63 -0
  127. package/Tests/App/Dashboard/ServiceMapGraph.test.tsx +626 -0
  128. package/Tests/App/Dashboard/TopologyDataLoading.test.tsx +348 -0
  129. package/Tests/App/Dashboard/TopologyPageNavigation.test.tsx +331 -0
  130. package/Tests/App/Dashboard/UserSettingsEmailPreferences.test.tsx +603 -0
  131. package/Tests/App/Dashboard/UserSettingsNotificationSettings.test.tsx +324 -0
  132. package/Tests/App/PublicDashboard/DashboardVariableSelector.test.tsx +13 -4
  133. package/Tests/App/StatusPage/StatusPageLastUpdated.test.tsx +263 -0
  134. package/Tests/App/StatusPage/StatusPageOidcOrigin.test.tsx +223 -0
  135. package/Tests/App/StatusPage/StatusPageOverviewLiveAndSearch.test.tsx +753 -0
  136. package/Tests/App/StatusPage/StatusPageResourceSearchBox.test.tsx +224 -0
  137. package/Tests/Models/AnalyticsModels/RumSessionReplayColumns.test.ts +262 -0
  138. package/Tests/Models/CustomFieldMappingColumns.test.ts +166 -0
  139. package/Tests/Models/DatabaseModels/SessionReplayModels.test.ts +20 -6
  140. package/Tests/Models/InventoryItemStatus.test.ts +147 -0
  141. package/Tests/Models/NetworkDeviceMacAddressColumn.test.ts +294 -0
  142. package/Tests/ResponsiveVisibility.test.ts +115 -0
  143. package/Tests/ResponsiveVisibility.ts +183 -0
  144. package/Tests/Server/API/DashboardPublicResourceListAPI.test.ts +129 -0
  145. package/Tests/Server/API/SessionReplayAPI.test.ts +1940 -216
  146. package/Tests/Server/API/UserNotificationSettingAPI.test.ts +186 -0
  147. package/Tests/Server/API/UserTelegramAPISecurity.test.ts +231 -0
  148. package/Tests/Server/EnvironmentConfigFrontendSecurity.test.ts +234 -0
  149. package/Tests/Server/EnvironmentConfigValkey.test.ts +227 -0
  150. package/Tests/Server/Infrastructure/Postgres/AddMacAddressToNetworkDeviceMigration.test.ts +365 -0
  151. package/Tests/Server/Infrastructure/Postgres/CustomFieldValueMappingMigration.test.ts +241 -0
  152. package/Tests/Server/Infrastructure/Postgres/InventoryItemArchiveMigration.test.ts +41 -0
  153. package/Tests/Server/Infrastructure/Postgres/SessionReplayRecordEverySessionByDefaultMigration.test.ts +169 -0
  154. package/Tests/Server/Infrastructure/QueueWorkerTimeout.test.ts +210 -0
  155. package/Tests/Server/Infrastructure/TelemetryExporterDeploymentConfig.test.ts +130 -0
  156. package/Tests/Server/Infrastructure/ValkeyDeploymentConfig.test.ts +265 -0
  157. package/Tests/Server/Middleware/HttpMetricsMiddleware.test.ts +382 -0
  158. package/Tests/Server/Middleware/ProjectAuthorizationApiKeyMiddleware.test.ts +4 -1
  159. package/Tests/Server/Middleware/SCIMAuthorization.test.ts +525 -0
  160. package/Tests/Server/Middleware/TelemetryIngestBrowserKey.test.ts +1163 -0
  161. package/Tests/Server/Middleware/TelemetryIngestTokenLog.test.ts +5 -3
  162. package/Tests/Server/Services/AddTelemetryIngestionKeyTypeMigration.test.ts +555 -0
  163. package/Tests/Server/Services/ApiKeyPermissionSecurity.test.ts +678 -0
  164. package/Tests/Server/Services/ApiKeyPermissionService.test.ts +276 -28
  165. package/Tests/Server/Services/CustomFieldDropdownOptionsColumnWidth.test.ts +431 -0
  166. package/Tests/Server/Services/CustomFieldMappingService.test.ts +850 -0
  167. package/Tests/Server/Services/DatabaseServiceAggregateBy.test.ts +291 -56
  168. package/Tests/Server/Services/DatabaseServiceSortTiebreaker.test.ts +151 -0
  169. package/Tests/Server/Services/DiscoveryScanClaimHookFreeSafety.test.ts +32 -0
  170. package/Tests/Server/Services/GlobalConfigService.test.ts +414 -1
  171. package/Tests/Server/Services/InventoryItemManualCreate.test.ts +105 -0
  172. package/Tests/Server/Services/InventoryItemPromotionGate.test.ts +178 -0
  173. package/Tests/Server/Services/MetricRawEntityKeyPrune.test.ts +592 -0
  174. package/Tests/Server/Services/MonitorTemplateServiceCustomFieldSync.test.ts +502 -0
  175. package/Tests/Server/Services/NetworkDeviceAutoImportRuleEngineService.test.ts +1351 -74
  176. package/Tests/Server/Services/NetworkDeviceDiscoveryScanRegistration.test.ts +520 -0
  177. package/Tests/Server/Services/NetworkDeviceMacAddressNormalization.test.ts +572 -0
  178. package/Tests/Server/Services/RoutineEmailSettingsPostgres.test.ts +329 -0
  179. package/Tests/Server/Services/RumSessionReplayViewService.test.ts +267 -0
  180. package/Tests/Server/Services/TeamMemberAutoAcceptInvitation.test.ts +9 -0
  181. package/Tests/Server/Services/TeamMemberInviteRegistrationToken.test.ts +9 -0
  182. package/Tests/Server/Services/TeamPrivilegeEscalation.test.ts +834 -0
  183. package/Tests/Server/Services/TelemetryIngestionKeyPolicyResolution.test.ts +814 -0
  184. package/Tests/Server/Services/TelemetryIngestionKeyValidation.test.ts +998 -0
  185. package/Tests/Server/Services/UserNotificationSettingRollupRouting.test.ts +141 -0
  186. package/Tests/Server/Services/UserNotificationSettingWorkspaceChannels.test.ts +18 -10
  187. package/Tests/Server/Services/UserTelegramVerificationSecurity.test.ts +688 -0
  188. package/Tests/Server/Types/Database/InventoryStatusPostgres.test.ts +368 -0
  189. package/Tests/Server/Types/Database/Permissions/ReadBlockPermission.test.ts +261 -14
  190. package/Tests/Server/Types/Database/QueryUtilIncludesAnyOfGroups.test.ts +169 -0
  191. package/Tests/Server/Types/Database/QueryUtilIncludesAnyOfGroupsPostgres.test.ts +297 -0
  192. package/Tests/Server/Types/Markdown.test.ts +194 -1
  193. package/Tests/Server/Types/Workflow/Components/IncomingWebhookUtils.test.ts +279 -0
  194. package/Tests/Server/Types/Workflow/Components/TextToJson.test.ts +212 -0
  195. package/Tests/Server/Utils/AI/SRE/Insights/FixRouting.test.ts +497 -0
  196. package/Tests/Server/Utils/AI/Toolbox/Serializer.test.ts +383 -0
  197. package/Tests/Server/Utils/APIKey/AccessPermission.test.ts +7 -3
  198. package/Tests/Server/Utils/AnalyticsDatabase/ClusterConfig.test.ts +315 -0
  199. package/Tests/Server/Utils/CustomField/CustomFieldMappingRegistry.test.ts +436 -0
  200. package/Tests/Server/Utils/CustomField/CustomFieldMappingValidator.test.ts +437 -0
  201. package/Tests/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.test.ts +214 -0
  202. package/Tests/Server/Utils/DataSource/EgressGuard.test.ts +300 -11
  203. package/Tests/Server/Utils/EmailRollup/EmailRollupBurstWindow.test.ts +83 -0
  204. package/Tests/Server/Utils/EmailRollup/EmailRollupFlushRunnerBehaviour.test.ts +2 -2
  205. package/Tests/Server/Utils/EmailRollup/EmailRollupFlushRunnerPreferences.test.ts +362 -0
  206. package/Tests/Server/Utils/EmailRollup/EmailRollupRenderer.test.ts +10 -3
  207. package/Tests/Server/Utils/EmailRollup/EmailRollupTestHarness.ts +77 -1
  208. package/Tests/Server/Utils/EventLoop.test.ts +359 -0
  209. package/Tests/Server/Utils/FrontendEnvironment.test.ts +190 -0
  210. package/Tests/Server/Utils/JsonWebToken.test.ts +30 -0
  211. package/Tests/Server/Utils/LogRedaction.test.ts +26 -0
  212. package/Tests/Server/Utils/LoggerCredentialLeak.test.ts +23 -9
  213. package/Tests/Server/Utils/Monitor/Criteria/CompareCriteria.test.ts +242 -0
  214. package/Tests/Server/Utils/Monitor/Criteria/CompareCriteriaAggregation.test.ts +590 -0
  215. package/Tests/Server/Utils/Monitor/Criteria/DatabaseMonitorCriteria.test.ts +29 -0
  216. package/Tests/Server/Utils/Monitor/Criteria/IncomingRequestHeaderCriteria.test.ts +236 -0
  217. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.test.ts +30 -6
  218. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteriaAnomalyUnits.test.ts +246 -0
  219. package/Tests/Server/Utils/Monitor/MonitorCriteriaEmailEvalLogAgreement.test.ts +280 -0
  220. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluator.test.ts +659 -8
  221. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorMetricUnits.test.ts +570 -0
  222. package/Tests/Server/Utils/Monitor/MonitorCriteriaExpectationBuilderUnits.test.ts +30 -5
  223. package/Tests/Server/Utils/Monitor/MonitorCriteriaMessageBuilderUnits.test.ts +68 -21
  224. package/Tests/Server/Utils/Monitor/MonitorCriteriaObservationBuilderUnits.test.ts +36 -19
  225. package/Tests/Server/Utils/Monitor/MonitorTemplateUtilSeriesContext.test.ts +176 -0
  226. package/Tests/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.test.ts +573 -0
  227. package/Tests/Server/Utils/Monitor/NetworkInventoryUtil.test.ts +317 -0
  228. package/Tests/Server/Utils/Monitor/SeriesContextEnricher.test.ts +356 -0
  229. package/Tests/Server/Utils/SSRFProtectionCloudServiceAddresses.test.ts +595 -0
  230. package/Tests/Server/Utils/SessionReplay/SessionReplayGateCachePolicy.test.ts +198 -1
  231. package/Tests/Server/Utils/SessionReplay/SessionReplayHealthCounters.test.ts +453 -0
  232. package/Tests/Server/Utils/SessionReplay/SessionReplayReadServiceQueries.test.ts +1378 -0
  233. package/Tests/Server/Utils/SessionReplayOriginAllowListRefactor.test.ts +420 -0
  234. package/Tests/Server/Utils/TelegramVerificationToken.test.ts +253 -0
  235. package/Tests/Server/Utils/Telemetry/ContextSpanProcessor.test.ts +281 -0
  236. package/Tests/Server/Utils/Telemetry/InventoryDuplicateIdentity.test.ts +316 -0
  237. package/Tests/Server/Utils/Telemetry/PinServiceName.test.ts +666 -0
  238. package/Tests/Server/Utils/Telemetry/TelemetryEntity.test.ts +111 -8
  239. package/Tests/Server/Utils/Telemetry/TelemetryFanInWriterCapacity.test.ts +458 -0
  240. package/Tests/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.test.ts +424 -0
  241. package/Tests/Server/Utils/Telemetry/TelemetryIngestionKeyRateLimiter.test.ts +528 -0
  242. package/Tests/Server/Utils/TelemetryExporterEnvironment.test.ts +56 -0
  243. package/Tests/Server/Utils/VM/VMRunnerHostBridgeLatency.test.ts +74 -0
  244. package/Tests/Server/Utils/VM/VMRunnerPrivateNetworkWiring.test.ts +100 -7
  245. package/Tests/Server/Utils/VM/VMRunnerSsrf.test.ts +1176 -6
  246. package/Tests/Types/CustomField/CustomFieldMappingCatalog.test.ts +220 -0
  247. package/Tests/Types/CustomField/CustomFieldValueMapping.test.ts +348 -0
  248. package/Tests/Types/Database/IncludesAnyOfGroups.test.ts +106 -0
  249. package/Tests/Types/JSONFunctions.test.ts +300 -0
  250. package/Tests/Types/Monitor/CephAlertTemplates.test.ts +422 -53
  251. package/Tests/Types/Monitor/DockerAlertTemplates.test.ts +495 -15
  252. package/Tests/Types/Monitor/DockerSwarmAlertTemplates.test.ts +191 -45
  253. package/Tests/Types/Monitor/HostAlertTemplates.test.ts +370 -42
  254. package/Tests/Types/Monitor/IotAlertTemplates.test.ts +375 -20
  255. package/Tests/Types/Monitor/KubernetesAlertTemplates.test.ts +636 -42
  256. package/Tests/Types/Monitor/KubernetesMetricCatalog.test.ts +189 -0
  257. package/Tests/Types/Monitor/KubernetesTemplateGroupByKeys.test.ts +95 -14
  258. package/Tests/Types/Monitor/PodmanAlertTemplates.test.ts +190 -21
  259. package/Tests/Types/Monitor/ProxmoxAlertTemplates.test.ts +352 -22
  260. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationAlertDebuggability.test.ts +363 -0
  261. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationNotificationMode.test.ts +93 -18
  262. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.test.ts +259 -0
  263. package/Tests/Types/Monitor/Recommendation/RecommendationCriteriaBuilder.test.ts +476 -0
  264. package/Tests/Types/Monitor/RumAlertTemplates.test.ts +136 -0
  265. package/Tests/Types/Monitor/SeriesContext/SeriesDebugHints.test.ts +661 -0
  266. package/Tests/Types/Monitor/SeriesContext/SeriesLabelDisplay.test.ts +507 -0
  267. package/Tests/Types/Monitor/ServiceAlertTemplates.test.ts +270 -0
  268. package/Tests/Types/Monitor/TemplateGroupByKeys.test.ts +38 -10
  269. package/Tests/Types/Monitor/Utils/RecommendationCriteriaAssertions.ts +102 -0
  270. package/Tests/Types/NetworkAutomation/AutoImportRunMerge.test.ts +235 -0
  271. package/Tests/Types/NetworkAutomation/RuleRunResult.test.ts +66 -0
  272. package/Tests/Types/NetworkAutomation/RuleRunResultDescribe.test.ts +309 -1
  273. package/Tests/Types/NotificationSetting/RoutineEmailEvents.test.ts +47 -0
  274. package/Tests/Types/Rum/SessionReplayApiContracts.test.ts +608 -0
  275. package/Tests/Types/Rum/SessionReplayCustomEvents.test.ts +434 -0
  276. package/Tests/Types/Telemetry/InventoryLiveness.test.ts +99 -0
  277. package/Tests/Types/WebsiteRequest.test.ts +285 -3
  278. package/Tests/UI/Components/Charts/ChartBucketIdentity.test.ts +16 -3
  279. package/Tests/UI/Components/Charts/ChartTrailingBucketGap.test.ts +691 -0
  280. package/Tests/UI/Components/ComponentsModal.test.tsx +12 -4
  281. package/Tests/UI/Components/ComponentsModalUsability.test.tsx +518 -0
  282. package/Tests/UI/Components/CustomFields/CustomFieldsDetail.test.tsx +160 -0
  283. package/Tests/UI/Components/CustomFields/CustomFieldsDetailMapping.test.tsx +324 -0
  284. package/Tests/UI/Components/CustomFields/MapFromCustomFieldInput.test.tsx +176 -0
  285. package/Tests/UI/Components/Graphs/DayUptimeGraph.test.tsx +484 -0
  286. package/Tests/UI/Components/HeaderRightRail.test.tsx +157 -0
  287. package/Tests/UI/Components/IconDropdownItem.test.tsx +102 -0
  288. package/Tests/UI/Components/JSONTablePrototypePollution.test.tsx +117 -0
  289. package/Tests/UI/Components/KeyboardShortcutsModal.test.tsx +214 -0
  290. package/Tests/UI/Components/ModelTable/ModelTableWrapContent.test.tsx +561 -0
  291. package/Tests/UI/Components/MonitorGraphs/UptimeBarDayModal.test.tsx +302 -0
  292. package/Tests/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.test.ts +28 -12
  293. package/Tests/UI/Components/ShortcutDialogGuard.test.tsx +74 -0
  294. package/Tests/UI/Components/StatusPage/ResourceGroupSectionAutoExpand.test.tsx +248 -0
  295. package/Tests/UI/Components/TableCellWrapping.test.tsx +625 -0
  296. package/Tests/UI/Components/TableLoadingStates.test.tsx +205 -0
  297. package/Tests/UI/Components/Workflow/NodePlacement.test.ts +148 -0
  298. package/Tests/UI/Components/Workflow/Workflow.test.tsx +981 -0
  299. package/Tests/UI/ConfigBrowserTelemetry.test.ts +70 -0
  300. package/Tests/UI/ReactRouterSingletonBuild.test.ts +133 -0
  301. package/Tests/UI/Rum/ChunkLoader.test.ts +1036 -2
  302. package/Tests/UI/Rum/FidelityNotices.test.ts +187 -0
  303. package/Tests/UI/Rum/InactivityMap.test.ts +341 -0
  304. package/Tests/UI/Rum/PrivacySummaryCard.test.tsx +259 -0
  305. package/Tests/UI/Rum/RecordingHealthCard.test.tsx +855 -0
  306. package/Tests/UI/Rum/RecordingHealthStrip.test.tsx +594 -0
  307. package/Tests/UI/Rum/ReplayCard.test.tsx +451 -0
  308. package/Tests/UI/Rum/ReplayCorrelationPanel.test.tsx +536 -0
  309. package/Tests/UI/Rum/ReplayEngine.test.ts +2503 -0
  310. package/Tests/UI/Rum/ReplayEngineTypes.test.ts +171 -0
  311. package/Tests/UI/Rum/ReplayHeader.test.tsx +719 -0
  312. package/Tests/UI/Rum/ReplayLink.test.tsx +119 -0
  313. package/Tests/UI/Rum/ReplayPinControl.test.tsx +408 -0
  314. package/Tests/UI/Rum/ReplayPlaybackIntent.test.ts +152 -0
  315. package/Tests/UI/Rum/ReplayPlayerChrome.test.tsx +514 -0
  316. package/Tests/UI/Rum/ReplayRail.test.tsx +1296 -0
  317. package/Tests/UI/Rum/ReplayRailDetail.test.tsx +900 -0
  318. package/Tests/UI/Rum/ReplayScrubber.test.tsx +763 -147
  319. package/Tests/UI/Rum/ReplaySignalTypes.test.ts +189 -0
  320. package/Tests/UI/Rum/ReplaySignals.test.ts +1579 -0
  321. package/Tests/UI/Rum/ReplayStage.test.tsx +396 -473
  322. package/Tests/UI/Rum/ReplayStageOverlays.test.tsx +918 -0
  323. package/Tests/UI/Rum/ReplayTimeFormat.test.ts +94 -0
  324. package/Tests/UI/Rum/ReplayTimeline.test.tsx +696 -0
  325. package/Tests/UI/Rum/ReplayUi.test.tsx +559 -0
  326. package/Tests/UI/Rum/SessionReplayEmptyState.test.tsx +499 -0
  327. package/Tests/UI/Rum/SessionReplaySearchBar.test.tsx +346 -0
  328. package/Tests/UI/Rum/SessionReplaySetupGuide.test.tsx +545 -0
  329. package/Tests/UI/Rum/SessionReplayTable.test.tsx +991 -0
  330. package/Tests/UI/Rum/TargetedCapturePanel.test.tsx +254 -0
  331. package/Tests/UI/Telemetry/BrowserExporterIsolation.test.ts +71 -0
  332. package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +5 -0
  333. package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +1 -0
  334. package/Tests/UI/Utils/GlobalKeyboardShortcut.test.ts +344 -0
  335. package/Tests/Utils/API.test.ts +308 -0
  336. package/Tests/Utils/Dashboard/Components/DashboardMonitorListComponent.test.ts +11 -1
  337. package/Tests/Utils/Dashboard/LabelVariable.test.ts +257 -0
  338. package/Tests/Utils/Dashboard/VariableUrlState.test.ts +144 -14
  339. package/Tests/Utils/HTTPResponseBodyReader.test.ts +323 -0
  340. package/Tests/Utils/MetricUnitUtil.test.ts +81 -0
  341. package/Tests/Utils/Monitor/DeviceMacLearningUtil.test.ts +497 -0
  342. package/Tests/Utils/Monitor/MetricValueFormatter.test.ts +546 -0
  343. package/Tests/Utils/Monitor/MonitorTemplateCustomFieldUtil.test.ts +218 -0
  344. package/Tests/Utils/Monitor/NetworkTopologyEndpointAdoption.test.ts +1755 -0
  345. package/Tests/Utils/NetworkAutomation/AutoImportRunChain.test.ts +320 -0
  346. package/Tests/Utils/NetworkDiscovery/DiscoveryScanStatus.test.ts +117 -0
  347. package/Tests/Utils/Rum/ChunkMath.test.ts +83 -0
  348. package/Tests/Utils/Rum/SessionReplayHealthDiagnosis.test.ts +993 -0
  349. package/Tests/Utils/Rum/SessionReplayStringMap.test.ts +245 -0
  350. package/Tests/Utils/StatusPage/ResourceSearch.test.ts +631 -0
  351. package/Tests/Utils/Telemetry/OriginAllowList.test.ts +633 -0
  352. package/Tests/Utils/Uptime/DayUptimeGraphUtil.test.ts +461 -0
  353. package/Tests/Utils/ValueFormatter.test.ts +148 -0
  354. package/Types/BaseDatabase/IncludesAnyOfGroups.ts +77 -0
  355. package/Types/CustomField/CustomFieldMappingCatalog.ts +161 -0
  356. package/Types/CustomField/CustomFieldMappingSourceResource.ts +19 -0
  357. package/Types/CustomField/CustomFieldValueMapping.ts +288 -0
  358. package/Types/Dashboard/DashboardComponents/ComponentArgument.ts +1 -0
  359. package/Types/Dashboard/DashboardComponents/DashboardMonitorListComponent.ts +1 -0
  360. package/Types/Dashboard/DashboardVariable.ts +8 -0
  361. package/Types/Icon/IconProp.ts +1 -0
  362. package/Types/JSON.ts +4 -0
  363. package/Types/JSONFunctions.ts +205 -43
  364. package/Types/Monitor/CephAlertTemplates.ts +157 -195
  365. package/Types/Monitor/DockerAlertTemplates.ts +338 -128
  366. package/Types/Monitor/DockerSwarmAlertTemplates.ts +95 -112
  367. package/Types/Monitor/HostAlertTemplates.ts +302 -130
  368. package/Types/Monitor/IotAlertTemplates.ts +112 -104
  369. package/Types/Monitor/KubernetesAlertTemplates.ts +415 -224
  370. package/Types/Monitor/KubernetesMetricCatalog.ts +23 -19
  371. package/Types/Monitor/PodmanAlertTemplates.ts +116 -163
  372. package/Types/Monitor/ProxmoxAlertTemplates.ts +217 -99
  373. package/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.ts +98 -6
  374. package/Types/Monitor/Recommendation/MonitorRecommendationUtil.ts +44 -7
  375. package/Types/Monitor/Recommendation/RecommendationCriteriaBuilder.ts +383 -0
  376. package/Types/Monitor/RumAlertTemplates.ts +46 -7
  377. package/Types/Monitor/SeriesContext/SeriesDebugHints.ts +596 -0
  378. package/Types/Monitor/SeriesContext/SeriesLabelDisplay.ts +554 -0
  379. package/Types/Monitor/ServiceAlertTemplates.ts +98 -11
  380. package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +13 -0
  381. package/Types/Monitor/UptimeHistoryLabels.ts +56 -0
  382. package/Types/NetworkAutomation/RuleRunResult.ts +402 -13
  383. package/Types/NotificationSetting/RoutineEmailEvents.ts +30 -0
  384. package/Types/Rum/SessionReplay.ts +209 -0
  385. package/Types/Rum/SessionReplayApi.ts +722 -0
  386. package/Types/Rum/SessionReplayCaptureTrigger.ts +21 -12
  387. package/Types/Rum/SessionReplayConsentMode.ts +11 -7
  388. package/Types/Rum/SessionReplayCustomEvents.ts +549 -0
  389. package/Types/Rum/SessionReplayHealth.ts +238 -0
  390. package/Types/SerializableObjectDictionary.ts +4 -0
  391. package/Types/Telemetry/InventoryLiveness.ts +56 -0
  392. package/Types/Telemetry/TelemetryIngestSurface.ts +95 -0
  393. package/Types/Telemetry/TelemetryIngestionKeyPolicy.ts +75 -0
  394. package/Types/Telemetry/TelemetryIngestionKeyType.ts +29 -0
  395. package/Types/WebsiteRequest.ts +85 -3
  396. package/UI/Components/CSVFileUpload/CSVFileUpload.tsx +9 -2
  397. package/UI/Components/Charts/Area/AreaChart.tsx +13 -2
  398. package/UI/Components/Charts/Bar/BarChart.tsx +6 -2
  399. package/UI/Components/Charts/Line/LineChart.tsx +13 -2
  400. package/UI/Components/Charts/Types/XAxis/XAxis.ts +29 -0
  401. package/UI/Components/Charts/Utils/DataPoint.ts +22 -3
  402. package/UI/Components/Charts/Utils/TimeAnnotation.ts +76 -9
  403. package/UI/Components/Charts/Utils/XAxis.ts +217 -0
  404. package/UI/Components/CustomFields/CustomFieldsDetail.tsx +189 -46
  405. package/UI/Components/CustomFields/MapFromCustomFieldInput.tsx +216 -0
  406. package/UI/Components/Dashboard/DashboardVariableControl.tsx +285 -0
  407. package/UI/Components/Graphs/DayUptimeGraph.tsx +173 -8
  408. package/UI/Components/Graphs/UptimeBarTooltip.tsx +22 -278
  409. package/UI/Components/Graphs/UptimeDaySummary.tsx +327 -0
  410. package/UI/Components/Header/Header.tsx +25 -9
  411. package/UI/Components/Header/IconDropdown/IconDropdownItem.tsx +10 -1
  412. package/UI/Components/Header/IconDropdown/IconDropdownMenu.tsx +6 -1
  413. package/UI/Components/Header/ProjectPicker/ProjectPicker.tsx +6 -1
  414. package/UI/Components/HeaderAlert/NotificationBell/NotificationBellDropdown.tsx +6 -1
  415. package/UI/Components/Icon/Icon.tsx +11 -0
  416. package/UI/Components/JSONTable/JSONTable.tsx +2 -2
  417. package/UI/Components/KeyboardShortcut/KeyboardShortcutsModal.tsx +125 -0
  418. package/UI/Components/KeyboardShortcut/Screenshots/README.md +19 -0
  419. package/UI/Components/KeyboardShortcut/Screenshots/keyboard-shortcuts-command-palette.png +0 -0
  420. package/UI/Components/KeyboardShortcut/Screenshots/keyboard-shortcuts-dialog-dark.png +0 -0
  421. package/UI/Components/KeyboardShortcut/Screenshots/keyboard-shortcuts-dialog.png +0 -0
  422. package/UI/Components/KeyboardShortcut/Screenshots/keyboard-shortcuts-help-menu.png +0 -0
  423. package/UI/Components/KeyboardShortcut/Screenshots/keyboard-shortcuts-narrow.png +0 -0
  424. package/UI/Components/ModelTable/Column.ts +15 -0
  425. package/UI/Components/Monitor/SeriesDebugCommandsViewer.tsx +64 -0
  426. package/UI/Components/Monitor/SeriesLabelsViewer.tsx +93 -0
  427. package/UI/Components/MonitorGraphs/Uptime.tsx +14 -2
  428. package/UI/Components/MonitorGraphs/UptimeBarDayModal.tsx +63 -10
  429. package/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.ts +44 -0
  430. package/UI/Components/StatusPage/ResourceGroupSection.tsx +40 -0
  431. package/UI/Components/Table/CellClassName.ts +81 -0
  432. package/UI/Components/Table/TableRow.tsx +20 -14
  433. package/UI/Components/Table/TableSkeletonRows.tsx +9 -9
  434. package/UI/Components/Table/Types/Column.ts +36 -0
  435. package/UI/Components/Tooltip/Tooltip.tsx +11 -1
  436. package/UI/Components/Workflow/ComponentsModal.tsx +60 -39
  437. package/UI/Components/Workflow/NodePlacement.ts +57 -0
  438. package/UI/Components/Workflow/Workflow.tsx +47 -18
  439. package/UI/Config.ts +15 -33
  440. package/UI/Utils/GlobalKeyboardShortcut.ts +208 -0
  441. package/UI/Utils/Telemetry/BrowserTelemetryConfig.ts +28 -0
  442. package/UI/Utils/Telemetry/Telemetry.ts +10 -8
  443. package/UI/esbuild-config.js +6 -0
  444. package/Utils/API.ts +129 -3
  445. package/Utils/Dashboard/Components/DashboardMonitorListComponent.ts +12 -1
  446. package/Utils/Dashboard/LabelVariable.ts +152 -0
  447. package/Utils/Dashboard/VariableUrlState.ts +27 -5
  448. package/Utils/HTTPResponseBodyReader.ts +221 -0
  449. package/Utils/MetricUnitUtil.ts +34 -0
  450. package/Utils/Monitor/DeviceMacLearningUtil.ts +177 -0
  451. package/Utils/Monitor/MetricValueFormatter.ts +342 -0
  452. package/Utils/Monitor/MonitorTemplateCustomFieldUtil.ts +124 -0
  453. package/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.ts +7 -2
  454. package/Utils/Monitor/NetworkTopologyUtil.ts +693 -0
  455. package/Utils/NetworkAutomation/AutoImportRunChain.ts +138 -0
  456. package/Utils/NetworkDiscovery/DiscoveryScanStatus.ts +77 -0
  457. package/Utils/Rum/ChunkMath.ts +106 -0
  458. package/Utils/Rum/SessionReplayHealth.ts +732 -0
  459. package/Utils/Rum/SessionReplayStringMap.ts +226 -0
  460. package/Utils/Schema/ModelSchema.ts +1 -0
  461. package/Utils/StatusPage/ResourceSearch.ts +290 -0
  462. package/Utils/Telemetry/OriginAllowList.ts +355 -0
  463. package/Utils/Uptime/DayUptimeGraphUtil.ts +205 -0
  464. package/Utils/ValueFormatter.ts +35 -1
  465. package/build/dist/Models/AnalyticsModels/RumSession.js +102 -19
  466. package/build/dist/Models/AnalyticsModels/RumSession.js.map +1 -1
  467. package/build/dist/Models/AnalyticsModels/RumSessionChunk.js +11 -9
  468. package/build/dist/Models/AnalyticsModels/RumSessionChunk.js.map +1 -1
  469. package/build/dist/Models/DatabaseModels/AlertCustomField.js +79 -3
  470. package/build/dist/Models/DatabaseModels/AlertCustomField.js.map +1 -1
  471. package/build/dist/Models/DatabaseModels/ApiKeyPermission.js +3 -27
  472. package/build/dist/Models/DatabaseModels/ApiKeyPermission.js.map +1 -1
  473. package/build/dist/Models/DatabaseModels/GlobalConfig.js +12 -12
  474. package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
  475. package/build/dist/Models/DatabaseModels/IncidentCustomField.js +79 -3
  476. package/build/dist/Models/DatabaseModels/IncidentCustomField.js.map +1 -1
  477. package/build/dist/Models/DatabaseModels/InventoryItem.js +34 -1
  478. package/build/dist/Models/DatabaseModels/InventoryItem.js.map +1 -1
  479. package/build/dist/Models/DatabaseModels/InventoryItemCustomField.js +79 -3
  480. package/build/dist/Models/DatabaseModels/InventoryItemCustomField.js.map +1 -1
  481. package/build/dist/Models/DatabaseModels/MonitorCustomField.js +79 -3
  482. package/build/dist/Models/DatabaseModels/MonitorCustomField.js.map +1 -1
  483. package/build/dist/Models/DatabaseModels/NetworkDevice.js +128 -0
  484. package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
  485. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyCustomField.js +79 -3
  486. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyCustomField.js.map +1 -1
  487. package/build/dist/Models/DatabaseModels/RumApplication.js +6 -6
  488. package/build/dist/Models/DatabaseModels/RumApplication.js.map +1 -1
  489. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceCustomField.js +79 -3
  490. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceCustomField.js.map +1 -1
  491. package/build/dist/Models/DatabaseModels/StatusPageCustomField.js +79 -3
  492. package/build/dist/Models/DatabaseModels/StatusPageCustomField.js.map +1 -1
  493. package/build/dist/Models/DatabaseModels/TeamCustomField.js +79 -3
  494. package/build/dist/Models/DatabaseModels/TeamCustomField.js.map +1 -1
  495. package/build/dist/Models/DatabaseModels/TeamMember.js +0 -9
  496. package/build/dist/Models/DatabaseModels/TeamMember.js.map +1 -1
  497. package/build/dist/Models/DatabaseModels/TeamMemberCustomField.js +79 -3
  498. package/build/dist/Models/DatabaseModels/TeamMemberCustomField.js.map +1 -1
  499. package/build/dist/Models/DatabaseModels/TeamPermission.js +3 -24
  500. package/build/dist/Models/DatabaseModels/TeamPermission.js.map +1 -1
  501. package/build/dist/Models/DatabaseModels/TelemetryIngestionKey.js +267 -0
  502. package/build/dist/Models/DatabaseModels/TelemetryIngestionKey.js.map +1 -1
  503. package/build/dist/Models/DatabaseModels/UserTelegram.js +0 -4
  504. package/build/dist/Models/DatabaseModels/UserTelegram.js.map +1 -1
  505. package/build/dist/Server/API/TelemetryAPI.js +769 -109
  506. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  507. package/build/dist/Server/API/UserNotificationSettingAPI.js +35 -0
  508. package/build/dist/Server/API/UserNotificationSettingAPI.js.map +1 -0
  509. package/build/dist/Server/API/UserTelegramAPI.js +28 -6
  510. package/build/dist/Server/API/UserTelegramAPI.js.map +1 -1
  511. package/build/dist/Server/EnvironmentConfig.js +73 -14
  512. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  513. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791300000000-AddTelemetryIngestionKeyType.js +60 -0
  514. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791300000000-AddTelemetryIngestionKeyType.js.map +1 -0
  515. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791400000000-SessionReplayRecordEverySessionByDefault.js +53 -0
  516. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791400000000-SessionReplayRecordEverySessionByDefault.js.map +1 -0
  517. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791500000000-WidenCustomFieldDropdownOptions.js +82 -0
  518. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791500000000-WidenCustomFieldDropdownOptions.js.map +1 -0
  519. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791600000000-AddCustomFieldValueMapping.js +46 -0
  520. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791600000000-AddCustomFieldValueMapping.js.map +1 -0
  521. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791700000000-AddMacAddressToNetworkDevice.js +14 -0
  522. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791700000000-AddMacAddressToNetworkDevice.js.map +1 -0
  523. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +10 -0
  524. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  525. package/build/dist/Server/Infrastructure/QueueWorker.js +19 -7
  526. package/build/dist/Server/Infrastructure/QueueWorker.js.map +1 -1
  527. package/build/dist/Server/Infrastructure/Redis.js +8 -8
  528. package/build/dist/Server/Infrastructure/Redis.js.map +1 -1
  529. package/build/dist/Server/Infrastructure/Semaphore.js +1 -0
  530. package/build/dist/Server/Infrastructure/Semaphore.js.map +1 -1
  531. package/build/dist/Server/Infrastructure/Status.js +10 -4
  532. package/build/dist/Server/Infrastructure/Status.js.map +1 -1
  533. package/build/dist/Server/Middleware/TelemetryIngest.js +315 -8
  534. package/build/dist/Server/Middleware/TelemetryIngest.js.map +1 -1
  535. package/build/dist/Server/Services/AccessTokenService.js +1 -0
  536. package/build/dist/Server/Services/AccessTokenService.js.map +1 -1
  537. package/build/dist/Server/Services/AlertCustomFieldService.js +93 -0
  538. package/build/dist/Server/Services/AlertCustomFieldService.js.map +1 -1
  539. package/build/dist/Server/Services/AlertService.js +34 -0
  540. package/build/dist/Server/Services/AlertService.js.map +1 -1
  541. package/build/dist/Server/Services/ApiKeyPermissionService.js +331 -28
  542. package/build/dist/Server/Services/ApiKeyPermissionService.js.map +1 -1
  543. package/build/dist/Server/Services/CustomFieldMappingService.js +614 -0
  544. package/build/dist/Server/Services/CustomFieldMappingService.js.map +1 -0
  545. package/build/dist/Server/Services/DatabaseService.js +58 -13
  546. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  547. package/build/dist/Server/Services/GlobalConfigService.js +133 -0
  548. package/build/dist/Server/Services/GlobalConfigService.js.map +1 -1
  549. package/build/dist/Server/Services/IncidentCustomFieldService.js +93 -0
  550. package/build/dist/Server/Services/IncidentCustomFieldService.js.map +1 -1
  551. package/build/dist/Server/Services/IncidentService.js +32 -0
  552. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  553. package/build/dist/Server/Services/InventoryItemService.js +28 -1
  554. package/build/dist/Server/Services/InventoryItemService.js.map +1 -1
  555. package/build/dist/Server/Services/MetricService.js +163 -0
  556. package/build/dist/Server/Services/MetricService.js.map +1 -1
  557. package/build/dist/Server/Services/MonitorService.js +29 -0
  558. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  559. package/build/dist/Server/Services/MonitorTemplateService.js +116 -17
  560. package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
  561. package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js +349 -64
  562. package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js.map +1 -1
  563. package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js +65 -0
  564. package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js.map +1 -1
  565. package/build/dist/Server/Services/NetworkDeviceService.js +54 -0
  566. package/build/dist/Server/Services/NetworkDeviceService.js.map +1 -1
  567. package/build/dist/Server/Services/ProjectService.js +22 -0
  568. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  569. package/build/dist/Server/Services/RoutineEmailSettingsService.js +69 -0
  570. package/build/dist/Server/Services/RoutineEmailSettingsService.js.map +1 -0
  571. package/build/dist/Server/Services/RumSessionReplayViewService.js +86 -24
  572. package/build/dist/Server/Services/RumSessionReplayViewService.js.map +1 -1
  573. package/build/dist/Server/Services/ScheduledMaintenanceCustomFieldService.js +93 -0
  574. package/build/dist/Server/Services/ScheduledMaintenanceCustomFieldService.js.map +1 -1
  575. package/build/dist/Server/Services/ScheduledMaintenanceService.js +29 -0
  576. package/build/dist/Server/Services/ScheduledMaintenanceService.js.map +1 -1
  577. package/build/dist/Server/Services/TeamMemberService.js +62 -2
  578. package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
  579. package/build/dist/Server/Services/TeamPermissionService.js +166 -3
  580. package/build/dist/Server/Services/TeamPermissionService.js.map +1 -1
  581. package/build/dist/Server/Services/TelemetryIngestionKeyService.js +562 -18
  582. package/build/dist/Server/Services/TelemetryIngestionKeyService.js.map +1 -1
  583. package/build/dist/Server/Services/UserNotificationRuleService.js +60 -25
  584. package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
  585. package/build/dist/Server/Services/UserNotificationSettingService.js +26 -6
  586. package/build/dist/Server/Services/UserNotificationSettingService.js.map +1 -1
  587. package/build/dist/Server/Services/UserTelegramService.js +257 -4
  588. package/build/dist/Server/Services/UserTelegramService.js.map +1 -1
  589. package/build/dist/Server/Types/Database/Permissions/ReadPermission.js +29 -4
  590. package/build/dist/Server/Types/Database/Permissions/ReadPermission.js.map +1 -1
  591. package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
  592. package/build/dist/Server/Types/Database/QueryUtil.js +36 -0
  593. package/build/dist/Server/Types/Database/QueryUtil.js.map +1 -1
  594. package/build/dist/Server/Types/Markdown.js +75 -0
  595. package/build/dist/Server/Types/Markdown.js.map +1 -1
  596. package/build/dist/Server/Utils/APIKey/AccessPermission.js +2 -2
  597. package/build/dist/Server/Utils/APIKey/AccessPermission.js.map +1 -1
  598. package/build/dist/Server/Utils/CustomField/CustomFieldDefinitionMappingHooks.js +27 -0
  599. package/build/dist/Server/Utils/CustomField/CustomFieldDefinitionMappingHooks.js.map +1 -0
  600. package/build/dist/Server/Utils/CustomField/CustomFieldMappingRegistry.js +226 -0
  601. package/build/dist/Server/Utils/CustomField/CustomFieldMappingRegistry.js.map +1 -0
  602. package/build/dist/Server/Utils/CustomField/CustomFieldMappingValidator.js +189 -0
  603. package/build/dist/Server/Utils/CustomField/CustomFieldMappingValidator.js.map +1 -0
  604. package/build/dist/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.js +53 -18
  605. package/build/dist/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.js.map +1 -1
  606. package/build/dist/Server/Utils/DataSource/EgressGuard.js +121 -7
  607. package/build/dist/Server/Utils/DataSource/EgressGuard.js.map +1 -1
  608. package/build/dist/Server/Utils/EmailRollup/EmailRollupConstants.js +38 -5
  609. package/build/dist/Server/Utils/EmailRollup/EmailRollupConstants.js.map +1 -1
  610. package/build/dist/Server/Utils/EmailRollup/EmailRollupFlushRunner.js +54 -3
  611. package/build/dist/Server/Utils/EmailRollup/EmailRollupFlushRunner.js.map +1 -1
  612. package/build/dist/Server/Utils/EmailRollup/EmailRollupRenderer.js +2 -2
  613. package/build/dist/Server/Utils/EmailRollup/EmailRollupRenderer.js.map +1 -1
  614. package/build/dist/Server/Utils/FrontendEnvironment.js +33 -0
  615. package/build/dist/Server/Utils/FrontendEnvironment.js.map +1 -0
  616. package/build/dist/Server/Utils/LogRedaction.js +20 -0
  617. package/build/dist/Server/Utils/LogRedaction.js.map +1 -1
  618. package/build/dist/Server/Utils/Logger.js +10 -0
  619. package/build/dist/Server/Utils/Logger.js.map +1 -1
  620. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +323 -81
  621. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js.map +1 -1
  622. package/build/dist/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.js +20 -4
  623. package/build/dist/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.js.map +1 -1
  624. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +29 -5
  625. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js.map +1 -1
  626. package/build/dist/Server/Utils/Monitor/MonitorAlert.js +29 -6
  627. package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
  628. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +254 -48
  629. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  630. package/build/dist/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.js +58 -9
  631. package/build/dist/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.js.map +1 -1
  632. package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageBuilder.js +9 -3
  633. package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageBuilder.js.map +1 -1
  634. package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageFormatter.js +26 -12
  635. package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageFormatter.js.map +1 -1
  636. package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js +74 -33
  637. package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js.map +1 -1
  638. package/build/dist/Server/Utils/Monitor/MonitorIncident.js +22 -6
  639. package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
  640. package/build/dist/Server/Utils/Monitor/MonitorTemplateUtil.js +22 -0
  641. package/build/dist/Server/Utils/Monitor/MonitorTemplateUtil.js.map +1 -1
  642. package/build/dist/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.js +114 -0
  643. package/build/dist/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.js.map +1 -0
  644. package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js +25 -0
  645. package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js.map +1 -1
  646. package/build/dist/Server/Utils/Monitor/SeriesContextEnricher.js +237 -0
  647. package/build/dist/Server/Utils/Monitor/SeriesContextEnricher.js.map +1 -0
  648. package/build/dist/Server/Utils/SSRFProtection.js +136 -9
  649. package/build/dist/Server/Utils/SSRFProtection.js.map +1 -1
  650. package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCache.js +174 -89
  651. package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCache.js.map +1 -1
  652. package/build/dist/Server/Utils/SessionReplay/SessionReplayHealthCounters.js +223 -0
  653. package/build/dist/Server/Utils/SessionReplay/SessionReplayHealthCounters.js.map +1 -0
  654. package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js +945 -164
  655. package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js.map +1 -1
  656. package/build/dist/Server/Utils/StartServer.js +3 -14
  657. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  658. package/build/dist/Server/Utils/TelegramVerificationToken.js +129 -0
  659. package/build/dist/Server/Utils/TelegramVerificationToken.js.map +1 -0
  660. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +9 -1
  661. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
  662. package/build/dist/Server/Utils/Telemetry/PinServiceName.js +163 -0
  663. package/build/dist/Server/Utils/Telemetry/PinServiceName.js.map +1 -0
  664. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js +70 -13
  665. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js.map +1 -1
  666. package/build/dist/Server/Utils/Telemetry/TelemetryFanInWriter.js +28 -15
  667. package/build/dist/Server/Utils/Telemetry/TelemetryFanInWriter.js.map +1 -1
  668. package/build/dist/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.js +64 -0
  669. package/build/dist/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.js.map +1 -0
  670. package/build/dist/Server/Utils/Telemetry/TelemetryIngestionKeyRateLimiter.js +171 -0
  671. package/build/dist/Server/Utils/Telemetry/TelemetryIngestionKeyRateLimiter.js.map +1 -0
  672. package/build/dist/Server/Utils/VM/VMAPI.js.map +1 -1
  673. package/build/dist/Server/Utils/VM/VMRunner.js +780 -69
  674. package/build/dist/Server/Utils/VM/VMRunner.js.map +1 -1
  675. package/build/dist/Types/BaseDatabase/IncludesAnyOfGroups.js +54 -0
  676. package/build/dist/Types/BaseDatabase/IncludesAnyOfGroups.js.map +1 -0
  677. package/build/dist/Types/CustomField/CustomFieldMappingCatalog.js +60 -0
  678. package/build/dist/Types/CustomField/CustomFieldMappingCatalog.js.map +1 -0
  679. package/build/dist/Types/CustomField/CustomFieldMappingSourceResource.js +20 -0
  680. package/build/dist/Types/CustomField/CustomFieldMappingSourceResource.js.map +1 -0
  681. package/build/dist/Types/CustomField/CustomFieldValueMapping.js +141 -0
  682. package/build/dist/Types/CustomField/CustomFieldValueMapping.js.map +1 -0
  683. package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js +1 -0
  684. package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js.map +1 -1
  685. package/build/dist/Types/Dashboard/DashboardVariable.js +1 -0
  686. package/build/dist/Types/Dashboard/DashboardVariable.js.map +1 -1
  687. package/build/dist/Types/Icon/IconProp.js +1 -0
  688. package/build/dist/Types/Icon/IconProp.js.map +1 -1
  689. package/build/dist/Types/JSON.js +1 -0
  690. package/build/dist/Types/JSON.js.map +1 -1
  691. package/build/dist/Types/JSONFunctions.js +131 -37
  692. package/build/dist/Types/JSONFunctions.js.map +1 -1
  693. package/build/dist/Types/Monitor/CephAlertTemplates.js +130 -171
  694. package/build/dist/Types/Monitor/CephAlertTemplates.js.map +1 -1
  695. package/build/dist/Types/Monitor/DockerAlertTemplates.js +286 -121
  696. package/build/dist/Types/Monitor/DockerAlertTemplates.js.map +1 -1
  697. package/build/dist/Types/Monitor/DockerSwarmAlertTemplates.js +84 -105
  698. package/build/dist/Types/Monitor/DockerSwarmAlertTemplates.js.map +1 -1
  699. package/build/dist/Types/Monitor/HostAlertTemplates.js +250 -122
  700. package/build/dist/Types/Monitor/HostAlertTemplates.js.map +1 -1
  701. package/build/dist/Types/Monitor/IotAlertTemplates.js +81 -81
  702. package/build/dist/Types/Monitor/IotAlertTemplates.js.map +1 -1
  703. package/build/dist/Types/Monitor/KubernetesAlertTemplates.js +375 -215
  704. package/build/dist/Types/Monitor/KubernetesAlertTemplates.js.map +1 -1
  705. package/build/dist/Types/Monitor/KubernetesMetricCatalog.js +19 -19
  706. package/build/dist/Types/Monitor/KubernetesMetricCatalog.js.map +1 -1
  707. package/build/dist/Types/Monitor/PodmanAlertTemplates.js +104 -153
  708. package/build/dist/Types/Monitor/PodmanAlertTemplates.js.map +1 -1
  709. package/build/dist/Types/Monitor/ProxmoxAlertTemplates.js +156 -91
  710. package/build/dist/Types/Monitor/ProxmoxAlertTemplates.js.map +1 -1
  711. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.js +67 -6
  712. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.js.map +1 -1
  713. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationUtil.js +38 -7
  714. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationUtil.js.map +1 -1
  715. package/build/dist/Types/Monitor/Recommendation/RecommendationCriteriaBuilder.js +261 -0
  716. package/build/dist/Types/Monitor/Recommendation/RecommendationCriteriaBuilder.js.map +1 -0
  717. package/build/dist/Types/Monitor/RumAlertTemplates.js +46 -7
  718. package/build/dist/Types/Monitor/RumAlertTemplates.js.map +1 -1
  719. package/build/dist/Types/Monitor/SeriesContext/SeriesDebugHints.js +422 -0
  720. package/build/dist/Types/Monitor/SeriesContext/SeriesDebugHints.js.map +1 -0
  721. package/build/dist/Types/Monitor/SeriesContext/SeriesLabelDisplay.js +441 -0
  722. package/build/dist/Types/Monitor/SeriesContext/SeriesLabelDisplay.js.map +1 -0
  723. package/build/dist/Types/Monitor/ServiceAlertTemplates.js +79 -15
  724. package/build/dist/Types/Monitor/ServiceAlertTemplates.js.map +1 -1
  725. package/build/dist/Types/Monitor/UptimeHistoryLabels.js +17 -0
  726. package/build/dist/Types/Monitor/UptimeHistoryLabels.js.map +1 -0
  727. package/build/dist/Types/NetworkAutomation/RuleRunResult.js +291 -6
  728. package/build/dist/Types/NetworkAutomation/RuleRunResult.js.map +1 -1
  729. package/build/dist/Types/NotificationSetting/RoutineEmailEvents.js +29 -0
  730. package/build/dist/Types/NotificationSetting/RoutineEmailEvents.js.map +1 -0
  731. package/build/dist/Types/Rum/SessionReplay.js +117 -0
  732. package/build/dist/Types/Rum/SessionReplay.js.map +1 -1
  733. package/build/dist/Types/Rum/SessionReplayApi.js +181 -0
  734. package/build/dist/Types/Rum/SessionReplayApi.js.map +1 -0
  735. package/build/dist/Types/Rum/SessionReplayCaptureTrigger.js +21 -12
  736. package/build/dist/Types/Rum/SessionReplayCaptureTrigger.js.map +1 -1
  737. package/build/dist/Types/Rum/SessionReplayConsentMode.js +11 -7
  738. package/build/dist/Types/Rum/SessionReplayConsentMode.js.map +1 -1
  739. package/build/dist/Types/Rum/SessionReplayCustomEvents.js +172 -0
  740. package/build/dist/Types/Rum/SessionReplayCustomEvents.js.map +1 -0
  741. package/build/dist/Types/Rum/SessionReplayHealth.js +37 -0
  742. package/build/dist/Types/Rum/SessionReplayHealth.js.map +1 -0
  743. package/build/dist/Types/SerializableObjectDictionary.js +4 -0
  744. package/build/dist/Types/SerializableObjectDictionary.js.map +1 -1
  745. package/build/dist/Types/Telemetry/InventoryLiveness.js +37 -0
  746. package/build/dist/Types/Telemetry/InventoryLiveness.js.map +1 -0
  747. package/build/dist/Types/Telemetry/TelemetryIngestSurface.js +85 -0
  748. package/build/dist/Types/Telemetry/TelemetryIngestSurface.js.map +1 -0
  749. package/build/dist/Types/Telemetry/TelemetryIngestionKeyPolicy.js +15 -0
  750. package/build/dist/Types/Telemetry/TelemetryIngestionKeyPolicy.js.map +1 -0
  751. package/build/dist/Types/Telemetry/TelemetryIngestionKeyType.js +30 -0
  752. package/build/dist/Types/Telemetry/TelemetryIngestionKeyType.js.map +1 -0
  753. package/build/dist/Types/WebsiteRequest.js +55 -3
  754. package/build/dist/Types/WebsiteRequest.js.map +1 -1
  755. package/build/dist/UI/Components/CSVFileUpload/CSVFileUpload.js +7 -0
  756. package/build/dist/UI/Components/CSVFileUpload/CSVFileUpload.js.map +1 -1
  757. package/build/dist/UI/Components/Charts/Area/AreaChart.js +13 -2
  758. package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
  759. package/build/dist/UI/Components/Charts/Bar/BarChart.js +6 -2
  760. package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
  761. package/build/dist/UI/Components/Charts/Line/LineChart.js +13 -2
  762. package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
  763. package/build/dist/UI/Components/Charts/Types/XAxis/XAxis.js.map +1 -1
  764. package/build/dist/UI/Components/Charts/Utils/DataPoint.js +15 -3
  765. package/build/dist/UI/Components/Charts/Utils/DataPoint.js.map +1 -1
  766. package/build/dist/UI/Components/Charts/Utils/TimeAnnotation.js +40 -9
  767. package/build/dist/UI/Components/Charts/Utils/TimeAnnotation.js.map +1 -1
  768. package/build/dist/UI/Components/Charts/Utils/XAxis.js +186 -0
  769. package/build/dist/UI/Components/Charts/Utils/XAxis.js.map +1 -1
  770. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js +89 -8
  771. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js.map +1 -1
  772. package/build/dist/UI/Components/CustomFields/MapFromCustomFieldInput.js +118 -0
  773. package/build/dist/UI/Components/CustomFields/MapFromCustomFieldInput.js.map +1 -0
  774. package/build/dist/UI/Components/Dashboard/DashboardVariableControl.js +137 -0
  775. package/build/dist/UI/Components/Dashboard/DashboardVariableControl.js.map +1 -0
  776. package/build/dist/UI/Components/Graphs/DayUptimeGraph.js +112 -7
  777. package/build/dist/UI/Components/Graphs/DayUptimeGraph.js.map +1 -1
  778. package/build/dist/UI/Components/Graphs/UptimeBarTooltip.js +5 -162
  779. package/build/dist/UI/Components/Graphs/UptimeBarTooltip.js.map +1 -1
  780. package/build/dist/UI/Components/Graphs/UptimeDaySummary.js +181 -0
  781. package/build/dist/UI/Components/Graphs/UptimeDaySummary.js.map +1 -0
  782. package/build/dist/UI/Components/Header/Header.js +2 -3
  783. package/build/dist/UI/Components/Header/Header.js.map +1 -1
  784. package/build/dist/UI/Components/Header/IconDropdown/IconDropdownItem.js +2 -1
  785. package/build/dist/UI/Components/Header/IconDropdown/IconDropdownItem.js.map +1 -1
  786. package/build/dist/UI/Components/Header/IconDropdown/IconDropdownMenu.js +7 -1
  787. package/build/dist/UI/Components/Header/IconDropdown/IconDropdownMenu.js.map +1 -1
  788. package/build/dist/UI/Components/Header/ProjectPicker/ProjectPicker.js +7 -1
  789. package/build/dist/UI/Components/Header/ProjectPicker/ProjectPicker.js.map +1 -1
  790. package/build/dist/UI/Components/HeaderAlert/NotificationBell/NotificationBellDropdown.js +7 -1
  791. package/build/dist/UI/Components/HeaderAlert/NotificationBell/NotificationBellDropdown.js.map +1 -1
  792. package/build/dist/UI/Components/Icon/Icon.js +5 -0
  793. package/build/dist/UI/Components/Icon/Icon.js.map +1 -1
  794. package/build/dist/UI/Components/JSONTable/JSONTable.js +2 -2
  795. package/build/dist/UI/Components/JSONTable/JSONTable.js.map +1 -1
  796. package/build/dist/UI/Components/KeyboardShortcut/KeyboardShortcutsModal.js +35 -0
  797. package/build/dist/UI/Components/KeyboardShortcut/KeyboardShortcutsModal.js.map +1 -0
  798. package/build/dist/UI/Components/Monitor/SeriesDebugCommandsViewer.js +34 -0
  799. package/build/dist/UI/Components/Monitor/SeriesDebugCommandsViewer.js.map +1 -0
  800. package/build/dist/UI/Components/Monitor/SeriesLabelsViewer.js +49 -0
  801. package/build/dist/UI/Components/Monitor/SeriesLabelsViewer.js.map +1 -0
  802. package/build/dist/UI/Components/MonitorGraphs/Uptime.js +1 -1
  803. package/build/dist/UI/Components/MonitorGraphs/Uptime.js.map +1 -1
  804. package/build/dist/UI/Components/MonitorGraphs/UptimeBarDayModal.js +32 -6
  805. package/build/dist/UI/Components/MonitorGraphs/UptimeBarDayModal.js.map +1 -1
  806. package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.js +38 -0
  807. package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.js.map +1 -1
  808. package/build/dist/UI/Components/StatusPage/ResourceGroupSection.js +22 -1
  809. package/build/dist/UI/Components/StatusPage/ResourceGroupSection.js.map +1 -1
  810. package/build/dist/UI/Components/Table/CellClassName.js +55 -0
  811. package/build/dist/UI/Components/Table/CellClassName.js.map +1 -0
  812. package/build/dist/UI/Components/Table/TableRow.js +16 -13
  813. package/build/dist/UI/Components/Table/TableRow.js.map +1 -1
  814. package/build/dist/UI/Components/Table/TableSkeletonRows.js +9 -8
  815. package/build/dist/UI/Components/Table/TableSkeletonRows.js.map +1 -1
  816. package/build/dist/UI/Components/Tooltip/Tooltip.js +25 -1
  817. package/build/dist/UI/Components/Tooltip/Tooltip.js.map +1 -1
  818. package/build/dist/UI/Components/Workflow/ComponentsModal.js +47 -30
  819. package/build/dist/UI/Components/Workflow/ComponentsModal.js.map +1 -1
  820. package/build/dist/UI/Components/Workflow/NodePlacement.js +37 -0
  821. package/build/dist/UI/Components/Workflow/NodePlacement.js.map +1 -0
  822. package/build/dist/UI/Components/Workflow/Workflow.js +35 -18
  823. package/build/dist/UI/Components/Workflow/Workflow.js.map +1 -1
  824. package/build/dist/UI/Config.js +10 -20
  825. package/build/dist/UI/Config.js.map +1 -1
  826. package/build/dist/UI/Utils/GlobalKeyboardShortcut.js +138 -0
  827. package/build/dist/UI/Utils/GlobalKeyboardShortcut.js.map +1 -0
  828. package/build/dist/UI/Utils/Telemetry/BrowserTelemetryConfig.js +14 -0
  829. package/build/dist/UI/Utils/Telemetry/BrowserTelemetryConfig.js.map +1 -0
  830. package/build/dist/UI/Utils/Telemetry/Telemetry.js +7 -5
  831. package/build/dist/UI/Utils/Telemetry/Telemetry.js.map +1 -1
  832. package/build/dist/Utils/API.js +67 -5
  833. package/build/dist/Utils/API.js.map +1 -1
  834. package/build/dist/Utils/Dashboard/Components/DashboardMonitorListComponent.js +9 -1
  835. package/build/dist/Utils/Dashboard/Components/DashboardMonitorListComponent.js.map +1 -1
  836. package/build/dist/Utils/Dashboard/LabelVariable.js +96 -0
  837. package/build/dist/Utils/Dashboard/LabelVariable.js.map +1 -0
  838. package/build/dist/Utils/Dashboard/VariableUrlState.js +19 -4
  839. package/build/dist/Utils/Dashboard/VariableUrlState.js.map +1 -1
  840. package/build/dist/Utils/HTTPResponseBodyReader.js +157 -0
  841. package/build/dist/Utils/HTTPResponseBodyReader.js.map +1 -0
  842. package/build/dist/Utils/MetricUnitUtil.js +26 -0
  843. package/build/dist/Utils/MetricUnitUtil.js.map +1 -1
  844. package/build/dist/Utils/Monitor/DeviceMacLearningUtil.js +112 -0
  845. package/build/dist/Utils/Monitor/DeviceMacLearningUtil.js.map +1 -0
  846. package/build/dist/Utils/Monitor/MetricValueFormatter.js +276 -0
  847. package/build/dist/Utils/Monitor/MetricValueFormatter.js.map +1 -0
  848. package/build/dist/Utils/Monitor/MonitorTemplateCustomFieldUtil.js +101 -0
  849. package/build/dist/Utils/Monitor/MonitorTemplateCustomFieldUtil.js.map +1 -0
  850. package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js +7 -2
  851. package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js.map +1 -1
  852. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +484 -0
  853. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
  854. package/build/dist/Utils/NetworkAutomation/AutoImportRunChain.js +59 -0
  855. package/build/dist/Utils/NetworkAutomation/AutoImportRunChain.js.map +1 -0
  856. package/build/dist/Utils/NetworkDiscovery/DiscoveryScanStatus.js +69 -0
  857. package/build/dist/Utils/NetworkDiscovery/DiscoveryScanStatus.js.map +1 -0
  858. package/build/dist/Utils/Rum/ChunkMath.js +76 -0
  859. package/build/dist/Utils/Rum/ChunkMath.js.map +1 -1
  860. package/build/dist/Utils/Rum/SessionReplayHealth.js +534 -0
  861. package/build/dist/Utils/Rum/SessionReplayHealth.js.map +1 -0
  862. package/build/dist/Utils/Rum/SessionReplayStringMap.js +165 -0
  863. package/build/dist/Utils/Rum/SessionReplayStringMap.js.map +1 -0
  864. package/build/dist/Utils/Schema/ModelSchema.js +1 -0
  865. package/build/dist/Utils/Schema/ModelSchema.js.map +1 -1
  866. package/build/dist/Utils/StatusPage/ResourceSearch.js +200 -0
  867. package/build/dist/Utils/StatusPage/ResourceSearch.js.map +1 -0
  868. package/build/dist/Utils/Telemetry/OriginAllowList.js +299 -0
  869. package/build/dist/Utils/Telemetry/OriginAllowList.js.map +1 -0
  870. package/build/dist/Utils/Uptime/DayUptimeGraphUtil.js +147 -0
  871. package/build/dist/Utils/Uptime/DayUptimeGraphUtil.js.map +1 -0
  872. package/build/dist/Utils/ValueFormatter.js +32 -1
  873. package/build/dist/Utils/ValueFormatter.js.map +1 -1
  874. package/jest.config.json +2 -0
  875. package/package.json +1 -1
  876. package/test-setup.sh +9 -9
  877. package/tsconfig.json +51 -11
@@ -6,8 +6,17 @@ import RumApplicationService from "../../../Server/Services/RumApplicationServic
6
6
  import RumSessionReplayViewService from "../../../Server/Services/RumSessionReplayViewService";
7
7
  import RumSessionService from "../../../Server/Services/RumSessionService";
8
8
  import RumSessionChunkService from "../../../Server/Services/RumSessionChunkService";
9
+ import ExceptionInstanceService from "../../../Server/Services/ExceptionInstanceService";
9
10
  import { Statement } from "../../../Server/Utils/AnalyticsDatabase/Statement";
10
11
  import SessionReplayIdentity from "../../../Server/Utils/SessionReplay/SessionReplayIdentity";
12
+ import SessionReplayReadService from "../../../Server/Utils/SessionReplay/SessionReplayReadService";
13
+ import NotFoundException from "../../../Types/Exception/NotFoundException";
14
+ import {
15
+ MAX_SESSION_REPLAY_CHUNKS_PER_READ,
16
+ SESSION_REPLAY_LIST_SEARCH_MAX_LENGTH,
17
+ SESSION_REPLAY_LIST_SEARCH_MAX_WINDOW_DAYS,
18
+ SESSION_REPLAY_MAX_SESSION_MS,
19
+ } from "../../../Types/Rum/SessionReplay";
11
20
  import RumApplication from "../../../Models/DatabaseModels/RumApplication";
12
21
  import RumSessionReplayView from "../../../Models/DatabaseModels/RumSessionReplayView";
13
22
  import Label from "../../../Models/DatabaseModels/Label";
@@ -30,7 +39,6 @@ import Permission, {
30
39
  } from "../../../Types/Permission";
31
40
  import PermissionScope from "../../../Types/Database/AccessControl/PermissionScope";
32
41
  import UserType from "../../../Types/UserType";
33
- import { MAX_SESSION_REPLAY_CHUNKS_PER_READ } from "../../../Types/Rum/SessionReplay";
34
42
  import {
35
43
  afterEach,
36
44
  beforeAll,
@@ -124,6 +132,7 @@ const FOR_EXCEPTION_ROUTE: string =
124
132
  "/telemetry/rum/session-replay/for-exception";
125
133
  const INGEST_STATUS_ROUTE: string =
126
134
  "/telemetry/rum/session-replay/ingest-status";
135
+ const VIEWS_ROUTE: string = "/telemetry/rum/session-replay/views";
127
136
 
128
137
  function findRoute(uri: string): RecordedRoute {
129
138
  const route: RecordedRoute | undefined = recordedRoutes.find(
@@ -414,6 +423,7 @@ describe("Session replay playback API", () => {
414
423
 
415
424
  let headerQuerySpy: jest.SpyInstance;
416
425
  let chunkQuerySpy: jest.SpyInstance;
426
+ let exceptionQuerySpy: jest.SpyInstance;
417
427
  let findOneBySpy: jest.SpyInstance;
418
428
  let findBySpy: jest.SpyInstance;
419
429
  let recordViewSpy: jest.SpyInstance;
@@ -450,6 +460,18 @@ describe("Session replay playback API", () => {
450
460
  .spyOn(RumSessionChunkService, "executeQuery")
451
461
  .mockResolvedValue(fakeResultSet([]) as never);
452
462
 
463
+ /*
464
+ * The exception -> replay lookup consults the exception instance
465
+ * table for live sessions before it reads headers. Empty by default;
466
+ * the for-exception suite overrides it where the side index matters.
467
+ */
468
+ exceptionQuerySpy = jest
469
+ .spyOn(ExceptionInstanceService, "executeQuery")
470
+ .mockResolvedValue(fakeResultSet([]) as never);
471
+
472
+ SessionReplayReadService.clearActivitySummaryCache();
473
+ SessionReplayReadService.setPublishedRecorderVersionProvider(null);
474
+
453
475
  findOneBySpy = jest.spyOn(RumApplicationService, "findOneBy");
454
476
  findBySpy = jest.spyOn(RumApplicationService, "findBy");
455
477
 
@@ -527,12 +549,13 @@ describe("Session replay playback API", () => {
527
549
  }
528
550
 
529
551
  describe("guard shape", () => {
530
- test("all six routes are registered and every one carries the three-middleware guard", () => {
552
+ test("all seven routes are registered and every one carries the three-middleware guard", () => {
531
553
  for (const uri of [
532
554
  LIST_ROUTE,
533
555
  MANIFEST_ROUTE,
534
556
  CHUNKS_ROUTE,
535
557
  HEARTBEAT_ROUTE,
558
+ VIEWS_ROUTE,
536
559
  FOR_EXCEPTION_ROUTE,
537
560
  INGEST_STATUS_ROUTE,
538
561
  ]) {
@@ -563,7 +586,12 @@ describe("Session replay playback API", () => {
563
586
  expect(listGuard).toBeDefined();
564
587
  expect(payloadGuard).not.toBe(listGuard);
565
588
 
566
- for (const uri of [MANIFEST_ROUTE, CHUNKS_ROUTE, HEARTBEAT_ROUTE]) {
589
+ for (const uri of [
590
+ MANIFEST_ROUTE,
591
+ CHUNKS_ROUTE,
592
+ HEARTBEAT_ROUTE,
593
+ VIEWS_ROUTE,
594
+ ]) {
567
595
  expect(findRoute(uri).handlers[2]).toBe(payloadGuard);
568
596
  }
569
597
 
@@ -608,7 +636,7 @@ describe("Session replay playback API", () => {
608
636
  );
609
637
  });
610
638
 
611
- test("both chunk queries filter on the application resolved from the header", async () => {
639
+ test("the chunk query filters on the application resolved from the header", async () => {
612
640
  const principal: {
613
641
  request: JSONObject;
614
642
  databaseProps: DatabaseCommonInteractionProps;
@@ -622,13 +650,11 @@ describe("Session replay playback API", () => {
622
650
  mockSessionHeader(applicationAId);
623
651
  mockApplication({ id: applicationAId, labelIds: [] });
624
652
 
625
- chunkQuerySpy
626
- .mockResolvedValueOnce(
627
- fakeResultSet([{ chunkIndex: 0, chunkStoredBytes: 3 }]) as never,
628
- )
629
- .mockResolvedValueOnce(
630
- fakeResultSet([{ chunkIndex: 0, payload: "[1]" }]) as never,
631
- );
653
+ chunkQuerySpy.mockResolvedValue(
654
+ fakeResultSet([
655
+ { chunkIndex: 0, servedPayload: "[1]", isServed: 1 },
656
+ ]) as never,
657
+ );
632
658
 
633
659
  await callRoute({
634
660
  uri: CHUNKS_ROUTE,
@@ -637,23 +663,85 @@ describe("Session replay playback API", () => {
637
663
  sessionId: "session-1",
638
664
  tabId: "tab-1",
639
665
  chunkIndexes: [0],
640
- /* A caller-supplied application id must change nothing. */
666
+ },
667
+ });
668
+
669
+ expect(chunkQuerySpy).toHaveBeenCalledTimes(1);
670
+
671
+ const statement: Statement = chunkQuerySpy.mock.calls[0]![0] as Statement;
672
+ expect(statement.query).toContain("rumApplicationId = ");
673
+ expect(Object.values(statement.query_params)).toContain(
674
+ applicationAId.toString(),
675
+ );
676
+ expect(Object.values(statement.query_params)).not.toContain(
677
+ applicationBId.toString(),
678
+ );
679
+ });
680
+
681
+ /*
682
+ * A caller-supplied rumApplicationId is a DISAMBIGUATOR: it narrows
683
+ * which header row is read, and the application THAT row names is
684
+ * what gets authorized. Naming an application the session was never
685
+ * recorded under finds no header, so it cannot widen access.
686
+ */
687
+ test("a caller-supplied rumApplicationId only narrows the header read and is authorized on its own merits", async () => {
688
+ const principal: {
689
+ request: JSONObject;
690
+ databaseProps: DatabaseCommonInteractionProps;
691
+ } = buildPrincipal({
692
+ projectId: projectId,
693
+ userId: userId,
694
+ permissions: [Permission.ReadRumSessionReplayPayload],
695
+ });
696
+
697
+ mockProps(principal.databaseProps);
698
+ /* No header exists under application B, whatever the body says. */
699
+ headerQuerySpy.mockResolvedValue(fakeResultSet([]) as never);
700
+ mockApplication({ id: applicationAId, labelIds: [] });
701
+
702
+ const result: CallResult = await callRoute({
703
+ uri: CHUNKS_ROUTE,
704
+ request: principal.request,
705
+ body: {
706
+ sessionId: "session-1",
707
+ tabId: "tab-1",
708
+ chunkIndexes: [0],
641
709
  rumApplicationId: applicationBId.toString(),
642
710
  },
643
711
  });
644
712
 
645
- expect(chunkQuerySpy).toHaveBeenCalledTimes(2);
713
+ const headerStatement: Statement = headerQuerySpy.mock
714
+ .calls[0]![0] as Statement;
715
+ expect(headerStatement.query).toContain("rumApplicationId = ");
716
+ expect(Object.values(headerStatement.query_params)).toContain(
717
+ applicationBId.toString(),
718
+ );
719
+
720
+ expect(result.thrownToNext).toBeInstanceOf(NotFoundException);
721
+ expect(chunkQuerySpy).not.toHaveBeenCalled();
722
+ });
646
723
 
647
- for (const call of chunkQuerySpy.mock.calls) {
648
- const statement: Statement = call[0] as Statement;
649
- expect(statement.query).toContain("rumApplicationId = ");
650
- expect(Object.values(statement.query_params)).toContain(
651
- applicationAId.toString(),
652
- );
653
- expect(Object.values(statement.query_params)).not.toContain(
654
- applicationBId.toString(),
655
- );
656
- }
724
+ test("a malformed disambiguator is a bad request", async () => {
725
+ const principal: {
726
+ request: JSONObject;
727
+ databaseProps: DatabaseCommonInteractionProps;
728
+ } = buildPrincipal({
729
+ projectId: projectId,
730
+ userId: userId,
731
+ permissions: [Permission.ReadRumSessionReplayPayload],
732
+ });
733
+
734
+ mockProps(principal.databaseProps);
735
+
736
+ const result: CallResult = await callRoute({
737
+ uri: MANIFEST_ROUTE,
738
+ request: principal.request,
739
+ body: { sessionId: "session-1", rumApplicationId: "nope" },
740
+ });
741
+
742
+ expect(result.thrownToNext).toBeInstanceOf(BadDataException);
743
+ expect(headerQuerySpy).not.toHaveBeenCalled();
744
+ expect(recordViewSpy).not.toHaveBeenCalled();
657
745
  });
658
746
 
659
747
  test("a sessionId that exists under two applications is refused rather than resolved to the newest", async () => {
@@ -1245,6 +1333,132 @@ describe("Session replay playback API", () => {
1245
1333
  expect(statement.query).not.toContain("aggIdentifiedUserKey =");
1246
1334
  });
1247
1335
 
1336
+ /*
1337
+ * Dropping the filter silently is the dangerous half of that gate. The
1338
+ * request "show me jane@example.com's sessions" then answers 200 with
1339
+ * every session in the application and the caller has no way to tell -
1340
+ * least of all when the application has no sessions at all, where even
1341
+ * inspecting the rows cannot reveal it. A support engineer opens the
1342
+ * wrong recording, and each opening writes an audit row against a real
1343
+ * end user. The Dashboard already reads this field and shows a notice.
1344
+ */
1345
+ test("a dropped identity filter is NAMED in the response, not silently ignored", async () => {
1346
+ const principal: {
1347
+ request: JSONObject;
1348
+ databaseProps: DatabaseCommonInteractionProps;
1349
+ } = buildPrincipal({
1350
+ projectId: projectId,
1351
+ userId: userId,
1352
+ permissions: [Permission.TelemetryAdmin],
1353
+ });
1354
+
1355
+ mockProps(principal.databaseProps);
1356
+ mockApplication({ id: applicationAId, labelIds: [] });
1357
+
1358
+ const result: CallResult = await callRoute({
1359
+ uri: LIST_ROUTE,
1360
+ request: principal.request,
1361
+ body: {
1362
+ rumApplicationId: applicationAId.toString(),
1363
+ filters: { identifiedUserRef: "jane@example.com" },
1364
+ },
1365
+ });
1366
+
1367
+ expect(result.jsonBody?.["ignoredFilters"]).toEqual([
1368
+ "identifiedUserRef",
1369
+ ]);
1370
+ });
1371
+
1372
+ test("a list that honoured every filter says so with an empty ignoredFilters", async () => {
1373
+ const principal: {
1374
+ request: JSONObject;
1375
+ databaseProps: DatabaseCommonInteractionProps;
1376
+ } = buildPrincipal({
1377
+ projectId: projectId,
1378
+ userId: userId,
1379
+ permissions: [Permission.ProjectOwner],
1380
+ });
1381
+
1382
+ mockProps(principal.databaseProps);
1383
+ mockApplication({ id: applicationAId, labelIds: [] });
1384
+
1385
+ const result: CallResult = await callRoute({
1386
+ uri: LIST_ROUTE,
1387
+ request: principal.request,
1388
+ body: {
1389
+ rumApplicationId: applicationAId.toString(),
1390
+ filters: { identifiedUserRef: "jane@example.com" },
1391
+ },
1392
+ });
1393
+
1394
+ /*
1395
+ * Always present, so a client never has to tell "nothing was ignored"
1396
+ * apart from "an older server that never said".
1397
+ */
1398
+ expect(result.jsonBody?.["ignoredFilters"]).toEqual([]);
1399
+ });
1400
+
1401
+ /*
1402
+ * Every array filter becomes an `IN (...)` inside a HAVING that
1403
+ * ClickHouse evaluates per group over the whole window, so an uncapped
1404
+ * array is the caller's length times the sessions in range - a cheap
1405
+ * denial of service for anyone holding the list permission.
1406
+ */
1407
+ test("filter arrays and free-text filters are capped before they reach the query", async () => {
1408
+ const principal: {
1409
+ request: JSONObject;
1410
+ databaseProps: DatabaseCommonInteractionProps;
1411
+ } = buildPrincipal({
1412
+ projectId: projectId,
1413
+ userId: userId,
1414
+ permissions: [Permission.ProjectOwner],
1415
+ });
1416
+
1417
+ mockProps(principal.databaseProps);
1418
+ mockApplication({ id: applicationAId, labelIds: [] });
1419
+
1420
+ const manyBrowsers: Array<string> = [];
1421
+
1422
+ for (let index: number = 0; index < 5000; index++) {
1423
+ manyBrowsers.push(`browser-${index}`);
1424
+ }
1425
+
1426
+ await callRoute({
1427
+ uri: LIST_ROUTE,
1428
+ request: principal.request,
1429
+ body: {
1430
+ rumApplicationId: applicationAId.toString(),
1431
+ filters: {
1432
+ browserNames: manyBrowsers,
1433
+ route: "r".repeat(10_000),
1434
+ },
1435
+ },
1436
+ });
1437
+
1438
+ const statement: Statement = headerQuerySpy.mock
1439
+ .calls[0]![0] as Statement;
1440
+
1441
+ const bound: Array<unknown> = Object.values(statement.query_params);
1442
+
1443
+ const browserArray: Array<string> | undefined = bound.find(
1444
+ (value: unknown): boolean => {
1445
+ return Array.isArray(value) && (value as Array<string>).length > 1;
1446
+ },
1447
+ ) as Array<string> | undefined;
1448
+
1449
+ expect(browserArray).toBeDefined();
1450
+ expect(browserArray!.length).toBeLessThanOrEqual(50);
1451
+
1452
+ const routeValue: string | undefined = bound.find(
1453
+ (value: unknown): boolean => {
1454
+ return typeof value === "string" && value.startsWith("rrr");
1455
+ },
1456
+ ) as string | undefined;
1457
+
1458
+ expect(routeValue).toBeDefined();
1459
+ expect(routeValue!.length).toBeLessThanOrEqual(256);
1460
+ });
1461
+
1248
1462
  /*
1249
1463
  * A filter the server cannot honour must be refused, not dropped. The
1250
1464
  * silent-drop shape returns the WHOLE project's sessions with a 200 and
@@ -1495,162 +1709,1143 @@ describe("Session replay playback API", () => {
1495
1709
  "(aggRageClickCount + aggDeadClickCount + aggErrorClickCount + aggRefreshRageCount) > 0",
1496
1710
  );
1497
1711
  });
1498
- });
1499
1712
 
1500
- describe("manifest", () => {
1501
- test("never names the payload column and writes the read audit row", async () => {
1713
+ function ownerPrincipal(): {
1714
+ request: JSONObject;
1715
+ databaseProps: DatabaseCommonInteractionProps;
1716
+ } {
1502
1717
  const principal: {
1503
1718
  request: JSONObject;
1504
1719
  databaseProps: DatabaseCommonInteractionProps;
1505
1720
  } = buildPrincipal({
1506
1721
  projectId: projectId,
1507
1722
  userId: userId,
1508
- permissions: [Permission.ReadRumSessionReplayPayload],
1723
+ permissions: [Permission.ProjectOwner],
1509
1724
  });
1510
1725
 
1511
1726
  mockProps(principal.databaseProps);
1512
- mockSessionHeader(applicationAId);
1513
1727
  mockApplication({ id: applicationAId, labelIds: [] });
1514
- const viewId: ObjectID = mockRecordedView();
1515
1728
 
1516
- chunkQuerySpy.mockResolvedValue(
1517
- fakeResultSet([
1518
- {
1519
- tabId: "tab-1",
1520
- chunkIndex: 0,
1521
- chunkStartOffsetMs: 0,
1522
- chunkEndOffsetMs: 15000,
1523
- eventCount: 100,
1524
- hasFullSnapshot: true,
1525
- chunkPayloadBytes: 1024,
1526
- errorCount: 2,
1527
- rageClickCount: 1,
1528
- deadClickCount: 4,
1529
- errorClickCount: 6,
1530
- refreshRageCount: 5,
1531
- routeCount: 3,
1532
- },
1533
- {
1534
- tabId: "tab-1",
1535
- chunkIndex: 3,
1536
- chunkStartOffsetMs: 45000,
1537
- chunkEndOffsetMs: 60000,
1538
- eventCount: 100,
1539
- hasFullSnapshot: false,
1540
- chunkPayloadBytes: 1024,
1541
- errorCount: 0,
1542
- rageClickCount: 0,
1543
- deadClickCount: 0,
1544
- errorClickCount: 0,
1545
- refreshRageCount: 0,
1546
- routeCount: 0,
1547
- },
1548
- ]) as never,
1549
- );
1729
+ return principal;
1730
+ }
1550
1731
 
1551
- const result: CallResult = await callRoute({
1552
- uri: MANIFEST_ROUTE,
1732
+ test("the new quick filters, url prefix and tags reach the query as HAVING predicates", async () => {
1733
+ const principal: {
1734
+ request: JSONObject;
1735
+ databaseProps: DatabaseCommonInteractionProps;
1736
+ } = ownerPrincipal();
1737
+
1738
+ await callRoute({
1739
+ uri: LIST_ROUTE,
1553
1740
  request: principal.request,
1554
- body: { sessionId: "session-1", accessReason: "incident triage" },
1741
+ body: {
1742
+ rumApplicationId: applicationAId.toString(),
1743
+ filters: {
1744
+ hasIdentifiedUser: true,
1745
+ isPlayable: true,
1746
+ hasTraces: true,
1747
+ urlPrefix: "/checkout",
1748
+ tags: { build: "1.2.3", ignored: 4 },
1749
+ },
1750
+ },
1555
1751
  });
1556
1752
 
1557
- const statement: Statement = chunkQuerySpy.mock.calls[0]![0] as Statement;
1753
+ const statement: Statement = headerQuerySpy.mock
1754
+ .calls[0]![0] as Statement;
1755
+ const having: string = statement.query.substring(
1756
+ statement.query.indexOf("HAVING 1 = 1"),
1757
+ );
1558
1758
 
1559
- expect(statement.query).not.toContain("payload,");
1560
- expect(statement.query).not.toMatch(/\bpayload\b(?!Bytes)/);
1561
- expect(statement.query).toContain("LIMIT 1 BY tabId, chunkIndex");
1759
+ expect(having).toContain("aggIdentifiedUserKey != ''");
1760
+ expect(having).toContain("aggIsFinalized = 0 OR aggChunkCount > 0");
1761
+ expect(having).toContain("aggTraceCount > 0");
1762
+ expect(having).toContain("arrayExists(r -> startsWith(r, ");
1763
+ expect(having).toContain("mapContains(aggTags, ");
1764
+
1765
+ const bound: Array<unknown> = Object.values(statement.query_params);
1766
+ expect(bound).toContain("/checkout");
1767
+ expect(bound).toContain("build");
1768
+ expect(bound).toContain("1.2.3");
1769
+ /* A non-string tag value is not a filter. */
1770
+ expect(bound).not.toContain("ignored");
1771
+ });
1562
1772
 
1563
- /*
1564
- * The signal counters must be projected: the player's error,
1565
- * frustration and route timeline lanes and the "next error" jump are
1566
- * fed exclusively by these per-chunk columns. Dropping one from the
1567
- * SELECT silently blanks a lane — it parses as 0 client-side.
1568
- */
1569
- for (const counterColumn of [
1570
- "errorCount",
1571
- "rageClickCount",
1572
- "deadClickCount",
1573
- "errorClickCount",
1574
- "refreshRageCount",
1575
- "routeCount",
1576
- ]) {
1577
- expect(statement.query).toContain(counterColumn);
1578
- }
1773
+ test("search is bound into the query, and names the label only for an identity-permitted caller", async () => {
1774
+ const owner: {
1775
+ request: JSONObject;
1776
+ databaseProps: DatabaseCommonInteractionProps;
1777
+ } = ownerPrincipal();
1579
1778
 
1580
- expect(recordViewSpy).toHaveBeenCalledTimes(1);
1581
- const auditArgs: JSONObject = recordViewSpy.mock
1779
+ await callRoute({
1780
+ uri: LIST_ROUTE,
1781
+ request: owner.request,
1782
+ body: {
1783
+ rumApplicationId: applicationAId.toString(),
1784
+ startTime: "2026-08-01T00:00:00.000Z",
1785
+ endTime: "2026-08-08T00:00:00.000Z",
1786
+ filters: { search: "jane" },
1787
+ },
1788
+ });
1789
+
1790
+ const permitted: Statement = headerQuerySpy.mock
1791
+ .calls[0]![0] as Statement;
1792
+ expect(permitted.query).toContain(
1793
+ "positionCaseInsensitiveUTF8(aggIdentifiedUserLabel, ",
1794
+ );
1795
+ expect(permitted.query).not.toContain("'jane'");
1796
+ expect(Object.values(permitted.query_params)).toContain("jane");
1797
+
1798
+ jest.clearAllMocks();
1799
+
1800
+ const telemetryAdmin: {
1801
+ request: JSONObject;
1802
+ databaseProps: DatabaseCommonInteractionProps;
1803
+ } = buildPrincipal({
1804
+ projectId: projectId,
1805
+ userId: userId,
1806
+ permissions: [Permission.TelemetryAdmin],
1807
+ });
1808
+
1809
+ mockProps(telemetryAdmin.databaseProps);
1810
+ mockApplication({ id: applicationAId, labelIds: [] });
1811
+
1812
+ await callRoute({
1813
+ uri: LIST_ROUTE,
1814
+ request: telemetryAdmin.request,
1815
+ body: {
1816
+ rumApplicationId: applicationAId.toString(),
1817
+ startTime: "2026-08-01T00:00:00.000Z",
1818
+ endTime: "2026-08-08T00:00:00.000Z",
1819
+ filters: { search: "jane" },
1820
+ },
1821
+ });
1822
+
1823
+ const gated: Statement = headerQuerySpy.mock.calls[0]![0] as Statement;
1824
+ expect(gated.query).toContain("startsWith(sessionId, ");
1825
+ expect(gated.query).not.toContain("identifiedUserLabel");
1826
+ });
1827
+
1828
+ test("a search longer than the cap is refused before any query", async () => {
1829
+ const principal: {
1830
+ request: JSONObject;
1831
+ databaseProps: DatabaseCommonInteractionProps;
1832
+ } = ownerPrincipal();
1833
+
1834
+ const result: CallResult = await callRoute({
1835
+ uri: LIST_ROUTE,
1836
+ request: principal.request,
1837
+ body: {
1838
+ rumApplicationId: applicationAId.toString(),
1839
+ filters: {
1840
+ search: "x".repeat(SESSION_REPLAY_LIST_SEARCH_MAX_LENGTH + 1),
1841
+ },
1842
+ },
1843
+ });
1844
+
1845
+ expect(result.deniedWith).toBeInstanceOf(BadDataException);
1846
+ expect(result.deniedWith?.message).toContain(
1847
+ `${SESSION_REPLAY_LIST_SEARCH_MAX_LENGTH}`,
1848
+ );
1849
+ expect(headerQuerySpy).not.toHaveBeenCalled();
1850
+ });
1851
+
1852
+ test("a search over a window wider than the cap says to narrow the range, distinctly", async () => {
1853
+ const principal: {
1854
+ request: JSONObject;
1855
+ databaseProps: DatabaseCommonInteractionProps;
1856
+ } = ownerPrincipal();
1857
+
1858
+ const endTime: Date = new Date("2026-08-08T00:00:00.000Z");
1859
+ const startTime: Date = new Date(
1860
+ endTime.getTime() -
1861
+ (SESSION_REPLAY_LIST_SEARCH_MAX_WINDOW_DAYS + 1) *
1862
+ 24 *
1863
+ 60 *
1864
+ 60 *
1865
+ 1000,
1866
+ );
1867
+
1868
+ const result: CallResult = await callRoute({
1869
+ uri: LIST_ROUTE,
1870
+ request: principal.request,
1871
+ body: {
1872
+ rumApplicationId: applicationAId.toString(),
1873
+ startTime: startTime.toISOString(),
1874
+ endTime: endTime.toISOString(),
1875
+ filters: { search: "acme" },
1876
+ },
1877
+ });
1878
+
1879
+ expect(result.deniedWith).toBeInstanceOf(BadDataException);
1880
+ expect(result.deniedWith?.message).toMatch(/narrow the range/i);
1881
+ expect(result.deniedWith?.message).toContain(
1882
+ `${SESSION_REPLAY_LIST_SEARCH_MAX_WINDOW_DAYS} days`,
1883
+ );
1884
+ expect(headerQuerySpy).not.toHaveBeenCalled();
1885
+
1886
+ /* The same window WITHOUT a search is fine. */
1887
+ jest.clearAllMocks();
1888
+ mockProps(principal.databaseProps);
1889
+ mockApplication({ id: applicationAId, labelIds: [] });
1890
+
1891
+ const unsearched: CallResult = await callRoute({
1892
+ uri: LIST_ROUTE,
1893
+ request: principal.request,
1894
+ body: {
1895
+ rumApplicationId: applicationAId.toString(),
1896
+ startTime: startTime.toISOString(),
1897
+ endTime: endTime.toISOString(),
1898
+ },
1899
+ });
1900
+
1901
+ expect(unsearched.deniedWith).toBeUndefined();
1902
+ expect(headerQuerySpy).toHaveBeenCalledTimes(1);
1903
+ });
1904
+
1905
+ test("an unknown sortBy is a bad request", async () => {
1906
+ const principal: {
1907
+ request: JSONObject;
1908
+ databaseProps: DatabaseCommonInteractionProps;
1909
+ } = ownerPrincipal();
1910
+
1911
+ const result: CallResult = await callRoute({
1912
+ uri: LIST_ROUTE,
1913
+ request: principal.request,
1914
+ body: {
1915
+ rumApplicationId: applicationAId.toString(),
1916
+ sortBy: "payloadBytes",
1917
+ },
1918
+ });
1919
+
1920
+ expect(result.deniedWith).toBeInstanceOf(BadDataException);
1921
+ expect(headerQuerySpy).not.toHaveBeenCalled();
1922
+ });
1923
+
1924
+ test("sortBy orders the query and the next cursor carries the sort key", async () => {
1925
+ const principal: {
1926
+ request: JSONObject;
1927
+ databaseProps: DatabaseCommonInteractionProps;
1928
+ } = ownerPrincipal();
1929
+
1930
+ headerQuerySpy.mockResolvedValue(
1931
+ fakeResultSet([
1932
+ { sessionId: "a", aggErrorCount: 9, aggStartTime: 3 },
1933
+ { sessionId: "b", aggErrorCount: 4, aggStartTime: 2 },
1934
+ ]) as never,
1935
+ );
1936
+
1937
+ const result: CallResult = await callRoute({
1938
+ uri: LIST_ROUTE,
1939
+ request: principal.request,
1940
+ body: {
1941
+ rumApplicationId: applicationAId.toString(),
1942
+ sortBy: "errorCount",
1943
+ limit: 1,
1944
+ },
1945
+ });
1946
+
1947
+ const statement: Statement = headerQuerySpy.mock
1948
+ .calls[0]![0] as Statement;
1949
+ expect(statement.query).toContain(
1950
+ "ORDER BY aggErrorCount DESC, sessionId DESC",
1951
+ );
1952
+
1953
+ expect((result.jsonBody as JSONObject)["nextCursor"]).toEqual({
1954
+ sortBy: "errorCount",
1955
+ sortValue: 9,
1956
+ sessionId: "a",
1957
+ });
1958
+ });
1959
+
1960
+ test("the newest-first list still emits and accepts the legacy cursor shape", async () => {
1961
+ const principal: {
1962
+ request: JSONObject;
1963
+ databaseProps: DatabaseCommonInteractionProps;
1964
+ } = ownerPrincipal();
1965
+
1966
+ headerQuerySpy.mockResolvedValue(
1967
+ fakeResultSet([
1968
+ { sessionId: "a", aggStartTime: 1700000002000 },
1969
+ { sessionId: "b", aggStartTime: 1700000001000 },
1970
+ ]) as never,
1971
+ );
1972
+
1973
+ const firstPage: CallResult = await callRoute({
1974
+ uri: LIST_ROUTE,
1975
+ request: principal.request,
1976
+ body: { rumApplicationId: applicationAId.toString(), limit: 1 },
1977
+ });
1978
+
1979
+ expect((firstPage.jsonBody as JSONObject)["nextCursor"]).toEqual({
1980
+ startTimeUnixMs: 1700000002000,
1981
+ sessionId: "a",
1982
+ });
1983
+
1984
+ jest.clearAllMocks();
1985
+ mockProps(principal.databaseProps);
1986
+ mockApplication({ id: applicationAId, labelIds: [] });
1987
+
1988
+ await callRoute({
1989
+ uri: LIST_ROUTE,
1990
+ request: principal.request,
1991
+ body: {
1992
+ rumApplicationId: applicationAId.toString(),
1993
+ limit: 1,
1994
+ cursor: { startTimeUnixMs: 1700000002000, sessionId: "a" },
1995
+ },
1996
+ });
1997
+
1998
+ const statement: Statement = headerQuerySpy.mock
1999
+ .calls[0]![0] as Statement;
2000
+ expect(statement.query).toMatch(
2001
+ /AND \(aggStartTime < \{p\d+:Double\} OR \(aggStartTime = \{p\d+:Double\} AND sessionId < \{p\d+:String\}\)\)/,
2002
+ );
2003
+ expect(Object.values(statement.query_params)).toContain("a");
2004
+ });
2005
+
2006
+ test("a cursor from another ordering, or a malformed one, is refused", async () => {
2007
+ const principal: {
2008
+ request: JSONObject;
2009
+ databaseProps: DatabaseCommonInteractionProps;
2010
+ } = ownerPrincipal();
2011
+
2012
+ const mismatched: CallResult = await callRoute({
2013
+ uri: LIST_ROUTE,
2014
+ request: principal.request,
2015
+ body: {
2016
+ rumApplicationId: applicationAId.toString(),
2017
+ sortBy: "durationMs",
2018
+ cursor: { sortBy: "errorCount", sortValue: 3, sessionId: "a" },
2019
+ },
2020
+ });
2021
+
2022
+ expect(mismatched.thrownToNext).toBeInstanceOf(BadDataException);
2023
+
2024
+ jest.clearAllMocks();
2025
+ mockProps(principal.databaseProps);
2026
+ mockApplication({ id: applicationAId, labelIds: [] });
2027
+
2028
+ const malformed: CallResult = await callRoute({
2029
+ uri: LIST_ROUTE,
2030
+ request: principal.request,
2031
+ body: {
2032
+ rumApplicationId: applicationAId.toString(),
2033
+ cursor: { nonsense: true },
2034
+ },
2035
+ });
2036
+
2037
+ expect(malformed.deniedWith).toBeInstanceOf(BadDataException);
2038
+ expect(headerQuerySpy).not.toHaveBeenCalled();
2039
+ });
2040
+
2041
+ /*
2042
+ * parseSessionReplayListCursor only checks Number.isFinite, so 1e300
2043
+ * passes it - and for the startTime ordering it becomes
2044
+ * `new Date(1e300)`, an Invalid Date that renders as NaN text and is
2045
+ * rejected by the ClickHouse driver. A crafted or corrupted cursor
2046
+ * answered 500 instead of the 400 the handler promises.
2047
+ */
2048
+ test("a cursor sortValue that cannot be bound is a 400, never a driver error", async () => {
2049
+ const principal: {
2050
+ request: JSONObject;
2051
+ databaseProps: DatabaseCommonInteractionProps;
2052
+ } = ownerPrincipal();
2053
+
2054
+ const absurd: CallResult = await callRoute({
2055
+ uri: LIST_ROUTE,
2056
+ request: principal.request,
2057
+ body: {
2058
+ rumApplicationId: applicationAId.toString(),
2059
+ cursor: { startTimeUnixMs: 1e300, sessionId: "a" },
2060
+ },
2061
+ });
2062
+
2063
+ expect(absurd.deniedWith).toBeInstanceOf(BadDataException);
2064
+ expect(headerQuerySpy).not.toHaveBeenCalled();
2065
+
2066
+ jest.clearAllMocks();
2067
+ mockProps(principal.databaseProps);
2068
+ mockApplication({ id: applicationAId, labelIds: [] });
2069
+
2070
+ /* A count cannot be negative, so no page begins there either. */
2071
+ const negative: CallResult = await callRoute({
2072
+ uri: LIST_ROUTE,
2073
+ request: principal.request,
2074
+ body: {
2075
+ rumApplicationId: applicationAId.toString(),
2076
+ sortBy: "errorCount",
2077
+ cursor: { sortBy: "errorCount", sortValue: -1, sessionId: "a" },
2078
+ },
2079
+ });
2080
+
2081
+ expect(negative.deniedWith).toBeInstanceOf(BadDataException);
2082
+ expect(headerQuerySpy).not.toHaveBeenCalled();
2083
+ });
2084
+
2085
+ test.each([2.5, 0, -1, "20"])(
2086
+ "limit %p is a bad request rather than a ClickHouse error",
2087
+ async (limit: unknown) => {
2088
+ const principal: {
2089
+ request: JSONObject;
2090
+ databaseProps: DatabaseCommonInteractionProps;
2091
+ } = ownerPrincipal();
2092
+
2093
+ const result: CallResult = await callRoute({
2094
+ uri: LIST_ROUTE,
2095
+ request: principal.request,
2096
+ body: {
2097
+ rumApplicationId: applicationAId.toString(),
2098
+ limit: limit as number,
2099
+ },
2100
+ });
2101
+
2102
+ expect(result.thrownToNext).toBeInstanceOf(BadDataException);
2103
+ expect((result.thrownToNext as Exception).message).toContain("limit");
2104
+ expect(headerQuerySpy).not.toHaveBeenCalled();
2105
+ },
2106
+ );
2107
+
2108
+ test("an unparseable startTime, or a window that ends before it starts, is a bad request", async () => {
2109
+ const principal: {
2110
+ request: JSONObject;
2111
+ databaseProps: DatabaseCommonInteractionProps;
2112
+ } = ownerPrincipal();
2113
+
2114
+ const garbage: CallResult = await callRoute({
2115
+ uri: LIST_ROUTE,
2116
+ request: principal.request,
2117
+ body: {
2118
+ rumApplicationId: applicationAId.toString(),
2119
+ startTime: "yesterday-ish",
2120
+ },
2121
+ });
2122
+
2123
+ expect(garbage.thrownToNext).toBeInstanceOf(BadDataException);
2124
+ expect(headerQuerySpy).not.toHaveBeenCalled();
2125
+
2126
+ jest.clearAllMocks();
2127
+ mockProps(principal.databaseProps);
2128
+ mockApplication({ id: applicationAId, labelIds: [] });
2129
+
2130
+ const inverted: CallResult = await callRoute({
2131
+ uri: LIST_ROUTE,
2132
+ request: principal.request,
2133
+ body: {
2134
+ rumApplicationId: applicationAId.toString(),
2135
+ startTime: "2026-08-08T00:00:00.000Z",
2136
+ endTime: "2026-08-01T00:00:00.000Z",
2137
+ },
2138
+ });
2139
+
2140
+ expect(inverted.deniedWith).toBeInstanceOf(BadDataException);
2141
+ expect(inverted.deniedWith?.message).toContain("startTime");
2142
+ expect(headerQuerySpy).not.toHaveBeenCalled();
2143
+ });
2144
+
2145
+ test("projects the new list columns, with traits only for an identity-permitted caller", async () => {
2146
+ const principal: {
2147
+ request: JSONObject;
2148
+ databaseProps: DatabaseCommonInteractionProps;
2149
+ } = ownerPrincipal();
2150
+
2151
+ headerQuerySpy.mockResolvedValue(
2152
+ fakeResultSet([
2153
+ {
2154
+ sessionId: "a",
2155
+ applicationId: applicationAId.toString(),
2156
+ aggStartTime: 1700000000000,
2157
+ aggEndTime: 1700000090000,
2158
+ aggRoutes: ["/a", "/b"],
2159
+ aggTraceCount: 2,
2160
+ aggExceptionGroupCount: 1,
2161
+ aggClickCount: 12,
2162
+ aggActiveMs: 50000,
2163
+ aggFirstErrorOffsetMs: 3000,
2164
+ aggExpiresAt: 1700604800000,
2165
+ aggTags: { env: "prod" },
2166
+ aggIdentifiedUserLabel: "jane@example.com",
2167
+ aggIdentifiedUserTraits: { plan: "pro" },
2168
+ },
2169
+ ]) as never,
2170
+ );
2171
+
2172
+ const result: CallResult = await callRoute({
2173
+ uri: LIST_ROUTE,
2174
+ request: principal.request,
2175
+ body: { rumApplicationId: applicationAId.toString() },
2176
+ });
2177
+
2178
+ const sessions: Array<JSONObject> = (result.jsonBody as JSONObject)[
2179
+ "sessions"
2180
+ ] as unknown as Array<JSONObject>;
2181
+ const row: JSONObject = sessions[0]!;
2182
+
2183
+ expect(row["routes"]).toEqual(["/a", "/b"]);
2184
+ expect(row["traceCount"]).toBe(2);
2185
+ expect(row["exceptionGroupCount"]).toBe(1);
2186
+ expect(row["clickCount"]).toBe(12);
2187
+ expect(row["activeMs"]).toBe(50000);
2188
+ expect(row["firstErrorOffsetMs"]).toBe(3000);
2189
+ expect(row["expiresAtUnixMs"]).toBe(1700604800000);
2190
+ expect(row["tags"]).toEqual({ env: "prod" });
2191
+ expect(row["startTimeUnixMs"]).toBe(1700000000000);
2192
+ expect(row["endTimeUnixMs"]).toBe(1700000090000);
2193
+ expect(row["identifiedUserLabel"]).toBe("jane@example.com");
2194
+ expect(row["identifiedUserTraits"]).toEqual({ plan: "pro" });
2195
+ });
2196
+ });
2197
+
2198
+ describe("manifest", () => {
2199
+ test("never names the payload column and writes the read audit row", async () => {
2200
+ const principal: {
2201
+ request: JSONObject;
2202
+ databaseProps: DatabaseCommonInteractionProps;
2203
+ } = buildPrincipal({
2204
+ projectId: projectId,
2205
+ userId: userId,
2206
+ permissions: [Permission.ReadRumSessionReplayPayload],
2207
+ });
2208
+
2209
+ mockProps(principal.databaseProps);
2210
+ mockSessionHeader(applicationAId);
2211
+ mockApplication({ id: applicationAId, labelIds: [] });
2212
+ const viewId: ObjectID = mockRecordedView();
2213
+
2214
+ chunkQuerySpy.mockResolvedValue(
2215
+ fakeResultSet([
2216
+ {
2217
+ tabId: "tab-1",
2218
+ chunkIndex: 0,
2219
+ chunkStartOffsetMs: 0,
2220
+ chunkEndOffsetMs: 15000,
2221
+ eventCount: 100,
2222
+ hasFullSnapshot: true,
2223
+ chunkPayloadBytes: 1024,
2224
+ errorCount: 2,
2225
+ rageClickCount: 1,
2226
+ deadClickCount: 4,
2227
+ errorClickCount: 6,
2228
+ refreshRageCount: 5,
2229
+ routeCount: 3,
2230
+ },
2231
+ {
2232
+ tabId: "tab-1",
2233
+ chunkIndex: 3,
2234
+ chunkStartOffsetMs: 45000,
2235
+ chunkEndOffsetMs: 60000,
2236
+ eventCount: 100,
2237
+ hasFullSnapshot: false,
2238
+ chunkPayloadBytes: 1024,
2239
+ errorCount: 0,
2240
+ rageClickCount: 0,
2241
+ deadClickCount: 0,
2242
+ errorClickCount: 0,
2243
+ refreshRageCount: 0,
2244
+ routeCount: 0,
2245
+ },
2246
+ ]) as never,
2247
+ );
2248
+
2249
+ const result: CallResult = await callRoute({
2250
+ uri: MANIFEST_ROUTE,
2251
+ request: principal.request,
2252
+ body: { sessionId: "session-1", accessReason: "incident triage" },
2253
+ });
2254
+
2255
+ const statement: Statement = chunkQuerySpy.mock.calls[0]![0] as Statement;
2256
+
2257
+ expect(statement.query).not.toContain("payload,");
2258
+ expect(statement.query).not.toMatch(/\bpayload\b(?!Bytes)/);
2259
+ expect(statement.query).toContain("LIMIT 1 BY tabId, chunkIndex");
2260
+
2261
+ /*
2262
+ * The signal counters must be projected: the player's error,
2263
+ * frustration and route timeline lanes and the "next error" jump are
2264
+ * fed exclusively by these per-chunk columns. Dropping one from the
2265
+ * SELECT silently blanks a lane — it parses as 0 client-side.
2266
+ */
2267
+ for (const counterColumn of [
2268
+ "errorCount",
2269
+ "rageClickCount",
2270
+ "deadClickCount",
2271
+ "errorClickCount",
2272
+ "refreshRageCount",
2273
+ "routeCount",
2274
+ ]) {
2275
+ expect(statement.query).toContain(counterColumn);
2276
+ }
2277
+
2278
+ expect(recordViewSpy).toHaveBeenCalledTimes(1);
2279
+ const auditArgs: JSONObject = recordViewSpy.mock
1582
2280
  .calls[0]![0] as JSONObject;
1583
2281
  expect((auditArgs["projectId"] as ObjectID).toString()).toBe(
1584
2282
  projectId.toString(),
1585
2283
  );
1586
- expect((auditArgs["rumApplicationId"] as ObjectID).toString()).toBe(
1587
- applicationAId.toString(),
2284
+ expect((auditArgs["rumApplicationId"] as ObjectID).toString()).toBe(
2285
+ applicationAId.toString(),
2286
+ );
2287
+ expect(auditArgs["sessionId"]).toBe("session-1");
2288
+ expect(auditArgs["accessReason"]).toBe("incident triage");
2289
+ expect(auditArgs["ipAddress"]).toBe("203.0.113.7");
2290
+
2291
+ const body: JSONObject = result.jsonBody as JSONObject;
2292
+ expect(body["viewId"]).toBe(viewId.toString());
2293
+
2294
+ const tabs: Array<JSONObject> = body[
2295
+ "tabs"
2296
+ ] as unknown as Array<JSONObject>;
2297
+ expect(tabs).toHaveLength(1);
2298
+ expect(tabs[0]!["chunkIndexes"]).toEqual([0, 3]);
2299
+ expect(tabs[0]!["fullSnapshotChunkIndexes"]).toEqual([0]);
2300
+
2301
+ /*
2302
+ * Every counter gets a DISTINCT nonzero value, asserted individually:
2303
+ * the client parses missing keys to 0, so a counter dropped from the
2304
+ * SELECT (or two counters cross-wired in the row mapping) fails
2305
+ * loudly here instead of silently blanking a timeline lane.
2306
+ */
2307
+ const manifestChunks: Array<JSONObject> = tabs[0]![
2308
+ "chunks"
2309
+ ] as unknown as Array<JSONObject>;
2310
+ expect(manifestChunks[0]!["errorCount"]).toBe(2);
2311
+ expect(manifestChunks[0]!["rageClickCount"]).toBe(1);
2312
+ expect(manifestChunks[0]!["deadClickCount"]).toBe(4);
2313
+ expect(manifestChunks[0]!["errorClickCount"]).toBe(6);
2314
+ expect(manifestChunks[0]!["refreshRageCount"]).toBe(5);
2315
+ expect(manifestChunks[0]!["routeCount"]).toBe(3);
2316
+ expect(manifestChunks[1]!["errorCount"]).toBe(0);
2317
+
2318
+ /*
2319
+ * The hole between chunk 0 and chunk 3 must be reported. Playback
2320
+ * that silently crosses a gap renders a DOM the user never saw.
2321
+ */
2322
+ const gaps: Array<JSONObject> = tabs[0]![
2323
+ "gaps"
2324
+ ] as unknown as Array<JSONObject>;
2325
+ expect(gaps).toHaveLength(1);
2326
+ expect(gaps[0]!["fromIndex"]).toBe(0);
2327
+ expect(gaps[0]!["toIndex"]).toBe(3);
2328
+ expect(gaps[0]!["missingMs"]).toBe(30000);
2329
+ });
2330
+
2331
+ test("a session that does not exist in this tenant is refused before any audit row", async () => {
2332
+ const principal: {
2333
+ request: JSONObject;
2334
+ databaseProps: DatabaseCommonInteractionProps;
2335
+ } = buildPrincipal({
2336
+ projectId: projectId,
2337
+ userId: userId,
2338
+ permissions: [Permission.ProjectOwner],
2339
+ });
2340
+
2341
+ mockProps(principal.databaseProps);
2342
+ headerQuerySpy.mockResolvedValue(fakeResultSet([]) as never);
2343
+
2344
+ const result: CallResult = await callRoute({
2345
+ uri: MANIFEST_ROUTE,
2346
+ request: principal.request,
2347
+ body: { sessionId: "does-not-exist" },
2348
+ });
2349
+
2350
+ expect(result.thrownToNext).toBeInstanceOf(NotFoundException);
2351
+ expect((result.thrownToNext as Exception).message).toMatch(/^not-found:/);
2352
+ expect(recordViewSpy).not.toHaveBeenCalled();
2353
+ });
2354
+
2355
+ /*
2356
+ * A link from an incident a week later must say "expired on <date>",
2357
+ * not "not found": the two need different actions from the reader.
2358
+ * The retention-filtered header read finds nothing, so a second read
2359
+ * without the filter answers when the row aged out - and only dates
2360
+ * and the application id, never a row's content.
2361
+ */
2362
+ test("an expired session is a distinguishable 404 that names the expiry", async () => {
2363
+ const principal: {
2364
+ request: JSONObject;
2365
+ databaseProps: DatabaseCommonInteractionProps;
2366
+ } = buildPrincipal({
2367
+ projectId: projectId,
2368
+ userId: userId,
2369
+ permissions: [Permission.ProjectOwner],
2370
+ });
2371
+
2372
+ mockProps(principal.databaseProps);
2373
+ headerQuerySpy
2374
+ .mockResolvedValueOnce(fakeResultSet([]) as never)
2375
+ .mockResolvedValueOnce(
2376
+ fakeResultSet([
2377
+ {
2378
+ applicationId: applicationAId.toString(),
2379
+ expiresAtUnixMs: Date.UTC(2026, 7, 8),
2380
+ startTimeUnixMs: Date.UTC(2026, 7, 1),
2381
+ },
2382
+ ]) as never,
2383
+ );
2384
+
2385
+ const result: CallResult = await callRoute({
2386
+ uri: MANIFEST_ROUTE,
2387
+ request: principal.request,
2388
+ body: { sessionId: "session-old" },
2389
+ });
2390
+
2391
+ expect(result.thrownToNext).toBeInstanceOf(NotFoundException);
2392
+ const message: string = (result.thrownToNext as Exception).message;
2393
+ expect(message).toMatch(/^expired:/);
2394
+ expect(message).toContain("2026-08-08");
2395
+ expect(message).toContain("7-day retention");
2396
+
2397
+ const expiryStatement: Statement = headerQuerySpy.mock
2398
+ .calls[1]![0] as Statement;
2399
+ expect(expiryStatement.query).not.toContain("retentionDate >= now()");
2400
+ expect(recordViewSpy).not.toHaveBeenCalled();
2401
+ });
2402
+
2403
+ /*
2404
+ * The expiry answer discloses that a recording existed, when it
2405
+ * started, and what retention the owning application runs - and it is
2406
+ * produced on the path where there is no header left to authorize
2407
+ * against, so it used to be answered BEFORE any access check. A
2408
+ * label-scoped reviewer could therefore probe session ids and learn all
2409
+ * three for applications outside their scope: exactly the existence
2410
+ * probing assertSessionReplayApplicationAccess refuses "the same way"
2411
+ * for a session that does exist.
2412
+ */
2413
+ test("the expiry answer is withheld from a caller outside the owning application's scope", async () => {
2414
+ const principal: {
2415
+ request: JSONObject;
2416
+ databaseProps: DatabaseCommonInteractionProps;
2417
+ } = buildPrincipal({
2418
+ projectId: projectId,
2419
+ userId: userId,
2420
+ permissions: [Permission.ReadRumSessionReplayPayload],
2421
+ /* Scoped to application B's label; the expired recording is A's. */
2422
+ labelIds: [labelBId],
2423
+ });
2424
+
2425
+ mockProps(principal.databaseProps);
2426
+ mockApplication({ id: applicationAId, labelIds: [labelAId] });
2427
+
2428
+ headerQuerySpy
2429
+ .mockResolvedValueOnce(fakeResultSet([]) as never)
2430
+ .mockResolvedValueOnce(
2431
+ fakeResultSet([
2432
+ {
2433
+ applicationId: applicationAId.toString(),
2434
+ expiresAtUnixMs: Date.UTC(2026, 7, 8),
2435
+ startTimeUnixMs: Date.UTC(2026, 7, 1),
2436
+ },
2437
+ ]) as never,
2438
+ );
2439
+
2440
+ const result: CallResult = await callRoute({
2441
+ uri: MANIFEST_ROUTE,
2442
+ request: principal.request,
2443
+ body: { sessionId: "session-old" },
2444
+ });
2445
+
2446
+ expect(result.thrownToNext).toBeInstanceOf(NotFoundException);
2447
+
2448
+ const message: string = (result.thrownToNext as Exception).message;
2449
+
2450
+ /* The generic answer: no date, no retention, no admission it existed. */
2451
+ expect(message).toMatch(/^not-found:/);
2452
+ expect(message).not.toContain("2026-08-08");
2453
+ expect(message).not.toContain("retention");
2454
+ });
2455
+
2456
+ test("projects the clock, expiry, tags and engagement counters on the header, and the per-tab first offset", async () => {
2457
+ const principal: {
2458
+ request: JSONObject;
2459
+ databaseProps: DatabaseCommonInteractionProps;
2460
+ } = buildPrincipal({
2461
+ projectId: projectId,
2462
+ userId: userId,
2463
+ permissions: [Permission.TelemetryAdmin],
2464
+ });
2465
+
2466
+ mockProps(principal.databaseProps);
2467
+ headerQuerySpy.mockResolvedValue(
2468
+ fakeResultSet([
2469
+ {
2470
+ ...buildHeaderRow({
2471
+ sessionId: "session-1",
2472
+ projectId: projectId,
2473
+ rumApplicationId: applicationAId,
2474
+ }),
2475
+ aggClientReportedStart: 1699999999500,
2476
+ aggTags: { build: "1.2.3" },
2477
+ aggExpiresAt: 1700604800000,
2478
+ aggClickCount: 41,
2479
+ aggCustomEventCount: 3,
2480
+ aggActiveMs: 36000,
2481
+ aggFirstErrorOffsetMs: 12000,
2482
+ aggAttributes: { "recorder.capabilities": "click-events,tags" },
2483
+ },
2484
+ ]) as never,
2485
+ );
2486
+ mockApplication({ id: applicationAId, labelIds: [] });
2487
+ mockRecordedView();
2488
+
2489
+ chunkQuerySpy.mockResolvedValue(
2490
+ fakeResultSet([
2491
+ {
2492
+ tabId: "tab-2",
2493
+ chunkIndex: 0,
2494
+ chunkStartOffsetMs: 134000,
2495
+ chunkEndOffsetMs: 150000,
2496
+ eventCount: 10,
2497
+ hasFullSnapshot: 1,
2498
+ chunkPayloadBytes: 10,
2499
+ clickCount: 2,
2500
+ url: "https://example.com/help",
2501
+ },
2502
+ ]) as never,
1588
2503
  );
1589
- expect(auditArgs["sessionId"]).toBe("session-1");
1590
- expect(auditArgs["accessReason"]).toBe("incident triage");
1591
- expect(auditArgs["ipAddress"]).toBe("203.0.113.7");
2504
+
2505
+ const result: CallResult = await callRoute({
2506
+ uri: MANIFEST_ROUTE,
2507
+ request: principal.request,
2508
+ body: { sessionId: "session-1" },
2509
+ });
1592
2510
 
1593
2511
  const body: JSONObject = result.jsonBody as JSONObject;
1594
- expect(body["viewId"]).toBe(viewId.toString());
2512
+ const header: JSONObject = body["header"] as JSONObject;
2513
+
2514
+ expect(header["startTimeUnixMs"]).toBe(1700000000000);
2515
+ expect(header["endTimeUnixMs"]).toBe(1700000060000);
2516
+ expect(header["clientReportedStartUnixMs"]).toBe(1699999999500);
2517
+ expect(header["tags"]).toEqual({ build: "1.2.3" });
2518
+ expect(header["expiresAtUnixMs"]).toBe(1700604800000);
2519
+ expect(header["clickCount"]).toBe(41);
2520
+ expect(header["customEventCount"]).toBe(3);
2521
+ expect(header["activeMs"]).toBe(36000);
2522
+ expect(header["firstErrorOffsetMs"]).toBe(12000);
2523
+ expect(header["recorderCapabilities"]).toEqual(["click-events", "tags"]);
1595
2524
 
1596
2525
  const tabs: Array<JSONObject> = body[
1597
2526
  "tabs"
1598
2527
  ] as unknown as Array<JSONObject>;
1599
- expect(tabs).toHaveLength(1);
1600
- expect(tabs[0]!["chunkIndexes"]).toEqual([0, 3]);
1601
- expect(tabs[0]!["fullSnapshotChunkIndexes"]).toEqual([0]);
1602
-
1603
- /*
1604
- * Every counter gets a DISTINCT nonzero value, asserted individually:
1605
- * the client parses missing keys to 0, so a counter dropped from the
1606
- * SELECT (or two counters cross-wired in the row mapping) fails
1607
- * loudly here instead of silently blanking a timeline lane.
1608
- */
1609
- const manifestChunks: Array<JSONObject> = tabs[0]![
2528
+ expect(tabs[0]!["firstChunkStartOffsetMs"]).toBe(134000);
2529
+ const chunks: Array<JSONObject> = tabs[0]![
1610
2530
  "chunks"
1611
2531
  ] as unknown as Array<JSONObject>;
1612
- expect(manifestChunks[0]!["errorCount"]).toBe(2);
1613
- expect(manifestChunks[0]!["rageClickCount"]).toBe(1);
1614
- expect(manifestChunks[0]!["deadClickCount"]).toBe(4);
1615
- expect(manifestChunks[0]!["errorClickCount"]).toBe(6);
1616
- expect(manifestChunks[0]!["refreshRageCount"]).toBe(5);
1617
- expect(manifestChunks[0]!["routeCount"]).toBe(3);
1618
- expect(manifestChunks[1]!["errorCount"]).toBe(0);
2532
+ expect(chunks[0]!["clickCount"]).toBe(2);
2533
+ expect(chunks[0]!["url"]).toBe("https://example.com/help");
2534
+ });
2535
+
2536
+ /*
2537
+ * The identity columns carry the narrowest ACL in the schema. A
2538
+ * TelemetryAdmin can watch a recording but is deliberately excluded
2539
+ * from SESSION_REPLAY_IDENTITY_PERMISSIONS, so no statement issued on
2540
+ * their behalf may even NAME identifiedUserLabel or the traits map.
2541
+ */
2542
+ test("omits identifiedUserLabel and traits, and never names them, without the identity permission", async () => {
2543
+ const principal: {
2544
+ request: JSONObject;
2545
+ databaseProps: DatabaseCommonInteractionProps;
2546
+ } = buildPrincipal({
2547
+ projectId: projectId,
2548
+ userId: userId,
2549
+ permissions: [Permission.TelemetryAdmin],
2550
+ });
2551
+
2552
+ mockProps(principal.databaseProps);
2553
+ mockSessionHeader(applicationAId);
2554
+ mockApplication({ id: applicationAId, labelIds: [] });
2555
+ mockRecordedView();
2556
+
2557
+ const result: CallResult = await callRoute({
2558
+ uri: MANIFEST_ROUTE,
2559
+ request: principal.request,
2560
+ body: { sessionId: "session-1" },
2561
+ });
2562
+
2563
+ for (const call of headerQuerySpy.mock.calls) {
2564
+ const statement: Statement = call[0] as Statement;
2565
+ expect(statement.query).not.toContain("identifiedUserLabel");
2566
+ expect(statement.query).not.toContain("identifiedUserTraits");
2567
+ }
2568
+
2569
+ const header: JSONObject = (result.jsonBody as JSONObject)[
2570
+ "header"
2571
+ ] as JSONObject;
2572
+ expect(header["identifiedUserLabel"]).toBeUndefined();
2573
+ expect(header["identifiedUserTraits"]).toBeUndefined();
2574
+ });
2575
+
2576
+ test("includes identifiedUserLabel and traits, read by a separate application-pinned statement, with the identity permission", async () => {
2577
+ const principal: {
2578
+ request: JSONObject;
2579
+ databaseProps: DatabaseCommonInteractionProps;
2580
+ } = buildPrincipal({
2581
+ projectId: projectId,
2582
+ userId: userId,
2583
+ permissions: [Permission.ReadRumSessionReplayPayload],
2584
+ labelIds: [labelAId],
2585
+ });
2586
+
2587
+ mockProps(principal.databaseProps);
2588
+ headerQuerySpy.mockImplementation(async (statement: Statement) => {
2589
+ if (statement.query.includes("identifiedUserTraits")) {
2590
+ return fakeResultSet([
2591
+ {
2592
+ aggIdentifiedUserLabel: "jane@example.com",
2593
+ aggIdentifiedUserTraits: { plan: "pro" },
2594
+ },
2595
+ ]);
2596
+ }
2597
+
2598
+ return fakeResultSet([
2599
+ buildHeaderRow({
2600
+ sessionId: "session-1",
2601
+ projectId: projectId,
2602
+ rumApplicationId: applicationAId,
2603
+ }),
2604
+ ]);
2605
+ });
2606
+ mockApplication({ id: applicationAId, labelIds: [labelAId] });
2607
+ mockRecordedView();
2608
+
2609
+ const result: CallResult = await callRoute({
2610
+ uri: MANIFEST_ROUTE,
2611
+ request: principal.request,
2612
+ body: { sessionId: "session-1" },
2613
+ });
2614
+
2615
+ const identityStatements: Array<Statement> = headerQuerySpy.mock.calls
2616
+ .map((call: Array<unknown>): Statement => {
2617
+ return call[0] as Statement;
2618
+ })
2619
+ .filter((statement: Statement): boolean => {
2620
+ return statement.query.includes("identifiedUserLabel");
2621
+ });
2622
+
2623
+ expect(identityStatements).toHaveLength(1);
2624
+ expect(identityStatements[0]!.query).toContain(
2625
+ "argMax(identifiedUserTraits, version) AS aggIdentifiedUserTraits",
2626
+ );
2627
+ expect(identityStatements[0]!.query).toContain("rumApplicationId = ");
2628
+ expect(Object.values(identityStatements[0]!.query_params)).toContain(
2629
+ applicationAId.toString(),
2630
+ );
2631
+
2632
+ /* The header read itself still never names them. */
2633
+ const headerStatement: Statement = headerQuerySpy.mock
2634
+ .calls[0]![0] as Statement;
2635
+ expect(headerStatement.query).not.toContain("identifiedUserLabel");
2636
+
2637
+ const header: JSONObject = (result.jsonBody as JSONObject)[
2638
+ "header"
2639
+ ] as JSONObject;
2640
+ expect(header["identifiedUserLabel"]).toBe("jane@example.com");
2641
+ expect(header["identifiedUserTraits"]).toEqual({ plan: "pro" });
2642
+ });
2643
+
2644
+ test("omits the identity columns when the identity grant is scoped to a different application", async () => {
2645
+ const principal: {
2646
+ request: JSONObject;
2647
+ databaseProps: DatabaseCommonInteractionProps;
2648
+ } = buildPrincipal({
2649
+ projectId: projectId,
2650
+ userId: userId,
2651
+ permissions: [Permission.ReadRumSessionReplayPayload],
2652
+ labelIds: [labelAId],
2653
+ });
2654
+
2655
+ mockProps(principal.databaseProps);
2656
+ mockSessionHeader(applicationBId);
2657
+ /* B carries label A too so the payload read is allowed... */
2658
+ mockApplication({ id: applicationBId, labelIds: [labelAId] });
2659
+ mockRecordedView();
2660
+
2661
+ const allowed: CallResult = await callRoute({
2662
+ uri: MANIFEST_ROUTE,
2663
+ request: principal.request,
2664
+ body: { sessionId: "session-1" },
2665
+ });
2666
+ expect(allowed.thrownToNext).toBeUndefined();
2667
+
2668
+ /* ...whereas with only a foreign label the whole read is refused. */
2669
+ jest.clearAllMocks();
2670
+ mockProps(principal.databaseProps);
2671
+ mockSessionHeader(applicationBId);
2672
+ mockApplication({ id: applicationBId, labelIds: [labelBId] });
2673
+
2674
+ const refused: CallResult = await callRoute({
2675
+ uri: MANIFEST_ROUTE,
2676
+ request: principal.request,
2677
+ body: { sessionId: "session-1" },
2678
+ });
2679
+
2680
+ expect(refused.thrownToNext).toBeInstanceOf(NotAuthorizedException);
2681
+ for (const call of headerQuerySpy.mock.calls) {
2682
+ expect((call[0] as Statement).query).not.toContain(
2683
+ "identifiedUserLabel",
2684
+ );
2685
+ }
2686
+ });
2687
+
2688
+ /*
2689
+ * Live polling. The player re-fetches the manifest every 30s while a
2690
+ * session is recording; each poll is the same viewing, and one audit
2691
+ * row per VIEW is the contract the E2E pins.
2692
+ */
2693
+ test("isRefresh with the caller's own viewId for this session skips the audit write and echoes the id", async () => {
2694
+ const principal: {
2695
+ request: JSONObject;
2696
+ databaseProps: DatabaseCommonInteractionProps;
2697
+ } = buildPrincipal({
2698
+ projectId: projectId,
2699
+ userId: userId,
2700
+ permissions: [Permission.ReadRumSessionReplayPayload],
2701
+ });
2702
+
2703
+ mockProps(principal.databaseProps);
2704
+ mockSessionHeader(applicationAId);
2705
+ mockApplication({ id: applicationAId, labelIds: [] });
2706
+
2707
+ const viewId: ObjectID = ObjectID.generate();
2708
+ mockOwnView({ viewId: viewId, rumApplicationId: applicationAId });
2709
+
2710
+ const result: CallResult = await callRoute({
2711
+ uri: MANIFEST_ROUTE,
2712
+ request: principal.request,
2713
+ body: {
2714
+ sessionId: "session-1",
2715
+ isRefresh: true,
2716
+ viewId: viewId.toString(),
2717
+ },
2718
+ });
2719
+
2720
+ expect(recordViewSpy).not.toHaveBeenCalled();
2721
+ expect((result.jsonBody as JSONObject)["viewId"]).toBe(viewId.toString());
2722
+
2723
+ /* Ownership AND the session are in the lookup's own predicate. */
2724
+ const lookupArgs: JSONObject = viewFindOneBySpy.mock
2725
+ .calls[0]![0] as JSONObject;
2726
+ const query: JSONObject = lookupArgs["query"] as JSONObject;
2727
+ expect((query["viewedByUserId"] as ObjectID).toString()).toBe(
2728
+ userId.toString(),
2729
+ );
2730
+ expect(query["sessionId"]).toBe("session-1");
2731
+ expect(query["_id"]).toBe(viewId.toString());
2732
+ });
2733
+
2734
+ test("isRefresh with a viewId that is not the caller's own for this session records a fresh view", async () => {
2735
+ const principal: {
2736
+ request: JSONObject;
2737
+ databaseProps: DatabaseCommonInteractionProps;
2738
+ } = buildPrincipal({
2739
+ projectId: projectId,
2740
+ userId: userId,
2741
+ permissions: [Permission.ReadRumSessionReplayPayload],
2742
+ });
2743
+
2744
+ mockProps(principal.databaseProps);
2745
+ mockSessionHeader(applicationAId);
2746
+ mockApplication({ id: applicationAId, labelIds: [] });
2747
+ const freshViewId: ObjectID = mockRecordedView();
2748
+
2749
+ /* The row is somebody else's, or for another session: no match. */
2750
+ viewFindOneBySpy.mockResolvedValue(null);
2751
+
2752
+ const result: CallResult = await callRoute({
2753
+ uri: MANIFEST_ROUTE,
2754
+ request: principal.request,
2755
+ body: {
2756
+ sessionId: "session-1",
2757
+ isRefresh: true,
2758
+ viewId: ObjectID.generate().toString(),
2759
+ },
2760
+ });
2761
+
2762
+ expect(recordViewSpy).toHaveBeenCalledTimes(1);
2763
+ expect((result.jsonBody as JSONObject)["viewId"]).toBe(
2764
+ freshViewId.toString(),
2765
+ );
2766
+ });
2767
+
2768
+ test("isRefresh with a view row for a different application than the one authorized records a fresh view", async () => {
2769
+ const principal: {
2770
+ request: JSONObject;
2771
+ databaseProps: DatabaseCommonInteractionProps;
2772
+ } = buildPrincipal({
2773
+ projectId: projectId,
2774
+ userId: userId,
2775
+ permissions: [Permission.ReadRumSessionReplayPayload],
2776
+ });
2777
+
2778
+ mockProps(principal.databaseProps);
2779
+ mockSessionHeader(applicationAId);
2780
+ mockApplication({ id: applicationAId, labelIds: [] });
2781
+ mockRecordedView();
2782
+
2783
+ const viewId: ObjectID = ObjectID.generate();
2784
+ mockOwnView({ viewId: viewId, rumApplicationId: applicationBId });
1619
2785
 
1620
- /*
1621
- * The hole between chunk 0 and chunk 3 must be reported. Playback
1622
- * that silently crosses a gap renders a DOM the user never saw.
1623
- */
1624
- const gaps: Array<JSONObject> = tabs[0]![
1625
- "gaps"
1626
- ] as unknown as Array<JSONObject>;
1627
- expect(gaps).toHaveLength(1);
1628
- expect(gaps[0]!["fromIndex"]).toBe(0);
1629
- expect(gaps[0]!["toIndex"]).toBe(3);
1630
- expect(gaps[0]!["missingMs"]).toBe(30000);
2786
+ await callRoute({
2787
+ uri: MANIFEST_ROUTE,
2788
+ request: principal.request,
2789
+ body: {
2790
+ sessionId: "session-1",
2791
+ isRefresh: true,
2792
+ viewId: viewId.toString(),
2793
+ },
2794
+ });
2795
+
2796
+ expect(recordViewSpy).toHaveBeenCalledTimes(1);
1631
2797
  });
1632
2798
 
1633
- test("a session that does not exist in this tenant is refused before any audit row", async () => {
2799
+ test("a malformed linkedIncidentId is dropped from the audit row rather than failing the read", async () => {
1634
2800
  const principal: {
1635
2801
  request: JSONObject;
1636
2802
  databaseProps: DatabaseCommonInteractionProps;
1637
2803
  } = buildPrincipal({
1638
2804
  projectId: projectId,
1639
2805
  userId: userId,
1640
- permissions: [Permission.ProjectOwner],
2806
+ permissions: [Permission.ReadRumSessionReplayPayload],
1641
2807
  });
1642
2808
 
1643
2809
  mockProps(principal.databaseProps);
1644
- headerQuerySpy.mockResolvedValue(fakeResultSet([]) as never);
2810
+ mockSessionHeader(applicationAId);
2811
+ mockApplication({ id: applicationAId, labelIds: [] });
2812
+ mockRecordedView();
1645
2813
 
1646
- const result: CallResult = await callRoute({
2814
+ const incidentId: ObjectID = ObjectID.generate();
2815
+
2816
+ await callRoute({
1647
2817
  uri: MANIFEST_ROUTE,
1648
2818
  request: principal.request,
1649
- body: { sessionId: "does-not-exist" },
2819
+ body: {
2820
+ sessionId: "session-1",
2821
+ linkedIncidentId: "not-a-uuid",
2822
+ linkedExceptionFingerprint: "fp-1",
2823
+ },
1650
2824
  });
1651
2825
 
1652
- expect(result.thrownToNext).toBeInstanceOf(BadDataException);
1653
- expect(recordViewSpy).not.toHaveBeenCalled();
2826
+ const dropped: JSONObject = recordViewSpy.mock.calls[0]![0] as JSONObject;
2827
+ expect(dropped["linkedIncidentId"]).toBeUndefined();
2828
+ expect(dropped["linkedExceptionFingerprint"]).toBe("fp-1");
2829
+
2830
+ jest.clearAllMocks();
2831
+ mockProps(principal.databaseProps);
2832
+ mockSessionHeader(applicationAId);
2833
+ mockApplication({ id: applicationAId, labelIds: [] });
2834
+ mockRecordedView();
2835
+
2836
+ await callRoute({
2837
+ uri: MANIFEST_ROUTE,
2838
+ request: principal.request,
2839
+ body: {
2840
+ sessionId: "session-1",
2841
+ linkedIncidentId: incidentId.toString(),
2842
+ },
2843
+ });
2844
+
2845
+ const kept: JSONObject = recordViewSpy.mock.calls[0]![0] as JSONObject;
2846
+ expect((kept["linkedIncidentId"] as ObjectID).toString()).toBe(
2847
+ incidentId.toString(),
2848
+ );
1654
2849
  });
1655
2850
  });
1656
2851
 
@@ -1702,7 +2897,17 @@ describe("Session replay playback API", () => {
1702
2897
  expect(headerQuerySpy).not.toHaveBeenCalled();
1703
2898
  });
1704
2899
 
1705
- test("rejects a total payload larger than the read byte cap before returning any bytes", async () => {
2900
+ /*
2901
+ * The cap has to bound the bytes this endpoint actually returns.
2902
+ * `payloadBytes` is the POST-GZIP wire size the recorder uploaded,
2903
+ * while the `payload` column holds the DECOMPRESSED JSON that is
2904
+ * served. Measuring the former let 8 chunks that pass a 8 MiB check
2905
+ * decompress into tens of megabytes of response. It is measured in
2906
+ * the SAME statement that ships the bytes, so the column is
2907
+ * decompressed once per page rather than once for a pre-check and
2908
+ * again for the read.
2909
+ */
2910
+ test("measures the stored (decompressed) size in the one statement that ships the bytes", async () => {
1706
2911
  const principal: {
1707
2912
  request: JSONObject;
1708
2913
  databaseProps: DatabaseCommonInteractionProps;
@@ -1714,34 +2919,36 @@ describe("Session replay playback API", () => {
1714
2919
 
1715
2920
  chunkQuerySpy.mockResolvedValue(
1716
2921
  fakeResultSet([
1717
- { chunkIndex: 0, chunkStoredBytes: 5 * 1024 * 1024 },
1718
- { chunkIndex: 1, chunkStoredBytes: 5 * 1024 * 1024 },
2922
+ { chunkIndex: 0, servedPayload: "[1]", isServed: 1 },
1719
2923
  ]) as never,
1720
2924
  );
1721
2925
 
1722
- const result: CallResult = await callRoute({
2926
+ await callRoute({
1723
2927
  uri: CHUNKS_ROUTE,
1724
2928
  request: principal.request,
1725
2929
  body: {
1726
2930
  sessionId: "session-1",
1727
2931
  tabId: "tab-1",
1728
- chunkIndexes: [0, 1],
2932
+ chunkIndexes: [0],
1729
2933
  },
1730
2934
  });
1731
2935
 
1732
- expect(result.thrownToNext).toBeInstanceOf(BadDataException);
1733
- /* Refused on the pre-check: the payload read never ran. */
1734
2936
  expect(chunkQuerySpy).toHaveBeenCalledTimes(1);
2937
+
2938
+ const statement: Statement = chunkQuerySpy.mock.calls[0]![0] as Statement;
2939
+
2940
+ expect(statement.query).toContain("length(payload)");
2941
+ expect(statement.query).not.toContain("toFloat64(payloadBytes)");
1735
2942
  });
1736
2943
 
1737
2944
  /*
1738
- * The cap has to bound the bytes this endpoint actually returns.
1739
- * `payloadBytes` is the POST-GZIP wire size the recorder uploaded,
1740
- * while the `payload` column holds the DECOMPRESSED JSON that is
1741
- * served. Measuring the former let 8 chunks that pass a 8 MiB check
1742
- * decompress into tens of megabytes of response.
2945
+ * A page that does not fit is answered with the prefix that does -
2946
+ * never with a refusal. The old 400 ("Request fewer chunks") was
2947
+ * reachable by the player's own page plan, because it plans against
2948
+ * the wire size the manifest exposes while the cap is on decompressed
2949
+ * bytes, and the player had no recovery for it.
1743
2950
  */
1744
- test("the pre-check measures the stored (decompressed) size, not the compressed wire size", async () => {
2951
+ test("serves the prefix of whole chunks that fits, names the rest, and never refuses", async () => {
1745
2952
  const principal: {
1746
2953
  request: JSONObject;
1747
2954
  databaseProps: DatabaseCommonInteractionProps;
@@ -1751,31 +2958,43 @@ describe("Session replay playback API", () => {
1751
2958
  mockSessionHeader(applicationAId);
1752
2959
  mockApplication({ id: applicationAId, labelIds: [] });
1753
2960
 
1754
- chunkQuerySpy
1755
- .mockResolvedValueOnce(
1756
- fakeResultSet([{ chunkIndex: 0, chunkStoredBytes: 4 }]) as never,
1757
- )
1758
- .mockResolvedValueOnce(
1759
- fakeResultSet([{ chunkIndex: 0, payload: "[1]" }]) as never,
1760
- );
2961
+ const fatPayload: string = "a".repeat(5 * 1024 * 1024);
1761
2962
 
1762
- await callRoute({
2963
+ chunkQuerySpy.mockResolvedValue(
2964
+ fakeResultSet([
2965
+ { chunkIndex: 0, servedPayload: fatPayload, isServed: 1 },
2966
+ { chunkIndex: 1, servedPayload: fatPayload, isServed: 1 },
2967
+ { chunkIndex: 2, servedPayload: "", isServed: 0 },
2968
+ ]) as never,
2969
+ );
2970
+
2971
+ const result: CallResult = await callRoute({
1763
2972
  uri: CHUNKS_ROUTE,
1764
2973
  request: principal.request,
1765
2974
  body: {
1766
2975
  sessionId: "session-1",
1767
2976
  tabId: "tab-1",
1768
- chunkIndexes: [0],
2977
+ chunkIndexes: [0, 1, 2],
1769
2978
  },
1770
2979
  });
1771
2980
 
1772
- const precheck: Statement = chunkQuerySpy.mock.calls[0]![0] as Statement;
2981
+ expect(result.thrownToNext).toBeUndefined();
2982
+ expect(result.deniedWith).toBeUndefined();
1773
2983
 
1774
- expect(precheck.query).toContain("length(payload)");
1775
- expect(precheck.query).not.toContain("toFloat64(payloadBytes)");
2984
+ const buffer: Buffer = result.sentBuffer as unknown as Buffer;
2985
+ expect(buffer.readUInt32LE(0)).toBe(0);
2986
+ expect(buffer.readUInt32LE(4)).toBe(fatPayload.length);
2987
+ expect(buffer.length).toBe(8 + fatPayload.length);
2988
+ expect(result.headers["X-OneUptime-Replay-Omitted-Chunks"]).toBe("1,2");
1776
2989
  });
1777
2990
 
1778
- test("refuses to serve more bytes than the cap even when the pre-check said the chunks were small", async () => {
2991
+ /*
2992
+ * A full snapshot between (cap - 8 bytes) and the cap - or one that
2993
+ * re-serialised slightly larger than the ingest inflate cap - used
2994
+ * to fail EVERY fetch, so playback of that tab dead-ended at it. A
2995
+ * lone chunk is bounded by the ingest cap already.
2996
+ */
2997
+ test("a single chunk at or over the read cap is still served", async () => {
1779
2998
  const principal: {
1780
2999
  request: JSONObject;
1781
3000
  databaseProps: DatabaseCommonInteractionProps;
@@ -1785,22 +3004,13 @@ describe("Session replay playback API", () => {
1785
3004
  mockSessionHeader(applicationAId);
1786
3005
  mockApplication({ id: applicationAId, labelIds: [] });
1787
3006
 
1788
- /* 10 MiB of real payload behind a pre-check that reported 8 bytes. */
1789
- const fatPayload: string = "a".repeat(5 * 1024 * 1024);
3007
+ const edgePayload: string = "a".repeat(8 * 1024 * 1024);
1790
3008
 
1791
- chunkQuerySpy
1792
- .mockResolvedValueOnce(
1793
- fakeResultSet([
1794
- { chunkIndex: 0, chunkStoredBytes: 4 },
1795
- { chunkIndex: 1, chunkStoredBytes: 4 },
1796
- ]) as never,
1797
- )
1798
- .mockResolvedValueOnce(
1799
- fakeResultSet([
1800
- { chunkIndex: 0, payload: fatPayload },
1801
- { chunkIndex: 1, payload: fatPayload },
1802
- ]) as never,
1803
- );
3009
+ chunkQuerySpy.mockResolvedValue(
3010
+ fakeResultSet([
3011
+ { chunkIndex: 0, servedPayload: edgePayload, isServed: 1 },
3012
+ ]) as never,
3013
+ );
1804
3014
 
1805
3015
  const result: CallResult = await callRoute({
1806
3016
  uri: CHUNKS_ROUTE,
@@ -1808,13 +3018,85 @@ describe("Session replay playback API", () => {
1808
3018
  body: {
1809
3019
  sessionId: "session-1",
1810
3020
  tabId: "tab-1",
1811
- chunkIndexes: [0, 1],
3021
+ chunkIndexes: [0],
1812
3022
  },
1813
3023
  });
1814
3024
 
1815
- expect(result.thrownToNext).toBeInstanceOf(BadDataException);
1816
- /* Nothing was served. */
1817
- expect(result.sentBuffer).toBeUndefined();
3025
+ expect(result.thrownToNext).toBeUndefined();
3026
+ const buffer: Buffer = result.sentBuffer as unknown as Buffer;
3027
+ expect(buffer.length).toBe(8 + edgePayload.length);
3028
+ expect(
3029
+ result.headers["X-OneUptime-Replay-Omitted-Chunks"],
3030
+ ).toBeUndefined();
3031
+ });
3032
+
3033
+ /*
3034
+ * A 480-chunk session is 60 chunk pages. Each used to re-run the
3035
+ * header GROUP BY and re-load the application's labels; now the
3036
+ * chunk route serves both from a 30s cache the manifest refreshes.
3037
+ * The label DECISION is still made per request against the caller's
3038
+ * permissions.
3039
+ */
3040
+ test("consecutive chunk pages reuse the resolved header and application", async () => {
3041
+ const principal: {
3042
+ request: JSONObject;
3043
+ databaseProps: DatabaseCommonInteractionProps;
3044
+ } = payloadPrincipal();
3045
+
3046
+ mockProps(principal.databaseProps);
3047
+ mockSessionHeader(applicationAId);
3048
+ mockApplication({ id: applicationAId, labelIds: [] });
3049
+
3050
+ chunkQuerySpy.mockResolvedValue(
3051
+ fakeResultSet([
3052
+ { chunkIndex: 0, servedPayload: "[1]", isServed: 1 },
3053
+ ]) as never,
3054
+ );
3055
+
3056
+ for (const chunkIndex of [0, 1, 2]) {
3057
+ await callRoute({
3058
+ uri: CHUNKS_ROUTE,
3059
+ request: principal.request,
3060
+ body: {
3061
+ sessionId: "session-1",
3062
+ tabId: "tab-1",
3063
+ chunkIndexes: [chunkIndex],
3064
+ },
3065
+ });
3066
+ }
3067
+
3068
+ expect(chunkQuerySpy).toHaveBeenCalledTimes(3);
3069
+ expect(headerQuerySpy).toHaveBeenCalledTimes(1);
3070
+ expect(findOneBySpy).toHaveBeenCalledTimes(1);
3071
+
3072
+ /*
3073
+ * A revoked grant takes effect on the very next page: the cached
3074
+ * data is re-checked against the caller's current permissions.
3075
+ */
3076
+ const viewer: {
3077
+ request: JSONObject;
3078
+ databaseProps: DatabaseCommonInteractionProps;
3079
+ } = buildPrincipal({
3080
+ projectId: projectId,
3081
+ userId: userId,
3082
+ permissions: [Permission.ReadRumSessionReplayPayload],
3083
+ labelIds: [labelBId],
3084
+ });
3085
+
3086
+ mockProps(viewer.databaseProps);
3087
+
3088
+ const refused: CallResult = await callRoute({
3089
+ uri: CHUNKS_ROUTE,
3090
+ request: viewer.request,
3091
+ body: {
3092
+ sessionId: "session-1",
3093
+ tabId: "tab-1",
3094
+ chunkIndexes: [3],
3095
+ },
3096
+ });
3097
+
3098
+ expect(refused.thrownToNext).toBeInstanceOf(NotAuthorizedException);
3099
+ expect(chunkQuerySpy).toHaveBeenCalledTimes(3);
1818
3100
  });
1819
3101
 
1820
3102
  test("returns length-prefixed binary frames and de-duplicates by version", async () => {
@@ -1827,19 +3109,12 @@ describe("Session replay playback API", () => {
1827
3109
  mockSessionHeader(applicationAId);
1828
3110
  mockApplication({ id: applicationAId, labelIds: [] });
1829
3111
 
1830
- chunkQuerySpy
1831
- .mockResolvedValueOnce(
1832
- fakeResultSet([
1833
- { chunkIndex: 0, chunkStoredBytes: 3 },
1834
- { chunkIndex: 1, chunkStoredBytes: 4 },
1835
- ]) as never,
1836
- )
1837
- .mockResolvedValueOnce(
1838
- fakeResultSet([
1839
- { chunkIndex: 0, payload: "[1]" },
1840
- { chunkIndex: 1, payload: "[22]" },
1841
- ]) as never,
1842
- );
3112
+ chunkQuerySpy.mockResolvedValue(
3113
+ fakeResultSet([
3114
+ { chunkIndex: 0, servedPayload: "[1]", isServed: 1 },
3115
+ { chunkIndex: 1, servedPayload: "[22]", isServed: 1 },
3116
+ ]) as never,
3117
+ );
1843
3118
 
1844
3119
  const result: CallResult = await callRoute({
1845
3120
  uri: CHUNKS_ROUTE,
@@ -1866,7 +3141,7 @@ describe("Session replay playback API", () => {
1866
3141
  expect(result.headers["Cache-Control"]).toBe("no-store");
1867
3142
 
1868
3143
  const payloadStatement: Statement = chunkQuerySpy.mock
1869
- .calls[1]![0] as Statement;
3144
+ .calls[0]![0] as Statement;
1870
3145
  expect(payloadStatement.query).toContain(
1871
3146
  "ORDER BY chunkIndex ASC, version DESC LIMIT 1 BY chunkIndex",
1872
3147
  );
@@ -1986,22 +3261,121 @@ describe("Session replay playback API", () => {
1986
3261
  },
1987
3262
  });
1988
3263
 
1989
- expect(result.deniedWith).toBeInstanceOf(NotAuthorizedException);
1990
- expect(recordSecondsWatchedSpy).not.toHaveBeenCalled();
3264
+ expect(result.deniedWith).toBeInstanceOf(NotAuthorizedException);
3265
+ expect(recordSecondsWatchedSpy).not.toHaveBeenCalled();
3266
+
3267
+ /* The ownership predicate is the thing being pinned. */
3268
+ const lookupArgs: JSONObject = viewFindOneBySpy.mock
3269
+ .calls[0]![0] as JSONObject;
3270
+ const query: JSONObject = lookupArgs["query"] as JSONObject;
3271
+ expect((query["viewedByUserId"] as ObjectID).toString()).toBe(
3272
+ userId.toString(),
3273
+ );
3274
+ expect((query["projectId"] as ObjectID).toString()).toBe(
3275
+ projectId.toString(),
3276
+ );
3277
+ });
3278
+
3279
+ test("a caller outside the view's application label scope cannot advance it", async () => {
3280
+ const principal: {
3281
+ request: JSONObject;
3282
+ databaseProps: DatabaseCommonInteractionProps;
3283
+ } = buildPrincipal({
3284
+ projectId: projectId,
3285
+ userId: userId,
3286
+ permissions: [Permission.ReadRumSessionReplayPayload],
3287
+ labelIds: [labelAId],
3288
+ });
3289
+
3290
+ mockProps(principal.databaseProps);
3291
+ /* The view points at application B, which carries a foreign label. */
3292
+ mockApplication({ id: applicationBId, labelIds: [labelBId] });
3293
+
3294
+ const viewId: ObjectID = ObjectID.generate();
3295
+ mockOwnView({ viewId: viewId, rumApplicationId: applicationBId });
3296
+
3297
+ const result: CallResult = await callRoute({
3298
+ uri: HEARTBEAT_ROUTE,
3299
+ request: principal.request,
3300
+ body: { viewId: viewId.toString(), secondsWatched: 30 },
3301
+ });
3302
+
3303
+ expect(result.thrownToNext).toBeInstanceOf(NotAuthorizedException);
3304
+ expect(recordSecondsWatchedSpy).not.toHaveBeenCalled();
3305
+ });
3306
+
3307
+ test("a malformed viewId is a bad request, not a database error", async () => {
3308
+ const principal: {
3309
+ request: JSONObject;
3310
+ databaseProps: DatabaseCommonInteractionProps;
3311
+ } = buildPrincipal({
3312
+ projectId: projectId,
3313
+ userId: userId,
3314
+ permissions: [Permission.ReadRumSessionReplayPayload],
3315
+ });
3316
+
3317
+ mockProps(principal.databaseProps);
3318
+
3319
+ const result: CallResult = await callRoute({
3320
+ uri: HEARTBEAT_ROUTE,
3321
+ request: principal.request,
3322
+ body: { viewId: "nope", secondsWatched: 30 },
3323
+ });
3324
+
3325
+ expect(result.thrownToNext).toBeInstanceOf(BadDataException);
3326
+ expect(viewFindOneBySpy).not.toHaveBeenCalled();
3327
+ expect(recordSecondsWatchedSpy).not.toHaveBeenCalled();
3328
+ });
3329
+
3330
+ /*
3331
+ * secondsWatched is time WATCHED, cumulative and monotonic. The one
3332
+ * ownership lookup also carries the row's current figure, so the
3333
+ * service is told what it already holds and a heartbeat that does not
3334
+ * advance costs no write; the response echoes the figure on record.
3335
+ */
3336
+ test("hands the service the row's current figure and never reports a lower one", async () => {
3337
+ const principal: {
3338
+ request: JSONObject;
3339
+ databaseProps: DatabaseCommonInteractionProps;
3340
+ } = buildPrincipal({
3341
+ projectId: projectId,
3342
+ userId: userId,
3343
+ permissions: [Permission.ReadRumSessionReplayPayload],
3344
+ });
3345
+
3346
+ mockProps(principal.databaseProps);
3347
+ mockApplication({ id: applicationAId, labelIds: [] });
3348
+
3349
+ const viewId: ObjectID = ObjectID.generate();
3350
+ const view: RumSessionReplayView = new RumSessionReplayView();
3351
+ view.id = viewId;
3352
+ view.projectId = projectId;
3353
+ view.rumApplicationId = applicationAId;
3354
+ view.viewedByUserId = userId;
3355
+ view.secondsWatched = 90;
3356
+ viewFindOneBySpy.mockResolvedValue(view);
3357
+
3358
+ const result: CallResult = await callRoute({
3359
+ uri: HEARTBEAT_ROUTE,
3360
+ request: principal.request,
3361
+ body: { viewId: viewId.toString(), secondsWatched: 44 },
3362
+ });
3363
+
3364
+ const args: JSONObject = recordSecondsWatchedSpy.mock
3365
+ .calls[0]![0] as JSONObject;
3366
+ expect(args["secondsWatched"]).toBe(30);
3367
+ expect(args["currentSecondsWatched"]).toBe(90);
3368
+ expect((result.jsonBody as JSONObject)["secondsWatched"]).toBe(90);
1991
3369
 
1992
- /* The ownership predicate is the thing being pinned. */
3370
+ /* The lookup selects the figure so no second read is needed. */
1993
3371
  const lookupArgs: JSONObject = viewFindOneBySpy.mock
1994
3372
  .calls[0]![0] as JSONObject;
1995
- const query: JSONObject = lookupArgs["query"] as JSONObject;
1996
- expect((query["viewedByUserId"] as ObjectID).toString()).toBe(
1997
- userId.toString(),
1998
- );
1999
- expect((query["projectId"] as ObjectID).toString()).toBe(
2000
- projectId.toString(),
2001
- );
3373
+ expect((lookupArgs["select"] as JSONObject)["secondsWatched"]).toBe(true);
2002
3374
  });
3375
+ });
2003
3376
 
2004
- test("a caller outside the view's application label scope cannot advance it", async () => {
3377
+ describe("views", () => {
3378
+ test("lists who watched, pinned to the authorized application, for a payload-permitted caller", async () => {
2005
3379
  const principal: {
2006
3380
  request: JSONObject;
2007
3381
  databaseProps: DatabaseCommonInteractionProps;
@@ -2009,47 +3383,67 @@ describe("Session replay playback API", () => {
2009
3383
  projectId: projectId,
2010
3384
  userId: userId,
2011
3385
  permissions: [Permission.ReadRumSessionReplayPayload],
2012
- labelIds: [labelAId],
2013
3386
  });
2014
3387
 
2015
3388
  mockProps(principal.databaseProps);
2016
- /* The view points at application B, which carries a foreign label. */
2017
- mockApplication({ id: applicationBId, labelIds: [labelBId] });
3389
+ mockSessionHeader(applicationAId);
3390
+ mockApplication({ id: applicationAId, labelIds: [] });
2018
3391
 
2019
- const viewId: ObjectID = ObjectID.generate();
2020
- mockOwnView({ viewId: viewId, rumApplicationId: applicationBId });
3392
+ const view: RumSessionReplayView = new RumSessionReplayView();
3393
+ view.id = ObjectID.generate();
3394
+ view.viewedAt = new Date("2026-08-01T10:00:00.000Z");
3395
+ view.secondsWatched = 45;
3396
+ view.accessReason = "incident triage";
3397
+ view.viewedByUserId = userId;
3398
+
3399
+ const getViewsSpy: jest.SpyInstance = jest
3400
+ .spyOn(RumSessionReplayViewService, "getViewsForSession")
3401
+ .mockResolvedValue([view]);
2021
3402
 
2022
3403
  const result: CallResult = await callRoute({
2023
- uri: HEARTBEAT_ROUTE,
3404
+ uri: VIEWS_ROUTE,
2024
3405
  request: principal.request,
2025
- body: { viewId: viewId.toString(), secondsWatched: 30 },
3406
+ body: { sessionId: "session-1" },
2026
3407
  });
2027
3408
 
2028
- expect(result.thrownToNext).toBeInstanceOf(NotAuthorizedException);
2029
- expect(recordSecondsWatchedSpy).not.toHaveBeenCalled();
3409
+ const args: JSONObject = getViewsSpy.mock.calls[0]![0] as JSONObject;
3410
+ expect((args["rumApplicationId"] as ObjectID).toString()).toBe(
3411
+ applicationAId.toString(),
3412
+ );
3413
+ expect(args["sessionId"]).toBe("session-1");
3414
+
3415
+ const views: Array<JSONObject> = (result.jsonBody as JSONObject)[
3416
+ "views"
3417
+ ] as unknown as Array<JSONObject>;
3418
+ expect(views).toHaveLength(1);
3419
+ expect(views[0]!["secondsWatched"]).toBe(45);
3420
+ expect(views[0]!["accessReason"]).toBe("incident triage");
3421
+ expect(views[0]!["viewedAt"]).toBe("2026-08-01T10:00:00.000Z");
3422
+ expect(views[0]!["viewedByUserId"]).toBe(userId.toString());
3423
+ /* Listing who watched is not itself a view. */
3424
+ expect(recordViewSpy).not.toHaveBeenCalled();
2030
3425
  });
2031
3426
 
2032
- test("a malformed viewId is a bad request, not a database error", async () => {
3427
+ test("a caller with only the list permission cannot see who watched", async () => {
2033
3428
  const principal: {
2034
3429
  request: JSONObject;
2035
3430
  databaseProps: DatabaseCommonInteractionProps;
2036
3431
  } = buildPrincipal({
2037
3432
  projectId: projectId,
2038
3433
  userId: userId,
2039
- permissions: [Permission.ReadRumSessionReplayPayload],
3434
+ permissions: [Permission.ReadRumSessionReplay],
2040
3435
  });
2041
3436
 
2042
3437
  mockProps(principal.databaseProps);
2043
3438
 
2044
3439
  const result: CallResult = await callRoute({
2045
- uri: HEARTBEAT_ROUTE,
3440
+ uri: VIEWS_ROUTE,
2046
3441
  request: principal.request,
2047
- body: { viewId: "nope", secondsWatched: 30 },
3442
+ body: { sessionId: "session-1" },
2048
3443
  });
2049
3444
 
2050
- expect(result.thrownToNext).toBeInstanceOf(BadDataException);
2051
- expect(viewFindOneBySpy).not.toHaveBeenCalled();
2052
- expect(recordSecondsWatchedSpy).not.toHaveBeenCalled();
3445
+ expect(result.deniedWith).toBeInstanceOf(NotAuthorizedException);
3446
+ expect(headerQuerySpy).not.toHaveBeenCalled();
2053
3447
  });
2054
3448
  });
2055
3449
 
@@ -2207,6 +3601,174 @@ describe("Session replay playback API", () => {
2207
3601
  expect(sessions[0]!["refreshRageCount"]).toBe(5);
2208
3602
  expect(sessions[0]!["maskingMode"]).toBe("MaskAllText");
2209
3603
  });
3604
+
3605
+ /*
3606
+ * RumSession is partitioned by day. Without a window the lookup
3607
+ * scanned every partition the project ever wrote, on every exception
3608
+ * page load; a 30-day default covers every retention tier a recording
3609
+ * can still be played under.
3610
+ */
3611
+ test("applies a default window when the caller gives none", async () => {
3612
+ const principal: {
3613
+ request: JSONObject;
3614
+ databaseProps: DatabaseCommonInteractionProps;
3615
+ } = buildPrincipal({
3616
+ projectId: projectId,
3617
+ userId: userId,
3618
+ permissions: [Permission.ProjectOwner],
3619
+ });
3620
+
3621
+ mockProps(principal.databaseProps);
3622
+
3623
+ await callRoute({
3624
+ uri: FOR_EXCEPTION_ROUTE,
3625
+ request: principal.request,
3626
+ body: { fingerprint: "fp-1" },
3627
+ });
3628
+
3629
+ const statement: Statement = headerQuerySpy.mock
3630
+ .calls[0]![0] as Statement;
3631
+ expect(statement.query).toContain("startTime >= ");
3632
+ expect(statement.query).toContain("startTime <= ");
3633
+ });
3634
+
3635
+ test("derives the window from the error's own time when the caller sends it", async () => {
3636
+ const principal: {
3637
+ request: JSONObject;
3638
+ databaseProps: DatabaseCommonInteractionProps;
3639
+ } = buildPrincipal({
3640
+ projectId: projectId,
3641
+ userId: userId,
3642
+ permissions: [Permission.ProjectOwner],
3643
+ });
3644
+
3645
+ mockProps(principal.databaseProps);
3646
+
3647
+ const errorTimeUnixMs: number = Date.UTC(2026, 7, 5, 12, 0, 0);
3648
+
3649
+ await callRoute({
3650
+ uri: FOR_EXCEPTION_ROUTE,
3651
+ request: principal.request,
3652
+ body: { fingerprint: "fp-1", errorTimeUnixMs: errorTimeUnixMs },
3653
+ });
3654
+
3655
+ const statement: Statement = headerQuerySpy.mock
3656
+ .calls[0]![0] as Statement;
3657
+ const bound: Array<unknown> = Object.values(statement.query_params);
3658
+
3659
+ /*
3660
+ * The bound DateTime64 strings carry the window: the start sits one
3661
+ * maximum session length (plus padding) before the error.
3662
+ */
3663
+ const earliestStart: Date = new Date(
3664
+ errorTimeUnixMs - SESSION_REPLAY_MAX_SESSION_MS - 5 * 60 * 1000,
3665
+ );
3666
+ const boundStrings: Array<string> = bound.filter(
3667
+ (value: unknown): value is string => {
3668
+ return typeof value === "string";
3669
+ },
3670
+ );
3671
+ expect(
3672
+ boundStrings.some((value: string): boolean => {
3673
+ return value.startsWith(
3674
+ earliestStart.toISOString().substring(0, 19).replace("T", " "),
3675
+ );
3676
+ }),
3677
+ ).toBe(true);
3678
+ });
3679
+
3680
+ /*
3681
+ * The header's fingerprint list is written by the finalizer, 10+
3682
+ * minutes after the session goes quiet - which is the whole incident
3683
+ * from the reporter's point of view. The exception instance table
3684
+ * knows the session id from the moment the error is ingested, so a
3685
+ * live session is found through it.
3686
+ */
3687
+ test("finds a live session through the exception instances before the finalizer has run", async () => {
3688
+ const principal: {
3689
+ request: JSONObject;
3690
+ databaseProps: DatabaseCommonInteractionProps;
3691
+ } = buildPrincipal({
3692
+ projectId: projectId,
3693
+ userId: userId,
3694
+ permissions: [Permission.ProjectOwner],
3695
+ });
3696
+
3697
+ mockProps(principal.databaseProps);
3698
+ exceptionQuerySpy.mockResolvedValue(
3699
+ fakeResultSet([{ sessionId: "live-session" }]) as never,
3700
+ );
3701
+
3702
+ await callRoute({
3703
+ uri: FOR_EXCEPTION_ROUTE,
3704
+ request: principal.request,
3705
+ body: { fingerprint: "fp-1" },
3706
+ });
3707
+
3708
+ const instanceStatement: Statement = exceptionQuerySpy.mock
3709
+ .calls[0]![0] as Statement;
3710
+ expect(instanceStatement.query).toContain("fingerprint = ");
3711
+ expect(instanceStatement.query).toContain("sessionId != ''");
3712
+
3713
+ const headerStatement: Statement = headerQuerySpy.mock
3714
+ .calls[0]![0] as Statement;
3715
+ expect(headerStatement.query).toContain("OR sessionId IN (");
3716
+ expect(Object.values(headerStatement.query_params)).toContainEqual([
3717
+ "live-session",
3718
+ ]);
3719
+ });
3720
+
3721
+ test("a pinned sessionId and a non-integer limit are handled before the query", async () => {
3722
+ const principal: {
3723
+ request: JSONObject;
3724
+ databaseProps: DatabaseCommonInteractionProps;
3725
+ } = buildPrincipal({
3726
+ projectId: projectId,
3727
+ userId: userId,
3728
+ permissions: [Permission.ProjectOwner],
3729
+ });
3730
+
3731
+ mockProps(principal.databaseProps);
3732
+ exceptionQuerySpy.mockResolvedValue(
3733
+ fakeResultSet([{ sessionId: "s-9" }]) as never,
3734
+ );
3735
+
3736
+ await callRoute({
3737
+ uri: FOR_EXCEPTION_ROUTE,
3738
+ request: principal.request,
3739
+ body: { fingerprint: "fp-1", sessionId: "s-9" },
3740
+ });
3741
+
3742
+ /*
3743
+ * The pin NARROWS the instance lookup rather than replacing it.
3744
+ * Returning the pinned id unchecked reduced the header statement to
3745
+ * `sessionId = X AND (hasAny(fingerprints, [f]) OR sessionId IN (X))`,
3746
+ * whose second arm is trivially true - so the card would claim any
3747
+ * accessible session had observed this exception.
3748
+ */
3749
+ const instanceStatement: Statement = exceptionQuerySpy.mock
3750
+ .calls[0]![0] as Statement;
3751
+ expect(instanceStatement.query).toContain("fingerprint = ");
3752
+ expect(instanceStatement.query).toContain("AND sessionId = ");
3753
+ expect(Object.values(instanceStatement.query_params)).toContain("s-9");
3754
+
3755
+ const statement: Statement = headerQuerySpy.mock
3756
+ .calls[0]![0] as Statement;
3757
+ expect(statement.query).toContain("AND sessionId = ");
3758
+ expect(Object.values(statement.query_params)).toContain("s-9");
3759
+
3760
+ jest.clearAllMocks();
3761
+ mockProps(principal.databaseProps);
3762
+
3763
+ const bad: CallResult = await callRoute({
3764
+ uri: FOR_EXCEPTION_ROUTE,
3765
+ request: principal.request,
3766
+ body: { fingerprint: "fp-1", limit: 2.5 },
3767
+ });
3768
+
3769
+ expect(bad.thrownToNext).toBeInstanceOf(BadDataException);
3770
+ expect(headerQuerySpy).not.toHaveBeenCalled();
3771
+ });
2210
3772
  });
2211
3773
 
2212
3774
  describe("ingest-status", () => {
@@ -2321,5 +3883,167 @@ describe("Session replay playback API", () => {
2321
3883
  expect(body["applicationBytesUsedThisMonth"]).toBeNull();
2322
3884
  expect(body["dailyByteLimit"]).toBeGreaterThan(0);
2323
3885
  });
3886
+
3887
+ /*
3888
+ * The full RecordingHealthStatus. Every counter that could not be
3889
+ * read is null - the diagnosis renders "unknown" - and every
3890
+ * timestamp is ISO or null. Jest runs without Redis, so the refusal
3891
+ * and drop counters are the null case here; the activity summary is
3892
+ * ClickHouse and is mocked.
3893
+ */
3894
+ test("carries the policy, liveness stamps and activity summary, with unreadable counters as null", async () => {
3895
+ const principal: {
3896
+ request: JSONObject;
3897
+ databaseProps: DatabaseCommonInteractionProps;
3898
+ } = buildPrincipal({
3899
+ projectId: projectId,
3900
+ userId: userId,
3901
+ permissions: [Permission.ProjectOwner],
3902
+ });
3903
+
3904
+ mockProps(principal.databaseProps);
3905
+
3906
+ const application: RumApplication = new RumApplication();
3907
+ application.id = applicationAId;
3908
+ application.projectId = projectId;
3909
+ application.labels = [];
3910
+ application.appIdentifier = "checkout-web";
3911
+ application.isSessionReplayEnabled = true;
3912
+ application.sessionReplayAllowedOrigins = [];
3913
+ application.sessionReplaySamplePercentage = 100;
3914
+ application.sessionReplayRetentionInDays = 14;
3915
+ application.lastSeenAt = new Date("2026-08-05T09:14:00.000Z");
3916
+ (application as unknown as JSONObject)["sessionReplayConsentMode"] =
3917
+ "NotRequired";
3918
+ (application as unknown as JSONObject)["sessionReplayMaskingMode"] =
3919
+ "MaskAllText";
3920
+ findOneBySpy.mockResolvedValue(application);
3921
+ mockProject(true);
3922
+
3923
+ headerQuerySpy
3924
+ .mockResolvedValueOnce(
3925
+ fakeResultSet([{ sessionCount: 143, unplayableCount: 3 }]) as never,
3926
+ )
3927
+ .mockResolvedValueOnce(
3928
+ fakeResultSet([
3929
+ { lastStartUnixMs: Date.UTC(2026, 7, 5, 9, 0) },
3930
+ ]) as never,
3931
+ );
3932
+
3933
+ const result: CallResult = await callRoute({
3934
+ uri: INGEST_STATUS_ROUTE,
3935
+ request: principal.request,
3936
+ body: { rumApplicationId: applicationAId.toString() },
3937
+ });
3938
+
3939
+ const body: JSONObject = result.jsonBody as JSONObject;
3940
+
3941
+ expect(body["consentMode"]).toBe("NotRequired");
3942
+ expect(body["maskingMode"]).toBe("MaskAllText");
3943
+ expect(body["retentionInDays"]).toBe(14);
3944
+ expect(body["lastConfigFetchAt"]).toBe("2026-08-05T09:14:00.000Z");
3945
+ expect(body["lastSessionStartedAt"]).toBe("2026-08-05T09:00:00.000Z");
3946
+ expect(body["sessionsLast24h"]).toBe(143);
3947
+ expect(body["playableSessionsLast24h"]).toBe(140);
3948
+ /* No recorder manifest reader registered, no Redis: unknown, not 0. */
3949
+ expect(body["publishedRecorderVersion"]).toBeNull();
3950
+ expect(body["refusalsLast24h"]).toBeNull();
3951
+ expect(body["dropsLast24h"]).toBeNull();
3952
+ /* The pre-existing fields are untouched. */
3953
+ expect(body["isProjectAllowed"]).toBe(true);
3954
+ expect(body["appIdentifier"]).toBe("checkout-web");
3955
+ });
3956
+
3957
+ test("reports the published recorder version from the registered reader, and never-seen stamps as null", async () => {
3958
+ const principal: {
3959
+ request: JSONObject;
3960
+ databaseProps: DatabaseCommonInteractionProps;
3961
+ } = buildPrincipal({
3962
+ projectId: projectId,
3963
+ userId: userId,
3964
+ permissions: [Permission.ProjectOwner],
3965
+ });
3966
+
3967
+ mockProps(principal.databaseProps);
3968
+ mockConfiguredApplication();
3969
+ mockProject(true);
3970
+
3971
+ SessionReplayReadService.setPublishedRecorderVersionProvider(
3972
+ (): string | null => {
3973
+ return "3.1.0";
3974
+ },
3975
+ );
3976
+
3977
+ /* The activity summary read fails: unknown, never zero. */
3978
+ headerQuerySpy.mockRejectedValue(
3979
+ new Error("clickhouse timeout") as never,
3980
+ );
3981
+
3982
+ const result: CallResult = await callRoute({
3983
+ uri: INGEST_STATUS_ROUTE,
3984
+ request: principal.request,
3985
+ body: { rumApplicationId: applicationAId.toString() },
3986
+ });
3987
+
3988
+ const body: JSONObject = result.jsonBody as JSONObject;
3989
+
3990
+ expect(body["publishedRecorderVersion"]).toBe("3.1.0");
3991
+ expect(body["lastConfigFetchAt"]).toBeNull();
3992
+ expect(body["lastSessionStartedAt"]).toBeNull();
3993
+ expect(body["sessionsLast24h"]).toBeNull();
3994
+ expect(body["playableSessionsLast24h"]).toBeNull();
3995
+ expect(body["retentionInDays"]).toBeNull();
3996
+ expect(body["recorderCapabilities"]).toBeNull();
3997
+ });
3998
+
3999
+ /*
4000
+ * The health card and the installation test both point an operator at
4001
+ * "the capabilities of the newest recorder that reported" - the one
4002
+ * way to spot a stale cached artifact ("click labels: no") without
4003
+ * opening a recording, which writes an audit row. The route never sent
4004
+ * the field, so both surfaces said "not reported yet" for every
4005
+ * application forever, which reads as a bug rather than as
4006
+ * information.
4007
+ */
4008
+ test("reports the newest session's recorder capabilities, filtered to the known vocabulary", async () => {
4009
+ const principal: {
4010
+ request: JSONObject;
4011
+ databaseProps: DatabaseCommonInteractionProps;
4012
+ } = buildPrincipal({
4013
+ projectId: projectId,
4014
+ userId: userId,
4015
+ permissions: [Permission.ProjectOwner],
4016
+ });
4017
+
4018
+ mockProps(principal.databaseProps);
4019
+ mockConfiguredApplication();
4020
+ mockProject(true);
4021
+
4022
+ headerQuerySpy
4023
+ .mockResolvedValueOnce(
4024
+ fakeResultSet([{ sessionCount: 2, unplayableCount: 0 }]) as never,
4025
+ )
4026
+ .mockResolvedValueOnce(
4027
+ fakeResultSet([
4028
+ {
4029
+ lastStartUnixMs: Date.UTC(2026, 7, 5, 9, 0),
4030
+ aggAttributes: {
4031
+ "recorder.capabilities": "click-events,tags,made-up",
4032
+ },
4033
+ },
4034
+ ]) as never,
4035
+ );
4036
+
4037
+ const result: CallResult = await callRoute({
4038
+ uri: INGEST_STATUS_ROUTE,
4039
+ request: principal.request,
4040
+ body: { rumApplicationId: applicationAId.toString() },
4041
+ });
4042
+
4043
+ expect((result.jsonBody as JSONObject)["recorderCapabilities"]).toEqual([
4044
+ "click-events",
4045
+ "tags",
4046
+ ]);
4047
+ });
2324
4048
  });
2325
4049
  });