Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[API DOCS] Telemetry #98610

Merged
merged 3 commits into from
Apr 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,505 changes: 404 additions & 1,101 deletions api_docs/telemetry.json

Large diffs are not rendered by default.

6 changes: 0 additions & 6 deletions api_docs/telemetry.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,6 @@ import telemetryObj from './telemetry.json';
### Start
<DocDefinitionList data={[telemetryObj.client.start]}/>

### Classes
<DocDefinitionList data={telemetryObj.client.classes}/>

### Interfaces
<DocDefinitionList data={telemetryObj.client.interfaces}/>

Expand All @@ -33,9 +30,6 @@ import telemetryObj from './telemetry.json';
### Start
<DocDefinitionList data={[telemetryObj.server.start]}/>

### Functions
<DocDefinitionList data={telemetryObj.server.functions}/>

### Interfaces
<DocDefinitionList data={telemetryObj.server.interfaces}/>

Expand Down
13 changes: 9 additions & 4 deletions src/plugins/telemetry/public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,15 @@
* Side Public License, v 1.
*/

import { PluginInitializerContext } from 'kibana/public';
import { TelemetryPlugin, TelemetryPluginConfig } from './plugin';
export type { TelemetryPluginStart, TelemetryPluginSetup, TelemetryPluginConfig } from './plugin';
export type { TelemetryNotifications, TelemetryService } from './services';
import type { PluginInitializerContext } from 'src/core/public';
import type { TelemetryPluginConfig } from './plugin';
import { TelemetryPlugin } from './plugin';
export type {
TelemetryPluginStart,
TelemetryPluginSetup,
TelemetryPluginConfig,
TelemetryServicePublicApis,
} from './plugin';

