@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
@@ -9,14 +9,16 @@ describe("AggregationType", () => {
9
9
  describe("PercentileAggregationLevels", () => {
10
10
  test("maps each percentile enum to its quantile fraction", () => {
11
11
  expect(PercentileAggregationLevels[AggregationType.P50]).toBe(0.5);
12
+ expect(PercentileAggregationLevels[AggregationType.P75]).toBe(0.75);
12
13
  expect(PercentileAggregationLevels[AggregationType.P90]).toBe(0.9);
13
14
  expect(PercentileAggregationLevels[AggregationType.P95]).toBe(0.95);
14
15
  expect(PercentileAggregationLevels[AggregationType.P99]).toBe(0.99);
15
16
  });
16
17
 
17
- test("contains exactly the four percentile aggregations", () => {
18
+ test("contains exactly the five percentile aggregations", () => {
18
19
  expect(Object.keys(PercentileAggregationLevels).sort()).toEqual([
19
20
  "P50",
21
+ "P75",
20
22
  "P90",
21
23
  "P95",
22
24
  "P99",
@@ -27,6 +29,7 @@ describe("AggregationType", () => {
27
29
  describe("isPercentileAggregation", () => {
28
30
  test("returns true for every percentile aggregation", () => {
29
31
  expect(isPercentileAggregation(AggregationType.P50)).toBe(true);
32
+ expect(isPercentileAggregation(AggregationType.P75)).toBe(true);
30
33
  expect(isPercentileAggregation(AggregationType.P90)).toBe(true);
31
34
  expect(isPercentileAggregation(AggregationType.P95)).toBe(true);
32
35
  expect(isPercentileAggregation(AggregationType.P99)).toBe(true);
@@ -44,6 +47,7 @@ describe("AggregationType", () => {
44
47
  describe("getPercentileLevel", () => {
45
48
  test("returns the quantile fraction for percentile aggregations", () => {
46
49
  expect(getPercentileLevel(AggregationType.P50)).toBe(0.5);
50
+ expect(getPercentileLevel(AggregationType.P75)).toBe(0.75);
47
51
  expect(getPercentileLevel(AggregationType.P99)).toBe(0.99);
48
52
  });
49
53
 
@@ -0,0 +1,184 @@
1
+ import AggregationType from "../../../Types/BaseDatabase/AggregationType";
2
+ import DashboardBaseComponent from "../../../Types/Dashboard/DashboardComponents/DashboardBaseComponent";
3
+ import DashboardComponentType from "../../../Types/Dashboard/DashboardComponentType";
4
+ import DashboardViewConfig from "../../../Types/Dashboard/DashboardViewConfig";
5
+ import {
6
+ DashboardTemplateType,
7
+ getTemplateConfig,
8
+ } from "../../../Types/Dashboard/DashboardTemplates";
9
+ import KubernetesClusterCpuTileRepair, {
10
+ BUSIEST_NODE_CPU_TILE_TITLE,
11
+ KUBERNETES_NODE_CPU_USAGE_METRIC,
12
+ LEGACY_CLUSTER_CPU_TILE_TITLE,
13
+ } from "../../../Utils/Dashboard/KubernetesClusterCpuTileRepair";
14
+ import { describe, expect, test } from "@jest/globals";
15
+
16
+ /*
17
+ * Editorial invariants for the Kubernetes template specifically. The
18
+ * structural rules every template must satisfy live in
19
+ * DashboardTemplateInvariants; this file pins the aggregation of the
20
+ * Kubernetes Value tiles.
21
+ *
22
+ * Every metric the Kubernetes template reads into a Value tile is a gauge
23
+ * the agent re-emits for every node or pod on every scrape. A Value tile
24
+ * reduces the whole window to one number: the server aggregates each time
25
+ * bucket over every series and scrape in it, and the widget then folds the
26
+ * buckets (ValueWidgetData.aggregateValues). With `Sum` both steps add, so
27
+ * the number grows with the node count, the scrape rate and the window:
28
+ * "Cluster CPU (cores in use)" read ~540 for three nodes on 1.5 cores each
29
+ * over the default hour. Avg, Max and Min have no such multiplier.
30
+ */
31
+
32
+ function loadKubernetesTemplate(): DashboardViewConfig {
33
+ const config: DashboardViewConfig | null = getTemplateConfig(
34
+ DashboardTemplateType.Kubernetes,
35
+ );
36
+
37
+ if (!config) {
38
+ throw new Error("Expected the Kubernetes template to resolve to a config");
39
+ }
40
+
41
+ return config;
42
+ }
43
+
44
+ function getArguments(
45
+ component: DashboardBaseComponent,
46
+ ): Record<string, unknown> {
47
+ return (component.arguments as Record<string, unknown>) || {};
48
+ }
49
+
50
+ function getFilterData(
51
+ component: DashboardBaseComponent,
52
+ ): Record<string, unknown> {
53
+ const queryConfig: Record<string, unknown> | undefined = getArguments(
54
+ component,
55
+ )["metricQueryConfig"] as Record<string, unknown> | undefined;
56
+ const queryData: Record<string, unknown> | undefined = queryConfig?.[
57
+ "metricQueryData"
58
+ ] as Record<string, unknown> | undefined;
59
+
60
+ return (queryData?.["filterData"] as Record<string, unknown>) || {};
61
+ }
62
+
63
+ function getValueTiles(
64
+ config: DashboardViewConfig,
65
+ ): Array<DashboardBaseComponent> {
66
+ return config.components.filter(
67
+ (component: DashboardBaseComponent): boolean => {
68
+ return component.componentType === DashboardComponentType.Value;
69
+ },
70
+ );
71
+ }
72
+
73
+ function describeTile(component: DashboardBaseComponent): string {
74
+ const filterData: Record<string, unknown> = getFilterData(component);
75
+
76
+ return `"${String(getArguments(component)["title"])}" ${String(
77
+ filterData["metricName"],
78
+ )}/${String(filterData["aggegationType"])}`;
79
+ }
80
+
81
+ describe("Kubernetes dashboard template", () => {
82
+ describe("busiest node CPU tile", () => {
83
+ function findNodeCpuTiles(): Array<DashboardBaseComponent> {
84
+ return getValueTiles(loadKubernetesTemplate()).filter(
85
+ (component: DashboardBaseComponent): boolean => {
86
+ return (
87
+ getFilterData(component)["metricName"] ===
88
+ KUBERNETES_NODE_CPU_USAGE_METRIC
89
+ );
90
+ },
91
+ );
92
+ }
93
+
94
+ test("reads k8s.node.cpu.usage with Max, never Sum", (): void => {
95
+ const busiestNodeTile: DashboardBaseComponent | undefined =
96
+ findNodeCpuTiles().find((component: DashboardBaseComponent) => {
97
+ return (
98
+ getArguments(component)["title"] === BUSIEST_NODE_CPU_TILE_TITLE
99
+ );
100
+ });
101
+
102
+ expect(busiestNodeTile).toBeDefined();
103
+ expect(getFilterData(busiestNodeTile!)["aggegationType"]).toBe(
104
+ AggregationType.Max,
105
+ );
106
+ });
107
+
108
+ test("no longer ships the summed cluster CPU tile under any title", (): void => {
109
+ const nodeCpuTiles: Array<DashboardBaseComponent> = findNodeCpuTiles();
110
+
111
+ expect(nodeCpuTiles.length).toBeGreaterThan(0);
112
+
113
+ for (const component of nodeCpuTiles) {
114
+ expect(describeTile(component)).not.toContain(
115
+ `/${AggregationType.Sum}`,
116
+ );
117
+ expect(getArguments(component)["title"]).not.toBe(
118
+ LEGACY_CLUSTER_CPU_TILE_TITLE,
119
+ );
120
+ }
121
+ });
122
+
123
+ /*
124
+ * The migration writes BUSIEST_NODE_CPU_TILE_TITLE and Max onto
125
+ * existing dashboards. If the template and the repair drift apart, a
126
+ * repaired dashboard and a freshly created one show different tiles.
127
+ */
128
+ test("is exactly what the repair writes onto existing dashboards", (): void => {
129
+ const busiestNodeTile: DashboardBaseComponent | undefined =
130
+ findNodeCpuTiles().find((component: DashboardBaseComponent) => {
131
+ return (
132
+ getArguments(component)["title"] === BUSIEST_NODE_CPU_TILE_TITLE
133
+ );
134
+ });
135
+
136
+ expect(busiestNodeTile).toBeDefined();
137
+
138
+ const legacyTile: DashboardBaseComponent = JSON.parse(
139
+ JSON.stringify(busiestNodeTile),
140
+ );
141
+ (legacyTile.arguments as Record<string, unknown>)["title"] =
142
+ LEGACY_CLUSTER_CPU_TILE_TITLE;
143
+ getFilterData(legacyTile)["aggegationType"] = AggregationType.Sum;
144
+
145
+ const repaired: ReturnType<typeof KubernetesClusterCpuTileRepair.repair> =
146
+ KubernetesClusterCpuTileRepair.repair({ components: [legacyTile] });
147
+
148
+ expect(repaired).not.toBeNull();
149
+ expect(
150
+ (repaired!.config["components"] as Array<unknown>)[0],
151
+ ).toStrictEqual(JSON.parse(JSON.stringify(busiestNodeTile)));
152
+ });
153
+
154
+ test("is not matched by the repair, so a new dashboard is never rewritten", (): void => {
155
+ expect(
156
+ KubernetesClusterCpuTileRepair.repair(
157
+ JSON.parse(JSON.stringify(loadKubernetesTemplate())),
158
+ )?.repairedTileCount ?? 0,
159
+ ).toBe(0);
160
+ });
161
+ });
162
+
163
+ test("no Value tile sums a per-resource gauge across the window", (): void => {
164
+ const valueTiles: Array<DashboardBaseComponent> = getValueTiles(
165
+ loadKubernetesTemplate(),
166
+ );
167
+
168
+ expect(valueTiles.length).toBeGreaterThan(0);
169
+
170
+ for (const component of valueTiles) {
171
+ const aggregation: unknown = getFilterData(component)["aggegationType"];
172
+
173
+ expect(describeTile(component)).toBe(
174
+ `"${String(getArguments(component)["title"])}" ${String(
175
+ getFilterData(component)["metricName"],
176
+ )}/${
177
+ aggregation === AggregationType.Sum
178
+ ? "<not Sum>"
179
+ : String(aggregation)
180
+ }`,
181
+ );
182
+ }
183
+ });
184
+ });
@@ -0,0 +1,107 @@
1
+ import DashboardViewConfig from "../../../Types/Dashboard/DashboardViewConfig";
2
+ import {
3
+ DashboardTemplateType,
4
+ getTemplateConfig,
5
+ } from "../../../Types/Dashboard/DashboardTemplates";
6
+ import { AGENT_EMITTED_METRIC_NAMES } from "../Monitor/Utils/KubernetesAgentEmittedMetrics";
7
+ import { describe, expect, test } from "@jest/globals";
8
+
9
+ /*
10
+ * The Kubernetes dashboard templates chart what the kubernetes-agent chart
11
+ * sends, and nothing else feeds them. A widget naming a metric the agent
12
+ * never sends does not error — its query returns zero rows and the widget
13
+ * renders empty on every dashboard created from the template.
14
+ *
15
+ * That shipped: the Kubernetes template's four CPU widgets read
16
+ * `k8s.pod.cpu.usage` and `k8s.node.cpu.usage`, which kubeletstats at the
17
+ * pinned collector leaves off by default, before the chart enabled them.
18
+ * Nothing checked the dashboards against AGENT_EMITTED_METRIC_NAMES, so
19
+ * this holds every widget of both templates to it.
20
+ */
21
+ const KUBERNETES_TEMPLATE_TYPES: Array<DashboardTemplateType> = [
22
+ DashboardTemplateType.Kubernetes,
23
+ DashboardTemplateType.KubernetesCost,
24
+ ];
25
+
26
+ interface MetricQuery {
27
+ widget: string;
28
+ metricName: string;
29
+ }
30
+
31
+ function getConfig(type: DashboardTemplateType): DashboardViewConfig {
32
+ const config: DashboardViewConfig | null = getTemplateConfig(type);
33
+
34
+ if (!config) {
35
+ throw new Error(`Expected ${type} to resolve to a config`);
36
+ }
37
+
38
+ return config;
39
+ }
40
+
41
+ /*
42
+ * Every `filterData.metricName` anywhere in a widget's arguments, so a
43
+ * widget that carries several queries cannot hide one from the check.
44
+ */
45
+ function collectMetricNames(value: unknown, into: Array<string>): void {
46
+ if (!value || typeof value !== "object") {
47
+ return;
48
+ }
49
+
50
+ const record: Record<string, unknown> = value as Record<string, unknown>;
51
+ const filterData: Record<string, unknown> | undefined = record[
52
+ "filterData"
53
+ ] as Record<string, unknown> | undefined;
54
+
55
+ if (filterData && typeof filterData["metricName"] === "string") {
56
+ into.push(filterData["metricName"]);
57
+ }
58
+
59
+ for (const child of Object.values(record)) {
60
+ collectMetricNames(child, into);
61
+ }
62
+ }
63
+
64
+ function getMetricQueries(type: DashboardTemplateType): Array<MetricQuery> {
65
+ const queries: Array<MetricQuery> = [];
66
+
67
+ for (const component of getConfig(type).components) {
68
+ const args: Record<string, unknown> =
69
+ (component.arguments as Record<string, unknown>) || {};
70
+ const label: unknown =
71
+ args["title"] ??
72
+ args["chartTitle"] ??
73
+ args["gaugeTitle"] ??
74
+ args["tableTitle"];
75
+ const metricNames: Array<string> = [];
76
+
77
+ collectMetricNames(args, metricNames);
78
+
79
+ for (const metricName of metricNames) {
80
+ queries.push({
81
+ widget: `${component.componentType} "${String(label ?? "<untitled>")}"`,
82
+ metricName: metricName,
83
+ });
84
+ }
85
+ }
86
+
87
+ return queries;
88
+ }
89
+
90
+ describe("Kubernetes dashboard templates", () => {
91
+ describe.each(KUBERNETES_TEMPLATE_TYPES)(
92
+ "%s",
93
+ (type: DashboardTemplateType) => {
94
+ test("queries at least one metric (guards the guard)", () => {
95
+ expect(getMetricQueries(type).length).toBeGreaterThan(0);
96
+ });
97
+
98
+ test("every widget queries a metric the agent emits", () => {
99
+ for (const query of getMetricQueries(type)) {
100
+ expect(
101
+ `${query.widget} → ${query.metricName} emitted: ${AGENT_EMITTED_METRIC_NAMES.has(query.metricName)}`,
102
+ ).toBe(`${query.widget} → ${query.metricName} emitted: true`);
103
+ }
104
+ });
105
+ },
106
+ );
107
+ });
@@ -1161,25 +1161,19 @@ describe("KubernetesAlertTemplates - deployment replica mismatch", () => {
1161
1161
  });
1162
1162
 
1163
1163
  /*
1164
- * The same bug, generalised: any template reading a k8s_cluster or
1165
- * kubeletstats series must read one the agent actually sends. That
1166
- * covers node, pod and container metrics too, including the kubeletstats
1167
- * ones that are off upstream and on only because the chart enables them
1168
- * (see the high node CPU block below). Undotted names come from the
1169
- * chart's Prometheus scrapes (control plane, cAdvisor, kube-state-metrics),
1170
- * which this list does not cover.
1164
+ * The same bug, generalised: every query of every template must read a
1165
+ * series the agent actually sends, whichever receiver or scrape it comes
1166
+ * from. That covers node, pod and container metrics, including the
1167
+ * kubeletstats ones that are off upstream and on only because the chart
1168
+ * enables them (see the high node CPU block below), and the undotted
1169
+ * names from the chart's Prometheus scrapes — control plane, cAdvisor,
1170
+ * kube-state-metrics, cost engine.
1171
1171
  */
1172
- test("no template queries a k8s_cluster or kubeletstats metric the agent does not emit", () => {
1173
- const receiverMetric: RegExp = /^(k8s|container)\./;
1174
-
1172
+ test("no template queries a metric the agent does not emit", () => {
1175
1173
  for (const template of getAllKubernetesAlertTemplates()) {
1176
1174
  for (const query of getQueryConfigs(template.id)) {
1177
1175
  const metricName: string = query.metricQueryData.filterData.metricName;
1178
1176
 
1179
- if (!receiverMetric.test(metricName)) {
1180
- continue;
1181
- }
1182
-
1183
1177
  expect(`${template.id}: ${metricName}`).toBe(
1184
1178
  AGENT_EMITTED_METRIC_NAMES.has(metricName)
1185
1179
  ? `${template.id}: ${metricName}`
@@ -526,9 +526,10 @@ describe("KubernetesMetricCatalog", () => {
526
526
  describe("every entry names a metric the shipped agent emits", () => {
527
527
  test("the agent chart still pins the collector these names were read from", () => {
528
528
  /*
529
- * AGENT_EMITTED_METRIC_NAMES is the receivers' metadata.yaml at this
530
- * collector version. Bumping the image fails here until that list is
531
- * re-read — receivers rename metrics between versions.
529
+ * The receiver half of AGENT_EMITTED_METRIC_NAMES is the receivers'
530
+ * metadata.yaml at this collector version. Bumping the image fails
531
+ * here until that list is re-read — receivers rename metrics between
532
+ * versions.
532
533
  */
533
534
  const values: Record<string, any> = readAgentChartValues();
534
535
 
@@ -161,12 +161,6 @@ export const CONTROL_PLANE_SCRAPE_METRICS: ReadonlyArray<string> = [
161
161
  "scheduler_pending_pods",
162
162
  ];
163
163
 
164
- export const AGENT_EMITTED_METRIC_NAMES: ReadonlySet<string> = new Set<string>([
165
- ...K8S_CLUSTER_RECEIVER_METRICS,
166
- ...KUBELETSTATS_RECEIVER_METRICS,
167
- ...CONTROL_PLANE_SCRAPE_METRICS,
168
- ]);
169
-
170
164
  const AGENT_CHART_VALUES_PATH: string = path.resolve(
171
165
  __dirname,
172
166
  "../../../../../../HelmChart/Public/kubernetes-agent/values.yaml",
@@ -179,3 +173,30 @@ export function readAgentChartValues(): Record<string, any> {
179
173
  any
180
174
  >;
181
175
  }
176
+
177
+ /*
178
+ * The chart's Prometheus scrapes that carry an allowlist — cAdvisor on the
179
+ * DaemonSet (on by default), kube-state-metrics and the cost engine on the
180
+ * Deployment (opt-in). Each job's `metric_relabel_configs` keeps exactly
181
+ * `^(<allowlist joined by |>)$`, and the prometheus receiver trims `_total`
182
+ * and unit suffixes only under `trim_metric_suffixes`, which is off by
183
+ * default and which the chart never sets. So the names that arrive are the
184
+ * allowlist entries verbatim — read from values.yaml rather than copied,
185
+ * so the two cannot drift.
186
+ */
187
+ const agentChartValues: Record<string, any> = readAgentChartValues();
188
+
189
+ export const ALLOWLISTED_SCRAPE_METRICS: ReadonlyArray<string> = [
190
+ ...(agentChartValues["cadvisor"]["metricsAllowlist"] as Array<string>),
191
+ ...(agentChartValues["kubeStateMetrics"][
192
+ "metricsAllowlist"
193
+ ] as Array<string>),
194
+ ...(agentChartValues["cost"]["metrics"]["metricsAllowlist"] as Array<string>),
195
+ ];
196
+
197
+ export const AGENT_EMITTED_METRIC_NAMES: ReadonlySet<string> = new Set<string>([
198
+ ...K8S_CLUSTER_RECEIVER_METRICS,
199
+ ...KUBELETSTATS_RECEIVER_METRICS,
200
+ ...CONTROL_PLANE_SCRAPE_METRICS,
201
+ ...ALLOWLISTED_SCRAPE_METRICS,
202
+ ]);
@@ -0,0 +1,176 @@
1
+ import { describe, expect, test } from "@jest/globals";
2
+ import { compareCodePoints } from "../../../Types/Topology/TopologyTypeRules";
3
+
4
+ /*
5
+ * compareCodePoints is how the Dashboard breaks the ties the server breaks
6
+ * with COLLATE "C" (code-point order on a UTF-8 database). It must equal
7
+ * JavaScript's `<` for every string made of characters up to U+FFFF, and
8
+ * order characters beyond U+FFFF (surrogate pairs) after U+E000-U+FFFF,
9
+ * which `<` does not.
10
+ */
11
+
12
+ function sign(value: number): number {
13
+ return value < 0 ? -1 : value > 0 ? 1 : 0;
14
+ }
15
+
16
+ /* An independent reference: compare the strings' code points one by one. */
17
+ function referenceCompare(a: string, b: string): number {
18
+ const left: Array<number> = Array.from(a).map((char: string): number => {
19
+ return char.codePointAt(0)!;
20
+ });
21
+ const right: Array<number> = Array.from(b).map((char: string): number => {
22
+ return char.codePointAt(0)!;
23
+ });
24
+ const length: number = Math.min(left.length, right.length);
25
+ for (let index: number = 0; index < length; index++) {
26
+ if (left[index] !== right[index]) {
27
+ return left[index]! < right[index]! ? -1 : 1;
28
+ }
29
+ }
30
+ return sign(left.length - right.length);
31
+ }
32
+
33
+ function codeUnitCompare(a: string, b: string): number {
34
+ return a < b ? -1 : a > b ? 1 : 0;
35
+ }
36
+
37
+ /* A small deterministic generator, so a failure names its inputs. */
38
+ function makeRandom(seed: number): () => number {
39
+ let state: number = seed >>> 0;
40
+ return (): number => {
41
+ state = (state * 1664525 + 1013904223) >>> 0;
42
+ return state / 4294967296;
43
+ };
44
+ }
45
+
46
+ /* Characters from the ranges where the two orders meet or part ways. */
47
+ const ALPHABET: Array<string> = [
48
+ "a",
49
+ "b",
50
+ "z",
51
+ "0",
52
+ "-",
53
+ "é",
54
+ "中",
55
+ "퟿",
56
+ "",
57
+ "",
58
+ "fi",
59
+ "�",
60
+ "￿",
61
+ "\u{10000}",
62
+ "\u{1F600}",
63
+ "\u{1F601}",
64
+ "\u{10FFFF}",
65
+ ];
66
+
67
+ function randomString(random: () => number, alphabet: Array<string>): string {
68
+ const length: number = Math.floor(random() * 5);
69
+ let out: string = "";
70
+ for (let index: number = 0; index < length; index++) {
71
+ out += alphabet[Math.floor(random() * alphabet.length)]!;
72
+ }
73
+ return out;
74
+ }
75
+
76
+ describe("compareCodePoints", () => {
77
+ test("equal strings compare equal", () => {
78
+ expect(compareCodePoints("", "")).toBe(0);
79
+ expect(compareCodePoints("node-a", "node-a")).toBe(0);
80
+ expect(compareCodePoints("\u{1F600}", "\u{1F600}")).toBe(0);
81
+ });
82
+
83
+ test("inside the Basic Multilingual Plane it orders exactly like `<`", () => {
84
+ const pairs: Array<[string, string]> = [
85
+ ["a", "b"],
86
+ ["node-a", "node-b"],
87
+ ["node", "node-a"],
88
+ ["", "a"],
89
+ ["Z", "a"],
90
+ ["a", "é"],
91
+ ["퟿", ""],
92
+ ["", "�"],
93
+ ["�", "￿"],
94
+ ["0123456789abcdef", "0123456789abcdf0"],
95
+ ];
96
+ for (const [a, b] of pairs) {
97
+ expect(compareCodePoints(a, b)).toBe(-1);
98
+ expect(compareCodePoints(b, a)).toBe(1);
99
+ expect(sign(compareCodePoints(a, b))).toBe(codeUnitCompare(a, b));
100
+ }
101
+ });
102
+
103
+ test("a character beyond U+FFFF sorts after U+E000..U+FFFF, where `<` puts it before", () => {
104
+ for (const high of ["", "", "�", "￿"]) {
105
+ for (const astral of ["\u{10000}", "\u{1F600}", "\u{10FFFF}"]) {
106
+ expect(compareCodePoints(high, astral)).toBe(-1);
107
+ expect(compareCodePoints(astral, high)).toBe(1);
108
+ expect(compareCodePoints(`node-${high}`, `node-${astral}`)).toBe(-1);
109
+ expect(compareCodePoints(`node-${astral}x`, `node-${high}`)).toBe(1);
110
+ /* The order `<` gives, which is what this exists to correct. */
111
+ expect(codeUnitCompare(high, astral)).toBe(1);
112
+ }
113
+ }
114
+ });
115
+
116
+ test("a character beyond U+FFFF still sorts after everything below U+D800", () => {
117
+ expect(compareCodePoints("퟿", "\u{10000}")).toBe(-1);
118
+ expect(compareCodePoints("z", "\u{1F600}")).toBe(-1);
119
+ expect(compareCodePoints("\u{1F600}", "z")).toBe(1);
120
+ });
121
+
122
+ test("two characters beyond U+FFFF compare by code point, high or low half", () => {
123
+ /* Different high surrogates (U+10000 is D800 DC00, U+1F600 is D83D DE00). */
124
+ expect(compareCodePoints("\u{10000}", "\u{1F600}")).toBe(-1);
125
+ /* Same high surrogate, different low surrogates. */
126
+ expect(compareCodePoints("\u{1F600}", "\u{1F601}")).toBe(-1);
127
+ expect(compareCodePoints("x\u{1F601}", "x\u{1F600}")).toBe(1);
128
+ });
129
+
130
+ test("a prefix sorts first", () => {
131
+ expect(compareCodePoints("node", "node-\u{1F600}")).toBe(-1);
132
+ expect(compareCodePoints("node-\u{1F600}", "node")).toBe(1);
133
+ expect(compareCodePoints("\u{1F600}", "\u{1F600}")).toBe(-1);
134
+ });
135
+
136
+ test("agrees with a code-point reference on thousands of mixed strings", () => {
137
+ const random: () => number = makeRandom(3973);
138
+ let disagreementsWithCodeUnits: number = 0;
139
+ for (let round: number = 0; round < 20000; round++) {
140
+ const a: string = randomString(random, ALPHABET);
141
+ const b: string = randomString(random, ALPHABET);
142
+ const expected: number = referenceCompare(a, b);
143
+ if (sign(compareCodePoints(a, b)) !== expected) {
144
+ throw new Error(
145
+ `compareCodePoints(${JSON.stringify(a)}, ${JSON.stringify(b)}) should be ${expected}`,
146
+ );
147
+ }
148
+ if (codeUnitCompare(a, b) !== expected) {
149
+ disagreementsWithCodeUnits++;
150
+ }
151
+ }
152
+ /* The inputs really did reach the cases where `<` is wrong. */
153
+ expect(disagreementsWithCodeUnits).toBeGreaterThan(100);
154
+ });
155
+
156
+ test('sorts a list the way COLLATE "C" would', () => {
157
+ const keys: Array<string> = [
158
+ "node-\u{1F600}",
159
+ "node-�",
160
+ "node-b",
161
+ "node-",
162
+ "node-a",
163
+ "node-\u{10000}",
164
+ "node",
165
+ ];
166
+ expect([...keys].sort(compareCodePoints)).toEqual([
167
+ "node",
168
+ "node-a",
169
+ "node-b",
170
+ "node-",
171
+ "node-�",
172
+ "node-\u{10000}",
173
+ "node-\u{1F600}",
174
+ ]);
175
+ });
176
+ });
@@ -1845,6 +1845,82 @@ describe("ChunkLoader signal extraction", () => {
1845
1845
  expect(longTask?.offsetMs).toBe(9000);
1846
1846
  });
1847
1847
 
1848
+ /*
1849
+ * Issue #3975: an INP is now reported per single-page-app view, with
1850
+ * the view's url and the slow interaction's attribution. Each field is
1851
+ * read on its own, so one a newer or buggier recorder got wrong costs
1852
+ * only that field.
1853
+ */
1854
+ it("carries an INP's view and attribution, dropping only the fields that are malformed", () => {
1855
+ const rows: Array<ReplayTimelineEvent> = ChunkLoader.extractTimelineEvents(
1856
+ makeEntry(0),
1857
+ [
1858
+ { type: 2, timestamp: CUSTOM_BASE_TS, data: {} },
1859
+ custom(
1860
+ "oneuptime.performance",
1861
+ {
1862
+ kind: "web-vital",
1863
+ metric: "INP",
1864
+ value: 480,
1865
+ rating: "needs-improvement",
1866
+ url: "https://shop.example.com/products",
1867
+ navigationType: "soft",
1868
+ interactionType: "pointer",
1869
+ interactionTarget: "nav > a.product-link",
1870
+ inputDelayMs: 5,
1871
+ processingDurationMs: 305,
1872
+ presentationDelayMs: 170,
1873
+ },
1874
+ CUSTOM_BASE_TS + 100,
1875
+ ),
1876
+ custom(
1877
+ "oneuptime.performance",
1878
+ {
1879
+ kind: "web-vital",
1880
+ metric: "INP",
1881
+ value: 90,
1882
+ rating: "good",
1883
+ navigationType: "sideways",
1884
+ interactionType: 7,
1885
+ interactionTarget: "",
1886
+ inputDelayMs: -3,
1887
+ processingDurationMs: "12",
1888
+ presentationDelayMs: 40,
1889
+ },
1890
+ CUSTOM_BASE_TS + 200,
1891
+ ),
1892
+ ],
1893
+ );
1894
+
1895
+ const [attributed, malformed] = rows.filter(
1896
+ (row: ReplayTimelineEvent): boolean => {
1897
+ return row.metric === "INP";
1898
+ },
1899
+ );
1900
+
1901
+ expect(attributed).toEqual(
1902
+ expect.objectContaining({
1903
+ value: 480,
1904
+ url: "https://shop.example.com/products",
1905
+ navigationType: "soft",
1906
+ interactionType: "pointer",
1907
+ interactionTarget: "nav > a.product-link",
1908
+ inputDelayMs: 5,
1909
+ processingDurationMs: 305,
1910
+ presentationDelayMs: 170,
1911
+ }),
1912
+ );
1913
+
1914
+ /* The row itself survives; only the bad fields are gone. */
1915
+ expect(malformed?.value).toBe(90);
1916
+ expect(malformed?.presentationDelayMs).toBe(40);
1917
+ expect(malformed?.navigationType).toBeUndefined();
1918
+ expect(malformed?.interactionType).toBeUndefined();
1919
+ expect(malformed?.interactionTarget).toBeUndefined();
1920
+ expect(malformed?.inputDelayMs).toBeUndefined();
1921
+ expect(malformed?.processingDurationMs).toBeUndefined();
1922
+ });
1923
+
1848
1924
  it("turns rrweb Meta events into navigation rows carrying the viewport", () => {
1849
1925
  const rows: Array<ReplayTimelineEvent> = ChunkLoader.extractTimelineEvents(
1850
1926
  makeEntry(0),