diff --git a/package.json b/package.json index ad7b41a63d069..f2223aee0089a 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index b64608d5e718b..7cd774e5173da 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -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", diff --git a/packages/analytics/client/src/analytics_client/analytics_client.ts b/packages/analytics/client/src/analytics_client/analytics_client.ts index e6815b4415254..57741f098c6ac 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 88b60dc100e89..2af29d88b5ceb 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 0c97bee3fdbb7..78b2f792e9e2b 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 c55ed2409da50..03873617af8fc 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 2540ccabd517c..59f18b8f11d4a 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 8ae572ae34528..d3514cb80edf3 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 @@ -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', () => { 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 580fbac92aa5d..938b0b043bc29 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 { 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. diff --git a/packages/core/analytics/core-analytics-server-internal/BUILD.bazel b/packages/core/analytics/core-analytics-server-internal/BUILD.bazel index b7ba4ed35a5a3..0ad590f1ee505 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 0000000000000..3d98cf4392926 --- /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 0000000000000..2609859ae9d8c --- /dev/null +++ b/packages/core/analytics/core-analytics-server-internal/src/analytics_service.test.ts @@ -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), + }); + }); +}); 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 0fa96ebe0ae51..46b0726660e4c 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 { 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 { diff --git a/packages/kbn-ebt-tools/BUILD.bazel b/packages/kbn-ebt-tools/BUILD.bazel new file mode 100644 index 0000000000000..e4c4209361906 --- /dev/null +++ b/packages/kbn-ebt-tools/BUILD.bazel @@ -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"], +) diff --git a/packages/kbn-ebt-tools/README.md b/packages/kbn-ebt-tools/README.md new file mode 100644 index 0000000000000..72aae680b49b8 --- /dev/null +++ b/packages/kbn-ebt-tools/README.md @@ -0,0 +1,3 @@ +# @kbn/ebt-tools + +Shared tools for event based telemetry \ No newline at end of file diff --git a/packages/kbn-ebt-tools/jest.config.js b/packages/kbn-ebt-tools/jest.config.js new file mode 100644 index 0000000000000..56faeaaa0b324 --- /dev/null +++ b/packages/kbn-ebt-tools/jest.config.js @@ -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: ['/packages/kbn-ebt-tools'], +}; diff --git a/packages/kbn-ebt-tools/package.json b/packages/kbn-ebt-tools/package.json new file mode 100644 index 0000000000000..5e5136966b5f9 --- /dev/null +++ b/packages/kbn-ebt-tools/package.json @@ -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 +} \ No newline at end of file diff --git a/packages/kbn-ebt-tools/src/index.ts b/packages/kbn-ebt-tools/src/index.ts new file mode 100644 index 0000000000000..9357513d40f55 --- /dev/null +++ b/packages/kbn-ebt-tools/src/index.ts @@ -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'; diff --git a/packages/kbn-ebt-tools/src/performance_metric_events/helpers.test.ts b/packages/kbn-ebt-tools/src/performance_metric_events/helpers.test.ts new file mode 100644 index 0000000000000..a6ad3970a6a97 --- /dev/null +++ b/packages/kbn-ebt-tools/src/performance_metric_events/helpers.test.ts @@ -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" + `); + }); + }); +}); diff --git a/packages/kbn-ebt-tools/src/performance_metric_events/helpers.ts b/packages/kbn-ebt-tools/src/performance_metric_events/helpers.ts new file mode 100644 index 0000000000000..ed971118687c3 --- /dev/null +++ b/packages/kbn-ebt-tools/src/performance_metric_events/helpers.ts @@ -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 +) { + analytics.registerEventType({ + 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, + eventData: PerformanceMetricEvent +) { + analytics.reportEvent(PERFORMANCE_METRIC_EVENT_TYPE, eventData); +} diff --git a/packages/kbn-ebt-tools/src/performance_metric_events/index.ts b/packages/kbn-ebt-tools/src/performance_metric_events/index.ts new file mode 100644 index 0000000000000..0002b082754dd --- /dev/null +++ b/packages/kbn-ebt-tools/src/performance_metric_events/index.ts @@ -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'; diff --git a/packages/kbn-ebt-tools/src/performance_metric_events/schema.ts b/packages/kbn-ebt-tools/src/performance_metric_events/schema.ts new file mode 100644 index 0000000000000..ed0b3a8eefde5 --- /dev/null +++ b/packages/kbn-ebt-tools/src/performance_metric_events/schema.ts @@ -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; + + /** + * @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 = { + 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 }, + }, +}; diff --git a/packages/kbn-ebt-tools/tsconfig.json b/packages/kbn-ebt-tools/tsconfig.json new file mode 100644 index 0000000000000..bb93370ed412f --- /dev/null +++ b/packages/kbn-ebt-tools/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.bazel.json", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "emitDeclarationOnly": true, + "outDir": "target_types", + "types": [ + "jest", + "node" + ] + }, + "include": [ + "src/**/*" + ] +} diff --git a/src/core/public/core_system.test.ts b/src/core/public/core_system.test.ts index eb8b564c2e9ea..f9f9f6e90afdd 100644 --- a/src/core/public/core_system.test.ts +++ b/src/core/public/core_system.test.ts @@ -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: LOAD_BOOTSTRAP_START, + startTime: 222, + }, + { + detail: LOAD_CORE_CREATED, + startTime: 333, }, { - detail: 'bootstrap_started', - startTime: 123, + 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, }); }); diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index 0ba7d4f1fc285..79d9f7b192f85 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -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 { + if (!performance) return {}; const reportData: Record = {}; 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: { diff --git a/src/core/public/events.ts b/src/core/public/events.ts new file mode 100644 index 0000000000000..e2f5d48ddfe3d --- /dev/null +++ b/src/core/public/events.ts @@ -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'; diff --git a/src/core/public/fetch_optional_memory_info.test.ts b/src/core/public/fetch_optional_memory_info.test.ts index f92fad9c14d63..4eca4cf0d11de 100644 --- a/src/core/public/fetch_optional_memory_info.test.ts +++ b/src/core/public/fetch_optional_memory_info.test.ts @@ -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, }); }); }); diff --git a/src/core/public/fetch_optional_memory_info.ts b/src/core/public/fetch_optional_memory_info.ts index b18f3ca2698da..957388d55453d 100644 --- a/src/core/public/fetch_optional_memory_info.ts +++ b/src/core/public/fetch_optional_memory_info.ts @@ -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, }; } } diff --git a/src/core/public/kbn_bootstrap.ts b/src/core/public/kbn_bootstrap.ts index e97c7e58a48d5..0359f9e4d6520 100644 --- a/src/core/public/kbn_bootstrap.ts +++ b/src/core/public/kbn_bootstrap.ts @@ -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( diff --git a/src/core/server/server.ts b/src/core/server/server.ts index 2b0180796b604..42878ca66e614 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 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), + }); + } } diff --git a/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx b/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx index aa2588c1b3b54..9f59e8637ba1c 100644 --- a/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx +++ b/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx @@ -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 void; + onDataLoaded?: (data: DashboardLoadedInfo) => void; } interface State { @@ -270,7 +274,7 @@ class DashboardGridUi extends React.Component { 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, diff --git a/src/plugins/dashboard/public/application/embeddable/types.ts b/src/plugins/dashboard/public/application/embeddable/types.ts index f8e37b07d721c..b64fe70f9eb9c 100644 --- a/src/plugins/dashboard/public/application/embeddable/types.ts +++ b/src/plugins/dashboard/public/application/embeddable/types.ts @@ -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; } diff --git a/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.tsx b/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.tsx index 318db746fbe42..ebb91ba57fd46 100644 --- a/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.tsx +++ b/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.tsx @@ -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 { diff --git a/src/plugins/dashboard/public/events.ts b/src/plugins/dashboard/public/events.ts new file mode 100644 index 0000000000000..aa28cb109abec --- /dev/null +++ b/src/plugins/dashboard/public/events.ts @@ -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'; diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index a9564ac0db49a..ba06709d69f7b 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -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({ - 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, { @@ -312,8 +287,6 @@ export class DashboardPlugin }, }; - this.registerEvents(core.analytics); - core.application.register(app); urlForwarding.forwardApp( DashboardConstants.DASHBOARDS_ID, diff --git a/test/analytics/fixtures/plugins/analytics_ftr_helpers/common/fetch_events.ts b/test/analytics/fixtures/plugins/analytics_ftr_helpers/common/fetch_events.ts index bf6980adcab87..5a5eeba914b7b 100644 --- a/test/analytics/fixtures/plugins/analytics_ftr_helpers/common/fetch_events.ts +++ b/test/analytics/fixtures/plugins/analytics_ftr_helpers/common/fetch_events.ts @@ -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 { - 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 3f66cde1fa3ae..97e1faff823a4 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 c5cb6d4df1dd3..b3f86172b751b 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 88cacd2bfd36d..2e5d93345b07f 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 6543b3c7955ef..3d0b018156ce7 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 @@ -16,7 +16,7 @@ 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: ['Loaded Kibana'] }); // Get the loaded Kibana event 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 178bed7cbbbe5..0522ab5b41c2c 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 @@ -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); }); /** 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 7ed47db35cbeb..e4ac2346b5893 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 @@ -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; + 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'); } }); }); 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 9a694c38b8b40..cf781bed5ad0e 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 07c03465f6c27..8f904c4c8844c 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 5380d4e392e52..d1b59ce9d4e9c 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,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'); + }); }); } diff --git a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx index 59f679d041024..0388dcd86d397 100644 --- a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx +++ b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx @@ -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({ diff --git a/yarn.lock b/yarn.lock index f441496e28989..561c545c659a7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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 ""