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

[Lens] Track actions in the UI by time #47919

Merged
merged 24 commits into from
Oct 16, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a832b1e
[Lens] Track actions in the UI by time
wylieconlon Oct 9, 2019
8c818e4
Switch collector to use task data
wylieconlon Oct 11, 2019
5281cbd
Report summarized version of task data when requested
wylieconlon Oct 11, 2019
bf26faa
Track a more complete set of metrics
wylieconlon Oct 12, 2019
5c7fdb7
Collect suggestion events separately
wylieconlon Oct 13, 2019
e8e2b70
Pre-aggregate by date in localStorage
wylieconlon Oct 14, 2019
1fcce38
Add integration tests
wylieconlon Oct 14, 2019
8a2dce4
Merge remote-tracking branch 'origin/master' into lens/click-telemetry
wylieconlon Oct 14, 2019
c4c5639
Fix test linter
wylieconlon Oct 14, 2019
e9590e8
Fix telemetry naming and run at midnight instead of every minute
wylieconlon Oct 14, 2019
bbe16fa
Improve cleanup at app level
wylieconlon Oct 14, 2019
b75ca97
Fix lint errors
wylieconlon Oct 14, 2019
863f329
Remove unused mock
wylieconlon Oct 14, 2019
7dc94b8
Fix tests
wylieconlon Oct 15, 2019
2a60204
Merge remote-tracking branch 'origin/master' into lens/click-telemetry
wylieconlon Oct 15, 2019
6c24d16
Fix types
wylieconlon Oct 15, 2019
f1a1399
Merge remote-tracking branch 'origin/master' into lens/click-telemetry
wylieconlon Oct 15, 2019
fd1f6d6
Update event names and fix local tracking
wylieconlon Oct 15, 2019
2a28500
Merge remote-tracking branch 'origin/master' into lens/click-telemetry
wylieconlon Oct 15, 2019
16ebf6c
Respond to review comments
wylieconlon Oct 15, 2019
4da50b3
Fix task time
wylieconlon Oct 15, 2019
b5d54a7
Merge remote-tracking branch 'origin/master' into lens/click-telemetry
wylieconlon Oct 15, 2019
5ef055f
Fix test
wylieconlon Oct 16, 2019
1836ed1
Merge remote-tracking branch 'origin/master' into lens/click-telemetry
wylieconlon Oct 16, 2019
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
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> };
}
? {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm unfamiliar with this TypeScript signature. TIL

[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');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this actually interesting? I'm not sure it's worth collecting, as I expect it to be just super noisy.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could combine this with filter changes, but I do think it's interesting

}

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(
wylieconlon marked this conversation as resolved.
Show resolved Hide resolved
new LensReportManager({
storage: new Storage(localStorage),
basePath: core.http.basePath.get(),
http: core.http,
})
);

const renderEditor = (routeProps: RouteComponentProps<{ id?: string }>) => {
trackUiEvent('loaded');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this worth tracking? Maybe. 404 certainly is.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we don't track some kind of loaded event it won't be possible to create any kind of funnel, because these metrics are in a window and no other metrics are

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');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is probably fine, but if we ever want to make this drag / drop thing more general, this would be something that requires yanking out.

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