Skip to content

Commit

Permalink
[Telemetry] Application Usage implemented in @kbn/analytics (#58401)
Browse files Browse the repository at this point in the history
* [Telemetry] Report the Application Usage (time of usage + number of clicks)

* Add Unit tests to the server side

* Do not use optional chaining in JS

* Add tests on the public end

* Fix jslint errors

* jest.useFakeTimers() + jest.clearAllTimers()

* Remove Jest timer handlers from my tests (only affecting to a minimum coverage bit)

* Catch ES actions in the setup/start steps because it broke core_services tests

* Fix boolean check

* Use core's ES.adminCLient over .createClient

* Fix tests after ES.adminClient

* [Telemetry] Application Usage implemented in kbn-analytics

* Use bulkCreate in store_report

* ApplicationUsagePluginStart does not exist anymore

* Fix usage_collection mock interface

* Check there is something to store before calling the bulkCreate method

* Add unit tests

* Fix types in tests

* Unit tests for rollTotals and actual fix for the bug found

* Fix usage_collection mock after #57693 got merged

Co-authored-by: Elastic Machine <[email protected]>
  • Loading branch information
afharo and elasticmachine authored Feb 28, 2020
1 parent 5b72705 commit fd25ae6
Show file tree
Hide file tree
Showing 32 changed files with 931 additions and 80 deletions.
56 changes: 56 additions & 0 deletions packages/kbn-analytics/src/metrics/application_usage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* 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, { Moment } from 'moment-timezone';
import { METRIC_TYPE } from './';

export interface ApplicationUsageCurrent {
type: METRIC_TYPE.APPLICATION_USAGE;
appId: string;
startTime: Moment;
numberOfClicks: number;
}

export class ApplicationUsage {
private currentUsage?: ApplicationUsageCurrent;

public start() {
// Count any clicks and assign it to the current app
if (window)
window.addEventListener(
'click',
() => this.currentUsage && this.currentUsage.numberOfClicks++
);
}

public appChanged(appId?: string) {
const currentUsage = this.currentUsage;

if (appId) {
this.currentUsage = {
type: METRIC_TYPE.APPLICATION_USAGE,
appId,
startTime: moment(),
numberOfClicks: 0,
};
} else {
this.currentUsage = void 0;
}
return currentUsage;
}
}
5 changes: 4 additions & 1 deletion packages/kbn-analytics/src/metrics/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,18 @@

import { UiStatsMetric } from './ui_stats';
import { UserAgentMetric } from './user_agent';
import { ApplicationUsageCurrent } from './application_usage';

export { UiStatsMetric, createUiStatsMetric, UiStatsMetricType } from './ui_stats';
export { Stats } from './stats';
export { trackUsageAgent } from './user_agent';
export { ApplicationUsage, ApplicationUsageCurrent } from './application_usage';

export type Metric = UiStatsMetric | UserAgentMetric;
export type Metric = UiStatsMetric | UserAgentMetric | ApplicationUsageCurrent;
export enum METRIC_TYPE {
COUNT = 'count',
LOADED = 'loaded',
CLICK = 'click',
USER_AGENT = 'user_agent',
APPLICATION_USAGE = 'application_usage',
}
29 changes: 27 additions & 2 deletions packages/kbn-analytics/src/report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
* under the License.
*/

import moment from 'moment-timezone';
import { UnreachableCaseError, wrapArray } from './util';
import { Metric, Stats, UiStatsMetricType, METRIC_TYPE } from './metrics';
const REPORT_VERSION = 1;
Expand All @@ -42,6 +43,13 @@ export interface Report {
appName: string;
}
>;
application_usage?: Record<
string,
{
minutesOnScreen: number;
numberOfClicks: number;
}
>;
}

export class ReportManager {
Expand All @@ -57,10 +65,11 @@ export class ReportManager {
this.report = ReportManager.createReport();
}
public isReportEmpty(): boolean {
const { uiStatsMetrics, userAgent } = this.report;
const { uiStatsMetrics, userAgent, application_usage: appUsage } = this.report;
const noUiStats = !uiStatsMetrics || Object.keys(uiStatsMetrics).length === 0;
const noUserAgent = !userAgent || Object.keys(userAgent).length === 0;
return noUiStats && noUserAgent;
const noAppUsage = !appUsage || Object.keys(appUsage).length === 0;
return noUiStats && noUserAgent && noAppUsage;
}
private incrementStats(count: number, stats?: Stats): Stats {
const { min = 0, max = 0, sum = 0 } = stats || {};
Expand Down Expand Up @@ -92,6 +101,8 @@ export class ReportManager {
const { appName, eventName, type } = metric;
return `${appName}-${type}-${eventName}`;
}
case METRIC_TYPE.APPLICATION_USAGE:
return metric.appId;
default:
throw new UnreachableCaseError(metric);
}
Expand Down Expand Up @@ -129,6 +140,20 @@ export class ReportManager {
};
return;
}
case METRIC_TYPE.APPLICATION_USAGE:
const { numberOfClicks, startTime } = metric;
const minutesOnScreen = moment().diff(startTime, 'minutes', true);

report.application_usage = report.application_usage || {};
const appExistingData = report.application_usage[key] || {
minutesOnScreen: 0,
numberOfClicks: 0,
};
report.application_usage[key] = {
minutesOnScreen: appExistingData.minutesOnScreen + minutesOnScreen,
numberOfClicks: appExistingData.numberOfClicks + numberOfClicks,
};
break;
default:
throw new UnreachableCaseError(metric);
}
Expand Down
41 changes: 38 additions & 3 deletions packages/kbn-analytics/src/reporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { Metric, createUiStatsMetric, trackUsageAgent, UiStatsMetricType } from

import { Storage, ReportStorageManager } from './storage';
import { Report, ReportManager } from './report';
import { ApplicationUsage } from './metrics';

export interface ReporterConfig {
http: ReportHTTP;
Expand All @@ -35,19 +36,22 @@ export type ReportHTTP = (report: Report) => Promise<void>;

export class Reporter {
checkInterval: number;
private interval: any;
private interval?: NodeJS.Timer;
private lastAppId?: string;
private http: ReportHTTP;
private reportManager: ReportManager;
private storageManager: ReportStorageManager;
private readonly applicationUsage: ApplicationUsage;
private debug: boolean;
private retryCount = 0;
private readonly maxRetries = 3;
private started = false;

constructor(config: ReporterConfig) {
const { http, storage, debug, checkInterval = 90000, storageKey = 'analytics' } = config;
this.http = http;
this.checkInterval = checkInterval;
this.interval = null;
this.applicationUsage = new ApplicationUsage();
this.storageManager = new ReportStorageManager(storageKey, storage);
const storedReport = this.storageManager.get();
this.reportManager = new ReportManager(storedReport);
Expand All @@ -68,10 +72,34 @@ export class Reporter {
public start = () => {
if (!this.interval) {
this.interval = setTimeout(() => {
this.interval = null;
this.interval = undefined;
this.sendReports();
}, this.checkInterval);
}

if (this.started) {
return;
}

if (window && document) {
// Before leaving the page, make sure we store the current usage
window.addEventListener('beforeunload', () => this.reportApplicationUsage());

// Monitoring dashboards might be open in background and we are fine with that
// but we don't want to report hours if the user goes to another tab and Kibana is not shown
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible' && this.lastAppId) {
this.reportApplicationUsage(this.lastAppId);
} else if (document.visibilityState === 'hidden') {
this.reportApplicationUsage();

// We also want to send the report now because intervals and timeouts be stalled when too long in the "hidden" state
this.sendReports();
}
});
}
this.started = true;
this.applicationUsage.start();
};

private log(message: any) {
Expand Down Expand Up @@ -102,6 +130,13 @@ export class Reporter {
this.saveToReport([report]);
};

public reportApplicationUsage(appId?: string) {
this.log(`Reporting application changed to ${appId}`);
this.lastAppId = appId || this.lastAppId;
const appChangedReport = this.applicationUsage.appChanged(appId);
if (appChangedReport) this.saveToReport([appChangedReport]);
}

public sendReports = async () => {
if (!this.reportManager.isReportEmpty()) {
try {
Expand Down
31 changes: 31 additions & 0 deletions src/legacy/core_plugins/application_usage/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* 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 { Legacy } from '../../../../kibana';
import { mappings } from './mappings';

// eslint-disable-next-line import/no-default-export
export default function ApplicationUsagePlugin(kibana: any) {
const config: Legacy.PluginSpecOptions = {
id: 'application_usage',
uiExports: { mappings }, // Needed to define the mappings for the SavedObjects
};

return new kibana.Plugin(config);
}
36 changes: 36 additions & 0 deletions src/legacy/core_plugins/application_usage/mappings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* 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 const mappings = {
application_usage_totals: {
properties: {
appId: { type: 'keyword' },
numberOfClicks: { type: 'long' },
minutesOnScreen: { type: 'float' },
},
},
application_usage_transactional: {
properties: {
timestamp: { type: 'date' },
appId: { type: 'keyword' },
numberOfClicks: { type: 'long' },
minutesOnScreen: { type: 'float' },
},
},
};
4 changes: 4 additions & 0 deletions src/legacy/core_plugins/application_usage/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "application_usage",
"version": "kibana"
}
5 changes: 5 additions & 0 deletions src/legacy/core_plugins/telemetry/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ export const TELEMETRY_STATS_TYPE = 'telemetry';
*/
export const UI_METRIC_USAGE_TYPE = 'ui_metric';

/**
* Application Usage type
*/
export const APPLICATION_USAGE_TYPE = 'application_usage';

/**
* Link to Advanced Settings.
*/
Expand Down
11 changes: 4 additions & 7 deletions src/legacy/core_plugins/telemetry/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import * as Rx from 'rxjs';
import { resolve } from 'path';
import JoiNamespace from 'joi';
import { Server } from 'hapi';
import { CoreSetup, PluginInitializerContext } from 'src/core/server';
import { PluginInitializerContext } from 'src/core/server';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { getConfigPath } from '../../../core/server/path';
// @ts-ignore
Expand Down Expand Up @@ -132,11 +132,6 @@ const telemetry = (kibana: any) => {
},
} as PluginInitializerContext;

const coreSetup = ({
http: { server },
log: server.log,
} as any) as CoreSetup;

try {
await handleOldSettings(server);
} catch (err) {
Expand All @@ -147,7 +142,9 @@ const telemetry = (kibana: any) => {
usageCollection,
};

telemetryPlugin(initializerContext).setup(coreSetup, pluginsSetup, server);
const npPlugin = telemetryPlugin(initializerContext);
await npPlugin.setup(server.newPlatform.setup.core, pluginsSetup, server);
await npPlugin.start(server.newPlatform.start.core);
},
});
};
Expand Down
Loading

0 comments on commit fd25ae6

Please sign in to comment.