@oneuptime/common 12.0.23 → 12.0.25
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/AnalyticsModels/Span.ts +101 -0
- 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/BaseAPI.ts +0 -24
- package/Server/API/SlackAPI.ts +0 -2
- 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/Middleware/SlackAuthorization.ts +96 -18
- 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 +181 -0
- package/Server/Utils/Telemetry/LlmSpan.ts +46 -0
- package/Server/Utils/Workspace/Slack/Actions/Auth.ts +0 -12
- 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/LlmCallsTableIdentity.test.tsx +322 -0
- package/Tests/App/Dashboard/LlmOverview.test.tsx +335 -0
- package/Tests/App/Dashboard/LlmSpanDisplay.test.ts +391 -0
- package/Tests/App/Dashboard/LlmUsageBreakdown.test.tsx +1007 -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/BaseAPI.test.ts +41 -0
- package/Tests/Server/API/BaseAPIUpdatePayloadValidation.test.ts +9 -16
- 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/Middleware/SlackAuthorization.test.ts +262 -5
- 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 +387 -4
- package/Tests/Server/Utils/Telemetry/LlmMetricSpend.test.ts +386 -0
- package/Tests/Server/Utils/Telemetry/LlmSpan.test.ts +804 -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 +584 -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 +770 -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/LlmConventions.ts +255 -0
- package/Types/Telemetry/LlmMetricConventions.ts +329 -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 +287 -0
- package/build/dist/Models/AnalyticsModels/Span.js +89 -0
- package/build/dist/Models/AnalyticsModels/Span.js.map +1 -1
- 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/BaseAPI.js +3 -19
- package/build/dist/Server/API/BaseAPI.js.map +1 -1
- package/build/dist/Server/API/SlackAPI.js +0 -2
- package/build/dist/Server/API/SlackAPI.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/Middleware/SlackAuthorization.js +58 -7
- package/build/dist/Server/Middleware/SlackAuthorization.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 +151 -0
- package/build/dist/Server/Utils/Telemetry/LlmMetricSpend.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/LlmSpan.js +24 -1
- package/build/dist/Server/Utils/Telemetry/LlmSpan.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/Slack/Actions/Auth.js +0 -10
- package/build/dist/Server/Utils/Workspace/Slack/Actions/Auth.js.map +1 -1
- 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/LlmConventions.js +236 -0
- package/build/dist/Types/Telemetry/LlmConventions.js.map +1 -1
- package/build/dist/Types/Telemetry/LlmMetricConventions.js +290 -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 +175 -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
|
@@ -0,0 +1,770 @@
|
|
|
1
|
+
import Metric from "../../../Models/AnalyticsModels/Metric";
|
|
2
|
+
import AggregatedModel from "../../../Types/BaseDatabase/AggregatedModel";
|
|
3
|
+
import InBetween from "../../../Types/BaseDatabase/InBetween";
|
|
4
|
+
import Includes from "../../../Types/BaseDatabase/Includes";
|
|
5
|
+
import Query from "../../../Types/BaseDatabase/Query";
|
|
6
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
7
|
+
import {
|
|
8
|
+
LlmCostMetricNames,
|
|
9
|
+
LlmMicroUsdCostMetricNames,
|
|
10
|
+
LlmTokenUsageMetricNames,
|
|
11
|
+
} from "../../../Types/Telemetry/LlmMetricConventions";
|
|
12
|
+
import LlmMetricQuery, {
|
|
13
|
+
LlmMetricScope,
|
|
14
|
+
LlmMetricTokenTotals,
|
|
15
|
+
METRIC_MODEL_ATTRIBUTE_KEY,
|
|
16
|
+
METRIC_SYSTEM_ATTRIBUTE_KEY,
|
|
17
|
+
METRIC_TEAM_ATTRIBUTE_KEY,
|
|
18
|
+
METRIC_USER_ATTRIBUTE_KEY,
|
|
19
|
+
} from "../../../Utils/Telemetry/LlmMetricQuery";
|
|
20
|
+
import { describe, expect, test } from "@jest/globals";
|
|
21
|
+
|
|
22
|
+
const PROJECT_ID: ObjectID = ObjectID.generate();
|
|
23
|
+
const SERVICE_ID: ObjectID = ObjectID.generate();
|
|
24
|
+
const START: Date = new Date("2026-08-26T00:00:00.000Z");
|
|
25
|
+
const END: Date = new Date("2026-08-26T13:45:00.000Z");
|
|
26
|
+
|
|
27
|
+
function scope(overrides?: Partial<LlmMetricScope>): LlmMetricScope {
|
|
28
|
+
return {
|
|
29
|
+
projectId: PROJECT_ID,
|
|
30
|
+
startTime: START,
|
|
31
|
+
endTime: END,
|
|
32
|
+
...overrides,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function row(value: unknown, attributes?: Record<string, unknown>): unknown {
|
|
37
|
+
return {
|
|
38
|
+
timestamp: START,
|
|
39
|
+
value: value,
|
|
40
|
+
...(attributes ? { attributes: attributes } : {}),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
describe("LlmMetricQuery", () => {
|
|
45
|
+
describe("buildBaseQuery", () => {
|
|
46
|
+
test("scopes to the project", () => {
|
|
47
|
+
const query: Record<string, unknown> = LlmMetricQuery.buildBaseQuery({
|
|
48
|
+
scope: scope(),
|
|
49
|
+
metricNames: ["a"],
|
|
50
|
+
}) as unknown as Record<string, unknown>;
|
|
51
|
+
|
|
52
|
+
expect(query["projectId"]).toBe(PROJECT_ID);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("matches any of the supplied metric names", () => {
|
|
56
|
+
const query: Record<string, unknown> = LlmMetricQuery.buildBaseQuery({
|
|
57
|
+
scope: scope(),
|
|
58
|
+
metricNames: ["a", "b", "c"],
|
|
59
|
+
}) as unknown as Record<string, unknown>;
|
|
60
|
+
|
|
61
|
+
expect(query["name"]).toBeInstanceOf(Includes);
|
|
62
|
+
expect((query["name"] as Includes).values).toEqual(["a", "b", "c"]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
/*
|
|
66
|
+
* The regression this guards: AggregateBy's startTimestamp/endTimestamp
|
|
67
|
+
* only choose the bucket grid. Without a `time` predicate in the query the
|
|
68
|
+
* aggregate scans the metric's whole retention and silently returns spend
|
|
69
|
+
* from outside the window.
|
|
70
|
+
*/
|
|
71
|
+
test("bounds rows with an explicit time predicate", () => {
|
|
72
|
+
const query: Record<string, unknown> = LlmMetricQuery.buildBaseQuery({
|
|
73
|
+
scope: scope(),
|
|
74
|
+
metricNames: ["a"],
|
|
75
|
+
}) as unknown as Record<string, unknown>;
|
|
76
|
+
|
|
77
|
+
const time: InBetween<Date> = query["time"] as InBetween<Date>;
|
|
78
|
+
|
|
79
|
+
expect(time).toBeInstanceOf(InBetween);
|
|
80
|
+
expect(time.startValue).toBe(START);
|
|
81
|
+
expect(time.endValue).toBe(END);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("omits service and attribute filters when unscoped", () => {
|
|
85
|
+
const query: Record<string, unknown> = LlmMetricQuery.buildBaseQuery({
|
|
86
|
+
scope: scope(),
|
|
87
|
+
metricNames: ["a"],
|
|
88
|
+
}) as unknown as Record<string, unknown>;
|
|
89
|
+
|
|
90
|
+
expect(query).not.toHaveProperty("primaryEntityId");
|
|
91
|
+
expect(query).not.toHaveProperty("attributes");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("narrows by telemetry service via primaryEntityId", () => {
|
|
95
|
+
const query: Record<string, unknown> = LlmMetricQuery.buildBaseQuery({
|
|
96
|
+
scope: scope({ serviceId: SERVICE_ID }),
|
|
97
|
+
metricNames: ["a"],
|
|
98
|
+
}) as unknown as Record<string, unknown>;
|
|
99
|
+
|
|
100
|
+
expect(query["primaryEntityId"]).toBe(SERVICE_ID);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("narrows by provider using the semconv attribute key", () => {
|
|
104
|
+
const query: Record<string, unknown> = LlmMetricQuery.buildBaseQuery({
|
|
105
|
+
scope: scope({ llmSystem: "openai" }),
|
|
106
|
+
metricNames: ["a"],
|
|
107
|
+
}) as unknown as Record<string, unknown>;
|
|
108
|
+
|
|
109
|
+
expect(query["attributes"]).toEqual({
|
|
110
|
+
[METRIC_SYSTEM_ATTRIBUTE_KEY]: "openai",
|
|
111
|
+
});
|
|
112
|
+
expect(METRIC_SYSTEM_ATTRIBUTE_KEY).toBe("gen_ai.system");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("narrows by model using the semconv attribute key", () => {
|
|
116
|
+
const query: Record<string, unknown> = LlmMetricQuery.buildBaseQuery({
|
|
117
|
+
scope: scope({ llmModel: "gpt-4o" }),
|
|
118
|
+
metricNames: ["a"],
|
|
119
|
+
}) as unknown as Record<string, unknown>;
|
|
120
|
+
|
|
121
|
+
expect(query["attributes"]).toEqual({
|
|
122
|
+
[METRIC_MODEL_ATTRIBUTE_KEY]: "gpt-4o",
|
|
123
|
+
});
|
|
124
|
+
expect(METRIC_MODEL_ATTRIBUTE_KEY).toBe("gen_ai.request.model");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("combines every scoping dimension at once", () => {
|
|
128
|
+
const query: Record<string, unknown> = LlmMetricQuery.buildBaseQuery({
|
|
129
|
+
scope: scope({
|
|
130
|
+
serviceId: SERVICE_ID,
|
|
131
|
+
llmSystem: "anthropic",
|
|
132
|
+
llmModel: "claude-sonnet-4",
|
|
133
|
+
}),
|
|
134
|
+
metricNames: ["a"],
|
|
135
|
+
}) as unknown as Record<string, unknown>;
|
|
136
|
+
|
|
137
|
+
expect(query["primaryEntityId"]).toBe(SERVICE_ID);
|
|
138
|
+
expect(query["attributes"]).toEqual({
|
|
139
|
+
[METRIC_SYSTEM_ATTRIBUTE_KEY]: "anthropic",
|
|
140
|
+
[METRIC_MODEL_ATTRIBUTE_KEY]: "claude-sonnet-4",
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("treats an empty-string scope value as unscoped", () => {
|
|
145
|
+
const query: Record<string, unknown> = LlmMetricQuery.buildBaseQuery({
|
|
146
|
+
scope: scope({ llmSystem: "", llmModel: "" }),
|
|
147
|
+
metricNames: ["a"],
|
|
148
|
+
}) as unknown as Record<string, unknown>;
|
|
149
|
+
|
|
150
|
+
expect(query).not.toHaveProperty("attributes");
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe("buildCostQuery / buildTokenQuery", () => {
|
|
155
|
+
test("cost query matches exactly the cost metric names", () => {
|
|
156
|
+
const query: Record<string, unknown> = LlmMetricQuery.buildCostQuery(
|
|
157
|
+
scope(),
|
|
158
|
+
) as unknown as Record<string, unknown>;
|
|
159
|
+
|
|
160
|
+
expect((query["name"] as Includes).values).toEqual(LlmCostMetricNames);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("token query matches exactly the token metric names", () => {
|
|
164
|
+
const query: Record<string, unknown> = LlmMetricQuery.buildTokenQuery(
|
|
165
|
+
scope(),
|
|
166
|
+
) as unknown as Record<string, unknown>;
|
|
167
|
+
|
|
168
|
+
expect((query["name"] as Includes).values).toEqual(
|
|
169
|
+
LlmTokenUsageMetricNames,
|
|
170
|
+
);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("the two queries never select the same metric names", () => {
|
|
174
|
+
const costNames: Array<string> = (
|
|
175
|
+
LlmMetricQuery.buildCostQuery(scope()) as unknown as Record<
|
|
176
|
+
string,
|
|
177
|
+
unknown
|
|
178
|
+
>
|
|
179
|
+
)["name"] as unknown as Array<string>;
|
|
180
|
+
|
|
181
|
+
const cost: Includes = (
|
|
182
|
+
LlmMetricQuery.buildCostQuery(scope()) as unknown as Record<
|
|
183
|
+
string,
|
|
184
|
+
unknown
|
|
185
|
+
>
|
|
186
|
+
)["name"] as Includes;
|
|
187
|
+
|
|
188
|
+
const token: Includes = (
|
|
189
|
+
LlmMetricQuery.buildTokenQuery(scope()) as unknown as Record<
|
|
190
|
+
string,
|
|
191
|
+
unknown
|
|
192
|
+
>
|
|
193
|
+
)["name"] as Includes;
|
|
194
|
+
|
|
195
|
+
expect(costNames).toBeDefined();
|
|
196
|
+
|
|
197
|
+
const overlap: Array<string> = (cost.values as Array<string>).filter(
|
|
198
|
+
(name: string) => {
|
|
199
|
+
return (token.values as Array<string>).includes(name);
|
|
200
|
+
},
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
expect(overlap).toEqual([]);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("both carry the caller's scoping", () => {
|
|
207
|
+
const scoped: LlmMetricScope = scope({
|
|
208
|
+
serviceId: SERVICE_ID,
|
|
209
|
+
llmSystem: "openai",
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
for (const query of [
|
|
213
|
+
LlmMetricQuery.buildCostQuery(scoped),
|
|
214
|
+
LlmMetricQuery.buildTokenQuery(scoped),
|
|
215
|
+
] as Array<Query<Metric>>) {
|
|
216
|
+
const record: Record<string, unknown> = query as unknown as Record<
|
|
217
|
+
string,
|
|
218
|
+
unknown
|
|
219
|
+
>;
|
|
220
|
+
expect(record["primaryEntityId"]).toBe(SERVICE_ID);
|
|
221
|
+
expect(record["attributes"]).toEqual({
|
|
222
|
+
[METRIC_SYSTEM_ATTRIBUTE_KEY]: "openai",
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
describe("sumAggregatedRows", () => {
|
|
229
|
+
test("sums the bucket values", () => {
|
|
230
|
+
expect(
|
|
231
|
+
LlmMetricQuery.sumAggregatedRows([
|
|
232
|
+
row(1.5),
|
|
233
|
+
row(2.25),
|
|
234
|
+
row(0.25),
|
|
235
|
+
] as Array<AggregatedModel>),
|
|
236
|
+
).toBeCloseTo(4);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test("returns 0 for an empty result", () => {
|
|
240
|
+
expect(LlmMetricQuery.sumAggregatedRows([])).toBe(0);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("returns 0 for undefined and null", () => {
|
|
244
|
+
expect(LlmMetricQuery.sumAggregatedRows(undefined)).toBe(0);
|
|
245
|
+
expect(LlmMetricQuery.sumAggregatedRows(null)).toBe(0);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test("returns 0 for a non-array", () => {
|
|
249
|
+
expect(
|
|
250
|
+
LlmMetricQuery.sumAggregatedRows(
|
|
251
|
+
"nope" as unknown as Array<AggregatedModel>,
|
|
252
|
+
),
|
|
253
|
+
).toBe(0);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test("treats a missing value as zero", () => {
|
|
257
|
+
expect(
|
|
258
|
+
LlmMetricQuery.sumAggregatedRows([
|
|
259
|
+
{ timestamp: START } as unknown as AggregatedModel,
|
|
260
|
+
row(3),
|
|
261
|
+
] as Array<AggregatedModel>),
|
|
262
|
+
).toBe(3);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test("coerces numeric strings", () => {
|
|
266
|
+
expect(
|
|
267
|
+
LlmMetricQuery.sumAggregatedRows([
|
|
268
|
+
row("2.5"),
|
|
269
|
+
row("1.5"),
|
|
270
|
+
] as Array<AggregatedModel>),
|
|
271
|
+
).toBeCloseTo(4);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
/*
|
|
275
|
+
* These figures feed budget monitors. One malformed row must not turn a
|
|
276
|
+
* budget's spend into NaN, which would compare false against every
|
|
277
|
+
* threshold and silently disable the alert.
|
|
278
|
+
*/
|
|
279
|
+
test("drops NaN rows instead of poisoning the total", () => {
|
|
280
|
+
expect(
|
|
281
|
+
LlmMetricQuery.sumAggregatedRows([
|
|
282
|
+
row(5),
|
|
283
|
+
row("not-a-number"),
|
|
284
|
+
row(5),
|
|
285
|
+
] as Array<AggregatedModel>),
|
|
286
|
+
).toBe(10);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test("drops Infinity rows instead of poisoning the total", () => {
|
|
290
|
+
expect(
|
|
291
|
+
LlmMetricQuery.sumAggregatedRows([
|
|
292
|
+
row(5),
|
|
293
|
+
row(Infinity),
|
|
294
|
+
row(-Infinity),
|
|
295
|
+
row(5),
|
|
296
|
+
] as Array<AggregatedModel>),
|
|
297
|
+
).toBe(10);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test("survives null entries inside the array", () => {
|
|
301
|
+
expect(
|
|
302
|
+
LlmMetricQuery.sumAggregatedRows([
|
|
303
|
+
row(2),
|
|
304
|
+
null as unknown as AggregatedModel,
|
|
305
|
+
row(3),
|
|
306
|
+
] as Array<AggregatedModel>),
|
|
307
|
+
).toBe(5);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
test("preserves negative values, which a delta counter can legitimately emit", () => {
|
|
311
|
+
expect(
|
|
312
|
+
LlmMetricQuery.sumAggregatedRows([
|
|
313
|
+
row(10),
|
|
314
|
+
row(-4),
|
|
315
|
+
] as Array<AggregatedModel>),
|
|
316
|
+
).toBe(6);
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
describe("reduceTokenRows", () => {
|
|
321
|
+
test("splits rows by the semconv token type attribute", () => {
|
|
322
|
+
const totals: LlmMetricTokenTotals = LlmMetricQuery.reduceTokenRows([
|
|
323
|
+
row(100, { "gen_ai.token.type": "input" }),
|
|
324
|
+
row(40, { "gen_ai.token.type": "output" }),
|
|
325
|
+
] as Array<AggregatedModel>);
|
|
326
|
+
|
|
327
|
+
expect(totals).toEqual({ inputTokens: 100, outputTokens: 40 });
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
test("accumulates repeated rows of the same direction", () => {
|
|
331
|
+
const totals: LlmMetricTokenTotals = LlmMetricQuery.reduceTokenRows([
|
|
332
|
+
row(10, { "gen_ai.token.type": "input" }),
|
|
333
|
+
row(15, { "gen_ai.token.type": "input" }),
|
|
334
|
+
row(1, { "gen_ai.token.type": "output" }),
|
|
335
|
+
] as Array<AggregatedModel>);
|
|
336
|
+
|
|
337
|
+
expect(totals).toEqual({ inputTokens: 25, outputTokens: 1 });
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test("accepts the pre-convention prompt/completion spellings", () => {
|
|
341
|
+
const totals: LlmMetricTokenTotals = LlmMetricQuery.reduceTokenRows([
|
|
342
|
+
row(7, { "gen_ai.token.type": "prompt" }),
|
|
343
|
+
row(3, { "gen_ai.token.type": "completion" }),
|
|
344
|
+
] as Array<AggregatedModel>);
|
|
345
|
+
|
|
346
|
+
expect(totals).toEqual({ inputTokens: 7, outputTokens: 3 });
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
test("accepts the alternate llm.token.type attribute key", () => {
|
|
350
|
+
const totals: LlmMetricTokenTotals = LlmMetricQuery.reduceTokenRows([
|
|
351
|
+
row(9, { "llm.token.type": "input" }),
|
|
352
|
+
row(4, { "llm.token.type": "output" }),
|
|
353
|
+
] as Array<AggregatedModel>);
|
|
354
|
+
|
|
355
|
+
expect(totals).toEqual({ inputTokens: 9, outputTokens: 4 });
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
test("is case and whitespace insensitive", () => {
|
|
359
|
+
const totals: LlmMetricTokenTotals = LlmMetricQuery.reduceTokenRows([
|
|
360
|
+
row(5, { "gen_ai.token.type": " INPUT " }),
|
|
361
|
+
row(6, { "gen_ai.token.type": "Output" }),
|
|
362
|
+
] as Array<AggregatedModel>);
|
|
363
|
+
|
|
364
|
+
expect(totals).toEqual({ inputTokens: 5, outputTokens: 6 });
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
/*
|
|
368
|
+
* The double-counting guard. A row carrying two recognized token-type keys
|
|
369
|
+
* is counted once, against the preferred (semconv) key.
|
|
370
|
+
*/
|
|
371
|
+
test("counts a row once when it carries two recognized token type keys", () => {
|
|
372
|
+
const totals: LlmMetricTokenTotals = LlmMetricQuery.reduceTokenRows([
|
|
373
|
+
row(50, {
|
|
374
|
+
"gen_ai.token.type": "input",
|
|
375
|
+
"llm.token.type": "input",
|
|
376
|
+
}),
|
|
377
|
+
] as Array<AggregatedModel>);
|
|
378
|
+
|
|
379
|
+
expect(totals).toEqual({ inputTokens: 50, outputTokens: 0 });
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
test("prefers the semconv key when the two keys disagree", () => {
|
|
383
|
+
const totals: LlmMetricTokenTotals = LlmMetricQuery.reduceTokenRows([
|
|
384
|
+
row(50, {
|
|
385
|
+
"gen_ai.token.type": "output",
|
|
386
|
+
"llm.token.type": "input",
|
|
387
|
+
}),
|
|
388
|
+
] as Array<AggregatedModel>);
|
|
389
|
+
|
|
390
|
+
expect(totals).toEqual({ inputTokens: 0, outputTokens: 50 });
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
test("falls through to the alternate key when the semconv value is unrecognized", () => {
|
|
394
|
+
const totals: LlmMetricTokenTotals = LlmMetricQuery.reduceTokenRows([
|
|
395
|
+
row(12, {
|
|
396
|
+
"gen_ai.token.type": "cache_read",
|
|
397
|
+
"llm.token.type": "input",
|
|
398
|
+
}),
|
|
399
|
+
] as Array<AggregatedModel>);
|
|
400
|
+
|
|
401
|
+
expect(totals).toEqual({ inputTokens: 12, outputTokens: 0 });
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
/*
|
|
405
|
+
* Cache tokens are real counts but are neither input nor output in the
|
|
406
|
+
* sense the span columns use. Folding them in would inflate the
|
|
407
|
+
* metric-sourced totals against the span-sourced ones they stand in for.
|
|
408
|
+
*/
|
|
409
|
+
test("drops cache token kinds", () => {
|
|
410
|
+
const totals: LlmMetricTokenTotals = LlmMetricQuery.reduceTokenRows([
|
|
411
|
+
row(100, { "gen_ai.token.type": "input" }),
|
|
412
|
+
row(9999, { "gen_ai.token.type": "cache_read" }),
|
|
413
|
+
row(8888, { "gen_ai.token.type": "cache_creation" }),
|
|
414
|
+
] as Array<AggregatedModel>);
|
|
415
|
+
|
|
416
|
+
expect(totals).toEqual({ inputTokens: 100, outputTokens: 0 });
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
test("drops rows with no token type attribute at all", () => {
|
|
420
|
+
const totals: LlmMetricTokenTotals = LlmMetricQuery.reduceTokenRows([
|
|
421
|
+
row(100),
|
|
422
|
+
row(50, {}),
|
|
423
|
+
row(25, { "some.other.attribute": "input" }),
|
|
424
|
+
] as Array<AggregatedModel>);
|
|
425
|
+
|
|
426
|
+
expect(totals).toEqual({ inputTokens: 0, outputTokens: 0 });
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
test("drops NaN and Infinity values", () => {
|
|
430
|
+
const totals: LlmMetricTokenTotals = LlmMetricQuery.reduceTokenRows([
|
|
431
|
+
row(10, { "gen_ai.token.type": "input" }),
|
|
432
|
+
row("bad", { "gen_ai.token.type": "input" }),
|
|
433
|
+
row(Infinity, { "gen_ai.token.type": "output" }),
|
|
434
|
+
row(4, { "gen_ai.token.type": "output" }),
|
|
435
|
+
] as Array<AggregatedModel>);
|
|
436
|
+
|
|
437
|
+
expect(totals).toEqual({ inputTokens: 10, outputTokens: 4 });
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
test("returns zeros for empty, undefined, null and non-array input", () => {
|
|
441
|
+
const zero: LlmMetricTokenTotals = { inputTokens: 0, outputTokens: 0 };
|
|
442
|
+
|
|
443
|
+
expect(LlmMetricQuery.reduceTokenRows([])).toEqual(zero);
|
|
444
|
+
expect(LlmMetricQuery.reduceTokenRows(undefined)).toEqual(zero);
|
|
445
|
+
expect(LlmMetricQuery.reduceTokenRows(null)).toEqual(zero);
|
|
446
|
+
expect(
|
|
447
|
+
LlmMetricQuery.reduceTokenRows(
|
|
448
|
+
"nope" as unknown as Array<AggregatedModel>,
|
|
449
|
+
),
|
|
450
|
+
).toEqual(zero);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
test("survives null entries inside the array", () => {
|
|
454
|
+
const totals: LlmMetricTokenTotals = LlmMetricQuery.reduceTokenRows([
|
|
455
|
+
row(3, { "gen_ai.token.type": "input" }),
|
|
456
|
+
null as unknown as AggregatedModel,
|
|
457
|
+
row(2, { "gen_ai.token.type": "output" }),
|
|
458
|
+
] as Array<AggregatedModel>);
|
|
459
|
+
|
|
460
|
+
expect(totals).toEqual({ inputTokens: 3, outputTokens: 2 });
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
test("tolerates a non-object attributes field", () => {
|
|
464
|
+
const totals: LlmMetricTokenTotals = LlmMetricQuery.reduceTokenRows([
|
|
465
|
+
{
|
|
466
|
+
timestamp: START,
|
|
467
|
+
value: 5,
|
|
468
|
+
attributes: "not-an-object",
|
|
469
|
+
} as unknown as AggregatedModel,
|
|
470
|
+
]);
|
|
471
|
+
|
|
472
|
+
expect(totals).toEqual({ inputTokens: 0, outputTokens: 0 });
|
|
473
|
+
});
|
|
474
|
+
});
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
/*
|
|
478
|
+
* Employee / team scoping of metric-sourced spend.
|
|
479
|
+
*
|
|
480
|
+
* The bug class: a scoping dimension that silently does nothing. If a filter
|
|
481
|
+
* is dropped, mis-keyed, or written to the wrong attribute name, the query
|
|
482
|
+
* quietly returns the WHOLE project's spend under one person's name — a
|
|
483
|
+
* number that renders perfectly and attributes an org-wide bill to one
|
|
484
|
+
* engineer. Nothing about that failure looks like a failure.
|
|
485
|
+
*/
|
|
486
|
+
describe("LlmMetricQuery — employee and team scoping", () => {
|
|
487
|
+
test("narrows by employee email using the primary identity key", () => {
|
|
488
|
+
const query: Record<string, unknown> = LlmMetricQuery.buildBaseQuery({
|
|
489
|
+
scope: scope({ llmUserEmail: "ada@example.com" }),
|
|
490
|
+
metricNames: ["a"],
|
|
491
|
+
}) as unknown as Record<string, unknown>;
|
|
492
|
+
|
|
493
|
+
expect(query["attributes"]).toEqual({
|
|
494
|
+
[METRIC_USER_ATTRIBUTE_KEY]: "ada@example.com",
|
|
495
|
+
});
|
|
496
|
+
expect(METRIC_USER_ATTRIBUTE_KEY).toBe("user.email");
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
test("narrows by employee id on the same primary identity key", () => {
|
|
500
|
+
/*
|
|
501
|
+
* The metric stream has ONE identity attribute, unlike the span side
|
|
502
|
+
* where id and email are separate columns — so an id-scoped query filters
|
|
503
|
+
* the same key.
|
|
504
|
+
*/
|
|
505
|
+
const query: Record<string, unknown> = LlmMetricQuery.buildBaseQuery({
|
|
506
|
+
scope: scope({ llmUserId: "acct-9f2" }),
|
|
507
|
+
metricNames: ["a"],
|
|
508
|
+
}) as unknown as Record<string, unknown>;
|
|
509
|
+
|
|
510
|
+
expect(query["attributes"]).toEqual({
|
|
511
|
+
[METRIC_USER_ATTRIBUTE_KEY]: "acct-9f2",
|
|
512
|
+
});
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
test("email wins over id when a caller supplies both", () => {
|
|
516
|
+
/*
|
|
517
|
+
* Both map to one key, so one has to win deterministically rather than by
|
|
518
|
+
* object-literal order. Email is the preferred spelling of the list.
|
|
519
|
+
*/
|
|
520
|
+
const query: Record<string, unknown> = LlmMetricQuery.buildBaseQuery({
|
|
521
|
+
scope: scope({ llmUserEmail: "ada@example.com", llmUserId: "acct-9f2" }),
|
|
522
|
+
metricNames: ["a"],
|
|
523
|
+
}) as unknown as Record<string, unknown>;
|
|
524
|
+
|
|
525
|
+
expect(query["attributes"]).toEqual({
|
|
526
|
+
[METRIC_USER_ATTRIBUTE_KEY]: "ada@example.com",
|
|
527
|
+
});
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
test("narrows by team using the primary team key", () => {
|
|
531
|
+
const query: Record<string, unknown> = LlmMetricQuery.buildBaseQuery({
|
|
532
|
+
scope: scope({ llmTeam: "platform" }),
|
|
533
|
+
metricNames: ["a"],
|
|
534
|
+
}) as unknown as Record<string, unknown>;
|
|
535
|
+
|
|
536
|
+
expect(query["attributes"]).toEqual({
|
|
537
|
+
[METRIC_TEAM_ATTRIBUTE_KEY]: "platform",
|
|
538
|
+
});
|
|
539
|
+
expect(METRIC_TEAM_ATTRIBUTE_KEY).toBe("team.id");
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
test("identity filters compose with provider, model and service", () => {
|
|
543
|
+
const query: Record<string, unknown> = LlmMetricQuery.buildBaseQuery({
|
|
544
|
+
scope: scope({
|
|
545
|
+
serviceId: SERVICE_ID,
|
|
546
|
+
llmSystem: "anthropic",
|
|
547
|
+
llmModel: "claude-opus-4-8",
|
|
548
|
+
llmUserEmail: "ada@example.com",
|
|
549
|
+
llmTeam: "platform",
|
|
550
|
+
}),
|
|
551
|
+
metricNames: ["a"],
|
|
552
|
+
}) as unknown as Record<string, unknown>;
|
|
553
|
+
|
|
554
|
+
expect(query["primaryEntityId"]).toBe(SERVICE_ID);
|
|
555
|
+
expect(query["attributes"]).toEqual({
|
|
556
|
+
[METRIC_SYSTEM_ATTRIBUTE_KEY]: "anthropic",
|
|
557
|
+
[METRIC_MODEL_ATTRIBUTE_KEY]: "claude-opus-4-8",
|
|
558
|
+
[METRIC_USER_ATTRIBUTE_KEY]: "ada@example.com",
|
|
559
|
+
[METRIC_TEAM_ATTRIBUTE_KEY]: "platform",
|
|
560
|
+
});
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
test("the user and team filters never collide on one key", () => {
|
|
564
|
+
// A shared key would make a team filter silently overwrite the person.
|
|
565
|
+
expect(METRIC_USER_ATTRIBUTE_KEY).not.toBe(METRIC_TEAM_ATTRIBUTE_KEY);
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
test("empty-string identity values leave the query unscoped", () => {
|
|
569
|
+
/*
|
|
570
|
+
* The dangerous direction is the opposite of under-counting: an empty
|
|
571
|
+
* filter that still emits an attribute predicate would match NOTHING and
|
|
572
|
+
* report $0 for a person who spent thousands.
|
|
573
|
+
*/
|
|
574
|
+
const query: Record<string, unknown> = LlmMetricQuery.buildBaseQuery({
|
|
575
|
+
scope: scope({ llmUserEmail: "", llmUserId: "", llmTeam: "" }),
|
|
576
|
+
metricNames: ["a"],
|
|
577
|
+
}) as unknown as Record<string, unknown>;
|
|
578
|
+
|
|
579
|
+
expect(query).not.toHaveProperty("attributes");
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
test("every cost and token query carries the identity scoping", () => {
|
|
583
|
+
const scoped: LlmMetricScope = scope({
|
|
584
|
+
llmUserEmail: "ada@example.com",
|
|
585
|
+
llmTeam: "platform",
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
for (const query of [
|
|
589
|
+
LlmMetricQuery.buildCostQuery(scoped),
|
|
590
|
+
LlmMetricQuery.buildMicroUsdCostQuery(scoped),
|
|
591
|
+
LlmMetricQuery.buildTokenQuery(scoped),
|
|
592
|
+
] as Array<Query<Metric>>) {
|
|
593
|
+
const record: Record<string, unknown> = query as unknown as Record<
|
|
594
|
+
string,
|
|
595
|
+
unknown
|
|
596
|
+
>;
|
|
597
|
+
|
|
598
|
+
expect(record["attributes"]).toEqual({
|
|
599
|
+
[METRIC_USER_ATTRIBUTE_KEY]: "ada@example.com",
|
|
600
|
+
[METRIC_TEAM_ATTRIBUTE_KEY]: "platform",
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
/*
|
|
607
|
+
* Micro-USD cost, and the single place its scale factor is applied.
|
|
608
|
+
*
|
|
609
|
+
* The bug class this block exists for is a MILLION-FOLD unit error. The
|
|
610
|
+
* OpenAI Codex CLI reports spend in millionths of a dollar. If its metric
|
|
611
|
+
* name reaches the USD query, or the 1e-6 factor is dropped or typo'd, a $3
|
|
612
|
+
* turn lands in a cost budget as $3,000,000 and instantly breaches every
|
|
613
|
+
* threshold a customer has configured. Nothing throws; the alerts just fire.
|
|
614
|
+
*/
|
|
615
|
+
describe("LlmMetricQuery — micro-USD cost", () => {
|
|
616
|
+
test("the micro-USD query selects exactly the micro-USD metric names", () => {
|
|
617
|
+
const query: Record<string, unknown> =
|
|
618
|
+
LlmMetricQuery.buildMicroUsdCostQuery(scope()) as unknown as Record<
|
|
619
|
+
string,
|
|
620
|
+
unknown
|
|
621
|
+
>;
|
|
622
|
+
|
|
623
|
+
expect((query["name"] as Includes).values).toEqual(
|
|
624
|
+
LlmMicroUsdCostMetricNames,
|
|
625
|
+
);
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
test("the USD and micro-USD queries never select the same metric name", () => {
|
|
629
|
+
const usd: Includes = (
|
|
630
|
+
LlmMetricQuery.buildCostQuery(scope()) as unknown as Record<
|
|
631
|
+
string,
|
|
632
|
+
unknown
|
|
633
|
+
>
|
|
634
|
+
)["name"] as Includes;
|
|
635
|
+
|
|
636
|
+
const micro: Includes = (
|
|
637
|
+
LlmMetricQuery.buildMicroUsdCostQuery(scope()) as unknown as Record<
|
|
638
|
+
string,
|
|
639
|
+
unknown
|
|
640
|
+
>
|
|
641
|
+
)["name"] as Includes;
|
|
642
|
+
|
|
643
|
+
const overlap: Array<string> = (usd.values as Array<string>).filter(
|
|
644
|
+
(name: string) => {
|
|
645
|
+
return (micro.values as Array<string>).includes(name);
|
|
646
|
+
},
|
|
647
|
+
);
|
|
648
|
+
|
|
649
|
+
expect(overlap).toEqual([]);
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
test("the micro-USD query bounds rows with the same time predicate", () => {
|
|
653
|
+
const query: Record<string, unknown> =
|
|
654
|
+
LlmMetricQuery.buildMicroUsdCostQuery(scope()) as unknown as Record<
|
|
655
|
+
string,
|
|
656
|
+
unknown
|
|
657
|
+
>;
|
|
658
|
+
|
|
659
|
+
const time: InBetween<Date> = query["time"] as InBetween<Date>;
|
|
660
|
+
|
|
661
|
+
expect(time).toBeInstanceOf(InBetween);
|
|
662
|
+
expect(time.startValue).toBe(START);
|
|
663
|
+
expect(time.endValue).toBe(END);
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
describe("combineCostTotals", () => {
|
|
667
|
+
test("a 1,500,000 micro-USD datapoint is $1.50", () => {
|
|
668
|
+
// The canonical Codex figure. This is the whole point of the function.
|
|
669
|
+
expect(
|
|
670
|
+
LlmMetricQuery.combineCostTotals({ usd: 0, microUsd: 1_500_000 }),
|
|
671
|
+
).toBeCloseTo(1.5, 10);
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
test("one million micro-USD is exactly one dollar", () => {
|
|
675
|
+
expect(
|
|
676
|
+
LlmMetricQuery.combineCostTotals({ usd: 0, microUsd: 1_000_000 }),
|
|
677
|
+
).toBe(1);
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
test("adds a USD total and a micro-USD total in the same unit", () => {
|
|
681
|
+
expect(
|
|
682
|
+
LlmMetricQuery.combineCostTotals({ usd: 4.25, microUsd: 1_500_000 }),
|
|
683
|
+
).toBeCloseTo(5.75, 10);
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
test("a micro-USD total is NOT added raw", () => {
|
|
687
|
+
/*
|
|
688
|
+
* The regression guard, stated as the failure it prevents: without the
|
|
689
|
+
* scale factor this returns 3,000,000 instead of 3.
|
|
690
|
+
*/
|
|
691
|
+
const combined: number = LlmMetricQuery.combineCostTotals({
|
|
692
|
+
usd: 0,
|
|
693
|
+
microUsd: 3_000_000,
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
expect(combined).toBeCloseTo(3, 10);
|
|
697
|
+
expect(combined).toBeLessThan(10);
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
test("sub-cent micro-USD figures survive as fractions", () => {
|
|
701
|
+
// A single cheap turn: 1250 micro-USD = $0.00125.
|
|
702
|
+
expect(
|
|
703
|
+
LlmMetricQuery.combineCostTotals({ usd: 0, microUsd: 1250 }),
|
|
704
|
+
).toBeCloseTo(0.00125, 10);
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
test("returns 0 when both totals are 0", () => {
|
|
708
|
+
expect(LlmMetricQuery.combineCostTotals({ usd: 0, microUsd: 0 })).toBe(0);
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
test("preserves a negative micro-USD total, which a delta counter can emit", () => {
|
|
712
|
+
expect(
|
|
713
|
+
LlmMetricQuery.combineCostTotals({ usd: 2, microUsd: -1_000_000 }),
|
|
714
|
+
).toBeCloseTo(1, 10);
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
/*
|
|
718
|
+
* Non-finite inputs contribute 0 rather than poisoning the result, exactly
|
|
719
|
+
* as sumAggregatedRows does. A NaN spend compares false against every
|
|
720
|
+
* threshold, which silently DISABLES a budget monitor instead of tripping
|
|
721
|
+
* it — the failure mode a cost alert must never have.
|
|
722
|
+
*/
|
|
723
|
+
test.each([
|
|
724
|
+
["NaN usd", NaN, 1_000_000, 1],
|
|
725
|
+
["NaN microUsd", 2, NaN, 2],
|
|
726
|
+
["Infinity usd", Infinity, 1_000_000, 1],
|
|
727
|
+
["-Infinity usd", -Infinity, 2_000_000, 2],
|
|
728
|
+
["Infinity microUsd", 3, Infinity, 3],
|
|
729
|
+
["-Infinity microUsd", 3, -Infinity, 3],
|
|
730
|
+
["both non-finite", NaN, Infinity, 0],
|
|
731
|
+
])(
|
|
732
|
+
"%s contributes 0 instead of poisoning the total",
|
|
733
|
+
(
|
|
734
|
+
_label: string,
|
|
735
|
+
usd: number,
|
|
736
|
+
microUsd: number,
|
|
737
|
+
expected: number,
|
|
738
|
+
): void => {
|
|
739
|
+
const combined: number = LlmMetricQuery.combineCostTotals({
|
|
740
|
+
usd: usd,
|
|
741
|
+
microUsd: microUsd,
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
expect(isFinite(combined)).toBe(true);
|
|
745
|
+
expect(combined).toBeCloseTo(expected, 10);
|
|
746
|
+
},
|
|
747
|
+
);
|
|
748
|
+
|
|
749
|
+
test("composes with sumAggregatedRows end to end", () => {
|
|
750
|
+
/*
|
|
751
|
+
* The realistic shape: a Claude Code cost metric in dollars and three
|
|
752
|
+
* Codex turns in micro-USD, folded the way getCostInUSD folds them.
|
|
753
|
+
*/
|
|
754
|
+
const usd: number = LlmMetricQuery.sumAggregatedRows([
|
|
755
|
+
row(0.5),
|
|
756
|
+
row(0.25),
|
|
757
|
+
] as Array<AggregatedModel>);
|
|
758
|
+
|
|
759
|
+
const microUsd: number = LlmMetricQuery.sumAggregatedRows([
|
|
760
|
+
row(1_500_000),
|
|
761
|
+
row(500_000),
|
|
762
|
+
row("not-a-number"),
|
|
763
|
+
] as Array<AggregatedModel>);
|
|
764
|
+
|
|
765
|
+
expect(
|
|
766
|
+
LlmMetricQuery.combineCostTotals({ usd: usd, microUsd: microUsd }),
|
|
767
|
+
).toBeCloseTo(2.75, 10);
|
|
768
|
+
});
|
|
769
|
+
});
|
|
770
|
+
});
|