[7.x] [telemetry] Analytics Package (#41113) (#41774)

This commit is contained in:
Ahmad Bamieh 2019-07-24 17:16:24 +03:00 committed by GitHub
parent 41739096a4
commit 980ec8caa3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
78 changed files with 1017 additions and 320 deletions

72
.github/CODEOWNERS vendored Normal file
View file

@ -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

View file

@ -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 <ahmadbamieh@gmail.com>",
"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"
}
}

View file

@ -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';

View file

@ -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<UiStatsMetricType>;
export type MetricType = keyof typeof METRIC_TYPE;
export enum METRIC_TYPE {
COUNT = 'count',
LOADED = 'loaded',
CLICK = 'click',
}

View file

@ -17,4 +17,9 @@
* under the License.
*/
export { registerUiMetricUsageCollector } from './collector';
export interface Stats {
min: number;
max: number;
sum: number;
avg: number;
}

View file

@ -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<T extends UiStatsMetricType> {
type: T;
appName: string;
eventName: string;
count?: number;
}
export interface UiStatsMetric<T extends UiStatsMetricType = UiStatsMetricType> {
type: T;
appName: string;
eventName: string;
count: number;
}
export function createUiStatsMetric<T extends UiStatsMetricType>({
type,
appName,
eventName,
count = 1,
}: UiStatsMetricConfig<T>): UiStatsMetric<T> {
return { type, appName, eventName, count };
}
export interface UiStatsMetricReport {
key: string;
appName: string;
eventName: string;
type: UiStatsMetricType;
stats: Stats;
}

View file

@ -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);
}
}
}

View file

@ -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<void>;
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;
}

View file

@ -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<string, any>;
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);
}
}

View file

@ -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<T extends any>(subj: T | T[]): T[] {
return Array.isArray(subj) ? subj : [subj];
}
export class UnreachableCaseError extends Error {
constructor(val: never) {
super(`Unreachable case: ${val}`);
}
}

View file

@ -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"
]
}

View file

@ -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');
});

View file

@ -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(`<AppName>`);
trackMetric(METRIC_TYPE.CLICK, `<EventName>`);
trackMetric('click', `<EventName>`);
```
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', <count> });`
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

View file

@ -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);
},
});
}

View file

@ -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);

View file

@ -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';

View file

@ -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 });
},
});
}

View file

@ -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 });
}
},
});
};
}

View file

@ -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);
}

View file

@ -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'));
});
});
});

View file

@ -11,6 +11,7 @@ import {
WorkpadLoadedMetric,
WorkpadLoadedWithErrorsMetric,
} from '../workpad_telemetry';
import { METRIC_TYPE } from '../../../../lib/ui_metric';
const trackMetric = jest.fn();
const Component = withUnconnectedElementsLoadedTelemetry(() => <div />, 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', () => {

View file

@ -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<P extends object>(
Component: React.ComponentType<P>,
trackMetric: (metric: string | string[]) => void = trackCanvasUiMetric
trackMetric = trackCanvasUiMetric
): React.SFC<P & ElementsLoadedTelemetryProps> {
return function ElementsLoadedTelemetry(
props: P & ElementsLoadedTelemetryProps
@ -117,11 +117,10 @@ export const withUnconnectedElementsLoadedTelemetry = function<P extends object>
resolvedArgsAreForWorkpad
) {
if (telemetryElementCounts.error > 0) {
trackMetric([WorkpadLoadedMetric, WorkpadLoadedWithErrorsMetric]);
trackMetric(METRIC_TYPE.LOADED, [WorkpadLoadedMetric, WorkpadLoadedWithErrorsMetric]);
} else {
trackMetric(WorkpadLoadedMetric);
trackMetric(METRIC_TYPE.LOADED, WorkpadLoadedMetric);
}
setHasReported(true);
}
});

View file

@ -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 => {

View file

@ -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

View file

@ -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 };

View file

