@oneuptime/common 14.0.5 → 14.0.7

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 (192) hide show
  1. package/Models/DatabaseModels/InventoryItem.ts +23 -12
  2. package/Server/Infrastructure/Postgres/SchemaMigrations/1795200000000-AddInventoryItemProjectEntityKeyIndex.ts +217 -0
  3. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  4. package/Server/Services/AlertService.ts +32 -36
  5. package/Server/Services/IncidentAlertService.ts +609 -2
  6. package/Server/Services/IncidentService.ts +134 -44
  7. package/Server/Services/InventoryItemService.ts +38 -0
  8. package/Server/Services/IoTDeviceCredentialService.ts +13 -4
  9. package/Server/Services/MetricService.ts +1 -1
  10. package/Server/Services/ScheduledMaintenanceService.ts +50 -46
  11. package/Server/Services/UserNotificationRuleService.ts +55 -53
  12. package/Server/Services/WorkspaceNotificationSummaryService.ts +38 -20
  13. package/Server/Types/Database/QueryHelper.ts +54 -1
  14. package/Server/Utils/AI/Toolbox/MetricTools.ts +13 -2
  15. package/Server/Utils/AI/Toolbox/ResourceTools.ts +12 -1
  16. package/Server/Utils/AffectedResources/LinkedAffectedResources.ts +576 -0
  17. package/Server/Utils/Alert/AlertStateChangeAuthorization.ts +129 -0
  18. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +1 -1
  19. package/Server/Utils/Inventory/InventoryOverviewAggregation.ts +111 -0
  20. package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +33 -0
  21. package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +4 -3
  22. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +3 -1
  23. package/Server/Utils/Response.ts +31 -0
  24. package/Server/Utils/Topology/TopologyConcurrencyLimiter.ts +229 -0
  25. package/Server/Utils/Topology/TopologyQueries.ts +1350 -0
  26. package/Server/Utils/Topology/TopologyRequest.ts +463 -0
  27. package/Server/Utils/Topology/TopologyResponseCache.ts +183 -0
  28. package/Server/Utils/Topology/TopologySql.ts +1024 -0
  29. package/Tests/App/Dashboard/AcknowledgeAlertsOnDeclare.test.ts +369 -0
  30. package/Tests/App/Dashboard/AffectedResourcesSlo.test.tsx +1 -1
  31. package/Tests/App/Dashboard/AffectedResourcesSummaryStep.test.tsx +510 -123
  32. package/Tests/App/Dashboard/AlertsTableAffectedResources.test.tsx +490 -0
  33. package/Tests/App/Dashboard/ChangeAlertStateDeclareIncident.test.tsx +886 -0
  34. package/Tests/App/Dashboard/CreateReviewStepRoleSummary.test.tsx +1250 -0
  35. package/Tests/App/Dashboard/CreateReviewStepSelectionSummary.test.tsx +738 -0
  36. package/Tests/App/Dashboard/DeclareIncidentFromAlert.test.ts +417 -0
  37. package/Tests/App/Dashboard/EntityDetailPanel.test.tsx +1948 -229
  38. package/Tests/App/Dashboard/EntityDetailPanelLifecycle.test.tsx +493 -0
  39. package/Tests/App/Dashboard/EntityDetailPanelNavigation.test.tsx +439 -0
  40. package/Tests/App/Dashboard/EventOverviewPages.test.tsx +292 -1
  41. package/Tests/App/Dashboard/EventStatusPanel.test.tsx +798 -2
  42. package/Tests/App/Dashboard/FetchStateAndSeverityElements.test.tsx +669 -0
  43. package/Tests/App/Dashboard/FetchUsers.test.tsx +230 -0
  44. package/Tests/App/Dashboard/IncidentCreateAcknowledgeAlerts.test.tsx +1611 -0
  45. package/Tests/App/Dashboard/InfrastructureExplorer.test.tsx +1365 -57
  46. package/Tests/App/Dashboard/InfrastructureGraph.test.tsx +964 -23
  47. package/Tests/App/Dashboard/InventoryRelationships.test.tsx +885 -0
  48. package/Tests/App/Dashboard/NetworkTopologyExplorerSearch.test.tsx +1267 -0
  49. package/Tests/App/Dashboard/ResourceOverviewTileTooltips.test.tsx +4 -1
  50. package/Tests/App/Dashboard/RumOverviewPage.test.tsx +127 -0
  51. package/Tests/App/Dashboard/ServiceMapDatabaseLink.test.tsx +201 -15
  52. package/Tests/App/Dashboard/ServiceMapGraph.test.tsx +508 -36
  53. package/Tests/App/Dashboard/SiteSearchBox.test.tsx +2110 -0
  54. package/Tests/App/Dashboard/TelemetryMetricsSignals.test.ts +168 -8
  55. package/Tests/App/Dashboard/TopologyDataLifecycle.test.tsx +583 -0
  56. package/Tests/App/Dashboard/TopologyDataLoading.test.tsx +982 -218
  57. package/Tests/App/Dashboard/TopologyPageNavigation.test.tsx +818 -114
  58. package/Tests/Server/Infrastructure/Postgres/RenameInventoryItemMigration.test.ts +90 -21
  59. package/Tests/Server/Services/DomainService.test.ts +525 -0
  60. package/Tests/Server/Services/IncidentAlertAcknowledgeOnDeclare.test.ts +2630 -0
  61. package/Tests/Server/Services/IncidentAlertPostgres.test.ts +1625 -1
  62. package/Tests/Server/Services/IncidentDeclareAcknowledgeAlerts.test.ts +2189 -0
  63. package/Tests/Server/Services/InventoryItemServiceOverviewCounts.test.ts +345 -0
  64. package/Tests/Server/Services/InventoryOverviewPostgres.test.ts +254 -0
  65. package/Tests/Server/Services/IoTDeviceCredentialService.test.ts +819 -0
  66. package/Tests/Server/Services/LinkedAffectedResourcesCreatedFeed.test.ts +420 -0
  67. package/Tests/Server/Services/MetricServiceAggregate.test.ts +12 -0
  68. package/Tests/Server/Services/SavedViewDefaults.test.ts +325 -0
  69. package/Tests/Server/Services/SloAffectedResourceCreatedFeed.test.ts +92 -36
  70. package/Tests/Server/Services/StatusPageCertificateService.test.ts +169 -0
  71. package/Tests/Server/Services/TeamComplianceSettingService.test.ts +191 -0
  72. package/Tests/Server/Types/Database/QueryHelperSearchAllWords.test.ts +651 -0
  73. package/Tests/Server/Utils/AI/AlertWriteTools.test.ts +256 -0
  74. package/Tests/Server/Utils/AI/IncidentWriteTools.test.ts +482 -0
  75. package/Tests/Server/Utils/AffectedResources/LinkedAffectedResources.test.ts +858 -0
  76. package/Tests/Server/Utils/Alert/AlertStateChangeAuthorization.test.ts +1172 -0
  77. package/Tests/Server/Utils/Monitor/Criteria/CompareCriteria.test.ts +54 -0
  78. package/Tests/Server/Utils/Monitor/MetricDecimalThreshold.test.ts +417 -0
  79. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorAffectedResourceEdgeCases.test.ts +96 -0
  80. package/Tests/Server/Utils/Topology/InventoryItemEntityKeyIndexMigration.test.ts +386 -0
  81. package/Tests/Server/Utils/Topology/InventoryItemEntityKeyIndexMigrationPostgres.test.ts +430 -0
  82. package/Tests/Server/Utils/Topology/TopologyActivityParityPostgres.test.ts +279 -0
  83. package/Tests/Server/Utils/Topology/TopologyConcurrencyLimiter.test.ts +534 -0
  84. package/Tests/Server/Utils/Topology/TopologyEstateGenerator.ts +624 -0
  85. package/Tests/Server/Utils/Topology/TopologyJsonResponse.test.ts +90 -0
  86. package/Tests/Server/Utils/Topology/TopologyQueries.test.ts +1634 -0
  87. package/Tests/Server/Utils/Topology/TopologyQueriesDifferentialPostgres.test.ts +683 -0
  88. package/Tests/Server/Utils/Topology/TopologyQueriesPostgres.test.ts +2624 -0
  89. package/Tests/Server/Utils/Topology/TopologyReference.ts +1018 -0
  90. package/Tests/Server/Utils/Topology/TopologyRequest.test.ts +643 -0
  91. package/Tests/Server/Utils/Topology/TopologyResponseCache.test.ts +305 -0
  92. package/Tests/Server/Utils/Topology/TopologySql.test.ts +973 -0
  93. package/Tests/Types/BaseDatabase/AggregationType.test.ts +5 -1
  94. package/Tests/Types/Dashboard/KubernetesDashboardTemplate.test.ts +184 -0
  95. package/Tests/Types/Dashboard/KubernetesDashboardTemplates.test.ts +107 -0
  96. package/Tests/Types/Monitor/KubernetesAlertTemplates.test.ts +8 -14
  97. package/Tests/Types/Monitor/KubernetesMetricCatalog.test.ts +4 -3
  98. package/Tests/Types/Monitor/Utils/KubernetesAgentEmittedMetrics.ts +27 -6
  99. package/Tests/Types/Topology/TopologyTypeRules.test.ts +176 -0
  100. package/Tests/UI/Rum/ChunkLoader.test.ts +76 -0
  101. package/Tests/UI/Rum/ReplayRailDetail.test.tsx +53 -0
  102. package/Tests/UI/Rum/ReplaySignals.test.ts +54 -0
  103. package/Tests/Utils/Dashboard/KubernetesClusterCpuTileRepair.test.ts +370 -0
  104. package/Tests/Utils/Incident/IncidentFromAlerts.test.ts +302 -0
  105. package/Tests/Utils/Rules/MonitorTypeRuleCriteria.test.ts +181 -0
  106. package/Types/BaseDatabase/AggregationType.ts +7 -0
  107. package/Types/Dashboard/DashboardTemplates.ts +20 -7
  108. package/Types/Incident/IncidentAlertLink.ts +19 -0
  109. package/Types/Rum/SessionReplayCustomEvents.ts +29 -3
  110. package/Types/Rum/WebVitals.ts +13 -0
  111. package/Types/Telemetry/InventoryLiveness.ts +26 -0
  112. package/Types/Topology/TopologyApi.ts +428 -0
  113. package/Types/Topology/TopologyTypeRules.ts +191 -0
  114. package/UI/Components/Input/Input.tsx +11 -0
  115. package/Utils/Dashboard/KubernetesClusterCpuTileRepair.ts +147 -0
  116. package/Utils/Incident/IncidentFromAlerts.ts +78 -0
  117. package/build/dist/Models/DatabaseModels/InventoryItem.js +20 -11
  118. package/build/dist/Models/DatabaseModels/InventoryItem.js.map +1 -1
  119. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795200000000-AddInventoryItemProjectEntityKeyIndex.js +165 -0
  120. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795200000000-AddInventoryItemProjectEntityKeyIndex.js.map +1 -0
  121. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  122. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  123. package/build/dist/Server/Services/AlertService.js +21 -33
  124. package/build/dist/Server/Services/AlertService.js.map +1 -1
  125. package/build/dist/Server/Services/IncidentAlertService.js +457 -3
  126. package/build/dist/Server/Services/IncidentAlertService.js.map +1 -1
  127. package/build/dist/Server/Services/IncidentService.js +96 -42
  128. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  129. package/build/dist/Server/Services/InventoryItemService.js +31 -0
  130. package/build/dist/Server/Services/InventoryItemService.js.map +1 -1
  131. package/build/dist/Server/Services/IoTDeviceCredentialService.js +12 -5
  132. package/build/dist/Server/Services/IoTDeviceCredentialService.js.map +1 -1
  133. package/build/dist/Server/Services/MetricService.js +1 -1
  134. package/build/dist/Server/Services/ScheduledMaintenanceService.js +37 -40
  135. package/build/dist/Server/Services/ScheduledMaintenanceService.js.map +1 -1
  136. package/build/dist/Server/Services/UserNotificationRuleService.js +55 -51
  137. package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
  138. package/build/dist/Server/Services/WorkspaceNotificationSummaryService.js +42 -25
  139. package/build/dist/Server/Services/WorkspaceNotificationSummaryService.js.map +1 -1
  140. package/build/dist/Server/Types/Database/QueryHelper.js +47 -1
  141. package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
  142. package/build/dist/Server/Utils/AI/Toolbox/MetricTools.js +13 -2
  143. package/build/dist/Server/Utils/AI/Toolbox/MetricTools.js.map +1 -1
  144. package/build/dist/Server/Utils/AI/Toolbox/ResourceTools.js +12 -1
  145. package/build/dist/Server/Utils/AI/Toolbox/ResourceTools.js.map +1 -1
  146. package/build/dist/Server/Utils/AffectedResources/LinkedAffectedResources.js +405 -0
  147. package/build/dist/Server/Utils/AffectedResources/LinkedAffectedResources.js.map +1 -0
  148. package/build/dist/Server/Utils/Alert/AlertStateChangeAuthorization.js +112 -0
  149. package/build/dist/Server/Utils/Alert/AlertStateChangeAuthorization.js.map +1 -0
  150. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +1 -1
  151. package/build/dist/Server/Utils/Inventory/InventoryOverviewAggregation.js +81 -0
  152. package/build/dist/Server/Utils/Inventory/InventoryOverviewAggregation.js.map +1 -0
  153. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +31 -0
  154. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js.map +1 -1
  155. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +1 -1
  156. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js.map +1 -1
  157. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +1 -1
  158. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  159. package/build/dist/Server/Utils/Response.js +30 -0
  160. package/build/dist/Server/Utils/Response.js.map +1 -1
  161. package/build/dist/Server/Utils/Topology/TopologyConcurrencyLimiter.js +178 -0
  162. package/build/dist/Server/Utils/Topology/TopologyConcurrencyLimiter.js.map +1 -0
  163. package/build/dist/Server/Utils/Topology/TopologyQueries.js +884 -0
  164. package/build/dist/Server/Utils/Topology/TopologyQueries.js.map +1 -0
  165. package/build/dist/Server/Utils/Topology/TopologyRequest.js +275 -0
  166. package/build/dist/Server/Utils/Topology/TopologyRequest.js.map +1 -0
  167. package/build/dist/Server/Utils/Topology/TopologyResponseCache.js +146 -0
  168. package/build/dist/Server/Utils/Topology/TopologyResponseCache.js.map +1 -0
  169. package/build/dist/Server/Utils/Topology/TopologySql.js +688 -0
  170. package/build/dist/Server/Utils/Topology/TopologySql.js.map +1 -0
  171. package/build/dist/Types/BaseDatabase/AggregationType.js +7 -0
  172. package/build/dist/Types/BaseDatabase/AggregationType.js.map +1 -1
  173. package/build/dist/Types/Dashboard/DashboardTemplates.js +20 -7
  174. package/build/dist/Types/Dashboard/DashboardTemplates.js.map +1 -1
  175. package/build/dist/Types/Incident/IncidentAlertLink.js +17 -0
  176. package/build/dist/Types/Incident/IncidentAlertLink.js.map +1 -1
  177. package/build/dist/Types/Rum/SessionReplayCustomEvents.js.map +1 -1
  178. package/build/dist/Types/Rum/WebVitals.js +12 -0
  179. package/build/dist/Types/Rum/WebVitals.js.map +1 -1
  180. package/build/dist/Types/Telemetry/InventoryLiveness.js +23 -0
  181. package/build/dist/Types/Telemetry/InventoryLiveness.js.map +1 -1
  182. package/build/dist/Types/Topology/TopologyApi.js +55 -0
  183. package/build/dist/Types/Topology/TopologyApi.js.map +1 -0
  184. package/build/dist/Types/Topology/TopologyTypeRules.js +165 -0
  185. package/build/dist/Types/Topology/TopologyTypeRules.js.map +1 -0
  186. package/build/dist/UI/Components/Input/Input.js +1 -1
  187. package/build/dist/UI/Components/Input/Input.js.map +1 -1
  188. package/build/dist/Utils/Dashboard/KubernetesClusterCpuTileRepair.js +110 -0
  189. package/build/dist/Utils/Dashboard/KubernetesClusterCpuTileRepair.js.map +1 -0
  190. package/build/dist/Utils/Incident/IncidentFromAlerts.js +38 -0
  191. package/build/dist/Utils/Incident/IncidentFromAlerts.js.map +1 -1
  192. package/package.json +1 -1
