@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.
- package/Server/Services/MetricService.ts +1 -1
- package/Server/Types/Database/QueryHelper.ts +54 -1
- package/Server/Utils/AI/Toolbox/MetricTools.ts +13 -2
- package/Server/Utils/AI/Toolbox/ResourceTools.ts +12 -1
- package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +1 -1
- package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +33 -0
- package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +4 -3
- package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +3 -1
- package/Tests/App/Dashboard/NetworkTopologyExplorerSearch.test.tsx +1267 -0
- package/Tests/App/Dashboard/ResourceOverviewTileTooltips.test.tsx +4 -1
- package/Tests/App/Dashboard/RumOverviewPage.test.tsx +127 -0
- package/Tests/App/Dashboard/SiteSearchBox.test.tsx +2110 -0
- package/Tests/App/Dashboard/TelemetryMetricsSignals.test.ts +168 -8
- package/Tests/Server/Services/MetricServiceAggregate.test.ts +12 -0
- package/Tests/Server/Services/SavedViewDefaults.test.ts +325 -0
- package/Tests/Server/Services/StatusPageCertificateService.test.ts +169 -0
- package/Tests/Server/Services/TeamComplianceSettingService.test.ts +191 -0
- package/Tests/Server/Types/Database/QueryHelperSearchAllWords.test.ts +651 -0
- package/Tests/Server/Utils/AI/AlertWriteTools.test.ts +256 -0
- package/Tests/Server/Utils/AI/IncidentWriteTools.test.ts +482 -0
- package/Tests/Server/Utils/Monitor/Criteria/CompareCriteria.test.ts +54 -0
- package/Tests/Server/Utils/Monitor/MetricDecimalThreshold.test.ts +417 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorAffectedResourceEdgeCases.test.ts +96 -0
- package/Tests/Types/BaseDatabase/AggregationType.test.ts +5 -1
- package/Tests/Types/Dashboard/KubernetesDashboardTemplate.test.ts +184 -0
- package/Tests/Types/Dashboard/KubernetesDashboardTemplates.test.ts +107 -0
- package/Tests/Types/Monitor/KubernetesAlertTemplates.test.ts +8 -14
- package/Tests/Types/Monitor/KubernetesMetricCatalog.test.ts +4 -3
- package/Tests/Types/Monitor/Utils/KubernetesAgentEmittedMetrics.ts +27 -6
- package/Tests/UI/Rum/ChunkLoader.test.ts +76 -0
- package/Tests/UI/Rum/ReplayRailDetail.test.tsx +53 -0
- package/Tests/UI/Rum/ReplaySignals.test.ts +54 -0
- package/Tests/Utils/Dashboard/KubernetesClusterCpuTileRepair.test.ts +370 -0
- package/Tests/Utils/Rules/MonitorTypeRuleCriteria.test.ts +181 -0
- package/Types/BaseDatabase/AggregationType.ts +7 -0
- package/Types/Dashboard/DashboardTemplates.ts +20 -7
- package/Types/Rum/SessionReplayCustomEvents.ts +29 -3
- package/Types/Rum/WebVitals.ts +13 -0
- package/UI/Components/Input/Input.tsx +11 -0
- package/Utils/Dashboard/KubernetesClusterCpuTileRepair.ts +147 -0
- package/build/dist/Server/Services/MetricService.js +1 -1
- package/build/dist/Server/Types/Database/QueryHelper.js +47 -1
- package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/MetricTools.js +13 -2
- package/build/dist/Server/Utils/AI/Toolbox/MetricTools.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/ResourceTools.js +12 -1
- package/build/dist/Server/Utils/AI/Toolbox/ResourceTools.js.map +1 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +31 -0
- package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
- package/build/dist/Types/BaseDatabase/AggregationType.js +7 -0
- package/build/dist/Types/BaseDatabase/AggregationType.js.map +1 -1
- package/build/dist/Types/Dashboard/DashboardTemplates.js +20 -7
- package/build/dist/Types/Dashboard/DashboardTemplates.js.map +1 -1
- package/build/dist/Types/Rum/SessionReplayCustomEvents.js.map +1 -1
- package/build/dist/Types/Rum/WebVitals.js +12 -0
- package/build/dist/Types/Rum/WebVitals.js.map +1 -1
- package/build/dist/UI/Components/Input/Input.js +1 -1
- package/build/dist/UI/Components/Input/Input.js.map +1 -1
- package/build/dist/Utils/Dashboard/KubernetesClusterCpuTileRepair.js +110 -0
- package/build/dist/Utils/Dashboard/KubernetesClusterCpuTileRepair.js.map +1 -0
- 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
|
+
});
|