@oneuptime/common 12.0.14 → 12.0.15

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 (195) hide show
  1. package/Models/DatabaseModels/GlobalOidc.ts +20 -0
  2. package/Models/DatabaseModels/GlobalSso.ts +20 -0
  3. package/Models/DatabaseModels/MarketingConversion.ts +19 -9
  4. package/Models/DatabaseModels/NetworkDeviceLinkRule.ts +46 -0
  5. package/Models/DatabaseModels/StatusPage.ts +7 -4
  6. package/Server/API/StatusPageAPI.ts +29 -4
  7. package/Server/EnvironmentConfig.ts +7 -0
  8. package/Server/Infrastructure/Postgres/SchemaMigrations/1787800000000-AddScopeToNetworkDeviceLinkRule.ts +19 -0
  9. package/Server/Infrastructure/Postgres/SchemaMigrations/1787900000000-AddRestrictToAttachedProjectsToGlobalSso.ts +40 -0
  10. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  11. package/Server/Middleware/SlackAuthorization.ts +43 -6
  12. package/Server/Middleware/UserAuthorization.ts +236 -26
  13. package/Server/Middleware/WhatsAppAuthorization.ts +43 -3
  14. package/Server/Services/GlobalOidcProjectService.ts +126 -1
  15. package/Server/Services/GlobalOidcService.ts +132 -0
  16. package/Server/Services/GlobalSsoProjectService.ts +126 -1
  17. package/Server/Services/GlobalSsoService.ts +132 -0
  18. package/Server/Services/UserEmailService.ts +4 -4
  19. package/Server/Utils/CronTab.ts +73 -0
  20. package/Server/Utils/GlobalSsoAuthorization.ts +175 -0
  21. package/Server/Utils/Marketing/ConversionUploadProvider.ts +35 -5
  22. package/Server/Utils/Marketing/Providers/GoogleAds.ts +1 -1
  23. package/Server/Utils/Marketing/Providers/LinkedIn.ts +1 -1
  24. package/Server/Utils/Marketing/Providers/Meta.ts +1 -1
  25. package/Server/Utils/Marketing/Providers/MicrosoftAds.ts +1 -1
  26. package/Server/Utils/Marketing/Providers/Reddit.ts +1 -1
  27. package/Server/Utils/Monitor/Criteria/APIRequestCriteria.ts +48 -33
  28. package/Server/Utils/Monitor/Criteria/DnsMonitorCriteria.ts +35 -30
  29. package/Server/Utils/Monitor/Criteria/DomainMonitorCriteria.ts +33 -28
  30. package/Server/Utils/Monitor/Criteria/EvaluateOverTime.ts +475 -47
  31. package/Server/Utils/Monitor/Criteria/ExternalStatusPageMonitorCriteria.ts +35 -30
  32. package/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.ts +34 -28
  33. package/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.ts +33 -28
  34. package/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.ts +69 -40
  35. package/Server/Utils/Monitor/Criteria/SnmpMonitorCriteria.ts +35 -29
  36. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +8 -0
  37. package/Server/Utils/Monitor/MonitorResource.ts +161 -21
  38. package/Server/Utils/StatusPageContentSecurityPolicy.ts +56 -0
  39. package/Server/Utils/StatusPageCustomizationAccess.ts +117 -0
  40. package/Tests/Server/API/StatusPageCustomizationOrigin.test.ts +348 -0
  41. package/Tests/Server/Infrastructure/Postgres/FixTotpOtpUrlAlgorithmMigration.test.ts +9 -59
  42. package/Tests/Server/Infrastructure/Postgres/SchemaMigrationsOrdering.test.ts +227 -0
  43. package/Tests/Server/Middleware/SlackAuthorization.test.ts +149 -0
  44. package/Tests/Server/Middleware/UserAuthorization.test.ts +27 -13
  45. package/Tests/Server/Middleware/UserAuthorizationGlobalSsoGovernance.test.ts +1455 -0
  46. package/Tests/Server/Middleware/UserAuthorizationSSOProvider.test.ts +91 -2
  47. package/Tests/Server/Middleware/WhatsAppAuthorization.test.ts +233 -0
  48. package/Tests/Server/Services/DiscoveryScanClaimHookFreeSafety.test.ts +21 -8
  49. package/Tests/Server/Services/GlobalSsoProviderAuthorization.test.ts +1242 -0
  50. package/Tests/Server/Services/MonitorResourceProbeAgreementHydration.test.ts +459 -0
  51. package/Tests/Server/Services/MonitorResourceProbeAgreementReuse.test.ts +10 -0
  52. package/Tests/Server/Types/AnalyticsDatabase/AggregateUtilBucketTimestamp.test.ts +416 -0
  53. package/Tests/Server/Types/Database/QueryHelperManyToManyAndEmptyValues.test.ts +551 -0
  54. package/Tests/Server/Types/Database/SelectUtil.test.ts +348 -0
  55. package/Tests/Server/Utils/CronTab.test.ts +116 -0
  56. package/Tests/Server/Utils/GlobalSSOToken.test.ts +581 -0
  57. package/Tests/Server/Utils/GlobalSsoAuthorization.test.ts +1401 -0
  58. package/Tests/Server/Utils/Marketing/AdUploadableConversionTypes.test.ts +254 -0
  59. package/Tests/Server/Utils/Marketing/ConversionUploadProvider.test.ts +1 -1
  60. package/Tests/Server/Utils/Monitor/Criteria/APIRequestCriteriaEvaluateOverTime.test.ts +508 -0
  61. package/Tests/Server/Utils/Monitor/Criteria/APIRequestCriteriaPortTimings.test.ts +30 -9
  62. package/Tests/Server/Utils/Monitor/Criteria/EvaluateOverTime.test.ts +987 -0
  63. package/Tests/Server/Utils/Monitor/Criteria/ExternalStatusPageMonitorCriteria.test.ts +916 -0
  64. package/Tests/Server/Utils/Monitor/Criteria/ProfileMonitorCriteria.test.ts +382 -0
  65. package/Tests/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.test.ts +1219 -0
  66. package/Tests/Server/Utils/Monitor/MonitorResourceIngestedStepSelection.test.ts +86 -0
  67. package/Tests/Server/Utils/StatusPageContentSecurityPolicy.test.ts +136 -0
  68. package/Tests/Server/Utils/StatusPageCustomizationAccess.test.ts +223 -0
  69. package/Tests/Types/Monitor/MonitorStepConfigHelpers.test.ts +165 -0
  70. package/Tests/Types/NetworkDevice/NetworkDeviceLinkRuleScope.test.ts +215 -0
  71. package/Tests/UI/Components/Forms/FormSubmitAnalytics.test.tsx +206 -0
  72. package/Tests/UI/Utils/AIChatExport/ConversationMarkdown.test.ts +774 -0
  73. package/Tests/UI/Utils/AIChatExport/MarkdownBlocks.test.ts +791 -0
  74. package/Tests/UI/Utils/AIChatExport/MarkdownSafety.test.ts +544 -0
  75. package/Tests/Utils/Analytics.test.ts +187 -0
  76. package/Tests/Utils/Monitor/MonitorMetricType.test.ts +80 -0
  77. package/Tests/Utils/Monitor/NetworkDeviceLinkRuleUtil.test.ts +1291 -0
  78. package/Tests/Utils/NetworkDiscovery/UnclaimedScanDiagnosis.test.ts +214 -0
  79. package/Types/Marketing/MarketingConversion.ts +12 -0
  80. package/Types/Monitor/CriteriaFilter.ts +9 -0
  81. package/Types/NetworkDevice/NetworkDeviceLinkRuleScope.ts +51 -0
  82. package/UI/Components/FormModal/BasicFormModal.tsx +12 -0
  83. package/UI/Components/Forms/BasicForm.tsx +13 -1
  84. package/UI/Components/Forms/BasicModelForm.tsx +2 -1
  85. package/UI/Components/Forms/ModelForm.tsx +10 -1
  86. package/UI/Components/Forms/Utils/FormAnalyticsName.ts +39 -0
  87. package/UI/Components/ModelFormModal/ModelFormModal.tsx +6 -1
  88. package/Utils/Analytics.ts +55 -2
  89. package/Utils/Monitor/MonitorMetricType.ts +40 -0
  90. package/Utils/Monitor/NetworkDeviceLinkRuleUtil.ts +587 -37
  91. package/Utils/NetworkDiscovery/UnclaimedScanDiagnosis.ts +115 -0
  92. package/build/dist/Models/DatabaseModels/GlobalOidc.js +21 -0
  93. package/build/dist/Models/DatabaseModels/GlobalOidc.js.map +1 -1
  94. package/build/dist/Models/DatabaseModels/GlobalSso.js +21 -0
  95. package/build/dist/Models/DatabaseModels/GlobalSso.js.map +1 -1
  96. package/build/dist/Models/DatabaseModels/MarketingConversion.js +20 -10
  97. package/build/dist/Models/DatabaseModels/MarketingConversion.js.map +1 -1
  98. package/build/dist/Models/DatabaseModels/NetworkDeviceLinkRule.js +47 -0
  99. package/build/dist/Models/DatabaseModels/NetworkDeviceLinkRule.js.map +1 -1
  100. package/build/dist/Models/DatabaseModels/StatusPage.js +4 -4
  101. package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
  102. package/build/dist/Server/API/StatusPageAPI.js +25 -4
  103. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  104. package/build/dist/Server/EnvironmentConfig.js +6 -0
  105. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  106. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787800000000-AddScopeToNetworkDeviceLinkRule.js +12 -0
  107. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787800000000-AddScopeToNetworkDeviceLinkRule.js.map +1 -0
  108. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787900000000-AddRestrictToAttachedProjectsToGlobalSso.js +29 -0
  109. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787900000000-AddRestrictToAttachedProjectsToGlobalSso.js.map +1 -0
  110. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  111. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  112. package/build/dist/Server/Middleware/SlackAuthorization.js +28 -4
  113. package/build/dist/Server/Middleware/SlackAuthorization.js.map +1 -1
  114. package/build/dist/Server/Middleware/UserAuthorization.js +197 -22
  115. package/build/dist/Server/Middleware/UserAuthorization.js.map +1 -1
  116. package/build/dist/Server/Middleware/WhatsAppAuthorization.js +32 -2
  117. package/build/dist/Server/Middleware/WhatsAppAuthorization.js.map +1 -1
  118. package/build/dist/Server/Services/GlobalOidcProjectService.js +105 -0
  119. package/build/dist/Server/Services/GlobalOidcProjectService.js.map +1 -1
  120. package/build/dist/Server/Services/GlobalOidcService.js +127 -0
  121. package/build/dist/Server/Services/GlobalOidcService.js.map +1 -1
  122. package/build/dist/Server/Services/GlobalSsoProjectService.js +105 -0
  123. package/build/dist/Server/Services/GlobalSsoProjectService.js.map +1 -1
  124. package/build/dist/Server/Services/GlobalSsoService.js +127 -0
  125. package/build/dist/Server/Services/GlobalSsoService.js.map +1 -1
  126. package/build/dist/Server/Services/UserEmailService.js +4 -4
  127. package/build/dist/Server/Utils/CronTab.js +66 -0
  128. package/build/dist/Server/Utils/CronTab.js.map +1 -1
  129. package/build/dist/Server/Utils/GlobalSsoAuthorization.js +112 -0
  130. package/build/dist/Server/Utils/GlobalSsoAuthorization.js.map +1 -0
  131. package/build/dist/Server/Utils/Marketing/ConversionUploadProvider.js +17 -1
  132. package/build/dist/Server/Utils/Marketing/ConversionUploadProvider.js.map +1 -1
  133. package/build/dist/Server/Utils/Marketing/Providers/GoogleAds.js +1 -1
  134. package/build/dist/Server/Utils/Marketing/Providers/GoogleAds.js.map +1 -1
  135. package/build/dist/Server/Utils/Marketing/Providers/LinkedIn.js +1 -1
  136. package/build/dist/Server/Utils/Marketing/Providers/LinkedIn.js.map +1 -1
  137. package/build/dist/Server/Utils/Marketing/Providers/Meta.js +1 -1
  138. package/build/dist/Server/Utils/Marketing/Providers/Meta.js.map +1 -1
  139. package/build/dist/Server/Utils/Marketing/Providers/MicrosoftAds.js +1 -1
  140. package/build/dist/Server/Utils/Marketing/Providers/MicrosoftAds.js.map +1 -1
  141. package/build/dist/Server/Utils/Marketing/Providers/Reddit.js +1 -1
  142. package/build/dist/Server/Utils/Marketing/Providers/Reddit.js.map +1 -1
  143. package/build/dist/Server/Utils/Monitor/Criteria/APIRequestCriteria.js +37 -35
  144. package/build/dist/Server/Utils/Monitor/Criteria/APIRequestCriteria.js.map +1 -1
  145. package/build/dist/Server/Utils/Monitor/Criteria/DnsMonitorCriteria.js +21 -25
  146. package/build/dist/Server/Utils/Monitor/Criteria/DnsMonitorCriteria.js.map +1 -1
  147. package/build/dist/Server/Utils/Monitor/Criteria/DomainMonitorCriteria.js +20 -22
  148. package/build/dist/Server/Utils/Monitor/Criteria/DomainMonitorCriteria.js.map +1 -1
  149. package/build/dist/Server/Utils/Monitor/Criteria/EvaluateOverTime.js +370 -42
  150. package/build/dist/Server/Utils/Monitor/Criteria/EvaluateOverTime.js.map +1 -1
  151. package/build/dist/Server/Utils/Monitor/Criteria/ExternalStatusPageMonitorCriteria.js +21 -25
  152. package/build/dist/Server/Utils/Monitor/Criteria/ExternalStatusPageMonitorCriteria.js.map +1 -1
  153. package/build/dist/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.js +21 -25
  154. package/build/dist/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.js.map +1 -1
  155. package/build/dist/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.js +21 -25
  156. package/build/dist/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.js.map +1 -1
  157. package/build/dist/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.js +64 -52
  158. package/build/dist/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.js.map +1 -1
  159. package/build/dist/Server/Utils/Monitor/Criteria/SnmpMonitorCriteria.js +25 -29
  160. package/build/dist/Server/Utils/Monitor/Criteria/SnmpMonitorCriteria.js.map +1 -1
  161. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +8 -0
  162. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  163. package/build/dist/Server/Utils/Monitor/MonitorResource.js +123 -23
  164. package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
  165. package/build/dist/Server/Utils/StatusPageContentSecurityPolicy.js +39 -0
  166. package/build/dist/Server/Utils/StatusPageContentSecurityPolicy.js.map +1 -0
  167. package/build/dist/Server/Utils/StatusPageCustomizationAccess.js +85 -0
  168. package/build/dist/Server/Utils/StatusPageCustomizationAccess.js.map +1 -0
  169. package/build/dist/Types/Marketing/MarketingConversion.js +10 -0
  170. package/build/dist/Types/Marketing/MarketingConversion.js.map +1 -1
  171. package/build/dist/Types/Monitor/CriteriaFilter.js +1 -0
  172. package/build/dist/Types/Monitor/CriteriaFilter.js.map +1 -1
  173. package/build/dist/Types/NetworkDevice/NetworkDeviceLinkRuleScope.js +46 -0
  174. package/build/dist/Types/NetworkDevice/NetworkDeviceLinkRuleScope.js.map +1 -0
  175. package/build/dist/UI/Components/FormModal/BasicFormModal.js +2 -1
  176. package/build/dist/UI/Components/FormModal/BasicFormModal.js.map +1 -1
  177. package/build/dist/UI/Components/Forms/BasicForm.js +10 -1
  178. package/build/dist/UI/Components/Forms/BasicForm.js.map +1 -1
  179. package/build/dist/UI/Components/Forms/BasicModelForm.js +2 -1
  180. package/build/dist/UI/Components/Forms/BasicModelForm.js.map +1 -1
  181. package/build/dist/UI/Components/Forms/ModelForm.js +4 -1
  182. package/build/dist/UI/Components/Forms/ModelForm.js.map +1 -1
  183. package/build/dist/UI/Components/Forms/Utils/FormAnalyticsName.js +33 -0
  184. package/build/dist/UI/Components/Forms/Utils/FormAnalyticsName.js.map +1 -0
  185. package/build/dist/UI/Components/ModelFormModal/ModelFormModal.js +2 -1
  186. package/build/dist/UI/Components/ModelFormModal/ModelFormModal.js.map +1 -1
  187. package/build/dist/Utils/Analytics.js +50 -2
  188. package/build/dist/Utils/Analytics.js.map +1 -1
  189. package/build/dist/Utils/Monitor/MonitorMetricType.js +37 -0
  190. package/build/dist/Utils/Monitor/MonitorMetricType.js.map +1 -1
  191. package/build/dist/Utils/Monitor/NetworkDeviceLinkRuleUtil.js +318 -37
  192. package/build/dist/Utils/Monitor/NetworkDeviceLinkRuleUtil.js.map +1 -1
  193. package/build/dist/Utils/NetworkDiscovery/UnclaimedScanDiagnosis.js +87 -0
  194. package/build/dist/Utils/NetworkDiscovery/UnclaimedScanDiagnosis.js.map +1 -0
  195. package/package.json +1 -1