export function plugin(initializerContext: PluginInitializerContext<TelemetryPluginConfig>) {
return new TelemetryPlugin(initializerContext);
Expand Down
80 changes: 70 additions & 10 deletions src/plugins/telemetry/public/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* Side Public License, v 1.
*/

import {
import type {
Plugin,
CoreStart,
CoreSetup,
Expand All @@ -15,10 +15,10 @@ import {
SavedObjectsClientContract,
SavedObjectsBatchResponse,
ApplicationStart,
} from '../../../core/public';
} from 'src/core/public';

import { TelemetrySender, TelemetryService, TelemetryNotifications } from './services';
import {
import type {
TelemetrySavedObjectAttributes,
TelemetrySavedObject,
} from '../common/telemetry_config/types';
Expand All @@ -30,27 +30,73 @@ import {
import { getNotifyUserAboutOptInDefault } from '../common/telemetry_config/get_telemetry_notify_user_about_optin_default';
import { PRIVACY_STATEMENT_URL } from '../common/constants';

/**
* Publicly exposed APIs from the Telemetry Service
*/
export interface TelemetryServicePublicApis {
/** Is the cluster opted-in to telemetry? **/
getIsOptedIn: () => boolean | null;
/** Is the user allowed to change the opt-in/out status? **/
userCanChangeSettings: boolean;
/** Is the cluster allowed to change the opt-in/out status? **/
getCanChangeOptInStatus: () => boolean;
/** Fetches an unencrypted telemetry payload so we can show it to the user **/
fetchExample: () => Promise<unknown[]>;
/**
* Overwrite the opt-in status.
* It will send a final request to the remote telemetry cluster to report about the opt-in/out change.
* @param optedIn Whether the user is opting-in (`true`) or out (`false`).
*/
setOptIn: (optedIn: boolean) => Promise<boolean>;
}

/**
* Public's setup exposed APIs by the telemetry plugin
*/
export interface TelemetryPluginSetup {
telemetryService: TelemetryService;
/** {@link TelemetryService} **/
telemetryService: TelemetryServicePublicApis;
}

/**
* Public's start exposed APIs by the telemetry plugin
*/
export interface TelemetryPluginStart {
telemetryService: TelemetryService;
telemetryNotifications: TelemetryNotifications;
/** {@link TelemetryServicePublicApis} **/
telemetryService: TelemetryServicePublicApis;
/** Notification helpers **/
telemetryNotifications: {
/** Notify that the user has been presented with the opt-in/out notice. */
setOptedInNoticeSeen: () => Promise<void>;
};
/** Set of publicly exposed telemetry constants **/
telemetryConstants: {
/** Elastic's privacy statement url **/
getPrivacyStatementUrl: () => string;
};
}

/**
* Public-exposed configuration
*/
export interface TelemetryPluginConfig {
/** Is the plugin enabled? **/
enabled: boolean;
/** Remote telemetry service's URL **/
url: string;
/** The banner is expected to be shown when needed **/
banner: boolean;
/** Does the cluster allow changing the opt-in/out status via the UI? **/
allowChangingOptInStatus: boolean;
/** Is the cluster opted-in? **/
optIn: boolean | null;
/** Opt-in/out notification URL **/
optInStatusUrl: string;
/** Should the telemetry payloads be sent from the server or the browser? **/
sendUsageFrom: 'browser' | 'server';
/** Should notify the user about the opt-in status? **/
telemetryNotifyUserAboutOptInDefault?: boolean;
/** Does the user have enough privileges to change the settings? **/
userCanChangeSettings?: boolean;
}

Expand Down Expand Up @@ -80,7 +126,7 @@ export class TelemetryPlugin implements Plugin<TelemetryPluginSetup, TelemetryPl
this.telemetrySender = new TelemetrySender(this.telemetryService);

return {
telemetryService: this.telemetryService,
telemetryService: this.getTelemetryServicePublicApis(),
};
}

Expand All @@ -92,11 +138,12 @@ export class TelemetryPlugin implements Plugin<TelemetryPluginSetup, TelemetryPl
this.canUserChangeSettings = this.getCanUserChangeSettings(application);
this.telemetryService.userCanChangeSettings = this.canUserChangeSettings;

this.telemetryNotifications = new TelemetryNotifications({
const telemetryNotifications = new TelemetryNotifications({
http,
overlays,
telemetryService: this.telemetryService,
});
this.telemetryNotifications = telemetryNotifications;

application.currentAppId$.subscribe(async () => {
const isUnauthenticated = this.getIsUnauthenticated(http);
Expand All @@ -119,14 +166,27 @@ export class TelemetryPlugin implements Plugin<TelemetryPluginSetup, TelemetryPl
});

return {
telemetryService: this.telemetryService,
telemetryNotifications: this.telemetryNotifications,
telemetryService: this.getTelemetryServicePublicApis(),
telemetryNotifications: {
setOptedInNoticeSeen: () => telemetryNotifications.setOptedInNoticeSeen(),
},
telemetryConstants: {
getPrivacyStatementUrl: () => PRIVACY_STATEMENT_URL,
},
};
}

private getTelemetryServicePublicApis(): TelemetryServicePublicApis {
const telemetryService = this.telemetryService!;
return {
getIsOptedIn: () => telemetryService.getIsOptedIn(),
setOptIn: (optedIn) => telemetryService.setOptIn(optedIn),
userCanChangeSettings: telemetryService.userCanChangeSettings,
getCanChangeOptInStatus: () => telemetryService.getCanChangeOptInStatus(),
fetchExample: () => telemetryService.fetchExample(),
};
}

/**
* Can the user edit the saved objects?
* This is a security feature, not included in the OSS build, so we need to fallback to `true`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ interface TelemetryNotificationsConstructor {
telemetryService: TelemetryService;
}

/**
* Helpers to the Telemetry banners spread through the code base in Welcome and Home landing pages.
*/
export class TelemetryNotifications {
private readonly http: CoreStart['http'];
private readonly overlays: CoreStart['overlays'];
Expand All @@ -30,12 +33,18 @@ export class TelemetryNotifications {
this.overlays = overlays;
}

/**
* Should the opted-in banner be shown to the user?
*/
public shouldShowOptedInNoticeBanner = (): boolean => {
const userShouldSeeOptInNotice = this.telemetryService.getUserShouldSeeOptInNotice();
const bannerOnScreen = typeof this.optedInNoticeBannerId !== 'undefined';
return !bannerOnScreen && userShouldSeeOptInNotice;
};

/**
* Renders the banner that claims the cluster is opted-in, and gives the option to opt-out.
*/
public renderOptedInNoticeBanner = (): void => {
const bannerId = renderOptedInNoticeBanner({
http: this.http,
Expand All @@ -46,12 +55,18 @@ export class TelemetryNotifications {
this.optedInNoticeBannerId = bannerId;
};

/**
* Should the banner to opt-in be shown to the user?
*/
public shouldShowOptInBanner = (): boolean => {
const isOptedIn = this.telemetryService.getIsOptedIn();
const bannerOnScreen = typeof this.optInBannerId !== 'undefined';
return !bannerOnScreen && isOptedIn === null;
};

/**
* Renders the banner that claims the cluster is opted-out, and gives the option to opt-in.
*/
public renderOptInBanner = (): void => {
const bannerId = renderOptInBanner({
setOptIn: this.onSetOptInClick,
Expand All @@ -61,6 +76,10 @@ export class TelemetryNotifications {
this.optInBannerId = bannerId;
};

/**
* Opt-in/out button handler
* @param isOptIn true/false whether the user opts-in/out
*/
private onSetOptInClick = async (isOptIn: boolean) => {
if (this.optInBannerId) {
this.overlays.banners.remove(this.optInBannerId);
Expand All @@ -70,6 +89,9 @@ export class TelemetryNotifications {
await this.telemetryService.setOptIn(isOptIn);
};

/**
* Clears the banner and stores the user's dismissal of the banner.
*/
public setOptedInNoticeSeen = async (): Promise<void> => {
if (this.optedInNoticeBannerId) {
this.overlays.banners.remove(this.optedInNoticeBannerId);
Expand Down
20 changes: 10 additions & 10 deletions src/plugins/telemetry/public/services/telemetry_sender.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,20 +71,20 @@ describe('TelemetrySender', () => {
const telemetryService = mockTelemetryService();
telemetryService.getIsOptedIn = jest.fn().mockReturnValue(false);
const telemetrySender = new TelemetrySender(telemetryService);
const shouldSendRerpot = telemetrySender['shouldSendReport']();
const shouldSendReport = telemetrySender['shouldSendReport']();

expect(telemetryService.getIsOptedIn).toBeCalledTimes(1);
expect(shouldSendRerpot).toBe(false);
expect(shouldSendReport).toBe(false);
});

it('returns true if lastReported is undefined', () => {
const telemetryService = mockTelemetryService();
telemetryService.getIsOptedIn = jest.fn().mockReturnValue(true);
const telemetrySender = new TelemetrySender(telemetryService);
const shouldSendRerpot = telemetrySender['shouldSendReport']();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm sure that was me with this faulty mac keyboard 😓

Copy link
Member Author

@afharo afharo Apr 29, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or me 😅 I have a spellchecker in my IDE because I'm always mixing up keystrokes... In mind, I think that I know what I'm doing, but my fingers think otherwise... 🤦

const shouldSendReport = telemetrySender['shouldSendReport']();

expect(telemetrySender['lastReported']).toBeUndefined();
expect(shouldSendRerpot).toBe(true);
expect(shouldSendReport).toBe(true);
});

it('returns true if lastReported passed REPORT_INTERVAL_MS', () => {
Expand All @@ -94,8 +94,8 @@ describe('TelemetrySender', () => {
telemetryService.getIsOptedIn = jest.fn().mockReturnValue(true);
const telemetrySender = new TelemetrySender(telemetryService);
telemetrySender['lastReported'] = `${lastReported}`;
const shouldSendRerpot = telemetrySender['shouldSendReport']();
expect(shouldSendRerpot).toBe(true);
const shouldSendReport = telemetrySender['shouldSendReport']();
expect(shouldSendReport).toBe(true);
});

it('returns false if lastReported is within REPORT_INTERVAL_MS', () => {
Expand All @@ -105,17 +105,17 @@ describe('TelemetrySender', () => {
telemetryService.getIsOptedIn = jest.fn().mockReturnValue(true);
const telemetrySender = new TelemetrySender(telemetryService);
telemetrySender['lastReported'] = `${lastReported}`;
const shouldSendRerpot = telemetrySender['shouldSendReport']();
expect(shouldSendRerpot).toBe(false);
const shouldSendReport = telemetrySender['shouldSendReport']();
expect(shouldSendReport).toBe(false);
});

it('returns true if lastReported is malformed', () => {
const telemetryService = mockTelemetryService();
telemetryService.getIsOptedIn = jest.fn().mockReturnValue(true);
const telemetrySender = new TelemetrySender(telemetryService);
telemetrySender['lastReported'] = `random_malformed_string`;
const shouldSendRerpot = telemetrySender['shouldSendReport']();
expect(shouldSendRerpot).toBe(true);
const shouldSendReport = telemetrySender['shouldSendReport']();
expect(shouldSendReport).toBe(true);
});

describe('sendIfDue', () => {
Expand Down
Loading