@oneuptime/common 11.4.0 → 11.4.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 (178) hide show
  1. package/Server/API/AIAgentTaskAPI.ts +10 -24
  2. package/Server/API/TelemetryAPI.ts +4 -3
  3. package/Server/Services/AIAgentTaskService.ts +62 -0
  4. package/Server/Services/AnalyticsDatabaseService.ts +27 -0
  5. package/Server/Services/LogAggregationService.ts +29 -11
  6. package/Server/Services/MetricService.ts +603 -13
  7. package/Server/Utils/AI/Sentinel/IncidentInvestigationRunner.ts +1 -1
  8. package/Server/Utils/AI/Sentinel/README.md +10 -0
  9. package/Server/Utils/Monitor/Criteria/APIRequestCriteria.ts +44 -0
  10. package/Server/Utils/Monitor/MonitorMetricUtil.ts +108 -2
  11. package/Tests/Server/Services/AIAgentTaskServiceClaim.test.ts +146 -0
  12. package/Tests/Server/Services/LogAggregationService.test.ts +22 -0
  13. package/Tests/Server/Services/MetricServiceAggregate.test.ts +194 -0
  14. package/Tests/Types/AI/AIAgentTaskStatus.test.ts +86 -0
  15. package/Tests/Types/BaseDatabase/AggregationIntervalUtil.test.ts +127 -0
  16. package/Tests/Types/BaseDatabase/DatabaseCommonInteractionPropsUtil.test.ts +152 -0
  17. package/Tests/Types/Trace/TraceRecordingRuleDefinition.test.ts +308 -0
  18. package/Tests/UI/Components/Dropdown.test.tsx +17 -0
  19. package/Tests/UI/Components/EntityDropdown.test.tsx +130 -0
  20. package/Tests/UI/Components/Modal.test.tsx +144 -2
  21. package/Tests/UI/Components/TelemetrySearchBar.test.tsx +90 -0
  22. package/Tests/UI/Utils/Theme.test.ts +161 -0
  23. package/Types/BaseDatabase/AggregateBy.ts +12 -0
  24. package/Types/Dashboard/DashboardComponentType.ts +1 -0
  25. package/Types/Dashboard/DashboardComponents/ComponentArgument.ts +1 -0
  26. package/Types/Dashboard/DashboardComponents/DashboardLogChartComponent.ts +19 -0
  27. package/Types/Dashboard/DashboardTemplates.ts +9 -1
  28. package/Types/Monitor/CriteriaFilter.ts +4 -0
  29. package/Types/Monitor/HttpPhaseTimings.ts +16 -0
  30. package/Types/Monitor/MonitorMetricType.ts +17 -0
  31. package/Types/Monitor/PingMonitor/PingMonitorResponse.ts +20 -0
  32. package/Types/Probe/ProbeMonitorResponse.ts +14 -0
  33. package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +8 -5
  34. package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +4 -1
  35. package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +6 -3
  36. package/UI/Components/CodeBlock/CodeBlock.tsx +2 -1
  37. package/UI/Components/CodeEditor/CodeEditor.tsx +3 -0
  38. package/UI/Components/Divider/Divider.tsx +1 -1
  39. package/UI/Components/Dropdown/Dropdown.tsx +13 -4
  40. package/UI/Components/EntityDropdown/EntityDropdown.tsx +110 -2
  41. package/UI/Components/ErrorBoundary.tsx +2 -1
  42. package/UI/Components/EventItem/EventItem.tsx +2 -2
  43. package/UI/Components/GanttChart/Bar/Index.tsx +1 -1
  44. package/UI/Components/Graphs/UptimeBarTooltip.tsx +30 -23
  45. package/UI/Components/Header/Header.tsx +7 -0
  46. package/UI/Components/Header/HeaderIconDropdownButton.tsx +2 -0
  47. package/UI/Components/Label/Label.tsx +2 -2
  48. package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +22 -10
  49. package/UI/Components/LogsViewer/components/LogsHistogram.tsx +9 -3
  50. package/UI/Components/Markdown.tsx/MarkdownEditor.tsx +8 -8
  51. package/UI/Components/Markdown.tsx/MarkdownViewer.tsx +44 -20
  52. package/UI/Components/Modal/ConfirmModal.tsx +1 -1
  53. package/UI/Components/Modal/Modal.tsx +213 -121
  54. package/UI/Components/Modal/ModalFooter.tsx +33 -22
  55. package/UI/Components/ModelTable/TableView.tsx +1 -1
  56. package/UI/Components/MonitorGraphs/UptimeBarDayModal.tsx +20 -12
  57. package/UI/Components/Pill/Pill.tsx +21 -13
  58. package/UI/Components/StatusBubble/StatusBubble.tsx +3 -3
  59. package/UI/Components/TelemetryViewer/TelemetryViewer.tsx +7 -1
  60. package/UI/Components/TelemetryViewer/components/TelemetryHistogram.tsx +9 -3
  61. package/UI/Components/TelemetryViewer/components/TelemetrySearchBar.tsx +62 -4
  62. package/UI/Components/Workflow/Component.tsx +53 -33
  63. package/UI/Components/Workflow/ComponentPortViewer.tsx +6 -6
  64. package/UI/Components/Workflow/ComponentReturnValueViewer.tsx +8 -8
  65. package/UI/Components/Workflow/ComponentsModal.tsx +16 -9
  66. package/UI/Components/Workflow/Workflow.tsx +13 -11
  67. package/UI/Utils/Theme.ts +140 -0
  68. package/Utils/Dashboard/Components/DashboardLogChartComponent.ts +104 -0
  69. package/Utils/Dashboard/Components/Index.ts +7 -0
  70. package/Utils/Monitor/MonitorMetricType.ts +70 -2
  71. package/build/dist/Server/API/AIAgentTaskAPI.js +8 -23
  72. package/build/dist/Server/API/AIAgentTaskAPI.js.map +1 -1
  73. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  74. package/build/dist/Server/Services/AIAgentTaskService.js +59 -0
  75. package/build/dist/Server/Services/AIAgentTaskService.js.map +1 -1
  76. package/build/dist/Server/Services/AnalyticsDatabaseService.js +19 -0
  77. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  78. package/build/dist/Server/Services/LogAggregationService.js +21 -7
  79. package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
  80. package/build/dist/Server/Services/MetricService.js +449 -7
  81. package/build/dist/Server/Services/MetricService.js.map +1 -1
  82. package/build/dist/Server/Utils/AI/Sentinel/IncidentInvestigationRunner.js +1 -1
  83. package/build/dist/Server/Utils/Monitor/Criteria/APIRequestCriteria.js +29 -0
  84. package/build/dist/Server/Utils/Monitor/Criteria/APIRequestCriteria.js.map +1 -1
  85. package/build/dist/Server/Utils/Monitor/MonitorMetricUtil.js +86 -2
  86. package/build/dist/Server/Utils/Monitor/MonitorMetricUtil.js.map +1 -1
  87. package/build/dist/Types/Dashboard/DashboardComponentType.js +1 -0
  88. package/build/dist/Types/Dashboard/DashboardComponentType.js.map +1 -1
  89. package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js +1 -0
  90. package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js.map +1 -1
  91. package/build/dist/Types/Dashboard/DashboardComponents/DashboardLogChartComponent.js +2 -0
  92. package/build/dist/Types/Dashboard/DashboardComponents/DashboardLogChartComponent.js.map +1 -0
  93. package/build/dist/Types/Dashboard/DashboardTemplates.js +9 -1
  94. package/build/dist/Types/Dashboard/DashboardTemplates.js.map +1 -1
  95. package/build/dist/Types/Monitor/CriteriaFilter.js +4 -0
  96. package/build/dist/Types/Monitor/CriteriaFilter.js.map +1 -1
  97. package/build/dist/Types/Monitor/HttpPhaseTimings.js +2 -0
  98. package/build/dist/Types/Monitor/HttpPhaseTimings.js.map +1 -0
  99. package/build/dist/Types/Monitor/MonitorMetricType.js +15 -0
  100. package/build/dist/Types/Monitor/MonitorMetricType.js.map +1 -1
  101. package/build/dist/Types/Monitor/PingMonitor/PingMonitorResponse.js +2 -0
  102. package/build/dist/Types/Monitor/PingMonitor/PingMonitorResponse.js.map +1 -0
  103. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js +7 -4
  104. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
  105. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +4 -1
  106. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
  107. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +6 -3
  108. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
  109. package/build/dist/UI/Components/CodeBlock/CodeBlock.js +1 -1
  110. package/build/dist/UI/Components/CodeBlock/CodeBlock.js.map +1 -1
  111. package/build/dist/UI/Components/CodeEditor/CodeEditor.js +3 -1
  112. package/build/dist/UI/Components/CodeEditor/CodeEditor.js.map +1 -1
  113. package/build/dist/UI/Components/Divider/Divider.js +1 -1
  114. package/build/dist/UI/Components/Divider/Divider.js.map +1 -1
  115. package/build/dist/UI/Components/Dropdown/Dropdown.js +10 -4
  116. package/build/dist/UI/Components/Dropdown/Dropdown.js.map +1 -1
  117. package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js +76 -3
  118. package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js.map +1 -1
  119. package/build/dist/UI/Components/ErrorBoundary.js +2 -1
  120. package/build/dist/UI/Components/ErrorBoundary.js.map +1 -1
  121. package/build/dist/UI/Components/EventItem/EventItem.js +2 -2
  122. package/build/dist/UI/Components/EventItem/EventItem.js.map +1 -1
  123. package/build/dist/UI/Components/GanttChart/Bar/Index.js +1 -1
  124. package/build/dist/UI/Components/GanttChart/Bar/Index.js.map +1 -1
  125. package/build/dist/UI/Components/Graphs/UptimeBarTooltip.js +29 -23
  126. package/build/dist/UI/Components/Graphs/UptimeBarTooltip.js.map +1 -1
  127. package/build/dist/UI/Components/Header/Header.js +1 -0
  128. package/build/dist/UI/Components/Header/Header.js.map +1 -1
  129. package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js +1 -1
  130. package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js.map +1 -1
  131. package/build/dist/UI/Components/Label/Label.js +2 -2
  132. package/build/dist/UI/Components/Label/Label.js.map +1 -1
  133. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +20 -8
  134. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
  135. package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js +8 -2
  136. package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js.map +1 -1
  137. package/build/dist/UI/Components/Markdown.tsx/MarkdownEditor.js +8 -8
  138. package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js +40 -20
  139. package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js.map +1 -1
  140. package/build/dist/UI/Components/Modal/ConfirmModal.js +1 -1
  141. package/build/dist/UI/Components/Modal/ConfirmModal.js.map +1 -1
  142. package/build/dist/UI/Components/Modal/Modal.js +109 -51
  143. package/build/dist/UI/Components/Modal/Modal.js.map +1 -1
  144. package/build/dist/UI/Components/Modal/ModalFooter.js +13 -10
  145. package/build/dist/UI/Components/Modal/ModalFooter.js.map +1 -1
  146. package/build/dist/UI/Components/ModelTable/TableView.js +1 -1
  147. package/build/dist/UI/Components/ModelTable/TableView.js.map +1 -1
  148. package/build/dist/UI/Components/MonitorGraphs/UptimeBarDayModal.js +17 -12
  149. package/build/dist/UI/Components/MonitorGraphs/UptimeBarDayModal.js.map +1 -1
  150. package/build/dist/UI/Components/Pill/Pill.js +12 -13
  151. package/build/dist/UI/Components/Pill/Pill.js.map +1 -1
  152. package/build/dist/UI/Components/StatusBubble/StatusBubble.js +3 -3
  153. package/build/dist/UI/Components/StatusBubble/StatusBubble.js.map +1 -1
  154. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js +1 -1
  155. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js.map +1 -1
  156. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js +8 -2
  157. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js.map +1 -1
  158. package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchBar.js +36 -6
  159. package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchBar.js.map +1 -1
  160. package/build/dist/UI/Components/Workflow/Component.js +43 -33
  161. package/build/dist/UI/Components/Workflow/Component.js.map +1 -1
  162. package/build/dist/UI/Components/Workflow/ComponentPortViewer.js +6 -6
  163. package/build/dist/UI/Components/Workflow/ComponentPortViewer.js.map +1 -1
  164. package/build/dist/UI/Components/Workflow/ComponentReturnValueViewer.js +8 -8
  165. package/build/dist/UI/Components/Workflow/ComponentReturnValueViewer.js.map +1 -1
  166. package/build/dist/UI/Components/Workflow/ComponentsModal.js +15 -9
  167. package/build/dist/UI/Components/Workflow/ComponentsModal.js.map +1 -1
  168. package/build/dist/UI/Components/Workflow/Workflow.js +13 -11
  169. package/build/dist/UI/Components/Workflow/Workflow.js.map +1 -1
  170. package/build/dist/UI/Utils/Theme.js +106 -0
  171. package/build/dist/UI/Utils/Theme.js.map +1 -0
  172. package/build/dist/Utils/Dashboard/Components/DashboardLogChartComponent.js +92 -0
  173. package/build/dist/Utils/Dashboard/Components/DashboardLogChartComponent.js.map +1 -0
  174. package/build/dist/Utils/Dashboard/Components/Index.js +4 -0
  175. package/build/dist/Utils/Dashboard/Components/Index.js.map +1 -1
  176. package/build/dist/Utils/Monitor/MonitorMetricType.js +70 -3
  177. package/build/dist/Utils/Monitor/MonitorMetricType.js.map +1 -1
  178. package/package.json +1 -1
