@oneuptime/common 13.0.4 → 13.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (673) hide show
  1. package/Models/DatabaseModels/Index.ts +4 -0
  2. package/Models/DatabaseModels/RumApplication.ts +1 -1
  3. package/Models/DatabaseModels/SecurityEventConnection.ts +457 -0
  4. package/Models/DatabaseModels/SecurityEventConnectionRun.ts +263 -0
  5. package/Models/DatabaseModels/TelemetryIngestionKey.ts +2 -2
  6. package/Server/API/AIInvestigationAPI.ts +484 -24
  7. package/Server/API/GoogleSecOpsConnectionAPI.ts +249 -8
  8. package/Server/API/SecurityEventConnectionAPI.ts +250 -0
  9. package/Server/API/TelemetryAPI.ts +402 -265
  10. package/Server/Infrastructure/Postgres/SchemaMigrations/1792700000000-AddSecurityEventConnections.ts +99 -0
  11. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  12. package/Server/Middleware/TelemetryIngest.ts +101 -6
  13. package/Server/Services/ExceptionAggregationService.ts +7 -16
  14. package/Server/Services/GoogleSecOpsConnectionService.ts +15 -0
  15. package/Server/Services/Index.ts +4 -0
  16. package/Server/Services/InventoryItemService.ts +28 -0
  17. package/Server/Services/LogAggregationService.ts +17 -23
  18. package/Server/Services/MetricAggregationService.ts +4 -14
  19. package/Server/Services/SecurityEventConnectionRunService.ts +10 -0
  20. package/Server/Services/SecurityEventConnectionService.ts +538 -0
  21. package/Server/Services/TraceAggregationService.ts +77 -28
  22. package/Server/Types/AnalyticsDatabase/ModelPermission.ts +9 -0
  23. package/Server/Utils/AI/Chat/ObservabilityAssistant.ts +13 -1
  24. package/Server/Utils/AI/SRE/AIInvestigationEngine.ts +15 -1
  25. package/Server/Utils/AI/SRE/InvestigationEvidence.ts +658 -0
  26. package/Server/Utils/AI/SRE/InvestigationReferences.ts +324 -0
  27. package/Server/Utils/AI/Toolbox/Index.ts +38 -0
  28. package/Server/Utils/AI/Toolbox/TimelineTools.ts +9 -2
  29. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +80 -0
  30. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +5 -3
  31. package/Server/Utils/SecurityEvent/Connectors/AwsSecurityHub/AwsSecurityHubClient.ts +1108 -0
  32. package/Server/Utils/SecurityEvent/Connectors/AwsSecurityHub/AwsSecurityHubConnector.ts +732 -0
  33. package/Server/Utils/SecurityEvent/Connectors/ConnectorPlatformHealth.ts +495 -0
  34. package/Server/Utils/SecurityEvent/Connectors/CrowdStrikeFalcon/CrowdStrikeFalconClient.ts +689 -0
  35. package/Server/Utils/SecurityEvent/Connectors/CrowdStrikeFalcon/CrowdStrikeFalconConnector.ts +767 -0
  36. package/Server/Utils/SecurityEvent/Connectors/ElasticSecurity/ElasticSecurityClient.ts +571 -0
  37. package/Server/Utils/SecurityEvent/Connectors/ElasticSecurity/ElasticSecurityConnector.ts +827 -0
  38. package/Server/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdr/MicrosoftDefenderXdrClient.ts +610 -0
  39. package/Server/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdr/MicrosoftDefenderXdrConnector.ts +549 -0
  40. package/Server/Utils/SecurityEvent/Connectors/MicrosoftSentinel/MicrosoftSentinelClient.ts +830 -0
  41. package/Server/Utils/SecurityEvent/Connectors/MicrosoftSentinel/MicrosoftSentinelConnector.ts +684 -0
  42. package/Server/Utils/SecurityEvent/Connectors/Okta/OktaClient.ts +487 -0
  43. package/Server/Utils/SecurityEvent/Connectors/Okta/OktaConnector.ts +778 -0
  44. package/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionPoller.ts +1002 -0
  45. package/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionRunExecutor.ts +556 -0
  46. package/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionTester.ts +275 -0
  47. package/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectorRegistry.ts +53 -0
  48. package/Server/Utils/SecurityEvent/Connectors/Splunk/SplunkClient.ts +709 -0
  49. package/Server/Utils/SecurityEvent/Connectors/Splunk/SplunkConnector.ts +596 -0
  50. package/Server/Utils/SecurityEvent/Connectors/Types.ts +191 -0
  51. package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.ts +353 -65
  52. package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsConnectionTester.ts +777 -0
  53. package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.ts +747 -148
  54. package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsRunExecutor.ts +48 -2
  55. package/Server/Utils/SecurityEvent/SecurityEventDedupe.ts +81 -0
  56. package/Server/Utils/SessionReplay/SessionReplayReadService.ts +741 -40
  57. package/Server/Utils/StartServer.ts +10 -0
  58. package/Server/Utils/Telemetry/EntityRegistry.ts +8 -0
  59. package/Server/Utils/Telemetry/KubernetesResourceAttributes.ts +108 -0
  60. package/Server/Utils/Telemetry/ResourceEntityFilter.ts +174 -93
  61. package/Server/Utils/Telemetry/ResourceFacetPlanner.ts +251 -0
  62. package/Server/Utils/Telemetry/ResourceFacetResolver.ts +248 -413
  63. package/Server/Utils/Telemetry/ServiceDependencyDiscovery.ts +668 -0
  64. package/Server/Utils/Telemetry/TraceContextPropagation.ts +348 -0
  65. package/Server/Utils/Telemetry.ts +18 -0
  66. package/Tests/App/Dashboard/AIInvestigationHeaderStatus.test.tsx +413 -0
  67. package/Tests/App/Dashboard/AIRootCauseFeedItem.test.tsx +285 -0
  68. package/Tests/App/Dashboard/AffectedResourcesDisplay.test.tsx +242 -0
  69. package/Tests/App/Dashboard/AlertEpisodeViewFields.test.tsx +601 -15
  70. package/Tests/App/Dashboard/ChangeAlertStateAIHeader.test.tsx +835 -0
  71. package/Tests/App/Dashboard/CloudResourceTelemetryScope.test.ts +41 -0
  72. package/Tests/App/Dashboard/ConnectionTestModal.test.tsx +604 -0
  73. package/Tests/App/Dashboard/ConnectorTestReportView.test.tsx +738 -0
  74. package/Tests/App/Dashboard/DashboardLazyPages.test.tsx +153 -6
  75. package/Tests/App/Dashboard/EntityDetailPanel.test.tsx +165 -0
  76. package/Tests/App/Dashboard/EpisodeChangeState.test.tsx +1069 -0
  77. package/Tests/App/Dashboard/EpisodeMembersCard.test.tsx +772 -0
  78. package/Tests/App/Dashboard/EventOverviewPages.test.tsx +2354 -0
  79. package/Tests/App/Dashboard/EventOverviewSkeleton.test.tsx +210 -0
  80. package/Tests/App/Dashboard/EventStatBar.test.tsx +392 -0
  81. package/Tests/App/Dashboard/EventStatusPanel.test.tsx +247 -0
  82. package/Tests/App/Dashboard/ExceptionAIAssistance.test.tsx +606 -0
  83. package/Tests/App/Dashboard/ExceptionBreadcrumbTimeline.test.tsx +273 -0
  84. package/Tests/App/Dashboard/ExceptionDetailSideMenu.test.tsx +176 -0
  85. package/Tests/App/Dashboard/ExceptionOverviewCards.test.tsx +366 -0
  86. package/Tests/App/Dashboard/ExceptionSignalPages.test.tsx +371 -0
  87. package/Tests/App/Dashboard/ExceptionStackFrameViewer.test.tsx +440 -0
  88. package/Tests/App/Dashboard/ExceptionSummary.test.tsx +381 -0
  89. package/Tests/App/Dashboard/ExceptionTriage.test.tsx +306 -0
  90. package/Tests/App/Dashboard/ExceptionsResourceFacets.test.tsx +604 -0
  91. package/Tests/App/Dashboard/ExceptionsViewerEntityChips.test.tsx +706 -0
  92. package/Tests/App/Dashboard/FeedOrdering.test.ts +32 -0
  93. package/Tests/App/Dashboard/GoogleSecOpsConnectionForm.test.tsx +838 -0
  94. package/Tests/App/Dashboard/GoogleSecOpsConnectionsErrors.test.tsx +18 -0
  95. package/Tests/App/Dashboard/GoogleSecOpsDiagnostics.test.tsx +297 -20
  96. package/Tests/App/Dashboard/IncidentEpisodeViewFields.test.tsx +618 -13
  97. package/Tests/App/Dashboard/InfrastructureExplorer.test.tsx +254 -207
  98. package/Tests/App/Dashboard/InfrastructureGraph.test.tsx +303 -135
  99. package/Tests/App/Dashboard/InvestigationEvidenceList.test.tsx +1251 -0
  100. package/Tests/App/Dashboard/InvestigationFeedRefresh.test.tsx +303 -0
  101. package/Tests/App/Dashboard/InvestigationPanel.test.tsx +876 -8
  102. package/Tests/App/Dashboard/InvestigationPanelStatus.test.tsx +35 -0
  103. package/Tests/App/Dashboard/InvestigationReferenceLink.test.tsx +177 -0
  104. package/Tests/App/Dashboard/InvestigationReportView.test.tsx +1295 -0
  105. package/Tests/App/Dashboard/KubernetesImageReferenceView.test.tsx +114 -0
  106. package/Tests/App/Dashboard/KubernetesLogsTabChips.test.tsx +285 -0
  107. package/Tests/App/Dashboard/LogsEntityChipNames.test.tsx +316 -0
  108. package/Tests/App/Dashboard/LogsResourceFacetChips.test.tsx +361 -0
  109. package/Tests/App/Dashboard/ProfileTableEntityNames.test.tsx +432 -0
  110. package/Tests/App/Dashboard/ProfileViewEntityName.test.tsx +250 -0
  111. package/Tests/App/Dashboard/ResourceArchiveSettings.test.tsx +10 -0
  112. package/Tests/App/Dashboard/ResourceLogsTabsBareLayout.test.tsx +700 -0
  113. package/Tests/App/Dashboard/ResourcePageEntityNameProps.test.tsx +803 -0
  114. package/Tests/App/Dashboard/RumSessionReplaySideMenu.test.tsx +11 -1
  115. package/Tests/App/Dashboard/ScheduledMaintenanceChangeState.test.tsx +1026 -0
  116. package/Tests/App/Dashboard/ScheduledMaintenanceFeedRefresh.test.tsx +436 -0
  117. package/Tests/App/Dashboard/ScheduledMaintenanceOverviewPage.test.tsx +1195 -0
  118. package/Tests/App/Dashboard/SecurityEventConnectionFormModal.test.tsx +1022 -0
  119. package/Tests/App/Dashboard/SecurityEventConnectionsTable.test.tsx +905 -0
  120. package/Tests/App/Dashboard/SecurityEventsSideMenu.test.tsx +247 -0
  121. package/Tests/App/Dashboard/ServiceMapGraph.test.tsx +375 -274
  122. package/Tests/App/Dashboard/SessionReplayAuditTable.test.tsx +410 -0
  123. package/Tests/App/Dashboard/TelemetryResourceRetentionSettings.test.tsx +774 -0
  124. package/Tests/App/Dashboard/TopErrorsPanelResourceNames.test.tsx +316 -0
  125. package/Tests/App/Dashboard/TopologyDataLoading.test.tsx +30 -0
  126. package/Tests/App/Dashboard/TopologyPageNavigation.test.tsx +88 -24
  127. package/Tests/App/Dashboard/TraceExplorer.test.tsx +1590 -0
  128. package/Tests/App/Dashboard/TraceWaterfall.test.tsx +488 -0
  129. package/Tests/App/Dashboard/TracesAnalyticsSeriesLabels.test.tsx +367 -0
  130. package/Tests/App/Dashboard/TracesEntityNames.test.tsx +585 -0
  131. package/Tests/App/Dashboard/TracesResourceFacets.test.tsx +613 -0
  132. package/Tests/App/Dashboard/UseServiceNames.test.tsx +359 -0
  133. package/Tests/Models/DatabaseModels/DomainRoleTierCoverage.test.ts +7 -0
  134. package/Tests/Models/InventoryItemNaming.test.ts +132 -16
  135. package/Tests/Models/SecurityDomainAccessControl.test.ts +61 -17
  136. package/Tests/Server/API/AIInvestigationAPI.test.ts +611 -2
  137. package/Tests/Server/API/AIInvestigationEvidenceAPI.test.ts +1098 -0
  138. package/Tests/Server/API/GoogleSecOpsConnectionAPI.test.ts +409 -0
  139. package/Tests/Server/API/SecurityEventConnectionAPI.test.ts +856 -0
  140. package/Tests/Server/API/SessionReplayAPI.test.ts +528 -1
  141. package/Tests/Server/API/TelemetryFacetsAPI.test.ts +810 -0
  142. package/Tests/Server/Infrastructure/E2EClickHouseFixtureAccess.test.ts +240 -0
  143. package/Tests/Server/Middleware/TelemetryIngestBrowserKey.test.ts +229 -0
  144. package/Tests/Server/Services/InventoryItemDisplayName.test.ts +66 -0
  145. package/Tests/Server/Services/OpenTelemetryResourceRetentionMemo.test.ts +28 -1
  146. package/Tests/Server/Services/SecurityEventConnectionService.test.ts +1233 -0
  147. package/Tests/Server/Services/TelemetryIngestionKeyValidation.test.ts +53 -0
  148. package/Tests/Server/Services/TelemetryResourceFacetCounting.test.ts +218 -0
  149. package/Tests/Server/Services/TelemetryResourceFacetFilters.test.ts +155 -1
  150. package/Tests/Server/Services/TraceAggregationExceptionScope.test.ts +142 -0
  151. package/Tests/Server/Services/TraceAnalyticsResourceDisplayNames.test.ts +85 -24
  152. package/Tests/Server/Types/Database/Permissions/SecurityRoleAccess.test.ts +33 -0
  153. package/Tests/Server/Utils/AI/InvestigationEvidence.test.ts +1395 -0
  154. package/Tests/Server/Utils/AI/InvestigationReferences.test.ts +616 -0
  155. package/Tests/Server/Utils/AI/TimelineTools.test.ts +119 -0
  156. package/Tests/Server/Utils/AI/Toolbox/AIToolbox.test.ts +151 -0
  157. package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +65 -0
  158. package/Tests/Server/Utils/AnalyticsDatabase/StatementGeneratorExceptionScope.test.ts +224 -0
  159. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorPerSeriesFanout.test.ts +55 -2
  160. package/Tests/Server/Utils/SecurityEvent/Connectors/AwsSecurityHub/AwsSecurityHubClient.test.ts +1240 -0
  161. package/Tests/Server/Utils/SecurityEvent/Connectors/AwsSecurityHub/AwsSecurityHubConnector.test.ts +1853 -0
  162. package/Tests/Server/Utils/SecurityEvent/Connectors/ConnectorPlatformHealth.test.ts +1129 -0
  163. package/Tests/Server/Utils/SecurityEvent/Connectors/CrowdStrikeFalcon/CrowdStrikeFalconClient.test.ts +1066 -0
  164. package/Tests/Server/Utils/SecurityEvent/Connectors/CrowdStrikeFalcon/CrowdStrikeFalconConnector.test.ts +1568 -0
  165. package/Tests/Server/Utils/SecurityEvent/Connectors/ElasticSecurity/ElasticSecurityClient.test.ts +930 -0
  166. package/Tests/Server/Utils/SecurityEvent/Connectors/ElasticSecurity/ElasticSecurityConnector.test.ts +1409 -0
  167. package/Tests/Server/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdr/MicrosoftDefenderXdrClient.test.ts +763 -0
  168. package/Tests/Server/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdr/MicrosoftDefenderXdrConnector.test.ts +1296 -0
  169. package/Tests/Server/Utils/SecurityEvent/Connectors/MicrosoftSentinel/MicrosoftSentinelClient.test.ts +1102 -0
  170. package/Tests/Server/Utils/SecurityEvent/Connectors/MicrosoftSentinel/MicrosoftSentinelConnector.test.ts +1174 -0
  171. package/Tests/Server/Utils/SecurityEvent/Connectors/Okta/OktaClient.test.ts +1032 -0
  172. package/Tests/Server/Utils/SecurityEvent/Connectors/Okta/OktaConnector.test.ts +1558 -0
  173. package/Tests/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionPoller.test.ts +3041 -0
  174. package/Tests/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionRunExecutor.test.ts +1168 -0
  175. package/Tests/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionTester.test.ts +906 -0
  176. package/Tests/Server/Utils/SecurityEvent/Connectors/Splunk/SplunkClient.test.ts +1068 -0
  177. package/Tests/Server/Utils/SecurityEvent/Connectors/Splunk/SplunkConnector.test.ts +1004 -0
  178. package/Tests/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsConnectionTester.test.ts +784 -0
  179. package/Tests/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsRunExecutor.test.ts +113 -1
  180. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsClient.test.ts +5 -1
  181. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsDiagnostics.test.ts +93 -13
  182. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsHttpIntegration.test.ts +23 -1
  183. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPoller.test.ts +25 -3
  184. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerFailureTaxonomy.test.ts +22 -8
  185. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerHardening.test.ts +82 -16
  186. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsSearchDetections.test.ts +730 -0
  187. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsThreePassPoller.test.ts +1577 -0
  188. package/Tests/Server/Utils/SessionReplay/SessionReplayOriginAllowlist.test.ts +41 -0
  189. package/Tests/Server/Utils/SessionReplay/SessionReplayReadServiceQueries.test.ts +1453 -18
  190. package/Tests/Server/Utils/Telemetry/KubernetesResourceAttributes.test.ts +162 -0
  191. package/Tests/Server/Utils/Telemetry/ResourceEntityFilter.test.ts +460 -13
  192. package/Tests/Server/Utils/Telemetry/ResourceFacetPlanner.test.ts +678 -0
  193. package/Tests/Server/Utils/Telemetry/ResourceFacetResolver.test.ts +348 -2
  194. package/Tests/Server/Utils/Telemetry/ServiceDependencyDiscovery.test.ts +525 -0
  195. package/Tests/Server/Utils/Telemetry/TraceContextPropagation.test.ts +420 -0
  196. package/Tests/Types/Rum/SessionReplayApiContracts.test.ts +73 -0
  197. package/Tests/Types/Rum/SessionReplayTransportContracts.test.ts +95 -0
  198. package/Tests/Types/SecurityEvent/Connectors/ConnectorDiagnostics.test.ts +82 -0
  199. package/Tests/Types/SecurityEvent/Connectors/SecurityEventConnectorCatalog.test.ts +314 -0
  200. package/Tests/Types/Telemetry/ExceptionSpanScope.test.ts +89 -0
  201. package/Tests/Types/Telemetry/ResourceEntityFacet.test.ts +96 -0
  202. package/Tests/Types/Telemetry/ResourceFacetCatalog.test.ts +304 -0
  203. package/Tests/UI/Components/ActiveFilterChipsLockedFilters.test.tsx +357 -0
  204. package/Tests/UI/Components/Card.test.tsx +312 -1
  205. package/Tests/UI/Components/CardModelDetailHeaderLayout.test.tsx +253 -0
  206. package/Tests/UI/Components/CardModelDetailRefresher.test.tsx +342 -0
  207. package/Tests/UI/Components/Checkbox.test.tsx +79 -0
  208. package/Tests/UI/Components/CopyTextButton.test.tsx +142 -0
  209. package/Tests/UI/Components/DetailCompactStyle.test.tsx +621 -0
  210. package/Tests/UI/Components/ErrorBoundary.test.tsx +60 -0
  211. package/Tests/UI/Components/FacetSections.test.tsx +581 -0
  212. package/Tests/UI/Components/FacetSidebarControls.test.tsx +812 -0
  213. package/Tests/UI/Components/FacetVisibility.test.ts +429 -0
  214. package/Tests/UI/Components/LockedFilterActions.test.tsx +396 -0
  215. package/Tests/UI/Components/LockedFilterChip.test.tsx +651 -0
  216. package/Tests/UI/Components/LogsEntityNames.test.ts +1769 -0
  217. package/Tests/UI/Components/LogsEntityNamesComponents.test.tsx +696 -0
  218. package/Tests/UI/Components/LogsFacetSidebarHiddenFacets.test.tsx +600 -0
  219. package/Tests/UI/Components/LogsViewerEntityNames.test.tsx +713 -0
  220. package/Tests/UI/Components/MarkdownInlineReferences.test.tsx +2079 -0
  221. package/Tests/UI/Components/MemberRoleAssignment.test.tsx +446 -0
  222. package/Tests/UI/Components/ModelDetailRefetch.test.tsx +397 -0
  223. package/Tests/UI/Components/ModelListRefetch.test.tsx +233 -0
  224. package/Tests/UI/Components/ModelPageModelChange.test.tsx +409 -0
  225. package/Tests/UI/Components/PaginationIntegration.test.tsx +25 -0
  226. package/Tests/UI/Components/ResourceFacetConfigs.test.ts +172 -0
  227. package/Tests/UI/Components/SavedViewsSidebarIntegration.test.tsx +32 -14
  228. package/Tests/UI/Components/TableSortAndMobileColumns.test.tsx +44 -1
  229. package/Tests/UI/Components/TelemetryActiveFilterChipsLockedFilters.test.tsx +304 -0
  230. package/Tests/UI/Components/TelemetryFacetSidebarHiddenFacets.test.tsx +718 -0
  231. package/Tests/UI/Monitor/EvaluationLogList.test.tsx +303 -0
  232. package/Tests/UI/Monitor/MonitorSummarySnapshotRender.test.tsx +55 -1
  233. package/Tests/UI/Rum/FidelityNotices.test.ts +22 -0
  234. package/Tests/UI/Rum/RecordingHealthCard.test.tsx +120 -136
  235. package/Tests/UI/Rum/RecordingHealthDashboard.test.tsx +988 -0
  236. package/Tests/UI/Rum/ReplayCard.test.tsx +32 -0
  237. package/Tests/UI/Rum/ReplayCorrelationPanel.test.tsx +709 -12
  238. package/Tests/UI/Rum/ReplayHeader.test.tsx +50 -1
  239. package/Tests/UI/Rum/ReplayPinControl.test.tsx +67 -10
  240. package/Tests/UI/Rum/ReplayPlayerChrome.test.tsx +41 -12
  241. package/Tests/UI/Rum/ReplayStage.test.tsx +663 -1
  242. package/Tests/UI/Rum/ReplayStageOverlays.test.tsx +173 -0
  243. package/Tests/UI/Rum/ReplayUi.test.tsx +189 -2
  244. package/Tests/UI/Rum/ReplayUserSessionsMenu.test.tsx +91 -0
  245. package/Tests/UI/Rum/SessionReplayEmptyState.test.tsx +1 -1
  246. package/Tests/UI/Rum/SessionReplaySettingsPolicyLoading.test.tsx +585 -0
  247. package/Tests/UI/Rum/SessionReplayTable.test.tsx +844 -51
  248. package/Tests/UI/Rum/SessionReplayUsersTable.test.tsx +29 -1
  249. package/Tests/UI/Telemetry/TelemetryDetailPanel.test.tsx +288 -0
  250. package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +31 -1
  251. package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +6 -0
  252. package/Tests/UI/Utils/Clipboard.test.ts +154 -0
  253. package/Tests/UI/Utils/Navigation.test.ts +24 -0
  254. package/Tests/UI/Utils/Telemetry/TelemetryEntityNames.test.ts +2205 -0
  255. package/Tests/UI/Utils/Telemetry/UseTelemetryEntityNames.test.tsx +844 -0
  256. package/Tests/Utils/AI/InvestigationReport.test.ts +2430 -0
  257. package/Tests/Utils/APIOutgoingRequestTracer.test.ts +137 -0
  258. package/Tests/Utils/Rum/SessionReplayHealthDiagnosis.test.ts +40 -0
  259. package/Tests/Utils/Rum/SessionReplayRecordingEnded.test.ts +619 -0
  260. package/Tests/Utils/SecurityEvent/Connectors/AwsSecurityHubNormalizer.test.ts +631 -0
  261. package/Tests/Utils/SecurityEvent/Connectors/CrowdStrikeFalconNormalizer.test.ts +479 -0
  262. package/Tests/Utils/SecurityEvent/Connectors/ElasticSecurityNormalizer.test.ts +507 -0
  263. package/Tests/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdrNormalizer.test.ts +686 -0
  264. package/Tests/Utils/SecurityEvent/Connectors/MicrosoftSentinelNormalizer.test.ts +474 -0
  265. package/Tests/Utils/SecurityEvent/Connectors/OktaNormalizer.test.ts +736 -0
  266. package/Tests/Utils/SecurityEvent/Connectors/SplunkNormalizer.test.ts +407 -0
  267. package/Tests/Utils/SecurityEvent/GoogleSecOpsAlertNormalizer.test.ts +129 -0
  268. package/Tests/Utils/Telemetry/CrossSignalScope.test.ts +57 -1
  269. package/Tests/Utils/Telemetry/EntityRelationship.test.ts +36 -0
  270. package/Tests/Utils/Telemetry/LockedFilterSearch.test.ts +600 -0
  271. package/Tests/Utils/Telemetry/NetworkHost.test.ts +48 -0
  272. package/Tests/Utils/Telemetry/OriginAllowList.test.ts +154 -0
  273. package/Tests/Utils/Traces/CriticalPath.test.ts +25 -0
  274. package/Types/AI/AIChatTypes.ts +8 -0
  275. package/Types/AI/InvestigationEvidence.ts +96 -0
  276. package/Types/Monitor/MonitorEvaluationSummary.ts +5 -0
  277. package/Types/Permission.ts +2 -2
  278. package/Types/Rum/SessionReplay.ts +109 -0
  279. package/Types/Rum/SessionReplayApi.ts +31 -0
  280. package/Types/SecurityEvent/Connectors/ConnectorDiagnostics.ts +111 -0
  281. package/Types/SecurityEvent/Connectors/SecurityEventConnectionDiagnostics.ts +87 -0
  282. package/Types/SecurityEvent/Connectors/SecurityEventConnectorCatalog.ts +492 -0
  283. package/Types/SecurityEvent/Connectors/SecurityEventConnectorProvider.ts +41 -0
  284. package/Types/SecurityEvent/GoogleSecOpsDiagnostics.ts +56 -1
  285. package/Types/SecurityEvent/GoogleSecOpsRegion.ts +34 -0
  286. package/Types/Telemetry/EntityType.ts +14 -0
  287. package/Types/Telemetry/ExceptionSpanScope.ts +83 -0
  288. package/Types/Telemetry/LockedFilterDetail.ts +46 -0
  289. package/Types/Telemetry/ResourceEntityFacet.ts +17 -10
  290. package/Types/Telemetry/ResourceFacetCatalog.ts +171 -0
  291. package/UI/Components/Card/Card.tsx +118 -83
  292. package/UI/Components/Checkbox/Checkbox.tsx +8 -5
  293. package/UI/Components/CopyTextButton/CopyTextButton.tsx +7 -1
  294. package/UI/Components/CustomFields/CustomFieldsDetail.tsx +4 -1
  295. package/UI/Components/Detail/Detail.tsx +137 -22
  296. package/UI/Components/ErrorBoundary.tsx +38 -0
  297. package/UI/Components/LogsViewer/LogsEntityNames.ts +912 -0
  298. package/UI/Components/LogsViewer/LogsViewer.tsx +156 -77
  299. package/UI/Components/LogsViewer/components/ActiveFilterChips.tsx +28 -11
  300. package/UI/Components/LogsViewer/components/FacetSection.tsx +66 -79
  301. package/UI/Components/LogsViewer/components/FacetValueRow.tsx +17 -2
  302. package/UI/Components/LogsViewer/components/LogDetailsPanel.tsx +45 -5
  303. package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +100 -19
  304. package/UI/Components/LogsViewer/components/LogsFacetSidebar.tsx +156 -39
  305. package/UI/Components/LogsViewer/components/LogsTable.tsx +30 -8
  306. package/UI/Components/LogsViewer/types.ts +6 -0
  307. package/UI/Components/Markdown.tsx/InlineReferences.tsx +628 -0
  308. package/UI/Components/Markdown.tsx/MarkdownViewer.tsx +385 -312
  309. package/UI/Components/MemberRoleAssignment/MemberRoleAssignment.tsx +50 -22
  310. package/UI/Components/ModelDetail/CardModelDetail.tsx +15 -0
  311. package/UI/Components/ModelDetail/ModelDetail.tsx +64 -19
  312. package/UI/Components/ModelList/ModelList.tsx +33 -8
  313. package/UI/Components/Page/ModelPage.tsx +119 -54
  314. package/UI/Components/Table/Table.tsx +8 -0
  315. package/UI/Components/Table/TableRow.tsx +13 -5
  316. package/UI/Components/TelemetryViewer/FacetVisibility.ts +229 -0
  317. package/UI/Components/TelemetryViewer/ResourceFacetConfigs.ts +46 -0
  318. package/UI/Components/TelemetryViewer/TelemetryViewer.tsx +11 -0
  319. package/UI/Components/TelemetryViewer/components/FacetSearchInput.tsx +79 -0
  320. package/UI/Components/TelemetryViewer/components/FacetSectionHeader.tsx +69 -0
  321. package/UI/Components/TelemetryViewer/components/FacetShowMoreButton.tsx +36 -0
  322. package/UI/Components/TelemetryViewer/components/HiddenFacetsFooter.tsx +88 -0
  323. package/UI/Components/TelemetryViewer/components/LockedFilterActions.tsx +205 -0
  324. package/UI/Components/TelemetryViewer/components/LockedFilterChip.tsx +496 -0
  325. package/UI/Components/TelemetryViewer/components/TelemetryActiveFilterChips.tsx +30 -10
  326. package/UI/Components/TelemetryViewer/components/TelemetryDetailPanel.tsx +203 -10
  327. package/UI/Components/TelemetryViewer/components/TelemetryFacetSection.tsx +60 -73
  328. package/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.tsx +94 -7
  329. package/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.tsx +17 -2
  330. package/UI/Components/TelemetryViewer/types.ts +19 -0
  331. package/UI/Components/TelemetryViewer/useFacetSearchExemptions.ts +158 -0
  332. package/UI/Components/TelemetryViewer/useFacetSectionSearch.ts +102 -0
  333. package/UI/Utils/Clipboard.ts +68 -2
  334. package/UI/Utils/Navigation.ts +17 -2
  335. package/UI/Utils/Telemetry/TelemetryEntityNames.ts +620 -0
  336. package/UI/Utils/Telemetry/UseTelemetryEntityNames.ts +151 -0
  337. package/Utils/AI/InvestigationReport.ts +1789 -0
  338. package/Utils/API.ts +90 -1
  339. package/Utils/Rum/SessionReplayHealth.ts +11 -0
  340. package/Utils/Rum/SessionReplayRecordingEnded.ts +134 -0
  341. package/Utils/SecurityEvent/Connectors/AwsSecurityHubNormalizer.ts +427 -0
  342. package/Utils/SecurityEvent/Connectors/CrowdStrikeFalconNormalizer.ts +197 -0
  343. package/Utils/SecurityEvent/Connectors/ElasticSecurityNormalizer.ts +464 -0
  344. package/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdrNormalizer.ts +567 -0
  345. package/Utils/SecurityEvent/Connectors/MicrosoftSentinelNormalizer.ts +299 -0
  346. package/Utils/SecurityEvent/Connectors/OktaNormalizer.ts +467 -0
  347. package/Utils/SecurityEvent/Connectors/SplunkNormalizer.ts +439 -0
  348. package/Utils/SecurityEvent/GoogleSecOpsAlertNormalizer.ts +124 -12
  349. package/Utils/Telemetry/CrossSignalScope.ts +22 -8
  350. package/Utils/Telemetry/EntityRelationship.ts +8 -0
  351. package/Utils/Telemetry/LockedFilterSearch.ts +412 -0
  352. package/Utils/Telemetry/NetworkHost.ts +43 -0
  353. package/Utils/Telemetry/OriginAllowList.ts +139 -4
  354. package/Utils/Traces/CriticalPath.ts +10 -4
  355. package/build/dist/Models/DatabaseModels/Index.js +4 -0
  356. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  357. package/build/dist/Models/DatabaseModels/RumApplication.js +1 -1
  358. package/build/dist/Models/DatabaseModels/RumApplication.js.map +1 -1
  359. package/build/dist/Models/DatabaseModels/SecurityEventConnection.js +490 -0
  360. package/build/dist/Models/DatabaseModels/SecurityEventConnection.js.map +1 -0
  361. package/build/dist/Models/DatabaseModels/SecurityEventConnectionRun.js +287 -0
  362. package/build/dist/Models/DatabaseModels/SecurityEventConnectionRun.js.map +1 -0
  363. package/build/dist/Models/DatabaseModels/TelemetryIngestionKey.js +2 -2
  364. package/build/dist/Models/DatabaseModels/TelemetryIngestionKey.js.map +1 -1
  365. package/build/dist/Server/API/AIInvestigationAPI.js +336 -27
  366. package/build/dist/Server/API/AIInvestigationAPI.js.map +1 -1
  367. package/build/dist/Server/API/GoogleSecOpsConnectionAPI.js +182 -7
  368. package/build/dist/Server/API/GoogleSecOpsConnectionAPI.js.map +1 -1
  369. package/build/dist/Server/API/SecurityEventConnectionAPI.js +171 -0
  370. package/build/dist/Server/API/SecurityEventConnectionAPI.js.map +1 -0
  371. package/build/dist/Server/API/TelemetryAPI.js +248 -170
  372. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  373. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1792700000000-AddSecurityEventConnections.js +40 -0
  374. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1792700000000-AddSecurityEventConnections.js.map +1 -0
  375. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  376. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  377. package/build/dist/Server/Middleware/TelemetryIngest.js +57 -4
  378. package/build/dist/Server/Middleware/TelemetryIngest.js.map +1 -1
  379. package/build/dist/Server/Services/ExceptionAggregationService.js +7 -16
  380. package/build/dist/Server/Services/ExceptionAggregationService.js.map +1 -1
  381. package/build/dist/Server/Services/GoogleSecOpsConnectionService.js +8 -0
  382. package/build/dist/Server/Services/GoogleSecOpsConnectionService.js.map +1 -1
  383. package/build/dist/Server/Services/Index.js +4 -0
  384. package/build/dist/Server/Services/Index.js.map +1 -1
  385. package/build/dist/Server/Services/InventoryItemService.js +22 -0
  386. package/build/dist/Server/Services/InventoryItemService.js.map +1 -1
  387. package/build/dist/Server/Services/LogAggregationService.js +8 -17
  388. package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
  389. package/build/dist/Server/Services/MetricAggregationService.js +4 -14
  390. package/build/dist/Server/Services/MetricAggregationService.js.map +1 -1
  391. package/build/dist/Server/Services/SecurityEventConnectionRunService.js +9 -0
  392. package/build/dist/Server/Services/SecurityEventConnectionRunService.js.map +1 -0
  393. package/build/dist/Server/Services/SecurityEventConnectionService.js +356 -0
  394. package/build/dist/Server/Services/SecurityEventConnectionService.js.map +1 -0
  395. package/build/dist/Server/Services/TraceAggregationService.js +51 -25
  396. package/build/dist/Server/Services/TraceAggregationService.js.map +1 -1
  397. package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js +9 -0
  398. package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js.map +1 -1
  399. package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js +3 -0
  400. package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js.map +1 -1
  401. package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js +14 -7
  402. package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js.map +1 -1
  403. package/build/dist/Server/Utils/AI/SRE/InvestigationEvidence.js +468 -0
  404. package/build/dist/Server/Utils/AI/SRE/InvestigationEvidence.js.map +1 -0
  405. package/build/dist/Server/Utils/AI/SRE/InvestigationReferences.js +230 -0
  406. package/build/dist/Server/Utils/AI/SRE/InvestigationReferences.js.map +1 -0
  407. package/build/dist/Server/Utils/AI/Toolbox/Index.js +32 -0
  408. package/build/dist/Server/Utils/AI/Toolbox/Index.js.map +1 -1
  409. package/build/dist/Server/Utils/AI/Toolbox/TimelineTools.js +6 -2
  410. package/build/dist/Server/Utils/AI/Toolbox/TimelineTools.js.map +1 -1
  411. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +59 -0
  412. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
  413. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +5 -3
  414. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  415. package/build/dist/Server/Utils/SecurityEvent/Connectors/AwsSecurityHub/AwsSecurityHubClient.js +752 -0
  416. package/build/dist/Server/Utils/SecurityEvent/Connectors/AwsSecurityHub/AwsSecurityHubClient.js.map +1 -0
  417. package/build/dist/Server/Utils/SecurityEvent/Connectors/AwsSecurityHub/AwsSecurityHubConnector.js +493 -0
  418. package/build/dist/Server/Utils/SecurityEvent/Connectors/AwsSecurityHub/AwsSecurityHubConnector.js.map +1 -0
  419. package/build/dist/Server/Utils/SecurityEvent/Connectors/ConnectorPlatformHealth.js +352 -0
  420. package/build/dist/Server/Utils/SecurityEvent/Connectors/ConnectorPlatformHealth.js.map +1 -0
  421. package/build/dist/Server/Utils/SecurityEvent/Connectors/CrowdStrikeFalcon/CrowdStrikeFalconClient.js +405 -0
  422. package/build/dist/Server/Utils/SecurityEvent/Connectors/CrowdStrikeFalcon/CrowdStrikeFalconClient.js.map +1 -0
  423. package/build/dist/Server/Utils/SecurityEvent/Connectors/CrowdStrikeFalcon/CrowdStrikeFalconConnector.js +466 -0
  424. package/build/dist/Server/Utils/SecurityEvent/Connectors/CrowdStrikeFalcon/CrowdStrikeFalconConnector.js.map +1 -0
  425. package/build/dist/Server/Utils/SecurityEvent/Connectors/ElasticSecurity/ElasticSecurityClient.js +344 -0
  426. package/build/dist/Server/Utils/SecurityEvent/Connectors/ElasticSecurity/ElasticSecurityClient.js.map +1 -0
  427. package/build/dist/Server/Utils/SecurityEvent/Connectors/ElasticSecurity/ElasticSecurityConnector.js +511 -0
  428. package/build/dist/Server/Utils/SecurityEvent/Connectors/ElasticSecurity/ElasticSecurityConnector.js.map +1 -0
  429. package/build/dist/Server/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdr/MicrosoftDefenderXdrClient.js +406 -0
  430. package/build/dist/Server/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdr/MicrosoftDefenderXdrClient.js.map +1 -0
  431. package/build/dist/Server/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdr/MicrosoftDefenderXdrConnector.js +336 -0
  432. package/build/dist/Server/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdr/MicrosoftDefenderXdrConnector.js.map +1 -0
  433. package/build/dist/Server/Utils/SecurityEvent/Connectors/MicrosoftSentinel/MicrosoftSentinelClient.js +474 -0
  434. package/build/dist/Server/Utils/SecurityEvent/Connectors/MicrosoftSentinel/MicrosoftSentinelClient.js.map +1 -0
  435. package/build/dist/Server/Utils/SecurityEvent/Connectors/MicrosoftSentinel/MicrosoftSentinelConnector.js +417 -0
  436. package/build/dist/Server/Utils/SecurityEvent/Connectors/MicrosoftSentinel/MicrosoftSentinelConnector.js.map +1 -0
  437. package/build/dist/Server/Utils/SecurityEvent/Connectors/Okta/OktaClient.js +314 -0
  438. package/build/dist/Server/Utils/SecurityEvent/Connectors/Okta/OktaClient.js.map +1 -0
  439. package/build/dist/Server/Utils/SecurityEvent/Connectors/Okta/OktaConnector.js +494 -0
  440. package/build/dist/Server/Utils/SecurityEvent/Connectors/Okta/OktaConnector.js.map +1 -0
  441. package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionPoller.js +719 -0
  442. package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionPoller.js.map +1 -0
  443. package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionRunExecutor.js +404 -0
  444. package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionRunExecutor.js.map +1 -0
  445. package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionTester.js +209 -0
  446. package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionTester.js.map +1 -0
  447. package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectorRegistry.js +40 -0
  448. package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectorRegistry.js.map +1 -0
  449. package/build/dist/Server/Utils/SecurityEvent/Connectors/Splunk/SplunkClient.js +494 -0
  450. package/build/dist/Server/Utils/SecurityEvent/Connectors/Splunk/SplunkClient.js.map +1 -0
  451. package/build/dist/Server/Utils/SecurityEvent/Connectors/Splunk/SplunkConnector.js +379 -0
  452. package/build/dist/Server/Utils/SecurityEvent/Connectors/Splunk/SplunkConnector.js.map +1 -0
  453. package/build/dist/Server/Utils/SecurityEvent/Connectors/Types.js +27 -0
  454. package/build/dist/Server/Utils/SecurityEvent/Connectors/Types.js.map +1 -0
  455. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js +234 -61
  456. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js.map +1 -1
  457. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsConnectionTester.js +537 -0
  458. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsConnectionTester.js.map +1 -0
  459. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js +572 -115
  460. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js.map +1 -1
  461. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsRunExecutor.js +35 -1
  462. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsRunExecutor.js.map +1 -1
  463. package/build/dist/Server/Utils/SecurityEvent/SecurityEventDedupe.js +52 -0
  464. package/build/dist/Server/Utils/SecurityEvent/SecurityEventDedupe.js.map +1 -0
  465. package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js +521 -32
  466. package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js.map +1 -1
  467. package/build/dist/Server/Utils/StartServer.js +10 -0
  468. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  469. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +8 -0
  470. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
  471. package/build/dist/Server/Utils/Telemetry/KubernetesResourceAttributes.js +80 -0
  472. package/build/dist/Server/Utils/Telemetry/KubernetesResourceAttributes.js.map +1 -0
  473. package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js +122 -67
  474. package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js.map +1 -1
  475. package/build/dist/Server/Utils/Telemetry/ResourceFacetPlanner.js +146 -0
  476. package/build/dist/Server/Utils/Telemetry/ResourceFacetPlanner.js.map +1 -0
  477. package/build/dist/Server/Utils/Telemetry/ResourceFacetResolver.js +174 -248
  478. package/build/dist/Server/Utils/Telemetry/ResourceFacetResolver.js.map +1 -1
  479. package/build/dist/Server/Utils/Telemetry/ServiceDependencyDiscovery.js +492 -0
  480. package/build/dist/Server/Utils/Telemetry/ServiceDependencyDiscovery.js.map +1 -0
  481. package/build/dist/Server/Utils/Telemetry/TraceContextPropagation.js +259 -0
  482. package/build/dist/Server/Utils/Telemetry/TraceContextPropagation.js.map +1 -0
  483. package/build/dist/Server/Utils/Telemetry.js +11 -5
  484. package/build/dist/Server/Utils/Telemetry.js.map +1 -1
  485. package/build/dist/Types/AI/InvestigationEvidence.js +2 -0
  486. package/build/dist/Types/AI/InvestigationEvidence.js.map +1 -0
  487. package/build/dist/Types/Permission.js +2 -2
  488. package/build/dist/Types/Permission.js.map +1 -1
  489. package/build/dist/Types/Rum/SessionReplay.js +95 -0
  490. package/build/dist/Types/Rum/SessionReplay.js.map +1 -1
  491. package/build/dist/Types/Rum/SessionReplayApi.js.map +1 -1
  492. package/build/dist/Types/SecurityEvent/Connectors/ConnectorDiagnostics.js +18 -0
  493. package/build/dist/Types/SecurityEvent/Connectors/ConnectorDiagnostics.js.map +1 -0
  494. package/build/dist/Types/SecurityEvent/Connectors/SecurityEventConnectionDiagnostics.js +2 -0
  495. package/build/dist/Types/SecurityEvent/Connectors/SecurityEventConnectionDiagnostics.js.map +1 -0
  496. package/build/dist/Types/SecurityEvent/Connectors/SecurityEventConnectorCatalog.js +374 -0
  497. package/build/dist/Types/SecurityEvent/Connectors/SecurityEventConnectorCatalog.js.map +1 -0
  498. package/build/dist/Types/SecurityEvent/Connectors/SecurityEventConnectorProvider.js +35 -0
  499. package/build/dist/Types/SecurityEvent/Connectors/SecurityEventConnectorProvider.js.map +1 -0
  500. package/build/dist/Types/SecurityEvent/GoogleSecOpsRegion.js +35 -0
  501. package/build/dist/Types/SecurityEvent/GoogleSecOpsRegion.js.map +1 -0
  502. package/build/dist/Types/Telemetry/EntityType.js +14 -0
  503. package/build/dist/Types/Telemetry/EntityType.js.map +1 -1
  504. package/build/dist/Types/Telemetry/ExceptionSpanScope.js +59 -0
  505. package/build/dist/Types/Telemetry/ExceptionSpanScope.js.map +1 -0
  506. package/build/dist/Types/Telemetry/LockedFilterDetail.js +16 -0
  507. package/build/dist/Types/Telemetry/LockedFilterDetail.js.map +1 -0
  508. package/build/dist/Types/Telemetry/ResourceEntityFacet.js +16 -10
  509. package/build/dist/Types/Telemetry/ResourceEntityFacet.js.map +1 -1
  510. package/build/dist/Types/Telemetry/ResourceFacetCatalog.js +119 -0
  511. package/build/dist/Types/Telemetry/ResourceFacetCatalog.js.map +1 -0
  512. package/build/dist/UI/Components/Card/Card.js +34 -17
  513. package/build/dist/UI/Components/Card/Card.js.map +1 -1
  514. package/build/dist/UI/Components/Checkbox/Checkbox.js +6 -4
  515. package/build/dist/UI/Components/Checkbox/Checkbox.js.map +1 -1
  516. package/build/dist/UI/Components/CopyTextButton/CopyTextButton.js +5 -1
  517. package/build/dist/UI/Components/CopyTextButton/CopyTextButton.js.map +1 -1
  518. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js +1 -1
  519. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js.map +1 -1
  520. package/build/dist/UI/Components/Detail/Detail.js +63 -13
  521. package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
  522. package/build/dist/UI/Components/ErrorBoundary.js +34 -0
  523. package/build/dist/UI/Components/ErrorBoundary.js.map +1 -1
  524. package/build/dist/UI/Components/LogsViewer/LogsEntityNames.js +535 -0
  525. package/build/dist/UI/Components/LogsViewer/LogsEntityNames.js.map +1 -0
  526. package/build/dist/UI/Components/LogsViewer/LogsViewer.js +117 -57
  527. package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
  528. package/build/dist/UI/Components/LogsViewer/components/ActiveFilterChips.js +5 -7
  529. package/build/dist/UI/Components/LogsViewer/components/ActiveFilterChips.js.map +1 -1
  530. package/build/dist/UI/Components/LogsViewer/components/FacetSection.js +31 -44
  531. package/build/dist/UI/Components/LogsViewer/components/FacetSection.js.map +1 -1
  532. package/build/dist/UI/Components/LogsViewer/components/FacetValueRow.js +17 -3
  533. package/build/dist/UI/Components/LogsViewer/components/FacetValueRow.js.map +1 -1
  534. package/build/dist/UI/Components/LogsViewer/components/LogDetailsPanel.js +19 -3
  535. package/build/dist/UI/Components/LogsViewer/components/LogDetailsPanel.js.map +1 -1
  536. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +64 -18
  537. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
  538. package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js +100 -41
  539. package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js.map +1 -1
  540. package/build/dist/UI/Components/LogsViewer/components/LogsTable.js +13 -5
  541. package/build/dist/UI/Components/LogsViewer/components/LogsTable.js.map +1 -1
  542. package/build/dist/UI/Components/LogsViewer/types.js.map +1 -1
  543. package/build/dist/UI/Components/Markdown.tsx/InlineReferences.js +403 -0
  544. package/build/dist/UI/Components/Markdown.tsx/InlineReferences.js.map +1 -0
  545. package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js +197 -179
  546. package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js.map +1 -1
  547. package/build/dist/UI/Components/MemberRoleAssignment/MemberRoleAssignment.js +30 -21
  548. package/build/dist/UI/Components/MemberRoleAssignment/MemberRoleAssignment.js.map +1 -1
  549. package/build/dist/UI/Components/ModelDetail/CardModelDetail.js +12 -0
  550. package/build/dist/UI/Components/ModelDetail/CardModelDetail.js.map +1 -1
  551. package/build/dist/UI/Components/ModelDetail/ModelDetail.js +46 -12
  552. package/build/dist/UI/Components/ModelDetail/ModelDetail.js.map +1 -1
  553. package/build/dist/UI/Components/ModelList/ModelList.js +26 -7
  554. package/build/dist/UI/Components/ModelList/ModelList.js.map +1 -1
  555. package/build/dist/UI/Components/Page/ModelPage.js +62 -34
  556. package/build/dist/UI/Components/Page/ModelPage.js.map +1 -1
  557. package/build/dist/UI/Components/Table/Table.js +3 -1
  558. package/build/dist/UI/Components/Table/Table.js.map +1 -1
  559. package/build/dist/UI/Components/Table/TableRow.js +8 -1
  560. package/build/dist/UI/Components/Table/TableRow.js.map +1 -1
  561. package/build/dist/UI/Components/TelemetryViewer/FacetVisibility.js +111 -0
  562. package/build/dist/UI/Components/TelemetryViewer/FacetVisibility.js.map +1 -0
  563. package/build/dist/UI/Components/TelemetryViewer/ResourceFacetConfigs.js +22 -0
  564. package/build/dist/UI/Components/TelemetryViewer/ResourceFacetConfigs.js.map +1 -0
  565. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js +1 -1
  566. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js.map +1 -1
  567. package/build/dist/UI/Components/TelemetryViewer/components/FacetSearchInput.js +36 -0
  568. package/build/dist/UI/Components/TelemetryViewer/components/FacetSearchInput.js.map +1 -0
  569. package/build/dist/UI/Components/TelemetryViewer/components/FacetSectionHeader.js +21 -0
  570. package/build/dist/UI/Components/TelemetryViewer/components/FacetSectionHeader.js.map +1 -0
  571. package/build/dist/UI/Components/TelemetryViewer/components/FacetShowMoreButton.js +14 -0
  572. package/build/dist/UI/Components/TelemetryViewer/components/FacetShowMoreButton.js.map +1 -0
  573. package/build/dist/UI/Components/TelemetryViewer/components/HiddenFacetsFooter.js +27 -0
  574. package/build/dist/UI/Components/TelemetryViewer/components/HiddenFacetsFooter.js.map +1 -0
  575. package/build/dist/UI/Components/TelemetryViewer/components/LockedFilterActions.js +88 -0
  576. package/build/dist/UI/Components/TelemetryViewer/components/LockedFilterActions.js.map +1 -0
  577. package/build/dist/UI/Components/TelemetryViewer/components/LockedFilterChip.js +228 -0
  578. package/build/dist/UI/Components/TelemetryViewer/components/LockedFilterChip.js.map +1 -0
  579. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryActiveFilterChips.js +6 -6
  580. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryActiveFilterChips.js.map +1 -1
  581. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryDetailPanel.js +111 -12
  582. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryDetailPanel.js.map +1 -1
  583. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSection.js +27 -40
  584. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSection.js.map +1 -1
  585. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.js +70 -10
  586. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.js.map +1 -1
  587. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.js +17 -3
  588. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.js.map +1 -1
  589. package/build/dist/UI/Components/TelemetryViewer/types.js +0 -4
  590. package/build/dist/UI/Components/TelemetryViewer/types.js.map +1 -1
  591. package/build/dist/UI/Components/TelemetryViewer/useFacetSearchExemptions.js +70 -0
  592. package/build/dist/UI/Components/TelemetryViewer/useFacetSearchExemptions.js.map +1 -0
  593. package/build/dist/UI/Components/TelemetryViewer/useFacetSectionSearch.js +52 -0
  594. package/build/dist/UI/Components/TelemetryViewer/useFacetSectionSearch.js.map +1 -0
  595. package/build/dist/UI/Utils/Clipboard.js +59 -2
  596. package/build/dist/UI/Utils/Clipboard.js.map +1 -1
  597. package/build/dist/UI/Utils/Navigation.js +11 -2
  598. package/build/dist/UI/Utils/Navigation.js.map +1 -1
  599. package/build/dist/UI/Utils/Telemetry/TelemetryEntityNames.js +471 -0
  600. package/build/dist/UI/Utils/Telemetry/TelemetryEntityNames.js.map +1 -0
  601. package/build/dist/UI/Utils/Telemetry/UseTelemetryEntityNames.js +113 -0
  602. package/build/dist/UI/Utils/Telemetry/UseTelemetryEntityNames.js.map +1 -0
  603. package/build/dist/Utils/AI/InvestigationReport.js +1241 -0
  604. package/build/dist/Utils/AI/InvestigationReport.js.map +1 -0
  605. package/build/dist/Utils/API.js +25 -0
  606. package/build/dist/Utils/API.js.map +1 -1
  607. package/build/dist/Utils/Rum/SessionReplayHealth.js +10 -0
  608. package/build/dist/Utils/Rum/SessionReplayHealth.js.map +1 -1
  609. package/build/dist/Utils/Rum/SessionReplayRecordingEnded.js +54 -0
  610. package/build/dist/Utils/Rum/SessionReplayRecordingEnded.js.map +1 -0
  611. package/build/dist/Utils/SecurityEvent/Connectors/AwsSecurityHubNormalizer.js +311 -0
  612. package/build/dist/Utils/SecurityEvent/Connectors/AwsSecurityHubNormalizer.js.map +1 -0
  613. package/build/dist/Utils/SecurityEvent/Connectors/CrowdStrikeFalconNormalizer.js +150 -0
  614. package/build/dist/Utils/SecurityEvent/Connectors/CrowdStrikeFalconNormalizer.js.map +1 -0
  615. package/build/dist/Utils/SecurityEvent/Connectors/ElasticSecurityNormalizer.js +328 -0
  616. package/build/dist/Utils/SecurityEvent/Connectors/ElasticSecurityNormalizer.js.map +1 -0
  617. package/build/dist/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdrNormalizer.js +405 -0
  618. package/build/dist/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdrNormalizer.js.map +1 -0
  619. package/build/dist/Utils/SecurityEvent/Connectors/MicrosoftSentinelNormalizer.js +212 -0
  620. package/build/dist/Utils/SecurityEvent/Connectors/MicrosoftSentinelNormalizer.js.map +1 -0
  621. package/build/dist/Utils/SecurityEvent/Connectors/OktaNormalizer.js +351 -0
  622. package/build/dist/Utils/SecurityEvent/Connectors/OktaNormalizer.js.map +1 -0
  623. package/build/dist/Utils/SecurityEvent/Connectors/SplunkNormalizer.js +348 -0
  624. package/build/dist/Utils/SecurityEvent/Connectors/SplunkNormalizer.js.map +1 -0
  625. package/build/dist/Utils/SecurityEvent/GoogleSecOpsAlertNormalizer.js +103 -10
  626. package/build/dist/Utils/SecurityEvent/GoogleSecOpsAlertNormalizer.js.map +1 -1
  627. package/build/dist/Utils/Telemetry/CrossSignalScope.js +16 -5
  628. package/build/dist/Utils/Telemetry/CrossSignalScope.js.map +1 -1
  629. package/build/dist/Utils/Telemetry/EntityRelationship.js +7 -0
  630. package/build/dist/Utils/Telemetry/EntityRelationship.js.map +1 -1
  631. package/build/dist/Utils/Telemetry/LockedFilterSearch.js +277 -0
  632. package/build/dist/Utils/Telemetry/LockedFilterSearch.js.map +1 -0
  633. package/build/dist/Utils/Telemetry/NetworkHost.js +39 -0
  634. package/build/dist/Utils/Telemetry/NetworkHost.js.map +1 -0
  635. package/build/dist/Utils/Telemetry/OriginAllowList.js +105 -4
  636. package/build/dist/Utils/Telemetry/OriginAllowList.js.map +1 -1
  637. package/build/dist/Utils/Traces/CriticalPath.js +9 -4
  638. package/build/dist/Utils/Traces/CriticalPath.js.map +1 -1
  639. package/package.json +1 -1
  640. package/Tests/UI/Rum/RecordingHealthStrip.test.tsx +0 -600
  641. package/UI/Components/GanttChart/Bar/BarLabel.tsx +0 -17
  642. package/UI/Components/GanttChart/Bar/Index.tsx +0 -149
  643. package/UI/Components/GanttChart/ChartContainer.tsx +0 -50
  644. package/UI/Components/GanttChart/Index.tsx +0 -90
  645. package/UI/Components/GanttChart/Row/Index.tsx +0 -22
  646. package/UI/Components/GanttChart/Row/Row.tsx +0 -231
  647. package/UI/Components/GanttChart/Row/RowLabel.tsx +0 -30
  648. package/UI/Components/GanttChart/Rows.tsx +0 -59
  649. package/UI/Components/GanttChart/Timeline/Index.tsx +0 -46
  650. package/UI/Components/GanttChart/Timeline/TimelineInterval.tsx +0 -33
  651. package/UI/Components/GanttChart/Timeline/TimelineIntervalMarks.tsx +0 -43
  652. package/build/dist/UI/Components/GanttChart/Bar/BarLabel.js +0 -8
  653. package/build/dist/UI/Components/GanttChart/Bar/BarLabel.js.map +0 -1
  654. package/build/dist/UI/Components/GanttChart/Bar/Index.js +0 -77
  655. package/build/dist/UI/Components/GanttChart/Bar/Index.js.map +0 -1
  656. package/build/dist/UI/Components/GanttChart/ChartContainer.js +0 -33
  657. package/build/dist/UI/Components/GanttChart/ChartContainer.js.map +0 -1
  658. package/build/dist/UI/Components/GanttChart/Index.js +0 -39
  659. package/build/dist/UI/Components/GanttChart/Index.js.map +0 -1
  660. package/build/dist/UI/Components/GanttChart/Row/Index.js +0 -7
  661. package/build/dist/UI/Components/GanttChart/Row/Index.js.map +0 -1
  662. package/build/dist/UI/Components/GanttChart/Row/Row.js +0 -106
  663. package/build/dist/UI/Components/GanttChart/Row/Row.js.map +0 -1
  664. package/build/dist/UI/Components/GanttChart/Row/RowLabel.js +0 -10
  665. package/build/dist/UI/Components/GanttChart/Row/RowLabel.js.map +0 -1
  666. package/build/dist/UI/Components/GanttChart/Rows.js +0 -19
  667. package/build/dist/UI/Components/GanttChart/Rows.js.map +0 -1
  668. package/build/dist/UI/Components/GanttChart/Timeline/Index.js +0 -16
  669. package/build/dist/UI/Components/GanttChart/Timeline/Index.js.map +0 -1
  670. package/build/dist/UI/Components/GanttChart/Timeline/TimelineInterval.js +0 -13
  671. package/build/dist/UI/Components/GanttChart/Timeline/TimelineInterval.js.map +0 -1
  672. package/build/dist/UI/Components/GanttChart/Timeline/TimelineIntervalMarks.js +0 -13
  673. package/build/dist/UI/Components/GanttChart/Timeline/TimelineIntervalMarks.js.map +0 -1
