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

[telemetry] expose getIsOptedIn function in plugin start contract #75143

Merged
58 changes: 58 additions & 0 deletions src/plugins/telemetry/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,61 @@ Telemetry allows Kibana features to have usage tracked in the wild. The general
3. Viewing usage data in the Kibana instance of the telemetry cluster (Viewing).

This plugin is responsible for sending usage data to the telemetry cluster. For collecting usage data, use the [`usageCollection` plugin](../usage_collection/README.md)

## Telemetry Plugin public API

### Setup

The `setup` function exposes the following interface:

- `getTelemetryUrl: () => Promise<URL>`:
An async function that resolves into the telemetry Url used to send telemetry. The url is wrapped with node's [URL constructor](https://nodejs.org/api/url.html). Here is an example on how to grab the url origin:
```
const telemetryUrl = await getTelemetryUrl();
> telemetryUrl.origin; // 'https://telemetry.elastic.co'
```
Note that the telemetry URL is a kibana.yml configuration hence it is recommended to call the `getTelemetryUrl` everytime before using the actual url.

### Start

The `start` function exposes the following interface:

- `async getIsOptedIn(): Promise<boolean>`:
An async function that resolves into `true` if the user has opted into send Elastic usage data.
Resolves to `false` if the user explicitly opted out of sending usage data to Elastic or did not choose
to opt-in or out yet after an minor or major upgrade.
Bamieh marked this conversation as resolved.
Show resolved Hide resolved

### Usage

To use the exposed plugin start and setup contracts:

1. Make sure `telemetry` is in your `optionalPlugins` in the `kibana.json` file:

```json5
// <plugin>/kibana.json
{
"id": "...",
"optionalPlugins": ["telemetry"]
}
```

2. Use the exposed contracts:
```ts
// <plugin>/server/plugin.ts

import { TelemetryPluginsStart } from '../telemetry/server`;

interface MyPlyginStartDeps {
telemetry?: TelemetryPluginsStart;
}

