@oneuptime/common 11.5.9 → 11.5.10

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 (376) hide show
  1. package/Models/AnalyticsModels/Index.ts +13 -0
  2. package/Models/AnalyticsModels/MetricItemAggMV1mByContainer.ts +178 -0
  3. package/Models/AnalyticsModels/MetricItemAggMV1mByK8sCluster.ts +179 -0
  4. package/Models/AnalyticsModels/MetricItemAggMV1mByService.ts +179 -0
  5. package/Models/AnalyticsModels/NetworkFlow.ts +397 -0
  6. package/Models/DatabaseModels/EnterpriseLicenseInstance.ts +18 -0
  7. package/Models/DatabaseModels/GlobalConfig.ts +36 -0
  8. package/Models/DatabaseModels/Index.ts +2 -0
  9. package/Models/DatabaseModels/MarketingConversion.ts +198 -0
  10. package/Models/DatabaseModels/MetricSavedView.ts +34 -0
  11. package/Models/DatabaseModels/MetricType.ts +74 -0
  12. package/Models/DatabaseModels/NetworkDevice.ts +332 -0
  13. package/Models/DatabaseModels/NetworkDeviceDiscoveryScan.ts +113 -0
  14. package/Models/DatabaseModels/NetworkInterface.ts +58 -0
  15. package/Models/DatabaseModels/Project.ts +37 -0
  16. package/Models/DatabaseModels/User.ts +37 -0
  17. package/Server/API/BaseAnalyticsAPI.ts +23 -0
  18. package/Server/API/EnterpriseLicenseAPI.ts +14 -0
  19. package/Server/EnvironmentConfig.ts +84 -0
  20. package/Server/Infrastructure/Postgres/SchemaMigrations/1784191414522-AddCounterSemanticsToMetricType.ts +38 -0
  21. package/Server/Infrastructure/Postgres/SchemaMigrations/1784191414600-AddViewTypeToMetricSavedView.ts +28 -0
  22. package/Server/Infrastructure/Postgres/SchemaMigrations/1784211212164-AddNetworkDeviceInventoryAndDiscoverySchedule.ts +108 -0
  23. package/Server/Infrastructure/Postgres/SchemaMigrations/1784218257664-AddEnterpriseLicenseNotificationColumns.ts +31 -0
  24. package/Server/Infrastructure/Postgres/SchemaMigrations/1784293516000-AddAttributionColumnsToUserAndProject.ts +29 -0
  25. package/Server/Infrastructure/Postgres/SchemaMigrations/1784298000000-AddMarketingConversionTable.ts +35 -0
  26. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +12 -0
  27. package/Server/Services/AnalyticsDatabaseService.ts +70 -1
  28. package/Server/Services/IncidentService.ts +13 -0
  29. package/Server/Services/Index.ts +10 -0
  30. package/Server/Services/MarketingConversionService.ts +10 -0
  31. package/Server/Services/MetricItemAggMV1mByContainerService.ts +31 -0
  32. package/Server/Services/MetricItemAggMV1mByK8sClusterService.ts +31 -0
  33. package/Server/Services/MetricItemAggMV1mByServiceService.ts +31 -0
  34. package/Server/Services/MetricService.ts +970 -122
  35. package/Server/Services/MonitorService.ts +14 -0
  36. package/Server/Services/NetworkDeviceOwnerTeamService.ts +42 -0
  37. package/Server/Services/NetworkDeviceOwnerUserService.ts +106 -0
  38. package/Server/Services/NetworkFlowService.ts +11 -0
  39. package/Server/Services/OnCallDutyPolicyService.ts +11 -1
  40. package/Server/Services/ProjectService.ts +136 -0
  41. package/Server/Services/SpanService.ts +13 -9
  42. package/Server/Services/StatusPageService.ts +10 -0
  43. package/Server/Services/TeamMemberService.ts +11 -0
  44. package/Server/Services/UserService.ts +59 -0
  45. package/Server/Types/AnalyticsDatabase/AggregateBy.ts +37 -7
  46. package/Server/Utils/Attribution.ts +99 -0
  47. package/Server/Utils/Marketing/ConversionUploadProvider.ts +103 -0
  48. package/Server/Utils/Marketing/ConversionUploadProviders.ts +22 -0
  49. package/Server/Utils/Marketing/Providers/GoogleAds.ts +270 -0
  50. package/Server/Utils/Marketing/Providers/LinkedIn.ts +147 -0
  51. package/Server/Utils/Marketing/Providers/Meta.ts +148 -0
  52. package/Server/Utils/Marketing/Providers/MicrosoftAds.ts +196 -0
  53. package/Server/Utils/Marketing/Providers/Reddit.ts +178 -0
  54. package/Server/Utils/Monitor/MonitorAlert.ts +71 -7
  55. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +31 -19
  56. package/Server/Utils/Monitor/MonitorIncident.ts +71 -7
  57. package/Server/Utils/Monitor/MonitorMetricUtil.ts +57 -0
  58. package/Server/Utils/Monitor/MonitorTemplateUtil.ts +94 -0
  59. package/Server/Utils/Monitor/NetworkDeviceHydrationUtil.ts +15 -0
  60. package/Server/Utils/Monitor/NetworkInventoryUtil.ts +125 -14
  61. package/Server/Utils/ProductAnalytics.ts +133 -0
  62. package/Server/Utils/Telemetry/Telemetry.ts +39 -0
  63. package/Server/Utils/Telemetry/TelemetryEntity.ts +21 -0
  64. package/Tests/Server/API/BaseAnalyticsAPI.test.ts +72 -0
  65. package/Tests/Server/Infrastructure/SemaphorePermit.test.ts +9 -1
  66. package/Tests/Server/Services/AnalyticsDatabaseService.test.ts +21 -2
  67. package/Tests/Server/Services/MetricEntityMVKeyParity.test.ts +173 -0
  68. package/Tests/Server/Services/MetricServiceAggregate.test.ts +1391 -0
  69. package/Tests/Server/Services/NetworkDeviceOwnerServices.test.ts +260 -0
  70. package/Tests/Server/Utils/Monitor/MonitorTemplateUtilNetworkDevice.test.ts +389 -0
  71. package/Tests/Server/Utils/Monitor/NetworkInventoryUtil.test.ts +764 -0
  72. package/Tests/Types/BaseDatabase/AggregationIntervalUtil.test.ts +111 -2
  73. package/Tests/Types/Database/EndsWith.test.ts +64 -0
  74. package/Tests/Types/Database/GreaterThan.test.ts +62 -0
  75. package/Tests/Types/Database/GreaterThanOrEqual.test.ts +65 -0
  76. package/Tests/Types/Database/GreaterThanOrNull.test.ts +63 -0
  77. package/Tests/Types/Database/Includes.test.ts +65 -0
  78. package/Tests/Types/Database/IncludesAll.test.ts +65 -0
  79. package/Tests/Types/Database/IncludesNone.test.ts +65 -0
  80. package/Tests/Types/Database/IsNull.test.ts +44 -0
  81. package/Tests/Types/Database/LessThan.test.ts +62 -0
  82. package/Tests/Types/Database/LessThanOrEqual.test.ts +62 -0
  83. package/Tests/Types/Database/LessThanOrNull.test.ts +62 -0
  84. package/Tests/Types/Database/MultiSearch.test.ts +81 -0
  85. package/Tests/Types/Database/NotContains.test.ts +64 -0
  86. package/Tests/Types/Database/NotNull.test.ts +44 -0
  87. package/Tests/Types/Database/StartsWith.test.ts +64 -0
  88. package/Tests/Types/Monitor/MonitorStepSqlMonitor.test.ts +83 -1
  89. package/Tests/Types/Monitor/SnmpV3EnumParsing.test.ts +247 -0
  90. package/Tests/Types/Monitor/SnmpVendorTemplate.test.ts +263 -0
  91. package/Tests/UI/Components/XAxis.test.ts +5 -3
  92. package/Tests/Utils/EnterpriseLicenseUsage.test.ts +76 -0
  93. package/Tests/Utils/Metrics/MetricExplorerUrl.test.ts +622 -0
  94. package/Tests/Utils/Metrics/MetricFormulaEvaluator.test.ts +474 -0
  95. package/Tests/Utils/Telemetry/HeartbeatAvailability.test.ts +52 -4
  96. package/Types/AnalyticsDatabase/AnalyticsTableName.ts +11 -0
  97. package/Types/BaseDatabase/AggregateBy.ts +31 -2
  98. package/Types/BaseDatabase/AggregatedResult.ts +22 -0
  99. package/Types/BaseDatabase/AggregationInterval.ts +11 -0
  100. package/Types/BaseDatabase/AggregationIntervalUtil.ts +30 -5
  101. package/Types/Dashboard/DashboardComponents/DashboardTraceChartComponent.ts +16 -0
  102. package/Types/Email/EmailTemplateType.ts +3 -0
  103. package/Types/Marketing/MarketingConversion.ts +11 -0
  104. package/Types/Metrics/MetricQueryConfigData.ts +21 -0
  105. package/Types/Metrics/MetricQueryData.ts +8 -0
  106. package/Types/Metrics/MetricViewData.ts +8 -0
  107. package/Types/Monitor/MonitorMetricType.ts +8 -0
  108. package/Types/Monitor/MonitorStep.ts +9 -0
  109. package/Types/Monitor/MonitorStepSqlMonitor.ts +12 -0
  110. package/Types/Monitor/MonitorType.ts +2 -1
  111. package/Types/Monitor/SSLMonitor/SslMonitorResponse.ts +17 -12
  112. package/Types/Monitor/SnmpMonitor/CdpNeighbor.ts +13 -0
  113. package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +37 -3
  114. package/Types/Monitor/SnmpMonitor/SnmpAuthProtocol.ts +46 -0
  115. package/Types/Monitor/SnmpMonitor/SnmpEntityInfo.ts +15 -0
  116. package/Types/Monitor/SnmpMonitor/SnmpInterface.ts +8 -0
  117. package/Types/Monitor/SnmpMonitor/SnmpMonitorResponse.ts +17 -8
  118. package/Types/Monitor/SnmpMonitor/SnmpPrivProtocol.ts +53 -0
  119. package/Types/Monitor/SnmpMonitor/SnmpSecurityLevel.ts +43 -0
  120. package/Types/Monitor/SnmpMonitor/SnmpSystemInfo.ts +16 -0
  121. package/Types/Monitor/SnmpMonitor/SnmpVendorTemplate.ts +119 -3
  122. package/Types/NetFlow/NetworkFlowRecord.ts +25 -0
  123. package/Types/Syslog/SyslogMessage.ts +18 -0
  124. package/Types/Telemetry/TelemetrySavedViewState.ts +9 -0
  125. package/Types/Telemetry/TelemetrySavedViewType.ts +12 -0
  126. package/UI/Components/Charts/Area/AreaChart.tsx +51 -0
  127. package/UI/Components/Charts/Bar/BarChart.tsx +51 -1
  128. package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +97 -26
  129. package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +328 -1
  130. package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +115 -0
  131. package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +328 -1
  132. package/UI/Components/Charts/ChartLibrary/Types/ChartDataPoint.ts +7 -0
  133. package/UI/Components/Charts/ChartLibrary/Types/FormattedReferenceRegion.ts +15 -0
  134. package/UI/Components/Charts/ChartLibrary/Types/FormattedTimeReferenceLine.ts +13 -0
  135. package/UI/Components/Charts/Line/LineChart.tsx +51 -0
  136. package/UI/Components/Charts/Types/ReferenceRegionProps.ts +12 -0
  137. package/UI/Components/Charts/Types/TimeReferenceLineProps.ts +12 -0
  138. package/UI/Components/Charts/Types/XAxis/XAxisPrecision.ts +1 -0
  139. package/UI/Components/Charts/Utils/DataPoint.ts +0 -0
  140. package/UI/Components/Charts/Utils/TimeAnnotation.ts +169 -0
  141. package/UI/Components/Charts/Utils/XAxis.ts +28 -4
  142. package/UI/Components/Icon/Icon.tsx +33 -13
  143. package/UI/Components/Markdown.tsx/MarkdownViewer.tsx +9 -0
  144. package/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.ts +67 -0
  145. package/UI/Components/Page/ModelPage.tsx +6 -1
  146. package/UI/Components/TelemetryViewer/components/SavedViewsDropdown.tsx +33 -4
  147. package/UI/Styles/Theme.css +10 -0
  148. package/UI/Utils/User.ts +41 -2
  149. package/Utils/Analytics.ts +8 -6
  150. package/Utils/EnterpriseLicense/EnterpriseLicenseUsage.ts +74 -0
  151. package/Utils/Metrics/MetricExplorerUrl.ts +771 -0
  152. package/Utils/Metrics/MetricFormulaEvaluator.ts +410 -46
  153. package/Utils/Monitor/NetworkTopologyUtil.ts +261 -49
  154. package/Utils/ValueFormatter.ts +22 -1
  155. package/build/dist/Models/AnalyticsModels/Index.js +13 -0
  156. package/build/dist/Models/AnalyticsModels/Index.js.map +1 -1
  157. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByContainer.js +159 -0
  158. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByContainer.js.map +1 -0
  159. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByK8sCluster.js +158 -0
  160. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByK8sCluster.js.map +1 -0
  161. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByService.js +160 -0
  162. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByService.js.map +1 -0
  163. package/build/dist/Models/AnalyticsModels/NetworkFlow.js +351 -0
  164. package/build/dist/Models/AnalyticsModels/NetworkFlow.js.map +1 -0
  165. package/build/dist/Models/DatabaseModels/EnterpriseLicenseInstance.js +19 -0
  166. package/build/dist/Models/DatabaseModels/EnterpriseLicenseInstance.js.map +1 -1
  167. package/build/dist/Models/DatabaseModels/GlobalConfig.js +38 -0
  168. package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
  169. package/build/dist/Models/DatabaseModels/Index.js +2 -0
  170. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  171. package/build/dist/Models/DatabaseModels/MarketingConversion.js +219 -0
  172. package/build/dist/Models/DatabaseModels/MarketingConversion.js.map +1 -0
  173. package/build/dist/Models/DatabaseModels/MetricSavedView.js +35 -0
  174. package/build/dist/Models/DatabaseModels/MetricSavedView.js.map +1 -1
  175. package/build/dist/Models/DatabaseModels/MetricType.js +76 -0
  176. package/build/dist/Models/DatabaseModels/MetricType.js.map +1 -1
  177. package/build/dist/Models/DatabaseModels/NetworkDevice.js +341 -0
  178. package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
  179. package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js +116 -0
  180. package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js.map +1 -1
  181. package/build/dist/Models/DatabaseModels/NetworkInterface.js +60 -0
  182. package/build/dist/Models/DatabaseModels/NetworkInterface.js.map +1 -1
  183. package/build/dist/Models/DatabaseModels/Project.js +38 -0
  184. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  185. package/build/dist/Models/DatabaseModels/User.js +38 -0
  186. package/build/dist/Models/DatabaseModels/User.js.map +1 -1
  187. package/build/dist/Server/API/BaseAnalyticsAPI.js +19 -0
  188. package/build/dist/Server/API/BaseAnalyticsAPI.js.map +1 -1
  189. package/build/dist/Server/API/EnterpriseLicenseAPI.js +8 -0
  190. package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +1 -1
  191. package/build/dist/Server/EnvironmentConfig.js +51 -0
  192. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  193. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784191414522-AddCounterSemanticsToMetricType.js +27 -0
  194. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784191414522-AddCounterSemanticsToMetricType.js.map +1 -0
  195. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784191414600-AddViewTypeToMetricSavedView.js +21 -0
  196. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784191414600-AddViewTypeToMetricSavedView.js.map +1 -0
  197. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784211212164-AddNetworkDeviceInventoryAndDiscoverySchedule.js +47 -0
  198. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784211212164-AddNetworkDeviceInventoryAndDiscoverySchedule.js.map +1 -0
  199. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784218257664-AddEnterpriseLicenseNotificationColumns.js +16 -0
  200. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784218257664-AddEnterpriseLicenseNotificationColumns.js.map +1 -0
  201. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784293516000-AddAttributionColumnsToUserAndProject.js +18 -0
  202. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784293516000-AddAttributionColumnsToUserAndProject.js.map +1 -0
  203. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784298000000-AddMarketingConversionTable.js +20 -0
  204. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784298000000-AddMarketingConversionTable.js.map +1 -0
  205. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +12 -0
  206. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  207. package/build/dist/Server/Services/AnalyticsDatabaseService.js +58 -4
  208. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  209. package/build/dist/Server/Services/IncidentService.js +12 -0
  210. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  211. package/build/dist/Server/Services/Index.js +10 -0
  212. package/build/dist/Server/Services/Index.js.map +1 -1
  213. package/build/dist/Server/Services/MarketingConversionService.js +9 -0
  214. package/build/dist/Server/Services/MarketingConversionService.js.map +1 -0
  215. package/build/dist/Server/Services/MetricItemAggMV1mByContainerService.js +29 -0
  216. package/build/dist/Server/Services/MetricItemAggMV1mByContainerService.js.map +1 -0
  217. package/build/dist/Server/Services/MetricItemAggMV1mByK8sClusterService.js +29 -0
  218. package/build/dist/Server/Services/MetricItemAggMV1mByK8sClusterService.js.map +1 -0
  219. package/build/dist/Server/Services/MetricItemAggMV1mByServiceService.js +29 -0
  220. package/build/dist/Server/Services/MetricItemAggMV1mByServiceService.js.map +1 -0
  221. package/build/dist/Server/Services/MetricService.js +761 -125
  222. package/build/dist/Server/Services/MetricService.js.map +1 -1
  223. package/build/dist/Server/Services/MonitorService.js +18 -5
  224. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  225. package/build/dist/Server/Services/NetworkDeviceOwnerTeamService.js +44 -0
  226. package/build/dist/Server/Services/NetworkDeviceOwnerTeamService.js.map +1 -1
  227. package/build/dist/Server/Services/NetworkDeviceOwnerUserService.js +88 -0
  228. package/build/dist/Server/Services/NetworkDeviceOwnerUserService.js.map +1 -1
  229. package/build/dist/Server/Services/NetworkFlowService.js +9 -0
  230. package/build/dist/Server/Services/NetworkFlowService.js.map +1 -0
  231. package/build/dist/Server/Services/OnCallDutyPolicyService.js +15 -6
  232. package/build/dist/Server/Services/OnCallDutyPolicyService.js.map +1 -1
  233. package/build/dist/Server/Services/ProjectService.js +107 -6
  234. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  235. package/build/dist/Server/Services/SpanService.js +8 -6
  236. package/build/dist/Server/Services/SpanService.js.map +1 -1
  237. package/build/dist/Server/Services/StatusPageService.js +10 -0
  238. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  239. package/build/dist/Server/Services/TeamMemberService.js +11 -0
  240. package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
  241. package/build/dist/Server/Services/UserService.js +52 -0
  242. package/build/dist/Server/Services/UserService.js.map +1 -1
  243. package/build/dist/Server/Types/AnalyticsDatabase/AggregateBy.js +33 -7
  244. package/build/dist/Server/Types/AnalyticsDatabase/AggregateBy.js.map +1 -1
  245. package/build/dist/Server/Utils/Attribution.js +74 -0
  246. package/build/dist/Server/Utils/Attribution.js.map +1 -0
  247. package/build/dist/Server/Utils/Marketing/ConversionUploadProvider.js +40 -0
  248. package/build/dist/Server/Utils/Marketing/ConversionUploadProvider.js.map +1 -0
  249. package/build/dist/Server/Utils/Marketing/ConversionUploadProviders.js +20 -0
  250. package/build/dist/Server/Utils/Marketing/ConversionUploadProviders.js.map +1 -0
  251. package/build/dist/Server/Utils/Marketing/Providers/GoogleAds.js +177 -0
  252. package/build/dist/Server/Utils/Marketing/Providers/GoogleAds.js.map +1 -0
  253. package/build/dist/Server/Utils/Marketing/Providers/LinkedIn.js +113 -0
  254. package/build/dist/Server/Utils/Marketing/Providers/LinkedIn.js.map +1 -0
  255. package/build/dist/Server/Utils/Marketing/Providers/Meta.js +109 -0
  256. package/build/dist/Server/Utils/Marketing/Providers/Meta.js.map +1 -0
  257. package/build/dist/Server/Utils/Marketing/Providers/MicrosoftAds.js +130 -0
  258. package/build/dist/Server/Utils/Marketing/Providers/MicrosoftAds.js.map +1 -0
  259. package/build/dist/Server/Utils/Marketing/Providers/Reddit.js +126 -0
  260. package/build/dist/Server/Utils/Marketing/Providers/Reddit.js.map +1 -0
  261. package/build/dist/Server/Utils/Monitor/MonitorAlert.js +53 -9
  262. package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
  263. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +21 -10
  264. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  265. package/build/dist/Server/Utils/Monitor/MonitorIncident.js +54 -11
  266. package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
  267. package/build/dist/Server/Utils/Monitor/MonitorMetricUtil.js +43 -3
  268. package/build/dist/Server/Utils/Monitor/MonitorMetricUtil.js.map +1 -1
  269. package/build/dist/Server/Utils/Monitor/MonitorTemplateUtil.js +68 -0
  270. package/build/dist/Server/Utils/Monitor/MonitorTemplateUtil.js.map +1 -1
  271. package/build/dist/Server/Utils/Monitor/NetworkDeviceHydrationUtil.js +15 -0
  272. package/build/dist/Server/Utils/Monitor/NetworkDeviceHydrationUtil.js.map +1 -1
  273. package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js +100 -15
  274. package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js.map +1 -1
  275. package/build/dist/Server/Utils/ProductAnalytics.js +111 -0
  276. package/build/dist/Server/Utils/ProductAnalytics.js.map +1 -0
  277. package/build/dist/Server/Utils/Telemetry/Telemetry.js +32 -5
  278. package/build/dist/Server/Utils/Telemetry/Telemetry.js.map +1 -1
  279. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js +17 -0
  280. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js.map +1 -1
  281. package/build/dist/Types/AnalyticsDatabase/AnalyticsTableName.js +11 -0
  282. package/build/dist/Types/AnalyticsDatabase/AnalyticsTableName.js.map +1 -1
  283. package/build/dist/Types/BaseDatabase/AggregationInterval.js +11 -0
  284. package/build/dist/Types/BaseDatabase/AggregationInterval.js.map +1 -1
  285. package/build/dist/Types/BaseDatabase/AggregationIntervalUtil.js +33 -5
  286. package/build/dist/Types/BaseDatabase/AggregationIntervalUtil.js.map +1 -1
  287. package/build/dist/Types/Email/EmailTemplateType.js +2 -0
  288. package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
  289. package/build/dist/Types/Marketing/MarketingConversion.js +13 -0
  290. package/build/dist/Types/Marketing/MarketingConversion.js.map +1 -0
  291. package/build/dist/Types/Monitor/MonitorMetricType.js +7 -0
  292. package/build/dist/Types/Monitor/MonitorMetricType.js.map +1 -1
  293. package/build/dist/Types/Monitor/MonitorStep.js +6 -0
  294. package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
  295. package/build/dist/Types/Monitor/MonitorStepSqlMonitor.js +3 -0
  296. package/build/dist/Types/Monitor/MonitorStepSqlMonitor.js.map +1 -1
  297. package/build/dist/Types/Monitor/MonitorType.js +2 -1
  298. package/build/dist/Types/Monitor/MonitorType.js.map +1 -1
  299. package/build/dist/Types/Monitor/SnmpMonitor/CdpNeighbor.js +2 -0
  300. package/build/dist/Types/Monitor/SnmpMonitor/CdpNeighbor.js.map +1 -0
  301. package/build/dist/Types/Monitor/SnmpMonitor/SnmpAuthProtocol.js +41 -0
  302. package/build/dist/Types/Monitor/SnmpMonitor/SnmpAuthProtocol.js.map +1 -1
  303. package/build/dist/Types/Monitor/SnmpMonitor/SnmpEntityInfo.js +2 -0
  304. package/build/dist/Types/Monitor/SnmpMonitor/SnmpEntityInfo.js.map +1 -0
  305. package/build/dist/Types/Monitor/SnmpMonitor/SnmpPrivProtocol.js +48 -0
  306. package/build/dist/Types/Monitor/SnmpMonitor/SnmpPrivProtocol.js.map +1 -1
  307. package/build/dist/Types/Monitor/SnmpMonitor/SnmpSecurityLevel.js +38 -0
  308. package/build/dist/Types/Monitor/SnmpMonitor/SnmpSecurityLevel.js.map +1 -1
  309. package/build/dist/Types/Monitor/SnmpMonitor/SnmpSystemInfo.js +2 -0
  310. package/build/dist/Types/Monitor/SnmpMonitor/SnmpSystemInfo.js.map +1 -0
  311. package/build/dist/Types/Monitor/SnmpMonitor/SnmpVendorTemplate.js +96 -3
  312. package/build/dist/Types/Monitor/SnmpMonitor/SnmpVendorTemplate.js.map +1 -1
  313. package/build/dist/Types/NetFlow/NetworkFlowRecord.js +2 -0
  314. package/build/dist/Types/NetFlow/NetworkFlowRecord.js.map +1 -0
  315. package/build/dist/Types/Syslog/SyslogMessage.js +2 -0
  316. package/build/dist/Types/Syslog/SyslogMessage.js.map +1 -0
  317. package/build/dist/Types/Telemetry/TelemetrySavedViewState.js +0 -7
  318. package/build/dist/Types/Telemetry/TelemetrySavedViewState.js.map +1 -1
  319. package/build/dist/Types/Telemetry/TelemetrySavedViewType.js +13 -0
  320. package/build/dist/Types/Telemetry/TelemetrySavedViewType.js.map +1 -0
  321. package/build/dist/UI/Components/Charts/Area/AreaChart.js +25 -1
  322. package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
  323. package/build/dist/UI/Components/Charts/Bar/BarChart.js +26 -2
  324. package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
  325. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +36 -12
  326. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
  327. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js +208 -6
  328. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
  329. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +52 -3
  330. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
  331. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +208 -6
  332. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
  333. package/build/dist/UI/Components/Charts/ChartLibrary/Types/ChartDataPoint.js +6 -1
  334. package/build/dist/UI/Components/Charts/ChartLibrary/Types/ChartDataPoint.js.map +1 -1
  335. package/build/dist/UI/Components/Charts/ChartLibrary/Types/FormattedReferenceRegion.js +2 -0
  336. package/build/dist/UI/Components/Charts/ChartLibrary/Types/FormattedReferenceRegion.js.map +1 -0
  337. package/build/dist/UI/Components/Charts/ChartLibrary/Types/FormattedTimeReferenceLine.js +2 -0
  338. package/build/dist/UI/Components/Charts/ChartLibrary/Types/FormattedTimeReferenceLine.js.map +1 -0
  339. package/build/dist/UI/Components/Charts/Line/LineChart.js +25 -1
  340. package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
  341. package/build/dist/UI/Components/Charts/Types/ReferenceRegionProps.js +2 -0
  342. package/build/dist/UI/Components/Charts/Types/ReferenceRegionProps.js.map +1 -0
  343. package/build/dist/UI/Components/Charts/Types/TimeReferenceLineProps.js +2 -0
  344. package/build/dist/UI/Components/Charts/Types/TimeReferenceLineProps.js.map +1 -0
  345. package/build/dist/UI/Components/Charts/Types/XAxis/XAxisPrecision.js +1 -0
  346. package/build/dist/UI/Components/Charts/Types/XAxis/XAxisPrecision.js.map +1 -1
  347. package/build/dist/UI/Components/Charts/Utils/DataPoint.js +0 -0
  348. package/build/dist/UI/Components/Charts/Utils/DataPoint.js.map +1 -1
  349. package/build/dist/UI/Components/Charts/Utils/TimeAnnotation.js +121 -0
  350. package/build/dist/UI/Components/Charts/Utils/TimeAnnotation.js.map +1 -0
  351. package/build/dist/UI/Components/Charts/Utils/XAxis.js +27 -4
  352. package/build/dist/UI/Components/Charts/Utils/XAxis.js.map +1 -1
  353. package/build/dist/UI/Components/Icon/Icon.js +15 -8
  354. package/build/dist/UI/Components/Icon/Icon.js.map +1 -1
  355. package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js.map +1 -1
  356. package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.js +58 -0
  357. package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.js.map +1 -1
  358. package/build/dist/UI/Components/Page/ModelPage.js +7 -1
  359. package/build/dist/UI/Components/Page/ModelPage.js.map +1 -1
  360. package/build/dist/UI/Components/TelemetryViewer/components/SavedViewsDropdown.js +10 -4
  361. package/build/dist/UI/Components/TelemetryViewer/components/SavedViewsDropdown.js.map +1 -1
  362. package/build/dist/UI/Utils/User.js +35 -2
  363. package/build/dist/UI/Utils/User.js.map +1 -1
  364. package/build/dist/Utils/Analytics.js +8 -5
  365. package/build/dist/Utils/Analytics.js.map +1 -1
  366. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseUsage.js +57 -0
  367. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseUsage.js.map +1 -1
  368. package/build/dist/Utils/Metrics/MetricExplorerUrl.js +418 -0
  369. package/build/dist/Utils/Metrics/MetricExplorerUrl.js.map +1 -0
  370. package/build/dist/Utils/Metrics/MetricFormulaEvaluator.js +296 -37
  371. package/build/dist/Utils/Metrics/MetricFormulaEvaluator.js.map +1 -1
  372. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +176 -40
  373. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
  374. package/build/dist/Utils/ValueFormatter.js +20 -1
  375. package/build/dist/Utils/ValueFormatter.js.map +1 -1
  376. package/package.json +2 -2
