mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Cases] Add telemetry to cases saved objects (#126254)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
865d0ed503
commit
2f07644a2c
32 changed files with 3104 additions and 35 deletions
|
@ -34,6 +34,7 @@ const previouslyRegisteredTypes = [
|
|||
'cases-connector-mappings',
|
||||
'cases-sub-case',
|
||||
'cases-user-actions',
|
||||
'cases-telemetry',
|
||||
'config',
|
||||
'connector_token',
|
||||
'core-usage-stats',
|
||||
|
|
|
@ -13,6 +13,7 @@ import { sortOrderSchema } from './common_schemas';
|
|||
* Schemas for the Bucket aggregations.
|
||||
*
|
||||
* Currently supported:
|
||||
* - date_range
|
||||
* - filter
|
||||
* - histogram
|
||||
* - nested
|
||||
|
@ -29,7 +30,6 @@ import { sortOrderSchema } from './common_schemas';
|
|||
* - children
|
||||
* - composite
|
||||
* - date_histogram
|
||||
* - date_range
|
||||
* - diversified_sampler
|
||||
* - geo_distance
|
||||
* - geohash_grid
|
||||
|
@ -64,6 +64,11 @@ const boolSchema = s.object({
|
|||
});
|
||||
|
||||
export const bucketAggsSchemas: Record<string, ObjectType> = {
|
||||
date_range: s.object({
|
||||
field: s.string(),
|
||||
format: s.string(),
|
||||
ranges: s.arrayOf(s.object({ from: s.maybe(s.string()), to: s.maybe(s.string()) })),
|
||||
}),
|
||||
filter: termSchema,
|
||||
filters: s.object({
|
||||
filters: s.recordOf(s.string(), s.oneOf([termSchema, boolSchema])),
|
||||
|
|
|
@ -8,8 +8,10 @@
|
|||
|
||||
import { bucketAggsSchemas } from './bucket_aggs';
|
||||
import { metricsAggsSchemas } from './metrics_aggs';
|
||||
import { pipelineAggsSchemas } from './pipeline_aggs';
|
||||
|
||||
export const aggregationSchemas = {
|
||||
...metricsAggsSchemas,
|
||||
...bucketAggsSchemas,
|
||||
...pipelineAggsSchemas,
|
||||
};
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { schema as s, ObjectType } from '@kbn/config-schema';
|
||||
|
||||
/**
|
||||
* Schemas for the Bucket aggregations.
|
||||
*
|
||||
* Currently supported:
|
||||
* - max_bucket
|
||||
*
|
||||
* Not implemented:
|
||||
* - avg_bucket
|
||||
* - bucket_script
|
||||
* - bucket_count_ks_test
|
||||
* - bucket_correlation
|
||||
* - bucket_selector
|
||||
* - bucket_sort
|
||||
* - cumulative_cardinality
|
||||
* - cumulative_sum
|
||||
* - derivative
|
||||
* - extended_stats_bucket
|
||||
* - inference
|
||||
* - min_bucket
|
||||
* - moving_fn
|
||||
* - moving_percentiles
|
||||
* - normalize
|
||||
* - percentiles_bucket
|
||||
* - serial_diff
|
||||
* - stats_bucket
|
||||
* - sum_bucket
|
||||
*/
|
||||
|
||||
export const pipelineAggsSchemas: Record<string, ObjectType> = {
|
||||
max_bucket: s.object({
|
||||
buckets_path: s.string(),
|
||||
gap: s.maybe(s.oneOf([s.literal('skip'), s.literal('insert_zeros'), s.literal('keep_values')])),
|
||||
format: s.maybe(s.string()),
|
||||
}),
|
||||
};
|
|
@ -121,3 +121,15 @@ export const DEFAULT_FEATURES: CasesFeaturesAllRequired = Object.freeze({
|
|||
alerts: { sync: true, enabled: true },
|
||||
metrics: [],
|
||||
});
|
||||
|
||||
/**
|
||||
* Task manager
|
||||
*/
|
||||
|
||||
export const CASES_TELEMETRY_TASK_NAME = 'cases-telemetry-task';
|
||||
|
||||
/**
|
||||
* Telemetry
|
||||
*/
|
||||
export const CASE_TELEMETRY_SAVED_OBJECT = 'cases-telemetry';
|
||||
export const CASE_TELEMETRY_SAVED_OBJECT_ID = 'cases-telemetry';
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
"home",
|
||||
"security",
|
||||
"spaces",
|
||||
"features",
|
||||
"taskManager",
|
||||
"usageCollection"
|
||||
],
|
||||
"owner":{
|
||||
|
|
|
@ -252,9 +252,6 @@ export const useGetCaseUserActions = (
|
|||
const response = await getCaseUserActions(thisCaseId, abortCtrlRef.current.signal);
|
||||
|
||||
if (!isCancelledRef.current) {
|
||||
// Attention Future developer
|
||||
// We are removing the first item because it will always be the creation of the case
|
||||
// and we do not want it to simplify our life
|
||||
const participants = !isEmpty(response)
|
||||
? uniqBy('createdBy.username', response).map((cau) => cau.createdBy)
|
||||
: [];
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
import { IContextProvider, KibanaRequest, Logger, PluginInitializerContext } from 'kibana/server';
|
||||
import { CoreSetup, CoreStart } from 'src/core/server';
|
||||
|
||||
import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server';
|
||||
import { SecurityPluginSetup, SecurityPluginStart } from '../../security/server';
|
||||
import {
|
||||
PluginSetupContract as ActionsPluginSetup,
|
||||
|
@ -22,6 +21,7 @@ import {
|
|||
caseConnectorMappingsSavedObjectType,
|
||||
createCaseSavedObjectType,
|
||||
caseUserActionSavedObjectType,
|
||||
casesTelemetrySavedObjectType,
|
||||
} from './saved_object_types';
|
||||
|
||||
import { CasesClient } from './client';
|
||||
|
@ -36,20 +36,25 @@ import { LensServerPluginSetup } from '../../lens/server';
|
|||
import { getCasesKibanaFeature } from './features';
|
||||
import { registerRoutes } from './routes/api/register_routes';
|
||||
import { getExternalRoutes } from './routes/api/get_external_routes';
|
||||
import { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_manager/server';
|
||||
import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server';
|
||||
import { createCasesTelemetry, scheduleCasesTelemetryTask } from './telemetry';
|
||||
|
||||
export interface PluginsSetup {
|
||||
actions: ActionsPluginSetup;
|
||||
lens: LensServerPluginSetup;
|
||||
features: FeaturesPluginSetup;
|
||||
usageCollection?: UsageCollectionSetup;
|
||||
security?: SecurityPluginSetup;
|
||||
taskManager?: TaskManagerSetupContract;
|
||||
usageCollection?: UsageCollectionSetup;
|
||||
}
|
||||
|
||||
export interface PluginsStart {
|
||||
security?: SecurityPluginStart;
|
||||
features: FeaturesPluginStart;
|
||||
spaces?: SpacesPluginStart;
|
||||
actions: ActionsPluginStart;
|
||||
features: FeaturesPluginStart;
|
||||
taskManager?: TaskManagerStartContract;
|
||||
security?: SecurityPluginStart;
|
||||
spaces?: SpacesPluginStart;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -66,7 +71,7 @@ export interface PluginStartContract {
|
|||
}
|
||||
|
||||
export class CasePlugin {
|
||||
private readonly log: Logger;
|
||||
private readonly logger: Logger;
|
||||
private readonly kibanaVersion: PluginInitializerContext['env']['packageInfo']['version'];
|
||||
private clientFactory: CasesClientFactory;
|
||||
private securityPluginSetup?: SecurityPluginSetup;
|
||||
|
@ -74,11 +79,17 @@ export class CasePlugin {
|
|||
|
||||
constructor(private readonly initializerContext: PluginInitializerContext) {
|
||||
this.kibanaVersion = initializerContext.env.packageInfo.version;
|
||||
this.log = this.initializerContext.logger.get();
|
||||
this.clientFactory = new CasesClientFactory(this.log);
|
||||
this.logger = this.initializerContext.logger.get();
|
||||
this.clientFactory = new CasesClientFactory(this.logger);
|
||||
}
|
||||
|
||||
public setup(core: CoreSetup, plugins: PluginsSetup) {
|
||||
this.logger.debug(
|
||||
`Setting up Case Workflow with core contract [${Object.keys(
|
||||
core
|
||||
)}] and plugins [${Object.keys(plugins)}]`
|
||||
);
|
||||
|
||||
this.securityPluginSetup = plugins.security;
|
||||
this.lensEmbeddableFactory = plugins.lens.lensEmbeddableFactory;
|
||||
|
||||
|
@ -93,14 +104,9 @@ export class CasePlugin {
|
|||
);
|
||||
core.savedObjects.registerType(caseConfigureSavedObjectType);
|
||||
core.savedObjects.registerType(caseConnectorMappingsSavedObjectType);
|
||||
core.savedObjects.registerType(createCaseSavedObjectType(core, this.log));
|
||||
core.savedObjects.registerType(createCaseSavedObjectType(core, this.logger));
|
||||
core.savedObjects.registerType(caseUserActionSavedObjectType);
|
||||
|
||||
this.log.debug(
|
||||
`Setting up Case Workflow with core contract [${Object.keys(
|
||||
core
|
||||
)}] and plugins [${Object.keys(plugins)}]`
|
||||
);
|
||||
core.savedObjects.registerType(casesTelemetrySavedObjectType);
|
||||
|
||||
core.http.registerRouteHandlerContext<CasesRequestHandlerContext, 'cases'>(
|
||||
APP_ID,
|
||||
|
@ -109,20 +115,34 @@ export class CasePlugin {
|
|||
})
|
||||
);
|
||||
|
||||
if (plugins.taskManager && plugins.usageCollection) {
|
||||
createCasesTelemetry({
|
||||
core,
|
||||
taskManager: plugins.taskManager,
|
||||
usageCollection: plugins.usageCollection,
|
||||
logger: this.logger,
|
||||
kibanaVersion: this.kibanaVersion,
|
||||
});
|
||||
}
|
||||
|
||||
const router = core.http.createRouter<CasesRequestHandlerContext>();
|
||||
const telemetryUsageCounter = plugins.usageCollection?.createUsageCounter(APP_ID);
|
||||
|
||||
registerRoutes({
|
||||
router,
|
||||
routes: getExternalRoutes(),
|
||||
logger: this.log,
|
||||
logger: this.logger,
|
||||
kibanaVersion: this.kibanaVersion,
|
||||
telemetryUsageCounter,
|
||||
});
|
||||
}
|
||||
|
||||
public start(core: CoreStart, plugins: PluginsStart): PluginStartContract {
|
||||
this.log.debug(`Starting Case Workflow`);
|
||||
this.logger.debug(`Starting Case Workflow`);
|
||||
|
||||
if (plugins.taskManager) {
|
||||
scheduleCasesTelemetryTask(plugins.taskManager, this.logger);
|
||||
}
|
||||
|
||||
this.clientFactory.initialize({
|
||||
securityPluginSetup: this.securityPluginSetup,
|
||||
|
@ -156,7 +176,7 @@ export class CasePlugin {
|
|||
}
|
||||
|
||||
public stop() {
|
||||
this.log.debug(`Stopping Case Workflow`);
|
||||
this.logger.debug(`Stopping Case Workflow`);
|
||||
}
|
||||
|
||||
private createRouteHandlerContext = ({
|
||||
|
|
|
@ -10,3 +10,4 @@ export { caseConfigureSavedObjectType } from './configure';
|
|||
export { createCaseCommentSavedObjectType } from './comments';
|
||||
export { caseUserActionSavedObjectType } from './user_actions';
|
||||
export { caseConnectorMappingsSavedObjectType } from './connector_mappings';
|
||||
export { casesTelemetrySavedObjectType } from './telemetry';
|
||||
|
|
19
x-pack/plugins/cases/server/saved_object_types/telemetry.ts
Normal file
19
x-pack/plugins/cases/server/saved_object_types/telemetry.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* 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 { SavedObjectsType } from 'src/core/server';
|
||||
import { CASE_TELEMETRY_SAVED_OBJECT } from '../../common/constants';
|
||||
|
||||
export const casesTelemetrySavedObjectType: SavedObjectsType = {
|
||||
name: CASE_TELEMETRY_SAVED_OBJECT,
|
||||
hidden: false,
|
||||
namespaceType: 'agnostic',
|
||||
mappings: {
|
||||
dynamic: false,
|
||||
properties: {},
|
||||
},
|
||||
};
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* 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 { getAlertsTelemetryData } from './queries/alerts';
|
||||
import { getCasesTelemetryData } from './queries/cases';
|
||||
import { getUserCommentsTelemetryData } from './queries/comments';
|
||||
import { getConfigurationTelemetryData } from './queries/configuration';
|
||||
import { getConnectorsTelemetryData } from './queries/connectors';
|
||||
import { getPushedTelemetryData } from './queries/pushes';
|
||||
import { getUserActionsTelemetryData } from './queries/user_actions';
|
||||
import { CasesTelemetry, CollectTelemetryDataParams } from './types';
|
||||
|
||||
export const collectTelemetryData = async ({
|
||||
savedObjectsClient,
|
||||
logger,
|
||||
}: CollectTelemetryDataParams): Promise<Partial<CasesTelemetry>> => {
|
||||
try {
|
||||
const [cases, userActions, comments, alerts, connectors, pushes, configuration] =
|
||||
await Promise.all([
|
||||
getCasesTelemetryData({ savedObjectsClient, logger }),
|
||||
getUserActionsTelemetryData({ savedObjectsClient, logger }),
|
||||
getUserCommentsTelemetryData({ savedObjectsClient, logger }),
|
||||
getAlertsTelemetryData({ savedObjectsClient, logger }),
|
||||
getConnectorsTelemetryData({ savedObjectsClient, logger }),
|
||||
getPushedTelemetryData({ savedObjectsClient, logger }),
|
||||
getConfigurationTelemetryData({ savedObjectsClient, logger }),
|
||||
]);
|
||||
|
||||
return {
|
||||
cases,
|
||||
userActions,
|
||||
comments,
|
||||
alerts,
|
||||
connectors,
|
||||
pushes,
|
||||
configuration,
|
||||
};
|
||||
} catch (err) {
|
||||
logger.debug('Failed collecting Cases telemetry data');
|
||||
logger.debug(err);
|
||||
/**
|
||||
* Return an empty object instead of an empty state to distinguish between
|
||||
* clusters that they do not use cases thus all counts will be zero
|
||||
* and clusters where an error occurred.
|
||||
* */
|
||||
|
||||
return {};
|
||||
}
|
||||
};
|
96
x-pack/plugins/cases/server/telemetry/index.ts
Normal file
96
x-pack/plugins/cases/server/telemetry/index.ts
Normal file
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
* 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 {
|
||||
CoreSetup,
|
||||
ISavedObjectsRepository,
|
||||
Logger,
|
||||
PluginInitializerContext,
|
||||
SavedObjectsErrorHelpers,
|
||||
} from '../../../../../src/core/server';
|
||||
import { TaskManagerSetupContract } from '../../../task_manager/server';
|
||||
import { UsageCollectionSetup } from '../../../../../src/plugins/usage_collection/server';
|
||||
import { collectTelemetryData } from './collect_telemetry_data';
|
||||
import {
|
||||
CASE_TELEMETRY_SAVED_OBJECT,
|
||||
CASES_TELEMETRY_TASK_NAME,
|
||||
CASE_TELEMETRY_SAVED_OBJECT_ID,
|
||||
SAVED_OBJECT_TYPES,
|
||||
} from '../../common/constants';
|
||||
import { CasesTelemetry } from './types';
|
||||
import { casesSchema } from './schema';
|
||||
|
||||
export { scheduleCasesTelemetryTask } from './schedule_telemetry_task';
|
||||
|
||||
interface CreateCasesTelemetryArgs {
|
||||
core: CoreSetup;
|
||||
taskManager: TaskManagerSetupContract;
|
||||
usageCollection: UsageCollectionSetup;
|
||||
logger: Logger;
|
||||
kibanaVersion: PluginInitializerContext['env']['packageInfo']['version'];
|
||||
}
|
||||
|
||||
export const createCasesTelemetry = async ({
|
||||
core,
|
||||
taskManager,
|
||||
usageCollection,
|
||||
logger,
|
||||
}: CreateCasesTelemetryArgs) => {
|
||||
const getInternalSavedObjectClient = async (): Promise<ISavedObjectsRepository> => {
|
||||
const [coreStart] = await core.getStartServices();
|
||||
return coreStart.savedObjects.createInternalRepository(SAVED_OBJECT_TYPES);
|
||||
};
|
||||
|
||||
taskManager.registerTaskDefinitions({
|
||||
[CASES_TELEMETRY_TASK_NAME]: {
|
||||
title: 'Collect Cases telemetry data',
|
||||
createTaskRunner: () => {
|
||||
return {
|
||||
run: async () => {
|
||||
await collectAndStore();
|
||||
},
|
||||
cancel: async () => {},
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const collectAndStore = async () => {
|
||||
const savedObjectsClient = await getInternalSavedObjectClient();
|
||||
const telemetryData = await collectTelemetryData({ savedObjectsClient, logger });
|
||||
|
||||
await savedObjectsClient.create(CASE_TELEMETRY_SAVED_OBJECT, telemetryData, {
|
||||
id: CASE_TELEMETRY_SAVED_OBJECT_ID,
|
||||
overwrite: true,
|
||||
});
|
||||
};
|
||||
|
||||
const collector = usageCollection.makeUsageCollector<CasesTelemetry | {}>({
|
||||
type: 'cases',
|
||||
schema: casesSchema,
|
||||
isReady: () => true,
|
||||
fetch: async () => {
|
||||
try {
|
||||
const savedObjectsClient = await getInternalSavedObjectClient();
|
||||
const data = (
|
||||
await savedObjectsClient.get(CASE_TELEMETRY_SAVED_OBJECT, CASE_TELEMETRY_SAVED_OBJECT_ID)
|
||||
).attributes as CasesTelemetry;
|
||||
|
||||
return data;
|
||||
} catch (err) {
|
||||
if (SavedObjectsErrorHelpers.isNotFoundError(err)) {
|
||||
// task has not run yet, so no saved object to return
|
||||
return {};
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
usageCollection.registerCollector(collector);
|
||||
};
|
128
x-pack/plugins/cases/server/telemetry/queries/alerts.test.ts
Normal file
128
x-pack/plugins/cases/server/telemetry/queries/alerts.test.ts
Normal file
|
@ -0,0 +1,128 @@
|
|||
/*
|
||||
* 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 {
|
||||
loggingSystemMock,
|
||||
savedObjectsRepositoryMock,
|
||||
} from '../../../../../../src/core/server/mocks';
|
||||
import { getAlertsTelemetryData } from './alerts';
|
||||
|
||||
describe('alerts', () => {
|
||||
const logger = loggingSystemMock.createLogger();
|
||||
|
||||
describe('getAlertsTelemetryData', () => {
|
||||
const savedObjectsClient = savedObjectsRepositoryMock.create();
|
||||
savedObjectsClient.find.mockResolvedValue({
|
||||
total: 5,
|
||||
saved_objects: [],
|
||||
per_page: 1,
|
||||
page: 1,
|
||||
aggregations: {
|
||||
counts: {
|
||||
buckets: [
|
||||
{ doc_count: 1, key: 1 },
|
||||
{ doc_count: 2, key: 2 },
|
||||
{ doc_count: 3, key: 3 },
|
||||
],
|
||||
},
|
||||
references: { cases: { max: { value: 1 } } },
|
||||
},
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('it returns the correct res', async () => {
|
||||
const res = await getAlertsTelemetryData({ savedObjectsClient, logger });
|
||||
expect(res).toEqual({
|
||||
all: {
|
||||
total: 5,
|
||||
daily: 3,
|
||||
weekly: 2,
|
||||
monthly: 1,
|
||||
maxOnACase: 1,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should call find with correct arguments', async () => {
|
||||
await getAlertsTelemetryData({ savedObjectsClient, logger });
|
||||
expect(savedObjectsClient.find).toBeCalledWith({
|
||||
aggs: {
|
||||
counts: {
|
||||
date_range: {
|
||||
field: 'cases-comments.attributes.created_at',
|
||||
format: 'dd/MM/YYYY',
|
||||
ranges: [
|
||||
{
|
||||
from: 'now-1d',
|
||||
to: 'now',
|
||||
},
|
||||
{
|
||||
from: 'now-1w',
|
||||
to: 'now',
|
||||
},
|
||||
{
|
||||
from: 'now-1M',
|
||||
to: 'now',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
references: {
|
||||
aggregations: {
|
||||
cases: {
|
||||
aggregations: {
|
||||
ids: {
|
||||
terms: {
|
||||
field: 'cases-comments.references.id',
|
||||
},
|
||||
},
|
||||
max: {
|
||||
max_bucket: {
|
||||
buckets_path: 'ids._count',
|
||||
},
|
||||
},
|
||||
},
|
||||
filter: {
|
||||
term: {
|
||||
'cases-comments.references.type': 'cases',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
nested: {
|
||||
path: 'cases-comments.references',
|
||||
},
|
||||
},
|
||||
},
|
||||
filter: {
|
||||
arguments: [
|
||||
{
|
||||
type: 'literal',
|
||||
value: 'cases-comments.attributes.type',
|
||||
},
|
||||
{
|
||||
type: 'literal',
|
||||
value: 'alert',
|
||||
},
|
||||
{
|
||||
type: 'literal',
|
||||
value: false,
|
||||
},
|
||||
],
|
||||
function: 'is',
|
||||
type: 'function',
|
||||
},
|
||||
page: 0,
|
||||
perPage: 0,
|
||||
type: 'cases-comments',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
22
x-pack/plugins/cases/server/telemetry/queries/alerts.ts
Normal file
22
x-pack/plugins/cases/server/telemetry/queries/alerts.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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 { CASE_COMMENT_SAVED_OBJECT } from '../../../common/constants';
|
||||
import { CasesTelemetry, CollectTelemetryDataParams } from '../types';
|
||||
import { getCountsAndMaxData, getOnlyAlertsCommentsFilter } from './utils';
|
||||
|
||||
export const getAlertsTelemetryData = async ({
|
||||
savedObjectsClient,
|
||||
}: CollectTelemetryDataParams): Promise<CasesTelemetry['comments']> => {
|
||||
const res = await getCountsAndMaxData({
|
||||
savedObjectsClient,
|
||||
savedObjectType: CASE_COMMENT_SAVED_OBJECT,
|
||||
filter: getOnlyAlertsCommentsFilter(),
|
||||
});
|
||||
|
||||
return res;
|
||||
};
|
432
x-pack/plugins/cases/server/telemetry/queries/cases.test.ts
Normal file
432
x-pack/plugins/cases/server/telemetry/queries/cases.test.ts
Normal file
|
@ -0,0 +1,432 @@
|
|||
/*
|
||||
* 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 { SavedObjectsFindResponse } from 'kibana/server';
|
||||
import {
|
||||
savedObjectsRepositoryMock,
|
||||
loggingSystemMock,
|
||||
} from '../../../../../../src/core/server/mocks';
|
||||
import { getCasesTelemetryData } from './cases';
|
||||
|
||||
describe('getCasesTelemetryData', () => {
|
||||
describe('getCasesTelemetryData', () => {
|
||||
const logger = loggingSystemMock.createLogger();
|
||||
const savedObjectsClient = savedObjectsRepositoryMock.create();
|
||||
|
||||
const mockFind = (
|
||||
aggs: Record<string, unknown> = {},
|
||||
so: SavedObjectsFindResponse['saved_objects'] = []
|
||||
) => {
|
||||
savedObjectsClient.find.mockResolvedValueOnce({
|
||||
total: 5,
|
||||
saved_objects: so,
|
||||
per_page: 1,
|
||||
page: 1,
|
||||
aggregations: {
|
||||
...aggs,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const mockSavedObjectResponse = (attributes: Record<string, unknown>) => {
|
||||
mockFind({}, [
|
||||
{
|
||||
attributes: { ...attributes },
|
||||
score: 1,
|
||||
id: 'test',
|
||||
references: [],
|
||||
type: 'cases',
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
const mockResponse = () => {
|
||||
const counts = {
|
||||
buckets: [
|
||||
{ doc_count: 1, key: 1 },
|
||||
{ doc_count: 2, key: 2 },
|
||||
{ doc_count: 3, key: 3 },
|
||||
],
|
||||
};
|
||||
|
||||
mockFind({
|
||||
users: { value: 1 },
|
||||
tags: { value: 2 },
|
||||
counts,
|
||||
securitySolution: { counts },
|
||||
observability: { counts },
|
||||
cases: { counts },
|
||||
syncAlerts: {
|
||||
buckets: [
|
||||
{
|
||||
key: 0,
|
||||
doc_count: 1,
|
||||
},
|
||||
{
|
||||
key: 1,
|
||||
doc_count: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
status: {
|
||||
buckets: [
|
||||
{
|
||||
key: 'open',
|
||||
doc_count: 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
totalsByOwner: {
|
||||
buckets: [
|
||||
{
|
||||
key: 'observability',
|
||||
doc_count: 1,
|
||||
},
|
||||
{
|
||||
key: 'securitySolution',
|
||||
doc_count: 1,
|
||||
},
|
||||
{
|
||||
key: 'cases',
|
||||
doc_count: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
mockFind({ participants: { value: 2 } });
|
||||
mockFind({ references: { referenceType: { referenceAgg: { value: 3 } } } });
|
||||
mockFind({ references: { referenceType: { referenceAgg: { value: 4 } } } });
|
||||
mockSavedObjectResponse({
|
||||
created_at: '2022-03-08T12:24:11.429Z',
|
||||
});
|
||||
mockSavedObjectResponse({
|
||||
updated_at: '2022-03-08T12:24:11.429Z',
|
||||
});
|
||||
mockSavedObjectResponse({
|
||||
closed_at: '2022-03-08T12:24:11.429Z',
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('it returns the correct res', async () => {
|
||||
mockResponse();
|
||||
|
||||
const res = await getCasesTelemetryData({ savedObjectsClient, logger });
|
||||
expect(res).toEqual({
|
||||
all: {
|
||||
total: 5,
|
||||
daily: 3,
|
||||
weekly: 2,
|
||||
monthly: 1,
|
||||
latestDates: {
|
||||
closedAt: '2022-03-08T12:24:11.429Z',
|
||||
createdAt: '2022-03-08T12:24:11.429Z',
|
||||
updatedAt: '2022-03-08T12:24:11.429Z',
|
||||
},
|
||||
status: {
|
||||
closed: 0,
|
||||
inProgress: 0,
|
||||
open: 2,
|
||||
},
|
||||
syncAlertsOff: 1,
|
||||
syncAlertsOn: 1,
|
||||
totalParticipants: 2,
|
||||
totalTags: 2,
|
||||
totalUsers: 1,
|
||||
totalWithAlerts: 3,
|
||||
totalWithConnectors: 4,
|
||||
},
|
||||
main: {
|
||||
total: 1,
|
||||
daily: 3,
|
||||
weekly: 2,
|
||||
monthly: 1,
|
||||
},
|
||||
obs: {
|
||||
total: 1,
|
||||
daily: 3,
|
||||
weekly: 2,
|
||||
monthly: 1,
|
||||
},
|
||||
sec: {
|
||||
total: 1,
|
||||
daily: 3,
|
||||
weekly: 2,
|
||||
monthly: 1,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should call find with correct arguments', async () => {
|
||||
mockResponse();
|
||||
|
||||
await getCasesTelemetryData({ savedObjectsClient, logger });
|
||||
|
||||
expect(savedObjectsClient.find.mock.calls[0][0]).toEqual({
|
||||
aggs: {
|
||||
cases: {
|
||||
aggs: {
|
||||
counts: {
|
||||
date_range: {
|
||||
field: 'cases.attributes.created_at',
|
||||
format: 'dd/MM/YYYY',
|
||||
ranges: [
|
||||
{
|
||||
from: 'now-1d',
|
||||
to: 'now',
|
||||
},
|
||||
{
|
||||
from: 'now-1w',
|
||||
to: 'now',
|
||||
},
|
||||
{
|
||||
from: 'now-1M',
|
||||
to: 'now',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
filter: {
|
||||
term: {
|
||||
'cases.attributes.owner': 'cases',
|
||||
},
|
||||
},
|
||||
},
|
||||
counts: {
|
||||
date_range: {
|
||||
field: 'cases.attributes.created_at',
|
||||
format: 'dd/MM/YYYY',
|
||||
ranges: [
|
||||
{
|
||||
from: 'now-1d',
|
||||
to: 'now',
|
||||
},
|
||||
{
|
||||
from: 'now-1w',
|
||||
to: 'now',
|
||||
},
|
||||
{
|
||||
from: 'now-1M',
|
||||
to: 'now',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
observability: {
|
||||
aggs: {
|
||||
counts: {
|
||||
date_range: {
|
||||
field: 'cases.attributes.created_at',
|
||||
format: 'dd/MM/YYYY',
|
||||
ranges: [
|
||||
{
|
||||
from: 'now-1d',
|
||||
to: 'now',
|
||||
},
|
||||
{
|
||||
from: 'now-1w',
|
||||
to: 'now',
|
||||
},
|
||||
{
|
||||
from: 'now-1M',
|
||||
to: 'now',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
filter: {
|
||||
term: {
|
||||
'cases.attributes.owner': 'observability',
|
||||
},
|
||||
},
|
||||
},
|
||||
securitySolution: {
|
||||
aggs: {
|
||||
counts: {
|
||||
date_range: {
|
||||
field: 'cases.attributes.created_at',
|
||||
format: 'dd/MM/YYYY',
|
||||
ranges: [
|
||||
{
|
||||
from: 'now-1d',
|
||||
to: 'now',
|
||||
},
|
||||
{
|
||||
from: 'now-1w',
|
||||
to: 'now',
|
||||
},
|
||||
{
|
||||
from: 'now-1M',
|
||||
to: 'now',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
filter: {
|
||||
term: {
|
||||
'cases.attributes.owner': 'securitySolution',
|
||||
},
|
||||
},
|
||||
},
|
||||
status: {
|
||||
terms: {
|
||||
field: 'cases.attributes.status',
|
||||
},
|
||||
},
|
||||
syncAlerts: {
|
||||
terms: {
|
||||
field: 'cases.attributes.settings.syncAlerts',
|
||||
},
|
||||
},
|
||||
tags: {
|
||||
cardinality: {
|
||||
field: 'cases.attributes.tags',
|
||||
},
|
||||
},
|
||||
totalsByOwner: {
|
||||
terms: {
|
||||
field: 'cases.attributes.owner',
|
||||
},
|
||||
},
|
||||
users: {
|
||||
cardinality: {
|
||||
field: 'cases.attributes.created_by.username',
|
||||
},
|
||||
},
|
||||
},
|
||||
page: 0,
|
||||
perPage: 0,
|
||||
type: 'cases',
|
||||
});
|
||||
|
||||
expect(savedObjectsClient.find.mock.calls[1][0]).toEqual({
|
||||
aggs: {
|
||||
participants: {
|
||||
cardinality: {
|
||||
field: 'cases-comments.attributes.created_by.username',
|
||||
},
|
||||
},
|
||||
},
|
||||
page: 0,
|
||||
perPage: 0,
|
||||
type: 'cases-comments',
|
||||
});
|
||||
|
||||
expect(savedObjectsClient.find.mock.calls[2][0]).toEqual({
|
||||
aggs: {
|
||||
references: {
|
||||
aggregations: {
|
||||
referenceType: {
|
||||
aggregations: {
|
||||
referenceAgg: {
|
||||
cardinality: {
|
||||
field: 'cases-comments.references.id',
|
||||
},
|
||||
},
|
||||
},
|
||||
filter: {
|
||||
term: {
|
||||
'cases-comments.references.type': 'cases',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
nested: {
|
||||
path: 'cases-comments.references',
|
||||
},
|
||||
},
|
||||
},
|
||||
filter: {
|
||||
arguments: [
|
||||
{
|
||||
type: 'literal',
|
||||
value: 'cases-comments.attributes.type',
|
||||
},
|
||||
{
|
||||
type: 'literal',
|
||||
value: 'alert',
|
||||
},
|
||||
{
|
||||
type: 'literal',
|
||||
value: false,
|
||||
},
|
||||
],
|
||||
function: 'is',
|
||||
type: 'function',
|
||||
},
|
||||
page: 0,
|
||||
perPage: 0,
|
||||
type: 'cases-comments',
|
||||
});
|
||||
|
||||
expect(savedObjectsClient.find.mock.calls[3][0]).toEqual({
|
||||
aggs: {
|
||||
references: {
|
||||
aggregations: {
|
||||
referenceType: {
|
||||
aggregations: {
|
||||
referenceAgg: {
|
||||
cardinality: {
|
||||
field: 'cases-user-actions.references.id',
|
||||
},
|
||||
},
|
||||
},
|
||||
filter: {
|
||||
term: {
|
||||
'cases-user-actions.references.type': 'cases',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
nested: {
|
||||
path: 'cases-user-actions.references',
|
||||
},
|
||||
},
|
||||
},
|
||||
filter: {
|
||||
arguments: [
|
||||
{
|
||||
type: 'literal',
|
||||
value: 'cases-user-actions.attributes.type',
|
||||
},
|
||||
{
|
||||
type: 'literal',
|
||||
value: 'connector',
|
||||
},
|
||||
{
|
||||
type: 'literal',
|
||||
value: false,
|
||||
},
|
||||
],
|
||||
function: 'is',
|
||||
type: 'function',
|
||||
},
|
||||
page: 0,
|
||||
perPage: 0,
|
||||
type: 'cases-user-actions',
|
||||
});
|
||||
|
||||
for (const [index, sortField] of ['created_at', 'updated_at', 'closed_at'].entries()) {
|
||||
const callIndex = index + 4;
|
||||
|
||||
expect(savedObjectsClient.find.mock.calls[callIndex][0]).toEqual({
|
||||
page: 1,
|
||||
perPage: 1,
|
||||
sortField,
|
||||
sortOrder: 'desc',
|
||||
type: 'cases',
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
212
x-pack/plugins/cases/server/telemetry/queries/cases.ts
Normal file
212
x-pack/plugins/cases/server/telemetry/queries/cases.ts
Normal file
|
@ -0,0 +1,212 @@
|
|||
/*
|
||||
* 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 {
|
||||
CASE_COMMENT_SAVED_OBJECT,
|
||||
CASE_SAVED_OBJECT,
|
||||
CASE_USER_ACTION_SAVED_OBJECT,
|
||||
} from '../../../common/constants';
|
||||
import { ESCaseAttributes } from '../../services/cases/types';
|
||||
import {
|
||||
CollectTelemetryDataParams,
|
||||
Buckets,
|
||||
CasesTelemetry,
|
||||
Cardinality,
|
||||
ReferencesAggregation,
|
||||
LatestDates,
|
||||
} from '../types';
|
||||
import {
|
||||
findValueInBuckets,
|
||||
getAggregationsBuckets,
|
||||
getCountsAggregationQuery,
|
||||
getCountsFromBuckets,
|
||||
getOnlyAlertsCommentsFilter,
|
||||
getOnlyConnectorsFilter,
|
||||
getReferencesAggregationQuery,
|
||||
} from './utils';
|
||||
|
||||
export const getLatestCasesDates = async ({
|
||||
savedObjectsClient,
|
||||
}: CollectTelemetryDataParams): Promise<LatestDates> => {
|
||||
const find = async (sortField: string) =>
|
||||
savedObjectsClient.find<ESCaseAttributes>({
|
||||
page: 1,
|
||||
perPage: 1,
|
||||
sortField,
|
||||
sortOrder: 'desc',
|
||||
type: CASE_SAVED_OBJECT,
|
||||
});
|
||||
|
||||
const savedObjects = await Promise.all([
|
||||
find('created_at'),
|
||||
find('updated_at'),
|
||||
find('closed_at'),
|
||||
]);
|
||||
|
||||
return {
|
||||
createdAt: savedObjects?.[0].saved_objects?.[0].attributes?.created_at ?? null,
|
||||
updatedAt: savedObjects?.[1].saved_objects?.[0].attributes?.updated_at ?? null,
|
||||
closedAt: savedObjects?.[2].saved_objects?.[0].attributes?.closed_at ?? null,
|
||||
};
|
||||
};
|
||||
|
||||
export const getCasesTelemetryData = async ({
|
||||
savedObjectsClient,
|
||||
logger,
|
||||
}: CollectTelemetryDataParams): Promise<CasesTelemetry['cases']> => {
|
||||
const owners = ['observability', 'securitySolution', 'cases'] as const;
|
||||
const byOwnerAggregationQuery = owners.reduce(
|
||||
(aggQuery, owner) => ({
|
||||
...aggQuery,
|
||||
[owner]: {
|
||||
filter: {
|
||||
term: {
|
||||
[`${CASE_SAVED_OBJECT}.attributes.owner`]: owner,
|
||||
},
|
||||
},
|
||||
aggs: getCountsAggregationQuery(CASE_SAVED_OBJECT),
|
||||
},
|
||||
}),
|
||||
{}
|
||||
);
|
||||
|
||||
const casesRes = await savedObjectsClient.find<
|
||||
unknown,
|
||||
Record<typeof owners[number], { counts: Buckets }> & {
|
||||
counts: Buckets;
|
||||
syncAlerts: Buckets;
|
||||
status: Buckets;
|
||||
users: Cardinality;
|
||||
tags: Cardinality;
|
||||
}
|
||||
>({
|
||||
page: 0,
|
||||
perPage: 0,
|
||||
type: CASE_SAVED_OBJECT,
|
||||
aggs: {
|
||||
...byOwnerAggregationQuery,
|
||||
...getCountsAggregationQuery(CASE_SAVED_OBJECT),
|
||||
totalsByOwner: {
|
||||
terms: { field: `${CASE_SAVED_OBJECT}.attributes.owner` },
|
||||
},
|
||||
syncAlerts: {
|
||||
terms: { field: `${CASE_SAVED_OBJECT}.attributes.settings.syncAlerts` },
|
||||
},
|
||||
status: {
|
||||
terms: {
|
||||
field: `${CASE_SAVED_OBJECT}.attributes.status`,
|
||||
},
|
||||
},
|
||||
users: {
|
||||
cardinality: {
|
||||
field: `${CASE_SAVED_OBJECT}.attributes.created_by.username`,
|
||||
},
|
||||
},
|
||||
tags: {
|
||||
cardinality: {
|
||||
field: `${CASE_SAVED_OBJECT}.attributes.tags`,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const commentsRes = await savedObjectsClient.find<
|
||||
unknown,
|
||||
Record<typeof owners[number], { counts: Buckets }> & {
|
||||
participants: Cardinality;
|
||||
} & ReferencesAggregation
|
||||
>({
|
||||
page: 0,
|
||||
perPage: 0,
|
||||
type: CASE_COMMENT_SAVED_OBJECT,
|
||||
aggs: {
|
||||
participants: {
|
||||
cardinality: {
|
||||
field: `${CASE_COMMENT_SAVED_OBJECT}.attributes.created_by.username`,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const totalAlertsRes = await savedObjectsClient.find<unknown, ReferencesAggregation>({
|
||||
page: 0,
|
||||
perPage: 0,
|
||||
type: CASE_COMMENT_SAVED_OBJECT,
|
||||
filter: getOnlyAlertsCommentsFilter(),
|
||||
aggs: {
|
||||
...getReferencesAggregationQuery({
|
||||
savedObjectType: CASE_COMMENT_SAVED_OBJECT,
|
||||
referenceType: 'cases',
|
||||
agg: 'cardinality',
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const totalConnectorsRes = await savedObjectsClient.find<unknown, ReferencesAggregation>({
|
||||
page: 0,
|
||||
perPage: 0,
|
||||
type: CASE_USER_ACTION_SAVED_OBJECT,
|
||||
filter: getOnlyConnectorsFilter(),
|
||||
aggs: {
|
||||
...getReferencesAggregationQuery({
|
||||
savedObjectType: CASE_USER_ACTION_SAVED_OBJECT,
|
||||
referenceType: 'cases',
|
||||
agg: 'cardinality',
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const latestDates = await getLatestCasesDates({ savedObjectsClient, logger });
|
||||
|
||||
const aggregationsBuckets = getAggregationsBuckets({
|
||||
aggs: casesRes.aggregations,
|
||||
keys: [
|
||||
'counts',
|
||||
'observability.counts',
|
||||
'securitySolution.counts',
|
||||
'cases.counts',
|
||||
'syncAlerts',
|
||||
'status',
|
||||
'totalsByOwner',
|
||||
'users',
|
||||
],
|
||||
});
|
||||
|
||||
return {
|
||||
all: {
|
||||
total: casesRes.total,
|
||||
...getCountsFromBuckets(aggregationsBuckets.counts),
|
||||
status: {
|
||||
open: findValueInBuckets(aggregationsBuckets.status, 'open'),
|
||||
inProgress: findValueInBuckets(aggregationsBuckets.status, 'in-progress'),
|
||||
closed: findValueInBuckets(aggregationsBuckets.status, 'closed'),
|
||||
},
|
||||
syncAlertsOn: findValueInBuckets(aggregationsBuckets.syncAlerts, 1),
|
||||
syncAlertsOff: findValueInBuckets(aggregationsBuckets.syncAlerts, 0),
|
||||
totalUsers: casesRes.aggregations?.users?.value ?? 0,
|
||||
totalParticipants: commentsRes.aggregations?.participants?.value ?? 0,
|
||||
totalTags: casesRes.aggregations?.tags?.value ?? 0,
|
||||
totalWithAlerts:
|
||||
totalAlertsRes.aggregations?.references?.referenceType?.referenceAgg?.value ?? 0,
|
||||
totalWithConnectors:
|
||||
totalConnectorsRes.aggregations?.references?.referenceType?.referenceAgg?.value ?? 0,
|
||||
latestDates,
|
||||
},
|
||||
sec: {
|
||||
total: findValueInBuckets(aggregationsBuckets.totalsByOwner, 'securitySolution'),
|
||||
...getCountsFromBuckets(aggregationsBuckets['securitySolution.counts']),
|
||||
},
|
||||
obs: {
|
||||
total: findValueInBuckets(aggregationsBuckets.totalsByOwner, 'observability'),
|
||||
...getCountsFromBuckets(aggregationsBuckets['observability.counts']),
|
||||
},
|
||||
main: {
|
||||
total: findValueInBuckets(aggregationsBuckets.totalsByOwner, 'cases'),
|
||||
...getCountsFromBuckets(aggregationsBuckets['cases.counts']),
|
||||
},
|
||||
};
|
||||
};
|
127
x-pack/plugins/cases/server/telemetry/queries/comments.test.ts
Normal file
127
x-pack/plugins/cases/server/telemetry/queries/comments.test.ts
Normal file
|
@ -0,0 +1,127 @@
|
|||
/*
|
||||
* 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 {
|
||||
loggingSystemMock,
|
||||
savedObjectsRepositoryMock,
|
||||
} from '../../../../../../src/core/server/mocks';
|
||||
import { getUserCommentsTelemetryData } from './comments';
|
||||
|
||||
describe('comments', () => {
|
||||
describe('getUserCommentsTelemetryData', () => {
|
||||
const logger = loggingSystemMock.createLogger();
|
||||
const savedObjectsClient = savedObjectsRepositoryMock.create();
|
||||
savedObjectsClient.find.mockResolvedValue({
|
||||
total: 5,
|
||||
saved_objects: [],
|
||||
per_page: 1,
|
||||
page: 1,
|
||||
aggregations: {
|
||||
counts: {
|
||||
buckets: [
|
||||
{ doc_count: 1, key: 1 },
|
||||
{ doc_count: 2, key: 2 },
|
||||
{ doc_count: 3, key: 3 },
|
||||
],
|
||||
},
|
||||
references: { cases: { max: { value: 1 } } },
|
||||
},
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('it returns the correct res', async () => {
|
||||
const res = await getUserCommentsTelemetryData({ savedObjectsClient, logger });
|
||||
expect(res).toEqual({
|
||||
all: {
|
||||
total: 5,
|
||||
daily: 3,
|
||||
weekly: 2,
|
||||
monthly: 1,
|
||||
maxOnACase: 1,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should call find with correct arguments', async () => {
|
||||
await getUserCommentsTelemetryData({ savedObjectsClient, logger });
|
||||
expect(savedObjectsClient.find).toBeCalledWith({
|
||||
aggs: {
|
||||
counts: {
|
||||
date_range: {
|
||||
field: 'cases-comments.attributes.created_at',
|
||||
format: 'dd/MM/YYYY',
|
||||
ranges: [
|
||||
{
|
||||
from: 'now-1d',
|
||||
to: 'now',
|
||||
},
|
||||
{
|
||||
from: 'now-1w',
|
||||
to: 'now',
|
||||
},
|
||||
{
|
||||
from: 'now-1M',
|
||||
to: 'now',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
references: {
|
||||
aggregations: {
|
||||
cases: {
|
||||
aggregations: {
|
||||
ids: {
|
||||
terms: {
|
||||
field: 'cases-comments.references.id',
|
||||
},
|
||||
},
|
||||
max: {
|
||||
max_bucket: {
|
||||
buckets_path: 'ids._count',
|
||||
},
|
||||
},
|
||||
},
|
||||
filter: {
|
||||
term: {
|
||||
'cases-comments.references.type': 'cases',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
nested: {
|
||||
path: 'cases-comments.references',
|
||||
},
|
||||
},
|
||||
},
|
||||
filter: {
|
||||
arguments: [
|
||||
{
|
||||
type: 'literal',
|
||||
value: 'cases-comments.attributes.type',
|
||||
},
|
||||
{
|
||||
type: 'literal',
|
||||
value: 'user',
|
||||
},
|
||||
{
|
||||
type: 'literal',
|
||||
value: false,
|
||||
},
|
||||
],
|
||||
function: 'is',
|
||||
type: 'function',
|
||||
},
|
||||
page: 0,
|
||||
perPage: 0,
|
||||
type: 'cases-comments',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
30
x-pack/plugins/cases/server/telemetry/queries/comments.ts
Normal file
30
x-pack/plugins/cases/server/telemetry/queries/comments.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* 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 { CASE_COMMENT_SAVED_OBJECT } from '../../../common/constants';
|
||||
import { buildFilter } from '../../client/utils';
|
||||
import { CasesTelemetry, CollectTelemetryDataParams } from '../types';
|
||||
import { getCountsAndMaxData } from './utils';
|
||||
|
||||
export const getUserCommentsTelemetryData = async ({
|
||||
savedObjectsClient,
|
||||
}: CollectTelemetryDataParams): Promise<CasesTelemetry['comments']> => {
|
||||
const onlyUserCommentsFilter = buildFilter({
|
||||
filters: ['user'],
|
||||
field: 'type',
|
||||
operator: 'or',
|
||||
type: CASE_COMMENT_SAVED_OBJECT,
|
||||
});
|
||||
|
||||
const res = await getCountsAndMaxData({
|
||||
savedObjectsClient,
|
||||
savedObjectType: CASE_COMMENT_SAVED_OBJECT,
|
||||
filter: onlyUserCommentsFilter,
|
||||
});
|
||||
|
||||
return res;
|
||||
};
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* 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 {
|
||||
loggingSystemMock,
|
||||
savedObjectsRepositoryMock,
|
||||
} from '../../../../../../src/core/server/mocks';
|
||||
import { getConfigurationTelemetryData } from './configuration';
|
||||
|
||||
describe('configuration', () => {
|
||||
describe('getConfigurationTelemetryData', () => {
|
||||
const logger = loggingSystemMock.createLogger();
|
||||
const savedObjectsClient = savedObjectsRepositoryMock.create();
|
||||
savedObjectsClient.find.mockResolvedValue({
|
||||
total: 5,
|
||||
saved_objects: [],
|
||||
per_page: 1,
|
||||
page: 1,
|
||||
aggregations: {
|
||||
closureType: {
|
||||
buckets: [
|
||||
{ doc_count: 1, key: 'close-by-user' },
|
||||
{ doc_count: 2, key: 'close-by-pushing' },
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('it returns the correct res', async () => {
|
||||
const res = await getConfigurationTelemetryData({ savedObjectsClient, logger });
|
||||
expect(res).toEqual({
|
||||
all: {
|
||||
closure: {
|
||||
manually: 1,
|
||||
automatic: 2,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should call find with correct arguments', async () => {
|
||||
await getConfigurationTelemetryData({ savedObjectsClient, logger });
|
||||
expect(savedObjectsClient.find).toBeCalledWith({
|
||||
aggs: {
|
||||
closureType: {
|
||||
terms: {
|
||||
field: 'cases-configure.attributes.closure_type',
|
||||
},
|
||||
},
|
||||
},
|
||||
filter: undefined,
|
||||
page: 0,
|
||||
perPage: 0,
|
||||
type: 'cases-configure',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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 { CASE_CONFIGURE_SAVED_OBJECT } from '../../../common/constants';
|
||||
import { Buckets, CasesTelemetry, CollectTelemetryDataParams } from '../types';
|
||||
import { findValueInBuckets } from './utils';
|
||||
|
||||
export const getConfigurationTelemetryData = async ({
|
||||
savedObjectsClient,
|
||||
}: CollectTelemetryDataParams): Promise<CasesTelemetry['configuration']> => {
|
||||
const res = await savedObjectsClient.find<
|
||||
unknown,
|
||||
{
|
||||
closureType: Buckets;
|
||||
}
|
||||
>({
|
||||
page: 0,
|
||||
perPage: 0,
|
||||
type: CASE_CONFIGURE_SAVED_OBJECT,
|
||||
aggs: {
|
||||
closureType: {
|
||||
terms: { field: `${CASE_CONFIGURE_SAVED_OBJECT}.attributes.closure_type` },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const closureBuckets = res.aggregations?.closureType?.buckets ?? [];
|
||||
|
||||
return {
|
||||
all: {
|
||||
closure: {
|
||||
manually: findValueInBuckets(closureBuckets, 'close-by-user'),
|
||||
automatic: findValueInBuckets(closureBuckets, 'close-by-pushing'),
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
219
x-pack/plugins/cases/server/telemetry/queries/connectors.test.ts
Normal file
219
x-pack/plugins/cases/server/telemetry/queries/connectors.test.ts
Normal file
|
@ -0,0 +1,219 @@
|
|||
/*
|
||||
* 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 {
|
||||
savedObjectsRepositoryMock,
|
||||
loggingSystemMock,
|
||||
} from '../../../../../../src/core/server/mocks';
|
||||
import { getConnectorsTelemetryData } from './connectors';
|
||||
|
||||
describe('getConnectorsTelemetryData', () => {
|
||||
describe('getConnectorsTelemetryData', () => {
|
||||
const logger = loggingSystemMock.createLogger();
|
||||
const savedObjectsClient = savedObjectsRepositoryMock.create();
|
||||
|
||||
const mockFind = (aggs: Record<string, unknown>) => {
|
||||
savedObjectsClient.find.mockResolvedValueOnce({
|
||||
total: 5,
|
||||
saved_objects: [],
|
||||
per_page: 1,
|
||||
page: 1,
|
||||
aggregations: {
|
||||
...aggs,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const mockResponse = () => {
|
||||
mockFind({ references: { referenceType: { referenceAgg: { value: 1 } } } });
|
||||
mockFind({ references: { cases: { max: { value: 2 } } } });
|
||||
mockFind({ references: { referenceType: { referenceAgg: { value: 3 } } } });
|
||||
mockFind({ references: { referenceType: { referenceAgg: { value: 4 } } } });
|
||||
mockFind({ references: { referenceType: { referenceAgg: { value: 5 } } } });
|
||||
mockFind({ references: { referenceType: { referenceAgg: { value: 6 } } } });
|
||||
mockFind({ references: { referenceType: { referenceAgg: { value: 7 } } } });
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('it returns the correct res', async () => {
|
||||
mockResponse();
|
||||
|
||||
const res = await getConnectorsTelemetryData({ savedObjectsClient, logger });
|
||||
expect(res).toEqual({
|
||||
all: {
|
||||
all: {
|
||||
totalAttached: 1,
|
||||
},
|
||||
itsm: {
|
||||
totalAttached: 3,
|
||||
},
|
||||
sir: {
|
||||
totalAttached: 4,
|
||||
},
|
||||
jira: {
|
||||
totalAttached: 5,
|
||||
},
|
||||
resilient: {
|
||||
totalAttached: 6,
|
||||
},
|
||||
swimlane: {
|
||||
totalAttached: 7,
|
||||
},
|
||||
maxAttachedToACase: 2,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should call find with correct arguments', async () => {
|
||||
mockResponse();
|
||||
|
||||
await getConnectorsTelemetryData({ savedObjectsClient, logger });
|
||||
|
||||
expect(savedObjectsClient.find.mock.calls[0][0]).toEqual({
|
||||
aggs: {
|
||||
references: {
|
||||
aggregations: {
|
||||
referenceType: {
|
||||
aggregations: {
|
||||
referenceAgg: {
|
||||
cardinality: {
|
||||
field: 'cases-user-actions.references.id',
|
||||
},
|
||||
},
|
||||
},
|
||||
filter: {
|
||||
term: {
|
||||
'cases-user-actions.references.type': 'action',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
nested: {
|
||||
path: 'cases-user-actions.references',
|
||||
},
|
||||
},
|
||||
},
|
||||
filter: undefined,
|
||||
page: 0,
|
||||
perPage: 0,
|
||||
type: 'cases-user-actions',
|
||||
});
|
||||
|
||||
expect(savedObjectsClient.find.mock.calls[1][0]).toEqual({
|
||||
aggs: {
|
||||
references: {
|
||||
aggregations: {
|
||||
cases: {
|
||||
aggregations: {
|
||||
ids: {
|
||||
terms: {
|
||||
field: 'cases-user-actions.references.id',
|
||||
},
|
||||
},
|
||||
max: {
|
||||
max_bucket: {
|
||||
buckets_path: 'ids._count',
|
||||
},
|
||||
},
|
||||
},
|
||||
filter: {
|
||||
term: {
|
||||
'cases-user-actions.references.type': 'cases',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
nested: {
|
||||
path: 'cases-user-actions.references',
|
||||
},
|
||||
},
|
||||
},
|
||||
filter: {
|
||||
arguments: [
|
||||
{
|
||||
type: 'literal',
|
||||
value: 'cases-user-actions.attributes.type',
|
||||
},
|
||||
{
|
||||
type: 'literal',
|
||||
value: 'connector',
|
||||
},
|
||||
{
|
||||
type: 'literal',
|
||||
value: false,
|
||||
},
|
||||
],
|
||||
function: 'is',
|
||||
type: 'function',
|
||||
},
|
||||
page: 0,
|
||||
perPage: 0,
|
||||
type: 'cases-user-actions',
|
||||
});
|
||||
|
||||
for (const [index, connector] of [
|
||||
'.servicenow',
|
||||
'.servicenow-sir',
|
||||
'.jira',
|
||||
'.resilient',
|
||||
'.swimlane',
|
||||
].entries()) {
|
||||
const callIndex = index + 2;
|
||||
|
||||
expect(savedObjectsClient.find.mock.calls[callIndex][0]).toEqual({
|
||||
aggs: {
|
||||
references: {
|
||||
aggregations: {
|
||||
referenceType: {
|
||||
aggregations: {
|
||||
referenceAgg: {
|
||||
cardinality: {
|
||||
field: 'cases-user-actions.references.id',
|
||||
},
|
||||
},
|
||||
},
|
||||
filter: {
|
||||
term: {
|
||||
'cases-user-actions.references.type': 'action',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
nested: {
|
||||
path: 'cases-user-actions.references',
|
||||
},
|
||||
},
|
||||
},
|
||||
filter: {
|
||||
arguments: [
|
||||
{
|
||||
type: 'literal',
|
||||
value: 'cases-user-actions.attributes.payload.connector.type',
|
||||
},
|
||||
{
|
||||
type: 'literal',
|
||||
value: connector,
|
||||
},
|
||||
{
|
||||
type: 'literal',
|
||||
value: false,
|
||||
},
|
||||
],
|
||||
function: 'is',
|
||||
type: 'function',
|
||||
},
|
||||
page: 0,
|
||||
perPage: 0,
|
||||
type: 'cases-user-actions',
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
118
x-pack/plugins/cases/server/telemetry/queries/connectors.ts
Normal file
118
x-pack/plugins/cases/server/telemetry/queries/connectors.ts
Normal file
|
@ -0,0 +1,118 @@
|
|||
/*
|
||||
* 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 { AggregationsAggregationContainer } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { KueryNode } from '@kbn/es-query';
|
||||
import { SavedObjectsFindResponse } from 'kibana/server';
|
||||
import { CASE_USER_ACTION_SAVED_OBJECT } from '../../../common/constants';
|
||||
import { buildFilter } from '../../client/utils';
|
||||
import {
|
||||
CasesTelemetry,
|
||||
CollectTelemetryDataParams,
|
||||
MaxBucketOnCaseAggregation,
|
||||
ReferencesAggregation,
|
||||
} from '../types';
|
||||
import {
|
||||
getConnectorsCardinalityAggregationQuery,
|
||||
getMaxBucketOnCaseAggregationQuery,
|
||||
getOnlyConnectorsFilter,
|
||||
} from './utils';
|
||||
|
||||
export const getConnectorsTelemetryData = async ({
|
||||
savedObjectsClient,
|
||||
}: CollectTelemetryDataParams): Promise<CasesTelemetry['connectors']> => {
|
||||
const getData = async <A>({
|
||||
filter,
|
||||
aggs,
|
||||
}: {
|
||||
filter?: KueryNode;
|
||||
aggs?: Record<string, AggregationsAggregationContainer>;
|
||||
} = {}) => {
|
||||
const res = await savedObjectsClient.find<unknown, A>({
|
||||
page: 0,
|
||||
perPage: 0,
|
||||
filter,
|
||||
type: CASE_USER_ACTION_SAVED_OBJECT,
|
||||
aggs: {
|
||||
...aggs,
|
||||
},
|
||||
});
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
const getConnectorData = async (connectorType: string) => {
|
||||
const connectorFilter = buildFilter({
|
||||
filters: [connectorType],
|
||||
field: 'payload.connector.type',
|
||||
operator: 'or',
|
||||
type: CASE_USER_ACTION_SAVED_OBJECT,
|
||||
});
|
||||
|
||||
const res = await getData<ReferencesAggregation>({
|
||||
filter: connectorFilter,
|
||||
aggs: getConnectorsCardinalityAggregationQuery(),
|
||||
});
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
const connectorTypes = [
|
||||
'.servicenow',
|
||||
'.servicenow-sir',
|
||||
'.jira',
|
||||
'.resilient',
|
||||
'.swimlane',
|
||||
] as const;
|
||||
|
||||
const all = await Promise.all([
|
||||
getData<ReferencesAggregation>({ aggs: getConnectorsCardinalityAggregationQuery() }),
|
||||
getData<MaxBucketOnCaseAggregation>({
|
||||
filter: getOnlyConnectorsFilter(),
|
||||
aggs: getMaxBucketOnCaseAggregationQuery(CASE_USER_ACTION_SAVED_OBJECT),
|
||||
}),
|
||||
...connectorTypes.map((connectorType) => getConnectorData(connectorType)),
|
||||
]);
|
||||
|
||||
const connectorData = all.slice(2) as Array<
|
||||
SavedObjectsFindResponse<unknown, ReferencesAggregation>
|
||||
>;
|
||||
|
||||
const data = connectorData.reduce(
|
||||
(acc, res, currentIndex) => ({
|
||||
...acc,
|
||||
[connectorTypes[currentIndex]]:
|
||||
res.aggregations?.references?.referenceType?.referenceAgg?.value ?? 0,
|
||||
}),
|
||||
{} as Record<typeof connectorTypes[number], number>
|
||||
);
|
||||
|
||||
const allAttached = all[0].aggregations?.references?.referenceType?.referenceAgg?.value ?? 0;
|
||||
const maxAttachedToACase = all[1].aggregations?.references?.cases?.max?.value ?? 0;
|
||||
|
||||
return {
|
||||
all: {
|
||||
all: { totalAttached: allAttached },
|
||||
itsm: { totalAttached: data['.servicenow'] },
|
||||
sir: { totalAttached: data['.servicenow-sir'] },
|
||||
jira: { totalAttached: data['.jira'] },
|
||||
resilient: { totalAttached: data['.resilient'] },
|
||||
swimlane: { totalAttached: data['.swimlane'] },
|
||||
/**
|
||||
* This metric is not 100% accurate. To get this metric we
|
||||
* we do a term aggregation based on the the case reference id.
|
||||
* Each bucket corresponds to a case and contains the total user actions
|
||||
* of type connector. Then from all buckets we take the maximum bucket.
|
||||
* A user actions of type connectors will be created if the connector is attached
|
||||
* to a case or the user updates the fields of the connector. This metric
|
||||
* contains also the updates on the fields of the connector. Ideally we would
|
||||
* like to filter for unique connector ids on each bucket.
|
||||
*/
|
||||
maxAttachedToACase,
|
||||
},
|
||||
};
|
||||
};
|
97
x-pack/plugins/cases/server/telemetry/queries/pushed.test.ts
Normal file
97
x-pack/plugins/cases/server/telemetry/queries/pushed.test.ts
Normal file
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
* 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 {
|
||||
savedObjectsRepositoryMock,
|
||||
loggingSystemMock,
|
||||
} from '../../../../../../src/core/server/mocks';
|
||||
import { getPushedTelemetryData } from './pushes';
|
||||
|
||||
describe('pushes', () => {
|
||||
describe('getPushedTelemetryData', () => {
|
||||
const logger = loggingSystemMock.createLogger();
|
||||
const savedObjectsClient = savedObjectsRepositoryMock.create();
|
||||
savedObjectsClient.find.mockResolvedValue({
|
||||
total: 5,
|
||||
saved_objects: [],
|
||||
per_page: 1,
|
||||
page: 1,
|
||||
aggregations: {
|
||||
references: { cases: { max: { value: 1 } } },
|
||||
},
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('it returns the correct res', async () => {
|
||||
const res = await getPushedTelemetryData({ savedObjectsClient, logger });
|
||||
expect(res).toEqual({
|
||||
all: {
|
||||
maxOnACase: 1,
|
||||
total: 5,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should call find with correct arguments', async () => {
|
||||
await getPushedTelemetryData({ savedObjectsClient, logger });
|
||||
expect(savedObjectsClient.find).toBeCalledWith({
|
||||
aggs: {
|
||||
references: {
|
||||
nested: {
|
||||
path: 'cases-user-actions.references',
|
||||
},
|
||||
aggregations: {
|
||||
cases: {
|
||||
aggregations: {
|
||||
ids: {
|
||||
terms: {
|
||||
field: 'cases-user-actions.references.id',
|
||||
},
|
||||
},
|
||||
max: {
|
||||
max_bucket: {
|
||||
buckets_path: 'ids._count',
|
||||
},
|
||||
},
|
||||
},
|
||||
filter: {
|
||||
term: {
|
||||
'cases-user-actions.references.type': 'cases',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
filter: {
|
||||
arguments: [
|
||||
{
|
||||
type: 'literal',
|
||||
value: 'cases-user-actions.attributes.type',
|
||||
},
|
||||
{
|
||||
type: 'literal',
|
||||
value: 'pushed',
|
||||
},
|
||||
{
|
||||
type: 'literal',
|
||||
value: false,
|
||||
},
|
||||
],
|
||||
function: 'is',
|
||||
type: 'function',
|
||||
},
|
||||
page: 0,
|
||||
perPage: 0,
|
||||
type: 'cases-user-actions',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
36
x-pack/plugins/cases/server/telemetry/queries/pushes.ts
Normal file
36
x-pack/plugins/cases/server/telemetry/queries/pushes.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* 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 { CASE_USER_ACTION_SAVED_OBJECT } from '../../../common/constants';
|
||||
import { buildFilter } from '../../client/utils';
|
||||
import { CasesTelemetry, CollectTelemetryDataParams, MaxBucketOnCaseAggregation } from '../types';
|
||||
import { getMaxBucketOnCaseAggregationQuery } from './utils';
|
||||
|
||||
export const getPushedTelemetryData = async ({
|
||||
savedObjectsClient,
|
||||
}: CollectTelemetryDataParams): Promise<CasesTelemetry['pushes']> => {
|
||||
const pushFilter = buildFilter({
|
||||
filters: ['pushed'],
|
||||
field: 'type',
|
||||
operator: 'or',
|
||||
type: CASE_USER_ACTION_SAVED_OBJECT,
|
||||
});
|
||||
|
||||
const res = await savedObjectsClient.find<unknown, MaxBucketOnCaseAggregation>({
|
||||
page: 0,
|
||||
perPage: 0,
|
||||
filter: pushFilter,
|
||||
type: CASE_USER_ACTION_SAVED_OBJECT,
|
||||
aggs: { ...getMaxBucketOnCaseAggregationQuery(CASE_USER_ACTION_SAVED_OBJECT) },
|
||||
});
|
||||
|
||||
const maxOnACase = res.aggregations?.references?.cases?.max?.value ?? 0;
|
||||
|
||||
return {
|
||||
all: { total: res.total, maxOnACase },
|
||||
};
|
||||
};
|
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
* 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 {
|
||||
savedObjectsRepositoryMock,
|
||||
loggingSystemMock,
|
||||
} from '../../../../../../src/core/server/mocks';
|
||||
import { getUserActionsTelemetryData } from './user_actions';
|
||||
|
||||
describe('user_actions', () => {
|
||||
describe('getUserActionsTelemetryData', () => {
|
||||
const logger = loggingSystemMock.createLogger();
|
||||
const savedObjectsClient = savedObjectsRepositoryMock.create();
|
||||
savedObjectsClient.find.mockResolvedValue({
|
||||
total: 5,
|
||||
saved_objects: [],
|
||||
per_page: 1,
|
||||
page: 1,
|
||||
aggregations: {
|
||||
counts: {
|
||||
buckets: [
|
||||
{ doc_count: 1, key: 1 },
|
||||
{ doc_count: 2, key: 2 },
|
||||
{ doc_count: 3, key: 3 },
|
||||
],
|
||||
},
|
||||
references: { cases: { max: { value: 1 } } },
|
||||
},
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('it returns the correct res', async () => {
|
||||
const res = await getUserActionsTelemetryData({ savedObjectsClient, logger });
|
||||
expect(res).toEqual({
|
||||
all: {
|
||||
total: 5,
|
||||
daily: 3,
|
||||
weekly: 2,
|
||||
monthly: 1,
|
||||
maxOnACase: 1,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should call find with correct arguments', async () => {
|
||||
await getUserActionsTelemetryData({ savedObjectsClient, logger });
|
||||
expect(savedObjectsClient.find).toBeCalledWith({
|
||||
aggs: {
|
||||
counts: {
|
||||
date_range: {
|
||||
field: 'cases-user-actions.attributes.created_at',
|
||||
format: 'dd/MM/YYYY',
|
||||
ranges: [
|
||||
{
|
||||
from: 'now-1d',
|
||||
to: 'now',
|
||||
},
|
||||
{
|
||||
from: 'now-1w',
|
||||
to: 'now',
|
||||
},
|
||||
{
|
||||
from: 'now-1M',
|
||||
to: 'now',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
references: {
|
||||
aggregations: {
|
||||
cases: {
|
||||
aggregations: {
|
||||
ids: {
|
||||
terms: {
|
||||
field: 'cases-user-actions.references.id',
|
||||
},
|
||||
},
|
||||
max: {
|
||||
max_bucket: {
|
||||
buckets_path: 'ids._count',
|
||||
},
|
||||
},
|
||||
},
|
||||
filter: {
|
||||
term: {
|
||||
'cases-user-actions.references.type': 'cases',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
nested: {
|
||||
path: 'cases-user-actions.references',
|
||||
},
|
||||
},
|
||||
},
|
||||
filter: undefined,
|
||||
page: 0,
|
||||
perPage: 0,
|
||||
type: 'cases-user-actions',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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 { CASE_USER_ACTION_SAVED_OBJECT } from '../../../common/constants';
|
||||
import { CasesTelemetry, CollectTelemetryDataParams } from '../types';
|
||||
import { getCountsAndMaxData } from './utils';
|
||||
|
||||
export const getUserActionsTelemetryData = async ({
|
||||
savedObjectsClient,
|
||||
}: CollectTelemetryDataParams): Promise<CasesTelemetry['userActions']> => {
|
||||
const res = await getCountsAndMaxData({
|
||||
savedObjectsClient,
|
||||
savedObjectType: CASE_USER_ACTION_SAVED_OBJECT,
|
||||
});
|
||||
|
||||
return res;
|
||||
};
|
423
x-pack/plugins/cases/server/telemetry/queries/utils.test.ts
Normal file
423
x-pack/plugins/cases/server/telemetry/queries/utils.test.ts
Normal file
|
@ -0,0 +1,423 @@
|
|||
/*
|
||||
* 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 { savedObjectsRepositoryMock } from '../../../../../../src/core/server/mocks';
|
||||
import {
|
||||
findValueInBuckets,
|
||||
getAggregationsBuckets,
|
||||
getBucketFromAggregation,
|
||||
getConnectorsCardinalityAggregationQuery,
|
||||
getCountsAggregationQuery,
|
||||
getCountsAndMaxData,
|
||||
getCountsFromBuckets,
|
||||
getMaxBucketOnCaseAggregationQuery,
|
||||
getOnlyAlertsCommentsFilter,
|
||||
getOnlyConnectorsFilter,
|
||||
getReferencesAggregationQuery,
|
||||
} from './utils';
|
||||
|
||||
describe('utils', () => {
|
||||
describe('getCountsAggregationQuery', () => {
|
||||
it('returns the correct query', () => {
|
||||
expect(getCountsAggregationQuery('test')).toEqual({
|
||||
counts: {
|
||||
date_range: {
|
||||
field: 'test.attributes.created_at',
|
||||
format: 'dd/MM/YYYY',
|
||||
ranges: [
|
||||
{ from: 'now-1d', to: 'now' },
|
||||
{ from: 'now-1w', to: 'now' },
|
||||
{ from: 'now-1M', to: 'now' },
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMaxBucketOnCaseAggregationQuery', () => {
|
||||
it('returns the correct query', () => {
|
||||
expect(getMaxBucketOnCaseAggregationQuery('test')).toEqual({
|
||||
references: {
|
||||
nested: {
|
||||
path: 'test.references',
|
||||
},
|
||||
aggregations: {
|
||||
cases: {
|
||||
filter: {
|
||||
term: {
|
||||
'test.references.type': 'cases',
|
||||
},
|
||||
},
|
||||
aggregations: {
|
||||
ids: {
|
||||
terms: {
|
||||
field: 'test.references.id',
|
||||
},
|
||||
},
|
||||
max: {
|
||||
max_bucket: {
|
||||
buckets_path: 'ids._count',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getReferencesAggregationQuery', () => {
|
||||
it('returns the correct query', () => {
|
||||
expect(
|
||||
getReferencesAggregationQuery({ savedObjectType: 'test', referenceType: 'cases' })
|
||||
).toEqual({
|
||||
references: {
|
||||
nested: {
|
||||
path: 'test.references',
|
||||
},
|
||||
aggregations: {
|
||||
referenceType: {
|
||||
filter: {
|
||||
term: {
|
||||
'test.references.type': 'cases',
|
||||
},
|
||||
},
|
||||
aggregations: {
|
||||
referenceAgg: {
|
||||
terms: {
|
||||
field: 'test.references.id',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the correct query when changing the agg', () => {
|
||||
expect(
|
||||
getReferencesAggregationQuery({
|
||||
savedObjectType: 'test',
|
||||
referenceType: 'cases',
|
||||
agg: 'cardinality',
|
||||
})
|
||||
).toEqual({
|
||||
references: {
|
||||
nested: {
|
||||
path: 'test.references',
|
||||
},
|
||||
aggregations: {
|
||||
referenceType: {
|
||||
filter: {
|
||||
term: {
|
||||
'test.references.type': 'cases',
|
||||
},
|
||||
},
|
||||
aggregations: {
|
||||
referenceAgg: {
|
||||
cardinality: {
|
||||
field: 'test.references.id',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getConnectorsCardinalityAggregationQuery', () => {
|
||||
it('returns the correct query', () => {
|
||||
expect(getConnectorsCardinalityAggregationQuery()).toEqual({
|
||||
references: {
|
||||
nested: {
|
||||
path: 'cases-user-actions.references',
|
||||
},
|
||||
aggregations: {
|
||||
referenceType: {
|
||||
filter: {
|
||||
term: {
|
||||
'cases-user-actions.references.type': 'action',
|
||||
},
|
||||
},
|
||||
aggregations: {
|
||||
referenceAgg: {
|
||||
cardinality: {
|
||||
field: 'cases-user-actions.references.id',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCountsFromBuckets', () => {
|
||||
it('returns the correct counts', () => {
|
||||
const buckets = [
|
||||
{ doc_count: 1, key: 1 },
|
||||
{ doc_count: 2, key: 2 },
|
||||
{ doc_count: 3, key: 3 },
|
||||
];
|
||||
|
||||
expect(getCountsFromBuckets(buckets)).toEqual({
|
||||
daily: 3,
|
||||
weekly: 2,
|
||||
monthly: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns zero counts when the bucket do not have the doc_count field', () => {
|
||||
const buckets = [{}];
|
||||
// @ts-expect-error
|
||||
expect(getCountsFromBuckets(buckets)).toEqual({
|
||||
daily: 0,
|
||||
weekly: 0,
|
||||
monthly: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns zero counts when the bucket is undefined', () => {
|
||||
// @ts-expect-error
|
||||
expect(getCountsFromBuckets(undefined)).toEqual({
|
||||
daily: 0,
|
||||
weekly: 0,
|
||||
monthly: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns zero counts when the doc_count field is missing in some buckets', () => {
|
||||
const buckets = [{ doc_count: 1, key: 1 }, {}, {}];
|
||||
// @ts-expect-error
|
||||
expect(getCountsFromBuckets(buckets)).toEqual({
|
||||
daily: 0,
|
||||
weekly: 0,
|
||||
monthly: 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCountsAndMaxData', () => {
|
||||
const savedObjectsClient = savedObjectsRepositoryMock.create();
|
||||
savedObjectsClient.find.mockResolvedValue({
|
||||
total: 5,
|
||||
saved_objects: [],
|
||||
per_page: 1,
|
||||
page: 1,
|
||||
aggregations: {
|
||||
counts: {
|
||||
buckets: [
|
||||
{ doc_count: 1, key: 1 },
|
||||
{ doc_count: 2, key: 2 },
|
||||
{ doc_count: 3, key: 3 },
|
||||
],
|
||||
},
|
||||
references: { cases: { max: { value: 1 } } },
|
||||
},
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('returns the correct counts and max data', async () => {
|
||||
const res = await getCountsAndMaxData({ savedObjectsClient, savedObjectType: 'test' });
|
||||
expect(res).toEqual({
|
||||
all: {
|
||||
total: 5,
|
||||
daily: 3,
|
||||
weekly: 2,
|
||||
monthly: 1,
|
||||
maxOnACase: 1,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('returns zero data if the response aggregation is not as expected', async () => {
|
||||
savedObjectsClient.find.mockResolvedValue({
|
||||
total: 5,
|
||||
saved_objects: [],
|
||||
per_page: 1,
|
||||
page: 1,
|
||||
});
|
||||
|
||||
const res = await getCountsAndMaxData({ savedObjectsClient, savedObjectType: 'test' });
|
||||
expect(res).toEqual({
|
||||
all: {
|
||||
total: 5,
|
||||
daily: 0,
|
||||
weekly: 0,
|
||||
monthly: 0,
|
||||
maxOnACase: 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should call find with correct arguments', async () => {
|
||||
await getCountsAndMaxData({ savedObjectsClient, savedObjectType: 'test' });
|
||||
expect(savedObjectsClient.find).toBeCalledWith({
|
||||
aggs: {
|
||||
counts: {
|
||||
date_range: {
|
||||
field: 'test.attributes.created_at',
|
||||
format: 'dd/MM/YYYY',
|
||||
ranges: [
|
||||
{
|
||||
from: 'now-1d',
|
||||
to: 'now',
|
||||
},
|
||||
{
|
||||
from: 'now-1w',
|
||||
to: 'now',
|
||||
},
|
||||
{
|
||||
from: 'now-1M',
|
||||
to: 'now',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
references: {
|
||||
aggregations: {
|
||||
cases: {
|
||||
aggregations: {
|
||||
ids: {
|
||||
terms: {
|
||||
field: 'test.references.id',
|
||||
},
|
||||
},
|
||||
max: {
|
||||
max_bucket: {
|
||||
buckets_path: 'ids._count',
|
||||
},
|
||||
},
|
||||
},
|
||||
filter: {
|
||||
term: {
|
||||
'test.references.type': 'cases',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
nested: {
|
||||
path: 'test.references',
|
||||
},
|
||||
},
|
||||
},
|
||||
filter: undefined,
|
||||
page: 0,
|
||||
perPage: 0,
|
||||
type: 'test',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getBucketFromAggregation', () => {
|
||||
it('returns the buckets', () => {
|
||||
expect(
|
||||
getBucketFromAggregation({
|
||||
aggs: { test: { deep: { buckets: [{ doc_count: 1, key: 1 }] } } },
|
||||
key: 'test.deep',
|
||||
})
|
||||
).toEqual([{ doc_count: 1, key: 1 }]);
|
||||
});
|
||||
|
||||
it('returns an empty array if the path does not exist', () => {
|
||||
expect(
|
||||
getBucketFromAggregation({
|
||||
key: 'test.deep',
|
||||
})
|
||||
).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findValueInBuckets', () => {
|
||||
it('find the value in the bucket', () => {
|
||||
const buckets = [
|
||||
{ doc_count: 1, key: 'test' },
|
||||
{ doc_count: 2, key: 'not' },
|
||||
];
|
||||
expect(findValueInBuckets(buckets, 'test')).toBe(1);
|
||||
});
|
||||
|
||||
it('return zero if the value is not found', () => {
|
||||
const buckets = [{ doc_count: 1, key: 'test' }];
|
||||
expect(findValueInBuckets(buckets, 'not-in-the-bucket')).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAggregationsBuckets', () => {
|
||||
it('return aggregation buckets', () => {
|
||||
const buckets = [
|
||||
{ doc_count: 1, key: 'test' },
|
||||
{ doc_count: 2, key: 'not' },
|
||||
];
|
||||
|
||||
const aggs = {
|
||||
foo: { baz: { buckets } },
|
||||
bar: { buckets },
|
||||
};
|
||||
|
||||
expect(getAggregationsBuckets({ aggs, keys: ['foo.baz', 'bar'] })).toEqual({
|
||||
'foo.baz': buckets,
|
||||
bar: buckets,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getOnlyAlertsCommentsFilter', () => {
|
||||
it('return the correct filter', () => {
|
||||
expect(getOnlyAlertsCommentsFilter()).toEqual({
|
||||
arguments: [
|
||||
{
|
||||
type: 'literal',
|
||||
value: 'cases-comments.attributes.type',
|
||||
},
|
||||
{
|
||||
type: 'literal',
|
||||
value: 'alert',
|
||||
},
|
||||
{
|
||||
type: 'literal',
|
||||
value: false,
|
||||
},
|
||||
],
|
||||
function: 'is',
|
||||
type: 'function',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getOnlyConnectorsFilter', () => {
|
||||
it('return the correct filter', () => {
|
||||
expect(getOnlyConnectorsFilter()).toEqual({
|
||||
arguments: [
|
||||
{
|
||||
type: 'literal',
|
||||
value: 'cases-user-actions.attributes.type',
|
||||
},
|
||||
{
|
||||
type: 'literal',
|
||||
value: 'connector',
|
||||
},
|
||||
{
|
||||
type: 'literal',
|
||||
value: false,
|
||||
},
|
||||
],
|
||||
function: 'is',
|
||||
type: 'function',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
238
x-pack/plugins/cases/server/telemetry/queries/utils.ts
Normal file
238
x-pack/plugins/cases/server/telemetry/queries/utils.ts
Normal file
|
@ -0,0 +1,238 @@
|
|||
/*
|
||||
* 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 { get } from 'lodash';
|
||||
import { KueryNode } from '@kbn/es-query';
|
||||
import { ISavedObjectsRepository } from 'kibana/server';
|
||||
import {
|
||||
CASE_COMMENT_SAVED_OBJECT,
|
||||
CASE_SAVED_OBJECT,
|
||||
CASE_USER_ACTION_SAVED_OBJECT,
|
||||
} from '../../../common/constants';
|
||||
import { Buckets, CasesTelemetry, MaxBucketOnCaseAggregation } from '../types';
|
||||
import { buildFilter } from '../../client/utils';
|
||||
|
||||
export const getCountsAggregationQuery = (savedObjectType: string) => ({
|
||||
counts: {
|
||||
date_range: {
|
||||
field: `${savedObjectType}.attributes.created_at`,
|
||||
format: 'dd/MM/YYYY',
|
||||
ranges: [
|
||||
{ from: 'now-1d', to: 'now' },
|
||||
{ from: 'now-1w', to: 'now' },
|
||||
{ from: 'now-1M', to: 'now' },
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const getMaxBucketOnCaseAggregationQuery = (savedObjectType: string) => ({
|
||||
references: {
|
||||
nested: {
|
||||
path: `${savedObjectType}.references`,
|
||||
},
|
||||
aggregations: {
|
||||
cases: {
|
||||
filter: {
|
||||
term: {
|
||||
[`${savedObjectType}.references.type`]: CASE_SAVED_OBJECT,
|
||||
},
|
||||
},
|
||||
aggregations: {
|
||||
ids: {
|
||||
terms: {
|
||||
field: `${savedObjectType}.references.id`,
|
||||
},
|
||||
},
|
||||
max: {
|
||||
max_bucket: {
|
||||
buckets_path: 'ids._count',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const getReferencesAggregationQuery = ({
|
||||
savedObjectType,
|
||||
referenceType,
|
||||
agg = 'terms',
|
||||
}: {
|
||||
savedObjectType: string;
|
||||
referenceType: string;
|
||||
agg?: string;
|
||||
}) => ({
|
||||
references: {
|
||||
nested: {
|
||||
path: `${savedObjectType}.references`,
|
||||
},
|
||||
aggregations: {
|
||||
referenceType: {
|
||||
filter: {
|
||||
term: {
|
||||
[`${savedObjectType}.references.type`]: referenceType,
|
||||
},
|
||||
},
|
||||
aggregations: {
|
||||
referenceAgg: {
|
||||
[agg]: {
|
||||
field: `${savedObjectType}.references.id`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const getConnectorsCardinalityAggregationQuery = () =>
|
||||
getReferencesAggregationQuery({
|
||||
savedObjectType: CASE_USER_ACTION_SAVED_OBJECT,
|
||||
referenceType: 'action',
|
||||
agg: 'cardinality',
|
||||
});
|
||||
|
||||
export const getCountsFromBuckets = (buckets: Buckets['buckets']) => ({
|
||||
daily: buckets?.[2]?.doc_count ?? 0,
|
||||
weekly: buckets?.[1]?.doc_count ?? 0,
|
||||
monthly: buckets?.[0]?.doc_count ?? 0,
|
||||
});
|
||||
|
||||
export const getCountsAndMaxData = async ({
|
||||
savedObjectsClient,
|
||||
savedObjectType,
|
||||
filter,
|
||||
}: {
|
||||
savedObjectsClient: ISavedObjectsRepository;
|
||||
savedObjectType: string;
|
||||
filter?: KueryNode;
|
||||
}) => {
|
||||
const res = await savedObjectsClient.find<
|
||||
unknown,
|
||||
{ counts: Buckets; references: MaxBucketOnCaseAggregation['references'] }
|
||||
>({
|
||||
page: 0,
|
||||
perPage: 0,
|
||||
filter,
|
||||
type: savedObjectType,
|
||||
aggs: {
|
||||
...getCountsAggregationQuery(savedObjectType),
|
||||
...getMaxBucketOnCaseAggregationQuery(savedObjectType),
|
||||
},
|
||||
});
|
||||
|
||||
const countsBuckets = res.aggregations?.counts?.buckets ?? [];
|
||||
const maxOnACase = res.aggregations?.references?.cases?.max?.value ?? 0;
|
||||
|
||||
return {
|
||||
all: {
|
||||
total: res.total,
|
||||
...getCountsFromBuckets(countsBuckets),
|
||||
maxOnACase,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const getBucketFromAggregation = ({
|
||||
aggs,
|
||||
key,
|
||||
}: {
|
||||
key: string;
|
||||
aggs?: Record<string, unknown>;
|
||||
}): Buckets['buckets'] => (get(aggs, `${key}.buckets`) ?? []) as Buckets['buckets'];
|
||||
|
||||
export const findValueInBuckets = (buckets: Buckets['buckets'], value: string | number): number =>
|
||||
buckets.find(({ key }) => key === value)?.doc_count ?? 0;
|
||||
|
||||
export const getAggregationsBuckets = ({
|
||||
aggs,
|
||||
keys,
|
||||
}: {
|
||||
keys: string[];
|
||||
aggs?: Record<string, unknown>;
|
||||
}): Record<string, Buckets['buckets']> =>
|
||||
keys.reduce(
|
||||
(acc, key) => ({
|
||||
...acc,
|
||||
[key]: getBucketFromAggregation({ aggs, key }),
|
||||
}),
|
||||
{}
|
||||
);
|
||||
|
||||
export const getOnlyAlertsCommentsFilter = () =>
|
||||
buildFilter({
|
||||
filters: ['alert'],
|
||||
field: 'type',
|
||||
operator: 'or',
|
||||
type: CASE_COMMENT_SAVED_OBJECT,
|
||||
});
|
||||
|
||||
export const getOnlyConnectorsFilter = () =>
|
||||
buildFilter({
|
||||
filters: ['connector'],
|
||||
field: 'type',
|
||||
operator: 'or',
|
||||
type: CASE_USER_ACTION_SAVED_OBJECT,
|
||||
});
|
||||
|
||||
export const getTelemetryDataEmptyState = (): CasesTelemetry => ({
|
||||
cases: {
|
||||
all: {
|
||||
total: 0,
|
||||
monthly: 0,
|
||||
weekly: 0,
|
||||
daily: 0,
|
||||
status: {
|
||||
open: 0,
|
||||
inProgress: 0,
|
||||
closed: 0,
|
||||
},
|
||||
syncAlertsOn: 0,
|
||||
syncAlertsOff: 0,
|
||||
totalUsers: 0,
|
||||
totalParticipants: 0,
|
||||
totalTags: 0,
|
||||
totalWithAlerts: 0,
|
||||
totalWithConnectors: 0,
|
||||
latestDates: {
|
||||
createdAt: null,
|
||||
updatedAt: null,
|
||||
closedAt: null,
|
||||
},
|
||||
},
|
||||
sec: { total: 0, monthly: 0, weekly: 0, daily: 0 },
|
||||
obs: { total: 0, monthly: 0, weekly: 0, daily: 0 },
|
||||
main: { total: 0, monthly: 0, weekly: 0, daily: 0 },
|
||||
},
|
||||
userActions: { all: { total: 0, monthly: 0, weekly: 0, daily: 0, maxOnACase: 0 } },
|
||||
comments: { all: { total: 0, monthly: 0, weekly: 0, daily: 0, maxOnACase: 0 } },
|
||||
alerts: { all: { total: 0, monthly: 0, weekly: 0, daily: 0, maxOnACase: 0 } },
|
||||
connectors: {
|
||||
all: {
|
||||
all: { totalAttached: 0 },
|
||||
itsm: { totalAttached: 0 },
|
||||
sir: { totalAttached: 0 },
|
||||
jira: { totalAttached: 0 },
|
||||
resilient: { totalAttached: 0 },
|
||||
swimlane: { totalAttached: 0 },
|
||||
maxAttachedToACase: 0,
|
||||
},
|
||||
},
|
||||
pushes: {
|
||||
all: { total: 0, maxOnACase: 0 },
|
||||
},
|
||||
configuration: {
|
||||
all: {
|
||||
closure: {
|
||||
manually: 0,
|
||||
automatic: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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 { Logger } from 'kibana/server';
|
||||
import { TaskManagerStartContract } from '../../../task_manager/server';
|
||||
import { CASES_TELEMETRY_TASK_NAME } from '../../common/constants';
|
||||
|
||||
const MINUTES_ON_HALF_DAY = 60 * 12;
|
||||
|
||||
export const scheduleCasesTelemetryTask = (
|
||||
taskManager: TaskManagerStartContract,
|
||||
logger: Logger
|
||||
) => {
|
||||
taskManager
|
||||
.ensureScheduled({
|
||||
id: CASES_TELEMETRY_TASK_NAME,
|
||||
taskType: CASES_TELEMETRY_TASK_NAME,
|
||||
schedule: {
|
||||
interval: `${MINUTES_ON_HALF_DAY}m`,
|
||||
},
|
||||
scope: ['cases'],
|
||||
params: {},
|
||||
state: {},
|
||||
})
|
||||
.catch((err) =>
|
||||
logger.debug(
|
||||
`Error scheduling cases task with ID ${CASES_TELEMETRY_TASK_NAME} and type ${CASES_TELEMETRY_TASK_NAME}. Received ${err.message}`
|
||||
)
|
||||
);
|
||||
};
|
82
x-pack/plugins/cases/server/telemetry/schema.ts
Normal file
82
x-pack/plugins/cases/server/telemetry/schema.ts
Normal file
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* 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 {
|
||||
CasesTelemetrySchema,
|
||||
TypeLong,
|
||||
CountSchema,
|
||||
StatusSchema,
|
||||
LatestDatesSchema,
|
||||
TypeString,
|
||||
} from './types';
|
||||
|
||||
const long: TypeLong = { type: 'long' };
|
||||
const string: TypeString = { type: 'keyword' };
|
||||
|
||||
const countSchema: CountSchema = {
|
||||
total: long,
|
||||
monthly: long,
|
||||
weekly: long,
|
||||
daily: long,
|
||||
};
|
||||
|
||||
const statusSchema: StatusSchema = {
|
||||
open: long,
|
||||
inProgress: long,
|
||||
closed: long,
|
||||
};
|
||||
|
||||
const latestDatesSchema: LatestDatesSchema = {
|
||||
createdAt: string,
|
||||
updatedAt: string,
|
||||
closedAt: string,
|
||||
};
|
||||
|
||||
export const casesSchema: CasesTelemetrySchema = {
|
||||
cases: {
|
||||
all: {
|
||||
...countSchema,
|
||||
status: statusSchema,
|
||||
syncAlertsOn: long,
|
||||
syncAlertsOff: long,
|
||||
totalUsers: long,
|
||||
totalParticipants: long,
|
||||
totalTags: long,
|
||||
totalWithAlerts: long,
|
||||
totalWithConnectors: long,
|
||||
latestDates: latestDatesSchema,
|
||||
},
|
||||
sec: countSchema,
|
||||
obs: countSchema,
|
||||
main: countSchema,
|
||||
},
|
||||
userActions: { all: { ...countSchema, maxOnACase: long } },
|
||||
comments: { all: { ...countSchema, maxOnACase: long } },
|
||||
alerts: { all: { ...countSchema, maxOnACase: long } },
|
||||
connectors: {
|
||||
all: {
|
||||
all: { totalAttached: long },
|
||||
itsm: { totalAttached: long },
|
||||
sir: { totalAttached: long },
|
||||
jira: { totalAttached: long },
|
||||
resilient: { totalAttached: long },
|
||||
swimlane: { totalAttached: long },
|
||||
maxAttachedToACase: long,
|
||||
},
|
||||
},
|
||||
pushes: {
|
||||
all: { total: long, maxOnACase: long },
|
||||
},
|
||||
configuration: {
|
||||
all: {
|
||||
closure: {
|
||||
manually: long,
|
||||
automatic: long,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
109
x-pack/plugins/cases/server/telemetry/types.ts
Normal file
109
x-pack/plugins/cases/server/telemetry/types.ts
Normal file
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* 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 { ISavedObjectsRepository, Logger } from 'kibana/server';
|
||||
import { MakeSchemaFrom } from 'src/plugins/usage_collection/server';
|
||||
|
||||
export interface Buckets {
|
||||
buckets: Array<{
|
||||
doc_count: number;
|
||||
key: number | string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface Cardinality {
|
||||
value: number;
|
||||
}
|
||||
|
||||
export interface MaxBucketOnCaseAggregation {
|
||||
references: { cases: { max: { value: number } } };
|
||||
}
|
||||
|
||||
export interface ReferencesAggregation {
|
||||
references: { referenceType: { referenceAgg: { value: number } } };
|
||||
}
|
||||
|
||||
export interface CollectTelemetryDataParams {
|
||||
savedObjectsClient: ISavedObjectsRepository;
|
||||
logger: Logger;
|
||||
}
|
||||
|
||||
export interface TypeLong {
|
||||
type: 'long';
|
||||
}
|
||||
|
||||
export interface TypeString {
|
||||
type: 'keyword';
|
||||
}
|
||||
|
||||
export interface Count {
|
||||
total: number;
|
||||
monthly: number;
|
||||
weekly: number;
|
||||
daily: number;
|
||||
}
|
||||
|
||||
export interface Status {
|
||||
open: number;
|
||||
inProgress: number;
|
||||
closed: number;
|
||||
}
|
||||
|
||||
export interface LatestDates {
|
||||
createdAt: string | null;
|
||||
updatedAt: string | null;
|
||||
closedAt: string | null;
|
||||
}
|
||||
|
||||
export interface CasesTelemetry {
|
||||
cases: {
|
||||
all: Count & {
|
||||
status: Status;
|
||||
syncAlertsOn: number;
|
||||
syncAlertsOff: number;
|
||||
totalUsers: number;
|
||||
totalParticipants: number;
|
||||
totalTags: number;
|
||||
totalWithAlerts: number;
|
||||
totalWithConnectors: number;
|
||||
latestDates: LatestDates;
|
||||
};
|
||||
sec: Count;
|
||||
obs: Count;
|
||||
main: Count;
|
||||
};
|
||||
userActions: { all: Count & { maxOnACase: number } };
|
||||
comments: { all: Count & { maxOnACase: number } };
|
||||
alerts: { all: Count & { maxOnACase: number } };
|
||||
connectors: {
|
||||
all: {
|
||||
all: { totalAttached: number };
|
||||
itsm: { totalAttached: number };
|
||||
sir: { totalAttached: number };
|
||||
jira: { totalAttached: number };
|
||||
resilient: { totalAttached: number };
|
||||
swimlane: { totalAttached: number };
|
||||
maxAttachedToACase: number;
|
||||
};
|
||||
};
|
||||
pushes: {
|
||||
all: { total: number; maxOnACase: number };
|
||||
};
|
||||
configuration: {
|
||||
all: {
|
||||
closure: {
|
||||
manually: number;
|
||||
automatic: number;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export type CountSchema = MakeSchemaFrom<Count>;
|
||||
export type StatusSchema = MakeSchemaFrom<Status>;
|
||||
export type LatestDatesSchema = MakeSchemaFrom<LatestDates>;
|
||||
export type CasesTelemetrySchema = MakeSchemaFrom<CasesTelemetry>;
|
|
@ -3413,6 +3413,279 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"cases": {
|
||||
"properties": {
|
||||
"cases": {
|
||||
"properties": {
|
||||
"all": {
|
||||
"properties": {
|
||||
"total": {
|
||||
"type": "long"
|
||||
},
|
||||
"monthly": {
|
||||
"type": "long"
|
||||
},
|
||||
"weekly": {
|
||||
"type": "long"
|
||||
},
|
||||
"daily": {
|
||||
"type": "long"
|
||||
},
|
||||
"status": {
|
||||
"properties": {
|
||||
"open": {
|
||||
"type": "long"
|
||||
},
|
||||
"inProgress": {
|
||||
"type": "long"
|
||||
},
|
||||
"closed": {
|
||||
"type": "long"
|
||||
}
|
||||
}
|
||||
},
|
||||
"syncAlertsOn": {
|
||||
"type": "long"
|
||||
},
|
||||
"syncAlertsOff": {
|
||||
"type": "long"
|
||||
},
|
||||
"totalUsers": {
|
||||
"type": "long"
|
||||
},
|
||||
"totalParticipants": {
|
||||
"type": "long"
|
||||
},
|
||||
"totalTags": {
|
||||
"type": "long"
|
||||
},
|
||||
"totalWithAlerts": {
|
||||
"type": "long"
|
||||
},
|
||||
"totalWithConnectors": {
|
||||
"type": "long"
|
||||
},
|
||||
"latestDates": {
|
||||
"properties": {
|
||||
"createdAt": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"updatedAt": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"closedAt": {
|
||||
"type": "keyword"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"sec": {
|
||||
"properties": {
|
||||
"total": {
|
||||
"type": "long"
|
||||
},
|
||||
"monthly": {
|
||||
"type": "long"
|
||||
},
|
||||
"weekly": {
|
||||
"type": "long"
|
||||
},
|
||||
"daily": {
|
||||
"type": "long"
|
||||
}
|
||||
}
|
||||
},
|
||||
"obs": {
|
||||
"properties": {
|
||||
"total": {
|
||||
"type": "long"
|
||||
},
|
||||
"monthly": {
|
||||
"type": "long"
|
||||
},
|
||||
"weekly": {
|
||||
"type": "long"
|
||||
},
|
||||
"daily": {
|
||||
"type": "long"
|
||||
}
|
||||
}
|
||||
},
|
||||
"main": {
|
||||
"properties": {
|
||||
"total": {
|
||||
"type": "long"
|
||||
},
|
||||
"monthly": {
|
||||
"type": "long"
|
||||
},
|
||||
"weekly": {
|
||||
"type": "long"
|
||||
},
|
||||
"daily": {
|
||||
"type": "long"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"userActions": {
|
||||
"properties": {
|
||||
"all": {
|
||||
"properties": {
|
||||
"total": {
|
||||
"type": "long"
|
||||
},
|
||||
"monthly": {
|
||||
"type": "long"
|
||||
},
|
||||
"weekly": {
|
||||
"type": "long"
|
||||
},
|
||||
"daily": {
|
||||
"type": "long"
|
||||
},
|
||||
"maxOnACase": {
|
||||
"type": "long"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"comments": {
|
||||
"properties": {
|
||||
"all": {
|
||||
"properties": {
|
||||
"total": {
|
||||
"type": "long"
|
||||
},
|
||||
"monthly": {
|
||||
"type": "long"
|
||||
},
|
||||
"weekly": {
|
||||
"type": "long"
|
||||
},
|
||||
"daily": {
|
||||
"type": "long"
|
||||
},
|
||||
"maxOnACase": {
|
||||
"type": "long"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"properties": {
|
||||
"all": {
|
||||
"properties": {
|
||||
"total": {
|
||||
"type": "long"
|
||||
},
|
||||
"monthly": {
|
||||
"type": "long"
|
||||
},
|
||||
"weekly": {
|
||||
"type": "long"
|
||||
},
|
||||
"daily": {
|
||||
"type": "long"
|
||||
},
|
||||
"maxOnACase": {
|
||||
"type": "long"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"connectors": {
|
||||
"properties": {
|
||||
"all": {
|
||||
"properties": {
|
||||
"all": {
|
||||
"properties": {
|
||||
"totalAttached": {
|
||||
"type": "long"
|
||||
}
|
||||
}
|
||||
},
|
||||
"itsm": {
|
||||
"properties": {
|
||||
"totalAttached": {
|
||||
"type": "long"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sir": {
|
||||
"properties": {
|
||||
"totalAttached": {
|
||||
"type": "long"
|
||||
}
|
||||
}
|
||||
},
|
||||
"jira": {
|
||||
"properties": {
|
||||
"totalAttached": {
|
||||
"type": "long"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resilient": {
|
||||
"properties": {
|
||||
"totalAttached": {
|
||||
"type": "long"
|
||||
}
|
||||
}
|
||||
},
|
||||
"swimlane": {
|
||||
"properties": {
|
||||
"totalAttached": {
|
||||
"type": "long"
|
||||
}
|
||||
}
|
||||
},
|
||||
"maxAttachedToACase": {
|
||||
"type": "long"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"pushes": {
|
||||
"properties": {
|
||||
"all": {
|
||||
"properties": {
|
||||
"total": {
|
||||
"type": "long"
|
||||
},
|
||||
"maxOnACase": {
|
||||
"type": "long"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"configuration": {
|
||||
"properties": {
|
||||
"all": {
|
||||
"properties": {
|
||||
"closure": {
|
||||
"properties": {
|
||||
"manually": {
|
||||
"type": "long"
|
||||
},
|
||||
"automatic": {
|
||||
"type": "long"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"cloud": {
|
||||
"properties": {
|
||||
"isCloudEnabled": {
|
||||
|
@ -3714,18 +3987,18 @@
|
|||
"description": "Number of times the user opened the in-product formula help popover."
|
||||
}
|
||||
},
|
||||
"toggle_fullscreen_formula": {
|
||||
"type": "long",
|
||||
"_meta": {
|
||||
"description": "Number of times the user toggled fullscreen mode on formula."
|
||||
}
|
||||
},
|
||||
"toggle_autoapply": {
|
||||
"type": "long",
|
||||
"_meta": {
|
||||
"description": "Number of times the user toggled auto-apply."
|
||||
}
|
||||
},
|
||||
"toggle_fullscreen_formula": {
|
||||
"type": "long",
|
||||
"_meta": {
|
||||
"description": "Number of times the user toggled fullscreen mode on formula."
|
||||
}
|
||||
},
|
||||
"indexpattern_field_info_click": {
|
||||
"type": "long"
|
||||
},
|
||||
|
@ -3967,18 +4240,18 @@
|
|||
"description": "Number of times the user opened the in-product formula help popover."
|
||||
}
|
||||
},
|
||||
"toggle_fullscreen_formula": {
|
||||
"type": "long",
|
||||
"_meta": {
|
||||
"description": "Number of times the user toggled fullscreen mode on formula."
|
||||
}
|
||||
},
|
||||
"toggle_autoapply": {
|
||||
"type": "long",
|
||||
"_meta": {
|
||||
"description": "Number of times the user toggled auto-apply."
|
||||
}
|
||||
},
|
||||
"toggle_fullscreen_formula": {
|
||||
"type": "long",
|
||||
"_meta": {
|
||||
"description": "Number of times the user toggled fullscreen mode on formula."
|
||||
}
|
||||
},
|
||||
"indexpattern_field_info_click": {
|
||||
"type": "long"
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue