Skip to content

Commit

Permalink
[Lens] Track actions in the UI by time (#47919)
Browse files Browse the repository at this point in the history
* [Lens] Track actions in the UI by time

* Switch collector to use task data

* Report summarized version of task data when requested

* Track a more complete set of metrics

* Collect suggestion events separately

* Pre-aggregate by date in localStorage

* Add integration tests

* Fix test linter

* Fix telemetry naming and run at midnight instead of every minute

* Improve cleanup at app level

* Fix lint errors

* Remove unused mock

* Fix tests

* Fix types

* Update event names and fix local tracking

* Respond to review comments

* Fix task time

* Fix test
  • Loading branch information
Wylie Conlon authored Oct 16, 2019
1 parent ec38b0e commit fba41f6
Show file tree
Hide file tree
Showing 32 changed files with 1,133 additions and 103 deletions.
21 changes: 16 additions & 5 deletions x-pack/legacy/plugins/apm/typings/elasticsearch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,22 @@ declare module 'elasticsearch' {

// eslint-disable-next-line @typescript-eslint/prefer-interface
type FiltersAggregation<SubAggregationMap> = {
buckets: Array<
{
doc_count: number;
} & SubAggregation<SubAggregationMap>
>;
// The filters aggregation can have named filters or anonymous filters,
// which changes the structure of the return
// https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-filters-aggregation.html
buckets: SubAggregationMap extends {
filters: { filters: Record<string, unknown> };
}
? {
[key in keyof SubAggregationMap['filters']['filters']]: {
doc_count: number;
} & SubAggregation<SubAggregationMap>;
}
: Array<
{
doc_count: number;
} & SubAggregation<SubAggregationMap>
>;
};

type SamplerAggregation<SubAggregationMap> = SubAggregation<
Expand Down
1 change: 1 addition & 0 deletions x-pack/legacy/plugins/lens/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export const lens: LegacyPluginInitializer = kibana => {
savedObjects: server.savedObjects,
usage: server.usage,
config: server.config(),
server,
});

server.events.on('stop', () => {
Expand Down
16 changes: 16 additions & 0 deletions x-pack/legacy/plugins/lens/mappings.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,21 @@
"type": "keyword"
}
}
},
"lens-ui-telemetry": {
"properties": {
"name": {
"type": "keyword"
},
"type": {
"type": "keyword"
},
"date": {
"type": "date"
},
"count": {
"type": "integer"
}
}
}
}
14 changes: 14 additions & 0 deletions x-pack/legacy/plugins/lens/public/app_plugin/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_reac
import { Document, SavedObjectStore } from '../persistence';
import { EditorFrameInstance } from '../types';
import { NativeRenderer } from '../native_renderer';
import { trackUiEvent } from '../lens_ui_telemetry';