@@ -1,14 +1,26 @@
1
1
  import "../TestingUtils/Init";
2
+ import fs from "fs";
3
+ import path from "path";
2
4
  import { MetricService } from "../../../Server/Services/MetricService";
3
5
  import Metric from "../../../Models/AnalyticsModels/Metric";
4
6
  import AggregateBy from "../../../Server/Types/AnalyticsDatabase/AggregateBy";
5
7
  import { Statement } from "../../../Server/Utils/AnalyticsDatabase/Statement";
8
+ import AggregatedResult from "../../../Types/BaseDatabase/AggregatedResult";
6
9
  import AggregationInterval from "../../../Types/BaseDatabase/AggregationInterval";
7
10
  import AggregationType from "../../../Types/BaseDatabase/AggregationType";
8
11
  import InBetween from "../../../Types/BaseDatabase/InBetween";
9
12
  import SortOrder from "../../../Types/BaseDatabase/SortOrder";
10
13
  import BadDataException from "../../../Types/Exception/BadDataException";
11
14
  import ObjectID from "../../../Types/ObjectID";
15
+ import PositiveNumber from "../../../Types/PositiveNumber";
16
+ import {
17
+ keyForHost,
18
+ keyForKubernetesCluster,
19
+ keyForService,
20
+ } from "../../../Utils/Telemetry/EntityKey";
21
+ import { keyForContainer } from "../../../Server/Utils/Telemetry/TelemetryEntity";
22
+ import TelemetryEntityService from "../../../Server/Services/TelemetryEntityService";
23
+ import TelemetryEntity from "../../../Models/DatabaseModels/TelemetryEntity";
12
24
 
