@oneuptime/common 12.0.23 → 12.0.24

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 (294) hide show
  1. package/Models/DatabaseModels/DetectionRule.ts +55 -0
  2. package/Models/DatabaseModels/NetworkDeviceAutoImportRule.ts +27 -7
  3. package/Models/DatabaseModels/NetworkDeviceDiscoveryScan.ts +67 -0
  4. package/Models/DatabaseModels/Project.ts +68 -0
  5. package/Models/DatabaseModels/User.ts +68 -0
  6. package/Server/API/TelemetryAPI.ts +29 -0
  7. package/Server/Infrastructure/Postgres/SchemaMigrations/1789400000000-AddNameToNetworkDeviceDiscoveryScan.ts +28 -0
  8. package/Server/Infrastructure/Postgres/SchemaMigrations/1789500000000-AddCampaignUtmFields.ts +49 -0
  9. package/Server/Infrastructure/Postgres/SchemaMigrations/1789512000000-AddDetectionRuleDistinctCountColumns.ts +33 -0
  10. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
  11. package/Server/Middleware/GzipRequestBody.ts +311 -0
  12. package/Server/Middleware/MultipartFormData.ts +87 -3
  13. package/Server/Services/DetectionRuleService.ts +73 -6
  14. package/Server/Services/Index.ts +205 -0
  15. package/Server/Services/NetworkDeviceDiscoveryScanService.ts +59 -0
  16. package/Server/Services/NetworkDeviceService.ts +148 -0
  17. package/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.ts +65 -0
  18. package/Server/Services/OnCallDutyPolicyFeedService.ts +1 -1
  19. package/Server/Services/ProjectService.ts +29 -19
  20. package/Server/Services/SecurityEventService.ts +90 -13
  21. package/Server/Services/StatusPageResourceService.ts +223 -0
  22. package/Server/Services/TelemetryAttributeService.ts +17 -0
  23. package/Server/Services/UserService.ts +4 -6
  24. package/Server/Types/Markdown.ts +161 -67
  25. package/Server/Utils/AI/Toolbox/OnCallTools.ts +12 -4
  26. package/Server/Utils/AnalyticsDatabase/Statement.ts +13 -1
  27. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +297 -6
  28. package/Server/Utils/Attribution.ts +4 -0
  29. package/Server/Utils/Marketing/MarketingEventUtil.ts +45 -7
  30. package/Server/Utils/SecurityEvent/DetectionRuleEvaluator.ts +55 -3
  31. package/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.ts +21 -0
  32. package/Server/Utils/StartServer.ts +12 -32
  33. package/Server/Utils/Telemetry/LlmCostBudgetEvaluator.ts +101 -4
  34. package/Server/Utils/Telemetry/LlmMetricSpend.ts +130 -0
  35. package/Server/Views/Partials/AnalyticsConsent.ejs +159 -102
  36. package/Server/Views/Partials/Head.ejs +2 -1
  37. package/Tests/App/Dashboard/AdminUserNotificationMethodsPage.test.tsx +776 -38
  38. package/Tests/App/Dashboard/CorrelateFilterBuilder.test.tsx +359 -0
  39. package/Tests/App/Dashboard/CorrelateGraph.test.tsx +865 -0
  40. package/Tests/App/Dashboard/CorrelationGraph.test.ts +346 -0
  41. package/Tests/App/Dashboard/CriteriaFilterConditionDropdown.test.tsx +256 -0
  42. package/Tests/App/Dashboard/CriteriaFilterDefaults.test.ts +738 -0
  43. package/Tests/App/Dashboard/CriteriaFilterMonitorTypeChange.test.ts +1122 -0
  44. package/Tests/App/Dashboard/DiscoveryScanWizardValidation.test.tsx +283 -0
  45. package/Tests/App/Dashboard/ExternalStatusPageCriteriaFilter.test.ts +407 -0
  46. package/Tests/App/Dashboard/MetricSeriesInvestigateMenu.test.tsx +5 -14
  47. package/Tests/App/Dashboard/MonitorCriteriaActionToggles.test.tsx +426 -0
  48. package/Tests/App/Dashboard/MonitorStepsCriteriaAlignmentWiring.test.tsx +352 -0
  49. package/Tests/App/Dashboard/OnCallLayerCardOverrides.test.tsx +278 -0
  50. package/Tests/App/Dashboard/OnCallLayerShiftPreviewOverrides.test.ts +237 -0
  51. package/Tests/App/Dashboard/OnCallReadinessSurfaces.test.tsx +2 -2
  52. package/Tests/App/Dashboard/OnCallSchedulePreviewOverrides.test.tsx +460 -0
  53. package/Tests/App/Dashboard/SecurityEventAttributeUtil.test.ts +171 -0
  54. package/Tests/App/Dashboard/SecurityEventCorrelation.test.ts +857 -0
  55. package/Tests/App/Dashboard/SecurityEventDetailObservables.test.tsx +194 -0
  56. package/Tests/App/Dashboard/SecurityEventsDetectionRulesPage.test.tsx +124 -0
  57. package/Tests/App/Dashboard/SecurityEventsMonitorStepForm.test.tsx +79 -19
  58. package/Tests/App/Dashboard/SecurityEventsTableColumns.test.tsx +382 -0
  59. package/Tests/App/StatusPage/BulkAddStatusPageMonitors.test.ts +216 -0
  60. package/Tests/App/StatusPage/BulkAddStatusPageMonitorsModal.test.tsx +590 -11
  61. package/Tests/App/StatusPage/StatusPageResourceExplorer.test.tsx +757 -0
  62. package/Tests/Models/DiscoveryScanNameColumn.test.ts +293 -0
  63. package/Tests/Server/API/SecurityEventAttributesAPI.test.ts +564 -0
  64. package/Tests/Server/Infrastructure/Postgres/SchemaMigrationsOrdering.test.ts +120 -4
  65. package/Tests/Server/Middleware/GzipRequestBody.test.ts +725 -0
  66. package/Tests/Server/Middleware/MultipartFormData.test.ts +298 -0
  67. package/Tests/Server/Services/AddDetectionRuleDistinctCountColumnsMigration.test.ts +311 -0
  68. package/Tests/Server/Services/DetectionRuleService.test.ts +134 -2
  69. package/Tests/Server/Services/DiscoveryScanClaimHookFreeSafety.test.ts +7 -1
  70. package/Tests/Server/Services/FeedRetentionConsistency.test.ts +131 -0
  71. package/Tests/Server/Services/NetworkDeviceDiscoveryScanService.test.ts +194 -0
  72. package/Tests/Server/Services/NetworkDeviceMonitorStatusStamp.test.ts +800 -0
  73. package/Tests/Server/Services/OnCallScheduleAttachRosterRefresh.test.ts +244 -0
  74. package/Tests/Server/Services/SecurityEventService.test.ts +376 -0
  75. package/Tests/Server/Services/StatusPageResourceDuplicates.test.ts +703 -0
  76. package/Tests/Server/Services/TelemetryAttributeServiceSecurityEvent.test.ts +256 -0
  77. package/Tests/Server/Types/Workflow/Components/ComponentRegistryParity.test.ts +417 -0
  78. package/Tests/Server/Utils/AnalyticsDatabase/StatementGeneratorWhereOperators.test.ts +677 -0
  79. package/Tests/Server/Utils/Marketing/MarketingEventUtil.test.ts +43 -0
  80. package/Tests/Server/Utils/Monitor/Criteria/ProfileMonitorCriteria.test.ts +15 -11
  81. package/Tests/Server/Utils/SecurityEvent/DetectionRuleEvaluator.test.ts +369 -8
  82. package/Tests/Server/Utils/SecurityEvent/SigmaClickhouseCompiler.test.ts +35 -0
  83. package/Tests/Server/Utils/Telemetry/LlmCostBudgetEvaluator.test.ts +368 -4
  84. package/Tests/Server/Utils/Telemetry/LlmMetricSpend.test.ts +245 -0
  85. package/Tests/Server/Views/AnalyticsConsentPartial.test.ts +48 -5
  86. package/Tests/Types/Code/YamlSyntax.test.ts +396 -0
  87. package/Tests/Types/Monitor/MetricAndProfileDefaultCriteria.test.ts +957 -0
  88. package/Tests/Types/Monitor/MonitorCriteriaActionValidation.test.ts +598 -0
  89. package/Tests/Types/Monitor/MonitorCriteriaInstance.test.ts +49 -0
  90. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationNotificationMode.test.ts +6 -6
  91. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.test.ts +4 -4
  92. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationUtil.test.ts +4 -3
  93. package/Tests/Types/Telemetry/LlmMetricConventions.test.ts +193 -0
  94. package/Tests/UI/Components/AiInvestigationSettingsCard.test.tsx +0 -12
  95. package/Tests/UI/Components/CardModelDetailEdit.test.tsx +1 -17
  96. package/Tests/UI/Components/CodeEditor.test.tsx +474 -10
  97. package/Tests/UI/Components/Detail/YamlField.test.tsx +105 -0
  98. package/Tests/UI/Components/EntityDropdownLabelsBulkAdd.test.tsx +319 -0
  99. package/Tests/UI/Components/Forms/FormFieldYaml.test.tsx +231 -0
  100. package/Tests/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.test.ts +1 -0
  101. package/Tests/UI/Components/Forms/ValidationYAML.test.ts +234 -0
  102. package/Tests/UI/Components/ModelTable/AttributeColumns.test.tsx +573 -0
  103. package/Tests/UI/Components/ModelTable/BaseModelTableAttributeColumns.test.tsx +824 -0
  104. package/Tests/UI/Components/ModelTable/ColumnCustomizationModalAddableColumns.test.tsx +707 -0
  105. package/Tests/UI/Components/SideOver.test.tsx +1 -1
  106. package/Tests/UI/Components/StatusPage/ResourceGridSelection.test.tsx +294 -0
  107. package/Tests/UI/Components/StatusPage/ResourceListSelection.test.tsx +436 -0
  108. package/Tests/UI/Components/TableBulkCsvExport.test.tsx +6 -2
  109. package/Tests/UI/Components/Toggle.test.tsx +25 -0
  110. package/Tests/UI/Components/YamlEditor.test.tsx +847 -0
  111. package/Tests/Utils/NetworkDevice/DeviceReachabilityUtil.test.ts +252 -0
  112. package/Tests/Utils/NetworkDiscovery/DiscoveredHostUtil.test.ts +221 -0
  113. package/Tests/Utils/NetworkDiscovery/ScanNameUtil.test.ts +312 -0
  114. package/Tests/Utils/StatusPage/ResourceExplorer.test.ts +468 -0
  115. package/Tests/Utils/Telemetry/LlmMetricQuery.test.ts +472 -0
  116. package/Types/Code/YamlSyntax.ts +396 -0
  117. package/Types/Exception/ExceptionCode.ts +1 -0
  118. package/Types/Exception/PayloadTooLargeException.ts +8 -0
  119. package/Types/Marketing/Attribution.ts +10 -0
  120. package/Types/Marketing/MarketingEvent.ts +4 -0
  121. package/Types/Monitor/MonitorCriteriaInstance.ts +116 -56
  122. package/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.ts +2 -1
  123. package/Types/SecurityEvent/DetectionFindingConstants.ts +11 -0
  124. package/Types/Telemetry/LlmMetricConventions.ts +124 -0
  125. package/UI/Components/AutocompleteTextInput/AutocompleteTextInput.tsx +3 -0
  126. package/UI/Components/BulkUpdate/BulkUpdateForm.tsx +10 -1
  127. package/UI/Components/CodeEditor/CodeEditor.tsx +241 -79
  128. package/UI/Components/CodeEditor/YamlEditor.tsx +369 -0
  129. package/UI/Components/Detail/Detail.tsx +10 -0
  130. package/UI/Components/EntityDropdown/EntityDropdown.tsx +33 -0
  131. package/UI/Components/Forms/Fields/FormField.tsx +31 -0
  132. package/UI/Components/Forms/Types/Field.ts +8 -0
  133. package/UI/Components/Forms/Types/FormFieldSchemaType.ts +1 -0
  134. package/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.ts +2 -0
  135. package/UI/Components/Forms/Validation.ts +46 -0
  136. package/UI/Components/Input/Input.tsx +3 -0
  137. package/UI/Components/ModelTable/AttributeColumns.tsx +321 -0
  138. package/UI/Components/ModelTable/BaseModelTable.tsx +263 -14
  139. package/UI/Components/ModelTable/Column.ts +8 -0
  140. package/UI/Components/ModelTable/ColumnCustomizationModal.tsx +310 -0
  141. package/UI/Components/ModelTable/ColumnPreference.ts +13 -1
  142. package/UI/Components/MonitorTemplateVariables/TemplateVariablesModal.tsx +2 -2
  143. package/UI/Components/SideOver/SideOver.tsx +5 -5
  144. package/UI/Components/StatusPage/ResourceGrid.tsx +82 -2
  145. package/UI/Components/StatusPage/ResourceList.tsx +197 -38
  146. package/UI/Components/Toggle/Toggle.tsx +9 -1
  147. package/UI/Components/Types/FieldType.ts +1 -0
  148. package/Utils/NetworkDevice/DeviceReachabilityUtil.ts +73 -1
  149. package/Utils/NetworkDiscovery/ScanNameUtil.ts +181 -0
  150. package/Utils/StatusPage/ResourceExplorer.ts +258 -0
  151. package/Utils/Telemetry/LlmMetricQuery.ts +204 -0
  152. package/build/dist/Models/DatabaseModels/DetectionRule.js +57 -0
  153. package/build/dist/Models/DatabaseModels/DetectionRule.js.map +1 -1
  154. package/build/dist/Models/DatabaseModels/NetworkDeviceAutoImportRule.js +27 -7
  155. package/build/dist/Models/DatabaseModels/NetworkDeviceAutoImportRule.js.map +1 -1
  156. package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js +69 -0
  157. package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js.map +1 -1
  158. package/build/dist/Models/DatabaseModels/Project.js +76 -0
  159. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  160. package/build/dist/Models/DatabaseModels/User.js +76 -0
  161. package/build/dist/Models/DatabaseModels/User.js.map +1 -1
  162. package/build/dist/Server/API/TelemetryAPI.js +17 -0
  163. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  164. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789400000000-AddNameToNetworkDeviceDiscoveryScan.js +21 -0
  165. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789400000000-AddNameToNetworkDeviceDiscoveryScan.js.map +1 -0
  166. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789500000000-AddCampaignUtmFields.js +42 -0
  167. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789500000000-AddCampaignUtmFields.js.map +1 -0
  168. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789512000000-AddDetectionRuleDistinctCountColumns.js +22 -0
  169. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789512000000-AddDetectionRuleDistinctCountColumns.js.map +1 -0
  170. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
  171. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  172. package/build/dist/Server/Middleware/GzipRequestBody.js +224 -0
  173. package/build/dist/Server/Middleware/GzipRequestBody.js.map +1 -0
  174. package/build/dist/Server/Middleware/MultipartFormData.js +71 -2
  175. package/build/dist/Server/Middleware/MultipartFormData.js.map +1 -1
  176. package/build/dist/Server/Services/DetectionRuleService.js +49 -4
  177. package/build/dist/Server/Services/DetectionRuleService.js.map +1 -1
  178. package/build/dist/Server/Services/Index.js +204 -0
  179. package/build/dist/Server/Services/Index.js.map +1 -1
  180. package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js +50 -0
  181. package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js.map +1 -1
  182. package/build/dist/Server/Services/NetworkDeviceService.js +124 -0
  183. package/build/dist/Server/Services/NetworkDeviceService.js.map +1 -1
  184. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js +68 -13
  185. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js.map +1 -1
  186. package/build/dist/Server/Services/OnCallDutyPolicyFeedService.js +1 -1
  187. package/build/dist/Server/Services/OnCallDutyPolicyFeedService.js.map +1 -1
  188. package/build/dist/Server/Services/ProjectService.js +25 -45
  189. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  190. package/build/dist/Server/Services/SecurityEventService.js +57 -5
  191. package/build/dist/Server/Services/SecurityEventService.js.map +1 -1
  192. package/build/dist/Server/Services/StatusPageResourceService.js +140 -0
  193. package/build/dist/Server/Services/StatusPageResourceService.js.map +1 -1
  194. package/build/dist/Server/Services/TelemetryAttributeService.js +17 -0
  195. package/build/dist/Server/Services/TelemetryAttributeService.js.map +1 -1
  196. package/build/dist/Server/Services/UserService.js +2 -12
  197. package/build/dist/Server/Services/UserService.js.map +1 -1
  198. package/build/dist/Server/Types/Markdown.js +147 -61
  199. package/build/dist/Server/Types/Markdown.js.map +1 -1
  200. package/build/dist/Server/Utils/AI/Toolbox/OnCallTools.js +12 -4
  201. package/build/dist/Server/Utils/AI/Toolbox/OnCallTools.js.map +1 -1
  202. package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js +12 -1
  203. package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js.map +1 -1
  204. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +246 -9
  205. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
  206. package/build/dist/Server/Utils/Attribution.js.map +1 -1
  207. package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js +27 -7
  208. package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js.map +1 -1
  209. package/build/dist/Server/Utils/SecurityEvent/DetectionRuleEvaluator.js +42 -7
  210. package/build/dist/Server/Utils/SecurityEvent/DetectionRuleEvaluator.js.map +1 -1
  211. package/build/dist/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.js +18 -0
  212. package/build/dist/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.js.map +1 -1
  213. package/build/dist/Server/Utils/StartServer.js +12 -21
  214. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  215. package/build/dist/Server/Utils/Telemetry/LlmCostBudgetEvaluator.js +76 -5
  216. package/build/dist/Server/Utils/Telemetry/LlmCostBudgetEvaluator.js.map +1 -1
  217. package/build/dist/Server/Utils/Telemetry/LlmMetricSpend.js +102 -0
  218. package/build/dist/Server/Utils/Telemetry/LlmMetricSpend.js.map +1 -0
  219. package/build/dist/Types/Code/YamlSyntax.js +212 -0
  220. package/build/dist/Types/Code/YamlSyntax.js.map +1 -0
  221. package/build/dist/Types/Exception/ExceptionCode.js +1 -0
  222. package/build/dist/Types/Exception/ExceptionCode.js.map +1 -1
  223. package/build/dist/Types/Exception/PayloadTooLargeException.js +8 -0
  224. package/build/dist/Types/Exception/PayloadTooLargeException.js.map +1 -0
  225. package/build/dist/Types/Marketing/Attribution.js +10 -0
  226. package/build/dist/Types/Marketing/Attribution.js.map +1 -1
  227. package/build/dist/Types/Monitor/MonitorCriteriaInstance.js +112 -51
  228. package/build/dist/Types/Monitor/MonitorCriteriaInstance.js.map +1 -1
  229. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.js +2 -1
  230. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.js.map +1 -1
  231. package/build/dist/Types/SecurityEvent/DetectionFindingConstants.js +9 -0
  232. package/build/dist/Types/SecurityEvent/DetectionFindingConstants.js.map +1 -1
  233. package/build/dist/Types/Telemetry/LlmMetricConventions.js +98 -0
  234. package/build/dist/Types/Telemetry/LlmMetricConventions.js.map +1 -0
  235. package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js +1 -1
  236. package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js.map +1 -1
  237. package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js +5 -2
  238. package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js.map +1 -1
  239. package/build/dist/UI/Components/CodeEditor/CodeEditor.js +206 -76
  240. package/build/dist/UI/Components/CodeEditor/CodeEditor.js.map +1 -1
  241. package/build/dist/UI/Components/CodeEditor/YamlEditor.js +201 -0
  242. package/build/dist/UI/Components/CodeEditor/YamlEditor.js.map +1 -0
  243. package/build/dist/UI/Components/Detail/Detail.js +9 -0
  244. package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
  245. package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js +16 -1
  246. package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js.map +1 -1
  247. package/build/dist/UI/Components/Forms/Fields/FormField.js +14 -1
  248. package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
  249. package/build/dist/UI/Components/Forms/Types/Field.js.map +1 -1
  250. package/build/dist/UI/Components/Forms/Types/FormFieldSchemaType.js +1 -0
  251. package/build/dist/UI/Components/Forms/Types/FormFieldSchemaType.js.map +1 -1
  252. package/build/dist/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.js +2 -0
  253. package/build/dist/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.js.map +1 -1
  254. package/build/dist/UI/Components/Forms/Validation.js +27 -0
  255. package/build/dist/UI/Components/Forms/Validation.js.map +1 -1
  256. package/build/dist/UI/Components/Input/Input.js +1 -1
  257. package/build/dist/UI/Components/Input/Input.js.map +1 -1
  258. package/build/dist/UI/Components/ModelTable/AttributeColumns.js +159 -0
  259. package/build/dist/UI/Components/ModelTable/AttributeColumns.js.map +1 -0
  260. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +179 -17
  261. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  262. package/build/dist/UI/Components/ModelTable/ColumnCustomizationModal.js +133 -2
  263. package/build/dist/UI/Components/ModelTable/ColumnCustomizationModal.js.map +1 -1
  264. package/build/dist/UI/Components/ModelTable/ColumnPreference.js +7 -1
  265. package/build/dist/UI/Components/ModelTable/ColumnPreference.js.map +1 -1
  266. package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesModal.js +2 -2
  267. package/build/dist/UI/Components/SideOver/SideOver.js +5 -5
  268. package/build/dist/UI/Components/StatusPage/ResourceGrid.js +28 -2
  269. package/build/dist/UI/Components/StatusPage/ResourceGrid.js.map +1 -1
  270. package/build/dist/UI/Components/StatusPage/ResourceList.js +87 -20
  271. package/build/dist/UI/Components/StatusPage/ResourceList.js.map +1 -1
  272. package/build/dist/UI/Components/Toggle/Toggle.js +9 -1
  273. package/build/dist/UI/Components/Toggle/Toggle.js.map +1 -1
  274. package/build/dist/UI/Components/Types/FieldType.js +1 -0
  275. package/build/dist/UI/Components/Types/FieldType.js.map +1 -1
  276. package/build/dist/Utils/NetworkDevice/DeviceReachabilityUtil.js +37 -1
  277. package/build/dist/Utils/NetworkDevice/DeviceReachabilityUtil.js.map +1 -1
  278. package/build/dist/Utils/NetworkDiscovery/ScanNameUtil.js +120 -0
  279. package/build/dist/Utils/NetworkDiscovery/ScanNameUtil.js.map +1 -0
  280. package/build/dist/Utils/StatusPage/ResourceExplorer.js +171 -0
  281. package/build/dist/Utils/StatusPage/ResourceExplorer.js.map +1 -1
  282. package/build/dist/Utils/Telemetry/LlmMetricQuery.js +119 -0
  283. package/build/dist/Utils/Telemetry/LlmMetricQuery.js.map +1 -0
  284. package/jest.config.json +2 -0
  285. package/package.json +1 -1
  286. package/tsconfig.json +2 -1
  287. package/Tests/UI/Components/Toast.test.tsx +0 -197
  288. package/Tests/UI/Components/ToastStacking.test.tsx +0 -198
  289. package/UI/Components/Toast/Toast.tsx +0 -189
  290. package/UI/Components/Toast/ToastInit.tsx +0 -110
  291. package/build/dist/UI/Components/Toast/Toast.js +0 -123
  292. package/build/dist/UI/Components/Toast/Toast.js.map +0 -1
  293. package/build/dist/UI/Components/Toast/ToastInit.js +0 -53
  294. package/build/dist/UI/Components/Toast/ToastInit.js.map +0 -1
@@ -19,6 +19,7 @@ import LlmCostBudgetService from "../../../../Server/Services/LlmCostBudgetServi
19
19
  import MetricService from "../../../../Server/Services/MetricService";
20
20
  import SpanService from "../../../../Server/Services/SpanService";
21
21
  import TelemetryUtil from "../../../../Server/Utils/Telemetry/Telemetry";
22
+ import { LlmMetricScope } from "../../../../Utils/Telemetry/LlmMetricQuery";
22
23
  /*
23
24
  * `jest` deliberately comes from the global scope, not @jest/globals — the
24
25
  * imported value would shadow the global `jest` NAMESPACE and break the
@@ -41,6 +42,11 @@ jest.mock("../../../../Server/Services/MetricService", () => {
41
42
  __esModule: true,
42
43
  default: {
43
44
  insertJsonRows: jest.fn(),
45
+ /*
46
+ * aggregateBy backs the metric-sourced spend fallback that
47
+ * resolveSpend reaches for when the span stream is empty.
48
+ */
49
+ aggregateBy: jest.fn(),
44
50
  },
45
51
  };
46
52
  });
@@ -70,10 +76,13 @@ const mockedSpanService: { aggregateBy: MockedFn } = SpanService as unknown as {
70
76
  aggregateBy: MockedFn;
71
77
  };
