You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@pinot.apache.org by ji...@apache.org on 2020/12/03 22:40:15 UTC
[incubator-pinot] branch master updated: [TE]frontend - Build the
tree parser for composite anomalies (#6290)
This is an automated email from the ASF dual-hosted git repository.
jihao pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-pinot.git
The following commit(s) were added to refs/heads/master by this push:
new 3cf3154 [TE]frontend - Build the tree parser for composite anomalies (#6290)
3cf3154 is described below
commit 3cf315484c15c2230ea76d5ee0d6fe936003c3b0
Author: Tejas Ajmera <33...@users.noreply.github.com>
AuthorDate: Thu Dec 3 14:39:58 2020 -0800
[TE]frontend - Build the tree parser for composite anomalies (#6290)
The parser contains the logic to extract data in order to build child components at any level of the tree when performing the drill-down analysis for Entity Monitoring. The output at each level of drill-down contains data formatted in a way that can be directly consumed by the child components referenced in the output without any additional transformation.
---
.../app/mocks/compositeAnomalies.js | 256 ++++++++++
.../app/utils/anomalies-tree-parser.js | 541 +++++++++++++++++++++
thirdeye/thirdeye-frontend/app/utils/constants.js | 5 +-
.../tests/unit/utils/anomalies-tree-parser-test.js | 255 ++++++++++
4 files changed, 1056 insertions(+), 1 deletion(-)
diff --git a/thirdeye/thirdeye-frontend/app/mocks/compositeAnomalies.js b/thirdeye/thirdeye-frontend/app/mocks/compositeAnomalies.js
new file mode 100644
index 0000000..e81c0cf
--- /dev/null
+++ b/thirdeye/thirdeye-frontend/app/mocks/compositeAnomalies.js
@@ -0,0 +1,256 @@
+export const mockData = [
+ {
+ id: 1,
+ startTime: 1599462000000,
+ endTime: 1599721200000,
+ feedback: null,
+ properties: {
+ detectorComponentName: "grouper_one:GROUPER",
+ subEntityName: "entity_one"
+ },
+ children: [
+ {
+ id: 2,
+ startTime: 1599462000000,
+ endTime: 1599721200000,
+ feedback: null,
+ avgCurrentVal: 32,
+ avgBaselineVal: 33,
+ properties: {
+ groupScore: "6.189942819613212",
+ detectorComponentName: "detection_grouper:ANOMALY_SUMMARIZE",
+ subEntityName: "group_entity_one",
+ groupKey: "groupConstituentOne"
+ },
+ children: [
+ {
+ id: 3,
+ metric: "metric_four",
+ dimensions: {
+ feature_name: "groupConstituentOne#",
+ feature_section: "groupConstituentOne",
+ dimension_three: "True",
+ use_case: "DESKTOP"
+ },
+ startTime: 1599462000000,
+ endTime: 1599548400000,
+ avgCurrentVal: 32,
+ avgBaselineVal: 33,
+ feedback: null,
+ children: []
+ },
+ {
+ id: 4,
+ metric: "metric_four",
+ dimensions: {
+ feature_name: "groupConstituentOne#",
+ feature_section: "groupConstituentOne",
+ dimension_three: "True",
+ use_case: "DESKTOP"
+ },
+ startTime: 1599462000000,
+ endTime: 1599548400000,
+ avgCurrentVal: 32,
+ avgBaselineVal: 33,
+ feedback: null,
+ children: []
+ }
+ ]
+ },
+ {
+ id: 5,
+ startTime: 1599462000000,
+ endTime: 1599721200000,
+ feedback: null,
+ avgCurrentVal: 32,
+ avgBaselineVal: 33,
+ properties: {
+ groupScore: "6.189942819613212",
+ detectorComponentName: "detection_grouper:ANOMALY_SUMMARIZE",
+ subEntityName: "group_entity_one",
+ groupKey: "groupConstituentTwo"
+ },
+ children: [
+ {
+ id: 6,
+ metric: "metric_four",
+ dimensions: {
+ feature_name: "groupConstituentTwo#",
+ feature_section: "groupConstituentTwo",
+ dimension_three: "True",
+ use_case: "DESKTOP"
+ },
+ startTime: 1599462000000,
+ endTime: 1599548400000,
+ avgCurrentVal: 32,
+ avgBaselineVal: 33,
+ feedback: null,
+ children: []
+ },
+ {
+ id: 7,
+ metric: "metric_four",
+ dimensions: {
+ feature_name: "groupConstituentTwo#",
+ feature_section: "groupConstituentTwo",
+ dimension_three: "True",
+ use_case: "DESKTOP"
+ },
+ startTime: 1599462000000,
+ endTime: 1599548400000,
+ avgCurrentVal: 32,
+ avgBaselineVal: 33,
+ feedback: null,
+ children: []
+ }
+ ]
+ },
+ {
+ id: 8,
+ startTime: 1599462000000,
+ endTime: 1599721200000,
+ avgCurrentVal: 32,
+ avgBaselineVal: 33,
+ feedback: null,
+ properties: {
+ groupScore: "6.189942819613212",
+ detectorComponentName: "detection_grouper:ANOMALY_SUMMARIZE",
+ subEntityName: "group_entity_two",
+ groupKey: "groupConstituentOne"
+ },
+ children: [
+ {
+ id: 9,
+ metric: "metric_four",
+ dimensions: {
+ feature_name: "groupConstituentOne#",
+ feature_section: "groupConstituentOne",
+ dimension_three: "True",
+ use_case: "DESKTOP"
+ },
+ startTime: 1599462000000,
+ endTime: 1599548400000,
+ avgCurrentVal: 32,
+ avgBaselineVal: 33,
+ feedback: null,
+ children: []
+ },
+ {
+ id: 10,
+ metric: "metric_four",
+ dimensions: {
+ feature_name: "groupConstituentOne#",
+ feature_section: "groupConstituentOne",
+ dimension_three: "True",
+ use_case: "DESKTOP"
+ },
+ startTime: 1599462000000,
+ endTime: 1599548400000,
+ avgCurrentVal: 32,
+ avgBaselineVal: 33,
+ feedback: null,
+ children: []
+ }
+ ]
+ },
+ {
+ id: 11,
+ startTime: 1599462000000,
+ endTime: 1599721200000,
+ avgCurrentVal: 32,
+ avgBaselineVal: 33,
+ feedback: null,
+ properties: {
+ groupScore: "6.189942819613212",
+ detectorComponentName: "detection_grouper:ANOMALY_SUMMARIZE",
+ subEntityName: "group_entity_two",
+ groupKey: "groupConstituentTwo"
+ },
+ children: [
+ {
+ id: 12,
+ metric: "metric_four",
+ dimensions: {
+ feature_name: "groupConstituentTwo#",
+ feature_section: "groupConstituentTwo",
+ dimension_three: "True",
+ use_case: "DESKTOP"
+ },
+ startTime: 1599462000000,
+ endTime: 1599548400000,
+ avgCurrentVal: 32,
+ avgBaselineVal: 33,
+ feedback: null,
+ children: []
+ },
+ {
+ id: 13,
+ metric: "metric_four",
+ dimensions: {
+ feature_name: "groupConstituentTwo#",
+ feature_section: "groupConstituentTwo",
+ dimension_three: "True",
+ use_case: "DESKTOP"
+ },
+ startTime: 1599462000000,
+ endTime: 1599548400000,
+ avgCurrentVal: 32,
+ avgBaselineVal: 33,
+ feedback: null,
+ children: []
+ }
+ ]
+ },
+ {
+ id: 15,
+ startTime: 1599462000000,
+ endTime: 1599462000000,
+ avgCurrentVal: 32,
+ avgBaselineVal: 33,
+ feedback: null,
+ children: [],
+ metric: "metric_one"
+ },
+ {
+ id: 16,
+ startTime: 1599462000000,
+ endTime: 1599462000000,
+ avgCurrentVal: 32,
+ avgBaselineVal: 33,
+ feedback: null,
+ children: [],
+ metric: "metric_one"
+ },
+ {
+ id: 17,
+ startTime: 1599462000000,
+ endTime: 1599462000000,
+ avgCurrentVal: 32,
+ avgBaselineVal: 33,
+ feedback: null,
+ children: [],
+ metric: "metric_two"
+ },
+ {
+ id: 18,
+ startTime: 1599462000000,
+ endTime: 1599462000000,
+ feedback: null,
+ metric: null,
+ properties: {},
+ children: [
+ {
+ id: 19,
+ startTime: 1599462000000,
+ endTime: 1599462000000,
+ avgCurrentVal: 32,
+ avgBaselineVal: 33,
+ feedback: null,
+ children: [],
+ metric: "metric_three"
+ }
+ ]
+ }
+ ]
+ }
+];
diff --git a/thirdeye/thirdeye-frontend/app/utils/anomalies-tree-parser.js b/thirdeye/thirdeye-frontend/app/utils/anomalies-tree-parser.js
new file mode 100644
index 0000000..af02b31
--- /dev/null
+++ b/thirdeye/thirdeye-frontend/app/utils/anomalies-tree-parser.js
@@ -0,0 +1,541 @@
+import { isEmpty } from '@ember/utils';
+import { set } from '@ember/object';
+import moment from 'moment';
+import { BREADCRUMB_TIME_DISPLAY_FORMAT } from 'thirdeye-frontend/utils/constants';
+
+const CLASSIFICATIONS = {
+ METRICS: {
+ KEY: 'metrics',
+ COMPONENT_PATH: 'entity-metrics',
+ DEFAULT_TITLE: 'Metric Anomalies'
+ },
+ GROUPS: {
+ KEY: 'groups',
+ COMPONENT_PATH: 'entity-groups',
+ DEFAULT_TITLE: 'ENTITY:'
+ },
+ ENTITIES: {
+ KEY: 'entities',
+ COMPONENT_PATH: 'parent-anomalies',
+ DEFAULT_TITLE: 'Entity'
+ }
+};
+
+/**
+ * Format the timestamp into the form to be shown in the breadcrumb
+ *
+ * @param {Number} timestamp
+ * The timestamp of anomaly creation time in milliseconds
+ *
+ * @returns {String}
+ * Formatted timestamp. Example of the required format - "Sep 15 16:49 EST"
+ */
+const getFormattedBreadcrumbTime = (timestamp) => {
+ const zoneName = moment.tz.guess();
+ const timeZoneAbbreviation = moment.tz(zoneName).zoneAbbr();
+
+ return `${moment(timestamp).format(BREADCRUMB_TIME_DISPLAY_FORMAT)} ${timeZoneAbbreviation}`;
+};
+
+/**
+ * Parse the anomalies generated by the composite alert to populate parent-anomalies table with relevent details about
+ * children for each anomaly.
+ *
+ * @param {Array<Object>} input
+ * The anomalies for composite alert.
+ *
+ * @returns {Array<Object>}
+ * Parsed out contents to populate parent-anomalies table
+ */
+const populateParentAnomaliesTable = (input) => {
+ const output = [];
+
+ for (const entry of input) {
+ const { id, startTime, endTime, feedback, children } = entry;
+ const entryOutput = {
+ id,
+ startTime,
+ endTime,
+ feedback
+ };
+
+ const details = {};
+ if (children.length > 0) {
+ for (const child of children) {
+ const { metric, properties: { subEntityName } = {} } = child;
+ const item = !isEmpty(metric) ? metric : subEntityName;
+
+ if (item in details) {
+ details[item]++;
+ } else {
+ details[item] = 1;
+ }
+ }
+ entryOutput.details = details;
+ output.push(entryOutput);
+ }
+ }
+
+ return output;
+};
+
+/**
+ * Parse the generated bucket for metric anomalies into the schema for the entity-metrics component
+ *
+ * @param {Object} input
+ * The metric anomalies bucket constituents
+ *
+ * @returns {Array<Object>}
+ * The content to be passed into the the leaf level entity-metrics component. Each item in the array represents
+ * contents for the row in the table.
+ */
+const parseMetricsBucket = (input) => {
+ return [input];
+};
+
+/**
+ * Parse the generated bucket for parent anomalies into the schema for the entity-groups component
+ *
+ * @param {Object} input
+ * The group anomalies bucket constituents
+ *
+ * @returns {Array<Object>}
+ * The content to be passed into the the entity-groups component. Each item in the array represents
+ * contents for the row in the table.
+ */
+const parseGroupsBucket = (input) => {
+ const output = [];
+
+ for (const group in input) {
+ output.push(input[group]);
+ }
+
+ return output;
+};
+
+/**
+ * Parse the generated bucket for parent anomalies into the schema for the parent-anomalies component
+ *
+ * @param {Object} input
+ * The parent anomalies bucket constituents
+ *
+ * @returns {Array<Object>}
+ * The content to be passed into the parent-anomalies component. Each item in the array represents
+ * contents for the row in the table.
+ */
+const parseEntitiesBucket = (input) => {
+ const output = [];
+
+ for (const entity in input) {
+ const { componentPath, title, data } = input[entity];
+
+ output.push({
+ componentPath,
+ title,
+ data: populateParentAnomaliesTable(data)
+ });
+ }
+
+ return output;
+};
+
+/**
+ * Add the anomaly referencing a metric to the metric bucket
+ *
+ * @param {Object} buckets
+ * The reference to buckets object within which the anomaly needs to be classified
+ * @param {Object} anomaly
+ * The metric anomaly that needs be classified added to the metric bucket
+ * @param {String} metric
+ * The metric for which this anomaly was generated
+ */
+const setMetricsBucket = (buckets, anomaly, metric) => {
+ const {
+ METRICS: { KEY: metricKey, DEFAULT_TITLE, COMPONENT_PATH }
+ } = CLASSIFICATIONS;
+ const { [metricKey]: { data } = {} } = buckets;
+ const { id, startTime, endTime, feedback, avgCurrentVal: current, avgBaselineVal: predicted } = anomaly;
+
+ const metricTableRow = {
+ id,
+ startTime,
+ endTime,
+ metric,
+ feedback,
+ current,
+ predicted
+ };
+
+ if (isEmpty(data)) {
+ const metricBucketObj = {
+ componentPath: COMPONENT_PATH,
+ title: DEFAULT_TITLE,
+ data: [metricTableRow]
+ };
+
+ set(buckets, `${metricKey}`, metricBucketObj);
+ } else {
+ data.push(metricTableRow);
+ }
+};
+
+/**
+ * Add the anomaly referencing a group constitient to the right group characterized by the subEntityName
+ *
+ * @param {Object} buckets
+ * The reference to buckets object within which the anomaly needs to be classified
+ * @param {Object} anomaly
+ * The anomaly produced due to the anomaly summarize grouper that needs be classified into the appropriate bucket
+ * @param {String} subEntityName
+ * The entity name under which certain set of metrics would be grouped
+ * @param {String} groupName
+ * The group constituent name. Each group constituent hosts anomalies from one metric.
+ */
+const setGroupsBucket = (buckets, anomaly, subEntityName, groupName) => {
+ const {
+ GROUPS: { KEY: groupKey, COMPONENT_PATH, DEFAULT_TITLE }
+ } = CLASSIFICATIONS;
+ const {
+ id,
+ startTime,
+ endTime,
+ feedback,
+ avgCurrentVal: current,
+ avgBaselineVal: predicted,
+ properties: { groupScore: criticality }
+ } = anomaly;
+ const groupTableRow = {
+ id,
+ groupName,
+ startTime,
+ endTime,
+ feedback,
+ criticality,
+ current,
+ predicted
+ };
+
+ if ([groupKey] in buckets) {
+ if (subEntityName in buckets[groupKey]) {
+ const {
+ [subEntityName]: { data }
+ } = buckets[groupKey];
+
+ data.push(groupTableRow);
+ } else {
+ set(buckets, `${groupKey}.${subEntityName}`, {
+ componentPath: COMPONENT_PATH,
+ title: `${DEFAULT_TITLE}${subEntityName}`,
+ data: [groupTableRow]
+ });
+ }
+ } else {
+ set(buckets, `${groupKey}`, {
+ [subEntityName]: {
+ componentPath: COMPONENT_PATH,
+ title: `${DEFAULT_TITLE}${subEntityName}`,
+ data: [groupTableRow]
+ }
+ });
+ }
+};
+
+/**
+ * Add the composite anomaly to the right bucket characterized by the subEntityName
+ *
+ * @param {Object} buckets
+ * The reference to buckets object within which the anomaly needs to be classified
+ * @param {Object} anomaly
+ * The composite anomaly that needs be classified into the appropriate bucket
+ * @param {String} subEntityName
+ * The entity name under which this anomaly falls
+ */
+const setEntitiesBucket = (buckets, anomaly, subEntityName) => {
+ const {
+ ENTITIES: { KEY: entityKey, COMPONENT_PATH }
+ } = CLASSIFICATIONS;
+ let title;
+
+ if (isEmpty(subEntityName)) {
+ const {
+ ENTITIES: { DEFAULT_TITLE }
+ } = CLASSIFICATIONS;
+
+ title = DEFAULT_TITLE;
+ } else {
+ title = subEntityName;
+ }
+
+ if ([entityKey] in buckets) {
+ if (subEntityName in buckets[entityKey]) {
+ const {
+ [subEntityName]: { data }
+ } = buckets[entityKey];
+
+ data.push(anomaly);
+ } else {
+ set(buckets, `${entityKey}.${subEntityName}`, {
+ componentPath: COMPONENT_PATH,
+ title: title,
+ data: [anomaly]
+ });
+ }
+ } else {
+ set(buckets, `${entityKey}`, {
+ [subEntityName]: {
+ componentPath: COMPONENT_PATH,
+ title: title,
+ data: [anomaly]
+ }
+ });
+ }
+};
+
+/**
+ * Classify the child anomalies of particular anomaly into metrics, groups and parent-anomalies
+ * -Anomalies of the yaml type METRIC_ALERT classify into "metrics"
+ * -Anomalies of the yaml type METRIC_ALERT and grouper as ANOMALY_SUMMARIZE classify into "groups"
+ * -Anomalies of the yaml type COMPOSITE_ALERT classify into "parent-anomalies"
+ *
+ * @param {Object} input
+ * The subtree structure that needs to be parsed
+ *
+ * @return {Object}
+ * The classification of children anomalies into the buckets of "metrics", "groups" and "entities".
+ * The structure will take the form as below
+ * {
+ * metrics: {
+ * componentPath: '',
+ * title: '',
+ * data:[{},{}] //anomaly entries
+ * },
+ * groups: {
+ * groupEntity1: {
+ * componentPath: '',
+ * title:'',
+ * data:[{},{}] //each entry in array corresponds to information for 1 group constituent
+ * },
+ * groupEntity2: {
+ * }
+ * },
+ * entities: {
+ * entity1: {
+ * componentPath: '',
+ * title:'',
+ * data:[{},{}]
+ * },
+ * entity2: {
+ * }
+ * }
+ * }
+ */
+const generateBuckets = (input) => {
+ const buckets = {};
+ const { children } = input;
+
+ for (const child of children) {
+ const { metric, properties: { detectorComponentName = '', subEntityName, groupKey } = {} } = child;
+
+ if (!isEmpty(metric)) {
+ setMetricsBucket(buckets, child, metric);
+ } else if (isEmpty(metric) && detectorComponentName.includes('ANOMALY_SUMMARIZE')) {
+ setGroupsBucket(buckets, child, subEntityName, groupKey);
+ } else {
+ setEntitiesBucket(buckets, child, subEntityName);
+ }
+ }
+
+ return buckets;
+};
+
+/**
+ * Perform drilldown of anomaly grouped by anomaly summarize grouper. This involves generating the breadcrumb information
+ * and component details for the subtree for this anomaly.
+ *
+ * @param {Object} input
+ * The subtree structure that needs to be parsed
+ *
+ * @return {Object}
+ * The breadcrumb info and data for populating component comprising of group constituents
+ */
+const parseGroupAnomaly = (input) => {
+ const output = [];
+ const data = [];
+ const {
+ GROUPS: { DEFAULT_TITLE, COMPONENT_PATH }
+ } = CLASSIFICATIONS;
+ const {
+ id,
+ children,
+ properties: { subEntityName, groupKey }
+ } = input;
+ const breadcrumbInfo = {
+ title: `${subEntityName}/${groupKey}`,
+ id
+ };
+
+ for (const anomaly of children) {
+ const {
+ id,
+ startTime,
+ endTime,
+ metric,
+ dimensions,
+ avgCurrentVal: current,
+ avgBaselineVal: predicted,
+ feedback
+ } = anomaly;
+
+ data.push({
+ id,
+ startTime,
+ endTime,
+ feedback,
+ metric,
+ dimensions,
+ current,
+ predicted
+ });
+ }
+
+ output.push({
+ componentPath: COMPONENT_PATH,
+ title: DEFAULT_TITLE,
+ data
+ });
+
+ return { breadcrumbInfo, output };
+};
+
+/**
+ * Perform drilldown of composite anomaly. This involves generating the breadcrumb information
+ * and component details for the subtree for the composite anomaly
+ *
+ * @param {Object} input
+ * The subtree structure that needs to be parsed
+ *
+ * @return {Object}
+ * The breadcrumb info and data for populating child components from input subtree.
+ */
+const parseCompositeAnomaly = (input) => {
+ const output = [];
+ const buckets = generateBuckets(input);
+ const {
+ METRICS: { KEY: metricKey },
+ GROUPS: { KEY: groupKey }
+ } = CLASSIFICATIONS;
+ const { id, startTime } = input;
+ const breadcrumbInfo = {
+ id,
+ title: getFormattedBreadcrumbTime(startTime)
+ };
+
+ for (const key in buckets) {
+ const entry = buckets[key];
+
+ if (key === metricKey) {
+ output.push(...parseMetricsBucket(entry));
+ } else if (key === groupKey) {
+ output.push(...parseGroupsBucket(entry));
+ } else {
+ output.push(...parseEntitiesBucket(entry));
+ }
+ }
+
+ return { breadcrumbInfo, output };
+};
+
+/**
+ * Perform depth-first-search to retrieve anomaly in the tree
+ *
+ * @param {Number} id
+ * The id of the anomaly to be searched
+ * @param {Object} input
+ * The subtree structure comprising the anomaly
+ *
+ * @return {Object}
+ * The anomaly referenced by id
+ */
+const findAnomaly = (id, input) => {
+ const { id: anomalyId, children } = input;
+
+ if (anomalyId === id) {
+ return input;
+ }
+ if (children.length > 0) {
+ for (const child of children) {
+ const anomaly = findAnomaly(id, child);
+ if (!isEmpty(anomaly)) {
+ return anomaly;
+ }
+ }
+ }
+};
+
+/**
+ * Parse the tree to get the breadcrumb and parent anomalies table data for root level exploration.
+ *
+ * @param {Number} explorationId
+ * The exploration Id for the alert
+ * @param {Array<Object>} input
+ * The tree structure representing anomalies data for the explorationId.
+ *
+ * @return {Object}
+ * The breadcrumb info and data for instantiating parent anomlaies table
+ */
+export const parseRoot = (explorationId, input) => {
+ const output = [];
+ const {
+ ENTITIES: { COMPONENT_PATH }
+ } = CLASSIFICATIONS;
+
+ const breadcrumbInfo = {
+ title: 'Alert Anomalies',
+ id: explorationId,
+ isRoot: true
+ };
+
+ if (Array.isArray(input)) {
+ const parentAnomaliesData = populateParentAnomaliesTable(input);
+
+ output.push({
+ componentPath: COMPONENT_PATH,
+ title: 'Entity',
+ data: parentAnomaliesData
+ });
+ }
+
+ return { breadcrumbInfo, output };
+};
+
+/**
+ * Parse the tree to get the breadcrumb and parent anomalies table data for exploration of non-root level anomaly.
+ *
+ * @param {Number} id
+ * The anomaly id
+ * @param {Array<Object> or Object} input
+ * The tree structure hosting the anomaly referenced by the id.
+ * -If the entire tree is being passed, it would in array form
+ * -If a subtree is being passed, it would be in object form
+ *
+ * @return {Object}
+ * The breadcrumb info and data for instantiating tables at any level in tree
+ */
+export const parseSubtree = (id, input) => {
+ let anomaly;
+ if (Array.isArray(input)) {
+ for (const entry of input) {
+ anomaly = findAnomaly(id, entry);
+ }
+ } else {
+ anomaly = findAnomaly(id, input);
+ }
+
+ const { metric, properties: { detectorComponentName = '' } = {} } = anomaly;
+
+ if (isEmpty(metric) && detectorComponentName.includes('ANOMALY_SUMMARIZE')) {
+ return parseGroupAnomaly(anomaly);
+ } else if (isEmpty(metric)) {
+ return parseCompositeAnomaly(anomaly);
+ }
+};
diff --git a/thirdeye/thirdeye-frontend/app/utils/constants.js b/thirdeye/thirdeye-frontend/app/utils/constants.js
index 261ac9b..99facfe 100644
--- a/thirdeye/thirdeye-frontend/app/utils/constants.js
+++ b/thirdeye/thirdeye-frontend/app/utils/constants.js
@@ -9,7 +9,10 @@ export const toastOptions = {
timeOut: 10000
};
+export const BREADCRUMB_TIME_DISPLAY_FORMAT = 'MMM D HH:mm';
+
export default {
deleteProps,
- toastOptions
+ toastOptions,
+ BREADCRUMB_TIME_DISPLAY_FORMAT
};
diff --git a/thirdeye/thirdeye-frontend/tests/unit/utils/anomalies-tree-parser-test.js b/thirdeye/thirdeye-frontend/tests/unit/utils/anomalies-tree-parser-test.js
new file mode 100644
index 0000000..bc6ad54
--- /dev/null
+++ b/thirdeye/thirdeye-frontend/tests/unit/utils/anomalies-tree-parser-test.js
@@ -0,0 +1,255 @@
+import { mockData } from "thirdeye-frontend/mocks/compositeAnomalies";
+import { module, test } from "qunit";
+import {
+ parseRoot,
+ parseSubtree
+} from "thirdeye-frontend/utils/anomalies-tree-parser";
+
+module("Unit | Utility | Anomalies tree parser utils", function() {
+ test("it parses root level parent anomalies correctly", function(assert) {
+ const explorationId = 121;
+ const { breadcrumbInfo, output } = parseRoot(explorationId, mockData);
+
+ const expectedBreadcrumbInfo = {
+ title: "Alert Anomalies",
+ isRoot: true,
+ id: 121
+ };
+
+ const expectedOutput = [
+ {
+ componentPath: "parent-anomalies",
+ data: [
+ {
+ id: 1,
+ startTime: 1599462000000,
+ endTime: 1599721200000,
+ feedback: null,
+ details: {
+ group_entity_one: 2,
+ group_entity_two: 2,
+ metric_one: 2,
+ metric_two: 1,
+ undefined: 1
+ }
+ }
+ ],
+ title: "Entity"
+ }
+ ];
+
+ assert.deepEqual(breadcrumbInfo, expectedBreadcrumbInfo);
+ assert.deepEqual(output, expectedOutput);
+ });
+
+ test("it drills down a composite anomaly correctly - level 1 drilldown example", function(assert) {
+ const anomalyId = 1;
+ const {
+ breadcrumbInfo: { id, isRoot },
+ output
+ } = parseSubtree(anomalyId, mockData);
+
+ const expectedOutput = [
+ {
+ componentPath: "entity-groups",
+ title: "ENTITY:group_entity_one",
+ data: [
+ {
+ id: 2,
+ groupName: "groupConstituentOne",
+ startTime: 1599462000000,
+ endTime: 1599721200000,
+ feedback: null,
+ criticality: "6.189942819613212",
+ current: 32,
+ predicted: 33
+ },
+ {
+ id: 5,
+ groupName: "groupConstituentTwo",
+ startTime: 1599462000000,
+ endTime: 1599721200000,
+ feedback: null,
+ criticality: "6.189942819613212",
+ current: 32,
+ predicted: 33
+ }
+ ]
+ },
+ {
+ componentPath: "entity-groups",
+ title: "ENTITY:group_entity_two",
+ data: [
+ {
+ id: 8,
+ groupName: "groupConstituentOne",
+ startTime: 1599462000000,
+ endTime: 1599721200000,
+ feedback: null,
+ criticality: "6.189942819613212",
+ current: 32,
+ predicted: 33
+ },
+ {
+ id: 11,
+ groupName: "groupConstituentTwo",
+ startTime: 1599462000000,
+ endTime: 1599721200000,
+ feedback: null,
+ criticality: "6.189942819613212",
+ current: 32,
+ predicted: 33
+ }
+ ]
+ },
+ {
+ componentPath: "entity-metrics",
+ title: "Metric Anomalies",
+ data: [
+ {
+ id: 15,
+ startTime: 1599462000000,
+ endTime: 1599462000000,
+ metric: "metric_one",
+ feedback: null,
+ current: 32,
+ predicted: 33
+ },
+ {
+ id: 16,
+ startTime: 1599462000000,
+ endTime: 1599462000000,
+ metric: "metric_one",
+ feedback: null,
+ current: 32,
+ predicted: 33
+ },
+ {
+ id: 17,
+ startTime: 1599462000000,
+ endTime: 1599462000000,
+ metric: "metric_two",
+ feedback: null,
+ current: 32,
+ predicted: 33
+ }
+ ]
+ },
+ {
+ componentPath: "parent-anomalies",
+ title: "Entity",
+ data: [
+ {
+ id: 18,
+ startTime: 1599462000000,
+ endTime: 1599462000000,
+ feedback: null,
+ details: {
+ metric_three: 1
+ }
+ }
+ ]
+ }
+ ];
+
+ //breadcrumb tests
+ assert.equal(id, 1);
+ assert.notEqual(
+ isRoot,
+ true,
+ "Breadcrumb state should be indicating we are not doing root level parsing of the tree"
+ );
+
+ //output tests
+ assert.deepEqual(output, expectedOutput);
+ });
+
+ test("it drills down an ananomaly grouped by anomaly summarize grouper correctly - level 2 drilldown example", function(assert) {
+ const anomalyId = 8;
+ const { breadcrumbInfo, output } = parseSubtree(anomalyId, mockData);
+
+ const expectedBreadcrumbInfo = {
+ title: "group_entity_two/groupConstituentOne",
+ id: 8
+ };
+
+ const expectedOutput = [
+ {
+ componentPath: "entity-groups",
+ title: "ENTITY:",
+ data: [
+ {
+ id: 9,
+ startTime: 1599462000000,
+ endTime: 1599548400000,
+ feedback: null,
+ metric: "metric_four",
+ dimensions: {
+ feature_name: "groupConstituentOne#",
+ feature_section: "groupConstituentOne",
+ dimension_three: "True",
+ use_case: "DESKTOP"
+ },
+ current: 32,
+ predicted: 33
+ },
+ {
+ id: 10,
+ startTime: 1599462000000,
+ endTime: 1599548400000,
+ feedback: null,
+ metric: "metric_four",
+ dimensions: {
+ feature_name: "groupConstituentOne#",
+ feature_section: "groupConstituentOne",
+ dimension_three: "True",
+ use_case: "DESKTOP"
+ },
+ current: 32,
+ predicted: 33
+ }
+ ]
+ }
+ ];
+
+ assert.deepEqual(breadcrumbInfo, expectedBreadcrumbInfo);
+ assert.deepEqual(output, expectedOutput);
+ });
+
+ test("it drills down a composite anomaly correctly - level 2 drilldown example (composite anomaly within a composite anomaly)", function(assert) {
+ const anomalyId = 18;
+ const {
+ breadcrumbInfo: { id, isRoot },
+ output
+ } = parseSubtree(anomalyId, mockData);
+
+ const expectedOutput = [
+ {
+ componentPath: "entity-metrics",
+ title: "Metric Anomalies",
+ data: [
+ {
+ id: 19,
+ startTime: 1599462000000,
+ endTime: 1599462000000,
+ metric: "metric_three",
+ feedback: null,
+ current: 32,
+ predicted: 33
+ }
+ ]
+ }
+ ];
+
+ //breadcrumb tests
+ assert.equal(id, 18);
+ assert.notEqual(
+ isRoot,
+ true,
+ "Breadcrumb state should be indicating we are not doing root level parsing of the tree"
+ );
+
+ //output tests
+ assert.deepEqual(output, expectedOutput);
+ });
+});
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@pinot.apache.org
For additional commands, e-mail: commits-help@pinot.apache.org