13
25
  describe("MetricService aggregate statement generation", () => {
14
26
  const projectId: ObjectID = ObjectID.generate();
@@ -358,4 +370,1383 @@ describe("MetricService aggregate statement generation", () => {
358
370
  expect(query).not.toContain("date_trunc");
359
371
  });
360
372
  });
373
+
374
+ /*
375
+ * The sub-hour tiers (FiveMinutes/FifteenMinutes/ThirtyMinutes) are NOT
376
+ * valid ClickHouse date_trunc units — they must compile through the
377
+ * shared AggregateUtil expression map to `toStartOfInterval(col,
378
+ * INTERVAL n MINUTE)`, never to `date_trunc('fiveminutes', ...)`.
379
+ */
380
+ describe("sub-hour interval tiers", () => {
381
+ const windowStart: Date = new Date("2026-07-09T00:00:00.000Z");
382
+
383
+ type BuildWindowedFunction = (
384
+ hours: number,
385
+ overrides?: Partial<AggregateBy<Metric>>,
386
+ ) => AggregateBy<Metric>;
387
+
388
+ const buildWindowed: BuildWindowedFunction = (
389
+ hours: number,
390
+ overrides?: Partial<AggregateBy<Metric>>,
391
+ ): AggregateBy<Metric> => {
392
+ const windowEnd: Date = new Date(
393
+ windowStart.getTime() + hours * 60 * 60 * 1000,
394
+ );
395
+ return buildAggregateBy({
396
+ query: {
397
+ projectId: projectId,
398
+ name: "http.client.request.duration",
399
+ time: new InBetween(windowStart, windowEnd),
400
+ } as AggregateBy<Metric>["query"],
401
+ startTimestamp: windowStart,
402
+ endTimestamp: windowEnd,
403
+ ...overrides,
404
+ });
405
+ };
406
+
407
+ it("serves a 4h window from the minute MV at 5-minute buckets", () => {
408
+ const query: string = getQuery(buildWindowed(4));
409
+
410
+ expect(query).toContain("MetricItemAggMV1m");
411
+ expect(query).toContain(
412
+ "toStartOfInterval(bucketTime, INTERVAL 5 MINUTE) as time",
413
+ );
414
+ expect(query).not.toContain("date_trunc");
415
+ });
416
+
417
+ it("serves an 18h window at 15-minute buckets and a 2d window at 30-minute buckets", () => {
418
+ const query18h: string = getQuery(buildWindowed(18));
419
+ expect(query18h).toContain(
420
+ "toStartOfInterval(bucketTime, INTERVAL 15 MINUTE) as time",
421
+ );
422
+
423
+ const query2d: string = getQuery(buildWindowed(48));
424
+ expect(query2d).toContain(
425
+ "toStartOfInterval(bucketTime, INTERVAL 30 MINUTE) as time",
426
+ );
427
+ });
428
+
429
+ it("keeps the legacy date_trunc form for windows <= 3h", () => {
430
+ const query: string = getQuery(buildWindowed(2));
431
+
432
+ expect(query).toContain(
433
+ "date_trunc('minute', toStartOfInterval(bucketTime, INTERVAL 1 minute))",
434
+ );
435
+ expect(query).not.toContain("INTERVAL 5 MINUTE");
436
+ });
437
+
438
+ it("compiles 5-minute buckets on the base scalar builder (distribution metric)", () => {
439
+ const aggregateBy: AggregateBy<Metric> = buildWindowed(4);
440
+ (
441
+ service as unknown as {
442
+ pointTypeHintByAggregate: WeakMap<AggregateBy<Metric>, string | null>;
443
+ }
444
+ ).pointTypeHintByAggregate.set(aggregateBy, "Histogram");
445
+
446
+ const query: string = getQuery(aggregateBy);
447
+
448
+ expect(query).not.toContain("MetricItemAggMV1m");
449
+ expect(query).toContain(
450
+ "toStartOfInterval(time, INTERVAL 5 MINUTE) as time",
451
+ );
452
+ expect(query).not.toContain("fiveminutes");
453
+ });
454
+
455
+ it("compiles 5-minute buckets on the percentile path", () => {
456
+ const query: string = getQuery(
457
+ buildWindowed(4, { aggregationType: AggregationType.P90 }),
458
+ );
459
+
460
+ expect(query).toContain("quantileExactWeighted(0.9)");
461
+ expect(query).toContain(
462
+ "toStartOfInterval(time, INTERVAL 5 MINUTE) as time",
463
+ );
464
+ });
465
+
466
+ it("compiles 5-minute buckets on the per-host MV path", () => {
467
+ const query: string = getQuery(
468
+ buildWindowed(4, {
469
+ query: {
470
+ projectId: projectId,
471
+ name: "http.client.request.duration",
472
+ time: new InBetween(
473
+ windowStart,
474
+ new Date(windowStart.getTime() + 4 * 60 * 60 * 1000),
475
+ ),
476
+ attributes: { "resource.host.name": "web-01" },
477
+ } as unknown as AggregateBy<Metric>["query"],
478
+ }),
479
+ );
480
+
481
+ expect(query).toContain("MetricItemAggMV1mByHostV2");
482
+ expect(query).toContain(
483
+ "toStartOfInterval(bucketTime, INTERVAL 5 MINUTE) as time",
484
+ );
485
+ });
486
+
487
+ it("honors an explicit FiveMinutes override independent of the window", () => {
488
+ // The default 5-minute window derives Minute; the override pins 5m.
489
+ const query: string = getQuery(
490
+ buildAggregateBy({
491
+ aggregationInterval: AggregationInterval.FiveMinutes,
492
+ }),
493
+ );
494
+
495
+ expect(query).toContain(
496
+ "toStartOfInterval(bucketTime, INTERVAL 5 MINUTE) as time",
497
+ );
498
+ });
499
+ });
500
+
501
+ describe("execution-time cap settings", () => {
502
+ const expectCapped: (query: string) => void = (query: string): void => {
503
+ expect(query).toContain("max_execution_time = 45");
504
+ expect(query).toContain("timeout_overflow_mode = 'break'");
505
+ };
506
+
507
+ it("caps the minute MV path", () => {
508
+ expectCapped(getQuery(buildAggregateBy()));
509
+ });
510
+
511
+ it("caps the base scalar path", () => {
512
+ expectCapped(
513
+ getQuery(
514
+ buildAggregateBy({
515
+ groupByAttributeKeys: ["host.name"],
516
+ }),
517
+ ),
518
+ );
519
+ });
520
+
521
+ it("caps the percentile path", () => {
522
+ expectCapped(
523
+ getQuery(buildAggregateBy({ aggregationType: AggregationType.P90 })),
524
+ );
525
+ });
526
+
527
+ it("caps the per-host MV path", () => {
528
+ expectCapped(
529
+ getQuery(
530
+ buildAggregateBy({
531
+ query: {
532
+ projectId: projectId,
533
+ name: "http.client.request.duration",
534
+ time: new InBetween(startDate, endDate),
535
+ attributes: { "resource.host.name": "web-01" },
536
+ } as unknown as AggregateBy<Metric>["query"],
537
+ }),
538
+ ),
539
+ );
540
+ });
541
+
542
+ /*
543
+ * Overflow-mode threading: chart callers keep the 'break' default
544
+ * (asserted by every expectCapped above), while alerting callers —
545
+ * the metric-monitor worker — pass timeoutOverflowMode: "throw" so
546
+ * a timed-out evaluation fails loudly instead of silently scoring
547
+ * partial buckets.
548
+ */
549
+ describe("timeoutOverflowMode threading", () => {
550
+ const expectThrows: (query: string) => void = (query: string): void => {
551
+ expect(query).toContain("timeout_overflow_mode = 'throw'");
552
+ expect(query).not.toContain("timeout_overflow_mode = 'break'");
553
+ };
554
+
555
+ it("threads 'throw' through the minute MV path", () => {
556
+ expectThrows(
557
+ getQuery(buildAggregateBy({ timeoutOverflowMode: "throw" })),
558
+ );
559
+ });
560
+
561
+ it("threads 'throw' through the base scalar path (grouped worker shape)", () => {
562
+ expectThrows(
563
+ getQuery(
564
+ buildAggregateBy({
565
+ groupByAttributeKeys: ["host.name"],
566
+ timeoutOverflowMode: "throw",
567
+ }),
568
+ ),
569
+ );
570
+ });
571
+
572
+ it("threads 'throw' through the percentile path", () => {
573
+ expectThrows(
574
+ getQuery(
575
+ buildAggregateBy({
576
+ aggregationType: AggregationType.P90,
577
+ timeoutOverflowMode: "throw",
578
+ }),
579
+ ),
580
+ );
581
+ });
582
+
583
+ it("threads 'throw' through the entity MV path", () => {
584
+ expectThrows(
585
+ getQuery(
586
+ buildAggregateBy({
587
+ query: {
588
+ projectId: projectId,
589
+ name: "http.client.request.duration",
590
+ time: new InBetween(startDate, endDate),
591
+ attributes: { "resource.host.name": "web-01" },
592
+ } as unknown as AggregateBy<Metric>["query"],
593
+ timeoutOverflowMode: "throw",
594
+ }),
595
+ ),
596
+ );
597
+ });
598
+
599
+ it("threads 'throw' through the mutable path", () => {
600
+ expectThrows(
601
+ getQuery(
602
+ buildAggregateBy({
603
+ query: {
604
+ projectId: projectId,
605
+ name: "oneuptime.incident.count",
606
+ time: new InBetween(startDate, endDate),
607
+ } as AggregateBy<Metric>["query"],
608
+ timeoutOverflowMode: "throw",
609
+ }),
610
+ ),
611
+ );
612
+ });
613
+
614
+ it("allow-lists the value: anything but the literal 'throw' degrades to 'break'", () => {
615
+ /*
616
+ * aggregateBy is deserialized wholesale from API clients and the
617
+ * setting reaches SQL as a literal, so it must never pass through.
618
+ */
619
+ const query: string = getQuery(
620
+ buildAggregateBy({
621
+ timeoutOverflowMode:
622
+ "break', max_execution_time = 0 --" as unknown as "break",
623
+ }),
624
+ );
625
+
626
+ expect(query).toContain("timeout_overflow_mode = 'break'");
627
+ expect(query).not.toContain("max_execution_time = 0");
628
+ });
629
+
630
+ it("the metric-monitor worker passes 'throw' on every aggregateBy call", () => {
631
+ /*
632
+ * Source-level pin: the worker lives in App (unimportable from
633
+ * this jest project without dragging in its queue/Redis import
634
+ * graph), but the invariant that alerting never evaluates
635
+ * silently-partial buckets is owned here, next to the setting
636
+ * it protects.
637
+ */
638
+ const workerSource: string = fs.readFileSync(
639
+ path.join(
640
+ __dirname,
641
+ "../../../../App/FeatureSet/Workers/Jobs/TelemetryMonitor/MonitorTelemetryMonitor.ts",
642
+ ),
643
+ "utf8",
644
+ );
645
+
646
+ const callSites: Array<string> = workerSource
647
+ .split("MetricService.aggregateBy(")
648
+ .slice(1);
649
+
650
+ expect(callSites.length).toBeGreaterThanOrEqual(9);
651
+ for (const callSite of callSites) {
652
+ // Each call's argument object is well under 1200 characters.
653
+ expect(callSite.slice(0, 1200)).toContain(
654
+ 'timeoutOverflowMode: "throw"',
655
+ );
656
+ }
657
+ });
658
+ });
659
+ });
660
+
661
+ /*
662
+ * Server-side Top-K: grouped aggregations rank groups over the whole
663
+ * window in an IN-subquery, restrict the bucketed aggregation to the
664
+ * winners, and carry the pre-trim group count as `__total_groups`.
665
+ *
666
+ * The raw-table restriction must be GLOBAL IN: a plain IN over the
667
+ * same Distributed table is a double-distributed subquery, which any
668
+ * 2+-shard cluster rejects with Code 288 (distributed_product_mode =
669
+ * 'deny'), and the cityHash64(projectId, name, primaryEntityId)
670
+ * sharding key spreads one metric's groups across shards so the
671
+ * ranking must run once globally anyway.
672
+ */
673
+ describe("server-side Top-K", () => {
674
+ it("restricts a grouped scalar aggregation to the top K groups (rankBy max) via GLOBAL IN", () => {
675
+ const result: { statement: Statement; columns: Array<string> } =
676
+ service.toAggregateStatement(
677
+ buildAggregateBy({
678
+ groupByAttributeKeys: ["host.name"],
679
+ topK: { count: 10, rankBy: "max" },
680
+ }),
681
+ );
682
+ const query: string = result.statement.query;
683
+
684
+ expect(query).toContain(") GLOBAL IN (SELECT ");
685
+ // Never the multi-shard-fatal plain IN over the Distributed table.
686
+ expect(query).not.toContain(") IN (SELECT ");
687
+ expect(query).toContain("ORDER BY max(value) DESC");
688
+ expect(query).toContain("uniqExact(");
689
+ expect(query).toContain("AS __total_groups");
690
+ // topK.count only reaches the SQL as a bound parameter.
691
+ expect(query).not.toContain("LIMIT 10)");
692
+ expect(
693
+ Object.values(result.statement.query_params).filter(
694
+ (value: unknown) => {
695
+ return value === 10;
696
+ },
697
+ ).length,
698
+ ).toBeGreaterThanOrEqual(1);
699
+ });
700
+
701
+ it("ranks by avg when requested", () => {
702
+ const query: string = getQuery(
703
+ buildAggregateBy({
704
+ groupByAttributeKeys: ["host.name"],
705
+ topK: { count: 5, rankBy: "avg" },
706
+ }),
707
+ );
708
+
709
+ expect(query).toContain("ORDER BY avg(value) DESC");
710
+ expect(query).not.toContain("ORDER BY max(value) DESC");
711
+ });
712
+
713
+ it("applies Top-K to grouped percentile aggregations (p95-by-host)", () => {
714
+ const query: string = getQuery(
715
+ buildAggregateBy({
716
+ aggregationType: AggregationType.P95,
717
+ groupByAttributeKeys: ["host.name"],
718
+ topK: { count: 10, rankBy: "max" },
719
+ }),
720
+ );
721
+
722
+ expect(query).toContain("quantileExactWeighted(0.95)");
723
+ expect(query).toContain(") GLOBAL IN (SELECT ");
724
+ expect(query).not.toContain(") IN (SELECT ");
725
+ expect(query).toContain("ORDER BY max(value) DESC");
726
+ expect(query).toContain("AS __total_groups");
727
+ });
728
+
729
+ it("applies Top-K to model-column group-bys on the scalar path", () => {
730
+ const query: string = getQuery(
731
+ buildAggregateBy({
732
+ groupBy: { metricPointType: true } as AggregateBy<Metric>["groupBy"],
733
+ topK: { count: 3, rankBy: "max" },
734
+ }),
735
+ );
736
+
737
+ expect(query).toContain(
738
+ "metricPointType) GLOBAL IN (SELECT metricPointType",
739
+ );
740
+ expect(query).toContain("uniqExact(metricPointType)");
741
+ });
742
+
743
+ it("ignores topK for ungrouped aggregations", () => {
744
+ const query: string = getQuery(
745
+ buildAggregateBy({
746
+ // Attribute filter forces the base scalar builder, no grouping.
747
+ query: {
748
+ projectId: projectId,
749
+ name: "http.client.request.duration",
750
+ time: new InBetween(startDate, endDate),
751
+ attributes: { "oneuptime.service.name": "starship-web" },
752
+ } as unknown as AggregateBy<Metric>["query"],
753
+ topK: { count: 10, rankBy: "max" },
754
+ }),
755
+ );
756
+
757
+ expect(query).not.toContain("__total_groups");
758
+ expect(query).not.toContain(") IN (SELECT ");
759
+ expect(query).not.toContain("GLOBAL IN");
760
+ });
761
+
762
+ it("rejects a non-positive topK.count", () => {
763
+ expect(() => {
764
+ return service.toAggregateStatement(
765
+ buildAggregateBy({
766
+ groupByAttributeKeys: ["host.name"],
767
+ topK: { count: 0, rankBy: "max" },
768
+ }),
769
+ );
770
+ }).toThrow(BadDataException);
771
+ });
772
+
773
+ it("rejects an unknown topK.rankBy", () => {
774
+ expect(() => {
775
+ return service.toAggregateStatement(
776
+ buildAggregateBy({
777
+ groupByAttributeKeys: ["host.name"],
778
+ topK: {
779
+ count: 10,
780
+ rankBy: "sum(1); DROP TABLE x" as unknown as "max",
781
+ },
782
+ }),
783
+ );
784
+ }).toThrow(BadDataException);
785
+ });
786
+
787
+ it("applies Top-K to grouped mutable-metric aggregations over the deduped rows", () => {
788
+ // "oneuptime.incident.count" is a registered mutable metric name.
789
+ const buildMutable: (
790
+ overrides?: Partial<AggregateBy<Metric>>,
791
+ ) => AggregateBy<Metric> = (
792
+ overrides?: Partial<AggregateBy<Metric>>,
793
+ ): AggregateBy<Metric> => {
794
+ return buildAggregateBy({
795
+ query: {
796
+ projectId: projectId,
797
+ name: "oneuptime.incident.count",
798
+ time: new InBetween(startDate, endDate),
799
+ } as AggregateBy<Metric>["query"],
800
+ groupBy: {
801
+ primaryEntityType: true,
802
+ } as AggregateBy<Metric>["groupBy"],
803
+ ...overrides,
804
+ });
805
+ };
806
+
807
+ const result: { statement: Statement; columns: Array<string> } =
808
+ service.toAggregateStatement(
809
+ buildMutable({ topK: { count: 5, rankBy: "max" } }),
810
+ );
811
+ const query: string = result.statement.query;
812
+
813
+ // Routed to the mutable table (bound as an identifier parameter).
814
+ expect(Object.values(result.statement.query_params)).toContain(
815
+ "MutableMetricItem",
816
+ );
817
+ expect(query).toContain(
818
+ "primaryEntityType) IN (SELECT primaryEntityType",
819
+ );
820
+ /*
821
+ * The mutable restriction deliberately stays plain IN: it sits in
822
+ * the initiator-evaluated outer query over the argMax-dedup
823
+ * subquery, which multi-shard ClickHouse accepts without GLOBAL.
824
+ */
825
+ expect(query).not.toContain("GLOBAL IN");
826
+ expect(query).toContain("ORDER BY max(value) DESC");
827
+ expect(query).toContain("uniqExact(primaryEntityType)");
828
+ expect(query).toContain("AS __total_groups");
829
+ // Ranking must run over the argMax-deduped rows, not raw versions.
830
+ expect(
831
+ query.split("argMax(value, version) AS value").length - 1,
832
+ ).toBeGreaterThanOrEqual(3);
833
+
834
+ // Without topK the mutable statement stays free of Top-K artifacts.
835
+ const plainQuery: string = getQuery(buildMutable());
836
+ expect(plainQuery).not.toContain("__total_groups");
837
+ expect(plainQuery).not.toContain(") IN (SELECT ");
838
+ });
839
+
840
+ it("emits an identical statement when topK is absent vs explicitly undefined", () => {
841
+ const withoutField: { statement: Statement; columns: Array<string> } =
842
+ service.toAggregateStatement(
843
+ buildAggregateBy({ groupByAttributeKeys: ["host.name"] }),
844
+ );
845
+ const withUndefined: { statement: Statement; columns: Array<string> } =
846
+ service.toAggregateStatement(
847
+ buildAggregateBy({
848
+ groupByAttributeKeys: ["host.name"],
849
+ topK: undefined,
850
+ }),
851
+ );
852
+
853
+ expect(withoutField.statement.query).toBe(withUndefined.statement.query);
854
+ expect(withoutField.statement.query_params).toStrictEqual(
855
+ withUndefined.statement.query_params,
856
+ );
857
+ // And no Top-K artifacts leak into the plain grouped statement.
858
+ expect(withoutField.statement.query).not.toContain("__total_groups");
859
+ expect(withoutField.statement.query).not.toContain(") IN (SELECT ");
860
+ expect(withoutField.statement.query).not.toContain("GLOBAL IN");
861
+ });
862
+
863
+ /*
864
+ * NULL-keyed groups: metricPointType/primaryEntityType/unit are
865
+ * Nullable in ClickHouse, GROUP BY treats NULL as its own group, but
866
+ * under the default transform_null_in=0 a NULL key never satisfies
867
+ * the Top-K IN restriction — a NULL-keyed group could win a ranking
868
+ * slot yet return zero rows. Top-K statements must therefore carry
869
+ * transform_null_in=1; plain statements must not.
870
+ */
871
+ describe("NULL group keys (transform_null_in)", () => {
872
+ it("sets transform_null_in=1 on a Top-K model-column group-by (scalar path)", () => {
873
+ const query: string = getQuery(
874
+ buildAggregateBy({
875
+ groupBy: {
876
+ metricPointType: true,
877
+ } as AggregateBy<Metric>["groupBy"],
878
+ topK: { count: 10, rankBy: "max" },
879
+ }),
880
+ );
881
+
882
+ expect(query).toContain("transform_null_in = 1");
883
+ });
884
+
885
+ it("sets transform_null_in=1 on the Top-K percentile path", () => {
886
+ const query: string = getQuery(
887
+ buildAggregateBy({
888
+ aggregationType: AggregationType.P95,
889
+ groupByAttributeKeys: ["host.name"],
890
+ topK: { count: 10, rankBy: "max" },
891
+ }),
892
+ );
893
+
894
+ expect(query).toContain("transform_null_in = 1");
895
+ });
896
+
897
+ it("sets transform_null_in=1 on the Top-K mutable path", () => {
898
+ const query: string = getQuery(
899
+ buildAggregateBy({
900
+ query: {
901
+ projectId: projectId,
902
+ name: "oneuptime.incident.count",
903
+ time: new InBetween(startDate, endDate),
904
+ } as AggregateBy<Metric>["query"],
905
+ groupBy: {
906
+ primaryEntityType: true,
907
+ } as AggregateBy<Metric>["groupBy"],
908
+ topK: { count: 5, rankBy: "max" },
909
+ }),
910
+ );
911
+
912
+ expect(query).toContain("transform_null_in = 1");
913
+ });
914
+
915
+ it("leaves transform_null_in off statements without a Top-K restriction", () => {
916
+ const grouped: string = getQuery(
917
+ buildAggregateBy({
918
+ groupBy: {
919
+ metricPointType: true,
920
+ } as AggregateBy<Metric>["groupBy"],
921
+ }),
922
+ );
923
+ const plain: string = getQuery(buildAggregateBy());
924
+
925
+ expect(grouped).not.toContain("transform_null_in");
926
+ expect(plain).not.toContain("transform_null_in");
927
+ });
928
+ });
929
+ });
930
+
931
+ /*
932
+ * Result assembly: `__total_groups` is lifted off the first row into
933
+ * AggregatedResult.totalGroups, and `truncated` flags both row-limit
934
+ * saturation (heuristic) and Top-K group truncation (exact).
935
+ */
936
+ describe("aggregate result assembly (truncated / totalGroups)", () => {
937
+ type MockRowsFunction = (rows: Array<Record<string, unknown>>) => void;
938
+
939
+ const mockExecuteQuery: MockRowsFunction = (
940
+ rows: Array<Record<string, unknown>>,
941
+ ): void => {
942
+ (
943
+ service as unknown as {
944
+ executeQuery: (statement: Statement) => Promise<unknown>;
945
+ }
946
+ ).executeQuery = async (): Promise<unknown> => {
947
+ return {
948
+ json: async (): Promise<{ data: Array<Record<string, unknown>> }> => {
949
+ return { data: rows };
950
+ },
951
+ };
952
+ };
953
+ };
954
+
955
+ const bucketRow: (
956
+ overrides?: Record<string, unknown>,
957
+ ) => Record<string, unknown> = (
958
+ overrides?: Record<string, unknown>,
959
+ ): Record<string, unknown> => {
960
+ return {
961
+ time: "2026-07-09T19:10:00.000Z",
962
+ value: 42,
963
+ attributes: { "host.name": "web-01" },
964
+ ...overrides,
965
+ };
966
+ };
967
+
968
+ it("returns truncated=false and no totalGroups on an unsaturated non-topK result", async () => {
969
+ mockExecuteQuery([bucketRow()]);
970
+
971
+ const result: AggregatedResult = await service.aggregateBy(
972
+ buildAggregateBy({ groupByAttributeKeys: ["host.name"] }),
973
+ );
974
+
975
+ expect(result.data).toHaveLength(1);
976
+ expect(result.truncated).toBe(false);
977
+ expect(result.totalGroups).toBeUndefined();
978
+ });
979
+
980
+ it("flags truncated=true when the row count hits the applied limit", async () => {
981
+ mockExecuteQuery([
982
+ bucketRow(),
983
+ bucketRow({ time: "2026-07-09T19:11:00.000Z" }),
984
+ ]);
985
+
986
+ const result: AggregatedResult = await service.aggregateBy(
987
+ buildAggregateBy({
988
+ groupByAttributeKeys: ["host.name"],
989
+ limit: 2,
990
+ }),
991
+ );
992
+
993
+ expect(result.data).toHaveLength(2);
994
+ expect(result.truncated).toBe(true);
995
+ });
996
+
997
+ it("lifts __total_groups into totalGroups and flags Top-K truncation", async () => {
998
+ mockExecuteQuery([
999
+ bucketRow({ __total_groups: "25" }),
1000
+ bucketRow({
1001
+ time: "2026-07-09T19:11:00.000Z",
1002
+ __total_groups: "25",
1003
+ }),
1004
+ ]);
1005
+
1006
+ const result: AggregatedResult = await service.aggregateBy(
1007
+ buildAggregateBy({
1008
+ groupByAttributeKeys: ["host.name"],
1009
+ topK: { count: 10, rankBy: "max" },
1010
+ }),
1011
+ );
1012
+
1013
+ expect(result.totalGroups).toBe(25);
1014
+ expect(result.truncated).toBe(true);
1015
+ // The helper column never reaches the per-row payload.
1016
+ expect(result.data[0]!["__total_groups"]).toBeUndefined();
1017
+ });
1018
+
1019
+ it("reports truncated=false when every group fit inside topK.count", async () => {
1020
+ mockExecuteQuery([bucketRow({ __total_groups: "3" })]);
1021
+
1022
+ const result: AggregatedResult = await service.aggregateBy(
1023
+ buildAggregateBy({
1024
+ groupByAttributeKeys: ["host.name"],
1025
+ topK: { count: 10, rankBy: "max" },
1026
+ }),
1027
+ );
1028
+
1029
+ expect(result.totalGroups).toBe(3);
1030
+ expect(result.truncated).toBe(false);
1031
+ });
1032
+ });
1033
+
1034
+ /*
1035
+ * Entity-scoped MV routing (tryBuildEntityAggregateMVStatement): each
1036
+ * branch of the routing decision table either routes to the matching
1037
+ * per-entity rollup with a key predicate at least as selective as (and
1038
+ * no more lossy than) the raw predicate it replaces, or bails to the
1039
+ * raw path. When in doubt the builder must NOT route.
1040
+ */
1041
+ describe("entity-scoped MV routing", () => {
1042
+ type BuildEntityQueryFunction = (
1043
+ entityFilter: Record<string, unknown>,
1044
+ overrides?: Partial<AggregateBy<Metric>>,
1045
+ ) => AggregateBy<Metric>;
1046
+
1047
+ const buildEntityAggregate: BuildEntityQueryFunction = (
1048
+ entityFilter: Record<string, unknown>,
1049
+ overrides?: Partial<AggregateBy<Metric>>,
1050
+ ): AggregateBy<Metric> => {
1051
+ return buildAggregateBy({
1052
+ query: {
1053
+ projectId: projectId,
1054
+ name: "http.client.request.duration",
1055
+ time: new InBetween(startDate, endDate),
1056
+ ...entityFilter,
1057
+ } as unknown as AggregateBy<Metric>["query"],
1058
+ ...overrides,
1059
+ });
1060
+ };
1061
+
1062
+ describe("attribute-equality routes", () => {
1063
+ it("routes a k8s.cluster.name filter to the per-cluster MV with the derived key", () => {
1064
+ const result: { statement: Statement; columns: Array<string> } =
1065
+ service.toAggregateStatement(
1066
+ buildEntityAggregate({
1067
+ attributes: { "resource.k8s.cluster.name": "Prod-EU-1" },
1068
+ }),
1069
+ );
1070
+ const query: string = result.statement.query;
1071
+
1072
+ expect(query).toContain("MetricItemAggMV1mByK8sCluster");
1073
+ expect(query).toMatch(/ AND k8sClusterEntityKey = \{p\d+:String\}/);
1074
+ // Avg merges the stored sum/count states.
1075
+ expect(query).toContain(
1076
+ "sumMerge(valueSumState) / countMerge(valueCountState)",
1077
+ );
1078
+ /*
1079
+ * The key reaches SQL only as a bound parameter, byte-identical
1080
+ * to what ingest stamps (canonicalized inside keyForKubernetesCluster).
1081
+ */
1082
+ expect(Object.values(result.statement.query_params)).toContain(
1083
+ keyForKubernetesCluster(projectId.toString(), "Prod-EU-1"),
1084
+ );
1085
+ });
1086
+
1087
+ it("routes a container.id filter to the per-container MV with the derived key", () => {
1088
+ const result: { statement: Statement; columns: Array<string> } =
1089
+ service.toAggregateStatement(
1090
+ buildEntityAggregate({
1091
+ attributes: { "resource.container.id": "abc123def456" },
1092
+ }),
1093
+ );
1094
+ const query: string = result.statement.query;
1095
+
1096
+ expect(query).toContain("MetricItemAggMV1mByContainer");
1097
+ expect(query).toMatch(/ AND containerEntityKey = \{p\d+:String\}/);
1098
+ expect(Object.values(result.statement.query_params)).toContain(
1099
+ keyForContainer(projectId.toString(), "abc123def456"),
1100
+ );
1101
+ });
1102
+
1103
+ it("keeps routing a resource.host.name filter to the per-host MV (regression)", () => {
1104
+ const result: { statement: Statement; columns: Array<string> } =
1105
+ service.toAggregateStatement(
1106
+ buildEntityAggregate({
1107
+ attributes: { "resource.host.name": "web-01" },
1108
+ }),
1109
+ );
1110
+ const query: string = result.statement.query;
1111
+
1112
+ expect(query).toContain("MetricItemAggMV1mByHostV2");
1113
+ expect(query).toMatch(/ AND hostEntityKey = \{p\d+:String\}/);
1114
+ expect(Object.values(result.statement.query_params)).toContain(
1115
+ keyForHost(projectId.toString(), "web-01"),
1116
+ );
1117
+ });
1118
+
1119
+ it("merges the matching state per aggregation type", () => {
1120
+ const cases: Array<[AggregationType, string]> = [
1121
+ [AggregationType.Sum, "sumMerge(valueSumState) as value"],
1122
+ [AggregationType.Count, "countMerge(valueCountState) as value"],
1123
+ [AggregationType.Min, "minMerge(valueMinState) as value"],
1124
+ [AggregationType.Max, "maxMerge(valueMaxState) as value"],
1125
+ ];
1126
+ for (const [aggregationType, expected] of cases) {
1127
+ const query: string = getQuery(
1128
+ buildEntityAggregate(
1129
+ {
1130
+ attributes: { "resource.k8s.cluster.name": "prod-eu-1" },
1131
+ },
1132
+ { aggregationType },
1133
+ ),
1134
+ );
1135
+ expect(query).toContain("MetricItemAggMV1mByK8sCluster");
1136
+ expect(query).toContain(expected);
1137
+ }
1138
+ });
1139
+
1140
+ it("caps execution time on the entity MV paths", () => {
1141
+ const query: string = getQuery(
1142
+ buildEntityAggregate({
1143
+ attributes: { "resource.k8s.cluster.name": "prod-eu-1" },
1144
+ }),
1145
+ );
1146
+ expect(query).toContain("max_execution_time = 45");
1147
+ expect(query).toContain("timeout_overflow_mode = 'break'");
1148
+ });
1149
+ });
1150
+
1151
+ describe("service route (registry key set)", () => {
1152
+ const setServiceKeysHint: (
1153
+ aggregateBy: AggregateBy<Metric>,
1154
+ keys: Array<string>,
1155
+ ) => void = (
1156
+ aggregateBy: AggregateBy<Metric>,
1157
+ keys: Array<string>,
1158
+ ): void => {
1159
+ (
1160
+ service as unknown as {
1161
+ serviceEntityKeysHintByAggregate: WeakMap<
1162
+ AggregateBy<Metric>,
1163
+ Array<string>
1164
+ >;
1165
+ }
1166
+ ).serviceEntityKeysHintByAggregate.set(aggregateBy, keys);
1167
+ };
1168
+
1169
+ it("falls back to raw without a registry hint (sync caller / registry miss)", () => {
1170
+ const query: string = getQuery(
1171
+ buildEntityAggregate({
1172
+ attributes: { "resource.service.name": "checkout" },
1173
+ }),
1174
+ );
1175
+
1176
+ expect(query).not.toContain("MetricItemAggMV1mByService");
1177
+ // Raw scalar path: distribution-aware expressions over the base table.
1178
+ expect(query).toContain("sum(multiIf(isNotNull(count)");
1179
+ });
1180
+
1181
+ it("routes with serviceEntityKey IN (<keys>) when the registry resolved several variants", () => {
1182
+ const keys: Array<string> = [
1183
+ "1111222233334444",
1184
+ keyForService(projectId.toString(), "checkout"),
1185
+ ].sort();
1186
+ const aggregateBy: AggregateBy<Metric> = buildEntityAggregate({
1187
+ attributes: { "resource.service.name": "checkout" },
1188
+ });
1189
+ setServiceKeysHint(aggregateBy, keys);
1190
+
1191
+ const result: { statement: Statement; columns: Array<string> } =
1192
+ service.toAggregateStatement(aggregateBy);
1193
+ const query: string = result.statement.query;
1194
+
1195
+ expect(query).toContain("MetricItemAggMV1mByService");
1196
+ expect(query).toMatch(
1197
+ / AND serviceEntityKey IN \{p\d+:Array\(String\)\}/,
1198
+ );
1199
+ expect(Object.values(result.statement.query_params)).toContainEqual(
1200
+ keys,
1201
+ );
1202
+ });
1203
+
1204
+ it("routes a single-key registry hit as a plain equality", () => {
1205
+ const keys: Array<string> = [
1206
+ keyForService(projectId.toString(), "checkout"),
1207
+ ];
1208
+ const aggregateBy: AggregateBy<Metric> = buildEntityAggregate({
1209
+ attributes: { "resource.service.name": "checkout" },
1210
+ });
1211
+ setServiceKeysHint(aggregateBy, keys);
1212
+
1213
+ const query: string = getQuery(aggregateBy);
1214
+
1215
+ expect(query).toContain("MetricItemAggMV1mByService");
1216
+ expect(query).toMatch(/ AND serviceEntityKey = \{p\d+:String\}/);
1217
+ });
1218
+
1219
+ it("never routes an empty hint (belt-and-braces for the raw fallback)", () => {
1220
+ const aggregateBy: AggregateBy<Metric> = buildEntityAggregate({
1221
+ attributes: { "resource.service.name": "checkout" },
1222
+ });
1223
+ setServiceKeysHint(aggregateBy, []);
1224
+
1225
+ expect(getQuery(aggregateBy)).not.toContain(
1226
+ "MetricItemAggMV1mByService",
1227
+ );
1228
+ });
1229
+ });
1230
+
1231
+ describe("entityScope routes", () => {
1232
+ it("routes a verified single-key host entityScope (Host Metrics tab shape)", () => {
1233
+ const hostKey: string = keyForHost(projectId.toString(), "web-01");
1234
+ const result: { statement: Statement; columns: Array<string> } =
1235
+ service.toAggregateStatement(
1236
+ buildEntityAggregate({
1237
+ entityScope: {
1238
+ entityKeys: [hostKey],
1239
+ attributeKey: "resource.host.name",
1240
+ attributeValue: "web-01",
1241
+ },
1242
+ }),
1243
+ );
1244
+ const query: string = result.statement.query;
1245
+
1246
+ expect(query).toContain("MetricItemAggMV1mByHostV2");
1247
+ expect(query).toMatch(/ AND hostEntityKey = \{p\d+:String\}/);
1248
+ expect(Object.values(result.statement.query_params)).toContain(hostKey);
1249
+ });
1250
+
1251
+ it("routes a verified single-key k8s cluster entityScope (Kubernetes Metrics tab shape)", () => {
1252
+ const clusterKey: string = keyForKubernetesCluster(
1253
+ projectId.toString(),
1254
+ "prod-eu-1",
1255
+ );
1256
+ const query: string = getQuery(
1257
+ buildEntityAggregate({
1258
+ entityScope: {
1259
+ entityKeys: [clusterKey],
1260
+ attributeKey: "resource.k8s.cluster.name",
1261
+ attributeValue: "prod-eu-1",
1262
+ },
1263
+ }),
1264
+ );
1265
+
1266
+ expect(query).toContain("MetricItemAggMV1mByK8sCluster");
1267
+ expect(query).toMatch(/ AND k8sClusterEntityKey = \{p\d+:String\}/);
1268
+ });
1269
+
1270
+ it("routes a verified single-key container entityScope", () => {
1271
+ const containerKey: string = keyForContainer(
1272
+ projectId.toString(),
1273
+ "abc123def456",
1274
+ );
1275
+ const query: string = getQuery(
1276
+ buildEntityAggregate({
1277
+ entityScope: {
1278
+ entityKeys: [containerKey],
1279
+ attributeKey: "resource.container.id",
1280
+ attributeValue: "abc123def456",
1281
+ },
1282
+ }),
1283
+ );
1284
+
1285
+ expect(query).toContain("MetricItemAggMV1mByContainer");
1286
+ expect(query).toMatch(/ AND containerEntityKey = \{p\d+:String\}/);
1287
+ });
1288
+
1289
+ it("bails when the scope key does not byte-match the derived key", () => {
1290
+ const query: string = getQuery(
1291
+ buildEntityAggregate({
1292
+ entityScope: {
1293
+ entityKeys: ["ffffffffffffffff"],
1294
+ attributeKey: "resource.host.name",
1295
+ attributeValue: "web-01",
1296
+ },
1297
+ }),
1298
+ );
1299
+
1300
+ expect(query).not.toContain("MetricItemAggMV1mByHostV2");
1301
+ });
1302
+
1303
+ it("bails on a multi-key scope (hasAny over foreign keys is not translatable)", () => {
1304
+ const hostKey: string = keyForHost(projectId.toString(), "web-01");
1305
+ const query: string = getQuery(
1306
+ buildEntityAggregate({
1307
+ entityScope: {
1308
+ entityKeys: [hostKey, "ffffffffffffffff"],
1309
+ attributeKey: "resource.host.name",
1310
+ attributeValue: "web-01",
1311
+ },
1312
+ }),
1313
+ );
1314
+
1315
+ expect(query).not.toContain("MetricItemAggMV1mByHostV2");
1316
+ });
1317
+
1318
+ it("bails on a service entityScope (namespace variants make the bare key lossy)", () => {
1319
+ const query: string = getQuery(
1320
+ buildEntityAggregate({
1321
+ entityScope: {
1322
+ entityKeys: [keyForService(projectId.toString(), "checkout")],
1323
+ attributeKey: "resource.service.name",
1324
+ attributeValue: "checkout",
1325
+ },
1326
+ }),
1327
+ );
1328
+
1329
+ expect(query).not.toContain("MetricItemAggMV1mByService");
1330
+ });
1331
+
1332
+ it("bails on an unknown scope attributeKey (e.g. k8s pod — composite identity)", () => {
1333
+ const query: string = getQuery(
1334
+ buildEntityAggregate({
1335
+ entityScope: {
1336
+ entityKeys: ["1234123412341234"],
1337
+ attributeKey: "resource.k8s.pod.name",
1338
+ attributeValue: "api-7f9c",
1339
+ },
1340
+ }),
1341
+ );
1342
+
1343
+ expect(query).not.toMatch(/MetricItemAggMV1mBy/);
1344
+ });
1345
+ });
1346
+
1347
+ describe("attribute + entityScope combined (sparkline shape)", () => {
1348
+ it("routes when the scope is redundant with the attribute filter", () => {
1349
+ const query: string = getQuery(
1350
+ buildEntityAggregate({
1351
+ attributes: { "resource.host.name": "web-01" },
1352
+ entityScope: {
1353
+ entityKeys: [keyForHost(projectId.toString(), "web-01")],
1354
+ attributeKey: "resource.host.name",
1355
+ attributeValue: "web-01",
1356
+ },
1357
+ }),
1358
+ );
1359
+
1360
+ expect(query).toContain("MetricItemAggMV1mByHostV2");
1361
+ expect(query).toMatch(/ AND hostEntityKey = \{p\d+:String\}/);
1362
+ });
1363
+
1364
+ it("bails when the scope disagrees on the attribute value (two genuine filters)", () => {
1365
+ const query: string = getQuery(
1366
+ buildEntityAggregate({
1367
+ attributes: { "resource.host.name": "web-01" },
1368
+ entityScope: {
1369
+ entityKeys: [keyForHost(projectId.toString(), "web-02")],
1370
+ attributeKey: "resource.host.name",
1371
+ attributeValue: "web-02",
1372
+ },
1373
+ }),
1374
+ );
1375
+
1376
+ expect(query).not.toContain("MetricItemAggMV1mByHostV2");
1377
+ });
1378
+
1379
+ it("bails when the scope filters a different entity type than the attribute", () => {
1380
+ const query: string = getQuery(
1381
+ buildEntityAggregate({
1382
+ attributes: { "resource.host.name": "web-01" },
1383
+ entityScope: {
1384
+ entityKeys: [
1385
+ keyForKubernetesCluster(projectId.toString(), "prod-eu-1"),
1386
+ ],
1387
+ attributeKey: "resource.k8s.cluster.name",
1388
+ attributeValue: "prod-eu-1",
1389
+ },
1390
+ }),
1391
+ );
1392
+
1393
+ expect(query).not.toContain("MetricItemAggMV1mByHostV2");
1394
+ expect(query).not.toContain("MetricItemAggMV1mByK8sCluster");
1395
+ });
1396
+
1397
+ it("bails on service + entityScope even when they agree", () => {
1398
+ const query: string = getQuery(
1399
+ buildEntityAggregate({
1400
+ attributes: { "resource.service.name": "checkout" },
1401
+ entityScope: {
1402
+ entityKeys: [keyForService(projectId.toString(), "checkout")],
1403
+ attributeKey: "resource.service.name",
1404
+ attributeValue: "checkout",
1405
+ },
1406
+ }),
1407
+ );
1408
+
1409
+ expect(query).not.toContain("MetricItemAggMV1mByService");
1410
+ });
1411
+ });
1412
+
1413
+ describe("bail conditions shared by every entity route", () => {
1414
+ const clusterFilter: Record<string, unknown> = {
1415
+ attributes: { "resource.k8s.cluster.name": "prod-eu-1" },
1416
+ };
1417
+
1418
+ it("bails on group-by attribute keys (legend series need the raw table)", () => {
1419
+ const query: string = getQuery(
1420
+ buildEntityAggregate(clusterFilter, {
1421
+ groupByAttributeKeys: ["k8s.node.name"],
1422
+ }),
1423
+ );
1424
+
1425
+ expect(query).not.toContain("MetricItemAggMV1mByK8sCluster");
1426
+ expect(query).toContain("__attr_grp_0");
1427
+ });
1428
+
1429
+ it("bails on a model-column group-by", () => {
1430
+ const query: string = getQuery(
1431
+ buildEntityAggregate(clusterFilter, {
1432
+ groupBy: {
1433
+ metricPointType: true,
1434
+ } as AggregateBy<Metric>["groupBy"],
1435
+ }),
1436
+ );
1437
+
1438
+ expect(query).not.toContain("MetricItemAggMV1mByK8sCluster");
1439
+ });
1440
+
1441
+ it("bails on percentile aggregations", () => {
1442
+ const query: string = getQuery(
1443
+ buildEntityAggregate(clusterFilter, {
1444
+ aggregationType: AggregationType.P90,
1445
+ }),
1446
+ );
1447
+
1448
+ expect(query).not.toContain("MetricItemAggMV1mByK8sCluster");
1449
+ expect(query).toContain("quantileExactWeighted(0.9)");
1450
+ });
1451
+
1452
+ it("bails on distribution metrics (states collapse the histogram sum)", () => {
1453
+ const aggregateBy: AggregateBy<Metric> =
1454
+ buildEntityAggregate(clusterFilter);
1455
+ (
1456
+ service as unknown as {
1457
+ pointTypeHintByAggregate: WeakMap<
1458
+ AggregateBy<Metric>,
1459
+ string | null
1460
+ >;
1461
+ }
1462
+ ).pointTypeHintByAggregate.set(aggregateBy, "Histogram");
1463
+
1464
+ const query: string = getQuery(aggregateBy);
1465
+
1466
+ expect(query).not.toContain("MetricItemAggMV1mByK8sCluster");
1467
+ expect(query).toContain("sum(multiIf(isNotNull(count)");
1468
+ });
1469
+
1470
+ it("bails when a second attribute filter is present", () => {
1471
+ const query: string = getQuery(
1472
+ buildEntityAggregate({
1473
+ attributes: {
1474
+ "resource.k8s.cluster.name": "prod-eu-1",
1475
+ "k8s.namespace.name": "default",
1476
+ },
1477
+ }),
1478
+ );
1479
+
1480
+ expect(query).not.toContain("MetricItemAggMV1mByK8sCluster");
1481
+ });
1482
+
1483
+ it("bails when the query carries a column the MV does not have", () => {
1484
+ const query: string = getQuery(
1485
+ buildEntityAggregate({
1486
+ attributes: { "resource.k8s.cluster.name": "prod-eu-1" },
1487
+ metricPointType: "Sum",
1488
+ }),
1489
+ );
1490
+
1491
+ expect(query).not.toContain("MetricItemAggMV1mByK8sCluster");
1492
+ });
1493
+
1494
+ it("bails on Total (whole-window) aggregations", () => {
1495
+ const query: string = getQuery(
1496
+ buildEntityAggregate(clusterFilter, {
1497
+ aggregationInterval: AggregationInterval.Total,
1498
+ }),
1499
+ );
1500
+
1501
+ expect(query).not.toContain("MetricItemAggMV1mByK8sCluster");
1502
+ expect(query).toContain("min(time) as time");
1503
+ });
1504
+
1505
+ it("bails without a projectId (entity keys are tenant-scoped)", () => {
1506
+ const query: string = getQuery(
1507
+ buildAggregateBy({
1508
+ query: {
1509
+ name: "http.client.request.duration",
1510
+ time: new InBetween(startDate, endDate),
1511
+ attributes: { "resource.k8s.cluster.name": "prod-eu-1" },
1512
+ } as unknown as AggregateBy<Metric>["query"],
1513
+ }),
1514
+ );
1515
+
1516
+ expect(query).not.toContain("MetricItemAggMV1mByK8sCluster");
1517
+ });
1518
+
1519
+ it("bails on a non-string attribute value", () => {
1520
+ const query: string = getQuery(
1521
+ buildEntityAggregate({
1522
+ attributes: { "resource.k8s.cluster.name": 42 },
1523
+ }),
1524
+ );
1525
+
1526
+ expect(query).not.toContain("MetricItemAggMV1mByK8sCluster");
1527
+ });
1528
+
1529
+ it("bails on a non-resource attribute spelling (datapoint attrs do not stamp entity keys)", () => {
1530
+ const query: string = getQuery(
1531
+ buildEntityAggregate({
1532
+ attributes: { "k8s.cluster.name": "prod-eu-1" },
1533
+ }),
1534
+ );
1535
+
1536
+ expect(query).not.toContain("MetricItemAggMV1mByK8sCluster");
1537
+ });
1538
+ });
1539
+
1540
+ /*
1541
+ * The async half of the service route: aggregateBy() resolves the
1542
+ * key set from the TelemetryEntity registry (short-TTL cached) and
1543
+ * hands it to the synchronous builder via the WeakMap hint.
1544
+ */
1545
+ describe("service registry lookup plumbing (aggregateBy)", () => {
1546
+ type CapturedStatements = Array<{
1547
+ query: string;
1548
+ params: Record<string, unknown>;
1549
+ }>;
1550
+
1551
+ const captureExecuteQuery: () => CapturedStatements = () => {
1552
+ const captured: CapturedStatements = [];
1553
+ (
1554
+ service as unknown as {
1555
+ executeQuery: (statement: Statement) => Promise<unknown>;
1556
+ }
1557
+ ).executeQuery = async (statement: Statement): Promise<unknown> => {
1558
+ captured.push({
1559
+ query: statement.query,
1560
+ params: statement.query_params,
1561
+ });
1562
+ return {
1563
+ json: async (): Promise<{
1564
+ data: Array<Record<string, unknown>>;
1565
+ }> => {
1566
+ return { data: [] };
1567
+ },
1568
+ };
1569
+ };
1570
+ return captured;
1571
+ };
1572
+
1573
+ const buildServiceAggregate: () => AggregateBy<Metric> =
1574
+ (): AggregateBy<Metric> => {
1575
+ return buildAggregateBy({
1576
+ query: {
1577
+ projectId: projectId,
1578
+ name: "http.client.request.duration",
1579
+ time: new InBetween(startDate, endDate),
1580
+ attributes: { "resource.service.name": "Checkout" },
1581
+ } as unknown as AggregateBy<Metric>["query"],
1582
+ });
1583
+ };
1584
+
1585
+ const originalFindBy: typeof TelemetryEntityService.findBy =
1586
+ TelemetryEntityService.findBy;
1587
+ const originalCountBy: typeof TelemetryEntityService.countBy =
1588
+ TelemetryEntityService.countBy;
1589
+
1590
+ /*
1591
+ * The registry lookup refuses to route when the project's Service
1592
+ * registry is at/over its entity budget (10000), so every test
1593
+ * that expects routing needs a comfortably-under-budget count.
1594
+ */
1595
+ const mockCountBy: (count: number) => void = (count: number): void => {
1596
+ TelemetryEntityService.countBy = jest.fn(
1597
+ async (): Promise<PositiveNumber> => {
1598
+ return new PositiveNumber(count);
1599
+ },
1600
+ ) as typeof TelemetryEntityService.countBy;
1601
+ };
1602
+
1603
+ beforeEach(() => {
1604
+ mockCountBy(3);
1605
+ });
1606
+
1607
+ afterEach(() => {
1608
+ TelemetryEntityService.findBy = originalFindBy;
1609
+ TelemetryEntityService.countBy = originalCountBy;
1610
+ });
1611
+
1612
+ it("routes with the registry keys unioned with the bare-name key", async () => {
1613
+ const namespacedKey: string = keyForService(
1614
+ projectId.toString(),
1615
+ "checkout",
1616
+ "prod",
1617
+ );
1618
+ TelemetryEntityService.findBy = jest.fn(
1619
+ async (): Promise<Array<TelemetryEntity>> => {
1620
+ return [
1621
+ {
1622
+ entityKey: namespacedKey,
1623
+ identifyingAttributes: {
1624
+ "service.name": "checkout",
1625
+ "service.namespace": "prod",
1626
+ },
1627
+ } as unknown as TelemetryEntity,
1628
+ ];
1629
+ },
1630
+ ) as typeof TelemetryEntityService.findBy;
1631
+
1632
+ const captured: CapturedStatements = captureExecuteQuery();
1633
+ await service.aggregateBy(buildServiceAggregate());
1634
+
1635
+ // Statement 1 is the point-type lookup; statement 2 the aggregate.
1636
+ const aggregateStatement: { query: string } | undefined =
1637
+ captured[captured.length - 1];
1638
+ expect(aggregateStatement!.query).toContain(
1639
+ "MetricItemAggMV1mByService",
1640
+ );
1641
+ const expectedKeys: Array<string> = [
1642
+ namespacedKey,
1643
+ keyForService(projectId.toString(), "Checkout"),
1644
+ ].sort();
1645
+ expect(
1646
+ Object.values(captured[captured.length - 1]!.params),
1647
+ ).toContainEqual(expectedKeys);
1648
+ });
1649
+
1650
+ it("falls back to raw on a registry miss", async () => {
1651
+ TelemetryEntityService.findBy = jest.fn(
1652
+ async (): Promise<Array<TelemetryEntity>> => {
1653
+ return [];
1654
+ },
1655
+ ) as typeof TelemetryEntityService.findBy;
1656
+
1657
+ const captured: CapturedStatements = captureExecuteQuery();
1658
+ await service.aggregateBy(buildServiceAggregate());
1659
+
1660
+ const aggregateStatement: { query: string } | undefined =
1661
+ captured[captured.length - 1];
1662
+ expect(aggregateStatement!.query).not.toContain(
1663
+ "MetricItemAggMV1mByService",
1664
+ );
1665
+ });
1666
+
1667
+ it("ignores registry rows whose identity is not this service.name (displayName collision guard)", async () => {
1668
+ TelemetryEntityService.findBy = jest.fn(
1669
+ async (): Promise<Array<TelemetryEntity>> => {
1670
+ return [
1671
+ {
1672
+ entityKey: "1234123412341234",
1673
+ identifyingAttributes: { "foo.name": "checkout" },
1674
+ } as unknown as TelemetryEntity,
1675
+ ];
1676
+ },
1677
+ ) as typeof TelemetryEntityService.findBy;
1678
+
1679
+ const captured: CapturedStatements = captureExecuteQuery();
1680
+ await service.aggregateBy(buildServiceAggregate());
1681
+
1682
+ const aggregateStatement: { query: string } | undefined =
1683
+ captured[captured.length - 1];
1684
+ expect(aggregateStatement!.query).not.toContain(
1685
+ "MetricItemAggMV1mByService",
1686
+ );
1687
+ });
1688
+
1689
+ it("caches the key set per (project, canonical name)", async () => {
1690
+ const findBySpy: jest.Mock = jest.fn(
1691
+ async (): Promise<Array<TelemetryEntity>> => {
1692
+ return [
1693
+ {
1694
+ entityKey: keyForService(projectId.toString(), "checkout"),
1695
+ identifyingAttributes: { "service.name": "checkout" },
1696
+ } as unknown as TelemetryEntity,
1697
+ ];
1698
+ },
1699
+ );
1700
+ TelemetryEntityService.findBy =
1701
+ findBySpy as unknown as typeof TelemetryEntityService.findBy;
1702
+
1703
+ captureExecuteQuery();
1704
+ await service.aggregateBy(buildServiceAggregate());
1705
+ await service.aggregateBy(buildServiceAggregate());
1706
+
1707
+ expect(findBySpy).toHaveBeenCalledTimes(1);
1708
+ });
1709
+
1710
+ it("refuses to route when the project's Service registry is budget-capped", async () => {
1711
+ /*
1712
+ * At/over the entity budget, ingest stops minting NEW Service
1713
+ * registry rows forever while namespaced serviceEntityKeys keep
1714
+ * flowing on signal rows — so a non-empty registry key set may
1715
+ * be PERMANENTLY missing identity variants. Routing on such a
1716
+ * partial set would silently under-report; the raw attributes
1717
+ * predicate is the only complete one.
1718
+ */
1719
+ TelemetryEntityService.findBy = jest.fn(
1720
+ async (): Promise<Array<TelemetryEntity>> => {
1721
+ // A partial-but-non-empty key set (the dangerous state).
1722
+ return [
1723
+ {
1724
+ entityKey: keyForService(
1725
+ projectId.toString(),
1726
+ "checkout",
1727
+ "prod",
1728
+ ),
1729
+ identifyingAttributes: {
1730
+ "service.name": "checkout",
1731
+ "service.namespace": "prod",
1732
+ },
1733
+ } as unknown as TelemetryEntity,
1734
+ ];
1735
+ },
1736
+ ) as typeof TelemetryEntityService.findBy;
1737
+ mockCountBy(10000); // getEntityBudget(EntityType.Service)
1738
+
1739
+ const captured: CapturedStatements = captureExecuteQuery();
1740
+ await service.aggregateBy(buildServiceAggregate());
1741
+
1742
+ const aggregateStatement: { query: string } | undefined =
1743
+ captured[captured.length - 1];
1744
+ expect(aggregateStatement!.query).not.toContain(
1745
+ "MetricItemAggMV1mByService",
1746
+ );
1747
+ // Raw fallback still serves the query.
1748
+ expect(aggregateStatement!.query).toContain("MetricItemV3");
1749
+ });
1750
+ });
1751
+ });
361
1752
  });