Skip to content

Commit

Permalink
Telemetry for Global Search (#160224)
Browse files Browse the repository at this point in the history
## Summary

Closes #158880

| | **`global_search_bar_click_application`** |
**`global_search_bar_click_saved_object`** |
**`global_search_bar_blur`** | **`global_search_bar_error`** |

|-------------------------------|-----------------------------------------|------------------------------------------|----------------------------|-----------------------------|
| **`application`: string** | X | | | |
| **`saved_object_type`: string** | | X | | |
| **`terms`: string** | X | X | | X |
| **`selected_rank`: number** | X | X | | |
| **`selected_label`: string** | X | X | | |
| **`focus_time_ms`: number** | | | X | |
| **`error_message`** | | | | X |

NOTE: The "blur" event needs to be created any time the user dismisses
the search bar, whether or not a selection has been made. Therefore, the
action of a user selecting an option in the global search will yield two
separate events.

### Checklist

Delete any items that are not applicable to this PR.

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [x] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)
  • Loading branch information
tsullivan authored Jun 27, 2023
1 parent 1fa9ab9 commit 81c6a90
Show file tree
Hide file tree
Showing 11 changed files with 704 additions and 137 deletions.
19 changes: 0 additions & 19 deletions x-pack/plugins/global_search_bar/public/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,6 @@

import { GlobalSearchResult } from '@kbn/global-search-plugin/public';

/* @internal */
export enum COUNT_METRIC {
UNHANDLED_ERROR = 'unhandled_error',
SEARCH_REQUEST = 'search_request',
SHORTCUT_USED = 'shortcut_used',
SEARCH_FOCUS = 'search_focus',
}

/* @internal */
export enum CLICK_METRIC {
USER_NAVIGATED_TO_APPLICATION = 'user_navigated_to_application',
USER_NAVIGATED_TO_SAVED_OBJECT = 'user_navigated_to_saved_object',
}

/* @internal */
export const getClickMetric = (metric: CLICK_METRIC, context: string) => {
return [metric, `${metric}_${context}`];
};

/* @internal */
export const isMac = navigator.platform.toLowerCase().indexOf('mac') >= 0;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,16 @@
*/

import type { ChromeStyle } from '@kbn/core-chrome-browser';
import { applicationServiceMock } from '@kbn/core/public/mocks';
import { applicationServiceMock, coreMock } from '@kbn/core/public/mocks';
import { GlobalSearchBatchedResults, GlobalSearchResult } from '@kbn/global-search-plugin/public';
import { globalSearchPluginMock } from '@kbn/global-search-plugin/public/mocks';
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
import { usageCollectionPluginMock } from '@kbn/usage-collection-plugin/public/mocks';
import { act, fireEvent, render, screen } from '@testing-library/react';
import React from 'react';
import { BehaviorSubject, of } from 'rxjs';
import { filter, map } from 'rxjs/operators';
import type { TrackUiMetricFn } from '../types';
import { EventReporter } from '../telemetry';
import { SearchBar } from './search_bar';

