@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
|
@@ -7,12 +7,18 @@ import InstrumentationTaskTrigger from "../../../../../Server/Utils/AI/SRE/Instr
|
|
|
7
7
|
import { ConfidenceSignal } from "../../../../../Server/Utils/AI/SRE/ConfidenceSignal";
|
|
8
8
|
import { ObservabilityAssistantResult } from "../../../../../Server/Utils/AI/Chat/ObservabilityAssistant";
|
|
9
9
|
import AIInsightService from "../../../../../Server/Services/AIInsightService";
|
|
10
|
+
import ProjectService from "../../../../../Server/Services/ProjectService";
|
|
11
|
+
import TelemetryExceptionService from "../../../../../Server/Services/TelemetryExceptionService";
|
|
10
12
|
import IncidentFeedService from "../../../../../Server/Services/IncidentFeedService";
|
|
11
13
|
import AlertFeedService from "../../../../../Server/Services/AlertFeedService";
|
|
12
14
|
import { AI_INSIGHT_TRIAGE_FEATURE } from "../../../../../Server/Services/AIService";
|
|
15
|
+
import InsightFixRouting from "../../../../../Server/Utils/AI/SRE/Insights/FixRouting";
|
|
13
16
|
import AIInsight from "../../../../../Models/DatabaseModels/AIInsight";
|
|
17
|
+
import Project from "../../../../../Models/DatabaseModels/Project";
|
|
14
18
|
import AIInsightType from "../../../../../Types/AI/AIInsightType";
|
|
15
19
|
import AIInsightSeverity from "../../../../../Types/AI/AIInsightSeverity";
|
|
20
|
+
import AIInsightStatus from "../../../../../Types/AI/AIInsightStatus";
|
|
21
|
+
import ExceptionAIClassification from "../../../../../Types/AI/ExceptionAIClassification";
|
|
16
22
|
import ObjectID from "../../../../../Types/ObjectID";
|
|
17
23
|
import { describe, expect, test, afterEach } from "@jest/globals";
|
|
18
24
|
|
|
@@ -34,9 +40,10 @@ const aiRunId: ObjectID = ObjectID.generate();
|
|
|
34
40
|
const projectId: ObjectID = ObjectID.generate();
|
|
35
41
|
const sentinelInsightId: ObjectID = ObjectID.generate();
|
|
36
42
|
|
|
37
|
-
function makeInsight(): AIInsight {
|
|
43
|
+
function makeInsight(overrides: Partial<AIInsight> = {}): AIInsight {
|
|
38
44
|
return {
|
|
39
45
|
id: sentinelInsightId,
|
|
46
|
+
projectId,
|
|
40
47
|
insightType: AIInsightType.ExceptionSpike,
|
|
41
48
|
severity: AIInsightSeverity.High,
|
|
42
49
|
title: "Spike: NullPointerException in checkout",
|
|
@@ -50,9 +57,34 @@ function makeInsight(): AIInsight {
|
|
|
50
57
|
spikeMultiplier: 60,
|
|
51
58
|
},
|
|
52
59
|
},
|
|
60
|
+
...overrides,
|
|
53
61
|
} as unknown as AIInsight;
|
|
54
62
|
}
|
|
55
63
|
|
|
64
|
+
/*
|
|
65
|
+
* Harness for the verdict-driven follow-through: the engine is mocked to
|
|
66
|
+
* immediately drive postAnalysis with the given analysis text, exactly the
|
|
67
|
+
* way the real engine hands over its result.
|
|
68
|
+
*/
|
|
69
|
+
function driveTriageWithAnalysis(analysisMarkdown: string): void {
|
|
70
|
+
jest
|
|
71
|
+
.spyOn(AIInvestigationEngine, "executeRun")
|
|
72
|
+
.mockImplementation(
|
|
73
|
+
async (data: {
|
|
74
|
+
aiRunId: ObjectID;
|
|
75
|
+
projectId: ObjectID;
|
|
76
|
+
attemptCount: number;
|
|
77
|
+
request: InvestigationRequest;
|
|
78
|
+
}): Promise<void> => {
|
|
79
|
+
await data.request.postAnalysis({
|
|
80
|
+
analysisMarkdown,
|
|
81
|
+
confidence: makeConfidence(),
|
|
82
|
+
result: {} as unknown as ObservabilityAssistantResult,
|
|
83
|
+
});
|
|
84
|
+
},
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
56
88
|
function makeConfidence(): ConfidenceSignal {
|
|
57
89
|
return { confident: true, source: "classification" };
|
|
58
90
|
}
|
|
@@ -218,4 +250,248 @@ describe("InsightTriageRunner.executeTriage", () => {
|
|
|
218
250
|
expect(alertFeed).not.toHaveBeenCalled();
|
|
219
251
|
expect(instrumentationTrigger).not.toHaveBeenCalled();
|
|
220
252
|
});
|
|
253
|
+
|
|
254
|
+
test("a code-fault verdict stamps the exception, routes the fix post-triage, and flips the insight to FixOpened", async () => {
|
|
255
|
+
const telemetryExceptionId: ObjectID = ObjectID.generate();
|
|
256
|
+
jest
|
|
257
|
+
.spyOn(AIInsightService, "findOneById")
|
|
258
|
+
.mockResolvedValue(makeInsight({ telemetryExceptionId }));
|
|
259
|
+
const persistInsight: jest.SpyInstance = jest
|
|
260
|
+
.spyOn(AIInsightService, "updateOneById")
|
|
261
|
+
.mockResolvedValue(undefined);
|
|
262
|
+
const flipStatus: jest.SpyInstance = jest
|
|
263
|
+
.spyOn(AIInsightService, "updateOneBy")
|
|
264
|
+
.mockResolvedValue(undefined as never);
|
|
265
|
+
const stampException: jest.SpyInstance = jest
|
|
266
|
+
.spyOn(TelemetryExceptionService, "updateOneById")
|
|
267
|
+
.mockResolvedValue(undefined as never);
|
|
268
|
+
jest.spyOn(ProjectService, "findOneById").mockResolvedValue({
|
|
269
|
+
id: projectId,
|
|
270
|
+
enableAi: true,
|
|
271
|
+
enableInsightFixTasks: true,
|
|
272
|
+
autoArchiveNonActionableExceptions: false,
|
|
273
|
+
} as unknown as Project);
|
|
274
|
+
const fixAiRunId: ObjectID = ObjectID.generate();
|
|
275
|
+
const routeFix: jest.SpyInstance = jest
|
|
276
|
+
.spyOn(InsightFixRouting, "routeInsightFix")
|
|
277
|
+
.mockResolvedValue({ fixAiRunId });
|
|
278
|
+
|
|
279
|
+
driveTriageWithAnalysis(
|
|
280
|
+
"Root cause: null deref in checkout.\n\nClassification: code-fault",
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
await InsightTriageRunner.executeTriage({
|
|
284
|
+
aiRunId,
|
|
285
|
+
projectId,
|
|
286
|
+
sentinelInsightId,
|
|
287
|
+
attemptCount: 1,
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// The verdict lands on the insight row alongside the summary.
|
|
291
|
+
expect(persistInsight).toHaveBeenCalledWith(
|
|
292
|
+
expect.objectContaining({
|
|
293
|
+
id: sentinelInsightId,
|
|
294
|
+
data: expect.objectContaining({
|
|
295
|
+
classification: ExceptionAIClassification.CodeFault,
|
|
296
|
+
}),
|
|
297
|
+
}),
|
|
298
|
+
);
|
|
299
|
+
// ...and on the exception group row.
|
|
300
|
+
expect(stampException).toHaveBeenCalledWith(
|
|
301
|
+
expect.objectContaining({
|
|
302
|
+
id: telemetryExceptionId,
|
|
303
|
+
data: expect.objectContaining({
|
|
304
|
+
aiClassification: ExceptionAIClassification.CodeFault,
|
|
305
|
+
}),
|
|
306
|
+
}),
|
|
307
|
+
);
|
|
308
|
+
/*
|
|
309
|
+
* The fix decision happens HERE, post-verdict — and flips the status
|
|
310
|
+
* CONDITIONALLY: only a still-ActionRequired insight becomes FixOpened.
|
|
311
|
+
*/
|
|
312
|
+
expect(routeFix).toHaveBeenCalledTimes(1);
|
|
313
|
+
expect(flipStatus).toHaveBeenCalledWith(
|
|
314
|
+
expect.objectContaining({
|
|
315
|
+
query: expect.objectContaining({
|
|
316
|
+
status: AIInsightStatus.ActionRequired,
|
|
317
|
+
}),
|
|
318
|
+
data: expect.objectContaining({
|
|
319
|
+
status: AIInsightStatus.FixOpened,
|
|
320
|
+
fixAiRunId,
|
|
321
|
+
}),
|
|
322
|
+
}),
|
|
323
|
+
);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test("a human Dismissed/Resolved during triage blocks all automation — no fix, no archive, terminal status untouched", async () => {
|
|
327
|
+
const telemetryExceptionId: ObjectID = ObjectID.generate();
|
|
328
|
+
|
|
329
|
+
/*
|
|
330
|
+
* First read (context build) returns the snapshot; the SECOND read is
|
|
331
|
+
* actOnClassification's status re-check — the human dismissed the
|
|
332
|
+
* insight while the triage run executed.
|
|
333
|
+
*/
|
|
334
|
+
jest
|
|
335
|
+
.spyOn(AIInsightService, "findOneById")
|
|
336
|
+
.mockResolvedValueOnce(makeInsight({ telemetryExceptionId }))
|
|
337
|
+
.mockResolvedValue(
|
|
338
|
+
makeInsight({
|
|
339
|
+
telemetryExceptionId,
|
|
340
|
+
status: AIInsightStatus.Dismissed,
|
|
341
|
+
}),
|
|
342
|
+
);
|
|
343
|
+
const persistInsight: jest.SpyInstance = jest
|
|
344
|
+
.spyOn(AIInsightService, "updateOneById")
|
|
345
|
+
.mockResolvedValue(undefined);
|
|
346
|
+
const flipStatus: jest.SpyInstance = jest
|
|
347
|
+
.spyOn(AIInsightService, "updateOneBy")
|
|
348
|
+
.mockResolvedValue(undefined as never);
|
|
349
|
+
const exceptionWrites: jest.SpyInstance = jest
|
|
350
|
+
.spyOn(TelemetryExceptionService, "updateOneById")
|
|
351
|
+
.mockResolvedValue(undefined as never);
|
|
352
|
+
const routeFix: jest.SpyInstance = jest.spyOn(
|
|
353
|
+
InsightFixRouting,
|
|
354
|
+
"routeInsightFix",
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
driveTriageWithAnalysis(
|
|
358
|
+
"A clear null deref.\n\nClassification: code-fault",
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
await InsightTriageRunner.executeTriage({
|
|
362
|
+
aiRunId,
|
|
363
|
+
projectId,
|
|
364
|
+
sentinelInsightId,
|
|
365
|
+
attemptCount: 1,
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// The verdict still lands (summary + classification are metadata)...
|
|
369
|
+
expect(persistInsight).toHaveBeenCalledWith(
|
|
370
|
+
expect.objectContaining({
|
|
371
|
+
data: expect.objectContaining({
|
|
372
|
+
classification: ExceptionAIClassification.CodeFault,
|
|
373
|
+
}),
|
|
374
|
+
}),
|
|
375
|
+
);
|
|
376
|
+
// ...but no automation fires, and the terminal status is never touched.
|
|
377
|
+
expect(routeFix).not.toHaveBeenCalled();
|
|
378
|
+
expect(flipStatus).not.toHaveBeenCalled();
|
|
379
|
+
expect(exceptionWrites).toHaveBeenCalledTimes(1); // classification stamp only
|
|
380
|
+
expect(exceptionWrites).not.toHaveBeenCalledWith(
|
|
381
|
+
expect.objectContaining({
|
|
382
|
+
data: expect.objectContaining({ isArchived: true }),
|
|
383
|
+
}),
|
|
384
|
+
);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
test("a user-error verdict NEVER routes a fix", async () => {
|
|
388
|
+
const telemetryExceptionId: ObjectID = ObjectID.generate();
|
|
389
|
+
jest
|
|
390
|
+
.spyOn(AIInsightService, "findOneById")
|
|
391
|
+
.mockResolvedValue(makeInsight({ telemetryExceptionId }));
|
|
392
|
+
jest.spyOn(AIInsightService, "updateOneById").mockResolvedValue(undefined);
|
|
393
|
+
const exceptionWrites: jest.SpyInstance = jest
|
|
394
|
+
.spyOn(TelemetryExceptionService, "updateOneById")
|
|
395
|
+
.mockResolvedValue(undefined as never);
|
|
396
|
+
jest.spyOn(ProjectService, "findOneById").mockResolvedValue({
|
|
397
|
+
id: projectId,
|
|
398
|
+
enableAi: true,
|
|
399
|
+
enableInsightFixTasks: true,
|
|
400
|
+
autoArchiveNonActionableExceptions: false,
|
|
401
|
+
} as unknown as Project);
|
|
402
|
+
const routeFix: jest.SpyInstance = jest.spyOn(
|
|
403
|
+
InsightFixRouting,
|
|
404
|
+
"routeInsightFix",
|
|
405
|
+
);
|
|
406
|
+
|
|
407
|
+
driveTriageWithAnalysis(
|
|
408
|
+
"The uuid in the URL was garbage user input.\n\nClassification: user-error",
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
await InsightTriageRunner.executeTriage({
|
|
412
|
+
aiRunId,
|
|
413
|
+
projectId,
|
|
414
|
+
sentinelInsightId,
|
|
415
|
+
attemptCount: 1,
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
expect(routeFix).not.toHaveBeenCalled();
|
|
419
|
+
// The verdict still lands on the exception; no archive (not a denial).
|
|
420
|
+
expect(exceptionWrites).toHaveBeenCalledTimes(1);
|
|
421
|
+
expect(exceptionWrites).toHaveBeenCalledWith(
|
|
422
|
+
expect.objectContaining({
|
|
423
|
+
data: expect.objectContaining({
|
|
424
|
+
aiClassification: ExceptionAIClassification.UserError,
|
|
425
|
+
}),
|
|
426
|
+
}),
|
|
427
|
+
);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
test("an expected-denial verdict auto-archives the exception ONLY when the project opted in", async () => {
|
|
431
|
+
const telemetryExceptionId: ObjectID = ObjectID.generate();
|
|
432
|
+
jest
|
|
433
|
+
.spyOn(AIInsightService, "findOneById")
|
|
434
|
+
.mockResolvedValue(makeInsight({ telemetryExceptionId }));
|
|
435
|
+
jest.spyOn(AIInsightService, "updateOneById").mockResolvedValue(undefined);
|
|
436
|
+
const exceptionWrites: jest.SpyInstance = jest
|
|
437
|
+
.spyOn(TelemetryExceptionService, "updateOneById")
|
|
438
|
+
.mockResolvedValue(undefined as never);
|
|
439
|
+
const routeFix: jest.SpyInstance = jest.spyOn(
|
|
440
|
+
InsightFixRouting,
|
|
441
|
+
"routeInsightFix",
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
// Opted IN: the group gets archived.
|
|
445
|
+
jest.spyOn(ProjectService, "findOneById").mockResolvedValue({
|
|
446
|
+
id: projectId,
|
|
447
|
+
enableAi: true,
|
|
448
|
+
enableInsightFixTasks: true,
|
|
449
|
+
autoArchiveNonActionableExceptions: true,
|
|
450
|
+
} as unknown as Project);
|
|
451
|
+
|
|
452
|
+
driveTriageWithAnalysis(
|
|
453
|
+
"This is the paywall doing its job.\n\nClassification: expected-denial",
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
await InsightTriageRunner.executeTriage({
|
|
457
|
+
aiRunId,
|
|
458
|
+
projectId,
|
|
459
|
+
sentinelInsightId,
|
|
460
|
+
attemptCount: 1,
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
expect(routeFix).not.toHaveBeenCalled();
|
|
464
|
+
expect(exceptionWrites).toHaveBeenCalledWith(
|
|
465
|
+
expect.objectContaining({
|
|
466
|
+
id: telemetryExceptionId,
|
|
467
|
+
data: expect.objectContaining({
|
|
468
|
+
isArchived: true,
|
|
469
|
+
markedAsArchivedAt: expect.any(Date),
|
|
470
|
+
}),
|
|
471
|
+
}),
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
// Opted OUT: verdict stamp only, no archive write.
|
|
475
|
+
exceptionWrites.mockClear();
|
|
476
|
+
jest.spyOn(ProjectService, "findOneById").mockResolvedValue({
|
|
477
|
+
id: projectId,
|
|
478
|
+
enableAi: true,
|
|
479
|
+
enableInsightFixTasks: true,
|
|
480
|
+
autoArchiveNonActionableExceptions: false,
|
|
481
|
+
} as unknown as Project);
|
|
482
|
+
|
|
483
|
+
await InsightTriageRunner.executeTriage({
|
|
484
|
+
aiRunId,
|
|
485
|
+
projectId,
|
|
486
|
+
sentinelInsightId,
|
|
487
|
+
attemptCount: 1,
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
expect(exceptionWrites).toHaveBeenCalledTimes(1);
|
|
491
|
+
expect(exceptionWrites).not.toHaveBeenCalledWith(
|
|
492
|
+
expect.objectContaining({
|
|
493
|
+
data: expect.objectContaining({ isArchived: true }),
|
|
494
|
+
}),
|
|
495
|
+
);
|
|
496
|
+
});
|
|
221
497
|
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import {
|
|
2
|
+
normalizeExceptionText,
|
|
3
|
+
sanitizeExceptionMessage,
|
|
4
|
+
sanitizeStackTrace,
|
|
5
|
+
} from "../../../Server/Utils/Telemetry/ExceptionSanitizer";
|
|
6
|
+
import { describe, expect, test } from "@jest/globals";
|
|
7
|
+
|
|
8
|
+
/*
|
|
9
|
+
* The sanitizer feeds two very different consumers:
|
|
10
|
+
*
|
|
11
|
+
* - normalizeExceptionText is the FINGERPRINT normalizer (moved here from
|
|
12
|
+
* App's ExceptionUtil) — its replacement behavior must stay stable or
|
|
13
|
+
* every existing exception group re-fingerprints.
|
|
14
|
+
* - sanitizeExceptionMessage / sanitizeStackTrace guard the surfaces that
|
|
15
|
+
* leave the platform: LLM prompts, PR titles/bodies, commit messages.
|
|
16
|
+
*/
|
|
17
|
+
describe("normalizeExceptionText", () => {
|
|
18
|
+
test("replaces dynamic tokens with placeholders", () => {
|
|
19
|
+
expect(
|
|
20
|
+
normalizeExceptionText(
|
|
21
|
+
'invalid input syntax for type uuid: "550e8400-e29b-41d4-a716-446655440000"',
|
|
22
|
+
),
|
|
23
|
+
).toContain("<UUID>");
|
|
24
|
+
|
|
25
|
+
expect(
|
|
26
|
+
normalizeExceptionText("failed to email john.doe@example.com"),
|
|
27
|
+
).toContain("<EMAIL>");
|
|
28
|
+
|
|
29
|
+
expect(normalizeExceptionText("connect ETIMEDOUT 10.2.3.4:5432")).toContain(
|
|
30
|
+
"<IP>",
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("keeps the static structure of the message", () => {
|
|
35
|
+
const normalized: string = normalizeExceptionText(
|
|
36
|
+
"Domain lookup failed for id=12345678",
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
expect(normalized).toContain("Domain lookup failed");
|
|
40
|
+
expect(normalized).not.toContain("12345678");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("empty input stays empty", () => {
|
|
44
|
+
expect(normalizeExceptionText("")).toBe("");
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe("sanitizeExceptionMessage", () => {
|
|
49
|
+
test("strips interpolated user data and secrets", () => {
|
|
50
|
+
const sanitized: string = sanitizeExceptionMessage(
|
|
51
|
+
"Login failed for jane@customer.com with token ghp_0123456789abcdefghijklmnopqrstuvwxyz1234",
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
expect(sanitized).not.toContain("jane@customer.com");
|
|
55
|
+
expect(sanitized).not.toContain("ghp_0123456789");
|
|
56
|
+
expect(sanitized).toContain("Login failed");
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe("sanitizeStackTrace", () => {
|
|
61
|
+
test("preserves file:line frames while stripping PII", () => {
|
|
62
|
+
const sanitized: string = sanitizeStackTrace(
|
|
63
|
+
"Error: boom for user jane@customer.com\n at charge (/app/src/billing/charge.ts:12:5)",
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
// The code agent needs the frame intact to locate the defect.
|
|
67
|
+
expect(sanitized).toContain("/app/src/billing/charge.ts:12:5");
|
|
68
|
+
expect(sanitized).not.toContain("jane@customer.com");
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import InsightTriageRunner from "../../../Server/Utils/AI/SRE/Insights/InsightTriageRunner";
|
|
2
|
+
import ExceptionAIClassification from "../../../Types/AI/ExceptionAIClassification";
|
|
3
|
+
import { describe, expect, test } from "@jest/globals";
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
* The triage verdict gates the automatic fix lane, so the parser must fail
|
|
7
|
+
* CLOSED: anything unparseable reads as Unknown, and Unknown never routes
|
|
8
|
+
* a fix pull request.
|
|
9
|
+
*/
|
|
10
|
+
describe("InsightTriageRunner.parseClassification", () => {
|
|
11
|
+
test("parses the mandatory trailing verdict line", () => {
|
|
12
|
+
expect(
|
|
13
|
+
InsightTriageRunner.parseClassification(
|
|
14
|
+
"Root cause: bad uuid in URL.\n\nClassification: user-error",
|
|
15
|
+
),
|
|
16
|
+
).toBe(ExceptionAIClassification.UserError);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("tolerates markdown emphasis and mixed case", () => {
|
|
20
|
+
expect(
|
|
21
|
+
InsightTriageRunner.parseClassification(
|
|
22
|
+
"analysis...\n**Classification:** code-fault",
|
|
23
|
+
),
|
|
24
|
+
).toBe(ExceptionAIClassification.CodeFault);
|
|
25
|
+
|
|
26
|
+
expect(
|
|
27
|
+
InsightTriageRunner.parseClassification(
|
|
28
|
+
"analysis...\nCLASSIFICATION: expected-denial",
|
|
29
|
+
),
|
|
30
|
+
).toBe(ExceptionAIClassification.ExpectedDenial);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("the LAST verdict line wins when the taxonomy is discussed earlier", () => {
|
|
34
|
+
const analysis: string = [
|
|
35
|
+
"The options were:",
|
|
36
|
+
"Classification: code-fault",
|
|
37
|
+
"...but the paywall check fired intentionally.",
|
|
38
|
+
"Classification: expected-denial",
|
|
39
|
+
].join("\n");
|
|
40
|
+
|
|
41
|
+
expect(InsightTriageRunner.parseClassification(analysis)).toBe(
|
|
42
|
+
ExceptionAIClassification.ExpectedDenial,
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("fails CLOSED: missing, malformed, or empty input reads as unknown", () => {
|
|
47
|
+
expect(InsightTriageRunner.parseClassification("no verdict here")).toBe(
|
|
48
|
+
ExceptionAIClassification.Unknown,
|
|
49
|
+
);
|
|
50
|
+
expect(
|
|
51
|
+
InsightTriageRunner.parseClassification("Classification: maybe-a-bug"),
|
|
52
|
+
).toBe(ExceptionAIClassification.Unknown);
|
|
53
|
+
expect(InsightTriageRunner.parseClassification("")).toBe(
|
|
54
|
+
ExceptionAIClassification.Unknown,
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import TelemetryImprovementTaskTrigger from "../../../Server/Utils/AI/SRE/TelemetryImprovementTaskTrigger";
|
|
2
|
+
import SubjectCodeFixRun from "../../../Server/Utils/AI/SRE/SubjectCodeFixRun";
|
|
3
|
+
import ServiceService from "../../../Server/Services/ServiceService";
|
|
4
|
+
import Service from "../../../Models/DatabaseModels/Service";
|
|
5
|
+
import AIRun from "../../../Models/DatabaseModels/AIRun";
|
|
6
|
+
import CodeFixTaskType from "../../../Types/AI/CodeFixTaskType";
|
|
7
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
8
|
+
import { describe, expect, test, afterEach, beforeEach } from "@jest/globals";
|
|
9
|
+
|
|
10
|
+
/*
|
|
11
|
+
* The service-scoped instrumentation-improvement triggers (ImproveLogging /
|
|
12
|
+
* ImproveTracing): human-triggered, so every unmet gate fails EARLY with a
|
|
13
|
+
* clear message — unsupported recipe, missing/foreign service, no
|
|
14
|
+
* GitHub-App repository, or a duplicate active run for the same service.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const projectId: ObjectID = ObjectID.generate();
|
|
18
|
+
const telemetryServiceId: ObjectID = ObjectID.generate();
|
|
19
|
+
const userId: ObjectID = ObjectID.generate();
|
|
20
|
+
|
|
21
|
+
function mockService(overrides: Partial<Service> = {}): void {
|
|
22
|
+
jest.spyOn(ServiceService, "findOneById").mockResolvedValue({
|
|
23
|
+
id: telemetryServiceId,
|
|
24
|
+
name: "checkout",
|
|
25
|
+
projectId: projectId,
|
|
26
|
+
...overrides,
|
|
27
|
+
} as unknown as Service);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe("TelemetryImprovementTaskTrigger.createTelemetryImprovementTask", () => {
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
jest
|
|
33
|
+
.spyOn(SubjectCodeFixRun, "hasGitHubAppConnectedRepository")
|
|
34
|
+
.mockResolvedValue(true);
|
|
35
|
+
jest
|
|
36
|
+
.spyOn(SubjectCodeFixRun, "findNonTerminalRunForTelemetryService")
|
|
37
|
+
.mockResolvedValue(null);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
jest.restoreAllMocks();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("rejects non-improvement recipes before touching anything", async () => {
|
|
45
|
+
const findService: jest.SpyInstance = jest.spyOn(
|
|
46
|
+
ServiceService,
|
|
47
|
+
"findOneById",
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
await expect(
|
|
51
|
+
TelemetryImprovementTaskTrigger.createTelemetryImprovementTask({
|
|
52
|
+
projectId,
|
|
53
|
+
telemetryServiceId,
|
|
54
|
+
taskType: CodeFixTaskType.FixException,
|
|
55
|
+
userId,
|
|
56
|
+
}),
|
|
57
|
+
).rejects.toThrow(/not a telemetry-improvement recipe/);
|
|
58
|
+
|
|
59
|
+
expect(findService).not.toHaveBeenCalled();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("rejects a service from another project (root read, explicit tenant check)", async () => {
|
|
63
|
+
mockService({ projectId: ObjectID.generate() });
|
|
64
|
+
|
|
65
|
+
await expect(
|
|
66
|
+
TelemetryImprovementTaskTrigger.createTelemetryImprovementTask({
|
|
67
|
+
projectId,
|
|
68
|
+
telemetryServiceId,
|
|
69
|
+
taskType: CodeFixTaskType.ImproveLogging,
|
|
70
|
+
userId,
|
|
71
|
+
}),
|
|
72
|
+
).rejects.toThrow(/not found/);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("rejects when no GitHub-App-connected repository exists", async () => {
|
|
76
|
+
mockService();
|
|
77
|
+
jest
|
|
78
|
+
.spyOn(SubjectCodeFixRun, "hasGitHubAppConnectedRepository")
|
|
79
|
+
.mockResolvedValue(false);
|
|
80
|
+
|
|
81
|
+
await expect(
|
|
82
|
+
TelemetryImprovementTaskTrigger.createTelemetryImprovementTask({
|
|
83
|
+
projectId,
|
|
84
|
+
telemetryServiceId,
|
|
85
|
+
taskType: CodeFixTaskType.ImproveLogging,
|
|
86
|
+
userId,
|
|
87
|
+
}),
|
|
88
|
+
).rejects.toThrow(/GitHub-App-connected repository/);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("per-(service, recipe) dedupe: an active run for the service blocks a second one", async () => {
|
|
92
|
+
mockService();
|
|
93
|
+
jest
|
|
94
|
+
.spyOn(SubjectCodeFixRun, "findNonTerminalRunForTelemetryService")
|
|
95
|
+
.mockResolvedValue({ id: ObjectID.generate() } as unknown as AIRun);
|
|
96
|
+
const enqueue: jest.SpyInstance = jest.spyOn(
|
|
97
|
+
SubjectCodeFixRun,
|
|
98
|
+
"enqueueSubjectCodeFixRun",
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
await expect(
|
|
102
|
+
TelemetryImprovementTaskTrigger.createTelemetryImprovementTask({
|
|
103
|
+
projectId,
|
|
104
|
+
telemetryServiceId,
|
|
105
|
+
taskType: CodeFixTaskType.ImproveTracing,
|
|
106
|
+
userId,
|
|
107
|
+
}),
|
|
108
|
+
).rejects.toThrow(/already queued or running/);
|
|
109
|
+
|
|
110
|
+
expect(enqueue).not.toHaveBeenCalled();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("happy path: enqueues with the service captured in taskContext and the user attributed", async () => {
|
|
114
|
+
mockService();
|
|
115
|
+
const createdRun: AIRun = { id: ObjectID.generate() } as unknown as AIRun;
|
|
116
|
+
const enqueue: jest.SpyInstance = jest
|
|
117
|
+
.spyOn(SubjectCodeFixRun, "enqueueSubjectCodeFixRun")
|
|
118
|
+
.mockResolvedValue(createdRun);
|
|
119
|
+
|
|
120
|
+
const run: AIRun =
|
|
121
|
+
await TelemetryImprovementTaskTrigger.createTelemetryImprovementTask({
|
|
122
|
+
projectId,
|
|
123
|
+
telemetryServiceId,
|
|
124
|
+
taskType: CodeFixTaskType.ImproveLogging,
|
|
125
|
+
userId,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
expect(run).toBe(createdRun);
|
|
129
|
+
expect(enqueue).toHaveBeenCalledWith(
|
|
130
|
+
expect.objectContaining({
|
|
131
|
+
projectId,
|
|
132
|
+
taskType: CodeFixTaskType.ImproveLogging,
|
|
133
|
+
userId,
|
|
134
|
+
taskContext: expect.objectContaining({
|
|
135
|
+
telemetryServiceId: telemetryServiceId.toString(),
|
|
136
|
+
serviceName: "checkout",
|
|
137
|
+
}),
|
|
138
|
+
}),
|
|
139
|
+
);
|
|
140
|
+
});
|
|
141
|
+
});
|