From 980ec8caa3b007638826b113b4247b4e87cd2dd0 Mon Sep 17 00:00:00 2001 From: Ahmad Bamieh Date: Wed, 24 Jul 2019 17:16:24 +0300 Subject: [PATCH] [7.x] [telemetry] Analytics Package (#41113) (#41774) --- .github/CODEOWNERS | 72 +++++++++++ packages/kbn-analytics/package.json | 21 ++++ packages/kbn-analytics/src/index.ts | 22 ++++ packages/kbn-analytics/src/metrics/index.ts | 37 ++++++ .../kbn-analytics/src/metrics/stats.ts | 7 +- .../kbn-analytics/src/metrics/ui_stats.ts | 53 ++++++++ packages/kbn-analytics/src/report.ts | 93 ++++++++++++++ packages/kbn-analytics/src/reporter.ts | 114 ++++++++++++++++++ packages/kbn-analytics/src/storage.ts | 38 ++++++ packages/kbn-analytics/src/util.ts | 28 +++++ packages/kbn-analytics/tsconfig.json | 20 +++ .../public/dashboard/lib/migrate_app_state.ts | 4 +- src/legacy/core_plugins/ui_metric/README.md | 29 +++-- src/legacy/core_plugins/ui_metric/index.ts | 23 +++- .../ui_metric/public/hacks/ui_metric_init.ts | 34 ++++++ .../core_plugins/ui_metric/public/index.ts | 38 +----- .../public/services/telemetry_analytics.ts | 60 +++++++++ .../ui_metric/server/routes/api/ui_metric.ts | 69 ++++++++--- .../ui_metric/server/usage/collector.ts | 59 --------- .../apis/ui_metric/ui_metric.js | 39 +++++- .../__tests__/workpad_telemetry.test.tsx | 10 +- .../workpad/workpad_app/workpad_telemetry.tsx | 9 +- .../public/components/element_types/index.js | 4 +- .../fullscreen_control/index.js | 3 +- .../plugins/canvas/public/lib/ui_metric.ts | 13 ++ .../auto_follow_pattern_list.test.js | 4 - .../follower_indices_list.test.js | 4 - .../__jest__/client_integration/home.test.js | 4 - .../auto_follow_pattern_list.js | 4 +- .../auto_follow_pattern_table.js | 4 +- .../follower_indices_table.js | 4 +- .../follower_indices_list.js | 4 +- .../public/app/services/track_ui_metric.js | 10 +- .../index_lifecycle_management/public/app.js | 2 +- .../components/policy_table/policy_table.js | 2 +- .../public/services/api.js | 10 +- .../public/services/ui_metric.js | 7 +- .../public/store/actions/lifecycle.js | 2 +- .../plugins/index_management/public/app.js | 2 +- .../index_list/index_table/index_table.js | 2 +- .../home/templates_list/templates_list.tsx | 4 +- .../index_management/public/services/api.ts | 40 +++--- .../public/services/track_ui_metric.ts | 10 +- .../public/services/use_request.ts | 4 +- .../public/store/reducers/detail_panel.js | 2 +- .../infra/public/hooks/use_track_metric.tsx | 74 ++++++++++++ .../helpers/setup_environment.js | 3 + .../plugins/remote_clusters/public/app/app.js | 4 +- .../remote_cluster_table.js | 4 +- .../public/app/services/api.js | 2 +- .../public/app/services/index.js | 3 +- .../public/app/services/track_ui_metric.js | 26 ---- .../public/app/services/ui_metric.ts | 25 +++- .../app/store/actions/remove_clusters.js | 3 +- .../plugins/remote_clusters/public/plugin.js | 4 +- .../plugins/remote_clusters/public/shim.ts | 4 +- .../job_create_review.test.js | 4 - .../client_integration/job_list.test.js | 4 - .../plugins/rollup/public/crud_app/app.js | 4 +- .../job_list/detail_panel/detail_panel.js | 4 +- .../sections/job_list/job_table/job_table.js | 4 +- .../rollup/public/crud_app/services/index.js | 1 + .../crud_app/services/track_ui_metric.js | 9 +- .../siem/public/components/flyout/index.tsx | 5 +- .../navigation/tab_navigation/index.tsx | 4 +- .../siem/public/lib/track_usage/index.ts | 8 +- .../app/services/ui_metric/ui_metric.ts | 15 ++- .../plugins/snapshot_restore/public/plugin.ts | 2 +- .../plugins/snapshot_restore/public/shim.ts | 6 +- .../plugins/telemetry/common/constants.ts | 6 + x-pack/legacy/plugins/telemetry/index.ts | 7 +- ...telemetry_trigger.js => telemetry_init.ts} | 21 ++-- .../public/services/telemetry_opt_in.js | 1 - .../telemetry/server/collectors/index.ts | 1 + .../server/collectors/ui_metric/index.ts} | 7 +- .../telemetry_ui_metric_collector.ts | 45 +++++++ x-pack/package.json | 1 + yarn.lock | 2 +- 78 files changed, 1017 insertions(+), 320 deletions(-) create mode 100644 .github/CODEOWNERS create mode 100644 packages/kbn-analytics/package.json create mode 100644 packages/kbn-analytics/src/index.ts create mode 100644 packages/kbn-analytics/src/metrics/index.ts rename src/legacy/core_plugins/ui_metric/server/usage/index.ts => packages/kbn-analytics/src/metrics/stats.ts (90%) create mode 100644 packages/kbn-analytics/src/metrics/ui_stats.ts create mode 100644 packages/kbn-analytics/src/report.ts create mode 100644 packages/kbn-analytics/src/reporter.ts create mode 100644 packages/kbn-analytics/src/storage.ts create mode 100644 packages/kbn-analytics/src/util.ts create mode 100644 packages/kbn-analytics/tsconfig.json create mode 100644 src/legacy/core_plugins/ui_metric/public/hacks/ui_metric_init.ts create mode 100644 src/legacy/core_plugins/ui_metric/public/services/telemetry_analytics.ts delete mode 100644 src/legacy/core_plugins/ui_metric/server/usage/collector.ts create mode 100644 x-pack/legacy/plugins/canvas/public/lib/ui_metric.ts create mode 100644 x-pack/legacy/plugins/infra/public/hooks/use_track_metric.tsx delete mode 100644 x-pack/legacy/plugins/remote_clusters/public/app/services/track_ui_metric.js rename x-pack/legacy/plugins/telemetry/public/hacks/{telemetry_trigger.js => telemetry_init.ts} (74%) rename x-pack/legacy/plugins/{canvas/public/lib/ui_metric.js => telemetry/server/collectors/ui_metric/index.ts} (54%) create mode 100644 x-pack/legacy/plugins/telemetry/server/collectors/ui_metric/telemetry_ui_metric_collector.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000000..274d9a25ef53 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,72 @@ +# GitHub CODEOWNERS definition +# Identify which groups will be pinged by changes to different parts of the codebase. +# For more info, see https://help.github.com/articles/about-codeowners/ + +# App Architecture +/src/plugins/data/ @elastic/kibana-app-arch +/src/plugins/kibana_utils/ @elastic/kibana-app-arch + +# APM +/x-pack/legacy/plugins/apm/ @elastic/apm-ui + +# Beats +/x-pack/legacy/plugins/beats_management/ @elastic/beats + +# Canvas +/x-pack/legacy/plugins/canvas/ @elastic/kibana-canvas + +# Code +/x-pack/legacy/plugins/code/ @teams/code +/x-pack/test/functional/apps/code/ @teams/code +/x-pack/test/api_integration/apis/code/ @teams/code + +# Infrastructure and Logs UI +/x-pack/legacy/plugins/infra/ @elastic/infra-logs-ui + +# Machine Learning +/x-pack/legacy/plugins/ml/ @elastic/ml-ui + +# Operations +/renovate.json5 @elastic/kibana-operations +/src/dev/ @elastic/kibana-operations +/src/setup_node_env/ @elastic/kibana-operations +/src/optimize/ @elastic/kibana-operations + +# Platform +/src/core/ @elastic/kibana-platform +/src/legacy/server/saved_objects/ @elastic/kibana-platform +/src/legacy/ui/public/saved_objects @elastic/kibana-platform + +# Security +/x-pack/legacy/plugins/security/ @elastic/kibana-security +/x-pack/legacy/plugins/spaces/ @elastic/kibana-security +/x-pack/legacy/plugins/encrypted_saved_objects/ @elastic/kibana-security +/src/legacy/server/csp/ @elastic/kibana-security +/x-pack/plugins/security/ @elastic/kibana-security + +# Kibana Stack Services +/packages/kbn-analytics/ @elastic/kibana-stack-services +/src/legacy/core_plugins/ui_metric/ @elastic/kibana-stack-services +/x-pack/legacy/plugins/telemetry @elastic/kibana-stack-services +/x-pack/legacy/plugins/alerting @elastic/kibana-stack-services +/x-pack/legacy/plugins/actions @elastic/kibana-stack-services +/x-pack/legacy/plugins/task_manager @elastic/kibana-stack-services + +# Design +**/*.scss @elastic/kibana-design + +# Elasticsearch UI +/src/legacy/core_plugins/console/ @elastic/es-ui +/x-pack/legacy/plugins/console_extensions/ @elastic/es-ui +/x-pack/legacy/plugins/cross_cluster_replication/ @elastic/es-ui +/x-pack/legacy/plugins/index_lifecycle_management/ @elastic/es-ui +/x-pack/legacy/plugins/index_management/ @elastic/es-ui +/x-pack/legacy/plugins/license_management/ @elastic/es-ui +/x-pack/legacy/plugins/remote_clusters/ @elastic/es-ui +/x-pack/legacy/plugins/rollup/ @elastic/es-ui +/x-pack/legacy/plugins/searchprofiler/ @elastic/es-ui +/x-pack/legacy/plugins/snapshot_restore/ @elastic/es-ui +/x-pack/legacy/plugins/watcher/ @elastic/es-ui + +# Kibana TSVB external contractors +/src/legacy/core_plugins/metrics/ @elastic/kibana-tsvb-external diff --git a/packages/kbn-analytics/package.json b/packages/kbn-analytics/package.json new file mode 100644 index 000000000000..e8fb1aea81a7 --- /dev/null +++ b/packages/kbn-analytics/package.json @@ -0,0 +1,21 @@ +{ + "name": "@kbn/analytics", + "private": true, + "version": "1.0.0", + "description": "Kibana Analytics tool", + "main": "target/index.js", + "types": "target/index.d.ts", + "author": "Ahmad Bamieh ", + "license": "Apache-2.0", + "scripts": { + "build": "tsc", + "kbn:bootstrap": "yarn build", + "kbn:watch": "yarn build --watch" + }, + "devDependencies": { + "typescript": "3.5.1" + }, + "dependencies": { + "@kbn/dev-utils": "1.0.0" + } +} diff --git a/packages/kbn-analytics/src/index.ts b/packages/kbn-analytics/src/index.ts new file mode 100644 index 000000000000..63fd115fa759 --- /dev/null +++ b/packages/kbn-analytics/src/index.ts @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { createReporter, ReportHTTP, Reporter, ReporterConfig } from './reporter'; +export { UiStatsMetricType, METRIC_TYPE } from './metrics'; +export { Report, ReportManager } from './report'; diff --git a/packages/kbn-analytics/src/metrics/index.ts b/packages/kbn-analytics/src/metrics/index.ts new file mode 100644 index 000000000000..13b9e5dc59e4 --- /dev/null +++ b/packages/kbn-analytics/src/metrics/index.ts @@ -0,0 +1,37 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { UiStatsMetric, UiStatsMetricType } from './ui_stats'; + +export { + UiStatsMetric, + createUiStatsMetric, + UiStatsMetricReport, + UiStatsMetricType, +} from './ui_stats'; +export { Stats } from './stats'; + +export type Metric = UiStatsMetric; +export type MetricType = keyof typeof METRIC_TYPE; + +export enum METRIC_TYPE { + COUNT = 'count', + LOADED = 'loaded', + CLICK = 'click', +} diff --git a/src/legacy/core_plugins/ui_metric/server/usage/index.ts b/packages/kbn-analytics/src/metrics/stats.ts similarity index 90% rename from src/legacy/core_plugins/ui_metric/server/usage/index.ts rename to packages/kbn-analytics/src/metrics/stats.ts index b3b88b94d126..993290167018 100644 --- a/src/legacy/core_plugins/ui_metric/server/usage/index.ts +++ b/packages/kbn-analytics/src/metrics/stats.ts @@ -17,4 +17,9 @@ * under the License. */ -export { registerUiMetricUsageCollector } from './collector'; +export interface Stats { + min: number; + max: number; + sum: number; + avg: number; +} diff --git a/packages/kbn-analytics/src/metrics/ui_stats.ts b/packages/kbn-analytics/src/metrics/ui_stats.ts new file mode 100644 index 000000000000..7615fd20645e --- /dev/null +++ b/packages/kbn-analytics/src/metrics/ui_stats.ts @@ -0,0 +1,53 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Stats } from './stats'; +import { METRIC_TYPE } from './'; + +export type UiStatsMetricType = METRIC_TYPE.CLICK | METRIC_TYPE.LOADED | METRIC_TYPE.COUNT; +export interface UiStatsMetricConfig { + type: T; + appName: string; + eventName: string; + count?: number; +} + +export interface UiStatsMetric { + type: T; + appName: string; + eventName: string; + count: number; +} + +export function createUiStatsMetric({ + type, + appName, + eventName, + count = 1, +}: UiStatsMetricConfig): UiStatsMetric { + return { type, appName, eventName, count }; +} + +export interface UiStatsMetricReport { + key: string; + appName: string; + eventName: string; + type: UiStatsMetricType; + stats: Stats; +} diff --git a/packages/kbn-analytics/src/report.ts b/packages/kbn-analytics/src/report.ts new file mode 100644 index 000000000000..6187455fa60a --- /dev/null +++ b/packages/kbn-analytics/src/report.ts @@ -0,0 +1,93 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { UnreachableCaseError } from './util'; +import { Metric, Stats, UiStatsMetricReport, METRIC_TYPE } from './metrics'; + +export interface Report { + uiStatsMetrics: { + [key: string]: UiStatsMetricReport; + }; +} + +export class ReportManager { + public report: Report; + constructor(report?: Report) { + this.report = report || ReportManager.createReport(); + } + static createReport() { + return { uiStatsMetrics: {} }; + } + public clearReport() { + this.report = ReportManager.createReport(); + } + public isReportEmpty(): boolean { + return Object.keys(this.report.uiStatsMetrics).length === 0; + } + private incrementStats(count: number, stats?: Stats): Stats { + const { min = 0, max = 0, sum = 0 } = stats || {}; + const newMin = Math.min(min, count); + const newMax = Math.max(max, count); + const newAvg = newMin + newMax / 2; + const newSum = sum + count; + + return { + min: newMin, + max: newMax, + avg: newAvg, + sum: newSum, + }; + } + assignReports(newMetrics: Metric[]) { + newMetrics.forEach(newMetric => this.assignReport(this.report, newMetric)); + } + static createMetricKey(metric: Metric): string { + switch (metric.type) { + case METRIC_TYPE.CLICK: + case METRIC_TYPE.LOADED: + case METRIC_TYPE.COUNT: { + const { appName, type, eventName } = metric; + return `${appName}-${type}-${eventName}`; + } + default: + throw new UnreachableCaseError(metric.type); + } + } + private assignReport(report: Report, metric: Metric) { + switch (metric.type) { + case METRIC_TYPE.CLICK: + case METRIC_TYPE.LOADED: + case METRIC_TYPE.COUNT: { + const { appName, type, eventName, count } = metric; + const key = ReportManager.createMetricKey(metric); + const existingStats = (report.uiStatsMetrics[key] || {}).stats; + this.report.uiStatsMetrics[key] = { + key, + appName, + eventName, + type, + stats: this.incrementStats(count, existingStats), + }; + return; + } + default: + throw new UnreachableCaseError(metric.type); + } + } +} diff --git a/packages/kbn-analytics/src/reporter.ts b/packages/kbn-analytics/src/reporter.ts new file mode 100644 index 000000000000..37d23aa44309 --- /dev/null +++ b/packages/kbn-analytics/src/reporter.ts @@ -0,0 +1,114 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { wrapArray } from './util'; +import { Metric, UiStatsMetric, createUiStatsMetric } from './metrics'; + +import { Storage, ReportStorageManager } from './storage'; +import { Report, ReportManager } from './report'; + +export interface ReporterConfig { + http: ReportHTTP; + storage?: Storage; + checkInterval?: number; + debug?: boolean; + storageKey?: string; +} + +export type ReportHTTP = (report: Report) => Promise; + +export class Reporter { + checkInterval: number; + private interval: any; + private http: ReportHTTP; + private reportManager: ReportManager; + private storageManager: ReportStorageManager; + private debug: boolean; + + constructor(config: ReporterConfig) { + const { http, storage, debug, checkInterval = 10000, storageKey = 'analytics' } = config; + + this.http = http; + this.checkInterval = checkInterval; + this.interval = null; + this.storageManager = new ReportStorageManager(storageKey, storage); + const storedReport = this.storageManager.get(); + this.reportManager = new ReportManager(storedReport); + this.debug = !!debug; + } + + private saveToReport(newMetrics: Metric[]) { + this.reportManager.assignReports(newMetrics); + this.storageManager.store(this.reportManager.report); + } + + private flushReport() { + this.reportManager.clearReport(); + this.storageManager.store(this.reportManager.report); + } + + public start() { + if (!this.interval) { + this.interval = setTimeout(() => { + this.interval = null; + this.sendReports(); + }, this.checkInterval); + } + } + + private log(message: any) { + if (this.debug) { + // eslint-disable-next-line + console.debug(message); + } + } + + public reportUiStats( + appName: string, + type: UiStatsMetric['type'], + eventNames: string | string[], + count?: number + ) { + const metrics = wrapArray(eventNames).map(eventName => { + if (this) this.log(`${type} Metric -> (${appName}:${eventName}):`); + const report = createUiStatsMetric({ type, appName, eventName, count }); + this.log(report); + return report; + }); + this.saveToReport(metrics); + } + + public async sendReports() { + if (!this.reportManager.isReportEmpty()) { + try { + await this.http(this.reportManager.report); + this.flushReport(); + } catch (err) { + this.log(`Error Sending Metrics Report ${err}`); + } + } + this.start(); + } +} + +export function createReporter(reportedConf: ReporterConfig) { + const reporter = new Reporter(reportedConf); + reporter.start(); + return reporter; +} diff --git a/packages/kbn-analytics/src/storage.ts b/packages/kbn-analytics/src/storage.ts new file mode 100644 index 000000000000..9abf9fa7dac2 --- /dev/null +++ b/packages/kbn-analytics/src/storage.ts @@ -0,0 +1,38 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Report } from './report'; + +export type Storage = Map; +export class ReportStorageManager { + storageKey: string; + private storage?: Storage; + constructor(storageKey: string, storage?: Storage) { + this.storageKey = storageKey; + this.storage = storage; + } + public get(): Report | undefined { + if (!this.storage) return; + return this.storage.get(this.storageKey); + } + public store(report: Report) { + if (!this.storage) return; + this.storage.set(this.storageKey, report); + } +} diff --git a/packages/kbn-analytics/src/util.ts b/packages/kbn-analytics/src/util.ts new file mode 100644 index 000000000000..fe3e8b8f9f7b --- /dev/null +++ b/packages/kbn-analytics/src/util.ts @@ -0,0 +1,28 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export function wrapArray(subj: T | T[]): T[] { + return Array.isArray(subj) ? subj : [subj]; +} + +export class UnreachableCaseError extends Error { + constructor(val: never) { + super(`Unreachable case: ${val}`); + } +} diff --git a/packages/kbn-analytics/tsconfig.json b/packages/kbn-analytics/tsconfig.json new file mode 100644 index 000000000000..fcb8ddbbde68 --- /dev/null +++ b/packages/kbn-analytics/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": true, + "declarationDir": "./target", + "outDir": "./target", + "stripInternal": true, + "declarationMap": true, + "types": [ + "jest", + "node" + ] + }, + "include": [ + "./src/**/*.ts" + ], + "exclude": [ + "target" + ] +} diff --git a/src/legacy/core_plugins/kibana/public/dashboard/lib/migrate_app_state.ts b/src/legacy/core_plugins/kibana/public/dashboard/lib/migrate_app_state.ts index e97719aed6bc..9bd93029f06d 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/lib/migrate_app_state.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/lib/migrate_app_state.ts @@ -20,7 +20,7 @@ import semver from 'semver'; import chrome from 'ui/chrome'; import { i18n } from '@kbn/i18n'; -import { trackUiMetric } from '../../../../ui_metric/public'; +import { createUiStatsReporter, METRIC_TYPE } from '../../../../ui_metric/public'; import { DashboardAppState, SavedDashboardPanelTo60, @@ -59,7 +59,7 @@ export function migrateAppState(appState: { [key: string]: unknown } | Dashboard const version = (panel as SavedDashboardPanel730ToLatest).version; // This will help us figure out when to remove support for older style URLs. - trackUiMetric('DashboardPanelVersionInUrl', `${version}`); + createUiStatsReporter('DashboardPanelVersionInUrl')(METRIC_TYPE.LOADED, `${version}`); return semver.satisfies(version, '<7.3'); }); diff --git a/src/legacy/core_plugins/ui_metric/README.md b/src/legacy/core_plugins/ui_metric/README.md index 9b78cf600dc4..90855faff61a 100644 --- a/src/legacy/core_plugins/ui_metric/README.md +++ b/src/legacy/core_plugins/ui_metric/README.md @@ -16,29 +16,32 @@ the name of a dashboard they've viewed, or the timestamp of the interaction. ## How to use it -To track a user interaction, import the `trackUiMetric` helper function from UI Metric app: +To track a user interaction, import the `createUiStatsReporter` helper function from UI Metric app: ```js -import { trackUiMetric } from 'relative/path/to/src/legacy/core_plugins/ui_metric/public'; +import { createUiStatsReporter, METRIC_TYPE } from 'relative/path/to/src/legacy/core_plugins/ui_metric/public'; +const trackMetric = createUiStatsReporter(``); +trackMetric(METRIC_TYPE.CLICK, ``); +trackMetric('click', ``); ``` +Metric Types: + - `METRIC_TYPE.CLICK` for tracking clicks `trackMetric(METRIC_TYPE.CLICK, 'my_button_clicked');` + - `METRIC_TYPE.LOADED` for a component load or page load `trackMetric(METRIC_TYPE.LOADED', 'my_component_loaded');` + - `METRIC_TYPE.COUNT` for a tracking a misc count `trackMetric(METRIC_TYPE.COUNT', 'my_counter', });` + Call this function whenever you would like to track a user interaction within your app. The function -accepts two arguments, `appName` and `metricType`. These should be underscore-delimited strings. -For example, to track the `my_metric` metric in the app `my_app` call `trackUiMetric('my_app', 'my_metric)`. +accepts two arguments, `metricType` and `eventNames`. These should be underscore-delimited strings. +For example, to track the `my_event` metric in the app `my_app` call `trackUiMetric(METRIC_TYPE.*, 'my_event)`. That's all you need to do! -To track multiple metrics within a single request, provide an array of metric types, e.g. `trackUiMetric('my_app', ['my_metric1', 'my_metric2', 'my_metric3'])`. - -**NOTE:** When called, this function sends a `POST` request to `/api/ui_metric/{appName}/{metricType}`. -It's important that this request is sent via the `trackUiMetric` function, because it contains special -logic for blocking the request if the user hasn't opted in to telemetry. +To track multiple metrics within a single request, provide an array of events, e.g. `trackMetric(METRIC_TYPE.*, ['my_event1', 'my_event2', 'my_event3'])`. ### Disallowed characters -The colon and comma characters (`,`, `:`) should not be used in app name or metric types. Colons play -a sepcial role in how metrics are stored as saved objects, and the API endpoint uses commas to delimit -multiple metric types in a single API request. +The colon character (`:`) should not be used in app name or event names. Colons play +a special role in how metrics are stored as saved objects. ### Tracking timed interactions @@ -47,7 +50,7 @@ logic yourself. You'll also need to predefine some buckets into which the UI met For example, if you're timing how long it takes to create a visualization, you may decide to measure interactions that take less than 1 minute, 1-5 minutes, 5-20 minutes, and longer than 20 minutes. To track these interactions, you'd use the timed length of the interaction to determine whether to -use a `metricType` of `create_vis_1m`, `create_vis_5m`, `create_vis_20m`, or `create_vis_infinity`. +use a `eventName` of `create_vis_1m`, `create_vis_5m`, `create_vis_20m`, or `create_vis_infinity`. ## How it works diff --git a/src/legacy/core_plugins/ui_metric/index.ts b/src/legacy/core_plugins/ui_metric/index.ts index 34ca346b8756..6c957f23b5c4 100644 --- a/src/legacy/core_plugins/ui_metric/index.ts +++ b/src/legacy/core_plugins/ui_metric/index.ts @@ -18,9 +18,10 @@ */ import { resolve } from 'path'; +import JoiNamespace from 'joi'; +import { Server } from 'hapi'; import { Legacy } from '../../../../kibana'; -import { registerUserActionRoute } from './server/routes/api/ui_metric'; -import { registerUiMetricUsageCollector } from './server/usage/index'; +import { registerUiMetricRoute } from './server/routes/api/ui_metric'; // eslint-disable-next-line import/no-default-export export default function(kibana: any) { @@ -28,15 +29,25 @@ export default function(kibana: any) { id: 'ui_metric', require: ['kibana', 'elasticsearch'], publicDir: resolve(__dirname, 'public'), - + config(Joi: typeof JoiNamespace) { + return Joi.object({ + enabled: Joi.boolean().default(true), + debug: Joi.boolean().default(Joi.ref('$dev')), + }).default(); + }, uiExports: { + injectDefaultVars(server: Server) { + const config = server.config(); + return { + debugUiMetric: config.get('ui_metric.debug'), + }; + }, mappings: require('./mappings.json'), - hacks: ['plugins/ui_metric'], + hacks: ['plugins/ui_metric/hacks/ui_metric_init'], }, init(server: Legacy.Server) { - registerUserActionRoute(server); - registerUiMetricUsageCollector(server); + registerUiMetricRoute(server); }, }); } diff --git a/src/legacy/core_plugins/ui_metric/public/hacks/ui_metric_init.ts b/src/legacy/core_plugins/ui_metric/public/hacks/ui_metric_init.ts new file mode 100644 index 000000000000..7aafc82cfe4c --- /dev/null +++ b/src/legacy/core_plugins/ui_metric/public/hacks/ui_metric_init.ts @@ -0,0 +1,34 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// @ts-ignore +import { uiModules } from 'ui/modules'; +import chrome from 'ui/chrome'; +import { createAnalyticsReporter, setTelemetryReporter } from '../services/telemetry_analytics'; + +function telemetryInit($injector: any) { + const localStorage = $injector.get('localStorage'); + const debug = chrome.getInjected('debugUiMetric'); + const $http = $injector.get('$http'); + const basePath = chrome.getBasePath(); + const uiReporter = createAnalyticsReporter({ localStorage, $http, basePath, debug }); + setTelemetryReporter(uiReporter); +} + +uiModules.get('kibana').run(telemetryInit); diff --git a/src/legacy/core_plugins/ui_metric/public/index.ts b/src/legacy/core_plugins/ui_metric/public/index.ts index 6bf54943a3ac..b1e78b56d05d 100644 --- a/src/legacy/core_plugins/ui_metric/public/index.ts +++ b/src/legacy/core_plugins/ui_metric/public/index.ts @@ -17,39 +17,5 @@ * under the License. */ -import chrome from 'ui/chrome'; -// @ts-ignore -import { uiModules } from 'ui/modules'; -import { getCanTrackUiMetrics } from 'ui/ui_metric'; -import { API_BASE_PATH } from '../common'; - -let _http: any; - -uiModules.get('kibana').run(($http: any) => { - _http = $http; -}); - -function createErrorMessage(subject: string): any { - const message = - `trackUiMetric was called with ${subject}, which is not allowed to contain a colon. ` + - `Colons play a special role in how metrics are saved as stored objects`; - return new Error(message); -} - -export function trackUiMetric(appName: string, metricType: string | string[]) { - if (!getCanTrackUiMetrics()) { - return; - } - - if (appName.includes(':')) { - throw createErrorMessage(`app name '${appName}'`); - } - - if (metricType.includes(':')) { - throw createErrorMessage(`metric type ${metricType}`); - } - - const metricTypes = Array.isArray(metricType) ? metricType.join(',') : metricType; - const uri = chrome.addBasePath(`${API_BASE_PATH}/${appName}/${metricTypes}`); - _http.post(uri); -} +export { createUiStatsReporter } from './services/telemetry_analytics'; +export { METRIC_TYPE } from '@kbn/analytics'; diff --git a/src/legacy/core_plugins/ui_metric/public/services/telemetry_analytics.ts b/src/legacy/core_plugins/ui_metric/public/services/telemetry_analytics.ts new file mode 100644 index 000000000000..63adccb3e02b --- /dev/null +++ b/src/legacy/core_plugins/ui_metric/public/services/telemetry_analytics.ts @@ -0,0 +1,60 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { createReporter, Reporter, UiStatsMetricType } from '@kbn/analytics'; + +let telemetryReporter: Reporter; + +export const setTelemetryReporter = (aTelemetryReporter: Reporter): void => { + telemetryReporter = aTelemetryReporter; +}; + +export const getTelemetryReporter = () => { + return telemetryReporter; +}; + +export const createUiStatsReporter = (appName: string) => ( + type: UiStatsMetricType, + eventNames: string | string[], + count?: number +): void => { + if (telemetryReporter) { + return telemetryReporter.reportUiStats(appName, type, eventNames, count); + } +}; + +interface AnalyicsReporterConfig { + localStorage: any; + basePath: string; + debug: boolean; + $http: ng.IHttpService; +} + +export function createAnalyticsReporter(config: AnalyicsReporterConfig) { + const { localStorage, basePath, $http, debug } = config; + + return createReporter({ + debug, + storage: localStorage, + async http(report) { + const url = `${basePath}/api/telemetry/report`; + await $http.post(url, { report }); + }, + }); +} diff --git a/src/legacy/core_plugins/ui_metric/server/routes/api/ui_metric.ts b/src/legacy/core_plugins/ui_metric/server/routes/api/ui_metric.ts index 4bbafbb18f55..8a7950c46fa3 100644 --- a/src/legacy/core_plugins/ui_metric/server/routes/api/ui_metric.ts +++ b/src/legacy/core_plugins/ui_metric/server/routes/api/ui_metric.ts @@ -17,36 +17,65 @@ * under the License. */ +import Joi from 'joi'; import Boom from 'boom'; +import { Report } from '@kbn/analytics'; import { Server } from 'hapi'; -import { API_BASE_PATH } from '../../../common'; -export const registerUserActionRoute = (server: Server) => { - /* - * Increment a count on an object representing a specific interaction with the UI. - */ +export async function storeReport(server: any, report: Report) { + const { getSavedObjectsRepository } = server.savedObjects; + const { callWithInternalUser } = server.plugins.elasticsearch.getCluster('admin'); + const internalRepository = getSavedObjectsRepository(callWithInternalUser); + + const metricKeys = Object.keys(report.uiStatsMetrics); + return Promise.all( + metricKeys.map(async key => { + const metric = report.uiStatsMetrics[key]; + const { appName, eventName } = metric; + const savedObjectId = `${appName}:${eventName}`; + return internalRepository.incrementCounter('ui-metric', savedObjectId, 'count'); + }) + ); +} + +export function registerUiMetricRoute(server: Server) { server.route({ - path: `${API_BASE_PATH}/{appName}/{metricTypes}`, method: 'POST', - handler: async (request: any) => { - const { appName, metricTypes } = request.params; + path: '/api/telemetry/report', + options: { + validate: { + payload: Joi.object({ + report: Joi.object({ + uiStatsMetrics: Joi.object() + .pattern( + /.*/, + Joi.object({ + key: Joi.string().required(), + type: Joi.string().required(), + appName: Joi.string().required(), + eventName: Joi.string().required(), + stats: Joi.object({ + min: Joi.number(), + sum: Joi.number(), + max: Joi.number(), + avg: Joi.number(), + }).allow(null), + }) + ) + .allow(null), + }), + }), + }, + }, + handler: async (req: any, h: any) => { + const { report } = req.payload; try { - const { getSavedObjectsRepository } = server.savedObjects; - const { callWithInternalUser } = server.plugins.elasticsearch.getCluster('admin'); - const internalRepository = getSavedObjectsRepository(callWithInternalUser); - - const incrementRequests = metricTypes.split(',').map((metricType: string) => { - const savedObjectId = `${appName}:${metricType}`; - // This object is created if it doesn't already exist. - return internalRepository.incrementCounter('ui-metric', savedObjectId, 'count'); - }); - - await Promise.all(incrementRequests); + await storeReport(server, report); return {}; } catch (error) { return new Boom('Something went wrong', { statusCode: error.status }); } }, }); -}; +} diff --git a/src/legacy/core_plugins/ui_metric/server/usage/collector.ts b/src/legacy/core_plugins/ui_metric/server/usage/collector.ts deleted file mode 100644 index bbb7b1af8e7c..000000000000 --- a/src/legacy/core_plugins/ui_metric/server/usage/collector.ts +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -const UI_METRIC_USAGE_TYPE = 'ui_metric'; - -export function registerUiMetricUsageCollector(server: any) { - const collector = server.usage.collectorSet.makeUsageCollector({ - type: UI_METRIC_USAGE_TYPE, - fetch: async (callCluster: any) => { - const { SavedObjectsClient, getSavedObjectsRepository } = server.savedObjects; - const { callWithInternalUser } = server.plugins.elasticsearch.getCluster('admin'); - const internalRepository = getSavedObjectsRepository(callWithInternalUser); - const savedObjectsClient = new SavedObjectsClient(internalRepository); - - const { saved_objects: rawUiMetrics } = await savedObjectsClient.find({ - type: 'ui-metric', - fields: ['count'], - }); - - const uiMetricsByAppName = rawUiMetrics.reduce((accum: any, rawUiMetric: any) => { - const { - id, - attributes: { count }, - } = rawUiMetric; - - const [appName, metricType] = id.split(':'); - - if (!accum[appName]) { - accum[appName] = []; - } - - const pair = { key: metricType, value: count }; - accum[appName].push(pair); - return accum; - }, {}); - - return uiMetricsByAppName; - }, - isReady: () => true, - }); - - server.usage.collectorSet.register(collector); -} diff --git a/test/api_integration/apis/ui_metric/ui_metric.js b/test/api_integration/apis/ui_metric/ui_metric.js index e841176025bb..efa6be47b50c 100644 --- a/test/api_integration/apis/ui_metric/ui_metric.js +++ b/test/api_integration/apis/ui_metric/ui_metric.js @@ -18,16 +18,33 @@ */ import expect from '@kbn/expect'; +import { ReportManager } from '@kbn/analytics'; export default function ({ getService }) { const supertest = getService('supertest'); const es = getService('es'); + const createMetric = (eventName) => ({ + key: ReportManager.createMetricKey({ appName: 'myApp', type: 'click', eventName }), + eventName, + appName: 'myApp', + type: 'click', + stats: { sum: 1, avg: 1, min: 1, max: 1 }, + }); + describe('ui_metric API', () => { + const uiStatsMetric = createMetric('myEvent'); + const report = { + uiStatsMetrics: { + [uiStatsMetric.key]: uiStatsMetric, + } + }; it('increments the count field in the document defined by the {app}/{action_type} path', async () => { await supertest - .post('/api/ui_metric/myApp/myAction') + .post('/api/telemetry/report') .set('kbn-xsrf', 'kibana') + .set('content-type', 'application/json') + .send({ report }) .expect(200); return es.search({ @@ -35,14 +52,24 @@ export default function ({ getService }) { q: 'type:user-action', }).then(response => { const ids = response.hits.hits.map(({ _id }) => _id); - expect(ids.includes('user-action:myApp:myAction')); + expect(ids.includes('user-action:myApp:myEvent')); }); }); - it('supports comma-delimited action types', async () => { + it('supports multiple events', async () => { + const uiStatsMetric1 = createMetric('myEvent1'); + const uiStatsMetric2 = createMetric('myEvent2'); + const report = { + uiStatsMetrics: { + [uiStatsMetric1.key]: uiStatsMetric1, + [uiStatsMetric2.key]: uiStatsMetric2, + } + }; await supertest - .post('/api/ui_metric/myApp/myAction1,myAction2') + .post('/api/telemetry/report') .set('kbn-xsrf', 'kibana') + .set('content-type', 'application/json') + .send({ report }) .expect(200); return es.search({ @@ -50,8 +77,8 @@ export default function ({ getService }) { q: 'type:user-action', }).then(response => { const ids = response.hits.hits.map(({ _id }) => _id); - expect(ids.includes('user-action:myApp:myAction1')); - expect(ids.includes('user-action:myApp:myAction2')); + expect(ids.includes('user-action:myApp:myEvent1')); + expect(ids.includes('user-action:myApp:myEvent2')); }); }); }); diff --git a/x-pack/legacy/plugins/canvas/public/apps/workpad/workpad_app/__tests__/workpad_telemetry.test.tsx b/x-pack/legacy/plugins/canvas/public/apps/workpad/workpad_app/__tests__/workpad_telemetry.test.tsx index db1427d6a720..b8779e7d44fc 100644 --- a/x-pack/legacy/plugins/canvas/public/apps/workpad/workpad_app/__tests__/workpad_telemetry.test.tsx +++ b/x-pack/legacy/plugins/canvas/public/apps/workpad/workpad_app/__tests__/workpad_telemetry.test.tsx @@ -11,6 +11,7 @@ import { WorkpadLoadedMetric, WorkpadLoadedWithErrorsMetric, } from '../workpad_telemetry'; +import { METRIC_TYPE } from '../../../../lib/ui_metric'; const trackMetric = jest.fn(); const Component = withUnconnectedElementsLoadedTelemetry(() =>
, trackMetric); @@ -83,7 +84,7 @@ describe('Elements Loaded Telemetry', () => { /> ); - expect(trackMetric).toBeCalledWith(WorkpadLoadedMetric); + expect(trackMetric).toBeCalledWith(METRIC_TYPE.LOADED, WorkpadLoadedMetric); }); it('only tracks loaded once', () => { @@ -154,7 +155,10 @@ describe('Elements Loaded Telemetry', () => { /> ); - expect(trackMetric).toBeCalledWith([WorkpadLoadedMetric, WorkpadLoadedWithErrorsMetric]); + expect(trackMetric).toBeCalledWith(METRIC_TYPE.LOADED, [ + WorkpadLoadedMetric, + WorkpadLoadedWithErrorsMetric, + ]); }); it('tracks when the workpad changes and is loaded', () => { @@ -198,7 +202,7 @@ describe('Elements Loaded Telemetry', () => { /> ); - expect(trackMetric).toBeCalledWith(WorkpadLoadedMetric); + expect(trackMetric).toBeCalledWith(METRIC_TYPE.LOADED, WorkpadLoadedMetric); }); it('does not track if workpad has no elements', () => { diff --git a/x-pack/legacy/plugins/canvas/public/apps/workpad/workpad_app/workpad_telemetry.tsx b/x-pack/legacy/plugins/canvas/public/apps/workpad/workpad_app/workpad_telemetry.tsx index 59375d3d0ef0..720f1726c1e3 100644 --- a/x-pack/legacy/plugins/canvas/public/apps/workpad/workpad_app/workpad_telemetry.tsx +++ b/x-pack/legacy/plugins/canvas/public/apps/workpad/workpad_app/workpad_telemetry.tsx @@ -7,7 +7,7 @@ import React, { useState, useEffect } from 'react'; import { connect } from 'react-redux'; // @ts-ignore: Local Untyped -import { trackCanvasUiMetric } from '../../../lib/ui_metric'; +import { trackCanvasUiMetric, METRIC_TYPE } from '../../../lib/ui_metric'; // @ts-ignore: Local Untyped import { getElementCounts } from '../../../state/selectors/workpad'; // @ts-ignore: Local Untyped @@ -79,7 +79,7 @@ function areAllElementsInResolvedArgs(workpad: Workpad, resolvedArgs: ResolvedAr export const withUnconnectedElementsLoadedTelemetry = function

( Component: React.ComponentType

, - trackMetric: (metric: string | string[]) => void = trackCanvasUiMetric + trackMetric = trackCanvasUiMetric ): React.SFC

{ return function ElementsLoadedTelemetry( props: P & ElementsLoadedTelemetryProps @@ -117,11 +117,10 @@ export const withUnconnectedElementsLoadedTelemetry = function

resolvedArgsAreForWorkpad ) { if (telemetryElementCounts.error > 0) { - trackMetric([WorkpadLoadedMetric, WorkpadLoadedWithErrorsMetric]); + trackMetric(METRIC_TYPE.LOADED, [WorkpadLoadedMetric, WorkpadLoadedWithErrorsMetric]); } else { - trackMetric(WorkpadLoadedMetric); + trackMetric(METRIC_TYPE.LOADED, WorkpadLoadedMetric); } - setHasReported(true); } }); diff --git a/x-pack/legacy/plugins/canvas/public/components/element_types/index.js b/x-pack/legacy/plugins/canvas/public/components/element_types/index.js index 557e1e3883c5..20d19d2fcbd8 100644 --- a/x-pack/legacy/plugins/canvas/public/components/element_types/index.js +++ b/x-pack/legacy/plugins/canvas/public/components/element_types/index.js @@ -15,7 +15,7 @@ import { notify } from '../../lib/notify'; import { selectToplevelNodes } from '../../state/actions/transient'; import { insertNodes, addElement } from '../../state/actions/elements'; import { getSelectedPage } from '../../state/selectors/workpad'; -import { trackCanvasUiMetric } from '../../lib/ui_metric'; +import { trackCanvasUiMetric, METRIC_TYPE } from '../../lib/ui_metric'; import { ElementTypes as Component } from './element_types'; const customElementAdded = 'elements-custom-added'; @@ -51,7 +51,7 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => { selectToplevelNodes(clonedNodes); // then select the cloned node(s) } onClose(); - trackCanvasUiMetric(customElementAdded); + trackCanvasUiMetric(METRIC_TYPE.LOADED, customElementAdded); }, // custom element search findCustomElements: async text => { diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/fullscreen_control/index.js b/x-pack/legacy/plugins/canvas/public/components/workpad_header/fullscreen_control/index.js index aba890512772..cf2e80cee03b 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/fullscreen_control/index.js +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/fullscreen_control/index.js @@ -16,7 +16,7 @@ import { getPages, getWorkpad, } from '../../../state/selectors/workpad'; -import { trackCanvasUiMetric } from '../../../lib/ui_metric'; +import { trackCanvasUiMetric, METRIC_TYPE } from '../../../lib/ui_metric'; import { LAUNCHED_FULLSCREEN, LAUNCHED_FULLSCREEN_AUTOPLAY, @@ -54,6 +54,7 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => { if (value === true) { trackCanvasUiMetric( + METRIC_TYPE.COUNT, stateProps.autoplayEnabled ? [LAUNCHED_FULLSCREEN, LAUNCHED_FULLSCREEN_AUTOPLAY] : LAUNCHED_FULLSCREEN diff --git a/x-pack/legacy/plugins/canvas/public/lib/ui_metric.ts b/x-pack/legacy/plugins/canvas/public/lib/ui_metric.ts new file mode 100644 index 000000000000..33976a147df4 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/lib/ui_metric.ts @@ -0,0 +1,13 @@ +/* + * 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 { + createUiStatsReporter, + METRIC_TYPE, +} from '../../../../../../src/legacy/core_plugins/ui_metric/public'; + +export const trackCanvasUiMetric = createUiStatsReporter('canvas'); +export { METRIC_TYPE }; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_list.test.js b/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_list.test.js index f116e5657280..167d0799d8c4 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_list.test.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_list.test.js @@ -19,10 +19,6 @@ jest.mock('ui/index_patterns', () => { return { INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE }; }); -jest.mock('../../../../../../src/legacy/core_plugins/ui_metric/public', () => ({ - trackUiMetric: jest.fn(), -})); - const { setup } = pageHelpers.autoFollowPatternList; describe('', () => { diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/follower_indices_list.test.js b/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/follower_indices_list.test.js index 8df920aa0642..f3047206c77b 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/follower_indices_list.test.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/follower_indices_list.test.js @@ -19,10 +19,6 @@ jest.mock('ui/index_patterns', () => { return { INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE }; }); -jest.mock('../../../../../../src/legacy/core_plugins/ui_metric/public', () => ({ - trackUiMetric: jest.fn(), -})); - const { setup } = pageHelpers.followerIndexList; describe('', () => { diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/home.test.js b/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/home.test.js index a4b559dc24d2..cf6ccd80f461 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/home.test.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/home.test.js @@ -28,10 +28,6 @@ jest.mock('ui/index_patterns', () => { return { INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE }; }); -jest.mock('../../../../../../src/legacy/core_plugins/ui_metric/public', () => ({ - trackUiMetric: jest.fn(), -})); - const { setup } = pageHelpers.home; describe('', () => { diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.js b/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.js index 12311f5f6d2d..d73fca1d76dc 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.js @@ -19,7 +19,7 @@ import { import routing from '../../../services/routing'; import { extractQueryParams } from '../../../services/query_params'; -import { trackUiMetric } from '../../../services/track_ui_metric'; +import { trackUiMetric, METRIC_TYPE } from '../../../services/track_ui_metric'; import { API_STATUS, UIM_AUTO_FOLLOW_PATTERN_LIST_LOAD } from '../../../constants'; import { SectionLoading, SectionError, SectionUnauthorized } from '../../../components'; import { AutoFollowPatternTable, DetailPanel } from './components'; @@ -60,7 +60,7 @@ export class AutoFollowPatternList extends PureComponent { componentDidMount() { const { loadAutoFollowPatterns, loadAutoFollowStats, selectAutoFollowPattern, history } = this.props; - trackUiMetric(UIM_AUTO_FOLLOW_PATTERN_LIST_LOAD); + trackUiMetric(METRIC_TYPE.LOADED, UIM_AUTO_FOLLOW_PATTERN_LIST_LOAD); loadAutoFollowPatterns(); loadAutoFollowStats(); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js b/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js index 8d38d7efec4f..dee85c54653b 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js @@ -20,7 +20,7 @@ import { import { API_STATUS, UIM_AUTO_FOLLOW_PATTERN_SHOW_DETAILS_CLICK } from '../../../../../constants'; import { AutoFollowPatternDeleteProvider } from '../../../../../components'; import routing from '../../../../../services/routing'; -import { trackUiMetric } from '../../../../../services/track_ui_metric'; +import { trackUiMetric, METRIC_TYPE } from '../../../../../services/track_ui_metric'; export class AutoFollowPatternTable extends PureComponent { static propTypes = { @@ -77,7 +77,7 @@ export class AutoFollowPatternTable extends PureComponent { return ( { - trackUiMetric(UIM_AUTO_FOLLOW_PATTERN_SHOW_DETAILS_CLICK); + trackUiMetric(METRIC_TYPE.CLICK, UIM_AUTO_FOLLOW_PATTERN_SHOW_DETAILS_CLICK); selectAutoFollowPattern(name); }} data-test-subj="autoFollowPatternLink" diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.js b/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.js index 19fe01c41742..620dd4e79c7e 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.js @@ -23,7 +23,7 @@ import { FollowerIndexUnfollowProvider } from '../../../../../components'; import routing from '../../../../../services/routing'; -import { trackUiMetric } from '../../../../../services/track_ui_metric'; +import { trackUiMetric, METRIC_TYPE } from '../../../../../services/track_ui_metric'; import { ContextMenu } from '../context_menu'; export class FollowerIndicesTable extends PureComponent { @@ -191,7 +191,7 @@ export class FollowerIndicesTable extends PureComponent { return ( { - trackUiMetric(UIM_FOLLOWER_INDEX_SHOW_DETAILS_CLICK); + trackUiMetric(METRIC_TYPE.CLICK, UIM_FOLLOWER_INDEX_SHOW_DETAILS_CLICK); selectFollowerIndex(name); }} data-test-subj="followerIndexLink" diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/follower_indices_list.js b/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/follower_indices_list.js index 5c593458dd50..b78f6399168a 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/follower_indices_list.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/follower_indices_list.js @@ -19,7 +19,7 @@ import { import routing from '../../../services/routing'; import { extractQueryParams } from '../../../services/query_params'; -import { trackUiMetric } from '../../../services/track_ui_metric'; +import { trackUiMetric, METRIC_TYPE } from '../../../services/track_ui_metric'; import { API_STATUS, UIM_FOLLOWER_INDEX_LIST_LOAD } from '../../../constants'; import { SectionLoading, SectionError, SectionUnauthorized } from '../../../components'; import { FollowerIndicesTable, DetailPanel } from './components'; @@ -58,7 +58,7 @@ export class FollowerIndicesList extends PureComponent { componentDidMount() { const { loadFollowerIndices, selectFollowerIndex, history } = this.props; - trackUiMetric(UIM_FOLLOWER_INDEX_LIST_LOAD); + trackUiMetric(METRIC_TYPE.LOADED, UIM_FOLLOWER_INDEX_LIST_LOAD); loadFollowerIndices(); // Select the pattern in the URL query params diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/track_ui_metric.js b/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/track_ui_metric.js index 7b2f0d040786..23314fdd0ef8 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/track_ui_metric.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/track_ui_metric.js @@ -4,13 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { trackUiMetric as track } from '../../../../../../../src/legacy/core_plugins/ui_metric/public'; +import { createUiStatsReporter, METRIC_TYPE } from '../../../../../../../src/legacy/core_plugins/ui_metric/public'; import { UIM_APP_NAME } from '../constants'; -export function trackUiMetric(actionType) { - track(UIM_APP_NAME, actionType); -} - +export const trackUiMetric = createUiStatsReporter(UIM_APP_NAME); +export { METRIC_TYPE }; /** * Transparently return provided request Promise, while allowing us to track * a successful completion of the request. @@ -18,7 +16,7 @@ export function trackUiMetric(actionType) { export function trackUserRequest(request, actionType) { // Only track successful actions. return request.then(response => { - trackUiMetric(actionType); + trackUiMetric(METRIC_TYPE.LOADED, actionType); // We return the response immediately without waiting for the tracking request to resolve, // to avoid adding additional latency. return response; diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/app.js b/x-pack/legacy/plugins/index_lifecycle_management/public/app.js index d9381d98f17a..ddd3ac2f68f8 100644 --- a/x-pack/legacy/plugins/index_lifecycle_management/public/app.js +++ b/x-pack/legacy/plugins/index_lifecycle_management/public/app.js @@ -12,7 +12,7 @@ import { PolicyTable } from './sections/policy_table'; import { trackUiMetric } from './services'; export const App = () => { - useEffect(() => trackUiMetric(UIM_APP_LOAD), []); + useEffect(() => trackUiMetric('loaded', UIM_APP_LOAD), []); return ( diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/sections/policy_table/components/policy_table/policy_table.js b/x-pack/legacy/plugins/index_lifecycle_management/public/sections/policy_table/components/policy_table/policy_table.js index 8d8d9700cbc2..25141e5a2fe5 100644 --- a/x-pack/legacy/plugins/index_lifecycle_management/public/sections/policy_table/components/policy_table/policy_table.js +++ b/x-pack/legacy/plugins/index_lifecycle_management/public/sections/policy_table/components/policy_table/policy_table.js @@ -178,7 +178,7 @@ export class PolicyTable extends Component { className="policyTable__link" data-test-subj="policyTablePolicyNameLink" href={getPolicyPath(value)} - onClick={() => trackUiMetric(UIM_EDIT_CLICK)} + onClick={() => trackUiMetric('click', UIM_EDIT_CLICK)} > {value} diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/services/api.js b/x-pack/legacy/plugins/index_lifecycle_management/public/services/api.js index f76a30761d34..3819822088e9 100644 --- a/x-pack/legacy/plugins/index_lifecycle_management/public/services/api.js +++ b/x-pack/legacy/plugins/index_lifecycle_management/public/services/api.js @@ -58,34 +58,34 @@ export async function savePolicy(policy, httpClient = getHttpClient()) { export async function deletePolicy(policyName, httpClient = getHttpClient()) { const response = await httpClient.delete(`${apiPrefix}/policies/${encodeURIComponent(policyName)}`); // Only track successful actions. - trackUiMetric(UIM_POLICY_DELETE); + trackUiMetric('count', UIM_POLICY_DELETE); return response.data; } export const retryLifecycleForIndex = async (indexNames, httpClient = getHttpClient()) => { const response = await httpClient.post(`${apiPrefix}/index/retry`, { indexNames }); // Only track successful actions. - trackUiMetric(UIM_INDEX_RETRY_STEP); + trackUiMetric('count', UIM_INDEX_RETRY_STEP); return response.data; }; export const removeLifecycleForIndex = async (indexNames, httpClient = getHttpClient()) => { const response = await httpClient.post(`${apiPrefix}/index/remove`, { indexNames }); // Only track successful actions. - trackUiMetric(UIM_POLICY_DETACH_INDEX); + trackUiMetric('count', UIM_POLICY_DETACH_INDEX); return response.data; }; export const addLifecyclePolicyToIndex = async (body, httpClient = getHttpClient()) => { const response = await httpClient.post(`${apiPrefix}/index/add`, body); // Only track successful actions. - trackUiMetric(UIM_POLICY_ATTACH_INDEX); + trackUiMetric('count', UIM_POLICY_ATTACH_INDEX); return response.data; }; export const addLifecyclePolicyToTemplate = async (body, httpClient = getHttpClient()) => { const response = await httpClient.post(`${apiPrefix}/template`, body); // Only track successful actions. - trackUiMetric(UIM_POLICY_ATTACH_INDEX_TEMPLATE); + trackUiMetric('count', UIM_POLICY_ATTACH_INDEX_TEMPLATE); return response.data; }; diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/services/ui_metric.js b/x-pack/legacy/plugins/index_lifecycle_management/public/services/ui_metric.js index 8382d16596d8..d33d6688f492 100644 --- a/x-pack/legacy/plugins/index_lifecycle_management/public/services/ui_metric.js +++ b/x-pack/legacy/plugins/index_lifecycle_management/public/services/ui_metric.js @@ -5,8 +5,7 @@ */ import { get } from 'lodash'; - -import { trackUiMetric as track } from '../../../../../../src/legacy/core_plugins/ui_metric/public'; +import { createUiStatsReporter } from '../../../../../../src/legacy/core_plugins/ui_metric/public'; import { UIM_APP_NAME, @@ -29,9 +28,7 @@ import { defaultHotPhase, } from '../store/defaults'; -export function trackUiMetric(metricType) { - track(UIM_APP_NAME, metricType); -} +export const trackUiMetric = createUiStatsReporter(UIM_APP_NAME); export function getUiMetricsForPhases(phases) { const phaseUiMetrics = [{ diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/store/actions/lifecycle.js b/x-pack/legacy/plugins/index_lifecycle_management/public/store/actions/lifecycle.js index 9acb76d0936e..64d73e4aaeff 100644 --- a/x-pack/legacy/plugins/index_lifecycle_management/public/store/actions/lifecycle.js +++ b/x-pack/legacy/plugins/index_lifecycle_management/public/store/actions/lifecycle.js @@ -32,7 +32,7 @@ export const saveLifecyclePolicy = (lifecycle, isNew) => async () => { const uiMetrics = getUiMetricsForPhases(lifecycle.phases); uiMetrics.push(isNew ? UIM_POLICY_CREATE : UIM_POLICY_UPDATE); - trackUiMetric(uiMetrics); + trackUiMetric('count', uiMetrics); const message = i18n.translate('xpack.indexLifecycleMgmt.editPolicy.successfulSaveMessage', { diff --git a/x-pack/legacy/plugins/index_management/public/app.js b/x-pack/legacy/plugins/index_management/public/app.js index c911759932e4..8f25183a21bc 100644 --- a/x-pack/legacy/plugins/index_management/public/app.js +++ b/x-pack/legacy/plugins/index_management/public/app.js @@ -11,7 +11,7 @@ import { IndexManagementHome } from './sections/home'; import { trackUiMetric } from './services'; export const App = () => { - useEffect(() => trackUiMetric(UIM_APP_LOAD), []); + useEffect(() => trackUiMetric('loaded', UIM_APP_LOAD), []); return ( diff --git a/x-pack/legacy/plugins/index_management/public/sections/home/index_list/index_table/index_table.js b/x-pack/legacy/plugins/index_management/public/sections/home/index_list/index_table/index_table.js index 708cd1eb8867..ddd3c14dba69 100644 --- a/x-pack/legacy/plugins/index_management/public/sections/home/index_list/index_table/index_table.js +++ b/x-pack/legacy/plugins/index_management/public/sections/home/index_list/index_table/index_table.js @@ -220,7 +220,7 @@ export class IndexTable extends Component { className="indTable__link" data-test-subj="indexTableIndexNameLink" onClick={() => { - trackUiMetric(UIM_SHOW_DETAILS_CLICK); + trackUiMetric('click', UIM_SHOW_DETAILS_CLICK); openDetailPanel(value); }} > diff --git a/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/templates_list.tsx b/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/templates_list.tsx index e5b5e9ad281e..a17bfaf1ce27 100644 --- a/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/templates_list.tsx +++ b/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/templates_list.tsx @@ -19,7 +19,7 @@ import { SectionError, SectionLoading } from '../../../components'; import { TemplatesTable } from './templates_table'; import { loadIndexTemplates } from '../../../services/api'; import { Template } from '../../../../common/types'; -import { trackUiMetric } from '../../../services/track_ui_metric'; +import { trackUiMetric, METRIC_TYPE } from '../../../services/track_ui_metric'; import { UIM_TEMPLATE_LIST_LOAD } from '../../../../common/constants'; export const TemplatesList: React.FunctionComponent = () => { @@ -38,7 +38,7 @@ export const TemplatesList: React.FunctionComponent = () => { // Track component loaded useEffect(() => { - trackUiMetric(UIM_TEMPLATE_LIST_LOAD); + trackUiMetric(METRIC_TYPE.LOADED, UIM_TEMPLATE_LIST_LOAD); }, []); if (isLoading) { diff --git a/x-pack/legacy/plugins/index_management/public/services/api.ts b/x-pack/legacy/plugins/index_management/public/services/api.ts index 0b16d02a733b..3a6c9fdf7b23 100644 --- a/x-pack/legacy/plugins/index_management/public/services/api.ts +++ b/x-pack/legacy/plugins/index_management/public/services/api.ts @@ -32,7 +32,7 @@ import { import { TAB_SETTINGS, TAB_MAPPING, TAB_STATS } from '../constants'; -import { trackUiMetric } from './track_ui_metric'; +import { trackUiMetric, METRIC_TYPE } from './track_ui_metric'; import { useRequest, sendRequest } from './use_request'; import { Template } from '../../common/types'; @@ -67,8 +67,8 @@ export async function closeIndices(indices: string[]) { }; const response = await httpClient.post(`${apiPrefix}/indices/close`, body); // Only track successful requests. - const actionType = indices.length > 1 ? UIM_INDEX_CLOSE_MANY : UIM_INDEX_CLOSE; - trackUiMetric(actionType); + const eventName = indices.length > 1 ? UIM_INDEX_CLOSE_MANY : UIM_INDEX_CLOSE; + trackUiMetric(METRIC_TYPE.COUNT, eventName); return response.data; } @@ -78,8 +78,8 @@ export async function deleteIndices(indices: string[]) { }; const response = await httpClient.post(`${apiPrefix}/indices/delete`, body); // Only track successful requests. - const actionType = indices.length > 1 ? UIM_INDEX_DELETE_MANY : UIM_INDEX_DELETE; - trackUiMetric(actionType); + const eventName = indices.length > 1 ? UIM_INDEX_DELETE_MANY : UIM_INDEX_DELETE; + trackUiMetric(METRIC_TYPE.COUNT, eventName); return response.data; } @@ -89,8 +89,8 @@ export async function openIndices(indices: string[]) { }; const response = await httpClient.post(`${apiPrefix}/indices/open`, body); // Only track successful requests. - const actionType = indices.length > 1 ? UIM_INDEX_OPEN_MANY : UIM_INDEX_OPEN; - trackUiMetric(actionType); + const eventName = indices.length > 1 ? UIM_INDEX_OPEN_MANY : UIM_INDEX_OPEN; + trackUiMetric(METRIC_TYPE.COUNT, eventName); return response.data; } @@ -100,8 +100,8 @@ export async function refreshIndices(indices: string[]) { }; const response = await httpClient.post(`${apiPrefix}/indices/refresh`, body); // Only track successful requests. - const actionType = indices.length > 1 ? UIM_INDEX_REFRESH_MANY : UIM_INDEX_REFRESH; - trackUiMetric(actionType); + const eventName = indices.length > 1 ? UIM_INDEX_REFRESH_MANY : UIM_INDEX_REFRESH; + trackUiMetric(METRIC_TYPE.COUNT, eventName); return response.data; } @@ -111,8 +111,8 @@ export async function flushIndices(indices: string[]) { }; const response = await httpClient.post(`${apiPrefix}/indices/flush`, body); // Only track successful requests. - const actionType = indices.length > 1 ? UIM_INDEX_FLUSH_MANY : UIM_INDEX_FLUSH; - trackUiMetric(actionType); + const eventName = indices.length > 1 ? UIM_INDEX_FLUSH_MANY : UIM_INDEX_FLUSH; + trackUiMetric(METRIC_TYPE.COUNT, eventName); return response.data; } @@ -123,8 +123,8 @@ export async function forcemergeIndices(indices: string[], maxNumSegments: strin }; const response = await httpClient.post(`${apiPrefix}/indices/forcemerge`, body); // Only track successful requests. - const actionType = indices.length > 1 ? UIM_INDEX_FORCE_MERGE_MANY : UIM_INDEX_FORCE_MERGE; - trackUiMetric(actionType); + const eventName = indices.length > 1 ? UIM_INDEX_FORCE_MERGE_MANY : UIM_INDEX_FORCE_MERGE; + trackUiMetric(METRIC_TYPE.COUNT, eventName); return response.data; } @@ -134,8 +134,8 @@ export async function clearCacheIndices(indices: string[]) { }; const response = await httpClient.post(`${apiPrefix}/indices/clear_cache`, body); // Only track successful requests. - const actionType = indices.length > 1 ? UIM_INDEX_CLEAR_CACHE_MANY : UIM_INDEX_CLEAR_CACHE; - trackUiMetric(actionType); + const eventName = indices.length > 1 ? UIM_INDEX_CLEAR_CACHE_MANY : UIM_INDEX_CLEAR_CACHE; + trackUiMetric(METRIC_TYPE.COUNT, eventName); return response.data; } export async function freezeIndices(indices: string[]) { @@ -144,8 +144,8 @@ export async function freezeIndices(indices: string[]) { }; const response = await httpClient.post(`${apiPrefix}/indices/freeze`, body); // Only track successful requests. - const actionType = indices.length > 1 ? UIM_INDEX_FREEZE_MANY : UIM_INDEX_FREEZE; - trackUiMetric(actionType); + const eventName = indices.length > 1 ? UIM_INDEX_FREEZE_MANY : UIM_INDEX_FREEZE; + trackUiMetric(METRIC_TYPE.COUNT, eventName); return response.data; } export async function unfreezeIndices(indices: string[]) { @@ -154,8 +154,8 @@ export async function unfreezeIndices(indices: string[]) { }; const response = await httpClient.post(`${apiPrefix}/indices/unfreeze`, body); // Only track successful requests. - const actionType = indices.length > 1 ? UIM_INDEX_UNFREEZE_MANY : UIM_INDEX_UNFREEZE; - trackUiMetric(actionType); + const eventName = indices.length > 1 ? UIM_INDEX_UNFREEZE_MANY : UIM_INDEX_UNFREEZE; + trackUiMetric(METRIC_TYPE.COUNT, eventName); return response.data; } @@ -167,7 +167,7 @@ export async function loadIndexSettings(indexName: string) { export async function updateIndexSettings(indexName: string, settings: object) { const response = await httpClient.put(`${apiPrefix}/settings/${indexName}`, settings); // Only track successful requests. - trackUiMetric(UIM_UPDATE_SETTINGS); + trackUiMetric(METRIC_TYPE.COUNT, UIM_UPDATE_SETTINGS); return response; } diff --git a/x-pack/legacy/plugins/index_management/public/services/track_ui_metric.ts b/x-pack/legacy/plugins/index_management/public/services/track_ui_metric.ts index 6378fa0aa413..cf5b04d513bc 100644 --- a/x-pack/legacy/plugins/index_management/public/services/track_ui_metric.ts +++ b/x-pack/legacy/plugins/index_management/public/services/track_ui_metric.ts @@ -4,9 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { trackUiMetric as track } from '../../../../../../src/legacy/core_plugins/ui_metric/public'; +import { + createUiStatsReporter, + METRIC_TYPE, +} from '../../../../../../src/legacy/core_plugins/ui_metric/public'; import { UIM_APP_NAME } from '../../common/constants'; -export function trackUiMetric(metricType: string) { - track(UIM_APP_NAME, metricType); -} +export { METRIC_TYPE }; +export const trackUiMetric = createUiStatsReporter(UIM_APP_NAME); diff --git a/x-pack/legacy/plugins/index_management/public/services/use_request.ts b/x-pack/legacy/plugins/index_management/public/services/use_request.ts index 4d275ec124c4..2168ec4f655d 100644 --- a/x-pack/legacy/plugins/index_management/public/services/use_request.ts +++ b/x-pack/legacy/plugins/index_management/public/services/use_request.ts @@ -6,7 +6,7 @@ import { useEffect, useState } from 'react'; import { getHttpClient } from './api'; -import { trackUiMetric } from './track_ui_metric'; +import { trackUiMetric, METRIC_TYPE } from './track_ui_metric'; interface SendRequest { path?: string; @@ -35,7 +35,7 @@ export const sendRequest = async ({ // Track successful request if (uimActionType) { - trackUiMetric(uimActionType); + trackUiMetric(METRIC_TYPE.COUNT, uimActionType); } return { diff --git a/x-pack/legacy/plugins/index_management/public/store/reducers/detail_panel.js b/x-pack/legacy/plugins/index_management/public/store/reducers/detail_panel.js index 47949f175185..6a3df9d38ee6 100644 --- a/x-pack/legacy/plugins/index_management/public/store/reducers/detail_panel.js +++ b/x-pack/legacy/plugins/index_management/public/store/reducers/detail_panel.js @@ -54,7 +54,7 @@ export const detailPanel = handleActions( }; if (panelTypeToUiMetricMap[panelType]) { - trackUiMetric(panelTypeToUiMetricMap[panelType]); + trackUiMetric('count', panelTypeToUiMetricMap[panelType]); } return { diff --git a/x-pack/legacy/plugins/infra/public/hooks/use_track_metric.tsx b/x-pack/legacy/plugins/infra/public/hooks/use_track_metric.tsx new file mode 100644 index 000000000000..54359cf0aa69 --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/hooks/use_track_metric.tsx @@ -0,0 +1,74 @@ +/* + * 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 { useEffect } from 'react'; +import { + createUiStatsReporter, + METRIC_TYPE, +} from '../../../../../../src/legacy/core_plugins/ui_metric/public'; + +/** + * Note: The UI Metric plugin will take care of sending this data to the telemetry server. + * You can find these metrics stored at: + * stack_stats.kibana.plugins.ui_metric.{app}.{metric}(__delayed_{n}ms)? + * which will be an array of objects each containing a key, representing the metric, and + * a value, which will be a counter + */ + +type ObservabilityApp = 'infra_metrics' | 'infra_logs' | 'apm' | 'uptime'; + +const trackerCache = new Map>(); + +function getTrackerForApp(app: string) { + const cached = trackerCache.get(app); + if (cached) { + return cached; + } + + const tracker = createUiStatsReporter(app); + trackerCache.set(app, tracker); + + return tracker; +} + +interface TrackOptions { + app: ObservabilityApp; + metricType?: METRIC_TYPE; + delay?: number; // in ms +} +type EffectDeps = unknown[]; + +type TrackMetricOptions = TrackOptions & { metric: string }; + +export { METRIC_TYPE }; +export function useTrackMetric( + { app, metric, metricType = METRIC_TYPE.COUNT, delay = 0 }: TrackMetricOptions, + effectDependencies: EffectDeps = [] +) { + useEffect(() => { + let decoratedMetric = metric; + if (delay > 0) { + decoratedMetric += `__delayed_${delay}ms`; + } + const trackUiMetric = getTrackerForApp(app); + const id = setTimeout(() => trackUiMetric(metricType, decoratedMetric), Math.max(delay, 0)); + return () => clearTimeout(id); + }, effectDependencies); +} + +/** + * useTrackPageview is a convenience wrapper for tracking a pageview + * Its metrics will be found at: + * stack_stats.kibana.plugins.ui_metric.{app}.pageview__{path}(__delayed_{n}ms)? + */ +type TrackPageviewProps = TrackOptions & { path: string }; + +export function useTrackPageview( + { path, ...rest }: TrackPageviewProps, + effectDependencies: EffectDeps = [] +) { + useTrackMetric({ ...rest, metric: `pageview__${path}` }, effectDependencies); +} diff --git a/x-pack/legacy/plugins/remote_clusters/__jest__/client_integration/helpers/setup_environment.js b/x-pack/legacy/plugins/remote_clusters/__jest__/client_integration/helpers/setup_environment.js index ba02da248ce1..c3bf8668af8d 100644 --- a/x-pack/legacy/plugins/remote_clusters/__jest__/client_integration/helpers/setup_environment.js +++ b/x-pack/legacy/plugins/remote_clusters/__jest__/client_integration/helpers/setup_environment.js @@ -13,7 +13,9 @@ import { fatalError, toastNotifications } from 'ui/notify'; // eslint-disable-li import { init as initBreadcrumb } from '../../../public/app/services/breadcrumb'; import { init as initHttp } from '../../../public/app/services/http'; import { init as initNotification } from '../../../public/app/services/notification'; +import { init as initUiMetric } from '../../../public/app/services/ui_metric'; import { init as initHttpRequests } from './http_requests'; +import { createUiStatsReporter } from '../../../../../../../src/legacy/core_plugins/ui_metric/public'; export const setupEnvironment = () => { chrome.breadcrumbs = { @@ -23,6 +25,7 @@ export const setupEnvironment = () => { initHttp(axios.create({ adapter: axiosXhrAdapter }), (path) => path); initBreadcrumb(() => {}, MANAGEMENT_BREADCRUMB); initNotification(toastNotifications, fatalError); + initUiMetric(createUiStatsReporter); const { server, httpRequestsMockHelpers } = initHttpRequests(); diff --git a/x-pack/legacy/plugins/remote_clusters/public/app/app.js b/x-pack/legacy/plugins/remote_clusters/public/app/app.js index 4062f6e476b8..6fea66ad03c9 100644 --- a/x-pack/legacy/plugins/remote_clusters/public/app/app.js +++ b/x-pack/legacy/plugins/remote_clusters/public/app/app.js @@ -9,7 +9,7 @@ import PropTypes from 'prop-types'; import { Switch, Route, Redirect } from 'react-router-dom'; import { CRUD_APP_BASE_PATH, UIM_APP_LOAD } from './constants'; -import { registerRouter, setUserHasLeftApp, trackUiMetric } from './services'; +import { registerRouter, setUserHasLeftApp, trackUiMetric, METRIC_TYPE } from './services'; import { RemoteClusterList, RemoteClusterAdd, RemoteClusterEdit } from './sections'; export class App extends Component { @@ -34,7 +34,7 @@ export class App extends Component { } componentDidMount() { - trackUiMetric(UIM_APP_LOAD); + trackUiMetric(METRIC_TYPE.LOADED, UIM_APP_LOAD); } componentWillUnmount() { diff --git a/x-pack/legacy/plugins/remote_clusters/public/app/sections/remote_cluster_list/remote_cluster_table/remote_cluster_table.js b/x-pack/legacy/plugins/remote_clusters/public/app/sections/remote_cluster_list/remote_cluster_table/remote_cluster_table.js index 6677d8f0c6fb..9c128ed6f744 100644 --- a/x-pack/legacy/plugins/remote_clusters/public/app/sections/remote_cluster_list/remote_cluster_table/remote_cluster_table.js +++ b/x-pack/legacy/plugins/remote_clusters/public/app/sections/remote_cluster_list/remote_cluster_table/remote_cluster_table.js @@ -21,7 +21,7 @@ import { } from '@elastic/eui'; import { CRUD_APP_BASE_PATH, UIM_SHOW_DETAILS_CLICK } from '../../../constants'; -import { getRouterLinkProps, trackUiMetric } from '../../../services'; +import { getRouterLinkProps, trackUiMetric, METRIC_TYPE } from '../../../services'; import { ConnectionStatus, RemoveClusterButtonProvider } from '../components'; export class RemoteClusterTable extends Component { @@ -91,7 +91,7 @@ export class RemoteClusterTable extends Component { { - trackUiMetric(UIM_SHOW_DETAILS_CLICK); + trackUiMetric(METRIC_TYPE.CLICK, UIM_SHOW_DETAILS_CLICK); openDetailPanel(name); }} > diff --git a/x-pack/legacy/plugins/remote_clusters/public/app/services/api.js b/x-pack/legacy/plugins/remote_clusters/public/app/services/api.js index 77a26221f7be..d7df62eef95e 100644 --- a/x-pack/legacy/plugins/remote_clusters/public/app/services/api.js +++ b/x-pack/legacy/plugins/remote_clusters/public/app/services/api.js @@ -5,7 +5,7 @@ */ import { UIM_CLUSTER_ADD, UIM_CLUSTER_UPDATE } from '../constants'; -import { trackUserRequest } from './track_ui_metric'; +import { trackUserRequest } from './ui_metric'; import { sendGet, sendPost, sendPut, sendDelete } from './http'; export async function loadClusters() { diff --git a/x-pack/legacy/plugins/remote_clusters/public/app/services/index.js b/x-pack/legacy/plugins/remote_clusters/public/app/services/index.js index 51308b4c0788..218576cc5195 100644 --- a/x-pack/legacy/plugins/remote_clusters/public/app/services/index.js +++ b/x-pack/legacy/plugins/remote_clusters/public/app/services/index.js @@ -40,4 +40,5 @@ export { export { trackUiMetric, -} from './track_ui_metric'; + METRIC_TYPE, +} from './ui_metric'; diff --git a/x-pack/legacy/plugins/remote_clusters/public/app/services/track_ui_metric.js b/x-pack/legacy/plugins/remote_clusters/public/app/services/track_ui_metric.js deleted file mode 100644 index 7b2f0d040786..000000000000 --- a/x-pack/legacy/plugins/remote_clusters/public/app/services/track_ui_metric.js +++ /dev/null @@ -1,26 +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 { trackUiMetric as track } from '../../../../../../../src/legacy/core_plugins/ui_metric/public'; -import { UIM_APP_NAME } from '../constants'; - -export function trackUiMetric(actionType) { - track(UIM_APP_NAME, actionType); -} - -/** - * Transparently return provided request Promise, while allowing us to track - * a successful completion of the request. - */ -export function trackUserRequest(request, actionType) { - // Only track successful actions. - return request.then(response => { - trackUiMetric(actionType); - // We return the response immediately without waiting for the tracking request to resolve, - // to avoid adding additional latency. - return response; - }); -} diff --git a/x-pack/legacy/plugins/remote_clusters/public/app/services/ui_metric.ts b/x-pack/legacy/plugins/remote_clusters/public/app/services/ui_metric.ts index 16f09ee10973..36a23476c187 100644 --- a/x-pack/legacy/plugins/remote_clusters/public/app/services/ui_metric.ts +++ b/x-pack/legacy/plugins/remote_clusters/public/app/services/ui_metric.ts @@ -5,13 +5,28 @@ */ import { UIM_APP_NAME } from '../constants'; +import { + createUiStatsReporter, + METRIC_TYPE, +} from '../../../../../../../src/legacy/core_plugins/ui_metric/public'; -export let track: any; +export let trackUiMetric: ReturnType; +export { METRIC_TYPE }; -export function init(_track: any): void { - track = _track; +export function init(getReporter: typeof createUiStatsReporter): void { + trackUiMetric = getReporter(UIM_APP_NAME); } -export function trackUiMetric(actionType: string): any { - return track(UIM_APP_NAME, actionType); +/** + * Transparently return provided request Promise, while allowing us to track + * a successful completion of the request. + */ +export function trackUserRequest(request: Promise, eventName: string) { + // Only track successful actions. + return request.then((response: any) => { + trackUiMetric(METRIC_TYPE.COUNT, eventName); + // We return the response immediately without waiting for the tracking request to resolve, + // to avoid adding additional latency. + return response; + }); } diff --git a/x-pack/legacy/plugins/remote_clusters/public/app/store/actions/remove_clusters.js b/x-pack/legacy/plugins/remote_clusters/public/app/store/actions/remove_clusters.js index c9cbc377f9e2..e85a40e8ed28 100644 --- a/x-pack/legacy/plugins/remote_clusters/public/app/store/actions/remove_clusters.js +++ b/x-pack/legacy/plugins/remote_clusters/public/app/store/actions/remove_clusters.js @@ -12,6 +12,7 @@ import { UIM_CLUSTER_REMOVE, UIM_CLUSTER_REMOVE_MANY } from '../../constants'; import { removeClusterRequest as sendRemoveClusterRequest, trackUiMetric, + METRIC_TYPE, } from '../../services'; import { @@ -83,7 +84,7 @@ export const removeClusters = (names) => async (dispatch, getState) => { if (itemsDeleted.length > 0) { // Only track successful requests. - trackUiMetric(names.length > 1 ? UIM_CLUSTER_REMOVE_MANY : UIM_CLUSTER_REMOVE); + trackUiMetric(METRIC_TYPE.COUNT, names.length > 1 ? UIM_CLUSTER_REMOVE_MANY : UIM_CLUSTER_REMOVE); if (itemsDeleted.length === 1) { toasts.addSuccess(i18n.translate('xpack.remoteClusters.removeAction.successSingleNotificationTitle', { diff --git a/x-pack/legacy/plugins/remote_clusters/public/plugin.js b/x-pack/legacy/plugins/remote_clusters/public/plugin.js index 3ea7c8c13d45..c370bf7243d1 100644 --- a/x-pack/legacy/plugins/remote_clusters/public/plugin.js +++ b/x-pack/legacy/plugins/remote_clusters/public/plugin.js @@ -35,7 +35,7 @@ export class Plugin { if (getInjectedVar('remoteClustersUiEnabled')) { const { management: { getSection, breadcrumb: managementBreadcrumb }, - uiMetric: { track }, + uiMetric: { createUiStatsReporter }, } = pluginsStart; const esSection = getSection('elasticsearch'); @@ -49,7 +49,7 @@ export class Plugin { // Initialize services initBreadcrumbs(setBreadcrumbs, managementBreadcrumb); initDocumentation(`${elasticWebsiteUrl}guide/en/elasticsearch/reference/${docLinkVersion}/`); - initUiMetric(track); + initUiMetric(createUiStatsReporter); initNotification(toasts, fatalError); const unmountReactApp = () => { diff --git a/x-pack/legacy/plugins/remote_clusters/public/shim.ts b/x-pack/legacy/plugins/remote_clusters/public/shim.ts index 213624d8e0d5..83975fa4bd0f 100644 --- a/x-pack/legacy/plugins/remote_clusters/public/shim.ts +++ b/x-pack/legacy/plugins/remote_clusters/public/shim.ts @@ -9,7 +9,7 @@ import { management, MANAGEMENT_BREADCRUMB } from 'ui/management'; import { fatalError } from 'ui/notify'; import { DOC_LINK_VERSION, ELASTIC_WEBSITE_URL } from 'ui/documentation_links'; -import { trackUiMetric as track } from '../../../../../src/legacy/core_plugins/ui_metric/public'; +import { createUiStatsReporter } from '../../../../../src/legacy/core_plugins/ui_metric/public'; export function createShim() { const { @@ -35,7 +35,7 @@ export function createShim() { breadcrumb: MANAGEMENT_BREADCRUMB, }, uiMetric: { - track, + createUiStatsReporter, }, }, }; diff --git a/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_create_review.test.js b/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_create_review.test.js index aa6ff1325c11..7acbbf800d63 100644 --- a/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_create_review.test.js +++ b/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_create_review.test.js @@ -19,10 +19,6 @@ jest.mock('ui/chrome', () => ({ jest.mock('lodash/function/debounce', () => fn => fn); -jest.mock('../../../../../../src/legacy/core_plugins/ui_metric/public', () => ({ - trackUiMetric: jest.fn(), -})); - const { setup } = pageHelpers.jobCreate; describe('Create Rollup Job, step 5: Metrics', () => { diff --git a/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_list.test.js b/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_list.test.js index 5a629c094a63..09aa13b9fefe 100644 --- a/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_list.test.js +++ b/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_list.test.js @@ -28,10 +28,6 @@ jest.mock('ui/chrome', () => ({ } })); -jest.mock('../../../../../../src/legacy/core_plugins/ui_metric/public', () => ({ - trackUiMetric: jest.fn(), -})); - jest.mock('../../public/crud_app/services', () => { const services = require.requireActual('../../public/crud_app/services'); return { diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/app.js b/x-pack/legacy/plugins/rollup/public/crud_app/app.js index e039bf1d40fc..0e4219409749 100644 --- a/x-pack/legacy/plugins/rollup/public/crud_app/app.js +++ b/x-pack/legacy/plugins/rollup/public/crud_app/app.js @@ -10,7 +10,7 @@ import { HashRouter, Switch, Route, Redirect } from 'react-router-dom'; import { UIM_APP_LOAD } from '../../common'; import { CRUD_APP_BASE_PATH } from './constants'; -import { registerRouter, setUserHasLeftApp, trackUiMetric } from './services'; +import { registerRouter, setUserHasLeftApp, trackUiMetric, METRIC_TYPE } from './services'; import { JobList, JobCreate } from './sections'; class ShareRouter extends Component { @@ -41,7 +41,7 @@ class ShareRouter extends Component { export class App extends Component { // eslint-disable-line react/no-multi-comp componentDidMount() { - trackUiMetric(UIM_APP_LOAD); + trackUiMetric(METRIC_TYPE.LOADED, UIM_APP_LOAD); } componentWillUnmount() { diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.js b/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.js index 445b436c123d..1b9d77dd128a 100644 --- a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.js +++ b/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.js @@ -33,7 +33,7 @@ import { UIM_DETAIL_PANEL_METRICS_TAB_CLICK, UIM_DETAIL_PANEL_JSON_TAB_CLICK, } from '../../../../../common'; -import { trackUiMetric } from '../../../services'; +import { trackUiMetric, METRIC_TYPE } from '../../../services'; import { JobActionMenu, @@ -114,7 +114,7 @@ export class DetailPanelUi extends Component { renderedTabs.push( { - trackUiMetric(tabToUiMetricMap[tab]); + trackUiMetric(METRIC_TYPE.CLICK, tabToUiMetricMap[tab]); openDetailPanel({ panelType: tab, jobId: id }); }} isSelected={isSelected} diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.js b/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.js index 21d8a5a984ee..e1c885156faa 100644 --- a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.js +++ b/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.js @@ -30,7 +30,7 @@ import { } from '@elastic/eui'; import { UIM_SHOW_DETAILS_CLICK } from '../../../../../common'; -import { trackUiMetric } from '../../../services'; +import { trackUiMetric, METRIC_TYPE } from '../../../services'; import { JobActionMenu, JobStatus } from '../../components'; const COLUMNS = [{ @@ -259,7 +259,7 @@ export class JobTableUi extends Component { content = ( { - trackUiMetric(UIM_SHOW_DETAILS_CLICK); + trackUiMetric(METRIC_TYPE.CLICK, UIM_SHOW_DETAILS_CLICK); openDetailPanel(job.id); }} > diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/services/index.js b/x-pack/legacy/plugins/rollup/public/crud_app/services/index.js index 7cc1a0abfdd3..fdff4ebc8973 100644 --- a/x-pack/legacy/plugins/rollup/public/crud_app/services/index.js +++ b/x-pack/legacy/plugins/rollup/public/crud_app/services/index.js @@ -95,4 +95,5 @@ export { export { trackUiMetric, + METRIC_TYPE, } from './track_ui_metric'; diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/services/track_ui_metric.js b/x-pack/legacy/plugins/rollup/public/crud_app/services/track_ui_metric.js index efb97e05a05e..de4d43b3f8c1 100644 --- a/x-pack/legacy/plugins/rollup/public/crud_app/services/track_ui_metric.js +++ b/x-pack/legacy/plugins/rollup/public/crud_app/services/track_ui_metric.js @@ -4,12 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { trackUiMetric as track } from '../../../../../../../src/legacy/core_plugins/ui_metric/public'; +import { createUiStatsReporter, METRIC_TYPE } from '../../../../../../../src/legacy/core_plugins/ui_metric/public'; import { UIM_APP_NAME } from '../../../common'; -export function trackUiMetric(actionType) { - track(UIM_APP_NAME, actionType); -} +export const trackUiMetric = createUiStatsReporter(UIM_APP_NAME); +export { METRIC_TYPE }; /** * Transparently return provided request Promise, while allowing us to track @@ -18,7 +17,7 @@ export function trackUiMetric(actionType) { export function trackUserRequest(request, actionType) { // Only track successful actions. return request.then(response => { - trackUiMetric(actionType); + trackUiMetric(METRIC_TYPE.LOADED, actionType); // We return the response immediately without waiting for the tracking request to resolve, // to avoid adding additional latency. return response; diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/index.tsx b/x-pack/legacy/plugins/siem/public/components/flyout/index.tsx index 531894b5e487..ac999b4287fc 100644 --- a/x-pack/legacy/plugins/siem/public/components/flyout/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/flyout/index.tsx @@ -14,12 +14,11 @@ import { ActionCreator } from 'typescript-fsa'; import { State, timelineSelectors } from '../../store'; import { DataProvider } from '../timeline/data_providers/data_provider'; - import { FlyoutButton } from './button'; import { Pane } from './pane'; import { timelineActions } from '../../store/actions'; import { DEFAULT_TIMELINE_WIDTH } from '../timeline/body/helpers'; -import { trackUiAction as track } from '../../lib/track_usage'; +import { trackUiAction as track, METRIC_TYPE } from '../../lib/track_usage'; /** The height in pixels of the flyout header, exported for use in height calculations */ export const flyoutHeaderHeight: number = 60; @@ -100,7 +99,7 @@ export const FlyoutComponent = pure( show={!show} timelineId={timelineId} onOpen={() => { - track('open_timeline'); + track(METRIC_TYPE.LOADED, 'open_timeline'); showTimeline!({ id: timelineId, show: true }); }} /> diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.tsx b/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.tsx index 823f41065fbb..94f6dcb22a63 100644 --- a/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.tsx @@ -8,7 +8,7 @@ import * as React from 'react'; import styled from 'styled-components'; import { getHostsUrl, getNetworkUrl, getOverviewUrl, getTimelinesUrl } from '../../link_to'; -import { trackUiAction as track } from '../../../lib/track_usage'; +import { trackUiAction as track, METRIC_TYPE } from '../../../lib/track_usage'; import * as i18n from '../translations'; @@ -101,7 +101,7 @@ export class TabNavigation extends React.PureComponent { - track(`tab_${tab.id}`); + track(METRIC_TYPE.CLICK, `tab_${tab.id}`); }} > {tab.name} diff --git a/x-pack/legacy/plugins/siem/public/lib/track_usage/index.ts b/x-pack/legacy/plugins/siem/public/lib/track_usage/index.ts index b2ebbaa2baea..934e5c441e07 100644 --- a/x-pack/legacy/plugins/siem/public/lib/track_usage/index.ts +++ b/x-pack/legacy/plugins/siem/public/lib/track_usage/index.ts @@ -5,7 +5,11 @@ */ // @ts-ignore -import { trackUiMetric } from '../../../../../../../src/legacy/core_plugins/ui_metric/public'; +import { + createUiStatsReporter, + METRIC_TYPE, +} from '../../../../../../../src/legacy/core_plugins/ui_metric/public'; import { APP_ID } from '../../../common/constants'; -export const trackUiAction = (metricType: string) => trackUiMetric(APP_ID, metricType); +export const trackUiAction = createUiStatsReporter(APP_ID); +export { METRIC_TYPE }; diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/services/ui_metric/ui_metric.ts b/x-pack/legacy/plugins/snapshot_restore/public/app/services/ui_metric/ui_metric.ts index 1f06ff83e4ee..a2f0a6e1a548 100644 --- a/x-pack/legacy/plugins/snapshot_restore/public/app/services/ui_metric/ui_metric.ts +++ b/x-pack/legacy/plugins/snapshot_restore/public/app/services/ui_metric/ui_metric.ts @@ -4,16 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ import { UIM_APP_NAME } from '../../constants'; +import { + createUiStatsReporter, + METRIC_TYPE, +} from '../../../../../../../../src/legacy/core_plugins/ui_metric/public'; class UiMetricService { - public track: any = () => {}; + track?: ReturnType; - public init = (track: any): void => { - this.track = track; + public init = (getReporter: typeof createUiStatsReporter): void => { + this.track = getReporter(UIM_APP_NAME); }; - public trackUiMetric = (actionType: string): any => { - return this.track(UIM_APP_NAME, actionType); + public trackUiMetric = (eventName: string): void => { + if (!this.track) throw Error('UiMetricService not initialized.'); + return this.track(METRIC_TYPE.COUNT, eventName); }; } diff --git a/x-pack/legacy/plugins/snapshot_restore/public/plugin.ts b/x-pack/legacy/plugins/snapshot_restore/public/plugin.ts index b3fce61d3876..f590237bec73 100644 --- a/x-pack/legacy/plugins/snapshot_restore/public/plugin.ts +++ b/x-pack/legacy/plugins/snapshot_restore/public/plugin.ts @@ -39,7 +39,7 @@ export class Plugin { textService.init(i18n); breadcrumbService.init(chrome, management.constants.BREADCRUMB); documentationLinksService.init(documentation.esDocBasePath, documentation.esPluginDocBasePath); - uiMetricService.init(uiMetric.track); + uiMetricService.init(uiMetric.createUiStatsReporter); const unmountReactApp = (): void => { const elem = document.getElementById(REACT_ROOT_ID); diff --git a/x-pack/legacy/plugins/snapshot_restore/public/shim.ts b/x-pack/legacy/plugins/snapshot_restore/public/shim.ts index 45083d997fdb..77604f90fd57 100644 --- a/x-pack/legacy/plugins/snapshot_restore/public/shim.ts +++ b/x-pack/legacy/plugins/snapshot_restore/public/shim.ts @@ -16,7 +16,7 @@ import routes from 'ui/routes'; import { HashRouter } from 'react-router-dom'; // @ts-ignore: allow traversal to fail on x-pack build -import { trackUiMetric as track } from '../../../../../src/legacy/core_plugins/ui_metric/public'; +import { createUiStatsReporter } from '../../../../../src/legacy/core_plugins/ui_metric/public'; export interface AppCore { i18n: { @@ -63,7 +63,7 @@ export interface Plugins extends AppPlugins { }; }; uiMetric: { - track: typeof track; + createUiStatsReporter: typeof createUiStatsReporter; }; } @@ -118,7 +118,7 @@ export function createShim(): { core: Core; plugins: Plugins } { }, }, uiMetric: { - track, + createUiStatsReporter, }, }, }; diff --git a/x-pack/legacy/plugins/telemetry/common/constants.ts b/x-pack/legacy/plugins/telemetry/common/constants.ts index 8c6cfb6e558d..c50f36ac9449 100644 --- a/x-pack/legacy/plugins/telemetry/common/constants.ts +++ b/x-pack/legacy/plugins/telemetry/common/constants.ts @@ -81,3 +81,9 @@ export const KIBANA_LOCALIZATION_STATS_TYPE = 'localization'; * @type {string} */ export const TELEMETRY_QUERY_SOURCE = 'TELEMETRY'; + +/** + * UI metric usage type + * @type {string} + */ +export const UI_METRIC_USAGE_TYPE = 'ui_metric'; diff --git a/x-pack/legacy/plugins/telemetry/index.ts b/x-pack/legacy/plugins/telemetry/index.ts index 4125dae82f9f..6d4e9be67fb9 100644 --- a/x-pack/legacy/plugins/telemetry/index.ts +++ b/x-pack/legacy/plugins/telemetry/index.ts @@ -17,6 +17,7 @@ import { telemetryPlugin } from './server'; import { createLocalizationUsageCollector, createTelemetryUsageCollector, + createUiMetricUsageCollector, } from './server/collectors'; const ENDPOINT_VERSION = 'v2'; @@ -72,10 +73,7 @@ export const telemetry = (kibana: any) => { activeSpace: null, }; }, - hacks: [ - 'plugins/telemetry/hacks/telemetry_opt_in', - 'plugins/telemetry/hacks/telemetry_trigger', - ], + hacks: ['plugins/telemetry/hacks/telemetry_init', 'plugins/telemetry/hacks/telemetry_opt_in'], mappings, }, init(server: Server) { @@ -89,6 +87,7 @@ export const telemetry = (kibana: any) => { // register collectors server.usage.collectorSet.register(createLocalizationUsageCollector(server)); server.usage.collectorSet.register(createTelemetryUsageCollector(server)); + server.usage.collectorSet.register(createUiMetricUsageCollector(server)); // expose server.expose('telemetryCollectionInterval', REPORT_INTERVAL_MS); diff --git a/x-pack/legacy/plugins/telemetry/public/hacks/telemetry_trigger.js b/x-pack/legacy/plugins/telemetry/public/hacks/telemetry_init.ts similarity index 74% rename from x-pack/legacy/plugins/telemetry/public/hacks/telemetry_trigger.js rename to x-pack/legacy/plugins/telemetry/public/hacks/telemetry_init.ts index efee9e07dc7b..c44da2b36bf1 100644 --- a/x-pack/legacy/plugins/telemetry/public/hacks/telemetry_trigger.js +++ b/x-pack/legacy/plugins/telemetry/public/hacks/telemetry_init.ts @@ -4,24 +4,31 @@ * you may not use this file except in compliance with the Elastic License. */ +// @ts-ignore import { uiModules } from 'ui/modules'; +// @ts-ignore import { Path } from 'plugins/xpack_main/services/path'; -import { Telemetry } from './telemetry'; -import { fetchTelemetry } from './fetch_telemetry'; +// @ts-ignore import { npStart } from 'ui/new_platform'; +// @ts-ignore +import { Telemetry } from './telemetry'; +// @ts-ignore +import { fetchTelemetry } from './fetch_telemetry'; + +function telemetryInit($injector: any) { + const $http = $injector.get('$http'); -function telemetryStart($injector) { const telemetryEnabled = npStart.core.injectedMetadata.getInjectedVar('telemetryEnabled'); if (telemetryEnabled) { // no telemetry for non-logged in users - if (Path.isUnauthenticated()) { return; } + if (Path.isUnauthenticated()) { + return; + } - const $http = $injector.get('$http'); const sender = new Telemetry($injector, () => fetchTelemetry($http)); - sender.start(); } } -uiModules.get('telemetry/hacks').run(telemetryStart); +uiModules.get('telemetry/hacks').run(telemetryInit); diff --git a/x-pack/legacy/plugins/telemetry/public/services/telemetry_opt_in.js b/x-pack/legacy/plugins/telemetry/public/services/telemetry_opt_in.js index a1b45b3fa91d..2fcd2012a152 100644 --- a/x-pack/legacy/plugins/telemetry/public/services/telemetry_opt_in.js +++ b/x-pack/legacy/plugins/telemetry/public/services/telemetry_opt_in.js @@ -17,7 +17,6 @@ export function TelemetryOptInProvider($injector, chrome) { getOptIn: () => currentOptInStatus, setOptIn: async (enabled) => { setCanTrackUiMetrics(enabled); - const $http = $injector.get('$http'); try { diff --git a/x-pack/legacy/plugins/telemetry/server/collectors/index.ts b/x-pack/legacy/plugins/telemetry/server/collectors/index.ts index 2123d5a9251a..c9b94a8ea5d5 100644 --- a/x-pack/legacy/plugins/telemetry/server/collectors/index.ts +++ b/x-pack/legacy/plugins/telemetry/server/collectors/index.ts @@ -11,4 +11,5 @@ export { getLocalStats } from './local'; export { getStats } from './get_stats'; export { encryptTelemetry } from './encryption'; export { createTelemetryUsageCollector } from './usage'; +export { createUiMetricUsageCollector } from './ui_metric'; export { createLocalizationUsageCollector } from './localization'; diff --git a/x-pack/legacy/plugins/canvas/public/lib/ui_metric.js b/x-pack/legacy/plugins/telemetry/server/collectors/ui_metric/index.ts similarity index 54% rename from x-pack/legacy/plugins/canvas/public/lib/ui_metric.js rename to x-pack/legacy/plugins/telemetry/server/collectors/ui_metric/index.ts index 57ec5b03356c..f5a49587d49c 100644 --- a/x-pack/legacy/plugins/canvas/public/lib/ui_metric.js +++ b/x-pack/legacy/plugins/telemetry/server/collectors/ui_metric/index.ts @@ -4,9 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import { trackUiMetric } from '../../../../../../src/legacy/core_plugins/ui_metric/public'; - -const APP = 'canvas'; -export const trackCanvasUiMetric = uiMetrics => { - trackUiMetric(APP, uiMetrics); -}; +export { createUiMetricUsageCollector } from './telemetry_ui_metric_collector'; diff --git a/x-pack/legacy/plugins/telemetry/server/collectors/ui_metric/telemetry_ui_metric_collector.ts b/x-pack/legacy/plugins/telemetry/server/collectors/ui_metric/telemetry_ui_metric_collector.ts new file mode 100644 index 000000000000..a931400399b4 --- /dev/null +++ b/x-pack/legacy/plugins/telemetry/server/collectors/ui_metric/telemetry_ui_metric_collector.ts @@ -0,0 +1,45 @@ +/* + * 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 { UI_METRIC_USAGE_TYPE } from '../../../common/constants'; + +export function createUiMetricUsageCollector(server: any) { + const { collectorSet } = server.usage; + return collectorSet.makeUsageCollector({ + type: UI_METRIC_USAGE_TYPE, + fetch: async () => { + const { SavedObjectsClient, getSavedObjectsRepository } = server.savedObjects; + const { callWithInternalUser } = server.plugins.elasticsearch.getCluster('admin'); + const internalRepository = getSavedObjectsRepository(callWithInternalUser); + const savedObjectsClient = new SavedObjectsClient(internalRepository); + + const { saved_objects: rawUiMetrics } = await savedObjectsClient.find({ + type: 'ui-metric', + fields: ['count'], + }); + + const uiMetricsByAppName = rawUiMetrics.reduce((accum: any, rawUiMetric: any) => { + const { + id, + attributes: { count }, + } = rawUiMetric; + + const [appName, metricType] = id.split(':'); + + if (!accum[appName]) { + accum[appName] = []; + } + + const pair = { key: metricType, value: count }; + accum[appName].push(pair); + return accum; + }, {}); + + return uiMetricsByAppName; + }, + isReady: () => true, + }); +} diff --git a/x-pack/package.json b/x-pack/package.json index 6c5379c28d2f..2c8fcdeb0676 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -178,6 +178,7 @@ "@elastic/nodegit": "0.25.0-alpha.22", "@elastic/numeral": "2.3.3", "@elastic/request-crypto": "^1.0.2", + "@kbn/analytics": "1.0.0", "@kbn/babel-preset": "1.0.0", "@kbn/config-schema": "1.0.0", "@kbn/elastic-idx": "1.0.0", diff --git a/yarn.lock b/yarn.lock index db20e2c06009..31320716262d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -27796,7 +27796,7 @@ typescript-fsa@^2.0.0, typescript-fsa@^2.5.0: resolved "https://registry.yarnpkg.com/typescript-fsa/-/typescript-fsa-2.5.0.tgz#1baec01b5e8f5f34c322679d1327016e9e294faf" integrity sha1-G67AG16PXzTDImedEycBbp4pT68= -typescript@3.5.3, typescript@^3.0.3, typescript@^3.3.3333, typescript@^3.4.5, typescript@~3.0.3, typescript@~3.3.3333, typescript@~3.4.3: +typescript@3.5.1, typescript@3.5.3, typescript@^3.0.3, typescript@^3.3.3333, typescript@^3.4.5, typescript@~3.0.3, typescript@~3.3.3333, typescript@~3.4.3: version "3.5.3" resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.5.3.tgz#c830f657f93f1ea846819e929092f5fe5983e977" integrity sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g==