[Telemetry] Set telemetry SO as hidden (#147631)

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Resolves https://github.com/elastic/kibana/issues/145399
This commit is contained in:
Alejandro Fernández Haro 2022-12-20 17:13:36 +01:00 committed by GitHub
parent 53e42eb633
commit 30469a427a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 474 additions and 358 deletions

View file

@ -0,0 +1,18 @@
/*
* 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.
*/
/**
* Fetch Telemetry Config
*/
export const FetchTelemetryConfigRoute = '/api/telemetry/v2/config';
export interface FetchTelemetryConfigResponse {
allowChangingOptInStatus: boolean;
optIn: boolean | null;
sendUsageFrom: 'server' | 'browser';
telemetryNotifyUserAboutOptInDefault: boolean;
}

View file

@ -6,11 +6,6 @@
* Side Public License, v 1.
*/
export { getTelemetryOptIn } from './get_telemetry_opt_in';
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,

View file

@ -12,22 +12,17 @@ import type {
CoreSetup,
HttpStart,
PluginInitializerContext,
SavedObjectsClientContract,
SavedObjectsBatchResponse,
ApplicationStart,
DocLinksStart,
HttpSetup,
} from '@kbn/core/public';
import type { ScreenshotModePluginSetup } from '@kbn/screenshot-mode-plugin/public';
import type { HomePublicPluginSetup } from '@kbn/home-plugin/public';
import { ElasticV3BrowserShipper } from '@kbn/analytics-shippers-elastic-v3-browser';
import { of } from 'rxjs';
import { FetchTelemetryConfigResponse, FetchTelemetryConfigRoute } from '../common/routes';
import { TelemetrySender, TelemetryService, TelemetryNotifications } from './services';
import type {
TelemetrySavedObjectAttributes,
TelemetrySavedObject,
} from '../common/telemetry_config/types';
import { getNotifyUserAboutOptInDefault } from '../common/telemetry_config/get_telemetry_notify_user_about_optin_default';
import { renderWelcomeTelemetryNotice } from './render_welcome_telemetry_notice';
/**
@ -122,7 +117,6 @@ export class TelemetryPlugin implements Plugin<TelemetryPluginSetup, TelemetryPl
private telemetryNotifications?: TelemetryNotifications;
private telemetryService?: TelemetryService;
private canUserChangeSettings: boolean = true;
private savedObjectsClient?: SavedObjectsClientContract;
constructor(initializerContext: PluginInitializerContext<TelemetryPluginConfig>) {
this.currentKibanaVersion = initializerContext.env.packageInfo.version;
@ -169,7 +163,7 @@ export class TelemetryPlugin implements Plugin<TelemetryPluginSetup, TelemetryPl
});
this.telemetrySender = new TelemetrySender(this.telemetryService, async () => {
await this.refreshConfig();
await this.refreshConfig(http);
analytics.optIn({ global: { enabled: this.telemetryService!.isOptedIn } });
});
@ -200,7 +194,6 @@ export class TelemetryPlugin implements Plugin<TelemetryPluginSetup, TelemetryPl
overlays,
theme,
application,
savedObjects,
docLinks,
}: CoreStart): TelemetryPluginStart {
if (!this.telemetryService) {
@ -220,8 +213,6 @@ export class TelemetryPlugin implements Plugin<TelemetryPluginSetup, TelemetryPl
});
this.telemetryNotifications = telemetryNotifications;
this.savedObjectsClient = savedObjects.client;
application.currentAppId$.subscribe(async () => {
const isUnauthenticated = this.getIsUnauthenticated(http);
if (isUnauthenticated) {
@ -229,7 +220,7 @@ export class TelemetryPlugin implements Plugin<TelemetryPluginSetup, TelemetryPl
}
// Refresh and get telemetry config
const updatedConfig = await this.refreshConfig();
const updatedConfig = await this.refreshConfig(http);
analytics.optIn({ global: { enabled: this.telemetryService!.isOptedIn } });
@ -267,14 +258,17 @@ export class TelemetryPlugin implements Plugin<TelemetryPluginSetup, TelemetryPl
};
}
private async refreshConfig(): Promise<TelemetryPluginConfig | undefined> {
if (this.savedObjectsClient && this.telemetryService) {
// Update the telemetry config based as a mix of the config files and saved objects
const telemetrySavedObject = await this.getTelemetrySavedObject(this.savedObjectsClient);
const updatedConfig = await this.updateConfigsBasedOnSavedObjects(telemetrySavedObject);
/**
* Retrieve the up-to-date configuration
* @param http HTTP helper to make requests to the server
* @private
*/
private async refreshConfig(http: HttpStart | HttpSetup): Promise<TelemetryPluginConfig> {
const updatedConfig = await this.fetchUpdatedConfig(http);
if (this.telemetryService) {
this.telemetryService.config = updatedConfig;
return updatedConfig;
}
return updatedConfig;
}
/**
@ -321,74 +315,22 @@ export class TelemetryPlugin implements Plugin<TelemetryPluginSetup, TelemetryPl
}
}
private async updateConfigsBasedOnSavedObjects(
telemetrySavedObject: TelemetrySavedObject
): Promise<TelemetryPluginConfig> {
const configTelemetrySendUsageFrom = this.config.sendUsageFrom;
const configTelemetryOptIn = this.config.optIn as boolean;
const configTelemetryAllowChangingOptInStatus = this.config.allowChangingOptInStatus;
const currentKibanaVersion = this.currentKibanaVersion;
const { getTelemetryAllowChangingOptInStatus, getTelemetryOptIn, getTelemetrySendUsageFrom } =
await import('../common/telemetry_config');
const allowChangingOptInStatus = getTelemetryAllowChangingOptInStatus({
configTelemetryAllowChangingOptInStatus,
telemetrySavedObject,
});
const optIn = getTelemetryOptIn({
configTelemetryOptIn,
allowChangingOptInStatus,
telemetrySavedObject,
currentKibanaVersion,
});
const sendUsageFrom = getTelemetrySendUsageFrom({
configTelemetrySendUsageFrom,
telemetrySavedObject,
});
const telemetryNotifyUserAboutOptInDefault = getNotifyUserAboutOptInDefault({
telemetrySavedObject,
allowChangingOptInStatus,
configTelemetryOptIn,
telemetryOptedIn: optIn,
});
/**
* Fetch configuration from the server and merge it with the one the browser already knows
* @param http The HTTP helper to make the requests
* @private
*/
private async fetchUpdatedConfig(http: HttpStart | HttpSetup): Promise<TelemetryPluginConfig> {
const { allowChangingOptInStatus, optIn, sendUsageFrom, telemetryNotifyUserAboutOptInDefault } =
await http.get<FetchTelemetryConfigResponse>(FetchTelemetryConfigRoute);
return {
...this.config,
allowChangingOptInStatus,
optIn,
sendUsageFrom,
telemetryNotifyUserAboutOptInDefault,
userCanChangeSettings: this.canUserChangeSettings,
};
}
private async getTelemetrySavedObject(savedObjectsClient: SavedObjectsClientContract) {
try {
// Use bulk get API here to avoid the queue. This could fail independent requests if we don't have rights to access the telemetry object otherwise
const {
savedObjects: [{ attributes }],
} = (await savedObjectsClient.bulkGet([
{
id: 'telemetry',
type: 'telemetry',
},
])) as SavedObjectsBatchResponse<TelemetrySavedObjectAttributes>;
return attributes;
} catch (error) {
const errorCode = error[Symbol('SavedObjectsClientErrorCode')];
if (errorCode === 'SavedObjectsClient/notFound') {
return null;
}
if (errorCode === 'SavedObjectsClient/forbidden') {
return false;
}
throw error;
}
}
}

View file

@ -7,11 +7,11 @@
*/
import { Observable, firstValueFrom } from 'rxjs';
import { ISavedObjectsRepository, SavedObjectsClient } from '@kbn/core/server';
import { ISavedObjectsRepository } from '@kbn/core/server';
import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server';
import { getTelemetrySavedObject, TelemetrySavedObject } from '../../telemetry_repository';
import { getTelemetryOptIn, getTelemetrySendUsageFrom } from '../../../common/telemetry_config';
import { getTelemetrySavedObject, TelemetrySavedObject } from '../../saved_objects';
import { TelemetryConfigType } from '../../config';
import { getTelemetryOptIn, getTelemetrySendUsageFrom } from '../../telemetry_config';
export interface TelemetryUsageStats {
opt_in_status?: boolean | null;
@ -39,9 +39,7 @@ export function createCollectorFetch({
try {
const internalRepository = getSavedObjectsClient()!;
telemetrySavedObject = await getTelemetrySavedObject(
new SavedObjectsClient(internalRepository)
);
telemetrySavedObject = await getTelemetrySavedObject(internalRepository);
} catch (err) {
// no-op
}

View file

@ -22,23 +22,27 @@ import {
import fetch from 'node-fetch';
import type { TelemetryCollectionManagerPluginStart } from '@kbn/telemetry-collection-manager-plugin/server';
import {
PluginInitializerContext,
Logger,
SavedObjectsClientContract,
type PluginInitializerContext,
type Logger,
type SavedObjectsClientContract,
SavedObjectsClient,
CoreStart,
type CoreStart,
} from '@kbn/core/server';
import { getTelemetryChannelEndpoint } from '../common/telemetry_config';
import {
TELEMETRY_SAVED_OBJECT_TYPE,
getTelemetrySavedObject,
updateTelemetrySavedObject,
} from './saved_objects';
import { getNextAttemptDate } from './get_next_attempt_date';
import {
getTelemetryChannelEndpoint,
getTelemetryOptIn,
getTelemetrySendUsageFrom,
getTelemetryFailureDetails,
} from '../common/telemetry_config';
import { getTelemetrySavedObject, updateTelemetrySavedObject } from './telemetry_repository';
} from './telemetry_config';
import { PAYLOAD_CONTENT_ENCODING } from '../common/constants';
import type { EncryptedTelemetryPayload } from '../common/types';
import { TelemetryConfigType } from './config';
import type { TelemetryConfigType } from './config';
import { isReportIntervalExpired } from '../common/is_report_interval_expired';
export interface FetcherTaskDepsStart {
@ -78,7 +82,9 @@ export class FetcherTask {
}
public start({ savedObjects }: CoreStart, { telemetryCollectionManager }: FetcherTaskDepsStart) {
this.internalRepository = new SavedObjectsClient(savedObjects.createInternalRepository());
this.internalRepository = new SavedObjectsClient(
savedObjects.createInternalRepository([TELEMETRY_SAVED_OBJECT_TYPE])
);
this.telemetryCollectionManager = telemetryCollectionManager;
this.subscriptions.add(this.validateConnectivity());

View file

@ -40,6 +40,12 @@ import type {
import type { SecurityPluginStart } from '@kbn/security-plugin/server';
import { SavedObjectsClient } from '@kbn/core/server';
import {
type TelemetrySavedObject,
getTelemetrySavedObject,
registerTelemetrySavedObject,
TELEMETRY_SAVED_OBJECT_TYPE,
} from './saved_objects';
import { registerRoutes } from './routes';
import { registerCollection } from './telemetry_collection';
import {
@ -48,9 +54,9 @@ import {
} from './collectors';
import type { TelemetryConfigLabels, TelemetryConfigType } from './config';
import { FetcherTask } from './fetcher';
import { getTelemetrySavedObject, TelemetrySavedObject } from './telemetry_repository';
import { OPT_IN_POLL_INTERVAL_MS } from '../common/constants';
import { getTelemetryOptIn, getTelemetryChannelEndpoint } from '../common/telemetry_config';
import { getTelemetryChannelEndpoint } from '../common/telemetry_config';
import { getTelemetryOptIn } from './telemetry_config';
interface TelemetryPluginsDepsSetup {
usageCollection: UsageCollectionSetup;
@ -87,8 +93,6 @@ export interface TelemetryPluginStart {
getIsOptedIn: () => Promise<boolean>;
}
type SavedObjectsRegisterType = CoreSetup['savedObjects']['registerType'];
export class TelemetryPlugin implements Plugin<TelemetryPluginSetup, TelemetryPluginStart> {
private readonly logger: Logger;
private readonly currentKibanaVersion: string;
@ -111,9 +115,9 @@ export class TelemetryPlugin implements Plugin<TelemetryPluginSetup, TelemetryPl
*
* Using the internal client in all cases ensures the permissions to interact the document.
*/
private savedObjectsInternalClient$ = new ReplaySubject<SavedObjectsClient>(1);
private readonly savedObjectsInternalClient$ = new ReplaySubject<SavedObjectsClient>(1);
private pluginStop$ = new ReplaySubject<void>(1);
private readonly pluginStop$ = new ReplaySubject<void>(1);
private security?: SecurityPluginStart;
@ -189,7 +193,7 @@ export class TelemetryPlugin implements Plugin<TelemetryPluginSetup, TelemetryPl
getSecurity: () => this.security,
});
this.registerMappings((opts) => savedObjects.registerType(opts));
registerTelemetrySavedObject((opts) => savedObjects.registerType(opts));
this.registerUsageCollectors(usageCollection);
return {
@ -213,7 +217,9 @@ export class TelemetryPlugin implements Plugin<TelemetryPluginSetup, TelemetryPl
this.isOptedIn$.subscribe((enabled) => analytics.optIn({ global: { enabled } }));
const savedObjectsInternalRepository = savedObjects.createInternalRepository();
const savedObjectsInternalRepository = savedObjects.createInternalRepository([
TELEMETRY_SAVED_OBJECT_TYPE,
]);
this.savedObjectsInternalRepository = savedObjectsInternalRepository;
this.savedObjectsInternalClient$.next(new SavedObjectsClient(savedObjectsInternalRepository));
@ -244,10 +250,7 @@ export class TelemetryPlugin implements Plugin<TelemetryPluginSetup, TelemetryPl
telemetrySavedObject = await getTelemetrySavedObject(internalRepositoryClient);
} catch (err) {
this.logger.debug('Failed to check telemetry opt-in status: ' + err.message);
}
// If we can't get the saved object due to permissions or other error other than 404, skip this round.
if (typeof telemetrySavedObject === 'undefined' || telemetrySavedObject === false) {
// If we can't get the saved object due to any error other than 404, skip this round.
return;
}
@ -271,46 +274,10 @@ export class TelemetryPlugin implements Plugin<TelemetryPluginSetup, TelemetryPl
core: CoreStart,
telemetryCollectionManager: TelemetryCollectionManagerPluginStart
) {
// We start the fetcher having updated everything we need to using the config settings
// We start the fetcher having updated everything we need to use the config settings
this.fetcherTask.start(core, { telemetryCollectionManager });
}
private registerMappings(registerType: SavedObjectsRegisterType) {
registerType({
name: 'telemetry',
hidden: false,
namespaceType: 'agnostic',
mappings: {
properties: {
enabled: {
type: 'boolean',
},
sendUsageFrom: {
type: 'keyword',
},
lastReported: {
type: 'date',
},
lastVersionChecked: {
type: 'keyword',
},
userHasSeenNotice: {
type: 'boolean',
},
reportFailureCount: {
type: 'integer',
},
reportFailureVersion: {
type: 'keyword',
},
allowChangingOptInStatus: {
type: 'boolean',
},
},
},
});
}
private registerUsageCollectors(usageCollection: UsageCollectionSetup) {
const getSavedObjectsClient = () => this.savedObjectsInternalRepository;

View file

@ -9,11 +9,12 @@
import type { Observable } from 'rxjs';
import type { IRouter, Logger, SavedObjectsClient } from '@kbn/core/server';
import type { TelemetryCollectionManagerPluginSetup } from '@kbn/telemetry-collection-manager-plugin/server';
import type { TelemetryConfigType } from '../config';
import { registerTelemetryConfigRoutes } from './telemetry_config';
import { registerTelemetryOptInRoutes } from './telemetry_opt_in';
import { registerTelemetryUsageStatsRoutes, SecurityGetter } from './telemetry_usage_stats';
import { registerTelemetryUsageStatsRoutes, type SecurityGetter } from './telemetry_usage_stats';
import { registerTelemetryOptInStatsRoutes } from './telemetry_opt_in_stats';
import { registerTelemetryUserHasSeenNotice } from './telemetry_user_has_seen_notice';
import type { TelemetryConfigType } from '../config';
import { registerTelemetryLastReported } from './telemetry_last_reported';
interface RegisterRoutesParams {
@ -31,6 +32,7 @@ export function registerRoutes(options: RegisterRoutesParams) {
const { isDev, telemetryCollectionManager, router, savedObjectsInternalClient$, getSecurity } =
options;
registerTelemetryOptInRoutes(options);
registerTelemetryConfigRoutes(options);
registerTelemetryUsageStatsRoutes(router, telemetryCollectionManager, isDev, getSecurity);
registerTelemetryOptInStatsRoutes(router, telemetryCollectionManager);
registerTelemetryUserHasSeenNotice(router);

View file

@ -0,0 +1,77 @@
/*
* 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 { type Observable, firstValueFrom } from 'rxjs';
import type { IRouter, SavedObjectsClient } from '@kbn/core/server';
import type { TelemetryConfigType } from '../config';
import { FetchTelemetryConfigResponse, FetchTelemetryConfigRoute } from '../../common/routes';
import { getTelemetrySavedObject } from '../saved_objects';
import {
getNotifyUserAboutOptInDefault,
getTelemetryAllowChangingOptInStatus,
getTelemetryOptIn,
getTelemetrySendUsageFrom,
} from '../telemetry_config';
interface RegisterTelemetryConfigRouteOptions {
router: IRouter;
config$: Observable<TelemetryConfigType>;
currentKibanaVersion: string;
savedObjectsInternalClient$: Observable<SavedObjectsClient>;
}
export function registerTelemetryConfigRoutes({
router,
config$,
currentKibanaVersion,
savedObjectsInternalClient$,
}: RegisterTelemetryConfigRouteOptions) {
// GET to retrieve
router.get(
{
path: FetchTelemetryConfigRoute,
validate: false,
},
async (context, req, res) => {
const config = await firstValueFrom(config$);
const savedObjectsInternalClient = await firstValueFrom(savedObjectsInternalClient$);
const telemetrySavedObject = await getTelemetrySavedObject(savedObjectsInternalClient);
const allowChangingOptInStatus = getTelemetryAllowChangingOptInStatus({
configTelemetryAllowChangingOptInStatus: config.allowChangingOptInStatus,
telemetrySavedObject,
});
const optIn = getTelemetryOptIn({
configTelemetryOptIn: config.optIn,
allowChangingOptInStatus,
telemetrySavedObject,
currentKibanaVersion,
});
const sendUsageFrom = getTelemetrySendUsageFrom({
configTelemetrySendUsageFrom: config.sendUsageFrom,
telemetrySavedObject,
});
const telemetryNotifyUserAboutOptInDefault = getNotifyUserAboutOptInDefault({
telemetrySavedObject,
allowChangingOptInStatus,
configTelemetryOptIn: config.optIn,
telemetryOptedIn: optIn,
});
const body: FetchTelemetryConfigResponse = {
allowChangingOptInStatus,
optIn,
sendUsageFrom,
telemetryNotifyUserAboutOptInDefault,
};
return res.ok({ body });
}
);
}

View file

@ -9,7 +9,7 @@
import type { IRouter, SavedObjectsClient } from '@kbn/core/server';
import type { Observable } from 'rxjs';
import { firstValueFrom } from 'rxjs';
import { getTelemetrySavedObject, updateTelemetrySavedObject } from '../telemetry_repository';
import { getTelemetrySavedObject, updateTelemetrySavedObject } from '../saved_objects';
export function registerTelemetryLastReported(
router: IRouter,

View file

@ -6,23 +6,24 @@
* Side Public License, v 1.
*/
import { firstValueFrom, Observable } from 'rxjs';
import { firstValueFrom, type Observable } from 'rxjs';
import { schema } from '@kbn/config-schema';
import { IRouter, Logger } from '@kbn/core/server';
import {
import type { IRouter, Logger } from '@kbn/core/server';
import { SavedObjectsErrorHelpers } from '@kbn/core/server';
import type {
StatsGetterConfig,
TelemetryCollectionManagerPluginSetup,
} from '@kbn/telemetry-collection-manager-plugin/server';
import { SavedObjectsErrorHelpers } from '@kbn/core/server';
import { getTelemetryAllowChangingOptInStatus } from '../../common/telemetry_config';
import { sendTelemetryOptInStatus } from './telemetry_opt_in_stats';
import {
TelemetrySavedObjectAttributes,
updateTelemetrySavedObject,
getTelemetrySavedObject,
} from '../telemetry_repository';
TELEMETRY_SAVED_OBJECT_TYPE,
type TelemetrySavedObject,
updateTelemetrySavedObject,
} from '../saved_objects';
import { TelemetryConfigType } from '../config';
import { getTelemetryAllowChangingOptInStatus } from '../telemetry_config';
interface RegisterOptInRoutesParams {
currentKibanaVersion: string;
@ -48,24 +49,29 @@ export function registerTelemetryOptInRoutes({
},
async (context, req, res) => {
const newOptInStatus = req.body.enabled;
const soClient = (await context.core).savedObjects.client;
const attributes: TelemetrySavedObjectAttributes = {
const soClient = (await context.core).savedObjects.getClient({
includedHiddenTypes: [TELEMETRY_SAVED_OBJECT_TYPE],
});
const attributes: TelemetrySavedObject = {
enabled: newOptInStatus,
lastVersionChecked: currentKibanaVersion,
};
const config = await firstValueFrom(config$);
const telemetrySavedObject = await getTelemetrySavedObject(soClient);
if (telemetrySavedObject === false) {
// If we get false, we couldn't get the saved object due to lack of permissions
// so we can assume the user won't be able to update it either
return res.forbidden();
let telemetrySavedObject: TelemetrySavedObject | undefined;
try {
telemetrySavedObject = await getTelemetrySavedObject(soClient);
} catch (err) {
if (SavedObjectsErrorHelpers.isForbiddenError(err)) {
// If we couldn't get the saved object due to lack of permissions,
// we can assume the user won't be able to update it either
return res.forbidden();
}
}
const configTelemetryAllowChangingOptInStatus = config.allowChangingOptInStatus;
const allowChangingOptInStatus = getTelemetryAllowChangingOptInStatus({
configTelemetryAllowChangingOptInStatus: config.allowChangingOptInStatus,
telemetrySavedObject,
configTelemetryAllowChangingOptInStatus,
});
if (!allowChangingOptInStatus) {
return res.badRequest({

View file

@ -8,15 +8,15 @@
import fetch from 'node-fetch';
import { IRouter } from '@kbn/core/server';
import type { IRouter } from '@kbn/core/server';
import { schema } from '@kbn/config-schema';
import {
import type {
TelemetryCollectionManagerPluginSetup,
StatsGetterConfig,
} from '@kbn/telemetry-collection-manager-plugin/server';
import { EncryptedTelemetryPayload, UnencryptedTelemetryPayload } from '../../common/types';
import { getTelemetryChannelEndpoint } from '../../common/telemetry_config';
import { PAYLOAD_CONTENT_ENCODING } from '../../common/constants';
import type { UnencryptedTelemetryPayload } from '../../common/types';
interface SendTelemetryOptInStatusConfig {
sendUsageTo: 'staging' | 'prod';
@ -35,7 +35,7 @@ export async function sendTelemetryOptInStatus(
channelName: 'optInStatus',
});
const optInStatusPayload: UnencryptedTelemetryPayload =
const optInStatusPayload: UnencryptedTelemetryPayload | EncryptedTelemetryPayload =
await telemetryCollectionManager.getOptInStats(newOptInStatus, statsGetterConfig);
await Promise.all(

View file

@ -7,8 +7,8 @@
*/
import { schema } from '@kbn/config-schema';
import { IRouter } from '@kbn/core/server';
import {
import type { IRouter } from '@kbn/core/server';
import type {
TelemetryCollectionManagerPluginSetup,
StatsGetterConfig,
} from '@kbn/telemetry-collection-manager-plugin/server';

View file

@ -6,13 +6,13 @@
* Side Public License, v 1.
*/
import { IRouter } from '@kbn/core/server';
import type { IRouter } from '@kbn/core/server';
import { TELEMETRY_SAVED_OBJECT_TYPE } from '../saved_objects';
import {
TelemetrySavedObject,
TelemetrySavedObjectAttributes,
type TelemetrySavedObjectAttributes,
getTelemetrySavedObject,
updateTelemetrySavedObject,
} from '../telemetry_repository';
} from '../saved_objects';
export function registerTelemetryUserHasSeenNotice(router: IRouter) {
router.put(
@ -21,17 +21,17 @@ export function registerTelemetryUserHasSeenNotice(router: IRouter) {
validate: false,
},
async (context, req, res) => {
const internalRepository = (await context.core).savedObjects.client;
const telemetrySavedObject: TelemetrySavedObject = await getTelemetrySavedObject(
internalRepository
);
const soClient = (await context.core).savedObjects.getClient({
includedHiddenTypes: [TELEMETRY_SAVED_OBJECT_TYPE],
});
const telemetrySavedObject = await getTelemetrySavedObject(soClient);
// update the object with a flag stating that the opt-in notice has been seen
const updatedAttributes: TelemetrySavedObjectAttributes = {
...telemetrySavedObject,
userHasSeenNotice: true,
};
await updateTelemetrySavedObject(internalRepository, updatedAttributes);
await updateTelemetrySavedObject(soClient, updatedAttributes);
return res.ok({ body: updatedAttributes });
}

View 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 const TELEMETRY_SAVED_OBJECT_TYPE = 'telemetry';
export const TELEMETRY_SAVED_OBJECT_ID = 'telemetry';

View file

@ -11,24 +11,24 @@ import { SavedObjectsErrorHelpers } from '@kbn/core/server';
import { savedObjectsClientMock } from '@kbn/core/server/mocks';
describe('getTelemetrySavedObject', () => {
it('returns null when saved object not found', async () => {
it('returns {} when saved object not found', async () => {
const params = getCallGetTelemetrySavedObjectParams({
savedObjectNotFound: true,
});
const result = await callGetTelemetrySavedObject(params);
expect(result).toBe(null);
expect(result).toStrictEqual({});
});
it('returns false when saved object forbidden', async () => {
it('throws when saved object forbidden', async () => {
const params = getCallGetTelemetrySavedObjectParams({
savedObjectForbidden: true,
});
const result = await callGetTelemetrySavedObject(params);
expect(result).toBe(false);
await expect(callGetTelemetrySavedObject(params)).rejects.toThrowErrorMatchingInlineSnapshot(
`"savedObjectForbidden"`
);
});
it('throws an error on unexpected saved object error', async () => {
@ -36,15 +36,7 @@ describe('getTelemetrySavedObject', () => {
savedObjectOtherError: true,
});
let threw = false;
try {
await callGetTelemetrySavedObject(params);
} catch (err) {
threw = true;
expect(err.message).toBe(SavedObjectOtherErrorMessage);
}
expect(threw).toBe(true);
await expect(callGetTelemetrySavedObject(params)).rejects.toThrow(SavedObjectOtherErrorMessage);
});
});

View file

@ -6,28 +6,27 @@
* Side Public License, v 1.
*/
import { SavedObjectsErrorHelpers, type SavedObjectsClientContract } from '@kbn/core/server';
import type { TelemetrySavedObject } from '.';
import type { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server';
import { SavedObjectsErrorHelpers } from '@kbn/core-saved-objects-utils-server';
import type { TelemetrySavedObject } from './types';
import { TELEMETRY_SAVED_OBJECT_TYPE, TELEMETRY_SAVED_OBJECT_ID } from './constants';
type GetTelemetrySavedObject = (
repository: SavedObjectsClientContract
soClient: SavedObjectsClientContract
) => Promise<TelemetrySavedObject>;
export const getTelemetrySavedObject: GetTelemetrySavedObject = async (
repository: SavedObjectsClientContract
soClient: SavedObjectsClientContract
) => {
try {
const { attributes } = await repository.get<TelemetrySavedObject>('telemetry', 'telemetry');
const { attributes } = await soClient.get<TelemetrySavedObject>(
TELEMETRY_SAVED_OBJECT_TYPE,
TELEMETRY_SAVED_OBJECT_ID
);
return attributes;
} catch (error) {
if (SavedObjectsErrorHelpers.isNotFoundError(error)) {
return null;
}
// if we aren't allowed to get the telemetry document, we can assume that we won't
// be able to opt into telemetry either, so we're returning `false` here instead of null
if (SavedObjectsErrorHelpers.isForbiddenError(error)) {
return false;
return {};
}
throw error;

View file

@ -6,9 +6,8 @@
* Side Public License, v 1.
*/
export { TELEMETRY_SAVED_OBJECT_TYPE, TELEMETRY_SAVED_OBJECT_ID } from './constants';
export { getTelemetrySavedObject } from './get_telemetry_saved_object';
export { registerTelemetrySavedObject } from './register_telemetry_saved_object';
export { updateTelemetrySavedObject } from './update_telemetry_saved_object';
export type {
TelemetrySavedObject,
TelemetrySavedObjectAttributes,
} from '../../common/telemetry_config/types';
export type { TelemetrySavedObject, TelemetrySavedObjectAttributes } from './types';

View file

@ -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 type { SavedObjectsServiceSetup } from '@kbn/core-saved-objects-server';
import { TELEMETRY_SAVED_OBJECT_ID } from './constants';
export function registerTelemetrySavedObject(
registerType: SavedObjectsServiceSetup['registerType']
) {
registerType({
name: TELEMETRY_SAVED_OBJECT_ID,
hidden: true,
namespaceType: 'agnostic',
mappings: {
properties: {
enabled: {
type: 'boolean',
},
sendUsageFrom: {
type: 'keyword',
},
lastReported: {
type: 'date',
},
lastVersionChecked: {
type: 'keyword',
},
userHasSeenNotice: {
type: 'boolean',
},
reportFailureCount: {
type: 'integer',
},
reportFailureVersion: {
type: 'keyword',
},
allowChangingOptInStatus: {
type: 'boolean',
},
},
},
});
}

View file

@ -17,4 +17,4 @@ export interface TelemetrySavedObjectAttributes {
reportFailureVersion?: string;
}
export type TelemetrySavedObject = TelemetrySavedObjectAttributes | null | false;
export type TelemetrySavedObject = TelemetrySavedObjectAttributes;

View file

@ -7,18 +7,23 @@
*/
import { SavedObjectsErrorHelpers, SavedObjectsClientContract } from '@kbn/core/server';
import { TelemetrySavedObjectAttributes } from '.';
import { TELEMETRY_SAVED_OBJECT_TYPE, TELEMETRY_SAVED_OBJECT_ID } from './constants';
import type { TelemetrySavedObjectAttributes } from './types';
export async function updateTelemetrySavedObject(
savedObjectsClient: SavedObjectsClientContract,
savedObjectAttributes: TelemetrySavedObjectAttributes
) {
try {
return await savedObjectsClient.update('telemetry', 'telemetry', savedObjectAttributes);
return await savedObjectsClient.update(
TELEMETRY_SAVED_OBJECT_TYPE,
TELEMETRY_SAVED_OBJECT_ID,
savedObjectAttributes
);
} catch (err) {
if (SavedObjectsErrorHelpers.isNotFoundError(err)) {
return await savedObjectsClient.create('telemetry', savedObjectAttributes, {
id: 'telemetry',
return await savedObjectsClient.create(TELEMETRY_SAVED_OBJECT_TYPE, savedObjectAttributes, {
id: TELEMETRY_SAVED_OBJECT_ID,
overwrite: true,
});
}

View file

@ -6,24 +6,16 @@
* Side Public License, v 1.
*/
import { TelemetrySavedObject } from './types';
import type { TelemetrySavedObject } from '../saved_objects';
interface GetTelemetryAllowChangingOptInStatus {
configTelemetryAllowChangingOptInStatus: boolean;
telemetrySavedObject: TelemetrySavedObject;
telemetrySavedObject?: TelemetrySavedObject;
}
export function getTelemetryAllowChangingOptInStatus({
telemetrySavedObject,
configTelemetryAllowChangingOptInStatus,
}: GetTelemetryAllowChangingOptInStatus) {
if (!telemetrySavedObject) {
return configTelemetryAllowChangingOptInStatus;
}
if (typeof telemetrySavedObject.allowChangingOptInStatus === 'undefined') {
return configTelemetryAllowChangingOptInStatus;
}
return telemetrySavedObject.allowChangingOptInStatus;
return telemetrySavedObject?.allowChangingOptInStatus ?? configTelemetryAllowChangingOptInStatus;
}

View file

@ -10,7 +10,7 @@ import { getTelemetryFailureDetails } from './get_telemetry_failure_details';
describe('getTelemetryFailureDetails: get details about server usage fetcher failures', () => {
it('returns `failureCount: 0` and `failureVersion: undefined` when telemetry does not have any custom configs in saved Object', () => {
const telemetrySavedObject = null;
const telemetrySavedObject = {};
const failureDetails = getTelemetryFailureDetails({ telemetrySavedObject });
expect(failureDetails).toStrictEqual({
failureVersion: undefined,

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import { TelemetrySavedObject } from './types';
import type { TelemetrySavedObject } from '../saved_objects';
interface GetTelemetryFailureDetailsConfig {
telemetrySavedObject: TelemetrySavedObject;

View file

@ -24,7 +24,7 @@ describe('getNotifyUserAboutOptInDefault: get a flag that describes if the user
expect(
getNotifyUserAboutOptInDefault({
allowChangingOptInStatus: false,
telemetrySavedObject: null,
telemetrySavedObject: {},
telemetryOptedIn: false,
configTelemetryOptIn: false,
})
@ -33,7 +33,7 @@ describe('getNotifyUserAboutOptInDefault: get a flag that describes if the user
expect(
getNotifyUserAboutOptInDefault({
allowChangingOptInStatus: false,
telemetrySavedObject: null,
telemetrySavedObject: {},
telemetryOptedIn: true,
configTelemetryOptIn: true,
})

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import { TelemetrySavedObject } from './types';
import type { TelemetrySavedObject } from '../saved_objects';
interface NotifyOpts {
allowChangingOptInStatus: boolean;

View file

@ -7,7 +7,7 @@
*/
import { getTelemetryOptIn } from './get_telemetry_opt_in';
import { TelemetrySavedObject } from './types';
import type { TelemetrySavedObject } from '../saved_objects';
describe('getTelemetryOptIn', () => {
it('returns null when saved object not found', () => {
@ -20,16 +20,6 @@ describe('getTelemetryOptIn', () => {
expect(result).toBe(null);
});
it('returns false when saved object forbidden', () => {
const params = getCallGetTelemetryOptInParams({
savedObjectForbidden: true,
});
const result = callGetTelemetryOptIn(params);
expect(result).toBe(false);
});
it('returns null if enabled is null or undefined', () => {
for (const enabled of [null, undefined]) {
const params = getCallGetTelemetryOptInParams({
@ -112,7 +102,6 @@ describe('getTelemetryOptIn', () => {
interface CallGetTelemetryOptInParams {
savedObjectNotFound: boolean;
savedObjectForbidden: boolean;
lastVersionChecked?: string; // should be a string, but test with non-strings
currentKibanaVersion: string;
result?: boolean | null;
@ -149,12 +138,9 @@ function callGetTelemetryOptIn(params: CallGetTelemetryOptInParams) {
}
function getMockTelemetrySavedObject(params: CallGetTelemetryOptInParams): TelemetrySavedObject {
const { savedObjectNotFound, savedObjectForbidden } = params;
if (savedObjectForbidden) {
return false;
}
const { savedObjectNotFound } = params;
if (savedObjectNotFound) {
return null;
return {};
}
return {

View file

@ -8,7 +8,7 @@
import type SemVer from 'semver/classes/semver';
import semverParse from 'semver/functions/parse';
import { TelemetrySavedObject } from './types';
import type { TelemetrySavedObject } from '../saved_objects';
interface GetTelemetryOptInConfig {
telemetrySavedObject: TelemetrySavedObject;
@ -29,11 +29,7 @@ export const getTelemetryOptIn: GetTelemetryOptIn = ({
return configTelemetryOptIn;
}
if (telemetrySavedObject === false) {
return false;
}
if (telemetrySavedObject === null || typeof telemetrySavedObject.enabled !== 'boolean') {
if (typeof telemetrySavedObject.enabled !== 'boolean') {
return configTelemetryOptIn;
}
@ -42,6 +38,8 @@ export const getTelemetryOptIn: GetTelemetryOptIn = ({
// if enabled is true, return it
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):
// - if the Kibana version has changed by at least a minor version,
// return null to re-prompt.

View file

@ -7,7 +7,7 @@
*/
import { getTelemetrySendUsageFrom } from './get_telemetry_send_usage_from';
import { TelemetrySavedObject } from './types';
import type { TelemetrySavedObject } from '../saved_objects';
describe('getTelemetrySendUsageFrom', () => {
it('returns kibana.yml config when saved object not found', () => {
@ -21,20 +21,9 @@ describe('getTelemetrySendUsageFrom', () => {
expect(result).toBe('browser');
});
it('returns kibana.yml config when saved object forbidden', () => {
const params: CallGetTelemetryUsageFetcherParams = {
savedObjectForbidden: true,
configSendUsageFrom: 'browser',
};
const result = callGetTelemetryUsageFetcher(params);
expect(result).toBe('browser');
});
it('returns kibana.yml config when saved object sendUsageFrom is undefined', () => {
const params: CallGetTelemetryUsageFetcherParams = {
savedSendUsagefrom: undefined,
savedSendUsageFrom: undefined,
configSendUsageFrom: 'server',
};
@ -46,8 +35,7 @@ describe('getTelemetrySendUsageFrom', () => {
interface CallGetTelemetryUsageFetcherParams {
savedObjectNotFound?: boolean;
savedObjectForbidden?: boolean;
savedSendUsagefrom?: 'browser' | 'server';
savedSendUsageFrom?: 'browser' | 'server';
configSendUsageFrom: 'browser' | 'server';
}
@ -60,15 +48,12 @@ function callGetTelemetryUsageFetcher(params: CallGetTelemetryUsageFetcherParams
function getMockTelemetrySavedObject(
params: CallGetTelemetryUsageFetcherParams
): TelemetrySavedObject {
const { savedObjectNotFound, savedObjectForbidden } = params;
if (savedObjectForbidden) {
return false;
}
const { savedObjectNotFound } = params;
if (savedObjectNotFound) {
return null;
return {};
}
return {
sendUsageFrom: params.savedSendUsagefrom,
sendUsageFrom: params.savedSendUsageFrom,
};
}

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import { TelemetrySavedObject } from './types';
import type { TelemetrySavedObject } from '../saved_objects';
interface GetTelemetryUsageFetcherConfig {
configTelemetrySendUsageFrom: 'browser' | 'server';

View file

@ -0,0 +1,16 @@
/*
* 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 { getTelemetryAllowChangingOptInStatus } from './get_telemetry_allow_changing_opt_in_status';
export {
getTelemetryFailureDetails,
type TelemetryFailureDetails,
} from './get_telemetry_failure_details';
export { getNotifyUserAboutOptInDefault } from './get_telemetry_notify_user_about_optin_default';
export { getTelemetryOptIn } from './get_telemetry_opt_in';
export { getTelemetrySendUsageFrom } from './get_telemetry_send_usage_from';

View file

@ -6,9 +6,12 @@
* Side Public License, v 1.
*/
export default function ({ loadTestFile }) {
import { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ loadTestFile }: FtrProviderContext) {
describe('Telemetry', () => {
loadTestFile(require.resolve('./opt_in'));
loadTestFile(require.resolve('./telemetry_config'));
loadTestFile(require.resolve('./telemetry_last_reported'));
loadTestFile(require.resolve('./telemetry_optin_notice_seen'));
});

View file

@ -7,87 +7,87 @@
*/
import expect from '@kbn/expect';
import { Client } from '@elastic/elasticsearch';
import { TelemetrySavedObjectAttributes } from '@kbn/telemetry-plugin/server/telemetry_repository';
import { TelemetrySavedObjectAttributes } from '@kbn/telemetry-plugin/server/saved_objects';
import SuperTest from 'supertest';
import { FtrProviderContext } from '../../ftr_provider_context';
export default function optInTest({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const kibanaServer = getService('kibanaServer');
const esArchiver = getService('esArchiver');
const esClient: Client = getService('es');
describe('/api/telemetry/v2/optIn API', () => {
let defaultAttributes: TelemetrySavedObjectAttributes;
let kibanaVersion: any;
let kibanaVersion: string;
before(async () => {
await esArchiver.emptyKibanaIndex();
const kibanaVersionAccessor = kibanaServer.version;
kibanaVersion = await kibanaVersionAccessor.get();
defaultAttributes =
(await getSavedObjectAttributes(supertest).catch((err) => {
if (err.message === 'expected 200 "OK", got 404 "Not Found"') {
return null;
}
throw err;
})) || {};
defaultAttributes = await getSavedObjectAttributes(esClient);
expect(typeof kibanaVersion).to.eql('string');
expect(kibanaVersion.length).to.be.greaterThan(0);
});
afterEach(async () => {
await updateSavedObjectAttributes(supertest, defaultAttributes);
await updateSavedObjectAttributes(esClient, defaultAttributes);
});
it('should support sending false with allowChangingOptInStatus true', async () => {
await updateSavedObjectAttributes(supertest, {
...defaultAttributes,
await updateSavedObjectAttributes(esClient, {
allowChangingOptInStatus: true,
});
await postTelemetryV2Optin(supertest, false, 200);
const { enabled, lastVersionChecked } = await getSavedObjectAttributes(supertest);
await postTelemetryV2OptIn(supertest, false, 200);
const { enabled, lastVersionChecked } = await getSavedObjectAttributes(esClient);
expect(enabled).to.be(false);
expect(lastVersionChecked).to.be(kibanaVersion);
});
it('should support sending true with allowChangingOptInStatus true', async () => {
await updateSavedObjectAttributes(supertest, {
await updateSavedObjectAttributes(esClient, {
...defaultAttributes,
allowChangingOptInStatus: true,
});
await postTelemetryV2Optin(supertest, true, 200);
const { enabled, lastVersionChecked } = await getSavedObjectAttributes(supertest);
await postTelemetryV2OptIn(supertest, true, 200);
const { enabled, lastVersionChecked } = await getSavedObjectAttributes(esClient);
expect(enabled).to.be(true);
expect(lastVersionChecked).to.be(kibanaVersion);
});
it('should not support sending false with allowChangingOptInStatus false', async () => {
await updateSavedObjectAttributes(supertest, {
await updateSavedObjectAttributes(esClient, {
...defaultAttributes,
allowChangingOptInStatus: false,
});
await postTelemetryV2Optin(supertest, false, 400);
await postTelemetryV2OptIn(supertest, false, 400);
});
it('should not support sending true with allowChangingOptInStatus false', async () => {
await updateSavedObjectAttributes(supertest, {
await updateSavedObjectAttributes(esClient, {
...defaultAttributes,
allowChangingOptInStatus: false,
});
await postTelemetryV2Optin(supertest, true, 400);
await postTelemetryV2OptIn(supertest, true, 400);
});
it('should not support sending null', async () => {
await postTelemetryV2Optin(supertest, null, 400);
await postTelemetryV2OptIn(supertest, null, 400);
});
it('should not support sending junk', async () => {
await postTelemetryV2Optin(supertest, 42, 400);
await postTelemetryV2OptIn(supertest, 42, 400);
});
});
}
async function postTelemetryV2Optin(supertest: any, value: any, statusCode: number): Promise<any> {
async function postTelemetryV2OptIn(
supertest: SuperTest.SuperTest<SuperTest.Test>,
value: unknown,
statusCode: number
): Promise<any> {
const { body } = await supertest
.post('/api/telemetry/v2/optIn')
.set('kbn-xsrf', 'xxx')
@ -98,18 +98,29 @@ async function postTelemetryV2Optin(supertest: any, value: any, statusCode: numb
}
async function updateSavedObjectAttributes(
supertest: any,
es: Client,
attributes: TelemetrySavedObjectAttributes
): Promise<any> {
return await supertest
.post('/api/saved_objects/telemetry/telemetry')
.query({ overwrite: true })
.set('kbn-xsrf', 'xxx')
.send({ attributes })
.expect(200);
): Promise<void> {
// Directly writing to the `.kibana` index because the SO type now is hidden, meaning it's not exposed via the SO HTTP API
await es.update({
index: '.kibana',
id: 'telemetry:telemetry',
doc: {
telemetry: attributes,
// there are many missing fields in the SO, hopefully it doesn't break Kibana
},
doc_as_upsert: true,
});
}
async function getSavedObjectAttributes(supertest: any): Promise<TelemetrySavedObjectAttributes> {
const { body } = await supertest.get('/api/saved_objects/telemetry/telemetry').expect(200);
return body.attributes;
async function getSavedObjectAttributes(es: Client): Promise<TelemetrySavedObjectAttributes> {
// Directly writing to the `.kibana` index because the SO type now is hidden, meaning it's not exposed via the SO HTTP API
const { _source: body } = await es.get<{ telemetry: TelemetrySavedObjectAttributes }>(
{
index: '.kibana',
id: 'telemetry:telemetry',
},
{ ignore: [404] }
);
return body?.telemetry || {};
}

View file

@ -0,0 +1,52 @@
/*
* 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 { FtrProviderContext } from '../../ftr_provider_context';
export default function optInTest({ getService }: FtrProviderContext) {
const client = getService('es');
const supertest = getService('supertest');
describe('/api/telemetry/v2/config API Telemetry config', () => {
before(async () => {
await client.delete(
{
index: '.kibana',
id: 'telemetry:telemetry',
},
{ ignore: [404] }
);
});
it('GET should get the default config', async () => {
await supertest.get('/api/telemetry/v2/config').set('kbn-xsrf', 'xxx').expect(200, {
allowChangingOptInStatus: true,
optIn: false, // the config.js for this FTR sets it to `false`
sendUsageFrom: 'server',
telemetryNotifyUserAboutOptInDefault: false, // it's not opted-in, so we don't notify about opt-in??
});
});
it('GET should get when opted-in', async () => {
// Opt-in
await supertest
.post('/api/telemetry/v2/optIn')
.set('kbn-xsrf', 'xxx')
.send({ enabled: true })
.expect(200);
await supertest.get('/api/telemetry/v2/config').set('kbn-xsrf', 'xxx').expect(200, {
allowChangingOptInStatus: true,
optIn: true,
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,
});
});
});
}

View file

@ -25,10 +25,7 @@ export default function optInTest({ getService }: FtrProviderContext) {
});
it('GET should return undefined when there is no stored telemetry.lastReported value', async () => {
await supertest
.get('/api/telemetry/v2/last_reported')
.set('kbn-xsrf', 'xxx')
.expect(200, { lastReported: undefined });
await supertest.get('/api/telemetry/v2/last_reported').set('kbn-xsrf', 'xxx').expect(200, {});
});
it('PUT should update telemetry.lastReported to now', async () => {

View file

@ -6,86 +6,87 @@
*/
import expect from '@kbn/expect';
import { Client } from '@elastic/elasticsearch';
import { TelemetrySavedObjectAttributes } from '@kbn/telemetry-plugin/server/telemetry_repository';
import { TelemetrySavedObjectAttributes } from '@kbn/telemetry-plugin/server/saved_objects';
import SuperTest from 'supertest';
import { FtrProviderContext } from '../../ftr_provider_context';
export default function optInTest({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const kibanaServer = getService('kibanaServer');
const esArchiver = getService('esArchiver');
const esClient: Client = getService('es');
describe('/api/telemetry/v2/optIn API', () => {
let defaultAttributes: TelemetrySavedObjectAttributes;
let kibanaVersion: any;
let kibanaVersion: string;
before(async () => {
await esArchiver.emptyKibanaIndex();
const kibanaVersionAccessor = kibanaServer.version;
kibanaVersion = await kibanaVersionAccessor.get();
defaultAttributes =
(await getSavedObjectAttributes(supertest).catch((err) => {
if (err.message === 'expected 200 "OK", got 404 "Not Found"') {
return null;
}
throw err;
})) || {};
defaultAttributes = await getSavedObjectAttributes(esClient);
expect(typeof kibanaVersion).to.eql('string');
expect(kibanaVersion.length).to.be.greaterThan(0);
});
afterEach(async () => {
await updateSavedObjectAttributes(supertest, defaultAttributes);
await updateSavedObjectAttributes(esClient, defaultAttributes);
});
it('should support sending false with allowChangingOptInStatus true', async () => {
await updateSavedObjectAttributes(supertest, {
...defaultAttributes,
await updateSavedObjectAttributes(esClient, {
allowChangingOptInStatus: true,
});
await postTelemetryV2Optin(supertest, false, 200);
const { enabled, lastVersionChecked } = await getSavedObjectAttributes(supertest);
await postTelemetryV2OptIn(supertest, false, 200);
const { enabled, lastVersionChecked } = await getSavedObjectAttributes(esClient);
expect(enabled).to.be(false);
expect(lastVersionChecked).to.be(kibanaVersion);
});
it('should support sending true with allowChangingOptInStatus true', async () => {
await updateSavedObjectAttributes(supertest, {
await updateSavedObjectAttributes(esClient, {
...defaultAttributes,
allowChangingOptInStatus: true,
});
await postTelemetryV2Optin(supertest, true, 200);
const { enabled, lastVersionChecked } = await getSavedObjectAttributes(supertest);
await postTelemetryV2OptIn(supertest, true, 200);
const { enabled, lastVersionChecked } = await getSavedObjectAttributes(esClient);
expect(enabled).to.be(true);
expect(lastVersionChecked).to.be(kibanaVersion);
});
it('should not support sending false with allowChangingOptInStatus false', async () => {
await updateSavedObjectAttributes(supertest, {
await updateSavedObjectAttributes(esClient, {
...defaultAttributes,
allowChangingOptInStatus: false,
});
await postTelemetryV2Optin(supertest, false, 400);
await postTelemetryV2OptIn(supertest, false, 400);
});
it('should not support sending true with allowChangingOptInStatus false', async () => {
await updateSavedObjectAttributes(supertest, {
await updateSavedObjectAttributes(esClient, {
...defaultAttributes,
allowChangingOptInStatus: false,
});
await postTelemetryV2Optin(supertest, true, 400);
await postTelemetryV2OptIn(supertest, true, 400);
});
it('should not support sending null', async () => {
await postTelemetryV2Optin(supertest, null, 400);
await postTelemetryV2OptIn(supertest, null, 400);
});
it('should not support sending junk', async () => {
await postTelemetryV2Optin(supertest, 42, 400);
await postTelemetryV2OptIn(supertest, 42, 400);
});
});
}
async function postTelemetryV2Optin(supertest: any, value: any, statusCode: number): Promise<any> {
async function postTelemetryV2OptIn(
supertest: SuperTest.SuperTest<SuperTest.Test>,
value: unknown,
statusCode: number
): Promise<any> {
const { body } = await supertest
.post('/api/telemetry/v2/optIn')
.set('kbn-xsrf', 'xxx')
@ -96,18 +97,29 @@ async function postTelemetryV2Optin(supertest: any, value: any, statusCode: numb
}
async function updateSavedObjectAttributes(
supertest: any,
es: Client,
attributes: TelemetrySavedObjectAttributes
): Promise<any> {
return await supertest
.post('/api/saved_objects/telemetry/telemetry')
.query({ overwrite: true })
.set('kbn-xsrf', 'xxx')
.send({ attributes })
.expect(200);
): Promise<void> {
// Directly writing to the `.kibana` index because the SO type now is hidden, meaning it's not exposed via the SO HTTP API
await es.update({
index: '.kibana',
id: 'telemetry:telemetry',
doc: {
telemetry: attributes,
// there are many missing fields in the SO, hopefully it doesn't break Kibana
},
doc_as_upsert: true,
});
}
async function getSavedObjectAttributes(supertest: any): Promise<TelemetrySavedObjectAttributes> {
const { body } = await supertest.get('/api/saved_objects/telemetry/telemetry').expect(200);
return body.attributes;
async function getSavedObjectAttributes(es: Client): Promise<TelemetrySavedObjectAttributes> {
// Directly writing to the `.kibana` index because the SO type now is hidden, meaning it's not exposed via the SO HTTP API
const { _source: body } = await es.get<{ telemetry: TelemetrySavedObjectAttributes }>(
{
index: '.kibana',
id: 'telemetry:telemetry',
},
{ ignore: [404] }
);
return body?.telemetry || {};
}