@@ -0,0 +1,2205 @@
1
+ import {
2
+ afterEach,
3
+ beforeEach,
4
+ describe,
5
+ expect,
6
+ jest,
7
+ test,
8
+ } from "@jest/globals";
9
+ import getJestMockFunction, { MockFunction } from "../../../MockType";
10
+
11
+ /*
12
+ * A telemetry row's primaryEntityId is polymorphic: it can be a Service,
13
+ * a RUM application, a host, a cluster, an incident, … Every viewer used
14
+ * to resolve it against the Service table only, so a RUM application's
15
+ * metrics page showed "Service: 84858d6c-…" as its locked filter. The
16
+ * resolver under test is the single place that knows every table an id
17
+ * can live in and how to label it, so these tests pin the registry, the
18
+ * query plan (Service first, then fan out), hints, restrictions, failure
19
+ * isolation, caching (hits, short-lived misses, expired hits refreshed
20
+ * from their own table), non-UUID input and in-flight dedup.
21
+ */
22
+
23
+ const getListMock: MockFunction = getJestMockFunction();
24
+
25
+ /*
26
+ * The arrow wrapper is load bearing: jest.mock is hoisted above the
27
+ * compiled requires, so getListMock is still unassigned when the factory
28
+ * runs.
29
+ */
30
+ jest.mock("../../../../UI/Utils/ModelAPI/ModelAPI", () => {
31
+ return {
32
+ __esModule: true,
33
+ default: {
34
+ getList: (...args: Array<any>) => {
35
+ return getListMock(...args);
36
+ },
37
+ },
38
+ };
39
+ });
40
+
41
+ import TelemetryEntityNameResolver, {
42
+ DEFAULT_TELEMETRY_ENTITY_LABEL,
43
+ getTelemetryEntityDisplay,
44
+ getTelemetryEntityTypeLabel,
45
+ MISS_CACHE_TTL_IN_MS,
46
+ ResolvedTelemetryEntity,
47
+ TELEMETRY_ENTITY_RESOLUTION_ORDER,
48
+ TELEMETRY_ENTITY_TYPES,
49
+ TelemetryEntityNameMap,
50
+ } from "../../../../UI/Utils/Telemetry/TelemetryEntityNames";
51
+ import { UNKNOWN_SERVICE_NAME } from "../../../../UI/Utils/TelemetryService";
52
+ import Alert from "../../../../Models/DatabaseModels/Alert";
53
+ import BaseModel from "../../../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
54
+ import Host from "../../../../Models/DatabaseModels/Host";
55
+ import Incident from "../../../../Models/DatabaseModels/Incident";
56
+ import IoTFleet from "../../../../Models/DatabaseModels/IoTFleet";
57
+ import RumApplication from "../../../../Models/DatabaseModels/RumApplication";
58
+ import ScheduledMaintenance from "../../../../Models/DatabaseModels/ScheduledMaintenance";
59
+ import Service from "../../../../Models/DatabaseModels/Service";
60
+ import Includes from "../../../../Types/BaseDatabase/Includes";
61
+ import ObjectID from "../../../../Types/ObjectID";
62
+ import ServiceType from "../../../../Types/Telemetry/ServiceType";
63
+
64
+ const PROJECT_ID: string = "9e1b6b0e-0000-4000-8000-000000000001";
65
+ const SERVICE_ID: string = "11111111-0000-4000-8000-000000000001";
66
+ const SERVICE_ID_2: string = "11111111-0000-4000-8000-000000000002";
67
+ const RUM_ID: string = "22222222-0000-4000-8000-000000000001";
68
+ const HOST_ID: string = "33333333-0000-4000-8000-000000000001";
69
+ const CLUSTER_ID: string = "44444444-0000-4000-8000-000000000001";
70
+ const INCIDENT_ID: string = "55555555-0000-4000-8000-000000000001";
71
+ const MISSING_ID: string = "99999999-0000-4000-8000-000000000001";
72
+
73
+ type ModelType = { new (): BaseModel };
74
+
75
+ interface FakeRow {
76
+ id: ObjectID;
77
+ [field: string]: unknown;
78
+ }
79
+
80
+ interface GetListArgs {
81
+ modelType: ModelType;
82
+ query: { projectId: ObjectID; _id: Includes };
83
+ limit: number;
84
+ skip: number;
85
+ select: Record<string, boolean>;
86
+ sort: Record<string, unknown>;
87
+ }
88
+
89
+ interface IssuedRequest {
90
+ entityType: ServiceType;
91
+ ids: Array<string>;
92
+ }
93
+
94
+ // Rows each fake table holds, keyed by the entity type that owns the table.
95
+ let tables: Partial<Record<ServiceType, Array<FakeRow>>> = {};
96
+ // Tables whose getList rejects (e.g. the role cannot read that model).
97
+ let failingTypes: Set<ServiceType> = new Set();
98
+
99
+ const row: (id: string, fields: Record<string, unknown>) => FakeRow = (
100
+ id: string,
101
+ fields: Record<string, unknown>,
102
+ ): FakeRow => {
103
+ return { id: new ObjectID(id), ...fields };
104
+ };
105
+
106
+ const entityTypeForModel: (modelType: ModelType) => ServiceType = (
107
+ modelType: ModelType,
108
+ ): ServiceType => {
109
+ const match: [string, { modelType?: ModelType | undefined }] | undefined =
110
+ Object.entries(TELEMETRY_ENTITY_TYPES).find(
111
+ (entry: [string, { modelType?: ModelType | undefined }]): boolean => {
112
+ return entry[1].modelType === modelType;
113
+ },
114
+ );
115
+ if (!match) {
116
+ throw new Error(`Unexpected model queried: ${modelType.name}`);
117
+ }
118
+ return match[0] as ServiceType;
119
+ };
120
+
121
+ const includedIds: (includes: Includes) => Array<string> = (
122
+ includes: Includes,
123
+ ): Array<string> => {
124
+ return (includes.values as Array<string | ObjectID>).map(
125
+ (value: string | ObjectID): string => {
126
+ return value.toString();
127
+ },
128
+ );
129
+ };
130
+
131
+ const argsOf: (callIndex: number) => GetListArgs = (
132
+ callIndex: number,
133
+ ): GetListArgs => {
134
+ return getListMock.mock.calls[callIndex]![0] as GetListArgs;
135
+ };
136
+
137
+ const issuedRequests: () => Array<IssuedRequest> = (): Array<IssuedRequest> => {
138
+ return getListMock.mock.calls.map((call: Array<unknown>): IssuedRequest => {
139
+ const args: GetListArgs = call[0] as GetListArgs;
140
+ return {
141
+ entityType: entityTypeForModel(args.modelType),
142
+ ids: includedIds(args.query._id),
143
+ };
144
+ });
145
+ };
146
+
147
+ const requestedTypes: () => Array<ServiceType> = (): Array<ServiceType> => {
148
+ return issuedRequests().map((request: IssuedRequest): ServiceType => {
149
+ return request.entityType;
150
+ });
151
+ };
152
+
153
+ const fakeGetList: (
154
+ args: GetListArgs,
155
+ ) => Promise<{ data: Array<FakeRow> }> = async (
156
+ args: GetListArgs,
157
+ ): Promise<{ data: Array<FakeRow> }> => {
158
+ const entityType: ServiceType = entityTypeForModel(args.modelType);
159
+ if (failingTypes.has(entityType)) {
160
+ throw new Error(`403: cannot read ${entityType}`);
161
+ }
162
+ const ids: Array<string> = includedIds(args.query._id);
163
+ return {
164
+ data: (tables[entityType] || []).filter((item: FakeRow): boolean => {
165
+ return ids.includes(item.id.toString());
166
+ }),
167
+ };
168
+ };
169
+
170
+ interface Deferred<T> {
171
+ promise: Promise<T>;
172
+ resolve: (value: T) => void;
173
+ }
174
+
175
+ const deferred: <T>() => Deferred<T> = <T>(): Deferred<T> => {
176
+ let resolve: (value: T) => void = (): void => {};
177
+ const promise: Promise<T> = new Promise<T>((res: (value: T) => void) => {
178
+ resolve = res;
179
+ });
180
+ return { promise, resolve };
181
+ };
182
+
183
+ const flushPromises: () => Promise<void> = async (): Promise<void> => {
184
+ for (let i: number = 0; i < 10; i++) {
185
+ await Promise.resolve();
186
+ }
187
+ };
188
+
189
+ // Hit TTL of the resolver's positive cache (not exported by the module).
190
+ const HIT_CACHE_TTL_IN_MS: number = 60 * 1000;
191
+ const START_TIME: number = 1_800_000_000_000;
192
+
193
+ /*
194
+ * Named structurally: jest.spyOn from @jest/globals returns jest-mock's
195
+ * SpyInstance, which does not assign to the global jest.SpiedFunction.
196
+ */
197
+ interface NowSpy {
198
+ mockReturnValue: (value: number) => unknown;
199
+ }
200
+
201
+ // Freeze Date.now at `at`; move the clock with the returned spy.
202
+ const freezeNow: (at: number) => NowSpy = (at: number): NowSpy => {
203
+ return jest.spyOn(Date, "now").mockReturnValue(at) as unknown as NowSpy;
204
+ };
205
+
206
+ const missCacheKeys: () => Array<string> = (): Array<string> => {
207
+ return Object.keys(
208
+ (
209
+ TelemetryEntityNameResolver as unknown as {
210
+ missCache: Record<string, number>;
211
+ }
212
+ ).missCache,
213
+ );
214
+ };
215
+
216
+ const requestsFor: (entityType: ServiceType) => Array<IssuedRequest> = (
217
+ entityType: ServiceType,
218
+ ): Array<IssuedRequest> => {
219
+ return issuedRequests().filter((request: IssuedRequest): boolean => {
220
+ return request.entityType === entityType;
221
+ });
222
+ };
223
+
224
+ beforeEach(() => {
225
+ TelemetryEntityNameResolver.clearCache();
226
+ getListMock.mockReset();
227
+ tables = {
228
+ [ServiceType.OpenTelemetry]: [
229
+ row(SERVICE_ID, { name: "checkout-api" }),
230
+ row(SERVICE_ID_2, { name: "payments-api" }),
231
+ ],
232
+ [ServiceType.RealUserMonitor]: [row(RUM_ID, { name: "checkout-web" })],
233
+ [ServiceType.Host]: [row(HOST_ID, { name: "prod-db-1" })],
234
+ [ServiceType.KubernetesCluster]: [
235
+ row(CLUSTER_ID, { name: "prod-eu-west" }),
236
+ ],
237
+ [ServiceType.Incident]: [row(INCIDENT_ID, { title: "API is down" })],
238
+ };
239
+ failingTypes = new Set();
240
+ getListMock.mockImplementation((...args: Array<unknown>) => {
241
+ return fakeGetList(args[0] as GetListArgs);
242
+ });
243
+ });
244
+
245
+ afterEach(() => {
246
+ jest.restoreAllMocks();
247
+ });
248
+
249
+ describe("TELEMETRY_ENTITY_TYPES registry", () => {
250
+ const allServiceTypes: Array<ServiceType> = Object.values(
251
+ ServiceType,
252
+ ) as Array<ServiceType>;
253
+
254
+ test("has an entry for every ServiceType", () => {
255
+ expect(Object.keys(TELEMETRY_ENTITY_TYPES).sort()).toEqual(
256
+ [...allServiceTypes].sort(),
257
+ );
258
+ });
259
+
260
+ test.each(allServiceTypes)(
261
+ "%s has a non-empty label",
262
+ (type: ServiceType) => {
263
+ expect(TELEMETRY_ENTITY_TYPES[type].label.trim().length).toBeGreaterThan(
264
+ 0,
265
+ );
266
+ },
267
+ );
268
+
269
+ test.each(
270
+ allServiceTypes.filter((type: ServiceType): boolean => {
271
+ return type !== ServiceType.Unknown;
272
+ }),
273
+ )(
274
+ "%s has a modelType and name fields that are real columns on it",
275
+ (type: ServiceType) => {
276
+ const config: (typeof TELEMETRY_ENTITY_TYPES)[ServiceType] =
277
+ TELEMETRY_ENTITY_TYPES[type];
278
+ expect(config.modelType).toBeDefined();
279
+ expect(config.nameFields.length).toBeGreaterThan(0);
280
+ const model: BaseModel = new config.modelType!();
281
+ for (const field of config.nameFields) {
282
+ expect({ type, field, isColumn: model.isTableColumn(field) }).toEqual({
283
+ type,
284
+ field,
285
+ isColumn: true,
286
+ });
287
+ }
288
+ },
289
+ );
290
+
291
+ test("Unknown has no table and no name fields (its id is the projectId)", () => {
292
+ expect(TELEMETRY_ENTITY_TYPES[ServiceType.Unknown].modelType).toBe(
293
+ undefined,
294
+ );
295
+ expect(TELEMETRY_ENTITY_TYPES[ServiceType.Unknown].nameFields).toEqual([]);
296
+ expect(TELEMETRY_ENTITY_TYPES[ServiceType.Unknown].label).toBe("Service");
297
+ });
298
+
299
+ test("maps the core entity types to the expected models and labels", () => {
300
+ expect(TELEMETRY_ENTITY_TYPES[ServiceType.OpenTelemetry]).toMatchObject({
301
+ label: "Service",
302
+ modelType: Service,
303
+ nameFields: ["name"],
304
+ });
305
+ expect(TELEMETRY_ENTITY_TYPES[ServiceType.RealUserMonitor]).toMatchObject({
306
+ label: "RUM Application",
307
+ modelType: RumApplication,
308
+ nameFields: ["name", "appIdentifier"],
309
+ });
310
+ expect(TELEMETRY_ENTITY_TYPES[ServiceType.Host]).toMatchObject({
311
+ label: "Host",
312
+ modelType: Host,
313
+ nameFields: ["name", "hostIdentifier"],
314
+ });
315
+ });
316
+
317
+ test("IoTDevice telemetry resolves against the IoT fleet table", () => {
318
+ expect(TELEMETRY_ENTITY_TYPES[ServiceType.IoTDevice].modelType).toBe(
319
+ IoTFleet,
320
+ );
321
+ expect(TELEMETRY_ENTITY_TYPES[ServiceType.IoTDevice].label).toBe(
322
+ "IoT Fleet",
323
+ );
324
+ });
325
+
326
+ test("Incident, Alert and ScheduledMaintenance are named by title", () => {
327
+ expect(TELEMETRY_ENTITY_TYPES[ServiceType.Incident]).toMatchObject({
328
+ modelType: Incident,
329
+ nameFields: ["title"],
330
+ });
331
+ expect(TELEMETRY_ENTITY_TYPES[ServiceType.Alert]).toMatchObject({
332
+ modelType: Alert,
333
+ nameFields: ["title"],
334
+ });
335
+ expect(
336
+ TELEMETRY_ENTITY_TYPES[ServiceType.ScheduledMaintenance],
337
+ ).toMatchObject({
338
+ modelType: ScheduledMaintenance,
339
+ nameFields: ["title"],
340
+ });
341
+ });
342
+
343
+ test("every model type is used by exactly one entity type", () => {
344
+ const models: Array<ModelType> = allServiceTypes
345
+ .map((type: ServiceType): ModelType | undefined => {
346
+ return TELEMETRY_ENTITY_TYPES[type].modelType;
347
+ })
348
+ .filter((model: ModelType | undefined): boolean => {
349
+ return Boolean(model);
350
+ }) as Array<ModelType>;
351
+ expect(new Set(models).size).toBe(models.length);
352
+ });
353
+ });
354
+
355
+ describe("TELEMETRY_ENTITY_RESOLUTION_ORDER", () => {
356
+ test("starts with Service (the common case resolves in one request)", () => {
357
+ expect(TELEMETRY_ENTITY_RESOLUTION_ORDER[0]).toBe(
358
+ ServiceType.OpenTelemetry,
359
+ );
360
+ });
361
+
362
+ test("covers every table-backed type exactly once and never Unknown", () => {
363
+ const expected: Array<ServiceType> = (
364
+ Object.values(ServiceType) as Array<ServiceType>
365
+ ).filter((type: ServiceType): boolean => {
366
+ return type !== ServiceType.Unknown;
367
+ });
368
+ expect([...TELEMETRY_ENTITY_RESOLUTION_ORDER].sort()).toEqual(
369
+ expected.sort(),
370
+ );
371
+ expect(new Set(TELEMETRY_ENTITY_RESOLUTION_ORDER).size).toBe(
372
+ TELEMETRY_ENTITY_RESOLUTION_ORDER.length,
373
+ );
374
+ expect(TELEMETRY_ENTITY_RESOLUTION_ORDER).not.toContain(
375
+ ServiceType.Unknown,
376
+ );
377
+ });
378
+ });
379
+
380
+ describe("getTelemetryEntityTypeLabel", () => {
381
+ test.each([
382
+ [ServiceType.OpenTelemetry, "Service"],
383
+ [ServiceType.RealUserMonitor, "RUM Application"],
384
+ [ServiceType.Host, "Host"],
385
+ [ServiceType.DockerHost, "Docker Host"],
386
+ [ServiceType.PodmanHost, "Podman Host"],
387
+ [ServiceType.KubernetesCluster, "Kubernetes Cluster"],
388
+ [ServiceType.ProxmoxCluster, "Proxmox Cluster"],
389
+ [ServiceType.CephCluster, "Ceph Cluster"],
390
+ [ServiceType.DockerSwarmCluster, "Docker Swarm Cluster"],
391
+ [ServiceType.VMwareVCenter, "vCenter"],
392
+ [ServiceType.IoTDevice, "IoT Fleet"],
393
+ [ServiceType.ServerlessFunction, "Serverless Function"],
394
+ [ServiceType.CloudResource, "Cloud Resource"],
395
+ [ServiceType.NetworkDevice, "Network Device"],
396
+ [ServiceType.Monitor, "Monitor"],
397
+ [ServiceType.Incident, "Incident"],
398
+ [ServiceType.Alert, "Alert"],
399
+ [ServiceType.ScheduledMaintenance, "Scheduled Maintenance"],
400
+ [ServiceType.Unknown, "Service"],
401
+ ])("%s -> %s", (type: ServiceType, label: string) => {
402
+ expect(getTelemetryEntityTypeLabel(type)).toBe(label);
403
+ });
404
+
405
+ test("accepts the enum's string value", () => {
406
+ expect(getTelemetryEntityTypeLabel("RealUserMonitor")).toBe(
407
+ "RUM Application",
408
+ );
409
+ });
410
+
411
+ test.each([
412
+ ["null", null],
413
+ ["undefined", undefined],
414
+ ["empty string", ""],
415
+ ["unknown string", "NotAType"],
416
+ ["prototype key", "toString"],
417
+ ["prototype key __proto__", "__proto__"],
418
+ ["wrong case", "realusermonitor"],
419
+ ])(
420
+ "falls back to the default label for %s",
421
+ (_name: string, value: string | null | undefined) => {
422
+ expect(getTelemetryEntityTypeLabel(value)).toBe(
423
+ DEFAULT_TELEMETRY_ENTITY_LABEL,
424
+ );
425
+ },
426
+ );
427
+
428
+ test("the default label is Service so Service pages keep their chip key", () => {
429
+ expect(DEFAULT_TELEMETRY_ENTITY_LABEL).toBe("Service");
430
+ });
431
+ });
432
+
433
+ describe("getTelemetryEntityDisplay", () => {
434
+ const nameMap: TelemetryEntityNameMap = {
435
+ [RUM_ID]: {
436
+ id: RUM_ID,
437
+ name: "checkout-web",
438
+ entityType: ServiceType.RealUserMonitor,
439
+ typeLabel: "RUM Application",
440
+ },
441
+ [SERVICE_ID]: {
442
+ id: SERVICE_ID,
443
+ name: "checkout-api",
444
+ entityType: ServiceType.OpenTelemetry,
445
+ typeLabel: "Service",
446
+ },
447
+ };
448
+
449
+ test("resolved entity: key is the type label, value is the name", () => {
450
+ expect(getTelemetryEntityDisplay({ id: RUM_ID, nameMap })).toEqual({
451
+ key: "RUM Application",
452
+ value: "checkout-web",
453
+ });
454
+ });
455
+
456
+ test("resolved entity wins over caller fallbacks", () => {
457
+ expect(
458
+ getTelemetryEntityDisplay({
459
+ id: RUM_ID,
460
+ nameMap,
461
+ fallbackKey: "Service",
462
+ fallbackValue: "stale-name",
463
+ }),
464
+ ).toEqual({ key: "RUM Application", value: "checkout-web" });
465
+ });
466
+
467
+ test("resolved Service keeps the Service key", () => {
468
+ expect(
469
+ getTelemetryEntityDisplay({
470
+ id: SERVICE_ID,
471
+ nameMap,
472
+ fallbackKey: "Service",
473
+ }),
474
+ ).toEqual({ key: "Service", value: "checkout-api" });
475
+ });
476
+
477
+ test("resolved entity with a blank type label uses the fallback key", () => {
478
+ expect(
479
+ getTelemetryEntityDisplay({
480
+ id: HOST_ID,
481
+ nameMap: {
482
+ [HOST_ID]: {
483
+ id: HOST_ID,
484
+ name: "prod-db-1",
485
+ entityType: ServiceType.Host,
486
+ typeLabel: "",
487
+ },
488
+ },
489
+ fallbackKey: "Resource",
490
+ }),
491
+ ).toEqual({ key: "Resource", value: "prod-db-1" });
492
+ });
493
+
494
+ test("unresolved id: fallback key and fallback value", () => {
495
+ expect(
496
+ getTelemetryEntityDisplay({
497
+ id: MISSING_ID,
498
+ nameMap,
499
+ fallbackKey: "Service",
500
+ fallbackValue: "legacy-name",
501
+ }),
502
+ ).toEqual({ key: "Service", value: "legacy-name" });
503
+ });
504
+
505
+ test("unresolved id without fallbacks: default key and the raw id", () => {
506
+ expect(getTelemetryEntityDisplay({ id: MISSING_ID, nameMap })).toEqual({
507
+ key: DEFAULT_TELEMETRY_ENTITY_LABEL,
508
+ value: MISSING_ID,
509
+ });
510
+ });
511
+
512
+ test("undefined name map behaves like unresolved", () => {
513
+ expect(
514
+ getTelemetryEntityDisplay({ id: RUM_ID, nameMap: undefined }),
515
+ ).toEqual({ key: "Service", value: RUM_ID });
516
+ });
517
+
518
+ test("empty-string fallbacks are treated as absent", () => {
519
+ expect(
520
+ getTelemetryEntityDisplay({
521
+ id: MISSING_ID,
522
+ nameMap: {},
523
+ fallbackKey: "",
524
+ fallbackValue: "",
525
+ }),
526
+ ).toEqual({ key: DEFAULT_TELEMETRY_ENTITY_LABEL, value: MISSING_ID });
527
+ });
528
+ });
529
+
530
+ describe("TelemetryEntityNameResolver.resolve — inputs", () => {
531
+ test("empty id list returns {} without any request", async () => {
532
+ await expect(
533
+ TelemetryEntityNameResolver.resolve({ ids: [], projectId: PROJECT_ID }),
534
+ ).resolves.toEqual({});
535
+ expect(getListMock).not.toHaveBeenCalled();
536
+ });
537
+
538
+ test("blank, null and undefined ids are ignored", async () => {
539
+ await expect(
540
+ TelemetryEntityNameResolver.resolve({
541
+ ids: ["", " ", null, undefined],
542
+ projectId: PROJECT_ID,
543
+ }),
544
+ ).resolves.toEqual({});
545
+ expect(getListMock).not.toHaveBeenCalled();
546
+ });
547
+
548
+ test.each([
549
+ ["null", null],
550
+ ["undefined", undefined],
551
+ ["empty string", ""],
552
+ ])(
553
+ "missing projectId (%s) returns {} and issues no request",
554
+ async (_name: string, projectId: string | null | undefined) => {
555
+ await expect(
556
+ TelemetryEntityNameResolver.resolve({
557
+ ids: [SERVICE_ID, RUM_ID],
558
+ projectId,
559
+ }),
560
+ ).resolves.toEqual({});
561
+ expect(getListMock).not.toHaveBeenCalled();
562
+ },
563
+ );
564
+
565
+ test("accepts ObjectID instances for ids and projectId", async () => {
566
+ const result: TelemetryEntityNameMap =
567
+ await TelemetryEntityNameResolver.resolve({
568
+ ids: [new ObjectID(SERVICE_ID)],
569
+ projectId: new ObjectID(PROJECT_ID),
570
+ });
571
+ expect(result[SERVICE_ID]?.name).toBe("checkout-api");
572
+ });
573
+
574
+ test("duplicate ids (strings, ObjectIDs, padded) are queried once", async () => {
575
+ await TelemetryEntityNameResolver.resolve({
576
+ ids: [SERVICE_ID, new ObjectID(SERVICE_ID), ` ${SERVICE_ID} `],
577
+ projectId: PROJECT_ID,
578
+ });
579
+ expect(issuedRequests()).toEqual([
580
+ { entityType: ServiceType.OpenTelemetry, ids: [SERVICE_ID] },
581
+ ]);
582
+ expect(argsOf(0).limit).toBe(1);
583
+ });
584
+
585
+ test("the projectId id resolves to Unknown Service without a request", async () => {
586
+ const result: TelemetryEntityNameMap =
587
+ await TelemetryEntityNameResolver.resolve({
588
+ ids: [PROJECT_ID],
589
+ projectId: PROJECT_ID,
590
+ });
591
+ expect(result).toEqual({
592
+ [PROJECT_ID]: {
593
+ id: PROJECT_ID,
594
+ name: UNKNOWN_SERVICE_NAME,
595
+ entityType: ServiceType.Unknown,
596
+ typeLabel: "Service",
597
+ },
598
+ });
599
+ expect(UNKNOWN_SERVICE_NAME).toBe("Unknown Service");
600
+ expect(getListMock).not.toHaveBeenCalled();
601
+ });
602
+
603
+ test("the projectId id is never sent to a table alongside real ids", async () => {
604
+ const result: TelemetryEntityNameMap =
605
+ await TelemetryEntityNameResolver.resolve({
606
+ ids: [PROJECT_ID, SERVICE_ID],
607
+ projectId: new ObjectID(PROJECT_ID),
608
+ });
609
+ expect(result[PROJECT_ID]?.name).toBe(UNKNOWN_SERVICE_NAME);
610
+ expect(result[SERVICE_ID]?.name).toBe("checkout-api");
611
+ expect(issuedRequests()).toEqual([
612
+ { entityType: ServiceType.OpenTelemetry, ids: [SERVICE_ID] },
613
+ ]);
614
+ });
615
+ });
616
+
617
+ describe("TelemetryEntityNameResolver.resolve — query plan", () => {
618
+ test("Service ids resolve with a single Service request", async () => {
619
+ const result: TelemetryEntityNameMap =
620
+ await TelemetryEntityNameResolver.resolve({
621
+ ids: [SERVICE_ID_2, SERVICE_ID],
622
+ projectId: PROJECT_ID,
623
+ });
624
+ expect(result).toEqual({
625
+ [SERVICE_ID]: {
626
+ id: SERVICE_ID,
627
+ name: "checkout-api",
628
+ entityType: ServiceType.OpenTelemetry,
629
+ typeLabel: "Service",
630
+ },
631
+ [SERVICE_ID_2]: {
632
+ id: SERVICE_ID_2,
633
+ name: "payments-api",
634
+ entityType: ServiceType.OpenTelemetry,
635
+ typeLabel: "Service",
636
+ },
637
+ });
638
+ expect(getListMock).toHaveBeenCalledTimes(1);
639
+ expect(requestedTypes()).toEqual([ServiceType.OpenTelemetry]);
640
+ });
641
+
642
+ test("query shape: projectId + Includes of sorted ids, limit, select with name fields", async () => {
643
+ await TelemetryEntityNameResolver.resolve({
644
+ ids: [SERVICE_ID_2, SERVICE_ID],
645
+ projectId: PROJECT_ID,
646
+ });
647
+ const args: GetListArgs = argsOf(0);
648
+ expect(args.modelType).toBe(Service);
649
+ expect(args.query.projectId).toBeInstanceOf(ObjectID);
650
+ expect(args.query.projectId.toString()).toBe(PROJECT_ID);
651
+ expect(args.query._id).toBeInstanceOf(Includes);
652
+ expect(includedIds(args.query._id)).toEqual([SERVICE_ID, SERVICE_ID_2]);
653
+ expect(args.limit).toBe(2);
654
+ expect(args.skip).toBe(0);
655
+ expect(args.select).toEqual({ _id: true, name: true });
656
+ expect(args.sort).toEqual({});
657
+ });
658
+
659
+ test("select includes every name fallback field of the queried table", async () => {
660
+ await TelemetryEntityNameResolver.resolve({
661
+ ids: [RUM_ID],
662
+ projectId: PROJECT_ID,
663
+ typeHints: { [RUM_ID]: ServiceType.RealUserMonitor },
664
+ });
665
+ expect(argsOf(0).select).toEqual({
666
+ _id: true,
667
+ name: true,
668
+ appIdentifier: true,
669
+ });
670
+
671
+ getListMock.mockClear();
672
+ await TelemetryEntityNameResolver.resolve({
673
+ ids: [INCIDENT_ID],
674
+ projectId: PROJECT_ID,
675
+ typeHints: { [INCIDENT_ID]: ServiceType.Incident },
676
+ });
677
+ expect(argsOf(0).select).toEqual({ _id: true, title: true });
678
+ });
679
+
680
+ test("unresolved ids fan out after Service to every other table, in resolution order", async () => {
681
+ const result: TelemetryEntityNameMap =
682
+ await TelemetryEntityNameResolver.resolve({
683
+ ids: [SERVICE_ID, RUM_ID, HOST_ID],
684
+ projectId: PROJECT_ID,
685
+ });
686
+
687
+ expect(result[SERVICE_ID]).toMatchObject({
688
+ name: "checkout-api",
689
+ entityType: ServiceType.OpenTelemetry,
690
+ typeLabel: "Service",
691
+ });
692
+ expect(result[RUM_ID]).toEqual({
693
+ id: RUM_ID,
694
+ name: "checkout-web",
695
+ entityType: ServiceType.RealUserMonitor,
696
+ typeLabel: "RUM Application",
697
+ });
698
+ expect(result[HOST_ID]).toEqual({
699
+ id: HOST_ID,
700
+ name: "prod-db-1",
701
+ entityType: ServiceType.Host,
702
+ typeLabel: "Host",
703
+ });
704
+
705
+ const requests: Array<IssuedRequest> = issuedRequests();
706
+ // Service first with every id…
707
+ expect(requests[0]).toEqual({
708
+ entityType: ServiceType.OpenTelemetry,
709
+ ids: [SERVICE_ID, RUM_ID, HOST_ID].sort(),
710
+ });
711
+ // …then every other table, with only what Service did not resolve.
712
+ expect(
713
+ requests.slice(1).map((request: IssuedRequest): ServiceType => {
714
+ return request.entityType;
715
+ }),
716
+ ).toEqual(TELEMETRY_ENTITY_RESOLUTION_ORDER.slice(1));
717
+ for (const request of requests.slice(1)) {
718
+ expect(request.ids).toEqual([RUM_ID, HOST_ID].sort());
719
+ }
720
+ expect(argsOf(1).limit).toBe(2);
721
+ });
722
+
723
+ test("the fan-out waits for Service, then runs the other tables in parallel", async () => {
724
+ const serviceResponse: Deferred<{ data: Array<FakeRow> }> = deferred();
725
+ const rumResponse: Deferred<{ data: Array<FakeRow> }> = deferred();
726
+
727
+ getListMock.mockImplementation((...callArgs: Array<unknown>) => {
728
+ const args: GetListArgs = callArgs[0] as GetListArgs;
729
+ const type: ServiceType = entityTypeForModel(args.modelType);
730
+ if (type === ServiceType.OpenTelemetry) {
731
+ return serviceResponse.promise;
732
+ }
733
+ if (type === ServiceType.RealUserMonitor) {
734
+ return rumResponse.promise;
735
+ }
736
+ return fakeGetList(args);
737
+ });
738
+
739
+ const pending: Promise<TelemetryEntityNameMap> =
740
+ TelemetryEntityNameResolver.resolve({
741
+ ids: [RUM_ID],
742
+ projectId: PROJECT_ID,
743
+ });
744
+
745
+ await flushPromises();
746
+ expect(requestedTypes()).toEqual([ServiceType.OpenTelemetry]);
747
+
748
+ serviceResponse.resolve({ data: [] });
749
+ await flushPromises();
750
+
751
+ // Every other table has been asked although RUM has not answered yet.
752
+ expect(requestedTypes()).toEqual(TELEMETRY_ENTITY_RESOLUTION_ORDER);
753
+
754
+ rumResponse.resolve({ data: [row(RUM_ID, { name: "checkout-web" })] });
755
+ const result: TelemetryEntityNameMap = await pending;
756
+ expect(result[RUM_ID]?.name).toBe("checkout-web");
757
+ });
758
+
759
+ test("an id that exists nowhere is simply absent from the result", async () => {
760
+ const result: TelemetryEntityNameMap =
761
+ await TelemetryEntityNameResolver.resolve({
762
+ ids: [MISSING_ID, SERVICE_ID],
763
+ projectId: PROJECT_ID,
764
+ });
765
+ expect(Object.keys(result)).toEqual([SERVICE_ID]);
766
+ expect(getListMock).toHaveBeenCalledTimes(
767
+ TELEMETRY_ENTITY_RESOLUTION_ORDER.length,
768
+ );
769
+ });
770
+
771
+ test("results only contain requested ids even if a table returns extra rows", async () => {
772
+ getListMock.mockImplementation((...callArgs: Array<unknown>) => {
773
+ const args: GetListArgs = callArgs[0] as GetListArgs;
774
+ const type: ServiceType = entityTypeForModel(args.modelType);
775
+ // A misbehaving backend that ignores the _id filter.
776
+ return Promise.resolve({ data: tables[type] || [] });
777
+ });
778
+
779
+ const result: TelemetryEntityNameMap =
780
+ await TelemetryEntityNameResolver.resolve({
781
+ ids: [SERVICE_ID],
782
+ projectId: PROJECT_ID,
783
+ });
784
+ expect(Object.keys(result)).toEqual([SERVICE_ID]);
785
+ });
786
+
787
+ test("a row without an id is ignored", async () => {
788
+ getListMock.mockResolvedValue({
789
+ data: [{ id: null, name: "ghost" }, row(SERVICE_ID, { name: "real" })],
790
+ });
791
+ const result: TelemetryEntityNameMap =
792
+ await TelemetryEntityNameResolver.resolve({
793
+ ids: [SERVICE_ID],
794
+ projectId: PROJECT_ID,
795
+ });
796
+ expect(result).toEqual({
797
+ [SERVICE_ID]: {
798
+ id: SERVICE_ID,
799
+ name: "real",
800
+ entityType: ServiceType.OpenTelemetry,
801
+ typeLabel: "Service",
802
+ },
803
+ });
804
+ });
805
+
806
+ test("a response without data is treated as empty", async () => {
807
+ getListMock.mockResolvedValue({});
808
+ await expect(
809
+ TelemetryEntityNameResolver.resolve({
810
+ ids: [SERVICE_ID],
811
+ projectId: PROJECT_ID,
812
+ entityTypes: [ServiceType.OpenTelemetry],
813
+ }),
814
+ ).resolves.toEqual({});
815
+ });
816
+
817
+ test("works with real model instances, not just plain rows", async () => {
818
+ const app: RumApplication = new RumApplication();
819
+ app._id = RUM_ID;
820
+ app.name = "checkout-web";
821
+ tables[ServiceType.RealUserMonitor] = [app as unknown as FakeRow];
822
+
823
+ const host: Host = new Host();
824
+ host._id = HOST_ID;
825
+ host.hostIdentifier = "ip-10-0-0-1";
826
+ tables[ServiceType.Host] = [host as unknown as FakeRow];
827
+
828
+ const result: TelemetryEntityNameMap =
829
+ await TelemetryEntityNameResolver.resolve({
830
+ ids: [RUM_ID, HOST_ID],
831
+ projectId: PROJECT_ID,
832
+ });
833
+ expect(result[RUM_ID]?.name).toBe("checkout-web");
834
+ expect(result[HOST_ID]?.name).toBe("ip-10-0-0-1");
835
+ });
836
+ });
837
+
838
+ describe("TelemetryEntityNameResolver.resolve — typeHints", () => {
839
+ test("a hinted RUM id is resolved with exactly one RumApplication request", async () => {
840
+ const result: TelemetryEntityNameMap =
841
+ await TelemetryEntityNameResolver.resolve({
842
+ ids: [RUM_ID],
843
+ projectId: PROJECT_ID,
844
+ typeHints: { [RUM_ID]: ServiceType.RealUserMonitor },
845
+ });
846
+ expect(result).toEqual({
847
+ [RUM_ID]: {
848
+ id: RUM_ID,
849
+ name: "checkout-web",
850
+ entityType: ServiceType.RealUserMonitor,
851
+ typeLabel: "RUM Application",
852
+ },
853
+ });
854
+ expect(issuedRequests()).toEqual([
855
+ { entityType: ServiceType.RealUserMonitor, ids: [RUM_ID] },
856
+ ]);
857
+ });
858
+
859
+ test("hinted ids are grouped per table; unhinted ids still go Service-first", async () => {
860
+ const result: TelemetryEntityNameMap =
861
+ await TelemetryEntityNameResolver.resolve({
862
+ ids: [RUM_ID, HOST_ID, SERVICE_ID],
863
+ projectId: PROJECT_ID,
864
+ typeHints: {
865
+ [RUM_ID]: ServiceType.RealUserMonitor,
866
+ [HOST_ID]: ServiceType.Host,
867
+ },
868
+ });
869
+ expect(result[RUM_ID]?.typeLabel).toBe("RUM Application");
870
+ expect(result[HOST_ID]?.typeLabel).toBe("Host");
871
+ expect(result[SERVICE_ID]?.typeLabel).toBe("Service");
872
+
873
+ const requests: Array<IssuedRequest> = issuedRequests();
874
+ expect(requests).toHaveLength(3);
875
+ // The hinted requests are issued together, before the Service pass.
876
+ expect(
877
+ requests
878
+ .slice(0, 2)
879
+ .map((request: IssuedRequest): string => {
880
+ return request.entityType;
881
+ })
882
+ .sort(),
883
+ ).toEqual([ServiceType.Host, ServiceType.RealUserMonitor].sort());
884
+ expect(requests).toContainEqual({
885
+ entityType: ServiceType.RealUserMonitor,
886
+ ids: [RUM_ID],
887
+ });
888
+ expect(requests).toContainEqual({
889
+ entityType: ServiceType.Host,
890
+ ids: [HOST_ID],
891
+ });
892
+ expect(requests[2]).toEqual({
893
+ entityType: ServiceType.OpenTelemetry,
894
+ ids: [SERVICE_ID],
895
+ });
896
+ });
897
+
898
+ test("several ids hinted to the same table share one request", async () => {
899
+ tables[ServiceType.OpenTelemetry] = [
900
+ row(SERVICE_ID, { name: "a" }),
901
+ row(SERVICE_ID_2, { name: "b" }),
902
+ ];
903
+ await TelemetryEntityNameResolver.resolve({
904
+ ids: [SERVICE_ID, SERVICE_ID_2],
905
+ projectId: PROJECT_ID,
906
+ typeHints: {
907
+ [SERVICE_ID]: ServiceType.OpenTelemetry,
908
+ [SERVICE_ID_2]: ServiceType.OpenTelemetry,
909
+ },
910
+ });
911
+ expect(issuedRequests()).toEqual([
912
+ {
913
+ entityType: ServiceType.OpenTelemetry,
914
+ ids: [SERVICE_ID, SERVICE_ID_2],
915
+ },
916
+ ]);
917
+ });
918
+
919
+ test("a hinted id that misses its table falls through to the general pass", async () => {
920
+ // Hinted as RUM, but it is actually a Host.
921
+ const result: TelemetryEntityNameMap =
922
+ await TelemetryEntityNameResolver.resolve({
923
+ ids: [HOST_ID],
924
+ projectId: PROJECT_ID,
925
+ typeHints: { [HOST_ID]: ServiceType.RealUserMonitor },
926
+ });
927
+ expect(result[HOST_ID]).toEqual({
928
+ id: HOST_ID,
929
+ name: "prod-db-1",
930
+ entityType: ServiceType.Host,
931
+ typeLabel: "Host",
932
+ });
933
+ const types: Array<ServiceType> = requestedTypes();
934
+ expect(types[0]).toBe(ServiceType.RealUserMonitor);
935
+ expect(types[1]).toBe(ServiceType.OpenTelemetry);
936
+ expect(types).toContain(ServiceType.Host);
937
+ });
938
+
939
+ test("a hinted id that misses is never asked of its hinted table again", async () => {
940
+ await TelemetryEntityNameResolver.resolve({
941
+ ids: [HOST_ID, MISSING_ID],
942
+ projectId: PROJECT_ID,
943
+ typeHints: { [HOST_ID]: ServiceType.RealUserMonitor },
944
+ });
945
+ const rumRequests: Array<IssuedRequest> = issuedRequests().filter(
946
+ (request: IssuedRequest): boolean => {
947
+ return request.entityType === ServiceType.RealUserMonitor;
948
+ },
949
+ );
950
+ // One hinted request for HOST_ID, one fan-out request for MISSING_ID only.
951
+ expect(rumRequests).toEqual([
952
+ { entityType: ServiceType.RealUserMonitor, ids: [HOST_ID] },
953
+ { entityType: ServiceType.RealUserMonitor, ids: [MISSING_ID] },
954
+ ]);
955
+ });
956
+
957
+ test("a hinted Service id that misses skips the Service pass for that id", async () => {
958
+ await TelemetryEntityNameResolver.resolve({
959
+ ids: [RUM_ID],
960
+ projectId: PROJECT_ID,
961
+ typeHints: { [RUM_ID]: ServiceType.OpenTelemetry },
962
+ });
963
+ const serviceRequests: Array<IssuedRequest> = issuedRequests().filter(
964
+ (request: IssuedRequest): boolean => {
965
+ return request.entityType === ServiceType.OpenTelemetry;
966
+ },
967
+ );
968
+ expect(serviceRequests).toEqual([
969
+ { entityType: ServiceType.OpenTelemetry, ids: [RUM_ID] },
970
+ ]);
971
+ expect(requestedTypes()).toContain(ServiceType.RealUserMonitor);
972
+ });
973
+
974
+ test("a hinted id whose table fails falls through to the general pass", async () => {
975
+ failingTypes.add(ServiceType.KubernetesCluster);
976
+ tables[ServiceType.OpenTelemetry] = [
977
+ row(CLUSTER_ID, { name: "actually-a-service" }),
978
+ ];
979
+ const result: TelemetryEntityNameMap =
980
+ await TelemetryEntityNameResolver.resolve({
981
+ ids: [CLUSTER_ID],
982
+ projectId: PROJECT_ID,
983
+ typeHints: { [CLUSTER_ID]: ServiceType.KubernetesCluster },
984
+ });
985
+ expect(result[CLUSTER_ID]).toMatchObject({
986
+ name: "actually-a-service",
987
+ entityType: ServiceType.OpenTelemetry,
988
+ });
989
+ });
990
+
991
+ test("hints outside the allowed entityTypes are ignored", async () => {
992
+ const result: TelemetryEntityNameMap =
993
+ await TelemetryEntityNameResolver.resolve({
994
+ ids: [RUM_ID],
995
+ projectId: PROJECT_ID,
996
+ typeHints: { [RUM_ID]: ServiceType.RealUserMonitor },
997
+ entityTypes: [ServiceType.OpenTelemetry],
998
+ });
999
+ expect(result).toEqual({});
1000
+ expect(requestedTypes()).toEqual([ServiceType.OpenTelemetry]);
1001
+ });
1002
+
1003
+ test("undefined hints and hints for ids not requested are ignored", async () => {
1004
+ await TelemetryEntityNameResolver.resolve({
1005
+ ids: [SERVICE_ID],
1006
+ projectId: PROJECT_ID,
1007
+ typeHints: {
1008
+ [SERVICE_ID]: undefined,
1009
+ [RUM_ID]: ServiceType.RealUserMonitor,
1010
+ },
1011
+ });
1012
+ expect(issuedRequests()).toEqual([
1013
+ { entityType: ServiceType.OpenTelemetry, ids: [SERVICE_ID] },
1014
+ ]);
1015
+ });
1016
+
1017
+ test("an Unknown hint never queries a table", async () => {
1018
+ const result: TelemetryEntityNameMap =
1019
+ await TelemetryEntityNameResolver.resolve({
1020
+ ids: [PROJECT_ID],
1021
+ projectId: PROJECT_ID,
1022
+ typeHints: { [PROJECT_ID]: ServiceType.Unknown },
1023
+ });
1024
+ expect(result[PROJECT_ID]?.name).toBe(UNKNOWN_SERVICE_NAME);
1025
+ expect(getListMock).not.toHaveBeenCalled();
1026
+ });
1027
+ });
1028
+
1029
+ describe("TelemetryEntityNameResolver.resolve — entityTypes restriction", () => {
1030
+ test("Service-only never queries RumApplication (or any other table)", async () => {
1031
+ const result: TelemetryEntityNameMap =
1032
+ await TelemetryEntityNameResolver.resolve({
1033
+ ids: [SERVICE_ID, RUM_ID],
1034
+ projectId: PROJECT_ID,
1035
+ entityTypes: [ServiceType.OpenTelemetry],
1036
+ });
1037
+ expect(Object.keys(result)).toEqual([SERVICE_ID]);
1038
+ expect(requestedTypes()).toEqual([ServiceType.OpenTelemetry]);
1039
+ });
1040
+
1041
+ test("a cached RUM entity is not returned to a Service-only caller", async () => {
1042
+ await TelemetryEntityNameResolver.resolve({
1043
+ ids: [RUM_ID],
1044
+ projectId: PROJECT_ID,
1045
+ });
1046
+ getListMock.mockClear();
1047
+
1048
+ const serviceOnly: TelemetryEntityNameMap =
1049
+ await TelemetryEntityNameResolver.resolve({
1050
+ ids: [RUM_ID],
1051
+ projectId: PROJECT_ID,
1052
+ entityTypes: [ServiceType.OpenTelemetry],
1053
+ });
1054
+ expect(serviceOnly).toEqual({});
1055
+ expect(requestedTypes()).toEqual([ServiceType.OpenTelemetry]);
1056
+
1057
+ // The unrestricted caller still gets it from cache.
1058
+ getListMock.mockClear();
1059
+ const unrestricted: TelemetryEntityNameMap =
1060
+ await TelemetryEntityNameResolver.resolve({
1061
+ ids: [RUM_ID],
1062
+ projectId: PROJECT_ID,
1063
+ });
1064
+ expect(unrestricted[RUM_ID]?.name).toBe("checkout-web");
1065
+ expect(getListMock).not.toHaveBeenCalled();
1066
+ });
1067
+
1068
+ test("the first allowed type (in resolution order) is queried first, the rest in parallel", async () => {
1069
+ // Given out of order on purpose.
1070
+ const result: TelemetryEntityNameMap =
1071
+ await TelemetryEntityNameResolver.resolve({
1072
+ ids: [HOST_ID],
1073
+ projectId: PROJECT_ID,
1074
+ entityTypes: [ServiceType.Host, ServiceType.RealUserMonitor],
1075
+ });
1076
+ expect(result[HOST_ID]?.name).toBe("prod-db-1");
1077
+ expect(requestedTypes()).toEqual([
1078
+ ServiceType.RealUserMonitor,
1079
+ ServiceType.Host,
1080
+ ]);
1081
+ });
1082
+
1083
+ test("a single non-Service type issues exactly one request", async () => {
1084
+ await TelemetryEntityNameResolver.resolve({
1085
+ ids: [MISSING_ID],
1086
+ projectId: PROJECT_ID,
1087
+ entityTypes: [ServiceType.KubernetesCluster],
1088
+ });
1089
+ expect(requestedTypes()).toEqual([ServiceType.KubernetesCluster]);
1090
+ });
1091
+
1092
+ test("an empty entityTypes array means no restriction", async () => {
1093
+ const result: TelemetryEntityNameMap =
1094
+ await TelemetryEntityNameResolver.resolve({
1095
+ ids: [CLUSTER_ID],
1096
+ projectId: PROJECT_ID,
1097
+ entityTypes: [],
1098
+ });
1099
+ expect(result[CLUSTER_ID]?.typeLabel).toBe("Kubernetes Cluster");
1100
+ expect(getListMock).toHaveBeenCalledTimes(
1101
+ TELEMETRY_ENTITY_RESOLUTION_ORDER.length,
1102
+ );
1103
+ });
1104
+
1105
+ test("the Unknown bucket is available to Service-only callers", async () => {
1106
+ const result: TelemetryEntityNameMap =
1107
+ await TelemetryEntityNameResolver.resolve({
1108
+ ids: [PROJECT_ID],
1109
+ projectId: PROJECT_ID,
1110
+ entityTypes: [ServiceType.OpenTelemetry],
1111
+ });
1112
+ expect(result[PROJECT_ID]?.entityType).toBe(ServiceType.Unknown);
1113
+ });
1114
+
1115
+ test("entityTypes [Unknown] resolves the projectId only and queries nothing", async () => {
1116
+ const result: TelemetryEntityNameMap =
1117
+ await TelemetryEntityNameResolver.resolve({
1118
+ ids: [PROJECT_ID, SERVICE_ID],
1119
+ projectId: PROJECT_ID,
1120
+ entityTypes: [ServiceType.Unknown],
1121
+ });
1122
+ expect(Object.keys(result)).toEqual([PROJECT_ID]);
1123
+ expect(getListMock).not.toHaveBeenCalled();
1124
+ });
1125
+
1126
+ test("the Unknown bucket is not returned when neither Unknown nor Service is allowed", async () => {
1127
+ const result: TelemetryEntityNameMap =
1128
+ await TelemetryEntityNameResolver.resolve({
1129
+ ids: [PROJECT_ID],
1130
+ projectId: PROJECT_ID,
1131
+ entityTypes: [ServiceType.Host],
1132
+ });
1133
+ expect(result).toEqual({});
1134
+ });
1135
+ });
1136
+
1137
+ describe("TelemetryEntityNameResolver.resolve — failure isolation", () => {
1138
+ test("a failing Service table does not stop the fan-out", async () => {
1139
+ failingTypes.add(ServiceType.OpenTelemetry);
1140
+ const result: TelemetryEntityNameMap =
1141
+ await TelemetryEntityNameResolver.resolve({
1142
+ ids: [SERVICE_ID, RUM_ID],
1143
+ projectId: PROJECT_ID,
1144
+ });
1145
+ expect(result[SERVICE_ID]).toBeUndefined();
1146
+ expect(result[RUM_ID]?.name).toBe("checkout-web");
1147
+ });
1148
+
1149
+ test("one failing table in the fan-out leaves the others resolved", async () => {
1150
+ failingTypes.add(ServiceType.RealUserMonitor);
1151
+ const result: TelemetryEntityNameMap =
1152
+ await TelemetryEntityNameResolver.resolve({
1153
+ ids: [SERVICE_ID, RUM_ID, HOST_ID, CLUSTER_ID, INCIDENT_ID],
1154
+ projectId: PROJECT_ID,
1155
+ });
1156
+ expect(result[SERVICE_ID]?.name).toBe("checkout-api");
1157
+ expect(result[RUM_ID]).toBeUndefined();
1158
+ expect(result[HOST_ID]?.name).toBe("prod-db-1");
1159
+ expect(result[CLUSTER_ID]?.name).toBe("prod-eu-west");
1160
+ expect(result[INCIDENT_ID]?.name).toBe("API is down");
1161
+ });
1162
+
1163
+ test("a synchronous throw from getList is isolated as well", async () => {
1164
+ getListMock.mockImplementation((...callArgs: Array<unknown>) => {
1165
+ const args: GetListArgs = callArgs[0] as GetListArgs;
1166
+ if (entityTypeForModel(args.modelType) === ServiceType.Host) {
1167
+ throw new Error("boom");
1168
+ }
1169
+ return fakeGetList(args);
1170
+ });
1171
+ const result: TelemetryEntityNameMap =
1172
+ await TelemetryEntityNameResolver.resolve({
1173
+ ids: [HOST_ID, CLUSTER_ID],
1174
+ projectId: PROJECT_ID,
1175
+ });
1176
+ expect(result[HOST_ID]).toBeUndefined();
1177
+ expect(result[CLUSTER_ID]?.name).toBe("prod-eu-west");
1178
+ });
1179
+
1180
+ test("every table failing resolves to {} instead of rejecting", async () => {
1181
+ getListMock.mockRejectedValue(new Error("offline"));
1182
+ await expect(
1183
+ TelemetryEntityNameResolver.resolve({
1184
+ ids: [SERVICE_ID, RUM_ID],
1185
+ projectId: PROJECT_ID,
1186
+ }),
1187
+ ).resolves.toEqual({});
1188
+ });
1189
+
1190
+ test("failures are only remembered briefly: the call after the miss TTL retries", async () => {
1191
+ /*
1192
+ * A failed table leaves its ids in the short-lived miss cache (so a
1193
+ * live view does not hammer a table the role cannot read), but never
1194
+ * as a hit: once the miss TTL passes the lookup is retried.
1195
+ */
1196
+ const nowSpy: NowSpy = freezeNow(START_TIME);
1197
+ failingTypes.add(ServiceType.OpenTelemetry);
1198
+ await TelemetryEntityNameResolver.resolve({
1199
+ ids: [SERVICE_ID],
1200
+ projectId: PROJECT_ID,
1201
+ entityTypes: [ServiceType.OpenTelemetry],
1202
+ });
1203
+ failingTypes.clear();
1204
+ nowSpy.mockReturnValue(START_TIME + MISS_CACHE_TTL_IN_MS + 1);
1205
+ const result: TelemetryEntityNameMap =
1206
+ await TelemetryEntityNameResolver.resolve({
1207
+ ids: [SERVICE_ID],
1208
+ projectId: PROJECT_ID,
1209
+ entityTypes: [ServiceType.OpenTelemetry],
1210
+ });
1211
+ expect(result[SERVICE_ID]?.name).toBe("checkout-api");
1212
+ expect(getListMock).toHaveBeenCalledTimes(2);
1213
+ });
1214
+ });
1215
+
1216
+ describe("TelemetryEntityNameResolver.resolve — name fields", () => {
1217
+ test("RumApplication without a name falls back to appIdentifier", async () => {
1218
+ tables[ServiceType.RealUserMonitor] = [
1219
+ row(RUM_ID, { name: undefined, appIdentifier: "checkout-web-prod" }),
1220
+ ];
1221
+ const result: TelemetryEntityNameMap =
1222
+ await TelemetryEntityNameResolver.resolve({
1223
+ ids: [RUM_ID],
1224
+ projectId: PROJECT_ID,
1225
+ typeHints: { [RUM_ID]: ServiceType.RealUserMonitor },
1226
+ });
1227
+ expect(result[RUM_ID]).toEqual({
1228
+ id: RUM_ID,
1229
+ name: "checkout-web-prod",
1230
+ entityType: ServiceType.RealUserMonitor,
1231
+ typeLabel: "RUM Application",
1232
+ });
1233
+ });
1234
+
1235
+ test("Host without a name falls back to hostIdentifier", async () => {
1236
+ tables[ServiceType.Host] = [
1237
+ row(HOST_ID, { name: null, hostIdentifier: "ip-10-0-0-12" }),
1238
+ ];
1239
+ const result: TelemetryEntityNameMap =
1240
+ await TelemetryEntityNameResolver.resolve({
1241
+ ids: [HOST_ID],
1242
+ projectId: PROJECT_ID,
1243
+ typeHints: { [HOST_ID]: ServiceType.Host },
1244
+ });
1245
+ expect(result[HOST_ID]?.name).toBe("ip-10-0-0-12");
1246
+ });
1247
+
1248
+ test("KubernetesCluster without a name falls back to clusterIdentifier", async () => {
1249
+ tables[ServiceType.KubernetesCluster] = [
1250
+ row(CLUSTER_ID, { clusterIdentifier: "eks-prod" }),
1251
+ ];
1252
+ const result: TelemetryEntityNameMap =
1253
+ await TelemetryEntityNameResolver.resolve({
1254
+ ids: [CLUSTER_ID],
1255
+ projectId: PROJECT_ID,
1256
+ typeHints: { [CLUSTER_ID]: ServiceType.KubernetesCluster },
1257
+ });
1258
+ expect(result[CLUSTER_ID]?.name).toBe("eks-prod");
1259
+ });
1260
+
1261
+ test("a blank name is skipped in favour of the next field", async () => {
1262
+ tables[ServiceType.RealUserMonitor] = [
1263
+ row(RUM_ID, { name: " ", appIdentifier: "web-app" }),
1264
+ ];
1265
+ const result: TelemetryEntityNameMap =
1266
+ await TelemetryEntityNameResolver.resolve({
1267
+ ids: [RUM_ID],
1268
+ projectId: PROJECT_ID,
1269
+ typeHints: { [RUM_ID]: ServiceType.RealUserMonitor },
1270
+ });
1271
+ expect(result[RUM_ID]?.name).toBe("web-app");
1272
+ });
1273
+
1274
+ test("the name wins over the fallback field when both are present", async () => {
1275
+ tables[ServiceType.Host] = [
1276
+ row(HOST_ID, { name: "Primary DB", hostIdentifier: "ip-10-0-0-12" }),
1277
+ ];
1278
+ const result: TelemetryEntityNameMap =
1279
+ await TelemetryEntityNameResolver.resolve({
1280
+ ids: [HOST_ID],
1281
+ projectId: PROJECT_ID,
1282
+ typeHints: { [HOST_ID]: ServiceType.Host },
1283
+ });
1284
+ expect(result[HOST_ID]?.name).toBe("Primary DB");
1285
+ });
1286
+
1287
+ test("names are trimmed", async () => {
1288
+ tables[ServiceType.OpenTelemetry] = [
1289
+ row(SERVICE_ID, { name: " checkout-api \n" }),
1290
+ ];
1291
+ const result: TelemetryEntityNameMap =
1292
+ await TelemetryEntityNameResolver.resolve({
1293
+ ids: [SERVICE_ID],
1294
+ projectId: PROJECT_ID,
1295
+ });
1296
+ expect(result[SERVICE_ID]?.name).toBe("checkout-api");
1297
+ });
1298
+
1299
+ test("a row with no usable name in any field is not resolved (and not cached as a hit)", async () => {
1300
+ const nowSpy: NowSpy = freezeNow(START_TIME);
1301
+ tables[ServiceType.OpenTelemetry] = [row(SERVICE_ID, { name: "" })];
1302
+ const first: TelemetryEntityNameMap =
1303
+ await TelemetryEntityNameResolver.resolve({
1304
+ ids: [SERVICE_ID],
1305
+ projectId: PROJECT_ID,
1306
+ entityTypes: [ServiceType.OpenTelemetry],
1307
+ });
1308
+ expect(first).toEqual({});
1309
+
1310
+ tables[ServiceType.OpenTelemetry] = [row(SERVICE_ID, { name: "renamed" })];
1311
+ // Past the (short) miss TTL, but well inside the hit TTL.
1312
+ nowSpy.mockReturnValue(START_TIME + MISS_CACHE_TTL_IN_MS + 1);
1313
+ const second: TelemetryEntityNameMap =
1314
+ await TelemetryEntityNameResolver.resolve({
1315
+ ids: [SERVICE_ID],
1316
+ projectId: PROJECT_ID,
1317
+ entityTypes: [ServiceType.OpenTelemetry],
1318
+ });
1319
+ expect(second[SERVICE_ID]?.name).toBe("renamed");
1320
+ });
1321
+
1322
+ test("Incident is named by its title, not a name column", async () => {
1323
+ tables[ServiceType.Incident] = [
1324
+ row(INCIDENT_ID, { name: "ignored", title: "Checkout latency spike" }),
1325
+ ];
1326
+ const result: TelemetryEntityNameMap =
1327
+ await TelemetryEntityNameResolver.resolve({
1328
+ ids: [INCIDENT_ID],
1329
+ projectId: PROJECT_ID,
1330
+ typeHints: { [INCIDENT_ID]: ServiceType.Incident },
1331
+ });
1332
+ expect(result[INCIDENT_ID]).toEqual({
1333
+ id: INCIDENT_ID,
1334
+ name: "Checkout latency spike",
1335
+ entityType: ServiceType.Incident,
1336
+ typeLabel: "Incident",
1337
+ });
1338
+ });
1339
+
1340
+ test("non-string names are stringified", async () => {
1341
+ tables[ServiceType.OpenTelemetry] = [row(SERVICE_ID, { name: 42 })];
1342
+ const result: TelemetryEntityNameMap =
1343
+ await TelemetryEntityNameResolver.resolve({
1344
+ ids: [SERVICE_ID],
1345
+ projectId: PROJECT_ID,
1346
+ });
1347
+ expect(result[SERVICE_ID]?.name).toBe("42");
1348
+ });
1349
+ });
1350
+
1351
+ describe("TelemetryEntityNameResolver — cache", () => {
1352
+ test("a cache hit avoids a second request", async () => {
1353
+ const first: TelemetryEntityNameMap =
1354
+ await TelemetryEntityNameResolver.resolve({
1355
+ ids: [SERVICE_ID, RUM_ID],
1356
+ projectId: PROJECT_ID,
1357
+ });
1358
+ const requestsAfterFirst: number = getListMock.mock.calls.length;
1359
+ expect(requestsAfterFirst).toBeGreaterThan(0);
1360
+
1361
+ const second: TelemetryEntityNameMap =
1362
+ await TelemetryEntityNameResolver.resolve({
1363
+ ids: [RUM_ID, SERVICE_ID],
1364
+ projectId: PROJECT_ID,
1365
+ });
1366
+ expect(second).toEqual(first);
1367
+ expect(getListMock.mock.calls.length).toBe(requestsAfterFirst);
1368
+ });
1369
+
1370
+ test("only the uncached ids are queried on a partial cache hit", async () => {
1371
+ await TelemetryEntityNameResolver.resolve({
1372
+ ids: [SERVICE_ID],
1373
+ projectId: PROJECT_ID,
1374
+ });
1375
+ getListMock.mockClear();
1376
+
1377
+ const result: TelemetryEntityNameMap =
1378
+ await TelemetryEntityNameResolver.resolve({
1379
+ ids: [SERVICE_ID, SERVICE_ID_2],
1380
+ projectId: PROJECT_ID,
1381
+ });
1382
+ expect(result[SERVICE_ID]?.name).toBe("checkout-api");
1383
+ expect(result[SERVICE_ID_2]?.name).toBe("payments-api");
1384
+ expect(issuedRequests()).toEqual([
1385
+ { entityType: ServiceType.OpenTelemetry, ids: [SERVICE_ID_2] },
1386
+ ]);
1387
+ });
1388
+
1389
+ test("a hint for a cached id does not trigger a request", async () => {
1390
+ await TelemetryEntityNameResolver.resolve({
1391
+ ids: [RUM_ID],
1392
+ projectId: PROJECT_ID,
1393
+ typeHints: { [RUM_ID]: ServiceType.RealUserMonitor },
1394
+ });
1395
+ getListMock.mockClear();
1396
+ await TelemetryEntityNameResolver.resolve({
1397
+ ids: [RUM_ID],
1398
+ projectId: PROJECT_ID,
1399
+ typeHints: { [RUM_ID]: ServiceType.RealUserMonitor },
1400
+ });
1401
+ expect(getListMock).not.toHaveBeenCalled();
1402
+ });
1403
+
1404
+ test("misses are only cached briefly, so a newly created resource resolves once the miss TTL passes", async () => {
1405
+ const nowSpy: NowSpy = freezeNow(START_TIME);
1406
+ await TelemetryEntityNameResolver.resolve({
1407
+ ids: [MISSING_ID],
1408
+ projectId: PROJECT_ID,
1409
+ entityTypes: [ServiceType.OpenTelemetry],
1410
+ });
1411
+ tables[ServiceType.OpenTelemetry]!.push(
1412
+ row(MISSING_ID, { name: "brand-new" }),
1413
+ );
1414
+ nowSpy.mockReturnValue(START_TIME + MISS_CACHE_TTL_IN_MS + 1);
1415
+ const result: TelemetryEntityNameMap =
1416
+ await TelemetryEntityNameResolver.resolve({
1417
+ ids: [MISSING_ID],
1418
+ projectId: PROJECT_ID,
1419
+ entityTypes: [ServiceType.OpenTelemetry],
1420
+ });
1421
+ expect(result[MISSING_ID]?.name).toBe("brand-new");
1422
+ expect(getListMock).toHaveBeenCalledTimes(2);
1423
+ });
1424
+
1425
+ test("the cache is scoped per project", async () => {
1426
+ const otherProject: string = "9e1b6b0e-0000-4000-8000-000000000002";
1427
+ await TelemetryEntityNameResolver.resolve({
1428
+ ids: [SERVICE_ID],
1429
+ projectId: PROJECT_ID,
1430
+ });
1431
+ getListMock.mockClear();
1432
+ await TelemetryEntityNameResolver.resolve({
1433
+ ids: [SERVICE_ID],
1434
+ projectId: otherProject,
1435
+ entityTypes: [ServiceType.OpenTelemetry],
1436
+ });
1437
+ expect(getListMock).toHaveBeenCalledTimes(1);
1438
+ expect(argsOf(0).query.projectId.toString()).toBe(otherProject);
1439
+ });
1440
+
1441
+ test("clearCache forces a fresh request", async () => {
1442
+ await TelemetryEntityNameResolver.resolve({
1443
+ ids: [SERVICE_ID],
1444
+ projectId: PROJECT_ID,
1445
+ });
1446
+ TelemetryEntityNameResolver.clearCache();
1447
+ tables[ServiceType.OpenTelemetry] = [
1448
+ row(SERVICE_ID, { name: "checkout-api-renamed" }),
1449
+ ];
1450
+ const result: TelemetryEntityNameMap =
1451
+ await TelemetryEntityNameResolver.resolve({
1452
+ ids: [SERVICE_ID],
1453
+ projectId: PROJECT_ID,
1454
+ });
1455
+ expect(result[SERVICE_ID]?.name).toBe("checkout-api-renamed");
1456
+ expect(getListMock).toHaveBeenCalledTimes(2);
1457
+ });
1458
+
1459
+ test("entries expire after the TTL (60s)", async () => {
1460
+ const start: number = 1_800_000_000_000;
1461
+ /*
1462
+ * Named structurally: jest.spyOn from @jest/globals returns jest-mock's
1463
+ * SpyInstance, which does not assign to the global jest.SpiedFunction.
1464
+ */
1465
+ const nowSpy: { mockReturnValue: (value: number) => unknown } = jest
1466
+ .spyOn(Date, "now")
1467
+ .mockReturnValue(start);
1468
+
1469
+ await TelemetryEntityNameResolver.resolve({
1470
+ ids: [SERVICE_ID],
1471
+ projectId: PROJECT_ID,
1472
+ });
1473
+ expect(getListMock).toHaveBeenCalledTimes(1);
1474
+
1475
+ nowSpy.mockReturnValue(start + 59 * 1000);
1476
+ await TelemetryEntityNameResolver.resolve({
1477
+ ids: [SERVICE_ID],
1478
+ projectId: PROJECT_ID,
1479
+ });
1480
+ expect(getListMock).toHaveBeenCalledTimes(1);
1481
+
1482
+ nowSpy.mockReturnValue(start + 61 * 1000);
1483
+ await TelemetryEntityNameResolver.resolve({
1484
+ ids: [SERVICE_ID],
1485
+ projectId: PROJECT_ID,
1486
+ });
1487
+ expect(getListMock).toHaveBeenCalledTimes(2);
1488
+ });
1489
+
1490
+ test("callers get the same entity values from cache as from the network", async () => {
1491
+ const fromNetwork: ResolvedTelemetryEntity | undefined = (
1492
+ await TelemetryEntityNameResolver.resolve({
1493
+ ids: [HOST_ID],
1494
+ projectId: PROJECT_ID,
1495
+ })
1496
+ )[HOST_ID];
1497
+ const fromCache: ResolvedTelemetryEntity | undefined = (
1498
+ await TelemetryEntityNameResolver.resolve({
1499
+ ids: [HOST_ID],
1500
+ projectId: PROJECT_ID,
1501
+ })
1502
+ )[HOST_ID];
1503
+ expect(fromCache).toEqual(fromNetwork);
1504
+ });
1505
+ });
1506
+
1507
+ describe("TelemetryEntityNameResolver — in-flight dedup", () => {
1508
+ test("concurrent identical calls share one set of requests", async () => {
1509
+ const results: Array<TelemetryEntityNameMap> = await Promise.all([
1510
+ TelemetryEntityNameResolver.resolve({
1511
+ ids: [SERVICE_ID, RUM_ID],
1512
+ projectId: PROJECT_ID,
1513
+ }),
1514
+ TelemetryEntityNameResolver.resolve({
1515
+ ids: [SERVICE_ID, RUM_ID],
1516
+ projectId: PROJECT_ID,
1517
+ }),
1518
+ // Same set in a different order, as ObjectIDs.
1519
+ TelemetryEntityNameResolver.resolve({
1520
+ ids: [new ObjectID(RUM_ID), new ObjectID(SERVICE_ID)],
1521
+ projectId: new ObjectID(PROJECT_ID),
1522
+ }),
1523
+ ]);
1524
+ expect(results[0]![RUM_ID]?.name).toBe("checkout-web");
1525
+ expect(results[0]![SERVICE_ID]?.name).toBe("checkout-api");
1526
+ expect(results[1]).toEqual(results[0]);
1527
+ expect(results[2]).toEqual(results[0]);
1528
+ expect(getListMock).toHaveBeenCalledTimes(
1529
+ TELEMETRY_ENTITY_RESOLUTION_ORDER.length,
1530
+ );
1531
+ expect(
1532
+ requestedTypes().filter((type: ServiceType): boolean => {
1533
+ return type === ServiceType.OpenTelemetry;
1534
+ }),
1535
+ ).toHaveLength(1);
1536
+ });
1537
+
1538
+ test("concurrent calls with different options are not merged", async () => {
1539
+ await Promise.all([
1540
+ TelemetryEntityNameResolver.resolve({
1541
+ ids: [RUM_ID],
1542
+ projectId: PROJECT_ID,
1543
+ entityTypes: [ServiceType.OpenTelemetry],
1544
+ }),
1545
+ TelemetryEntityNameResolver.resolve({
1546
+ ids: [RUM_ID],
1547
+ projectId: PROJECT_ID,
1548
+ typeHints: { [RUM_ID]: ServiceType.RealUserMonitor },
1549
+ }),
1550
+ ]);
1551
+ expect(requestedTypes().sort()).toEqual(
1552
+ [ServiceType.OpenTelemetry, ServiceType.RealUserMonitor].sort(),
1553
+ );
1554
+ });
1555
+
1556
+ test("once a call settles, the next identical call is served by the cache, not the stale promise", async () => {
1557
+ const nowSpy: NowSpy = freezeNow(START_TIME);
1558
+ await TelemetryEntityNameResolver.resolve({
1559
+ ids: [MISSING_ID],
1560
+ projectId: PROJECT_ID,
1561
+ entityTypes: [ServiceType.OpenTelemetry],
1562
+ });
1563
+ tables[ServiceType.OpenTelemetry]!.push(
1564
+ row(MISSING_ID, { name: "appeared" }),
1565
+ );
1566
+ /*
1567
+ * Past the miss TTL the identical call must issue a new request; a
1568
+ * settled promise left in the in-flight map would replay the miss.
1569
+ */
1570
+ nowSpy.mockReturnValue(START_TIME + MISS_CACHE_TTL_IN_MS + 1);
1571
+ const result: TelemetryEntityNameMap =
1572
+ await TelemetryEntityNameResolver.resolve({
1573
+ ids: [MISSING_ID],
1574
+ projectId: PROJECT_ID,
1575
+ entityTypes: [ServiceType.OpenTelemetry],
1576
+ });
1577
+ expect(result[MISSING_ID]?.name).toBe("appeared");
1578
+ });
1579
+
1580
+ test("clearCache also drops in-flight entries", async () => {
1581
+ const serviceResponse: Deferred<{ data: Array<FakeRow> }> = deferred();
1582
+ getListMock.mockImplementationOnce(() => {
1583
+ return serviceResponse.promise;
1584
+ });
1585
+
1586
+ const stuck: Promise<TelemetryEntityNameMap> =
1587
+ TelemetryEntityNameResolver.resolve({
1588
+ ids: [SERVICE_ID],
1589
+ projectId: PROJECT_ID,
1590
+ entityTypes: [ServiceType.OpenTelemetry],
1591
+ });
1592
+ TelemetryEntityNameResolver.clearCache();
1593
+
1594
+ const fresh: TelemetryEntityNameMap =
1595
+ await TelemetryEntityNameResolver.resolve({
1596
+ ids: [SERVICE_ID],
1597
+ projectId: PROJECT_ID,
1598
+ entityTypes: [ServiceType.OpenTelemetry],
1599
+ });
1600
+ expect(fresh[SERVICE_ID]?.name).toBe("checkout-api");
1601
+ expect(getListMock).toHaveBeenCalledTimes(2);
1602
+
1603
+ serviceResponse.resolve({ data: [] });
1604
+ await expect(stuck).resolves.toEqual({});
1605
+ });
1606
+ });
1607
+
1608
+ describe("TelemetryEntityNameResolver — miss cache", () => {
1609
+ /*
1610
+ * An id that resolves nowhere (a deleted service still in retention, a
1611
+ * table the role cannot read) used to be searched across every table
1612
+ * each time a live view's id set changed. Misses are now remembered for
1613
+ * a short TTL, keyed by project + id + the tables that were searched.
1614
+ */
1615
+
1616
+ test("the miss TTL is positive and shorter than the hit TTL", () => {
1617
+ expect(MISS_CACHE_TTL_IN_MS).toBeGreaterThan(0);
1618
+ expect(MISS_CACHE_TTL_IN_MS).toBeLessThan(HIT_CACHE_TTL_IN_MS);
1619
+ });
1620
+
1621
+ test("an id that resolved nowhere is not searched again within the miss TTL", async () => {
1622
+ const nowSpy: NowSpy = freezeNow(START_TIME);
1623
+ await expect(
1624
+ TelemetryEntityNameResolver.resolve({
1625
+ ids: [MISSING_ID],
1626
+ projectId: PROJECT_ID,
1627
+ }),
1628
+ ).resolves.toEqual({});
1629
+ expect(getListMock).toHaveBeenCalledTimes(
1630
+ TELEMETRY_ENTITY_RESOLUTION_ORDER.length,
1631
+ );
1632
+
1633
+ getListMock.mockClear();
1634
+ nowSpy.mockReturnValue(START_TIME + MISS_CACHE_TTL_IN_MS - 1);
1635
+ await expect(
1636
+ TelemetryEntityNameResolver.resolve({
1637
+ ids: [MISSING_ID],
1638
+ projectId: PROJECT_ID,
1639
+ }),
1640
+ ).resolves.toEqual({});
1641
+ expect(getListMock).not.toHaveBeenCalled();
1642
+ });
1643
+
1644
+ test("after the miss TTL the id is searched across every table again", async () => {
1645
+ const nowSpy: NowSpy = freezeNow(START_TIME);
1646
+ await TelemetryEntityNameResolver.resolve({
1647
+ ids: [MISSING_ID],
1648
+ projectId: PROJECT_ID,
1649
+ });
1650
+ getListMock.mockClear();
1651
+
1652
+ nowSpy.mockReturnValue(START_TIME + MISS_CACHE_TTL_IN_MS + 1);
1653
+ await TelemetryEntityNameResolver.resolve({
1654
+ ids: [MISSING_ID],
1655
+ projectId: PROJECT_ID,
1656
+ });
1657
+ expect(requestedTypes()).toEqual(TELEMETRY_ENTITY_RESOLUTION_ORDER);
1658
+ for (const request of issuedRequests()) {
1659
+ expect(request.ids).toEqual([MISSING_ID]);
1660
+ }
1661
+ });
1662
+
1663
+ test("a remembered miss is left out of the next batch without holding back the other ids", async () => {
1664
+ freezeNow(START_TIME);
1665
+ await TelemetryEntityNameResolver.resolve({
1666
+ ids: [MISSING_ID],
1667
+ projectId: PROJECT_ID,
1668
+ });
1669
+ getListMock.mockClear();
1670
+
1671
+ const result: TelemetryEntityNameMap =
1672
+ await TelemetryEntityNameResolver.resolve({
1673
+ ids: [MISSING_ID, SERVICE_ID_2],
1674
+ projectId: PROJECT_ID,
1675
+ });
1676
+ expect(Object.keys(result)).toEqual([SERVICE_ID_2]);
1677
+ expect(result[SERVICE_ID_2]?.name).toBe("payments-api");
1678
+ expect(issuedRequests()).toEqual([
1679
+ { entityType: ServiceType.OpenTelemetry, ids: [SERVICE_ID_2] },
1680
+ ]);
1681
+ });
1682
+
1683
+ test("the miss key includes the allowed types: a Service-only miss does not block an unrestricted lookup", async () => {
1684
+ freezeNow(START_TIME);
1685
+ await expect(
1686
+ TelemetryEntityNameResolver.resolve({
1687
+ ids: [RUM_ID],
1688
+ projectId: PROJECT_ID,
1689
+ entityTypes: [ServiceType.OpenTelemetry],
1690
+ }),
1691
+ ).resolves.toEqual({});
1692
+ expect(requestedTypes()).toEqual([ServiceType.OpenTelemetry]);
1693
+
1694
+ // The same Service-only lookup is remembered…
1695
+ getListMock.mockClear();
1696
+ await TelemetryEntityNameResolver.resolve({
1697
+ ids: [RUM_ID],
1698
+ projectId: PROJECT_ID,
1699
+ entityTypes: [ServiceType.OpenTelemetry],
1700
+ });
1701
+ expect(getListMock).not.toHaveBeenCalled();
1702
+
1703
+ // …but a lookup allowed to search every table still finds the RUM app.
1704
+ getListMock.mockClear();
1705
+ const unrestricted: TelemetryEntityNameMap =
1706
+ await TelemetryEntityNameResolver.resolve({
1707
+ ids: [RUM_ID],
1708
+ projectId: PROJECT_ID,
1709
+ });
1710
+ expect(unrestricted[RUM_ID]).toEqual({
1711
+ id: RUM_ID,
1712
+ name: "checkout-web",
1713
+ entityType: ServiceType.RealUserMonitor,
1714
+ typeLabel: "RUM Application",
1715
+ });
1716
+ expect(requestsFor(ServiceType.RealUserMonitor)).toEqual([
1717
+ { entityType: ServiceType.RealUserMonitor, ids: [RUM_ID] },
1718
+ ]);
1719
+ });
1720
+
1721
+ test("misses are remembered per project", async () => {
1722
+ const otherProject: string = "9e1b6b0e-0000-4000-8000-000000000002";
1723
+ freezeNow(START_TIME);
1724
+ await TelemetryEntityNameResolver.resolve({
1725
+ ids: [MISSING_ID],
1726
+ projectId: PROJECT_ID,
1727
+ entityTypes: [ServiceType.OpenTelemetry],
1728
+ });
1729
+ getListMock.mockClear();
1730
+
1731
+ await TelemetryEntityNameResolver.resolve({
1732
+ ids: [MISSING_ID],
1733
+ projectId: otherProject,
1734
+ entityTypes: [ServiceType.OpenTelemetry],
1735
+ });
1736
+ expect(getListMock).toHaveBeenCalledTimes(1);
1737
+ expect(argsOf(0).query.projectId.toString()).toBe(otherProject);
1738
+ });
1739
+
1740
+ test("a hit that lands while a miss for the same lookup is remembered clears that miss", async () => {
1741
+ freezeNow(START_TIME);
1742
+ const plainResponse: Deferred<{ data: Array<FakeRow> }> = deferred();
1743
+ const hintedResponse: Deferred<{ data: Array<FakeRow> }> = deferred();
1744
+ getListMock
1745
+ .mockImplementationOnce(() => {
1746
+ return plainResponse.promise;
1747
+ })
1748
+ .mockImplementationOnce(() => {
1749
+ return hintedResponse.promise;
1750
+ });
1751
+
1752
+ /*
1753
+ * Two concurrent lookups over the same tables (so the same miss key)
1754
+ * that are not deduplicated because their hints differ.
1755
+ */
1756
+ const plain: Promise<TelemetryEntityNameMap> =
1757
+ TelemetryEntityNameResolver.resolve({
1758
+ ids: [MISSING_ID],
1759
+ projectId: PROJECT_ID,
1760
+ entityTypes: [ServiceType.OpenTelemetry],
1761
+ });
1762
+ const hinted: Promise<TelemetryEntityNameMap> =
1763
+ TelemetryEntityNameResolver.resolve({
1764
+ ids: [MISSING_ID],
1765
+ projectId: PROJECT_ID,
1766
+ entityTypes: [ServiceType.OpenTelemetry],
1767
+ typeHints: { [MISSING_ID]: ServiceType.OpenTelemetry },
1768
+ });
1769
+ await flushPromises();
1770
+ expect(getListMock).toHaveBeenCalledTimes(2);
1771
+
1772
+ plainResponse.resolve({ data: [] });
1773
+ await expect(plain).resolves.toEqual({});
1774
+ expect(
1775
+ missCacheKeys().filter((key: string): boolean => {
1776
+ return key.includes(MISSING_ID);
1777
+ }),
1778
+ ).toHaveLength(1);
1779
+
1780
+ hintedResponse.resolve({
1781
+ data: [row(MISSING_ID, { name: "just-created" })],
1782
+ });
1783
+ expect((await hinted)[MISSING_ID]?.name).toBe("just-created");
1784
+ expect(
1785
+ missCacheKeys().filter((key: string): boolean => {
1786
+ return key.includes(MISSING_ID);
1787
+ }),
1788
+ ).toEqual([]);
1789
+
1790
+ getListMock.mockClear();
1791
+ const again: TelemetryEntityNameMap =
1792
+ await TelemetryEntityNameResolver.resolve({
1793
+ ids: [MISSING_ID],
1794
+ projectId: PROJECT_ID,
1795
+ entityTypes: [ServiceType.OpenTelemetry],
1796
+ });
1797
+ expect(again[MISSING_ID]?.name).toBe("just-created");
1798
+ expect(getListMock).not.toHaveBeenCalled();
1799
+ });
1800
+
1801
+ test("a later hit after the miss TTL replaces the expired miss entry", async () => {
1802
+ const nowSpy: NowSpy = freezeNow(START_TIME);
1803
+ await TelemetryEntityNameResolver.resolve({
1804
+ ids: [MISSING_ID],
1805
+ projectId: PROJECT_ID,
1806
+ });
1807
+ expect(missCacheKeys()).toHaveLength(1);
1808
+
1809
+ tables[ServiceType.Host]!.push(row(MISSING_ID, { name: "late-host" }));
1810
+ nowSpy.mockReturnValue(START_TIME + MISS_CACHE_TTL_IN_MS + 1);
1811
+ const result: TelemetryEntityNameMap =
1812
+ await TelemetryEntityNameResolver.resolve({
1813
+ ids: [MISSING_ID],
1814
+ projectId: PROJECT_ID,
1815
+ });
1816
+ expect(result[MISSING_ID]).toMatchObject({
1817
+ name: "late-host",
1818
+ entityType: ServiceType.Host,
1819
+ });
1820
+ expect(missCacheKeys()).toEqual([]);
1821
+ });
1822
+
1823
+ test("clearCache forgets misses", async () => {
1824
+ freezeNow(START_TIME);
1825
+ await TelemetryEntityNameResolver.resolve({
1826
+ ids: [MISSING_ID],
1827
+ projectId: PROJECT_ID,
1828
+ entityTypes: [ServiceType.OpenTelemetry],
1829
+ });
1830
+ tables[ServiceType.OpenTelemetry]!.push(
1831
+ row(MISSING_ID, { name: "brand-new" }),
1832
+ );
1833
+
1834
+ TelemetryEntityNameResolver.clearCache();
1835
+ expect(missCacheKeys()).toEqual([]);
1836
+
1837
+ // Same instant: only the cleared miss could have suppressed this.
1838
+ const result: TelemetryEntityNameMap =
1839
+ await TelemetryEntityNameResolver.resolve({
1840
+ ids: [MISSING_ID],
1841
+ projectId: PROJECT_ID,
1842
+ entityTypes: [ServiceType.OpenTelemetry],
1843
+ });
1844
+ expect(result[MISSING_ID]?.name).toBe("brand-new");
1845
+ expect(getListMock).toHaveBeenCalledTimes(2);
1846
+ });
1847
+
1848
+ test("ids left unresolved by a failing table are remembered too", async () => {
1849
+ const nowSpy: NowSpy = freezeNow(START_TIME);
1850
+ failingTypes.add(ServiceType.RealUserMonitor);
1851
+ await expect(
1852
+ TelemetryEntityNameResolver.resolve({
1853
+ ids: [RUM_ID],
1854
+ projectId: PROJECT_ID,
1855
+ }),
1856
+ ).resolves.toEqual({});
1857
+ expect(requestedTypes()).toContain(ServiceType.RealUserMonitor);
1858
+
1859
+ getListMock.mockClear();
1860
+ failingTypes.clear();
1861
+ await expect(
1862
+ TelemetryEntityNameResolver.resolve({
1863
+ ids: [RUM_ID],
1864
+ projectId: PROJECT_ID,
1865
+ }),
1866
+ ).resolves.toEqual({});
1867
+ expect(getListMock).not.toHaveBeenCalled();
1868
+
1869
+ nowSpy.mockReturnValue(START_TIME + MISS_CACHE_TTL_IN_MS + 1);
1870
+ const result: TelemetryEntityNameMap =
1871
+ await TelemetryEntityNameResolver.resolve({
1872
+ ids: [RUM_ID],
1873
+ projectId: PROJECT_ID,
1874
+ });
1875
+ expect(result[RUM_ID]?.name).toBe("checkout-web");
1876
+ });
1877
+
1878
+ test("a hinted id that resolved nowhere is remembered as well", async () => {
1879
+ freezeNow(START_TIME);
1880
+ await TelemetryEntityNameResolver.resolve({
1881
+ ids: [MISSING_ID],
1882
+ projectId: PROJECT_ID,
1883
+ typeHints: { [MISSING_ID]: ServiceType.RealUserMonitor },
1884
+ });
1885
+ expect(getListMock).toHaveBeenCalledTimes(
1886
+ TELEMETRY_ENTITY_RESOLUTION_ORDER.length,
1887
+ );
1888
+
1889
+ getListMock.mockClear();
1890
+ await TelemetryEntityNameResolver.resolve({
1891
+ ids: [MISSING_ID],
1892
+ projectId: PROJECT_ID,
1893
+ typeHints: { [MISSING_ID]: ServiceType.RealUserMonitor },
1894
+ });
1895
+ expect(getListMock).not.toHaveBeenCalled();
1896
+ });
1897
+ });
1898
+
1899
+ describe("TelemetryEntityNameResolver — expired hits", () => {
1900
+ /*
1901
+ * An expired hit already says which table the id lives in, so its
1902
+ * refresh goes straight there instead of probing Services and then every
1903
+ * other table again.
1904
+ */
1905
+
1906
+ test("an expired RUM hit is refreshed with exactly one RumApplication request and no Service request", async () => {
1907
+ const nowSpy: NowSpy = freezeNow(START_TIME);
1908
+ await TelemetryEntityNameResolver.resolve({
1909
+ ids: [RUM_ID],
1910
+ projectId: PROJECT_ID,
1911
+ });
1912
+ getListMock.mockClear();
1913
+ tables[ServiceType.RealUserMonitor] = [
1914
+ row(RUM_ID, { name: "checkout-web-v2" }),
1915
+ ];
1916
+
1917
+ // Still fresh: served from cache with the old name.
1918
+ nowSpy.mockReturnValue(START_TIME + HIT_CACHE_TTL_IN_MS - 1);
1919
+ const fresh: TelemetryEntityNameMap =
1920
+ await TelemetryEntityNameResolver.resolve({
1921
+ ids: [RUM_ID],
1922
+ projectId: PROJECT_ID,
1923
+ });
1924
+ expect(fresh[RUM_ID]?.name).toBe("checkout-web");
1925
+ expect(getListMock).not.toHaveBeenCalled();
1926
+
1927
+ nowSpy.mockReturnValue(START_TIME + HIT_CACHE_TTL_IN_MS + 1);
1928
+ const refreshed: TelemetryEntityNameMap =
1929
+ await TelemetryEntityNameResolver.resolve({
1930
+ ids: [RUM_ID],
1931
+ projectId: PROJECT_ID,
1932
+ });
1933
+ expect(refreshed[RUM_ID]).toEqual({
1934
+ id: RUM_ID,
1935
+ name: "checkout-web-v2",
1936
+ entityType: ServiceType.RealUserMonitor,
1937
+ typeLabel: "RUM Application",
1938
+ });
1939
+ expect(issuedRequests()).toEqual([
1940
+ { entityType: ServiceType.RealUserMonitor, ids: [RUM_ID] },
1941
+ ]);
1942
+ });
1943
+
1944
+ test("expired hits in several tables refresh in one request per table, with no fan-out", async () => {
1945
+ const nowSpy: NowSpy = freezeNow(START_TIME);
1946
+ await TelemetryEntityNameResolver.resolve({
1947
+ ids: [SERVICE_ID, RUM_ID, HOST_ID],
1948
+ projectId: PROJECT_ID,
1949
+ });
1950
+ getListMock.mockClear();
1951
+
1952
+ nowSpy.mockReturnValue(START_TIME + HIT_CACHE_TTL_IN_MS + 1);
1953
+ const result: TelemetryEntityNameMap =
1954
+ await TelemetryEntityNameResolver.resolve({
1955
+ ids: [SERVICE_ID, RUM_ID, HOST_ID],
1956
+ projectId: PROJECT_ID,
1957
+ });
1958
+ expect(Object.keys(result).sort()).toEqual(
1959
+ [SERVICE_ID, RUM_ID, HOST_ID].sort(),
1960
+ );
1961
+ const requests: Array<IssuedRequest> = issuedRequests();
1962
+ expect(requests).toHaveLength(3);
1963
+ expect(requests).toContainEqual({
1964
+ entityType: ServiceType.OpenTelemetry,
1965
+ ids: [SERVICE_ID],
1966
+ });
1967
+ expect(requests).toContainEqual({
1968
+ entityType: ServiceType.RealUserMonitor,
1969
+ ids: [RUM_ID],
1970
+ });
1971
+ expect(requests).toContainEqual({
1972
+ entityType: ServiceType.Host,
1973
+ ids: [HOST_ID],
1974
+ });
1975
+ });
1976
+
1977
+ test("an expired hit whose refresh misses falls through to the general pass without asking its table again", async () => {
1978
+ const nowSpy: NowSpy = freezeNow(START_TIME);
1979
+ await TelemetryEntityNameResolver.resolve({
1980
+ ids: [RUM_ID],
1981
+ projectId: PROJECT_ID,
1982
+ });
1983
+ getListMock.mockClear();
1984
+ // The RUM application is gone from its table.
1985
+ tables[ServiceType.RealUserMonitor] = [];
1986
+
1987
+ nowSpy.mockReturnValue(START_TIME + HIT_CACHE_TTL_IN_MS + 1);
1988
+ await expect(
1989
+ TelemetryEntityNameResolver.resolve({
1990
+ ids: [RUM_ID],
1991
+ projectId: PROJECT_ID,
1992
+ }),
1993
+ ).resolves.toEqual({});
1994
+
1995
+ const types: Array<ServiceType> = requestedTypes();
1996
+ expect(types[0]).toBe(ServiceType.RealUserMonitor);
1997
+ expect(types[1]).toBe(ServiceType.OpenTelemetry);
1998
+ expect([...types].sort()).toEqual(
1999
+ [...TELEMETRY_ENTITY_RESOLUTION_ORDER].sort(),
2000
+ );
2001
+ expect(requestsFor(ServiceType.RealUserMonitor)).toHaveLength(1);
2002
+
2003
+ // …and that full miss is now remembered.
2004
+ getListMock.mockClear();
2005
+ await TelemetryEntityNameResolver.resolve({
2006
+ ids: [RUM_ID],
2007
+ projectId: PROJECT_ID,
2008
+ });
2009
+ expect(getListMock).not.toHaveBeenCalled();
2010
+ });
2011
+
2012
+ test("an expired hit whose refresh misses can still resolve in another table", async () => {
2013
+ const nowSpy: NowSpy = freezeNow(START_TIME);
2014
+ await TelemetryEntityNameResolver.resolve({
2015
+ ids: [RUM_ID],
2016
+ projectId: PROJECT_ID,
2017
+ });
2018
+ tables[ServiceType.RealUserMonitor] = [];
2019
+ tables[ServiceType.Host]!.push(row(RUM_ID, { name: "now-a-host" }));
2020
+
2021
+ nowSpy.mockReturnValue(START_TIME + HIT_CACHE_TTL_IN_MS + 1);
2022
+ const result: TelemetryEntityNameMap =
2023
+ await TelemetryEntityNameResolver.resolve({
2024
+ ids: [RUM_ID],
2025
+ projectId: PROJECT_ID,
2026
+ });
2027
+ expect(result[RUM_ID]).toEqual({
2028
+ id: RUM_ID,
2029
+ name: "now-a-host",
2030
+ entityType: ServiceType.Host,
2031
+ typeLabel: "Host",
2032
+ });
2033
+ });
2034
+
2035
+ test("an expired hit of a type the caller does not allow is not used as a hint", async () => {
2036
+ const nowSpy: NowSpy = freezeNow(START_TIME);
2037
+ await TelemetryEntityNameResolver.resolve({
2038
+ ids: [RUM_ID],
2039
+ projectId: PROJECT_ID,
2040
+ });
2041
+ getListMock.mockClear();
2042
+
2043
+ nowSpy.mockReturnValue(START_TIME + HIT_CACHE_TTL_IN_MS + 1);
2044
+ await expect(
2045
+ TelemetryEntityNameResolver.resolve({
2046
+ ids: [RUM_ID],
2047
+ projectId: PROJECT_ID,
2048
+ entityTypes: [ServiceType.OpenTelemetry],
2049
+ }),
2050
+ ).resolves.toEqual({});
2051
+ expect(issuedRequests()).toEqual([
2052
+ { entityType: ServiceType.OpenTelemetry, ids: [RUM_ID] },
2053
+ ]);
2054
+ });
2055
+
2056
+ test("a caller's explicit hint wins over the expired hit's table", async () => {
2057
+ const nowSpy: NowSpy = freezeNow(START_TIME);
2058
+ await TelemetryEntityNameResolver.resolve({
2059
+ ids: [RUM_ID],
2060
+ projectId: PROJECT_ID,
2061
+ });
2062
+ getListMock.mockClear();
2063
+
2064
+ nowSpy.mockReturnValue(START_TIME + HIT_CACHE_TTL_IN_MS + 1);
2065
+ const result: TelemetryEntityNameMap =
2066
+ await TelemetryEntityNameResolver.resolve({
2067
+ ids: [RUM_ID],
2068
+ projectId: PROJECT_ID,
2069
+ typeHints: { [RUM_ID]: ServiceType.Host },
2070
+ });
2071
+ expect(requestedTypes()[0]).toBe(ServiceType.Host);
2072
+ expect(result[RUM_ID]?.name).toBe("checkout-web");
2073
+ });
2074
+ });
2075
+
2076
+ describe("TelemetryEntityNameResolver.resolve — non-UUID ids", () => {
2077
+ /*
2078
+ * Postgres rejects a malformed value in `_id IN (...)`, failing the whole
2079
+ * query. The fake below does the same, so an id such as a service *name*
2080
+ * that slipped into the id list would leave every valid id beside it
2081
+ * unresolved if the resolver sent it.
2082
+ */
2083
+ beforeEach(() => {
2084
+ getListMock.mockImplementation((...args: Array<unknown>) => {
2085
+ const getListArgs: GetListArgs = args[0] as GetListArgs;
2086
+ const invalid: Array<string> = includedIds(getListArgs.query._id).filter(
2087
+ (id: string): boolean => {
2088
+ return !ObjectID.isValidUUID(id);
2089
+ },
2090
+ );
2091
+ if (invalid.length > 0) {
2092
+ return Promise.reject(
2093
+ new Error(`invalid input syntax for type uuid: "${invalid[0]}"`),
2094
+ );
2095
+ }
2096
+ return fakeGetList(getListArgs);
2097
+ });
2098
+ });
2099
+
2100
+ test.each([
2101
+ ["a service name", "checkout"],
2102
+ ["a dashed non-uuid", "not-a-uuid"],
2103
+ ["digits", "12345"],
2104
+ ["a uuid missing a digit", "11111111-0000-4000-8000-00000000000"],
2105
+ ["a uuid without dashes", "11111111000040008000000000000001"],
2106
+ ["a braced uuid", "{11111111-0000-4000-8000-000000000001}"],
2107
+ ])("%s is dropped without any request", async (_name: string, id: string) => {
2108
+ await expect(
2109
+ TelemetryEntityNameResolver.resolve({
2110
+ ids: [id],
2111
+ projectId: PROJECT_ID,
2112
+ }),
2113
+ ).resolves.toEqual({});
2114
+ expect(getListMock).not.toHaveBeenCalled();
2115
+ expect(missCacheKeys()).toEqual([]);
2116
+ });
2117
+
2118
+ test("non-UUID ids do not poison the batch: the valid ids beside them still resolve", async () => {
2119
+ const result: TelemetryEntityNameMap =
2120
+ await TelemetryEntityNameResolver.resolve({
2121
+ ids: ["checkout", SERVICE_ID, RUM_ID, PROJECT_ID, "frontend"],
2122
+ projectId: PROJECT_ID,
2123
+ });
2124
+
2125
+ expect(Object.keys(result).sort()).toEqual(
2126
+ [SERVICE_ID, RUM_ID, PROJECT_ID].sort(),
2127
+ );
2128
+ expect(result[SERVICE_ID]?.name).toBe("checkout-api");
2129
+ expect(result[RUM_ID]?.name).toBe("checkout-web");
2130
+ expect(result[PROJECT_ID]).toEqual({
2131
+ id: PROJECT_ID,
2132
+ name: UNKNOWN_SERVICE_NAME,
2133
+ entityType: ServiceType.Unknown,
2134
+ typeLabel: "Service",
2135
+ });
2136
+
2137
+ const sentIds: Array<string> = issuedRequests().flatMap(
2138
+ (request: IssuedRequest): Array<string> => {
2139
+ return request.ids;
2140
+ },
2141
+ );
2142
+ expect(sentIds.length).toBeGreaterThan(0);
2143
+ expect(sentIds).not.toContain("checkout");
2144
+ expect(sentIds).not.toContain("frontend");
2145
+ expect(sentIds).not.toContain(PROJECT_ID);
2146
+ });
2147
+
2148
+ test("a non-UUID id with a type hint is dropped too", async () => {
2149
+ await expect(
2150
+ TelemetryEntityNameResolver.resolve({
2151
+ ids: ["checkout"],
2152
+ projectId: PROJECT_ID,
2153
+ typeHints: { checkout: ServiceType.RealUserMonitor },
2154
+ }),
2155
+ ).resolves.toEqual({});
2156
+ expect(getListMock).not.toHaveBeenCalled();
2157
+ });
2158
+
2159
+ test("a hinted valid id still resolves in one request next to a dropped one", async () => {
2160
+ const result: TelemetryEntityNameMap =
2161
+ await TelemetryEntityNameResolver.resolve({
2162
+ ids: ["checkout", RUM_ID],
2163
+ projectId: PROJECT_ID,
2164
+ typeHints: {
2165
+ checkout: ServiceType.RealUserMonitor,
2166
+ [RUM_ID]: ServiceType.RealUserMonitor,
2167
+ },
2168
+ });
2169
+ expect(Object.keys(result)).toEqual([RUM_ID]);
2170
+ expect(issuedRequests()).toEqual([
2171
+ { entityType: ServiceType.RealUserMonitor, ids: [RUM_ID] },
2172
+ ]);
2173
+ });
2174
+
2175
+ test("an upper-case UUID is still a valid id and is queried", async () => {
2176
+ await TelemetryEntityNameResolver.resolve({
2177
+ ids: [SERVICE_ID.toUpperCase()],
2178
+ projectId: PROJECT_ID,
2179
+ entityTypes: [ServiceType.OpenTelemetry],
2180
+ });
2181
+ expect(issuedRequests()).toEqual([
2182
+ {
2183
+ entityType: ServiceType.OpenTelemetry,
2184
+ ids: [SERVICE_ID.toUpperCase()],
2185
+ },
2186
+ ]);
2187
+ });
2188
+
2189
+ test("the projectId still maps to Unknown Service even when it is not UUID-shaped", async () => {
2190
+ const result: TelemetryEntityNameMap =
2191
+ await TelemetryEntityNameResolver.resolve({
2192
+ ids: ["project-slug", "checkout"],
2193
+ projectId: "project-slug",
2194
+ });
2195
+ expect(result).toEqual({
2196
+ "project-slug": {
2197
+ id: "project-slug",
2198
+ name: UNKNOWN_SERVICE_NAME,
2199
+ entityType: ServiceType.Unknown,
2200
+ typeLabel: "Service",
2201
+ },
2202
+ });
2203
+ expect(getListMock).not.toHaveBeenCalled();
2204
+ });
2205
+ });