[Telemetry] Use server's lastReported on the browser (#121656) (#122525)

(cherry picked from commit 2d1755439d)
This commit is contained in:
Alejandro Fernández Haro 2022-01-10 13:17:27 +01:00 committed by GitHub
parent dc12009833
commit 927cc4d48e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 343 additions and 74 deletions

View file

@ -442,6 +442,7 @@ exports[`home isNewKibanaInstance should set isNewKibanaInstance to true when th
"userCanChangeSettings": true,
},
"fetchExample": [Function],
"fetchLastReported": [Function],
"fetchTelemetry": [Function],
"getCanChangeOptInStatus": [Function],
"getIsOptedIn": [Function],
@ -492,6 +493,7 @@ exports[`home isNewKibanaInstance should set isNewKibanaInstance to true when th
"reportOptInStatusChange": true,
"setOptIn": [Function],
"setUserHasSeenNotice": [Function],
"updateLastReported": [Function],
"updatedConfig": undefined,
},
},
@ -509,6 +511,7 @@ exports[`home isNewKibanaInstance should set isNewKibanaInstance to true when th
"userCanChangeSettings": true,
},
"fetchExample": [Function],
"fetchLastReported": [Function],
"fetchTelemetry": [Function],
"getCanChangeOptInStatus": [Function],
"getIsOptedIn": [Function],
@ -559,6 +562,7 @@ exports[`home isNewKibanaInstance should set isNewKibanaInstance to true when th
"reportOptInStatusChange": true,
"setOptIn": [Function],
"setUserHasSeenNotice": [Function],
"updateLastReported": [Function],
"updatedConfig": undefined,
},
}

View file

@ -0,0 +1,67 @@
/*
* 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 { REPORT_INTERVAL_MS } from './constants';
import { isReportIntervalExpired } from './is_report_interval_expired';
describe('isReportIntervalExpired', () => {
test('true when undefined', () => {
expect(isReportIntervalExpired(undefined)).toBe(true);
expect(isReportIntervalExpired(void 0)).toBe(true);
});
describe('true when NaN', () => {
test('NaN', () => {
expect(isReportIntervalExpired(NaN)).toBe(true);
});
test('parseInt(undefined)', () => {
expect(isReportIntervalExpired(parseInt(undefined as unknown as string, 10))).toBe(true);
});
test('parseInt(null)', () => {
expect(isReportIntervalExpired(parseInt(null as unknown as string, 10))).toBe(true);
});
test('parseInt("")', () => {
expect(isReportIntervalExpired(parseInt('', 10))).toBe(true);
});
test('empty string', () => {
expect(isReportIntervalExpired('' as unknown as number)).toBe(true);
});
test('malformed string', () => {
expect(isReportIntervalExpired(`random_malformed_string` as unknown as number)).toBe(true);
});
test('other object', () => {
expect(isReportIntervalExpired({} as unknown as number)).toBe(true);
});
});
test('true when 0', () => {
expect(isReportIntervalExpired(0)).toBe(true);
});
test('true when actually expired', () => {
expect(isReportIntervalExpired(Date.now() - REPORT_INTERVAL_MS - 1000)).toBe(true);
});
test('false when close but not yet', () => {
expect(isReportIntervalExpired(Date.now() - REPORT_INTERVAL_MS + 1000)).toBe(false);
});
test('false when date in the future', () => {
expect(isReportIntervalExpired(Date.now() + 1000)).toBe(false);
});
test('false when date is now', () => {
expect(isReportIntervalExpired(Date.now())).toBe(false);
});
});

View file

@ -0,0 +1,19 @@
/*
* 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 { REPORT_INTERVAL_MS } from './constants';
/**
* The report is considered expired if:
* - `lastReportAt` does not exist, is NaN or `REPORT_INTERVAL_MS` have passed ever since.
* @param lastReportAt
* @returns `true` if the report interval is considered expired
*/
export function isReportIntervalExpired(lastReportAt: number | undefined) {
return !lastReportAt || isNaN(lastReportAt) || Date.now() - lastReportAt > REPORT_INTERVAL_MS;
}

View file

@ -42,87 +42,98 @@ describe('TelemetrySender', () => {
});
it('uses lastReport if set', () => {
const lastReport = `${Date.now()}`;
mockLocalStorage.getItem.mockReturnValueOnce(JSON.stringify({ lastReport }));
const lastReport = Date.now();
mockLocalStorage.getItem.mockReturnValueOnce(JSON.stringify({ lastReport: `${lastReport}` }));
const telemetryService = mockTelemetryService();
const telemetrySender = new TelemetrySender(telemetryService);
expect(telemetrySender['lastReported']).toBe(lastReport);
});
});
describe('saveToBrowser', () => {
it('uses lastReport', () => {
const lastReport = `${Date.now()}`;
describe('updateLastReported', () => {
it('stores the new lastReported value in the storage', () => {
const lastReport = Date.now();
const telemetryService = mockTelemetryService();
const telemetrySender = new TelemetrySender(telemetryService);
telemetrySender['lastReported'] = lastReport;
telemetrySender['saveToBrowser']();
telemetrySender['updateLastReported'](lastReport);
expect(mockLocalStorage.setItem).toHaveBeenCalledTimes(1);
expect(mockLocalStorage.setItem).toHaveBeenCalledWith(
LOCALSTORAGE_KEY,
JSON.stringify({ lastReport })
JSON.stringify({ lastReport: `${lastReport}` })
);
});
});
describe('shouldSendReport', () => {
it('returns false whenever optIn is false', () => {
it('returns false whenever optIn is false', async () => {
const telemetryService = mockTelemetryService();
telemetryService.getIsOptedIn = jest.fn().mockReturnValue(false);
const telemetrySender = new TelemetrySender(telemetryService);
const shouldSendReport = telemetrySender['shouldSendReport']();
const shouldSendReport = await telemetrySender['shouldSendReport']();
expect(telemetryService.getIsOptedIn).toBeCalledTimes(1);
expect(shouldSendReport).toBe(false);
});
it('returns true if lastReported is undefined', () => {
it('returns true if lastReported is undefined (both local and global)', async () => {
const telemetryService = mockTelemetryService();
telemetryService.getIsOptedIn = jest.fn().mockReturnValue(true);
telemetryService.fetchLastReported = jest.fn().mockResolvedValue(undefined);
const telemetrySender = new TelemetrySender(telemetryService);
const shouldSendReport = telemetrySender['shouldSendReport']();
const shouldSendReport = await telemetrySender['shouldSendReport']();
expect(telemetrySender['lastReported']).toBeUndefined();
expect(shouldSendReport).toBe(true);
expect(telemetryService.fetchLastReported).toHaveBeenCalledTimes(1);
});
it('returns true if lastReported passed REPORT_INTERVAL_MS', () => {
it('returns true if lastReported passed REPORT_INTERVAL_MS', async () => {
const lastReported = Date.now() - (REPORT_INTERVAL_MS + 1000);
const telemetryService = mockTelemetryService();
telemetryService.getIsOptedIn = jest.fn().mockReturnValue(true);
const telemetrySender = new TelemetrySender(telemetryService);
telemetrySender['lastReported'] = `${lastReported}`;
const shouldSendReport = telemetrySender['shouldSendReport']();
telemetrySender['lastReported'] = lastReported;
const shouldSendReport = await telemetrySender['shouldSendReport']();
expect(shouldSendReport).toBe(true);
});
it('returns false if lastReported is within REPORT_INTERVAL_MS', () => {
it('returns false if local lastReported is within REPORT_INTERVAL_MS', async () => {
const lastReported = Date.now() + 1000;
const telemetryService = mockTelemetryService();
telemetryService.getIsOptedIn = jest.fn().mockReturnValue(true);
const telemetrySender = new TelemetrySender(telemetryService);
telemetrySender['lastReported'] = `${lastReported}`;
const shouldSendReport = telemetrySender['shouldSendReport']();
telemetrySender['lastReported'] = lastReported;
const shouldSendReport = await telemetrySender['shouldSendReport']();
expect(shouldSendReport).toBe(false);
});
it('returns true if lastReported is malformed', () => {
it('returns false if local lastReported is expired but the remote is within REPORT_INTERVAL_MS', async () => {
const telemetryService = mockTelemetryService();
telemetryService.getIsOptedIn = jest.fn().mockReturnValue(true);
telemetryService.fetchLastReported = jest.fn().mockResolvedValue(Date.now() + 1000);
const telemetrySender = new TelemetrySender(telemetryService);
telemetrySender['lastReported'] = Date.now() - (REPORT_INTERVAL_MS + 1000);
const shouldSendReport = await telemetrySender['shouldSendReport']();
expect(shouldSendReport).toBe(false);
});
it('returns true if lastReported is malformed', async () => {
const telemetryService = mockTelemetryService();
telemetryService.getIsOptedIn = jest.fn().mockReturnValue(true);
const telemetrySender = new TelemetrySender(telemetryService);
telemetrySender['lastReported'] = `random_malformed_string`;
const shouldSendReport = telemetrySender['shouldSendReport']();
telemetrySender['lastReported'] = `random_malformed_string` as unknown as number;
const shouldSendReport = await telemetrySender['shouldSendReport']();
expect(shouldSendReport).toBe(true);
});
it('returns false if we are in screenshot mode', () => {
it('returns false if we are in screenshot mode', async () => {
const telemetryService = mockTelemetryService({ isScreenshotMode: true });
telemetryService.getIsOptedIn = jest.fn().mockReturnValue(false);
const telemetrySender = new TelemetrySender(telemetryService);
const shouldSendReport = telemetrySender['shouldSendReport']();
const shouldSendReport = await telemetrySender['shouldSendReport']();
expect(telemetryService.getIsOptedIn).toBeCalledTimes(0);
expect(shouldSendReport).toBe(false);
@ -165,13 +176,14 @@ describe('TelemetrySender', () => {
const telemetrySender = new TelemetrySender(telemetryService);
telemetrySender['shouldSendReport'] = jest.fn().mockReturnValue(true);
telemetrySender['sendUsageData'] = jest.fn().mockReturnValue(true);
telemetrySender['saveToBrowser'] = jest.fn();
telemetrySender['lastReported'] = `${lastReported}`;
telemetrySender['updateLastReported'] = jest.fn().mockImplementation((value) => {
expect(value).not.toBe(lastReported);
});
telemetrySender['lastReported'] = lastReported;
await telemetrySender['sendIfDue']();
expect(telemetrySender['lastReported']).not.toBe(lastReported);
expect(telemetrySender['saveToBrowser']).toBeCalledTimes(1);
expect(telemetrySender['updateLastReported']).toBeCalledTimes(1);
expect(telemetrySender['retryCount']).toEqual(0);
expect(telemetrySender['sendUsageData']).toHaveBeenCalledTimes(1);
});
@ -181,7 +193,7 @@ describe('TelemetrySender', () => {
const telemetrySender = new TelemetrySender(telemetryService);
telemetrySender['shouldSendReport'] = jest.fn().mockReturnValue(true);
telemetrySender['sendUsageData'] = jest.fn();
telemetrySender['saveToBrowser'] = jest.fn();
telemetrySender['updateLastReported'] = jest.fn();
telemetrySender['retryCount'] = 9;
await telemetrySender['sendIfDue']();
@ -272,7 +284,7 @@ describe('TelemetrySender', () => {
telemetryService.getTelemetryUrl = jest.fn().mockReturnValue(mockTelemetryUrl);
telemetryService.fetchTelemetry = jest.fn().mockReturnValue(mockTelemetryPayload);
telemetrySender['shouldSendReport'] = jest.fn().mockReturnValue(true);
telemetrySender['saveToBrowser'] = jest.fn();
telemetrySender['updateLastReported'] = jest.fn();
await telemetrySender['sendUsageData']();

View file

@ -6,18 +6,15 @@
* Side Public License, v 1.
*/
import {
REPORT_INTERVAL_MS,
LOCALSTORAGE_KEY,
PAYLOAD_CONTENT_ENCODING,
} from '../../common/constants';
import { LOCALSTORAGE_KEY, PAYLOAD_CONTENT_ENCODING } from '../../common/constants';
import { TelemetryService } from './telemetry_service';
import { Storage } from '../../../kibana_utils/public';
import type { EncryptedTelemetryPayload } from '../../common/types';
import { isReportIntervalExpired } from '../../common/is_report_interval_expired';
export class TelemetrySender {
private readonly telemetryService: TelemetryService;
private lastReported?: string;
private lastReported?: number;
private readonly storage: Storage;
private intervalId: number = 0; // setInterval returns a positive integer, 0 means no interval is set
private retryCount: number = 0;
@ -32,38 +29,56 @@ export class TelemetrySender {
const attributes = this.storage.get(LOCALSTORAGE_KEY);
if (attributes) {
this.lastReported = attributes.lastReport;
this.lastReported = parseInt(attributes.lastReport, 10);
}
}
private saveToBrowser = () => {
private updateLastReported = (lastReported: number) => {
this.lastReported = lastReported;
// we are the only code that manipulates this key, so it's safe to blindly overwrite the whole object
this.storage.set(LOCALSTORAGE_KEY, { lastReport: this.lastReported });
this.storage.set(LOCALSTORAGE_KEY, { lastReport: `${this.lastReported}` });
};
private shouldSendReport = (): boolean => {
/**
* Using the local and SO's `lastReported` values, it decides whether the last report should be considered as expired
* @returns `true` if a new report should be generated. `false` otherwise.
*/
private isReportDue = async (): Promise<boolean> => {
// Try to decide with the local `lastReported` to avoid querying the server
if (!isReportIntervalExpired(this.lastReported)) {
// If it is not expired locally, there's no need to send it again yet.
return false;
}
// Double-check with the server's value
const globalLastReported = await this.telemetryService.fetchLastReported();
if (globalLastReported) {
// Update the local value to avoid repetitions of this request (it was already expired, so it doesn't really matter if the server's value is older)
this.updateLastReported(globalLastReported);
}
return isReportIntervalExpired(globalLastReported);
};
/**
* Using configuration and the lastReported dates, it decides whether a new telemetry report should be sent.
* @returns `true` if a new report should be sent. `false` otherwise.
*/
private shouldSendReport = async (): Promise<boolean> => {
if (this.telemetryService.canSendTelemetry()) {
if (!this.lastReported) {
return true;
}
// returns NaN for any malformed or unset (null/undefined) value
const lastReported = parseInt(this.lastReported, 10);
// If it's been a day since we last sent telemetry
if (isNaN(lastReported) || Date.now() - lastReported > REPORT_INTERVAL_MS) {
return true;
}
return await this.isReportDue();
}
return false;
};
private sendIfDue = async (): Promise<void> => {
if (!this.shouldSendReport()) {
if (!(await this.shouldSendReport())) {
return;
}
// optimistically update the report date and reset the retry counter for a new time report interval window
this.lastReported = `${Date.now()}`;
this.saveToBrowser();
this.updateLastReported(Date.now());
this.retryCount = 0;
await this.sendUsageData();
};
@ -89,6 +104,8 @@ export class TelemetrySender {
})
)
);
await this.telemetryService.updateLastReported().catch(() => {}); // Let's catch the error. Worst-case scenario another Telemetry report will be generated somewhere else.
} catch (err) {
// ignore err and try again but after a longer wait period.
this.retryCount = this.retryCount + 1;

View file

@ -138,6 +138,17 @@ export class TelemetryService {
return !this.isScreenshotMode && this.getIsOptedIn();
};
public fetchLastReported = async (): Promise<number | undefined> => {
const response = await this.http.get<{ lastReported?: number }>(
'/api/telemetry/v2/last_reported'
);
return response?.lastReported;
};
public updateLastReported = async (): Promise<number | undefined> => {
return this.http.put('/api/telemetry/v2/last_reported');
};
/** Fetches an unencrypted telemetry payload so we can show it to the user **/
public fetchExample = async (): Promise<UnencryptedTelemetryPayload> => {
return await this.fetchTelemetry({ unencrypted: true, refreshCache: true });

View file

@ -24,9 +24,10 @@ import {
getTelemetryFailureDetails,
} from '../common/telemetry_config';
import { getTelemetrySavedObject, updateTelemetrySavedObject } from './telemetry_repository';
import { REPORT_INTERVAL_MS, PAYLOAD_CONTENT_ENCODING } from '../common/constants';
import { PAYLOAD_CONTENT_ENCODING } from '../common/constants';
import type { EncryptedTelemetryPayload } from '../common/types';
import { TelemetryConfigType } from './config';
import { isReportIntervalExpired } from '../common/is_report_interval_expired';
export interface FetcherTaskDepsStart {
telemetryCollectionManager: TelemetryCollectionManagerPluginStart;
@ -39,6 +40,7 @@ interface TelemetryConfig {
failureCount: number;
failureVersion: string | undefined;
currentVersion: string;
lastReported: number | undefined;
}
export class FetcherTask {
@ -59,10 +61,7 @@ export class FetcherTask {
this.logger = initializerContext.logger.get('fetcher');
}
public start(
{ savedObjects, elasticsearch }: CoreStart,
{ telemetryCollectionManager }: FetcherTaskDepsStart
) {
public start({ savedObjects }: CoreStart, { telemetryCollectionManager }: FetcherTaskDepsStart) {
this.internalRepository = new SavedObjectsClient(savedObjects.createInternalRepository());
this.telemetryCollectionManager = telemetryCollectionManager;
@ -148,6 +147,7 @@ export class FetcherTask {
failureCount,
failureVersion,
currentVersion: currentKibanaVersion,
lastReported: telemetrySavedObject ? telemetrySavedObject.lastReported : void 0,
};
}
@ -178,13 +178,16 @@ export class FetcherTask {
failureCount,
failureVersion,
currentVersion,
lastReported,
}: TelemetryConfig) {
if (failureCount > 2 && failureVersion === currentVersion) {
return false;
}
if (telemetryOptIn && telemetrySendUsageFrom === 'server') {
if (!this.lastReported || Date.now() - this.lastReported > REPORT_INTERVAL_MS) {
// Check both: in-memory and SO-driven value.
// This will avoid the server retrying over and over when it has issues with storing the state in the SO.
if (isReportIntervalExpired(this.lastReported) && isReportIntervalExpired(lastReported)) {
return true;
}
}

View file

@ -7,22 +7,23 @@
*/
import { URL } from 'url';
import { Observable } from 'rxjs';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import {
import type { Observable } from 'rxjs';
import { ReplaySubject } from 'rxjs';
import { take } from 'rxjs/operators';
import type { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import type {
TelemetryCollectionManagerPluginSetup,
TelemetryCollectionManagerPluginStart,
} from 'src/plugins/telemetry_collection_manager/server';
import { take } from 'rxjs/operators';
import {
import type {
CoreSetup,
PluginInitializerContext,
ISavedObjectsRepository,
CoreStart,
SavedObjectsClient,
Plugin,
Logger,
} from '../../../core/server';
} from 'src/core/server';
import { SavedObjectsClient } from '../../../core/server';
import { registerRoutes } from './routes';
import { registerCollection } from './telemetry_collection';
import {
@ -77,7 +78,17 @@ export class TelemetryPlugin implements Plugin<TelemetryPluginSetup, TelemetryPl
/**
* @private Used to mark the completion of the old UI Settings migration
*/
private savedObjectsClient?: ISavedObjectsRepository;
private savedObjectsInternalRepository?: ISavedObjectsRepository;
/**
* @private
* Used to interact with the Telemetry Saved Object.
* Some users may not have access to the document but some queries
* are still relevant to them like fetching when was the last time it was reported.
*
* Using the internal client in all cases ensures the permissions to interact the document.
*/
private savedObjectsInternalClient$ = new ReplaySubject<SavedObjectsClient>(1);
constructor(initializerContext: PluginInitializerContext<TelemetryConfigType>) {
this.logger = initializerContext.logger.get();
@ -107,6 +118,7 @@ export class TelemetryPlugin implements Plugin<TelemetryPluginSetup, TelemetryPl
logger: this.logger,
router,
telemetryCollectionManager,
savedObjectsInternalClient$: this.savedObjectsInternalClient$,
});
this.registerMappings((opts) => savedObjects.registerType(opts));
@ -128,13 +140,16 @@ export class TelemetryPlugin implements Plugin<TelemetryPluginSetup, TelemetryPl
public start(core: CoreStart, { telemetryCollectionManager }: TelemetryPluginsDepsStart) {
const { savedObjects } = core;
const savedObjectsInternalRepository = savedObjects.createInternalRepository();
this.savedObjectsClient = savedObjectsInternalRepository;
this.savedObjectsInternalRepository = savedObjectsInternalRepository;
this.savedObjectsInternalClient$.next(new SavedObjectsClient(savedObjectsInternalRepository));
this.startFetcher(core, telemetryCollectionManager);
return {
getIsOptedIn: async () => {
const internalRepository = new SavedObjectsClient(savedObjectsInternalRepository);
const telemetrySavedObject = await getTelemetrySavedObject(internalRepository);
const internalRepositoryClient = await this.savedObjectsInternalClient$
.pipe(take(1))
.toPromise();
const telemetrySavedObject = await getTelemetrySavedObject(internalRepositoryClient);
const config = await this.config$.pipe(take(1)).toPromise();
const allowChangingOptInStatus = config.allowChangingOptInStatus;
@ -197,7 +212,7 @@ export class TelemetryPlugin implements Plugin<TelemetryPluginSetup, TelemetryPl
}
private registerUsageCollectors(usageCollection: UsageCollectionSetup) {
const getSavedObjectsClient = () => this.savedObjectsClient;
const getSavedObjectsClient = () => this.savedObjectsInternalRepository;
registerTelemetryPluginUsageCollector(usageCollection, {
currentKibanaVersion: this.currentKibanaVersion,

View file

@ -6,14 +6,15 @@
* Side Public License, v 1.
*/
import { Observable } from 'rxjs';
import { IRouter, Logger } from 'kibana/server';
import { TelemetryCollectionManagerPluginSetup } from 'src/plugins/telemetry_collection_manager/server';
import type { Observable } from 'rxjs';
import type { IRouter, Logger, SavedObjectsClient } from 'kibana/server';
import type { TelemetryCollectionManagerPluginSetup } from 'src/plugins/telemetry_collection_manager/server';
import { registerTelemetryOptInRoutes } from './telemetry_opt_in';
import { registerTelemetryUsageStatsRoutes } from './telemetry_usage_stats';
import { registerTelemetryOptInStatsRoutes } from './telemetry_opt_in_stats';
import { registerTelemetryUserHasSeenNotice } from './telemetry_user_has_seen_notice';
import { TelemetryConfigType } from '../config';
import type { TelemetryConfigType } from '../config';
import { registerTelemetryLastReported } from './telemetry_last_reported';
interface RegisterRoutesParams {
isDev: boolean;
@ -22,12 +23,14 @@ interface RegisterRoutesParams {
currentKibanaVersion: string;
router: IRouter;
telemetryCollectionManager: TelemetryCollectionManagerPluginSetup;
savedObjectsInternalClient$: Observable<SavedObjectsClient>;
}
export function registerRoutes(options: RegisterRoutesParams) {
const { isDev, telemetryCollectionManager, router } = options;
const { isDev, telemetryCollectionManager, router, savedObjectsInternalClient$ } = options;
registerTelemetryOptInRoutes(options);
registerTelemetryUsageStatsRoutes(router, telemetryCollectionManager, isDev);
registerTelemetryOptInStatsRoutes(router, telemetryCollectionManager);
registerTelemetryUserHasSeenNotice(router);
registerTelemetryLastReported(router, savedObjectsInternalClient$);
}

View file

@ -0,0 +1,55 @@
/*
* 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 { IRouter, SavedObjectsClient } from 'kibana/server';
import type { Observable } from 'rxjs';
import { take } from 'rxjs/operators';
import { getTelemetrySavedObject, updateTelemetrySavedObject } from '../telemetry_repository';
export function registerTelemetryLastReported(
router: IRouter,
savedObjectsInternalClient$: Observable<SavedObjectsClient>
) {
// GET to retrieve
router.get(
{
path: '/api/telemetry/v2/last_reported',
validate: false,
},
async (context, req, res) => {
const savedObjectsInternalClient = await savedObjectsInternalClient$
.pipe(take(1))
.toPromise();
const telemetrySavedObject = await getTelemetrySavedObject(savedObjectsInternalClient);
return res.ok({
body: {
lastReported: telemetrySavedObject && telemetrySavedObject?.lastReported,
},
});
}
);
// PUT to update
router.put(
{
path: '/api/telemetry/v2/last_reported',
validate: false,
},
async (context, req, res) => {
const savedObjectsInternalClient = await savedObjectsInternalClient$
.pipe(take(1))
.toPromise();
await updateTelemetrySavedObject(savedObjectsInternalClient, {
lastReported: Date.now(),
});
return res.ok();
}
);
}

View file

@ -258,6 +258,7 @@ exports[`TelemetryManagementSectionComponent renders null because allowChangingO
"sendUsageTo": "staging",
},
"fetchExample": [Function],
"fetchLastReported": [Function],
"fetchTelemetry": [Function],
"getCanChangeOptInStatus": [Function],
"getIsOptedIn": [Function],
@ -308,6 +309,7 @@ exports[`TelemetryManagementSectionComponent renders null because allowChangingO
"reportOptInStatusChange": false,
"setOptIn": [Function],
"setUserHasSeenNotice": [Function],
"updateLastReported": [Function],
"updatedConfig": undefined,
}
}

View file

@ -9,6 +9,7 @@
export default function ({ loadTestFile }) {
describe('Telemetry', () => {
loadTestFile(require.resolve('./opt_in'));
loadTestFile(require.resolve('./telemetry_last_reported'));
loadTestFile(require.resolve('./telemetry_optin_notice_seen'));
});
}

View file

@ -0,0 +1,60 @@
/*
* 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 expect from '@kbn/expect';
import { FtrProviderContext } from '../../ftr_provider_context';
export default function optInTest({ getService }: FtrProviderContext) {
const client = getService('es');
const supertest = getService('supertest');
describe('/api/telemetry/v2/last_reported API Telemetry lastReported', () => {
before(async () => {
await client.delete(
{
index: '.kibana',
id: 'telemetry:telemetry',
},
{ ignore: [404] }
);
});
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 });
});
it('PUT should update telemetry.lastReported to now', async () => {
await supertest.put('/api/telemetry/v2/last_reported').set('kbn-xsrf', 'xxx').expect(200);
const { _source } = await client.get<{ telemetry: { lastReported: number } }>({
index: '.kibana',
id: 'telemetry:telemetry',
});
expect(_source?.telemetry.lastReported).to.be.a('number');
});
it('GET should return the previously stored lastReported value', async () => {
const { _source } = await client.get<{ telemetry: { lastReported: number } }>({
index: '.kibana',
id: 'telemetry:telemetry',
});
expect(_source?.telemetry.lastReported).to.be.a('number');
const lastReported = _source?.telemetry.lastReported;
await supertest
.get('/api/telemetry/v2/last_reported')
.set('kbn-xsrf', 'xxx')
.expect(200, { lastReported });
});
});
}