mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[EBT] Combine packages (#186048)
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
f820f78807
commit
ab21d4fee4
181 changed files with 1423 additions and 1589 deletions
6
.github/CODEOWNERS
vendored
6
.github/CODEOWNERS
vendored
|
@ -35,14 +35,9 @@ packages/kbn-ambient-ftr-types @elastic/kibana-operations @elastic/appex-qa
|
|||
packages/kbn-ambient-storybook-types @elastic/kibana-operations
|
||||
packages/kbn-ambient-ui-types @elastic/kibana-operations
|
||||
packages/kbn-analytics @elastic/kibana-core
|
||||
packages/analytics/client @elastic/kibana-core
|
||||
packages/analytics/utils/analytics_collection_utils @elastic/kibana-core
|
||||
test/analytics/plugins/analytics_ftr_helpers @elastic/kibana-core
|
||||
test/analytics/plugins/analytics_plugin_a @elastic/kibana-core
|
||||
packages/analytics/shippers/elastic_v3/browser @elastic/kibana-core
|
||||
packages/analytics/shippers/elastic_v3/common @elastic/kibana-core
|
||||
packages/analytics/shippers/elastic_v3/server @elastic/kibana-core
|
||||
packages/analytics/shippers/fullstory @elastic/kibana-core
|
||||
packages/kbn-apm-config-loader @elastic/kibana-core @vigneshshanmugam
|
||||
x-pack/plugins/observability_solution/apm_data_access @elastic/obs-knowledge-team @elastic/obs-ux-infra_services-team
|
||||
packages/kbn-apm-data-view @elastic/obs-ux-infra_services-team
|
||||
|
@ -378,6 +373,7 @@ packages/kbn-discover-utils @elastic/kibana-data-discovery
|
|||
packages/kbn-doc-links @elastic/docs
|
||||
packages/kbn-docs-utils @elastic/kibana-operations
|
||||
packages/kbn-dom-drag-drop @elastic/kibana-visualizations @elastic/kibana-data-discovery
|
||||
packages/analytics/ebt @elastic/kibana-core
|
||||
packages/kbn-ebt-tools @elastic/kibana-core
|
||||
x-pack/packages/security-solution/ecs_data_quality_dashboard @elastic/security-threat-hunting-explore
|
||||
x-pack/plugins/ecs_data_quality_dashboard @elastic/security-threat-hunting-explore
|
||||
|
|
|
@ -166,14 +166,9 @@
|
|||
"@kbn/alerts-restricted-fixtures-plugin": "link:x-pack/test/alerting_api_integration/common/plugins/alerts_restricted",
|
||||
"@kbn/alerts-ui-shared": "link:packages/kbn-alerts-ui-shared",
|
||||
"@kbn/analytics": "link:packages/kbn-analytics",
|
||||
"@kbn/analytics-client": "link:packages/analytics/client",
|
||||
"@kbn/analytics-collection-utils": "link:packages/analytics/utils/analytics_collection_utils",
|
||||
"@kbn/analytics-ftr-helpers-plugin": "link:test/analytics/plugins/analytics_ftr_helpers",
|
||||
"@kbn/analytics-plugin-a-plugin": "link:test/analytics/plugins/analytics_plugin_a",
|
||||
"@kbn/analytics-shippers-elastic-v3-browser": "link:packages/analytics/shippers/elastic_v3/browser",
|
||||
"@kbn/analytics-shippers-elastic-v3-common": "link:packages/analytics/shippers/elastic_v3/common",
|
||||
"@kbn/analytics-shippers-elastic-v3-server": "link:packages/analytics/shippers/elastic_v3/server",
|
||||
"@kbn/analytics-shippers-fullstory": "link:packages/analytics/shippers/fullstory",
|
||||
"@kbn/apm-config-loader": "link:packages/kbn-apm-config-loader",
|
||||
"@kbn/apm-data-access-plugin": "link:x-pack/plugins/observability_solution/apm_data_access",
|
||||
"@kbn/apm-data-view": "link:packages/kbn-apm-data-view",
|
||||
|
@ -433,6 +428,7 @@
|
|||
"@kbn/discover-utils": "link:packages/kbn-discover-utils",
|
||||
"@kbn/doc-links": "link:packages/kbn-doc-links",
|
||||
"@kbn/dom-drag-drop": "link:packages/kbn-dom-drag-drop",
|
||||
"@kbn/ebt": "link:packages/analytics/ebt",
|
||||
"@kbn/ebt-tools": "link:packages/kbn-ebt-tools",
|
||||
"@kbn/ecs-data-quality-dashboard": "link:x-pack/packages/security-solution/ecs_data_quality_dashboard",
|
||||
"@kbn/ecs-data-quality-dashboard-plugin": "link:x-pack/plugins/ecs_data_quality_dashboard",
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
# @kbn/analytics-*
|
||||
|
||||
This module implements the Analytics client used for Event-Based Telemetry. The intention of the client is to be usable on both: the UI and the Server sides.
|
||||
|
||||
## Client
|
||||
|
||||
Holds the public APIs to report events, enrich the events' context and set up the transport mechanisms. Refer to the [client's docs](./client/README.md) for more information.
|
||||
|
||||
## Prebuilt shippers
|
||||
|
||||
Elastic-approved shippers are available as `@kbn/analytics-shippers-*` packages. Refer to the [shippers' docs](./shippers/README.md) for more information.
|
11
packages/analytics/ebt/README.md
Normal file
11
packages/analytics/ebt/README.md
Normal file
|
@ -0,0 +1,11 @@
|
|||
# @kbn/ebt/*
|
||||
|
||||
This module implements the Analytics client used for Event-Based Telemetry. The intention of the client is to be usable on both: the UI and the Server sides.
|
||||
|
||||
## Client
|
||||
|
||||
`@kbn/ebt/client` holds the public APIs to report events, enrich the events' context and set up the transport mechanisms. Refer to the [client's docs](./client/README.md) for more information.
|
||||
|
||||
## Prebuilt shippers
|
||||
|
||||
Elastic-approved shippers are available as `@kbn/ebt/shippers/*` packages. Refer to the [shippers' docs](./shippers/README.md) for more information.
|
|
@ -1,4 +1,4 @@
|
|||
# @kbn/analytics-client
|
||||
# @kbn/ebt/client
|
||||
|
||||
This module implements the Analytics client used for Event-Based Telemetry. The intention of the client is to be usable on both: the UI and the Server sides.
|
||||
|
||||
|
@ -7,7 +7,7 @@ This module implements the Analytics client used for Event-Based Telemetry. The
|
|||
It all starts by creating the client with the `createAnalytics` API:
|
||||
|
||||
```typescript
|
||||
import { createAnalytics } from '@kbn/analytics-client';
|
||||
import { createAnalytics } from '@kbn/ebt/client';
|
||||
|
||||
const analytics = createAnalytics({
|
||||
// Set to `true` when running in developer mode.
|
||||
|
@ -167,7 +167,7 @@ import type {
|
|||
EventContext,
|
||||
IShipper,
|
||||
TelemetryCounter
|
||||
} from '@kbn/analytics-client';
|
||||
} from '@kbn/ebt/client';
|
||||
|
||||
class MyVeryOwnShipper implements IShipper {
|
||||
constructor(myOptions: MyOptions, initContext: AnalyticsClientInitContext) {
|
|
@ -33,6 +33,7 @@ export type {
|
|||
ShipperName,
|
||||
// Types for the registerContextProvider API
|
||||
ContextProviderOpts,
|
||||
ContextProviderName,
|
||||
// Types for the registerEventType API
|
||||
EventTypeOpts,
|
||||
} from './src/analytics_client';
|
|
@ -8,7 +8,6 @@
|
|||
|
||||
// eslint-disable-next-line max-classes-per-file
|
||||
import { Subject, lastValueFrom, take, toArray } from 'rxjs';
|
||||
import { fakeSchedulers } from 'rxjs-marbles/jest';
|
||||
import type { MockedLogger } from '@kbn/logging-mocks';
|
||||
import { loggerMock } from '@kbn/logging-mocks';
|
||||
import { AnalyticsClient } from './analytics_client';
|
||||
|
@ -21,7 +20,7 @@ describe('AnalyticsClient', () => {
|
|||
let logger: MockedLogger;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers({ legacyFakeTimers: true });
|
||||
jest.useFakeTimers();
|
||||
logger = loggerMock.create();
|
||||
analyticsClient = new AnalyticsClient({
|
||||
logger,
|
||||
|
@ -338,76 +337,67 @@ describe('AnalyticsClient', () => {
|
|||
expect(optIn).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
test(
|
||||
'Spreads the context updates to the shipper (only after opt-in)',
|
||||
fakeSchedulers((advance) => {
|
||||
const extendContextMock = jest.fn();
|
||||
analyticsClient.registerShipper(MockedShipper, { extendContextMock });
|
||||
expect(extendContextMock).toHaveBeenCalledTimes(0); // Not until we have opt-in
|
||||
analyticsClient.optIn({ global: { enabled: true } });
|
||||
advance(10);
|
||||
expect(extendContextMock).toHaveBeenCalledWith({}); // The initial context
|
||||
test('Spreads the context updates to the shipper (only after opt-in)', async () => {
|
||||
const extendContextMock = jest.fn();
|
||||
analyticsClient.registerShipper(MockedShipper, { extendContextMock });
|
||||
expect(extendContextMock).toHaveBeenCalledTimes(0); // Not until we have opt-in
|
||||
analyticsClient.optIn({ global: { enabled: true } });
|
||||
await jest.advanceTimersByTimeAsync(10);
|
||||
expect(extendContextMock).toHaveBeenCalledWith({}); // The initial context
|
||||
|
||||
const context$ = new Subject<{ a_field: boolean }>();
|
||||
analyticsClient.registerContextProvider({
|
||||
name: 'contextProviderA',
|
||||
schema: {
|
||||
a_field: {
|
||||
type: 'boolean',
|
||||
_meta: {
|
||||
description: 'a_field description',
|
||||
},
|
||||
const context$ = new Subject<{ a_field: boolean }>();
|
||||
analyticsClient.registerContextProvider({
|
||||
name: 'contextProviderA',
|
||||
schema: {
|
||||
a_field: {
|
||||
type: 'boolean',
|
||||
_meta: {
|
||||
description: 'a_field description',
|
||||
},
|
||||
},
|
||||
context$,
|
||||
});
|
||||
},
|
||||
context$,
|
||||
});
|
||||
|
||||
context$.next({ a_field: true });
|
||||
expect(extendContextMock).toHaveBeenCalledWith({ a_field: true }); // After update
|
||||
})
|
||||
);
|
||||
context$.next({ a_field: true });
|
||||
expect(extendContextMock).toHaveBeenCalledWith({ a_field: true }); // After update
|
||||
});
|
||||
|
||||
test(
|
||||
'Does not spread the context if opt-in === false',
|
||||
fakeSchedulers((advance) => {
|
||||
const extendContextMock = jest.fn();
|
||||
analyticsClient.registerShipper(MockedShipper, { extendContextMock });
|
||||
expect(extendContextMock).toHaveBeenCalledTimes(0); // Not until we have opt-in
|
||||
analyticsClient.optIn({ global: { enabled: false } });
|
||||
advance(10);
|
||||
expect(extendContextMock).toHaveBeenCalledTimes(0); // Not until we have opt-in
|
||||
})
|
||||
);
|
||||
test('Does not spread the context if opt-in === false', async () => {
|
||||
const extendContextMock = jest.fn();
|
||||
analyticsClient.registerShipper(MockedShipper, { extendContextMock });
|
||||
expect(extendContextMock).toHaveBeenCalledTimes(0); // Not until we have opt-in
|
||||
analyticsClient.optIn({ global: { enabled: false } });
|
||||
await jest.advanceTimersByTimeAsync(10);
|
||||
expect(extendContextMock).toHaveBeenCalledTimes(0); // Not until we have opt-in
|
||||
});
|
||||
|
||||
test(
|
||||
'Handles errors in the shipper',
|
||||
fakeSchedulers(async (advance) => {
|
||||
const optInMock = jest.fn().mockImplementation(() => {
|
||||
throw new Error('Something went terribly wrong');
|
||||
});
|
||||
const extendContextMock = jest.fn().mockImplementation(() => {
|
||||
throw new Error('Something went terribly wrong');
|
||||
});
|
||||
const shutdownMock = jest.fn().mockImplementation(() => {
|
||||
throw new Error('Something went terribly wrong');
|
||||
});
|
||||
analyticsClient.registerShipper(MockedShipper, {
|
||||
optInMock,
|
||||
extendContextMock,
|
||||
shutdownMock,
|
||||
});
|
||||
expect(() => analyticsClient.optIn({ global: { enabled: true } })).not.toThrow();
|
||||
advance(10);
|
||||
expect(optInMock).toHaveBeenCalledWith(true);
|
||||
expect(extendContextMock).toHaveBeenCalledWith({}); // The initial context
|
||||
expect(logger.warn).toHaveBeenCalledWith(
|
||||
`Shipper "${MockedShipper.shipperName}" failed to extend the context`,
|
||||
expect.any(Error)
|
||||
);
|
||||
await expect(analyticsClient.shutdown()).resolves.toBeUndefined();
|
||||
expect(shutdownMock).toHaveBeenCalled();
|
||||
})
|
||||
);
|
||||
test('Handles errors in the shipper', async () => {
|
||||
const optInMock = jest.fn().mockImplementation(() => {
|
||||
throw new Error('Something went terribly wrong');
|
||||
});
|
||||
const extendContextMock = jest.fn().mockImplementation(() => {
|
||||
throw new Error('Something went terribly wrong');
|
||||
});
|
||||
const shutdownMock = jest.fn().mockImplementation(() => {
|
||||
throw new Error('Something went terribly wrong');
|
||||
});
|
||||
analyticsClient.registerShipper(MockedShipper, {
|
||||
optInMock,
|
||||
extendContextMock,
|
||||
shutdownMock,
|
||||
});
|
||||
expect(() => analyticsClient.optIn({ global: { enabled: true } })).not.toThrow();
|
||||
await jest.advanceTimersByTimeAsync(10);
|
||||
expect(optInMock).toHaveBeenCalledWith(true);
|
||||
expect(extendContextMock).toHaveBeenCalledWith({}); // The initial context
|
||||
expect(logger.warn).toHaveBeenCalledWith(
|
||||
`Shipper "${MockedShipper.shipperName}" failed to extend the context`,
|
||||
expect.any(Error)
|
||||
);
|
||||
await expect(analyticsClient.shutdown()).resolves.toBeUndefined();
|
||||
expect(shutdownMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('ContextProvider APIs', () => {
|
||||
|
@ -633,89 +623,86 @@ describe('AnalyticsClient', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
test(
|
||||
'Sends events from the internal queue when there are shippers and an opt-in response is true',
|
||||
fakeSchedulers(async (advance) => {
|
||||
const telemetryCounterPromise = lastValueFrom(
|
||||
analyticsClient.telemetryCounter$.pipe(take(3 + 2), toArray()) // Waiting for 3 enqueued + 2 batch-shipped events
|
||||
);
|
||||
test('Sends events from the internal queue when there are shippers and an opt-in response is true', async () => {
|
||||
const telemetryCounterPromise = lastValueFrom(
|
||||
analyticsClient.telemetryCounter$.pipe(take(3 + 2), toArray()) // Waiting for 3 enqueued + 2 batch-shipped events
|
||||
);
|
||||
|
||||
// Send multiple events of 1 type to test the grouping logic as well
|
||||
analyticsClient.reportEvent('event-type-a', { a_field: 'a' });
|
||||
analyticsClient.reportEvent('event-type-b', { b_field: 100 });
|
||||
analyticsClient.reportEvent('event-type-a', { a_field: 'b' });
|
||||
// Send multiple events of 1 type to test the grouping logic as well
|
||||
analyticsClient.reportEvent('event-type-a', { a_field: 'a' });
|
||||
analyticsClient.reportEvent('event-type-b', { b_field: 100 });
|
||||
analyticsClient.reportEvent('event-type-a', { a_field: 'b' });
|
||||
|
||||
// As proven in the previous test, the events are still enqueued.
|
||||
// Let's register a shipper and opt-in to test the dequeue logic.
|
||||
const reportEventsMock = jest.fn();
|
||||
analyticsClient.registerShipper(MockedShipper1, { reportEventsMock });
|
||||
analyticsClient.optIn({ global: { enabled: true } });
|
||||
advance(10);
|
||||
// As proven in the previous test, the events are still enqueued.
|
||||
// Let's register a shipper and opt-in to test the dequeue logic.
|
||||
const reportEventsMock = jest.fn();
|
||||
analyticsClient.registerShipper(MockedShipper1, { reportEventsMock });
|
||||
analyticsClient.optIn({ global: { enabled: true } });
|
||||
await jest.advanceTimersByTimeAsync(10);
|
||||
|
||||
expect(reportEventsMock).toHaveBeenCalledTimes(2);
|
||||
expect(reportEventsMock).toHaveBeenNthCalledWith(1, [
|
||||
{
|
||||
context: {},
|
||||
event_type: 'event-type-a',
|
||||
properties: { a_field: 'a' },
|
||||
timestamp: expect.any(String),
|
||||
},
|
||||
{
|
||||
context: {},
|
||||
event_type: 'event-type-a',
|
||||
properties: { a_field: 'b' },
|
||||
timestamp: expect.any(String),
|
||||
},
|
||||
]);
|
||||
expect(reportEventsMock).toHaveBeenNthCalledWith(2, [
|
||||
{
|
||||
context: {},
|
||||
event_type: 'event-type-b',
|
||||
properties: { b_field: 100 },
|
||||
timestamp: expect.any(String),
|
||||
},
|
||||
]);
|
||||
expect(reportEventsMock).toHaveBeenCalledTimes(2);
|
||||
expect(reportEventsMock).toHaveBeenNthCalledWith(1, [
|
||||
{
|
||||
context: {},
|
||||
event_type: 'event-type-a',
|
||||
properties: { a_field: 'a' },
|
||||
timestamp: expect.any(String),
|
||||
},
|
||||
{
|
||||
context: {},
|
||||
event_type: 'event-type-a',
|
||||
properties: { a_field: 'b' },
|
||||
timestamp: expect.any(String),
|
||||
},
|
||||
]);
|
||||
expect(reportEventsMock).toHaveBeenNthCalledWith(2, [
|
||||
{
|
||||
context: {},
|
||||
event_type: 'event-type-b',
|
||||
properties: { b_field: 100 },
|
||||
timestamp: expect.any(String),
|
||||
},
|
||||
]);
|
||||
|
||||
// Expect 3 enqueued events, and 2 sent_to_shipper batched requests
|
||||
await expect(telemetryCounterPromise).resolves.toEqual([
|
||||
{
|
||||
type: 'enqueued',
|
||||
source: 'client',
|
||||
event_type: 'event-type-a',
|
||||
code: 'enqueued',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
type: 'enqueued',
|
||||
source: 'client',
|
||||
event_type: 'event-type-b',
|
||||
code: 'enqueued',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
type: 'enqueued',
|
||||
source: 'client',
|
||||
event_type: 'event-type-a',
|
||||
code: 'enqueued',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
type: 'sent_to_shipper',
|
||||
source: 'client',
|
||||
event_type: 'event-type-a',
|
||||
code: 'OK',
|
||||
count: 2,
|
||||
},
|
||||
{
|
||||
type: 'sent_to_shipper',
|
||||
source: 'client',
|
||||
event_type: 'event-type-b',
|
||||
code: 'OK',
|
||||
count: 1,
|
||||
},
|
||||
]);
|
||||
})
|
||||
);
|
||||
// Expect 3 enqueued events, and 2 sent_to_shipper batched requests
|
||||
await expect(telemetryCounterPromise).resolves.toEqual([
|
||||
{
|
||||
type: 'enqueued',
|
||||
source: 'client',
|
||||
event_type: 'event-type-a',
|
||||
code: 'enqueued',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
type: 'enqueued',
|
||||
source: 'client',
|
||||
event_type: 'event-type-b',
|
||||
code: 'enqueued',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
type: 'enqueued',
|
||||
source: 'client',
|
||||
event_type: 'event-type-a',
|
||||
code: 'enqueued',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
type: 'sent_to_shipper',
|
||||
source: 'client',
|
||||
event_type: 'event-type-a',
|
||||
code: 'OK',
|
||||
count: 2,
|
||||
},
|
||||
{
|
||||
type: 'sent_to_shipper',
|
||||
source: 'client',
|
||||
event_type: 'event-type-b',
|
||||
code: 'OK',
|
||||
count: 1,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('Discards events from the internal queue when there are shippers and an opt-in response is false', async () => {
|
||||
const telemetryCounterPromise = lastValueFrom(
|
||||
|
@ -823,259 +810,250 @@ describe('AnalyticsClient', () => {
|
|||
expect(analyticsClient['internalEventQueue$'].observed).toBe(false);
|
||||
});
|
||||
|
||||
test(
|
||||
'Discards only one type of the enqueued events based on event_type config',
|
||||
fakeSchedulers(async (advance) => {
|
||||
const telemetryCounterPromise = lastValueFrom(
|
||||
analyticsClient.telemetryCounter$.pipe(take(3 + 1), toArray()) // Waiting for 3 enqueued + 1 batch-shipped events
|
||||
);
|
||||
test('Discards only one type of the enqueued events based on event_type config', async () => {
|
||||
const telemetryCounterPromise = lastValueFrom(
|
||||
analyticsClient.telemetryCounter$.pipe(take(3 + 1), toArray()) // Waiting for 3 enqueued + 1 batch-shipped events
|
||||
);
|
||||
|
||||
// Send multiple events of 1 type to test the grouping logic as well
|
||||
analyticsClient.reportEvent('event-type-a', { a_field: 'a' });
|
||||
analyticsClient.reportEvent('event-type-b', { b_field: 100 });
|
||||
analyticsClient.reportEvent('event-type-a', { a_field: 'b' });
|
||||
// Send multiple events of 1 type to test the grouping logic as well
|
||||
analyticsClient.reportEvent('event-type-a', { a_field: 'a' });
|
||||
analyticsClient.reportEvent('event-type-b', { b_field: 100 });
|
||||
analyticsClient.reportEvent('event-type-a', { a_field: 'b' });
|
||||
|
||||
const reportEventsMock = jest.fn();
|
||||
analyticsClient.registerShipper(MockedShipper1, { reportEventsMock });
|
||||
analyticsClient.optIn({
|
||||
global: { enabled: true },
|
||||
event_types: { ['event-type-a']: { enabled: false } },
|
||||
});
|
||||
advance(10);
|
||||
const reportEventsMock = jest.fn();
|
||||
analyticsClient.registerShipper(MockedShipper1, { reportEventsMock });
|
||||
analyticsClient.optIn({
|
||||
global: { enabled: true },
|
||||
event_types: { ['event-type-a']: { enabled: false } },
|
||||
});
|
||||
await jest.advanceTimersByTimeAsync(10);
|
||||
|
||||
expect(reportEventsMock).toHaveBeenCalledTimes(1);
|
||||
expect(reportEventsMock).toHaveBeenNthCalledWith(1, [
|
||||
{
|
||||
context: {},
|
||||
event_type: 'event-type-b',
|
||||
properties: { b_field: 100 },
|
||||
timestamp: expect.any(String),
|
||||
},
|
||||
]);
|
||||
expect(reportEventsMock).toHaveBeenCalledTimes(1);
|
||||
expect(reportEventsMock).toHaveBeenNthCalledWith(1, [
|
||||
{
|
||||
context: {},
|
||||
event_type: 'event-type-b',
|
||||
properties: { b_field: 100 },
|
||||
timestamp: expect.any(String),
|
||||
},
|
||||
]);
|
||||
|
||||
// Expect 3 enqueued events, and 1 sent_to_shipper batched request
|
||||
await expect(telemetryCounterPromise).resolves.toEqual([
|
||||
{
|
||||
type: 'enqueued',
|
||||
source: 'client',
|
||||
event_type: 'event-type-a',
|
||||
code: 'enqueued',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
type: 'enqueued',
|
||||
source: 'client',
|
||||
event_type: 'event-type-b',
|
||||
code: 'enqueued',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
type: 'enqueued',
|
||||
source: 'client',
|
||||
event_type: 'event-type-a',
|
||||
code: 'enqueued',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
type: 'sent_to_shipper',
|
||||
source: 'client',
|
||||
event_type: 'event-type-b',
|
||||
code: 'OK',
|
||||
count: 1,
|
||||
},
|
||||
]);
|
||||
})
|
||||
);
|
||||
// Expect 3 enqueued events, and 1 sent_to_shipper batched request
|
||||
await expect(telemetryCounterPromise).resolves.toEqual([
|
||||
{
|
||||
type: 'enqueued',
|
||||
source: 'client',
|
||||
event_type: 'event-type-a',
|
||||
code: 'enqueued',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
type: 'enqueued',
|
||||
source: 'client',
|
||||
event_type: 'event-type-b',
|
||||
code: 'enqueued',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
type: 'enqueued',
|
||||
source: 'client',
|
||||
event_type: 'event-type-a',
|
||||
code: 'enqueued',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
type: 'sent_to_shipper',
|
||||
source: 'client',
|
||||
event_type: 'event-type-b',
|
||||
code: 'OK',
|
||||
count: 1,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test(
|
||||
'Discards the event at the shipper level (for a specific event)',
|
||||
fakeSchedulers(async (advance) => {
|
||||
const telemetryCounterPromise = lastValueFrom(
|
||||
analyticsClient.telemetryCounter$.pipe(take(3 + 2), toArray()) // Waiting for 3 enqueued + 2 batch-shipped events
|
||||
);
|
||||
test('Discards the event at the shipper level (for a specific event)', async () => {
|
||||
const telemetryCounterPromise = lastValueFrom(
|
||||
analyticsClient.telemetryCounter$.pipe(take(3 + 2), toArray()) // Waiting for 3 enqueued + 2 batch-shipped events
|
||||
);
|
||||
|
||||
// Send multiple events of 1 type to test the grouping logic as well
|
||||
analyticsClient.reportEvent('event-type-a', { a_field: 'a' });
|
||||
analyticsClient.reportEvent('event-type-b', { b_field: 100 });
|
||||
analyticsClient.reportEvent('event-type-a', { a_field: 'b' });
|
||||
// Send multiple events of 1 type to test the grouping logic as well
|
||||
analyticsClient.reportEvent('event-type-a', { a_field: 'a' });
|
||||
analyticsClient.reportEvent('event-type-b', { b_field: 100 });
|
||||
analyticsClient.reportEvent('event-type-a', { a_field: 'b' });
|
||||
|
||||
// Register 2 shippers and set 1 of them as disabled for event-type-a
|
||||
const reportEventsMock1 = jest.fn();
|
||||
const reportEventsMock2 = jest.fn();
|
||||
analyticsClient.registerShipper(MockedShipper1, { reportEventsMock: reportEventsMock1 });
|
||||
analyticsClient.registerShipper(MockedShipper2, { reportEventsMock: reportEventsMock2 });
|
||||
analyticsClient.optIn({
|
||||
global: { enabled: true },
|
||||
event_types: {
|
||||
['event-type-a']: { enabled: true, shippers: { [MockedShipper2.shipperName]: false } },
|
||||
},
|
||||
});
|
||||
advance(10);
|
||||
// Register 2 shippers and set 1 of them as disabled for event-type-a
|
||||
const reportEventsMock1 = jest.fn();
|
||||
const reportEventsMock2 = jest.fn();
|
||||
analyticsClient.registerShipper(MockedShipper1, { reportEventsMock: reportEventsMock1 });
|
||||
analyticsClient.registerShipper(MockedShipper2, { reportEventsMock: reportEventsMock2 });
|
||||
analyticsClient.optIn({
|
||||
global: { enabled: true },
|
||||
event_types: {
|
||||
['event-type-a']: { enabled: true, shippers: { [MockedShipper2.shipperName]: false } },
|
||||
},
|
||||
});
|
||||
await jest.advanceTimersByTimeAsync(10);
|
||||
|
||||
expect(reportEventsMock1).toHaveBeenCalledTimes(2);
|
||||
expect(reportEventsMock1).toHaveBeenNthCalledWith(1, [
|
||||
{
|
||||
context: {},
|
||||
event_type: 'event-type-a',
|
||||
properties: { a_field: 'a' },
|
||||
timestamp: expect.any(String),
|
||||
},
|
||||
{
|
||||
context: {},
|
||||
event_type: 'event-type-a',
|
||||
properties: { a_field: 'b' },
|
||||
timestamp: expect.any(String),
|
||||
},
|
||||
]);
|
||||
expect(reportEventsMock1).toHaveBeenNthCalledWith(2, [
|
||||
{
|
||||
context: {},
|
||||
event_type: 'event-type-b',
|
||||
properties: { b_field: 100 },
|
||||
timestamp: expect.any(String),
|
||||
},
|
||||
]);
|
||||
expect(reportEventsMock2).toHaveBeenCalledTimes(1);
|
||||
expect(reportEventsMock2).toHaveBeenNthCalledWith(1, [
|
||||
{
|
||||
context: {},
|
||||
event_type: 'event-type-b',
|
||||
properties: { b_field: 100 },
|
||||
timestamp: expect.any(String),
|
||||
},
|
||||
]);
|
||||
expect(reportEventsMock1).toHaveBeenCalledTimes(2);
|
||||
expect(reportEventsMock1).toHaveBeenNthCalledWith(1, [
|
||||
{
|
||||
context: {},
|
||||
event_type: 'event-type-a',
|
||||
properties: { a_field: 'a' },
|
||||
timestamp: expect.any(String),
|
||||
},
|
||||
{
|
||||
context: {},
|
||||
event_type: 'event-type-a',
|
||||
properties: { a_field: 'b' },
|
||||
timestamp: expect.any(String),
|
||||
},
|
||||
]);
|
||||
expect(reportEventsMock1).toHaveBeenNthCalledWith(2, [
|
||||
{
|
||||
context: {},
|
||||
event_type: 'event-type-b',
|
||||
properties: { b_field: 100 },
|
||||
timestamp: expect.any(String),
|
||||
},
|
||||
]);
|
||||
expect(reportEventsMock2).toHaveBeenCalledTimes(1);
|
||||
expect(reportEventsMock2).toHaveBeenNthCalledWith(1, [
|
||||
{
|
||||
context: {},
|
||||
event_type: 'event-type-b',
|
||||
properties: { b_field: 100 },
|
||||
timestamp: expect.any(String),
|
||||
},
|
||||
]);
|
||||
|
||||
// Expect 3 enqueued events, and 2 sent_to_shipper batched requests
|
||||
await expect(telemetryCounterPromise).resolves.toEqual([
|
||||
{
|
||||
type: 'enqueued',
|
||||
source: 'client',
|
||||
event_type: 'event-type-a',
|
||||
code: 'enqueued',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
type: 'enqueued',
|
||||
source: 'client',
|
||||
event_type: 'event-type-b',
|
||||
code: 'enqueued',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
type: 'enqueued',
|
||||
source: 'client',
|
||||
event_type: 'event-type-a',
|
||||
code: 'enqueued',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
type: 'sent_to_shipper',
|
||||
source: 'client',
|
||||
event_type: 'event-type-a',
|
||||
code: 'OK',
|
||||
count: 2,
|
||||
},
|
||||
{
|
||||
type: 'sent_to_shipper',
|
||||
source: 'client',
|
||||
event_type: 'event-type-b',
|
||||
code: 'OK',
|
||||
count: 1,
|
||||
},
|
||||
]);
|
||||
})
|
||||
);
|
||||
// Expect 3 enqueued events, and 2 sent_to_shipper batched requests
|
||||
await expect(telemetryCounterPromise).resolves.toEqual([
|
||||
{
|
||||
type: 'enqueued',
|
||||
source: 'client',
|
||||
event_type: 'event-type-a',
|
||||
code: 'enqueued',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
type: 'enqueued',
|
||||
source: 'client',
|
||||
event_type: 'event-type-b',
|
||||
code: 'enqueued',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
type: 'enqueued',
|
||||
source: 'client',
|
||||
event_type: 'event-type-a',
|
||||
code: 'enqueued',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
type: 'sent_to_shipper',
|
||||
source: 'client',
|
||||
event_type: 'event-type-a',
|
||||
code: 'OK',
|
||||
count: 2,
|
||||
},
|
||||
{
|
||||
type: 'sent_to_shipper',
|
||||
source: 'client',
|
||||
event_type: 'event-type-b',
|
||||
code: 'OK',
|
||||
count: 1,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test(
|
||||
'Discards all the events at the shipper level (globally disabled)',
|
||||
fakeSchedulers(async (advance) => {
|
||||
const telemetryCounterPromise = lastValueFrom(
|
||||
analyticsClient.telemetryCounter$.pipe(take(3 + 2), toArray()) // Waiting for 3 enqueued + 2 batch-shipped events
|
||||
);
|
||||
test('Discards all the events at the shipper level (globally disabled)', async () => {
|
||||
const telemetryCounterPromise = lastValueFrom(
|
||||
analyticsClient.telemetryCounter$.pipe(take(3 + 2), toArray()) // Waiting for 3 enqueued + 2 batch-shipped events
|
||||
);
|
||||
|
||||
// Send multiple events of 1 type to test the grouping logic as well
|
||||
analyticsClient.reportEvent('event-type-a', { a_field: 'a' });
|
||||
analyticsClient.reportEvent('event-type-b', { b_field: 100 });
|
||||
analyticsClient.reportEvent('event-type-a', { a_field: 'b' });
|
||||
// Send multiple events of 1 type to test the grouping logic as well
|
||||
analyticsClient.reportEvent('event-type-a', { a_field: 'a' });
|
||||
analyticsClient.reportEvent('event-type-b', { b_field: 100 });
|
||||
analyticsClient.reportEvent('event-type-a', { a_field: 'b' });
|
||||
|
||||
// Register 2 shippers and set 1 of them as globally disabled
|
||||
const reportEventsMock1 = jest.fn();
|
||||
const reportEventsMock2 = jest.fn();
|
||||
analyticsClient.registerShipper(MockedShipper1, { reportEventsMock: reportEventsMock1 });
|
||||
analyticsClient.registerShipper(MockedShipper2, { reportEventsMock: reportEventsMock2 });
|
||||
analyticsClient.optIn({
|
||||
global: { enabled: true, shippers: { [MockedShipper2.shipperName]: false } },
|
||||
event_types: {
|
||||
['event-type-a']: { enabled: true },
|
||||
},
|
||||
});
|
||||
advance(10);
|
||||
// Register 2 shippers and set 1 of them as globally disabled
|
||||
const reportEventsMock1 = jest.fn();
|
||||
const reportEventsMock2 = jest.fn();
|
||||
analyticsClient.registerShipper(MockedShipper1, { reportEventsMock: reportEventsMock1 });
|
||||
analyticsClient.registerShipper(MockedShipper2, { reportEventsMock: reportEventsMock2 });
|
||||
analyticsClient.optIn({
|
||||
global: { enabled: true, shippers: { [MockedShipper2.shipperName]: false } },
|
||||
event_types: {
|
||||
['event-type-a']: { enabled: true },
|
||||
},
|
||||
});
|
||||
await jest.advanceTimersByTimeAsync(10);
|
||||
|
||||
expect(reportEventsMock1).toHaveBeenCalledTimes(2);
|
||||
expect(reportEventsMock1).toHaveBeenNthCalledWith(1, [
|
||||
{
|
||||
context: {},
|
||||
event_type: 'event-type-a',
|
||||
properties: { a_field: 'a' },
|
||||
timestamp: expect.any(String),
|
||||
},
|
||||
{
|
||||
context: {},
|
||||
event_type: 'event-type-a',
|
||||
properties: { a_field: 'b' },
|
||||
timestamp: expect.any(String),
|
||||
},
|
||||
]);
|
||||
expect(reportEventsMock1).toHaveBeenNthCalledWith(2, [
|
||||
{
|
||||
context: {},
|
||||
event_type: 'event-type-b',
|
||||
properties: { b_field: 100 },
|
||||
timestamp: expect.any(String),
|
||||
},
|
||||
]);
|
||||
expect(reportEventsMock2).toHaveBeenCalledTimes(0);
|
||||
expect(reportEventsMock1).toHaveBeenCalledTimes(2);
|
||||
expect(reportEventsMock1).toHaveBeenNthCalledWith(1, [
|
||||
{
|
||||
context: {},
|
||||
event_type: 'event-type-a',
|
||||
properties: { a_field: 'a' },
|
||||
timestamp: expect.any(String),
|
||||
},
|
||||
{
|
||||
context: {},
|
||||
event_type: 'event-type-a',
|
||||
properties: { a_field: 'b' },
|
||||
timestamp: expect.any(String),
|
||||
},
|
||||
]);
|
||||
expect(reportEventsMock1).toHaveBeenNthCalledWith(2, [
|
||||
{
|
||||
context: {},
|
||||
event_type: 'event-type-b',
|
||||
properties: { b_field: 100 },
|
||||
timestamp: expect.any(String),
|
||||
},
|
||||
]);
|
||||
expect(reportEventsMock2).toHaveBeenCalledTimes(0);
|
||||
|
||||
// Expect 3 enqueued events, and 2 sent_to_shipper batched requests
|
||||
await expect(telemetryCounterPromise).resolves.toEqual([
|
||||
{
|
||||
type: 'enqueued',
|
||||
source: 'client',
|
||||
event_type: 'event-type-a',
|
||||
code: 'enqueued',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
type: 'enqueued',
|
||||
source: 'client',
|
||||
event_type: 'event-type-b',
|
||||
code: 'enqueued',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
type: 'enqueued',
|
||||
source: 'client',
|
||||
event_type: 'event-type-a',
|
||||
code: 'enqueued',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
type: 'sent_to_shipper',
|
||||
source: 'client',
|
||||
event_type: 'event-type-a',
|
||||
code: 'OK',
|
||||
count: 2,
|
||||
},
|
||||
{
|
||||
type: 'sent_to_shipper',
|
||||
source: 'client',
|
||||
event_type: 'event-type-b',
|
||||
code: 'OK',
|
||||
count: 1,
|
||||
},
|
||||
]);
|
||||
})
|
||||
);
|
||||
// Expect 3 enqueued events, and 2 sent_to_shipper batched requests
|
||||
await expect(telemetryCounterPromise).resolves.toEqual([
|
||||
{
|
||||
type: 'enqueued',
|
||||
source: 'client',
|
||||
event_type: 'event-type-a',
|
||||
code: 'enqueued',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
type: 'enqueued',
|
||||
source: 'client',
|
||||
event_type: 'event-type-b',
|
||||
code: 'enqueued',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
type: 'enqueued',
|
||||
source: 'client',
|
||||
event_type: 'event-type-a',
|
||||
code: 'enqueued',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
type: 'sent_to_shipper',
|
||||
source: 'client',
|
||||
event_type: 'event-type-a',
|
||||
code: 'OK',
|
||||
count: 2,
|
||||
},
|
||||
{
|
||||
type: 'sent_to_shipper',
|
||||
source: 'client',
|
||||
event_type: 'event-type-b',
|
||||
code: 'OK',
|
||||
count: 1,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('Discards incoming events when opt-in response is false', async () => {
|
||||
// Set OptIn and shipper first to test the "once-set up" scenario
|
57
packages/analytics/ebt/index.ts
Normal file
57
packages/analytics/ebt/index.ts
Normal file
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// Exporting the types here as a utility only
|
||||
// The recommended way of using this library is to import from the subdirectories /client, /shippers/*
|
||||
// The reason is to avoid leaking server-side code to the browser, and vice-versa
|
||||
export type {
|
||||
AnalyticsClient,
|
||||
// Types for the constructor
|
||||
AnalyticsClientInitContext,
|
||||
// Types for the registerShipper API
|
||||
ShipperClassConstructor,
|
||||
RegisterShipperOpts,
|
||||
// Types for the optIn API
|
||||
OptInConfig,
|
||||
OptInConfigPerType,
|
||||
ShipperName,
|
||||
// Types for the registerContextProvider API
|
||||
ContextProviderOpts,
|
||||
ContextProviderName,
|
||||
// Types for the registerEventType API
|
||||
EventTypeOpts,
|
||||
// Events
|
||||
Event,
|
||||
EventContext,
|
||||
EventType,
|
||||
TelemetryCounter,
|
||||
TelemetryCounterType,
|
||||
// Schema
|
||||
RootSchema,
|
||||
SchemaObject,
|
||||
SchemaArray,
|
||||
SchemaChildValue,
|
||||
SchemaMeta,
|
||||
SchemaValue,
|
||||
SchemaMetaOptional,
|
||||
PossibleSchemaTypes,
|
||||
AllowedSchemaBooleanTypes,
|
||||
AllowedSchemaNumberTypes,
|
||||
AllowedSchemaStringTypes,
|
||||
AllowedSchemaTypes,
|
||||
// Shippers
|
||||
IShipper,
|
||||
} from './client';
|
||||
export type { ElasticV3ShipperOptions } from './shippers/elastic_v3/common';
|
||||
export type { ElasticV3BrowserShipper } from './shippers/elastic_v3/browser';
|
||||
export type { ElasticV3ServerShipper } from './shippers/elastic_v3/server';
|
||||
export type {
|
||||
FullStoryShipperConfig,
|
||||
FullStoryShipper,
|
||||
FullStorySnippetConfig,
|
||||
} from './shippers/fullstory';
|
|
@ -7,7 +7,7 @@
|
|||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test/jest_node',
|
||||
rootDir: '../../../',
|
||||
roots: ['<rootDir>/packages/analytics/client'],
|
||||
preset: '@kbn/test',
|
||||
rootDir: '../../..',
|
||||
roots: ['<rootDir>/packages/analytics/ebt'],
|
||||
};
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"type": "shared-common",
|
||||
"id": "@kbn/analytics-client",
|
||||
"id": "@kbn/ebt",
|
||||
"owner": "@elastic/kibana-core"
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
{
|
||||
"name": "@kbn/analytics-client",
|
||||
"name": "@kbn/ebt",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"author": "Kibana Core",
|
||||
"license": "SSPL-1.0 OR Elastic License 2.0"
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
# @kbn/analytics-shippers-*
|
||||
# @kbn/ebt/shippers/*
|
||||
|
||||
This directory holds the implementation of the _built-in_ shippers provided by the Analytics client. At the moment, the shippers are:
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
# @kbn/analytics-shippers-elastic-v3-browser
|
||||
# @kbn/ebt/shippers/elastic_v3/browser
|
||||
|
||||
UI-side implementation of the Elastic V3 shipper for the `@kbn/analytics-client`.
|
||||
UI-side implementation of the Elastic V3 shipper for the `@kbn/ebt/client`.
|
||||
|
||||
## How to use it
|
||||
|
||||
This module is intended to be used **on the browser only**. Due to the nature of the UI events, they are usually more scattered in time, and we can assume a much lower load than the server. For that reason, it doesn't apply the necessary backpressure mechanisms to prevent the server from getting overloaded with too many events neither identifies if the server sits behind a firewall to discard any incoming events. Refer to `@kbn/analytics-shippers-elastic-v3-server` for the server-side implementation.
|
||||
This module is intended to be used **on the browser only**. Due to the nature of the UI events, they are usually more scattered in time, and we can assume a much lower load than the server. For that reason, it doesn't apply the necessary backpressure mechanisms to prevent the server from getting overloaded with too many events neither identifies if the server sits behind a firewall to discard any incoming events. Refer to `@kbn/ebt/shippers/elastic_v3/server` for the server-side implementation.
|
||||
|
||||
```typescript
|
||||
import { ElasticV3BrowserShipper } from "@kbn/analytics-shippers-elastic-v3-browser";
|
||||
import { ElasticV3BrowserShipper } from "@kbn/ebt/shippers/elastic_v3/browser";
|
||||
|
||||
analytics.registerShipper(ElasticV3BrowserShipper, { channelName: 'myChannel', version: '1.0.0' });
|
||||
```
|
|
@ -6,5 +6,5 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export type { ElasticV3ShipperOptions } from '@kbn/analytics-shippers-elastic-v3-common';
|
||||
export type { ElasticV3ShipperOptions } from '../common';
|
||||
export { ElasticV3BrowserShipper } from './src/browser_shipper';
|
|
@ -7,9 +7,8 @@
|
|||
*/
|
||||
|
||||
import { loggerMock } from '@kbn/logging-mocks';
|
||||
import type { AnalyticsClientInitContext, Event } from '@kbn/analytics-client';
|
||||
import { fakeSchedulers } from 'rxjs-marbles/jest';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import type { AnalyticsClientInitContext, Event } from '../../../../client';
|
||||
import { ElasticV3BrowserShipper } from './browser_shipper';
|
||||
|
||||
describe('ElasticV3BrowserShipper', () => {
|
||||
|
@ -33,7 +32,7 @@ describe('ElasticV3BrowserShipper', () => {
|
|||
let fetchMock: jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers({ legacyFakeTimers: true });
|
||||
jest.useFakeTimers();
|
||||
|
||||
fetchMock = jest.fn().mockResolvedValue({
|
||||
status: 200,
|
||||
|
@ -109,37 +108,32 @@ describe('ElasticV3BrowserShipper', () => {
|
|||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test(
|
||||
'calls to reportEvents do not call `fetch` after 1s because no optIn value is set yet',
|
||||
fakeSchedulers((advance) => {
|
||||
shipper.reportEvents(events);
|
||||
advance(1000);
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
})
|
||||
);
|
||||
test('calls to reportEvents do not call `fetch` after 1s because no optIn value is set yet', async () => {
|
||||
shipper.reportEvents(events);
|
||||
await jest.advanceTimersByTimeAsync(1000);
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test(
|
||||
'calls to reportEvents call `fetch` after 1s when optIn value is set to true',
|
||||
fakeSchedulers(async (advance) => {
|
||||
shipper.reportEvents(events);
|
||||
shipper.optIn(true);
|
||||
const counter = firstValueFrom(shipper.telemetryCounter$);
|
||||
advance(1000);
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'https://telemetry-staging.elastic.co/v3/send/test-channel',
|
||||
{
|
||||
body: '{"timestamp":"2020-01-01T00:00:00.000Z","event_type":"test-event-type","context":{},"properties":{}}\n',
|
||||
headers: {
|
||||
'content-type': 'application/x-ndjson',
|
||||
'x-elastic-cluster-id': 'UNKNOWN',
|
||||
'x-elastic-stack-version': '1.2.3',
|
||||
},
|
||||
keepalive: true,
|
||||
method: 'POST',
|
||||
query: { debug: true },
|
||||
}
|
||||
);
|
||||
await expect(counter).resolves.toMatchInlineSnapshot(`
|
||||
test('calls to reportEvents call `fetch` after 1s when optIn value is set to true', async () => {
|
||||
shipper.reportEvents(events);
|
||||
shipper.optIn(true);
|
||||
const counter = firstValueFrom(shipper.telemetryCounter$);
|
||||
await jest.advanceTimersByTimeAsync(1000);
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'https://telemetry-staging.elastic.co/v3/send/test-channel',
|
||||
{
|
||||
body: '{"timestamp":"2020-01-01T00:00:00.000Z","event_type":"test-event-type","context":{},"properties":{}}\n',
|
||||
headers: {
|
||||
'content-type': 'application/x-ndjson',
|
||||
'x-elastic-cluster-id': 'UNKNOWN',
|
||||
'x-elastic-stack-version': '1.2.3',
|
||||
},
|
||||
keepalive: true,
|
||||
method: 'POST',
|
||||
query: { debug: true },
|
||||
}
|
||||
);
|
||||
await expect(counter).resolves.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"code": "200",
|
||||
"count": 1,
|
||||
|
@ -148,43 +142,35 @@ describe('ElasticV3BrowserShipper', () => {
|
|||
"type": "succeeded",
|
||||
}
|
||||
`);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'calls to reportEvents do not call `fetch` after 1s when optIn value is set to false',
|
||||
fakeSchedulers((advance) => {
|
||||
shipper.reportEvents(events);
|
||||
shipper.optIn(false);
|
||||
advance(1000);
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
})
|
||||
);
|
||||
test('calls to reportEvents do not call `fetch` after 1s when optIn value is set to false', async () => {
|
||||
shipper.reportEvents(events);
|
||||
shipper.optIn(false);
|
||||
await jest.advanceTimersByTimeAsync(1000);
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test(
|
||||
'calls to flush forces the client to send all the pending events',
|
||||
fakeSchedulers(async (advance) => {
|
||||
shipper.optIn(true);
|
||||
shipper.reportEvents(events);
|
||||
const counter = firstValueFrom(shipper.telemetryCounter$);
|
||||
const promise = shipper.flush();
|
||||
advance(0); // bufferWhen requires some sort of fake scheduling to advance (but we are not advancing 1s)
|
||||
await promise;
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'https://telemetry-staging.elastic.co/v3/send/test-channel',
|
||||
{
|
||||
body: '{"timestamp":"2020-01-01T00:00:00.000Z","event_type":"test-event-type","context":{},"properties":{}}\n',
|
||||
headers: {
|
||||
'content-type': 'application/x-ndjson',
|
||||
'x-elastic-cluster-id': 'UNKNOWN',
|
||||
'x-elastic-stack-version': '1.2.3',
|
||||
},
|
||||
keepalive: true,
|
||||
method: 'POST',
|
||||
query: { debug: true },
|
||||
}
|
||||
);
|
||||
await expect(counter).resolves.toMatchInlineSnapshot(`
|
||||
test('calls to flush forces the client to send all the pending events', async () => {
|
||||
shipper.optIn(true);
|
||||
shipper.reportEvents(events);
|
||||
const counter = firstValueFrom(shipper.telemetryCounter$);
|
||||
await shipper.flush();
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'https://telemetry-staging.elastic.co/v3/send/test-channel',
|
||||
{
|
||||
body: '{"timestamp":"2020-01-01T00:00:00.000Z","event_type":"test-event-type","context":{},"properties":{}}\n',
|
||||
headers: {
|
||||
'content-type': 'application/x-ndjson',
|
||||
'x-elastic-cluster-id': 'UNKNOWN',
|
||||
'x-elastic-stack-version': '1.2.3',
|
||||
},
|
||||
keepalive: true,
|
||||
method: 'POST',
|
||||
query: { debug: true },
|
||||
}
|
||||
);
|
||||
await expect(counter).resolves.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"code": "200",
|
||||
"count": 1,
|
||||
|
@ -193,8 +179,7 @@ describe('ElasticV3BrowserShipper', () => {
|
|||
"type": "succeeded",
|
||||
}
|
||||
`);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('calls to flush resolve immediately if there is nothing to send', async () => {
|
||||
shipper.optIn(true);
|
||||
|
@ -243,55 +228,50 @@ describe('ElasticV3BrowserShipper', () => {
|
|||
`);
|
||||
});
|
||||
|
||||
test(
|
||||
'does not add the query.debug: true property to the request if the shipper is not set with the debug flag',
|
||||
fakeSchedulers((advance) => {
|
||||
shipper = new ElasticV3BrowserShipper(
|
||||
{ version: '1.2.3', channelName: 'test-channel' },
|
||||
initContext
|
||||
);
|
||||
shipper.reportEvents(events);
|
||||
shipper.optIn(true);
|
||||
advance(1000);
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'https://telemetry-staging.elastic.co/v3/send/test-channel',
|
||||
{
|
||||
body: '{"timestamp":"2020-01-01T00:00:00.000Z","event_type":"test-event-type","context":{},"properties":{}}\n',
|
||||
headers: {
|
||||
'content-type': 'application/x-ndjson',
|
||||
'x-elastic-cluster-id': 'UNKNOWN',
|
||||
'x-elastic-stack-version': '1.2.3',
|
||||
},
|
||||
keepalive: true,
|
||||
method: 'POST',
|
||||
}
|
||||
);
|
||||
})
|
||||
);
|
||||
test('does not add the query.debug: true property to the request if the shipper is not set with the debug flag', async () => {
|
||||
shipper = new ElasticV3BrowserShipper(
|
||||
{ version: '1.2.3', channelName: 'test-channel' },
|
||||
initContext
|
||||
);
|
||||
shipper.reportEvents(events);
|
||||
shipper.optIn(true);
|
||||
await jest.advanceTimersByTimeAsync(1000);
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'https://telemetry-staging.elastic.co/v3/send/test-channel',
|
||||
{
|
||||
body: '{"timestamp":"2020-01-01T00:00:00.000Z","event_type":"test-event-type","context":{},"properties":{}}\n',
|
||||
headers: {
|
||||
'content-type': 'application/x-ndjson',
|
||||
'x-elastic-cluster-id': 'UNKNOWN',
|
||||
'x-elastic-stack-version': '1.2.3',
|
||||
},
|
||||
keepalive: true,
|
||||
method: 'POST',
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'handles when the fetch request fails',
|
||||
fakeSchedulers(async (advance) => {
|
||||
fetchMock.mockRejectedValueOnce(new Error('Failed to fetch'));
|
||||
shipper.reportEvents(events);
|
||||
shipper.optIn(true);
|
||||
const counter = firstValueFrom(shipper.telemetryCounter$);
|
||||
advance(1000);
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'https://telemetry-staging.elastic.co/v3/send/test-channel',
|
||||
{
|
||||
body: '{"timestamp":"2020-01-01T00:00:00.000Z","event_type":"test-event-type","context":{},"properties":{}}\n',
|
||||
headers: {
|
||||
'content-type': 'application/x-ndjson',
|
||||
'x-elastic-cluster-id': 'UNKNOWN',
|
||||
'x-elastic-stack-version': '1.2.3',
|
||||
},
|
||||
keepalive: true,
|
||||
method: 'POST',
|
||||
query: { debug: true },
|
||||
}
|
||||
);
|
||||
await expect(counter).resolves.toMatchInlineSnapshot(`
|
||||
test('handles when the fetch request fails', async () => {
|
||||
fetchMock.mockRejectedValueOnce(new Error('Failed to fetch'));
|
||||
shipper.reportEvents(events);
|
||||
shipper.optIn(true);
|
||||
const counter = firstValueFrom(shipper.telemetryCounter$);
|
||||
await jest.advanceTimersByTimeAsync(1000);
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'https://telemetry-staging.elastic.co/v3/send/test-channel',
|
||||
{
|
||||
body: '{"timestamp":"2020-01-01T00:00:00.000Z","event_type":"test-event-type","context":{},"properties":{}}\n',
|
||||
headers: {
|
||||
'content-type': 'application/x-ndjson',
|
||||
'x-elastic-cluster-id': 'UNKNOWN',
|
||||
'x-elastic-stack-version': '1.2.3',
|
||||
},
|
||||
keepalive: true,
|
||||
method: 'POST',
|
||||
query: { debug: true },
|
||||
}
|
||||
);
|
||||
await expect(counter).resolves.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"code": "Failed to fetch",
|
||||
"count": 1,
|
||||
|
@ -300,36 +280,33 @@ describe('ElasticV3BrowserShipper', () => {
|
|||
"type": "failed",
|
||||
}
|
||||
`);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'handles when the fetch request fails (request completes but not OK response)',
|
||||
fakeSchedulers(async (advance) => {
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: false,
|
||||
status: 400,
|
||||
text: () => Promise.resolve('{"status": "not ok"}'),
|
||||
});
|
||||
shipper.reportEvents(events);
|
||||
shipper.optIn(true);
|
||||
const counter = firstValueFrom(shipper.telemetryCounter$);
|
||||
advance(1000);
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'https://telemetry-staging.elastic.co/v3/send/test-channel',
|
||||
{
|
||||
body: '{"timestamp":"2020-01-01T00:00:00.000Z","event_type":"test-event-type","context":{},"properties":{}}\n',
|
||||
headers: {
|
||||
'content-type': 'application/x-ndjson',
|
||||
'x-elastic-cluster-id': 'UNKNOWN',
|
||||
'x-elastic-stack-version': '1.2.3',
|
||||
},
|
||||
keepalive: true,
|
||||
method: 'POST',
|
||||
query: { debug: true },
|
||||
}
|
||||
);
|
||||
await expect(counter).resolves.toMatchInlineSnapshot(`
|
||||
test('handles when the fetch request fails (request completes but not OK response)', async () => {
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: false,
|
||||
status: 400,
|
||||
text: () => Promise.resolve('{"status": "not ok"}'),
|
||||
});
|
||||
shipper.reportEvents(events);
|
||||
shipper.optIn(true);
|
||||
const counter = firstValueFrom(shipper.telemetryCounter$);
|
||||
await jest.advanceTimersByTimeAsync(1000);
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'https://telemetry-staging.elastic.co/v3/send/test-channel',
|
||||
{
|
||||
body: '{"timestamp":"2020-01-01T00:00:00.000Z","event_type":"test-event-type","context":{},"properties":{}}\n',
|
||||
headers: {
|
||||
'content-type': 'application/x-ndjson',
|
||||
'x-elastic-cluster-id': 'UNKNOWN',
|
||||
'x-elastic-stack-version': '1.2.3',
|
||||
},
|
||||
keepalive: true,
|
||||
method: 'POST',
|
||||
query: { debug: true },
|
||||
}
|
||||
);
|
||||
await expect(counter).resolves.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"code": "400",
|
||||
"count": 1,
|
||||
|
@ -338,6 +315,5 @@ describe('ElasticV3BrowserShipper', () => {
|
|||
"type": "failed",
|
||||
}
|
||||
`);
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
|
@ -23,14 +23,9 @@ import type {
|
|||
EventContext,
|
||||
IShipper,
|
||||
TelemetryCounter,
|
||||
} from '@kbn/analytics-client';
|
||||
import { ElasticV3ShipperOptions, ErrorWithCode } from '@kbn/analytics-shippers-elastic-v3-common';
|
||||
import {
|
||||
buildHeaders,
|
||||
buildUrl,
|
||||
createTelemetryCounterHelper,
|
||||
eventsToNDJSON,
|
||||
} from '@kbn/analytics-shippers-elastic-v3-common';
|
||||
} from '../../../../client';
|
||||
import { ElasticV3ShipperOptions, ErrorWithCode } from '../../common';
|
||||
import { buildHeaders, buildUrl, createTelemetryCounterHelper, eventsToNDJSON } from '../../common';
|
||||
|
||||
/**
|
||||
* Elastic V3 shipper to use in the browser.
|
|
@ -1,4 +1,4 @@
|
|||
# @kbn/analytics-shippers-elastic-v3-common
|
||||
# @kbn/ebt/shippers/elastic_v3/common
|
||||
|
||||
This package holds the common code for the Elastic V3 shippers:
|
||||
|
||||
|
@ -7,4 +7,4 @@ This package holds the common code for the Elastic V3 shippers:
|
|||
- `eventsToNdjson` utility converts any array of events to NDJSON format.
|
||||
- `reportTelemetryCounters` helps with building the TelemetryCounter to emit after processing an event.
|
||||
|
||||
It should be considered an internal package and should not be used other than by the shipper implementations: `@kbn/analytics-shippers-elastic-v3-browser` and `@kbn/analytics-shippers-elastic-v3-server`
|
||||
It should be considered an internal package and should not be used other than by the shipper implementations: `@kbn/ebt/shippers/elastic_v3/browser` and `@kbn/ebt/shippers/elastic_v3/server`
|
|
@ -6,7 +6,7 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { Event } from '@kbn/analytics-client';
|
||||
import type { Event } from '../../../../client';
|
||||
import { eventsToNDJSON } from './events_to_ndjson';
|
||||
|
||||
describe('eventsToNDJSON', () => {
|
|
@ -6,7 +6,7 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { Event } from '@kbn/analytics-client';
|
||||
import type { Event } from '../../../../client';
|
||||
|
||||
/**
|
||||
* Converts an array of events to a single ndjson string.
|
|
@ -7,7 +7,7 @@
|
|||
*/
|
||||
|
||||
import { firstValueFrom, Subject, take, toArray } from 'rxjs';
|
||||
import type { Event, TelemetryCounter } from '@kbn/analytics-client';
|
||||
import type { Event, TelemetryCounter } from '../../../../client';
|
||||
import { createTelemetryCounterHelper } from './report_telemetry_counters';
|
||||
|
||||
describe('reportTelemetryCounters', () => {
|
|
@ -7,7 +7,7 @@
|
|||
*/
|
||||
|
||||
import type { Subject } from 'rxjs';
|
||||
import type { Event, TelemetryCounter, TelemetryCounterType } from '@kbn/analytics-client';
|
||||
import type { Event, TelemetryCounter, TelemetryCounterType } from '../../../../client';
|
||||
|
||||
/**
|
||||
* Creates a telemetry counter helper to make it easier to generate them
|
|
@ -1,13 +1,13 @@
|
|||
# @kbn/analytics-shippers-elastic-v3-server
|
||||
# @kbn/ebt/shippers/elastic_v3/server
|
||||
|
||||
Server-side implementation of the Elastic V3 shipper for the `@kbn/analytics-client`.
|
||||
Server-side implementation of the Elastic V3 shipper for the `@kbn/ebt/client`.
|
||||
|
||||
## How to use it
|
||||
|
||||
This module is intended to be used **on the server-side only**. It is specially designed to apply the necessary backpressure mechanisms to prevent the server from getting overloaded with too many events and identify if the server sits behind a firewall to discard any incoming events. Refer to `@kbn/analytics-shippers-elastic-v3-browser` for the browser-side implementation.
|
||||
This module is intended to be used **on the server-side only**. It is specially designed to apply the necessary backpressure mechanisms to prevent the server from getting overloaded with too many events and identify if the server sits behind a firewall to discard any incoming events. Refer to `@kbn/ebt/shippers/elastic_v3/browser` for the browser-side implementation.
|
||||
|
||||
```typescript
|
||||
import { ElasticV3ServerShipper } from "@kbn/analytics-shippers-elastic-v3-server";
|
||||
import { ElasticV3ServerShipper } from "@kbn/ebt/shippers/elastic_v3/server";
|
||||
|
||||
analytics.registerShipper(ElasticV3ServerShipper, { channelName: 'myChannel', version: '1.0.0' });
|
||||
```
|
|
@ -6,5 +6,5 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export type { ElasticV3ShipperOptions } from '@kbn/analytics-shippers-elastic-v3-common';
|
||||
export type { ElasticV3ShipperOptions } from '../common';
|
||||
export { ElasticV3ServerShipper } from './src/server_shipper';
|
|
@ -0,0 +1,571 @@
|
|||
/*
|
||||
* 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 { loggerMock } from '@kbn/logging-mocks';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import type { AnalyticsClientInitContext, Event } from '../../../../client';
|
||||
import { fetchMock } from './server_shipper.test.mocks';
|
||||
import { ElasticV3ServerShipper } from './server_shipper';
|
||||
|
||||
const SECONDS = 1000;
|
||||
const MINUTES = 60 * SECONDS;
|
||||
|
||||
describe('ElasticV3ServerShipper', () => {
|
||||
const events: Event[] = [
|
||||
{
|
||||
timestamp: '2020-01-01T00:00:00.000Z',
|
||||
event_type: 'test-event-type',
|
||||
context: {},
|
||||
properties: {},
|
||||
},
|
||||
];
|
||||
|
||||
const initContext: AnalyticsClientInitContext = {
|
||||
sendTo: 'staging',
|
||||
isDev: true,
|
||||
logger: loggerMock.create(),
|
||||
};
|
||||
|
||||
let shipper: ElasticV3ServerShipper;
|
||||
|
||||
// eslint-disable-next-line dot-notation
|
||||
const setLastBatchSent = (ms: number) => (shipper['lastBatchSent'] = ms);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers({ legacyFakeTimers: false });
|
||||
|
||||
shipper = new ElasticV3ServerShipper(
|
||||
{ version: '1.2.3', channelName: 'test-channel', debug: true },
|
||||
initContext
|
||||
);
|
||||
// eslint-disable-next-line dot-notation
|
||||
shipper['firstTimeOffline'] = null; // The tests think connectivity is OK initially for easier testing.
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
shipper.shutdown();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('set optIn should update the isOptedIn$ observable', () => {
|
||||
// eslint-disable-next-line dot-notation
|
||||
const getInternalOptIn = () => shipper['isOptedIn$'].value;
|
||||
|
||||
// Initially undefined
|
||||
expect(getInternalOptIn()).toBeUndefined();
|
||||
|
||||
shipper.optIn(true);
|
||||
expect(getInternalOptIn()).toBe(true);
|
||||
|
||||
shipper.optIn(false);
|
||||
expect(getInternalOptIn()).toBe(false);
|
||||
});
|
||||
|
||||
test('clears the queue after optIn: false', () => {
|
||||
shipper.reportEvents(events);
|
||||
// eslint-disable-next-line dot-notation
|
||||
expect(shipper['internalQueue'].length).toBe(1);
|
||||
|
||||
shipper.optIn(false);
|
||||
// eslint-disable-next-line dot-notation
|
||||
expect(shipper['internalQueue'].length).toBe(0);
|
||||
});
|
||||
|
||||
test('set extendContext should store local values: clusterUuid and licenseId', () => {
|
||||
// eslint-disable-next-line dot-notation
|
||||
const getInternalClusterUuid = () => shipper['clusterUuid'];
|
||||
// eslint-disable-next-line dot-notation
|
||||
const getInternalLicenseId = () => shipper['licenseId'];
|
||||
|
||||
// Initial values
|
||||
expect(getInternalClusterUuid()).toBe('UNKNOWN');
|
||||
expect(getInternalLicenseId()).toBeUndefined();
|
||||
|
||||
shipper.extendContext({ cluster_uuid: 'test-cluster-uuid' });
|
||||
expect(getInternalClusterUuid()).toBe('test-cluster-uuid');
|
||||
expect(getInternalLicenseId()).toBeUndefined();
|
||||
|
||||
shipper.extendContext({ license_id: 'test-license-id' });
|
||||
expect(getInternalClusterUuid()).toBe('test-cluster-uuid');
|
||||
expect(getInternalLicenseId()).toBe('test-license-id');
|
||||
|
||||
shipper.extendContext({ cluster_uuid: 'test-cluster-uuid-2', license_id: 'test-license-id-2' });
|
||||
expect(getInternalClusterUuid()).toBe('test-cluster-uuid-2');
|
||||
expect(getInternalLicenseId()).toBe('test-license-id-2');
|
||||
});
|
||||
|
||||
test('calls to reportEvents do not call `fetch` straight away', () => {
|
||||
shipper.reportEvents(events);
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('calls to reportEvents do not call `fetch` after 10 minutes because no optIn value is set yet', async () => {
|
||||
shipper.reportEvents(events);
|
||||
await jest.advanceTimersByTimeAsync(10 * MINUTES);
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('calls to reportEvents call `fetch` after 10 seconds when optIn value is set to true', async () => {
|
||||
shipper.reportEvents(events);
|
||||
shipper.optIn(true);
|
||||
const counter = firstValueFrom(shipper.telemetryCounter$);
|
||||
setLastBatchSent(Date.now() - 10 * SECONDS);
|
||||
await jest.advanceTimersByTimeAsync(1 * SECONDS); // Moving 1 second should be enough to trigger the logic
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'https://telemetry-staging.elastic.co/v3/send/test-channel',
|
||||
{
|
||||
body: '{"timestamp":"2020-01-01T00:00:00.000Z","event_type":"test-event-type","context":{},"properties":{}}\n',
|
||||
headers: {
|
||||
'content-type': 'application/x-ndjson',
|
||||
'x-elastic-cluster-id': 'UNKNOWN',
|
||||
'x-elastic-stack-version': '1.2.3',
|
||||
},
|
||||
method: 'POST',
|
||||
query: { debug: true },
|
||||
}
|
||||
);
|
||||
await expect(counter).resolves.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"code": "200",
|
||||
"count": 1,
|
||||
"event_type": "test-event-type",
|
||||
"source": "elastic_v3_server",
|
||||
"type": "succeeded",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('calls to reportEvents do not call `fetch` after 10 seconds when optIn value is set to false', async () => {
|
||||
shipper.reportEvents(events);
|
||||
shipper.optIn(false);
|
||||
setLastBatchSent(Date.now() - 10 * SECONDS);
|
||||
await jest.advanceTimersByTimeAsync(1 * SECONDS); // Moving 1 second should be enough to trigger the logic
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('calls to reportEvents call `fetch` when shutting down if optIn value is set to true', async () => {
|
||||
shipper.reportEvents(events);
|
||||
shipper.optIn(true);
|
||||
const counter = firstValueFrom(shipper.telemetryCounter$);
|
||||
shipper.shutdown();
|
||||
jest.advanceTimersToNextTimer(); // We are handling the shutdown in a promise, so we need to wait for the next tick.
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'https://telemetry-staging.elastic.co/v3/send/test-channel',
|
||||
{
|
||||
body: '{"timestamp":"2020-01-01T00:00:00.000Z","event_type":"test-event-type","context":{},"properties":{}}\n',
|
||||
headers: {
|
||||
'content-type': 'application/x-ndjson',
|
||||
'x-elastic-cluster-id': 'UNKNOWN',
|
||||
'x-elastic-stack-version': '1.2.3',
|
||||
},
|
||||
method: 'POST',
|
||||
query: { debug: true },
|
||||
}
|
||||
);
|
||||
await expect(counter).resolves.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"code": "200",
|
||||
"count": 1,
|
||||
"event_type": "test-event-type",
|
||||
"source": "elastic_v3_server",
|
||||
"type": "succeeded",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('does not add the query.debug: true property to the request if the shipper is not set with the debug flag', async () => {
|
||||
shipper = new ElasticV3ServerShipper(
|
||||
{ version: '1.2.3', channelName: 'test-channel' },
|
||||
initContext
|
||||
);
|
||||
// eslint-disable-next-line dot-notation
|
||||
shipper['firstTimeOffline'] = null;
|
||||
shipper.reportEvents(events);
|
||||
shipper.optIn(true);
|
||||
setLastBatchSent(Date.now() - 10 * SECONDS);
|
||||
await jest.advanceTimersByTimeAsync(1 * SECONDS); // Moving 1 second should be enough to trigger the logic
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'https://telemetry-staging.elastic.co/v3/send/test-channel',
|
||||
{
|
||||
body: '{"timestamp":"2020-01-01T00:00:00.000Z","event_type":"test-event-type","context":{},"properties":{}}\n',
|
||||
headers: {
|
||||
'content-type': 'application/x-ndjson',
|
||||
'x-elastic-cluster-id': 'UNKNOWN',
|
||||
'x-elastic-stack-version': '1.2.3',
|
||||
},
|
||||
method: 'POST',
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test('sends when the queue overflows the 10kB leaky bucket one batch every 10s', async () => {
|
||||
expect.assertions(2 * 9 + 2);
|
||||
|
||||
shipper.reportEvents(new Array(1000).fill(events[0]));
|
||||
shipper.optIn(true);
|
||||
|
||||
// Due to the size of the test events, it matches 8 rounds.
|
||||
for (let i = 0; i < 9; i++) {
|
||||
const counter = firstValueFrom(shipper.telemetryCounter$);
|
||||
setLastBatchSent(Date.now() - 10 * SECONDS);
|
||||
await jest.advanceTimersByTimeAsync(1 * SECONDS); // Moving 1 second should be enough to trigger the logic
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(
|
||||
i + 1,
|
||||
'https://telemetry-staging.elastic.co/v3/send/test-channel',
|
||||
{
|
||||
body: new Array(103)
|
||||
.fill(
|
||||
'{"timestamp":"2020-01-01T00:00:00.000Z","event_type":"test-event-type","context":{},"properties":{}}\n'
|
||||
)
|
||||
.join(''),
|
||||
headers: {
|
||||
'content-type': 'application/x-ndjson',
|
||||
'x-elastic-cluster-id': 'UNKNOWN',
|
||||
'x-elastic-stack-version': '1.2.3',
|
||||
},
|
||||
method: 'POST',
|
||||
query: { debug: true },
|
||||
}
|
||||
);
|
||||
await expect(counter).resolves.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"code": "200",
|
||||
"count": 103,
|
||||
"event_type": "test-event-type",
|
||||
"source": "elastic_v3_server",
|
||||
"type": "succeeded",
|
||||
}
|
||||
`);
|
||||
jest.advanceTimersToNextTimer();
|
||||
}
|
||||
// eslint-disable-next-line dot-notation
|
||||
expect(shipper['internalQueue'].length).toBe(1000 - 9 * 103); // 73
|
||||
|
||||
// If we call it again, it should not enqueue all the events (only the ones to fill the queue):
|
||||
shipper.reportEvents(new Array(1000).fill(events[0]));
|
||||
// eslint-disable-next-line dot-notation
|
||||
expect(shipper['internalQueue'].length).toBe(1000);
|
||||
});
|
||||
|
||||
test('handles when the fetch request fails', async () => {
|
||||
fetchMock.mockRejectedValueOnce(new Error('Failed to fetch'));
|
||||
shipper.reportEvents(events);
|
||||
shipper.optIn(true);
|
||||
const counter = firstValueFrom(shipper.telemetryCounter$);
|
||||
setLastBatchSent(Date.now() - 10 * SECONDS);
|
||||
await jest.advanceTimersByTimeAsync(1 * SECONDS); // Moving 1 second should be enough to trigger the logic
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'https://telemetry-staging.elastic.co/v3/send/test-channel',
|
||||
{
|
||||
body: '{"timestamp":"2020-01-01T00:00:00.000Z","event_type":"test-event-type","context":{},"properties":{}}\n',
|
||||
headers: {
|
||||
'content-type': 'application/x-ndjson',
|
||||
'x-elastic-cluster-id': 'UNKNOWN',
|
||||
'x-elastic-stack-version': '1.2.3',
|
||||
},
|
||||
method: 'POST',
|
||||
query: { debug: true },
|
||||
}
|
||||
);
|
||||
await expect(counter).resolves.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"code": "Failed to fetch",
|
||||
"count": 1,
|
||||
"event_type": "test-event-type",
|
||||
"source": "elastic_v3_server",
|
||||
"type": "failed",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('handles when the fetch request fails (request completes but not OK response)', async () => {
|
||||
fetchMock.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 400,
|
||||
text: () => Promise.resolve('{"status": "not ok"}'),
|
||||
});
|
||||
shipper.reportEvents(events);
|
||||
shipper.optIn(true);
|
||||
const counter = firstValueFrom(shipper.telemetryCounter$);
|
||||
setLastBatchSent(Date.now() - 10 * SECONDS);
|
||||
await jest.advanceTimersByTimeAsync(1 * SECONDS); // Moving 1 second should be enough to trigger the logic
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'https://telemetry-staging.elastic.co/v3/send/test-channel',
|
||||
{
|
||||
body: '{"timestamp":"2020-01-01T00:00:00.000Z","event_type":"test-event-type","context":{},"properties":{}}\n',
|
||||
headers: {
|
||||
'content-type': 'application/x-ndjson',
|
||||
'x-elastic-cluster-id': 'UNKNOWN',
|
||||
'x-elastic-stack-version': '1.2.3',
|
||||
},
|
||||
method: 'POST',
|
||||
query: { debug: true },
|
||||
}
|
||||
);
|
||||
await expect(counter).resolves.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"code": "400",
|
||||
"count": 1,
|
||||
"event_type": "test-event-type",
|
||||
"source": "elastic_v3_server",
|
||||
"type": "failed",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
describe('Connectivity Checks', () => {
|
||||
describe('connectivity check when connectivity is confirmed (firstTimeOffline === null)', () => {
|
||||
test.each([undefined, false, true])('does not run for opt-in %p', async (optInValue) => {
|
||||
if (optInValue !== undefined) {
|
||||
shipper.optIn(optInValue);
|
||||
}
|
||||
|
||||
// From the start, it doesn't check connectivity because already confirmed
|
||||
expect(fetchMock).not.toHaveBeenCalledWith(
|
||||
'https://telemetry-staging.elastic.co/v3/send/test-channel',
|
||||
{ method: 'OPTIONS' }
|
||||
);
|
||||
|
||||
// Wait a big time (1 minute should be enough, but for the sake of tests...)
|
||||
await jest.advanceTimersByTimeAsync(10 * MINUTES);
|
||||
|
||||
expect(fetchMock).not.toHaveBeenCalledWith(
|
||||
'https://telemetry-staging.elastic.co/v3/send/test-channel',
|
||||
{ method: 'OPTIONS' }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('connectivity check with initial unknown state of the connectivity', () => {
|
||||
beforeEach(() => {
|
||||
// eslint-disable-next-line dot-notation
|
||||
shipper['firstTimeOffline'] = undefined; // Initial unknown state of the connectivity
|
||||
});
|
||||
|
||||
test.each([undefined, false])('does not run for opt-in %p', async (optInValue) => {
|
||||
if (optInValue !== undefined) {
|
||||
shipper.optIn(optInValue);
|
||||
}
|
||||
|
||||
// From the start, it doesn't check connectivity because already confirmed
|
||||
expect(fetchMock).not.toHaveBeenCalledWith(
|
||||
'https://telemetry-staging.elastic.co/v3/send/test-channel',
|
||||
{ method: 'OPTIONS' }
|
||||
);
|
||||
|
||||
// Wait a big time (1 minute should be enough, but for the sake of tests...)
|
||||
await jest.advanceTimersByTimeAsync(10 * MINUTES);
|
||||
|
||||
expect(fetchMock).not.toHaveBeenCalledWith(
|
||||
'https://telemetry-staging.elastic.co/v3/send/test-channel',
|
||||
{ method: 'OPTIONS' }
|
||||
);
|
||||
});
|
||||
|
||||
test('runs as soon as opt-in is set to true', () => {
|
||||
shipper.optIn(true);
|
||||
|
||||
// From the start, it doesn't check connectivity because opt-in is not true
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'https://telemetry-staging.elastic.co/v3/send/test-channel',
|
||||
{ method: 'OPTIONS' }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('connectivity check with the connectivity confirmed to be faulty', () => {
|
||||
beforeEach(() => {
|
||||
// eslint-disable-next-line dot-notation
|
||||
shipper['firstTimeOffline'] = 100; // Failed at some point
|
||||
});
|
||||
|
||||
test.each([undefined, false])('does not run for opt-in %p', async (optInValue) => {
|
||||
if (optInValue !== undefined) {
|
||||
shipper.optIn(optInValue);
|
||||
}
|
||||
|
||||
// From the start, it doesn't check connectivity because already confirmed
|
||||
expect(fetchMock).not.toHaveBeenCalledWith(
|
||||
'https://telemetry-staging.elastic.co/v3/send/test-channel',
|
||||
{ method: 'OPTIONS' }
|
||||
);
|
||||
|
||||
// Wait a big time (1 minute should be enough, but for the sake of tests...)
|
||||
await jest.advanceTimersByTimeAsync(10 * MINUTES);
|
||||
|
||||
expect(fetchMock).not.toHaveBeenCalledWith(
|
||||
'https://telemetry-staging.elastic.co/v3/send/test-channel',
|
||||
{ method: 'OPTIONS' }
|
||||
);
|
||||
});
|
||||
|
||||
test('runs as soon as opt-in is set to true', () => {
|
||||
shipper.optIn(true);
|
||||
|
||||
// From the start, it doesn't check connectivity because opt-in is not true
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'https://telemetry-staging.elastic.co/v3/send/test-channel',
|
||||
{ method: 'OPTIONS' }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('after report failure', () => {
|
||||
// generate the report failure for each test
|
||||
beforeEach(async () => {
|
||||
fetchMock.mockRejectedValueOnce(new Error('Failed to fetch'));
|
||||
shipper.reportEvents(events);
|
||||
shipper.optIn(true);
|
||||
const counter = firstValueFrom(shipper.telemetryCounter$);
|
||||
setLastBatchSent(Date.now() - 10 * SECONDS);
|
||||
await jest.advanceTimersByTimeAsync(1 * SECONDS); // Moving 1 second should be enough to trigger the logic
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'https://telemetry-staging.elastic.co/v3/send/test-channel',
|
||||
{
|
||||
body: '{"timestamp":"2020-01-01T00:00:00.000Z","event_type":"test-event-type","context":{},"properties":{}}\n',
|
||||
headers: {
|
||||
'content-type': 'application/x-ndjson',
|
||||
'x-elastic-cluster-id': 'UNKNOWN',
|
||||
'x-elastic-stack-version': '1.2.3',
|
||||
},
|
||||
method: 'POST',
|
||||
query: { debug: true },
|
||||
}
|
||||
);
|
||||
await expect(counter).resolves.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"code": "Failed to fetch",
|
||||
"count": 1,
|
||||
"event_type": "test-event-type",
|
||||
"source": "elastic_v3_server",
|
||||
"type": "failed",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('connectivity check runs periodically', async () => {
|
||||
fetchMock.mockRejectedValueOnce(new Error('Failed to fetch'));
|
||||
await jest.advanceTimersByTimeAsync(1 * MINUTES);
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'https://telemetry-staging.elastic.co/v3/send/test-channel',
|
||||
{ method: 'OPTIONS' }
|
||||
);
|
||||
fetchMock.mockResolvedValueOnce({ ok: false });
|
||||
await jest.advanceTimersByTimeAsync(2 * MINUTES);
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
'https://telemetry-staging.elastic.co/v3/send/test-channel',
|
||||
{ method: 'OPTIONS' }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('after being offline for longer than 24h', () => {
|
||||
beforeEach(() => {
|
||||
shipper.optIn(true);
|
||||
shipper.reportEvents(events);
|
||||
// eslint-disable-next-line dot-notation
|
||||
expect(shipper['internalQueue'].length).toBe(1);
|
||||
// eslint-disable-next-line dot-notation
|
||||
shipper['firstTimeOffline'] = 100;
|
||||
});
|
||||
|
||||
test('the following connectivity check clears the queue', async () => {
|
||||
fetchMock.mockRejectedValueOnce(new Error('Failed to fetch'));
|
||||
await jest.advanceTimersByTimeAsync(1 * MINUTES);
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'https://telemetry-staging.elastic.co/v3/send/test-channel',
|
||||
{ method: 'OPTIONS' }
|
||||
);
|
||||
// eslint-disable-next-line dot-notation
|
||||
expect(shipper['internalQueue'].length).toBe(0);
|
||||
});
|
||||
|
||||
test('new events are not added to the queue', async () => {
|
||||
fetchMock.mockRejectedValueOnce(new Error('Failed to fetch'));
|
||||
await jest.advanceTimersByTimeAsync(1 * MINUTES);
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'https://telemetry-staging.elastic.co/v3/send/test-channel',
|
||||
{ method: 'OPTIONS' }
|
||||
);
|
||||
// eslint-disable-next-line dot-notation
|
||||
expect(shipper['internalQueue'].length).toBe(0);
|
||||
|
||||
shipper.reportEvents(events);
|
||||
// eslint-disable-next-line dot-notation
|
||||
expect(shipper['internalQueue'].length).toBe(0);
|
||||
});
|
||||
|
||||
test('regains the connection', async () => {
|
||||
fetchMock.mockResolvedValueOnce({ ok: true });
|
||||
await jest.advanceTimersByTimeAsync(1 * MINUTES);
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'https://telemetry-staging.elastic.co/v3/send/test-channel',
|
||||
{ method: 'OPTIONS' }
|
||||
);
|
||||
// eslint-disable-next-line dot-notation
|
||||
expect(shipper['firstTimeOffline']).toBe(null);
|
||||
|
||||
await jest.advanceTimersByTimeAsync(10 * MINUTES);
|
||||
expect(fetchMock).not.toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'https://telemetry-staging.elastic.co/v3/send/test-channel',
|
||||
{ method: 'OPTIONS' }
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('flush method', () => {
|
||||
test('resolves straight away if it should not send anything', async () => {
|
||||
await expect(shipper.flush()).resolves.toBe(undefined);
|
||||
});
|
||||
|
||||
test('resolves when all the ongoing requests are complete', async () => {
|
||||
shipper.optIn(true);
|
||||
shipper.reportEvents(events);
|
||||
expect(fetchMock).toHaveBeenCalledTimes(0);
|
||||
fetchMock.mockImplementation(async () => {
|
||||
// eslint-disable-next-line dot-notation
|
||||
expect(shipper['inFlightRequests$'].value).toBe(1);
|
||||
});
|
||||
await expect(shipper.flush()).resolves.toBe(undefined);
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'https://telemetry-staging.elastic.co/v3/send/test-channel',
|
||||
{
|
||||
body: '{"timestamp":"2020-01-01T00:00:00.000Z","event_type":"test-event-type","context":{},"properties":{}}\n',
|
||||
headers: {
|
||||
'content-type': 'application/x-ndjson',
|
||||
'x-elastic-cluster-id': 'UNKNOWN',
|
||||
'x-elastic-stack-version': '1.2.3',
|
||||
},
|
||||
method: 'POST',
|
||||
query: { debug: true },
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test('calling flush multiple times does not keep hanging', async () => {
|
||||
await expect(shipper.flush()).resolves.toBe(undefined);
|
||||
await expect(shipper.flush()).resolves.toBe(undefined);
|
||||
await Promise.all([shipper.flush(), shipper.flush()]);
|
||||
});
|
||||
|
||||
test('calling flush after shutdown does not keep hanging', async () => {
|
||||
shipper.shutdown();
|
||||
await expect(shipper.flush()).resolves.toBe(undefined);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -25,13 +25,6 @@ import {
|
|||
skip,
|
||||
firstValueFrom,
|
||||
} from 'rxjs';
|
||||
import type {
|
||||
AnalyticsClientInitContext,
|
||||
Event,
|
||||
EventContext,
|
||||
IShipper,
|
||||
TelemetryCounter,
|
||||
} from '@kbn/analytics-client';
|
||||
import {
|
||||
type ElasticV3ShipperOptions,
|
||||
buildHeaders,
|
||||
|
@ -39,7 +32,14 @@ import {
|
|||
createTelemetryCounterHelper,
|
||||
eventsToNDJSON,
|
||||
ErrorWithCode,
|
||||
} from '@kbn/analytics-shippers-elastic-v3-common';
|
||||
} from '../../common';
|
||||
import type {
|
||||
AnalyticsClientInitContext,
|
||||
Event,
|
||||
EventContext,
|
||||
IShipper,
|
||||
TelemetryCounter,
|
||||
} from '../../../../client';
|
||||
|
||||
const SECOND = 1000;
|
||||
const MINUTE = 60 * SECOND;
|
|
@ -1,13 +1,13 @@
|
|||
# @kbn/analytics-shippers-fullstory
|
||||
# @kbn/ebt/shippers/fullstory
|
||||
|
||||
FullStory implementation as a shipper for the `@kbn/analytics-client`.
|
||||
FullStory implementation as a shipper for the `@kbn/ebt/client`.
|
||||
|
||||
## How to use it
|
||||
|
||||
This module is intended to be used **on the browser only**. It does not support server-side events.
|
||||
|
||||
```typescript
|
||||
import { FullStoryShipper } from "@kbn/analytics-shippers-fullstory";
|
||||
import { FullStoryShipper } from "@kbn/ebt/shippers/fullstory";
|
||||
|
||||
analytics.registerShipper(FullStoryShipper, { fullStoryOrgId: '12345' })
|
||||
```
|
|
@ -6,15 +6,10 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type {
|
||||
AnalyticsClientInitContext,
|
||||
EventContext,
|
||||
Event,
|
||||
IShipper,
|
||||
} from '@kbn/analytics-client';
|
||||
import { Subject, distinct, debounceTime, map, filter, Subscription } from 'rxjs';
|
||||
import { get, has } from 'lodash';
|
||||
import { set } from '@kbn/safer-lodash-set';
|
||||
import type { AnalyticsClientInitContext, EventContext, Event, IShipper } from '../../../client';
|
||||
import type { FullStoryApi } from './types';
|
||||
import type { FullStorySnippetConfig } from './load_snippet';
|
||||
import { formatPayload } from './format_payload';
|
||||
|
@ -56,6 +51,10 @@ export interface FullStoryShipperConfig extends FullStorySnippetConfig {
|
|||
* If this setting is provided, it'll only send the event types specified in this list.
|
||||
*/
|
||||
eventTypesAllowlist?: string[];
|
||||
/**
|
||||
* FullStory only allows calling setVars('page') once per navigation.
|
||||
* This setting defines how much time to hold from calling the API while additional lazy context is being resolved.
|
||||
*/
|
||||
pageVarsDebounceTimeMs?: number;
|
||||
}
|
||||
|
|
@ -11,25 +11,11 @@ import { loadSnippet } from './load_snippet';
|
|||
describe('loadSnippet', () => {
|
||||
beforeAll(() => {
|
||||
// Define necessary window and document global variables for the tests
|
||||
Object.defineProperty(global, 'window', {
|
||||
writable: true,
|
||||
value: {},
|
||||
});
|
||||
|
||||
Object.defineProperty(global, 'document', {
|
||||
writable: true,
|
||||
value: {
|
||||
createElement: jest.fn().mockReturnValue({}),
|
||||
getElementsByTagName: jest
|
||||
.fn()
|
||||
.mockReturnValue([{ parentNode: { insertBefore: jest.fn() } }]),
|
||||
},
|
||||
});
|
||||
|
||||
Object.defineProperty(global, '_fs_script', {
|
||||
writable: true,
|
||||
value: '',
|
||||
});
|
||||
jest
|
||||
.spyOn(global.document, 'getElementsByTagName')
|
||||
.mockReturnValue([
|
||||
{ parentNode: { insertBefore: jest.fn() } },
|
||||
] as unknown as HTMLCollectionOf<Element>);
|
||||
});
|
||||
|
||||
it('should return the FullStory API', () => {
|
|
@ -4,17 +4,20 @@
|
|||
"outDir": "target/types",
|
||||
"types": [
|
||||
"jest",
|
||||
"node"
|
||||
"node",
|
||||
"react"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts"
|
||||
],
|
||||
"kbn_references": [
|
||||
"@kbn/logging",
|
||||
"@kbn/logging-mocks"
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
"target/**/*"
|
||||
],
|
||||
"kbn_references": [
|
||||
"@kbn/logging-mocks",
|
||||
"@kbn/logging",
|
||||
"@kbn/safer-lodash-set",
|
||||
]
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
/*
|
||||
* 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/jest_node',
|
||||
rootDir: '../../../../../',
|
||||
roots: ['<rootDir>/packages/analytics/shippers/elastic_v3/browser'],
|
||||
};
|
|
@ -1,5 +0,0 @@
|
|||
{
|
||||
"type": "shared-common",
|
||||
"id": "@kbn/analytics-shippers-elastic-v3-browser",
|
||||
"owner": "@elastic/kibana-core"
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"name": "@kbn/analytics-shippers-elastic-v3-browser",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"author": "Kibana Core",
|
||||
"license": "SSPL-1.0 OR Elastic License 2.0"
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
{
|
||||
"extends": "../../../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target/types",
|
||||
"types": [
|
||||
"jest",
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts"
|
||||
],
|
||||
"kbn_references": [
|
||||
"@kbn/analytics-client",
|
||||
"@kbn/analytics-shippers-elastic-v3-common",
|
||||
"@kbn/logging-mocks"
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
]
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
/*
|
||||
* 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/jest_node',
|
||||
rootDir: '../../../../../',
|
||||
roots: ['<rootDir>/packages/analytics/shippers/elastic_v3/common'],
|
||||
};
|
|
@ -1,5 +0,0 @@
|
|||
{
|
||||
"type": "shared-common",
|
||||
"id": "@kbn/analytics-shippers-elastic-v3-common",
|
||||
"owner": "@elastic/kibana-core"
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"name": "@kbn/analytics-shippers-elastic-v3-common",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"author": "Kibana Core",
|
||||
"license": "SSPL-1.0 OR Elastic License 2.0"
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
{
|
||||
"extends": "../../../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target/types",
|
||||
"types": [
|
||||
"jest",
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts"
|
||||
],
|
||||
"kbn_references": [
|
||||
"@kbn/analytics-client"
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
]
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
/*
|
||||
* 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/jest_node',
|
||||
rootDir: '../../../../../',
|
||||
roots: ['<rootDir>/packages/analytics/shippers/elastic_v3/server'],
|
||||
};
|
|
@ -1,5 +0,0 @@
|
|||
{
|
||||
"type": "shared-common",
|
||||
"id": "@kbn/analytics-shippers-elastic-v3-server",
|
||||
"owner": "@elastic/kibana-core"
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"name": "@kbn/analytics-shippers-elastic-v3-server",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"author": "Kibana Core",
|
||||
"license": "SSPL-1.0 OR Elastic License 2.0"
|
||||
}
|
|
@ -1,624 +0,0 @@
|
|||
/*
|
||||
* 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 { loggerMock } from '@kbn/logging-mocks';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { fakeSchedulers } from 'rxjs-marbles/jest';
|
||||
import type { AnalyticsClientInitContext, Event } from '@kbn/analytics-client';
|
||||
import { fetchMock } from './server_shipper.test.mocks';
|
||||
import { ElasticV3ServerShipper } from './server_shipper';
|
||||
|
||||
const SECONDS = 1000;
|
||||
const MINUTES = 60 * SECONDS;
|
||||
|
||||
describe('ElasticV3ServerShipper', () => {
|
||||
const events: Event[] = [
|
||||
{
|
||||
timestamp: '2020-01-01T00:00:00.000Z',
|
||||
event_type: 'test-event-type',
|
||||
context: {},
|
||||
properties: {},
|
||||
},
|
||||
];
|
||||
|
||||
const nextTick = () => new Promise((resolve) => setImmediate(resolve));
|
||||
|
||||
const initContext: AnalyticsClientInitContext = {
|
||||
sendTo: 'staging',
|
||||
isDev: true,
|
||||
logger: loggerMock.create(),
|
||||
};
|
||||
|
||||
let shipper: ElasticV3ServerShipper;
|
||||
|
||||
// eslint-disable-next-line dot-notation
|
||||
const setLastBatchSent = (ms: number) => (shipper['lastBatchSent'] = ms);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers({ legacyFakeTimers: true });
|
||||
|
||||
shipper = new ElasticV3ServerShipper(
|
||||
{ version: '1.2.3', channelName: 'test-channel', debug: true },
|
||||
initContext
|
||||
);
|
||||
// eslint-disable-next-line dot-notation
|
||||
shipper['firstTimeOffline'] = null; // The tests think connectivity is OK initially for easier testing.
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
shipper.shutdown();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('set optIn should update the isOptedIn$ observable', () => {
|
||||
// eslint-disable-next-line dot-notation
|
||||
const getInternalOptIn = () => shipper['isOptedIn$'].value;
|
||||
|
||||
// Initially undefined
|
||||
expect(getInternalOptIn()).toBeUndefined();
|
||||
|
||||
shipper.optIn(true);
|
||||
expect(getInternalOptIn()).toBe(true);
|
||||
|
||||
shipper.optIn(false);
|
||||
expect(getInternalOptIn()).toBe(false);
|
||||
});
|
||||
|
||||
test('clears the queue after optIn: false', () => {
|
||||
shipper.reportEvents(events);
|
||||
// eslint-disable-next-line dot-notation
|
||||
expect(shipper['internalQueue'].length).toBe(1);
|
||||
|
||||
shipper.optIn(false);
|
||||
// eslint-disable-next-line dot-notation
|
||||
expect(shipper['internalQueue'].length).toBe(0);
|
||||
});
|
||||
|
||||
test('set extendContext should store local values: clusterUuid and licenseId', () => {
|
||||
// eslint-disable-next-line dot-notation
|
||||
const getInternalClusterUuid = () => shipper['clusterUuid'];
|
||||
// eslint-disable-next-line dot-notation
|
||||
const getInternalLicenseId = () => shipper['licenseId'];
|
||||
|
||||
// Initial values
|
||||
expect(getInternalClusterUuid()).toBe('UNKNOWN');
|
||||
expect(getInternalLicenseId()).toBeUndefined();
|
||||
|
||||
shipper.extendContext({ cluster_uuid: 'test-cluster-uuid' });
|
||||
expect(getInternalClusterUuid()).toBe('test-cluster-uuid');
|
||||
expect(getInternalLicenseId()).toBeUndefined();
|
||||
|
||||
shipper.extendContext({ license_id: 'test-license-id' });
|
||||
expect(getInternalClusterUuid()).toBe('test-cluster-uuid');
|
||||
expect(getInternalLicenseId()).toBe('test-license-id');
|
||||
|
||||
shipper.extendContext({ cluster_uuid: 'test-cluster-uuid-2', license_id: 'test-license-id-2' });
|
||||
expect(getInternalClusterUuid()).toBe('test-cluster-uuid-2');
|
||||
expect(getInternalLicenseId()).toBe('test-license-id-2');
|
||||
});
|
||||
|
||||
test('calls to reportEvents do not call `fetch` straight away', () => {
|
||||
shipper.reportEvents(events);
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test(
|
||||
'calls to reportEvents do not call `fetch` after 10 minutes because no optIn value is set yet',
|
||||
fakeSchedulers((advance) => {
|
||||
shipper.reportEvents(events);
|
||||
advance(10 * MINUTES);
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
})
|
||||
);
|
||||
|
||||
test(
|
||||
'calls to reportEvents call `fetch` after 10 seconds when optIn value is set to true',
|
||||
fakeSchedulers(async (advance) => {
|
||||
shipper.reportEvents(events);
|
||||
shipper.optIn(true);
|
||||
const counter = firstValueFrom(shipper.telemetryCounter$);
|
||||
setLastBatchSent(Date.now() - 10 * SECONDS);
|
||||
advance(1 * SECONDS); // Moving 1 second should be enough to trigger the logic
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'https://telemetry-staging.elastic.co/v3/send/test-channel',
|
||||
{
|
||||
body: '{"timestamp":"2020-01-01T00:00:00.000Z","event_type":"test-event-type","context":{},"properties":{}}\n',
|
||||
headers: {
|
||||
'content-type': 'application/x-ndjson',
|
||||
'x-elastic-cluster-id': 'UNKNOWN',
|
||||
'x-elastic-stack-version': '1.2.3',
|
||||
},
|
||||
method: 'POST',
|
||||
query: { debug: true },
|
||||
}
|
||||
);
|
||||
await expect(counter).resolves.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"code": "200",
|
||||
"count": 1,
|
||||
"event_type": "test-event-type",
|
||||
"source": "elastic_v3_server",
|
||||
"type": "succeeded",
|
||||
}
|
||||
`);
|
||||
})
|
||||
);
|
||||
|
||||
test(
|
||||
'calls to reportEvents do not call `fetch` after 10 seconds when optIn value is set to false',
|
||||
fakeSchedulers((advance) => {
|
||||
shipper.reportEvents(events);
|
||||
shipper.optIn(false);
|
||||
setLastBatchSent(Date.now() - 10 * SECONDS);
|
||||
advance(1 * SECONDS); // Moving 1 second should be enough to trigger the logic
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
})
|
||||
);
|
||||
|
||||
test('calls to reportEvents call `fetch` when shutting down if optIn value is set to true', async () => {
|
||||
shipper.reportEvents(events);
|
||||
shipper.optIn(true);
|
||||
const counter = firstValueFrom(shipper.telemetryCounter$);
|
||||
shipper.shutdown();
|
||||
await nextTick(); // We are handling the shutdown in a promise, so we need to wait for the next tick.
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'https://telemetry-staging.elastic.co/v3/send/test-channel',
|
||||
{
|
||||
body: '{"timestamp":"2020-01-01T00:00:00.000Z","event_type":"test-event-type","context":{},"properties":{}}\n',
|
||||
headers: {
|
||||
'content-type': 'application/x-ndjson',
|
||||
'x-elastic-cluster-id': 'UNKNOWN',
|
||||
'x-elastic-stack-version': '1.2.3',
|
||||
},
|
||||
method: 'POST',
|
||||
query: { debug: true },
|
||||
}
|
||||
);
|
||||
await expect(counter).resolves.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"code": "200",
|
||||
"count": 1,
|
||||
"event_type": "test-event-type",
|
||||
"source": "elastic_v3_server",
|
||||
"type": "succeeded",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test(
|
||||
'does not add the query.debug: true property to the request if the shipper is not set with the debug flag',
|
||||
fakeSchedulers((advance) => {
|
||||
shipper = new ElasticV3ServerShipper(
|
||||
{ version: '1.2.3', channelName: 'test-channel' },
|
||||
initContext
|
||||
);
|
||||
// eslint-disable-next-line dot-notation
|
||||
shipper['firstTimeOffline'] = null;
|
||||
shipper.reportEvents(events);
|
||||
shipper.optIn(true);
|
||||
setLastBatchSent(Date.now() - 10 * SECONDS);
|
||||
advance(1 * SECONDS); // Moving 1 second should be enough to trigger the logic
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'https://telemetry-staging.elastic.co/v3/send/test-channel',
|
||||
{
|
||||
body: '{"timestamp":"2020-01-01T00:00:00.000Z","event_type":"test-event-type","context":{},"properties":{}}\n',
|
||||
headers: {
|
||||
'content-type': 'application/x-ndjson',
|
||||
'x-elastic-cluster-id': 'UNKNOWN',
|
||||
'x-elastic-stack-version': '1.2.3',
|
||||
},
|
||||
method: 'POST',
|
||||
}
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
test(
|
||||
'sends when the queue overflows the 10kB leaky bucket one batch every 10s',
|
||||
fakeSchedulers(async (advance) => {
|
||||
expect.assertions(2 * 9 + 2);
|
||||
|
||||
shipper.reportEvents(new Array(1000).fill(events[0]));
|
||||
shipper.optIn(true);
|
||||
|
||||
// Due to the size of the test events, it matches 8 rounds.
|
||||
for (let i = 0; i < 9; i++) {
|
||||
const counter = firstValueFrom(shipper.telemetryCounter$);
|
||||
setLastBatchSent(Date.now() - 10 * SECONDS);
|
||||
advance(1 * SECONDS); // Moving 1 second should be enough to trigger the logic
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(
|
||||
i + 1,
|
||||
'https://telemetry-staging.elastic.co/v3/send/test-channel',
|
||||
{
|
||||
body: new Array(103)
|
||||
.fill(
|
||||
'{"timestamp":"2020-01-01T00:00:00.000Z","event_type":"test-event-type","context":{},"properties":{}}\n'
|
||||
)
|
||||
.join(''),
|
||||
headers: {
|
||||
'content-type': 'application/x-ndjson',
|
||||
'x-elastic-cluster-id': 'UNKNOWN',
|
||||
'x-elastic-stack-version': '1.2.3',
|
||||
},
|
||||
method: 'POST',
|
||||
query: { debug: true },
|
||||
}
|
||||
);
|
||||
await expect(counter).resolves.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"code": "200",
|
||||
"count": 103,
|
||||
"event_type": "test-event-type",
|
||||
"source": "elastic_v3_server",
|
||||
"type": "succeeded",
|
||||
}
|
||||
`);
|
||||
await nextTick();
|
||||
}
|
||||
// eslint-disable-next-line dot-notation
|
||||
expect(shipper['internalQueue'].length).toBe(1000 - 9 * 103); // 73
|
||||
|
||||
// If we call it again, it should not enqueue all the events (only the ones to fill the queue):
|
||||
shipper.reportEvents(new Array(1000).fill(events[0]));
|
||||
// eslint-disable-next-line dot-notation
|
||||
expect(shipper['internalQueue'].length).toBe(1000);
|
||||
})
|
||||
);
|
||||
|
||||
test(
|
||||
'handles when the fetch request fails',
|
||||
fakeSchedulers(async (advance) => {
|
||||
fetchMock.mockRejectedValueOnce(new Error('Failed to fetch'));
|
||||
shipper.reportEvents(events);
|
||||
shipper.optIn(true);
|
||||
const counter = firstValueFrom(shipper.telemetryCounter$);
|
||||
setLastBatchSent(Date.now() - 10 * SECONDS);
|
||||
advance(1 * SECONDS); // Moving 1 second should be enough to trigger the logic
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'https://telemetry-staging.elastic.co/v3/send/test-channel',
|
||||
{
|
||||
body: '{"timestamp":"2020-01-01T00:00:00.000Z","event_type":"test-event-type","context":{},"properties":{}}\n',
|
||||
headers: {
|
||||
'content-type': 'application/x-ndjson',
|
||||
'x-elastic-cluster-id': 'UNKNOWN',
|
||||
'x-elastic-stack-version': '1.2.3',
|
||||
},
|
||||
method: 'POST',
|
||||
query: { debug: true },
|
||||
}
|
||||
);
|
||||
await expect(counter).resolves.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"code": "Failed to fetch",
|
||||
"count": 1,
|
||||
"event_type": "test-event-type",
|
||||
"source": "elastic_v3_server",
|
||||
"type": "failed",
|
||||
}
|
||||
`);
|
||||
})
|
||||
);
|
||||
|
||||
test(
|
||||
'handles when the fetch request fails (request completes but not OK response)',
|
||||
fakeSchedulers(async (advance) => {
|
||||
fetchMock.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 400,
|
||||
text: () => Promise.resolve('{"status": "not ok"}'),
|
||||
});
|
||||
shipper.reportEvents(events);
|
||||
shipper.optIn(true);
|
||||
const counter = firstValueFrom(shipper.telemetryCounter$);
|
||||
setLastBatchSent(Date.now() - 10 * SECONDS);
|
||||
advance(1 * SECONDS); // Moving 1 second should be enough to trigger the logic
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'https://telemetry-staging.elastic.co/v3/send/test-channel',
|
||||
{
|
||||
body: '{"timestamp":"2020-01-01T00:00:00.000Z","event_type":"test-event-type","context":{},"properties":{}}\n',
|
||||
headers: {
|
||||
'content-type': 'application/x-ndjson',
|
||||
'x-elastic-cluster-id': 'UNKNOWN',
|
||||
'x-elastic-stack-version': '1.2.3',
|
||||
},
|
||||
method: 'POST',
|
||||
query: { debug: true },
|
||||
}
|
||||
);
|
||||
await expect(counter).resolves.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"code": "400",
|
||||
"count": 1,
|
||||
"event_type": "test-event-type",
|
||||
"source": "elastic_v3_server",
|
||||
"type": "failed",
|
||||
}
|
||||
`);
|
||||
})
|
||||
);
|
||||
|
||||
describe('Connectivity Checks', () => {
|
||||
describe('connectivity check when connectivity is confirmed (firstTimeOffline === null)', () => {
|
||||
test.each([undefined, false, true])('does not run for opt-in %p', (optInValue) =>
|
||||
fakeSchedulers(async (advance) => {
|
||||
if (optInValue !== undefined) {
|
||||
shipper.optIn(optInValue);
|
||||
}
|
||||
|
||||
// From the start, it doesn't check connectivity because already confirmed
|
||||
expect(fetchMock).not.toHaveBeenCalledWith(
|
||||
'https://telemetry-staging.elastic.co/v3/send/test-channel',
|
||||
{ method: 'OPTIONS' }
|
||||
);
|
||||
|
||||
// Wait a big time (1 minute should be enough, but for the sake of tests...)
|
||||
advance(10 * MINUTES);
|
||||
await nextTick();
|
||||
|
||||
expect(fetchMock).not.toHaveBeenCalledWith(
|
||||
'https://telemetry-staging.elastic.co/v3/send/test-channel',
|
||||
{ method: 'OPTIONS' }
|
||||
);
|
||||
})()
|
||||
);
|
||||
});
|
||||
|
||||
describe('connectivity check with initial unknown state of the connectivity', () => {
|
||||
beforeEach(() => {
|
||||
// eslint-disable-next-line dot-notation
|
||||
shipper['firstTimeOffline'] = undefined; // Initial unknown state of the connectivity
|
||||
});
|
||||
|
||||
test.each([undefined, false])('does not run for opt-in %p', (optInValue) =>
|
||||
fakeSchedulers(async (advance) => {
|
||||
if (optInValue !== undefined) {
|
||||
shipper.optIn(optInValue);
|
||||
}
|
||||
|
||||
// From the start, it doesn't check connectivity because already confirmed
|
||||
expect(fetchMock).not.toHaveBeenCalledWith(
|
||||
'https://telemetry-staging.elastic.co/v3/send/test-channel',
|
||||
{ method: 'OPTIONS' }
|
||||
);
|
||||
|
||||
// Wait a big time (1 minute should be enough, but for the sake of tests...)
|
||||
advance(10 * MINUTES);
|
||||
await nextTick();
|
||||
|
||||
expect(fetchMock).not.toHaveBeenCalledWith(
|
||||
'https://telemetry-staging.elastic.co/v3/send/test-channel',
|
||||
{ method: 'OPTIONS' }
|
||||
);
|
||||
})()
|
||||
);
|
||||
|
||||
test('runs as soon as opt-in is set to true', () => {
|
||||
shipper.optIn(true);
|
||||
|
||||
// From the start, it doesn't check connectivity because opt-in is not true
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'https://telemetry-staging.elastic.co/v3/send/test-channel',
|
||||
{ method: 'OPTIONS' }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('connectivity check with the connectivity confirmed to be faulty', () => {
|
||||
beforeEach(() => {
|
||||
// eslint-disable-next-line dot-notation
|
||||
shipper['firstTimeOffline'] = 100; // Failed at some point
|
||||
});
|
||||
|
||||
test.each([undefined, false])('does not run for opt-in %p', (optInValue) =>
|
||||
fakeSchedulers(async (advance) => {
|
||||
if (optInValue !== undefined) {
|
||||
shipper.optIn(optInValue);
|
||||
}
|
||||
|
||||
// From the start, it doesn't check connectivity because already confirmed
|
||||
expect(fetchMock).not.toHaveBeenCalledWith(
|
||||
'https://telemetry-staging.elastic.co/v3/send/test-channel',
|
||||
{ method: 'OPTIONS' }
|
||||
);
|
||||
|
||||
// Wait a big time (1 minute should be enough, but for the sake of tests...)
|
||||
advance(10 * MINUTES);
|
||||
await nextTick();
|
||||
|
||||
expect(fetchMock).not.toHaveBeenCalledWith(
|
||||
'https://telemetry-staging.elastic.co/v3/send/test-channel',
|
||||
{ method: 'OPTIONS' }
|
||||
);
|
||||
})()
|
||||
);
|
||||
|
||||
test('runs as soon as opt-in is set to true', () => {
|
||||
shipper.optIn(true);
|
||||
|
||||
// From the start, it doesn't check connectivity because opt-in is not true
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'https://telemetry-staging.elastic.co/v3/send/test-channel',
|
||||
{ method: 'OPTIONS' }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('after report failure', () => {
|
||||
// generate the report failure for each test
|
||||
beforeEach(
|
||||
fakeSchedulers(async (advance) => {
|
||||
fetchMock.mockRejectedValueOnce(new Error('Failed to fetch'));
|
||||
shipper.reportEvents(events);
|
||||
shipper.optIn(true);
|
||||
const counter = firstValueFrom(shipper.telemetryCounter$);
|
||||
setLastBatchSent(Date.now() - 10 * SECONDS);
|
||||
advance(1 * SECONDS); // Moving 1 second should be enough to trigger the logic
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'https://telemetry-staging.elastic.co/v3/send/test-channel',
|
||||
{
|
||||
body: '{"timestamp":"2020-01-01T00:00:00.000Z","event_type":"test-event-type","context":{},"properties":{}}\n',
|
||||
headers: {
|
||||
'content-type': 'application/x-ndjson',
|
||||
'x-elastic-cluster-id': 'UNKNOWN',
|
||||
'x-elastic-stack-version': '1.2.3',
|
||||
},
|
||||
method: 'POST',
|
||||
query: { debug: true },
|
||||
}
|
||||
);
|
||||
await expect(counter).resolves.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"code": "Failed to fetch",
|
||||
"count": 1,
|
||||
"event_type": "test-event-type",
|
||||
"source": "elastic_v3_server",
|
||||
"type": "failed",
|
||||
}
|
||||
`);
|
||||
})
|
||||
);
|
||||
|
||||
test(
|
||||
'connectivity check runs periodically',
|
||||
fakeSchedulers(async (advance) => {
|
||||
fetchMock.mockRejectedValueOnce(new Error('Failed to fetch'));
|
||||
advance(1 * MINUTES);
|
||||
await nextTick();
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'https://telemetry-staging.elastic.co/v3/send/test-channel',
|
||||
{ method: 'OPTIONS' }
|
||||
);
|
||||
fetchMock.mockResolvedValueOnce({ ok: false });
|
||||
advance(2 * MINUTES);
|
||||
await nextTick();
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
'https://telemetry-staging.elastic.co/v3/send/test-channel',
|
||||
{ method: 'OPTIONS' }
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
describe('after being offline for longer than 24h', () => {
|
||||
beforeEach(() => {
|
||||
shipper.optIn(true);
|
||||
shipper.reportEvents(events);
|
||||
// eslint-disable-next-line dot-notation
|
||||
expect(shipper['internalQueue'].length).toBe(1);
|
||||
// eslint-disable-next-line dot-notation
|
||||
shipper['firstTimeOffline'] = 100;
|
||||
});
|
||||
|
||||
test(
|
||||
'the following connectivity check clears the queue',
|
||||
fakeSchedulers(async (advance) => {
|
||||
fetchMock.mockRejectedValueOnce(new Error('Failed to fetch'));
|
||||
advance(1 * MINUTES);
|
||||
await nextTick();
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'https://telemetry-staging.elastic.co/v3/send/test-channel',
|
||||
{ method: 'OPTIONS' }
|
||||
);
|
||||
// eslint-disable-next-line dot-notation
|
||||
expect(shipper['internalQueue'].length).toBe(0);
|
||||
})
|
||||
);
|
||||
|
||||
test(
|
||||
'new events are not added to the queue',
|
||||
fakeSchedulers(async (advance) => {
|
||||
fetchMock.mockRejectedValueOnce(new Error('Failed to fetch'));
|
||||
advance(1 * MINUTES);
|
||||
await nextTick();
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'https://telemetry-staging.elastic.co/v3/send/test-channel',
|
||||
{ method: 'OPTIONS' }
|
||||
);
|
||||
// eslint-disable-next-line dot-notation
|
||||
expect(shipper['internalQueue'].length).toBe(0);
|
||||
|
||||
shipper.reportEvents(events);
|
||||
// eslint-disable-next-line dot-notation
|
||||
expect(shipper['internalQueue'].length).toBe(0);
|
||||
})
|
||||
);
|
||||
|
||||
test(
|
||||
'regains the connection',
|
||||
fakeSchedulers(async (advance) => {
|
||||
fetchMock.mockResolvedValueOnce({ ok: true });
|
||||
advance(1 * MINUTES);
|
||||
await nextTick();
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'https://telemetry-staging.elastic.co/v3/send/test-channel',
|
||||
{ method: 'OPTIONS' }
|
||||
);
|
||||
// eslint-disable-next-line dot-notation
|
||||
expect(shipper['firstTimeOffline']).toBe(null);
|
||||
|
||||
advance(10 * MINUTES);
|
||||
await nextTick();
|
||||
expect(fetchMock).not.toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'https://telemetry-staging.elastic.co/v3/send/test-channel',
|
||||
{ method: 'OPTIONS' }
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('flush method', () => {
|
||||
test('resolves straight away if it should not send anything', async () => {
|
||||
await expect(shipper.flush()).resolves.toBe(undefined);
|
||||
});
|
||||
|
||||
test('resolves when all the ongoing requests are complete', async () => {
|
||||
shipper.optIn(true);
|
||||
shipper.reportEvents(events);
|
||||
expect(fetchMock).toHaveBeenCalledTimes(0);
|
||||
fetchMock.mockImplementation(async () => {
|
||||
// eslint-disable-next-line dot-notation
|
||||
expect(shipper['inFlightRequests$'].value).toBe(1);
|
||||
});
|
||||
await expect(shipper.flush()).resolves.toBe(undefined);
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'https://telemetry-staging.elastic.co/v3/send/test-channel',
|
||||
{
|
||||
body: '{"timestamp":"2020-01-01T00:00:00.000Z","event_type":"test-event-type","context":{},"properties":{}}\n',
|
||||
headers: {
|
||||
'content-type': 'application/x-ndjson',
|
||||
'x-elastic-cluster-id': 'UNKNOWN',
|
||||
'x-elastic-stack-version': '1.2.3',
|
||||
},
|
||||
method: 'POST',
|
||||
query: { debug: true },
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test('calling flush multiple times does not keep hanging', async () => {
|
||||
await expect(shipper.flush()).resolves.toBe(undefined);
|
||||
await expect(shipper.flush()).resolves.toBe(undefined);
|
||||
await Promise.all([shipper.flush(), shipper.flush()]);
|
||||
});
|
||||
|
||||
test('calling flush after shutdown does not keep hanging', async () => {
|
||||
shipper.shutdown();
|
||||
await expect(shipper.flush()).resolves.toBe(undefined);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,21 +0,0 @@
|
|||
{
|
||||
"extends": "../../../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target/types",
|
||||
"types": [
|
||||
"jest",
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts"
|
||||
],
|
||||
"kbn_references": [
|
||||
"@kbn/analytics-client",
|
||||
"@kbn/analytics-shippers-elastic-v3-common",
|
||||
"@kbn/logging-mocks"
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
]
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
/*
|
||||
* 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/jest_node',
|
||||
rootDir: '../../../../',
|
||||
roots: ['<rootDir>/packages/analytics/shippers/fullstory'],
|
||||
};
|
|
@ -1,5 +0,0 @@
|
|||
{
|
||||
"type": "shared-common",
|
||||
"id": "@kbn/analytics-shippers-fullstory",
|
||||
"owner": "@elastic/kibana-core"
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"name": "@kbn/analytics-shippers-fullstory",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"author": "Kibana Core",
|
||||
"license": "SSPL-1.0 OR Elastic License 2.0"
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
{
|
||||
"extends": "../../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target/types",
|
||||
"types": [
|
||||
"jest",
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts"
|
||||
],
|
||||
"kbn_references": [
|
||||
"@kbn/analytics-client",
|
||||
"@kbn/logging-mocks",
|
||||
"@kbn/safer-lodash-set"
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
]
|
||||
}
|
|
@ -6,7 +6,7 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { AnalyticsClient } from '@kbn/analytics-client';
|
||||
import { AnalyticsClient } from '@kbn/ebt/client';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
export const analyticsClientMock: jest.Mocked<AnalyticsClient> = {
|
||||
|
@ -21,6 +21,6 @@ export const analyticsClientMock: jest.Mocked<AnalyticsClient> = {
|
|||
shutdown: jest.fn(),
|
||||
};
|
||||
|
||||
jest.doMock('@kbn/analytics-client', () => ({
|
||||
jest.doMock('@kbn/ebt/client', () => ({
|
||||
createAnalytics: () => analyticsClientMock,
|
||||
}));
|
||||
|
|
|
@ -7,8 +7,8 @@
|
|||
*/
|
||||
|
||||
import { of, Subscription } from 'rxjs';
|
||||
import type { AnalyticsClient } from '@kbn/analytics-client';
|
||||
import { createAnalytics } from '@kbn/analytics-client';
|
||||
import type { AnalyticsClient } from '@kbn/ebt/client';
|
||||
import { createAnalytics } from '@kbn/ebt/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';
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
*/
|
||||
|
||||
import { fromEvent } from 'rxjs';
|
||||
import type { AnalyticsClient } from '@kbn/analytics-client';
|
||||
import type { AnalyticsClient } from '@kbn/ebt/client';
|
||||
|
||||
/** HTML attributes that should be skipped from reporting because they might contain data we do not wish to collect */
|
||||
const HTML_ATTRIBUTES_TO_REMOVE = [
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 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 { AnalyticsClient } from '@kbn/ebt/client';
|
||||
import { reportPerformanceMetricEvent } from '@kbn/ebt-tools';
|
||||
|
||||
export function trackPerformanceMeasureEntries(analytics: AnalyticsClient, isDevMode: boolean) {
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
*/
|
||||
|
||||
import { debounceTime, fromEvent, map, merge, of, shareReplay } from 'rxjs';
|
||||
import type { AnalyticsClient, RootSchema } from '@kbn/analytics-client';
|
||||
import type { AnalyticsClient, RootSchema } from '@kbn/ebt/client';
|
||||
|
||||
export interface ViewportSize {
|
||||
viewport_width: number;
|
||||
|
|
|
@ -6,13 +6,13 @@
|
|||
},
|
||||
"include": ["**/*.ts"],
|
||||
"kbn_references": [
|
||||
"@kbn/analytics-client",
|
||||
"@kbn/ebt-tools",
|
||||
"@kbn/core-base-browser-internal",
|
||||
"@kbn/core-injected-metadata-browser-internal",
|
||||
"@kbn/core-analytics-browser",
|
||||
"@kbn/core-base-browser-mocks",
|
||||
"@kbn/core-injected-metadata-browser-mocks",
|
||||
"@kbn/ebt",
|
||||
],
|
||||
"exclude": ["target/**/*"]
|
||||
}
|
||||
|
|
|
@ -11,3 +11,41 @@ export type {
|
|||
AnalyticsServiceStart,
|
||||
KbnAnalyticsWindowApi,
|
||||
} from './src/types';
|
||||
|
||||
export type {
|
||||
AnalyticsClient,
|
||||
AnalyticsClientInitContext,
|
||||
// Types for the registerShipper API
|
||||
ShipperClassConstructor,
|
||||
RegisterShipperOpts,
|
||||
// Types for the optIn API
|
||||
OptInConfig,
|
||||
OptInConfigPerType,
|
||||
ShipperName,
|
||||
// Types for the registerContextProvider API
|
||||
ContextProviderOpts,
|
||||
ContextProviderName,
|
||||
// Types for the registerEventType API
|
||||
EventTypeOpts,
|
||||
// Events
|
||||
Event,
|
||||
EventContext,
|
||||
EventType,
|
||||
TelemetryCounter,
|
||||
TelemetryCounterType,
|
||||
// Schema
|
||||
RootSchema,
|
||||
SchemaObject,
|
||||
SchemaArray,
|
||||
SchemaChildValue,
|
||||
SchemaMeta,
|
||||
SchemaValue,
|
||||
SchemaMetaOptional,
|
||||
PossibleSchemaTypes,
|
||||
AllowedSchemaBooleanTypes,
|
||||
AllowedSchemaNumberTypes,
|
||||
AllowedSchemaStringTypes,
|
||||
AllowedSchemaTypes,
|
||||
// Shippers
|
||||
IShipper,
|
||||
} from '@kbn/ebt/client';
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { AnalyticsClient } from '@kbn/analytics-client';
|
||||
import type { AnalyticsClient } from '@kbn/ebt/client';
|
||||
|
||||
/**
|
||||
* Exposes the public APIs of the AnalyticsClient during the setup phase.
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
"**/*.ts"
|
||||
],
|
||||
"kbn_references": [
|
||||
"@kbn/analytics-client"
|
||||
"@kbn/ebt",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { AnalyticsClient } from '@kbn/analytics-client';
|
||||
import { AnalyticsClient } from '@kbn/ebt/client';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
export const analyticsClientMock: jest.Mocked<AnalyticsClient> = {
|
||||
|
@ -21,6 +21,6 @@ export const analyticsClientMock: jest.Mocked<AnalyticsClient> = {
|
|||
flush: jest.fn(),
|
||||
};
|
||||
|
||||
jest.doMock('@kbn/analytics-client', () => ({
|
||||
jest.doMock('@kbn/ebt/client', () => ({
|
||||
createAnalytics: () => analyticsClientMock,
|
||||
}));
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue