diff --git a/packages/analytics/client/src/analytics_client/analytics_client.ts b/packages/analytics/client/src/analytics_client/analytics_client.ts index e6815b44152543..57741f098c6acc 100644 --- a/packages/analytics/client/src/analytics_client/analytics_client.ts +++ b/packages/analytics/client/src/analytics_client/analytics_client.ts @@ -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; } export class AnalyticsClient implements IAnalyticsClient { @@ -65,7 +65,7 @@ export class AnalyticsClient implements IAnalyticsClient { private readonly shipperRegistered$ = new Subject(); private readonly eventTypeRegistry = new Map< EventType, - EventTypeOpts & { validator?: Type> } + EventTypeOpts & { validator?: Mixed } >(); private readonly contextService: ContextService; private readonly context$ = new BehaviorSubject>({}); @@ -88,7 +88,7 @@ export class AnalyticsClient implements IAnalyticsClient { this.reportEnqueuedEventsWhenClientIsReady(); } - public reportEvent = >( + public reportEvent = ( 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( + `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, }; // debug-logging before checking the opt-in status to help during development diff --git a/packages/analytics/client/src/analytics_client/types.ts b/packages/analytics/client/src/analytics_client/types.ts index 88b60dc100e895..2af29d88b5cebf 100644 --- a/packages/analytics/client/src/analytics_client/types.ts +++ b/packages/analytics/client/src/analytics_client/types.ts @@ -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: >( + reportEvent: ( eventType: EventType, eventData: EventTypeData ) => void; diff --git a/packages/analytics/client/src/events/types.ts b/packages/analytics/client/src/events/types.ts index 0c97bee3fdbb7c..78b2f792e9e2bd 100644 --- a/packages/analytics/client/src/events/types.ts +++ b/packages/analytics/client/src/events/types.ts @@ -108,7 +108,7 @@ export interface TelemetryCounter { /** * Definition of the full event structure */ -export interface Event { +export interface Event> { /** * 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; + properties: Properties; /** * The {@link EventContext} enriched during the processing pipeline. */ diff --git a/packages/analytics/shippers/fullstory/src/format_payload.ts b/packages/analytics/shippers/fullstory/src/format_payload.ts index c55ed2409da502..03873617af8fc2 100644 --- a/packages/analytics/shippers/fullstory/src/format_payload.ts +++ b/packages/analytics/shippers/fullstory/src/format_payload.ts @@ -19,7 +19,7 @@ const FULLSTORY_RESERVED_PROPERTIES = [ 'pageName', ]; -export function formatPayload(context: Record): Record { +export function formatPayload(context: object): Record { // 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) diff --git a/packages/core/analytics/core-analytics-browser-internal/BUILD.bazel b/packages/core/analytics/core-analytics-browser-internal/BUILD.bazel index 2540ccabd517c2..59f18b8f11d4a4 100644 --- a/packages/core/analytics/core-analytics-browser-internal/BUILD.bazel +++ b/packages/core/analytics/core-analytics-browser-internal/BUILD.bazel @@ -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", diff --git a/packages/core/analytics/core-analytics-browser-internal/src/analytics_service.test.ts b/packages/core/analytics/core-analytics-browser-internal/src/analytics_service.test.ts index 8ae572ae345289..cdd5a27ee9500a 100644 --- a/packages/core/analytics/core-analytics-browser-internal/src/analytics_service.test.ts +++ b/packages/core/analytics/core-analytics-browser-internal/src/analytics_service.test.ts @@ -23,15 +23,15 @@ describe('AnalyticsService', () => { 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", - } - `); + 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) }); @@ -44,6 +44,155 @@ describe('AnalyticsService', () => { }); }); + test('should register the `metrics` and `clicks` event types on creation', () => { + expect(analyticsClientMock.registerEventType).toHaveBeenCalledTimes(2); + expect(analyticsClientMock.registerEventType.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "eventType": "metric", + "schema": Object { + "duration": Object { + "_meta": Object { + "description": "The main event duration in ms", + "optional": true, + }, + "type": "integer", + }, + "eventName": Object { + "_meta": Object { + "description": "Type of the event", + }, + "type": "keyword", + }, + "jsHeapSizeLimit": Object { + "_meta": Object { + "description": "performance.memory.jsHeapSizeLimit", + "optional": true, + }, + "type": "long", + }, + "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", + }, + "status": Object { + "_meta": Object { + "description": "A status", + "optional": true, + }, + "type": "keyword", + }, + "totalJSHeapSize": Object { + "_meta": Object { + "description": "performance.memory.totalJSHeapSize", + "optional": true, + }, + "type": "long", + }, + "usedJSHeapSize": Object { + "_meta": Object { + "description": "performance.memory.usedJSHeapSize", + "optional": true, + }, + "type": "long", + }, + "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({ @@ -76,12 +225,12 @@ describe('AnalyticsService', () => { await expect( firstValueFrom(analyticsClientMock.registerContextProvider.mock.calls[3][0].context$) ).resolves.toMatchInlineSnapshot(` - Object { - "cluster_name": "cluster_name", - "cluster_uuid": "cluster_uuid", - "cluster_version": "version", - } - `); + Object { + "cluster_name": "cluster_name", + "cluster_uuid": "cluster_uuid", + "cluster_version": "version", + } + `); }); test('setup should expose only the APIs report and opt-in', () => { diff --git a/packages/core/analytics/core-analytics-browser-internal/src/analytics_service.ts b/packages/core/analytics/core-analytics-browser-internal/src/analytics_service.ts index 580fbac92aa5d5..84eb75df040985 100644 --- a/packages/core/analytics/core-analytics-browser-internal/src/analytics_service.ts +++ b/packages/core/analytics/core-analytics-browser-internal/src/analytics_service.ts @@ -9,6 +9,7 @@ import { of } from 'rxjs'; import type { AnalyticsClient } from '@kbn/analytics-client'; import { createAnalytics } from '@kbn/analytics-client'; +import { registerMetricEventType } 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,8 @@ export class AnalyticsService { }); this.registerBuildInfoAnalyticsContext(core); + // Register special `metrics` type + registerMetricEventType(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. diff --git a/packages/core/analytics/core-analytics-server-internal/BUILD.bazel b/packages/core/analytics/core-analytics-server-internal/BUILD.bazel index b7ba4ed35a5a3a..0ad590f1ee5054 100644 --- a/packages/core/analytics/core-analytics-server-internal/BUILD.bazel +++ b/packages/core/analytics/core-analytics-server-internal/BUILD.bazel @@ -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", ] diff --git a/packages/core/analytics/core-analytics-server-internal/src/analytics_service.test.mocks.ts b/packages/core/analytics/core-analytics-server-internal/src/analytics_service.test.mocks.ts new file mode 100644 index 00000000000000..3d98cf43929262 --- /dev/null +++ b/packages/core/analytics/core-analytics-server-internal/src/analytics_service.test.mocks.ts @@ -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 = { + 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, +})); diff --git a/packages/core/analytics/core-analytics-server-internal/src/analytics_service.test.ts b/packages/core/analytics/core-analytics-server-internal/src/analytics_service.test.ts new file mode 100644 index 00000000000000..74d1b6e4119ee0 --- /dev/null +++ b/packages/core/analytics/core-analytics-server-internal/src/analytics_service.test.ts @@ -0,0 +1,187 @@ +/* + * 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( + firstValueFrom(analyticsClientMock.registerContextProvider.mock.calls[0][0].context$) + ).resolves.toMatchInlineSnapshot(` + Object { + "branch": "main", + "buildNum": 9007199254740991, + "buildSha": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "isDev": true, + "isDistributable": false, + "version": "8.4.0", + } + `); + }); + + test('should register the `metrics` event type on creation', () => { + expect(analyticsClientMock.registerEventType).toHaveBeenCalledTimes(1); + expect(analyticsClientMock.registerEventType.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "eventType": "metric", + "schema": Object { + "duration": Object { + "_meta": Object { + "description": "The main event duration in ms", + "optional": true, + }, + "type": "integer", + }, + "eventName": Object { + "_meta": Object { + "description": "Type of the event", + }, + "type": "keyword", + }, + "jsHeapSizeLimit": Object { + "_meta": Object { + "description": "performance.memory.jsHeapSizeLimit", + "optional": true, + }, + "type": "long", + }, + "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", + }, + "status": Object { + "_meta": Object { + "description": "A status", + "optional": true, + }, + "type": "keyword", + }, + "totalJSHeapSize": Object { + "_meta": Object { + "description": "performance.memory.totalJSHeapSize", + "optional": true, + }, + "type": "long", + }, + "usedJSHeapSize": Object { + "_meta": Object { + "description": "performance.memory.usedJSHeapSize", + "optional": true, + }, + "type": "long", + }, + "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), + }); + }); +}); diff --git a/packages/core/analytics/core-analytics-server-internal/src/analytics_service.ts b/packages/core/analytics/core-analytics-server-internal/src/analytics_service.ts index 0fa96ebe0ae513..7aaebe2235955e 100644 --- a/packages/core/analytics/core-analytics-server-internal/src/analytics_service.ts +++ b/packages/core/analytics/core-analytics-server-internal/src/analytics_service.ts @@ -9,6 +9,7 @@ import { of } from 'rxjs'; import type { AnalyticsClient } from '@kbn/analytics-client'; import { createAnalytics } from '@kbn/analytics-client'; +import { registerMetricEventType } from '@kbn/ebt-tools'; import type { CoreContext } from '@kbn/core-base-server-internal'; import type { AnalyticsServiceSetup, @@ -29,6 +30,8 @@ export class AnalyticsService { }); this.registerBuildInfoAnalyticsContext(core); + // Register special `metrics` type + registerMetricEventType(this.analyticsClient); } public preboot(): AnalyticsServicePreboot { diff --git a/packages/kbn-ebt-tools/BUILD.bazel b/packages/kbn-ebt-tools/BUILD.bazel index 3b4e37ca356ffb..e4c4209361906f 100644 --- a/packages/kbn-ebt-tools/BUILD.bazel +++ b/packages/kbn-ebt-tools/BUILD.bazel @@ -24,16 +24,10 @@ NPM_MODULE_EXTRA_FILES = [ "README.md" ] -RUNTIME_DEPS = [ - "//packages/analytics/client", - "@npm//load-json-file", - "@npm//tslib", -] +RUNTIME_DEPS = [] TYPES_DEPS = [ "//packages/analytics/client:npm_module_types", - "@npm//load-json-file", - "@npm//tslib", "@npm//@types/jest", "@npm//@types/node", ] diff --git a/packages/kbn-ebt-tools/src/metric_events/helpers.test.ts b/packages/kbn-ebt-tools/src/metric_events/helpers.test.ts new file mode 100644 index 00000000000000..376e5d926c2527 --- /dev/null +++ b/packages/kbn-ebt-tools/src/metric_events/helpers.test.ts @@ -0,0 +1,97 @@ +/* + * 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 { registerMetricEventType, reportMetricEvent } from './helpers'; +import { METRIC_EVENT_SCHEMA } from './schema'; + +describe('metric event helpers', () => { + let analyticsClient: AnalyticsClient; + + describe('registerMetricEventType', () => { + 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 `metrics` eventType to the analytics client', () => { + const registerEventTypeSpy = jest.spyOn(analyticsClient, 'registerEventType'); + + expect(() => registerMetricEventType(analyticsClient)).not.toThrow(); + + expect(registerEventTypeSpy).toHaveBeenCalledWith({ + eventType: 'metric', + schema: METRIC_EVENT_SCHEMA, + }); + }); + }); + + describe('reportMetricEvent', () => { + 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(), + }); + registerMetricEventType(analyticsClient); + }); + + test('reports the minimum allowed event', () => { + reportMetricEvent(analyticsClient, { eventName: 'test-event' }); + }); + + test('reports all the allowed fields in the event', () => { + reportMetricEvent(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 is missing', () => { + expect(() => + reportMetricEvent( + analyticsClient, + // @ts-expect-error + {} + ) + ).toThrowErrorMatchingInlineSnapshot(` + "Failed to validate payload coming from \\"Event Type 'metric'\\": + - [eventName]: {\\"expected\\":\\"string\\",\\"actual\\":\\"undefined\\",\\"value\\":\\"undefined\\"}" + `); + }); + + test('should fail if any additional unknown keys are added', () => { + expect(() => + reportMetricEvent(analyticsClient, { + eventName: 'test-event', + // @ts-expect-error + an_unknown_field: 'blah', + }) + ).toThrowErrorMatchingInlineSnapshot(` + "Failed to validate payload coming from \\"Event Type 'metric'\\": + - []: excess key 'an_unknown_field' found" + `); + }); + }); +}); diff --git a/packages/kbn-ebt-tools/src/metric_events/helpers.ts b/packages/kbn-ebt-tools/src/metric_events/helpers.ts index ba30d10633261e..d385abb676cd89 100644 --- a/packages/kbn-ebt-tools/src/metric_events/helpers.ts +++ b/packages/kbn-ebt-tools/src/metric_events/helpers.ts @@ -7,17 +7,30 @@ */ import type { AnalyticsClient } from '@kbn/analytics-client'; -import { MetricEvent, METRIC_EVENT_SCHEMA } from './schema'; +import { type MetricEvent, METRIC_EVENT_SCHEMA } from './schema'; const METRIC_EVENT_TYPE = 'metric'; -export function registerMetricEvent(analytics: AnalyticsClient) { - analytics.registerEventType({ +/** + * Register the `metrics` 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 registerMetricEventType(analytics: Pick) { + analytics.registerEventType({ eventType: METRIC_EVENT_TYPE, schema: METRIC_EVENT_SCHEMA, }); } -export function reportMetricEvent(analytics: AnalyticsClient, eventData: MetricEvent) { +/** + * Report a `metrics` 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 reportMetricEvent( + analytics: Pick, + eventData: MetricEvent +) { analytics.reportEvent(METRIC_EVENT_TYPE, eventData); } diff --git a/packages/kbn-ebt-tools/src/metric_events/index.ts b/packages/kbn-ebt-tools/src/metric_events/index.ts index 5a23ca6627c6de..1b9e5c762d7f42 100644 --- a/packages/kbn-ebt-tools/src/metric_events/index.ts +++ b/packages/kbn-ebt-tools/src/metric_events/index.ts @@ -6,4 +6,4 @@ * Side Public License, v 1. */ export type { MetricEvent } from './schema'; -export { registerMetricEvent, reportMetricEvent } from './helpers'; +export { registerMetricEventType, reportMetricEvent } from './helpers'; diff --git a/packages/kbn-ebt-tools/src/metric_events/schema.ts b/packages/kbn-ebt-tools/src/metric_events/schema.ts index 121317dc2464bc..3d572963b07db0 100644 --- a/packages/kbn-ebt-tools/src/metric_events/schema.ts +++ b/packages/kbn-ebt-tools/src/metric_events/schema.ts @@ -6,31 +6,100 @@ * Side Public License, v 1. */ -export interface MetricEvent extends Record { +import type { RootSchema } from '@kbn/analytics-client'; + +/** + * Structure of the `metric` event + */ +export interface MetricEvent { + /** + * The name of the action that is tracked in the metrics. + */ eventName: string; - meta?: Record; + /** + * Searchable but not aggregateable metadata relevant to the tracked action. + */ + meta?: Record; - // Standardized fields + /** + * @group Standardized fields + * The time (in milliseconds) it took to run the entire action. + */ duration?: number; + /** + * @group Standardized fields + * A status relevant to the action (i.e.: `failed`, `succeeded`). + */ status?: string; + /** + * @group Standardized fields + * performance.memory.jsHeapSizeLimit + */ jsHeapSizeLimit?: number; + /** + * @group Standardized fields + * performance.memory.totalJSHeapSize + */ totalJSHeapSize?: number; + /** + * @group Standardized fields + * performance.memory.usedJSHeapSize + */ usedJSHeapSize?: number; - // Free fields - will be mapped in the index; + /** + * @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: Record = { +export const METRIC_EVENT_SCHEMA: RootSchema = { eventName: { type: 'keyword', _meta: { description: 'Type of the event' }, @@ -61,42 +130,42 @@ export const METRIC_EVENT_SCHEMA: Record = { }, key1: { type: 'keyword', - _meta: { description: 'Performance metric label', optional: true }, + _meta: { description: 'Performance metric label 1', optional: true }, }, value1: { type: 'long', - _meta: { description: 'Performance metric value', optional: true }, + _meta: { description: 'Performance metric value 1', optional: true }, }, key2: { type: 'keyword', - _meta: { description: 'Performance metric label', optional: true }, + _meta: { description: 'Performance metric label 2', optional: true }, }, value2: { type: 'long', - _meta: { description: 'Performance metric value', optional: true }, + _meta: { description: 'Performance metric value 2', optional: true }, }, key3: { type: 'keyword', - _meta: { description: 'Performance metric label', optional: true }, + _meta: { description: 'Performance metric label 3', optional: true }, }, value3: { type: 'long', - _meta: { description: 'Performance metric value', optional: true }, + _meta: { description: 'Performance metric value 3', optional: true }, }, key4: { type: 'keyword', - _meta: { description: 'Performance metric label', optional: true }, + _meta: { description: 'Performance metric label 4', optional: true }, }, value4: { type: 'long', - _meta: { description: 'Performance metric value', optional: true }, + _meta: { description: 'Performance metric value 4', optional: true }, }, key5: { type: 'keyword', - _meta: { description: 'Performance metric label', optional: true }, + _meta: { description: 'Performance metric label 5', optional: true }, }, value5: { type: 'long', - _meta: { description: 'Performance metric value', optional: true }, + _meta: { description: 'Performance metric value 5', optional: true }, }, }; diff --git a/src/core/public/core_system.test.mocks.ts b/src/core/public/core_system.test.mocks.ts index b96d81200e6e0b..99f525b1f8aba6 100644 --- a/src/core/public/core_system.test.mocks.ts +++ b/src/core/public/core_system.test.mocks.ts @@ -31,6 +31,11 @@ jest.doMock('@kbn/core-analytics-browser-internal', () => ({ AnalyticsService: AnalyticsServiceConstructor, })); +export const fetchOptionalMemoryInfoMock = jest.fn(); +jest.doMock('./fetch_optional_memory_info', () => ({ + fetchOptionalMemoryInfo: fetchOptionalMemoryInfoMock, +})); + export const MockInjectedMetadataService = injectedMetadataServiceMock.create(); export const InjectedMetadataServiceConstructor = jest .fn() diff --git a/src/core/public/core_system.test.ts b/src/core/public/core_system.test.ts index 7e6d1a30c042e7..0cafa8ed6bd39e 100644 --- a/src/core/public/core_system.test.ts +++ b/src/core/public/core_system.test.ts @@ -37,6 +37,7 @@ import { AnalyticsServiceConstructor, MockAnalyticsService, analyticsServiceStartMock, + fetchOptionalMemoryInfoMock, } from './core_system.test.mocks'; import { CoreSystem } from './core_system'; @@ -274,8 +275,8 @@ describe('#start()', () => { 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', protocol: 'http:', }); @@ -283,12 +284,15 @@ describe('#start()', () => { expect(window.performance.clearMarks).toHaveBeenCalledTimes(1); }); - it('reports the event kibana-loaded and clears marks', async () => { + it('reports the metric event kibana-loaded and clears marks', async () => { await startCore(); - expect(analyticsServiceStartMock.reportEvent).toHaveBeenCalledTimes(1); - expect(analyticsServiceStartMock.reportEvent).toHaveBeenCalledWith(KIBANA_LOADED_EVENT, { - kibana_version: '1.2.3', - protocol: 'http:', + expect(analyticsServiceStartMock.reportEvent).toHaveBeenCalledTimes(2); + expect(analyticsServiceStartMock.reportEvent).toHaveBeenNthCalledWith(2, 'metric', { + eventName: KIBANA_LOADED_EVENT, + meta: { + kibana_version: '1.2.3', + protocol: 'http:', + }, key1: LOAD_START, key2: LOAD_BOOTSTRAP_START, key3: LOAD_CORE_CREATED, @@ -306,22 +310,34 @@ describe('#start()', () => { }); 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(KIBANA_LOADED_EVENT, { - kibana_version: '1.2.3', - duration: 666, + + expect(analyticsServiceStartMock.reportEvent).toHaveBeenCalledTimes(2); + expect(analyticsServiceStartMock.reportEvent).toHaveBeenNthCalledWith(2, '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, + ...performanceMemory, value1: 111, value2: 222, value3: 333, value4: 444, value5: 555, - protocol: 'http:', + duration: 666, }); }); diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index 75a43832b5d38c..34ad06766fabcd 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -26,7 +26,8 @@ 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 { registerMetricEvent, reportMetricEvent } from '@kbn/ebt-tools'; +import { reportMetricEvent } from '@kbn/ebt-tools'; +import { fetchOptionalMemoryInfo } from './fetch_optional_memory_info'; import { CoreSetup, CoreStart } from '.'; import { ChromeService } from './chrome'; import { NotificationsService } from './notifications'; @@ -155,7 +156,7 @@ export class CoreSystem { private reportKibanaLoadedEvent(analytics: AnalyticsServiceStart) { /** - * @deprecated here for backwards compatibility in Fullstory + * @deprecated here for backwards compatibility in FullStory **/ analytics.reportEvent('Loaded Kibana', { kibana_version: this.coreContext.env.packageInfo.version, @@ -170,8 +171,7 @@ export class CoreSystem { protocol: window.location.protocol, }, duration: timing[LOAD_FIRST_NAV], - // @ts-expect-error 2339 - ...performance.memory, + ...fetchOptionalMemoryInfo(), key1: LOAD_START, value1: timing[LOAD_START], key2: LOAD_BOOTSTRAP_START, @@ -204,7 +204,6 @@ export class CoreSystem { const analytics = this.analytics.setup({ injectedMetadata }); this.registerLoadedKibanaEventType(analytics); - registerMetricEvent(analytics); const executionContext = this.executionContext.setup({ analytics }); const http = this.http.setup({ diff --git a/src/core/public/fetch_optional_memory_info.test.ts b/src/core/public/fetch_optional_memory_info.test.ts new file mode 100644 index 00000000000000..4eca4cf0d11de8 --- /dev/null +++ b/src/core/public/fetch_optional_memory_info.test.ts @@ -0,0 +1,35 @@ +/* + * 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 { fetchOptionalMemoryInfo } from './fetch_optional_memory_info'; + +describe('fetchOptionalMemoryInfo', () => { + test('should return undefined if no memory info is available', () => { + expect(fetchOptionalMemoryInfo()).toBeUndefined(); + }); + + test('should return the memory info when available', () => { + // @ts-expect-error 2339 + window.performance.memory = { + get jsHeapSizeLimit() { + return 3; + }, + get totalJSHeapSize() { + return 2; + }, + get usedJSHeapSize() { + return 1; + }, + }; + expect(fetchOptionalMemoryInfo()).toEqual({ + jsHeapSizeLimit: 3, + totalJSHeapSize: 2, + usedJSHeapSize: 1, + }); + }); +}); diff --git a/src/core/public/fetch_optional_memory_info.ts b/src/core/public/fetch_optional_memory_info.ts new file mode 100644 index 00000000000000..957388d55453d5 --- /dev/null +++ b/src/core/public/fetch_optional_memory_info.ts @@ -0,0 +1,42 @@ +/* + * 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. + */ + +/** + * `Performance.memory` output. + * https://developer.mozilla.org/en-US/docs/Web/API/Performance/memory + */ +export interface BrowserPerformanceMemoryInfo { + /** + * The maximum size of the heap, in bytes, that is available to the context. + */ + jsHeapSizeLimit: number; + /** + * The total allocated heap size, in bytes. + */ + totalJSHeapSize: number; + /** + * The currently active segment of JS heap, in bytes. + */ + usedJSHeapSize: number; +} + +/** + * Get performance information from the browser (non-standard property). + * @remarks Only available in Google Chrome and MS Edge for now. + */ +export function fetchOptionalMemoryInfo(): BrowserPerformanceMemoryInfo | undefined { + // @ts-expect-error 2339 + const memory = window.performance.memory; + if (memory) { + return { + jsHeapSizeLimit: memory.jsHeapSizeLimit, + totalJSHeapSize: memory.totalJSHeapSize, + usedJSHeapSize: memory.usedJSHeapSize, + }; + } +} diff --git a/src/core/server/server.ts b/src/core/server/server.ts index 1f6cb6a5272cf4..1549e42f3d79c1 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -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 { registerMetricEvent, reportMetricEvent } from '@kbn/ebt-tools'; +import type { AnalyticsServiceSetup, AnalyticsServiceStart } from '@kbn/core-analytics-server'; +import { reportMetricEvent } from '@kbn/ebt-tools'; import { EnvironmentService, pidConfig } from '@kbn/core-environment-server-internal'; import { ExecutionContextService, @@ -235,7 +236,7 @@ export class Server { const analyticsSetup = this.analytics.setup(); - registerMetricEvent(analyticsSetup); + this.registerKibanaStartedEventType(analyticsSetup); const environmentSetup = this.environment.setup(); @@ -399,25 +400,7 @@ export class Server { startTransaction?.end(); this.uptimePerStep.start = { start: startStartUptime, end: process.uptime() }; - - const ups = this.uptimePerStep; - - reportMetricEvent(this.coreStart.analytics, { - eventName: KIBANA_STARTED_EVENT, - duration: ups.start!.end - ups.constructor!.start, - key1: 'constructor-time', - value1: ups.constructor!.end - ups.constructor!.start, - key2: 'preboot-time', - value2: ups.preboot!.end - ups.preboot!.start, - key3: 'setup-time', - value3: ups.setup!.end - ups.setup!.start, - key4: 'start-time', - value4: ups.start!.end - ups.start!.start, - // backwards compatibility - meta: { - uptime_per_step: this.uptimePerStep, - }, - }); + this.reportKibanaStartedEvents(analyticsStart); return this.coreStart; } @@ -478,4 +461,125 @@ export class Server { this.configService.setSchema(descriptor.path, descriptor.schema); } } + + /** + * 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, + schema: { + uptime_per_step: { + properties: { + constructor: { + properties: { + start: { + type: 'float', + _meta: { + description: + 'Number of seconds the Node.js process has been running until the constructor was called', + }, + }, + end: { + type: 'float', + _meta: { + description: + 'Number of seconds the Node.js process has been running until the constructor finished', + }, + }, + }, + }, + preboot: { + properties: { + start: { + type: 'float', + _meta: { + description: + 'Number of seconds the Node.js process has been running until `preboot` was called', + }, + }, + end: { + type: 'float', + _meta: { + description: + 'Number of seconds the Node.js process has been running until `preboot` finished', + }, + }, + }, + }, + setup: { + properties: { + start: { + type: 'float', + _meta: { + description: + 'Number of seconds the Node.js process has been running until `setup` was called', + }, + }, + end: { + type: 'float', + _meta: { + description: + 'Number of seconds the Node.js process has been running until `setup` finished', + }, + }, + }, + }, + start: { + properties: { + start: { + type: 'float', + _meta: { + description: + 'Number of seconds the Node.js process has been running until `start` was called', + }, + }, + end: { + type: 'float', + _meta: { + description: + 'Number of seconds the Node.js process has been running until `start` finished', + }, + }, + }, + }, + }, + _meta: { + description: + 'Number of seconds the Node.js process has been running until each phase of the server execution is called and finished.', + }, + }, + }, + }); + } + + /** + * 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; + + // Report the metric-shaped KIBANA_STARTED_EVENT. + reportMetricEvent(analyticsStart, { + eventName: KIBANA_STARTED_EVENT, + duration: ups.start!.end - ups.constructor!.start, + key1: 'time-to-constructor', + value1: ups.constructor?.start, + key2: 'constructor-time', + value2: ups.constructor!.end - ups.constructor!.start, + key3: 'preboot-time', + value3: ups.preboot!.end - ups.preboot!.start, + key4: 'setup-time', + value4: ups.setup!.end - ups.setup!.start, + key5: 'start-time', + value5: ups.start!.end - ups.start!.start, + }); + } } diff --git a/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx b/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx index 57325222dc8bf6..c77a4b7d4769c0 100644 --- a/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx +++ b/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx @@ -165,15 +165,17 @@ export class DashboardContainer extends Container { - 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; }) ); diff --git a/test/analytics/fixtures/plugins/analytics_ftr_helpers/common/types.ts b/test/analytics/fixtures/plugins/analytics_ftr_helpers/common/types.ts index 3f66cde1fa3ae0..97e1faff823a48 100644 --- a/test/analytics/fixtures/plugins/analytics_ftr_helpers/common/types.ts +++ b/test/analytics/fixtures/plugins/analytics_ftr_helpers/common/types.ts @@ -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; } 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; + getEvents: ( + takeNumberOfEvents: number, + options?: GetEventsOptions + ) => Promise>>>; /** * 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. diff --git a/test/analytics/fixtures/plugins/analytics_ftr_helpers/public/plugin.test.ts b/test/analytics/fixtures/plugins/analytics_ftr_helpers/public/plugin.test.ts index c5cb6d4df1dd3b..b3f86172b751b4 100644 --- a/test/analytics/fixtures/plugins/analytics_ftr_helpers/public/plugin.test.ts +++ b/test/analytics/fixtures/plugins/analytics_ftr_helpers/public/plugin.test.ts @@ -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 } }, + ]); + }); }); }); diff --git a/test/analytics/fixtures/plugins/analytics_ftr_helpers/server/plugin.ts b/test/analytics/fixtures/plugins/analytics_ftr_helpers/server/plugin.ts index 88cacd2bfd36d0..2e5d93345b07fa 100644 --- a/test/analytics/fixtures/plugins/analytics_ftr_helpers/server/plugin.ts +++ b/test/analytics/fixtures/plugins/analytics_ftr_helpers/server/plugin.ts @@ -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() + ) + ) + ), }), }, }, diff --git a/test/analytics/tests/instrumented_events/from_the_browser/core_context_providers.ts b/test/analytics/tests/instrumented_events/from_the_browser/core_context_providers.ts index 4fda618fffb076..3d0b018156ce73 100644 --- a/test/analytics/tests/instrumented_events/from_the_browser/core_context_providers.ts +++ b/test/analytics/tests/instrumented_events/from_the_browser/core_context_providers.ts @@ -8,7 +8,6 @@ import expect from '@kbn/expect'; import { Event } from '@kbn/core/public'; -import { KIBANA_LOADED_EVENT } from '@kbn/core/public/events'; import { FtrProviderContext } from '../../../services'; export default function ({ getService, getPageObjects }: FtrProviderContext) { @@ -17,10 +16,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const { common } = getPageObjects(['common']); describe('Core Context Providers', () => { - let event: Event; + let event: Event>; before(async () => { await common.navigateToApp('home'); - [event] = await ebtUIHelper.getEvents(1, { eventTypes: [KIBANA_LOADED_EVENT] }); // Get the loaded Kibana event + [event] = await ebtUIHelper.getEvents(1, { eventTypes: ['Loaded Kibana'] }); // Get the loaded Kibana event }); it('should have the properties provided by the "cluster info" context provider', () => { diff --git a/test/analytics/tests/instrumented_events/from_the_browser/loaded_dashboard.ts b/test/analytics/tests/instrumented_events/from_the_browser/loaded_dashboard.ts index 9b243f7c2e8654..88143016324d09 100644 --- a/test/analytics/tests/instrumented_events/from_the_browser/loaded_dashboard.ts +++ b/test/analytics/tests/instrumented_events/from_the_browser/loaded_dashboard.ts @@ -8,9 +8,10 @@ import { GetEventsOptions } from '@kbn/analytics-ftr-helpers-plugin/common/types'; import expect from '@kbn/expect'; -import { DASHBOARD_LOADED_EVENT } from '@kbn/dashboard-plugin/public/events'; 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([ @@ -30,9 +31,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const getEvents = async (count: number, options?: GetEventsOptions) => ebtUIHelper.getEvents(count, { - eventTypes: [DASHBOARD_LOADED_EVENT], + eventTypes: ['metric'], fromTimestamp, withTimeoutMs: 1000, + filters: { 'properties.eventName': { eq: DASHBOARD_LOADED_EVENT } }, ...options, }); @@ -40,13 +42,17 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 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_LOADED_EVENT); + expect(event.event_type).to.eql('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; @@ -82,7 +88,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(); }); @@ -110,7 +116,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(); }); @@ -128,7 +134,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(); }); @@ -152,7 +158,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(); }); @@ -172,7 +178,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(); }); diff --git a/test/analytics/tests/instrumented_events/from_the_browser/loaded_kibana.ts b/test/analytics/tests/instrumented_events/from_the_browser/loaded_kibana.ts index f3f7f980115aae..0c7f39d3617b0c 100644 --- a/test/analytics/tests/instrumented_events/from_the_browser/loaded_kibana.ts +++ b/test/analytics/tests/instrumented_events/from_the_browser/loaded_kibana.ts @@ -6,41 +6,38 @@ * Side Public License, v 1. */ -import { Event } from '@kbn/analytics-client'; import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../services'; -const KIBANA_LOADED_EVENT = 'kibana_loaded'; - export default function ({ getService, getPageObjects }: FtrProviderContext) { const ebtUIHelper = getService('kibana_ebt_ui'); const { common } = getPageObjects(['common']); + const browser = getService('browser'); describe('Loaded Kibana', () => { beforeEach(async () => { await common.navigateToApp('home'); }); - it('should emit the legacy "Loaded Kibana" and new kibana-loaded events', async () => { - const events = await ebtUIHelper.getEvents(2, { - eventTypes: [KIBANA_LOADED_EVENT, 'Loaded Kibana'], - }); + it('should emit the legacy "Loaded Kibana"', async () => { + const [event] = await ebtUIHelper.getEvents(1, { eventTypes: ['Loaded Kibana'] }); - const legacyEvent = events.find( - (e) => e.event_type !== KIBANA_LOADED_EVENT - ) as unknown as Event; - const event = events.find((e) => e.event_type === KIBANA_LOADED_EVENT) as unknown as Event; + 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'); + }); - // Legacy event - expect(legacyEvent.event_type).to.eql('Loaded Kibana'); - expect(legacyEvent.properties).to.have.property('kibana_version'); - expect(legacyEvent.properties.kibana_version).to.be.a('string'); - expect(legacyEvent.properties).to.have.property('protocol'); - expect(legacyEvent.properties.protocol).to.be.a('string'); + it('should emit the new kibana-loaded events', async () => { + const [event] = await ebtUIHelper.getEvents(1, { + eventTypes: ['metric'], + filters: { 'properties.eventName': { eq: 'kibana_loaded' } }, + }); // New event expect(event.event_type).to.eql('metric'); - expect(event.properties.eventName).to.eql(KIBANA_LOADED_EVENT); + expect(event.properties.eventName).to.eql('kibana_loaded'); // meta expect(event.properties).to.have.property('meta'); @@ -64,6 +61,16 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 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('jsHeapSizeLimit'); + expect(event.properties.jsHeapSizeLimit).to.be.a('number'); + expect(event.properties).to.have.property('totalJSHeapSize'); + expect(event.properties.totalJSHeapSize).to.be.a('number'); + expect(event.properties).to.have.property('usedJSHeapSize'); + expect(event.properties.usedJSHeapSize).to.be.a('number'); + } }); }); } diff --git a/test/analytics/tests/instrumented_events/from_the_server/core_context_providers.ts b/test/analytics/tests/instrumented_events/from_the_server/core_context_providers.ts index 9a694c38b8b407..cf781bed5ad0ea 100644 --- a/test/analytics/tests/instrumented_events/from_the_server/core_context_providers.ts +++ b/test/analytics/tests/instrumented_events/from_the_server/core_context_providers.ts @@ -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>; before(async () => { let i = 2; do { diff --git a/test/analytics/tests/instrumented_events/from_the_server/core_overall_status_changed.ts b/test/analytics/tests/instrumented_events/from_the_server/core_overall_status_changed.ts index 07c03465f6c27b..8f904c4c8844c9 100644 --- a/test/analytics/tests/instrumented_events/from_the_server/core_overall_status_changed.ts +++ b/test/analytics/tests/instrumented_events/from_the_server/core_overall_status_changed.ts @@ -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>; + let secondEvent: Event>; before(async () => { [initialEvent, secondEvent] = await ebtServerHelper.getEvents(2, { diff --git a/test/analytics/tests/instrumented_events/from_the_server/kibana_started.ts b/test/analytics/tests/instrumented_events/from_the_server/kibana_started.ts index 9d806e589f4595..533ee329954380 100644 --- a/test/analytics/tests/instrumented_events/from_the_server/kibana_started.ts +++ b/test/analytics/tests/instrumented_events/from_the_server/kibana_started.ts @@ -13,10 +13,9 @@ 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('metric'); - expect(event.properties.event_name).to.eql('kibana_started'); + expect(event.event_type).to.eql('kibana_started'); const uptimePerStep = event.properties.uptime_per_step as Record< 'constructor' | 'preboot' | 'setup' | 'start', Record<'start' | 'end', number> @@ -30,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: ['metric'], + filters: { 'properties.eventName': { eq: 'kibana_started' } }, + }); + expect(event.event_type).to.eql('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'); + }); }); }