[Step 2] VisEditors Telemetry enhancements (remove legacy Lens telemetries) (#135684)

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Joe Reuter <johannes.reuter@elastic.co>
This commit is contained in:
Alexey Antonov 2022-07-25 13:09:29 +03:00 committed by GitHub
parent ee3cfb6d75
commit 51816a0631
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 7 additions and 2349 deletions

View file

@ -5,8 +5,7 @@
* 2.0.
*/
import { Plugin, CoreSetup, CoreStart, PluginInitializerContext, Logger } from '@kbn/core/server';
import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server';
import { Plugin, CoreSetup, CoreStart, PluginInitializerContext } from '@kbn/core/server';
import { PluginStart as DataViewsServerPluginStart } from '@kbn/data-views-plugin/server';
import {
PluginStart as DataPluginStart,
@ -23,18 +22,12 @@ import {
import { EmbeddableSetup } from '@kbn/embeddable-plugin/server';
import { setupRoutes } from './routes';
import { getUiSettings } from './ui_settings';
import {
registerLensUsageCollector,
initializeLensTelemetry,
scheduleLensTelemetry,
} from './usage';
import { setupSavedObjects } from './saved_objects';
import { setupExpressions } from './expressions';
import { makeLensEmbeddableFactory } from './embeddable/make_lens_embeddable_factory';
import type { CustomVisualizationMigrations } from './migrations/types';
export interface PluginSetupContract {
usageCollection?: UsageCollectionSetup;
taskManager?: TaskManagerSetupContract;
embeddable: EmbeddableSetup;
expressions: ExpressionsServerSetup;
@ -63,12 +56,9 @@ export interface LensServerPluginSetup {
}
export class LensServerPlugin implements Plugin<LensServerPluginSetup, {}, {}, {}> {
private readonly telemetryLogger: Logger;
private customVisualizationMigrations: CustomVisualizationMigrations = {};
constructor(private initializerContext: PluginInitializerContext) {
this.telemetryLogger = initializerContext.logger.get('usage');
}
constructor(private initializerContext: PluginInitializerContext) {}
setup(core: CoreSetup<PluginStartContract>, plugins: PluginSetupContract) {
const getFilterMigrations = plugins.data.query.filterManager.getAllMigrations.bind(
@ -79,16 +69,6 @@ export class LensServerPlugin implements Plugin<LensServerPluginSetup, {}, {}, {
setupExpressions(core, plugins.expressions);
core.uiSettings.register(getUiSettings());
if (plugins.usageCollection && plugins.taskManager) {
registerLensUsageCollector(
plugins.usageCollection,
core
.getStartServices()
.then(([_, { taskManager }]) => taskManager as TaskManagerStartContract)
);
initializeLensTelemetry(this.telemetryLogger, core, plugins.taskManager);
}
const lensEmbeddableFactory = makeLensEmbeddableFactory(
getFilterMigrations,
this.customVisualizationMigrations
@ -109,9 +89,6 @@ export class LensServerPlugin implements Plugin<LensServerPluginSetup, {}, {}, {
}
start(core: CoreStart, plugins: PluginStartContract) {
if (plugins.taskManager) {
scheduleLensTelemetry(this.telemetryLogger, plugins.taskManager);
}
return {};
}

View file

@ -9,10 +9,8 @@ import { CoreSetup, Logger } from '@kbn/core/server';
import { PluginStartContract } from '../plugin';
import { existingFieldsRoute } from './existing_fields';
import { initFieldsRoute } from './field_stats';
import { initLensUsageRoute } from './telemetry';
export function setupRoutes(setup: CoreSetup<PluginStartContract>, logger: Logger) {
existingFieldsRoute(setup, logger);
initFieldsRoute(setup);
initLensUsageRoute(setup);
}

View file

@ -1,93 +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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { errors } from '@elastic/elasticsearch';
import { CoreSetup } from '@kbn/core/server';
import { schema } from '@kbn/config-schema';
import { SavedObjectsErrorHelpers } from '@kbn/core/server';
import { BASE_API_URL } from '../../common';
import { PluginStartContract } from '../plugin';
// This route is responsible for taking a batch of click events from the browser
// and writing them to saved objects
export async function initLensUsageRoute(setup: CoreSetup<PluginStartContract>) {
const router = setup.http.createRouter();
router.post(
{
path: `${BASE_API_URL}/stats`,
validate: {
body: schema.object({
events: schema.mapOf(schema.string(), schema.mapOf(schema.string(), schema.number())),
suggestionEvents: schema.mapOf(
schema.string(),
schema.mapOf(schema.string(), schema.number())
),
}),
},
},
async (context, req, res) => {
const { events, suggestionEvents } = req.body;
try {
const client = (await context.core).savedObjects.client;
const allEvents: Array<{
type: 'lens-ui-telemetry';
attributes: {};
}> = [];
events.forEach((subMap, date) => {
subMap.forEach((count, key) => {
allEvents.push({
type: 'lens-ui-telemetry',
attributes: {
name: key,
date,
count,
type: 'regular',
},
});
});
});
suggestionEvents.forEach((subMap, date) => {
subMap.forEach((count, key) => {
allEvents.push({
type: 'lens-ui-telemetry',
attributes: {
name: key,
date,
count,
type: 'suggestion',
},
});
});
});
if (allEvents.length) {
await client.bulkCreate(allEvents);
}
return res.ok({ body: {} });
} catch (e) {
if (SavedObjectsErrorHelpers.isForbiddenError(e)) {
return res.forbidden();
}
if (e instanceof errors.ResponseError && e.statusCode === 404) {
return res.notFound();
}
if (e.isBoom) {
if (e.output.statusCode === 404) {
return res.notFound();
}
throw new Error(e.output.message);
} else {
throw e;
}
}
}
);
}

View file

@ -1,119 +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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import moment from 'moment';
import { get } from 'lodash';
import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server';
import { TaskManagerStartContract } from '@kbn/task-manager-plugin/server';
import { LensUsage, LensTelemetryState } from './types';
import { lensUsageSchema } from './schema';
const emptyUsageCollection = {
saved_multiterms_overall: {},
saved_multiterms_30_days: {},
saved_multiterms_90_days: {},
saved_overall: {},
saved_30_days: {},
saved_90_days: {},
saved_overall_total: 0,
saved_30_days_total: 0,
saved_90_days_total: 0,
events_30_days: {},
events_90_days: {},
suggestion_events_30_days: {},
suggestion_events_90_days: {},
};
export function registerLensUsageCollector(
usageCollection: UsageCollectionSetup,
taskManager: Promise<TaskManagerStartContract>
) {
const lensUsageCollector = usageCollection.makeUsageCollector<LensUsage>({
type: 'lens',
async fetch() {
try {
const docs = await getLatestTaskState(await taskManager);
// get the accumulated state from the recurring task
const state: LensTelemetryState = get(docs, '[0].state');
const events = getDataByDate(state.byDate);
const suggestions = getDataByDate(state.suggestionsByDate);
return {
...emptyUsageCollection,
...state.saved,
...state.multiterms,
events_30_days: events.last30,
events_90_days: events.last90,
suggestion_events_30_days: suggestions.last30,
suggestion_events_90_days: suggestions.last90,
};
} catch (err) {
return emptyUsageCollection;
}
},
isReady: async () => {
await taskManager;
return true;
},
schema: lensUsageSchema,
});
usageCollection.registerCollector(lensUsageCollector);
}
function addEvents(prevEvents: Record<string, number>, newEvents: Record<string, number>) {
Object.keys(newEvents).forEach((key) => {
prevEvents[key] = (prevEvents[key] || 0) + newEvents[key];
});
}
async function getLatestTaskState(taskManager: TaskManagerStartContract) {
try {
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();
/*
The usage service WILL to try to fetch from this collector before the task manager has been initialized, because the
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')) {
throw err;
}
}
return null;
}
function getDataByDate(dates: Record<string, Record<string, number>>) {
const byDate = Object.keys(dates || {}).map((dateStr) => parseInt(dateStr, 10));
const last30: Record<string, number> = {};
const last90: Record<string, number> = {};
const last30Timestamp = moment().subtract(30, 'days').unix();
const last90Timestamp = moment().subtract(90, 'days').unix();
byDate.forEach((dateKey) => {
if (dateKey >= last30Timestamp) {
addEvents(last30, dates[dateKey]);
addEvents(last90, dates[dateKey]);
} else if (dateKey > last90Timestamp) {
addEvents(last90, dates[dateKey]);
}
});
return {
last30,
last90,
};
}

View file

@ -1,9 +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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export * from './collectors';
export * from './task';

View file

@ -1,106 +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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { ElasticsearchClient } from '@kbn/core/server';
import { createMetricQuery } from './saved_objects_metric_factory';
import { LensMultitermsUsage } from './types';
export async function getMultitermsCounts(
getEsClient: () => Promise<ElasticsearchClient>,
kibanaIndex: string
): Promise<LensMultitermsUsage> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function bucketsToObject(arg: any) {
const obj: Record<string, number> = {};
if (arg.multitermsDocs.doc_count > 0) {
obj.multiterms_docs = arg.multitermsDocs.doc_count;
obj.multiterms_terms_count = arg.multitermsTermsCount.value;
obj.multiterms_operations_count = arg.multitermsOperationsCount.value;
}
return obj;
}
const forEachMultitermsOperationScript = (operationToApply: string) => {
return `
try {
if(doc['lens.state'].size() == 0) return;
HashMap layers = params['_source'].get('lens').get('state').get('datasourceStates').get('indexpattern').get('layers');
for(layerId in layers.keySet()) {
HashMap columns = layers.get(layerId).get('columns');
for(columnId in columns.keySet()) {
if(columns.get(columnId).get('operationType') == 'terms'){
if(columns.get(columnId).get('params').get('secondaryFields').size() > 0){
${operationToApply}
}
}
}
}
} catch(Exception e) {}`;
};
const fn = createMetricQuery(getEsClient, kibanaIndex);
const result = await fn({
aggregations: {
multitermsOperationsCount: {
sum: {
field: 'multiterms_operations_count',
},
},
multitermsTermsCount: {
sum: {
field: 'multiterms_count',
},
},
multitermsDocs: {
filter: {
match: {
operation_type: 'multiterms',
},
},
},
},
runtimeMappings: {
operation_type: {
type: 'keyword',
script: {
lang: 'painless',
source: forEachMultitermsOperationScript("emit('multiterms');"),
},
},
multiterms_count: {
type: 'double',
script: {
lang: 'painless',
source: `
double terms = 0;
${forEachMultitermsOperationScript(
"terms += columns.get(columnId).get('params').get('secondaryFields').size() + 1;"
)}
emit(terms);`,
},
},
multiterms_operations_count: {
type: 'double',
script: {
lang: 'painless',
source: `
double operations = 0;
${forEachMultitermsOperationScript('operations += 1;')}
emit(operations);`,
},
},
},
bucketsToObject,
});
// remap the result with the multiterms shape
return {
saved_multiterms_overall: result.saved_overall,
saved_multiterms_30_days: result.saved_30_days,
saved_multiterms_90_days: result.saved_90_days,
};
}

View file

@ -1,84 +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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import {
AggregationsAggregationContainer,
MappingRuntimeFields,
} from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { ElasticsearchClient } from '@kbn/core/server';
import { GenericSavedUsage } from './types';
export function createMetricQuery(
getEsClient: () => Promise<ElasticsearchClient>,
kibanaIndex: string
) {
return async function ({
aggregations,
runtimeMappings,
bucketsToObject,
}: {
aggregations: Record<string, AggregationsAggregationContainer>;
runtimeMappings?: MappingRuntimeFields;
bucketsToObject?: (arg: unknown) => Record<string, number>;
}): Promise<GenericSavedUsage> {
const esClient = await getEsClient();
const results = await esClient.search({
index: kibanaIndex,
body: {
query: {
bool: {
filter: [{ term: { type: 'lens' } }],
},
},
aggs: {
groups: {
filters: {
filters: {
last30: { bool: { filter: { range: { updated_at: { gte: 'now-30d' } } } } },
last90: { bool: { filter: { range: { updated_at: { gte: 'now-90d' } } } } },
overall: { match_all: {} },
},
},
aggs: {
...aggregations,
},
},
},
runtime_mappings: {
...runtimeMappings,
},
size: 0,
},
});
// @ts-expect-error specify aggregations type explicitly
const buckets = results.aggregations.groups.buckets;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function bucketsToObjectFallback(arg: any) {
const obj: Record<string, number> = {};
const key = Object.keys(arg).find((argKey) => arg[argKey]?.buckets?.length);
if (key) {
arg[key].buckets.forEach((bucket: { key: string; doc_count: number }) => {
obj[bucket.key] = bucket.doc_count + (obj[bucket.key] ?? 0);
});
}
return obj;
}
const mapFn = bucketsToObject ?? bucketsToObjectFallback;
return {
saved_overall: mapFn(buckets.overall),
saved_30_days: mapFn(buckets.last30),
saved_90_days: mapFn(buckets.last90),
saved_overall_total: buckets.overall.doc_count,
saved_30_days_total: buckets.last30.doc_count,
saved_90_days_total: buckets.last90.doc_count,
};
};
}

View file

@ -1,262 +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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { MakeSchemaFrom } from '@kbn/usage-collection-plugin/server';
import { LensUsage } from './types';
const eventsSchema: MakeSchemaFrom<LensUsage['events_30_days']> = {
app_query_change: { type: 'long' },
open_help_popover: {
type: 'long',
_meta: { description: 'Number of times the user opened one of the in-product help popovers.' },
},
error_fix_action: {
type: 'long',
_meta: {
description:
'Number of times the user used the fix action of an error displayed in the workspace.',
},
},
open_formula_popover: {
type: 'long',
_meta: { description: 'Number of times the user opened the in-product formula help popover.' },
},
toggle_autoapply: {
type: 'long',
_meta: {
description: 'Number of times the user toggled auto-apply.',
},
},
toggle_fullscreen_formula: {
type: 'long',
_meta: {
description: 'Number of times the user toggled fullscreen mode on formula.',
},
},
indexpattern_field_info_click: { type: 'long' },
loaded: { type: 'long' },
app_filters_updated: { type: 'long' },
app_date_change: { type: 'long' },
save_failed: { type: 'long' },
loaded_404: { type: 'long' },
drop_total: { type: 'long' },
chart_switch: { type: 'long' },
suggestion_confirmed: { type: 'long' },
suggestion_clicked: { type: 'long' },
drop_onto_workspace: { type: 'long' },
drop_non_empty: { type: 'long' },
drop_empty: { type: 'long' },
indexpattern_changed: { type: 'long' },
indexpattern_filters_cleared: { type: 'long' },
indexpattern_type_filter_toggled: { type: 'long' },
indexpattern_existence_toggled: { type: 'long' },
indexpattern_show_all_fields_clicked: { type: 'long' },
drop_onto_dimension: { type: 'long' },
indexpattern_dimension_removed: { type: 'long' },
indexpattern_dimension_field_changed: { type: 'long' },
xy_change_layer_display: { type: 'long' },
xy_layer_removed: { type: 'long' },
xy_layer_added: { type: 'long' },
open_field_editor_edit: {
type: 'long',
_meta: {
description:
'Number of times the user opened the editor flyout to edit a field from within Lens.',
},
},
open_field_editor_add: {
type: 'long',
_meta: {
description:
'Number of times the user opened the editor flyout to add a field from within Lens.',
},
},
save_field_edit: {
type: 'long',
_meta: {
description: 'Number of times the user edited a field from within Lens.',
},
},
save_field_add: {
type: 'long',
_meta: {
description: 'Number of times the user added a field from within Lens.',
},
},
open_field_delete_modal: {
type: 'long',
_meta: {
description: 'Number of times the user opened the field delete modal from within Lens.',
},
},
delete_field: {
type: 'long',
_meta: {
description: 'Number of times the user deleted a field from within Lens.',
},
},
indexpattern_dimension_operation_terms: {
type: 'long',
_meta: {
description: 'Number of times the top values function was selected',
},
},
indexpattern_dimension_operation_date_histogram: {
type: 'long',
_meta: {
description: 'Number of times the date histogram function was selected',
},
},
indexpattern_dimension_operation_avg: {
type: 'long',
_meta: {
description: 'Number of times the average function was selected',
},
},
indexpattern_dimension_operation_min: {
type: 'long',
_meta: {
description: 'Number of times the min function was selected',
},
},
indexpattern_dimension_operation_max: {
type: 'long',
_meta: {
description: 'Number of times the max function was selected',
},
},
indexpattern_dimension_operation_sum: {
type: 'long',
_meta: {
description: 'Number of times the sum function was selected',
},
},
indexpattern_dimension_operation_count: {
type: 'long',
_meta: {
description: 'Number of times the count function was selected',
},
},
indexpattern_dimension_operation_cardinality: {
type: 'long',
_meta: {
description: 'Number of times the cardinality function was selected',
},
},
indexpattern_dimension_operation_filters: {
type: 'long',
_meta: {
description: 'Number of times the filters function was selected',
},
},
indexpattern_dimension_operation_range: {
type: 'long',
_meta: { description: 'Number of times the range function was selected' },
},
indexpattern_dimension_operation_median: {
type: 'long',
_meta: { description: 'Number of times the median function was selected' },
},
indexpattern_dimension_operation_percentile: {
type: 'long',
_meta: { description: 'Number of times the percentile function was selected' },
},
indexpattern_dimension_operation_last_value: {
type: 'long',
_meta: { description: 'Number of times the last value function was selected' },
},
indexpattern_dimension_operation_cumulative_sum: {
type: 'long',
_meta: { description: 'Number of times the cumulative sum function was selected' },
},
indexpattern_dimension_operation_counter_rate: {
type: 'long',
_meta: { description: 'Number of times the counter rate function was selected' },
},
indexpattern_dimension_operation_derivative: {
type: 'long',
_meta: { description: 'Number of times the derivative function was selected' },
},
indexpattern_dimension_operation_moving_average: {
type: 'long',
_meta: { description: 'Number of times the moving average function was selected' },
},
indexpattern_dimension_operation_formula: {
type: 'long',
_meta: { description: 'Number of times the formula function was selected' },
},
};
const suggestionEventsSchema: MakeSchemaFrom<LensUsage['suggestion_events_30_days']> = {
back_to_current: { type: 'long' },
reload: { type: 'long' },
};
const savedSchema: MakeSchemaFrom<LensUsage['saved_overall']> = {
bar: { type: 'long' },
bar_horizontal: { type: 'long' },
line: { type: 'long' },
area: { type: 'long' },
bar_stacked: { type: 'long' },
bar_percentage_stacked: { type: 'long' },
bar_horizontal_stacked: { type: 'long' },
bar_horizontal_percentage_stacked: { type: 'long' },
area_stacked: { type: 'long' },
area_percentage_stacked: { type: 'long' },
lnsDatatable: { type: 'long' },
lnsPie: { type: 'long' },
lnsMetric: { type: 'long' },
formula: {
type: 'long',
_meta: {
description: 'Number of saved lens visualizations which are using at least one formula',
},
},
};
const savedMultitermsSchema: MakeSchemaFrom<LensUsage['saved_multiterms_overall']> = {
multiterms_docs: {
type: 'long',
_meta: {
description:
'Number of saved lens visualizations which are using at least one multiterms operation',
},
},
multiterms_terms_count: {
type: 'long',
_meta: {
description: 'Sum of terms used for multiterms operations of saved lens visualizations',
},
},
multiterms_operations_count: {
type: 'long',
_meta: {
description: 'Sum of operations using multiterms of saved lens visualizations',
},
},
};
export const lensUsageSchema: MakeSchemaFrom<LensUsage> = {
// LensClickUsage
events_30_days: eventsSchema,
events_90_days: eventsSchema,
suggestion_events_30_days: suggestionEventsSchema,
suggestion_events_90_days: suggestionEventsSchema,
// LensVisualizationUsage
saved_overall_total: { type: 'long' },
saved_30_days_total: { type: 'long' },
saved_90_days_total: { type: 'long' },
saved_overall: savedSchema,
saved_30_days: savedSchema,
saved_90_days: savedSchema,
saved_multiterms_overall: savedMultitermsSchema,
saved_multiterms_30_days: savedMultitermsSchema,
saved_multiterms_90_days: savedMultitermsSchema,
};

View file

@ -1,215 +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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { CoreSetup, Logger, ElasticsearchClient } from '@kbn/core/server';
import moment from 'moment';
import {
RunContext,
TaskManagerSetupContract,
TaskManagerStartContract,
} from '@kbn/task-manager-plugin/server';
import { ESSearchResponse } from '@kbn/core/types/elasticsearch';
import { getVisualizationCounts } from './visualization_counts';
import { getMultitermsCounts } from './multiterms_count';
// 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
const TELEMETRY_TASK_TYPE = 'lens_telemetry';
export const TASK_ID = `Lens-${TELEMETRY_TASK_TYPE}`;
export function initializeLensTelemetry(
logger: Logger,
core: CoreSetup,
taskManager: TaskManagerSetupContract
) {
registerLensTelemetryTask(logger, core, taskManager);
}
export function scheduleLensTelemetry(logger: Logger, taskManager?: TaskManagerStartContract) {
if (taskManager) {
scheduleTasks(logger, taskManager);
}
}
function registerLensTelemetryTask(
logger: Logger,
core: CoreSetup,
taskManager: TaskManagerSetupContract
) {
taskManager.registerTaskDefinitions({
[TELEMETRY_TASK_TYPE]: {
title: 'Lens usage fetch task',
timeout: '1m',
createTaskRunner: telemetryTaskRunner(logger, core),
},
});
}
async function scheduleTasks(logger: Logger, taskManager: TaskManagerStartContract) {
try {
await taskManager.ensureScheduled({
id: TASK_ID,
taskType: TELEMETRY_TASK_TYPE,
state: { byDate: {}, suggestionsByDate: {}, saved: {}, runs: 0 },
params: {},
});
} catch (e) {
logger.debug(`Error scheduling task, received ${e.message}`);
}
}
export async function getDailyEvents(
kibanaIndex: string,
getEsClient: () => Promise<ElasticsearchClient>
): Promise<{
byDate: Record<string, Record<string, number>>;
suggestionsByDate: Record<string, Record<string, number>>;
}> {
const esClient = await getEsClient();
const aggs = {
daily: {
date_histogram: {
field: 'lens-ui-telemetry.date',
calendar_interval: '1d' as const,
min_doc_count: 1,
},
aggs: {
groups: {
filters: {
filters: {
suggestionEvents: {
bool: {
filter: {
term: { 'lens-ui-telemetry.type': 'suggestion' },
},
},
},
regularEvents: {
bool: {
must_not: {
term: { 'lens-ui-telemetry.type': 'suggestion' },
},
},
},
},
},
aggs: {
names: {
terms: { field: 'lens-ui-telemetry.name', size: 100 },
aggs: {
sums: { sum: { field: 'lens-ui-telemetry.count' } },
},
},
},
},
},
},
};
const metrics = await esClient.search<ESSearchResponse<unknown, { body: { aggs: typeof aggs } }>>(
{
index: kibanaIndex,
body: {
query: {
bool: {
filter: [
{ term: { type: 'lens-ui-telemetry' } },
{ range: { 'lens-ui-telemetry.date': { gte: 'now-90d/d' } } },
],
},
},
aggs,
},
size: 0,
}
);
const byDateByType: Record<string, Record<string, number>> = {};
const suggestionsByDate: Record<string, Record<string, number>> = {};
// @ts-expect-error no way to declare aggregations for search response
metrics.aggregations!.daily.buckets.forEach((daily) => {
const byType: Record<string, number> = byDateByType[daily.key] || {};
// @ts-expect-error no way to declare aggregations for search response
daily.groups.buckets.regularEvents.names.buckets.forEach((bucket) => {
byType[bucket.key] = (bucket.sums.value || 0) + (byType[daily.key] || 0);
});
byDateByType[daily.key] = byType;
const suggestionsByType: Record<string, number> = suggestionsByDate[daily.key] || {};
// @ts-expect-error no way to declare aggregations for search response
daily.groups.buckets.suggestionEvents.names.buckets.forEach((bucket) => {
suggestionsByType[bucket.key] =
(bucket.sums.value || 0) + (suggestionsByType[daily.key] || 0);
});
suggestionsByDate[daily.key] = suggestionsByType;
});
// Always delete old date because we don't report it
await esClient.deleteByQuery({
index: kibanaIndex,
wait_for_completion: true,
body: {
query: {
bool: {
filter: [
{ term: { type: 'lens-ui-telemetry' } },
{ range: { 'lens-ui-telemetry.date': { lt: 'now-90d/d' } } },
],
},
},
},
});
return {
byDate: byDateByType,
suggestionsByDate,
};
}
export function telemetryTaskRunner(logger: Logger, core: CoreSetup) {
return ({ taskInstance }: RunContext) => {
const { state } = taskInstance;
const getEsClient = async () => {
const [coreStart] = await core.getStartServices();
return coreStart.elasticsearch.client.asInternalUser;
};
return {
async run() {
const kibanaIndex = core.savedObjects.getKibanaIndex();
return Promise.all([
getDailyEvents(kibanaIndex, getEsClient),
getVisualizationCounts(getEsClient, kibanaIndex),
getMultitermsCounts(getEsClient, kibanaIndex),
])
.then(([lensTelemetry, lensVisualizations, lensMultiterms]) => {
return {
state: {
runs: (state.runs || 0) + 1,
byDate: (lensTelemetry && lensTelemetry.byDate) || {},
suggestionsByDate: (lensTelemetry && lensTelemetry.suggestionsByDate) || {},
saved: lensVisualizations,
multiterms: lensMultiterms,
},
runAt: getNextMidnight(),
};
})
.catch((errMsg) => logger.warn(`Error executing lens telemetry task: ${errMsg}`));
},
async cancel() {},
};
};
}
function getNextMidnight() {
return moment().add(1, 'day').startOf('day').toDate();
}

View file

@ -1,47 +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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export interface LensTelemetryState {
runs: number;
byDate: Record<string, Record<string, number>>;
suggestionsByDate: Record<string, Record<string, number>>;
saved: LensVisualizationUsage;
multiterms: LensMultitermsUsage;
}
export interface LensVisualizationUsage {
saved_overall: Record<string, number>;
saved_30_days: Record<string, number>;
saved_90_days: Record<string, number>;
saved_overall_total: number;
saved_30_days_total: number;
saved_90_days_total: number;
}
export interface LensMultitermsUsage {
saved_multiterms_overall: Record<string, number>;
saved_multiterms_30_days: Record<string, number>;
saved_multiterms_90_days: Record<string, number>;
}
export interface LensClickUsage {
events_30_days: Record<string, number>;
events_90_days: Record<string, number>;
suggestion_events_30_days: Record<string, number>;
suggestion_events_90_days: Record<string, number>;
}
export interface GenericSavedUsage {
saved_overall: Record<string, number>;
saved_30_days: Record<string, number>;
saved_90_days: Record<string, number>;
saved_overall_total: number;
saved_30_days_total: number;
saved_90_days_total: number;
}
export type LensUsage = LensVisualizationUsage & LensMultitermsUsage & LensClickUsage;

View file

@ -1,73 +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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { ElasticsearchClient } from '@kbn/core/server';
import { createMetricQuery } from './saved_objects_metric_factory';
import { LensVisualizationUsage } from './types';
export function getVisualizationCounts(
getEsClient: () => Promise<ElasticsearchClient>,
kibanaIndex: string
): Promise<LensVisualizationUsage> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function bucketsToObject(arg: any) {
const obj: Record<string, number> = {};
arg.byType.buckets.forEach((bucket: { key: string; doc_count: number }) => {
obj[bucket.key] = bucket.doc_count + (obj[bucket.key] ?? 0);
});
if (arg.usesFormula.doc_count > 0) {
obj.formula = arg.usesFormula.doc_count;
}
return obj;
}
return createMetricQuery(
getEsClient,
kibanaIndex
)({
aggregations: {
byType: {
terms: {
// The script relies on having flattened keyword mapping for the Lens saved object,
// without this kind of mapping we would not be able to access `lens.state` in painless
script: `
String visType = doc['lens.visualizationType'].value;
String niceType = visType == 'lnsXY' ? doc['lens.state.visualization.preferredSeriesType'].value : visType;
return niceType;
`,
size: 100,
},
},
usesFormula: {
filter: {
match: {
operation_type: 'formula',
},
},
},
},
runtimeMappings: {
operation_type: {
type: 'keyword',
script: {
lang: 'painless',
source: `try {
if(doc['lens.state'].size() == 0) return;
HashMap layers = params['_source'].get('lens').get('state').get('datasourceStates').get('indexpattern').get('layers');
for(layerId in layers.keySet()) {
HashMap columns = layers.get(layerId).get('columns');
for(columnId in columns.keySet()) {
emit(columns.get(columnId).get('operationType'))
}
}
} catch(Exception e) {}`,
},
},
},
bucketsToObject,
});
}