@oneuptime/common 14.0.0 → 14.0.1

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 (196) hide show
  1. package/Models/DatabaseModels/Index.ts +4 -0
  2. package/Models/DatabaseModels/ServiceLevelObjectiveFeed.ts +2 -0
  3. package/Models/DatabaseModels/ServiceLevelObjectiveLabelRule.ts +522 -0
  4. package/Models/DatabaseModels/ServiceLevelObjectiveMonitorRule.ts +38 -4
  5. package/Models/DatabaseModels/ServiceLevelObjectiveOwnerRule.ts +603 -0
  6. package/Server/Infrastructure/Postgres/SchemaMigrations/1794100000000-AddSloLabelAndOwnerRules.ts +248 -0
  7. package/Server/Infrastructure/Postgres/SchemaMigrations/1794200000000-AddSloMonitorRuleMonitorType.ts +19 -0
  8. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  9. package/Server/Services/DatabaseService.ts +10 -0
  10. package/Server/Services/Index.ts +4 -0
  11. package/Server/Services/MonitorService.ts +3 -2
  12. package/Server/Services/ServiceLevelObjectiveLabelRuleEngineService.ts +385 -0
  13. package/Server/Services/ServiceLevelObjectiveLabelRuleService.ts +51 -0
  14. package/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.ts +92 -16
  15. package/Server/Services/ServiceLevelObjectiveMonitorRuleService.ts +36 -1
  16. package/Server/Services/ServiceLevelObjectiveOwnerRuleEngineService.ts +521 -0
  17. package/Server/Services/ServiceLevelObjectiveOwnerRuleService.ts +51 -0
  18. package/Server/Services/ServiceLevelObjectiveService.ts +28 -0
  19. package/Server/Types/Workflow/Components/Telegram/SendMessageToChat.ts +27 -1
  20. package/Server/Utils/AI/Toolbox/SloTools.ts +1 -0
  21. package/Server/Utils/Monitor/AffectedResourceList.ts +31 -73
  22. package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +1 -1
  23. package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +1 -1
  24. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +181 -136
  25. package/Server/Utils/Monitor/RootCauseList.ts +139 -0
  26. package/Server/Utils/Rules/RuleRun/RuleRunRegistry.ts +28 -0
  27. package/Server/Utils/Slo/SloRulePatternValidator.ts +42 -0
  28. package/Tests/App/Dashboard/FeedFilterSortIntegration.test.tsx +1632 -0
  29. package/Tests/App/Dashboard/FeedOptionsLocales.test.ts +250 -0
  30. package/Tests/App/Dashboard/FeedOrdering.test.ts +279 -9
  31. package/Tests/App/Dashboard/InvestigationFeedRefresh.test.tsx +60 -1
  32. package/Tests/App/Dashboard/InvestigationPanel.test.tsx +594 -3
  33. package/Tests/App/Dashboard/ProductOrientation.test.tsx +646 -0
  34. package/Tests/App/Dashboard/ResourceFeed.test.tsx +612 -2
  35. package/Tests/App/Dashboard/ResourceFeedPageWiring.test.ts +30 -0
  36. package/Tests/App/Dashboard/ResourceFeedPagesFilterSort.test.tsx +1202 -0
  37. package/Tests/App/Dashboard/ScheduledMaintenanceFeedRefresh.test.tsx +52 -1
  38. package/Tests/App/Dashboard/SloFeed.test.tsx +106 -1
  39. package/Tests/App/Dashboard/SloMonitorRuleCriteria.test.tsx +566 -0
  40. package/Tests/App/Dashboard/SloNavigationWiring.test.tsx +6 -4
  41. package/Tests/App/Dashboard/SloNoticeBanner.test.tsx +186 -0
  42. package/Tests/App/Dashboard/SloOverviewKpiStrip.test.tsx +388 -44
  43. package/Tests/App/Dashboard/SloOverviewLayout.test.tsx +613 -0
  44. package/Tests/Models/DatabaseModels/ResourceFeedModels.test.ts +0 -8
  45. package/Tests/Models/DatabaseModels/SloLabelAndOwnerRuleModels.test.ts +404 -0
  46. package/Tests/Models/DatabaseModels/SloOverhaulModels.test.ts +17 -1
  47. package/Tests/Models/DatabaseModels/StaticRuleCriteriaInheritance.test.ts +4 -2
  48. package/Tests/Server/Infrastructure/Postgres/AddConfigurableRuleCriteriaMigration.test.ts +3 -0
  49. package/Tests/Server/Infrastructure/Postgres/AddSloLabelAndOwnerRulesMigration.test.ts +407 -0
  50. package/Tests/Server/Infrastructure/Postgres/AddSloMonitorRuleMonitorTypeMigration.test.ts +171 -0
  51. package/Tests/Server/Infrastructure/Postgres/SloProductOverhaulMigration.test.ts +88 -4
  52. package/Tests/Server/Services/DatabaseServiceRuleCriteriaValidation.test.ts +104 -0
  53. package/Tests/Server/Services/RuleRun/LabelRuleEnginesGroupBRun.test.ts +48 -1
  54. package/Tests/Server/Services/RuleRun/OwnerRuleEnginesGroupBRun.test.ts +30 -0
  55. package/Tests/Server/Services/ServiceLevelObjectiveLabelAndOwnerRuleEngines.test.ts +1170 -0
  56. package/Tests/Server/Services/ServiceLevelObjectiveLabelAndOwnerRuleServices.test.ts +172 -0
  57. package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.test.ts +323 -0
  58. package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleService.test.ts +115 -0
  59. package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleTriggers.test.ts +21 -2
  60. package/Tests/Server/Services/ServiceLevelObjectiveMonitorTypePostgres.test.ts +480 -0
  61. package/Tests/Server/Services/ServiceLevelObjectiveService.test.ts +162 -0
  62. package/Tests/Server/Services/StaticRuleCriteriaRuntimeCoverage.test.ts +45 -6
  63. package/Tests/Server/Types/Database/FeedEventTypeFilterQuery.test.ts +745 -0
  64. package/Tests/Server/Types/Markdown.test.ts +2 -1
  65. package/Tests/Server/Types/Workflow/Components/ApiComponentSsrf.test.ts +49 -0
  66. package/Tests/Server/Types/Workflow/Components/TelegramSendMessageToChat.test.ts +331 -0
  67. package/Tests/Server/Utils/AI/SloTools.test.ts +10 -0
  68. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteriaAnomalyUnits.test.ts +1 -1
  69. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluator.test.ts +405 -0
  70. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorBreachingSamples.test.ts +1101 -0
  71. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorMetricUnits.test.ts +106 -80
  72. package/Tests/Server/Utils/Monitor/RootCauseList.test.ts +252 -0
  73. package/Tests/Server/Utils/Rules/RuleRun/RuleRunPermission.test.ts +154 -0
  74. package/Tests/Server/Utils/SSRFProtection.test.ts +111 -0
  75. package/Tests/Server/Utils/Slo/SloRulePatternValidator.test.ts +67 -0
  76. package/Tests/Server/Utils/VM/VMRunnerSsrf.test.ts +213 -0
  77. package/Tests/Types/API/Route.test.ts +56 -0
  78. package/Tests/Types/API/URLSchemeLikePath.test.ts +304 -0
  79. package/Tests/UI/Components/FeedOptions.test.ts +1404 -0
  80. package/Tests/UI/Components/FeedOptionsButton.test.tsx +2603 -0
  81. package/Tests/UI/Components/Icon.test.tsx +26 -0
  82. package/Tests/UI/Components/RuleCriteria/RuleCriteriaModelTable.test.tsx +3 -3
  83. package/Tests/UI/Components/useFeedItemsViewKey.test.tsx +1017 -0
  84. package/Tests/UI/Components/useFeedOptions.test.tsx +1107 -0
  85. package/Tests/Utils/LabelRuleImportExport.test.ts +102 -0
  86. package/Tests/Utils/Slo/SloDuration.test.ts +115 -24
  87. package/Tests/Utils/Slo/SloHealth.test.ts +93 -10
  88. package/Tests/Utils/Slo/SloMonitorRuleCriteria.test.ts +109 -0
  89. package/Tests/Utils/Slo/SloOverviewText.test.ts +64 -0
  90. package/Types/API/Route.ts +19 -2
  91. package/Types/API/URL.ts +26 -2
  92. package/Types/Monitor/MetricMonitor/MetricCriteriaContext.ts +6 -5
  93. package/Types/Monitor/MetricMonitor/MetricMonitorResponse.ts +9 -0
  94. package/Types/Permission.ts +90 -0
  95. package/Types/Rules/RuleCriteriaFieldRegistry.ts +11 -0
  96. package/Types/Rules/RuleRun.ts +4 -0
  97. package/UI/Components/Feed/FeedOptions.ts +427 -0
  98. package/UI/Components/Feed/FeedOptionsButton.tsx +692 -0
  99. package/UI/Components/Feed/useFeedItems.ts +28 -9
  100. package/UI/Components/Feed/useFeedOptions.ts +225 -0
  101. package/UI/Components/Icon/Icon.tsx +1 -1
  102. package/Utils/LabelRuleImportExport.ts +27 -9
  103. package/Utils/Monitor/MetricValueFormatter.ts +2 -2
  104. package/Utils/Rules/MonitorTypeRuleCriteria.ts +31 -0
  105. package/Utils/Slo/SloDuration.ts +22 -6
  106. package/Utils/Slo/SloHealth.ts +8 -7
  107. package/Utils/Slo/SloMonitorRuleCriteria.ts +33 -1
  108. package/Utils/Slo/SloOverviewText.ts +33 -0
  109. package/Utils/Slo/SloProjection.ts +4 -3
  110. package/build/dist/Models/DatabaseModels/Index.js +4 -0
  111. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  112. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveFeed.js +2 -0
  113. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveFeed.js.map +1 -1
  114. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveLabelRule.js +530 -0
  115. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveLabelRule.js.map +1 -0
  116. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveMonitorRule.js +39 -4
  117. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveMonitorRule.js.map +1 -1
  118. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveOwnerRule.js +612 -0
  119. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveOwnerRule.js.map +1 -0
  120. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794100000000-AddSloLabelAndOwnerRules.js +93 -0
  121. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794100000000-AddSloLabelAndOwnerRules.js.map +1 -0
  122. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794200000000-AddSloMonitorRuleMonitorType.js +12 -0
  123. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794200000000-AddSloMonitorRuleMonitorType.js.map +1 -0
  124. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  125. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  126. package/build/dist/Server/Services/DatabaseService.js +7 -0
  127. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  128. package/build/dist/Server/Services/Index.js +4 -0
  129. package/build/dist/Server/Services/Index.js.map +1 -1
  130. package/build/dist/Server/Services/MonitorService.js +3 -2
  131. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  132. package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleEngineService.js +295 -0
  133. package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleEngineService.js.map +1 -0
  134. package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleService.js +56 -0
  135. package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleService.js.map +1 -0
  136. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js +59 -14
  137. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js.map +1 -1
  138. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleService.js +22 -1
  139. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleService.js.map +1 -1
  140. package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleEngineService.js +400 -0
  141. package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleEngineService.js.map +1 -0
  142. package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleService.js +56 -0
  143. package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleService.js.map +1 -0
  144. package/build/dist/Server/Services/ServiceLevelObjectiveService.js +21 -0
  145. package/build/dist/Server/Services/ServiceLevelObjectiveService.js.map +1 -1
  146. package/build/dist/Server/Types/Workflow/Components/Telegram/SendMessageToChat.js +20 -1
  147. package/build/dist/Server/Types/Workflow/Components/Telegram/SendMessageToChat.js.map +1 -1
  148. package/build/dist/Server/Utils/AI/Toolbox/SloTools.js +1 -0
  149. package/build/dist/Server/Utils/AI/Toolbox/SloTools.js.map +1 -1
  150. package/build/dist/Server/Utils/Monitor/AffectedResourceList.js +22 -51
  151. package/build/dist/Server/Utils/Monitor/AffectedResourceList.js.map +1 -1
  152. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +1 -1
  153. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +1 -1
  154. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +139 -109
  155. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  156. package/build/dist/Server/Utils/Monitor/RootCauseList.js +101 -0
  157. package/build/dist/Server/Utils/Monitor/RootCauseList.js.map +1 -0
  158. package/build/dist/Server/Utils/Rules/RuleRun/RuleRunRegistry.js +28 -0
  159. package/build/dist/Server/Utils/Rules/RuleRun/RuleRunRegistry.js.map +1 -1
  160. package/build/dist/Server/Utils/Slo/SloRulePatternValidator.js +28 -0
  161. package/build/dist/Server/Utils/Slo/SloRulePatternValidator.js.map +1 -0
  162. package/build/dist/Types/API/Route.js +18 -2
  163. package/build/dist/Types/API/Route.js.map +1 -1
  164. package/build/dist/Types/API/URL.js +23 -2
  165. package/build/dist/Types/API/URL.js.map +1 -1
  166. package/build/dist/Types/Permission.js +84 -0
  167. package/build/dist/Types/Permission.js.map +1 -1
  168. package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js +11 -0
  169. package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js.map +1 -1
  170. package/build/dist/Types/Rules/RuleRun.js +4 -0
  171. package/build/dist/Types/Rules/RuleRun.js.map +1 -1
  172. package/build/dist/UI/Components/Feed/FeedOptions.js +235 -0
  173. package/build/dist/UI/Components/Feed/FeedOptions.js.map +1 -0
  174. package/build/dist/UI/Components/Feed/FeedOptionsButton.js +346 -0
  175. package/build/dist/UI/Components/Feed/FeedOptionsButton.js.map +1 -0
  176. package/build/dist/UI/Components/Feed/useFeedItems.js +17 -7
  177. package/build/dist/UI/Components/Feed/useFeedItems.js.map +1 -1
  178. package/build/dist/UI/Components/Feed/useFeedOptions.js +130 -0
  179. package/build/dist/UI/Components/Feed/useFeedOptions.js.map +1 -0
  180. package/build/dist/UI/Components/Icon/Icon.js +1 -1
  181. package/build/dist/UI/Components/Icon/Icon.js.map +1 -1
  182. package/build/dist/Utils/LabelRuleImportExport.js +22 -6
  183. package/build/dist/Utils/LabelRuleImportExport.js.map +1 -1
  184. package/build/dist/Utils/Monitor/MetricValueFormatter.js +2 -2
  185. package/build/dist/Utils/Rules/MonitorTypeRuleCriteria.js +18 -0
  186. package/build/dist/Utils/Rules/MonitorTypeRuleCriteria.js.map +1 -0
  187. package/build/dist/Utils/Slo/SloDuration.js +13 -5
  188. package/build/dist/Utils/Slo/SloDuration.js.map +1 -1
  189. package/build/dist/Utils/Slo/SloHealth.js +8 -7
  190. package/build/dist/Utils/Slo/SloHealth.js.map +1 -1
  191. package/build/dist/Utils/Slo/SloMonitorRuleCriteria.js +16 -1
  192. package/build/dist/Utils/Slo/SloMonitorRuleCriteria.js.map +1 -1
  193. package/build/dist/Utils/Slo/SloOverviewText.js +16 -0
  194. package/build/dist/Utils/Slo/SloOverviewText.js.map +1 -1
  195. package/build/dist/Utils/Slo/SloProjection.js.map +1 -1
  196. package/package.json +1 -1
