[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:
Nathan Reese 2022-07-21 14:59:09 -06:00 committed by GitHub
parent 3c76959464
commit 91f15ff355
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 380 additions and 133 deletions

View file

@ -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,

View file

@ -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: {

View file

@ -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),
};
};

View file

@ -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;
}

View file

@ -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",
},
],

View file

@ -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],

View file

@ -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)',
});
});
});