[Telemetry] Fix OptedOut banner (#151084)

Resolves #135107.
This commit is contained in:
Alejandro Fernández Haro 2023-02-14 13:53:55 +01:00 committed by GitHub
parent 9049386f78
commit 58c68c94d4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 203 additions and 43 deletions

View file

@ -74,7 +74,8 @@ export class TelemetryNotifications {
* Should the banner to opt-in be shown to the user? * Should the banner to opt-in be shown to the user?
*/ */
public shouldShowOptInBanner = (): boolean => { public shouldShowOptInBanner = (): boolean => {
const isOptedIn = this.telemetryService.getIsOptedIn(); // 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'; const bannerOnScreen = typeof this.optInBannerId !== 'undefined';
return !bannerOnScreen && isOptedIn === null; return !bannerOnScreen && isOptedIn === null;
}; };

View file

@ -29,17 +29,12 @@ export const getTelemetryOptIn: GetTelemetryOptIn = ({
return configTelemetryOptIn; return configTelemetryOptIn;
} }
if (typeof telemetrySavedObject.enabled !== 'boolean') { // If `enabled` is not available in the SO, fall back to the config value.
return configTelemetryOptIn; const savedOptIn = telemetrySavedObject.enabled ?? configTelemetryOptIn;
}
const savedOptIn = telemetrySavedObject.enabled; // if the stored value is true, return it
// if enabled is true, return it
if (savedOptIn === true) return savedOptIn; if (savedOptIn === true) return savedOptIn;
// TODO: Should we split the logic below into another OptIn getter?
// Additional check if they've already opted out (enabled: false): // Additional check if they've already opted out (enabled: false):
// - if the Kibana version has changed by at least a minor version, // - if the Kibana version has changed by at least a minor version,
// return null to re-prompt. // return null to re-prompt.
@ -49,7 +44,7 @@ export const getTelemetryOptIn: GetTelemetryOptIn = ({
// if the last kibana version isn't set, or is somehow not a string, return null // if the last kibana version isn't set, or is somehow not a string, return null
if (typeof lastKibanaVersion !== 'string') return null; if (typeof lastKibanaVersion !== 'string') return null;
// if version hasn't changed, just return enabled value // if version hasn't changed, just return the stored value
if (lastKibanaVersion === currentKibanaVersion) return savedOptIn; if (lastKibanaVersion === currentKibanaVersion) return savedOptIn;
const lastSemver = parseSemver(lastKibanaVersion); const lastSemver = parseSemver(lastKibanaVersion);
@ -64,7 +59,7 @@ export const getTelemetryOptIn: GetTelemetryOptIn = ({
if (currentSemver.minor > lastSemver.minor) return null; if (currentSemver.minor > lastSemver.minor) return null;
} }
// current version X.Y is not greater than last version X.Y, return enabled // current version X.Y is not greater than last version X.Y, return the stored value
return savedOptIn; return savedOptIn;
}; };

View file

@ -6,33 +6,38 @@
* Side Public License, v 1. * Side Public License, v 1.
*/ */
import { AxiosError } from 'axios';
import { FtrProviderContext } from '../../ftr_provider_context'; import { FtrProviderContext } from '../../ftr_provider_context';
export default function optInTest({ getService }: FtrProviderContext) { const TELEMETRY_SO_TYPE = 'telemetry';
const client = getService('es'); const TELEMETRY_SO_ID = 'telemetry';
export default function telemetryConfigTest({ getService }: FtrProviderContext) {
const kbnClient = getService('kibanaServer');
const supertest = getService('supertest'); const supertest = getService('supertest');
describe('/api/telemetry/v2/config API Telemetry config', () => { describe('/api/telemetry/v2/config API Telemetry config', () => {
before(async () => { before(async () => {
await client.delete( try {
{ await kbnClient.savedObjects.delete({ type: TELEMETRY_SO_TYPE, id: TELEMETRY_SO_ID });
index: '.kibana', } catch (err) {
id: 'telemetry:telemetry', const is404Error = err instanceof AxiosError && err.response?.status === 404;
}, if (!is404Error) {
{ ignore: [404] } throw err;
); }
}
}); });
it('GET should get the default config', async () => { it('GET should get the default config', async () => {
await supertest.get('/api/telemetry/v2/config').set('kbn-xsrf', 'xxx').expect(200, { await supertest.get('/api/telemetry/v2/config').set('kbn-xsrf', 'xxx').expect(200, {
allowChangingOptInStatus: true, allowChangingOptInStatus: true,
optIn: false, // the config.js for this FTR sets it to `false` optIn: null, // the config.js for this FTR sets it to `false`, we are bound to ask again.
sendUsageFrom: 'server', sendUsageFrom: 'server',
telemetryNotifyUserAboutOptInDefault: false, // it's not opted-in, so we don't notify about opt-in?? telemetryNotifyUserAboutOptInDefault: false, // it's not opted-in by default (that's what this flag is about)
}); });
}); });
it('GET should get when opted-in', async () => { it('GET should get `true` when opted-in', async () => {
// Opt-in // Opt-in
await supertest await supertest
.post('/api/telemetry/v2/optIn') .post('/api/telemetry/v2/optIn')
@ -44,9 +49,79 @@ export default function optInTest({ getService }: FtrProviderContext) {
allowChangingOptInStatus: true, allowChangingOptInStatus: true,
optIn: true, optIn: true,
sendUsageFrom: 'server', sendUsageFrom: 'server',
// it's not opted-in (in the YAML config) despite being opted-in via API/UI, and we still say false??
telemetryNotifyUserAboutOptInDefault: false, telemetryNotifyUserAboutOptInDefault: false,
}); });
}); });
it('GET should get false when opted-out', async () => {
// Opt-in
await supertest
.post('/api/telemetry/v2/optIn')
.set('kbn-xsrf', 'xxx')
.send({ enabled: false })
.expect(200);
await supertest.get('/api/telemetry/v2/config').set('kbn-xsrf', 'xxx').expect(200, {
allowChangingOptInStatus: true,
optIn: false,
sendUsageFrom: 'server',
telemetryNotifyUserAboutOptInDefault: false,
});
});
describe('From a previous version', function () {
this.tags(['skipCloud']);
// Get current values
let attributes: Record<string, unknown>;
let currentVersion: string;
let previousMinor: string;
before(async () => {
[{ attributes }, currentVersion] = await Promise.all([
kbnClient.savedObjects.get({ type: TELEMETRY_SO_TYPE, id: TELEMETRY_SO_ID }),
kbnClient.version.get(),
]);
const [major, minor, patch] = currentVersion.match(/^(\d+)\.(\d+)\.(\d+)/)!.map(parseInt);
previousMinor = `${minor === 0 ? major - 1 : major}.${
minor === 0 ? minor : minor - 1
}.${patch}`;
});
it('GET should get `true` when opted-in in the current version', async () => {
// Opt-in from a previous version
await kbnClient.savedObjects.create({
overwrite: true,
type: TELEMETRY_SO_TYPE,
id: TELEMETRY_SO_ID,
attributes: { ...attributes, enabled: true, lastVersionChecked: previousMinor },
});
await supertest.get('/api/telemetry/v2/config').set('kbn-xsrf', 'xxx').expect(200, {
allowChangingOptInStatus: true,
optIn: true,
sendUsageFrom: 'server',
telemetryNotifyUserAboutOptInDefault: false,
});
});
it('GET should get `null` when opted-out in a previous version', async () => {
// Opt-out from previous version
await kbnClient.savedObjects.create({
overwrite: true,
type: TELEMETRY_SO_TYPE,
id: TELEMETRY_SO_ID,
attributes: { ...attributes, enabled: false, lastVersionChecked: previousMinor },
});
await supertest.get('/api/telemetry/v2/config').set('kbn-xsrf', 'xxx').expect(200, {
allowChangingOptInStatus: true,
optIn: null,
sendUsageFrom: 'server',
telemetryNotifyUserAboutOptInDefault: false,
});
});
});
}); });
} }

