@oneuptime/common 14.0.5 → 14.0.6
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/Server/Services/MetricService.ts +1 -1
- 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/AnalyticsDatabase/StatementGenerator.ts +1 -1
- 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/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/SiteSearchBox.test.tsx +2110 -0
- package/Tests/App/Dashboard/TelemetryMetricsSignals.test.ts +168 -8
- package/Tests/Server/Services/MetricServiceAggregate.test.ts +12 -0
- package/Tests/Server/Services/SavedViewDefaults.test.ts +325 -0
- 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/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/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/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/Rules/MonitorTypeRuleCriteria.test.ts +181 -0
- package/Types/BaseDatabase/AggregationType.ts +7 -0
- package/Types/Dashboard/DashboardTemplates.ts +20 -7
- package/Types/Rum/SessionReplayCustomEvents.ts +29 -3
- package/Types/Rum/WebVitals.ts +13 -0
- package/UI/Components/Input/Input.tsx +11 -0
- package/Utils/Dashboard/KubernetesClusterCpuTileRepair.ts +147 -0
- package/build/dist/Server/Services/MetricService.js +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/AnalyticsDatabase/StatementGenerator.js +1 -1
- 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/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/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/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/package.json +1 -1
|
@@ -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
|
+
]);
|
|
@@ -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),
|
|
@@ -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", () => {
|