@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.
- package/Models/DatabaseModels/InventoryItem.ts +23 -12
- package/Server/Infrastructure/Postgres/SchemaMigrations/1795200000000-AddInventoryItemProjectEntityKeyIndex.ts +217 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Server/Services/AlertService.ts +32 -36
- package/Server/Services/IncidentAlertService.ts +609 -2
- package/Server/Services/IncidentService.ts +134 -44
- package/Server/Services/InventoryItemService.ts +38 -0
- package/Server/Services/IoTDeviceCredentialService.ts +13 -4
- package/Server/Services/MetricService.ts +1 -1
- package/Server/Services/ScheduledMaintenanceService.ts +50 -46
- package/Server/Services/UserNotificationRuleService.ts +55 -53
- package/Server/Services/WorkspaceNotificationSummaryService.ts +38 -20
- package/Server/Types/Database/QueryHelper.ts +54 -1
- package/Server/Utils/AI/Toolbox/MetricTools.ts +13 -2
- package/Server/Utils/AI/Toolbox/ResourceTools.ts +12 -1
- package/Server/Utils/AffectedResources/LinkedAffectedResources.ts +576 -0
- package/Server/Utils/Alert/AlertStateChangeAuthorization.ts +129 -0
- package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +1 -1
- package/Server/Utils/Inventory/InventoryOverviewAggregation.ts +111 -0
- package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +33 -0
- package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +4 -3
- package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +3 -1
- package/Server/Utils/Response.ts +31 -0
- package/Server/Utils/Topology/TopologyConcurrencyLimiter.ts +229 -0
- package/Server/Utils/Topology/TopologyQueries.ts +1350 -0
- package/Server/Utils/Topology/TopologyRequest.ts +463 -0
- package/Server/Utils/Topology/TopologyResponseCache.ts +183 -0
- package/Server/Utils/Topology/TopologySql.ts +1024 -0
- package/Tests/App/Dashboard/AcknowledgeAlertsOnDeclare.test.ts +369 -0
- package/Tests/App/Dashboard/AffectedResourcesSlo.test.tsx +1 -1
- package/Tests/App/Dashboard/AffectedResourcesSummaryStep.test.tsx +510 -123
- package/Tests/App/Dashboard/AlertsTableAffectedResources.test.tsx +490 -0
- package/Tests/App/Dashboard/ChangeAlertStateDeclareIncident.test.tsx +886 -0
- package/Tests/App/Dashboard/CreateReviewStepRoleSummary.test.tsx +1250 -0
- package/Tests/App/Dashboard/CreateReviewStepSelectionSummary.test.tsx +738 -0
- package/Tests/App/Dashboard/DeclareIncidentFromAlert.test.ts +417 -0
- package/Tests/App/Dashboard/EntityDetailPanel.test.tsx +1948 -229
- package/Tests/App/Dashboard/EntityDetailPanelLifecycle.test.tsx +493 -0
- package/Tests/App/Dashboard/EntityDetailPanelNavigation.test.tsx +439 -0
- package/Tests/App/Dashboard/EventOverviewPages.test.tsx +292 -1
- package/Tests/App/Dashboard/EventStatusPanel.test.tsx +798 -2
- package/Tests/App/Dashboard/FetchStateAndSeverityElements.test.tsx +669 -0
- package/Tests/App/Dashboard/FetchUsers.test.tsx +230 -0
- package/Tests/App/Dashboard/IncidentCreateAcknowledgeAlerts.test.tsx +1611 -0
- package/Tests/App/Dashboard/InfrastructureExplorer.test.tsx +1365 -57
- package/Tests/App/Dashboard/InfrastructureGraph.test.tsx +964 -23
- package/Tests/App/Dashboard/InventoryRelationships.test.tsx +885 -0
- package/Tests/App/Dashboard/NetworkTopologyExplorerSearch.test.tsx +1267 -0
- package/Tests/App/Dashboard/ResourceOverviewTileTooltips.test.tsx +4 -1
- package/Tests/App/Dashboard/RumOverviewPage.test.tsx +127 -0
- package/Tests/App/Dashboard/ServiceMapDatabaseLink.test.tsx +201 -15
- package/Tests/App/Dashboard/ServiceMapGraph.test.tsx +508 -36
- package/Tests/App/Dashboard/SiteSearchBox.test.tsx +2110 -0
- package/Tests/App/Dashboard/TelemetryMetricsSignals.test.ts +168 -8
- package/Tests/App/Dashboard/TopologyDataLifecycle.test.tsx +583 -0
- package/Tests/App/Dashboard/TopologyDataLoading.test.tsx +982 -218
- package/Tests/App/Dashboard/TopologyPageNavigation.test.tsx +818 -114
- package/Tests/Server/Infrastructure/Postgres/RenameInventoryItemMigration.test.ts +90 -21
- package/Tests/Server/Services/DomainService.test.ts +525 -0
- package/Tests/Server/Services/IncidentAlertAcknowledgeOnDeclare.test.ts +2630 -0
- package/Tests/Server/Services/IncidentAlertPostgres.test.ts +1625 -1
- package/Tests/Server/Services/IncidentDeclareAcknowledgeAlerts.test.ts +2189 -0
- package/Tests/Server/Services/InventoryItemServiceOverviewCounts.test.ts +345 -0
- package/Tests/Server/Services/InventoryOverviewPostgres.test.ts +254 -0
- package/Tests/Server/Services/IoTDeviceCredentialService.test.ts +819 -0
- package/Tests/Server/Services/LinkedAffectedResourcesCreatedFeed.test.ts +420 -0
- package/Tests/Server/Services/MetricServiceAggregate.test.ts +12 -0
- package/Tests/Server/Services/SavedViewDefaults.test.ts +325 -0
- package/Tests/Server/Services/SloAffectedResourceCreatedFeed.test.ts +92 -36
- package/Tests/Server/Services/StatusPageCertificateService.test.ts +169 -0
- package/Tests/Server/Services/TeamComplianceSettingService.test.ts +191 -0
- package/Tests/Server/Types/Database/QueryHelperSearchAllWords.test.ts +651 -0
- package/Tests/Server/Utils/AI/AlertWriteTools.test.ts +256 -0
- package/Tests/Server/Utils/AI/IncidentWriteTools.test.ts +482 -0
- package/Tests/Server/Utils/AffectedResources/LinkedAffectedResources.test.ts +858 -0
- package/Tests/Server/Utils/Alert/AlertStateChangeAuthorization.test.ts +1172 -0
- package/Tests/Server/Utils/Monitor/Criteria/CompareCriteria.test.ts +54 -0
- package/Tests/Server/Utils/Monitor/MetricDecimalThreshold.test.ts +417 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorAffectedResourceEdgeCases.test.ts +96 -0
- package/Tests/Server/Utils/Topology/InventoryItemEntityKeyIndexMigration.test.ts +386 -0
- package/Tests/Server/Utils/Topology/InventoryItemEntityKeyIndexMigrationPostgres.test.ts +430 -0
- package/Tests/Server/Utils/Topology/TopologyActivityParityPostgres.test.ts +279 -0
- package/Tests/Server/Utils/Topology/TopologyConcurrencyLimiter.test.ts +534 -0
- package/Tests/Server/Utils/Topology/TopologyEstateGenerator.ts +624 -0
- package/Tests/Server/Utils/Topology/TopologyJsonResponse.test.ts +90 -0
- package/Tests/Server/Utils/Topology/TopologyQueries.test.ts +1634 -0
- package/Tests/Server/Utils/Topology/TopologyQueriesDifferentialPostgres.test.ts +683 -0
- package/Tests/Server/Utils/Topology/TopologyQueriesPostgres.test.ts +2624 -0
- package/Tests/Server/Utils/Topology/TopologyReference.ts +1018 -0
- package/Tests/Server/Utils/Topology/TopologyRequest.test.ts +643 -0
- package/Tests/Server/Utils/Topology/TopologyResponseCache.test.ts +305 -0
- package/Tests/Server/Utils/Topology/TopologySql.test.ts +973 -0
- package/Tests/Types/BaseDatabase/AggregationType.test.ts +5 -1
- package/Tests/Types/Dashboard/KubernetesDashboardTemplate.test.ts +184 -0
- package/Tests/Types/Dashboard/KubernetesDashboardTemplates.test.ts +107 -0
- package/Tests/Types/Monitor/KubernetesAlertTemplates.test.ts +8 -14
- package/Tests/Types/Monitor/KubernetesMetricCatalog.test.ts +4 -3
- package/Tests/Types/Monitor/Utils/KubernetesAgentEmittedMetrics.ts +27 -6
- package/Tests/Types/Topology/TopologyTypeRules.test.ts +176 -0
- package/Tests/UI/Rum/ChunkLoader.test.ts +76 -0
- package/Tests/UI/Rum/ReplayRailDetail.test.tsx +53 -0
- package/Tests/UI/Rum/ReplaySignals.test.ts +54 -0
- package/Tests/Utils/Dashboard/KubernetesClusterCpuTileRepair.test.ts +370 -0
- package/Tests/Utils/Incident/IncidentFromAlerts.test.ts +302 -0
- package/Tests/Utils/Rules/MonitorTypeRuleCriteria.test.ts +181 -0
- package/Types/BaseDatabase/AggregationType.ts +7 -0
- package/Types/Dashboard/DashboardTemplates.ts +20 -7
- package/Types/Incident/IncidentAlertLink.ts +19 -0
- package/Types/Rum/SessionReplayCustomEvents.ts +29 -3
- package/Types/Rum/WebVitals.ts +13 -0
- package/Types/Telemetry/InventoryLiveness.ts +26 -0
- package/Types/Topology/TopologyApi.ts +428 -0
- package/Types/Topology/TopologyTypeRules.ts +191 -0
- package/UI/Components/Input/Input.tsx +11 -0
- package/Utils/Dashboard/KubernetesClusterCpuTileRepair.ts +147 -0
- package/Utils/Incident/IncidentFromAlerts.ts +78 -0
- package/build/dist/Models/DatabaseModels/InventoryItem.js +20 -11
- package/build/dist/Models/DatabaseModels/InventoryItem.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795200000000-AddInventoryItemProjectEntityKeyIndex.js +165 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795200000000-AddInventoryItemProjectEntityKeyIndex.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/AlertService.js +21 -33
- package/build/dist/Server/Services/AlertService.js.map +1 -1
- package/build/dist/Server/Services/IncidentAlertService.js +457 -3
- package/build/dist/Server/Services/IncidentAlertService.js.map +1 -1
- package/build/dist/Server/Services/IncidentService.js +96 -42
- package/build/dist/Server/Services/IncidentService.js.map +1 -1
- package/build/dist/Server/Services/InventoryItemService.js +31 -0
- package/build/dist/Server/Services/InventoryItemService.js.map +1 -1
- package/build/dist/Server/Services/IoTDeviceCredentialService.js +12 -5
- package/build/dist/Server/Services/IoTDeviceCredentialService.js.map +1 -1
- package/build/dist/Server/Services/MetricService.js +1 -1
- package/build/dist/Server/Services/ScheduledMaintenanceService.js +37 -40
- package/build/dist/Server/Services/ScheduledMaintenanceService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationRuleService.js +55 -51
- package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
- package/build/dist/Server/Services/WorkspaceNotificationSummaryService.js +42 -25
- package/build/dist/Server/Services/WorkspaceNotificationSummaryService.js.map +1 -1
- package/build/dist/Server/Types/Database/QueryHelper.js +47 -1
- package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/MetricTools.js +13 -2
- package/build/dist/Server/Utils/AI/Toolbox/MetricTools.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/ResourceTools.js +12 -1
- package/build/dist/Server/Utils/AI/Toolbox/ResourceTools.js.map +1 -1
- package/build/dist/Server/Utils/AffectedResources/LinkedAffectedResources.js +405 -0
- package/build/dist/Server/Utils/AffectedResources/LinkedAffectedResources.js.map +1 -0
- package/build/dist/Server/Utils/Alert/AlertStateChangeAuthorization.js +112 -0
- package/build/dist/Server/Utils/Alert/AlertStateChangeAuthorization.js.map +1 -0
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +1 -1
- package/build/dist/Server/Utils/Inventory/InventoryOverviewAggregation.js +81 -0
- package/build/dist/Server/Utils/Inventory/InventoryOverviewAggregation.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +31 -0
- package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
- package/build/dist/Server/Utils/Response.js +30 -0
- package/build/dist/Server/Utils/Response.js.map +1 -1
- package/build/dist/Server/Utils/Topology/TopologyConcurrencyLimiter.js +178 -0
- package/build/dist/Server/Utils/Topology/TopologyConcurrencyLimiter.js.map +1 -0
- package/build/dist/Server/Utils/Topology/TopologyQueries.js +884 -0
- package/build/dist/Server/Utils/Topology/TopologyQueries.js.map +1 -0
- package/build/dist/Server/Utils/Topology/TopologyRequest.js +275 -0
- package/build/dist/Server/Utils/Topology/TopologyRequest.js.map +1 -0
- package/build/dist/Server/Utils/Topology/TopologyResponseCache.js +146 -0
- package/build/dist/Server/Utils/Topology/TopologyResponseCache.js.map +1 -0
- package/build/dist/Server/Utils/Topology/TopologySql.js +688 -0
- package/build/dist/Server/Utils/Topology/TopologySql.js.map +1 -0
- package/build/dist/Types/BaseDatabase/AggregationType.js +7 -0
- package/build/dist/Types/BaseDatabase/AggregationType.js.map +1 -1
- package/build/dist/Types/Dashboard/DashboardTemplates.js +20 -7
- package/build/dist/Types/Dashboard/DashboardTemplates.js.map +1 -1
- package/build/dist/Types/Incident/IncidentAlertLink.js +17 -0
- package/build/dist/Types/Incident/IncidentAlertLink.js.map +1 -1
- package/build/dist/Types/Rum/SessionReplayCustomEvents.js.map +1 -1
- package/build/dist/Types/Rum/WebVitals.js +12 -0
- package/build/dist/Types/Rum/WebVitals.js.map +1 -1
- package/build/dist/Types/Telemetry/InventoryLiveness.js +23 -0
- package/build/dist/Types/Telemetry/InventoryLiveness.js.map +1 -1
- package/build/dist/Types/Topology/TopologyApi.js +55 -0
- package/build/dist/Types/Topology/TopologyApi.js.map +1 -0
- package/build/dist/Types/Topology/TopologyTypeRules.js +165 -0
- package/build/dist/Types/Topology/TopologyTypeRules.js.map +1 -0
- package/build/dist/UI/Components/Input/Input.js +1 -1
- package/build/dist/UI/Components/Input/Input.js.map +1 -1
- package/build/dist/Utils/Dashboard/KubernetesClusterCpuTileRepair.js +110 -0
- package/build/dist/Utils/Dashboard/KubernetesClusterCpuTileRepair.js.map +1 -0
- package/build/dist/Utils/Incident/IncidentFromAlerts.js +38 -0
- package/build/dist/Utils/Incident/IncidentFromAlerts.js.map +1 -1
- 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
|
|
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:
|
|
1165
|
-
*
|
|
1166
|
-
* covers node, pod and container metrics
|
|
1167
|
-
* ones that are off upstream and on only because the chart
|
|
1168
|
-
* (see the high node CPU block below)
|
|
1169
|
-
* chart's Prometheus scrapes
|
|
1170
|
-
*
|
|
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
|
|
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'
|
|
530
|
-
* collector version. Bumping the image fails
|
|
531
|
-
* re-read — receivers rename metrics between
|
|
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),
|