[7.x] [Telemetry] Migrate public to NP (#56285) (#57534)

This commit is contained in:
Ahmad Bamieh 2020-02-13 11:41:31 +02:00 committed by GitHub
parent f3104aa77e
commit d02fb7a928
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
104 changed files with 1814 additions and 3098 deletions

View file

@ -37,7 +37,10 @@
"savedObjects": "src/plugins/saved_objects",
"server": "src/legacy/server",
"statusPage": "src/legacy/core_plugins/status_page",
"telemetry": "src/legacy/core_plugins/telemetry",
"telemetry": [
"src/legacy/core_plugins/telemetry",
"src/plugins/telemetry"
],
"tileMap": "src/legacy/core_plugins/tile_map",
"timelion": ["src/legacy/core_plugins/timelion", "src/legacy/core_plugins/vis_type_timelion", "src/plugins/timelion"],
"uiActions": "src/plugins/ui_actions",

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [ApplicationStart](./kibana-plugin-public.applicationstart.md) &gt; [currentAppId$](./kibana-plugin-public.applicationstart.currentappid_.md)
## ApplicationStart.currentAppId$ property
An observable that emits the current application id and each subsequent id update.
<b>Signature:</b>
```typescript
currentAppId$: Observable<string | undefined>;
```

View file

@ -16,6 +16,7 @@ export interface ApplicationStart
| Property | Type | Description |
| --- | --- | --- |
| [capabilities](./kibana-plugin-public.applicationstart.capabilities.md) | <code>RecursiveReadonly&lt;Capabilities&gt;</code> | Gets the read-only capabilities. |
| [currentAppId$](./kibana-plugin-public.applicationstart.currentappid_.md) | <code>Observable&lt;string &#124; undefined&gt;</code> | An observable that emits the current application id and each subsequent id update. |
## Methods

View file

@ -43,12 +43,17 @@ const createInternalSetupContractMock = (): jest.Mocked<InternalApplicationSetup
registerMountContext: jest.fn(),
});
const createStartContractMock = (): jest.Mocked<ApplicationStart> => ({
capabilities: capabilitiesServiceMock.createStartContract().capabilities,
navigateToApp: jest.fn(),
getUrlForApp: jest.fn(),
registerMountContext: jest.fn(),
});
const createStartContractMock = (): jest.Mocked<ApplicationStart> => {
const currentAppId$ = new Subject<string | undefined>();
return {
currentAppId$: currentAppId$.asObservable(),
capabilities: capabilitiesServiceMock.createStartContract().capabilities,
navigateToApp: jest.fn(),
getUrlForApp: jest.fn(),
registerMountContext: jest.fn(),
};
};
const createInternalStartContractMock = (): jest.Mocked<InternalApplicationStart> => {
const currentAppId$ = new Subject<string | undefined>();

View file

@ -612,11 +612,19 @@ export interface ApplicationStart {
contextName: T,
provider: IContextProvider<AppMountDeprecated, T>
): void;
/**
* An observable that emits the current application id and each subsequent id update.
*/
currentAppId$: Observable<string | undefined>;
}
/** @internal */
export interface InternalApplicationStart
extends Pick<ApplicationStart, 'capabilities' | 'navigateToApp' | 'getUrlForApp'> {
extends Pick<
ApplicationStart,
'capabilities' | 'navigateToApp' | 'getUrlForApp' | 'currentAppId$'
> {
/**
* Apps available based on the current capabilities.
* Should be used to show navigation links and make routing decisions.
@ -640,7 +648,6 @@ export interface InternalApplicationStart
): void;
// Internal APIs
currentAppId$: Observable<string | undefined>;
getComponent(): JSX.Element | null;
}

View file

@ -121,6 +121,7 @@ export class LegacyPlatformService {
const legacyCore: LegacyCoreStart = {
...core,
application: {
currentAppId$: core.application.currentAppId$,
capabilities: core.application.capabilities,
getUrlForApp: core.application.getUrlForApp,
navigateToApp: core.application.navigateToApp,

View file

@ -134,6 +134,7 @@ export function createPluginStartContext<
): CoreStart {
return {
application: {
currentAppId$: deps.application.currentAppId$,
capabilities: deps.application.capabilities,
navigateToApp: deps.application.navigateToApp,
getUrlForApp: deps.application.getUrlForApp,

View file

@ -98,6 +98,7 @@ export interface ApplicationSetup {
// @public (undocumented)
export interface ApplicationStart {
capabilities: RecursiveReadonly<Capabilities>;
currentAppId$: Observable<string | undefined>;
getUrlForApp(appId: string, options?: {
path?: string;
}): string;

View file

@ -115,6 +115,9 @@ export const coreDeprecationProvider: ConfigDeprecationProvider = ({
renameFromRoot('optimize.lazyHost', 'optimize.watchHost'),
renameFromRoot('optimize.lazyPrebuild', 'optimize.watchPrebuild'),
renameFromRoot('optimize.lazyProxyTimeout', 'optimize.watchProxyTimeout'),
renameFromRoot('xpack.xpack_main.telemetry.config', 'telemetry.config'),
renameFromRoot('xpack.xpack_main.telemetry.url', 'telemetry.url'),
renameFromRoot('xpack.xpack_main.telemetry.enabled', 'telemetry.enabled'),
renameFromRoot('xpack.telemetry.enabled', 'telemetry.enabled'),
renameFromRoot('xpack.telemetry.config', 'telemetry.config'),
renameFromRoot('xpack.telemetry.banner', 'telemetry.banner'),

View file

@ -18,30 +18,7 @@
*/
import { npSetup, npStart } from 'ui/new_platform';
import chrome from 'ui/chrome';
import { HomePlugin, LegacyAngularInjectedDependencies } from './plugin';
import { TelemetryOptInProvider } from '../../../telemetry/public/services';
import { IPrivate } from '../../../../../plugins/kibana_legacy/public';
/**
* Get dependencies relying on the global angular context.
* They also have to get resolved together with the legacy imports above
*/
async function getAngularDependencies(): Promise<LegacyAngularInjectedDependencies> {
const injector = await chrome.dangerouslyGetActiveInjector();
const Private = injector.get<IPrivate>('Private');
const telemetryEnabled = npStart.core.injectedMetadata.getInjectedVar('telemetryEnabled');
const telemetryBanner = npStart.core.injectedMetadata.getInjectedVar('telemetryBanner');
const telemetryOptInProvider = Private(TelemetryOptInProvider);
return {
telemetryOptInProvider,
shouldShowTelemetryOptIn:
telemetryEnabled && telemetryBanner && !telemetryOptInProvider.getOptIn(),
};
}
import { HomePlugin } from './plugin';
(async () => {
const instance = new HomePlugin();
@ -49,10 +26,8 @@ async function getAngularDependencies(): Promise<LegacyAngularInjectedDependenci
...npSetup.plugins,
__LEGACY: {
metadata: npStart.core.injectedMetadata.getLegacyMetadata(),
getAngularDependencies,
},
});
instance.start(npStart.core, {
...npStart.plugins,
});
instance.start(npStart.core, npStart.plugins);
})();

View file

