@oneuptime/common 14.0.2 → 14.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (765) hide show
  1. package/Models/DatabaseModels/Alert.ts +55 -0
  2. package/Models/DatabaseModels/AlertFeed.ts +2 -0
  3. package/Models/DatabaseModels/DatabaseServer.ts +1650 -0
  4. package/Models/DatabaseModels/DatabaseServerEndpoint.ts +540 -0
  5. package/Models/DatabaseModels/DatabaseServerFeed.ts +625 -0
  6. package/Models/DatabaseModels/DatabaseServerLabelRule.ts +514 -0
  7. package/Models/DatabaseModels/DatabaseServerOwnerRule.ts +596 -0
  8. package/Models/DatabaseModels/DatabaseServerOwnerTeam.ts +491 -0
  9. package/Models/DatabaseModels/DatabaseServerOwnerUser.ts +490 -0
  10. package/Models/DatabaseModels/Incident.ts +55 -0
  11. package/Models/DatabaseModels/IncidentAlert.ts +540 -0
  12. package/Models/DatabaseModels/IncidentFeed.ts +2 -0
  13. package/Models/DatabaseModels/Index.ts +18 -0
  14. package/Models/DatabaseModels/NetworkDeviceDiscoveryScan.ts +26 -0
  15. package/Models/DatabaseModels/Project.ts +60 -0
  16. package/Models/DatabaseModels/RumApplication.ts +52 -1
  17. package/Models/DatabaseModels/ScheduledMaintenance.ts +55 -0
  18. package/Models/DatabaseModels/TelemetryException.ts +2 -0
  19. package/Models/DatabaseModels/UserProjectSsoConsent.ts +146 -0
  20. package/Server/Infrastructure/Postgres/SchemaMigrations/1794800000000-AddSessionReplaySameOriginTracePropagation.ts +28 -0
  21. package/Server/Infrastructure/Postgres/SchemaMigrations/1794900000000-AddIncidentAlert.ts +89 -0
  22. package/Server/Infrastructure/Postgres/SchemaMigrations/1795000000000-AddDatabaseServerTables.ts +673 -0
  23. package/Server/Infrastructure/Postgres/SchemaMigrations/1795100000000-AddUserProjectSsoConsent.ts +51 -0
  24. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
  25. package/Server/Middleware/MultipartFormData.ts +1 -1
  26. package/Server/Middleware/TelemetryIngest.ts +15 -8
  27. package/Server/Services/AlertReminderRuleService.ts +100 -24
  28. package/Server/Services/DatabaseServerEndpointService.ts +895 -0
  29. package/Server/Services/DatabaseServerFeedService.ts +128 -0
  30. package/Server/Services/DatabaseServerLabelRuleEngineService.ts +340 -0
  31. package/Server/Services/DatabaseServerLabelRuleService.ts +14 -0
  32. package/Server/Services/DatabaseServerOwnerRuleEngineService.ts +425 -0
  33. package/Server/Services/DatabaseServerOwnerRuleService.ts +14 -0
  34. package/Server/Services/DatabaseServerOwnerTeamService.ts +180 -0
  35. package/Server/Services/DatabaseServerOwnerUserService.ts +173 -0
  36. package/Server/Services/DatabaseServerService.ts +4546 -0
  37. package/Server/Services/DatabaseService.ts +14 -8
  38. package/Server/Services/DockerResourceService.ts +43 -2
  39. package/Server/Services/IncidentAlertService.ts +1811 -0
  40. package/Server/Services/IncidentReminderRuleService.ts +103 -23
  41. package/Server/Services/IncidentService.ts +210 -10
  42. package/Server/Services/IncidentStateTimelineService.ts +24 -0
  43. package/Server/Services/Index.ts +18 -0
  44. package/Server/Services/InventoryItemService.ts +113 -5
  45. package/Server/Services/IoTDeviceService.ts +145 -37
  46. package/Server/Services/OpenTelemetryIngestService.ts +19 -0
  47. package/Server/Services/PodmanResourceService.ts +43 -2
  48. package/Server/Services/ScheduledMaintenanceReminderRuleService.ts +107 -23
  49. package/Server/Services/ServiceService.ts +1 -1
  50. package/Server/Services/TelemetryUsageBillingService.ts +29 -2
  51. package/Server/Services/UserProjectSsoConsentService.ts +74 -0
  52. package/Server/Services/UserService.ts +14 -1
  53. package/Server/Types/Database/Permissions/OwnerTableRegistry.ts +18 -0
  54. package/Server/Types/Database/QueryHelper.ts +30 -1
  55. package/Server/Utils/AI/Toolbox/ResourceTools.ts +69 -1
  56. package/Server/Utils/Database/AffectedResourceRelations.ts +6 -0
  57. package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +188 -6
  58. package/Server/Utils/Monitor/MonitorAlert.ts +28 -1
  59. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +2262 -755
  60. package/Server/Utils/Monitor/MonitorIncident.ts +28 -1
  61. package/Server/Utils/Monitor/MonitorMaintenanceSuppression.ts +268 -6
  62. package/Server/Utils/Monitor/MonitorResource.ts +47 -11
  63. package/Server/Utils/Monitor/MonitorResourceContext.ts +1 -0
  64. package/Server/Utils/Monitor/MonitorStepResourceIdentity.ts +239 -21
  65. package/Server/Utils/Monitor/PerSeriesResolutionRootCause.ts +43 -0
  66. package/Server/Utils/Monitor/SeriesResourceLabels.ts +291 -0
  67. package/Server/Utils/Monitor/SeriesResourceLinker.ts +111 -12
  68. package/Server/Utils/Rules/RuleRun/RuleRunRegistry.ts +25 -0
  69. package/Server/Utils/SessionReplay/SessionReplayGateCache.ts +24 -2
  70. package/Server/Utils/StartServer.ts +26 -6
  71. package/Server/Utils/TeamMember/ProjectLeaveResourceCleanup.ts +2 -0
  72. package/Server/Utils/Telemetry/DatabaseEndpointDiscovery.ts +688 -0
  73. package/Server/Utils/Telemetry/EntityRegistry.ts +5 -1
  74. package/Server/Utils/Telemetry/LogExceptionExtractor.ts +18 -1
  75. package/Server/Utils/Telemetry/ResourceEntityFilter.ts +175 -12
  76. package/Server/Utils/Telemetry/ResourceFacetResolver.ts +12 -0
  77. package/Server/Utils/Telemetry/ServiceDependencyDiscovery.ts +606 -36
  78. package/Server/Utils/Telemetry/Telemetry.ts +16 -0
  79. package/Server/Utils/Telemetry/TelemetryEntity.ts +22 -22
  80. package/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.ts +16 -10
  81. package/Tests/App/Accounts/SignedInRedirect.test.tsx +241 -0
  82. package/Tests/App/AdminDashboard/AdminHeaderSmallScreens.test.tsx +221 -4
  83. package/Tests/App/Dashboard/AIInsightsResourcePageContext.test.ts +42 -0
  84. package/Tests/App/Dashboard/AffectedResourcesDisplay.test.tsx +100 -0
  85. package/Tests/App/Dashboard/AffectedResourcesPicker.test.tsx +86 -0
  86. package/Tests/App/Dashboard/AffectedResourcesPickerHiddenTypes.test.tsx +2 -0
  87. package/Tests/App/Dashboard/AffectedResourcesSummaryStep.test.tsx +19 -0
  88. package/Tests/App/Dashboard/BreadcrumbCoverage.test.tsx +14 -0
  89. package/Tests/App/Dashboard/BulkIncidentLinkActions.test.tsx +869 -0
  90. package/Tests/App/Dashboard/CephMetricTooltips.test.tsx +1777 -0
  91. package/Tests/App/Dashboard/CloudFleetSummary.test.ts +8 -0
  92. package/Tests/App/Dashboard/CloudMetricTooltips.test.tsx +887 -0
  93. package/Tests/App/Dashboard/ContainerHostMetricTooltips.test.tsx +1309 -0
  94. package/Tests/App/Dashboard/CorrelateFilterBuilder.test.tsx +26 -1
  95. package/Tests/App/Dashboard/DashboardAppSsoError.test.tsx +501 -0
  96. package/Tests/App/Dashboard/DashboardHeaderRequests.test.tsx +64 -1
  97. package/Tests/App/Dashboard/DashboardHeaderSmallScreens.test.tsx +178 -18
  98. package/Tests/App/Dashboard/DashboardMasterPageSsoRedirect.test.tsx +444 -0
  99. package/Tests/App/Dashboard/DashboardNavBarForeignHiddenRule.test.tsx +573 -0
  100. package/Tests/App/Dashboard/DatabaseChartAxes.test.tsx +431 -0
  101. package/Tests/App/Dashboard/DatabaseContainerDetailBadge.test.tsx +249 -0
  102. package/Tests/App/Dashboard/DatabaseDashboardE2EFixes.test.ts +681 -0
  103. package/Tests/App/Dashboard/DatabaseDocumentationMarkdown.test.ts +1801 -0
  104. package/Tests/App/Dashboard/DatabaseFleetSummaryRefresh.test.tsx +280 -0
  105. package/Tests/App/Dashboard/DatabaseFormHelpers.test.tsx +321 -0
  106. package/Tests/App/Dashboard/DatabaseMetricMonitorLink.test.tsx +795 -0
  107. package/Tests/App/Dashboard/DatabaseOverviewHeader.test.tsx +580 -0
  108. package/Tests/App/Dashboard/DatabaseRecommendationsEngineMetrics.test.ts +270 -0
  109. package/Tests/App/Dashboard/DatabaseServerComponents.test.tsx +1486 -0
  110. package/Tests/App/Dashboard/DatabaseServerEngineMetricsFilter.test.ts +223 -0
  111. package/Tests/App/Dashboard/DatabaseServerPresentation.test.ts +435 -0
  112. package/Tests/App/Dashboard/DatabaseServerSummary.test.ts +186 -0
  113. package/Tests/App/Dashboard/DatabaseServerTelemetryQueries.test.ts +1553 -0
  114. package/Tests/App/Dashboard/DatabaseTableTruncation.test.tsx +326 -0
  115. package/Tests/App/Dashboard/DatabaseTelemetryScope.test.ts +512 -0
  116. package/Tests/App/Dashboard/DatabaseTelemetryTabs.test.tsx +774 -0
  117. package/Tests/App/Dashboard/DatabaseViewLayout.test.tsx +233 -0
  118. package/Tests/App/Dashboard/DatabaseWorkloadBadge.test.tsx +972 -0
  119. package/Tests/App/Dashboard/DiscoveryReviewInventoryRefresh.test.tsx +5 -0
  120. package/Tests/App/Dashboard/DiscoveryReviewUnnamedHostHint.test.tsx +1061 -0
  121. package/Tests/App/Dashboard/DiscoveryScanLivePage.test.tsx +39 -0
  122. package/Tests/App/Dashboard/DockerSwarmClustersListTooltips.test.tsx +414 -0
  123. package/Tests/App/Dashboard/DockerSwarmMetricTooltips.test.ts +796 -0
  124. package/Tests/App/Dashboard/DockerSwarmOverviewTooltips.test.tsx +747 -0
  125. package/Tests/App/Dashboard/EventOverlayScope.test.ts +147 -0
  126. package/Tests/App/Dashboard/EventOverlayScopeHook.test.tsx +1 -0
  127. package/Tests/App/Dashboard/ExceptionMonitorEnvironmentFilter.test.tsx +38 -18
  128. package/Tests/App/Dashboard/HostDetailViewTooltips.test.tsx +725 -0
  129. package/Tests/App/Dashboard/HostListColumnTooltips.test.tsx +472 -0
  130. package/Tests/App/Dashboard/HostMetricTooltips.test.ts +709 -0
  131. package/Tests/App/Dashboard/HostOverviewTooltips.test.tsx +698 -0
  132. package/Tests/App/Dashboard/HostTooltipHarness.ts +124 -0
  133. package/Tests/App/Dashboard/HostsListTooltips.test.tsx +395 -0
  134. package/Tests/App/Dashboard/IncidentAlertLinkDialog.test.tsx +691 -0
  135. package/Tests/App/Dashboard/IncidentAlertLinkHelpers.test.ts +458 -0
  136. package/Tests/App/Dashboard/IncidentAlertLinkPages.test.tsx +828 -0
  137. package/Tests/App/Dashboard/IncidentAlertLinkSideMenus.test.tsx +231 -0
  138. package/Tests/App/Dashboard/IncidentCreateFromAlerts.test.tsx +689 -0
  139. package/Tests/App/Dashboard/IoTMetricTooltips.test.tsx +1123 -0
  140. package/Tests/App/Dashboard/KubernetesClusterMetricTooltips.test.ts +837 -0
  141. package/Tests/App/Dashboard/KubernetesClusterOverviewTooltips.test.tsx +699 -0
  142. package/Tests/App/Dashboard/KubernetesContainersTabTooltips.test.tsx +320 -0
  143. package/Tests/App/Dashboard/KubernetesCostTooltips.test.tsx +502 -0
  144. package/Tests/App/Dashboard/KubernetesInsightsTooltips.test.tsx +222 -0
  145. package/Tests/App/Dashboard/KubernetesResourceMetricTooltips.test.ts +561 -0
  146. package/Tests/App/Dashboard/KubernetesResourcePagesTooltips.test.tsx +1168 -0
  147. package/Tests/App/Dashboard/LinkIncidentAlertModal.test.tsx +154 -0
  148. package/Tests/App/Dashboard/LogsResourceFacetChips.test.tsx +1 -0
  149. package/Tests/App/Dashboard/MetricDescriptionRules.ts +170 -0
  150. package/Tests/App/Dashboard/MetricDescriptionsCatalog.test.ts +206 -0
  151. package/Tests/App/Dashboard/MetricDescriptionsConsistency.test.ts +275 -0
  152. package/Tests/App/Dashboard/MetricsViewerDrillDown.test.tsx +310 -0
  153. package/Tests/App/Dashboard/MetricsViewerEntityScopedRows.test.tsx +495 -0
  154. package/Tests/App/Dashboard/MonitorCreateFromMonitorBackedDevice.test.tsx +132 -1
  155. package/Tests/App/Dashboard/NavigationSearchKeywords.test.tsx +5 -0
  156. package/Tests/App/Dashboard/NetworkDeviceTooltips.test.tsx +852 -0
  157. package/Tests/App/Dashboard/NetworkDiagnosticsTooltips.test.tsx +556 -0
  158. package/Tests/App/Dashboard/NetworkMetricTooltips.test.ts +1348 -0
  159. package/Tests/App/Dashboard/NetworkSiteTooltips.test.tsx +679 -0
  160. package/Tests/App/Dashboard/ProductOrientation.test.tsx +4 -3
  161. package/Tests/App/Dashboard/ProxmoxMetricTooltips.test.ts +592 -0
  162. package/Tests/App/Dashboard/ProxmoxOverviewTooltips.test.tsx +1002 -0
  163. package/Tests/App/Dashboard/ProxmoxResourcePageTooltips.test.tsx +923 -0
  164. package/Tests/App/Dashboard/RecommendationsLoadedContext.test.tsx +349 -0
  165. package/Tests/App/Dashboard/ResourceArchiveSettings.test.tsx +62 -0
  166. package/Tests/App/Dashboard/ResourceFeedPageWiring.test.ts +8 -1
  167. package/Tests/App/Dashboard/ResourceFeedPagesFilterSort.test.tsx +14 -0
  168. package/Tests/App/Dashboard/ResourceOverviewTileTooltips.test.tsx +1066 -0
  169. package/Tests/App/Dashboard/RumOverviewPage.test.tsx +1195 -0
  170. package/Tests/App/Dashboard/ScheduledMaintenanceOverviewPage.test.tsx +2 -0
  171. package/Tests/App/Dashboard/ServerlessMetricTooltips.test.tsx +474 -0
  172. package/Tests/App/Dashboard/ServiceMapDatabaseLink.test.tsx +1085 -0
  173. package/Tests/App/Dashboard/ServiceMetricTooltips.test.tsx +772 -0
  174. package/Tests/App/Dashboard/SessionReplayAuditTable.test.tsx +56 -1
  175. package/Tests/App/Dashboard/TelemetryMetricsSignals.test.ts +1207 -0
  176. package/Tests/App/Dashboard/TelemetryResourceRetentionSettings.test.tsx +10 -0
  177. package/Tests/App/Dashboard/TelemetryResourceTabTooltips.test.tsx +437 -0
  178. package/Tests/App/Dashboard/TracesResourceFacets.test.tsx +2 -1
  179. package/Tests/App/Dashboard/VMwareMetricTooltips.test.ts +801 -0
  180. package/Tests/App/Dashboard/VMwareOverviewTooltips.test.tsx +601 -0
  181. package/Tests/App/Dashboard/VMwareResourcePagesTooltips.test.tsx +646 -0
  182. package/Tests/App/Dashboard/VMwareTooltipHarness.ts +145 -0
  183. package/Tests/App/StatusPage/StatusPageHeaderForeignHiddenRule.test.tsx +284 -0
  184. package/Tests/ClassStrings.ts +178 -0
  185. package/Tests/Fixtures/MobilePasskeyUser.json +1 -2
  186. package/Tests/ForeignHiddenRuleGuard.ts +2152 -0
  187. package/Tests/Models/DatabaseModels/DatabaseServerModels.test.ts +2250 -0
  188. package/Tests/Models/DatabaseModels/IncidentAlertModel.test.ts +554 -0
  189. package/Tests/Models/DatabaseModels/ResourceFeedModels.test.ts +24 -5
  190. package/Tests/Models/DatabaseModels/SessionReplayModels.test.ts +117 -0
  191. package/Tests/Models/DatabaseModels/StaticRuleCriteriaInheritance.test.ts +4 -2
  192. package/Tests/RenderedMarkup.ts +518 -0
  193. package/Tests/ResponsiveVisibility.test.ts +490 -4
  194. package/Tests/ResponsiveVisibility.ts +505 -52
  195. package/Tests/Server/Infrastructure/Postgres/AddConfigurableRuleCriteriaMigration.test.ts +3 -0
  196. package/Tests/Server/Infrastructure/Postgres/AddDatabaseServerTablesMigration.test.ts +541 -0
  197. package/Tests/Server/Infrastructure/Postgres/AddIncidentAlertMigration.test.ts +341 -0
  198. package/Tests/Server/Infrastructure/Postgres/AddSessionReplaySameOriginTracePropagationMigration.test.ts +247 -0
  199. package/Tests/Server/Infrastructure/Postgres/AddUserProjectSsoConsentMigration.test.ts +234 -0
  200. package/Tests/Server/Services/ContainerSnapshotUpsertPostgres.test.ts +319 -0
  201. package/Tests/Server/Services/CrossProjectRelationWriteGuard.test.ts +13 -0
  202. package/Tests/Server/Services/DatabaseServerEndpointService.test.ts +2033 -0
  203. package/Tests/Server/Services/DatabaseServerFeedAndOwnerServices.test.ts +529 -0
  204. package/Tests/Server/Services/DatabaseServerLifecycle.test.ts +3302 -0
  205. package/Tests/Server/Services/DatabaseServerRegistries.test.ts +603 -0
  206. package/Tests/Server/Services/DatabaseServerRuleEngines.test.ts +754 -0
  207. package/Tests/Server/Services/DatabaseServerService.test.ts +3558 -0
  208. package/Tests/Server/Services/DatabaseServerSqlPostgres.test.ts +2045 -0
  209. package/Tests/Server/Services/DatabaseServiceUpdateColumnsIfUnlockedPostgres.test.ts +362 -0
  210. package/Tests/Server/Services/DatabaseServiceUpdateColumnsIfUnlockedReply.test.ts +210 -0
  211. package/Tests/Server/Services/HookFreeWriteLengthClamp.test.ts +18 -0
  212. package/Tests/Server/Services/IncidentAlertPostgres.test.ts +903 -0
  213. package/Tests/Server/Services/IncidentAlertRegistration.test.ts +96 -0
  214. package/Tests/Server/Services/IncidentAlertService.test.ts +2225 -0
  215. package/Tests/Server/Services/IncidentAlertSloLinkSurvivesEdit.test.ts +2 -0
  216. package/Tests/Server/Services/IncidentAlertStateCascade.test.ts +970 -0
  217. package/Tests/Server/Services/IncidentCreateFromAlerts.test.ts +1284 -0
  218. package/Tests/Server/Services/IncomingCallPolicyEscalationRuleTarget.test.ts +225 -0
  219. package/Tests/Server/Services/InventoryItemDisplayName.test.ts +280 -2
  220. package/Tests/Server/Services/InventoryUpsertSchemaParity.test.ts +8 -2
  221. package/Tests/Server/Services/IoTDeviceLatestMetricsPostgres.test.ts +689 -0
  222. package/Tests/Server/Services/IoTDeviceService.test.ts +379 -0
  223. package/Tests/Server/Services/LatestMetricsValuesShape.test.ts +360 -0
  224. package/Tests/Server/Services/LlmModelPriceService.test.ts +402 -0
  225. package/Tests/Server/Services/OpenTelemetryResourceRetentionMemo.test.ts +7 -1
  226. package/Tests/Server/Services/OwnerRowUniqueness.test.ts +61 -9
  227. package/Tests/Server/Services/ReminderRuleLookupPostgres.test.ts +2250 -0
  228. package/Tests/Server/Services/ReminderRuleRefreshOpenSubjectsPaging.test.ts +653 -0
  229. package/Tests/Server/Services/ResourceUpdateLastSeenLivenessFallback.test.ts +68 -20
  230. package/Tests/Server/Services/StaticRuleCriteriaRuntimeCoverage.test.ts +3 -3
  231. package/Tests/Server/Services/TelemetryResourceFacetCounting.test.ts +9 -0
  232. package/Tests/Server/Services/UserProjectSsoConsentPostgres.test.ts +153 -0
  233. package/Tests/Server/Services/UserProjectSsoConsentService.test.ts +192 -0
  234. package/Tests/Server/Services/UserServiceFirstMasterAdminElection.test.ts +71 -0
  235. package/Tests/Server/Services/UserSignupIdTakeoverPostgres.test.ts +238 -0
  236. package/Tests/Server/Services/WorkspaceNotificationSummaryHelpers.test.ts +592 -0
  237. package/Tests/Server/Types/Billing/PayAsYouGoMeteredAuthorization.test.ts +3 -0
  238. package/Tests/Server/Types/Database/EventOverlayScopePostgres.test.ts +1 -0
  239. package/Tests/Server/Types/Database/FeedEventTypeFilterQuery.test.ts +11 -0
  240. package/Tests/Server/Types/Database/Permissions/InfrastructureOwnershipScope.test.ts +8 -0
  241. package/Tests/Server/Types/Database/QueryHelperBooleanForCriteriaBackedRule.test.ts +1094 -0
  242. package/Tests/Server/Utils/AI/ResourceTools.test.ts +134 -0
  243. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.test.ts +41 -0
  244. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteriaAnomalyUnits.test.ts +279 -9
  245. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteriaDisplayContext.test.ts +933 -0
  246. package/Tests/Server/Utils/Monitor/DatabaseMaintenanceSuppression.test.ts +582 -0
  247. package/Tests/Server/Utils/Monitor/DatabaseServerMonitorLinking.test.ts +753 -0
  248. package/Tests/Server/Utils/Monitor/MonitorActionTimestamps.test.ts +1 -0
  249. package/Tests/Server/Utils/Monitor/MonitorAlertCriteriaReferences.test.ts +1 -0
  250. package/Tests/Server/Utils/Monitor/MonitorAlertResourceLinking.test.ts +119 -0
  251. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluator.test.ts +60 -25
  252. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorAffectedResourceEdgeCases.test.ts +1719 -0
  253. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorKubernetesAffectedResources.test.ts +1447 -0
  254. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorKubernetesRatioEmail.test.ts +352 -0
  255. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorPlatformMetricUnits.test.ts +717 -0
  256. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorPlatformRawScan.test.ts +2125 -0
  257. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorPlatformSeriesAffectedResources.test.ts +1288 -0
  258. package/Tests/Server/Utils/Monitor/MonitorDependencySuppressionCreatorSkip.test.ts +1 -0
  259. package/Tests/Server/Utils/Monitor/MonitorIncidentCriteriaReferences.test.ts +1 -0
  260. package/Tests/Server/Utils/Monitor/MonitorIncidentMemberRoleMembership.test.ts +1 -0
  261. package/Tests/Server/Utils/Monitor/MonitorIncidentResourceLinking.test.ts +88 -0
  262. package/Tests/Server/Utils/Monitor/MonitorMaintenanceSuppression.test.ts +205 -0
  263. package/Tests/Server/Utils/Monitor/MonitorResourceContext.test.ts +62 -0
  264. package/Tests/Server/Utils/Monitor/MonitorResourceDatabaseMaintenance.test.ts +803 -0
  265. package/Tests/Server/Utils/Monitor/MonitorStepResourceIdentity.test.ts +133 -3
  266. package/Tests/Server/Utils/Monitor/MonitorSummaryPersistence.test.ts +1 -0
  267. package/Tests/Server/Utils/Monitor/PerSeriesRecoveryResolution.test.ts +472 -1
  268. package/Tests/Server/Utils/Monitor/PerSeriesSecondEntityAlerting.test.ts +1 -0
  269. package/Tests/Server/Utils/Monitor/SeriesResourceLinker.test.ts +236 -2
  270. package/Tests/Server/Utils/SeriesResourceLabels.test.ts +70 -0
  271. package/Tests/Server/Utils/SessionReplay/SessionReplayGateCachePolicy.test.ts +103 -0
  272. package/Tests/Server/Utils/StartServerProtobufContentType.test.ts +54 -0
  273. package/Tests/Server/Utils/Telemetry/DatabaseEndpointDiscovery.test.ts +1895 -0
  274. package/Tests/Server/Utils/Telemetry/EntityRegistry.test.ts +7 -0
  275. package/Tests/Server/Utils/Telemetry/LogExceptionExtractorSeverityGate.test.ts +90 -0
  276. package/Tests/Server/Utils/Telemetry/ResourceEntityFilter.test.ts +570 -0
  277. package/Tests/Server/Utils/Telemetry/ResourceFacetResolver.test.ts +97 -2
  278. package/Tests/Server/Utils/Telemetry/ServiceDependencyDiscovery.test.ts +1337 -0
  279. package/Tests/Server/Utils/Telemetry/TelemetryEntity.test.ts +295 -0
  280. package/Tests/Types/AI/AIResourceContext.test.ts +30 -0
  281. package/Tests/Types/DatabaseServer/DatabaseContainerClassifier.test.ts +3644 -0
  282. package/Tests/Types/DatabaseServer/DatabaseContainerCommand.test.ts +340 -0
  283. package/Tests/Types/DatabaseServer/DatabaseContainerCommandCorpus.ts +826 -0
  284. package/Tests/Types/DatabaseServer/DatabaseContainerCommandPostgres.test.ts +150 -0
  285. package/Tests/Types/DatabaseServer/DatabaseEndpoint.test.ts +2657 -0
  286. package/Tests/Types/DatabaseServer/DatabaseServerDiscoverySource.test.ts +64 -0
  287. package/Tests/Types/DatabaseServer/DatabaseServerEntityKeys.test.ts +952 -0
  288. package/Tests/Types/DatabaseServer/DatabaseServerMetricCatalog.test.ts +1161 -0
  289. package/Tests/Types/DatabaseServer/DatabaseSystem.test.ts +1514 -0
  290. package/Tests/Types/DatabaseServer/DatabaseTelemetryResolver.test.ts +1558 -0
  291. package/Tests/Types/Monitor/CephAlertTemplates.test.ts +255 -6
  292. package/Tests/Types/Monitor/DatabaseAlertTemplates.test.ts +2254 -0
  293. package/Tests/Types/Monitor/KubernetesAlertTemplates.test.ts +298 -0
  294. package/Tests/Types/Monitor/KubernetesMetricCatalog.test.ts +271 -0
  295. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationCatalog.test.ts +253 -9
  296. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationCoverage.test.ts +13 -5
  297. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationNotificationMode.test.ts +4 -2
  298. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationUtil.test.ts +111 -2
  299. package/Tests/Types/Monitor/Recommendation/RecommendationCriteriaBuilder.test.ts +61 -0
  300. package/Tests/Types/Monitor/SeriesContext/SeriesLabelDisplay.test.ts +114 -0
  301. package/Tests/Types/Monitor/Utils/KubernetesAgentEmittedMetrics.ts +181 -0
  302. package/Tests/Types/NetworkDevice/DiscoveredHostNamingStatus.test.ts +401 -0
  303. package/Tests/Types/Rum/WebVitals.test.ts +173 -0
  304. package/Tests/Types/Telemetry/ResourceEntityFacet.test.ts +4 -1
  305. package/Tests/Types/Telemetry/ResourceFacetCatalog.test.ts +24 -2
  306. package/Tests/UI/Components/Card.test.tsx +93 -2
  307. package/Tests/UI/Components/FeedOptions.test.ts +6 -0
  308. package/Tests/UI/Components/FeedOptionsButton.test.tsx +8 -1
  309. package/Tests/UI/Components/FieldLabelInfoCardTooltip.test.tsx +413 -0
  310. package/Tests/UI/Components/ForeignHiddenRuleResilience.test.tsx +1147 -0
  311. package/Tests/UI/Components/HeaderRightRail.test.tsx +41 -1
  312. package/Tests/UI/Components/InfoTooltip.test.tsx +242 -0
  313. package/Tests/UI/Components/LogsEntityNames.test.ts +16 -1
  314. package/Tests/UI/Components/LogsFacetSidebarHiddenFacets.test.tsx +7 -3
  315. package/Tests/UI/Components/ModelTable/BaseModelTableBulkDelete.test.tsx +207 -3
  316. package/Tests/UI/Components/ModelTable/BaseModelTableBulkDeleteVerb.test.tsx +367 -0
  317. package/Tests/UI/Components/ModelTable/BaseModelTableCreateButtonTitle.test.tsx +275 -0
  318. package/Tests/UI/Components/ResourceFacetConfigs.test.ts +2 -1
  319. package/Tests/UI/Components/RuleCriteria/RuleCriteriaModelTable.test.tsx +3 -3
  320. package/Tests/UI/Components/SrOnlyForeignRule.test.tsx +994 -0
  321. package/Tests/UI/Components/StatusPage/ResourceListSelection.test.tsx +34 -1
  322. package/Tests/UI/Components/TableHeaderTooltip.test.tsx +662 -0
  323. package/Tests/UI/Components/TelemetryFacetSidebarHiddenFacets.test.tsx +4 -1
  324. package/Tests/UI/ForeignHiddenRuleGuard.test.ts +1951 -0
  325. package/Tests/UI/Rum/PrivacySummaryCard.test.tsx +166 -9
  326. package/Tests/UI/Rum/RecordingHealthTooltips.test.tsx +447 -0
  327. package/Tests/UI/Rum/ReplayCorrelationPanel.test.tsx +164 -0
  328. package/Tests/UI/Rum/ReplayFrameCapture.test.ts +6750 -0
  329. package/Tests/UI/Rum/ReplayPlayerChrome.test.tsx +19 -1
  330. package/Tests/UI/Rum/ReplayRail.test.tsx +723 -12
  331. package/Tests/UI/Rum/ReplayRailDetail.test.tsx +86 -0
  332. package/Tests/UI/Rum/ReplayScreenshot.test.ts +2124 -0
  333. package/Tests/UI/Rum/ReplayScreenshotActions.test.tsx +3288 -0
  334. package/Tests/UI/Rum/ReplayStage.test.tsx +62 -0
  335. package/Tests/UI/Rum/ReplayStageOverlays.test.tsx +983 -2
  336. package/Tests/UI/Rum/ReplayTabSwitcher.test.tsx +19 -1
  337. package/Tests/UI/Rum/SessionReplaySettingsPolicyLoading.test.tsx +149 -0
  338. package/Tests/UI/Rum/SessionReplaySetupGuide.test.tsx +107 -6
  339. package/Tests/UI/Rum/SessionReplayUsersTableTooltips.test.tsx +330 -0
  340. package/Tests/UI/Rum/UserFlowTooltips.test.tsx +886 -0
  341. package/Tests/UI/TailwindPlayCdnForeignHiddenRule.test.ts +1005 -0
  342. package/Tests/UI/ThemeGroupHoverReveal.test.ts +847 -0
  343. package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +80 -0
  344. package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +23 -0
  345. package/Tests/UI/Utils/ModelAPISsoRedirect.test.ts +127 -0
  346. package/Tests/UI/Utils/Navigation.test.ts +102 -0
  347. package/Tests/UI/Utils/PermissionGate.test.ts +76 -0
  348. package/Tests/UI/Utils/Telemetry/TelemetryEntityNames.test.ts +23 -0
  349. package/Tests/Utils/Incident/IncidentFromAlerts.test.ts +696 -0
  350. package/Tests/Utils/LabelRuleImportExport.test.ts +5 -0
  351. package/Tests/Utils/MetricUnitUtil.test.ts +227 -2
  352. package/Tests/Utils/Monitor/MetricValueFormatter.test.ts +288 -2
  353. package/Tests/Utils/Monitor/PlatformResourceIdentityAndUnits.test.ts +1559 -0
  354. package/Tests/Utils/NetworkDiscovery/DiscoveredHostNamingDiagnosis.test.ts +2546 -0
  355. package/Tests/Utils/NetworkDiscovery/DiscoveredHostUtil.test.ts +315 -0
  356. package/Tests/Utils/NetworkDiscovery/DiscoveryReverseDnsChain.test.ts +336 -1
  357. package/Tests/Utils/NetworkDiscovery/NetbiosNameUtil.test.ts +244 -0
  358. package/Tests/Utils/Rum/SessionTraceState.test.ts +239 -0
  359. package/Tests/Utils/ValueFormatter.test.ts +50 -0
  360. package/Tests/tsconfig.json +16 -0
  361. package/Types/AI/AIResourceContext.ts +7 -0
  362. package/Types/Dashboard/DashboardTemplates.ts +2 -1
  363. package/Types/DatabaseServer/DatabaseConnectionSpan.ts +77 -0
  364. package/Types/DatabaseServer/DatabaseContainerClassifier.ts +2068 -0
  365. package/Types/DatabaseServer/DatabaseContainerCommand.ts +801 -0
  366. package/Types/DatabaseServer/DatabaseEndpoint.ts +1943 -0
  367. package/Types/DatabaseServer/DatabaseServerDiscoverySource.ts +68 -0
  368. package/Types/DatabaseServer/DatabaseServerMetricCatalog.ts +966 -0
  369. package/Types/DatabaseServer/DatabaseSystem.ts +2124 -0
  370. package/Types/DatabaseServer/DatabaseTelemetryResolver.ts +555 -0
  371. package/Types/Email/EmailTemplateType.ts +1 -0
  372. package/Types/Incident/IncidentAlertLink.ts +38 -0
  373. package/Types/Monitor/CephAlertTemplates.ts +18 -0
  374. package/Types/Monitor/DatabaseAlertTemplates.ts +2020 -0
  375. package/Types/Monitor/KubernetesAlertTemplates.ts +64 -24
  376. package/Types/Monitor/KubernetesMetricCatalog.ts +118 -19
  377. package/Types/Monitor/MetricMonitor/MetricCriteriaContext.ts +14 -0
  378. package/Types/Monitor/MetricMonitor/MetricMonitorResponse.ts +93 -10
  379. package/Types/Monitor/Recommendation/MonitorRecommendationCatalog.ts +109 -8
  380. package/Types/Monitor/Recommendation/MonitorRecommendationTypes.ts +36 -7
  381. package/Types/Monitor/Recommendation/MonitorRecommendationUtil.ts +43 -2
  382. package/Types/Monitor/Recommendation/RecommendationCriteriaBuilder.ts +41 -6
  383. package/Types/Monitor/SeriesContext/SeriesLabelDisplay.ts +26 -0
  384. package/Types/NetworkDevice/DiscoveredHostNamingStatus.ts +147 -0
  385. package/Types/Permission.ts +315 -0
  386. package/Types/Probe/ProbeApiIngestResponse.ts +8 -0
  387. package/Types/Rules/RuleCriteriaFieldRegistry.ts +10 -0
  388. package/Types/Rules/RuleRun.ts +12 -0
  389. package/Types/Rum/SessionReplay.ts +25 -6
  390. package/Types/Rum/WebVitals.ts +52 -0
  391. package/Types/Telemetry/EntityType.ts +23 -0
  392. package/Types/Telemetry/ResourceFacetCatalog.ts +14 -0
  393. package/Types/Telemetry/ServiceType.ts +8 -0
  394. package/UI/Components/Banner/Banner.tsx +1 -1
  395. package/UI/Components/Card/Card.tsx +1 -1
  396. package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +2 -2
  397. package/UI/Components/Charts/Line/LineChart.tsx +7 -0
  398. package/UI/Components/CommandPalette/CommandPalette.tsx +1 -1
  399. package/UI/Components/Date/StartAndEndDate.tsx +5 -5
  400. package/UI/Components/Detail/FieldLabel.tsx +8 -0
  401. package/UI/Components/Diagram/HorizontalStepChain.tsx +1 -1
  402. package/UI/Components/Feed/FeedOptionsButton.tsx +1 -1
  403. package/UI/Components/Forms/BasicForm.tsx +1 -1
  404. package/UI/Components/Header/Header.tsx +1 -1
  405. package/UI/Components/Header/HeaderIconDropdownButton.tsx +1 -1
  406. package/UI/Components/HeaderAlert/HeaderAlert.tsx +1 -1
  407. package/UI/Components/InfoCard/InfoCard.tsx +47 -1
  408. package/UI/Components/LogsViewer/components/FacetValueRow.tsx +1 -1
  409. package/UI/Components/LogsViewer/components/LogsFacetSidebar.tsx +1 -1
  410. package/UI/Components/Modal/ModalFooter.tsx +1 -1
  411. package/UI/Components/ModelDetail/CardModelDetail.tsx +2 -2
  412. package/UI/Components/ModelTable/BaseModelTable.tsx +99 -21
  413. package/UI/Components/ModelTable/Column.ts +5 -0
  414. package/UI/Components/Navbar/NavBar.tsx +9 -2
  415. package/UI/Components/Navbar/NavBarMenuModal.tsx +2 -2
  416. package/UI/Components/Page/Page.tsx +1 -1
  417. package/UI/Components/Pagination/Pagination.tsx +3 -3
  418. package/UI/Components/ProgressButtons/ProgressButtonItem.tsx +1 -1
  419. package/UI/Components/SideMenu/SideMenu.tsx +4 -1
  420. package/UI/Components/StatusPage/ResourceGroupNavigator.tsx +4 -2
  421. package/UI/Components/StatusPage/ResourceList.tsx +6 -6
  422. package/UI/Components/Table/TableHeader.tsx +47 -2
  423. package/UI/Components/Table/Types/Column.ts +7 -0
  424. package/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.tsx +1 -1
  425. package/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.tsx +1 -1
  426. package/UI/Components/Tooltip/InfoTooltip.tsx +78 -0
  427. package/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.tsx +1 -1
  428. package/UI/Components/Workflow/ComponentsModal.tsx +2 -2
  429. package/UI/Components/Workflow/StepTraceViewer.tsx +1 -1
  430. package/UI/Styles/Theme.css +8 -1
  431. package/UI/Utils/Navigation.ts +89 -42
  432. package/UI/Utils/PermissionGate.ts +11 -2
  433. package/UI/Utils/Telemetry/TelemetryEntityNames.ts +7 -0
  434. package/UI/Utils/TelemetryService.ts +4 -3
  435. package/Utils/Incident/IncidentFromAlerts.ts +516 -0
  436. package/Utils/LabelRuleImportExport.ts +2 -0
  437. package/Utils/MetricUnitUtil.ts +103 -11
  438. package/Utils/Monitor/MetricValueFormatter.ts +66 -13
  439. package/Utils/Monitor/PlatformMetricUnitUtil.ts +215 -0
  440. package/Utils/Monitor/PlatformResourceIdentity.ts +514 -0
  441. package/Utils/NetworkDiscovery/DiscoveredHostNamingDiagnosis.ts +425 -0
  442. package/Utils/NetworkDiscovery/DiscoveredHostUtil.ts +32 -0
  443. package/Utils/NetworkDiscovery/NetbiosNameUtil.ts +73 -0
  444. package/Utils/Rum/SessionTraceState.ts +212 -0
  445. package/Utils/Telemetry/DatabaseServerEntityKeys.ts +275 -0
  446. package/Utils/Telemetry/EntityKey.ts +176 -0
  447. package/Utils/ValueFormatter.ts +16 -0
  448. package/build/dist/Models/DatabaseModels/Alert.js +54 -0
  449. package/build/dist/Models/DatabaseModels/Alert.js.map +1 -1
  450. package/build/dist/Models/DatabaseModels/AlertFeed.js +2 -0
  451. package/build/dist/Models/DatabaseModels/AlertFeed.js.map +1 -1
  452. package/build/dist/Models/DatabaseModels/DatabaseServer.js +1683 -0
  453. package/build/dist/Models/DatabaseModels/DatabaseServer.js.map +1 -0
  454. package/build/dist/Models/DatabaseModels/DatabaseServerEndpoint.js +559 -0
  455. package/build/dist/Models/DatabaseModels/DatabaseServerEndpoint.js.map +1 -0
  456. package/build/dist/Models/DatabaseModels/DatabaseServerFeed.js +648 -0
  457. package/build/dist/Models/DatabaseModels/DatabaseServerFeed.js.map +1 -0
  458. package/build/dist/Models/DatabaseModels/DatabaseServerLabelRule.js +522 -0
  459. package/build/dist/Models/DatabaseModels/DatabaseServerLabelRule.js.map +1 -0
  460. package/build/dist/Models/DatabaseModels/DatabaseServerOwnerRule.js +603 -0
  461. package/build/dist/Models/DatabaseModels/DatabaseServerOwnerRule.js.map +1 -0
  462. package/build/dist/Models/DatabaseModels/DatabaseServerOwnerTeam.js +506 -0
  463. package/build/dist/Models/DatabaseModels/DatabaseServerOwnerTeam.js.map +1 -0
  464. package/build/dist/Models/DatabaseModels/DatabaseServerOwnerUser.js +505 -0
  465. package/build/dist/Models/DatabaseModels/DatabaseServerOwnerUser.js.map +1 -0
  466. package/build/dist/Models/DatabaseModels/Incident.js +54 -0
  467. package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
  468. package/build/dist/Models/DatabaseModels/IncidentAlert.js +553 -0
  469. package/build/dist/Models/DatabaseModels/IncidentAlert.js.map +1 -0
  470. package/build/dist/Models/DatabaseModels/IncidentFeed.js +2 -0
  471. package/build/dist/Models/DatabaseModels/IncidentFeed.js.map +1 -1
  472. package/build/dist/Models/DatabaseModels/Index.js +18 -0
  473. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  474. package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js.map +1 -1
  475. package/build/dist/Models/DatabaseModels/Project.js +62 -0
  476. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  477. package/build/dist/Models/DatabaseModels/RumApplication.js +53 -1
  478. package/build/dist/Models/DatabaseModels/RumApplication.js.map +1 -1
  479. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js +54 -0
  480. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js.map +1 -1
  481. package/build/dist/Models/DatabaseModels/TelemetryException.js +2 -0
  482. package/build/dist/Models/DatabaseModels/TelemetryException.js.map +1 -1
  483. package/build/dist/Models/DatabaseModels/UserProjectSsoConsent.js +162 -0
  484. package/build/dist/Models/DatabaseModels/UserProjectSsoConsent.js.map +1 -0
  485. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794800000000-AddSessionReplaySameOriginTracePropagation.js +20 -0
  486. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794800000000-AddSessionReplaySameOriginTracePropagation.js.map +1 -0
  487. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794900000000-AddIncidentAlert.js +42 -0
  488. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794900000000-AddIncidentAlert.js.map +1 -0
  489. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795000000000-AddDatabaseServerTables.js +240 -0
  490. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795000000000-AddDatabaseServerTables.js.map +1 -0
  491. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795100000000-AddUserProjectSsoConsent.js +26 -0
  492. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795100000000-AddUserProjectSsoConsent.js.map +1 -0
  493. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
  494. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  495. package/build/dist/Server/Middleware/MultipartFormData.js +1 -1
  496. package/build/dist/Server/Middleware/TelemetryIngest.js +15 -8
  497. package/build/dist/Server/Middleware/TelemetryIngest.js.map +1 -1
  498. package/build/dist/Server/Services/AlertReminderRuleService.js +72 -23
  499. package/build/dist/Server/Services/AlertReminderRuleService.js.map +1 -1
  500. package/build/dist/Server/Services/DatabaseServerEndpointService.js +694 -0
  501. package/build/dist/Server/Services/DatabaseServerEndpointService.js.map +1 -0
  502. package/build/dist/Server/Services/DatabaseServerFeedService.js +110 -0
  503. package/build/dist/Server/Services/DatabaseServerFeedService.js.map +1 -0
  504. package/build/dist/Server/Services/DatabaseServerLabelRuleEngineService.js +272 -0
  505. package/build/dist/Server/Services/DatabaseServerLabelRuleEngineService.js.map +1 -0
  506. package/build/dist/Server/Services/DatabaseServerLabelRuleService.js +13 -0
  507. package/build/dist/Server/Services/DatabaseServerLabelRuleService.js.map +1 -0
  508. package/build/dist/Server/Services/DatabaseServerOwnerRuleEngineService.js +340 -0
  509. package/build/dist/Server/Services/DatabaseServerOwnerRuleEngineService.js.map +1 -0
  510. package/build/dist/Server/Services/DatabaseServerOwnerRuleService.js +13 -0
  511. package/build/dist/Server/Services/DatabaseServerOwnerRuleService.js.map +1 -0
  512. package/build/dist/Server/Services/DatabaseServerOwnerTeamService.js +160 -0
  513. package/build/dist/Server/Services/DatabaseServerOwnerTeamService.js.map +1 -0
  514. package/build/dist/Server/Services/DatabaseServerOwnerUserService.js +153 -0
  515. package/build/dist/Server/Services/DatabaseServerOwnerUserService.js.map +1 -0
  516. package/build/dist/Server/Services/DatabaseServerService.js +3272 -0
  517. package/build/dist/Server/Services/DatabaseServerService.js.map +1 -0
  518. package/build/dist/Server/Services/DatabaseService.js +8 -4
  519. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  520. package/build/dist/Server/Services/DockerResourceService.js +30 -3
  521. package/build/dist/Server/Services/DockerResourceService.js.map +1 -1
  522. package/build/dist/Server/Services/IncidentAlertService.js +1370 -0
  523. package/build/dist/Server/Services/IncidentAlertService.js.map +1 -0
  524. package/build/dist/Server/Services/IncidentReminderRuleService.js +73 -23
  525. package/build/dist/Server/Services/IncidentReminderRuleService.js.map +1 -1
  526. package/build/dist/Server/Services/IncidentService.js +164 -12
  527. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  528. package/build/dist/Server/Services/IncidentStateTimelineService.js +21 -0
  529. package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
  530. package/build/dist/Server/Services/Index.js +18 -0
  531. package/build/dist/Server/Services/Index.js.map +1 -1
  532. package/build/dist/Server/Services/InventoryItemService.js +88 -5
  533. package/build/dist/Server/Services/InventoryItemService.js.map +1 -1
  534. package/build/dist/Server/Services/IoTDeviceService.js +111 -24
  535. package/build/dist/Server/Services/IoTDeviceService.js.map +1 -1
  536. package/build/dist/Server/Services/OpenTelemetryIngestService.js +24 -9
  537. package/build/dist/Server/Services/OpenTelemetryIngestService.js.map +1 -1
  538. package/build/dist/Server/Services/PodmanResourceService.js +30 -3
  539. package/build/dist/Server/Services/PodmanResourceService.js.map +1 -1
  540. package/build/dist/Server/Services/ScheduledMaintenanceReminderRuleService.js +75 -23
  541. package/build/dist/Server/Services/ScheduledMaintenanceReminderRuleService.js.map +1 -1
  542. package/build/dist/Server/Services/ServiceService.js +1 -1
  543. package/build/dist/Server/Services/ServiceService.js.map +1 -1
  544. package/build/dist/Server/Services/TelemetryUsageBillingService.js +23 -2
  545. package/build/dist/Server/Services/TelemetryUsageBillingService.js.map +1 -1
  546. package/build/dist/Server/Services/UserProjectSsoConsentService.js +79 -0
  547. package/build/dist/Server/Services/UserProjectSsoConsentService.js.map +1 -0
  548. package/build/dist/Server/Services/UserService.js +13 -1
  549. package/build/dist/Server/Services/UserService.js.map +1 -1
  550. package/build/dist/Server/Types/Database/Permissions/OwnerTableRegistry.js +18 -0
  551. package/build/dist/Server/Types/Database/Permissions/OwnerTableRegistry.js.map +1 -1
  552. package/build/dist/Server/Types/Database/QueryHelper.js +26 -1
  553. package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
  554. package/build/dist/Server/Utils/AI/Toolbox/ResourceTools.js +46 -0
  555. package/build/dist/Server/Utils/AI/Toolbox/ResourceTools.js.map +1 -1
  556. package/build/dist/Server/Utils/Database/AffectedResourceRelations.js +6 -0
  557. package/build/dist/Server/Utils/Database/AffectedResourceRelations.js.map +1 -1
  558. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +134 -13
  559. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js.map +1 -1
  560. package/build/dist/Server/Utils/Monitor/MonitorAlert.js +22 -6
  561. package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
  562. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +1489 -524
  563. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  564. package/build/dist/Server/Utils/Monitor/MonitorIncident.js +22 -6
  565. package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
  566. package/build/dist/Server/Utils/Monitor/MonitorMaintenanceSuppression.js +196 -6
  567. package/build/dist/Server/Utils/Monitor/MonitorMaintenanceSuppression.js.map +1 -1
  568. package/build/dist/Server/Utils/Monitor/MonitorResource.js +36 -9
  569. package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
  570. package/build/dist/Server/Utils/Monitor/MonitorResourceContext.js +1 -0
  571. package/build/dist/Server/Utils/Monitor/MonitorResourceContext.js.map +1 -1
  572. package/build/dist/Server/Utils/Monitor/MonitorStepResourceIdentity.js +175 -17
  573. package/build/dist/Server/Utils/Monitor/MonitorStepResourceIdentity.js.map +1 -1
  574. package/build/dist/Server/Utils/Monitor/PerSeriesResolutionRootCause.js +29 -0
  575. package/build/dist/Server/Utils/Monitor/PerSeriesResolutionRootCause.js.map +1 -0
  576. package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js +211 -0
  577. package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js.map +1 -1
  578. package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js +82 -10
  579. package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js.map +1 -1
  580. package/build/dist/Server/Utils/Rules/RuleRun/RuleRunRegistry.js +25 -0
  581. package/build/dist/Server/Utils/Rules/RuleRun/RuleRunRegistry.js.map +1 -1
  582. package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCache.js +8 -0
  583. package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCache.js.map +1 -1
  584. package/build/dist/Server/Utils/StartServer.js +20 -4
  585. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  586. package/build/dist/Server/Utils/TeamMember/ProjectLeaveResourceCleanup.js +2 -0
  587. package/build/dist/Server/Utils/TeamMember/ProjectLeaveResourceCleanup.js.map +1 -1
  588. package/build/dist/Server/Utils/Telemetry/DatabaseEndpointDiscovery.js +442 -0
  589. package/build/dist/Server/Utils/Telemetry/DatabaseEndpointDiscovery.js.map +1 -0
  590. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +5 -1
  591. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
  592. package/build/dist/Server/Utils/Telemetry/LogExceptionExtractor.js +15 -1
  593. package/build/dist/Server/Utils/Telemetry/LogExceptionExtractor.js.map +1 -1
  594. package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js +121 -5
  595. package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js.map +1 -1
  596. package/build/dist/Server/Utils/Telemetry/ResourceFacetResolver.js +12 -0
  597. package/build/dist/Server/Utils/Telemetry/ResourceFacetResolver.js.map +1 -1
  598. package/build/dist/Server/Utils/Telemetry/ServiceDependencyDiscovery.js +438 -36
  599. package/build/dist/Server/Utils/Telemetry/ServiceDependencyDiscovery.js.map +1 -1
  600. package/build/dist/Server/Utils/Telemetry/Telemetry.js +12 -0
  601. package/build/dist/Server/Utils/Telemetry/Telemetry.js.map +1 -1
  602. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js +17 -18
  603. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js.map +1 -1
  604. package/build/dist/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.js +16 -10
  605. package/build/dist/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.js.map +1 -1
  606. package/build/dist/Types/AI/AIResourceContext.js +7 -0
  607. package/build/dist/Types/AI/AIResourceContext.js.map +1 -1
  608. package/build/dist/Types/Dashboard/DashboardTemplates.js +2 -1
  609. package/build/dist/Types/Dashboard/DashboardTemplates.js.map +1 -1
  610. package/build/dist/Types/DatabaseServer/DatabaseConnectionSpan.js +71 -0
  611. package/build/dist/Types/DatabaseServer/DatabaseConnectionSpan.js.map +1 -0
  612. package/build/dist/Types/DatabaseServer/DatabaseContainerClassifier.js +1462 -0
  613. package/build/dist/Types/DatabaseServer/DatabaseContainerClassifier.js.map +1 -0
  614. package/build/dist/Types/DatabaseServer/DatabaseContainerCommand.js +691 -0
  615. package/build/dist/Types/DatabaseServer/DatabaseContainerCommand.js.map +1 -0
  616. package/build/dist/Types/DatabaseServer/DatabaseEndpoint.js +1407 -0
  617. package/build/dist/Types/DatabaseServer/DatabaseEndpoint.js.map +1 -0
  618. package/build/dist/Types/DatabaseServer/DatabaseServerDiscoverySource.js +60 -0
  619. package/build/dist/Types/DatabaseServer/DatabaseServerDiscoverySource.js.map +1 -0
  620. package/build/dist/Types/DatabaseServer/DatabaseServerMetricCatalog.js +804 -0
  621. package/build/dist/Types/DatabaseServer/DatabaseServerMetricCatalog.js.map +1 -0
  622. package/build/dist/Types/DatabaseServer/DatabaseSystem.js +1860 -0
  623. package/build/dist/Types/DatabaseServer/DatabaseSystem.js.map +1 -0
  624. package/build/dist/Types/DatabaseServer/DatabaseTelemetryResolver.js +406 -0
  625. package/build/dist/Types/DatabaseServer/DatabaseTelemetryResolver.js.map +1 -0
  626. package/build/dist/Types/Email/EmailTemplateType.js +1 -0
  627. package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
  628. package/build/dist/Types/Incident/IncidentAlertLink.js +34 -0
  629. package/build/dist/Types/Incident/IncidentAlertLink.js.map +1 -0
  630. package/build/dist/Types/Monitor/CephAlertTemplates.js +18 -0
  631. package/build/dist/Types/Monitor/CephAlertTemplates.js.map +1 -1
  632. package/build/dist/Types/Monitor/DatabaseAlertTemplates.js +1518 -0
  633. package/build/dist/Types/Monitor/DatabaseAlertTemplates.js.map +1 -0
  634. package/build/dist/Types/Monitor/KubernetesAlertTemplates.js +57 -24
  635. package/build/dist/Types/Monitor/KubernetesAlertTemplates.js.map +1 -1
  636. package/build/dist/Types/Monitor/KubernetesMetricCatalog.js +104 -18
  637. package/build/dist/Types/Monitor/KubernetesMetricCatalog.js.map +1 -1
  638. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js +80 -3
  639. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js.map +1 -1
  640. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js +10 -3
  641. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js.map +1 -1
  642. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationUtil.js +29 -0
  643. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationUtil.js.map +1 -1
  644. package/build/dist/Types/Monitor/Recommendation/RecommendationCriteriaBuilder.js +21 -6
  645. package/build/dist/Types/Monitor/Recommendation/RecommendationCriteriaBuilder.js.map +1 -1
  646. package/build/dist/Types/Monitor/SeriesContext/SeriesLabelDisplay.js +25 -0
  647. package/build/dist/Types/Monitor/SeriesContext/SeriesLabelDisplay.js.map +1 -1
  648. package/build/dist/Types/NetworkDevice/DiscoveredHostNamingStatus.js +136 -0
  649. package/build/dist/Types/NetworkDevice/DiscoveredHostNamingStatus.js.map +1 -0
  650. package/build/dist/Types/Permission.js +276 -0
  651. package/build/dist/Types/Permission.js.map +1 -1
  652. package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js +10 -0
  653. package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js.map +1 -1
  654. package/build/dist/Types/Rules/RuleRun.js +4 -0
  655. package/build/dist/Types/Rules/RuleRun.js.map +1 -1
  656. package/build/dist/Types/Rum/WebVitals.js +25 -0
  657. package/build/dist/Types/Rum/WebVitals.js.map +1 -1
  658. package/build/dist/Types/Telemetry/EntityType.js +23 -0
  659. package/build/dist/Types/Telemetry/EntityType.js.map +1 -1
  660. package/build/dist/Types/Telemetry/ResourceFacetCatalog.js +14 -0
  661. package/build/dist/Types/Telemetry/ResourceFacetCatalog.js.map +1 -1
  662. package/build/dist/Types/Telemetry/ServiceType.js +8 -0
  663. package/build/dist/Types/Telemetry/ServiceType.js.map +1 -1
  664. package/build/dist/UI/Components/Banner/Banner.js +1 -1
  665. package/build/dist/UI/Components/Banner/Banner.js.map +1 -1
  666. package/build/dist/UI/Components/Card/Card.js +1 -1
  667. package/build/dist/UI/Components/Card/Card.js.map +1 -1
  668. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +2 -2
  669. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
  670. package/build/dist/UI/Components/Charts/Line/LineChart.js +1 -1
  671. package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
  672. package/build/dist/UI/Components/CommandPalette/CommandPalette.js +1 -1
  673. package/build/dist/UI/Components/CommandPalette/CommandPalette.js.map +1 -1
  674. package/build/dist/UI/Components/Date/StartAndEndDate.js +5 -5
  675. package/build/dist/UI/Components/Date/StartAndEndDate.js.map +1 -1
  676. package/build/dist/UI/Components/Detail/FieldLabel.js +2 -0
  677. package/build/dist/UI/Components/Detail/FieldLabel.js.map +1 -1
  678. package/build/dist/UI/Components/Diagram/HorizontalStepChain.js +1 -1
  679. package/build/dist/UI/Components/Diagram/HorizontalStepChain.js.map +1 -1
  680. package/build/dist/UI/Components/Feed/FeedOptionsButton.js +1 -1
  681. package/build/dist/UI/Components/Feed/FeedOptionsButton.js.map +1 -1
  682. package/build/dist/UI/Components/Forms/BasicForm.js +1 -1
  683. package/build/dist/UI/Components/Forms/BasicForm.js.map +1 -1
  684. package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js +1 -1
  685. package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js.map +1 -1
  686. package/build/dist/UI/Components/HeaderAlert/HeaderAlert.js +1 -1
  687. package/build/dist/UI/Components/HeaderAlert/HeaderAlert.js.map +1 -1
  688. package/build/dist/UI/Components/InfoCard/InfoCard.js +22 -1
  689. package/build/dist/UI/Components/InfoCard/InfoCard.js.map +1 -1
  690. package/build/dist/UI/Components/LogsViewer/components/FacetValueRow.js +1 -1
  691. package/build/dist/UI/Components/LogsViewer/components/FacetValueRow.js.map +1 -1
  692. package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js +1 -1
  693. package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js.map +1 -1
  694. package/build/dist/UI/Components/Modal/ModalFooter.js +1 -1
  695. package/build/dist/UI/Components/Modal/ModalFooter.js.map +1 -1
  696. package/build/dist/UI/Components/ModelDetail/CardModelDetail.js +2 -2
  697. package/build/dist/UI/Components/ModelDetail/CardModelDetail.js.map +1 -1
  698. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +70 -18
  699. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  700. package/build/dist/UI/Components/Navbar/NavBar.js +9 -2
  701. package/build/dist/UI/Components/Navbar/NavBar.js.map +1 -1
  702. package/build/dist/UI/Components/Navbar/NavBarMenuModal.js +2 -2
  703. package/build/dist/UI/Components/Navbar/NavBarMenuModal.js.map +1 -1
  704. package/build/dist/UI/Components/Page/Page.js +1 -1
  705. package/build/dist/UI/Components/Page/Page.js.map +1 -1
  706. package/build/dist/UI/Components/Pagination/Pagination.js +3 -3
  707. package/build/dist/UI/Components/Pagination/Pagination.js.map +1 -1
  708. package/build/dist/UI/Components/ProgressButtons/ProgressButtonItem.js +1 -1
  709. package/build/dist/UI/Components/ProgressButtons/ProgressButtonItem.js.map +1 -1
  710. package/build/dist/UI/Components/SideMenu/SideMenu.js +4 -1
  711. package/build/dist/UI/Components/SideMenu/SideMenu.js.map +1 -1
  712. package/build/dist/UI/Components/StatusPage/ResourceGroupNavigator.js +3 -1
  713. package/build/dist/UI/Components/StatusPage/ResourceGroupNavigator.js.map +1 -1
  714. package/build/dist/UI/Components/StatusPage/ResourceList.js +6 -6
  715. package/build/dist/UI/Components/StatusPage/ResourceList.js.map +1 -1
  716. package/build/dist/UI/Components/Table/TableHeader.js +18 -2
  717. package/build/dist/UI/Components/Table/TableHeader.js.map +1 -1
  718. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.js +1 -1
  719. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.js.map +1 -1
  720. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.js +1 -1
  721. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.js.map +1 -1
  722. package/build/dist/UI/Components/Tooltip/InfoTooltip.js +28 -0
  723. package/build/dist/UI/Components/Tooltip/InfoTooltip.js.map +1 -0
  724. package/build/dist/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.js +1 -1
  725. package/build/dist/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.js.map +1 -1
  726. package/build/dist/UI/Components/Workflow/ComponentsModal.js +2 -2
  727. package/build/dist/UI/Components/Workflow/ComponentsModal.js.map +1 -1
  728. package/build/dist/UI/Components/Workflow/StepTraceViewer.js +1 -1
  729. package/build/dist/UI/Components/Workflow/StepTraceViewer.js.map +1 -1
  730. package/build/dist/UI/Utils/Navigation.js +66 -33
  731. package/build/dist/UI/Utils/Navigation.js.map +1 -1
  732. package/build/dist/UI/Utils/PermissionGate.js +4 -2
  733. package/build/dist/UI/Utils/PermissionGate.js.map +1 -1
  734. package/build/dist/UI/Utils/Telemetry/TelemetryEntityNames.js +7 -0
  735. package/build/dist/UI/Utils/Telemetry/TelemetryEntityNames.js.map +1 -1
  736. package/build/dist/UI/Utils/TelemetryService.js +3 -2
  737. package/build/dist/UI/Utils/TelemetryService.js.map +1 -1
  738. package/build/dist/Utils/Incident/IncidentFromAlerts.js +306 -0
  739. package/build/dist/Utils/Incident/IncidentFromAlerts.js.map +1 -0
  740. package/build/dist/Utils/LabelRuleImportExport.js +2 -0
  741. package/build/dist/Utils/LabelRuleImportExport.js.map +1 -1
  742. package/build/dist/Utils/MetricUnitUtil.js +77 -13
  743. package/build/dist/Utils/MetricUnitUtil.js.map +1 -1
  744. package/build/dist/Utils/Monitor/MetricValueFormatter.js +58 -13
  745. package/build/dist/Utils/Monitor/MetricValueFormatter.js.map +1 -1
  746. package/build/dist/Utils/Monitor/PlatformMetricUnitUtil.js +161 -0
  747. package/build/dist/Utils/Monitor/PlatformMetricUnitUtil.js.map +1 -0
  748. package/build/dist/Utils/Monitor/PlatformResourceIdentity.js +323 -0
  749. package/build/dist/Utils/Monitor/PlatformResourceIdentity.js.map +1 -0
  750. package/build/dist/Utils/NetworkDiscovery/DiscoveredHostNamingDiagnosis.js +270 -0
  751. package/build/dist/Utils/NetworkDiscovery/DiscoveredHostNamingDiagnosis.js.map +1 -0
  752. package/build/dist/Utils/NetworkDiscovery/DiscoveredHostUtil.js +23 -0
  753. package/build/dist/Utils/NetworkDiscovery/DiscoveredHostUtil.js.map +1 -1
  754. package/build/dist/Utils/NetworkDiscovery/NetbiosNameUtil.js +61 -0
  755. package/build/dist/Utils/NetworkDiscovery/NetbiosNameUtil.js.map +1 -1
  756. package/build/dist/Utils/Rum/SessionTraceState.js +154 -0
  757. package/build/dist/Utils/Rum/SessionTraceState.js.map +1 -0
  758. package/build/dist/Utils/Telemetry/DatabaseServerEntityKeys.js +178 -0
  759. package/build/dist/Utils/Telemetry/DatabaseServerEntityKeys.js.map +1 -0
  760. package/build/dist/Utils/Telemetry/EntityKey.js +133 -0
  761. package/build/dist/Utils/Telemetry/EntityKey.js.map +1 -1
  762. package/build/dist/Utils/ValueFormatter.js +15 -0
  763. package/build/dist/Utils/ValueFormatter.js.map +1 -1
  764. package/package.json +1 -1
  765. package/tsconfig.tests.json +7 -5
@@ -0,0 +1,2124 @@
1
+ import {
2
+ afterEach,
3
+ beforeEach,
4
+ describe,
5
+ expect,
6
+ it,
7
+ jest,
8
+ } from "@jest/globals";
9
+ import type { SpyInstance } from "jest-mock";
10
+ import getJestMockFunction, { MockFunction } from "../../MockType";
11
+ import {
12
+ REPLAY_SCREENSHOT_URL_TTL_MS,
13
+ ReplayClipboardFailure,
14
+ ReplayScreenshot,
15
+ ReplayScreenshotClipboardError,
16
+ buildReplayScreenshotFileName,
17
+ captureReplayerScreenshot,
18
+ copyReplayScreenshotToClipboard,
19
+ downloadReplayScreenshot,
20
+ formatReplayScreenshotOffset,
21
+ getReplayImageClipboardSupport,
22
+ readReplayPointer,
23
+ readReplayViewport,
24
+ } from "../../../../App/FeatureSet/Dashboard/src/Components/SessionReplay/ReplayScreenshot";
25
+ import {
26
+ REPLAY_FRAME_MAX_CANVAS_PIXELS,
27
+ ReplayFrameCaptureError,
28
+ ReplayFramePointer,
29
+ ReplayFrameRasterDeps,
30
+ ReplayFrameViewport,
31
+ resolveReplayFrameCanvasSize,
32
+ resolveReplayFramePixelRatio,
33
+ } from "../../../../App/FeatureSet/Dashboard/src/Components/SessionReplay/ReplayFrameCapture";
34
+ import { ReplayerLike } from "../../../../App/FeatureSet/Dashboard/src/Components/SessionReplay/Engine/ReplayEngineTypes";
35
+
36
+ /*
37
+ * The player's side of the paused-frame screenshot: what the file is
38
+ * called, which document and viewport get captured, where the stage's
39
+ * pointer was, and the two browser APIs the PNG leaves through.
40
+ *
41
+ * Pinned here:
42
+ * - the file name: a colon-free playhead ("4m07.3s", floored, never
43
+ * rounded up into the next second), the first eight id characters,
44
+ * an optional tab slug, and nothing a Windows or macOS file system
45
+ * would refuse;
46
+ * - the recorded viewport comes from the iframe's width/height
47
+ * ATTRIBUTES (the Replayer's unscaled truth), the client box only as
48
+ * a fallback;
49
+ * - the pointer is the stage's .replayer-mouse, centred on its box and
50
+ * left out whenever the stage is not showing it;
51
+ * - captureReplayerScreenshot rejects with a ReplayFrameCaptureError
52
+ * (never throws) and hands its raster deps, pixel ratio and pointer
53
+ * through, without writing to the live replay document, and reports
54
+ * the PNG's own pixel size - the canvas it was drawn on, capped at 2x
55
+ * and kept within the canvas budget;
56
+ * - the clipboard write starts SYNCHRONOUSLY with the still-pending
57
+ * image promise - Safari only honours a write that begins inside the
58
+ * click, and the capture outlasts that window - and a capture failure
59
+ * is what the viewer hears when both the capture and the write fail;
60
+ * - the download's object URL outlives the click (Safari cancels a
61
+ * download whose URL is revoked in the same task), even when the
62
+ * click throws.
63
+ *
64
+ * Unhandled rejections: the test's `process` is jest's copy, whose
65
+ * listeners never hear the real event, but jest-circus listens for
66
+ * 'unhandledRejection' on the REAL process and fails whichever test is
67
+ * running when it fires. Every "does not leave ... unhandled" test
68
+ * therefore rejects the promise and then waits real macrotasks
69
+ * (flushRejectionTracking), so Node's tracker runs - and would fail the
70
+ * test - before the test ends. (An it.failing control cannot pin this:
71
+ * it.failing judges only the test function's own outcome, not an error
72
+ * circus records beside it.)
73
+ */
74
+
75
+ const SESSION_ID: string = "3F9A2C1B-4D5E-4F60-9A7B-1C2D3E4F5A6B";
76
+ const OFFSET_MS: number = 247300;
77
+ const OBJECT_URL: string = "blob:https://oneuptime.test/7d1f0c52";
78
+ const REPLAY_BODY: string =
79
+ '<main><h1>Checkout</h1><p>Total: $42</p><input id="email" /></main>';
80
+ const WINDOWS_RESERVED_CHARACTERS: Array<string> = [
81
+ "<",
82
+ ">",
83
+ ":",
84
+ '"',
85
+ "/",
86
+ "\\",
87
+ "|",
88
+ "?",
89
+ "*",
90
+ ];
91
+ const SAFE_FILE_NAME_PATTERN: RegExp = /^session-replay-[a-z0-9.-]+\.png$/;
92
+ const SVG_DATA_URL_PREFIX: string = "data:image/svg+xml;base64,";
93
+
94
+ /* Kept before any test installs fake timers. */
95
+ const realSetTimeout: typeof setTimeout = setTimeout;
96
+
97
+ /* ---- Globals the browser has and jsdom does not. ---- */
98
+
99
+ const restorers: Array<() => void> = [];
100
+
101
+ function overrideProperty(target: unknown, key: string, value: unknown): void {
102
+ const previous: PropertyDescriptor | undefined =
103
+ Object.getOwnPropertyDescriptor(target, key);
104
+
105
+ Object.defineProperty(target, key, {
106
+ value: value,
107
+ configurable: true,
108
+ writable: true,
109
+ });
110
+
111
+ restorers.push((): void => {
112
+ if (previous) {
113
+ Object.defineProperty(target, key, previous);
114
+ } else {
115
+ delete (target as Record<string, unknown>)[key];
116
+ }
117
+ });
118
+ }
119
+
120
+ function restoreOverrides(): void {
121
+ let restore: (() => void) | undefined = restorers.pop();
122
+
123
+ while (restore) {
124
+ restore();
125
+ restore = restorers.pop();
126
+ }
127
+ }
128
+
129
+ function defineGetter(target: unknown, key: string, get: () => unknown): void {
130
+ Object.defineProperty(target, key, { configurable: true, get: get });
131
+ }
132
+
133
+ function sizeElement(
134
+ element: HTMLElement,
135
+ box: { client?: [number, number]; offset?: [number, number] },
136
+ ): void {
137
+ if (box.client) {
138
+ const [width, height]: [number, number] = box.client;
139
+
140
+ defineGetter(element, "clientWidth", (): number => {
141
+ return width;
142
+ });
143
+ defineGetter(element, "clientHeight", (): number => {
144
+ return height;
145
+ });
146
+ }
147
+
148
+ if (box.offset) {
149
+ const [width, height]: [number, number] = box.offset;
150
+
151
+ defineGetter(element, "offsetWidth", (): number => {
152
+ return width;
153
+ });
154
+ defineGetter(element, "offsetHeight", (): number => {
155
+ return height;
156
+ });
157
+ }
158
+ }
159
+
160
+ /* ---- Promises the test settles by hand. ---- */
161
+
162
+ interface Deferred<T> {
163
+ promise: Promise<T>;
164
+ resolve: (value: T) => void;
165
+ reject: (reason: unknown) => void;
166
+ isSettled: () => boolean;
167
+ }
168
+
169
+ function makeDeferred<T>(): Deferred<T> {
170
+ let settled: boolean = false;
171
+ const handlers: {
172
+ resolve: ((value: T) => void) | null;
173
+ reject: ((reason: unknown) => void) | null;
174
+ } = { resolve: null, reject: null };
175
+ const promise: Promise<T> = new Promise<T>(
176
+ (resolve: (value: T) => void, reject: (reason: unknown) => void): void => {
177
+ handlers.resolve = resolve;
178
+ handlers.reject = reject;
179
+ },
180
+ );
181
+
182
+ return {
183
+ promise: promise,
184
+ resolve: (value: T): void => {
185
+ settled = true;
186
+
187
+ if (handlers.resolve) {
188
+ handlers.resolve(value);
189
+ }
190
+ },
191
+ reject: (reason: unknown): void => {
192
+ settled = true;
193
+
194
+ if (handlers.reject) {
195
+ handlers.reject(reason);
196
+ }
197
+ },
198
+ isSettled: (): boolean => {
199
+ return settled;
200
+ },
201
+ };
202
+ }
203
+
204
+ /*
205
+ * Two real macrotasks: microtasks drain, then Node's rejection tracker
206
+ * runs, while the current test is still the one jest-circus blames.
207
+ */
208
+ async function flushRejectionTracking(): Promise<void> {
209
+ for (let pass: number = 0; pass < 2; pass++) {
210
+ await new Promise<void>((resolve: () => void): void => {
211
+ realSetTimeout(resolve, 0);
212
+ });
213
+ }
214
+ }
215
+
216
+ async function catchRejection(promise: Promise<unknown>): Promise<unknown> {
217
+ try {
218
+ await promise;
219
+ } catch (error: unknown) {
220
+ return error;
221
+ }
222
+
223
+ throw new Error("expected the promise to reject");
224
+ }
225
+
226
+ function makePng(): Blob {
227
+ return new Blob(["\u0089PNG fake frame"], { type: "image/png" });
228
+ }
229
+
230
+ /* ---- A Replayer with a real (jsdom) replay document. ---- */
231
+
232
+ interface ReplayerFixture {
233
+ replayer: ReplayerLike;
234
+ iframe: HTMLIFrameElement;
235
+ wrapper: HTMLElement;
236
+ replayDocument: Document;
237
+ }
238
+
239
+ function makeReplayerLike(
240
+ iframe: HTMLIFrameElement,
241
+ wrapper: HTMLElement,
242
+ ): ReplayerLike {
243
+ return {
244
+ iframe: iframe,
245
+ wrapper: wrapper,
246
+ play: (): void => {
247
+ // Not exercised by the screenshot.
248
+ },
249
+ pause: (): void => {
250
+ // Not exercised by the screenshot.
251
+ },
252
+ destroy: (): void => {
253
+ wrapper.remove();
254
+ },
255
+ addEvent: (): void => {
256
+ // Not exercised by the screenshot.
257
+ },
258
+ getCurrentTime: (): number => {
259
+ return OFFSET_MS;
260
+ },
261
+ setConfig: (): void => {
262
+ // Not exercised by the screenshot.
263
+ },
264
+ on: (): unknown => {
265
+ return undefined;
266
+ },
267
+ };
268
+ }
269
+
270
+ function makeReplayer(options?: {
271
+ width?: string | null;
272
+ height?: string | null;
273
+ body?: string;
274
+ }): ReplayerFixture {
275
+ const wrapper: HTMLElement = document.createElement("div");
276
+ const iframe: HTMLIFrameElement = document.createElement("iframe");
277
+ const width: string | null =
278
+ options?.width === undefined ? "800" : options.width;
279
+ const height: string | null =
280
+ options?.height === undefined ? "600" : options.height;
281
+
282
+ wrapper.className = "replayer-wrapper";
283
+
284
+ if (width !== null) {
285
+ iframe.setAttribute("width", width);
286
+ }
287
+
288
+ if (height !== null) {
289
+ iframe.setAttribute("height", height);
290
+ }
291
+
292
+ wrapper.appendChild(iframe);
293
+ document.body.appendChild(wrapper);
294
+
295
+ const replayDocument: Document | null = iframe.contentDocument;
296
+
297
+ if (!replayDocument) {
298
+ throw new Error("jsdom gave the replay iframe no document");
299
+ }
300
+
301
+ replayDocument.body.innerHTML = options?.body ?? REPLAY_BODY;
302
+
303
+ return {
304
+ replayer: makeReplayerLike(iframe, wrapper),
305
+ iframe: iframe,
306
+ wrapper: wrapper,
307
+ replayDocument: replayDocument,
308
+ };
309
+ }
310
+
311
+ function addPointer(
312
+ wrapper: HTMLElement,
313
+ options?: { left?: string; top?: string; size?: [number, number] },
314
+ ): HTMLElement {
315
+ const pointer: HTMLElement = wrapper.ownerDocument.createElement("div");
316
+
317
+ pointer.className = "replayer-mouse";
318
+
319
+ if (options?.left !== "") {
320
+ pointer.style.left = options?.left ?? "100px";
321
+ }
322
+
323
+ if (options?.top !== "") {
324
+ pointer.style.top = options?.top ?? "50px";
325
+ }
326
+
327
+ wrapper.appendChild(pointer);
328
+
329
+ if (options?.size) {
330
+ sizeElement(pointer, { offset: options.size });
331
+ }
332
+
333
+ return pointer;
334
+ }
335
+
336
+ /* ---- The canvas and image loading jsdom does not have. ---- */
337
+
338
+ interface FakeRaster {
339
+ /* The clock and the Safari settle delay keep their defaults. */
340
+ deps: Pick<ReplayFrameRasterDeps, "createCanvas" | "loadImage" | "nextFrame">;
341
+ createCanvas: MockFunction;
342
+ loadImage: MockFunction;
343
+ nextFrame: MockFunction;
344
+ toBlob: MockFunction;
345
+ arc: MockFunction;
346
+ drawImage: MockFunction;
347
+ blob: Blob;
348
+ }
349
+
350
+ function makeRaster(options?: {
351
+ blob?: Blob | null;
352
+ hasContext?: boolean;
353
+ loadImage?: (url: string) => Promise<CanvasImageSource>;
354
+ }): FakeRaster {
355
+ const blob: Blob = makePng();
356
+ const produced: Blob | null =
357
+ options?.blob === undefined ? blob : options.blob;
358
+ const arc: MockFunction = getJestMockFunction();
359
+ const drawImage: MockFunction = getJestMockFunction();
360
+ const context: Record<string, unknown> = {
361
+ save: getJestMockFunction(),
362
+ restore: getJestMockFunction(),
363
+ beginPath: getJestMockFunction(),
364
+ fill: getJestMockFunction(),
365
+ stroke: getJestMockFunction(),
366
+ clearRect: getJestMockFunction(),
367
+ fillRect: getJestMockFunction(),
368
+ arc: arc,
369
+ drawImage: drawImage,
370
+ fillStyle: "",
371
+ strokeStyle: "",
372
+ lineWidth: 1,
373
+ };
374
+ const toBlob: MockFunction = getJestMockFunction().mockImplementation(
375
+ (callback: BlobCallback): void => {
376
+ callback(produced);
377
+ },
378
+ );
379
+ const createCanvas: MockFunction = getJestMockFunction().mockImplementation(
380
+ (width: number, height: number): HTMLCanvasElement => {
381
+ const canvas: HTMLCanvasElement = document.createElement("canvas");
382
+
383
+ canvas.width = width;
384
+ canvas.height = height;
385
+ Object.defineProperty(canvas, "getContext", {
386
+ value: (): unknown => {
387
+ return options?.hasContext === false ? null : context;
388
+ },
389
+ });
390
+ Object.defineProperty(canvas, "toBlob", { value: toBlob });
391
+ Object.defineProperty(canvas, "toDataURL", {
392
+ value: (): string => {
393
+ return "data:image/png;base64,AAAA";
394
+ },
395
+ });
396
+
397
+ return canvas;
398
+ },
399
+ );
400
+ const loadImage: MockFunction = getJestMockFunction().mockImplementation(
401
+ options?.loadImage ??
402
+ ((): Promise<CanvasImageSource> => {
403
+ return Promise.resolve(document.createElement("img"));
404
+ }),
405
+ );
406
+ const nextFrame: MockFunction = getJestMockFunction().mockImplementation(
407
+ (): Promise<void> => {
408
+ return Promise.resolve();
409
+ },
410
+ );
411
+
412
+ return {
413
+ deps: {
414
+ createCanvas: createCanvas,
415
+ loadImage: loadImage,
416
+ nextFrame: nextFrame,
417
+ },
418
+ createCanvas: createCanvas,
419
+ loadImage: loadImage,
420
+ nextFrame: nextFrame,
421
+ toBlob: toBlob,
422
+ arc: arc,
423
+ drawImage: drawImage,
424
+ blob: blob,
425
+ };
426
+ }
427
+
428
+ function decodeSvgDataUrl(url: string): string {
429
+ expect(url.startsWith(SVG_DATA_URL_PREFIX)).toBe(true);
430
+
431
+ const binary: string = atob(url.slice(SVG_DATA_URL_PREFIX.length));
432
+ const bytes: Uint8Array = new Uint8Array(
433
+ Array.from(binary, (character: string): number => {
434
+ return character.charCodeAt(0);
435
+ }),
436
+ );
437
+
438
+ return new TextDecoder().decode(bytes);
439
+ }
440
+
441
+ function loadedSvg(raster: FakeRaster): string {
442
+ expect(raster.loadImage).toHaveBeenCalledTimes(1);
443
+
444
+ return decodeSvgDataUrl(raster.loadImage.mock.calls[0][0] as string);
445
+ }
446
+
447
+ /* ---- The async Clipboard API. ---- */
448
+
449
+ interface ClipboardHarness {
450
+ write: MockFunction;
451
+ /* What each ClipboardItem was constructed with, in order. */
452
+ entries: Array<Record<string, unknown>>;
453
+ instances: Array<unknown>;
454
+ }
455
+
456
+ function installClipboard(options?: {
457
+ write?: (items: Array<unknown>) => Promise<void>;
458
+ itemThrows?: { error: unknown };
459
+ }): ClipboardHarness {
460
+ const entries: Array<Record<string, unknown>> = [];
461
+ const instances: Array<unknown> = [];
462
+ const itemThrows: { error: unknown } | undefined = options?.itemThrows;
463
+ const write: MockFunction = getJestMockFunction().mockImplementation(
464
+ options?.write ??
465
+ ((): Promise<void> => {
466
+ return Promise.resolve();
467
+ }),
468
+ );
469
+
470
+ class FakeClipboardItem {
471
+ public readonly types: Array<string>;
472
+
473
+ public constructor(items: Record<string, unknown>) {
474
+ if (itemThrows) {
475
+ throw itemThrows.error;
476
+ }
477
+
478
+ entries.push(items);
479
+ instances.push(this);
480
+ this.types = Object.keys(items);
481
+ }
482
+ }
483
+
484
+ overrideProperty(window, "isSecureContext", true);
485
+ overrideProperty(navigator, "clipboard", { write: write });
486
+ overrideProperty(window, "ClipboardItem", FakeClipboardItem);
487
+
488
+ return { write: write, entries: entries, instances: instances };
489
+ }
490
+
491
+ afterEach(() => {
492
+ restoreOverrides();
493
+ jest.useRealTimers();
494
+ document.head.innerHTML = "";
495
+ document.body.innerHTML = "";
496
+ });
497
+
498
+ describe("formatReplayScreenshotOffset", () => {
499
+ it("writes the start of the recording as 0m00.0s", () => {
500
+ expect(formatReplayScreenshotOffset(0)).toBe("0m00.0s");
501
+ });
502
+
503
+ it("keeps tenths of a second so shots a few frames apart do not collide", () => {
504
+ expect(formatReplayScreenshotOffset(99)).toBe("0m00.0s");
505
+ expect(formatReplayScreenshotOffset(100)).toBe("0m00.1s");
506
+ expect(formatReplayScreenshotOffset(7300)).toBe("0m07.3s");
507
+ expect(formatReplayScreenshotOffset(7400)).toBe("0m07.4s");
508
+ });
509
+
510
+ it("floors instead of rounding, so the name never runs ahead of the frame", () => {
511
+ expect(formatReplayScreenshotOffset(7399)).toBe("0m07.3s");
512
+ expect(formatReplayScreenshotOffset(7399.99)).toBe("0m07.3s");
513
+ expect(formatReplayScreenshotOffset(0.5)).toBe("0m00.0s");
514
+ });
515
+
516
+ it("does not round 59.95s up into the next minute", () => {
517
+ expect(formatReplayScreenshotOffset(59950)).toBe("0m59.9s");
518
+ expect(formatReplayScreenshotOffset(59999)).toBe("0m59.9s");
519
+ expect(formatReplayScreenshotOffset(60000)).toBe("1m00.0s");
520
+ });
521
+
522
+ it("counts whole minutes with zero-padded seconds", () => {
523
+ expect(formatReplayScreenshotOffset(OFFSET_MS)).toBe("4m07.3s");
524
+ expect(formatReplayScreenshotOffset(725000)).toBe("12m05.0s");
525
+ expect(formatReplayScreenshotOffset(3599999)).toBe("59m59.9s");
526
+ });
527
+
528
+ it("adds hours with zero-padded minutes once past the hour", () => {
529
+ expect(formatReplayScreenshotOffset(3600000)).toBe("1h00m00.0s");
530
+ expect(formatReplayScreenshotOffset(3723400)).toBe("1h02m03.4s");
531
+ expect(formatReplayScreenshotOffset(36000000)).toBe("10h00m00.0s");
532
+ expect(formatReplayScreenshotOffset(26 * 3600000 + 5 * 60000)).toBe(
533
+ "26h05m00.0s",
534
+ );
535
+ });
536
+
537
+ it("treats negative, NaN and infinite offsets as the start", () => {
538
+ const invalid: Array<number> = [
539
+ -1,
540
+ -60000,
541
+ Number.NaN,
542
+ Number.POSITIVE_INFINITY,
543
+ Number.NEGATIVE_INFINITY,
544
+ ];
545
+
546
+ invalid.forEach((offsetMs: number): void => {
547
+ expect(formatReplayScreenshotOffset(offsetMs)).toBe("0m00.0s");
548
+ });
549
+ });
550
+
551
+ it("never contains a colon, which a Windows file name cannot", () => {
552
+ [0, 999, 61234, 3723400, 99999999].forEach((offsetMs: number): void => {
553
+ expect(formatReplayScreenshotOffset(offsetMs)).not.toContain(":");
554
+ });
555
+ });
556
+ });
557
+
558
+ describe("buildReplayScreenshotFileName", () => {
559
+ it("names the file after the first eight characters of the session id and the playhead", () => {
560
+ expect(
561
+ buildReplayScreenshotFileName({
562
+ sessionId: SESSION_ID,
563
+ offsetMs: OFFSET_MS,
564
+ }),
565
+ ).toBe("session-replay-3f9a2c1b-4m07.3s.png");
566
+ });
567
+
568
+ it("lowercases the id and drops every character that is not a letter or digit", () => {
569
+ expect(
570
+ buildReplayScreenshotFileName({
571
+ sessionId: "a1-B2_c3.D4/e5:f6",
572
+ offsetMs: 0,
573
+ }),
574
+ ).toBe("session-replay-a1b2c3d4-0m00.0s.png");
575
+ expect(
576
+ buildReplayScreenshotFileName({
577
+ sessionId: "ñandú-0123456789",
578
+ offsetMs: 0,
579
+ }),
580
+ ).toBe("session-replay-and01234-0m00.0s.png");
581
+ });
582
+
583
+ it("keeps an id shorter than eight characters whole", () => {
584
+ expect(
585
+ buildReplayScreenshotFileName({ sessionId: "abc", offsetMs: 0 }),
586
+ ).toBe("session-replay-abc-0m00.0s.png");
587
+ });
588
+
589
+ it("falls back to 'session' when the id has nothing usable", () => {
590
+ const ids: Array<string> = ["", "----___", "日本語", " "];
591
+
592
+ ids.forEach((sessionId: string): void => {
593
+ expect(buildReplayScreenshotFileName({ sessionId, offsetMs: 0 })).toBe(
594
+ "session-replay-session-0m00.0s.png",
595
+ );
596
+ });
597
+
598
+ expect(
599
+ buildReplayScreenshotFileName({
600
+ sessionId: undefined as unknown as string,
601
+ offsetMs: 0,
602
+ }),
603
+ ).toBe("session-replay-session-0m00.0s.png");
604
+ });
605
+
606
+ it("adds the tab as a slug only when a label is given", () => {
607
+ expect(
608
+ buildReplayScreenshotFileName({
609
+ sessionId: SESSION_ID,
610
+ offsetMs: OFFSET_MS,
611
+ tabLabel: "Tab 2",
612
+ }),
613
+ ).toBe("session-replay-3f9a2c1b-tab-2-4m07.3s.png");
614
+
615
+ [null, undefined, ""].forEach(
616
+ (tabLabel: string | null | undefined): void => {
617
+ expect(
618
+ buildReplayScreenshotFileName({
619
+ sessionId: SESSION_ID,
620
+ offsetMs: OFFSET_MS,
621
+ tabLabel: tabLabel,
622
+ }),
623
+ ).toBe("session-replay-3f9a2c1b-4m07.3s.png");
624
+ },
625
+ );
626
+ });
627
+
628
+ it("slugs awkward labels to lowercase words joined by single dashes", () => {
629
+ const cases: Array<[string, string]> = [
630
+ [" Tab #3!! ", "tab-3"],
631
+ ["Checkout / Payment", "checkout-payment"],
632
+ ["C:\\Users\\alice\\evil", "c-users-alice-evil"],
633
+ ["../../etc/passwd", "etc-passwd"],
634
+ ["ÄÖÜ tab", "tab"],
635
+ ["TAB_7", "tab-7"],
636
+ ];
637
+
638
+ cases.forEach(([tabLabel, slug]: [string, string]): void => {
639
+ expect(
640
+ buildReplayScreenshotFileName({
641
+ sessionId: SESSION_ID,
642
+ offsetMs: 0,
643
+ tabLabel: tabLabel,
644
+ }),
645
+ ).toBe(`session-replay-3f9a2c1b-${slug}-0m00.0s.png`);
646
+ });
647
+ });
648
+
649
+ it("leaves the tab out when the label slugs to nothing", () => {
650
+ ["🙂🙂", "---", " ", "***"].forEach((tabLabel: string): void => {
651
+ expect(
652
+ buildReplayScreenshotFileName({
653
+ sessionId: SESSION_ID,
654
+ offsetMs: 0,
655
+ tabLabel: tabLabel,
656
+ }),
657
+ ).toBe("session-replay-3f9a2c1b-0m00.0s.png");
658
+ });
659
+ });
660
+
661
+ it("caps a long label at 24 characters", () => {
662
+ expect(
663
+ buildReplayScreenshotFileName({
664
+ sessionId: SESSION_ID,
665
+ offsetMs: 0,
666
+ tabLabel: "abcdefghijklmnopqrstuvwxyzabcdefghijklmn",
667
+ }),
668
+ ).toBe("session-replay-3f9a2c1b-abcdefghijklmnopqrstuvwx-0m00.0s.png");
669
+ });
670
+
671
+ it("never leaves a dangling dash where a long label is cut", () => {
672
+ const name: string = buildReplayScreenshotFileName({
673
+ sessionId: SESSION_ID,
674
+ offsetMs: 0,
675
+ tabLabel: `${"a".repeat(23)} b`,
676
+ });
677
+
678
+ expect(name).toBe(`session-replay-3f9a2c1b-${"a".repeat(23)}-0m00.0s.png`);
679
+ expect(name).not.toContain("--");
680
+ });
681
+
682
+ it("always ends in .png and never holds a character a file system refuses", () => {
683
+ const labels: Array<string | null> = [
684
+ null,
685
+ "Tab 2",
686
+ 'a<b>c:d"e/f\\g|h?i*j',
687
+ "\u0000\u001f control",
688
+ "trailing dot.",
689
+ " spaced ",
690
+ ];
691
+ const ids: Array<string> = [SESSION_ID, "", 'x:y*z?"<>|', "\u0007bell"];
692
+ const offsets: Array<number> = [0, 7399, 3723400, Number.NaN, -5];
693
+
694
+ ids.forEach((sessionId: string): void => {
695
+ labels.forEach((tabLabel: string | null): void => {
696
+ offsets.forEach((offsetMs: number): void => {
697
+ const name: string = buildReplayScreenshotFileName({
698
+ sessionId: sessionId,
699
+ offsetMs: offsetMs,
700
+ tabLabel: tabLabel,
701
+ });
702
+
703
+ expect(name.endsWith(".png")).toBe(true);
704
+ expect(SAFE_FILE_NAME_PATTERN.test(name)).toBe(true);
705
+ WINDOWS_RESERVED_CHARACTERS.forEach((character: string): void => {
706
+ expect(name).not.toContain(character);
707
+ });
708
+ Array.from(name).forEach((character: string): void => {
709
+ expect(character.charCodeAt(0)).toBeGreaterThanOrEqual(32);
710
+ });
711
+ });
712
+ });
713
+ });
714
+ });
715
+ });
716
+
717
+ describe("readReplayViewport", () => {
718
+ it("reads the recorded size from the width and height attributes", () => {
719
+ const iframe: HTMLIFrameElement = document.createElement("iframe");
720
+
721
+ iframe.setAttribute("width", "1440");
722
+ iframe.setAttribute("height", "900");
723
+
724
+ expect(readReplayViewport(iframe)).toEqual({ width: 1440, height: 900 });
725
+ });
726
+
727
+ it("prefers the attributes over the scaled client box", () => {
728
+ const iframe: HTMLIFrameElement = document.createElement("iframe");
729
+
730
+ iframe.setAttribute("width", "1440");
731
+ iframe.setAttribute("height", "900");
732
+ sizeElement(iframe, { client: [720, 450] });
733
+
734
+ expect(readReplayViewport(iframe)).toEqual({ width: 1440, height: 900 });
735
+ });
736
+
737
+ it("keeps a fractional attribute as it is", () => {
738
+ const iframe: HTMLIFrameElement = document.createElement("iframe");
739
+
740
+ iframe.setAttribute("width", "390.5");
741
+ iframe.setAttribute("height", " 844 ");
742
+
743
+ expect(readReplayViewport(iframe)).toEqual({ width: 390.5, height: 844 });
744
+ });
745
+
746
+ it("falls back to clientWidth and clientHeight when the attributes are missing", () => {
747
+ const iframe: HTMLIFrameElement = document.createElement("iframe");
748
+
749
+ sizeElement(iframe, { client: [1024, 768] });
750
+
751
+ expect(readReplayViewport(iframe)).toEqual({ width: 1024, height: 768 });
752
+ });
753
+
754
+ it("falls back per axis when only one attribute is usable", () => {
755
+ const iframe: HTMLIFrameElement = document.createElement("iframe");
756
+
757
+ iframe.setAttribute("width", "1280");
758
+ iframe.setAttribute("height", "0");
759
+ sizeElement(iframe, { client: [640, 360] });
760
+
761
+ expect(readReplayViewport(iframe)).toEqual({ width: 1280, height: 360 });
762
+ });
763
+
764
+ it("ignores zero, negative, empty, infinite and non-numeric attributes", () => {
765
+ ["0", "-800", "", "Infinity", "800px", "wide", "NaN"].forEach(
766
+ (value: string): void => {
767
+ const iframe: HTMLIFrameElement = document.createElement("iframe");
768
+
769
+ iframe.setAttribute("width", value);
770
+ iframe.setAttribute("height", value);
771
+ sizeElement(iframe, { client: [300, 200] });
772
+
773
+ expect(readReplayViewport(iframe)).toEqual({ width: 300, height: 200 });
774
+ },
775
+ );
776
+ });
777
+
778
+ it("returns null when neither the attributes nor the client box has a size", () => {
779
+ const unsized: HTMLIFrameElement = document.createElement("iframe");
780
+
781
+ expect(readReplayViewport(unsized)).toBeNull();
782
+
783
+ const zeroed: HTMLIFrameElement = document.createElement("iframe");
784
+
785
+ zeroed.setAttribute("width", "0");
786
+ zeroed.setAttribute("height", "0");
787
+ sizeElement(zeroed, { client: [0, 0] });
788
+
789
+ expect(readReplayViewport(zeroed)).toBeNull();
790
+ });
791
+
792
+ it("returns null when only one axis has a size", () => {
793
+ const widthOnly: HTMLIFrameElement = document.createElement("iframe");
794
+
795
+ widthOnly.setAttribute("width", "800");
796
+
797
+ expect(readReplayViewport(widthOnly)).toBeNull();
798
+
799
+ const heightOnly: HTMLIFrameElement = document.createElement("iframe");
800
+
801
+ sizeElement(heightOnly, { client: [0, 600] });
802
+
803
+ expect(readReplayViewport(heightOnly)).toBeNull();
804
+ });
805
+ });
806
+
807
+ describe("readReplayPointer", () => {
808
+ function makeWrapper(): HTMLElement {
809
+ const wrapper: HTMLElement = document.createElement("div");
810
+
811
+ document.body.appendChild(wrapper);
812
+
813
+ return wrapper;
814
+ }
815
+
816
+ it("returns null when the stage has no pointer element", () => {
817
+ expect(readReplayPointer(makeWrapper())).toBeNull();
818
+ });
819
+
820
+ it("returns null for a pointer that has not been placed yet", () => {
821
+ const noLeft: HTMLElement = makeWrapper();
822
+
823
+ addPointer(noLeft, { left: "", size: [20, 20] });
824
+
825
+ expect(readReplayPointer(noLeft)).toBeNull();
826
+
827
+ const noTop: HTMLElement = makeWrapper();
828
+
829
+ addPointer(noTop, { top: "", size: [20, 20] });
830
+
831
+ expect(readReplayPointer(noTop)).toBeNull();
832
+ });
833
+
834
+ it("returns null for a pointer the stage hides with display none", () => {
835
+ const wrapper: HTMLElement = makeWrapper();
836
+ const pointer: HTMLElement = addPointer(wrapper, { size: [20, 20] });
837
+
838
+ pointer.style.display = "none";
839
+
840
+ expect(readReplayPointer(wrapper)).toBeNull();
841
+ });
842
+
843
+ it("returns null for a pointer hidden by a stylesheet rule", () => {
844
+ const style: HTMLStyleElement = document.createElement("style");
845
+
846
+ style.textContent = ".replayer-mouse.hidden-cursor { display: none; }";
847
+ document.head.appendChild(style);
848
+
849
+ const wrapper: HTMLElement = makeWrapper();
850
+ const pointer: HTMLElement = addPointer(wrapper, { size: [20, 20] });
851
+
852
+ pointer.classList.add("hidden-cursor");
853
+
854
+ expect(readReplayPointer(wrapper)).toBeNull();
855
+ });
856
+
857
+ it("returns null for a pointer with visibility hidden, its own or inherited", () => {
858
+ const own: HTMLElement = makeWrapper();
859
+
860
+ addPointer(own, { size: [20, 20] }).style.visibility = "hidden";
861
+
862
+ expect(readReplayPointer(own)).toBeNull();
863
+
864
+ const inherited: HTMLElement = makeWrapper();
865
+
866
+ inherited.style.visibility = "hidden";
867
+ addPointer(inherited, { size: [20, 20] });
868
+
869
+ expect(readReplayPointer(inherited)).toBeNull();
870
+ });
871
+
872
+ /*
873
+ * The stage draws the dot for a touch recording too - at the last tap -
874
+ * so the screenshot keeps it.
875
+ */
876
+ it("keeps a touch recording's pointer at the last tap", () => {
877
+ const wrapper: HTMLElement = makeWrapper();
878
+
879
+ addPointer(wrapper, { size: [20, 20] }).classList.add("touch-device");
880
+
881
+ expect(readReplayPointer(wrapper)).toEqual({ x: 110, y: 60 });
882
+ });
883
+
884
+ it("returns null for a pointer in a document with no window", () => {
885
+ const inert: Document = document.implementation.createHTMLDocument("");
886
+ const wrapper: HTMLElement = inert.createElement("div");
887
+
888
+ inert.body.appendChild(wrapper);
889
+ addPointer(wrapper, { size: [20, 20] });
890
+
891
+ expect(readReplayPointer(wrapper)).toBeNull();
892
+ });
893
+
894
+ it("returns null when left or top is not a number", () => {
895
+ const wrapper: HTMLElement = makeWrapper();
896
+
897
+ addPointer(wrapper, { left: "auto", size: [20, 20] });
898
+
899
+ expect(readReplayPointer(wrapper)).toBeNull();
900
+
901
+ const topAuto: HTMLElement = makeWrapper();
902
+
903
+ addPointer(topAuto, { top: "auto", size: [20, 20] });
904
+
905
+ expect(readReplayPointer(topAuto)).toBeNull();
906
+ });
907
+
908
+ it("returns the centre of the dot: its corner plus half its laid-out box", () => {
909
+ const wrapper: HTMLElement = makeWrapper();
910
+
911
+ addPointer(wrapper, { left: "100px", top: "50px", size: [20, 20] });
912
+
913
+ expect(readReplayPointer(wrapper)).toEqual({ x: 110, y: 60 });
914
+ });
915
+
916
+ it("uses the offset box even when the computed width says otherwise", () => {
917
+ const wrapper: HTMLElement = makeWrapper();
918
+ const pointer: HTMLElement = addPointer(wrapper, {
919
+ left: "100px",
920
+ top: "50px",
921
+ size: [30, 10],
922
+ });
923
+
924
+ pointer.style.width = "80px";
925
+ pointer.style.height = "80px";
926
+
927
+ expect(readReplayPointer(wrapper)).toEqual({ x: 115, y: 55 });
928
+ });
929
+
930
+ it("adds the pointer's margins", () => {
931
+ const wrapper: HTMLElement = makeWrapper();
932
+ const pointer: HTMLElement = addPointer(wrapper, {
933
+ left: "100px",
934
+ top: "50px",
935
+ size: [20, 20],
936
+ });
937
+
938
+ pointer.style.marginLeft = "-10px";
939
+ pointer.style.marginTop = "-4px";
940
+
941
+ expect(readReplayPointer(wrapper)).toEqual({ x: 100, y: 56 });
942
+ });
943
+
944
+ it("falls back to the computed width and height when there is no layout", () => {
945
+ const wrapper: HTMLElement = makeWrapper();
946
+ const pointer: HTMLElement = addPointer(wrapper, {
947
+ left: "100px",
948
+ top: "50px",
949
+ });
950
+
951
+ pointer.style.width = "24px";
952
+ pointer.style.height = "16px";
953
+
954
+ expect(readReplayPointer(wrapper)).toEqual({ x: 112, y: 58 });
955
+ });
956
+
957
+ it("reads the stage stylesheet's 20px dot when nothing is inline", () => {
958
+ const style: HTMLStyleElement = document.createElement("style");
959
+
960
+ style.textContent =
961
+ ".replayer-wrapper .replayer-mouse { position: absolute; width: 20px; height: 20px; }";
962
+ document.head.appendChild(style);
963
+
964
+ const wrapper: HTMLElement = makeWrapper();
965
+
966
+ wrapper.className = "replayer-wrapper";
967
+ addPointer(wrapper, { left: "640.5px", top: "12px" });
968
+
969
+ expect(readReplayPointer(wrapper)).toEqual({ x: 650.5, y: 22 });
970
+ });
971
+
972
+ it("returns the corner itself when the dot has no size at all", () => {
973
+ const wrapper: HTMLElement = makeWrapper();
974
+
975
+ addPointer(wrapper, { left: "7px", top: "9px" });
976
+
977
+ expect(readReplayPointer(wrapper)).toEqual({ x: 7, y: 9 });
978
+ });
979
+
980
+ it("finds the pointer anywhere inside the wrapper", () => {
981
+ const wrapper: HTMLElement = makeWrapper();
982
+ const layer: HTMLElement = document.createElement("div");
983
+
984
+ wrapper.appendChild(layer);
985
+ addPointer(layer, { left: "0px", top: "0px", size: [20, 20] });
986
+
987
+ const pointer: ReplayFramePointer | null = readReplayPointer(wrapper);
988
+
989
+ expect(pointer).toEqual({ x: 10, y: 10 });
990
+ });
991
+ });
992
+
993
+ describe("captureReplayerScreenshot", () => {
994
+ it("rejects with no-document when there is no Replayer yet", async () => {
995
+ const raster: FakeRaster = makeRaster();
996
+ const pending: Promise<ReplayScreenshot> = captureReplayerScreenshot({
997
+ replayer: null,
998
+ sessionId: SESSION_ID,
999
+ offsetMs: OFFSET_MS,
1000
+ deps: raster.deps,
1001
+ });
1002
+ const error: unknown = await catchRejection(pending);
1003
+
1004
+ expect(error).toBeInstanceOf(ReplayFrameCaptureError);
1005
+ expect((error as ReplayFrameCaptureError).reason).toBe("no-document");
1006
+ expect((error as ReplayFrameCaptureError).name).toBe(
1007
+ "ReplayFrameCaptureError",
1008
+ );
1009
+ expect(raster.createCanvas).not.toHaveBeenCalled();
1010
+ });
1011
+
1012
+ it("rejects with no-document when the iframe has no document", async () => {
1013
+ const wrapper: HTMLElement = document.createElement("div");
1014
+ const detached: HTMLIFrameElement = document.createElement("iframe");
1015
+
1016
+ detached.setAttribute("width", "800");
1017
+ detached.setAttribute("height", "600");
1018
+ wrapper.appendChild(detached);
1019
+
1020
+ expect(detached.contentDocument).toBeNull();
1021
+
1022
+ const error: unknown = await catchRejection(
1023
+ captureReplayerScreenshot({
1024
+ replayer: makeReplayerLike(detached, wrapper),
1025
+ sessionId: SESSION_ID,
1026
+ offsetMs: OFFSET_MS,
1027
+ deps: makeRaster().deps,
1028
+ }),
1029
+ );
1030
+
1031
+ expect(error).toBeInstanceOf(ReplayFrameCaptureError);
1032
+ expect((error as ReplayFrameCaptureError).reason).toBe("no-document");
1033
+ });
1034
+
1035
+ it("rejects with no-document when reading the document throws", async () => {
1036
+ const fixture: ReplayerFixture = makeReplayer();
1037
+
1038
+ defineGetter(fixture.iframe, "contentDocument", (): never => {
1039
+ throw new DOMException("Blocked a frame", "SecurityError");
1040
+ });
1041
+
1042
+ const error: unknown = await catchRejection(
1043
+ captureReplayerScreenshot({
1044
+ replayer: fixture.replayer,
1045
+ sessionId: SESSION_ID,
1046
+ offsetMs: OFFSET_MS,
1047
+ deps: makeRaster().deps,
1048
+ }),
1049
+ );
1050
+
1051
+ expect(error).toBeInstanceOf(ReplayFrameCaptureError);
1052
+ expect((error as ReplayFrameCaptureError).reason).toBe("no-document");
1053
+ });
1054
+
1055
+ it("rejects with no-document when the document has no root element", async () => {
1056
+ const fixture: ReplayerFixture = makeReplayer();
1057
+
1058
+ fixture.replayDocument.removeChild(fixture.replayDocument.documentElement);
1059
+
1060
+ const error: unknown = await catchRejection(
1061
+ captureReplayerScreenshot({
1062
+ replayer: fixture.replayer,
1063
+ sessionId: SESSION_ID,
1064
+ offsetMs: OFFSET_MS,
1065
+ deps: makeRaster().deps,
1066
+ }),
1067
+ );
1068
+
1069
+ expect((error as ReplayFrameCaptureError).reason).toBe("no-document");
1070
+ });
1071
+
1072
+ it("rejects with empty-viewport when the recorded size is unknown", async () => {
1073
+ const fixture: ReplayerFixture = makeReplayer({
1074
+ width: null,
1075
+ height: null,
1076
+ });
1077
+ const raster: FakeRaster = makeRaster();
1078
+ const error: unknown = await catchRejection(
1079
+ captureReplayerScreenshot({
1080
+ replayer: fixture.replayer,
1081
+ sessionId: SESSION_ID,
1082
+ offsetMs: OFFSET_MS,
1083
+ deps: raster.deps,
1084
+ }),
1085
+ );
1086
+
1087
+ expect(error).toBeInstanceOf(ReplayFrameCaptureError);
1088
+ expect((error as ReplayFrameCaptureError).reason).toBe("empty-viewport");
1089
+ expect(raster.createCanvas).not.toHaveBeenCalled();
1090
+ expect(raster.loadImage).not.toHaveBeenCalled();
1091
+ });
1092
+
1093
+ it("rejects with empty-viewport when one axis is zero", async () => {
1094
+ const fixture: ReplayerFixture = makeReplayer({ height: "0" });
1095
+ const error: unknown = await catchRejection(
1096
+ captureReplayerScreenshot({
1097
+ replayer: fixture.replayer,
1098
+ sessionId: SESSION_ID,
1099
+ offsetMs: OFFSET_MS,
1100
+ deps: makeRaster().deps,
1101
+ }),
1102
+ );
1103
+
1104
+ expect((error as ReplayFrameCaptureError).reason).toBe("empty-viewport");
1105
+ });
1106
+
1107
+ it("resolves the encoded PNG with the file name and the recorded size", async () => {
1108
+ const fixture: ReplayerFixture = makeReplayer();
1109
+ const raster: FakeRaster = makeRaster();
1110
+ const screenshot: ReplayScreenshot = await captureReplayerScreenshot({
1111
+ replayer: fixture.replayer,
1112
+ sessionId: SESSION_ID,
1113
+ offsetMs: OFFSET_MS,
1114
+ tabLabel: "Tab 2",
1115
+ pixelRatio: 2,
1116
+ deps: raster.deps,
1117
+ });
1118
+
1119
+ expect(screenshot.blob).toBe(raster.blob);
1120
+ expect(screenshot.fileName).toBe(
1121
+ "session-replay-3f9a2c1b-tab-2-4m07.3s.png",
1122
+ );
1123
+ expect(screenshot.fileName).toBe(
1124
+ buildReplayScreenshotFileName({
1125
+ sessionId: SESSION_ID,
1126
+ offsetMs: OFFSET_MS,
1127
+ tabLabel: "Tab 2",
1128
+ }),
1129
+ );
1130
+ /* CSS pixels, however dense the PNG. */
1131
+ expect(screenshot.width).toBe(800);
1132
+ expect(screenshot.height).toBe(600);
1133
+ expect(raster.createCanvas).toHaveBeenCalledWith(1600, 1200);
1134
+ expect(raster.toBlob).toHaveBeenCalledTimes(1);
1135
+ expect(raster.toBlob.mock.calls[0][1]).toBe("image/png");
1136
+ });
1137
+
1138
+ it("leaves the tab out of the name for a single-tab session", async () => {
1139
+ const fixture: ReplayerFixture = makeReplayer();
1140
+ const screenshot: ReplayScreenshot = await captureReplayerScreenshot({
1141
+ replayer: fixture.replayer,
1142
+ sessionId: SESSION_ID,
1143
+ offsetMs: 0,
1144
+ tabLabel: null,
1145
+ deps: makeRaster().deps,
1146
+ });
1147
+
1148
+ expect(screenshot.fileName).toBe("session-replay-3f9a2c1b-0m00.0s.png");
1149
+ });
1150
+
1151
+ it("draws the replay document itself, through the injected raster deps", async () => {
1152
+ const fixture: ReplayerFixture = makeReplayer();
1153
+ const raster: FakeRaster = makeRaster();
1154
+
1155
+ await captureReplayerScreenshot({
1156
+ replayer: fixture.replayer,
1157
+ sessionId: SESSION_ID,
1158
+ offsetMs: OFFSET_MS,
1159
+ pixelRatio: 1,
1160
+ deps: raster.deps,
1161
+ });
1162
+
1163
+ const svg: string = loadedSvg(raster);
1164
+
1165
+ expect(svg).toContain("<foreignObject");
1166
+ expect(svg).toContain('width="800"');
1167
+ expect(svg).toContain('height="600"');
1168
+ expect(svg).toContain("Checkout");
1169
+ expect(svg).toContain("Total: $42");
1170
+ expect(raster.nextFrame).toHaveBeenCalled();
1171
+ expect(raster.drawImage).toHaveBeenCalled();
1172
+ });
1173
+
1174
+ it("clones the document before it returns, so later playback cannot change the picture", async () => {
1175
+ const fixture: ReplayerFixture = makeReplayer();
1176
+ const raster: FakeRaster = makeRaster();
1177
+ const pending: Promise<ReplayScreenshot> = captureReplayerScreenshot({
1178
+ replayer: fixture.replayer,
1179
+ sessionId: SESSION_ID,
1180
+ offsetMs: OFFSET_MS,
1181
+ deps: raster.deps,
1182
+ });
1183
+
1184
+ fixture.replayDocument.body.innerHTML = "<p>Order confirmed</p>";
1185
+ await pending;
1186
+
1187
+ const svg: string = loadedSvg(raster);
1188
+
1189
+ expect(svg).toContain("Checkout");
1190
+ expect(svg).not.toContain("Order confirmed");
1191
+ });
1192
+
1193
+ it("never writes to the live replay document", async () => {
1194
+ const fixture: ReplayerFixture = makeReplayer();
1195
+ const input: HTMLInputElement | null =
1196
+ fixture.replayDocument.querySelector("#email");
1197
+
1198
+ if (!input) {
1199
+ throw new Error("fixture has no input");
1200
+ }
1201
+
1202
+ input.value = "typed@example.com";
1203
+
1204
+ const view: (Window & typeof globalThis) | null = fixture.replayDocument
1205
+ .defaultView as (Window & typeof globalThis) | null;
1206
+
1207
+ if (!view) {
1208
+ throw new Error("the replay document has no window");
1209
+ }
1210
+
1211
+ const before: string = fixture.replayDocument.documentElement.outerHTML;
1212
+ const observer: MutationObserver = new view.MutationObserver((): void => {
1213
+ // Records are read with takeRecords below.
1214
+ });
1215
+
1216
+ observer.observe(fixture.replayDocument, {
1217
+ subtree: true,
1218
+ childList: true,
1219
+ attributes: true,
1220
+ characterData: true,
1221
+ });
1222
+
1223
+ await captureReplayerScreenshot({
1224
+ replayer: fixture.replayer,
1225
+ sessionId: SESSION_ID,
1226
+ offsetMs: OFFSET_MS,
1227
+ deps: makeRaster().deps,
1228
+ });
1229
+
1230
+ const records: Array<MutationRecord> = observer.takeRecords();
1231
+
1232
+ observer.disconnect();
1233
+
1234
+ expect(records).toHaveLength(0);
1235
+ expect(fixture.replayDocument.documentElement.outerHTML).toBe(before);
1236
+ expect(input.getAttribute("value")).toBeNull();
1237
+ expect(input.value).toBe("typed@example.com");
1238
+ });
1239
+
1240
+ describe("pixel ratio", () => {
1241
+ it("uses window.devicePixelRatio when none is given", async () => {
1242
+ overrideProperty(window, "devicePixelRatio", 1.5);
1243
+
1244
+ const raster: FakeRaster = makeRaster();
1245
+
1246
+ await captureReplayerScreenshot({
1247
+ replayer: makeReplayer().replayer,
1248
+ sessionId: SESSION_ID,
1249
+ offsetMs: OFFSET_MS,
1250
+ deps: raster.deps,
1251
+ });
1252
+
1253
+ expect(raster.createCanvas).toHaveBeenCalledWith(1200, 900);
1254
+ });
1255
+
1256
+ it("prefers an explicit pixel ratio over the display's", async () => {
1257
+ overrideProperty(window, "devicePixelRatio", 2);
1258
+
1259
+ const raster: FakeRaster = makeRaster();
1260
+
1261
+ await captureReplayerScreenshot({
1262
+ replayer: makeReplayer().replayer,
1263
+ sessionId: SESSION_ID,
1264
+ offsetMs: OFFSET_MS,
1265
+ pixelRatio: 1,
1266
+ deps: raster.deps,
1267
+ });
1268
+
1269
+ expect(raster.createCanvas).toHaveBeenCalledWith(800, 600);
1270
+ });
1271
+
1272
+ it("draws at 1x when the display reports no usable ratio", async () => {
1273
+ overrideProperty(window, "devicePixelRatio", 0);
1274
+
1275
+ const raster: FakeRaster = makeRaster();
1276
+
1277
+ await captureReplayerScreenshot({
1278
+ replayer: makeReplayer().replayer,
1279
+ sessionId: SESSION_ID,
1280
+ offsetMs: OFFSET_MS,
1281
+ deps: raster.deps,
1282
+ });
1283
+
1284
+ expect(raster.createCanvas).toHaveBeenCalledWith(800, 600);
1285
+ });
1286
+
1287
+ it("caps a 3x display at a 2x PNG", async () => {
1288
+ overrideProperty(window, "devicePixelRatio", 3);
1289
+
1290
+ const raster: FakeRaster = makeRaster();
1291
+ const screenshot: ReplayScreenshot = await captureReplayerScreenshot({
1292
+ replayer: makeReplayer().replayer,
1293
+ sessionId: SESSION_ID,
1294
+ offsetMs: OFFSET_MS,
1295
+ deps: raster.deps,
1296
+ });
1297
+
1298
+ expect(raster.createCanvas).toHaveBeenCalledWith(1600, 1200);
1299
+ expect(screenshot.width).toBe(800);
1300
+ });
1301
+ });
1302
+
1303
+ describe("the PNG's own size", () => {
1304
+ async function shoot(input: {
1305
+ width: string;
1306
+ height: string;
1307
+ pixelRatio?: number;
1308
+ }): Promise<{ screenshot: ReplayScreenshot; raster: FakeRaster }> {
1309
+ const raster: FakeRaster = makeRaster();
1310
+ const screenshot: ReplayScreenshot = await captureReplayerScreenshot({
1311
+ replayer: makeReplayer({ width: input.width, height: input.height })
1312
+ .replayer,
1313
+ sessionId: SESSION_ID,
1314
+ offsetMs: OFFSET_MS,
1315
+ pixelRatio: input.pixelRatio,
1316
+ deps: raster.deps,
1317
+ });
1318
+
1319
+ return { screenshot: screenshot, raster: raster };
1320
+ }
1321
+
1322
+ it("is the recorded viewport times the ratio it was drawn at", async () => {
1323
+ const shot: { screenshot: ReplayScreenshot; raster: FakeRaster } =
1324
+ await shoot({ width: "1200", height: "760", pixelRatio: 2 });
1325
+
1326
+ expect(shot.screenshot.pixelWidth).toBe(2400);
1327
+ expect(shot.screenshot.pixelHeight).toBe(1520);
1328
+ /* The viewport itself stays in CSS pixels. */
1329
+ expect(shot.screenshot.width).toBe(1200);
1330
+ expect(shot.screenshot.height).toBe(760);
1331
+ expect(shot.raster.createCanvas).toHaveBeenCalledTimes(1);
1332
+ expect(shot.raster.createCanvas).toHaveBeenCalledWith(2400, 1520);
1333
+ });
1334
+
1335
+ it("is the viewport itself at 1x", async () => {
1336
+ const shot: { screenshot: ReplayScreenshot; raster: FakeRaster } =
1337
+ await shoot({ width: "1200", height: "760", pixelRatio: 1 });
1338
+
1339
+ expect(shot.screenshot.pixelWidth).toBe(1200);
1340
+ expect(shot.screenshot.pixelHeight).toBe(760);
1341
+ });
1342
+
1343
+ it("caps a 3x display at 2x, as the canvas does", async () => {
1344
+ overrideProperty(window, "devicePixelRatio", 3);
1345
+
1346
+ const shot: { screenshot: ReplayScreenshot; raster: FakeRaster } =
1347
+ await shoot({ width: "1200", height: "760" });
1348
+
1349
+ expect(shot.screenshot.pixelWidth).toBe(2400);
1350
+ expect(shot.screenshot.pixelHeight).toBe(1520);
1351
+ expect(shot.raster.createCanvas).toHaveBeenCalledWith(2400, 1520);
1352
+ });
1353
+
1354
+ it("reads the display's ratio when none is given", async () => {
1355
+ overrideProperty(window, "devicePixelRatio", 1.5);
1356
+
1357
+ const shot: { screenshot: ReplayScreenshot; raster: FakeRaster } =
1358
+ await shoot({ width: "1200", height: "760" });
1359
+
1360
+ expect(shot.screenshot.pixelWidth).toBe(1800);
1361
+ expect(shot.screenshot.pixelHeight).toBe(1140);
1362
+ });
1363
+
1364
+ it("floors a fractional product rather than rounding it up", async () => {
1365
+ const shot: { screenshot: ReplayScreenshot; raster: FakeRaster } =
1366
+ await shoot({ width: "390.5", height: "844.5", pixelRatio: 1.5 });
1367
+
1368
+ /* 585.75 x 1266.75 */
1369
+ expect(shot.screenshot.pixelWidth).toBe(585);
1370
+ expect(shot.screenshot.pixelHeight).toBe(1266);
1371
+ expect(shot.screenshot.width).toBe(390.5);
1372
+ expect(shot.raster.createCanvas).toHaveBeenCalledWith(585, 1266);
1373
+ });
1374
+
1375
+ it("scales a huge viewport down under the canvas budget and reports what it drew", async () => {
1376
+ const viewport: ReplayFrameViewport = { width: 5000, height: 4000 };
1377
+ const shot: { screenshot: ReplayScreenshot; raster: FakeRaster } =
1378
+ await shoot({ width: "5000", height: "4000", pixelRatio: 2 });
1379
+ const expected: ReplayFrameViewport = resolveReplayFrameCanvasSize(
1380
+ viewport,
1381
+ resolveReplayFramePixelRatio(2, viewport),
1382
+ );
1383
+
1384
+ /* sqrt(16777216 / 20000000) = 0.9159: below even 1x. */
1385
+ expect(expected).toEqual({ width: 4579, height: 3663 });
1386
+ expect(shot.screenshot.pixelWidth).toBe(4579);
1387
+ expect(shot.screenshot.pixelHeight).toBe(3663);
1388
+ expect(
1389
+ (shot.screenshot.pixelWidth as number) *
1390
+ (shot.screenshot.pixelHeight as number),
1391
+ ).toBeLessThanOrEqual(REPLAY_FRAME_MAX_CANVAS_PIXELS);
1392
+ expect(shot.raster.createCanvas).toHaveBeenCalledWith(4579, 3663);
1393
+ expect(shot.screenshot.width).toBe(5000);
1394
+ expect(shot.screenshot.height).toBe(4000);
1395
+ });
1396
+
1397
+ it("always matches the canvas the PNG was encoded from", async () => {
1398
+ const ratios: Array<number> = [0.5, 1, 1.25, 1.5, 2, 2.5, 3];
1399
+
1400
+ for (const pixelRatio of ratios) {
1401
+ const shot: { screenshot: ReplayScreenshot; raster: FakeRaster } =
1402
+ await shoot({ width: "1366", height: "683", pixelRatio: pixelRatio });
1403
+
1404
+ expect(shot.raster.createCanvas).toHaveBeenCalledTimes(1);
1405
+ expect(shot.raster.createCanvas).toHaveBeenCalledWith(
1406
+ shot.screenshot.pixelWidth,
1407
+ shot.screenshot.pixelHeight,
1408
+ );
1409
+ }
1410
+ });
1411
+ });
1412
+
1413
+ describe("the stage pointer", () => {
1414
+ it("draws the pointer read from the Replayer's wrapper, scaled with the PNG", async () => {
1415
+ const fixture: ReplayerFixture = makeReplayer();
1416
+ const raster: FakeRaster = makeRaster();
1417
+
1418
+ addPointer(fixture.wrapper, {
1419
+ left: "100px",
1420
+ top: "50px",
1421
+ size: [20, 20],
1422
+ });
1423
+
1424
+ await captureReplayerScreenshot({
1425
+ replayer: fixture.replayer,
1426
+ sessionId: SESSION_ID,
1427
+ offsetMs: OFFSET_MS,
1428
+ pixelRatio: 2,
1429
+ deps: raster.deps,
1430
+ });
1431
+
1432
+ expect(raster.arc).toHaveBeenCalledTimes(1);
1433
+ expect(raster.arc).toHaveBeenCalledWith(220, 120, 20, 0, Math.PI * 2);
1434
+ });
1435
+
1436
+ it("draws no pointer when the stage has none", async () => {
1437
+ const raster: FakeRaster = makeRaster();
1438
+
1439
+ await captureReplayerScreenshot({
1440
+ replayer: makeReplayer().replayer,
1441
+ sessionId: SESSION_ID,
1442
+ offsetMs: OFFSET_MS,
1443
+ deps: raster.deps,
1444
+ });
1445
+
1446
+ expect(raster.arc).not.toHaveBeenCalled();
1447
+ });
1448
+
1449
+ it("draws the tap dot of a touch recording, as the stage does", async () => {
1450
+ const fixture: ReplayerFixture = makeReplayer();
1451
+ const raster: FakeRaster = makeRaster();
1452
+
1453
+ addPointer(fixture.wrapper, { size: [20, 20] }).classList.add(
1454
+ "touch-device",
1455
+ );
1456
+
1457
+ await captureReplayerScreenshot({
1458
+ replayer: fixture.replayer,
1459
+ sessionId: SESSION_ID,
1460
+ offsetMs: OFFSET_MS,
1461
+ pixelRatio: 2,
1462
+ deps: raster.deps,
1463
+ });
1464
+
1465
+ expect(raster.arc).toHaveBeenCalledTimes(1);
1466
+ expect(raster.arc).toHaveBeenCalledWith(220, 120, 20, 0, Math.PI * 2);
1467
+ });
1468
+
1469
+ it("draws no pointer that sits outside the recorded viewport", async () => {
1470
+ const fixture: ReplayerFixture = makeReplayer();
1471
+ const raster: FakeRaster = makeRaster();
1472
+
1473
+ addPointer(fixture.wrapper, {
1474
+ left: "5000px",
1475
+ top: "50px",
1476
+ size: [20, 20],
1477
+ });
1478
+
1479
+ await captureReplayerScreenshot({
1480
+ replayer: fixture.replayer,
1481
+ sessionId: SESSION_ID,
1482
+ offsetMs: OFFSET_MS,
1483
+ deps: raster.deps,
1484
+ });
1485
+
1486
+ expect(raster.arc).not.toHaveBeenCalled();
1487
+ });
1488
+ });
1489
+
1490
+ describe("raster failures", () => {
1491
+ it("rejects with render-failed when the image cannot be drawn", async () => {
1492
+ const raster: FakeRaster = makeRaster({
1493
+ loadImage: (): Promise<CanvasImageSource> => {
1494
+ return Promise.reject(new Error("image decode failed"));
1495
+ },
1496
+ });
1497
+ const error: unknown = await catchRejection(
1498
+ captureReplayerScreenshot({
1499
+ replayer: makeReplayer().replayer,
1500
+ sessionId: SESSION_ID,
1501
+ offsetMs: OFFSET_MS,
1502
+ deps: raster.deps,
1503
+ }),
1504
+ );
1505
+
1506
+ expect(error).toBeInstanceOf(ReplayFrameCaptureError);
1507
+ expect((error as ReplayFrameCaptureError).reason).toBe("render-failed");
1508
+ });
1509
+
1510
+ it("rejects with render-failed when there is no 2D canvas", async () => {
1511
+ const error: unknown = await catchRejection(
1512
+ captureReplayerScreenshot({
1513
+ replayer: makeReplayer().replayer,
1514
+ sessionId: SESSION_ID,
1515
+ offsetMs: OFFSET_MS,
1516
+ deps: makeRaster({ hasContext: false }).deps,
1517
+ }),
1518
+ );
1519
+
1520
+ expect((error as ReplayFrameCaptureError).reason).toBe("render-failed");
1521
+ });
1522
+
1523
+ it("rejects with encode-failed when the canvas yields no PNG", async () => {
1524
+ const error: unknown = await catchRejection(
1525
+ captureReplayerScreenshot({
1526
+ replayer: makeReplayer().replayer,
1527
+ sessionId: SESSION_ID,
1528
+ offsetMs: OFFSET_MS,
1529
+ deps: makeRaster({ blob: null }).deps,
1530
+ }),
1531
+ );
1532
+
1533
+ expect(error).toBeInstanceOf(ReplayFrameCaptureError);
1534
+ expect((error as ReplayFrameCaptureError).reason).toBe("encode-failed");
1535
+ });
1536
+ });
1537
+
1538
+ it("keeps the default for any raster dep a test leaves out", async () => {
1539
+ const raster: FakeRaster = makeRaster();
1540
+ const screenshot: ReplayScreenshot = await captureReplayerScreenshot({
1541
+ replayer: makeReplayer().replayer,
1542
+ sessionId: SESSION_ID,
1543
+ offsetMs: OFFSET_MS,
1544
+ pixelRatio: 1,
1545
+ deps: {
1546
+ createCanvas: raster.deps.createCanvas,
1547
+ loadImage: raster.deps.loadImage,
1548
+ },
1549
+ });
1550
+
1551
+ expect(screenshot.blob).toBe(raster.blob);
1552
+ expect(raster.nextFrame).not.toHaveBeenCalled();
1553
+ });
1554
+ });
1555
+
1556
+ describe("getReplayImageClipboardSupport", () => {
1557
+ it("reports insecure-context on a plain-http page, whatever else is there", () => {
1558
+ installClipboard();
1559
+ overrideProperty(window, "isSecureContext", false);
1560
+
1561
+ expect(getReplayImageClipboardSupport()).toBe("insecure-context");
1562
+
1563
+ overrideProperty(navigator, "clipboard", undefined);
1564
+
1565
+ expect(getReplayImageClipboardSupport()).toBe("insecure-context");
1566
+ });
1567
+
1568
+ it("reports unsupported without navigator.clipboard", () => {
1569
+ installClipboard();
1570
+ overrideProperty(navigator, "clipboard", undefined);
1571
+
1572
+ expect(getReplayImageClipboardSupport()).toBe("unsupported");
1573
+ });
1574
+
1575
+ it("reports unsupported for a text-only clipboard", () => {
1576
+ installClipboard();
1577
+ overrideProperty(navigator, "clipboard", {
1578
+ writeText: getJestMockFunction(),
1579
+ });
1580
+
1581
+ expect(getReplayImageClipboardSupport()).toBe("unsupported");
1582
+
1583
+ overrideProperty(navigator, "clipboard", { write: "not a function" });
1584
+
1585
+ expect(getReplayImageClipboardSupport()).toBe("unsupported");
1586
+ });
1587
+
1588
+ it("reports unsupported without ClipboardItem", () => {
1589
+ installClipboard();
1590
+ overrideProperty(window, "ClipboardItem", undefined);
1591
+
1592
+ expect(getReplayImageClipboardSupport()).toBe("unsupported");
1593
+ });
1594
+
1595
+ it("reports supported when the secure page has clipboard.write and ClipboardItem", () => {
1596
+ installClipboard();
1597
+
1598
+ expect(getReplayImageClipboardSupport()).toBe("supported");
1599
+ });
1600
+
1601
+ it("only treats an explicit isSecureContext false as insecure", () => {
1602
+ installClipboard();
1603
+ overrideProperty(window, "isSecureContext", undefined);
1604
+
1605
+ expect(getReplayImageClipboardSupport()).toBe("supported");
1606
+ });
1607
+
1608
+ it("reports unsupported in jsdom's own environment, which has no clipboard", () => {
1609
+ expect(getReplayImageClipboardSupport()).toBe("unsupported");
1610
+ });
1611
+ });
1612
+
1613
+ describe("copyReplayScreenshotToClipboard", () => {
1614
+ describe("when the browser cannot copy images", () => {
1615
+ it("rejects with unsupported and never touches the clipboard", async () => {
1616
+ const harness: ClipboardHarness = installClipboard();
1617
+
1618
+ overrideProperty(window, "ClipboardItem", undefined);
1619
+
1620
+ const error: unknown = await catchRejection(
1621
+ copyReplayScreenshotToClipboard(Promise.resolve(makePng())),
1622
+ );
1623
+
1624
+ expect(error).toBeInstanceOf(ReplayScreenshotClipboardError);
1625
+ expect((error as ReplayScreenshotClipboardError).reason).toBe(
1626
+ "unsupported",
1627
+ );
1628
+ expect((error as ReplayScreenshotClipboardError).name).toBe(
1629
+ "ReplayScreenshotClipboardError",
1630
+ );
1631
+ expect(harness.write).not.toHaveBeenCalled();
1632
+ });
1633
+
1634
+ it("rejects with insecure-context on a plain-http page", async () => {
1635
+ const harness: ClipboardHarness = installClipboard();
1636
+
1637
+ overrideProperty(window, "isSecureContext", false);
1638
+
1639
+ const error: unknown = await catchRejection(
1640
+ copyReplayScreenshotToClipboard(Promise.resolve(makePng())),
1641
+ );
1642
+
1643
+ expect((error as ReplayScreenshotClipboardError).reason).toBe(
1644
+ "insecure-context",
1645
+ );
1646
+ expect((error as Error).message).toContain("https");
1647
+ expect(harness.write).not.toHaveBeenCalled();
1648
+ expect(harness.instances).toHaveLength(0);
1649
+ });
1650
+
1651
+ it("does not leave a later capture failure unhandled", async () => {
1652
+ overrideProperty(window, "isSecureContext", false);
1653
+
1654
+ const image: Deferred<Blob> = makeDeferred<Blob>();
1655
+ const error: unknown = await catchRejection(
1656
+ copyReplayScreenshotToClipboard(image.promise),
1657
+ );
1658
+
1659
+ expect((error as ReplayScreenshotClipboardError).reason).toBe(
1660
+ "insecure-context",
1661
+ );
1662
+
1663
+ image.reject(
1664
+ new ReplayFrameCaptureError("render-failed", "could not draw"),
1665
+ );
1666
+ await flushRejectionTracking();
1667
+ });
1668
+
1669
+ it("does not leave a later capture failure unhandled when unsupported", async () => {
1670
+ const image: Deferred<Blob> = makeDeferred<Blob>();
1671
+ const error: unknown = await catchRejection(
1672
+ copyReplayScreenshotToClipboard(image.promise),
1673
+ );
1674
+
1675
+ expect((error as ReplayScreenshotClipboardError).reason).toBe(
1676
+ "unsupported",
1677
+ );
1678
+
1679
+ image.reject(
1680
+ new ReplayFrameCaptureError("encode-failed", "could not encode"),
1681
+ );
1682
+ await flushRejectionTracking();
1683
+ });
1684
+ });
1685
+
1686
+ it("builds the ClipboardItem and starts the write synchronously, with the PENDING image", async () => {
1687
+ const harness: ClipboardHarness = installClipboard();
1688
+ const image: Deferred<Blob> = makeDeferred<Blob>();
1689
+ let wasPendingAtWrite: boolean | null = null;
1690
+
1691
+ harness.write.mockImplementation((): Promise<void> => {
1692
+ wasPendingAtWrite = !image.isSettled();
1693
+
1694
+ return Promise.resolve();
1695
+ });
1696
+
1697
+ const copied: Promise<void> = copyReplayScreenshotToClipboard(
1698
+ image.promise,
1699
+ );
1700
+
1701
+ /* Nothing has been awaited yet: this is still inside the click. */
1702
+ expect(harness.instances).toHaveLength(1);
1703
+ expect(Object.keys(harness.entries[0] as Record<string, unknown>)).toEqual([
1704
+ "image/png",
1705
+ ]);
1706
+ expect((harness.entries[0] as Record<string, unknown>)["image/png"]).toBe(
1707
+ image.promise,
1708
+ );
1709
+ expect(harness.write).toHaveBeenCalledTimes(1);
1710
+
1711
+ const written: Array<unknown> = harness.write.mock
1712
+ .calls[0][0] as Array<unknown>;
1713
+
1714
+ expect(written).toHaveLength(1);
1715
+ expect(written[0]).toBe(harness.instances[0]);
1716
+ expect(wasPendingAtWrite).toBe(true);
1717
+ expect(image.isSettled()).toBe(false);
1718
+
1719
+ image.resolve(makePng());
1720
+
1721
+ await expect(copied).resolves.toBeUndefined();
1722
+ });
1723
+
1724
+ it("resolves only once both the image and the write have settled", async () => {
1725
+ const image: Deferred<Blob> = makeDeferred<Blob>();
1726
+ const write: Deferred<void> = makeDeferred<void>();
1727
+
1728
+ installClipboard({
1729
+ write: (): Promise<void> => {
1730
+ return write.promise;
1731
+ },
1732
+ });
1733
+
1734
+ let isDone: boolean = false;
1735
+ const copied: Promise<void> = copyReplayScreenshotToClipboard(
1736
+ image.promise,
1737
+ ).then((): void => {
1738
+ isDone = true;
1739
+ });
1740
+
1741
+ image.resolve(makePng());
1742
+ await flushRejectionTracking();
1743
+
1744
+ expect(isDone).toBe(false);
1745
+
1746
+ write.resolve();
1747
+ await copied;
1748
+
1749
+ expect(isDone).toBe(true);
1750
+ });
1751
+
1752
+ it("waits for the image even when the write settles first", async () => {
1753
+ const image: Deferred<Blob> = makeDeferred<Blob>();
1754
+
1755
+ installClipboard();
1756
+
1757
+ let isDone: boolean = false;
1758
+ const copied: Promise<void> = copyReplayScreenshotToClipboard(
1759
+ image.promise,
1760
+ ).then((): void => {
1761
+ isDone = true;
1762
+ });
1763
+
1764
+ await flushRejectionTracking();
1765
+
1766
+ expect(isDone).toBe(false);
1767
+
1768
+ image.resolve(makePng());
1769
+ await copied;
1770
+
1771
+ expect(isDone).toBe(true);
1772
+ });
1773
+
1774
+ describe("a failed capture wins over the clipboard's complaint", () => {
1775
+ it("rejects with the capture error when the write also rejects afterwards", async () => {
1776
+ const image: Deferred<Blob> = makeDeferred<Blob>();
1777
+ const write: Deferred<void> = makeDeferred<void>();
1778
+ const captureError: ReplayFrameCaptureError = new ReplayFrameCaptureError(
1779
+ "render-failed",
1780
+ "The browser could not draw this frame.",
1781
+ );
1782
+
1783
+ installClipboard({
1784
+ write: (): Promise<void> => {
1785
+ return write.promise;
1786
+ },
1787
+ });
1788
+
1789
+ const copied: Promise<void> = copyReplayScreenshotToClipboard(
1790
+ image.promise,
1791
+ );
1792
+
1793
+ image.reject(captureError);
1794
+ write.reject(new DOMException("rejected promise", "NotAllowedError"));
1795
+
1796
+ expect(await catchRejection(copied)).toBe(captureError);
1797
+ await flushRejectionTracking();
1798
+ });
1799
+
1800
+ it("rejects with the capture error when the write rejected first", async () => {
1801
+ const image: Deferred<Blob> = makeDeferred<Blob>();
1802
+ const captureError: ReplayFrameCaptureError = new ReplayFrameCaptureError(
1803
+ "encode-failed",
1804
+ "The frame could not be encoded as a PNG.",
1805
+ );
1806
+
1807
+ installClipboard({
1808
+ write: (): Promise<void> => {
1809
+ return Promise.reject(new DOMException("no", "DataError"));
1810
+ },
1811
+ });
1812
+
1813
+ const copied: Promise<void> = copyReplayScreenshotToClipboard(
1814
+ image.promise,
1815
+ );
1816
+
1817
+ /* The write's rejection sits unawaited while the capture runs. */
1818
+ await flushRejectionTracking();
1819
+ image.reject(captureError);
1820
+
1821
+ expect(await catchRejection(copied)).toBe(captureError);
1822
+ await flushRejectionTracking();
1823
+ });
1824
+
1825
+ it("rejects with the capture error even when the write resolved", async () => {
1826
+ const image: Deferred<Blob> = makeDeferred<Blob>();
1827
+ const captureError: ReplayFrameCaptureError = new ReplayFrameCaptureError(
1828
+ "no-document",
1829
+ "The replay has not drawn a frame yet.",
1830
+ );
1831
+
1832
+ installClipboard();
1833
+
1834
+ const copied: Promise<void> = copyReplayScreenshotToClipboard(
1835
+ image.promise,
1836
+ );
1837
+
1838
+ image.reject(captureError);
1839
+
1840
+ expect(await catchRejection(copied)).toBe(captureError);
1841
+ });
1842
+ });
1843
+
1844
+ describe("clipboard failures", () => {
1845
+ const cases: Array<[string, unknown, ReplayClipboardFailure]> = [
1846
+ [
1847
+ "a NotAllowedError DOMException",
1848
+ new DOMException("Write permission denied.", "NotAllowedError"),
1849
+ "denied",
1850
+ ],
1851
+ [
1852
+ "a NotAllowedError-shaped object",
1853
+ { name: "NotAllowedError", message: "Document is not focused." },
1854
+ "denied",
1855
+ ],
1856
+ [
1857
+ "a SecurityError",
1858
+ new DOMException("Blocked", "SecurityError"),
1859
+ "denied",
1860
+ ],
1861
+ [
1862
+ "a NotSupportedError",
1863
+ new DOMException("Type image/png not supported", "NotSupportedError"),
1864
+ "unsupported",
1865
+ ],
1866
+ ["a DataError", { name: "DataError" }, "unsupported"],
1867
+ ["an AbortError", new DOMException("Aborted", "AbortError"), "failed"],
1868
+ ["a TypeError", new TypeError("Failed to execute 'write'"), "failed"],
1869
+ ["a wrongly-cased name", { name: "notallowederror" }, "failed"],
1870
+ ["a non-string name", { name: 42 }, "failed"],
1871
+ ["a bare string", "denied", "failed"],
1872
+ ["null", null, "failed"],
1873
+ ["undefined", undefined, "failed"],
1874
+ ];
1875
+
1876
+ cases.forEach(
1877
+ ([label, rejection, reason]: [
1878
+ string,
1879
+ unknown,
1880
+ ReplayClipboardFailure,
1881
+ ]): void => {
1882
+ it(`maps a write rejected with ${label} to ${reason}`, async () => {
1883
+ installClipboard({
1884
+ write: (): Promise<void> => {
1885
+ return Promise.reject(rejection);
1886
+ },
1887
+ });
1888
+
1889
+ const error: unknown = await catchRejection(
1890
+ copyReplayScreenshotToClipboard(Promise.resolve(makePng())),
1891
+ );
1892
+
1893
+ expect(error).toBeInstanceOf(ReplayScreenshotClipboardError);
1894
+ expect((error as ReplayScreenshotClipboardError).reason).toBe(reason);
1895
+ expect((error as Error).message.length).toBeGreaterThan(0);
1896
+ });
1897
+ },
1898
+ );
1899
+
1900
+ it("maps a write that throws synchronously", async () => {
1901
+ installClipboard({
1902
+ write: (): Promise<void> => {
1903
+ throw new DOMException("Not focused", "NotAllowedError");
1904
+ },
1905
+ });
1906
+
1907
+ const image: Deferred<Blob> = makeDeferred<Blob>();
1908
+ const error: unknown = await catchRejection(
1909
+ copyReplayScreenshotToClipboard(image.promise),
1910
+ );
1911
+
1912
+ expect((error as ReplayScreenshotClipboardError).reason).toBe("denied");
1913
+
1914
+ image.reject(new ReplayFrameCaptureError("render-failed", "late"));
1915
+ await flushRejectionTracking();
1916
+ });
1917
+
1918
+ it("maps a ClipboardItem constructor that throws, without writing", async () => {
1919
+ const harness: ClipboardHarness = installClipboard({
1920
+ itemThrows: {
1921
+ error: new DOMException(
1922
+ "Promises are not supported",
1923
+ "NotSupportedError",
1924
+ ),
1925
+ },
1926
+ });
1927
+ const image: Deferred<Blob> = makeDeferred<Blob>();
1928
+ const error: unknown = await catchRejection(
1929
+ copyReplayScreenshotToClipboard(image.promise),
1930
+ );
1931
+
1932
+ expect(error).toBeInstanceOf(ReplayScreenshotClipboardError);
1933
+ expect((error as ReplayScreenshotClipboardError).reason).toBe(
1934
+ "unsupported",
1935
+ );
1936
+ expect(harness.write).not.toHaveBeenCalled();
1937
+
1938
+ image.reject(new ReplayFrameCaptureError("render-failed", "late"));
1939
+ await flushRejectionTracking();
1940
+ });
1941
+
1942
+ it("maps a ClipboardItem constructor TypeError to failed", async () => {
1943
+ installClipboard({
1944
+ itemThrows: { error: new TypeError("Illegal constructor") },
1945
+ });
1946
+
1947
+ const error: unknown = await catchRejection(
1948
+ copyReplayScreenshotToClipboard(Promise.resolve(makePng())),
1949
+ );
1950
+
1951
+ expect((error as ReplayScreenshotClipboardError).reason).toBe("failed");
1952
+ });
1953
+
1954
+ it("does not leave a write rejection unhandled while the capture is still running", async () => {
1955
+ const image: Deferred<Blob> = makeDeferred<Blob>();
1956
+
1957
+ installClipboard({
1958
+ write: (): Promise<void> => {
1959
+ return Promise.reject(new DOMException("No", "NotAllowedError"));
1960
+ },
1961
+ });
1962
+
1963
+ const copied: Promise<void> = copyReplayScreenshotToClipboard(
1964
+ image.promise,
1965
+ );
1966
+
1967
+ await flushRejectionTracking();
1968
+ image.resolve(makePng());
1969
+
1970
+ const error: unknown = await catchRejection(copied);
1971
+
1972
+ expect((error as ReplayScreenshotClipboardError).reason).toBe("denied");
1973
+ });
1974
+ });
1975
+ });
1976
+
1977
+ describe("downloadReplayScreenshot", () => {
1978
+ interface AnchorAtClick {
1979
+ anchor: HTMLAnchorElement;
1980
+ href: string;
1981
+ download: string;
1982
+ rel: string;
1983
+ display: string;
1984
+ parent: Node | null;
1985
+ }
1986
+
1987
+ let createObjectURL: MockFunction;
1988
+ let revokeObjectURL: MockFunction;
1989
+ let clicks: Array<AnchorAtClick>;
1990
+ let clickError: Error | null;
1991
+
1992
+ beforeEach(() => {
1993
+ jest.useFakeTimers();
1994
+ clicks = [];
1995
+ clickError = null;
1996
+ createObjectURL = getJestMockFunction().mockReturnValue(OBJECT_URL);
1997
+ revokeObjectURL = getJestMockFunction();
1998
+ overrideProperty(window.URL, "createObjectURL", createObjectURL);
1999
+ overrideProperty(window.URL, "revokeObjectURL", revokeObjectURL);
2000
+
2001
+ const click: SpyInstance<() => void> = jest
2002
+ .spyOn(HTMLAnchorElement.prototype, "click")
2003
+ .mockImplementation(function (this: HTMLAnchorElement): void {
2004
+ clicks.push({
2005
+ anchor: this,
2006
+ href: this.href,
2007
+ download: this.download,
2008
+ rel: this.rel,
2009
+ display: this.style.display,
2010
+ parent: this.parentNode,
2011
+ });
2012
+
2013
+ if (clickError) {
2014
+ throw clickError;
2015
+ }
2016
+ });
2017
+
2018
+ restorers.push((): void => {
2019
+ click.mockRestore();
2020
+ });
2021
+ });
2022
+
2023
+ it("saves through a hidden, noopener anchor named after the screenshot", () => {
2024
+ const blob: Blob = makePng();
2025
+
2026
+ downloadReplayScreenshot({
2027
+ blob: blob,
2028
+ fileName: "session-replay-3f9a2c1b-4m07.3s.png",
2029
+ });
2030
+
2031
+ expect(createObjectURL).toHaveBeenCalledTimes(1);
2032
+ expect(createObjectURL.mock.calls[0][0]).toBe(blob);
2033
+ expect(clicks).toHaveLength(1);
2034
+
2035
+ const click: AnchorAtClick = clicks[0] as AnchorAtClick;
2036
+
2037
+ expect(click.href).toBe(OBJECT_URL);
2038
+ expect(click.download).toBe("session-replay-3f9a2c1b-4m07.3s.png");
2039
+ expect(click.rel).toBe("noopener");
2040
+ expect(click.display).toBe("none");
2041
+ expect(click.parent).toBe(document.body);
2042
+ });
2043
+
2044
+ it("removes its anchor after the click and leaves the page's own links alone", () => {
2045
+ const existing: HTMLAnchorElement = document.createElement("a");
2046
+
2047
+ existing.href = "https://oneuptime.test/docs";
2048
+ document.body.appendChild(existing);
2049
+
2050
+ downloadReplayScreenshot({ blob: makePng(), fileName: "frame.png" });
2051
+
2052
+ const click: AnchorAtClick = clicks[0] as AnchorAtClick;
2053
+
2054
+ expect(click.anchor.isConnected).toBe(false);
2055
+ expect(document.querySelectorAll("a")).toHaveLength(1);
2056
+ expect(document.querySelector("a")).toBe(existing);
2057
+ });
2058
+
2059
+ it("keeps the object URL alive for REPLAY_SCREENSHOT_URL_TTL_MS after the click", () => {
2060
+ downloadReplayScreenshot({ blob: makePng(), fileName: "frame.png" });
2061
+
2062
+ expect(revokeObjectURL).not.toHaveBeenCalled();
2063
+
2064
+ jest.advanceTimersByTime(REPLAY_SCREENSHOT_URL_TTL_MS - 1);
2065
+
2066
+ expect(revokeObjectURL).not.toHaveBeenCalled();
2067
+
2068
+ jest.advanceTimersByTime(1);
2069
+
2070
+ expect(revokeObjectURL).toHaveBeenCalledTimes(1);
2071
+ expect(revokeObjectURL).toHaveBeenCalledWith(OBJECT_URL);
2072
+
2073
+ jest.advanceTimersByTime(REPLAY_SCREENSHOT_URL_TTL_MS * 2);
2074
+
2075
+ expect(revokeObjectURL).toHaveBeenCalledTimes(1);
2076
+ });
2077
+
2078
+ it("still removes the anchor and revokes the URL when the click throws, and rethrows", () => {
2079
+ const blocked: Error = new Error("Download blocked by the browser");
2080
+
2081
+ clickError = blocked;
2082
+
2083
+ expect((): void => {
2084
+ downloadReplayScreenshot({ blob: makePng(), fileName: "frame.png" });
2085
+ }).toThrow(blocked);
2086
+
2087
+ expect(clicks).toHaveLength(1);
2088
+ expect((clicks[0] as AnchorAtClick).anchor.isConnected).toBe(false);
2089
+ expect(document.querySelectorAll("a")).toHaveLength(0);
2090
+ expect(revokeObjectURL).not.toHaveBeenCalled();
2091
+
2092
+ jest.advanceTimersByTime(REPLAY_SCREENSHOT_URL_TTL_MS);
2093
+
2094
+ expect(revokeObjectURL).toHaveBeenCalledTimes(1);
2095
+ expect(revokeObjectURL).toHaveBeenCalledWith(OBJECT_URL);
2096
+ });
2097
+
2098
+ it("gives each download its own URL, revoked on its own clock", () => {
2099
+ const firstUrl: string = "blob:https://oneuptime.test/first";
2100
+ const secondUrl: string = "blob:https://oneuptime.test/second";
2101
+
2102
+ createObjectURL
2103
+ .mockReturnValueOnce(firstUrl)
2104
+ .mockReturnValueOnce(secondUrl);
2105
+
2106
+ downloadReplayScreenshot({ blob: makePng(), fileName: "first.png" });
2107
+ jest.advanceTimersByTime(REPLAY_SCREENSHOT_URL_TTL_MS / 2);
2108
+ downloadReplayScreenshot({ blob: makePng(), fileName: "second.png" });
2109
+
2110
+ expect(
2111
+ clicks.map((click: AnchorAtClick): string => {
2112
+ return click.href;
2113
+ }),
2114
+ ).toEqual([firstUrl, secondUrl]);
2115
+
2116
+ jest.advanceTimersByTime(REPLAY_SCREENSHOT_URL_TTL_MS / 2);
2117
+
2118
+ expect(revokeObjectURL.mock.calls).toEqual([[firstUrl]]);
2119
+
2120
+ jest.advanceTimersByTime(REPLAY_SCREENSHOT_URL_TTL_MS / 2);
2121
+
2122
+ expect(revokeObjectURL.mock.calls).toEqual([[firstUrl], [secondUrl]]);
2123
+ });
2124
+ });