Skip to content

Commit

Permalink
Generic web analytics tracking implementation (#681)
Browse files Browse the repository at this point in the history
* Generic web analytics tracking implementation

Signed-off-by: Mykhailo Semenchenko <mykhailo.semenchenko@logz.io>

* Update due to comments, refactor Google Analytic tracker

Signed-off-by: Mykhailo Semenchenko <mykhailo.semenchenko@logz.io>

* Add unit tests

Signed-off-by: Mykhailo Semenchenko <mykhailo.semenchenko@logz.io>

* Update tests

Signed-off-by: Mykhailo Semenchenko <mykhailo.semenchenko@logz.io>

* Update tests

Signed-off-by: Mykhailo Semenchenko <mykhailo.semenchenko@logz.io>

* Increase test coverage

Signed-off-by: Mykhailo Semenchenko <mykhailo.semenchenko@logz.io>
  • Loading branch information
th3M1ke committed Feb 16, 2021
1 parent c572171 commit abdf99d
Show file tree
Hide file tree
Showing 12 changed files with 684 additions and 245 deletions.
3 changes: 2 additions & 1 deletion packages/jaeger-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,8 @@
"!src/setup*.js",
"!src/utils/DraggableManager/demo/*.tsx",
"!src/utils/test/**/*.js",
"!src/demo/**/*.js"
"!src/demo/**/*.js",
"!src/types/*"
]
},
"browserslist": [
Expand Down
1 change: 1 addition & 0 deletions packages/jaeger-ui/src/constants/default-config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export default deepFreeze(
tracking: {
gaID: null,
trackErrors: true,
customWebAnalytics: null,
},
},
// fields that should be individually merged vs wholesale replaced
Expand Down
4 changes: 2 additions & 2 deletions packages/jaeger-ui/src/middlewares/track.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { Dispatch, Store } from 'react-redux';

import { middlewareHooks as searchHooks } from '../components/SearchTracePage/SearchForm.track';
import { middlewareHooks as timelineHooks } from '../components/TracePage/TraceTimelineViewer/duck.track';
import { isGaEnabled } from '../utils/tracking';
import { isWaEnabled } from '../utils/tracking';
import { ReduxState } from '../types';

type TMiddlewareFn = (store: Store<ReduxState>, action: Action<any>) => void;
Expand All @@ -36,4 +36,4 @@ function trackingMiddleware(store: Store<ReduxState>) {
};
}

export default isGaEnabled ? trackingMiddleware : undefined;
export default isWaEnabled ? trackingMiddleware : undefined;
2 changes: 2 additions & 0 deletions packages/jaeger-ui/src/types/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@ import { EmbeddedState } from './embedded';
import { SearchQuery } from './search';
import TDdgState from './TDdgState';
import TNil from './TNil';
import IWebAnalytics from './tracking';
import { Trace } from './trace';
import TTraceDiffState from './TTraceDiffState';
import TTraceTimeline from './TTraceTimeline';

export type TNil = TNil;
export type IWebAnalytics = IWebAnalytics;

export type FetchedState = 'FETCH_DONE' | 'FETCH_ERROR' | 'FETCH_LOADING';

Expand Down
35 changes: 35 additions & 0 deletions packages/jaeger-ui/src/types/tracking.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Copyright (c) 2021 The Jaeger Authors.
//
// Licensed 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 { RavenStatic } from 'raven-js';
import { TNil } from '.';
import { Config } from './config';

export interface IWebAnalyticsFunc {
(config: Config, versionShort: string, versionLong: string): IWebAnalytics;
}

export default interface IWebAnalytics {
init: () => void;
context: boolean | RavenStatic | null;
isEnabled: () => boolean;
trackPageView: (pathname: string, search: string | TNil) => void;
trackError: (description: string) => void;
trackEvent: (
category: string,
action: string,
labelOrValue?: string | number | TNil,
value?: number | TNil
) => void;
}
218 changes: 218 additions & 0 deletions packages/jaeger-ui/src/utils/tracking/ga.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed 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.

/* eslint-disable import/first */
jest.mock('./conv-raven-to-ga', () => () => ({
category: 'jaeger/a',
action: 'some-action',
message: 'jaeger/a',
}));

jest.mock('./index', () => {
global.process.env.REACT_APP_VSN_STATE = '{}';
return require.requireActual('./index');
});

import ReactGA from 'react-ga';
import * as GA from './ga';
import * as utils from './utils';

let longStr = '---';
function getStr(len) {
while (longStr.length < len) {
longStr += longStr.slice(0, len - longStr.length);
}
return longStr.slice(0, len);
}