@@ -0,0 +1,508 @@
1
+ import APIRequestCriteria from "../../../../../Server/Utils/Monitor/Criteria/APIRequestCriteria";
2
+ import MetricService from "../../../../../Server/Services/MetricService";
3
+ import FindBy from "../../../../../Server/Types/AnalyticsDatabase/FindBy";
4
+ import Metric from "../../../../../Models/AnalyticsModels/Metric";
5
+ import {
6
+ CheckOn,
7
+ CriteriaFilter,
8
+ EvaluateOverTimeType,
9
+ FilterType,
10
+ NoDataPolicy,
11
+ } from "../../../../../Types/Monitor/CriteriaFilter";
12
+ import ProbeMonitorResponse from "../../../../../Types/Probe/ProbeMonitorResponse";
13
+ import ObjectID from "../../../../../Types/ObjectID";
14
+ import {
15
+ afterEach,
16
+ beforeEach,
17
+ describe,
18
+ expect,
19
+ jest,
20
+ test,
21
+ } from "@jest/globals";
22
+
23
+ /*
24
+ * End-to-end cover for the bug in
25
+ * https://github.com/OneUptime/oneuptime/issues/2321: a criteria filter set
26
+ * to look at "All Values over the last N minutes" fired on the first bad
27
+ * probe instead of waiting for the window.
28
+ *
29
+ * These tests drive the real APIRequestCriteria evaluator and stub only the
30
+ * metric read, so the window arithmetic, the coverage guard, the no-data
31
+ * policy and the comparison message are all exercised together.
32
+ */
33
+
34
+ const NOW: Date = new Date("2026-08-20T12:00:00.000Z");
35
+
36
+ const PROJECT_ID: ObjectID = new ObjectID(
37
+ "11111111-1111-4111-8111-111111111111",
38
+ );
39
+ const MONITOR_ID: ObjectID = new ObjectID(
40
+ "22222222-2222-4222-8222-222222222222",
41
+ );
42
+
43
+ /** Once a minute, oldest first, ending at `now`. */
44
+ function everyMinute(values: Array<number>): Array<Metric> {
45
+ return values.map((value: number, index: number) => {
46
+ const metric: Metric = new Metric();
47
+ metric.value = value;
48
+ metric.time = new Date(
49
+ NOW.getTime() - (values.length - 1 - index) * 60 * 1000,
50
+ );
51
+ return metric;
52
+ });
53
+ }
54
+
55
+ function buildResponse(
56
+ overrides: Partial<ProbeMonitorResponse> = {},
57
+ ): ProbeMonitorResponse {
58
+ return {
59
+ projectId: PROJECT_ID,
60
+ monitorId: MONITOR_ID,
61
+ monitorStepId: ObjectID.generate(),
62
+ probeId: ObjectID.generate(),
63
+ failureCause: "",
64
+ monitoredAt: NOW,
65
+ isOnline: true,
66
+ responseCode: 200,
67
+ responseTimeInMs: 100,
68
+ ...overrides,
69
+ };
70
+ }
71
+
72
+ let windowSamples: Array<Metric> = [];
73
+
74
+ function evaluate(input: {
75
+ criteriaFilter: CriteriaFilter;
76
+ dataToProcess: ProbeMonitorResponse;
77
+ monitoringInterval?: string | undefined;
78
+ }): Promise<string | null> {
79
+ return APIRequestCriteria.isMonitorInstanceCriteriaFilterMet({
80
+ dataToProcess: input.dataToProcess,
81
+ criteriaFilter: input.criteriaFilter,
82
+ monitoringInterval: input.monitoringInterval ?? "* * * * *",
83
+ });
84
+ }
85
+
86
+ describe("APIRequestCriteria evaluate over time", () => {
87
+ beforeEach(() => {
88
+ jest.useFakeTimers();
89
+ jest.setSystemTime(NOW);
90
+
91
+ windowSamples = [];
92
+
93
+ jest
94
+ .spyOn(MetricService, "findBy")
95
+ .mockImplementation((_findBy: FindBy<Metric>): Promise<Array<Metric>> => {
96
+ return Promise.resolve(windowSamples);
97
+ });
98
+ });
99
+
100
+ afterEach(() => {
101
+ jest.useRealTimers();
102
+ jest.restoreAllMocks();
103
+ });
104
+
105
+ describe("the reported scenario: one bad status code in a healthy window", () => {
106
+ const offlineCriteria: CriteriaFilter = {
107
+ checkOn: CheckOn.ResponseStatusCode,
108
+ filterType: FilterType.NotEqualTo,
109
+ value: 200,
110
+ evaluateOverTime: true,
111
+ evaluateOverTimeOptions: {
112
+ timeValueInMinutes: 5,
113
+ evaluateOverTimeType: EvaluateOverTimeType.AllValues,
114
+ },
115
+ };
116
+
117
+ test("does not fire when only the newest sample is bad", async () => {
118
+ windowSamples = everyMinute([200, 200, 200, 200, 404]);
119
+
120
+ const result: string | null = await evaluate({
121
+ criteriaFilter: offlineCriteria,
122
+ dataToProcess: buildResponse({ responseCode: 404 }),
123
+ });
124
+
125
+ expect(result).toBeNull();
126
+ });
127
+
128
+ /*
129
+ * The heart of the bug. Nothing had been recorded yet, so the evaluator
130
+ * used to compare the single status code that arrived with this check
131
+ * and report it as "all values over the last 5 minutes".
132
+ */
133
+ test("does not fire when the window is empty", async () => {
134
+ windowSamples = [];
135
+
136
+ const result: string | null = await evaluate({
137
+ criteriaFilter: offlineCriteria,
138
+ dataToProcess: buildResponse({ responseCode: 404 }),
139
+ });
140
+
141
+ expect(result).toBeNull();
142
+ });
143
+
144
+ test("does not fire on a monitor that has only checked once", async () => {
145
+ windowSamples = everyMinute([404]);
146
+
147
+ const result: string | null = await evaluate({
148
+ criteriaFilter: offlineCriteria,
149
+ dataToProcess: buildResponse({ responseCode: 404 }),
150
+ });
151
+
152
+ expect(result).toBeNull();
153
+ });
154
+
155
+ test("does not fire two minutes into a five minute window", async () => {
156
+ windowSamples = everyMinute([404, 404]);
157
+
158
+ const result: string | null = await evaluate({
159
+ criteriaFilter: offlineCriteria,
160
+ dataToProcess: buildResponse({ responseCode: 404 }),
161
+ });
162
+
163
+ expect(result).toBeNull();
164
+ });
165
+
166
+ test("fires once the whole window has been bad", async () => {
167
+ windowSamples = everyMinute([404, 404, 404, 404, 404]);
168
+
169
+ const result: string | null = await evaluate({
170
+ criteriaFilter: offlineCriteria,
171
+ dataToProcess: buildResponse({ responseCode: 404 }),
172
+ });
173
+
174
+ expect(result).toContain("All values of");
175
+ expect(result).toContain(CheckOn.ResponseStatusCode as string);
176
+ expect(result).toContain("over the last 5 minutes");
177
+ expect(result).toContain("not equal to 200");
178
+ });
179
+
180
+ /*
181
+ * The recovery direction matters too: a single good sample inside an
182
+ * otherwise bad window must clear the criteria.
183
+ */
184
+ test("stops firing as soon as one sample recovers", async () => {
185
+ windowSamples = everyMinute([404, 404, 404, 404, 200]);
186
+
187
+ const result: string | null = await evaluate({
188
+ criteriaFilter: offlineCriteria,
189
+ dataToProcess: buildResponse({ responseCode: 200 }),
190
+ });
191
+
192
+ expect(result).toBeNull();
193
+ });
194
+ });
195
+
196
+ describe("the reported scenario: a single latency spike", () => {
197
+ /*
198
+ * From the second report on the issue - one 9171 ms tick inside a two
199
+ * minute window of sub-second responses opened an incident that
200
+ * auto-resolved 72 seconds later.
201
+ */
202
+ const slowCriteria: CriteriaFilter = {
203
+ checkOn: CheckOn.ResponseTime,
204
+ filterType: FilterType.GreaterThan,
205
+ value: 8000,
206
+ evaluateOverTime: true,
207
+ evaluateOverTimeOptions: {
208
+ timeValueInMinutes: 2,
209
+ evaluateOverTimeType: EvaluateOverTimeType.AllValues,
210
+ },
211
+ };
212
+
213
+ test("does not fire on a lone spike among healthy samples", async () => {
214
+ windowSamples = everyMinute([397, 805, 9171]);
215
+
216
+ const result: string | null = await evaluate({
217
+ criteriaFilter: slowCriteria,
218
+ dataToProcess: buildResponse({ responseTimeInMs: 9171 }),
219
+ });
220
+
221
+ expect(result).toBeNull();
222
+ });
223
+
224
+ test("fires when the whole window is slow", async () => {
225
+ windowSamples = everyMinute([9171, 9002, 8800]);
226
+
227
+ const result: string | null = await evaluate({
228
+ criteriaFilter: slowCriteria,
229
+ dataToProcess: buildResponse({ responseTimeInMs: 8800 }),
230
+ });
231
+
232
+ expect(result).toContain("All values of");
233
+ expect(result).toContain("greater than 8000");
234
+ });
235
+
236
+ /*
237
+ * "Any Value" is the setting for "tell me the moment one check
238
+ * breaches", and it must keep behaving that way.
239
+ */
240
+ test("Any Value still fires on the lone spike", async () => {
241
+ windowSamples = everyMinute([397, 805, 9171]);
242
+
243
+ const result: string | null = await evaluate({
244
+ criteriaFilter: {
245
+ ...slowCriteria,
246
+ evaluateOverTimeOptions: {
247
+ timeValueInMinutes: 2,
248
+ evaluateOverTimeType: EvaluateOverTimeType.AnyValue,
249
+ },
250
+ },
251
+ dataToProcess: buildResponse({ responseTimeInMs: 9171 }),
252
+ });
253
+
254
+ expect(result).toContain("Any value of");
255
+ });
256
+
257
+ test("Any Value fires off the very first sample", async () => {
258
+ windowSamples = everyMinute([9171]);
259
+
260
+ const result: string | null = await evaluate({
261
+ criteriaFilter: {
262
+ ...slowCriteria,
263
+ evaluateOverTimeOptions: {
264
+ timeValueInMinutes: 2,
265
+ evaluateOverTimeType: EvaluateOverTimeType.AnyValue,
266
+ },
267
+ },
268
+ dataToProcess: buildResponse({ responseTimeInMs: 9171 }),
269
+ });
270
+
271
+ expect(result).not.toBeNull();
272
+ });
273
+ });
274
+
275
+ describe("Is Online over time", () => {
276
+ const offlineCriteria: CriteriaFilter = {
277
+ checkOn: CheckOn.IsOnline,
278
+ filterType: FilterType.False,
279
+ value: undefined,
280
+ evaluateOverTime: true,
281
+ evaluateOverTimeOptions: {
282
+ timeValueInMinutes: 5,
283
+ evaluateOverTimeType: EvaluateOverTimeType.AllValues,
284
+ },
285
+ };
286
+
287
+ test("fires only after the whole window has been offline", async () => {
288
+ windowSamples = everyMinute([0, 0, 0, 0, 0]);
289
+
290
+ const result: string | null = await evaluate({
291
+ criteriaFilter: offlineCriteria,
292
+ dataToProcess: buildResponse({ isOnline: false }),
293
+ });
294
+
295
+ expect(result).toContain(CheckOn.IsOnline as string);
296
+ });
297
+
298
+ test("does not fire on the first failed check", async () => {
299
+ windowSamples = everyMinute([1, 1, 1, 1, 0]);
300
+
301
+ const result: string | null = await evaluate({
302
+ criteriaFilter: offlineCriteria,
303
+ dataToProcess: buildResponse({ isOnline: false }),
304
+ });
305
+
306
+ expect(result).toBeNull();
307
+ });
308
+
309
+ test("does not fire while the window is still filling", async () => {
310
+ windowSamples = everyMinute([0]);
311
+
312
+ const result: string | null = await evaluate({
313
+ criteriaFilter: offlineCriteria,
314
+ dataToProcess: buildResponse({ isOnline: false }),
315
+ });
316
+
317
+ expect(result).toBeNull();
318
+ });
319
+ });
320
+
321
+ describe("no-data policy", () => {
322
+ const criteriaWith: (policy: NoDataPolicy) => CriteriaFilter = (
323
+ policy: NoDataPolicy,
324
+ ): CriteriaFilter => {
325
+ return {
326
+ checkOn: CheckOn.ResponseStatusCode,
327
+ filterType: FilterType.NotEqualTo,
328
+ value: 200,
329
+ evaluateOverTime: true,
330
+ evaluateOverTimeOptions: {
331
+ timeValueInMinutes: 5,
332
+ evaluateOverTimeType: EvaluateOverTimeType.AllValues,
333
+ onNoDataPolicy: policy,
334
+ },
335
+ };
336
+ };
337
+
338
+ test("Ignore keeps quiet when there is nothing to look at", async () => {
339
+ windowSamples = [];
340
+
341
+ const result: string | null = await evaluate({
342
+ criteriaFilter: criteriaWith(NoDataPolicy.Ignore),
343
+ dataToProcess: buildResponse({ responseCode: 404 }),
344
+ });
345
+
346
+ expect(result).toBeNull();
347
+ });
348
+
349
+ /*
350
+ * For heartbeat-style checks the absence of data is itself the problem,
351
+ * which is what Trigger is for.
352
+ */
353
+ test("Trigger reports the missing data as the root cause", async () => {
354
+ windowSamples = [];
355
+
356
+ const result: string | null = await evaluate({
357
+ criteriaFilter: criteriaWith(NoDataPolicy.Trigger),
358
+ dataToProcess: buildResponse({ responseCode: 200 }),
359
+ });
360
+
361
+ expect(result).toContain("no data");
362
+ expect(result).toContain("5 minutes");
363
+ });
364
+
365
+ test("Treat As Zero compares the window as a single zero", async () => {
366
+ windowSamples = [];
367
+
368
+ const result: string | null = await evaluate({
369
+ criteriaFilter: criteriaWith(NoDataPolicy.TreatAsZero),
370
+ dataToProcess: buildResponse({ responseCode: 200 }),
371
+ });
372
+
373
+ // 0 is not equal to 200, so the filter is met.
374
+ expect(result).toContain("not equal to 200");
375
+ });
376
+ });
377
+
378
+ describe("monitors that are polled less often than the window", () => {
379
+ /*
380
+ * A monitor checked every five minutes can only ever put one sample into
381
+ * a five minute window, so that sample IS the whole window. Requiring
382
+ * more would mean it never fires at all.
383
+ */
384
+ test("a single sample covers the window at a five minute interval", async () => {
385
+ windowSamples = everyMinute([404]);
386
+
387
+ const result: string | null = await evaluate({
388
+ criteriaFilter: {
389
+ checkOn: CheckOn.ResponseStatusCode,
390
+ filterType: FilterType.NotEqualTo,
391
+ value: 200,
392
+ evaluateOverTime: true,
393
+ evaluateOverTimeOptions: {
394
+ timeValueInMinutes: 5,
395
+ evaluateOverTimeType: EvaluateOverTimeType.AllValues,
396
+ },
397
+ },
398
+ dataToProcess: buildResponse({ responseCode: 404 }),
399
+ monitoringInterval: "*/5 * * * *",
400
+ });
401
+
402
+ expect(result).not.toBeNull();
403
+ });
404
+ });
405
+
406
+ describe("attribute scoping", () => {
407
+ /*
408
+ * Only the disk-usage series carries a diskPath attribute. A stray
409
+ * serverMonitorOptions left on a non-disk filter used to be harmless
410
+ * because an empty window fell back to the live value; now it would
411
+ * silence the filter, so it must not reach the query.
412
+ */
413
+ test("does not scope a response-time window by a stray disk path", async () => {
414
+ windowSamples = everyMinute([9000, 9000, 9000]);
415
+
416
+ const findBySpy: ReturnType<typeof jest.spyOn> = jest.spyOn(
417
+ MetricService,
418
+ "findBy",
419
+ );
420
+
421
+ const result: string | null = await evaluate({
422
+ criteriaFilter: {
423
+ checkOn: CheckOn.ResponseTime,
424
+ filterType: FilterType.GreaterThan,
425
+ value: 8000,
426
+ evaluateOverTime: true,
427
+ evaluateOverTimeOptions: {
428
+ timeValueInMinutes: 2,
429
+ evaluateOverTimeType: EvaluateOverTimeType.AllValues,
430
+ },
431
+ serverMonitorOptions: { diskPath: "/" },
432
+ },
433
+ dataToProcess: buildResponse({ responseTimeInMs: 9000 }),
434
+ });
435
+
436
+ const query: Record<string, unknown> = (
437
+ findBySpy.mock.calls[0]![0] as { query: Record<string, unknown> }
438
+ ).query;
439
+
440
+ expect(query["attributes"]).toBeUndefined();
441
+ expect(result).not.toBeNull();
442
+ });
443
+ });
444
+
445
+ describe("filters that do not evaluate over time", () => {
446
+ test("still compare the value that arrived with this check", async () => {
447
+ windowSamples = [];
448
+
449
+ const result: string | null = await evaluate({
450
+ criteriaFilter: {
451
+ checkOn: CheckOn.ResponseStatusCode,
452
+ filterType: FilterType.NotEqualTo,
453
+ value: 200,
454
+ evaluateOverTime: false,
455
+ },
456
+ dataToProcess: buildResponse({ responseCode: 404 }),
457
+ });
458
+
459
+ expect(result).toContain("not equal to 200");
460
+ });
461
+
462
+ test("the metric store is never read for them", async () => {
463
+ const findBySpy: ReturnType<typeof jest.spyOn> = jest.spyOn(
464
+ MetricService,
465
+ "findBy",
466
+ );
467
+
468
+ await evaluate({
469
+ criteriaFilter: {
470
+ checkOn: CheckOn.ResponseStatusCode,
471
+ filterType: FilterType.NotEqualTo,
472
+ value: 200,
473
+ evaluateOverTime: false,
474
+ },
475
+ dataToProcess: buildResponse({ responseCode: 404 }),
476
+ });
477
+
478
+ expect(findBySpy).not.toHaveBeenCalled();
479
+ });
480
+ });
481
+
482
+ describe("checks with no metric series of their own", () => {
483
+ /*
484
+ * Response Body is not recorded as a metric, so an over-time flag on it
485
+ * cannot be honoured. It falls back to the body from this check rather
486
+ * than silently never matching.
487
+ */
488
+ test("fall back to the value from this check", async () => {
489
+ windowSamples = [];
490
+
491
+ const result: string | null = await evaluate({
492
+ criteriaFilter: {
493
+ checkOn: CheckOn.ResponseBody,
494
+ filterType: FilterType.Contains,
495
+ value: "healthy",
496
+ evaluateOverTime: true,
497
+ evaluateOverTimeOptions: {
498
+ timeValueInMinutes: 5,
499
+ evaluateOverTimeType: EvaluateOverTimeType.AllValues,
500
+ },
501
+ },
502
+ dataToProcess: buildResponse({ responseBody: "all healthy here" }),
503
+ });
504
+
505
+ expect(result).toContain("Response body contains healthy");
506
+ });
507
+ });
508
+ });
@@ -1,5 +1,7 @@
1
1
  import APIRequestCriteria from "../../../../../Server/Utils/Monitor/Criteria/APIRequestCriteria";
2
- import EvaluateOverTime from "../../../../../Server/Utils/Monitor/Criteria/EvaluateOverTime";
2
+ import EvaluateOverTime, {
3
+ OverTimeCriteriaValue,
4
+ } from "../../../../../Server/Utils/Monitor/Criteria/EvaluateOverTime";
3
5
  import {
4
6
  CheckOn,
5
7
  CriteriaFilter,
@@ -142,8 +144,11 @@ describe("APIRequestCriteria Port connection phase timings", () => {
142
144
 
143
145
  test("uses an evaluate-over-time aggregate instead of the current phase", async () => {
144
146
  const overTimeSpy: ReturnType<typeof jest.spyOn> = jest
145
- .spyOn(EvaluateOverTime, "getValueOverTime")
146
- .mockResolvedValue(250);
147
+ .spyOn(EvaluateOverTime, "getOverTimeValueForCriteriaFilter")
148
+ .mockResolvedValue({
149
+ earlyReturn: null,
150
+ value: 250,
151
+ } as OverTimeCriteriaValue);
147
152
 
148
153
  const response: ProbeMonitorResponse = buildResponse({
149
154
  dnsLookupInMs: 5,
@@ -164,13 +169,20 @@ describe("APIRequestCriteria Port connection phase timings", () => {
164
169
  expect.objectContaining({
165
170
  projectId: response.projectId,
166
171
  monitorId: response.monitorId,
167
- metricType: CheckOn.PortDnsLookupTime,
172
+ criteriaFilter: expect.objectContaining({
173
+ checkOn: CheckOn.PortDnsLookupTime,
174
+ }),
168
175
  }),
169
176
  );
170
177
  });
171
178
 
172
179
  test("does not replace a zero evaluate-over-time aggregate with the current value", async () => {
173
- jest.spyOn(EvaluateOverTime, "getValueOverTime").mockResolvedValue(0);
180
+ jest
181
+ .spyOn(EvaluateOverTime, "getOverTimeValueForCriteriaFilter")
182
+ .mockResolvedValue({
183
+ earlyReturn: null,
184
+ value: 0,
185
+ } as OverTimeCriteriaValue);
174
186
 
175
187
  const result: string | null = await evaluate(
176
188
  buildResponse({
@@ -189,10 +201,19 @@ describe("APIRequestCriteria Port connection phase timings", () => {
189
201
  expect(result).not.toBeNull();
190
202
  });
191
203
 
192
- test("falls back to the current phase if the history query fails", async () => {
204
+ /*
205
+ * An over-time filter whose history could not be read says nothing about
206
+ * the last N minutes, so it must not fire. It used to quietly compare the
207
+ * single phase timing from this one check instead - the same fallback that
208
+ * let "all values over the last N minutes" fire off one bad probe.
209
+ */
210
+ test("does not fire off the current phase if the history query fails", async () => {
193
211
  jest
194
- .spyOn(EvaluateOverTime, "getValueOverTime")
195
- .mockRejectedValue(new Error("history unavailable"));
212
+ .spyOn(EvaluateOverTime, "getOverTimeValueForCriteriaFilter")
213
+ .mockResolvedValue({
214
+ earlyReturn: { result: null },
215
+ value: undefined,
216
+ } as OverTimeCriteriaValue);
196
217
 
197
218
  const result: string | null = await evaluate(
198
219
  buildResponse({
@@ -207,6 +228,6 @@ describe("APIRequestCriteria Port connection phase timings", () => {
207
228
  }),
208
229
  );
209
230
 
210
- expect(result).not.toBeNull();
231
+ expect(result).toBeNull();
211
232
  });
212
233
  });