[EBT] Call optIn(false) if the telemetry plugin is disabled (#132748)

This commit is contained in:
Alejandro Fernández Haro 2022-05-25 13:13:19 +02:00 committed by GitHub
parent 5798664add
commit 003474c5b4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 292 additions and 126 deletions

View file

@ -935,7 +935,7 @@ describe('AnalyticsClient', () => {
test('Discards events from the internal queue when there are shippers and an opt-in response is false', async () => {
const telemetryCounterPromise = lastValueFrom(
analyticsClient.telemetryCounter$.pipe(take(3), toArray()) // Waiting for 3 enqueued
analyticsClient.telemetryCounter$.pipe(take(4), toArray()) // Waiting for 4 enqueued
);
// Send multiple events of 1 type to test the grouping logic as well
@ -947,9 +947,12 @@ describe('AnalyticsClient', () => {
analyticsClient.registerShipper(MockedShipper1, { reportEventsMock });
analyticsClient.optIn({ global: { enabled: false } });
// Report event after opted-out
analyticsClient.reportEvent('event-type-a', { a_field: 'c' });
expect(reportEventsMock).toHaveBeenCalledTimes(0);
// Expect 2 enqueued, but not shipped
// Expect 4 enqueued, but not shipped
await expect(telemetryCounterPromise).resolves.toEqual([
{
type: 'enqueued',
@ -972,7 +975,68 @@ describe('AnalyticsClient', () => {
code: 'enqueued',
count: 1,
},
{
type: 'enqueued',
source: 'client',
event_type: 'event-type-a',
code: 'enqueued',
count: 1,
},
]);
// eslint-disable-next-line dot-notation
expect(analyticsClient['internalEventQueue$'].observed).toBe(false);
});
test('Discards events from the internal queue when there are no shippers and an opt-in response is false', async () => {
const telemetryCounterPromise = lastValueFrom(
analyticsClient.telemetryCounter$.pipe(take(4), toArray()) // Waiting for 4 enqueued
);
// 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' });
analyticsClient.optIn({ global: { enabled: false } });
// Report event after opted-out
analyticsClient.reportEvent('event-type-a', { a_field: 'c' });
// Expect 4 enqueued, but not shipped
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: 'enqueued',
source: 'client',
event_type: 'event-type-a',
code: 'enqueued',
count: 1,
},
]);
// eslint-disable-next-line dot-notation
expect(analyticsClient['internalEventQueue$'].observed).toBe(false);
});
test(

View file

@ -7,7 +7,7 @@
*/
import type { Observable } from 'rxjs';
import { BehaviorSubject, Subject, combineLatest, from } from 'rxjs';
import { BehaviorSubject, Subject, combineLatest, from, merge } from 'rxjs';
import {
buffer,
bufferCount,
@ -267,7 +267,11 @@ export class AnalyticsClient implements IAnalyticsClient {
// Observer that will emit when both events occur: the OptInConfig is set + a shipper has been registered
const configReceivedAndShipperReceivedObserver$ = combineLatest([
this.optInConfigWithReplay$,
this.shipperRegistered$,
merge([
this.shipperRegistered$,
// Merging shipperRegistered$ with the optInConfigWithReplay$ when optedIn is false, so that we don't need to wait for the shipper if opted-in === false
this.optInConfigWithReplay$.pipe(filter((cfg) => cfg?.isOptedIn() === false)),
]),
]);
// Flush the internal queue when we get any optInConfig and, at least, 1 shipper

View file

@ -10,5 +10,7 @@
"requiredPlugins": [
"usageCollection"
],
"optionalPlugins": []
"optionalPlugins": [
"telemetry"
]
}

View file

@ -0,0 +1,39 @@
/*
* 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 { coreMock } from '@kbn/core/public/mocks';
import { usageCollectionPluginMock } from '@kbn/usage-collection-plugin/public/mocks';
import { telemetryPluginMock } from '@kbn/telemetry-plugin/public/mocks';
import { plugin } from '.';
describe('kibana_usage_collection/public', () => {
const pluginInstance = plugin();
describe('optIn fallback from telemetry', () => {
test('should call optIn(false) when telemetry is disabled', () => {
const coreSetup = coreMock.createSetup();
const usageCollectionMock = usageCollectionPluginMock.createSetupContract();
expect(pluginInstance.setup(coreSetup, { usageCollection: usageCollectionMock })).toBe(
undefined
);
expect(coreSetup.analytics.optIn).toHaveBeenCalledWith({ global: { enabled: false } });
});
test('should NOT call optIn(false) when telemetry is enabled', () => {
const coreSetup = coreMock.createSetup();
const usageCollectionMock = usageCollectionPluginMock.createSetupContract();
const telemetry = telemetryPluginMock.createSetupContract();
expect(
pluginInstance.setup(coreSetup, { usageCollection: usageCollectionMock, telemetry })
).toBe(undefined);
expect(coreSetup.analytics.optIn).not.toHaveBeenCalled();
});
});
});

View file

@ -8,14 +8,23 @@
import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public';
import type { CoreSetup, Plugin } from '@kbn/core/public';
import type { TelemetryPluginSetup } from '@kbn/telemetry-plugin/public';
import { registerEbtCounters } from './ebt_counters';
interface KibanaUsageCollectionPluginsDepsSetup {
usageCollection: UsageCollectionSetup;
telemetry?: TelemetryPluginSetup;
}
export class KibanaUsageCollectionPlugin implements Plugin {
public setup(coreSetup: CoreSetup, { usageCollection }: KibanaUsageCollectionPluginsDepsSetup) {
public setup(
coreSetup: CoreSetup,
{ usageCollection, telemetry }: KibanaUsageCollectionPluginsDepsSetup
) {
if (!telemetry) {
// If the telemetry plugin is disabled, let's set optIn false to flush the queues.
coreSetup.analytics.optIn({ global: { enabled: false } });
}
registerEbtCounters(coreSetup.analytics, usageCollection);
}

View file

@ -15,11 +15,13 @@ import {
CollectorOptions,
createUsageCollectionSetupMock,
} from '@kbn/usage-collection-plugin/server/mocks';
import { telemetryPluginMock } from '@kbn/telemetry-plugin/server/mocks';
import { cloudDetailsMock, registerEbtCountersMock } from './plugin.test.mocks';
import { plugin } from '.';
describe('kibana_usage_collection', () => {
const pluginInstance = plugin(coreMock.createPluginInitializerContext({}));
const telemetry = telemetryPluginMock.createSetupContract();
const usageCollectors: CollectorOptions[] = [];
@ -141,4 +143,26 @@ describe('kibana_usage_collection', () => {
test('Runs the stop method without issues', () => {
expect(pluginInstance.stop()).toBe(undefined);
});
describe('optIn fallback from telemetry', () => {
test('should call optIn(false) when telemetry is disabled', () => {
const coreSetup = coreMock.createSetup();
const usageCollectionMock = createUsageCollectionSetupMock();
expect(pluginInstance.setup(coreSetup, { usageCollection: usageCollectionMock })).toBe(
undefined
);
expect(coreSetup.analytics.optIn).toHaveBeenCalledWith({ global: { enabled: false } });
});
test('should NOT call optIn(false) when telemetry is enabled', () => {
const coreSetup = coreMock.createSetup();
const usageCollectionMock = createUsageCollectionSetupMock();
expect(
pluginInstance.setup(coreSetup, { usageCollection: usageCollectionMock, telemetry })
).toBe(undefined);
expect(coreSetup.analytics.optIn).not.toHaveBeenCalled();
});
});
});

View file

@ -7,6 +7,7 @@
*/
import type { UsageCollectionSetup, UsageCounter } from '@kbn/usage-collection-plugin/server';
import type { TelemetryPluginSetup } from '@kbn/telemetry-plugin/server';
import { Subject } from 'rxjs';
import type {
PluginInitializerContext,
@ -47,6 +48,7 @@ import {
interface KibanaUsageCollectionPluginsDepsSetup {
usageCollection: UsageCollectionSetup;
telemetry?: TelemetryPluginSetup;
}
type SavedObjectsRegisterType = SavedObjectsServiceSetup['registerType'];
@ -68,7 +70,14 @@ export class KibanaUsageCollectionPlugin implements Plugin {
this.instanceUuid = initializerContext.env.instanceUuid;
}
public setup(coreSetup: CoreSetup, { usageCollection }: KibanaUsageCollectionPluginsDepsSetup) {
public setup(
coreSetup: CoreSetup,
{ usageCollection, telemetry }: KibanaUsageCollectionPluginsDepsSetup
) {
if (!telemetry) {
// If the telemetry plugin is disabled, let's set optIn false to flush the queues.
coreSetup.analytics.optIn({ global: { enabled: false } });
}
registerEbtCounters(coreSetup.analytics, usageCollection);
usageCollection.createUsageCounter('uiCounters');
this.eventLoopUsageCounter = usageCollection.createUsageCounter('eventLoop');

View file

@ -16,5 +16,6 @@
"references": [
{ "path": "../../core/tsconfig.json" },
{ "path": "../../plugins/usage_collection/tsconfig.json" },
{ "path": "../../plugins/telemetry/tsconfig.json" },
]
}

View file

@ -70,7 +70,11 @@ export class AnalyticsPluginAPlugin implements Plugin {
return res.ok({
body: stats
.filter((counter) => counter.event_type === eventType)
.filter(
(counter) =>
counter.event_type === eventType &&
['client', 'FTR-shipper'].includes(counter.source)
)
.slice(-takeNumberOfCounters),
});
}

View file

@ -34,8 +34,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
...functionalConfig.get('kbnTestServer'),
serverArgs: [
...functionalConfig.get('kbnTestServer.serverArgs'),
// Disabling telemetry, so it doesn't call opt-in before the tests run.
'--telemetry.enabled=false',
'--telemetry.optIn=true',
`--plugin-path=${path.resolve(__dirname, './__fixtures__/plugins/analytics_plugin_a')}`,
`--plugin-path=${path.resolve(__dirname, './__fixtures__/plugins/analytics_ftr_helpers')}`,
],

View file

@ -24,7 +24,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
return await browser.execute(
({ takeNumberOfCounters }) =>
window.__analyticsPluginA__.stats
.filter((counter) => counter.event_type === 'test-plugin-lifecycle')
.filter(
(counter) =>
counter.event_type === 'test-plugin-lifecycle' &&
['client', 'FTR-shipper'].includes(counter.source)
)
.slice(-takeNumberOfCounters),
{ takeNumberOfCounters: _takeNumberOfCounters }
);
@ -36,13 +40,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
);
};
beforeEach(async () => {
before(async () => {
await common.navigateToApp('home');
});
// this test should run first because it depends on optInConfig being undefined
it('should have internally enqueued the "lifecycle" events but not handed over to the shipper yet', async () => {
const telemetryCounters = await getTelemetryCounters(2);
it('should see both events enqueued and sent to the shipper', async () => {
const telemetryCounters = await getTelemetryCounters(5);
expect(telemetryCounters).to.eql([
{
type: 'enqueued',
@ -58,83 +61,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
code: 'enqueued',
count: 1,
},
]);
});
it('after setting opt-in, it should extend the contexts and send the events', async () => {
await ebtUIHelper.setOptIn(true);
const actions = await getActions();
const [_, extendContextAction, ...reportEventsActions] = actions;
// Validating the remote user_agent because that's the only field that it's added by the FTR plugin.
const context = extendContextAction.meta;
expect(context).to.have.property('user_agent');
expect(context.user_agent).to.be.a('string');
reportEventsActions.forEach((reportEventAction) => {
expect(reportEventAction.action).to.eql('reportEvents');
// Get the first event type
const initiallyBatchedEventType = reportEventAction.meta[0].event_type;
// Check that all event types in this batch are the same.
reportEventAction.meta.forEach((event: Event) => {
expect(event.event_type).to.eql(initiallyBatchedEventType);
});
});
// Find the action calling to report test-plugin-lifecycle events.
const reportTestPluginLifecycleEventsAction = reportEventsActions.find(
(reportEventAction) => {
return (
reportEventAction.action === 'reportEvents' &&
reportEventAction.meta[0].event_type === 'test-plugin-lifecycle'
);
}
);
// Some context providers emit very early. We are OK with that.
const initialContext = reportTestPluginLifecycleEventsAction!.meta[0].context;
const reportEventContext = reportTestPluginLifecycleEventsAction!.meta[1].context;
const setupEvent = reportTestPluginLifecycleEventsAction!.meta.findIndex(
(event: Event) =>
event.event_type === 'test-plugin-lifecycle' &&
event.properties.plugin === 'analyticsPluginA' &&
event.properties.step === 'setup'
);
const startEvent = reportTestPluginLifecycleEventsAction!.meta.findIndex(
(event: Event) =>
event.event_type === 'test-plugin-lifecycle' &&
event.properties.plugin === 'analyticsPluginA' &&
event.properties.step === 'start'
);
expect(setupEvent).to.be.greaterThan(-1);
expect(startEvent).to.be.greaterThan(setupEvent);
expect(reportEventContext).to.have.property('user_agent');
expect(reportEventContext.user_agent).to.be.a('string');
// Testing the FTR helper as well
expect(await ebtUIHelper.getLastEvents(2, ['test-plugin-lifecycle'])).to.eql([
{
timestamp: reportTestPluginLifecycleEventsAction!.meta[setupEvent].timestamp,
event_type: 'test-plugin-lifecycle',
context: initialContext,
properties: { plugin: 'analyticsPluginA', step: 'setup' },
},
{
timestamp: reportTestPluginLifecycleEventsAction!.meta[startEvent].timestamp,
event_type: 'test-plugin-lifecycle',
context: reportEventContext,
properties: { plugin: 'analyticsPluginA', step: 'start' },
},
]);
const telemetryCounters = await getTelemetryCounters(3);
expect(telemetryCounters).to.eql([
{
type: 'succeeded',
event_type: 'test-plugin-lifecycle',
@ -158,5 +84,95 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
},
]);
});
describe('after setting opt-in', () => {
let actions: Action[];
let context: Action['meta'];
before(async () => {
actions = await getActions();
context = actions[1].meta;
});
it('it should extend the contexts with pid injected by "analytics_plugin_a"', () => {
// Validating the remote user_agent because that's the only field that it's added by the FTR plugin.
expect(context).to.have.property('user_agent');
expect(context.user_agent).to.be.a('string');
});
it('it calls optIn first, then extendContext, followed by reportEvents', async () => {
const [optInAction, extendContextAction, ...reportEventsAction] = actions;
expect(optInAction).to.eql({ action: 'optIn', meta: true });
expect(extendContextAction).to.eql({ action: 'extendContext', meta: context });
while (reportEventsAction[0].action === 'extendContext') {
// it could happen that a context provider emits a bit delayed
reportEventsAction.shift();
}
reportEventsAction.forEach((entry) => expect(entry.action).to.eql('reportEvents'));
});
it('Initial calls to reportEvents from cached events group the requests by event_type', async () => {
// We know that after opting-in, the client will send the events in batches, grouped by event-type.
const reportEventsActions = actions.filter(({ action }) => action === 'reportEvents');
reportEventsActions.forEach((reportEventAction) => {
// Get the first event type
const initiallyBatchedEventType = reportEventAction.meta[0].event_type;
// Check that all event types in this batch are the same.
reportEventAction.meta.forEach((event: Event) => {
expect(event.event_type).to.eql(initiallyBatchedEventType);
});
});
});
it('"test-plugin-lifecycle" is received in the expected order of "setup" first, then "start"', async () => {
// We know that after opting-in, the client will send the events in batches, grouped by event-type.
const reportEventsActions = actions.filter(({ action }) => action === 'reportEvents');
// Find the action calling to report test-plugin-lifecycle events.
const reportTestPluginLifecycleEventsAction = reportEventsActions.find(
(reportEventAction) => {
return (
reportEventAction.action === 'reportEvents' &&
reportEventAction.meta[0].event_type === 'test-plugin-lifecycle'
);
}
);
// Find the setup and start events and validate that they are sent in the correct order.
const initialContext = reportTestPluginLifecycleEventsAction!.meta[0].context; // read this from the reportTestPlugin
const reportEventContext = reportTestPluginLifecycleEventsAction!.meta[1].context;
const setupEvent = reportTestPluginLifecycleEventsAction!.meta.findIndex(
(event: Event) =>
event.event_type === 'test-plugin-lifecycle' &&
event.properties.plugin === 'analyticsPluginA' &&
event.properties.step === 'setup'
);
const startEvent = reportTestPluginLifecycleEventsAction!.meta.findIndex(
(event: Event) =>
event.event_type === 'test-plugin-lifecycle' &&
event.properties.plugin === 'analyticsPluginA' &&
event.properties.step === 'start'
);
expect(setupEvent).to.be.greaterThan(-1);
expect(startEvent).to.be.greaterThan(setupEvent);
// This helps us to also test the helpers
const events = await ebtUIHelper.getLastEvents(2, ['test-plugin-lifecycle']);
expect(events).to.eql([
{
timestamp: reportTestPluginLifecycleEventsAction!.meta[setupEvent].timestamp,
event_type: 'test-plugin-lifecycle',
context: initialContext,
properties: { plugin: 'analyticsPluginA', step: 'setup' },
},
{
timestamp: reportTestPluginLifecycleEventsAction!.meta[startEvent].timestamp,
event_type: 'test-plugin-lifecycle',
context: reportEventContext,
properties: { plugin: 'analyticsPluginA', step: 'start' },
},
]);
});
});
});
}

View file

@ -37,9 +37,8 @@ export default function ({ getService }: FtrProviderContext) {
};
describe('analytics service: server side', () => {
// this test should run first because it depends on optInConfig being undefined
it('should have internally enqueued the "lifecycle" events but not handed over to the shipper yet', async () => {
const telemetryCounters = await getTelemetryCounters(2);
it('should see both events enqueued and sent to the shipper', async () => {
const telemetryCounters = await getTelemetryCounters(5);
expect(telemetryCounters).to.eql([
{
type: 'enqueued',
@ -55,6 +54,27 @@ export default function ({ getService }: FtrProviderContext) {
code: 'enqueued',
count: 1,
},
{
type: 'succeeded',
event_type: 'test-plugin-lifecycle',
source: 'FTR-shipper',
code: '200',
count: 1,
},
{
type: 'succeeded',
event_type: 'test-plugin-lifecycle',
source: 'FTR-shipper',
code: '200',
count: 1,
},
{
type: 'sent_to_shipper',
event_type: 'test-plugin-lifecycle',
source: 'client',
code: 'OK',
count: 2,
},
]);
});
@ -63,7 +83,6 @@ export default function ({ getService }: FtrProviderContext) {
let context: Action['meta'];
before(async () => {
await ebtServerHelper.setOptIn(true);
actions = await getActions();
context = actions[1].meta;
});
@ -78,14 +97,17 @@ export default function ({ getService }: FtrProviderContext) {
const [optInAction, extendContextAction, ...reportEventsAction] = actions;
expect(optInAction).to.eql({ action: 'optIn', meta: true });
expect(extendContextAction).to.eql({ action: 'extendContext', meta: context });
while (reportEventsAction[0].action === 'extendContext') {
// it could happen that a context provider emits a bit delayed
reportEventsAction.shift();
}
reportEventsAction.forEach((entry) => expect(entry.action).to.eql('reportEvents'));
});
it('Initial calls to reportEvents from cached events group the requests by event_type', async () => {
// We know that after opting-in, the client will send the events in batches, grouped by event-type.
const [, , ...reportEventsActions] = actions;
const reportEventsActions = actions.filter(({ action }) => action === 'reportEvents');
reportEventsActions.forEach((reportEventAction) => {
expect(reportEventAction.action).to.eql('reportEvents');
// Get the first event type
const initiallyBatchedEventType = reportEventAction.meta[0].event_type;
// Check that all event types in this batch are the same.
@ -97,7 +119,7 @@ export default function ({ getService }: FtrProviderContext) {
it('"test-plugin-lifecycle" is received in the expected order of "setup" first, then "start"', async () => {
// We know that after opting-in, the client will send the events in batches, grouped by event-type.
const [, , ...reportEventsActions] = actions;
const reportEventsActions = actions.filter(({ action }) => action === 'reportEvents');
// Find the action calling to report test-plugin-lifecycle events.
const reportTestPluginLifecycleEventsAction = reportEventsActions.find(
(reportEventAction) => {
@ -144,33 +166,6 @@ export default function ({ getService }: FtrProviderContext) {
},
]);
});
it('should report the plugin lifecycle events as telemetry counters', async () => {
const telemetryCounters = await getTelemetryCounters(3);
expect(telemetryCounters).to.eql([
{
type: 'succeeded',
event_type: 'test-plugin-lifecycle',
source: 'FTR-shipper',
code: '200',
count: 1,
},
{
type: 'succeeded',
event_type: 'test-plugin-lifecycle',
source: 'FTR-shipper',
code: '200',
count: 1,
},
{
type: 'sent_to_shipper',
event_type: 'test-plugin-lifecycle',
source: 'client',
code: 'OK',
count: 2,
},
]);
});
});
});
}