From 69cdc32266779a3b58aefa2cede7e0b59d2787b6 Mon Sep 17 00:00:00 2001 From: Liza K Date: Tue, 16 Feb 2021 15:23:20 +0200 Subject: [PATCH] Report the value suggestions funnel --- .../autocomplete/autocomplete_service.ts | 14 ++++- .../collectors/create_usage_collector.ts | 57 +++++++++++++++++++ .../public/autocomplete/collectors/index.ts | 10 ++++ .../public/autocomplete/collectors/types.ts | 21 +++++++ .../providers/value_suggestion_provider.ts | 49 +++++++++++----- src/plugins/data/public/plugin.ts | 5 +- 6 files changed, 139 insertions(+), 17 deletions(-) create mode 100644 src/plugins/data/public/autocomplete/collectors/create_usage_collector.ts create mode 100644 src/plugins/data/public/autocomplete/collectors/index.ts create mode 100644 src/plugins/data/public/autocomplete/collectors/types.ts diff --git a/src/plugins/data/public/autocomplete/autocomplete_service.ts b/src/plugins/data/public/autocomplete/autocomplete_service.ts index a99943c6cd878..6b288c4507f06 100644 --- a/src/plugins/data/public/autocomplete/autocomplete_service.ts +++ b/src/plugins/data/public/autocomplete/autocomplete_service.ts @@ -16,6 +16,8 @@ import { } from './providers/value_suggestion_provider'; import { ConfigSchema } from '../../config'; +import { UsageCollectionSetup } from '../../../usage_collection/public'; +import { createUsageCollector } from './collectors'; export class AutocompleteService { autocompleteConfig: ConfigSchema['autocomplete']; @@ -47,9 +49,17 @@ export class AutocompleteService { private hasQuerySuggestions = (language: string) => this.querySuggestionProviders.has(language); /** @public **/ - public setup(core: CoreSetup, { timefilter }: { timefilter: TimefilterSetup }) { + public setup( + core: CoreSetup, + { + timefilter, + usageCollection, + }: { timefilter: TimefilterSetup; usageCollection?: UsageCollectionSetup } + ) { + const usageCollector = createUsageCollector(core.getStartServices, usageCollection); + this.getValueSuggestions = this.autocompleteConfig.valueSuggestions.enabled - ? setupValueSuggestionProvider(core, { timefilter }) + ? setupValueSuggestionProvider(core, { timefilter, usageCollector }) : getEmptyValueSuggestions; return { diff --git a/src/plugins/data/public/autocomplete/collectors/create_usage_collector.ts b/src/plugins/data/public/autocomplete/collectors/create_usage_collector.ts new file mode 100644 index 0000000000000..fc0cea2fdbc52 --- /dev/null +++ b/src/plugins/data/public/autocomplete/collectors/create_usage_collector.ts @@ -0,0 +1,57 @@ +/* + * 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 { first } from 'rxjs/operators'; +import { StartServicesAccessor } from '../../../../../core/public'; +import { METRIC_TYPE, UsageCollectionSetup } from '../../../../usage_collection/public'; +import { AUTOCOMPLETE_EVENT_TYPE, AutocompleteUsageCollector } from './types'; + +export const createUsageCollector = ( + getStartServices: StartServicesAccessor, + usageCollection?: UsageCollectionSetup +): AutocompleteUsageCollector => { + const getCurrentApp = async () => { + const [{ application }] = await getStartServices(); + return application.currentAppId$.pipe(first()).toPromise(); + }; + + return { + trackCall: async () => { + const currentApp = await getCurrentApp(); + return usageCollection?.reportUiCounter( + currentApp!, + METRIC_TYPE.LOADED, + AUTOCOMPLETE_EVENT_TYPE.CALL + ); + }, + trackRequest: async () => { + const currentApp = await getCurrentApp(); + return usageCollection?.reportUiCounter( + currentApp!, + METRIC_TYPE.LOADED, + AUTOCOMPLETE_EVENT_TYPE.REQUEST + ); + }, + trackResult: async () => { + const currentApp = await getCurrentApp(); + return usageCollection?.reportUiCounter( + currentApp!, + METRIC_TYPE.LOADED, + AUTOCOMPLETE_EVENT_TYPE.RESULT + ); + }, + trackError: async () => { + const currentApp = await getCurrentApp(); + return usageCollection?.reportUiCounter( + currentApp!, + METRIC_TYPE.LOADED, + AUTOCOMPLETE_EVENT_TYPE.ERROR + ); + }, + }; +}; diff --git a/src/plugins/data/public/autocomplete/collectors/index.ts b/src/plugins/data/public/autocomplete/collectors/index.ts new file mode 100644 index 0000000000000..5cfaab19787da --- /dev/null +++ b/src/plugins/data/public/autocomplete/collectors/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 { createUsageCollector } from './create_usage_collector'; +export { AUTOCOMPLETE_EVENT_TYPE, AutocompleteUsageCollector } from './types'; diff --git a/src/plugins/data/public/autocomplete/collectors/types.ts b/src/plugins/data/public/autocomplete/collectors/types.ts new file mode 100644 index 0000000000000..582c7a5485e35 --- /dev/null +++ b/src/plugins/data/public/autocomplete/collectors/types.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 enum AUTOCOMPLETE_EVENT_TYPE { + CALL = 'call', + REQUEST = 'req', + RESULT = 'res', + ERROR = 'err', +} + +export interface AutocompleteUsageCollector { + trackCall: () => Promise; + trackRequest: () => Promise; + trackResult: () => Promise; + trackError: () => Promise; +} diff --git a/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.ts b/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.ts index d8c6d16174d14..032861070c708 100644 --- a/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.ts +++ b/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.ts @@ -11,12 +11,7 @@ import { memoize } from 'lodash'; import { CoreSetup } from 'src/core/public'; import { IIndexPattern, IFieldType, UI_SETTINGS, buildQueryFromFilters } from '../../../common'; import { TimefilterSetup } from '../../query'; - -function resolver(title: string, field: IFieldType, query: string, filters: any[]) { - // Only cache results for a minute - const ttl = Math.floor(Date.now() / 1000 / 60); - return [ttl, query, title, field.name, JSON.stringify(filters)].join('|'); -} +import { AutocompleteUsageCollector } from '../collectors'; export type ValueSuggestionsGetFn = (args: ValueSuggestionsGetFnArgs) => Promise; @@ -47,15 +42,32 @@ export const getEmptyValueSuggestions = (() => Promise.resolve([])) as ValueSugg export const setupValueSuggestionProvider = ( core: CoreSetup, - { timefilter }: { timefilter: TimefilterSetup } + { + timefilter, + usageCollector, + }: { timefilter: TimefilterSetup; usageCollector?: AutocompleteUsageCollector } ): ValueSuggestionsGetFn => { + function resolver(title: string, field: IFieldType, query: string, filters: any[]) { + usageCollector?.trackCall(); + // Only cache results for a minute + const ttl = Math.floor(Date.now() / 1000 / 60); + return [ttl, query, title, field.name, JSON.stringify(filters)].join('|'); + } + const requestSuggestions = memoize( - (index: string, field: IFieldType, query: string, filters: any = [], signal?: AbortSignal) => - core.http.fetch(`/api/kibana/suggestions/values/${index}`, { - method: 'POST', - body: JSON.stringify({ query, field: field.name, filters }), - signal, - }), + (index: string, field: IFieldType, query: string, filters: any = [], signal?: AbortSignal) => { + usageCollector?.trackRequest(); + return core.http + .fetch(`/api/kibana/suggestions/values/${index}`, { + method: 'POST', + body: JSON.stringify({ query, field: field.name, filters }), + signal, + }) + .then((r) => { + usageCollector?.trackResult(); + return r; + }); + }, resolver ); @@ -85,6 +97,15 @@ export const setupValueSuggestionProvider = ( : undefined; const filterQuery = timeFilter ? buildQueryFromFilters([timeFilter], indexPattern).filter : []; const filters = [...(boolFilter ? boolFilter : []), ...filterQuery]; - return await requestSuggestions(title, field, query, filters, signal); + try { + return await requestSuggestions(title, field, query, filters, signal); + } catch (e) { + if (!signal?.aborted) { + usageCollector?.trackError(); + } + // Remove rejected results from memoize cache + requestSuggestions.cache.delete(resolver(title, field, query, filters)); + return []; + } }; }; diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index e55e42e5fdeff..862dd63948a22 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -115,7 +115,10 @@ export class DataPublicPlugin ); return { - autocomplete: this.autocomplete.setup(core, { timefilter: queryService.timefilter }), + autocomplete: this.autocomplete.setup(core, { + timefilter: queryService.timefilter, + usageCollection, + }), search: searchService, fieldFormats: this.fieldFormatsService.setup(core), query: queryService,