@oneuptime/common 11.5.12 → 11.5.13
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/Models/DatabaseModels/AIInsight.ts +30 -0
- package/Models/DatabaseModels/Project.ts +30 -0
- package/Models/DatabaseModels/TelemetryException.ts +128 -0
- package/Server/API/AIAgentDataAPI.ts +129 -5
- package/Server/API/AIInvestigationAPI.ts +87 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1784640000000-AddExceptionTriageAndPrivacyColumns.ts +56 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Server/Services/AIRunService.ts +8 -0
- package/Server/Services/TelemetryExceptionService.ts +320 -24
- package/Server/Utils/AI/SRE/Insights/Detectors/ExceptionSpikeDetector.ts +15 -4
- package/Server/Utils/AI/SRE/Insights/Detectors/NewExceptionDetector.ts +15 -4
- package/Server/Utils/AI/SRE/Insights/InsightScanner.ts +222 -24
- package/Server/Utils/AI/SRE/Insights/InsightTriageRunner.ts +225 -9
- package/Server/Utils/AI/SRE/SubjectCodeFixRun.ts +39 -0
- package/Server/Utils/AI/SRE/TelemetryImprovementTaskTrigger.ts +114 -0
- package/Server/Utils/Telemetry/ExceptionSanitizer.ts +222 -0
- package/Tests/Server/Services/TelemetryExceptionCodeFixRun.test.ts +205 -1
- package/Tests/Server/Utils/AI/Insights/InsightScanner.test.ts +150 -141
- package/Tests/Server/Utils/AI/Insights/InsightTriageRunner.test.ts +277 -1
- package/Tests/Server/Utils/ExceptionSanitizer.test.ts +70 -0
- package/Tests/Server/Utils/InsightTriageClassification.test.ts +57 -0
- package/Tests/Server/Utils/TelemetryImprovementTaskTrigger.test.ts +141 -0
- package/Tests/UI/Components/ModelTableSelectFromColumns.test.ts +259 -0
- package/Types/AI/CodeFixTaskContext.ts +6 -0
- package/Types/AI/CodeFixTaskType.ts +38 -1
- package/Types/AI/ExceptionAIClassification.ts +27 -0
- package/Types/Log/LogScrubPatternType.ts +7 -0
- package/Types/Trace/TraceScrubPatternType.ts +7 -0
- package/UI/Components/ModelTable/BaseModelTable.tsx +15 -46
- package/UI/Components/ModelTable/SelectFromColumns.ts +129 -0
- package/build/dist/Models/DatabaseModels/AIInsight.js +31 -0
- package/build/dist/Models/DatabaseModels/AIInsight.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Project.js +31 -0
- package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
- package/build/dist/Models/DatabaseModels/TelemetryException.js +134 -0
- package/build/dist/Models/DatabaseModels/TelemetryException.js.map +1 -1
- package/build/dist/Server/API/AIAgentDataAPI.js +97 -8
- package/build/dist/Server/API/AIAgentDataAPI.js.map +1 -1
- package/build/dist/Server/API/AIInvestigationAPI.js +55 -0
- package/build/dist/Server/API/AIInvestigationAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784640000000-AddExceptionTriageAndPrivacyColumns.js +33 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784640000000-AddExceptionTriageAndPrivacyColumns.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/AIRunService.js +9 -2
- package/build/dist/Server/Services/AIRunService.js.map +1 -1
- package/build/dist/Server/Services/TelemetryExceptionService.js +263 -22
- package/build/dist/Server/Services/TelemetryExceptionService.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/Insights/Detectors/ExceptionSpikeDetector.js +12 -4
- package/build/dist/Server/Utils/AI/SRE/Insights/Detectors/ExceptionSpikeDetector.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/Insights/Detectors/NewExceptionDetector.js +12 -4
- package/build/dist/Server/Utils/AI/SRE/Insights/Detectors/NewExceptionDetector.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/Insights/InsightScanner.js +193 -22
- package/build/dist/Server/Utils/AI/SRE/Insights/InsightScanner.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/Insights/InsightTriageRunner.js +178 -9
- package/build/dist/Server/Utils/AI/SRE/Insights/InsightTriageRunner.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/SubjectCodeFixRun.js +37 -0
- package/build/dist/Server/Utils/AI/SRE/SubjectCodeFixRun.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/TelemetryImprovementTaskTrigger.js +98 -0
- package/build/dist/Server/Utils/AI/SRE/TelemetryImprovementTaskTrigger.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/ExceptionSanitizer.js +149 -0
- package/build/dist/Server/Utils/Telemetry/ExceptionSanitizer.js.map +1 -0
- package/build/dist/Types/AI/CodeFixTaskType.js +38 -1
- package/build/dist/Types/AI/CodeFixTaskType.js.map +1 -1
- package/build/dist/Types/AI/ExceptionAIClassification.js +28 -0
- package/build/dist/Types/AI/ExceptionAIClassification.js.map +1 -0
- package/build/dist/Types/Log/LogScrubPatternType.js +7 -0
- package/build/dist/Types/Log/LogScrubPatternType.js.map +1 -1
- package/build/dist/Types/Trace/TraceScrubPatternType.js +7 -0
- package/build/dist/Types/Trace/TraceScrubPatternType.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +12 -37
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/SelectFromColumns.js +82 -0
- package/build/dist/UI/Components/ModelTable/SelectFromColumns.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import Columns from "../../../UI/Components/ModelTable/Columns";
|
|
2
|
+
import {
|
|
3
|
+
getRelationSelectFromColumns,
|
|
4
|
+
getSelectFromColumns,
|
|
5
|
+
} from "../../../UI/Components/ModelTable/SelectFromColumns";
|
|
6
|
+
import FieldType from "../../../UI/Components/Types/FieldType";
|
|
7
|
+
import Alert from "../../../Models/DatabaseModels/Alert";
|
|
8
|
+
import ProxmoxCluster from "../../../Models/DatabaseModels/ProxmoxCluster";
|
|
9
|
+
import BadDataException from "../../../Types/Exception/BadDataException";
|
|
10
|
+
import { describe, expect, test } from "@jest/globals";
|
|
11
|
+
|
|
12
|
+
describe("ModelTable SelectFromColumns", () => {
|
|
13
|
+
describe("getSelectFromColumns", () => {
|
|
14
|
+
test("it selects every field a column declares, not just the first", () => {
|
|
15
|
+
/*
|
|
16
|
+
* Regression for OneUptime/oneuptime#2756: the Proxmox clusters list
|
|
17
|
+
* rendered "0/3 online" for a fully online cluster because only the
|
|
18
|
+
* first key of the column's field map was selected, leaving
|
|
19
|
+
* onlineNodeCount undefined on every row.
|
|
20
|
+
*/
|
|
21
|
+
const columns: Columns<ProxmoxCluster> = [
|
|
22
|
+
{
|
|
23
|
+
field: {
|
|
24
|
+
nodeCount: true,
|
|
25
|
+
onlineNodeCount: true,
|
|
26
|
+
},
|
|
27
|
+
title: "Nodes",
|
|
28
|
+
type: FieldType.Element,
|
|
29
|
+
},
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const select: Record<string, unknown> =
|
|
33
|
+
getSelectFromColumns<ProxmoxCluster>({
|
|
34
|
+
columns: columns,
|
|
35
|
+
model: new ProxmoxCluster(),
|
|
36
|
+
}) as Record<string, unknown>;
|
|
37
|
+
|
|
38
|
+
expect(select["nodeCount"]).toBe(true);
|
|
39
|
+
expect(select["onlineNodeCount"]).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("it always selects _id", () => {
|
|
43
|
+
const select: Record<string, unknown> =
|
|
44
|
+
getSelectFromColumns<ProxmoxCluster>({
|
|
45
|
+
columns: [],
|
|
46
|
+
model: new ProxmoxCluster(),
|
|
47
|
+
}) as Record<string, unknown>;
|
|
48
|
+
|
|
49
|
+
expect(select["_id"]).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("it merges fields across multiple columns", () => {
|
|
53
|
+
const columns: Columns<ProxmoxCluster> = [
|
|
54
|
+
{
|
|
55
|
+
field: { name: true },
|
|
56
|
+
title: "Name",
|
|
57
|
+
type: FieldType.Text,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
field: { guestCount: true, storageCount: true },
|
|
61
|
+
title: "Inventory",
|
|
62
|
+
type: FieldType.Element,
|
|
63
|
+
},
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
const select: Record<string, unknown> =
|
|
67
|
+
getSelectFromColumns<ProxmoxCluster>({
|
|
68
|
+
columns: columns,
|
|
69
|
+
model: new ProxmoxCluster(),
|
|
70
|
+
}) as Record<string, unknown>;
|
|
71
|
+
|
|
72
|
+
expect(select["name"]).toBe(true);
|
|
73
|
+
expect(select["guestCount"]).toBe(true);
|
|
74
|
+
expect(select["storageCount"]).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("it skips columns that declare no field", () => {
|
|
78
|
+
const columns: Columns<ProxmoxCluster> = [
|
|
79
|
+
{
|
|
80
|
+
field: undefined as never,
|
|
81
|
+
title: "Actions",
|
|
82
|
+
type: FieldType.Element,
|
|
83
|
+
},
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
const select: Record<string, unknown> =
|
|
87
|
+
getSelectFromColumns<ProxmoxCluster>({
|
|
88
|
+
columns: columns,
|
|
89
|
+
model: new ProxmoxCluster(),
|
|
90
|
+
}) as Record<string, unknown>;
|
|
91
|
+
|
|
92
|
+
expect(Object.keys(select)).toEqual(["_id"]);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("it throws when the primary field is not a column on the model", () => {
|
|
96
|
+
const columns: Columns<ProxmoxCluster> = [
|
|
97
|
+
{
|
|
98
|
+
field: { notARealColumn: true } as never,
|
|
99
|
+
title: "Nope",
|
|
100
|
+
type: FieldType.Text,
|
|
101
|
+
},
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
expect(() => {
|
|
105
|
+
return getSelectFromColumns<ProxmoxCluster>({
|
|
106
|
+
columns: columns,
|
|
107
|
+
model: new ProxmoxCluster(),
|
|
108
|
+
});
|
|
109
|
+
}).toThrow(BadDataException);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("it ignores a non-column secondary field rather than blanking the page", () => {
|
|
113
|
+
const columns: Columns<ProxmoxCluster> = [
|
|
114
|
+
{
|
|
115
|
+
field: { nodeCount: true, notARealColumn: true } as never,
|
|
116
|
+
title: "Nodes",
|
|
117
|
+
type: FieldType.Element,
|
|
118
|
+
},
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
const consoleError: jest.SpyInstance = jest
|
|
122
|
+
.spyOn(console, "error")
|
|
123
|
+
.mockImplementation(() => {});
|
|
124
|
+
|
|
125
|
+
const select: Record<string, unknown> =
|
|
126
|
+
getSelectFromColumns<ProxmoxCluster>({
|
|
127
|
+
columns: columns,
|
|
128
|
+
model: new ProxmoxCluster(),
|
|
129
|
+
}) as Record<string, unknown>;
|
|
130
|
+
|
|
131
|
+
expect(select["nodeCount"]).toBe(true);
|
|
132
|
+
expect(select["notARealColumn"]).toBeUndefined();
|
|
133
|
+
// It must not drop the field silently — that is how #2756 hid.
|
|
134
|
+
expect(consoleError).toHaveBeenCalled();
|
|
135
|
+
|
|
136
|
+
consoleError.mockRestore();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("it drops a secondary field the user cannot read, so the API does not reject the whole request", () => {
|
|
140
|
+
/*
|
|
141
|
+
* checkSelectPermission throws NotAuthorizedException for the entire
|
|
142
|
+
* request when the select names an unreadable column, so widening the
|
|
143
|
+
* select must never add one.
|
|
144
|
+
*/
|
|
145
|
+
const columns: Columns<ProxmoxCluster> = [
|
|
146
|
+
{
|
|
147
|
+
field: {
|
|
148
|
+
nodeCount: true,
|
|
149
|
+
onlineNodeCount: true,
|
|
150
|
+
},
|
|
151
|
+
title: "Nodes",
|
|
152
|
+
type: FieldType.Element,
|
|
153
|
+
},
|
|
154
|
+
];
|
|
155
|
+
|
|
156
|
+
const select: Record<string, unknown> =
|
|
157
|
+
getSelectFromColumns<ProxmoxCluster>({
|
|
158
|
+
columns: columns,
|
|
159
|
+
model: new ProxmoxCluster(),
|
|
160
|
+
hasPermissionToReadField: (field: string): boolean => {
|
|
161
|
+
return field !== "onlineNodeCount";
|
|
162
|
+
},
|
|
163
|
+
}) as Record<string, unknown>;
|
|
164
|
+
|
|
165
|
+
expect(select["nodeCount"]).toBe(true);
|
|
166
|
+
expect(select["onlineNodeCount"]).toBeUndefined();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("it keeps the primary field even when the permission callback rejects it, preserving prior behaviour", () => {
|
|
170
|
+
const columns: Columns<ProxmoxCluster> = [
|
|
171
|
+
{
|
|
172
|
+
field: { nodeCount: true, onlineNodeCount: true },
|
|
173
|
+
title: "Nodes",
|
|
174
|
+
type: FieldType.Element,
|
|
175
|
+
},
|
|
176
|
+
];
|
|
177
|
+
|
|
178
|
+
const select: Record<string, unknown> =
|
|
179
|
+
getSelectFromColumns<ProxmoxCluster>({
|
|
180
|
+
columns: columns,
|
|
181
|
+
model: new ProxmoxCluster(),
|
|
182
|
+
hasPermissionToReadField: (): boolean => {
|
|
183
|
+
return false;
|
|
184
|
+
},
|
|
185
|
+
}) as Record<string, unknown>;
|
|
186
|
+
|
|
187
|
+
expect(select["nodeCount"]).toBe(true);
|
|
188
|
+
expect(select["onlineNodeCount"]).toBeUndefined();
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe("getRelationSelectFromColumns", () => {
|
|
193
|
+
test("it selects every relation a column declares, not just the first", () => {
|
|
194
|
+
const columns: Columns<Alert> = [
|
|
195
|
+
{
|
|
196
|
+
field: {
|
|
197
|
+
monitor: { name: true, _id: true },
|
|
198
|
+
labels: { name: true, color: true },
|
|
199
|
+
},
|
|
200
|
+
title: "Resource",
|
|
201
|
+
type: FieldType.Element,
|
|
202
|
+
},
|
|
203
|
+
];
|
|
204
|
+
|
|
205
|
+
const relationSelect: Record<string, unknown> =
|
|
206
|
+
getRelationSelectFromColumns<Alert>({
|
|
207
|
+
columns: columns,
|
|
208
|
+
model: new Alert(),
|
|
209
|
+
}) as Record<string, unknown>;
|
|
210
|
+
|
|
211
|
+
expect(relationSelect["monitor"]).toEqual({ name: true, _id: true });
|
|
212
|
+
expect(relationSelect["labels"]).toEqual({ name: true, color: true });
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("it leaves non-entity fields out of the relation select", () => {
|
|
216
|
+
const columns: Columns<ProxmoxCluster> = [
|
|
217
|
+
{
|
|
218
|
+
field: {
|
|
219
|
+
nodeCount: true,
|
|
220
|
+
onlineNodeCount: true,
|
|
221
|
+
},
|
|
222
|
+
title: "Nodes",
|
|
223
|
+
type: FieldType.Element,
|
|
224
|
+
},
|
|
225
|
+
];
|
|
226
|
+
|
|
227
|
+
const relationSelect: Record<string, unknown> =
|
|
228
|
+
getRelationSelectFromColumns<ProxmoxCluster>({
|
|
229
|
+
columns: columns,
|
|
230
|
+
model: new ProxmoxCluster(),
|
|
231
|
+
}) as Record<string, unknown>;
|
|
232
|
+
|
|
233
|
+
expect(relationSelect).toEqual({});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test("it merges the same relation declared by two columns", () => {
|
|
237
|
+
const columns: Columns<Alert> = [
|
|
238
|
+
{
|
|
239
|
+
field: { monitor: { name: true } },
|
|
240
|
+
title: "Monitor",
|
|
241
|
+
type: FieldType.Element,
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
field: { monitor: { _id: true } },
|
|
245
|
+
title: "Monitor Id",
|
|
246
|
+
type: FieldType.Element,
|
|
247
|
+
},
|
|
248
|
+
];
|
|
249
|
+
|
|
250
|
+
const relationSelect: Record<string, unknown> =
|
|
251
|
+
getRelationSelectFromColumns<Alert>({
|
|
252
|
+
columns: columns,
|
|
253
|
+
model: new Alert(),
|
|
254
|
+
}) as Record<string, unknown>;
|
|
255
|
+
|
|
256
|
+
expect(relationSelect["monitor"]).toEqual({ name: true, _id: true });
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
});
|
|
@@ -83,6 +83,12 @@ export interface CodeFixTaskContext {
|
|
|
83
83
|
performanceFindings?: Array<PerformanceFinding> | undefined;
|
|
84
84
|
// code.* attribute locations for stack-trace-style repo resolution.
|
|
85
85
|
codeLocations?: Array<PerformanceCodeLocation> | undefined;
|
|
86
|
+
/*
|
|
87
|
+
* ImproveLogging / ImproveTracing: the telemetry service whose
|
|
88
|
+
* instrumentation the recipe improves (serviceName above carries its
|
|
89
|
+
* name for repository resolution and PR wording).
|
|
90
|
+
*/
|
|
91
|
+
telemetryServiceId?: string | undefined;
|
|
86
92
|
}
|
|
87
93
|
|
|
88
94
|
export default CodeFixTaskContext;
|
|
@@ -12,6 +12,16 @@ enum CodeFixTaskType {
|
|
|
12
12
|
FixException = "FixException",
|
|
13
13
|
// Write a failing-then-passing regression test that reproduces the exception.
|
|
14
14
|
WriteRegressionTest = "WriteRegressionTest",
|
|
15
|
+
/*
|
|
16
|
+
* Improve how the code HANDLES and REPORTS this exception without
|
|
17
|
+
* changing business behavior: parameterize messages that interpolate
|
|
18
|
+
* user data (PII out of error text, stable fingerprints), validate bad
|
|
19
|
+
* input earlier with an actionable error, and mark expected/operational
|
|
20
|
+
* errors as handled in telemetry. The recipe for exceptions the triage
|
|
21
|
+
* classified as user errors or expected denials — where "fix the bug"
|
|
22
|
+
* is the wrong instruction because there is no bug.
|
|
23
|
+
*/
|
|
24
|
+
ImproveExceptionHandling = "ImproveExceptionHandling",
|
|
15
25
|
/*
|
|
16
26
|
* Add the observability an INCONCLUSIVE AI investigation was missing
|
|
17
27
|
* (structured logs, spans, metric counters on the implicated code paths).
|
|
@@ -32,6 +42,27 @@ enum CodeFixTaskType {
|
|
|
32
42
|
* Common/Server/Utils/AI/SRE/FixPerformanceTaskTrigger.ts).
|
|
33
43
|
*/
|
|
34
44
|
FixPerformance = "FixPerformance",
|
|
45
|
+
/*
|
|
46
|
+
* Improve a SERVICE's logging hygiene: structured/parameterized log
|
|
47
|
+
* messages (no user data interpolated into log text), correct severity
|
|
48
|
+
* levels, trace correlation, proper exception recording instead of raw
|
|
49
|
+
* stack dumps in log bodies, and less noise. Human-triggered from the
|
|
50
|
+
* service's Logs page (POST /ai-investigation/
|
|
51
|
+
* create-telemetry-improvement-task); its context (service id + name) is
|
|
52
|
+
* captured into AIRun.taskContext at trigger time. Instrumentation only —
|
|
53
|
+
* never a behavior change.
|
|
54
|
+
*/
|
|
55
|
+
ImproveLogging = "ImproveLogging",
|
|
56
|
+
/*
|
|
57
|
+
* Improve a SERVICE's tracing instrumentation: spans on uninstrumented
|
|
58
|
+
* entry points and significant operations, low-cardinality span names,
|
|
59
|
+
* exceptions recorded on spans with correct status/escaped semantics,
|
|
60
|
+
* code.* and semantic-convention attributes, context propagation across
|
|
61
|
+
* async boundaries. Human-triggered from the service's Traces page (same
|
|
62
|
+
* endpoint as ImproveLogging); context captured into AIRun.taskContext.
|
|
63
|
+
* Instrumentation only — never a behavior change.
|
|
64
|
+
*/
|
|
65
|
+
ImproveTracing = "ImproveTracing",
|
|
35
66
|
/*
|
|
36
67
|
* Fix the root cause a completed AI investigation identified. Human-
|
|
37
68
|
* triggered from the investigation panel (POST /ai-investigation/
|
|
@@ -77,7 +108,11 @@ export class CodeFixTaskTypeHelper {
|
|
|
77
108
|
* (POST /ai-investigation/create-performance-fix-task).
|
|
78
109
|
*/
|
|
79
110
|
public static getUserTriggerableTaskTypes(): Array<CodeFixTaskType> {
|
|
80
|
-
return [
|
|
111
|
+
return [
|
|
112
|
+
CodeFixTaskType.FixException,
|
|
113
|
+
CodeFixTaskType.WriteRegressionTest,
|
|
114
|
+
CodeFixTaskType.ImproveExceptionHandling,
|
|
115
|
+
];
|
|
81
116
|
}
|
|
82
117
|
|
|
83
118
|
public static isUserTriggerable(taskType: CodeFixTaskType): boolean {
|
|
@@ -97,6 +132,8 @@ export class CodeFixTaskTypeHelper {
|
|
|
97
132
|
case CodeFixTaskType.FixFromIncident:
|
|
98
133
|
return CodeFixContextKind.IncidentOrAlertSubject;
|
|
99
134
|
case CodeFixTaskType.FixPerformance:
|
|
135
|
+
case CodeFixTaskType.ImproveLogging:
|
|
136
|
+
case CodeFixTaskType.ImproveTracing:
|
|
100
137
|
return CodeFixContextKind.TaskContext;
|
|
101
138
|
default:
|
|
102
139
|
return CodeFixContextKind.TelemetryException;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* AI triage verdict for a telemetry exception group. Decides how the
|
|
3
|
+
* automatic fix lane treats the group:
|
|
4
|
+
*
|
|
5
|
+
* - CodeFault: a defect in the monitored code — the only class the
|
|
6
|
+
* automatic lane opens fix pull requests for.
|
|
7
|
+
* - UserError: expected consequence of invalid end-user input (bad
|
|
8
|
+
* parameters, malformed values). The right change, if any, is earlier
|
|
9
|
+
* validation and clearer error UX — routed to a human, never auto-fixed.
|
|
10
|
+
* - ExpectedDenial: an intentional check doing its job (auth failure,
|
|
11
|
+
* plan/paywall denial, scanner/fuzzer probe tripping validation).
|
|
12
|
+
* Never auto-fixed; optionally auto-archived.
|
|
13
|
+
* - Infrastructure: environmental conditions (timeouts, connection
|
|
14
|
+
* resets, resource exhaustion) where a code "fix" is usually tuning —
|
|
15
|
+
* routed to a human.
|
|
16
|
+
* - Unknown: triage could not decide — treated conservatively (no
|
|
17
|
+
* automatic fix).
|
|
18
|
+
*/
|
|
19
|
+
enum ExceptionAIClassification {
|
|
20
|
+
CodeFault = "code-fault",
|
|
21
|
+
UserError = "user-error",
|
|
22
|
+
ExpectedDenial = "expected-denial",
|
|
23
|
+
Infrastructure = "infrastructure",
|
|
24
|
+
Unknown = "unknown",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export default ExceptionAIClassification;
|
|
@@ -4,6 +4,13 @@ enum LogScrubPatternType {
|
|
|
4
4
|
SSN = "ssn",
|
|
5
5
|
PhoneNumber = "phoneNumber",
|
|
6
6
|
IPAddress = "ipAddress",
|
|
7
|
+
/*
|
|
8
|
+
* Key-targeted: scrubs the WHOLE value of any attribute whose KEY looks
|
|
9
|
+
* sensitive (password, token, apiKey, authorization, cookie, ...) —
|
|
10
|
+
* catches secrets whose values match no shape-based pattern. Applies to
|
|
11
|
+
* attributes only; body text has no keys to match.
|
|
12
|
+
*/
|
|
13
|
+
SensitiveKeys = "sensitiveKeys",
|
|
7
14
|
Custom = "custom",
|
|
8
15
|
}
|
|
9
16
|
|
|
@@ -4,6 +4,13 @@ enum TraceScrubPatternType {
|
|
|
4
4
|
SSN = "ssn",
|
|
5
5
|
PhoneNumber = "phoneNumber",
|
|
6
6
|
IPAddress = "ipAddress",
|
|
7
|
+
/*
|
|
8
|
+
* Key-targeted: scrubs the WHOLE value of any attribute whose KEY looks
|
|
9
|
+
* sensitive (password, token, apiKey, authorization, cookie, ...) —
|
|
10
|
+
* catches secrets whose values match no shape-based pattern. Applies to
|
|
11
|
+
* span/event attributes only; span names have no keys to match.
|
|
12
|
+
*/
|
|
13
|
+
SensitiveKeys = "sensitiveKeys",
|
|
7
14
|
Custom = "custom",
|
|
8
15
|
}
|
|
9
16
|
|
|
@@ -53,6 +53,10 @@ import TableColumn from "../Table/Types/Column";
|
|
|
53
53
|
import FieldType from "../Types/FieldType";
|
|
54
54
|
import ModelTableColumn from "./Column";
|
|
55
55
|
import Columns from "./Columns";
|
|
56
|
+
import {
|
|
57
|
+
getRelationSelectFromColumns,
|
|
58
|
+
getSelectFromColumns,
|
|
59
|
+
} from "./SelectFromColumns";
|
|
56
60
|
import AnalyticsBaseModel, {
|
|
57
61
|
AnalyticsBaseModelType,
|
|
58
62
|
} from "../../../Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel";
|
|
@@ -664,33 +668,10 @@ const BaseModelTable: <TBaseModel extends BaseModel | AnalyticsBaseModel>(
|
|
|
664
668
|
|
|
665
669
|
const getRelationSelect: GetRelationSelectFunction =
|
|
666
670
|
(): Select<TBaseModel> => {
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
? (Object.keys(column.field)[0] as string)
|
|
672
|
-
: null;
|
|
673
|
-
|
|
674
|
-
if (key && model.isFileColumn(key)) {
|
|
675
|
-
(relationSelect as JSONObject)[key] = {
|
|
676
|
-
file: true,
|
|
677
|
-
_id: true,
|
|
678
|
-
fileType: true,
|
|
679
|
-
name: true,
|
|
680
|
-
};
|
|
681
|
-
} else if (key && model.isEntityColumn(key)) {
|
|
682
|
-
if (!(relationSelect as JSONObject)[key]) {
|
|
683
|
-
(relationSelect as JSONObject)[key] = {};
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
(relationSelect as JSONObject)[key] = {
|
|
687
|
-
...((relationSelect as JSONObject)[key] as JSONObject),
|
|
688
|
-
...(column.field as any)[key],
|
|
689
|
-
};
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
return relationSelect;
|
|
671
|
+
return getRelationSelectFromColumns<TBaseModel>({
|
|
672
|
+
columns: props.columns || [],
|
|
673
|
+
model: model,
|
|
674
|
+
});
|
|
694
675
|
};
|
|
695
676
|
|
|
696
677
|
type DeleteItemFunction = (item: TBaseModel) => Promise<void>;
|
|
@@ -1226,25 +1207,13 @@ const BaseModelTable: <TBaseModel extends BaseModel | AnalyticsBaseModel>(
|
|
|
1226
1207
|
type GetSelectFunction = () => Select<TBaseModel>;
|
|
1227
1208
|
|
|
1228
1209
|
const getSelect: GetSelectFunction = (): Select<TBaseModel> => {
|
|
1229
|
-
const selectFields: Select<TBaseModel> = {
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
: null;
|
|
1237
|
-
|
|
1238
|
-
if (key) {
|
|
1239
|
-
if (model.hasColumn(key)) {
|
|
1240
|
-
(selectFields as Dictionary<boolean>)[key] = true;
|
|
1241
|
-
} else {
|
|
1242
|
-
throw new BadDataException(
|
|
1243
|
-
`${key} column not found on ${model.singularName}`,
|
|
1244
|
-
);
|
|
1245
|
-
}
|
|
1246
|
-
}
|
|
1247
|
-
}
|
|
1210
|
+
const selectFields: Select<TBaseModel> = getSelectFromColumns<TBaseModel>({
|
|
1211
|
+
columns: props.columns || [],
|
|
1212
|
+
model: model,
|
|
1213
|
+
hasPermissionToReadField: (field: string): boolean => {
|
|
1214
|
+
return hasPermissionToReadField(field as keyof TBaseModel);
|
|
1215
|
+
},
|
|
1216
|
+
});
|
|
1248
1217
|
|
|
1249
1218
|
const selectMoreFields: Array<keyof TBaseModel> = props.selectMoreFields
|
|
1250
1219
|
? (Object.keys(props.selectMoreFields) as Array<keyof TBaseModel>)
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import Columns from "./Columns";
|
|
2
|
+
import AnalyticsBaseModel from "../../../Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel";
|
|
3
|
+
import BaseModel from "../../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
|
|
4
|
+
import Select from "../../../Types/BaseDatabase/Select";
|
|
5
|
+
import Dictionary from "../../../Types/Dictionary";
|
|
6
|
+
import BadDataException from "../../../Types/Exception/BadDataException";
|
|
7
|
+
import { JSONObject } from "../../../Types/JSON";
|
|
8
|
+
import { Logger } from "../../Utils/Logger";
|
|
9
|
+
|
|
10
|
+
/*
|
|
11
|
+
* Builds the API `select` for a ModelTable from its column definitions.
|
|
12
|
+
*
|
|
13
|
+
* A column may declare MORE THAN ONE field when its cell renders a value
|
|
14
|
+
* composed from several columns — e.g. a "Nodes" cell that renders
|
|
15
|
+
* `{onlineNodeCount}/{nodeCount} online` declares
|
|
16
|
+
* `field: { nodeCount: true, onlineNodeCount: true }`.
|
|
17
|
+
*
|
|
18
|
+
* These used to select only the FIRST key, which left every other declared
|
|
19
|
+
* field `undefined` on the fetched row, so such cells silently rendered
|
|
20
|
+
* their zero/empty fallback — a fully online Proxmox cluster displayed
|
|
21
|
+
* "0/3 online" (OneUptime/oneuptime#2756). ModelDetail has always selected
|
|
22
|
+
* every declared key; these helpers keep ModelTable in sync with it.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export function getSelectFromColumns<
|
|
26
|
+
TBaseModel extends BaseModel | AnalyticsBaseModel,
|
|
27
|
+
>(data: {
|
|
28
|
+
columns: Columns<TBaseModel>;
|
|
29
|
+
model: TBaseModel;
|
|
30
|
+
/*
|
|
31
|
+
* Secondary fields are gated on this. The API rejects the WHOLE request
|
|
32
|
+
* with NotAuthorizedException when the select names a column the caller
|
|
33
|
+
* cannot read (Common/Server/Types/Database/Permissions/SelectPermission
|
|
34
|
+
* .checkSelectPermission), so a secondary field the user has no read
|
|
35
|
+
* permission on must be dropped rather than blank the table for them.
|
|
36
|
+
* Primary fields are left exactly as they always were.
|
|
37
|
+
*/
|
|
38
|
+
hasPermissionToReadField?: ((field: string) => boolean) | undefined;
|
|
39
|
+
}): Select<TBaseModel> {
|
|
40
|
+
const selectFields: Select<TBaseModel> = {
|
|
41
|
+
_id: true,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
for (const column of data.columns) {
|
|
45
|
+
if (!column.field) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const keys: Array<string> = Object.keys(column.field);
|
|
50
|
+
|
|
51
|
+
for (const key of keys) {
|
|
52
|
+
const isPrimaryField: boolean = key === keys[0];
|
|
53
|
+
|
|
54
|
+
if (!data.model.hasColumn(key)) {
|
|
55
|
+
if (isPrimaryField) {
|
|
56
|
+
/*
|
|
57
|
+
* Only the primary field is a hard error — it is also the key
|
|
58
|
+
* used for sorting and for the default cell renderer, so a bad
|
|
59
|
+
* one is a genuine mistake.
|
|
60
|
+
*/
|
|
61
|
+
throw new BadDataException(
|
|
62
|
+
`${key} column not found on ${data.model.singularName}`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/*
|
|
67
|
+
* A stray secondary key degrades to "not selected" rather than
|
|
68
|
+
* throwing, because throwing here blanks the entire page. Log it:
|
|
69
|
+
* silently dropping it is what produced #2756 in the first place.
|
|
70
|
+
*/
|
|
71
|
+
Logger.error(
|
|
72
|
+
`ModelTable column "${column.title}" on ${data.model.singularName} declares field "${key}", which is not a column on the model. It will not be fetched, so the cell will render its empty value.`,
|
|
73
|
+
);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (
|
|
78
|
+
!isPrimaryField &&
|
|
79
|
+
data.hasPermissionToReadField &&
|
|
80
|
+
!data.hasPermissionToReadField(key)
|
|
81
|
+
) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
(selectFields as Dictionary<boolean>)[key] = true;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return selectFields;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function getRelationSelectFromColumns<
|
|
93
|
+
TBaseModel extends BaseModel | AnalyticsBaseModel,
|
|
94
|
+
>(data: {
|
|
95
|
+
columns: Columns<TBaseModel>;
|
|
96
|
+
model: TBaseModel;
|
|
97
|
+
}): Select<TBaseModel> {
|
|
98
|
+
const relationSelect: Select<TBaseModel> = {};
|
|
99
|
+
|
|
100
|
+
for (const column of data.columns) {
|
|
101
|
+
if (!column.field) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/*
|
|
106
|
+
* Same multi-field contract as getSelectFromColumns: one cell can
|
|
107
|
+
* render several relations (e.g. an alert "Resource" cell spanning
|
|
108
|
+
* hosts / kubernetesClusters / dockerHosts / podmanHosts / services).
|
|
109
|
+
*/
|
|
110
|
+
for (const key of Object.keys(column.field)) {
|
|
111
|
+
if (data.model.isFileColumn(key)) {
|
|
112
|
+
(relationSelect as JSONObject)[key] = {
|
|
113
|
+
file: true,
|
|
114
|
+
_id: true,
|
|
115
|
+
fileType: true,
|
|
116
|
+
name: true,
|
|
117
|
+
};
|
|
118
|
+
} else if (data.model.isEntityColumn(key)) {
|
|
119
|
+
(relationSelect as JSONObject)[key] = {
|
|
120
|
+
...(((relationSelect as JSONObject)[key] as JSONObject) || {}),
|
|
121
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
122
|
+
...((column.field as any)[key] as JSONObject),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return relationSelect;
|
|
129
|
+
}
|
|
@@ -48,6 +48,13 @@ let AIInsight = class AIInsight extends BaseModel {
|
|
|
48
48
|
this.insightType = undefined;
|
|
49
49
|
this.status = undefined;
|
|
50
50
|
this.severity = undefined;
|
|
51
|
+
/*
|
|
52
|
+
* Triage verdict (ExceptionAIClassification values) written by the
|
|
53
|
+
* insight triage runner. Null until triage completes. The automatic
|
|
54
|
+
* fix lane only opens pull requests for insights triaged as
|
|
55
|
+
* code-fault — see InsightScanner / InsightFixRouting.
|
|
56
|
+
*/
|
|
57
|
+
this.classification = undefined;
|
|
51
58
|
/*
|
|
52
59
|
* The detector's stable dedupe key for this finding (e.g.
|
|
53
60
|
* "new-exception:<telemetryExceptionId>"). The scanner refreshes the
|
|
@@ -225,6 +232,30 @@ __decorate([
|
|
|
225
232
|
}),
|
|
226
233
|
__metadata("design:type", String)
|
|
227
234
|
], AIInsight.prototype, "severity", void 0);
|
|
235
|
+
__decorate([
|
|
236
|
+
ColumnAccessControl({
|
|
237
|
+
create: [],
|
|
238
|
+
read: [
|
|
239
|
+
Permission.ProjectOwner,
|
|
240
|
+
Permission.ProjectAdmin,
|
|
241
|
+
Permission.ProjectMember,
|
|
242
|
+
],
|
|
243
|
+
update: [],
|
|
244
|
+
}),
|
|
245
|
+
TableColumn({
|
|
246
|
+
required: false,
|
|
247
|
+
type: TableColumnType.ShortText,
|
|
248
|
+
title: "Classification",
|
|
249
|
+
description: "AI triage verdict: code-fault, user-error, expected-denial, infrastructure or unknown. Automatic fix pull requests are only opened for code-fault.",
|
|
250
|
+
canReadOnRelationQuery: true,
|
|
251
|
+
}),
|
|
252
|
+
Column({
|
|
253
|
+
nullable: true,
|
|
254
|
+
type: ColumnType.ShortText,
|
|
255
|
+
length: ColumnLength.ShortText,
|
|
256
|
+
}),
|
|
257
|
+
__metadata("design:type", String)
|
|
258
|
+
], AIInsight.prototype, "classification", void 0);
|
|
228
259
|
__decorate([
|
|
229
260
|
ColumnAccessControl({
|
|
230
261
|
create: [],
|