mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[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:
parent
ee3cfb6d75
commit
51816a0631
41 changed files with 7 additions and 2349 deletions
|
@ -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 {};
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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';
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
}
|
|
@ -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,
|
||||
};
|
|
@ -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();
|
||||
}
|
|
@ -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;
|
|
@ -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,
|
||||
});
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue