Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Telemetry for Global Search #160224

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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