@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.
Files changed (193) hide show
  1. package/Models/DatabaseModels/AIInsight.ts +30 -0
  2. package/Models/DatabaseModels/EnterpriseLicense.ts +21 -0
  3. package/Models/DatabaseModels/EnterpriseLicenseInstance.ts +25 -0
  4. package/Models/DatabaseModels/GlobalConfig.ts +76 -0
  5. package/Models/DatabaseModels/Project.ts +30 -0
  6. package/Models/DatabaseModels/TelemetryException.ts +128 -0
  7. package/Server/API/AIAgentDataAPI.ts +129 -5
  8. package/Server/API/AIInvestigationAPI.ts +87 -0
  9. package/Server/API/EnterpriseLicenseAPI.ts +57 -0
  10. package/Server/API/GlobalConfigAPI.ts +62 -1
  11. package/Server/EnvironmentConfig.ts +22 -0
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/1784640000000-AddExceptionTriageAndPrivacyColumns.ts +56 -0
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/1784659816363-AddInstanceVersionAndLatestReleaseColumns.ts +38 -0
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/1784705487674-AddEnterpriseLicenseEvaluationColumns.ts +25 -0
  15. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
  16. package/Server/Services/AIRunService.ts +8 -0
  17. package/Server/Services/AnalyticsDatabaseService.ts +48 -45
  18. package/Server/Services/DatabaseService.ts +66 -1
  19. package/Server/Services/TelemetryExceptionService.ts +320 -24
  20. package/Server/Services/WorkflowService.ts +100 -39
  21. package/Server/Types/Workflow/Components/Schedule.ts +57 -32
  22. package/Server/Utils/AI/SRE/Insights/Detectors/ExceptionSpikeDetector.ts +15 -4
  23. package/Server/Utils/AI/SRE/Insights/Detectors/NewExceptionDetector.ts +15 -4
  24. package/Server/Utils/AI/SRE/Insights/InsightScanner.ts +222 -24
  25. package/Server/Utils/AI/SRE/Insights/InsightTriageRunner.ts +225 -9
  26. package/Server/Utils/AI/SRE/SubjectCodeFixRun.ts +39 -0
  27. package/Server/Utils/AI/SRE/TelemetryImprovementTaskTrigger.ts +114 -0
  28. package/Server/Utils/AnalyticsDatabase/InsertDedupContext.ts +59 -0
  29. package/Server/Utils/Express.ts +13 -0
  30. package/Server/Utils/Telemetry/ExceptionSanitizer.ts +222 -0
  31. package/Server/Utils/Telemetry/TelemetryFanInWriter.ts +799 -0
  32. package/Server/Utils/Telemetry/TelemetryWriterClient.ts +263 -0
  33. package/Server/Utils/Telemetry/TelemetryWriterServer.ts +240 -0
  34. package/Server/Utils/Telemetry/TelemetryWriterShedMetrics.ts +93 -0
  35. package/Tests/Server/Services/DatabaseServiceSanitizeUpdateData.test.ts +232 -0
  36. package/Tests/Server/Services/TelemetryExceptionCodeFixRun.test.ts +205 -1
  37. package/Tests/Server/Services/UpdateOneByIdKeepsRowId.test.ts +107 -0
  38. package/Tests/Server/Services/WorkflowService.test.ts +308 -0
  39. package/Tests/Server/Utils/AI/Insights/InsightScanner.test.ts +150 -141
  40. package/Tests/Server/Utils/AI/Insights/InsightTriageRunner.test.ts +277 -1
  41. package/Tests/Server/Utils/Attribution.test.ts +203 -0
  42. package/Tests/Server/Utils/ExceptionSanitizer.test.ts +70 -0
  43. package/Tests/Server/Utils/InsightTriageClassification.test.ts +57 -0
  44. package/Tests/Server/Utils/Marketing/ConversionUploadProvider.test.ts +183 -0
  45. package/Tests/Server/Utils/Marketing/GoogleAds.test.ts +340 -0
  46. package/Tests/Server/Utils/Monitor/Criteria/ExceptionMonitorCriteria.test.ts +87 -0
  47. package/Tests/Server/Utils/Monitor/Criteria/LogMonitorCriteria.test.ts +165 -0
  48. package/Tests/Server/Utils/Monitor/Criteria/TraceMonitorCriteria.test.ts +86 -0
  49. package/Tests/Server/Utils/Telemetry/TelemetryFanInWriter.test.ts +1565 -0
  50. package/Tests/Server/Utils/Telemetry/TelemetryWriterClient.test.ts +313 -0
  51. package/Tests/Server/Utils/Telemetry/TelemetryWriterServer.test.ts +270 -0
  52. package/Tests/Server/Utils/Telemetry/TelemetryWriterShedMetrics.test.ts +127 -0
  53. package/Tests/Server/Utils/TelemetryImprovementTaskTrigger.test.ts +141 -0
  54. package/Tests/Types/AI/CodeFixTaskType.test.ts +3 -0
  55. package/Tests/Types/Monitor/MonitorCriteriaMetricVariables.test.ts +228 -0
  56. package/Tests/Types/Monitor/MonitorStepDefaultTelemetryConfig.test.ts +169 -0
  57. package/Tests/Types/Monitor/MonitorStepExceptionMonitor.test.ts +289 -0
  58. package/Tests/Types/Monitor/MonitorStepLogMonitor.test.ts +231 -0
  59. package/Tests/Types/Monitor/MonitorStepMetricViewConfigUtil.test.ts +245 -0
  60. package/Tests/Types/Monitor/MonitorStepNetworkDeviceMonitor.test.ts +155 -0
  61. package/Tests/Types/Monitor/MonitorStepTelemetrySerialization.test.ts +141 -0
  62. package/Tests/Types/Monitor/MonitorStepTraceMonitor.test.ts +137 -0
  63. package/Tests/Types/Workspace/NotificationRules/NotificationRuleCondition.test.ts +311 -0
  64. package/Tests/UI/Components/ModelTableSelectFromColumns.test.ts +259 -0
  65. package/Tests/UI/Utils/Breadcrumb/BreadcrumbTrailResolver.test.ts +461 -0
  66. package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +2852 -0
  67. package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +758 -0
  68. package/Tests/UI/Utils/NotificationMethodUtil.test.ts +564 -0
  69. package/Tests/Utils/Array.test.ts +156 -0
  70. package/Tests/Utils/CronTab.test.ts +199 -0
  71. package/Tests/Utils/ModelImportExport.test.ts +101 -0
  72. package/Tests/Utils/Number.test.ts +179 -0
  73. package/Tests/Utils/VersionUtil.test.ts +348 -0
  74. package/Tests/jest.setup.ts +1 -0
  75. package/Types/AI/CodeFixTaskContext.ts +6 -0
  76. package/Types/AI/CodeFixTaskType.ts +38 -1
  77. package/Types/AI/ExceptionAIClassification.ts +27 -0
  78. package/Types/EnterpriseLicense/EnterpriseLicenseInstanceSummary.ts +6 -0
  79. package/Types/Log/LogScrubPatternType.ts +7 -0
  80. package/Types/Monitor/MonitorStep.ts +24 -4
  81. package/Types/Monitor/MonitorStepMetricViewConfigUtil.ts +105 -0
  82. package/Types/Trace/TraceScrubPatternType.ts +7 -0
  83. package/UI/Components/EditionLabel/EditionLabel.tsx +445 -12
  84. package/UI/Components/ModelTable/BaseModelTable.tsx +15 -46
  85. package/UI/Components/ModelTable/SelectFromColumns.ts +129 -0
  86. package/UI/Components/Workflow/ArgumentsForm.tsx +68 -30
  87. package/UI/Components/Workflow/CronScheduleField.tsx +411 -0
  88. package/UI/Utils/Breadcrumb/BreadcrumbTrailResolver.ts +176 -0
  89. package/UI/Utils/NotificationMethodUtil.ts +310 -0
  90. package/Utils/CronTab.ts +823 -0
  91. package/Utils/ModelImportExport.ts +21 -6
  92. package/Utils/VersionUtil.ts +260 -0
  93. package/build/dist/Models/DatabaseModels/AIInsight.js +31 -0
  94. package/build/dist/Models/DatabaseModels/AIInsight.js.map +1 -1
  95. package/build/dist/Models/DatabaseModels/EnterpriseLicense.js +22 -0
  96. package/build/dist/Models/DatabaseModels/EnterpriseLicense.js.map +1 -1
  97. package/build/dist/Models/DatabaseModels/EnterpriseLicenseInstance.js +26 -0
  98. package/build/dist/Models/DatabaseModels/EnterpriseLicenseInstance.js.map +1 -1
  99. package/build/dist/Models/DatabaseModels/GlobalConfig.js +81 -0
  100. package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
  101. package/build/dist/Models/DatabaseModels/Project.js +31 -0
  102. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  103. package/build/dist/Models/DatabaseModels/TelemetryException.js +134 -0
  104. package/build/dist/Models/DatabaseModels/TelemetryException.js.map +1 -1
  105. package/build/dist/Server/API/AIAgentDataAPI.js +97 -8
  106. package/build/dist/Server/API/AIAgentDataAPI.js.map +1 -1
  107. package/build/dist/Server/API/AIInvestigationAPI.js +55 -0
  108. package/build/dist/Server/API/AIInvestigationAPI.js.map +1 -1
  109. package/build/dist/Server/API/EnterpriseLicenseAPI.js +47 -0
  110. package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +1 -1
  111. package/build/dist/Server/API/GlobalConfigAPI.js +52 -1
  112. package/build/dist/Server/API/GlobalConfigAPI.js.map +1 -1
  113. package/build/dist/Server/EnvironmentConfig.js +17 -0
  114. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  115. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784640000000-AddExceptionTriageAndPrivacyColumns.js +33 -0
  116. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784640000000-AddExceptionTriageAndPrivacyColumns.js.map +1 -0
  117. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784659816363-AddInstanceVersionAndLatestReleaseColumns.js +18 -0
  118. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784659816363-AddInstanceVersionAndLatestReleaseColumns.js.map +1 -0
  119. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784705487674-AddEnterpriseLicenseEvaluationColumns.js +14 -0
  120. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784705487674-AddEnterpriseLicenseEvaluationColumns.js.map +1 -0
  121. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
  122. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  123. package/build/dist/Server/Services/AIRunService.js +9 -2
  124. package/build/dist/Server/Services/AIRunService.js.map +1 -1
  125. package/build/dist/Server/Services/AnalyticsDatabaseService.js +44 -16
  126. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  127. package/build/dist/Server/Services/DatabaseService.js +53 -1
  128. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  129. package/build/dist/Server/Services/TelemetryExceptionService.js +263 -22
  130. package/build/dist/Server/Services/TelemetryExceptionService.js.map +1 -1
  131. package/build/dist/Server/Services/WorkflowService.js +80 -30
  132. package/build/dist/Server/Services/WorkflowService.js.map +1 -1
  133. package/build/dist/Server/Types/Workflow/Components/Schedule.js +44 -19
  134. package/build/dist/Server/Types/Workflow/Components/Schedule.js.map +1 -1
  135. package/build/dist/Server/Utils/AI/SRE/Insights/Detectors/ExceptionSpikeDetector.js +12 -4
  136. package/build/dist/Server/Utils/AI/SRE/Insights/Detectors/ExceptionSpikeDetector.js.map +1 -1
  137. package/build/dist/Server/Utils/AI/SRE/Insights/Detectors/NewExceptionDetector.js +12 -4
  138. package/build/dist/Server/Utils/AI/SRE/Insights/Detectors/NewExceptionDetector.js.map +1 -1
  139. package/build/dist/Server/Utils/AI/SRE/Insights/InsightScanner.js +193 -22
  140. package/build/dist/Server/Utils/AI/SRE/Insights/InsightScanner.js.map +1 -1
  141. package/build/dist/Server/Utils/AI/SRE/Insights/InsightTriageRunner.js +178 -9
  142. package/build/dist/Server/Utils/AI/SRE/Insights/InsightTriageRunner.js.map +1 -1
  143. package/build/dist/Server/Utils/AI/SRE/SubjectCodeFixRun.js +37 -0
  144. package/build/dist/Server/Utils/AI/SRE/SubjectCodeFixRun.js.map +1 -1
  145. package/build/dist/Server/Utils/AI/SRE/TelemetryImprovementTaskTrigger.js +98 -0
  146. package/build/dist/Server/Utils/AI/SRE/TelemetryImprovementTaskTrigger.js.map +1 -0
  147. package/build/dist/Server/Utils/AnalyticsDatabase/InsertDedupContext.js +24 -0
  148. package/build/dist/Server/Utils/AnalyticsDatabase/InsertDedupContext.js.map +1 -0
  149. package/build/dist/Server/Utils/Express.js +12 -0
  150. package/build/dist/Server/Utils/Express.js.map +1 -1
  151. package/build/dist/Server/Utils/Telemetry/ExceptionSanitizer.js +149 -0
  152. package/build/dist/Server/Utils/Telemetry/ExceptionSanitizer.js.map +1 -0
  153. package/build/dist/Server/Utils/Telemetry/TelemetryFanInWriter.js +496 -0
  154. package/build/dist/Server/Utils/Telemetry/TelemetryFanInWriter.js.map +1 -0
  155. package/build/dist/Server/Utils/Telemetry/TelemetryWriterClient.js +165 -0
  156. package/build/dist/Server/Utils/Telemetry/TelemetryWriterClient.js.map +1 -0
  157. package/build/dist/Server/Utils/Telemetry/TelemetryWriterServer.js +136 -0
  158. package/build/dist/Server/Utils/Telemetry/TelemetryWriterServer.js.map +1 -0
  159. package/build/dist/Server/Utils/Telemetry/TelemetryWriterShedMetrics.js +76 -0
  160. package/build/dist/Server/Utils/Telemetry/TelemetryWriterShedMetrics.js.map +1 -0
  161. package/build/dist/Types/AI/CodeFixTaskType.js +38 -1
  162. package/build/dist/Types/AI/CodeFixTaskType.js.map +1 -1
  163. package/build/dist/Types/AI/ExceptionAIClassification.js +28 -0
  164. package/build/dist/Types/AI/ExceptionAIClassification.js.map +1 -0
  165. package/build/dist/Types/Log/LogScrubPatternType.js +7 -0
  166. package/build/dist/Types/Log/LogScrubPatternType.js.map +1 -1
  167. package/build/dist/Types/Monitor/MonitorStep.js +20 -4
  168. package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
  169. package/build/dist/Types/Monitor/MonitorStepMetricViewConfigUtil.js +73 -0
  170. package/build/dist/Types/Monitor/MonitorStepMetricViewConfigUtil.js.map +1 -0
  171. package/build/dist/Types/Trace/TraceScrubPatternType.js +7 -0
  172. package/build/dist/Types/Trace/TraceScrubPatternType.js.map +1 -1
  173. package/build/dist/UI/Components/EditionLabel/EditionLabel.js +261 -13
  174. package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
  175. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +12 -37
  176. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  177. package/build/dist/UI/Components/ModelTable/SelectFromColumns.js +82 -0
  178. package/build/dist/UI/Components/ModelTable/SelectFromColumns.js.map +1 -0
  179. package/build/dist/UI/Components/Workflow/ArgumentsForm.js +30 -6
  180. package/build/dist/UI/Components/Workflow/ArgumentsForm.js.map +1 -1
  181. package/build/dist/UI/Components/Workflow/CronScheduleField.js +209 -0
  182. package/build/dist/UI/Components/Workflow/CronScheduleField.js.map +1 -0
  183. package/build/dist/UI/Utils/Breadcrumb/BreadcrumbTrailResolver.js +129 -0
  184. package/build/dist/UI/Utils/Breadcrumb/BreadcrumbTrailResolver.js.map +1 -0
  185. package/build/dist/UI/Utils/NotificationMethodUtil.js +189 -0
  186. package/build/dist/UI/Utils/NotificationMethodUtil.js.map +1 -0
  187. package/build/dist/Utils/CronTab.js +609 -0
  188. package/build/dist/Utils/CronTab.js.map +1 -0
  189. package/build/dist/Utils/ModelImportExport.js +20 -6
  190. package/build/dist/Utils/ModelImportExport.js.map +1 -1
  191. package/build/dist/Utils/VersionUtil.js +193 -0
  192. package/build/dist/Utils/VersionUtil.js.map +1 -0
  193. package/package.json +1 -1