describe('google analytics tracking', () => {
let calls;
let tracking;

beforeAll(() => {
tracking = GA.default(
{
tracking: {
gaID: 'UA-123456',
trackErrors: true,
cookiesToDimensions: [{ cookie: 'page', dimension: 'dimension1' }],
},
},
'c0mm1ts',
'c0mm1tL'
);
});

beforeEach(() => {
calls = ReactGA.testModeAPI.calls;
calls.length = 0;
});

describe('init', () => {
it('check init function (no cookies)', () => {
tracking.init();
expect(calls).toEqual([
['create', 'UA-123456', 'auto'],
[
'set',
{
appId: 'github.com/jaegertracing/jaeger-ui',
appName: 'Jaeger UI',
appVersion: 'c0mm1tL',
},
],
]);
});

it('check init function (no cookies)', () => {
document.cookie = 'page=1;';
tracking.init();
expect(calls).toEqual([
['create', 'UA-123456', 'auto'],
[
'set',
{
appId: 'github.com/jaegertracing/jaeger-ui',
appName: 'Jaeger UI',
appVersion: 'c0mm1tL',
},
],
['set', { dimension1: '1' }],
]);
});
});

describe('trackPageView', () => {
it('tracks a page view', () => {
tracking.trackPageView('a', 'b');
expect(calls).toEqual([['send', { hitType: 'pageview', page: 'ab' }]]);
});

it('ignores search when it is falsy', () => {
tracking.trackPageView('a');
expect(calls).toEqual([['send', { hitType: 'pageview', page: 'a' }]]);
});
});

describe('trackError', () => {
it('tracks an error', () => {
tracking.trackError('a');
expect(calls).toEqual([
['send', { hitType: 'exception', exDescription: expect.any(String), exFatal: false }],
]);
});

it('ensures "jaeger" is prepended', () => {
tracking.trackError('a');
expect(calls).toEqual([['send', { hitType: 'exception', exDescription: 'jaeger/a', exFatal: false }]]);
});

it('truncates if needed', () => {
const str = `jaeger/${getStr(200)}`;
tracking.trackError(str);
expect(calls).toEqual([
['send', { hitType: 'exception', exDescription: str.slice(0, 149), exFatal: false }],
]);
});
});

describe('trackEvent', () => {
it('tracks an event', () => {
const category = 'jaeger/some-category';
const action = 'some-action';
tracking.trackEvent(category, action);
expect(calls).toEqual([
[
'send',
{
hitType: 'event',
eventCategory: category,
eventAction: action,
},
],
]);
});

it('prepends "jaeger/" to the category, if needed', () => {
const category = 'some-category';
const action = 'some-action';
tracking.trackEvent(category, action);
expect(calls).toEqual([
['send', { hitType: 'event', eventCategory: `jaeger/${category}`, eventAction: action }],
]);
});

it('truncates values, if needed', () => {
const str = `jaeger/${getStr(600)}`;
tracking.trackEvent(str, str, str);
expect(calls).toEqual([
[
'send',
{
hitType: 'event',
eventCategory: str.slice(0, 149),
eventAction: str.slice(0, 499),
eventLabel: str.slice(0, 499),
},
],
]);
});
});

it('converting raven-js errors', () => {
window.onunhandledrejection({ reason: new Error('abc') });
expect(calls).toEqual([
['send', { hitType: 'exception', exDescription: expect.any(String), exFatal: false }],
['send', { hitType: 'event', eventCategory: expect.any(String), eventAction: expect.any(String) }],
]);
});

describe('Debug mode', () => {
let trackingDebug;

beforeAll(() => {
const originalWindow = { ...window };
const windowSpy = jest.spyOn(global, 'window', 'get');
windowSpy.mockImplementation(() => ({
...originalWindow,
location: {
...originalWindow.location,
href: 'http://my.test/page',
search: 'ga-debug=true',
},
}));

trackingDebug = GA.default(
{
tracking: {
gaID: 'UA-123456',
trackErrors: true,
cookiesToDimensions: [{ cookie: 'page', dimension: 'dimension1' }],
},
},
'c0mm1ts',
'c0mm1tL'
);
});

it('isDebugMode = true', () => {
utils.logTrackingCalls = jest.fn();
trackingDebug.init();
trackingDebug.trackError();
trackingDebug.trackEvent('jaeger/some-category', 'some-action');
trackingDebug.trackPageView('a', 'b');
expect(utils.logTrackingCalls).toHaveBeenCalledTimes(4);
});
});
});
Loading

0 comments on commit abdf99d

Please sign in to comment.