diff --git a/.i18nrc.json b/.i18nrc.json index c171b842254ee..6874d02304e49 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -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", diff --git a/docs/development/core/public/kibana-plugin-public.applicationstart.currentappid_.md b/docs/development/core/public/kibana-plugin-public.applicationstart.currentappid_.md new file mode 100644 index 0000000000000..d3ceeabcd81f4 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.applicationstart.currentappid_.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ApplicationStart](./kibana-plugin-public.applicationstart.md) > [currentAppId$](./kibana-plugin-public.applicationstart.currentappid_.md) + +## ApplicationStart.currentAppId$ property + +An observable that emits the current application id and each subsequent id update. + +Signature: + +```typescript +currentAppId$: Observable; +``` diff --git a/docs/development/core/public/kibana-plugin-public.applicationstart.geturlforapp.md b/docs/development/core/public/kibana-plugin-public.applicationstart.geturlforapp.md index 7eadd4d4e9d44..1ae368a11674f 100644 --- a/docs/development/core/public/kibana-plugin-public.applicationstart.geturlforapp.md +++ b/docs/development/core/public/kibana-plugin-public.applicationstart.geturlforapp.md @@ -4,13 +4,16 @@ ## ApplicationStart.getUrlForApp() method -Returns a relative URL to a given app, including the global base path. +Returns an URL to a given app, including the global base path. By default, the URL is relative (/basePath/app/my-app). Use the `absolute` option to generate an absolute url (http://host:port/basePath/app/my-app) + +Note that when generating absolute urls, the protocol, host and port are determined from the browser location. Signature: ```typescript getUrlForApp(appId: string, options?: { path?: string; + absolute?: boolean; }): string; ``` @@ -19,7 +22,7 @@ getUrlForApp(appId: string, options?: { | Parameter | Type | Description | | --- | --- | --- | | appId | string | | -| options | {
path?: string;
} | | +| options | {
path?: string;
absolute?: boolean;
} | | Returns: diff --git a/docs/development/core/public/kibana-plugin-public.applicationstart.md b/docs/development/core/public/kibana-plugin-public.applicationstart.md index 3ad7e3b1656d8..d5a0bef9470f7 100644 --- a/docs/development/core/public/kibana-plugin-public.applicationstart.md +++ b/docs/development/core/public/kibana-plugin-public.applicationstart.md @@ -16,12 +16,13 @@ export interface ApplicationStart | Property | Type | Description | | --- | --- | --- | | [capabilities](./kibana-plugin-public.applicationstart.capabilities.md) | RecursiveReadonly<Capabilities> | Gets the read-only capabilities. | +| [currentAppId$](./kibana-plugin-public.applicationstart.currentappid_.md) | Observable<string | undefined> | An observable that emits the current application id and each subsequent id update. | ## Methods | Method | Description | | --- | --- | -| [getUrlForApp(appId, options)](./kibana-plugin-public.applicationstart.geturlforapp.md) | Returns a relative URL to a given app, including the global base path. | +| [getUrlForApp(appId, options)](./kibana-plugin-public.applicationstart.geturlforapp.md) | Returns an URL to a given app, including the global base path. By default, the URL is relative (/basePath/app/my-app). Use the absolute option to generate an absolute url (http://host:port/basePath/app/my-app)Note that when generating absolute urls, the protocol, host and port are determined from the browser location. | | [navigateToApp(appId, options)](./kibana-plugin-public.applicationstart.navigatetoapp.md) | Navigate to a given app | | [registerMountContext(contextName, provider)](./kibana-plugin-public.applicationstart.registermountcontext.md) | Register a context provider for application mounting. Will only be available to applications that depend on the plugin that registered this context. Deprecated, use [CoreSetup.getStartServices()](./kibana-plugin-public.coresetup.getstartservices.md). | diff --git a/src/core/public/application/application_service.mock.ts b/src/core/public/application/application_service.mock.ts index dee47315fc322..d2a827d381be5 100644 --- a/src/core/public/application/application_service.mock.ts +++ b/src/core/public/application/application_service.mock.ts @@ -43,12 +43,17 @@ const createInternalSetupContractMock = (): jest.Mocked => ({ - capabilities: capabilitiesServiceMock.createStartContract().capabilities, - navigateToApp: jest.fn(), - getUrlForApp: jest.fn(), - registerMountContext: jest.fn(), -}); +const createStartContractMock = (): jest.Mocked => { + const currentAppId$ = new Subject(); + + return { + currentAppId$: currentAppId$.asObservable(), + capabilities: capabilitiesServiceMock.createStartContract().capabilities, + navigateToApp: jest.fn(), + getUrlForApp: jest.fn(), + registerMountContext: jest.fn(), + }; +}; const createInternalStartContractMock = (): jest.Mocked => { const currentAppId$ = new Subject(); diff --git a/src/core/public/application/application_service.test.ts b/src/core/public/application/application_service.test.ts index 18716bd872842..5487ca53170dd 100644 --- a/src/core/public/application/application_service.test.ts +++ b/src/core/public/application/application_service.test.ts @@ -580,7 +580,6 @@ describe('#start()', () => { it('creates URLs with path parameter', async () => { service.setup(setupDeps); - const { getUrlForApp } = await service.start(startDeps); expect(getUrlForApp('app1', { path: 'deep/link' })).toBe('/base-path/app/app1/deep/link'); @@ -588,6 +587,16 @@ describe('#start()', () => { expect(getUrlForApp('app1', { path: '//deep/link//' })).toBe('/base-path/app/app1/deep/link'); expect(getUrlForApp('app1', { path: 'deep/link///' })).toBe('/base-path/app/app1/deep/link'); }); + + it('creates absolute URLs when `absolute` parameter is true', async () => { + service.setup(setupDeps); + const { getUrlForApp } = await service.start(startDeps); + + expect(getUrlForApp('app1', { absolute: true })).toBe('http://localhost/base-path/app/app1'); + expect(getUrlForApp('app2', { path: 'deep/link', absolute: true })).toBe( + 'http://localhost/base-path/app/app2/deep/link' + ); + }); }); describe('navigateToApp', () => { diff --git a/src/core/public/application/application_service.tsx b/src/core/public/application/application_service.tsx index d100457f4027f..77f06e316c0aa 100644 --- a/src/core/public/application/application_service.tsx +++ b/src/core/public/application/application_service.tsx @@ -272,8 +272,13 @@ export class ApplicationService { takeUntil(this.stop$) ), registerMountContext: this.mountContext.registerContext, - getUrlForApp: (appId, { path }: { path?: string } = {}) => - http.basePath.prepend(getAppUrl(availableMounters, appId, path)), + getUrlForApp: ( + appId, + { path, absolute = false }: { path?: string; absolute?: boolean } = {} + ) => { + const relUrl = http.basePath.prepend(getAppUrl(availableMounters, appId, path)); + return absolute ? relativeToAbsolute(relUrl) : relUrl; + }, navigateToApp: async (appId, { path, state }: { path?: string; state?: any } = {}) => { if (await this.shouldNavigate(overlays)) { this.appLeaveHandlers.delete(this.currentAppId$.value!); @@ -364,3 +369,10 @@ const updateStatus = (app: T, statusUpdaters: AppUpdaterWrapp ...changes, }; }; + +function relativeToAbsolute(url: string) { + // convert all link urls to absolute urls + const a = document.createElement('a'); + a.setAttribute('href', url); + return a.href; +} diff --git a/src/core/public/application/types.ts b/src/core/public/application/types.ts index 17fdfc627187e..977bb7a52da22 100644 --- a/src/core/public/application/types.ts +++ b/src/core/public/application/types.ts @@ -593,11 +593,17 @@ export interface ApplicationStart { navigateToApp(appId: string, options?: { path?: string; state?: any }): Promise; /** - * Returns a relative URL to a given app, including the global base path. + * Returns an URL to a given app, including the global base path. + * By default, the URL is relative (/basePath/app/my-app). + * Use the `absolute` option to generate an absolute url (http://host:port/basePath/app/my-app) + * + * Note that when generating absolute urls, the protocol, host and port are determined from the browser location. + * * @param appId * @param options.path - optional path inside application to deep link to + * @param options.absolute - if true, will returns an absolute url instead of a relative one */ - getUrlForApp(appId: string, options?: { path?: string }): string; + getUrlForApp(appId: string, options?: { path?: string; absolute?: boolean }): string; /** * Register a context provider for application mounting. Will only be available to applications that depend on the @@ -612,11 +618,19 @@ export interface ApplicationStart { contextName: T, provider: IContextProvider ): void; + + /** + * An observable that emits the current application id and each subsequent id update. + */ + currentAppId$: Observable; } /** @internal */ export interface InternalApplicationStart - extends Pick { + 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 +654,6 @@ export interface InternalApplicationStart ): void; // Internal APIs - currentAppId$: Observable; getComponent(): JSX.Element | null; } diff --git a/src/core/public/legacy/legacy_service.ts b/src/core/public/legacy/legacy_service.ts index e4788e686dd45..1b7e25f585566 100644 --- a/src/core/public/legacy/legacy_service.ts +++ b/src/core/public/legacy/legacy_service.ts @@ -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, diff --git a/src/core/public/plugins/plugin_context.ts b/src/core/public/plugins/plugin_context.ts index 48100cba4f26e..19cfadf70be1b 100644 --- a/src/core/public/plugins/plugin_context.ts +++ b/src/core/public/plugins/plugin_context.ts @@ -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, diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index aa7ca4fee675e..5e9b609bde916 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -98,8 +98,10 @@ export interface ApplicationSetup { // @public (undocumented) export interface ApplicationStart { capabilities: RecursiveReadonly; + currentAppId$: Observable; getUrlForApp(appId: string, options?: { path?: string; + absolute?: boolean; }): string; navigateToApp(appId: string, options?: { path?: string; diff --git a/src/core/server/config/deprecation/core_deprecations.ts b/src/core/server/config/deprecation/core_deprecations.ts index 3aa7f9e2aa8ad..4fa51dcd5a082 100644 --- a/src/core/server/config/deprecation/core_deprecations.ts +++ b/src/core/server/config/deprecation/core_deprecations.ts @@ -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'), diff --git a/src/legacy/core_plugins/kibana/public/home/index.ts b/src/legacy/core_plugins/kibana/public/home/index.ts index c4e58e1a5e1ae..768e1a96de935 100644 --- a/src/legacy/core_plugins/kibana/public/home/index.ts +++ b/src/legacy/core_plugins/kibana/public/home/index.ts @@ -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 { - const injector = await chrome.dangerouslyGetActiveInjector(); - - const Private = injector.get('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 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; diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/__snapshots__/home.test.js.snap b/src/legacy/core_plugins/kibana/public/home/np_ready/components/__snapshots__/home.test.js.snap index 4563b633c3dfc..9d27362e62739 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/__snapshots__/home.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/__snapshots__/home.test.js.snap @@ -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`] = ` diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/__snapshots__/welcome.test.tsx.snap b/src/legacy/core_plugins/kibana/public/home/np_ready/components/__snapshots__/welcome.test.tsx.snap index 6f76ceecbba13..df7cc7bcbaed0 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/__snapshots__/welcome.test.tsx.snap +++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/__snapshots__/welcome.test.tsx.snap @@ -67,44 +67,6 @@ exports[`should render a Welcome screen with no telemetry disclaimer 1`] = ` - - - - - - - - - - - @@ -200,16 +162,16 @@ exports[`should render a Welcome screen with the telemetry disclaimer 1`] = ` /> diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/home.js b/src/legacy/core_plugins/kibana/public/home/np_ready/components/home.js index 0c09c6c3c74fc..617a1810028fc 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/home.js +++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/home.js @@ -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 { ); } @@ -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, + }), }; diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/home_app.js b/src/legacy/core_plugins/kibana/public/home/np_ready/components/home_app.js index f6c91b412381c..d7531864582a3 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/home_app.js +++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/home_app.js @@ -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} /> diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/welcome.test.tsx b/src/legacy/core_plugins/kibana/public/home/np_ready/components/welcome.test.tsx index 55c469fa58fc6..d9da47a2b43da 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/welcome.test.tsx +++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/welcome.test.tsx @@ -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 - {}} onOptInSeen={() => {}} /> + {}} 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 - {}} onOptInSeen={() => {}} currentOptInStatus={true} /> + {}} 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 - {}} onOptInSeen={() => {}} currentOptInStatus={false} /> + {}} 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 - {}} onOptInSeen={() => {}} /> + {}} 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 - {}} onOptInSeen={seen} /> + {}} telemetry={telemetry} /> ); - expect(seen).toHaveBeenCalled(); + expect(mockSetOptedInNoticeSeen).toHaveBeenCalled(); }); diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/welcome.tsx b/src/legacy/core_plugins/kibana/public/home/np_ready/components/welcome.tsx index 6983aabc4c7b1..7906caeda1b38 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/welcome.tsx +++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/welcome.tsx @@ -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 { }; 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 { } private renderTelemetryEnabledOrDisabledText = () => { - if (this.props.currentOptInStatus) { + const { telemetry } = this.props; + if (!telemetry) { + return null; + } + + const isOptedIn = telemetry.telemetryService.getIsOptedIn(); + if (isOptedIn) { return ( { }; render() { - const { urlBasePath } = this.props; + const { urlBasePath, telemetry } = this.props; return (
@@ -154,24 +164,24 @@ export class Welcome extends React.Component { onDecline={this.onSampleDataDecline} /> - - - - - - {this.renderTelemetryEnabledOrDisabledText()} - - + {!!telemetry && ( + + + + + + + {this.renderTelemetryEnabledOrDisabledText()} + + + + )}
diff --git a/src/legacy/core_plugins/kibana/public/home/plugin.ts b/src/legacy/core_plugins/kibana/public/home/plugin.ts index e530906d5698e..5cc7c9c11dd2f 100644 --- a/src/legacy/core_plugins/kibana/public/home/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/home/plugin.ts @@ -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; }; 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; } diff --git a/src/legacy/core_plugins/telemetry/common/constants.ts b/src/legacy/core_plugins/telemetry/common/constants.ts index cf2c9c883871b..52981c04ad34a 100644 --- a/src/legacy/core_plugins/telemetry/common/constants.ts +++ b/src/legacy/core_plugins/telemetry/common/constants.ts @@ -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. */ diff --git a/src/legacy/core_plugins/telemetry/common/get_xpack_config_with_deprecated.ts b/src/legacy/core_plugins/telemetry/common/get_xpack_config_with_deprecated.ts deleted file mode 100644 index 3f7a8d3410993..0000000000000 --- a/src/legacy/core_plugins/telemetry/common/get_xpack_config_with_deprecated.ts +++ /dev/null @@ -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); -} diff --git a/src/legacy/core_plugins/telemetry/index.ts b/src/legacy/core_plugins/telemetry/index.ts index 2a81e3fa05c6c..ec70380d83a0a 100644 --- a/src/legacy/core_plugins/telemetry/index.ts +++ b/src/legacy/core_plugins/telemetry/index.ts @@ -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, }; diff --git a/src/legacy/core_plugins/telemetry/public/components/__snapshots__/telemetry_form.test.js.snap b/src/legacy/core_plugins/telemetry/public/components/__snapshots__/telemetry_form.test.js.snap deleted file mode 100644 index 079a43e77616d..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/components/__snapshots__/telemetry_form.test.js.snap +++ /dev/null @@ -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`] = ` - - - - - - -

- -

-
-
-
- - -

- - - , - } - } - /> -

-

- - - -

- , - "type": "boolean", - "value": false, - } - } - /> -
-
-
-`; diff --git a/src/legacy/core_plugins/telemetry/public/components/telemetry_form.test.js b/src/legacy/core_plugins/telemetry/public/components/telemetry_form.test.js deleted file mode 100644 index fe0c2c3449af1..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/components/telemetry_form.test.js +++ /dev/null @@ -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( - - ) - ).toMatchSnapshot(); - }); - - it(`doesn't render form when not allowed to change optIn status`, () => { - mockInjectedMetadata({ telemetryOptedIn: null, allowChangingOptInStatus: false }); - - expect( - shallowWithIntl( - - ) - ).toMatchSnapshot(); - }); -}); diff --git a/src/legacy/core_plugins/telemetry/public/hacks/__tests__/fetch_telemetry.js b/src/legacy/core_plugins/telemetry/public/hacks/__tests__/fetch_telemetry.js deleted file mode 100644 index ad9ee0998e3bb..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/hacks/__tests__/fetch_telemetry.js +++ /dev/null @@ -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 - ); - }); -}); diff --git a/src/legacy/core_plugins/telemetry/public/hacks/__tests__/telemetry.js b/src/legacy/core_plugins/telemetry/public/hacks/__tests__/telemetry.js deleted file mode 100644 index 74f1de4934a78..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/hacks/__tests__/telemetry.js +++ /dev/null @@ -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'); diff --git a/src/legacy/core_plugins/telemetry/public/hacks/fetch_telemetry.js b/src/legacy/core_plugins/telemetry/public/hacks/fetch_telemetry.js deleted file mode 100644 index ede81f638a3fc..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/hacks/fetch_telemetry.js +++ /dev/null @@ -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(), - }, - }); -} diff --git a/src/legacy/core_plugins/telemetry/public/hacks/telemetry.js b/src/legacy/core_plugins/telemetry/public/hacks/telemetry.js deleted file mode 100644 index 8fa777ead3e4b..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/hacks/telemetry.js +++ /dev/null @@ -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 diff --git a/src/legacy/core_plugins/telemetry/public/hacks/telemetry.test.js b/src/legacy/core_plugins/telemetry/public/hacks/telemetry.test.js deleted file mode 100644 index 45a0653cd7a54..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/hacks/telemetry.test.js +++ /dev/null @@ -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()); - }); -}); diff --git a/src/legacy/core_plugins/telemetry/public/hacks/telemetry_init.ts b/src/legacy/core_plugins/telemetry/public/hacks/telemetry_init.ts deleted file mode 100644 index 1930d65d5c09b..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/hacks/telemetry_init.ts +++ /dev/null @@ -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); diff --git a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/click_banner.js b/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/click_banner.js deleted file mode 100644 index 44971e2466794..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/click_banner.js +++ /dev/null @@ -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: ( - - ), - text: ( - -

- -

- - - -
- ), - }); - } -} diff --git a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/click_banner.test.js b/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/click_banner.test.js deleted file mode 100644 index 0caabe826ae57..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/click_banner.test.js +++ /dev/null @@ -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); - }); -}); diff --git a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/handle_old_settings.js b/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/handle_old_settings.js deleted file mode 100644 index c03fdb85c4d1c..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/handle_old_settings.js +++ /dev/null @@ -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 will 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; -} diff --git a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/handle_old_settings.test.js b/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/handle_old_settings.test.js deleted file mode 100644 index 8f05675565a5e..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/handle_old_settings.test.js +++ /dev/null @@ -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); - }); -}); diff --git a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/inject_banner.js b/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/inject_banner.js deleted file mode 100644 index c4c5c3e9e0aa2..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/inject_banner.js +++ /dev/null @@ -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); - } -} diff --git a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/render_banner.js b/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/render_banner.js deleted file mode 100644 index 70b5030866620..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/render_banner.js +++ /dev/null @@ -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: ( - clickBanner(telemetryOptInProvider, optIn)} - fetchTelemetry={fetchTelemetry} - /> - ), - priority: 10000, - }); - - telemetryOptInProvider.setBannerId(bannerId); -} diff --git a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/render_notice_banner.js b/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/render_notice_banner.js deleted file mode 100644 index 2aa53db11c1d9..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/render_notice_banner.js +++ /dev/null @@ -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: , - priority: 10000, - }); - - telemetryOptInProvider.setOptInBannerNoticeId(bannerId); -} diff --git a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/should_show_banner.js b/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/should_show_banner.js deleted file mode 100644 index ee55f6cc76266..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/should_show_banner.js +++ /dev/null @@ -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)) - ); -} diff --git a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/should_show_banner.test.js b/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/should_show_banner.test.js deleted file mode 100644 index 9578d462bc85c..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/should_show_banner.test.js +++ /dev/null @@ -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); - }); -}); diff --git a/src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.test.js b/src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.test.js deleted file mode 100644 index 494ed24bcc1cb..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.test.js +++ /dev/null @@ -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); - }); - }); -}); diff --git a/src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.test.mocks.js b/src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.test.mocks.js deleted file mode 100644 index 4543266be46df..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.test.mocks.js +++ /dev/null @@ -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(), - }, - }, -})); diff --git a/src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.ts b/src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.ts deleted file mode 100644 index af908bea7f4b1..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.ts +++ /dev/null @@ -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; -} diff --git a/src/legacy/core_plugins/telemetry/public/views/management/index.js b/src/legacy/core_plugins/telemetry/public/views/management/index.ts similarity index 100% rename from src/legacy/core_plugins/telemetry/public/views/management/index.js rename to src/legacy/core_plugins/telemetry/public/views/management/index.ts diff --git a/src/legacy/core_plugins/telemetry/public/views/management/management.js b/src/legacy/core_plugins/telemetry/public/views/management/management.tsx similarity index 52% rename from src/legacy/core_plugins/telemetry/public/views/management/management.js rename to src/legacy/core_plugins/telemetry/public/views/management/management.tsx index 7032775e391bb..c8ae410e0aa57 100644 --- a/src/legacy/core_plugins/telemetry/public/views/management/management.js +++ b/src/legacy/core_plugins/telemetry/public/views/management/management.tsx @@ -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 => ( - - ); + if (telemetry && advancedSettings) { + const componentRegistry = advancedSettings.component; + const Component = (props: any) => ( + + ); - componentRegistry.register( - componentRegistry.componentType.PAGE_FOOTER_COMPONENT, - Component, - true - ); + componentRegistry.register( + componentRegistry.componentType.PAGE_FOOTER_COMPONENT, + Component, + true + ); + } }, }, }); diff --git a/src/legacy/core_plugins/telemetry/server/collectors/usage/telemetry_usage_collector.ts b/src/legacy/core_plugins/telemetry/server/collectors/usage/telemetry_usage_collector.ts index 99090cb2fb7ef..6919b6959aa8c 100644 --- a/src/legacy/core_plugins/telemetry/server/collectors/usage/telemetry_usage_collector.ts +++ b/src/legacy/core_plugins/telemetry/server/collectors/usage/telemetry_usage_collector.ts @@ -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); }, diff --git a/src/legacy/core_plugins/telemetry/server/fetcher.ts b/src/legacy/core_plugins/telemetry/server/fetcher.ts index 9edd8457f2b89..6e16328c4abd8 100644 --- a/src/legacy/core_plugins/telemetry/server/fetcher.ts +++ b/src/legacy/core_plugins/telemetry/server/fetcher.ts @@ -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({ diff --git a/src/legacy/core_plugins/telemetry/server/handle_old_settings/handle_old_settings.ts b/src/legacy/core_plugins/telemetry/server/handle_old_settings/handle_old_settings.ts new file mode 100644 index 0000000000000..b28a01bffa44d --- /dev/null +++ b/src/legacy/core_plugins/telemetry/server/handle_old_settings/handle_old_settings.ts @@ -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 will 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, + }); + } +} diff --git a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/index.js b/src/legacy/core_plugins/telemetry/server/handle_old_settings/index.ts similarity index 93% rename from src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/index.js rename to src/legacy/core_plugins/telemetry/server/handle_old_settings/index.ts index ffb0e88c60a0d..77eae0d80db61 100644 --- a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/index.js +++ b/src/legacy/core_plugins/telemetry/server/handle_old_settings/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { injectBanner } from './inject_banner'; +export { handleOldSettings } from './handle_old_settings'; diff --git a/src/legacy/core_plugins/telemetry/server/index.ts b/src/legacy/core_plugins/telemetry/server/index.ts index 6c62d03adf25c..85d7d80234ffc 100644 --- a/src/legacy/core_plugins/telemetry/server/index.ts +++ b/src/legacy/core_plugins/telemetry/server/index.ts @@ -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) => diff --git a/src/legacy/ui/public/new_platform/new_platform.ts b/src/legacy/ui/public/new_platform/new_platform.ts index e300ce4a0caf8..ff8fc9b07879c 100644 --- a/src/legacy/ui/public/new_platform/new_platform.ts +++ b/src/legacy/ui/public/new_platform/new_platform.ts @@ -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 = { diff --git a/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap b/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap index 2f2332bb06e3c..eebbc63f6f1e4 100644 --- a/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap +++ b/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap @@ -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], diff --git a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/should_show_opt_in_banner.js b/src/plugins/telemetry/common/constants.ts similarity index 61% rename from src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/should_show_opt_in_banner.js rename to src/plugins/telemetry/common/constants.ts index 45539c4eea46c..7b7694ed9aed7 100644 --- a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/should_show_opt_in_banner.js +++ b/src/plugins/telemetry/common/constants.ts @@ -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 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 async function shouldShowOptInBanner(telemetryOptInProvider) { - return telemetryOptInProvider.notifyUserAboutOptInDefault(); -} +export const PRIVACY_STATEMENT_URL = `https://www.elastic.co/legal/privacy-statement`; diff --git a/src/plugins/telemetry/kibana.json b/src/plugins/telemetry/kibana.json new file mode 100644 index 0000000000000..3a28149276c3e --- /dev/null +++ b/src/plugins/telemetry/kibana.json @@ -0,0 +1,6 @@ +{ + "id": "telemetry", + "version": "kibana", + "server": false, + "ui": true +} diff --git a/src/plugins/telemetry/public/components/__snapshots__/opt_in_banner.test.tsx.snap b/src/plugins/telemetry/public/components/__snapshots__/opt_in_banner.test.tsx.snap new file mode 100644 index 0000000000000..87e60869f6c21 --- /dev/null +++ b/src/plugins/telemetry/public/components/__snapshots__/opt_in_banner.test.tsx.snap @@ -0,0 +1,54 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`OptInDetailsComponent renders as expected 1`] = ` + + } +> + + + + + + + + + + + + + + + +`; diff --git a/src/legacy/core_plugins/telemetry/public/components/__snapshots__/opt_in_details_component.test.tsx.snap b/src/plugins/telemetry/public/components/__snapshots__/opt_in_example_flyout.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/telemetry/public/components/__snapshots__/opt_in_details_component.test.tsx.snap rename to src/plugins/telemetry/public/components/__snapshots__/opt_in_example_flyout.test.tsx.snap diff --git a/src/legacy/core_plugins/telemetry/public/components/__snapshots__/opt_in_message.test.tsx.snap b/src/plugins/telemetry/public/components/__snapshots__/opt_in_message.test.tsx.snap similarity index 97% rename from src/legacy/core_plugins/telemetry/public/components/__snapshots__/opt_in_message.test.tsx.snap rename to src/plugins/telemetry/public/components/__snapshots__/opt_in_message.test.tsx.snap index c80485332fa8a..7fa69a7409c6a 100644 --- a/src/legacy/core_plugins/telemetry/public/components/__snapshots__/opt_in_message.test.tsx.snap +++ b/src/plugins/telemetry/public/components/__snapshots__/opt_in_message.test.tsx.snap @@ -9,6 +9,7 @@ exports[`OptInMessage renders as expected 1`] = ` Object { "privacyStatementLink": { + it('renders as expected', () => { + expect(shallowWithIntl( {}} />)).toMatchSnapshot(); + }); + + it('fires the "onChangeOptInClick" prop with true when a enable is clicked', () => { + const onClick = jest.fn(); + const component = shallowWithIntl(); + + 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(); + + 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); + }); +}); diff --git a/src/legacy/core_plugins/telemetry/public/components/opt_in_banner_component.tsx b/src/plugins/telemetry/public/components/opt_in_banner.tsx similarity index 84% rename from src/legacy/core_plugins/telemetry/public/components/opt_in_banner_component.tsx rename to src/plugins/telemetry/public/components/opt_in_banner.tsx index 2813af9c499e7..adf7b8bc84719 100644 --- a/src/legacy/core_plugins/telemetry/public/components/opt_in_banner_component.tsx +++ b/src/plugins/telemetry/public/components/opt_in_banner.tsx @@ -23,15 +23,12 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { OptInMessage } from './opt_in_message'; interface Props { - fetchTelemetry: () => Promise; - optInClick: (optIn: boolean) => void; + onChangeOptInClick: (isOptIn: boolean) => void; } -/** - * React component for displaying the Telemetry opt-in banner. - */ export class OptInBanner extends React.PureComponent { render() { + const { onChangeOptInClick } = this.props; const title = ( { ); return ( - + - this.props.optInClick(true)}> + onChangeOptInClick(true)}> { - this.props.optInClick(false)}> + onChangeOptInClick(false)}> { it('renders as expected', () => { expect( shallowWithIntl( - ({ data: [] }))} - onClose={jest.fn()} - /> + [])} onClose={jest.fn()} /> ) ).toMatchSnapshot(); }); diff --git a/src/legacy/core_plugins/telemetry/public/components/opt_in_details_component.tsx b/src/plugins/telemetry/public/components/opt_in_example_flyout.tsx similarity index 91% rename from src/legacy/core_plugins/telemetry/public/components/opt_in_details_component.tsx rename to src/plugins/telemetry/public/components/opt_in_example_flyout.tsx index 12ab780e75990..9ecbd4df20560 100644 --- a/src/legacy/core_plugins/telemetry/public/components/opt_in_details_component.tsx +++ b/src/plugins/telemetry/public/components/opt_in_example_flyout.tsx @@ -37,7 +37,7 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; interface Props { - fetchTelemetry: () => Promise; + fetchExample: () => Promise; onClose: () => void; } @@ -57,22 +57,21 @@ export class OptInExampleFlyout extends React.PureComponent { 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) { diff --git a/src/legacy/core_plugins/telemetry/public/components/opt_in_message.test.tsx b/src/plugins/telemetry/public/components/opt_in_message.test.tsx similarity index 89% rename from src/legacy/core_plugins/telemetry/public/components/opt_in_message.test.tsx rename to src/plugins/telemetry/public/components/opt_in_message.test.tsx index 1a9fabceda907..dbe0941345a02 100644 --- a/src/legacy/core_plugins/telemetry/public/components/opt_in_message.test.tsx +++ b/src/plugins/telemetry/public/components/opt_in_message.test.tsx @@ -22,8 +22,6 @@ import { OptInMessage } from './opt_in_message'; describe('OptInMessage', () => { it('renders as expected', () => { - expect( - shallowWithIntl( [])} />) - ).toMatchSnapshot(); + expect(shallowWithIntl()).toMatchSnapshot(); }); }); diff --git a/src/legacy/core_plugins/telemetry/public/components/opt_in_message.tsx b/src/plugins/telemetry/public/components/opt_in_message.tsx similarity index 81% rename from src/legacy/core_plugins/telemetry/public/components/opt_in_message.tsx rename to src/plugins/telemetry/public/components/opt_in_message.tsx index 4221d78516e10..590a115b2bb6c 100644 --- a/src/legacy/core_plugins/telemetry/public/components/opt_in_message.tsx +++ b/src/plugins/telemetry/public/components/opt_in_message.tsx @@ -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; -} - -interface State { - showDetails: boolean; - showExample: boolean; -} - -export class OptInMessage extends React.PureComponent { - public readonly state: State = { - showDetails: false, - showExample: false, - }; - - toggleShowExample = () => { - this.setState(prevState => ({ - showExample: !prevState.showExample, - })); - }; - +export class OptInMessage extends React.PureComponent { render() { return ( @@ -52,7 +31,7 @@ export class OptInMessage extends React.PureComponent { 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: ( - + { it('renders as expected', () => { - expect(shallowWithIntl( {}} />)).toMatchSnapshot(); + expect(shallowWithIntl( {}} />)).toMatchSnapshot(); }); it('fires the "onSeenBanner" prop when a link is clicked', () => { const onLinkClick = jest.fn(); - const component = shallowWithIntl(); + const component = shallowWithIntl(); const button = component.findWhere(n => n.type() === EuiButton); diff --git a/src/legacy/core_plugins/telemetry/public/components/opted_in_notice_banner.tsx b/src/plugins/telemetry/public/components/opted_in_notice_banner.tsx similarity index 75% rename from src/legacy/core_plugins/telemetry/public/components/opted_in_notice_banner.tsx rename to src/plugins/telemetry/public/components/opted_in_notice_banner.tsx index e37fa73ebe7b8..090893964c881 100644 --- a/src/legacy/core_plugins/telemetry/public/components/opted_in_notice_banner.tsx +++ b/src/plugins/telemetry/public/components/opted_in_notice_banner.tsx @@ -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 { - onLinkClick = () => { - this.props.onSeenBanner(); - return; - }; - +export class OptedInNoticeBanner extends React.PureComponent { render() { + const { onSeenBanner } = this.props; + const bannerTitle = i18n.translate('telemetry.telemetryOptedInNoticeTitle', { + defaultMessage: 'Help us improve the Elastic Stack', + }); + return ( - + @@ -59,10 +56,7 @@ export class OptedInBanner extends React.PureComponent { ), disableLink: ( - + { }} /> - + void; + showAppliesSettingMessage: boolean; + enableSaving: boolean; + query?: any; +} + +interface State { + processing: boolean; + showExample: boolean; + queryMatches: boolean | null; +} - state = { +export class TelemetryManagementSection extends Component { + 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 { {showExample && ( telemetryOptInProvider.fetchExample()} + fetchExample={telemetryService.fetchExample} onClose={this.toggleExample} /> )} @@ -106,15 +112,23 @@ export class TelemetryForm extends Component { {this.maybeGetAppliesSettingMessage()} ); - toggleOptIn = async () => { - const newOptInValue = !this.props.telemetryOptInProvider.getOptIn(); + toggleOptIn = async (): Promise => { + 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); } - ); + }); }); }; diff --git a/src/legacy/core_plugins/telemetry/public/hacks/telemetry_opt_in.js b/src/plugins/telemetry/public/index.ts similarity index 81% rename from src/legacy/core_plugins/telemetry/public/hacks/telemetry_opt_in.js rename to src/plugins/telemetry/public/index.ts index 4e53c7ecd7030..2f86d7749bb9b 100644 --- a/src/legacy/core_plugins/telemetry/public/hacks/telemetry_opt_in.js +++ b/src/plugins/telemetry/public/index.ts @@ -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(); +} diff --git a/src/plugins/telemetry/public/mocks.ts b/src/plugins/telemetry/public/mocks.ts new file mode 100644 index 0000000000000..93dc13c327509 --- /dev/null +++ b/src/plugins/telemetry/public/mocks.ts @@ -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; + +export const telemetryPluginMock = { + createSetupContract, +}; + +function createSetupContract(): Setup { + const telemetryService = mockTelemetryService(); + const telemetryNotifications = mockTelemetryNotifications({ telemetryService }); + + const setupContract: Setup = { + telemetryService, + telemetryNotifications, + }; + + return setupContract; +} diff --git a/src/plugins/telemetry/public/plugin.ts b/src/plugins/telemetry/public/plugin.ts new file mode 100644 index 0000000000000..7ba51cacd1949 --- /dev/null +++ b/src/plugins/telemetry/public/plugin.ts @@ -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 { + 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(); + } + } +} diff --git a/src/plugins/telemetry/public/services/index.ts b/src/plugins/telemetry/public/services/index.ts new file mode 100644 index 0000000000000..ff4404c626fe0 --- /dev/null +++ b/src/plugins/telemetry/public/services/index.ts @@ -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'; diff --git a/src/legacy/core_plugins/telemetry/public/services/index.ts b/src/plugins/telemetry/public/services/telemetry_notifications/index.ts similarity index 88% rename from src/legacy/core_plugins/telemetry/public/services/index.ts rename to src/plugins/telemetry/public/services/telemetry_notifications/index.ts index 8b02f8ce4c5b0..c6ba2cce1edb0 100644 --- a/src/legacy/core_plugins/telemetry/public/services/index.ts +++ b/src/plugins/telemetry/public/services/telemetry_notifications/index.ts @@ -17,5 +17,4 @@ * under the License. */ -export { TelemetryOptInProvider } from './telemetry_opt_in'; -export { isUnauthenticated } from './path'; +export { TelemetryNotifications } from './telemetry_notifications'; diff --git a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/render_notice_banner.test.js b/src/plugins/telemetry/public/services/telemetry_notifications/render_opt_in_banner.test.ts similarity index 56% rename from src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/render_notice_banner.test.js rename to src/plugins/telemetry/public/services/telemetry_notifications/render_opt_in_banner.test.ts index f40e0b188c198..020d8023b6003 100644 --- a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/render_notice_banner.test.js +++ b/src/plugins/telemetry/public/services/telemetry_notifications/render_opt_in_banner.test.ts @@ -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); }); }); diff --git a/src/plugins/telemetry/public/services/telemetry_notifications/render_opt_in_banner.tsx b/src/plugins/telemetry/public/services/telemetry_notifications/render_opt_in_banner.tsx new file mode 100644 index 0000000000000..6e0164df6403a --- /dev/null +++ b/src/plugins/telemetry/public/services/telemetry_notifications/render_opt_in_banner.tsx @@ -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; +} + +export function renderOptInBanner({ setOptIn, overlays }: RenderBannerConfig) { + const mount = toMountPoint(); + const bannerId = overlays.banners.add(mount, 10000); + + return bannerId; +} diff --git a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/render_banner.test.js b/src/plugins/telemetry/public/services/telemetry_notifications/render_opted_in_notice_banner.test.ts similarity index 52% rename from src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/render_banner.test.js rename to src/plugins/telemetry/public/services/telemetry_notifications/render_opted_in_notice_banner.test.ts index b4a86b36d922f..2d175024a74fb 100644 --- a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/render_banner.test.js +++ b/src/plugins/telemetry/public/services/telemetry_notifications/render_opted_in_notice_banner.test.ts @@ -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); }); }); diff --git a/src/legacy/core_plugins/telemetry/public/services/path.ts b/src/plugins/telemetry/public/services/telemetry_notifications/render_opted_in_notice_banner.tsx similarity index 59% rename from src/legacy/core_plugins/telemetry/public/services/path.ts rename to src/plugins/telemetry/public/services/telemetry_notifications/render_opted_in_notice_banner.tsx index 4af545e982eaa..e63e46af6e8ca 100644 --- a/src/legacy/core_plugins/telemetry/public/services/path.ts +++ b/src/plugins/telemetry/public/services/telemetry_notifications/render_opted_in_notice_banner.tsx @@ -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(); + const bannerId = overlays.banners.add(mount, 10000); + + return bannerId; } diff --git a/src/plugins/telemetry/public/services/telemetry_notifications/telemetry_notifications.test.ts b/src/plugins/telemetry/public/services/telemetry_notifications/telemetry_notifications.test.ts new file mode 100644 index 0000000000000..f767615d25253 --- /dev/null +++ b/src/plugins/telemetry/public/services/telemetry_notifications/telemetry_notifications.test.ts @@ -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); + }); +}); diff --git a/src/plugins/telemetry/public/services/telemetry_notifications/telemetry_notifications.ts b/src/plugins/telemetry/public/services/telemetry_notifications/telemetry_notifications.ts new file mode 100644 index 0000000000000..bf25bb592db82 --- /dev/null +++ b/src/plugins/telemetry/public/services/telemetry_notifications/telemetry_notifications.ts @@ -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 => { + if (this.optedInNoticeBannerId) { + this.overlays.banners.remove(this.optedInNoticeBannerId); + this.optedInNoticeBannerId = undefined; + } + + await this.telemetryService.setUserHasSeenNotice(); + }; +} diff --git a/src/plugins/telemetry/public/services/telemetry_sender.test.ts b/src/plugins/telemetry/public/services/telemetry_sender.test.ts new file mode 100644 index 0000000000000..e9f5765c10412 --- /dev/null +++ b/src/plugins/telemetry/public/services/telemetry_sender.test.ts @@ -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 { + 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; + + 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; + + 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); + }); + }); +}); diff --git a/src/plugins/telemetry/public/services/telemetry_sender.ts b/src/plugins/telemetry/public/services/telemetry_sender.ts new file mode 100644 index 0000000000000..fec2db0506eb7 --- /dev/null +++ b/src/plugins/telemetry/public/services/telemetry_sender.ts @@ -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 => { + 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); + } + }; +} diff --git a/src/plugins/telemetry/public/services/telemetry_service.test.ts b/src/plugins/telemetry/public/services/telemetry_service.test.ts new file mode 100644 index 0000000000000..0ebcd52f1423c --- /dev/null +++ b/src/plugins/telemetry/public/services/telemetry_service.test.ts @@ -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); + }); + }); +}); diff --git a/src/plugins/telemetry/public/services/telemetry_service.ts b/src/plugins/telemetry/public/services/telemetry_service.ts new file mode 100644 index 0000000000000..073886e7d1327 --- /dev/null +++ b/src/plugins/telemetry/public/services/telemetry_service.ts @@ -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 => { + 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 => { + 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 => { + 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 + } + }; +} diff --git a/test/functional/config.ie.js b/test/functional/config.ie.js index 5e8ea56a848dc..2c32ccb69db03 100644 --- a/test/functional/config.ie.js +++ b/test/functional/config.ie.js @@ -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,11 @@ export default async function({ readConfigFile }) { kbnTestServer: { ...defaultConfig.get('kbnTestServer'), - serverArgs: [...defaultConfig.get('kbnTestServer.serverArgs'), '--csp.strict=false'], + serverArgs: [ + ...defaultConfig.get('kbnTestServer.serverArgs'), + '--csp.strict=false', + '--telemetry.optIn=false', + ], }, }; } diff --git a/test/functional/config.js b/test/functional/config.js index 134ddf4e84b2d..155e844578c54 100644 --- a/test/functional/config.js +++ b/test/functional/config.js @@ -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, }, }, diff --git a/x-pack/.gitignore b/x-pack/.gitignore index 40a52f88dbbba..6bac5e181861d 100644 --- a/x-pack/.gitignore +++ b/x-pack/.gitignore @@ -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 diff --git a/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/telemetry_opt_in.test.js.snap b/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/telemetry_opt_in.test.js.snap deleted file mode 100644 index 575c47205f9c0..0000000000000 --- a/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/telemetry_opt_in.test.js.snap +++ /dev/null @@ -1,576 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`TelemetryOptIn should display when telemetry not opted in 1`] = ` - - -
- - -

- - Help Elastic support provide better service - -

-
- -
- - - - - - } - className="eui-AlignBaseline" - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - id="readMorePopover" - isOpen={false} - ownFocus={true} - panelPaddingSize="m" - > - -

- - - , - "telemetryPrivacyStatementLink": - - , - } - } - /> -

-
- , - } - } - /> - - } - onChange={[Function]} - > -
- -
- -
- - -`; - -exports[`TelemetryOptIn should not display when telemetry is opted in 1`] = ` - -`; - -exports[`TelemetryOptIn shouldn't display when telemetry optIn status can't change 1`] = ` - -`; diff --git a/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap b/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap index 353dc58e6d401..3bb8e4f8608a7 100644 --- a/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap +++ b/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap @@ -965,7 +965,6 @@ exports[`UploadLicense should display a modal when license requires acknowledgem className="euiSpacer euiSpacer--m" /> - @@ -1434,7 +1433,6 @@ exports[`UploadLicense should display an error when ES says license is expired 1 className="euiSpacer euiSpacer--m" /> - @@ -1903,7 +1901,6 @@ exports[`UploadLicense should display an error when ES says license is invalid 1 className="euiSpacer euiSpacer--m" /> - @@ -2368,7 +2365,6 @@ exports[`UploadLicense should display an error when submitting invalid JSON 1`] className="euiSpacer euiSpacer--m" /> - @@ -2837,7 +2833,6 @@ exports[`UploadLicense should display error when ES returns error 1`] = ` className="euiSpacer euiSpacer--m" /> - diff --git a/x-pack/legacy/plugins/license_management/__jest__/telemetry_opt_in.test.js b/x-pack/legacy/plugins/license_management/__jest__/telemetry_opt_in.test.js deleted file mode 100644 index 1b03ce869e52b..0000000000000 --- a/x-pack/legacy/plugins/license_management/__jest__/telemetry_opt_in.test.js +++ /dev/null @@ -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(); - expect(rendered).toMatchSnapshot(); - }); - test('should not display when telemetry is opted in', () => { - setTelemetryOptInService({ - getOptIn: () => true, - canChangeOptInStatus: () => true, - }); - const rendered = mountWithIntl(); - expect(rendered).toMatchSnapshot(); - }); - test(`shouldn't display when telemetry optIn status can't change`, () => { - setTelemetryOptInService({ - getOptIn: () => false, - canChangeOptInStatus: () => false, - }); - const rendered = mountWithIntl(); - expect(rendered).toMatchSnapshot(); - }); -}); diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/app.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/app.js index 7c497518b9df5..6a6c38fa6abb6 100644 --- a/x-pack/legacy/plugins/license_management/public/np_ready/application/app.js +++ b/x-pack/legacy/plugins/license_management/public/np_ready/application/app.js @@ -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 => ; return ( - - + + ); diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/boot.tsx b/x-pack/legacy/plugins/license_management/public/np_ready/application/boot.tsx index 2780b54230eba..49bb4ce984e48 100644 --- a/x-pack/legacy/plugins/license_management/public/np_ready/application/boot.tsx +++ b/x-pack/legacy/plugins/license_management/public/np_ready/application/boot.tsx @@ -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( - + , diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/components/telemetry_opt_in/index.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/components/telemetry_opt_in/index.ts similarity index 100% rename from x-pack/legacy/plugins/license_management/public/np_ready/application/components/telemetry_opt_in/index.js rename to x-pack/legacy/plugins/license_management/public/np_ready/application/components/telemetry_opt_in/index.ts diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/components/telemetry_opt_in/telemetry_opt_in.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/components/telemetry_opt_in/telemetry_opt_in.tsx similarity index 84% rename from x-pack/legacy/plugins/license_management/public/np_ready/application/components/telemetry_opt_in/telemetry_opt_in.js rename to x-pack/legacy/plugins/license_management/public/np_ready/application/components/telemetry_opt_in/telemetry_opt_in.tsx index 5e570ae955dbf..eff5c6cc21c43 100644 --- a/x-pack/legacy/plugins/license_management/public/np_ready/application/components/telemetry_opt_in/telemetry_opt_in.js +++ b/x-pack/legacy/plugins/license_management/public/np_ready/application/components/telemetry_opt_in/telemetry_opt_in.tsx @@ -6,26 +6,31 @@ import React, { Fragment } from 'react'; import { EuiLink, EuiCheckbox, EuiSpacer, EuiText, EuiTitle, EuiPopover } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; import { - shouldShowTelemetryOptIn, - getTelemetryFetcher, - PRIVACY_STATEMENT_URL, OptInExampleFlyout, + PRIVACY_STATEMENT_URL, + TelemetryPluginSetup, } from '../../lib/telemetry'; -import { FormattedMessage } from '@kbn/i18n/react'; -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 { + 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 = ( this.setState({ showExample: false })} - fetchTelemetry={getTelemetryFetcher} + fetchExample={telemetry.telemetryService.fetchExample} /> ); } @@ -123,7 +130,7 @@ export class TelemetryOptIn extends React.Component { ); - return shouldShowTelemetryOptIn() ? ( + return ( {example} {toCurrentCustomers} @@ -144,6 +151,6 @@ export class TelemetryOptIn extends React.Component { onChange={this.onChangeOptIn} /> - ) : null; + ); } } diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/lib/telemetry.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/lib/telemetry.js deleted file mode 100644 index 10da5d7705a8c..0000000000000 --- a/x-pack/legacy/plugins/license_management/public/np_ready/application/lib/telemetry.js +++ /dev/null @@ -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 }); -}; diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/lib/telemetry.ts b/x-pack/legacy/plugins/license_management/public/np_ready/application/lib/telemetry.ts new file mode 100644 index 0000000000000..9cc4ec5978fdc --- /dev/null +++ b/x-pack/legacy/plugins/license_management/public/np_ready/application/lib/telemetry.ts @@ -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; +} diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/license_dashboard.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/license_dashboard.js index e14d392fe6706..56c307a0d76e5 100644 --- a/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/license_dashboard.js +++ b/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/license_dashboard.js @@ -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: () => {} } - + diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/start_trial/index.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/start_trial/index.ts similarity index 95% rename from x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/start_trial/index.js rename to x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/start_trial/index.ts index b9b33e7e3f2cb..1b3c956edc3ab 100644 --- a/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/start_trial/index.js +++ b/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/start_trial/index.ts @@ -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'; diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/start_trial/start_trial.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/start_trial/start_trial.tsx similarity index 87% rename from x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/start_trial/start_trial.js rename to x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/start_trial/start_trial.tsx index 532c1d5e1a32f..e0f8ade8e45da 100644 --- a/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/start_trial/start_trial.js +++ b/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/start_trial/start_trial.tsx @@ -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 { + 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 { - { - this.telemetryOptIn = ref; - }} - /> + {shouldShowTelemetryOptIn(telemetry) && ( + + )} @@ -182,7 +208,7 @@ export class StartTrial extends React.PureComponent { { + 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 ( @@ -170,11 +179,13 @@ export class UploadLicense extends React.PureComponent { - { - this.telemetryOptIn = ref; - }} - /> + {shouldShowTelemetryOptIn(telemetry) && ( + + )} diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/plugin.ts b/x-pack/legacy/plugins/license_management/public/np_ready/plugin.ts index 1da3c942830ca..60876c9b638d1 100644 --- a/x-pack/legacy/plugins/license_management/public/np_ready/plugin.ts +++ b/x-pack/legacy/plugins/license_management/public/np_ready/plugin.ts @@ -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 { - 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 { http, element, chrome, + telemetry, }); }, }); diff --git a/x-pack/legacy/plugins/license_management/public/register_route.ts b/x-pack/legacy/plugins/license_management/public/register_route.ts index fc1678a866ad3..a8f27a7236a47 100644 --- a/x-pack/legacy/plugins/license_management/public/register_route.ts +++ b/x-pack/legacy/plugins/license_management/public/register_route.ts @@ -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 = `
`; @@ -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 }, } ); diff --git a/x-pack/legacy/plugins/xpack_main/index.js b/x-pack/legacy/plugins/xpack_main/index.js index f3994f7ebcc34..809d90d58d796 100644 --- a/x-pack/legacy/plugins/xpack_main/index.js +++ b/x-pack/legacy/plugins/xpack_main/index.js @@ -11,8 +11,6 @@ import { replaceInjectedVars } from './server/lib/replace_injected_vars'; import { setupXPackMain } from './server/lib/setup_xpack_main'; import { xpackInfoRoute, settingsRoute } from './server/routes/api/v1'; -import { has } from 'lodash'; - export { callClusterFactory } from './server/lib/call_cluster_factory'; import { registerMonitoringCollection } from './server/telemetry_collection'; @@ -82,21 +80,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'), - ]; - }, }); }; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index dbc6a015f9c97..6bcf61b53fd5f 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2492,10 +2492,6 @@ "telemetry.seeExampleOfWhatWeCollectLinkText": "収集されるデータの例を見る", "telemetry.telemetryBannerDescription": "Elastic Stackの改善にご協力ください使用状況データの収集は現在無効です。使用状況データの収集を有効にすると、製品とサービスを管理して改善することができます。詳細については、{privacyStatementLink}をご覧ください。", "telemetry.telemetryConfigDescription": "基本的な機能の利用状況に関する統計情報を提供して、Elastic Stack の改善にご協力ください。このデータは Elastic 社外と共有されません。", - "telemetry.telemetryConfigTitle": "遠隔測定オプトイン", - "telemetry.telemetryErrorNotificationMessageDescription.tryAgainText": "Kibana と Elasticsearch が現在も実行中であることを確認し、再試行してください。", - "telemetry.telemetryErrorNotificationMessageDescription.unableToSaveTelemetryPreferenceText": "遠隔測定設定を保存できません。", - "telemetry.telemetryErrorNotificationMessageTitle": "遠隔測定エラー", "telemetry.telemetryOptedInDisableUsage": "ここで使用状況データを無効にする", "telemetry.telemetryOptedInDismissMessage": "閉じる", "telemetry.telemetryOptedInNoticeDescription": "使用状況データがどのように製品とサービスの管理と改善につながるのかに関する詳細については、{privacyStatementLink}をご覧ください。収集を停止するには、{disableLink}。", @@ -13187,4 +13183,4 @@ "xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "フィールドを選択してください。", "xpack.watcher.watcherDescription": "アラートの作成、管理、監視によりデータへの変更を検知します。" } -} +} \ No newline at end of file diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 4a2c33eba79da..25382221716dd 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2492,10 +2492,6 @@ "telemetry.seeExampleOfWhatWeCollectLinkText": "查看我们收集的内容示例", "telemetry.telemetryBannerDescription": "想帮助我们改进 Elastic Stack?数据使用情况收集当前已禁用。启用数据使用情况收集可帮助我们管理并改善产品和服务。有关详情,请参阅我们的{privacyStatementLink}。", "telemetry.telemetryConfigDescription": "通过提供基本功能的使用情况统计信息,来帮助我们改进 Elastic Stack。我们不会在 Elastic 之外共享此数据。", - "telemetry.telemetryConfigTitle": "遥测选择加入", - "telemetry.telemetryErrorNotificationMessageDescription.tryAgainText": "确认 Kibana 和 Elasticsearch 仍在运行,然后重试。", - "telemetry.telemetryErrorNotificationMessageDescription.unableToSaveTelemetryPreferenceText": "无法保存遥测首选项。", - "telemetry.telemetryErrorNotificationMessageTitle": "遥测错误", "telemetry.telemetryOptedInDisableUsage": "请在此禁用使用情况数据", "telemetry.telemetryOptedInDismissMessage": "关闭", "telemetry.telemetryOptedInNoticeDescription": "要了解使用情况数据如何帮助我们管理和改善产品和服务,请参阅我们的{privacyStatementLink}。要停止收集,{disableLink}。", @@ -13186,4 +13182,4 @@ "xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "此字段必填。", "xpack.watcher.watcherDescription": "通过创建、管理和监测警报来检测数据中的更改。" } -} +} \ No newline at end of file diff --git a/x-pack/test/functional/config.ie.js b/x-pack/test/functional/config.ie.js index 081bab4b80457..bac4547b4aa5c 100644 --- a/x-pack/test/functional/config.ie.js +++ b/x-pack/test/functional/config.ie.js @@ -58,14 +58,17 @@ export default async function({ readConfigFile }) { defaults: { 'accessibility:disableAnimations': true, 'dateFormat:tz': 'UTC', - 'telemetry:optIn': false, 'state:storeInSessionStorage': true, }, }, kbnTestServer: { ...defaultConfig.get('kbnTestServer'), - serverArgs: [...defaultConfig.get('kbnTestServer.serverArgs'), '--csp.strict=false'], + serverArgs: [ + ...defaultConfig.get('kbnTestServer.serverArgs'), + '--csp.strict=false', + '--telemetry.optIn=false', + ], }, }; } diff --git a/x-pack/test/reporting/functional/reporting.js b/x-pack/test/reporting/functional/reporting.js index 0e1078a2a4c8b..012f0922c28cf 100644 --- a/x-pack/test/reporting/functional/reporting.js +++ b/x-pack/test/reporting/functional/reporting.js @@ -94,8 +94,8 @@ export default function({ getService, getPageObjects }) { // Generating and then comparing reports can take longer than the default 60s timeout because the comparePngs // function is taking about 15 seconds per comparison in jenkins. this.timeout(300000); - - await PageObjects.dashboard.switchToEditMode(); + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.gotoDashboardEditMode('My PDF Dashboard'); await PageObjects.reporting.setTimepickerInDataRange(); const visualizations = PageObjects.dashboard.getTestVisualizationNames(); @@ -135,7 +135,8 @@ export default function({ getService, getPageObjects }) { it('matches baseline report', async function() { this.timeout(300000); - await PageObjects.dashboard.switchToEditMode(); + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.gotoDashboardEditMode('My PNG Dash'); await PageObjects.reporting.setTimepickerInDataRange(); const visualizations = PageObjects.dashboard.getTestVisualizationNames();