@ -29,6 +29,7 @@ import {
UiSettingsState,
} from 'kibana/public';
import { UiStatsMetricType } from '@kbn/analytics';
import { TelemetryPluginStart } from '../../../../../plugins/telemetry/public';
import {
Environment,
HomePublicPluginSetup,
@ -53,7 +54,6 @@ export interface HomeKibanaServices {
};
getInjected: (name: string, defaultValue?: any) => unknown;
chrome: ChromeStart;
telemetryOptInProvider: any;
uiSettings: IUiSettingsClient;
config: KibanaLegacySetup['config'];
homeConfig: HomePublicPluginSetup['config'];
@ -64,10 +64,10 @@ export interface HomeKibanaServices {
banners: OverlayStart['banners'];
trackUiMetric: (type: UiStatsMetricType, eventNames: string | string[], count?: number) => void;
getBasePath: () => string;
shouldShowTelemetryOptIn: boolean;
docLinks: DocLinksStart;
addBasePath: (url: string) => string;
environment: Environment;
telemetry?: TelemetryPluginStart;
}
let services: HomeKibanaServices | null = null;

View file

@ -1054,7 +1054,6 @@ exports[`home welcome should show the normal home page if welcome screen is disa
exports[`home welcome should show the welcome screen if enabled, and there are no index patterns defined 1`] = `
<Welcome
onOptInSeen={[Function]}
onSkip={[Function]}
urlBasePath="goober"
/>

View file

@ -67,44 +67,6 @@ exports[`should render a Welcome screen with no telemetry disclaimer 1`] = `
<EuiSpacer
size="s"
/>
<EuiTextColor
className="euiText--small"
color="subdued"
>
<FormattedMessage
defaultMessage="To learn about how usage data helps us manage and improve our products and services, see our "
id="kbn.home.dataManagementDisclaimerPrivacy"
values={Object {}}
/>
<EuiLink
href="https://www.elastic.co/legal/privacy-statement"
rel="noopener"
target="_blank"
>
<FormattedMessage
defaultMessage="Privacy Statement."
id="kbn.home.dataManagementDisclaimerPrivacyLink"
values={Object {}}
/>
</EuiLink>
<FormattedMessage
defaultMessage=" To start collection, "
id="kbn.home.dataManagementEnableCollection"
values={Object {}}
/>
<EuiLink
href="#/management/kibana/settings"
>
<FormattedMessage
defaultMessage="enable usage data here."
id="kbn.home.dataManagementEnableCollectionLink"
values={Object {}}
/>
</EuiLink>
</EuiTextColor>
<EuiSpacer
size="xs"
/>
</EuiFlexItem>
</EuiFlexGroup>
</div>
@ -200,16 +162,16 @@ exports[`should render a Welcome screen with the telemetry disclaimer 1`] = `
/>
</EuiLink>
<FormattedMessage
defaultMessage=" To start collection, "
id="kbn.home.dataManagementEnableCollection"
defaultMessage=" To stop collection, "
id="kbn.home.dataManagementDisableCollection"
values={Object {}}
/>
<EuiLink
href="#/management/kibana/settings"
>
<FormattedMessage
defaultMessage="enable usage data here."
id="kbn.home.dataManagementEnableCollectionLink"
defaultMessage="disable usage data here."
id="kbn.home.dataManagementDisableCollectionLink"
values={Object {}}
/>
</EuiLink>

View file

@ -51,7 +51,6 @@ export class Home extends Component {
getServices().homeConfig.disableWelcomeScreen ||
props.localStorage.getItem(KEY_ENABLE_WELCOME) === 'false'
);
const currentOptInStatus = this.props.getOptInStatus();
this.state = {
// If welcome is enabled, we wait for loading to complete
// before rendering. This prevents an annoying flickering
@ -60,7 +59,6 @@ export class Home extends Component {
isLoading: isWelcomeEnabled,
isNewKibanaInstance: false,
isWelcomeEnabled,
currentOptInStatus,
};
}
@ -224,8 +222,7 @@ export class Home extends Component {
<Welcome
onSkip={this.skipWelcome}
urlBasePath={this.props.urlBasePath}
onOptInSeen={this.props.onOptInSeen}
currentOptInStatus={this.state.currentOptInStatus}
telemetry={this.props.telemetry}
/>
);
}
@ -264,6 +261,8 @@ Home.propTypes = {
localStorage: PropTypes.object.isRequired,
urlBasePath: PropTypes.string.isRequired,
mlEnabled: PropTypes.bool.isRequired,
onOptInSeen: PropTypes.func.isRequired,
getOptInStatus: PropTypes.func.isRequired,
telemetry: PropTypes.shape({
telemetryService: PropTypes.any,
telemetryNotifications: PropTypes.any,
}),
};

View file

@ -35,7 +35,7 @@ export function HomeApp({ directories }) {
getBasePath,
addBasePath,
environment,
telemetryOptInProvider: { setOptInNoticeSeen, getOptIn },
telemetry,
} = getServices();
const isCloudEnabled = environment.cloud;
const mlEnabled = environment.ml;
@ -84,8 +84,7 @@ export function HomeApp({ directories }) {
find={savedObjectsClient.find}
localStorage={localStorage}
urlBasePath={getBasePath()}
onOptInSeen={setOptInNoticeSeen}
getOptInStatus={getOptIn}
telemetry={telemetry}
/>
</Route>
<Route path="/home">

View file

@ -20,6 +20,7 @@
import React from 'react';
import { shallow } from 'enzyme';
import { Welcome } from './welcome';
import { telemetryPluginMock } from '../../../../../../../plugins/telemetry/public/mocks';
jest.mock('../../kibana_services', () => ({
getServices: () => ({
@ -29,27 +30,32 @@ jest.mock('../../kibana_services', () => ({
}));
test('should render a Welcome screen with the telemetry disclaimer', () => {
const telemetry = telemetryPluginMock.createSetupContract();
const component = shallow(
// @ts-ignore
<Welcome urlBasePath="/" onSkip={() => {}} onOptInSeen={() => {}} />
<Welcome urlBasePath="/" onSkip={() => {}} telemetry={telemetry} />
);
expect(component).toMatchSnapshot();
});
test('should render a Welcome screen with the telemetry disclaimer when optIn is true', () => {
const telemetry = telemetryPluginMock.createSetupContract();
telemetry.telemetryService.getIsOptedIn = jest.fn().mockReturnValue(true);
const component = shallow(
// @ts-ignore
<Welcome urlBasePath="/" onSkip={() => {}} onOptInSeen={() => {}} currentOptInStatus={true} />
<Welcome urlBasePath="/" onSkip={() => {}} telemetry={telemetry} />
);
expect(component).toMatchSnapshot();
});
test('should render a Welcome screen with the telemetry disclaimer when optIn is false', () => {
const telemetry = telemetryPluginMock.createSetupContract();
telemetry.telemetryService.getIsOptedIn = jest.fn().mockReturnValue(false);
const component = shallow(
// @ts-ignore
<Welcome urlBasePath="/" onSkip={() => {}} onOptInSeen={() => {}} currentOptInStatus={false} />
<Welcome urlBasePath="/" onSkip={() => {}} telemetry={telemetry} />
);
expect(component).toMatchSnapshot();
@ -59,19 +65,21 @@ test('should render a Welcome screen with no telemetry disclaimer', () => {
// @ts-ignore
const component = shallow(
// @ts-ignore
<Welcome urlBasePath="/" onSkip={() => {}} onOptInSeen={() => {}} />
<Welcome urlBasePath="/" onSkip={() => {}} telemetry={null} />
);
expect(component).toMatchSnapshot();
});
test('fires opt-in seen when mounted', () => {
const seen = jest.fn();
const telemetry = telemetryPluginMock.createSetupContract();
const mockSetOptedInNoticeSeen = jest.fn();
// @ts-ignore
telemetry.telemetryNotifications.setOptedInNoticeSeen = mockSetOptedInNoticeSeen;
shallow(
// @ts-ignore
<Welcome urlBasePath="/" onSkip={() => {}} onOptInSeen={seen} />
<Welcome urlBasePath="/" onSkip={() => {}} telemetry={telemetry} />
);
expect(seen).toHaveBeenCalled();
expect(mockSetOptedInNoticeSeen).toHaveBeenCalled();
});

View file

@ -38,13 +38,14 @@ import {
import { METRIC_TYPE } from '@kbn/analytics';
import { FormattedMessage } from '@kbn/i18n/react';
import { getServices } from '../../kibana_services';
import { TelemetryPluginStart } from '../../../../../../../plugins/telemetry/public';
import { PRIVACY_STATEMENT_URL } from '../../../../../../../plugins/telemetry/common/constants';
import { SampleDataCard } from './sample_data';
interface Props {
urlBasePath: string;
onSkip: () => void;
onOptInSeen: () => any;
currentOptInStatus: boolean;
telemetry?: TelemetryPluginStart;
}
/**
@ -75,8 +76,11 @@ export class Welcome extends React.Component<Props> {
};
componentDidMount() {
const { telemetry } = this.props;
this.services.trackUiMetric(METRIC_TYPE.LOADED, 'welcomeScreenMount');
this.props.onOptInSeen();
if (telemetry) {
telemetry.telemetryNotifications.setOptedInNoticeSeen();
}
document.addEventListener('keydown', this.hideOnEsc);
}
@ -85,7 +89,13 @@ export class Welcome extends React.Component<Props> {
}
private renderTelemetryEnabledOrDisabledText = () => {
if (this.props.currentOptInStatus) {
const { telemetry } = this.props;
if (!telemetry) {
return null;
}
const isOptedIn = telemetry.telemetryService.getIsOptedIn();
if (isOptedIn) {
return (
<Fragment>
<FormattedMessage
@ -119,7 +129,7 @@ export class Welcome extends React.Component<Props> {
};
render() {
const { urlBasePath } = this.props;
const { urlBasePath, telemetry } = this.props;
return (
<EuiPortal>
<div className="homWelcome">
@ -154,24 +164,24 @@ export class Welcome extends React.Component<Props> {
onDecline={this.onSampleDataDecline}
/>
<EuiSpacer size="s" />
<EuiTextColor className="euiText--small" color="subdued">
<FormattedMessage
id="kbn.home.dataManagementDisclaimerPrivacy"
defaultMessage="To learn about how usage data helps us manage and improve our products and services, see our "
/>
<EuiLink
href="https://www.elastic.co/legal/privacy-statement"
target="_blank"
rel="noopener"
>
<FormattedMessage
id="kbn.home.dataManagementDisclaimerPrivacyLink"
defaultMessage="Privacy Statement."
/>
</EuiLink>
{this.renderTelemetryEnabledOrDisabledText()}
</EuiTextColor>
<EuiSpacer size="xs" />
{!!telemetry && (
<Fragment>
<EuiTextColor className="euiText--small" color="subdued">
<FormattedMessage
id="kbn.home.dataManagementDisclaimerPrivacy"
defaultMessage="To learn about how usage data helps us manage and improve our products and services, see our "
/>
<EuiLink href={PRIVACY_STATEMENT_URL} target="_blank" rel="noopener">
<FormattedMessage
id="kbn.home.dataManagementDisclaimerPrivacyLink"
defaultMessage="Privacy Statement."
/>
</EuiLink>
{this.renderTelemetryEnabledOrDisabledText()}
</EuiTextColor>
<EuiSpacer size="xs" />
</Fragment>
)}
</EuiFlexItem>
</EuiFlexGroup>
</div>

View file

@ -20,6 +20,7 @@
import { CoreSetup, CoreStart, LegacyNavLink, Plugin, UiSettingsState } from 'kibana/public';
import { DataPublicPluginStart } from 'src/plugins/data/public';
import { TelemetryPluginStart } from 'src/plugins/telemetry/public';
import { setServices } from './kibana_services';
import { KibanaLegacySetup } from '../../../../../plugins/kibana_legacy/public';
import { UsageCollectionSetup } from '../../../../../plugins/usage_collection/public';
@ -30,14 +31,10 @@ import {
FeatureCatalogueEntry,
} from '../../../../../plugins/home/public';
export interface LegacyAngularInjectedDependencies {
telemetryOptInProvider: any;
shouldShowTelemetryOptIn: boolean;
}
export interface HomePluginStartDependencies {
data: DataPublicPluginStart;
home: HomePublicPluginStart;
telemetry?: TelemetryPluginStart;
}
export interface HomePluginSetupDependencies {
@ -55,7 +52,6 @@ export interface HomePluginSetupDependencies {
devMode: boolean;
uiSettings: { defaults: UiSettingsState; user?: UiSettingsState | undefined };
};
getAngularDependencies: () => Promise<LegacyAngularInjectedDependencies>;
};
usageCollection: UsageCollectionSetup;
kibanaLegacy: KibanaLegacySetup;
@ -67,6 +63,7 @@ export class HomePlugin implements Plugin {
private savedObjectsClient: any = null;
private environment: Environment | null = null;
private directories: readonly FeatureCatalogueEntry[] | null = null;
private telemetry?: TelemetryPluginStart;
setup(
core: CoreSetup,
@ -74,7 +71,7 @@ export class HomePlugin implements Plugin {
home,
kibanaLegacy,
usageCollection,
__LEGACY: { getAngularDependencies, ...legacyServices },
__LEGACY: { ...legacyServices },
}: HomePluginSetupDependencies
) {
kibanaLegacy.registerLegacyApp({
@ -82,7 +79,6 @@ export class HomePlugin implements Plugin {
title: 'Home',
mount: async ({ core: contextCore }, params) => {
const trackUiMetric = usageCollection.reportUiStats.bind(usageCollection, 'Kibana_home');
const angularDependencies = await getAngularDependencies();
setServices({
...legacyServices,
trackUiMetric,
@ -92,6 +88,7 @@ export class HomePlugin implements Plugin {
getInjected: core.injectedMetadata.getInjectedVar,
docLinks: contextCore.docLinks,
savedObjectsClient: this.savedObjectsClient!,
telemetry: this.telemetry,
chrome: contextCore.chrome,
uiSettings: core.uiSettings,
addBasePath: core.http.basePath.prepend,
@ -101,7 +98,6 @@ export class HomePlugin implements Plugin {
config: kibanaLegacy.config,
homeConfig: home.config,
directories: this.directories!,
...angularDependencies,
});
const { renderApp } = await import('./np_ready/application');
return await renderApp(params.element);
@ -109,10 +105,11 @@ export class HomePlugin implements Plugin {
});
}
start(core: CoreStart, { data, home }: HomePluginStartDependencies) {
start(core: CoreStart, { data, home, telemetry }: HomePluginStartDependencies) {
this.environment = home.environment.get();
this.directories = home.featureCatalogue.get();
this.dataStart = data;
this.telemetry = telemetry;
this.savedObjectsClient = core.savedObjects.client;
}

View file

@ -43,11 +43,6 @@ export const getConfigTelemetryDesc = () => {
*/
export const REPORT_INTERVAL_MS = 86400000;
/*
* Key for the localStorage service
*/
export const LOCALSTORAGE_KEY = 'telemetry.data';
/**
* Link to the Elastic Telemetry privacy statement.
*/

View file

@ -1,41 +0,0 @@
/*
* 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 { KibanaConfig } from 'src/legacy/server/kbn_server';
export function getXpackConfigWithDeprecated(config: KibanaConfig, configPath: string) {
try {
const deprecatedXpackmainConfig = config.get(`xpack.xpack_main.${configPath}`);
if (typeof deprecatedXpackmainConfig !== 'undefined') {
return deprecatedXpackmainConfig;
}
} catch (err) {
// swallow error
}
try {
const deprecatedXpackConfig = config.get(`xpack.${configPath}`);
if (typeof deprecatedXpackConfig !== 'undefined') {
return deprecatedXpackConfig;
}
} catch (err) {
// swallow error
}
return config.get(configPath);
}

View file

@ -22,14 +22,17 @@ import { resolve } from 'path';
import JoiNamespace from 'joi';
import { Server } from 'hapi';
import { CoreSetup, PluginInitializerContext } from 'src/core/server';
import { i18n } from '@kbn/i18n';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { getConfigPath } from '../../../core/server/path';
// @ts-ignore
import mappings from './mappings.json';
import { CONFIG_TELEMETRY, getConfigTelemetryDesc } from './common/constants';
import { getXpackConfigWithDeprecated } from './common/get_xpack_config_with_deprecated';
import { telemetryPlugin, replaceTelemetryInjectedVars, FetcherTask, PluginsSetup } from './server';
import {
telemetryPlugin,
replaceTelemetryInjectedVars,
FetcherTask,
PluginsSetup,
handleOldSettings,
} from './server';
const ENDPOINT_VERSION = 'v2';
@ -76,16 +79,6 @@ const telemetry = (kibana: any) => {
},
uiExports: {
managementSections: ['plugins/telemetry/views/management'],
uiSettingDefaults: {
[CONFIG_TELEMETRY]: {
name: i18n.translate('telemetry.telemetryConfigTitle', {
defaultMessage: 'Telemetry opt-in',
}),
description: getConfigTelemetryDesc(),
value: false,
readonly: true,
},
},
savedObjectSchemas: {
telemetry: {
isNamespaceAgnostic: true,
@ -98,11 +91,11 @@ const telemetry = (kibana: any) => {
injectDefaultVars(server: Server) {
const config = server.config();
return {
telemetryEnabled: getXpackConfigWithDeprecated(config, 'telemetry.enabled'),
telemetryUrl: getXpackConfigWithDeprecated(config, 'telemetry.url'),
telemetryEnabled: config.get('telemetry.enabled'),
telemetryUrl: config.get('telemetry.url'),
telemetryBanner:
config.get('telemetry.allowChangingOptInStatus') !== false &&
getXpackConfigWithDeprecated(config, 'telemetry.banner'),
config.get('telemetry.banner'),
telemetryOptedIn: config.get('telemetry.optIn'),
telemetryOptInStatusUrl: config.get('telemetry.optInStatusUrl'),
allowChangingOptInStatus: config.get('telemetry.allowChangingOptInStatus'),
@ -110,14 +103,13 @@ const telemetry = (kibana: any) => {
telemetryNotifyUserAboutOptInDefault: false,
};
},
hacks: ['plugins/telemetry/hacks/telemetry_init', 'plugins/telemetry/hacks/telemetry_opt_in'],
mappings,
},
postInit(server: Server) {
const fetcherTask = new FetcherTask(server);
fetcherTask.start();
},
init(server: Server) {
async init(server: Server) {
const { usageCollection } = server.newPlatform.setup.plugins;
const initializerContext = {
env: {
@ -145,6 +137,12 @@ const telemetry = (kibana: any) => {
log: server.log,
} as any) as CoreSetup;
try {
await handleOldSettings(server);
} catch (err) {
server.log(['warning', 'telemetry'], 'Unable to update legacy telemetry configs.');
}
const pluginsSetup: PluginsSetup = {
usageCollection,
};

View file

@ -1,80 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`TelemetryForm doesn't render form when not allowed to change optIn status 1`] = `""`;
exports[`TelemetryForm renders as expected when allows to change optIn status 1`] = `
<Fragment>
<EuiPanel
paddingSize="l"
>
<EuiForm>
<EuiText>
<EuiFlexGroup
alignItems="baseline"
>
<EuiFlexItem
grow={false}
>
<h2>
<FormattedMessage
defaultMessage="Usage Data"
id="telemetry.usageDataTitle"
values={Object {}}
/>
</h2>
</EuiFlexItem>
</EuiFlexGroup>
</EuiText>
<EuiSpacer
size="s"
/>
<Field
clear={[Function]}
enableSaving={true}
save={[Function]}
setting={
Object {
"ariaName": "Provide usage statistics",
"defVal": true,
"description": <React.Fragment>
<p>
<FormattedMessage
defaultMessage="Enabling data usage collection helps us manage and improve our products and services. See our {privacyStatementLink} for more details."
id="telemetry.telemetryConfigAndLinkDescription"
values={
Object {
"privacyStatementLink": <ForwardRef
href="https://www.elastic.co/legal/privacy-statement"
target="_blank"
>
<FormattedMessage
defaultMessage="Privacy Statement"
id="telemetry.readOurUsageDataPrivacyStatementLinkText"
values={Object {}}
/>
</ForwardRef>,
}
}
/>
</p>
<p>
<ForwardRef
onClick={[Function]}
>
<FormattedMessage
defaultMessage="See an example of what we collect"
id="telemetry.seeExampleOfWhatWeCollectLinkText"
values={Object {}}
/>
</ForwardRef>
</p>
</React.Fragment>,
"type": "boolean",
"value": false,
}
}
/>
</EuiForm>
</EuiPanel>
</Fragment>
`;

View file

@ -1,83 +0,0 @@
/*
* 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 { mockInjectedMetadata } from '../services/telemetry_opt_in.test.mocks';
import React from 'react';
import { shallowWithIntl } from 'test_utils/enzyme_helpers';
import { TelemetryForm } from './telemetry_form';
import { TelemetryOptInProvider } from '../services';
const buildTelemetryOptInProvider = () => {
const mockHttp = {
post: jest.fn(),
};
const mockInjector = {
get: key => {
switch (key) {
case '$http':
return mockHttp;
case 'allowChangingOptInStatus':
return true;
default:
return null;
}
},
};
const chrome = {
addBasePath: url => url,
};
return new TelemetryOptInProvider(mockInjector, chrome);
};
describe('TelemetryForm', () => {
it('renders as expected when allows to change optIn status', () => {
mockInjectedMetadata({ telemetryOptedIn: null, allowChangingOptInStatus: true });
expect(
shallowWithIntl(
<TelemetryForm
spacesEnabled={false}
query={{ text: '' }}
onQueryMatchChange={jest.fn()}
telemetryOptInProvider={buildTelemetryOptInProvider()}
enableSaving={true}
/>
)
).toMatchSnapshot();
});
it(`doesn't render form when not allowed to change optIn status`, () => {
mockInjectedMetadata({ telemetryOptedIn: null, allowChangingOptInStatus: false });
expect(
shallowWithIntl(
<TelemetryForm
spacesEnabled={false}
query={{ text: '' }}
onQueryMatchChange={jest.fn()}
telemetryOptInProvider={buildTelemetryOptInProvider()}
enableSaving={true}
/>
)
).toMatchSnapshot();
});
});

View file

@ -1,55 +0,0 @@
/*
* 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 expect from '@kbn/expect';
import sinon from 'sinon';
import { fetchTelemetry } from '../fetch_telemetry';
describe('fetch_telemetry', () => {
it('fetchTelemetry calls expected URL with 20 minutes - now', () => {
const response = Promise.resolve();
const $http = {
post: sinon.stub(),
};
const basePath = 'fake';
const moment = {
subtract: sinon.stub(),
toISOString: () => 'max123',
};
moment.subtract.withArgs(20, 'minutes').returns({
toISOString: () => 'min456',
});
$http.post
.withArgs(`fake/api/telemetry/v2/clusters/_stats`, {
unencrypted: true,
timeRange: {
min: 'min456',
max: 'max123',
},
})
.returns(response);
expect(fetchTelemetry($http, { basePath, _moment: () => moment, unencrypted: true })).to.be(
response
);
});
});

View file

@ -1,29 +0,0 @@
/*
* 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 { uiModules } from 'ui/modules';
// This overrides settings for other UI tests
uiModules
.get('kibana')
// disable stat reporting while running tests,
// MockInjector used in these tests is not impacted
.constant('telemetryEnabled', false)
.constant('telemetryOptedIn', null)
.constant('telemetryUrl', 'not.a.valid.url.0');

View file

@ -1,44 +0,0 @@
/*
* 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 uiChrome from 'ui/chrome';
import moment from 'moment';
/**
* Fetch Telemetry data by calling the Kibana API.
*
* @param {Object} $http The HTTP handler
* @param {String} basePath The base URI
* @param {Function} _moment moment.js, but injectable for tests
* @return {Promise} An array of cluster Telemetry objects.
*/
export function fetchTelemetry(
$http,
{ basePath = uiChrome.getBasePath(), _moment = moment, unencrypted = false } = {}
) {
return $http.post(`${basePath}/api/telemetry/v2/clusters/_stats`, {
unencrypted,
timeRange: {
min: _moment()
.subtract(20, 'minutes')
.toISOString(),
max: _moment().toISOString(),
},
});
}

View file

@ -1,120 +0,0 @@
/*
* 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 { REPORT_INTERVAL_MS, LOCALSTORAGE_KEY } from '../../common/constants';
export class Telemetry {
/**
* @param {Object} $injector - AngularJS injector service
* @param {Function} fetchTelemetry Method used to fetch telemetry data (expects an array response)
*/
constructor($injector, fetchTelemetry) {
this._storage = $injector.get('localStorage');
this._$http = $injector.get('$http');
this._telemetryUrl = $injector.get('telemetryUrl');
this._telemetryOptedIn = $injector.get('telemetryOptedIn');
this._fetchTelemetry = fetchTelemetry;
this._sending = false;
// try to load the local storage data
const attributes = this._storage.get(LOCALSTORAGE_KEY) || {};
this._lastReport = attributes.lastReport;
}
_saveToBrowser() {
// we are the only code that manipulates this key, so it's safe to blindly overwrite the whole object
this._storage.set(LOCALSTORAGE_KEY, { lastReport: this._lastReport });
}
/**
* Determine if we are due to send a new report.
*
* @returns {Boolean} true if a new report should be sent. false otherwise.
*/
_checkReportStatus() {
// check if opt-in for telemetry is enabled
if (this._telemetryOptedIn) {
// returns NaN for any malformed or unset (null/undefined) value
const lastReport = parseInt(this._lastReport, 10);
// If it's been a day since we last sent telemetry
if (isNaN(lastReport) || Date.now() - lastReport > REPORT_INTERVAL_MS) {
return true;
}
}
return false;
}
/**
* Check report permission and if passes, send the report
*
* @returns {Promise} Always.
*/
_sendIfDue() {
if (this._sending || !this._checkReportStatus()) {
return Promise.resolve(false);
}
// mark that we are working so future requests are ignored until we're done
this._sending = true;
return (
this._fetchTelemetry()
.then(response => {
const clusters = [].concat(response.data);
return Promise.all(
clusters.map(cluster => {
const req = {
method: 'POST',
url: this._telemetryUrl,
data: cluster,
};
// if passing data externally, then suppress kbnXsrfToken
if (this._telemetryUrl.match(/^https/)) {
req.kbnXsrfToken = false;
}
return this._$http(req);
})
);
})
// the response object is ignored because we do not check it
.then(() => {
// we sent a report, so we need to record and store the current timestamp
this._lastReport = Date.now();
this._saveToBrowser();
})
// no ajaxErrorHandlers for telemetry
.catch(() => null)
.then(() => {
this._sending = false;
return true; // sent, but not necessarilly successfully
})
);
}
/**
* Public method
*
* @returns {Number} `window.setInterval` response to allow cancelling the interval.
*/
start() {
// continuously check if it's due time for a report
return window.setInterval(() => this._sendIfDue(), 60000);
}
} // end class

View file

@ -1,306 +0,0 @@
/*
* 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 { Telemetry } from './telemetry';
import { REPORT_INTERVAL_MS, LOCALSTORAGE_KEY } from '../../common/constants';
describe('telemetry class', () => {
const clusters = [{ cluster_uuid: 'fake-123' }, { cluster_uuid: 'fake-456' }];
const telemetryUrl = 'https://not.a.valid.url.0';
const mockFetchTelemetry = () => Promise.resolve({ data: clusters });
// returns a function that behaves like the injector by fetching the requested key from the object directly
// for example:
// { '$http': jest.fn() } would be how to mock the '$http' injector value
const mockInjectorFromObject = object => {
return { get: key => object[key] };
};
describe('constructor', () => {
test('defaults lastReport if unset', () => {
const injector = {
localStorage: {
get: jest.fn().mockReturnValueOnce(undefined),
},
$http: jest.fn(),
telemetryOptedIn: true,
telemetryUrl,
};
const telemetry = new Telemetry(mockInjectorFromObject(injector), mockFetchTelemetry);
expect(telemetry._storage).toBe(injector.localStorage);
expect(telemetry._$http).toBe(injector.$http);
expect(telemetry._telemetryOptedIn).toBe(injector.telemetryOptedIn);
expect(telemetry._telemetryUrl).toBe(injector.telemetryUrl);
expect(telemetry._fetchTelemetry).toBe(mockFetchTelemetry);
expect(telemetry._sending).toBe(false);
expect(telemetry._lastReport).toBeUndefined();
expect(injector.localStorage.get).toHaveBeenCalledTimes(1);
expect(injector.localStorage.get).toHaveBeenCalledWith(LOCALSTORAGE_KEY);
});
test('uses lastReport if set', () => {
const lastReport = Date.now();
const injector = {
localStorage: {
get: jest.fn().mockReturnValueOnce({ lastReport }),
},
$http: jest.fn(),
telemetryOptedIn: true,
telemetryUrl,
};
const telemetry = new Telemetry(mockInjectorFromObject(injector), mockFetchTelemetry);
expect(telemetry._storage).toBe(injector.localStorage);
expect(telemetry._$http).toBe(injector.$http);
expect(telemetry._telemetryOptedIn).toBe(injector.telemetryOptedIn);
expect(telemetry._telemetryUrl).toBe(injector.telemetryUrl);
expect(telemetry._fetchTelemetry).toBe(mockFetchTelemetry);
expect(telemetry._sending).toBe(false);
expect(telemetry._lastReport).toBe(lastReport);
expect(injector.localStorage.get).toHaveBeenCalledTimes(1);
expect(injector.localStorage.get).toHaveBeenCalledWith(LOCALSTORAGE_KEY);
});
});
test('_saveToBrowser uses _lastReport', () => {
const injector = {
localStorage: {
get: jest.fn().mockReturnValueOnce({ random: 'junk', gets: 'thrown away' }),
set: jest.fn(),
},
};
const lastReport = Date.now();
const telemetry = new Telemetry(mockInjectorFromObject(injector), mockFetchTelemetry);
telemetry._lastReport = lastReport;
telemetry._saveToBrowser();
expect(injector.localStorage.set).toHaveBeenCalledTimes(1);
expect(injector.localStorage.set).toHaveBeenCalledWith(LOCALSTORAGE_KEY, { lastReport });
});
describe('_checkReportStatus', () => {
// send the report if we get to check the time
const lastReportShouldSendNow = Date.now() - REPORT_INTERVAL_MS - 1;
test('returns false whenever telemetryOptedIn is null', () => {
const injector = {
localStorage: {
get: jest.fn().mockReturnValueOnce({ lastReport: lastReportShouldSendNow }),
},
telemetryOptedIn: null, // not yet opted in
};
const telemetry = new Telemetry(mockInjectorFromObject(injector), mockFetchTelemetry);
expect(telemetry._checkReportStatus()).toBe(false);
});
test('returns false whenever telemetryOptedIn is false', () => {
const injector = {
localStorage: {
get: jest.fn().mockReturnValueOnce({ lastReport: lastReportShouldSendNow }),
},
telemetryOptedIn: false, // opted out explicitly
};
const telemetry = new Telemetry(mockInjectorFromObject(injector), mockFetchTelemetry);
expect(telemetry._checkReportStatus()).toBe(false);
});
// FLAKY: https://github.com/elastic/kibana/issues/27922
test.skip('returns false if last report is too recent', () => {
const injector = {
localStorage: {
// we expect '>', not '>='
get: jest.fn().mockReturnValueOnce({ lastReport: Date.now() - REPORT_INTERVAL_MS }),
},
telemetryOptedIn: true,
};
const telemetry = new Telemetry(mockInjectorFromObject(injector), mockFetchTelemetry);
expect(telemetry._checkReportStatus()).toBe(false);
});
test('returns true if last report is not defined', () => {
const injector = {
localStorage: {
get: jest.fn().mockReturnValueOnce({}),
},
telemetryOptedIn: true,
};
const telemetry = new Telemetry(mockInjectorFromObject(injector), mockFetchTelemetry);
expect(telemetry._checkReportStatus()).toBe(true);
});
test('returns true if last report is defined and old enough', () => {
const injector = {
localStorage: {
get: jest.fn().mockReturnValueOnce({ lastReport: lastReportShouldSendNow }),
},
telemetryOptedIn: true,
};
const telemetry = new Telemetry(mockInjectorFromObject(injector), mockFetchTelemetry);
expect(telemetry._checkReportStatus()).toBe(true);
});
test('returns true if last report is defined and old enough as a string', () => {
const injector = {
localStorage: {
get: jest.fn().mockReturnValueOnce({ lastReport: lastReportShouldSendNow.toString() }),
},
telemetryOptedIn: true,
};
const telemetry = new Telemetry(mockInjectorFromObject(injector), mockFetchTelemetry);
expect(telemetry._checkReportStatus()).toBe(true);
});
test('returns true if last report is defined and malformed', () => {
const injector = {
localStorage: {
get: jest.fn().mockReturnValueOnce({ lastReport: { not: { a: 'number' } } }),
},
telemetryOptedIn: true,
};
const telemetry = new Telemetry(mockInjectorFromObject(injector), mockFetchTelemetry);
expect(telemetry._checkReportStatus()).toBe(true);
});
});
describe('_sendIfDue', () => {
test('ignores and returns false if already sending', () => {
const injector = {
localStorage: {
get: jest.fn().mockReturnValueOnce(undefined), // never sent
},
telemetryOptedIn: true,
};
const telemetry = new Telemetry(mockInjectorFromObject(injector), mockFetchTelemetry);
telemetry._sending = true;
return expect(telemetry._sendIfDue()).resolves.toBe(false);
});
test('ignores and returns false if _checkReportStatus says so', () => {
const injector = {
localStorage: {
get: jest.fn().mockReturnValueOnce(undefined), // never sent, so it would try if opted in
},
telemetryOptedIn: false, // opted out
};
const telemetry = new Telemetry(mockInjectorFromObject(injector), mockFetchTelemetry);
return expect(telemetry._sendIfDue()).resolves.toBe(false);
});
test('sends telemetry when requested', () => {
const now = Date.now();
const injector = {
$http: jest.fn().mockResolvedValue({}), // ignored response
localStorage: {
get: jest.fn().mockReturnValueOnce({ lastReport: now - REPORT_INTERVAL_MS - 1 }),
set: jest.fn(),
},
telemetryOptedIn: true,
telemetryUrl,
};
const telemetry = new Telemetry(mockInjectorFromObject(injector), mockFetchTelemetry);
expect.hasAssertions();
return telemetry._sendIfDue().then(result => {
expect(result).toBe(true);
expect(telemetry._sending).toBe(false);
// should be updated
const lastReport = telemetry._lastReport;
// if the test runs fast enough it should be exactly equal, but probably a few ms greater
expect(lastReport).toBeGreaterThanOrEqual(now);
expect(injector.$http).toHaveBeenCalledTimes(2);
// assert that it sent every cluster's telemetry
clusters.forEach(cluster => {
expect(injector.$http).toHaveBeenCalledWith({
method: 'POST',
url: telemetryUrl,
data: cluster,
kbnXsrfToken: false,
});
});
expect(injector.localStorage.set).toHaveBeenCalledTimes(1);
expect(injector.localStorage.set).toHaveBeenCalledWith(LOCALSTORAGE_KEY, { lastReport });
});
});
test('sends telemetry when requested and catches exceptions', () => {
const lastReport = Date.now() - REPORT_INTERVAL_MS - 1;
const injector = {
$http: jest.fn().mockRejectedValue(new Error('TEST - expected')), // caught failure
localStorage: {
get: jest.fn().mockReturnValueOnce({ lastReport }),
set: jest.fn(),
},
telemetryOptedIn: true,
telemetryUrl,
};
const telemetry = new Telemetry(mockInjectorFromObject(injector), mockFetchTelemetry);
expect.hasAssertions();
return telemetry._sendIfDue().then(result => {
expect(result).toBe(true); // attempted to send
expect(telemetry._sending).toBe(false);
// should be unchanged
expect(telemetry._lastReport).toBe(lastReport);
expect(injector.localStorage.set).toHaveBeenCalledTimes(0);
expect(injector.$http).toHaveBeenCalledTimes(2);
// assert that it sent every cluster's telemetry
clusters.forEach(cluster => {
expect(injector.$http).toHaveBeenCalledWith({
method: 'POST',
url: telemetryUrl,
data: cluster,
kbnXsrfToken: false,
});
});
});
});
});
test('start', () => {
const injector = {
localStorage: {
get: jest.fn().mockReturnValueOnce(undefined),
},
telemetryOptedIn: false, // opted out
};
const telemetry = new Telemetry(mockInjectorFromObject(injector), mockFetchTelemetry);
clearInterval(telemetry.start());
});
});

View file

@ -1,53 +0,0 @@
/*
* 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 { npStart } from 'ui/new_platform';
// @ts-ignore
import { uiModules } from 'ui/modules';
import { isUnauthenticated } from '../services';
// @ts-ignore
import { Telemetry } from './telemetry';
// @ts-ignore
import { fetchTelemetry } from './fetch_telemetry';
// @ts-ignore
import { isOptInHandleOldSettings } from './welcome_banner/handle_old_settings';
import { TelemetryOptInProvider } from '../services';
function telemetryInit($injector: any) {
const $http = $injector.get('$http');
const Private = $injector.get('Private');
const config = $injector.get('config');
const telemetryOptInProvider = Private(TelemetryOptInProvider);
const telemetryEnabled = npStart.core.injectedMetadata.getInjectedVar('telemetryEnabled');
const telemetryOptedIn = isOptInHandleOldSettings(config, telemetryOptInProvider);
const sendUsageFrom = npStart.core.injectedMetadata.getInjectedVar('telemetrySendUsageFrom');
if (telemetryEnabled && telemetryOptedIn && sendUsageFrom === 'browser') {
// no telemetry for non-logged in users
if (isUnauthenticated()) {
return;
}
const sender = new Telemetry($injector, () => fetchTelemetry($http));
sender.start();
}
}
uiModules.get('telemetry/hacks').run(telemetryInit);

View file

@ -1,77 +0,0 @@
/*
* 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 React from 'react';
import { banners, toastNotifications } from 'ui/notify';
import { EuiText } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
/**
* Handle clicks from the user on the opt-in banner.
*
* @param {Object} telemetryOptInProvider the telemetry opt-in provider
* @param {Boolean} optIn {@code true} to opt into telemetry.
* @param {Object} _banners Singleton banners. Can be overridden for tests.
* @param {Object} _toastNotifications Singleton toast notifications. Can be overridden for tests.
*/
export async function clickBanner(
telemetryOptInProvider,
optIn,
{ _banners = banners, _toastNotifications = toastNotifications } = {}
) {
const bannerId = telemetryOptInProvider.getBannerId();
let set = false;
try {
set = await telemetryOptInProvider.setOptIn(optIn);
} catch (err) {
// set is already false
console.log('Unexpected error while trying to save setting.', err);
}
if (set) {
_banners.remove(bannerId);
} else {
_toastNotifications.addDanger({
title: (
<FormattedMessage
id="telemetry.telemetryErrorNotificationMessageTitle"
defaultMessage="Telemetry Error"
/>
),
text: (
<EuiText>
<p>
<FormattedMessage
id="telemetry.telemetryErrorNotificationMessageDescription.unableToSaveTelemetryPreferenceText"
defaultMessage="Unable to save telemetry preference."
/>
</p>
<EuiText size="xs">
<FormattedMessage
id="telemetry.telemetryErrorNotificationMessageDescription.tryAgainText"
defaultMessage="Check that Kibana and Elasticsearch are still running, then try again."
/>
</EuiText>
</EuiText>
),
});
}
}

View file

@ -1,128 +0,0 @@
/*
* 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 { mockInjectedMetadata } from '../../services/telemetry_opt_in.test.mocks';
import sinon from 'sinon';
import { uiModules } from 'ui/modules';
uiModules
.get('kibana')
// disable stat reporting while running tests,
// MockInjector used in these tests is not impacted
.constant('telemetryOptedIn', null);
import { clickBanner } from './click_banner';
import { TelemetryOptInProvider } from '../../services/telemetry_opt_in';
const getMockInjector = ({ simulateFailure }) => {
const get = sinon.stub();
const mockHttp = {
post: sinon.stub(),
};
if (simulateFailure) {
mockHttp.post.returns(Promise.reject(new Error('something happened')));
} else {
mockHttp.post.returns(Promise.resolve({}));
}
get.withArgs('$http').returns(mockHttp);
return { get };
};
const getTelemetryOptInProvider = ({ simulateFailure = false, simulateError = false } = {}) => {
const injector = getMockInjector({ simulateFailure });
const chrome = {
addBasePath: url => url,
};
const provider = new TelemetryOptInProvider(injector, chrome, false);
if (simulateError) {
provider.setOptIn = () => Promise.reject('unhandled error');
}
return provider;
};
describe('click_banner', () => {
it('sets setting successfully and removes banner', async () => {
const banners = {
remove: sinon.spy(),
};
const optIn = true;
const bannerId = 'bruce-banner';
mockInjectedMetadata({ telemetryOptedIn: optIn, allowChangingOptInStatus: true });
const telemetryOptInProvider = getTelemetryOptInProvider();
telemetryOptInProvider.setBannerId(bannerId);
await clickBanner(telemetryOptInProvider, optIn, { _banners: banners });
expect(telemetryOptInProvider.getOptIn()).toBe(optIn);
expect(banners.remove.calledOnce).toBe(true);
expect(banners.remove.calledWith(bannerId)).toBe(true);
});
it('sets setting unsuccessfully, adds toast, and does not touch banner', async () => {
const toastNotifications = {
addDanger: sinon.spy(),
};
const banners = {
remove: sinon.spy(),
};
const optIn = true;
mockInjectedMetadata({ telemetryOptedIn: null, allowChangingOptInStatus: true });
const telemetryOptInProvider = getTelemetryOptInProvider({ simulateFailure: true });
await clickBanner(telemetryOptInProvider, optIn, {
_banners: banners,
_toastNotifications: toastNotifications,
});
expect(telemetryOptInProvider.getOptIn()).toBe(null);
expect(toastNotifications.addDanger.calledOnce).toBe(true);
expect(banners.remove.notCalled).toBe(true);
});
it('sets setting unsuccessfully with error, adds toast, and does not touch banner', async () => {
const toastNotifications = {
addDanger: sinon.spy(),
};
const banners = {
remove: sinon.spy(),
};
const optIn = false;
mockInjectedMetadata({ telemetryOptedIn: null, allowChangingOptInStatus: true });
const telemetryOptInProvider = getTelemetryOptInProvider({ simulateError: true });
await clickBanner(telemetryOptInProvider, optIn, {
_banners: banners,
_toastNotifications: toastNotifications,
});
expect(telemetryOptInProvider.getOptIn()).toBe(null);
expect(toastNotifications.addDanger.calledOnce).toBe(true);
expect(banners.remove.notCalled).toBe(true);
});
});

View file

@ -1,85 +0,0 @@
/*
* 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 { CONFIG_TELEMETRY } from '../../../common/constants';
/**
* Clean up any old, deprecated settings and determine if we should continue.
*
* This <em>will</em> update the latest telemetry setting if necessary.
*
* @param {Object} config The advanced settings config object.
* @return {Boolean} {@code true} if the banner should still be displayed. {@code false} if the banner should not be displayed.
*/
const CONFIG_ALLOW_REPORT = 'xPackMonitoring:allowReport';
export async function handleOldSettings(config, telemetryOptInProvider) {
const CONFIG_SHOW_BANNER = 'xPackMonitoring:showBanner';
const oldAllowReportSetting = config.get(CONFIG_ALLOW_REPORT, null);
const oldTelemetrySetting = config.get(CONFIG_TELEMETRY, null);
let legacyOptInValue = null;
if (typeof oldTelemetrySetting === 'boolean') {
legacyOptInValue = oldTelemetrySetting;
} else if (typeof oldAllowReportSetting === 'boolean') {
legacyOptInValue = oldAllowReportSetting;
}
if (legacyOptInValue !== null) {
try {
await telemetryOptInProvider.setOptIn(legacyOptInValue);
// delete old keys once we've successfully changed the setting (if it fails, we just wait until next time)
config.remove(CONFIG_ALLOW_REPORT);
config.remove(CONFIG_SHOW_BANNER);
config.remove(CONFIG_TELEMETRY);
} finally {
return false;
}
}
const oldShowSetting = config.get(CONFIG_SHOW_BANNER, null);
if (oldShowSetting !== null) {
config.remove(CONFIG_SHOW_BANNER);
}
return true;
}
export async function isOptInHandleOldSettings(config, telemetryOptInProvider) {
const currentOptInSettting = telemetryOptInProvider.getOptIn();
if (typeof currentOptInSettting === 'boolean') {
return currentOptInSettting;
}
const oldTelemetrySetting = config.get(CONFIG_TELEMETRY, null);
if (typeof oldTelemetrySetting === 'boolean') {
return oldTelemetrySetting;
}
const oldAllowReportSetting = config.get(CONFIG_ALLOW_REPORT, null);
if (typeof oldAllowReportSetting === 'boolean') {
return oldAllowReportSetting;
}
return null;
}

View file

@ -1,208 +0,0 @@
/*
* 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 { mockInjectedMetadata } from '../../services/telemetry_opt_in.test.mocks';
import sinon from 'sinon';
import { CONFIG_TELEMETRY } from '../../../common/constants';
import { handleOldSettings } from './handle_old_settings';
import { TelemetryOptInProvider } from '../../services/telemetry_opt_in';
const getTelemetryOptInProvider = (enabled, { simulateFailure = false } = {}) => {
const $http = {
post: async () => {
if (simulateFailure) {
return Promise.reject(new Error('something happened'));
}
return {};
},
};
const chrome = {
addBasePath: url => url,
};
mockInjectedMetadata({ telemetryOptedIn: enabled, allowChangingOptInStatus: true });
const $injector = {
get: key => {
if (key === '$http') {
return $http;
}
throw new Error(`unexpected mock injector usage for ${key}`);
},
};
return new TelemetryOptInProvider($injector, chrome, false);
};
describe('handle_old_settings', () => {
it('re-uses old "allowReport" setting and stays opted in', async () => {
const config = {
get: sinon.stub(),
remove: sinon.spy(),
set: sinon.stub(),
};
const telemetryOptInProvider = getTelemetryOptInProvider(null);
expect(telemetryOptInProvider.getOptIn()).toBe(null);
config.get.withArgs('xPackMonitoring:allowReport', null).returns(true);
config.set.withArgs(CONFIG_TELEMETRY, true).returns(Promise.resolve(true));
expect(await handleOldSettings(config, telemetryOptInProvider)).toBe(false);
expect(config.get.calledTwice).toBe(true);
expect(config.set.called).toBe(false);
expect(config.remove.calledThrice).toBe(true);
expect(config.remove.getCall(0).args[0]).toBe('xPackMonitoring:allowReport');
expect(config.remove.getCall(1).args[0]).toBe('xPackMonitoring:showBanner');
expect(config.remove.getCall(2).args[0]).toBe(CONFIG_TELEMETRY);
expect(telemetryOptInProvider.getOptIn()).toBe(true);
});
it('re-uses old "telemetry:optIn" setting and stays opted in', async () => {
const config = {
get: sinon.stub(),
remove: sinon.spy(),
set: sinon.stub(),
};
const telemetryOptInProvider = getTelemetryOptInProvider(null);
expect(telemetryOptInProvider.getOptIn()).toBe(null);
config.get.withArgs('xPackMonitoring:allowReport', null).returns(false);
config.get.withArgs(CONFIG_TELEMETRY, null).returns(true);
expect(await handleOldSettings(config, telemetryOptInProvider)).toBe(false);
expect(config.get.calledTwice).toBe(true);
expect(config.set.called).toBe(false);
expect(config.remove.calledThrice).toBe(true);
expect(config.remove.getCall(0).args[0]).toBe('xPackMonitoring:allowReport');
expect(config.remove.getCall(1).args[0]).toBe('xPackMonitoring:showBanner');
expect(config.remove.getCall(2).args[0]).toBe(CONFIG_TELEMETRY);
expect(telemetryOptInProvider.getOptIn()).toBe(true);
});
it('re-uses old "allowReport" setting and stays opted out', async () => {
const config = {
get: sinon.stub(),
remove: sinon.spy(),
set: sinon.stub(),
};
const telemetryOptInProvider = getTelemetryOptInProvider(null);
expect(telemetryOptInProvider.getOptIn()).toBe(null);
config.get.withArgs('xPackMonitoring:allowReport', null).returns(false);
config.set.withArgs(CONFIG_TELEMETRY, false).returns(Promise.resolve(true));
expect(await handleOldSettings(config, telemetryOptInProvider)).toBe(false);
expect(config.get.calledTwice).toBe(true);
expect(config.set.called).toBe(false);
expect(config.remove.calledThrice).toBe(true);
expect(config.remove.getCall(0).args[0]).toBe('xPackMonitoring:allowReport');
expect(config.remove.getCall(1).args[0]).toBe('xPackMonitoring:showBanner');
expect(config.remove.getCall(2).args[0]).toBe(CONFIG_TELEMETRY);
expect(telemetryOptInProvider.getOptIn()).toBe(false);
});
it('re-uses old "telemetry:optIn" setting and stays opted out', async () => {
const config = {
get: sinon.stub(),
remove: sinon.spy(),
set: sinon.stub(),
};
const telemetryOptInProvider = getTelemetryOptInProvider(null);
config.get.withArgs(CONFIG_TELEMETRY, null).returns(false);
config.get.withArgs('xPackMonitoring:allowReport', null).returns(true);
expect(await handleOldSettings(config, telemetryOptInProvider)).toBe(false);
expect(config.get.calledTwice).toBe(true);
expect(config.set.called).toBe(false);
expect(config.remove.calledThrice).toBe(true);
expect(config.remove.getCall(0).args[0]).toBe('xPackMonitoring:allowReport');
expect(config.remove.getCall(1).args[0]).toBe('xPackMonitoring:showBanner');
expect(config.remove.getCall(2).args[0]).toBe(CONFIG_TELEMETRY);
expect(telemetryOptInProvider.getOptIn()).toBe(false);
});
it('acknowledges users old setting even if re-setting fails', async () => {
const config = {
get: sinon.stub(),
set: sinon.stub(),
};
const telemetryOptInProvider = getTelemetryOptInProvider(null, { simulateFailure: true });
config.get.withArgs('xPackMonitoring:allowReport', null).returns(false);
//todo: make the new version of this fail!
config.set.withArgs(CONFIG_TELEMETRY, false).returns(Promise.resolve(false));
// note: because it doesn't remove the old settings _and_ returns false, there's no risk of suddenly being opted in
expect(await handleOldSettings(config, telemetryOptInProvider)).toBe(false);
expect(config.get.calledTwice).toBe(true);
expect(config.set.called).toBe(false);
});
it('removes show banner setting and presents user with choice', async () => {
const config = {
get: sinon.stub(),
remove: sinon.spy(),
};
const telemetryOptInProvider = getTelemetryOptInProvider(null);
config.get.withArgs('xPackMonitoring:allowReport', null).returns(null);
config.get.withArgs('xPackMonitoring:showBanner', null).returns(false);
expect(await handleOldSettings(config, telemetryOptInProvider)).toBe(true);
expect(config.get.calledThrice).toBe(true);
expect(config.remove.calledOnce).toBe(true);
expect(config.remove.getCall(0).args[0]).toBe('xPackMonitoring:showBanner');
});
it('is effectively ignored on fresh installs', async () => {
const config = {
get: sinon.stub(),
};
const telemetryOptInProvider = getTelemetryOptInProvider(null);
config.get.withArgs('xPackMonitoring:allowReport', null).returns(null);
config.get.withArgs('xPackMonitoring:showBanner', null).returns(null);
expect(await handleOldSettings(config, telemetryOptInProvider)).toBe(true);
expect(config.get.calledThrice).toBe(true);
});
});

View file

@ -1,76 +0,0 @@
/*
* 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 chrome from 'ui/chrome';
import { fetchTelemetry } from '../fetch_telemetry';
import { renderBanner } from './render_banner';
import { renderOptedInBanner } from './render_notice_banner';
import { shouldShowBanner } from './should_show_banner';
import { shouldShowOptInBanner } from './should_show_opt_in_banner';
import { TelemetryOptInProvider, isUnauthenticated } from '../../services';
import { npStart } from 'ui/new_platform';
/**
* Add the Telemetry opt-in banner if the user has not already made a decision.
*
* Note: this is an async function, but Angular fails to use it as one. Its usage does not need to be awaited,
* and thus it can be wrapped in the run method to just be a normal, non-async function.
*
* @param {Object} $injector The Angular injector
*/
async function asyncInjectBanner($injector) {
const Private = $injector.get('Private');
const telemetryOptInProvider = Private(TelemetryOptInProvider);
const config = $injector.get('config');
// and no banner for non-logged in users
if (isUnauthenticated()) {
return;
}
// and no banner on status page
if (chrome.getApp().id === 'status_page') {
return;
}
const $http = $injector.get('$http');
// determine if the banner should be displayed
if (await shouldShowBanner(telemetryOptInProvider, config)) {
renderBanner(telemetryOptInProvider, () => fetchTelemetry($http, { unencrypted: true }));
}
if (await shouldShowOptInBanner(telemetryOptInProvider, config)) {
renderOptedInBanner(telemetryOptInProvider, () => fetchTelemetry($http, { unencrypted: true }));
}
}
/**
* Add the Telemetry opt-in banner when appropriate.
*
* @param {Object} $injector The Angular injector
*/
export function injectBanner($injector) {
const telemetryEnabled = npStart.core.injectedMetadata.getInjectedVar('telemetryEnabled');
const telemetryBanner = npStart.core.injectedMetadata.getInjectedVar('telemetryBanner');
if (telemetryEnabled && telemetryBanner) {
asyncInjectBanner($injector);
}
}

View file

@ -1,46 +0,0 @@
/*
* 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 React from 'react';
import { banners } from 'ui/notify';
import { clickBanner } from './click_banner';
import { OptInBanner } from '../../components/opt_in_banner_component';
/**
* Render the Telemetry Opt-in banner.
*
* @param {Object} telemetryOptInProvider The telemetry opt-in provider.
* @param {Function} fetchTelemetry Function to pull telemetry on demand.
* @param {Object} _banners Banners singleton, which can be overridden for tests.
*/
export function renderBanner(telemetryOptInProvider, fetchTelemetry, { _banners = banners } = {}) {
const bannerId = _banners.add({
component: (
<OptInBanner
optInClick={optIn => clickBanner(telemetryOptInProvider, optIn)}
fetchTelemetry={fetchTelemetry}
/>
),
priority: 10000,
});
telemetryOptInProvider.setBannerId(bannerId);
}

View file

@ -1,38 +0,0 @@
/*
* 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 React from 'react';
import { banners } from 'ui/notify';
import { OptedInBanner } from '../../components/opted_in_notice_banner';
/**
* Render the Telemetry Opt-in notice banner.
*
* @param {Object} telemetryOptInProvider The telemetry opt-in provider.
* @param {Object} _banners Banners singleton, which can be overridden for tests.
*/
export function renderOptedInBanner(telemetryOptInProvider, { _banners = banners } = {}) {
const bannerId = _banners.add({
component: <OptedInBanner onSeenBanner={telemetryOptInProvider.setOptInNoticeSeen} />,
priority: 10000,
});
telemetryOptInProvider.setOptInBannerNoticeId(bannerId);
}

View file

@ -1,40 +0,0 @@
/*
* 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 { handleOldSettings } from './handle_old_settings';
/**
* Determine if the banner should be displayed.
*
* This method can have side-effects related to deprecated config settings.
*
* @param {Object} config The advanced settings config object.
* @param {Object} _handleOldSettings handleOldSettings function, but overridable for tests.
* @return {Boolean} {@code true} if the banner should be displayed. {@code false} otherwise.
*/
export async function shouldShowBanner(
telemetryOptInProvider,
config,
{ _handleOldSettings = handleOldSettings } = {}
) {
return (
telemetryOptInProvider.getOptIn() === null &&
(await _handleOldSettings(config, telemetryOptInProvider))
);
}

View file

@ -1,91 +0,0 @@
/*
* 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 { mockInjectedMetadata } from '../../services/telemetry_opt_in.test.mocks';
import sinon from 'sinon';
import { CONFIG_TELEMETRY } from '../../../common/constants';
import { shouldShowBanner } from './should_show_banner';
import { TelemetryOptInProvider } from '../../services';
const getMockInjector = () => {
const get = sinon.stub();
const mockHttp = {
post: sinon.stub(),
};
get.withArgs('$http').returns(mockHttp);
return { get };
};
const getTelemetryOptInProvider = ({ telemetryOptedIn = null } = {}) => {
mockInjectedMetadata({ telemetryOptedIn, allowChangingOptInStatus: true });
const injector = getMockInjector();
const chrome = {
addBasePath: url => url,
};
return new TelemetryOptInProvider(injector, chrome);
};
describe('should_show_banner', () => {
it('returns whatever handleOldSettings does when telemetry opt-in setting is unset', async () => {
const config = { get: sinon.stub() };
const telemetryOptInProvider = getTelemetryOptInProvider();
const handleOldSettingsTrue = sinon.stub();
const handleOldSettingsFalse = sinon.stub();
config.get.withArgs(CONFIG_TELEMETRY, null).returns(null);
handleOldSettingsTrue.returns(Promise.resolve(true));
handleOldSettingsFalse.returns(Promise.resolve(false));
const showBannerTrue = await shouldShowBanner(telemetryOptInProvider, config, {
_handleOldSettings: handleOldSettingsTrue,
});
const showBannerFalse = await shouldShowBanner(telemetryOptInProvider, config, {
_handleOldSettings: handleOldSettingsFalse,
});
expect(showBannerTrue).toBe(true);
expect(showBannerFalse).toBe(false);
expect(config.get.callCount).toBe(0);
expect(handleOldSettingsTrue.calledOnce).toBe(true);
expect(handleOldSettingsFalse.calledOnce).toBe(true);
});
it('returns false if telemetry opt-in setting is set to true', async () => {
const config = { get: sinon.stub() };
const telemetryOptInProvider = getTelemetryOptInProvider({ telemetryOptedIn: true });
expect(await shouldShowBanner(telemetryOptInProvider, config)).toBe(false);
});
it('returns false if telemetry opt-in setting is set to false', async () => {
const config = { get: sinon.stub() };
const telemetryOptInProvider = getTelemetryOptInProvider({ telemetryOptedIn: false });
expect(await shouldShowBanner(telemetryOptInProvider, config)).toBe(false);
});
});

View file

@ -1,148 +0,0 @@
/*
* 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 { mockInjectedMetadata } from './telemetry_opt_in.test.mocks';
import { TelemetryOptInProvider } from './telemetry_opt_in';
describe('TelemetryOptInProvider', () => {
const setup = ({ optedIn, simulatePostError, simulatePutError }) => {
const mockHttp = {
post: jest.fn(async () => {
if (simulatePostError) {
return Promise.reject('Something happened');
}
}),
put: jest.fn(async () => {
if (simulatePutError) {
return Promise.reject('Something happened');
}
}),
};
const mockChrome = {
addBasePath: url => url,
};
mockInjectedMetadata({
telemetryOptedIn: optedIn,
allowChangingOptInStatus: true,
telemetryNotifyUserAboutOptInDefault: true,
});
const mockInjector = {
get: key => {
switch (key) {
case '$http': {
return mockHttp;
}
default:
throw new Error('unexpected injector request: ' + key);
}
},
};
const provider = new TelemetryOptInProvider(mockInjector, mockChrome, false);
return {
provider,
mockHttp,
};
};
it('should return the current opt-in status', () => {
const { provider: optedInProvider } = setup({ optedIn: true });
expect(optedInProvider.getOptIn()).toEqual(true);
const { provider: optedOutProvider } = setup({ optedIn: false });
expect(optedOutProvider.getOptIn()).toEqual(false);
});
it('should allow an opt-out to take place', async () => {
const { provider, mockHttp } = setup({ optedIn: true });
await provider.setOptIn(false);
expect(mockHttp.post).toHaveBeenCalledWith(`/api/telemetry/v2/optIn`, { enabled: false });
expect(provider.getOptIn()).toEqual(false);
});
it('should allow an opt-in to take place', async () => {
const { provider, mockHttp } = setup({ optedIn: false });
await provider.setOptIn(true);
expect(mockHttp.post).toHaveBeenCalledWith(`/api/telemetry/v2/optIn`, { enabled: true });
expect(provider.getOptIn()).toEqual(true);
});
it('should gracefully handle errors', async () => {
const { provider, mockHttp } = setup({ optedIn: false, simulatePostError: true });
await provider.setOptIn(true);
expect(mockHttp.post).toHaveBeenCalledWith(`/api/telemetry/v2/optIn`, { enabled: true });
// opt-in change should not be reflected
expect(provider.getOptIn()).toEqual(false);
});
it('should return the current bannerId', () => {
const { provider } = setup({});
const bannerId = 'bruce-banner';
provider.setBannerId(bannerId);
expect(provider.getBannerId()).toEqual(bannerId);
});
describe('Notice Banner', () => {
it('should return the current bannerId', () => {
const { provider } = setup({});
const bannerId = 'bruce-wayne';
provider.setOptInBannerNoticeId(bannerId);
expect(provider.getOptInBannerNoticeId()).toEqual(bannerId);
expect(provider.getBannerId()).not.toEqual(bannerId);
});
it('should persist that a user has seen the notice', async () => {
const { provider, mockHttp } = setup({});
await provider.setOptInNoticeSeen();
expect(mockHttp.put).toHaveBeenCalledWith(`/api/telemetry/v2/userHasSeenNotice`);
expect(provider.notifyUserAboutOptInDefault()).toEqual(false);
});
it('should only call the API once', async () => {
const { provider, mockHttp } = setup({});
await provider.setOptInNoticeSeen();
await provider.setOptInNoticeSeen();
expect(mockHttp.put).toHaveBeenCalledTimes(1);
expect(provider.notifyUserAboutOptInDefault()).toEqual(false);
});
it('should gracefully handle errors', async () => {
const { provider } = setup({ simulatePutError: true });
await provider.setOptInNoticeSeen();
// opt-in change should not be reflected
expect(provider.notifyUserAboutOptInDefault()).toEqual(true);
});
});
});

View file

@ -1,60 +0,0 @@
/*
* 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 {
injectedMetadataServiceMock,
notificationServiceMock,
overlayServiceMock,
} from '../../../../../core/public/mocks';
const injectedMetadataMock = injectedMetadataServiceMock.createStartContract();
export function mockInjectedMetadata({
telemetryOptedIn,
allowChangingOptInStatus,
telemetryNotifyUserAboutOptInDefault,
}) {
const mockGetInjectedVar = jest.fn().mockImplementation(key => {
switch (key) {
case 'telemetryOptedIn':
return telemetryOptedIn;
case 'allowChangingOptInStatus':
return allowChangingOptInStatus;
case 'telemetryNotifyUserAboutOptInDefault':
return telemetryNotifyUserAboutOptInDefault;
default:
throw new Error(`unexpected injectedVar ${key}`);
}
});
injectedMetadataMock.getInjectedVar = mockGetInjectedVar;
}
jest.doMock('ui/new_platform', () => ({
npSetup: {
core: {
notifications: notificationServiceMock.createSetupContract(),
},
},
npStart: {
core: {
injectedMetadata: injectedMetadataMock,
overlays: overlayServiceMock.createStartContract(),
},
},
}));

View file

@ -1,154 +0,0 @@
/*
* 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 moment from 'moment';
// @ts-ignore
import { banners, toastNotifications } from 'ui/notify';
import { npStart } from 'ui/new_platform';
import { i18n } from '@kbn/i18n';
let bannerId: string | null = null;
let optInBannerNoticeId: string | null = null;
let currentOptInStatus = false;
let telemetryNotifyUserAboutOptInDefault = true;
async function sendOptInStatus($injector: any, chrome: any, enabled: boolean) {
const telemetryOptInStatusUrl = npStart.core.injectedMetadata.getInjectedVar(
'telemetryOptInStatusUrl'
) as string;
const $http = $injector.get('$http');
try {
const optInStatus = await $http.post(
chrome.addBasePath('/api/telemetry/v2/clusters/_opt_in_stats'),
{
enabled,
unencrypted: false,
}
);
if (optInStatus.data && optInStatus.data.length) {
return await fetch(telemetryOptInStatusUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(optInStatus.data),
});
}
} catch (err) {
// Sending the ping is best-effort. Telemetry tries to send the ping once and discards it immediately if sending fails.
// swallow any errors
}
}
export function TelemetryOptInProvider($injector: any, chrome: any, sendOptInStatusChange = true) {
currentOptInStatus = npStart.core.injectedMetadata.getInjectedVar('telemetryOptedIn') as boolean;
const allowChangingOptInStatus = npStart.core.injectedMetadata.getInjectedVar(
'allowChangingOptInStatus'
) as boolean;
telemetryNotifyUserAboutOptInDefault = npStart.core.injectedMetadata.getInjectedVar(
'telemetryNotifyUserAboutOptInDefault'
) as boolean;
const provider = {
getBannerId: () => bannerId,
getOptInBannerNoticeId: () => optInBannerNoticeId,
getOptIn: () => currentOptInStatus,
canChangeOptInStatus: () => allowChangingOptInStatus,
notifyUserAboutOptInDefault: () => telemetryNotifyUserAboutOptInDefault,
setBannerId(id: string) {
bannerId = id;
},
setOptInBannerNoticeId(id: string) {
optInBannerNoticeId = id;
},
setOptInNoticeSeen: async () => {
const $http = $injector.get('$http');
// If they've seen the notice don't spam the API
if (!telemetryNotifyUserAboutOptInDefault) {
return telemetryNotifyUserAboutOptInDefault;
}
if (optInBannerNoticeId) {
banners.remove(optInBannerNoticeId);
}
try {
await $http.put(chrome.addBasePath('/api/telemetry/v2/userHasSeenNotice'));
telemetryNotifyUserAboutOptInDefault = false;
} catch (error) {
toastNotifications.addError(error, {
title: i18n.translate('telemetry.optInNoticeSeenErrorTitle', {
defaultMessage: 'Error',
}),
toastMessage: i18n.translate('telemetry.optInNoticeSeenErrorToastText', {
defaultMessage: 'An error occurred dismissing the notice',
}),
});
telemetryNotifyUserAboutOptInDefault = true;
}
return telemetryNotifyUserAboutOptInDefault;
},
setOptIn: async (enabled: boolean) => {
if (!allowChangingOptInStatus) {
return;
}
const $http = $injector.get('$http');
try {
await $http.post(chrome.addBasePath('/api/telemetry/v2/optIn'), { enabled });
if (sendOptInStatusChange) {
await sendOptInStatus($injector, chrome, enabled);
}
currentOptInStatus = enabled;
} catch (error) {
toastNotifications.addError(error, {
title: i18n.translate('telemetry.optInErrorToastTitle', {
defaultMessage: 'Error',
}),
toastMessage: i18n.translate('telemetry.optInErrorToastText', {
defaultMessage:
'An error occurred while trying to set the usage statistics preference.',
}),
});
return false;
}
return true;
},
fetchExample: async () => {
const $http = $injector.get('$http');
return $http.post(chrome.addBasePath(`/api/telemetry/v2/clusters/_stats`), {
unencrypted: true,
timeRange: {
min: moment()
.subtract(20, 'minutes')
.toISOString(),
max: moment().toISOString(),
},
});
},
};
return provider;
}

View file

@ -18,30 +18,32 @@
*/
import React from 'react';
import routes from 'ui/routes';
import { npSetup } from 'ui/new_platform';
import { TelemetryOptInProvider } from '../../services';
import { TelemetryForm } from '../../components';
import { npStart, npSetup } from 'ui/new_platform';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { TelemetryManagementSection } from '../../../../../../plugins/telemetry/public/components';
routes.defaults(/\/management/, {
resolve: {
telemetryManagementSection: function(Private) {
const telemetryOptInProvider = Private(TelemetryOptInProvider);
const componentRegistry = npSetup.plugins.advancedSettings.component;
telemetryManagementSection() {
const { telemetry } = npStart.plugins as any;
const { advancedSettings } = npSetup.plugins as any;
const Component = props => (
<TelemetryForm
showAppliesSettingMessage={true}
telemetryOptInProvider={telemetryOptInProvider}
{...props}
/>
);
if (telemetry && advancedSettings) {
const componentRegistry = advancedSettings.component;
const Component = (props: any) => (
<TelemetryManagementSection
showAppliesSettingMessage={true}
telemetryService={telemetry.telemetryService}
{...props}
/>
);
componentRegistry.register(
componentRegistry.componentType.PAGE_FOOTER_COMPONENT,
Component,
true
);
componentRegistry.register(
componentRegistry.componentType.PAGE_FOOTER_COMPONENT,
Component,
true
);
}
},
},
});

View file

@ -24,7 +24,6 @@ import { dirname, join } from 'path';
// look for telemetry.yml in the same places we expect kibana.yml
import { ensureDeepObject } from './ensure_deep_object';
import { getXpackConfigWithDeprecated } from '../../../common/get_xpack_config_with_deprecated';
import { UsageCollectionSetup } from '../../../../../../plugins/usage_collection/server';
/**
@ -85,7 +84,7 @@ export function createTelemetryUsageCollector(
isReady: () => true,
fetch: async () => {
const config = server.config();
const configPath = getXpackConfigWithDeprecated(config, 'telemetry.config') as string;
const configPath = config.get('telemetry.config') as string;
const telemetryPath = join(dirname(configPath), 'telemetry.yml');
return await readTelemetryFile(telemetryPath);
},

View file

@ -24,7 +24,6 @@ import { telemetryCollectionManager } from './collection_manager';
import { getTelemetryOptIn, getTelemetrySendUsageFrom } from './telemetry_config';
import { getTelemetrySavedObject, updateTelemetrySavedObject } from './telemetry_repository';
import { REPORT_INTERVAL_MS } from '../common/constants';
import { getXpackConfigWithDeprecated } from '../common/get_xpack_config_with_deprecated';
export class FetcherTask {
private readonly checkDurationMs = 60 * 1000 * 5;
@ -52,7 +51,7 @@ export class FetcherTask {
const configTelemetrySendUsageFrom = config.get('telemetry.sendUsageFrom');
const allowChangingOptInStatus = config.get('telemetry.allowChangingOptInStatus');
const configTelemetryOptIn = config.get('telemetry.optIn');
const telemetryUrl = getXpackConfigWithDeprecated(config, 'telemetry.url') as string;
const telemetryUrl = config.get('telemetry.url') as string;
return {
telemetryOptIn: getTelemetryOptIn({

View file

@ -0,0 +1,59 @@
/*
* 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.
*/
/**
* Clean up any old, deprecated settings and determine if we should continue.
*
* This <em>will</em> update the latest telemetry setting if necessary.
*
* @param {Object} config The advanced settings config object.
* @return {Boolean} {@code true} if the banner should still be displayed. {@code false} if the banner should not be displayed.
*/
import { Server } from 'hapi';
import { CONFIG_TELEMETRY } from '../../common/constants';
import { updateTelemetrySavedObject } from '../telemetry_repository';
const CONFIG_ALLOW_REPORT = 'xPackMonitoring:allowReport';
export async function handleOldSettings(server: Server) {
const { getSavedObjectsRepository } = server.savedObjects;
const { callWithInternalUser } = server.plugins.elasticsearch.getCluster('admin');
const savedObjectsClient = getSavedObjectsRepository(callWithInternalUser);
const uiSettings = server.uiSettingsServiceFactory({ savedObjectsClient });
const oldTelemetrySetting = await uiSettings.get(CONFIG_TELEMETRY);
const oldAllowReportSetting = await uiSettings.get(CONFIG_ALLOW_REPORT);
let legacyOptInValue = null;
if (typeof oldTelemetrySetting === 'boolean') {
legacyOptInValue = oldTelemetrySetting;
} else if (
typeof oldAllowReportSetting === 'boolean' &&
uiSettings.isOverridden(CONFIG_ALLOW_REPORT)
) {
legacyOptInValue = oldAllowReportSetting;
}
if (legacyOptInValue !== null) {
await updateTelemetrySavedObject(savedObjectsClient, {
enabled: legacyOptInValue,
});
}
}

View file

@ -17,4 +17,4 @@
* under the License.
*/
export { injectBanner } from './inject_banner';
export { handleOldSettings } from './handle_old_settings';

View file

@ -23,6 +23,7 @@ import * as constants from '../common/constants';
export { FetcherTask } from './fetcher';
export { replaceTelemetryInjectedVars } from './telemetry_config';
export { handleOldSettings } from './handle_old_settings';
export { telemetryCollectionManager } from './collection_manager';
export { PluginsSetup } from './plugin';
export const telemetryPlugin = (initializerContext: PluginInitializerContext) =>

View file

@ -39,6 +39,7 @@ import {
import { ManagementSetup, ManagementStart } from '../../../../plugins/management/public';
import { BfetchPublicSetup, BfetchPublicStart } from '../../../../plugins/bfetch/public';
import { UsageCollectionSetup } from '../../../../plugins/usage_collection/public';
import { TelemetryPluginSetup, TelemetryPluginStart } from '../../../../plugins/telemetry/public';
import {
NavigationPublicPluginSetup,
NavigationPublicPluginStart,
@ -60,6 +61,7 @@ export interface PluginsSetup {
usageCollection: UsageCollectionSetup;
advancedSettings: AdvancedSettingsSetup;
management: ManagementSetup;
telemetry?: TelemetryPluginSetup;
}
export interface PluginsStart {
@ -77,6 +79,7 @@ export interface PluginsStart {
share: SharePluginStart;
management: ManagementStart;
advancedSettings: AdvancedSettingsStart;
telemetry?: TelemetryPluginStart;
}
export const npSetup = {

View file

@ -97,6 +97,17 @@ exports[`QueryStringInput Should disable autoFocus on EuiFieldText when disableA
"management": Object {},
"navLinks": Object {},
},
"currentAppId$": Observable {
"_isScalar": false,
"source": Subject {
"_isScalar": false,
"closed": false,
"hasError": false,
"isStopped": false,
"observers": Array [],
"thrownError": null,
},
},
"getUrlForApp": [MockFunction],
"navigateToApp": [MockFunction],
"registerMountContext": [MockFunction],
@ -738,6 +749,17 @@ exports[`QueryStringInput Should disable autoFocus on EuiFieldText when disableA
"management": Object {},
"navLinks": Object {},
},
"currentAppId$": Observable {
"_isScalar": false,
"source": Subject {
"_isScalar": false,
"closed": false,
"hasError": false,
"isStopped": false,
"observers": Array [],
"thrownError": null,
},
},
"getUrlForApp": [MockFunction],
"navigateToApp": [MockFunction],
"registerMountContext": [MockFunction],
@ -1361,6 +1383,17 @@ exports[`QueryStringInput Should pass the query language to the language switche
"management": Object {},
"navLinks": Object {},
},
"currentAppId$": Observable {
"_isScalar": false,
"source": Subject {
"_isScalar": false,
"closed": false,
"hasError": false,
"isStopped": false,
"observers": Array [],
"thrownError": null,
},
},
"getUrlForApp": [MockFunction],
"navigateToApp": [MockFunction],
"registerMountContext": [MockFunction],
@ -1999,6 +2032,17 @@ exports[`QueryStringInput Should pass the query language to the language switche
"management": Object {},
"navLinks": Object {},
},
"currentAppId$": Observable {
"_isScalar": false,
"source": Subject {
"_isScalar": false,
"closed": false,
"hasError": false,
"isStopped": false,
"observers": Array [],
"thrownError": null,
},
},
"getUrlForApp": [MockFunction],
"navigateToApp": [MockFunction],
"registerMountContext": [MockFunction],
@ -2622,6 +2666,17 @@ exports[`QueryStringInput Should render the given query 1`] = `
"management": Object {},
"navLinks": Object {},
},
"currentAppId$": Observable {
"_isScalar": false,
"source": Subject {
"_isScalar": false,
"closed": false,
"hasError": false,
"isStopped": false,
"observers": Array [],
"thrownError": null,
},
},
"getUrlForApp": [MockFunction],
"navigateToApp": [MockFunction],
"registerMountContext": [MockFunction],
@ -3260,6 +3315,17 @@ exports[`QueryStringInput Should render the given query 1`] = `
"management": Object {},
"navLinks": Object {},
},
"currentAppId$": Observable {
"_isScalar": false,
"source": Subject {
"_isScalar": false,
"closed": false,
"hasError": false,
"isStopped": false,
"observers": Array [],
"thrownError": null,
},
},
"getUrlForApp": [MockFunction],
"navigateToApp": [MockFunction],
"registerMountContext": [MockFunction],

View file

@ -18,13 +18,22 @@
*/
/**
* Determine if the notice banner should be displayed.
*
* This method can have side-effects related to deprecated config settings.
*
* @param {Object} telemetryOptInProvider The Telemetry opt-in provider singleton.
* @return {Boolean} {@code true} if the banner should be displayed. {@code false} otherwise.
* The amount of time, in milliseconds, to wait between reports when enabled.
* Currently 24 hours.
*/
export async function shouldShowOptInBanner(telemetryOptInProvider) {
return telemetryOptInProvider.notifyUserAboutOptInDefault();
}
export const REPORT_INTERVAL_MS = 86400000;
/*
* Key for the localStorage service
*/
export const LOCALSTORAGE_KEY = 'telemetry.data';
/**
* Link to Advanced Settings.
*/
export const PATH_TO_ADVANCED_SETTINGS = 'kibana#/management/kibana/settings';
/**
* Link to the Elastic Telemetry privacy statement.
*/
export const PRIVACY_STATEMENT_URL = `https://www.elastic.co/legal/privacy-statement`;

View file

@ -0,0 +1,6 @@
{
"id": "telemetry",
"version": "kibana",
"server": false,
"ui": true
}

View file

@ -0,0 +1,54 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`OptInDetailsComponent renders as expected 1`] = `
<EuiCallOut
iconType="questionInCircle"
title={
<FormattedMessage
defaultMessage="Help us improve the Elastic Stack"
id="telemetry.welcomeBanner.title"
values={Object {}}
/>
}
>
<OptInMessage />
<EuiSpacer
size="s"
/>
<EuiFlexGroup
alignItems="center"
gutterSize="s"
>
<EuiFlexItem
grow={false}
>
<EuiButton
data-test-subj="enable"
onClick={[Function]}
size="s"
>
<FormattedMessage
defaultMessage="Enable"
id="telemetry.welcomeBanner.enableButtonLabel"
values={Object {}}
/>
</EuiButton>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<EuiButton
data-test-subj="disable"
onClick={[Function]}
size="s"
>
<FormattedMessage
defaultMessage="Disable"
id="telemetry.welcomeBanner.disableButtonLabel"
values={Object {}}
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiCallOut>
`;

View file

@ -9,6 +9,7 @@ exports[`OptInMessage renders as expected 1`] = `
Object {
"privacyStatementLink": <ForwardRef
href="https://www.elastic.co/legal/privacy-statement"
rel="noopener"
target="_blank"
>
<FormattedMessage

View file

@ -17,8 +17,6 @@
* under the License.
*/
// @ts-ignore
export { TelemetryForm } from './telemetry_form';
export { OptInExampleFlyout } from './opt_in_details_component';
export { OptInBanner } from './opt_in_banner_component';
export { OptInMessage } from './opt_in_message';
export { OptInExampleFlyout } from './opt_in_example_flyout';
export { TelemetryManagementSection } from './telemetry_management_section';
export { OptedInNoticeBanner } from './opted_in_notice_banner';

View file

@ -0,0 +1,64 @@
/*
* 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 React from 'react';
import { EuiButton } from '@elastic/eui';
import { shallowWithIntl } from 'test_utils/enzyme_helpers';
import { OptInBanner } from './opt_in_banner';
describe('OptInDetailsComponent', () => {
it('renders as expected', () => {
expect(shallowWithIntl(<OptInBanner onChangeOptInClick={() => {}} />)).toMatchSnapshot();
});
it('fires the "onChangeOptInClick" prop with true when a enable is clicked', () => {
const onClick = jest.fn();
const component = shallowWithIntl(<OptInBanner onChangeOptInClick={onClick} />);
const enableButton = component.findWhere(n => {
const props = n.props();
return n.type() === EuiButton && props['data-test-subj'] === 'enable';
});
if (!enableButton) {
throw new Error(`Couldn't find any opt in enable button.`);
}
enableButton.simulate('click');
expect(onClick).toHaveBeenCalled();
expect(onClick).toBeCalledWith(true);
});
it('fires the "onChangeOptInClick" with false when a disable is clicked', () => {
const onClick = jest.fn();
const component = shallowWithIntl(<OptInBanner onChangeOptInClick={onClick} />);
const disableButton = component.findWhere(n => {
const props = n.props();
return n.type() === EuiButton && props['data-test-subj'] === 'disable';
});
if (!disableButton) {
throw new Error(`Couldn't find any opt in disable button.`);
}
disableButton.simulate('click');
expect(onClick).toHaveBeenCalled();
expect(onClick).toBeCalledWith(false);
});
});

View file

@ -23,15 +23,12 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { OptInMessage } from './opt_in_message';
interface Props {
fetchTelemetry: () => Promise<any[]>;
optInClick: (optIn: boolean) => void;
onChangeOptInClick: (isOptIn: boolean) => void;
}
/**
* React component for displaying the Telemetry opt-in banner.
*/
export class OptInBanner extends React.PureComponent<Props> {
render() {
const { onChangeOptInClick } = this.props;
const title = (
<FormattedMessage
id="telemetry.welcomeBanner.title"
@ -40,11 +37,11 @@ export class OptInBanner extends React.PureComponent<Props> {
);
return (
<EuiCallOut iconType="questionInCircle" title={title}>
<OptInMessage fetchTelemetry={this.props.fetchTelemetry} />
<OptInMessage />
<EuiSpacer size="s" />
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>
<EuiButton size="s" onClick={() => this.props.optInClick(true)}>
<EuiButton size="s" data-test-subj="enable" onClick={() => onChangeOptInClick(true)}>
<FormattedMessage
id="telemetry.welcomeBanner.enableButtonLabel"
defaultMessage="Enable"
@ -52,7 +49,7 @@ export class OptInBanner extends React.PureComponent<Props> {
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton size="s" onClick={() => this.props.optInClick(false)}>
<EuiButton size="s" data-test-subj="disable" onClick={() => onChangeOptInClick(false)}>
<FormattedMessage
id="telemetry.welcomeBanner.disableButtonLabel"
defaultMessage="Disable"

View file

@ -18,16 +18,13 @@
*/
import React from 'react';
import { shallowWithIntl } from 'test_utils/enzyme_helpers';
import { OptInExampleFlyout } from './opt_in_details_component';
import { OptInExampleFlyout } from './opt_in_example_flyout';
describe('OptInDetailsComponent', () => {
it('renders as expected', () => {
expect(
shallowWithIntl(
<OptInExampleFlyout
fetchTelemetry={jest.fn(async () => ({ data: [] }))}
onClose={jest.fn()}
/>
<OptInExampleFlyout fetchExample={jest.fn(async () => [])} onClose={jest.fn()} />
)
).toMatchSnapshot();
});

View file

@ -37,7 +37,7 @@ import {
import { FormattedMessage } from '@kbn/i18n/react';
interface Props {
fetchTelemetry: () => Promise<any>;
fetchExample: () => Promise<any[]>;
onClose: () => void;
}
@ -57,22 +57,21 @@ export class OptInExampleFlyout extends React.PureComponent<Props, State> {
hasPrivilegeToRead: false,
};
componentDidMount() {
this.props
.fetchTelemetry()
.then(response =>
this.setState({
data: Array.isArray(response.data) ? response.data : null,
isLoading: false,
hasPrivilegeToRead: true,
})
)
.catch(err => {
this.setState({
isLoading: false,
hasPrivilegeToRead: err.status !== 403,
});
async componentDidMount() {
try {
const { fetchExample } = this.props;
const clusters = await fetchExample();
this.setState({
data: Array.isArray(clusters) ? clusters : null,
isLoading: false,
hasPrivilegeToRead: true,
});
} catch (err) {
this.setState({
isLoading: false,
hasPrivilegeToRead: err.status !== 403,
});
}
}
renderBody({ data, isLoading, hasPrivilegeToRead }: State) {

View file

@ -22,8 +22,6 @@ import { OptInMessage } from './opt_in_message';
describe('OptInMessage', () => {
it('renders as expected', () => {
expect(
shallowWithIntl(<OptInMessage fetchTelemetry={jest.fn(async () => [])} />)
).toMatchSnapshot();
expect(shallowWithIntl(<OptInMessage />)).toMatchSnapshot();
});
});

View file

@ -20,30 +20,9 @@
import * as React from 'react';
import { EuiLink } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { PRIVACY_STATEMENT_URL } from '../../common/constants';
interface Props {
fetchTelemetry: () => Promise<any[]>;
}
interface State {
showDetails: boolean;
showExample: boolean;
}
export class OptInMessage extends React.PureComponent<Props, State> {
public readonly state: State = {
showDetails: false,
showExample: false,
};
toggleShowExample = () => {
this.setState(prevState => ({
showExample: !prevState.showExample,
}));
};
export class OptInMessage extends React.PureComponent {
render() {
return (
<React.Fragment>
@ -52,7 +31,7 @@ export class OptInMessage extends React.PureComponent<Props, State> {
defaultMessage="Want to help us improve the Elastic Stack? Data usage collection is currently disabled. Enabling data usage collection helps us manage and improve our products and services. See our {privacyStatementLink} for more details."
values={{
privacyStatementLink: (
<EuiLink href={PRIVACY_STATEMENT_URL} target="_blank">
<EuiLink href={PRIVACY_STATEMENT_URL} target="_blank" rel="noopener">
<FormattedMessage
id="telemetry.welcomeBanner.telemetryConfigDetailsDescription.telemetryPrivacyStatementLinkText"
defaultMessage="Privacy Statement"

View file

@ -19,16 +19,16 @@
import React from 'react';
import { EuiButton } from '@elastic/eui';
import { shallowWithIntl } from 'test_utils/enzyme_helpers';
import { OptedInBanner } from './opted_in_notice_banner';
import { OptedInNoticeBanner } from './opted_in_notice_banner';
describe('OptInDetailsComponent', () => {
it('renders as expected', () => {
expect(shallowWithIntl(<OptedInBanner onSeenBanner={() => {}} />)).toMatchSnapshot();
expect(shallowWithIntl(<OptedInNoticeBanner onSeenBanner={() => {}} />)).toMatchSnapshot();
});
it('fires the "onSeenBanner" prop when a link is clicked', () => {
const onLinkClick = jest.fn();
const component = shallowWithIntl(<OptedInBanner onSeenBanner={onLinkClick} />);
const component = shallowWithIntl(<OptedInNoticeBanner onSeenBanner={onLinkClick} />);
const button = component.findWhere(n => n.type() === EuiButton);

View file

@ -20,35 +20,32 @@
/* eslint @elastic/eui/href-or-on-click:0 */
import * as React from 'react';
import chrome from 'ui/chrome';
import { EuiButton, EuiLink, EuiCallOut, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { PATH_TO_ADVANCED_SETTINGS } from '../../common/constants';
import { i18n } from '@kbn/i18n';
import { PATH_TO_ADVANCED_SETTINGS, PRIVACY_STATEMENT_URL } from '../../common/constants';
interface Props {
onSeenBanner: () => any;
}
/**
* React component for displaying the Telemetry opt-in notice.
*/
export class OptedInBanner extends React.PureComponent<Props> {
onLinkClick = () => {
this.props.onSeenBanner();
return;
};
export class OptedInNoticeBanner extends React.PureComponent<Props> {
render() {
const { onSeenBanner } = this.props;
const bannerTitle = i18n.translate('telemetry.telemetryOptedInNoticeTitle', {
defaultMessage: 'Help us improve the Elastic Stack',
});
return (
<EuiCallOut title="Help us improve the Elastic Stack">
<EuiCallOut title={bannerTitle}>
<FormattedMessage
id="telemetry.telemetryOptedInNoticeDescription"
defaultMessage="To learn about how usage data helps us manage and improve our products and services, see our {privacyStatementLink}. To stop collection, {disableLink}."
values={{
privacyStatementLink: (
<EuiLink
onClick={this.onLinkClick}
href="https://www.elastic.co/legal/privacy-statement"
onClick={onSeenBanner}
href={PRIVACY_STATEMENT_URL}
target="_blank"
rel="noopener"
>
@ -59,10 +56,7 @@ export class OptedInBanner extends React.PureComponent<Props> {
</EuiLink>
),
disableLink: (
<EuiLink
href={chrome.addBasePath(PATH_TO_ADVANCED_SETTINGS)}
onClick={this.onLinkClick}
>
<EuiLink href={PATH_TO_ADVANCED_SETTINGS} onClick={onSeenBanner}>
<FormattedMessage
id="telemetry.telemetryOptedInDisableUsage"
defaultMessage="disable usage data here"
@ -72,7 +66,7 @@ export class OptedInBanner extends React.PureComponent<Props> {
}}
/>
<EuiSpacer size="s" />
<EuiButton size="s" onClick={this.props.onSeenBanner}>
<EuiButton size="s" onClick={onSeenBanner}>
<FormattedMessage
id="telemetry.telemetryOptedInDismissMessage"
defaultMessage="Dismiss"

View file

@ -18,7 +18,6 @@
*/
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import {
EuiCallOut,
EuiPanel,
@ -29,30 +28,38 @@ import {
EuiSpacer,
EuiText,
} from '@elastic/eui';
import { PRIVACY_STATEMENT_URL } from '../../common/constants';
import { OptInExampleFlyout } from './opt_in_details_component';
import { Field } from '../../../../../plugins/advanced_settings/public';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { PRIVACY_STATEMENT_URL } from '../../common/constants';
import { OptInExampleFlyout } from './opt_in_example_flyout';
// @ts-ignore
import { Field } from '../../../advanced_settings/public';
import { TelemetryService } from '../services/telemetry_service';
const SEARCH_TERMS = ['telemetry', 'usage', 'data', 'usage data'];
export class TelemetryForm extends Component {
static propTypes = {
telemetryOptInProvider: PropTypes.object.isRequired,
query: PropTypes.object,
onQueryMatchChange: PropTypes.func.isRequired,
showAppliesSettingMessage: PropTypes.bool.isRequired,
enableSaving: PropTypes.bool.isRequired,
};
interface Props {
telemetryService: TelemetryService;
onQueryMatchChange: (searchTermMatches: boolean) => void;
showAppliesSettingMessage: boolean;
enableSaving: boolean;
query?: any;
}
state = {
interface State {
processing: boolean;
showExample: boolean;
queryMatches: boolean | null;
}
export class TelemetryManagementSection extends Component<Props, State> {
state: State = {
processing: false,
showExample: false,
queryMatches: null,
};
UNSAFE_componentWillReceiveProps(nextProps) {
UNSAFE_componentWillReceiveProps(nextProps: Props) {
const { query } = nextProps;
const searchTerm = (query.text || '').toLowerCase();
@ -71,11 +78,10 @@ export class TelemetryForm extends Component {
}
render() {
const { telemetryOptInProvider } = this.props;
const { telemetryService } = this.props;
const { showExample, queryMatches } = this.state;
if (!telemetryOptInProvider.canChangeOptInStatus()) {
if (!telemetryService.getCanChangeOptInStatus()) {
return null;
}
@ -87,7 +93,7 @@ export class TelemetryForm extends Component {
<Fragment>
{showExample && (
<OptInExampleFlyout
fetchTelemetry={() => telemetryOptInProvider.fetchExample()}
fetchExample={telemetryService.fetchExample}
onClose={this.toggleExample}
/>
)}
@ -106,15 +112,23 @@ export class TelemetryForm extends Component {
{this.maybeGetAppliesSettingMessage()}
<EuiSpacer size="s" />
<Field
setting={{
type: 'boolean',
value: telemetryOptInProvider.getOptIn() || false,
description: this.renderDescription(),
defVal: true,
ariaName: i18n.translate('telemetry.provideUsageStatisticsLabel', {
defaultMessage: 'Provide usage statistics',
}),
}}
setting={
{
type: 'boolean',
name: 'telemetry:enabled',
displayName: i18n.translate('telemetry.provideUsageStatisticsTitle', {
defaultMessage: 'Provide usage statistics',
}),
value: telemetryService.getIsOptedIn(),
description: this.renderDescription(),
defVal: true,
ariaName: i18n.translate('telemetry.provideUsageStatisticsAriaName', {
defaultMessage: 'Provide usage statistics',
}),
} as any
}
dockLinks={null as any}
toasts={null as any}
save={this.toggleOptIn}
clear={this.toggleOptIn}
enableSaving={this.props.enableSaving}
@ -185,29 +199,21 @@ export class TelemetryForm extends Component {
</Fragment>
);
toggleOptIn = async () => {
const newOptInValue = !this.props.telemetryOptInProvider.getOptIn();
toggleOptIn = async (): Promise<boolean> => {
const { telemetryService } = this.props;
const newOptInValue = !telemetryService.getIsOptedIn();
return new Promise((resolve, reject) => {
this.setState(
{
enabled: newOptInValue,
processing: true,
},
() => {
this.props.telemetryOptInProvider.setOptIn(newOptInValue).then(
() => {
this.setState({ processing: false });
resolve();
},
e => {
// something went wrong
this.setState({ processing: false });
reject(e);
}
);
this.setState({ processing: true }, async () => {
try {
await telemetryService.setOptIn(newOptInValue);
this.setState({ processing: false });
resolve(true);
} catch (err) {
this.setState({ processing: false });
reject(err);
}
);
});
});
};

View file

@ -17,8 +17,9 @@
* under the License.
*/
import { uiModules } from 'ui/modules';
import { TelemetryPlugin } from './plugin';
export { TelemetryPluginStart, TelemetryPluginSetup } from './plugin';
import { injectBanner } from './welcome_banner';
uiModules.get('telemetry/hacks').run(injectBanner);
export function plugin() {
return new TelemetryPlugin();
}

View file

@ -0,0 +1,85 @@
/*
* 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.
*/
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { overlayServiceMock } from '../../../core/public/overlays/overlay_service.mock';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { httpServiceMock } from '../../../core/public/http/http_service.mock';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { notificationServiceMock } from '../../../core/public/notifications/notifications_service.mock';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { injectedMetadataServiceMock } from '../../../core/public/injected_metadata/injected_metadata_service.mock';
import { TelemetryService } from './services/telemetry_service';
import { TelemetryNotifications } from './services/telemetry_notifications/telemetry_notifications';
import { TelemetryPluginStart } from './plugin';
export function mockTelemetryService({
reportOptInStatusChange,
}: { reportOptInStatusChange?: boolean } = {}) {
const injectedMetadata = injectedMetadataServiceMock.createStartContract();
injectedMetadata.getInjectedVar.mockImplementation((key: string) => {
switch (key) {
case 'telemetryNotifyUserAboutOptInDefault':
return true;
case 'allowChangingOptInStatus':
return true;
case 'telemetryOptedIn':
return true;
default: {
throw Error(`Unhandled getInjectedVar key "${key}".`);
}
}
});
return new TelemetryService({
injectedMetadata,
http: httpServiceMock.createStartContract(),
notifications: notificationServiceMock.createStartContract(),
reportOptInStatusChange,
});
}
export function mockTelemetryNotifications({
telemetryService,
}: {
telemetryService: TelemetryService;
}) {
return new TelemetryNotifications({
overlays: overlayServiceMock.createStartContract(),
telemetryService,
});
}
export type Setup = jest.Mocked<TelemetryPluginStart>;
export const telemetryPluginMock = {
createSetupContract,
};
function createSetupContract(): Setup {
const telemetryService = mockTelemetryService();
const telemetryNotifications = mockTelemetryNotifications({ telemetryService });
const setupContract: Setup = {
telemetryService,
telemetryNotifications,
};
return setupContract;
}

View file

@ -0,0 +1,118 @@
/*
* 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 { Plugin, CoreStart, CoreSetup, HttpStart } from '../../../core/public';
import { TelemetrySender, TelemetryService, TelemetryNotifications } from './services';
export interface TelemetryPluginSetup {
telemetryService: TelemetryService;
}
export interface TelemetryPluginStart {
telemetryService: TelemetryService;
telemetryNotifications: TelemetryNotifications;
}
export class TelemetryPlugin implements Plugin<TelemetryPluginSetup, TelemetryPluginStart> {
private telemetrySender?: TelemetrySender;
private telemetryNotifications?: TelemetryNotifications;
private telemetryService?: TelemetryService;
public setup({ http, injectedMetadata, notifications }: CoreSetup): TelemetryPluginSetup {
this.telemetryService = new TelemetryService({
http,
injectedMetadata,
notifications,
});
this.telemetrySender = new TelemetrySender(this.telemetryService);
return {
telemetryService: this.telemetryService,
};
}
public start({ injectedMetadata, http, overlays, application }: CoreStart): TelemetryPluginStart {
if (!this.telemetryService) {
throw Error('Telemetry plugin failed to initialize properly.');
}
const telemetryBanner = injectedMetadata.getInjectedVar('telemetryBanner') as boolean;
const sendUsageFrom = injectedMetadata.getInjectedVar('telemetrySendUsageFrom') as
| 'browser'
| 'server';
this.telemetryNotifications = new TelemetryNotifications({
overlays,
telemetryService: this.telemetryService,
});
application.currentAppId$.subscribe(appId => {
const isUnauthenticated = this.getIsUnauthenticated(http);
if (isUnauthenticated) {
return;
}
this.maybeStartTelemetryPoller({ sendUsageFrom });
if (telemetryBanner) {
this.maybeShowOptedInNotificationBanner();
this.maybeShowOptInBanner();
}
});
return {
telemetryService: this.telemetryService,
telemetryNotifications: this.telemetryNotifications,
};
}
private getIsUnauthenticated(http: HttpStart) {
const { anonymousPaths } = http;
return anonymousPaths.isAnonymous(window.location.pathname);
}
private maybeStartTelemetryPoller({ sendUsageFrom }: { sendUsageFrom: string }) {
if (!this.telemetrySender) {
return;
}
if (sendUsageFrom === 'browser') {
this.telemetrySender.startChecking();
}
}
private maybeShowOptedInNotificationBanner() {
if (!this.telemetryNotifications) {
return;
}
const shouldShowBanner = this.telemetryNotifications.shouldShowOptedInNoticeBanner();
if (shouldShowBanner) {
this.telemetryNotifications.renderOptedInNoticeBanner();
}
}
private maybeShowOptInBanner() {
if (!this.telemetryNotifications) {
return;
}
const shouldShowBanner = this.telemetryNotifications.shouldShowOptInBanner();
if (shouldShowBanner) {
this.telemetryNotifications.renderOptInBanner();
}
}
}

View file

@ -0,0 +1,22 @@
/*
* 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.
*/
export { TelemetrySender } from './telemetry_sender';
export { TelemetryService } from './telemetry_service';
export { TelemetryNotifications } from './telemetry_notifications';

View file

@ -17,5 +17,4 @@
* under the License.
*/
export { TelemetryOptInProvider } from './telemetry_opt_in';
export { isUnauthenticated } from './path';
export { TelemetryNotifications } from './telemetry_notifications';

View file

@ -17,24 +17,27 @@
* under the License.
*/
import '../../services/telemetry_opt_in.test.mocks';
import { renderOptedInBanner } from './render_notice_banner';
import { renderOptInBanner } from './render_opt_in_banner';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { overlayServiceMock } from '../../../../../core/public/overlays/overlay_service.mock';
describe('render_notice_banner', () => {
describe('renderOptInBanner', () => {
it('adds a banner to banners with priority of 10000', () => {
const bannerID = 'brucer-wayne';
const overlays = overlayServiceMock.createStartContract();
overlays.banners.add.mockReturnValue(bannerID);
const telemetryOptInProvider = { setOptInBannerNoticeId: jest.fn() };
const banners = { add: jest.fn().mockReturnValue(bannerID) };
const returnedBannerId = renderOptInBanner({
setOptIn: jest.fn(),
overlays,
});
renderOptedInBanner(telemetryOptInProvider, { _banners: banners });
expect(overlays.banners.add).toBeCalledTimes(1);
expect(banners.add).toBeCalledTimes(1);
expect(telemetryOptInProvider.setOptInBannerNoticeId).toBeCalledWith(bannerID);
expect(returnedBannerId).toBe(bannerID);
const bannerConfig = overlays.banners.add.mock.calls[0];
const bannerConfig = banners.add.mock.calls[0][0];
expect(bannerConfig.component).not.toBe(undefined);
expect(bannerConfig.priority).toBe(10000);
expect(bannerConfig[0]).not.toBe(undefined);
expect(bannerConfig[1]).toBe(10000);
});
});

View file

@ -0,0 +1,35 @@
/*
* 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 React from 'react';
import { CoreStart } from 'kibana/public';
import { OptInBanner } from '../../components/opt_in_banner';
import { toMountPoint } from '../../../../kibana_react/public';
interface RenderBannerConfig {
overlays: CoreStart['overlays'];
setOptIn: (isOptIn: boolean) => Promise<any>;
}
export function renderOptInBanner({ setOptIn, overlays }: RenderBannerConfig) {
const mount = toMountPoint(<OptInBanner onChangeOptInClick={setOptIn} />);
const bannerId = overlays.banners.add(mount, 10000);
return bannerId;
}

View file

@ -17,26 +17,27 @@
* under the License.
*/
import '../../services/telemetry_opt_in.test.mocks';
import { renderBanner } from './render_banner';
import { renderOptedInNoticeBanner } from './render_opted_in_notice_banner';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { overlayServiceMock } from '../../../../../core/public/overlays/overlay_service.mock';
describe('render_banner', () => {
describe('renderOptedInNoticeBanner', () => {
it('adds a banner to banners with priority of 10000', () => {
const bannerID = 'brucer-banner';
const bannerID = 'brucer-wayne';
const overlays = overlayServiceMock.createStartContract();
overlays.banners.add.mockReturnValue(bannerID);
const telemetryOptInProvider = { setBannerId: jest.fn() };
const banners = { add: jest.fn().mockReturnValue(bannerID) };
const fetchTelemetry = jest.fn();
const returnedBannerId = renderOptedInNoticeBanner({
onSeen: jest.fn(),
overlays,
});
renderBanner(telemetryOptInProvider, fetchTelemetry, { _banners: banners });
expect(overlays.banners.add).toBeCalledTimes(1);
expect(banners.add).toBeCalledTimes(1);
expect(fetchTelemetry).toBeCalledTimes(0);
expect(telemetryOptInProvider.setBannerId).toBeCalledWith(bannerID);
expect(returnedBannerId).toBe(bannerID);
const bannerConfig = overlays.banners.add.mock.calls[0];
const bannerConfig = banners.add.mock.calls[0][0];
expect(bannerConfig.component).not.toBe(undefined);
expect(bannerConfig.priority).toBe(10000);
expect(bannerConfig[0]).not.toBe(undefined);
expect(bannerConfig[1]).toBe(10000);
});
});

View file

@ -17,9 +17,18 @@
* under the License.
*/
import chrome from 'ui/chrome';
import React from 'react';
import { CoreStart } from 'kibana/public';
import { OptedInNoticeBanner } from '../../components/opted_in_notice_banner';
import { toMountPoint } from '../../../../kibana_react/public';
export function isUnauthenticated() {
const path = (chrome as any).removeBasePath(window.location.pathname);
return path === '/login' || path === '/logout' || path === '/logged_out' || path === '/status';
interface RenderBannerConfig {
overlays: CoreStart['overlays'];
onSeen: () => void;
}
export function renderOptedInNoticeBanner({ onSeen, overlays }: RenderBannerConfig) {
const mount = toMountPoint(<OptedInNoticeBanner onSeenBanner={onSeen} />);
const bannerId = overlays.banners.add(mount, 10000);
return bannerId;
}

View file

@ -0,0 +1,55 @@
/*
* 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.
*/
/* eslint-disable dot-notation */
import { mockTelemetryNotifications, mockTelemetryService } from '../../mocks';
describe('onSetOptInClick', () => {
it('sets setting successfully and removes banner', async () => {
const optIn = true;
const bannerId = 'bruce-banner';
const telemetryService = mockTelemetryService();
telemetryService.setOptIn = jest.fn();
const telemetryNotifications = mockTelemetryNotifications({ telemetryService });
telemetryNotifications['optInBannerId'] = bannerId;
await telemetryNotifications['onSetOptInClick'](optIn);
expect(telemetryNotifications['overlays'].banners.remove).toBeCalledTimes(1);
expect(telemetryNotifications['overlays'].banners.remove).toBeCalledWith(bannerId);
expect(telemetryService.setOptIn).toBeCalledTimes(1);
expect(telemetryService.setOptIn).toBeCalledWith(optIn);
});
});
describe('setOptedInNoticeSeen', () => {
it('sets setting successfully and removes banner', async () => {
const bannerId = 'bruce-banner';
const telemetryService = mockTelemetryService();
telemetryService.setUserHasSeenNotice = jest.fn();
const telemetryNotifications = mockTelemetryNotifications({ telemetryService });
telemetryNotifications['optedInNoticeBannerId'] = bannerId;
await telemetryNotifications.setOptedInNoticeSeen();
expect(telemetryNotifications['overlays'].banners.remove).toBeCalledTimes(1);
expect(telemetryNotifications['overlays'].banners.remove).toBeCalledWith(bannerId);
expect(telemetryService.setUserHasSeenNotice).toBeCalledTimes(1);
});
});

View file

@ -0,0 +1,88 @@
/*
* 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 { CoreStart } from 'kibana/public';
import { renderOptedInNoticeBanner } from './render_opted_in_notice_banner';
import { renderOptInBanner } from './render_opt_in_banner';
import { TelemetryService } from '../telemetry_service';
interface TelemetryNotificationsConstructor {
overlays: CoreStart['overlays'];
telemetryService: TelemetryService;
}
export class TelemetryNotifications {
private readonly overlays: CoreStart['overlays'];
private readonly telemetryService: TelemetryService;
private optedInNoticeBannerId?: string;
private optInBannerId?: string;
constructor({ overlays, telemetryService }: TelemetryNotificationsConstructor) {
this.telemetryService = telemetryService;
this.overlays = overlays;
}
public shouldShowOptedInNoticeBanner = (): boolean => {
const userHasSeenOptedInNotice = this.telemetryService.getUserHasSeenOptedInNotice();
const bannerOnScreen = typeof this.optedInNoticeBannerId !== 'undefined';
return !bannerOnScreen && userHasSeenOptedInNotice;
};
public renderOptedInNoticeBanner = (): void => {
const bannerId = renderOptedInNoticeBanner({
onSeen: this.setOptedInNoticeSeen,
overlays: this.overlays,
});
this.optedInNoticeBannerId = bannerId;
};
public shouldShowOptInBanner = (): boolean => {
const isOptedIn = this.telemetryService.getIsOptedIn();
const bannerOnScreen = typeof this.optInBannerId !== 'undefined';
return !bannerOnScreen && isOptedIn === null;
};
public renderOptInBanner = (): void => {
const bannerId = renderOptInBanner({
setOptIn: this.onSetOptInClick,
overlays: this.overlays,
});
this.optInBannerId = bannerId;
};
private onSetOptInClick = async (isOptIn: boolean) => {
if (this.optInBannerId) {
this.overlays.banners.remove(this.optInBannerId);
this.optInBannerId = undefined;
}
await this.telemetryService.setOptIn(isOptIn);
};
public setOptedInNoticeSeen = async (): Promise<void> => {
if (this.optedInNoticeBannerId) {
this.overlays.banners.remove(this.optedInNoticeBannerId);
this.optedInNoticeBannerId = undefined;
}
await this.telemetryService.setUserHasSeenNotice();
};
}

View file

@ -0,0 +1,272 @@
/*
* 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.
*/
/* eslint-disable dot-notation */
import { TelemetrySender } from './telemetry_sender';
import { mockTelemetryService } from '../mocks';
import { REPORT_INTERVAL_MS, LOCALSTORAGE_KEY } from '../../common/constants';
class LocalStorageMock implements Partial<Storage> {
getItem = jest.fn();
setItem = jest.fn();
}
describe('TelemetrySender', () => {
let originalLocalStorage: Storage;
let mockLocalStorage: LocalStorageMock;
beforeAll(() => {
originalLocalStorage = window.localStorage;
});
// @ts-ignore
beforeEach(() => (window.localStorage = mockLocalStorage = new LocalStorageMock()));
// @ts-ignore
afterAll(() => (window.localStorage = originalLocalStorage));
describe('constructor', () => {
it('defaults lastReport if unset', () => {
const telemetryService = mockTelemetryService();
const telemetrySender = new TelemetrySender(telemetryService);
expect(telemetrySender['lastReported']).toBeUndefined();
expect(mockLocalStorage.getItem).toBeCalledTimes(1);
expect(mockLocalStorage.getItem).toHaveBeenCalledWith(LOCALSTORAGE_KEY);
});
it('uses lastReport if set', () => {
const lastReport = `${Date.now()}`;
mockLocalStorage.getItem.mockReturnValueOnce(JSON.stringify({ lastReport }));
const telemetryService = mockTelemetryService();
const telemetrySender = new TelemetrySender(telemetryService);
expect(telemetrySender['lastReported']).toBe(lastReport);
});
});
describe('saveToBrowser', () => {
it('uses lastReport', () => {
const lastReport = `${Date.now()}`;
const telemetryService = mockTelemetryService();
const telemetrySender = new TelemetrySender(telemetryService);
telemetrySender['lastReported'] = lastReport;
telemetrySender['saveToBrowser']();
expect(mockLocalStorage.setItem).toHaveBeenCalledTimes(1);
expect(mockLocalStorage.setItem).toHaveBeenCalledWith(
LOCALSTORAGE_KEY,
JSON.stringify({ lastReport })
);
});
});
describe('shouldSendReport', () => {
it('returns false whenever optIn is false', () => {
const telemetryService = mockTelemetryService();
telemetryService.getIsOptedIn = jest.fn().mockReturnValue(false);
const telemetrySender = new TelemetrySender(telemetryService);
const shouldSendRerpot = telemetrySender['shouldSendReport']();
expect(telemetryService.getIsOptedIn).toBeCalledTimes(1);
expect(shouldSendRerpot).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']();
expect(telemetrySender['lastReported']).toBeUndefined();
expect(shouldSendRerpot).toBe(true);
});
it('returns true if lastReported passed REPORT_INTERVAL_MS', () => {
const lastReported = Date.now() - (REPORT_INTERVAL_MS + 1000);
const telemetryService = mockTelemetryService();
telemetryService.getIsOptedIn = jest.fn().mockReturnValue(true);
const telemetrySender = new TelemetrySender(telemetryService);
telemetrySender['lastReported'] = `${lastReported}`;
const shouldSendRerpot = telemetrySender['shouldSendReport']();
expect(shouldSendRerpot).toBe(true);
});
it('returns false if lastReported is within REPORT_INTERVAL_MS', () => {
const lastReported = Date.now() + 1000;
const telemetryService = mockTelemetryService();
telemetryService.getIsOptedIn = jest.fn().mockReturnValue(true);
const telemetrySender = new TelemetrySender(telemetryService);
telemetrySender['lastReported'] = `${lastReported}`;
const shouldSendRerpot = telemetrySender['shouldSendReport']();
expect(shouldSendRerpot).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);
});
describe('sendIfDue', () => {
let originalFetch: typeof window['fetch'];
let mockFetch: jest.Mock<typeof window['fetch']>;
beforeAll(() => {
originalFetch = window.fetch;
});
// @ts-ignore
beforeEach(() => (window.fetch = mockFetch = jest.fn()));
// @ts-ignore
afterAll(() => (window.fetch = originalFetch));
it('does not send if already sending', async () => {
const telemetryService = mockTelemetryService();
const telemetrySender = new TelemetrySender(telemetryService);
telemetrySender['shouldSendReport'] = jest.fn();
telemetrySender['isSending'] = true;
await telemetrySender['sendIfDue']();
expect(telemetrySender['shouldSendReport']).toBeCalledTimes(0);
expect(mockFetch).toBeCalledTimes(0);
});
it('does not send if shouldSendReport returns false', async () => {
const telemetryService = mockTelemetryService();
const telemetrySender = new TelemetrySender(telemetryService);
telemetrySender['shouldSendReport'] = jest.fn().mockReturnValue(false);
telemetrySender['isSending'] = false;
await telemetrySender['sendIfDue']();
expect(telemetrySender['shouldSendReport']).toBeCalledTimes(1);
expect(mockFetch).toBeCalledTimes(0);
});
it('sends report if due', async () => {
const mockTelemetryUrl = 'telemetry_cluster_url';
const mockTelemetryPayload = ['hashed_cluster_usage_data1'];
const telemetryService = mockTelemetryService();
const telemetrySender = new TelemetrySender(telemetryService);
telemetryService.getTelemetryUrl = jest.fn().mockReturnValue(mockTelemetryUrl);
telemetryService.fetchTelemetry = jest.fn().mockReturnValue(mockTelemetryPayload);
telemetrySender['shouldSendReport'] = jest.fn().mockReturnValue(true);
telemetrySender['isSending'] = false;
await telemetrySender['sendIfDue']();
expect(telemetryService.fetchTelemetry).toBeCalledTimes(1);
expect(mockFetch).toBeCalledTimes(1);
expect(mockFetch).toBeCalledWith(mockTelemetryUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: mockTelemetryPayload[0],
});
});
it('sends report separately for every cluster', async () => {
const mockTelemetryUrl = 'telemetry_cluster_url';
const mockTelemetryPayload = ['hashed_cluster_usage_data1', 'hashed_cluster_usage_data2'];
const telemetryService = mockTelemetryService();
const telemetrySender = new TelemetrySender(telemetryService);
telemetryService.getTelemetryUrl = jest.fn().mockReturnValue(mockTelemetryUrl);
telemetryService.fetchTelemetry = jest.fn().mockReturnValue(mockTelemetryPayload);
telemetrySender['shouldSendReport'] = jest.fn().mockReturnValue(true);
telemetrySender['isSending'] = false;
await telemetrySender['sendIfDue']();
expect(telemetryService.fetchTelemetry).toBeCalledTimes(1);
expect(mockFetch).toBeCalledTimes(2);
});
it('updates last lastReported and calls saveToBrowser', async () => {
const mockTelemetryUrl = 'telemetry_cluster_url';
const mockTelemetryPayload = ['hashed_cluster_usage_data1'];
const telemetryService = mockTelemetryService();
const telemetrySender = new TelemetrySender(telemetryService);
telemetryService.getTelemetryUrl = jest.fn().mockReturnValue(mockTelemetryUrl);
telemetryService.fetchTelemetry = jest.fn().mockReturnValue(mockTelemetryPayload);
telemetrySender['shouldSendReport'] = jest.fn().mockReturnValue(true);
telemetrySender['saveToBrowser'] = jest.fn();
await telemetrySender['sendIfDue']();
expect(mockFetch).toBeCalledTimes(1);
expect(telemetrySender['lastReported']).toBeDefined();
expect(telemetrySender['saveToBrowser']).toBeCalledTimes(1);
expect(telemetrySender['isSending']).toBe(false);
});
it('catches fetchTelemetry errors and sets isSending to false', async () => {
const telemetryService = mockTelemetryService();
const telemetrySender = new TelemetrySender(telemetryService);
telemetryService.getTelemetryUrl = jest.fn();
telemetryService.fetchTelemetry = jest.fn().mockImplementation(() => {
throw Error('Error fetching usage');
});
await telemetrySender['sendIfDue']();
expect(telemetryService.fetchTelemetry).toBeCalledTimes(1);
expect(telemetrySender['lastReported']).toBeUndefined();
expect(telemetrySender['isSending']).toBe(false);
});
it('catches fetch errors and sets isSending to false', async () => {
const mockTelemetryPayload = ['hashed_cluster_usage_data1', 'hashed_cluster_usage_data2'];
const telemetryService = mockTelemetryService();
const telemetrySender = new TelemetrySender(telemetryService);
telemetryService.getTelemetryUrl = jest.fn();
telemetryService.fetchTelemetry = jest.fn().mockReturnValue(mockTelemetryPayload);
mockFetch.mockImplementation(() => {
throw Error('Error sending usage');
});
await telemetrySender['sendIfDue']();
expect(telemetryService.fetchTelemetry).toBeCalledTimes(1);
expect(mockFetch).toBeCalledTimes(2);
expect(telemetrySender['lastReported']).toBeUndefined();
expect(telemetrySender['isSending']).toBe(false);
});
});
});
describe('startChecking', () => {
let originalSetInterval: typeof window['setInterval'];
let mockSetInterval: jest.Mock<typeof window['setInterval']>;
beforeAll(() => {
originalSetInterval = window.setInterval;
});
// @ts-ignore
beforeEach(() => (window.setInterval = mockSetInterval = jest.fn()));
// @ts-ignore
afterAll(() => (window.setInterval = originalSetInterval));
it('calls sendIfDue every 60000 ms', () => {
const telemetryService = mockTelemetryService();
const telemetrySender = new TelemetrySender(telemetryService);
telemetrySender.startChecking();
expect(mockSetInterval).toBeCalledTimes(1);
expect(mockSetInterval).toBeCalledWith(telemetrySender['sendIfDue'], 60000);
});
});
});

View file

@ -0,0 +1,100 @@
/*
* 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 { REPORT_INTERVAL_MS, LOCALSTORAGE_KEY } from '../../common/constants';
import { TelemetryService } from './telemetry_service';
import { Storage } from '../../../kibana_utils/public';
export class TelemetrySender {
private readonly telemetryService: TelemetryService;
private isSending: boolean = false;
private lastReported?: string;
private readonly storage: Storage;
private intervalId?: number;
constructor(telemetryService: TelemetryService) {
this.telemetryService = telemetryService;
this.storage = new Storage(window.localStorage);
const attributes = this.storage.get(LOCALSTORAGE_KEY);
if (attributes) {
this.lastReported = attributes.lastReport;
}
}
private saveToBrowser = () => {
// we are the only code that manipulates this key, so it's safe to blindly overwrite the whole object
this.storage.set(LOCALSTORAGE_KEY, { lastReport: this.lastReported });
};
private shouldSendReport = (): boolean => {
// check if opt-in for telemetry is enabled
if (this.telemetryService.getIsOptedIn()) {
if (!this.lastReported) {
return true;
}
// returns NaN for any malformed or unset (null/undefined) value
const lastReported = parseInt(this.lastReported, 10);
// If it's been a day since we last sent telemetry
if (isNaN(lastReported) || Date.now() - lastReported > REPORT_INTERVAL_MS) {
return true;
}
}
return false;
};
private sendIfDue = async (): Promise<void> => {
if (this.isSending || !this.shouldSendReport()) {
return;
}
// mark that we are working so future requests are ignored until we're done
this.isSending = true;
try {
const telemetryUrl = this.telemetryService.getTelemetryUrl();
const telemetryData: any | any[] = await this.telemetryService.fetchTelemetry();
const clusters: string[] = [].concat(telemetryData);
await Promise.all(
clusters.map(
async cluster =>
await fetch(telemetryUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: cluster,
})
)
);
this.lastReported = `${Date.now()}`;
this.saveToBrowser();
} catch (err) {
// ignore err
} finally {
this.isSending = false;
}
};
public startChecking = () => {
if (typeof this.intervalId === 'undefined') {
this.intervalId = window.setInterval(this.sendIfDue, 60000);
}
};
}

View file

@ -0,0 +1,139 @@
/*
* 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.
*/
/* eslint-disable dot-notation */
import { mockTelemetryService } from '../mocks';
const mockSubtract = jest.fn().mockImplementation(() => {
return {
toISOString: jest.fn(),
};
});
jest.mock('moment', () => {
return jest.fn().mockImplementation(() => {
return {
subtract: mockSubtract,
toISOString: jest.fn(),
};
});
});
describe('TelemetryService', () => {
describe('fetchTelemetry', () => {
it('calls expected URL with 20 minutes - now', async () => {
const telemetryService = mockTelemetryService();
await telemetryService.fetchTelemetry();
expect(telemetryService['http'].post).toBeCalledWith('/api/telemetry/v2/clusters/_stats', {
body: JSON.stringify({ unencrypted: false, timeRange: {} }),
});
expect(mockSubtract).toBeCalledWith(20, 'minutes');
});
});
describe('fetchExample', () => {
it('calls fetchTelemetry with unencrupted: true', async () => {
const telemetryService = mockTelemetryService();
telemetryService.fetchTelemetry = jest.fn();
await telemetryService.fetchExample();
expect(telemetryService.fetchTelemetry).toBeCalledWith({ unencrypted: true });
});
});
describe('setOptIn', () => {
it('calls api if canChangeOptInStatus', async () => {
const telemetryService = mockTelemetryService({ reportOptInStatusChange: false });
telemetryService.getCanChangeOptInStatus = jest.fn().mockReturnValue(true);
await telemetryService.setOptIn(true);
expect(telemetryService['http'].post).toBeCalledTimes(1);
});
it('sends enabled true if optedIn: true', async () => {
const telemetryService = mockTelemetryService({ reportOptInStatusChange: false });
telemetryService.getCanChangeOptInStatus = jest.fn().mockReturnValue(true);
const optedIn = true;
await telemetryService.setOptIn(optedIn);
expect(telemetryService['http'].post).toBeCalledWith('/api/telemetry/v2/optIn', {
body: JSON.stringify({ enabled: optedIn }),
});
});
it('sends enabled false if optedIn: false', async () => {
const telemetryService = mockTelemetryService({ reportOptInStatusChange: false });
telemetryService.getCanChangeOptInStatus = jest.fn().mockReturnValue(true);
const optedIn = false;
await telemetryService.setOptIn(optedIn);
expect(telemetryService['http'].post).toBeCalledWith('/api/telemetry/v2/optIn', {
body: JSON.stringify({ enabled: optedIn }),
});
});
it('does not call reportOptInStatus if reportOptInStatusChange is false', async () => {
const telemetryService = mockTelemetryService({ reportOptInStatusChange: false });
telemetryService.getCanChangeOptInStatus = jest.fn().mockReturnValue(true);
telemetryService['reportOptInStatus'] = jest.fn();
await telemetryService.setOptIn(true);
expect(telemetryService['reportOptInStatus']).toBeCalledTimes(0);
expect(telemetryService['http'].post).toBeCalledTimes(1);
});
it('calls reportOptInStatus if reportOptInStatusChange is true', async () => {
const telemetryService = mockTelemetryService({ reportOptInStatusChange: true });
telemetryService.getCanChangeOptInStatus = jest.fn().mockReturnValue(true);
telemetryService['reportOptInStatus'] = jest.fn();
await telemetryService.setOptIn(true);
expect(telemetryService['reportOptInStatus']).toBeCalledTimes(1);
expect(telemetryService['http'].post).toBeCalledTimes(1);
});
it('adds an error toast on api error', async () => {
const telemetryService = mockTelemetryService({ reportOptInStatusChange: false });
telemetryService.getCanChangeOptInStatus = jest.fn().mockReturnValue(true);
telemetryService['reportOptInStatus'] = jest.fn();
telemetryService['http'].post = jest.fn().mockImplementation((url: string) => {
if (url === '/api/telemetry/v2/optIn') {
throw Error('failed to update opt in.');
}
});
await telemetryService.setOptIn(true);
expect(telemetryService['http'].post).toBeCalledTimes(1);
expect(telemetryService['reportOptInStatus']).toBeCalledTimes(0);
expect(telemetryService['notifications'].toasts.addError).toBeCalledTimes(1);
});
it('adds an error toast on reportOptInStatus error', async () => {
const telemetryService = mockTelemetryService({ reportOptInStatusChange: true });
telemetryService.getCanChangeOptInStatus = jest.fn().mockReturnValue(true);
telemetryService['reportOptInStatus'] = jest.fn().mockImplementation(() => {
throw Error('failed to report OptIn Status.');
});
await telemetryService.setOptIn(true);
expect(telemetryService['http'].post).toBeCalledTimes(1);
expect(telemetryService['reportOptInStatus']).toBeCalledTimes(1);
expect(telemetryService['notifications'].toasts.addError).toBeCalledTimes(1);
});
});
});

View file

@ -0,0 +1,165 @@
/*
* 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 moment from 'moment';
import { i18n } from '@kbn/i18n';
import { CoreStart } from 'kibana/public';
interface TelemetryServiceConstructor {
http: CoreStart['http'];
injectedMetadata: CoreStart['injectedMetadata'];
notifications: CoreStart['notifications'];
reportOptInStatusChange?: boolean;
}
export class TelemetryService {
private readonly http: CoreStart['http'];
private readonly injectedMetadata: CoreStart['injectedMetadata'];
private readonly reportOptInStatusChange: boolean;
private readonly notifications: CoreStart['notifications'];
private isOptedIn: boolean | null;
private userHasSeenOptedInNotice: boolean;
constructor({
http,
injectedMetadata,
notifications,
reportOptInStatusChange = true,
}: TelemetryServiceConstructor) {
const isOptedIn = injectedMetadata.getInjectedVar('telemetryOptedIn') as boolean | null;
const userHasSeenOptedInNotice = injectedMetadata.getInjectedVar(
'telemetryNotifyUserAboutOptInDefault'
) as boolean;
this.reportOptInStatusChange = reportOptInStatusChange;
this.injectedMetadata = injectedMetadata;
this.notifications = notifications;
this.http = http;
this.isOptedIn = isOptedIn;
this.userHasSeenOptedInNotice = userHasSeenOptedInNotice;
}
public getCanChangeOptInStatus = () => {
const allowChangingOptInStatus = this.injectedMetadata.getInjectedVar(
'allowChangingOptInStatus'
) as boolean;
return allowChangingOptInStatus;
};
public getOptInStatusUrl = () => {
const telemetryOptInStatusUrl = this.injectedMetadata.getInjectedVar(
'telemetryOptInStatusUrl'
) as string;
return telemetryOptInStatusUrl;
};
public getTelemetryUrl = () => {
const telemetryUrl = this.injectedMetadata.getInjectedVar('telemetryUrl') as string;
return telemetryUrl;
};
public getUserHasSeenOptedInNotice = () => {
return this.userHasSeenOptedInNotice;
};
public getIsOptedIn = () => {
return this.isOptedIn;
};
public fetchExample = async () => {
return await this.fetchTelemetry({ unencrypted: true });
};
public fetchTelemetry = async ({ unencrypted = false } = {}) => {
const now = moment();
return this.http.post('/api/telemetry/v2/clusters/_stats', {
body: JSON.stringify({
unencrypted,
timeRange: {
min: now.subtract(20, 'minutes').toISOString(),
max: now.toISOString(),
},
}),
});
};
public setOptIn = async (optedIn: boolean): Promise<boolean> => {
const canChangeOptInStatus = this.getCanChangeOptInStatus();
if (!canChangeOptInStatus) {
return false;
}
try {
await this.http.post('/api/telemetry/v2/optIn', {
body: JSON.stringify({ enabled: optedIn }),
});
if (this.reportOptInStatusChange) {
await this.reportOptInStatus(optedIn);
}
this.isOptedIn = optedIn;
} catch (err) {
this.notifications.toasts.addError(err, {
title: i18n.translate('telemetry.optInErrorToastTitle', {
defaultMessage: 'Error',
}),
toastMessage: i18n.translate('telemetry.optInErrorToastText', {
defaultMessage: 'An error occurred while trying to set the usage statistics preference.',
}),
});
return false;
}
return true;
};
public setUserHasSeenNotice = async (): Promise<void> => {
try {
await this.http.put('/api/telemetry/v2/userHasSeenNotice');
this.userHasSeenOptedInNotice = true;
} catch (error) {
this.notifications.toasts.addError(error, {
title: i18n.translate('telemetry.optInNoticeSeenErrorTitle', {
defaultMessage: 'Error',
}),
toastMessage: i18n.translate('telemetry.optInNoticeSeenErrorToastText', {
defaultMessage: 'An error occurred dismissing the notice',
}),
});
this.userHasSeenOptedInNotice = false;
}
};
private reportOptInStatus = async (OptInStatus: boolean): Promise<void> => {
const telemetryOptInStatusUrl = this.getOptInStatusUrl();
try {
await fetch(telemetryOptInStatusUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ enabled: OptInStatus }),
});
} catch (err) {
// Sending the ping is best-effort. Telemetry tries to send the ping once and discards it immediately if sending fails.
// swallow any errors
}
};
}

View file

@ -35,7 +35,6 @@ export default async function({ readConfigFile }) {
defaults: {
'accessibility:disableAnimations': true,
'dateFormat:tz': 'UTC',
'telemetry:optIn': false,
'state:storeInSessionStorage': true,
'notifications:lifetime:info': 10000,
},
@ -43,7 +42,7 @@ export default async function({ readConfigFile }) {
kbnTestServer: {
...defaultConfig.get('kbnTestServer'),
serverArgs: [...defaultConfig.get('kbnTestServer.serverArgs')],
serverArgs: [...defaultConfig.get('kbnTestServer.serverArgs'), '--telemetry.optIn=false'],
},
};
}

View file

@ -44,14 +44,17 @@ export default async function({ readConfigFile }) {
kbnTestServer: {
...commonConfig.get('kbnTestServer'),
serverArgs: [...commonConfig.get('kbnTestServer.serverArgs'), '--oss'],
serverArgs: [
...commonConfig.get('kbnTestServer.serverArgs'),
'--oss',
'--telemetry.optIn=false',
],
},
uiSettings: {
defaults: {
'accessibility:disableAnimations': true,
'dateFormat:tz': 'UTC',
'telemetry:optIn': false,
},
},

1
x-pack/.gitignore vendored
View file

@ -4,6 +4,7 @@
/test/functional/failure_debug
/test/functional/screenshots
/test/functional/apps/reporting/reports/session
/test/reporting/configs/failure_debug/
/legacy/plugins/reporting/.chromium/
/legacy/plugins/reporting/.phantom/
/.aws-config.json

View file

@ -1,576 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`TelemetryOptIn should display when telemetry not opted in 1`] = `
<TelemetryOptIn
intl={
Object {
"defaultFormats": Object {},
"defaultLocale": "en",
"formatDate": [Function],
"formatHTMLMessage": [Function],
"formatMessage": [Function],
"formatNumber": [Function],
"formatPlural": [Function],
"formatRelative": [Function],
"formatTime": [Function],
"formats": Object {
"date": Object {
"full": Object {
"day": "numeric",
"month": "long",
"weekday": "long",
"year": "numeric",
},
"long": Object {
"day": "numeric",
"month": "long",
"year": "numeric",
},
"medium": Object {
"day": "numeric",
"month": "short",
"year": "numeric",
},
"short": Object {
"day": "numeric",
"month": "numeric",
"year": "2-digit",
},
},
"number": Object {
"currency": Object {
"style": "currency",
},
"percent": Object {
"style": "percent",
},
},
"relative": Object {
"days": Object {
"units": "day",
},
"hours": Object {
"units": "hour",
},
"minutes": Object {
"units": "minute",
},
"months": Object {
"units": "month",
},
"seconds": Object {
"units": "second",
},
"years": Object {
"units": "year",
},
},
"time": Object {
"full": Object {
"hour": "numeric",
"minute": "numeric",
"second": "numeric",
"timeZoneName": "short",
},
"long": Object {
"hour": "numeric",
"minute": "numeric",
"second": "numeric",
"timeZoneName": "short",
},
"medium": Object {
"hour": "numeric",
"minute": "numeric",
"second": "numeric",
},
"short": Object {
"hour": "numeric",
"minute": "numeric",
},
},
},
"formatters": Object {
"getDateTimeFormat": [Function],
"getMessageFormat": [Function],
"getNumberFormat": [Function],
"getPluralFormat": [Function],
"getRelativeFormat": [Function],
},
"locale": "en",
"messages": Object {},
"now": [Function],
"onError": [Function],
"textComponent": Symbol(react.fragment),
"timeZone": null,
}
}
>
<EuiSpacer
size="s"
>
<div
className="euiSpacer euiSpacer--s"
/>
</EuiSpacer>
<EuiTitle
size="s"
>
<h4
className="euiTitle euiTitle--small"
>
<FormattedMessage
defaultMessage="Help Elastic support provide better service"
id="xpack.licenseMgmt.telemetryOptIn.customersHelpSupportDescription"
values={Object {}}
>
Help Elastic support provide better service
</FormattedMessage>
</h4>
</EuiTitle>
<EuiSpacer
size="s"
>
<div
className="euiSpacer euiSpacer--s"
/>
</EuiSpacer>
<EuiCheckbox
checked={false}
compressed={false}
disabled={false}
id="isOptingInToTelemetry"
indeterminate={false}
label={
<span>
<FormattedMessage
defaultMessage="Send basic feature usage statistics to Elastic periodically. {popover}"
id="xpack.licenseMgmt.telemetryOptIn.sendBasicFeatureStatisticsLabel"
values={
Object {
"popover": <EuiPopover
anchorPosition="downCenter"
button={
<ForwardRef
onClick={[Function]}
>
<FormattedMessage
defaultMessage="Read more"
id="xpack.licenseMgmt.telemetryOptIn.readMoreLinkText"
values={Object {}}
/>
</ForwardRef>
}
className="eui-AlignBaseline"
closePopover={[Function]}
display="inlineBlock"
hasArrow={true}
id="readMorePopover"
isOpen={false}
ownFocus={true}
panelPaddingSize="m"
>
<EuiText
className="licManagement__narrowText"
>
<p>
<FormattedMessage
defaultMessage="This feature periodically sends basic feature usage statistics. This information will not be shared outside of Elastic. See an {exampleLink} or read our {telemetryPrivacyStatementLink}. You can disable this feature any time."
id="xpack.licenseMgmt.telemetryOptIn.featureUsageWarningMessage"
values={
Object {
"exampleLink": <ForwardRef
onClick={[Function]}
>
<FormattedMessage
defaultMessage="example"
id="xpack.licenseMgmt.telemetryOptIn.exampleLinkText"
values={Object {}}
/>
</ForwardRef>,
"telemetryPrivacyStatementLink": <ForwardRef
href="https://www.elastic.co/legal/privacy-statement"
target="_blank"
>
<FormattedMessage
defaultMessage="telemetry privacy statement"
id="xpack.licenseMgmt.telemetryOptIn.telemetryPrivacyStatementLinkText"
values={Object {}}
/>
</ForwardRef>,
}
}
/>
</p>
</EuiText>
</EuiPopover>,
}
}
/>
</span>
}
onChange={[Function]}
>
<div
className="euiCheckbox"
>
<input
checked={false}
className="euiCheckbox__input"
disabled={false}
id="isOptingInToTelemetry"
onChange={[Function]}
type="checkbox"
/>
<div
className="euiCheckbox__square"
/>
<label
className="euiCheckbox__label"
htmlFor="isOptingInToTelemetry"
>
<span>
<FormattedMessage
defaultMessage="Send basic feature usage statistics to Elastic periodically. {popover}"
id="xpack.licenseMgmt.telemetryOptIn.sendBasicFeatureStatisticsLabel"
values={
Object {
"popover": <EuiPopover
anchorPosition="downCenter"
button={
<ForwardRef
onClick={[Function]}
>
<FormattedMessage
defaultMessage="Read more"
id="xpack.licenseMgmt.telemetryOptIn.readMoreLinkText"
values={Object {}}
/>
</ForwardRef>
}
className="eui-AlignBaseline"
closePopover={[Function]}
display="inlineBlock"
hasArrow={true}
id="readMorePopover"
isOpen={false}
ownFocus={true}
panelPaddingSize="m"
>
<EuiText
className="licManagement__narrowText"
>
<p>
<FormattedMessage
defaultMessage="This feature periodically sends basic feature usage statistics. This information will not be shared outside of Elastic. See an {exampleLink} or read our {telemetryPrivacyStatementLink}. You can disable this feature any time."
id="xpack.licenseMgmt.telemetryOptIn.featureUsageWarningMessage"
values={
Object {
"exampleLink": <ForwardRef
onClick={[Function]}
>
<FormattedMessage
defaultMessage="example"
id="xpack.licenseMgmt.telemetryOptIn.exampleLinkText"
values={Object {}}
/>
</ForwardRef>,
"telemetryPrivacyStatementLink": <ForwardRef
href="https://www.elastic.co/legal/privacy-statement"
target="_blank"
>
<FormattedMessage
defaultMessage="telemetry privacy statement"
id="xpack.licenseMgmt.telemetryOptIn.telemetryPrivacyStatementLinkText"
values={Object {}}
/>
</ForwardRef>,
}
}
/>
</p>
</EuiText>
</EuiPopover>,
}
}
>
Send basic feature usage statistics to Elastic periodically.
<EuiPopover
anchorPosition="downCenter"
button={
<ForwardRef
onClick={[Function]}
>
<FormattedMessage
defaultMessage="Read more"
id="xpack.licenseMgmt.telemetryOptIn.readMoreLinkText"
values={Object {}}
/>
</ForwardRef>
}
className="eui-AlignBaseline"
closePopover={[Function]}
display="inlineBlock"
hasArrow={true}
id="readMorePopover"
isOpen={false}
ownFocus={true}
panelPaddingSize="m"
>
<EuiOutsideClickDetector
isDisabled={true}
onOutsideClick={[Function]}
>
<div
className="euiPopover euiPopover--anchorDownCenter eui-AlignBaseline"
id="readMorePopover"
onKeyDown={[Function]}
onMouseDown={[Function]}
onMouseUp={[Function]}
onTouchEnd={[Function]}
onTouchStart={[Function]}
>
<div
className="euiPopover__anchor"
>
<EuiLink
onClick={[Function]}
>
<button
className="euiLink euiLink--primary"
onClick={[Function]}
type="button"
>
<FormattedMessage
defaultMessage="Read more"
id="xpack.licenseMgmt.telemetryOptIn.readMoreLinkText"
values={Object {}}
>
Read more
</FormattedMessage>
</button>
</EuiLink>
</div>
</div>
</EuiOutsideClickDetector>
</EuiPopover>
</FormattedMessage>
</span>
</label>
</div>
</EuiCheckbox>
</TelemetryOptIn>
`;
exports[`TelemetryOptIn should not display when telemetry is opted in 1`] = `
<TelemetryOptIn
intl={
Object {
"defaultFormats": Object {},
"defaultLocale": "en",
"formatDate": [Function],
"formatHTMLMessage": [Function],
"formatMessage": [Function],
"formatNumber": [Function],
"formatPlural": [Function],
"formatRelative": [Function],
"formatTime": [Function],
"formats": Object {
"date": Object {
"full": Object {
"day": "numeric",
"month": "long",
"weekday": "long",
"year": "numeric",
},
"long": Object {
"day": "numeric",
"month": "long",
"year": "numeric",
},
"medium": Object {
"day": "numeric",
"month": "short",
"year": "numeric",
},
"short": Object {
"day": "numeric",
"month": "numeric",
"year": "2-digit",
},
},
"number": Object {
"currency": Object {
"style": "currency",
},
"percent": Object {
"style": "percent",
},
},
"relative": Object {
"days": Object {
"units": "day",
},
"hours": Object {
"units": "hour",
},
"minutes": Object {
"units": "minute",
},
"months": Object {
"units": "month",
},
"seconds": Object {
"units": "second",
},
"years": Object {
"units": "year",
},
},
"time": Object {
"full": Object {
"hour": "numeric",
"minute": "numeric",
"second": "numeric",
"timeZoneName": "short",
},
"long": Object {
"hour": "numeric",
"minute": "numeric",
"second": "numeric",
"timeZoneName": "short",
},
"medium": Object {
"hour": "numeric",
"minute": "numeric",
"second": "numeric",
},
"short": Object {
"hour": "numeric",
"minute": "numeric",
},
},
},
"formatters": Object {
"getDateTimeFormat": [Function],
"getMessageFormat": [Function],
"getNumberFormat": [Function],
"getPluralFormat": [Function],
"getRelativeFormat": [Function],
},
"locale": "en",
"messages": Object {},
"now": [Function],
"onError": [Function],
"textComponent": Symbol(react.fragment),
"timeZone": null,
}
}
/>
`;
exports[`TelemetryOptIn shouldn't display when telemetry optIn status can't change 1`] = `
<TelemetryOptIn
intl={
Object {
"defaultFormats": Object {},
"defaultLocale": "en",
"formatDate": [Function],
"formatHTMLMessage": [Function],
"formatMessage": [Function],
"formatNumber": [Function],
"formatPlural": [Function],
"formatRelative": [Function],
"formatTime": [Function],
"formats": Object {
"date": Object {
"full": Object {
"day": "numeric",
"month": "long",
"weekday": "long",
"year": "numeric",
},
"long": Object {
"day": "numeric",
"month": "long",
"year": "numeric",
},
"medium": Object {
"day": "numeric",
"month": "short",
"year": "numeric",
},
"short": Object {
"day": "numeric",
"month": "numeric",
"year": "2-digit",
},
},
"number": Object {
"currency": Object {
"style": "currency",
},
"percent": Object {
"style": "percent",
},
},
"relative": Object {
"days": Object {
"units": "day",
},
"hours": Object {
"units": "hour",
},
"minutes": Object {
"units": "minute",
},
"months": Object {
"units": "month",
},
"seconds": Object {
"units": "second",
},
"years": Object {
"units": "year",
},
},
"time": Object {
"full": Object {
"hour": "numeric",
"minute": "numeric",
"second": "numeric",
"timeZoneName": "short",
},
"long": Object {
"hour": "numeric",
"minute": "numeric",
"second": "numeric",
"timeZoneName": "short",
},
"medium": Object {
"hour": "numeric",
"minute": "numeric",
"second": "numeric",
},
"short": Object {
"hour": "numeric",
"minute": "numeric",
},
},
},
"formatters": Object {
"getDateTimeFormat": [Function],
"getMessageFormat": [Function],
"getNumberFormat": [Function],
"getPluralFormat": [Function],
"getRelativeFormat": [Function],
},
"locale": "en",
"messages": Object {},
"now": [Function],
"onError": [Function],
"textComponent": Symbol(react.fragment),
"timeZone": null,
}
}
/>
`;

View file

@ -965,7 +965,6 @@ exports[`UploadLicense should display a modal when license requires acknowledgem
className="euiSpacer euiSpacer--m"
/>
</EuiSpacer>
<TelemetryOptIn />
<EuiSpacer
size="m"
>
@ -1434,7 +1433,6 @@ exports[`UploadLicense should display an error when ES says license is expired 1
className="euiSpacer euiSpacer--m"
/>
</EuiSpacer>
<TelemetryOptIn />
<EuiSpacer
size="m"
>
@ -1903,7 +1901,6 @@ exports[`UploadLicense should display an error when ES says license is invalid 1
className="euiSpacer euiSpacer--m"
/>
</EuiSpacer>
<TelemetryOptIn />
<EuiSpacer
size="m"
>
@ -2368,7 +2365,6 @@ exports[`UploadLicense should display an error when submitting invalid JSON 1`]
className="euiSpacer euiSpacer--m"
/>
</EuiSpacer>
<TelemetryOptIn />
<EuiSpacer
size="m"
>
@ -2837,7 +2833,6 @@ exports[`UploadLicense should display error when ES returns error 1`] = `
className="euiSpacer euiSpacer--m"
/>
</EuiSpacer>
<TelemetryOptIn />
<EuiSpacer
size="m"
>

View file

@ -1,43 +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;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import {
setTelemetryEnabled,
setTelemetryOptInService,
} from '../public/np_ready/application/lib/telemetry';
import { TelemetryOptIn } from '../public/np_ready/application/components/telemetry_opt_in';
import { mountWithIntl } from '../../../../test_utils/enzyme_helpers';
jest.mock('ui/new_platform');
setTelemetryEnabled(true);
describe('TelemetryOptIn', () => {
test('should display when telemetry not opted in', () => {
setTelemetryOptInService({
getOptIn: () => false,
canChangeOptInStatus: () => true,
});
const rendered = mountWithIntl(<TelemetryOptIn />);
expect(rendered).toMatchSnapshot();
});
test('should not display when telemetry is opted in', () => {
setTelemetryOptInService({
getOptIn: () => true,
canChangeOptInStatus: () => true,
});
const rendered = mountWithIntl(<TelemetryOptIn />);
expect(rendered).toMatchSnapshot();
});
test(`shouldn't display when telemetry optIn status can't change`, () => {
setTelemetryOptInService({
getOptIn: () => false,
canChangeOptInStatus: () => false,
});
const rendered = mountWithIntl(<TelemetryOptIn />);
expect(rendered).toMatchSnapshot();
});
});

View file

@ -18,7 +18,7 @@ export class App extends Component {
}
render() {
const { hasPermission, permissionsLoading, permissionsError } = this.props;
const { hasPermission, permissionsLoading, permissionsError, telemetry } = this.props;
if (permissionsLoading) {
return (
@ -85,11 +85,12 @@ export class App extends Component {
);
}
const withTelemetry = Component => props => <Component {...props} telemetry={telemetry} />;
return (
<EuiPageBody>
<Switch>
<Route path={`${BASE_PATH}upload_license`} component={UploadLicense} />
<Route path={BASE_PATH} component={LicenseDashboard} />
<Route path={`${BASE_PATH}upload_license`} component={withTelemetry(UploadLicense)} />
<Route path={BASE_PATH} component={withTelemetry(LicenseDashboard)} />
</Switch>
</EuiPageBody>
);

View file

@ -11,6 +11,7 @@ import { render, unmountComponentAtNode } from 'react-dom';
import * as history from 'history';
import { DocLinksStart, HttpSetup, ToastsSetup, ChromeStart } from 'src/core/public';
import { TelemetryPluginSetup } from 'src/plugins/telemetry/public';
// @ts-ignore
import { App } from './app.container';
// @ts-ignore
@ -34,10 +35,11 @@ interface AppDependencies {
toasts: ToastsSetup;
docLinks: DocLinksStart;
http: HttpSetup;
telemetry?: TelemetryPluginSetup;
}
export const boot = (deps: AppDependencies) => {
const { I18nContext, element, legacy, toasts, docLinks, http, chrome } = deps;
const { I18nContext, element, legacy, toasts, docLinks, http, chrome, telemetry } = deps;
const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = docLinks;
const esBase = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}`;
const securityDocumentationLink = `${esBase}/security-settings.html`;
@ -56,15 +58,17 @@ export const boot = (deps: AppDependencies) => {
toasts,
http,
chrome,
telemetry,
MANAGEMENT_BREADCRUMB: legacy.MANAGEMENT_BREADCRUMB,
};
const store = licenseManagementStore(initialState, services);
render(
<I18nContext>
<Provider store={store}>
<HashRouter>
<App />
<App telemetry={telemetry} />
</HashRouter>
</Provider>
</I18nContext>,

View file

@ -6,26 +6,31 @@
import React, { Fragment } from 'react';
import { EuiLink, EuiCheckbox, EuiSpacer, EuiText, EuiTitle, EuiPopover } from '@elastic/eui';
import {
shouldShowTelemetryOptIn,
getTelemetryFetcher,
PRIVACY_STATEMENT_URL,
OptInExampleFlyout,
} from '../../lib/telemetry';
import { FormattedMessage } from '@kbn/i18n/react';
import {
OptInExampleFlyout,
PRIVACY_STATEMENT_URL,
TelemetryPluginSetup,
} from '../../lib/telemetry';
export class TelemetryOptIn extends React.Component {
constructor() {
super();
this.state = {
showMoreTelemetryInfo: false,
isOptingInToTelemetry: false,
showExample: false,
};
}
isOptingInToTelemetry = () => {
return this.state.isOptingInToTelemetry;
interface State {
showMoreTelemetryInfo: boolean;
showExample: boolean;
}
interface Props {
onOptInChange: (isOptingInToTelemetry: boolean) => void;
isOptingInToTelemetry: boolean;
isStartTrial: boolean;
telemetry: TelemetryPluginSetup;
}
export class TelemetryOptIn extends React.Component<Props, State> {
state: State = {
showMoreTelemetryInfo: false,
showExample: false,
};
closeReadMorePopover = () => {
this.setState({ showMoreTelemetryInfo: false });
};
@ -37,20 +42,22 @@ export class TelemetryOptIn extends React.Component {
this.setState({ showExample: true });
this.closeReadMorePopover();
};
onChangeOptIn = event => {
onChangeOptIn = (event: any) => {
const isOptingInToTelemetry = event.target.checked;
this.setState({ isOptingInToTelemetry });
const { onOptInChange } = this.props;
onOptInChange(isOptingInToTelemetry);
};
render() {
const { showMoreTelemetryInfo, isOptingInToTelemetry, showExample } = this.state;
const { isStartTrial } = this.props;
const { showMoreTelemetryInfo, showExample } = this.state;
const { isStartTrial, isOptingInToTelemetry, telemetry } = this.props;
let example = null;
if (showExample) {
example = (
<OptInExampleFlyout
onClose={() => this.setState({ showExample: false })}
fetchTelemetry={getTelemetryFetcher}
fetchExample={telemetry.telemetryService.fetchExample}
/>
);
}
@ -123,7 +130,7 @@ export class TelemetryOptIn extends React.Component {
</EuiPopover>
);
return shouldShowTelemetryOptIn() ? (
return (
<Fragment>
{example}
{toCurrentCustomers}
@ -144,6 +151,6 @@ export class TelemetryOptIn extends React.Component {
onChange={this.onChangeOptIn}
/>
</Fragment>
) : null;
);
}
}

View file

@ -1,36 +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;
* you may not use this file except in compliance with the Elastic License.
*/
import { fetchTelemetry } from '../../../../../../../../src/legacy/core_plugins/telemetry/public/hacks/fetch_telemetry';
export { PRIVACY_STATEMENT_URL } from '../../../../../../../../src/legacy/core_plugins/telemetry/common/constants';
export { TelemetryOptInProvider } from '../../../../../../../../src/legacy/core_plugins/telemetry/public/services';
export { OptInExampleFlyout } from '../../../../../../../../src/legacy/core_plugins/telemetry/public/components';
let telemetryEnabled;
let httpClient;
let telemetryOptInService;
export const setTelemetryEnabled = isTelemetryEnabled => {
telemetryEnabled = isTelemetryEnabled;
};
export const setHttpClient = anHttpClient => {
httpClient = anHttpClient;
};
export const setTelemetryOptInService = aTelemetryOptInService => {
telemetryOptInService = aTelemetryOptInService;
};
export const optInToTelemetry = async enableTelemetry => {
await telemetryOptInService.setOptIn(enableTelemetry);
};
export const shouldShowTelemetryOptIn = () => {
return (
telemetryEnabled &&
!telemetryOptInService.getOptIn() &&
telemetryOptInService.canChangeOptInStatus()
);
};
export const getTelemetryFetcher = () => {
return fetchTelemetry(httpClient, { unencrypted: true });
};

View file

@ -0,0 +1,24 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { TelemetryPluginSetup } from '../../../../../../../../src/plugins/telemetry/public';
export { OptInExampleFlyout } from '../../../../../../../../src/plugins/telemetry/public/components';
export { PRIVACY_STATEMENT_URL } from '../../../../../../../../src/plugins/telemetry/common/constants';
export { TelemetryPluginSetup, shouldShowTelemetryOptIn };
function shouldShowTelemetryOptIn(
telemetry?: TelemetryPluginSetup
): telemetry is TelemetryPluginSetup {
if (telemetry) {
const { telemetryService } = telemetry;
const isOptedIn = telemetryService.getIsOptedIn();
const canChangeOptInStatus = telemetryService.getCanChangeOptInStatus();
return canChangeOptInStatus && !isOptedIn;
}
return false;
}

View file

@ -12,7 +12,7 @@ import { AddLicense } from './add_license';
import { RequestTrialExtension } from './request_trial_extension';
import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
export const LicenseDashboard = ({ setBreadcrumb } = { setBreadcrumb: () => {} }) => {
export const LicenseDashboard = ({ setBreadcrumb, telemetry } = { setBreadcrumb: () => {} }) => {
useEffect(() => {
setBreadcrumb('dashboard');
});
@ -25,7 +25,7 @@ export const LicenseDashboard = ({ setBreadcrumb } = { setBreadcrumb: () => {} }
<EuiFlexItem>
<AddLicense />
</EuiFlexItem>
<StartTrial />
<StartTrial telemetry={telemetry} />
<RequestTrialExtension />
<RevertToBasic />
</EuiFlexGroup>

View file

@ -4,4 +4,5 @@
* you may not use this file except in compliance with the Elastic License.
*/
// @ts-ignore
export { StartTrial } from './start_trial.container';

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import React, { Component } from 'react';
import {
EuiButtonEmpty,
@ -22,32 +22,56 @@ import {
EuiModalHeaderTitle,
} from '@elastic/eui';
import { TelemetryOptIn } from '../../../components/telemetry_opt_in';
import { optInToTelemetry } from '../../../lib/telemetry';
import { FormattedMessage } from '@kbn/i18n/react';
import { TelemetryOptIn } from '../../../components/telemetry_opt_in';
import { EXTERNAL_LINKS } from '../../../../../../common/constants';
import { getDocLinks } from '../../../lib/docs_links';
import { TelemetryPluginSetup, shouldShowTelemetryOptIn } from '../../../lib/telemetry';
interface Props {
loadTrialStatus: () => void;
startLicenseTrial: () => void;
telemetry?: TelemetryPluginSetup;
shouldShowStartTrial: boolean;
}
interface State {
showConfirmation: boolean;
isOptingInToTelemetry: boolean;
}
export class StartTrial extends Component<Props, State> {
cancelRef: any;
confirmRef: any;
state: State = {
showConfirmation: false,
isOptingInToTelemetry: false,
};
export class StartTrial extends React.PureComponent {
constructor(props) {
super(props);
this.state = { showConfirmation: false };
}
UNSAFE_componentWillMount() {
this.props.loadTrialStatus();
}
startLicenseTrial = () => {
const { startLicenseTrial } = this.props;
if (this.telemetryOptIn.isOptingInToTelemetry()) {
optInToTelemetry(true);
onOptInChange = (isOptingInToTelemetry: boolean) => {
this.setState({ isOptingInToTelemetry });
};
onStartLicenseTrial = () => {
const { telemetry, startLicenseTrial } = this.props;
if (this.state.isOptingInToTelemetry && telemetry) {
telemetry.telemetryService.setOptIn(true);
}
startLicenseTrial();
};
cancel = () => {
this.setState({ showConfirmation: false });
};
acknowledgeModal() {
const { showConfirmation } = this.state;
const { showConfirmation, isOptingInToTelemetry } = this.state;
const { telemetry } = this.props;
if (!showConfirmation) {
return null;
}
@ -158,12 +182,14 @@ export class StartTrial extends React.PureComponent {
<EuiModalFooter>
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
<EuiFlexItem grow={false}>
<TelemetryOptIn
isStartTrial={true}
ref={ref => {
this.telemetryOptIn = ref;
}}
/>
{shouldShowTelemetryOptIn(telemetry) && (
<TelemetryOptIn
telemetry={telemetry}
isStartTrial={true}
onOptInChange={this.onOptInChange}
isOptingInToTelemetry={isOptingInToTelemetry}
/>
)}
</EuiFlexItem>
<EuiFlexItem grow={false} className="licManagement__ieFlex">
<EuiFlexGroup responsive={false}>
@ -182,7 +208,7 @@ export class StartTrial extends React.PureComponent {
<EuiFlexItem grow={false} className="licManagement__ieFlex">
<EuiButton
data-test-subj="confirmModalConfirmButton"
onClick={this.startLicenseTrial}
onClick={this.onStartLicenseTrial}
fill
buttonRef={this.confirmRef}
color="primary"

View file

@ -22,20 +22,28 @@ import {
EuiPageContentBody,
} from '@elastic/eui';
import { TelemetryOptIn } from '../../components/telemetry_opt_in';
import { optInToTelemetry } from '../../lib/telemetry';
import { shouldShowTelemetryOptIn } from '../../lib/telemetry';
import { FormattedMessage } from '@kbn/i18n/react';
export class UploadLicense extends React.PureComponent {
state = {
isOptingInToTelemetry: false,
};
componentDidMount() {
this.props.setBreadcrumb('upload');
this.props.addUploadErrorMessage('');
}
onOptInChange = isOptingInToTelemetry => {
this.setState({ isOptingInToTelemetry });
};
send = acknowledge => {
const file = this.file;
const fr = new FileReader();
fr.onload = ({ target: { result } }) => {
if (this.telemetryOptIn.isOptingInToTelemetry()) {
optInToTelemetry(true);
if (this.state.isOptingInToTelemetry) {
this.props.telemetry?.telemetryService.setOptIn(true);
}
this.props.uploadLicense(result, this.props.currentLicenseType, acknowledge);
};
@ -116,7 +124,8 @@ export class UploadLicense extends React.PureComponent {
}
};
render() {
const { currentLicenseType, applying } = this.props;
const { currentLicenseType, applying, telemetry } = this.props;
return (
<Fragment>
<EuiPageContent horizontalPosition="center" verticalPosition="center">
@ -170,11 +179,13 @@ export class UploadLicense extends React.PureComponent {
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="m" />
<TelemetryOptIn
ref={ref => {
this.telemetryOptIn = ref;
}}
/>
{shouldShowTelemetryOptIn(telemetry) && (
<TelemetryOptIn
isOptingInToTelemetry={this.state.isOptingInToTelemetry}
onOptInChange={this.onOptInChange}
telemetry={telemetry}
/>
)}
<EuiSpacer size="m" />
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>

View file

@ -5,11 +5,12 @@
*/
import { CoreSetup, CoreStart, Plugin } from 'src/core/public';
import { TelemetryPluginSetup } from 'src/plugins/telemetry/public';
import { XPackMainPlugin } from '../../../xpack_main/server/xpack_main';
import { PLUGIN } from '../../common/constants';
import { Breadcrumb } from './application/breadcrumbs';
export interface Plugins {
telemetry: TelemetryPluginSetup;
__LEGACY: {
xpackInfo: XPackMainPlugin;
refreshXpack: () => void;
@ -18,7 +19,7 @@ export interface Plugins {
}
export class LicenseManagementUIPlugin implements Plugin<void, void, any, any> {
setup({ application, notifications, http }: CoreSetup, { __LEGACY }: Plugins) {
setup({ application, notifications, http }: CoreSetup, { __LEGACY, telemetry }: Plugins) {
application.register({
id: PLUGIN.ID,
title: PLUGIN.TITLE,
@ -41,6 +42,7 @@ export class LicenseManagementUIPlugin implements Plugin<void, void, any, any> {
http,
element,
chrome,
telemetry,
});
},
});

View file

@ -15,15 +15,6 @@ import routes from 'ui/routes';
import { xpackInfo } from 'plugins/xpack_main/services/xpack_info';
import { plugin } from './np_ready';
import {
setTelemetryOptInService,
setTelemetryEnabled,
setHttpClient,
TelemetryOptInProvider,
// @ts-ignore
} from './np_ready/application/lib/telemetry';
import { BASE_PATH } from '../common/constants';
const licenseManagementUiEnabled = chrome.getInjected('licenseManagementUiEnabled');
@ -51,15 +42,6 @@ if (licenseManagementUiEnabled) {
});
};
const initializeTelemetry = ($injector: any) => {
const telemetryEnabled = npStart.core.injectedMetadata.getInjectedVar('telemetryEnabled');
const Private = $injector.get('Private');
const telemetryOptInProvider = Private(TelemetryOptInProvider);
setTelemetryOptInService(telemetryOptInProvider);
setTelemetryEnabled(telemetryEnabled);
setHttpClient($injector.get('$http'));
};
const template = `<kbn-management-app section="elasticsearch/license_management">
<div id="licenseReactRoot"></div>
</kbn-management-app>`;
@ -69,8 +51,6 @@ if (licenseManagementUiEnabled) {
controllerAs: 'licenseManagement',
controller: class LicenseManagementController {
constructor($injector: any, $rootScope: any, $scope: any, $route: any) {
initializeTelemetry($injector);
$scope.$$postDigest(() => {
const element = document.getElementById('licenseReactRoot')!;
@ -94,6 +74,7 @@ if (licenseManagementUiEnabled) {
},
},
{
telemetry: (npSetup.plugins as any).telemetry,
__LEGACY: { xpackInfo, refreshXpack, MANAGEMENT_BREADCRUMB },
}
);

View file

@ -13,8 +13,6 @@ import { setupXPackMain } from './server/lib/setup_xpack_main';
import { xpackInfoRoute, settingsRoute } from './server/routes/api/v1';
import { i18n } from '@kbn/i18n';
import { has } from 'lodash';
export { callClusterFactory } from './server/lib/call_cluster_factory';
import { registerMonitoringCollection } from './server/telemetry_collection';
@ -98,21 +96,5 @@ export const xpackMain = kibana => {
xpackInfoRoute(server);
settingsRoute(server, this.kbnServer);
},
deprecations: () => {
function movedToTelemetry(configPath) {
return (settings, log) => {
if (has(settings, configPath)) {
log(
`Config key "xpack.xpack_main.${configPath}" is deprecated. Use "telemetry.${configPath}" instead.`
);
}
};
}
return [
movedToTelemetry('telemetry.config'),
movedToTelemetry('telemetry.url'),
movedToTelemetry('telemetry.enabled'),
];
},
});
};

Some files were not shown because too many files have changed in this diff Show more