Report telemetry for autocomplete (#91428)

* Report telemetry about autocomplete
* error cound
* position of selected suggestion
* length of query used to generate the suggestions

Also added a couple if handly telemetry events:
* query language switch
* filter bar bulk actions

* Fix ts

* Report the value suggestions funnel

* docs

* docs

* fix jest

* test

* code review

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Liza Katz 2021-02-17 14:41:15 +02:00 committed by GitHub
parent edc11d9680
commit 98bd8e0211
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 244 additions and 68 deletions

View file

@ -21,4 +21,5 @@ export interface IDataPluginServices extends Partial<CoreStart>
| [savedObjects](./kibana-plugin-plugins-data-public.idatapluginservices.savedobjects.md) | <code>CoreStart['savedObjects']</code> | |
| [storage](./kibana-plugin-plugins-data-public.idatapluginservices.storage.md) | <code>IStorageWrapper</code> | |
| [uiSettings](./kibana-plugin-plugins-data-public.idatapluginservices.uisettings.md) | <code>CoreStart['uiSettings']</code> | |
| [usageCollection](./kibana-plugin-plugins-data-public.idatapluginservices.usagecollection.md) | <code>UsageCollectionStart</code> | |

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [IDataPluginServices](./kibana-plugin-plugins-data-public.idatapluginservices.md) &gt; [usageCollection](./kibana-plugin-plugins-data-public.idatapluginservices.usagecollection.md)
## IDataPluginServices.usageCollection property
<b>Signature:</b>
```typescript
usageCollection?: UsageCollectionStart;
```

View file

@ -7,7 +7,7 @@
<b>Signature:</b>
```typescript
SearchBar: React.ComponentClass<Pick<Pick<SearchBarProps, "query" | "isLoading" | "indexPatterns" | "filters" | "dataTestSubj" | "intl" | "refreshInterval" | "screenTitle" | "onRefresh" | "onRefreshChange" | "showQueryInput" | "showDatePicker" | "showAutoRefreshOnly" | "dateRangeFrom" | "dateRangeTo" | "isRefreshPaused" | "customSubmitButton" | "timeHistory" | "indicateNoData" | "onFiltersUpdated" | "trackUiMetric" | "savedQuery" | "showSaveQuery" | "onClearSavedQuery" | "showQueryBar" | "showFilterBar" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated">, "query" | "isLoading" | "indexPatterns" | "filters" | "dataTestSubj" | "refreshInterval" | "screenTitle" | "onRefresh" | "onRefreshChange" | "showQueryInput" | "showDatePicker" | "showAutoRefreshOnly" | "dateRangeFrom" | "dateRangeTo" | "isRefreshPaused" | "customSubmitButton" | "timeHistory" | "indicateNoData" | "onFiltersUpdated" | "trackUiMetric" | "savedQuery" | "showSaveQuery" | "onClearSavedQuery" | "showQueryBar" | "showFilterBar" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated">, any> & {
WrappedComponent: React.ComponentType<Pick<SearchBarProps, "query" | "isLoading" | "indexPatterns" | "filters" | "dataTestSubj" | "intl" | "refreshInterval" | "screenTitle" | "onRefresh" | "onRefreshChange" | "showQueryInput" | "showDatePicker" | "showAutoRefreshOnly" | "dateRangeFrom" | "dateRangeTo" | "isRefreshPaused" | "customSubmitButton" | "timeHistory" | "indicateNoData" | "onFiltersUpdated" | "trackUiMetric" | "savedQuery" | "showSaveQuery" | "onClearSavedQuery" | "showQueryBar" | "showFilterBar" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated"> & ReactIntl.InjectedIntlProps>;
SearchBar: React.ComponentClass<Pick<Pick<SearchBarProps, "query" | "isLoading" | "indexPatterns" | "filters" | "dataTestSubj" | "intl" | "refreshInterval" | "screenTitle" | "onRefresh" | "onRefreshChange" | "showQueryInput" | "showDatePicker" | "showAutoRefreshOnly" | "dateRangeFrom" | "dateRangeTo" | "isRefreshPaused" | "customSubmitButton" | "timeHistory" | "indicateNoData" | "onFiltersUpdated" | "savedQuery" | "showSaveQuery" | "onClearSavedQuery" | "showQueryBar" | "showFilterBar" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated">, "query" | "isLoading" | "indexPatterns" | "filters" | "dataTestSubj" | "refreshInterval" | "screenTitle" | "onRefresh" | "onRefreshChange" | "showQueryInput" | "showDatePicker" | "showAutoRefreshOnly" | "dateRangeFrom" | "dateRangeTo" | "isRefreshPaused" | "customSubmitButton" | "timeHistory" | "indicateNoData" | "onFiltersUpdated" | "savedQuery" | "showSaveQuery" | "onClearSavedQuery" | "showQueryBar" | "showFilterBar" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated">, any> & {
WrappedComponent: React.ComponentType<Pick<SearchBarProps, "query" | "isLoading" | "indexPatterns" | "filters" | "dataTestSubj" | "intl" | "refreshInterval" | "screenTitle" | "onRefresh" | "onRefreshChange" | "showQueryInput" | "showDatePicker" | "showAutoRefreshOnly" | "dateRangeFrom" | "dateRangeTo" | "isRefreshPaused" | "customSubmitButton" | "timeHistory" | "indicateNoData" | "onFiltersUpdated" | "savedQuery" | "showSaveQuery" | "onClearSavedQuery" | "showQueryBar" | "showFilterBar" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated"> & ReactIntl.InjectedIntlProps>;
}
```

View file

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

View file

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

View file

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

View file

@ -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 = 'autocomplete:call',
REQUEST = 'autocomplete:req',
RESULT = 'autocomplete:res',
ERROR = 'autocomplete:err',
}
export interface AutocompleteUsageCollector {
trackCall: () => Promise<void>;
trackRequest: () => Promise<void>;
trackResult: () => Promise<void>;
trackError: () => Promise<void>;
}

