Skip to content

Commit

Permalink
add flexible EBT Performance Metric Schema (#136395)
Browse files Browse the repository at this point in the history
  • Loading branch information
Liza Katz committed Aug 3, 2022
1 parent 041bd90 commit af45ef8
Show file tree
Hide file tree
Showing 48 changed files with 1,222 additions and 212 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions packages/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
16 changes: 10 additions & 6 deletions packages/analytics/client/src/analytics_client/analytics_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -43,7 +43,7 @@ import { ContextService } from './context_service';
import { schemaToIoTs, validateSchema } from '../schema/validation';

interface EventDebugLogMeta extends LogMeta {
ebt_event: Event;
ebt_event: Event<unknown>;
}

export class AnalyticsClient implements IAnalyticsClient {
Expand All @@ -65,7 +65,7 @@ export class AnalyticsClient implements IAnalyticsClient {
private readonly shipperRegistered$ = new Subject<void>();
private readonly eventTypeRegistry = new Map<
EventType,
EventTypeOpts<unknown> & { validator?: Type<Record<string, unknown>> }
EventTypeOpts<unknown> & { validator?: Mixed }
>();
private readonly contextService: ContextService;
private readonly context$ = new BehaviorSubject<Partial<EventContext>>({});
Expand All @@ -88,7 +88,7 @@ export class AnalyticsClient implements IAnalyticsClient {
this.reportEnqueuedEventsWhenClientIsReady();
}

public reportEvent = <EventTypeData extends Record<string, unknown>>(
public reportEvent = <EventTypeData extends object>(
eventType: EventType,
eventData: EventTypeData
) => {
Expand Down Expand Up @@ -119,14 +119,18 @@ export class AnalyticsClient implements IAnalyticsClient {

// If the validator is registered (dev-mode only), perform the validation.
if (eventTypeOpts.validator) {
validateSchema(`Event Type '${eventType}'`, eventTypeOpts.validator, eventData);
validateSchema<EventTypeData>(
`Event Type '${eventType}'`,
eventTypeOpts.validator,
eventData
);
}

const event: Event = {
timestamp,
event_type: eventType,
context: this.context$.value,
properties: eventData,
properties: eventData as unknown as Record<string, unknown>,
};

// debug-logging before checking the opt-in status to help during development
Expand Down
2 changes: 1 addition & 1 deletion packages/analytics/client/src/analytics_client/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ export interface IAnalyticsClient {
* @param eventType The event type registered via the `registerEventType` API.
* @param eventData The properties matching the schema declared in the `registerEventType` API.
*/
reportEvent: <EventTypeData extends Record<string, unknown>>(
reportEvent: <EventTypeData extends object>(
eventType: EventType,
eventData: EventTypeData
) => void;
Expand Down
4 changes: 2 additions & 2 deletions packages/analytics/client/src/events/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ export interface TelemetryCounter {
/**
* Definition of the full event structure
*/
export interface Event {
export interface Event<Properties = Record<string, unknown>> {
/**
* The time the event was generated in ISO format.
*/
Expand All @@ -120,7 +120,7 @@ export interface Event {
/**
* The specific properties of the event type.
*/
properties: Record<string, unknown>;
properties: Properties;
/**
* The {@link EventContext} enriched during the processing pipeline.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const FULLSTORY_RESERVED_PROPERTIES = [
'pageName',
];

export function formatPayload(context: Record<string, unknown>): Record<string, unknown> {
export function formatPayload(context: object): Record<string, unknown> {
// format context keys as required for env vars, see docs: https://help.fullstory.com/hc/en-us/articles/360020623234
return Object.fromEntries(
Object.entries(context)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
]
Expand All @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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 () => {
Expand All @@ -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', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,15 @@ NPM_MODULE_EXTRA_FILES = [
RUNTIME_DEPS = [
"@npm//rxjs",
"//packages/analytics/client",
"//packages/kbn-ebt-tools",
]

TYPES_DEPS = [
"@npm//@types/node",
"@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",
]
Expand Down
Loading

0 comments on commit af45ef8

Please sign in to comment.