mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[maps] add context for 'No longer contained' geo-containment alert (#136451)
* [maps] add context for 'No longer contained' geo-containement alert * populate recovered alert context * do not include context.containingBoundaryId and context.containingBoundaryName for recovered alert context * [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix' * do not set containement in recovered context * add comments * clean up * comment typo * review feedback rename containtments => containments * update context variable descriptions * get_context unit tests * update jest snapshot Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
3c76959464
commit
91f15ff355
7 changed files with 380 additions and 133 deletions
|
@ -46,67 +46,73 @@ export type GeoContainmentExtractedParams = Omit<
|
|||
boundaryIndexRefName: string;
|
||||
};
|
||||
|
||||
const actionVariableContextEntityIdLabel = i18n.translate(
|
||||
'xpack.stackAlerts.geoContainment.actionVariableContextEntityIdLabel',
|
||||
{
|
||||
defaultMessage: 'The entity ID of the document that triggered the alert',
|
||||
}
|
||||
);
|
||||
|
||||
const actionVariableContextEntityDateTimeLabel = i18n.translate(
|
||||
'xpack.stackAlerts.geoContainment.actionVariableContextFromEntityDateTimeLabel',
|
||||
{
|
||||
defaultMessage: `The date the entity was recorded in the boundary`,
|
||||
}
|
||||
);
|
||||
|
||||
const actionVariableContextEntityDocumentIdLabel = i18n.translate(
|
||||
'xpack.stackAlerts.geoContainment.actionVariableContextFromEntityDocumentIdLabel',
|
||||
{
|
||||
defaultMessage: 'The id of the contained entity document',
|
||||
}
|
||||
);
|
||||
|
||||
const actionVariableContextDetectionDateTimeLabel = i18n.translate(
|
||||
'xpack.stackAlerts.geoContainment.actionVariableContextDetectionDateTimeLabel',
|
||||
{
|
||||
defaultMessage: 'The alert interval end time this change was recorded',
|
||||
}
|
||||
);
|
||||
|
||||
const actionVariableContextEntityLocationLabel = i18n.translate(
|
||||
'xpack.stackAlerts.geoContainment.actionVariableContextFromEntityLocationLabel',
|
||||
{
|
||||
defaultMessage: 'The location of the entity',
|
||||
}
|
||||
);
|
||||
|
||||
const actionVariableContextContainingBoundaryIdLabel = i18n.translate(
|
||||
'xpack.stackAlerts.geoContainment.actionVariableContextContainingBoundaryIdLabel',
|
||||
{
|
||||
defaultMessage: 'The id of the boundary containing the entity',
|
||||
}
|
||||
);
|
||||
|
||||
const actionVariableContextContainingBoundaryNameLabel = i18n.translate(
|
||||
'xpack.stackAlerts.geoContainment.actionVariableContextContainingBoundaryNameLabel',
|
||||
{
|
||||
defaultMessage: 'The boundary the entity is currently located within',
|
||||
}
|
||||
);
|
||||
|
||||
const actionVariables = {
|
||||
context: [
|
||||
// Alert-specific data
|
||||
{ name: 'entityId', description: actionVariableContextEntityIdLabel },
|
||||
{ name: 'entityDateTime', description: actionVariableContextEntityDateTimeLabel },
|
||||
{ name: 'entityDocumentId', description: actionVariableContextEntityDocumentIdLabel },
|
||||
{ name: 'detectionDateTime', description: actionVariableContextDetectionDateTimeLabel },
|
||||
{ name: 'entityLocation', description: actionVariableContextEntityLocationLabel },
|
||||
{ name: 'containingBoundaryId', description: actionVariableContextContainingBoundaryIdLabel },
|
||||
{
|
||||
name: 'entityId',
|
||||
description: i18n.translate(
|
||||
'xpack.stackAlerts.geoContainment.actionVariableContextEntityIdLabel',
|
||||
{
|
||||
defaultMessage: 'The entity ID of the document that triggered the alert',
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'entityDateTime',
|
||||
description: i18n.translate(
|
||||
'xpack.stackAlerts.geoContainment.actionVariableContextFromEntityDateTimeLabel',
|
||||
{
|
||||
defaultMessage: `The date the entity was recorded in the boundary`,
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'entityDocumentId',
|
||||
description: i18n.translate(
|
||||
'xpack.stackAlerts.geoContainment.actionVariableContextFromEntityDocumentIdLabel',
|
||||
{
|
||||
defaultMessage: 'The id of the contained entity document',
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'detectionDateTime',
|
||||
description: i18n.translate(
|
||||
'xpack.stackAlerts.geoContainment.actionVariableContextDetectionDateTimeLabel',
|
||||
{
|
||||
defaultMessage: 'The alert interval end time this change was recorded',
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'entityLocation',
|
||||
description: i18n.translate(
|
||||
'xpack.stackAlerts.geoContainment.actionVariableContextFromEntityLocationLabel',
|
||||
{
|
||||
defaultMessage: 'The location of the entity',
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'containingBoundaryId',
|
||||
description: i18n.translate(
|
||||
'xpack.stackAlerts.geoContainment.actionVariableContextContainingBoundaryIdLabel',
|
||||
{
|
||||
defaultMessage:
|
||||
'The id of the boundary containing the entity. Value not set for recovered alerts',
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'containingBoundaryName',
|
||||
description: actionVariableContextContainingBoundaryNameLabel,
|
||||
description: i18n.translate(
|
||||
'xpack.stackAlerts.geoContainment.actionVariableContextContainingBoundaryNameLabel',
|
||||
{
|
||||
defaultMessage:
|
||||
'The name of the boundary containing the entity. Value not set for recovered alerts',
|
||||
}
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
@ -143,8 +149,9 @@ export interface GeoContainmentInstanceContext extends AlertInstanceContext {
|
|||
entityDocumentId: string;
|
||||
detectionDateTime: string;
|
||||
entityLocation: string;
|
||||
containingBoundaryId: string;
|
||||
containingBoundaryName: unknown;
|
||||
// recovered alerts are not contained in boundary so context does not include boundary state
|
||||
containingBoundaryId?: string;
|
||||
containingBoundaryName?: unknown;
|
||||
}
|
||||
|
||||
export type GeoContainmentAlertType = RuleType<
|
||||
|
@ -229,6 +236,7 @@ export function getAlertType(logger: Logger): GeoContainmentAlertType {
|
|||
defaultMessage: 'No longer contained',
|
||||
}),
|
||||
},
|
||||
doesSetRecoveryContext: true,
|
||||
defaultActionGroupId: ActionGroupId,
|
||||
executor: getGeoContainmentExecutor(logger),
|
||||
producer: STACK_ALERTS_FEATURE_ID,
|
||||
|
|
|
@ -18,10 +18,15 @@ import {
|
|||
|
||||
export const OTHER_CATEGORY = 'other';
|
||||
// Consider dynamically obtaining from config?
|
||||
const MAX_TOP_LEVEL_QUERY_SIZE = 0;
|
||||
const MAX_SHAPES_QUERY_SIZE = 10000;
|
||||
const MAX_BUCKETS_LIMIT = 65535;
|
||||
|
||||
interface BoundaryHit {
|
||||
_index: string;
|
||||
_id: string;
|
||||
fields?: Record<string, unknown[]>;
|
||||
}
|
||||
|
||||
export const getEsFormattedQuery = (query: Query, indexPattern?: DataViewBase) => {
|
||||
let esFormattedQuery;
|
||||
|
||||
|
@ -48,33 +53,40 @@ export async function getShapesFilters(
|
|||
const filters: Record<string, unknown> = {};
|
||||
const shapesIdsNamesMap: Record<string, unknown> = {};
|
||||
// Get all shapes in index
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const boundaryData = await esClient.search<Record<string, any>>({
|
||||
const boundaryData = await esClient.search<Record<string, BoundaryHit>>({
|
||||
index: boundaryIndexTitle,
|
||||
body: {
|
||||
size: MAX_SHAPES_QUERY_SIZE,
|
||||
_source: false,
|
||||
fields: boundaryNameField ? [boundaryNameField] : [],
|
||||
...(boundaryIndexQuery ? { query: getEsFormattedQuery(boundaryIndexQuery) } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
boundaryData.hits.hits.forEach(({ _index, _id }: { _index: string; _id: string }) => {
|
||||
filters[_id] = {
|
||||
for (let i = 0; i < boundaryData.hits.hits.length; i++) {
|
||||
const boundaryHit: BoundaryHit = boundaryData.hits.hits[i];
|
||||
filters[boundaryHit._id] = {
|
||||
geo_shape: {
|
||||
[geoField]: {
|
||||
indexed_shape: {
|
||||
index: _index,
|
||||
id: _id,
|
||||
index: boundaryHit._index,
|
||||
id: boundaryHit._id,
|
||||
path: boundaryGeoField,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
if (boundaryNameField) {
|
||||
boundaryData.hits.hits.forEach(({ _source, _id }) => {
|
||||
shapesIdsNamesMap[_id] = _source![boundaryNameField];
|
||||
});
|
||||
if (
|
||||
boundaryNameField &&
|
||||
boundaryHit.fields &&
|
||||
boundaryHit.fields[boundaryNameField] &&
|
||||
boundaryHit.fields[boundaryNameField].length
|
||||
) {
|
||||
// fields API always returns an array, grab first value
|
||||
shapesIdsNamesMap[boundaryHit._id] = boundaryHit.fields[boundaryNameField][0];
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
shapesFilters: filters,
|
||||
shapesIdsNamesMap,
|
||||
|
@ -125,7 +137,7 @@ export async function executeEsQueryFactory(
|
|||
const esQuery: Record<string, any> = {
|
||||
index,
|
||||
body: {
|
||||
size: MAX_TOP_LEVEL_QUERY_SIZE,
|
||||
size: 0, // do not fetch hits
|
||||
aggs: {
|
||||
shapes: {
|
||||
filters: {
|
||||
|
|
|
@ -19,15 +19,14 @@ import {
|
|||
} from './alert_type';
|
||||
|
||||
import { GEO_CONTAINMENT_ID } from './alert_type';
|
||||
|
||||
export type LatestEntityLocation = GeoContainmentInstanceState;
|
||||
import { getAlertId, getContainedAlertContext, getRecoveredAlertContext } from './get_context';
|
||||
|
||||
// Flatten agg results and get latest locations for each entity
|
||||
export function transformResults(
|
||||
results: estypes.SearchResponse<unknown> | undefined,
|
||||
dateField: string,
|
||||
geoField: string
|
||||
): Map<string, LatestEntityLocation[]> {
|
||||
): Map<string, GeoContainmentInstanceState[]> {
|
||||
if (!results) {
|
||||
return new Map();
|
||||
}
|
||||
|
@ -67,8 +66,8 @@ export function transformResults(
|
|||
// Get unique
|
||||
.reduce(
|
||||
(
|
||||
accu: Map<string, LatestEntityLocation[]>,
|
||||
el: LatestEntityLocation & { entityName: string }
|
||||
accu: Map<string, GeoContainmentInstanceState[]>,
|
||||
el: GeoContainmentInstanceState & { entityName: string }
|
||||
) => {
|
||||
const { entityName, ...locationData } = el;
|
||||
if (entityName) {
|
||||
|
@ -84,61 +83,65 @@ export function transformResults(
|
|||
return orderedResults;
|
||||
}
|
||||
|
||||
export function getActiveEntriesAndGenerateAlerts(
|
||||
prevLocationMap: Map<string, LatestEntityLocation[]>,
|
||||
currLocationMap: Map<string, LatestEntityLocation[]>,
|
||||
export function getEntitiesAndGenerateAlerts(
|
||||
prevLocationMap: Map<string, GeoContainmentInstanceState[]>,
|
||||
currLocationMap: Map<string, GeoContainmentInstanceState[]>,
|
||||
alertFactory: RuleExecutorServices<
|
||||
GeoContainmentInstanceState,
|
||||
GeoContainmentInstanceContext,
|
||||
typeof ActionGroupId
|
||||
>['alertFactory'],
|
||||
shapesIdsNamesMap: Record<string, unknown>,
|
||||
currIntervalEndTime: Date
|
||||
) {
|
||||
const allActiveEntriesMap: Map<string, LatestEntityLocation[]> = new Map([
|
||||
windowEnd: Date
|
||||
): {
|
||||
activeEntities: Map<string, GeoContainmentInstanceState[]>;
|
||||
inactiveEntities: Map<string, GeoContainmentInstanceState[]>;
|
||||
} {
|
||||
const activeEntities: Map<string, GeoContainmentInstanceState[]> = new Map([
|
||||
...prevLocationMap,
|
||||
...currLocationMap,
|
||||
]);
|
||||
allActiveEntriesMap.forEach((locationsArr, entityName) => {
|
||||
const inactiveEntities: Map<string, GeoContainmentInstanceState[]> = new Map();
|
||||
activeEntities.forEach((containments, entityName) => {
|
||||
// Generate alerts
|
||||
locationsArr.forEach(({ location, shapeLocationId, dateInShape, docId }) => {
|
||||
const context = {
|
||||
entityId: entityName,
|
||||
entityDateTime: dateInShape || null,
|
||||
entityDocumentId: docId,
|
||||
detectionDateTime: new Date(currIntervalEndTime).toISOString(),
|
||||
entityLocation: `POINT (${location[0]} ${location[1]})`,
|
||||
containingBoundaryId: shapeLocationId,
|
||||
containingBoundaryName: shapesIdsNamesMap[shapeLocationId] || shapeLocationId,
|
||||
};
|
||||
const alertInstanceId = `${entityName}-${context.containingBoundaryName}`;
|
||||
if (shapeLocationId !== OTHER_CATEGORY) {
|
||||
alertFactory.create(alertInstanceId).scheduleActions(ActionGroupId, context);
|
||||
containments.forEach((containment) => {
|
||||
if (containment.shapeLocationId !== OTHER_CATEGORY) {
|
||||
const context = getContainedAlertContext({
|
||||
entityName,
|
||||
containment,
|
||||
shapesIdsNamesMap,
|
||||
windowEnd,
|
||||
});
|
||||
alertFactory
|
||||
.create(getAlertId(entityName, context.containingBoundaryName))
|
||||
.scheduleActions(ActionGroupId, context);
|
||||
}
|
||||
});
|
||||
|
||||
if (locationsArr[0].shapeLocationId === OTHER_CATEGORY) {
|
||||
allActiveEntriesMap.delete(entityName);
|
||||
// Entity in "other" filter bucket is no longer contained by any boundary and switches from "active" to "inactive"
|
||||
if (containments[0].shapeLocationId === OTHER_CATEGORY) {
|
||||
inactiveEntities.set(entityName, containments);
|
||||
activeEntities.delete(entityName);
|
||||
return;
|
||||
}
|
||||
|
||||
const otherCatIndex = locationsArr.findIndex(
|
||||
const otherCatIndex = containments.findIndex(
|
||||
({ shapeLocationId }) => shapeLocationId === OTHER_CATEGORY
|
||||
);
|
||||
if (otherCatIndex >= 0) {
|
||||
const afterOtherLocationsArr = locationsArr.slice(0, otherCatIndex);
|
||||
allActiveEntriesMap.set(entityName, afterOtherLocationsArr);
|
||||
const afterOtherLocationsArr = containments.slice(0, otherCatIndex);
|
||||
activeEntities.set(entityName, afterOtherLocationsArr);
|
||||
} else {
|
||||
allActiveEntriesMap.set(entityName, locationsArr);
|
||||
activeEntities.set(entityName, containments);
|
||||
}
|
||||
});
|
||||
return allActiveEntriesMap;
|
||||
return { activeEntities, inactiveEntities };
|
||||
}
|
||||
|
||||
export const getGeoContainmentExecutor = (log: Logger): GeoContainmentAlertType['executor'] =>
|
||||
async function ({
|
||||
previousStartedAt: currIntervalStartTime,
|
||||
startedAt: currIntervalEndTime,
|
||||
previousStartedAt: windowStart,
|
||||
startedAt: windowEnd,
|
||||
services,
|
||||
params,
|
||||
alertId,
|
||||
|
@ -166,37 +169,57 @@ export const getGeoContainmentExecutor = (log: Logger): GeoContainmentAlertType[
|
|||
|
||||
// Start collecting data only on the first cycle
|
||||
let currentIntervalResults: estypes.SearchResponse<unknown> | undefined;
|
||||
if (!currIntervalStartTime) {
|
||||
if (!windowStart) {
|
||||
log.debug(`alert ${GEO_CONTAINMENT_ID}:${alertId} alert initialized. Collecting data`);
|
||||
// Consider making first time window configurable?
|
||||
const START_TIME_WINDOW = 1;
|
||||
const tempPreviousEndTime = new Date(currIntervalEndTime);
|
||||
const tempPreviousEndTime = new Date(windowEnd);
|
||||
tempPreviousEndTime.setMinutes(tempPreviousEndTime.getMinutes() - START_TIME_WINDOW);
|
||||
currentIntervalResults = await executeEsQuery(tempPreviousEndTime, currIntervalEndTime);
|
||||
currentIntervalResults = await executeEsQuery(tempPreviousEndTime, windowEnd);
|
||||
} else {
|
||||
currentIntervalResults = await executeEsQuery(currIntervalStartTime, currIntervalEndTime);
|
||||
currentIntervalResults = await executeEsQuery(windowStart, windowEnd);
|
||||
}
|
||||
|
||||
const currLocationMap: Map<string, LatestEntityLocation[]> = transformResults(
|
||||
const currLocationMap: Map<string, GeoContainmentInstanceState[]> = transformResults(
|
||||
currentIntervalResults,
|
||||
params.dateField,
|
||||
params.geoField
|
||||
);
|
||||
|
||||
const prevLocationMap: Map<string, LatestEntityLocation[]> = new Map([
|
||||
...Object.entries((state.prevLocationMap as Record<string, LatestEntityLocation[]>) || {}),
|
||||
const prevLocationMap: Map<string, GeoContainmentInstanceState[]> = new Map([
|
||||
...Object.entries(
|
||||
(state.prevLocationMap as Record<string, GeoContainmentInstanceState[]>) || {}
|
||||
),
|
||||
]);
|
||||
const allActiveEntriesMap = getActiveEntriesAndGenerateAlerts(
|
||||
const { activeEntities, inactiveEntities } = getEntitiesAndGenerateAlerts(
|
||||
prevLocationMap,
|
||||
currLocationMap,
|
||||
services.alertFactory,
|
||||
shapesIdsNamesMap,
|
||||
currIntervalEndTime
|
||||
windowEnd
|
||||
);
|
||||
|
||||
const { getRecoveredAlerts } = services.alertFactory.done();
|
||||
for (const recoveredAlert of getRecoveredAlerts()) {
|
||||
const recoveredAlertId = recoveredAlert.getId();
|
||||
try {
|
||||
const context = getRecoveredAlertContext({
|
||||
alertId: recoveredAlertId,
|
||||
activeEntities,
|
||||
inactiveEntities,
|
||||
windowEnd,
|
||||
});
|
||||
if (context) {
|
||||
recoveredAlert.setContext(context);
|
||||
}
|
||||
} catch (e) {
|
||||
log.warn(`Unable to set alert context for recovered alert, error: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
shapesFilters,
|
||||
shapesIdsNamesMap,
|
||||
prevLocationMap: Object.fromEntries(allActiveEntriesMap),
|
||||
prevLocationMap: Object.fromEntries(activeEntities),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import { GeoContainmentInstanceContext, GeoContainmentInstanceState } from './alert_type';
|
||||
|
||||
export function getAlertId(entityName: string, boundaryName: unknown) {
|
||||
return `${entityName}-${boundaryName}`;
|
||||
}
|
||||
|
||||
function splitAlertId(alertId: string): { entityName: string; boundaryName: string } {
|
||||
const split = alertId.split('-');
|
||||
|
||||
// entityName and boundaryName values are "user provided data" from elasticsearch
|
||||
// Values may contain '-', breaking alertId parsing
|
||||
// In these cases, recovered alert context cannot be obtained
|
||||
if (split.length !== 2) {
|
||||
throw new Error(
|
||||
`Can not split alertId '${alertId}' into entity name and boundary name. This can happen when entity name and boundary name contain '-' character.`
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
entityName: split[0],
|
||||
boundaryName: split[1],
|
||||
};
|
||||
}
|
||||
|
||||
function getAlertContext({
|
||||
entityName,
|
||||
containment,
|
||||
shapesIdsNamesMap,
|
||||
windowEnd,
|
||||
isRecovered,
|
||||
}: {
|
||||
entityName: string;
|
||||
containment: GeoContainmentInstanceState;
|
||||
shapesIdsNamesMap?: Record<string, unknown>;
|
||||
windowEnd: Date;
|
||||
isRecovered: boolean;
|
||||
}): GeoContainmentInstanceContext {
|
||||
const context: GeoContainmentInstanceContext = {
|
||||
entityId: entityName,
|
||||
entityDateTime: containment.dateInShape || null,
|
||||
entityDocumentId: containment.docId,
|
||||
entityLocation: `POINT (${containment.location[0]} ${containment.location[1]})`,
|
||||
detectionDateTime: new Date(windowEnd).toISOString(),
|
||||
};
|
||||
if (!isRecovered) {
|
||||
context.containingBoundaryId = containment.shapeLocationId;
|
||||
context.containingBoundaryName =
|
||||
(shapesIdsNamesMap && shapesIdsNamesMap[containment.shapeLocationId]) ||
|
||||
containment.shapeLocationId;
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export function getContainedAlertContext(args: {
|
||||
entityName: string;
|
||||
containment: GeoContainmentInstanceState;
|
||||
shapesIdsNamesMap: Record<string, unknown>;
|
||||
windowEnd: Date;
|
||||
}): GeoContainmentInstanceContext {
|
||||
return getAlertContext({ ...args, isRecovered: false });
|
||||
}
|
||||
|
||||
export function getRecoveredAlertContext({
|
||||
alertId,
|
||||
activeEntities,
|
||||
inactiveEntities,
|
||||
windowEnd,
|
||||
}: {
|
||||
alertId: string;
|
||||
activeEntities: Map<string, GeoContainmentInstanceState[]>;
|
||||
inactiveEntities: Map<string, GeoContainmentInstanceState[]>;
|
||||
windowEnd: Date;
|
||||
}): GeoContainmentInstanceContext | null {
|
||||
const { entityName } = splitAlertId(alertId);
|
||||
|
||||
// recovered alert's latest entity location is either:
|
||||
// 1) activeEntities - entity moved from one boundary to another boundary
|
||||
// 2) inactiveEntities - entity moved from one boundary to outside all boundaries
|
||||
let containment: GeoContainmentInstanceState | undefined;
|
||||
if (activeEntities.has(entityName) && activeEntities.get(entityName)?.length) {
|
||||
containment = _.orderBy(activeEntities.get(entityName), ['dateInShape'], ['desc'])[0];
|
||||
} else if (inactiveEntities.has(entityName) && inactiveEntities.get(entityName)?.length) {
|
||||
containment = inactiveEntities.get(entityName)![0];
|
||||
}
|
||||
|
||||
return containment
|
||||
? getAlertContext({
|
||||
entityName,
|
||||
containment,
|
||||
windowEnd,
|
||||
isRecovered: true,
|
||||
})
|
||||
: null;
|
||||
}
|
|
@ -24,11 +24,11 @@ Object {
|
|||
"name": "entityLocation",
|
||||
},
|
||||
Object {
|
||||
"description": "The id of the boundary containing the entity",
|
||||
"description": "The id of the boundary containing the entity. Value not set for recovered alerts",
|
||||
"name": "containingBoundaryId",
|
||||
},
|
||||
Object {
|
||||
"description": "The boundary the entity is currently located within",
|
||||
"description": "The name of the boundary containing the entity. Value not set for recovered alerts",
|
||||
"name": "containingBoundaryName",
|
||||
},
|
||||
],
|
||||
|
|
|
@ -12,7 +12,7 @@ import sampleAggsJsonResponse from './es_sample_response.json';
|
|||
import sampleShapesJsonResponse from './es_sample_response_shapes.json';
|
||||
import sampleAggsJsonResponseWithNesting from './es_sample_response_with_nesting.json';
|
||||
import {
|
||||
getActiveEntriesAndGenerateAlerts,
|
||||
getEntitiesAndGenerateAlerts,
|
||||
transformResults,
|
||||
getGeoContainmentExecutor,
|
||||
} from '../geo_containment';
|
||||
|
@ -170,7 +170,7 @@ describe('geo_containment', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('getActiveEntriesAndGenerateAlerts', () => {
|
||||
describe('getEntitiesAndGenerateAlerts', () => {
|
||||
const testAlertActionArr: unknown[] = [];
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
@ -252,14 +252,14 @@ describe('geo_containment', () => {
|
|||
|
||||
it('should use currently active entities if no older entity entries', () => {
|
||||
const emptyPrevLocationMap = new Map();
|
||||
const allActiveEntriesMap = getActiveEntriesAndGenerateAlerts(
|
||||
const { activeEntities } = getEntitiesAndGenerateAlerts(
|
||||
emptyPrevLocationMap,
|
||||
currLocationMap,
|
||||
alertFactory(contextKeys, testAlertActionArr),
|
||||
emptyShapesIdsNamesMap,
|
||||
currentDateTime
|
||||
);
|
||||
expect(allActiveEntriesMap).toEqual(currLocationMap);
|
||||
expect(activeEntities).toEqual(currLocationMap);
|
||||
expect(testAlertActionArr).toMatchObject(expectedAlertResults);
|
||||
});
|
||||
|
||||
|
@ -277,14 +277,14 @@ describe('geo_containment', () => {
|
|||
],
|
||||
],
|
||||
]);
|
||||
const allActiveEntriesMap = getActiveEntriesAndGenerateAlerts(
|
||||
const { activeEntities } = getEntitiesAndGenerateAlerts(
|
||||
prevLocationMapWithIdenticalEntityEntry,
|
||||
currLocationMap,
|
||||
alertFactory(contextKeys, testAlertActionArr),
|
||||
emptyShapesIdsNamesMap,
|
||||
currentDateTime
|
||||
);
|
||||
expect(allActiveEntriesMap).toEqual(currLocationMap);
|
||||
expect(activeEntities).toEqual(currLocationMap);
|
||||
expect(testAlertActionArr).toMatchObject(expectedAlertResults);
|
||||
});
|
||||
|
||||
|
@ -316,15 +316,15 @@ describe('geo_containment', () => {
|
|||
...expectedAlertResults,
|
||||
];
|
||||
|
||||
const allActiveEntriesMap = getActiveEntriesAndGenerateAlerts(
|
||||
const { activeEntities } = getEntitiesAndGenerateAlerts(
|
||||
prevLocationMapWithNonIdenticalEntityEntry,
|
||||
currLocationMap,
|
||||
alertFactory(contextKeys, testAlertActionArr),
|
||||
emptyShapesIdsNamesMap,
|
||||
currentDateTime
|
||||
);
|
||||
expect(allActiveEntriesMap).not.toEqual(currLocationMap);
|
||||
expect(allActiveEntriesMap.has('d')).toBeTruthy();
|
||||
expect(activeEntities).not.toEqual(currLocationMap);
|
||||
expect(activeEntities.has('d')).toBeTruthy();
|
||||
expect(testAlertActionArr).toMatchObject(expectedAlertResultsPlusD);
|
||||
});
|
||||
|
||||
|
@ -339,14 +339,29 @@ describe('geo_containment', () => {
|
|||
},
|
||||
]);
|
||||
expect(currLocationMapWithOther).not.toEqual(currLocationMap);
|
||||
const allActiveEntriesMap = getActiveEntriesAndGenerateAlerts(
|
||||
const { activeEntities, inactiveEntities } = getEntitiesAndGenerateAlerts(
|
||||
emptyPrevLocationMap,
|
||||
currLocationMapWithOther,
|
||||
alertFactory(contextKeys, testAlertActionArr),
|
||||
emptyShapesIdsNamesMap,
|
||||
currentDateTime
|
||||
);
|
||||
expect(allActiveEntriesMap).toEqual(currLocationMap);
|
||||
expect(activeEntities).toEqual(currLocationMap);
|
||||
expect(inactiveEntities).toEqual(
|
||||
new Map([
|
||||
[
|
||||
'd',
|
||||
[
|
||||
{
|
||||
location: [0, 0],
|
||||
shapeLocationId: 'other',
|
||||
dateInShape: 'Wed Dec 09 2020 14:31:31 GMT-0700 (Mountain Standard Time)',
|
||||
docId: 'docId1',
|
||||
},
|
||||
],
|
||||
],
|
||||
])
|
||||
);
|
||||
expect(testAlertActionArr).toMatchObject(expectedAlertResults);
|
||||
});
|
||||
|
||||
|
@ -372,7 +387,7 @@ describe('geo_containment', () => {
|
|||
docId: 'docId3',
|
||||
},
|
||||
]);
|
||||
getActiveEntriesAndGenerateAlerts(
|
||||
getEntitiesAndGenerateAlerts(
|
||||
emptyPrevLocationMap,
|
||||
currLocationMapWithThreeMore,
|
||||
alertFactory(contextKeys, testAlertActionArr),
|
||||
|
@ -409,14 +424,14 @@ describe('geo_containment', () => {
|
|||
},
|
||||
]);
|
||||
expect(currLocationMapWithOther).not.toEqual(currLocationMap);
|
||||
const allActiveEntriesMap = getActiveEntriesAndGenerateAlerts(
|
||||
const { activeEntities } = getEntitiesAndGenerateAlerts(
|
||||
emptyPrevLocationMap,
|
||||
currLocationMapWithOther,
|
||||
alertFactory(contextKeys, testAlertActionArr),
|
||||
emptyShapesIdsNamesMap,
|
||||
currentDateTime
|
||||
);
|
||||
expect(allActiveEntriesMap).toEqual(currLocationMap);
|
||||
expect(activeEntities).toEqual(currLocationMap);
|
||||
});
|
||||
|
||||
it('should return entity as active entry if "other" not the latest location but remove "other" and earlier entries', () => {
|
||||
|
@ -441,14 +456,14 @@ describe('geo_containment', () => {
|
|||
docId: 'docId1',
|
||||
},
|
||||
]);
|
||||
const allActiveEntriesMap = getActiveEntriesAndGenerateAlerts(
|
||||
const { activeEntities } = getEntitiesAndGenerateAlerts(
|
||||
emptyPrevLocationMap,
|
||||
currLocationMapWithOther,
|
||||
alertFactory(contextKeys, testAlertActionArr),
|
||||
emptyShapesIdsNamesMap,
|
||||
currentDateTime
|
||||
);
|
||||
expect(allActiveEntriesMap).toEqual(
|
||||
expect(activeEntities).toEqual(
|
||||
new Map([...currLocationMap]).set('d', [
|
||||
{
|
||||
location: [0, 0],
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { getContainedAlertContext, getRecoveredAlertContext } from '../get_context';
|
||||
import { OTHER_CATEGORY } from '../es_query_builder';
|
||||
|
||||
test('getContainedAlertContext', () => {
|
||||
expect(
|
||||
getContainedAlertContext({
|
||||
entityName: 'entity1',
|
||||
containment: {
|
||||
location: [100, 0],
|
||||
shapeLocationId: 'boundary1Id',
|
||||
dateInShape: '2022-06-21T16:56:11.923Z',
|
||||
docId: 'docId',
|
||||
},
|
||||
shapesIdsNamesMap: { boundary1Id: 'boundary1Name' },
|
||||
windowEnd: new Date('2022-06-21T17:00:00.000Z'),
|
||||
})
|
||||
).toEqual({
|
||||
containingBoundaryId: 'boundary1Id',
|
||||
containingBoundaryName: 'boundary1Name',
|
||||
detectionDateTime: '2022-06-21T17:00:00.000Z',
|
||||
entityDateTime: '2022-06-21T16:56:11.923Z',
|
||||
entityDocumentId: 'docId',
|
||||
entityId: 'entity1',
|
||||
entityLocation: 'POINT (100 0)',
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRecoveredAlertContext', () => {
|
||||
test('should set context from contained entity location when entity is contained by another boundary', () => {
|
||||
const activeEntities = new Map();
|
||||
activeEntities.set('entity1', [
|
||||
{
|
||||
location: [100, 0],
|
||||
shapeLocationId: 'boundary1Id',
|
||||
dateInShape: '2022-06-21T16:56:11.923Z',
|
||||
docId: 'docId',
|
||||
},
|
||||
]);
|
||||
expect(
|
||||
getRecoveredAlertContext({
|
||||
alertId: 'entity1-boundary1Name',
|
||||
activeEntities,
|
||||
inactiveEntities: new Map(),
|
||||
windowEnd: new Date('2022-06-21T17:00:00.000Z'),
|
||||
})
|
||||
).toEqual({
|
||||
detectionDateTime: '2022-06-21T17:00:00.000Z',
|
||||
entityDateTime: '2022-06-21T16:56:11.923Z',
|
||||
entityDocumentId: 'docId',
|
||||
entityId: 'entity1',
|
||||
entityLocation: 'POINT (100 0)',
|
||||
});
|
||||
});
|
||||
|
||||
test('should set context from uncontained entity location when entity is not contained by another boundary', () => {
|
||||
const inactiveEntities = new Map();
|
||||
inactiveEntities.set('entity1', [
|
||||
{
|
||||
location: [100, 0],
|
||||
shapeLocationId: OTHER_CATEGORY,
|
||||
dateInShape: '2022-06-21T16:56:11.923Z',
|
||||
docId: 'docId',
|
||||
},
|
||||
]);
|
||||
expect(
|
||||
getRecoveredAlertContext({
|
||||
alertId: 'entity1-boundary1Name',
|
||||
activeEntities: new Map(),
|
||||
inactiveEntities,
|
||||
windowEnd: new Date('2022-06-21T17:00:00.000Z'),
|
||||
})
|
||||
).toEqual({
|
||||
detectionDateTime: '2022-06-21T17:00:00.000Z',
|
||||
entityDateTime: '2022-06-21T16:56:11.923Z',
|
||||
entityDocumentId: 'docId',
|
||||
entityId: 'entity1',
|
||||
entityLocation: 'POINT (100 0)',
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue