Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Telemetry] Application Usage implemented in @kbn/analytics #58401

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
0732a9d
[Telemetry] Report the Application Usage (time of usage + number of c…
afharo Feb 18, 2020
0eaf763
Add Unit tests to the server side
afharo Feb 19, 2020
8c2cc28
Do not use optional chaining in JS
afharo Feb 20, 2020
d952a92
Merge branch 'master' of github.com:elastic/kibana into telemetry/app…
afharo Feb 20, 2020
799540f
Add tests on the public end
afharo Feb 20, 2020
debba6b
Merge branch 'master' of github.com:elastic/kibana into telemetry/app…
afharo Feb 20, 2020
665dd4e
Fix jslint errors
afharo Feb 20, 2020
91fca2e
jest.useFakeTimers() + jest.clearAllTimers()
afharo Feb 20, 2020
5e81450
Merge branch 'master' of github.com:elastic/kibana into telemetry/app…
afharo Feb 20, 2020
2e35ef5
Remove Jest timer handlers from my tests (only affecting to a minimum…
afharo Feb 21, 2020
39c6730
Merge branch 'master' into telemetry/application-usage-plugin
elasticmachine Feb 21, 2020
9226712
Catch ES actions in the setup/start steps because it broke core_servi…
afharo Feb 21, 2020
ec22dad
Fix boolean check
afharo Feb 21, 2020
531267b
Use core's ES.adminCLient over .createClient
afharo Feb 21, 2020
9fd1324
Fix tests after ES.adminClient
afharo Feb 21, 2020
5721b08
Merge branch 'master' of github.com:elastic/kibana into telemetry/app…
afharo Feb 24, 2020
fbad163
[Telemetry] Application Usage implemented in kbn-analytics
afharo Feb 24, 2020
6d1f137
Use bulkCreate in store_report
afharo Feb 24, 2020
be901eb
Merge branch 'master' into telemetry/application-usage-in-kbn-analytics
elasticmachine Feb 24, 2020
771e3a2
ApplicationUsagePluginStart does not exist anymore
afharo Feb 24, 2020
a4d73cd
Fix usage_collection mock interface
afharo Feb 25, 2020
528cb7f
Check there is something to store before calling the bulkCreate method
afharo Feb 25, 2020
ad91cfe
Add unit tests
afharo Feb 26, 2020
061899b
Merge branch 'master' of github.com:elastic/kibana into telemetry/app…
afharo Feb 26, 2020
56906b0
Fix types in tests
afharo Feb 26, 2020
6db8986
Merge branch 'master' of github.com:elastic/kibana into telemetry/app…
afharo Feb 28, 2020
d7e8313
Unit tests for rollTotals and actual fix for the bug found
afharo Feb 28, 2020
fc272ba
Merge branch 'master' of github.com:elastic/kibana into telemetry/app…
afharo Feb 28, 2020
c53a740
Fix usage_collection mock after #57693 got merged
afharo Feb 28, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aside and not for this PR: We might want to consider adding a generic type that devs can use for metrics not falling into any of the current metric types. For example: the recent File Upload migration to the NP includes a a request to add File Upload as a type. We should think about how to handle cases like these where we might not necessarily allow an overwrite of the generic type but rather have that if a generic type is used, there must be another descriptor (like a description) tag in the data we're capturing. e.g. metric type='generic', and description='File Upload`.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a fair comment. I think the main reason why we haven't used the UIStatsMetrics more is because of the way we report them (as arrays).

I'm sure we'll be coming back to this at some point with the new Pulse approach where arrays won't be an issue anymore.

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