diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx b/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx index 7d29ae0284bae..bc52927f889fc 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx @@ -25,7 +25,7 @@ import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_reac import { Document, SavedObjectStore } from '../persistence'; import { EditorFrameInstance } from '../types'; import { NativeRenderer } from '../native_renderer'; -import { useLensTelemetry } from '../lens_ui_telemetry'; +import { trackUiEvent } from '../lens_ui_telemetry'; interface State { isLoading: boolean; @@ -65,7 +65,6 @@ export function App({ const timeDefaults = core.uiSettings.get('timepicker:timeDefaults'); const language = store.get('kibana.userQueryLanguage') || core.uiSettings.get('search:queryLanguage'); - const { trackClick } = useLensTelemetry(); const [state, setState] = useState({ isLoading: !!docId, @@ -87,7 +86,7 @@ export function App({ next: () => { setState(s => ({ ...s, filters: dataShim.filter.filterManager.getFilters() })); - trackClick('app_filters_updated'); + trackUiEvent('app_filters_updated'); }, }); return () => { @@ -202,7 +201,7 @@ export function App({ } }) .catch(() => { - trackClick('save_failed'); + trackUiEvent('save_failed'); core.notifications.toasts.addDanger( i18n.translate('xpack.lens.editorFrame.docSavingError', { defaultMessage: 'Error saving document', @@ -228,7 +227,7 @@ export function App({ query: query || s.query, })); - trackClick('date_or_query_change'); + trackUiEvent('date_or_query_change'); }} appName={'lens'} indexPatterns={state.indexPatternsForTopNav} diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx b/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx index 686a842194407..858f39c50d5af 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx @@ -25,7 +25,7 @@ import { } from '../datatable_visualization_plugin'; import { App } from './app'; import { EditorFrameInstance } from '../types'; -import { LensReportManager, LensTelemetryContext } from '../lens_ui_telemetry'; +import { LensReportManager, setReportManager, trackUiEvent } from '../lens_ui_telemetry'; export interface LensPluginStartDependencies { data: DataPublicPluginStart; @@ -65,44 +65,39 @@ export class AppPlugin { this.instance = editorFrameStartInterface.createInstance({}); - this.reporter = new LensReportManager({ - storage: new Storage(localStorage), - basePath: core.http.basePath.get(), - http: core.http, - }); + // Sets it in memory for future click tracking + setReportManager( + new LensReportManager({ + storage: new Storage(localStorage), + basePath: core.http.basePath.get(), + http: core.http, + }) + ); const renderEditor = (routeProps: RouteComponentProps<{ id?: string }>) => { - if (this.reporter) { - this.reporter.trackClick('loaded'); - } + trackUiEvent('loaded'); return ( - this.reporter && this.reporter.trackClick(name), - trackSuggestionClick: name => this.reporter && this.reporter.trackSuggestionClick(name), + { + if (!id) { + routeProps.history.push('/'); + } else { + routeProps.history.push(`/edit/${id}`); + } }} - > - { - if (!id) { - routeProps.history.push('/'); - } else { - routeProps.history.push(`/edit/${id}`); - } - }} - /> - + /> ); }; function NotFound() { + trackUiEvent('loaded-404'); return ; } diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/chart_switch.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/chart_switch.tsx index 1c94460034019..95b3b7b61957c 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/chart_switch.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/chart_switch.tsx @@ -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; @@ -76,6 +77,8 @@ export function ChartSwitch(props: Props) { const commitSelection = (selection: VisualizationSelection) => { setFlyoutOpen(false); + trackUiEvent(`chart-switch-${selection.subVisualizationId || selection.visualizationId}`); + switchToSuggestion( props.framePublicAPI, props.dispatch, diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.tsx index de25c2f7f638b..9fd5b4908e25d 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.tsx @@ -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 { trackSuggestionEvent } from '../../lens_ui_telemetry'; const MAX_SUGGESTIONS_DISPLAYED = 5; @@ -226,6 +227,7 @@ export function SuggestionPanel({ } function rollbackToCurrentVisualization() { + trackSuggestionEvent('rollback'); if (lastSelectedSuggestion !== -1) { setLastSelectedSuggestion(-1); dispatch({ @@ -310,6 +312,7 @@ export function SuggestionPanel({ if (lastSelectedSuggestion === index) { rollbackToCurrentVisualization(); } else { + trackSuggestionEvent(`${suggestion.visualizationId}-${suggestion.changeType}`); setLastSelectedSuggestion(index); switchToSuggestion(frame, dispatch, suggestion); } diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx index 66fac5d6cf705..04616143e7f31 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx @@ -16,6 +16,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; @@ -84,12 +85,15 @@ export function InnerWorkspacePanel({ function onDrop() { if (suggestionForDraggedField) { + trackUiEvent('workspace-drop-success'); switchToSuggestion( framePublicAPI, dispatch, suggestionForDraggedField, 'SWITCH_VISUALIZATION' ); + } else { + trackUiEvent('workspace-drop-failure'); } } diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx index 683a039128325..cb81ec3d69985 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx @@ -31,7 +31,6 @@ import { EditorFrame } from './editor_frame'; import { mergeTables } from './merge_tables'; import { EmbeddableFactory } from './embeddable/embeddable_factory'; import { getActiveDatasourceIdFromDoc } from './editor_frame/state_management'; -import { LensTelemetryContext, useLensTelemetry } from '../lens_ui_telemetry'; export interface EditorFrameSetupPlugins { data: typeof dataSetup; @@ -84,28 +83,24 @@ export class EditorFramePlugin { render( - - - + , domElement ); diff --git a/x-pack/legacy/plugins/lens/public/lens_ui_telemetry/__mocks__/index.ts b/x-pack/legacy/plugins/lens/public/lens_ui_telemetry/__mocks__/index.ts index 7f6a7481b0fef..6a8ff68e8e350 100644 --- a/x-pack/legacy/plugins/lens/public/lens_ui_telemetry/__mocks__/index.ts +++ b/x-pack/legacy/plugins/lens/public/lens_ui_telemetry/__mocks__/index.ts @@ -4,14 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -import { createContext, useContext } from 'react'; - -export const LensTelemetryContext = createContext<{ - trackClick: (name: string) => void; - trackSuggestionClick: (name: string, suggestionData: unknown) => void; -}>({ - trackClick: jest.fn(), - trackSuggestionClick: jest.fn(), -}); - -export const useLensTelemetry = () => useContext(LensTelemetryContext); +export const trackClick = jest.fn(); +export const trackSuggestionClick = jest.fn(); \ No newline at end of file diff --git a/x-pack/legacy/plugins/lens/public/lens_ui_telemetry/factory.ts b/x-pack/legacy/plugins/lens/public/lens_ui_telemetry/factory.ts index 0d0304c78f4c8..2d344867e047b 100644 --- a/x-pack/legacy/plugins/lens/public/lens_ui_telemetry/factory.ts +++ b/x-pack/legacy/plugins/lens/public/lens_ui_telemetry/factory.ts @@ -44,7 +44,7 @@ export class LensReportManager { }, 10000); } - public trackClick(name: string) { + public trackEvent(name: string) { this.clicks.push({ name, date: new Date().toISOString(), @@ -52,7 +52,7 @@ export class LensReportManager { this.write(); } - public trackSuggestionClick(name: string) { + public trackSuggestionEvent(name: string) { this.suggestionClicks.push({ name, date: new Date().toISOString(), @@ -75,7 +75,7 @@ export class LensReportManager { await this.http.post(`${this.basePath}${BASE_API_URL}/telemetry`, { body: JSON.stringify({ clicks: this.clicks, - suggestionClicks: this.clicks, + suggestionClicks: this.suggestionClicks, }), }); this.clicks = []; @@ -87,3 +87,21 @@ export class LensReportManager { } } } + +let reportManager: LensReportManager; + +export function setReportManager(newManager: LensReportManager) { + reportManager = newManager; +} + +export function trackUiEvent(name: string) { + if (reportManager) { + reportManager.trackEvent(name); + } +} + +export function trackSuggestionEvent(name: string) { + if (reportManager) { + reportManager.trackSuggestionEvent(name); + } +} diff --git a/x-pack/legacy/plugins/lens/public/lens_ui_telemetry/index.ts b/x-pack/legacy/plugins/lens/public/lens_ui_telemetry/index.ts index d7a8719137104..79575a59f565e 100644 --- a/x-pack/legacy/plugins/lens/public/lens_ui_telemetry/index.ts +++ b/x-pack/legacy/plugins/lens/public/lens_ui_telemetry/index.ts @@ -5,4 +5,3 @@ */ export * from './factory'; -export * from './provider'; diff --git a/x-pack/legacy/plugins/lens/public/lens_ui_telemetry/provider.ts b/x-pack/legacy/plugins/lens/public/lens_ui_telemetry/provider.ts deleted file mode 100644 index b7852cfff2062..0000000000000 --- a/x-pack/legacy/plugins/lens/public/lens_ui_telemetry/provider.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { createContext, useContext } from 'react'; - -export const LensTelemetryContext = createContext<{ - trackClick: (name: string) => void; - trackSuggestionClick: (name: string, suggestionData: unknown) => void; -}>({ - trackClick: () => {}, - trackSuggestionClick: () => {}, -}); - -export const useLensTelemetry = () => useContext(LensTelemetryContext); diff --git a/x-pack/legacy/plugins/lens/server/routes/telemetry.ts b/x-pack/legacy/plugins/lens/server/routes/telemetry.ts index b0a92c7d12b5a..e7cefe0f1e35d 100644 --- a/x-pack/legacy/plugins/lens/server/routes/telemetry.ts +++ b/x-pack/legacy/plugins/lens/server/routes/telemetry.ts @@ -76,7 +76,11 @@ export async function initLensUsageRoute( }, })); - await client.bulkCreate(clickEvents.concat(suggestionEvents)); + const events = clickEvents.concat(suggestionEvents); + + if (events.length > 0) { + await client.bulkCreate(events); + } return res.ok({ body: {} }); } catch (e) { diff --git a/x-pack/legacy/plugins/lens/server/usage/collectors.ts b/x-pack/legacy/plugins/lens/server/usage/collectors.ts index 68c1cffce6934..305b2eac20849 100644 --- a/x-pack/legacy/plugins/lens/server/usage/collectors.ts +++ b/x-pack/legacy/plugins/lens/server/usage/collectors.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import moment from 'moment'; import { get } from 'lodash'; import { Server, KibanaConfig } from 'src/legacy/server/kbn_server'; import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; @@ -43,9 +44,37 @@ export function registerLensUsageCollector( type: 'lens', fetch: async (callCluster: CallCluster): Promise => { try { - const docs = await fetch(plugins.server); + const docs = await getLatestTaskState(plugins.server); // get the accumulated state from the recurring task - return get(docs, '[0].state.stats'); + const state = get(docs, '[0].state'); + + const dates = Object.keys(state.byDate || {}).map(dateStr => parseInt(dateStr, 10)); + + const eventsLast30: Record = {}; + const eventsLast90: Record = {}; + + const last30 = moment() + .subtract(30, 'days') + .unix(); + const last90 = moment() + .subtract(90, 'days') + .unix(); + + dates.forEach(date => { + if (date > last30) { + addEvents(eventsLast30, state.byDate[date]); + addEvents(eventsLast90, state.byDate[date]); + } else if (date > last90) { + addEvents(eventsLast90, state.byDate[date]); + } + }); + + console.log(state); + return { + ...state.saved, + clicks_last_30_days: eventsLast30, + clicks_last_90_days: eventsLast90, + }; // return getVisualizationCounts(callCluster, plugins.config); } catch (err) { return { @@ -68,19 +97,24 @@ export function registerLensUsageCollector( plugins.usage.collectorSet.register(lensUsageCollector); } +function addEvents(prevEvents: Record, newEvents: Record) { + Object.keys(newEvents).forEach(key => { + prevEvents[key] = prevEvents[key] || 0 + newEvents[key]; + }); +} + async function isTaskManagerReady(server: Server) { - const result = await fetch(server); - return result !== null; + return (await getLatestTaskState(server)) !== null; } -async function fetch(server: Server) { +async function getLatestTaskState(server: Server) { const taskManager = server.plugins.task_manager!; - let docs; try { - ({ docs } = await taskManager.fetch({ + const result = await taskManager.fetch({ query: { bool: { filter: { term: { _id: `task:Lens-lens_telemetry` } } } }, - })); + }); + return result.docs; } catch (err) { const errMessage = err && err.message ? err.message : err.toString(); /* @@ -88,12 +122,10 @@ async function fetch(server: Server) { task manager has to wait for all plugins to initialize first. It's fine to ignore it as next time around it will be initialized (or it will throw a different type of error) */ - if (errMessage.includes('NotInitialized')) { - docs = null; - } else { + if (!errMessage.includes('NotInitialized')) { throw err; } } - return docs; + return null; } diff --git a/x-pack/legacy/plugins/lens/server/usage/task.ts b/x-pack/legacy/plugins/lens/server/usage/task.ts index df6e49d357990..f47934b2b2bc3 100644 --- a/x-pack/legacy/plugins/lens/server/usage/task.ts +++ b/x-pack/legacy/plugins/lens/server/usage/task.ts @@ -8,8 +8,9 @@ import { Server, SavedObjectsClient as SavedObjectsClientType } from 'src/legacy import { CoreSetup } from 'src/core/server'; import { CallClusterOptions } from 'src/legacy/core_plugins/elasticsearch'; import { SearchParams, SearchResponse, DeleteDocumentByQueryResponse } from 'elasticsearch'; -import { RunContext } from '../../../task_manager'; +import { RunContext, ConcreteTaskInstance } from '../../../task_manager'; import { getVisualizationCounts } from './visualization_counts'; +import { LensUsage } from './types'; // This task is responsible for running daily and aggregating all the Lens click event objects // into daily rolled-up documents, which will be used in reporting click stats @@ -64,7 +65,7 @@ function scheduleTasks(server: Server) { await taskManager.schedule({ id: TASK_ID, taskType: TELEMETRY_TASK_TYPE, - state: { stats: {}, runs: 0 }, + state: { byDate: {}, saved: {}, runs: 0 }, params: {}, }); } catch (e) { @@ -74,7 +75,13 @@ function scheduleTasks(server: Server) { }); } -async function doWork(server: Server, callCluster: ClusterSearchType & ClusterDeleteType) { +// type LensTaskState = LensUsage | {}; + +async function doWork( + prevState: any, + server: Server, + callCluster: ClusterSearchType & ClusterDeleteType +) { const kibanaIndex = server.config().get('kibana.index'); const metrics = await callCluster('search', { @@ -106,14 +113,12 @@ async function doWork(server: Server, callCluster: ClusterSearchType & ClusterDe size: 0, }); - console.log(JSON.stringify(metrics)); - - const byDateByType: Record> = {}; + const byDateByType: Record> = prevState.byDate || {}; metrics.aggregations.daily.buckets.forEach(bucket => { - const byType: Record = {}; + const byType: Record = byDateByType[bucket.key] || {}; bucket.names.buckets.forEach(({ key, doc_count }) => { - byType[key] = doc_count; + byType[key] = doc_count + (byType[key] || 0); }); byDateByType[bucket.key] = byType; }); @@ -148,7 +153,7 @@ function telemetryTaskRunner(server: Server) { return { async run() { try { - lensTelemetryTask = doWork(server, callCluster); + lensTelemetryTask = doWork(prevState, server, callCluster); lensVisualizationTask = getVisualizationCounts(callCluster, server.config()); } catch (err) { @@ -159,12 +164,9 @@ function telemetryTaskRunner(server: Server) { .then(([lensTelemetry, lensVisualizations]) => { return { state: { - runs: state.runs || 1, - stats: Object.assign( - {}, - lensTelemetry || prevState.stats || {}, - lensVisualizations - ), + runs: (state.runs || 0) + 1, + byDate: lensTelemetry || {}, + saved: lensVisualizations, }, runAt: getNextMidnight(), };