View file

@ -53,6 +53,8 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
'--corePluginDeprecations.noLongerUsed=still_using', '--corePluginDeprecations.noLongerUsed=still_using',
// for testing set buffer duration to 0 to immediately flush counters into saved objects. // for testing set buffer duration to 0 to immediately flush counters into saved objects.
'--usageCollection.usageCounters.bufferDuration=0', '--usageCollection.usageCounters.bufferDuration=0',
// We want to test when the banner is shown
'--telemetry.banner=true',
// explicitly enable the cloud integration plugins to validate the rendered config keys // explicitly enable the cloud integration plugins to validate the rendered config keys
'--xpack.cloud_integrations.chat.enabled=true', '--xpack.cloud_integrations.chat.enabled=true',
'--xpack.cloud_integrations.chat.chatURL=a_string', '--xpack.cloud_integrations.chat.chatURL=a_string',

View file

@ -10,36 +10,123 @@ import expect from '@kbn/expect';
import { KBN_SCREENSHOT_MODE_ENABLED_KEY } from '@kbn/screenshot-mode-plugin/public'; import { KBN_SCREENSHOT_MODE_ENABLED_KEY } from '@kbn/screenshot-mode-plugin/public';
import { PluginFunctionalProviderContext } from '../../services'; import { PluginFunctionalProviderContext } from '../../services';
const TELEMETRY_SO_TYPE = 'telemetry';
const TELEMETRY_SO_ID = 'telemetry';
export default function ({ getService, getPageObjects }: PluginFunctionalProviderContext) { export default function ({ getService, getPageObjects }: PluginFunctionalProviderContext) {
const kbnClient = getService('kibanaServer');
const browser = getService('browser'); const browser = getService('browser');
const find = getService('find');
const supertest = getService('supertest');
const PageObjects = getPageObjects(['common']); const PageObjects = getPageObjects(['common']);
describe('Telemetry service', () => { describe('Telemetry service', () => {
const checkCanSendTelemetry = (): Promise<boolean> => { describe('Screenshot mode', () => {
return browser.executeAsync<boolean>((cb) => { const checkCanSendTelemetry = (): Promise<boolean> => {
(window as unknown as Record<string, () => Promise<boolean>>) return browser.executeAsync<boolean>((cb) => {
._checkCanSendTelemetry() (window as unknown as Record<string, () => Promise<boolean>>)
.then(cb); ._checkCanSendTelemetry()
}); .then(cb);
}; });
};
after(async () => { after(async () => {
await browser.removeLocalStorageItem(KBN_SCREENSHOT_MODE_ENABLED_KEY); await browser.removeLocalStorageItem(KBN_SCREENSHOT_MODE_ENABLED_KEY);
await browser.executeAsync<void>((cb) => { await browser.executeAsync<void>((cb) => {
(window as unknown as Record<string, () => Promise<boolean>>) (window as unknown as Record<string, () => Promise<boolean>>)
._resetTelemetry() ._resetTelemetry()
.then(() => cb()); .then(() => cb());
});
});
it('detects that telemetry cannot be sent in screenshot mode', async () => {
await PageObjects.common.navigateToApp('home');
expect(await checkCanSendTelemetry()).to.be(true);
await browser.setLocalStorageItem(KBN_SCREENSHOT_MODE_ENABLED_KEY, 'true');
await PageObjects.common.navigateToApp('home');
expect(await checkCanSendTelemetry()).to.be(false);
}); });
}); });
it('detects that telemetry cannot be sent in screenshot mode', async () => { describe('Opt-in/out banners', function () {
await PageObjects.common.navigateToApp('home'); this.tags(['skipCloud']);
expect(await checkCanSendTelemetry()).to.be(true);
await browser.setLocalStorageItem(KBN_SCREENSHOT_MODE_ENABLED_KEY, 'true'); // Get current values
await PageObjects.common.navigateToApp('home'); let attributes: Record<string, unknown>;
let currentVersion: string;
let previousMinor: string;
expect(await checkCanSendTelemetry()).to.be(false); before(async () => {
[{ attributes }, currentVersion] = await Promise.all([
kbnClient.savedObjects.get({ type: TELEMETRY_SO_TYPE, id: TELEMETRY_SO_ID }),
kbnClient.version.get(),
]);
const [major, minor, patch] = currentVersion.match(/^(\d+)\.(\d+)\.(\d+)/)!.map(parseInt);
previousMinor = `${minor === 0 ? major - 1 : major}.${
minor === 0 ? minor : minor - 1
}.${patch}`;
await kbnClient.savedObjects.delete({ type: TELEMETRY_SO_TYPE, id: TELEMETRY_SO_ID });
});
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);
});
it('does not show the banner if opted-in', async () => {
await supertest
.post('/api/telemetry/v2/optIn')
.set('kbn-xsrf', 'xxx')
.send({ enabled: true })
.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);
});
it('does not show the banner if opted-out in this version', async () => {
await supertest
.post('/api/telemetry/v2/optIn')
.set('kbn-xsrf', 'xxx')
.send({ enabled: false })
.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);
});
it('shows the banner if opted-out in a previous version', async () => {
await kbnClient.savedObjects.create({
overwrite: true,
type: TELEMETRY_SO_TYPE,
id: TELEMETRY_SO_ID,
attributes: { ...attributes, enabled: false, lastVersionChecked: previousMinor },
});
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);
});
it('does not show the banner if opted-in in a previous version', async () => {
await kbnClient.savedObjects.create({
overwrite: true,
type: TELEMETRY_SO_TYPE,
id: TELEMETRY_SO_ID,
attributes: { ...attributes, enabled: true, lastVersionChecked: previousMinor },
});
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);
});
}); });
}); });
} }