Skip to content

Commit

Permalink
Merge branch 'master' into adding-job-synchronization-callout-to-job-…
Browse files Browse the repository at this point in the history
…list-pages
  • Loading branch information
kibanamachine authored Dec 16, 2020
2 parents 6053e39 + d559f0c commit 2848418
Show file tree
Hide file tree
Showing 339 changed files with 13,703 additions and 2,670 deletions.
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@
"@kbn/ui-framework": "link:packages/kbn-ui-framework",
"@kbn/ui-shared-deps": "link:packages/kbn-ui-shared-deps",
"@kbn/utils": "link:packages/kbn-utils",
"@loaders.gl/core": "^2.3.1",
"@loaders.gl/json": "^2.3.1",
"@slack/webhook": "^5.0.0",
"@storybook/addons": "^6.0.16",
"@turf/circle": "6.0.1",
Expand Down Expand Up @@ -375,6 +377,7 @@
"@kbn/test": "link:packages/kbn-test",
"@kbn/test-subj-selector": "link:packages/kbn-test-subj-selector",
"@kbn/utility-types": "link:packages/kbn-utility-types",
"@loaders.gl/polyfills": "^2.3.5",
"@mapbox/geojson-rewind": "^0.5.0",
"@mapbox/mapbox-gl-draw": "^1.2.0",
"@mapbox/mapbox-gl-rtl-text": "^0.2.3",
Expand Down
174 changes: 174 additions & 0 deletions packages/kbn-analytics/src/application_usage_tracker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
/*
* 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 { Reporter } from './reporter';
import { createApplicationUsageMetric, ApplicationUsageMetric } from './metrics';

type TrackedApplication = Record<string, ApplicationUsageMetric>;
interface ApplicationKey {
appId: string;
viewId: string;
}

export class ApplicationUsageTracker {
private trackedApplicationViews: TrackedApplication = {};
private reporter: Reporter;

private currentAppId?: string;
private currentApplicationKeys: ApplicationKey[] = [];

private beforeUnloadListener?: EventListener;
private onVisiblityChangeListener?: EventListener;

constructor(reporter: Reporter) {
this.reporter = reporter;
}

private createKey(appId: string, viewId: string): ApplicationKey {
return { appId, viewId };
}

static serializeKey({ appId, viewId }: ApplicationKey): string {
return `${appId}-${viewId}`;
}

private trackApplications(appKeys: ApplicationKey[]) {
for (const { appId, viewId } of appKeys.filter(Boolean)) {
const serializedKey = ApplicationUsageTracker.serializeKey({ appId, viewId });
if (typeof this.trackedApplicationViews[serializedKey] !== 'undefined') {
continue;
}
const metric = createApplicationUsageMetric(appId, viewId);
this.trackedApplicationViews[serializedKey] = metric;
}
}

private attachListeners() {
if (typeof window === 'undefined' || typeof document === 'undefined') {
return;
}

this.beforeUnloadListener = () => {
this.flushTrackedViews();
};

this.onVisiblityChangeListener = () => {
if (document.visibilityState === 'visible') {
this.resumeTrackingAll();
} else if (document.visibilityState === 'hidden') {
this.pauseTrackingAll();
}
};

// Before leaving the page, make sure we store the current usage
window.addEventListener('beforeunload', this.beforeUnloadListener);

// 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', this.onVisiblityChangeListener);
}

private detachListeners() {
if (typeof window === 'undefined' || typeof document === 'undefined') {
return;
}

if (this.beforeUnloadListener) {
window.removeEventListener('beforeunload', this.beforeUnloadListener);
}
if (this.onVisiblityChangeListener) {
document.removeEventListener('visibilitychange', this.onVisiblityChangeListener);
}
}

private sendMetricsToReporter(metrics: ApplicationUsageMetric[]) {
metrics.forEach((metric) => {
this.reporter.reportApplicationUsage(metric);
});
}

public updateViewClickCounter(viewId: string) {
if (!this.currentAppId) {
return;
}
const appKey = ApplicationUsageTracker.serializeKey({ appId: this.currentAppId, viewId });
if (this.trackedApplicationViews[appKey]) {
this.trackedApplicationViews[appKey].numberOfClicks++;
}
}

private flushTrackedViews() {
const appViewMetrics = Object.values(this.trackedApplicationViews);
this.sendMetricsToReporter(appViewMetrics);
this.trackedApplicationViews = {};
}

public start() {
this.attachListeners();
}

public stop() {
this.flushTrackedViews();
this.detachListeners();
}

public setCurrentAppId(appId: string) {
// application change, flush current views first.
this.flushTrackedViews();
this.currentAppId = appId;
}

public trackApplicationViewUsage(viewId: string): void {
if (!this.currentAppId) {
return;
}
const appKey = this.createKey(this.currentAppId, viewId);
this.trackApplications([appKey]);
}

public pauseTrackingAll() {
this.currentApplicationKeys = Object.values(
this.trackedApplicationViews
).map(({ appId, viewId }) => this.createKey(appId, viewId));

this.flushTrackedViews();
}

public resumeTrackingAll() {
this.trackApplications(this.currentApplicationKeys);
this.currentApplicationKeys = [];

// We also want to send the report now because intervals and timeouts be stalled when too long in the "hidden" state
// Note: it might be better to create a separate listener in the reporter for this.
this.reporter.sendReports();
}

public flushTrackedView(viewId: string) {
if (!this.currentAppId) {
return;
}

const appKey = this.createKey(this.currentAppId, viewId);
const serializedKey = ApplicationUsageTracker.serializeKey(appKey);
const appViewMetric = this.trackedApplicationViews[serializedKey];
this.sendMetricsToReporter([appViewMetric]);

delete this.trackedApplicationViews[serializedKey];
}
}
1 change: 1 addition & 0 deletions packages/kbn-analytics/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,5 @@
export { ReportHTTP, Reporter, ReporterConfig } from './reporter';
export { UiCounterMetricType, METRIC_TYPE } from './metrics';
export { Report, ReportManager } from './report';
export { ApplicationUsageTracker } from './application_usage_tracker';
export { Storage } from './storage';
41 changes: 13 additions & 28 deletions packages/kbn-analytics/src/metrics/application_usage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,38 +19,23 @@
import moment, { Moment } from 'moment-timezone';
import { METRIC_TYPE } from './';

export interface ApplicationUsageCurrent {
export interface ApplicationUsageMetric {
type: METRIC_TYPE.APPLICATION_USAGE;
appId: string;
viewId: 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;
}
export function createApplicationUsageMetric(
appId: string,
viewId: string
): ApplicationUsageMetric {
return {
type: METRIC_TYPE.APPLICATION_USAGE,
appId,
viewId,
startTime: moment(),
numberOfClicks: 0,
};
}
6 changes: 3 additions & 3 deletions packages/kbn-analytics/src/metrics/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@

import { UiCounterMetric } from './ui_counter';
import { UserAgentMetric } from './user_agent';
import { ApplicationUsageCurrent } from './application_usage';
import { ApplicationUsageMetric } from './application_usage';

export { UiCounterMetric, createUiCounterMetric, UiCounterMetricType } from './ui_counter';
export { trackUsageAgent } from './user_agent';
export { ApplicationUsage, ApplicationUsageCurrent } from './application_usage';
export { createApplicationUsageMetric, ApplicationUsageMetric } from './application_usage';

export type Metric = UiCounterMetric | UserAgentMetric | ApplicationUsageCurrent;
export type Metric = UiCounterMetric | UserAgentMetric | ApplicationUsageMetric;
export enum METRIC_TYPE {
COUNT = 'count',
LOADED = 'loaded',
Expand Down
22 changes: 16 additions & 6 deletions packages/kbn-analytics/src/report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@

import moment from 'moment-timezone';
import { UnreachableCaseError, wrapArray } from './util';
import { ApplicationUsageTracker } from './application_usage_tracker';
import { Metric, UiCounterMetricType, METRIC_TYPE } from './metrics';
const REPORT_VERSION = 2;
const REPORT_VERSION = 3;

export interface Report {
reportVersion: typeof REPORT_VERSION;
Expand All @@ -46,6 +47,8 @@ export interface Report {
application_usage?: Record<
string,
{
appId: string;
viewId: string;
minutesOnScreen: number;
numberOfClicks: number;
}
Expand Down Expand Up @@ -91,8 +94,10 @@ export class ReportManager {
const { appName, eventName, type } = metric;
return `${appName}-${type}-${eventName}`;
}
case METRIC_TYPE.APPLICATION_USAGE:
return metric.appId;
case METRIC_TYPE.APPLICATION_USAGE: {
const { appId, viewId } = metric;
return ApplicationUsageTracker.serializeKey({ appId, viewId });
}
default:
throw new UnreachableCaseError(metric);
}
Expand Down Expand Up @@ -130,20 +135,25 @@ export class ReportManager {
};
return;
}
case METRIC_TYPE.APPLICATION_USAGE:
const { numberOfClicks, startTime } = metric;
case METRIC_TYPE.APPLICATION_USAGE: {
const { numberOfClicks, startTime, appId, viewId } = metric;
const minutesOnScreen = moment().diff(startTime, 'minutes', true);

report.application_usage = report.application_usage || {};
const appExistingData = report.application_usage[key] || {
minutesOnScreen: 0,
numberOfClicks: 0,
appId,
viewId,
};
report.application_usage[key] = {
...appExistingData,
minutesOnScreen: appExistingData.minutesOnScreen + minutesOnScreen,
numberOfClicks: appExistingData.numberOfClicks + numberOfClicks,
};
break;

return;
}
default:
throw new UnreachableCaseError(metric);
}
Expand Down
Loading

0 comments on commit 2848418

Please sign in to comment.