@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.
- package/Models/DatabaseModels/DetectionRule.ts +55 -0
- package/Models/DatabaseModels/NetworkDeviceAutoImportRule.ts +27 -7
- package/Models/DatabaseModels/NetworkDeviceDiscoveryScan.ts +67 -0
- package/Models/DatabaseModels/Project.ts +68 -0
- package/Models/DatabaseModels/User.ts +68 -0
- package/Server/API/TelemetryAPI.ts +29 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1789400000000-AddNameToNetworkDeviceDiscoveryScan.ts +28 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1789500000000-AddCampaignUtmFields.ts +49 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1789512000000-AddDetectionRuleDistinctCountColumns.ts +33 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
- package/Server/Middleware/GzipRequestBody.ts +311 -0
- package/Server/Middleware/MultipartFormData.ts +87 -3
- package/Server/Services/DetectionRuleService.ts +73 -6
- package/Server/Services/Index.ts +205 -0
- package/Server/Services/NetworkDeviceDiscoveryScanService.ts +59 -0
- package/Server/Services/NetworkDeviceService.ts +148 -0
- package/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.ts +65 -0
- package/Server/Services/OnCallDutyPolicyFeedService.ts +1 -1
- package/Server/Services/ProjectService.ts +29 -19
- package/Server/Services/SecurityEventService.ts +90 -13
- package/Server/Services/StatusPageResourceService.ts +223 -0
- package/Server/Services/TelemetryAttributeService.ts +17 -0
- package/Server/Services/UserService.ts +4 -6
- package/Server/Types/Markdown.ts +161 -67
- package/Server/Utils/AI/Toolbox/OnCallTools.ts +12 -4
- package/Server/Utils/AnalyticsDatabase/Statement.ts +13 -1
- package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +297 -6
- package/Server/Utils/Attribution.ts +4 -0
- package/Server/Utils/Marketing/MarketingEventUtil.ts +45 -7
- package/Server/Utils/SecurityEvent/DetectionRuleEvaluator.ts +55 -3
- package/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.ts +21 -0
- package/Server/Utils/StartServer.ts +12 -32
- package/Server/Utils/Telemetry/LlmCostBudgetEvaluator.ts +101 -4
- package/Server/Utils/Telemetry/LlmMetricSpend.ts +130 -0
- package/Server/Views/Partials/AnalyticsConsent.ejs +159 -102
- package/Server/Views/Partials/Head.ejs +2 -1
- package/Tests/App/Dashboard/AdminUserNotificationMethodsPage.test.tsx +776 -38
- package/Tests/App/Dashboard/CorrelateFilterBuilder.test.tsx +359 -0
- package/Tests/App/Dashboard/CorrelateGraph.test.tsx +865 -0
- package/Tests/App/Dashboard/CorrelationGraph.test.ts +346 -0
- package/Tests/App/Dashboard/CriteriaFilterConditionDropdown.test.tsx +256 -0
- package/Tests/App/Dashboard/CriteriaFilterDefaults.test.ts +738 -0
- package/Tests/App/Dashboard/CriteriaFilterMonitorTypeChange.test.ts +1122 -0
- package/Tests/App/Dashboard/DiscoveryScanWizardValidation.test.tsx +283 -0
- package/Tests/App/Dashboard/ExternalStatusPageCriteriaFilter.test.ts +407 -0
- package/Tests/App/Dashboard/MetricSeriesInvestigateMenu.test.tsx +5 -14
- package/Tests/App/Dashboard/MonitorCriteriaActionToggles.test.tsx +426 -0
- package/Tests/App/Dashboard/MonitorStepsCriteriaAlignmentWiring.test.tsx +352 -0
- package/Tests/App/Dashboard/OnCallLayerCardOverrides.test.tsx +278 -0
- package/Tests/App/Dashboard/OnCallLayerShiftPreviewOverrides.test.ts +237 -0
- package/Tests/App/Dashboard/OnCallReadinessSurfaces.test.tsx +2 -2
- package/Tests/App/Dashboard/OnCallSchedulePreviewOverrides.test.tsx +460 -0
- package/Tests/App/Dashboard/SecurityEventAttributeUtil.test.ts +171 -0
- package/Tests/App/Dashboard/SecurityEventCorrelation.test.ts +857 -0
- package/Tests/App/Dashboard/SecurityEventDetailObservables.test.tsx +194 -0
- package/Tests/App/Dashboard/SecurityEventsDetectionRulesPage.test.tsx +124 -0
- package/Tests/App/Dashboard/SecurityEventsMonitorStepForm.test.tsx +79 -19
- package/Tests/App/Dashboard/SecurityEventsTableColumns.test.tsx +382 -0
- package/Tests/App/StatusPage/BulkAddStatusPageMonitors.test.ts +216 -0
- package/Tests/App/StatusPage/BulkAddStatusPageMonitorsModal.test.tsx +590 -11
- package/Tests/App/StatusPage/StatusPageResourceExplorer.test.tsx +757 -0
- package/Tests/Models/DiscoveryScanNameColumn.test.ts +293 -0
- package/Tests/Server/API/SecurityEventAttributesAPI.test.ts +564 -0
- package/Tests/Server/Infrastructure/Postgres/SchemaMigrationsOrdering.test.ts +120 -4
- package/Tests/Server/Middleware/GzipRequestBody.test.ts +725 -0
- package/Tests/Server/Middleware/MultipartFormData.test.ts +298 -0
- package/Tests/Server/Services/AddDetectionRuleDistinctCountColumnsMigration.test.ts +311 -0
- package/Tests/Server/Services/DetectionRuleService.test.ts +134 -2
- package/Tests/Server/Services/DiscoveryScanClaimHookFreeSafety.test.ts +7 -1
- package/Tests/Server/Services/FeedRetentionConsistency.test.ts +131 -0
- package/Tests/Server/Services/NetworkDeviceDiscoveryScanService.test.ts +194 -0
- package/Tests/Server/Services/NetworkDeviceMonitorStatusStamp.test.ts +800 -0
- package/Tests/Server/Services/OnCallScheduleAttachRosterRefresh.test.ts +244 -0
- package/Tests/Server/Services/SecurityEventService.test.ts +376 -0
- package/Tests/Server/Services/StatusPageResourceDuplicates.test.ts +703 -0
- package/Tests/Server/Services/TelemetryAttributeServiceSecurityEvent.test.ts +256 -0
- package/Tests/Server/Types/Workflow/Components/ComponentRegistryParity.test.ts +417 -0
- package/Tests/Server/Utils/AnalyticsDatabase/StatementGeneratorWhereOperators.test.ts +677 -0
- package/Tests/Server/Utils/Marketing/MarketingEventUtil.test.ts +43 -0
- package/Tests/Server/Utils/Monitor/Criteria/ProfileMonitorCriteria.test.ts +15 -11
- package/Tests/Server/Utils/SecurityEvent/DetectionRuleEvaluator.test.ts +369 -8
- package/Tests/Server/Utils/SecurityEvent/SigmaClickhouseCompiler.test.ts +35 -0
- package/Tests/Server/Utils/Telemetry/LlmCostBudgetEvaluator.test.ts +368 -4
- package/Tests/Server/Utils/Telemetry/LlmMetricSpend.test.ts +245 -0
- package/Tests/Server/Views/AnalyticsConsentPartial.test.ts +48 -5
- package/Tests/Types/Code/YamlSyntax.test.ts +396 -0
- package/Tests/Types/Monitor/MetricAndProfileDefaultCriteria.test.ts +957 -0
- package/Tests/Types/Monitor/MonitorCriteriaActionValidation.test.ts +598 -0
- package/Tests/Types/Monitor/MonitorCriteriaInstance.test.ts +49 -0
- package/Tests/Types/Monitor/Recommendation/MonitorRecommendationNotificationMode.test.ts +6 -6
- package/Tests/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.test.ts +4 -4
- package/Tests/Types/Monitor/Recommendation/MonitorRecommendationUtil.test.ts +4 -3
- package/Tests/Types/Telemetry/LlmMetricConventions.test.ts +193 -0
- package/Tests/UI/Components/AiInvestigationSettingsCard.test.tsx +0 -12
- package/Tests/UI/Components/CardModelDetailEdit.test.tsx +1 -17
- package/Tests/UI/Components/CodeEditor.test.tsx +474 -10
- package/Tests/UI/Components/Detail/YamlField.test.tsx +105 -0
- package/Tests/UI/Components/EntityDropdownLabelsBulkAdd.test.tsx +319 -0
- package/Tests/UI/Components/Forms/FormFieldYaml.test.tsx +231 -0
- package/Tests/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.test.ts +1 -0
- package/Tests/UI/Components/Forms/ValidationYAML.test.ts +234 -0
- package/Tests/UI/Components/ModelTable/AttributeColumns.test.tsx +573 -0
- package/Tests/UI/Components/ModelTable/BaseModelTableAttributeColumns.test.tsx +824 -0
- package/Tests/UI/Components/ModelTable/ColumnCustomizationModalAddableColumns.test.tsx +707 -0
- package/Tests/UI/Components/SideOver.test.tsx +1 -1
- package/Tests/UI/Components/StatusPage/ResourceGridSelection.test.tsx +294 -0
- package/Tests/UI/Components/StatusPage/ResourceListSelection.test.tsx +436 -0
- package/Tests/UI/Components/TableBulkCsvExport.test.tsx +6 -2
- package/Tests/UI/Components/Toggle.test.tsx +25 -0
- package/Tests/UI/Components/YamlEditor.test.tsx +847 -0
- package/Tests/Utils/NetworkDevice/DeviceReachabilityUtil.test.ts +252 -0
- package/Tests/Utils/NetworkDiscovery/DiscoveredHostUtil.test.ts +221 -0
- package/Tests/Utils/NetworkDiscovery/ScanNameUtil.test.ts +312 -0
- package/Tests/Utils/StatusPage/ResourceExplorer.test.ts +468 -0
- package/Tests/Utils/Telemetry/LlmMetricQuery.test.ts +472 -0
- package/Types/Code/YamlSyntax.ts +396 -0
- package/Types/Exception/ExceptionCode.ts +1 -0
- package/Types/Exception/PayloadTooLargeException.ts +8 -0
- package/Types/Marketing/Attribution.ts +10 -0
- package/Types/Marketing/MarketingEvent.ts +4 -0
- package/Types/Monitor/MonitorCriteriaInstance.ts +116 -56
- package/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.ts +2 -1
- package/Types/SecurityEvent/DetectionFindingConstants.ts +11 -0
- package/Types/Telemetry/LlmMetricConventions.ts +124 -0
- package/UI/Components/AutocompleteTextInput/AutocompleteTextInput.tsx +3 -0
- package/UI/Components/BulkUpdate/BulkUpdateForm.tsx +10 -1
- package/UI/Components/CodeEditor/CodeEditor.tsx +241 -79
- package/UI/Components/CodeEditor/YamlEditor.tsx +369 -0
- package/UI/Components/Detail/Detail.tsx +10 -0
- package/UI/Components/EntityDropdown/EntityDropdown.tsx +33 -0
- package/UI/Components/Forms/Fields/FormField.tsx +31 -0
- package/UI/Components/Forms/Types/Field.ts +8 -0
- package/UI/Components/Forms/Types/FormFieldSchemaType.ts +1 -0
- package/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.ts +2 -0
- package/UI/Components/Forms/Validation.ts +46 -0
- package/UI/Components/Input/Input.tsx +3 -0
- package/UI/Components/ModelTable/AttributeColumns.tsx +321 -0
- package/UI/Components/ModelTable/BaseModelTable.tsx +263 -14
- package/UI/Components/ModelTable/Column.ts +8 -0
- package/UI/Components/ModelTable/ColumnCustomizationModal.tsx +310 -0
- package/UI/Components/ModelTable/ColumnPreference.ts +13 -1
- package/UI/Components/MonitorTemplateVariables/TemplateVariablesModal.tsx +2 -2
- package/UI/Components/SideOver/SideOver.tsx +5 -5
- package/UI/Components/StatusPage/ResourceGrid.tsx +82 -2
- package/UI/Components/StatusPage/ResourceList.tsx +197 -38
- package/UI/Components/Toggle/Toggle.tsx +9 -1
- package/UI/Components/Types/FieldType.ts +1 -0
- package/Utils/NetworkDevice/DeviceReachabilityUtil.ts +73 -1
- package/Utils/NetworkDiscovery/ScanNameUtil.ts +181 -0
- package/Utils/StatusPage/ResourceExplorer.ts +258 -0
- package/Utils/Telemetry/LlmMetricQuery.ts +204 -0
- package/build/dist/Models/DatabaseModels/DetectionRule.js +57 -0
- package/build/dist/Models/DatabaseModels/DetectionRule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDeviceAutoImportRule.js +27 -7
- package/build/dist/Models/DatabaseModels/NetworkDeviceAutoImportRule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js +69 -0
- package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Project.js +76 -0
- package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
- package/build/dist/Models/DatabaseModels/User.js +76 -0
- package/build/dist/Models/DatabaseModels/User.js.map +1 -1
- package/build/dist/Server/API/TelemetryAPI.js +17 -0
- package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789400000000-AddNameToNetworkDeviceDiscoveryScan.js +21 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789400000000-AddNameToNetworkDeviceDiscoveryScan.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789500000000-AddCampaignUtmFields.js +42 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789500000000-AddCampaignUtmFields.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789512000000-AddDetectionRuleDistinctCountColumns.js +22 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789512000000-AddDetectionRuleDistinctCountColumns.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Middleware/GzipRequestBody.js +224 -0
- package/build/dist/Server/Middleware/GzipRequestBody.js.map +1 -0
- package/build/dist/Server/Middleware/MultipartFormData.js +71 -2
- package/build/dist/Server/Middleware/MultipartFormData.js.map +1 -1
- package/build/dist/Server/Services/DetectionRuleService.js +49 -4
- package/build/dist/Server/Services/DetectionRuleService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +204 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js +50 -0
- package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceService.js +124 -0
- package/build/dist/Server/Services/NetworkDeviceService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js +68 -13
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyFeedService.js +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyFeedService.js.map +1 -1
- package/build/dist/Server/Services/ProjectService.js +25 -45
- package/build/dist/Server/Services/ProjectService.js.map +1 -1
- package/build/dist/Server/Services/SecurityEventService.js +57 -5
- package/build/dist/Server/Services/SecurityEventService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageResourceService.js +140 -0
- package/build/dist/Server/Services/StatusPageResourceService.js.map +1 -1
- package/build/dist/Server/Services/TelemetryAttributeService.js +17 -0
- package/build/dist/Server/Services/TelemetryAttributeService.js.map +1 -1
- package/build/dist/Server/Services/UserService.js +2 -12
- package/build/dist/Server/Services/UserService.js.map +1 -1
- package/build/dist/Server/Types/Markdown.js +147 -61
- package/build/dist/Server/Types/Markdown.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/OnCallTools.js +12 -4
- package/build/dist/Server/Utils/AI/Toolbox/OnCallTools.js.map +1 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js +12 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js.map +1 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +246 -9
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
- package/build/dist/Server/Utils/Attribution.js.map +1 -1
- package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js +27 -7
- package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js.map +1 -1
- package/build/dist/Server/Utils/SecurityEvent/DetectionRuleEvaluator.js +42 -7
- package/build/dist/Server/Utils/SecurityEvent/DetectionRuleEvaluator.js.map +1 -1
- package/build/dist/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.js +18 -0
- package/build/dist/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.js.map +1 -1
- package/build/dist/Server/Utils/StartServer.js +12 -21
- package/build/dist/Server/Utils/StartServer.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/LlmCostBudgetEvaluator.js +76 -5
- package/build/dist/Server/Utils/Telemetry/LlmCostBudgetEvaluator.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/LlmMetricSpend.js +102 -0
- package/build/dist/Server/Utils/Telemetry/LlmMetricSpend.js.map +1 -0
- package/build/dist/Types/Code/YamlSyntax.js +212 -0
- package/build/dist/Types/Code/YamlSyntax.js.map +1 -0
- package/build/dist/Types/Exception/ExceptionCode.js +1 -0
- package/build/dist/Types/Exception/ExceptionCode.js.map +1 -1
- package/build/dist/Types/Exception/PayloadTooLargeException.js +8 -0
- package/build/dist/Types/Exception/PayloadTooLargeException.js.map +1 -0
- package/build/dist/Types/Marketing/Attribution.js +10 -0
- package/build/dist/Types/Marketing/Attribution.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorCriteriaInstance.js +112 -51
- package/build/dist/Types/Monitor/MonitorCriteriaInstance.js.map +1 -1
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.js +2 -1
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.js.map +1 -1
- package/build/dist/Types/SecurityEvent/DetectionFindingConstants.js +9 -0
- package/build/dist/Types/SecurityEvent/DetectionFindingConstants.js.map +1 -1
- package/build/dist/Types/Telemetry/LlmMetricConventions.js +98 -0
- package/build/dist/Types/Telemetry/LlmMetricConventions.js.map +1 -0
- package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js +1 -1
- package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js.map +1 -1
- package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js +5 -2
- package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js.map +1 -1
- package/build/dist/UI/Components/CodeEditor/CodeEditor.js +206 -76
- package/build/dist/UI/Components/CodeEditor/CodeEditor.js.map +1 -1
- package/build/dist/UI/Components/CodeEditor/YamlEditor.js +201 -0
- package/build/dist/UI/Components/CodeEditor/YamlEditor.js.map +1 -0
- package/build/dist/UI/Components/Detail/Detail.js +9 -0
- package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
- package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js +16 -1
- package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js.map +1 -1
- package/build/dist/UI/Components/Forms/Fields/FormField.js +14 -1
- package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
- package/build/dist/UI/Components/Forms/Types/Field.js.map +1 -1
- package/build/dist/UI/Components/Forms/Types/FormFieldSchemaType.js +1 -0
- package/build/dist/UI/Components/Forms/Types/FormFieldSchemaType.js.map +1 -1
- package/build/dist/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.js +2 -0
- package/build/dist/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.js.map +1 -1
- package/build/dist/UI/Components/Forms/Validation.js +27 -0
- package/build/dist/UI/Components/Forms/Validation.js.map +1 -1
- package/build/dist/UI/Components/Input/Input.js +1 -1
- package/build/dist/UI/Components/Input/Input.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/AttributeColumns.js +159 -0
- package/build/dist/UI/Components/ModelTable/AttributeColumns.js.map +1 -0
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +179 -17
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/ColumnCustomizationModal.js +133 -2
- package/build/dist/UI/Components/ModelTable/ColumnCustomizationModal.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/ColumnPreference.js +7 -1
- package/build/dist/UI/Components/ModelTable/ColumnPreference.js.map +1 -1
- package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesModal.js +2 -2
- package/build/dist/UI/Components/SideOver/SideOver.js +5 -5
- package/build/dist/UI/Components/StatusPage/ResourceGrid.js +28 -2
- package/build/dist/UI/Components/StatusPage/ResourceGrid.js.map +1 -1
- package/build/dist/UI/Components/StatusPage/ResourceList.js +87 -20
- package/build/dist/UI/Components/StatusPage/ResourceList.js.map +1 -1
- package/build/dist/UI/Components/Toggle/Toggle.js +9 -1
- package/build/dist/UI/Components/Toggle/Toggle.js.map +1 -1
- package/build/dist/UI/Components/Types/FieldType.js +1 -0
- package/build/dist/UI/Components/Types/FieldType.js.map +1 -1
- package/build/dist/Utils/NetworkDevice/DeviceReachabilityUtil.js +37 -1
- package/build/dist/Utils/NetworkDevice/DeviceReachabilityUtil.js.map +1 -1
- package/build/dist/Utils/NetworkDiscovery/ScanNameUtil.js +120 -0
- package/build/dist/Utils/NetworkDiscovery/ScanNameUtil.js.map +1 -0
- package/build/dist/Utils/StatusPage/ResourceExplorer.js +171 -0
- package/build/dist/Utils/StatusPage/ResourceExplorer.js.map +1 -1
- package/build/dist/Utils/Telemetry/LlmMetricQuery.js +119 -0
- package/build/dist/Utils/Telemetry/LlmMetricQuery.js.map +1 -0
- package/jest.config.json +2 -0
- package/package.json +1 -1
- package/tsconfig.json +2 -1
- package/Tests/UI/Components/Toast.test.tsx +0 -197
- package/Tests/UI/Components/ToastStacking.test.tsx +0 -198
- package/UI/Components/Toast/Toast.tsx +0 -189
- package/UI/Components/Toast/ToastInit.tsx +0 -110
- package/build/dist/UI/Components/Toast/Toast.js +0 -123
- package/build/dist/UI/Components/Toast/Toast.js.map +0 -1
- package/build/dist/UI/Components/Toast/ToastInit.js +0 -53
- 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: {
|
|
74
|
-
|
|
75
|
-
|
|
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
|
+
});
|