[7.x] [Telemetry] Introduce UI Counters (#84224) (#85038)

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Ahmad Bamieh 2020-12-04 19:55:54 +02:00 committed by GitHub
parent ff03b5b30d
commit 7824e075a3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
119 changed files with 1647 additions and 401 deletions

View file

@ -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&lt;string, string&gt;</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 |

View file

@ -15,7 +15,7 @@ Metric to track once this property changes
```typescript
metric?: {
type: UiStatsMetricType;
type: UiCounterMetricType;
name: string;
};
```

View file

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

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsIncrementCounterField](./kibana-plugin-core-server.savedobjectsincrementcounterfield.md) &gt; [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;
```

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsIncrementCounterField](./kibana-plugin-core-server.savedobjectsincrementcounterfield.md) &gt; [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;
```

View file

@ -0,0 +1,20 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [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). |

View file

@ -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&lt;string &#124; SavedObjectsIncrementCounterField&gt;</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>

View file

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

View file

@ -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&lt;string, string&gt;</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 |

View file

@ -15,7 +15,7 @@ Metric to track once this property changes
```typescript
metric?: {
type: UiStatsMetricType;
type: UiCounterMetricType;
name: string;
};
```

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -302,6 +302,7 @@ export {
SavedObjectsRepository,
SavedObjectsDeleteByNamespaceOptions,
SavedObjectsIncrementCounterOptions,
SavedObjectsIncrementCounterField,
SavedObjectsComplexFieldMapping,
SavedObjectsCoreFieldMapping,
SavedObjectsFieldMapping,

View file

@ -48,6 +48,7 @@ export {
export {
ISavedObjectsRepository,
SavedObjectsIncrementCounterOptions,
SavedObjectsIncrementCounterField,
SavedObjectsDeleteByNamespaceOptions,
} from './service/lib/repository';

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -212,7 +212,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 {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,22 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export { registerUiCountersUsageCollector } from './register_ui_counters_collector';
export { registerUiCounterSavedObjectType } from './ui_counter_saved_object_type';
export { registerUiCountersRollups } from './rollups';

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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. We are building a new way of reporting telemetry called [Pulse](../../../rfcs/text/0008_pulse.md) that will help on making these UI-Metrics easier to consume.
# 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))

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

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

View file

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

View file

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

View file

@ -41,7 +41,7 @@ describe('renderApp', () => {
const plugins = {
licensing: { license$: new Observable() },
triggersActionsUi: { actionTypeRegistry: {}, alertTypeRegistry: {} },
usageCollection: { reportUiStats: () => {} },
usageCollection: { reportUiCounter: () => {} },
data: {
query: {
timefilter: {

View file

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

View file

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

View file

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

View file

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

View file

@ -137,7 +137,7 @@ export const initializeCanvas = async (
});
if (setupPlugins.usageCollection) {
initStatsReporter(setupPlugins.usageCollection.reportUiStats);
initStatsReporter(setupPlugins.usageCollection.reportUiCounter);
}
return canvasStore;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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