Skip to content

Commit

Permalink
Pre-aggregate by date in localStorage
Browse files Browse the repository at this point in the history
  • Loading branch information
wylieconlon committed Oct 14, 2019
1 parent 5c7fdb7 commit e8e2b70
Show file tree
Hide file tree
Showing 13 changed files with 435 additions and 153 deletions.
21 changes: 16 additions & 5 deletions x-pack/legacy/plugins/apm/typings/elasticsearch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,22 @@ declare module 'elasticsearch' {

// eslint-disable-next-line @typescript-eslint/prefer-interface
type FiltersAggregation<SubAggregationMap> = {
buckets: Array<
{
doc_count: number;
} & SubAggregation<SubAggregationMap>
>;
// The filters aggregation can have named filters or anonymous filters,
// which changes the structure of the return
// https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-filters-aggregation.html
buckets: SubAggregationMap extends {
filters: { filters: Record<string, unknown> };
}
? {
[key in keyof SubAggregationMap['filters']['filters']]: {
doc_count: number;
} & SubAggregation<SubAggregationMap>;
}
: Array<
{
doc_count: number;
} & SubAggregation<SubAggregationMap>
>;
};

type SamplerAggregation<SubAggregationMap> = SubAggregation<
Expand Down
10 changes: 0 additions & 10 deletions x-pack/legacy/plugins/lens/common/clickdata.ts

This file was deleted.

1 change: 0 additions & 1 deletion x-pack/legacy/plugins/lens/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,3 @@

export * from './api';
export * from './constants';
export * from './clickdata';
3 changes: 3 additions & 0 deletions x-pack/legacy/plugins/lens/mappings.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@
},
"date": {
"type": "date"
},
"count": {
"type": "integer"
}
}
}
Expand Down
110 changes: 110 additions & 0 deletions x-pack/legacy/plugins/lens/public/lens_ui_telemetry/factory.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import {
LensReportManager,
setReportManager,
stopReportManager,
trackUiEvent,
trackSuggestionEvent,
} from './factory';
import { Storage } from 'src/legacy/core_plugins/data/public/types';
import { coreMock } from 'src/core/public/mocks';
import { HttpServiceBase } from 'kibana/public';

jest.useFakeTimers();

const createMockStorage = () => ({
// store: createMockWebStorage(),
get: jest.fn(),
set: jest.fn(),
remove: jest.fn(),
clear: jest.fn(),
});

describe('Lens UI telemetry', () => {
let storage: jest.Mocked<Storage>;
let http: jest.Mocked<HttpServiceBase>;

beforeEach(() => {
storage = createMockStorage();
http = coreMock.createSetup().http;
const fakeManager = new LensReportManager({
http,
storage,
basePath: '/basepath',
});
setReportManager(fakeManager);
});

afterEach(() => {
stopReportManager();
});

it('should write immediately and track local state', () => {
trackUiEvent('loaded');

expect(storage.set).toHaveBeenCalledWith('lens-ui-telemetry', {
clicks: [
{
name: 'loaded',
date: expect.any(String),
},
],
suggestionClicks: [],
});

trackSuggestionEvent('reload');

expect(storage.set).toHaveBeenLastCalledWith('lens-ui-telemetry', {
clicks: [
{
name: 'loaded',
date: expect.any(String),
},
],
suggestionClicks: [
{
name: 'reload',
date: expect.any(String),
},
],
});
});

it('should post the results after waiting 10 seconds, if there is data', () => {
jest.runTimersToTime(10000);

expect(http.post).not.toHaveBeenCalled();

trackUiEvent('load');

jest.runTimersToTime(10000);

expect(http.post).toHaveBeenCalledWith(`/basepath/api/lens/telemetry`, {
// The contents of the body are not checked here because they depend on time
body: expect.any(String),
});

expect(storage.set).toHaveBeenCalledTimes(2);
expect(storage.set).toHaveBeenLastCalledWith('lens-ui-telemetry', {
clicks: [],
suggestionClicks: [],
});
});

it('should keep its local state after an http error', () => {
http.post.mockRejectedValue('http error');

trackUiEvent('load');
expect(storage.set).toHaveBeenCalledTimes(1);

jest.advanceTimersByTime(10000);

expect(http.post).toHaveBeenCalled();
expect(storage.set).toHaveBeenCalledTimes(1);
});
});
109 changes: 61 additions & 48 deletions x-pack/legacy/plugins/lens/public/lens_ui_telemetry/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,41 @@
* you may not use this file except in compliance with the Elastic License.
*/

import moment from 'moment';
import { HttpServiceBase } from 'src/core/public';

import { Storage } from 'src/legacy/core_plugins/data/public/types';
import { LensClickEvent, BASE_API_URL } from '../../common';
import { BASE_API_URL } from '../../common';

const STORAGE_KEY = 'lens-ui-telemetry';

let reportManager: LensReportManager;

export function setReportManager(newManager: LensReportManager) {
reportManager = newManager;
}

export function stopReportManager() {
if (reportManager) {
reportManager.stop();
}
}

export function trackUiEvent(name: string) {
if (reportManager) {
reportManager.trackEvent(name);
}
}

export function trackSuggestionEvent(name: string) {
if (reportManager) {
reportManager.trackSuggestionEvent(name);
}
}

export class LensReportManager {
private clicks: LensClickEvent[];
private suggestionClicks: LensClickEvent[];
private clicks: Record<string, LensClickEvent>;
private suggestionClicks: Record<string, LensClickEvent>;

private storage: Storage;
private http: HttpServiceBase;
Expand All @@ -34,34 +59,20 @@ export class LensReportManager {
this.basePath = basePath;

const unsent = this.storage.get(STORAGE_KEY);
this.clicks = unsent && unsent.clicks ? unsent.clicks : [];
this.suggestionClicks = unsent && unsent.suggestionClicks ? unsent.suggestionClicks : [];
this.clicks = unsent && unsent.clicks ? unsent.clicks : {};
this.suggestionClicks = unsent && unsent.suggestionClicks ? unsent.suggestionClicks : {};

this.timer = setInterval(() => {
if (this.clicks.length || this.suggestionClicks.length) {
this.postToServer();
}
this.postToServer();
}, 10000);
}

public trackEvent(name: string) {
this.clicks.push({
name,
date: new Date().toISOString(),
});
this.write();
this.trackTo(this.clicks, name);
}

public trackSuggestionEvent(name: string) {
this.suggestionClicks.push({
name,
date: new Date().toISOString(),
});
this.write();
}

private write() {
this.storage.set(STORAGE_KEY, { clicks: this.clicks, suggestionClicks: this.suggestionClicks });
this.trackTo(this.suggestionClicks, name);
}

public stop() {
Expand All @@ -71,37 +82,39 @@ export class LensReportManager {
}

private async postToServer() {
try {
await this.http.post(`${this.basePath}${BASE_API_URL}/telemetry`, {
body: JSON.stringify({
clicks: this.clicks,
suggestionClicks: this.suggestionClicks,
}),
});
this.clicks = [];
this.suggestionClicks = [];
this.write();
} catch (e) {
// Maybe show an error
console.log(e);
if (Object.keys(this.clicks).length || Object.keys(this.suggestionClicks).length) {
try {
await this.http.post(`${this.basePath}${BASE_API_URL}/telemetry`, {
body: JSON.stringify({
clicks: this.clicks,
suggestionClicks: this.suggestionClicks,
}),
});
this.clicks = {};
this.suggestionClicks = {};
this.write();
} catch (e) {
// Silent error because events will be reported during the next timer
}
}
}
}

let reportManager: LensReportManager;

export function setReportManager(newManager: LensReportManager) {
reportManager = newManager;
}
private trackTo(target: Record<string, Record<string, number>>, name: string) {
const date = moment().format('YYYY-MM-DD');
if (!target[date]) {
target[date] = {
[name]: 1,
};
} else if (!target[date][name]) {
target[date][name] = 1;
} else {
target[date][name] += 1;
}

export function trackUiEvent(name: string) {
if (reportManager) {
reportManager.trackEvent(name);
this.write();
}
}

export function trackSuggestionEvent(name: string) {
if (reportManager) {
reportManager.trackSuggestionEvent(name);
private write() {
this.storage.set(STORAGE_KEY, { clicks: this.clicks, suggestionClicks: this.suggestionClicks });
}
}
Empty file.
Loading

0 comments on commit e8e2b70

Please sign in to comment.