mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
(cherry picked from commit 2d1755439d
)
This commit is contained in:
parent
dc12009833
commit
927cc4d48e
13 changed files with 343 additions and 74 deletions
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
19
src/plugins/telemetry/common/is_report_interval_expired.ts
Normal file
19
src/plugins/telemetry/common/is_report_interval_expired.ts
Normal 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;
|
||||
}
|
|
@ -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']();
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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$);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
);
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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 });
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue