mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Telemetry] Migrate ui_metric plugin to NP under usageCollecti… (#51972)
* move cloud dir to plugins from legacy * create ui_metrics in NP * migrate first plugin * ui_metric plugin uses npStart * sinin mock * karma mocks * type check fix * rename old configs * fix mocks and use configs * use fo debug * ui_metric deprecation configs * remove commented out code * remove unused type import * mock ui_metric in client_integration * jest.mock ui/new_platform * fix all failing tests * platform team code review fixes * reset interval back to default * apm cypress config use usageCollection * revert kibana.yml change * remove license type from NP def * undo revert of NP type * code review fixes * report schema in a separate dir
This commit is contained in:
parent
0dac43516e
commit
3658220048
46 changed files with 566 additions and 336 deletions
|
@ -20,3 +20,4 @@
|
|||
export { ReportHTTP, Reporter, ReporterConfig } from './reporter';
|
||||
export { UiStatsMetricType, METRIC_TYPE } from './metrics';
|
||||
export { Report, ReportManager } from './report';
|
||||
export { Storage } from './storage';
|
||||
|
|
|
@ -23,23 +23,25 @@ const REPORT_VERSION = 1;
|
|||
|
||||
export interface Report {
|
||||
reportVersion: typeof REPORT_VERSION;
|
||||
uiStatsMetrics: {
|
||||
[key: string]: {
|
||||
uiStatsMetrics?: Record<
|
||||
string,
|
||||
{
|
||||
key: string;
|
||||
appName: string;
|
||||
eventName: string;
|
||||
type: UiStatsMetricType;
|
||||
stats: Stats;
|
||||
};
|
||||
};
|
||||
userAgent?: {
|
||||
[key: string]: {
|
||||
}
|
||||
>;
|
||||
userAgent?: Record<
|
||||
string,
|
||||
{
|
||||
userAgent: string;
|
||||
key: string;
|
||||
type: METRIC_TYPE.USER_AGENT;
|
||||
appName: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
>;
|
||||
}
|
||||
|
||||
export class ReportManager {
|
||||
|
@ -49,14 +51,15 @@ export class ReportManager {
|
|||
this.report = report || ReportManager.createReport();
|
||||
}
|
||||
static createReport(): Report {
|
||||
return { reportVersion: REPORT_VERSION, uiStatsMetrics: {} };
|
||||
return { reportVersion: REPORT_VERSION };
|
||||
}
|
||||
public clearReport() {
|
||||
this.report = ReportManager.createReport();
|
||||
}
|
||||
public isReportEmpty(): boolean {
|
||||
const noUiStats = Object.keys(this.report.uiStatsMetrics).length === 0;
|
||||
const noUserAgent = !this.report.userAgent || Object.keys(this.report.userAgent).length === 0;
|
||||
const { uiStatsMetrics, userAgent } = this.report;
|
||||
const noUiStats = !uiStatsMetrics || Object.keys(uiStatsMetrics).length === 0;
|
||||
const noUserAgent = !userAgent || Object.keys(userAgent).length === 0;
|
||||
return noUiStats && noUserAgent;
|
||||
}
|
||||
private incrementStats(count: number, stats?: Stats): Stats {
|
||||
|
@ -113,14 +116,17 @@ export class ReportManager {
|
|||
case METRIC_TYPE.LOADED:
|
||||
case METRIC_TYPE.COUNT: {
|
||||
const { appName, type, eventName, count } = metric;
|
||||
const existingStats = (report.uiStatsMetrics[key] || {}).stats;
|
||||
this.report.uiStatsMetrics[key] = {
|
||||
key,
|
||||
appName,
|
||||
eventName,
|
||||
type,
|
||||
stats: this.incrementStats(count, existingStats),
|
||||
};
|
||||
if (report.uiStatsMetrics) {
|
||||
const existingStats = (report.uiStatsMetrics[key] || {}).stats;
|
||||
this.report.uiStatsMetrics = this.report.uiStatsMetrics || {};
|
||||
this.report.uiStatsMetrics[key] = {
|
||||
key,
|
||||
appName,
|
||||
eventName,
|
||||
type,
|
||||
stats: this.incrementStats(count, existingStats),
|
||||
};
|
||||
}
|
||||
return;
|
||||
}
|
||||
default:
|
||||
|
|
|
@ -19,7 +19,13 @@
|
|||
|
||||
import { Report } from './report';
|
||||
|
||||
export type Storage = Map<string, any>;
|
||||
export interface Storage<T = any, S = void> {
|
||||
get: (key: string) => T | null;
|
||||
set: (key: string, value: T) => S;
|
||||
remove: (key: string) => T | null;
|
||||
clear: () => void;
|
||||
}
|
||||
|
||||
export class ReportStorageManager {
|
||||
storageKey: string;
|
||||
private storage?: Storage;
|
||||
|
|
|
@ -23,6 +23,7 @@ import { mountWithIntl } from 'test_utils/enzyme_helpers';
|
|||
import { VisType } from '../legacy_imports';
|
||||
import { TypesStart } from '../../../../visualizations/public/np_ready/public/types';
|
||||
|
||||
jest.mock('ui/new_platform');
|
||||
jest.mock('../legacy_imports', () => ({
|
||||
State: () => null,
|
||||
AppState: () => null,
|
||||
|
|
|
@ -18,7 +18,6 @@
|
|||
*/
|
||||
|
||||
import moment from 'moment';
|
||||
import { setCanTrackUiMetrics } from 'ui/ui_metric';
|
||||
// @ts-ignore
|
||||
import { banners, toastNotifications } from 'ui/notify';
|
||||
import { npStart } from 'ui/new_platform';
|
||||
|
@ -69,8 +68,6 @@ export function TelemetryOptInProvider($injector: any, chrome: any, sendOptInSta
|
|||
'telemetryNotifyUserAboutOptInDefault'
|
||||
) as boolean;
|
||||
|
||||
setCanTrackUiMetrics(currentOptInStatus);
|
||||
|
||||
const provider = {
|
||||
getBannerId: () => bannerId,
|
||||
getOptInBannerNoticeId: () => optInBannerNoticeId,
|
||||
|
@ -116,7 +113,6 @@ export function TelemetryOptInProvider($injector: any, chrome: any, sendOptInSta
|
|||
if (!allowChangingOptInStatus) {
|
||||
return;
|
||||
}
|
||||
setCanTrackUiMetrics(enabled);
|
||||
const $http = $injector.get('$http');
|
||||
|
||||
try {
|
||||
|
|
|
@ -1,78 +0,0 @@
|
|||
# UI Metric app
|
||||
|
||||
## Purpose
|
||||
|
||||
The purpose of the UI Metric app is to provide a tool for gathering data on how users interact with
|
||||
various UIs within Kibana. It's useful for gathering _aggregate_ information, e.g. "How many times
|
||||
has Button X been clicked" or "How many times has Page Y been viewed".
|
||||
|
||||
With some finagling, it's even possible to add more meaning to the info you gather, such as "How many
|
||||
visualizations were created in less than 5 minutes".
|
||||
|
||||
### What it doesn't do
|
||||
|
||||
The UI Metric app doesn't gather any metadata around a user interaction, e.g. the user's identity,
|
||||
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 `createUiStatsReporter` helper function from UI Metric app:
|
||||
|
||||
```js
|
||||
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, `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 events, e.g. `trackMetric(METRIC_TYPE.*, ['my_event1', 'my_event2', 'my_event3'])`.
|
||||
|
||||
### Disallowed characters
|
||||
|
||||
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
|
||||
|
||||
If you want to track how long it takes a user to do something, you'll need to implement the timing
|
||||
logic yourself. You'll also need to predefine some buckets into which the UI metric can fall.
|
||||
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 `eventName` of `create_vis_1m`, `create_vis_5m`, `create_vis_20m`, or `create_vis_infinity`.
|
||||
|
||||
## How it works
|
||||
|
||||
Under the hood, your app and metric type will be stored in a saved object of type `user-metric` and the
|
||||
ID `ui-metric:my_app:my_metric`. This saved object will have a `count` property which will be incremented
|
||||
every time the above URI is hit.
|
||||
|
||||
These saved objects are automatically consumed by the stats API and surfaced under the
|
||||
`ui_metric` namespace.
|
||||
|
||||
```json
|
||||
{
|
||||
"ui_metric": {
|
||||
"my_app": [
|
||||
{
|
||||
"key": "my_metric",
|
||||
"value": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
By storing these metrics and their counts as key-value pairs, we can add more metrics without having
|
||||
to worry about exceeding the 1000-field soft limit in Elasticsearch.
|
|
@ -18,10 +18,7 @@
|
|||
*/
|
||||
|
||||
import { resolve } from 'path';
|
||||
import JoiNamespace from 'joi';
|
||||
import { Server } from 'hapi';
|
||||
import { Legacy } from '../../../../kibana';
|
||||
import { registerUiMetricRoute } from './server/routes/api/ui_metric';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function(kibana: any) {
|
||||
|
@ -29,25 +26,16 @@ 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 {
|
||||
uiMetricEnabled: config.get('ui_metric.enabled'),
|
||||
debugUiMetric: config.get('ui_metric.debug'),
|
||||
};
|
||||
},
|
||||
mappings: require('./mappings.json'),
|
||||
hacks: ['plugins/ui_metric/hacks/ui_metric_init'],
|
||||
},
|
||||
init(server: Legacy.Server) {
|
||||
registerUiMetricRoute(server);
|
||||
const { getSavedObjectsRepository } = server.savedObjects;
|
||||
const { callWithInternalUser } = server.plugins.elasticsearch.getCluster('admin');
|
||||
const internalRepository = getSavedObjectsRepository(callWithInternalUser);
|
||||
const { usageCollection } = server.newPlatform.setup.plugins;
|
||||
|
||||
usageCollection.registerLegacySavedObjects(internalRepository);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -17,5 +17,5 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export { createUiStatsReporter, trackUserAgent } from './services/telemetry_analytics';
|
||||
export { createUiStatsReporter } from './services/telemetry_analytics';
|
||||
export { METRIC_TYPE, UiStatsMetricType } from '@kbn/analytics';
|
||||
|
|
|
@ -16,61 +16,9 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { npSetup } from 'ui/new_platform';
|
||||
|
||||
import { Reporter, UiStatsMetricType } from '@kbn/analytics';
|
||||
// @ts-ignore
|
||||
import { addSystemApiHeader } from 'ui/system_api';
|
||||
|
||||
let telemetryReporter: Reporter;
|
||||
|
||||
export const setTelemetryReporter = (aTelemetryReporter: Reporter): void => {
|
||||
telemetryReporter = aTelemetryReporter;
|
||||
export const createUiStatsReporter = (appName: string) => {
|
||||
const { usageCollection } = npSetup.plugins;
|
||||
return usageCollection.reportUiStats.bind(usageCollection, appName);
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
export const trackUserAgent = (appName: string) => {
|
||||
if (telemetryReporter) {
|
||||
return telemetryReporter.reportUserAgent(appName);
|
||||
}
|
||||
};
|
||||
|
||||
interface AnalyicsReporterConfig {
|
||||
localStorage: any;
|
||||
debug: boolean;
|
||||
kfetch: any;
|
||||
}
|
||||
|
||||
export function createAnalyticsReporter(config: AnalyicsReporterConfig) {
|
||||
const { localStorage, debug, kfetch } = config;
|
||||
|
||||
return new Reporter({
|
||||
debug,
|
||||
storage: localStorage,
|
||||
async http(report) {
|
||||
const response = await kfetch({
|
||||
method: 'POST',
|
||||
pathname: '/api/telemetry/report',
|
||||
body: JSON.stringify(report),
|
||||
headers: addSystemApiHeader({}),
|
||||
});
|
||||
|
||||
if (response.status !== 'ok') {
|
||||
throw Error('Unable to store report.');
|
||||
}
|
||||
return response;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,102 +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.
|
||||
*/
|
||||
|
||||
import Joi from 'joi';
|
||||
import { Report } from '@kbn/analytics';
|
||||
import { Server } from 'hapi';
|
||||
|
||||
export async function storeReport(server: any, report: Report) {
|
||||
const { getSavedObjectsRepository } = server.savedObjects;
|
||||
const { callWithInternalUser } = server.plugins.elasticsearch.getCluster('admin');
|
||||
const internalRepository = getSavedObjectsRepository(callWithInternalUser);
|
||||
|
||||
const uiStatsMetrics = report.uiStatsMetrics ? Object.entries(report.uiStatsMetrics) : [];
|
||||
const userAgents = report.userAgent ? Object.entries(report.userAgent) : [];
|
||||
return Promise.all([
|
||||
...userAgents.map(async ([key, metric]) => {
|
||||
const { userAgent } = metric;
|
||||
const savedObjectId = `${key}:${userAgent}`;
|
||||
return await internalRepository.create(
|
||||
'ui-metric',
|
||||
{ count: 1 },
|
||||
{
|
||||
id: savedObjectId,
|
||||
overwrite: true,
|
||||
}
|
||||
);
|
||||
}),
|
||||
...uiStatsMetrics.map(async ([key, metric]) => {
|
||||
const { appName, eventName } = metric;
|
||||
const savedObjectId = `${appName}:${eventName}`;
|
||||
return await internalRepository.incrementCounter('ui-metric', savedObjectId, 'count');
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
export function registerUiMetricRoute(server: Server) {
|
||||
server.route({
|
||||
method: 'POST',
|
||||
path: '/api/telemetry/report',
|
||||
options: {
|
||||
validate: {
|
||||
payload: Joi.object({
|
||||
reportVersion: Joi.number().optional(),
|
||||
userAgent: Joi.object()
|
||||
.pattern(
|
||||
/.*/,
|
||||
Joi.object({
|
||||
key: Joi.string().required(),
|
||||
type: Joi.string().required(),
|
||||
appName: Joi.string().required(),
|
||||
userAgent: Joi.string().required(),
|
||||
})
|
||||
)
|
||||
.allow(null)
|
||||
.optional(),
|
||||
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) => {
|
||||
try {
|
||||
const report = req.payload;
|
||||
await storeReport(server, report);
|
||||
return { status: 'ok' };
|
||||
} catch (error) {
|
||||
return { status: 'fail' };
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
|
@ -24,6 +24,7 @@ import { embeddablePluginMock } from '../../../../../plugins/embeddable/public/m
|
|||
import { expressionsPluginMock } from '../../../../../plugins/expressions/public/mocks';
|
||||
import { inspectorPluginMock } from '../../../../../plugins/inspector/public/mocks';
|
||||
import { uiActionsPluginMock } from '../../../../../plugins/ui_actions/public/mocks';
|
||||
import { usageCollectionPluginMock } from '../../../../../plugins/usage_collection/public/mocks';
|
||||
/* eslint-enable @kbn/eslint/no-restricted-paths */
|
||||
|
||||
export const pluginsMock = {
|
||||
|
@ -33,6 +34,7 @@ export const pluginsMock = {
|
|||
inspector: inspectorPluginMock.createSetupContract(),
|
||||
expressions: expressionsPluginMock.createSetupContract(),
|
||||
uiActions: uiActionsPluginMock.createSetupContract(),
|
||||
usageCollection: usageCollectionPluginMock.createSetupContract(),
|
||||
}),
|
||||
createStart: () => ({
|
||||
data: dataPluginMock.createStartContract(),
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
|
||||
import sinon from 'sinon';
|
||||
import { getFieldFormatsRegistry } from '../../../../test_utils/public/stub_field_formats';
|
||||
import { METRIC_TYPE } from '@kbn/analytics';
|
||||
|
||||
const mockObservable = () => {
|
||||
return {
|
||||
|
@ -50,6 +51,11 @@ export const npSetup = {
|
|||
uiSettings: mockUiSettings,
|
||||
},
|
||||
plugins: {
|
||||
usageCollection: {
|
||||
allowTrackUserAgent: sinon.fake(),
|
||||
reportUiStats: sinon.fake(),
|
||||
METRIC_TYPE,
|
||||
},
|
||||
embeddable: {
|
||||
registerEmbeddableFactory: sinon.fake(),
|
||||
},
|
||||
|
|
|
@ -32,6 +32,7 @@ import { DevToolsSetup, DevToolsStart } from '../../../../plugins/dev_tools/publ
|
|||
import { KibanaLegacySetup, KibanaLegacyStart } from '../../../../plugins/kibana_legacy/public';
|
||||
import { HomePublicPluginSetup, HomePublicPluginStart } from '../../../../plugins/home/public';
|
||||
import { SharePluginSetup, SharePluginStart } from '../../../../plugins/share/public';
|
||||
import { UsageCollectionSetup } from '../../../../plugins/usage_collection/public';
|
||||
|
||||
export interface PluginsSetup {
|
||||
data: ReturnType<DataPlugin['setup']>;
|
||||
|
@ -43,6 +44,7 @@ export interface PluginsSetup {
|
|||
dev_tools: DevToolsSetup;
|
||||
kibana_legacy: KibanaLegacySetup;
|
||||
share: SharePluginSetup;
|
||||
usageCollection: UsageCollectionSetup;
|
||||
}
|
||||
|
||||
export interface PluginsStart {
|
||||
|
|
|
@ -137,3 +137,83 @@ There are a few ways you can test that your usage collector is working properly.
|
|||
Keep it simple, and keep it to a model that Kibana will be able to understand. In short, that means don't rely on nested fields (arrays with objects). Flat arrays, such as arrays of strings are fine.
|
||||
2. **If I accumulate an event counter in server memory, which my fetch method returns, won't it reset when the Kibana server restarts?**
|
||||
Yes, but that is not a major concern. A visualization on such info might be a date histogram that gets events-per-second or something, which would be impacted by server restarts, so we'll have to offset the beginning of the time range when we detect that the latest metric is smaller than the earliest metric. That would be a pretty custom visualization, but perhaps future Kibana enhancements will be able to support that.
|
||||
|
||||
|
||||
# UI Metric app
|
||||
|
||||
## Purpose
|
||||
|
||||
The purpose of the UI Metric app is to provide a tool for gathering data on how users interact with
|
||||
various UIs within Kibana. It's useful for gathering _aggregate_ information, e.g. "How many times
|
||||
has Button X been clicked" or "How many times has Page Y been viewed".
|
||||
|
||||
With some finagling, it's even possible to add more meaning to the info you gather, such as "How many
|
||||
visualizations were created in less than 5 minutes".
|
||||
|
||||
### What it doesn't do
|
||||
|
||||
The UI Metric app doesn't gather any metadata around a user interaction, e.g. the user's identity,
|
||||
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 `createUiStatsReporter` helper function from UI Metric app:
|
||||
|
||||
```js
|
||||
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, `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 events, e.g. `trackMetric(METRIC_TYPE.*, ['my_event1', 'my_event2', 'my_event3'])`.
|
||||
|
||||
### Disallowed characters
|
||||
|
||||
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
|
||||
|
||||
If you want to track how long it takes a user to do something, you'll need to implement the timing
|
||||
logic yourself. You'll also need to predefine some buckets into which the UI metric can fall.
|
||||
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 `eventName` of `create_vis_1m`, `create_vis_5m`, `create_vis_20m`, or `create_vis_infinity`.
|
||||
|
||||
## How it works
|
||||
|
||||
Under the hood, your app and metric type will be stored in a saved object of type `user-metric` and the
|
||||
ID `ui-metric:my_app:my_metric`. This saved object will have a `count` property which will be incremented
|
||||
every time the above URI is hit.
|
||||
|
||||
These saved objects are automatically consumed by the stats API and surfaced under the
|
||||
`ui_metric` namespace.
|
||||
|
||||
```json
|
||||
{
|
||||
"ui_metric": {
|
||||
"my_app": [
|
||||
{
|
||||
"key": "my_metric",
|
||||
"value": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
By storing these metrics and their counts as key-value pairs, we can add more metrics without having
|
||||
to worry about exceeding the 1000-field soft limit in Elasticsearch.
|
|
@ -18,3 +18,4 @@
|
|||
*/
|
||||
|
||||
export const KIBANA_STATS_TYPE = 'kibana_stats';
|
||||
export const DEFAULT_MAXIMUM_WAIT_TIME_FOR_ALL_COLLECTORS_IN_S = 60;
|
||||
|
|
|
@ -3,5 +3,5 @@
|
|||
"configPath": ["usageCollection"],
|
||||
"version": "kibana",
|
||||
"server": true,
|
||||
"ui": false
|
||||
"ui": true
|
||||
}
|
||||
|
|
28
src/plugins/usage_collection/public/index.ts
Normal file
28
src/plugins/usage_collection/public/index.ts
Normal 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.
|
||||
*/
|
||||
|
||||
import { PluginInitializerContext } from '../../../core/public';
|
||||
import { UsageCollectionPlugin } from './plugin';
|
||||
|
||||
export { METRIC_TYPE } from '@kbn/analytics';
|
||||
export { UsageCollectionSetup } from './plugin';
|
||||
|
||||
export function plugin(initializerContext: PluginInitializerContext) {
|
||||
return new UsageCollectionPlugin(initializerContext);
|
||||
}
|
36
src/plugins/usage_collection/public/mocks.ts
Normal file
36
src/plugins/usage_collection/public/mocks.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* 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 { UsageCollectionSetup, METRIC_TYPE } from '.';
|
||||
|
||||
export type Setup = jest.Mocked<UsageCollectionSetup>;
|
||||
|
||||
const createSetupContract = (): Setup => {
|
||||
const setupContract: Setup = {
|
||||
allowTrackUserAgent: jest.fn(),
|
||||
reportUiStats: jest.fn(),
|
||||
METRIC_TYPE,
|
||||
};
|
||||
|
||||
return setupContract;
|
||||
};
|
||||
|
||||
export const usageCollectionPluginMock = {
|
||||
createSetupContract,
|
||||
};
|
91
src/plugins/usage_collection/public/plugin.ts
Normal file
91
src/plugins/usage_collection/public/plugin.ts
Normal file
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* 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 { Reporter, METRIC_TYPE } from '@kbn/analytics';
|
||||
import { Storage } from '../../kibana_utils/public';
|
||||
import { createReporter } from './services';
|
||||
import {
|
||||
PluginInitializerContext,
|
||||
Plugin,
|
||||
CoreSetup,
|
||||
CoreStart,
|
||||
HttpServiceBase,
|
||||
} from '../../../core/public';
|
||||
|
||||
interface PublicConfigType {
|
||||
uiMetric: {
|
||||
enabled: boolean;
|
||||
debug: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface UsageCollectionSetup {
|
||||
allowTrackUserAgent: (allow: boolean) => void;
|
||||
reportUiStats: Reporter['reportUiStats'];
|
||||
METRIC_TYPE: typeof METRIC_TYPE;
|
||||
}
|
||||
|
||||
export function isUnauthenticated(http: HttpServiceBase) {
|
||||
const { anonymousPaths } = http;
|
||||
return anonymousPaths.isAnonymous(window.location.pathname);
|
||||
}
|
||||
|
||||
export class UsageCollectionPlugin implements Plugin<UsageCollectionSetup> {
|
||||
private trackUserAgent: boolean = true;
|
||||
private reporter?: Reporter;
|
||||
private config: PublicConfigType;
|
||||
constructor(initializerContext: PluginInitializerContext) {
|
||||
this.config = initializerContext.config.get<PublicConfigType>();
|
||||
}
|
||||
|
||||
public setup({ http }: CoreSetup): UsageCollectionSetup {
|
||||
const localStorage = new Storage(window.localStorage);
|
||||
const debug = this.config.uiMetric.debug;
|
||||
|
||||
this.reporter = createReporter({
|
||||
localStorage,
|
||||
debug,
|
||||
fetch: http,
|
||||
});
|
||||
|
||||
return {
|
||||
allowTrackUserAgent: (allow: boolean) => {
|
||||
this.trackUserAgent = allow;
|
||||
},
|
||||
reportUiStats: this.reporter.reportUiStats,
|
||||
METRIC_TYPE,
|
||||
};
|
||||
}
|
||||
|
||||
public start({ http }: CoreStart) {
|
||||
if (!this.reporter) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.config.uiMetric.enabled && !isUnauthenticated(http)) {
|
||||
this.reporter.start();
|
||||
}
|
||||
|
||||
if (this.trackUserAgent) {
|
||||
this.reporter.reportUserAgent('kibana');
|
||||
}
|
||||
}
|
||||
|
||||
public stop() {}
|
||||
}
|
|
@ -17,29 +17,30 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
// @ts-ignore
|
||||
import { uiModules } from 'ui/modules';
|
||||
import chrome from 'ui/chrome';
|
||||
import { kfetch } from 'ui/kfetch';
|
||||
import {
|
||||
createAnalyticsReporter,
|
||||
setTelemetryReporter,
|
||||
trackUserAgent,
|
||||
} from '../services/telemetry_analytics';
|
||||
import { isUnauthenticated } from '../../../telemetry/public/services';
|
||||
import { Reporter, Storage } from '@kbn/analytics';
|
||||
import { HttpServiceBase } from 'kibana/public';
|
||||
|
||||
function telemetryInit($injector: any) {
|
||||
const uiMetricEnabled = chrome.getInjected('uiMetricEnabled');
|
||||
const debug = chrome.getInjected('debugUiMetric');
|
||||
if (!uiMetricEnabled || isUnauthenticated()) {
|
||||
return;
|
||||
}
|
||||
const localStorage = $injector.get('localStorage');
|
||||
|
||||
const uiReporter = createAnalyticsReporter({ localStorage, debug, kfetch });
|
||||
setTelemetryReporter(uiReporter);
|
||||
uiReporter.start();
|
||||
trackUserAgent('kibana');
|
||||
interface AnalyicsReporterConfig {
|
||||
localStorage: Storage;
|
||||
debug: boolean;
|
||||
fetch: HttpServiceBase;
|
||||
}
|
||||
|
||||
uiModules.get('kibana').run(telemetryInit);
|
||||
export function createReporter(config: AnalyicsReporterConfig): Reporter {
|
||||
const { localStorage, debug, fetch } = config;
|
||||
|
||||
return new Reporter({
|
||||
debug,
|
||||
storage: localStorage,
|
||||
async http(report) {
|
||||
const response = await fetch.post('/api/ui_metric/report', {
|
||||
body: JSON.stringify({ report }),
|
||||
});
|
||||
|
||||
if (response.status !== 'ok') {
|
||||
throw Error('Unable to store report.');
|
||||
}
|
||||
return response;
|
||||
},
|
||||
});
|
||||
}
|
|
@ -17,4 +17,4 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export const API_BASE_PATH = '/api/ui_metric';
|
||||
export { createReporter } from './create_reporter';
|
|
@ -17,8 +17,29 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { schema, TypeOf } from '@kbn/config-schema';
|
||||
import { PluginConfigDescriptor } from 'kibana/server';
|
||||
import { DEFAULT_MAXIMUM_WAIT_TIME_FOR_ALL_COLLECTORS_IN_S } from '../common/constants';
|
||||
|
||||
export const ConfigSchema = schema.object({
|
||||
maximumWaitTimeForAllCollectorsInS: schema.number({ defaultValue: 60 }),
|
||||
export const configSchema = schema.object({
|
||||
uiMetric: schema.object({
|
||||
enabled: schema.boolean({ defaultValue: true }),
|
||||
debug: schema.boolean({ defaultValue: schema.contextRef('dev') }),
|
||||
}),
|
||||
maximumWaitTimeForAllCollectorsInS: schema.number({
|
||||
defaultValue: DEFAULT_MAXIMUM_WAIT_TIME_FOR_ALL_COLLECTORS_IN_S,
|
||||
}),
|
||||
});
|
||||
|
||||
export type ConfigType = TypeOf<typeof configSchema>;
|
||||
|
||||
export const config: PluginConfigDescriptor<ConfigType> = {
|
||||
schema: configSchema,
|
||||
deprecations: ({ renameFromRoot }) => [
|
||||
renameFromRoot('ui_metric.enabled', 'usageCollection.uiMetric.enabled'),
|
||||
renameFromRoot('ui_metric.debug', 'usageCollection.uiMetric.debug'),
|
||||
],
|
||||
exposeToBrowser: {
|
||||
uiMetric: true,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -18,10 +18,9 @@
|
|||
*/
|
||||
|
||||
import { PluginInitializerContext } from '../../../../src/core/server';
|
||||
import { Plugin } from './plugin';
|
||||
import { ConfigSchema } from './config';
|
||||
import { UsageCollectionPlugin } from './plugin';
|
||||
|
||||
export { UsageCollectionSetup } from './plugin';
|
||||
export const config = { schema: ConfigSchema };
|
||||
export { config } from './config';
|
||||
export const plugin = (initializerContext: PluginInitializerContext) =>
|
||||
new Plugin(initializerContext);
|
||||
new UsageCollectionPlugin(initializerContext);
|
||||
|
|
|
@ -18,22 +18,25 @@
|
|||
*/
|
||||
|
||||
import { first } from 'rxjs/operators';
|
||||
import { TypeOf } from '@kbn/config-schema';
|
||||
import { ConfigSchema } from './config';
|
||||
import { PluginInitializerContext, Logger } from '../../../../src/core/server';
|
||||
import { ConfigType } from './config';
|
||||
import { PluginInitializerContext, Logger, CoreSetup } from '../../../../src/core/server';
|
||||
import { CollectorSet } from './collector';
|
||||
import { setupRoutes } from './routes';
|
||||
|
||||
export type UsageCollectionSetup = CollectorSet;
|
||||
export type UsageCollectionSetup = CollectorSet & {
|
||||
registerLegacySavedObjects: (legacySavedObjects: any) => void;
|
||||
};
|
||||
|
||||
export class Plugin {
|
||||
export class UsageCollectionPlugin {
|
||||
logger: Logger;
|
||||
private legacySavedObjects: any;
|
||||
constructor(private readonly initializerContext: PluginInitializerContext) {
|
||||
this.logger = this.initializerContext.logger.get();
|
||||
}
|
||||
|
||||
public async setup(): Promise<UsageCollectionSetup> {
|
||||
public async setup(core: CoreSetup) {
|
||||
const config = await this.initializerContext.config
|
||||
.create<TypeOf<typeof ConfigSchema>>()
|
||||
.create<ConfigType>()
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
|
||||
|
@ -42,7 +45,16 @@ export class Plugin {
|
|||
maximumWaitTimeForAllCollectorsInS: config.maximumWaitTimeForAllCollectorsInS,
|
||||
});
|
||||
|
||||
return collectorSet;
|
||||
const router = core.http.createRouter();
|
||||
const getLegacySavedObjects = () => this.legacySavedObjects;
|
||||
setupRoutes(router, getLegacySavedObjects);
|
||||
|
||||
return {
|
||||
...collectorSet,
|
||||
registerLegacySavedObjects: (legacySavedObjects: any) => {
|
||||
this.legacySavedObjects = legacySavedObjects;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public start() {
|
||||
|
|
|
@ -17,12 +17,5 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
let _canTrackUiMetrics = false;
|
||||
|
||||
export function setCanTrackUiMetrics(flag: boolean) {
|
||||
_canTrackUiMetrics = flag;
|
||||
}
|
||||
|
||||
export function getCanTrackUiMetrics(): boolean {
|
||||
return _canTrackUiMetrics;
|
||||
}
|
||||
export { storeReport } from './store_report';
|
||||
export { reportSchema } from './schema';
|
59
src/plugins/usage_collection/server/report/schema.ts
Normal file
59
src/plugins/usage_collection/server/report/schema.ts
Normal file
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* 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 { schema, TypeOf } from '@kbn/config-schema';
|
||||
import { METRIC_TYPE } from '@kbn/analytics';
|
||||
|
||||
export const reportSchema = schema.object({
|
||||
reportVersion: schema.maybe(schema.literal(1)),
|
||||
userAgent: schema.maybe(
|
||||
schema.recordOf(
|
||||
schema.string(),
|
||||
schema.object({
|
||||
key: schema.string(),
|
||||
type: schema.string(),
|
||||
appName: schema.string(),
|
||||
userAgent: schema.string(),
|
||||
})
|
||||
)
|
||||
),
|
||||
uiStatsMetrics: schema.maybe(
|
||||
schema.recordOf(
|
||||
schema.string(),
|
||||
schema.object({
|
||||
key: schema.string(),
|
||||
type: schema.oneOf([
|
||||
schema.literal<METRIC_TYPE>(METRIC_TYPE.CLICK),
|
||||
schema.literal<METRIC_TYPE>(METRIC_TYPE.LOADED),
|
||||
schema.literal<METRIC_TYPE>(METRIC_TYPE.COUNT),
|
||||
]),
|
||||
appName: schema.string(),
|
||||
eventName: schema.string(),
|
||||
stats: schema.object({
|
||||
min: schema.number(),
|
||||
sum: schema.number(),
|
||||
max: schema.number(),
|
||||
avg: schema.number(),
|
||||
}),
|
||||
})
|
||||
)
|
||||
),
|
||||
});
|
||||
|
||||
export type ReportSchemaType = TypeOf<typeof reportSchema>;
|
44
src/plugins/usage_collection/server/report/store_report.ts
Normal file
44
src/plugins/usage_collection/server/report/store_report.ts
Normal file
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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 { ReportSchemaType } from './schema';
|
||||
|
||||
export async function storeReport(internalRepository: any, report: ReportSchemaType) {
|
||||
const uiStatsMetrics = report.uiStatsMetrics ? Object.entries(report.uiStatsMetrics) : [];
|
||||
const userAgents = report.userAgent ? Object.entries(report.userAgent) : [];
|
||||
return Promise.all([
|
||||
...userAgents.map(async ([key, metric]) => {
|
||||
const { userAgent } = metric;
|
||||
const savedObjectId = `${key}:${userAgent}`;
|
||||
return await internalRepository.create(
|
||||
'ui-metric',
|
||||
{ count: 1 },
|
||||
{
|
||||
id: savedObjectId,
|
||||
overwrite: true,
|
||||
}
|
||||
);
|
||||
}),
|
||||
...uiStatsMetrics.map(async ([key, metric]) => {
|
||||
const { appName, eventName } = metric;
|
||||
const savedObjectId = `${appName}:${eventName}`;
|
||||
return await internalRepository.incrementCounter('ui-metric', savedObjectId, 'count');
|
||||
}),
|
||||
]);
|
||||
}
|
25
src/plugins/usage_collection/server/routes/index.ts
Normal file
25
src/plugins/usage_collection/server/routes/index.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* 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 { IRouter } from '../../../../../src/core/server';
|
||||
import { registerUiMetricRoute } from './report_metrics';
|
||||
|
||||
export function setupRoutes(router: IRouter, getLegacySavedObjects: any) {
|
||||
registerUiMetricRoute(router, getLegacySavedObjects);
|
||||
}
|
45
src/plugins/usage_collection/server/routes/report_metrics.ts
Normal file
45
src/plugins/usage_collection/server/routes/report_metrics.ts
Normal file
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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 { schema } from '@kbn/config-schema';
|
||||
import { IRouter } from '../../../../../src/core/server';
|
||||
import { storeReport, reportSchema } from '../report';
|
||||
|
||||
export function registerUiMetricRoute(router: IRouter, getLegacySavedObjects: () => any) {
|
||||
router.post(
|
||||
{
|
||||
path: '/api/ui_metric/report',
|
||||
validate: {
|
||||
body: schema.object({
|
||||
report: reportSchema,
|
||||
}),
|
||||
},
|
||||
},
|
||||
async (context, req, res) => {
|
||||
const { report } = req.body;
|
||||
try {
|
||||
const internalRepository = getLegacySavedObjects();
|
||||
await storeReport(internalRepository, report);
|
||||
return res.ok({ body: { status: 'ok' } });
|
||||
} catch (error) {
|
||||
return res.ok({ body: { status: 'fail' } });
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
|
@ -49,10 +49,10 @@ export default function ({ getService }) {
|
|||
}
|
||||
};
|
||||
await supertest
|
||||
.post('/api/telemetry/report')
|
||||
.post('/api/ui_metric/report')
|
||||
.set('kbn-xsrf', 'kibana')
|
||||
.set('content-type', 'application/json')
|
||||
.send(report)
|
||||
.send({ report })
|
||||
.expect(200);
|
||||
|
||||
const response = await es.search({ index: '.kibana', q: 'type:ui-metric' });
|
||||
|
@ -77,10 +77,10 @@ export default function ({ getService }) {
|
|||
}
|
||||
};
|
||||
await supertest
|
||||
.post('/api/telemetry/report')
|
||||
.post('/api/ui_metric/report')
|
||||
.set('kbn-xsrf', 'kibana')
|
||||
.set('content-type', 'application/json')
|
||||
.send(report)
|
||||
.send({ report })
|
||||
.expect(200);
|
||||
|
||||
const response = await es.search({ index: '.kibana', q: 'type:ui-metric' });
|
||||
|
|
|
@ -17,6 +17,8 @@ import {
|
|||
} from '../../../../utils/testHelpers';
|
||||
import { ApmPluginContextValue } from '../../../../context/ApmPluginContext';
|
||||
|
||||
jest.mock('ui/new_platform');
|
||||
|
||||
function wrapper({ children }: { children: ReactChild }) {
|
||||
return (
|
||||
<MockApmPluginContextWrapper
|
||||
|
|
|
@ -27,6 +27,7 @@ import { MockApmPluginContextWrapper } from '../../../../utils/testHelpers';
|
|||
jest.spyOn(history, 'push');
|
||||
jest.spyOn(history, 'replace');
|
||||
|
||||
jest.mock('ui/new_platform');
|
||||
function setup({
|
||||
urlParams,
|
||||
serviceTransactionTypes
|
||||
|
|
|
@ -10,9 +10,10 @@ import {
|
|||
withUnconnectedElementsLoadedTelemetry,
|
||||
WorkpadLoadedMetric,
|
||||
WorkpadLoadedWithErrorsMetric,
|
||||
} from '../workpad_telemetry';
|
||||
import { METRIC_TYPE } from '../../../../lib/ui_metric';
|
||||
} from './workpad_telemetry';
|
||||
import { METRIC_TYPE } from '../../../lib/ui_metric';
|
||||
|
||||
jest.mock('ui/new_platform');
|
||||
const trackMetric = jest.fn();
|
||||
const Component = withUnconnectedElementsLoadedTelemetry(() => <div />, trackMetric);
|
||||
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { updateFields, updateFormErrors } from './follower_index_form';
|
||||
|
||||
jest.mock('ui/new_platform');
|
||||
jest.mock('ui/indices', () => ({
|
||||
INDEX_ILLEGAL_CHARACTERS_VISIBLE: [],
|
||||
}));
|
||||
|
|
|
@ -8,6 +8,7 @@ import { reducer, initialState } from './api';
|
|||
import { API_STATUS } from '../../constants';
|
||||
import { apiRequestStart, apiRequestEnd, setApiError } from '../actions';
|
||||
|
||||
jest.mock('ui/new_platform');
|
||||
jest.mock('../../constants', () => ({
|
||||
API_STATUS: {
|
||||
IDLE: 'idle',
|
||||
|
|
|
@ -33,6 +33,8 @@ import {
|
|||
maximumDocumentsRequiredMessage,
|
||||
} from '../../public/store/selectors/lifecycle';
|
||||
|
||||
jest.mock('ui/new_platform');
|
||||
|
||||
let server;
|
||||
let store;
|
||||
const policy = {
|
||||
|
|
|
@ -17,6 +17,7 @@ import axiosXhrAdapter from 'axios/lib/adapters/xhr';
|
|||
import { setHttpClient } from '../../public/services/api';
|
||||
setHttpClient(axios.create({ adapter: axiosXhrAdapter }));
|
||||
import sinon from 'sinon';
|
||||
jest.mock('ui/new_platform');
|
||||
let server = null;
|
||||
|
||||
let store = null;
|
||||
|
|
|
@ -14,6 +14,8 @@ import {
|
|||
ilmFilterExtension,
|
||||
ilmSummaryExtension,
|
||||
} from '../public/extend_index_management';
|
||||
|
||||
jest.mock('ui/new_platform');
|
||||
const indexWithoutLifecyclePolicy = {
|
||||
health: 'yellow',
|
||||
status: 'open',
|
||||
|
|
|
@ -21,6 +21,7 @@ import {
|
|||
} from '../constants';
|
||||
|
||||
import { getUiMetricsForPhases } from './ui_metric';
|
||||
jest.mock('ui/new_platform');
|
||||
|
||||
describe('getUiMetricsForPhases', () => {
|
||||
test('gets cold phase', () => {
|
||||
|
|
|
@ -15,7 +15,6 @@ 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 = {
|
||||
|
@ -25,7 +24,7 @@ export const setupEnvironment = () => {
|
|||
initHttp(axios.create({ adapter: axiosXhrAdapter }), (path) => path);
|
||||
initBreadcrumb(() => {}, MANAGEMENT_BREADCRUMB);
|
||||
initNotification(toastNotifications, fatalError);
|
||||
initUiMetric(createUiStatsReporter);
|
||||
initUiMetric(() => () => {});
|
||||
|
||||
const { server, httpRequestsMockHelpers } = initHttpRequests();
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ import {
|
|||
tabToHumanizedMap,
|
||||
} from '../../components';
|
||||
|
||||
jest.mock('ui/new_platform');
|
||||
jest.mock('../../../services', () => {
|
||||
const services = require.requireActual('../../../services');
|
||||
return {
|
||||
|
|
|
@ -8,6 +8,8 @@ import { registerTestBed } from '../../../../../../../test_utils';
|
|||
import { rollupJobsStore } from '../../store';
|
||||
import { JobList } from './job_list';
|
||||
|
||||
jest.mock('ui/new_platform');
|
||||
|
||||
jest.mock('ui/chrome', () => ({
|
||||
addBasePath: () => {},
|
||||
breadcrumbs: { set: () => {} },
|
||||
|
|
|
@ -11,6 +11,7 @@ import { getJobs, jobCount } from '../../../../../fixtures';
|
|||
import { rollupJobsStore } from '../../../store';
|
||||
import { JobTable } from './job_table';
|
||||
|
||||
jest.mock('ui/new_platform');
|
||||
jest.mock('../../../services', () => {
|
||||
const services = require.requireActual('../../../services');
|
||||
return {
|
||||
|
|
|
@ -19,6 +19,7 @@ const testWidth = 640;
|
|||
const usersViewing = ['elastic'];
|
||||
|
||||
const mockUseKibanaCore = useKibanaCore as jest.Mock;
|
||||
jest.mock('ui/new_platform');
|
||||
jest.mock('../../../lib/compose/kibana_core');
|
||||
mockUseKibanaCore.mockImplementation(() => ({
|
||||
uiSettings: mockUiSettings,
|
||||
|
|
|
@ -15,6 +15,7 @@ import { HostsTableType } from '../../store/hosts/model';
|
|||
import { RouteSpyState } from '../../utils/route/types';
|
||||
import { SiemNavigationProps, SiemNavigationComponentProps } from './types';
|
||||
|
||||
jest.mock('ui/new_platform');
|
||||
jest.mock('./breadcrumbs', () => ({
|
||||
setBreadcrumbs: jest.fn(),
|
||||
}));
|
||||
|
|
|
@ -16,6 +16,8 @@ import { CONSTANTS } from '../../url_state/constants';
|
|||
import { TabNavigationComponent } from './';
|
||||
import { TabNavigationProps } from './types';
|
||||
|
||||
jest.mock('ui/new_platform');
|
||||
|
||||
describe('Tab Navigation', () => {
|
||||
const pageName = SiemPageName.hosts;
|
||||
const hostName = 'siem-window';
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue