@oneuptime/common 14.0.5 → 14.0.6

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 (66) hide show
  1. package/Server/Services/MetricService.ts +1 -1
  2. package/Server/Types/Database/QueryHelper.ts +54 -1
  3. package/Server/Utils/AI/Toolbox/MetricTools.ts +13 -2
  4. package/Server/Utils/AI/Toolbox/ResourceTools.ts +12 -1
  5. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +1 -1
  6. package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +33 -0
  7. package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +4 -3
  8. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +3 -1
  9. package/Tests/App/Dashboard/NetworkTopologyExplorerSearch.test.tsx +1267 -0
  10. package/Tests/App/Dashboard/ResourceOverviewTileTooltips.test.tsx +4 -1
  11. package/Tests/App/Dashboard/RumOverviewPage.test.tsx +127 -0
  12. package/Tests/App/Dashboard/SiteSearchBox.test.tsx +2110 -0
  13. package/Tests/App/Dashboard/TelemetryMetricsSignals.test.ts +168 -8
  14. package/Tests/Server/Services/MetricServiceAggregate.test.ts +12 -0
  15. package/Tests/Server/Services/SavedViewDefaults.test.ts +325 -0
  16. package/Tests/Server/Services/StatusPageCertificateService.test.ts +169 -0
  17. package/Tests/Server/Services/TeamComplianceSettingService.test.ts +191 -0
  18. package/Tests/Server/Types/Database/QueryHelperSearchAllWords.test.ts +651 -0
  19. package/Tests/Server/Utils/AI/AlertWriteTools.test.ts +256 -0
  20. package/Tests/Server/Utils/AI/IncidentWriteTools.test.ts +482 -0
  21. package/Tests/Server/Utils/Monitor/Criteria/CompareCriteria.test.ts +54 -0
  22. package/Tests/Server/Utils/Monitor/MetricDecimalThreshold.test.ts +417 -0
  23. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorAffectedResourceEdgeCases.test.ts +96 -0
  24. package/Tests/Types/BaseDatabase/AggregationType.test.ts +5 -1
  25. package/Tests/Types/Dashboard/KubernetesDashboardTemplate.test.ts +184 -0
  26. package/Tests/Types/Dashboard/KubernetesDashboardTemplates.test.ts +107 -0
  27. package/Tests/Types/Monitor/KubernetesAlertTemplates.test.ts +8 -14
  28. package/Tests/Types/Monitor/KubernetesMetricCatalog.test.ts +4 -3
  29. package/Tests/Types/Monitor/Utils/KubernetesAgentEmittedMetrics.ts +27 -6
  30. package/Tests/UI/Rum/ChunkLoader.test.ts +76 -0
  31. package/Tests/UI/Rum/ReplayRailDetail.test.tsx +53 -0
  32. package/Tests/UI/Rum/ReplaySignals.test.ts +54 -0
  33. package/Tests/Utils/Dashboard/KubernetesClusterCpuTileRepair.test.ts +370 -0
  34. package/Tests/Utils/Rules/MonitorTypeRuleCriteria.test.ts +181 -0
  35. package/Types/BaseDatabase/AggregationType.ts +7 -0
  36. package/Types/Dashboard/DashboardTemplates.ts +20 -7
  37. package/Types/Rum/SessionReplayCustomEvents.ts +29 -3
  38. package/Types/Rum/WebVitals.ts +13 -0
  39. package/UI/Components/Input/Input.tsx +11 -0
  40. package/Utils/Dashboard/KubernetesClusterCpuTileRepair.ts +147 -0
  41. package/build/dist/Server/Services/MetricService.js +1 -1
  42. package/build/dist/Server/Types/Database/QueryHelper.js +47 -1
  43. package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
  44. package/build/dist/Server/Utils/AI/Toolbox/MetricTools.js +13 -2
  45. package/build/dist/Server/Utils/AI/Toolbox/MetricTools.js.map +1 -1
  46. package/build/dist/Server/Utils/AI/Toolbox/ResourceTools.js +12 -1
  47. package/build/dist/Server/Utils/AI/Toolbox/ResourceTools.js.map +1 -1
  48. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +1 -1
  49. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +31 -0
  50. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js.map +1 -1
  51. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +1 -1
  52. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js.map +1 -1
  53. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +1 -1
  54. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  55. package/build/dist/Types/BaseDatabase/AggregationType.js +7 -0
  56. package/build/dist/Types/BaseDatabase/AggregationType.js.map +1 -1
  57. package/build/dist/Types/Dashboard/DashboardTemplates.js +20 -7
  58. package/build/dist/Types/Dashboard/DashboardTemplates.js.map +1 -1
  59. package/build/dist/Types/Rum/SessionReplayCustomEvents.js.map +1 -1
  60. package/build/dist/Types/Rum/WebVitals.js +12 -0
  61. package/build/dist/Types/Rum/WebVitals.js.map +1 -1
  62. package/build/dist/UI/Components/Input/Input.js +1 -1
  63. package/build/dist/UI/Components/Input/Input.js.map +1 -1
  64. package/build/dist/Utils/Dashboard/KubernetesClusterCpuTileRepair.js +110 -0
  65. package/build/dist/Utils/Dashboard/KubernetesClusterCpuTileRepair.js.map +1 -0
  66. package/package.json +1 -1
