@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
@@ -983,4 +983,57 @@ describe("ReplayRailDetail performance wording", () => {
983
983
  expect(screen.getByText("Long task")).toBeInTheDocument();
984
984
  expect(screen.queryByText("long-task")).toBeNull();
985
985
  });
986
+
987
+ /* Issue #3975: which view, which element, and where the time went. */
988
+ it("explains a slow INP: the view, the input, the element and the phase split", () => {
989
+ renderDetail(
990
+ performanceSignal({
991
+ kind: "web-vital",
992
+ durationMs: null,
993
+ budgetMs: null,
994
+ isOverBudget: true,
995
+ metric: "INP",
996
+ value: 480,
997
+ rating: "needs-improvement",
998
+ url: "https://shop.example.com/products",
999
+ atUnixMs: null,
1000
+ navigationType: "soft",
1001
+ interactionType: "pointer",
1002
+ interactionTarget: "nav > a.product-link",
1003
+ inputDelayMs: 5,
1004
+ processingDurationMs: 305,
1005
+ presentationDelayMs: 170,
1006
+ }),
1007
+ );
1008
+
1009
+ expect(
1010
+ screen.getByText("Route change (single-page app)"),
1011
+ ).toBeInTheDocument();
1012
+ expect(screen.getByText("Pointer")).toBeInTheDocument();
1013
+ expect(screen.getByText("nav > a.product-link")).toBeInTheDocument();
1014
+ expect(screen.getByText("Input delay")).toBeInTheDocument();
1015
+ expect(screen.getByText("Processing")).toBeInTheDocument();
1016
+ expect(screen.getByText("Presentation")).toBeInTheDocument();
1017
+ expect(screen.getByText("305ms")).toBeInTheDocument();
1018
+ });
1019
+
1020
+ it("shows none of the INP rows for a recording that predates them", () => {
1021
+ renderDetail(
1022
+ performanceSignal({
1023
+ kind: "web-vital",
1024
+ durationMs: null,
1025
+ budgetMs: null,
1026
+ isOverBudget: false,
1027
+ metric: "INP",
1028
+ value: 120,
1029
+ rating: "good",
1030
+ url: null,
1031
+ atUnixMs: null,
1032
+ }),
1033
+ );
1034
+
1035
+ expect(screen.queryByText("View")).toBeNull();
1036
+ expect(screen.queryByText("Element")).toBeNull();
1037
+ expect(screen.queryByText("Input delay")).toBeNull();
1038
+ });
986
1039
  });
@@ -646,6 +646,60 @@ describe("performance rows", () => {
646
646
  expect(signal.severity).toBe("info");
647
647
  expect(signal.title).not.toContain("0ms");
648
648
  });
649
+
650
+ /* Issue #3975: an INP per view, labelled with the route it measured. */
651
+ it("carries an INP's view and attribution into the detail, and the route into the subtitle", () => {
652
+ const signal: ReplaySignal = fromTimelineEvent(
653
+ makeEvent("performance", {
654
+ performanceKind: "web-vital",
655
+ metric: "INP",
656
+ value: 480,
657
+ rating: "needs-improvement",
658
+ url: "https://shop.example.com/products",
659
+ navigationType: "soft",
660
+ interactionType: "keyboard",
661
+ interactionTarget: "input#search",
662
+ inputDelayMs: 12,
663
+ processingDurationMs: 400,
664
+ presentationDelayMs: 68,
665
+ }),
666
+ { startTimeUnixMs: null },
667
+ );
668
+ const detail: ReplayPerformanceSignalDetail =
669
+ signal.detail as ReplayPerformanceSignalDetail;
670
+
671
+ expect(signal.title).toBe("INP 480ms needs improvement");
672
+ expect(signal.subtitle).toBe("/products");
673
+ expect(detail).toEqual(
674
+ expect.objectContaining({
675
+ navigationType: "soft",
676
+ interactionType: "keyboard",
677
+ interactionTarget: "input#search",
678
+ inputDelayMs: 12,
679
+ processingDurationMs: 400,
680
+ presentationDelayMs: 68,
681
+ }),
682
+ );
683
+ });
684
+
685
+ it("leaves the attribution empty for a recording that predates it", () => {
686
+ const detail: ReplayPerformanceSignalDetail = fromTimelineEvent(
687
+ makeEvent("performance", {
688
+ performanceKind: "web-vital",
689
+ metric: "INP",
690
+ value: 320,
691
+ rating: "poor",
692
+ }),
693
+ { startTimeUnixMs: null },
694
+ ).detail as ReplayPerformanceSignalDetail;
695
+
696
+ expect(detail.navigationType).toBeNull();
697
+ expect(detail.interactionType).toBeNull();
698
+ expect(detail.interactionTarget).toBeNull();
699
+ expect(detail.inputDelayMs).toBeNull();
700
+ expect(detail.processingDurationMs).toBeNull();
701
+ expect(detail.presentationDelayMs).toBeNull();
702
+ });
649
703
  });
