Skip to content

Commit

Permalink
Generic web analytics tracking implementation
Browse files Browse the repository at this point in the history
Signed-off-by: Mykhailo Semenchenko <mykhailo.semenchenko@logz.io>
  • Loading branch information
th3M1ke committed Jan 21, 2021
1 parent 48956d5 commit 5b22d04
Show file tree
Hide file tree
Showing 7 changed files with 254 additions and 157 deletions.
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
30 changes: 30 additions & 0 deletions packages/jaeger-ui/src/types/tracking.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright (c) 2021 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.

import { RavenStatic } from 'raven-js';
import { TNil } from '.';

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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ function getStr(len) {
return longStr.slice(0, len);
}

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

beforeEach(() => {
Expand Down
187 changes: 187 additions & 0 deletions packages/jaeger-ui/src/utils/tracking/ga.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
// Copyright (c) 2021 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.

import _get from 'lodash/get';
import queryString from 'query-string';
import ReactGA from 'react-ga';
import Raven, { RavenOptions, RavenTransportOptions } from 'raven-js';

import convRavenToGa from './conv-raven-to-ga';
import { TNil, IWebAnalytics } from '../../types';

export default class GA implements IWebAnalytics {
isDebugMode = false;
context = null;
isProd = process.env.NODE_ENV === 'production';
isDev = process.env.NODE_ENV === 'development';
isTest = process.env.NODE_ENV === 'test';
gaID = null;
isErrorsEnabled = false;

private cookiesToDimensions = undefined;
private EVENT_LENGTHS = {
action: 499,
category: 149,
label: 499,
};

constructor(config: any) {
this.isDebugMode =
(this.isDev && GA.isTruish(process.env.REACT_APP_GA_DEBUG)) ||
GA.isTruish(queryString.parse(_get(window, 'location.search'))['ga-debug']);

this.gaID = _get(config, 'tracking.gaID');
this.isErrorsEnabled = this.isDebugMode || Boolean(_get(config, 'tracking.trackErrors'));
this.cookiesToDimensions = _get(config, 'tracking.cookiesToDimensions');

this.context = this.isErrorsEnabled ? Raven : (null as any);
}

isEnabled() {
return this.isTest || this.isDebugMode || (this.isProd && Boolean(this.gaID));
}

init() {
let versionShort;
let versionLong;
if (process.env.REACT_APP_VSN_STATE) {
try {
const data = JSON.parse(process.env.REACT_APP_VSN_STATE);
const joiner = [data.objName];
if (data.changed.hasChanged) {
joiner.push(data.changed.pretty);
}
versionShort = joiner.join(' ');
versionLong = data.pretty;
} catch (_) {
versionShort = process.env.REACT_APP_VSN_STATE;
versionLong = process.env.REACT_APP_VSN_STATE;
}
versionLong = versionLong.length > 99 ? `${versionLong.slice(0, 96)}...` : versionLong;
} else {
versionShort = 'unknown';
versionLong = 'unknown';
}
const gaConfig = { testMode: this.isTest || this.isDebugMode, titleCase: false, debug: true };
ReactGA.initialize(this.gaID || 'debug-mode', gaConfig);
ReactGA.set({
appId: 'github.com/jaegertracing/jaeger-ui',
appName: 'Jaeger UI',
appVersion: versionLong,
});
if (this.cookiesToDimensions !== undefined) {
((this.cookiesToDimensions as unknown) as Array<{ cookie: string; dimension: string }>).forEach(
({ cookie, dimension }: { cookie: string; dimension: string }) => {
const match = ` ${document.cookie}`.match(new RegExp(`[; ]${cookie}=([^\\s;]*)`));
if (match) ReactGA.set({ [dimension]: match[1] });
// eslint-disable-next-line no-console
else console.warn(`${cookie} not present in cookies, could not set dimension: ${dimension}`);
}
);
}
if (this.isErrorsEnabled) {
const ravenConfig: RavenOptions = {
autoBreadcrumbs: {
xhr: true,
console: false,
dom: true,
location: true,
},
environment: process.env.NODE_ENV || 'unkonwn',
transport: this.trackRavenError.bind(this),
};
if (versionShort && versionShort !== 'unknown') {
ravenConfig.tags = {
git: versionShort,
};
}
Raven.config('https://fakedsn@omg.com/1', ravenConfig).install();
window.onunhandledrejection = function trackRejectedPromise(evt: PromiseRejectionEvent) {
Raven.captureException(evt.reason);
};
}
if (this.isDebugMode) {
this.logTrackingCalls();
}
}

trackPageView(pathname: string, search: string | TNil) {
const pagePath = search ? `${pathname}${search}` : pathname;
ReactGA.pageview(pagePath);
if (this.isDebugMode) {
this.logTrackingCalls();
}
}

trackError(description: string) {
let msg = description;
if (!/^jaeger/i.test(msg)) {
msg = `jaeger/${msg}`;
}
msg = msg.slice(0, 149);
ReactGA.exception({ description: msg, fatal: false });
if (this.isDebugMode) {
this.logTrackingCalls();
}
}

trackEvent(category: string, action: string, labelOrValue?: string | number | TNil, value?: number | TNil) {
const event: {
action: string;
category: string;
label?: string;
value?: number;
} = {
category: !/^jaeger/i.test(category)
? `jaeger/${category}`.slice(0, this.EVENT_LENGTHS.category)
: category.slice(0, this.EVENT_LENGTHS.category),
action: action.slice(0, this.EVENT_LENGTHS.action),
};
if (labelOrValue != null) {
if (typeof labelOrValue === 'string') {
event.label = labelOrValue.slice(0, this.EVENT_LENGTHS.action);
} else {
// value should be an int
event.value = Math.round(labelOrValue);
}
}
if (value != null) {
event.value = Math.round(value);
}
ReactGA.event(event);
if (this.isDebugMode) {
this.logTrackingCalls();
}
}

// eslint-disable-next-line class-methods-use-this
private logTrackingCalls() {
const calls = ReactGA.testModeAPI.calls;
for (let i = 0; i < calls.length; i++) {
// eslint-disable-next-line no-console
console.log('[react-ga]', ...calls[i]);
}
calls.length = 0;
}

private trackRavenError(ravenData: RavenTransportOptions) {
const { message, category, action, label, value } = convRavenToGa(ravenData);
this.trackError(message);
this.trackEvent(category, action, label, value);
}

static isTruish(value?: string | string[]) {
return Boolean(value) && value !== '0' && value !== 'false';
}
}
Loading

0 comments on commit 5b22d04

Please sign in to comment.