UNPKG

@netdata/charts

Version:

Netdata frontend SDK and chart utilities

613 lines (612 loc) 20.3 kB
"use strict"; var _testUtilities = require("@jest/testUtilities"); var _dataTransformer = require("./dataTransformer"); function _slicedToArray(r, e) { return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest(); } function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } } function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; } function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t["return"] && (u = t["return"](), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } } function _arrayWithHoles(r) { if (Array.isArray(r)) return r; } var firstNodeId = "9d445810-58a2-4d8c-9749-d80785f49d93"; var _makeTestChart = (0, _testUtilities.makeTestChart)({ attributes: { nodes: { "lab-cloud-unpaid": { nm: "Production node" } } } }), chart = _makeTestChart.chart; var weightsResponse = { result: [{ id: "load1,system.load,".concat(firstNodeId), nm: "load1,system.load,lab-cloud-unpaid", v: [[0.3837659, 0.3837659, 0.3837659, 0.3837659, 1], [0.05, 0.3837659, 2, 69.0778583, 180, 0]] }, { id: "load5,system.load,".concat(firstNodeId), nm: "load5,system.load,lab-cloud-unpaid", v: [[0.3523553, 0.3523553, 0.3523553, 0.3523553, 1], [0.14, 0.3523553, 0.64, 63.4239464, 180, 0]] }, { id: "load1,system.load,b6d0e457-0281-4ef9-8c20-86bcbeffd1a8", nm: "load1,system.load,nd-child-unpaid04", v: [[0.3889211, 0.3889211, 0.3889211, 0.3889211, 1], [0.05, 0.3889211, 1.84, 70.0058063, 180, 0]] }] }; var makeTreeItem = function makeTreeItem(_ref, groupByOrder) { var values = _ref.values, _ref$names = _ref.names, names = _ref$names === void 0 ? values : _ref$names, _ref$nm = _ref.nm, nm = _ref$nm === void 0 ? names.join(",") : _ref$nm, _ref$weight = _ref.weight, weight = _ref$weight === void 0 ? { min: 1, avg: 1, max: 1, sum: 1, count: 1 } : _ref$weight, _ref$timeframe = _ref.timeframe, timeframe = _ref$timeframe === void 0 ? { min: 1, avg: 1, max: 1, sum: 1, count: 1, anomaly_count: 0 } : _ref$timeframe; var groupedBy = groupByOrder.reduce(function (result, field, index) { result[field] = values[index]; return result; }, {}); var groupedByNames = groupByOrder.reduce(function (result, field, index) { result[field] = names[index]; return result; }, {}); return { id: values.join(","), nm: nm, label: names[0], groupedBy: groupedBy, groupedByNames: groupedByNames, weight: weight, timeframe: timeframe, anomalyRate: timeframe.count > 0 ? timeframe.anomaly_count * 100 / timeframe.count : 0, contribution: weight.sum, level: 0, parentId: null }; }; var _countTreeNodes = function countTreeNodes(nodes) { return nodes.reduce(function (count, node) { return count + 1 + _countTreeNodes(node.children || []); }, 0); }; describe("dataTransformer", function () { describe("transformWeightsData", function () { it("returns empty array when no data", function () { expect((0, _dataTransformer.transformWeightsData)(null, ["dimension"], chart)).toEqual([]); expect((0, _dataTransformer.transformWeightsData)({ result: [] }, ["dimension"], chart)).toEqual([]); expect((0, _dataTransformer.transformWeightsData)(weightsResponse, [], chart)).toEqual([]); }); it("transforms weights response to flat data", function () { var groupBy = ["dimension", "context", "node"]; var result = (0, _dataTransformer.transformWeightsData)(weightsResponse, groupBy, chart); expect(result).toHaveLength(3); var firstItem = result[0]; expect(firstItem.id).toBe("load1,system.load,".concat(firstNodeId)); expect(firstItem.nm).toBe("load1,system.load,lab-cloud-unpaid"); expect(firstItem.label).toBe("load1"); expect(firstItem.groupedBy).toEqual({ dimension: "load1", context: "system.load", node: firstNodeId }); expect(firstItem.groupedByNames).toEqual({ dimension: "load1", context: "system.load", node: "Production node" }); }); it("maps weight statistics correctly", function () { var groupBy = ["dimension", "context", "node"]; var result = (0, _dataTransformer.transformWeightsData)(weightsResponse, groupBy, chart); expect(result[0].weight).toEqual({ min: 0.3837659, avg: 0.3837659, max: 0.3837659, sum: 0.3837659, count: 1 }); }); it("maps timeframe statistics correctly", function () { var groupBy = ["dimension", "context", "node"]; var result = (0, _dataTransformer.transformWeightsData)(weightsResponse, groupBy, chart); expect(result[0].timeframe).toEqual({ min: 0.05, avg: 0.3837659, max: 2, sum: 69.0778583, count: 180, anomaly_count: 0 }); }); it("uses schema indexes and sanitizes non-finite timeframe statistics", function () { var response = { v_schema: { items: [{ name: "timeframe" }, { name: "weight" }] }, result: [{ id: "dimension", nm: "Dimension", v: [[Infinity, -Infinity, NaN, undefined, null, Infinity], [1, 2, 3, 4, 5]] }] }; var _transformWeightsData = (0, _dataTransformer.transformWeightsData)(response, ["dimension"], chart), _transformWeightsData2 = _slicedToArray(_transformWeightsData, 1), result = _transformWeightsData2[0]; expect(result.weight).toEqual({ min: 1, avg: 2, max: 3, sum: 4, count: 5 }); expect(result.timeframe).toEqual({ min: null, avg: null, max: null, sum: null, count: null, anomaly_count: null }); expect(result.anomalyRate).toBe(0); }); it("calculates anomaly rate correctly", function () { var groupBy = ["dimension", "context", "node"]; var response = { result: [{ id: "test,test,test", nm: "test", v: [[1, 1, 1, 1, 1], [1, 1, 1, 1, 100, 10]] }] }; expect((0, _dataTransformer.transformWeightsData)(weightsResponse, groupBy, chart)[0].anomalyRate).toBe(0); expect((0, _dataTransformer.transformWeightsData)(response, groupBy, chart)[0].anomalyRate).toBe(10); }); it("handles different groupBy orders", function () { var groupBy = ["node", "dimension", "context"]; var result = (0, _dataTransformer.transformWeightsData)(weightsResponse, groupBy, chart); expect(result[0].label).toBe("load1"); expect(result[0].groupedBy).toEqual({ node: "load1", dimension: "system.load", context: firstNodeId }); }); }); describe("buildHierarchicalTree", function () { it("returns the original flat data when no groupBy order is provided", function () { var flatData = [{ id: "test", level: 0 }]; expect((0, _dataTransformer.buildHierarchicalTree)(flatData, [])).toBe(flatData); }); it("builds the hierarchy with stable parent-child order", function () { var groupBy = ["dimension", "context", "node"]; var flatData = (0, _dataTransformer.transformWeightsData)(weightsResponse, groupBy, chart); var tree = (0, _dataTransformer.buildHierarchicalTree)(flatData, groupBy); expect(tree.map(function (node) { return node.label; })).toEqual(["load1", "load5"]); var _tree = _slicedToArray(tree, 2), load1Group = _tree[0], load5Group = _tree[1]; expect(load1Group).toMatchObject({ level: 0, parentId: null, isGroupNode: true }); expect(load5Group).toMatchObject({ level: 0, parentId: null, isGroupNode: true }); expect(load1Group.children).toHaveLength(1); var _load1Group$children = _slicedToArray(load1Group.children, 1), contextGroup = _load1Group$children[0]; expect(contextGroup).toMatchObject({ label: "system.load", level: 1, parentId: "load1", isGroupNode: true }); expect(contextGroup.children.map(function (node) { return node.label; })).toEqual(["Production node", "nd-child-unpaid04"]); expect(contextGroup.children.every(function (node) { return !node.isGroupNode; })).toBe(true); }); it("preserves exact aggregate semantics for nested groups and duplicate leaves", function () { var groupBy = ["region", "node", "dimension"]; var flatData = [makeTreeItem({ values: ["west", "node-a", "cpu"], nm: "cpu", weight: { min: 1, avg: 5, max: 5, sum: 10, count: 2 }, timeframe: { min: 2, avg: 4, max: 6, sum: 8, count: 2, anomaly_count: 1 } }, groupBy), makeTreeItem({ values: ["west", "node-a", "memory"], nm: "memory-first", weight: { min: null, avg: 20, max: 8, sum: 20, count: 0 }, timeframe: { min: null, avg: 10, max: 12, sum: 20, count: 3, anomaly_count: 2 } }, groupBy), makeTreeItem({ values: ["west", "node-a", "memory"], nm: "memory-last", weight: { min: 0, avg: 5, max: null, sum: 5, count: 1 }, timeframe: { min: 1, avg: 2, max: null, sum: 2, count: 1, anomaly_count: 0 } }, groupBy), makeTreeItem({ values: ["west", "node-b", "disk"], nm: "disk", weight: { min: -1, avg: 0, max: 9, sum: 0, count: 4 }, timeframe: { min: 0, avg: 0, max: 0, sum: 0, count: 0, anomaly_count: 3 } }, groupBy), makeTreeItem({ values: ["east", "node-c", "network"] }, groupBy)]; var tree = (0, _dataTransformer.buildHierarchicalTree)(flatData, groupBy); var _tree2 = _slicedToArray(tree, 1), west = _tree2[0]; var _west$children = _slicedToArray(west.children, 2), nodeA = _west$children[0], nodeB = _west$children[1]; expect(west.weight).toEqual({ min: -1, max: 9, sum: 35, avg: 35 / 3, count: 7, anomaly_count: undefined }); expect(west.timeframe).toEqual({ min: 0, max: 12, sum: 30, avg: 40 / 6, count: 6, anomaly_count: 6 }); expect(west).toMatchObject({ contribution: 35, anomalyRate: 100 }); expect(nodeA.weight).toEqual({ min: 0, max: 8, sum: 35, avg: 17.5, count: 3, anomaly_count: undefined }); expect(nodeA.timeframe).toEqual({ min: 1, max: 12, sum: 30, avg: 40 / 6, count: 6, anomaly_count: 3 }); expect(nodeA).toMatchObject({ contribution: 35, anomalyRate: 50 }); expect(nodeA.children.map(function (node) { return node.nm; })).toEqual(["cpu", "memory-last"]); expect(nodeA.children[1].weight).toEqual(flatData[2].weight); expect(nodeB.timeframe).toEqual({ min: 0, max: 0, sum: 0, avg: 0, count: 0, anomaly_count: 3 }); expect(nodeB.anomalyRate).toBe(0); }); it("keeps aggregate membership distinct when public path keys collide", function () { var groupBy = ["first", "second", "leaf"]; var firstItem = makeTreeItem({ values: ["a|b", "c", "leaf-a"], nm: "leaf-a", weight: { min: 1, avg: 2, max: 3, sum: 2, count: 1 }, timeframe: { min: 1, avg: 2, max: 3, sum: 2, count: 1, anomaly_count: 1 } }, groupBy); var secondItem = makeTreeItem({ values: ["a", "b|c", "leaf-b"], nm: "leaf-b", weight: { min: 100, avg: 100, max: 100, sum: 100, count: 1 }, timeframe: { min: 100, avg: 100, max: 100, sum: 100, count: 1, anomaly_count: 0 } }, groupBy); var tree = (0, _dataTransformer.buildHierarchicalTree)([firstItem, secondItem], groupBy); var _tree3 = _slicedToArray(tree, 2), firstRoot = _tree3[0], secondRoot = _tree3[1]; var _firstRoot$children = _slicedToArray(firstRoot.children, 1), collidingGroup = _firstRoot$children[0]; expect(tree.map(function (node) { return node.id; })).toEqual(["a|b", "a"]); expect(collidingGroup.id).toBe("a|b|c"); expect(collidingGroup.children.map(function (node) { return node.nm; })).toEqual(["leaf-a", "leaf-b"]); expect(collidingGroup.weight).toEqual({ min: 1, max: 3, sum: 2, avg: 2, count: 1, anomaly_count: undefined }); expect(collidingGroup.timeframe).toEqual({ min: 1, max: 3, sum: 2, avg: 2, count: 1, anomaly_count: 1 }); expect(secondRoot.weight.sum).toBe(100); expect(secondRoot.children).toEqual([]); }); it("preserves strict-equality membership behavior for NaN grouping values", function () { var groupBy = ["group", "leaf"]; var flatData = [makeTreeItem({ values: [NaN, "dimension"] }, groupBy)]; var _buildHierarchicalTre = (0, _dataTransformer.buildHierarchicalTree)(flatData, groupBy), _buildHierarchicalTre2 = _slicedToArray(_buildHierarchicalTre, 1), group = _buildHierarchicalTre2[0]; expect(group.id).toBe("NaN"); expect(group.children).toHaveLength(1); expect(group).not.toHaveProperty("weight"); expect(group).not.toHaveProperty("timeframe"); }); it("handles grouping values that match object prototype property names", function () { var groupBy = ["group", "leaf"]; var flatData = [makeTreeItem({ values: ["__proto__", "constructor"], nm: "prototype leaf" }, groupBy)]; var _buildHierarchicalTre3 = (0, _dataTransformer.buildHierarchicalTree)(flatData, groupBy), _buildHierarchicalTre4 = _slicedToArray(_buildHierarchicalTre3, 1), group = _buildHierarchicalTre4[0]; expect(group).toMatchObject({ id: "__proto__", label: "__proto__", isGroupNode: true }); expect(group.children).toHaveLength(1); expect(group.children[0]).toMatchObject({ id: "__proto__|constructor", nm: "prototype leaf", isGroupNode: false }); }); it("preserves object property ordering for integer-like root keys", function () { var groupBy = ["group", "leaf"]; var flatData = ["10", "2", "alpha"].map(function (value) { return makeTreeItem({ values: [value, "leaf-".concat(value)] }, groupBy); }); var tree = (0, _dataTransformer.buildHierarchicalTree)(flatData, groupBy); expect(tree.map(function (node) { return node.id; })).toEqual(["2", "10", "alpha"]); }); it("handles single-level grouping", function () { var groupBy = ["dimension"]; var flatData = (0, _dataTransformer.transformWeightsData)(weightsResponse, groupBy, chart); var tree = (0, _dataTransformer.buildHierarchicalTree)(flatData, groupBy); expect(tree.map(function (node) { return node.label; })).toEqual(["load1", "load5"]); expect(tree.every(function (node) { return !node.isGroupNode; })).toBe(true); }); it("builds and exactly aggregates a 6,000-row high-cardinality hierarchy", function () { var groupBy = ["region", "cluster", "node", "dimension"]; var flatData = Array.from({ length: 6000 }, function (_, index) { var region = Math.floor(index / 600); var cluster = Math.floor(index % 600 / 30); var node = index % 30; var value = index + 1; return makeTreeItem({ values: ["region-".concat(region), "cluster-".concat(cluster), "node-".concat(node), "dimension-".concat(index)], weight: { min: value - 0.5, avg: value, max: value + 0.5, sum: value, count: index % 4 === 0 ? 0 : 2 }, timeframe: { min: value - 1, avg: value, max: value + 1, sum: value * 2, count: 2, anomaly_count: index % 3 === 0 ? 1 : 0 } }, groupBy); }); var tree = (0, _dataTransformer.buildHierarchicalTree)(flatData, groupBy); var region = tree[0]; var cluster = region.children[0]; var node = cluster.children[1]; expect(tree.map(function (item) { return item.label; })).toEqual(Array.from({ length: 10 }, function (_, index) { return "region-".concat(index); })); expect(_countTreeNodes(tree)).toBe(12210); expect(region.children.map(function (item) { return item.label; })).toEqual(Array.from({ length: 20 }, function (_, index) { return "cluster-".concat(index); })); expect(cluster.children.map(function (item) { return item.label; })).toEqual(Array.from({ length: 30 }, function (_, index) { return "node-".concat(index); })); expect(region.weight).toEqual({ min: 0.5, max: 600.5, sum: 180300, avg: 180300 / 450, count: 900, anomaly_count: undefined }); expect(region.timeframe).toEqual({ min: 0, max: 601, sum: 360600, avg: 300.5, count: 1200, anomaly_count: 200 }); expect(region.anomalyRate).toBe(200 * 100 / 1200); expect(cluster.weight).toEqual({ min: 0.5, max: 30.5, sum: 465, avg: 465 / 22, count: 44, anomaly_count: undefined }); expect(cluster.timeframe).toEqual({ min: 0, max: 31, sum: 930, avg: 15.5, count: 60, anomaly_count: 10 }); expect(node.weight).toEqual({ min: 1.5, max: 2.5, sum: 2, avg: 2, count: 2, anomaly_count: undefined }); expect(node.timeframe).toEqual({ min: 1, max: 3, sum: 4, avg: 2, count: 2, anomaly_count: 0 }); expect(node.children).toHaveLength(1); expect(node.children[0].label).toBe("dimension-1"); }); }); });