Ability to have telemetry always opted in (#49798) (#50236)

* Initial work

* WIP changes

* Turn off banner when allowChangingOptInStatus is true

* Fix bugs

* Fix broken jest tests

* Add jest tests for TelemetryForm

* Add TelemetryOptIn jest tests

* Make some adjustments to allow always being opted in

* Disallow turning telemetry completely off

* Fix bug in Joi config

* Keep route there
This commit is contained in:
Mike Côté 2019-11-11 19:43:50 -05:00 committed by GitHub
parent 02a63d69d6
commit deebc34331
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 470 additions and 23 deletions

View file

@ -17,6 +17,7 @@
* under the License.
*/
import * as Rx from 'rxjs';
import { resolve } from 'path';
import JoiNamespace from 'joi';
import { Server } from 'hapi';
@ -45,6 +46,14 @@ const telemetry = (kibana: any) => {
config(Joi: typeof JoiNamespace) {
return Joi.object({
enabled: Joi.boolean().default(true),
optIn: Joi.when('allowChangingOptInStatus', {
is: false,
then: Joi.valid(true),
otherwise: Joi.boolean()
.allow(null)
.default(null),
}),
allowChangingOptInStatus: Joi.boolean().default(true),
// `config` is used internally and not intended to be set
config: Joi.string().default(Joi.ref('$defaultConfigPath')),
banner: Joi.boolean().default(true),
@ -80,8 +89,25 @@ const telemetry = (kibana: any) => {
},
},
async replaceInjectedVars(originalInjectedVars: any, request: any) {
const config = request.server.config();
const optIn = config.get('telemetry.optIn');
const allowChangingOptInStatus = config.get('telemetry.allowChangingOptInStatus');
const currentKibanaVersion = getCurrentKibanaVersion(request.server);
const telemetryOptedIn = await getTelemetryOptIn({ request, currentKibanaVersion });
let telemetryOptedIn: boolean | null;
if (typeof optIn === 'boolean' && !allowChangingOptInStatus) {
// When not allowed to change optIn status and an optIn value is set, we'll overwrite with that
telemetryOptedIn = optIn;
} else {
telemetryOptedIn = await getTelemetryOptIn({
request,
currentKibanaVersion,
});
if (telemetryOptedIn === null) {
// In the senario there's no value set in telemetryOptedIn, we'll return optIn value
telemetryOptedIn = optIn;
}
}
return {
...originalInjectedVars,
@ -93,20 +119,36 @@ const telemetry = (kibana: any) => {
return {
telemetryEnabled: getXpackConfigWithDeprecated(config, 'telemetry.enabled'),
telemetryUrl: getXpackConfigWithDeprecated(config, 'telemetry.url'),
telemetryBanner: getXpackConfigWithDeprecated(config, 'telemetry.banner'),
telemetryOptedIn: null,
telemetryBanner:
config.get('telemetry.allowChangingOptInStatus') !== false &&
getXpackConfigWithDeprecated(config, 'telemetry.banner'),
telemetryOptedIn: config.get('telemetry.optIn'),
allowChangingOptInStatus: config.get('telemetry.allowChangingOptInStatus'),
};
},
hacks: ['plugins/telemetry/hacks/telemetry_init', 'plugins/telemetry/hacks/telemetry_opt_in'],
mappings,
},
init(server: Server) {
async init(server: Server) {
const initializerContext = {
env: {
packageInfo: {
version: getCurrentKibanaVersion(server),
},
},
config: {
create() {
const config = server.config();
return Rx.of({
enabled: config.get('telemetry.enabled'),
optIn: config.get('telemetry.optIn'),
config: config.get('telemetry.config'),
banner: config.get('telemetry.banner'),
url: config.get('telemetry.url'),
allowChangingOptInStatus: config.get('telemetry.allowChangingOptInStatus'),
});
},
},
} as PluginInitializerContext;
const coreSetup = ({
@ -114,7 +156,7 @@ const telemetry = (kibana: any) => {
log: server.log,
} as any) as CoreSetup;
telemetryPlugin(initializerContext).setup(coreSetup);
await telemetryPlugin(initializerContext).setup(coreSetup);
// register collectors
server.usage.collectorSet.register(createLocalizationUsageCollector(server));

View file

@ -1,6 +1,8 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`TelemetryForm renders as expected 1`] = `
exports[`TelemetryForm doesn't render form when not allowed to change optIn status 1`] = `""`;
exports[`TelemetryForm renders as expected when allows to change optIn status 1`] = `
<Fragment>
<EuiPanel
paddingSize="l"

View file

@ -78,6 +78,10 @@ export class TelemetryForm extends Component {
queryMatches,
} = this.state;
if (!telemetryOptInProvider.canChangeOptInStatus()) {
return null;
}
if (queryMatches !== null && !queryMatches) {
return null;
}

View file

@ -17,7 +17,7 @@
* under the License.
*/
import '../services/telemetry_opt_in.test.mocks';
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';
@ -33,6 +33,8 @@ const buildTelemetryOptInProvider = () => {
switch (key) {
case '$http':
return mockHttp;
case 'allowChangingOptInStatus':
return true;
default:
return null;
}
@ -47,7 +49,23 @@ const buildTelemetryOptInProvider = () => {
};
describe('TelemetryForm', () => {
it('renders as expected', () => {
it('renders as expected when allows to change optIn status', () => {
mockInjectedMetadata({ telemetryOptedIn: null, allowChangingOptInStatus: true });
expect(shallowWithIntl(
<TelemetryForm
spacesEnabled={false}
query={{ text: '' }}
onQueryMatchChange={jest.fn()}
telemetryOptInProvider={buildTelemetryOptInProvider()}
enableSaving={true}
/>)
).toMatchSnapshot();
});
it(`doesn't render form when not allowed to change optIn status`, () => {
mockInjectedMetadata({ telemetryOptedIn: null, allowChangingOptInStatus: false });
expect(shallowWithIntl(
<TelemetryForm
spacesEnabled={false}

View file

@ -72,7 +72,7 @@ describe('click_banner', () => {
const optIn = true;
const bannerId = 'bruce-banner';
mockInjectedMetadata({ telemetryOptedIn: optIn });
mockInjectedMetadata({ telemetryOptedIn: optIn, allowChangingOptInStatus: true });
const telemetryOptInProvider = getTelemetryOptInProvider();
telemetryOptInProvider.setBannerId(bannerId);
@ -92,7 +92,7 @@ describe('click_banner', () => {
remove: sinon.spy()
};
const optIn = true;
mockInjectedMetadata({ telemetryOptedIn: null });
mockInjectedMetadata({ telemetryOptedIn: null, allowChangingOptInStatus: true });
const telemetryOptInProvider = getTelemetryOptInProvider({ simulateFailure: true });
await clickBanner(telemetryOptInProvider, optIn, { _banners: banners, _toastNotifications: toastNotifications });
@ -110,7 +110,7 @@ describe('click_banner', () => {
remove: sinon.spy()
};
const optIn = false;
mockInjectedMetadata({ telemetryOptedIn: null });
mockInjectedMetadata({ telemetryOptedIn: null, allowChangingOptInStatus: true });
const telemetryOptInProvider = getTelemetryOptInProvider({ simulateError: true });
await clickBanner(telemetryOptInProvider, optIn, { _banners: banners, _toastNotifications: toastNotifications });

View file

@ -38,7 +38,7 @@ const getTelemetryOptInProvider = (enabled, { simulateFailure = false } = {}) =>
const chrome = {
addBasePath: url => url
};
mockInjectedMetadata({ telemetryOptedIn: enabled });
mockInjectedMetadata({ telemetryOptedIn: enabled, allowChangingOptInStatus: true });
const $injector = {
get: (key) => {

View file

@ -38,7 +38,7 @@ const getMockInjector = () => {
};
const getTelemetryOptInProvider = ({ telemetryOptedIn = null } = {}) => {
mockInjectedMetadata({ telemetryOptedIn });
mockInjectedMetadata({ telemetryOptedIn, allowChangingOptInStatus: true });
const injector = getMockInjector();
const chrome = {
addBasePath: (url) => url

View file

@ -34,7 +34,7 @@ describe('TelemetryOptInProvider', () => {
addBasePath: (url) => url
};
mockInjectedMetadata({ telemetryOptedIn: optedIn });
mockInjectedMetadata({ telemetryOptedIn: optedIn, allowChangingOptInStatus: true });
const mockInjector = {
get: (key) => {

View file

@ -24,10 +24,11 @@ import {
} from '../../../../../core/public/mocks';
const injectedMetadataMock = injectedMetadataServiceMock.createStartContract();
export function mockInjectedMetadata({ telemetryOptedIn }) {
export function mockInjectedMetadata({ telemetryOptedIn, allowChangingOptInStatus }) {
const mockGetInjectedVar = jest.fn().mockImplementation((key) => {
switch (key) {
case 'telemetryOptedIn': return telemetryOptedIn;
case 'allowChangingOptInStatus': return allowChangingOptInStatus;
default: throw new Error(`unexpected injectedVar ${key}`);
}
});

View file

@ -28,11 +28,15 @@ let currentOptInStatus = false;
export function TelemetryOptInProvider($injector: any, chrome: any) {
currentOptInStatus = npStart.core.injectedMetadata.getInjectedVar('telemetryOptedIn') as boolean;
const allowChangingOptInStatus = npStart.core.injectedMetadata.getInjectedVar(
'allowChangingOptInStatus'
) as boolean;
setCanTrackUiMetrics(currentOptInStatus);
const provider = {
getBannerId: () => bannerId,
getOptIn: () => currentOptInStatus,
canChangeOptInStatus: () => allowChangingOptInStatus,
setBannerId(id: string) {
bannerId = id;
},

View file

@ -29,7 +29,7 @@ export class TelemetryPlugin {
this.currentKibanaVersion = initializerContext.env.packageInfo.version;
}
public setup(core: CoreSetup) {
public async setup(core: CoreSetup) {
const currentKibanaVersion = this.currentKibanaVersion;
telemetryCollectionManager.setStatsGetter(getStats, 'local');
registerRoutes({ core, currentKibanaVersion });

View file

@ -27,6 +27,6 @@ interface RegisterRoutesParams {
}
export function registerRoutes({ core, currentKibanaVersion }: RegisterRoutesParams) {
registerOptInRoutes({ core, currentKibanaVersion });
registerTelemetryDataRoutes(core);
registerOptInRoutes({ core, currentKibanaVersion });
}

View file

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

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { setTelemetryEnabled, setTelemetryOptInService } from '../public/lib/telemetry';
import { TelemetryOptIn } from '../public/components/telemetry_opt_in';
import { mountWithIntl } from '../../../../test_utils/enzyme_helpers';
@ -11,16 +12,30 @@ jest.mock('ui/capabilities', () => ({
get: jest.fn(),
}));
setTelemetryEnabled(true);
describe('TelemetryOptIn', () => {
test('should display when telemetry not opted in', () => {
const telemetry = require('../public/lib/telemetry');
telemetry.showTelemetryOptIn = () => { return true; };
setTelemetryOptInService({
getOptIn: () => false,
canChangeOptInStatus: () => true,
});
const rendered = mountWithIntl(<TelemetryOptIn />);
expect(rendered).toMatchSnapshot();
});
test('should not display when telemetry is opted in', () => {
const telemetry = require('../public/lib/telemetry');
telemetry.showTelemetryOptIn = () => { return false; };
setTelemetryOptInService({
getOptIn: () => true,
canChangeOptInStatus: () => true,
});
const rendered = mountWithIntl(<TelemetryOptIn />);
expect(rendered).toMatchSnapshot();
});
test(`shouldn't display when telemetry optIn status can't change`, () => {
setTelemetryOptInService({
getOptIn: () => false,
canChangeOptInStatus: () => false,
});
const rendered = mountWithIntl(<TelemetryOptIn />);
expect(rendered).toMatchSnapshot();
});

View file

@ -25,7 +25,7 @@ export const optInToTelemetry = async (enableTelemetry) => {
await telemetryOptInService.setOptIn(enableTelemetry);
};
export const shouldShowTelemetryOptIn = () => {
return telemetryEnabled && !telemetryOptInService.getOptIn();
return telemetryEnabled && !telemetryOptInService.getOptIn() && telemetryOptInService.canChangeOptInStatus();
};
export const getTelemetryFetcher = () => {
return fetchTelemetry(httpClient, { unencrypted: true });