@@ -27,6 +27,7 @@ import MonitorCriteriaMessageBuilder from "./MonitorCriteriaMessageBuilder";
27
27
  import MonitorCriteriaDataExtractor from "./MonitorCriteriaDataExtractor";
28
28
  import MonitorCriteriaMessageFormatter from "./MonitorCriteriaMessageFormatter";
29
29
  import AffectedResourceList from "./AffectedResourceList";
30
+ import RootCauseList from "./RootCauseList";
30
31
  import MonitorCriteriaInstance from "../../../Types/Monitor/MonitorCriteriaInstance";
31
32
  import MonitorStep from "../../../Types/Monitor/MonitorStep";
32
33
  import FilterCondition from "../../../Types/Filter/FilterCondition";
@@ -1114,9 +1115,7 @@ ${contextBlock}
1114
1115
  samples: breachingSamples,
1115
1116
  totalSamples: ctx.totalSamplesInWindow,
1116
1117
  unit: ctx.unit,
1117
- metricName: ctx.metricName,
1118
1118
  unitHeuristicMetricName: unitHeuristicMetricName,
1119
- alias: ctx.alias,
1120
1119
  components: ctx.components || [],
1121
1120
  })}`);
1122
1121
  }
@@ -1152,24 +1151,41 @@ ${contextBlock}
1152
1151
  return sections.join("\n");
1153
1152
  }
1154
1153
  /**
1155
- * Build the **Breaching Samples** markdown section — a table of
1156
- * timestamps, values, and any group-by attributes. Caps the row count
1157
- * so a 30-minute window at 1-second granularity doesn't dump thousands
1158
- * of lines onto the root-cause page; the caller can always drill in
1159
- * via the metric explorer link.
1154
+ * Build the **Breaching Samples** markdown section — a numbered list of
1155
+ * the samples that breached, oldest first. Caps the list so a 30-minute
1156
+ * window at 1-second granularity doesn't dump thousands of lines onto
1157
+ * the root-cause page; the caller can always drill in via the metric
1158
+ * explorer link.
1159
+ *
1160
+ * 1. `2026-08-14T10:30:00.000Z` — **1.07 GB**
1161
+ * - `a`: 537 MB
1162
+ * - `b`: 1.5 sec
1163
+ * - `k8s.pod.name`: `web-1`
1164
+ *
1165
+ * It used to be a table — Timestamp | Metric | Alias | Value, a column
1166
+ * per formula component and one per attribute key — which grew wider
1167
+ * with every attribute and read badly in the email card, in Slack and on
1168
+ * the dashboard (see RootCauseList). Its Metric and Alias columns
1169
+ * repeated the same two values on every row, the ones the Metric
1170
+ * Details list directly above already states, so the list leaves them
1171
+ * out; and a component or attribute a sample has no value for is left
1172
+ * out rather than printed as "-".
1160
1173
  *
1161
1174
  * Timestamps are emitted as inline code wrapping ISO 8601 strings so
1162
1175
  * the client-side markdown viewer can localize them to the viewer's
1163
1176
  * timezone (without losing the canonical instant).
1164
1177
  */
1165
1178
  static formatBreachingSamplesSection(input) {
1166
- const MAX_ROWS = 20;
1179
+ const MAX_SAMPLES_SHOWN = 20;
1167
1180
  // Sort chronologically and de-duplicate any accidental repeats
1168
1181
  const sorted = [...input.samples].sort((a, b) => {
1169
1182
  return (new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
1170
1183
  });
1171
- const displayedSamples = sorted.slice(0, MAX_ROWS);
1172
- // Collect attribute keys that appear on any displayed sample
1184
+ const displayedSamples = sorted.slice(0, MAX_SAMPLES_SHOWN);
1185
+ /*
1186
+ * Collect attribute keys that appear on any displayed sample, so every
1187
+ * item lists the attributes it has in the same order.
1188
+ */
1173
1189
  const attrKeySet = new Set();
1174
1190
  for (const s of displayedSamples) {
1175
1191
  for (const k of Object.keys(s.attributes || {})) {
@@ -1177,94 +1193,64 @@ ${contextBlock}
1177
1193
  }
1178
1194
  }
1179
1195
  const attrKeys = Array.from(attrKeySet);
1180
- /*
1181
- * Escape pipe characters that could appear in the metric display
1182
- * name (formulas like "a | b" are unlikely but possible) so they
1183
- * don't break GitHub-flavored-markdown tables.
1184
- */
1185
- const escapeCell = (value) => {
1186
- return value.replace(/\|/g, "\\|");
1187
- };
1188
- /*
1189
- * Column layout:
1190
- * Timestamp | Metric | Alias | Value | <component_1> | ... | <attr_1> | ...
1191
- *
1192
- * The component columns let the reader see what each variable of a
1193
- * formula resolved to at the breach time — e.g. "when c = a + b
1194
- * breached 100, a was 55, b was 46". They are omitted for plain
1195
- * metric criteria.
1196
- */
1197
- const headerCells = [
1198
- "Timestamp",
1199
- "Metric",
1200
- "Alias",
1201
- "Value",
1202
- ];
1203
- /*
1204
- * The component columns carry no unit in their header any more. Each
1205
- * cell now names the unit it actually landed on, and auto-scaling
1206
- * means neighbouring rows can land on different ones — a header that
1207
- * read "a (By)" over cells reading "900 KB" and "1.2 MB" states a
1208
- * unit that neither row uses. The configured unit is not lost: the
1209
- * Components bullet list above the table still records it.
1210
- */
1211
- for (const component of input.components) {
1212
- headerCells.push(component.alias);
1213
- }
1214
- headerCells.push(...attrKeys);
1215
- const headerRow = `| ${headerCells.join(" | ")} |`;
1216
- const dividerRow = `| ${headerCells
1217
- .map(() => {
1218
- return "---";
1219
- })
1220
- .join(" | ")} |`;
1221
- const metricCell = `\`${escapeCell(input.metricName)}\``;
1222
- const aliasCell = input.alias
1223
- ? `\`${escapeCell(input.alias)}\``
1224
- : "-";
1225
- const dataRows = displayedSamples.map((s) => {
1226
- const timestampIso = new Date(s.timestamp).toISOString();
1227
- const cells = [
1228
- `\`${timestampIso}\``,
1229
- metricCell,
1230
- aliasCell,
1231
- MetricValueFormatter.format({
1232
- value: s.value,
1233
- unit: input.unit,
1234
- metricName: input.unitHeuristicMetricName,
1235
- }),
1236
- ];
1196
+ const items = displayedSamples.map((s) => {
1197
+ const details = [];
1198
+ /*
1199
+ * What each variable of a formula resolved to at the breach time —
1200
+ * e.g. "when c = a + b breached 100, a was 55, b was 46". Absent
1201
+ * for plain metric criteria.
1202
+ */
1237
1203
  for (const component of input.components) {
1238
1204
  const match = (s.componentValues || []).find((cv) => {
1239
1205
  return cv.alias === component.alias;
1240
1206
  });
1241
- if (match && typeof match.value === "number") {
1242
- /*
1243
- * Each component keeps its OWN unit. Component values are
1244
- * indexed off the per-query series and never go through the
1245
- * sample→threshold-unit conversion the main Value column
1246
- * does, so they are in the component's legendUnit — mixing
1247
- * units across the row is deliberate, and is why every cell
1248
- * has to say which one it is in.
1249
- */
1250
- cells.push(MetricValueFormatter.format({
1207
+ if (!match || typeof match.value !== "number") {
1208
+ continue;
1209
+ }
1210
+ /*
1211
+ * Each component keeps its OWN unit. Component values are
1212
+ * indexed off the per-query series and never go through the
1213
+ * sample→threshold-unit conversion the sample's value does, so
1214
+ * they are in the component's legendUnit — which is why each
1215
+ * one names the unit it is in. Its configured unit is in the
1216
+ * Components list of the Metric Details above.
1217
+ */
1218
+ details.push({
1219
+ label: RootCauseList.code(component.alias),
1220
+ value: MetricValueFormatter.format({
1251
1221
  value: match.value,
1252
1222
  unit: component.unit,
1253
1223
  metricName: MonitorCriteriaEvaluator.metricNameForUnitHeuristics({
1254
1224
  metricName: component.name,
1255
1225
  isFormula: component.isFormula,
1256
1226
  }),
1257
- }));
1258
- }
1259
- else {
1260
- cells.push("-");
1261
- }
1227
+ }),
1228
+ });
1262
1229
  }
1230
+ /*
1231
+ * The attributes identify the series the sample came from. Keys
1232
+ * and values are telemetry, so both go in code spans:
1233
+ * RootCauseList.code keeps whatever they contain inside the span.
1234
+ */
1263
1235
  for (const k of attrKeys) {
1264
1236
  const v = s.attributes[k];
1265
- cells.push(v === undefined || v === null ? "-" : String(v));
1237
+ if (v === undefined || v === null) {
1238
+ continue;
1239
+ }
1240
+ details.push({
1241
+ label: RootCauseList.code(k),
1242
+ value: RootCauseList.code(String(v)),
1243
+ });
1266
1244
  }
1267
- return `| ${cells.join(" | ")} |`;
1245
+ return {
1246
+ title: RootCauseList.code(new Date(s.timestamp).toISOString()),
1247
+ value: `**${MetricValueFormatter.format({
1248
+ value: s.value,
1249
+ unit: input.unit,
1250
+ metricName: input.unitHeuristicMetricName,
1251
+ })}**`,
1252
+ details: details,
1253
+ };
1268
1254
  });
1269
1255
  const lines = [
1270
1256
  `**Breaching Samples**`,
@@ -1273,12 +1259,14 @@ ${contextBlock}
1273
1259
  totalSamples: input.totalSamples,
1274
1260
  }),
1275
1261
  "",
1276
- headerRow,
1277
- dividerRow,
1278
- ...dataRows,
1262
+ RootCauseList.render(items),
1279
1263
  ];
1280
1264
  if (sorted.length > displayedSamples.length) {
1281
- lines.push(`\n_Showing the first ${displayedSamples.length} of ${sorted.length} breaching samples._`);
1265
+ /*
1266
+ * The blank line matters: without it the note is a lazy
1267
+ * continuation of the last item, not a paragraph of its own.
1268
+ */
1269
+ lines.push("", `_Showing the first ${displayedSamples.length} of ${sorted.length} breaching samples._`);
1282
1270
  }
1283
1271
  return lines.join("\n");
1284
1272
  }
@@ -1582,15 +1570,34 @@ ${contextBlock}
1582
1570
  sections.push(`**Kubernetes Cluster Details**\n${clusterDetails.join("\n")}`);
1583
1571
  // Affected resources
1584
1572
  if (breakdown.affectedResources && breakdown.affectedResources.length > 0) {
1585
- // Sort by metric value descending (worst first) and filter out zero-value resources
1586
- const sortedResources = [
1587
- ...breakdown.affectedResources,
1588
- ]
1573
+ /*
1574
+ * Keep the rows that satisfy the criteria that just matched, worst
1575
+ * first — NOT simply the non-zero rows. k8s-node-not-ready fires on
1576
+ * `k8s.node.condition_ready = 0`, so the zero rows are the NotReady
1577
+ * nodes; the old hardcoded `> 0` dropped exactly those and named a
1578
+ * healthy node in the analysis below.
1579
+ */
1580
+ const breach = MonitorCriteriaEvaluator.getAffectedResourceBreachPredicate({
1581
+ criteriaInstance: input.criteriaInstance,
1582
+ floorEqualityFiresOnFall: true,
1583
+ });
1584
+ const sortedResources = breakdown.affectedResources
1585
+ .map((r) => {
1586
+ var _a;
1587
+ /*
1588
+ * A fall criteria breached on the resource's lowest sample
1589
+ * in the window, not its highest.
1590
+ */
1591
+ return breach.worstIsLowest
1592
+ ? Object.assign(Object.assign({}, r), { metricValue: (_a = r.lowestMetricValue) !== null && _a !== void 0 ? _a : r.metricValue }) : r;
1593
+ })
1589
1594
  .filter((r) => {
1590
- return r.metricValue > 0;
1595
+ return breach.matches(r.metricValue);
1591
1596
  })
1592
1597
  .sort((a, b) => {
1593
- return b.metricValue - a.metricValue;
1598
+ return breach.worstIsLowest
1599
+ ? a.metricValue - b.metricValue
1600
+ : b.metricValue - a.metricValue;
1594
1601
  });
1595
1602
  if (sortedResources.length === 0) {
1596
1603
  return sections.join("\n");
@@ -2318,31 +2325,49 @@ ${contextBlock}
2318
2325
  return sections.length > 0 ? sections.join("\n") : null;
2319
2326
  }
2320
2327
  /*
2321
- * Which rows of the Ceph affected-resources list actually BREACHED.
2328
+ * Which rows of an affected-resources list actually BREACHED.
2322
2329
  *
2323
2330
  * The blanket `metricValue > 0` below is right for every count- or
2324
2331
  * level-style criteria ("> 0 active health checks", "> 85 % used"), but
2325
- * it is exactly backwards for the availability signals: the OSD Down /
2326
- * OSD Out / Quorum Degraded criteria fire on `< 1` over ceph_osd_up /
2327
- * ceph_osd_in / ceph_mon_quorum_status, so the ZERO rows are the down
2328
- * daemons. Dropping them rendered a list of the HEALTHY daemons under
2329
- * an incident that tells the reader to "check the root cause for the
2330
- * affected ceph_daemon label".
2332
+ * it is exactly backwards for the availability signals: the Ceph OSD
2333
+ * Down / OSD Out / Quorum Degraded criteria fire on `< 1` over
2334
+ * ceph_osd_up / ceph_osd_in / ceph_mon_quorum_status, so the ZERO rows
2335
+ * are the down daemons. Dropping them rendered a list of the HEALTHY
2336
+ * daemons under an incident that tells the reader to "check the root
2337
+ * cause for the affected ceph_daemon label".
2331
2338
  *
2332
2339
  * Invert only for a criteria that fires when the metric FALLS.
2333
2340
  * Everything else — every `> 0` health check, PG count, latency and
2334
- * capacity ratio, and every `= 0` recovery criteria — keeps the
2335
- * existing `> 0`, worst-highest-first list byte for byte.
2341
+ * capacity ratio, and (by default) every `= 0` recovery criteria —
2342
+ * keeps the existing `> 0`, worst-highest-first list byte for byte.
2343
+ *
2344
+ * `floorEqualityFiresOnFall` additionally counts a FIRING `= 0` (or
2345
+ * below) as a fall. Kubernetes needs it: k8s-node-not-ready and
2346
+ * k8s-etcd-no-leader fire on `= 0` over k8s.node.condition_ready /
2347
+ * etcd_server_has_leader, and zero is the floor of those metrics, so
2348
+ * the only way to reach it is down. It is gated on the criteria opening
2349
+ * an incident or alert because the same `= 0` is also how the restart,
2350
+ * failed-pod and unavailable-replica templates express RECOVERY, and
2351
+ * that all-clear must not become a list of healthy zero rows. Ceph
2352
+ * leaves it off: all of its `= 0` criteria are recoveries.
2336
2353
  */
2337
- static getCephBreachPredicate(criteriaInstance) {
2338
- var _a;
2339
- const metricFilters = (((_a = criteriaInstance === null || criteriaInstance === void 0 ? void 0 : criteriaInstance.data) === null || _a === void 0 ? void 0 : _a.filters) || []).filter((f) => {
2354
+ static getAffectedResourceBreachPredicate(input) {
2355
+ var _a, _b, _c, _d, _e, _f;
2356
+ const metricFilters = (((_b = (_a = input.criteriaInstance) === null || _a === void 0 ? void 0 : _a.data) === null || _b === void 0 ? void 0 : _b.filters) || []).filter((f) => {
2340
2357
  return f.checkOn === CheckOn.MetricValue && typeof f.value === "number";
2341
2358
  });
2359
+ const opensIncidentOrAlert = ((_d = (_c = input.criteriaInstance) === null || _c === void 0 ? void 0 : _c.data) === null || _d === void 0 ? void 0 : _d.createIncidents) === true ||
2360
+ ((_f = (_e = input.criteriaInstance) === null || _e === void 0 ? void 0 : _e.data) === null || _f === void 0 ? void 0 : _f.createAlerts) === true;
2342
2361
  const firesWhenMetricFalls = metricFilters.length > 0 &&
2343
2362
  metricFilters.every((f) => {
2344
- return (f.filterType === FilterType.LessThan ||
2345
- f.filterType === FilterType.LessThanOrEqualTo);
2363
+ if (f.filterType === FilterType.LessThan ||
2364
+ f.filterType === FilterType.LessThanOrEqualTo) {
2365
+ return true;
2366
+ }
2367
+ return (input.floorEqualityFiresOnFall === true &&
2368
+ opensIncidentOrAlert &&
2369
+ f.filterType === FilterType.EqualTo &&
2370
+ f.value <= 0);
2346
2371
  });
2347
2372
  if (!firesWhenMetricFalls) {
2348
2373
  return {
@@ -2356,6 +2381,9 @@ ${contextBlock}
2356
2381
  matches: (value) => {
2357
2382
  return metricFilters.some((f) => {
2358
2383
  const threshold = f.value;
2384
+ if (f.filterType === FilterType.EqualTo) {
2385
+ return value === threshold;
2386
+ }
2359
2387
  return f.filterType === FilterType.LessThan
2360
2388
  ? value < threshold
2361
2389
  : value <= threshold;
@@ -2472,7 +2500,9 @@ ${contextBlock}
2472
2500
  * ceph_osd_in / ceph_mon_quorum_status the criteria is `< 1`, so
2473
2501
  * the zero rows are the whole point of the incident.
2474
2502
  */
2475
- const breach = MonitorCriteriaEvaluator.getCephBreachPredicate(input.criteriaInstance);
2503
+ const breach = MonitorCriteriaEvaluator.getAffectedResourceBreachPredicate({
2504
+ criteriaInstance: input.criteriaInstance,
2505
+ });
2476
2506
  const sortedResources = [
2477
2507
  ...breakdown.affectedResources,
2478
2508
  ]