diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchclient.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchclient.md index 9a04a1d5817658..1dfb1ab7a0b424 100644 --- a/docs/development/core/server/kibana-plugin-core-server.elasticsearchclient.md +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchclient.md @@ -9,5 +9,5 @@ Client used to query the elasticsearch cluster. Signature: ```typescript -export declare type ElasticsearchClient = Omit; +export declare type ElasticsearchClient = Omit; ``` diff --git a/packages/kbn-doc-links/src/get_doc_links.ts b/packages/kbn-doc-links/src/get_doc_links.ts index d73760b280d496..03948af6379104 100644 --- a/packages/kbn-doc-links/src/get_doc_links.ts +++ b/packages/kbn-doc-links/src/get_doc_links.ts @@ -196,6 +196,7 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => { std_dev: `${ELASTICSEARCH_DOCS}search-aggregations-metrics-extendedstats-aggregation.html`, sum: `${ELASTICSEARCH_DOCS}search-aggregations-metrics-sum-aggregation.html`, top_hits: `${ELASTICSEARCH_DOCS}search-aggregations-metrics-top-hits-aggregation.html`, + top_metrics: `${ELASTICSEARCH_DOCS}search-aggregations-metrics-top-metrics.html`, }, runtimeFields: { overview: `${ELASTICSEARCH_DOCS}runtime.html`, diff --git a/packages/kbn-type-summarizer/src/lib/source_mapper.ts b/packages/kbn-type-summarizer/src/lib/source_mapper.ts index f6075684e04a6d..0b0e69571469c3 100644 --- a/packages/kbn-type-summarizer/src/lib/source_mapper.ts +++ b/packages/kbn-type-summarizer/src/lib/source_mapper.ts @@ -17,6 +17,8 @@ import { tryReadFile } from './helpers/fs'; import { parseJson } from './helpers/json'; import { isNodeModule } from './is_node_module'; +type SourceMapConsumerEntry = [ts.SourceFile, BasicSourceMapConsumer | undefined]; + export class SourceMapper { static async forSourceFiles( log: Logger, @@ -24,10 +26,8 @@ export class SourceMapper { repoRelativePackageDir: string, sourceFiles: readonly ts.SourceFile[] ) { - const consumers = new Map(); - - await Promise.all( - sourceFiles.map(async (sourceFile) => { + const entries = await Promise.all( + sourceFiles.map(async (sourceFile): Promise => { if (isNodeModule(dtsDir, sourceFile.fileName)) { return; } @@ -35,8 +35,7 @@ export class SourceMapper { const text = sourceFile.getText(); const match = text.match(/^\/\/#\s*sourceMappingURL=(.*)/im); if (!match) { - consumers.set(sourceFile, undefined); - return; + return [sourceFile, undefined]; } const relSourceFile = Path.relative(process.cwd(), sourceFile.fileName); @@ -50,11 +49,16 @@ export class SourceMapper { } const json = parseJson(sourceJson, `source map at [${relSourceMapPath}]`); - consumers.set(sourceFile, await new SourceMapConsumer(json)); - log.debug('loaded sourcemap for', relSourceFile); + return [sourceFile, await new SourceMapConsumer(json)]; }) ); + const consumers = new Map(entries.filter((e): e is SourceMapConsumerEntry => !!e)); + log.debug( + 'loaded sourcemaps for', + Array.from(consumers.keys()).map((s) => Path.relative(process.cwd(), s.fileName)) + ); + return new SourceMapper(consumers, repoRelativePackageDir); } diff --git a/packages/kbn-type-summarizer/tests/integration_tests/class.test.ts b/packages/kbn-type-summarizer/tests/integration_tests/class.test.ts index 84c1ee80c5f166..d2b39ab69d47bf 100644 --- a/packages/kbn-type-summarizer/tests/integration_tests/class.test.ts +++ b/packages/kbn-type-summarizer/tests/integration_tests/class.test.ts @@ -69,7 +69,7 @@ it('prints basic class correctly', async () => { } `); expect(output.logs).toMatchInlineSnapshot(` - "debug loaded sourcemap for packages/kbn-type-summarizer/tests/__tmp__/dist_dts/index.d.ts + "debug loaded sourcemaps for [ 'packages/kbn-type-summarizer/tests/__tmp__/dist_dts/index.d.ts' ] debug Ignoring 1 global declarations for \\"Record\\" debug Ignoring 5 global declarations for \\"Promise\\" " diff --git a/packages/kbn-type-summarizer/tests/integration_tests/function.test.ts b/packages/kbn-type-summarizer/tests/integration_tests/function.test.ts index 6afc04afe8faad..ec15d941ca1536 100644 --- a/packages/kbn-type-summarizer/tests/integration_tests/function.test.ts +++ b/packages/kbn-type-summarizer/tests/integration_tests/function.test.ts @@ -74,8 +74,10 @@ it('prints the function declaration, including comments', async () => { } `); expect(result.logs).toMatchInlineSnapshot(` - "debug loaded sourcemap for packages/kbn-type-summarizer/tests/__tmp__/dist_dts/bar.d.ts - debug loaded sourcemap for packages/kbn-type-summarizer/tests/__tmp__/dist_dts/index.d.ts + "debug loaded sourcemaps for [ + 'packages/kbn-type-summarizer/tests/__tmp__/dist_dts/bar.d.ts', + 'packages/kbn-type-summarizer/tests/__tmp__/dist_dts/index.d.ts' + ] " `); }); diff --git a/packages/kbn-type-summarizer/tests/integration_tests/import_boundary.test.ts b/packages/kbn-type-summarizer/tests/integration_tests/import_boundary.test.ts index f23b6c3656d508..35cf08e2973598 100644 --- a/packages/kbn-type-summarizer/tests/integration_tests/import_boundary.test.ts +++ b/packages/kbn-type-summarizer/tests/integration_tests/import_boundary.test.ts @@ -52,7 +52,7 @@ it('output type links to named import from node modules', async () => { } `); expect(output.logs).toMatchInlineSnapshot(` - "debug loaded sourcemap for packages/kbn-type-summarizer/tests/__tmp__/dist_dts/index.d.ts + "debug loaded sourcemaps for [ 'packages/kbn-type-summarizer/tests/__tmp__/dist_dts/index.d.ts' ] " `); }); @@ -84,7 +84,7 @@ it('output type links to default import from node modules', async () => { } `); expect(output.logs).toMatchInlineSnapshot(` - "debug loaded sourcemap for packages/kbn-type-summarizer/tests/__tmp__/dist_dts/index.d.ts + "debug loaded sourcemaps for [ 'packages/kbn-type-summarizer/tests/__tmp__/dist_dts/index.d.ts' ] " `); }); diff --git a/packages/kbn-type-summarizer/tests/integration_tests/interface.test.ts b/packages/kbn-type-summarizer/tests/integration_tests/interface.test.ts index da53e91302eef0..cc821f1c9fc90f 100644 --- a/packages/kbn-type-summarizer/tests/integration_tests/interface.test.ts +++ b/packages/kbn-type-summarizer/tests/integration_tests/interface.test.ts @@ -55,7 +55,7 @@ it('prints the whole interface, including comments', async () => { } `); expect(result.logs).toMatchInlineSnapshot(` - "debug loaded sourcemap for packages/kbn-type-summarizer/tests/__tmp__/dist_dts/index.d.ts + "debug loaded sourcemaps for [ 'packages/kbn-type-summarizer/tests/__tmp__/dist_dts/index.d.ts' ] debug Ignoring 5 global declarations for \\"Promise\\" " `); diff --git a/packages/kbn-type-summarizer/tests/integration_tests/references.test.ts b/packages/kbn-type-summarizer/tests/integration_tests/references.test.ts index 1733b43694000d..796bcd5fac3d1d 100644 --- a/packages/kbn-type-summarizer/tests/integration_tests/references.test.ts +++ b/packages/kbn-type-summarizer/tests/integration_tests/references.test.ts @@ -59,9 +59,11 @@ it('collects references from source files which contribute to result', async () } `); expect(result.logs).toMatchInlineSnapshot(` - "debug loaded sourcemap for packages/kbn-type-summarizer/tests/__tmp__/dist_dts/files/foo.d.ts - debug loaded sourcemap for packages/kbn-type-summarizer/tests/__tmp__/dist_dts/files/index.d.ts - debug loaded sourcemap for packages/kbn-type-summarizer/tests/__tmp__/dist_dts/index.d.ts + "debug loaded sourcemaps for [ + 'packages/kbn-type-summarizer/tests/__tmp__/dist_dts/files/foo.d.ts', + 'packages/kbn-type-summarizer/tests/__tmp__/dist_dts/files/index.d.ts', + 'packages/kbn-type-summarizer/tests/__tmp__/dist_dts/index.d.ts' + ] debug Ignoring 5 global declarations for \\"Promise\\" debug Ignoring 4 global declarations for \\"Symbol\\" debug Ignoring 2 global declarations for \\"Component\\" diff --git a/packages/kbn-type-summarizer/tests/integration_tests/type_alias.test.ts b/packages/kbn-type-summarizer/tests/integration_tests/type_alias.test.ts index 79c2ea69b94777..f099bad9f3de6a 100644 --- a/packages/kbn-type-summarizer/tests/integration_tests/type_alias.test.ts +++ b/packages/kbn-type-summarizer/tests/integration_tests/type_alias.test.ts @@ -36,7 +36,7 @@ it('prints basic type alias', async () => { } `); expect(output.logs).toMatchInlineSnapshot(` - "debug loaded sourcemap for packages/kbn-type-summarizer/tests/__tmp__/dist_dts/index.d.ts + "debug loaded sourcemaps for [ 'packages/kbn-type-summarizer/tests/__tmp__/dist_dts/index.d.ts' ] " `); }); diff --git a/packages/kbn-type-summarizer/tests/integration_tests/variables.test.ts b/packages/kbn-type-summarizer/tests/integration_tests/variables.test.ts index daa6abcc34c594..c51c9b0098b6c6 100644 --- a/packages/kbn-type-summarizer/tests/integration_tests/variables.test.ts +++ b/packages/kbn-type-summarizer/tests/integration_tests/variables.test.ts @@ -62,7 +62,7 @@ it('prints basic variable exports with sourcemaps', async () => { } `); expect(output.logs).toMatchInlineSnapshot(` - "debug loaded sourcemap for packages/kbn-type-summarizer/tests/__tmp__/dist_dts/index.d.ts + "debug loaded sourcemaps for [ 'packages/kbn-type-summarizer/tests/__tmp__/dist_dts/index.d.ts' ] " `); }); diff --git a/src/core/server/elasticsearch/client/types.ts b/src/core/server/elasticsearch/client/types.ts index 68fbc87193074f..17248e491962d5 100644 --- a/src/core/server/elasticsearch/client/types.ts +++ b/src/core/server/elasticsearch/client/types.ts @@ -15,7 +15,7 @@ import type { Client } from '@elastic/elasticsearch'; */ export type ElasticsearchClient = Omit< Client, - 'connectionPool' | 'serializer' | 'extend' | 'child' | 'close' | 'diagnostic' + 'connectionPool' | 'serializer' | 'extend' | 'close' | 'diagnostic' >; /** diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 5fe1942ed8453b..6d5b06346225b7 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -886,7 +886,7 @@ export { EcsEventOutcome } export { EcsEventType } // @public -export type ElasticsearchClient = Omit; +export type ElasticsearchClient = Omit; // @public export type ElasticsearchClientConfig = Pick & { diff --git a/src/fixtures/telemetry_collectors/imported_interface_from_export/index.ts b/src/fixtures/telemetry_collectors/imported_interface_from_export/index.ts index 222a89bf322984..991f8336e70202 100644 --- a/src/fixtures/telemetry_collectors/imported_interface_from_export/index.ts +++ b/src/fixtures/telemetry_collectors/imported_interface_from_export/index.ts @@ -11,7 +11,7 @@ import { createUsageCollectionSetupMock } from '../../../plugins/usage_collectio const { makeUsageCollector } = createUsageCollectionSetupMock(); -export const myCollector = makeUsageCollector({ +export const myCollector = makeUsageCollector({ type: 'importing_from_export_collector', isReady: () => true, fetch() { diff --git a/src/fixtures/telemetry_collectors/stats_collector.ts b/src/fixtures/telemetry_collectors/stats_collector.ts index c8f513a07253ba..6046973f42e849 100644 --- a/src/fixtures/telemetry_collectors/stats_collector.ts +++ b/src/fixtures/telemetry_collectors/stats_collector.ts @@ -19,7 +19,7 @@ interface Usage { * We should collect them when the schema is defined. */ -export const myCollectorWithSchema = makeStatsCollector({ +export const myCollectorWithSchema = makeStatsCollector({ type: 'my_stats_collector_with_schema', isReady: () => true, fetch() { diff --git a/src/plugins/dashboard/tsconfig.json b/src/plugins/dashboard/tsconfig.json index 55049447aee576..862bed9d667a01 100644 --- a/src/plugins/dashboard/tsconfig.json +++ b/src/plugins/dashboard/tsconfig.json @@ -24,6 +24,7 @@ { "path": "../navigation/tsconfig.json" }, { "path": "../saved_objects_tagging_oss/tsconfig.json" }, { "path": "../saved_objects/tsconfig.json" }, + { "path": "../screenshot_mode/tsconfig.json" }, { "path": "../ui_actions/tsconfig.json" }, { "path": "../charts/tsconfig.json" }, { "path": "../discover/tsconfig.json" }, diff --git a/src/plugins/data/common/search/aggs/agg_types.ts b/src/plugins/data/common/search/aggs/agg_types.ts index 01ccd401c07acc..d7750c48016cd5 100644 --- a/src/plugins/data/common/search/aggs/agg_types.ts +++ b/src/plugins/data/common/search/aggs/agg_types.ts @@ -36,6 +36,7 @@ export const getAggTypes = () => ({ { name: METRIC_TYPES.PERCENTILES, fn: metrics.getPercentilesMetricAgg }, { name: METRIC_TYPES.PERCENTILE_RANKS, fn: metrics.getPercentileRanksMetricAgg }, { name: METRIC_TYPES.TOP_HITS, fn: metrics.getTopHitMetricAgg }, + { name: METRIC_TYPES.TOP_METRICS, fn: metrics.getTopMetricsMetricAgg }, { name: METRIC_TYPES.DERIVATIVE, fn: metrics.getDerivativeMetricAgg }, { name: METRIC_TYPES.CUMULATIVE_SUM, fn: metrics.getCumulativeSumMetricAgg }, { name: METRIC_TYPES.MOVING_FN, fn: metrics.getMovingAvgMetricAgg }, @@ -109,4 +110,5 @@ export const getAggTypesFunctions = () => [ metrics.aggStdDeviation, metrics.aggSum, metrics.aggTopHit, + metrics.aggTopMetrics, ]; diff --git a/src/plugins/data/common/search/aggs/aggs_service.test.ts b/src/plugins/data/common/search/aggs/aggs_service.test.ts index b7237c7b801342..6090e965489e74 100644 --- a/src/plugins/data/common/search/aggs/aggs_service.test.ts +++ b/src/plugins/data/common/search/aggs/aggs_service.test.ts @@ -95,6 +95,7 @@ describe('Aggs service', () => { "percentiles", "percentile_ranks", "top_hits", + "top_metrics", "derivative", "cumulative_sum", "moving_avg", @@ -147,6 +148,7 @@ describe('Aggs service', () => { "percentiles", "percentile_ranks", "top_hits", + "top_metrics", "derivative", "cumulative_sum", "moving_avg", diff --git a/src/plugins/data/common/search/aggs/metrics/index.ts b/src/plugins/data/common/search/aggs/metrics/index.ts index d37b74a1a28aef..4d80e363251001 100644 --- a/src/plugins/data/common/search/aggs/metrics/index.ts +++ b/src/plugins/data/common/search/aggs/metrics/index.ts @@ -56,3 +56,5 @@ export * from './sum_fn'; export * from './sum'; export * from './top_hit_fn'; export * from './top_hit'; +export * from './top_metrics'; +export * from './top_metrics_fn'; diff --git a/src/plugins/data/common/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts b/src/plugins/data/common/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts index 478b8309272e39..1fe703313218d8 100644 --- a/src/plugins/data/common/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts +++ b/src/plugins/data/common/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts @@ -15,6 +15,7 @@ import { parentPipelineAggWriter } from './parent_pipeline_agg_writer'; const metricAggFilter = [ '!top_hits', + '!top_metrics', '!percentiles', '!percentile_ranks', '!median', diff --git a/src/plugins/data/common/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts b/src/plugins/data/common/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts index f8c903b8cfe420..243a119847a2cf 100644 --- a/src/plugins/data/common/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts +++ b/src/plugins/data/common/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts @@ -13,6 +13,7 @@ import { IMetricAggConfig, MetricAggParam } from '../metric_agg_type'; const metricAggFilter: string[] = [ '!top_hits', + '!top_metrics', '!percentiles', '!percentile_ranks', '!median', diff --git a/src/plugins/data/common/search/aggs/metrics/metric_agg_type.ts b/src/plugins/data/common/search/aggs/metrics/metric_agg_type.ts index 6ddb0fdd9410d4..5237c1ecffe584 100644 --- a/src/plugins/data/common/search/aggs/metrics/metric_agg_type.ts +++ b/src/plugins/data/common/search/aggs/metrics/metric_agg_type.ts @@ -22,6 +22,7 @@ export interface MetricAggParam extends AggParamType { filterFieldTypes?: FieldTypes; onlyAggregatable?: boolean; + scriptable?: boolean; } const metricType = 'metrics'; diff --git a/src/plugins/data/common/search/aggs/metrics/metric_agg_types.ts b/src/plugins/data/common/search/aggs/metrics/metric_agg_types.ts index a308153b3816b7..eed6d0a378fc2d 100644 --- a/src/plugins/data/common/search/aggs/metrics/metric_agg_types.ts +++ b/src/plugins/data/common/search/aggs/metrics/metric_agg_types.ts @@ -27,6 +27,7 @@ export enum METRIC_TYPES { SERIAL_DIFF = 'serial_diff', SUM = 'sum', TOP_HITS = 'top_hits', + TOP_METRICS = 'top_metrics', PERCENTILES = 'percentiles', PERCENTILE_RANKS = 'percentile_ranks', STD_DEV = 'std_dev', diff --git a/src/plugins/data/common/search/aggs/metrics/top_metrics.test.ts b/src/plugins/data/common/search/aggs/metrics/top_metrics.test.ts new file mode 100644 index 00000000000000..9bf5f581aa0a4a --- /dev/null +++ b/src/plugins/data/common/search/aggs/metrics/top_metrics.test.ts @@ -0,0 +1,194 @@ +/* + * 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 { getTopMetricsMetricAgg } from './top_metrics'; +import { AggConfigs } from '../agg_configs'; +import { mockAggTypesRegistry } from '../test_helpers'; +import { IMetricAggConfig } from './metric_agg_type'; +import { KBN_FIELD_TYPES } from '../../../../common'; + +describe('Top metrics metric', () => { + let aggConfig: IMetricAggConfig; + + const init = ({ + fieldName = 'field', + fieldType = KBN_FIELD_TYPES.NUMBER, + sortFieldName = 'sortField', + sortFieldType = KBN_FIELD_TYPES.NUMBER, + sortOrder = 'desc', + size = 1, + }: any) => { + const typesRegistry = mockAggTypesRegistry(); + const field = { + name: fieldName, + displayName: fieldName, + type: fieldType, + }; + + const sortField = { + name: sortFieldName, + displayName: sortFieldName, + type: sortFieldType, + }; + + const params = { + size, + field: field.name, + sortField: sortField.name, + sortOrder: { + value: sortOrder, + }, + }; + + const indexPattern = { + id: '1234', + title: 'logstash-*', + fields: { + getByName: (name: string) => { + if (name === sortFieldName) return sortField; + if (name === fieldName) return field; + return null; + }, + filter: () => [field, sortField], + }, + } as any; + + const aggConfigs = new AggConfigs( + indexPattern, + [ + { + id: '1', + type: 'top_metrics', + schema: 'metric', + params, + }, + ], + { typesRegistry } + ); + + // Grab the aggConfig off the vis (we don't actually use the vis for anything else) + aggConfig = aggConfigs.aggs[0] as IMetricAggConfig; + }; + + it('should return a label prefixed with Last if sorting in descending order', () => { + init({ fieldName: 'bytes', sortFieldName: '@timestamp' }); + expect(getTopMetricsMetricAgg().makeLabel(aggConfig)).toEqual( + 'Last "bytes" value by "@timestamp"' + ); + }); + + it('should return a label prefixed with First if sorting in ascending order', () => { + init({ + fieldName: 'bytes', + sortFieldName: '@timestamp', + sortOrder: 'asc', + }); + expect(getTopMetricsMetricAgg().makeLabel(aggConfig)).toEqual( + 'First "bytes" value by "@timestamp"' + ); + }); + + it('should return a label with size if larger then 1', () => { + init({ + fieldName: 'bytes', + sortFieldName: '@timestamp', + sortOrder: 'asc', + size: 3, + }); + expect(getTopMetricsMetricAgg().makeLabel(aggConfig)).toEqual( + 'First 3 "bytes" values by "@timestamp"' + ); + }); + + it('should return a fieldName in getValueBucketPath', () => { + init({ + fieldName: 'bytes', + sortFieldName: '@timestamp', + sortOrder: 'asc', + size: 3, + }); + expect(getTopMetricsMetricAgg().getValueBucketPath(aggConfig)).toEqual('1[bytes]'); + }); + + it('produces the expected expression ast', () => { + init({ fieldName: 'machine.os', sortFieldName: '@timestamp' }); + expect(aggConfig.toExpressionAst()).toMatchInlineSnapshot(` + Object { + "chain": Array [ + Object { + "arguments": Object { + "enabled": Array [ + true, + ], + "field": Array [ + "machine.os", + ], + "id": Array [ + "1", + ], + "schema": Array [ + "metric", + ], + "size": Array [ + 1, + ], + "sortField": Array [ + "@timestamp", + ], + "sortOrder": Array [ + "desc", + ], + }, + "function": "aggTopMetrics", + "type": "function", + }, + ], + "type": "expression", + } + `); + }); + + describe('gets value from top metrics bucket', () => { + it('should return null if there is no hits', () => { + const bucket = { + '1': { + top: [], + }, + }; + + init({ fieldName: 'bytes' }); + expect(getTopMetricsMetricAgg().getValue(aggConfig, bucket)).toBe(null); + }); + + it('should return a single value if there is a single hit', () => { + const bucket = { + '1': { + top: [{ sort: [3], metrics: { bytes: 1024 } }], + }, + }; + + init({ fieldName: 'bytes' }); + expect(getTopMetricsMetricAgg().getValue(aggConfig, bucket)).toBe(1024); + }); + + it('should return an array of values if there is a multiple results', () => { + const bucket = { + '1': { + top: [ + { sort: [3], metrics: { bytes: 1024 } }, + { sort: [2], metrics: { bytes: 512 } }, + { sort: [1], metrics: { bytes: 256 } }, + ], + }, + }; + + init({ fieldName: 'bytes' }); + expect(getTopMetricsMetricAgg().getValue(aggConfig, bucket)).toEqual([1024, 512, 256]); + }); + }); +}); diff --git a/src/plugins/data/common/search/aggs/metrics/top_metrics.ts b/src/plugins/data/common/search/aggs/metrics/top_metrics.ts new file mode 100644 index 00000000000000..2079925e0435b1 --- /dev/null +++ b/src/plugins/data/common/search/aggs/metrics/top_metrics.ts @@ -0,0 +1,155 @@ +/* + * 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 _ from 'lodash'; +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { i18n } from '@kbn/i18n'; +import { aggTopMetricsFnName } from './top_metrics_fn'; +import { IMetricAggConfig, MetricAggType } from './metric_agg_type'; +import { METRIC_TYPES } from './metric_agg_types'; +import { KBN_FIELD_TYPES } from '../../../../common'; +import { BaseAggParams } from '../types'; + +export interface AggParamsTopMetrics extends BaseAggParams { + field: string; + sortField?: string; + sortOrder?: 'desc' | 'asc'; + size?: number; +} + +export const getTopMetricsMetricAgg = () => { + return new MetricAggType({ + name: METRIC_TYPES.TOP_METRICS, + expressionName: aggTopMetricsFnName, + title: i18n.translate('data.search.aggs.metrics.topMetricsTitle', { + defaultMessage: 'Top metrics', + }), + makeLabel(aggConfig) { + const isDescOrder = aggConfig.getParam('sortOrder').value === 'desc'; + const size = aggConfig.getParam('size'); + const field = aggConfig.getParam('field'); + const sortField = aggConfig.getParam('sortField'); + + if (isDescOrder) { + if (size > 1) { + return i18n.translate('data.search.aggs.metrics.topMetrics.descWithSizeLabel', { + defaultMessage: `Last {size} "{fieldName}" values by "{sortField}"`, + values: { + size, + fieldName: field?.displayName, + sortField: sortField?.displayName ?? '_score', + }, + }); + } else { + return i18n.translate('data.search.aggs.metrics.topMetrics.descNoSizeLabel', { + defaultMessage: `Last "{fieldName}" value by "{sortField}"`, + values: { + fieldName: field?.displayName, + sortField: sortField?.displayName ?? '_score', + }, + }); + } + } else { + if (size > 1) { + return i18n.translate('data.search.aggs.metrics.topMetrics.ascWithSizeLabel', { + defaultMessage: `First {size} "{fieldName}" values by "{sortField}"`, + values: { + size, + fieldName: field?.displayName, + sortField: sortField?.displayName ?? '_score', + }, + }); + } else { + return i18n.translate('data.search.aggs.metrics.topMetrics.ascNoSizeLabel', { + defaultMessage: `First "{fieldName}" value by "{sortField}"`, + values: { + fieldName: field?.displayName, + sortField: sortField?.displayName ?? '_score', + }, + }); + } + } + }, + params: [ + { + name: 'field', + type: 'field', + scriptable: false, + filterFieldTypes: [ + KBN_FIELD_TYPES.STRING, + KBN_FIELD_TYPES.IP, + KBN_FIELD_TYPES.BOOLEAN, + KBN_FIELD_TYPES.NUMBER, + KBN_FIELD_TYPES.DATE, + ], + write(agg, output) { + const field = agg.getParam('field'); + output.params.metrics = { field: field.name }; + }, + }, + { + name: 'size', + default: 1, + }, + { + name: 'sortField', + type: 'field', + scriptable: false, + filterFieldTypes: [KBN_FIELD_TYPES.NUMBER, KBN_FIELD_TYPES.DATE], + default(agg: IMetricAggConfig) { + return agg.getIndexPattern().timeFieldName; + }, + write: _.noop, // prevent default write, it is handled below + }, + { + name: 'sortOrder', + type: 'optioned', + default: 'desc', + options: [ + { + text: i18n.translate('data.search.aggs.metrics.topMetrics.descendingLabel', { + defaultMessage: 'Descending', + }), + value: 'desc', + }, + { + text: i18n.translate('data.search.aggs.metrics.topMetrics.ascendingLabel', { + defaultMessage: 'Ascending', + }), + value: 'asc', + }, + ], + write(agg, output) { + const sortField = agg.params.sortField; + const sortOrder = agg.params.sortOrder; + + if (sortField && sortOrder) { + output.params.sort = { + [sortField.name]: sortOrder.value, + }; + } else { + output.params.sort = '_score'; + } + }, + }, + ], + // override is needed to support top_metrics as an orderAgg of terms agg + getValueBucketPath(agg) { + const field = agg.getParam('field').name; + return `${agg.id}[${field}]`; + }, + getValue(agg, aggregate: Record) { + const metricFieldName = agg.getParam('field').name; + const results = aggregate[agg.id]?.top.map((result) => result.metrics[metricFieldName]) ?? []; + + if (results.length === 0) return null; + if (results.length === 1) return results[0]; + return results; + }, + }); +}; diff --git a/src/plugins/data/common/search/aggs/metrics/top_metrics_fn.test.ts b/src/plugins/data/common/search/aggs/metrics/top_metrics_fn.test.ts new file mode 100644 index 00000000000000..848fccda283faa --- /dev/null +++ b/src/plugins/data/common/search/aggs/metrics/top_metrics_fn.test.ts @@ -0,0 +1,79 @@ +/* + * 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 { functionWrapper } from '../test_helpers'; +import { aggTopMetrics } from './top_metrics_fn'; + +describe('agg_expression_functions', () => { + describe('aggTopMetrics', () => { + const fn = functionWrapper(aggTopMetrics()); + + test('fills in defaults when only required args are provided', () => { + const actual = fn({ + field: 'machine.os.keyword', + }); + expect(actual).toMatchInlineSnapshot(` + Object { + "type": "agg_type", + "value": Object { + "enabled": true, + "id": undefined, + "params": Object { + "customLabel": undefined, + "field": "machine.os.keyword", + "json": undefined, + "size": undefined, + "sortField": undefined, + "sortOrder": undefined, + }, + "schema": undefined, + "type": "top_metrics", + }, + } + `); + }); + + test('includes optional params when they are provided', () => { + const actual = fn({ + id: '1', + enabled: false, + schema: 'whatever', + field: 'machine.os.keyword', + sortOrder: 'asc', + size: 6, + sortField: 'bytes', + }); + + expect(actual.value).toMatchInlineSnapshot(` + Object { + "enabled": false, + "id": "1", + "params": Object { + "customLabel": undefined, + "field": "machine.os.keyword", + "json": undefined, + "size": 6, + "sortField": "bytes", + "sortOrder": "asc", + }, + "schema": "whatever", + "type": "top_metrics", + } + `); + }); + + test('correctly parses json string argument', () => { + const actual = fn({ + field: 'machine.os.keyword', + json: '{ "foo": true }', + }); + + expect(actual.value.params.json).toEqual('{ "foo": true }'); + }); + }); +}); diff --git a/src/plugins/data/common/search/aggs/metrics/top_metrics_fn.ts b/src/plugins/data/common/search/aggs/metrics/top_metrics_fn.ts new file mode 100644 index 00000000000000..6fe9ba97fe4483 --- /dev/null +++ b/src/plugins/data/common/search/aggs/metrics/top_metrics_fn.ts @@ -0,0 +1,106 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; +import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; + +export const aggTopMetricsFnName = 'aggTopMetrics'; + +type Input = any; +type AggArgs = AggExpressionFunctionArgs; +type Output = AggExpressionType; +type FunctionDefinition = ExpressionFunctionDefinition< + typeof aggTopMetricsFnName, + Input, + AggArgs, + Output +>; + +export const aggTopMetrics = (): FunctionDefinition => ({ + name: aggTopMetricsFnName, + help: i18n.translate('data.search.aggs.function.metrics.topMetrics.help', { + defaultMessage: 'Generates a serialized aggregation configuration for Top metrics.', + }), + type: 'agg_type', + args: { + id: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.topMetrics.id.help', { + defaultMessage: 'ID for this aggregation', + }), + }, + enabled: { + types: ['boolean'], + default: true, + help: i18n.translate('data.search.aggs.metrics.topMetrics.enabled.help', { + defaultMessage: 'Specifies whether this aggregation should be enabled', + }), + }, + schema: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.topMetrics.schema.help', { + defaultMessage: 'Schema to use for this aggregation', + }), + }, + field: { + types: ['string'], + required: true, + help: i18n.translate('data.search.aggs.metrics.topMetrics.field.help', { + defaultMessage: 'Field to use for this aggregation', + }), + }, + size: { + types: ['number'], + help: i18n.translate('data.search.aggs.metrics.topMetrics.size.help', { + defaultMessage: 'Number of top values to retrieve', + }), + }, + sortOrder: { + types: ['string'], + options: ['desc', 'asc'], + help: i18n.translate('data.search.aggs.metrics.topMetrics.sortOrder.help', { + defaultMessage: 'Order in which to return the results: asc or desc', + }), + }, + sortField: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.topMetrics.sortField.help', { + defaultMessage: 'Field to order results by', + }), + }, + json: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.topMetrics.json.help', { + defaultMessage: 'Advanced JSON to include when the aggregation is sent to Elasticsearch', + }), + }, + customLabel: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.topMetrics.customLabel.help', { + defaultMessage: 'Represents a custom label for this aggregation', + }), + }, + }, + fn: (input, args) => { + const { id, enabled, schema, ...rest } = args; + + return { + type: 'agg_type', + value: { + id, + enabled, + schema, + type: METRIC_TYPES.TOP_METRICS, + params: { + ...rest, + }, + }, + }; + }, +}); diff --git a/src/plugins/data/common/search/aggs/param_types/field.ts b/src/plugins/data/common/search/aggs/param_types/field.ts index 940fdafd548753..b56787121f7248 100644 --- a/src/plugins/data/common/search/aggs/param_types/field.ts +++ b/src/plugins/data/common/search/aggs/param_types/field.ts @@ -43,6 +43,7 @@ export class FieldParamType extends BaseParamType { this.filterFieldTypes = config.filterFieldTypes || '*'; this.onlyAggregatable = config.onlyAggregatable !== false; + this.scriptable = config.scriptable !== false; this.filterField = config.filterField; if (!config.write) { diff --git a/src/plugins/data/common/search/aggs/types.ts b/src/plugins/data/common/search/aggs/types.ts index cf9a6123b14c8d..edc328bcb5099a 100644 --- a/src/plugins/data/common/search/aggs/types.ts +++ b/src/plugins/data/common/search/aggs/types.ts @@ -93,6 +93,8 @@ import { import { AggParamsSampler } from './buckets/sampler'; import { AggParamsDiversifiedSampler } from './buckets/diversified_sampler'; import { AggParamsSignificantText } from './buckets/significant_text'; +import { AggParamsTopMetrics } from './metrics/top_metrics'; +import { aggTopMetrics } from './metrics/top_metrics_fn'; export type { IAggConfig, AggConfigSerialized } from './agg_config'; export type { CreateAggConfigParams, IAggConfigs } from './agg_configs'; @@ -187,6 +189,7 @@ export interface AggParamsMapping { [METRIC_TYPES.PERCENTILES]: AggParamsPercentiles; [METRIC_TYPES.SERIAL_DIFF]: AggParamsSerialDiff; [METRIC_TYPES.TOP_HITS]: AggParamsTopHit; + [METRIC_TYPES.TOP_METRICS]: AggParamsTopMetrics; } /** @@ -229,4 +232,5 @@ export interface AggFunctionsMapping { aggStdDeviation: ReturnType; aggSum: ReturnType; aggTopHit: ReturnType; + aggTopMetrics: ReturnType; } diff --git a/src/plugins/data/public/search/aggs/aggs_service.test.ts b/src/plugins/data/public/search/aggs/aggs_service.test.ts index 101c2c909c7e1d..83328e196fa0a5 100644 --- a/src/plugins/data/public/search/aggs/aggs_service.test.ts +++ b/src/plugins/data/public/search/aggs/aggs_service.test.ts @@ -54,7 +54,7 @@ describe('AggsService - public', () => { service.setup(setupDeps); const start = service.start(startDeps); expect(start.types.getAll().buckets.length).toBe(16); - expect(start.types.getAll().metrics.length).toBe(23); + expect(start.types.getAll().metrics.length).toBe(24); }); test('registers custom agg types', () => { @@ -71,7 +71,7 @@ describe('AggsService - public', () => { const start = service.start(startDeps); expect(start.types.getAll().buckets.length).toBe(17); expect(start.types.getAll().buckets.some(({ name }) => name === 'foo')).toBe(true); - expect(start.types.getAll().metrics.length).toBe(24); + expect(start.types.getAll().metrics.length).toBe(25); expect(start.types.getAll().metrics.some(({ name }) => name === 'bar')).toBe(true); }); }); diff --git a/src/plugins/home/kibana.json b/src/plugins/home/kibana.json index d8c09ab5e80c6a..02b33e814e2a1c 100644 --- a/src/plugins/home/kibana.json +++ b/src/plugins/home/kibana.json @@ -8,6 +8,6 @@ "server": true, "ui": true, "requiredPlugins": ["dataViews", "share", "urlForwarding"], - "optionalPlugins": ["usageCollection", "telemetry", "customIntegrations"], + "optionalPlugins": ["usageCollection", "customIntegrations"], "requiredBundles": ["kibanaReact"] } diff --git a/src/plugins/home/public/application/components/__snapshots__/home.test.tsx.snap b/src/plugins/home/public/application/components/__snapshots__/home.test.tsx.snap index ab6ad1b6cc0c55..43d8f935221b36 100644 --- a/src/plugins/home/public/application/components/__snapshots__/home.test.tsx.snap +++ b/src/plugins/home/public/application/components/__snapshots__/home.test.tsx.snap @@ -374,202 +374,6 @@ exports[`home isNewKibanaInstance should set isNewKibanaInstance to false when t exports[`home isNewKibanaInstance should set isNewKibanaInstance to true when there are no index patterns 1`] = ` `; diff --git a/src/plugins/home/public/application/components/__snapshots__/welcome.test.tsx.snap b/src/plugins/home/public/application/components/__snapshots__/welcome.test.tsx.snap index 17f7d2520e8621..861e0ee895887c 100644 --- a/src/plugins/home/public/application/components/__snapshots__/welcome.test.tsx.snap +++ b/src/plugins/home/public/application/components/__snapshots__/welcome.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`should render a Welcome screen with no telemetry disclaimer 1`] = ` +exports[`should render a Welcome screen 1`] = `
`; - -exports[`should render a Welcome screen with the telemetry disclaimer 1`] = ` - -
-
-
- - - - - -

- -

-
- -
-
-
- - - - - - - - - - - - - - - - - -
-
-
-`; - -exports[`should render a Welcome screen with the telemetry disclaimer when optIn is false 1`] = ` - -
-
-
- - - - - -

- -

-
- -
-
-
- - - - - - - - - - - - - - - - - -
-
-
-`; - -exports[`should render a Welcome screen with the telemetry disclaimer when optIn is true 1`] = ` - -
-
-
- - - - - -

- -

-
- -
-
-
- - - - - - - - - - - - - - - - - -
-
-
-`; - -exports[`should render a Welcome screen without the opt in/out link when user cannot change optIn status 1`] = ` - -
-
-
- - - - - -

- -

-
- -
-
-
- - - - - - - - - - - - - -
-
-
-`; diff --git a/src/plugins/home/public/application/components/home.test.tsx b/src/plugins/home/public/application/components/home.test.tsx index 9983afa3d4d611..f27a286488c2b1 100644 --- a/src/plugins/home/public/application/components/home.test.tsx +++ b/src/plugins/home/public/application/components/home.test.tsx @@ -12,7 +12,6 @@ import type { HomeProps } from './home'; import { Home } from './home'; import { FeatureCatalogueCategory } from '../../services'; -import { telemetryPluginMock } from '../../../../telemetry/public/mocks'; import { Welcome } from './welcome'; let mockHasIntegrationsPermission = true; @@ -57,7 +56,6 @@ describe('home', () => { setItem: jest.fn(), }, urlBasePath: 'goober', - telemetry: telemetryPluginMock.createStartContract(), addBasePath(url) { return `base_path/${url}`; }, diff --git a/src/plugins/home/public/application/components/home.tsx b/src/plugins/home/public/application/components/home.tsx index fdf04ea5806538..1fb0b3c790ab7e 100644 --- a/src/plugins/home/public/application/components/home.tsx +++ b/src/plugins/home/public/application/components/home.tsx @@ -10,7 +10,6 @@ import React, { Component } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { METRIC_TYPE } from '@kbn/analytics'; import { i18n } from '@kbn/i18n'; -import type { TelemetryPluginStart } from 'src/plugins/telemetry/public'; import { KibanaPageTemplate, OverviewPageFooter } from '../../../../kibana_react/public'; import { HOME_APP_BASE_PATH } from '../../../common/constants'; import type { FeatureCatalogueEntry, FeatureCatalogueSolution } from '../../services'; @@ -29,7 +28,6 @@ export interface HomeProps { solutions: FeatureCatalogueSolution[]; localStorage: Storage; urlBasePath: string; - telemetry: TelemetryPluginStart; hasUserDataView: () => Promise; } @@ -175,13 +173,7 @@ export class Home extends Component { } private renderWelcome() { - return ( - this.skipWelcome()} - urlBasePath={this.props.urlBasePath} - telemetry={this.props.telemetry} - /> - ); + return this.skipWelcome()} urlBasePath={this.props.urlBasePath} />; } public render() { diff --git a/src/plugins/home/public/application/components/home_app.js b/src/plugins/home/public/application/components/home_app.js index 62df479ecbfdf6..a634573aaf21ec 100644 --- a/src/plugins/home/public/application/components/home_app.js +++ b/src/plugins/home/public/application/components/home_app.js @@ -26,7 +26,6 @@ export function HomeApp({ directories, solutions }) { getBasePath, addBasePath, environmentService, - telemetry, dataViewsService, } = getServices(); const environment = environmentService.getEnvironment(); @@ -75,7 +74,6 @@ export function HomeApp({ directories, solutions }) { solutions={solutions} localStorage={localStorage} urlBasePath={getBasePath()} - telemetry={telemetry} hasUserDataView={() => dataViewsService.hasUserDataView()} /> diff --git a/src/plugins/home/public/application/components/welcome.test.mocks.ts b/src/plugins/home/public/application/components/welcome.test.mocks.ts new file mode 100644 index 00000000000000..fc9854bae31990 --- /dev/null +++ b/src/plugins/home/public/application/components/welcome.test.mocks.ts @@ -0,0 +1,17 @@ +/* + * 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 { welcomeServiceMock } from '../../services/welcome/welcome_service.mocks'; + +jest.doMock('../kibana_services', () => ({ + getServices: () => ({ + addBasePath: (path: string) => `root${path}`, + trackUiMetric: () => {}, + welcomeService: welcomeServiceMock.create(), + }), +})); diff --git a/src/plugins/home/public/application/components/welcome.test.tsx b/src/plugins/home/public/application/components/welcome.test.tsx index b042a91e58c9d2..3400b4bfcdb75f 100644 --- a/src/plugins/home/public/application/components/welcome.test.tsx +++ b/src/plugins/home/public/application/components/welcome.test.tsx @@ -8,58 +8,11 @@ import React from 'react'; import { shallow } from 'enzyme'; +import './welcome.test.mocks'; import { Welcome } from './welcome'; -import { telemetryPluginMock } from '../../../../telemetry/public/mocks'; -jest.mock('../kibana_services', () => ({ - getServices: () => ({ - addBasePath: (path: string) => `root${path}`, - trackUiMetric: () => {}, - }), -})); - -test('should render a Welcome screen with the telemetry disclaimer', () => { - const telemetry = telemetryPluginMock.createStartContract(); - const component = shallow( {}} telemetry={telemetry} />); - - expect(component).toMatchSnapshot(); -}); - -test('should render a Welcome screen with the telemetry disclaimer when optIn is true', () => { - const telemetry = telemetryPluginMock.createStartContract(); - telemetry.telemetryService.getIsOptedIn = jest.fn().mockReturnValue(true); - const component = shallow( {}} telemetry={telemetry} />); - - expect(component).toMatchSnapshot(); -}); - -test('should render a Welcome screen with the telemetry disclaimer when optIn is false', () => { - const telemetry = telemetryPluginMock.createStartContract(); - telemetry.telemetryService.getIsOptedIn = jest.fn().mockReturnValue(false); - const component = shallow( {}} telemetry={telemetry} />); - - expect(component).toMatchSnapshot(); -}); - -test('should render a Welcome screen with no telemetry disclaimer', () => { +test('should render a Welcome screen', () => { const component = shallow( {}} />); expect(component).toMatchSnapshot(); }); - -test('should render a Welcome screen without the opt in/out link when user cannot change optIn status', () => { - const telemetry = telemetryPluginMock.createStartContract(); - telemetry.telemetryService.getCanChangeOptInStatus = jest.fn().mockReturnValue(false); - const component = shallow( {}} telemetry={telemetry} />); - - expect(component).toMatchSnapshot(); -}); - -test('fires opt-in seen when mounted', () => { - const telemetry = telemetryPluginMock.createStartContract(); - const mockSetOptedInNoticeSeen = jest.fn(); - telemetry.telemetryNotifications.setOptedInNoticeSeen = mockSetOptedInNoticeSeen; - shallow( {}} telemetry={telemetry} />); - - expect(mockSetOptedInNoticeSeen).toHaveBeenCalled(); -}); diff --git a/src/plugins/home/public/application/components/welcome.tsx b/src/plugins/home/public/application/components/welcome.tsx index 1a6251ebdca118..9efa6d356d9716 100644 --- a/src/plugins/home/public/application/components/welcome.tsx +++ b/src/plugins/home/public/application/components/welcome.tsx @@ -12,27 +12,17 @@ * in Elasticsearch. */ -import React, { Fragment } from 'react'; -import { - EuiLink, - EuiTextColor, - EuiTitle, - EuiSpacer, - EuiFlexGroup, - EuiFlexItem, - EuiIcon, - EuiPortal, -} from '@elastic/eui'; +import React from 'react'; +import { EuiTitle, EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiPortal } from '@elastic/eui'; import { METRIC_TYPE } from '@kbn/analytics'; import { FormattedMessage } from '@kbn/i18n-react'; import { getServices } from '../kibana_services'; -import { TelemetryPluginStart } from '../../../../telemetry/public'; import { SampleDataCard } from './sample_data'; + interface Props { urlBasePath: string; onSkip: () => void; - telemetry?: TelemetryPluginStart; } /** @@ -47,7 +37,7 @@ export class Welcome extends React.Component { } }; - private redirecToAddData() { + private redirectToAddData() { this.services.application.navigateToApp('integrations', { path: '/browse' }); } @@ -58,68 +48,23 @@ export class Welcome extends React.Component { private onSampleDataConfirm = () => { this.services.trackUiMetric(METRIC_TYPE.CLICK, 'sampleDataConfirm'); - this.redirecToAddData(); + this.redirectToAddData(); }; componentDidMount() { - const { telemetry } = this.props; + const { welcomeService } = this.services; this.services.trackUiMetric(METRIC_TYPE.LOADED, 'welcomeScreenMount'); - if (telemetry?.telemetryService.userCanChangeSettings) { - telemetry.telemetryNotifications.setOptedInNoticeSeen(); - } document.addEventListener('keydown', this.hideOnEsc); + welcomeService.onRendered(); } componentWillUnmount() { document.removeEventListener('keydown', this.hideOnEsc); } - private renderTelemetryEnabledOrDisabledText = () => { - const { telemetry } = this.props; - if ( - !telemetry || - !telemetry.telemetryService.userCanChangeSettings || - !telemetry.telemetryService.getCanChangeOptInStatus() - ) { - return null; - } - - const isOptedIn = telemetry.telemetryService.getIsOptedIn(); - if (isOptedIn) { - return ( - - - - - - - ); - } else { - return ( - - - - - - - ); - } - }; - render() { - const { urlBasePath, telemetry } = this.props; + const { urlBasePath } = this.props; + const { welcomeService } = this.services; return (
@@ -146,28 +91,7 @@ export class Welcome extends React.Component { onDecline={this.onSampleDataDecline} /> - {!!telemetry && ( - - - - - - - {this.renderTelemetryEnabledOrDisabledText()} - - - - )} + {welcomeService.renderTelemetryNotice()}
diff --git a/src/plugins/home/public/application/kibana_services.ts b/src/plugins/home/public/application/kibana_services.ts index fdd325df96ac57..3ccfd9413a88ad 100644 --- a/src/plugins/home/public/application/kibana_services.ts +++ b/src/plugins/home/public/application/kibana_services.ts @@ -17,7 +17,6 @@ import { ApplicationStart, } from 'kibana/public'; import { UiCounterMetricType } from '@kbn/analytics'; -import { TelemetryPluginStart } from '../../../telemetry/public'; import { UrlForwardingStart } from '../../../url_forwarding/public'; import { DataViewsContract } from '../../../data_views/public'; import { TutorialService } from '../services/tutorials'; @@ -26,6 +25,7 @@ import { FeatureCatalogueRegistry } from '../services/feature_catalogue'; import { EnvironmentService } from '../services/environment'; import { ConfigSchema } from '../../config'; import { SharePluginSetup } from '../../../share/public'; +import type { WelcomeService } from '../services/welcome'; export interface HomeKibanaServices { dataViewsService: DataViewsContract; @@ -46,9 +46,9 @@ export interface HomeKibanaServices { docLinks: DocLinksStart; addBasePath: (url: string) => string; environmentService: EnvironmentService; - telemetry?: TelemetryPluginStart; tutorialService: TutorialService; addDataService: AddDataService; + welcomeService: WelcomeService; } let services: HomeKibanaServices | null = null; diff --git a/src/plugins/home/public/index.ts b/src/plugins/home/public/index.ts index 009382eee0009a..3450f4f9d2caf9 100644 --- a/src/plugins/home/public/index.ts +++ b/src/plugins/home/public/index.ts @@ -27,6 +27,8 @@ export type { TutorialVariables, TutorialDirectoryHeaderLinkComponent, TutorialModuleNoticeComponent, + WelcomeRenderTelemetryNotice, + WelcomeServiceSetup, } from './services'; export { INSTRUCTION_VARIANT, getDisplayText } from '../common/instruction_variant'; diff --git a/src/plugins/home/public/mocks.ts b/src/plugins/home/public/mocks.ts index 10c186ee3f4e30..42e489dea9d2a3 100644 --- a/src/plugins/home/public/mocks.ts +++ b/src/plugins/home/public/mocks.ts @@ -8,16 +8,17 @@ import { featureCatalogueRegistryMock } from './services/feature_catalogue/feature_catalogue_registry.mock'; import { environmentServiceMock } from './services/environment/environment.mock'; -import { configSchema } from '../config'; import { tutorialServiceMock } from './services/tutorials/tutorial_service.mock'; import { addDataServiceMock } from './services/add_data/add_data_service.mock'; +import { HomePublicPluginSetup } from './plugin'; +import { welcomeServiceMock } from './services/welcome/welcome_service.mocks'; -const createSetupContract = () => ({ +const createSetupContract = (): jest.Mocked => ({ featureCatalogue: featureCatalogueRegistryMock.createSetup(), environment: environmentServiceMock.createSetup(), tutorials: tutorialServiceMock.createSetup(), addData: addDataServiceMock.createSetup(), - config: configSchema.validate({}), + welcomeScreen: welcomeServiceMock.createSetup(), }); export const homePluginMock = { diff --git a/src/plugins/home/public/plugin.test.mocks.ts b/src/plugins/home/public/plugin.test.mocks.ts index c3e3c50a2fe0f3..22d314cbd6d068 100644 --- a/src/plugins/home/public/plugin.test.mocks.ts +++ b/src/plugins/home/public/plugin.test.mocks.ts @@ -10,14 +10,17 @@ import { featureCatalogueRegistryMock } from './services/feature_catalogue/featu import { environmentServiceMock } from './services/environment/environment.mock'; import { tutorialServiceMock } from './services/tutorials/tutorial_service.mock'; import { addDataServiceMock } from './services/add_data/add_data_service.mock'; +import { welcomeServiceMock } from './services/welcome/welcome_service.mocks'; export const registryMock = featureCatalogueRegistryMock.create(); export const environmentMock = environmentServiceMock.create(); export const tutorialMock = tutorialServiceMock.create(); export const addDataMock = addDataServiceMock.create(); +export const welcomeMock = welcomeServiceMock.create(); jest.doMock('./services', () => ({ FeatureCatalogueRegistry: jest.fn(() => registryMock), EnvironmentService: jest.fn(() => environmentMock), TutorialService: jest.fn(() => tutorialMock), AddDataService: jest.fn(() => addDataMock), + WelcomeService: jest.fn(() => welcomeMock), })); diff --git a/src/plugins/home/public/plugin.test.ts b/src/plugins/home/public/plugin.test.ts index 990f0dce54a05f..57a1f5ec112aaf 100644 --- a/src/plugins/home/public/plugin.test.ts +++ b/src/plugins/home/public/plugin.test.ts @@ -79,5 +79,18 @@ describe('HomePublicPlugin', () => { expect(setup).toHaveProperty('tutorials'); expect(setup.tutorials).toHaveProperty('setVariable'); }); + + test('wires up and returns welcome service', async () => { + const setup = await new HomePublicPlugin(mockInitializerContext).setup( + coreMock.createSetup() as any, + { + share: mockShare, + urlForwarding: urlForwardingPluginMock.createSetupContract(), + } + ); + expect(setup).toHaveProperty('welcomeScreen'); + expect(setup.welcomeScreen).toHaveProperty('registerOnRendered'); + expect(setup.welcomeScreen).toHaveProperty('registerTelemetryNoticeRenderer'); + }); }); }); diff --git a/src/plugins/home/public/plugin.ts b/src/plugins/home/public/plugin.ts index 1ece73e71f393f..af43e56a1d75d3 100644 --- a/src/plugins/home/public/plugin.ts +++ b/src/plugins/home/public/plugin.ts @@ -25,11 +25,12 @@ import { TutorialServiceSetup, AddDataService, AddDataServiceSetup, + WelcomeService, + WelcomeServiceSetup, } from './services'; import { ConfigSchema } from '../config'; import { setServices } from './application/kibana_services'; import { DataViewsPublicPluginStart } from '../../data_views/public'; -import { TelemetryPluginStart } from '../../telemetry/public'; import { UsageCollectionSetup } from '../../usage_collection/public'; import { UrlForwardingSetup, UrlForwardingStart } from '../../url_forwarding/public'; import { AppNavLinkStatus } from '../../../core/public'; @@ -38,7 +39,6 @@ import { SharePluginSetup } from '../../share/public'; export interface HomePluginStartDependencies { dataViews: DataViewsPublicPluginStart; - telemetry?: TelemetryPluginStart; urlForwarding: UrlForwardingStart; } @@ -61,6 +61,7 @@ export class HomePublicPlugin private readonly environmentService = new EnvironmentService(); private readonly tutorialService = new TutorialService(); private readonly addDataService = new AddDataService(); + private readonly welcomeService = new WelcomeService(); constructor(private readonly initializerContext: PluginInitializerContext) {} @@ -76,7 +77,7 @@ export class HomePublicPlugin const trackUiMetric = usageCollection ? usageCollection.reportUiCounter.bind(usageCollection, 'Kibana_home') : () => {}; - const [coreStart, { telemetry, dataViews, urlForwarding: urlForwardingStart }] = + const [coreStart, { dataViews, urlForwarding: urlForwardingStart }] = await core.getStartServices(); setServices({ share, @@ -89,7 +90,6 @@ export class HomePublicPlugin savedObjectsClient: coreStart.savedObjects.client, chrome: coreStart.chrome, application: coreStart.application, - telemetry, uiSettings: core.uiSettings, addBasePath: core.http.basePath.prepend, getBasePath: core.http.basePath.get, @@ -100,6 +100,7 @@ export class HomePublicPlugin tutorialService: this.tutorialService, addDataService: this.addDataService, featureCatalogue: this.featuresCatalogueRegistry, + welcomeService: this.welcomeService, }); coreStart.chrome.docTitle.change( i18n.translate('home.pageTitle', { defaultMessage: 'Home' }) @@ -132,6 +133,7 @@ export class HomePublicPlugin environment: { ...this.environmentService.setup() }, tutorials: { ...this.tutorialService.setup() }, addData: { ...this.addDataService.setup() }, + welcomeScreen: { ...this.welcomeService.setup() }, }; } @@ -159,12 +161,12 @@ export interface HomePublicPluginSetup { tutorials: TutorialServiceSetup; addData: AddDataServiceSetup; featureCatalogue: FeatureCatalogueSetup; + welcomeScreen: WelcomeServiceSetup; /** * The environment service is only available for a transition period and will * be replaced by display specific extension points. * @deprecated */ - environment: EnvironmentSetup; } diff --git a/src/plugins/home/public/services/index.ts b/src/plugins/home/public/services/index.ts index 2ee68a9eef0c29..41bc9ee258cebb 100644 --- a/src/plugins/home/public/services/index.ts +++ b/src/plugins/home/public/services/index.ts @@ -28,3 +28,6 @@ export type { export { AddDataService } from './add_data'; export type { AddDataServiceSetup, AddDataTab } from './add_data'; + +export { WelcomeService } from './welcome'; +export type { WelcomeServiceSetup, WelcomeRenderTelemetryNotice } from './welcome'; diff --git a/src/plugins/home/public/services/welcome/index.ts b/src/plugins/home/public/services/welcome/index.ts new file mode 100644 index 00000000000000..371c6044c5dc5c --- /dev/null +++ b/src/plugins/home/public/services/welcome/index.ts @@ -0,0 +1,10 @@ +/* + * 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. + */ + +export type { WelcomeServiceSetup, WelcomeRenderTelemetryNotice } from './welcome_service'; +export { WelcomeService } from './welcome_service'; diff --git a/src/plugins/home/public/services/welcome/welcome_service.mocks.ts b/src/plugins/home/public/services/welcome/welcome_service.mocks.ts new file mode 100644 index 00000000000000..921cb990663276 --- /dev/null +++ b/src/plugins/home/public/services/welcome/welcome_service.mocks.ts @@ -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 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 { PublicMethodsOf } from '@kbn/utility-types'; +import { WelcomeService, WelcomeServiceSetup } from './welcome_service'; + +const createSetupMock = (): jest.Mocked => { + const welcomeService = new WelcomeService(); + const welcomeServiceSetup = welcomeService.setup(); + return { + registerTelemetryNoticeRenderer: jest + .fn() + .mockImplementation(welcomeServiceSetup.registerTelemetryNoticeRenderer), + registerOnRendered: jest.fn().mockImplementation(welcomeServiceSetup.registerOnRendered), + }; +}; + +const createMock = (): jest.Mocked> => { + const welcomeService = new WelcomeService(); + + return { + setup: jest.fn().mockImplementation(welcomeService.setup), + onRendered: jest.fn().mockImplementation(welcomeService.onRendered), + renderTelemetryNotice: jest.fn().mockImplementation(welcomeService.renderTelemetryNotice), + }; +}; + +export const welcomeServiceMock = { + createSetup: createSetupMock, + create: createMock, +}; diff --git a/src/plugins/home/public/services/welcome/welcome_service.test.ts b/src/plugins/home/public/services/welcome/welcome_service.test.ts new file mode 100644 index 00000000000000..df2f95718c78b5 --- /dev/null +++ b/src/plugins/home/public/services/welcome/welcome_service.test.ts @@ -0,0 +1,86 @@ +/* + * 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 { WelcomeService, WelcomeServiceSetup } from './welcome_service'; + +describe('WelcomeService', () => { + let welcomeService: WelcomeService; + let welcomeServiceSetup: WelcomeServiceSetup; + + beforeEach(() => { + welcomeService = new WelcomeService(); + welcomeServiceSetup = welcomeService.setup(); + }); + describe('onRendered', () => { + test('it should register an onRendered listener', () => { + const onRendered = jest.fn(); + welcomeServiceSetup.registerOnRendered(onRendered); + + welcomeService.onRendered(); + expect(onRendered).toHaveBeenCalledTimes(1); + }); + + test('it should handle onRendered errors', () => { + const onRendered = jest.fn().mockImplementation(() => { + throw new Error('Something went terribly wrong'); + }); + welcomeServiceSetup.registerOnRendered(onRendered); + + expect(() => welcomeService.onRendered()).not.toThrow(); + expect(onRendered).toHaveBeenCalledTimes(1); + }); + + test('it should allow registering multiple onRendered listeners', () => { + const onRendered = jest.fn(); + const onRendered2 = jest.fn(); + welcomeServiceSetup.registerOnRendered(onRendered); + welcomeServiceSetup.registerOnRendered(onRendered2); + + welcomeService.onRendered(); + expect(onRendered).toHaveBeenCalledTimes(1); + expect(onRendered2).toHaveBeenCalledTimes(1); + }); + + test('if the same handler is registered twice, it is called twice', () => { + const onRendered = jest.fn(); + welcomeServiceSetup.registerOnRendered(onRendered); + welcomeServiceSetup.registerOnRendered(onRendered); + + welcomeService.onRendered(); + expect(onRendered).toHaveBeenCalledTimes(2); + }); + }); + describe('renderTelemetryNotice', () => { + test('it should register a renderer', () => { + const renderer = jest.fn().mockReturnValue('rendered text'); + welcomeServiceSetup.registerTelemetryNoticeRenderer(renderer); + + expect(welcomeService.renderTelemetryNotice()).toEqual('rendered text'); + }); + + test('it should fail to register a 2nd renderer and still use the first registered renderer', () => { + const renderer = jest.fn().mockReturnValue('rendered text'); + const renderer2 = jest.fn().mockReturnValue('other text'); + welcomeServiceSetup.registerTelemetryNoticeRenderer(renderer); + expect(() => welcomeServiceSetup.registerTelemetryNoticeRenderer(renderer2)).toThrowError( + 'Only one renderTelemetryNotice handler can be registered' + ); + + expect(welcomeService.renderTelemetryNotice()).toEqual('rendered text'); + }); + + test('it should handle errors in the renderer', () => { + const renderer = jest.fn().mockImplementation(() => { + throw new Error('Something went terribly wrong'); + }); + welcomeServiceSetup.registerTelemetryNoticeRenderer(renderer); + + expect(welcomeService.renderTelemetryNotice()).toEqual(null); + }); + }); +}); diff --git a/src/plugins/home/public/services/welcome/welcome_service.ts b/src/plugins/home/public/services/welcome/welcome_service.ts new file mode 100644 index 00000000000000..46cf139adb36a3 --- /dev/null +++ b/src/plugins/home/public/services/welcome/welcome_service.ts @@ -0,0 +1,63 @@ +/* + * 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. + */ + +export type WelcomeRenderTelemetryNotice = () => null | JSX.Element; + +export interface WelcomeServiceSetup { + /** + * Register listeners to be called when the Welcome component is mounted. + * It can be called multiple times to register multiple listeners. + */ + registerOnRendered: (onRendered: () => void) => void; + /** + * Register a renderer of the telemetry notice to be shown below the Welcome page. + */ + registerTelemetryNoticeRenderer: (renderTelemetryNotice: WelcomeRenderTelemetryNotice) => void; +} + +export class WelcomeService { + private readonly onRenderedHandlers: Array<() => void> = []; + private renderTelemetryNoticeHandler?: WelcomeRenderTelemetryNotice; + + public setup = (): WelcomeServiceSetup => { + return { + registerOnRendered: (onRendered) => { + this.onRenderedHandlers.push(onRendered); + }, + registerTelemetryNoticeRenderer: (renderTelemetryNotice) => { + if (this.renderTelemetryNoticeHandler) { + throw new Error('Only one renderTelemetryNotice handler can be registered'); + } + this.renderTelemetryNoticeHandler = renderTelemetryNotice; + }, + }; + }; + + public onRendered = () => { + this.onRenderedHandlers.forEach((onRendered) => { + try { + onRendered(); + } catch (err) { + // eslint-disable-next-line no-console + console.error(err); + } + }); + }; + + public renderTelemetryNotice = () => { + if (this.renderTelemetryNoticeHandler) { + try { + return this.renderTelemetryNoticeHandler(); + } catch (err) { + // eslint-disable-next-line no-console + console.error(err); + } + } + return null; + }; +} diff --git a/src/plugins/home/tsconfig.json b/src/plugins/home/tsconfig.json index fa98b98ff8e1c3..17d0fc7bd91acf 100644 --- a/src/plugins/home/tsconfig.json +++ b/src/plugins/home/tsconfig.json @@ -15,7 +15,6 @@ { "path": "../kibana_react/tsconfig.json" }, { "path": "../share/tsconfig.json" }, { "path": "../url_forwarding/tsconfig.json" }, - { "path": "../usage_collection/tsconfig.json" }, - { "path": "../telemetry/tsconfig.json" } + { "path": "../usage_collection/tsconfig.json" } ] } diff --git a/src/plugins/telemetry/kibana.json b/src/plugins/telemetry/kibana.json index 09cc6accb68f4b..a6796e42f92282 100644 --- a/src/plugins/telemetry/kibana.json +++ b/src/plugins/telemetry/kibana.json @@ -8,6 +8,7 @@ "server": true, "ui": true, "requiredPlugins": ["telemetryCollectionManager", "usageCollection", "screenshotMode"], + "optionalPlugins": ["home", "security"], "extraPublicDirs": ["common/constants"], "requiredBundles": ["kibanaUtils", "kibanaReact"] } diff --git a/src/plugins/telemetry/public/plugin.ts b/src/plugins/telemetry/public/plugin.ts index 3072ff67703d78..794183cb8a8f5d 100644 --- a/src/plugins/telemetry/public/plugin.ts +++ b/src/plugins/telemetry/public/plugin.ts @@ -31,6 +31,8 @@ import { } from '../common/telemetry_config'; import { getNotifyUserAboutOptInDefault } from '../common/telemetry_config/get_telemetry_notify_user_about_optin_default'; import { PRIVACY_STATEMENT_URL } from '../common/constants'; +import { HomePublicPluginSetup } from '../../home/public'; +import { renderWelcomeTelemetryNotice } from './render_welcome_telemetry_notice'; /** * Publicly exposed APIs from the Telemetry Service @@ -82,6 +84,7 @@ export interface TelemetryPluginStart { interface TelemetryPluginSetupDependencies { screenshotMode: ScreenshotModePluginSetup; + home?: HomePublicPluginSetup; } /** @@ -121,7 +124,7 @@ export class TelemetryPlugin implements Plugin { + if (this.telemetryService?.userCanChangeSettings) { + this.telemetryNotifications?.setOptedInNoticeSeen(); + } + }); + + home.welcomeScreen.registerTelemetryNoticeRenderer(() => + renderWelcomeTelemetryNotice(this.telemetryService!, http.basePath.prepend) + ); + } + return { telemetryService: this.getTelemetryServicePublicApis(), }; diff --git a/src/plugins/telemetry/public/render_welcome_telemetry_notice.test.ts b/src/plugins/telemetry/public/render_welcome_telemetry_notice.test.ts new file mode 100644 index 00000000000000..6da76db915656d --- /dev/null +++ b/src/plugins/telemetry/public/render_welcome_telemetry_notice.test.ts @@ -0,0 +1,32 @@ +/* + * 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 { mountWithIntl } from '@kbn/test-jest-helpers'; +import { renderWelcomeTelemetryNotice } from './render_welcome_telemetry_notice'; +import { mockTelemetryService } from './mocks'; + +describe('renderWelcomeTelemetryNotice', () => { + test('it should show the opt-out message', () => { + const telemetryService = mockTelemetryService(); + const component = mountWithIntl(renderWelcomeTelemetryNotice(telemetryService, (url) => url)); + expect(component.exists('[id="telemetry.dataManagementDisableCollectionLink"]')).toBe(true); + }); + + test('it should show the opt-in message', () => { + const telemetryService = mockTelemetryService({ config: { optIn: false } }); + const component = mountWithIntl(renderWelcomeTelemetryNotice(telemetryService, (url) => url)); + expect(component.exists('[id="telemetry.dataManagementEnableCollectionLink"]')).toBe(true); + }); + + test('it should not show opt-in/out options if user cannot change the settings', () => { + const telemetryService = mockTelemetryService({ config: { allowChangingOptInStatus: false } }); + const component = mountWithIntl(renderWelcomeTelemetryNotice(telemetryService, (url) => url)); + expect(component.exists('[id="telemetry.dataManagementDisableCollectionLink"]')).toBe(false); + expect(component.exists('[id="telemetry.dataManagementEnableCollectionLink"]')).toBe(false); + }); +}); diff --git a/src/plugins/telemetry/public/render_welcome_telemetry_notice.tsx b/src/plugins/telemetry/public/render_welcome_telemetry_notice.tsx new file mode 100644 index 00000000000000..8ef26fb797d532 --- /dev/null +++ b/src/plugins/telemetry/public/render_welcome_telemetry_notice.tsx @@ -0,0 +1,80 @@ +/* + * 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 React from 'react'; +import { EuiLink, EuiSpacer, EuiTextColor } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import type { TelemetryService } from './services'; +import { PRIVACY_STATEMENT_URL } from '../common/constants'; + +export function renderWelcomeTelemetryNotice( + telemetryService: TelemetryService, + addBasePath: (url: string) => string +) { + return ( + <> + + + + + + {renderTelemetryEnabledOrDisabledText(telemetryService, addBasePath)} + + + + ); +} + +function renderTelemetryEnabledOrDisabledText( + telemetryService: TelemetryService, + addBasePath: (url: string) => string +) { + if (!telemetryService.userCanChangeSettings || !telemetryService.getCanChangeOptInStatus()) { + return null; + } + + const isOptedIn = telemetryService.getIsOptedIn(); + + if (isOptedIn) { + return ( + <> + + + + + + ); + } else { + return ( + <> + + + + + + ); + } +} diff --git a/src/plugins/telemetry/server/plugin.ts b/src/plugins/telemetry/server/plugin.ts index 73c61ea1c50386..681a871ba105b6 100644 --- a/src/plugins/telemetry/server/plugin.ts +++ b/src/plugins/telemetry/server/plugin.ts @@ -23,6 +23,7 @@ import type { Plugin, Logger, } from 'src/core/server'; +import type { SecurityPluginStart } from '../../../../x-pack/plugins/security/server'; import { SavedObjectsClient } from '../../../core/server'; import { registerRoutes } from './routes'; import { registerCollection } from './telemetry_collection'; @@ -42,6 +43,7 @@ interface TelemetryPluginsDepsSetup { interface TelemetryPluginsDepsStart { telemetryCollectionManager: TelemetryCollectionManagerPluginStart; + security?: SecurityPluginStart; } /** @@ -90,6 +92,8 @@ export class TelemetryPlugin implements Plugin(1); + private security?: SecurityPluginStart; + constructor(initializerContext: PluginInitializerContext) { this.logger = initializerContext.logger.get(); this.isDev = initializerContext.env.mode.dev; @@ -119,6 +123,7 @@ export class TelemetryPlugin implements Plugin this.security, }); this.registerMappings((opts) => savedObjects.registerType(opts)); @@ -137,11 +142,17 @@ export class TelemetryPlugin implements Plugin; + getSecurity: SecurityGetter; } export function registerRoutes(options: RegisterRoutesParams) { - const { isDev, telemetryCollectionManager, router, savedObjectsInternalClient$ } = options; + const { isDev, telemetryCollectionManager, router, savedObjectsInternalClient$, getSecurity } = + options; registerTelemetryOptInRoutes(options); - registerTelemetryUsageStatsRoutes(router, telemetryCollectionManager, isDev); + registerTelemetryUsageStatsRoutes(router, telemetryCollectionManager, isDev, getSecurity); registerTelemetryOptInStatsRoutes(router, telemetryCollectionManager); registerTelemetryUserHasSeenNotice(router); registerTelemetryLastReported(router, savedObjectsInternalClient$); diff --git a/src/plugins/telemetry/server/routes/telemetry_opt_in_stats.ts b/src/plugins/telemetry/server/routes/telemetry_opt_in_stats.ts index 2a956656621944..6139eee3e10ca6 100644 --- a/src/plugins/telemetry/server/routes/telemetry_opt_in_stats.ts +++ b/src/plugins/telemetry/server/routes/telemetry_opt_in_stats.ts @@ -75,7 +75,6 @@ export function registerTelemetryOptInStatsRoutes( const statsGetterConfig: StatsGetterConfig = { unencrypted, - request: req, }; const optInStatus = await telemetryCollectionManager.getOptInStats( diff --git a/src/plugins/telemetry/server/routes/telemetry_usage_stats.test.ts b/src/plugins/telemetry/server/routes/telemetry_usage_stats.test.ts index 736367446d3c05..bc7569585c127b 100644 --- a/src/plugins/telemetry/server/routes/telemetry_usage_stats.test.ts +++ b/src/plugins/telemetry/server/routes/telemetry_usage_stats.test.ts @@ -8,7 +8,8 @@ import { registerTelemetryUsageStatsRoutes } from './telemetry_usage_stats'; import { coreMock, httpServerMock } from 'src/core/server/mocks'; -import type { RequestHandlerContext, IRouter } from 'kibana/server'; +import type { RequestHandlerContext, IRouter } from 'src/core/server'; +import { securityMock } from '../../../../../x-pack/plugins/security/server/mocks'; import { telemetryCollectionManagerPluginMock } from '../../../telemetry_collection_manager/server/mocks'; async function runRequest( @@ -35,13 +36,18 @@ describe('registerTelemetryUsageStatsRoutes', () => { }; const telemetryCollectionManager = telemetryCollectionManagerPluginMock.createSetupContract(); const mockCoreSetup = coreMock.createSetup(); - const mockRouter = mockCoreSetup.http.createRouter(); const mockStats = [{ clusterUuid: 'text', stats: 'enc_str' }]; telemetryCollectionManager.getStats.mockResolvedValue(mockStats); + const getSecurity = jest.fn(); + + let mockRouter: IRouter; + beforeEach(() => { + mockRouter = mockCoreSetup.http.createRouter(); + }); describe('clusters/_stats POST route', () => { it('registers _stats POST route and accepts body configs', () => { - registerTelemetryUsageStatsRoutes(mockRouter, telemetryCollectionManager, true); + registerTelemetryUsageStatsRoutes(mockRouter, telemetryCollectionManager, true, getSecurity); expect(mockRouter.post).toBeCalledTimes(1); const [routeConfig, handler] = (mockRouter.post as jest.Mock).mock.calls[0]; expect(routeConfig.path).toMatchInlineSnapshot(`"/api/telemetry/v2/clusters/_stats"`); @@ -50,11 +56,10 @@ describe('registerTelemetryUsageStatsRoutes', () => { }); it('responds with encrypted stats with no cache refresh by default', async () => { - registerTelemetryUsageStatsRoutes(mockRouter, telemetryCollectionManager, true); + registerTelemetryUsageStatsRoutes(mockRouter, telemetryCollectionManager, true, getSecurity); - const { mockRequest, mockResponse } = await runRequest(mockRouter); + const { mockResponse } = await runRequest(mockRouter); expect(telemetryCollectionManager.getStats).toBeCalledWith({ - request: mockRequest, unencrypted: undefined, refreshCache: undefined, }); @@ -63,39 +68,99 @@ describe('registerTelemetryUsageStatsRoutes', () => { }); it('when unencrypted is set getStats is called with unencrypted and refreshCache', async () => { - registerTelemetryUsageStatsRoutes(mockRouter, telemetryCollectionManager, true); + registerTelemetryUsageStatsRoutes(mockRouter, telemetryCollectionManager, true, getSecurity); - const { mockRequest } = await runRequest(mockRouter, { unencrypted: true }); + await runRequest(mockRouter, { unencrypted: true }); expect(telemetryCollectionManager.getStats).toBeCalledWith({ - request: mockRequest, unencrypted: true, refreshCache: true, }); }); it('calls getStats with refreshCache when set in body', async () => { - registerTelemetryUsageStatsRoutes(mockRouter, telemetryCollectionManager, true); - const { mockRequest } = await runRequest(mockRouter, { refreshCache: true }); + registerTelemetryUsageStatsRoutes(mockRouter, telemetryCollectionManager, true, getSecurity); + await runRequest(mockRouter, { refreshCache: true }); expect(telemetryCollectionManager.getStats).toBeCalledWith({ - request: mockRequest, unencrypted: undefined, refreshCache: true, }); }); it('calls getStats with refreshCache:true even if set to false in body when unencrypted is set to true', async () => { - registerTelemetryUsageStatsRoutes(mockRouter, telemetryCollectionManager, true); - const { mockRequest } = await runRequest(mockRouter, { + registerTelemetryUsageStatsRoutes(mockRouter, telemetryCollectionManager, true, getSecurity); + await runRequest(mockRouter, { refreshCache: false, unencrypted: true, }); expect(telemetryCollectionManager.getStats).toBeCalledWith({ - request: mockRequest, unencrypted: true, refreshCache: true, }); }); + it('returns 403 when the user does not have enough permissions to request unencrypted telemetry', async () => { + const getSecurityMock = jest.fn().mockImplementation(() => { + const securityStartMock = securityMock.createStart(); + securityStartMock.authz.checkPrivilegesWithRequest.mockReturnValue({ + globally: () => ({ hasAllRequested: false }), + }); + return securityStartMock; + }); + registerTelemetryUsageStatsRoutes( + mockRouter, + telemetryCollectionManager, + true, + getSecurityMock + ); + const { mockResponse } = await runRequest(mockRouter, { + refreshCache: false, + unencrypted: true, + }); + expect(mockResponse.forbidden).toBeCalled(); + }); + + it('returns 200 when the user has enough permissions to request unencrypted telemetry', async () => { + const getSecurityMock = jest.fn().mockImplementation(() => { + const securityStartMock = securityMock.createStart(); + securityStartMock.authz.checkPrivilegesWithRequest.mockReturnValue({ + globally: () => ({ hasAllRequested: true }), + }); + return securityStartMock; + }); + registerTelemetryUsageStatsRoutes( + mockRouter, + telemetryCollectionManager, + true, + getSecurityMock + ); + const { mockResponse } = await runRequest(mockRouter, { + refreshCache: false, + unencrypted: true, + }); + expect(mockResponse.ok).toBeCalled(); + }); + + it('returns 200 when the user does not have enough permissions to request unencrypted telemetry but it requests encrypted', async () => { + const getSecurityMock = jest.fn().mockImplementation(() => { + const securityStartMock = securityMock.createStart(); + securityStartMock.authz.checkPrivilegesWithRequest.mockReturnValue({ + globally: () => ({ hasAllRequested: false }), + }); + return securityStartMock; + }); + registerTelemetryUsageStatsRoutes( + mockRouter, + telemetryCollectionManager, + true, + getSecurityMock + ); + const { mockResponse } = await runRequest(mockRouter, { + refreshCache: false, + unencrypted: false, + }); + expect(mockResponse.ok).toBeCalled(); + }); + it.todo('always returns an empty array on errors on encrypted payload'); it.todo('returns the actual request error object when in development mode'); it.todo('returns forbidden on unencrypted and ES returns 403 in getStats'); diff --git a/src/plugins/telemetry/server/routes/telemetry_usage_stats.ts b/src/plugins/telemetry/server/routes/telemetry_usage_stats.ts index 2f72ae818f1121..4647f5afe0760b 100644 --- a/src/plugins/telemetry/server/routes/telemetry_usage_stats.ts +++ b/src/plugins/telemetry/server/routes/telemetry_usage_stats.ts @@ -12,11 +12,15 @@ import { TelemetryCollectionManagerPluginSetup, StatsGetterConfig, } from 'src/plugins/telemetry_collection_manager/server'; +import type { SecurityPluginStart } from '../../../../../x-pack/plugins/security/server'; + +export type SecurityGetter = () => SecurityPluginStart | undefined; export function registerTelemetryUsageStatsRoutes( router: IRouter, telemetryCollectionManager: TelemetryCollectionManagerPluginSetup, - isDev: boolean + isDev: boolean, + getSecurity: SecurityGetter ) { router.post( { @@ -31,9 +35,22 @@ export function registerTelemetryUsageStatsRoutes( async (context, req, res) => { const { unencrypted, refreshCache } = req.body; + const security = getSecurity(); + if (security && unencrypted) { + // Normally we would use `options: { tags: ['access:decryptedTelemetry'] }` in the route definition to check authorization for an + // API action, however, we want to check this conditionally based on the `unencrypted` parameter. In this case we need to use the + // security API directly to check privileges for this action. Note that the 'decryptedTelemetry' API privilege string is only + // granted to users that have "Global All" or "Global Read" privileges in Kibana. + const { checkPrivilegesWithRequest, actions } = security.authz; + const privileges = { kibana: actions.api.get('decryptedTelemetry') }; + const { hasAllRequested } = await checkPrivilegesWithRequest(req).globally(privileges); + if (!hasAllRequested) { + return res.forbidden(); + } + } + try { const statsConfig: StatsGetterConfig = { - request: req, unencrypted, refreshCache: unencrypted || refreshCache, }; diff --git a/src/plugins/telemetry/server/telemetry_collection/get_kibana.ts b/src/plugins/telemetry/server/telemetry_collection/get_kibana.ts index 83f33a894b9032..4340eaafd2d8ff 100644 --- a/src/plugins/telemetry/server/telemetry_collection/get_kibana.ts +++ b/src/plugins/telemetry/server/telemetry_collection/get_kibana.ts @@ -7,10 +7,9 @@ */ import { omit } from 'lodash'; -import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { KibanaRequest, SavedObjectsClientContract } from 'kibana/server'; -import { StatsCollectionContext } from 'src/plugins/telemetry_collection_manager/server'; -import { ElasticsearchClient } from 'src/core/server'; +import type { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server'; +import type { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import type { StatsCollectionContext } from 'src/plugins/telemetry_collection_manager/server'; export interface KibanaUsageStats { kibana: { @@ -71,9 +70,8 @@ export function handleKibanaStats( export async function getKibana( usageCollection: UsageCollectionSetup, asInternalUser: ElasticsearchClient, - soClient: SavedObjectsClientContract, - kibanaRequest: KibanaRequest | undefined // intentionally `| undefined` to enforce providing the parameter + soClient: SavedObjectsClientContract ): Promise { - const usage = await usageCollection.bulkFetch(asInternalUser, soClient, kibanaRequest); + const usage = await usageCollection.bulkFetch(asInternalUser, soClient); return usageCollection.toObject(usage); } diff --git a/src/plugins/telemetry/server/telemetry_collection/get_local_stats.test.ts b/src/plugins/telemetry/server/telemetry_collection/get_local_stats.test.ts index 2392ac570ecbc1..fa45438e00fbe3 100644 --- a/src/plugins/telemetry/server/telemetry_collection/get_local_stats.test.ts +++ b/src/plugins/telemetry/server/telemetry_collection/get_local_stats.test.ts @@ -14,7 +14,7 @@ import { usageCollectionPluginMock, createCollectorFetchContextMock, } from '../../../usage_collection/server/mocks'; -import { elasticsearchServiceMock, httpServerMock } from '../../../../../src/core/server/mocks'; +import { elasticsearchServiceMock } from '../../../../../src/core/server/mocks'; import { StatsCollectionConfig } from '../../../telemetry_collection_manager/server'; function mockUsageCollection(kibanaUsage = {}) { @@ -74,7 +74,6 @@ function mockStatsCollectionConfig( ...createCollectorFetchContextMock(), esClient: mockGetLocalStats(clusterInfo, clusterStats), usageCollection: mockUsageCollection(kibana), - kibanaRequest: httpServerMock.createKibanaRequest(), refreshCache: false, }; } diff --git a/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts b/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts index ae2a849ccfa19a..73de59ae8156aa 100644 --- a/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts +++ b/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts @@ -65,7 +65,7 @@ export const getLocalStats: StatsGetter = async ( config, context ) => { - const { usageCollection, esClient, soClient, kibanaRequest } = config; + const { usageCollection, esClient, soClient } = config; return await Promise.all( clustersDetails.map(async (clustersDetail) => { @@ -73,7 +73,7 @@ export const getLocalStats: StatsGetter = async ( getClusterInfo(esClient), // cluster info getClusterStats(esClient), // cluster stats (not to be confused with cluster _state_) getNodesUsage(esClient), // nodes_usage info - getKibana(usageCollection, esClient, soClient, kibanaRequest), + getKibana(usageCollection, esClient, soClient), getDataTelemetry(esClient), ]); return handleLocalStats( diff --git a/src/plugins/telemetry/tsconfig.json b/src/plugins/telemetry/tsconfig.json index d50ccd563fe5ac..052d484447e428 100644 --- a/src/plugins/telemetry/tsconfig.json +++ b/src/plugins/telemetry/tsconfig.json @@ -17,10 +17,12 @@ ], "references": [ { "path": "../../core/tsconfig.json" }, + { "path": "../../plugins/home/tsconfig.json" }, { "path": "../../plugins/kibana_react/tsconfig.json" }, { "path": "../../plugins/kibana_utils/tsconfig.json" }, { "path": "../../plugins/screenshot_mode/tsconfig.json" }, { "path": "../../plugins/telemetry_collection_manager/tsconfig.json" }, - { "path": "../../plugins/usage_collection/tsconfig.json" } + { "path": "../../plugins/usage_collection/tsconfig.json" }, + { "path": "../../../x-pack/plugins/security/tsconfig.json" } ] } diff --git a/src/plugins/telemetry_collection_manager/server/plugin.test.ts b/src/plugins/telemetry_collection_manager/server/plugin.test.ts index ca932e92d98bdb..990e237b6b2724 100644 --- a/src/plugins/telemetry_collection_manager/server/plugin.test.ts +++ b/src/plugins/telemetry_collection_manager/server/plugin.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { coreMock, httpServerMock } from '../../../core/server/mocks'; +import { coreMock } from '../../../core/server/mocks'; import { usageCollectionPluginMock } from '../../usage_collection/server/mocks'; import { TelemetryCollectionManagerPlugin } from './plugin'; import type { BasicStatsPayload, CollectionStrategyConfig, StatsGetterConfig } from './types'; @@ -217,19 +217,17 @@ describe('Telemetry Collection Manager', () => { }); }); describe('unencrypted: true', () => { - const mockRequest = httpServerMock.createKibanaRequest(); const config: StatsGetterConfig = { unencrypted: true, - request: mockRequest, }; describe('getStats', () => { - test('getStats returns empty because clusterDetails returns empty, and the soClient is not an instance of the TelemetrySavedObjectsClient', async () => { + test('getStats returns empty because clusterDetails returns empty, and the soClient is an instance of the TelemetrySavedObjectsClient', async () => { collectionStrategy.clusterDetailsGetter.mockResolvedValue([]); await expect(setupApi.getStats(config)).resolves.toStrictEqual([]); expect( collectionStrategy.clusterDetailsGetter.mock.calls[0][0].soClient - ).not.toBeInstanceOf(TelemetrySavedObjectsClient); + ).toBeInstanceOf(TelemetrySavedObjectsClient); }); test('returns encrypted payload (assumes opted-in when no explicitly opted-out)', async () => { collectionStrategy.clusterDetailsGetter.mockResolvedValue([ @@ -249,7 +247,7 @@ describe('Telemetry Collection Manager', () => { expect( collectionStrategy.clusterDetailsGetter.mock.calls[0][0].soClient - ).not.toBeInstanceOf(TelemetrySavedObjectsClient); + ).toBeInstanceOf(TelemetrySavedObjectsClient); }); it('calls getStats with config { refreshCache: true } even if set to false', async () => { @@ -267,7 +265,6 @@ describe('Telemetry Collection Manager', () => { expect(getStatsCollectionConfig).toReturnWith( expect.objectContaining({ refreshCache: true, - kibanaRequest: mockRequest, }) ); @@ -281,7 +278,7 @@ describe('Telemetry Collection Manager', () => { await expect(setupApi.getOptInStats(true, config)).resolves.toStrictEqual([]); expect( collectionStrategy.clusterDetailsGetter.mock.calls[0][0].soClient - ).not.toBeInstanceOf(TelemetrySavedObjectsClient); + ).toBeInstanceOf(TelemetrySavedObjectsClient); }); test('returns results for opt-in true', async () => { @@ -296,7 +293,7 @@ describe('Telemetry Collection Manager', () => { ]); expect( collectionStrategy.clusterDetailsGetter.mock.calls[0][0].soClient - ).not.toBeInstanceOf(TelemetrySavedObjectsClient); + ).toBeInstanceOf(TelemetrySavedObjectsClient); }); test('returns results for opt-in false', async () => { @@ -311,7 +308,7 @@ describe('Telemetry Collection Manager', () => { ]); expect( collectionStrategy.clusterDetailsGetter.mock.calls[0][0].soClient - ).not.toBeInstanceOf(TelemetrySavedObjectsClient); + ).toBeInstanceOf(TelemetrySavedObjectsClient); }); }); }); diff --git a/src/plugins/telemetry_collection_manager/server/plugin.ts b/src/plugins/telemetry_collection_manager/server/plugin.ts index fad51ca1dbfde8..cffe736f8eeaf5 100644 --- a/src/plugins/telemetry_collection_manager/server/plugin.ts +++ b/src/plugins/telemetry_collection_manager/server/plugin.ts @@ -126,11 +126,10 @@ export class TelemetryCollectionManagerPlugin const esClient = this.getElasticsearchClient(config); const soClient = this.getSavedObjectsClient(config); // Provide the kibanaRequest so opted-in plugins can scope their custom clients only if the request is not encrypted - const kibanaRequest = config.unencrypted ? config.request : void 0; const refreshCache = config.unencrypted ? true : !!config.refreshCache; if (esClient && soClient) { - return { usageCollection, esClient, soClient, kibanaRequest, refreshCache }; + return { usageCollection, esClient, soClient, refreshCache }; } } @@ -142,9 +141,7 @@ export class TelemetryCollectionManagerPlugin * @private */ private getElasticsearchClient(config: StatsGetterConfig): ElasticsearchClient | undefined { - return config.unencrypted - ? this.elasticsearchClient?.asScoped(config.request).asCurrentUser - : this.elasticsearchClient?.asInternalUser; + return this.elasticsearchClient?.asInternalUser; } /** @@ -155,11 +152,7 @@ export class TelemetryCollectionManagerPlugin * @private */ private getSavedObjectsClient(config: StatsGetterConfig): SavedObjectsClientContract | undefined { - if (config.unencrypted) { - // Intentionally using the scoped client here to make use of all the security wrappers. - // It also returns spaces-scoped telemetry. - return this.savedObjectsService?.getScopedClient(config.request); - } else if (this.savedObjectsService) { + if (this.savedObjectsService) { // Wrapping the internalRepository with the `TelemetrySavedObjectsClient` // to ensure some best practices when collecting "all the telemetry" // (i.e.: `.find` requests should query all spaces) diff --git a/src/plugins/telemetry_collection_manager/server/types.ts b/src/plugins/telemetry_collection_manager/server/types.ts index 7ea32844a858cb..9658c0d68d05db 100644 --- a/src/plugins/telemetry_collection_manager/server/types.ts +++ b/src/plugins/telemetry_collection_manager/server/types.ts @@ -6,14 +6,9 @@ * Side Public License, v 1. */ -import { - ElasticsearchClient, - Logger, - KibanaRequest, - SavedObjectsClientContract, -} from 'src/core/server'; -import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { TelemetryCollectionManagerPlugin } from './plugin'; +import type { ElasticsearchClient, Logger, SavedObjectsClientContract } from 'src/core/server'; +import type { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import type { TelemetryCollectionManagerPlugin } from './plugin'; export interface TelemetryCollectionManagerPluginSetup { setCollectionStrategy: ( @@ -36,7 +31,6 @@ export interface TelemetryOptInStats { export interface BaseStatsGetterConfig { unencrypted: boolean; refreshCache?: boolean; - request?: KibanaRequest; } export interface EncryptedStatsGetterConfig extends BaseStatsGetterConfig { @@ -45,7 +39,6 @@ export interface EncryptedStatsGetterConfig extends BaseStatsGetterConfig { export interface UnencryptedStatsGetterConfig extends BaseStatsGetterConfig { unencrypted: true; - request: KibanaRequest; } export interface ClusterDetails { @@ -56,7 +49,6 @@ export interface StatsCollectionConfig { usageCollection: UsageCollectionSetup; esClient: ElasticsearchClient; soClient: SavedObjectsClientContract; - kibanaRequest: KibanaRequest | undefined; // intentionally `| undefined` to enforce providing the parameter refreshCache: boolean; } diff --git a/src/plugins/usage_collection/README.mdx b/src/plugins/usage_collection/README.mdx index a58f197818bf4e..03d8f7badb8c2a 100644 --- a/src/plugins/usage_collection/README.mdx +++ b/src/plugins/usage_collection/README.mdx @@ -297,8 +297,7 @@ Some background: - `isReady` (added in v7.2.0 and v6.8.4) is a way for a usage collector to announce that some async process must finish first before it can return data in the `fetch` method (e.g. a client needs to ne initialized, or the task manager needs to run a task first). If any collector reports that it is not ready when we call its `fetch` method, we reset a flag to try again and, after a set amount of time, collect data from those collectors that are ready and skip any that are not. This means that if a collector returns `true` for `isReady` and it actually isn't ready to return data, there won't be telemetry data from that collector in that telemetry report (usually once per day). You should consider what it means if your collector doesn't return data in the first few documents when Kibana starts or, if we should wait for any other reason (e.g. the task manager needs to run your task first). If you need to tell telemetry collection to wait, you should implement this function with custom logic. If your `fetch` method can run without the need of any previous dependencies, then you can return true for `isReady` as shown in the example below. -- The `fetch` method needs to support multiple contexts in which it is called. For example, when a user requests the example of what we collect in the **Kibana>Advanced Settings>Usage data** section, the clients provided in the context of the function (`CollectorFetchContext`) are scoped to that user's privileges. The reason is to avoid exposing via telemetry any data that user should not have access to (i.e.: if the user does not have access to certain indices, they shouldn't be allowed to see the number of documents that exists in it). In this case, the `fetch` method receives the clients `esClient` and `soClient` scoped to the user who performed the HTTP API request. Alternatively, when requesting the usage data to be reported to the Remote Telemetry Service, the clients are scoped to the internal Kibana user (`kibana_system`). Please, mind it might have lower-level access than the default super-admin `elastic` test user. -In some scenarios, your collector might need to maintain its own client. An example of that is the `monitoring` plugin, that maintains a connection to the Remote Monitoring Cluster to push its monitoring data. If that's the case, your plugin can opt-in to receive the additional `kibanaRequest` parameter by adding `extendFetchContext.kibanaRequest: true` to the collector's config: it will be appended to the context of the `fetch` method only if the request needs to be scoped to a user other than Kibana Internal, so beware that your collector will need to work for both scenarios (especially for the scenario when `kibanaRequest` is missing). +- The clients provided to the `fetch` method are scoped to the internal Kibana user (`kibana_system`). Note: there will be many cases where you won't need to use the `esClient` or `soClient` function that gets passed in to your `fetch` method at all. Your feature might have an accumulating value in server memory, or read something from the OS. diff --git a/src/plugins/usage_collection/server/collector/collector.ts b/src/plugins/usage_collection/server/collector/collector.ts index 74373d44a359b6..1ff04cf3650c0b 100644 --- a/src/plugins/usage_collection/server/collector/collector.ts +++ b/src/plugins/usage_collection/server/collector/collector.ts @@ -7,20 +7,14 @@ */ import type { Logger } from 'src/core/server'; -import type { - CollectorFetchMethod, - CollectorOptions, - CollectorOptionsFetchExtendedContext, - ICollector, -} from './types'; +import type { CollectorFetchMethod, CollectorOptions, ICollector } from './types'; export class Collector implements ICollector { - public readonly extendFetchContext: CollectorOptionsFetchExtendedContext; - public readonly type: CollectorOptions['type']; - public readonly fetch: CollectorFetchMethod; - public readonly isReady: CollectorOptions['isReady']; + public readonly type: CollectorOptions['type']; + public readonly fetch: CollectorFetchMethod; + public readonly isReady: CollectorOptions['isReady']; /** * @private Constructor of a Collector. It should be called via the CollectorSet factory methods: `makeStatsCollector` and `makeUsageCollector` * @param log {@link Logger} @@ -28,15 +22,7 @@ export class Collector */ constructor( public readonly log: Logger, - { - type, - fetch, - isReady, - extendFetchContext = {}, - ...options - }: // Any does not affect here, but needs to be set so it doesn't affect anything else down the line - // eslint-disable-next-line @typescript-eslint/no-explicit-any - CollectorOptions + { type, fetch, isReady, ...options }: CollectorOptions ) { if (type === undefined) { throw new Error('Collector must be instantiated with a options.type string property'); @@ -50,6 +36,5 @@ export class Collector this.type = type; this.fetch = fetch; this.isReady = typeof isReady === 'function' ? isReady : () => true; - this.extendFetchContext = extendFetchContext; } } diff --git a/src/plugins/usage_collection/server/collector/collector_set.test.ts b/src/plugins/usage_collection/server/collector/collector_set.test.ts index 5e0698b286f79b..87e841f3de4c54 100644 --- a/src/plugins/usage_collection/server/collector/collector_set.test.ts +++ b/src/plugins/usage_collection/server/collector/collector_set.test.ts @@ -15,7 +15,6 @@ import { elasticsearchServiceMock, loggingSystemMock, savedObjectsClientMock, - httpServerMock, executionContextServiceMock, } from '../../../../core/server/mocks'; import type { ExecutionContextSetup, Logger } from 'src/core/server'; @@ -39,7 +38,6 @@ describe('CollectorSet', () => { }); const mockEsClient = elasticsearchServiceMock.createClusterClient().asInternalUser; const mockSoClient = savedObjectsClientMock.create(); - const req = void 0; // No need to instantiate any KibanaRequest in these tests it('should throw an error if non-Collector type of object is registered', () => { const collectors = new CollectorSet(collectorSetConfig); @@ -88,7 +86,7 @@ describe('CollectorSet', () => { }) ); - const result = await collectors.bulkFetch(mockEsClient, mockSoClient, req); + const result = await collectors.bulkFetch(mockEsClient, mockSoClient); expect(logger.debug).toHaveBeenCalledTimes(2); expect(logger.debug).toHaveBeenCalledWith('Getting ready collectors'); expect(logger.debug).toHaveBeenCalledWith('Fetching data from MY_TEST_COLLECTOR collector'); @@ -121,7 +119,7 @@ describe('CollectorSet', () => { let result; try { - result = await collectors.bulkFetch(mockEsClient, mockSoClient, req); + result = await collectors.bulkFetch(mockEsClient, mockSoClient); } catch (err) { // Do nothing } @@ -150,7 +148,7 @@ describe('CollectorSet', () => { }) ); - const result = await collectors.bulkFetch(mockEsClient, mockSoClient, req); + const result = await collectors.bulkFetch(mockEsClient, mockSoClient); expect(result).toStrictEqual([ { type: 'MY_TEST_COLLECTOR', @@ -178,7 +176,7 @@ describe('CollectorSet', () => { }) ); - const result = await collectors.bulkFetch(mockEsClient, mockSoClient, req); + const result = await collectors.bulkFetch(mockEsClient, mockSoClient); expect(result).toStrictEqual([ { type: 'MY_TEST_COLLECTOR', @@ -269,50 +267,6 @@ describe('CollectorSet', () => { collectorSet = new CollectorSet(collectorSetConfig); }); - test('TS should hide kibanaRequest when not opted-in', () => { - collectorSet.makeStatsCollector({ - type: 'MY_TEST_COLLECTOR', - isReady: () => true, - schema: { test: { type: 'long' } }, - fetch: (ctx) => { - // @ts-expect-error - const { kibanaRequest } = ctx; - return { test: kibanaRequest ? 1 : 0 }; - }, - }); - }); - - test('TS should hide kibanaRequest when not opted-in (explicit false)', () => { - collectorSet.makeStatsCollector({ - type: 'MY_TEST_COLLECTOR', - isReady: () => true, - schema: { test: { type: 'long' } }, - fetch: (ctx) => { - // @ts-expect-error - const { kibanaRequest } = ctx; - return { test: kibanaRequest ? 1 : 0 }; - }, - extendFetchContext: { - kibanaRequest: false, - }, - }); - }); - - test('TS should allow using kibanaRequest when opted-in (explicit true)', () => { - collectorSet.makeStatsCollector({ - type: 'MY_TEST_COLLECTOR', - isReady: () => true, - schema: { test: { type: 'long' } }, - fetch: (ctx) => { - const { kibanaRequest } = ctx; - return { test: kibanaRequest ? 1 : 0 }; - }, - extendFetchContext: { - kibanaRequest: true, - }, - }); - }); - test('fetch can use the logger (TS allows it)', () => { const collector = collectorSet.makeStatsCollector({ type: 'MY_TEST_COLLECTOR', @@ -339,188 +293,6 @@ describe('CollectorSet', () => { collectorSet = new CollectorSet(collectorSetConfig); }); - describe('TS validations', () => { - describe('when types are inferred', () => { - test('TS should hide kibanaRequest when not opted-in', () => { - collectorSet.makeUsageCollector({ - type: 'MY_TEST_COLLECTOR', - isReady: () => true, - schema: { test: { type: 'long' } }, - fetch: (ctx) => { - // @ts-expect-error - const { kibanaRequest } = ctx; - return { test: kibanaRequest ? 1 : 0 }; - }, - }); - }); - - test('TS should hide kibanaRequest when not opted-in (explicit false)', () => { - collectorSet.makeUsageCollector({ - type: 'MY_TEST_COLLECTOR', - isReady: () => true, - schema: { test: { type: 'long' } }, - fetch: (ctx) => { - // @ts-expect-error - const { kibanaRequest } = ctx; - return { test: kibanaRequest ? 1 : 0 }; - }, - extendFetchContext: { - kibanaRequest: false, - }, - }); - }); - - test('TS should allow using kibanaRequest when opted-in (explicit true)', () => { - collectorSet.makeUsageCollector({ - type: 'MY_TEST_COLLECTOR', - isReady: () => true, - schema: { test: { type: 'long' } }, - fetch: (ctx) => { - const { kibanaRequest } = ctx; - return { test: kibanaRequest ? 1 : 0 }; - }, - extendFetchContext: { - kibanaRequest: true, - }, - }); - }); - }); - - describe('when types are explicit', () => { - test('TS should hide `kibanaRequest` from ctx when undefined or false', () => { - collectorSet.makeUsageCollector<{ test: number }>({ - type: 'MY_TEST_COLLECTOR', - isReady: () => true, - schema: { test: { type: 'long' } }, - fetch: (ctx) => { - // @ts-expect-error - const { kibanaRequest } = ctx; - return { test: kibanaRequest ? 1 : 0 }; - }, - }); - collectorSet.makeUsageCollector<{ test: number }, false>({ - type: 'MY_TEST_COLLECTOR', - isReady: () => true, - schema: { test: { type: 'long' } }, - fetch: (ctx) => { - // @ts-expect-error - const { kibanaRequest } = ctx; - return { test: kibanaRequest ? 1 : 0 }; - }, - extendFetchContext: { - kibanaRequest: false, - }, - }); - collectorSet.makeUsageCollector<{ test: number }, false>({ - type: 'MY_TEST_COLLECTOR', - isReady: () => true, - schema: { test: { type: 'long' } }, - fetch: (ctx) => { - // @ts-expect-error - const { kibanaRequest } = ctx; - return { test: kibanaRequest ? 1 : 0 }; - }, - }); - }); - test('TS should not allow `true` when types declare false', () => { - // false is the default when at least 1 type is specified - collectorSet.makeUsageCollector<{ test: number }>({ - type: 'MY_TEST_COLLECTOR', - isReady: () => true, - schema: { test: { type: 'long' } }, - fetch: (ctx) => { - // @ts-expect-error - const { kibanaRequest } = ctx; - return { test: kibanaRequest ? 1 : 0 }; - }, - extendFetchContext: { - // @ts-expect-error - kibanaRequest: true, - }, - }); - collectorSet.makeUsageCollector<{ test: number }, false>({ - type: 'MY_TEST_COLLECTOR', - isReady: () => true, - schema: { test: { type: 'long' } }, - fetch: (ctx) => { - // @ts-expect-error - const { kibanaRequest } = ctx; - return { test: kibanaRequest ? 1 : 0 }; - }, - extendFetchContext: { - // @ts-expect-error - kibanaRequest: true, - }, - }); - }); - - test('TS should allow `true` when types explicitly declare `true` and do not allow `false` or undefined', () => { - // false is the default when at least 1 type is specified - collectorSet.makeUsageCollector<{ test: number }, true>({ - type: 'MY_TEST_COLLECTOR', - isReady: () => true, - schema: { test: { type: 'long' } }, - fetch: (ctx) => { - const { kibanaRequest } = ctx; - return { test: kibanaRequest ? 1 : 0 }; - }, - extendFetchContext: { - kibanaRequest: true, - }, - }); - collectorSet.makeUsageCollector<{ test: number }, true>({ - type: 'MY_TEST_COLLECTOR', - isReady: () => true, - schema: { test: { type: 'long' } }, - fetch: (ctx) => { - const { kibanaRequest } = ctx; - return { test: kibanaRequest ? 1 : 0 }; - }, - extendFetchContext: { - // @ts-expect-error - kibanaRequest: false, - }, - }); - collectorSet.makeUsageCollector<{ test: number }, true>({ - type: 'MY_TEST_COLLECTOR', - isReady: () => true, - schema: { test: { type: 'long' } }, - fetch: (ctx) => { - const { kibanaRequest } = ctx; - return { test: kibanaRequest ? 1 : 0 }; - }, - extendFetchContext: { - // @ts-expect-error - kibanaRequest: undefined, - }, - }); - collectorSet.makeUsageCollector<{ test: number }, true>({ - type: 'MY_TEST_COLLECTOR', - isReady: () => true, - schema: { test: { type: 'long' } }, - fetch: (ctx) => { - const { kibanaRequest } = ctx; - return { test: kibanaRequest ? 1 : 0 }; - }, - // @ts-expect-error - extendFetchContext: {}, - }); - collectorSet.makeUsageCollector<{ test: number }, true>( - // @ts-expect-error - { - type: 'MY_TEST_COLLECTOR', - isReady: () => true, - schema: { test: { type: 'long' } }, - fetch: (ctx) => { - const { kibanaRequest } = ctx; - return { test: kibanaRequest ? 1 : 0 }; - }, - } - ); - }); - }); - }); - test('fetch can use the logger (TS allows it)', () => { const collector = collectorSet.makeUsageCollector({ type: 'MY_TEST_COLLECTOR', @@ -777,31 +549,5 @@ describe('CollectorSet', () => { expect.any(Function) ); }); - - it('adds extra context to collectors with extendFetchContext config', async () => { - const mockReadyFetch = jest.fn().mockResolvedValue({}); - collectorSet.registerCollector( - collectorSet.makeUsageCollector({ - type: 'ready_col', - isReady: () => true, - schema: {}, - fetch: mockReadyFetch, - extendFetchContext: { kibanaRequest: true }, - }) - ); - - const mockEsClient = elasticsearchServiceMock.createClusterClient().asInternalUser; - const mockSoClient = savedObjectsClientMock.create(); - const request = httpServerMock.createKibanaRequest(); - const results = await collectorSet.bulkFetch(mockEsClient, mockSoClient, request); - - expect(mockReadyFetch).toBeCalledTimes(1); - expect(mockReadyFetch).toBeCalledWith({ - esClient: mockEsClient, - soClient: mockSoClient, - kibanaRequest: request, - }); - expect(results).toHaveLength(2); - }); }); }); diff --git a/src/plugins/usage_collection/server/collector/collector_set.ts b/src/plugins/usage_collection/server/collector/collector_set.ts index 49332b0a1826fc..3a7c0a66ac60d1 100644 --- a/src/plugins/usage_collection/server/collector/collector_set.ts +++ b/src/plugins/usage_collection/server/collector/collector_set.ts @@ -11,7 +11,6 @@ import type { Logger, ElasticsearchClient, SavedObjectsClientContract, - KibanaRequest, KibanaExecutionContext, ExecutionContextSetup, } from 'src/core/server'; @@ -64,12 +63,8 @@ export class CollectorSet { * Instantiates a stats collector with the definition provided in the options * @param options Definition of the collector {@link CollectorOptions} */ - public makeStatsCollector = < - TFetchReturn, - WithKibanaRequest extends boolean, - ExtraOptions extends object = {} - >( - options: CollectorOptions + public makeStatsCollector = ( + options: CollectorOptions ) => { return new Collector(this.logger, options); }; @@ -78,15 +73,8 @@ export class CollectorSet { * Instantiates an usage collector with the definition provided in the options * @param options Definition of the collector {@link CollectorOptions} */ - public makeUsageCollector = < - TFetchReturn, - // TODO: Right now, users will need to explicitly claim `true` for TS to allow `kibanaRequest` usage. - // If we improve `telemetry-check-tools` so plugins do not need to specify TFetchReturn, - // we'll be able to remove the type defaults and TS will successfully infer the config value as provided in JS. - WithKibanaRequest extends boolean = false, - ExtraOptions extends object = {} - >( - options: UsageCollectorOptions + public makeUsageCollector = ( + options: UsageCollectorOptions ) => { return new UsageCollector(this.logger, options); }; @@ -191,7 +179,6 @@ export class CollectorSet { public bulkFetch = async ( esClient: ElasticsearchClient, soClient: SavedObjectsClientContract, - kibanaRequest: KibanaRequest | undefined, // intentionally `| undefined` to enforce providing the parameter collectors: Map = this.collectors ) => { this.logger.debug(`Getting ready collectors`); @@ -209,11 +196,7 @@ export class CollectorSet { readyCollectors.map(async (collector) => { this.logger.debug(`Fetching data from ${collector.type} collector`); try { - const context = { - esClient, - soClient, - ...(collector.extendFetchContext.kibanaRequest && { kibanaRequest }), - }; + const context = { esClient, soClient }; const executionContext: KibanaExecutionContext = { type: 'usage_collection', name: 'collector.fetch', @@ -254,16 +237,10 @@ export class CollectorSet { public bulkFetchUsage = async ( esClient: ElasticsearchClient, - savedObjectsClient: SavedObjectsClientContract, - kibanaRequest: KibanaRequest | undefined // intentionally `| undefined` to enforce providing the parameter + savedObjectsClient: SavedObjectsClientContract ) => { const usageCollectors = this.getFilteredCollectorSet((c) => c instanceof UsageCollector); - return await this.bulkFetch( - esClient, - savedObjectsClient, - kibanaRequest, - usageCollectors.collectors - ); + return await this.bulkFetch(esClient, savedObjectsClient, usageCollectors.collectors); }; /** diff --git a/src/plugins/usage_collection/server/collector/index.ts b/src/plugins/usage_collection/server/collector/index.ts index ca240a520ee24a..e284844b34c344 100644 --- a/src/plugins/usage_collection/server/collector/index.ts +++ b/src/plugins/usage_collection/server/collector/index.ts @@ -17,7 +17,6 @@ export type { CollectorOptions, CollectorFetchContext, CollectorFetchMethod, - CollectorOptionsFetchExtendedContext, ICollector as Collector, } from './types'; export type { UsageCollectorOptions } from './usage_collector'; diff --git a/src/plugins/usage_collection/server/collector/types.ts b/src/plugins/usage_collection/server/collector/types.ts index bf1e9f4644b1b7..8d427d211a191b 100644 --- a/src/plugins/usage_collection/server/collector/types.ts +++ b/src/plugins/usage_collection/server/collector/types.ts @@ -6,12 +6,7 @@ * Side Public License, v 1. */ -import type { - ElasticsearchClient, - KibanaRequest, - SavedObjectsClientContract, - Logger, -} from 'src/core/server'; +import type { ElasticsearchClient, SavedObjectsClientContract, Logger } from 'src/core/server'; /** Types matching number values **/ export type AllowedSchemaNumberTypes = @@ -73,7 +68,7 @@ export type MakeSchemaFrom = { * * @remark Bear in mind when testing your collector that your user has the same privileges as the Kibana Internal user to ensure the expected data is sent to the remote cluster. */ -export type CollectorFetchContext = { +export interface CollectorFetchContext { /** * Request-scoped Elasticsearch client * @remark Bear in mind when testing your collector that your user has the same privileges as the Kibana Internal user to ensure the expected data is sent to the remote cluster (more info: {@link CollectorFetchContext}) @@ -84,58 +79,22 @@ export type CollectorFetchContext = ( +export type CollectorFetchMethod = ( this: ICollector & ExtraOptions, // Specify the context of `this` for this.log and others to become available - context: CollectorFetchContext + context: CollectorFetchContext ) => Promise | TReturn; -export interface ICollectorOptionsFetchExtendedContext { - /** - * Set to `true` if your `fetch` method requires the `KibanaRequest` object to be added in its context {@link CollectorFetchContextWithRequest}. - * @remark You should fully acknowledge that by using the `KibanaRequest` in your collector, you need to ensure it should specially work without it because it won't be provided when building the telemetry payload actually sent to the remote telemetry service. - */ - kibanaRequest?: WithKibanaRequest; -} - -/** - * The options to extend the context provided to the `fetch` method. - * @remark Only to be used in very rare scenarios when this is really needed. - */ -export type CollectorOptionsFetchExtendedContext = - ICollectorOptionsFetchExtendedContext & - (WithKibanaRequest extends true // If enforced to true via Types, the config must be expected - ? Required, 'kibanaRequest'>> - : {}); - /** * Options to instantiate a collector */ -export type CollectorOptions< - TFetchReturn = unknown, - WithKibanaRequest extends boolean = boolean, - ExtraOptions extends object = {} -> = { +export type CollectorOptions = { /** * Unique string identifier for the collector */ @@ -152,17 +111,8 @@ export type CollectorOptions< * The method that will collect and return the data in the final format. * @param collectorFetchContext {@link CollectorFetchContext} */ - fetch: CollectorFetchMethod; -} & ExtraOptions & - (WithKibanaRequest extends true // If enforced to true via Types, the config must be enforced - ? { - /** {@link CollectorOptionsFetchExtendedContext} **/ - extendFetchContext: CollectorOptionsFetchExtendedContext; - } - : { - /** {@link CollectorOptionsFetchExtendedContext} **/ - extendFetchContext?: CollectorOptionsFetchExtendedContext; - }); + fetch: CollectorFetchMethod; +} & ExtraOptions; /** * Common interface for Usage and Stats Collectors @@ -170,13 +120,8 @@ export type CollectorOptions< export interface ICollector { /** Logger **/ readonly log: Logger; - /** - * The options to extend the context provided to the `fetch` method: {@link CollectorOptionsFetchExtendedContext}. - * @remark Only to be used in very rare scenarios when this is really needed. - */ - readonly extendFetchContext: CollectorOptionsFetchExtendedContext; /** The registered type (aka name) of the collector **/ - readonly type: CollectorOptions['type']; + readonly type: CollectorOptions['type']; /** * The actual logic that reports the Usage collection. * It will be called on every collection request. @@ -188,9 +133,9 @@ export interface ICollector { * [type]: await fetch(context) * } */ - readonly fetch: CollectorFetchMethod; + readonly fetch: CollectorFetchMethod; /** * Should return `true` when it's safe to call the `fetch` method. */ - readonly isReady: CollectorOptions['isReady']; + readonly isReady: CollectorOptions['isReady']; } diff --git a/src/plugins/usage_collection/server/collector/usage_collector.ts b/src/plugins/usage_collection/server/collector/usage_collector.ts index 15f7cd9c627fcb..2ed8c2a50dbafd 100644 --- a/src/plugins/usage_collection/server/collector/usage_collector.ts +++ b/src/plugins/usage_collection/server/collector/usage_collector.ts @@ -15,10 +15,9 @@ import { Collector } from './collector'; */ export type UsageCollectorOptions< TFetchReturn = unknown, - WithKibanaRequest extends boolean = false, ExtraOptions extends object = {} -> = CollectorOptions & - Required, 'schema'>>; +> = CollectorOptions & + Required, 'schema'>>; /** * @private Only used in fixtures as a type @@ -27,12 +26,7 @@ export class UsageCollector exte TFetchReturn, ExtraOptions > { - constructor( - log: Logger, - // Needed because it doesn't affect on anything here but being explicit creates a lot of pain down the line - // eslint-disable-next-line @typescript-eslint/no-explicit-any - collectorOptions: UsageCollectorOptions - ) { + constructor(log: Logger, collectorOptions: UsageCollectorOptions) { super(log, collectorOptions); } } diff --git a/src/plugins/usage_collection/server/index.ts b/src/plugins/usage_collection/server/index.ts index 74fa77be9843cb..907a61a752052a 100644 --- a/src/plugins/usage_collection/server/index.ts +++ b/src/plugins/usage_collection/server/index.ts @@ -17,7 +17,6 @@ export type { UsageCollectorOptions, CollectorFetchContext, CollectorFetchMethod, - CollectorOptionsFetchExtendedContext, } from './collector'; export type { diff --git a/src/plugins/usage_collection/server/mocks.ts b/src/plugins/usage_collection/server/mocks.ts index 6f7d4f19cbaf12..ac7ad69ed4bce7 100644 --- a/src/plugins/usage_collection/server/mocks.ts +++ b/src/plugins/usage_collection/server/mocks.ts @@ -9,7 +9,6 @@ import { elasticsearchServiceMock, executionContextServiceMock, - httpServerMock, loggingSystemMock, savedObjectsClientMock, } from '../../../../src/core/server/mocks'; @@ -45,25 +44,14 @@ export const createUsageCollectionSetupMock = () => { return usageCollectionSetupMock; }; -export function createCollectorFetchContextMock(): jest.Mocked> { - const collectorFetchClientsMock: jest.Mocked> = { +export function createCollectorFetchContextMock(): jest.Mocked { + const collectorFetchClientsMock: jest.Mocked = { esClient: elasticsearchServiceMock.createClusterClient().asInternalUser, soClient: savedObjectsClientMock.create(), }; return collectorFetchClientsMock; } -export function createCollectorFetchContextWithKibanaMock(): jest.Mocked< - CollectorFetchContext -> { - const collectorFetchClientsMock: jest.Mocked> = { - esClient: elasticsearchServiceMock.createClusterClient().asInternalUser, - soClient: savedObjectsClientMock.create(), - kibanaRequest: httpServerMock.createKibanaRequest(), - }; - return collectorFetchClientsMock; -} - export const usageCollectionPluginMock = { createSetupContract: createUsageCollectionSetupMock, }; diff --git a/src/plugins/usage_collection/server/plugin.ts b/src/plugins/usage_collection/server/plugin.ts index f415dd768dc226..7cde8bad706dd1 100644 --- a/src/plugins/usage_collection/server/plugin.ts +++ b/src/plugins/usage_collection/server/plugin.ts @@ -15,7 +15,6 @@ import type { Plugin, ElasticsearchClient, SavedObjectsClientContract, - KibanaRequest, } from 'src/core/server'; import type { ConfigType } from './config'; import { CollectorSet } from './collector'; @@ -39,12 +38,8 @@ export interface UsageCollectionSetup { * Creates a usage collector to collect plugin telemetry data. * registerCollector must be called to connect the created collector with the service. */ - makeUsageCollector: < - TFetchReturn, - WithKibanaRequest extends boolean = false, - ExtraOptions extends object = {} - >( - options: UsageCollectorOptions + makeUsageCollector: ( + options: UsageCollectorOptions ) => Collector; /** * Register a usage collector or a stats collector. @@ -66,7 +61,6 @@ export interface UsageCollectionSetup { bulkFetch: ( esClient: ElasticsearchClient, soClient: SavedObjectsClientContract, - kibanaRequest: KibanaRequest | undefined, // intentionally `| undefined` to enforce providing the parameter collectors?: Map> ) => Promise>; /** @@ -88,12 +82,8 @@ export interface UsageCollectionSetup { * registerCollector must be called to connect the created collector with the service. * @internal: telemetry and monitoring use */ - makeStatsCollector: < - TFetchReturn, - WithKibanaRequest extends boolean, - ExtraOptions extends object = {} - >( - options: CollectorOptions + makeStatsCollector: ( + options: CollectorOptions ) => Collector; } diff --git a/src/plugins/usage_collection/server/routes/stats/stats.ts b/src/plugins/usage_collection/server/routes/stats/stats.ts index 8e5382d1631721..72cbd2e5899ff5 100644 --- a/src/plugins/usage_collection/server/routes/stats/stats.ts +++ b/src/plugins/usage_collection/server/routes/stats/stats.ts @@ -15,7 +15,6 @@ import { first } from 'rxjs/operators'; import { ElasticsearchClient, IRouter, - KibanaRequest, MetricsServiceSetup, SavedObjectsClientContract, ServiceStatus, @@ -55,10 +54,9 @@ export function registerStatsRoute({ }) { const getUsage = async ( esClient: ElasticsearchClient, - savedObjectsClient: SavedObjectsClientContract, - kibanaRequest: KibanaRequest + savedObjectsClient: SavedObjectsClientContract ): Promise => { - const usage = await collectorSet.bulkFetchUsage(esClient, savedObjectsClient, kibanaRequest); + const usage = await collectorSet.bulkFetchUsage(esClient, savedObjectsClient); return collectorSet.toObject(usage); }; @@ -97,7 +95,7 @@ export function registerStatsRoute({ const [usage, clusterUuid] = await Promise.all([ shouldGetUsage - ? getUsage(asCurrentUser, savedObjectsClient, req) + ? getUsage(asCurrentUser, savedObjectsClient) : Promise.resolve({}), getClusterUuid(asCurrentUser), ]); diff --git a/src/plugins/vis_default_editor/public/components/agg_params_map.ts b/src/plugins/vis_default_editor/public/components/agg_params_map.ts index a61df61f2316c6..283e1d7511b750 100644 --- a/src/plugins/vis_default_editor/public/components/agg_params_map.ts +++ b/src/plugins/vis_default_editor/public/components/agg_params_map.ts @@ -67,6 +67,11 @@ const metrics = { sortField: controls.TopSortFieldParamEditor, sortOrder: controls.OrderParamEditor, }, + [METRIC_TYPES.TOP_METRICS]: { + field: controls.FieldParamEditor, + sortField: controls.TopSortFieldParamEditor, + sortOrder: controls.OrderParamEditor, + }, [METRIC_TYPES.PERCENTILES]: { percents: controls.PercentilesEditor, }, diff --git a/src/plugins/vis_default_editor/public/components/controls/metric_agg.tsx b/src/plugins/vis_default_editor/public/components/controls/metric_agg.tsx index 1f844b50424746..2888d399bc0148 100644 --- a/src/plugins/vis_default_editor/public/components/controls/metric_agg.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/metric_agg.tsx @@ -13,7 +13,14 @@ import { i18n } from '@kbn/i18n'; import { useAvailableOptions, useFallbackMetric, useValidation } from './utils'; import { AggParamEditorProps } from '../agg_param_props'; -const aggFilter = ['!top_hits', '!percentiles', '!percentile_ranks', '!median', '!std_dev']; +const aggFilter = [ + '!top_hits', + '!top_metrics', + '!percentiles', + '!percentile_ranks', + '!median', + '!std_dev', +]; const EMPTY_VALUE = 'EMPTY_VALUE'; const DEFAULT_OPTIONS = [{ text: '', value: EMPTY_VALUE, hidden: true }]; diff --git a/src/plugins/visualizations/tsconfig.json b/src/plugins/visualizations/tsconfig.json index 57d21d8719ede3..2bc25cfb3c3463 100644 --- a/src/plugins/visualizations/tsconfig.json +++ b/src/plugins/visualizations/tsconfig.json @@ -30,6 +30,7 @@ { "path": "../home/tsconfig.json" }, { "path": "../share/tsconfig.json" }, { "path": "../presentation_util/tsconfig.json" }, + { "path": "../screenshot_mode/tsconfig.json" }, { "path": "../../../x-pack/plugins/spaces/tsconfig.json" } ] } diff --git a/test/functional/apps/management/_scripted_fields_filter.ts b/test/functional/apps/management/_scripted_fields_filter.ts index 82d15908197506..4f6d1a41d05237 100644 --- a/test/functional/apps/management/_scripted_fields_filter.ts +++ b/test/functional/apps/management/_scripted_fields_filter.ts @@ -17,7 +17,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const PageObjects = getPageObjects(['settings']); - describe('filter scripted fields', function describeIndexTests() { + // FLAKY: https://github.com/elastic/kibana/issues/126027 + describe.skip('filter scripted fields', function describeIndexTests() { before(async function () { // delete .kibana index and then wait for Kibana to re-create it await browser.setWindowSize(1200, 800); diff --git a/test/functional_ccs/apps/discover/data_view_ccs.ts b/test/functional_ccs/apps/discover/data_view_ccs.ts index 3bc7ed405b4de4..44258b9cbadd63 100644 --- a/test/functional_ccs/apps/discover/data_view_ccs.ts +++ b/test/functional_ccs/apps/discover/data_view_ccs.ts @@ -27,7 +27,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await testSubjects.click('saveIndexPatternButton'); }; - describe('discover integration with data view editor', function describeIndexTests() { + // FLAKY: https://github.com/elastic/kibana/issues/126658 + describe.skip('discover integration with data view editor', function describeIndexTests() { before(async function () { await security.testUser.setRoles([ 'kibana_admin', diff --git a/test/interpreter_functional/test_suites/run_pipeline/esaggs_topmetrics.ts b/test/interpreter_functional/test_suites/run_pipeline/esaggs_topmetrics.ts new file mode 100644 index 00000000000000..4f43709ba4a7e0 --- /dev/null +++ b/test/interpreter_functional/test_suites/run_pipeline/esaggs_topmetrics.ts @@ -0,0 +1,112 @@ +/* + * 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 expect from '@kbn/expect'; +import { ExpectExpression, expectExpressionProvider } from './helpers'; +import { FtrProviderContext } from '../../../functional/ftr_provider_context'; + +export default function ({ + getService, + updateBaselines, +}: FtrProviderContext & { updateBaselines: boolean }) { + let expectExpression: ExpectExpression; + + describe('esaggs_topmetrics', () => { + before(() => { + expectExpression = expectExpressionProvider({ getService, updateBaselines }); + }); + + const timeRange = { + from: '2015-09-21T00:00:00Z', + to: '2015-09-22T00:00:00Z', + }; + + describe('aggTopMetrics', () => { + it('can execute aggTopMetrics', async () => { + const expression = ` + kibana_context timeRange={timerange from='${timeRange.from}' to='${timeRange.to}'} + | esaggs index={indexPatternLoad id='logstash-*'} + aggs={aggTerms id="1" enabled=true schema="bucket" field="extension.raw"} + aggs={aggTopMetrics id="2" enabled=true schema="metric" field="bytes" sortField="@timestamp" sortOrder="desc" size=3 } + `; + const result = await expectExpression('aggTopMetrics', expression).getResponse(); + + expect(result.rows.map((r: { 'col-0-1': string }) => r['col-0-1'])).to.eql([ + 'jpg', + 'css', + 'png', + 'gif', + 'php', + ]); + + result.rows.forEach((r: { 'col-1-2': number[] }) => { + expect(r['col-1-2'].length).to.be(3); + expect( + r['col-1-2'].forEach((metric) => { + expect(typeof metric).to.be('number'); + }) + ); + }); + }); + + it('can execute aggTopMetrics with different sortOrder and size', async () => { + const expression = ` + kibana_context timeRange={timerange from='${timeRange.from}' to='${timeRange.to}'} + | esaggs index={indexPatternLoad id='logstash-*'} + aggs={aggTerms id="1" enabled=true schema="bucket" field="extension.raw"} + aggs={aggTopMetrics id="2" enabled=true schema="metric" field="bytes" sortField="@timestamp" sortOrder="asc" size=1 } + `; + const result = await expectExpression('aggTopMetrics', expression).getResponse(); + + expect(result.rows.map((r: { 'col-0-1': string }) => r['col-0-1'])).to.eql([ + 'jpg', + 'css', + 'png', + 'gif', + 'php', + ]); + + result.rows.forEach((r: { 'col-1-2': number[] }) => { + expect(typeof r['col-1-2']).to.be('number'); + }); + }); + + it('can use aggTopMetrics as an orderAgg of aggTerms', async () => { + const expressionSortBytesAsc = ` + kibana_context timeRange={timerange from='${timeRange.from}' to='${timeRange.to}'} + | esaggs index={indexPatternLoad id='logstash-*'} + aggs={aggTerms id="1" enabled=true schema="bucket" field="extension.raw" size=1 orderAgg={aggTopMetrics id="order" enabled=true schema="metric" field="bytes" sortField="@timestamp" sortOrder="asc" size=1}} + aggs={aggCount id="2" enabled=true schema="metric"} + `; + + const resultSortBytesAsc = await expectExpression( + 'sortBytesAsc', + expressionSortBytesAsc + ).getResponse(); + + const expressionSortBytesDesc = ` + kibana_context timeRange={timerange from='${timeRange.from}' to='${timeRange.to}'} + | esaggs index={indexPatternLoad id='logstash-*'} + aggs={aggTerms id="1" enabled=true schema="bucket" field="extension.raw" size=1 orderAgg={aggTopMetrics id="order" enabled=true schema="metric" field="bytes" sortField="@timestamp" sortOrder="desc" size=1}} + aggs={aggCount id="2" enabled=true schema="metric"} + `; + + const resultSortBytesDesc = await expectExpression( + 'sortBytesDesc', + expressionSortBytesDesc + ).getResponse(); + + expect(resultSortBytesAsc.rows.length).to.be(1); + expect(resultSortBytesAsc.rows[0]['col-0-1']).to.be('jpg'); + + expect(resultSortBytesDesc.rows.length).to.be(1); + expect(resultSortBytesDesc.rows[0]['col-0-1']).to.be('php'); + }); + }); + }); +} diff --git a/test/interpreter_functional/test_suites/run_pipeline/index.ts b/test/interpreter_functional/test_suites/run_pipeline/index.ts index 97387fc0a965fc..e24563a5918eb8 100644 --- a/test/interpreter_functional/test_suites/run_pipeline/index.ts +++ b/test/interpreter_functional/test_suites/run_pipeline/index.ts @@ -25,6 +25,7 @@ export default function ({ getService, getPageObjects, loadTestFile }: FtrProvid await kibanaServer.uiSettings.replace({ 'dateFormat:tz': 'Australia/North', defaultIndex: 'logstash-*', + 'bfetch:disableCompression': true, // makes it easier to debug while developing tests }); await browser.setWindowSize(1300, 900); await PageObjects.common.navigateToApp('settings'); @@ -47,5 +48,6 @@ export default function ({ getService, getPageObjects, loadTestFile }: FtrProvid loadTestFile(require.resolve('./esaggs_sampler')); loadTestFile(require.resolve('./esaggs_significanttext')); loadTestFile(require.resolve('./esaggs_rareterms')); + loadTestFile(require.resolve('./esaggs_topmetrics')); }); } diff --git a/x-pack/plugins/alerting/server/lib/wrap_scoped_cluster_client.test.ts b/x-pack/plugins/alerting/server/lib/wrap_scoped_cluster_client.test.ts index 15f5a37edb910e..d6101c9c6cec4f 100644 --- a/x-pack/plugins/alerting/server/lib/wrap_scoped_cluster_client.test.ts +++ b/x-pack/plugins/alerting/server/lib/wrap_scoped_cluster_client.test.ts @@ -9,7 +9,6 @@ import { Client } from '@elastic/elasticsearch'; import { loggingSystemMock } from 'src/core/server/mocks'; import { elasticsearchServiceMock } from '../../../../../src/core/server/mocks'; import { createWrappedScopedClusterClientFactory } from './wrap_scoped_cluster_client'; -import { ElasticsearchClientWithChild } from '../types'; const esQuery = { body: { query: { bool: { filter: { range: { '@timestamp': { gte: 0 } } } } } }, @@ -41,9 +40,7 @@ describe('wrapScopedClusterClient', () => { const scopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); const childClient = elasticsearchServiceMock.createElasticsearchClient(); - ( - scopedClusterClient.asInternalUser as unknown as jest.Mocked - ).child.mockReturnValue(childClient as unknown as Client); + scopedClusterClient.asInternalUser.child.mockReturnValue(childClient as unknown as Client); const asInternalUserWrappedSearchFn = childClient.search; const wrappedSearchClient = createWrappedScopedClusterClientFactory({ @@ -62,9 +59,7 @@ describe('wrapScopedClusterClient', () => { const scopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); const childClient = elasticsearchServiceMock.createElasticsearchClient(); - ( - scopedClusterClient.asCurrentUser as unknown as jest.Mocked - ).child.mockReturnValue(childClient as unknown as Client); + scopedClusterClient.asCurrentUser.child.mockReturnValue(childClient as unknown as Client); const asCurrentUserWrappedSearchFn = childClient.search; const wrappedSearchClient = createWrappedScopedClusterClientFactory({ @@ -83,9 +78,7 @@ describe('wrapScopedClusterClient', () => { const scopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); const childClient = elasticsearchServiceMock.createElasticsearchClient(); - ( - scopedClusterClient.asInternalUser as unknown as jest.Mocked - ).child.mockReturnValue(childClient as unknown as Client); + scopedClusterClient.asInternalUser.child.mockReturnValue(childClient as unknown as Client); const asInternalUserWrappedSearchFn = childClient.search; const wrappedSearchClient = createWrappedScopedClusterClientFactory({ @@ -106,9 +99,7 @@ describe('wrapScopedClusterClient', () => { const scopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); const childClient = elasticsearchServiceMock.createElasticsearchClient(); - ( - scopedClusterClient.asInternalUser as unknown as jest.Mocked - ).child.mockReturnValue(childClient as unknown as Client); + scopedClusterClient.asInternalUser.child.mockReturnValue(childClient as unknown as Client); const asInternalUserWrappedSearchFn = childClient.search; asInternalUserWrappedSearchFn.mockRejectedValueOnce(new Error('something went wrong!')); @@ -127,9 +118,7 @@ describe('wrapScopedClusterClient', () => { const scopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); const childClient = elasticsearchServiceMock.createElasticsearchClient(); - ( - scopedClusterClient.asInternalUser as unknown as jest.Mocked - ).child.mockReturnValue(childClient as unknown as Client); + scopedClusterClient.asInternalUser.child.mockReturnValue(childClient as unknown as Client); const asInternalUserWrappedSearchFn = childClient.search; // @ts-ignore incomplete return type asInternalUserWrappedSearchFn.mockResolvedValue({}); @@ -156,9 +145,7 @@ describe('wrapScopedClusterClient', () => { const scopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); const childClient = elasticsearchServiceMock.createElasticsearchClient(); - ( - scopedClusterClient.asInternalUser as unknown as jest.Mocked - ).child.mockReturnValue(childClient as unknown as Client); + scopedClusterClient.asInternalUser.child.mockReturnValue(childClient as unknown as Client); const asInternalUserWrappedSearchFn = childClient.search; // @ts-ignore incomplete return type asInternalUserWrappedSearchFn.mockResolvedValue({ took: 333 }); diff --git a/x-pack/plugins/alerting/server/lib/wrap_scoped_cluster_client.ts b/x-pack/plugins/alerting/server/lib/wrap_scoped_cluster_client.ts index dfe32a48ce4384..2b71f95cd9f1c6 100644 --- a/x-pack/plugins/alerting/server/lib/wrap_scoped_cluster_client.ts +++ b/x-pack/plugins/alerting/server/lib/wrap_scoped_cluster_client.ts @@ -21,7 +21,7 @@ import type { AggregationsAggregate, } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { IScopedClusterClient, ElasticsearchClient, Logger } from 'src/core/server'; -import { ElasticsearchClientWithChild, RuleExecutionMetrics } from '../types'; +import { RuleExecutionMetrics } from '../types'; import { Alert as Rule } from '../types'; type RuleInfo = Pick & { spaceId: string }; @@ -87,8 +87,7 @@ function wrapScopedClusterClient(opts: WrapScopedClusterClientOpts): IScopedClus function wrapEsClient(opts: WrapEsClientOpts): ElasticsearchClient { const { esClient, ...rest } = opts; - // Core hides access to .child via TS - const wrappedClient = (esClient as ElasticsearchClientWithChild).child({}); + const wrappedClient = esClient.child({}); // Mutating the functions we want to wrap wrappedClient.search = getWrappedSearchFn({ esClient: wrappedClient, ...rest }); diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.ts index c05bdc3cf7bd94..ccf9659a8e67d1 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.ts @@ -142,7 +142,7 @@ export class TaskRunner< this.executionId = uuid.v4(); } - async getDecryptedAttributes( + private async getDecryptedAttributes( ruleId: string, spaceId: string ): Promise<{ apiKey: string | null; enabled: boolean }> { @@ -267,7 +267,7 @@ export class TaskRunner< } } - async executeAlert( + private async executeAlert( alertId: string, alert: CreatedAlert, executionHandler: ExecutionHandler @@ -283,7 +283,7 @@ export class TaskRunner< return executionHandler({ actionGroup, actionSubgroup, context, state, alertId }); } - async executeAlerts( + private async executeAlerts( fakeRequest: KibanaRequest, rule: SanitizedAlert, params: Params, @@ -548,7 +548,7 @@ export class TaskRunner< }; } - async validateAndExecuteRule( + private async validateAndExecuteRule( fakeRequest: KibanaRequest, apiKey: RawRule['apiKey'], rule: SanitizedAlert, @@ -574,7 +574,9 @@ export class TaskRunner< return this.executeAlerts(fakeRequest, rule, validatedParams, executionHandler, spaceId, event); } - async loadRuleAttributesAndRun(event: Event): Promise> { + private async loadRuleAttributesAndRun( + event: Event + ): Promise> { const { params: { alertId: ruleId, spaceId }, } = this.taskInstance; diff --git a/x-pack/plugins/alerting/server/types.ts b/x-pack/plugins/alerting/server/types.ts index 6b06f7efe30660..5499ba0c76caf6 100644 --- a/x-pack/plugins/alerting/server/types.ts +++ b/x-pack/plugins/alerting/server/types.ts @@ -5,12 +5,10 @@ * 2.0. */ -import { Client } from '@elastic/elasticsearch'; import type { IRouter, RequestHandlerContext, SavedObjectReference, - ElasticsearchClient, IUiSettingsClient, } from 'src/core/server'; import type { PublicMethodsOf } from '@kbn/utility-types'; @@ -48,10 +46,6 @@ import { IAbortableClusterClient } from './lib/create_abortable_es_client_factor export type WithoutQueryAndParams = Pick>; export type SpaceIdToNamespaceFunction = (spaceId?: string) => string | undefined; -export interface ElasticsearchClientWithChild extends ElasticsearchClient { - child: Client['child']; -} - /** * @public */ diff --git a/x-pack/plugins/cloud/public/fullstory.ts b/x-pack/plugins/cloud/public/fullstory.ts index 4f76abf540cae3..602b1c4cc63d37 100644 --- a/x-pack/plugins/cloud/public/fullstory.ts +++ b/x-pack/plugins/cloud/public/fullstory.ts @@ -15,9 +15,11 @@ export interface FullStoryDeps { } export type FullstoryUserVars = Record; +export type FullstoryVars = Record; export interface FullStoryApi { identify(userId: string, userVars?: FullstoryUserVars): void; + setVars(pageName: string, vars?: FullstoryVars): void; setUserVars(userVars?: FullstoryUserVars): void; event(eventName: string, eventProperties: Record): void; } diff --git a/x-pack/plugins/cloud/public/plugin.test.mocks.ts b/x-pack/plugins/cloud/public/plugin.test.mocks.ts index b79fb1bc651307..1c185d0194912a 100644 --- a/x-pack/plugins/cloud/public/plugin.test.mocks.ts +++ b/x-pack/plugins/cloud/public/plugin.test.mocks.ts @@ -11,6 +11,7 @@ import type { FullStoryDeps, FullStoryApi, FullStoryService } from './fullstory' export const fullStoryApiMock: jest.Mocked = { event: jest.fn(), setUserVars: jest.fn(), + setVars: jest.fn(), identify: jest.fn(), }; export const initializeFullStoryMock = jest.fn(() => ({ diff --git a/x-pack/plugins/cloud/public/plugin.test.ts b/x-pack/plugins/cloud/public/plugin.test.ts index 1eef581610f004..edbf724e25390e 100644 --- a/x-pack/plugins/cloud/public/plugin.test.ts +++ b/x-pack/plugins/cloud/public/plugin.test.ts @@ -12,6 +12,7 @@ import { securityMock } from '../../security/public/mocks'; import { fullStoryApiMock, initializeFullStoryMock } from './plugin.test.mocks'; import { CloudPlugin, CloudConfigType, loadFullStoryUserId } from './plugin'; import { Observable, Subject } from 'rxjs'; +import { KibanaExecutionContext } from 'kibana/public'; describe('Cloud Plugin', () => { describe('#setup', () => { @@ -24,12 +25,12 @@ describe('Cloud Plugin', () => { config = {}, securityEnabled = true, currentUserProps = {}, - currentAppId$ = undefined, + currentContext$ = undefined, }: { config?: Partial; securityEnabled?: boolean; currentUserProps?: Record; - currentAppId$?: Observable; + currentContext$?: Observable; }) => { const initContext = coreMock.createPluginInitializerContext({ id: 'cloudId', @@ -51,8 +52,8 @@ describe('Cloud Plugin', () => { const coreSetup = coreMock.createSetup(); const coreStart = coreMock.createStart(); - if (currentAppId$) { - coreStart.application.currentAppId$ = currentAppId$; + if (currentContext$) { + coreStart.executionContext.context$ = currentContext$; } coreSetup.getStartServices.mockResolvedValue([coreStart, {}, undefined]); @@ -94,44 +95,98 @@ describe('Cloud Plugin', () => { }); expect(fullStoryApiMock.identify).toHaveBeenCalledWith( - '03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4', + '5ef112cfdae3dea57097bc276e275b2816e73ef2a398dc0ffaf5b6b4e3af2041', { version_str: 'version', version_major_int: -1, version_minor_int: -1, version_patch_int: -1, + org_id_str: 'cloudId', } ); }); - it('calls FS.setUserVars everytime an app changes', async () => { - const currentAppId$ = new Subject(); + it('user hash includes org id', async () => { + await setupPlugin({ + config: { full_story: { enabled: true, org_id: 'foo' }, id: 'esOrg1' }, + currentUserProps: { + username: '1234', + }, + }); + + const hashId1 = fullStoryApiMock.identify.mock.calls[0][0]; + + await setupPlugin({ + config: { full_story: { enabled: true, org_id: 'foo' }, id: 'esOrg2' }, + currentUserProps: { + username: '1234', + }, + }); + + const hashId2 = fullStoryApiMock.identify.mock.calls[1][0]; + + expect(hashId1).not.toEqual(hashId2); + }); + + it('calls FS.setVars everytime an app changes', async () => { + const currentContext$ = new Subject(); const { plugin } = await setupPlugin({ config: { full_story: { enabled: true, org_id: 'foo' } }, currentUserProps: { username: '1234', }, - currentAppId$, + currentContext$, }); - expect(fullStoryApiMock.setUserVars).not.toHaveBeenCalled(); - currentAppId$.next('App1'); - expect(fullStoryApiMock.setUserVars).toHaveBeenCalledWith({ + // takes the app name + expect(fullStoryApiMock.setVars).not.toHaveBeenCalled(); + currentContext$.next({ + name: 'App1', + description: '123', + }); + + expect(fullStoryApiMock.setVars).toHaveBeenCalledWith('page', { + pageName: 'App1', app_id_str: 'App1', }); - currentAppId$.next(); - expect(fullStoryApiMock.setUserVars).toHaveBeenCalledWith({ - app_id_str: 'unknown', + + // context clear + currentContext$.next({}); + expect(fullStoryApiMock.setVars).toHaveBeenCalledWith('page', { + pageName: 'App1', + app_id_str: 'App1', }); - currentAppId$.next('App2'); - expect(fullStoryApiMock.setUserVars).toHaveBeenCalledWith({ + // different app + currentContext$.next({ + name: 'App2', + page: 'page2', + id: '123', + }); + expect(fullStoryApiMock.setVars).toHaveBeenCalledWith('page', { + pageName: 'App2:page2', app_id_str: 'App2', + page_str: 'page2', + ent_id_str: '123', + }); + + // Back to first app + currentContext$.next({ + name: 'App1', + page: 'page3', + id: '123', + }); + + expect(fullStoryApiMock.setVars).toHaveBeenCalledWith('page', { + pageName: 'App1:page3', + app_id_str: 'App1', + page_str: 'page3', + ent_id_str: '123', }); - expect(currentAppId$.observers.length).toBe(1); + expect(currentContext$.observers.length).toBe(1); plugin.stop(); - expect(currentAppId$.observers.length).toBe(0); + expect(currentContext$.observers.length).toBe(0); }); it('does not call FS.identify when security is not available', async () => { diff --git a/x-pack/plugins/cloud/public/plugin.tsx b/x-pack/plugins/cloud/public/plugin.tsx index 991a7c1f8b565c..89f24971de25c1 100644 --- a/x-pack/plugins/cloud/public/plugin.tsx +++ b/x-pack/plugins/cloud/public/plugin.tsx @@ -13,11 +13,12 @@ import { PluginInitializerContext, HttpStart, IBasePath, - ApplicationStart, + ExecutionContextStart, } from 'src/core/public'; import { i18n } from '@kbn/i18n'; import useObservable from 'react-use/lib/useObservable'; import { BehaviorSubject, Subscription } from 'rxjs'; +import { compact, isUndefined, omitBy } from 'lodash'; import type { AuthenticatedUser, SecurityPluginSetup, @@ -83,8 +84,9 @@ export interface CloudSetup { } interface SetupFullstoryDeps extends CloudSetupDependencies { - application?: Promise; + executionContextPromise?: Promise; basePath: IBasePath; + esOrgId?: string; } interface SetupChatDeps extends Pick { @@ -103,11 +105,16 @@ export class CloudPlugin implements Plugin { } public setup(core: CoreSetup, { home, security }: CloudSetupDependencies) { - const application = core.getStartServices().then(([coreStart]) => { - return coreStart.application; + const executionContextPromise = core.getStartServices().then(([coreStart]) => { + return coreStart.executionContext; }); - this.setupFullstory({ basePath: core.http.basePath, security, application }).catch((e) => + this.setupFullstory({ + basePath: core.http.basePath, + security, + executionContextPromise, + esOrgId: this.config.id, + }).catch((e) => // eslint-disable-next-line no-console console.debug(`Error setting up FullStory: ${e.toString()}`) ); @@ -223,9 +230,14 @@ export class CloudPlugin implements Plugin { return user?.roles.includes('superuser') ?? true; } - private async setupFullstory({ basePath, security, application }: SetupFullstoryDeps) { - const { enabled, org_id: orgId } = this.config.full_story; - if (!enabled || !orgId) { + private async setupFullstory({ + basePath, + security, + executionContextPromise, + esOrgId, + }: SetupFullstoryDeps) { + const { enabled, org_id: fsOrgId } = this.config.full_story; + if (!enabled || !fsOrgId) { return; // do not load any fullstory code in the browser if not enabled } @@ -243,7 +255,7 @@ export class CloudPlugin implements Plugin { const { fullStory, sha256 } = initializeFullStory({ basePath, - orgId, + orgId: fsOrgId, packageInfo: this.initializerContext.env.packageInfo, }); @@ -252,16 +264,29 @@ export class CloudPlugin implements Plugin { // This needs to be called syncronously to be sure that we populate the user ID soon enough to make sessions merging // across domains work if (userId) { - // Do the hashing here to keep it at clear as possible in our source code that we do not send literal user IDs - const hashedId = sha256(userId.toString()); - application - ?.then(async () => { - const appStart = await application; - this.appSubscription = appStart.currentAppId$.subscribe((appId) => { - // Update the current application every time it changes - fullStory.setUserVars({ - app_id_str: appId ?? 'unknown', - }); + // Join the cloud org id and the user to create a truly unique user id. + // The hashing here is to keep it at clear as possible in our source code that we do not send literal user IDs + const hashedId = sha256(esOrgId ? `${esOrgId}:${userId}` : `${userId}`); + + executionContextPromise + ?.then(async (executionContext) => { + this.appSubscription = executionContext.context$.subscribe((context) => { + const { name, page, id } = context; + // Update the current context every time it changes + fullStory.setVars( + 'page', + omitBy( + { + // Read about the special pageName property + // https://help.fullstory.com/hc/en-us/articles/1500004101581-FS-setVars-API-Sending-custom-page-data-to-FullStory + pageName: `${compact([name, page]).join(':')}`, + app_id_str: name ?? 'unknown', + page_str: page, + ent_id_str: id, + }, + isUndefined + ) + ); }); }) .catch((e) => { @@ -282,6 +307,7 @@ export class CloudPlugin implements Plugin { version_major_int: parsedVer[0] ?? -1, version_minor_int: parsedVer[1] ?? -1, version_patch_int: parsedVer[2] ?? -1, + org_id_str: esOrgId, }); } } catch (e) { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_custom_settings_flyout/crawl_custom_settings_flyout_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_custom_settings_flyout/crawl_custom_settings_flyout_logic.test.ts index ddfc23b5aa6285..bb20e0e639aa2a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_custom_settings_flyout/crawl_custom_settings_flyout_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_custom_settings_flyout/crawl_custom_settings_flyout_logic.test.ts @@ -297,7 +297,25 @@ describe('CrawlCustomSettingsFlyoutLogic', () => { }); describe('startCustomCrawl', () => { - it('starts a custom crawl with the user set values', async () => { + it('can start a custom crawl for selected domains', async () => { + mount({ + includeSitemapsInRobotsTxt: true, + maxCrawlDepth: 5, + selectedDomainUrls: ['https://www.elastic.co', 'https://swiftype.com'], + }); + jest.spyOn(CrawlerLogic.actions, 'startCrawl'); + + CrawlCustomSettingsFlyoutLogic.actions.startCustomCrawl(); + await nextTick(); + + expect(CrawlerLogic.actions.startCrawl).toHaveBeenCalledWith({ + domain_allowlist: ['https://www.elastic.co', 'https://swiftype.com'], + max_crawl_depth: 5, + sitemap_discovery_disabled: false, + }); + }); + + it('can start a custom crawl selected domains, sitemaps, and seed urls', async () => { mount({ includeSitemapsInRobotsTxt: true, maxCrawlDepth: 5, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_custom_settings_flyout/crawl_custom_settings_flyout_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_custom_settings_flyout/crawl_custom_settings_flyout_logic.ts index f22dcc7487af3d..3b04e1b28c17eb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_custom_settings_flyout/crawl_custom_settings_flyout_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_custom_settings_flyout/crawl_custom_settings_flyout_logic.ts @@ -11,7 +11,7 @@ import { flashAPIErrors } from '../../../../../shared/flash_messages'; import { HttpLogic } from '../../../../../shared/http'; import { EngineLogic } from '../../../engine'; -import { CrawlerLogic } from '../../crawler_logic'; +import { CrawlerLogic, CrawlRequestOverrides } from '../../crawler_logic'; import { DomainConfig, DomainConfigFromServer } from '../../types'; import { domainConfigServerToClient } from '../../utils'; import { extractDomainAndEntryPointFromUrl } from '../add_domain/utils'; @@ -213,13 +213,23 @@ export const CrawlCustomSettingsFlyoutLogic = kea< actions.fetchDomainConfigData(); }, startCustomCrawl: () => { - CrawlerLogic.actions.startCrawl({ - domain_allowlist: values.selectedDomainUrls, - max_crawl_depth: values.maxCrawlDepth, - seed_urls: [...values.selectedEntryPointUrls, ...values.customEntryPointUrls], - sitemap_urls: [...values.selectedSitemapUrls, ...values.customSitemapUrls], + const overrides: CrawlRequestOverrides = { sitemap_discovery_disabled: !values.includeSitemapsInRobotsTxt, - }); + max_crawl_depth: values.maxCrawlDepth, + domain_allowlist: values.selectedDomainUrls, + }; + + const seedUrls = [...values.selectedEntryPointUrls, ...values.customEntryPointUrls]; + if (seedUrls.length > 0) { + overrides.seed_urls = seedUrls; + } + + const sitemapUrls = [...values.selectedSitemapUrls, ...values.customSitemapUrls]; + if (sitemapUrls.length > 0) { + overrides.sitemap_urls = sitemapUrls; + } + + CrawlerLogic.actions.startCrawl(overrides); }, }), }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.ts index 68b1cb6ec9b267..2d1b8a9e7aa27e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.ts @@ -33,7 +33,7 @@ const ACTIVE_STATUSES = [ CrawlerStatus.Canceling, ]; -interface CrawlRequestOverrides { +export interface CrawlRequestOverrides { domain_allowlist?: string[]; max_crawl_depth?: number; seed_urls?: string[]; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/get_role_abilities.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/get_role_abilities.test.ts index 60d0dcc0c5911e..0d5e4e9824f170 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/get_role_abilities.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/get_role_abilities.test.ts @@ -23,6 +23,7 @@ describe('getRoleAbilities', () => { // Has access canViewAccountCredentials: true, canManageEngines: true, + canManageMetaEngines: true, // Does not have access canViewMetaEngines: false, canViewEngineAnalytics: false, @@ -35,7 +36,6 @@ describe('getRoleAbilities', () => { canViewMetaEngineSourceEngines: false, canViewSettings: false, canViewRoleMappings: false, - canManageMetaEngines: false, canManageLogSettings: false, canManageSettings: false, canManageEngineCrawler: false, @@ -81,10 +81,10 @@ describe('getRoleAbilities', () => { expect(myRole.canManageMetaEngines).toEqual(true); }); - it('returns false when the user can manage any engines but the account does not have a platinum license', () => { + it('returns true when the user can manage any engines but the account does not have a platinum license', () => { const myRole = getRoleAbilities({ ...mockRole, ...canManageEngines }, false); - expect(myRole.canManageMetaEngines).toEqual(false); + expect(myRole.canManageMetaEngines).toEqual(true); }); it('returns false when has a platinum license but the user cannot manage any engines', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/get_role_abilities.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/get_role_abilities.ts index ef3e22d851f387..15cba16ab04345 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/get_role_abilities.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/get_role_abilities.ts @@ -49,7 +49,7 @@ export const getRoleAbilities = (role: Account['role'], hasPlatinumLicense = fal canViewSettings: myRole.can('view', 'account_settings'), canViewRoleMappings: myRole.can('view', 'role_mappings'), canManageEngines: myRole.can('manage', 'account_engines'), - canManageMetaEngines: hasPlatinumLicense && myRole.can('manage', 'account_engines'), + canManageMetaEngines: myRole.can('manage', 'account_engines'), canManageLogSettings: myRole.can('manage', 'account_log_settings'), canManageSettings: myRole.can('manage', 'account_settings'), canManageEngineCrawler: myRole.can('manage', 'engine_crawler'), diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.test.tsx index 4598ca337f4e2c..76c6c3cfa9d592 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.test.tsx @@ -27,6 +27,7 @@ import { staticSourceData } from '../../source_data'; import { AddSource } from './add_source'; import { AddSourceSteps } from './add_source_logic'; import { ConfigCompleted } from './config_completed'; +import { ConfigurationChoice } from './configuration_choice'; import { ConfigurationIntro } from './configuration_intro'; import { ConfigureOauth } from './configure_oauth'; import { ConnectInstance } from './connect_instance'; @@ -71,6 +72,22 @@ describe('AddSourceList', () => { expect(setAddSourceStep).toHaveBeenCalledWith(AddSourceSteps.SaveConfigStep); }); + it('renders default state correctly when there are multiple connector options', () => { + const wrapper = shallow( + + ); + wrapper.find(ConfigurationIntro).prop('advanceStep')(); + + expect(setAddSourceStep).toHaveBeenCalledWith(AddSourceSteps.ChoiceStep); + }); + describe('layout', () => { it('renders the default workplace search layout when on an organization view', () => { setMockValues({ ...mockValues, isOrganization: true }); @@ -153,4 +170,19 @@ describe('AddSourceList', () => { expect(wrapper.find(Reauthenticate)).toHaveLength(1); }); + + it('renders Config Choice step', () => { + setMockValues({ + ...mockValues, + addSourceCurrentStep: AddSourceSteps.ChoiceStep, + }); + const wrapper = shallow(); + const advance = wrapper.find(ConfigurationChoice).prop('goToInternalStep'); + expect(advance).toBeDefined(); + if (advance) { + advance(); + } + + expect(setAddSourceStep).toHaveBeenCalledWith(AddSourceSteps.SaveConfigStep); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.tsx index 1e9be74224c5ed..f03c77290f22dd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.tsx @@ -21,9 +21,12 @@ import { import { NAV } from '../../../../constants'; import { SOURCES_PATH, getSourcesPath, getAddPath } from '../../../../routes'; +import { hasMultipleConnectorOptions } from '../../../../utils'; + import { AddSourceHeader } from './add_source_header'; import { AddSourceLogic, AddSourceProps, AddSourceSteps } from './add_source_logic'; import { ConfigCompleted } from './config_completed'; +import { ConfigurationChoice } from './configuration_choice'; import { ConfigurationIntro } from './configuration_intro'; import { ConfigureOauth } from './configure_oauth'; import { ConnectInstance } from './connect_instance'; @@ -51,6 +54,7 @@ export const AddSource: React.FC = (props) => { const goToSaveConfig = () => setAddSourceStep(AddSourceSteps.SaveConfigStep); const setConfigCompletedStep = () => setAddSourceStep(AddSourceSteps.ConfigCompletedStep); const goToConfigCompleted = () => saveSourceConfig(false, setConfigCompletedStep); + const goToChoice = () => setAddSourceStep(AddSourceSteps.ChoiceStep); const FORM_SOURCE_ADDED_SUCCESS_MESSAGE = i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.contentSource.formSourceAddedSuccessMessage', { @@ -75,7 +79,11 @@ export const AddSource: React.FC = (props) => { return ( {addSourceCurrentStep === AddSourceSteps.ConfigIntroStep && ( - + )} {addSourceCurrentStep === AddSourceSteps.SaveConfigStep && ( = (props) => { {addSourceCurrentStep === AddSourceSteps.ReauthenticateStep && ( )} + {addSourceCurrentStep === AddSourceSteps.ChoiceStep && ( + + )} ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.test.ts index 80f8a2fc18218d..a633beac3a1c23 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.test.ts @@ -664,11 +664,13 @@ describe('AddSourceLogic', () => { }); it('handles error', async () => { + const setButtonNotLoadingSpy = jest.spyOn(AddSourceLogic.actions, 'setButtonNotLoading'); http.post.mockReturnValue(Promise.reject('this is an error')); AddSourceLogic.actions.createContentSource(serviceType, successCallback, errorCallback); await nextTick(); + expect(setButtonNotLoadingSpy).toHaveBeenCalled(); expect(errorCallback).toHaveBeenCalled(); expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.ts index db0c5b97372636..92fab713a3fa09 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.ts @@ -41,6 +41,7 @@ export enum AddSourceSteps { ConnectInstanceStep = 'Connect Instance', ConfigureOauthStep = 'Configure Oauth', ReauthenticateStep = 'Reauthenticate', + ChoiceStep = 'Choice', } export interface OauthParams { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_choice.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_choice.test.tsx index bfb916847d865e..392ce175d271db 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_choice.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_choice.test.tsx @@ -13,10 +13,6 @@ import { shallow } from 'enzyme'; import { EuiText, EuiButton } from '@elastic/eui'; -import { - PersonalDashboardLayout, - WorkplaceSearchPageTemplate, -} from '../../../../components/layout'; import { staticSourceData } from '../../source_data'; import { ConfigurationChoice } from './configuration_choice'; @@ -35,22 +31,6 @@ describe('ConfigurationChoice', () => { jest.clearAllMocks(); }); - describe('layout', () => { - it('renders the default workplace search layout when on an organization view', () => { - setMockValues({ ...mockValues, isOrganization: true }); - const wrapper = shallow(); - - expect(wrapper.type()).toEqual(WorkplaceSearchPageTemplate); - }); - - it('renders the personal dashboard layout when not in an organization', () => { - setMockValues({ ...mockValues, isOrganization: false }); - const wrapper = shallow(); - - expect(wrapper.type()).toEqual(PersonalDashboardLayout); - }); - }); - it('renders internal connector if available', () => { const wrapper = shallow(); @@ -64,6 +44,16 @@ describe('ConfigurationChoice', () => { button.simulate('click'); expect(navigateToUrl).toHaveBeenCalledWith('/sources/add/box/internal/'); }); + it('should call prop function when provided on internal connector click', () => { + const advanceSpy = jest.fn(); + const wrapper = shallow( + + ); + const button = wrapper.find(EuiButton); + button.simulate('click'); + expect(navigateToUrl).not.toHaveBeenCalled(); + expect(advanceSpy).toHaveBeenCalled(); + }); it('renders external connector if available', () => { const wrapper = shallow( diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_choice.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_choice.tsx index 46a8998c9dd10a..f5d6d51651dd4d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_choice.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_choice.tsx @@ -9,16 +9,11 @@ import React from 'react'; import { useValues } from 'kea'; -import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer, EuiText } from '@elastic/eui'; +import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiPanel, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { KibanaLogic } from '../../../../../shared/kibana'; import { AppLogic } from '../../../../app_logic'; -import { - WorkplaceSearchPageTemplate, - PersonalDashboardLayout, -} from '../../../../components/layout'; -import { NAV } from '../../../../constants'; import { getAddPath, getSourcesPath } from '../../../../routes'; import { SourceDataItem } from '../../../../types'; @@ -26,6 +21,7 @@ import { AddSourceHeader } from './add_source_header'; interface ConfigurationIntroProps { sourceData: SourceDataItem; + goToInternalStep?: () => void; } export const ConfigurationChoice: React.FC = ({ @@ -36,15 +32,18 @@ export const ConfigurationChoice: React.FC = ({ internalConnectorAvailable, customConnectorAvailable, }, + goToInternalStep, }) => { const { isOrganization } = useValues(AppLogic); - const goToInternal = () => - KibanaLogic.values.navigateToUrl( - `${getSourcesPath( - `${getSourcesPath(getAddPath(serviceType), isOrganization)}/internal`, - isOrganization - )}/` - ); + const goToInternal = goToInternalStep + ? goToInternalStep + : () => + KibanaLogic.values.navigateToUrl( + `${getSourcesPath( + `${getSourcesPath(getAddPath(serviceType), isOrganization)}/internal`, + isOrganization + )}/` + ); const goToExternal = () => KibanaLogic.values.navigateToUrl( `${getSourcesPath( @@ -59,12 +58,10 @@ export const ConfigurationChoice: React.FC = ({ isOrganization )}/` ); - const Layout = isOrganization ? WorkplaceSearchPageTemplate : PersonalDashboardLayout; return ( - + <> - = ({ )} - + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.test.tsx index 0ee80019ea720e..0ae176dbef019f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.test.tsx @@ -36,9 +36,8 @@ describe('ConnectInstance', () => { const getSourceConnectData = jest.fn((_, redirectOauth) => { redirectOauth(); }); - const createContentSource = jest.fn((_, redirectFormCreated, handleFormSubmitError) => { + const createContentSource = jest.fn((_, redirectFormCreated) => { redirectFormCreated(); - handleFormSubmitError(); }); const credentialsSourceData = staticSourceData[13]; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.tsx index a9e24c7b944aba..352addd8176d84 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useState, useEffect, FormEvent } from 'react'; +import React, { useEffect, FormEvent } from 'react'; import { useActions, useValues } from 'kea'; @@ -51,8 +51,6 @@ export const ConnectInstance: React.FC = ({ onFormCreated, header, }) => { - const [formLoading, setFormLoading] = useState(false); - const { hasPlatinumLicense } = useValues(LicensingLogic); const { @@ -64,7 +62,7 @@ export const ConnectInstance: React.FC = ({ setSourceIndexPermissionsValue, } = useActions(AddSourceLogic); - const { loginValue, passwordValue, indexPermissionsValue, subdomainValue } = + const { buttonLoading, loginValue, passwordValue, indexPermissionsValue, subdomainValue } = useValues(AddSourceLogic); const { isOrganization } = useValues(AppLogic); @@ -77,12 +75,9 @@ export const ConnectInstance: React.FC = ({ const redirectOauth = (oauthUrl: string) => window.location.replace(oauthUrl); const redirectFormCreated = () => onFormCreated(name); const onOauthFormSubmit = () => getSourceConnectData(serviceType, redirectOauth); - const handleFormSubmitError = () => setFormLoading(false); - const onCredentialsFormSubmit = () => - createContentSource(serviceType, redirectFormCreated, handleFormSubmitError); + const onCredentialsFormSubmit = () => createContentSource(serviceType, redirectFormCreated); const handleFormSubmit = (e: FormEvent) => { - setFormLoading(true); e.preventDefault(); const onSubmit = hasOauthRedirect ? onOauthFormSubmit : onCredentialsFormSubmit; onSubmit(); @@ -145,7 +140,7 @@ export const ConnectInstance: React.FC = ({ {permissionsExcluded && !hasPlatinumLicense && } - + {i18n.translate('xpack.enterpriseSearch.workplaceSearch.contentSource.connect.button', { defaultMessage: 'Connect {name}', values: { name }, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.tsx index c2cd58a90f209d..e735119f687cc1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.tsx @@ -90,9 +90,10 @@ export const SourcesRouter: React.FC = () => { : externalConnectorAvailable ? 'external' : 'custom'; + const showChoice = defaultOption !== 'internal' && hasMultipleConnectorOptions(sourceData); return ( - {hasMultipleConnectorOptions(sourceData) ? ( + {showChoice ? ( ) : ( diff --git a/x-pack/plugins/maps/common/constants.ts b/x-pack/plugins/maps/common/constants.ts index 6b9f32b92b9c19..1720f0ebdb5582 100644 --- a/x-pack/plugins/maps/common/constants.ts +++ b/x-pack/plugins/maps/common/constants.ts @@ -144,6 +144,7 @@ export enum DRAW_SHAPE { LINE = 'LINE', SIMPLE_SELECT = 'SIMPLE_SELECT', DELETE = 'DELETE', + WAIT = 'WAIT', } export const AGG_DELIMITER = '_of_'; diff --git a/x-pack/plugins/maps/public/actions/map_actions.ts b/x-pack/plugins/maps/public/actions/map_actions.ts index 2abbfc0d076a1e..cccb49f3606226 100644 --- a/x-pack/plugins/maps/public/actions/map_actions.ts +++ b/x-pack/plugins/maps/public/actions/map_actions.ts @@ -14,6 +14,7 @@ import turfBooleanContains from '@turf/boolean-contains'; import { Filter } from '@kbn/es-query'; import { Query, TimeRange } from 'src/plugins/data/public'; import { Geometry, Position } from 'geojson'; +import { asyncForEach } from '@kbn/std'; import { DRAW_MODE, DRAW_SHAPE } from '../../common/constants'; import type { MapExtentState, MapViewContext } from '../reducers/map/types'; import { MapStoreState } from '../reducers/store'; @@ -63,9 +64,10 @@ import { DrawState, MapCenterAndZoom, MapExtent, Timeslice } from '../../common/ import { INITIAL_LOCATION } from '../../common/constants'; import { updateTooltipStateForLayer } from './tooltip_actions'; import { isVectorLayer, IVectorLayer } from '../classes/layers/vector_layer'; -import { SET_DRAW_MODE } from './ui_actions'; +import { SET_DRAW_MODE, pushDeletedFeatureId, clearDeletedFeatureIds } from './ui_actions'; import { expandToTileBoundaries } from '../classes/util/geo_tile_utils'; import { getToasts } from '../kibana_services'; +import { getDeletedFeatureIds } from '../selectors/ui_selectors'; export function setMapInitError(errorMessage: string) { return { @@ -321,6 +323,10 @@ export function updateEditShape(shapeToDraw: DRAW_SHAPE | null) { drawShape: shapeToDraw, }, }); + + if (shapeToDraw !== DRAW_SHAPE.DELETE) { + dispatch(clearDeletedFeatureIds()); + } }; } @@ -353,7 +359,7 @@ export function updateEditLayer(layerId: string | null) { }; } -export function addNewFeatureToIndex(geometry: Geometry | Position[]) { +export function addNewFeatureToIndex(geometries: Array) { return async ( dispatch: ThunkDispatch, getState: () => MapStoreState @@ -369,7 +375,10 @@ export function addNewFeatureToIndex(geometry: Geometry | Position[]) { } try { - await (layer as IVectorLayer).addFeature(geometry); + dispatch(updateEditShape(DRAW_SHAPE.WAIT)); + await asyncForEach(geometries, async (geometry) => { + await (layer as IVectorLayer).addFeature(geometry); + }); await dispatch(syncDataForLayerDueToDrawing(layer)); } catch (e) { getToasts().addError(e, { @@ -378,6 +387,7 @@ export function addNewFeatureToIndex(geometry: Geometry | Position[]) { }), }); } + dispatch(updateEditShape(DRAW_SHAPE.SIMPLE_SELECT)); }; } @@ -386,6 +396,12 @@ export function deleteFeatureFromIndex(featureId: string) { dispatch: ThunkDispatch, getState: () => MapStoreState ) => { + // There is a race condition where users can click on a previously deleted feature before layer has re-rendered after feature delete. + // Check ensures delete requests for previously deleted features are aborted. + if (getDeletedFeatureIds(getState()).includes(featureId)) { + return; + } + const editState = getEditState(getState()); const layerId = editState ? editState.layerId : undefined; if (!layerId) { @@ -395,8 +411,11 @@ export function deleteFeatureFromIndex(featureId: string) { if (!layer || !isVectorLayer(layer)) { return; } + try { + dispatch(updateEditShape(DRAW_SHAPE.WAIT)); await (layer as IVectorLayer).deleteFeature(featureId); + dispatch(pushDeletedFeatureId(featureId)); await dispatch(syncDataForLayerDueToDrawing(layer)); } catch (e) { getToasts().addError(e, { @@ -405,5 +424,6 @@ export function deleteFeatureFromIndex(featureId: string) { }), }); } + dispatch(updateEditShape(DRAW_SHAPE.DELETE)); }; } diff --git a/x-pack/plugins/maps/public/actions/ui_actions.ts b/x-pack/plugins/maps/public/actions/ui_actions.ts index 70e24283ef48f7..1ffcf416f6f8fa 100644 --- a/x-pack/plugins/maps/public/actions/ui_actions.ts +++ b/x-pack/plugins/maps/public/actions/ui_actions.ts @@ -24,6 +24,8 @@ export const SET_OPEN_TOC_DETAILS = 'SET_OPEN_TOC_DETAILS'; export const SHOW_TOC_DETAILS = 'SHOW_TOC_DETAILS'; export const HIDE_TOC_DETAILS = 'HIDE_TOC_DETAILS'; export const SET_DRAW_MODE = 'SET_DRAW_MODE'; +export const PUSH_DELETED_FEATURE_ID = 'PUSH_DELETED_FEATURE_ID'; +export const CLEAR_DELETED_FEATURE_IDS = 'CLEAR_DELETED_FEATURE_IDS'; export function exitFullScreen() { return { @@ -123,3 +125,16 @@ export function closeTimeslider() { dispatch(setQuery({ clearTimeslice: true })); }; } + +export function pushDeletedFeatureId(featureId: string) { + return { + type: PUSH_DELETED_FEATURE_ID, + featureId, + }; +} + +export function clearDeletedFeatureIds() { + return { + type: CLEAR_DELETED_FEATURE_IDS, + }; +} diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_control.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_control.tsx index a404db91a942ef..8cbfcd3a41e80e 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_control.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_control.tsx @@ -20,15 +20,6 @@ import { DRAW_SHAPE } from '../../../../common/constants'; import { DrawCircle, DRAW_CIRCLE_RADIUS_LABEL_STYLE } from './draw_circle'; import { DrawTooltip } from './draw_tooltip'; -const mbModeEquivalencies = new Map([ - ['simple_select', DRAW_SHAPE.SIMPLE_SELECT], - ['draw_rectangle', DRAW_SHAPE.BOUNDS], - ['draw_circle', DRAW_SHAPE.DISTANCE], - ['draw_polygon', DRAW_SHAPE.POLYGON], - ['draw_line_string', DRAW_SHAPE.LINE], - ['draw_point', DRAW_SHAPE.POINT], -]); - const DRAW_RECTANGLE = 'draw_rectangle'; const DRAW_CIRCLE = 'draw_circle'; const mbDrawModes = MapboxDraw.modes; @@ -41,7 +32,6 @@ export interface Props { onClick?: (event: MapMouseEvent, drawControl?: MapboxDraw) => void; mbMap: MbMap; enable: boolean; - updateEditShape: (shapeToDraw: DRAW_SHAPE) => void; } export class DrawControl extends Component { @@ -91,12 +81,6 @@ export class DrawControl extends Component { } }, 0); - _onModeChange = ({ mode }: { mode: string }) => { - if (mbModeEquivalencies.has(mode)) { - this.props.updateEditShape(mbModeEquivalencies.get(mode)!); - } - }; - _removeDrawControl() { // Do not remove draw control after mbMap.remove is called, causes execeptions and mbMap.remove cleans up all map resources. const isMapRemoved = !this.props.mbMap.loaded(); @@ -105,7 +89,6 @@ export class DrawControl extends Component { } this.props.mbMap.getCanvas().style.cursor = ''; - this.props.mbMap.off('draw.modechange', this._onModeChange); this.props.mbMap.off('draw.create', this._onDraw); if (this.props.onClick) { this.props.mbMap.off('click', this._onClick); @@ -118,7 +101,6 @@ export class DrawControl extends Component { if (!this._mbDrawControlAdded) { this.props.mbMap.addControl(this._mbDrawControl); this._mbDrawControlAdded = true; - this.props.mbMap.on('draw.modechange', this._onModeChange); this.props.mbMap.on('draw.create', this._onDraw); if (this.props.onClick) { @@ -144,6 +126,9 @@ export class DrawControl extends Component { this._mbDrawControl.changeMode(DRAW_POINT); } else if (this.props.drawShape === DRAW_SHAPE.DELETE) { this._mbDrawControl.changeMode(SIMPLE_SELECT); + } else if (this.props.drawShape === DRAW_SHAPE.WAIT) { + this.props.mbMap.getCanvas().style.cursor = 'wait'; + this._mbDrawControl.changeMode(SIMPLE_SELECT); } else { this._mbDrawControl.changeMode(SIMPLE_SELECT); } diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_feature_control/draw_feature_control.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_feature_control/draw_feature_control.tsx index 6c7fe9f0ad213a..b6ffacc491030e 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_feature_control/draw_feature_control.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_feature_control/draw_feature_control.tsx @@ -15,7 +15,7 @@ import { i18n } from '@kbn/i18n'; import * as jsts from 'jsts'; import { MapMouseEvent } from '@kbn/mapbox-gl'; import { getToasts } from '../../../../kibana_services'; -import { DrawControl } from '../'; +import { DrawControl } from '../draw_control'; import { DRAW_MODE, DRAW_SHAPE } from '../../../../../common/constants'; import { ILayer } from '../../../../classes/layers/layer'; import { EXCLUDE_CENTROID_FEATURES } from '../../../../classes/util/mb_filter_expressions'; @@ -29,9 +29,8 @@ export interface ReduxStateProps { } export interface ReduxDispatchProps { - addNewFeatureToIndex: (geometry: Geometry | Position[]) => void; + addNewFeatureToIndex: (geometries: Array) => void; deleteFeatureFromIndex: (featureId: string) => void; - disableDrawState: () => void; } export interface OwnProps { @@ -43,6 +42,7 @@ type Props = ReduxStateProps & ReduxDispatchProps & OwnProps; export class DrawFeatureControl extends Component { _onDraw = async (e: { features: Feature[] }, mbDrawControl: MapboxDraw) => { try { + const geometries: Array = []; e.features.forEach((feature: Feature) => { const { geometry } = geoJSONReader.read(feature); if (!geometry.isSimple() || !geometry.isValid()) { @@ -58,9 +58,13 @@ export class DrawFeatureControl extends Component { this.props.drawMode === DRAW_MODE.DRAW_POINTS ? feature.geometry.coordinates : feature.geometry; - this.props.addNewFeatureToIndex(featureGeom); + geometries.push(featureGeom); } }); + + if (geometries.length) { + this.props.addNewFeatureToIndex(geometries); + } } catch (error) { getToasts().addWarning( i18n.translate('xpack.maps.drawFeatureControl.unableToCreateFeature', { @@ -71,7 +75,6 @@ export class DrawFeatureControl extends Component { }) ); } finally { - this.props.disableDrawState(); try { mbDrawControl.deleteAll(); } catch (_e) { @@ -86,6 +89,7 @@ export class DrawFeatureControl extends Component { if (!this.props.editLayer || this.props.drawShape !== DRAW_SHAPE.DELETE) { return; } + const mbEditLayerIds = this.props.editLayer .getMbLayerIds() .filter((mbLayerId) => !!this.props.mbMap.getLayer(mbLayerId)); diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_feature_control/index.ts b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_feature_control/index.ts index e1d703173fc2da..d2c369b4bd50a2 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_feature_control/index.ts +++ b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_feature_control/index.ts @@ -15,7 +15,7 @@ import { ReduxStateProps, OwnProps, } from './draw_feature_control'; -import { addNewFeatureToIndex, deleteFeatureFromIndex, updateEditShape } from '../../../../actions'; +import { addNewFeatureToIndex, deleteFeatureFromIndex } from '../../../../actions'; import { MapStoreState } from '../../../../reducers/store'; import { getEditState, getLayerById } from '../../../../selectors/map_selectors'; import { getDrawMode } from '../../../../selectors/ui_selectors'; @@ -34,15 +34,12 @@ function mapDispatchToProps( dispatch: ThunkDispatch ): ReduxDispatchProps { return { - addNewFeatureToIndex(geometry: Geometry | Position[]) { - dispatch(addNewFeatureToIndex(geometry)); + addNewFeatureToIndex(geometries: Array) { + dispatch(addNewFeatureToIndex(geometries)); }, deleteFeatureFromIndex(featureId: string) { dispatch(deleteFeatureFromIndex(featureId)); }, - disableDrawState() { - dispatch(updateEditShape(null)); - }, }; } diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_filter_control/draw_filter_control.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_filter_control/draw_filter_control.tsx index 2f652506857d2a..98d88d43fc65f9 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_filter_control/draw_filter_control.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_filter_control/draw_filter_control.tsx @@ -20,7 +20,7 @@ import { roundCoordinates, } from '../../../../../common/elasticsearch_util'; import { getToasts } from '../../../../kibana_services'; -import { DrawControl } from '../'; +import { DrawControl } from '../draw_control'; import { DrawCircleProperties } from '../draw_circle'; export interface Props { diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/index.ts b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/index.ts deleted file mode 100644 index b0f1941caec08c..00000000000000 --- a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * 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 { ThunkDispatch } from 'redux-thunk'; -import { AnyAction } from 'redux'; -import { connect } from 'react-redux'; -import { updateEditShape } from '../../../actions'; -import { MapStoreState } from '../../../reducers/store'; -import { DrawControl } from './draw_control'; -import { DRAW_SHAPE } from '../../../../common/constants'; - -function mapDispatchToProps(dispatch: ThunkDispatch) { - return { - updateEditShape(shapeToDraw: DRAW_SHAPE) { - dispatch(updateEditShape(shapeToDraw)); - }, - }; -} - -const connected = connect(null, mapDispatchToProps)(DrawControl); -export { connected as DrawControl }; diff --git a/x-pack/plugins/maps/public/reducers/ui.ts b/x-pack/plugins/maps/public/reducers/ui.ts index f3f948bb96508a..f0f22c5a8c4a93 100644 --- a/x-pack/plugins/maps/public/reducers/ui.ts +++ b/x-pack/plugins/maps/public/reducers/ui.ts @@ -19,6 +19,8 @@ import { SHOW_TOC_DETAILS, HIDE_TOC_DETAILS, SET_DRAW_MODE, + PUSH_DELETED_FEATURE_ID, + CLEAR_DELETED_FEATURE_IDS, } from '../actions'; import { DRAW_MODE } from '../../common/constants'; @@ -37,6 +39,7 @@ export type MapUiState = { isLayerTOCOpen: boolean; isTimesliderOpen: boolean; openTOCDetails: string[]; + deletedFeatureIds: string[]; }; export const DEFAULT_IS_LAYER_TOC_OPEN = true; @@ -51,6 +54,7 @@ export const DEFAULT_MAP_UI_STATE = { // storing TOC detail visibility outside of map.layerList because its UI state and not map rendering state. // This also makes for easy read/write access for embeddables. openTOCDetails: [], + deletedFeatureIds: [], }; // Reducer @@ -82,6 +86,16 @@ export function ui(state: MapUiState = DEFAULT_MAP_UI_STATE, action: any) { return layerId !== action.layerId; }), }; + case PUSH_DELETED_FEATURE_ID: + return { + ...state, + deletedFeatureIds: [...state.deletedFeatureIds, action.featureId], + }; + case CLEAR_DELETED_FEATURE_IDS: + return { + ...state, + deletedFeatureIds: [], + }; default: return state; } diff --git a/x-pack/plugins/maps/public/selectors/ui_selectors.ts b/x-pack/plugins/maps/public/selectors/ui_selectors.ts index 942a5190691a15..6bdf5a35679a73 100644 --- a/x-pack/plugins/maps/public/selectors/ui_selectors.ts +++ b/x-pack/plugins/maps/public/selectors/ui_selectors.ts @@ -17,3 +17,4 @@ export const getIsTimesliderOpen = ({ ui }: MapStoreState): boolean => ui.isTime export const getOpenTOCDetails = ({ ui }: MapStoreState): string[] => ui.openTOCDetails; export const getIsFullScreen = ({ ui }: MapStoreState): boolean => ui.isFullScreen; export const getIsReadOnly = ({ ui }: MapStoreState): boolean => ui.isReadOnly; +export const getDeletedFeatureIds = ({ ui }: MapStoreState): string[] => ui.deletedFeatureIds; diff --git a/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts index 3df5016f560c07..4a621bc5f608b5 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts +++ b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts @@ -124,6 +124,17 @@ export class DataRecognizer { private _resultsService: ReturnType; private _calculateModelMemoryLimit: ReturnType; + /** + * A temporary cache of configs loaded from disk and from save object service. + * The configs from disk will not change while kibana is running. + * The configs from saved objects could potentially change while an instance of + * DataRecognizer exists, if a fleet package containing modules is installed. + * However the chance of this happening is very low and so the benefit of using + * this cache outweighs the risk of the cache being out of date during the short + * existence of a DataRecognizer instance. + */ + private _configCache: Config[] | null = null; + /** * List of the module jobs that require model memory estimation */ @@ -181,6 +192,10 @@ export class DataRecognizer { } private async _loadConfigs(): Promise { + if (this._configCache !== null) { + return this._configCache; + } + const configs: Config[] = []; const dirs = await this._listDirs(this._modulesDir); await Promise.all( @@ -211,7 +226,9 @@ export class DataRecognizer { isSavedObject: true, })); - return [...configs, ...savedObjectConfigs]; + this._configCache = [...configs, ...savedObjectConfigs]; + + return this._configCache; } private async _loadSavedObjectModules() { diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_settings_collector.ts b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_settings_collector.ts index 7096647854c157..76cc9adeb43ecd 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_settings_collector.ts +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_settings_collector.ts @@ -90,7 +90,6 @@ export function getSettingsCollector( ) { return usageCollection.makeStatsCollector< EmailSettingData | undefined, - false, KibanaSettingsCollectorExtraOptions >({ type: 'kibana_settings', diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_usage_collector.ts b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_usage_collector.ts index cbbfe64f5e3e22..0c952949c56b49 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_usage_collector.ts +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_usage_collector.ts @@ -18,7 +18,7 @@ export function getMonitoringUsageCollector( config: MonitoringConfig, getClient: () => IClusterClient ) { - return usageCollection.makeUsageCollector({ + return usageCollection.makeUsageCollector({ type: 'monitoring', isReady: () => true, schema: { @@ -95,13 +95,8 @@ export function getMonitoringUsageCollector( }, }, }, - extendFetchContext: { - kibanaRequest: true, - }, - fetch: async ({ kibanaRequest }) => { - const callCluster = kibanaRequest - ? getClient().asScoped(kibanaRequest).asCurrentUser - : getClient().asInternalUser; + fetch: async () => { + const callCluster = getClient().asInternalUser; const usageClusters: MonitoringClusterStackProductUsage[] = []; const availableCcs = config.ui.ccs.enabled; const clusters = await fetchClusters(callCluster); diff --git a/x-pack/plugins/monitoring/server/telemetry_collection/register_monitoring_telemetry_collection.ts b/x-pack/plugins/monitoring/server/telemetry_collection/register_monitoring_telemetry_collection.ts index bce6f57d6f950a..344b04fb4780d4 100644 --- a/x-pack/plugins/monitoring/server/telemetry_collection/register_monitoring_telemetry_collection.ts +++ b/x-pack/plugins/monitoring/server/telemetry_collection/register_monitoring_telemetry_collection.ts @@ -34,13 +34,9 @@ export function registerMonitoringTelemetryCollection( getClient: () => IClusterClient, maxBucketSize: number ) { - const monitoringStatsCollector = usageCollection.makeStatsCollector< - MonitoringTelemetryUsage, - true - >({ + const monitoringStatsCollector = usageCollection.makeStatsCollector({ type: 'monitoringTelemetry', isReady: () => true, - extendFetchContext: { kibanaRequest: true }, schema: { stats: { type: 'array', @@ -137,13 +133,13 @@ export function registerMonitoringTelemetryCollection( }, }, }, - fetch: async ({ kibanaRequest, esClient }) => { + fetch: async () => { const timestamp = Date.now(); // Collect the telemetry from the monitoring indices for this moment. // NOTE: Usually, the monitoring indices index stats for each product every 10s (by default). // However, some data may be delayed up-to 24h because monitoring only collects extended Kibana stats in that interval // to avoid overloading of the system when retrieving data from the collectors (that delay is dealt with in the Kibana Stats getter inside the `getAllStats` method). // By 8.x, we expect to stop collecting the Kibana extended stats and keep only the monitoring-related metrics. - const callCluster = kibanaRequest ? esClient : getClient().asInternalUser; + const callCluster = getClient().asInternalUser; const clusterDetails = await getClusterUuids(callCluster, timestamp, maxBucketSize); const [licenses, stats] = await Promise.all([ getLicenses(clusterDetails, callCluster, maxBucketSize), diff --git a/x-pack/plugins/reporting/server/lib/tasks/monitor_reports.ts b/x-pack/plugins/reporting/server/lib/tasks/monitor_reports.ts index 1d406d7a5cc623..56e12d7d5512bd 100644 --- a/x-pack/plugins/reporting/server/lib/tasks/monitor_reports.ts +++ b/x-pack/plugins/reporting/server/lib/tasks/monitor_reports.ts @@ -92,31 +92,42 @@ export class MonitorReportsTask implements ReportingTask { return; } - const { - _id: jobId, - _source: { process_expiration: processExpiration, status }, - } = recoveredJob; + const report = new SavedReport({ ...recoveredJob, ...recoveredJob._source }); + const { _id: jobId, process_expiration: processExpiration, status } = report; + const eventLog = this.reporting.getEventLogger(report); if (![statuses.JOB_STATUS_PENDING, statuses.JOB_STATUS_PROCESSING].includes(status)) { - throw new Error(`Invalid job status in the monitoring search result: ${status}`); // only pending or processing jobs possibility need rescheduling + const invalidStatusError = new Error( + `Invalid job status in the monitoring search result: ${status}` + ); // only pending or processing jobs possibility need rescheduling + this.logger.error(invalidStatusError); + eventLog.logError(invalidStatusError); + + // fatal: can not reschedule the job + throw invalidStatusError; } if (status === statuses.JOB_STATUS_PENDING) { - this.logger.info( + const migratingJobError = new Error( `${jobId} was scheduled in a previous version and left in [${status}] status. Rescheduling...` ); + this.logger.error(migratingJobError); + eventLog.logError(migratingJobError); } if (status === statuses.JOB_STATUS_PROCESSING) { const expirationTime = moment(processExpiration); const overdueValue = moment().valueOf() - expirationTime.valueOf(); - this.logger.info( + const overdueExpirationError = new Error( `${jobId} status is [${status}] and the expiration time was [${overdueValue}ms] ago. Rescheduling...` ); + this.logger.error(overdueExpirationError); + eventLog.logError(overdueExpirationError); } + eventLog.logRetry(); + // clear process expiration and set status to pending - const report = new SavedReport({ ...recoveredJob, ...recoveredJob._source }); await reportingStore.prepareReportForRetry(report); // if there is a version conflict response, this just throws and logs an error // clear process expiration and reschedule @@ -154,8 +165,6 @@ export class MonitorReportsTask implements ReportingTask { const newTask = await this.reporting.scheduleTask(task); - this.reporting.getEventLogger({ _id: task.id, ...task }, newTask).logRetry(); - return newTask; } diff --git a/x-pack/plugins/security_solution/cypress/integration/hosts/host_risk_tab.spec.ts b/x-pack/plugins/security_solution/cypress/integration/hosts/host_risk_tab.spec.ts index 38a639e19c6b87..3af77036649aa1 100644 --- a/x-pack/plugins/security_solution/cypress/integration/hosts/host_risk_tab.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/hosts/host_risk_tab.spec.ts @@ -7,14 +7,20 @@ import { cleanKibana } from '../../tasks/common'; import { esArchiverLoad, esArchiverUnload } from '../../tasks/es_archiver'; -import { navigateToHostRiskDetailTab } from '../../tasks/host_risk'; +import { + navigateToHostRiskDetailTab, + openRiskTableFilterAndSelectTheCriticalOption, + removeCritialFilter, + selectFiveItemsPerPageOption, +} from '../../tasks/host_risk'; import { HOST_BY_RISK_TABLE_CELL, - HOST_BY_RISK_TABLE_FILTER, - HOST_BY_RISK_TABLE_FILTER_CRITICAL, + HOST_BY_RISK_TABLE_HOSTNAME_CELL, + HOST_BY_RISK_TABLE_NEXT_PAGE_BUTTON, } from '../../screens/hosts/host_risk'; import { loginAndWaitForPage } from '../../tasks/login'; import { HOSTS_URL } from '../../urls/navigation'; +import { clearSearchBar, kqlSearch } from '../../tasks/security_header'; describe('risk tab', () => { before(() => { @@ -29,15 +35,30 @@ describe('risk tab', () => { }); it('renders the table', () => { + kqlSearch('host.name: "siem-kibana" {enter}'); cy.get(HOST_BY_RISK_TABLE_CELL).eq(3).should('have.text', 'siem-kibana'); cy.get(HOST_BY_RISK_TABLE_CELL).eq(4).should('have.text', '21.00'); cy.get(HOST_BY_RISK_TABLE_CELL).eq(5).should('have.text', 'Low'); + clearSearchBar(); }); it('filters the table', () => { - cy.get(HOST_BY_RISK_TABLE_FILTER).click(); - cy.get(HOST_BY_RISK_TABLE_FILTER_CRITICAL).click(); + openRiskTableFilterAndSelectTheCriticalOption(); cy.get(HOST_BY_RISK_TABLE_CELL).eq(3).should('not.have.text', 'siem-kibana'); + + removeCritialFilter(); + }); + + it('should be able to change items count per page', () => { + selectFiveItemsPerPageOption(); + + cy.get(HOST_BY_RISK_TABLE_HOSTNAME_CELL).should('have.length', 5); + }); + + it('should not allow page change when page is empty', () => { + kqlSearch('host.name: "nonexistent_host" {enter}'); + cy.get(HOST_BY_RISK_TABLE_NEXT_PAGE_BUTTON).should(`not.exist`); + clearSearchBar(); }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/overview/risky_hosts_panel.spec.ts b/x-pack/plugins/security_solution/cypress/integration/overview/risky_hosts_panel.spec.ts index 1c55a38b324953..652b3c1118b30e 100644 --- a/x-pack/plugins/security_solution/cypress/integration/overview/risky_hosts_panel.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/overview/risky_hosts_panel.spec.ts @@ -69,7 +69,7 @@ describe('Risky Hosts Link Panel', () => { `${OVERVIEW_RISKY_HOSTS_LINKS} ${OVERVIEW_RISKY_HOSTS_LINKS_WARNING_INNER_PANEL}` ).should('not.exist'); cy.get(`${OVERVIEW_RISKY_HOSTS_VIEW_DASHBOARD_BUTTON}`).should('be.disabled'); - cy.get(`${OVERVIEW_RISKY_HOSTS_TOTAL_EVENT_COUNT}`).should('have.text', 'Showing: 1 host'); + cy.get(`${OVERVIEW_RISKY_HOSTS_TOTAL_EVENT_COUNT}`).should('have.text', 'Showing: 6 hosts'); changeSpace(testSpaceName); cy.visit(`/s/${testSpaceName}${OVERVIEW_URL}`); diff --git a/x-pack/plugins/security_solution/cypress/screens/hosts/host_risk.ts b/x-pack/plugins/security_solution/cypress/screens/hosts/host_risk.ts index 58331518255df7..3209200cf25a18 100644 --- a/x-pack/plugins/security_solution/cypress/screens/hosts/host_risk.ts +++ b/x-pack/plugins/security_solution/cypress/screens/hosts/host_risk.ts @@ -24,3 +24,14 @@ export const HOST_BY_RISK_TABLE_FILTER = '[data-test-subj="host-risk-filter-butt export const HOST_BY_RISK_TABLE_FILTER_CRITICAL = '[data-test-subj="host-risk-filter-item-Critical"]'; + +export const HOST_BY_RISK_TABLE_PERPAGE_BUTTON = + '[data-test-subj="loadingMoreSizeRowPopover"] button'; + +export const HOST_BY_RISK_TABLE_PERPAGE_OPTIONS = + '[data-test-subj="loadingMorePickSizeRow"] button'; + +export const HOST_BY_RISK_TABLE_NEXT_PAGE_BUTTON = + '[data-test-subj="numberedPagination"] [data-test-subj="pagination-button-next"]'; + +export const HOST_BY_RISK_TABLE_HOSTNAME_CELL = '[data-test-subj="render-content-host.name"]'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/host_risk.ts b/x-pack/plugins/security_solution/cypress/tasks/host_risk.ts index 7a357e8a5c7fb2..afa04bb6de0ca9 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/host_risk.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/host_risk.ts @@ -5,7 +5,15 @@ * 2.0. */ -import { LOADING_TABLE, RISK_DETAILS_NAV, RISK_FLYOUT_TRIGGER } from '../screens/hosts/host_risk'; +import { + HOST_BY_RISK_TABLE_FILTER, + HOST_BY_RISK_TABLE_FILTER_CRITICAL, + HOST_BY_RISK_TABLE_PERPAGE_BUTTON, + HOST_BY_RISK_TABLE_PERPAGE_OPTIONS, + LOADING_TABLE, + RISK_DETAILS_NAV, + RISK_FLYOUT_TRIGGER, +} from '../screens/hosts/host_risk'; export const navigateToHostRiskDetailTab = () => cy.get(RISK_DETAILS_NAV).click(); @@ -15,3 +23,15 @@ export const waitForTableToLoad = () => { cy.get(LOADING_TABLE).should('exist'); cy.get(LOADING_TABLE).should('not.exist'); }; + +export const openRiskTableFilterAndSelectTheCriticalOption = () => { + cy.get(HOST_BY_RISK_TABLE_FILTER).click(); + cy.get(HOST_BY_RISK_TABLE_FILTER_CRITICAL).click(); +}; +export const removeCritialFilter = () => { + cy.get(HOST_BY_RISK_TABLE_FILTER_CRITICAL).click(); +}; +export const selectFiveItemsPerPageOption = () => { + cy.get(HOST_BY_RISK_TABLE_PERPAGE_BUTTON).click(); + cy.get(HOST_BY_RISK_TABLE_PERPAGE_OPTIONS).first().click(); +}; diff --git a/x-pack/plugins/security_solution/public/common/components/paginated_table/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/paginated_table/index.test.tsx index 64c3584bc668cd..0c09dce9c07cb3 100644 --- a/x-pack/plugins/security_solution/public/common/components/paginated_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/paginated_table/index.test.tsx @@ -327,6 +327,33 @@ describe('Paginated Table Component', () => { ); expect(wrapper.find('[data-test-subj="loadingMoreSizeRowPopover"]').exists()).toBeFalsy(); }); + + test('Should hide pagination if totalCount is zero', () => { + const wrapper = mount( + + {'My test supplement.'}

} + headerTitle="Hosts" + headerTooltip="My test tooltip" + headerUnit="Test Unit" + itemsPerRow={rowItems} + limit={DEFAULT_MAX_TABLE_QUERY_SIZE} + loading={false} + loadPage={loadPage} + pageOfItems={mockData.Hosts.edges} + showMorePagesIndicator={true} + totalCount={0} + updateActivePage={updateActivePage} + updateLimitPagination={(limit) => updateLimitPagination({ limit })} + /> +
+ ); + + expect(wrapper.find('[data-test-subj="numberedPagination"]').exists()).toBeFalsy(); + }); }); describe('Events', () => { diff --git a/x-pack/plugins/security_solution/public/common/components/paginated_table/index.tsx b/x-pack/plugins/security_solution/public/common/components/paginated_table/index.tsx index 6100c03d38bfa1..310ab039057c2f 100644 --- a/x-pack/plugins/security_solution/public/common/components/paginated_table/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/paginated_table/index.tsx @@ -303,12 +303,14 @@ const PaginatedTableComponent: FC = ({ - + {totalCount > 0 && ( + + )} {(isInspect || myLoading) && ( diff --git a/x-pack/plugins/security_solution/public/hosts/containers/host_risk_score/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/host_risk_score/index.tsx index 7f2c41f1414cfc..516895d49b8667 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/host_risk_score/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/host_risk_score/index.tsx @@ -180,7 +180,7 @@ export const useHostRiskScore = ({ factoryQueryType: HostsQueries.hostsRiskScore, filterQuery: createFilter(filterQuery), pagination: - cursorStart && querySize + cursorStart !== undefined && querySize !== undefined ? { cursorStart, querySize, diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/__mocks__/index.ts b/x-pack/plugins/security_solution/server/lib/telemetry/__mocks__/index.ts index 4d9b67af6c311c..e18d104b0d73ac 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/__mocks__/index.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/__mocks__/index.ts @@ -44,6 +44,7 @@ export const createMockTelemetryReceiver = ( fetchTrustedApplications: jest.fn(), fetchEndpointList: jest.fn(), fetchDetectionRules: jest.fn().mockReturnValue({ body: null }), + fetchEndpointMetadata: jest.fn(), } as unknown as jest.Mocked; }; diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/receiver.ts b/x-pack/plugins/security_solution/server/lib/telemetry/receiver.ts index 6e24cea41b7187..91054577656b13 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/receiver.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/receiver.ts @@ -80,6 +80,13 @@ export interface ITelemetryReceiver { TransportResult>, unknown> >; + fetchEndpointMetadata( + executeFrom: string, + executeTo: string + ): Promise< + TransportResult>, unknown> + >; + fetchDiagnosticAlerts( executeFrom: string, executeTo: string @@ -270,6 +277,53 @@ export class TelemetryReceiver implements ITelemetryReceiver { return this.esClient.search(query, { meta: true }); } + public async fetchEndpointMetadata(executeFrom: string, executeTo: string) { + if (this.esClient === undefined || this.esClient === null) { + throw Error('elasticsearch client is unavailable: cannot retrieve elastic endpoint metrics'); + } + + const query: SearchRequest = { + expand_wildcards: ['open' as const, 'hidden' as const], + index: `.ds-metrics-endpoint.metadata-*`, + ignore_unavailable: false, + size: 0, // no query results required - only aggregation quantity + body: { + query: { + range: { + '@timestamp': { + gte: executeFrom, + lt: executeTo, + }, + }, + }, + aggs: { + endpoint_metadata: { + terms: { + field: 'agent.id', + size: this.max_records, + }, + aggs: { + latest_metadata: { + top_hits: { + size: 1, + sort: [ + { + '@timestamp': { + order: 'desc' as const, + }, + }, + ], + }, + }, + }, + }, + }, + }, + }; + + return this.esClient.search(query, { meta: true }); + } + public async fetchDiagnosticAlerts(executeFrom: string, executeTo: string) { if (this.esClient === undefined || this.esClient === null) { throw Error('elasticsearch client is unavailable: cannot retrieve diagnostic alerts'); diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/tasks/endpoint.ts b/x-pack/plugins/security_solution/server/lib/telemetry/tasks/endpoint.ts index e9cc36bbff907f..c2c318debccdaa 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/tasks/endpoint.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/tasks/endpoint.ts @@ -11,6 +11,8 @@ import type { EndpointMetricsAggregation, EndpointPolicyResponseAggregation, EndpointPolicyResponseDocument, + EndpointMetadataAggregation, + EndpointMetadataDocument, ESClusterInfo, ESLicense, } from '../types'; @@ -188,7 +190,36 @@ export function createTelemetryEndpointTaskConfig(maxTelemetryBatch: number) { ) : new Map(); - /** STAGE 4 - Create the telemetry log records + /** STAGE 4 - Fetch Endpoint Agent Metadata + * + * Reads Endpoint Agent metadata out of the `.ds-metrics-endpoint.metadata` data stream + * and buckets them by Endpoint Agent id and sorts by the top hit. The EP agent will + * report its metadata once per day OR every time a policy change has occured. If + * a metadata document(s) exists for an EP agent we map to fleet agent and policy + */ + if (endpointData.endpointMetadata === undefined) { + logger.debug(`no endpoint metadata to report`); + } + + const { body: endpointMetadataResponse } = endpointData.endpointMetadata as unknown as { + body: EndpointMetadataAggregation; + }; + + if (endpointMetadataResponse.aggregations === undefined) { + logger.debug(`no endpoint metadata to report`); + } + + const endpointMetadata = + endpointMetadataResponse.aggregations.endpoint_metadata.buckets.reduce( + (cache, endpointAgentId) => { + const doc = endpointAgentId.latest_metadata.hits.hits[0]; + cache.set(endpointAgentId.key, doc); + return cache; + }, + new Map() + ); + + /** STAGE 5 - Create the telemetry log records * * Iterates through the endpoint metrics documents at STAGE 1 and joins them together * to form the telemetry log that is sent back to Elastic Security developers to @@ -199,6 +230,7 @@ export function createTelemetryEndpointTaskConfig(maxTelemetryBatch: number) { const telemetryPayloads = endpointMetrics.map((endpoint) => { let policyConfig = null; let failedPolicy = null; + let endpointMetadataById = null; const fleetAgentId = endpoint.endpoint_metrics.elastic.agent.id; const endpointAgentId = endpoint.endpoint_agent; @@ -212,6 +244,10 @@ export function createTelemetryEndpointTaskConfig(maxTelemetryBatch: number) { } } + if (endpointMetadata) { + endpointMetadataById = endpointMetadata.get(endpointAgentId); + } + const { cpu, memory, @@ -242,6 +278,10 @@ export function createTelemetryEndpointTaskConfig(maxTelemetryBatch: number) { }, endpoint_meta: { os: endpoint.endpoint_metrics.host.os, + capabilities: + endpointMetadataById !== null && endpointMetadataById !== undefined + ? endpointMetadataById._source.Endpoint.capabilities + : [], }, policy_config: endpointPolicyDetail !== null ? endpointPolicyDetail : {}, policy_response: @@ -265,7 +305,7 @@ export function createTelemetryEndpointTaskConfig(maxTelemetryBatch: number) { }); /** - * STAGE 5 - Send the documents + * STAGE 6 - Send the documents * * Send the documents in a batches of maxTelemetryBatch */ @@ -287,11 +327,13 @@ async function fetchEndpointData( executeFrom: string, executeTo: string ) { - const [fleetAgentsResponse, epMetricsResponse, policyResponse] = await Promise.allSettled([ - receiver.fetchFleetAgents(), - receiver.fetchEndpointMetrics(executeFrom, executeTo), - receiver.fetchEndpointPolicyResponses(executeFrom, executeTo), - ]); + const [fleetAgentsResponse, epMetricsResponse, policyResponse, endpointMetadata] = + await Promise.allSettled([ + receiver.fetchFleetAgents(), + receiver.fetchEndpointMetrics(executeFrom, executeTo), + receiver.fetchEndpointPolicyResponses(executeFrom, executeTo), + receiver.fetchEndpointMetadata(executeFrom, executeTo), + ]); return { fleetAgentsResponse: @@ -300,5 +342,6 @@ async function fetchEndpointData( : EmptyFleetAgentResponse, endpointMetrics: epMetricsResponse.status === 'fulfilled' ? epMetricsResponse.value : undefined, epPolicyResponse: policyResponse.status === 'fulfilled' ? policyResponse.value : undefined, + endpointMetadata: endpointMetadata.status === 'fulfilled' ? endpointMetadata.value : undefined, }; } diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/types.ts b/x-pack/plugins/security_solution/server/lib/telemetry/types.ts index 35b531ae6941c3..c1c65a428f62d9 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/types.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/types.ts @@ -243,6 +243,44 @@ interface EndpointMetricOS { full: string; } +// EP Metadata + +export interface EndpointMetadataAggregation { + hits: { + total: { value: number }; + }; + aggregations: { + endpoint_metadata: { + buckets: Array<{ key: string; doc_count: number; latest_metadata: EndpointMetadataHits }>; + }; + }; +} + +interface EndpointMetadataHits { + hits: { + total: { value: number }; + hits: EndpointMetadataDocument[]; + }; +} + +export interface EndpointMetadataDocument { + _source: { + '@timestamp': string; + agent: { + id: string; + version: string; + }; + Endpoint: { + capabilities: string[]; + }; + elastic: { + agent: { + id: string; + }; + }; + }; +} + // List HTTP Types export const GetTrustedAppsRequestSchema = { diff --git a/x-pack/plugins/task_manager/server/usage/task_manager_usage_collector.test.ts b/x-pack/plugins/task_manager/server/usage/task_manager_usage_collector.test.ts index 9b66792efcd9e3..b7a52a7a41bcf6 100644 --- a/x-pack/plugins/task_manager/server/usage/task_manager_usage_collector.test.ts +++ b/x-pack/plugins/task_manager/server/usage/task_manager_usage_collector.test.ts @@ -9,7 +9,7 @@ import { merge } from 'lodash'; import { loggingSystemMock } from 'src/core/server/mocks'; import { Collector, - createCollectorFetchContextWithKibanaMock, + createCollectorFetchContextMock, createUsageCollectionSetupMock, } from 'src/plugins/usage_collection/server/mocks'; import { HealthStatus } from '../monitoring'; @@ -26,7 +26,7 @@ describe('registerTaskManagerUsageCollector', () => { it('should report telemetry on the ephemeral queue', async () => { const monitoringStats$ = new Subject(); const usageCollectionMock = createUsageCollectionSetupMock(); - const fetchContext = createCollectorFetchContextWithKibanaMock(); + const fetchContext = createCollectorFetchContextMock(); usageCollectionMock.makeUsageCollector.mockImplementation((config) => { collector = new Collector(logger, config); return createUsageCollectionSetupMock().makeUsageCollector(config); @@ -53,7 +53,7 @@ describe('registerTaskManagerUsageCollector', () => { it('should report telemetry on the excluded task types', async () => { const monitoringStats$ = new Subject(); const usageCollectionMock = createUsageCollectionSetupMock(); - const fetchContext = createCollectorFetchContextWithKibanaMock(); + const fetchContext = createCollectorFetchContextMock(); usageCollectionMock.makeUsageCollector.mockImplementation((config) => { collector = new Collector(logger, config); return createUsageCollectionSetupMock().makeUsageCollector(config); diff --git a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.test.ts b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.test.ts index 7febebc2a51795..e1bea8d1aa0e18 100644 --- a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.test.ts +++ b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.test.ts @@ -112,7 +112,6 @@ describe('Telemetry Collection: Get Aggregated Stats', () => { esClient, usageCollection, soClient, - kibanaRequest: undefined, refreshCache: false, }, context @@ -135,7 +134,6 @@ describe('Telemetry Collection: Get Aggregated Stats', () => { esClient, usageCollection, soClient, - kibanaRequest: undefined, refreshCache: false, }, context @@ -163,7 +161,6 @@ describe('Telemetry Collection: Get Aggregated Stats', () => { esClient, usageCollection, soClient, - kibanaRequest: undefined, refreshCache: false, }, context diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index c35b5dbe666782..3df1c0a71b0957 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -3394,12 +3394,6 @@ "home.addData.uploadFileButtonLabel": "ファイルをアップロード", "home.breadcrumbs.homeTitle": "ホーム", "home.breadcrumbs.integrationsAppTitle": "統合", - "home.dataManagementDisableCollection": " 収集を停止するには、", - "home.dataManagementDisableCollectionLink": "ここで使用状況データを無効にします。", - "home.dataManagementDisclaimerPrivacy": "使用状況データがどのように製品とサービスの管理と改善につながるのかに関する詳細については ", - "home.dataManagementDisclaimerPrivacyLink": "プライバシーポリシーをご覧ください。", - "home.dataManagementEnableCollection": " 収集を開始するには、", - "home.dataManagementEnableCollectionLink": "ここで使用状況データを有効にします。", "home.exploreButtonLabel": "独りで閲覧", "home.exploreYourDataDescription": "すべてのステップを終えたら、データ閲覧準備の完了です。", "home.header.title": "ようこそホーム", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index f706762740ad8e..0326413eb0ac87 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -3402,12 +3402,6 @@ "home.addData.uploadFileButtonLabel": "上传文件", "home.breadcrumbs.homeTitle": "主页", "home.breadcrumbs.integrationsAppTitle": "集成", - "home.dataManagementDisableCollection": " 要停止收集,", - "home.dataManagementDisableCollectionLink": "请在此禁用使用情况数据。", - "home.dataManagementDisclaimerPrivacy": "要了解使用情况数据如何帮助我们管理和改善产品和服务,请参阅我们的 ", - "home.dataManagementDisclaimerPrivacyLink": "隐私声明。", - "home.dataManagementEnableCollection": " 要启动收集,", - "home.dataManagementEnableCollectionLink": "请在此处启用使用情况数据。", "home.exploreButtonLabel": "自己浏览", "home.exploreYourDataDescription": "完成所有步骤后,您便可以随时浏览自己的数据。", "home.header.title": "欢迎归来", diff --git a/x-pack/test/alerting_api_integration/basic/tests/alerts/basic_noop_alert_type.ts b/x-pack/test/alerting_api_integration/basic/tests/alerts/basic_noop_alert_type.ts index 805f4440909ec2..a176d4e73ccf36 100644 --- a/x-pack/test/alerting_api_integration/basic/tests/alerts/basic_noop_alert_type.ts +++ b/x-pack/test/alerting_api_integration/basic/tests/alerts/basic_noop_alert_type.ts @@ -5,19 +5,19 @@ * 2.0. */ -import { getTestAlertData } from '../../../common/lib'; +import { getTestRuleData } from '../../../common/lib'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export -export default function basicAlertTest({ getService }: FtrProviderContext) { +export default function basicRuleTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - describe('basic alert', () => { - it('should return 200 when creating a basic license alert', async () => { + describe('basic rule', () => { + it('should return 200 when creating a basic license rule', async () => { await supertest .post(`/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); }); }); diff --git a/x-pack/test/alerting_api_integration/basic/tests/alerts/gold_noop_alert_type.ts b/x-pack/test/alerting_api_integration/basic/tests/alerts/gold_noop_alert_type.ts index 9e66282d42454d..5726ad8d5d86cd 100644 --- a/x-pack/test/alerting_api_integration/basic/tests/alerts/gold_noop_alert_type.ts +++ b/x-pack/test/alerting_api_integration/basic/tests/alerts/gold_noop_alert_type.ts @@ -5,19 +5,19 @@ * 2.0. */ -import { getTestAlertData } from '../../../common/lib'; +import { getTestRuleData } from '../../../common/lib'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function emailTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - describe('create gold noop alert', () => { - it('should return 403 when creating an gold alert', async () => { + describe('create gold noop rule', () => { + it('should return 403 when creating an gold rule', async () => { await supertest .post(`/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData({ rule_type_id: 'test.gold.noop' })) + .send(getTestRuleData({ rule_type_id: 'test.gold.noop' })) .expect(403, { statusCode: 403, error: 'Forbidden', diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts index 7937a9a2db92c6..0f345c81f08b4c 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts @@ -5,9 +5,15 @@ * 2.0. */ -import { Plugin, CoreSetup, Logger, PluginInitializerContext } from 'kibana/server'; +import { Plugin, CoreSetup, CoreStart, Logger, PluginInitializerContext } from 'kibana/server'; +import { Subject } from 'rxjs'; +import { first } from 'rxjs/operators'; import { PluginSetupContract as ActionsPluginSetup } from '../../../../../../../plugins/actions/server/plugin'; import { PluginSetupContract as AlertingPluginSetup } from '../../../../../../../plugins/alerting/server/plugin'; +import { + TaskManagerSetupContract, + TaskManagerStartContract, +} from '../../../../../../../plugins/task_manager/server/plugin'; import { EncryptedSavedObjectsPluginStart } from '../../../../../../../plugins/encrypted_saved_objects/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../../../../../../plugins/features/server'; import { defineAlertTypes } from './alert_types'; @@ -21,6 +27,7 @@ export interface FixtureSetupDeps { features: FeaturesPluginSetup; actions: ActionsPluginSetup; alerting: AlertingPluginSetup; + taskManager: TaskManagerSetupContract; } export interface FixtureStartDeps { @@ -28,11 +35,17 @@ export interface FixtureStartDeps { security?: SecurityPluginStart; spaces?: SpacesPluginStart; actions: ActionsPluginStart; + taskManager: TaskManagerStartContract; } export class FixturePlugin implements Plugin { private readonly logger: Logger; + taskManagerStart$: Subject = new Subject(); + taskManagerStart: Promise = this.taskManagerStart$ + .pipe(first()) + .toPromise(); + constructor(initializerContext: PluginInitializerContext) { this.logger = initializerContext.logger.get('fixtures', 'plugins', 'alerts'); } @@ -127,9 +140,12 @@ export class FixturePlugin implements Plugin, { logger }: { logger: Logger }) { +export function defineRoutes( + core: CoreSetup, + taskManagerStart: Promise, + { logger }: { logger: Logger } +) { const router = core.http.createRouter(); router.put( { @@ -324,4 +329,39 @@ export function defineRoutes(core: CoreSetup, { logger }: { lo } } ); + + router.post( + { + path: `/api/alerting_actions_telemetry/run_now`, + validate: { + body: schema.object({ + taskId: schema.string({ + validate: (telemetryTaskId: string) => { + if ( + ['Alerting-alerting_telemetry', 'Actions-actions_telemetry'].includes( + telemetryTaskId + ) + ) { + return; + } + return 'invalid telemetry task id'; + }, + }), + }), + }, + }, + async function ( + context: RequestHandlerContext, + req: KibanaRequest, + res: KibanaResponseFactory + ): Promise> { + const { taskId } = req.body; + try { + const taskManager = await taskManagerStart; + return res.ok({ body: await taskManager.runNow(taskId) }); + } catch (err) { + return res.ok({ body: { id: taskId, error: `${err}` } }); + } + } + ); } diff --git a/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts b/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts index ea16351b495438..9da73e1ca6f43e 100644 --- a/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts +++ b/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts @@ -9,7 +9,7 @@ import { Space, User } from '../types'; import { ObjectRemover } from './object_remover'; import { getUrlPrefix } from './space_test_utils'; import { ES_TEST_INDEX_NAME } from './es_test_index_tool'; -import { getTestAlertData } from './get_test_alert_data'; +import { getTestRuleData } from './get_test_rule_data'; export interface AlertUtilsOpts { user?: User; @@ -293,7 +293,7 @@ export class AlertUtils { request = request.auth(this.user.username, this.user.password); } const response = await request.send({ - ...getTestAlertData(), + ...getTestRuleData(), ...overwrites, }); if (response.statusCode === 200) { diff --git a/x-pack/test/alerting_api_integration/common/lib/get_test_alert_data.ts b/x-pack/test/alerting_api_integration/common/lib/get_test_rule_data.ts similarity index 91% rename from x-pack/test/alerting_api_integration/common/lib/get_test_alert_data.ts rename to x-pack/test/alerting_api_integration/common/lib/get_test_rule_data.ts index 22dc93b110a074..ace220a5e81deb 100644 --- a/x-pack/test/alerting_api_integration/common/lib/get_test_alert_data.ts +++ b/x-pack/test/alerting_api_integration/common/lib/get_test_rule_data.ts @@ -5,7 +5,7 @@ * 2.0. */ -export function getTestAlertData(overwrites = {}) { +export function getTestRuleData(overwrites = {}) { return { enabled: true, name: 'abc', diff --git a/x-pack/test/alerting_api_integration/common/lib/index.ts b/x-pack/test/alerting_api_integration/common/lib/index.ts index 305c42b5c1d64d..df7895ed03f6ac 100644 --- a/x-pack/test/alerting_api_integration/common/lib/index.ts +++ b/x-pack/test/alerting_api_integration/common/lib/index.ts @@ -8,7 +8,7 @@ export { ObjectRemover } from './object_remover'; export { getUrlPrefix } from './space_test_utils'; export { ES_TEST_INDEX_NAME, ESTestIndexTool } from './es_test_index_tool'; -export { getTestAlertData } from './get_test_alert_data'; +export { getTestRuleData } from './get_test_rule_data'; export { AlertUtils, getConsumerUnauthorizedErrorMessage, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts index 5692e5dd8f8b2c..8ae50b91584878 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import { UserAtSpaceScenarios } from '../../scenarios'; -import { getUrlPrefix, getTestAlertData, ObjectRemover } from '../../../common/lib'; +import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../common/lib'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export @@ -134,7 +134,7 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ actions: [ { group: 'default', diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts index c3e3c4fc930054..37455149a2a429 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts @@ -14,7 +14,7 @@ import { ESTestIndexTool, ES_TEST_INDEX_NAME, getUrlPrefix, - getTestAlertData, + getTestRuleData, ObjectRemover, AlertUtils, getConsumerUnauthorizedErrorMessage, @@ -494,7 +494,7 @@ instanceStateValue: true .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.always-firing', params: { index: ES_TEST_INDEX_NAME, @@ -603,7 +603,7 @@ instanceStateValue: true .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.authorization', params: { callClusterAuthorizationIndex: authorizationIndex, @@ -711,7 +711,7 @@ instanceStateValue: true .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.always-firing', params: { index: ES_TEST_INDEX_NAME, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/create.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/create.ts index eaa73facb37345..3044142e3c54c4 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/create.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/create.ts @@ -9,7 +9,7 @@ import expect from '@kbn/expect'; import { UserAtSpaceScenarios } from '../../scenarios'; import { checkAAD, - getTestAlertData, + getTestRuleData, getConsumerUnauthorizedErrorMessage, getUrlPrefix, ObjectRemover, @@ -57,7 +57,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send( - getTestAlertData({ + getTestRuleData({ actions: [ { id: createdAction.id, @@ -155,7 +155,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.restricted-noop', consumer: 'alertsRestrictedFixture', }) @@ -194,7 +194,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.unrestricted-noop', consumer: 'alertsFixture', }) @@ -244,7 +244,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.noop', consumer: 'alerts', }) @@ -290,7 +290,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.noop', consumer: 'some consumer patrick invented', }) @@ -325,7 +325,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) - .send(getTestAlertData({ enabled: false })); + .send(getTestRuleData({ enabled: false })); switch (scenario.id) { case 'no_kibana_privileges at space1': @@ -361,7 +361,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send( - getTestAlertData({ + getTestRuleData({ name: ' leading and trailing whitespace ', }) ); @@ -400,7 +400,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.unregistered-alert-type', }) ); @@ -458,7 +458,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.validation', }) ); @@ -500,7 +500,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) - .send(getTestAlertData(getTestAlertData({ schedule: { interval: '10x' } }))); + .send(getTestRuleData({ schedule: { interval: '10x' } })); switch (scenario.id) { case 'no_kibana_privileges at space1': @@ -527,7 +527,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) - .send(getTestAlertData(getTestAlertData({ schedule: { interval: '0s' } }))); + .send(getTestRuleData({ schedule: { interval: '0s' } })); switch (scenario.id) { case 'no_kibana_privileges at space1': diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/delete.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/delete.ts index d43fb2e7d835fa..93e39a011ba3aa 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/delete.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/delete.ts @@ -9,7 +9,7 @@ import expect from '@kbn/expect'; import { UserAtSpaceScenarios } from '../../scenarios'; import { getUrlPrefix, - getTestAlertData, + getTestRuleData, getConsumerUnauthorizedErrorMessage, getProducerUnauthorizedErrorMessage, ObjectRemover, @@ -42,7 +42,7 @@ export default function createDeleteTests({ getService }: FtrProviderContext) { const { body: createdAlert } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); const response = await supertestWithoutAuth @@ -91,7 +91,7 @@ export default function createDeleteTests({ getService }: FtrProviderContext) { .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.restricted-noop', consumer: 'alertsRestrictedFixture', }) @@ -144,7 +144,7 @@ export default function createDeleteTests({ getService }: FtrProviderContext) { .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.unrestricted-noop', consumer: 'alertsFixture', }) @@ -211,7 +211,7 @@ export default function createDeleteTests({ getService }: FtrProviderContext) { .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.noop', consumer: 'alerts', }) @@ -270,7 +270,7 @@ export default function createDeleteTests({ getService }: FtrProviderContext) { const { body: createdAlert } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting'); @@ -303,7 +303,7 @@ export default function createDeleteTests({ getService }: FtrProviderContext) { const { body: createdAlert } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); await retry.try(async () => { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/disable.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/disable.ts index 66f01000ede5e7..8a4266eb8dc8a5 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/disable.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/disable.ts @@ -12,7 +12,7 @@ import { AlertUtils, checkAAD, getUrlPrefix, - getTestAlertData, + getTestRuleData, ObjectRemover, getConsumerUnauthorizedErrorMessage, getProducerUnauthorizedErrorMessage, @@ -58,7 +58,7 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ enabled: true, actions: [ { @@ -121,7 +121,7 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.restricted-noop', consumer: 'alertsRestrictedFixture', enabled: true, @@ -170,7 +170,7 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.unrestricted-noop', consumer: 'alertsFixture', enabled: true, @@ -230,7 +230,7 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.noop', consumer: 'alerts', enabled: true, @@ -285,7 +285,7 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte const { body: createdAlert } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData({ enabled: true })) + .send(getTestRuleData({ enabled: true })) .expect(200); objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting'); @@ -351,7 +351,7 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte const { body: createdAlert } = await supertest .post(`${getUrlPrefix('other')}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData({ enabled: true })) + .send(getTestRuleData({ enabled: true })) .expect(200); objectRemover.add('other', createdAlert.id, 'rule', 'alerting'); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/enable.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/enable.ts index 1589a63cb7108a..205bfe3fda2abb 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/enable.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/enable.ts @@ -12,7 +12,7 @@ import { AlertUtils, checkAAD, getUrlPrefix, - getTestAlertData, + getTestRuleData, ObjectRemover, getConsumerUnauthorizedErrorMessage, getProducerUnauthorizedErrorMessage, @@ -60,7 +60,7 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ enabled: false, actions: [ { @@ -146,7 +146,7 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.restricted-noop', consumer: 'alertsRestrictedFixture', enabled: false, @@ -195,7 +195,7 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.unrestricted-noop', consumer: 'alertsFixture', enabled: false, @@ -249,7 +249,7 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.noop', consumer: 'alerts', enabled: false, @@ -304,7 +304,7 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex const { body: createdAlert } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData({ enabled: false })) + .send(getTestRuleData({ enabled: false })) .expect(200); objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting'); @@ -375,7 +375,7 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex const { body: createdAlert } = await supertest .post(`${getUrlPrefix('other')}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData({ enabled: false })) + .send(getTestRuleData({ enabled: false })) .expect(200); objectRemover.add('other', createdAlert.id, 'rule', 'alerting'); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/event_log.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/event_log.ts index 940203a9b1f8c5..04ff3d929dc159 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/event_log.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/event_log.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import { Spaces } from '../../scenarios'; -import { getUrlPrefix, getTestAlertData, ObjectRemover, getEventLog } from '../../../common/lib'; +import { getUrlPrefix, getTestRuleData, ObjectRemover, getEventLog } from '../../../common/lib'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; import { validateEvent } from '../../../spaces_only/tests/alerting/event_log'; @@ -27,7 +27,7 @@ export default function eventLogTests({ getService }: FtrProviderContext) { .post(`${getUrlPrefix(spaceId)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.noop', schedule: { interval: '1s' }, throttle: null, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/excluded.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/excluded.ts index add30b178c7e6e..eae80da85dc599 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/excluded.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/excluded.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; import { UserAtSpaceScenarios } from '../../scenarios'; import { - getTestAlertData, + getTestRuleData, getUrlPrefix, ObjectRemover, getEventLog, @@ -57,7 +57,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'foo') .auth(scenario.user.username, scenario.user.password) .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.always-firing', schedule: { interval: '1s' }, throttle: '1s', diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/execution_status.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/execution_status.ts index 2bae1c541bc485..dba73cba184ddb 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/execution_status.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/execution_status.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; import { AlertExecutionStatusErrorReasons } from '../../../../../plugins/alerting/common'; import { Spaces } from '../../scenarios'; -import { getUrlPrefix, getTestAlertData, ObjectRemover } from '../../../common/lib'; +import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../common/lib'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export @@ -28,7 +28,7 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon .post(`${getUrlPrefix(spaceId)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.noop', schedule: { interval: '1s' }, }) diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts index 3274e25e48301a..f6ba70e7c21972 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts @@ -10,7 +10,7 @@ import { SuperTest, Test } from 'supertest'; import { chunk, omit } from 'lodash'; import uuid from 'uuid'; import { UserAtSpaceScenarios } from '../../scenarios'; -import { getUrlPrefix, getTestAlertData, ObjectRemover } from '../../../common/lib'; +import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../common/lib'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; const findTestUtils = ( @@ -29,7 +29,7 @@ const findTestUtils = ( const { body: createdAlert } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting'); @@ -94,7 +94,7 @@ const findTestUtils = ( it('should filter out types that the user is not authorized to `get` retaining pagination', async () => { async function createNoOpAlert(overrides = {}) { - const alert = getTestAlertData(overrides); + const alert = getTestRuleData(overrides); const { body: createdAlert } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') @@ -212,7 +212,7 @@ const findTestUtils = ( .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ enabled: false, actions: [ { @@ -297,7 +297,7 @@ const findTestUtils = ( .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ enabled: false, tags: [myTag], rule_type_id: 'test.restricted-noop', @@ -312,7 +312,7 @@ const findTestUtils = ( .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ tags: [myTag], rule_type_id: 'test.restricted-noop', consumer: 'alertsRestrictedFixture', @@ -374,7 +374,7 @@ const findTestUtils = ( .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ enabled: false, tags: [myTag], rule_type_id: 'test.restricted-noop', @@ -389,7 +389,7 @@ const findTestUtils = ( .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ tags: [myTag], rule_type_id: 'test.restricted-noop', consumer: 'alertsRestrictedFixture', @@ -451,7 +451,7 @@ const findTestUtils = ( const { body: createdAlert } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting'); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get.ts index 05b053f468a694..6d072b2e26f455 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get.ts @@ -10,7 +10,7 @@ import { SuperTest, Test } from 'supertest'; import { UserAtSpaceScenarios } from '../../scenarios'; import { getUrlPrefix, - getTestAlertData, + getTestRuleData, ObjectRemover, getConsumerUnauthorizedErrorMessage, getProducerUnauthorizedErrorMessage, @@ -32,7 +32,7 @@ const getTestUtils = ( const { body: createdAlert } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting'); @@ -96,7 +96,7 @@ const getTestUtils = ( .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.restricted-noop', consumer: 'alertsRestrictedFixture', }) @@ -143,7 +143,7 @@ const getTestUtils = ( .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.unrestricted-noop', consumer: 'alertsFixture', }) @@ -201,7 +201,7 @@ const getTestUtils = ( .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.restricted-noop', consumer: 'alerts', }) @@ -258,7 +258,7 @@ const getTestUtils = ( const { body: createdAlert } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting'); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_state.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_state.ts index e00d8e53e438eb..3bdfe49464fcfe 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_state.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_state.ts @@ -9,7 +9,7 @@ import expect from '@kbn/expect'; import { getUrlPrefix, ObjectRemover, - getTestAlertData, + getTestRuleData, getConsumerUnauthorizedErrorMessage, getProducerUnauthorizedErrorMessage, } from '../../../common/lib'; @@ -33,7 +33,7 @@ export default function createGetAlertStateTests({ getService }: FtrProviderCont const { body: createdAlert } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting'); @@ -69,7 +69,7 @@ export default function createGetAlertStateTests({ getService }: FtrProviderCont .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.unrestricted-noop', consumer: 'alertsFixture', }) @@ -123,7 +123,7 @@ export default function createGetAlertStateTests({ getService }: FtrProviderCont const { body: createdAlert } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting'); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_summary.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_summary.ts index 3becd487116f72..eb4e592a91d8af 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_summary.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_summary.ts @@ -10,7 +10,7 @@ import { omit } from 'lodash'; import { getUrlPrefix, ObjectRemover, - getTestAlertData, + getTestRuleData, getConsumerUnauthorizedErrorMessage, getProducerUnauthorizedErrorMessage, } from '../../../common/lib'; @@ -34,7 +34,7 @@ export default function createGetAlertSummaryTests({ getService }: FtrProviderCo const { body: createdRule } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add(space.id, createdRule.id, 'rule', 'alerting'); @@ -98,7 +98,7 @@ export default function createGetAlertSummaryTests({ getService }: FtrProviderCo .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.unrestricted-noop', consumer: 'alertsFixture', }) @@ -154,7 +154,7 @@ export default function createGetAlertSummaryTests({ getService }: FtrProviderCo const { body: createdRule } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add(space.id, createdRule.id, 'rule', 'alerting'); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/health.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/health.ts index 22bf2cdc4204bd..d51cf8cc96af90 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/health.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/health.ts @@ -10,7 +10,7 @@ import { UserAtSpaceScenarios } from '../../scenarios'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; import { getUrlPrefix, - getTestAlertData, + getTestRuleData, ObjectRemover, AlertUtils, ESTestIndexTool, @@ -105,7 +105,7 @@ export default function createFindTests({ getService }: FtrProviderContext) { .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ schedule: { interval: '5m', }, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mustache_templates.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mustache_templates.ts index 8344d4a281ba1f..7e3a7599a73e06 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mustache_templates.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mustache_templates.ts @@ -19,7 +19,7 @@ import httpProxy from 'http-proxy'; import expect from '@kbn/expect'; import { Spaces } from '../../scenarios'; -import { getUrlPrefix, getTestAlertData, ObjectRemover } from '../../../common/lib'; +import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../common/lib'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; import { getSlackServer } from '../../../common/fixtures/plugins/actions_simulators/server/plugin'; import { getHttpProxyServer } from '../../../common/lib/get_proxy_server'; @@ -81,7 +81,7 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon .post(`${getUrlPrefix(Spaces[0].id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ name: 'testing context variable kibanaBaseUrl', rule_type_id: 'test.patternFiring', params: { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mute_all.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mute_all.ts index 993b66353756f6..bb570e5754e99f 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mute_all.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mute_all.ts @@ -12,7 +12,7 @@ import { AlertUtils, checkAAD, getUrlPrefix, - getTestAlertData, + getTestRuleData, ObjectRemover, getConsumerUnauthorizedErrorMessage, getProducerUnauthorizedErrorMessage, @@ -49,7 +49,7 @@ export default function createMuteAlertTests({ getService }: FtrProviderContext) .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ enabled: false, actions: [ { @@ -117,7 +117,7 @@ export default function createMuteAlertTests({ getService }: FtrProviderContext) .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ enabled: false, rule_type_id: 'test.restricted-noop', consumer: 'alertsRestrictedFixture', @@ -173,7 +173,7 @@ export default function createMuteAlertTests({ getService }: FtrProviderContext) .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ enabled: false, rule_type_id: 'test.unrestricted-noop', consumer: 'alertsFixture', @@ -240,7 +240,7 @@ export default function createMuteAlertTests({ getService }: FtrProviderContext) .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ enabled: false, rule_type_id: 'test.restricted-noop', consumer: 'alerts', diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mute_instance.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mute_instance.ts index 4948737e0778a3..3948f910423a9d 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mute_instance.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mute_instance.ts @@ -12,7 +12,7 @@ import { AlertUtils, checkAAD, getUrlPrefix, - getTestAlertData, + getTestRuleData, ObjectRemover, getConsumerUnauthorizedErrorMessage, getProducerUnauthorizedErrorMessage, @@ -49,7 +49,7 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ enabled: false, actions: [ { @@ -117,7 +117,7 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ enabled: false, rule_type_id: 'test.restricted-noop', consumer: 'alertsRestrictedFixture', @@ -173,7 +173,7 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ enabled: false, rule_type_id: 'test.unrestricted-noop', consumer: 'alertsFixture', @@ -240,7 +240,7 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ enabled: false, rule_type_id: 'test.restricted-noop', consumer: 'alerts', @@ -306,7 +306,7 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider const { body: createdAlert } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData({ enabled: false })) + .send(getTestRuleData({ enabled: false })) .expect(200); objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting'); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/unmute_all.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/unmute_all.ts index 526f809033646c..f9c1bce2b03188 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/unmute_all.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/unmute_all.ts @@ -12,7 +12,7 @@ import { AlertUtils, checkAAD, getUrlPrefix, - getTestAlertData, + getTestRuleData, ObjectRemover, getConsumerUnauthorizedErrorMessage, getProducerUnauthorizedErrorMessage, @@ -49,7 +49,7 @@ export default function createUnmuteAlertTests({ getService }: FtrProviderContex .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ enabled: false, actions: [ { @@ -122,7 +122,7 @@ export default function createUnmuteAlertTests({ getService }: FtrProviderContex .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ enabled: false, rule_type_id: 'test.restricted-noop', consumer: 'alertsRestrictedFixture', @@ -183,7 +183,7 @@ export default function createUnmuteAlertTests({ getService }: FtrProviderContex .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ enabled: false, rule_type_id: 'test.unrestricted-noop', consumer: 'alertsFixture', @@ -255,7 +255,7 @@ export default function createUnmuteAlertTests({ getService }: FtrProviderContex .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ enabled: false, rule_type_id: 'test.restricted-noop', consumer: 'alerts', diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/unmute_instance.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/unmute_instance.ts index 9c045db8883911..17ee25e822a6da 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/unmute_instance.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/unmute_instance.ts @@ -12,7 +12,7 @@ import { AlertUtils, checkAAD, getUrlPrefix, - getTestAlertData, + getTestRuleData, ObjectRemover, getConsumerUnauthorizedErrorMessage, getProducerUnauthorizedErrorMessage, @@ -49,7 +49,7 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ enabled: false, actions: [ { @@ -122,7 +122,7 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ enabled: false, rule_type_id: 'test.restricted-noop', consumer: 'alertsRestrictedFixture', @@ -183,7 +183,7 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ enabled: false, rule_type_id: 'test.unrestricted-noop', consumer: 'alertsFixture', @@ -255,7 +255,7 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ enabled: false, rule_type_id: 'test.restricted-noop', consumer: 'alerts', diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts index e628f0b3d950e2..b2a1ae223f62c9 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts @@ -11,7 +11,7 @@ import { UserAtSpaceScenarios } from '../../scenarios'; import { checkAAD, getUrlPrefix, - getTestAlertData, + getTestRuleData, ObjectRemover, ensureDatetimeIsWithinRange, getConsumerUnauthorizedErrorMessage, @@ -55,7 +55,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { const { body: createdAlert } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting'); @@ -156,7 +156,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.restricted-noop', consumer: 'alertsRestrictedFixture', }) @@ -240,7 +240,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.unrestricted-noop', consumer: 'alertsFixture', }) @@ -335,7 +335,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.restricted-noop', consumer: 'alerts', }) @@ -429,7 +429,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { const { body: createdAlert } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting'); @@ -522,7 +522,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { const { body: createdAlert } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting'); @@ -574,7 +574,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { const { body: createdAlert } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting'); @@ -618,7 +618,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { const { body: createdAlert } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting'); @@ -691,7 +691,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.validation', params: { param1: 'test', @@ -753,7 +753,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send( - getTestAlertData({ + getTestRuleData({ schedule: { interval: '10x' }, enabled: undefined, consumer: undefined, @@ -785,7 +785,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ schedule: { interval: '30m' }, }) ) @@ -931,7 +931,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ schedule: { interval: '1m' }, }) ) diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update_api_key.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update_api_key.ts index a434109a18933a..1c25ec550c41ef 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update_api_key.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update_api_key.ts @@ -12,7 +12,7 @@ import { AlertUtils, checkAAD, getUrlPrefix, - getTestAlertData, + getTestRuleData, ObjectRemover, getConsumerUnauthorizedErrorMessage, getProducerUnauthorizedErrorMessage, @@ -50,7 +50,7 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ actions: [ { id: createdAction.id, @@ -116,7 +116,7 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.restricted-noop', consumer: 'alertsRestrictedFixture', }) @@ -170,7 +170,7 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.unrestricted-noop', consumer: 'alertsFixture', }) @@ -235,7 +235,7 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.restricted-noop', consumer: 'alerts', }) @@ -299,7 +299,7 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte const { body: createdAlert } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting'); @@ -363,7 +363,7 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte const { body: createdAlert } = await supertest .post(`${getUrlPrefix('other')}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add('other', createdAlert.id, 'rule', 'alerting'); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/index.ts index 211fe9ec268632..2b26410afeaede 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/index.ts @@ -60,6 +60,7 @@ export default function alertingApiIntegrationTests({ loadTestFile }: FtrProvide describe('alerting api integration security and spaces enabled', function () { this.tags('ciGroup17'); + loadTestFile(require.resolve('./telemetry')); loadTestFile(require.resolve('./actions')); loadTestFile(require.resolve('./alerting')); }); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/telemetry/actions_telemetry.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/telemetry/actions_telemetry.ts new file mode 100644 index 00000000000000..350f0019641b8b --- /dev/null +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/telemetry/actions_telemetry.ts @@ -0,0 +1,241 @@ +/* + * 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 expect from '@kbn/expect'; +import { Spaces, Superuser } from '../../scenarios'; +import { + getUrlPrefix, + getEventLog, + getTestRuleData, + ObjectRemover, + TaskManagerDoc, +} from '../../../common/lib'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function createActionsTelemetryTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const es = getService('es'); + const retry = getService('retry'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + describe('actions telemetry', () => { + const alwaysFiringRuleId: { [key: string]: string } = {}; + const objectRemover = new ObjectRemover(supertest); + + before(async () => { + // reset the state in the telemetry task + await es.update({ + id: `task:Actions-actions_telemetry`, + index: '.kibana_task_manager', + body: { + doc: { + task: { + state: '{}', + }, + }, + }, + }); + }); + after(() => objectRemover.removeAll()); + + async function createConnector(opts: { name: string; space: string; connectorTypeId: string }) { + const { name, space, connectorTypeId } = opts; + const { body: createdConnector } = await supertestWithoutAuth + .post(`${getUrlPrefix(space)}/api/actions/connector`) + .set('kbn-xsrf', 'foo') + .auth(Superuser.username, Superuser.password) + .send({ + name, + connector_type_id: connectorTypeId, + config: {}, + secrets: {}, + }) + .expect(200); + objectRemover.add(space, createdConnector.id, 'action', 'actions'); + return createdConnector.id; + } + + async function createRule(opts: { space: string; ruleOverwrites: any }) { + const { ruleOverwrites, space } = opts; + const ruleResponse = await supertestWithoutAuth + .post(`${getUrlPrefix(space)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .auth(Superuser.username, Superuser.password) + .send(getTestRuleData(ruleOverwrites)); + expect(ruleResponse.status).to.eql(200); + objectRemover.add(space, ruleResponse.body.id, 'rule', 'alerting'); + return ruleResponse.body.id; + } + + async function setup() { + // Create rules and connectors in multiple spaces + for (const space of Spaces) { + const noopConnectorId = await createConnector({ + name: 'noop connector', + space: space.id, + connectorTypeId: 'test.noop', + }); + const failingConnectorId = await createConnector({ + name: 'connector that throws', + space: space.id, + connectorTypeId: 'test.throw', + }); + + await createConnector({ + name: 'unused connector', + space: space.id, + connectorTypeId: 'test.excluded', + }); + await createRule({ + space: space.id, + ruleOverwrites: { + rule_type_id: 'test.noop', + schedule: { interval: '1s' }, + throttle: null, + notify_when: 'onActiveAlert', + params: {}, + actions: [ + { + id: noopConnectorId, + group: 'default', + params: {}, + }, + ], + }, + }); + await createRule({ + space: space.id, + ruleOverwrites: { + rule_type_id: 'test.noop', + schedule: { interval: '1s' }, + throttle: null, + params: {}, + notify_when: 'onActiveAlert', + actions: [ + { + id: failingConnectorId, + group: 'default', + params: {}, + }, + ], + }, + }); + + alwaysFiringRuleId[space.id] = await createRule({ + space: space.id, + ruleOverwrites: { + rule_type_id: 'test.cumulative-firing', + schedule: { interval: '3s' }, + throttle: null, + notify_when: 'onActiveAlert', + params: {}, + actions: [ + { + id: noopConnectorId, + group: 'default', + params: {}, + }, + { + id: failingConnectorId, + group: 'default', + params: {}, + }, + { + id: 'my-slack1', + group: 'other', + params: {}, + }, + ], + }, + }); + } + } + + it('should retrieve telemetry data in the expected format', async () => { + await setup(); + + // let it run for a bit + await retry.try(async () => { + return await getEventLog({ + getService, + spaceId: Spaces[0].id, + type: 'alert', + id: alwaysFiringRuleId[Spaces[0].id], + provider: 'alerting', + actions: new Map([['execute', { gte: 5 }]]), + }); + }); + + // request telemetry task to run + await supertest + .post('/api/alerting_actions_telemetry/run_now') + .set('kbn-xsrf', 'xxx') + .send({ taskId: 'Actions-actions_telemetry' }) + .expect(200); + + // get telemetry task doc + const telemetryTask = await es.get({ + id: `task:Actions-actions_telemetry`, + index: '.kibana_task_manager', + }); + const taskState = telemetryTask?._source?.task?.state; + expect(taskState).not.to.be(undefined); + const telemetry = JSON.parse(taskState!); + + // total number of connectors + expect(telemetry.count_total).to.equal(17); + + // total number of active connectors (used by a rule) + expect(telemetry.count_active_total).to.equal(7); + + // total number of connectors broken down by connector type + expect(telemetry.count_by_type['test.throw']).to.equal(3); + expect(telemetry.count_by_type['test.excluded']).to.equal(3); + expect(telemetry.count_by_type['test.noop']).to.equal(3); + expect(telemetry.count_by_type.__slack).to.equal(1); + expect(telemetry.count_by_type['system-abc-action-type']).to.equal(1); + expect(telemetry.count_by_type.__index).to.equal(1); + expect(telemetry.count_by_type['test.index-record']).to.equal(1); + expect(telemetry.count_by_type.__webhook).to.equal(4); + + // total number of active connectors broken down by connector type + expect(telemetry.count_active_by_type['test.throw']).to.equal(3); + expect(telemetry.count_active_by_type['test.noop']).to.equal(3); + expect(telemetry.count_active_by_type.__slack).to.equal(1); + + // total number of rules using the alert history connector + expect(telemetry.count_active_alert_history_connectors).to.equal(0); + + // total number of email connectors used by rules broken down by service type + // testing for existence of this field but we don't have any rules using email + // connectors in this test + expect(telemetry.count_active_email_connectors_by_service_type).to.be.empty(); + + // number of spaces with connectors + expect(telemetry.count_actions_namespaces).to.equal(3); + + // number of action executions - just checking for non-zero as we can't set an exact number + expect(telemetry.count_actions_executions_per_day > 0).to.be(true); + + // number of action executions broken down by connector type + expect(telemetry.count_actions_executions_by_type_per_day['test.noop'] > 0).to.be(true); + + // average execution time - just checking for non-zero as we can't set an exact number + expect(telemetry.avg_execution_time_per_day > 0).to.be(true); + + // average execution time broken down by rule type + expect(telemetry.avg_execution_time_by_type_per_day['test.noop'] > 0).to.be(true); + + // number of failed executions + expect(telemetry.count_actions_executions_failed_per_day > 0).to.be(true); + expect(telemetry.count_actions_executions_failed_by_type_per_day['test.throw'] > 0).to.be( + true + ); + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/telemetry/alerting_telemetry.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/telemetry/alerting_telemetry.ts new file mode 100644 index 00000000000000..9b8a96bc056cef --- /dev/null +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/telemetry/alerting_telemetry.ts @@ -0,0 +1,304 @@ +/* + * 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 expect from '@kbn/expect'; +import { Spaces, Superuser } from '../../scenarios'; +import { + getUrlPrefix, + getEventLog, + getTestRuleData, + ObjectRemover, + TaskManagerDoc, +} from '../../../common/lib'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function createAlertingTelemetryTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const es = getService('es'); + const retry = getService('retry'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + describe('alerting telemetry', () => { + const alwaysFiringRuleId: { [key: string]: string } = {}; + const objectRemover = new ObjectRemover(supertest); + + before(async () => { + // reset the state in the telemetry task + await es.update({ + id: `task:Alerting-alerting_telemetry`, + index: '.kibana_task_manager', + body: { + doc: { + task: { + state: '{}', + }, + }, + }, + }); + }); + after(() => objectRemover.removeAll()); + + async function createConnector(opts: { name: string; space: string; connectorTypeId: string }) { + const { name, space, connectorTypeId } = opts; + const { body: createdConnector } = await supertestWithoutAuth + .post(`${getUrlPrefix(space)}/api/actions/connector`) + .set('kbn-xsrf', 'foo') + .auth(Superuser.username, Superuser.password) + .send({ + name, + connector_type_id: connectorTypeId, + config: {}, + secrets: {}, + }) + .expect(200); + objectRemover.add(space, createdConnector.id, 'action', 'actions'); + return createdConnector.id; + } + + async function createRule(opts: { space: string; ruleOverwrites: any }) { + const { ruleOverwrites, space } = opts; + const ruleResponse = await supertestWithoutAuth + .post(`${getUrlPrefix(space)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .auth(Superuser.username, Superuser.password) + .send(getTestRuleData(ruleOverwrites)); + expect(ruleResponse.status).to.eql(200); + objectRemover.add(space, ruleResponse.body.id, 'rule', 'alerting'); + return ruleResponse.body.id; + } + + async function setup() { + // Create rules and connectors in multiple spaces + for (const space of Spaces) { + const noopConnectorId = await createConnector({ + name: 'noop connector', + space: space.id, + connectorTypeId: 'test.noop', + }); + await createConnector({ + name: 'connector that errors', + space: space.id, + connectorTypeId: 'test.throw', + }); + await createRule({ + space: space.id, + ruleOverwrites: { + rule_type_id: 'test.noop', + schedule: { interval: '30s' }, + throttle: '1s', + params: {}, + actions: [ + { + id: noopConnectorId, + group: 'default', + params: {}, + }, + ], + }, + }); + await createRule({ + space: space.id, + ruleOverwrites: { + rule_type_id: 'test.onlyContextVariables', + schedule: { interval: '10s' }, + throttle: '10m', + params: {}, + actions: [ + { + id: noopConnectorId, + group: 'default', + params: {}, + }, + ], + }, + }); + await createRule({ + space: space.id, + ruleOverwrites: { + rule_type_id: 'test.throw', + schedule: { interval: '1m' }, + throttle: '30s', + params: {}, + actions: [ + { + id: noopConnectorId, + group: 'default', + params: {}, + }, + ], + }, + }); + + alwaysFiringRuleId[space.id] = await createRule({ + space: space.id, + ruleOverwrites: { + rule_type_id: 'example.always-firing', + schedule: { interval: '3s' }, + throttle: null, + params: {}, + actions: [ + { + id: noopConnectorId, + group: 'small', + params: {}, + }, + { + id: noopConnectorId, + group: 'medium', + params: {}, + }, + { + id: noopConnectorId, + group: 'large', + params: {}, + }, + ], + }, + }); + + await createRule({ + space: space.id, + ruleOverwrites: { + rule_type_id: 'test.noop', + schedule: { interval: '5m' }, + throttle: null, + enabled: false, + params: {}, + actions: [ + { + id: noopConnectorId, + group: 'default', + params: {}, + }, + ], + }, + }); + } + } + + it('should retrieve telemetry data in the expected format', async () => { + await setup(); + + // let it run for a bit + await retry.try(async () => { + return await getEventLog({ + getService, + spaceId: Spaces[0].id, + type: 'alert', + id: alwaysFiringRuleId[Spaces[0].id], + provider: 'alerting', + actions: new Map([['execute', { gte: 5 }]]), + }); + }); + + // request telemetry task to run + await supertest + .post('/api/alerting_actions_telemetry/run_now') + .set('kbn-xsrf', 'xxx') + .send({ taskId: 'Alerting-alerting_telemetry' }) + .expect(200); + + // get telemetry task doc + const telemetryTask = await es.get({ + id: `task:Alerting-alerting_telemetry`, + index: '.kibana_task_manager', + }); + const taskState = telemetryTask?._source?.task?.state; + expect(taskState).not.to.be(undefined); + const telemetry = JSON.parse(taskState!); + + // total number of rules + expect(telemetry.count_total).to.equal(15); + + // total number of enabled rules + expect(telemetry.count_active_total).to.equal(12); + + // total number of disabled rules + expect(telemetry.count_disabled_total).to.equal(3); + + // total number of rules broken down by rule type + expect(telemetry.count_by_type.test__onlyContextVariables).to.equal(3); + expect(telemetry.count_by_type['example__always-firing']).to.equal(3); + expect(telemetry.count_by_type.test__throw).to.equal(3); + expect(telemetry.count_by_type.test__noop).to.equal(6); + + // total number of enabled rules broken down by rule type + expect(telemetry.count_active_by_type.test__onlyContextVariables).to.equal(3); + expect(telemetry.count_active_by_type['example__always-firing']).to.equal(3); + expect(telemetry.count_active_by_type.test__throw).to.equal(3); + expect(telemetry.count_active_by_type.test__noop).to.equal(3); + + // throttle time stats + expect(telemetry.throttle_time.min).to.equal('0s'); + expect(telemetry.throttle_time.avg).to.equal('157.75s'); + expect(telemetry.throttle_time.max).to.equal('600s'); + expect(telemetry.throttle_time_number_s.min).to.equal(0); + expect(telemetry.throttle_time_number_s.avg).to.equal(157.75); + expect(telemetry.throttle_time_number_s.max).to.equal(600); + + // schedule interval stats + expect(telemetry.schedule_time.min).to.equal('3s'); + expect(telemetry.schedule_time.avg).to.equal('80.6s'); + expect(telemetry.schedule_time.max).to.equal('300s'); + expect(telemetry.schedule_time_number_s.min).to.equal(3); + expect(telemetry.schedule_time_number_s.avg).to.equal(80.6); + expect(telemetry.schedule_time_number_s.max).to.equal(300); + + // attached connectors stats + expect(telemetry.connectors_per_alert.min).to.equal(1); + expect(telemetry.connectors_per_alert.avg).to.equal(1.4); + expect(telemetry.connectors_per_alert.max).to.equal(3); + + // number of spaces with rules + expect(telemetry.count_rules_namespaces).to.equal(3); + + // number of rule executions - just checking for non-zero as we can't set an exact number + // each rule should have had a chance to execute once + expect(telemetry.count_rules_executions_per_day >= 15).to.be(true); + + // number of rule executions broken down by rule type + expect(telemetry.count_by_type.test__onlyContextVariables >= 3).to.be(true); + expect(telemetry.count_by_type['example__always-firing'] >= 3).to.be(true); + expect(telemetry.count_by_type.test__throw >= 3).to.be(true); + expect(telemetry.count_by_type.test__noop >= 3).to.be(true); + + // average execution time - just checking for non-zero as we can't set an exact number + expect(telemetry.avg_execution_time_per_day > 0).to.be(true); + + // average execution time broken down by rule type + expect(telemetry.avg_execution_time_by_type_per_day.test__onlyContextVariables > 0).to.be( + true + ); + expect(telemetry.avg_execution_time_by_type_per_day['example__always-firing'] > 0).to.be( + true + ); + expect(telemetry.avg_execution_time_by_type_per_day.test__throw > 0).to.be(true); + expect(telemetry.avg_execution_time_by_type_per_day.test__noop > 0).to.be(true); + + // number of failed executions - we have one rule that always fails + expect(telemetry.count_rules_executions_failured_per_day >= 1).to.be(true); + expect(telemetry.count_rules_executions_failured_by_reason_per_day.execute >= 1).to.be(true); + expect( + telemetry.count_rules_executions_failured_by_reason_by_type_per_day.execute.test__throw >= 1 + ).to.be(true); + + // number of execution timeouts - testing for existence of this field but + // this test doesn't have any rules that timeout + expect(telemetry.count_rules_executions_timeouts_per_day).to.equal(0); + expect(telemetry.count_rules_executions_timeouts_by_type_per_day).to.be.empty(); + + // number of failed/unrecognized tasks - testing for existence of this field but + // this test doesn't have any unrecognized rule types + expect(telemetry.count_failed_and_unrecognized_rule_tasks_per_day).to.equal(0); + expect(telemetry.count_failed_and_unrecognized_rule_tasks_by_status_per_day).to.be.empty(); + expect( + telemetry.count_failed_and_unrecognized_rule_tasks_by_status_by_type_per_day + ).to.be.empty(); + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/telemetry/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/telemetry/index.ts new file mode 100644 index 00000000000000..9e73fafc9f7bd5 --- /dev/null +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/telemetry/index.ts @@ -0,0 +1,26 @@ +/* + * 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 { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { setupSpacesAndUsers, tearDown } from '..'; + +// eslint-disable-next-line import/no-default-export +export default function actionsTests({ loadTestFile, getService }: FtrProviderContext) { + describe('Alerting and Actions Telemetry', () => { + before(async () => { + await setupSpacesAndUsers(getService); + }); + + after(async () => { + await tearDown(getService); + }); + + // run telemetry tests before anything else + loadTestFile(require.resolve('./actions_telemetry')); + loadTestFile(require.resolve('./alerting_telemetry')); + }); +} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/preconfigured_alert_history_connector.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/preconfigured_alert_history_connector.ts index dea873073f61f8..274e147898d9bc 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/preconfigured_alert_history_connector.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/preconfigured_alert_history_connector.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import { getTestAlertData, ObjectRemover } from '../../../../common/lib'; +import { getTestRuleData, ObjectRemover } from '../../../../common/lib'; import { AlertHistoryDefaultIndexName } from '../../../../../../plugins/actions/common'; const ALERT_HISTORY_OVERRIDE_INDEX = 'kibana-alert-history-not-the-default'; @@ -27,7 +27,7 @@ export default function preconfiguredAlertHistoryConnectorTests({ const alertId = 'instance'; function getTestData(params = {}) { - return getTestAlertData({ + return getTestRuleData({ rule_type_id: ruleTypeId, schedule: { interval: '1s' }, params: { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/aggregate.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/aggregate.ts index cf7ebffef85a28..961a15dd5223de 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/aggregate.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/aggregate.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import { Spaces } from '../../scenarios'; -import { getUrlPrefix, getTestAlertData, ObjectRemover } from '../../../common/lib'; +import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../common/lib'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export @@ -239,7 +239,7 @@ export default function createAggregateTests({ getService }: FtrProviderContext) const { body: createdAlert } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData(testAlertOverrides)) + .send(getTestRuleData(testAlertOverrides)) .expect(200); await waitForStatus(createdAlert.id, new Set([status])); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_base.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_base.ts index ea818a6e64b0dc..58ddd5516d8a54 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_base.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_base.ts @@ -16,7 +16,7 @@ import { ESTestIndexTool, ES_TEST_INDEX_NAME, getUrlPrefix, - getTestAlertData, + getTestRuleData, ObjectRemover, AlertUtils, ensureDatetimeIsWithinRange, @@ -198,7 +198,7 @@ instanceStateValue: true .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.patternFiring', schedule: { interval: '1s' }, throttle: null, @@ -259,7 +259,7 @@ instanceStateValue: true .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.patternFiring', schedule: { interval: '1s' }, enabled: false, @@ -354,7 +354,7 @@ instanceStateValue: true .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ schedule: { interval: '1m' }, rule_type_id: 'test.always-firing', params: { @@ -427,7 +427,7 @@ instanceStateValue: true .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.authorization', params: { callClusterAuthorizationIndex: authorizationIndex, @@ -473,7 +473,7 @@ instanceStateValue: true .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.always-firing', params: { index: ES_TEST_INDEX_NAME, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts index b002e0668dc527..1d5eb16ff3f899 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts @@ -11,7 +11,7 @@ import { Spaces } from '../../scenarios'; import { checkAAD, getUrlPrefix, - getTestAlertData, + getTestRuleData, ObjectRemover, getConsumerUnauthorizedErrorMessage, TaskManagerDoc, @@ -53,7 +53,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ actions: [ { id: createdAction.id, @@ -132,7 +132,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ actions: [ { id: createdAction.id, @@ -244,7 +244,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ params: { ignoredButPersisted: lotsOfSpaces, }, @@ -288,7 +288,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ params: { risk_score: 40, severity: 'medium', @@ -325,7 +325,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { const response = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule/${customId}`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()); + .send(getTestRuleData()); expect(response.status).to.eql(200); objectRemover.add(Spaces.space1.id, response.body.id, 'rule', 'alerting'); @@ -344,7 +344,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { const response = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule/${customId}`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()); + .send(getTestRuleData()); expect(response.status).to.eql(200); objectRemover.add(Spaces.space1.id, response.body.id, 'rule', 'alerting'); @@ -363,7 +363,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { const response = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule/${customId}`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()); + .send(getTestRuleData()); expect(response.status).to.eql(400); expect(response.body).to.eql({ @@ -379,13 +379,13 @@ export default function createAlertTests({ getService }: FtrProviderContext) { const createdAlertResponse = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule/${customId}`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add(Spaces.space1.id, createdAlertResponse.body.id, 'rule', 'alerting'); await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule/${customId}`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(409); }); @@ -393,7 +393,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { const response = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData({ consumer: 'some consumer patrick invented' })); + .send(getTestRuleData({ consumer: 'some consumer patrick invented' })); expect(response.status).to.eql(403); expect(response.body).to.eql({ @@ -411,7 +411,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { const response = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData({ enabled: false })); + .send(getTestRuleData({ enabled: false })); expect(response.status).to.eql(200); objectRemover.add(Spaces.space1.id, response.body.id, 'rule', 'alerting'); @@ -435,7 +435,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { rule_type_id: alertTypeId, notify_when: notifyWhen, ...testAlert - } = getTestAlertData({ + } = getTestRuleData({ actions: [ { id: createdAction.id, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/delete.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/delete.ts index 0a2df70b6316ac..073d76dc859a57 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/delete.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/delete.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import { Spaces } from '../../scenarios'; -import { getUrlPrefix, getTestAlertData, ObjectRemover } from '../../../common/lib'; +import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../common/lib'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export @@ -31,7 +31,7 @@ export default function createDeleteTests({ getService }: FtrProviderContext) { const { body: createdAlert } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); await supertest @@ -51,7 +51,7 @@ export default function createDeleteTests({ getService }: FtrProviderContext) { const { body: createdAlert } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); await supertest @@ -69,7 +69,7 @@ export default function createDeleteTests({ getService }: FtrProviderContext) { const { body: createdAlert } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); await supertest diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/disable.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/disable.ts index 51cb54aa5f9e54..2a1d27a4d3b399 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/disable.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/disable.ts @@ -12,7 +12,7 @@ import { AlertUtils as RuleUtils, checkAAD, getUrlPrefix, - getTestAlertData as getTestRuleData, + getTestRuleData, ObjectRemover, getEventLog, } from '../../../common/lib'; diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/enable.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/enable.ts index 611c2498dd9d33..c0c56ed354a843 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/enable.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/enable.ts @@ -12,7 +12,7 @@ import { AlertUtils, checkAAD, getUrlPrefix, - getTestAlertData, + getTestRuleData, ObjectRemover, TaskManagerDoc, } from '../../../common/lib'; @@ -40,7 +40,7 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex const { body: createdAlert } = await supertestWithoutAuth .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData({ enabled: false })) + .send(getTestRuleData({ enabled: false })) .expect(200); objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); @@ -72,7 +72,7 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex const { body: createdAlert } = await supertestWithoutAuth .post(`${getUrlPrefix(Spaces.other.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData({ enabled: false })) + .send(getTestRuleData({ enabled: false })) .expect(200); objectRemover.add(Spaces.other.id, createdAlert.id, 'rule', 'alerting'); @@ -88,7 +88,7 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex const { body: createdAlert } = await supertestWithoutAuth .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData({ enabled: false })) + .send(getTestRuleData({ enabled: false })) .expect(200); objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/ephemeral.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/ephemeral.ts index a3b8c75f79e621..ac095fd4c44190 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/ephemeral.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/ephemeral.ts @@ -11,7 +11,7 @@ import { Spaces } from '../../scenarios'; import { getUrlPrefix, ObjectRemover, - getTestAlertData, + getTestRuleData, getEventLog, ESTestIndexTool, ES_TEST_INDEX_NAME, @@ -69,7 +69,7 @@ export default function createNotifyWhenTests({ getService }: FtrProviderContext const pattern = { instance: [true, true, true, false, true, true], }; - const alertData = getTestAlertData({ + const alertData = getTestRuleData({ rule_type_id: 'test.patternFiring', params: { pattern }, schedule: { interval: '1m' }, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts index c36f9a0da75bcd..2cc2044653fd9c 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts @@ -10,7 +10,7 @@ import uuid from 'uuid'; import { Spaces } from '../../scenarios'; import { getUrlPrefix, - getTestAlertData, + getTestRuleData, ObjectRemover, getEventLog, ESTestIndexTool, @@ -62,7 +62,7 @@ export default function eventLogTests({ getService }: FtrProviderContext) { .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.patternFiring', schedule: { interval: '1s' }, throttle: null, @@ -300,7 +300,7 @@ export default function eventLogTests({ getService }: FtrProviderContext) { .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.multipleSearches', schedule: { interval: '1s' }, throttle: null, @@ -405,7 +405,7 @@ export default function eventLogTests({ getService }: FtrProviderContext) { .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.patternFiring', schedule: { interval: '1s' }, throttle: null, @@ -599,7 +599,7 @@ export default function eventLogTests({ getService }: FtrProviderContext) { .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.throw', schedule: { interval: '1s' }, throttle: null, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log_alerts.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log_alerts.ts index 6fa3eb1a43b620..6d7f95df10d9b2 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log_alerts.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log_alerts.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import { Spaces } from '../../scenarios'; -import { getUrlPrefix, getTestAlertData, ObjectRemover, getEventLog } from '../../../common/lib'; +import { getUrlPrefix, getTestRuleData, ObjectRemover, getEventLog } from '../../../common/lib'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; import { IValidatedEvent } from '../../../../../plugins/event_log/server'; @@ -31,7 +31,7 @@ export default function eventLogAlertTests({ getService }: FtrProviderContext) { .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.patternFiring', schedule: { interval: '1s' }, throttle: null, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/execution_status.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/execution_status.ts index 0f7ed80cfd38df..d5bcd0c7a9ae2b 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/execution_status.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/execution_status.ts @@ -10,7 +10,7 @@ import { Spaces } from '../../scenarios'; import { checkAAD, getUrlPrefix, - getTestAlertData, + getTestRuleData, ObjectRemover, ensureDatetimesAreOrdered, } from '../../../common/lib'; @@ -30,7 +30,7 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon const response = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()); + .send(getTestRuleData()); const dateEnd = Date.now(); expect(response.status).to.eql(200); objectRemover.add(Spaces.space1.id, response.body.id, 'rule', 'alerting'); @@ -61,7 +61,7 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.noop', schedule: { interval: '1s' }, }) @@ -94,7 +94,7 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.patternFiring', schedule: { interval: '1s' }, params: { @@ -130,7 +130,7 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.throw', schedule: { interval: '1s' }, }) @@ -166,7 +166,7 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.throw', schedule: { interval: '1s' }, }) @@ -188,7 +188,7 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.validation', schedule: { interval: '1s' }, params: { param1: 'valid now, but will change to a number soon!' }, @@ -227,7 +227,7 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.throw', schedule: { interval: '1s' }, }) diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts index 7a4a91bd575bb1..5ab632c6a66b86 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; import { SuperTest, Test } from 'supertest'; import { Spaces } from '../../scenarios'; -import { getUrlPrefix, getTestAlertData, ObjectRemover } from '../../../common/lib'; +import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../common/lib'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; async function createAlert( @@ -19,7 +19,7 @@ async function createAlert( const { body: createdAlert } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData(overwrites)) + .send(getTestRuleData(overwrites)) .expect(200); objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); return createdAlert; @@ -36,7 +36,7 @@ const findTestUtils = ( const { body: createdAlert } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); @@ -82,7 +82,7 @@ const findTestUtils = ( const { body: createdAlert } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); @@ -266,7 +266,7 @@ export default function createFindTests({ getService }: FtrProviderContext) { const { body: createdAlert } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get.ts index 9a4be8951f8f00..81f67c8d49e332 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; import { SuperTest, Test } from 'supertest'; import { Spaces } from '../../scenarios'; -import { getUrlPrefix, getTestAlertData, ObjectRemover } from '../../../common/lib'; +import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../common/lib'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; const getTestUtils = ( @@ -22,7 +22,7 @@ const getTestUtils = ( const { body: createdAlert } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); @@ -64,7 +64,7 @@ const getTestUtils = ( const { body: createdAlert } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); @@ -113,7 +113,7 @@ export default function createGetTests({ getService }: FtrProviderContext) { const { body: createdAlert } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_state.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_state.ts index 318dfdfe065dfe..61d38b522bb59d 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_state.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_state.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import { Spaces } from '../../scenarios'; -import { getUrlPrefix, ObjectRemover, getTestAlertData } from '../../../common/lib'; +import { getUrlPrefix, ObjectRemover, getTestRuleData } from '../../../common/lib'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export @@ -24,7 +24,7 @@ export default function createGetAlertStateTests({ getService }: FtrProviderCont const { body: createdAlert } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_summary.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_summary.ts index 8cd9a0bbb12900..d13da4694bbe27 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_summary.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_summary.ts @@ -12,7 +12,7 @@ import { Spaces } from '../../scenarios'; import { getUrlPrefix, ObjectRemover, - getTestAlertData, + getTestRuleData, AlertUtils, getEventLog, } from '../../../common/lib'; @@ -44,7 +44,7 @@ export default function createGetAlertSummaryTests({ getService }: FtrProviderCo const { body: createdRule } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add(Spaces.space1.id, createdRule.id, 'rule', 'alerting'); @@ -83,7 +83,7 @@ export default function createGetAlertSummaryTests({ getService }: FtrProviderCo const { body: createdRule } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add(Spaces.space1.id, createdRule.id, 'rule', 'alerting'); @@ -122,7 +122,7 @@ export default function createGetAlertSummaryTests({ getService }: FtrProviderCo const { body: createdRule } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add(Spaces.space1.id, createdRule.id, 'rule', 'alerting'); @@ -142,7 +142,7 @@ export default function createGetAlertSummaryTests({ getService }: FtrProviderCo const { body: createdRule } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add(Spaces.space1.id, createdRule.id, 'rule', 'alerting'); @@ -165,7 +165,7 @@ export default function createGetAlertSummaryTests({ getService }: FtrProviderCo const { body: createdRule } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add(Spaces.space1.id, createdRule.id, 'rule', 'alerting'); @@ -189,7 +189,7 @@ export default function createGetAlertSummaryTests({ getService }: FtrProviderCo const { body: createdRule } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData({ rule_type_id: 'test.throw' })) + .send(getTestRuleData({ rule_type_id: 'test.throw' })) .expect(200); objectRemover.add(Spaces.space1.id, createdRule.id, 'rule', 'alerting'); @@ -216,7 +216,7 @@ export default function createGetAlertSummaryTests({ getService }: FtrProviderCo .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.patternFiring', params: { pattern }, schedule: { interval: '1s' }, @@ -271,7 +271,7 @@ export default function createGetAlertSummaryTests({ getService }: FtrProviderCo .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.patternFiring', params: { pattern }, schedule: { interval: '1s' }, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/monitoring.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/monitoring.ts index 9b91d395d16c6f..c08a28b3c3ca30 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/monitoring.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/monitoring.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import { Spaces } from '../../scenarios'; -import { getUrlPrefix, getTestAlertData, ObjectRemover } from '../../../common/lib'; +import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../common/lib'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export @@ -23,7 +23,7 @@ export default function monitoringAlertTests({ getService }: FtrProviderContext) const createResponse = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData({ schedule: { interval: '3s' } })); + .send(getTestRuleData({ schedule: { interval: '3s' } })); expect(createResponse.status).to.eql(200); objectRemover.add(Spaces.space1.id, createResponse.body.id, 'rule', 'alerting'); @@ -44,7 +44,7 @@ export default function monitoringAlertTests({ getService }: FtrProviderContext) const createResponse = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData({ schedule: { interval: '3s' } })); + .send(getTestRuleData({ schedule: { interval: '3s' } })); expect(createResponse.status).to.eql(200); objectRemover.add(Spaces.space1.id, createResponse.body.id, 'rule', 'alerting'); @@ -69,7 +69,7 @@ export default function monitoringAlertTests({ getService }: FtrProviderContext) .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.patternSuccessOrFailure', schedule: { interval: '3s' }, params: { @@ -102,7 +102,7 @@ export default function monitoringAlertTests({ getService }: FtrProviderContext) .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ schedule: { interval: '3s' }, }) ); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/mustache_templates.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/mustache_templates.ts index 8d300733bafc37..ff596db062b75d 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/mustache_templates.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/mustache_templates.ts @@ -19,7 +19,7 @@ import axios from 'axios'; import expect from '@kbn/expect'; import { Spaces } from '../../scenarios'; -import { getUrlPrefix, getTestAlertData, ObjectRemover } from '../../../common/lib'; +import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../common/lib'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; import { getWebhookServer, @@ -88,7 +88,7 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ name: 'testing variable escapes for webhook', rule_type_id: 'test.patternFiring', params: { @@ -139,7 +139,7 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ name: 'testing variable escapes for slack', rule_type_id: 'test.patternFiring', params: { @@ -189,7 +189,7 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ name: 'testing context variable expansion', rule_type_id: 'test.patternFiring', params: { @@ -239,7 +239,7 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ name: 'testing context variable kibanaBaseUrl', rule_type_id: 'test.patternFiring', params: { @@ -290,7 +290,7 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ name: 'testing variable escapes for webhook', rule_type_id: 'test.patternFiring', params: { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/mute_all.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/mute_all.ts index c21a13edbf2cb3..27475049ac9a6f 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/mute_all.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/mute_all.ts @@ -12,7 +12,7 @@ import { AlertUtils, checkAAD, getUrlPrefix, - getTestAlertData, + getTestRuleData, ObjectRemover, } from '../../../common/lib'; @@ -30,7 +30,7 @@ export default function createMuteTests({ getService }: FtrProviderContext) { const { body: createdAlert } = await supertestWithoutAuth .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData({ enabled: false })) + .send(getTestRuleData({ enabled: false })) .expect(200); objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); @@ -56,7 +56,7 @@ export default function createMuteTests({ getService }: FtrProviderContext) { const { body: createdAlert } = await supertestWithoutAuth .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData({ enabled: false })) + .send(getTestRuleData({ enabled: false })) .expect(200); objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/mute_instance.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/mute_instance.ts index afe29280748a5a..d32b74fd394475 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/mute_instance.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/mute_instance.ts @@ -12,7 +12,7 @@ import { AlertUtils, checkAAD, getUrlPrefix, - getTestAlertData, + getTestRuleData, ObjectRemover, } from '../../../common/lib'; @@ -30,7 +30,7 @@ export default function createMuteInstanceTests({ getService }: FtrProviderConte const { body: createdAlert } = await supertestWithoutAuth .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData({ enabled: false })) + .send(getTestRuleData({ enabled: false })) .expect(200); objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); @@ -56,7 +56,7 @@ export default function createMuteInstanceTests({ getService }: FtrProviderConte const { body: createdAlert } = await supertestWithoutAuth .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData({ enabled: false })) + .send(getTestRuleData({ enabled: false })) .expect(200); objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/notify_when.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/notify_when.ts index 7f1b82614a1000..5049f7c863a06a 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/notify_when.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/notify_when.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; import { Spaces } from '../../scenarios'; -import { getUrlPrefix, ObjectRemover, getTestAlertData, getEventLog } from '../../../common/lib'; +import { getUrlPrefix, ObjectRemover, getTestRuleData, getEventLog } from '../../../common/lib'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; import { IValidatedEvent } from '../../../../../plugins/event_log/server'; @@ -55,7 +55,7 @@ export default function createNotifyWhenTests({ getService }: FtrProviderContext .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.patternFiring', params: { pattern }, schedule: { interval: '1s' }, @@ -131,7 +131,7 @@ export default function createNotifyWhenTests({ getService }: FtrProviderContext .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.patternFiring', params: { pattern }, schedule: { interval: '1s' }, @@ -222,7 +222,7 @@ export default function createNotifyWhenTests({ getService }: FtrProviderContext .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( - getTestAlertData({ + getTestRuleData({ rule_type_id: 'test.patternFiring', params: { pattern }, schedule: { interval: '1s' }, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/scheduled_task_id.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/scheduled_task_id.ts index 9f087b73921325..a83cd4241d1446 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/scheduled_task_id.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/scheduled_task_id.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { getUrlPrefix, TaskManagerDoc, ObjectRemover, getTestAlertData } from '../../../common/lib'; +import { getUrlPrefix, TaskManagerDoc, ObjectRemover, getTestRuleData } from '../../../common/lib'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; const MIGRATED_RULE_ID = '74f3e6d7-b7bb-477d-ac28-92ee22728e6e'; @@ -47,7 +47,7 @@ export default function createScheduledTaskIdTests({ getService }: FtrProviderCo await supertest .post(`${getUrlPrefix(``)}/api/alerting/rule/${MIGRATED_TASK_ID}`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(409); }); @@ -94,7 +94,7 @@ export default function createScheduledTaskIdTests({ getService }: FtrProviderCo const response = await supertestWithoutAuth .post(`${getUrlPrefix(``)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()); + .send(getTestRuleData()); expect(response.status).to.eql(200); objectRemover.add('default', response.body.id, 'rule', 'alerting'); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/unmute_all.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/unmute_all.ts index 2fffa9189e0ad7..47f61250157a37 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/unmute_all.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/unmute_all.ts @@ -12,7 +12,7 @@ import { AlertUtils, checkAAD, getUrlPrefix, - getTestAlertData, + getTestRuleData, ObjectRemover, } from '../../../common/lib'; @@ -30,7 +30,7 @@ export default function createUnmuteTests({ getService }: FtrProviderContext) { const { body: createdAlert } = await supertestWithoutAuth .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData({ enabled: false })) + .send(getTestRuleData({ enabled: false })) .expect(200); objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); @@ -57,7 +57,7 @@ export default function createUnmuteTests({ getService }: FtrProviderContext) { const { body: createdAlert } = await supertestWithoutAuth .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData({ enabled: false })) + .send(getTestRuleData({ enabled: false })) .expect(200); objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/unmute_instance.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/unmute_instance.ts index e0c42136628d3d..086f40d9febae1 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/unmute_instance.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/unmute_instance.ts @@ -12,7 +12,7 @@ import { AlertUtils, checkAAD, getUrlPrefix, - getTestAlertData, + getTestRuleData, ObjectRemover, } from '../../../common/lib'; @@ -30,7 +30,7 @@ export default function createUnmuteInstanceTests({ getService }: FtrProviderCon const { body: createdAlert } = await supertestWithoutAuth .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData({ enabled: false })) + .send(getTestRuleData({ enabled: false })) .expect(200); objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); @@ -57,7 +57,7 @@ export default function createUnmuteInstanceTests({ getService }: FtrProviderCon const { body: createdAlert } = await supertestWithoutAuth .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData({ enabled: false })) + .send(getTestRuleData({ enabled: false })) .expect(200); objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update.ts index d97ca18c52d4a8..c5a9c93d45e81a 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import { Spaces } from '../../scenarios'; -import { checkAAD, getUrlPrefix, getTestAlertData, ObjectRemover } from '../../../common/lib'; +import { checkAAD, getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../common/lib'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export @@ -23,7 +23,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { const { body: createdAlert } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); @@ -94,7 +94,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { const { body: createdAlert } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); @@ -124,7 +124,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { const { body: createdAlert } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update_api_key.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update_api_key.ts index 78ceadec44a9ae..9fe5c7e112c79d 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update_api_key.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update_api_key.ts @@ -12,7 +12,7 @@ import { AlertUtils, checkAAD, getUrlPrefix, - getTestAlertData, + getTestRuleData, ObjectRemover, } from '../../../common/lib'; @@ -34,7 +34,7 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte const { body: createdAlert } = await supertestWithoutAuth .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); @@ -59,7 +59,7 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte const { body: createdAlert } = await supertestWithoutAuth .post(`${getUrlPrefix(Spaces.other.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add(Spaces.other.id, createdAlert.id, 'rule', 'alerting'); @@ -75,7 +75,7 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte const { body: createdAlert } = await supertestWithoutAuth .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send(getTestRuleData()) .expect(200); objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); diff --git a/x-pack/test/api_integration/apis/telemetry/telemetry.ts b/x-pack/test/api_integration/apis/telemetry/telemetry.ts index 088678a74813b6..4b0137ab5f8427 100644 --- a/x-pack/test/api_integration/apis/telemetry/telemetry.ts +++ b/x-pack/test/api_integration/apis/telemetry/telemetry.ts @@ -10,6 +10,7 @@ import moment from 'moment'; import type SuperTest from 'supertest'; import deepmerge from 'deepmerge'; import type { FtrProviderContext } from '../../ftr_provider_context'; +import type { SecurityService } from '../../../../../test/common/services/security/security'; import multiClusterFixture from './fixtures/multicluster.json'; import basicClusterFixture from './fixtures/basiccluster.json'; @@ -90,10 +91,31 @@ function updateMonitoringDates( ]); } +async function createUserWithRole( + security: SecurityService, + userName: string, + roleName: string, + role: unknown +) { + await security.role.create(roleName, role); + + await security.user.create(userName, { + password: password(userName), + roles: [roleName], + full_name: `User ${userName}`, + }); +} + +function password(userName: string) { + return `${userName}-password`; +} + export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); // We need this because `.auth` in the already authed one does not work as expected const esArchiver = getService('esArchiver'); const esSupertest = getService('esSupertest'); + const security = getService('security'); describe('/api/telemetry/v2/clusters/_stats', () => { const timestamp = new Date().toISOString(); @@ -236,5 +258,114 @@ export default function ({ getService }: FtrProviderContext) { expect(new Date(fetchedAt).getTime()).to.be.greaterThan(now); }); }); + + describe('Only global read+ users can fetch unencrypted telemetry', () => { + describe('superadmin user', () => { + it('should return unencrypted telemetry for the admin user', async () => { + await supertest + .post('/api/telemetry/v2/clusters/_stats') + .set('kbn-xsrf', 'xxx') + .send({ unencrypted: true }) + .expect(200); + }); + + it('should return encrypted telemetry for the admin user', async () => { + await supertest + .post('/api/telemetry/v2/clusters/_stats') + .set('kbn-xsrf', 'xxx') + .send({ unencrypted: false }) + .expect(200); + }); + }); + + describe('global-read user', () => { + const globalReadOnlyUser = 'telemetry-global-read-only-user'; + const globalReadOnlyRole = 'telemetry-global-read-only-role'; + + before('create user', async () => { + await createUserWithRole(security, globalReadOnlyUser, globalReadOnlyRole, { + kibana: [ + { + spaces: ['*'], + base: ['read'], + feature: {}, + }, + ], + }); + }); + + after(async () => { + await security.user.delete(globalReadOnlyUser); + await security.role.delete(globalReadOnlyRole); + }); + + it('should return encrypted telemetry for the global-read user', async () => { + await supertestWithoutAuth + .post('/api/telemetry/v2/clusters/_stats') + .auth(globalReadOnlyUser, password(globalReadOnlyUser)) + .set('kbn-xsrf', 'xxx') + .send({ unencrypted: false }) + .expect(200); + }); + + it('should return unencrypted telemetry for the global-read user', async () => { + await supertestWithoutAuth + .post('/api/telemetry/v2/clusters/_stats') + .auth(globalReadOnlyUser, password(globalReadOnlyUser)) + .set('kbn-xsrf', 'xxx') + .send({ unencrypted: true }) + .expect(200); + }); + }); + + describe('non global-read user', () => { + const noGlobalUser = 'telemetry-no-global-user'; + const noGlobalRole = 'telemetry-no-global-role'; + + before('create user', async () => { + await createUserWithRole(security, noGlobalUser, noGlobalRole, { + kibana: [ + { + spaces: ['*'], + base: [], + feature: { + // It has access to many features specified individually but not a global one + discover: ['all'], + dashboard: ['all'], + canvas: ['all'], + maps: ['all'], + ml: ['all'], + visualize: ['all'], + dev_tools: ['all'], + }, + }, + ], + }); + }); + + after(async () => { + await security.user.delete(noGlobalUser); + await security.role.delete(noGlobalRole); + }); + + it('should return encrypted telemetry for the read-only user', async () => { + await supertestWithoutAuth + .post('/api/telemetry/v2/clusters/_stats') + .auth(noGlobalUser, password(noGlobalUser)) + .set('kbn-xsrf', 'xxx') + .send({ unencrypted: false }) + .expect(200); + }); + + it('should return 403 when the read-only user requests unencrypted telemetry', async () => { + await supertestWithoutAuth + .post('/api/telemetry/v2/clusters/_stats') + .auth(noGlobalUser, password(noGlobalUser)) + .set('kbn-xsrf', 'xxx') + .send({ unencrypted: true }) + .expect(403); + }); + }); + }); }); } diff --git a/x-pack/test/functional/services/transform/edit_flyout.ts b/x-pack/test/functional/services/transform/edit_flyout.ts index fb1d77f7abc6c1..0198a1d3d8c366 100644 --- a/x-pack/test/functional/services/transform/edit_flyout.ts +++ b/x-pack/test/functional/services/transform/edit_flyout.ts @@ -45,28 +45,32 @@ export function TransformEditFlyoutProvider({ getService }: FtrProviderContext) await testSubjects.existOrFail(`transformEditRetentionPolicySwitch`, { timeout: 1000, }); - const isEnabled = await testSubjects.isEnabled(`transformEditRetentionPolicySwitch`); - expect(isEnabled).to.eql( - expectedValue, - `Expected 'transformEditRetentionPolicySwitch' input to be '${ - expectedValue ? 'enabled' : 'disabled' - }' (got '${isEnabled ? 'enabled' : 'disabled'}')` - ); + await retry.tryForTime(5000, async () => { + const isEnabled = await testSubjects.isEnabled(`transformEditRetentionPolicySwitch`); + expect(isEnabled).to.eql( + expectedValue, + `Expected 'transformEditRetentionPolicySwitch' input to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + }); }, async assertTransformEditFlyoutRetentionPolicyFieldSelectEnabled(expectedValue: boolean) { await testSubjects.existOrFail(`transformEditFlyoutRetentionPolicyFieldSelect`, { timeout: 1000, }); - const isEnabled = await testSubjects.isEnabled( - `transformEditFlyoutRetentionPolicyFieldSelect` - ); - expect(isEnabled).to.eql( - expectedValue, - `Expected 'transformEditFlyoutRetentionPolicyFieldSelect' input to be '${ - expectedValue ? 'enabled' : 'disabled' - }' (got '${isEnabled ? 'enabled' : 'disabled'}')` - ); + await retry.tryForTime(5000, async () => { + const isEnabled = await testSubjects.isEnabled( + `transformEditFlyoutRetentionPolicyFieldSelect` + ); + expect(isEnabled).to.eql( + expectedValue, + `Expected 'transformEditFlyoutRetentionPolicyFieldSelect' input to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + }); }, async assertTransformEditFlyoutRetentionPolicyFieldSelectValue(expectedValue: string) { diff --git a/x-pack/test/security_solution_cypress/es_archives/risky_hosts/data.json b/x-pack/test/security_solution_cypress/es_archives/risky_hosts/data.json index cde819a836b0a3..3e468d7a84ca26 100644 --- a/x-pack/test/security_solution_cypress/es_archives/risky_hosts/data.json +++ b/x-pack/test/security_solution_cypress/es_archives/risky_hosts/data.json @@ -13,7 +13,7 @@ "rule_risk": 42 } ] - }, + }, "host":{ "name":"siem-kibana" }, @@ -23,6 +23,131 @@ } } +{ + "type":"doc", + "value":{ + "id":"a2cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb71f", + "index":"ml_host_risk_score_latest_default", + "source":{ + "@timestamp":"2021-03-10T14:51:05.766Z", + "risk_stats": { + "risk_score": 50, + "rule_risks": [ + { + "rule_name": "Unusual Linux Username", + "rule_risk": 42 + } + ] + }, + "host":{ + "name":"fake-1" + }, + "ingest_timestamp":"2021-03-09T18:02:08.319296053Z", + "risk":"Moderate" + } + } +} + +{ + "type":"doc", + "value":{ + "id":"a2cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb72f", + "index":"ml_host_risk_score_latest_default", + "source":{ + "@timestamp":"2021-03-10T14:51:05.766Z", + "risk_stats": { + "risk_score": 50, + "rule_risks": [ + { + "rule_name": "Unusual Linux Username", + "rule_risk": 42 + } + ] + }, + "host":{ + "name":"fake-2" + }, + "ingest_timestamp":"2021-03-09T18:02:08.319296053Z", + "risk":"Moderate" + } + } +} + +{ + "type":"doc", + "value":{ + "id":"a2cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb73f", + "index":"ml_host_risk_score_latest_default", + "source":{ + "@timestamp":"2021-03-10T14:51:05.766Z", + "risk_stats": { + "risk_score": 50, + "rule_risks": [ + { + "rule_name": "Unusual Linux Username", + "rule_risk": 42 + } + ] + }, + "host":{ + "name":"fake-3" + }, + "ingest_timestamp":"2021-03-09T18:02:08.319296053Z", + "risk":"Moderate" + } + } +} + +{ + "type":"doc", + "value":{ + "id":"a2cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb74f", + "index":"ml_host_risk_score_latest_default", + "source":{ + "@timestamp":"2021-03-10T14:51:05.766Z", + "risk_stats": { + "risk_score": 50, + "rule_risks": [ + { + "rule_name": "Unusual Linux Username", + "rule_risk": 42 + } + ] + }, + "host":{ + "name":"fake-4" + }, + "ingest_timestamp":"2021-03-09T18:02:08.319296053Z", + "risk":"Moderate" + } + } +} + +{ + "type":"doc", + "value":{ + "id":"a2cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb75f", + "index":"ml_host_risk_score_latest_default", + "source":{ + "@timestamp":"2021-03-10T14:51:05.766Z", + "risk_stats": { + "risk_score": 50, + "rule_risks": [ + { + "rule_name": "Unusual Linux Username", + "rule_risk": 42 + } + ] + }, + "host":{ + "name":"fake-5" + }, + "ingest_timestamp":"2021-03-09T18:02:08.319296053Z", + "risk":"Moderate" + } + } +} + { "type":"doc", "value":{ @@ -38,7 +163,7 @@ "rule_risk": 42 } ] - }, + }, "host":{ "name":"siem-kibana" },