@oneuptime/common 14.0.0 → 14.0.1
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/Index.ts +4 -0
- package/Models/DatabaseModels/ServiceLevelObjectiveFeed.ts +2 -0
- package/Models/DatabaseModels/ServiceLevelObjectiveLabelRule.ts +522 -0
- package/Models/DatabaseModels/ServiceLevelObjectiveMonitorRule.ts +38 -4
- package/Models/DatabaseModels/ServiceLevelObjectiveOwnerRule.ts +603 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1794100000000-AddSloLabelAndOwnerRules.ts +248 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1794200000000-AddSloMonitorRuleMonitorType.ts +19 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
- package/Server/Services/DatabaseService.ts +10 -0
- package/Server/Services/Index.ts +4 -0
- package/Server/Services/MonitorService.ts +3 -2
- package/Server/Services/ServiceLevelObjectiveLabelRuleEngineService.ts +385 -0
- package/Server/Services/ServiceLevelObjectiveLabelRuleService.ts +51 -0
- package/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.ts +92 -16
- package/Server/Services/ServiceLevelObjectiveMonitorRuleService.ts +36 -1
- package/Server/Services/ServiceLevelObjectiveOwnerRuleEngineService.ts +521 -0
- package/Server/Services/ServiceLevelObjectiveOwnerRuleService.ts +51 -0
- package/Server/Services/ServiceLevelObjectiveService.ts +28 -0
- package/Server/Types/Workflow/Components/Telegram/SendMessageToChat.ts +27 -1
- package/Server/Utils/AI/Toolbox/SloTools.ts +1 -0
- package/Server/Utils/Monitor/AffectedResourceList.ts +31 -73
- package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +1 -1
- package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +1 -1
- package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +181 -136
- package/Server/Utils/Monitor/RootCauseList.ts +139 -0
- package/Server/Utils/Rules/RuleRun/RuleRunRegistry.ts +28 -0
- package/Server/Utils/Slo/SloRulePatternValidator.ts +42 -0
- package/Tests/App/Dashboard/FeedFilterSortIntegration.test.tsx +1632 -0
- package/Tests/App/Dashboard/FeedOptionsLocales.test.ts +250 -0
- package/Tests/App/Dashboard/FeedOrdering.test.ts +279 -9
- package/Tests/App/Dashboard/InvestigationFeedRefresh.test.tsx +60 -1
- package/Tests/App/Dashboard/InvestigationPanel.test.tsx +594 -3
- package/Tests/App/Dashboard/ProductOrientation.test.tsx +646 -0
- package/Tests/App/Dashboard/ResourceFeed.test.tsx +612 -2
- package/Tests/App/Dashboard/ResourceFeedPageWiring.test.ts +30 -0
- package/Tests/App/Dashboard/ResourceFeedPagesFilterSort.test.tsx +1202 -0
- package/Tests/App/Dashboard/ScheduledMaintenanceFeedRefresh.test.tsx +52 -1
- package/Tests/App/Dashboard/SloFeed.test.tsx +106 -1
- package/Tests/App/Dashboard/SloMonitorRuleCriteria.test.tsx +566 -0
- package/Tests/App/Dashboard/SloNavigationWiring.test.tsx +6 -4
- package/Tests/App/Dashboard/SloNoticeBanner.test.tsx +186 -0
- package/Tests/App/Dashboard/SloOverviewKpiStrip.test.tsx +388 -44
- package/Tests/App/Dashboard/SloOverviewLayout.test.tsx +613 -0
- package/Tests/Models/DatabaseModels/ResourceFeedModels.test.ts +0 -8
- package/Tests/Models/DatabaseModels/SloLabelAndOwnerRuleModels.test.ts +404 -0
- package/Tests/Models/DatabaseModels/SloOverhaulModels.test.ts +17 -1
- package/Tests/Models/DatabaseModels/StaticRuleCriteriaInheritance.test.ts +4 -2
- package/Tests/Server/Infrastructure/Postgres/AddConfigurableRuleCriteriaMigration.test.ts +3 -0
- package/Tests/Server/Infrastructure/Postgres/AddSloLabelAndOwnerRulesMigration.test.ts +407 -0
- package/Tests/Server/Infrastructure/Postgres/AddSloMonitorRuleMonitorTypeMigration.test.ts +171 -0
- package/Tests/Server/Infrastructure/Postgres/SloProductOverhaulMigration.test.ts +88 -4
- package/Tests/Server/Services/DatabaseServiceRuleCriteriaValidation.test.ts +104 -0
- package/Tests/Server/Services/RuleRun/LabelRuleEnginesGroupBRun.test.ts +48 -1
- package/Tests/Server/Services/RuleRun/OwnerRuleEnginesGroupBRun.test.ts +30 -0
- package/Tests/Server/Services/ServiceLevelObjectiveLabelAndOwnerRuleEngines.test.ts +1170 -0
- package/Tests/Server/Services/ServiceLevelObjectiveLabelAndOwnerRuleServices.test.ts +172 -0
- package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.test.ts +323 -0
- package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleService.test.ts +115 -0
- package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleTriggers.test.ts +21 -2
- package/Tests/Server/Services/ServiceLevelObjectiveMonitorTypePostgres.test.ts +480 -0
- package/Tests/Server/Services/ServiceLevelObjectiveService.test.ts +162 -0
- package/Tests/Server/Services/StaticRuleCriteriaRuntimeCoverage.test.ts +45 -6
- package/Tests/Server/Types/Database/FeedEventTypeFilterQuery.test.ts +745 -0
- package/Tests/Server/Types/Markdown.test.ts +2 -1
- package/Tests/Server/Types/Workflow/Components/ApiComponentSsrf.test.ts +49 -0
- package/Tests/Server/Types/Workflow/Components/TelegramSendMessageToChat.test.ts +331 -0
- package/Tests/Server/Utils/AI/SloTools.test.ts +10 -0
- package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteriaAnomalyUnits.test.ts +1 -1
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluator.test.ts +405 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorBreachingSamples.test.ts +1101 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorMetricUnits.test.ts +106 -80
- package/Tests/Server/Utils/Monitor/RootCauseList.test.ts +252 -0
- package/Tests/Server/Utils/Rules/RuleRun/RuleRunPermission.test.ts +154 -0
- package/Tests/Server/Utils/SSRFProtection.test.ts +111 -0
- package/Tests/Server/Utils/Slo/SloRulePatternValidator.test.ts +67 -0
- package/Tests/Server/Utils/VM/VMRunnerSsrf.test.ts +213 -0
- package/Tests/Types/API/Route.test.ts +56 -0
- package/Tests/Types/API/URLSchemeLikePath.test.ts +304 -0
- package/Tests/UI/Components/FeedOptions.test.ts +1404 -0
- package/Tests/UI/Components/FeedOptionsButton.test.tsx +2603 -0
- package/Tests/UI/Components/Icon.test.tsx +26 -0
- package/Tests/UI/Components/RuleCriteria/RuleCriteriaModelTable.test.tsx +3 -3
- package/Tests/UI/Components/useFeedItemsViewKey.test.tsx +1017 -0
- package/Tests/UI/Components/useFeedOptions.test.tsx +1107 -0
- package/Tests/Utils/LabelRuleImportExport.test.ts +102 -0
- package/Tests/Utils/Slo/SloDuration.test.ts +115 -24
- package/Tests/Utils/Slo/SloHealth.test.ts +93 -10
- package/Tests/Utils/Slo/SloMonitorRuleCriteria.test.ts +109 -0
- package/Tests/Utils/Slo/SloOverviewText.test.ts +64 -0
- package/Types/API/Route.ts +19 -2
- package/Types/API/URL.ts +26 -2
- package/Types/Monitor/MetricMonitor/MetricCriteriaContext.ts +6 -5
- package/Types/Monitor/MetricMonitor/MetricMonitorResponse.ts +9 -0
- package/Types/Permission.ts +90 -0
- package/Types/Rules/RuleCriteriaFieldRegistry.ts +11 -0
- package/Types/Rules/RuleRun.ts +4 -0
- package/UI/Components/Feed/FeedOptions.ts +427 -0
- package/UI/Components/Feed/FeedOptionsButton.tsx +692 -0
- package/UI/Components/Feed/useFeedItems.ts +28 -9
- package/UI/Components/Feed/useFeedOptions.ts +225 -0
- package/UI/Components/Icon/Icon.tsx +1 -1
- package/Utils/LabelRuleImportExport.ts +27 -9
- package/Utils/Monitor/MetricValueFormatter.ts +2 -2
- package/Utils/Rules/MonitorTypeRuleCriteria.ts +31 -0
- package/Utils/Slo/SloDuration.ts +22 -6
- package/Utils/Slo/SloHealth.ts +8 -7
- package/Utils/Slo/SloMonitorRuleCriteria.ts +33 -1
- package/Utils/Slo/SloOverviewText.ts +33 -0
- package/Utils/Slo/SloProjection.ts +4 -3
- package/build/dist/Models/DatabaseModels/Index.js +4 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveFeed.js +2 -0
- package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveFeed.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveLabelRule.js +530 -0
- package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveLabelRule.js.map +1 -0
- package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveMonitorRule.js +39 -4
- package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveMonitorRule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveOwnerRule.js +612 -0
- package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveOwnerRule.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794100000000-AddSloLabelAndOwnerRules.js +93 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794100000000-AddSloLabelAndOwnerRules.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794200000000-AddSloMonitorRuleMonitorType.js +12 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794200000000-AddSloMonitorRuleMonitorType.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +7 -0
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +4 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/MonitorService.js +3 -2
- package/build/dist/Server/Services/MonitorService.js.map +1 -1
- package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleEngineService.js +295 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleEngineService.js.map +1 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleService.js +56 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleService.js.map +1 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js +59 -14
- package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleService.js +22 -1
- package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleService.js.map +1 -1
- package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleEngineService.js +400 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleEngineService.js.map +1 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleService.js +56 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleService.js.map +1 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveService.js +21 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveService.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/Telegram/SendMessageToChat.js +20 -1
- package/build/dist/Server/Types/Workflow/Components/Telegram/SendMessageToChat.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/SloTools.js +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/SloTools.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/AffectedResourceList.js +22 -51
- package/build/dist/Server/Utils/Monitor/AffectedResourceList.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +139 -109
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/RootCauseList.js +101 -0
- package/build/dist/Server/Utils/Monitor/RootCauseList.js.map +1 -0
- package/build/dist/Server/Utils/Rules/RuleRun/RuleRunRegistry.js +28 -0
- package/build/dist/Server/Utils/Rules/RuleRun/RuleRunRegistry.js.map +1 -1
- package/build/dist/Server/Utils/Slo/SloRulePatternValidator.js +28 -0
- package/build/dist/Server/Utils/Slo/SloRulePatternValidator.js.map +1 -0
- package/build/dist/Types/API/Route.js +18 -2
- package/build/dist/Types/API/Route.js.map +1 -1
- package/build/dist/Types/API/URL.js +23 -2
- package/build/dist/Types/API/URL.js.map +1 -1
- package/build/dist/Types/Permission.js +84 -0
- package/build/dist/Types/Permission.js.map +1 -1
- package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js +11 -0
- package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js.map +1 -1
- package/build/dist/Types/Rules/RuleRun.js +4 -0
- package/build/dist/Types/Rules/RuleRun.js.map +1 -1
- package/build/dist/UI/Components/Feed/FeedOptions.js +235 -0
- package/build/dist/UI/Components/Feed/FeedOptions.js.map +1 -0
- package/build/dist/UI/Components/Feed/FeedOptionsButton.js +346 -0
- package/build/dist/UI/Components/Feed/FeedOptionsButton.js.map +1 -0
- package/build/dist/UI/Components/Feed/useFeedItems.js +17 -7
- package/build/dist/UI/Components/Feed/useFeedItems.js.map +1 -1
- package/build/dist/UI/Components/Feed/useFeedOptions.js +130 -0
- package/build/dist/UI/Components/Feed/useFeedOptions.js.map +1 -0
- package/build/dist/UI/Components/Icon/Icon.js +1 -1
- package/build/dist/UI/Components/Icon/Icon.js.map +1 -1
- package/build/dist/Utils/LabelRuleImportExport.js +22 -6
- package/build/dist/Utils/LabelRuleImportExport.js.map +1 -1
- package/build/dist/Utils/Monitor/MetricValueFormatter.js +2 -2
- package/build/dist/Utils/Rules/MonitorTypeRuleCriteria.js +18 -0
- package/build/dist/Utils/Rules/MonitorTypeRuleCriteria.js.map +1 -0
- package/build/dist/Utils/Slo/SloDuration.js +13 -5
- package/build/dist/Utils/Slo/SloDuration.js.map +1 -1
- package/build/dist/Utils/Slo/SloHealth.js +8 -7
- package/build/dist/Utils/Slo/SloHealth.js.map +1 -1
- package/build/dist/Utils/Slo/SloMonitorRuleCriteria.js +16 -1
- package/build/dist/Utils/Slo/SloMonitorRuleCriteria.js.map +1 -1
- package/build/dist/Utils/Slo/SloOverviewText.js +16 -0
- package/build/dist/Utils/Slo/SloOverviewText.js.map +1 -1
- package/build/dist/Utils/Slo/SloProjection.js.map +1 -1
- package/package.json +1 -1
|
@@ -8,6 +8,7 @@ import Label from "../../Models/DatabaseModels/Label";
|
|
|
8
8
|
import Monitor from "../../Models/DatabaseModels/Monitor";
|
|
9
9
|
import MonitorLabelRule from "../../Models/DatabaseModels/MonitorLabelRule";
|
|
10
10
|
import NetworkDeviceLabelRule from "../../Models/DatabaseModels/NetworkDeviceLabelRule";
|
|
11
|
+
import ServiceLevelObjectiveLabelRule from "../../Models/DatabaseModels/ServiceLevelObjectiveLabelRule";
|
|
11
12
|
import TableColumnType from "../../Types/Database/TableColumnType";
|
|
12
13
|
import FilterCondition from "../../Types/Filter/FilterCondition";
|
|
13
14
|
import { JSONObject } from "../../Types/JSON";
|
|
@@ -175,6 +176,11 @@ const criteriaByType: Record<string, [string, string, string]> = {
|
|
|
175
176
|
"serviceDescriptionPattern",
|
|
176
177
|
"serviceLabels",
|
|
177
178
|
],
|
|
179
|
+
ServiceLevelObjectiveLabelRule: [
|
|
180
|
+
"serviceLevelObjectiveNamePattern",
|
|
181
|
+
"serviceLevelObjectiveDescriptionPattern",
|
|
182
|
+
"serviceLevelObjectiveLabels",
|
|
183
|
+
],
|
|
178
184
|
StatusPageLabelRule: [
|
|
179
185
|
"statusPageNamePattern",
|
|
180
186
|
"statusPageDescriptionPattern",
|
|
@@ -590,6 +596,102 @@ describe("portable label rule files", () => {
|
|
|
590
596
|
},
|
|
591
597
|
);
|
|
592
598
|
|
|
599
|
+
/*
|
|
600
|
+
* SLO label rules match patterns the way network device rules do - a regex,
|
|
601
|
+
* with a '*' wildcard fallback, on the trimmed pattern - so the two trade
|
|
602
|
+
* any pattern, and both refuse one whose meaning a regex-only rule type
|
|
603
|
+
* would change.
|
|
604
|
+
*/
|
|
605
|
+
test("preserves wildcard SLO patterns when importing SLO rules", () => {
|
|
606
|
+
expect(
|
|
607
|
+
parse(
|
|
608
|
+
[{ ...base, serviceLevelObjectiveNamePattern: "*checkout*" }],
|
|
609
|
+
ServiceLevelObjectiveLabelRule,
|
|
610
|
+
).items[0]!.json["serviceLevelObjectiveNamePattern"],
|
|
611
|
+
).toBe("*checkout*");
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
test.each([
|
|
615
|
+
[NetworkDeviceLabelRule, ServiceLevelObjectiveLabelRule],
|
|
616
|
+
[ServiceLevelObjectiveLabelRule, NetworkDeviceLabelRule],
|
|
617
|
+
])(
|
|
618
|
+
"moves a wildcard between %p and %p without changing its meaning",
|
|
619
|
+
(source: DatabaseBaseModelType, destination: DatabaseBaseModelType) => {
|
|
620
|
+
const sourceField: string =
|
|
621
|
+
source === NetworkDeviceLabelRule
|
|
622
|
+
? "networkDeviceNamePattern"
|
|
623
|
+
: "serviceLevelObjectiveNamePattern";
|
|
624
|
+
const destinationField: string =
|
|
625
|
+
destination === NetworkDeviceLabelRule
|
|
626
|
+
? "networkDeviceNamePattern"
|
|
627
|
+
: "serviceLevelObjectiveNamePattern";
|
|
628
|
+
|
|
629
|
+
expect(
|
|
630
|
+
parse([{ ...base, [sourceField]: "*edge*" }], source, destination)
|
|
631
|
+
.items[0]!.json[destinationField],
|
|
632
|
+
).toBe("*edge*");
|
|
633
|
+
},
|
|
634
|
+
);
|
|
635
|
+
|
|
636
|
+
test.each(["*checkout*", "checkout.*", " checkout ", " "])(
|
|
637
|
+
"rejects moving the SLO pattern %p to a regex-only rule type",
|
|
638
|
+
(pattern: string) => {
|
|
639
|
+
expect(() => {
|
|
640
|
+
parse(
|
|
641
|
+
[{ ...base, serviceLevelObjectiveNamePattern: pattern }],
|
|
642
|
+
ServiceLevelObjectiveLabelRule,
|
|
643
|
+
MonitorLabelRule,
|
|
644
|
+
);
|
|
645
|
+
}).toThrow("without changing its meaning");
|
|
646
|
+
},
|
|
647
|
+
);
|
|
648
|
+
|
|
649
|
+
test.each(["checkout.*", " checkout "])(
|
|
650
|
+
"rejects importing the regex %p into the broader SLO matcher",
|
|
651
|
+
(pattern: string) => {
|
|
652
|
+
expect(() => {
|
|
653
|
+
parse(
|
|
654
|
+
[{ ...base, monitorNamePattern: pattern }],
|
|
655
|
+
MonitorLabelRule,
|
|
656
|
+
ServiceLevelObjectiveLabelRule,
|
|
657
|
+
);
|
|
658
|
+
}).toThrow("without changing its meaning");
|
|
659
|
+
},
|
|
660
|
+
);
|
|
661
|
+
|
|
662
|
+
test("moves a plain SLO pattern to a regex-only rule type", () => {
|
|
663
|
+
expect(
|
|
664
|
+
parse(
|
|
665
|
+
[{ ...base, serviceLevelObjectiveNamePattern: "^checkout-" }],
|
|
666
|
+
ServiceLevelObjectiveLabelRule,
|
|
667
|
+
MonitorLabelRule,
|
|
668
|
+
).items[0]!.json["monitorNamePattern"],
|
|
669
|
+
).toBe("^checkout-");
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
test("accepts a wildcard, but not an invalid pattern, for SLO rules", () => {
|
|
673
|
+
expect(() => {
|
|
674
|
+
parse(
|
|
675
|
+
[{ ...base, serviceLevelObjectiveDescriptionPattern: "*tier 1*" }],
|
|
676
|
+
ServiceLevelObjectiveLabelRule,
|
|
677
|
+
);
|
|
678
|
+
}).not.toThrow();
|
|
679
|
+
|
|
680
|
+
expect(() => {
|
|
681
|
+
parse(
|
|
682
|
+
[{ ...base, serviceLevelObjectiveDescriptionPattern: "tier-(1" }],
|
|
683
|
+
ServiceLevelObjectiveLabelRule,
|
|
684
|
+
);
|
|
685
|
+
}).toThrow("is not a valid regular expression or wildcard pattern");
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
// A regex-only rule type still refuses the wildcard syntax outright.
|
|
689
|
+
test("still rejects a bare wildcard for a regex-only rule type", () => {
|
|
690
|
+
expect(() => {
|
|
691
|
+
parse([{ ...base, monitorNamePattern: "*checkout*" }], MonitorLabelRule);
|
|
692
|
+
}).toThrow("is not a valid regular expression.");
|
|
693
|
+
});
|
|
694
|
+
|
|
593
695
|
test("preserves wildcard network patterns when importing network rules", () => {
|
|
594
696
|
expect(
|
|
595
697
|
parse(
|
|
@@ -4,8 +4,6 @@ import {
|
|
|
4
4
|
formatErrorBudgetRemainingOfTotal,
|
|
5
5
|
} from "../../../Utils/Slo/SloDuration";
|
|
6
6
|
|
|
7
|
-
const MINUS_SIGN: string = "−";
|
|
8
|
-
|
|
9
7
|
describe("SloDuration", () => {
|
|
10
8
|
describe("formatDurationCompact", () => {
|
|
11
9
|
it("renders seconds under a minute", () => {
|
|
@@ -74,12 +72,38 @@ describe("SloDuration", () => {
|
|
|
74
72
|
|
|
75
73
|
it("renders an exhausted-but-not-overspent budget as 0s left", () => {
|
|
76
74
|
expect(formatErrorBudgetRemaining(0)).toBe("0s left");
|
|
75
|
+
expect(formatErrorBudgetRemaining(-0)).toBe("0s left");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("states the overage as a positive duration with over budget", () => {
|
|
79
|
+
expect(formatErrorBudgetRemaining(-750)).toBe("12m 30s over budget");
|
|
77
80
|
});
|
|
78
81
|
|
|
79
|
-
it("
|
|
80
|
-
expect(formatErrorBudgetRemaining(-
|
|
81
|
-
|
|
82
|
-
|
|
82
|
+
it("keeps a multi-day overage readable", () => {
|
|
83
|
+
expect(formatErrorBudgetRemaining(-346980)).toBe("4d 23m over budget");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it.each([0.001, 0.4, 0.999])(
|
|
87
|
+
"distinguishes a positive %ps remainder from an empty budget",
|
|
88
|
+
(seconds: number) => {
|
|
89
|
+
expect(formatErrorBudgetRemaining(seconds)).toBe("under 1s left");
|
|
90
|
+
},
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
it.each([-0.001, -0.4, -0.999])(
|
|
94
|
+
"preserves a %ps overage instead of rounding it to zero",
|
|
95
|
+
(seconds: number) => {
|
|
96
|
+
expect(formatErrorBudgetRemaining(seconds)).toBe(
|
|
97
|
+
"under 1s over budget",
|
|
98
|
+
);
|
|
99
|
+
},
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
it("uses seconds at the one-second boundary without rounding up", () => {
|
|
103
|
+
expect(formatErrorBudgetRemaining(1)).toBe("1s left");
|
|
104
|
+
expect(formatErrorBudgetRemaining(-1)).toBe("1s over budget");
|
|
105
|
+
expect(formatErrorBudgetRemaining(59.9)).toBe("59s left");
|
|
106
|
+
expect(formatErrorBudgetRemaining(-59.9)).toBe("59s over budget");
|
|
83
107
|
});
|
|
84
108
|
|
|
85
109
|
it("returns null when the SLO has not been evaluated", () => {
|
|
@@ -90,6 +114,7 @@ describe("SloDuration", () => {
|
|
|
90
114
|
it("returns null for non-finite values", () => {
|
|
91
115
|
expect(formatErrorBudgetRemaining(Number.NaN)).toBeNull();
|
|
92
116
|
expect(formatErrorBudgetRemaining(Number.POSITIVE_INFINITY)).toBeNull();
|
|
117
|
+
expect(formatErrorBudgetRemaining(Number.NEGATIVE_INFINITY)).toBeNull();
|
|
93
118
|
});
|
|
94
119
|
|
|
95
120
|
it("rejects a numeric string rather than coercing it", () => {
|
|
@@ -97,6 +122,11 @@ describe("SloDuration", () => {
|
|
|
97
122
|
formatErrorBudgetRemaining("2592" as unknown as number),
|
|
98
123
|
).toBeNull();
|
|
99
124
|
});
|
|
125
|
+
|
|
126
|
+
it("rejects booleans rather than treating them as zero or one", () => {
|
|
127
|
+
expect(formatErrorBudgetRemaining(false as unknown as number)).toBeNull();
|
|
128
|
+
expect(formatErrorBudgetRemaining(true as unknown as number)).toBeNull();
|
|
129
|
+
});
|
|
100
130
|
});
|
|
101
131
|
|
|
102
132
|
describe("formatErrorBudgetRemainingOfTotal", () => {
|
|
@@ -109,40 +139,101 @@ describe("SloDuration", () => {
|
|
|
109
139
|
).toBe("12m 30s left of 43m 12s");
|
|
110
140
|
});
|
|
111
141
|
|
|
112
|
-
it("
|
|
142
|
+
it("identifies the allowance separately from how far over budget it is", () => {
|
|
113
143
|
expect(
|
|
114
144
|
formatErrorBudgetRemainingOfTotal({
|
|
115
145
|
remainingSeconds: -750,
|
|
116
146
|
totalSeconds: 2592,
|
|
117
147
|
}),
|
|
118
|
-
).toBe(
|
|
148
|
+
).toBe("12m 30s over budget · 43m 12s allowed");
|
|
119
149
|
});
|
|
120
150
|
|
|
121
|
-
it("
|
|
151
|
+
it("explains the reported multi-day overage against its small allowance", () => {
|
|
122
152
|
expect(
|
|
123
153
|
formatErrorBudgetRemainingOfTotal({
|
|
124
|
-
remainingSeconds:
|
|
125
|
-
totalSeconds:
|
|
154
|
+
remainingSeconds: -346980,
|
|
155
|
+
totalSeconds: 347,
|
|
126
156
|
}),
|
|
127
|
-
).toBe("
|
|
157
|
+
).toBe("4d 23m over budget · 5m 47s allowed");
|
|
128
158
|
});
|
|
129
159
|
|
|
130
|
-
it("
|
|
160
|
+
it("retains the allowance at exactly zero without claiming an overage", () => {
|
|
131
161
|
expect(
|
|
132
162
|
formatErrorBudgetRemainingOfTotal({
|
|
133
163
|
remainingSeconds: 0,
|
|
134
|
-
totalSeconds: 0,
|
|
135
|
-
}),
|
|
136
|
-
).toBe("0s left");
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it("returns null when the SLO has not been evaluated", () => {
|
|
140
|
-
expect(
|
|
141
|
-
formatErrorBudgetRemainingOfTotal({
|
|
142
|
-
remainingSeconds: null,
|
|
143
164
|
totalSeconds: 2592,
|
|
144
165
|
}),
|
|
145
|
-
).
|
|
146
|
-
});
|
|
166
|
+
).toBe("0s left of 43m 12s");
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it.each([
|
|
170
|
+
[0.25, "under 1s left of under 1s"],
|
|
171
|
+
[-0.25, "under 1s over budget · under 1s allowed"],
|
|
172
|
+
[0, "0s left of under 1s"],
|
|
173
|
+
])(
|
|
174
|
+
"does not round a subsecond allowance to zero for a %ps remainder",
|
|
175
|
+
(remainingSeconds: number, expected: string) => {
|
|
176
|
+
expect(
|
|
177
|
+
formatErrorBudgetRemainingOfTotal({
|
|
178
|
+
remainingSeconds,
|
|
179
|
+
totalSeconds: 0.5,
|
|
180
|
+
}),
|
|
181
|
+
).toBe(expected);
|
|
182
|
+
},
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
it.each([
|
|
186
|
+
["missing", undefined],
|
|
187
|
+
["null", null],
|
|
188
|
+
["zero", 0],
|
|
189
|
+
["negative", -2592],
|
|
190
|
+
["NaN", Number.NaN],
|
|
191
|
+
["positive infinity", Number.POSITIVE_INFINITY],
|
|
192
|
+
["negative infinity", Number.NEGATIVE_INFINITY],
|
|
193
|
+
["numeric string", "2592" as unknown as number],
|
|
194
|
+
["boolean", true as unknown as number],
|
|
195
|
+
])(
|
|
196
|
+
"omits an unusable %s total while preserving the remaining or over-budget state",
|
|
197
|
+
(_label: string, totalSeconds: number | null | undefined) => {
|
|
198
|
+
expect(
|
|
199
|
+
formatErrorBudgetRemainingOfTotal({
|
|
200
|
+
remainingSeconds: 750,
|
|
201
|
+
totalSeconds,
|
|
202
|
+
}),
|
|
203
|
+
).toBe("12m 30s left");
|
|
204
|
+
expect(
|
|
205
|
+
formatErrorBudgetRemainingOfTotal({
|
|
206
|
+
remainingSeconds: -750,
|
|
207
|
+
totalSeconds,
|
|
208
|
+
}),
|
|
209
|
+
).toBe("12m 30s over budget");
|
|
210
|
+
expect(
|
|
211
|
+
formatErrorBudgetRemainingOfTotal({
|
|
212
|
+
remainingSeconds: 0,
|
|
213
|
+
totalSeconds,
|
|
214
|
+
}),
|
|
215
|
+
).toBe("0s left");
|
|
216
|
+
},
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
it.each([
|
|
220
|
+
["missing", undefined],
|
|
221
|
+
["null", null],
|
|
222
|
+
["NaN", Number.NaN],
|
|
223
|
+
["positive infinity", Number.POSITIVE_INFINITY],
|
|
224
|
+
["negative infinity", Number.NEGATIVE_INFINITY],
|
|
225
|
+
["numeric string", "750" as unknown as number],
|
|
226
|
+
["boolean", false as unknown as number],
|
|
227
|
+
])(
|
|
228
|
+
"does not let a known total imply an evaluated budget when the remainder is %s",
|
|
229
|
+
(_label: string, remainingSeconds: number | null | undefined) => {
|
|
230
|
+
expect(
|
|
231
|
+
formatErrorBudgetRemainingOfTotal({
|
|
232
|
+
remainingSeconds,
|
|
233
|
+
totalSeconds: 2592,
|
|
234
|
+
}),
|
|
235
|
+
).toBeNull();
|
|
236
|
+
},
|
|
237
|
+
);
|
|
147
238
|
});
|
|
148
239
|
});
|
|
@@ -58,7 +58,7 @@ const EVERY_NOTICE_INPUT: Array<{ name: string; data: SloNoticeData }> = [
|
|
|
58
58
|
},
|
|
59
59
|
},
|
|
60
60
|
{
|
|
61
|
-
name: "
|
|
61
|
+
name: "waiting for evaluation with monitors attached",
|
|
62
62
|
data: { ...MEASURING, sloStatus: SloStatus.Misconfigured },
|
|
63
63
|
},
|
|
64
64
|
{ name: "paused", data: { ...MEASURING, sloStatus: SloStatus.Paused } },
|
|
@@ -431,17 +431,100 @@ describe("SloHealth", () => {
|
|
|
431
431
|
expect(notice?.body).toContain(SliType.MonitorUptime);
|
|
432
432
|
});
|
|
433
433
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
monitorCount:
|
|
440
|
-
|
|
441
|
-
|
|
434
|
+
describe("waiting for evaluation after monitors are attached", () => {
|
|
435
|
+
it.each([
|
|
436
|
+
{ monitorCount: 1, lastEvaluatedAt: null },
|
|
437
|
+
{ monitorCount: 16, lastEvaluatedAt: null },
|
|
438
|
+
{ monitorCount: 1, lastEvaluatedAt: undefined },
|
|
439
|
+
{ monitorCount: 16, lastEvaluatedAt: undefined },
|
|
440
|
+
{
|
|
441
|
+
monitorCount: 1,
|
|
442
|
+
lastEvaluatedAt: new Date("2026-09-21T12:00:00.000Z"),
|
|
443
|
+
},
|
|
444
|
+
{
|
|
445
|
+
monitorCount: 16,
|
|
446
|
+
lastEvaluatedAt: "2026-09-21T12:00:00.000Z",
|
|
447
|
+
},
|
|
448
|
+
])(
|
|
449
|
+
"is informational with $monitorCount monitors and lastEvaluatedAt=$lastEvaluatedAt",
|
|
450
|
+
({
|
|
451
|
+
monitorCount,
|
|
452
|
+
lastEvaluatedAt,
|
|
453
|
+
}: {
|
|
454
|
+
monitorCount: number;
|
|
455
|
+
lastEvaluatedAt: Date | string | null | undefined;
|
|
456
|
+
}) => {
|
|
457
|
+
const notice: SloNotice | null = getSloNotice({
|
|
458
|
+
...MEASURING,
|
|
459
|
+
sloStatus: SloStatus.Misconfigured,
|
|
460
|
+
monitorCount,
|
|
461
|
+
lastEvaluatedAt,
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
expect(notice?.type).toBe(SloNoticeType.Info);
|
|
465
|
+
expect(notice?.title).toBe("Waiting for SLO evaluation");
|
|
466
|
+
expect(notice?.body).toContain("Monitors are attached");
|
|
467
|
+
expect(notice?.body).toContain("every few minutes");
|
|
468
|
+
expect(notice?.body).toContain(
|
|
469
|
+
"once monitor status data is available and the next evaluation completes",
|
|
470
|
+
);
|
|
471
|
+
expect(notice?.action).toEqual({
|
|
472
|
+
label: "View monitors",
|
|
473
|
+
target: SloNoticeActionTarget.Monitors,
|
|
474
|
+
});
|
|
475
|
+
},
|
|
476
|
+
);
|
|
477
|
+
|
|
478
|
+
it.each([undefined, null])(
|
|
479
|
+
"retains the default monitor-uptime behavior when sliType is %s",
|
|
480
|
+
(sliType: undefined | null) => {
|
|
481
|
+
expect(
|
|
482
|
+
getSloNotice({
|
|
483
|
+
...MEASURING,
|
|
484
|
+
sloStatus: SloStatus.Misconfigured,
|
|
485
|
+
sliType,
|
|
486
|
+
})?.type,
|
|
487
|
+
).toBe(SloNoticeType.Info);
|
|
488
|
+
},
|
|
489
|
+
);
|
|
490
|
+
|
|
491
|
+
it.each([0, -1, 100, 101, Number.NaN, Infinity, null, undefined])(
|
|
492
|
+
"still warns about target %s even with monitors attached",
|
|
493
|
+
(targetPercentage: number | null | undefined) => {
|
|
494
|
+
const notice: SloNotice | null = getSloNotice({
|
|
495
|
+
...MEASURING,
|
|
496
|
+
sloStatus: SloStatus.Misconfigured,
|
|
497
|
+
monitorCount: 16,
|
|
498
|
+
targetPercentage,
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
expect(notice?.type).toBe(SloNoticeType.Warning);
|
|
502
|
+
expect(notice?.title).toBe("The target is out of range");
|
|
503
|
+
expect(notice?.action?.target).toBe(SloNoticeActionTarget.Settings);
|
|
504
|
+
},
|
|
505
|
+
);
|
|
506
|
+
|
|
507
|
+
it("still warns about an unsupported SLI with monitors attached", () => {
|
|
508
|
+
const notice: SloNotice | null = getSloNotice({
|
|
509
|
+
...MEASURING,
|
|
510
|
+
sloStatus: SloStatus.Misconfigured,
|
|
511
|
+
sliType: SliType.Metric,
|
|
512
|
+
monitorCount: 16,
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
expect(notice?.type).toBe(SloNoticeType.Warning);
|
|
516
|
+
expect(notice?.title).toBe("This SLO cannot be evaluated");
|
|
517
|
+
expect(notice?.action).toBeUndefined();
|
|
442
518
|
});
|
|
443
519
|
|
|
444
|
-
|
|
520
|
+
it.each([SloStatus.Healthy, SloStatus.AtRisk, SloStatus.BudgetExhausted])(
|
|
521
|
+
"stops displaying the notice after evaluation reports %s",
|
|
522
|
+
(sloStatus: SloStatus) => {
|
|
523
|
+
expect(
|
|
524
|
+
getSloNotice({ ...MEASURING, sloStatus, monitorCount: 16 }),
|
|
525
|
+
).toBeNull();
|
|
526
|
+
},
|
|
527
|
+
);
|
|
445
528
|
});
|
|
446
529
|
|
|
447
530
|
it("explains Paused", () => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import FilterCondition from "../../../Types/Filter/FilterCondition";
|
|
2
2
|
import ObjectID from "../../../Types/ObjectID";
|
|
3
|
+
import MonitorType from "../../../Types/Monitor/MonitorType";
|
|
3
4
|
import RuleCriteria, {
|
|
4
5
|
RULE_CRITERIA_LEGACY_NEVER_MATCH_PATTERN,
|
|
5
6
|
RULE_CRITERIA_SCHEMA_VERSION,
|
|
@@ -385,3 +386,111 @@ describe("getSloMonitorRuleCriteriaKey", () => {
|
|
|
385
386
|
);
|
|
386
387
|
});
|
|
387
388
|
});
|
|
389
|
+
|
|
390
|
+
describe("SLO monitor type criteria descriptions", () => {
|
|
391
|
+
test.each(["", "Invalid monitor type"])(
|
|
392
|
+
"describes an invalid legacy monitor type as matching no monitors: %p",
|
|
393
|
+
(monitorType: string) => {
|
|
394
|
+
expect(
|
|
395
|
+
describeSloMonitorRuleCriteria({
|
|
396
|
+
rule: {
|
|
397
|
+
monitorType: monitorType as MonitorType,
|
|
398
|
+
monitorNamePattern: ".*",
|
|
399
|
+
},
|
|
400
|
+
}),
|
|
401
|
+
).toBe(SLO_MONITOR_RULE_INVALID_CRITERIA_DESCRIPTION);
|
|
402
|
+
},
|
|
403
|
+
);
|
|
404
|
+
|
|
405
|
+
test.each([
|
|
406
|
+
{ operator: RuleCriteriaOperator.Equals, value: "Invalid monitor type" },
|
|
407
|
+
{ operator: RuleCriteriaOperator.NotEquals, value: "" },
|
|
408
|
+
{ operator: RuleCriteriaOperator.Contains, value: MonitorType.API },
|
|
409
|
+
{ operator: RuleCriteriaOperator.HasAnyOf, value: [MonitorType.API] },
|
|
410
|
+
])(
|
|
411
|
+
"describes invalid type criteria as matching no monitors: %p",
|
|
412
|
+
(filter: Pick<RuleCriteriaFilter, "operator" | "value">) => {
|
|
413
|
+
expect(
|
|
414
|
+
describeSloMonitorRuleCriteria({
|
|
415
|
+
rule: {
|
|
416
|
+
criteria: criteria(FilterCondition.Any, [
|
|
417
|
+
{ field: "monitorType", ...filter },
|
|
418
|
+
]),
|
|
419
|
+
},
|
|
420
|
+
}),
|
|
421
|
+
).toBe(SLO_MONITOR_RULE_INVALID_CRITERIA_DESCRIPTION);
|
|
422
|
+
},
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
test.each([RuleCriteriaOperator.Equals, RuleCriteriaOperator.NotEquals])(
|
|
426
|
+
"describes configured monitor type %s without looking up labels",
|
|
427
|
+
(operator: RuleCriteriaOperator) => {
|
|
428
|
+
const rule: SloMonitorRuleCriteriaCarrier = {
|
|
429
|
+
criteria: criteria(FilterCondition.All, [
|
|
430
|
+
{ field: "monitorType", operator, value: MonitorType.API },
|
|
431
|
+
{
|
|
432
|
+
field: "monitorNamePattern",
|
|
433
|
+
operator: RuleCriteriaOperator.DoesNotMatchPattern,
|
|
434
|
+
value: "^staging-",
|
|
435
|
+
},
|
|
436
|
+
]),
|
|
437
|
+
monitorType: MonitorType.Website,
|
|
438
|
+
};
|
|
439
|
+
expect(describeSloMonitorRuleCriteria({ rule })).toBe(
|
|
440
|
+
`Type ${SLO_MONITOR_RULE_OPERATOR_PHRASES[operator]} "API" AND Name does not match pattern "^staging-"`,
|
|
441
|
+
);
|
|
442
|
+
expect(getSloMonitorRuleLabelIds(rule)).toEqual([]);
|
|
443
|
+
},
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
test("describes a legacy monitor type combined with other attributes", () => {
|
|
447
|
+
expect(
|
|
448
|
+
describeSloMonitorRuleCriteria({
|
|
449
|
+
rule: {
|
|
450
|
+
monitorType: MonitorType.Website,
|
|
451
|
+
monitorLabels: [{ _id: PRODUCTION_LABEL_ID, name: "Production" }],
|
|
452
|
+
monitorNamePattern: "^checkout-",
|
|
453
|
+
},
|
|
454
|
+
}),
|
|
455
|
+
).toBe(
|
|
456
|
+
'Labels has any of "Production" AND Type is "Website" AND Name matches pattern "^checkout-"',
|
|
457
|
+
);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
test("describes a type-only legacy rule as a configured condition", () => {
|
|
461
|
+
expect(
|
|
462
|
+
describeSloMonitorRuleCriteria({
|
|
463
|
+
rule: { monitorType: MonitorType.API },
|
|
464
|
+
}),
|
|
465
|
+
).toBe('Type is "API"');
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
test("detects changes to legacy monitor types for audit and feed updates", () => {
|
|
469
|
+
const apiKey: string = getSloMonitorRuleCriteriaKey({
|
|
470
|
+
monitorType: MonitorType.API,
|
|
471
|
+
});
|
|
472
|
+
expect(apiKey).not.toBe(
|
|
473
|
+
getSloMonitorRuleCriteriaKey({ monitorType: MonitorType.Website }),
|
|
474
|
+
);
|
|
475
|
+
expect(apiKey).not.toBe(getSloMonitorRuleCriteriaKey({}));
|
|
476
|
+
expect(getSloMonitorRuleCriteriaKey({ monitorType: null })).toBe(
|
|
477
|
+
getSloMonitorRuleCriteriaKey({}),
|
|
478
|
+
);
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
test("ignores legacy monitor type shadows for configured rule identity", () => {
|
|
482
|
+
const configured: RuleCriteria = criteria(FilterCondition.All, [
|
|
483
|
+
{
|
|
484
|
+
field: "monitorType",
|
|
485
|
+
operator: RuleCriteriaOperator.Equals,
|
|
486
|
+
value: MonitorType.API,
|
|
487
|
+
},
|
|
488
|
+
]);
|
|
489
|
+
expect(
|
|
490
|
+
getSloMonitorRuleCriteriaKey({
|
|
491
|
+
criteria: configured,
|
|
492
|
+
monitorType: MonitorType.Website,
|
|
493
|
+
}),
|
|
494
|
+
).toBe(getSloMonitorRuleCriteriaKey({ criteria: configured }));
|
|
495
|
+
});
|
|
496
|
+
});
|
|
@@ -5,6 +5,7 @@ import SloWindowType from "../../../Types/ServiceLevelObjective/SloWindowType";
|
|
|
5
5
|
import {
|
|
6
6
|
getSliTypeText,
|
|
7
7
|
getSloAtRiskText,
|
|
8
|
+
getSloBudgetRemainingText,
|
|
8
9
|
getSloDowntimeStatusesText,
|
|
9
10
|
getSloHeadline,
|
|
10
11
|
getSloMonitorCountText,
|
|
@@ -79,6 +80,69 @@ describe("getSloTargetText", () => {
|
|
|
79
80
|
});
|
|
80
81
|
});
|
|
81
82
|
|
|
83
|
+
describe("getSloBudgetRemainingText", () => {
|
|
84
|
+
test.each([-99882.8, -120, -0.001, -Number.MIN_VALUE, -Number.MAX_VALUE])(
|
|
85
|
+
"explains a %p%% remaining budget as exceeded, even if it rounds to zero",
|
|
86
|
+
(percentage: number) => {
|
|
87
|
+
expect(getSloBudgetRemainingText(percentage)).toBe("Budget exceeded");
|
|
88
|
+
},
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
test.each([0, -0])(
|
|
92
|
+
"distinguishes an exactly exhausted %p%% budget from an overage",
|
|
93
|
+
(percentage: number) => {
|
|
94
|
+
expect(getSloBudgetRemainingText(percentage)).toBe("No budget left");
|
|
95
|
+
},
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
test.each([Number.MIN_VALUE, 0.001, 0.049, 0.05, 0.0999])(
|
|
99
|
+
"keeps a small positive %p%% budget distinct from no budget left",
|
|
100
|
+
(percentage: number) => {
|
|
101
|
+
expect(getSloBudgetRemainingText(percentage)).toBe("<0.1%");
|
|
102
|
+
},
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
test.each([
|
|
106
|
+
[0.1, "0.1%"],
|
|
107
|
+
[0.14, "0.1%"],
|
|
108
|
+
[0.15, "0.2%"],
|
|
109
|
+
[15, "15%"],
|
|
110
|
+
[20, "20%"],
|
|
111
|
+
[62.5, "62.5%"],
|
|
112
|
+
[62.56, "62.6%"],
|
|
113
|
+
[100, "100%"],
|
|
114
|
+
])(
|
|
115
|
+
"shows a positive %p%% remainder as %s without unnecessary decimal places",
|
|
116
|
+
(percentage: number, expected: string) => {
|
|
117
|
+
expect(getSloBudgetRemainingText(percentage)).toBe(expected);
|
|
118
|
+
},
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
test.each([100.01, 150, Number.MAX_VALUE])(
|
|
122
|
+
"never claims more than a full budget when the input is %p%%",
|
|
123
|
+
(percentage: number) => {
|
|
124
|
+
expect(getSloBudgetRemainingText(percentage)).toBe("100%");
|
|
125
|
+
},
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
test.each([
|
|
129
|
+
["missing", undefined],
|
|
130
|
+
["null", null],
|
|
131
|
+
["NaN", Number.NaN],
|
|
132
|
+
["positive infinity", Number.POSITIVE_INFINITY],
|
|
133
|
+
["negative infinity", Number.NEGATIVE_INFINITY],
|
|
134
|
+
["numeric string", "62.5" as unknown as number],
|
|
135
|
+
["negative numeric string", "-99882.8" as unknown as number],
|
|
136
|
+
["true", true as unknown as number],
|
|
137
|
+
["false", false as unknown as number],
|
|
138
|
+
])(
|
|
139
|
+
"makes no budget claim for %s data",
|
|
140
|
+
(_label: string, percentage: number | null | undefined) => {
|
|
141
|
+
expect(getSloBudgetRemainingText(percentage)).toBeNull();
|
|
142
|
+
},
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
|
|
82
146
|
describe("getSloMonitorCountText", () => {
|
|
83
147
|
test.each([
|
|
84
148
|
[0, "No monitors"],
|
package/Types/API/Route.ts
CHANGED
|
@@ -7,13 +7,30 @@ import { FindOperator } from "typeorm";
|
|
|
7
7
|
export default class Route extends DatabaseProperty {
|
|
8
8
|
private static readonly SCHEME_PREFIX: RegExp = /^[a-zA-Z][a-zA-Z\d+.-]*:/;
|
|
9
9
|
|
|
10
|
+
/*
|
|
11
|
+
* True when the value, read on its own as a relative reference, would start
|
|
12
|
+
* with a scheme (RFC 3986 4.2): "javascript:alert(1)", but equally
|
|
13
|
+
* "bot123:ABC/sendMessage". Route refuses such values so a scheme can never
|
|
14
|
+
* reach a navigation sink; URL uses this to know when a path it parsed has
|
|
15
|
+
* to keep its leading "/".
|
|
16
|
+
*/
|
|
17
|
+
public static hasSchemePrefix(route: string): boolean {
|
|
18
|
+
return Route.SCHEME_PREFIX.test(route);
|
|
19
|
+
}
|
|
20
|
+
|
|
10
21
|
private static validateRoute(route: string): void {
|
|
11
|
-
if (Route.
|
|
22
|
+
if (Route.hasSchemePrefix(route)) {
|
|
12
23
|
throw new BadDataException(`Invalid route: ${route}`);
|
|
13
24
|
}
|
|
14
25
|
|
|
26
|
+
/*
|
|
27
|
+
* RFC 3986 path characters: unreserved (which includes "~"), sub-delims,
|
|
28
|
+
* ":" and "@", plus "/", "?", "#", "[", "]" and "%" for the rest of a
|
|
29
|
+
* path-and-query. "~" was missing, so "https://example.com/~user" could
|
|
30
|
+
* not be parsed at all.
|
|
31
|
+
*/
|
|
15
32
|
const matchRouteCharacters: RegExp =
|
|
16
|
-
/^[a-zA-Z_\d\-!#$%&'()*+,./:;=?@[\]]*$/;
|
|
33
|
+
/^[a-zA-Z_\d\-!#$%&'()*+,./:;=?@[\]~]*$/;
|
|
17
34
|
|
|
18
35
|
if (route && !matchRouteCharacters.test(route)) {
|
|
19
36
|
throw new BadDataException(`Invalid route: ${route}`);
|