jest.mock(
Expand Down Expand Up @@ -47,17 +48,18 @@ const createBatch = (...results: Result[]): GlobalSearchBatchedResults => ({
jest.useFakeTimers({ legacyFakeTimers: true });

describe('SearchBar', () => {
let searchService: ReturnType<typeof globalSearchPluginMock.createStartContract>;
let applications: ReturnType<typeof applicationServiceMock.createStartContract>;
let trackUiMetric: TrackUiMetricFn;
const usageCollection = usageCollectionPluginMock.createSetupContract();
const core = coreMock.createStart();

const basePathUrl = '/plugins/globalSearchBar/assets/';
const darkMode = false;
const eventReporter = new EventReporter({ analytics: core.analytics, usageCollection });
let searchService: ReturnType<typeof globalSearchPluginMock.createStartContract>;
let applications: ReturnType<typeof applicationServiceMock.createStartContract>;

beforeEach(() => {
applications = applicationServiceMock.createStartContract();
searchService = globalSearchPluginMock.createStartContract();
trackUiMetric = jest.fn();
});

const update = () => {
Expand Down Expand Up @@ -109,7 +111,7 @@ describe('SearchBar', () => {
basePathUrl={basePathUrl}
darkMode={darkMode}
chromeStyle$={chromeStyle$}
trackUiMetric={trackUiMetric}
reportEvent={eventReporter}
/>
</IntlProvider>
);
Expand All @@ -127,10 +129,6 @@ describe('SearchBar', () => {
await assertSearchResults(['Discover • Kibana', 'My Dashboard • Test']);
expect(searchService.find).toHaveBeenCalledTimes(2);
expect(searchService.find).toHaveBeenLastCalledWith({ term: 'd' }, {});

expect(trackUiMetric).nthCalledWith(1, 'count', 'search_focus');
expect(trackUiMetric).nthCalledWith(2, 'count', 'search_request');
expect(trackUiMetric).toHaveBeenCalledTimes(2);
});

it('supports keyboard shortcuts', async () => {
Expand All @@ -142,7 +140,7 @@ describe('SearchBar', () => {
basePathUrl={basePathUrl}
darkMode={darkMode}
chromeStyle$={chromeStyle$}
trackUiMetric={trackUiMetric}
reportEvent={eventReporter}
/>
</IntlProvider>
);
Expand All @@ -153,10 +151,6 @@ describe('SearchBar', () => {
const inputElement = await screen.findByTestId('nav-search-input');

expect(document.activeElement).toEqual(inputElement);

expect(trackUiMetric).nthCalledWith(1, 'count', 'shortcut_used');
expect(trackUiMetric).nthCalledWith(2, 'count', 'search_focus');
expect(trackUiMetric).toHaveBeenCalledTimes(2);
});

it('only display results from the last search', async () => {
Expand All @@ -179,7 +173,7 @@ describe('SearchBar', () => {
basePathUrl={basePathUrl}
darkMode={darkMode}
chromeStyle$={chromeStyle$}
trackUiMetric={trackUiMetric}
reportEvent={eventReporter}
/>
</IntlProvider>
);
Expand All @@ -197,45 +191,6 @@ describe('SearchBar', () => {

await assertSearchResults(['Visualize • Kibana', 'Map • Kibana']);
});

it('tracks the application navigated to', async () => {
searchService.find.mockReturnValueOnce(
of(createBatch('Discover', { id: 'My Dashboard', type: 'test' }))
);

render(
<IntlProvider locale="en">
<SearchBar
globalSearch={searchService}
navigateToUrl={applications.navigateToUrl}
basePathUrl={basePathUrl}
darkMode={darkMode}
chromeStyle$={chromeStyle$}
trackUiMetric={trackUiMetric}
/>
</IntlProvider>
);

expect(searchService.find).toHaveBeenCalledTimes(0);

await focusAndUpdate();

expect(searchService.find).toHaveBeenCalledTimes(1);
expect(searchService.find).toHaveBeenCalledWith({}, {});
await assertSearchResults(['Discover • Kibana']);

const navSearchOptionToClick = await screen.findByTestId('nav-search-option');
act(() => {
fireEvent.click(navSearchOptionToClick);
});

expect(trackUiMetric).nthCalledWith(1, 'count', 'search_focus');
expect(trackUiMetric).nthCalledWith(2, 'click', [
'user_navigated_to_application',
'user_navigated_to_application_discover',
]);
expect(trackUiMetric).toHaveBeenCalledTimes(2);
});
});

describe('chromeStyle: project', () => {
Expand All @@ -250,7 +205,7 @@ describe('SearchBar', () => {
basePathUrl={basePathUrl}
darkMode={darkMode}
chromeStyle$={chromeStyle$}
trackUiMetric={trackUiMetric}
reportEvent={eventReporter}
/>
</IntlProvider>
);
Expand All @@ -259,16 +214,10 @@ describe('SearchBar', () => {
fireEvent.keyDown(window, { key: '/', ctrlKey: true, metaKey: true });
});

const inputElement = await screen.findByTestId('nav-search-input');

expect(document.activeElement).toEqual(inputElement);
expect(await screen.findByTestId('nav-search-input')).toEqual(document.activeElement);

fireEvent.click(await screen.findByTestId('nav-search-conceal'));
expect(screen.queryAllByTestId('nav-search-input')).toHaveLength(0);

expect(trackUiMetric).nthCalledWith(1, 'count', 'shortcut_used');
expect(trackUiMetric).nthCalledWith(2, 'count', 'search_focus');
expect(trackUiMetric).toHaveBeenCalledTimes(2);
});

it('supports show/hide', async () => {
Expand All @@ -280,7 +229,7 @@ describe('SearchBar', () => {
basePathUrl={basePathUrl}
darkMode={darkMode}
chromeStyle$={chromeStyle$}
trackUiMetric={trackUiMetric}
reportEvent={eventReporter}
/>
</IntlProvider>
);
Expand All @@ -290,8 +239,6 @@ describe('SearchBar', () => {

fireEvent.click(await screen.findByTestId('nav-search-conceal'));
expect(screen.queryAllByTestId('nav-search-input')).toHaveLength(0);

expect(trackUiMetric).nthCalledWith(1, 'count', 'search_focus');
});
});
});
74 changes: 42 additions & 32 deletions x-pack/plugins/global_search_bar/public/components/search_bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,14 @@ import {
euiSelectableTemplateSitewideRenderOptions,
useEuiTheme,
} from '@elastic/eui';
import { METRIC_TYPE } from '@kbn/analytics';
import type { GlobalSearchFindParams, GlobalSearchResult } from '@kbn/global-search-plugin/public';
import React, { FC, useCallback, useEffect, useRef, useState } from 'react';
import useDebounce from 'react-use/lib/useDebounce';
import useEvent from 'react-use/lib/useEvent';
import useMountedState from 'react-use/lib/useMountedState';
import useObservable from 'react-use/lib/useObservable';
import { Subscription } from 'rxjs';
import { blurEvent, CLICK_METRIC, COUNT_METRIC, getClickMetric, isMac, sort } from '.';
import { blurEvent, isMac, sort } from '.';
import { resultToOption, suggestionToOption } from '../lib';
import { parseSearchParams } from '../search_syntax';
import { i18nStrings } from '../strings';
Expand All @@ -48,14 +47,9 @@ const EmptyMessage = () => (
</EuiFlexGroup>
);

export const SearchBar: FC<SearchBarProps> = ({
globalSearch,
taggingApi,
navigateToUrl,
trackUiMetric,
chromeStyle$,
...props
}) => {
export const SearchBar: FC<SearchBarProps> = (opts) => {
const { globalSearch, taggingApi, navigateToUrl, reportEvent, chromeStyle$, ...props } = opts;

const isMounted = useMountedState();
const { euiTheme } = useEuiTheme();
const chromeStyle = useObservable(chromeStyle$);
Expand Down Expand Up @@ -134,7 +128,7 @@ export const SearchBar: FC<SearchBarProps> = ({

let aggregatedResults: GlobalSearchResult[] = [];
if (searchValue.length !== 0) {
trackUiMetric(METRIC_TYPE.COUNT, COUNT_METRIC.SEARCH_REQUEST);
reportEvent.searchRequest();
}

const rawParams = parseSearchParams(searchValue);
Expand Down Expand Up @@ -170,10 +164,10 @@ export const SearchBar: FC<SearchBarProps> = ({

setOptions(aggregatedResults, suggestions, searchParams.tags);
},
error: () => {
error: (err) => {
// Not doing anything on error right now because it'll either just show the previous
// results or empty results which is basically what we want anyways
trackUiMetric(METRIC_TYPE.COUNT, COUNT_METRIC.UNHANDLED_ERROR);
reportEvent.error({ message: err, searchValue });
},
complete: () => {},
});
Expand All @@ -187,7 +181,7 @@ export const SearchBar: FC<SearchBarProps> = ({
(event: KeyboardEvent) => {
if (event.key === '/' && (isMac ? event.metaKey : event.ctrlKey)) {
event.preventDefault();
trackUiMetric(METRIC_TYPE.COUNT, COUNT_METRIC.SHORTCUT_USED);
reportEvent.shortcutUsed();
if (chromeStyle === 'project' && !isVisible) {
visibilityButtonRef.current?.click();
} else if (searchRef) {
Expand All @@ -197,16 +191,26 @@ export const SearchBar: FC<SearchBarProps> = ({
}
}
},
[chromeStyle, isVisible, buttonRef, searchRef, trackUiMetric]
[chromeStyle, isVisible, buttonRef, searchRef, reportEvent]
);

const onChange = useCallback(
(selection: EuiSelectableTemplateSitewideOption[]) => {
const selected = selection.find(({ checked }) => checked === 'on');
let selectedRank: number | null = null;
const selected = selection.find(({ checked }, rank) => {
const isChecked = checked === 'on';
if (isChecked) {
selectedRank = rank + 1;
}
return isChecked;
});

if (!selected) {
return;
}

const selectedLabel = selected.label ?? null;

// @ts-ignore - ts error is "union type is too complex to express"
const { url, type, suggestion } = selected;

Expand All @@ -222,19 +226,24 @@ export const SearchBar: FC<SearchBarProps> = ({
if (type === 'application') {
const key = selected.key ?? 'unknown';
const application = `${key.toLowerCase().replaceAll(' ', '_')}`;
trackUiMetric(
METRIC_TYPE.CLICK,
getClickMetric(CLICK_METRIC.USER_NAVIGATED_TO_APPLICATION, application)
);
reportEvent.navigateToApplication({
application,
searchValue,
selectedLabel,
selectedRank,
});
} else {
trackUiMetric(
METRIC_TYPE.CLICK,
getClickMetric(CLICK_METRIC.USER_NAVIGATED_TO_SAVED_OBJECT, type)
);
reportEvent.navigateToSavedObject({
type,
searchValue,
selectedLabel,
selectedRank,
});
}
} catch (e) {
} catch (err) {
reportEvent.error({ message: err, searchValue });
// eslint-disable-next-line no-console
console.log('Error trying to track searchbar metrics', e);
console.log('Error trying to track searchbar metrics', err);
}

navigateToUrl(url);
Expand All @@ -245,7 +254,7 @@ export const SearchBar: FC<SearchBarProps> = ({
searchRef.dispatchEvent(blurEvent);
}
},
[trackUiMetric, navigateToUrl, searchRef]
[reportEvent, navigateToUrl, searchRef, searchValue]
);

const clearField = () => setSearchValue('');
Expand All @@ -257,17 +266,16 @@ export const SearchBar: FC<SearchBarProps> = ({
useEvent('keydown', onKeyDown);

if (chromeStyle === 'project' && !isVisible) {
const onShowSearch = () => {
setIsVisible(true);
};
return (
<EuiButtonIcon
aria-label={i18nStrings.showSearchAriaText}
buttonRef={visibilityButtonRef}
color="text"
data-test-subj="nav-search-reveal"
iconType="search"
onClick={onShowSearch}
onClick={() => {
setIsVisible(true);
}}
/>
);
}
Expand All @@ -281,6 +289,7 @@ export const SearchBar: FC<SearchBarProps> = ({
data-test-subj="nav-search-conceal"
iconType="cross"
onClick={() => {
reportEvent.searchBlur();
setIsVisible(false);
}}
/>
Expand Down Expand Up @@ -318,11 +327,12 @@ export const SearchBar: FC<SearchBarProps> = ({
'aria-label': i18nStrings.placeholderText,
placeholder: i18nStrings.placeholderText,
onFocus: () => {
trackUiMetric(METRIC_TYPE.COUNT, COUNT_METRIC.SEARCH_FOCUS);
reportEvent.searchFocus();
setInitialLoad(true);
setShowAppend(false);
},
onBlur: () => {
reportEvent.searchBlur();
setShowAppend(!searchValue.length);
},
fullWidth: true,
Expand Down
Loading

0 comments on commit 81c6a90

Please sign in to comment.