mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
* initial push * self code review * ignore node-fetch type * usageFetcher api * user agent metric * telemetry plugin collector * remove extra unused method * remove unused import * type check * fix collections tests * pass kfetch as dep * add ui metrics integration test for user agent * dont start ui metrics when not authenticated * user agent count always 1 * fix broken ui-metric integration tests * try using config.get * avoid fetching configs if sending * type unknown -> string * check if fetcher is causing the issue * disable ui_metric from functional tests * enable ui_metric back again * ignore keyword above 256 * check requesting app first * clean up after all the debugging :) * fix tests * always return 200 for ui metric reporting * remove boom import * logout after removing role/user * undo some changes in tests * inside try catch * prevent potential race conditions in priorities with = * use snake_case for telemetry plugin collection * usageFetcher -> sendUsageFrom * more replacements * remove extra unused route * config() -> config * Update src/legacy/core_plugins/telemetry/index.ts Co-Authored-By: Mike Côté <mikecote@users.noreply.github.com> * Update src/legacy/core_plugins/ui_metric/server/routes/api/ui_metric.ts Co-Authored-By: Mike Côté <mikecote@users.noreply.github.com> * config() -> config * fix SO update logic given the current changes * fix opt in check * triple check * check for non boolean * take into account older settings * import TelemetryOptInProvider * update test case
This commit is contained in:
parent
ddc265d341
commit
dc4700c275
53 changed files with 1240 additions and 508 deletions
|
@ -17,6 +17,6 @@
|
|||
"@babel/cli": "7.5.5",
|
||||
"@kbn/dev-utils": "1.0.0",
|
||||
"@kbn/babel-preset": "1.0.0",
|
||||
"typescript": "3.5.1"
|
||||
"typescript": "3.5.3"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,6 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export { createReporter, ReportHTTP, Reporter, ReporterConfig } from './reporter';
|
||||
export { ReportHTTP, Reporter, ReporterConfig } from './reporter';
|
||||
export { UiStatsMetricType, METRIC_TYPE } from './metrics';
|
||||
export { Report, ReportManager } from './report';
|
||||
|
|
|
@ -17,21 +17,17 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { UiStatsMetric, UiStatsMetricType } from './ui_stats';
|
||||
import { UiStatsMetric } from './ui_stats';
|
||||
import { UserAgentMetric } from './user_agent';
|
||||
|
||||
export {
|
||||
UiStatsMetric,
|
||||
createUiStatsMetric,
|
||||
UiStatsMetricReport,
|
||||
UiStatsMetricType,
|
||||
} from './ui_stats';
|
||||
export { UiStatsMetric, createUiStatsMetric, UiStatsMetricType } from './ui_stats';
|
||||
export { Stats } from './stats';
|
||||
export { trackUsageAgent } from './user_agent';
|
||||
|
||||
export type Metric = UiStatsMetric<UiStatsMetricType>;
|
||||
export type MetricType = keyof typeof METRIC_TYPE;
|
||||
|
||||
export type Metric = UiStatsMetric | UserAgentMetric;
|
||||
export enum METRIC_TYPE {
|
||||
COUNT = 'count',
|
||||
LOADED = 'loaded',
|
||||
CLICK = 'click',
|
||||
USER_AGENT = 'user_agent',
|
||||
}
|
||||
|
|
|
@ -17,37 +17,33 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { Stats } from './stats';
|
||||
import { METRIC_TYPE } from './';
|
||||
|
||||
export type UiStatsMetricType = METRIC_TYPE.CLICK | METRIC_TYPE.LOADED | METRIC_TYPE.COUNT;
|
||||
export interface UiStatsMetricConfig<T extends UiStatsMetricType> {
|
||||
type: T;
|
||||
export interface UiStatsMetricConfig {
|
||||
type: UiStatsMetricType;
|
||||
appName: string;
|
||||
eventName: string;
|
||||
count?: number;
|
||||
}
|
||||
|
||||
export interface UiStatsMetric<T extends UiStatsMetricType = UiStatsMetricType> {
|
||||
type: T;
|
||||
export interface UiStatsMetric {
|
||||
type: UiStatsMetricType;
|
||||
appName: string;
|
||||
eventName: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export function createUiStatsMetric<T extends UiStatsMetricType>({
|
||||
export function createUiStatsMetric({
|
||||
type,
|
||||
appName,
|
||||
eventName,
|
||||
count = 1,
|
||||
}: UiStatsMetricConfig<T>): UiStatsMetric<T> {
|
||||
return { type, appName, eventName, count };
|
||||
}
|
||||
|
||||
export interface UiStatsMetricReport {
|
||||
key: string;
|
||||
appName: string;
|
||||
eventName: string;
|
||||
type: UiStatsMetricType;
|
||||
stats: Stats;
|
||||
}: UiStatsMetricConfig): UiStatsMetric {
|
||||
return {
|
||||
type,
|
||||
appName,
|
||||
eventName,
|
||||
count,
|
||||
};
|
||||
}
|
||||
|
|
35
packages/kbn-analytics/src/metrics/user_agent.ts
Normal file
35
packages/kbn-analytics/src/metrics/user_agent.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { METRIC_TYPE } from './';
|
||||
|
||||
export interface UserAgentMetric {
|
||||
type: METRIC_TYPE.USER_AGENT;
|
||||
appName: string;
|
||||
userAgent: string;
|
||||
}
|
||||
|
||||
export function trackUsageAgent(appName: string): UserAgentMetric {
|
||||
const userAgent = (window && window.navigator && window.navigator.userAgent) || '';
|
||||
|
||||
return {
|
||||
type: METRIC_TYPE.USER_AGENT,
|
||||
appName,
|
||||
userAgent,
|
||||
};
|
||||
}
|
|
@ -17,28 +17,47 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { UnreachableCaseError } from './util';
|
||||
import { Metric, Stats, UiStatsMetricReport, METRIC_TYPE } from './metrics';
|
||||
import { UnreachableCaseError, wrapArray } from './util';
|
||||
import { Metric, Stats, UiStatsMetricType, METRIC_TYPE } from './metrics';
|
||||
const REPORT_VERSION = 1;
|
||||
|
||||
export interface Report {
|
||||
reportVersion: typeof REPORT_VERSION;
|
||||
uiStatsMetrics: {
|
||||
[key: string]: UiStatsMetricReport;
|
||||
[key: string]: {
|
||||
key: string;
|
||||
appName: string;
|
||||
eventName: string;
|
||||
type: UiStatsMetricType;
|
||||
stats: Stats;
|
||||
};
|
||||
};
|
||||
userAgent?: {
|
||||
[key: string]: {
|
||||
userAgent: string;
|
||||
key: string;
|
||||
type: METRIC_TYPE.USER_AGENT;
|
||||
appName: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export class ReportManager {
|
||||
static REPORT_VERSION = REPORT_VERSION;
|
||||
public report: Report;
|
||||
constructor(report?: Report) {
|
||||
this.report = report || ReportManager.createReport();
|
||||
}
|
||||
static createReport() {
|
||||
return { uiStatsMetrics: {} };
|
||||
static createReport(): Report {
|
||||
return { reportVersion: REPORT_VERSION, uiStatsMetrics: {} };
|
||||
}
|
||||
public clearReport() {
|
||||
this.report = ReportManager.createReport();
|
||||
}
|
||||
public isReportEmpty(): boolean {
|
||||
return Object.keys(this.report.uiStatsMetrics).length === 0;
|
||||
const noUiStats = Object.keys(this.report.uiStatsMetrics).length === 0;
|
||||
const noUserAgent = !this.report.userAgent || Object.keys(this.report.userAgent).length === 0;
|
||||
return noUiStats && noUserAgent;
|
||||
}
|
||||
private incrementStats(count: number, stats?: Stats): Stats {
|
||||
const { min = 0, max = 0, sum = 0 } = stats || {};
|
||||
|
@ -54,28 +73,46 @@ export class ReportManager {
|
|||
sum: newSum,
|
||||
};
|
||||
}
|
||||
assignReports(newMetrics: Metric[]) {
|
||||
newMetrics.forEach(newMetric => this.assignReport(this.report, newMetric));
|
||||
assignReports(newMetrics: Metric | Metric[]) {
|
||||
wrapArray(newMetrics).forEach(newMetric => this.assignReport(this.report, newMetric));
|
||||
}
|
||||
static createMetricKey(metric: Metric): string {
|
||||
switch (metric.type) {
|
||||
case METRIC_TYPE.USER_AGENT: {
|
||||
const { appName, type } = metric;
|
||||
return `${appName}-${type}`;
|
||||
}
|
||||
case METRIC_TYPE.CLICK:
|
||||
case METRIC_TYPE.LOADED:
|
||||
case METRIC_TYPE.COUNT: {
|
||||
const { appName, type, eventName } = metric;
|
||||
const { appName, eventName, type } = metric;
|
||||
return `${appName}-${type}-${eventName}`;
|
||||
}
|
||||
default:
|
||||
throw new UnreachableCaseError(metric.type);
|
||||
throw new UnreachableCaseError(metric);
|
||||
}
|
||||
}
|
||||
private assignReport(report: Report, metric: Metric) {
|
||||
const key = ReportManager.createMetricKey(metric);
|
||||
switch (metric.type) {
|
||||
case METRIC_TYPE.USER_AGENT: {
|
||||
const { appName, type, userAgent } = metric;
|
||||
if (userAgent) {
|
||||
this.report.userAgent = {
|
||||
[key]: {
|
||||
key,
|
||||
appName,
|
||||
type,
|
||||
userAgent: metric.userAgent,
|
||||
},
|
||||
};
|
||||
}
|
||||
return;
|
||||
}
|
||||
case METRIC_TYPE.CLICK:
|
||||
case METRIC_TYPE.LOADED:
|
||||
case METRIC_TYPE.COUNT: {
|
||||
const { appName, type, eventName, count } = metric;
|
||||
const key = ReportManager.createMetricKey(metric);
|
||||
const existingStats = (report.uiStatsMetrics[key] || {}).stats;
|
||||
this.report.uiStatsMetrics[key] = {
|
||||
key,
|
||||
|
@ -87,7 +124,7 @@ export class ReportManager {
|
|||
return;
|
||||
}
|
||||
default:
|
||||
throw new UnreachableCaseError(metric.type);
|
||||
throw new UnreachableCaseError(metric);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
*/
|
||||
|
||||
import { wrapArray } from './util';
|
||||
import { Metric, UiStatsMetric, createUiStatsMetric } from './metrics';
|
||||
import { Metric, createUiStatsMetric, trackUsageAgent, UiStatsMetricType } from './metrics';
|
||||
|
||||
import { Storage, ReportStorageManager } from './storage';
|
||||
import { Report, ReportManager } from './report';
|
||||
|
@ -40,10 +40,11 @@ export class Reporter {
|
|||
private reportManager: ReportManager;
|
||||
private storageManager: ReportStorageManager;
|
||||
private debug: boolean;
|
||||
private retryCount = 0;
|
||||
private readonly maxRetries = 3;
|
||||
|
||||
constructor(config: ReporterConfig) {
|
||||
const { http, storage, debug, checkInterval = 10000, storageKey = 'analytics' } = config;
|
||||
|
||||
const { http, storage, debug, checkInterval = 90000, storageKey = 'analytics' } = config;
|
||||
this.http = http;
|
||||
this.checkInterval = checkInterval;
|
||||
this.interval = null;
|
||||
|
@ -59,18 +60,19 @@ export class Reporter {
|
|||
}
|
||||
|
||||
private flushReport() {
|
||||
this.retryCount = 0;
|
||||
this.reportManager.clearReport();
|
||||
this.storageManager.store(this.reportManager.report);
|
||||
}
|
||||
|
||||
public start() {
|
||||
public start = () => {
|
||||
if (!this.interval) {
|
||||
this.interval = setTimeout(() => {
|
||||
this.interval = null;
|
||||
this.sendReports();
|
||||
}, this.checkInterval);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private log(message: any) {
|
||||
if (this.debug) {
|
||||
|
@ -79,36 +81,42 @@ export class Reporter {
|
|||
}
|
||||
}
|
||||
|
||||
public reportUiStats(
|
||||
public reportUiStats = (
|
||||
appName: string,
|
||||
type: UiStatsMetric['type'],
|
||||
type: UiStatsMetricType,
|
||||
eventNames: string | string[],
|
||||
count?: number
|
||||
) {
|
||||
) => {
|
||||
const metrics = wrapArray(eventNames).map(eventName => {
|
||||
if (this) this.log(`${type} Metric -> (${appName}:${eventName}):`);
|
||||
this.log(`${type} Metric -> (${appName}:${eventName}):`);
|
||||
const report = createUiStatsMetric({ type, appName, eventName, count });
|
||||
this.log(report);
|
||||
return report;
|
||||
});
|
||||
this.saveToReport(metrics);
|
||||
}
|
||||
};
|
||||
|
||||
public async sendReports() {
|
||||
public reportUserAgent = (appName: string) => {
|
||||
this.log(`Reporting user-agent.`);
|
||||
const report = trackUsageAgent(appName);
|
||||
this.saveToReport([report]);
|
||||
};
|
||||
|
||||
public sendReports = async () => {
|
||||
if (!this.reportManager.isReportEmpty()) {
|
||||
try {
|
||||
await this.http(this.reportManager.report);
|
||||
this.flushReport();
|
||||
} catch (err) {
|
||||
this.log(`Error Sending Metrics Report ${err}`);
|
||||
this.retryCount = this.retryCount + 1;
|
||||
const versionMismatch =
|
||||
this.reportManager.report.reportVersion !== ReportManager.REPORT_VERSION;
|
||||
if (versionMismatch || this.retryCount > this.maxRetries) {
|
||||
this.flushReport();
|
||||
}
|
||||
}
|
||||
}
|
||||
this.start();
|
||||
}
|
||||
}
|
||||
|
||||
export function createReporter(reportedConf: ReporterConfig) {
|
||||
const reporter = new Reporter(reportedConf);
|
||||
reporter.start();
|
||||
return reporter;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -185,6 +185,7 @@ kibana_vars=(
|
|||
xpack.security.public.hostname
|
||||
xpack.security.public.port
|
||||
telemetry.enabled
|
||||
telemetry.sendUsageFrom
|
||||
)
|
||||
|
||||
longopts=''
|
||||
|
|
|
@ -59,6 +59,12 @@ export const PRIVACY_STATEMENT_URL = `https://www.elastic.co/legal/telemetry-pri
|
|||
*/
|
||||
export const KIBANA_LOCALIZATION_STATS_TYPE = 'localization';
|
||||
|
||||
/**
|
||||
* The type name used to publish telemetry plugin stats.
|
||||
* @type {string}
|
||||
*/
|
||||
export const TELEMETRY_STATS_TYPE = 'telemetry';
|
||||
|
||||
/**
|
||||
* UI metric usage type
|
||||
* @type {string}
|
||||
|
|
|
@ -27,12 +27,13 @@ import { i18n } from '@kbn/i18n';
|
|||
import mappings from './mappings.json';
|
||||
import { CONFIG_TELEMETRY, getConfigTelemetryDesc } from './common/constants';
|
||||
import { getXpackConfigWithDeprecated } from './common/get_xpack_config_with_deprecated';
|
||||
import { telemetryPlugin, getTelemetryOptIn } from './server';
|
||||
import { telemetryPlugin, replaceTelemetryInjectedVars, FetcherTask } from './server';
|
||||
|
||||
import {
|
||||
createLocalizationUsageCollector,
|
||||
createTelemetryUsageCollector,
|
||||
createUiMetricUsageCollector,
|
||||
createTelemetryPluginUsageCollector,
|
||||
} from './server/collectors';
|
||||
|
||||
const ENDPOINT_VERSION = 'v2';
|
||||
|
@ -46,20 +47,18 @@ const telemetry = (kibana: any) => {
|
|||
config(Joi: typeof JoiNamespace) {
|
||||
return Joi.object({
|
||||
enabled: Joi.boolean().default(true),
|
||||
allowChangingOptInStatus: Joi.boolean().default(true),
|
||||
optIn: Joi.when('allowChangingOptInStatus', {
|
||||
is: false,
|
||||
then: Joi.valid(true),
|
||||
then: Joi.valid(true).required(),
|
||||
otherwise: Joi.boolean()
|
||||
.allow(null)
|
||||
.default(null),
|
||||
}),
|
||||
allowChangingOptInStatus: Joi.boolean().default(true),
|
||||
|
||||
// `config` is used internally and not intended to be set
|
||||
config: Joi.string().default(Joi.ref('$defaultConfigPath')),
|
||||
banner: Joi.boolean().default(true),
|
||||
lastVersionChecked: Joi.string()
|
||||
.allow('')
|
||||
.default(''),
|
||||
url: Joi.when('$dev', {
|
||||
is: true,
|
||||
then: Joi.string().default(
|
||||
|
@ -69,6 +68,9 @@ const telemetry = (kibana: any) => {
|
|||
`https://telemetry.elastic.co/xpack/${ENDPOINT_VERSION}/send`
|
||||
),
|
||||
}),
|
||||
sendUsageFrom: Joi.string()
|
||||
.allow(['server', 'browser'])
|
||||
.default('browser'),
|
||||
}).default();
|
||||
},
|
||||
uiExports: {
|
||||
|
@ -89,30 +91,8 @@ const telemetry = (kibana: any) => {
|
|||
},
|
||||
},
|
||||
async replaceInjectedVars(originalInjectedVars: any, request: any) {
|
||||
const config = request.server.config();
|
||||
const optIn = config.get('telemetry.optIn');
|
||||
const allowChangingOptInStatus = config.get('telemetry.allowChangingOptInStatus');
|
||||
const currentKibanaVersion = getCurrentKibanaVersion(request.server);
|
||||
let telemetryOptedIn: boolean | null;
|
||||
|
||||
if (typeof optIn === 'boolean' && !allowChangingOptInStatus) {
|
||||
// When not allowed to change optIn status and an optIn value is set, we'll overwrite with that
|
||||
telemetryOptedIn = optIn;
|
||||
} else {
|
||||
telemetryOptedIn = await getTelemetryOptIn({
|
||||
request,
|
||||
currentKibanaVersion,
|
||||
});
|
||||
if (telemetryOptedIn === null) {
|
||||
// In the senario there's no value set in telemetryOptedIn, we'll return optIn value
|
||||
telemetryOptedIn = optIn;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...originalInjectedVars,
|
||||
telemetryOptedIn,
|
||||
};
|
||||
const telemetryInjectedVars = await replaceTelemetryInjectedVars(request);
|
||||
return Object.assign({}, originalInjectedVars, telemetryInjectedVars);
|
||||
},
|
||||
injectDefaultVars(server: Server) {
|
||||
const config = server.config();
|
||||
|
@ -124,16 +104,21 @@ const telemetry = (kibana: any) => {
|
|||
getXpackConfigWithDeprecated(config, 'telemetry.banner'),
|
||||
telemetryOptedIn: config.get('telemetry.optIn'),
|
||||
allowChangingOptInStatus: config.get('telemetry.allowChangingOptInStatus'),
|
||||
telemetrySendUsageFrom: config.get('telemetry.sendUsageFrom'),
|
||||
};
|
||||
},
|
||||
hacks: ['plugins/telemetry/hacks/telemetry_init', 'plugins/telemetry/hacks/telemetry_opt_in'],
|
||||
mappings,
|
||||
},
|
||||
async init(server: Server) {
|
||||
postInit(server: Server) {
|
||||
const fetcherTask = new FetcherTask(server);
|
||||
fetcherTask.start();
|
||||
},
|
||||
init(server: Server) {
|
||||
const initializerContext = {
|
||||
env: {
|
||||
packageInfo: {
|
||||
version: getCurrentKibanaVersion(server),
|
||||
version: server.config().get('pkg.version'),
|
||||
},
|
||||
},
|
||||
config: {
|
||||
|
@ -156,9 +141,10 @@ const telemetry = (kibana: any) => {
|
|||
log: server.log,
|
||||
} as any) as CoreSetup;
|
||||
|
||||
await telemetryPlugin(initializerContext).setup(coreSetup);
|
||||
telemetryPlugin(initializerContext).setup(coreSetup);
|
||||
|
||||
// register collectors
|
||||
server.usage.collectorSet.register(createTelemetryPluginUsageCollector(server));
|
||||
server.usage.collectorSet.register(createLocalizationUsageCollector(server));
|
||||
server.usage.collectorSet.register(createTelemetryUsageCollector(server));
|
||||
server.usage.collectorSet.register(createUiMetricUsageCollector(server));
|
||||
|
@ -168,7 +154,3 @@ const telemetry = (kibana: any) => {
|
|||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default telemetry;
|
||||
|
||||
function getCurrentKibanaVersion(server: Server): string {
|
||||
return server.config().get('pkg.version');
|
||||
}
|
||||
|
|
|
@ -4,7 +4,15 @@
|
|||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"sendUsageFrom": {
|
||||
"ignore_above": 256,
|
||||
"type": "keyword"
|
||||
},
|
||||
"lastReported": {
|
||||
"type": "date"
|
||||
},
|
||||
"lastVersionChecked": {
|
||||
"ignore_above": 256,
|
||||
"type": "keyword"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,13 +25,21 @@ import { isUnauthenticated } from '../services';
|
|||
import { Telemetry } from './telemetry';
|
||||
// @ts-ignore
|
||||
import { fetchTelemetry } from './fetch_telemetry';
|
||||
// @ts-ignore
|
||||
import { isOptInHandleOldSettings } from './welcome_banner/handle_old_settings';
|
||||
import { TelemetryOptInProvider } from '../services';
|
||||
|
||||
function telemetryInit($injector: any) {
|
||||
const $http = $injector.get('$http');
|
||||
const Private = $injector.get('Private');
|
||||
const config = $injector.get('config');
|
||||
const telemetryOptInProvider = Private(TelemetryOptInProvider);
|
||||
|
||||
const telemetryEnabled = npStart.core.injectedMetadata.getInjectedVar('telemetryEnabled');
|
||||
const telemetryOptedIn = isOptInHandleOldSettings(config, telemetryOptInProvider);
|
||||
const sendUsageFrom = npStart.core.injectedMetadata.getInjectedVar('telemetrySendUsageFrom');
|
||||
|
||||
if (telemetryEnabled) {
|
||||
if (telemetryEnabled && telemetryOptedIn && sendUsageFrom === 'browser') {
|
||||
// no telemetry for non-logged in users
|
||||
if (isUnauthenticated()) {
|
||||
return;
|
||||
|
|
|
@ -27,8 +27,9 @@ import { CONFIG_TELEMETRY } from '../../../common/constants';
|
|||
* @param {Object} config The advanced settings config object.
|
||||
* @return {Boolean} {@code true} if the banner should still be displayed. {@code false} if the banner should not be displayed.
|
||||
*/
|
||||
const CONFIG_ALLOW_REPORT = 'xPackMonitoring:allowReport';
|
||||
|
||||
export async function handleOldSettings(config, telemetryOptInProvider) {
|
||||
const CONFIG_ALLOW_REPORT = 'xPackMonitoring:allowReport';
|
||||
const CONFIG_SHOW_BANNER = 'xPackMonitoring:showBanner';
|
||||
const oldAllowReportSetting = config.get(CONFIG_ALLOW_REPORT, null);
|
||||
const oldTelemetrySetting = config.get(CONFIG_TELEMETRY, null);
|
||||
|
@ -62,3 +63,24 @@ export async function handleOldSettings(config, telemetryOptInProvider) {
|
|||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
export async function isOptInHandleOldSettings(config, telemetryOptInProvider) {
|
||||
const currentOptInSettting = telemetryOptInProvider.getOptIn();
|
||||
|
||||
if (typeof currentOptInSettting === 'boolean') {
|
||||
return currentOptInSettting;
|
||||
}
|
||||
|
||||
const oldTelemetrySetting = config.get(CONFIG_TELEMETRY, null);
|
||||
if (typeof oldTelemetrySetting === 'boolean') {
|
||||
return oldTelemetrySetting;
|
||||
}
|
||||
|
||||
const oldAllowReportSetting = config.get(CONFIG_ALLOW_REPORT, null);
|
||||
if (typeof oldAllowReportSetting === 'boolean') {
|
||||
return oldAllowReportSetting;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -41,6 +41,9 @@ export function TelemetryOptInProvider($injector: any, chrome: any) {
|
|||
bannerId = id;
|
||||
},
|
||||
setOptIn: async (enabled: boolean) => {
|
||||
if (!allowChangingOptInStatus) {
|
||||
return;
|
||||
}
|
||||
setCanTrackUiMetrics(enabled);
|
||||
const $http = $injector.get('$http');
|
||||
|
||||
|
|
|
@ -17,20 +17,72 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
class TelemetryCollectionManager {
|
||||
private getterMethod?: any;
|
||||
private collectionTitle?: string;
|
||||
private getterMethodPriority = 0;
|
||||
import { encryptTelemetry } from './collectors';
|
||||
|
||||
public setStatsGetter = (statsGetter: any, title: string, priority = 0) => {
|
||||
if (priority >= this.getterMethodPriority) {
|
||||
export type EncryptedStatsGetterConfig = { unencrypted: false } & {
|
||||
server: any;
|
||||
start: any;
|
||||
end: any;
|
||||
isDev: boolean;
|
||||
};
|
||||
|
||||
export type UnencryptedStatsGetterConfig = { unencrypted: true } & {
|
||||
req: any;
|
||||
start: any;
|
||||
end: any;
|
||||
isDev: boolean;
|
||||
};
|
||||
|
||||
export interface StatsCollectionConfig {
|
||||
callCluster: any;
|
||||
server: any;
|
||||
start: any;
|
||||
end: any;
|
||||
}
|
||||
|
||||
export type StatsGetterConfig = UnencryptedStatsGetterConfig | EncryptedStatsGetterConfig;
|
||||
|
||||
export type StatsGetter = (config: StatsGetterConfig) => Promise<any[]>;
|
||||
|
||||
export const getStatsCollectionConfig = (
|
||||
config: StatsGetterConfig,
|
||||
esClustser: string
|
||||
): StatsCollectionConfig => {
|
||||
const { start, end } = config;
|
||||
const server = config.unencrypted ? config.req.server : config.server;
|
||||
const { callWithRequest, callWithInternalUser } = server.plugins.elasticsearch.getCluster(
|
||||
esClustser
|
||||
);
|
||||
const callCluster = config.unencrypted
|
||||
? (...args: any[]) => callWithRequest(config.req, ...args)
|
||||
: callWithInternalUser;
|
||||
|
||||
return { server, callCluster, start, end };
|
||||
};
|
||||
|
||||
export class TelemetryCollectionManager {
|
||||
private getterMethod?: StatsGetter;
|
||||
private collectionTitle?: string;
|
||||
private getterMethodPriority = -1;
|
||||
|
||||
public setStatsGetter = (statsGetter: StatsGetter, title: string, priority = 0) => {
|
||||
if (priority > this.getterMethodPriority) {
|
||||
this.getterMethod = statsGetter;
|
||||
this.collectionTitle = title;
|
||||
this.getterMethodPriority = priority;
|
||||
}
|
||||
};
|
||||
|
||||
getCollectionTitle = () => {
|
||||
private getStats = async (config: StatsGetterConfig) => {
|
||||
if (!this.getterMethod) {
|
||||
throw Error('Stats getter method not set.');
|
||||
}
|
||||
const usageData = await this.getterMethod(config);
|
||||
|
||||
if (config.unencrypted) return usageData;
|
||||
return encryptTelemetry(usageData, config.isDev);
|
||||
};
|
||||
public getCollectionTitle = () => {
|
||||
return this.collectionTitle;
|
||||
};
|
||||
|
||||
|
@ -39,7 +91,7 @@ class TelemetryCollectionManager {
|
|||
throw Error('Stats getter method not set.');
|
||||
}
|
||||
return {
|
||||
getStats: this.getterMethod,
|
||||
getStats: this.getStats,
|
||||
priority: this.getterMethodPriority,
|
||||
title: this.collectionTitle,
|
||||
};
|
||||
|
|
|
@ -21,3 +21,4 @@ export { encryptTelemetry } from './encryption';
|
|||
export { createTelemetryUsageCollector } from './usage';
|
||||
export { createUiMetricUsageCollector } from './ui_metric';
|
||||
export { createLocalizationUsageCollector } from './localization';
|
||||
export { createTelemetryPluginUsageCollector } from './telemetry_plugin';
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
export { createTelemetryPluginUsageCollector } from './telemetry_plugin_collector';
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { TELEMETRY_STATS_TYPE } from '../../../common/constants';
|
||||
import { getTelemetrySavedObject, TelemetrySavedObject } from '../../telemetry_repository';
|
||||
import { getTelemetryOptIn, getTelemetryUsageFetcher } from '../../telemetry_config';
|
||||
export interface TelemetryUsageStats {
|
||||
opt_in_status?: boolean | null;
|
||||
usage_fetcher?: 'browser' | 'server';
|
||||
last_reported?: number;
|
||||
}
|
||||
|
||||
export function createCollectorFetch(server: any) {
|
||||
return async function fetchUsageStats(): Promise<TelemetryUsageStats> {
|
||||
const config = server.config();
|
||||
const configTelemetrySendUsageFrom = config.get('telemetry.sendUsageFrom');
|
||||
const allowChangingOptInStatus = config.get('telemetry.allowChangingOptInStatus');
|
||||
const configTelemetryOptIn = config.get('telemetry.optIn');
|
||||
const currentKibanaVersion = config.get('pkg.version');
|
||||
|
||||
let telemetrySavedObject: TelemetrySavedObject = {};
|
||||
|
||||
try {
|
||||
const { getSavedObjectsRepository } = server.savedObjects;
|
||||
const { callWithInternalUser } = server.plugins.elasticsearch.getCluster('admin');
|
||||
const internalRepository = getSavedObjectsRepository(callWithInternalUser);
|
||||
telemetrySavedObject = await getTelemetrySavedObject(internalRepository);
|
||||
} catch (err) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
return {
|
||||
opt_in_status: getTelemetryOptIn({
|
||||
currentKibanaVersion,
|
||||
telemetrySavedObject,
|
||||
allowChangingOptInStatus,
|
||||
configTelemetryOptIn,
|
||||
}),
|
||||
last_reported: telemetrySavedObject ? telemetrySavedObject.lastReported : undefined,
|
||||
usage_fetcher: getTelemetryUsageFetcher({
|
||||
telemetrySavedObject,
|
||||
configTelemetrySendUsageFrom,
|
||||
}),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
* @param {Object} server
|
||||
* @return {Object} kibana usage stats type collection object
|
||||
*/
|
||||
export function createTelemetryPluginUsageCollector(server: any) {
|
||||
const { collectorSet } = server.usage;
|
||||
return collectorSet.makeUsageCollector({
|
||||
type: TELEMETRY_STATS_TYPE,
|
||||
isReady: () => true,
|
||||
fetch: createCollectorFetch(server),
|
||||
});
|
||||
}
|
148
src/legacy/core_plugins/telemetry/server/fetcher.ts
Normal file
148
src/legacy/core_plugins/telemetry/server/fetcher.ts
Normal file
|
@ -0,0 +1,148 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import moment from 'moment';
|
||||
// @ts-ignore
|
||||
import fetch from 'node-fetch';
|
||||
import { telemetryCollectionManager } from './collection_manager';
|
||||
import { getTelemetryOptIn, getTelemetryUsageFetcher } from './telemetry_config';
|
||||
import { getTelemetrySavedObject, updateTelemetrySavedObject } from './telemetry_repository';
|
||||
import { REPORT_INTERVAL_MS } from '../common/constants';
|
||||
import { getXpackConfigWithDeprecated } from '../common/get_xpack_config_with_deprecated';
|
||||
|
||||
export class FetcherTask {
|
||||
private readonly checkDurationMs = 60 * 1000 * 5;
|
||||
private intervalId?: NodeJS.Timeout;
|
||||
private lastReported?: number;
|
||||
private isSending = false;
|
||||
private server: any;
|
||||
|
||||
constructor(server: any) {
|
||||
this.server = server;
|
||||
}
|
||||
|
||||
private getInternalRepository = () => {
|
||||
const { getSavedObjectsRepository } = this.server.savedObjects;
|
||||
const { callWithInternalUser } = this.server.plugins.elasticsearch.getCluster('admin');
|
||||
const internalRepository = getSavedObjectsRepository(callWithInternalUser);
|
||||
return internalRepository;
|
||||
};
|
||||
|
||||
private getCurrentConfigs = async () => {
|
||||
const internalRepository = this.getInternalRepository();
|
||||
const telemetrySavedObject = await getTelemetrySavedObject(internalRepository);
|
||||
const config = this.server.config();
|
||||
const currentKibanaVersion = config.get('pkg.version');
|
||||
const configTelemetrySendUsageFrom = config.get('telemetry.sendUsageFrom');
|
||||
const allowChangingOptInStatus = config.get('telemetry.allowChangingOptInStatus');
|
||||
const configTelemetryOptIn = config.get('telemetry.optIn');
|
||||
const telemetryUrl = getXpackConfigWithDeprecated(config, 'telemetry.url') as string;
|
||||
|
||||
return {
|
||||
telemetryOptIn: getTelemetryOptIn({
|
||||
currentKibanaVersion,
|
||||
telemetrySavedObject,
|
||||
allowChangingOptInStatus,
|
||||
configTelemetryOptIn,
|
||||
}),
|
||||
telemetrySendUsageFrom: getTelemetryUsageFetcher({
|
||||
telemetrySavedObject,
|
||||
configTelemetrySendUsageFrom,
|
||||
}),
|
||||
telemetryUrl,
|
||||
};
|
||||
};
|
||||
|
||||
private updateLastReported = async () => {
|
||||
const internalRepository = this.getInternalRepository();
|
||||
this.lastReported = Date.now();
|
||||
updateTelemetrySavedObject(internalRepository, {
|
||||
lastReported: this.lastReported,
|
||||
});
|
||||
};
|
||||
|
||||
private shouldSendReport = ({ telemetryOptIn, telemetrySendUsageFrom }: any) => {
|
||||
if (telemetryOptIn && telemetrySendUsageFrom === 'server') {
|
||||
if (!this.lastReported || Date.now() - this.lastReported > REPORT_INTERVAL_MS) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
private fetchTelemetry = async () => {
|
||||
const { getStats, title } = telemetryCollectionManager.getStatsGetter();
|
||||
this.server.log(['debug', 'telemetry', 'fetcher'], `Fetching usage using ${title} getter.`);
|
||||
const config = this.server.config();
|
||||
|
||||
return await getStats({
|
||||
unencrypted: false,
|
||||
server: this.server,
|
||||
start: moment()
|
||||
.subtract(20, 'minutes')
|
||||
.toISOString(),
|
||||
end: moment().toISOString(),
|
||||
isDev: config.get('env.dev'),
|
||||
});
|
||||
};
|
||||
|
||||
private sendTelemetry = async (url: string, cluster: any): Promise<void> => {
|
||||
this.server.log(['debug', 'telemetry', 'fetcher'], `Sending usage stats.`);
|
||||
await fetch(url, {
|
||||
method: 'post',
|
||||
body: cluster,
|
||||
});
|
||||
};
|
||||
|
||||
private sendIfDue = async () => {
|
||||
if (this.isSending) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const telemetryConfig = await this.getCurrentConfigs();
|
||||
if (!this.shouldSendReport(telemetryConfig)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// mark that we are working so future requests are ignored until we're done
|
||||
this.isSending = true;
|
||||
const clusters = await this.fetchTelemetry();
|
||||
for (const cluster of clusters) {
|
||||
await this.sendTelemetry(telemetryConfig.telemetryUrl, cluster);
|
||||
}
|
||||
|
||||
await this.updateLastReported();
|
||||
} catch (err) {
|
||||
this.server.log(
|
||||
['warning', 'telemetry', 'fetcher'],
|
||||
`Error sending telemetry usage data: ${err}`
|
||||
);
|
||||
}
|
||||
this.isSending = false;
|
||||
};
|
||||
|
||||
public start = () => {
|
||||
this.intervalId = setInterval(() => this.sendIfDue(), this.checkDurationMs);
|
||||
};
|
||||
public stop = () => {
|
||||
if (this.intervalId) {
|
||||
clearInterval(this.intervalId);
|
||||
}
|
||||
};
|
||||
}
|
|
@ -21,7 +21,8 @@ import { PluginInitializerContext } from 'src/core/server';
|
|||
import { TelemetryPlugin } from './plugin';
|
||||
import * as constants from '../common/constants';
|
||||
|
||||
export { getTelemetryOptIn } from './get_telemetry_opt_in';
|
||||
export { FetcherTask } from './fetcher';
|
||||
export { replaceTelemetryInjectedVars } from './telemetry_config';
|
||||
export { telemetryCollectionManager } from './collection_manager';
|
||||
|
||||
export const telemetryPlugin = (initializerContext: PluginInitializerContext) =>
|
||||
|
|
|
@ -29,7 +29,7 @@ export class TelemetryPlugin {
|
|||
this.currentKibanaVersion = initializerContext.env.packageInfo.version;
|
||||
}
|
||||
|
||||
public async setup(core: CoreSetup) {
|
||||
public setup(core: CoreSetup) {
|
||||
const currentKibanaVersion = this.currentKibanaVersion;
|
||||
telemetryCollectionManager.setStatsGetter(getStats, 'local');
|
||||
registerRoutes({ core, currentKibanaVersion });
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
*/
|
||||
|
||||
import { CoreSetup } from 'src/core/server';
|
||||
import { registerOptInRoutes } from './opt_in';
|
||||
import { registerTelemetryConfigRoutes } from './telemetry_config';
|
||||
import { registerTelemetryDataRoutes } from './telemetry_stats';
|
||||
|
||||
interface RegisterRoutesParams {
|
||||
|
@ -27,6 +27,6 @@ interface RegisterRoutesParams {
|
|||
}
|
||||
|
||||
export function registerRoutes({ core, currentKibanaVersion }: RegisterRoutesParams) {
|
||||
registerTelemetryConfigRoutes({ core, currentKibanaVersion });
|
||||
registerTelemetryDataRoutes(core);
|
||||
registerOptInRoutes({ core, currentKibanaVersion });
|
||||
}
|
||||
|
|
|
@ -20,18 +20,21 @@
|
|||
import Joi from 'joi';
|
||||
import { boomify } from 'boom';
|
||||
import { CoreSetup } from 'src/core/server';
|
||||
import { getTelemetryAllowChangingOptInStatus } from '../telemetry_config';
|
||||
import {
|
||||
TelemetrySavedObjectAttributes,
|
||||
updateTelemetrySavedObject,
|
||||
} from '../telemetry_repository';
|
||||
|
||||
interface RegisterOptInRoutesParams {
|
||||
core: CoreSetup;
|
||||
currentKibanaVersion: string;
|
||||
}
|
||||
|
||||
export interface SavedObjectAttributes {
|
||||
enabled?: boolean;
|
||||
lastVersionChecked: string;
|
||||
}
|
||||
|
||||
export function registerOptInRoutes({ core, currentKibanaVersion }: RegisterOptInRoutesParams) {
|
||||
export function registerTelemetryConfigRoutes({
|
||||
core,
|
||||
currentKibanaVersion,
|
||||
}: RegisterOptInRoutesParams) {
|
||||
const { server } = core.http as any;
|
||||
|
||||
server.route({
|
||||
|
@ -45,17 +48,24 @@ export function registerOptInRoutes({ core, currentKibanaVersion }: RegisterOptI
|
|||
},
|
||||
},
|
||||
handler: async (req: any, h: any) => {
|
||||
const savedObjectsClient = req.getSavedObjectsClient();
|
||||
const savedObject: SavedObjectAttributes = {
|
||||
enabled: req.payload.enabled,
|
||||
lastVersionChecked: currentKibanaVersion,
|
||||
};
|
||||
const options = {
|
||||
id: 'telemetry',
|
||||
overwrite: true,
|
||||
};
|
||||
try {
|
||||
await savedObjectsClient.create('telemetry', savedObject, options);
|
||||
const attributes: TelemetrySavedObjectAttributes = {
|
||||
enabled: req.payload.enabled,
|
||||
lastVersionChecked: currentKibanaVersion,
|
||||
};
|
||||
const config = req.server.config();
|
||||
const savedObjectsClient = req.getSavedObjectsClient();
|
||||
const configTelemetryAllowChangingOptInStatus = config.get(
|
||||
'telemetry.allowChangingOptInStatus'
|
||||
);
|
||||
const allowChangingOptInStatus = getTelemetryAllowChangingOptInStatus({
|
||||
telemetrySavedObject: savedObjectsClient,
|
||||
configTelemetryAllowChangingOptInStatus,
|
||||
});
|
||||
if (!allowChangingOptInStatus) {
|
||||
return h.response({ error: 'Not allowed to change Opt-in Status.' }).code(400);
|
||||
}
|
||||
await updateTelemetrySavedObject(savedObjectsClient, attributes);
|
||||
} catch (err) {
|
||||
return boomify(err);
|
||||
}
|
|
@ -20,7 +20,6 @@
|
|||
import Joi from 'joi';
|
||||
import { boomify } from 'boom';
|
||||
import { CoreSetup } from 'src/core/server';
|
||||
import { encryptTelemetry } from '../collectors';
|
||||
import { telemetryCollectionManager } from '../collection_manager';
|
||||
|
||||
export function registerTelemetryDataRoutes(core: CoreSetup) {
|
||||
|
@ -49,12 +48,16 @@ export function registerTelemetryDataRoutes(core: CoreSetup) {
|
|||
|
||||
try {
|
||||
const { getStats, title } = telemetryCollectionManager.getStatsGetter();
|
||||
server.log(['debug', 'telemetry'], `Using Stats Getter: ${title}`);
|
||||
server.log(['debug', 'telemetry', 'fetcher'], `Fetching usage using ${title} getter.`);
|
||||
|
||||
const usageData = await getStats(req, config, start, end, unencrypted);
|
||||
|
||||
if (unencrypted) return usageData;
|
||||
return encryptTelemetry(usageData, isDev);
|
||||
return await getStats({
|
||||
unencrypted,
|
||||
server,
|
||||
req,
|
||||
start,
|
||||
end,
|
||||
isDev,
|
||||
});
|
||||
} catch (err) {
|
||||
if (isDev) {
|
||||
// don't ignore errors when running in dev mode
|
||||
|
|
|
@ -26,7 +26,6 @@ import { mockGetClusterStats } from './get_cluster_stats';
|
|||
import { omit } from 'lodash';
|
||||
import {
|
||||
getLocalStats,
|
||||
getLocalStatsWithCaller,
|
||||
handleLocalStats,
|
||||
} from '../get_local_stats';
|
||||
|
||||
|
@ -153,7 +152,7 @@ describe('get_local_stats', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('getLocalStatsWithCaller', () => {
|
||||
describe('getLocalStats', () => {
|
||||
it('returns expected object without xpack data when X-Pack fails to respond', async () => {
|
||||
const callClusterUsageFailed = sinon.stub();
|
||||
|
||||
|
@ -162,8 +161,10 @@ describe('get_local_stats', () => {
|
|||
Promise.resolve(clusterInfo),
|
||||
Promise.resolve(clusterStats),
|
||||
);
|
||||
|
||||
const result = await getLocalStatsWithCaller(getMockServer(), callClusterUsageFailed);
|
||||
const result = await getLocalStats({
|
||||
server: getMockServer(),
|
||||
callCluster: callClusterUsageFailed,
|
||||
});
|
||||
expect(result.cluster_uuid).to.eql(combinedStatsResult.cluster_uuid);
|
||||
expect(result.cluster_name).to.eql(combinedStatsResult.cluster_name);
|
||||
expect(result.cluster_stats).to.eql(combinedStatsResult.cluster_stats);
|
||||
|
@ -184,51 +185,13 @@ describe('get_local_stats', () => {
|
|||
Promise.resolve(clusterStats),
|
||||
);
|
||||
|
||||
const result = await getLocalStatsWithCaller(getMockServer(callCluster, kibana), callCluster);
|
||||
const result = await getLocalStats({
|
||||
server: getMockServer(callCluster, kibana),
|
||||
callCluster,
|
||||
});
|
||||
|
||||
expect(result.stack_stats.xpack).to.eql(combinedStatsResult.stack_stats.xpack);
|
||||
expect(result.stack_stats.kibana).to.eql(combinedStatsResult.stack_stats.kibana);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLocalStats', () => {
|
||||
it('uses callWithInternalUser from data cluster', async () => {
|
||||
const getCluster = sinon.stub();
|
||||
const req = { server: getMockServer(getCluster) };
|
||||
const callWithInternalUser = sinon.stub();
|
||||
|
||||
getCluster.withArgs('data').returns({ callWithInternalUser });
|
||||
|
||||
mockGetLocalStats(
|
||||
callWithInternalUser,
|
||||
Promise.resolve(clusterInfo),
|
||||
Promise.resolve(clusterStats),
|
||||
);
|
||||
|
||||
const result = await getLocalStats(req, { useInternalUser: true });
|
||||
expect(result.cluster_uuid).to.eql(combinedStatsResult.cluster_uuid);
|
||||
expect(result.cluster_name).to.eql(combinedStatsResult.cluster_name);
|
||||
expect(result.version).to.eql(combinedStatsResult.version);
|
||||
expect(result.cluster_stats).to.eql(combinedStatsResult.cluster_stats);
|
||||
});
|
||||
it('uses callWithRequest from data cluster', async () => {
|
||||
const getCluster = sinon.stub();
|
||||
const req = { server: getMockServer(getCluster) };
|
||||
const callWithRequest = sinon.stub();
|
||||
|
||||
getCluster.withArgs('data').returns({ callWithRequest });
|
||||
|
||||
mockGetLocalStats(
|
||||
callWithRequest,
|
||||
Promise.resolve(clusterInfo),
|
||||
Promise.resolve(clusterStats),
|
||||
req
|
||||
);
|
||||
|
||||
const result = await getLocalStats(req, { useInternalUser: false });
|
||||
expect(result.cluster_uuid).to.eql(combinedStatsResult.cluster_uuid);
|
||||
expect(result.cluster_name).to.eql(combinedStatsResult.cluster_name);
|
||||
expect(result.version).to.eql(combinedStatsResult.version);
|
||||
expect(result.cluster_stats).to.eql(combinedStatsResult.cluster_stats);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -51,7 +51,7 @@ export function handleLocalStats(server, clusterInfo, clusterStats, kibana) {
|
|||
* @param {function} callCluster The callWithInternalUser handler (exposed for testing)
|
||||
* @return {Promise} The object containing the current Elasticsearch cluster's telemetry.
|
||||
*/
|
||||
export async function getLocalStatsWithCaller(server, callCluster) {
|
||||
export async function getLocalStats({ server, callCluster }) {
|
||||
const [ clusterInfo, clusterStats, kibana ] = await Promise.all([
|
||||
getClusterInfo(callCluster), // cluster info
|
||||
getClusterStats(callCluster), // cluster stats (not to be confused with cluster _state_)
|
||||
|
@ -60,19 +60,3 @@ export async function getLocalStatsWithCaller(server, callCluster) {
|
|||
|
||||
return handleLocalStats(server, clusterInfo, clusterStats, kibana);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get statistics for the connected Elasticsearch cluster.
|
||||
*
|
||||
* @param {Object} req The incoming request
|
||||
* @param {Boolean} useRequestUser callWithRequest, otherwise callWithInternalUser
|
||||
* @return {Promise} The cluster object containing telemetry.
|
||||
*/
|
||||
export async function getLocalStats(req, { useInternalUser = false } = {}) {
|
||||
const { server } = req;
|
||||
const { callWithRequest, callWithInternalUser } = server.plugins.elasticsearch.getCluster('data');
|
||||
const callCluster = useInternalUser ? callWithInternalUser : (...args) => callWithRequest(req, ...args);
|
||||
|
||||
return await getLocalStatsWithCaller(server, callCluster);
|
||||
}
|
||||
|
|
|
@ -19,27 +19,10 @@
|
|||
|
||||
// @ts-ignore
|
||||
import { getLocalStats } from './get_local_stats';
|
||||
import { StatsGetter, getStatsCollectionConfig } from '../collection_manager';
|
||||
|
||||
/**
|
||||
* Get the telemetry data.
|
||||
*
|
||||
* @param {Object} req The incoming request.
|
||||
* @param {Object} config Kibana config.
|
||||
* @param {String} start The start time of the request (likely 20m ago).
|
||||
* @param {String} end The end time of the request.
|
||||
* @param {Boolean} unencrypted Is the request payload going to be unencrypted.
|
||||
* @return {Promise} An array of telemetry objects.
|
||||
*/
|
||||
export async function getStats(
|
||||
req: any,
|
||||
config: any,
|
||||
start: string,
|
||||
end: string,
|
||||
unencrypted: boolean
|
||||
) {
|
||||
return [
|
||||
await getLocalStats(req, {
|
||||
useInternalUser: !unencrypted,
|
||||
}),
|
||||
];
|
||||
}
|
||||
export const getStats: StatsGetter = async function(config) {
|
||||
const { callCluster, server } = getStatsCollectionConfig(config, 'data');
|
||||
|
||||
return [await getLocalStats({ callCluster, server })];
|
||||
};
|
||||
|
|
|
@ -19,6 +19,4 @@
|
|||
|
||||
// @ts-ignore
|
||||
export { getLocalStats } from './get_local_stats';
|
||||
|
||||
// @ts-ignore
|
||||
export { getStats } from './get_stats';
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { TelemetrySavedObject } from '../telemetry_repository/get_telemetry_saved_object';
|
||||
|
||||
interface GetTelemetryAllowChangingOptInStatus {
|
||||
configTelemetryAllowChangingOptInStatus: boolean;
|
||||
telemetrySavedObject: TelemetrySavedObject;
|
||||
}
|
||||
|
||||
export function getTelemetryAllowChangingOptInStatus({
|
||||
telemetrySavedObject,
|
||||
configTelemetryAllowChangingOptInStatus,
|
||||
}: GetTelemetryAllowChangingOptInStatus) {
|
||||
if (!telemetrySavedObject) {
|
||||
return configTelemetryAllowChangingOptInStatus;
|
||||
}
|
||||
|
||||
if (typeof telemetrySavedObject.telemetryAllowChangingOptInStatus === 'undefined') {
|
||||
return configTelemetryAllowChangingOptInStatus;
|
||||
}
|
||||
|
||||
return telemetrySavedObject.telemetryAllowChangingOptInStatus;
|
||||
}
|
|
@ -18,72 +18,47 @@
|
|||
*/
|
||||
|
||||
import { getTelemetryOptIn } from './get_telemetry_opt_in';
|
||||
import { TelemetrySavedObject } from '../telemetry_repository/get_telemetry_saved_object';
|
||||
|
||||
describe('get_telemetry_opt_in', () => {
|
||||
it('returns false when request path is not /app*', async () => {
|
||||
const params = getCallGetTelemetryOptInParams({
|
||||
requestPath: '/foo/bar',
|
||||
});
|
||||
|
||||
const result = await callGetTelemetryOptIn(params);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('returns null when saved object not found', async () => {
|
||||
describe('getTelemetryOptIn', () => {
|
||||
it('returns null when saved object not found', () => {
|
||||
const params = getCallGetTelemetryOptInParams({
|
||||
savedObjectNotFound: true,
|
||||
});
|
||||
|
||||
const result = await callGetTelemetryOptIn(params);
|
||||
const result = callGetTelemetryOptIn(params);
|
||||
|
||||
expect(result).toBe(null);
|
||||
});
|
||||
|
||||
it('returns false when saved object forbidden', async () => {
|
||||
it('returns false when saved object forbidden', () => {
|
||||
const params = getCallGetTelemetryOptInParams({
|
||||
savedObjectForbidden: true,
|
||||
});
|
||||
|
||||
const result = await callGetTelemetryOptIn(params);
|
||||
const result = callGetTelemetryOptIn(params);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('throws an error on unexpected saved object error', async () => {
|
||||
const params = getCallGetTelemetryOptInParams({
|
||||
savedObjectOtherError: true,
|
||||
});
|
||||
|
||||
let threw = false;
|
||||
try {
|
||||
await callGetTelemetryOptIn(params);
|
||||
} catch (err) {
|
||||
threw = true;
|
||||
expect(err.message).toBe(SavedObjectOtherErrorMessage);
|
||||
}
|
||||
|
||||
expect(threw).toBe(true);
|
||||
});
|
||||
|
||||
it('returns null if enabled is null or undefined', async () => {
|
||||
it('returns null if enabled is null or undefined', () => {
|
||||
for (const enabled of [null, undefined]) {
|
||||
const params = getCallGetTelemetryOptInParams({
|
||||
enabled,
|
||||
});
|
||||
|
||||
const result = await callGetTelemetryOptIn(params);
|
||||
const result = callGetTelemetryOptIn(params);
|
||||
|
||||
expect(result).toBe(null);
|
||||
}
|
||||
});
|
||||
|
||||
it('returns true when enabled is true', async () => {
|
||||
it('returns true when enabled is true', () => {
|
||||
const params = getCallGetTelemetryOptInParams({
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
const result = await callGetTelemetryOptIn(params);
|
||||
const result = callGetTelemetryOptIn(params);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
@ -146,24 +121,24 @@ describe('get_telemetry_opt_in', () => {
|
|||
});
|
||||
|
||||
interface CallGetTelemetryOptInParams {
|
||||
requestPath: string;
|
||||
savedObjectNotFound: boolean;
|
||||
savedObjectForbidden: boolean;
|
||||
savedObjectOtherError: boolean;
|
||||
enabled: boolean | null | undefined;
|
||||
lastVersionChecked?: any; // should be a string, but test with non-strings
|
||||
currentKibanaVersion: string;
|
||||
result?: boolean | null;
|
||||
enabled: boolean | null | undefined;
|
||||
configTelemetryOptIn: boolean | null;
|
||||
allowChangingOptInStatus: boolean;
|
||||
}
|
||||
|
||||
const DefaultParams = {
|
||||
requestPath: '/app/something',
|
||||
savedObjectNotFound: false,
|
||||
savedObjectForbidden: false,
|
||||
savedObjectOtherError: false,
|
||||
enabled: true,
|
||||
lastVersionChecked: '8.0.0',
|
||||
currentKibanaVersion: '8.0.0',
|
||||
configTelemetryOptIn: null,
|
||||
allowChangingOptInStatus: true,
|
||||
};
|
||||
|
||||
function getCallGetTelemetryOptInParams(
|
||||
|
@ -172,43 +147,28 @@ function getCallGetTelemetryOptInParams(
|
|||
return { ...DefaultParams, ...overrides };
|
||||
}
|
||||
|
||||
async function callGetTelemetryOptIn(params: CallGetTelemetryOptInParams): Promise<boolean | null> {
|
||||
const { currentKibanaVersion } = params;
|
||||
const request = getMockRequest(params);
|
||||
return await getTelemetryOptIn({ request, currentKibanaVersion });
|
||||
function callGetTelemetryOptIn(params: CallGetTelemetryOptInParams) {
|
||||
const { currentKibanaVersion, configTelemetryOptIn, allowChangingOptInStatus } = params;
|
||||
const telemetrySavedObject = getMockTelemetrySavedObject(params);
|
||||
return getTelemetryOptIn({
|
||||
currentKibanaVersion,
|
||||
telemetrySavedObject,
|
||||
allowChangingOptInStatus,
|
||||
configTelemetryOptIn,
|
||||
});
|
||||
}
|
||||
|
||||
function getMockRequest(params: CallGetTelemetryOptInParams): any {
|
||||
function getMockTelemetrySavedObject(params: CallGetTelemetryOptInParams): TelemetrySavedObject {
|
||||
const { savedObjectNotFound, savedObjectForbidden } = params;
|
||||
if (savedObjectForbidden) {
|
||||
return false;
|
||||
}
|
||||
if (savedObjectNotFound) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
path: params.requestPath,
|
||||
getSavedObjectsClient() {
|
||||
return getMockSavedObjectsClient(params);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const SavedObjectNotFoundMessage = 'savedObjectNotFound';
|
||||
const SavedObjectForbiddenMessage = 'savedObjectForbidden';
|
||||
const SavedObjectOtherErrorMessage = 'savedObjectOtherError';
|
||||
|
||||
function getMockSavedObjectsClient(params: CallGetTelemetryOptInParams) {
|
||||
return {
|
||||
async get(type: string, id: string) {
|
||||
if (params.savedObjectNotFound) throw new Error(SavedObjectNotFoundMessage);
|
||||
if (params.savedObjectForbidden) throw new Error(SavedObjectForbiddenMessage);
|
||||
if (params.savedObjectOtherError) throw new Error(SavedObjectOtherErrorMessage);
|
||||
|
||||
const enabled = params.enabled;
|
||||
const lastVersionChecked = params.lastVersionChecked;
|
||||
return { attributes: { enabled, lastVersionChecked } };
|
||||
},
|
||||
errors: {
|
||||
isNotFoundError(error: any) {
|
||||
return error.message === SavedObjectNotFoundMessage;
|
||||
},
|
||||
isForbiddenError(error: any) {
|
||||
return error.message === SavedObjectForbiddenMessage;
|
||||
},
|
||||
},
|
||||
enabled: params.enabled,
|
||||
lastVersionChecked: params.lastVersionChecked,
|
||||
};
|
||||
}
|
|
@ -18,67 +18,51 @@
|
|||
*/
|
||||
|
||||
import semver from 'semver';
|
||||
import { TelemetrySavedObject } from '../telemetry_repository/get_telemetry_saved_object';
|
||||
|
||||
import { SavedObjectAttributes } from './routes/opt_in';
|
||||
|
||||
interface GetTelemetryOptIn {
|
||||
request: any;
|
||||
interface GetTelemetryOptInConfig {
|
||||
telemetrySavedObject: TelemetrySavedObject;
|
||||
currentKibanaVersion: string;
|
||||
allowChangingOptInStatus: boolean;
|
||||
configTelemetryOptIn: boolean | null;
|
||||
}
|
||||
|
||||
// Returns whether telemetry has been opt'ed into or not.
|
||||
// Returns null not set, meaning Kibana should prompt in the UI.
|
||||
export async function getTelemetryOptIn({
|
||||
request,
|
||||
currentKibanaVersion,
|
||||
}: GetTelemetryOptIn): Promise<boolean | null> {
|
||||
const isRequestingApplication = request.path.startsWith('/app');
|
||||
type GetTelemetryOptIn = (config: GetTelemetryOptInConfig) => null | boolean;
|
||||
|
||||
// Prevent interstitial screens (such as the space selector) from prompting for telemetry
|
||||
if (!isRequestingApplication) {
|
||||
export const getTelemetryOptIn: GetTelemetryOptIn = ({
|
||||
telemetrySavedObject,
|
||||
currentKibanaVersion,
|
||||
allowChangingOptInStatus,
|
||||
configTelemetryOptIn,
|
||||
}) => {
|
||||
if (typeof configTelemetryOptIn === 'boolean' && !allowChangingOptInStatus) {
|
||||
return configTelemetryOptIn;
|
||||
}
|
||||
|
||||
if (telemetrySavedObject === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const savedObjectsClient = request.getSavedObjectsClient();
|
||||
|
||||
let savedObject;
|
||||
try {
|
||||
savedObject = await savedObjectsClient.get('telemetry', 'telemetry');
|
||||
} catch (error) {
|
||||
if (savedObjectsClient.errors.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 (savedObjectsClient.errors.isForbiddenError(error)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
throw error;
|
||||
if (telemetrySavedObject === null || typeof telemetrySavedObject.enabled !== 'boolean') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { attributes }: { attributes: SavedObjectAttributes } = savedObject;
|
||||
|
||||
// if enabled is already null, return null
|
||||
if (attributes.enabled == null) return null;
|
||||
|
||||
const enabled = !!attributes.enabled;
|
||||
const savedOptIn = telemetrySavedObject.enabled;
|
||||
|
||||
// if enabled is true, return it
|
||||
if (enabled === true) return enabled;
|
||||
if (savedOptIn === true) return savedOptIn;
|
||||
|
||||
// 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.
|
||||
|
||||
const lastKibanaVersion = attributes.lastVersionChecked;
|
||||
const lastKibanaVersion = telemetrySavedObject.lastVersionChecked;
|
||||
|
||||
// if the last kibana version isn't set, or is somehow not a string, return null
|
||||
if (typeof lastKibanaVersion !== 'string') return null;
|
||||
|
||||
// if version hasn't changed, just return enabled value
|
||||
if (lastKibanaVersion === currentKibanaVersion) return enabled;
|
||||
if (lastKibanaVersion === currentKibanaVersion) return savedOptIn;
|
||||
|
||||
const lastSemver = parseSemver(lastKibanaVersion);
|
||||
const currentSemver = parseSemver(currentKibanaVersion);
|
||||
|
@ -93,8 +77,8 @@ export async function getTelemetryOptIn({
|
|||
}
|
||||
|
||||
// current version X.Y is not greater than last version X.Y, return enabled
|
||||
return enabled;
|
||||
}
|
||||
return savedOptIn;
|
||||
};
|
||||
|
||||
function parseSemver(version: string): semver.SemVer | null {
|
||||
// semver functions both return nulls AND throw exceptions: "it depends!"
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { getTelemetryUsageFetcher } from './get_telemetry_usage_fetcher';
|
||||
import { TelemetrySavedObject } from '../telemetry_repository/get_telemetry_saved_object';
|
||||
|
||||
describe('getTelemetryUsageFetcher', () => {
|
||||
it('returns kibana.yml config when saved object not found', () => {
|
||||
const params: CallGetTelemetryUsageFetcherParams = {
|
||||
savedObjectNotFound: true,
|
||||
configSendUsageFrom: 'browser',
|
||||
};
|
||||
|
||||
const result = callGetTelemetryUsageFetcher(params);
|
||||
|
||||
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,
|
||||
configSendUsageFrom: 'server',
|
||||
};
|
||||
|
||||
const result = callGetTelemetryUsageFetcher(params);
|
||||
|
||||
expect(result).toBe('server');
|
||||
});
|
||||
});
|
||||
|
||||
interface CallGetTelemetryUsageFetcherParams {
|
||||
savedObjectNotFound?: boolean;
|
||||
savedObjectForbidden?: boolean;
|
||||
savedSendUsagefrom?: 'browser' | 'server';
|
||||
configSendUsageFrom: 'browser' | 'server';
|
||||
}
|
||||
|
||||
function callGetTelemetryUsageFetcher(params: CallGetTelemetryUsageFetcherParams) {
|
||||
const telemetrySavedObject = getMockTelemetrySavedObject(params);
|
||||
const configTelemetrySendUsageFrom = params.configSendUsageFrom;
|
||||
return getTelemetryUsageFetcher({ configTelemetrySendUsageFrom, telemetrySavedObject });
|
||||
}
|
||||
|
||||
function getMockTelemetrySavedObject(
|
||||
params: CallGetTelemetryUsageFetcherParams
|
||||
): TelemetrySavedObject {
|
||||
const { savedObjectNotFound, savedObjectForbidden } = params;
|
||||
if (savedObjectForbidden) {
|
||||
return false;
|
||||
}
|
||||
if (savedObjectNotFound) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
sendUsageFrom: params.savedSendUsagefrom,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { TelemetrySavedObject } from '../telemetry_repository/get_telemetry_saved_object';
|
||||
|
||||
interface GetTelemetryUsageFetcherConfig {
|
||||
configTelemetrySendUsageFrom: 'browser' | 'server';
|
||||
telemetrySavedObject: TelemetrySavedObject;
|
||||
}
|
||||
|
||||
export function getTelemetryUsageFetcher({
|
||||
telemetrySavedObject,
|
||||
configTelemetrySendUsageFrom,
|
||||
}: GetTelemetryUsageFetcherConfig) {
|
||||
if (!telemetrySavedObject) {
|
||||
return configTelemetrySendUsageFrom;
|
||||
}
|
||||
|
||||
if (typeof telemetrySavedObject.sendUsageFrom === 'undefined') {
|
||||
return configTelemetrySendUsageFrom;
|
||||
}
|
||||
|
||||
return telemetrySavedObject.sendUsageFrom;
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
export { replaceTelemetryInjectedVars } from './replace_injected_vars';
|
||||
export { getTelemetryOptIn } from './get_telemetry_opt_in';
|
||||
export { getTelemetryUsageFetcher } from './get_telemetry_usage_fetcher';
|
||||
export { getTelemetryAllowChangingOptInStatus } from './get_telemetry_allow_changing_opt_in_status';
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { getTelemetrySavedObject } from '../telemetry_repository';
|
||||
import { getTelemetryOptIn } from './get_telemetry_opt_in';
|
||||
import { getTelemetryUsageFetcher } from './get_telemetry_usage_fetcher';
|
||||
import { getTelemetryAllowChangingOptInStatus } from './get_telemetry_allow_changing_opt_in_status';
|
||||
|
||||
export async function replaceTelemetryInjectedVars(request: any) {
|
||||
const config = request.server.config();
|
||||
const configTelemetrySendUsageFrom = config.get('telemetry.sendUsageFrom');
|
||||
const configTelemetryOptIn = config.get('telemetry.optIn');
|
||||
const configTelemetryAllowChangingOptInStatus = config.get('telemetry.allowChangingOptInStatus');
|
||||
const isRequestingApplication = request.path.startsWith('/app');
|
||||
|
||||
// Prevent interstitial screens (such as the space selector) from prompting for telemetry
|
||||
if (!isRequestingApplication) {
|
||||
return {
|
||||
telemetryOptedIn: false,
|
||||
};
|
||||
}
|
||||
|
||||
const currentKibanaVersion = config.get('pkg.version');
|
||||
const savedObjectsClient = request.getSavedObjectsClient();
|
||||
const telemetrySavedObject = await getTelemetrySavedObject(savedObjectsClient);
|
||||
const allowChangingOptInStatus = getTelemetryAllowChangingOptInStatus({
|
||||
configTelemetryAllowChangingOptInStatus,
|
||||
telemetrySavedObject,
|
||||
});
|
||||
|
||||
const telemetryOptedIn = getTelemetryOptIn({
|
||||
configTelemetryOptIn,
|
||||
allowChangingOptInStatus,
|
||||
telemetrySavedObject,
|
||||
currentKibanaVersion,
|
||||
});
|
||||
|
||||
const telemetrySendUsageFrom = getTelemetryUsageFetcher({
|
||||
configTelemetrySendUsageFrom,
|
||||
telemetrySavedObject,
|
||||
});
|
||||
|
||||
return {
|
||||
telemetryOptedIn,
|
||||
telemetrySendUsageFrom,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { getTelemetrySavedObject } from './get_telemetry_saved_object';
|
||||
import { SavedObjectsErrorHelpers } from '../../../../../core/server';
|
||||
|
||||
describe('getTelemetrySavedObject', () => {
|
||||
it('returns null when saved object not found', async () => {
|
||||
const params = getCallGetTelemetrySavedObjectParams({
|
||||
savedObjectNotFound: true,
|
||||
});
|
||||
|
||||
const result = await callGetTelemetrySavedObject(params);
|
||||
|
||||
expect(result).toBe(null);
|
||||
});
|
||||
|
||||
it('returns false when saved object forbidden', async () => {
|
||||
const params = getCallGetTelemetrySavedObjectParams({
|
||||
savedObjectForbidden: true,
|
||||
});
|
||||
|
||||
const result = await callGetTelemetrySavedObject(params);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('throws an error on unexpected saved object error', async () => {
|
||||
const params = getCallGetTelemetrySavedObjectParams({
|
||||
savedObjectOtherError: true,
|
||||
});
|
||||
|
||||
let threw = false;
|
||||
try {
|
||||
await callGetTelemetrySavedObject(params);
|
||||
} catch (err) {
|
||||
threw = true;
|
||||
expect(err.message).toBe(SavedObjectOtherErrorMessage);
|
||||
}
|
||||
|
||||
expect(threw).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
interface CallGetTelemetrySavedObjectParams {
|
||||
savedObjectNotFound: boolean;
|
||||
savedObjectForbidden: boolean;
|
||||
savedObjectOtherError: boolean;
|
||||
result?: any;
|
||||
}
|
||||
|
||||
const DefaultParams = {
|
||||
savedObjectNotFound: false,
|
||||
savedObjectForbidden: false,
|
||||
savedObjectOtherError: false,
|
||||
};
|
||||
|
||||
function getCallGetTelemetrySavedObjectParams(
|
||||
overrides: Partial<CallGetTelemetrySavedObjectParams>
|
||||
): CallGetTelemetrySavedObjectParams {
|
||||
return { ...DefaultParams, ...overrides };
|
||||
}
|
||||
|
||||
async function callGetTelemetrySavedObject(params: CallGetTelemetrySavedObjectParams) {
|
||||
const savedObjectsClient = getMockSavedObjectsClient(params);
|
||||
return await getTelemetrySavedObject(savedObjectsClient);
|
||||
}
|
||||
|
||||
const SavedObjectForbiddenMessage = 'savedObjectForbidden';
|
||||
const SavedObjectOtherErrorMessage = 'savedObjectOtherError';
|
||||
|
||||
function getMockSavedObjectsClient(params: CallGetTelemetrySavedObjectParams) {
|
||||
return {
|
||||
async get(type: string, id: string) {
|
||||
if (params.savedObjectNotFound) throw SavedObjectsErrorHelpers.createGenericNotFoundError();
|
||||
if (params.savedObjectForbidden)
|
||||
throw SavedObjectsErrorHelpers.decorateForbiddenError(
|
||||
new Error(SavedObjectForbiddenMessage)
|
||||
);
|
||||
if (params.savedObjectOtherError)
|
||||
throw SavedObjectsErrorHelpers.decorateGeneralError(
|
||||
new Error(SavedObjectOtherErrorMessage)
|
||||
);
|
||||
|
||||
return { attributes: { enabled: null } };
|
||||
},
|
||||
};
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { TelemetrySavedObjectAttributes } from './';
|
||||
import { SavedObjectsErrorHelpers } from '../../../../../core/server';
|
||||
|
||||
export type TelemetrySavedObject = TelemetrySavedObjectAttributes | null | false;
|
||||
type GetTelemetrySavedObject = (repository: any) => Promise<TelemetrySavedObject>;
|
||||
|
||||
export const getTelemetrySavedObject: GetTelemetrySavedObject = async (repository: any) => {
|
||||
try {
|
||||
const { attributes } = await repository.get('telemetry', 'telemetry');
|
||||
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;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
export { getTelemetrySavedObject, TelemetrySavedObject } from './get_telemetry_saved_object';
|
||||
export { updateTelemetrySavedObject } from './update_telemetry_saved_object';
|
||||
|
||||
export interface TelemetrySavedObjectAttributes {
|
||||
enabled?: boolean | null;
|
||||
lastVersionChecked?: string;
|
||||
sendUsageFrom?: 'browser' | 'server';
|
||||
lastReported?: number;
|
||||
telemetryAllowChangingOptInStatus?: boolean;
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { TelemetrySavedObjectAttributes } from './';
|
||||
import { SavedObjectsErrorHelpers } from '../../../../../core/server';
|
||||
|
||||
export async function updateTelemetrySavedObject(
|
||||
savedObjectsClient: any,
|
||||
savedObjectAttributes: TelemetrySavedObjectAttributes
|
||||
) {
|
||||
try {
|
||||
return await savedObjectsClient.update('telemetry', 'telemetry', savedObjectAttributes);
|
||||
} catch (err) {
|
||||
if (SavedObjectsErrorHelpers.isNotFoundError(err)) {
|
||||
return await savedObjectsClient.create('telemetry', savedObjectAttributes, {
|
||||
id: 'telemetry',
|
||||
overwrite: true,
|
||||
});
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
|
@ -39,13 +39,13 @@ export default function(kibana: any) {
|
|||
injectDefaultVars(server: Server) {
|
||||
const config = server.config();
|
||||
return {
|
||||
uiMetricEnabled: config.get('ui_metric.enabled'),
|
||||
debugUiMetric: config.get('ui_metric.debug'),
|
||||
};
|
||||
},
|
||||
mappings: require('./mappings.json'),
|
||||
hacks: ['plugins/ui_metric/hacks/ui_metric_init'],
|
||||
},
|
||||
|
||||
init(server: Legacy.Server) {
|
||||
registerUiMetricRoute(server);
|
||||
},
|
||||
|
|
|
@ -20,15 +20,26 @@
|
|||
// @ts-ignore
|
||||
import { uiModules } from 'ui/modules';
|
||||
import chrome from 'ui/chrome';
|
||||
import { createAnalyticsReporter, setTelemetryReporter } from '../services/telemetry_analytics';
|
||||
import { kfetch } from 'ui/kfetch';
|
||||
import {
|
||||
createAnalyticsReporter,
|
||||
setTelemetryReporter,
|
||||
trackUserAgent,
|
||||
} from '../services/telemetry_analytics';
|
||||
import { isUnauthenticated } from '../../../telemetry/public/services';
|
||||
|
||||
function telemetryInit($injector: any) {
|
||||
const localStorage = $injector.get('localStorage');
|
||||
const uiMetricEnabled = chrome.getInjected('uiMetricEnabled');
|
||||
const debug = chrome.getInjected('debugUiMetric');
|
||||
const $http = $injector.get('$http');
|
||||
const basePath = chrome.getBasePath();
|
||||
const uiReporter = createAnalyticsReporter({ localStorage, $http, basePath, debug });
|
||||
if (!uiMetricEnabled || isUnauthenticated()) {
|
||||
return;
|
||||
}
|
||||
const localStorage = $injector.get('localStorage');
|
||||
|
||||
const uiReporter = createAnalyticsReporter({ localStorage, debug, kfetch });
|
||||
setTelemetryReporter(uiReporter);
|
||||
uiReporter.start();
|
||||
trackUserAgent('kibana');
|
||||
}
|
||||
|
||||
uiModules.get('kibana').run(telemetryInit);
|
||||
|
|
|
@ -17,5 +17,5 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export { createUiStatsReporter } from './services/telemetry_analytics';
|
||||
export { METRIC_TYPE } from '@kbn/analytics';
|
||||
export { createUiStatsReporter, trackUserAgent } from './services/telemetry_analytics';
|
||||
export { METRIC_TYPE, UiStatsMetricType } from '@kbn/analytics';
|
||||
|
|
|
@ -17,7 +17,9 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { createReporter, Reporter, UiStatsMetricType } from '@kbn/analytics';
|
||||
import { Reporter, UiStatsMetricType } from '@kbn/analytics';
|
||||
// @ts-ignore
|
||||
import { addSystemApiHeader } from 'ui/system_api';
|
||||
|
||||
let telemetryReporter: Reporter;
|
||||
|
||||
|
@ -39,28 +41,36 @@ export const createUiStatsReporter = (appName: string) => (
|
|||
}
|
||||
};
|
||||
|
||||
export const trackUserAgent = (appName: string) => {
|
||||
if (telemetryReporter) {
|
||||
return telemetryReporter.reportUserAgent(appName);
|
||||
}
|
||||
};
|
||||
|
||||
interface AnalyicsReporterConfig {
|
||||
localStorage: any;
|
||||
basePath: string;
|
||||
debug: boolean;
|
||||
$http: ng.IHttpService;
|
||||
kfetch: any;
|
||||
}
|
||||
|
||||
export function createAnalyticsReporter(config: AnalyicsReporterConfig) {
|
||||
const { localStorage, basePath, debug } = config;
|
||||
const { localStorage, debug, kfetch } = config;
|
||||
|
||||
return createReporter({
|
||||
return new Reporter({
|
||||
debug,
|
||||
storage: localStorage,
|
||||
async http(report) {
|
||||
const url = `${basePath}/api/telemetry/report`;
|
||||
await fetch(url, {
|
||||
const response = await kfetch({
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'kbn-xsrf': 'true',
|
||||
},
|
||||
body: JSON.stringify({ report }),
|
||||
pathname: '/api/telemetry/report',
|
||||
body: JSON.stringify(report),
|
||||
headers: addSystemApiHeader({}),
|
||||
});
|
||||
|
||||
if (response.status !== 'ok') {
|
||||
throw Error('Unable to store report.');
|
||||
}
|
||||
return response;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -18,7 +18,6 @@
|
|||
*/
|
||||
|
||||
import Joi from 'joi';
|
||||
import Boom from 'boom';
|
||||
import { Report } from '@kbn/analytics';
|
||||
import { Server } from 'hapi';
|
||||
|
||||
|
@ -27,15 +26,27 @@ export async function storeReport(server: any, report: Report) {
|
|||
const { callWithInternalUser } = server.plugins.elasticsearch.getCluster('admin');
|
||||
const internalRepository = getSavedObjectsRepository(callWithInternalUser);
|
||||
|
||||
const metricKeys = Object.keys(report.uiStatsMetrics);
|
||||
return Promise.all(
|
||||
metricKeys.map(async key => {
|
||||
const metric = report.uiStatsMetrics[key];
|
||||
const uiStatsMetrics = report.uiStatsMetrics ? Object.entries(report.uiStatsMetrics) : [];
|
||||
const userAgents = report.userAgent ? Object.entries(report.userAgent) : [];
|
||||
return Promise.all([
|
||||
...userAgents.map(async ([key, metric]) => {
|
||||
const { userAgent } = metric;
|
||||
const savedObjectId = `${key}:${userAgent}`;
|
||||
return await internalRepository.create(
|
||||
'ui-metric',
|
||||
{ count: 1 },
|
||||
{
|
||||
id: savedObjectId,
|
||||
overwrite: true,
|
||||
}
|
||||
);
|
||||
}),
|
||||
...uiStatsMetrics.map(async ([key, metric]) => {
|
||||
const { appName, eventName } = metric;
|
||||
const savedObjectId = `${appName}:${eventName}`;
|
||||
return internalRepository.incrementCounter('ui-metric', savedObjectId, 'count');
|
||||
})
|
||||
);
|
||||
return await internalRepository.incrementCounter('ui-metric', savedObjectId, 'count');
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
export function registerUiMetricRoute(server: Server) {
|
||||
|
@ -45,36 +56,46 @@ export function registerUiMetricRoute(server: Server) {
|
|||
options: {
|
||||
validate: {
|
||||
payload: Joi.object({
|
||||
report: Joi.object({
|
||||
uiStatsMetrics: Joi.object()
|
||||
.pattern(
|
||||
/.*/,
|
||||
Joi.object({
|
||||
key: Joi.string().required(),
|
||||
type: Joi.string().required(),
|
||||
appName: Joi.string().required(),
|
||||
eventName: Joi.string().required(),
|
||||
stats: Joi.object({
|
||||
min: Joi.number(),
|
||||
sum: Joi.number(),
|
||||
max: Joi.number(),
|
||||
avg: Joi.number(),
|
||||
}).allow(null),
|
||||
})
|
||||
)
|
||||
.allow(null),
|
||||
}),
|
||||
reportVersion: Joi.number().optional(),
|
||||
userAgent: Joi.object()
|
||||
.pattern(
|
||||
/.*/,
|
||||
Joi.object({
|
||||
key: Joi.string().required(),
|
||||
type: Joi.string().required(),
|
||||
appName: Joi.string().required(),
|
||||
userAgent: Joi.string().required(),
|
||||
})
|
||||
)
|
||||
.allow(null)
|
||||
.optional(),
|
||||
uiStatsMetrics: Joi.object()
|
||||
.pattern(
|
||||
/.*/,
|
||||
Joi.object({
|
||||
key: Joi.string().required(),
|
||||
type: Joi.string().required(),
|
||||
appName: Joi.string().required(),
|
||||
eventName: Joi.string().required(),
|
||||
stats: Joi.object({
|
||||
min: Joi.number(),
|
||||
sum: Joi.number(),
|
||||
max: Joi.number(),
|
||||
avg: Joi.number(),
|
||||
}).allow(null),
|
||||
})
|
||||
)
|
||||
.allow(null),
|
||||
}),
|
||||
},
|
||||
},
|
||||
handler: async (req: any, h: any) => {
|
||||
const { report } = req.payload;
|
||||
|
||||
try {
|
||||
const report = req.payload;
|
||||
await storeReport(server, report);
|
||||
return {};
|
||||
return { status: 'ok' };
|
||||
} catch (error) {
|
||||
return new Boom('Something went wrong', { statusCode: error.status });
|
||||
return { status: 'fail' };
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -18,48 +18,59 @@
|
|||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { ReportManager } from '@kbn/analytics';
|
||||
import { ReportManager, METRIC_TYPE } from '@kbn/analytics';
|
||||
|
||||
export default function ({ getService }) {
|
||||
const supertest = getService('supertest');
|
||||
const es = getService('es');
|
||||
|
||||
const createMetric = (eventName) => ({
|
||||
key: ReportManager.createMetricKey({ appName: 'myApp', type: 'click', eventName }),
|
||||
const createStatsMetric = (eventName) => ({
|
||||
key: ReportManager.createMetricKey({ appName: 'myApp', type: METRIC_TYPE.CLICK, eventName }),
|
||||
eventName,
|
||||
appName: 'myApp',
|
||||
type: 'click',
|
||||
type: METRIC_TYPE.CLICK,
|
||||
stats: { sum: 1, avg: 1, min: 1, max: 1 },
|
||||
});
|
||||
|
||||
const createUserAgentMetric = (appName) => ({
|
||||
key: ReportManager.createMetricKey({ appName, type: METRIC_TYPE.USER_AGENT }),
|
||||
appName,
|
||||
type: METRIC_TYPE.USER_AGENT,
|
||||
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.87 Safari/537.36',
|
||||
});
|
||||
|
||||
describe('ui_metric API', () => {
|
||||
const uiStatsMetric = createMetric('myEvent');
|
||||
const report = {
|
||||
uiStatsMetrics: {
|
||||
[uiStatsMetric.key]: uiStatsMetric,
|
||||
}
|
||||
};
|
||||
|
||||
it('increments the count field in the document defined by the {app}/{action_type} path', async () => {
|
||||
const uiStatsMetric = createStatsMetric('myEvent');
|
||||
const report = {
|
||||
uiStatsMetrics: {
|
||||
[uiStatsMetric.key]: uiStatsMetric,
|
||||
}
|
||||
};
|
||||
await supertest
|
||||
.post('/api/telemetry/report')
|
||||
.set('kbn-xsrf', 'kibana')
|
||||
.set('content-type', 'application/json')
|
||||
.send({ report })
|
||||
.send(report)
|
||||
.expect(200);
|
||||
|
||||
return es.search({
|
||||
index: '.kibana',
|
||||
q: 'type:user-action',
|
||||
}).then(response => {
|
||||
const ids = response.hits.hits.map(({ _id }) => _id);
|
||||
expect(ids.includes('user-action:myApp:myEvent'));
|
||||
});
|
||||
const response = await es.search({ index: '.kibana', q: 'type:ui-metric' });
|
||||
const ids = response.hits.hits.map(({ _id }) => _id);
|
||||
expect(ids.includes('ui-metric:myApp:myEvent')).to.eql(true);
|
||||
});
|
||||
|
||||
it('supports multiple events', async () => {
|
||||
const uiStatsMetric1 = createMetric('myEvent1');
|
||||
const uiStatsMetric2 = createMetric('myEvent2');
|
||||
const userAgentMetric = createUserAgentMetric('kibana');
|
||||
const uiStatsMetric1 = createStatsMetric('myEvent');
|
||||
const hrTime = process.hrtime();
|
||||
const nano = hrTime[0] * 1000000000 + hrTime[1];
|
||||
const uniqueEventName = `myEvent${nano}`;
|
||||
const uiStatsMetric2 = createStatsMetric(uniqueEventName);
|
||||
const report = {
|
||||
userAgent: {
|
||||
[userAgentMetric.key]: userAgentMetric,
|
||||
},
|
||||
uiStatsMetrics: {
|
||||
[uiStatsMetric1.key]: uiStatsMetric1,
|
||||
[uiStatsMetric2.key]: uiStatsMetric2,
|
||||
|
@ -69,17 +80,14 @@ export default function ({ getService }) {
|
|||
.post('/api/telemetry/report')
|
||||
.set('kbn-xsrf', 'kibana')
|
||||
.set('content-type', 'application/json')
|
||||
.send({ report })
|
||||
.send(report)
|
||||
.expect(200);
|
||||
|
||||
return es.search({
|
||||
index: '.kibana',
|
||||
q: 'type:user-action',
|
||||
}).then(response => {
|
||||
const ids = response.hits.hits.map(({ _id }) => _id);
|
||||
expect(ids.includes('user-action:myApp:myEvent1'));
|
||||
expect(ids.includes('user-action:myApp:myEvent2'));
|
||||
});
|
||||
const response = await es.search({ index: '.kibana', q: 'type:ui-metric' });
|
||||
const ids = response.hits.hits.map(({ _id }) => _id);
|
||||
expect(ids.includes('ui-metric:myApp:myEvent')).to.eql(true);
|
||||
expect(ids.includes(`ui-metric:myApp:${uniqueEventName}`)).to.eql(true);
|
||||
expect(ids.includes(`ui-metric:kibana-user_agent:${userAgentMetric.userAgent}`)).to.eql(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -59,7 +59,6 @@ export default function () {
|
|||
`--server.maxPayloadBytes=1679958`,
|
||||
],
|
||||
},
|
||||
|
||||
services
|
||||
};
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
import { useEffect } from 'react';
|
||||
import {
|
||||
createUiStatsReporter,
|
||||
UiStatsMetricType,
|
||||
METRIC_TYPE,
|
||||
} from '../../../../../../src/legacy/core_plugins/ui_metric/public';
|
||||
|
||||
|
@ -36,7 +37,7 @@ function getTrackerForApp(app: string) {
|
|||
|
||||
interface TrackOptions {
|
||||
app: ObservabilityApp;
|
||||
metricType?: METRIC_TYPE;
|
||||
metricType?: UiStatsMetricType;
|
||||
delay?: number; // in ms
|
||||
}
|
||||
type EffectDeps = unknown[];
|
||||
|
@ -76,7 +77,7 @@ export function useTrackPageview(
|
|||
interface TrackEventProps {
|
||||
app: ObservabilityApp;
|
||||
name: string;
|
||||
metricType?: METRIC_TYPE;
|
||||
metricType?: UiStatsMetricType;
|
||||
}
|
||||
|
||||
export function trackEvent({ app, name, metricType = METRIC_TYPE.CLICK }: TrackEventProps) {
|
||||
|
|
|
@ -12,8 +12,7 @@ describe('get_all_stats', () => {
|
|||
const size = 123;
|
||||
const start = 0;
|
||||
const end = 1;
|
||||
const callWithRequest = sinon.stub();
|
||||
const callWithInternalUser = sinon.stub();
|
||||
const callCluster = sinon.stub();
|
||||
const server = {
|
||||
config: sinon.stub().returns({
|
||||
get: sinon.stub().withArgs('xpack.monitoring.elasticsearch.index_pattern').returns('.monitoring-es-N-*')
|
||||
|
@ -21,16 +20,8 @@ describe('get_all_stats', () => {
|
|||
.withArgs('xpack.monitoring.logstash.index_pattern').returns('.monitoring-logstash-N-*')
|
||||
.withArgs('xpack.monitoring.max_bucket_size').returns(size)
|
||||
}),
|
||||
plugins: {
|
||||
elasticsearch: {
|
||||
getCluster: sinon.stub().withArgs('monitoring').returns({
|
||||
callWithInternalUser,
|
||||
callWithRequest
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
const req = { server };
|
||||
|
||||
const esClusters = [
|
||||
{ cluster_uuid: 'a' },
|
||||
{ cluster_uuid: 'b', random_setting_not_removed: false },
|
||||
|
@ -188,19 +179,13 @@ describe('get_all_stats', () => {
|
|||
}
|
||||
];
|
||||
|
||||
callWithRequest.withArgs(req, 'search')
|
||||
callCluster.withArgs('search')
|
||||
.onCall(0).returns(Promise.resolve(clusterUuidsResponse))
|
||||
.onCall(1).returns(Promise.resolve(esStatsResponse))
|
||||
.onCall(2).returns(Promise.resolve(kibanaStatsResponse))
|
||||
.onCall(3).returns(Promise.resolve(logstashStatsResponse));
|
||||
|
||||
callWithInternalUser.withArgs('search')
|
||||
.onCall(0).returns(Promise.resolve(clusterUuidsResponse))
|
||||
.onCall(1).returns(Promise.resolve(esStatsResponse))
|
||||
.onCall(2).returns(Promise.resolve(kibanaStatsResponse))
|
||||
.onCall(3).returns(Promise.resolve(logstashStatsResponse));
|
||||
|
||||
expect(await getAllStats(req, start, end)).to.eql(allClusters);
|
||||
expect(await getAllStats({ callCluster, server, start, end })).to.eql(allClusters);
|
||||
});
|
||||
|
||||
it('returns empty clusters', async () => {
|
||||
|
@ -208,10 +193,9 @@ describe('get_all_stats', () => {
|
|||
aggregations: { cluster_uuids: { buckets: [ ] } }
|
||||
};
|
||||
|
||||
callWithRequest.withArgs(req, 'search').returns(Promise.resolve(clusterUuidsResponse));
|
||||
callWithInternalUser.withArgs('search').returns(Promise.resolve(clusterUuidsResponse));
|
||||
callCluster.withArgs('search').returns(Promise.resolve(clusterUuidsResponse));
|
||||
|
||||
expect(await getAllStats(req, start, end)).to.eql([]);
|
||||
expect(await getAllStats({ callCluster, server, start, end })).to.eql([]);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -17,23 +17,6 @@ import { getKibanaStats } from './get_kibana_stats';
|
|||
import { getBeatsStats } from './get_beats_stats';
|
||||
import { getHighLevelStats } from './get_high_level_stats';
|
||||
|
||||
|
||||
/**
|
||||
* Get statistics for all products joined by Elasticsearch cluster.
|
||||
*
|
||||
* @param {Object} req The incoming request
|
||||
* @param {Date} start The starting range to request data
|
||||
* @param {Date} end The ending range to request data
|
||||
* @return {Promise} The array of clusters joined with the Kibana and Logstash instances.
|
||||
*/
|
||||
export function getAllStats(req, start, end, { useInternalUser = false } = {}) {
|
||||
const server = req.server;
|
||||
const { callWithRequest, callWithInternalUser } = server.plugins.elasticsearch.getCluster('monitoring');
|
||||
const callCluster = useInternalUser ? callWithInternalUser : (...args) => callWithRequest(req, ...args);
|
||||
|
||||
return getAllStatsWithCaller(server, callCluster, start, end);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get statistics for all products joined by Elasticsearch cluster.
|
||||
*
|
||||
|
@ -43,7 +26,7 @@ export function getAllStats(req, start, end, { useInternalUser = false } = {}) {
|
|||
* @param {Date} end The ending range to request data
|
||||
* @return {Promise} The array of clusters joined with the Kibana and Logstash instances.
|
||||
*/
|
||||
function getAllStatsWithCaller(server, callCluster, start, end) {
|
||||
export function getAllStats({ server, callCluster, start, end } = {}) {
|
||||
return getClusterUuids(server, callCluster, start, end)
|
||||
.then(clusterUuids => {
|
||||
// don't bother doing a further lookup
|
||||
|
|
|
@ -7,37 +7,24 @@
|
|||
// @ts-ignore
|
||||
import { getAllStats } from './get_all_stats';
|
||||
import { getStatsWithXpack } from '../../../xpack_main/server/telemetry_collection';
|
||||
import {
|
||||
StatsGetter,
|
||||
getStatsCollectionConfig,
|
||||
} from '../../../../../../src/legacy/core_plugins/telemetry/server/collection_manager';
|
||||
|
||||
/**
|
||||
* Get the telemetry data.
|
||||
*
|
||||
* @param {Object} req The incoming request.
|
||||
* @param {Object} config Kibana config.
|
||||
* @param {String} start The start time of the request (likely 20m ago).
|
||||
* @param {String} end The end time of the request.
|
||||
* @param {Boolean} unencrypted Is the request payload going to be unencrypted.
|
||||
* @return {Promise} An array of telemetry objects.
|
||||
*/
|
||||
export async function getStatsWithMonitoring(
|
||||
req: any,
|
||||
config: any,
|
||||
start: string,
|
||||
end: string,
|
||||
unencrypted: boolean
|
||||
) {
|
||||
export const getStatsWithMonitoring: StatsGetter = async function(config) {
|
||||
let response = [];
|
||||
const useInternalUser = !unencrypted;
|
||||
|
||||
try {
|
||||
// attempt to collect stats from multiple clusters in monitoring data
|
||||
response = await getAllStats(req, start, end, { useInternalUser });
|
||||
const { start, end, server, callCluster } = getStatsCollectionConfig(config, 'monitoring');
|
||||
response = await getAllStats({ server, callCluster, start, end });
|
||||
} catch (err) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
if (!Array.isArray(response) || response.length === 0) {
|
||||
response = await getStatsWithXpack(req, config, start, end, unencrypted);
|
||||
response = await getStatsWithXpack(config);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -7,36 +7,19 @@
|
|||
// @ts-ignore
|
||||
import { getXPack } from './get_xpack';
|
||||
import { getLocalStats } from '../../../../../../src/legacy/core_plugins/telemetry/server/telemetry_collection';
|
||||
import {
|
||||
StatsGetter,
|
||||
getStatsCollectionConfig,
|
||||
} from '../../../../../../src/legacy/core_plugins/telemetry/server/collection_manager';
|
||||
|
||||
/**
|
||||
* Get the telemetry data.
|
||||
*
|
||||
* @param {Object} req The incoming request.
|
||||
* @param {Object} config Kibana config.
|
||||
* @param {String} start The start time of the request (likely 20m ago).
|
||||
* @param {String} end The end time of the request.
|
||||
* @param {Boolean} unencrypted Is the request payload going to be unencrypted.
|
||||
* @return {Promise} An array of telemetry objects.
|
||||
*/
|
||||
export async function getStatsWithXpack(
|
||||
req: any,
|
||||
config: any,
|
||||
start: string,
|
||||
end: string,
|
||||
unencrypted: boolean
|
||||
) {
|
||||
const useInternalUser = !unencrypted;
|
||||
const { server } = req;
|
||||
const { callWithRequest, callWithInternalUser } = server.plugins.elasticsearch.getCluster('data');
|
||||
const callCluster = useInternalUser
|
||||
? callWithInternalUser
|
||||
: (...args: any[]) => callWithRequest(req, ...args);
|
||||
export const getStatsWithXpack: StatsGetter = async function(config) {
|
||||
const { server, callCluster } = getStatsCollectionConfig(config, 'data');
|
||||
|
||||
const localStats = await getLocalStats(req, { useInternalUser });
|
||||
const localStats = await getLocalStats({ server, callCluster });
|
||||
const { license, xpack } = await getXPack(callCluster);
|
||||
|
||||
localStats.license = license;
|
||||
localStats.stack_stats.xpack = xpack;
|
||||
|
||||
return [localStats];
|
||||
}
|
||||
};
|
||||
|
|
|
@ -47,7 +47,6 @@ export default function({ getPageObjects, getService }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
await PageObjects.security.logout();
|
||||
|
||||
await PageObjects.security.login(
|
||||
'global_advanced_settings_all_user',
|
||||
'global_advanced_settings_all_user-password',
|
||||
|
@ -55,7 +54,6 @@ export default function({ getPageObjects, getService }: FtrProviderContext) {
|
|||
expectSpaceSelector: false,
|
||||
}
|
||||
);
|
||||
|
||||
await kibanaServer.uiSettings.replace({});
|
||||
await PageObjects.settings.navigateTo();
|
||||
});
|
||||
|
|
|
@ -27983,7 +27983,7 @@ typescript-fsa@^2.0.0, typescript-fsa@^2.5.0:
|
|||
resolved "https://registry.yarnpkg.com/typescript-fsa/-/typescript-fsa-2.5.0.tgz#1baec01b5e8f5f34c322679d1327016e9e294faf"
|
||||
integrity sha1-G67AG16PXzTDImedEycBbp4pT68=
|
||||
|
||||
typescript@3.5.1, typescript@3.5.3, typescript@^3.0.1, typescript@^3.0.3, typescript@^3.2.2, typescript@^3.3.3333, typescript@^3.4.5, typescript@~3.3.3333, typescript@~3.5.3:
|
||||
typescript@3.5.3, typescript@^3.0.1, typescript@^3.0.3, typescript@^3.2.2, typescript@^3.3.3333, typescript@^3.4.5, typescript@~3.3.3333, typescript@~3.5.3:
|
||||
version "3.5.3"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.5.3.tgz#c830f657f93f1ea846819e929092f5fe5983e977"
|
||||
integrity sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g==
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue