@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.
Files changed (196) hide show
  1. package/Models/DatabaseModels/Index.ts +4 -0
  2. package/Models/DatabaseModels/ServiceLevelObjectiveFeed.ts +2 -0
  3. package/Models/DatabaseModels/ServiceLevelObjectiveLabelRule.ts +522 -0
  4. package/Models/DatabaseModels/ServiceLevelObjectiveMonitorRule.ts +38 -4
  5. package/Models/DatabaseModels/ServiceLevelObjectiveOwnerRule.ts +603 -0
  6. package/Server/Infrastructure/Postgres/SchemaMigrations/1794100000000-AddSloLabelAndOwnerRules.ts +248 -0
  7. package/Server/Infrastructure/Postgres/SchemaMigrations/1794200000000-AddSloMonitorRuleMonitorType.ts +19 -0
  8. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  9. package/Server/Services/DatabaseService.ts +10 -0
  10. package/Server/Services/Index.ts +4 -0
  11. package/Server/Services/MonitorService.ts +3 -2
  12. package/Server/Services/ServiceLevelObjectiveLabelRuleEngineService.ts +385 -0
  13. package/Server/Services/ServiceLevelObjectiveLabelRuleService.ts +51 -0
  14. package/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.ts +92 -16
  15. package/Server/Services/ServiceLevelObjectiveMonitorRuleService.ts +36 -1
  16. package/Server/Services/ServiceLevelObjectiveOwnerRuleEngineService.ts +521 -0
  17. package/Server/Services/ServiceLevelObjectiveOwnerRuleService.ts +51 -0
  18. package/Server/Services/ServiceLevelObjectiveService.ts +28 -0
  19. package/Server/Types/Workflow/Components/Telegram/SendMessageToChat.ts +27 -1
  20. package/Server/Utils/AI/Toolbox/SloTools.ts +1 -0
  21. package/Server/Utils/Monitor/AffectedResourceList.ts +31 -73
  22. package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +1 -1
  23. package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +1 -1
  24. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +181 -136
  25. package/Server/Utils/Monitor/RootCauseList.ts +139 -0
  26. package/Server/Utils/Rules/RuleRun/RuleRunRegistry.ts +28 -0
  27. package/Server/Utils/Slo/SloRulePatternValidator.ts +42 -0
  28. package/Tests/App/Dashboard/FeedFilterSortIntegration.test.tsx +1632 -0
  29. package/Tests/App/Dashboard/FeedOptionsLocales.test.ts +250 -0
  30. package/Tests/App/Dashboard/FeedOrdering.test.ts +279 -9
  31. package/Tests/App/Dashboard/InvestigationFeedRefresh.test.tsx +60 -1
  32. package/Tests/App/Dashboard/InvestigationPanel.test.tsx +594 -3
  33. package/Tests/App/Dashboard/ProductOrientation.test.tsx +646 -0
  34. package/Tests/App/Dashboard/ResourceFeed.test.tsx +612 -2
  35. package/Tests/App/Dashboard/ResourceFeedPageWiring.test.ts +30 -0
  36. package/Tests/App/Dashboard/ResourceFeedPagesFilterSort.test.tsx +1202 -0
  37. package/Tests/App/Dashboard/ScheduledMaintenanceFeedRefresh.test.tsx +52 -1
  38. package/Tests/App/Dashboard/SloFeed.test.tsx +106 -1
  39. package/Tests/App/Dashboard/SloMonitorRuleCriteria.test.tsx +566 -0
  40. package/Tests/App/Dashboard/SloNavigationWiring.test.tsx +6 -4
  41. package/Tests/App/Dashboard/SloNoticeBanner.test.tsx +186 -0
  42. package/Tests/App/Dashboard/SloOverviewKpiStrip.test.tsx +388 -44
  43. package/Tests/App/Dashboard/SloOverviewLayout.test.tsx +613 -0
  44. package/Tests/Models/DatabaseModels/ResourceFeedModels.test.ts +0 -8
  45. package/Tests/Models/DatabaseModels/SloLabelAndOwnerRuleModels.test.ts +404 -0
  46. package/Tests/Models/DatabaseModels/SloOverhaulModels.test.ts +17 -1
  47. package/Tests/Models/DatabaseModels/StaticRuleCriteriaInheritance.test.ts +4 -2
  48. package/Tests/Server/Infrastructure/Postgres/AddConfigurableRuleCriteriaMigration.test.ts +3 -0
  49. package/Tests/Server/Infrastructure/Postgres/AddSloLabelAndOwnerRulesMigration.test.ts +407 -0
  50. package/Tests/Server/Infrastructure/Postgres/AddSloMonitorRuleMonitorTypeMigration.test.ts +171 -0
  51. package/Tests/Server/Infrastructure/Postgres/SloProductOverhaulMigration.test.ts +88 -4
  52. package/Tests/Server/Services/DatabaseServiceRuleCriteriaValidation.test.ts +104 -0
  53. package/Tests/Server/Services/RuleRun/LabelRuleEnginesGroupBRun.test.ts +48 -1
  54. package/Tests/Server/Services/RuleRun/OwnerRuleEnginesGroupBRun.test.ts +30 -0
  55. package/Tests/Server/Services/ServiceLevelObjectiveLabelAndOwnerRuleEngines.test.ts +1170 -0
  56. package/Tests/Server/Services/ServiceLevelObjectiveLabelAndOwnerRuleServices.test.ts +172 -0
  57. package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.test.ts +323 -0
  58. package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleService.test.ts +115 -0
  59. package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleTriggers.test.ts +21 -2
  60. package/Tests/Server/Services/ServiceLevelObjectiveMonitorTypePostgres.test.ts +480 -0
  61. package/Tests/Server/Services/ServiceLevelObjectiveService.test.ts +162 -0
  62. package/Tests/Server/Services/StaticRuleCriteriaRuntimeCoverage.test.ts +45 -6
  63. package/Tests/Server/Types/Database/FeedEventTypeFilterQuery.test.ts +745 -0
  64. package/Tests/Server/Types/Markdown.test.ts +2 -1
  65. package/Tests/Server/Types/Workflow/Components/ApiComponentSsrf.test.ts +49 -0
  66. package/Tests/Server/Types/Workflow/Components/TelegramSendMessageToChat.test.ts +331 -0
  67. package/Tests/Server/Utils/AI/SloTools.test.ts +10 -0
  68. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteriaAnomalyUnits.test.ts +1 -1
  69. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluator.test.ts +405 -0
  70. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorBreachingSamples.test.ts +1101 -0
  71. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorMetricUnits.test.ts +106 -80
  72. package/Tests/Server/Utils/Monitor/RootCauseList.test.ts +252 -0
  73. package/Tests/Server/Utils/Rules/RuleRun/RuleRunPermission.test.ts +154 -0
  74. package/Tests/Server/Utils/SSRFProtection.test.ts +111 -0
  75. package/Tests/Server/Utils/Slo/SloRulePatternValidator.test.ts +67 -0
  76. package/Tests/Server/Utils/VM/VMRunnerSsrf.test.ts +213 -0
  77. package/Tests/Types/API/Route.test.ts +56 -0
  78. package/Tests/Types/API/URLSchemeLikePath.test.ts +304 -0
  79. package/Tests/UI/Components/FeedOptions.test.ts +1404 -0
  80. package/Tests/UI/Components/FeedOptionsButton.test.tsx +2603 -0
  81. package/Tests/UI/Components/Icon.test.tsx +26 -0
  82. package/Tests/UI/Components/RuleCriteria/RuleCriteriaModelTable.test.tsx +3 -3
  83. package/Tests/UI/Components/useFeedItemsViewKey.test.tsx +1017 -0
  84. package/Tests/UI/Components/useFeedOptions.test.tsx +1107 -0
  85. package/Tests/Utils/LabelRuleImportExport.test.ts +102 -0
  86. package/Tests/Utils/Slo/SloDuration.test.ts +115 -24
  87. package/Tests/Utils/Slo/SloHealth.test.ts +93 -10
  88. package/Tests/Utils/Slo/SloMonitorRuleCriteria.test.ts +109 -0
  89. package/Tests/Utils/Slo/SloOverviewText.test.ts +64 -0
  90. package/Types/API/Route.ts +19 -2
  91. package/Types/API/URL.ts +26 -2
  92. package/Types/Monitor/MetricMonitor/MetricCriteriaContext.ts +6 -5
  93. package/Types/Monitor/MetricMonitor/MetricMonitorResponse.ts +9 -0
  94. package/Types/Permission.ts +90 -0
  95. package/Types/Rules/RuleCriteriaFieldRegistry.ts +11 -0
  96. package/Types/Rules/RuleRun.ts +4 -0
  97. package/UI/Components/Feed/FeedOptions.ts +427 -0
  98. package/UI/Components/Feed/FeedOptionsButton.tsx +692 -0
  99. package/UI/Components/Feed/useFeedItems.ts +28 -9
  100. package/UI/Components/Feed/useFeedOptions.ts +225 -0
  101. package/UI/Components/Icon/Icon.tsx +1 -1
  102. package/Utils/LabelRuleImportExport.ts +27 -9
  103. package/Utils/Monitor/MetricValueFormatter.ts +2 -2
  104. package/Utils/Rules/MonitorTypeRuleCriteria.ts +31 -0
  105. package/Utils/Slo/SloDuration.ts +22 -6
  106. package/Utils/Slo/SloHealth.ts +8 -7
  107. package/Utils/Slo/SloMonitorRuleCriteria.ts +33 -1
  108. package/Utils/Slo/SloOverviewText.ts +33 -0
  109. package/Utils/Slo/SloProjection.ts +4 -3
  110. package/build/dist/Models/DatabaseModels/Index.js +4 -0
  111. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  112. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveFeed.js +2 -0
  113. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveFeed.js.map +1 -1
  114. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveLabelRule.js +530 -0
  115. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveLabelRule.js.map +1 -0
  116. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveMonitorRule.js +39 -4
  117. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveMonitorRule.js.map +1 -1
  118. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveOwnerRule.js +612 -0
  119. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveOwnerRule.js.map +1 -0
  120. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794100000000-AddSloLabelAndOwnerRules.js +93 -0
  121. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794100000000-AddSloLabelAndOwnerRules.js.map +1 -0
  122. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794200000000-AddSloMonitorRuleMonitorType.js +12 -0
  123. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794200000000-AddSloMonitorRuleMonitorType.js.map +1 -0
  124. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  125. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  126. package/build/dist/Server/Services/DatabaseService.js +7 -0
  127. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  128. package/build/dist/Server/Services/Index.js +4 -0
  129. package/build/dist/Server/Services/Index.js.map +1 -1
  130. package/build/dist/Server/Services/MonitorService.js +3 -2
  131. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  132. package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleEngineService.js +295 -0
  133. package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleEngineService.js.map +1 -0
  134. package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleService.js +56 -0
  135. package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleService.js.map +1 -0
  136. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js +59 -14
  137. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js.map +1 -1
  138. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleService.js +22 -1
  139. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleService.js.map +1 -1
  140. package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleEngineService.js +400 -0
  141. package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleEngineService.js.map +1 -0
  142. package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleService.js +56 -0
  143. package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleService.js.map +1 -0
  144. package/build/dist/Server/Services/ServiceLevelObjectiveService.js +21 -0
  145. package/build/dist/Server/Services/ServiceLevelObjectiveService.js.map +1 -1
  146. package/build/dist/Server/Types/Workflow/Components/Telegram/SendMessageToChat.js +20 -1
  147. package/build/dist/Server/Types/Workflow/Components/Telegram/SendMessageToChat.js.map +1 -1
  148. package/build/dist/Server/Utils/AI/Toolbox/SloTools.js +1 -0
  149. package/build/dist/Server/Utils/AI/Toolbox/SloTools.js.map +1 -1
  150. package/build/dist/Server/Utils/Monitor/AffectedResourceList.js +22 -51
  151. package/build/dist/Server/Utils/Monitor/AffectedResourceList.js.map +1 -1
  152. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +1 -1
  153. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +1 -1
  154. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +139 -109
  155. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  156. package/build/dist/Server/Utils/Monitor/RootCauseList.js +101 -0
  157. package/build/dist/Server/Utils/Monitor/RootCauseList.js.map +1 -0
  158. package/build/dist/Server/Utils/Rules/RuleRun/RuleRunRegistry.js +28 -0
  159. package/build/dist/Server/Utils/Rules/RuleRun/RuleRunRegistry.js.map +1 -1
  160. package/build/dist/Server/Utils/Slo/SloRulePatternValidator.js +28 -0
  161. package/build/dist/Server/Utils/Slo/SloRulePatternValidator.js.map +1 -0
  162. package/build/dist/Types/API/Route.js +18 -2
  163. package/build/dist/Types/API/Route.js.map +1 -1
  164. package/build/dist/Types/API/URL.js +23 -2
  165. package/build/dist/Types/API/URL.js.map +1 -1
  166. package/build/dist/Types/Permission.js +84 -0
  167. package/build/dist/Types/Permission.js.map +1 -1
  168. package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js +11 -0
  169. package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js.map +1 -1
  170. package/build/dist/Types/Rules/RuleRun.js +4 -0
  171. package/build/dist/Types/Rules/RuleRun.js.map +1 -1
  172. package/build/dist/UI/Components/Feed/FeedOptions.js +235 -0
  173. package/build/dist/UI/Components/Feed/FeedOptions.js.map +1 -0
  174. package/build/dist/UI/Components/Feed/FeedOptionsButton.js +346 -0
  175. package/build/dist/UI/Components/Feed/FeedOptionsButton.js.map +1 -0
  176. package/build/dist/UI/Components/Feed/useFeedItems.js +17 -7
  177. package/build/dist/UI/Components/Feed/useFeedItems.js.map +1 -1
  178. package/build/dist/UI/Components/Feed/useFeedOptions.js +130 -0
  179. package/build/dist/UI/Components/Feed/useFeedOptions.js.map +1 -0
  180. package/build/dist/UI/Components/Icon/Icon.js +1 -1
  181. package/build/dist/UI/Components/Icon/Icon.js.map +1 -1
  182. package/build/dist/Utils/LabelRuleImportExport.js +22 -6
  183. package/build/dist/Utils/LabelRuleImportExport.js.map +1 -1
  184. package/build/dist/Utils/Monitor/MetricValueFormatter.js +2 -2
  185. package/build/dist/Utils/Rules/MonitorTypeRuleCriteria.js +18 -0
  186. package/build/dist/Utils/Rules/MonitorTypeRuleCriteria.js.map +1 -0
  187. package/build/dist/Utils/Slo/SloDuration.js +13 -5
  188. package/build/dist/Utils/Slo/SloDuration.js.map +1 -1
  189. package/build/dist/Utils/Slo/SloHealth.js +8 -7
  190. package/build/dist/Utils/Slo/SloHealth.js.map +1 -1
  191. package/build/dist/Utils/Slo/SloMonitorRuleCriteria.js +16 -1
  192. package/build/dist/Utils/Slo/SloMonitorRuleCriteria.js.map +1 -1
  193. package/build/dist/Utils/Slo/SloOverviewText.js +16 -0
  194. package/build/dist/Utils/Slo/SloOverviewText.js.map +1 -1
  195. package/build/dist/Utils/Slo/SloProjection.js.map +1 -1
  196. package/package.json +1 -1
@@ -0,0 +1,172 @@
1
+ import { describe, expect, it } from "@jest/globals";
2
+ import ServiceLevelObjectiveLabelRule from "../../../Models/DatabaseModels/ServiceLevelObjectiveLabelRule";
3
+ import ServiceLevelObjectiveOwnerRule from "../../../Models/DatabaseModels/ServiceLevelObjectiveOwnerRule";
4
+ import ServiceLevelObjectiveLabelRuleService from "../../../Server/Services/ServiceLevelObjectiveLabelRuleService";
5
+ import ServiceLevelObjectiveOwnerRuleService from "../../../Server/Services/ServiceLevelObjectiveOwnerRuleService";
6
+ import BadDataException from "../../../Types/Exception/BadDataException";
7
+ import ObjectID from "../../../Types/ObjectID";
8
+
9
+ /*
10
+ * The SLO label and owner rule services.
11
+ *
12
+ * A pattern that is neither a regex nor a '*' wildcard matches nothing, and the
13
+ * engine can only log that long after the user left the form - so the write is
14
+ * refused, on create AND on update, naming the field in the SLO's own words.
15
+ *
16
+ * And a rule is configuration: unlike the feed-style retention most rule
17
+ * services copy, it is never hard-deleted by age.
18
+ */
19
+
20
+ const PROJECT_ID: ObjectID = new ObjectID(
21
+ "22222222-2222-4222-8222-222222222222",
22
+ );
23
+
24
+ type RuleService =
25
+ | typeof ServiceLevelObjectiveLabelRuleService
26
+ | typeof ServiceLevelObjectiveOwnerRuleService;
27
+
28
+ interface ServiceCase {
29
+ name: string;
30
+ service: RuleService;
31
+ makeRule: () =>
32
+ | ServiceLevelObjectiveLabelRule
33
+ | ServiceLevelObjectiveOwnerRule;
34
+ }
35
+
36
+ const SERVICES: Array<ServiceCase> = [
37
+ {
38
+ name: "ServiceLevelObjectiveLabelRuleService",
39
+ service: ServiceLevelObjectiveLabelRuleService,
40
+ makeRule: () => {
41
+ return new ServiceLevelObjectiveLabelRule();
42
+ },
43
+ },
44
+ {
45
+ name: "ServiceLevelObjectiveOwnerRuleService",
46
+ service: ServiceLevelObjectiveOwnerRuleService,
47
+ makeRule: () => {
48
+ return new ServiceLevelObjectiveOwnerRule();
49
+ },
50
+ },
51
+ ];
52
+
53
+ // Calls a protected hook without widening the service's public surface.
54
+ function callHook(
55
+ service: unknown,
56
+ name: string,
57
+ ...args: Array<unknown>
58
+ ): Promise<unknown> {
59
+ const hooks: Record<
60
+ string,
61
+ (...hookArgs: Array<unknown>) => Promise<unknown>
62
+ > = service as Record<
63
+ string,
64
+ (...hookArgs: Array<unknown>) => Promise<unknown>
65
+ >;
66
+
67
+ return hooks[name]!.apply(service, args);
68
+ }
69
+
70
+ describe.each(SERVICES)("$name", (c: ServiceCase) => {
71
+ function create(fields: Record<string, unknown>): Promise<unknown> {
72
+ const rule:
73
+ | ServiceLevelObjectiveLabelRule
74
+ | ServiceLevelObjectiveOwnerRule = c.makeRule();
75
+ Object.assign(rule as unknown as Record<string, unknown>, {
76
+ projectId: PROJECT_ID,
77
+ name: "Checkout SLOs",
78
+ ...fields,
79
+ });
80
+
81
+ return callHook(c.service, "onBeforeCreate", {
82
+ data: rule,
83
+ props: { isRoot: true },
84
+ });
85
+ }
86
+
87
+ function update(data: Record<string, unknown>): Promise<unknown> {
88
+ return callHook(c.service, "onBeforeUpdate", {
89
+ query: { _id: ObjectID.generate().toString() },
90
+ data: data,
91
+ props: { isRoot: true },
92
+ limit: 1,
93
+ skip: 0,
94
+ });
95
+ }
96
+
97
+ describe("on create", () => {
98
+ it.each([
99
+ ["a regex", "^checkout-.*"],
100
+ ["a '*' wildcard", "*checkout*"],
101
+ ["a plain word", "checkout"],
102
+ ])(
103
+ "accepts %s as the SLO name pattern",
104
+ async (_: string, pattern: string) => {
105
+ await expect(
106
+ create({ serviceLevelObjectiveNamePattern: pattern }),
107
+ ).resolves.toEqual(expect.objectContaining({ carryForward: null }));
108
+ },
109
+ );
110
+
111
+ it("accepts a rule without patterns", async () => {
112
+ await expect(create({})).resolves.toBeDefined();
113
+ });
114
+
115
+ it("refuses a name pattern that can never match, naming the field", async () => {
116
+ await expect(
117
+ create({ serviceLevelObjectiveNamePattern: "checkout-(01" }),
118
+ ).rejects.toThrow(
119
+ new BadDataException(
120
+ `SLO Name Pattern "checkout-(01" is not a valid regular expression, and contains no '*' wildcard to fall back on, so it would never match an SLO. Use a regex such as checkout-.* or a wildcard such as *checkout*.`,
121
+ ),
122
+ );
123
+ });
124
+
125
+ it("refuses a description pattern that can never match, naming the field", async () => {
126
+ await expect(
127
+ create({ serviceLevelObjectiveDescriptionPattern: "[unclosed" }),
128
+ ).rejects.toThrow('SLO Description Pattern "[unclosed"');
129
+ });
130
+
131
+ it("hands the create through unchanged", async () => {
132
+ const result: { createBy: { data: Record<string, unknown> } } =
133
+ (await create({
134
+ serviceLevelObjectiveNamePattern: "*checkout*",
135
+ })) as { createBy: { data: Record<string, unknown> } };
136
+
137
+ expect(result.createBy.data["serviceLevelObjectiveNamePattern"]).toBe(
138
+ "*checkout*",
139
+ );
140
+ });
141
+ });
142
+
143
+ describe("on update", () => {
144
+ it("accepts a valid pattern", async () => {
145
+ await expect(
146
+ update({ serviceLevelObjectiveNamePattern: "*checkout*" }),
147
+ ).resolves.toEqual(expect.objectContaining({ carryForward: null }));
148
+ });
149
+
150
+ // Updating another column must not trip over the patterns it leaves alone.
151
+ it("accepts an update that does not touch the patterns", async () => {
152
+ await expect(update({ isEnabled: false })).resolves.toBeDefined();
153
+ });
154
+
155
+ it("refuses a name pattern that can never match", async () => {
156
+ await expect(
157
+ update({ serviceLevelObjectiveNamePattern: "api-(01" }),
158
+ ).rejects.toThrow('SLO Name Pattern "api-(01"');
159
+ });
160
+
161
+ it("refuses a description pattern that can never match", async () => {
162
+ await expect(
163
+ update({ serviceLevelObjectiveDescriptionPattern: "(" }),
164
+ ).rejects.toThrow("SLO Description Pattern");
165
+ });
166
+ });
167
+
168
+ it("never hard-deletes rules by age", () => {
169
+ expect(c.service.hardDeleteItemByColumnName).toBe("");
170
+ expect(c.service.hardDeleteItemsOlderThanDays).toBe(0);
171
+ });
172
+ });
@@ -14,6 +14,7 @@ import logger from "../../../Server/Utils/Logger";
14
14
  import SloLegacyMonitorLabelAdoption from "../../../Server/Utils/Slo/SloLegacyMonitorLabelAdoption";
15
15
  import FilterCondition from "../../../Types/Filter/FilterCondition";
16
16
  import ObjectID from "../../../Types/ObjectID";
17
+ import MonitorType from "../../../Types/Monitor/MonitorType";
17
18
  import RuleCriteria, {
18
19
  RULE_CRITERIA_LEGACY_NEVER_MATCH_PATTERN,
19
20
  RULE_CRITERIA_SCHEMA_VERSION,
@@ -93,6 +94,7 @@ function fakeMonitor(
93
94
  description?: string | undefined;
94
95
  labelIds?: Array<ObjectID> | undefined;
95
96
  projectId?: ObjectID | null | undefined;
97
+ monitorType?: MonitorType | undefined;
96
98
  },
97
99
  ): Monitor {
98
100
  return {
@@ -101,6 +103,7 @@ function fakeMonitor(
101
103
  projectId: fields?.projectId === undefined ? PROJECT_ID : fields.projectId,
102
104
  name: fields?.name === undefined ? `Monitor ${id.toString()}` : fields.name,
103
105
  description: fields?.description,
106
+ monitorType: fields?.monitorType,
104
107
  labels: (fields?.labelIds || []).map(fakeLabel),
105
108
  } as unknown as Monitor;
106
109
  }
@@ -134,6 +137,7 @@ function fakeRule(fields: {
134
137
  id?: ObjectID | undefined;
135
138
  serviceLevelObjectiveId?: ObjectID | undefined;
136
139
  labelIds?: Array<ObjectID> | undefined;
140
+ monitorType?: MonitorType | undefined;
137
141
  monitorNamePattern?: string | undefined;
138
142
  monitorDescriptionPattern?: string | undefined;
139
143
  criteria?: RuleCriteria | null | undefined;
@@ -148,6 +152,7 @@ function fakeRule(fields: {
148
152
  serviceLevelObjectiveId: fields.serviceLevelObjectiveId || SLO_ID,
149
153
  isEnabled: fields.isEnabled === undefined ? true : fields.isEnabled,
150
154
  monitorLabels: (fields.labelIds || []).map(fakeLabel),
155
+ monitorType: fields.monitorType,
151
156
  monitorNamePattern: fields.monitorNamePattern,
152
157
  monitorDescriptionPattern: fields.monitorDescriptionPattern,
153
158
  criteria: fields.criteria,
@@ -298,6 +303,66 @@ describe("ServiceLevelObjectiveMonitorRuleEngineService.syncMonitorsForSlo", ()
298
303
  jest.restoreAllMocks();
299
304
  });
300
305
 
306
+ it.each([false, true])(
307
+ "backfills matching monitor types and detaches stale rule attachments with criteria=%p",
308
+ async (configured: boolean) => {
309
+ spies.sloFindOneById.mockResolvedValue(
310
+ fakeSlo({
311
+ monitors: [MONITOR_B_ID, MONITOR_C_ID],
312
+ autoAddedMonitors: [MONITOR_B_ID],
313
+ }),
314
+ );
315
+ spies.ruleFindBy.mockResolvedValue([
316
+ fakeRule(
317
+ configured
318
+ ? {
319
+ criteria: criteria(FilterCondition.All, [
320
+ {
321
+ field: "monitorType",
322
+ operator: RuleCriteriaOperator.Equals,
323
+ value: MonitorType.API,
324
+ },
325
+ ]),
326
+ monitorNamePattern: RULE_CRITERIA_LEGACY_NEVER_MATCH_PATTERN,
327
+ }
328
+ : { monitorType: MonitorType.API },
329
+ ),
330
+ ]);
331
+ spies.monitorFindBy.mockResolvedValue([
332
+ fakeMonitor(MONITOR_A_ID, { monitorType: MonitorType.API }),
333
+ fakeMonitor(MONITOR_B_ID, { monitorType: MonitorType.Website }),
334
+ fakeMonitor(MONITOR_C_ID, { monitorType: MonitorType.Ping }),
335
+ ]);
336
+
337
+ const result: SloMonitorSyncResult = await syncSlo();
338
+
339
+ expect(result).toEqual({
340
+ monitorIdsAdded: [MONITOR_A_ID.toString()],
341
+ monitorIdsRemoved: [MONITOR_B_ID.toString()],
342
+ });
343
+ expect(writtenIds(spies.sloUpdateOneById, "monitors")).toEqual(
344
+ sortedIds([MONITOR_A_ID, MONITOR_C_ID]),
345
+ );
346
+ expect(writtenIds(spies.sloUpdateOneById, "autoAddedMonitors")).toEqual([
347
+ MONITOR_A_ID.toString(),
348
+ ]);
349
+ expect(spies.monitorFindBy).toHaveBeenCalledWith(
350
+ expect.objectContaining({
351
+ query: { projectId: PROJECT_ID },
352
+ select: expect.objectContaining({ monitorType: true }),
353
+ }),
354
+ );
355
+ expect(spies.ruleFindBy).toHaveBeenCalledWith(
356
+ expect.objectContaining({
357
+ select: expect.objectContaining({
358
+ monitorType: true,
359
+ criteria: true,
360
+ }),
361
+ }),
362
+ );
363
+ },
364
+ );
365
+
301
366
  it("attaches every monitor an enabled rule's labels match", async () => {
302
367
  spies.ruleFindBy.mockResolvedValue([
303
368
  fakeRule({ labelIds: [LABEL_PRODUCTION_ID] }),
@@ -536,6 +601,7 @@ describe("ServiceLevelObjectiveMonitorRuleEngineService.syncMonitorsForSlo", ()
536
601
  _id: true,
537
602
  name: true,
538
603
  description: true,
604
+ monitorType: true,
539
605
  labels: { _id: true },
540
606
  });
541
607
  expect(call.props).toEqual({ isRoot: true });
@@ -998,6 +1064,51 @@ describe("ServiceLevelObjectiveMonitorRuleEngineService.syncSlosForMonitor", ()
998
1064
  );
999
1065
  }
1000
1066
 
1067
+ it.each([MonitorType.API, MonitorType.Website])(
1068
+ "reconciles monitor-side membership when the monitor type becomes %s",
1069
+ async (monitorType: MonitorType) => {
1070
+ const shouldMatch: boolean = monitorType === MonitorType.API;
1071
+ spies.monitorFindOneById.mockResolvedValue(
1072
+ fakeMonitor(MONITOR_A_ID, { monitorType }),
1073
+ );
1074
+ spies.ruleFindBy.mockResolvedValue([
1075
+ fakeRule({
1076
+ criteria: criteria(FilterCondition.All, [
1077
+ {
1078
+ field: "monitorType",
1079
+ operator: RuleCriteriaOperator.Equals,
1080
+ value: MonitorType.API,
1081
+ },
1082
+ ]),
1083
+ }),
1084
+ ]);
1085
+ spies.sloFindBy.mockImplementation(
1086
+ answerSloReads({
1087
+ byId: [
1088
+ fakeSlo({
1089
+ monitors: shouldMatch ? [] : [MONITOR_A_ID],
1090
+ autoAddedMonitors: shouldMatch ? [] : [MONITOR_A_ID],
1091
+ }),
1092
+ ],
1093
+ }) as never,
1094
+ );
1095
+
1096
+ const results: Array<SloMonitorSyncResult> = await syncMonitor();
1097
+
1098
+ expect(results).toEqual([
1099
+ {
1100
+ monitorIdsAdded: shouldMatch ? [MONITOR_A_ID.toString()] : [],
1101
+ monitorIdsRemoved: shouldMatch ? [] : [MONITOR_A_ID.toString()],
1102
+ },
1103
+ ]);
1104
+ expect(spies.monitorFindOneById).toHaveBeenCalledWith(
1105
+ expect.objectContaining({
1106
+ select: expect.objectContaining({ monitorType: true }),
1107
+ }),
1108
+ );
1109
+ },
1110
+ );
1111
+
1001
1112
  it("attaches the monitor to an SLO whose enabled rule its labels satisfy", async () => {
1002
1113
  spies.ruleFindBy.mockResolvedValue([
1003
1114
  fakeRule({ labelIds: [LABEL_PRODUCTION_ID] }),
@@ -1320,6 +1431,7 @@ describe("ServiceLevelObjectiveMonitorRuleEngineService.syncSlosForMonitor", ()
1320
1431
  projectId: true,
1321
1432
  name: true,
1322
1433
  description: true,
1434
+ monitorType: true,
1323
1435
  labels: { _id: true },
1324
1436
  });
1325
1437
  });
@@ -1793,3 +1905,214 @@ describe("ServiceLevelObjectiveMonitorRuleEngineService.doesMonitorMatchRule", (
1793
1905
  });
1794
1906
  }
1795
1907
  });
1908
+
1909
+ describe("SLO monitor type matching", () => {
1910
+ function matches(
1911
+ filters: Array<RuleCriteriaFilter>,
1912
+ monitorType: MonitorType | undefined,
1913
+ condition: FilterCondition = FilterCondition.All,
1914
+ ): boolean {
1915
+ return ServiceLevelObjectiveMonitorRuleEngineService.doesMonitorMatchRule({
1916
+ monitor: fakeMonitor(MONITOR_A_ID, {
1917
+ monitorType,
1918
+ name: "api-production",
1919
+ description: "primary",
1920
+ labelIds: [LABEL_PRODUCTION_ID],
1921
+ }),
1922
+ rule: fakeRule({
1923
+ criteria: criteria(condition, filters),
1924
+ monitorNamePattern: RULE_CRITERIA_LEGACY_NEVER_MATCH_PATTERN,
1925
+ }),
1926
+ });
1927
+ }
1928
+
1929
+ it.each(Object.values(MonitorType))(
1930
+ "matches exact enum type %s",
1931
+ (monitorType: MonitorType) => {
1932
+ expect(
1933
+ matches(
1934
+ [
1935
+ {
1936
+ field: "monitorType",
1937
+ operator: RuleCriteriaOperator.Equals,
1938
+ value: monitorType,
1939
+ },
1940
+ ],
1941
+ monitorType,
1942
+ ),
1943
+ ).toBe(true);
1944
+ expect(
1945
+ matches(
1946
+ [
1947
+ {
1948
+ field: "monitorType",
1949
+ operator: RuleCriteriaOperator.NotEquals,
1950
+ value: monitorType,
1951
+ },
1952
+ ],
1953
+ monitorType,
1954
+ ),
1955
+ ).toBe(false);
1956
+ },
1957
+ );
1958
+
1959
+ it("distinguishes Docker from Docker Swarm with both equality operators", () => {
1960
+ expect(
1961
+ matches(
1962
+ [
1963
+ {
1964
+ field: "monitorType",
1965
+ operator: RuleCriteriaOperator.Equals,
1966
+ value: MonitorType.Docker,
1967
+ },
1968
+ ],
1969
+ MonitorType.DockerSwarm,
1970
+ ),
1971
+ ).toBe(false);
1972
+ expect(
1973
+ matches(
1974
+ [
1975
+ {
1976
+ field: "monitorType",
1977
+ operator: RuleCriteriaOperator.NotEquals,
1978
+ value: MonitorType.Docker,
1979
+ },
1980
+ ],
1981
+ MonitorType.DockerSwarm,
1982
+ ),
1983
+ ).toBe(true);
1984
+ });
1985
+
1986
+ it.each([undefined, "Unknown" as MonitorType])(
1987
+ "does not match missing or invalid monitor types (%s), including negation",
1988
+ (monitorType: MonitorType | undefined) => {
1989
+ expect(
1990
+ matches(
1991
+ [
1992
+ {
1993
+ field: "monitorType",
1994
+ operator: RuleCriteriaOperator.Equals,
1995
+ value: MonitorType.API,
1996
+ },
1997
+ ],
1998
+ monitorType,
1999
+ ),
2000
+ ).toBe(false);
2001
+ expect(
2002
+ matches(
2003
+ [
2004
+ {
2005
+ field: "monitorType",
2006
+ operator: RuleCriteriaOperator.NotEquals,
2007
+ value: MonitorType.API,
2008
+ },
2009
+ ],
2010
+ monitorType,
2011
+ ),
2012
+ ).toBe(false);
2013
+ },
2014
+ );
2015
+
2016
+ it.each([
2017
+ { operator: RuleCriteriaOperator.Equals, value: "Unknown" },
2018
+ { operator: RuleCriteriaOperator.NotEquals, value: "api" },
2019
+ { operator: RuleCriteriaOperator.NotEquals, value: true },
2020
+ { operator: RuleCriteriaOperator.Contains, value: MonitorType.API },
2021
+ { operator: RuleCriteriaOperator.DoesNotMatchPattern, value: ".*" },
2022
+ { operator: RuleCriteriaOperator.HasNoneOf, value: [MonitorType.API] },
2023
+ ])(
2024
+ "fails the entire rule for invalid type predicate %p even under Match any",
2025
+ (filter: Omit<RuleCriteriaFilter, "field">) => {
2026
+ expect(
2027
+ matches(
2028
+ [
2029
+ { field: "monitorType", ...filter },
2030
+ {
2031
+ field: "monitorNamePattern",
2032
+ operator: RuleCriteriaOperator.Contains,
2033
+ value: "api",
2034
+ },
2035
+ ],
2036
+ MonitorType.API,
2037
+ FilterCondition.Any,
2038
+ ),
2039
+ ).toBe(false);
2040
+ },
2041
+ );
2042
+
2043
+ it("combines types with name, description, and label conditions", () => {
2044
+ const filters: Array<RuleCriteriaFilter> = [
2045
+ {
2046
+ field: "monitorType",
2047
+ operator: RuleCriteriaOperator.Equals,
2048
+ value: MonitorType.API,
2049
+ },
2050
+ {
2051
+ field: "monitorNamePattern",
2052
+ operator: RuleCriteriaOperator.MatchesPattern,
2053
+ value: "api-*",
2054
+ },
2055
+ {
2056
+ field: "monitorDescriptionPattern",
2057
+ operator: RuleCriteriaOperator.DoesNotMatchPattern,
2058
+ value: "standby*",
2059
+ },
2060
+ {
2061
+ field: "monitorLabels",
2062
+ operator: RuleCriteriaOperator.HasAnyOf,
2063
+ value: [LABEL_PRODUCTION_ID.toString()],
2064
+ },
2065
+ ];
2066
+ expect(matches(filters, MonitorType.API)).toBe(true);
2067
+ expect(matches(filters, MonitorType.Website)).toBe(false);
2068
+ expect(matches(filters, MonitorType.Website, FilterCondition.Any)).toBe(
2069
+ true,
2070
+ );
2071
+ });
2072
+
2073
+ it("supports several type alternatives", () => {
2074
+ const filters: Array<RuleCriteriaFilter> = [
2075
+ MonitorType.API,
2076
+ MonitorType.Website,
2077
+ ].map((monitorType: MonitorType): RuleCriteriaFilter => {
2078
+ return {
2079
+ field: "monitorType",
2080
+ operator: RuleCriteriaOperator.Equals,
2081
+ value: monitorType,
2082
+ };
2083
+ });
2084
+ expect(matches(filters, MonitorType.Website, FilterCondition.Any)).toBe(
2085
+ true,
2086
+ );
2087
+ expect(matches(filters, MonitorType.Ping, FilterCondition.Any)).toBe(false);
2088
+ expect(matches(filters, MonitorType.API)).toBe(false);
2089
+ });
2090
+
2091
+ it("ANDs a legacy type with legacy name and labels", () => {
2092
+ const rule: ServiceLevelObjectiveMonitorRule = fakeRule({
2093
+ monitorType: MonitorType.Docker,
2094
+ monitorNamePattern: "api-*",
2095
+ labelIds: [LABEL_PRODUCTION_ID],
2096
+ });
2097
+ expect(
2098
+ ServiceLevelObjectiveMonitorRuleEngineService.doesMonitorMatchRule({
2099
+ rule,
2100
+ monitor: fakeMonitor(MONITOR_A_ID, {
2101
+ monitorType: MonitorType.Docker,
2102
+ name: "api-prod",
2103
+ labelIds: [LABEL_PRODUCTION_ID],
2104
+ }),
2105
+ }),
2106
+ ).toBe(true);
2107
+ expect(
2108
+ ServiceLevelObjectiveMonitorRuleEngineService.doesMonitorMatchRule({
2109
+ rule,
2110
+ monitor: fakeMonitor(MONITOR_A_ID, {
2111
+ monitorType: MonitorType.DockerSwarm,
2112
+ name: "api-prod",
2113
+ labelIds: [LABEL_PRODUCTION_ID],
2114
+ }),
2115
+ }),
2116
+ ).toBe(false);
2117
+ });
2118
+ });
@@ -21,6 +21,7 @@ import LIMIT_MAX from "../../../Types/Database/LimitMax";
21
21
  import BadDataException from "../../../Types/Exception/BadDataException";
22
22
  import FilterCondition from "../../../Types/Filter/FilterCondition";
23
23
  import ObjectID from "../../../Types/ObjectID";
24
+ import MonitorType from "../../../Types/Monitor/MonitorType";
24
25
  import RuleCriteria, {
25
26
  RULE_CRITERIA_SCHEMA_VERSION,
26
27
  RuleCriteriaOperator,
@@ -101,6 +102,7 @@ function makeRule(fields: {
101
102
  description?: string | undefined;
102
103
  isEnabled?: boolean | undefined;
103
104
  labelIds?: Array<ObjectID> | undefined;
105
+ monitorType?: MonitorType | undefined;
104
106
  monitorNamePattern?: string | undefined;
105
107
  monitorDescriptionPattern?: string | undefined;
106
108
  criteria?: RuleCriteria | null | undefined;
@@ -119,6 +121,7 @@ function makeRule(fields: {
119
121
  monitorLabels: (fields.labelIds || []).map((labelId: ObjectID) => {
120
122
  return { id: labelId, _id: labelId.toString() } as unknown as Label;
121
123
  }),
124
+ monitorType: fields.monitorType,
122
125
  monitorNamePattern: fields.monitorNamePattern,
123
126
  monitorDescriptionPattern: fields.monitorDescriptionPattern,
124
127
  criteria: fields.criteria,
@@ -247,6 +250,72 @@ describe("ServiceLevelObjectiveMonitorRuleService.onBeforeCreate", () => {
247
250
  jest.restoreAllMocks();
248
251
  });
249
252
 
253
+ it.each([true, false])(
254
+ "accepts a monitor-type-only rule with configured criteria=%p",
255
+ async (configured: boolean) => {
256
+ await expect(
257
+ callHook(
258
+ "onBeforeCreate",
259
+ makeCreateBy({
260
+ projectId: PROJECT_ID,
261
+ serviceLevelObjectiveId: SLO_ID,
262
+ ...(configured
263
+ ? {
264
+ criteria: criteria(FilterCondition.All, [
265
+ {
266
+ field: "monitorType",
267
+ operator: RuleCriteriaOperator.Equals,
268
+ value: MonitorType.API,
269
+ },
270
+ ]),
271
+ }
272
+ : { monitorType: MonitorType.API }),
273
+ }),
274
+ ),
275
+ ).resolves.toBeDefined();
276
+ },
277
+ );
278
+
279
+ it.each([
280
+ { monitorType: "Unknown" },
281
+ {
282
+ criteria: criteria(FilterCondition.All, [
283
+ {
284
+ field: "monitorType",
285
+ operator: RuleCriteriaOperator.NotEquals,
286
+ value: "Unknown",
287
+ },
288
+ ]),
289
+ },
290
+ {
291
+ criteria: criteria(FilterCondition.All, [
292
+ {
293
+ field: "monitorType",
294
+ operator: RuleCriteriaOperator.Contains,
295
+ value: MonitorType.API,
296
+ },
297
+ ]),
298
+ },
299
+ ])(
300
+ "rejects invalid monitor type conditions before checking or adopting an SLO: %p",
301
+ async (fields: Record<string, unknown>) => {
302
+ await expect(
303
+ callHook(
304
+ "onBeforeCreate",
305
+ makeCreateBy({
306
+ projectId: PROJECT_ID,
307
+ serviceLevelObjectiveId: SLO_ID,
308
+ ...fields,
309
+ }),
310
+ ),
311
+ ).rejects.toThrow("Monitor type criteria");
312
+ expect(spies.referenceValidator).not.toHaveBeenCalled();
313
+ expect(
314
+ SloLegacyMonitorLabelAdoption.adoptLegacyMonitorLabels,
315
+ ).not.toHaveBeenCalled();
316
+ },
317
+ );
318
+
250
319
  it("refuses a rule that names no SLO", async () => {
251
320
  const promise: Promise<unknown> = callHook(
252
321
  "onBeforeCreate",
@@ -627,6 +696,52 @@ describe("ServiceLevelObjectiveMonitorRuleService.onBeforeUpdate", () => {
627
696
  jest.restoreAllMocks();
628
697
  });
629
698
 
699
+ it("keeps a legacy type condition when clearing a different condition", async () => {
700
+ spies.ruleFindBy.mockResolvedValue([
701
+ makeRule({ monitorType: MonitorType.API, monitorNamePattern: "api-*" }),
702
+ ]);
703
+ await expect(
704
+ callHook("onBeforeUpdate", makeUpdateBy({ monitorNamePattern: null })),
705
+ ).resolves.toBeDefined();
706
+ expect(spies.ruleFindBy).toHaveBeenCalledWith(
707
+ expect.objectContaining({
708
+ select: expect.objectContaining({ monitorType: true }),
709
+ }),
710
+ );
711
+ });
712
+
713
+ it("rejects removing the only legacy type condition", async () => {
714
+ spies.ruleFindBy.mockResolvedValue([
715
+ makeRule({ monitorType: MonitorType.API }),
716
+ ]);
717
+ await expect(
718
+ callHook("onBeforeUpdate", makeUpdateBy({ monitorType: null })),
719
+ ).rejects.toThrow("at least one match criterion");
720
+ });
721
+
722
+ it("validates a changed legacy monitor type and configured monitor type", async () => {
723
+ spies.ruleFindBy.mockResolvedValue([
724
+ makeRule({ monitorType: MonitorType.API }),
725
+ ]);
726
+ await expect(
727
+ callHook("onBeforeUpdate", makeUpdateBy({ monitorType: "Unknown" })),
728
+ ).rejects.toThrow("valid monitor type");
729
+ await expect(
730
+ callHook(
731
+ "onBeforeUpdate",
732
+ makeUpdateBy({
733
+ criteria: criteria(FilterCondition.All, [
734
+ {
735
+ field: "monitorType",
736
+ operator: RuleCriteriaOperator.DoesNotMatchPattern,
737
+ value: MonitorType.API,
738
+ },
739
+ ]),
740
+ }),
741
+ ),
742
+ ).rejects.toThrow("only support Equals");
743
+ });
744
+
630
745
  it("refuses a pattern the engine could never match, before reading anything", async () => {
631
746
  await expect(
632
747
  callHook(