diff --git a/x-pack/legacy/plugins/apm/typings/elasticsearch.ts b/x-pack/legacy/plugins/apm/typings/elasticsearch.ts index c1a8a7f9b3985..8fd902bd49d14 100644 --- a/x-pack/legacy/plugins/apm/typings/elasticsearch.ts +++ b/x-pack/legacy/plugins/apm/typings/elasticsearch.ts @@ -64,11 +64,22 @@ declare module 'elasticsearch' { // eslint-disable-next-line @typescript-eslint/prefer-interface type FiltersAggregation = { - buckets: Array< - { - doc_count: number; - } & SubAggregation - >; + // 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 }; + } + ? { + [key in keyof SubAggregationMap['filters']['filters']]: { + doc_count: number; + } & SubAggregation; + } + : Array< + { + doc_count: number; + } & SubAggregation + >; }; type SamplerAggregation = SubAggregation< diff --git a/x-pack/legacy/plugins/lens/common/clickdata.ts b/x-pack/legacy/plugins/lens/common/clickdata.ts deleted file mode 100644 index 8e64f83ce57d8..0000000000000 --- a/x-pack/legacy/plugins/lens/common/clickdata.ts +++ /dev/null @@ -1,10 +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. - */ - -export interface LensClickEvent { - name: string; - date: string; -} diff --git a/x-pack/legacy/plugins/lens/common/index.ts b/x-pack/legacy/plugins/lens/common/index.ts index f5291a5941dd5..eead93dd33480 100644 --- a/x-pack/legacy/plugins/lens/common/index.ts +++ b/x-pack/legacy/plugins/lens/common/index.ts @@ -6,4 +6,3 @@ export * from './api'; export * from './constants'; -export * from './clickdata'; diff --git a/x-pack/legacy/plugins/lens/mappings.json b/x-pack/legacy/plugins/lens/mappings.json index e33fb5456360d..8304cf9c9cb64 100644 --- a/x-pack/legacy/plugins/lens/mappings.json +++ b/x-pack/legacy/plugins/lens/mappings.json @@ -26,6 +26,9 @@ }, "date": { "type": "date" + }, + "count": { + "type": "integer" } } } diff --git a/x-pack/legacy/plugins/lens/public/lens_ui_telemetry/factory.test.ts b/x-pack/legacy/plugins/lens/public/lens_ui_telemetry/factory.test.ts new file mode 100644 index 0000000000000..babeddd9fb18d --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/lens_ui_telemetry/factory.test.ts @@ -0,0 +1,110 @@ +/* + * 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 { + LensReportManager, + setReportManager, + stopReportManager, + trackUiEvent, + trackSuggestionEvent, +} from './factory'; +import { Storage } from 'src/legacy/core_plugins/data/public/types'; +import { coreMock } from 'src/core/public/mocks'; +import { HttpServiceBase } from 'kibana/public'; + +jest.useFakeTimers(); + +const createMockStorage = () => ({ + // store: createMockWebStorage(), + get: jest.fn(), + set: jest.fn(), + remove: jest.fn(), + clear: jest.fn(), +}); + +describe('Lens UI telemetry', () => { + let storage: jest.Mocked; + let http: jest.Mocked; + + beforeEach(() => { + storage = createMockStorage(); + http = coreMock.createSetup().http; + const fakeManager = new LensReportManager({ + http, + storage, + basePath: '/basepath', + }); + setReportManager(fakeManager); + }); + + afterEach(() => { + stopReportManager(); + }); + + it('should write immediately and track local state', () => { + trackUiEvent('loaded'); + + expect(storage.set).toHaveBeenCalledWith('lens-ui-telemetry', { + clicks: [ + { + name: 'loaded', + date: expect.any(String), + }, + ], + suggestionClicks: [], + }); + + trackSuggestionEvent('reload'); + + expect(storage.set).toHaveBeenLastCalledWith('lens-ui-telemetry', { + clicks: [ + { + name: 'loaded', + date: expect.any(String), + }, + ], + suggestionClicks: [ + { + name: 'reload', + date: expect.any(String), + }, + ], + }); + }); + + it('should post the results after waiting 10 seconds, if there is data', () => { + jest.runTimersToTime(10000); + + expect(http.post).not.toHaveBeenCalled(); + + trackUiEvent('load'); + + jest.runTimersToTime(10000); + + expect(http.post).toHaveBeenCalledWith(`/basepath/api/lens/telemetry`, { + // The contents of the body are not checked here because they depend on time + body: expect.any(String), + }); + + expect(storage.set).toHaveBeenCalledTimes(2); + expect(storage.set).toHaveBeenLastCalledWith('lens-ui-telemetry', { + clicks: [], + suggestionClicks: [], + }); + }); + + it('should keep its local state after an http error', () => { + http.post.mockRejectedValue('http error'); + + trackUiEvent('load'); + expect(storage.set).toHaveBeenCalledTimes(1); + + jest.advanceTimersByTime(10000); + + expect(http.post).toHaveBeenCalled(); + expect(storage.set).toHaveBeenCalledTimes(1); + }); +}); 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 2d344867e047b..c555000719853 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 @@ -4,16 +4,41 @@ * you may not use this file except in compliance with the Elastic License. */ +import moment from 'moment'; import { HttpServiceBase } from 'src/core/public'; import { Storage } from 'src/legacy/core_plugins/data/public/types'; -import { LensClickEvent, BASE_API_URL } from '../../common'; +import { BASE_API_URL } from '../../common'; const STORAGE_KEY = 'lens-ui-telemetry'; +let reportManager: LensReportManager; + +export function setReportManager(newManager: LensReportManager) { + reportManager = newManager; +} + +export function stopReportManager() { + if (reportManager) { + reportManager.stop(); + } +} + +export function trackUiEvent(name: string) { + if (reportManager) { + reportManager.trackEvent(name); + } +} + +export function trackSuggestionEvent(name: string) { + if (reportManager) { + reportManager.trackSuggestionEvent(name); + } +} + export class LensReportManager { - private clicks: LensClickEvent[]; - private suggestionClicks: LensClickEvent[]; + private clicks: Record; + private suggestionClicks: Record; private storage: Storage; private http: HttpServiceBase; @@ -34,34 +59,20 @@ export class LensReportManager { this.basePath = basePath; const unsent = this.storage.get(STORAGE_KEY); - this.clicks = unsent && unsent.clicks ? unsent.clicks : []; - this.suggestionClicks = unsent && unsent.suggestionClicks ? unsent.suggestionClicks : []; + this.clicks = unsent && unsent.clicks ? unsent.clicks : {}; + this.suggestionClicks = unsent && unsent.suggestionClicks ? unsent.suggestionClicks : {}; this.timer = setInterval(() => { - if (this.clicks.length || this.suggestionClicks.length) { - this.postToServer(); - } + this.postToServer(); }, 10000); } public trackEvent(name: string) { - this.clicks.push({ - name, - date: new Date().toISOString(), - }); - this.write(); + this.trackTo(this.clicks, name); } public trackSuggestionEvent(name: string) { - this.suggestionClicks.push({ - name, - date: new Date().toISOString(), - }); - this.write(); - } - - private write() { - this.storage.set(STORAGE_KEY, { clicks: this.clicks, suggestionClicks: this.suggestionClicks }); + this.trackTo(this.suggestionClicks, name); } public stop() { @@ -71,37 +82,39 @@ export class LensReportManager { } private async postToServer() { - try { - await this.http.post(`${this.basePath}${BASE_API_URL}/telemetry`, { - body: JSON.stringify({ - clicks: this.clicks, - suggestionClicks: this.suggestionClicks, - }), - }); - this.clicks = []; - this.suggestionClicks = []; - this.write(); - } catch (e) { - // Maybe show an error - console.log(e); + if (Object.keys(this.clicks).length || Object.keys(this.suggestionClicks).length) { + try { + await this.http.post(`${this.basePath}${BASE_API_URL}/telemetry`, { + body: JSON.stringify({ + clicks: this.clicks, + suggestionClicks: this.suggestionClicks, + }), + }); + this.clicks = {}; + this.suggestionClicks = {}; + this.write(); + } catch (e) { + // Silent error because events will be reported during the next timer + } } } -} - -let reportManager: LensReportManager; -export function setReportManager(newManager: LensReportManager) { - reportManager = newManager; -} + private trackTo(target: Record>, name: string) { + const date = moment().format('YYYY-MM-DD'); + if (!target[date]) { + target[date] = { + [name]: 1, + }; + } else if (!target[date][name]) { + target[date][name] = 1; + } else { + target[date][name] += 1; + } -export function trackUiEvent(name: string) { - if (reportManager) { - reportManager.trackEvent(name); + this.write(); } -} -export function trackSuggestionEvent(name: string) { - if (reportManager) { - reportManager.trackSuggestionEvent(name); + private write() { + this.storage.set(STORAGE_KEY, { clicks: this.clicks, suggestionClicks: this.suggestionClicks }); } } diff --git a/x-pack/legacy/plugins/lens/server/routes/telemetry.test.ts b/x-pack/legacy/plugins/lens/server/routes/telemetry.test.ts new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/x-pack/legacy/plugins/lens/server/routes/telemetry.ts b/x-pack/legacy/plugins/lens/server/routes/telemetry.ts index e7cefe0f1e35d..2bd0c10837eb2 100644 --- a/x-pack/legacy/plugins/lens/server/routes/telemetry.ts +++ b/x-pack/legacy/plugins/lens/server/routes/telemetry.ts @@ -26,7 +26,6 @@ export async function initLensUsageRoute( plugins: { savedObjects: SavedObjectsLegacyService; config: KibanaConfig; - // server: Server; } ) { const router = setup.http.createRouter(); @@ -36,17 +35,10 @@ export async function initLensUsageRoute( validate: { params: schema.object({}), body: schema.object({ - clicks: schema.arrayOf( - schema.object({ - name: schema.string(), - date: schema.string(), - }) - ), - suggestionClicks: schema.arrayOf( - schema.object({ - name: schema.string(), - date: schema.string(), - }) + clicks: schema.mapOf(schema.string(), schema.mapOf(schema.string(), schema.number())), + suggestionClicks: schema.mapOf( + schema.string(), + schema.mapOf(schema.string(), schema.number()) ), }), }, @@ -59,27 +51,40 @@ export async function initLensUsageRoute( try { const client = getSavedObjectsClient(plugins.savedObjects, dataClient.callAsCurrentUser); - const clickEvents = clicks.map(event => ({ - type: 'lens-ui-telemetry', - attributes: { - name: event.name, - type: 'click', - date: event.date, - }, - })); - const suggestionEvents = suggestionClicks.map(event => ({ - type: 'lens-ui-telemetry', - attributes: { - name: event.name, - type: 'suggestion', - date: event.date, - }, - })); + const allEvents: Array<{ + type: 'lens-ui-telemetry'; + attributes: {}; + }> = []; - const events = clickEvents.concat(suggestionEvents); + clicks.forEach((subMap, date) => { + subMap.forEach((count, key) => { + allEvents.push({ + type: 'lens-ui-telemetry', + attributes: { + name: key, + date, + count, + type: 'regular', + }, + }); + }); + }); + suggestionClicks.forEach((subMap, date) => { + subMap.forEach((count, key) => { + allEvents.push({ + type: 'lens-ui-telemetry', + attributes: { + name: key, + date, + count, + type: 'suggestion', + }, + }); + }); + }); - if (events.length > 0) { - await client.bulkCreate(events); + if (allEvents.length) { + await client.bulkCreate(allEvents); } return res.ok({ body: {} }); diff --git a/x-pack/legacy/plugins/lens/server/usage/collectors.ts b/x-pack/legacy/plugins/lens/server/usage/collectors.ts index 60eb3c6e3651f..07c854cb0e681 100644 --- a/x-pack/legacy/plugins/lens/server/usage/collectors.ts +++ b/x-pack/legacy/plugins/lens/server/usage/collectors.ts @@ -86,8 +86,8 @@ export function registerLensUsageCollector( ...state.saved, events_30_days: eventsLast30, events_90_days: eventsLast90, - suggestions_last_30_days: suggestionsLast30, - suggestions_last_90_days: suggestionsLast90, + suggestion_events_30_days: suggestionsLast30, + suggestion_events_90_days: suggestionsLast90, }; } catch (err) { return { diff --git a/x-pack/legacy/plugins/lens/server/usage/task.test.ts b/x-pack/legacy/plugins/lens/server/usage/task.test.ts new file mode 100644 index 0000000000000..7197e139aa255 --- /dev/null +++ b/x-pack/legacy/plugins/lens/server/usage/task.test.ts @@ -0,0 +1,53 @@ +/* + * 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 moment from 'moment'; +import { Server } from 'src/legacy/server/kbn_server'; +import { TaskInstance, RunContext, ConcreteTaskInstance } from '../../../task_manager'; +import { getMockCallWithInternal, getMockKbnServer } from '../../../oss_telemetry/test_utils'; +import { telemetryTaskRunner } from './task'; + +function getMockTaskInstance() { + return { + state: { + runs: 1, + byType: {}, + suggestionsByType: {}, + saved: {}, + }, + params: {}, + taskType: 'lens-ui-telemetry', + }; +} + +describe('lensTaskRunner', () => { + let mockTaskInstance: RunContext; + let mockKbnServer: Server; + beforeEach(() => { + mockTaskInstance = getMockTaskInstance(); + mockKbnServer = getMockKbnServer(); + }); + + describe('Error handling', () => { + test('catches its own errors', async () => { + const mockCallWithInternal = () => Promise.reject(new Error('Things did not go well!')); + mockKbnServer = getMockKbnServer(mockCallWithInternal); + + const runner = telemetryTaskRunner(mockKbnServer); + const { run } = await runner({ + taskInstance: mockTaskInstance, + }); + const result = await run(); + expect(result).toMatchObject({ + error: 'Things did not go well!', + state: { + runs: 1, + stats: undefined, + }, + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/lens/server/usage/task.ts b/x-pack/legacy/plugins/lens/server/usage/task.ts index 3fcbb26001fc3..982b40341422d 100644 --- a/x-pack/legacy/plugins/lens/server/usage/task.ts +++ b/x-pack/legacy/plugins/lens/server/usage/task.ts @@ -5,13 +5,19 @@ */ import moment from 'moment'; -import { Server } from 'src/legacy/server/kbn_server'; +import KbnServer, { Server } from 'src/legacy/server/kbn_server'; import { CoreSetup } from 'src/core/server'; import { CallClusterOptions } from 'src/legacy/core_plugins/elasticsearch'; -import { SearchParams, SearchResponse, DeleteDocumentByQueryResponse } from 'elasticsearch'; -import { RunContext, ConcreteTaskInstance } from '../../../task_manager'; +import { + SearchParams, + SearchResponse, + DeleteDocumentByQueryResponse, + AggregationSearchResponse, +} from 'elasticsearch'; +import { XPackMainPlugin } from '../../../xpack_main/xpack_main'; +import { RunContext } from '../../../task_manager'; +import { LensTelemetryState } from './types'; 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 @@ -52,7 +58,9 @@ function registerLensTelemetryTask(core: CoreSetup, { server }: { server: Server function scheduleTasks(server: Server) { const taskManager = server.plugins.task_manager; - const { kbnServer } = server.plugins.xpack_main.status.plugin; + const { kbnServer } = (server.plugins.xpack_main as XPackMainPlugin & { + status: { plugin: { kbnServer: KbnServer } }; + }).status.plugin; kbnServer.afterPluginsInit(() => { // The code block below can't await directly within "afterPluginsInit" @@ -63,7 +71,7 @@ function scheduleTasks(server: Server) { // function block. (async () => { try { - await taskManager.schedule({ + await taskManager!.schedule({ id: TASK_ID, taskType: TELEMETRY_TASK_TYPE, state: { byDate: {}, suggestionsByDate: {}, saved: {}, runs: 0 }, @@ -76,55 +84,71 @@ function scheduleTasks(server: Server) { }); } -// type LensTaskState = LensUsage | {}; - export async function doWork( - prevState: any, + prevState: LensTelemetryState, server: Server, callCluster: ClusterSearchType & ClusterDeleteType -) { +): Promise<{ + byDate: Record>; + suggestionsByDate: Record>; +}> { const kibanaIndex = server.config().get('kibana.index'); - const metrics = await callCluster('search', { - index: kibanaIndex, - rest_total_hits_as_int: true, - body: { - query: { - bool: { - filter: [ - { term: { type: 'lens-ui-telemetry' } }, - { range: { updated_at: { gte: 'now-90d/d' } } }, - ], - }, + const aggs = { + daily: { + date_histogram: { + field: 'lens-ui-telemetry.events.date', + calendar_interval: '1d', }, aggs: { - daily: { - date_histogram: { - field: 'updated_at', - calendar_interval: '1d', - }, - aggs: { - groups: { - filters: { - filters: { - suggestionEvent: { - bool: { filter: { term: { 'lens-ui-telemetry.type': 'suggestion' } } }, - }, - regularEvents: { - bool: { must_not: { term: { 'lens-ui-telemetry.type': 'suggestion' } } }, + groups: { + filters: { + filters: { + suggestionEvents: { + bool: { + filter: { + term: { 'lens-ui-telemetry.type': 'suggestion' }, }, }, }, - aggs: { - names: { - terms: { field: 'lens-ui-telemetry.name', size: 100 }, + regularEvents: { + bool: { + must_not: { + term: { 'lens-ui-telemetry.type': 'suggestion' }, + }, }, }, }, }, + aggs: { + names: { + terms: { field: 'lens-ui-telemetry.name', size: 100 }, + }, + }, }, }, }, + }; + + const metrics: AggregationSearchResponse< + unknown, + { + body: { aggs: typeof aggs }; + } + > = await callCluster('search', { + index: kibanaIndex, + rest_total_hits_as_int: true, + body: { + query: { + bool: { + filter: [ + { term: { type: 'lens-ui-telemetry' } }, + { range: { 'lens-ui-telemetry.date': { gte: 'now-90d/d' } } }, + ], + }, + }, + aggs, + }, size: 0, }); @@ -135,7 +159,6 @@ export async function doWork( Object.keys(byDateByType).forEach(key => { // Unix time if (moment(key, 'x').isBefore(moment().subtract(30, 'days'))) { - // Remove this key delete byDateByType[key]; return; } @@ -144,22 +167,23 @@ export async function doWork( Object.keys(suggestionsByDate).forEach(key => { // Unix time if (moment(key, 'x').isBefore(moment().subtract(30, 'days'))) { - // Remove this key delete suggestionsByDate[key]; return; } }); - metrics.aggregations.daily.buckets.forEach(daily => { + // TODO: These metrics are counting total matching docs, but each doc might + // represent multiple events + metrics.aggregations!.daily.buckets.forEach(daily => { const byType: Record = byDateByType[daily.key] || {}; - daily.groups.buckets.regularEvents.names.buckets.forEach(({ key, doc_count }) => { - byType[key] = doc_count + (byType[daily.key] || 0); + daily.groups.buckets.regularEvents.names.buckets.forEach(bucket => { + byType[bucket.key] = bucket.doc_count + (byType[daily.key] || 0); }); byDateByType[daily.key] = byType; const suggestionsByType: Record = suggestionsByDate[daily.key] || {}; - daily.groups.buckets.suggestionEvent.names.buckets.forEach(({ key, doc_count }) => { - suggestionsByType[key] = doc_count + (byType[daily.key] || 0); + daily.groups.buckets.suggestionEvents.names.buckets.forEach(bucket => { + suggestionsByType[bucket.key] = bucket.doc_count + (byType[daily.key] || 0); }); suggestionsByDate[daily.key] = suggestionsByType; }); @@ -184,18 +208,18 @@ export async function doWork( }; } -function telemetryTaskRunner(server: Server) { +export function telemetryTaskRunner(server: Server) { return ({ taskInstance }: RunContext) => { const { state } = taskInstance; - const prevState = state; + const prevState = state as LensTelemetryState; const callCluster = server.plugins.elasticsearch.getCluster('admin').callWithInternalUser; - let lensTelemetryTask: Promise; - let lensVisualizationTask: ReturnType; - return { async run() { + let lensTelemetryTask; + let lensVisualizationTask; + try { lensTelemetryTask = doWork(prevState, server, callCluster); @@ -206,12 +230,6 @@ function telemetryTaskRunner(server: Server) { return Promise.all([lensTelemetryTask, lensVisualizationTask]) .then(([lensTelemetry, lensVisualizations]) => { - console.log({ - runs: (state.runs || 0) + 1, - byDate: (lensTelemetry && lensTelemetry.byDate) || {}, - suggestionsByDate: (lensTelemetry && lensTelemetry.suggestionsByDate) || {}, - saved: lensVisualizations, - }); return { state: { runs: (state.runs || 0) + 1, diff --git a/x-pack/legacy/plugins/xpack_main/xpack_main.d.ts b/x-pack/legacy/plugins/xpack_main/xpack_main.d.ts index 8ae2801dededb..2a197811cc032 100644 --- a/x-pack/legacy/plugins/xpack_main/xpack_main.d.ts +++ b/x-pack/legacy/plugins/xpack_main/xpack_main.d.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import KbnServer from 'src/legacy/server/kbn_server'; import { Feature, FeatureWithAllOrReadPrivileges } from '../../../plugins/features/server'; import { XPackInfo, XPackInfoOptions } from './server/lib/xpack_info'; export { XPackFeature } from './server/lib/xpack_info'; diff --git a/x-pack/test/api_integration/apis/lens/telemetry.ts b/x-pack/test/api_integration/apis/lens/telemetry.ts new file mode 100644 index 0000000000000..01ec10d78075f --- /dev/null +++ b/x-pack/test/api_integration/apis/lens/telemetry.ts @@ -0,0 +1,79 @@ +/* + * 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 expect from '@kbn/expect'; +import { Client, SearchParams } from 'elasticsearch'; + +import { FtrProviderContext } from '../../ftr_provider_context'; + +// const TEST_START_TIME = '2015-09-19T06:31:44.000'; +// const TEST_END_TIME = '2015-09-23T18:31:44.000'; +const COMMON_HEADERS = { + 'kbn-xsrf': 'some-xsrf-token', +}; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const es: Client = getService('es'); + + describe('lens telemetry', () => { + afterEach(() => { + es.deleteByQuery({ + index: '.kibana', + q: 'type:lens-ui-telemetry', + }); + }); + + it('should do nothing on empty post', () => { + await supertest + .post('/api/lens/telemetry') + .set(COMMON_HEADERS) + .send({ + events: {}, + suggestionEvents: {}, + }) + .expect(200); + + const { count } = await es.count({ + index: '.kibana', + q: 'type:lens-ui-telemetry', + }); + + expect(count).to.be(0); + }); + + it('should write a document per results', async () => { + await supertest + .post('/api/lens/telemetry') + .set(COMMON_HEADERS) + .send({ + events: { + '2019-10-13': { + loaded: 5, + dragged: 2, + }, + '2019-10-14': { + loaded: 1, + }, + }, + suggestionEvents: { + '2019-11-01': { + switched: 2, + }, + }, + }) + .expect(200); + + const { count } = await es.count({ + index: '.kibana', + q: 'type:lens-ui-telemetry', + }); + + expect(count).to.be(4); + }); + }); +};