View file

@ -18,7 +18,7 @@ describe('FieldSuggestions', () => {
beforeEach(() => {
const uiSettings = { get: (key: string) => shouldSuggestValues } as IUiSettingsClient;
http = { fetch: jest.fn() };
http = { fetch: jest.fn().mockResolvedValue([]) };
getValueSuggestions = setupValueSuggestionProvider({ http, uiSettings } as CoreSetup, {
timefilter: ({

View file

@ -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<any[]>;
@ -47,15 +42,31 @@ 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[]) {
// 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 +96,16 @@ 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 {
usageCollector?.trackCall();
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 [];
}
};
};

View file

@ -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,
@ -195,10 +198,7 @@ export class DataPublicPlugin
core,
data: dataServices,
storage: this.storage,
trackUiMetric: this.usageCollection?.reportUiCounter.bind(
this.usageCollection,
'data_plugin'
),
usageCollection: this.usageCollection,
});
return {

View file

@ -105,7 +105,6 @@ import { Type } from '@kbn/config-schema';
import { TypeOf } from '@kbn/config-schema';
import { UiActionsSetup } from 'src/plugins/ui_actions/public';
import { UiActionsStart } from 'src/plugins/ui_actions/public';
import { UiCounterMetricType } from '@kbn/analytics';
import { Unit } from '@elastic/datemath';
import { UnregisterCallback } from 'history';
import { URL } from 'url';
@ -1094,6 +1093,10 @@ export interface IDataPluginServices extends Partial<CoreStart_2> {
storage: IStorageWrapper;
// (undocumented)
uiSettings: CoreStart_2['uiSettings'];
// Warning: (ae-forgotten-export) The symbol "UsageCollectionStart" needs to be exported by the entry point index.d.ts
//
// (undocumented)
usageCollection?: UsageCollectionStart;
}
// Warning: (ae-missing-release-tag) "IEsSearchRequest" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
@ -2250,8 +2253,8 @@ export const SEARCH_SESSIONS_MANAGEMENT_ID = "search_sessions";
// Warning: (ae-missing-release-tag) "SearchBar" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const SearchBar: React.ComponentClass<Pick<Pick<SearchBarProps, "query" | "isLoading" | "indexPatterns" | "filters" | "dataTestSubj" | "intl" | "refreshInterval" | "screenTitle" | "onRefresh" | "onRefreshChange" | "showQueryInput" | "showDatePicker" | "showAutoRefreshOnly" | "dateRangeFrom" | "dateRangeTo" | "isRefreshPaused" | "customSubmitButton" | "timeHistory" | "indicateNoData" | "onFiltersUpdated" | "trackUiMetric" | "savedQuery" | "showSaveQuery" | "onClearSavedQuery" | "showQueryBar" | "showFilterBar" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated">, "query" | "isLoading" | "indexPatterns" | "filters" | "dataTestSubj" | "refreshInterval" | "screenTitle" | "onRefresh" | "onRefreshChange" | "showQueryInput" | "showDatePicker" | "showAutoRefreshOnly" | "dateRangeFrom" | "dateRangeTo" | "isRefreshPaused" | "customSubmitButton" | "timeHistory" | "indicateNoData" | "onFiltersUpdated" | "trackUiMetric" | "savedQuery" | "showSaveQuery" | "onClearSavedQuery" | "showQueryBar" | "showFilterBar" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated">, any> & {
WrappedComponent: React.ComponentType<Pick<SearchBarProps, "query" | "isLoading" | "indexPatterns" | "filters" | "dataTestSubj" | "intl" | "refreshInterval" | "screenTitle" | "onRefresh" | "onRefreshChange" | "showQueryInput" | "showDatePicker" | "showAutoRefreshOnly" | "dateRangeFrom" | "dateRangeTo" | "isRefreshPaused" | "customSubmitButton" | "timeHistory" | "indicateNoData" | "onFiltersUpdated" | "trackUiMetric" | "savedQuery" | "showSaveQuery" | "onClearSavedQuery" | "showQueryBar" | "showFilterBar" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated"> & ReactIntl.InjectedIntlProps>;
export const SearchBar: React.ComponentClass<Pick<Pick<SearchBarProps, "query" | "isLoading" | "indexPatterns" | "filters" | "dataTestSubj" | "intl" | "refreshInterval" | "screenTitle" | "onRefresh" | "onRefreshChange" | "showQueryInput" | "showDatePicker" | "showAutoRefreshOnly" | "dateRangeFrom" | "dateRangeTo" | "isRefreshPaused" | "customSubmitButton" | "timeHistory" | "indicateNoData" | "onFiltersUpdated" | "savedQuery" | "showSaveQuery" | "onClearSavedQuery" | "showQueryBar" | "showFilterBar" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated">, "query" | "isLoading" | "indexPatterns" | "filters" | "dataTestSubj" | "refreshInterval" | "screenTitle" | "onRefresh" | "onRefreshChange" | "showQueryInput" | "showDatePicker" | "showAutoRefreshOnly" | "dateRangeFrom" | "dateRangeTo" | "isRefreshPaused" | "customSubmitButton" | "timeHistory" | "indicateNoData" | "onFiltersUpdated" | "savedQuery" | "showSaveQuery" | "onClearSavedQuery" | "showQueryBar" | "showFilterBar" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated">, any> & {
WrappedComponent: React.ComponentType<Pick<SearchBarProps, "query" | "isLoading" | "indexPatterns" | "filters" | "dataTestSubj" | "intl" | "refreshInterval" | "screenTitle" | "onRefresh" | "onRefreshChange" | "showQueryInput" | "showDatePicker" | "showAutoRefreshOnly" | "dateRangeFrom" | "dateRangeTo" | "isRefreshPaused" | "customSubmitButton" | "timeHistory" | "indicateNoData" | "onFiltersUpdated" | "savedQuery" | "showSaveQuery" | "onClearSavedQuery" | "showQueryBar" | "showFilterBar" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated"> & ReactIntl.InjectedIntlProps>;
};
// Warning: (ae-forgotten-export) The symbol "SearchBarOwnProps" needs to be exported by the entry point index.d.ts

View file

@ -19,7 +19,7 @@ import { ISearchSetup, ISearchStart, SearchEnhancements } from './search';
import { QuerySetup, QueryStart } from './query';
import { IndexPatternsContract } from './index_patterns';
import { IndexPatternSelectProps, StatefulSearchBarProps } from './ui';
import { UsageCollectionSetup } from '../../usage_collection/public';
import { UsageCollectionSetup, UsageCollectionStart } from '../../usage_collection/public';
import { Setup as InspectorSetup } from '../../inspector/public';
import { NowProviderPublicContract } from './now_provider';
@ -120,4 +120,5 @@ export interface IDataPluginServices extends Partial<CoreStart> {
http: CoreStart['http'];
storage: IStorageWrapper;
data: DataPublicPluginStart;
usageCollection?: UsageCollectionStart;
}

View file

@ -11,12 +11,12 @@ import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
import classNames from 'classnames';
import React, { useState } from 'react';
import { METRIC_TYPE, UiCounterMetricType } from '@kbn/analytics';
import { METRIC_TYPE } from '@kbn/analytics';
import { FilterEditor } from './filter_editor';
import { FILTER_EDITOR_WIDTH, FilterItem } from './filter_item';
import { FilterOptions } from './filter_options';
import { useKibana } from '../../../../kibana_react/public';
import { IIndexPattern } from '../..';
import { IDataPluginServices, IIndexPattern } from '../..';
import {
buildEmptyFilter,
Filter,
@ -36,17 +36,16 @@ interface Props {
indexPatterns: IIndexPattern[];
intl: InjectedIntl;
appName: string;
// Track UI Metrics
trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void;
}
function FilterBarUI(props: Props) {
const [isAddFilterPopoverOpen, setIsAddFilterPopoverOpen] = useState(false);
const kibana = useKibana();
const uiSettings = kibana.services.uiSettings;
const kibana = useKibana<IDataPluginServices>();
const { appName, usageCollection, uiSettings } = kibana.services;
if (!uiSettings) return null;
const reportUiCounter = usageCollection?.reportUiCounter.bind(usageCollection, appName);
function onFiltersUpdated(filters: Filter[]) {
if (props.onFiltersUpdated) {
props.onFiltersUpdated(filters);
@ -119,66 +118,65 @@ function FilterBarUI(props: Props) {
}
function onAdd(filter: Filter) {
reportUiCounter?.(METRIC_TYPE.CLICK, `filter:added`);
setIsAddFilterPopoverOpen(false);
if (props.trackUiMetric) {
props.trackUiMetric(METRIC_TYPE.CLICK, `${props.appName}:filter_added`);
}
const filters = [...props.filters, filter];
onFiltersUpdated(filters);
}
function onRemove(i: number) {
reportUiCounter?.(METRIC_TYPE.CLICK, `filter:removed`);
const filters = [...props.filters];
filters.splice(i, 1);
onFiltersUpdated(filters);
}
function onUpdate(i: number, filter: Filter) {
if (props.trackUiMetric) {
props.trackUiMetric(METRIC_TYPE.CLICK, `${props.appName}:filter_edited`);
}
reportUiCounter?.(METRIC_TYPE.CLICK, `filter:edited`);
const filters = [...props.filters];
filters[i] = filter;
onFiltersUpdated(filters);
}
function onEnableAll() {
reportUiCounter?.(METRIC_TYPE.CLICK, `filter:enable_all`);
const filters = props.filters.map(enableFilter);
onFiltersUpdated(filters);
}
function onDisableAll() {
reportUiCounter?.(METRIC_TYPE.CLICK, `filter:disable_all`);
const filters = props.filters.map(disableFilter);
onFiltersUpdated(filters);
}
function onPinAll() {
reportUiCounter?.(METRIC_TYPE.CLICK, `filter:pin_all`);
const filters = props.filters.map(pinFilter);
onFiltersUpdated(filters);
}
function onUnpinAll() {
reportUiCounter?.(METRIC_TYPE.CLICK, `filter:unpin_all`);
const filters = props.filters.map(unpinFilter);
onFiltersUpdated(filters);
}
function onToggleAllNegated() {
if (props.trackUiMetric) {
props.trackUiMetric(METRIC_TYPE.CLICK, `${props.appName}:filter_invertInclusion`);
}
reportUiCounter?.(METRIC_TYPE.CLICK, `filter:invert_all`);
const filters = props.filters.map(toggleFilterNegated);
onFiltersUpdated(filters);
}
function onToggleAllDisabled() {
if (props.trackUiMetric) {
props.trackUiMetric(METRIC_TYPE.CLICK, `${props.appName}:filter_toggleAllDisabled`);
}
reportUiCounter?.(METRIC_TYPE.CLICK, `filter:toggle_all`);
const filters = props.filters.map(toggleFilterDisabled);
onFiltersUpdated(filters);
}
function onRemoveAll() {
reportUiCounter?.(METRIC_TYPE.CLICK, `filter:remove_all`);
onFiltersUpdated([]);
}

View file

@ -26,6 +26,7 @@ import {
import { FormattedMessage } from '@kbn/i18n/react';
import { debounce, compact, isEqual, isFunction } from 'lodash';
import { Toast } from 'src/core/public';
import { METRIC_TYPE } from '@kbn/analytics';
import { IDataPluginServices, IIndexPattern, Query } from '../..';
import { QuerySuggestion, QuerySuggestionTypes } from '../../autocomplete';
@ -105,6 +106,10 @@ export default class QueryStringInputUI extends Component<Props, State> {
private abortController?: AbortController;
private fetchIndexPatternsAbortController?: AbortController;
private services = this.props.kibana.services;
private reportUiCounter = this.services.usageCollection?.reportUiCounter.bind(
this.services.usageCollection,
this.services.appName
);
private componentIsUnmounting = false;
private queryBarInputDivRefInstance: RefObject<HTMLDivElement> = createRef();
@ -178,12 +183,14 @@ export default class QueryStringInputUI extends Component<Props, State> {
selectionEnd,
signal: this.abortController.signal,
})) || [];
return [...suggestions, ...recentSearchSuggestions];
} catch (e) {
// TODO: Waiting on https://github.com/elastic/kibana/issues/51406 for a properly typed error
// Ignore aborted requests
if (e.message === 'The user aborted a request.') return;
this.reportUiCounter?.(METRIC_TYPE.LOADED, `query_string:suggestions_error`);
throw e;
}
};
@ -302,7 +309,7 @@ export default class QueryStringInputUI extends Component<Props, State> {
}
if (isSuggestionsVisible && index !== null && this.state.suggestions[index]) {
event.preventDefault();
this.selectSuggestion(this.state.suggestions[index]);
this.selectSuggestion(this.state.suggestions[index], index);
} else {
this.onSubmit(this.props.query);
this.setState({
@ -335,7 +342,7 @@ export default class QueryStringInputUI extends Component<Props, State> {
}
};
private selectSuggestion = (suggestion: QuerySuggestion) => {
private selectSuggestion = (suggestion: QuerySuggestion, listIndex: number) => {
if (!this.inputRef) {
return;
}
@ -352,6 +359,17 @@ export default class QueryStringInputUI extends Component<Props, State> {
const value = query.substr(0, selectionStart) + query.substr(selectionEnd);
const newQueryString = value.substr(0, start) + text + value.substr(end);
this.reportUiCounter?.(
METRIC_TYPE.LOADED,
`query_string:${type}:suggestions_select_position`,
listIndex
);
this.reportUiCounter?.(
METRIC_TYPE.LOADED,
`query_string:${type}:suggestions_select_q_length`,
end - start
);
this.onQueryStringChange(newQueryString);
this.setState({
@ -458,6 +476,7 @@ export default class QueryStringInputUI extends Component<Props, State> {
const newQuery = { query: '', language };
this.onChange(newQuery);
this.onSubmit(newQuery);
this.reportUiCounter?.(METRIC_TYPE.LOADED, `query_string:language:${language}`);
};
private onOutsideClick = () => {
@ -480,11 +499,11 @@ export default class QueryStringInputUI extends Component<Props, State> {
}
};
private onClickSuggestion = (suggestion: QuerySuggestion) => {
private onClickSuggestion = (suggestion: QuerySuggestion, index: number) => {
if (!this.inputRef) {
return;
}
this.selectSuggestion(suggestion);
this.selectSuggestion(suggestion, index);
this.inputRef.focus();
};
@ -588,6 +607,7 @@ export default class QueryStringInputUI extends Component<Props, State> {
if (this.props.onChangeQueryInputFocus) {
this.props.onChangeQueryInputFocus(true);
}
requestAnimationFrame(() => {
this.handleAutoHeight();
});

View file

@ -10,7 +10,6 @@ import _ from 'lodash';
import React, { useEffect, useRef } from 'react';
import { CoreStart } from 'src/core/public';
import { IStorageWrapper } from 'src/plugins/kibana_utils/public';
import { UiCounterMetricType } from '@kbn/analytics';
import { KibanaContextProvider } from '../../../../kibana_react/public';
import { QueryStart, SavedQuery } from '../../query';
import { SearchBar, SearchBarOwnProps } from './';
@ -20,12 +19,13 @@ import { useSavedQuery } from './lib/use_saved_query';
import { DataPublicPluginStart } from '../../types';
import { Filter, Query, TimeRange } from '../../../common';
import { useQueryStringManager } from './lib/use_query_string_manager';
import { UsageCollectionSetup } from '../../../../usage_collection/public';
interface StatefulSearchBarDeps {
core: CoreStart;
data: Omit<DataPublicPluginStart, 'ui'>;
storage: IStorageWrapper;
trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void;
usageCollection?: UsageCollectionSetup;
}
export type StatefulSearchBarProps = SearchBarOwnProps & {
@ -110,7 +110,7 @@ const overrideDefaultBehaviors = (props: StatefulSearchBarProps) => {
return props.useDefaultBehaviors ? {} : props;
};
export function createSearchBar({ core, storage, data, trackUiMetric }: StatefulSearchBarDeps) {
export function createSearchBar({ core, storage, data, usageCollection }: StatefulSearchBarDeps) {
// App name should come from the core application service.
// Until it's available, we'll ask the user to provide it for the pre-wired component.
return (props: StatefulSearchBarProps) => {
@ -161,6 +161,7 @@ export function createSearchBar({ core, storage, data, trackUiMetric }: Stateful
appName: props.appName,
data,
storage,
usageCollection,
...core,
}}
>
@ -188,7 +189,6 @@ export function createSearchBar({ core, storage, data, trackUiMetric }: Stateful
onClearSavedQuery={defaultOnClearSavedQuery(props, clearSavedQuery)}
onSavedQueryUpdated={defaultOnSavedQueryUpdated(props, setSavedQuery)}
onSaved={defaultOnSavedQueryUpdated(props, setSavedQuery)}
trackUiMetric={trackUiMetric}
{...overrideDefaultBehaviors(props)}
/>
</KibanaContextProvider>

View file

@ -13,7 +13,7 @@ import React, { Component } from 'react';
import ResizeObserver from 'resize-observer-polyfill';
import { get, isEqual } from 'lodash';
import { METRIC_TYPE, UiCounterMetricType } from '@kbn/analytics';
import { METRIC_TYPE } from '@kbn/analytics';
import { withKibana, KibanaReactContextValue } from '../../../../kibana_react/public';
import QueryBarTopRow from '../query_string_input/query_bar_top_row';
@ -68,8 +68,6 @@ export interface SearchBarOwnProps {
onRefresh?: (payload: { dateRange: TimeRange }) => void;
indicateNoData?: boolean;
// Track UI Metrics
trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void;
}
export type SearchBarProps = SearchBarOwnProps & SearchBarInjectedDeps;
@ -323,9 +321,11 @@ class SearchBarUI extends Component<SearchBarProps, State> {
},
});
}
if (this.props.trackUiMetric) {
this.props.trackUiMetric(METRIC_TYPE.CLICK, `${this.services.appName}:query_submitted`);
}
this.services.usageCollection?.reportUiCounter(
this.services.appName,
METRIC_TYPE.CLICK,
'query_submitted'
);
}
);
};
@ -428,7 +428,6 @@ class SearchBarUI extends Component<SearchBarProps, State> {
onFiltersUpdated={this.props.onFiltersUpdated}
indexPatterns={this.props.indexPatterns!}
appName={this.services.appName}
trackUiMetric={this.props.trackUiMetric}
/>
</div>
</div>

View file

@ -22,6 +22,7 @@ exports[`SuggestionsComponent Passing the index should control which suggestion
>
<SuggestionComponent
ariaId="suggestion-0"
index={0}
innerRef={[Function]}
key="value - as promised, not helpful"
onClick={[Function]}
@ -40,6 +41,7 @@ exports[`SuggestionsComponent Passing the index should control which suggestion
/>
<SuggestionComponent
ariaId="suggestion-1"
index={1}
innerRef={[Function]}
key="field - yep"
onClick={[Function]}
@ -84,6 +86,7 @@ exports[`SuggestionsComponent Should display given suggestions if the show prop
>
<SuggestionComponent
ariaId="suggestion-0"
index={0}
innerRef={[Function]}
key="value - as promised, not helpful"
onClick={[Function]}
@ -102,6 +105,7 @@ exports[`SuggestionsComponent Should display given suggestions if the show prop
/>
<SuggestionComponent
ariaId="suggestion-1"
index={1}
innerRef={[Function]}
key="field - yep"
onClick={[Function]}

View file

@ -27,6 +27,7 @@ describe('SuggestionComponent', () => {
it('Should display the suggestion and use the provided ariaId', () => {
const component = shallow(
<SuggestionComponent
index={0}
onClick={noop}
onMouseEnter={noop}
selected={false}
@ -43,6 +44,7 @@ describe('SuggestionComponent', () => {
it('Should make the element active if the selected prop is true', () => {
const component = shallow(
<SuggestionComponent
index={0}
onClick={noop}
onMouseEnter={noop}
selected={true}
@ -64,6 +66,7 @@ describe('SuggestionComponent', () => {
mount(
<SuggestionComponent
index={0}
onClick={noop}
onMouseEnter={noop}
selected={false}
@ -80,6 +83,7 @@ describe('SuggestionComponent', () => {
const component = shallow(
<SuggestionComponent
index={0}
onClick={mockHandler}
onMouseEnter={noop}
selected={false}
@ -92,7 +96,7 @@ describe('SuggestionComponent', () => {
component.simulate('click');
expect(mockHandler).toHaveBeenCalledTimes(1);
expect(mockHandler).toHaveBeenCalledWith(mockSuggestion);
expect(mockHandler).toHaveBeenCalledWith(mockSuggestion, 0);
});
it('Should call onMouseEnter when user mouses over the element', () => {
@ -100,6 +104,7 @@ describe('SuggestionComponent', () => {
const component = shallow(
<SuggestionComponent
index={0}
onClick={noop}
onMouseEnter={mockHandler}
selected={false}

View file

@ -10,6 +10,7 @@ import { EuiIcon } from '@elastic/eui';
import classNames from 'classnames';
import React from 'react';
import { QuerySuggestion } from '../../autocomplete';
import { SuggestionOnClick } from './types';
function getEuiIconType(type: string) {
switch (type) {
@ -29,9 +30,10 @@ function getEuiIconType(type: string) {
}
interface Props {
onClick: (suggestion: QuerySuggestion) => void;
onClick: SuggestionOnClick;
onMouseEnter: () => void;
selected: boolean;
index: number;
suggestion: QuerySuggestion;
innerRef: (node: HTMLDivElement) => void;
ariaId: string;
@ -48,7 +50,7 @@ export function SuggestionComponent(props: Props) {
active: props.selected,
})}
role="option"
onClick={() => props.onClick(props.suggestion)}
onClick={() => props.onClick(props.suggestion, props.index)}
onMouseEnter={props.onMouseEnter}
ref={props.innerRef}
id={props.ariaId}

View file

@ -115,7 +115,7 @@ describe('SuggestionsComponent', () => {
component.find(SuggestionComponent).at(1).simulate('click');
expect(mockCallback).toHaveBeenCalledTimes(1);
expect(mockCallback).toHaveBeenCalledWith(mockSuggestions[1]);
expect(mockCallback).toHaveBeenCalledWith(mockSuggestions[1], 1);
});
it('Should call onMouseEnter with the index of the suggestion that was entered', () => {

View file

@ -17,11 +17,12 @@ import {
SUGGESTIONS_LIST_REQUIRED_TOP_OFFSET,
SUGGESTIONS_LIST_REQUIRED_WIDTH,
} from './constants';
import { SuggestionOnClick } from './types';
// @internal
export interface SuggestionsComponentProps {
index: number | null;
onClick: (suggestion: QuerySuggestion) => void;
onClick: SuggestionOnClick;
onMouseEnter: (index: number) => void;
show: boolean;
suggestions: QuerySuggestion[];
@ -50,6 +51,7 @@ export default class SuggestionsComponent extends Component<SuggestionsComponent
<SuggestionComponent
innerRef={(node) => (this.childNodes[index] = node)}
selected={index === this.props.index}
index={index}
suggestion={suggestion}
onClick={this.props.onClick}
onMouseEnter={() => this.props.onMouseEnter(index)}

View file

@ -0,0 +1,11 @@
/*
* 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 { QuerySuggestion } from '../../autocomplete';
export type SuggestionOnClick = (suggestion: QuerySuggestion, index: number) => void;