parent
34906a6f59
commit
cfae29aeed
23 changed files with 492 additions and 94 deletions
|
@ -191,6 +191,7 @@ kibana_vars=(
|
|||
telemetry.enabled
|
||||
telemetry.optIn
|
||||
telemetry.optInStatusUrl
|
||||
telemetry.sendUsageTo
|
||||
telemetry.sendUsageFrom
|
||||
tilemap.options.attribution
|
||||
tilemap.options.maxZoom
|
||||
|
|
|
@ -49,3 +49,17 @@ export const PRIVACY_STATEMENT_URL = `https://www.elastic.co/legal/privacy-state
|
|||
* The endpoint version when hitting the remote telemetry service
|
||||
*/
|
||||
export const ENDPOINT_VERSION = 'v2';
|
||||
|
||||
/**
|
||||
* The telemetry endpoints for the remote telemetry service.
|
||||
*/
|
||||
export const TELEMETRY_ENDPOINT = {
|
||||
MAIN_CHANNEL: {
|
||||
PROD: `https://telemetry.elastic.co/xpack/${ENDPOINT_VERSION}/send`,
|
||||
STAGING: `https://telemetry-staging.elastic.co/xpack/${ENDPOINT_VERSION}/send`,
|
||||
},
|
||||
OPT_IN_STATUS_CHANNEL: {
|
||||
PROD: `https://telemetry.elastic.co/opt_in_status/${ENDPOINT_VERSION}/send`,
|
||||
STAGING: `https://telemetry-staging.elastic.co/opt_in_status/${ENDPOINT_VERSION}/send`,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* 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 { getTelemetryChannelEndpoint } from './get_telemetry_channel_endpoint';
|
||||
import { TELEMETRY_ENDPOINT } from '../constants';
|
||||
|
||||
describe('getTelemetryChannelEndpoint', () => {
|
||||
it('throws on unknown env', () => {
|
||||
expect(() =>
|
||||
// @ts-expect-error
|
||||
getTelemetryChannelEndpoint({ env: 'ANY', channelName: 'main' })
|
||||
).toThrowErrorMatchingInlineSnapshot(`"Unknown telemetry endpoint env ANY."`);
|
||||
});
|
||||
|
||||
it('throws on unknown channelName', () => {
|
||||
expect(() =>
|
||||
// @ts-expect-error
|
||||
getTelemetryChannelEndpoint({ env: 'prod', channelName: 'ANY' })
|
||||
).toThrowErrorMatchingInlineSnapshot(`"Unknown telemetry channel ANY."`);
|
||||
});
|
||||
|
||||
describe('main channel', () => {
|
||||
it('returns correct prod endpoint', () => {
|
||||
const endpoint = getTelemetryChannelEndpoint({ env: 'prod', channelName: 'main' });
|
||||
expect(endpoint).toBe(TELEMETRY_ENDPOINT.MAIN_CHANNEL.PROD);
|
||||
});
|
||||
it('returns correct staging endpoint', () => {
|
||||
const endpoint = getTelemetryChannelEndpoint({ env: 'staging', channelName: 'main' });
|
||||
expect(endpoint).toBe(TELEMETRY_ENDPOINT.MAIN_CHANNEL.STAGING);
|
||||
});
|
||||
});
|
||||
|
||||
describe('optInStatus channel', () => {
|
||||
it('returns correct prod endpoint', () => {
|
||||
const endpoint = getTelemetryChannelEndpoint({ env: 'prod', channelName: 'optInStatus' });
|
||||
expect(endpoint).toBe(TELEMETRY_ENDPOINT.OPT_IN_STATUS_CHANNEL.PROD);
|
||||
});
|
||||
it('returns correct staging endpoint', () => {
|
||||
const endpoint = getTelemetryChannelEndpoint({ env: 'staging', channelName: 'optInStatus' });
|
||||
expect(endpoint).toBe(TELEMETRY_ENDPOINT.OPT_IN_STATUS_CHANNEL.STAGING);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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 { TELEMETRY_ENDPOINT } from '../constants';
|
||||
|
||||
export interface GetTelemetryChannelEndpointConfig {
|
||||
channelName: 'main' | 'optInStatus';
|
||||
env: 'staging' | 'prod';
|
||||
}
|
||||
|
||||
export function getTelemetryChannelEndpoint({
|
||||
channelName,
|
||||
env,
|
||||
}: GetTelemetryChannelEndpointConfig): string {
|
||||
if (env !== 'staging' && env !== 'prod') {
|
||||
throw new Error(`Unknown telemetry endpoint env ${env}.`);
|
||||
}
|
||||
|
||||
const endpointEnv = env === 'staging' ? 'STAGING' : 'PROD';
|
||||
|
||||
switch (channelName) {
|
||||
case 'main':
|
||||
return TELEMETRY_ENDPOINT.MAIN_CHANNEL[endpointEnv];
|
||||
case 'optInStatus':
|
||||
return TELEMETRY_ENDPOINT.OPT_IN_STATUS_CHANNEL[endpointEnv];
|
||||
default:
|
||||
throw new Error(`Unknown telemetry channel ${channelName}.`);
|
||||
}
|
||||
}
|
|
@ -11,3 +11,5 @@ export { getTelemetrySendUsageFrom } from './get_telemetry_send_usage_from';
|
|||
export { getTelemetryAllowChangingOptInStatus } from './get_telemetry_allow_changing_opt_in_status';
|
||||
export { getTelemetryFailureDetails } from './get_telemetry_failure_details';
|
||||
export type { TelemetryFailureDetails } from './get_telemetry_failure_details';
|
||||
export { getTelemetryChannelEndpoint } from './get_telemetry_channel_endpoint';
|
||||
export type { GetTelemetryChannelEndpointConfig } from './get_telemetry_channel_endpoint';
|
||||
|
|
|
@ -34,8 +34,7 @@ export function mockTelemetryService({
|
|||
}: TelemetryServiceMockOptions = {}) {
|
||||
const config = {
|
||||
enabled: true,
|
||||
url: 'http://localhost',
|
||||
optInStatusUrl: 'http://localhost',
|
||||
sendUsageTo: 'staging' as const,
|
||||
sendUsageFrom: 'browser' as const,
|
||||
optIn: true,
|
||||
banner: true,
|
||||
|
|
|
@ -90,16 +90,14 @@ interface TelemetryPluginSetupDependencies {
|
|||
export interface TelemetryPluginConfig {
|
||||
/** Is the plugin enabled? **/
|
||||
enabled: boolean;
|
||||
/** Remote telemetry service's URL **/
|
||||
url: string;
|
||||
/** The banner is expected to be shown when needed **/
|
||||
banner: boolean;
|
||||
/** Does the cluster allow changing the opt-in/out status via the UI? **/
|
||||
allowChangingOptInStatus: boolean;
|
||||
/** Is the cluster opted-in? **/
|
||||
optIn: boolean | null;
|
||||
/** Opt-in/out notification URL **/
|
||||
optInStatusUrl: string;
|
||||
/** Specify if telemetry should send usage to the prod or staging remote telemetry service **/
|
||||
sendUsageTo: 'prod' | 'staging';
|
||||
/** Should the telemetry payloads be sent from the server or the browser? **/
|
||||
sendUsageFrom: 'browser' | 'server';
|
||||
/** Should notify the user about the opt-in status? **/
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
/* eslint-disable dot-notation */
|
||||
|
||||
import { mockTelemetryService } from '../mocks';
|
||||
|
||||
import { TELEMETRY_ENDPOINT } from '../../common/constants';
|
||||
describe('TelemetryService', () => {
|
||||
describe('fetchTelemetry', () => {
|
||||
it('calls expected URL with 20 minutes - now', async () => {
|
||||
|
@ -137,13 +137,42 @@ describe('TelemetryService', () => {
|
|||
});
|
||||
|
||||
describe('getTelemetryUrl', () => {
|
||||
it('should return the config.url parameter', async () => {
|
||||
const url = 'http://test.com';
|
||||
it('should return staging endpoint when sendUsageTo is set to staging', async () => {
|
||||
const telemetryService = mockTelemetryService({
|
||||
config: { url },
|
||||
config: { sendUsageTo: 'staging' },
|
||||
});
|
||||
|
||||
expect(telemetryService.getTelemetryUrl()).toBe(url);
|
||||
expect(telemetryService.getTelemetryUrl()).toBe(TELEMETRY_ENDPOINT.MAIN_CHANNEL.STAGING);
|
||||
});
|
||||
|
||||
it('should return prod endpoint when sendUsageTo is set to prod', async () => {
|
||||
const telemetryService = mockTelemetryService({
|
||||
config: { sendUsageTo: 'prod' },
|
||||
});
|
||||
|
||||
expect(telemetryService.getTelemetryUrl()).toBe(TELEMETRY_ENDPOINT.MAIN_CHANNEL.PROD);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getOptInStatusUrl', () => {
|
||||
it('should return staging endpoint when sendUsageTo is set to staging', async () => {
|
||||
const telemetryService = mockTelemetryService({
|
||||
config: { sendUsageTo: 'staging' },
|
||||
});
|
||||
|
||||
expect(telemetryService.getOptInStatusUrl()).toBe(
|
||||
TELEMETRY_ENDPOINT.OPT_IN_STATUS_CHANNEL.STAGING
|
||||
);
|
||||
});
|
||||
|
||||
it('should return prod endpoint when sendUsageTo is set to prod', async () => {
|
||||
const telemetryService = mockTelemetryService({
|
||||
config: { sendUsageTo: 'prod' },
|
||||
});
|
||||
|
||||
expect(telemetryService.getOptInStatusUrl()).toBe(
|
||||
TELEMETRY_ENDPOINT.OPT_IN_STATUS_CHANNEL.PROD
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { CoreStart } from 'kibana/public';
|
||||
import { TelemetryPluginConfig } from '../plugin';
|
||||
import { getTelemetryChannelEndpoint } from '../../common/telemetry_config';
|
||||
|
||||
interface TelemetryServiceConstructor {
|
||||
config: TelemetryPluginConfig;
|
||||
|
@ -93,14 +94,14 @@ export class TelemetryService {
|
|||
|
||||
/** Retrieve the opt-in/out notification URL **/
|
||||
public getOptInStatusUrl = () => {
|
||||
const telemetryOptInStatusUrl = this.config.optInStatusUrl;
|
||||
return telemetryOptInStatusUrl;
|
||||
const { sendUsageTo } = this.config;
|
||||
return getTelemetryChannelEndpoint({ channelName: 'optInStatus', env: sendUsageTo });
|
||||
};
|
||||
|
||||
/** Retrieve the URL to report telemetry **/
|
||||
public getTelemetryUrl = () => {
|
||||
const telemetryUrl = this.config.url;
|
||||
return telemetryUrl;
|
||||
const { sendUsageTo } = this.config;
|
||||
return getTelemetryChannelEndpoint({ channelName: 'main', env: sendUsageTo });
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -6,11 +6,18 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { schema, TypeOf } from '@kbn/config-schema';
|
||||
import { schema, TypeOf, Type } from '@kbn/config-schema';
|
||||
import { getConfigPath } from '@kbn/utils';
|
||||
import { ENDPOINT_VERSION } from '../common/constants';
|
||||
import { PluginConfigDescriptor } from 'kibana/server';
|
||||
import { TELEMETRY_ENDPOINT } from '../../common/constants';
|
||||
import { deprecateEndpointConfigs } from './deprecations';
|
||||
|
||||
export const configSchema = schema.object({
|
||||
const clusterEnvSchema: [Type<'prod'>, Type<'staging'>] = [
|
||||
schema.literal('prod'),
|
||||
schema.literal('staging'),
|
||||
];
|
||||
|
||||
const configSchema = schema.object({
|
||||
enabled: schema.boolean({ defaultValue: true }),
|
||||
allowChangingOptInStatus: schema.boolean({ defaultValue: true }),
|
||||
optIn: schema.conditional(
|
||||
|
@ -23,24 +30,38 @@ export const configSchema = schema.object({
|
|||
// `config` is used internally and not intended to be set
|
||||
config: schema.string({ defaultValue: getConfigPath() }),
|
||||
banner: schema.boolean({ defaultValue: true }),
|
||||
sendUsageTo: schema.conditional(
|
||||
schema.contextRef('dist'),
|
||||
schema.literal(false), // Point to staging if it's not a distributable release
|
||||
schema.oneOf(clusterEnvSchema, { defaultValue: 'staging' }),
|
||||
schema.oneOf(clusterEnvSchema, { defaultValue: 'prod' })
|
||||
),
|
||||
/**
|
||||
* REMOVE IN 8.0 - INTERNAL CONFIG DEPRECATED IN 7.15
|
||||
* REPLACED WITH `telemetry.sendUsageTo: staging | prod`
|
||||
*/
|
||||
url: schema.conditional(
|
||||
schema.contextRef('dist'),
|
||||
schema.literal(false), // Point to staging if it's not a distributable release
|
||||
schema.string({
|
||||
defaultValue: `https://telemetry-staging.elastic.co/xpack/${ENDPOINT_VERSION}/send`,
|
||||
defaultValue: TELEMETRY_ENDPOINT.MAIN_CHANNEL.STAGING,
|
||||
}),
|
||||
schema.string({
|
||||
defaultValue: `https://telemetry.elastic.co/xpack/${ENDPOINT_VERSION}/send`,
|
||||
defaultValue: TELEMETRY_ENDPOINT.MAIN_CHANNEL.PROD,
|
||||
})
|
||||
),
|
||||
/**
|
||||
* REMOVE IN 8.0 - INTERNAL CONFIG DEPRECATED IN 7.15
|
||||
* REPLACED WITH `telemetry.sendUsageTo: staging | prod`
|
||||
*/
|
||||
optInStatusUrl: schema.conditional(
|
||||
schema.contextRef('dist'),
|
||||
schema.literal(false), // Point to staging if it's not a distributable release
|
||||
schema.string({
|
||||
defaultValue: `https://telemetry-staging.elastic.co/opt_in_status/${ENDPOINT_VERSION}/send`,
|
||||
defaultValue: TELEMETRY_ENDPOINT.OPT_IN_STATUS_CHANNEL.STAGING,
|
||||
}),
|
||||
schema.string({
|
||||
defaultValue: `https://telemetry.elastic.co/opt_in_status/${ENDPOINT_VERSION}/send`,
|
||||
defaultValue: TELEMETRY_ENDPOINT.OPT_IN_STATUS_CHANNEL.PROD,
|
||||
})
|
||||
),
|
||||
sendUsageFrom: schema.oneOf([schema.literal('server'), schema.literal('browser')], {
|
||||
|
@ -49,3 +70,16 @@ export const configSchema = schema.object({
|
|||
});
|
||||
|
||||
export type TelemetryConfigType = TypeOf<typeof configSchema>;
|
||||
|
||||
export const config: PluginConfigDescriptor<TelemetryConfigType> = {
|
||||
schema: configSchema,
|
||||
exposeToBrowser: {
|
||||
enabled: true,
|
||||
banner: true,
|
||||
allowChangingOptInStatus: true,
|
||||
optIn: true,
|
||||
sendUsageFrom: true,
|
||||
sendUsageTo: true,
|
||||
},
|
||||
deprecations: () => [deprecateEndpointConfigs],
|
||||
};
|
164
src/plugins/telemetry/server/config/deprecations.test.ts
Normal file
164
src/plugins/telemetry/server/config/deprecations.test.ts
Normal file
|
@ -0,0 +1,164 @@
|
|||
/*
|
||||
* 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 { deprecateEndpointConfigs } from './deprecations';
|
||||
import type { TelemetryConfigType } from './config';
|
||||
import { TELEMETRY_ENDPOINT } from '../../common/constants';
|
||||
describe('deprecateEndpointConfigs', () => {
|
||||
const fromPath = 'telemetry';
|
||||
const mockAddDeprecation = jest.fn();
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
function createMockRawConfig(telemetryConfig?: Partial<TelemetryConfigType>) {
|
||||
return {
|
||||
elasticsearch: { username: 'kibana_system', password: 'changeme' },
|
||||
plugins: { paths: [] },
|
||||
server: { port: 5603, basePath: '/hln', rewriteBasePath: true },
|
||||
logging: { json: false },
|
||||
...(telemetryConfig ? { telemetry: telemetryConfig } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
it('returns void if telemetry.* config is not set', () => {
|
||||
const rawConfig = createMockRawConfig();
|
||||
const result = deprecateEndpointConfigs(rawConfig, fromPath, mockAddDeprecation);
|
||||
expect(result).toBe(undefined);
|
||||
});
|
||||
|
||||
it('sets "telemetryConfig.sendUsageTo: staging" if "telemetry.url" uses the staging endpoint', () => {
|
||||
const rawConfig = createMockRawConfig({
|
||||
url: TELEMETRY_ENDPOINT.MAIN_CHANNEL.STAGING,
|
||||
});
|
||||
const result = deprecateEndpointConfigs(rawConfig, fromPath, mockAddDeprecation);
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"set": Array [
|
||||
Object {
|
||||
"path": "telemetry.sendUsageTo",
|
||||
"value": "staging",
|
||||
},
|
||||
],
|
||||
"unset": Array [
|
||||
Object {
|
||||
"path": "telemetry.url",
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('sets "telemetryConfig.sendUsageTo: prod" if "telemetry.url" uses the non-staging endpoint', () => {
|
||||
const rawConfig = createMockRawConfig({
|
||||
url: 'random-endpoint',
|
||||
});
|
||||
const result = deprecateEndpointConfigs(rawConfig, fromPath, mockAddDeprecation);
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"set": Array [
|
||||
Object {
|
||||
"path": "telemetry.sendUsageTo",
|
||||
"value": "prod",
|
||||
},
|
||||
],
|
||||
"unset": Array [
|
||||
Object {
|
||||
"path": "telemetry.url",
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('sets "telemetryConfig.sendUsageTo: staging" if "telemetry.optInStatusUrl" uses the staging endpoint', () => {
|
||||
const rawConfig = createMockRawConfig({
|
||||
optInStatusUrl: TELEMETRY_ENDPOINT.MAIN_CHANNEL.STAGING,
|
||||
});
|
||||
const result = deprecateEndpointConfigs(rawConfig, fromPath, mockAddDeprecation);
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"set": Array [
|
||||
Object {
|
||||
"path": "telemetry.sendUsageTo",
|
||||
"value": "staging",
|
||||
},
|
||||
],
|
||||
"unset": Array [
|
||||
Object {
|
||||
"path": "telemetry.optInStatusUrl",
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('sets "telemetryConfig.sendUsageTo: prod" if "telemetry.optInStatusUrl" uses the non-staging endpoint', () => {
|
||||
const rawConfig = createMockRawConfig({
|
||||
optInStatusUrl: 'random-endpoint',
|
||||
});
|
||||
const result = deprecateEndpointConfigs(rawConfig, fromPath, mockAddDeprecation);
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"set": Array [
|
||||
Object {
|
||||
"path": "telemetry.sendUsageTo",
|
||||
"value": "prod",
|
||||
},
|
||||
],
|
||||
"unset": Array [
|
||||
Object {
|
||||
"path": "telemetry.optInStatusUrl",
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('registers deprecation when "telemetry.url" is set', () => {
|
||||
const rawConfig = createMockRawConfig({
|
||||
url: TELEMETRY_ENDPOINT.MAIN_CHANNEL.PROD,
|
||||
});
|
||||
deprecateEndpointConfigs(rawConfig, fromPath, mockAddDeprecation);
|
||||
expect(mockAddDeprecation).toBeCalledTimes(1);
|
||||
expect(mockAddDeprecation.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"correctiveActions": Object {
|
||||
"manualSteps": Array [
|
||||
"Remove \\"telemetry.url\\" from the Kibana configuration.",
|
||||
"To send usage to the staging endpoint add \\"telemetry.sendUsageTo: staging\\" to the Kibana configuration.",
|
||||
],
|
||||
},
|
||||
"message": "\\"telemetry.url\\" has been deprecated. Set \\"telemetry.sendUsageTo: staging\\" to the Kibana configurations to send usage to the staging endpoint.",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('registers deprecation when "telemetry.optInStatusUrl" is set', () => {
|
||||
const rawConfig = createMockRawConfig({
|
||||
optInStatusUrl: 'random-endpoint',
|
||||
});
|
||||
deprecateEndpointConfigs(rawConfig, fromPath, mockAddDeprecation);
|
||||
expect(mockAddDeprecation).toBeCalledTimes(1);
|
||||
expect(mockAddDeprecation.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"correctiveActions": Object {
|
||||
"manualSteps": Array [
|
||||
"Remove \\"telemetry.optInStatusUrl\\" from the Kibana configuration.",
|
||||
"To send usage to the staging endpoint add \\"telemetry.sendUsageTo: staging\\" to the Kibana configuration.",
|
||||
],
|
||||
},
|
||||
"message": "\\"telemetry.optInStatusUrl\\" has been deprecated. Set \\"telemetry.sendUsageTo: staging\\" to the Kibana configurations to send usage to the staging endpoint.",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
63
src/plugins/telemetry/server/config/deprecations.ts
Normal file
63
src/plugins/telemetry/server/config/deprecations.ts
Normal file
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import type { ConfigDeprecation } from 'kibana/server';
|
||||
import type { TelemetryConfigType } from './config';
|
||||
|
||||
export const deprecateEndpointConfigs: ConfigDeprecation = (
|
||||
rawConfig,
|
||||
fromPath,
|
||||
addDeprecation
|
||||
) => {
|
||||
const telemetryConfig: TelemetryConfigType = rawConfig[fromPath];
|
||||
if (!telemetryConfig) {
|
||||
return;
|
||||
}
|
||||
|
||||
const unset: Array<{ path: string }> = [];
|
||||
const endpointConfigPaths = ['url', 'optInStatusUrl'] as const;
|
||||
let useStaging = telemetryConfig.sendUsageTo === 'staging' ? true : false;
|
||||
|
||||
for (const configPath of endpointConfigPaths) {
|
||||
const configValue = telemetryConfig[configPath];
|
||||
const fullConfigPath = `telemetry.${configPath}`;
|
||||
if (typeof configValue !== 'undefined') {
|
||||
unset.push({ path: fullConfigPath });
|
||||
|
||||
if (/telemetry-staging\.elastic\.co/i.test(configValue)) {
|
||||
useStaging = true;
|
||||
}
|
||||
|
||||
addDeprecation({
|
||||
message: i18n.translate('telemetry.endpointConfigs.deprecationMessage', {
|
||||
defaultMessage:
|
||||
'"{configPath}" has been deprecated. Set "telemetry.sendUsageTo: staging" to the Kibana configurations to send usage to the staging endpoint.',
|
||||
values: { configPath: fullConfigPath },
|
||||
}),
|
||||
correctiveActions: {
|
||||
manualSteps: [
|
||||
i18n.translate('telemetry.endpointConfigs.deprecationManualStep1', {
|
||||
defaultMessage: 'Remove "{configPath}" from the Kibana configuration.',
|
||||
values: { configPath: fullConfigPath },
|
||||
}),
|
||||
i18n.translate('telemetry.endpointConfigs.deprecationManualStep2', {
|
||||
defaultMessage:
|
||||
'To send usage to the staging endpoint add "telemetry.sendUsageTo: staging" to the Kibana configuration.',
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
set: [{ path: 'telemetry.sendUsageTo', value: useStaging ? 'staging' : 'prod' }],
|
||||
unset,
|
||||
};
|
||||
};
|
10
src/plugins/telemetry/server/config/index.ts
Normal file
10
src/plugins/telemetry/server/config/index.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { config } from './config';
|
||||
export type { TelemetryConfigType } from './config';
|
|
@ -19,6 +19,7 @@ import {
|
|||
ICustomClusterClient,
|
||||
} from '../../../core/server';
|
||||
import {
|
||||
getTelemetryChannelEndpoint,
|
||||
getTelemetryOptIn,
|
||||
getTelemetrySendUsageFrom,
|
||||
getTelemetryFailureDetails,
|
||||
|
@ -139,7 +140,10 @@ export class FetcherTask {
|
|||
const configTelemetrySendUsageFrom = config.sendUsageFrom;
|
||||
const allowChangingOptInStatus = config.allowChangingOptInStatus;
|
||||
const configTelemetryOptIn = typeof config.optIn === 'undefined' ? null : config.optIn;
|
||||
const telemetryUrl = config.url;
|
||||
const telemetryUrl = getTelemetryChannelEndpoint({
|
||||
channelName: 'main',
|
||||
env: config.sendUsageTo,
|
||||
});
|
||||
const { failureCount, failureVersion } = getTelemetryFailureDetails({
|
||||
telemetrySavedObject,
|
||||
});
|
||||
|
@ -208,17 +212,17 @@ export class FetcherTask {
|
|||
});
|
||||
}
|
||||
|
||||
private async sendTelemetry(url: string, cluster: string): Promise<void> {
|
||||
private async sendTelemetry(telemetryUrl: string, cluster: string): Promise<void> {
|
||||
this.logger.debug(`Sending usage stats.`);
|
||||
/**
|
||||
* send OPTIONS before sending usage data.
|
||||
* OPTIONS is less intrusive as it does not contain any payload and is used here to check if the endpoint is reachable.
|
||||
*/
|
||||
await fetch(url, {
|
||||
await fetch(telemetryUrl, {
|
||||
method: 'options',
|
||||
});
|
||||
|
||||
await fetch(url, {
|
||||
await fetch(telemetryUrl, {
|
||||
method: 'post',
|
||||
body: cluster,
|
||||
headers: { 'X-Elastic-Stack-Version': this.currentKibanaVersion },
|
||||
|
|
|
@ -6,25 +6,13 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { PluginInitializerContext, PluginConfigDescriptor } from 'kibana/server';
|
||||
import type { PluginInitializerContext } from 'kibana/server';
|
||||
import type { TelemetryConfigType } from './config';
|
||||
import { TelemetryPlugin } from './plugin';
|
||||
import { configSchema, TelemetryConfigType } from './config';
|
||||
|
||||
export { config } from './config';
|
||||
export type { TelemetryPluginSetup, TelemetryPluginStart } from './plugin';
|
||||
|
||||
export const config: PluginConfigDescriptor<TelemetryConfigType> = {
|
||||
schema: configSchema,
|
||||
exposeToBrowser: {
|
||||
enabled: true,
|
||||
url: true,
|
||||
banner: true,
|
||||
allowChangingOptInStatus: true,
|
||||
optIn: true,
|
||||
optInStatusUrl: true,
|
||||
sendUsageFrom: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const plugin = (initializerContext: PluginInitializerContext<TelemetryConfigType>) =>
|
||||
new TelemetryPlugin(initializerContext);
|
||||
export { getClusterUuids, getLocalStats } from './telemetry_collection';
|
||||
|
|
|
@ -30,11 +30,11 @@ import {
|
|||
registerTelemetryUsageCollector,
|
||||
registerTelemetryPluginUsageCollector,
|
||||
} from './collectors';
|
||||
import { TelemetryConfigType } from './config';
|
||||
import type { TelemetryConfigType } from './config';
|
||||
import { FetcherTask } from './fetcher';
|
||||
import { handleOldSettings } from './handle_old_settings';
|
||||
import { getTelemetrySavedObject } from './telemetry_repository';
|
||||
import { getTelemetryOptIn } from '../common/telemetry_config';
|
||||
import { getTelemetryOptIn, getTelemetryChannelEndpoint } from '../common/telemetry_config';
|
||||
|
||||
interface TelemetryPluginsDepsSetup {
|
||||
usageCollection: UsageCollectionSetup;
|
||||
|
@ -117,8 +117,10 @@ export class TelemetryPlugin implements Plugin<TelemetryPluginSetup, TelemetryPl
|
|||
|
||||
return {
|
||||
getTelemetryUrl: async () => {
|
||||
const config = await config$.pipe(take(1)).toPromise();
|
||||
return new URL(config.url);
|
||||
const { sendUsageTo } = await config$.pipe(take(1)).toPromise();
|
||||
const telemetryUrl = getTelemetryChannelEndpoint({ env: sendUsageTo, channelName: 'main' });
|
||||
|
||||
return new URL(telemetryUrl);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -83,15 +83,15 @@ export function registerTelemetryOptInRoutes({
|
|||
);
|
||||
|
||||
if (config.sendUsageFrom === 'server') {
|
||||
const optInStatusUrl = config.optInStatusUrl;
|
||||
const { sendUsageTo } = config;
|
||||
sendTelemetryOptInStatus(
|
||||
telemetryCollectionManager,
|
||||
{ optInStatusUrl, newOptInStatus, currentKibanaVersion },
|
||||
{ sendUsageTo, newOptInStatus, currentKibanaVersion },
|
||||
statsGetterConfig
|
||||
).catch((err) => {
|
||||
// The server is likely behind a firewall and can't reach the remote service
|
||||
logger.warn(
|
||||
`Failed to notify "${optInStatusUrl}" from the server about the opt-in selection. Possibly blocked by a firewall? - Error: ${err.message}`
|
||||
`Failed to notify the telemetry endpoint about the opt-in selection. Possibly blocked by a firewall? - Error: ${err.message}`
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -10,30 +10,53 @@ jest.mock('node-fetch');
|
|||
import fetch from 'node-fetch';
|
||||
import { sendTelemetryOptInStatus } from './telemetry_opt_in_stats';
|
||||
import { StatsGetterConfig } from 'src/plugins/telemetry_collection_manager/server';
|
||||
|
||||
import { TELEMETRY_ENDPOINT } from '../../common/constants';
|
||||
describe('sendTelemetryOptInStatus', () => {
|
||||
const mockStatsGetterConfig = { unencrypted: false } as StatsGetterConfig;
|
||||
const mockTelemetryCollectionManager = {
|
||||
getOptInStats: jest.fn().mockResolvedValue(['mock_opt_in_hashed_value']),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('calls fetch with the opt in status returned from the telemetryCollectionManager', async () => {
|
||||
const mockOptInStatus = ['mock_opt_in_hashed_value'];
|
||||
const mockTelemetryCollectionManager = {
|
||||
getOptInStats: jest.fn().mockResolvedValue(mockOptInStatus),
|
||||
};
|
||||
const mockConfig = {
|
||||
optInStatusUrl: 'some_url',
|
||||
sendUsageTo: 'prod' as const,
|
||||
newOptInStatus: true,
|
||||
currentKibanaVersion: 'mock_kibana_version',
|
||||
};
|
||||
const mockStatsGetterConfig = {
|
||||
unencrypted: false,
|
||||
};
|
||||
|
||||
const result = await sendTelemetryOptInStatus(
|
||||
mockTelemetryCollectionManager,
|
||||
mockConfig,
|
||||
mockStatsGetterConfig as StatsGetterConfig
|
||||
mockStatsGetterConfig
|
||||
);
|
||||
expect(result).toBeUndefined();
|
||||
expect(fetch).toBeCalledTimes(1);
|
||||
expect(fetch).toBeCalledWith(mockConfig.optInStatusUrl, {
|
||||
expect(fetch).toBeCalledWith(TELEMETRY_ENDPOINT.OPT_IN_STATUS_CHANNEL.PROD, {
|
||||
method: 'post',
|
||||
body: '["mock_opt_in_hashed_value"]',
|
||||
headers: { 'X-Elastic-Stack-Version': mockConfig.currentKibanaVersion },
|
||||
});
|
||||
});
|
||||
|
||||
it('sends to staging endpoint on "sendUsageTo: staging"', async () => {
|
||||
const mockConfig = {
|
||||
sendUsageTo: 'staging' as const,
|
||||
newOptInStatus: true,
|
||||
currentKibanaVersion: 'mock_kibana_version',
|
||||
};
|
||||
|
||||
await sendTelemetryOptInStatus(
|
||||
mockTelemetryCollectionManager,
|
||||
mockConfig,
|
||||
mockStatsGetterConfig
|
||||
);
|
||||
|
||||
expect(fetch).toBeCalledTimes(1);
|
||||
expect(fetch).toBeCalledWith(TELEMETRY_ENDPOINT.OPT_IN_STATUS_CHANNEL.STAGING, {
|
||||
method: 'post',
|
||||
body: '["mock_opt_in_hashed_value"]',
|
||||
headers: { 'X-Elastic-Stack-Version': mockConfig.currentKibanaVersion },
|
||||
|
|
|
@ -14,9 +14,10 @@ import {
|
|||
TelemetryCollectionManagerPluginSetup,
|
||||
StatsGetterConfig,
|
||||
} from 'src/plugins/telemetry_collection_manager/server';
|
||||
import { getTelemetryChannelEndpoint } from '../../common/telemetry_config';
|
||||
|
||||
interface SendTelemetryOptInStatusConfig {
|
||||
optInStatusUrl: string;
|
||||
sendUsageTo: 'staging' | 'prod';
|
||||
newOptInStatus: boolean;
|
||||
currentKibanaVersion: string;
|
||||
}
|
||||
|
@ -26,7 +27,12 @@ export async function sendTelemetryOptInStatus(
|
|||
config: SendTelemetryOptInStatusConfig,
|
||||
statsGetterConfig: StatsGetterConfig
|
||||
) {
|
||||
const { optInStatusUrl, newOptInStatus, currentKibanaVersion } = config;
|
||||
const { sendUsageTo, newOptInStatus, currentKibanaVersion } = config;
|
||||
const optInStatusUrl = getTelemetryChannelEndpoint({
|
||||
env: sendUsageTo,
|
||||
channelName: 'optInStatus',
|
||||
});
|
||||
|
||||
const optInStatus = await telemetryCollectionManager.getOptInStats(
|
||||
newOptInStatus,
|
||||
statsGetterConfig
|
||||
|
|
|
@ -311,9 +311,8 @@ exports[`TelemetryManagementSectionComponent renders null because allowChangingO
|
|||
"banner": true,
|
||||
"enabled": true,
|
||||
"optIn": true,
|
||||
"optInStatusUrl": "",
|
||||
"sendUsageFrom": "browser",
|
||||
"url": "",
|
||||
"sendUsageTo": "staging",
|
||||
},
|
||||
"fetchExample": [Function],
|
||||
"fetchTelemetry": [Function],
|
||||
|
|
|
@ -24,12 +24,11 @@ describe('TelemetryManagementSectionComponent', () => {
|
|||
const isSecurityExampleEnabled = jest.fn().mockReturnValue(true);
|
||||
const telemetryService = new TelemetryService({
|
||||
config: {
|
||||
sendUsageTo: 'staging',
|
||||
enabled: true,
|
||||
url: '',
|
||||
banner: true,
|
||||
allowChangingOptInStatus: true,
|
||||
optIn: true,
|
||||
optInStatusUrl: '',
|
||||
sendUsageFrom: 'browser',
|
||||
},
|
||||
isScreenshotMode: false,
|
||||
|
@ -60,12 +59,11 @@ describe('TelemetryManagementSectionComponent', () => {
|
|||
const telemetryService = new TelemetryService({
|
||||
config: {
|
||||
enabled: true,
|
||||
url: '',
|
||||
banner: true,
|
||||
allowChangingOptInStatus: true,
|
||||
optIn: false,
|
||||
optInStatusUrl: '',
|
||||
sendUsageFrom: 'browser',
|
||||
sendUsageTo: 'staging',
|
||||
},
|
||||
isScreenshotMode: false,
|
||||
reportOptInStatusChange: false,
|
||||
|
@ -116,11 +114,10 @@ describe('TelemetryManagementSectionComponent', () => {
|
|||
const telemetryService = new TelemetryService({
|
||||
config: {
|
||||
enabled: true,
|
||||
url: '',
|
||||
banner: true,
|
||||
allowChangingOptInStatus: true,
|
||||
optIn: false,
|
||||
optInStatusUrl: '',
|
||||
sendUsageTo: 'staging',
|
||||
sendUsageFrom: 'browser',
|
||||
},
|
||||
isScreenshotMode: false,
|
||||
|
@ -165,11 +162,10 @@ describe('TelemetryManagementSectionComponent', () => {
|
|||
const telemetryService = new TelemetryService({
|
||||
config: {
|
||||
enabled: true,
|
||||
url: '',
|
||||
banner: true,
|
||||
allowChangingOptInStatus: false,
|
||||
optIn: true,
|
||||
optInStatusUrl: '',
|
||||
sendUsageTo: 'staging',
|
||||
sendUsageFrom: 'browser',
|
||||
},
|
||||
isScreenshotMode: false,
|
||||
|
@ -205,11 +201,10 @@ describe('TelemetryManagementSectionComponent', () => {
|
|||
const telemetryService = new TelemetryService({
|
||||
config: {
|
||||
enabled: true,
|
||||
url: '',
|
||||
banner: true,
|
||||
allowChangingOptInStatus: true,
|
||||
optIn: false,
|
||||
optInStatusUrl: '',
|
||||
sendUsageTo: 'staging',
|
||||
sendUsageFrom: 'browser',
|
||||
},
|
||||
isScreenshotMode: false,
|
||||
|
@ -246,11 +241,10 @@ describe('TelemetryManagementSectionComponent', () => {
|
|||
const telemetryService = new TelemetryService({
|
||||
config: {
|
||||
enabled: true,
|
||||
url: '',
|
||||
banner: true,
|
||||
allowChangingOptInStatus: true,
|
||||
optIn: false,
|
||||
optInStatusUrl: '',
|
||||
sendUsageTo: 'staging',
|
||||
sendUsageFrom: 'browser',
|
||||
},
|
||||
isScreenshotMode: false,
|
||||
|
@ -287,11 +281,10 @@ describe('TelemetryManagementSectionComponent', () => {
|
|||
const telemetryService = new TelemetryService({
|
||||
config: {
|
||||
enabled: true,
|
||||
url: '',
|
||||
banner: true,
|
||||
allowChangingOptInStatus: true,
|
||||
optIn: false,
|
||||
optInStatusUrl: '',
|
||||
sendUsageTo: 'staging',
|
||||
sendUsageFrom: 'browser',
|
||||
},
|
||||
isScreenshotMode: false,
|
||||
|
@ -328,11 +321,10 @@ describe('TelemetryManagementSectionComponent', () => {
|
|||
const telemetryService = new TelemetryService({
|
||||
config: {
|
||||
enabled: true,
|
||||
url: '',
|
||||
banner: true,
|
||||
allowChangingOptInStatus: true,
|
||||
optIn: false,
|
||||
optInStatusUrl: '',
|
||||
sendUsageTo: 'staging',
|
||||
sendUsageFrom: 'browser',
|
||||
},
|
||||
isScreenshotMode: false,
|
||||
|
@ -379,11 +371,10 @@ describe('TelemetryManagementSectionComponent', () => {
|
|||
const telemetryService = new TelemetryService({
|
||||
config: {
|
||||
enabled: true,
|
||||
url: '',
|
||||
banner: true,
|
||||
allowChangingOptInStatus: false,
|
||||
optIn: false,
|
||||
optInStatusUrl: '',
|
||||
sendUsageTo: 'staging',
|
||||
sendUsageFrom: 'browser',
|
||||
},
|
||||
isScreenshotMode: false,
|
||||
|
|
|
@ -17,17 +17,6 @@ import {
|
|||
TelemetryManagementSectionWrapperProps,
|
||||
} from './components/telemetry_management_section_wrapper';
|
||||
|
||||
export interface TelemetryPluginConfig {
|
||||
enabled: boolean;
|
||||
url: string;
|
||||
banner: boolean;
|
||||
allowChangingOptInStatus: boolean;
|
||||
optIn: boolean | null;
|
||||
optInStatusUrl: string;
|
||||
sendUsageFrom: 'browser' | 'server';
|
||||
telemetryNotifyUserAboutOptInDefault?: boolean;
|
||||
}
|
||||
|
||||
export interface TelemetryManagementSectionPluginDepsSetup {
|
||||
telemetry: TelemetryPluginSetup;
|
||||
advancedSettings: AdvancedSettingsSetup;
|
||||
|
|
|
@ -48,8 +48,7 @@ export default function () {
|
|||
'--telemetry.banner=false',
|
||||
'--telemetry.optIn=false',
|
||||
// These are *very* important to have them pointing to staging
|
||||
'--telemetry.url=https://telemetry-staging.elastic.co/xpack/v2/send',
|
||||
'--telemetry.optInStatusUrl=https://telemetry-staging.elastic.co/opt_in_status/v2/send',
|
||||
'--telemetry.sendUsageTo=staging',
|
||||
`--server.maxPayload=1679958`,
|
||||
// newsfeed mock service
|
||||
`--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'newsfeed')}`,
|
||||
|
|
Loading…
Reference in a new issue