@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
|
@@ -28,6 +28,12 @@ import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
|
|
28
28
|
import Query from "../Types/Database/Query";
|
|
29
29
|
import Includes from "../../Types/BaseDatabase/Includes";
|
|
30
30
|
import logger from "../Utils/Logger";
|
|
31
|
+
import AIAgentTaskPullRequest from "../../Models/DatabaseModels/AIAgentTaskPullRequest";
|
|
32
|
+
import AIAgentTaskPullRequestService from "./AIAgentTaskPullRequestService";
|
|
33
|
+
import PullRequestState from "../../Types/CodeRepository/PullRequestState";
|
|
34
|
+
import { normalizeExceptionText } from "../Utils/Telemetry/ExceptionSanitizer";
|
|
35
|
+
import Semaphore, { SemaphoreMutex } from "../Infrastructure/Semaphore";
|
|
36
|
+
import LIMIT_MAX from "../../Types/Database/LimitMax";
|
|
31
37
|
|
|
32
38
|
/*
|
|
33
39
|
* Hard cap on the fingerprint NOT IN list handed to the ClickHouse count
|
|
@@ -221,6 +227,11 @@ export class Service extends DatabaseService<Model> {
|
|
|
221
227
|
select: {
|
|
222
228
|
_id: true,
|
|
223
229
|
projectId: true,
|
|
230
|
+
message: true,
|
|
231
|
+
exceptionType: true,
|
|
232
|
+
isResolved: true,
|
|
233
|
+
isArchived: true,
|
|
234
|
+
aiFixDeclinedAt: true,
|
|
224
235
|
},
|
|
225
236
|
props,
|
|
226
237
|
});
|
|
@@ -229,6 +240,45 @@ export class Service extends DatabaseService<Model> {
|
|
|
229
240
|
throw new BadDataException("Telemetry Exception not found");
|
|
230
241
|
}
|
|
231
242
|
|
|
243
|
+
/*
|
|
244
|
+
* Server-side lifecycle gate (the dashboard hides the button for
|
|
245
|
+
* resolved exceptions, but the API must enforce it — the automatic
|
|
246
|
+
* insight lane and any direct API caller land here too).
|
|
247
|
+
*/
|
|
248
|
+
if (telemetryException.isResolved) {
|
|
249
|
+
throw new BadDataException(
|
|
250
|
+
"This exception is marked as resolved. Unresolve it before starting an AI agent task for it.",
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (telemetryException.isArchived) {
|
|
255
|
+
throw new BadDataException(
|
|
256
|
+
"This exception is archived. Unarchive it before starting an AI agent task for it.",
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/*
|
|
261
|
+
* Human "closed without merging" feedback: when an AI FIX PR for this
|
|
262
|
+
* exception was declined, the automatic lane must not keep re-opening
|
|
263
|
+
* fix PRs for it. Scoped to the FixException recipe — the stamp is only
|
|
264
|
+
* ever SET for closed fix PRs (SyncPullRequestStates), so a regression
|
|
265
|
+
* test or error-handling task is neither blocked by it nor allowed to
|
|
266
|
+
* clear it. A HUMAN clicking "Fix with AI" is the explicit override;
|
|
267
|
+
* the stamp is cleared only AFTER the retry run is actually created
|
|
268
|
+
* (see below), so a retry that fails a later gate leaves the human's
|
|
269
|
+
* decline in force.
|
|
270
|
+
*/
|
|
271
|
+
const isDeclinedFixOverride: boolean = Boolean(
|
|
272
|
+
telemetryException.aiFixDeclinedAt &&
|
|
273
|
+
taskType === CodeFixTaskType.FixException,
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
if (isDeclinedFixOverride && !props.userId) {
|
|
277
|
+
throw new BadDataException(
|
|
278
|
+
"A previous AI fix pull request for this exception was closed without merging, so automatic fix attempts are paused for it. A user can retry from the exception page.",
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
232
282
|
/*
|
|
233
283
|
* G11 guardrail: the per-project daily fix-run budget. Checked before
|
|
234
284
|
* the (more expensive) readiness probes — an over-budget project gets
|
|
@@ -261,38 +311,107 @@ export class Service extends DatabaseService<Model> {
|
|
|
261
311
|
}
|
|
262
312
|
|
|
263
313
|
/*
|
|
264
|
-
*
|
|
265
|
-
*
|
|
314
|
+
* The duplicate guards below are check-then-create: without
|
|
315
|
+
* serialization, two triage completions for splintered variants of the
|
|
316
|
+
* same root cause can both pass the guards and both create runs. A
|
|
317
|
+
* per-project Redis mutex closes the window; acquisition failure
|
|
318
|
+
* (Redis down) degrades to the unserialized behavior rather than
|
|
319
|
+
* blocking fix creation — the guards still run, they just race.
|
|
266
320
|
*/
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
321
|
+
let mutex: SemaphoreMutex | null = null;
|
|
322
|
+
|
|
323
|
+
try {
|
|
324
|
+
mutex = await Semaphore.lock({
|
|
325
|
+
key: telemetryException.projectId.toString(),
|
|
326
|
+
namespace: "TelemetryExceptionService.createCodeFixRunForException",
|
|
327
|
+
lockTimeout: 15000,
|
|
328
|
+
});
|
|
329
|
+
} catch (err) {
|
|
330
|
+
logger.debug(
|
|
331
|
+
`Could not acquire the code-fix creation mutex for project ${telemetryException.projectId.toString()} — proceeding without serialization: ${err}`,
|
|
332
|
+
);
|
|
279
333
|
}
|
|
280
334
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
335
|
+
let createdRun: AIRun;
|
|
336
|
+
|
|
337
|
+
try {
|
|
338
|
+
/*
|
|
339
|
+
* Duplicate guard is per (exception, taskType): an active
|
|
340
|
+
* FixException run must not block queuing a WriteRegressionTest run
|
|
341
|
+
* and vice versa.
|
|
342
|
+
*/
|
|
343
|
+
await this.validateNoActiveCodeFixRunExists(
|
|
344
|
+
telemetryExceptionId,
|
|
345
|
+
taskType,
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
/*
|
|
349
|
+
* Cross-group duplicate guard: interpolated dynamic values (garbage
|
|
350
|
+
* UUIDs, file paths, request payloads) splinter one root cause into
|
|
351
|
+
* many fingerprints, and the per-exception guard above cannot see
|
|
352
|
+
* that. Compare NORMALIZED messages against exceptions that already
|
|
353
|
+
* have an active run or an open AI pull request, so one root cause
|
|
354
|
+
* yields one PR — not one per interpolated variant.
|
|
355
|
+
*/
|
|
356
|
+
await this.validateNoOpenFixForSimilarException({
|
|
357
|
+
projectId: telemetryException.projectId,
|
|
358
|
+
telemetryExceptionId: telemetryExceptionId,
|
|
359
|
+
taskType: taskType,
|
|
360
|
+
message: telemetryException.message || "",
|
|
361
|
+
exceptionType: telemetryException.exceptionType || "",
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
const run: AIRun = new AIRun();
|
|
365
|
+
run.projectId = telemetryException.projectId;
|
|
366
|
+
run.runType = AIRunType.CodeFix;
|
|
367
|
+
run.codeFixTaskType = taskType;
|
|
368
|
+
run.status = AIRunStatus.Queued;
|
|
369
|
+
run.triggeredByTelemetryExceptionId = telemetryExceptionId;
|
|
370
|
+
|
|
371
|
+
// Attribution: the user who clicked "Fix with AI Agent".
|
|
372
|
+
if (props.userId) {
|
|
373
|
+
run.userId = props.userId;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/*
|
|
377
|
+
* Created as root: AIRun rows are server-written only (empty create
|
|
378
|
+
* ACL); the user's access was already checked by the exception read.
|
|
379
|
+
*/
|
|
380
|
+
createdRun = await AIRunService.create({
|
|
381
|
+
data: run,
|
|
382
|
+
props: {
|
|
383
|
+
isRoot: true,
|
|
384
|
+
},
|
|
385
|
+
});
|
|
386
|
+
} finally {
|
|
387
|
+
if (mutex) {
|
|
388
|
+
try {
|
|
389
|
+
await Semaphore.release(mutex);
|
|
390
|
+
} catch (err) {
|
|
391
|
+
logger.debug(`Failed to release the code-fix creation mutex: ${err}`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
291
395
|
|
|
292
396
|
if (!createdRun.id) {
|
|
293
397
|
throw new BadDataException("Failed to create the AI fix run");
|
|
294
398
|
}
|
|
295
399
|
|
|
400
|
+
/*
|
|
401
|
+
* The human's retry actually went through — only now consume the
|
|
402
|
+
* decline override. A retry that failed any gate above leaves the
|
|
403
|
+
* suppression in force.
|
|
404
|
+
*/
|
|
405
|
+
if (isDeclinedFixOverride) {
|
|
406
|
+
await this.updateOneById({
|
|
407
|
+
id: telemetryExceptionId,
|
|
408
|
+
data: {
|
|
409
|
+
aiFixDeclinedAt: null,
|
|
410
|
+
},
|
|
411
|
+
props: { isRoot: true },
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
|
|
296
415
|
return createdRun;
|
|
297
416
|
}
|
|
298
417
|
|
|
@@ -338,6 +457,183 @@ export class Service extends DatabaseService<Model> {
|
|
|
338
457
|
}
|
|
339
458
|
}
|
|
340
459
|
|
|
460
|
+
/*
|
|
461
|
+
* Cross-group duplicate guard. Two exception groups are "the same work"
|
|
462
|
+
* for the fix lane when their exceptionType and NORMALIZED message match
|
|
463
|
+
* (normalizeExceptionText replaces UUIDs, IDs, emails, paths' dynamic
|
|
464
|
+
* segments, timestamps...). Candidates are the exceptions behind
|
|
465
|
+
* (a) non-terminal CodeFix runs of the same recipe, and (b) OPEN
|
|
466
|
+
* AI-authored pull requests — a completed run whose PR is still open
|
|
467
|
+
* must keep blocking, or every interpolated variant re-fixes the same
|
|
468
|
+
* root cause (observed: 15 near-identical PRs for one invalid-uuid
|
|
469
|
+
* throw site).
|
|
470
|
+
*/
|
|
471
|
+
@CaptureSpan()
|
|
472
|
+
private async validateNoOpenFixForSimilarException(data: {
|
|
473
|
+
projectId: ObjectID;
|
|
474
|
+
telemetryExceptionId: ObjectID;
|
|
475
|
+
taskType: CodeFixTaskType;
|
|
476
|
+
message: string;
|
|
477
|
+
exceptionType: string;
|
|
478
|
+
}): Promise<void> {
|
|
479
|
+
// (a) Exceptions with a non-terminal run of the same recipe.
|
|
480
|
+
const activeRuns: Array<AIRun> = await AIRunService.findBy({
|
|
481
|
+
query: {
|
|
482
|
+
projectId: data.projectId,
|
|
483
|
+
runType: AIRunType.CodeFix,
|
|
484
|
+
/*
|
|
485
|
+
* A null codeFixTaskType means FixException (rows created before
|
|
486
|
+
* task recipes existed) — same legacy-null matching as
|
|
487
|
+
* validateNoActiveCodeFixRunExists.
|
|
488
|
+
*/
|
|
489
|
+
codeFixTaskType:
|
|
490
|
+
data.taskType === CodeFixTaskType.FixException
|
|
491
|
+
? QueryHelper.equalToOrNull(CodeFixTaskType.FixException)
|
|
492
|
+
: data.taskType,
|
|
493
|
+
status: QueryHelper.notIn([
|
|
494
|
+
AIRunStatus.Completed,
|
|
495
|
+
AIRunStatus.NoFixFound,
|
|
496
|
+
AIRunStatus.Error,
|
|
497
|
+
AIRunStatus.Cancelled,
|
|
498
|
+
AIRunStatus.Stale,
|
|
499
|
+
]),
|
|
500
|
+
},
|
|
501
|
+
select: {
|
|
502
|
+
triggeredByTelemetryExceptionId: true,
|
|
503
|
+
},
|
|
504
|
+
limit: LIMIT_MAX,
|
|
505
|
+
skip: 0,
|
|
506
|
+
props: { isRoot: true },
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
// (b) Exceptions behind still-open AI pull requests.
|
|
510
|
+
const openPullRequests: Array<AIAgentTaskPullRequest> =
|
|
511
|
+
await AIAgentTaskPullRequestService.findBy({
|
|
512
|
+
query: {
|
|
513
|
+
projectId: data.projectId,
|
|
514
|
+
pullRequestState: PullRequestState.Open,
|
|
515
|
+
},
|
|
516
|
+
select: {
|
|
517
|
+
aiRunId: true,
|
|
518
|
+
},
|
|
519
|
+
limit: LIMIT_MAX,
|
|
520
|
+
skip: 0,
|
|
521
|
+
props: { isRoot: true },
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
/*
|
|
525
|
+
* A guard that silently stops guarding is worse than none — surface
|
|
526
|
+
* candidate-scan truncation instead of quietly missing duplicates.
|
|
527
|
+
*/
|
|
528
|
+
if (
|
|
529
|
+
activeRuns.length >= LIMIT_MAX ||
|
|
530
|
+
openPullRequests.length >= LIMIT_MAX
|
|
531
|
+
) {
|
|
532
|
+
logger.warn(
|
|
533
|
+
`Cross-group AI fix dedupe guard: candidate scan truncated at ${LIMIT_MAX} rows for project ${data.projectId.toString()} — the duplicate-PR guard may miss matches.`,
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const openPrRunIds: Array<ObjectID> = openPullRequests
|
|
538
|
+
.map((pullRequest: AIAgentTaskPullRequest) => {
|
|
539
|
+
return pullRequest.aiRunId;
|
|
540
|
+
})
|
|
541
|
+
.filter((runId: ObjectID | undefined): runId is ObjectID => {
|
|
542
|
+
return Boolean(runId);
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
const openPrRuns: Array<AIRun> =
|
|
546
|
+
openPrRunIds.length > 0
|
|
547
|
+
? await AIRunService.findBy({
|
|
548
|
+
query: {
|
|
549
|
+
_id: new Includes(openPrRunIds),
|
|
550
|
+
projectId: data.projectId,
|
|
551
|
+
runType: AIRunType.CodeFix,
|
|
552
|
+
codeFixTaskType:
|
|
553
|
+
data.taskType === CodeFixTaskType.FixException
|
|
554
|
+
? QueryHelper.equalToOrNull(CodeFixTaskType.FixException)
|
|
555
|
+
: data.taskType,
|
|
556
|
+
},
|
|
557
|
+
select: {
|
|
558
|
+
triggeredByTelemetryExceptionId: true,
|
|
559
|
+
},
|
|
560
|
+
limit: LIMIT_MAX,
|
|
561
|
+
skip: 0,
|
|
562
|
+
props: { isRoot: true },
|
|
563
|
+
})
|
|
564
|
+
: [];
|
|
565
|
+
|
|
566
|
+
/*
|
|
567
|
+
* The target exception is deliberately NOT excluded: its own COMPLETED
|
|
568
|
+
* run with a still-open PR must keep blocking a re-fix (the
|
|
569
|
+
* non-terminal guard above cannot see completed runs). Its own ACTIVE
|
|
570
|
+
* runs were already rejected by validateNoActiveCodeFixRunExists.
|
|
571
|
+
*/
|
|
572
|
+
const candidateExceptionIds: Array<ObjectID> = [];
|
|
573
|
+
const seenIds: Set<string> = new Set<string>();
|
|
574
|
+
|
|
575
|
+
for (const run of [...activeRuns, ...openPrRuns]) {
|
|
576
|
+
const exceptionId: ObjectID | undefined =
|
|
577
|
+
run.triggeredByTelemetryExceptionId;
|
|
578
|
+
|
|
579
|
+
if (!exceptionId) {
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const idString: string = exceptionId.toString();
|
|
584
|
+
|
|
585
|
+
if (seenIds.has(idString)) {
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
seenIds.add(idString);
|
|
590
|
+
candidateExceptionIds.push(exceptionId);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
if (candidateExceptionIds.length === 0) {
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const targetSignature: string = `${data.exceptionType}|${normalizeExceptionText(
|
|
598
|
+
data.message,
|
|
599
|
+
)}`;
|
|
600
|
+
|
|
601
|
+
const candidates: Array<Model> = await this.findBy({
|
|
602
|
+
query: {
|
|
603
|
+
projectId: data.projectId,
|
|
604
|
+
_id: new Includes(candidateExceptionIds),
|
|
605
|
+
},
|
|
606
|
+
select: {
|
|
607
|
+
_id: true,
|
|
608
|
+
message: true,
|
|
609
|
+
exceptionType: true,
|
|
610
|
+
},
|
|
611
|
+
limit: candidateExceptionIds.length,
|
|
612
|
+
skip: 0,
|
|
613
|
+
props: { isRoot: true },
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
for (const candidate of candidates) {
|
|
617
|
+
const candidateSignature: string = `${candidate.exceptionType || ""}|${normalizeExceptionText(
|
|
618
|
+
candidate.message || "",
|
|
619
|
+
)}`;
|
|
620
|
+
|
|
621
|
+
if (candidateSignature !== targetSignature) {
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
if (candidate._id?.toString() === data.telemetryExceptionId.toString()) {
|
|
626
|
+
throw new BadDataException(
|
|
627
|
+
"This exception already has an open AI pull request for this task type. Review that pull request instead of opening another one.",
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
throw new BadDataException(
|
|
632
|
+
`A similar exception (same type and normalized message, exception ${candidate._id?.toString()}) already has an AI agent task in progress or an open AI pull request. Review that pull request instead of opening another one for the same root cause.`,
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
341
637
|
@CaptureSpan()
|
|
342
638
|
public async getDashboardSummary(
|
|
343
639
|
props: DatabaseCommonInteractionProps,
|
|
@@ -13,6 +13,7 @@ import TelemetryExceptionService from "../../../../../Services/TelemetryExceptio
|
|
|
13
13
|
import ExceptionInstanceService from "../../../../../Services/ExceptionInstanceService";
|
|
14
14
|
import ServiceService from "../../../../../Services/ServiceService";
|
|
15
15
|
import QueryHelper from "../../../../../Types/Database/QueryHelper";
|
|
16
|
+
import { sanitizeExceptionMessage } from "../../../../Telemetry/ExceptionSanitizer";
|
|
16
17
|
import {
|
|
17
18
|
InsightCandidate,
|
|
18
19
|
InsightDetector,
|
|
@@ -239,9 +240,19 @@ export default class ExceptionSpikeDetector implements InsightDetector {
|
|
|
239
240
|
exception.primaryEntityId,
|
|
240
241
|
);
|
|
241
242
|
|
|
243
|
+
/*
|
|
244
|
+
* The message flows into the insight title/detail/evidence, and from
|
|
245
|
+
* there into the triage LLM prompt — sanitize it once here so no
|
|
246
|
+
* interpolated user data (IDs, emails, domains) rides along. The raw
|
|
247
|
+
* message stays on the exception row for the dashboard.
|
|
248
|
+
*/
|
|
249
|
+
const safeMessage: string = sanitizeExceptionMessage(
|
|
250
|
+
exception.message || "",
|
|
251
|
+
);
|
|
252
|
+
|
|
242
253
|
const label: string = ExceptionSpikeDetector.buildExceptionLabel(
|
|
243
254
|
exception.exceptionType,
|
|
244
|
-
|
|
255
|
+
safeMessage,
|
|
245
256
|
);
|
|
246
257
|
|
|
247
258
|
const firstSeenAtIso: string | undefined = exception.firstSeenAt
|
|
@@ -255,8 +266,8 @@ export default class ExceptionSpikeDetector implements InsightDetector {
|
|
|
255
266
|
"",
|
|
256
267
|
`- Exception: \`${label}\``,
|
|
257
268
|
];
|
|
258
|
-
if (
|
|
259
|
-
detailLines.push(`- Message: ${
|
|
269
|
+
if (safeMessage) {
|
|
270
|
+
detailLines.push(`- Message: ${safeMessage}`);
|
|
260
271
|
}
|
|
261
272
|
if (serviceName) {
|
|
262
273
|
detailLines.push(`- Service: ${serviceName}`);
|
|
@@ -281,7 +292,7 @@ export default class ExceptionSpikeDetector implements InsightDetector {
|
|
|
281
292
|
telemetryExceptionId: exception.id,
|
|
282
293
|
evidence: {
|
|
283
294
|
exception: {
|
|
284
|
-
exceptionMessage:
|
|
295
|
+
exceptionMessage: safeMessage,
|
|
285
296
|
exceptionType: exception.exceptionType,
|
|
286
297
|
recentOccurrenceCount: recentCount.toNumber(),
|
|
287
298
|
baselineHourlyAverage: decision.baselineHourlyAverage,
|
|
@@ -9,6 +9,7 @@ import Service from "../../../../../../Models/DatabaseModels/Service";
|
|
|
9
9
|
import TelemetryExceptionService from "../../../../../Services/TelemetryExceptionService";
|
|
10
10
|
import ServiceService from "../../../../../Services/ServiceService";
|
|
11
11
|
import QueryHelper from "../../../../../Types/Database/QueryHelper";
|
|
12
|
+
import { sanitizeExceptionMessage } from "../../../../Telemetry/ExceptionSanitizer";
|
|
12
13
|
import {
|
|
13
14
|
InsightCandidate,
|
|
14
15
|
InsightDetector,
|
|
@@ -166,9 +167,19 @@ export default class NewExceptionDetector implements InsightDetector {
|
|
|
166
167
|
exception.primaryEntityId,
|
|
167
168
|
);
|
|
168
169
|
|
|
170
|
+
/*
|
|
171
|
+
* The message flows into the insight title/detail/evidence, and from
|
|
172
|
+
* there into the triage LLM prompt — sanitize it once here so no
|
|
173
|
+
* interpolated user data (IDs, emails, domains) rides along. The raw
|
|
174
|
+
* message stays on the exception row for the dashboard.
|
|
175
|
+
*/
|
|
176
|
+
const safeMessage: string = sanitizeExceptionMessage(
|
|
177
|
+
exception.message || "",
|
|
178
|
+
);
|
|
179
|
+
|
|
169
180
|
const label: string = NewExceptionDetector.buildExceptionLabel(
|
|
170
181
|
exception.exceptionType,
|
|
171
|
-
|
|
182
|
+
safeMessage,
|
|
172
183
|
);
|
|
173
184
|
|
|
174
185
|
const firstSeenAtIso: string | undefined = exception.firstSeenAt
|
|
@@ -180,8 +191,8 @@ export default class NewExceptionDetector implements InsightDetector {
|
|
|
180
191
|
"",
|
|
181
192
|
`- Exception: \`${label}\``,
|
|
182
193
|
];
|
|
183
|
-
if (
|
|
184
|
-
detailLines.push(`- Message: ${
|
|
194
|
+
if (safeMessage) {
|
|
195
|
+
detailLines.push(`- Message: ${safeMessage}`);
|
|
185
196
|
}
|
|
186
197
|
if (serviceName) {
|
|
187
198
|
detailLines.push(`- Service: ${serviceName}`);
|
|
@@ -204,7 +215,7 @@ export default class NewExceptionDetector implements InsightDetector {
|
|
|
204
215
|
telemetryExceptionId: exception.id,
|
|
205
216
|
evidence: {
|
|
206
217
|
exception: {
|
|
207
|
-
exceptionMessage:
|
|
218
|
+
exceptionMessage: safeMessage,
|
|
208
219
|
exceptionType: exception.exceptionType,
|
|
209
220
|
totalOccurrenceCount: occurrenceCount,
|
|
210
221
|
firstSeenAt: firstSeenAtIso,
|