@@ -0,0 +1,256 @@
1
+ import {
2
+ AcknowledgeAlertTool,
3
+ ResolveAlertTool,
4
+ } from "../../../../Server/Utils/AI/Toolbox/AlertWriteTools";
5
+ import {
6
+ ObservabilityTool,
7
+ ToolContext,
8
+ ToolExecutionResult,
9
+ } from "../../../../Server/Utils/AI/Toolbox/ToolTypes";
10
+ import AlertService from "../../../../Server/Services/AlertService";
11
+ import Alert from "../../../../Models/DatabaseModels/Alert";
12
+ import { AIChatCitationTargetType } from "../../../../Types/AI/AIChatTypes";
13
+ import ObjectID from "../../../../Types/ObjectID";
14
+ import { afterEach, describe, expect, test } from "@jest/globals";
15
+
16
+ /*
17
+ * acknowledge_alert / resolve_alert move an alert's state from the AI copilot.
18
+ * They are mutations, so what matters is not the prose they hand the model but
19
+ * the contract around the write: the alert must be visible to the requesting
20
+ * user under their own RBAC before anything changes, the acting user comes
21
+ * from ctx.props and never from a tool argument, and a missing id or a missing
22
+ * user fails loudly instead of writing.
23
+ */
24
+
25
+ const USER_ID: ObjectID = ObjectID.generate();
26
+ const ALERT_ID: ObjectID = ObjectID.generate();
27
+
28
+ const ctx: ToolContext = {
29
+ projectId: ObjectID.generate(),
30
+ props: { isRoot: true, userId: USER_ID },
31
+ };
32
+
33
+ function buildAlert(): Alert {
34
+ const alert: Alert = new Alert();
35
+ alert._id = ALERT_ID.toString();
36
+ alert.alertNumber = 9;
37
+ alert.title = "High CPU on api-1";
38
+ return alert;
39
+ }
40
+
41
+ afterEach(() => {
42
+ jest.restoreAllMocks();
43
+ });
44
+
45
+ type StateCase = {
46
+ label: string;
47
+ tool: ObservabilityTool;
48
+ serviceMethod: "acknowledgeAlert" | "resolveAlert";
49
+ newStateName: string;
50
+ actionTitlePrefix: string;
51
+ };
52
+
53
+ const cases: Array<StateCase> = [
54
+ {
55
+ label: "acknowledge_alert",
56
+ tool: AcknowledgeAlertTool,
57
+ serviceMethod: "acknowledgeAlert",
58
+ newStateName: "Acknowledged",
59
+ actionTitlePrefix: "Acknowledge alert",
60
+ },
61
+ {
62
+ label: "resolve_alert",
63
+ tool: ResolveAlertTool,
64
+ serviceMethod: "resolveAlert",
65
+ newStateName: "Resolved",
66
+ actionTitlePrefix: "Resolve alert",
67
+ },
68
+ ];
69
+
70
+ describe.each(cases)(
71
+ "$label",
72
+ ({ tool, serviceMethod, newStateName, actionTitlePrefix }: StateCase) => {
73
+ test("changes the state as ctx's user and cites the alert", async () => {
74
+ jest
75
+ .spyOn(AlertService, "findOneById")
76
+ .mockResolvedValue(buildAlert() as never);
77
+ const changeSpy: jest.SpyInstance = jest
78
+ .spyOn(AlertService, serviceMethod)
79
+ .mockResolvedValue(undefined as never);
80
+
81
+ const result: ToolExecutionResult = await tool.execute(
82
+ { alertId: ALERT_ID.toString() },
83
+ ctx,
84
+ );
85
+
86
+ expect(changeSpy).toHaveBeenCalledTimes(1);
87
+ const [calledAlertId, calledUserId] = changeSpy.mock.calls[0] as [
88
+ ObjectID,
89
+ ObjectID,
90
+ ];
91
+ expect(calledAlertId.toString()).toBe(ALERT_ID.toString());
92
+ expect(calledUserId).toBe(USER_ID);
93
+
94
+ expect(result.rowCount).toBe(1);
95
+ expect(result.isTruncated).toBe(false);
96
+ expect(result.dataForLlm).toContain("#9");
97
+ expect(result.dataForLlm).toContain("High CPU on api-1");
98
+ expect(result.dataForLlm).toContain(newStateName);
99
+ expect(result.citationLabel).toBe(`${newStateName} alert #9`);
100
+ expect(result.citationTarget).toEqual({
101
+ type: AIChatCitationTargetType.AlertView,
102
+ params: { alertId: ALERT_ID.toString() },
103
+ });
104
+ });
105
+
106
+ test("reads the alert under the caller's own props, not as root", async () => {
107
+ const findSpy: jest.SpyInstance = jest
108
+ .spyOn(AlertService, "findOneById")
109
+ .mockResolvedValue(buildAlert() as never);
110
+ jest
111
+ .spyOn(AlertService, serviceMethod)
112
+ .mockResolvedValue(undefined as never);
113
+
114
+ await tool.execute({ alertId: ALERT_ID.toString() }, ctx);
115
+
116
+ const findArgs: Record<string, unknown> = findSpy.mock
117
+ .calls[0]?.[0] as Record<string, unknown>;
118
+ expect(findArgs["props"]).toBe(ctx.props);
119
+ expect(String(findArgs["id"])).toBe(ALERT_ID.toString());
120
+ });
121
+
122
+ test("the acting user comes from ctx.props even when args smuggle a userId", async () => {
123
+ jest
124
+ .spyOn(AlertService, "findOneById")
125
+ .mockResolvedValue(buildAlert() as never);
126
+ const changeSpy: jest.SpyInstance = jest
127
+ .spyOn(AlertService, serviceMethod)
128
+ .mockResolvedValue(undefined as never);
129
+
130
+ await tool.execute(
131
+ {
132
+ alertId: ALERT_ID.toString(),
133
+ userId: ObjectID.generate().toString(),
134
+ acknowledgedByUserId: ObjectID.generate().toString(),
135
+ resolvedByUserId: ObjectID.generate().toString(),
136
+ },
137
+ ctx,
138
+ );
139
+
140
+ expect(changeSpy.mock.calls[0]?.[1]).toBe(USER_ID);
141
+ });
142
+
143
+ test("refuses to write when the alert is not visible to the user", async () => {
144
+ jest.spyOn(AlertService, "findOneById").mockResolvedValue(null as never);
145
+ const changeSpy: jest.SpyInstance = jest.spyOn(
146
+ AlertService,
147
+ serviceMethod,
148
+ );
149
+
150
+ await expect(
151
+ tool.execute({ alertId: ALERT_ID.toString() }, ctx),
152
+ ).rejects.toThrow("Alert not found");
153
+ expect(changeSpy).not.toHaveBeenCalled();
154
+ });
155
+
156
+ test("missing alertId is a loud BadData error and reads nothing", async () => {
157
+ const findSpy: jest.SpyInstance = jest.spyOn(AlertService, "findOneById");
158
+
159
+ await expect(tool.execute({}, ctx)).rejects.toThrow(
160
+ "alertId is required",
161
+ );
162
+ expect(findSpy).not.toHaveBeenCalled();
163
+ });
164
+
165
+ test("refuses to write without an authenticated user in context", async () => {
166
+ const anonymousCtx: ToolContext = {
167
+ projectId: ctx.projectId,
168
+ props: { isRoot: true },
169
+ };
170
+ const findSpy: jest.SpyInstance = jest.spyOn(AlertService, "findOneById");
171
+
172
+ await expect(
173
+ tool.execute({ alertId: ALERT_ID.toString() }, anonymousCtx),
174
+ ).rejects.toThrow("No authenticated user");
175
+ // The user check comes before the read, so nothing is touched at all.
176
+ expect(findSpy).not.toHaveBeenCalled();
177
+ });
178
+
179
+ test("carries a resource card widget that deep-links to the alert", async () => {
180
+ jest
181
+ .spyOn(AlertService, "findOneById")
182
+ .mockResolvedValue(buildAlert() as never);
183
+ jest
184
+ .spyOn(AlertService, serviceMethod)
185
+ .mockResolvedValue(undefined as never);
186
+
187
+ const result: ToolExecutionResult = await tool.execute(
188
+ { alertId: ALERT_ID.toString() },
189
+ ctx,
190
+ );
191
+
192
+ const widgetData: Record<string, unknown> = result.widget?.data as Record<
193
+ string,
194
+ unknown
195
+ >;
196
+ expect(widgetData["resourceType"]).toBe("Alert");
197
+ expect(widgetData["heading"]).toBe("#9 · High CPU on api-1");
198
+ expect(widgetData["subheading"]).toBe(`Now ${newStateName}`);
199
+ expect(widgetData["link"]).toEqual({
200
+ type: AIChatCitationTargetType.AlertView,
201
+ params: { alertId: ALERT_ID.toString() },
202
+ });
203
+ expect(widgetData["fields"]).toEqual([
204
+ { label: "Number", value: "#9" },
205
+ { label: "State", value: newStateName },
206
+ ]);
207
+ });
208
+
209
+ test("buildActionTitle names the alert, and stays tidy with no id", () => {
210
+ expect(tool.buildActionTitle!({ alertId: ALERT_ID.toString() })).toBe(
211
+ `${actionTitlePrefix} ${ALERT_ID.toString()}`,
212
+ );
213
+ // No trailing space when the model calls it before it has an id.
214
+ expect(tool.buildActionTitle!({})).toBe(actionTitlePrefix);
215
+ });
216
+
217
+ test("is a mutation whose permissions derive from the Alert model's update ACL", () => {
218
+ expect(tool.isMutation).toBe(true);
219
+ expect(tool.requiredPermissions).toEqual(
220
+ new Alert().getUpdatePermissions(),
221
+ );
222
+ expect(tool.requiredPermissions.length).toBeGreaterThan(0);
223
+ // Resolved lazily, but the same list every time it is asked for.
224
+ expect(tool.requiredPermissions).toEqual(tool.requiredPermissions);
225
+ });
226
+ },
227
+ );
228
+
229
+ describe("the alert write tools as a pair", () => {
230
+ test("acknowledge and resolve are distinct tools with distinct names", () => {
231
+ expect(AcknowledgeAlertTool.name).toBe("acknowledge_alert");
232
+ expect(ResolveAlertTool.name).toBe("resolve_alert");
233
+ });
234
+
235
+ test("resolve does not acknowledge, and acknowledge does not resolve", async () => {
236
+ jest
237
+ .spyOn(AlertService, "findOneById")
238
+ .mockResolvedValue(buildAlert() as never);
239
+ const acknowledgeSpy: jest.SpyInstance = jest
240
+ .spyOn(AlertService, "acknowledgeAlert")
241
+ .mockResolvedValue(undefined as never);
242
+ const resolveSpy: jest.SpyInstance = jest
243
+ .spyOn(AlertService, "resolveAlert")
244
+ .mockResolvedValue(undefined as never);
245
+
246
+ await ResolveAlertTool.execute({ alertId: ALERT_ID.toString() }, ctx);
247
+
248
+ expect(resolveSpy).toHaveBeenCalledTimes(1);
249
+ expect(acknowledgeSpy).not.toHaveBeenCalled();
250
+
251
+ await AcknowledgeAlertTool.execute({ alertId: ALERT_ID.toString() }, ctx);
252
+
253
+ expect(acknowledgeSpy).toHaveBeenCalledTimes(1);
254
+ expect(resolveSpy).toHaveBeenCalledTimes(1);
255
+ });
256
+ });