@@ -15,6 +15,7 @@ import ComponentID from "../../../../Types/Workflow/ComponentID";
15
15
  import ScheduleComponents from "../../../../Types/Workflow/Components/Schedule";
16
16
  import Workflow from "../../../../Models/DatabaseModels/Workflow";
17
17
  import CaptureSpan from "../../../Utils/Telemetry/CaptureSpan";
18
+ import logger from "../../../Utils/Logger";
18
19
 
19
20
  export default class WebhookTrigger extends TriggerCode {
20
21
  public constructor() {
@@ -52,24 +53,37 @@ export default class WebhookTrigger extends TriggerCode {
52
53
 
53
54
  // query all workflows.
54
55
  for (const workflow of workflows) {
55
- const executeWorkflow: ExecuteWorkflowType = {
56
- workflowId: new ObjectID(workflow._id!),
57
- returnValues: {},
58
- };
59
-
60
- if (
61
- workflow.triggerArguments &&
62
- workflow.triggerArguments["schedule"] &&
63
- workflow.isEnabled
64
- ) {
65
- await props.scheduleWorkflow(
66
- executeWorkflow,
67
- workflow.triggerArguments["schedule"] as string,
56
+ /*
57
+ * Isolate each workflow: a single workflow whose schedule can't be
58
+ * registered (e.g. an invalid cron, or a transient queue error) must not
59
+ * abort the startup scan and leave every other scheduled workflow
60
+ * unregistered.
61
+ */
62
+ try {
63
+ const executeWorkflow: ExecuteWorkflowType = {
64
+ workflowId: new ObjectID(workflow._id!),
65
+ returnValues: {},
66
+ };
67
+
68
+ if (
69
+ workflow.triggerArguments &&
70
+ workflow.triggerArguments["schedule"] &&
71
+ workflow.isEnabled
72
+ ) {
73
+ await props.scheduleWorkflow(
74
+ executeWorkflow,
75
+ workflow.triggerArguments["schedule"] as string,
76
+ );
77
+ }
78
+
79
+ if (!workflow.isEnabled) {
80
+ await props.removeWorkflow(workflow.id!);
81
+ }
82
+ } catch (err) {
83
+ logger.error(
84
+ `Failed to register schedule for workflow ${workflow._id}`,
68
85
  );
69
- }
70
-
71
- if (!workflow.isEnabled) {
72
- await props.removeWorkflow(workflow.id!);
86
+ logger.error(err);
73
87
  }
74
88
  }
75
89
  }
@@ -128,23 +142,34 @@ export default class WebhookTrigger extends TriggerCode {
128
142
  returnValues: {},
129
143
  };
130
144
 
131
- if (
132
- workflow.triggerArguments &&
133
- workflow.triggerArguments["schedule"] &&
134
- workflow.isEnabled
135
- ) {
136
- await this.scheduleWorkflow(
137
- executeWorkflow,
138
- workflow.triggerArguments["schedule"] as string,
139
- );
140
- }
145
+ /*
146
+ * Guard the (re)registration so a bad schedule can't fail the workflow
147
+ * save that triggered this update. Invalid crons are already surfaced as a
148
+ * workflow log by the queue layer; this catch is a backstop for unexpected
149
+ * errors (e.g. a transient queue error).
150
+ */
151
+ try {
152
+ if (
153
+ workflow.triggerArguments &&
154
+ workflow.triggerArguments["schedule"] &&
155
+ workflow.isEnabled
156
+ ) {
157
+ await this.scheduleWorkflow(
158
+ executeWorkflow,
159
+ workflow.triggerArguments["schedule"] as string,
160
+ );
161
+ }
141
162
 
142
- if (!this.removeWorkflow) {
143
- return;
144
- }
163
+ if (!this.removeWorkflow) {
164
+ return;
165
+ }
145
166
 
146
- if (!workflow.isEnabled) {
147
- await this.removeWorkflow(workflow.id!);
167
+ if (!workflow.isEnabled) {
168
+ await this.removeWorkflow(workflow.id!);
169
+ }
170
+ } catch (err) {
171
+ logger.error(`Failed to update schedule for workflow ${workflow._id}`);
172
+ logger.error(err);
148
173
  }
149
174
  }
150
175
  }
@@ -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
- exception.message,
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 (exception.message) {
259
- detailLines.push(`- Message: ${exception.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: exception.message,
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
- exception.message,
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 (exception.message) {
184
- detailLines.push(`- Message: ${exception.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: exception.message,
218
+ exceptionMessage: safeMessage,
208
219
  exceptionType: exception.exceptionType,
209
220
  totalOccurrenceCount: occurrenceCount,
210
221
  firstSeenAt: firstSeenAtIso,
@@ -11,6 +11,12 @@ import InsightStore, { UpsertCandidatesResult } from "./InsightStore";
11
11
  import InsightFixRouting, { InsightFixRoutingResult } from "./FixRouting";
12
12
  import InsightTriage, { InsightTriageResult } from "./Triage";
13
13
  import { InsightCandidate, InsightDetector, InsightScanContext } from "./Types";
14
+ import AIInsightType from "../../../../../Types/AI/AIInsightType";
15
+ import AIRunType from "../../../../../Types/AI/AIRunType";
16
+ import AIRunStatus from "../../../../../Types/AI/AIRunStatus";
17
+ import ExceptionAIClassification from "../../../../../Types/AI/ExceptionAIClassification";
18
+ import AIRunService from "../../../../Services/AIRunService";
19
+ import QueryHelper from "../../../../Types/Database/QueryHelper";
14
20
  import logger from "../../../Logger";
15
21
  import CaptureSpan from "../../../Telemetry/CaptureSpan";
16
22
 
@@ -19,8 +25,9 @@ import CaptureSpan from "../../../Telemetry/CaptureSpan";
19
25
  *
20
26
  * THE RULE: no LLM here. Detectors are deterministic statistical sensors;
21
27
  * the LLM only engages per-finding AFTERWARDS through InsightTriage
22
- * (budgeted, read-only), and the fix decision inside InsightFixRouting is
23
- * deterministic and reuses the existing budgeted CodeFix creation paths.
28
+ * (budgeted, read-only). The automatic fix decision happens after triage
29
+ * completes (InsightTriageRunner InsightFixRouting) and only for
30
+ * insights the triage classified as code faults.
24
31
  *
25
32
  * Everything is opt-in and quiet: only projects with the default-false
26
33
  * enableAiInsights flag are scanned, insights never page and never
@@ -73,8 +80,8 @@ export default class InsightScanner {
73
80
  * One project's scan: self-heal any insight an earlier tick left unrouted,
74
81
  * run every registered detector (isolated — one failing sensor must not
75
82
  * blind the others), dedupe/upsert the candidates through InsightStore,
76
- * then route each NEWLY created insight to FixOpened or ActionRequired and
77
- * enqueue its triage.
83
+ * then route each NEWLY created insight to ActionRequired and enqueue its
84
+ * triage (which, on a code-fault verdict, routes the fix).
78
85
  */
79
86
  @CaptureSpan()
80
87
  public static async scanProjectForInsights(project: Project): Promise<void> {
@@ -211,19 +218,200 @@ export default class InsightScanner {
211
218
  );
212
219
  }
213
220
  }
221
+
222
+ await this.resweepUntriagedInsights({ projectId });
223
+ await this.resweepUnroutedCodeFaults({ projectId, project });
224
+ }
225
+
226
+ /*
227
+ * Stranded shape 2: ActionRequired insights that never got a triage
228
+ * verdict. With triage gating the automatic fix, a triage that was
229
+ * quiet-skipped (token budget exhausted), expired in the queue, or
230
+ * failed would otherwise strand the insight verdict-less FOREVER — and
231
+ * with it any automatic fix. Re-enqueueing is safe and cheap: the
232
+ * triage enqueue dedupes to one non-terminal run per insight and
233
+ * quiet-skips when the budget still has no headroom. Bounded two ways:
234
+ * only insights from the last 7 days (older rows predate the verdict
235
+ * gate or were consciously left alone), and only while fewer than 3
236
+ * triage attempts have ended in failure (a permanently failing triage
237
+ * must not retry every 15 minutes for a week).
238
+ */
239
+ private static async resweepUntriagedInsights(data: {
240
+ projectId: ObjectID;
241
+ }): Promise<void> {
242
+ const { projectId } = data;
243
+
244
+ try {
245
+ const untriaged: Array<AIInsight> = await AIInsightService.findBy({
246
+ query: {
247
+ projectId: projectId,
248
+ status: AIInsightStatus.ActionRequired,
249
+ triageCompletedAt: QueryHelper.isNull(),
250
+ createdAt: QueryHelper.greaterThanEqualTo(
251
+ OneUptimeDate.addRemoveDays(OneUptimeDate.getCurrentDate(), -7),
252
+ ),
253
+ },
254
+ select: {
255
+ _id: true,
256
+ projectId: true,
257
+ insightType: true,
258
+ serviceName: true,
259
+ traceId: true,
260
+ telemetryExceptionId: true,
261
+ evidence: true,
262
+ },
263
+ limit: LIMIT_MAX,
264
+ skip: 0,
265
+ props: { isRoot: true },
266
+ });
267
+
268
+ for (const insight of untriaged) {
269
+ try {
270
+ const failedTriageAttempts: number = (
271
+ await AIRunService.countBy({
272
+ query: {
273
+ runType: AIRunType.Investigation,
274
+ triggeredByAiInsightId: insight.id!,
275
+ status: QueryHelper.any([
276
+ AIRunStatus.Error,
277
+ AIRunStatus.Cancelled,
278
+ AIRunStatus.Stale,
279
+ ]),
280
+ },
281
+ props: { isRoot: true },
282
+ })
283
+ ).toNumber();
284
+
285
+ if (failedTriageAttempts >= 3) {
286
+ continue;
287
+ }
288
+
289
+ const triageResult: InsightTriageResult =
290
+ await InsightTriage.enqueueInsightTriage({ insight: insight });
291
+
292
+ if (triageResult.triageAiRunId) {
293
+ await AIInsightService.updateOneById({
294
+ id: insight.id!,
295
+ data: {
296
+ triageAiRunId: triageResult.triageAiRunId,
297
+ },
298
+ props: { isRoot: true },
299
+ });
300
+ }
301
+ } catch (error) {
302
+ logger.error(
303
+ `AI Insights: triage re-enqueue failed for insight ${insight.id?.toString()} — continuing: ${error}`,
304
+ );
305
+ }
306
+ }
307
+ } catch (error) {
308
+ logger.error(
309
+ `AI Insights: failed to sweep untriaged insights for project ${projectId.toString()} — continuing with the scan: ${error}`,
310
+ );
311
+ }
312
+ }
313
+
314
+ /*
315
+ * Stranded shape 3: a code-fault verdict was recorded but the fix was
316
+ * never routed — the pod died between the verdict write and the fix
317
+ * routing, or routing quiet-skipped on a transient gate (daily fix
318
+ * budget, readiness). Re-routing is idempotent: the creation path's
319
+ * per-(exception, recipe) dedupe and the cross-group guard refuse
320
+ * duplicates, and the conditional status flip only moves
321
+ * ActionRequired → FixOpened. Bounded to verdicts from the last 24
322
+ * hours so a permanently refused group (declined by a human, capped
323
+ * repo) does not retry forever. Latency insights are excluded — they
324
+ * are fix-routed deterministically at scan time.
325
+ */
326
+ private static async resweepUnroutedCodeFaults(data: {
327
+ projectId: ObjectID;
328
+ project: Project;
329
+ }): Promise<void> {
330
+ const { projectId, project } = data;
331
+
332
+ try {
333
+ const unrouted: Array<AIInsight> = await AIInsightService.findBy({
334
+ query: {
335
+ projectId: projectId,
336
+ status: AIInsightStatus.ActionRequired,
337
+ classification: ExceptionAIClassification.CodeFault,
338
+ fixAiRunId: QueryHelper.isNull(),
339
+ triageCompletedAt: QueryHelper.greaterThanEqualTo(
340
+ OneUptimeDate.addRemoveDays(OneUptimeDate.getCurrentDate(), -1),
341
+ ),
342
+ },
343
+ select: {
344
+ _id: true,
345
+ projectId: true,
346
+ insightType: true,
347
+ serviceName: true,
348
+ traceId: true,
349
+ telemetryExceptionId: true,
350
+ evidence: true,
351
+ },
352
+ limit: LIMIT_MAX,
353
+ skip: 0,
354
+ props: { isRoot: true },
355
+ });
356
+
357
+ for (const insight of unrouted) {
358
+ if (insight.insightType === AIInsightType.TraceLatencyRegression) {
359
+ continue;
360
+ }
361
+
362
+ try {
363
+ const fixResult: InsightFixRoutingResult =
364
+ await InsightFixRouting.routeInsightFix({
365
+ insight: insight,
366
+ project: project,
367
+ });
368
+
369
+ if (fixResult.fixAiRunId) {
370
+ await AIInsightService.updateOneBy({
371
+ query: {
372
+ _id: insight.id!.toString(),
373
+ status: AIInsightStatus.ActionRequired,
374
+ },
375
+ data: {
376
+ status: AIInsightStatus.FixOpened,
377
+ fixAiRunId: fixResult.fixAiRunId,
378
+ },
379
+ props: { isRoot: true },
380
+ });
381
+ }
382
+ } catch (error) {
383
+ logger.error(
384
+ `AI Insights: fix re-routing failed for insight ${insight.id?.toString()} — continuing: ${error}`,
385
+ );
386
+ }
387
+ }
388
+ } catch (error) {
389
+ logger.error(
390
+ `AI Insights: failed to sweep unrouted code-fault insights for project ${projectId.toString()} — continuing with the scan: ${error}`,
391
+ );
392
+ }
214
393
  }
215
394
 
216
395
  /*
217
396
  * Route one insight — newly created this tick, or swept up as stranded
218
- * from an earlier one — out of the defensive Detected state:
219
- * 1. deterministic fix routing (gated on enableAi + enableInsightFixTasks
220
- * inside InsightFixRouting) FixOpened + fixAiRunId, else
221
- * ActionRequired;
222
- * 2. triage enqueue for EVERY new insight triage enriches fix and
223
- * non-fix insights alike triageAiRunId when a run was enqueued.
224
- * Both collaborators are never-throws by contract, but they are wrapped
225
- * anyway: a contract breach must degrade (no fix / no triage), not leave
226
- * the insight stuck in Detected or fail the scan.
397
+ * from an earlier one — out of the defensive Detected state.
398
+ *
399
+ * TRIAGE GATES THE FIX. The automatic fix decision no longer happens
400
+ * here: every new insight goes to ActionRequired and gets its triage
401
+ * enqueued; when the triage run completes with a code-fault
402
+ * classification, InsightTriageRunner routes the fix (via
403
+ * InsightFixRouting) and flips the insight to FixOpened. This exists
404
+ * because roughly half of real exception insights turn out to be
405
+ * expected user errors or intentional denials (auth, paywalls, scanner
406
+ * probes) — opening an unreviewed fix PR for those is worse than
407
+ * opening none: the agent's only "fix" is to weaken the check that
408
+ * correctly fired. Fail-closed by design: no triage verdict, no
409
+ * automatic PR. The manual "Fix with AI" button on the exception page
410
+ * is unaffected.
411
+ *
412
+ * The collaborator is never-throws by contract, but it is wrapped
413
+ * anyway: a contract breach must degrade (no triage), not leave the
414
+ * insight stuck in Detected or fail the scan.
227
415
  */
228
416
  private static async routeNewInsight(data: {
229
417
  insight: AIInsight;
@@ -231,19 +419,29 @@ export default class InsightScanner {
231
419
  }): Promise<void> {
232
420
  const { insight, project } = data;
233
421
 
422
+ /*
423
+ * TraceLatencyRegression keeps DETERMINISTIC scan-time fix routing:
424
+ * its evidence is the SpanTreeAnalyzer's own findings (N+1, dominant
425
+ * span, sequential fan-out) — no LLM verdict adds signal there, and
426
+ * the exception-centric classification taxonomy would mislabel most
427
+ * latency regressions as "infrastructure". Exception insights go
428
+ * through the verdict gate instead.
429
+ */
234
430
  let fixAiRunId: ObjectID | undefined = undefined;
235
431
 
236
- try {
237
- const fixResult: InsightFixRoutingResult =
238
- await InsightFixRouting.routeInsightFix({
239
- insight: insight,
240
- project: project,
241
- });
242
- fixAiRunId = fixResult.fixAiRunId;
243
- } catch (error) {
244
- logger.error(
245
- `AI Insights: fix routing threw for insight ${insight.id?.toString()} (treating as no fix opened): ${error}`,
246
- );
432
+ if (insight.insightType === AIInsightType.TraceLatencyRegression) {
433
+ try {
434
+ const fixResult: InsightFixRoutingResult =
435
+ await InsightFixRouting.routeInsightFix({
436
+ insight: insight,
437
+ project: project,
438
+ });
439
+ fixAiRunId = fixResult.fixAiRunId;
440
+ } catch (error) {
441
+ logger.error(
442
+ `AI Insights: fix routing threw for latency insight ${insight.id?.toString()} (treating as no fix opened): ${error}`,
443
+ );
444
+ }
247
445
  }
248
446
 
249
447
  if (fixAiRunId) {