72
78
 
73
- const mockedMetricService: { insertJsonRows: MockedFn } =
74
- MetricService as unknown as {
75
- insertJsonRows: MockedFn;
76
- };
79
+ const mockedMetricService: {
80
+ insertJsonRows: MockedFn;
81
+ aggregateBy: MockedFn;
82
+ } = MetricService as unknown as {
83
+ insertJsonRows: MockedFn;
84
+ aggregateBy: MockedFn;
85
+ };
77
86
 
78
87
  const mockedBudgetService: {
79
88
  findBy: MockedFn;
@@ -328,6 +337,7 @@ describe("LlmCostBudgetEvaluator.evaluateBudget — orchestration", () => {
328
337
  beforeEach(() => {
329
338
  jest.clearAllMocks();
330
339
  mockedSpanService.aggregateBy.mockResolvedValue({ data: [] } as never);
340
+ mockedMetricService.aggregateBy.mockResolvedValue({ data: [] } as never);
331
341
  mockedMetricService.insertJsonRows.mockResolvedValue(undefined as never);
332
342
  mockedBudgetService.updateOneById.mockResolvedValue(undefined as never);
333
343
  mockedTelemetryUtil.indexMetricNameServiceNameMap.mockResolvedValue(
@@ -447,6 +457,7 @@ describe("LlmCostBudgetEvaluator.evaluateBudget — orchestration", () => {
447
457
  describe("LlmCostBudgetEvaluator.evaluateAllBudgets — sweep resilience", () => {
448
458
  beforeEach(() => {
449
459
  jest.clearAllMocks();
460
+ mockedMetricService.aggregateBy.mockResolvedValue({ data: [] } as never);
450
461
  mockedMetricService.insertJsonRows.mockResolvedValue(undefined as never);
451
462
  mockedBudgetService.updateOneById.mockResolvedValue(undefined as never);
452
463
  mockedTelemetryUtil.indexMetricNameServiceNameMap.mockResolvedValue(
@@ -481,6 +492,7 @@ describe("LlmCostBudgetEvaluator.evaluateAllBudgets — sweep resilience", () =>
481
492
  await LlmCostBudgetEvaluator.evaluateAllBudgets();
482
493
 
483
494
  expect(mockedSpanService.aggregateBy).not.toHaveBeenCalled();
495
+ expect(mockedMetricService.aggregateBy).not.toHaveBeenCalled();
484
496
  });
485
497
 
486
498
  test("only enabled budgets are loaded", async () => {
@@ -495,3 +507,355 @@ describe("LlmCostBudgetEvaluator.evaluateAllBudgets — sweep resilience", () =>
495
507
  expect(findArg.query["isEnabled"]).toBe(true);
496
508
  });
497
509
  });
510
+
511
+ describe("LlmCostBudgetEvaluator.buildMetricScope", () => {
512
+ const DAY_START: Date = new Date(Date.UTC(2026, 7, 22, 0, 0, 0));
513
+
514
+ test("carries the project and the day-so-far window", () => {
515
+ const budget: LlmCostBudget = makeBudget();
516
+
517
+ const scope: LlmMetricScope = LlmCostBudgetEvaluator.buildMetricScope({
518
+ budget,
519
+ dayStart: DAY_START,
520
+ now: NOW,
521
+ });
522
+
523
+ expect(scope.projectId).toBe(budget.projectId);
524
+ expect(scope.startTime).toBe(DAY_START);
525
+ expect(scope.endTime).toBe(NOW);
526
+ });
527
+
528
+ test("leaves an unscoped budget unscoped — it must see every datapoint", () => {
529
+ const scope: LlmMetricScope = LlmCostBudgetEvaluator.buildMetricScope({
530
+ budget: makeBudget(),
531
+ dayStart: DAY_START,
532
+ now: NOW,
533
+ });
534
+
535
+ expect(scope.serviceId).toBeUndefined();
536
+ expect(scope.llmSystem).toBeUndefined();
537
+ expect(scope.llmModel).toBeUndefined();
538
+ });
539
+
540
+ test("mirrors the budget's service / provider / model scoping", () => {
541
+ const serviceId: ObjectID = ObjectID.generate();
542
+
543
+ const scope: LlmMetricScope = LlmCostBudgetEvaluator.buildMetricScope({
544
+ budget: makeBudget({
545
+ serviceId: serviceId,
546
+ llmSystem: "anthropic",
547
+ llmModel: "claude-sonnet-4",
548
+ }),
549
+ dayStart: DAY_START,
550
+ now: NOW,
551
+ });
552
+
553
+ expect(scope.serviceId).toBe(serviceId);
554
+ expect(scope.llmSystem).toBe("anthropic");
555
+ expect(scope.llmModel).toBe("claude-sonnet-4");
556
+ });
557
+
558
+ /*
559
+ * The span query and the metric query must narrow to the same slice of
560
+ * traffic, or the fallback would silently measure something else.
561
+ */
562
+ test("agrees with buildSpanQuery on the service scope", () => {
563
+ const serviceId: ObjectID = ObjectID.generate();
564
+ const budget: LlmCostBudget = makeBudget({ serviceId: serviceId });
565
+
566
+ const spanQuery: Record<string, unknown> =
567
+ LlmCostBudgetEvaluator.buildSpanQuery({
568
+ budget,
569
+ dayStart: DAY_START,
570
+ now: NOW,
571
+ }) as unknown as Record<string, unknown>;
572
+
573
+ const scope: LlmMetricScope = LlmCostBudgetEvaluator.buildMetricScope({
574
+ budget,
575
+ dayStart: DAY_START,
576
+ now: NOW,
577
+ });
578
+
579
+ expect(spanQuery["primaryEntityId"]).toBe(scope.serviceId);
580
+ });
581
+ });
582
+
583
+ describe("LlmCostBudgetEvaluator.resolveSpend — source selection", () => {
584
+ const DAY_START: Date = new Date(Date.UTC(2026, 7, 22, 0, 0, 0));
585
+
586
+ beforeEach(() => {
587
+ jest.clearAllMocks();
588
+ mockedSpanService.aggregateBy.mockResolvedValue({ data: [] } as never);
589
+ mockedMetricService.aggregateBy.mockResolvedValue({ data: [] } as never);
590
+ });
591
+
592
+ function mockSpanSpend(value: number): void {
593
+ mockedSpanService.aggregateBy.mockResolvedValue({
594
+ data: [{ timestamp: NOW, value: value }],
595
+ } as never);
596
+ }
597
+
598
+ function mockMetricSpend(value: number): void {
599
+ mockedMetricService.aggregateBy.mockResolvedValue({
600
+ data: [{ timestamp: NOW, value: value }],
601
+ } as never);
602
+ }
603
+
604
+ test("uses spans when the span stream reported spend", async () => {
605
+ mockSpanSpend(42);
606
+ mockMetricSpend(999);
607
+
608
+ await expect(
609
+ LlmCostBudgetEvaluator.resolveSpend({
610
+ budget: makeBudget(),
611
+ dayStart: DAY_START,
612
+ now: NOW,
613
+ }),
614
+ ).resolves.toEqual({ spendInUSD: 42, source: "spans" });
615
+ });
616
+
617
+ /*
618
+ * The double-counting guard, and the reason this is a fallback rather than a
619
+ * sum: an SDK emitting both signals would otherwise report 1041 here, and a
620
+ * budget that fires on spend that never happened is worse than one that
621
+ * misses.
622
+ */
623
+ test("never adds metric spend on top of span spend", async () => {
624
+ mockSpanSpend(42);
625
+ mockMetricSpend(999);
626
+
627
+ const resolved: { spendInUSD: number } =
628
+ await LlmCostBudgetEvaluator.resolveSpend({
629
+ budget: makeBudget(),
630
+ dayStart: DAY_START,
631
+ now: NOW,
632
+ });
633
+
634
+ expect(resolved.spendInUSD).toBe(42);
635
+ expect(resolved.spendInUSD).not.toBe(1041);
636
+ });
637
+
638
+ test("does not query metrics at all when spans answered", async () => {
639
+ mockSpanSpend(42);
640
+
641
+ await LlmCostBudgetEvaluator.resolveSpend({
642
+ budget: makeBudget(),
643
+ dayStart: DAY_START,
644
+ now: NOW,
645
+ });
646
+
647
+ expect(mockedMetricService.aggregateBy).not.toHaveBeenCalled();
648
+ });
649
+
650
+ test("falls back to metrics when the span stream is empty", async () => {
651
+ mockMetricSpend(17.5);
652
+
653
+ await expect(
654
+ LlmCostBudgetEvaluator.resolveSpend({
655
+ budget: makeBudget(),
656
+ dayStart: DAY_START,
657
+ now: NOW,
658
+ }),
659
+ ).resolves.toEqual({ spendInUSD: 17.5, source: "metrics" });
660
+ });
661
+
662
+ test("reports source 'none' when both streams are empty", async () => {
663
+ await expect(
664
+ LlmCostBudgetEvaluator.resolveSpend({
665
+ budget: makeBudget(),
666
+ dayStart: DAY_START,
667
+ now: NOW,
668
+ }),
669
+ ).resolves.toEqual({ spendInUSD: 0, source: "none" });
670
+ });
671
+
672
+ test("treats a zero-valued span bucket as no span spend", async () => {
673
+ mockSpanSpend(0);
674
+ mockMetricSpend(8);
675
+
676
+ await expect(
677
+ LlmCostBudgetEvaluator.resolveSpend({
678
+ budget: makeBudget(),
679
+ dayStart: DAY_START,
680
+ now: NOW,
681
+ }),
682
+ ).resolves.toEqual({ spendInUSD: 8, source: "metrics" });
683
+ });
684
+
685
+ test("a zero metric total still resolves to 'none', not 'metrics'", async () => {
686
+ mockMetricSpend(0);
687
+
688
+ await expect(
689
+ LlmCostBudgetEvaluator.resolveSpend({
690
+ budget: makeBudget(),
691
+ dayStart: DAY_START,
692
+ now: NOW,
693
+ }),
694
+ ).resolves.toEqual({ spendInUSD: 0, source: "none" });
695
+ });
696
+
697
+ /*
698
+ * Returning 0 on a failed metric query would silently suppress a real
699
+ * breach — the same reasoning behind the span aggregate's
700
+ * timeoutOverflowMode: 'throw'. evaluateAllBudgets isolates the failure.
701
+ */
702
+ test("propagates a metric-query failure rather than reporting zero spend", async () => {
703
+ mockedMetricService.aggregateBy.mockRejectedValue(
704
+ new Error("clickhouse timeout") as never,
705
+ );
706
+
707
+ await expect(
708
+ LlmCostBudgetEvaluator.resolveSpend({
709
+ budget: makeBudget(),
710
+ dayStart: DAY_START,
711
+ now: NOW,
712
+ }),
713
+ ).rejects.toThrow("clickhouse timeout");
714
+ });
715
+
716
+ test("propagates a span-query failure without reaching the fallback", async () => {
717
+ mockedSpanService.aggregateBy.mockRejectedValue(
718
+ new Error("span boom") as never,
719
+ );
720
+
721
+ await expect(
722
+ LlmCostBudgetEvaluator.resolveSpend({
723
+ budget: makeBudget(),
724
+ dayStart: DAY_START,
725
+ now: NOW,
726
+ }),
727
+ ).rejects.toThrow("span boom");
728
+
729
+ expect(mockedMetricService.aggregateBy).not.toHaveBeenCalled();
730
+ });
731
+
732
+ test("sums multiple metric buckets", async () => {
733
+ mockedMetricService.aggregateBy.mockResolvedValue({
734
+ data: [
735
+ { timestamp: NOW, value: 1.5 },
736
+ { timestamp: NOW, value: 2.5 },
737
+ ],
738
+ } as never);
739
+
740
+ await expect(
741
+ LlmCostBudgetEvaluator.resolveSpend({
742
+ budget: makeBudget(),
743
+ dayStart: DAY_START,
744
+ now: NOW,
745
+ }),
746
+ ).resolves.toEqual({ spendInUSD: 4, source: "metrics" });
747
+ });
748
+
749
+ test("passes the budget's scope through to the metric query", async () => {
750
+ const serviceId: ObjectID = ObjectID.generate();
751
+ mockMetricSpend(3);
752
+
753
+ await LlmCostBudgetEvaluator.resolveSpend({
754
+ budget: makeBudget({ serviceId: serviceId, llmSystem: "openai" }),
755
+ dayStart: DAY_START,
756
+ now: NOW,
757
+ });
758
+
759
+ const aggregateArg: { query: Record<string, unknown> } = mockedMetricService
760
+ .aggregateBy.mock.calls[0]![0] as {
761
+ query: Record<string, unknown>;
762
+ };
763
+
764
+ expect(aggregateArg.query["primaryEntityId"]).toBe(serviceId);
765
+ expect(aggregateArg.query["attributes"]).toEqual({
766
+ "gen_ai.system": "openai",
767
+ });
768
+ });
769
+ });
770
+
771
+ describe("LlmCostBudgetEvaluator.evaluateBudget — metric-sourced spend", () => {
772
+ beforeEach(() => {
773
+ jest.clearAllMocks();
774
+ mockedSpanService.aggregateBy.mockResolvedValue({ data: [] } as never);
775
+ mockedMetricService.aggregateBy.mockResolvedValue({ data: [] } as never);
776
+ mockedMetricService.insertJsonRows.mockResolvedValue(undefined as never);
777
+ mockedBudgetService.updateOneById.mockResolvedValue(undefined as never);
778
+ mockedTelemetryUtil.indexMetricNameServiceNameMap.mockResolvedValue(
779
+ undefined as never,
780
+ );
781
+ });
782
+
783
+ test("a metrics-only project gets its spend stamped on the budget row", async () => {
784
+ mockedMetricService.aggregateBy.mockResolvedValue({
785
+ data: [{ timestamp: NOW, value: 60 }],
786
+ } as never);
787
+
788
+ const budget: LlmCostBudget = makeBudget({ dailyBudgetInUSD: 100 });
789
+
790
+ await LlmCostBudgetEvaluator.evaluateBudget({ budget, now: NOW });
791
+
792
+ const updateArg: { data: Record<string, unknown> } = mockedBudgetService
793
+ .updateOneById.mock.calls[0]![0] as {
794
+ data: Record<string, unknown>;
795
+ };
796
+
797
+ expect(updateArg.data["currentDaySpendInUSD"]).toBe(60);
798
+ });
799
+
800
+ test("a metrics-only project publishes the same budget series as a span-backed one", async () => {
801
+ mockedMetricService.aggregateBy.mockResolvedValue({
802
+ data: [{ timestamp: NOW, value: 80 }],
803
+ } as never);
804
+
805
+ await LlmCostBudgetEvaluator.evaluateBudget({
806
+ budget: makeBudget({ dailyBudgetInUSD: 100 }),
807
+ now: NOW,
808
+ });
809
+
810
+ const rows: Array<Record<string, unknown>> = mockedMetricService
811
+ .insertJsonRows.mock.calls[0]![0] as Array<Record<string, unknown>>;
812
+
813
+ expect(rows).toHaveLength(2);
814
+ expect(rows[0]!["name"]).toBe(LLM_BUDGET_SPEND_METRIC_NAME);
815
+ expect(rows[0]!["value"]).toBe(80);
816
+ expect(rows[1]!["name"]).toBe(LLM_BUDGET_PERCENT_METRIC_NAME);
817
+ expect(rows[1]!["value"]).toBe(80);
818
+ });
819
+
820
+ /*
821
+ * The published series must stay byte-identical to what a span-backed budget
822
+ * emits — existing monitors filter on oneuptime.llm.budget.id and must not
823
+ * have to learn about the source.
824
+ */
825
+ test("the published series carries no source-specific attribute", async () => {
826
+ mockedMetricService.aggregateBy.mockResolvedValue({
827
+ data: [{ timestamp: NOW, value: 5 }],
828
+ } as never);
829
+
830
+ const budget: LlmCostBudget = makeBudget();
831
+
832
+ await LlmCostBudgetEvaluator.evaluateBudget({ budget, now: NOW });
833
+
834
+ const rows: Array<Record<string, unknown>> = mockedMetricService
835
+ .insertJsonRows.mock.calls[0]![0] as Array<Record<string, unknown>>;
836
+
837
+ const attributes: Record<string, string> = rows[0]!["attributes"] as Record<
838
+ string,
839
+ string
840
+ >;
841
+
842
+ expect(Object.keys(attributes).sort()).toEqual(
843
+ [LLM_BUDGET_ID_ATTRIBUTE, LLM_BUDGET_NAME_ATTRIBUTE].sort(),
844
+ );
845
+ });
846
+
847
+ test("an idle project still stamps zero and publishes its series", async () => {
848
+ await LlmCostBudgetEvaluator.evaluateBudget({
849
+ budget: makeBudget(),
850
+ now: NOW,
851
+ });
852
+
853
+ const updateArg: { data: Record<string, unknown> } = mockedBudgetService
854
+ .updateOneById.mock.calls[0]![0] as {
855
+ data: Record<string, unknown>;
856
+ };
857
+
858
+ expect(updateArg.data["currentDaySpendInUSD"]).toBe(0);
859
+ expect(mockedMetricService.insertJsonRows).toHaveBeenCalledTimes(1);
860
+ });
861
+ });
@@ -0,0 +1,245 @@
1
+ import Metric from "../../../../Models/AnalyticsModels/Metric";
2
+ import AggregationInterval from "../../../../Types/BaseDatabase/AggregationInterval";
3
+ import AggregationType from "../../../../Types/BaseDatabase/AggregationType";
4
+ import Includes from "../../../../Types/BaseDatabase/Includes";
5
+ import ObjectID from "../../../../Types/ObjectID";
6
+ import {
7
+ LlmCostMetricNames,
8
+ LlmTokenTypeAttributeKeys,
9
+ LlmTokenUsageMetricNames,
10
+ } from "../../../../Types/Telemetry/LlmMetricConventions";
11
+ import AggregateBy from "../../../../Server/Types/AnalyticsDatabase/AggregateBy";
12
+ import MetricService from "../../../../Server/Services/MetricService";
13
+ import LlmMetricSpend from "../../../../Server/Utils/Telemetry/LlmMetricSpend";
14
+ import { LlmMetricScope } from "../../../../Utils/Telemetry/LlmMetricQuery";
15
+ /*
16
+ * `jest` deliberately comes from the global scope, not @jest/globals — the
17
+ * imported value would shadow the global `jest` NAMESPACE and break the
18
+ * `jest.Mock` type annotations below (house convention).
19
+ */
20
+ import { beforeEach, describe, expect, test } from "@jest/globals";
21
+
22
+ jest.mock("../../../../Server/Services/MetricService", () => {
23
+ return {
24
+ __esModule: true,
25
+ default: {
26
+ aggregateBy: jest.fn(),
27
+ },
28
+ };
29
+ });
30
+
31
+ type MockedFn = jest.Mock;
32
+
33
+ const mockedMetricService: { aggregateBy: MockedFn } =
34
+ MetricService as unknown as {
35
+ aggregateBy: MockedFn;
36
+ };
37
+
38
+ const PROJECT_ID: ObjectID = ObjectID.generate();
39
+ const SERVICE_ID: ObjectID = ObjectID.generate();
40
+ const START: Date = new Date("2026-08-26T00:00:00.000Z");
41
+ const END: Date = new Date("2026-08-26T09:30:00.000Z");
42
+
43
+ function scope(overrides?: Partial<LlmMetricScope>): LlmMetricScope {
44
+ return {
45
+ projectId: PROJECT_ID,
46
+ startTime: START,
47
+ endTime: END,
48
+ ...overrides,
49
+ };
50
+ }
51
+
52
+ describe("LlmMetricSpend", () => {
53
+ beforeEach(() => {
54
+ mockedMetricService.aggregateBy.mockReset();
55
+ });
56
+
57
+ describe("buildCostAggregateBy", () => {
58
+ test("sums the value column over the whole window", () => {
59
+ const aggregate: AggregateBy<Metric> =
60
+ LlmMetricSpend.buildCostAggregateBy(scope());
61
+
62
+ expect(aggregate.aggregationType).toBe(AggregationType.Sum);
63
+ expect(aggregate.aggregateColumnName).toBe("value");
64
+ expect(aggregate.aggregationTimestampColumnName).toBe("time");
65
+ expect(aggregate.aggregationInterval).toBe(AggregationInterval.Total);
66
+ expect(aggregate.startTimestamp).toBe(START);
67
+ expect(aggregate.endTimestamp).toBe(END);
68
+ });
69
+
70
+ /*
71
+ * The figure feeds budget monitors. The default 'break' overflow mode
72
+ * returns silently-partial sums on a query timeout, which would understate
73
+ * spend and suppress a real breach.
74
+ */
75
+ test("fails loud on query timeout rather than returning a partial sum", () => {
76
+ expect(
77
+ LlmMetricSpend.buildCostAggregateBy(scope()).timeoutOverflowMode,
78
+ ).toBe("throw");
79
+ });
80
+
81
+ test("runs with root props — the worker has no user context", () => {
82
+ expect(LlmMetricSpend.buildCostAggregateBy(scope()).props).toEqual({
83
+ isRoot: true,
84
+ });
85
+ });
86
+
87
+ test("selects the cost metric names", () => {
88
+ const query: Record<string, unknown> =
89
+ LlmMetricSpend.buildCostAggregateBy(scope()).query as unknown as Record<
90
+ string,
91
+ unknown
92
+ >;
93
+
94
+ expect((query["name"] as Includes).values).toEqual(LlmCostMetricNames);
95
+ });
96
+
97
+ test("is not grouped — cost needs one scalar total", () => {
98
+ expect(
99
+ LlmMetricSpend.buildCostAggregateBy(scope()).groupByAttributeKeys,
100
+ ).toBeUndefined();
101
+ });
102
+
103
+ test("carries the caller's scoping into the query", () => {
104
+ const query: Record<string, unknown> =
105
+ LlmMetricSpend.buildCostAggregateBy(
106
+ scope({ serviceId: SERVICE_ID, llmSystem: "openai" }),
107
+ ).query as unknown as Record<string, unknown>;
108
+
109
+ expect(query["primaryEntityId"]).toBe(SERVICE_ID);
110
+ expect(query["attributes"]).toEqual({ "gen_ai.system": "openai" });
111
+ });
112
+ });
113
+
114
+ describe("buildTokenAggregateBy", () => {
115
+ test("selects the token usage metric names", () => {
116
+ const query: Record<string, unknown> =
117
+ LlmMetricSpend.buildTokenAggregateBy(scope())
118
+ .query as unknown as Record<string, unknown>;
119
+
120
+ expect((query["name"] as Includes).values).toEqual(
121
+ LlmTokenUsageMetricNames,
122
+ );
123
+ });
124
+
125
+ /*
126
+ * Grouping by every candidate token-type key in ONE query is what keeps
127
+ * the fold both complete and free of double counting — a query per key
128
+ * would double-count any datapoint carrying two of them.
129
+ */
130
+ test("groups by every recognized token type attribute key", () => {
131
+ expect(
132
+ LlmMetricSpend.buildTokenAggregateBy(scope()).groupByAttributeKeys,
133
+ ).toEqual(LlmTokenTypeAttributeKeys);
134
+ });
135
+
136
+ test("does not alias the shared conventions array", () => {
137
+ const keys: Array<string> = LlmMetricSpend.buildTokenAggregateBy(scope())
138
+ .groupByAttributeKeys as Array<string>;
139
+
140
+ keys.push("mutation");
141
+
142
+ expect(LlmTokenTypeAttributeKeys).not.toContain("mutation");
143
+ });
144
+
145
+ test("fails loud on query timeout", () => {
146
+ expect(
147
+ LlmMetricSpend.buildTokenAggregateBy(scope()).timeoutOverflowMode,
148
+ ).toBe("throw");
149
+ });
150
+ });
151
+
152
+ describe("getCostInUSD", () => {
153
+ test("sums the returned buckets", async () => {
154
+ mockedMetricService.aggregateBy.mockResolvedValue({
155
+ data: [
156
+ { timestamp: START, value: 1.25 },
157
+ { timestamp: START, value: 2.75 },
158
+ ],
159
+ });
160
+
161
+ await expect(LlmMetricSpend.getCostInUSD(scope())).resolves.toBeCloseTo(
162
+ 4,
163
+ );
164
+ });
165
+
166
+ test("returns 0 when nothing matched", async () => {
167
+ mockedMetricService.aggregateBy.mockResolvedValue({ data: [] });
168
+
169
+ await expect(LlmMetricSpend.getCostInUSD(scope())).resolves.toBe(0);
170
+ });
171
+
172
+ test("returns 0 when the result has no data field", async () => {
173
+ mockedMetricService.aggregateBy.mockResolvedValue({});
174
+
175
+ await expect(LlmMetricSpend.getCostInUSD(scope())).resolves.toBe(0);
176
+ });
177
+
178
+ test("propagates a query failure instead of reporting zero spend", async () => {
179
+ mockedMetricService.aggregateBy.mockRejectedValue(
180
+ new Error("clickhouse timeout"),
181
+ );
182
+
183
+ await expect(LlmMetricSpend.getCostInUSD(scope())).rejects.toThrow(
184
+ "clickhouse timeout",
185
+ );
186
+ });
187
+
188
+ test("issues exactly one aggregate", async () => {
189
+ mockedMetricService.aggregateBy.mockResolvedValue({ data: [] });
190
+
191
+ await LlmMetricSpend.getCostInUSD(scope());
192
+
193
+ expect(mockedMetricService.aggregateBy).toHaveBeenCalledTimes(1);
194
+ });
195
+ });
196
+
197
+ describe("getTokenTotals", () => {
198
+ test("folds grouped rows into input and output totals", async () => {
199
+ mockedMetricService.aggregateBy.mockResolvedValue({
200
+ data: [
201
+ {
202
+ timestamp: START,
203
+ value: 900,
204
+ attributes: { "gen_ai.token.type": "input" },
205
+ },
206
+ {
207
+ timestamp: START,
208
+ value: 300,
209
+ attributes: { "gen_ai.token.type": "output" },
210
+ },
211
+ ],
212
+ });
213
+
214
+ await expect(LlmMetricSpend.getTokenTotals(scope())).resolves.toEqual({
215
+ inputTokens: 900,
216
+ outputTokens: 300,
217
+ });
218
+ });
219
+
220
+ test("returns zeros when nothing matched", async () => {
221
+ mockedMetricService.aggregateBy.mockResolvedValue({ data: [] });
222
+
223
+ await expect(LlmMetricSpend.getTokenTotals(scope())).resolves.toEqual({
224
+ inputTokens: 0,
225
+ outputTokens: 0,
226
+ });
227
+ });
228
+
229
+ test("propagates a query failure", async () => {
230
+ mockedMetricService.aggregateBy.mockRejectedValue(new Error("boom"));
231
+
232
+ await expect(LlmMetricSpend.getTokenTotals(scope())).rejects.toThrow(
233
+ "boom",
234
+ );
235
+ });
236
+
237
+ test("issues exactly one aggregate for both directions", async () => {
238
+ mockedMetricService.aggregateBy.mockResolvedValue({ data: [] });
239
+
240
+ await LlmMetricSpend.getTokenTotals(scope());
241
+
242
+ expect(mockedMetricService.aggregateBy).toHaveBeenCalledTimes(1);
243
+ });
244
+ });
245
+ });