650
704
 
651
705
  describe("custom and marker rows", () => {
@@ -0,0 +1,370 @@
1
+ import AggregationType from "../../../Types/BaseDatabase/AggregationType";
2
+ import { JSONObject } from "../../../Types/JSON";
3
+ import KubernetesClusterCpuTileRepair, {
4
+ BUSIEST_NODE_CPU_TILE_TITLE,
5
+ KUBERNETES_NODE_CPU_USAGE_METRIC,
6
+ KubernetesClusterCpuTileRepairResult,
7
+ LEGACY_CLUSTER_CPU_TILE_TITLE,
8
+ } from "../../../Utils/Dashboard/KubernetesClusterCpuTileRepair";
9
+ import { describe, expect, test } from "@jest/globals";
10
+
11
+ /*
12
+ * The repair behind the RepairKubernetesDashboardClusterCpuTile migration.
13
+ * Existing Kubernetes dashboards hold a COPY of the template, so their
14
+ * "Cluster CPU (cores in use)" tile still sums k8s.node.cpu.usage over every
15
+ * node, scrape and bucket. What is pinned here: exactly which tiles are
16
+ * rewritten, to what, that nothing else on the dashboard moves, and that
17
+ * the Postgres filter the migration selects rows with agrees with the
18
+ * in-memory matcher.
19
+ *
20
+ * Fixtures are shaped like a stored dashboardViewConfig: plain jsonb, with
21
+ * componentId serialised as { _type: "ObjectID", value } and the
22
+ * misspelled `aggegationType` the fetch layer reads.
23
+ */
24
+
25
+ function makeValueTile(data: {
26
+ componentId?: string;
27
+ title?: string;
28
+ metricName?: string;
29
+ aggregation?: string;
30
+ extraArguments?: JSONObject;
31
+ extraFilterData?: JSONObject;
32
+ }): JSONObject {
33
+ return {
34
+ _type: "DashboardComponent",
35
+ componentType: "Value",
36
+ componentId: {
37
+ _type: "ObjectID",
38
+ value: data.componentId || "0b6a2c3e-0000-4000-8000-000000000001",
39
+ },
40
+ topInDashboardUnits: 11,
41
+ leftInDashboardUnits: 0,
42
+ widthInDashboardUnits: 4,
43
+ heightInDashboardUnits: 1,
44
+ minHeightInDashboardUnits: 1,
45
+ minWidthInDashboardUnits: 1,
46
+ arguments: {
47
+ title: data.title ?? LEGACY_CLUSTER_CPU_TILE_TITLE,
48
+ metricQueryConfig: {
49
+ metricQueryData: {
50
+ filterData: {
51
+ metricName: data.metricName ?? KUBERNETES_NODE_CPU_USAGE_METRIC,
52
+ aggegationType: data.aggregation ?? AggregationType.Sum,
53
+ ...(data.extraFilterData || {}),
54
+ },
55
+ },
56
+ },
57
+ trendDirection: "HigherIsWorse",
58
+ ...(data.extraArguments || {}),
59
+ },
60
+ };
61
+ }
62
+
63
+ function makeConfig(components: Array<unknown>): JSONObject {
64
+ return {
65
+ _type: "DashboardViewConfig",
66
+ heightInDashboardUnits: 21,
67
+ variables: [
68
+ {
69
+ id: "cluster",
70
+ name: "Cluster",
71
+ type: "Telemetry Attribute",
72
+ attributeKey: "resource.k8s.cluster.name",
73
+ },
74
+ ],
75
+ components: components as Array<JSONObject>,
76
+ };
77
+ }
78
+
79
+ function componentsOf(config: JSONObject): Array<JSONObject> {
80
+ return config["components"] as Array<JSONObject>;
81
+ }
82
+
83
+ function filterDataOf(component: JSONObject): JSONObject {
84
+ const args: JSONObject = component["arguments"] as JSONObject;
85
+ const metricQueryConfig: JSONObject = args["metricQueryConfig"] as JSONObject;
86
+ const metricQueryData: JSONObject = metricQueryConfig[
87
+ "metricQueryData"
88
+ ] as JSONObject;
89
+
90
+ return metricQueryData["filterData"] as JSONObject;
91
+ }
92
+
93
+ /*
94
+ * Postgres `@>` for jsonb, as documented (8.14.3): an object contains
95
+ * another when every key of the right side is present and contains its
96
+ * value; an array contains another when every element of the right side is
97
+ * contained by SOME element of the left; scalars must be equal. Used only to
98
+ * show that the migration's SQL filter and isLegacyTile pick the same rows.
99
+ */
100
+ function jsonbContains(left: unknown, right: unknown): boolean {
101
+ if (Array.isArray(right)) {
102
+ return (
103
+ Array.isArray(left) &&
104
+ right.every((rightItem: unknown) => {
105
+ return left.some((leftItem: unknown) => {
106
+ return jsonbContains(leftItem, rightItem);
107
+ });
108
+ })
109
+ );
110
+ }
111
+
112
+ if (right && typeof right === "object") {
113
+ if (!left || typeof left !== "object" || Array.isArray(left)) {
114
+ return false;
115
+ }
116
+
117
+ return Object.entries(right as JSONObject).every(
118
+ ([key, value]: [string, unknown]) => {
119
+ return (
120
+ Object.prototype.hasOwnProperty.call(left, key) &&
121
+ jsonbContains((left as JSONObject)[key], value)
122
+ );
123
+ },
124
+ );
125
+ }
126
+
127
+ return left === right;
128
+ }
129
+
130
+ const TEXT_HEADER: JSONObject = {
131
+ _type: "DashboardComponent",
132
+ componentType: "Text",
133
+ componentId: { _type: "ObjectID", value: "text-header" },
134
+ arguments: { text: "Resource Health", isBold: true },
135
+ };
136
+
137
+ const NETWORK_CHART: JSONObject = {
138
+ _type: "DashboardComponent",
139
+ componentType: "Chart",
140
+ componentId: { _type: "ObjectID", value: "network-chart" },
141
+ arguments: {
142
+ chartTitle: "Network I/O",
143
+ metricQueryConfig: {
144
+ metricQueryData: {
145
+ filterData: {
146
+ metricName: "k8s.pod.network.io",
147
+ aggegationType: AggregationType.Sum,
148
+ },
149
+ },
150
+ },
151
+ },
152
+ };
153
+
154
+ describe("KubernetesClusterCpuTileRepair", () => {
155
+ describe("repair", () => {
156
+ test("rewrites the legacy tile to the busiest node's peak CPU", () => {
157
+ const result: KubernetesClusterCpuTileRepairResult | null =
158
+ KubernetesClusterCpuTileRepair.repair(
159
+ makeConfig([TEXT_HEADER, makeValueTile({}), NETWORK_CHART]),
160
+ );
161
+
162
+ expect(result).not.toBeNull();
163
+ expect(result!.repairedTileCount).toBe(1);
164
+
165
+ const tile: JSONObject = componentsOf(result!.config)[1]!;
166
+ expect((tile["arguments"] as JSONObject)["title"]).toBe(
167
+ BUSIEST_NODE_CPU_TILE_TITLE,
168
+ );
169
+ expect(filterDataOf(tile)["aggegationType"]).toBe(AggregationType.Max);
170
+ expect(filterDataOf(tile)["metricName"]).toBe(
171
+ KUBERNETES_NODE_CPU_USAGE_METRIC,
172
+ );
173
+ });
174
+
175
+ test("changes only the title and the aggregation", () => {
176
+ const original: JSONObject = makeConfig([
177
+ TEXT_HEADER,
178
+ makeValueTile({
179
+ extraArguments: { warningThreshold: 3, criticalThreshold: 6 },
180
+ extraFilterData: { attributes: { "resource.k8s.node.name": "a" } },
181
+ }),
182
+ NETWORK_CHART,
183
+ ]);
184
+
185
+ const expected: JSONObject = JSON.parse(JSON.stringify(original));
186
+ const expectedTile: JSONObject = componentsOf(expected)[1]!;
187
+ (expectedTile["arguments"] as JSONObject)["title"] =
188
+ BUSIEST_NODE_CPU_TILE_TITLE;
189
+ filterDataOf(expectedTile)["aggegationType"] = AggregationType.Max;
190
+
191
+ expect(KubernetesClusterCpuTileRepair.repair(original)!.config).toEqual(
192
+ expected,
193
+ );
194
+ });
195
+
196
+ test("never mutates the config it was given", () => {
197
+ const original: JSONObject = makeConfig([makeValueTile({})]);
198
+ const snapshot: string = JSON.stringify(original);
199
+
200
+ KubernetesClusterCpuTileRepair.repair(original);
201
+
202
+ expect(JSON.stringify(original)).toBe(snapshot);
203
+ });
204
+
205
+ test("repairs every copy of the tile on one dashboard", () => {
206
+ const result: KubernetesClusterCpuTileRepairResult | null =
207
+ KubernetesClusterCpuTileRepair.repair(
208
+ makeConfig([
209
+ makeValueTile({ componentId: "first" }),
210
+ makeValueTile({ componentId: "second" }),
211
+ ]),
212
+ );
213
+
214
+ expect(result!.repairedTileCount).toBe(2);
215
+ for (const tile of componentsOf(result!.config)) {
216
+ expect(filterDataOf(tile)["aggegationType"]).toBe(AggregationType.Max);
217
+ }
218
+ });
219
+
220
+ test("is a no-op on its own output, so a second run writes nothing", () => {
221
+ const once: KubernetesClusterCpuTileRepairResult | null =
222
+ KubernetesClusterCpuTileRepair.repair(makeConfig([makeValueTile({})]));
223
+
224
+ expect(once).not.toBeNull();
225
+ expect(KubernetesClusterCpuTileRepair.repair(once!.config)).toBeNull();
226
+ });
227
+
228
+ /*
229
+ * The migration only corrects what the template put there. A user who
230
+ * renamed the tile, re-aggregated it, pointed it at another metric or
231
+ * drew the same query as a chart made that choice themselves.
232
+ */
233
+ test.each([
234
+ ["a renamed tile", makeValueTile({ title: "All Nodes CPU" })],
235
+ [
236
+ "a title that differs only in whitespace",
237
+ makeValueTile({ title: ` ${LEGACY_CLUSTER_CPU_TILE_TITLE}` }),
238
+ ],
239
+ [
240
+ "a tile already re-aggregated to Avg",
241
+ makeValueTile({ aggregation: AggregationType.Avg }),
242
+ ],
243
+ [
244
+ "a tile already re-aggregated to Max",
245
+ makeValueTile({ aggregation: AggregationType.Max }),
246
+ ],
247
+ [
248
+ "a Sum tile over another metric",
249
+ makeValueTile({ metricName: "k8s.pod.cpu.usage" }),
250
+ ],
251
+ [
252
+ "a chart with the same title, metric and Sum",
253
+ { ...makeValueTile({}), componentType: "Chart" },
254
+ ],
255
+ ["a text widget", TEXT_HEADER],
256
+ ])("leaves %s alone", (_label: string, component: JSONObject) => {
257
+ expect(
258
+ KubernetesClusterCpuTileRepair.repair(makeConfig([component])),
259
+ ).toBeNull();
260
+ });
261
+
262
+ test.each([
263
+ ["undefined", undefined],
264
+ ["null", null],
265
+ ["a string", "not a config"],
266
+ ["an array", [makeValueTile({})]],
267
+ ["a config without components", { _type: "DashboardViewConfig" }],
268
+ [
269
+ "a config whose components are not an array",
270
+ { components: { 0: makeValueTile({}) } },
271
+ ],
272
+ ["a config with no components", makeConfig([])],
273
+ ])("returns null for %s", (_label: string, config: unknown) => {
274
+ expect(KubernetesClusterCpuTileRepair.repair(config)).toBeNull();
275
+ });
276
+
277
+ test("skips malformed components without throwing", () => {
278
+ const result: KubernetesClusterCpuTileRepairResult | null =
279
+ KubernetesClusterCpuTileRepair.repair(
280
+ makeConfig([
281
+ null,
282
+ "text",
283
+ 42,
284
+ { componentType: "Value" },
285
+ { componentType: "Value", arguments: null },
286
+ {
287
+ componentType: "Value",
288
+ arguments: { title: LEGACY_CLUSTER_CPU_TILE_TITLE },
289
+ },
290
+ makeValueTile({}),
291
+ ]),
292
+ );
293
+
294
+ expect(result!.repairedTileCount).toBe(1);
295
+ expect(componentsOf(result!.config).slice(0, 6)).toEqual([
296
+ null,
297
+ "text",
298
+ 42,
299
+ { componentType: "Value" },
300
+ { componentType: "Value", arguments: null },
301
+ {
302
+ componentType: "Value",
303
+ arguments: { title: LEGACY_CLUSTER_CPU_TILE_TITLE },
304
+ },
305
+ ]);
306
+ });
307
+ });
308
+
309
+ describe("getLegacyTileContainment", () => {
310
+ /*
311
+ * The migration reads only the dashboards Postgres matches with this
312
+ * document, then repairs them with isLegacyTile. If the two disagree,
313
+ * a dashboard is either never read or read and left unrepaired.
314
+ */
315
+ test.each([
316
+ ["the legacy tile", makeConfig([TEXT_HEADER, makeValueTile({})]), true],
317
+ [
318
+ "the legacy tile with user thresholds and filters",
319
+ makeConfig([
320
+ makeValueTile({
321
+ extraArguments: { warningThreshold: 3 },
322
+ extraFilterData: { attributes: { "resource.k8s.node.name": "a" } },
323
+ }),
324
+ ]),
325
+ true,
326
+ ],
327
+ [
328
+ "a repaired tile",
329
+ makeConfig([
330
+ makeValueTile({
331
+ title: BUSIEST_NODE_CPU_TILE_TITLE,
332
+ aggregation: AggregationType.Max,
333
+ }),
334
+ ]),
335
+ false,
336
+ ],
337
+ [
338
+ "the title on one tile and the Sum on another",
339
+ makeConfig([
340
+ makeValueTile({ aggregation: AggregationType.Avg }),
341
+ makeValueTile({ title: "All Nodes CPU" }),
342
+ ]),
343
+ false,
344
+ ],
345
+ [
346
+ "a chart with the legacy signature",
347
+ makeConfig([{ ...makeValueTile({}), componentType: "Chart" }]),
348
+ false,
349
+ ],
350
+ [
351
+ "an unrelated dashboard",
352
+ makeConfig([TEXT_HEADER, NETWORK_CHART]),
353
+ false,
354
+ ],
355
+ ])(
356
+ "agrees with repair() on %s",
357
+ (_label: string, config: JSONObject, shouldMatch: boolean) => {
358
+ expect(
359
+ jsonbContains(
360
+ config,
361
+ KubernetesClusterCpuTileRepair.getLegacyTileContainment(),
362
+ ),
363
+ ).toBe(shouldMatch);
364
+ expect(KubernetesClusterCpuTileRepair.repair(config) !== null).toBe(
365
+ shouldMatch,
366
+ );
367
+ },
368
+ );
369
+ });
370
+ });