interface State {
isLoading: boolean;
Expand Down Expand Up @@ -84,6 +85,8 @@ export function App({
const subscription = dataShim.filter.filterManager.getUpdates$().subscribe({
next: () => {
setState(s => ({ ...s, filters: dataShim.filter.filterManager.getFilters() }));

trackUiEvent('app_filters_updated');
},
});
return () => {
Expand Down Expand Up @@ -191,6 +194,16 @@ export function App({
screenTitle={'lens'}
onQuerySubmit={payload => {
const { dateRange, query } = payload;

if (
dateRange.from !== state.dateRange.fromDate ||
dateRange.to !== state.dateRange.toDate
) {
trackUiEvent('app_date_change');
} else {
trackUiEvent('app_query_change');
}

setState(s => ({
...s,
dateRange: {
Expand Down Expand Up @@ -309,6 +322,7 @@ export function App({
}
})
.catch(() => {
trackUiEvent('save_failed');
core.notifications.toasts.addDanger(
i18n.translate('xpack.lens.app.docSavingError', {
defaultMessage: 'Error saving document',
Expand Down
18 changes: 18 additions & 0 deletions x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ import {
} from '../datatable_visualization_plugin';
import { App } from './app';
import { EditorFrameInstance } from '../types';
import {
LensReportManager,
setReportManager,
stopReportManager,
trackUiEvent,
} from '../lens_ui_telemetry';

export interface LensPluginStartDependencies {
data: DataPublicPluginStart;
Expand Down Expand Up @@ -63,7 +69,16 @@ export class AppPlugin {

this.instance = editorFrameStartInterface.createInstance({});

setReportManager(
new LensReportManager({
storage: new Storage(localStorage),
basePath: core.http.basePath.get(),
http: core.http,
})
);

const renderEditor = (routeProps: RouteComponentProps<{ id?: string }>) => {
trackUiEvent('loaded');
return (
<App
core={core}
Expand All @@ -85,6 +100,7 @@ export class AppPlugin {
};

function NotFound() {
trackUiEvent('loaded_404');
return <FormattedMessage id="xpack.lens.app404" defaultMessage="404 Not Found" />;
}

Expand All @@ -106,6 +122,8 @@ export class AppPlugin {
this.instance.unmount();
}

stopReportManager();

// TODO this will be handled by the plugin platform itself
indexPatternDatasourceStop();
xyVisualizationStop();
Expand Down
26 changes: 25 additions & 1 deletion x-pack/legacy/plugins/lens/public/drag_drop/drag_drop.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ describe('DragDrop', () => {

const component = mount(
<ChildDragDropProvider dragging="hola" setDragging={setDragging}>
<DragDrop onDrop={onDrop} value={value}>
<DragDrop onDrop={onDrop} droppable={true} value={value}>
Hello!
</DragDrop>
</ChildDragDropProvider>
Expand All @@ -87,6 +87,30 @@ describe('DragDrop', () => {
expect(onDrop).toBeCalledWith('hola');
});

test('drop function is not called on droppable=false', async () => {
const preventDefault = jest.fn();
const stopPropagation = jest.fn();
const setDragging = jest.fn();
const onDrop = jest.fn();

const component = mount(
<ChildDragDropProvider dragging="hola" setDragging={setDragging}>
<DragDrop onDrop={onDrop} droppable={false} value={{}}>
Hello!
</DragDrop>
</ChildDragDropProvider>
);

component
.find('[data-test-subj="lnsDragDrop"]')
.simulate('drop', { preventDefault, stopPropagation });

expect(preventDefault).toBeCalled();
expect(stopPropagation).toBeCalled();
expect(setDragging).toBeCalledWith(undefined);
expect(onDrop).not.toHaveBeenCalled();
});

test('droppable is reflected in the className', () => {
const component = render(
<DragDrop
Expand Down
4 changes: 3 additions & 1 deletion x-pack/legacy/plugins/lens/public/drag_drop/drag_drop.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import React, { useState, useContext } from 'react';
import classNames from 'classnames';
import { DragContext } from './providers';
import { trackUiEvent } from '../lens_ui_telemetry';

type DroppableEvent = React.DragEvent<HTMLElement>;

Expand Down Expand Up @@ -121,7 +122,8 @@ export function DragDrop(props: Props) {
setState({ ...state, isActive: false });
setDragging(undefined);

if (onDrop) {
if (onDrop && droppable) {
trackUiEvent('drop_total');
onDrop(dragging);
}
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { i18n } from '@kbn/i18n';
import { Visualization, FramePublicAPI, Datasource } from '../../types';
import { Action } from './state_management';
import { getSuggestions, switchToSuggestion, Suggestion } from './suggestion_helpers';
import { trackUiEvent } from '../../lens_ui_telemetry';

interface VisualizationSelection {
visualizationId: string;
Expand Down Expand Up @@ -76,6 +77,8 @@ export function ChartSwitch(props: Props) {
const commitSelection = (selection: VisualizationSelection) => {
setFlyoutOpen(false);

trackUiEvent(`chart_switch`);

switchToSuggestion(
props.framePublicAPI,
props.dispatch,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { getSuggestions, switchToSuggestion } from './suggestion_helpers';
import { ExpressionRenderer } from '../../../../../../../src/legacy/core_plugins/expressions/public';
import { prependDatasourceExpression, prependKibanaContext } from './expression_helpers';
import { debouncedComponent } from '../../debounced_component';
import { trackUiEvent, trackSuggestionEvent } from '../../lens_ui_telemetry';

const MAX_SUGGESTIONS_DISPLAYED = 5;

Expand Down Expand Up @@ -227,6 +228,7 @@ export function SuggestionPanel({

function rollbackToCurrentVisualization() {
if (lastSelectedSuggestion !== -1) {
trackSuggestionEvent('back_to_current');
setLastSelectedSuggestion(-1);
dispatch({
type: 'ROLLBACK_SUGGESTION',
Expand Down Expand Up @@ -261,6 +263,7 @@ export function SuggestionPanel({
data-test-subj="lensSubmitSuggestion"
size="xs"
onClick={() => {
trackUiEvent('suggestion_confirmed');
dispatch({
type: 'SUBMIT_SUGGESTION',
});
Expand Down Expand Up @@ -307,9 +310,11 @@ export function SuggestionPanel({
ExpressionRenderer={ExpressionRendererComponent}
key={index}
onSelect={() => {
trackUiEvent('suggestion_clicked');
if (lastSelectedSuggestion === index) {
rollbackToCurrentVisualization();
} else {
trackSuggestionEvent(`position_${index}_of_${suggestions.length}`);
setLastSelectedSuggestion(index);
switchToSuggestion(frame, dispatch, suggestion);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { DragDrop, DragContext } from '../../drag_drop';
import { getSuggestions, switchToSuggestion } from './suggestion_helpers';
import { buildExpression } from './expression_helpers';
import { debouncedComponent } from '../../debounced_component';
import { trackUiEvent } from '../../lens_ui_telemetry';

export interface WorkspacePanelProps {
activeVisualizationId: string | null;
Expand Down Expand Up @@ -91,8 +92,37 @@ export function InnerWorkspacePanel({
return suggestions.find(s => s.visualizationId === activeVisualizationId) || suggestions[0];
}, [dragDropContext.dragging]);

const [expressionError, setExpressionError] = useState<unknown>(undefined);

const activeVisualization = activeVisualizationId
? visualizationMap[activeVisualizationId]
: null;
const expression = useMemo(() => {
try {
return buildExpression({
visualization: activeVisualization,
visualizationState,
datasourceMap,
datasourceStates,
framePublicAPI,
});
} catch (e) {
setExpressionError(e.toString());
}
}, [
activeVisualization,
visualizationState,
datasourceMap,
datasourceStates,
framePublicAPI.dateRange,
framePublicAPI.query,
framePublicAPI.filters,
]);

function onDrop() {
if (suggestionForDraggedField) {
trackUiEvent('drop_onto_workspace');
trackUiEvent(expression ? 'drop_non_empty' : 'drop_empty');
switchToSuggestion(
framePublicAPI,
dispatch,
Expand Down Expand Up @@ -146,33 +176,6 @@ export function InnerWorkspacePanel({
}

function renderVisualization() {
const [expressionError, setExpressionError] = useState<unknown>(undefined);

const activeVisualization = activeVisualizationId
? visualizationMap[activeVisualizationId]
: null;
const expression = useMemo(() => {
try {
return buildExpression({
visualization: activeVisualization,
visualizationState,
datasourceMap,
datasourceStates,
framePublicAPI,
});
} catch (e) {
setExpressionError(e.toString());
}
}, [
activeVisualization,
visualizationState,
datasourceMap,
datasourceStates,
framePublicAPI.dateRange,
framePublicAPI.query,
framePublicAPI.filters,
]);

useEffect(() => {
// reset expression error if component attempts to run it again
if (expressionError) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import React, { useState } from 'react';
import { EuiButtonEmpty, EuiPopover, EuiSelectable, EuiButtonEmptyProps } from '@elastic/eui';
import { EuiSelectableProps } from '@elastic/eui/src/components/selectable/selectable';
import { IndexPatternRef } from './types';
import { trackUiEvent } from '../lens_ui_telemetry';

export type ChangeIndexPatternTriggerProps = EuiButtonEmptyProps & {
label: string;
Expand Down Expand Up @@ -74,6 +75,7 @@ export function ChangeIndexPattern({
const choice = (choices.find(({ checked }) => checked) as unknown) as {
value: string;
};
trackUiEvent('indexpattern_changed');
onChangeIndexPattern(choice.value);
setPopoverIsOpen(false);
}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
IndexPatternField,
IndexPatternRef,
} from './types';
import { trackUiEvent } from '../lens_ui_telemetry';
import { syncExistingFields } from './loader';
import { fieldExists } from './pure_helpers';

Expand Down Expand Up @@ -301,6 +302,7 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
defaultMessage: 'Clear name and type filters',
}),
onClick: () => {
trackUiEvent('indexpattern_filters_cleared');
setLocalState(s => ({
...s,
nameFilter: '',
Expand Down Expand Up @@ -370,14 +372,15 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
key={type}
icon={localState.typeFilter.includes(type) ? 'check' : 'empty'}
data-test-subj={`typeFilter-${type}`}
onClick={() =>
onClick={() => {
trackUiEvent('indexpattern_type_filter_toggled');
setLocalState(s => ({
...s,
typeFilter: localState.typeFilter.includes(type)
? localState.typeFilter.filter(t => t !== type)
: [...localState.typeFilter, type],
}))
}
}));
}}
>
<LensFieldIcon type={type} /> {fieldTypeNames[type]}
</EuiContextMenuItem>
Expand All @@ -388,6 +391,7 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
compressed
checked={!showEmptyFields}
onChange={() => {
trackUiEvent('indexpattern_existence_toggled');
onToggleEmptyFields();
}}
label={i18n.translate('xpack.lens.indexPatterns.toggleEmptyFieldsSwitch', {
Expand Down
Loading

0 comments on commit fba41f6

Please sign in to comment.