@@ -24,7 +24,9 @@ import OnCallDutyPolicyService from "./OnCallDutyPolicyService";
24
24
  import TeamMemberService from "./TeamMemberService";
25
25
  import UserService from "./UserService";
26
26
  import URL from "../../Types/API/URL";
27
- import { getSloAffectedResourceMarkdownLines } from "../../Utils/Slo/SloAffectedResourceMarkdown";
27
+ import LinkedAffectedResources, {
28
+ LinkedAffectedResource,
29
+ } from "../Utils/AffectedResources/LinkedAffectedResources";
28
30
  import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
29
31
  import SortOrder from "../../Types/BaseDatabase/SortOrder";
30
32
  import LIMIT_MAX, { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
@@ -112,9 +114,14 @@ import IncidentAIContextBuilder, {
112
114
  IncidentContextData,
113
115
  } from "../Utils/AI/IncidentAIContextBuilder";
114
116
  import IncidentAlertService, {
117
+ AcknowledgeDeclaredAlertsResult,
118
+ AlertsToAcknowledgeOnDeclare,
115
119
  LinkAlertsToIncidentResult,
116
120
  } from "./IncidentAlertService";
117
- import { INCIDENT_ALERT_IDS_TO_LINK_KEY } from "../../Types/Incident/IncidentAlertLink";
121
+ import {
122
+ INCIDENT_ACKNOWLEDGE_ALERTS_TO_LINK_KEY,
123
+ INCIDENT_ALERT_IDS_TO_LINK_KEY,
124
+ } from "../../Types/Incident/IncidentAlertLink";
118
125
 
119
126
  // key is incidentId for this dictionary.
120
127
  type UpdateCarryForward = Dictionary<{
@@ -131,6 +138,14 @@ type UpdateCarryForward = Dictionary<{
131
138
  type IncidentCreateCarryForward = {
132
139
  // Validated, deduplicated alert ids to link once the incident exists.
133
140
  alertIdsToLink: Array<ObjectID>;
141
+ /*
142
+ * Acknowledge alerts once they are linked, as the declaring user
143
+ * (INCIDENT_ACKNOWLEDGE_ALERTS_TO_LINK_KEY) - what stops their escalation.
144
+ * When asked to: the project's Acknowledged alert state, and the alerts
145
+ * not acknowledged yet, which the caller was checked for. Null otherwise.
146
+ */
147
+ acknowledgedAlertStateId: ObjectID | null;
148
+ alertIdsToAcknowledge: Array<ObjectID>;
134
149
  } | null;
135
150
 
136
151
  type IncidentUpdatePayload = {
@@ -809,14 +824,36 @@ export class Service extends DatabaseService<Model> {
809
824
  const alertIdsToLink: unknown =
810
825
  createBy.miscDataProps?.[INCIDENT_ALERT_IDS_TO_LINK_KEY];
811
826
 
812
- if (alertIdsToLink !== undefined && alertIdsToLink !== null) {
813
- carryForward = {
814
- alertIdsToLink:
815
- await IncidentAlertService.validateAlertIdsForNewIncident({
827
+ const validatedAlertIds: Array<ObjectID> =
828
+ alertIdsToLink !== undefined && alertIdsToLink !== null
829
+ ? await IncidentAlertService.validateAlertIdsForNewIncident({
816
830
  projectId: projectId,
817
831
  alertIds: alertIdsToLink,
818
832
  props: createBy.props,
819
- }),
833
+ })
834
+ : [];
835
+
836
+ /*
837
+ * Asking to acknowledge the alerts is checked here too, with the alert
838
+ * ids, so a request that cannot be honoured (no Acknowledged alert state,
839
+ * or a caller who may not change these alerts' states) is refused before
840
+ * the incident exists rather than leaving alerts that keep paging.
841
+ */
842
+ const alertsToAcknowledge: AlertsToAcknowledgeOnDeclare | null =
843
+ await IncidentAlertService.validateAcknowledgeAlertsForNewIncident({
844
+ projectId: projectId,
845
+ acknowledgeAlerts:
846
+ createBy.miscDataProps?.[INCIDENT_ACKNOWLEDGE_ALERTS_TO_LINK_KEY],
847
+ alertIds: validatedAlertIds,
848
+ props: createBy.props,
849
+ });
850
+
851
+ if (validatedAlertIds.length > 0) {
852
+ carryForward = {
853
+ alertIdsToLink: validatedAlertIds,
854
+ acknowledgedAlertStateId:
855
+ alertsToAcknowledge?.acknowledgedAlertStateId || null,
856
+ alertIdsToAcknowledge: alertsToAcknowledge?.alertIdsToAcknowledge || [],
820
857
  };
821
858
  }
822
859
 
@@ -1272,20 +1309,10 @@ export class Service extends DatabaseService<Model> {
1272
1309
  labels: {
1273
1310
  name: true,
1274
1311
  },
1275
- monitors: {
1276
- name: true,
1277
- _id: true,
1278
- },
1279
1312
  /*
1280
- * Named under "Resources Affected" in the created feed item. This read
1281
- * runs as root, so projectId comes along and the feed names only this
1282
- * project's SLOs (getSloAffectedResourceMarkdownLines).
1313
+ * The resources named under "Resources Affected" are read by the feed
1314
+ * builder itself (LinkedAffectedResources), one relation at a time.
1283
1315
  */
1284
- serviceLevelObjectives: {
1285
- name: true,
1286
- _id: true,
1287
- projectId: true,
1288
- },
1289
1316
  },
1290
1317
  props: {
1291
1318
  isRoot: true,
@@ -1744,6 +1771,24 @@ export class Service extends DatabaseService<Model> {
1744
1771
  return carryForward?.alertIdsToLink || [];
1745
1772
  }
1746
1773
 
1774
+ /*
1775
+ * The alerts the declaration asked to acknowledge that were not
1776
+ * acknowledged yet when it was checked - the only ones the caller was
1777
+ * authorized for, so the only ones that may be written.
1778
+ */
1779
+ private getAlertIdsToAcknowledgeDeclaredWith(
1780
+ onCreate: OnCreate<Model>,
1781
+ ): Array<ObjectID> {
1782
+ const carryForward: IncidentCreateCarryForward =
1783
+ (onCreate.carryForward as IncidentCreateCarryForward) || null;
1784
+
1785
+ if (!carryForward?.acknowledgedAlertStateId) {
1786
+ return [];
1787
+ }
1788
+
1789
+ return carryForward.alertIdsToAcknowledge || [];
1790
+ }
1791
+
1747
1792
  /*
1748
1793
  * Who declared the incident, as "Linked by" on the links and the actor of
1749
1794
  * their feed entries. A user is recorded as themselves and an API key as
@@ -1778,6 +1823,13 @@ export class Service extends DatabaseService<Model> {
1778
1823
  * the incident's title when the incident is private, and a rule can make
1779
1824
  * it private after it was saved. (The alerts' owners are added to a private
1780
1825
  * incident later in the chain, once its workspace channels exist.)
1826
+ *
1827
+ * When the declaration asked for it, the alerts are then acknowledged as
1828
+ * the declaring user, which stops their on-call escalation at the
1829
+ * workers' next run. That is not waited for: each acknowledgement is a
1830
+ * full state change (feed entry, Slack / Microsoft Teams post, owner
1831
+ * notification), up to one per alert, and the page the user lands on does
1832
+ * not show the alerts' states. It never fails the incident.
1781
1833
  */
1782
1834
  @CaptureSpan()
1783
1835
  private async linkAlertsDeclaredWithIncident(
@@ -1793,6 +1845,8 @@ export class Service extends DatabaseService<Model> {
1793
1845
 
1794
1846
  await privacyRulesApplied;
1795
1847
 
1848
+ let linkedAlertIds: Array<ObjectID> = [];
1849
+
1796
1850
  try {
1797
1851
  const result: LinkAlertsToIncidentResult =
1798
1852
  await IncidentAlertService.linkAlertsToIncident({
@@ -1806,6 +1860,11 @@ export class Service extends DatabaseService<Model> {
1806
1860
  },
1807
1861
  });
1808
1862
 
1863
+ linkedAlertIds = [
1864
+ ...result.linkedAlertIds,
1865
+ ...result.alreadyLinkedAlertIds,
1866
+ ];
1867
+
1809
1868
  if (result.failed.length > 0) {
1810
1869
  logger.error(
1811
1870
  `${result.failed.length} of ${alertIds.length} alerts could not be linked to the incident they were declared with.`,
@@ -1824,6 +1883,44 @@ export class Service extends DatabaseService<Model> {
1824
1883
  } as LogAttributes,
1825
1884
  );
1826
1885
  }
1886
+
1887
+ const alertIdsToAcknowledge: Array<ObjectID> =
1888
+ this.getAlertIdsToAcknowledgeDeclaredWith(onCreate);
1889
+
1890
+ if (alertIdsToAcknowledge.length === 0) {
1891
+ return;
1892
+ }
1893
+
1894
+ const projectId: ObjectID = createdItem.projectId;
1895
+ const incidentId: ObjectID = createdItem.id;
1896
+
1897
+ IncidentAlertService.acknowledgeAlertsDeclaredWithIncident({
1898
+ projectId: projectId,
1899
+ incidentId: incidentId,
1900
+ alertIds: alertIdsToAcknowledge,
1901
+ linkedAlertIds: linkedAlertIds,
1902
+ acknowledgedByUserId: this.getDeclaringUserId(onCreate, createdItem),
1903
+ })
1904
+ .then((acknowledged: AcknowledgeDeclaredAlertsResult) => {
1905
+ if (acknowledged.failed.length > 0) {
1906
+ logger.error(
1907
+ `${acknowledged.failed.length} of ${alertIdsToAcknowledge.length} alerts could not be acknowledged when the incident was declared from them.`,
1908
+ {
1909
+ projectId: projectId.toString(),
1910
+ incidentId: incidentId.toString(),
1911
+ } as LogAttributes,
1912
+ );
1913
+ }
1914
+ })
1915
+ .catch((error: Error) => {
1916
+ logger.error(
1917
+ `Acknowledging the alerts an incident was declared from failed in IncidentService.onCreateSuccess: ${error}`,
1918
+ {
1919
+ projectId: projectId.toString(),
1920
+ incidentId: incidentId.toString(),
1921
+ } as LogAttributes,
1922
+ );
1923
+ });
1827
1924
  }
1828
1925
 
1829
1926
  @CaptureSpan()
@@ -1905,35 +2002,28 @@ ${incident.description || "No description provided."}
1905
2002
  }
1906
2003
 
1907
2004
  /*
1908
- * Monitors, then the SLOs this incident is linked to. A burn-rate
1909
- * incident carries no monitors on purpose, so its SLO is the only
1910
- * resource there is to name - and the feed's only way back to the
1911
- * objective that declared it. The SLO link is built inline:
1912
- * ServiceLevelObjectiveService cannot be imported here (it reaches
1913
- * this service through the burn-rate rule service).
2005
+ * Everything the incident's Affected Resources card lists: monitors,
2006
+ * the hosts, clusters and services it is attached to, then its SLOs. A
2007
+ * burn-rate incident carries no monitors on purpose, so its SLO is the
2008
+ * only resource there is to name - and the feed's only way back to the
2009
+ * objective that declared it.
1914
2010
  */
1915
- const sloLines: Array<string> =
1916
- incident.serviceLevelObjectives &&
1917
- incident.serviceLevelObjectives.length > 0
1918
- ? getSloAffectedResourceMarkdownLines({
1919
- dashboardUrl: await DatabaseConfig.getDashboardUrl(),
1920
- projectId: incident.projectId!,
1921
- serviceLevelObjectives: incident.serviceLevelObjectives,
1922
- })
1923
- : [];
2011
+ const resources: Array<LinkedAffectedResource> =
2012
+ await LinkedAffectedResources.readForIncident({
2013
+ service: this,
2014
+ projectId: incident.projectId!,
2015
+ incidentId: incident.id!,
2016
+ });
1924
2017
 
1925
- if (
1926
- (incident.monitors && incident.monitors.length > 0) ||
1927
- sloLines.length > 0
1928
- ) {
2018
+ if (resources.length > 0) {
1929
2019
  feedInfoInMarkdown += `🌎 **Resources Affected**:\n`;
1930
2020
 
1931
- for (const monitor of incident.monitors || []) {
1932
- feedInfoInMarkdown += `- [${monitor.name}](${(await MonitorService.getMonitorLinkInDashboard(incident.projectId!, monitor.id!)).toString()})\n`;
1933
- }
1934
-
1935
- for (const sloLine of sloLines) {
1936
- feedInfoInMarkdown += `${sloLine}\n`;
2021
+ for (const resourceLine of LinkedAffectedResources.getMarkdownLines({
2022
+ dashboardUrl: await DatabaseConfig.getDashboardUrl(),
2023
+ projectId: incident.projectId!,
2024
+ resources: resources,
2025
+ })) {
2026
+ feedInfoInMarkdown += `${resourceLine}\n`;
1937
2027
  }
1938
2028
 
1939
2029
  feedInfoInMarkdown += `\n\n`;
@@ -10,6 +10,8 @@ import ColumnLength from "../../Types/Database/ColumnLength";
10
10
  import QueryDeepPartialEntity from "../../Types/Database/PartialEntity";
11
11
  import CreateBy from "../Types/Database/CreateBy";
12
12
  import { OnCreate } from "../Types/Database/Hooks";
13
+ import { AggregateRow } from "../Types/Database/AggregateBy";
14
+ import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
13
15
  import BadDataException from "../../Types/Exception/BadDataException";
14
16
  import Dictionary from "../../Types/Dictionary";
15
17
  import { JSONObject } from "../../Types/JSON";
@@ -22,6 +24,12 @@ import {
22
24
  MANUAL_ENTITY_IDENTITY_ATTRIBUTE,
23
25
  } from "../../Utils/Telemetry/EntityKey";
24
26
  import logger from "../Utils/Logger";
27
+ import {
28
+ INVENTORY_OVERVIEW_GROUP_BY,
29
+ INVENTORY_OVERVIEW_SELECT,
30
+ InventoryOverviewCounts,
31
+ readInventoryOverviewGroups,
32
+ } from "../Utils/Inventory/InventoryOverviewAggregation";
25
33
  import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
26
34
  import { ExtractedEntity } from "../Utils/Telemetry/TelemetryEntity";
27
35
  import {
@@ -147,6 +155,36 @@ export class InventoryItemService extends DatabaseService<Model> {
147
155
  return { createBy, carryForward: null };
148
156
  }
149
157
 
158
+ /**
159
+ * The Inventory Overview's tiles and per-type breakdown, counted over the
160
+ * project's whole unarchived estate. See INVENTORY_OVERVIEW_SELECT.
161
+ *
162
+ * Archived rows are excluded because every tile drills into the live Items
163
+ * list, which excludes them too.
164
+ *
165
+ * The GROUP BY carries no limit: capping it would cap the totals summed
166
+ * from it. The number of groups is bounded by the entity-type vocabulary —
167
+ * ingest maps to a fixed set of types, manual creates are confined to
168
+ * MANUAL_ENTITY_TYPES, and the column is not updatable.
169
+ */
170
+ @CaptureSpan()
171
+ public async getOverviewCounts(data: {
172
+ projectId: ObjectID;
173
+ props: DatabaseCommonInteractionProps;
174
+ }): Promise<InventoryOverviewCounts> {
175
+ const rows: Array<AggregateRow> = await this.aggregateBy({
176
+ query: {
177
+ projectId: data.projectId,
178
+ isArchived: false,
179
+ },
180
+ groupBy: INVENTORY_OVERVIEW_GROUP_BY,
181
+ select: INVENTORY_OVERVIEW_SELECT,
182
+ props: data.props,
183
+ });
184
+
185
+ return readInventoryOverviewGroups(rows);
186
+ }
187
+
150
188
  /**
151
189
  * Forward-only registry reconciliation: upsert a catalog row per
152
190
  * discovered entity, bump `lastSeenAt`, merge descriptive attributes
@@ -204,14 +204,23 @@ export class Service extends DatabaseService<Model> {
204
204
  return cached;
205
205
  }
206
206
 
207
- let idObject: ObjectID;
208
- try {
209
- idObject = new ObjectID(credentialId);
210
- } catch {
207
+ /*
208
+ * Validated rather than caught. ObjectID's constructor stores whatever
209
+ * string it is handed and never throws, so the try/catch this replaced
210
+ * caught nothing: a malformed id reached Postgres as an invalid uuid
211
+ * literal, which errors on every attempt AND skipped the negative cache
212
+ * below, because the row was never absent - the query failed. credentialId
213
+ * is an MQTT username, so it is attacker-controlled, and that made the
214
+ * negative cache bypassable with random garbage. Same guard, and the same
215
+ * reason for it, as ProjectAuthorization's API-key check.
216
+ */
217
+ if (!ObjectID.isValidUUID(credentialId)) {
211
218
  this.contextCache.set(credentialId, null, NEGATIVE_TTL_MS);
212
219
  return null;
213
220
  }
214
221
 
222
+ const idObject: ObjectID = new ObjectID(credentialId);
223
+
215
224
  const credential: Model | null = await this.findOneBy({
216
225
  query: { _id: idObject },
217
226
  select: {
@@ -952,7 +952,7 @@ export class MetricService extends AnalyticsDatabaseService<Metric> {
952
952
  *
953
953
  * For non-percentile aggregations (Sum/Avg/Min/Max/Count) we delegate
954
954
  * to the base implementation. For percentile aggregations
955
- * (P50/P90/P95/P99) we build a subquery that fans each metric row out
955
+ * (P50/P75/P90/P95/P99) we build a subquery that fans each metric row out
956
956
  * into one or more `(midpoint, weight)` samples — derived from
957
957
  * histogram buckets when present — and then runs
958
958
  * `quantileExactWeighted` over the fanned-out distribution. This means
@@ -9,6 +9,10 @@ import DatabaseService from "./DatabaseService";
9
9
  import ScheduledMaintenanceCustomField from "../../Models/DatabaseModels/ScheduledMaintenanceCustomField";
10
10
  import CustomFieldMappingService from "./CustomFieldMappingService";
11
11
  import MonitorService from "./MonitorService";
12
+ import LinkedAffectedResources, {
13
+ LinkedAffectedResource,
14
+ LinkedAffectedResourceRelation,
15
+ } from "../Utils/AffectedResources/LinkedAffectedResources";
12
16
  import ScheduledMaintenanceOwnerTeamService from "./ScheduledMaintenanceOwnerTeamService";
13
17
  import ScheduledMaintenanceOwnerUserService from "./ScheduledMaintenanceOwnerUserService";
14
18
  import ScheduledMaintenanceStateService from "./ScheduledMaintenanceStateService";
@@ -1180,10 +1184,6 @@ ${resourcesAffected ? `**Resources Affected:** ${resourcesAffected}` : ""}
1180
1184
  },
1181
1185
  startsAt: true,
1182
1186
  endsAt: true,
1183
- monitors: {
1184
- name: true,
1185
- _id: true,
1186
- },
1187
1187
  labels: {
1188
1188
  name: true,
1189
1189
  },
@@ -1448,14 +1448,23 @@ ${scheduledMaintenance.description || "No description provided."}
1448
1448
  feedInfoInMarkdown += `⏳ **Scheduled Maintenance State**: ${scheduledMaintenance.currentScheduledMaintenanceState.name} \n\n`;
1449
1449
  }
1450
1450
 
1451
- if (
1452
- scheduledMaintenance.monitors &&
1453
- scheduledMaintenance.monitors.length > 0
1454
- ) {
1451
+ // Everything the event's Affected Resources card lists, monitors first.
1452
+ const resources: Array<LinkedAffectedResource> =
1453
+ await LinkedAffectedResources.readForScheduledMaintenance({
1454
+ service: this,
1455
+ projectId: scheduledMaintenance.projectId!,
1456
+ scheduledMaintenanceId: scheduledMaintenance.id!,
1457
+ });
1458
+
1459
+ if (resources.length > 0) {
1455
1460
  feedInfoInMarkdown += `🌎 **Resources Affected**:\n`;
1456
1461
 
1457
- for (const monitor of scheduledMaintenance.monitors) {
1458
- feedInfoInMarkdown += `- [${monitor.name}](${(await MonitorService.getMonitorLinkInDashboard(scheduledMaintenance.projectId!, monitor.id!)).toString()})\n`;
1462
+ for (const resourceLine of LinkedAffectedResources.getMarkdownLines({
1463
+ dashboardUrl: await DatabaseConfig.getDashboardUrl(),
1464
+ projectId: scheduledMaintenance.projectId!,
1465
+ resources: resources,
1466
+ })) {
1467
+ feedInfoInMarkdown += `${resourceLine}\n`;
1459
1468
  }
1460
1469
 
1461
1470
  feedInfoInMarkdown += `\n\n`;
@@ -1820,47 +1829,42 @@ ${(
1820
1829
  shouldAddScheduledMaintenanceFeed = true;
1821
1830
  }
1822
1831
 
1823
- if (
1824
- onUpdate.updateBy.data.monitors &&
1825
- onUpdate.updateBy.data.monitors.length > 0 &&
1826
- Array.isArray(onUpdate.updateBy.data.monitors)
1827
- ) {
1828
- const monitorIds: Array<ObjectID> = (
1829
- onUpdate.updateBy.data.monitors as any
1830
- )
1831
- .map((monitor: Label) => {
1832
- if (monitor._id) {
1833
- return new ObjectID(monitor._id?.toString());
1834
- }
1832
+ /*
1833
+ * Any affected-resource list in the payload - not only monitors - is
1834
+ * a change to what the event affects. The event is read back rather
1835
+ * than the ids in the payload being looked up: the read is held to
1836
+ * this project, and it names the whole list the card now shows.
1837
+ */
1838
+ const affectedResourcesChanged: boolean =
1839
+ LinkedAffectedResources.getRelations(this.getModel()).some(
1840
+ (relation: LinkedAffectedResourceRelation): boolean => {
1841
+ const value: unknown = (
1842
+ onUpdate.updateBy.data as Record<string, unknown>
1843
+ )[relation.column];
1844
+
1845
+ return Array.isArray(value) && value.length > 0;
1846
+ },
1847
+ );
1835
1848
 
1836
- return null;
1837
- })
1838
- .filter((monitorId: ObjectID | null) => {
1839
- return monitorId !== null;
1840
- });
1849
+ if (affectedResourcesChanged && onUpdate.updateBy.props.tenantId) {
1850
+ const projectId: ObjectID = onUpdate.updateBy.props
1851
+ .tenantId as ObjectID;
1841
1852
 
1842
- const monitors: Array<Label> = await MonitorService.findBy({
1843
- query: {
1844
- _id: QueryHelper.any(monitorIds),
1845
- },
1846
- select: {
1847
- name: true,
1848
- },
1849
- limit: LIMIT_PER_PROJECT,
1850
- skip: 0,
1851
- props: {
1852
- isRoot: true,
1853
- },
1854
- });
1853
+ const resources: Array<LinkedAffectedResource> =
1854
+ await LinkedAffectedResources.readForScheduledMaintenance({
1855
+ service: this,
1856
+ projectId: projectId,
1857
+ scheduledMaintenanceId: scheduledMaintenanceId,
1858
+ });
1855
1859
 
1856
- if (monitors.length > 0) {
1860
+ if (resources.length > 0) {
1857
1861
  feedInfoInMarkdown += `\n\n**Resources Affected**:
1858
1862
 
1859
- ${monitors
1860
- .map((monitor: Monitor) => {
1861
- return `- ${monitor.name}`;
1862
- })
1863
- .join("\n")}
1863
+ ${LinkedAffectedResources.getMarkdownLines({
1864
+ dashboardUrl: await DatabaseConfig.getDashboardUrl(),
1865
+ projectId: projectId,
1866
+ resources: resources,
1867
+ }).join("\n")}
1864
1868
  `;
1865
1869
 
1866
1870
  shouldAddScheduledMaintenanceFeed = true;
@@ -10,6 +10,8 @@ import Markdown, { MarkdownContentType } from "../Types/Markdown";
10
10
  import CallService from "./CallService";
11
11
  import DatabaseService from "./DatabaseService";
12
12
  import IncidentService from "./IncidentService";
13
+ import LinkedAffectedResources from "../Utils/AffectedResources/LinkedAffectedResources";
14
+ import SeriesLabelDisplay from "../../Types/Monitor/SeriesContext/SeriesLabelDisplay";
13
15
  import IncidentSeverityService from "./IncidentSeverityService";
14
16
  import MailService from "./MailService";
15
17
  import ProjectCallSMSConfigService from "./ProjectCallSMSConfigService";
@@ -983,15 +985,10 @@ export class Service extends DatabaseService<Model> {
983
985
  incidentNumber: true,
984
986
  incidentNumberWithPrefix: true,
985
987
  /*
986
- * AcknowledgeIncident.hbs asks for a Resources Affected row and
987
- * the generator had nothing to build it from, so the row was
988
- * silently dropped — the same `{{#if}}` swallow that hid the
989
- * alert's Root Cause.
988
+ * No relations here: AcknowledgeIncident.hbs's Resources Affected
989
+ * row is built from every resource the incident is linked to,
990
+ * which generateEmailTemplateForIncidentCreated reads itself.
990
991
  */
991
- monitors: {
992
- _id: true,
993
- name: true,
994
- },
995
992
  },
996
993
  });
997
994
  }
@@ -1031,11 +1028,13 @@ export class Service extends DatabaseService<Model> {
1031
1028
  * select it.
1032
1029
  */
1033
1030
  rootCause: true,
1034
- // Same story for the template's Resources Affected row.
1035
- monitor: {
1036
- _id: true,
1037
- name: true,
1038
- },
1031
+ /*
1032
+ * The series a grouped monitor raised this alert for (the pod or
1033
+ * container), which the Resources Affected row names in place of
1034
+ * the monitor - as the alert owner emails do. The linked resources
1035
+ * themselves are read by generateEmailTemplateForAlertCreated.
1036
+ */
1037
+ seriesLabels: true,
1039
1038
  },
1040
1039
  });
1041
1040
  }
@@ -4613,7 +4612,22 @@ export class Service extends DatabaseService<Model> {
4613
4612
  MarkdownContentType.Email,
4614
4613
  ),
4615
4614
  alertSeverity: alert.alertSeverity!.name!,
4616
- resourcesAffected: alert.monitor?.name || "No resources identified",
4615
+ /*
4616
+ * Every resource the alert's Affected Resources card lists. An SLO
4617
+ * burn-rate alert has no monitor, and this used to read "No resources
4618
+ * identified" beside a card naming its SLO.
4619
+ */
4620
+ resourcesAffected: LinkedAffectedResources.getText({
4621
+ resources: await LinkedAffectedResources.readForAlert({
4622
+ service: AlertService,
4623
+ projectId: alert.projectId!,
4624
+ alertId: alert.id!,
4625
+ }),
4626
+ seriesSummary: SeriesLabelDisplay.buildInlineSummary(
4627
+ alert.seriesLabels,
4628
+ ),
4629
+ fallback: "No resources identified",
4630
+ }),
4617
4631
  rootCause: await Markdown.convertToHTML(
4618
4632
  alert.rootCause || "No root cause identified for this alert",
4619
4633
  MarkdownContentType.Email,
@@ -4664,15 +4678,15 @@ export class Service extends DatabaseService<Model> {
4664
4678
  MarkdownContentType.Email,
4665
4679
  ),
4666
4680
  incidentSeverity: incident.incidentSeverity!.name!,
4667
- resourcesAffected:
4668
- (incident.monitors || [])
4669
- .map((monitor: Monitor): string => {
4670
- return monitor.name || "";
4671
- })
4672
- .filter((name: string): boolean => {
4673
- return name.length > 0;
4674
- })
4675
- .join(", ") || "No resources identified",
4681
+ // Every resource the incident's Affected Resources card lists.
4682
+ resourcesAffected: LinkedAffectedResources.getText({
4683
+ resources: await LinkedAffectedResources.readForIncident({
4684
+ service: IncidentService,
4685
+ projectId: incident.projectId!,
4686
+ incidentId: incident.id!,
4687
+ }),
4688
+ fallback: "No resources identified",
4689
+ }),
4676
4690
  rootCause: await Markdown.convertToHTML(
4677
4691
  incident.rootCause || "No root cause identified for this incident",
4678
4692
  MarkdownContentType.Email,
@@ -4762,18 +4776,15 @@ export class Service extends DatabaseService<Model> {
4762
4776
  })
4763
4777
  : [];
4764
4778
 
4765
- // Get unique monitors (resources affected)
4766
- const monitorNames: Set<string> = new Set();
4767
- for (const alert of alerts) {
4768
- if (alert.monitor?.name) {
4769
- monitorNames.add(alert.monitor.name);
4770
- }
4771
- }
4772
-
4773
- const resourcesAffected: string =
4774
- monitorNames.size > 0
4775
- ? Array.from(monitorNames).join(", ")
4776
- : "No resources identified";
4779
+ // Every resource any alert in the episode is linked to, each named once.
4780
+ const resourcesAffected: string = LinkedAffectedResources.getText({
4781
+ resources: await LinkedAffectedResources.readForAlerts({
4782
+ service: AlertService,
4783
+ projectId: alertEpisode.projectId!,
4784
+ alertIds: alertIds,
4785
+ }),
4786
+ fallback: "No resources identified",
4787
+ });
4777
4788
 
4778
4789
  // Build alerts list HTML with proper email styling
4779
4790
  let alertsListHtml: string = "";
@@ -4933,24 +4944,15 @@ export class Service extends DatabaseService<Model> {
4933
4944
  })
4934
4945
  : [];
4935
4946
 
4936
- /*
4937
- * Unique monitors across every incident in the episode. An incident carries
4938
- * a list of monitors (unlike an alert, which has exactly one), so this
4939
- * flattens rather than reading a single relation.
4940
- */
4941
- const monitorNames: Set<string> = new Set();
4942
- for (const incident of incidents) {
4943
- for (const monitor of incident.monitors || []) {
4944
- if (monitor.name) {
4945
- monitorNames.add(monitor.name);
4946
- }
4947
- }
4948
- }
4949
-
4950
- const resourcesAffected: string =
4951
- monitorNames.size > 0
4952
- ? Array.from(monitorNames).join(", ")
4953
- : "No resources identified";
4947
+ // Every resource any incident in the episode is linked to, each named once.
4948
+ const resourcesAffected: string = LinkedAffectedResources.getText({
4949
+ resources: await LinkedAffectedResources.readForIncidents({
4950
+ service: IncidentService,
4951
+ projectId: incidentEpisode.projectId!,
4952
+ incidentIds: incidentIds,
4953
+ }),
4954
+ fallback: "No resources identified",
4955
+ });
4954
4956
 
4955
4957
  // Build incidents list HTML with proper email styling
4956
4958
  let incidentsListHtml: string = "";