@ -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('<AutoFollowPatternList />', () => {

View file

@ -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('<FollowerIndicesList />', () => {

View file

@ -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('<CrossClusterReplicationHome />', () => {

View file

@ -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();

View file

@ -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 (
<EuiLink
onClick={() => {
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"

View file

@ -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 (
<EuiLink
onClick={() => {
trackUiMetric(UIM_FOLLOWER_INDEX_SHOW_DETAILS_CLICK);
trackUiMetric(METRIC_TYPE.CLICK, UIM_FOLLOWER_INDEX_SHOW_DETAILS_CLICK);
selectFollowerIndex(name);
}}
data-test-subj="followerIndexLink"

View file

@ -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

View file

@ -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;

View file

@ -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 (
<HashRouter>

View file

@ -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}
</EuiLink>

View file

@ -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;
};

View file

@ -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 = [{

View file

@ -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',
{

View file

@ -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 (
<HashRouter>

View file

@ -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);
}}
>

View file

@ -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) {

View file

@ -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;
}

View file

@ -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);

View file

@ -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 {

View file

@ -54,7 +54,7 @@ export const detailPanel = handleActions(
};
if (panelTypeToUiMetricMap[panelType]) {
trackUiMetric(panelTypeToUiMetricMap[panelType]);
trackUiMetric('count', panelTypeToUiMetricMap[panelType]);
}
return {

View file

@ -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<string, ReturnType<typeof createUiStatsReporter>>();
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);
}

View file

@ -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();

View file

@ -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() {

View file

@ -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 {
<EuiLink
data-test-subj="remoteClustersTableListClusterLink"
onClick={() => {
trackUiMetric(UIM_SHOW_DETAILS_CLICK);
trackUiMetric(METRIC_TYPE.CLICK, UIM_SHOW_DETAILS_CLICK);
openDetailPanel(name);
}}
>

View file

@ -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() {

View file

@ -40,4 +40,5 @@ export {
export {
trackUiMetric,
} from './track_ui_metric';
METRIC_TYPE,
} from './ui_metric';

View file

@ -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;
});
}

View file

@ -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<typeof createUiStatsReporter>;
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<any>, 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;
});
}

View file

@ -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', {

View file

@ -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 = () => {

View file

@ -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,
},
},
};

View file

@ -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', () => {

View file

@ -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 {

View file

@ -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() {

View file

@ -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(
<EuiTab
onClick={() => {
trackUiMetric(tabToUiMetricMap[tab]);
trackUiMetric(METRIC_TYPE.CLICK, tabToUiMetricMap[tab]);
openDetailPanel({ panelType: tab, jobId: id });
}}
isSelected={isSelected}

View file

@ -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 = (
<EuiLink
onClick={() => {
trackUiMetric(UIM_SHOW_DETAILS_CLICK);
trackUiMetric(METRIC_TYPE.CLICK, UIM_SHOW_DETAILS_CLICK);
openDetailPanel(job.id);
}}
>

View file

@ -95,4 +95,5 @@ export {
export {
trackUiMetric,
METRIC_TYPE,
} from './track_ui_metric';

View file

@ -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;

View file

@ -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<Props>(
show={!show}
timelineId={timelineId}
onOpen={() => {
track('open_timeline');
track(METRIC_TYPE.LOADED, 'open_timeline');
showTimeline!({ id: timelineId, show: true });
}}
/>

View file

@ -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<TabNavigationProps, TabNa
disabled={tab.disabled}
isSelected={this.state.selectedTabId === tab.id}
onClick={() => {
track(`tab_${tab.id}`);
track(METRIC_TYPE.CLICK, `tab_${tab.id}`);
}}
>
{tab.name}

View file

@ -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 };

View file

@ -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<typeof createUiStatsReporter>;
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);
};
}

View file

@ -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);

View file

@ -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,
},
},
};

View file

@ -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';

View file

@ -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);

View file

@ -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);

View file

@ -17,7 +17,6 @@ export function TelemetryOptInProvider($injector, chrome) {
getOptIn: () => currentOptInStatus,
setOptIn: async (enabled) => {
setCanTrackUiMetrics(enabled);
const $http = $injector.get('$http');
try {

View file

@ -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';

View file

@ -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';

View file

@ -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,
});
}

View file

@ -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",

View file

@ -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==