@@ -23,7 +23,7 @@ import CaptureSpan from "../../Telemetry/CaptureSpan";
23
23
  * posts the result to the incident's feed.
24
24
  *
25
25
  * Built on the fire-and-forget async pattern already used throughout
26
- * IncidentService.onCreateSuccess. See Internal/Roadmap/AISentinelReliabilityBrain.md.
26
+ * IncidentService.onCreateSuccess. See Internal/Roadmap/AISentinelExecution.md.
27
27
  */
28
28
  export default class SentinelIncidentInvestigationRunner {
29
29
  /*
@@ -0,0 +1,10 @@
1
+ # Sentinel — autonomous investigation engine
2
+
3
+ Wake-on-signal AI SRE: when an incident or alert is created (and the project has opted in), Sentinel runs a read-only, budgeted, citation-minting investigation over the project's own telemetry and posts a root-cause analysis to the subject's feed, with a glass-box `AIRun`/`AIRunEvent` trail behind a live dashboard panel.
4
+
5
+ - `SentinelInvestigationEngine.ts` — shared run lifecycle, budgets, persona, quiet-mode confidence gate
6
+ - `IncidentInvestigationRunner.ts` / `AlertInvestigationRunner.ts` — per-subject context assembly + result posting
7
+ - `IncidentPostmortemRunner.ts` — auto-draft postmortem when an incident resolves
8
+ - `SentinelMemory.ts` — recurrence context from past resolved incidents (retrieval-only v1)
9
+
10
+ **Roadmap & status:** [Internal/Roadmap/AISentinelExecution.md](../../../../../Internal/Roadmap/AISentinelExecution.md) (tracker — update it in the same PR when you change behavior here) and [Internal/Roadmap/AISentinelVision.md](../../../../../Internal/Roadmap/AISentinelVision.md) (strategy).
@@ -88,6 +88,50 @@ export default class APIRequestCriteria {
88
88
  });
89
89
  }
90
90
 
91
+ // check packet loss (Ping/IP monitors with multi-packet checks)
92
+ if (input.criteriaFilter.checkOn === CheckOn.PacketLossPercent) {
93
+ const packetLossPercent: number | undefined = (
94
+ input.dataToProcess as ProbeMonitorResponse
95
+ ).pingResponse?.packetLossPercent;
96
+
97
+ const value: Array<number> | number | undefined =
98
+ (overTimeValue as Array<number>) || packetLossPercent;
99
+
100
+ if (value === undefined) {
101
+ return null;
102
+ }
103
+
104
+ threshold = CompareCriteria.convertToNumber(threshold);
105
+
106
+ return CompareCriteria.compareCriteriaNumbers({
107
+ value: value,
108
+ threshold: threshold as number,
109
+ criteriaFilter: input.criteriaFilter,
110
+ });
111
+ }
112
+
113
+ // check jitter (Ping/IP monitors with multi-packet checks)
114
+ if (input.criteriaFilter.checkOn === CheckOn.Jitter) {
115
+ const jitterInMs: number | undefined = (
116
+ input.dataToProcess as ProbeMonitorResponse
117
+ ).pingResponse?.jitterInMs;
118
+
119
+ const value: Array<number> | number | undefined =
120
+ (overTimeValue as Array<number>) || jitterInMs;
121
+
122
+ if (value === undefined) {
123
+ return null;
124
+ }
125
+
126
+ threshold = CompareCriteria.convertToNumber(threshold);
127
+
128
+ return CompareCriteria.compareCriteriaNumbers({
129
+ value: value,
130
+ threshold: threshold as number,
131
+ criteriaFilter: input.criteriaFilter,
132
+ });
133
+ }
134
+
91
135
  //check response code
92
136
  if (
93
137
  input.criteriaFilter.checkOn === CheckOn.ResponseStatusCode &&
@@ -14,7 +14,9 @@ import BasicInfrastructureMetrics, {
14
14
  import Dictionary from "../../../Types/Dictionary";
15
15
  import { JSONObject } from "../../../Types/JSON";
16
16
  import CapturedMetric from "../../../Types/Monitor/CustomCodeMonitor/CapturedMetric";
17
+ import HttpPhaseTimings from "../../../Types/Monitor/HttpPhaseTimings";
17
18
  import MonitorMetricType from "../../../Types/Monitor/MonitorMetricType";
19
+ import PingMonitorResponse from "../../../Types/Monitor/PingMonitor/PingMonitorResponse";
18
20
  import ProbeMonitorResponse from "../../../Types/Probe/ProbeMonitorResponse";
19
21
  import ServerMonitorResponse from "../../../Types/Monitor/ServerMonitor/ServerMonitorResponse";
20
22
  import SyntheticMonitorResponse from "../../../Types/Monitor/SyntheticMonitors/SyntheticMonitorResponse";
@@ -23,8 +25,12 @@ import ObjectID from "../../../Types/ObjectID";
23
25
  import OneUptimeDate from "../../../Types/Date";
24
26
 
25
27
  export default class MonitorMetricUtil {
26
- // Default retention in days if GlobalConfig is not set
27
- private static readonly DEFAULT_RETENTION_DAYS: number = 1;
28
+ /*
29
+ * Default retention in days if GlobalConfig is not set. 30 days keeps
30
+ * enough history for trend analysis and evaluate-over-time criteria;
31
+ * one day made week-over-week charts impossible out of the box.
32
+ */
33
+ private static readonly DEFAULT_RETENTION_DAYS: number = 30;
28
34
 
29
35
  // Cached retention value to avoid querying GlobalConfig on every monitor check
30
36
  private static cachedRetentionDays: number | null = null;
@@ -883,6 +889,106 @@ export default class MonitorMetricUtil {
883
889
  metricNameServiceNameMap[MonitorMetricType.ResponseTime] = metricType;
884
890
  }
885
891
 
892
+ if ((data.dataToProcess as ProbeMonitorResponse).httpTimings) {
893
+ const httpTimings: HttpPhaseTimings = (
894
+ data.dataToProcess as ProbeMonitorResponse
895
+ ).httpTimings!;
896
+
897
+ const extraAttributes: JSONObject = {
898
+ probeId: (
899
+ data.dataToProcess as ProbeMonitorResponse
900
+ ).probeId.toString(),
901
+ };
902
+
903
+ const phaseMetrics: Array<{
904
+ metricName: MonitorMetricType;
905
+ value: number | undefined;
906
+ description: string;
907
+ }> = [
908
+ {
909
+ metricName: MonitorMetricType.DnsLookupTime,
910
+ value: httpTimings.dnsLookupInMs,
911
+ description: "DNS lookup time for this monitor",
912
+ },
913
+ {
914
+ metricName: MonitorMetricType.TcpConnectTime,
915
+ value: httpTimings.tcpConnectInMs,
916
+ description: "TCP connect time for this monitor",
917
+ },
918
+ {
919
+ metricName: MonitorMetricType.TlsHandshakeTime,
920
+ value: httpTimings.tlsHandshakeInMs,
921
+ description: "TLS handshake time for this monitor",
922
+ },
923
+ {
924
+ metricName: MonitorMetricType.TimeToFirstByte,
925
+ value: httpTimings.timeToFirstByteInMs,
926
+ description: "Time to first byte for this monitor",
927
+ },
928
+ {
929
+ metricName: MonitorMetricType.DownloadTime,
930
+ value: httpTimings.downloadInMs,
931
+ description: "Response download time for this monitor",
932
+ },
933
+ ];
934
+
935
+ for (const phaseMetric of phaseMetrics) {
936
+ await this.pushMonitorMetric({
937
+ projectId: data.projectId,
938
+ monitorId: data.monitorId,
939
+ monitorName: data.monitorName,
940
+ probeName: data.probeName,
941
+ metricName: phaseMetric.metricName,
942
+ value: phaseMetric.value,
943
+ description: phaseMetric.description,
944
+ unit: "ms",
945
+ extraAttributes: extraAttributes,
946
+ metricRows: metricRows,
947
+ metricNameServiceNameMap: metricNameServiceNameMap,
948
+ });
949
+ }
950
+ }
951
+
952
+ if ((data.dataToProcess as ProbeMonitorResponse).pingResponse) {
953
+ const pingResponse: PingMonitorResponse = (
954
+ data.dataToProcess as ProbeMonitorResponse
955
+ ).pingResponse!;
956
+
957
+ const extraAttributes: JSONObject = {
958
+ probeId: (
959
+ data.dataToProcess as ProbeMonitorResponse
960
+ ).probeId.toString(),
961
+ };
962
+
963
+ await this.pushMonitorMetric({
964
+ projectId: data.projectId,
965
+ monitorId: data.monitorId,
966
+ monitorName: data.monitorName,
967
+ probeName: data.probeName,
968
+ metricName: MonitorMetricType.PacketLossPercent,
969
+ value: pingResponse.packetLossPercent,
970
+ description: CheckOn.PacketLossPercent + " for this monitor",
971
+ unit: "%",
972
+ extraAttributes: extraAttributes,
973
+ metricRows: metricRows,
974
+ metricNameServiceNameMap: metricNameServiceNameMap,
975
+ });
976
+
977
+ await this.pushMonitorMetric({
978
+ projectId: data.projectId,
979
+ monitorId: data.monitorId,
980
+ monitorName: data.monitorName,
981
+ probeName: data.probeName,
982
+ metricName: MonitorMetricType.Jitter,
983
+ value: pingResponse.jitterInMs,
984
+ description: CheckOn.Jitter + " for this monitor",
985
+ unit: "ms",
986
+ extraAttributes: extraAttributes,
987
+ metricRows: metricRows,
988
+ metricNameServiceNameMap: metricNameServiceNameMap,
989
+ });
990
+ }
991
+
886
992
  if ((data.dataToProcess as ProbeMonitorResponse).isOnline !== undefined) {
887
993
  const extraAttributes: JSONObject = {
888
994
  probeId: (
@@ -0,0 +1,146 @@
1
+ import AIAgentTaskService from "../../../Server/Services/AIAgentTaskService";
2
+ import Model from "../../../Models/DatabaseModels/AIAgentTask";
3
+ import AIAgentTaskStatus from "../../../Types/AI/AIAgentTaskStatus";
4
+ import ObjectID from "../../../Types/ObjectID";
5
+ import { describe, expect, test, afterEach } from "@jest/globals";
6
+
7
+ /*
8
+ * claimNextScheduledTask is the atomic-claim fix for the get-pending-task
9
+ * double-processing race: multiple KEDA-scaled AIAgent workers poll the same
10
+ * endpoint, and the old plain findOneBy returned the oldest Scheduled task
11
+ * without mutating it, so two workers could both receive — and both execute —
12
+ * the same task.
13
+ *
14
+ * The claim collapses the read-then-take into findOneBy + a status-guarded
15
+ * updateOneBy (Scheduled -> InProgress). updateOneBy returns the number of
16
+ * rows changed: 1 means this caller won the claim; 0 means another worker
17
+ * already took the task, and the loop retries with the next candidate.
18
+ *
19
+ * These tests mock the two inherited persistence helpers (no Postgres) to
20
+ * lock in:
21
+ * (a) a won claim returns the task, with the guard and the InProgress
22
+ * transition on the exact row that was read;
23
+ * (b) a lost claim (0 rows) is never returned — the loop moves to the
24
+ * next candidate;
25
+ * (c) no Scheduled tasks => null, with no write at all;
26
+ * (d) the retry loop is bounded, so a pathological all-losses storm
27
+ * terminates.
28
+ */
29
+
30
+ function fakeTask(id: ObjectID): Model {
31
+ return {
32
+ id,
33
+ _id: id.toString(),
34
+ projectId: ObjectID.generate(),
35
+ taskType: "FixException",
36
+ } as unknown as Model;
37
+ }
38
+
39
+ describe("AIAgentTaskService.claimNextScheduledTask", () => {
40
+ const aiAgentId: ObjectID = ObjectID.generate();
41
+
42
+ afterEach(() => {
43
+ jest.restoreAllMocks();
44
+ });
45
+
46
+ test("returns the task when the claim wins, guarding on Scheduled status", async () => {
47
+ const task: Model = fakeTask(ObjectID.generate());
48
+
49
+ const findOneBy: jest.SpyInstance = jest
50
+ .spyOn(AIAgentTaskService, "findOneBy")
51
+ .mockResolvedValue(task);
52
+ const updateOneBy: jest.SpyInstance = jest
53
+ .spyOn(AIAgentTaskService, "updateOneBy")
54
+ .mockResolvedValue(1);
55
+
56
+ const claimed: Model | null =
57
+ await AIAgentTaskService.claimNextScheduledTask({
58
+ aiAgentId: aiAgentId,
59
+ });
60
+
61
+ expect(claimed).toBe(task);
62
+ expect(findOneBy).toHaveBeenCalledTimes(1);
63
+ expect(findOneBy).toHaveBeenCalledWith(
64
+ expect.objectContaining({
65
+ query: { status: AIAgentTaskStatus.Scheduled },
66
+ }),
67
+ );
68
+ expect(updateOneBy).toHaveBeenCalledTimes(1);
69
+ expect(updateOneBy).toHaveBeenCalledWith(
70
+ expect.objectContaining({
71
+ query: {
72
+ _id: task.id!.toString(),
73
+ status: AIAgentTaskStatus.Scheduled,
74
+ },
75
+ data: expect.objectContaining({
76
+ status: AIAgentTaskStatus.InProgress,
77
+ aiAgentId: aiAgentId,
78
+ startedAt: expect.any(Date),
79
+ }),
80
+ }),
81
+ );
82
+ });
83
+
84
+ test("retries with the next candidate when another worker wins the race", async () => {
85
+ const lostTask: Model = fakeTask(ObjectID.generate());
86
+ const wonTask: Model = fakeTask(ObjectID.generate());
87
+
88
+ const findOneBy: jest.SpyInstance = jest
89
+ .spyOn(AIAgentTaskService, "findOneBy")
90
+ .mockResolvedValueOnce(lostTask)
91
+ .mockResolvedValueOnce(wonTask);
92
+ const updateOneBy: jest.SpyInstance = jest
93
+ .spyOn(AIAgentTaskService, "updateOneBy")
94
+ .mockResolvedValueOnce(0)
95
+ .mockResolvedValueOnce(1);
96
+
97
+ const claimed: Model | null =
98
+ await AIAgentTaskService.claimNextScheduledTask({
99
+ aiAgentId: aiAgentId,
100
+ });
101
+
102
+ expect(claimed).toBe(wonTask);
103
+ expect(findOneBy).toHaveBeenCalledTimes(2);
104
+ expect(updateOneBy).toHaveBeenCalledTimes(2);
105
+ expect(updateOneBy).toHaveBeenLastCalledWith(
106
+ expect.objectContaining({
107
+ query: {
108
+ _id: wonTask.id!.toString(),
109
+ status: AIAgentTaskStatus.Scheduled,
110
+ },
111
+ }),
112
+ );
113
+ });
114
+
115
+ test("returns null without writing when no Scheduled task exists", async () => {
116
+ jest.spyOn(AIAgentTaskService, "findOneBy").mockResolvedValue(null);
117
+ const updateOneBy: jest.SpyInstance = jest
118
+ .spyOn(AIAgentTaskService, "updateOneBy")
119
+ .mockResolvedValue(1);
120
+
121
+ const claimed: Model | null =
122
+ await AIAgentTaskService.claimNextScheduledTask({
123
+ aiAgentId: aiAgentId,
124
+ });
125
+
126
+ expect(claimed).toBeNull();
127
+ expect(updateOneBy).not.toHaveBeenCalled();
128
+ });
129
+
130
+ test("gives up after a bounded number of lost claims", async () => {
131
+ const findOneBy: jest.SpyInstance = jest
132
+ .spyOn(AIAgentTaskService, "findOneBy")
133
+ .mockImplementation(async () => {
134
+ return fakeTask(ObjectID.generate());
135
+ });
136
+ jest.spyOn(AIAgentTaskService, "updateOneBy").mockResolvedValue(0);
137
+
138
+ const claimed: Model | null =
139
+ await AIAgentTaskService.claimNextScheduledTask({
140
+ aiAgentId: aiAgentId,
141
+ });
142
+
143
+ expect(claimed).toBeNull();
144
+ expect(findOneBy).toHaveBeenCalledTimes(5);
145
+ });
146
+ });
@@ -98,6 +98,7 @@ describe("LogAggregationService", () => {
98
98
  ).buildHistogramStatement({
99
99
  ...defaultRequest,
100
100
  facetKey: undefined,
101
+ bucketSizeInMinutes: 60,
101
102
  attributes: { requestid: "uuid-123" },
102
103
  });
103
104
 
@@ -110,4 +111,25 @@ describe("LogAggregationService", () => {
110
111
  expect(Object.values(statement.query_params)).toContain("requestid");
111
112
  expect(Object.values(statement.query_params)).toContain("uuid-123");
112
113
  });
114
+
115
+ test("histogram supports multi-value dashboard attribute filters", () => {
116
+ const statement: Statement = (
117
+ LogAggregationService as any
118
+ ).buildHistogramStatement({
119
+ ...defaultRequest,
120
+ facetKey: undefined,
121
+ bucketSizeInMinutes: 60,
122
+ attributes: { region: ["eu-west-1", "us-east-1"] },
123
+ });
124
+
125
+ expect(statement.query).toContain(
126
+ "arrayExists((k, v) -> lowerUTF8(k) = lowerUTF8(",
127
+ );
128
+ expect(statement.query).toContain("AND v IN (");
129
+ expect(Object.values(statement.query_params)).toContain("region");
130
+ expect(Object.values(statement.query_params)).toContainEqual([
131
+ "eu-west-1",
132
+ "us-east-1",
133
+ ]);
134
+ });
113
135
  });
@@ -0,0 +1,194 @@
1
+ import "../TestingUtils/Init";
2
+ import { MetricService } from "../../../Server/Services/MetricService";
3
+ import Metric from "../../../Models/AnalyticsModels/Metric";
4
+ import AggregateBy from "../../../Server/Types/AnalyticsDatabase/AggregateBy";
5
+ import { Statement } from "../../../Server/Utils/AnalyticsDatabase/Statement";
6
+ import AggregationType from "../../../Types/BaseDatabase/AggregationType";
7
+ import InBetween from "../../../Types/BaseDatabase/InBetween";
8
+ import SortOrder from "../../../Types/BaseDatabase/SortOrder";
9
+ import BadDataException from "../../../Types/Exception/BadDataException";
10
+ import ObjectID from "../../../Types/ObjectID";
11
+
12
+ describe("MetricService aggregate statement generation", () => {
13
+ const projectId: ObjectID = ObjectID.generate();
14
+ const startDate: Date = new Date("2026-07-09T19:08:00.000Z");
15
+ const endDate: Date = new Date("2026-07-09T19:13:00.000Z");
16
+
17
+ let service: MetricService;
18
+
19
+ beforeEach(() => {
20
+ service = new MetricService();
21
+ });
22
+
23
+ type BuildAggregateByFunction = (
24
+ overrides?: Partial<AggregateBy<Metric>>,
25
+ ) => AggregateBy<Metric>;
26
+
27
+ const buildAggregateBy: BuildAggregateByFunction = (
28
+ overrides?: Partial<AggregateBy<Metric>>,
29
+ ): AggregateBy<Metric> => {
30
+ return {
31
+ query: {
32
+ projectId: projectId,
33
+ name: "http.client.request.duration",
34
+ time: new InBetween(startDate, endDate),
35
+ } as AggregateBy<Metric>["query"],
36
+ aggregationType: AggregationType.Avg,
37
+ aggregateColumnName: "value",
38
+ aggregationTimestampColumnName: "time",
39
+ startTimestamp: startDate,
40
+ endTimestamp: endDate,
41
+ limit: 10000,
42
+ skip: 0,
43
+ sort: { time: SortOrder.Ascending } as AggregateBy<Metric>["sort"],
44
+ props: { isRoot: true },
45
+ ...overrides,
46
+ };
47
+ };
48
+
49
+ type GetQueryFunction = (aggregateBy: AggregateBy<Metric>) => string;
50
+
51
+ const getQuery: GetQueryFunction = (
52
+ aggregateBy: AggregateBy<Metric>,
53
+ ): string => {
54
+ const result: { statement: Statement; columns: Array<string> } =
55
+ service.toAggregateStatement(aggregateBy);
56
+ return result.statement.query;
57
+ };
58
+
59
+ describe("scalar aggregations", () => {
60
+ it("routes plain Avg (no filters, no group-by, no distribution hint) to the minute MV", () => {
61
+ const query: string = getQuery(buildAggregateBy());
62
+ expect(query).toContain("MetricItemAggMV1m");
63
+ });
64
+
65
+ it("skips the MVs and uses count-weighted expressions when the metric is a distribution (histogram) metric", () => {
66
+ const aggregateBy: AggregateBy<Metric> = buildAggregateBy();
67
+ (
68
+ service as unknown as {
69
+ pointTypeHintByAggregate: WeakMap<AggregateBy<Metric>, string | null>;
70
+ }
71
+ ).pointTypeHintByAggregate.set(aggregateBy, "Histogram");
72
+
73
+ const query: string = getQuery(aggregateBy);
74
+
75
+ expect(query).not.toContain("MetricItemAggMV1m");
76
+ /*
77
+ * Avg must be count-weighted (sum of observation totals divided
78
+ * by sum of observation counts) — NOT avg(value), which for
79
+ * histogram rows averages per-export-interval sums.
80
+ */
81
+ expect(query).toContain("sum(multiIf(isNotNull(count)");
82
+ expect(query).toContain("toFloat64(count)");
83
+ expect(query).not.toContain("avg(value)");
84
+ });
85
+
86
+ it("uses count-weighted expressions on the base table when an attribute filter blocks the MVs", () => {
87
+ const aggregateBy: AggregateBy<Metric> = buildAggregateBy({
88
+ query: {
89
+ projectId: projectId,
90
+ name: "http.client.request.duration",
91
+ time: new InBetween(startDate, endDate),
92
+ attributes: { "oneuptime.service.name": "starship-web" },
93
+ } as unknown as AggregateBy<Metric>["query"],
94
+ });
95
+
96
+ const query: string = getQuery(aggregateBy);
97
+
98
+ expect(query).not.toContain("MetricItemAggMV1m");
99
+ expect(query).toContain("sum(multiIf(isNotNull(count)");
100
+ });
101
+
102
+ it("groups by individual attribute keys, not the whole attributes map", () => {
103
+ const aggregateBy: AggregateBy<Metric> = buildAggregateBy({
104
+ groupByAttributeKeys: ["oneuptime.service.name"],
105
+ });
106
+
107
+ const result: { statement: Statement; columns: Array<string> } =
108
+ service.toAggregateStatement(aggregateBy);
109
+ const query: string = result.statement.query;
110
+
111
+ // Key extraction is parameter-bound and aliased...
112
+ expect(query).toMatch(/attributes\[\{p\d+:String\}\] AS __attr_grp_0/);
113
+ // ...regrouped rows re-project the selected keys as a compact map...
114
+ expect(query).toMatch(
115
+ /map\(\{p\d+:String\}, __attr_grp_0\) AS attributes/,
116
+ );
117
+ // ...grouping happens on the extracted key, never the Map column.
118
+ expect(query).toContain("GROUP BY time, __attr_grp_0");
119
+ expect(query).not.toContain("GROUP BY time, attributes");
120
+
121
+ // The selected key reaches the query only as a bound parameter.
122
+ const parameterValues: Array<unknown> = Object.values(
123
+ result.statement.query_params,
124
+ );
125
+ expect(
126
+ parameterValues.filter((value: unknown) => {
127
+ return value === "oneuptime.service.name";
128
+ }),
129
+ ).toHaveLength(2); // extraction + map key
130
+
131
+ expect(result.columns).toContain("attributes");
132
+ });
133
+
134
+ it("drops a legacy whole-map attributes group-by when key-scoped grouping is requested", () => {
135
+ const aggregateBy: AggregateBy<Metric> = buildAggregateBy({
136
+ groupBy: { attributes: true } as AggregateBy<Metric>["groupBy"],
137
+ groupByAttributeKeys: ["oneuptime.service.name"],
138
+ });
139
+
140
+ const query: string = getQuery(aggregateBy);
141
+
142
+ expect(query).toContain("GROUP BY time, __attr_grp_0");
143
+ expect(query).not.toContain("GROUP BY time, attributes");
144
+ });
145
+
146
+ it("rejects more than 10 group-by attribute keys", () => {
147
+ const aggregateBy: AggregateBy<Metric> = buildAggregateBy({
148
+ groupByAttributeKeys: Array.from(
149
+ { length: 11 },
150
+ (_: unknown, i: number) => {
151
+ return `key.${i}`;
152
+ },
153
+ ),
154
+ });
155
+
156
+ expect(() => {
157
+ return service.toAggregateStatement(aggregateBy);
158
+ }).toThrow(BadDataException);
159
+ });
160
+ });
161
+
162
+ describe("percentile aggregations", () => {
163
+ it("keeps the histogram bucket fanout and pools by selected attribute keys", () => {
164
+ const aggregateBy: AggregateBy<Metric> = buildAggregateBy({
165
+ aggregationType: AggregationType.P90,
166
+ groupByAttributeKeys: ["oneuptime.service.name"],
167
+ });
168
+
169
+ const result: { statement: Statement; columns: Array<string> } =
170
+ service.toAggregateStatement(aggregateBy);
171
+ const query: string = result.statement.query;
172
+
173
+ expect(query).toContain("quantileExactWeighted(0.9)");
174
+ expect(query).toMatch(/attributes\[\{p\d+:String\}\] AS __attr_grp_0/);
175
+ expect(query).toMatch(
176
+ /map\(\{p\d+:String\}, __attr_grp_0\) AS attributes/,
177
+ );
178
+ expect(query).toContain("GROUP BY time, __attr_grp_0");
179
+ expect(result.columns).toContain("attributes");
180
+ });
181
+
182
+ it("does not group when no attribute keys are selected (regression: whole-map fragmentation)", () => {
183
+ const aggregateBy: AggregateBy<Metric> = buildAggregateBy({
184
+ aggregationType: AggregationType.P90,
185
+ });
186
+
187
+ const query: string = getQuery(aggregateBy);
188
+
189
+ expect(query).toContain("quantileExactWeighted(0.9)");
190
+ expect(query).not.toContain("__attr_grp_0");
191
+ expect(query).toContain("GROUP BY time");
192
+ });
193
+ });
194
+ });
@@ -0,0 +1,86 @@
1
+ import AIAgentTaskStatus, {
2
+ AIAgentTaskStatusHelper,
3
+ AIAgentTaskStatusProps,
4
+ } from "../../../Types/AI/AIAgentTaskStatus";
5
+
6
+ describe("AIAgentTaskStatusHelper", () => {
7
+ describe("getAllStatusProps", () => {
8
+ test("returns one entry per status with a title and description", () => {
9
+ const props: Array<AIAgentTaskStatusProps> =
10
+ AIAgentTaskStatusHelper.getAllStatusProps();
11
+
12
+ const statuses: Array<AIAgentTaskStatus> =
13
+ Object.values(AIAgentTaskStatus);
14
+ expect(props.length).toBe(statuses.length);
15
+
16
+ for (const prop of props) {
17
+ expect(typeof prop.title).toBe("string");
18
+ expect(prop.title.length).toBeGreaterThan(0);
19
+ expect(typeof prop.description).toBe("string");
20
+ expect(prop.description.length).toBeGreaterThan(0);
21
+ }
22
+ });
23
+
24
+ test("covers every status in the enum exactly once", () => {
25
+ const seen: Set<AIAgentTaskStatus> = new Set<AIAgentTaskStatus>();
26
+ for (const prop of AIAgentTaskStatusHelper.getAllStatusProps()) {
27
+ expect(seen.has(prop.status)).toBe(false);
28
+ seen.add(prop.status);
29
+ }
30
+
31
+ for (const status of Object.values(AIAgentTaskStatus)) {
32
+ expect(seen.has(status)).toBe(true);
33
+ }
34
+ });
35
+ });
36
+
37
+ describe("getTitle", () => {
38
+ test("returns the configured title for a known status", () => {
39
+ expect(
40
+ AIAgentTaskStatusHelper.getTitle(AIAgentTaskStatus.InProgress),
41
+ ).toBe("In Progress");
42
+ expect(
43
+ AIAgentTaskStatusHelper.getTitle(AIAgentTaskStatus.Completed),
44
+ ).toBe("Completed");
45
+ });
46
+
47
+ test("returns an empty string for an unknown status", () => {
48
+ expect(
49
+ AIAgentTaskStatusHelper.getTitle("NonExistent" as AIAgentTaskStatus),
50
+ ).toBe("");
51
+ });
52
+ });
53
+
54
+ describe("getDescription", () => {
55
+ test("returns a non-empty description for a known status", () => {
56
+ expect(
57
+ AIAgentTaskStatusHelper.getDescription(AIAgentTaskStatus.Scheduled)
58
+ .length,
59
+ ).toBeGreaterThan(0);
60
+ });
61
+
62
+ test("returns an empty string for an unknown status", () => {
63
+ expect(
64
+ AIAgentTaskStatusHelper.getDescription(
65
+ "NonExistent" as AIAgentTaskStatus,
66
+ ),
67
+ ).toBe("");
68
+ });
69
+ });
70
+
71
+ describe("isTerminalStatus", () => {
72
+ test.each([AIAgentTaskStatus.Completed, AIAgentTaskStatus.Error])(
73
+ "returns true for terminal status %s",
74
+ (status: AIAgentTaskStatus) => {
75
+ expect(AIAgentTaskStatusHelper.isTerminalStatus(status)).toBe(true);
76
+ },
77
+ );
78
+
79
+ test.each([AIAgentTaskStatus.Scheduled, AIAgentTaskStatus.InProgress])(
80
+ "returns false for non-terminal status %s",
81
+ (status: AIAgentTaskStatus) => {
82
+ expect(AIAgentTaskStatusHelper.isTerminalStatus(status)).toBe(false);
83
+ },
84
+ );
85
+ });
86
+ });