mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
add flexible EBT Performance Metric Schema (#136395)
This commit is contained in:
parent
041bd90826
commit
af45ef831e
48 changed files with 1222 additions and 212 deletions
|
@ -249,6 +249,7 @@
|
|||
"@kbn/crypto-browser": "link:bazel-bin/packages/kbn-crypto-browser",
|
||||
"@kbn/datemath": "link:bazel-bin/packages/kbn-datemath",
|
||||
"@kbn/doc-links": "link:bazel-bin/packages/kbn-doc-links",
|
||||
"@kbn/ebt-tools": "link:bazel-bin/packages/kbn-ebt-tools",
|
||||
"@kbn/es-errors": "link:bazel-bin/packages/kbn-es-errors",
|
||||
"@kbn/es-query": "link:bazel-bin/packages/kbn-es-query",
|
||||
"@kbn/eslint-plugin-disable": "link:bazel-bin/packages/kbn-eslint-plugin-disable",
|
||||
|
@ -888,6 +889,7 @@
|
|||
"@types/kbn__dev-utils": "link:bazel-bin/packages/kbn-dev-utils/npm_module_types",
|
||||
"@types/kbn__doc-links": "link:bazel-bin/packages/kbn-doc-links/npm_module_types",
|
||||
"@types/kbn__docs-utils": "link:bazel-bin/packages/kbn-docs-utils/npm_module_types",
|
||||
"@types/kbn__ebt-tools": "link:bazel-bin/packages/kbn-ebt-tools/npm_module_types",
|
||||
"@types/kbn__es-archiver": "link:bazel-bin/packages/kbn-es-archiver/npm_module_types",
|
||||
"@types/kbn__es-errors": "link:bazel-bin/packages/kbn-es-errors/npm_module_types",
|
||||
"@types/kbn__es-query": "link:bazel-bin/packages/kbn-es-query/npm_module_types",
|
||||
|
|
|
@ -147,6 +147,7 @@ filegroup(
|
|||
"//packages/kbn-dev-utils:build",
|
||||
"//packages/kbn-doc-links:build",
|
||||
"//packages/kbn-docs-utils:build",
|
||||
"//packages/kbn-ebt-tools:build",
|
||||
"//packages/kbn-es-archiver:build",
|
||||
"//packages/kbn-es-errors:build",
|
||||
"//packages/kbn-es-query:build",
|
||||
|
@ -399,6 +400,7 @@ filegroup(
|
|||
"//packages/kbn-dev-utils:build_types",
|
||||
"//packages/kbn-doc-links:build_types",
|
||||
"//packages/kbn-docs-utils:build_types",
|
||||
"//packages/kbn-ebt-tools:build_types",
|
||||
"//packages/kbn-es-archiver:build_types",
|
||||
"//packages/kbn-es-errors:build_types",
|
||||
"//packages/kbn-es-query:build_types",
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { Type } from 'io-ts';
|
||||
import type { Mixed } from 'io-ts';
|
||||
import type { Observable } from 'rxjs';
|
||||
import { BehaviorSubject, Subject, combineLatest, from, merge } from 'rxjs';
|
||||
import {
|
||||
|
@ -43,7 +43,7 @@ import { ContextService } from './context_service';
|
|||
import { schemaToIoTs, validateSchema } from '../schema/validation';
|
||||
|
||||
interface EventDebugLogMeta extends LogMeta {
|
||||
ebt_event: Event;
|
||||
ebt_event: Event<unknown>;
|
||||
}
|
||||
|
||||
export class AnalyticsClient implements IAnalyticsClient {
|
||||
|
@ -65,7 +65,7 @@ export class AnalyticsClient implements IAnalyticsClient {
|
|||
private readonly shipperRegistered$ = new Subject<void>();
|
||||
private readonly eventTypeRegistry = new Map<
|
||||
EventType,
|
||||
EventTypeOpts<unknown> & { validator?: Type<Record<string, unknown>> }
|
||||
EventTypeOpts<unknown> & { validator?: Mixed }
|
||||
>();
|
||||
private readonly contextService: ContextService;
|
||||
private readonly context$ = new BehaviorSubject<Partial<EventContext>>({});
|
||||
|
@ -88,7 +88,7 @@ export class AnalyticsClient implements IAnalyticsClient {
|
|||
this.reportEnqueuedEventsWhenClientIsReady();
|
||||
}
|
||||
|
||||
public reportEvent = <EventTypeData extends Record<string, unknown>>(
|
||||
public reportEvent = <EventTypeData extends object>(
|
||||
eventType: EventType,
|
||||
eventData: EventTypeData
|
||||
) => {
|
||||
|
@ -119,14 +119,18 @@ export class AnalyticsClient implements IAnalyticsClient {
|
|||
|
||||
// If the validator is registered (dev-mode only), perform the validation.
|
||||
if (eventTypeOpts.validator) {
|
||||
validateSchema(`Event Type '${eventType}'`, eventTypeOpts.validator, eventData);
|
||||
validateSchema<EventTypeData>(
|
||||
`Event Type '${eventType}'`,
|
||||
eventTypeOpts.validator,
|
||||
eventData
|
||||
);
|
||||
}
|
||||
|
||||
const event: Event = {
|
||||
timestamp,
|
||||
event_type: eventType,
|
||||
context: this.context$.value,
|
||||
properties: eventData,
|
||||
properties: eventData as unknown as Record<string, unknown>,
|
||||
};
|
||||
|
||||
// debug-logging before checking the opt-in status to help during development
|
||||
|
|
|
@ -170,7 +170,7 @@ export interface IAnalyticsClient {
|
|||
* @param eventType The event type registered via the `registerEventType` API.
|
||||
* @param eventData The properties matching the schema declared in the `registerEventType` API.
|
||||
*/
|
||||
reportEvent: <EventTypeData extends Record<string, unknown>>(
|
||||
reportEvent: <EventTypeData extends object>(
|
||||
eventType: EventType,
|
||||
eventData: EventTypeData
|
||||
) => void;
|
||||
|
|
|
@ -108,7 +108,7 @@ export interface TelemetryCounter {
|
|||
/**
|
||||
* Definition of the full event structure
|
||||
*/
|
||||
export interface Event {
|
||||
export interface Event<Properties = Record<string, unknown>> {
|
||||
/**
|
||||
* The time the event was generated in ISO format.
|
||||
*/
|
||||
|
@ -120,7 +120,7 @@ export interface Event {
|
|||
/**
|
||||
* The specific properties of the event type.
|
||||
*/
|
||||
properties: Record<string, unknown>;
|
||||
properties: Properties;
|
||||
/**
|
||||
* The {@link EventContext} enriched during the processing pipeline.
|
||||
*/
|
||||
|
|
|
@ -19,7 +19,7 @@ const FULLSTORY_RESERVED_PROPERTIES = [
|
|||
'pageName',
|
||||
];
|
||||
|
||||
export function formatPayload(context: Record<string, unknown>): Record<string, unknown> {
|
||||
export function formatPayload(context: object): Record<string, unknown> {
|
||||
// format context keys as required for env vars, see docs: https://help.fullstory.com/hc/en-us/articles/360020623234
|
||||
return Object.fromEntries(
|
||||
Object.entries(context)
|
||||
|
|
|
@ -30,6 +30,7 @@ RUNTIME_DEPS = [
|
|||
"@npm//rxjs",
|
||||
"@npm//uuid",
|
||||
"//packages/analytics/client",
|
||||
"//packages/kbn-ebt-tools",
|
||||
"//packages/core/base/core-base-browser-mocks",
|
||||
"//packages/core/injected-metadata/core-injected-metadata-browser-mocks",
|
||||
]
|
||||
|
@ -41,6 +42,7 @@ TYPES_DEPS = [
|
|||
"@npm//rxjs",
|
||||
"//packages/kbn-logging:npm_module_types",
|
||||
"//packages/analytics/client:npm_module_types",
|
||||
"//packages/kbn-ebt-tools:npm_module_types",
|
||||
"//packages/core/base/core-base-browser-internal:npm_module_types",
|
||||
"//packages/core/injected-metadata/core-injected-metadata-browser-internal:npm_module_types",
|
||||
"//packages/core/analytics/core-analytics-browser:npm_module_types",
|
||||
|
|
|
@ -20,30 +20,150 @@ describe('AnalyticsService', () => {
|
|||
});
|
||||
test('should register some context providers on creation', async () => {
|
||||
expect(analyticsClientMock.registerContextProvider).toHaveBeenCalledTimes(3);
|
||||
await expect(
|
||||
firstValueFrom(analyticsClientMock.registerContextProvider.mock.calls[0][0].context$)
|
||||
).resolves.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"branch": "branch",
|
||||
"buildNum": 100,
|
||||
"buildSha": "buildSha",
|
||||
"isDev": true,
|
||||
"isDistributable": false,
|
||||
"version": "version",
|
||||
}
|
||||
`);
|
||||
await expect(
|
||||
firstValueFrom(analyticsClientMock.registerContextProvider.mock.calls[1][0].context$)
|
||||
).resolves.toEqual({ session_id: expect.any(String) });
|
||||
await expect(
|
||||
firstValueFrom(analyticsClientMock.registerContextProvider.mock.calls[2][0].context$)
|
||||
).resolves.toEqual({
|
||||
expect(
|
||||
await firstValueFrom(analyticsClientMock.registerContextProvider.mock.calls[0][0].context$)
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"branch": "branch",
|
||||
"buildNum": 100,
|
||||
"buildSha": "buildSha",
|
||||
"isDev": true,
|
||||
"isDistributable": false,
|
||||
"version": "version",
|
||||
}
|
||||
`);
|
||||
expect(
|
||||
await firstValueFrom(analyticsClientMock.registerContextProvider.mock.calls[1][0].context$)
|
||||
).toEqual({ session_id: expect.any(String) });
|
||||
expect(
|
||||
await firstValueFrom(analyticsClientMock.registerContextProvider.mock.calls[2][0].context$)
|
||||
).toEqual({
|
||||
preferred_language: 'en-US',
|
||||
preferred_languages: ['en-US', 'en'],
|
||||
user_agent: expect.any(String),
|
||||
});
|
||||
});
|
||||
|
||||
test('should register the `performance_metric` and `click` event types on creation', () => {
|
||||
expect(analyticsClientMock.registerEventType).toHaveBeenCalledTimes(2);
|
||||
expect(analyticsClientMock.registerEventType.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"eventType": "performance_metric",
|
||||
"schema": Object {
|
||||
"duration": Object {
|
||||
"_meta": Object {
|
||||
"description": "The main event duration in ms",
|
||||
},
|
||||
"type": "integer",
|
||||
},
|
||||
"eventName": Object {
|
||||
"_meta": Object {
|
||||
"description": "The name of the event that is tracked in the metrics i.e. kibana_loaded, kibana_started",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"key1": Object {
|
||||
"_meta": Object {
|
||||
"description": "Performance metric label 1",
|
||||
"optional": true,
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"key2": Object {
|
||||
"_meta": Object {
|
||||
"description": "Performance metric label 2",
|
||||
"optional": true,
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"key3": Object {
|
||||
"_meta": Object {
|
||||
"description": "Performance metric label 3",
|
||||
"optional": true,
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"key4": Object {
|
||||
"_meta": Object {
|
||||
"description": "Performance metric label 4",
|
||||
"optional": true,
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"key5": Object {
|
||||
"_meta": Object {
|
||||
"description": "Performance metric label 5",
|
||||
"optional": true,
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"meta": Object {
|
||||
"_meta": Object {
|
||||
"description": "Meta data that is searchable but not aggregatable",
|
||||
"optional": true,
|
||||
},
|
||||
"type": "pass_through",
|
||||
},
|
||||
"value1": Object {
|
||||
"_meta": Object {
|
||||
"description": "Performance metric value 1",
|
||||
"optional": true,
|
||||
},
|
||||
"type": "long",
|
||||
},
|
||||
"value2": Object {
|
||||
"_meta": Object {
|
||||
"description": "Performance metric value 2",
|
||||
"optional": true,
|
||||
},
|
||||
"type": "long",
|
||||
},
|
||||
"value3": Object {
|
||||
"_meta": Object {
|
||||
"description": "Performance metric value 3",
|
||||
"optional": true,
|
||||
},
|
||||
"type": "long",
|
||||
},
|
||||
"value4": Object {
|
||||
"_meta": Object {
|
||||
"description": "Performance metric value 4",
|
||||
"optional": true,
|
||||
},
|
||||
"type": "long",
|
||||
},
|
||||
"value5": Object {
|
||||
"_meta": Object {
|
||||
"description": "Performance metric value 5",
|
||||
"optional": true,
|
||||
},
|
||||
"type": "long",
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
`);
|
||||
expect(analyticsClientMock.registerEventType.mock.calls[1]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"eventType": "click",
|
||||
"schema": Object {
|
||||
"target": Object {
|
||||
"items": Object {
|
||||
"_meta": Object {
|
||||
"description": "The attributes of the clicked element and all its parents in the form \`{attr.name}={attr.value}\`. It allows finding the clicked elements by looking up its attributes like \\"data-test-subj=my-button\\".",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"type": "array",
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('setup should expose all the register APIs, reportEvent and opt-in', () => {
|
||||
const injectedMetadata = injectedMetadataServiceMock.createSetupContract();
|
||||
expect(analyticsService.setup({ injectedMetadata })).toStrictEqual({
|
||||
|
@ -60,9 +180,9 @@ describe('AnalyticsService', () => {
|
|||
test('setup should register the elasticsearch info context provider (undefined)', async () => {
|
||||
const injectedMetadata = injectedMetadataServiceMock.createSetupContract();
|
||||
analyticsService.setup({ injectedMetadata });
|
||||
await expect(
|
||||
firstValueFrom(analyticsClientMock.registerContextProvider.mock.calls[3][0].context$)
|
||||
).resolves.toMatchInlineSnapshot(`undefined`);
|
||||
expect(
|
||||
await firstValueFrom(analyticsClientMock.registerContextProvider.mock.calls[3][0].context$)
|
||||
).toMatchInlineSnapshot(`undefined`);
|
||||
});
|
||||
|
||||
test('setup should register the elasticsearch info context provider (with info)', async () => {
|
||||
|
@ -73,15 +193,15 @@ describe('AnalyticsService', () => {
|
|||
cluster_version: 'version',
|
||||
});
|
||||
analyticsService.setup({ injectedMetadata });
|
||||
await expect(
|
||||
firstValueFrom(analyticsClientMock.registerContextProvider.mock.calls[3][0].context$)
|
||||
).resolves.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"cluster_name": "cluster_name",
|
||||
"cluster_uuid": "cluster_uuid",
|
||||
"cluster_version": "version",
|
||||
}
|
||||
`);
|
||||
expect(
|
||||
await firstValueFrom(analyticsClientMock.registerContextProvider.mock.calls[3][0].context$)
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"cluster_name": "cluster_name",
|
||||
"cluster_uuid": "cluster_uuid",
|
||||
"cluster_version": "version",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('setup should expose only the APIs report and opt-in', () => {
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
import { of } from 'rxjs';
|
||||
import type { AnalyticsClient } from '@kbn/analytics-client';
|
||||
import { createAnalytics } from '@kbn/analytics-client';
|
||||
import { registerPerformanceMetricEventType } from '@kbn/ebt-tools';
|
||||
import type { CoreContext } from '@kbn/core-base-browser-internal';
|
||||
import type { InternalInjectedMetadataSetup } from '@kbn/core-injected-metadata-browser-internal';
|
||||
import type { AnalyticsServiceSetup, AnalyticsServiceStart } from '@kbn/core-analytics-browser';
|
||||
|
@ -34,6 +35,7 @@ export class AnalyticsService {
|
|||
});
|
||||
|
||||
this.registerBuildInfoAnalyticsContext(core);
|
||||
registerPerformanceMetricEventType(this.analyticsClient);
|
||||
|
||||
// We may eventually move the following to the client's package since they are not Kibana-specific
|
||||
// and can benefit other consumers of the client.
|
||||
|
|
|
@ -28,6 +28,7 @@ NPM_MODULE_EXTRA_FILES = [
|
|||
RUNTIME_DEPS = [
|
||||
"@npm//rxjs",
|
||||
"//packages/analytics/client",
|
||||
"//packages/kbn-ebt-tools",
|
||||
]
|
||||
|
||||
TYPES_DEPS = [
|
||||
|
@ -35,6 +36,7 @@ TYPES_DEPS = [
|
|||
"@npm//@types/jest",
|
||||
"@npm//rxjs",
|
||||
"//packages/analytics/client:npm_module_types",
|
||||
"//packages/kbn-ebt-tools:npm_module_types",
|
||||
"//packages/core/base/core-base-server-internal:npm_module_types",
|
||||
"//packages/core/analytics/core-analytics-server:npm_module_types",
|
||||
]
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { AnalyticsClient } from '@kbn/analytics-client';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
export const analyticsClientMock: jest.Mocked<AnalyticsClient> = {
|
||||
optIn: jest.fn(),
|
||||
reportEvent: jest.fn(),
|
||||
registerEventType: jest.fn(),
|
||||
registerContextProvider: jest.fn(),
|
||||
removeContextProvider: jest.fn(),
|
||||
registerShipper: jest.fn(),
|
||||
telemetryCounter$: new Subject(),
|
||||
shutdown: jest.fn(),
|
||||
};
|
||||
|
||||
jest.doMock('@kbn/analytics-client', () => ({
|
||||
createAnalytics: () => analyticsClientMock,
|
||||
}));
|
|
@ -0,0 +1,158 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { firstValueFrom, Observable } from 'rxjs';
|
||||
import { mockCoreContext } from '@kbn/core-base-server-mocks';
|
||||
import { analyticsClientMock } from './analytics_service.test.mocks';
|
||||
import { AnalyticsService } from './analytics_service';
|
||||
|
||||
describe('AnalyticsService', () => {
|
||||
let analyticsService: AnalyticsService;
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
analyticsService = new AnalyticsService(mockCoreContext.create());
|
||||
});
|
||||
|
||||
test('should register the context provider `build info` on creation', async () => {
|
||||
expect(analyticsClientMock.registerContextProvider).toHaveBeenCalledTimes(1);
|
||||
await expect(
|
||||
await firstValueFrom(analyticsClientMock.registerContextProvider.mock.calls[0][0].context$)
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"branch": "main",
|
||||
"buildNum": 9007199254740991,
|
||||
"buildSha": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
|
||||
"isDev": true,
|
||||
"isDistributable": false,
|
||||
"version": "8.5.0",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('should register the `performance_metric` event type on creation', () => {
|
||||
expect(analyticsClientMock.registerEventType).toHaveBeenCalledTimes(1);
|
||||
expect(analyticsClientMock.registerEventType.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"eventType": "performance_metric",
|
||||
"schema": Object {
|
||||
"duration": Object {
|
||||
"_meta": Object {
|
||||
"description": "The main event duration in ms",
|
||||
},
|
||||
"type": "integer",
|
||||
},
|
||||
"eventName": Object {
|
||||
"_meta": Object {
|
||||
"description": "The name of the event that is tracked in the metrics i.e. kibana_loaded, kibana_started",
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"key1": Object {
|
||||
"_meta": Object {
|
||||
"description": "Performance metric label 1",
|
||||
"optional": true,
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"key2": Object {
|
||||
"_meta": Object {
|
||||
"description": "Performance metric label 2",
|
||||
"optional": true,
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"key3": Object {
|
||||
"_meta": Object {
|
||||
"description": "Performance metric label 3",
|
||||
"optional": true,
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"key4": Object {
|
||||
"_meta": Object {
|
||||
"description": "Performance metric label 4",
|
||||
"optional": true,
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"key5": Object {
|
||||
"_meta": Object {
|
||||
"description": "Performance metric label 5",
|
||||
"optional": true,
|
||||
},
|
||||
"type": "keyword",
|
||||
},
|
||||
"meta": Object {
|
||||
"_meta": Object {
|
||||
"description": "Meta data that is searchable but not aggregatable",
|
||||
"optional": true,
|
||||
},
|
||||
"type": "pass_through",
|
||||
},
|
||||
"value1": Object {
|
||||
"_meta": Object {
|
||||
"description": "Performance metric value 1",
|
||||
"optional": true,
|
||||
},
|
||||
"type": "long",
|
||||
},
|
||||
"value2": Object {
|
||||
"_meta": Object {
|
||||
"description": "Performance metric value 2",
|
||||
"optional": true,
|
||||
},
|
||||
"type": "long",
|
||||
},
|
||||
"value3": Object {
|
||||
"_meta": Object {
|
||||
"description": "Performance metric value 3",
|
||||
"optional": true,
|
||||
},
|
||||
"type": "long",
|
||||
},
|
||||
"value4": Object {
|
||||
"_meta": Object {
|
||||
"description": "Performance metric value 4",
|
||||
"optional": true,
|
||||
},
|
||||
"type": "long",
|
||||
},
|
||||
"value5": Object {
|
||||
"_meta": Object {
|
||||
"description": "Performance metric value 5",
|
||||
"optional": true,
|
||||
},
|
||||
"type": "long",
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('setup should expose all the register APIs, reportEvent and opt-in', () => {
|
||||
expect(analyticsService.setup()).toStrictEqual({
|
||||
registerShipper: expect.any(Function),
|
||||
registerContextProvider: expect.any(Function),
|
||||
removeContextProvider: expect.any(Function),
|
||||
registerEventType: expect.any(Function),
|
||||
reportEvent: expect.any(Function),
|
||||
optIn: expect.any(Function),
|
||||
telemetryCounter$: expect.any(Observable),
|
||||
});
|
||||
});
|
||||
|
||||
test('setup should expose only the APIs report and opt-in', () => {
|
||||
expect(analyticsService.start()).toStrictEqual({
|
||||
reportEvent: expect.any(Function),
|
||||
optIn: expect.any(Function),
|
||||
telemetryCounter$: expect.any(Observable),
|
||||
});
|
||||
});
|
||||
});
|
|
@ -9,6 +9,7 @@
|
|||
import { of } from 'rxjs';
|
||||
import type { AnalyticsClient } from '@kbn/analytics-client';
|
||||
import { createAnalytics } from '@kbn/analytics-client';
|
||||
import { registerPerformanceMetricEventType } from '@kbn/ebt-tools';
|
||||
import type { CoreContext } from '@kbn/core-base-server-internal';
|
||||
import type {
|
||||
AnalyticsServiceSetup,
|
||||
|
@ -29,6 +30,7 @@ export class AnalyticsService {
|
|||
});
|
||||
|
||||
this.registerBuildInfoAnalyticsContext(core);
|
||||
registerPerformanceMetricEventType(this.analyticsClient);
|
||||
}
|
||||
|
||||
public preboot(): AnalyticsServicePreboot {
|
||||
|
|
101
packages/kbn-ebt-tools/BUILD.bazel
Normal file
101
packages/kbn-ebt-tools/BUILD.bazel
Normal file
|
@ -0,0 +1,101 @@
|
|||
load("@npm//@bazel/typescript:index.bzl", "ts_config")
|
||||
load("@build_bazel_rules_nodejs//:index.bzl", "js_library")
|
||||
load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project")
|
||||
|
||||
PKG_BASE_NAME = "kbn-ebt-tools"
|
||||
PKG_REQUIRE_NAME = "@kbn/ebt-tools"
|
||||
|
||||
SOURCE_FILES = glob(
|
||||
[
|
||||
"src/**/*.ts",
|
||||
],
|
||||
exclude = ["**/*.test.*"],
|
||||
)
|
||||
|
||||
SRCS = SOURCE_FILES
|
||||
|
||||
filegroup(
|
||||
name = "srcs",
|
||||
srcs = SRCS,
|
||||
)
|
||||
|
||||
NPM_MODULE_EXTRA_FILES = [
|
||||
"package.json",
|
||||
"README.md"
|
||||
]
|
||||
|
||||
RUNTIME_DEPS = []
|
||||
|
||||
TYPES_DEPS = [
|
||||
"//packages/analytics/client:npm_module_types",
|
||||
"@npm//@types/jest",
|
||||
"@npm//@types/node",
|
||||
]
|
||||
|
||||
jsts_transpiler(
|
||||
name = "target_node",
|
||||
srcs = SRCS,
|
||||
build_pkg_name = package_name(),
|
||||
)
|
||||
|
||||
ts_config(
|
||||
name = "tsconfig",
|
||||
src = "tsconfig.json",
|
||||
deps = [
|
||||
"//:tsconfig.base.json",
|
||||
"//:tsconfig.bazel.json",
|
||||
],
|
||||
)
|
||||
|
||||
ts_project(
|
||||
name = "tsc_types",
|
||||
args = ['--pretty'],
|
||||
srcs = SRCS,
|
||||
deps = TYPES_DEPS,
|
||||
declaration = True,
|
||||
declaration_map = True,
|
||||
emit_declaration_only = True,
|
||||
out_dir = "target_types",
|
||||
root_dir = "src",
|
||||
tsconfig = ":tsconfig",
|
||||
)
|
||||
|
||||
js_library(
|
||||
name = PKG_BASE_NAME,
|
||||
srcs = NPM_MODULE_EXTRA_FILES,
|
||||
deps = RUNTIME_DEPS + [":target_node", ":tsc_types"],
|
||||
package_name = PKG_REQUIRE_NAME,
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
pkg_npm(
|
||||
name = "npm_module",
|
||||
deps = [
|
||||
":%s" % PKG_BASE_NAME,
|
||||
]
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "build",
|
||||
srcs = [
|
||||
":npm_module",
|
||||
],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
pkg_npm_types(
|
||||
name = "npm_module_types",
|
||||
srcs = SRCS,
|
||||
deps = [":tsc_types"],
|
||||
package_name = PKG_REQUIRE_NAME,
|
||||
tsconfig = ":tsconfig",
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "build_types",
|
||||
srcs = [
|
||||
":npm_module_types",
|
||||
],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
3
packages/kbn-ebt-tools/README.md
Normal file
3
packages/kbn-ebt-tools/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# @kbn/ebt-tools
|
||||
|
||||
Shared tools for event based telemetry
|
13
packages/kbn-ebt-tools/jest.config.js
Normal file
13
packages/kbn-ebt-tools/jest.config.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test',
|
||||
rootDir: '../..',
|
||||
roots: ['<rootDir>/packages/kbn-ebt-tools'],
|
||||
};
|
8
packages/kbn-ebt-tools/package.json
Normal file
8
packages/kbn-ebt-tools/package.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"name": "@kbn/ebt-tools",
|
||||
"version": "1.0.0",
|
||||
"license": "SSPL-1.0 OR Elastic License 2.0",
|
||||
"browser": "./target_web/index.js",
|
||||
"main": "./target_node/index.js",
|
||||
"private": true
|
||||
}
|
9
packages/kbn-ebt-tools/src/index.ts
Normal file
9
packages/kbn-ebt-tools/src/index.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export * from './performance_metric_events';
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { createAnalytics, type AnalyticsClient } from '@kbn/analytics-client';
|
||||
import { loggerMock } from '@kbn/logging-mocks';
|
||||
import { registerPerformanceMetricEventType, reportPerformanceMetricEvent } from './helpers';
|
||||
import { METRIC_EVENT_SCHEMA } from './schema';
|
||||
|
||||
describe('performance metric event helpers', () => {
|
||||
let analyticsClient: AnalyticsClient;
|
||||
|
||||
describe('registerPerformanceMetricEventType', () => {
|
||||
beforeEach(() => {
|
||||
analyticsClient = createAnalytics({
|
||||
isDev: true, // Explicitly setting `true` to ensure we have event validation to make sure the events sent pass our validation.
|
||||
sendTo: 'staging',
|
||||
logger: loggerMock.create(),
|
||||
});
|
||||
});
|
||||
|
||||
test('registers the `performance_metric` eventType to the analytics client', () => {
|
||||
const registerEventTypeSpy = jest.spyOn(analyticsClient, 'registerEventType');
|
||||
|
||||
expect(() => registerPerformanceMetricEventType(analyticsClient)).not.toThrow();
|
||||
|
||||
expect(registerEventTypeSpy).toHaveBeenCalledWith({
|
||||
eventType: 'performance_metric',
|
||||
schema: METRIC_EVENT_SCHEMA,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('reportPerformanceMetricEvent', () => {
|
||||
beforeEach(() => {
|
||||
analyticsClient = createAnalytics({
|
||||
isDev: true, // Explicitly setting `true` to ensure we have event validation to make sure the events sent pass our validation.
|
||||
sendTo: 'staging',
|
||||
logger: loggerMock.create(),
|
||||
});
|
||||
registerPerformanceMetricEventType(analyticsClient);
|
||||
});
|
||||
|
||||
test('reports the minimum allowed event', () => {
|
||||
reportPerformanceMetricEvent(analyticsClient, { eventName: 'test-event', duration: 1000 });
|
||||
});
|
||||
|
||||
test('reports all the allowed fields in the event', () => {
|
||||
reportPerformanceMetricEvent(analyticsClient, {
|
||||
eventName: 'test-event',
|
||||
meta: { my: { custom: { fields: 'here' } }, another_field: true, status: 'something' },
|
||||
duration: 10,
|
||||
key1: 'something',
|
||||
value1: 10,
|
||||
key2: 'something',
|
||||
value2: 10,
|
||||
key3: 'something',
|
||||
value3: 10,
|
||||
key4: 'something',
|
||||
value4: 10,
|
||||
key5: 'something',
|
||||
value5: 10,
|
||||
});
|
||||
});
|
||||
|
||||
test('should fail if eventName and duration is missing', () => {
|
||||
expect(() =>
|
||||
reportPerformanceMetricEvent(
|
||||
analyticsClient,
|
||||
// @ts-expect-error
|
||||
{}
|
||||
)
|
||||
).toThrowErrorMatchingInlineSnapshot(`
|
||||
"Failed to validate payload coming from \\"Event Type 'performance_metric'\\":
|
||||
- [eventName]: {\\"expected\\":\\"string\\",\\"actual\\":\\"undefined\\",\\"value\\":\\"undefined\\"}
|
||||
- [duration]: {\\"expected\\":\\"number\\",\\"actual\\":\\"undefined\\",\\"value\\":\\"undefined\\"}"
|
||||
`);
|
||||
});
|
||||
|
||||
test('should fail if any additional unknown keys are added', () => {
|
||||
expect(() =>
|
||||
reportPerformanceMetricEvent(analyticsClient, {
|
||||
eventName: 'test-event',
|
||||
duration: 1000,
|
||||
// @ts-expect-error
|
||||
an_unknown_field: 'blah',
|
||||
})
|
||||
).toThrowErrorMatchingInlineSnapshot(`
|
||||
"Failed to validate payload coming from \\"Event Type 'performance_metric'\\":
|
||||
- []: excess key 'an_unknown_field' found"
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { AnalyticsClient } from '@kbn/analytics-client';
|
||||
import { type PerformanceMetricEvent, METRIC_EVENT_SCHEMA } from './schema';
|
||||
|
||||
const PERFORMANCE_METRIC_EVENT_TYPE = 'performance_metric';
|
||||
|
||||
/**
|
||||
* Register the `performance_metric` event type
|
||||
* @param analytics The {@link AnalyticsClient} during the setup phase (it has the method `registerEventType`)
|
||||
* @private To be called only by core's Analytics Service
|
||||
*/
|
||||
export function registerPerformanceMetricEventType(
|
||||
analytics: Pick<AnalyticsClient, 'registerEventType'>
|
||||
) {
|
||||
analytics.registerEventType<PerformanceMetricEvent>({
|
||||
eventType: PERFORMANCE_METRIC_EVENT_TYPE,
|
||||
schema: METRIC_EVENT_SCHEMA,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Report a `performance_metric` event type.
|
||||
* @param analytics The {@link AnalyticsClient} to report the events.
|
||||
* @param eventData The data to send, conforming the structure of a {@link MetricEvent}.
|
||||
*/
|
||||
export function reportPerformanceMetricEvent(
|
||||
analytics: Pick<AnalyticsClient, 'reportEvent'>,
|
||||
eventData: PerformanceMetricEvent
|
||||
) {
|
||||
analytics.reportEvent(PERFORMANCE_METRIC_EVENT_TYPE, eventData);
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
export type { PerformanceMetricEvent as MetricEvent } from './schema';
|
||||
export {
|
||||
registerPerformanceMetricEventType as registerPerformanceMetricEventType,
|
||||
reportPerformanceMetricEvent,
|
||||
} from './helpers';
|
138
packages/kbn-ebt-tools/src/performance_metric_events/schema.ts
Normal file
138
packages/kbn-ebt-tools/src/performance_metric_events/schema.ts
Normal file
|
@ -0,0 +1,138 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { RootSchema } from '@kbn/analytics-client';
|
||||
|
||||
/**
|
||||
* Structure of the `metric` event
|
||||
*/
|
||||
export interface PerformanceMetricEvent {
|
||||
/**
|
||||
* The name of the event that is tracked in the metrics i.e. kibana_loaded, kibana_started
|
||||
*/
|
||||
eventName: string;
|
||||
/**
|
||||
* Searchable but not aggregateable metadata relevant to the tracked action.
|
||||
*/
|
||||
meta?: Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* @group Standardized fields
|
||||
* The time (in milliseconds) it took to run the entire action.
|
||||
*/
|
||||
duration: number;
|
||||
|
||||
/**
|
||||
* @group Free fields for custom metrics (searchable and aggregateable)
|
||||
* Description label for the metric 1
|
||||
*/
|
||||
key1?: string;
|
||||
/**
|
||||
* @group Free fields for custom metrics (searchable and aggregateable)
|
||||
* Value for the metric 1
|
||||
*/
|
||||
value1?: number;
|
||||
/**
|
||||
* @group Free fields for custom metrics (searchable and aggregateable)
|
||||
* Description label for the metric 2
|
||||
*/
|
||||
key2?: string;
|
||||
/**
|
||||
* @group Free fields for custom metrics (searchable and aggregateable)
|
||||
* Value for the metric 2
|
||||
*/
|
||||
value2?: number;
|
||||
/**
|
||||
* @group Free fields for custom metrics (searchable and aggregateable)
|
||||
* Description label for the metric 3
|
||||
*/
|
||||
key3?: string;
|
||||
/**
|
||||
* @group Free fields for custom metrics (searchable and aggregateable)
|
||||
* Value for the metric 3
|
||||
*/
|
||||
value3?: number;
|
||||
/**
|
||||
* @group Free fields for custom metrics (searchable and aggregateable)
|
||||
* Description label for the metric 4
|
||||
*/
|
||||
key4?: string;
|
||||
/**
|
||||
* @group Free fields for custom metrics (searchable and aggregateable)
|
||||
* Value for the metric 4
|
||||
*/
|
||||
value4?: number;
|
||||
/**
|
||||
* @group Free fields for custom metrics (searchable and aggregateable)
|
||||
* Description label for the metric 5
|
||||
*/
|
||||
key5?: string;
|
||||
/**
|
||||
* @group Free fields for custom metrics (searchable and aggregateable)
|
||||
* Value for the metric 5
|
||||
*/
|
||||
value5?: number;
|
||||
}
|
||||
|
||||
export const METRIC_EVENT_SCHEMA: RootSchema<PerformanceMetricEvent> = {
|
||||
eventName: {
|
||||
type: 'keyword',
|
||||
_meta: {
|
||||
description:
|
||||
'The name of the event that is tracked in the metrics i.e. kibana_loaded, kibana_started',
|
||||
},
|
||||
},
|
||||
meta: {
|
||||
type: 'pass_through',
|
||||
_meta: { description: 'Meta data that is searchable but not aggregatable', optional: true },
|
||||
},
|
||||
duration: {
|
||||
type: 'integer',
|
||||
_meta: { description: 'The main event duration in ms' },
|
||||
},
|
||||
key1: {
|
||||
type: 'keyword',
|
||||
_meta: { description: 'Performance metric label 1', optional: true },
|
||||
},
|
||||
value1: {
|
||||
type: 'long',
|
||||
_meta: { description: 'Performance metric value 1', optional: true },
|
||||
},
|
||||
key2: {
|
||||
type: 'keyword',
|
||||
_meta: { description: 'Performance metric label 2', optional: true },
|
||||
},
|
||||
value2: {
|
||||
type: 'long',
|
||||
_meta: { description: 'Performance metric value 2', optional: true },
|
||||
},
|
||||
key3: {
|
||||
type: 'keyword',
|
||||
_meta: { description: 'Performance metric label 3', optional: true },
|
||||
},
|
||||
value3: {
|
||||
type: 'long',
|
||||
_meta: { description: 'Performance metric value 3', optional: true },
|
||||
},
|
||||
key4: {
|
||||
type: 'keyword',
|
||||
_meta: { description: 'Performance metric label 4', optional: true },
|
||||
},
|
||||
value4: {
|
||||
type: 'long',
|
||||
_meta: { description: 'Performance metric value 4', optional: true },
|
||||
},
|
||||
key5: {
|
||||
type: 'keyword',
|
||||
_meta: { description: 'Performance metric label 5', optional: true },
|
||||
},
|
||||
value5: {
|
||||
type: 'long',
|
||||
_meta: { description: 'Performance metric value 5', optional: true },
|
||||
},
|
||||
};
|
16
packages/kbn-ebt-tools/tsconfig.json
Normal file
16
packages/kbn-ebt-tools/tsconfig.json
Normal file
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"extends": "../../tsconfig.bazel.json",
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"outDir": "target_types",
|
||||
"types": [
|
||||
"jest",
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
]
|
||||
}
|
|
@ -41,6 +41,15 @@ import {
|
|||
} from './core_system.test.mocks';
|
||||
|
||||
import { CoreSystem } from './core_system';
|
||||
import {
|
||||
KIBANA_LOADED_EVENT,
|
||||
LOAD_START,
|
||||
LOAD_BOOTSTRAP_START,
|
||||
LOAD_CORE_CREATED,
|
||||
LOAD_FIRST_NAV,
|
||||
LOAD_SETUP_DONE,
|
||||
LOAD_START_DONE,
|
||||
} from './events';
|
||||
|
||||
jest.spyOn(CoreSystem.prototype, 'stop');
|
||||
|
||||
|
@ -75,12 +84,28 @@ beforeEach(() => {
|
|||
window.performance.clearMarks = jest.fn();
|
||||
window.performance.getEntriesByName = jest.fn().mockReturnValue([
|
||||
{
|
||||
detail: 'load_started',
|
||||
startTime: 456,
|
||||
detail: LOAD_START,
|
||||
startTime: 111,
|
||||
},
|
||||
{
|
||||
detail: 'bootstrap_started',
|
||||
startTime: 123,
|
||||
detail: LOAD_BOOTSTRAP_START,
|
||||
startTime: 222,
|
||||
},
|
||||
{
|
||||
detail: LOAD_CORE_CREATED,
|
||||
startTime: 333,
|
||||
},
|
||||
{
|
||||
detail: LOAD_SETUP_DONE,
|
||||
startTime: 444,
|
||||
},
|
||||
{
|
||||
detail: LOAD_START_DONE,
|
||||
startTime: 555,
|
||||
},
|
||||
{
|
||||
detail: LOAD_FIRST_NAV,
|
||||
startTime: 666,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
@ -248,38 +273,71 @@ describe('#start()', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('reports the event Loaded Kibana and clears marks', async () => {
|
||||
it('reports the deprecated event Loaded Kibana', async () => {
|
||||
await startCore();
|
||||
expect(analyticsServiceStartMock.reportEvent).toHaveBeenCalledTimes(1);
|
||||
expect(analyticsServiceStartMock.reportEvent).toHaveBeenCalledWith('Loaded Kibana', {
|
||||
expect(analyticsServiceStartMock.reportEvent).toHaveBeenCalledTimes(2);
|
||||
expect(analyticsServiceStartMock.reportEvent).toHaveBeenNthCalledWith(1, 'Loaded Kibana', {
|
||||
kibana_version: '1.2.3',
|
||||
load_started: 456,
|
||||
bootstrap_started: 123,
|
||||
protocol: 'http:',
|
||||
});
|
||||
|
||||
expect(window.performance.clearMarks).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('reports the event Loaded Kibana (with memory)', async () => {
|
||||
fetchOptionalMemoryInfoMock.mockReturnValue({
|
||||
load_started: 456,
|
||||
bootstrap_started: 123,
|
||||
memory_js_heap_size_limit: 3,
|
||||
memory_js_heap_size_total: 2,
|
||||
memory_js_heap_size_used: 1,
|
||||
it('reports the metric event kibana-loaded and clears marks', async () => {
|
||||
await startCore();
|
||||
expect(analyticsServiceStartMock.reportEvent).toHaveBeenCalledTimes(2);
|
||||
expect(analyticsServiceStartMock.reportEvent).toHaveBeenNthCalledWith(2, 'performance_metric', {
|
||||
eventName: KIBANA_LOADED_EVENT,
|
||||
meta: {
|
||||
kibana_version: '1.2.3',
|
||||
protocol: 'http:',
|
||||
},
|
||||
key1: LOAD_START,
|
||||
key2: LOAD_BOOTSTRAP_START,
|
||||
key3: LOAD_CORE_CREATED,
|
||||
key4: LOAD_SETUP_DONE,
|
||||
key5: LOAD_START_DONE,
|
||||
value1: 111,
|
||||
value2: 222,
|
||||
value3: 333,
|
||||
value4: 444,
|
||||
value5: 555,
|
||||
duration: 666,
|
||||
});
|
||||
|
||||
expect(window.performance.clearMarks).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('reports the event kibana-loaded (with memory)', async () => {
|
||||
const performanceMemory = {
|
||||
usedJSHeapSize: 1,
|
||||
jsHeapSizeLimit: 3,
|
||||
totalJSHeapSize: 4,
|
||||
};
|
||||
fetchOptionalMemoryInfoMock.mockReturnValue(performanceMemory);
|
||||
|
||||
await startCore();
|
||||
expect(analyticsServiceStartMock.reportEvent).toHaveBeenCalledTimes(1);
|
||||
expect(analyticsServiceStartMock.reportEvent).toHaveBeenCalledWith('Loaded Kibana', {
|
||||
load_started: 456,
|
||||
bootstrap_started: 123,
|
||||
kibana_version: '1.2.3',
|
||||
memory_js_heap_size_limit: 3,
|
||||
memory_js_heap_size_total: 2,
|
||||
memory_js_heap_size_used: 1,
|
||||
protocol: 'http:',
|
||||
|
||||
expect(analyticsServiceStartMock.reportEvent).toHaveBeenCalledTimes(2);
|
||||
expect(analyticsServiceStartMock.reportEvent).toHaveBeenNthCalledWith(2, 'performance_metric', {
|
||||
eventName: KIBANA_LOADED_EVENT,
|
||||
meta: {
|
||||
kibana_version: '1.2.3',
|
||||
protocol: 'http:',
|
||||
...performanceMemory,
|
||||
},
|
||||
key1: LOAD_START,
|
||||
key2: LOAD_BOOTSTRAP_START,
|
||||
key3: LOAD_CORE_CREATED,
|
||||
key4: LOAD_SETUP_DONE,
|
||||
key5: LOAD_START_DONE,
|
||||
value1: 111,
|
||||
value2: 222,
|
||||
value3: 333,
|
||||
value4: 444,
|
||||
value5: 555,
|
||||
duration: 666,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -26,9 +26,11 @@ import { HttpService } from '@kbn/core-http-browser-internal';
|
|||
import { UiSettingsService } from '@kbn/core-ui-settings-browser-internal';
|
||||
import { DeprecationsService } from '@kbn/core-deprecations-browser-internal';
|
||||
import { IntegrationsService } from '@kbn/core-integrations-browser-internal';
|
||||
import { reportPerformanceMetricEvent } from '@kbn/ebt-tools';
|
||||
import { OverlayService } from '@kbn/core-overlays-browser-internal';
|
||||
import { KBN_LOAD_MARKS } from '@kbn/core-mount-utils-browser-internal';
|
||||
import { NotificationsService } from '@kbn/core-notifications-browser-internal';
|
||||
import { fetchOptionalMemoryInfo } from './fetch_optional_memory_info';
|
||||
import { CoreSetup, CoreStart } from '.';
|
||||
import { ChromeService } from './chrome';
|
||||
import { PluginsService } from './plugins';
|
||||
|
@ -37,7 +39,16 @@ import { RenderingService } from './rendering';
|
|||
import { SavedObjectsService } from './saved_objects';
|
||||
import { CoreApp } from './core_app';
|
||||
import type { InternalApplicationSetup, InternalApplicationStart } from './application/types';
|
||||
import { fetchOptionalMemoryInfo } from './fetch_optional_memory_info';
|
||||
|
||||
import {
|
||||
LOAD_SETUP_DONE,
|
||||
LOAD_START_DONE,
|
||||
KIBANA_LOADED_EVENT,
|
||||
LOAD_CORE_CREATED,
|
||||
LOAD_FIRST_NAV,
|
||||
LOAD_BOOTSTRAP_START,
|
||||
LOAD_START,
|
||||
} from './events';
|
||||
|
||||
interface Params {
|
||||
rootDomElement: HTMLElement;
|
||||
|
@ -129,12 +140,12 @@ export class CoreSystem {
|
|||
this.coreApp = new CoreApp(this.coreContext);
|
||||
|
||||
performance.mark(KBN_LOAD_MARKS, {
|
||||
detail: 'core_created',
|
||||
detail: LOAD_CORE_CREATED,
|
||||
});
|
||||
}
|
||||
|
||||
private getLoadMarksInfo() {
|
||||
if (!performance) return [];
|
||||
private getLoadMarksInfo(): Record<string, number> {
|
||||
if (!performance) return {};
|
||||
const reportData: Record<string, number> = {};
|
||||
const marks = performance.getEntriesByName(KBN_LOAD_MARKS);
|
||||
for (const mark of marks) {
|
||||
|
@ -145,11 +156,33 @@ export class CoreSystem {
|
|||
}
|
||||
|
||||
private reportKibanaLoadedEvent(analytics: AnalyticsServiceStart) {
|
||||
/**
|
||||
* @deprecated here for backwards compatibility in FullStory
|
||||
**/
|
||||
analytics.reportEvent('Loaded Kibana', {
|
||||
kibana_version: this.coreContext.env.packageInfo.version,
|
||||
protocol: window.location.protocol,
|
||||
...fetchOptionalMemoryInfo(),
|
||||
...this.getLoadMarksInfo(),
|
||||
});
|
||||
|
||||
const timing = this.getLoadMarksInfo();
|
||||
reportPerformanceMetricEvent(analytics, {
|
||||
eventName: KIBANA_LOADED_EVENT,
|
||||
meta: {
|
||||
kibana_version: this.coreContext.env.packageInfo.version,
|
||||
protocol: window.location.protocol,
|
||||
...fetchOptionalMemoryInfo(),
|
||||
},
|
||||
duration: timing[LOAD_FIRST_NAV],
|
||||
key1: LOAD_START,
|
||||
value1: timing[LOAD_START],
|
||||
key2: LOAD_BOOTSTRAP_START,
|
||||
value2: timing[LOAD_BOOTSTRAP_START],
|
||||
key3: LOAD_CORE_CREATED,
|
||||
value3: timing[LOAD_CORE_CREATED],
|
||||
key4: LOAD_SETUP_DONE,
|
||||
value4: timing[LOAD_SETUP_DONE],
|
||||
key5: LOAD_START_DONE,
|
||||
value5: timing[LOAD_START_DONE],
|
||||
});
|
||||
performance.clearMarks(KBN_LOAD_MARKS);
|
||||
}
|
||||
|
@ -170,6 +203,7 @@ export class CoreSystem {
|
|||
this.docLinks.setup();
|
||||
|
||||
const analytics = this.analytics.setup({ injectedMetadata });
|
||||
|
||||
this.registerLoadedKibanaEventType(analytics);
|
||||
|
||||
const executionContext = this.executionContext.setup({ analytics });
|
||||
|
@ -200,7 +234,7 @@ export class CoreSystem {
|
|||
await this.plugins.setup(core);
|
||||
|
||||
performance.mark(KBN_LOAD_MARKS, {
|
||||
detail: 'setup_done',
|
||||
detail: LOAD_SETUP_DONE,
|
||||
});
|
||||
|
||||
return { fatalErrors: this.fatalErrorsSetup };
|
||||
|
@ -300,13 +334,13 @@ export class CoreSystem {
|
|||
});
|
||||
|
||||
performance.mark(KBN_LOAD_MARKS, {
|
||||
detail: 'start_done',
|
||||
detail: LOAD_START_DONE,
|
||||
});
|
||||
|
||||
// Wait for the first app navigation to report Kibana Loaded
|
||||
firstValueFrom(application.currentAppId$.pipe(filter(Boolean))).then(() => {
|
||||
performance.mark(KBN_LOAD_MARKS, {
|
||||
detail: 'first_app_nav',
|
||||
detail: LOAD_FIRST_NAV,
|
||||
});
|
||||
this.reportKibanaLoadedEvent(analytics);
|
||||
});
|
||||
|
@ -342,6 +376,9 @@ export class CoreSystem {
|
|||
this.rootDomElement.textContent = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
private registerLoadedKibanaEventType(analytics: AnalyticsServiceSetup) {
|
||||
analytics.registerEventType({
|
||||
eventType: 'Loaded Kibana',
|
||||
|
@ -350,45 +387,6 @@ export class CoreSystem {
|
|||
type: 'keyword',
|
||||
_meta: { description: 'The version of Kibana' },
|
||||
},
|
||||
memory_js_heap_size_limit: {
|
||||
type: 'long',
|
||||
_meta: { description: 'The maximum size of the heap', optional: true },
|
||||
},
|
||||
memory_js_heap_size_total: {
|
||||
type: 'long',
|
||||
_meta: { description: 'The total size of the heap', optional: true },
|
||||
},
|
||||
memory_js_heap_size_used: {
|
||||
type: 'long',
|
||||
_meta: { description: 'The used size of the heap', optional: true },
|
||||
},
|
||||
load_started: {
|
||||
type: 'long',
|
||||
_meta: { description: 'When the render template starts loading assets', optional: true },
|
||||
},
|
||||
bootstrap_started: {
|
||||
type: 'long',
|
||||
_meta: { description: 'When kbnBootstrap callback is called', optional: true },
|
||||
},
|
||||
core_created: {
|
||||
type: 'long',
|
||||
_meta: { description: 'When core system is created', optional: true },
|
||||
},
|
||||
setup_done: {
|
||||
type: 'long',
|
||||
_meta: { description: 'When core system setup is complete', optional: true },
|
||||
},
|
||||
start_done: {
|
||||
type: 'long',
|
||||
_meta: { description: 'When core system start is complete', optional: true },
|
||||
},
|
||||
first_app_nav: {
|
||||
type: 'long',
|
||||
_meta: {
|
||||
description: 'When the application emits the first app navigation',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
protocol: {
|
||||
type: 'keyword',
|
||||
_meta: {
|
||||
|
|
19
src/core/public/events.ts
Normal file
19
src/core/public/events.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
/** @internal */
|
||||
export const KBN_LOAD_MARKS = 'kbnLoad';
|
||||
|
||||
export const KIBANA_LOADED_EVENT = 'kibana_loaded';
|
||||
|
||||
export const LOAD_START = 'load_started';
|
||||
export const LOAD_BOOTSTRAP_START = 'bootstrap_started';
|
||||
export const LOAD_CORE_CREATED = 'core_created';
|
||||
export const LOAD_SETUP_DONE = 'setup_done';
|
||||
export const LOAD_START_DONE = 'start_done';
|
||||
export const LOAD_FIRST_NAV = 'first_app_nav';
|
|
@ -27,9 +27,9 @@ describe('fetchOptionalMemoryInfo', () => {
|
|||
},
|
||||
};
|
||||
expect(fetchOptionalMemoryInfo()).toEqual({
|
||||
memory_js_heap_size_limit: 3,
|
||||
memory_js_heap_size_total: 2,
|
||||
memory_js_heap_size_used: 1,
|
||||
jsHeapSizeLimit: 3,
|
||||
totalJSHeapSize: 2,
|
||||
usedJSHeapSize: 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -14,15 +14,15 @@ export interface BrowserPerformanceMemoryInfo {
|
|||
/**
|
||||
* The maximum size of the heap, in bytes, that is available to the context.
|
||||
*/
|
||||
memory_js_heap_size_limit: number;
|
||||
jsHeapSizeLimit: number;
|
||||
/**
|
||||
* The total allocated heap size, in bytes.
|
||||
*/
|
||||
memory_js_heap_size_total: number;
|
||||
totalJSHeapSize: number;
|
||||
/**
|
||||
* The currently active segment of JS heap, in bytes.
|
||||
*/
|
||||
memory_js_heap_size_used: number;
|
||||
usedJSHeapSize: number;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -34,9 +34,9 @@ export function fetchOptionalMemoryInfo(): BrowserPerformanceMemoryInfo | undefi
|
|||
const memory = window.performance.memory;
|
||||
if (memory) {
|
||||
return {
|
||||
memory_js_heap_size_limit: memory.jsHeapSizeLimit,
|
||||
memory_js_heap_size_total: memory.totalJSHeapSize,
|
||||
memory_js_heap_size_used: memory.usedJSHeapSize,
|
||||
jsHeapSizeLimit: memory.jsHeapSizeLimit,
|
||||
totalJSHeapSize: memory.totalJSHeapSize,
|
||||
usedJSHeapSize: memory.usedJSHeapSize,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,10 +11,12 @@ import { KBN_LOAD_MARKS } from '@kbn/core-mount-utils-browser-internal';
|
|||
import { CoreSystem } from './core_system';
|
||||
import { ApmSystem } from './apm_system';
|
||||
|
||||
import { LOAD_BOOTSTRAP_START } from './events';
|
||||
|
||||
/** @internal */
|
||||
export async function __kbnBootstrap__() {
|
||||
performance.mark(KBN_LOAD_MARKS, {
|
||||
detail: 'bootstrap_started',
|
||||
detail: LOAD_BOOTSTRAP_START,
|
||||
});
|
||||
|
||||
const injectedMetadata = JSON.parse(
|
||||
|
|
|
@ -23,7 +23,8 @@ import {
|
|||
} from '@kbn/core-config-server-internal';
|
||||
import { NodeService, nodeConfig } from '@kbn/core-node-server-internal';
|
||||
import { AnalyticsService } from '@kbn/core-analytics-server-internal';
|
||||
import type { AnalyticsServiceSetup } from '@kbn/core-analytics-server';
|
||||
import type { AnalyticsServiceSetup, AnalyticsServiceStart } from '@kbn/core-analytics-server';
|
||||
import { reportPerformanceMetricEvent } from '@kbn/ebt-tools';
|
||||
import { EnvironmentService, pidConfig } from '@kbn/core-environment-server-internal';
|
||||
import {
|
||||
ExecutionContextService,
|
||||
|
@ -402,7 +403,7 @@ export class Server {
|
|||
startTransaction?.end();
|
||||
|
||||
this.uptimePerStep.start = { start: startStartUptime, end: process.uptime() };
|
||||
analyticsStart.reportEvent(KIBANA_STARTED_EVENT, { uptime_per_step: this.uptimePerStep });
|
||||
this.reportKibanaStartedEvents(analyticsStart);
|
||||
|
||||
return this.coreStart;
|
||||
}
|
||||
|
@ -464,6 +465,11 @@ export class Server {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the legacy KIBANA_STARTED_EVENT.
|
||||
* @param analyticsSetup The {@link AnalyticsServiceSetup}
|
||||
* @private
|
||||
*/
|
||||
private registerKibanaStartedEventType(analyticsSetup: AnalyticsServiceSetup) {
|
||||
analyticsSetup.registerEventType<{ uptime_per_step: UptimeSteps }>({
|
||||
eventType: KIBANA_STARTED_EVENT,
|
||||
|
@ -551,4 +557,33 @@ export class Server {
|
|||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports the new and legacy KIBANA_STARTED_EVENT.
|
||||
* @param analyticsStart The {@link AnalyticsServiceStart}.
|
||||
* @private
|
||||
*/
|
||||
private reportKibanaStartedEvents(analyticsStart: AnalyticsServiceStart) {
|
||||
// Report the legacy KIBANA_STARTED_EVENT.
|
||||
analyticsStart.reportEvent(KIBANA_STARTED_EVENT, { uptime_per_step: this.uptimePerStep });
|
||||
|
||||
const ups = this.uptimePerStep;
|
||||
|
||||
const toMs = (sec: number) => Math.round(sec * 1000);
|
||||
// Report the metric-shaped KIBANA_STARTED_EVENT.
|
||||
reportPerformanceMetricEvent(analyticsStart, {
|
||||
eventName: KIBANA_STARTED_EVENT,
|
||||
duration: toMs(ups.start!.end - ups.constructor!.start),
|
||||
key1: 'time_to_constructor',
|
||||
value1: toMs(ups.constructor!.start),
|
||||
key2: 'constructor_time',
|
||||
value2: toMs(ups.constructor!.end - ups.constructor!.start),
|
||||
key3: 'preboot_time',
|
||||
value3: toMs(ups.preboot!.end - ups.preboot!.start),
|
||||
key4: 'setup_time',
|
||||
value4: toMs(ups.setup!.end - ups.setup!.start),
|
||||
key5: 'start_time',
|
||||
value5: toMs(ups.start!.end - ups.start!.start),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import { I18nProvider } from '@kbn/i18n-react';
|
|||
import uuid from 'uuid';
|
||||
import { CoreStart, IUiSettingsClient, KibanaExecutionContext } from '@kbn/core/public';
|
||||
import { Start as InspectorStartContract } from '@kbn/inspector-plugin/public';
|
||||
import { reportPerformanceMetricEvent } from '@kbn/ebt-tools';
|
||||
|
||||
import { ControlGroupContainer } from '@kbn/controls-plugin/public';
|
||||
import { Filter, TimeRange } from '@kbn/es-query';
|
||||
|
@ -31,7 +32,7 @@ import {
|
|||
} from '../../services/embeddable';
|
||||
import { DASHBOARD_CONTAINER_TYPE } from './dashboard_constants';
|
||||
import { createPanelState } from './panel';
|
||||
import { DashboardLoadedEvent, DashboardPanelState } from './types';
|
||||
import { DashboardPanelState } from './types';
|
||||
import { DashboardViewport } from './viewport/dashboard_viewport';
|
||||
import {
|
||||
KibanaContextProvider,
|
||||
|
@ -40,6 +41,7 @@ import {
|
|||
KibanaThemeProvider,
|
||||
} from '../../services/kibana_react';
|
||||
import { PLACEHOLDER_EMBEDDABLE } from './placeholder';
|
||||
import { DASHBOARD_LOADED_EVENT } from '../../events';
|
||||
import { DashboardAppCapabilities, DashboardContainerInput } from '../../types';
|
||||
import { PresentationUtilPluginStart } from '../../services/presentation_util';
|
||||
import type { ScreenshotModePluginStart } from '../../services/screenshot_mode';
|
||||
|
@ -66,6 +68,13 @@ export interface DashboardContainerServices {
|
|||
analytics?: CoreStart['analytics'];
|
||||
}
|
||||
|
||||
export interface DashboardLoadedInfo {
|
||||
timeToData: number;
|
||||
timeToDone: number;
|
||||
numOfPanels: number;
|
||||
status: string;
|
||||
}
|
||||
|
||||
interface IndexSignature {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
@ -155,10 +164,17 @@ export class DashboardContainer extends Container<InheritedChildInput, Dashboard
|
|||
}
|
||||
}
|
||||
|
||||
private onDataLoaded(data: DashboardLoadedEvent) {
|
||||
this.services.analytics?.reportEvent('dashboard-data-loaded', {
|
||||
...data,
|
||||
});
|
||||
private onDataLoaded(data: DashboardLoadedInfo) {
|
||||
if (this.services.analytics) {
|
||||
reportPerformanceMetricEvent(this.services.analytics, {
|
||||
eventName: DASHBOARD_LOADED_EVENT,
|
||||
duration: data.timeToDone,
|
||||
key1: 'time_to_data',
|
||||
value1: data.timeToData,
|
||||
key2: 'num_of_panels',
|
||||
value2: data.numOfPanels,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected createNewPanelState<
|
||||
|
|
|
@ -19,9 +19,13 @@ import ReactGridLayout, { Layout, ReactGridLayoutProps } from 'react-grid-layout
|
|||
import { GridData } from '../../../../common';
|
||||
import { ViewMode, EmbeddablePhaseEvent } from '../../../services/embeddable';
|
||||
import { DASHBOARD_GRID_COLUMN_COUNT, DASHBOARD_GRID_HEIGHT } from '../dashboard_constants';
|
||||
import { DashboardLoadedEventStatus, DashboardLoadedEvent, DashboardPanelState } from '../types';
|
||||
import { DashboardLoadedEventStatus, DashboardPanelState } from '../types';
|
||||
import { withKibana } from '../../../services/kibana_react';
|
||||
import { DashboardContainer, DashboardReactContextValue } from '../dashboard_container';
|
||||
import {
|
||||
DashboardContainer,
|
||||
DashboardReactContextValue,
|
||||
DashboardLoadedInfo,
|
||||
} from '../dashboard_container';
|
||||
import { DashboardGridItem } from './dashboard_grid_item';
|
||||
|
||||
let lastValidGridSize = 0;
|
||||
|
@ -103,7 +107,7 @@ const ResponsiveSizedGrid = sizeMe(config)(ResponsiveGrid);
|
|||
export interface DashboardGridProps extends ReactIntl.InjectedIntlProps {
|
||||
kibana: DashboardReactContextValue;
|
||||
container: DashboardContainer;
|
||||
onDataLoaded?: (data: DashboardLoadedEvent) => void;
|
||||
onDataLoaded?: (data: DashboardLoadedInfo) => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
|
@ -270,7 +274,7 @@ class DashboardGridUi extends React.Component<DashboardGridProps, State> {
|
|||
doneCount++;
|
||||
if (doneCount === panelsInOrder.length) {
|
||||
const doneTime = performance.now();
|
||||
const data: DashboardLoadedEvent = {
|
||||
const data: DashboardLoadedInfo = {
|
||||
timeToData: (lastTimeToData || doneTime) - loadStartTime,
|
||||
timeToDone: doneTime - loadStartTime,
|
||||
numOfPanels: panelsInOrder.length,
|
||||
|
|
|
@ -10,11 +10,6 @@ export * from '../../../common/types';
|
|||
|
||||
export type DashboardLoadedEventStatus = 'done' | 'error';
|
||||
|
||||
export interface DashboardLoadedEvent {
|
||||
// Time from start to when data is loaded
|
||||
timeToData: number;
|
||||
// Time from start until render or error
|
||||
timeToDone: number;
|
||||
numOfPanels: number;
|
||||
export interface DashboardLoadedEventMeta {
|
||||
status: DashboardLoadedEventStatus;
|
||||
}
|
||||
|
|
|
@ -14,18 +14,21 @@ import {
|
|||
LazyControlsCallout,
|
||||
} from '@kbn/controls-plugin/public';
|
||||
import { ViewMode } from '../../../services/embeddable';
|
||||
import { DashboardContainer, DashboardReactContextValue } from '../dashboard_container';
|
||||
import {
|
||||
DashboardContainer,
|
||||
DashboardReactContextValue,
|
||||
DashboardLoadedInfo,
|
||||
} from '../dashboard_container';
|
||||
import { DashboardGrid } from '../grid';
|
||||
import { context } from '../../../services/kibana_react';
|
||||
import { DashboardEmptyScreen } from '../empty_screen/dashboard_empty_screen';
|
||||
import { withSuspense } from '../../../services/presentation_util';
|
||||
import { DashboardLoadedEvent } from '../types';
|
||||
|
||||
export interface DashboardViewportProps {
|
||||
container: DashboardContainer;
|
||||
controlGroup?: ControlGroupContainer;
|
||||
controlsEnabled?: boolean;
|
||||
onDataLoaded?: (data: DashboardLoadedEvent) => void;
|
||||
onDataLoaded?: (data: DashboardLoadedInfo) => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
|
|
9
src/plugins/dashboard/public/events.ts
Normal file
9
src/plugins/dashboard/public/events.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export const DASHBOARD_LOADED_EVENT = 'dashboard_loaded';
|
|
@ -71,7 +71,6 @@ import {
|
|||
LibraryNotificationAction,
|
||||
CopyToDashboardAction,
|
||||
DashboardCapabilities,
|
||||
DashboardLoadedEvent,
|
||||
} from './application';
|
||||
import { DashboardAppLocatorDefinition, DashboardAppLocator } from './locator';
|
||||
import { createSavedDashboardLoader } from './saved_dashboards';
|
||||
|
@ -139,30 +138,6 @@ export class DashboardPlugin
|
|||
private dashboardFeatureFlagConfig?: DashboardFeatureFlagConfig;
|
||||
private locator?: DashboardAppLocator;
|
||||
|
||||
private registerEvents(analytics: CoreSetup['analytics']) {
|
||||
analytics.registerEventType<DashboardLoadedEvent>({
|
||||
eventType: 'dashboard-data-loaded',
|
||||
schema: {
|
||||
timeToData: {
|
||||
type: 'long',
|
||||
_meta: { description: 'Time all embeddables took to load data' },
|
||||
},
|
||||
timeToDone: {
|
||||
type: 'long',
|
||||
_meta: { description: 'Time all embeddables took to load data' },
|
||||
},
|
||||
status: {
|
||||
type: 'keyword',
|
||||
_meta: { description: 'Error ok' },
|
||||
},
|
||||
numOfPanels: {
|
||||
type: 'long',
|
||||
_meta: { description: 'Number of panels loaded' },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public setup(
|
||||
core: CoreSetup<DashboardStartDependencies, DashboardStart>,
|
||||
{
|
||||
|
@ -312,8 +287,6 @@ export class DashboardPlugin
|
|||
},
|
||||
};
|
||||
|
||||
this.registerEvents(core.analytics);
|
||||
|
||||
core.application.register(app);
|
||||
urlForwarding.forwardApp(
|
||||
DashboardConstants.DASHBOARDS_ID,
|
||||
|
|
|
@ -17,6 +17,7 @@ import {
|
|||
timer,
|
||||
toArray,
|
||||
} from 'rxjs';
|
||||
import { get } from 'lodash';
|
||||
import type { Event } from '@kbn/analytics-client';
|
||||
import type { GetEventsOptions } from './types';
|
||||
|
||||
|
@ -25,7 +26,7 @@ export async function fetchEvents(
|
|||
takeNumberOfEvents: number,
|
||||
options: GetEventsOptions = {}
|
||||
): Promise<Event[]> {
|
||||
const { eventTypes = [], withTimeoutMs, fromTimestamp } = options;
|
||||
const { eventTypes = [], withTimeoutMs, fromTimestamp, filters } = options;
|
||||
|
||||
const filteredEvents$ = events$.pipe(
|
||||
filter((event) => {
|
||||
|
@ -39,6 +40,28 @@ export async function fetchEvents(
|
|||
return new Date(event.timestamp).getTime() - new Date(fromTimestamp).getTime() > 0;
|
||||
}
|
||||
return true;
|
||||
}),
|
||||
filter((event) => {
|
||||
if (filters) {
|
||||
return Object.entries(filters).every(([key, comparison]) => {
|
||||
const value = get(event, key);
|
||||
return Object.entries(comparison).every(([operation, valueToCompare]) => {
|
||||
switch (operation) {
|
||||
case 'eq':
|
||||
return value === valueToCompare;
|
||||
case 'gte':
|
||||
return value >= (valueToCompare as typeof value);
|
||||
case 'gt':
|
||||
return value > (valueToCompare as typeof value);
|
||||
case 'lte':
|
||||
return value <= (valueToCompare as typeof value);
|
||||
case 'lt':
|
||||
return value < (valueToCompare as typeof value);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
return true;
|
||||
})
|
||||
);
|
||||
|
||||
|
|
|
@ -8,6 +8,10 @@
|
|||
|
||||
import type { Event, EventType } from '@kbn/analytics-client';
|
||||
|
||||
export type FiltersOptions = {
|
||||
[key in 'eq' | 'gte' | 'gt' | 'lte' | 'lt']?: unknown;
|
||||
};
|
||||
|
||||
export interface GetEventsOptions {
|
||||
/**
|
||||
* eventTypes (optional) array of event types to return
|
||||
|
@ -24,6 +28,18 @@ export interface GetEventsOptions {
|
|||
* @remarks Useful when we need to retrieve the events after a specific action, and we don't care about anything prior to that.
|
||||
*/
|
||||
fromTimestamp?: string;
|
||||
/**
|
||||
* List of internal keys to validate in the event with the validation comparison.
|
||||
* @example
|
||||
* {
|
||||
* filters: {
|
||||
* 'properties.my_key': {
|
||||
* eq: 'my expected value',
|
||||
* },
|
||||
* },
|
||||
* }
|
||||
*/
|
||||
filters?: Record<string, FiltersOptions>;
|
||||
}
|
||||
|
||||
export interface EBTHelpersContract {
|
||||
|
@ -37,7 +53,10 @@ export interface EBTHelpersContract {
|
|||
* @param takeNumberOfEvents - number of events to return
|
||||
* @param options (optional) list of options to filter events or for advanced usage of the API {@link GetEventsOptions}.
|
||||
*/
|
||||
getEvents: (takeNumberOfEvents: number, options?: GetEventsOptions) => Promise<Event[]>;
|
||||
getEvents: (
|
||||
takeNumberOfEvents: number,
|
||||
options?: GetEventsOptions
|
||||
) => Promise<Array<Event<Record<string, unknown>>>>;
|
||||
/**
|
||||
* Count the number of events that match the filters.
|
||||
* @param options list of options to filter the events {@link GetEventsOptions}. `withTimeoutMs` is required in this API.
|
||||
|
|
|
@ -127,5 +127,32 @@ describe('AnalyticsFTRHelpers', () => {
|
|||
})
|
||||
).resolves.toEqual([{ ...event, timestamp: '2022-06-10T00:00:00.000Z' }]);
|
||||
});
|
||||
|
||||
test('should filter by `filters` when provided', async () => {
|
||||
// 3 enqueued events
|
||||
const events = [
|
||||
{ ...event, timestamp: '2022-01-10T00:00:00.000Z' },
|
||||
{ ...event, timestamp: '2022-03-10T00:00:00.000Z', properties: { my_property: 20 } },
|
||||
{ ...event, timestamp: '2022-06-10T00:00:00.000Z' },
|
||||
];
|
||||
events.forEach((ev) => events$.next(ev));
|
||||
|
||||
await expect(
|
||||
window.__analytics_ftr_helpers__.getEvents(1, {
|
||||
eventTypes: [event.event_type],
|
||||
filters: {
|
||||
'properties.my_property': {
|
||||
eq: 20,
|
||||
gte: 20,
|
||||
lte: 20,
|
||||
gt: 10,
|
||||
lt: 30,
|
||||
},
|
||||
},
|
||||
})
|
||||
).resolves.toEqual([
|
||||
{ ...event, timestamp: '2022-03-10T00:00:00.000Z', properties: { my_property: 20 } },
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -48,6 +48,21 @@ export class AnalyticsFTRHelpers implements Plugin {
|
|||
eventTypes: schema.arrayOf(schema.string()),
|
||||
withTimeoutMs: schema.maybe(schema.number()),
|
||||
fromTimestamp: schema.maybe(schema.string()),
|
||||
filters: schema.maybe(
|
||||
schema.recordOf(
|
||||
schema.string(),
|
||||
schema.recordOf(
|
||||
schema.oneOf([
|
||||
schema.literal('eq'),
|
||||
schema.literal('gte'),
|
||||
schema.literal('gt'),
|
||||
schema.literal('lte'),
|
||||
schema.literal('lt'),
|
||||
]),
|
||||
schema.any()
|
||||
)
|
||||
)
|
||||
),
|
||||
}),
|
||||
},
|
||||
},
|
||||
|
|
|
@ -16,7 +16,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
const { common } = getPageObjects(['common']);
|
||||
|
||||
describe('Core Context Providers', () => {
|
||||
let event: Event;
|
||||
let event: Event<Record<string, unknown>>;
|
||||
before(async () => {
|
||||
await common.navigateToApp('home');
|
||||
[event] = await ebtUIHelper.getEvents(1, { eventTypes: ['Loaded Kibana'] }); // Get the loaded Kibana event
|
||||
|
|
|
@ -10,6 +10,8 @@ import { GetEventsOptions } from '@kbn/analytics-ftr-helpers-plugin/common/types
|
|||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../../services';
|
||||
|
||||
const DASHBOARD_LOADED_EVENT = 'dashboard_loaded';
|
||||
|
||||
export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||
const ebtUIHelper = getService('kibana_ebt_ui');
|
||||
const PageObjects = getPageObjects([
|
||||
|
@ -29,23 +31,26 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
|
||||
const getEvents = async (count: number, options?: GetEventsOptions) =>
|
||||
ebtUIHelper.getEvents(count, {
|
||||
eventTypes: ['dashboard-data-loaded'],
|
||||
eventTypes: ['performance_metric'],
|
||||
fromTimestamp,
|
||||
withTimeoutMs: 1000,
|
||||
filters: { 'properties.eventName': { eq: DASHBOARD_LOADED_EVENT } },
|
||||
...options,
|
||||
});
|
||||
|
||||
const checkEmitsOnce = async (options?: GetEventsOptions) => {
|
||||
const events = await getEvents(Number.MAX_SAFE_INTEGER, options);
|
||||
expect(events.length).to.be(1);
|
||||
const event = events[0];
|
||||
expect(event.event_type).to.eql('dashboard-data-loaded');
|
||||
expect(event.event_type).to.eql('performance_metric');
|
||||
expect(event.properties.eventName).to.eql(DASHBOARD_LOADED_EVENT);
|
||||
expect(event.context.applicationId).to.be('dashboards');
|
||||
expect(event.context.page).to.be('app');
|
||||
expect(event.context.pageName).to.be('application:dashboards:app');
|
||||
expect(event.properties.status).to.be('done');
|
||||
expect(event.properties.timeToData).to.be.a('number');
|
||||
expect(event.properties.timeToDone).to.be.a('number');
|
||||
expect(event.properties.duration).to.be.a('number');
|
||||
expect(event.properties.key1).to.eql('time_to_data');
|
||||
expect(event.properties.value1).to.be.a('number');
|
||||
expect(event.properties.key2).to.eql('num_of_panels');
|
||||
expect(event.properties.value2).to.be.a('number');
|
||||
|
||||
// update fromTimestamp
|
||||
fromTimestamp = event.timestamp;
|
||||
|
@ -81,7 +86,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
await PageObjects.common.navigateToApp('dashboards');
|
||||
});
|
||||
|
||||
it('doesnt emit on empty dashboard', async () => {
|
||||
it("doesn't emit on empty dashboard", async () => {
|
||||
await PageObjects.dashboard.clickNewDashboard();
|
||||
await checkDoesNotEmit();
|
||||
});
|
||||
|
@ -100,7 +105,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
const event = await checkEmitsOnce();
|
||||
|
||||
expect(event.context.entityId).to.be('new');
|
||||
expect(event.properties.numOfPanels).to.be(1);
|
||||
expect(event.properties.key2).to.be('num_of_panels');
|
||||
expect(event.properties.value2).to.be(1);
|
||||
});
|
||||
|
||||
it('emits on saved search refreshed', async () => {
|
||||
|
@ -108,7 +114,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
await checkEmitsOnce();
|
||||
});
|
||||
|
||||
it('doesnt emit when removing saved search panel', async () => {
|
||||
it("doesn't emit when removing saved search panel", async () => {
|
||||
await dashboardPanelActions.removePanelByTitle(SAVED_SEARCH_PANEL_TITLE);
|
||||
await checkDoesNotEmit();
|
||||
});
|
||||
|
@ -126,7 +132,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
await checkEmitsOnce();
|
||||
});
|
||||
|
||||
it('doesnt emit when removing vis panel', async () => {
|
||||
it("doesn't emit when removing vis panel", async () => {
|
||||
await dashboardPanelActions.removePanelByTitle(VIS_PANEL_TITLE);
|
||||
await checkDoesNotEmit();
|
||||
});
|
||||
|
@ -150,7 +156,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
await checkEmitsOnce();
|
||||
});
|
||||
|
||||
it('doesnt emit when removing markup panel', async () => {
|
||||
it("doesn't emit when removing markup panel", async () => {
|
||||
await dashboardPanelActions.removePanelByTitle(MARKDOWN_PANEL_TITLE);
|
||||
await checkDoesNotEmit();
|
||||
});
|
||||
|
@ -170,7 +176,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
await checkEmitsOnce();
|
||||
});
|
||||
|
||||
it('doesnt emit when removing map panel', async () => {
|
||||
it("doesn't emit when removing map panel", async () => {
|
||||
await dashboardPanelActions.removePanelByTitle(MAP_PANEL_TITLE);
|
||||
await checkDoesNotEmit();
|
||||
});
|
||||
|
@ -187,10 +193,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
|
||||
const event = await checkEmitsOnce();
|
||||
expect(event.context.entityId).to.be('7adfa750-4c81-11e8-b3d7-01146121b73d');
|
||||
expect(event.properties.numOfPanels).to.be(17);
|
||||
expect(event.properties.timeToDone as number).to.be.greaterThan(
|
||||
event.properties.timeToData as number
|
||||
|
||||
expect(event.properties.key1).to.be('time_to_data');
|
||||
expect(event.properties.duration as number).to.be.greaterThan(
|
||||
event.properties.value1 as number
|
||||
);
|
||||
|
||||
expect(event.properties.key2).to.be('num_of_panels');
|
||||
expect(event.properties.value2).to.be(17);
|
||||
});
|
||||
|
||||
/**
|
||||
|
|
|
@ -19,36 +19,57 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
await common.navigateToApp('home');
|
||||
});
|
||||
|
||||
it('should emit the "Loaded Kibana" event', async () => {
|
||||
it('should emit the legacy "Loaded Kibana"', async () => {
|
||||
const [event] = await ebtUIHelper.getEvents(1, { eventTypes: ['Loaded Kibana'] });
|
||||
|
||||
expect(event.event_type).to.eql('Loaded Kibana');
|
||||
expect(event.properties).to.have.property('kibana_version');
|
||||
expect(event.properties.kibana_version).to.be.a('string');
|
||||
expect(event.properties).to.have.property('protocol');
|
||||
expect(event.properties.protocol).to.be.a('string');
|
||||
});
|
||||
|
||||
it('should emit the kibana_loaded event', async () => {
|
||||
const [event] = await ebtUIHelper.getEvents(1, {
|
||||
eventTypes: ['performance_metric'],
|
||||
filters: { 'properties.eventName': { eq: 'kibana_loaded' } },
|
||||
});
|
||||
|
||||
// New event
|
||||
expect(event.event_type).to.eql('performance_metric');
|
||||
expect(event.properties.eventName).to.eql('kibana_loaded');
|
||||
|
||||
// meta
|
||||
expect(event.properties).to.have.property('meta');
|
||||
|
||||
const meta = event.properties.meta as Record<string, any>;
|
||||
expect(meta.kibana_version).to.be.a('string');
|
||||
expect(meta.protocol).to.be.a('string');
|
||||
|
||||
// Kibana Loaded timings
|
||||
expect(event.properties).to.have.property('load_started');
|
||||
expect(event.properties.load_started).to.be.a('number');
|
||||
expect(event.properties).to.have.property('bootstrap_started');
|
||||
expect(event.properties.bootstrap_started).to.be.a('number');
|
||||
expect(event.properties).to.have.property('core_created');
|
||||
expect(event.properties.core_created).to.be.a('number');
|
||||
expect(event.properties).to.have.property('setup_done');
|
||||
expect(event.properties.setup_done).to.be.a('number');
|
||||
expect(event.properties).to.have.property('start_done');
|
||||
expect(event.properties.start_done).to.be.a('number');
|
||||
expect(event.properties).to.have.property('first_app_nav');
|
||||
expect(event.properties.start_done).to.be.a('number');
|
||||
expect(event.properties).to.have.property('duration');
|
||||
expect(event.properties.duration).to.be.a('number');
|
||||
|
||||
expect(event.properties).to.have.property('key1', 'load_started');
|
||||
expect(event.properties).to.have.property('key2', 'bootstrap_started');
|
||||
expect(event.properties).to.have.property('key3', 'core_created');
|
||||
expect(event.properties).to.have.property('key4', 'setup_done');
|
||||
expect(event.properties).to.have.property('key5', 'start_done');
|
||||
|
||||
expect(event.properties.value1).to.be.a('number');
|
||||
expect(event.properties.value2).to.be.a('number');
|
||||
expect(event.properties.value3).to.be.a('number');
|
||||
expect(event.properties.value4).to.be.a('number');
|
||||
expect(event.properties.value5).to.be.a('number');
|
||||
|
||||
if (browser.isChromium) {
|
||||
// Kibana Loaded memory
|
||||
expect(event.properties).to.have.property('memory_js_heap_size_limit');
|
||||
expect(event.properties.memory_js_heap_size_limit).to.be.a('number');
|
||||
expect(event.properties).to.have.property('memory_js_heap_size_total');
|
||||
expect(event.properties.memory_js_heap_size_total).to.be.a('number');
|
||||
expect(event.properties).to.have.property('memory_js_heap_size_used');
|
||||
expect(event.properties.memory_js_heap_size_used).to.be.a('number');
|
||||
expect(meta).to.have.property('jsHeapSizeLimit');
|
||||
expect(meta.jsHeapSizeLimit).to.be.a('number');
|
||||
expect(meta).to.have.property('totalJSHeapSize');
|
||||
expect(meta.totalJSHeapSize).to.be.a('number');
|
||||
expect(meta).to.have.property('usedJSHeapSize');
|
||||
expect(meta.usedJSHeapSize).to.be.a('number');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -15,7 +15,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
const ebtServerHelper = getService('kibana_ebt_server');
|
||||
|
||||
describe('Core Context Providers', () => {
|
||||
let event: Event;
|
||||
let event: Event<Record<string, unknown>>;
|
||||
before(async () => {
|
||||
let i = 2;
|
||||
do {
|
||||
|
|
|
@ -14,8 +14,8 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
const ebtServerHelper = getService('kibana_ebt_server');
|
||||
|
||||
describe('core-overall_status_changed', () => {
|
||||
let initialEvent: Event;
|
||||
let secondEvent: Event;
|
||||
let initialEvent: Event<Record<string, unknown>>;
|
||||
let secondEvent: Event<Record<string, unknown>>;
|
||||
|
||||
before(async () => {
|
||||
[initialEvent, secondEvent] = await ebtServerHelper.getEvents(2, {
|
||||
|
|
|
@ -13,7 +13,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
const ebtServerHelper = getService('kibana_ebt_server');
|
||||
|
||||
describe('kibana_started', () => {
|
||||
it('should emit the "kibana_started" event', async () => {
|
||||
it('should emit the legacy "kibana_started" event', async () => {
|
||||
const [event] = await ebtServerHelper.getEvents(1, { eventTypes: ['kibana_started'] });
|
||||
expect(event.event_type).to.eql('kibana_started');
|
||||
const uptimePerStep = event.properties.uptime_per_step as Record<
|
||||
|
@ -29,5 +29,25 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
expect(uptimePerStep.start.start).to.be.a('number');
|
||||
expect(uptimePerStep.start.end).to.be.a('number');
|
||||
});
|
||||
|
||||
it('should emit the "kibana_started" metric event', async () => {
|
||||
const [event] = await ebtServerHelper.getEvents(1, {
|
||||
eventTypes: ['performance_metric'],
|
||||
filters: { 'properties.eventName': { eq: 'kibana_started' } },
|
||||
});
|
||||
expect(event.event_type).to.eql('performance_metric');
|
||||
expect(event.properties.eventName).to.eql('kibana_started');
|
||||
expect(event.properties.duration).to.be.a('number');
|
||||
expect(event.properties.key1).to.eql('time_to_constructor');
|
||||
expect(event.properties.value1).to.be.a('number');
|
||||
expect(event.properties.key2).to.eql('constructor_time');
|
||||
expect(event.properties.value2).to.be.a('number');
|
||||
expect(event.properties.key3).to.eql('preboot_time');
|
||||
expect(event.properties.value3).to.be.a('number');
|
||||
expect(event.properties.key4).to.eql('setup_time');
|
||||
expect(event.properties.value4).to.be.a('number');
|
||||
expect(event.properties.key5).to.eql('start_time');
|
||||
expect(event.properties.value5).to.be.a('number');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -735,7 +735,7 @@ export class MapEmbeddable
|
|||
) {
|
||||
/**
|
||||
* Maps emit rendered when the data is loaded, as we don't have feedback from the maps rendering library atm.
|
||||
* This means that the dashboard-loaded event might be fired while a map is still rendering in some cases.
|
||||
* This means that the DASHBOARD_LOADED_EVENT event might be fired while a map is still rendering in some cases.
|
||||
* For more details please contact the maps team.
|
||||
*/
|
||||
this.updateOutput({
|
||||
|
|
|
@ -3463,6 +3463,10 @@
|
|||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/ebt-tools@link:bazel-bin/packages/kbn-ebt-tools":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/es-archiver@link:bazel-bin/packages/kbn-es-archiver":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
@ -7351,6 +7355,10 @@
|
|||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@types/kbn__ebt-tools@link:bazel-bin/packages/kbn-ebt-tools/npm_module_types":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@types/kbn__es-archiver@link:bazel-bin/packages/kbn-es-archiver/npm_module_types":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue