@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
@@ -1,6 +1,9 @@
1
1
  import { describe, expect, test } from "@jest/globals";
2
2
  import IncidentFromAlerts, {
3
+ AlertForAcknowledgement,
3
4
  AlertForIncidentPrefill,
5
+ AlertsToAcknowledge,
6
+ AlertStateForAcknowledgement,
4
7
  INCIDENT_PREFILL_RESOURCE_KEYS,
5
8
  IncidentPrefillFromAlerts,
6
9
  NamedResource,
@@ -694,3 +697,302 @@ describe("IncidentFromAlerts", () => {
694
697
  });
695
698
  });
696
699
  });
700
+
701
+ /*
702
+ * Which alerts the create page offers to acknowledge as the incident is
703
+ * declared. States are compared by order, as the server does: anything at or
704
+ * after the project's Acknowledged state (Resolved, or a custom state in
705
+ * between) has been acknowledged already. An alert whose state cannot be
706
+ * placed is offered - the server skips it if it turns out to be acknowledged.
707
+ */
708
+ describe("IncidentFromAlerts.getAlertsToAcknowledge", () => {
709
+ const CREATED: AlertStateForAcknowledgement = {
710
+ id: "state-created",
711
+ order: 1,
712
+ isAcknowledgedState: false,
713
+ };
714
+ const ACKNOWLEDGED: AlertStateForAcknowledgement = {
715
+ id: "state-acknowledged",
716
+ order: 2,
717
+ isAcknowledgedState: true,
718
+ };
719
+ const RESOLVED: AlertStateForAcknowledgement = {
720
+ id: "state-resolved",
721
+ order: 3,
722
+ isAcknowledgedState: false,
723
+ };
724
+
725
+ const DEFAULT_ALERT_STATES: Array<AlertStateForAcknowledgement> = [
726
+ CREATED,
727
+ ACKNOWLEDGED,
728
+ RESOLVED,
729
+ ];
730
+
731
+ type AlertInStateFunction = (
732
+ id: string,
733
+ currentAlertStateId?: string | undefined,
734
+ ) => AlertForAcknowledgement;
735
+
736
+ const alertIn: AlertInStateFunction = (
737
+ id: string,
738
+ currentAlertStateId?: string | undefined,
739
+ ): AlertForAcknowledgement => {
740
+ return { id: id, currentAlertStateId: currentAlertStateId };
741
+ };
742
+
743
+ type GetFunction = (
744
+ alerts: Array<AlertForAcknowledgement>,
745
+ alertStates?: Array<AlertStateForAcknowledgement>,
746
+ ) => AlertsToAcknowledge | null;
747
+
748
+ const get: GetFunction = (
749
+ alerts: Array<AlertForAcknowledgement>,
750
+ alertStates?: Array<AlertStateForAcknowledgement>,
751
+ ): AlertsToAcknowledge | null => {
752
+ return IncidentFromAlerts.getAlertsToAcknowledge({
753
+ alerts: alerts,
754
+ alertStates: alertStates || DEFAULT_ALERT_STATES,
755
+ });
756
+ };
757
+
758
+ describe("when nothing could be acknowledged", () => {
759
+ test("is null when the project has no alert states at all", () => {
760
+ expect(get([alertIn("a1", CREATED.id)], [])).toBeNull();
761
+ });
762
+
763
+ test("is null when no state is the Acknowledged state", () => {
764
+ expect(
765
+ get(
766
+ [alertIn("a1", CREATED.id)],
767
+ [CREATED, { ...ACKNOWLEDGED, isAcknowledgedState: false }, RESOLVED],
768
+ ),
769
+ ).toBeNull();
770
+ });
771
+
772
+ test("is null when the Acknowledged flag is missing rather than false", () => {
773
+ expect(
774
+ get(
775
+ [alertIn("a1", CREATED.id)],
776
+ [
777
+ CREATED,
778
+ { id: ACKNOWLEDGED.id, order: ACKNOWLEDGED.order },
779
+ RESOLVED,
780
+ ],
781
+ ),
782
+ ).toBeNull();
783
+ });
784
+
785
+ test("is null when the Acknowledged state has no order to compare by", () => {
786
+ expect(
787
+ get(
788
+ [alertIn("a1", CREATED.id)],
789
+ [CREATED, { ...ACKNOWLEDGED, order: undefined }, RESOLVED],
790
+ ),
791
+ ).toBeNull();
792
+ });
793
+
794
+ test("is null without an Acknowledged state even when there are no alerts", () => {
795
+ expect(get([], [CREATED, RESOLVED])).toBeNull();
796
+ });
797
+ });
798
+
799
+ describe("comparing by order", () => {
800
+ test("offers an alert whose state comes before Acknowledged", () => {
801
+ expect(get([alertIn("a1", CREATED.id)])).toEqual({
802
+ alertIds: ["a1"],
803
+ alreadyAcknowledgedCount: 0,
804
+ });
805
+ });
806
+
807
+ test("leaves out an alert that is in the Acknowledged state itself", () => {
808
+ expect(get([alertIn("a1", ACKNOWLEDGED.id)])).toEqual({
809
+ alertIds: [],
810
+ alreadyAcknowledgedCount: 1,
811
+ });
812
+ });
813
+
814
+ test("leaves out an alert that is resolved", () => {
815
+ expect(get([alertIn("a1", RESOLVED.id)])).toEqual({
816
+ alertIds: [],
817
+ alreadyAcknowledgedCount: 1,
818
+ });
819
+ });
820
+
821
+ test("treats a custom state between Acknowledged and Resolved as acknowledged already", () => {
822
+ const investigating: AlertStateForAcknowledgement = {
823
+ id: "state-investigating",
824
+ order: 3,
825
+ };
826
+ const resolved: AlertStateForAcknowledgement = {
827
+ id: "state-resolved",
828
+ order: 4,
829
+ };
830
+
831
+ expect(
832
+ get(
833
+ [alertIn("a1", investigating.id), alertIn("a2", resolved.id)],
834
+ [CREATED, ACKNOWLEDGED, investigating, resolved],
835
+ ),
836
+ ).toEqual({ alertIds: [], alreadyAcknowledgedCount: 2 });
837
+ });
838
+
839
+ test("offers an alert in a custom state before Acknowledged", () => {
840
+ const triaged: AlertStateForAcknowledgement = {
841
+ id: "state-triaged",
842
+ order: 2,
843
+ };
844
+ const acknowledged: AlertStateForAcknowledgement = {
845
+ id: "state-acknowledged",
846
+ order: 3,
847
+ isAcknowledgedState: true,
848
+ };
849
+
850
+ expect(
851
+ get(
852
+ [alertIn("a1", triaged.id), alertIn("a2", CREATED.id)],
853
+ [CREATED, triaged, acknowledged, { ...RESOLVED, order: 4 }],
854
+ ),
855
+ ).toEqual({ alertIds: ["a1", "a2"], alreadyAcknowledgedCount: 0 });
856
+ });
857
+
858
+ test("compares by order, not by where the state sits in the list", () => {
859
+ // Listed newest-first: the list position must not decide anything.
860
+ const reversed: Array<AlertStateForAcknowledgement> = [
861
+ RESOLVED,
862
+ ACKNOWLEDGED,
863
+ CREATED,
864
+ ];
865
+
866
+ expect(
867
+ get(
868
+ [
869
+ alertIn("a1", RESOLVED.id),
870
+ alertIn("a2", CREATED.id),
871
+ alertIn("a3", ACKNOWLEDGED.id),
872
+ ],
873
+ reversed,
874
+ ),
875
+ ).toEqual({ alertIds: ["a2"], alreadyAcknowledgedCount: 2 });
876
+ });
877
+
878
+ test("an Acknowledged order of 0 is still an order", () => {
879
+ const acknowledgedAtZero: AlertStateForAcknowledgement = {
880
+ id: "state-acknowledged",
881
+ order: 0,
882
+ isAcknowledgedState: true,
883
+ };
884
+ const before: AlertStateForAcknowledgement = {
885
+ id: "state-before",
886
+ order: -1,
887
+ };
888
+
889
+ expect(
890
+ get(
891
+ [
892
+ alertIn("a1", acknowledgedAtZero.id),
893
+ alertIn("a2", before.id),
894
+ alertIn("a3", RESOLVED.id),
895
+ ],
896
+ [before, acknowledgedAtZero, RESOLVED],
897
+ ),
898
+ ).toEqual({ alertIds: ["a2"], alreadyAcknowledgedCount: 2 });
899
+ });
900
+ });
901
+
902
+ describe("alerts whose state cannot be placed", () => {
903
+ test("offers an alert whose current state is not one of the project's states", () => {
904
+ expect(get([alertIn("a1", "state-from-another-project")])).toEqual({
905
+ alertIds: ["a1"],
906
+ alreadyAcknowledgedCount: 0,
907
+ });
908
+ });
909
+
910
+ test("offers an alert with no current state id", () => {
911
+ expect(get([alertIn("a1", undefined), alertIn("a2", "")])).toEqual({
912
+ alertIds: ["a1", "a2"],
913
+ alreadyAcknowledgedCount: 0,
914
+ });
915
+ });
916
+
917
+ test("offers an alert whose current state has no order", () => {
918
+ const unordered: AlertStateForAcknowledgement = {
919
+ id: "state-unordered",
920
+ };
921
+
922
+ expect(
923
+ get(
924
+ [alertIn("a1", unordered.id)],
925
+ [CREATED, ACKNOWLEDGED, RESOLVED, unordered],
926
+ ),
927
+ ).toEqual({ alertIds: ["a1"], alreadyAcknowledgedCount: 0 });
928
+ });
929
+ });
930
+
931
+ test("matches state ids case-insensitively and ignoring surrounding spaces", () => {
932
+ const upperAcknowledged: AlertStateForAcknowledgement = {
933
+ id: "AAAAAAAA-AAAA-4AAA-8AAA-000000000002",
934
+ order: 2,
935
+ isAcknowledgedState: true,
936
+ };
937
+ const upperCreated: AlertStateForAcknowledgement = {
938
+ id: "AAAAAAAA-AAAA-4AAA-8AAA-000000000001",
939
+ order: 1,
940
+ };
941
+
942
+ expect(
943
+ get(
944
+ [
945
+ alertIn("a1", " aaaaaaaa-aaaa-4aaa-8aaa-000000000002 "),
946
+ alertIn("a2", "aaaaaaaa-aaaa-4aaa-8aaa-000000000001"),
947
+ ],
948
+ [upperCreated, upperAcknowledged],
949
+ ),
950
+ ).toEqual({ alertIds: ["a2"], alreadyAcknowledgedCount: 1 });
951
+ });
952
+
953
+ test("keeps the alerts in the order they were given and counts the rest", () => {
954
+ const result: AlertsToAcknowledge | null = get([
955
+ alertIn("a5", CREATED.id),
956
+ alertIn("a1", ACKNOWLEDGED.id),
957
+ alertIn("a4", undefined),
958
+ alertIn("a2", RESOLVED.id),
959
+ alertIn("a3", CREATED.id),
960
+ alertIn("a6", RESOLVED.id),
961
+ ]);
962
+
963
+ expect(result).toEqual({
964
+ alertIds: ["a5", "a4", "a3"],
965
+ alreadyAcknowledgedCount: 3,
966
+ });
967
+ });
968
+
969
+ test("returns the alert ids exactly as given", () => {
970
+ expect(get([alertIn("ALERT-One", CREATED.id)])!.alertIds).toEqual([
971
+ "ALERT-One",
972
+ ]);
973
+ });
974
+
975
+ test("is empty, not null, when there are no alerts", () => {
976
+ expect(get([])).toEqual({ alertIds: [], alreadyAcknowledgedCount: 0 });
977
+ });
978
+
979
+ test("does not change its input", () => {
980
+ const alerts: Array<AlertForAcknowledgement> = [
981
+ alertIn("a1", CREATED.id),
982
+ alertIn("a2", ACKNOWLEDGED.id),
983
+ ];
984
+ const alertStates: Array<AlertStateForAcknowledgement> = [
985
+ RESOLVED,
986
+ CREATED,
987
+ ACKNOWLEDGED,
988
+ ];
989
+
990
+ get(alerts, alertStates);
991
+
992
+ expect(alerts).toEqual([
993
+ { id: "a1", currentAlertStateId: CREATED.id },
994
+ { id: "a2", currentAlertStateId: ACKNOWLEDGED.id },
995
+ ]);
996
+ expect(alertStates).toEqual([RESOLVED, CREATED, ACKNOWLEDGED]);
997
+ });
998
+ });
@@ -0,0 +1,181 @@
1
+ import {
2
+ getMonitorTypeCriteriaValidationError,
3
+ isMonitorTypeCriteriaValue,
4
+ } from "../../../Utils/Rules/MonitorTypeRuleCriteria";
5
+ import MonitorType from "../../../Types/Monitor/MonitorType";
6
+ import {
7
+ RuleCriteriaFilter,
8
+ RuleCriteriaOperator,
9
+ RuleCriteriaValue,
10
+ } from "../../../Types/Rules/RuleCriteria";
11
+ import { describe, expect, test } from "@jest/globals";
12
+
13
+ /*
14
+ * The gate that decides whether a rule's `monitorType` criterion may be saved.
15
+ * DatabaseService runs it on every write and the SLO monitor-rule engine runs
16
+ * it again when it matches monitors, so a criterion this lets through has to
17
+ * be one the engine can actually evaluate: a monitor type is an enum choice,
18
+ * which means an exact match either way and nothing else. A text operator
19
+ * slipping past here is a rule that silently matches nothing.
20
+ */
21
+
22
+ function buildFilter(
23
+ operator: RuleCriteriaOperator,
24
+ value: RuleCriteriaValue,
25
+ ): RuleCriteriaFilter {
26
+ return { field: "monitorType", operator: operator, value: value };
27
+ }
28
+
29
+ const OPERATOR_ERROR: string =
30
+ "Monitor type criteria only support Equals and Does not equal operators.";
31
+ const VALUE_ERROR: string =
32
+ "Monitor type criteria require a valid monitor type.";
33
+
34
+ describe("isMonitorTypeCriteriaValue", () => {
35
+ test("accepts every monitor type the product defines", () => {
36
+ const allTypes: Array<string> = Object.values(MonitorType);
37
+ expect(allTypes.length).toBeGreaterThan(0);
38
+ for (const monitorType of allTypes) {
39
+ expect(isMonitorTypeCriteriaValue(monitorType)).toBe(true);
40
+ }
41
+ });
42
+
43
+ test("rejects a string that is not a monitor type", () => {
44
+ expect(isMonitorTypeCriteriaValue("Website")).toBe(
45
+ Object.values(MonitorType).includes("Website" as MonitorType),
46
+ );
47
+ expect(isMonitorTypeCriteriaValue("NotAMonitorType")).toBe(false);
48
+ expect(isMonitorTypeCriteriaValue("")).toBe(false);
49
+ });
50
+
51
+ test("is case- and whitespace-sensitive, as an enum comparison must be", () => {
52
+ const [first] = Object.values(MonitorType) as Array<string>;
53
+ expect(isMonitorTypeCriteriaValue(first!)).toBe(true);
54
+ expect(isMonitorTypeCriteriaValue(first!.toLowerCase())).toBe(
55
+ first === first!.toLowerCase(),
56
+ );
57
+ expect(isMonitorTypeCriteriaValue(` ${first!}`)).toBe(false);
58
+ expect(isMonitorTypeCriteriaValue(`${first!} `)).toBe(false);
59
+ });
60
+
61
+ test("rejects every non-string value a criterion can carry", () => {
62
+ const nonStrings: Array<unknown> = [
63
+ undefined,
64
+ null,
65
+ 0,
66
+ 1,
67
+ true,
68
+ false,
69
+ [],
70
+ [Object.values(MonitorType)[0]],
71
+ {},
72
+ { value: Object.values(MonitorType)[0] },
73
+ ];
74
+ for (const value of nonStrings) {
75
+ expect(isMonitorTypeCriteriaValue(value)).toBe(false);
76
+ }
77
+ });
78
+
79
+ test("does not accept an inherited Object property as a monitor type", () => {
80
+ // The lookup is a Set, not a plain object, so "toString" is not a member.
81
+ expect(isMonitorTypeCriteriaValue("toString")).toBe(false);
82
+ expect(isMonitorTypeCriteriaValue("constructor")).toBe(false);
83
+ expect(isMonitorTypeCriteriaValue("__proto__")).toBe(false);
84
+ });
85
+ });
86
+
87
+ describe("getMonitorTypeCriteriaValidationError", () => {
88
+ const validType: MonitorType = Object.values(MonitorType)[0] as MonitorType;
89
+
90
+ test("passes Equals and NotEquals against a real monitor type", () => {
91
+ for (const operator of [
92
+ RuleCriteriaOperator.Equals,
93
+ RuleCriteriaOperator.NotEquals,
94
+ ]) {
95
+ expect(
96
+ getMonitorTypeCriteriaValidationError(buildFilter(operator, validType)),
97
+ ).toBeNull();
98
+ }
99
+ });
100
+
101
+ test("passes every monitor type under Equals", () => {
102
+ for (const monitorType of Object.values(MonitorType)) {
103
+ expect(
104
+ getMonitorTypeCriteriaValidationError(
105
+ buildFilter(RuleCriteriaOperator.Equals, monitorType),
106
+ ),
107
+ ).toBeNull();
108
+ }
109
+ });
110
+
111
+ test("rejects every operator other than Equals and NotEquals", () => {
112
+ const rejected: Array<RuleCriteriaOperator> = Object.values(
113
+ RuleCriteriaOperator,
114
+ ).filter((operator: RuleCriteriaOperator) => {
115
+ return (
116
+ operator !== RuleCriteriaOperator.Equals &&
117
+ operator !== RuleCriteriaOperator.NotEquals
118
+ );
119
+ });
120
+ expect(rejected.length).toBeGreaterThan(0);
121
+
122
+ for (const operator of rejected) {
123
+ expect(
124
+ getMonitorTypeCriteriaValidationError(buildFilter(operator, validType)),
125
+ ).toBe(OPERATOR_ERROR);
126
+ }
127
+ });
128
+
129
+ test("reports the operator first when both the operator and the value are wrong", () => {
130
+ expect(
131
+ getMonitorTypeCriteriaValidationError(
132
+ buildFilter(RuleCriteriaOperator.Contains, "NotAMonitorType"),
133
+ ),
134
+ ).toBe(OPERATOR_ERROR);
135
+ });
136
+
137
+ test("rejects a value that is not a monitor type under a supported operator", () => {
138
+ for (const operator of [
139
+ RuleCriteriaOperator.Equals,
140
+ RuleCriteriaOperator.NotEquals,
141
+ ]) {
142
+ expect(
143
+ getMonitorTypeCriteriaValidationError(
144
+ buildFilter(operator, "NotAMonitorType"),
145
+ ),
146
+ ).toBe(VALUE_ERROR);
147
+ expect(
148
+ getMonitorTypeCriteriaValidationError(buildFilter(operator, "")),
149
+ ).toBe(VALUE_ERROR);
150
+ /*
151
+ * An array is what the HasAnyOf-style operators carry; it is not a
152
+ * monitor type even when every member of it is one.
153
+ */
154
+ expect(
155
+ getMonitorTypeCriteriaValidationError(
156
+ buildFilter(operator, [validType]),
157
+ ),
158
+ ).toBe(VALUE_ERROR);
159
+ expect(
160
+ getMonitorTypeCriteriaValidationError(buildFilter(operator, true)),
161
+ ).toBe(VALUE_ERROR);
162
+ expect(
163
+ getMonitorTypeCriteriaValidationError(buildFilter(operator, 0)),
164
+ ).toBe(VALUE_ERROR);
165
+ }
166
+ });
167
+
168
+ test("ignores the filter's field: the caller decides which field this applies to", () => {
169
+ expect(
170
+ getMonitorTypeCriteriaValidationError({
171
+ field: "somethingElse",
172
+ operator: RuleCriteriaOperator.Equals,
173
+ value: validType,
174
+ }),
175
+ ).toBeNull();
176
+ });
177
+
178
+ test("the two errors are distinct, so a form can say which one it is", () => {
179
+ expect(OPERATOR_ERROR).not.toBe(VALUE_ERROR);
180
+ });
181
+ });
@@ -15,6 +15,12 @@ enum AggregationType {
15
15
  * over the raw column, which is the right thing for scalar columns.
16
16
  */
17
17
  P50 = "P50",
18
+ /*
19
+ * The level Core Web Vitals are judged at: Google's good / poor
20
+ * thresholds for LCP, INP and CLS apply to the 75th percentile of page
21
+ * views, so a web-vital chart or monitor wants exactly this one.
22
+ */
23
+ P75 = "P75",
18
24
  P90 = "P90",
19
25
  P95 = "P95",
20
26
  P99 = "P99",
@@ -24,6 +30,7 @@ export default AggregationType;
24
30
 
25
31
  export const PercentileAggregationLevels: Record<string, number> = {
26
32
  [AggregationType.P50]: 0.5,
33
+ [AggregationType.P75]: 0.75,
27
34
  [AggregationType.P90]: 0.9,
28
35
  [AggregationType.P95]: 0.95,
29
36
  [AggregationType.P99]: 0.99,
@@ -1742,6 +1742,18 @@ function createKubernetesDashboardConfig(): DashboardViewConfig {
1742
1742
  * metrics and label them in cores ("2.3 cores"). The Kubernetes
1743
1743
  * cluster overview page — which fetches `k8s.node.allocatable_cpu` —
1744
1744
  * is where CPU is shown as a real "% of capacity".
1745
+ *
1746
+ * - "Cluster CPU (cores in use)" used to be a Value widget over
1747
+ * k8s.node.cpu.usage with `Sum` — the same trap as Pod Count. The
1748
+ * server sums every node's reading in each bucket and the Value widget
1749
+ * then sums the buckets, so a one-hour window of minute buckets and
1750
+ * 30s kubelet scrapes read 540 for three nodes on 1.5 cores each,
1751
+ * instead of 4.5. A Value widget can't express "sum across nodes at one
1752
+ * instant", so the tile is now the busiest node's peak (`Max`), which
1753
+ * is honest at any window and complements the per-node average in
1754
+ * row 1. Cluster-wide CPU against capacity lives on the cluster
1755
+ * overview page. Dashboards already created from this template are
1756
+ * repaired by the RepairKubernetesDashboardClusterCpuTile migration.
1745
1757
  */
1746
1758
  const components: Array<DashboardBaseComponent> = [
1747
1759
  // Row 0: Title
@@ -1875,22 +1887,23 @@ function createKubernetesDashboardConfig(): DashboardViewConfig {
1875
1887
  }),
1876
1888
 
1877
1889
  /*
1878
- * Row 11-13: cluster CPU cores tile and the network throughput
1879
- * chart. This was a 0-100 CPU gauge, but the cores-valued
1890
+ * Row 11-13: busiest-node CPU tile and the network throughput chart.
1891
+ * This was a 0-100 CPU gauge, but the cores-valued
1880
1892
  * `k8s.node.cpu.utilization` pinned it to nonsense (e.g. 711%) and
1881
1893
  * the templated renderer can't divide by allocatable CPU to make a
1882
- * real percentage — so we show total cores in use instead. The old
1883
- * "Memory Utilization" gauge over raw bytes is gone — see
1884
- * top-of-function comment.
1894
+ * real percentage. It then became a `Sum` of cores, which multiplied
1895
+ * by the scrape count — see top-of-function comment. `Max` is the
1896
+ * highest reading any single node reported in the window. The old
1897
+ * "Memory Utilization" gauge over raw bytes is gone too.
1885
1898
  */
1886
1899
  createValueComponent({
1887
- title: "Cluster CPU (cores in use)",
1900
+ title: "Busiest Node CPU (cores, peak)",
1888
1901
  top: 11,
1889
1902
  left: 0,
1890
1903
  width: 4,
1891
1904
  metricConfig: {
1892
1905
  metricName: "k8s.node.cpu.usage",
1893
- aggregationType: MetricsAggregationType.Sum,
1906
+ aggregationType: MetricsAggregationType.Max,
1894
1907
  },
1895
1908
  trendDirection: DashboardValueTrendDirection.HigherIsWorse,
1896
1909
  }),
@@ -12,6 +12,25 @@
12
12
  */
13
13
  export const INCIDENT_ALERT_IDS_TO_LINK_KEY: string = "alertIdsToLink";
14
14
 
15
+ /*
16
+ * Declaring an incident from alerts does not, on its own, change the alerts:
17
+ * they keep escalating until somebody acknowledges them. Sending
18
+ * `miscDataProps[INCIDENT_ACKNOWLEDGE_ALERTS_TO_LINK_KEY] = true` along with
19
+ * INCIDENT_ALERT_IDS_TO_LINK_KEY also acknowledges them, as the declaring
20
+ * user, once they are linked - which is what stops their on-call escalation.
21
+ * Alerts already acknowledged (or past it) are left as they are, and the
22
+ * caller must be allowed to change the state of every alert that is not.
23
+ * Off unless asked for.
24
+ *
25
+ * One exception: when the incident is declared straight into an
26
+ * acknowledged or resolved state and the project's linked-alert switches
27
+ * already move linked alerts along with it, those alerts are left to the
28
+ * switches (one writer per alert) - acknowledged or resolved as the switch
29
+ * does it, not credited to the declaring user.
30
+ */
31
+ export const INCIDENT_ACKNOWLEDGE_ALERTS_TO_LINK_KEY: string =
32
+ "acknowledgeAlertsToLink";
33
+
15
34
  /*
16
35
  * The create-incident page reads the alerts to declare an incident from out of
17
36
  * this query string parameter, as a comma separated list of alert ids.
@@ -215,15 +215,27 @@ export type SessionReplayWebVitalMetric =
215
215
  export type SessionReplayWebVitalRating = "good" | "needs-improvement" | "poor";
216
216
 
217
217
  /*
218
- * A web vital observed once per metric per page. Informational only:
219
- * vitals never trigger an upload, and CLS's value is unitless while the
220
- * others are milliseconds.
218
+ * How the view an INP describes began: the document load ("hard") or a
219
+ * single-page-app route change ("soft").
220
+ */
221
+ export type SessionReplayNavigationType = "hard" | "soft";
222
+
223
+ /* The input that produced an INP, as the browser classes it. */
224
+ export type SessionReplayInteractionType = "pointer" | "keyboard";
225
+
226
+ /*
227
+ * A web vital. LCP, FCP, CLS and TTFB are observed once per page load;
228
+ * INP once per VIEW (newer recorders), so a single-page app reports one
229
+ * INP for each route the user interacted on instead of one for the whole
230
+ * visit. Informational only: vitals never trigger an upload, and CLS's
231
+ * value is unitless while the others are milliseconds.
221
232
  */
222
233
  export interface SessionReplayWebVitalPayload {
223
234
  kind: "web-vital";
224
235
  metric: SessionReplayWebVitalMetric;
225
236
  value: number;
226
237
  rating: SessionReplayWebVitalRating;
238
+ /* INP: the scrubbed URL of the view the interaction happened on. */
227
239
  url?: string;
228
240
  /*
229
241
  * As on SessionReplayPerformanceBudgetPayload: when the measured entry
@@ -232,6 +244,20 @@ export interface SessionReplayWebVitalPayload {
232
244
  * after the moment the number describes. Optional for older recorders.
233
245
  */
234
246
  occurredAtUnixMs?: number;
247
+
248
+ /*
249
+ * INP attribution, all optional (older recorders and engines without
250
+ * the timing fields omit them). The three phases add up to roughly the
251
+ * value: waiting for the main thread, running the page's handlers, and
252
+ * rendering the next frame. The target is a structural selector built
253
+ * exactly as a click's is - never an attribute value or text.
254
+ */
255
+ navigationType?: SessionReplayNavigationType;
256
+ interactionType?: SessionReplayInteractionType;
257
+ interactionTarget?: string;
258
+ inputDelayMs?: number;
259
+ processingDurationMs?: number;
260
+ presentationDelayMs?: number;
235
261
  }
236
262
 
237
263
  export type SessionReplayPerformancePayload =
@@ -86,6 +86,19 @@ export const WebVitalDefinitions: Array<WebVitalDefinition> = [
86
86
  },
87
87
  ];
88
88
 
89
+ /*
90
+ * The metric attribute that names the route a web vital was measured on,
91
+ * tried in order for the RUM overview's per-route breakdown. `app.route`
92
+ * is what the Core Web Vitals docs tell instrumentation to set;
93
+ * `url.template` is OpenTelemetry's semantic-convention name for the same
94
+ * thing. Either must hold the route PATTERN ("/products/:id"), never the
95
+ * concrete URL - one series per product page is a cardinality problem.
96
+ */
97
+ export const WebVitalRouteAttributeKeys: Array<string> = [
98
+ "app.route",
99
+ "url.template",
100
+ ];
101
+
89
102
  // A threshold the way a person would say it: "2.5 s", "800 ms", "0.1".
90
103
  export function formatWebVitalThreshold(
91
104
  value: number,
@@ -54,3 +54,29 @@ export const getInventoryLivenessState: (
54
54
 
55
55
  return InventoryLiveness.Stale;
56
56
  };
57
+
58
+ /**
59
+ * `getInventoryLivenessState` as a Postgres expression over the row aliased
60
+ * `tableAlias`, which must already be quoted (`"InventoryItem"`).
61
+ *
62
+ * The one SQL definition of the rule: the `inventoryStatus` virtual column
63
+ * selects and filters with it, and the Inventory Overview counts "Gone
64
+ * Quiet" with it, so the number on that tile and the rows the Stale facet
65
+ * returns cannot disagree about what stale means.
66
+ *
67
+ * Measured against the database clock. The function above floors elapsed
68
+ * minutes before applying inclusive thresholds — 30m59s is still Live and
69
+ * 24h00m59s is still Recent — and strict comparisons against the following
70
+ * minute preserve exactly those boundaries.
71
+ */
72
+ export const getInventoryLivenessSql: (tableAlias: string) => string = (
73
+ tableAlias: string,
74
+ ): string => {
75
+ return `CASE
76
+ WHEN ${tableAlias}."source" IS DISTINCT FROM '${EntitySource.Discovered}' THEN '${InventoryLiveness.NotTracked}'
77
+ WHEN ${tableAlias}."lastSeenAt" IS NULL THEN '${InventoryLiveness.Never}'
78
+ WHEN ${tableAlias}."lastSeenAt" > CURRENT_TIMESTAMP - INTERVAL '${INVENTORY_LIVE_WINDOW_MINUTES + 1} minutes' THEN '${InventoryLiveness.Live}'
79
+ WHEN ${tableAlias}."lastSeenAt" > CURRENT_TIMESTAMP - INTERVAL '${INVENTORY_STALE_AFTER_MINUTES + 1} minutes' THEN '${InventoryLiveness.Recent}'
80
+ ELSE '${InventoryLiveness.Stale}'
81
+ END`;
82
+ };