@oneuptime/common 11.5.12 → 11.6.0
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/EnterpriseLicense.ts +21 -0
- package/Models/DatabaseModels/EnterpriseLicenseInstance.ts +25 -0
- package/Models/DatabaseModels/GlobalConfig.ts +76 -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/API/EnterpriseLicenseAPI.ts +57 -0
- package/Server/API/GlobalConfigAPI.ts +62 -1
- package/Server/EnvironmentConfig.ts +22 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1784640000000-AddExceptionTriageAndPrivacyColumns.ts +56 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1784659816363-AddInstanceVersionAndLatestReleaseColumns.ts +38 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1784705487674-AddEnterpriseLicenseEvaluationColumns.ts +25 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
- package/Server/Services/AIRunService.ts +8 -0
- package/Server/Services/AnalyticsDatabaseService.ts +48 -45
- package/Server/Services/DatabaseService.ts +66 -1
- package/Server/Services/TelemetryExceptionService.ts +320 -24
- package/Server/Services/WorkflowService.ts +100 -39
- package/Server/Types/Workflow/Components/Schedule.ts +57 -32
- 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/AnalyticsDatabase/InsertDedupContext.ts +59 -0
- package/Server/Utils/Express.ts +13 -0
- package/Server/Utils/Telemetry/ExceptionSanitizer.ts +222 -0
- package/Server/Utils/Telemetry/TelemetryFanInWriter.ts +799 -0
- package/Server/Utils/Telemetry/TelemetryWriterClient.ts +263 -0
- package/Server/Utils/Telemetry/TelemetryWriterServer.ts +240 -0
- package/Server/Utils/Telemetry/TelemetryWriterShedMetrics.ts +93 -0
- package/Tests/Server/Services/DatabaseServiceSanitizeUpdateData.test.ts +232 -0
- package/Tests/Server/Services/TelemetryExceptionCodeFixRun.test.ts +205 -1
- package/Tests/Server/Services/UpdateOneByIdKeepsRowId.test.ts +107 -0
- package/Tests/Server/Services/WorkflowService.test.ts +308 -0
- 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/Attribution.test.ts +203 -0
- package/Tests/Server/Utils/ExceptionSanitizer.test.ts +70 -0
- package/Tests/Server/Utils/InsightTriageClassification.test.ts +57 -0
- package/Tests/Server/Utils/Marketing/ConversionUploadProvider.test.ts +183 -0
- package/Tests/Server/Utils/Marketing/GoogleAds.test.ts +340 -0
- package/Tests/Server/Utils/Monitor/Criteria/ExceptionMonitorCriteria.test.ts +87 -0
- package/Tests/Server/Utils/Monitor/Criteria/LogMonitorCriteria.test.ts +165 -0
- package/Tests/Server/Utils/Monitor/Criteria/TraceMonitorCriteria.test.ts +86 -0
- package/Tests/Server/Utils/Telemetry/TelemetryFanInWriter.test.ts +1565 -0
- package/Tests/Server/Utils/Telemetry/TelemetryWriterClient.test.ts +313 -0
- package/Tests/Server/Utils/Telemetry/TelemetryWriterServer.test.ts +270 -0
- package/Tests/Server/Utils/Telemetry/TelemetryWriterShedMetrics.test.ts +127 -0
- package/Tests/Server/Utils/TelemetryImprovementTaskTrigger.test.ts +141 -0
- package/Tests/Types/AI/CodeFixTaskType.test.ts +3 -0
- package/Tests/Types/Monitor/MonitorCriteriaMetricVariables.test.ts +228 -0
- package/Tests/Types/Monitor/MonitorStepDefaultTelemetryConfig.test.ts +169 -0
- package/Tests/Types/Monitor/MonitorStepExceptionMonitor.test.ts +289 -0
- package/Tests/Types/Monitor/MonitorStepLogMonitor.test.ts +231 -0
- package/Tests/Types/Monitor/MonitorStepMetricViewConfigUtil.test.ts +245 -0
- package/Tests/Types/Monitor/MonitorStepNetworkDeviceMonitor.test.ts +155 -0
- package/Tests/Types/Monitor/MonitorStepTelemetrySerialization.test.ts +141 -0
- package/Tests/Types/Monitor/MonitorStepTraceMonitor.test.ts +137 -0
- package/Tests/Types/Workspace/NotificationRules/NotificationRuleCondition.test.ts +311 -0
- package/Tests/UI/Components/ModelTableSelectFromColumns.test.ts +259 -0
- package/Tests/UI/Utils/Breadcrumb/BreadcrumbTrailResolver.test.ts +461 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +2852 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +758 -0
- package/Tests/UI/Utils/NotificationMethodUtil.test.ts +564 -0
- package/Tests/Utils/Array.test.ts +156 -0
- package/Tests/Utils/CronTab.test.ts +199 -0
- package/Tests/Utils/ModelImportExport.test.ts +101 -0
- package/Tests/Utils/Number.test.ts +179 -0
- package/Tests/Utils/VersionUtil.test.ts +348 -0
- package/Tests/jest.setup.ts +1 -0
- package/Types/AI/CodeFixTaskContext.ts +6 -0
- package/Types/AI/CodeFixTaskType.ts +38 -1
- package/Types/AI/ExceptionAIClassification.ts +27 -0
- package/Types/EnterpriseLicense/EnterpriseLicenseInstanceSummary.ts +6 -0
- package/Types/Log/LogScrubPatternType.ts +7 -0
- package/Types/Monitor/MonitorStep.ts +24 -4
- package/Types/Monitor/MonitorStepMetricViewConfigUtil.ts +105 -0
- package/Types/Trace/TraceScrubPatternType.ts +7 -0
- package/UI/Components/EditionLabel/EditionLabel.tsx +445 -12
- package/UI/Components/ModelTable/BaseModelTable.tsx +15 -46
- package/UI/Components/ModelTable/SelectFromColumns.ts +129 -0
- package/UI/Components/Workflow/ArgumentsForm.tsx +68 -30
- package/UI/Components/Workflow/CronScheduleField.tsx +411 -0
- package/UI/Utils/Breadcrumb/BreadcrumbTrailResolver.ts +176 -0
- package/UI/Utils/NotificationMethodUtil.ts +310 -0
- package/Utils/CronTab.ts +823 -0
- package/Utils/ModelImportExport.ts +21 -6
- package/Utils/VersionUtil.ts +260 -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/EnterpriseLicense.js +22 -0
- package/build/dist/Models/DatabaseModels/EnterpriseLicense.js.map +1 -1
- package/build/dist/Models/DatabaseModels/EnterpriseLicenseInstance.js +26 -0
- package/build/dist/Models/DatabaseModels/EnterpriseLicenseInstance.js.map +1 -1
- package/build/dist/Models/DatabaseModels/GlobalConfig.js +81 -0
- package/build/dist/Models/DatabaseModels/GlobalConfig.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/API/EnterpriseLicenseAPI.js +47 -0
- package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +1 -1
- package/build/dist/Server/API/GlobalConfigAPI.js +52 -1
- package/build/dist/Server/API/GlobalConfigAPI.js.map +1 -1
- package/build/dist/Server/EnvironmentConfig.js +17 -0
- package/build/dist/Server/EnvironmentConfig.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/1784659816363-AddInstanceVersionAndLatestReleaseColumns.js +18 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784659816363-AddInstanceVersionAndLatestReleaseColumns.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784705487674-AddEnterpriseLicenseEvaluationColumns.js +14 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784705487674-AddEnterpriseLicenseEvaluationColumns.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -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/AnalyticsDatabaseService.js +44 -16
- package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +53 -1
- package/build/dist/Server/Services/DatabaseService.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/Services/WorkflowService.js +80 -30
- package/build/dist/Server/Services/WorkflowService.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/Schedule.js +44 -19
- package/build/dist/Server/Types/Workflow/Components/Schedule.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/AnalyticsDatabase/InsertDedupContext.js +24 -0
- package/build/dist/Server/Utils/AnalyticsDatabase/InsertDedupContext.js.map +1 -0
- package/build/dist/Server/Utils/Express.js +12 -0
- package/build/dist/Server/Utils/Express.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/ExceptionSanitizer.js +149 -0
- package/build/dist/Server/Utils/Telemetry/ExceptionSanitizer.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/TelemetryFanInWriter.js +496 -0
- package/build/dist/Server/Utils/Telemetry/TelemetryFanInWriter.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/TelemetryWriterClient.js +165 -0
- package/build/dist/Server/Utils/Telemetry/TelemetryWriterClient.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/TelemetryWriterServer.js +136 -0
- package/build/dist/Server/Utils/Telemetry/TelemetryWriterServer.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/TelemetryWriterShedMetrics.js +76 -0
- package/build/dist/Server/Utils/Telemetry/TelemetryWriterShedMetrics.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/Monitor/MonitorStep.js +20 -4
- package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorStepMetricViewConfigUtil.js +73 -0
- package/build/dist/Types/Monitor/MonitorStepMetricViewConfigUtil.js.map +1 -0
- package/build/dist/Types/Trace/TraceScrubPatternType.js +7 -0
- package/build/dist/Types/Trace/TraceScrubPatternType.js.map +1 -1
- package/build/dist/UI/Components/EditionLabel/EditionLabel.js +261 -13
- package/build/dist/UI/Components/EditionLabel/EditionLabel.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/build/dist/UI/Components/Workflow/ArgumentsForm.js +30 -6
- package/build/dist/UI/Components/Workflow/ArgumentsForm.js.map +1 -1
- package/build/dist/UI/Components/Workflow/CronScheduleField.js +209 -0
- package/build/dist/UI/Components/Workflow/CronScheduleField.js.map +1 -0
- package/build/dist/UI/Utils/Breadcrumb/BreadcrumbTrailResolver.js +129 -0
- package/build/dist/UI/Utils/Breadcrumb/BreadcrumbTrailResolver.js.map +1 -0
- package/build/dist/UI/Utils/NotificationMethodUtil.js +189 -0
- package/build/dist/UI/Utils/NotificationMethodUtil.js.map +1 -0
- package/build/dist/Utils/CronTab.js +609 -0
- package/build/dist/Utils/CronTab.js.map +1 -0
- package/build/dist/Utils/ModelImportExport.js +20 -6
- package/build/dist/Utils/ModelImportExport.js.map +1 -1
- package/build/dist/Utils/VersionUtil.js +193 -0
- package/build/dist/Utils/VersionUtil.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,203 @@
|
|
|
1
|
+
import Attribution from "../../../Server/Utils/Attribution";
|
|
2
|
+
import { JSONObject } from "../../../Types/JSON";
|
|
3
|
+
import { describe, expect, test } from "@jest/globals";
|
|
4
|
+
|
|
5
|
+
describe("Attribution", () => {
|
|
6
|
+
describe("sanitizeClickIds", () => {
|
|
7
|
+
test("keeps every supported ad-platform click identifier", () => {
|
|
8
|
+
const clickIds: JSONObject = {
|
|
9
|
+
gclid: "google-click",
|
|
10
|
+
wbraid: "google-web-to-app",
|
|
11
|
+
gbraid: "google-app-to-web",
|
|
12
|
+
fbclid: "meta-click",
|
|
13
|
+
msclkid: "microsoft-click",
|
|
14
|
+
li_fat_id: "linkedin-click",
|
|
15
|
+
twclid: "x-click",
|
|
16
|
+
rdt_cid: "reddit-click",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
expect(Attribution.sanitizeClickIds(clickIds)).toEqual(clickIds);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("drops keys that are not explicitly allowlisted", () => {
|
|
23
|
+
expect(
|
|
24
|
+
Attribution.sanitizeClickIds({
|
|
25
|
+
gclid: "valid",
|
|
26
|
+
attackerControlledKey: "must-not-persist",
|
|
27
|
+
__proto__: "must-not-persist",
|
|
28
|
+
}),
|
|
29
|
+
).toEqual({ gclid: "valid" });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test.each([null, undefined, "gclid=x", 42, true, ["gclid"]])(
|
|
33
|
+
"rejects a non-object value: %p",
|
|
34
|
+
(value: unknown) => {
|
|
35
|
+
expect(Attribution.sanitizeClickIds(value as never)).toBeUndefined();
|
|
36
|
+
},
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
test("drops empty, boolean and object click-id values", () => {
|
|
40
|
+
expect(
|
|
41
|
+
Attribution.sanitizeClickIds({
|
|
42
|
+
gclid: "",
|
|
43
|
+
fbclid: false,
|
|
44
|
+
msclkid: { nested: "value" },
|
|
45
|
+
}),
|
|
46
|
+
).toBeUndefined();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("normalizes numeric identifiers to strings", () => {
|
|
50
|
+
expect(
|
|
51
|
+
Attribution.sanitizeClickIds({
|
|
52
|
+
gclid: 12345,
|
|
53
|
+
msclkid: 67890,
|
|
54
|
+
}),
|
|
55
|
+
).toEqual({
|
|
56
|
+
gclid: "12345",
|
|
57
|
+
msclkid: "67890",
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("caps each identifier at 500 characters", () => {
|
|
62
|
+
const result: JSONObject | undefined = Attribution.sanitizeClickIds({
|
|
63
|
+
gclid: "g".repeat(600),
|
|
64
|
+
fbclid: "f".repeat(501),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
expect(result?.["gclid"]).toBe("g".repeat(500));
|
|
68
|
+
expect(result?.["fbclid"]).toBe("f".repeat(500));
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("returns a fresh object instead of retaining the untrusted input", () => {
|
|
72
|
+
const input: JSONObject = { gclid: "original" };
|
|
73
|
+
const result: JSONObject | undefined =
|
|
74
|
+
Attribution.sanitizeClickIds(input);
|
|
75
|
+
|
|
76
|
+
input["gclid"] = "mutated";
|
|
77
|
+
|
|
78
|
+
expect(result).toEqual({ gclid: "original" });
|
|
79
|
+
expect(result).not.toBe(input);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe("sanitizeFirstTouchAttribution", () => {
|
|
84
|
+
test("keeps the complete supported first-touch payload", () => {
|
|
85
|
+
const firstTouch: JSONObject = {
|
|
86
|
+
utmSource: "google",
|
|
87
|
+
utmMedium: "cpc",
|
|
88
|
+
utmCampaign: "pagerduty-alternative",
|
|
89
|
+
utmTerm: "pagerduty alternative",
|
|
90
|
+
utmContent: "comparison-ad-a",
|
|
91
|
+
landingUrl: "https://oneuptime.com/compare/pagerduty?gclid=abc",
|
|
92
|
+
referrer: "https://google.com/",
|
|
93
|
+
timestamp: "2026-07-22T10:00:00.000Z",
|
|
94
|
+
clickIds: {
|
|
95
|
+
gclid: "abc",
|
|
96
|
+
msclkid: "def",
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
expect(Attribution.sanitizeFirstTouchAttribution(firstTouch)).toEqual(
|
|
101
|
+
firstTouch,
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("drops unknown top-level and nested click-id keys", () => {
|
|
106
|
+
expect(
|
|
107
|
+
Attribution.sanitizeFirstTouchAttribution({
|
|
108
|
+
utmSource: "google",
|
|
109
|
+
arbitrary: "do-not-store",
|
|
110
|
+
clickIds: {
|
|
111
|
+
gclid: "valid",
|
|
112
|
+
arbitraryClickId: "do-not-store",
|
|
113
|
+
},
|
|
114
|
+
}),
|
|
115
|
+
).toEqual({
|
|
116
|
+
utmSource: "google",
|
|
117
|
+
clickIds: { gclid: "valid" },
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("keeps valid first-touch fields when clickIds is malformed", () => {
|
|
122
|
+
expect(
|
|
123
|
+
Attribution.sanitizeFirstTouchAttribution({
|
|
124
|
+
utmSource: "newsletter",
|
|
125
|
+
clickIds: ["not", "an", "object"],
|
|
126
|
+
}),
|
|
127
|
+
).toEqual({ utmSource: "newsletter" });
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("keeps valid click IDs when no UTM values exist", () => {
|
|
131
|
+
expect(
|
|
132
|
+
Attribution.sanitizeFirstTouchAttribution({
|
|
133
|
+
clickIds: { gclid: "auto-tagged-click" },
|
|
134
|
+
}),
|
|
135
|
+
).toEqual({
|
|
136
|
+
clickIds: { gclid: "auto-tagged-click" },
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("caps all first-touch strings and nested click IDs", () => {
|
|
141
|
+
const result: JSONObject | undefined =
|
|
142
|
+
Attribution.sanitizeFirstTouchAttribution({
|
|
143
|
+
utmCampaign: "c".repeat(700),
|
|
144
|
+
landingUrl: "l".repeat(700),
|
|
145
|
+
clickIds: { gclid: "g".repeat(700) },
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
expect(result?.["utmCampaign"]).toBe("c".repeat(500));
|
|
149
|
+
expect(result?.["landingUrl"]).toBe("l".repeat(500));
|
|
150
|
+
expect((result?.["clickIds"] as JSONObject)?.["gclid"]).toBe(
|
|
151
|
+
"g".repeat(500),
|
|
152
|
+
);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("normalizes numeric scalar values", () => {
|
|
156
|
+
expect(
|
|
157
|
+
Attribution.sanitizeFirstTouchAttribution({
|
|
158
|
+
utmCampaign: 2026,
|
|
159
|
+
timestamp: 123456,
|
|
160
|
+
}),
|
|
161
|
+
).toEqual({
|
|
162
|
+
utmCampaign: "2026",
|
|
163
|
+
timestamp: "123456",
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test.each([null, undefined, "utm_source=google", 42, true, []])(
|
|
168
|
+
"rejects a non-object first-touch value: %p",
|
|
169
|
+
(value: unknown) => {
|
|
170
|
+
expect(
|
|
171
|
+
Attribution.sanitizeFirstTouchAttribution(value as never),
|
|
172
|
+
).toBeUndefined();
|
|
173
|
+
},
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
test("returns undefined when every supplied field is invalid", () => {
|
|
177
|
+
expect(
|
|
178
|
+
Attribution.sanitizeFirstTouchAttribution({
|
|
179
|
+
unsupported: "value",
|
|
180
|
+
clickIds: { unsupported: "value" },
|
|
181
|
+
}),
|
|
182
|
+
).toBeUndefined();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("does not retain mutable nested input objects", () => {
|
|
186
|
+
const clickIds: JSONObject = { gclid: "original" };
|
|
187
|
+
const input: JSONObject = {
|
|
188
|
+
utmSource: "google",
|
|
189
|
+
clickIds,
|
|
190
|
+
};
|
|
191
|
+
const result: JSONObject | undefined =
|
|
192
|
+
Attribution.sanitizeFirstTouchAttribution(input);
|
|
193
|
+
|
|
194
|
+
clickIds["gclid"] = "mutated";
|
|
195
|
+
input["utmSource"] = "mutated";
|
|
196
|
+
|
|
197
|
+
expect(result).toEqual({
|
|
198
|
+
utmSource: "google",
|
|
199
|
+
clickIds: { gclid: "original" },
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
});
|
|
@@ -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
|
+
});
|