mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Telemetry] Introduce UI Counters (#84224)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
e7d8dd48f3
commit
aa9a353205
119 changed files with 1647 additions and 401 deletions
|
@ -19,7 +19,7 @@ export interface UiSettingsParams<T = unknown>
|
|||
| [category](./kibana-plugin-core-public.uisettingsparams.category.md) | <code>string[]</code> | used to group the configured setting in the UI |
|
||||
| [deprecation](./kibana-plugin-core-public.uisettingsparams.deprecation.md) | <code>DeprecationSettings</code> | optional deprecation information. Used to generate a deprecation warning. |
|
||||
| [description](./kibana-plugin-core-public.uisettingsparams.description.md) | <code>string</code> | description provided to a user in UI |
|
||||
| [metric](./kibana-plugin-core-public.uisettingsparams.metric.md) | <code>{</code><br/><code> type: UiStatsMetricType;</code><br/><code> name: string;</code><br/><code> }</code> | Metric to track once this property changes |
|
||||
| [metric](./kibana-plugin-core-public.uisettingsparams.metric.md) | <code>{</code><br/><code> type: UiCounterMetricType;</code><br/><code> name: string;</code><br/><code> }</code> | Metric to track once this property changes |
|
||||
| [name](./kibana-plugin-core-public.uisettingsparams.name.md) | <code>string</code> | title in the UI |
|
||||
| [optionLabels](./kibana-plugin-core-public.uisettingsparams.optionlabels.md) | <code>Record<string, string></code> | text labels for 'select' type UI element |
|
||||
| [options](./kibana-plugin-core-public.uisettingsparams.options.md) | <code>string[]</code> | array of permitted values for this setting |
|
||||
|
|
|
@ -15,7 +15,7 @@ Metric to track once this property changes
|
|||
|
||||
```typescript
|
||||
metric?: {
|
||||
type: UiStatsMetricType;
|
||||
type: UiCounterMetricType;
|
||||
name: string;
|
||||
};
|
||||
```
|
||||
|
|
|
@ -177,6 +177,7 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
|
|||
| [SavedObjectsImportSuccess](./kibana-plugin-core-server.savedobjectsimportsuccess.md) | Represents a successful import. |
|
||||
| [SavedObjectsImportUnknownError](./kibana-plugin-core-server.savedobjectsimportunknownerror.md) | Represents a failure to import due to an unknown reason. |
|
||||
| [SavedObjectsImportUnsupportedTypeError](./kibana-plugin-core-server.savedobjectsimportunsupportedtypeerror.md) | Represents a failure to import due to having an unsupported saved object type. |
|
||||
| [SavedObjectsIncrementCounterField](./kibana-plugin-core-server.savedobjectsincrementcounterfield.md) | |
|
||||
| [SavedObjectsIncrementCounterOptions](./kibana-plugin-core-server.savedobjectsincrementcounteroptions.md) | |
|
||||
| [SavedObjectsMappingProperties](./kibana-plugin-core-server.savedobjectsmappingproperties.md) | Describe the fields of a [saved object type](./kibana-plugin-core-server.savedobjectstypemappingdefinition.md)<!-- -->. |
|
||||
| [SavedObjectsMigrationLogger](./kibana-plugin-core-server.savedobjectsmigrationlogger.md) | |
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsIncrementCounterField](./kibana-plugin-core-server.savedobjectsincrementcounterfield.md) > [fieldName](./kibana-plugin-core-server.savedobjectsincrementcounterfield.fieldname.md)
|
||||
|
||||
## SavedObjectsIncrementCounterField.fieldName property
|
||||
|
||||
The field name to increment the counter by.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
fieldName: string;
|
||||
```
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsIncrementCounterField](./kibana-plugin-core-server.savedobjectsincrementcounterfield.md) > [incrementBy](./kibana-plugin-core-server.savedobjectsincrementcounterfield.incrementby.md)
|
||||
|
||||
## SavedObjectsIncrementCounterField.incrementBy property
|
||||
|
||||
The number to increment the field by (defaults to 1).
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
incrementBy?: number;
|
||||
```
|
|
@ -0,0 +1,20 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsIncrementCounterField](./kibana-plugin-core-server.savedobjectsincrementcounterfield.md)
|
||||
|
||||
## SavedObjectsIncrementCounterField interface
|
||||
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export interface SavedObjectsIncrementCounterField
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [fieldName](./kibana-plugin-core-server.savedobjectsincrementcounterfield.fieldname.md) | <code>string</code> | The field name to increment the counter by. |
|
||||
| [incrementBy](./kibana-plugin-core-server.savedobjectsincrementcounterfield.incrementby.md) | <code>number</code> | The number to increment the field by (defaults to 1). |
|
||||
|
|
@ -4,12 +4,12 @@
|
|||
|
||||
## SavedObjectsRepository.incrementCounter() method
|
||||
|
||||
Increments all the specified counter fields by one. Creates the document if one doesn't exist for the given id.
|
||||
Increments all the specified counter fields (by one by default). Creates the document if one doesn't exist for the given id.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
incrementCounter<T = unknown>(type: string, id: string, counterFieldNames: string[], options?: SavedObjectsIncrementCounterOptions): Promise<SavedObject<T>>;
|
||||
incrementCounter<T = unknown>(type: string, id: string, counterFields: Array<string | SavedObjectsIncrementCounterField>, options?: SavedObjectsIncrementCounterOptions): Promise<SavedObject<T>>;
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
@ -18,7 +18,7 @@ incrementCounter<T = unknown>(type: string, id: string, counterFieldNames: strin
|
|||
| --- | --- | --- |
|
||||
| type | <code>string</code> | The type of saved object whose fields should be incremented |
|
||||
| id | <code>string</code> | The id of the document whose fields should be incremented |
|
||||
| counterFieldNames | <code>string[]</code> | An array of field names to increment |
|
||||
| counterFields | <code>Array<string | SavedObjectsIncrementCounterField></code> | An array of field names to increment or an array of [SavedObjectsIncrementCounterField](./kibana-plugin-core-server.savedobjectsincrementcounterfield.md) |
|
||||
| options | <code>SavedObjectsIncrementCounterOptions</code> | [SavedObjectsIncrementCounterOptions](./kibana-plugin-core-server.savedobjectsincrementcounteroptions.md) |
|
||||
|
||||
<b>Returns:</b>
|
||||
|
|
|
@ -26,7 +26,7 @@ export declare class SavedObjectsRepository
|
|||
| [deleteFromNamespaces(type, id, namespaces, options)](./kibana-plugin-core-server.savedobjectsrepository.deletefromnamespaces.md) | | Removes one or more namespaces from a given multi-namespace saved object. If no namespaces remain, the saved object is deleted entirely. This method and \[<code>addToNamespaces</code>\][SavedObjectsRepository.addToNamespaces()](./kibana-plugin-core-server.savedobjectsrepository.addtonamespaces.md) are the only ways to change which Spaces a multi-namespace saved object is shared to. |
|
||||
| [find(options)](./kibana-plugin-core-server.savedobjectsrepository.find.md) | | |
|
||||
| [get(type, id, options)](./kibana-plugin-core-server.savedobjectsrepository.get.md) | | Gets a single object |
|
||||
| [incrementCounter(type, id, counterFieldNames, options)](./kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md) | | Increments all the specified counter fields by one. Creates the document if one doesn't exist for the given id. |
|
||||
| [incrementCounter(type, id, counterFields, options)](./kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md) | | Increments all the specified counter fields (by one by default). Creates the document if one doesn't exist for the given id. |
|
||||
| [removeReferencesTo(type, id, options)](./kibana-plugin-core-server.savedobjectsrepository.removereferencesto.md) | | Updates all objects containing a reference to the given {<!-- -->type, id<!-- -->} tuple to remove the said reference. |
|
||||
| [update(type, id, attributes, options)](./kibana-plugin-core-server.savedobjectsrepository.update.md) | | Updates an object |
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ export interface UiSettingsParams<T = unknown>
|
|||
| [category](./kibana-plugin-core-server.uisettingsparams.category.md) | <code>string[]</code> | used to group the configured setting in the UI |
|
||||
| [deprecation](./kibana-plugin-core-server.uisettingsparams.deprecation.md) | <code>DeprecationSettings</code> | optional deprecation information. Used to generate a deprecation warning. |
|
||||
| [description](./kibana-plugin-core-server.uisettingsparams.description.md) | <code>string</code> | description provided to a user in UI |
|
||||
| [metric](./kibana-plugin-core-server.uisettingsparams.metric.md) | <code>{</code><br/><code> type: UiStatsMetricType;</code><br/><code> name: string;</code><br/><code> }</code> | Metric to track once this property changes |
|
||||
| [metric](./kibana-plugin-core-server.uisettingsparams.metric.md) | <code>{</code><br/><code> type: UiCounterMetricType;</code><br/><code> name: string;</code><br/><code> }</code> | Metric to track once this property changes |
|
||||
| [name](./kibana-plugin-core-server.uisettingsparams.name.md) | <code>string</code> | title in the UI |
|
||||
| [optionLabels](./kibana-plugin-core-server.uisettingsparams.optionlabels.md) | <code>Record<string, string></code> | text labels for 'select' type UI element |
|
||||
| [options](./kibana-plugin-core-server.uisettingsparams.options.md) | <code>string[]</code> | array of permitted values for this setting |
|
||||
|
|
|
@ -15,7 +15,7 @@ Metric to track once this property changes
|
|||
|
||||
```typescript
|
||||
metric?: {
|
||||
type: UiStatsMetricType;
|
||||
type: UiCounterMetricType;
|
||||
name: string;
|
||||
};
|
||||
```
|
||||
|
|
|
@ -18,6 +18,6 @@
|
|||
*/
|
||||
|
||||
export { ReportHTTP, Reporter, ReporterConfig } from './reporter';
|
||||
export { UiStatsMetricType, METRIC_TYPE } from './metrics';
|
||||
export { UiCounterMetricType, METRIC_TYPE } from './metrics';
|
||||
export { Report, ReportManager } from './report';
|
||||
export { Storage } from './storage';
|
||||
|
|
|
@ -17,16 +17,15 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { UiStatsMetric } from './ui_stats';
|
||||
import { UiCounterMetric } from './ui_counter';
|
||||
import { UserAgentMetric } from './user_agent';
|
||||
import { ApplicationUsageCurrent } from './application_usage';
|
||||
|
||||
export { UiStatsMetric, createUiStatsMetric, UiStatsMetricType } from './ui_stats';
|
||||
export { Stats } from './stats';
|
||||
export { UiCounterMetric, createUiCounterMetric, UiCounterMetricType } from './ui_counter';
|
||||
export { trackUsageAgent } from './user_agent';
|
||||
export { ApplicationUsage, ApplicationUsageCurrent } from './application_usage';
|
||||
|
||||
export type Metric = UiStatsMetric | UserAgentMetric | ApplicationUsageCurrent;
|
||||
export type Metric = UiCounterMetric | UserAgentMetric | ApplicationUsageCurrent;
|
||||
export enum METRIC_TYPE {
|
||||
COUNT = 'count',
|
||||
LOADED = 'loaded',
|
||||
|
|
|
@ -19,27 +19,27 @@
|
|||
|
||||
import { METRIC_TYPE } from './';
|
||||
|
||||
export type UiStatsMetricType = METRIC_TYPE.CLICK | METRIC_TYPE.LOADED | METRIC_TYPE.COUNT;
|
||||
export interface UiStatsMetricConfig {
|
||||
type: UiStatsMetricType;
|
||||
export type UiCounterMetricType = METRIC_TYPE.CLICK | METRIC_TYPE.LOADED | METRIC_TYPE.COUNT;
|
||||
export interface UiCounterMetricConfig {
|
||||
type: UiCounterMetricType;
|
||||
appName: string;
|
||||
eventName: string;
|
||||
count?: number;
|
||||
}
|
||||
|
||||
export interface UiStatsMetric {
|
||||
type: UiStatsMetricType;
|
||||
export interface UiCounterMetric {
|
||||
type: UiCounterMetricType;
|
||||
appName: string;
|
||||
eventName: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export function createUiStatsMetric({
|
||||
export function createUiCounterMetric({
|
||||
type,
|
||||
appName,
|
||||
eventName,
|
||||
count = 1,
|
||||
}: UiStatsMetricConfig): UiStatsMetric {
|
||||
}: UiCounterMetricConfig): UiCounterMetric {
|
||||
return {
|
||||
type,
|
||||
appName,
|
|
@ -19,19 +19,19 @@
|
|||
|
||||
import moment from 'moment-timezone';
|
||||
import { UnreachableCaseError, wrapArray } from './util';
|
||||
import { Metric, Stats, UiStatsMetricType, METRIC_TYPE } from './metrics';
|
||||
const REPORT_VERSION = 1;
|
||||
import { Metric, UiCounterMetricType, METRIC_TYPE } from './metrics';
|
||||
const REPORT_VERSION = 2;
|
||||
|
||||
export interface Report {
|
||||
reportVersion: typeof REPORT_VERSION;
|
||||
uiStatsMetrics?: Record<
|
||||
uiCounter?: Record<
|
||||
string,
|
||||
{
|
||||
key: string;
|
||||
appName: string;
|
||||
eventName: string;
|
||||
type: UiStatsMetricType;
|
||||
stats: Stats;
|
||||
type: UiCounterMetricType;
|
||||
total: number;
|
||||
}
|
||||
>;
|
||||
userAgent?: Record<
|
||||
|
@ -65,25 +65,15 @@ export class ReportManager {
|
|||
this.report = ReportManager.createReport();
|
||||
}
|
||||
public isReportEmpty(): boolean {
|
||||
const { uiStatsMetrics, userAgent, application_usage: appUsage } = this.report;
|
||||
const noUiStats = !uiStatsMetrics || Object.keys(uiStatsMetrics).length === 0;
|
||||
const noUserAgent = !userAgent || Object.keys(userAgent).length === 0;
|
||||
const { uiCounter, userAgent, application_usage: appUsage } = this.report;
|
||||
const noUiCounters = !uiCounter || Object.keys(uiCounter).length === 0;
|
||||
const noUserAgents = !userAgent || Object.keys(userAgent).length === 0;
|
||||
const noAppUsage = !appUsage || Object.keys(appUsage).length === 0;
|
||||
return noUiStats && noUserAgent && noAppUsage;
|
||||
return noUiCounters && noUserAgents && noAppUsage;
|
||||
}
|
||||
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,
|
||||
};
|
||||
private incrementTotal(count: number, currentTotal?: number): number {
|
||||
const currentTotalNumber = typeof currentTotal === 'number' ? currentTotal : 0;
|
||||
return count + currentTotalNumber;
|
||||
}
|
||||
assignReports(newMetrics: Metric | Metric[]) {
|
||||
wrapArray(newMetrics).forEach((newMetric) => this.assignReport(this.report, newMetric));
|
||||
|
@ -129,14 +119,14 @@ export class ReportManager {
|
|||
case METRIC_TYPE.LOADED:
|
||||
case METRIC_TYPE.COUNT: {
|
||||
const { appName, type, eventName, count } = metric;
|
||||
report.uiStatsMetrics = report.uiStatsMetrics || {};
|
||||
const existingStats = (report.uiStatsMetrics[key] || {}).stats;
|
||||
report.uiStatsMetrics[key] = {
|
||||
report.uiCounter = report.uiCounter || {};
|
||||
const currentTotal = report.uiCounter[key]?.total;
|
||||
report.uiCounter[key] = {
|
||||
key,
|
||||
appName,
|
||||
eventName,
|
||||
type,
|
||||
stats: this.incrementStats(count, existingStats),
|
||||
total: this.incrementTotal(count, currentTotal),
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
*/
|
||||
|
||||
import { wrapArray } from './util';
|
||||
import { Metric, createUiStatsMetric, trackUsageAgent, UiStatsMetricType } from './metrics';
|
||||
import { Metric, createUiCounterMetric, trackUsageAgent, UiCounterMetricType } from './metrics';
|
||||
|
||||
import { Storage, ReportStorageManager } from './storage';
|
||||
import { Report, ReportManager } from './report';
|
||||
|
@ -109,15 +109,15 @@ export class Reporter {
|
|||
}
|
||||
}
|
||||
|
||||
public reportUiStats = (
|
||||
public reportUiCounter = (
|
||||
appName: string,
|
||||
type: UiStatsMetricType,
|
||||
type: UiCounterMetricType,
|
||||
eventNames: string | string[],
|
||||
count?: number
|
||||
) => {
|
||||
const metrics = wrapArray(eventNames).map((eventName) => {
|
||||
this.log(`${type} Metric -> (${appName}:${eventName}):`);
|
||||
const report = createUiStatsMetric({ type, appName, eventName, count });
|
||||
const report = createUiCounterMetric({ type, appName, eventName, count });
|
||||
this.log(report);
|
||||
return report;
|
||||
});
|
||||
|
|
|
@ -38,7 +38,7 @@ import { TransportRequestParams } from '@elastic/elasticsearch/lib/Transport';
|
|||
import { TransportRequestPromise } from '@elastic/elasticsearch/lib/Transport';
|
||||
import { Type } from '@kbn/config-schema';
|
||||
import { TypeOf } from '@kbn/config-schema';
|
||||
import { UiStatsMetricType } from '@kbn/analytics';
|
||||
import { UiCounterMetricType } from '@kbn/analytics';
|
||||
import { UnregisterCallback } from 'history';
|
||||
import { UserProvidedValues as UserProvidedValues_2 } from 'src/core/server/types';
|
||||
|
||||
|
@ -1434,7 +1434,7 @@ export interface UiSettingsParams<T = unknown> {
|
|||
description?: string;
|
||||
// @deprecated
|
||||
metric?: {
|
||||
type: UiStatsMetricType;
|
||||
type: UiCounterMetricType;
|
||||
name: string;
|
||||
};
|
||||
name?: string;
|
||||
|
|
|
@ -302,6 +302,7 @@ export {
|
|||
SavedObjectsRepository,
|
||||
SavedObjectsDeleteByNamespaceOptions,
|
||||
SavedObjectsIncrementCounterOptions,
|
||||
SavedObjectsIncrementCounterField,
|
||||
SavedObjectsComplexFieldMapping,
|
||||
SavedObjectsCoreFieldMapping,
|
||||
SavedObjectsFieldMapping,
|
||||
|
|
|
@ -48,6 +48,7 @@ export {
|
|||
export {
|
||||
ISavedObjectsRepository,
|
||||
SavedObjectsIncrementCounterOptions,
|
||||
SavedObjectsIncrementCounterField,
|
||||
SavedObjectsDeleteByNamespaceOptions,
|
||||
} from './service/lib/repository';
|
||||
|
||||
|
|
|
@ -3412,11 +3412,13 @@ describe('SavedObjectsRepository', () => {
|
|||
await test({});
|
||||
});
|
||||
|
||||
it(`throws when counterFieldName is not a string`, async () => {
|
||||
it(`throws when counterField is not CounterField type`, async () => {
|
||||
const test = async (field) => {
|
||||
await expect(
|
||||
savedObjectsRepository.incrementCounter(type, id, field)
|
||||
).rejects.toThrowError(`"counterFieldNames" argument must be an array of strings`);
|
||||
).rejects.toThrowError(
|
||||
`"counterFields" argument must be of type Array<string | { incrementBy?: number; fieldName: string }>`
|
||||
);
|
||||
expect(client.update).not.toHaveBeenCalled();
|
||||
};
|
||||
|
||||
|
@ -3425,6 +3427,7 @@ describe('SavedObjectsRepository', () => {
|
|||
await test([false]);
|
||||
await test([{}]);
|
||||
await test([{}, false, 42, null, 'string']);
|
||||
await test([{ fieldName: 'string' }, false, null, 'string']);
|
||||
});
|
||||
|
||||
it(`throws when type is invalid`, async () => {
|
||||
|
@ -3513,6 +3516,25 @@ describe('SavedObjectsRepository', () => {
|
|||
originId,
|
||||
});
|
||||
});
|
||||
|
||||
it('increments counter by incrementBy config', async () => {
|
||||
await incrementCounterSuccess(type, id, [{ fieldName: counterFields[0], incrementBy: 3 }]);
|
||||
|
||||
expect(client.update).toBeCalledTimes(1);
|
||||
expect(client.update).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
body: expect.objectContaining({
|
||||
script: expect.objectContaining({
|
||||
params: expect.objectContaining({
|
||||
counterFieldNames: [counterFields[0]],
|
||||
counts: [3],
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { omit } from 'lodash';
|
||||
import { omit, isObject } from 'lodash';
|
||||
import uuid from 'uuid';
|
||||
import {
|
||||
ElasticsearchClient,
|
||||
|
@ -133,6 +133,16 @@ const DEFAULT_REFRESH_SETTING = 'wait_for';
|
|||
*/
|
||||
export type ISavedObjectsRepository = Pick<SavedObjectsRepository, keyof SavedObjectsRepository>;
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface SavedObjectsIncrementCounterField {
|
||||
/** The field name to increment the counter by.*/
|
||||
fieldName: string;
|
||||
/** The number to increment the field by (defaults to 1).*/
|
||||
incrementBy?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
|
@ -1524,7 +1534,7 @@ export class SavedObjectsRepository {
|
|||
}
|
||||
|
||||
/**
|
||||
* Increments all the specified counter fields by one. Creates the document
|
||||
* Increments all the specified counter fields (by one by default). Creates the document
|
||||
* if one doesn't exist for the given id.
|
||||
*
|
||||
* @remarks
|
||||
|
@ -1558,30 +1568,47 @@ export class SavedObjectsRepository {
|
|||
*
|
||||
* @param type - The type of saved object whose fields should be incremented
|
||||
* @param id - The id of the document whose fields should be incremented
|
||||
* @param counterFieldNames - An array of field names to increment
|
||||
* @param counterFields - An array of field names to increment or an array of {@link SavedObjectsIncrementCounterField}
|
||||
* @param options - {@link SavedObjectsIncrementCounterOptions}
|
||||
* @returns The saved object after the specified fields were incremented
|
||||
*/
|
||||
async incrementCounter<T = unknown>(
|
||||
type: string,
|
||||
id: string,
|
||||
counterFieldNames: string[],
|
||||
counterFields: Array<string | SavedObjectsIncrementCounterField>,
|
||||
options: SavedObjectsIncrementCounterOptions = {}
|
||||
): Promise<SavedObject<T>> {
|
||||
if (typeof type !== 'string') {
|
||||
throw new Error('"type" argument must be a string');
|
||||
}
|
||||
const isArrayOfStrings =
|
||||
Array.isArray(counterFieldNames) &&
|
||||
!counterFieldNames.some((field) => typeof field !== 'string');
|
||||
if (!isArrayOfStrings) {
|
||||
throw new Error('"counterFieldNames" argument must be an array of strings');
|
||||
|
||||
const isArrayOfCounterFields =
|
||||
Array.isArray(counterFields) &&
|
||||
counterFields.every(
|
||||
(field) =>
|
||||
typeof field === 'string' || (isObject(field) && typeof field.fieldName === 'string')
|
||||
);
|
||||
|
||||
if (!isArrayOfCounterFields) {
|
||||
throw new Error(
|
||||
'"counterFields" argument must be of type Array<string | { incrementBy?: number; fieldName: string }>'
|
||||
);
|
||||
}
|
||||
if (!this._allowedTypes.includes(type)) {
|
||||
throw SavedObjectsErrorHelpers.createUnsupportedTypeError(type);
|
||||
}
|
||||
|
||||
const { migrationVersion, refresh = DEFAULT_REFRESH_SETTING, initialize = false } = options;
|
||||
|
||||
const normalizedCounterFields = counterFields.map((counterField) => {
|
||||
const fieldName = typeof counterField === 'string' ? counterField : counterField.fieldName;
|
||||
const incrementBy = typeof counterField === 'string' ? 1 : counterField.incrementBy || 1;
|
||||
|
||||
return {
|
||||
fieldName,
|
||||
incrementBy: initialize ? 0 : incrementBy,
|
||||
};
|
||||
});
|
||||
const namespace = normalizeNamespace(options.namespace);
|
||||
|
||||
const time = this._getCurrentTime();
|
||||
|
@ -1594,13 +1621,15 @@ export class SavedObjectsRepository {
|
|||
savedObjectNamespaces = await this.preflightGetNamespaces(type, id, namespace);
|
||||
}
|
||||
|
||||
// attributes: { [counterFieldName]: incrementBy },
|
||||
const migrated = this._migrator.migrateDocument({
|
||||
id,
|
||||
type,
|
||||
...(savedObjectNamespace && { namespace: savedObjectNamespace }),
|
||||
...(savedObjectNamespaces && { namespaces: savedObjectNamespaces }),
|
||||
attributes: counterFieldNames.reduce((acc, counterFieldName) => {
|
||||
acc[counterFieldName] = initialize ? 0 : 1;
|
||||
attributes: normalizedCounterFields.reduce((acc, counterField) => {
|
||||
const { fieldName, incrementBy } = counterField;
|
||||
acc[fieldName] = incrementBy;
|
||||
return acc;
|
||||
}, {} as Record<string, number>),
|
||||
migrationVersion,
|
||||
|
@ -1617,22 +1646,29 @@ export class SavedObjectsRepository {
|
|||
body: {
|
||||
script: {
|
||||
source: `
|
||||
for (counterFieldName in params.counterFieldNames) {
|
||||
for (int i = 0; i < params.counterFieldNames.length; i++) {
|
||||
def counterFieldName = params.counterFieldNames[i];
|
||||
def count = params.counts[i];
|
||||
|
||||
if (ctx._source[params.type][counterFieldName] == null) {
|
||||
ctx._source[params.type][counterFieldName] = params.count;
|
||||
ctx._source[params.type][counterFieldName] = count;
|
||||
}
|
||||
else {
|
||||
ctx._source[params.type][counterFieldName] += params.count;
|
||||
ctx._source[params.type][counterFieldName] += count;
|
||||
}
|
||||
}
|
||||
ctx._source.updated_at = params.time;
|
||||
`,
|
||||
lang: 'painless',
|
||||
params: {
|
||||
count: initialize ? 0 : 1,
|
||||
counts: normalizedCounterFields.map(
|
||||
(normalizedCounterField) => normalizedCounterField.incrementBy
|
||||
),
|
||||
counterFieldNames: normalizedCounterFields.map(
|
||||
(normalizedCounterField) => normalizedCounterField.fieldName
|
||||
),
|
||||
time,
|
||||
type,
|
||||
counterFieldNames,
|
||||
},
|
||||
},
|
||||
upsert: raw._source,
|
||||
|
|
|
@ -160,7 +160,7 @@ import { TransportRequestParams } from '@elastic/elasticsearch/lib/Transport';
|
|||
import { TransportRequestPromise } from '@elastic/elasticsearch/lib/Transport';
|
||||
import { Type } from '@kbn/config-schema';
|
||||
import { TypeOf } from '@kbn/config-schema';
|
||||
import { UiStatsMetricType } from '@kbn/analytics';
|
||||
import { UiCounterMetricType } from '@kbn/analytics';
|
||||
import { UpdateDocumentByQueryParams } from 'elasticsearch';
|
||||
import { UpdateDocumentParams } from 'elasticsearch';
|
||||
import { URL } from 'url';
|
||||
|
@ -2405,6 +2405,12 @@ export interface SavedObjectsImportUnsupportedTypeError {
|
|||
type: 'unsupported_type';
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export interface SavedObjectsIncrementCounterField {
|
||||
fieldName: string;
|
||||
incrementBy?: number;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export interface SavedObjectsIncrementCounterOptions extends SavedObjectsBaseOptions {
|
||||
initialize?: boolean;
|
||||
|
@ -2486,7 +2492,7 @@ export class SavedObjectsRepository {
|
|||
// (undocumented)
|
||||
find<T = unknown>(options: SavedObjectsFindOptions): Promise<SavedObjectsFindResponse<T>>;
|
||||
get<T = unknown>(type: string, id: string, options?: SavedObjectsBaseOptions): Promise<SavedObject<T>>;
|
||||
incrementCounter<T = unknown>(type: string, id: string, counterFieldNames: string[], options?: SavedObjectsIncrementCounterOptions): Promise<SavedObject<T>>;
|
||||
incrementCounter<T = unknown>(type: string, id: string, counterFields: Array<string | SavedObjectsIncrementCounterField>, options?: SavedObjectsIncrementCounterOptions): Promise<SavedObject<T>>;
|
||||
removeReferencesTo(type: string, id: string, options?: SavedObjectsRemoveReferencesToOptions): Promise<SavedObjectsRemoveReferencesToResponse>;
|
||||
update<T = unknown>(type: string, id: string, attributes: Partial<T>, options?: SavedObjectsUpdateOptions): Promise<SavedObjectsUpdateResponse<T>>;
|
||||
}
|
||||
|
@ -2791,7 +2797,7 @@ export interface UiSettingsParams<T = unknown> {
|
|||
description?: string;
|
||||
// @deprecated
|
||||
metric?: {
|
||||
type: UiStatsMetricType;
|
||||
type: UiCounterMetricType;
|
||||
name: string;
|
||||
};
|
||||
name?: string;
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
import { Type } from '@kbn/config-schema';
|
||||
import { UiStatsMetricType } from '@kbn/analytics';
|
||||
import { UiCounterMetricType } from '@kbn/analytics';
|
||||
|
||||
/**
|
||||
* UI element type to represent the settings.
|
||||
|
@ -87,7 +87,7 @@ export interface UiSettingsParams<T = unknown> {
|
|||
* Temporary measure until https://github.com/elastic/kibana/issues/83084 is in place
|
||||
*/
|
||||
metric?: {
|
||||
type: UiStatsMetricType;
|
||||
type: UiCounterMetricType;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ import { Subscription } from 'rxjs';
|
|||
import { Comparators, EuiFlexGroup, EuiFlexItem, EuiSpacer, Query } from '@elastic/eui';
|
||||
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { UiStatsMetricType } from '@kbn/analytics';
|
||||
import { UiCounterMetricType } from '@kbn/analytics';
|
||||
import { CallOuts } from './components/call_outs';
|
||||
import { Search } from './components/search';
|
||||
import { Form } from './components/form';
|
||||
|
@ -40,7 +40,7 @@ interface AdvancedSettingsProps {
|
|||
dockLinks: DocLinksStart['links'];
|
||||
toasts: ToastsStart;
|
||||
componentRegistry: ComponentRegistry['start'];
|
||||
trackUiMetric?: (metricType: UiStatsMetricType, eventName: string | string[]) => void;
|
||||
trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void;
|
||||
}
|
||||
|
||||
interface AdvancedSettingsComponentProps extends AdvancedSettingsProps {
|
||||
|
|
|
@ -36,7 +36,7 @@ import {
|
|||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { UiStatsMetricType } from '@kbn/analytics';
|
||||
import { UiCounterMetricType } from '@kbn/analytics';
|
||||
import { toMountPoint } from '../../../../../kibana_react/public';
|
||||
import { DocLinksStart, ToastsStart } from '../../../../../../core/public';
|
||||
|
||||
|
@ -57,7 +57,7 @@ interface FormProps {
|
|||
enableSaving: boolean;
|
||||
dockLinks: DocLinksStart['links'];
|
||||
toasts: ToastsStart;
|
||||
trackUiMetric?: (metricType: UiStatsMetricType, eventName: string | string[]) => void;
|
||||
trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void;
|
||||
}
|
||||
|
||||
interface FormState {
|
||||
|
|
|
@ -57,7 +57,7 @@ export async function mountManagementSection(
|
|||
const [{ uiSettings, notifications, docLinks, application, chrome }] = await getStartServices();
|
||||
|
||||
const canSave = application.capabilities.advancedSettings.save as boolean;
|
||||
const trackUiMetric = usageCollection?.reportUiStats.bind(usageCollection, 'advanced_settings');
|
||||
const trackUiMetric = usageCollection?.reportUiCounter.bind(usageCollection, 'advanced_settings');
|
||||
|
||||
if (!canSave) {
|
||||
chrome.setBadge(readOnlyBadge);
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { UiStatsMetricType } from '@kbn/analytics';
|
||||
import { UiCounterMetricType } from '@kbn/analytics';
|
||||
import { UiSettingsType, StringValidation, ImageValidation } from '../../../../core/public';
|
||||
|
||||
export interface FieldSetting {
|
||||
|
@ -41,7 +41,7 @@ export interface FieldSetting {
|
|||
docLinksKey: string;
|
||||
};
|
||||
metric?: {
|
||||
type: UiStatsMetricType;
|
||||
type: UiCounterMetricType;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -17,15 +17,15 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { METRIC_TYPE, UiStatsMetricType } from '@kbn/analytics';
|
||||
import { METRIC_TYPE, UiCounterMetricType } from '@kbn/analytics';
|
||||
import { MetricsTracker } from '../types';
|
||||
import { UsageCollectionSetup } from '../../../usage_collection/public';
|
||||
|
||||
const APP_TRACKER_NAME = 'console';
|
||||
|
||||
export const createUsageTracker = (usageCollection?: UsageCollectionSetup): MetricsTracker => {
|
||||
const track = (type: UiStatsMetricType, name: string) =>
|
||||
usageCollection?.reportUiStats(APP_TRACKER_NAME, type, name);
|
||||
const track = (type: UiCounterMetricType, name: string) =>
|
||||
usageCollection?.reportUiCounter(APP_TRACKER_NAME, type, name);
|
||||
|
||||
return {
|
||||
count: (eventName: string) => {
|
||||
|
|
|
@ -65,7 +65,11 @@ export function migrateAppState(
|
|||
|
||||
if (usageCollection) {
|
||||
// This will help us figure out when to remove support for older style URLs.
|
||||
usageCollection.reportUiStats('DashboardPanelVersionInUrl', METRIC_TYPE.LOADED, `${version}`);
|
||||
usageCollection.reportUiCounter(
|
||||
'DashboardPanelVersionInUrl',
|
||||
METRIC_TYPE.LOADED,
|
||||
`${version}`
|
||||
);
|
||||
}
|
||||
|
||||
return semverSatisfies(version, '<7.3');
|
||||
|
|
|
@ -207,7 +207,10 @@ export class DataPublicPlugin
|
|||
core,
|
||||
data: dataServices,
|
||||
storage: this.storage,
|
||||
trackUiMetric: this.usageCollection?.reportUiStats.bind(this.usageCollection, 'data_plugin'),
|
||||
trackUiMetric: this.usageCollection?.reportUiCounter.bind(
|
||||
this.usageCollection,
|
||||
'data_plugin'
|
||||
),
|
||||
});
|
||||
|
||||
return {
|
||||
|
|
|
@ -94,7 +94,7 @@ import { TransportRequestPromise } from '@elastic/elasticsearch/lib/Transport';
|
|||
import { TypeOf } from '@kbn/config-schema';
|
||||
import { UiActionsSetup } from 'src/plugins/ui_actions/public';
|
||||
import { UiActionsStart } from 'src/plugins/ui_actions/public';
|
||||
import { UiStatsMetricType } from '@kbn/analytics';
|
||||
import { UiCounterMetricType } from '@kbn/analytics';
|
||||
import { Unit } from '@elastic/datemath';
|
||||
import { UnregisterCallback } from 'history';
|
||||
import { UserProvidedValues } from 'src/core/server/types';
|
||||
|
|
|
@ -47,19 +47,19 @@ describe('Search Usage Collector', () => {
|
|||
|
||||
test('tracks query timeouts', async () => {
|
||||
await usageCollector.trackQueryTimedOut();
|
||||
expect(mockUsageCollectionSetup.reportUiStats).toHaveBeenCalled();
|
||||
expect(mockUsageCollectionSetup.reportUiStats.mock.calls[0][0]).toBe('foo/bar');
|
||||
expect(mockUsageCollectionSetup.reportUiStats.mock.calls[0][1]).toBe(METRIC_TYPE.LOADED);
|
||||
expect(mockUsageCollectionSetup.reportUiStats.mock.calls[0][2]).toBe(
|
||||
expect(mockUsageCollectionSetup.reportUiCounter).toHaveBeenCalled();
|
||||
expect(mockUsageCollectionSetup.reportUiCounter.mock.calls[0][0]).toBe('foo/bar');
|
||||
expect(mockUsageCollectionSetup.reportUiCounter.mock.calls[0][1]).toBe(METRIC_TYPE.LOADED);
|
||||
expect(mockUsageCollectionSetup.reportUiCounter.mock.calls[0][2]).toBe(
|
||||
SEARCH_EVENT_TYPE.QUERY_TIMED_OUT
|
||||
);
|
||||
});
|
||||
|
||||
test('tracks query cancellation', async () => {
|
||||
await usageCollector.trackQueriesCancelled();
|
||||
expect(mockUsageCollectionSetup.reportUiStats).toHaveBeenCalled();
|
||||
expect(mockUsageCollectionSetup.reportUiStats.mock.calls[0][1]).toBe(METRIC_TYPE.LOADED);
|
||||
expect(mockUsageCollectionSetup.reportUiStats.mock.calls[0][2]).toBe(
|
||||
expect(mockUsageCollectionSetup.reportUiCounter).toHaveBeenCalled();
|
||||
expect(mockUsageCollectionSetup.reportUiCounter.mock.calls[0][1]).toBe(METRIC_TYPE.LOADED);
|
||||
expect(mockUsageCollectionSetup.reportUiCounter.mock.calls[0][2]).toBe(
|
||||
SEARCH_EVENT_TYPE.QUERIES_CANCELLED
|
||||
);
|
||||
});
|
||||
|
|
|
@ -34,7 +34,7 @@ export const createUsageCollector = (
|
|||
return {
|
||||
trackQueryTimedOut: async () => {
|
||||
const currentApp = await getCurrentApp();
|
||||
return usageCollection?.reportUiStats(
|
||||
return usageCollection?.reportUiCounter(
|
||||
currentApp!,
|
||||
METRIC_TYPE.LOADED,
|
||||
SEARCH_EVENT_TYPE.QUERY_TIMED_OUT
|
||||
|
@ -42,7 +42,7 @@ export const createUsageCollector = (
|
|||
},
|
||||
trackQueriesCancelled: async () => {
|
||||
const currentApp = await getCurrentApp();
|
||||
return usageCollection?.reportUiStats(
|
||||
return usageCollection?.reportUiCounter(
|
||||
currentApp!,
|
||||
METRIC_TYPE.LOADED,
|
||||
SEARCH_EVENT_TYPE.QUERIES_CANCELLED
|
||||
|
|
|
@ -22,7 +22,7 @@ import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
|
|||
import classNames from 'classnames';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { METRIC_TYPE, UiStatsMetricType } from '@kbn/analytics';
|
||||
import { METRIC_TYPE, UiCounterMetricType } from '@kbn/analytics';
|
||||
import { FilterEditor } from './filter_editor';
|
||||
import { FILTER_EDITOR_WIDTH, FilterItem } from './filter_item';
|
||||
import { FilterOptions } from './filter_options';
|
||||
|
@ -48,7 +48,7 @@ interface Props {
|
|||
intl: InjectedIntl;
|
||||
appName: string;
|
||||
// Track UI Metrics
|
||||
trackUiMetric?: (metricType: UiStatsMetricType, eventName: string | string[]) => void;
|
||||
trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void;
|
||||
}
|
||||
|
||||
function FilterBarUI(props: Props) {
|
||||
|
|
|
@ -21,7 +21,7 @@ import _ from 'lodash';
|
|||
import React, { useEffect, useRef } from 'react';
|
||||
import { CoreStart } from 'src/core/public';
|
||||
import { IStorageWrapper } from 'src/plugins/kibana_utils/public';
|
||||
import { UiStatsMetricType } from '@kbn/analytics';
|
||||
import { UiCounterMetricType } from '@kbn/analytics';
|
||||
import { KibanaContextProvider } from '../../../../kibana_react/public';
|
||||
import { QueryStart, SavedQuery } from '../../query';
|
||||
import { SearchBar, SearchBarOwnProps } from './';
|
||||
|
@ -36,7 +36,7 @@ interface StatefulSearchBarDeps {
|
|||
core: CoreStart;
|
||||
data: Omit<DataPublicPluginStart, 'ui'>;
|
||||
storage: IStorageWrapper;
|
||||
trackUiMetric?: (metricType: UiStatsMetricType, eventName: string | string[]) => void;
|
||||
trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void;
|
||||
}
|
||||
|
||||
export type StatefulSearchBarProps = SearchBarOwnProps & {
|
||||
|
|
|
@ -24,7 +24,7 @@ import React, { Component } from 'react';
|
|||
import ResizeObserver from 'resize-observer-polyfill';
|
||||
import { get, isEqual } from 'lodash';
|
||||
|
||||
import { METRIC_TYPE, UiStatsMetricType } from '@kbn/analytics';
|
||||
import { METRIC_TYPE, UiCounterMetricType } from '@kbn/analytics';
|
||||
import { withKibana, KibanaReactContextValue } from '../../../../kibana_react/public';
|
||||
|
||||
import QueryBarTopRow from '../query_string_input/query_bar_top_row';
|
||||
|
@ -80,7 +80,7 @@ export interface SearchBarOwnProps {
|
|||
onRefresh?: (payload: { dateRange: TimeRange }) => void;
|
||||
indicateNoData?: boolean;
|
||||
// Track UI Metrics
|
||||
trackUiMetric?: (metricType: UiStatsMetricType, eventName: string | string[]) => void;
|
||||
trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void;
|
||||
}
|
||||
|
||||
export type SearchBarProps = SearchBarOwnProps & SearchBarInjectedDeps;
|
||||
|
|
|
@ -65,7 +65,7 @@ import { ToastInputFields } from 'src/core/public/notifications';
|
|||
import { TransportRequestPromise } from '@elastic/elasticsearch/lib/Transport';
|
||||
import { Type } from '@kbn/config-schema';
|
||||
import { TypeOf } from '@kbn/config-schema';
|
||||
import { UiStatsMetricType } from '@kbn/analytics';
|
||||
import { UiCounterMetricType } from '@kbn/analytics';
|
||||
import { Unit } from '@elastic/datemath';
|
||||
|
||||
// Warning: (ae-forgotten-export) The symbol "AggConfigSerialized" needs to be exported by the entry point index.d.ts
|
||||
|
|
|
@ -21,7 +21,7 @@ import './discover_field.scss';
|
|||
import React, { useState } from 'react';
|
||||
import { EuiPopover, EuiPopoverTitle, EuiButtonIcon, EuiToolTip } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { UiStatsMetricType } from '@kbn/analytics';
|
||||
import { UiCounterMetricType } from '@kbn/analytics';
|
||||
import classNames from 'classnames';
|
||||
import { DiscoverFieldDetails } from './discover_field_details';
|
||||
import { FieldIcon, FieldButton } from '../../../../../kibana_react/public';
|
||||
|
@ -68,7 +68,7 @@ export interface DiscoverFieldProps {
|
|||
* @param metricType
|
||||
* @param eventName
|
||||
*/
|
||||
trackUiMetric?: (metricType: UiStatsMetricType, eventName: string | string[]) => void;
|
||||
trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void;
|
||||
}
|
||||
|
||||
export function DiscoverField({
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { EuiLink, EuiIconTip, EuiText, EuiPopoverFooter, EuiButton, EuiSpacer } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { METRIC_TYPE, UiStatsMetricType } from '@kbn/analytics';
|
||||
import { METRIC_TYPE, UiCounterMetricType } from '@kbn/analytics';
|
||||
import { DiscoverFieldBucket } from './discover_field_bucket';
|
||||
import { getWarnings } from './lib/get_warnings';
|
||||
import {
|
||||
|
@ -36,7 +36,7 @@ interface DiscoverFieldDetailsProps {
|
|||
indexPattern: IndexPattern;
|
||||
details: FieldDetails;
|
||||
onAddFilter: (field: IndexPatternField | string, value: string, type: '+' | '-') => void;
|
||||
trackUiMetric?: (metricType: UiStatsMetricType, eventName: string | string[]) => void;
|
||||
trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void;
|
||||
}
|
||||
|
||||
export function DiscoverFieldDetails({
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
import './discover_sidebar.scss';
|
||||
import React, { useCallback, useEffect, useState, useMemo } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { UiStatsMetricType } from '@kbn/analytics';
|
||||
import { UiCounterMetricType } from '@kbn/analytics';
|
||||
import {
|
||||
EuiAccordion,
|
||||
EuiFlexItem,
|
||||
|
@ -105,7 +105,7 @@ export interface DiscoverSidebarProps {
|
|||
* @param metricType
|
||||
* @param eventName
|
||||
*/
|
||||
trackUiMetric?: (metricType: UiStatsMetricType, eventName: string | string[]) => void;
|
||||
trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void;
|
||||
/**
|
||||
* Shows index pattern and a button that displays the sidebar in a flyout
|
||||
*/
|
||||
|
|
|
@ -20,7 +20,7 @@ import React, { useState } from 'react';
|
|||
import { sortBy } from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { UiStatsMetricType } from '@kbn/analytics';
|
||||
import { UiCounterMetricType } from '@kbn/analytics';
|
||||
import {
|
||||
EuiTitle,
|
||||
EuiHideFor,
|
||||
|
@ -98,7 +98,7 @@ export interface DiscoverSidebarResponsiveProps {
|
|||
* @param metricType
|
||||
* @param eventName
|
||||
*/
|
||||
trackUiMetric?: (metricType: UiStatsMetricType, eventName: string | string[]) => void;
|
||||
trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void;
|
||||
/**
|
||||
* Shows index pattern and a button that displays the sidebar in a flyout
|
||||
*/
|
||||
|
|
|
@ -37,7 +37,7 @@ import { Start as InspectorPublicPluginStart } from 'src/plugins/inspector/publi
|
|||
import { SharePluginStart } from 'src/plugins/share/public';
|
||||
import { ChartsPluginStart } from 'src/plugins/charts/public';
|
||||
|
||||
import { UiStatsMetricType } from '@kbn/analytics';
|
||||
import { UiCounterMetricType } from '@kbn/analytics';
|
||||
import { DiscoverStartPlugins } from './plugin';
|
||||
import { createSavedSearchesLoader, SavedSearch } from './saved_searches';
|
||||
import { getHistory } from './kibana_services';
|
||||
|
@ -68,7 +68,7 @@ export interface DiscoverServices {
|
|||
getSavedSearchUrlById: (id: string) => Promise<string>;
|
||||
getEmbeddableInjector: any;
|
||||
uiSettings: IUiSettingsClient;
|
||||
trackUiMetric?: (metricType: UiStatsMetricType, eventName: string | string[]) => void;
|
||||
trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void;
|
||||
}
|
||||
|
||||
export async function buildServices(
|
||||
|
@ -109,6 +109,6 @@ export async function buildServices(
|
|||
timefilter: plugins.data.query.timefilter.timefilter,
|
||||
toastNotifications: core.notifications.toasts,
|
||||
uiSettings: core.uiSettings,
|
||||
trackUiMetric: usageCollection?.reportUiStats.bind(usageCollection, 'discover'),
|
||||
trackUiMetric: usageCollection?.reportUiCounter.bind(usageCollection, 'discover'),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -88,7 +88,7 @@ import { TransportRequestParams } from '@elastic/elasticsearch/lib/Transport';
|
|||
import { TransportRequestPromise } from '@elastic/elasticsearch/lib/Transport';
|
||||
import { TypeOf } from '@kbn/config-schema';
|
||||
import { UiComponent } from 'src/plugins/kibana_utils/public';
|
||||
import { UiStatsMetricType } from '@kbn/analytics';
|
||||
import { UiCounterMetricType } from '@kbn/analytics';
|
||||
import { UnregisterCallback } from 'history';
|
||||
import { UserProvidedValues } from 'src/core/server/types';
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@ import {
|
|||
IUiSettingsClient,
|
||||
ApplicationStart,
|
||||
} from 'kibana/public';
|
||||
import { UiStatsMetricType } from '@kbn/analytics';
|
||||
import { UiCounterMetricType } from '@kbn/analytics';
|
||||
import { TelemetryPluginStart } from '../../../telemetry/public';
|
||||
import { UrlForwardingStart } from '../../../url_forwarding/public';
|
||||
import { TutorialService } from '../services/tutorials';
|
||||
|
@ -48,7 +48,7 @@ export interface HomeKibanaServices {
|
|||
savedObjectsClient: SavedObjectsClientContract;
|
||||
toastNotifications: NotificationsSetup['toasts'];
|
||||
banners: OverlayStart['banners'];
|
||||
trackUiMetric: (type: UiStatsMetricType, eventNames: string | string[], count?: number) => void;
|
||||
trackUiMetric: (type: UiCounterMetricType, eventNames: string | string[], count?: number) => void;
|
||||
getBasePath: () => string;
|
||||
docLinks: DocLinksStart;
|
||||
addBasePath: (url: string) => string;
|
||||
|
|
|
@ -80,7 +80,7 @@ export class HomePublicPlugin
|
|||
navLinkStatus: AppNavLinkStatus.hidden,
|
||||
mount: async (params: AppMountParameters) => {
|
||||
const trackUiMetric = usageCollection
|
||||
? usageCollection.reportUiStats.bind(usageCollection, 'Kibana_home')
|
||||
? usageCollection.reportUiCounter.bind(usageCollection, 'Kibana_home')
|
||||
: () => {};
|
||||
const [
|
||||
coreStart,
|
||||
|
|
|
@ -1,17 +1,19 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`kibana_usage_collection Runs the setup method without issues 1`] = `false`;
|
||||
exports[`kibana_usage_collection Runs the setup method without issues 1`] = `true`;
|
||||
|
||||
exports[`kibana_usage_collection Runs the setup method without issues 2`] = `true`;
|
||||
exports[`kibana_usage_collection Runs the setup method without issues 2`] = `false`;
|
||||
|
||||
exports[`kibana_usage_collection Runs the setup method without issues 3`] = `false`;
|
||||
exports[`kibana_usage_collection Runs the setup method without issues 3`] = `true`;
|
||||
|
||||
exports[`kibana_usage_collection Runs the setup method without issues 4`] = `false`;
|
||||
|
||||
exports[`kibana_usage_collection Runs the setup method without issues 5`] = `false`;
|
||||
|
||||
exports[`kibana_usage_collection Runs the setup method without issues 6`] = `true`;
|
||||
exports[`kibana_usage_collection Runs the setup method without issues 6`] = `false`;
|
||||
|
||||
exports[`kibana_usage_collection Runs the setup method without issues 7`] = `false`;
|
||||
exports[`kibana_usage_collection Runs the setup method without issues 7`] = `true`;
|
||||
|
||||
exports[`kibana_usage_collection Runs the setup method without issues 8`] = `true`;
|
||||
exports[`kibana_usage_collection Runs the setup method without issues 8`] = `false`;
|
||||
|
||||
exports[`kibana_usage_collection Runs the setup method without issues 9`] = `true`;
|
||||
|
|
|
@ -30,7 +30,7 @@ This collection occurs by default for every application registered via the menti
|
|||
|
||||
In order to keep the count of the events, this collector uses 3 Saved Objects:
|
||||
|
||||
1. `application_usage_transactional`: It stores each individually reported event. Grouped by `timestamp` and `appId`. The reason for having these documents instead of editing `application_usage_daily` documents on very report is to provide faster response to the requests to `/api/ui_metric/report` (creating new documents instead of finding and editing existing ones) and to avoid conflicts when multiple users reach to the API concurrently.
|
||||
1. `application_usage_transactional`: It stores each individually reported event. Grouped by `timestamp` and `appId`. The reason for having these documents instead of editing `application_usage_daily` documents on very report is to provide faster response to the requests to `/api/ui_counters/_report` (creating new documents instead of finding and editing existing ones) and to avoid conflicts when multiple users reach to the API concurrently.
|
||||
2. `application_usage_daily`: Periodically, documents from `application_usage_transactional` are aggregated to daily summaries and deleted. Also grouped by `timestamp` and `appId`.
|
||||
3. `application_usage_totals`: It stores the sum of all the events older than 90 days old, grouped by `appId`.
|
||||
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Roll total indices every 24h
|
||||
*/
|
||||
export const ROLL_TOTAL_INDICES_INTERVAL = 24 * 60 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* Roll daily indices every 30 minutes.
|
||||
* This means that, assuming a user can visit all the 44 apps we can possibly report
|
||||
* in the 3 minutes interval the browser reports to the server, up to 22 users can have the same
|
||||
* behaviour and we wouldn't need to paginate in the transactional documents (less than 10k docs).
|
||||
*
|
||||
* Based on a more normal expected use case, the users could visit up to 5 apps in those 3 minutes,
|
||||
* allowing up to 200 users before reaching the limit.
|
||||
*/
|
||||
export const ROLL_DAILY_INDICES_INTERVAL = 30 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* Start rolling indices after 5 minutes up
|
||||
*/
|
||||
export const ROLL_INDICES_START = 5 * 60 * 1000;
|
|
@ -24,11 +24,8 @@ import {
|
|||
} from '../../../../usage_collection/server/usage_collection.mock';
|
||||
|
||||
import { createCollectorFetchContextMock } from 'src/plugins/usage_collection/server/mocks';
|
||||
import {
|
||||
ROLL_INDICES_START,
|
||||
ROLL_TOTAL_INDICES_INTERVAL,
|
||||
registerApplicationUsageCollector,
|
||||
} from './telemetry_application_usage_collector';
|
||||
import { ROLL_TOTAL_INDICES_INTERVAL, ROLL_INDICES_START } from './constants';
|
||||
import { registerApplicationUsageCollector } from './telemetry_application_usage_collector';
|
||||
import {
|
||||
SAVED_OBJECTS_DAILY_TYPE,
|
||||
SAVED_OBJECTS_TOTAL_TYPE,
|
||||
|
|
|
@ -32,27 +32,11 @@ import {
|
|||
} from './saved_objects_types';
|
||||
import { applicationUsageSchema } from './schema';
|
||||
import { rollDailyData, rollTotals } from './rollups';
|
||||
|
||||
/**
|
||||
* Roll total indices every 24h
|
||||
*/
|
||||
export const ROLL_TOTAL_INDICES_INTERVAL = 24 * 60 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* Roll daily indices every 30 minutes.
|
||||
* This means that, assuming a user can visit all the 44 apps we can possibly report
|
||||
* in the 3 minutes interval the browser reports to the server, up to 22 users can have the same
|
||||
* behaviour and we wouldn't need to paginate in the transactional documents (less than 10k docs).
|
||||
*
|
||||
* Based on a more normal expected use case, the users could visit up to 5 apps in those 3 minutes,
|
||||
* allowing up to 200 users before reaching the limit.
|
||||
*/
|
||||
export const ROLL_DAILY_INDICES_INTERVAL = 30 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* Start rolling indices after 5 minutes up
|
||||
*/
|
||||
export const ROLL_INDICES_START = 5 * 60 * 1000;
|
||||
import {
|
||||
ROLL_TOTAL_INDICES_INTERVAL,
|
||||
ROLL_DAILY_INDICES_INTERVAL,
|
||||
ROLL_INDICES_START,
|
||||
} from './constants';
|
||||
|
||||
export interface ApplicationUsageTelemetryReport {
|
||||
[appId: string]: {
|
||||
|
|
|
@ -25,3 +25,8 @@ export { registerOpsStatsCollector } from './ops_stats';
|
|||
export { registerCspCollector } from './csp';
|
||||
export { registerCoreUsageCollector } from './core';
|
||||
export { registerLocalizationUsageCollector } from './localization';
|
||||
export {
|
||||
registerUiCountersUsageCollector,
|
||||
registerUiCounterSavedObjectType,
|
||||
registerUiCountersRollups,
|
||||
} from './ui_counters';
|
||||
|
|
|
@ -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 { registerUiCountersUsageCollector } from './register_ui_counters_collector';
|
||||
export { registerUiCounterSavedObjectType } from './ui_counter_saved_object_type';
|
||||
export { registerUiCountersRollups } from './rollups';
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* 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 { transformRawCounter } from './register_ui_counters_collector';
|
||||
import { UICounterSavedObject } from './ui_counter_saved_object_type';
|
||||
|
||||
describe('transformRawCounter', () => {
|
||||
const mockRawUiCounters = [
|
||||
{
|
||||
type: 'ui-counter',
|
||||
id: 'Kibana_home:24112020:click:ingest_data_card_home_tutorial_directory',
|
||||
attributes: {
|
||||
count: 3,
|
||||
},
|
||||
references: [],
|
||||
updated_at: '2020-11-24T11:27:57.067Z',
|
||||
version: 'WzI5LDFd',
|
||||
},
|
||||
{
|
||||
type: 'ui-counter',
|
||||
id: 'Kibana_home:24112020:click:home_tutorial_directory',
|
||||
attributes: {
|
||||
count: 1,
|
||||
},
|
||||
references: [],
|
||||
updated_at: '2020-11-24T11:27:57.067Z',
|
||||
version: 'WzI5NDRd',
|
||||
},
|
||||
{
|
||||
type: 'ui-counter',
|
||||
id: 'Kibana_home:24112020:loaded:home_tutorial_directory',
|
||||
attributes: {
|
||||
count: 3,
|
||||
},
|
||||
references: [],
|
||||
updated_at: '2020-10-23T11:27:57.067Z',
|
||||
version: 'WzI5NDRd',
|
||||
},
|
||||
] as UICounterSavedObject[];
|
||||
|
||||
it('transforms saved object raw entries', () => {
|
||||
const result = mockRawUiCounters.map(transformRawCounter);
|
||||
expect(result).toEqual([
|
||||
{
|
||||
appName: 'Kibana_home',
|
||||
eventName: 'ingest_data_card_home_tutorial_directory',
|
||||
lastUpdatedAt: '2020-11-24T11:27:57.067Z',
|
||||
fromTimestamp: '2020-11-24T00:00:00Z',
|
||||
counterType: 'click',
|
||||
total: 3,
|
||||
},
|
||||
{
|
||||
appName: 'Kibana_home',
|
||||
eventName: 'home_tutorial_directory',
|
||||
lastUpdatedAt: '2020-11-24T11:27:57.067Z',
|
||||
fromTimestamp: '2020-11-24T00:00:00Z',
|
||||
counterType: 'click',
|
||||
total: 1,
|
||||
},
|
||||
{
|
||||
appName: 'Kibana_home',
|
||||
eventName: 'home_tutorial_directory',
|
||||
lastUpdatedAt: '2020-10-23T11:27:57.067Z',
|
||||
fromTimestamp: '2020-10-23T00:00:00Z',
|
||||
counterType: 'loaded',
|
||||
total: 3,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* 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 moment from 'moment';
|
||||
import { CollectorFetchContext, UsageCollectionSetup } from 'src/plugins/usage_collection/server';
|
||||
import {
|
||||
UICounterSavedObject,
|
||||
UICounterSavedObjectAttributes,
|
||||
UI_COUNTER_SAVED_OBJECT_TYPE,
|
||||
} from './ui_counter_saved_object_type';
|
||||
|
||||
interface UiCounterEvent {
|
||||
appName: string;
|
||||
eventName: string;
|
||||
lastUpdatedAt?: string;
|
||||
fromTimestamp?: string;
|
||||
counterType: string;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface UiCountersUsage {
|
||||
dailyEvents: UiCounterEvent[];
|
||||
}
|
||||
|
||||
export function transformRawCounter(rawUiCounter: UICounterSavedObject) {
|
||||
const { id, attributes, updated_at: lastUpdatedAt } = rawUiCounter;
|
||||
const [appName, , counterType, ...restId] = id.split(':');
|
||||
const eventName = restId.join(':');
|
||||
const counterTotal: unknown = attributes.count;
|
||||
const total = typeof counterTotal === 'number' ? counterTotal : 0;
|
||||
const fromTimestamp = moment(lastUpdatedAt).utc().startOf('day').format();
|
||||
|
||||
return {
|
||||
appName,
|
||||
eventName,
|
||||
lastUpdatedAt,
|
||||
fromTimestamp,
|
||||
counterType,
|
||||
total,
|
||||
};
|
||||
}
|
||||
|
||||
export function registerUiCountersUsageCollector(usageCollection: UsageCollectionSetup) {
|
||||
const collector = usageCollection.makeUsageCollector<UiCountersUsage>({
|
||||
type: 'ui_counters',
|
||||
schema: {
|
||||
dailyEvents: {
|
||||
type: 'array',
|
||||
items: {
|
||||
appName: { type: 'keyword' },
|
||||
eventName: { type: 'keyword' },
|
||||
lastUpdatedAt: { type: 'date' },
|
||||
fromTimestamp: { type: 'date' },
|
||||
counterType: { type: 'keyword' },
|
||||
total: { type: 'integer' },
|
||||
},
|
||||
},
|
||||
},
|
||||
fetch: async ({ soClient }: CollectorFetchContext) => {
|
||||
const { saved_objects: rawUiCounters } = await soClient.find<UICounterSavedObjectAttributes>({
|
||||
type: UI_COUNTER_SAVED_OBJECT_TYPE,
|
||||
fields: ['count'],
|
||||
perPage: 10000,
|
||||
});
|
||||
|
||||
return {
|
||||
dailyEvents: rawUiCounters.reduce((acc, raw) => {
|
||||
try {
|
||||
const aggEvent = transformRawCounter(raw);
|
||||
acc.push(aggEvent);
|
||||
} catch (_) {
|
||||
// swallow error; allows sending successfully transformed objects.
|
||||
}
|
||||
return acc;
|
||||
}, [] as UiCounterEvent[]),
|
||||
};
|
||||
},
|
||||
isReady: () => true,
|
||||
});
|
||||
|
||||
usageCollection.registerCollector(collector);
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Roll indices every 24h
|
||||
*/
|
||||
export const ROLL_INDICES_INTERVAL = 24 * 60 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* Start rolling indices after 5 minutes up
|
||||
*/
|
||||
export const ROLL_INDICES_START = 5 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* Number of days to keep the UI counters saved object documents
|
||||
*/
|
||||
export const UI_COUNTERS_KEEP_DOCS_FOR_DAYS = 3;
|
|
@ -17,9 +17,4 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export interface Stats {
|
||||
min: number;
|
||||
max: number;
|
||||
sum: number;
|
||||
avg: number;
|
||||
}
|
||||
export { registerUiCountersRollups } from './register_rollups';
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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 { timer } from 'rxjs';
|
||||
import { Logger, ISavedObjectsRepository } from 'kibana/server';
|
||||
import { ROLL_INDICES_INTERVAL, ROLL_INDICES_START } from './constants';
|
||||
import { rollUiCounterIndices } from './rollups';
|
||||
|
||||
export function registerUiCountersRollups(
|
||||
logger: Logger,
|
||||
getSavedObjectsClient: () => ISavedObjectsRepository | undefined
|
||||
) {
|
||||
timer(ROLL_INDICES_START, ROLL_INDICES_INTERVAL).subscribe(() =>
|
||||
rollUiCounterIndices(logger, getSavedObjectsClient())
|
||||
);
|
||||
}
|
|
@ -0,0 +1,175 @@
|
|||
/*
|
||||
* 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 moment from 'moment';
|
||||
import { isSavedObjectOlderThan, rollUiCounterIndices } from './rollups';
|
||||
import { savedObjectsRepositoryMock, loggingSystemMock } from '../../../../../../core/server/mocks';
|
||||
import { SavedObjectsFindResult } from 'kibana/server';
|
||||
import {
|
||||
UICounterSavedObjectAttributes,
|
||||
UI_COUNTER_SAVED_OBJECT_TYPE,
|
||||
} from '../ui_counter_saved_object_type';
|
||||
import { UI_COUNTERS_KEEP_DOCS_FOR_DAYS } from './constants';
|
||||
|
||||
const createMockSavedObjectDoc = (updatedAt: moment.Moment, id: string) =>
|
||||
({
|
||||
id,
|
||||
type: 'ui-counter',
|
||||
attributes: {
|
||||
count: 3,
|
||||
},
|
||||
references: [],
|
||||
updated_at: updatedAt.format(),
|
||||
version: 'WzI5LDFd',
|
||||
score: 0,
|
||||
} as SavedObjectsFindResult<UICounterSavedObjectAttributes>);
|
||||
|
||||
describe('isSavedObjectOlderThan', () => {
|
||||
it(`returns true if doc is older than x days`, () => {
|
||||
const numberOfDays = 1;
|
||||
const startDate = moment().format();
|
||||
const doc = createMockSavedObjectDoc(moment().subtract(2, 'days'), 'some-id');
|
||||
const result = isSavedObjectOlderThan({
|
||||
numberOfDays,
|
||||
startDate,
|
||||
doc,
|
||||
});
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it(`returns false if doc is exactly x days old`, () => {
|
||||
const numberOfDays = 1;
|
||||
const startDate = moment().format();
|
||||
const doc = createMockSavedObjectDoc(moment().subtract(1, 'days'), 'some-id');
|
||||
const result = isSavedObjectOlderThan({
|
||||
numberOfDays,
|
||||
startDate,
|
||||
doc,
|
||||
});
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it(`returns false if doc is younger than x days`, () => {
|
||||
const numberOfDays = 2;
|
||||
const startDate = moment().format();
|
||||
const doc = createMockSavedObjectDoc(moment().subtract(1, 'days'), 'some-id');
|
||||
const result = isSavedObjectOlderThan({
|
||||
numberOfDays,
|
||||
startDate,
|
||||
doc,
|
||||
});
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('rollUiCounterIndices', () => {
|
||||
let logger: ReturnType<typeof loggingSystemMock.createLogger>;
|
||||
let savedObjectClient: ReturnType<typeof savedObjectsRepositoryMock.create>;
|
||||
|
||||
beforeEach(() => {
|
||||
logger = loggingSystemMock.createLogger();
|
||||
savedObjectClient = savedObjectsRepositoryMock.create();
|
||||
});
|
||||
|
||||
it('returns undefined if no savedObjectsClient initialised yet', async () => {
|
||||
await expect(rollUiCounterIndices(logger, undefined)).resolves.toBe(undefined);
|
||||
expect(logger.warn).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('does not delete any documents on empty saved objects', async () => {
|
||||
savedObjectClient.find.mockImplementation(async ({ type, page = 1, perPage = 10 }) => {
|
||||
switch (type) {
|
||||
case UI_COUNTER_SAVED_OBJECT_TYPE:
|
||||
return { saved_objects: [], total: 0, page, per_page: perPage };
|
||||
default:
|
||||
throw new Error(`Unexpected type [${type}]`);
|
||||
}
|
||||
});
|
||||
await expect(rollUiCounterIndices(logger, savedObjectClient)).resolves.toEqual([]);
|
||||
expect(savedObjectClient.find).toBeCalled();
|
||||
expect(savedObjectClient.delete).not.toBeCalled();
|
||||
expect(logger.warn).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it(`deletes documents older than ${UI_COUNTERS_KEEP_DOCS_FOR_DAYS} days`, async () => {
|
||||
const mockSavedObjects = [
|
||||
createMockSavedObjectDoc(moment().subtract(5, 'days'), 'doc-id-1'),
|
||||
createMockSavedObjectDoc(moment().subtract(1, 'days'), 'doc-id-2'),
|
||||
createMockSavedObjectDoc(moment().subtract(6, 'days'), 'doc-id-3'),
|
||||
];
|
||||
|
||||
savedObjectClient.find.mockImplementation(async ({ type, page = 1, perPage = 10 }) => {
|
||||
switch (type) {
|
||||
case UI_COUNTER_SAVED_OBJECT_TYPE:
|
||||
return { saved_objects: mockSavedObjects, total: 0, page, per_page: perPage };
|
||||
default:
|
||||
throw new Error(`Unexpected type [${type}]`);
|
||||
}
|
||||
});
|
||||
await expect(rollUiCounterIndices(logger, savedObjectClient)).resolves.toHaveLength(2);
|
||||
expect(savedObjectClient.find).toBeCalled();
|
||||
expect(savedObjectClient.delete).toHaveBeenCalledTimes(2);
|
||||
expect(savedObjectClient.delete).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
UI_COUNTER_SAVED_OBJECT_TYPE,
|
||||
'doc-id-1'
|
||||
);
|
||||
expect(savedObjectClient.delete).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
UI_COUNTER_SAVED_OBJECT_TYPE,
|
||||
'doc-id-3'
|
||||
);
|
||||
expect(logger.warn).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it(`logs warnings on savedObject.find failure`, async () => {
|
||||
savedObjectClient.find.mockImplementation(async () => {
|
||||
throw new Error(`Expected error!`);
|
||||
});
|
||||
await expect(rollUiCounterIndices(logger, savedObjectClient)).resolves.toEqual(undefined);
|
||||
expect(savedObjectClient.find).toBeCalled();
|
||||
expect(savedObjectClient.delete).not.toBeCalled();
|
||||
expect(logger.warn).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it(`logs warnings on savedObject.delete failure`, async () => {
|
||||
const mockSavedObjects = [createMockSavedObjectDoc(moment().subtract(5, 'days'), 'doc-id-1')];
|
||||
|
||||
savedObjectClient.find.mockImplementation(async ({ type, page = 1, perPage = 10 }) => {
|
||||
switch (type) {
|
||||
case UI_COUNTER_SAVED_OBJECT_TYPE:
|
||||
return { saved_objects: mockSavedObjects, total: 0, page, per_page: perPage };
|
||||
default:
|
||||
throw new Error(`Unexpected type [${type}]`);
|
||||
}
|
||||
});
|
||||
savedObjectClient.delete.mockImplementation(async () => {
|
||||
throw new Error(`Expected error!`);
|
||||
});
|
||||
await expect(rollUiCounterIndices(logger, savedObjectClient)).resolves.toEqual(undefined);
|
||||
expect(savedObjectClient.find).toBeCalled();
|
||||
expect(savedObjectClient.delete).toHaveBeenCalledTimes(1);
|
||||
expect(savedObjectClient.delete).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
UI_COUNTER_SAVED_OBJECT_TYPE,
|
||||
'doc-id-1'
|
||||
);
|
||||
expect(logger.warn).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* 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 { ISavedObjectsRepository, Logger } from 'kibana/server';
|
||||
import moment from 'moment';
|
||||
|
||||
import { UI_COUNTERS_KEEP_DOCS_FOR_DAYS } from './constants';
|
||||
import {
|
||||
UICounterSavedObject,
|
||||
UI_COUNTER_SAVED_OBJECT_TYPE,
|
||||
} from '../ui_counter_saved_object_type';
|
||||
|
||||
export function isSavedObjectOlderThan({
|
||||
numberOfDays,
|
||||
startDate,
|
||||
doc,
|
||||
}: {
|
||||
numberOfDays: number;
|
||||
startDate: moment.Moment | string | number;
|
||||
doc: Pick<UICounterSavedObject, 'updated_at'>;
|
||||
}): boolean {
|
||||
const { updated_at: updatedAt } = doc;
|
||||
const today = moment(startDate).startOf('day');
|
||||
const updateDay = moment(updatedAt).startOf('day');
|
||||
|
||||
const diffInDays = today.diff(updateDay, 'days');
|
||||
if (diffInDays > numberOfDays) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function rollUiCounterIndices(
|
||||
logger: Logger,
|
||||
savedObjectsClient?: ISavedObjectsRepository
|
||||
) {
|
||||
if (!savedObjectsClient) {
|
||||
return;
|
||||
}
|
||||
|
||||
const now = moment();
|
||||
|
||||
try {
|
||||
const { saved_objects: rawUiCounterDocs } = await savedObjectsClient.find<UICounterSavedObject>(
|
||||
{
|
||||
type: UI_COUNTER_SAVED_OBJECT_TYPE,
|
||||
perPage: 1000, // Process 1000 at a time as a compromise of speed and overload
|
||||
}
|
||||
);
|
||||
|
||||
const docsToDelete = rawUiCounterDocs.filter((doc) =>
|
||||
isSavedObjectOlderThan({
|
||||
numberOfDays: UI_COUNTERS_KEEP_DOCS_FOR_DAYS,
|
||||
startDate: now,
|
||||
doc,
|
||||
})
|
||||
);
|
||||
|
||||
return await Promise.all(
|
||||
docsToDelete.map(({ id }) => savedObjectsClient.delete(UI_COUNTER_SAVED_OBJECT_TYPE, id))
|
||||
);
|
||||
} catch (err) {
|
||||
logger.warn(`Failed to rollup UI Counters saved objects.`);
|
||||
logger.warn(err);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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 { SavedObject, SavedObjectAttributes, SavedObjectsServiceSetup } from 'kibana/server';
|
||||
|
||||
export interface UICounterSavedObjectAttributes extends SavedObjectAttributes {
|
||||
count: number;
|
||||
}
|
||||
|
||||
export type UICounterSavedObject = SavedObject<UICounterSavedObjectAttributes>;
|
||||
|
||||
export const UI_COUNTER_SAVED_OBJECT_TYPE = 'ui-counter';
|
||||
|
||||
export function registerUiCounterSavedObjectType(savedObjectsSetup: SavedObjectsServiceSetup) {
|
||||
savedObjectsSetup.registerType({
|
||||
name: UI_COUNTER_SAVED_OBJECT_TYPE,
|
||||
hidden: false,
|
||||
namespaceType: 'agnostic',
|
||||
mappings: {
|
||||
properties: {
|
||||
count: { type: 'integer' },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
|
@ -80,7 +80,7 @@ const uiMetricFromDataPluginSchema: MakeSchemaFrom<UIMetricUsage> = {
|
|||
};
|
||||
|
||||
// TODO: Find a way to retrieve it automatically
|
||||
// Searching `reportUiStats` across Kibana
|
||||
// Searching `reportUiCounter` across Kibana
|
||||
export const uiMetricSchema: MakeSchemaFrom<UIMetricUsage> = {
|
||||
console: commonSchema,
|
||||
DashboardPanelVersionInUrl: commonSchema,
|
||||
|
|
|
@ -42,6 +42,9 @@ import {
|
|||
registerCspCollector,
|
||||
registerCoreUsageCollector,
|
||||
registerLocalizationUsageCollector,
|
||||
registerUiCountersUsageCollector,
|
||||
registerUiCounterSavedObjectType,
|
||||
registerUiCountersRollups,
|
||||
} from './collectors';
|
||||
|
||||
interface KibanaUsageCollectionPluginsDepsSetup {
|
||||
|
@ -65,8 +68,11 @@ export class KibanaUsageCollectionPlugin implements Plugin {
|
|||
}
|
||||
|
||||
public setup(coreSetup: CoreSetup, { usageCollection }: KibanaUsageCollectionPluginsDepsSetup) {
|
||||
this.registerUsageCollectors(usageCollection, coreSetup, this.metric$, (opts) =>
|
||||
coreSetup.savedObjects.registerType(opts)
|
||||
this.registerUsageCollectors(
|
||||
usageCollection,
|
||||
coreSetup,
|
||||
this.metric$,
|
||||
coreSetup.savedObjects.registerType.bind(coreSetup.savedObjects)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -93,6 +99,10 @@ export class KibanaUsageCollectionPlugin implements Plugin {
|
|||
const getUiSettingsClient = () => this.uiSettingsClient;
|
||||
const getCoreUsageDataService = () => this.coreUsageData!;
|
||||
|
||||
registerUiCounterSavedObjectType(coreSetup.savedObjects);
|
||||
registerUiCountersRollups(this.logger.get('ui-counters'), getSavedObjectsClient);
|
||||
registerUiCountersUsageCollector(usageCollection);
|
||||
|
||||
registerOpsStatsCollector(usageCollection, metric$);
|
||||
registerKibanaUsageCollector(usageCollection, this.legacyConfig$);
|
||||
registerManagementUsageCollector(usageCollection, getUiSettingsClient);
|
||||
|
|
|
@ -1920,6 +1920,35 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"ui_counters": {
|
||||
"properties": {
|
||||
"dailyEvents": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"properties": {
|
||||
"appName": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"eventName": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"lastUpdatedAt": {
|
||||
"type": "date"
|
||||
},
|
||||
"fromTimestamp": {
|
||||
"type": "date"
|
||||
},
|
||||
"counterType": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"total": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"ui_metric": {
|
||||
"properties": {
|
||||
"console": {
|
||||
|
|
|
@ -328,27 +328,22 @@ There are a few ways you can test that your usage collector is working properly.
|
|||
|
||||
# UI Metric app
|
||||
|
||||
The UI metrics implementation in its current state is not useful. We are working on improving the implementation to enable teams to use the data to visualize and gather information from what is being reported. Please refer to the telemetry team if you are interested in adding ui_metrics to your plugin.
|
||||
UI_metric is deprecated in favor of UI Counters.
|
||||
|
||||
**Until a better implementation is introduced, please defer from adding any new ui metrics.**
|
||||
# UI Counters
|
||||
|
||||
## 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".
|
||||
UI Counters provides instrumentation in the UI to count triggered events such as component loaded, button clicked, or counting when an event occurs. 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.
|
||||
The events have a per day granularity.
|
||||
|
||||
## How to use it
|
||||
|
||||
To track a user interaction, use the `reportUiStats` method exposed by the plugin `usageCollection` in the public side:
|
||||
To track a user interaction, use the `usageCollection.reportUiCounter` method exposed by the plugin `usageCollection` in the public side:
|
||||
|
||||
1. Similarly to the server-side usage collection, make sure `usageCollection` is in your optional Plugins:
|
||||
|
||||
|
@ -364,34 +359,49 @@ To track a user interaction, use the `reportUiStats` method exposed by the plugi
|
|||
|
||||
```ts
|
||||
// public/plugin.ts
|
||||
import { METRIC_TYPE } from '@kbn/analytics';
|
||||
|
||||
class Plugin {
|
||||
setup(core, { usageCollection }) {
|
||||
if (usageCollection) {
|
||||
// Call the following method as many times as you want to report an increase in the count for this event
|
||||
usageCollection.reportUiStats(`<AppName>`, usageCollection.METRIC_TYPE.CLICK, `<EventName>`);
|
||||
usageCollection.reportUiCounter(`<AppName>`, METRIC_TYPE.CLICK, `<EventName>`);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Metric Types:
|
||||
### 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> });`
|
||||
- `METRIC_TYPE.CLICK` for tracking clicks.
|
||||
- `METRIC_TYPE.LOADED` for a component load, a page load, or a request load.
|
||||
- `METRIC_TYPE.COUNT` is the generic counter for miscellaneous events.
|
||||
|
||||
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)`.
|
||||
accepts three arguments, `AppName`, `metricType` and `eventNames`. These should be underscore-delimited strings.
|
||||
|
||||
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'])`.
|
||||
### Reporting multiple events at once
|
||||
|
||||
To track multiple metrics within a single request, provide an array of events
|
||||
|
||||
```
|
||||
usageCollection.reportUiCounter(`<AppName>`, METRIC_TYPE.CLICK, [`<EventName1>`, `<EventName2>`]);
|
||||
```
|
||||
|
||||
### Increamenting counter by more than 1
|
||||
|
||||
To track an event occurance more than once in the same call, provide a 4th argument to the `reportUiCounter` function:
|
||||
|
||||
```
|
||||
usageCollection.reportUiCounter(`<AppName>`, METRIC_TYPE.CLICK, `<EventName>`, 3);
|
||||
```
|
||||
|
||||
### 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.
|
||||
The colon character (`:`) should not be used in the app name. Colons play
|
||||
a special role for `appName` in how metrics are stored as saved objects.
|
||||
|
||||
### Tracking timed interactions
|
||||
|
||||
|
@ -402,34 +412,7 @@ measure interactions that take less than 1 minute, 1-5 minutes, 5-20 minutes, an
|
|||
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.
|
||||
|
||||
The only caveat is that it makes it harder to consume in Kibana when analysing each entry in the array separately. In the telemetry team we are working to find a solution to this.
|
||||
|
||||
# Routes registered by this plugin
|
||||
|
||||
- `/api/ui_metric/report`: Used by `ui_metrics` usage collector instances to report their usage data to the server
|
||||
- `/api/ui_counters/_report`: Used by `ui_metrics` and `ui_counters` usage collector instances to report their usage data to the server
|
||||
- `/api/stats`: Get the metrics and usage ([details](./server/routes/stats/README.md))
|
||||
|
|
|
@ -24,7 +24,7 @@ export type Setup = jest.Mocked<UsageCollectionSetup>;
|
|||
const createSetupContract = (): Setup => {
|
||||
const setupContract: Setup = {
|
||||
allowTrackUserAgent: jest.fn(),
|
||||
reportUiStats: jest.fn(),
|
||||
reportUiCounter: jest.fn(),
|
||||
METRIC_TYPE,
|
||||
__LEGACY: {
|
||||
appChanged: jest.fn(),
|
||||
|
|
|
@ -31,7 +31,7 @@ import {
|
|||
import { reportApplicationUsage } from './services/application_usage';
|
||||
|
||||
export interface PublicConfigType {
|
||||
uiMetric: {
|
||||
uiCounters: {
|
||||
enabled: boolean;
|
||||
debug: boolean;
|
||||
};
|
||||
|
@ -39,7 +39,7 @@ export interface PublicConfigType {
|
|||
|
||||
export interface UsageCollectionSetup {
|
||||
allowTrackUserAgent: (allow: boolean) => void;
|
||||
reportUiStats: Reporter['reportUiStats'];
|
||||
reportUiCounter: Reporter['reportUiCounter'];
|
||||
METRIC_TYPE: typeof METRIC_TYPE;
|
||||
__LEGACY: {
|
||||
/**
|
||||
|
@ -53,7 +53,7 @@ export interface UsageCollectionSetup {
|
|||
}
|
||||
|
||||
export interface UsageCollectionStart {
|
||||
reportUiStats: Reporter['reportUiStats'];
|
||||
reportUiCounter: Reporter['reportUiCounter'];
|
||||
METRIC_TYPE: typeof METRIC_TYPE;
|
||||
}
|
||||
|
||||
|
@ -73,7 +73,7 @@ export class UsageCollectionPlugin implements Plugin<UsageCollectionSetup, Usage
|
|||
|
||||
public setup({ http }: CoreSetup): UsageCollectionSetup {
|
||||
const localStorage = new Storage(window.localStorage);
|
||||
const debug = this.config.uiMetric.debug;
|
||||
const debug = this.config.uiCounters.debug;
|
||||
|
||||
this.reporter = createReporter({
|
||||
localStorage,
|
||||
|
@ -85,7 +85,7 @@ export class UsageCollectionPlugin implements Plugin<UsageCollectionSetup, Usage
|
|||
allowTrackUserAgent: (allow: boolean) => {
|
||||
this.trackUserAgent = allow;
|
||||
},
|
||||
reportUiStats: this.reporter.reportUiStats,
|
||||
reportUiCounter: this.reporter.reportUiCounter,
|
||||
METRIC_TYPE,
|
||||
__LEGACY: {
|
||||
appChanged: (appId) => this.legacyAppId$.next(appId),
|
||||
|
@ -98,7 +98,7 @@ export class UsageCollectionPlugin implements Plugin<UsageCollectionSetup, Usage
|
|||
throw new Error('Usage collection reporter not set up correctly');
|
||||
}
|
||||
|
||||
if (this.config.uiMetric.enabled && !isUnauthenticated(http)) {
|
||||
if (this.config.uiCounters.enabled && !isUnauthenticated(http)) {
|
||||
this.reporter.start();
|
||||
}
|
||||
|
||||
|
@ -109,7 +109,7 @@ export class UsageCollectionPlugin implements Plugin<UsageCollectionSetup, Usage
|
|||
reportApplicationUsage(merge(application.currentAppId$, this.legacyAppId$), this.reporter);
|
||||
|
||||
return {
|
||||
reportUiStats: this.reporter.reportUiStats,
|
||||
reportUiCounter: this.reporter.reportUiCounter,
|
||||
METRIC_TYPE,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@ export function createReporter(config: AnalyicsReporterConfig): Reporter {
|
|||
debug,
|
||||
storage: localStorage,
|
||||
async http(report) {
|
||||
const response = await fetch.post('/api/ui_metric/report', {
|
||||
const response = await fetch.post('/api/ui_counters/_report', {
|
||||
body: JSON.stringify({ report }),
|
||||
});
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ import { PluginConfigDescriptor } from 'src/core/server';
|
|||
import { DEFAULT_MAXIMUM_WAIT_TIME_FOR_ALL_COLLECTORS_IN_S } from '../common/constants';
|
||||
|
||||
export const configSchema = schema.object({
|
||||
uiMetric: schema.object({
|
||||
uiCounters: schema.object({
|
||||
enabled: schema.boolean({ defaultValue: true }),
|
||||
debug: schema.boolean({ defaultValue: schema.contextRef('dev') }),
|
||||
}),
|
||||
|
@ -36,10 +36,12 @@ 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'),
|
||||
renameFromRoot('ui_metric.enabled', 'usageCollection.uiCounters.enabled'),
|
||||
renameFromRoot('ui_metric.debug', 'usageCollection.uiCounters.debug'),
|
||||
renameFromRoot('usageCollection.uiMetric.enabled', 'usageCollection.uiCounters.enabled'),
|
||||
renameFromRoot('usageCollection.uiMetric.debug', 'usageCollection.uiCounters.debug'),
|
||||
],
|
||||
exposeToBrowser: {
|
||||
uiMetric: true,
|
||||
uiCounters: true,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -21,7 +21,7 @@ import { schema, TypeOf } from '@kbn/config-schema';
|
|||
import { METRIC_TYPE } from '@kbn/analytics';
|
||||
|
||||
export const reportSchema = schema.object({
|
||||
reportVersion: schema.maybe(schema.literal(1)),
|
||||
reportVersion: schema.maybe(schema.oneOf([schema.literal(1), schema.literal(2)])),
|
||||
userAgent: schema.maybe(
|
||||
schema.recordOf(
|
||||
schema.string(),
|
||||
|
@ -33,7 +33,7 @@ export const reportSchema = schema.object({
|
|||
})
|
||||
)
|
||||
),
|
||||
uiStatsMetrics: schema.maybe(
|
||||
uiCounter: schema.maybe(
|
||||
schema.recordOf(
|
||||
schema.string(),
|
||||
schema.object({
|
||||
|
@ -45,12 +45,7 @@ export const reportSchema = schema.object({
|
|||
]),
|
||||
appName: schema.string(),
|
||||
eventName: schema.string(),
|
||||
stats: schema.object({
|
||||
min: schema.number(),
|
||||
sum: schema.number(),
|
||||
max: schema.number(),
|
||||
avg: schema.number(),
|
||||
}),
|
||||
total: schema.number(),
|
||||
})
|
||||
)
|
||||
),
|
||||
|
|
|
@ -21,12 +21,16 @@ import { savedObjectsRepositoryMock } from '../../../../core/server/mocks';
|
|||
import { storeReport } from './store_report';
|
||||
import { ReportSchemaType } from './schema';
|
||||
import { METRIC_TYPE } from '@kbn/analytics';
|
||||
import moment from 'moment';
|
||||
|
||||
describe('store_report', () => {
|
||||
const momentTimestamp = moment();
|
||||
const date = momentTimestamp.format('DDMMYYYY');
|
||||
|
||||
test('stores report for all types of data', async () => {
|
||||
const savedObjectClient = savedObjectsRepositoryMock.create();
|
||||
const report: ReportSchemaType = {
|
||||
reportVersion: 1,
|
||||
reportVersion: 2,
|
||||
userAgent: {
|
||||
'key-user-agent': {
|
||||
key: 'test-key',
|
||||
|
@ -35,18 +39,20 @@ describe('store_report', () => {
|
|||
userAgent: 'test-user-agent',
|
||||
},
|
||||
},
|
||||
uiStatsMetrics: {
|
||||
any: {
|
||||
uiCounter: {
|
||||
eventOneId: {
|
||||
key: 'test-key',
|
||||
type: METRIC_TYPE.LOADED,
|
||||
appName: 'test-app-name',
|
||||
eventName: 'test-event-name',
|
||||
total: 1,
|
||||
},
|
||||
eventTwoId: {
|
||||
key: 'test-key',
|
||||
type: METRIC_TYPE.CLICK,
|
||||
appName: 'test-app-name',
|
||||
eventName: 'test-event-name',
|
||||
stats: {
|
||||
min: 1,
|
||||
max: 2,
|
||||
avg: 1.5,
|
||||
sum: 3,
|
||||
},
|
||||
total: 2,
|
||||
},
|
||||
},
|
||||
application_usage: {
|
||||
|
@ -66,12 +72,25 @@ describe('store_report', () => {
|
|||
overwrite: true,
|
||||
}
|
||||
);
|
||||
expect(savedObjectClient.incrementCounter).toHaveBeenCalledWith(
|
||||
expect(savedObjectClient.incrementCounter).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'ui-metric',
|
||||
'test-app-name:test-event-name',
|
||||
['count']
|
||||
[{ fieldName: 'count', incrementBy: 3 }]
|
||||
);
|
||||
expect(savedObjectClient.bulkCreate).toHaveBeenCalledWith([
|
||||
expect(savedObjectClient.incrementCounter).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'ui-counter',
|
||||
`test-app-name:${date}:${METRIC_TYPE.LOADED}:test-event-name`,
|
||||
[{ fieldName: 'count', incrementBy: 1 }]
|
||||
);
|
||||
expect(savedObjectClient.incrementCounter).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
'ui-counter',
|
||||
`test-app-name:${date}:${METRIC_TYPE.CLICK}:test-event-name`,
|
||||
[{ fieldName: 'count', incrementBy: 2 }]
|
||||
);
|
||||
expect(savedObjectClient.bulkCreate).toHaveBeenNthCalledWith(1, [
|
||||
{
|
||||
type: 'application_usage_transactional',
|
||||
attributes: {
|
||||
|
@ -89,7 +108,7 @@ describe('store_report', () => {
|
|||
const report: ReportSchemaType = {
|
||||
reportVersion: 1,
|
||||
userAgent: void 0,
|
||||
uiStatsMetrics: void 0,
|
||||
uiCounter: void 0,
|
||||
application_usage: void 0,
|
||||
};
|
||||
await storeReport(savedObjectClient, report);
|
||||
|
|
|
@ -17,50 +17,76 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { ISavedObjectsRepository, SavedObject } from 'src/core/server';
|
||||
import { ISavedObjectsRepository } from 'src/core/server';
|
||||
import moment from 'moment';
|
||||
import { chain, sumBy } from 'lodash';
|
||||
import { ReportSchemaType } from './schema';
|
||||
|
||||
export async function storeReport(
|
||||
internalRepository: ISavedObjectsRepository,
|
||||
report: ReportSchemaType
|
||||
) {
|
||||
const uiStatsMetrics = report.uiStatsMetrics ? Object.entries(report.uiStatsMetrics) : [];
|
||||
const uiCounters = report.uiCounter ? Object.entries(report.uiCounter) : [];
|
||||
const userAgents = report.userAgent ? Object.entries(report.userAgent) : [];
|
||||
const appUsage = report.application_usage ? Object.entries(report.application_usage) : [];
|
||||
const timestamp = new Date();
|
||||
return Promise.all<{ saved_objects: Array<SavedObject<any>> }>([
|
||||
|
||||
const momentTimestamp = moment();
|
||||
const timestamp = momentTimestamp.toDate();
|
||||
const date = momentTimestamp.format('DDMMYYYY');
|
||||
|
||||
return Promise.allSettled([
|
||||
// User Agent
|
||||
...userAgents.map(async ([key, metric]) => {
|
||||
const { userAgent } = metric;
|
||||
const savedObjectId = `${key}:${userAgent}`;
|
||||
return {
|
||||
saved_objects: [
|
||||
await internalRepository.create(
|
||||
'ui-metric',
|
||||
{ count: 1 },
|
||||
{
|
||||
id: savedObjectId,
|
||||
overwrite: true,
|
||||
}
|
||||
),
|
||||
],
|
||||
};
|
||||
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 {
|
||||
saved_objects: [
|
||||
await internalRepository.incrementCounter('ui-metric', savedObjectId, ['count']),
|
||||
],
|
||||
};
|
||||
// Deprecated UI metrics, Use data from UI Counters.
|
||||
...chain(report.uiCounter)
|
||||
.groupBy((e) => `${e.appName}:${e.eventName}`)
|
||||
.entries()
|
||||
.map(([savedObjectId, metric]) => {
|
||||
return {
|
||||
savedObjectId,
|
||||
incrementBy: sumBy(metric, 'total'),
|
||||
};
|
||||
})
|
||||
.map(async ({ savedObjectId, incrementBy }) => {
|
||||
return await internalRepository.incrementCounter('ui-metric', savedObjectId, [
|
||||
{ fieldName: 'count', incrementBy },
|
||||
]);
|
||||
})
|
||||
.value(),
|
||||
// UI Counters
|
||||
...uiCounters.map(async ([key, metric]) => {
|
||||
const { appName, eventName, total, type } = metric;
|
||||
const savedObjectId = `${appName}:${date}:${type}:${eventName}`;
|
||||
return [
|
||||
await internalRepository.incrementCounter('ui-counter', savedObjectId, [
|
||||
{ fieldName: 'count', incrementBy: total },
|
||||
]),
|
||||
];
|
||||
}),
|
||||
appUsage.length
|
||||
? internalRepository.bulkCreate(
|
||||
// Application Usage
|
||||
...[
|
||||
(async () => {
|
||||
if (!appUsage.length) return [];
|
||||
const { saved_objects: savedObjects } = await internalRepository.bulkCreate(
|
||||
appUsage.map(([appId, metric]) => ({
|
||||
type: 'application_usage_transactional',
|
||||
attributes: { ...metric, appId, timestamp },
|
||||
}))
|
||||
)
|
||||
: { saved_objects: [] },
|
||||
);
|
||||
|
||||
return savedObjects;
|
||||
})(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ import {
|
|||
} from 'src/core/server';
|
||||
import { Observable } from 'rxjs';
|
||||
import { CollectorSet } from '../collector';
|
||||
import { registerUiMetricRoute } from './report_metrics';
|
||||
import { registerUiCountersRoute } from './ui_counters';
|
||||
import { registerStatsRoute } from './stats';
|
||||
|
||||
export function setupRoutes({
|
||||
|
@ -50,6 +50,6 @@ export function setupRoutes({
|
|||
metrics: MetricsServiceSetup;
|
||||
overallStatus$: Observable<ServiceStatus>;
|
||||
}) {
|
||||
registerUiMetricRoute(router, getSavedObjects);
|
||||
registerUiCountersRoute(router, getSavedObjects);
|
||||
registerStatsRoute({ router, ...rest });
|
||||
}
|
||||
|
|
|
@ -21,13 +21,13 @@ import { schema } from '@kbn/config-schema';
|
|||
import { IRouter, ISavedObjectsRepository } from 'src/core/server';
|
||||
import { storeReport, reportSchema } from '../report';
|
||||
|
||||
export function registerUiMetricRoute(
|
||||
export function registerUiCountersRoute(
|
||||
router: IRouter,
|
||||
getSavedObjects: () => ISavedObjectsRepository | undefined
|
||||
) {
|
||||
router.post(
|
||||
{
|
||||
path: '/api/ui_metric/report',
|
||||
path: '/api/ui_counters/_report',
|
||||
validate: {
|
||||
body: schema.object({
|
||||
report: reportSchema,
|
|
@ -22,7 +22,7 @@ import React from 'react';
|
|||
import { EuiModal, EuiOverlayMask } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { METRIC_TYPE, UiStatsMetricType } from '@kbn/analytics';
|
||||
import { METRIC_TYPE, UiCounterMetricType } from '@kbn/analytics';
|
||||
import {
|
||||
ApplicationStart,
|
||||
IUiSettingsClient,
|
||||
|
@ -72,7 +72,7 @@ class NewVisModal extends React.Component<TypeSelectionProps, TypeSelectionState
|
|||
|
||||
private readonly isLabsEnabled: boolean;
|
||||
private readonly trackUiMetric:
|
||||
| ((type: UiStatsMetricType, eventNames: string | string[], count?: number) => void)
|
||||
| ((type: UiCounterMetricType, eventNames: string | string[], count?: number) => void)
|
||||
| undefined;
|
||||
|
||||
constructor(props: TypeSelectionProps) {
|
||||
|
@ -84,7 +84,7 @@ class NewVisModal extends React.Component<TypeSelectionProps, TypeSelectionState
|
|||
showGroups: true,
|
||||
};
|
||||
|
||||
this.trackUiMetric = this.props.usageCollection?.reportUiStats.bind(
|
||||
this.trackUiMetric = this.props.usageCollection?.reportUiCounter.bind(
|
||||
this.props.usageCollection,
|
||||
'visualize'
|
||||
);
|
||||
|
|
|
@ -33,6 +33,7 @@ export default function ({ loadTestFile }) {
|
|||
loadTestFile(require.resolve('./status'));
|
||||
loadTestFile(require.resolve('./stats'));
|
||||
loadTestFile(require.resolve('./ui_metric'));
|
||||
loadTestFile(require.resolve('./ui_counters'));
|
||||
loadTestFile(require.resolve('./telemetry'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* 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 const basicUiCounters = {
|
||||
dailyEvents: [
|
||||
{
|
||||
appName: 'myApp',
|
||||
eventName: 'my_event_885082425109579',
|
||||
lastUpdatedAt: '2020-11-30T11:43:00.961Z',
|
||||
fromTimestamp: '2020-11-30T00:00:00Z',
|
||||
counterType: 'loaded',
|
||||
total: 1,
|
||||
},
|
||||
{
|
||||
appName: 'myApp',
|
||||
eventName: 'my_event_885082425109579_2',
|
||||
lastUpdatedAt: '2020-10-28T11:43:00.961Z',
|
||||
fromTimestamp: '2020-10-28T00:00:00Z',
|
||||
counterType: 'count',
|
||||
total: 1,
|
||||
},
|
||||
{
|
||||
appName: 'myApp',
|
||||
eventName: 'my_event_885082425109579',
|
||||
lastUpdatedAt: '2020-11-30T11:43:00.961Z',
|
||||
fromTimestamp: '2020-11-30T00:00:00Z',
|
||||
counterType: 'click',
|
||||
total: 2,
|
||||
},
|
||||
],
|
||||
};
|
|
@ -19,7 +19,7 @@
|
|||
|
||||
import expect from '@kbn/expect';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { basicUiCounters } from './__fixtures__/ui_counters';
|
||||
/*
|
||||
* Create a single-level array with strings for all the paths to values in the
|
||||
* source object, up to 3 deep. Going deeper than 3 causes a bit too much churn
|
||||
|
@ -45,11 +45,11 @@ export default function ({ getService }) {
|
|||
after('cleanup saved objects changes', () => esArchiver.unload('saved_objects/basic'));
|
||||
|
||||
before('create some telemetry-data tracked indices', async () => {
|
||||
return es.indices.create({ index: 'filebeat-telemetry_tests_logs' });
|
||||
await es.indices.create({ index: 'filebeat-telemetry_tests_logs' });
|
||||
});
|
||||
|
||||
after('cleanup telemetry-data tracked indices', () => {
|
||||
return es.indices.delete({ index: 'filebeat-telemetry_tests_logs' });
|
||||
after('cleanup telemetry-data tracked indices', async () => {
|
||||
await es.indices.delete({ index: 'filebeat-telemetry_tests_logs' });
|
||||
});
|
||||
|
||||
it('should pull local stats and validate data types', async () => {
|
||||
|
@ -74,6 +74,7 @@ export default function ({ getService }) {
|
|||
expect(stats.stack_stats.kibana.plugins.telemetry.usage_fetcher).to.be.a('string');
|
||||
expect(stats.stack_stats.kibana.plugins.stack_management).to.be.an('object');
|
||||
expect(stats.stack_stats.kibana.plugins.ui_metric).to.be.an('object');
|
||||
expect(stats.stack_stats.kibana.plugins.ui_counters).to.be.an('object');
|
||||
expect(stats.stack_stats.kibana.plugins.application_usage).to.be.an('object');
|
||||
expect(stats.stack_stats.kibana.plugins.kql.defaultQueryLanguage).to.be.a('string');
|
||||
expect(stats.stack_stats.kibana.plugins['tsvb-validation']).to.be.an('object');
|
||||
|
@ -94,6 +95,22 @@ export default function ({ getService }) {
|
|||
expect(stats.stack_stats.data[0].size_in_bytes).to.be.a('number');
|
||||
});
|
||||
|
||||
describe('UI Counters telemetry', () => {
|
||||
before('Add UI Counters saved objects', () => esArchiver.load('saved_objects/ui_counters'));
|
||||
after('cleanup saved objects changes', () => esArchiver.unload('saved_objects/ui_counters'));
|
||||
it('returns ui counters aggregated by day', async () => {
|
||||
const { body } = await supertest
|
||||
.post('/api/telemetry/v2/clusters/_stats')
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.send({ unencrypted: true })
|
||||
.expect(200);
|
||||
|
||||
expect(body.length).to.be(1);
|
||||
const stats = body[0];
|
||||
expect(stats.stack_stats.kibana.plugins.ui_counters).to.eql(basicUiCounters);
|
||||
});
|
||||
});
|
||||
|
||||
it('should pull local stats and validate fields', async () => {
|
||||
const { body } = await supertest
|
||||
.post('/api/telemetry/v2/clusters/_stats')
|
||||
|
|
24
test/api_integration/apis/ui_counters/index.js
Normal file
24
test/api_integration/apis/ui_counters/index.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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 default function ({ loadTestFile }) {
|
||||
describe('UI Counters', () => {
|
||||
loadTestFile(require.resolve('./ui_counters'));
|
||||
});
|
||||
}
|
97
test/api_integration/apis/ui_counters/ui_counters.js
Normal file
97
test/api_integration/apis/ui_counters/ui_counters.js
Normal file
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
import { ReportManager, METRIC_TYPE } from '@kbn/analytics';
|
||||
import moment from 'moment';
|
||||
|
||||
export default function ({ getService }) {
|
||||
const supertest = getService('supertest');
|
||||
const es = getService('legacyEs');
|
||||
|
||||
const createUiCounterEvent = (eventName, type, count = 1) => ({
|
||||
eventName,
|
||||
appName: 'myApp',
|
||||
type,
|
||||
count,
|
||||
});
|
||||
|
||||
describe('UI Counters API', () => {
|
||||
const dayDate = moment().format('DDMMYYYY');
|
||||
|
||||
it('stores ui counter events in savedObjects', async () => {
|
||||
const reportManager = new ReportManager();
|
||||
|
||||
const { report } = reportManager.assignReports([
|
||||
createUiCounterEvent('my_event', METRIC_TYPE.COUNT),
|
||||
]);
|
||||
|
||||
await supertest
|
||||
.post('/api/ui_counters/_report')
|
||||
.set('kbn-xsrf', 'kibana')
|
||||
.set('content-type', 'application/json')
|
||||
.send({ report })
|
||||
.expect(200);
|
||||
|
||||
const response = await es.search({ index: '.kibana', q: 'type:ui-counter' });
|
||||
|
||||
const ids = response.hits.hits.map(({ _id }) => _id);
|
||||
expect(ids.includes(`ui-counter:myApp:${dayDate}:${METRIC_TYPE.COUNT}:my_event`)).to.eql(
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it('supports multiple events', async () => {
|
||||
const reportManager = new ReportManager();
|
||||
const hrTime = process.hrtime();
|
||||
const nano = hrTime[0] * 1000000000 + hrTime[1];
|
||||
const uniqueEventName = `my_event_${nano}`;
|
||||
const { report } = reportManager.assignReports([
|
||||
createUiCounterEvent(uniqueEventName, METRIC_TYPE.COUNT),
|
||||
createUiCounterEvent(`${uniqueEventName}_2`, METRIC_TYPE.COUNT),
|
||||
createUiCounterEvent(uniqueEventName, METRIC_TYPE.CLICK, 2),
|
||||
]);
|
||||
await supertest
|
||||
.post('/api/ui_counters/_report')
|
||||
.set('kbn-xsrf', 'kibana')
|
||||
.set('content-type', 'application/json')
|
||||
.send({ report })
|
||||
.expect(200);
|
||||
|
||||
const {
|
||||
hits: { hits },
|
||||
} = await es.search({ index: '.kibana', q: 'type:ui-counter' });
|
||||
|
||||
const countTypeEvent = hits.find(
|
||||
(hit) => hit._id === `ui-counter:myApp:${dayDate}:${METRIC_TYPE.COUNT}:${uniqueEventName}`
|
||||
);
|
||||
expect(countTypeEvent._source['ui-counter'].count).to.eql(1);
|
||||
|
||||
const clickTypeEvent = hits.find(
|
||||
(hit) => hit._id === `ui-counter:myApp:${dayDate}:${METRIC_TYPE.CLICK}:${uniqueEventName}`
|
||||
);
|
||||
expect(clickTypeEvent._source['ui-counter'].count).to.eql(2);
|
||||
|
||||
const secondEvent = hits.find(
|
||||
(hit) => hit._id === `ui-counter:myApp:${dayDate}:${METRIC_TYPE.COUNT}:${uniqueEventName}_2`
|
||||
);
|
||||
expect(secondEvent._source['ui-counter'].count).to.eql(1);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -24,11 +24,11 @@ export default function ({ getService }) {
|
|||
const supertest = getService('supertest');
|
||||
const es = getService('legacyEs');
|
||||
|
||||
const createStatsMetric = (eventName) => ({
|
||||
const createStatsMetric = (eventName, type = METRIC_TYPE.CLICK, count = 1) => ({
|
||||
eventName,
|
||||
appName: 'myApp',
|
||||
type: METRIC_TYPE.CLICK,
|
||||
count: 1,
|
||||
type,
|
||||
count,
|
||||
});
|
||||
|
||||
const createUserAgentMetric = (appName) => ({
|
||||
|
@ -38,13 +38,13 @@ export default function ({ getService }) {
|
|||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.87 Safari/537.36',
|
||||
});
|
||||
|
||||
describe('ui_metric API', () => {
|
||||
describe('ui_metric savedObject data', () => {
|
||||
it('increments the count field in the document defined by the {app}/{action_type} path', async () => {
|
||||
const reportManager = new ReportManager();
|
||||
const uiStatsMetric = createStatsMetric('myEvent');
|
||||
const { report } = reportManager.assignReports([uiStatsMetric]);
|
||||
await supertest
|
||||
.post('/api/ui_metric/report')
|
||||
.post('/api/ui_counters/_report')
|
||||
.set('kbn-xsrf', 'kibana')
|
||||
.set('content-type', 'application/json')
|
||||
.send({ report })
|
||||
|
@ -69,7 +69,7 @@ export default function ({ getService }) {
|
|||
uiStatsMetric2,
|
||||
]);
|
||||
await supertest
|
||||
.post('/api/ui_metric/report')
|
||||
.post('/api/ui_counters/_report')
|
||||
.set('kbn-xsrf', 'kibana')
|
||||
.set('content-type', 'application/json')
|
||||
.send({ report })
|
||||
|
@ -81,5 +81,30 @@ export default function ({ getService }) {
|
|||
expect(ids.includes(`ui-metric:myApp:${uniqueEventName}`)).to.eql(true);
|
||||
expect(ids.includes(`ui-metric:kibana-user_agent:${userAgentMetric.userAgent}`)).to.eql(true);
|
||||
});
|
||||
|
||||
it('aggregates multiple events with same eventID', async () => {
|
||||
const reportManager = new ReportManager();
|
||||
const hrTime = process.hrtime();
|
||||
const nano = hrTime[0] * 1000000000 + hrTime[1];
|
||||
const uniqueEventName = `my_event_${nano}`;
|
||||
const { report } = reportManager.assignReports([
|
||||
,
|
||||
createStatsMetric(uniqueEventName, METRIC_TYPE.CLICK, 2),
|
||||
createStatsMetric(uniqueEventName, METRIC_TYPE.LOADED),
|
||||
]);
|
||||
await supertest
|
||||
.post('/api/ui_counters/_report')
|
||||
.set('kbn-xsrf', 'kibana')
|
||||
.set('content-type', 'application/json')
|
||||
.send({ report })
|
||||
.expect(200);
|
||||
|
||||
const {
|
||||
hits: { hits },
|
||||
} = await es.search({ index: '.kibana', q: 'type:ui-metric' });
|
||||
|
||||
const countTypeEvent = hits.find((hit) => hit._id === `ui-metric:myApp:${uniqueEventName}`);
|
||||
expect(countTypeEvent._source['ui-metric'].count).to.eql(3);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
Binary file not shown.
|
@ -0,0 +1,274 @@
|
|||
{
|
||||
"type": "index",
|
||||
"value": {
|
||||
"index": ".kibana",
|
||||
"settings": {
|
||||
"index": {
|
||||
"number_of_shards": "1",
|
||||
"number_of_replicas": "1"
|
||||
}
|
||||
},
|
||||
"mappings": {
|
||||
"dynamic": "strict",
|
||||
"properties": {
|
||||
"config": {
|
||||
"dynamic": "true",
|
||||
"properties": {
|
||||
"buildNum": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"defaultIndex": {
|
||||
"type": "text",
|
||||
"fields": {
|
||||
"keyword": {
|
||||
"type": "keyword",
|
||||
"ignore_above": 256
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"ui-counter": {
|
||||
"properties": {
|
||||
"count": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dashboard": {
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "text"
|
||||
},
|
||||
"hits": {
|
||||
"type": "integer"
|
||||
},
|
||||
"kibanaSavedObjectMeta": {
|
||||
"properties": {
|
||||
"searchSourceJSON": {
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"optionsJSON": {
|
||||
"type": "text"
|
||||
},
|
||||
"panelsJSON": {
|
||||
"type": "text"
|
||||
},
|
||||
"refreshInterval": {
|
||||
"properties": {
|
||||
"display": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"pause": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"section": {
|
||||
"type": "integer"
|
||||
},
|
||||
"value": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"timeFrom": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"timeRestore": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"timeTo": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"title": {
|
||||
"type": "text"
|
||||
},
|
||||
"uiStateJSON": {
|
||||
"type": "text"
|
||||
},
|
||||
"version": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"index-pattern": {
|
||||
"properties": {
|
||||
"fieldFormatMap": {
|
||||
"type": "text"
|
||||
},
|
||||
"fields": {
|
||||
"type": "text"
|
||||
},
|
||||
"intervalName": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"notExpandable": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"sourceFilters": {
|
||||
"type": "text"
|
||||
},
|
||||
"timeFieldName": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"title": {
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"properties": {
|
||||
"columns": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"description": {
|
||||
"type": "text"
|
||||
},
|
||||
"hits": {
|
||||
"type": "integer"
|
||||
},
|
||||
"kibanaSavedObjectMeta": {
|
||||
"properties": {
|
||||
"searchSourceJSON": {
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sort": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"title": {
|
||||
"type": "text"
|
||||
},
|
||||
"version": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"server": {
|
||||
"properties": {
|
||||
"uuid": {
|
||||
"type": "keyword"
|
||||
}
|
||||
}
|
||||
},
|
||||
"timelion-sheet": {
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "text"
|
||||
},
|
||||
"hits": {
|
||||
"type": "integer"
|
||||
},
|
||||
"kibanaSavedObjectMeta": {
|
||||
"properties": {
|
||||
"searchSourceJSON": {
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"timelion_chart_height": {
|
||||
"type": "integer"
|
||||
},
|
||||
"timelion_columns": {
|
||||
"type": "integer"
|
||||
},
|
||||
"timelion_interval": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"timelion_other_interval": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"timelion_rows": {
|
||||
"type": "integer"
|
||||
},
|
||||
"timelion_sheet": {
|
||||
"type": "text"
|
||||
},
|
||||
"title": {
|
||||
"type": "text"
|
||||
},
|
||||
"version": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"namespace": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"references": {
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"name": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"type": {
|
||||
"type": "keyword"
|
||||
}
|
||||
},
|
||||
"type": "nested"
|
||||
},
|
||||
"type": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "date"
|
||||
},
|
||||
"url": {
|
||||
"properties": {
|
||||
"accessCount": {
|
||||
"type": "long"
|
||||
},
|
||||
"accessDate": {
|
||||
"type": "date"
|
||||
},
|
||||
"createDate": {
|
||||
"type": "date"
|
||||
},
|
||||
"url": {
|
||||
"type": "text",
|
||||
"fields": {
|
||||
"keyword": {
|
||||
"type": "keyword",
|
||||
"ignore_above": 2048
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"visualization": {
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "text"
|
||||
},
|
||||
"kibanaSavedObjectMeta": {
|
||||
"properties": {
|
||||
"searchSourceJSON": {
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"savedSearchId": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"title": {
|
||||
"type": "text"
|
||||
},
|
||||
"uiStateJSON": {
|
||||
"type": "text"
|
||||
},
|
||||
"version": {
|
||||
"type": "integer"
|
||||
},
|
||||
"visState": {
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -41,7 +41,7 @@ describe('renderApp', () => {
|
|||
const plugins = {
|
||||
licensing: { license$: new Observable() },
|
||||
triggersActionsUi: { actionTypeRegistry: {}, alertTypeRegistry: {} },
|
||||
usageCollection: { reportUiStats: () => {} },
|
||||
usageCollection: { reportUiCounter: () => {} },
|
||||
data: {
|
||||
query: {
|
||||
timefilter: {
|
||||
|
|
|
@ -17,7 +17,7 @@ import * as useFetcherModule from '../../../hooks/use_fetcher';
|
|||
import { ServiceMap } from './';
|
||||
|
||||
const KibanaReactContext = createKibanaReactContext({
|
||||
usageCollection: { reportUiStats: () => {} },
|
||||
usageCollection: { reportUiCounter: () => {} },
|
||||
} as Partial<CoreStart>);
|
||||
|
||||
const activeLicense = new License({
|
||||
|
|
|
@ -26,7 +26,7 @@ import { MockUrlParamsContextProvider } from '../../../context/url_params_contex
|
|||
import * as hook from './use_anomaly_detection_jobs_fetcher';
|
||||
|
||||
const KibanaReactContext = createKibanaReactContext({
|
||||
usageCollection: { reportUiStats: () => {} },
|
||||
usageCollection: { reportUiCounter: () => {} },
|
||||
} as Partial<CoreStart>);
|
||||
|
||||
const addWarning = jest.fn();
|
||||
|
|
|
@ -23,7 +23,7 @@ import { renderWithTheme } from '../../../utils/testHelpers';
|
|||
import { ServiceOverview } from './';
|
||||
|
||||
const KibanaReactContext = createKibanaReactContext({
|
||||
usageCollection: { reportUiStats: () => {} },
|
||||
usageCollection: { reportUiCounter: () => {} },
|
||||
} as Partial<CoreStart>);
|
||||
|
||||
function Wrapper({ children }: { children?: ReactNode }) {
|
||||
|
|
|
@ -25,7 +25,7 @@ import { fromQuery } from '../../shared/Links/url_helpers';
|
|||
import { TransactionOverview } from './';
|
||||
|
||||
const KibanaReactContext = createKibanaReactContext({
|
||||
usageCollection: { reportUiStats: () => {} },
|
||||
usageCollection: { reportUiCounter: () => {} },
|
||||
} as Partial<CoreStart>);
|
||||
|
||||
const history = createMemoryHistory();
|
||||
|
|
|
@ -137,7 +137,7 @@ export const initializeCanvas = async (
|
|||
});
|
||||
|
||||
if (setupPlugins.usageCollection) {
|
||||
initStatsReporter(setupPlugins.usageCollection.reportUiStats);
|
||||
initStatsReporter(setupPlugins.usageCollection.reportUiCounter);
|
||||
}
|
||||
|
||||
return canvasStore;
|
||||
|
|
|
@ -4,21 +4,21 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { UiStatsMetricType, METRIC_TYPE } from '@kbn/analytics';
|
||||
import { UiCounterMetricType, METRIC_TYPE } from '@kbn/analytics';
|
||||
import { UsageCollectionSetup } from 'src/plugins/usage_collection/public';
|
||||
|
||||
export { METRIC_TYPE };
|
||||
|
||||
export let reportUiStats: UsageCollectionSetup['reportUiStats'] | undefined;
|
||||
export let reportUiCounter: UsageCollectionSetup['reportUiCounter'] | undefined;
|
||||
|
||||
export function init(_reportUiStats: UsageCollectionSetup['reportUiStats']): void {
|
||||
reportUiStats = _reportUiStats;
|
||||
export function init(_reportUiCounter: UsageCollectionSetup['reportUiCounter']): void {
|
||||
reportUiCounter = _reportUiCounter;
|
||||
}
|
||||
|
||||
export function trackCanvasUiMetric(metricType: UiStatsMetricType, name: string | string[]) {
|
||||
if (!reportUiStats) {
|
||||
export function trackCanvasUiMetric(metricType: UiCounterMetricType, name: string | string[]) {
|
||||
if (!reportUiCounter) {
|
||||
return;
|
||||
}
|
||||
|
||||
reportUiStats('canvas', metricType, name);
|
||||
reportUiCounter('canvas', metricType, name);
|
||||
}
|
||||
|
|
|
@ -5,17 +5,17 @@
|
|||
*/
|
||||
|
||||
import { UsageCollectionSetup } from 'src/plugins/usage_collection/public';
|
||||
import { UiStatsMetricType, METRIC_TYPE } from '@kbn/analytics';
|
||||
import { UiCounterMetricType, METRIC_TYPE } from '@kbn/analytics';
|
||||
|
||||
import { UIM_APP_NAME } from '../constants';
|
||||
|
||||
export { METRIC_TYPE };
|
||||
|
||||
// usageCollection is an optional dependency, so we default to a no-op.
|
||||
export let trackUiMetric = (metricType: UiStatsMetricType, eventName: string) => {};
|
||||
export let trackUiMetric = (metricType: UiCounterMetricType, eventName: string) => {};
|
||||
|
||||
export function init(usageCollection: UsageCollectionSetup): void {
|
||||
trackUiMetric = usageCollection.reportUiStats.bind(usageCollection, UIM_APP_NAME);
|
||||
trackUiMetric = usageCollection.reportUiCounter.bind(usageCollection, UIM_APP_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -16,7 +16,7 @@ import {
|
|||
EuiSelectableTemplateSitewideOption,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import { METRIC_TYPE, UiStatsMetricType } from '@kbn/analytics';
|
||||
import { METRIC_TYPE, UiCounterMetricType } from '@kbn/analytics';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { ApplicationStart } from 'kibana/public';
|
||||
|
@ -36,8 +36,8 @@ import { parseSearchParams } from '../search_syntax';
|
|||
interface Props {
|
||||
globalSearch: GlobalSearchPluginStart['find'];
|
||||
navigateToUrl: ApplicationStart['navigateToUrl'];
|
||||
trackUiMetric: (metricType: UiCounterMetricType, eventName: string | string[]) => void;
|
||||
taggingApi?: SavedObjectTaggingPluginStart;
|
||||
trackUiMetric: (metricType: UiStatsMetricType, eventName: string | string[]) => void;
|
||||
basePathUrl: string;
|
||||
darkMode: boolean;
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { UiStatsMetricType } from '@kbn/analytics';
|
||||
import { UiCounterMetricType } from '@kbn/analytics';
|
||||
import { I18nProvider } from '@kbn/i18n/react';
|
||||
import { ApplicationStart } from 'kibana/public';
|
||||
import { CoreStart, Plugin } from 'src/core/public';
|
||||
|
@ -31,8 +31,8 @@ export class GlobalSearchBarPlugin implements Plugin<{}, {}> {
|
|||
{ globalSearch, savedObjectsTagging, usageCollection }: GlobalSearchBarPluginStartDeps
|
||||
) {
|
||||
const trackUiMetric = usageCollection
|
||||
? usageCollection.reportUiStats.bind(usageCollection, 'global_search_bar')
|
||||
: (metricType: UiStatsMetricType, eventName: string | string[]) => {};
|
||||
? usageCollection.reportUiCounter.bind(usageCollection, 'global_search_bar')
|
||||
: (metricType: UiCounterMetricType, eventName: string | string[]) => {};
|
||||
|
||||
core.chrome.navControls.registerCenter({
|
||||
order: 1000,
|
||||
|
@ -65,7 +65,7 @@ export class GlobalSearchBarPlugin implements Plugin<{}, {}> {
|
|||
navigateToUrl: ApplicationStart['navigateToUrl'];
|
||||
basePathUrl: string;
|
||||
darkMode: boolean;
|
||||
trackUiMetric: (metricType: UiStatsMetricType, eventName: string | string[]) => void;
|
||||
trackUiMetric: (metricType: UiCounterMetricType, eventName: string | string[]) => void;
|
||||
}) {
|
||||
ReactDOM.render(
|
||||
<I18nProvider>
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
*/
|
||||
|
||||
import { UsageCollectionSetup } from 'src/plugins/usage_collection/public';
|
||||
import { UiStatsMetricType } from '@kbn/analytics';
|
||||
import { UiCounterMetricType } from '@kbn/analytics';
|
||||
|
||||
import {
|
||||
UIM_APP_NAME,
|
||||
|
@ -25,11 +25,11 @@ import {
|
|||
|
||||
import { Phases } from '../../../common/types';
|
||||
|
||||
export let trackUiMetric = (metricType: UiStatsMetricType, eventName: string | string[]) => {};
|
||||
export let trackUiMetric = (metricType: UiCounterMetricType, eventName: string | string[]) => {};
|
||||
|
||||
export function init(usageCollection?: UsageCollectionSetup): void {
|
||||
if (usageCollection) {
|
||||
trackUiMetric = usageCollection.reportUiStats.bind(usageCollection, UIM_APP_NAME);
|
||||
trackUiMetric = usageCollection.reportUiCounter.bind(usageCollection, UIM_APP_NAME);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -39,7 +39,7 @@ export const services = {
|
|||
uiMetricService: new UiMetricService('index_management'),
|
||||
};
|
||||
|
||||
services.uiMetricService.setup({ reportUiStats() {} } as any);
|
||||
services.uiMetricService.setup({ reportUiCounter() {} } as any);
|
||||
setExtensionsService(services.extensionsService);
|
||||
setUiMetricService(services.uiMetricService);
|
||||
|
||||
|
|
|
@ -119,7 +119,7 @@ describe('index table', () => {
|
|||
extensionsService: new ExtensionsService(),
|
||||
uiMetricService: new UiMetricService('index_management'),
|
||||
};
|
||||
services.uiMetricService.setup({ reportUiStats() {} });
|
||||
services.uiMetricService.setup({ reportUiCounter() {} });
|
||||
setExtensionsService(services.extensionsService);
|
||||
setUiMetricService(services.uiMetricService);
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import { METRIC_TYPE } from '@kbn/analytics';
|
||||
import { Router, Switch, Route, Redirect } from 'react-router-dom';
|
||||
import { ScopedHistory } from 'kibana/public';
|
||||
|
||||
|
@ -14,7 +15,6 @@ import { IndexManagementHome, homeSections } from './sections/home';
|
|||
import { TemplateCreate } from './sections/template_create';
|
||||
import { TemplateClone } from './sections/template_clone';
|
||||
import { TemplateEdit } from './sections/template_edit';
|
||||
|
||||
import { useServices } from './app_context';
|
||||
import {
|
||||
ComponentTemplateCreate,
|
||||
|
@ -24,7 +24,7 @@ import {
|
|||
|
||||
export const App = ({ history }: { history: ScopedHistory }) => {
|
||||
const { uiMetricService } = useServices();
|
||||
useEffect(() => uiMetricService.trackMetric('loaded', UIM_APP_LOAD), [uiMetricService]);
|
||||
useEffect(() => uiMetricService.trackMetric(METRIC_TYPE.LOADED, UIM_APP_LOAD), [uiMetricService]);
|
||||
|
||||
return (
|
||||
<Router history={history}>
|
||||
|
|
|
@ -11,7 +11,6 @@ import { UsageCollectionSetup } from 'src/plugins/usage_collection/public';
|
|||
import { CoreSetup, CoreStart } from '../../../../../src/core/public';
|
||||
|
||||
import { FleetSetup } from '../../../fleet/public';
|
||||
import { IndexMgmtMetricsType } from '../types';
|
||||
import { UiMetricService, NotificationService, HttpService } from './services';
|
||||
import { ExtensionsService } from '../services';
|
||||
import { SharePluginStart } from '../../../../../src/plugins/share/public';
|
||||
|
@ -28,7 +27,7 @@ export interface AppDependencies {
|
|||
fleet?: FleetSetup;
|
||||
};
|
||||
services: {
|
||||
uiMetricService: UiMetricService<IndexMgmtMetricsType>;
|
||||
uiMetricService: UiMetricService;
|
||||
extensionsService: ExtensionsService;
|
||||
httpService: HttpService;
|
||||
notificationService: NotificationService;
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { RouteComponentProps } from 'react-router-dom';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { METRIC_TYPE } from '@kbn/analytics';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { ScopedHistory } from 'kibana/public';
|
||||
import { EuiLink, EuiText, EuiSpacer } from '@elastic/eui';
|
||||
|
@ -72,7 +73,7 @@ export const ComponentTemplateList: React.FunctionComponent<Props> = ({
|
|||
|
||||
// Track component loaded
|
||||
useEffect(() => {
|
||||
trackMetric('loaded', UIM_COMPONENT_TEMPLATE_LIST_LOAD);
|
||||
trackMetric(METRIC_TYPE.LOADED, UIM_COMPONENT_TEMPLATE_LIST_LOAD);
|
||||
}, [trackMetric]);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
import React, { FunctionComponent, useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { METRIC_TYPE } from '@kbn/analytics';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import {
|
||||
EuiInMemoryTable,
|
||||
|
@ -160,7 +161,7 @@ export const ComponentTable: FunctionComponent<Props> = ({
|
|||
{
|
||||
pathname: encodeURI(`/component_templates/${encodeURIComponent(name)}`),
|
||||
},
|
||||
() => trackMetric('click', UIM_COMPONENT_TEMPLATE_DETAILS)
|
||||
() => trackMetric(METRIC_TYPE.CLICK, UIM_COMPONENT_TEMPLATE_DETAILS)
|
||||
)}
|
||||
data-test-subj="templateDetailsLink"
|
||||
>
|
||||
|
|
|
@ -5,8 +5,9 @@
|
|||
*/
|
||||
|
||||
import React, { createContext, useContext } from 'react';
|
||||
import { HttpSetup, DocLinksStart, NotificationsSetup, CoreStart } from 'src/core/public';
|
||||
import { UiCounterMetricType } from '@kbn/analytics';
|
||||
|
||||
import { HttpSetup, DocLinksStart, NotificationsSetup, CoreStart } from 'src/core/public';
|
||||
import { ManagementAppMountParams } from 'src/plugins/management/public';
|
||||
import { getApi, getUseRequest, getSendRequest, getDocumentation, getBreadcrumbs } from './lib';
|
||||
|
||||
|
@ -15,7 +16,7 @@ const ComponentTemplatesContext = createContext<Context | undefined>(undefined);
|
|||
interface Props {
|
||||
httpClient: HttpSetup;
|
||||
apiBasePath: string;
|
||||
trackMetric: (type: 'loaded' | 'click' | 'count', eventName: string) => void;
|
||||
trackMetric: (type: UiCounterMetricType, eventName: string) => void;
|
||||
docLinks: DocLinksStart;
|
||||
toasts: NotificationsSetup['toasts'];
|
||||
setBreadcrumbs: ManagementAppMountParams['setBreadcrumbs'];
|
||||
|
@ -28,7 +29,7 @@ interface Context {
|
|||
api: ReturnType<typeof getApi>;
|
||||
documentation: ReturnType<typeof getDocumentation>;
|
||||
breadcrumbs: ReturnType<typeof getBreadcrumbs>;
|
||||
trackMetric: (type: 'loaded' | 'click' | 'count', eventName: string) => void;
|
||||
trackMetric: (type: UiCounterMetricType, eventName: string) => void;
|
||||
toasts: NotificationsSetup['toasts'];
|
||||
getUrlForApp: CoreStart['application']['getUrlForApp'];
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { METRIC_TYPE, UiCounterMetricType } from '@kbn/analytics';
|
||||
import {
|
||||
ComponentTemplateListItem,
|
||||
ComponentTemplateDeserialized,
|
||||
|
@ -17,12 +18,11 @@ import {
|
|||
UIM_COMPONENT_TEMPLATE_UPDATE,
|
||||
} from '../constants';
|
||||
import { UseRequestHook, SendRequestHook } from './request';
|
||||
|
||||
export const getApi = (
|
||||
useRequest: UseRequestHook,
|
||||
sendRequest: SendRequestHook,
|
||||
apiBasePath: string,
|
||||
trackMetric: (type: 'loaded' | 'click' | 'count', eventName: string) => void
|
||||
trackMetric: (type: UiCounterMetricType, eventName: string) => void
|
||||
) => {
|
||||
function useLoadComponentTemplates() {
|
||||
return useRequest<ComponentTemplateListItem[], Error>({
|
||||
|
@ -40,7 +40,7 @@ export const getApi = (
|
|||
});
|
||||
|
||||
trackMetric(
|
||||
'count',
|
||||
METRIC_TYPE.COUNT,
|
||||
names.length > 1 ? UIM_COMPONENT_TEMPLATE_DELETE_MANY : UIM_COMPONENT_TEMPLATE_DELETE
|
||||
);
|
||||
|
||||
|
@ -61,7 +61,7 @@ export const getApi = (
|
|||
body: JSON.stringify(componentTemplate),
|
||||
});
|
||||
|
||||
trackMetric('count', UIM_COMPONENT_TEMPLATE_CREATE);
|
||||
trackMetric(METRIC_TYPE.COUNT, UIM_COMPONENT_TEMPLATE_CREATE);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
@ -74,7 +74,7 @@ export const getApi = (
|
|||
body: JSON.stringify(componentTemplate),
|
||||
});
|
||||
|
||||
trackMetric('count', UIM_COMPONENT_TEMPLATE_UPDATE);
|
||||
trackMetric(METRIC_TYPE.COUNT, UIM_COMPONENT_TEMPLATE_UPDATE);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue