in public/pages/utils/anomalyResultUtils.ts [591:1023]
startTime: get(rawAnomaly, 'data_start_time'),
endTime: get(rawAnomaly, 'data_end_time'),
plotTime: get(rawAnomaly, 'data_end_time'),
});
});
}
}
});
return {
anomalies: anomalies,
featureData: featureData,
};
};
export const parseAnomalySummary = (
anomalySummaryResult: any
): AnomalySummary => {
const anomalyCount = get(
anomalySummaryResult,
'response.aggregations.count_anomalies.value',
0
);
return {
anomalyOccurrence: anomalyCount,
minAnomalyGrade: anomalyCount
? toFixedNumberForAnomaly(
get(
anomalySummaryResult,
'response.aggregations.min_anomaly_grade.value'
)
)
: 0,
maxAnomalyGrade: anomalyCount
? toFixedNumberForAnomaly(
get(
anomalySummaryResult,
'response.aggregations.max_anomaly_grade.value'
)
)
: 0,
avgAnomalyGrade: anomalyCount
? toFixedNumberForAnomaly(
get(
anomalySummaryResult,
'response.aggregations.avg_anomaly_grade.value'
)
)
: 0,
minConfidence: anomalyCount
? toFixedNumberForAnomaly(
get(
anomalySummaryResult,
'response.aggregations.min_confidence.value'
)
)
: 0,
maxConfidence: anomalyCount
? toFixedNumberForAnomaly(
get(
anomalySummaryResult,
'response.aggregations.max_confidence.value'
)
)
: 0,
lastAnomalyOccurrence: anomalyCount
? minuteDateFormatter(
get(
anomalySummaryResult,
'response.aggregations.max_data_end_time.value'
)
)
: '',
};
};
export const parsePureAnomalies = (
anomalySummaryResult: any
): AnomalyData[] => {
const anomaliesHits = get(anomalySummaryResult, 'response.hits.hits', []);
const anomalies = [] as AnomalyData[];
if (anomaliesHits.length > 0) {
anomaliesHits.forEach((item: any) => {
const rawAnomaly = get(item, '_source');
anomalies.push({
anomalyGrade: toFixedNumberForAnomaly(get(rawAnomaly, 'anomaly_grade')),
confidence: toFixedNumberForAnomaly(get(rawAnomaly, 'confidence')),
startTime: get(rawAnomaly, 'data_start_time'),
endTime: get(rawAnomaly, 'data_end_time'),
plotTime: get(rawAnomaly, 'data_end_time'),
entity: get(rawAnomaly, 'entity'),
});
});
}
return anomalies;
};
export type FeatureDataPoint = {
isMissing: boolean;
plotTime: number;
startTime: number;
endTime: number;
};
export const FEATURE_DATA_CHECK_WINDOW_OFFSET = 2;
export const getFeatureDataPoints = (
featureData: FeatureAggregationData[],
interval: number,
dateRange: DateRange | undefined
): FeatureDataPoint[] => {
const featureDataPoints = [] as FeatureDataPoint[];
if (!dateRange) {
return featureDataPoints;
}
const existingTimes = isEmpty(featureData)
? []
: featureData
.map((feature) => getRoundedTimeInMin(feature.startTime))
.filter((featureTime) => featureTime != undefined);
for (
let currentTime = getRoundedTimeInMin(dateRange.startDate);
currentTime <
// skip checking for latest interval as data point for it may not be delivered in time
getRoundedTimeInMin(
dateRange.endDate -
FEATURE_DATA_CHECK_WINDOW_OFFSET * interval * MIN_IN_MILLI_SECS
);
currentTime += interval * MIN_IN_MILLI_SECS
) {
const isExisting = findTimeExistsInWindow(
existingTimes,
getRoundedTimeInMin(currentTime),
getRoundedTimeInMin(currentTime) + interval * MIN_IN_MILLI_SECS
);
featureDataPoints.push({
isMissing: !isExisting,
plotTime: currentTime + interval * MIN_IN_MILLI_SECS,
startTime: currentTime,
endTime: currentTime + interval * MIN_IN_MILLI_SECS,
});
}
return featureDataPoints;
};
const findTimeExistsInWindow = (
timestamps: any[],
startTime: number,
endTime: number
): boolean => {
// timestamps is in desc order
let result = false;
if (isEmpty(timestamps)) {
return result;
}
let left = 0;
let right = timestamps.length - 1;
while (left <= right) {
let middle = Math.floor((right + left) / 2);
if (timestamps[middle] >= startTime && timestamps[middle] < endTime) {
result = true;
break;
}
if (timestamps[middle] < startTime) {
right = middle - 1;
}
if (timestamps[middle] >= endTime) {
left = middle + 1;
}
}
return result;
};
const getRoundedTimeInMin = (timestamp: number): number => {
return Math.round(timestamp / MIN_IN_MILLI_SECS) * MIN_IN_MILLI_SECS;
};
const sampleFeatureMissingDataPoints = (
featureMissingDataPoints: FeatureDataPoint[],
dateRange?: DateRange
): FeatureDataPoint[] => {
if (!dateRange) {
return featureMissingDataPoints;
}
const sampleTimeWindows = calculateTimeWindowsWithMaxDataPoints(
MAX_FEATURE_ANNOTATIONS,
dateRange
);
const sampledResults = [] as FeatureDataPoint[];
for (const timeWindow of sampleTimeWindows) {
const sampledDataPoint = getMiddleDataPoint(
getDataPointsInWindow(featureMissingDataPoints, timeWindow)
);
if (sampledDataPoint) {
sampledResults.push({
...sampledDataPoint,
startTime: Math.min(timeWindow.startDate, sampledDataPoint.startTime),
endTime: Math.max(timeWindow.endDate, sampledDataPoint.endTime),
} as FeatureDataPoint);
}
}
return sampledResults;
};
const getMiddleDataPoint = (arr: any[]) => {
if (arr && arr.length > 0) {
return arr[Math.floor(arr.length / 2)];
}
return undefined;
};
const getDataPointsInWindow = (
dataPoints: FeatureDataPoint[],
timeWindow: DateRange
) => {
return dataPoints.filter(
(dataPoint) =>
get(dataPoint, 'plotTime', 0) >= timeWindow.startDate &&
get(dataPoint, 'plotTime', 0) < timeWindow.endDate
);
};
const generateFeatureMissingAnnotations = (
featureMissingDataPoints: FeatureDataPoint[]
) => {
return featureMissingDataPoints.map((feature) => ({
dataValue: feature.plotTime,
details: `There is feature data point missing between ${moment(
feature.startTime
).format('MM/DD/YY h:mm A')} and ${moment(feature.endTime).format(
'MM/DD/YY h:mm A'
)}`,
header: dateFormatter(feature.plotTime),
}));
};
const finalizeFeatureMissingDataAnnotations = (
featureMissingDataPoints: any[],
dateRange?: DateRange
) => {
const sampledFeatureMissingDataPoints = sampleFeatureMissingDataPoints(
featureMissingDataPoints,
dateRange
);
return generateFeatureMissingAnnotations(sampledFeatureMissingDataPoints);
};
export const getFeatureMissingDataAnnotations = (
featureData: FeatureAggregationData[],
interval: number,
queryDateRange?: DateRange,
displayDateRange?: DateRange
) => {
const featureMissingDataPoints = getFeatureDataPoints(
featureData,
interval,
queryDateRange
).filter((dataPoint) => get(dataPoint, 'isMissing', false));
const featureMissingAnnotations = finalizeFeatureMissingDataAnnotations(
featureMissingDataPoints,
displayDateRange
);
return featureMissingAnnotations;
};
// returns feature data points(missing/existing both included) for detector in a map like
// {
// 'featureName': data points[]
// }
export const getFeatureDataPointsForDetector = (
detector: Detector,
featuresData: { [key: string]: FeatureAggregationData[] },
interval: number,
dateRange?: DateRange
) => {
let featureDataPointsForDetector = {} as {
[key: string]: FeatureDataPoint[];
};
const allFeatures = get(
detector,
'featureAttributes',
[] as FeatureAttributes[]
);
allFeatures.forEach((feature) => {
//@ts-ignore
const featureData = featuresData[feature.featureId];
const featureDataPoints = getFeatureDataPoints(
featureData,
interval,
dateRange
);
featureDataPointsForDetector = {
...featureDataPointsForDetector,
[feature.featureName]: featureDataPoints,
};
});
return featureDataPointsForDetector;
};
export const getFeatureMissingSeverities = (featuresDataPoint: {
[key: string]: FeatureDataPoint[];
}): Map<MISSING_FEATURE_DATA_SEVERITY, string[]> => {
const featureMissingSeverities = new Map();
for (const [featureName, featureDataPoints] of Object.entries(
featuresDataPoint
)) {
// all feature data points should have same length
let featuresWithMissingData = [] as string[];
if (featureDataPoints.length <= 1) {
// return empty map
return featureMissingSeverities;
}
if (
featureDataPoints.length === 2 &&
featureDataPoints[0].isMissing &&
featureDataPoints[1].isMissing
) {
if (featureMissingSeverities.has(MISSING_FEATURE_DATA_SEVERITY.YELLOW)) {
featuresWithMissingData = featureMissingSeverities.get(
MISSING_FEATURE_DATA_SEVERITY.YELLOW
);
}
featuresWithMissingData.push(featureName);
featureMissingSeverities.set(
MISSING_FEATURE_DATA_SEVERITY.YELLOW,
featuresWithMissingData
);
continue;
}
const orderedFeatureDataPoints = orderBy(
featureDataPoints,
// sort by plot time in desc order
(dataPoint) => get(dataPoint, 'plotTime', 0),
SORT_DIRECTION.DESC
);
// feature has >= 3 data points
if (
orderedFeatureDataPoints.length >= 3 &&
orderedFeatureDataPoints[0].isMissing &&
orderedFeatureDataPoints[1].isMissing
) {
// at least latest 2 ones are missing
let currentSeverity = MISSING_FEATURE_DATA_SEVERITY.YELLOW;
if (orderedFeatureDataPoints[2].isMissing) {
// all the latest 3 ones are missing
currentSeverity = MISSING_FEATURE_DATA_SEVERITY.RED;
}
if (featureMissingSeverities.has(currentSeverity)) {
featuresWithMissingData = featureMissingSeverities.get(currentSeverity);
}
featuresWithMissingData.push(featureName);
featureMissingSeverities.set(currentSeverity, featuresWithMissingData);
}
}
return featureMissingSeverities;
};
export const getFeatureDataMissingMessageAndActionItem = (
featureMissingSev: MISSING_FEATURE_DATA_SEVERITY | undefined,
featuresWithMissingData: string[],
hideFeatureMessage: boolean
) => {
switch (featureMissingSev) {
case MISSING_FEATURE_DATA_SEVERITY.YELLOW:
return {
message: `Recent data is missing for feature${
featuresWithMissingData.length > 1 ? 's' : ''
}: ${featuresWithMissingData.join(
', '
)}. So, anomaly result is missing during this time.`,
actionItem:
'Make sure your data is ingested correctly.' + hideFeatureMessage
? ''
: ' See the feature data shown below for more details.',
};
case MISSING_FEATURE_DATA_SEVERITY.RED:
return {
message: `Data is not being ingested correctly for feature${
featuresWithMissingData.length > 1 ? 's' : ''
}: ${featuresWithMissingData.join(
', '
)}. So, anomaly result is missing during this time.`,
actionItem:
`${DETECTOR_INIT_FAILURES.NO_TRAINING_DATA.actionItem}` +
hideFeatureMessage
? ''
: ' See the feature data shown below for more details.',
};
default:
return {
message: '',
actionItem: '',
};
}
};
// Generates query to get the top anomalous entities (or entity pairs)
// for some detector, sorting by severity or occurrence.
export const getTopAnomalousEntitiesQuery = (
startTime: number,
endTime: number,
detectorId: string,
size: number,
sortType: AnomalyHeatmapSortType,
isMultiCategory: boolean,
isHistorical?: boolean,
taskId?: string,
includedEntities?: Entity[]
) => {
const termField =
isHistorical && taskId ? { task_id: taskId } : { detector_id: detectorId };
// To handle BWC, we will return 2 possible queries based on the # of categorical fields:
// (1) legacy way (1 category field): bucket aggregate over the single, nested, 'entity.value' field
// (2) new way (>= 2 category fields): bucket aggregate over the new 'model_id' field
let requestBody = isMultiCategory
? {
size: 0,
query: {
bool: {
filter: [
{
range: {