class MyPlugin {
public async start(
core: CoreStart,
{ telemetry }: MyPlyginStartDeps
) {
const isOptedIn = await telemetry?.getIsOptedIn();
...
}
}
```
2 changes: 1 addition & 1 deletion src/plugins/telemetry/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { configSchema, TelemetryConfigType } from './config';

export { FetcherTask } from './fetcher';
export { handleOldSettings } from './handle_old_settings';
export { TelemetryPluginsSetup } from './plugin';
export { TelemetryPluginSetup, TelemetryPluginStart } from './plugin';

export const config: PluginConfigDescriptor<TelemetryConfigType> = {
schema: configSchema,
Expand Down
46 changes: 46 additions & 0 deletions src/plugins/telemetry/server/mocks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import { URL } from 'url';
import { TelemetryPluginStart, TelemetryPluginSetup } from './plugin';

export type Setup = jest.Mocked<TelemetryPluginSetup>;
export type Start = jest.Mocked<TelemetryPluginStart>;

export const telemetryPluginMock = {
createSetupContract,
createStartContract,
};

function createSetupContract(): Setup {
const telemetryUrl = new URL('https://telemetry-staging.elastic.co/xpack/MOCK_URL/send');
const setupContract: Setup = {
getTelemetryUrl: jest.fn().mockResolvedValue(telemetryUrl),
};

return setupContract;
}

function createStartContract(): Start {
const startContract: Start = {
getIsOptedIn: jest.fn(),
};

return startContract;
}
62 changes: 56 additions & 6 deletions src/plugins/telemetry/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@
* under the License.
*/

import { URL } from 'url';
import { Observable } from 'rxjs';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import {
TelemetryCollectionManagerPluginSetup,
TelemetryCollectionManagerPluginStart,
} from 'src/plugins/telemetry_collection_manager/server';
import { take } from 'rxjs/operators';
import {
CoreSetup,
PluginInitializerContext,
Expand All @@ -42,19 +44,38 @@ import {
import { TelemetryConfigType } from './config';
import { FetcherTask } from './fetcher';
import { handleOldSettings } from './handle_old_settings';
import { getTelemetrySavedObject } from './telemetry_repository';
import { getTelemetryOptIn } from '../common/telemetry_config';

export interface TelemetryPluginsSetup {
interface TelemetryPluginsDepsSetup {
usageCollection: UsageCollectionSetup;
telemetryCollectionManager: TelemetryCollectionManagerPluginSetup;
}

export interface TelemetryPluginsStart {
interface TelemetryPluginsDepsStart {
telemetryCollectionManager: TelemetryCollectionManagerPluginStart;
}

export interface TelemetryPluginSetup {
/**
* Resolves into the telemetry Url used to send telemetry.
* The url is wrapped with node's [URL constructor](https://nodejs.org/api/url.html).
*/
getTelemetryUrl: () => Promise<URL>;
}

export interface TelemetryPluginStart {
/**
* Resolves `true` if the user has opted into send Elastic usage data.
* Resolves `false` if the user explicitly opted out of sending usage data to Elastic
* or did not choose to opt-in or out -yet- after an minor or major upgrade.
Bamieh marked this conversation as resolved.
Show resolved Hide resolved
*/
getIsOptedIn: () => Promise<boolean>;
}

type SavedObjectsRegisterType = CoreSetup['savedObjects']['registerType'];

export class TelemetryPlugin implements Plugin {
export class TelemetryPlugin implements Plugin<TelemetryPluginSetup, TelemetryPluginStart> {
private readonly logger: Logger;
private readonly currentKibanaVersion: string;
private readonly config$: Observable<TelemetryConfigType>;
Expand All @@ -76,8 +97,8 @@ export class TelemetryPlugin implements Plugin {

public async setup(
{ elasticsearch, http, savedObjects }: CoreSetup,
{ usageCollection, telemetryCollectionManager }: TelemetryPluginsSetup
) {
{ usageCollection, telemetryCollectionManager }: TelemetryPluginsDepsSetup
): Promise<TelemetryPluginSetup> {
const currentKibanaVersion = this.currentKibanaVersion;
const config$ = this.config$;
const isDev = this.isDev;
Expand All @@ -96,9 +117,19 @@ export class TelemetryPlugin implements Plugin {

this.registerMappings((opts) => savedObjects.registerType(opts));
this.registerUsageCollectors(usageCollection);

return {
getTelemetryUrl: async () => {
const config = await config$.pipe(take(1)).toPromise();
return new URL(config.url);
},
};
}

public async start(core: CoreStart, { telemetryCollectionManager }: TelemetryPluginsStart) {
public async start(
core: CoreStart,
{ telemetryCollectionManager }: TelemetryPluginsDepsStart
): Promise<TelemetryPluginStart> {
const { savedObjects, uiSettings } = core;
this.savedObjectsClient = savedObjects.createInternalRepository();
const savedObjectsClient = new SavedObjectsClient(this.savedObjectsClient);
Expand All @@ -111,6 +142,25 @@ export class TelemetryPlugin implements Plugin {
}

this.fetcherTask.start(core, { telemetryCollectionManager });

return {
getIsOptedIn: async () => {
const internalRepository = new SavedObjectsClient(savedObjects.createInternalRepository());
const telemetrySavedObject = await getTelemetrySavedObject(internalRepository!);
const config = await this.config$.pipe(take(1)).toPromise();
const allowChangingOptInStatus = config.allowChangingOptInStatus;
const configTelemetryOptIn = typeof config.optIn === 'undefined' ? null : config.optIn;
Copy link
Member

Choose a reason for hiding this comment

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

NIT: I recently learned this can be also written as:

Suggested change
const configTelemetryOptIn = typeof config.optIn === 'undefined' ? null : config.optIn;
const configTelemetryOptIn = config.optIn ?? null;

🤯 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing_operator

const currentKibanaVersion = this.currentKibanaVersion;
const isOptedIn = getTelemetryOptIn({
currentKibanaVersion,
telemetrySavedObject,
allowChangingOptInStatus,
configTelemetryOptIn,
});

return isOptedIn === true;
},
};
}

private registerMappings(registerType: SavedObjectsRegisterType) {
Expand Down