[8.8] [Telemetry] Update notice message (#158669) (#158842)

# Backport

This will backport the following commits from `main` to `8.8`:
- [[Telemetry] Update notice message
(#158669)](https://github.com/elastic/kibana/pull/158669)

<!--- Backport version: 8.9.7 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Alejandro Fernández
Haro","email":"alejandro.haro@elastic.co"},"sourceCommit":{"committedDate":"2023-06-01T15:53:02Z","message":"[Telemetry]
Update notice message (#158669)\n\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>\r\nCo-authored-by:
Jean-Louis Leysens
<jloleysens@gmail.com>","sha":"312ba3a758a05cea2d490a5174838cd84ac2cc8d","branchLabelMapping":{"^v8.9.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["Team:Docs","Team:Core","Feature:Telemetry","release_note:skip","backport:prev-minor","v8.9.0"],"number":158669,"url":"https://github.com/elastic/kibana/pull/158669","mergeCommit":{"message":"[Telemetry]
Update notice message (#158669)\n\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>\r\nCo-authored-by:
Jean-Louis Leysens
<jloleysens@gmail.com>","sha":"312ba3a758a05cea2d490a5174838cd84ac2cc8d"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v8.9.0","labelRegex":"^v8.9.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/158669","number":158669,"mergeCommit":{"message":"[Telemetry]
Update notice message (#158669)\n\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>\r\nCo-authored-by:
Jean-Louis Leysens
<jloleysens@gmail.com>","sha":"312ba3a758a05cea2d490a5174838cd84ac2cc8d"}}]}]
BACKPORT-->

Co-authored-by: Alejandro Fernández Haro <alejandro.haro@elastic.co>
This commit is contained in:
Kibana Machine 2023-06-01 13:08:50 -04:00 committed by GitHub
parent d841880951
commit 045c4bcb10
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 391 additions and 696 deletions

View file

@ -9,7 +9,7 @@ Usage Collection (also known as Telemetry) is enabled by default. This allows us
Refer to our https://www.elastic.co/legal/product-privacy-statement[Privacy Statement] to learn more.
You can control whether this data is sent from the {kib} servers, or if it should be sent
from the user's browser, in case a firewall is blocking the connections from the server. Additionally, you can decide to completely disable this feature either in the config file or in {kib} via *Management > Kibana > Advanced Settings > Usage Data*.
from the user's browser, in case a firewall is blocking the connections from the server. Additionally, you can disable this feature either in *Stack Management > {kib} > Advanced Settings > Global Settings > Usage collection* or the config file with the following settings.
[float]
[[telemetry-general-settings]]

View file

@ -348,7 +348,7 @@ export interface ISavedObjectsRepository {
* It will not create a nested structure like:
* `{attributes: {stats: {api: {counter: 1}}}}`
*
* When using incrementCounter for collecting usage data, you need to ensure
* When using incrementCounter you need to ensure
* that usage collection happens on a best-effort basis and doesn't
* negatively affect your plugin or users. See https://github.com/elastic/kibana/blob/main/src/plugins/usage_collection/README.mdx#tracking-interactions-with-incrementcounter)
*

View file

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

View file

@ -1,25 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`OptInMessage renders as expected 1`] = `
<Fragment>
<FormattedMessage
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."
id="telemetry.telemetryBannerDescription"
values={
Object {
"privacyStatementLink": <EuiLink
href="https://some-host/some-url"
rel="noopener"
target="_blank"
>
<FormattedMessage
defaultMessage="Privacy Statement"
id="telemetry.welcomeBanner.telemetryConfigDetailsDescription.telemetryPrivacyStatementLinkText"
values={Object {}}
/>
</EuiLink>,
}
}
/>
</Fragment>
`;

View file

@ -1,52 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`OptInDetailsComponent renders as expected 1`] = `
<EuiCallOut
title="Help us improve the Elastic Stack"
>
<FormattedMessage
defaultMessage="To learn about how usage data helps us manage and improve our products and services, see our {privacyStatementLink}. To stop collection, {disableLink}."
id="telemetry.telemetryOptedInNoticeDescription"
values={
Object {
"disableLink": <EuiLink
href="/app/management/kibana/settings"
onClick={[Function]}
>
<FormattedMessage
defaultMessage="disable usage data here"
id="telemetry.telemetryOptedInDisableUsage"
values={Object {}}
/>
</EuiLink>,
"privacyStatementLink": <EuiLink
href="https://some-host/some-url"
onClick={[Function]}
rel="noopener"
target="_blank"
>
<FormattedMessage
defaultMessage="Privacy Statement"
id="telemetry.telemetryOptedInPrivacyStatement"
values={Object {}}
/>
</EuiLink>,
}
}
/>
<EuiSpacer
size="s"
/>
<EuiButton
color="primary"
onClick={[Function]}
size="s"
>
<FormattedMessage
defaultMessage="Dismiss"
id="telemetry.telemetryOptedInDismissMessage"
values={Object {}}
/>
</EuiButton>
</EuiCallOut>
`;

View file

@ -1,65 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { EuiButton } from '@elastic/eui';
import { shallowWithIntl } from '@kbn/test-jest-helpers';
import { OptInBanner } from './opt_in_banner';
import { mockTelemetryConstants } from '../mocks';
describe('OptInDetailsComponent', () => {
const telemetryConstants = mockTelemetryConstants();
it('renders as expected', () => {
expect(
shallowWithIntl(
<OptInBanner onChangeOptInClick={() => {}} telemetryConstants={telemetryConstants} />
)
).toMatchSnapshot();
});
it('fires the "onChangeOptInClick" prop with true when a enable is clicked', () => {
const onClick = jest.fn();
const component = shallowWithIntl(
<OptInBanner onChangeOptInClick={onClick} telemetryConstants={telemetryConstants} />
);
const enableButton = component.findWhere((n) => {
const props = n.props();
return n.type() === EuiButton && props['data-test-subj'] === 'enable';
});
if (!enableButton) {
throw new Error(`Couldn't find any opt in enable button.`);
}
enableButton.simulate('click');
expect(onClick).toHaveBeenCalled();
expect(onClick).toBeCalledWith(true);
});
it('fires the "onChangeOptInClick" with false when a disable is clicked', () => {
const onClick = jest.fn();
const component = shallowWithIntl(
<OptInBanner onChangeOptInClick={onClick} telemetryConstants={telemetryConstants} />
);
const disableButton = component.findWhere((n) => {
const props = n.props();
return n.type() === EuiButton && props['data-test-subj'] === 'disable';
});
if (!disableButton) {
throw new Error(`Couldn't find any opt in disable button.`);
}
disableButton.simulate('click');
expect(onClick).toHaveBeenCalled();
expect(onClick).toBeCalledWith(false);
});
});

View file

@ -1,54 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import * as React from 'react';
import { EuiButton, EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { OptInMessage } from './opt_in_message';
import { TelemetryConstants } from '..';
interface Props {
onChangeOptInClick: (isOptIn: boolean) => void;
telemetryConstants: TelemetryConstants;
}
export class OptInBanner extends React.PureComponent<Props> {
render() {
const { onChangeOptInClick, telemetryConstants } = this.props;
const title = (
<FormattedMessage
id="telemetry.welcomeBanner.title"
defaultMessage="Help us improve the Elastic Stack"
/>
);
return (
<EuiCallOut iconType="questionInCircle" title={title}>
<OptInMessage telemetryConstants={telemetryConstants} />
<EuiSpacer size="s" />
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>
<EuiButton size="s" data-test-subj="enable" onClick={() => onChangeOptInClick(true)}>
<FormattedMessage
id="telemetry.welcomeBanner.enableButtonLabel"
defaultMessage="Enable"
/>
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton size="s" data-test-subj="disable" onClick={() => onChangeOptInClick(false)}>
<FormattedMessage
id="telemetry.welcomeBanner.disableButtonLabel"
defaultMessage="Disable"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiCallOut>
);
}
}

View file

@ -7,16 +7,97 @@
*/
import React from 'react';
import { shallowWithIntl } from '@kbn/test-jest-helpers';
import type { ReactWrapper } from 'enzyme';
import { mountWithIntl } from '@kbn/test-jest-helpers';
import { httpServiceMock } from '@kbn/core-http-browser-mocks';
import { mockTelemetryConstants, mockTelemetryService } from '../mocks';
import { OptInMessage } from './opt_in_message';
import { mockTelemetryConstants } from '../mocks';
const telemetryConstants = mockTelemetryConstants();
describe('OptInMessage', () => {
it('renders as expected', () => {
expect(
shallowWithIntl(<OptInMessage telemetryConstants={telemetryConstants} />)
).toMatchSnapshot();
const addBasePath = httpServiceMock.createBasePath().prepend;
const telemetryConstants = mockTelemetryConstants();
describe('when opted-in', () => {
const telemetryService = mockTelemetryService({ config: { optIn: true } });
let dom: ReactWrapper;
beforeAll(() => {
dom = mountWithIntl(
<OptInMessage
telemetryConstants={telemetryConstants}
telemetryService={telemetryService}
addBasePath={addBasePath}
/>
);
});
afterAll(() => {
dom.unmount();
});
it('claims that telemetry is enabled', () => {
expect(dom.text()).toContain('Usage collection (also known as Telemetry) is enabled.');
});
it('offers the link to disable it', () => {
expect(dom.text()).toContain('Disable usage collection.');
});
});
describe('when opted-out', () => {
const telemetryService = mockTelemetryService({ config: { optIn: false } });
let dom: ReactWrapper;
beforeAll(() => {
dom = mountWithIntl(
<OptInMessage
telemetryConstants={telemetryConstants}
telemetryService={telemetryService}
addBasePath={addBasePath}
/>
);
});
afterAll(() => {
dom.unmount();
});
it('claims that telemetry is disabled', () => {
expect(dom.text()).toContain('Usage collection (also known as Telemetry) is disabled.');
});
it('offers the link to enable it', () => {
expect(dom.text()).toContain('Enable usage collection.');
});
});
describe('when null', () => {
const telemetryService = mockTelemetryService({ config: { optIn: null } });
let dom: ReactWrapper;
beforeAll(() => {
dom = mountWithIntl(
<OptInMessage
telemetryConstants={telemetryConstants}
telemetryService={telemetryService}
addBasePath={addBasePath}
/>
);
});
afterAll(() => {
dom.unmount();
});
it('claims that telemetry is disabled', () => {
expect(dom.text()).toContain('Usage collection (also known as Telemetry) is disabled.');
});
it('offers the link to enable it', () => {
expect(dom.text()).toContain('Enable usage collection.');
});
});
});

View file

@ -9,35 +9,87 @@
import * as React from 'react';
import { EuiLink } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { TelemetryConstants } from '..';
import type { IBasePath } from '@kbn/core-http-browser';
import type { TelemetryService } from '../services';
import type { TelemetryConstants } from '..';
interface Props {
export interface OptInMessageProps {
telemetryConstants: TelemetryConstants;
telemetryService: TelemetryService;
addBasePath: IBasePath['prepend'];
onClick?: () => unknown;
}
export class OptInMessage extends React.PureComponent<Props> {
render() {
return (
<React.Fragment>
<FormattedMessage
id="telemetry.telemetryBannerDescription"
defaultMessage="Want to help us improve the Elastic Stack? Data usage collection is currently disabled. Enabling data usage collection helps us manage and improve our products and services. See our {privacyStatementLink} for more details."
values={{
privacyStatementLink: (
<EuiLink
href={this.props.telemetryConstants.getPrivacyStatementUrl()}
target="_blank"
rel="noopener"
>
<FormattedMessage
id="telemetry.welcomeBanner.telemetryConfigDetailsDescription.telemetryPrivacyStatementLinkText"
defaultMessage="Privacy Statement"
/>
</EuiLink>
),
}}
/>
</React.Fragment>
);
export const OptInMessage: React.FC<OptInMessageProps> = ({
addBasePath,
telemetryService,
telemetryConstants,
onClick,
}) => {
return (
<React.Fragment>
<FormattedMessage
id="telemetry.dataManagementDisclaimerPrivacy"
defaultMessage="Usage collection (also known as Telemetry) is {optInStatus}.
This allows us to learn what our users are most interested in, so we can improve our products and services.
Refer to our {privacyStatementLink}."
values={{
optInStatus: (
<em>
{telemetryService.isOptedIn ? (
<FormattedMessage id="telemetry.enabledStatus" defaultMessage="enabled" />
) : (
<FormattedMessage id="telemetry.disabledStatus" defaultMessage="disabled" />
)}
</em>
),
privacyStatementLink: (
/* eslint-disable-next-line @elastic/eui/href-or-on-click */
<EuiLink
onClick={onClick}
href={telemetryConstants.getPrivacyStatementUrl()}
target="_blank"
rel="noopener"
>
<FormattedMessage
id="telemetry.dataManagementDisclaimerPrivacyLink"
defaultMessage="Privacy Statement"
/>
</EuiLink>
),
}}
/>{' '}
{renderTelemetryEnabledOrDisabledText(telemetryService, addBasePath, onClick)}
</React.Fragment>
);
};
function renderTelemetryEnabledOrDisabledText(
telemetryService: TelemetryService,
addBasePath: (url: string) => string,
onClick?: () => unknown
) {
if (!telemetryService.userCanChangeSettings || !telemetryService.getCanChangeOptInStatus()) {
return null;
}
const isOptedIn = telemetryService.getIsOptedIn();
const actionMessage = isOptedIn ? (
<FormattedMessage
id="telemetry.dataManagementDisableCollectionLink"
defaultMessage="Disable usage collection."
/>
) : (
<FormattedMessage
id="telemetry.dataManagementEnableCollectionLink"
defaultMessage="Enable usage collection."
/>
);
return (
/* eslint-disable-next-line @elastic/eui/href-or-on-click */
<EuiLink href={addBasePath('management/kibana/settings')} onClick={onClick}>
{actionMessage}
</EuiLink>
);
}

View file

@ -8,34 +8,47 @@
import React from 'react';
import { EuiButton } from '@elastic/eui';
import { shallowWithIntl } from '@kbn/test-jest-helpers';
import { OptedInNoticeBanner } from './opted_in_notice_banner';
import { httpServiceMock } from '@kbn/core/public/mocks';
import { mockTelemetryConstants } from '../mocks';
import { shallowWithIntl } from '@kbn/test-jest-helpers';
import { mockTelemetryConstants, mockTelemetryService } from '../mocks';
import { OptInStatusNoticeBanner } from './opt_in_status_notice_banner';
import { OptInMessage } from './opt_in_message';
const mockHttp = httpServiceMock.createStartContract();
const telemetryConstants = mockTelemetryConstants();
const telemetryService = mockTelemetryService();
describe('OptInDetailsComponent', () => {
describe('OptInStatusNoticeBanner', () => {
it('renders as expected', () => {
const onSeenBanner = () => {};
const dom = shallowWithIntl(
<OptInStatusNoticeBanner
onSeenBanner={onSeenBanner}
http={mockHttp}
telemetryConstants={telemetryConstants}
telemetryService={telemetryService}
/>
);
expect(
shallowWithIntl(
<OptedInNoticeBanner
onSeenBanner={() => {}}
http={mockHttp}
dom.containsMatchingElement(
<OptInMessage
telemetryConstants={telemetryConstants}
telemetryService={telemetryService}
addBasePath={mockHttp.basePath.prepend}
onClick={onSeenBanner}
/>
)
).toMatchSnapshot();
).toBe(true);
});
it('fires the "onSeenBanner" prop when a link is clicked', () => {
const onLinkClick = jest.fn();
const component = shallowWithIntl(
<OptedInNoticeBanner
<OptInStatusNoticeBanner
onSeenBanner={onLinkClick}
http={mockHttp}
telemetryConstants={telemetryConstants}
telemetryService={telemetryService}
/>
);

View file

@ -0,0 +1,53 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
/* eslint @elastic/eui/href-or-on-click:0 */
import * as React from 'react';
import { EuiButton, EuiCallOut, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { HttpSetup } from '@kbn/core/public';
import { OptInMessage } from './opt_in_message';
import { TelemetryService } from '../services';
import { TelemetryConstants } from '..';
interface Props {
http: HttpSetup;
onSeenBanner: () => unknown;
telemetryConstants: TelemetryConstants;
telemetryService: TelemetryService;
}
export const OptInStatusNoticeBanner: React.FC<Props> = ({
onSeenBanner,
http,
telemetryConstants,
telemetryService,
}) => {
const addBasePath = http.basePath.prepend;
const bannerTitle = i18n.translate('telemetry.telemetryOptedInNoticeTitle', {
defaultMessage: 'Help us improve the Elastic Stack',
});
return (
<EuiCallOut title={bannerTitle}>
<OptInMessage
telemetryConstants={telemetryConstants}
telemetryService={telemetryService}
addBasePath={addBasePath}
onClick={onSeenBanner}
/>
<EuiSpacer size="s" />
<EuiButton size="s" onClick={onSeenBanner}>
<FormattedMessage id="telemetry.telemetryOptedInDismissMessage" defaultMessage="Dismiss" />
</EuiButton>
</EuiCallOut>
);
};

View file

@ -1,73 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
/* eslint @elastic/eui/href-or-on-click:0 */
import * as React from 'react';
import { EuiButton, EuiLink, EuiCallOut, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { HttpSetup } from '@kbn/core/public';
import { PATH_TO_ADVANCED_SETTINGS } from '../../common/constants';
import { TelemetryConstants } from '..';
interface Props {
http: HttpSetup;
onSeenBanner: () => unknown;
telemetryConstants: TelemetryConstants;
}
export class OptedInNoticeBanner extends React.PureComponent<Props> {
render() {
const { onSeenBanner, http, telemetryConstants } = this.props;
const basePath = http.basePath.get();
const bannerTitle = i18n.translate('telemetry.telemetryOptedInNoticeTitle', {
defaultMessage: 'Help us improve the Elastic Stack',
});
return (
<EuiCallOut title={bannerTitle}>
<FormattedMessage
id="telemetry.telemetryOptedInNoticeDescription"
defaultMessage="To learn about how usage data helps us manage and improve our products and services, see our {privacyStatementLink}. To stop collection, {disableLink}."
values={{
privacyStatementLink: (
<EuiLink
onClick={onSeenBanner}
href={telemetryConstants.getPrivacyStatementUrl()}
target="_blank"
rel="noopener"
>
<FormattedMessage
id="telemetry.telemetryOptedInPrivacyStatement"
defaultMessage="Privacy Statement"
/>
</EuiLink>
),
disableLink: (
<EuiLink href={`${basePath}${PATH_TO_ADVANCED_SETTINGS}`} onClick={onSeenBanner}>
<FormattedMessage
id="telemetry.telemetryOptedInDisableUsage"
defaultMessage="disable usage data here"
/>
</EuiLink>
),
}}
/>
<EuiSpacer size="s" />
<EuiButton size="s" onClick={onSeenBanner}>
<FormattedMessage
id="telemetry.telemetryOptedInDismissMessage"
defaultMessage="Dismiss"
/>
</EuiButton>
</EuiCallOut>
);
}
}

View file

@ -7,81 +7,16 @@
*/
import React from 'react';
import { EuiLink, EuiSpacer, EuiTextColor } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import type { TelemetryConstants } from '../plugin';
import type { TelemetryService } from '../services';
import { EuiSpacer, EuiTextColor } from '@elastic/eui';
import { OptInMessage, type OptInMessageProps } from './opt_in_message';
interface Props {
telemetryService: TelemetryService;
addBasePath: (url: string) => string;
telemetryConstants: TelemetryConstants;
}
export const WelcomeTelemetryNotice: React.FC<Props> = ({
telemetryService,
addBasePath,
telemetryConstants,
}: Props) => {
export const WelcomeTelemetryNotice: React.FC<OptInMessageProps> = (props) => {
return (
<>
<EuiTextColor className="euiText--small" color="subdued">
<FormattedMessage
id="telemetry.dataManagementDisclaimerPrivacy"
defaultMessage="To learn about how usage data helps us manage and improve our products and services, see our "
/>
<EuiLink href={telemetryConstants.getPrivacyStatementUrl()} target="_blank" rel="noopener">
<FormattedMessage
id="telemetry.dataManagementDisclaimerPrivacyLink"
defaultMessage="Privacy Statement."
/>
</EuiLink>
{renderTelemetryEnabledOrDisabledText(telemetryService, addBasePath)}
<OptInMessage {...props} />
</EuiTextColor>
<EuiSpacer size="xs" />
</>
);
};
function renderTelemetryEnabledOrDisabledText(
telemetryService: TelemetryService,
addBasePath: (url: string) => string
) {
if (!telemetryService.userCanChangeSettings || !telemetryService.getCanChangeOptInStatus()) {
return null;
}
const isOptedIn = telemetryService.getIsOptedIn();
if (isOptedIn) {
return (
<>
<FormattedMessage
id="telemetry.dataManagementDisableCollection"
defaultMessage=" To stop collection, "
/>
<EuiLink href={addBasePath('management/kibana/settings')}>
<FormattedMessage
id="telemetry.dataManagementDisableCollectionLink"
defaultMessage="disable usage data here."
/>
</EuiLink>
</>
);
} else {
return (
<>
<FormattedMessage
id="telemetry.dataManagementEnableCollection"
defaultMessage=" To start collection, "
/>
<EuiLink href={addBasePath('management/kibana/settings')}>
<FormattedMessage
id="telemetry.dataManagementEnableCollectionLink"
defaultMessage="enable usage data here."
/>
</EuiLink>
</>
);
}
}

View file

@ -12,10 +12,9 @@ import {
notificationServiceMock,
themeServiceMock,
} from '@kbn/core/public/mocks';
import { TelemetryService } from './services/telemetry_service';
import { TelemetryNotifications } from './services/telemetry_notifications/telemetry_notifications';
import { TelemetryPluginStart, TelemetryPluginSetup, TelemetryPluginConfig } from './plugin';
import { TelemetryConstants } from '.';
import type { TelemetryConstants } from '.';
import type { TelemetryPluginStart, TelemetryPluginSetup, TelemetryPluginConfig } from './plugin';
import { TelemetryService, TelemetryNotifications } from './services';
// The following is to be able to access private methods
/* eslint-disable dot-notation */
@ -102,12 +101,13 @@ function createSetupContract(): Setup {
function createStartContract(): Start {
const telemetryService = mockTelemetryService();
const telemetryNotifications = mockTelemetryNotifications({ telemetryService });
const telemetryConstants = mockTelemetryConstants();
const startContract: Start = {
telemetryService,
telemetryNotifications,
telemetryNotifications: {
setOptedInNoticeSeen: jest.fn(),
},
telemetryConstants,
};

View file

@ -171,7 +171,7 @@ export class TelemetryPlugin implements Plugin<TelemetryPluginSetup, TelemetryPl
if (home && !this.config.hidePrivacyStatement) {
home.welcomeScreen.registerOnRendered(() => {
if (this.telemetryService?.userCanChangeSettings) {
this.telemetryNotifications?.setOptedInNoticeSeen();
this.telemetryNotifications?.setOptInStatusNoticeSeen();
}
});
@ -230,14 +230,13 @@ export class TelemetryPlugin implements Plugin<TelemetryPluginSetup, TelemetryPl
this.maybeStartTelemetryPoller();
if (telemetryBanner) {
this.maybeShowOptedInNotificationBanner();
this.maybeShowOptInBanner();
}
});
return {
telemetryService: this.getTelemetryServicePublicApis(),
telemetryNotifications: {
setOptedInNoticeSeen: () => telemetryNotifications.setOptedInNoticeSeen(),
setOptedInNoticeSeen: () => telemetryNotifications.setOptInStatusNoticeSeen(),
},
telemetryConstants,
};
@ -300,19 +299,9 @@ export class TelemetryPlugin implements Plugin<TelemetryPluginSetup, TelemetryPl
if (!this.telemetryNotifications) {
return;
}
const shouldShowBanner = this.telemetryNotifications.shouldShowOptedInNoticeBanner();
const shouldShowBanner = this.telemetryNotifications.shouldShowOptInStatusNoticeBanner();
if (shouldShowBanner) {
this.telemetryNotifications.renderOptedInNoticeBanner();
}
}
private maybeShowOptInBanner() {
if (!this.telemetryNotifications) {
return;
}
const shouldShowBanner = this.telemetryNotifications.shouldShowOptInBanner();
if (shouldShowBanner) {
this.telemetryNotifications.renderOptInBanner();
this.telemetryNotifications.renderOptInStatusNoticeBanner();
}
}

View file

@ -1,34 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { renderOptInBanner } from './render_opt_in_banner';
import { overlayServiceMock } from '@kbn/core/public/mocks';
import { mockTelemetryConstants } from '../../mocks';
describe('renderOptInBanner', () => {
it('adds a banner to banners with priority of 10000', () => {
const bannerID = 'brucer-wayne';
const overlays = overlayServiceMock.createStartContract();
const telemetryConstants = mockTelemetryConstants();
overlays.banners.add.mockReturnValue(bannerID);
const returnedBannerId = renderOptInBanner({
setOptIn: jest.fn(),
overlays,
telemetryConstants,
});
expect(overlays.banners.add).toBeCalledTimes(1);
expect(returnedBannerId).toBe(bannerID);
const bannerConfig = overlays.banners.add.mock.calls[0];
expect(bannerConfig[0]).not.toBe(undefined);
expect(bannerConfig[1]).toBe(10000);
});
});

View file

@ -1,34 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import type { OverlayStart } from '@kbn/core/public';
import { toMountPoint } from '@kbn/kibana-react-plugin/public';
import { withSuspense } from '@kbn/shared-ux-utility';
import type { TelemetryConstants } from '../..';
interface RenderBannerConfig {
overlays: OverlayStart;
setOptIn: (isOptIn: boolean) => Promise<unknown>;
telemetryConstants: TelemetryConstants;
}
export function renderOptInBanner({ setOptIn, overlays, telemetryConstants }: RenderBannerConfig) {
const OptInBannerLazy = withSuspense(
React.lazy(() =>
import('../../components/opt_in_banner').then(({ OptInBanner }) => ({ default: OptInBanner }))
)
);
const mount = toMountPoint(
<OptInBannerLazy onChangeOptInClick={setOptIn} telemetryConstants={telemetryConstants} />
);
const bannerId = overlays.banners.add(mount, 10000);
return bannerId;
}

View file

@ -6,25 +6,27 @@
* Side Public License, v 1.
*/
import { renderOptedInNoticeBanner } from './render_opted_in_notice_banner';
import { renderOptInStatusNoticeBanner } from './render_opt_in_status_notice_banner';
import { overlayServiceMock, httpServiceMock, themeServiceMock } from '@kbn/core/public/mocks';
import { mockTelemetryConstants } from '../../mocks';
import { mockTelemetryConstants, mockTelemetryService } from '../../mocks';
describe('renderOptedInNoticeBanner', () => {
describe('renderOptInStatusNoticeBanner', () => {
it('adds a banner to banners with priority of 10000', () => {
const bannerID = 'brucer-wayne';
const overlays = overlayServiceMock.createStartContract();
const mockHttp = httpServiceMock.createStartContract();
const theme = themeServiceMock.createStartContract();
const telemetryConstants = mockTelemetryConstants();
const telemetryService = mockTelemetryService();
overlays.banners.add.mockReturnValue(bannerID);
const returnedBannerId = renderOptedInNoticeBanner({
const returnedBannerId = renderOptInStatusNoticeBanner({
http: mockHttp,
onSeen: jest.fn(),
overlays,
theme,
telemetryConstants,
telemetryService,
});
expect(overlays.banners.add).toBeCalledTimes(1);

View file

@ -10,6 +10,7 @@ import React from 'react';
import type { HttpStart, OverlayStart, ThemeServiceStart } from '@kbn/core/public';
import { toMountPoint } from '@kbn/kibana-react-plugin/public';
import { withSuspense } from '@kbn/shared-ux-utility';
import { TelemetryService } from '..';
import type { TelemetryConstants } from '../..';
interface RenderBannerConfig {
@ -18,31 +19,37 @@ interface RenderBannerConfig {
theme: ThemeServiceStart;
onSeen: () => void;
telemetryConstants: TelemetryConstants;
telemetryService: TelemetryService;
}
export function renderOptedInNoticeBanner({
export function renderOptInStatusNoticeBanner({
onSeen,
overlays,
http,
theme,
telemetryConstants,
telemetryService,
}: RenderBannerConfig) {
const OptedInNoticeBannerLazy = withSuspense(
React.lazy(() =>
import('../../components/opted_in_notice_banner').then(({ OptedInNoticeBanner }) => ({
default: OptedInNoticeBanner,
}))
import('../../components/opt_in_status_notice_banner').then(
({ OptInStatusNoticeBanner }) => ({
default: OptInStatusNoticeBanner,
})
)
)
);
const mount = toMountPoint(
<OptedInNoticeBannerLazy
onSeenBanner={onSeen}
http={http}
telemetryConstants={telemetryConstants}
telemetryService={telemetryService}
/>,
{ theme$: theme.theme$ }
);
const bannerId = overlays.banners.add(mount, 10000);
const bannerId = overlays.banners.add(mount, 10000);
return bannerId;
}

View file

@ -9,24 +9,6 @@
/* 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';
@ -34,8 +16,8 @@ describe('setOptedInNoticeSeen', () => {
const telemetryService = mockTelemetryService();
telemetryService.setUserHasSeenNotice = jest.fn();
const telemetryNotifications = mockTelemetryNotifications({ telemetryService });
telemetryNotifications['optedInNoticeBannerId'] = bannerId;
await telemetryNotifications.setOptedInNoticeSeen();
telemetryNotifications['optInStatusNoticeBannerId'] = bannerId;
await telemetryNotifications.setOptInStatusNoticeSeen();
expect(telemetryNotifications['overlays'].banners.remove).toBeCalledTimes(1);
expect(telemetryNotifications['overlays'].banners.remove).toBeCalledWith(bannerId);
@ -44,25 +26,38 @@ describe('setOptedInNoticeSeen', () => {
});
describe('shouldShowOptedInNoticeBanner', () => {
it("should return true because a banner hasn't been shown, the notice hasn't been seen and the user has privileges to edit saved objects", () => {
describe(`when the banner isn't visible yet`, () => {
const telemetryService = mockTelemetryService();
telemetryService.getUserShouldSeeOptInNotice = jest.fn().mockReturnValue(true);
const getUserShouldSeeOptInNotice = jest.fn();
telemetryService.getUserShouldSeeOptInNotice = getUserShouldSeeOptInNotice;
const telemetryNotifications = mockTelemetryNotifications({ telemetryService });
expect(telemetryNotifications.shouldShowOptedInNoticeBanner()).toBe(true);
it('should return true when `telemetryService.getUserShouldSeeOptInNotice returns true', () => {
getUserShouldSeeOptInNotice.mockReturnValue(true);
expect(telemetryNotifications.shouldShowOptInStatusNoticeBanner()).toBe(true);
});
it('should return false when `telemetryService.getUserShouldSeeOptInNotice returns false', () => {
getUserShouldSeeOptInNotice.mockReturnValue(false);
expect(telemetryNotifications.shouldShowOptInStatusNoticeBanner()).toBe(false);
});
});
it('should return false because the banner is already on screen', () => {
describe(`when the banner is already visible`, () => {
const telemetryService = mockTelemetryService();
telemetryService.getUserShouldSeeOptInNotice = jest.fn().mockReturnValue(true);
const getUserShouldSeeOptInNotice = jest.fn();
telemetryService.getUserShouldSeeOptInNotice = getUserShouldSeeOptInNotice;
const telemetryNotifications = mockTelemetryNotifications({ telemetryService });
telemetryNotifications['optedInNoticeBannerId'] = 'bruce-banner';
expect(telemetryNotifications.shouldShowOptedInNoticeBanner()).toBe(false);
});
telemetryNotifications['optInStatusNoticeBannerId'] = 'bruce-banner';
it("should return false because the banner has already been seen or the user doesn't have privileges to change saved objects", () => {
const telemetryService = mockTelemetryService();
telemetryService.getUserShouldSeeOptInNotice = jest.fn().mockReturnValue(false);
const telemetryNotifications = mockTelemetryNotifications({ telemetryService });
expect(telemetryNotifications.shouldShowOptedInNoticeBanner()).toBe(false);
it('should return false when `telemetryService.getUserShouldSeeOptInNotice` returns true', () => {
getUserShouldSeeOptInNotice.mockReturnValue(true);
expect(telemetryNotifications.shouldShowOptInStatusNoticeBanner()).toBe(false);
});
it('should return false when `telemetryService.getUserShouldSeeOptInNotice returns false', () => {
getUserShouldSeeOptInNotice.mockReturnValue(false);
expect(telemetryNotifications.shouldShowOptInStatusNoticeBanner()).toBe(false);
});
});
});

View file

@ -6,11 +6,10 @@
* Side Public License, v 1.
*/
import { HttpStart, OverlayStart, ThemeServiceStart } from '@kbn/core/public';
import { renderOptedInNoticeBanner } from './render_opted_in_notice_banner';
import { renderOptInBanner } from './render_opt_in_banner';
import { TelemetryService } from '../telemetry_service';
import { TelemetryConstants } from '../..';
import type { HttpStart, OverlayStart, ThemeServiceStart } from '@kbn/core/public';
import type { TelemetryService } from '../telemetry_service';
import type { TelemetryConstants } from '../..';
import { renderOptInStatusNoticeBanner } from './render_opt_in_status_notice_banner';
interface TelemetryNotificationsConstructor {
http: HttpStart;
@ -29,8 +28,7 @@ export class TelemetryNotifications {
private readonly theme: ThemeServiceStart;
private readonly telemetryConstants: TelemetryConstants;
private readonly telemetryService: TelemetryService;
private optedInNoticeBannerId?: string;
private optInBannerId?: string;
private optInStatusNoticeBannerId?: string;
constructor({
http,
@ -49,70 +47,35 @@ export class TelemetryNotifications {
/**
* Should the opted-in banner be shown to the user?
*/
public shouldShowOptedInNoticeBanner = (): boolean => {
public shouldShowOptInStatusNoticeBanner = (): boolean => {
const userShouldSeeOptInNotice = this.telemetryService.getUserShouldSeeOptInNotice();
const bannerOnScreen = typeof this.optedInNoticeBannerId !== 'undefined';
const bannerOnScreen = typeof this.optInStatusNoticeBannerId !== 'undefined';
return !bannerOnScreen && userShouldSeeOptInNotice;
};
/**
* Renders the banner that claims the cluster is opted-in, and gives the option to opt-out.
*/
public renderOptedInNoticeBanner = (): void => {
const bannerId = renderOptedInNoticeBanner({
public renderOptInStatusNoticeBanner = (): void => {
const bannerId = renderOptInStatusNoticeBanner({
http: this.http,
onSeen: this.setOptedInNoticeSeen,
onSeen: this.setOptInStatusNoticeSeen,
overlays: this.overlays,
theme: this.theme,
telemetryConstants: this.telemetryConstants,
telemetryService: this.telemetryService,
});
this.optedInNoticeBannerId = bannerId;
};
/**
* Should the banner to opt-in be shown to the user?
*/
public shouldShowOptInBanner = (): boolean => {
// Using `config.optIn` instead of the getter `getIsOptedIn()` because the latter only returns boolean, and we want to compare it against `null`.
const isOptedIn = this.telemetryService.config.optIn;
const bannerOnScreen = typeof this.optInBannerId !== 'undefined';
return !bannerOnScreen && isOptedIn === null;
};
/**
* Renders the banner that claims the cluster is opted-out, and gives the option to opt-in.
*/
public renderOptInBanner = (): void => {
const bannerId = renderOptInBanner({
setOptIn: this.onSetOptInClick,
overlays: this.overlays,
telemetryConstants: this.telemetryConstants,
});
this.optInBannerId = bannerId;
};
/**
* Opt-in/out button handler
* @param isOptIn true/false whether the user opts-in/out
*/
private onSetOptInClick = async (isOptIn: boolean) => {
if (this.optInBannerId) {
this.overlays.banners.remove(this.optInBannerId);
this.optInBannerId = undefined;
}
await this.telemetryService.setOptIn(isOptIn);
this.optInStatusNoticeBannerId = bannerId;
};
/**
* Clears the banner and stores the user's dismissal of the banner.
*/
public setOptedInNoticeSeen = async (): Promise<void> => {
if (this.optedInNoticeBannerId) {
this.overlays.banners.remove(this.optedInNoticeBannerId);
this.optedInNoticeBannerId = undefined;
public setOptInStatusNoticeSeen = async (): Promise<void> => {
if (this.optInStatusNoticeBannerId) {
this.overlays.banners.remove(this.optInStatusNoticeBannerId);
this.optInStatusNoticeBannerId = undefined;
}
await this.telemetryService.setUserHasSeenNotice();

View file

@ -232,6 +232,19 @@ describe('TelemetryService', () => {
expect(telemetryService.getUserShouldSeeOptInNotice()).toBe(false);
});
it('should return true when optIn: null even when previously seen', () => {
const telemetryService = mockTelemetryService({
config: {
userCanChangeSettings: true,
telemetryNotifyUserAboutOptInDefault: false,
optIn: null,
},
});
expect(telemetryService.config.userCanChangeSettings).toBe(true);
expect(telemetryService.userCanChangeSettings).toBe(true);
expect(telemetryService.getUserShouldSeeOptInNotice()).toBe(true);
});
it('returns whether the user can update the telemetry config (has SavedObjects access)', () => {
const telemetryService = mockTelemetryService({
config: { userCanChangeSettings: undefined },

View file

@ -7,8 +7,8 @@
*/
import { i18n } from '@kbn/i18n';
import { CoreSetup, CoreStart } from '@kbn/core/public';
import { TelemetryPluginConfig } from '../plugin';
import type { CoreSetup, CoreStart } from '@kbn/core/public';
import type { TelemetryPluginConfig } from '../plugin';
import { getTelemetryChannelEndpoint } from '../../common/telemetry_config/get_telemetry_channel_endpoint';
import type {
UnencryptedTelemetryPayload,
@ -110,15 +110,19 @@ export class TelemetryService {
};
/**
* Returns if an user should be shown the notice about Opt-In/Out telemetry.
* The decision is made based on whether any user has already dismissed the message or
* the user can't actually change the settings (in which case, there's no point on bothering them)
* Returns whether a user should be shown the notice about Opt-In/Out telemetry.
* The decision is made based on:
* 1. The config hidePrivacyStatement is unset
* 2. The user has enough privileges to change the settings
* 3. At least one of the following:
* * It is opted-in, and the user has already been notified at any given point in the deployment's life.
* * It is opted-out, and the user has been notified for this version (excluding patch updates)
*/
public getUserShouldSeeOptInNotice(): boolean {
return (
(!this.config.hidePrivacyStatement &&
this.config.telemetryNotifyUserAboutOptInDefault &&
this.config.userCanChangeSettings) ??
this.config.userCanChangeSettings &&
(this.config.telemetryNotifyUserAboutOptInDefault || this.config.optIn === null)) ??
false
);
}

View file

@ -190,7 +190,7 @@ export class FetcherTask {
} catch (err) {
await this.updateReportFailure(telemetryConfig);
this.logger.warn(`Error sending telemetry usage data. (${err})`);
this.logger.warn(`Error sending usage to Elastic. (${err})`);
}
}

View file

@ -84,7 +84,7 @@ export interface TelemetryPluginSetup {
*/
export interface TelemetryPluginStart {
/**
* Resolves `true` if the user has opted into send Elastic usage data.
* Resolves `true` if sending usage to Elastic is enabled.
* Resolves `false` if the user explicitly opted out of sending usage data to Elastic
* or did not choose to opt-in or out -yet- after a minor or major upgrade (only when previously opted-out).
*

View file

@ -32,6 +32,8 @@
"@kbn/core-saved-objects-server",
"@kbn/core-saved-objects-api-server",
"@kbn/std",
"@kbn/core-http-browser-mocks",
"@kbn/core-http-browser",
],
"exclude": [
"target/**/*",

View file

@ -12,7 +12,7 @@ exports[`TelemetryManagementSectionComponent renders as expected 1`] = `
<EuiTitle>
<h2>
<FormattedMessage
defaultMessage="Usage Data"
defaultMessage="Usage collection"
id="telemetry.usageDataTitle"
values={Object {}}
/>
@ -57,13 +57,13 @@ exports[`TelemetryManagementSectionComponent renders as expected 1`] = `
loading={false}
setting={
Object {
"ariaName": "Provide usage data",
"ariaName": "Share usage with Elastic",
"category": Array [],
"defVal": true,
"description": <React.Fragment>
<p>
<FormattedMessage
defaultMessage="Enabling data usage collection helps us manage and improve our products and services. See our {privacyStatementLink} for more details."
defaultMessage="Enabling usage collection (also known as Telemetry) allows us to learn what our users are most interested in, so we can improve our products and services. Refer to our {privacyStatementLink}."
id="telemetry.telemetryConfigAndLinkDescription"
values={
Object {
@ -113,7 +113,7 @@ exports[`TelemetryManagementSectionComponent renders as expected 1`] = `
/>
</p>
</React.Fragment>,
"displayName": "Provide usage data",
"displayName": "Share usage with Elastic",
"isCustom": true,
"isOverridden": false,
"name": "telemetry:enabled",

View file

@ -27,7 +27,21 @@ import { OptInExampleFlyout } from './opt_in_example_flyout';
type TelemetryService = TelemetryPluginSetup['telemetryService'];
const SEARCH_TERMS = ['telemetry', 'usage', 'data', 'usage data'];
const SEARCH_TERMS: string[] = [
'telemetry',
'usage data', // Keeping this term for BWC
'usage collection',
i18n.translate('telemetry.telemetryConstant', {
defaultMessage: 'telemetry',
}),
i18n.translate('telemetry.usageCollectionConstant', {
defaultMessage: 'usage collection',
}),
].flatMap((term) => {
// Automatically lower-case and split by space the terms from above
const lowerCased = term.toLowerCase();
return [lowerCased, ...lowerCased.split(' ')];
});
interface Props {
telemetryService: TelemetryService;
@ -113,7 +127,10 @@ export class TelemetryManagementSection extends Component<Props, State> {
<EuiSplitPanel.Inner color="subdued">
<EuiTitle>
<h2>
<FormattedMessage id="telemetry.usageDataTitle" defaultMessage="Usage Data" />
<FormattedMessage
id="telemetry.usageDataTitle"
defaultMessage="Usage collection"
/>
</h2>
</EuiTitle>
</EuiSplitPanel.Inner>
@ -126,13 +143,13 @@ export class TelemetryManagementSection extends Component<Props, State> {
type: 'boolean',
name: 'telemetry:enabled',
displayName: i18n.translate('telemetry.provideUsageDataTitle', {
defaultMessage: 'Provide usage data',
defaultMessage: 'Share usage with Elastic',
}),
value: enabled,
description: this.renderDescription(),
defVal: true,
ariaName: i18n.translate('telemetry.provideUsageDataAriaName', {
defaultMessage: 'Provide usage data',
defaultMessage: 'Share usage with Elastic',
}),
requiresPageReload: false,
category: [],
@ -204,8 +221,9 @@ export class TelemetryManagementSection extends Component<Props, State> {
<p>
<FormattedMessage
id="telemetry.telemetryConfigAndLinkDescription"
defaultMessage="Enabling data usage collection helps us manage and improve our products and services.
See our {privacyStatementLink} for more details."
defaultMessage="Enabling usage collection (also known as Telemetry) allows us to learn
what our users are most interested in, so we can improve our products and services.
Refer to our {privacyStatementLink}."
values={{
privacyStatementLink: (
<EuiLink href={docLinks.legal.privacyStatement} target="_blank">
@ -249,10 +267,10 @@ export class TelemetryManagementSection extends Component<Props, State> {
toasts.addSuccess(
newOptInValue
? i18n.translate('telemetry.optInSuccessOn', {
defaultMessage: 'Usage data collection turned on.',
defaultMessage: 'Sharing usage with Elastic is enabled.',
})
: i18n.translate('telemetry.optInSuccessOff', {
defaultMessage: 'Usage data collection turned off.',
defaultMessage: 'No longer sharing usage with Elastic.',
})
);
resolve(true);

View file

@ -392,14 +392,14 @@ document. Examples of interactions include tracking:
- the number of API calls
- the number of times users installed and uninstalled the sample datasets
When using `incrementCounter` for collecting usage data, you need to ensure
When using `incrementCounter` for collecting usage, you need to ensure
that usage collection happens on a best-effort basis and doesn't
negatively affect your plugin or users (see the example):
- Swallow any exceptions thrown from the incrementCounter method and log
a message in development.
- Don't block your application on the incrementCounter method (e.g.
don't use `await`)
- Set the `refresh` option to false to prevent unecessary index refreshes
- Set the `refresh` option to false to prevent unnecessary index refreshes
which slows down Elasticsearch performance

View file

@ -74,8 +74,8 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide
it('shows the banner in the default configuration', async () => {
await PageObjects.common.navigateToApp('home');
expect(await find.existsByCssSelector('[data-test-subj="enable"]')).to.eql(true);
expect(await find.existsByCssSelector('[data-test-subj="disable"]')).to.eql(true);
expect(await find.existsByLinkText('Enable usage collection.')).to.eql(true);
expect(await find.existsByLinkText('Disable usage collection.')).to.eql(false);
});
it('does not show the banner if opted-in', async () => {
@ -86,8 +86,8 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide
.expect(200);
await PageObjects.common.navigateToApp('home');
expect(await find.existsByCssSelector('[data-test-subj="enable"]')).to.eql(false);
expect(await find.existsByCssSelector('[data-test-subj="disable"]')).to.eql(false);
expect(await find.existsByLinkText('Enable usage collection.')).to.eql(false);
expect(await find.existsByLinkText('Disable usage collection.')).to.eql(false);
});
it('does not show the banner if opted-out in this version', async () => {
@ -98,8 +98,8 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide
.expect(200);
await PageObjects.common.navigateToApp('home');
expect(await find.existsByCssSelector('[data-test-subj="enable"]')).to.eql(false);
expect(await find.existsByCssSelector('[data-test-subj="disable"]')).to.eql(false);
expect(await find.existsByLinkText('Enable usage collection.')).to.eql(false);
expect(await find.existsByLinkText('Disable usage collection.')).to.eql(false);
});
it('shows the banner if opted-out in a previous version', async () => {
@ -111,8 +111,8 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide
});
await PageObjects.common.navigateToApp('home');
expect(await find.existsByCssSelector('[data-test-subj="enable"]')).to.eql(true);
expect(await find.existsByCssSelector('[data-test-subj="disable"]')).to.eql(true);
expect(await find.existsByLinkText('Enable usage collection.')).to.eql(true);
expect(await find.existsByLinkText('Disable usage collection.')).to.eql(false);
});
it('does not show the banner if opted-in in a previous version', async () => {
@ -124,8 +124,8 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide
});
await PageObjects.common.navigateToApp('home');
expect(await find.existsByCssSelector('[data-test-subj="enable"]')).to.eql(false);
expect(await find.existsByCssSelector('[data-test-subj="disable"]')).to.eql(false);
expect(await find.existsByLinkText('Enable usage collection.')).to.eql(false);
expect(await find.existsByLinkText('Disable usage collection.')).to.eql(false);
});
});
});

View file

@ -5307,9 +5307,7 @@
"sharedUXPackages.userProfileComponents.userProfilesSelectable.suggestedLabel": "Suggérée",
"telemetry.callout.appliesSettingTitle": "Les modifications apportées à ce paramètre s'appliquent dans {allOfKibanaText} et sont enregistrées automatiquement.",
"telemetry.seeExampleOfClusterDataAndEndpointSecuity": "Découvrez des exemples des {clusterData} et {securityData} que nous collectons.",
"telemetry.telemetryBannerDescription": "Vous souhaitez nous aider à améliorer la Suite Elastic ? La collecte de données d'utilisation est actuellement désactivée. En activant la collecte de données d'utilisation, vous nous aidez à gérer et à améliorer nos produits et nos services. Pour en savoir plus, consultez notre {privacyStatementLink}.",
"telemetry.telemetryConfigAndLinkDescription": "En activant la collecte de données d'utilisation, vous nous aidez à gérer et à améliorer nos produits et nos services. Pour en savoir plus, consultez notre {privacyStatementLink}.",
"telemetry.telemetryOptedInNoticeDescription": "Pour en savoir plus sur la manière dont les données d'utilisation nous aident à gérer et à améliorer nos produits et nos services, consultez notre {privacyStatementLink}. Pour mettre fin à la collecte, {disableLink}.",
"telemetry.callout.appliesSettingTitle.allOfKibanaText": "tout Kibana",
"telemetry.callout.clusterStatisticsDescription": "Voici un exemple des statistiques de cluster de base que nous collecterons. Cela comprend le nombre d'index, de partitions et de nœuds. Cela comprend également des statistiques d'utilisation de niveau élevé, comme l'état d'activation du monitoring.",
"telemetry.callout.clusterStatisticsTitle": "Statistiques du cluster",
@ -5318,11 +5316,8 @@
"telemetry.callout.errorUnprivilegedUserDescription": "Vous ne disposez pas de l'accès requis pour voir les statistiques non chiffrées du cluster.",
"telemetry.callout.errorUnprivilegedUserTitle": "Erreur lors de l'affichage des statistiques du cluster",
"telemetry.clusterData": "données du cluster",
"telemetry.dataManagementDisableCollection": " Pour mettre fin à la collecte, ",
"telemetry.dataManagementDisableCollectionLink": "désactivez les données d'utilisation ici.",
"telemetry.dataManagementDisclaimerPrivacy": "Pour en savoir plus sur la manière dont les données d'utilisation nous aident à gérer et à améliorer nos produits et nos services, consultez notre ",
"telemetry.dataManagementDisclaimerPrivacyLink": "Déclaration de confidentialité.",
"telemetry.dataManagementEnableCollection": " Pour démarrer la collecte, ",
"telemetry.dataManagementEnableCollectionLink": "activez les données d'utilisation ici.",
"telemetry.optInErrorToastText": "Une erreur s'est produite lors de la définition des préférences relatives aux statistiques d'utilisation.",
"telemetry.optInErrorToastTitle": "Erreur",
@ -5334,15 +5329,9 @@
"telemetry.provideUsageDataTitle": "Fournir les données d'utilisation",
"telemetry.readOurUsageDataPrivacyStatementLinkText": "Déclaration de confidentialité",
"telemetry.securityData": "données de sécurité",
"telemetry.telemetryOptedInDisableUsage": "désactivez les données d'utilisation ici",
"telemetry.telemetryOptedInDismissMessage": "Rejeter",
"telemetry.telemetryOptedInNoticeTitle": "Aidez-nous à améliorer la Suite Elastic.",
"telemetry.telemetryOptedInPrivacyStatement": "Déclaration de confidentialité",
"telemetry.usageDataTitle": "Données d'utilisation",
"telemetry.welcomeBanner.disableButtonLabel": "Désactiver",
"telemetry.welcomeBanner.enableButtonLabel": "Activer",
"telemetry.welcomeBanner.telemetryConfigDetailsDescription.telemetryPrivacyStatementLinkText": "Déclaration de confidentialité",
"telemetry.welcomeBanner.title": "Aidez-nous à améliorer la Suite Elastic.",
"timelion.help.functions.aggregate.args.functionHelpText": "L'une des {functions}",
"timelion.help.functions.aggregateHelpText": "Crée une ligne statique sur la base du résultat du traitement de tous les points de la série. Fonctions disponibles : {functions}",
"timelion.help.functions.common.args.fitHelpText": "Algorithme à utiliser pour adapter les séries à l'intervalle et à la période cible. Disponible : {fitFunctions}",

View file

@ -5308,9 +5308,7 @@
"sharedUXPackages.userProfileComponents.userProfilesSelectable.suggestedLabel": "候補",
"telemetry.callout.appliesSettingTitle": "この設定に加えた変更は{allOfKibanaText}に適用され、自動的に保存されます。",
"telemetry.seeExampleOfClusterDataAndEndpointSecuity": "収集される{clusterData}および{securityData}の例を参照してください。",
"telemetry.telemetryBannerDescription": "Elastic Stackの改善にご協力ください使用状況データの収集は現在無効です。使用状況データの収集を有効にすると、製品とサービスを管理して改善することができます。詳細は{privacyStatementLink}をご覧ください。",
"telemetry.telemetryConfigAndLinkDescription": "使用状況データの収集を有効にすると、製品とサービスを管理して改善することができます。詳細は{privacyStatementLink}をご覧ください。",
"telemetry.telemetryOptedInNoticeDescription": "使用状況データがどのように製品とサービスの管理と改善につながるのかに関する詳細については、{privacyStatementLink}を参照してください。収集を停止するには、{disableLink}。",
"telemetry.callout.appliesSettingTitle.allOfKibanaText": "Kibana のすべて",
"telemetry.callout.clusterStatisticsDescription": "これは収集される基本的なクラスター統計の例です。インデックス、シャード、ノードの数が含まれます。監視がオンになっているかどうかなどのハイレベルの使用統計も含まれます。",
"telemetry.callout.clusterStatisticsTitle": "クラスター統計",
@ -5319,11 +5317,8 @@
"telemetry.callout.errorUnprivilegedUserDescription": "暗号化されていないクラスター統計を表示するアクセス権がありません。",
"telemetry.callout.errorUnprivilegedUserTitle": "クラスター統計の表示エラー",
"telemetry.clusterData": "クラスターデータ",
"telemetry.dataManagementDisableCollection": " 収集を停止するには、",
"telemetry.dataManagementDisableCollectionLink": "ここで使用状況データを無効にします。",
"telemetry.dataManagementDisclaimerPrivacy": "使用状況データがどのように製品とサービスの管理と改善につながるのかに関する詳細については ",
"telemetry.dataManagementDisclaimerPrivacyLink": "プライバシーポリシーをご覧ください。",
"telemetry.dataManagementEnableCollection": " 収集を開始するには、",
"telemetry.dataManagementEnableCollectionLink": "ここで使用状況データを有効にします。",
"telemetry.optInErrorToastText": "使用状況統計設定の設定中にエラーが発生しました。",
"telemetry.optInErrorToastTitle": "エラー",
@ -5335,15 +5330,9 @@
"telemetry.provideUsageDataTitle": "使用状況データを提供",
"telemetry.readOurUsageDataPrivacyStatementLinkText": "プライバシーポリシー",
"telemetry.securityData": "セキュリティデータ",
"telemetry.telemetryOptedInDisableUsage": "ここで使用状況データを無効にする",
"telemetry.telemetryOptedInDismissMessage": "閉じる",
"telemetry.telemetryOptedInNoticeTitle": "Elastic Stack の改善にご協力ください",
"telemetry.telemetryOptedInPrivacyStatement": "プライバシーポリシー",
"telemetry.usageDataTitle": "使用データ",
"telemetry.welcomeBanner.disableButtonLabel": "無効にする",
"telemetry.welcomeBanner.enableButtonLabel": "有効にする",
"telemetry.welcomeBanner.telemetryConfigDetailsDescription.telemetryPrivacyStatementLinkText": "プライバシーポリシー",
"telemetry.welcomeBanner.title": "Elastic Stack の改善にご協力ください",
"timelion.help.functions.aggregate.args.functionHelpText": "{functions}の1つ",
"timelion.help.functions.aggregateHelpText": "数列のすべての点の処理結果に基づく線を作成します。利用可能な関数:{functions}",
"timelion.help.functions.common.args.fitHelpText": "ターゲットの期間と間隔に数列を合わせるためのアルゴリズムです。利用可能:{fitFunctions}",

View file

@ -5307,9 +5307,7 @@
"sharedUXPackages.userProfileComponents.userProfilesSelectable.suggestedLabel": "已建议",
"telemetry.callout.appliesSettingTitle": "对此设置的更改将应用到{allOfKibanaText} 且会自动保存。",
"telemetry.seeExampleOfClusterDataAndEndpointSecuity": "查看我们收集的{clusterData}和{securityData}示例。",
"telemetry.telemetryBannerDescription": "想帮助我们改进 Elastic Stack数据使用情况收集当前已禁用。启用使用情况数据收集可帮助我们管理并改善产品和服务。有关详情请参阅我们的{privacyStatementLink}。",
"telemetry.telemetryConfigAndLinkDescription": "启用使用情况数据收集可帮助我们管理并改善产品和服务。有关详情,请参阅我们的{privacyStatementLink}。",
"telemetry.telemetryOptedInNoticeDescription": "要了解使用情况数据如何帮助我们管理和改善产品和服务,请参阅我们的{privacyStatementLink}。要停止收集,{disableLink}。",
"telemetry.callout.appliesSettingTitle.allOfKibanaText": "整个 Kibana",
"telemetry.callout.clusterStatisticsDescription": "这是我们将收集的基本集群统计信息的示例。其包括索引、分片和节点的数目。还包括概括性的使用情况统计信息,例如监测是否打开。",
"telemetry.callout.clusterStatisticsTitle": "集群统计信息",
@ -5318,11 +5316,8 @@
"telemetry.callout.errorUnprivilegedUserDescription": "您无权查看未加密的集群统计信息。",
"telemetry.callout.errorUnprivilegedUserTitle": "显示集群统计信息时出错",
"telemetry.clusterData": "集群数据",
"telemetry.dataManagementDisableCollection": " 要停止收集,",
"telemetry.dataManagementDisableCollectionLink": "请在此禁用使用情况数据。",
"telemetry.dataManagementDisclaimerPrivacy": "要了解使用情况数据如何帮助我们管理和改善产品和服务,请参阅我们的 ",
"telemetry.dataManagementDisclaimerPrivacyLink": "隐私声明。",
"telemetry.dataManagementEnableCollection": " 要启动收集,",
"telemetry.dataManagementEnableCollectionLink": "请在此处启用使用情况数据。",
"telemetry.optInErrorToastText": "尝试设置使用情况统计信息首选项时发生错误。",
"telemetry.optInErrorToastTitle": "错误",
@ -5334,15 +5329,9 @@
"telemetry.provideUsageDataTitle": "提供使用情况数据",
"telemetry.readOurUsageDataPrivacyStatementLinkText": "隐私声明",
"telemetry.securityData": "安全数据",
"telemetry.telemetryOptedInDisableUsage": "请在此禁用使用情况数据",
"telemetry.telemetryOptedInDismissMessage": "关闭",
"telemetry.telemetryOptedInNoticeTitle": "帮助我们改进 Elastic Stack",
"telemetry.telemetryOptedInPrivacyStatement": "隐私声明",
"telemetry.usageDataTitle": "使用情况数据",
"telemetry.welcomeBanner.disableButtonLabel": "禁用",
"telemetry.welcomeBanner.enableButtonLabel": "启用",
"telemetry.welcomeBanner.telemetryConfigDetailsDescription.telemetryPrivacyStatementLinkText": "隐私声明",
"telemetry.welcomeBanner.title": "帮助我们改进 Elastic Stack",
"timelion.help.functions.aggregate.args.functionHelpText": "以下选项之一:{functions}",
"timelion.help.functions.aggregateHelpText": "基于对序列中所有点的处理结果创建静态线。可用函数:{functions}",
"timelion.help.functions.common.args.fitHelpText": "用于将序列拟合到目标时间跨度和时间间隔的算法。可用:{fitFunctions}",