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

Track JS errors in GA #189

Merged
merged 17 commits into from
Feb 27, 2018
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Tests for tracking
Signed-off-by: Joe Farro <joef@uber.com>
tiffon committed Feb 12, 2018
commit 8a90112c2cd6f40c095d66de7916bca770a6f923
5 changes: 4 additions & 1 deletion src/utils/tracking/README.md
Original file line number Diff line number Diff line change
@@ -4,7 +4,10 @@ Page-views and errors are tracked in production when a GA tracking ID is provide
[documentation](http://jaeger.readthedocs.io/en/latest/deployment/#ui-configuration) for details on the UI
config.

The page-view tracking is pretty basic, so details aren't provided. The GA tracking is configured with [App Tracking](https://developers.google.com/analytics/devguides/collection/analyticsjs/field-reference#apptracking) data. These fields, described [below](#app-tracking), can be used as a secondary dimension when viewing event data in GA. The error tracking is described, [below](#error-tracking).
The page-view tracking is pretty basic, so details aren't provided. The GA tracking is configured with
[App Tracking](https://developers.google.com/analytics/devguides/collection/analyticsjs/field-reference#apptracking)
data. These fields, described [below](#app-tracking), can be used as a secondary dimension when viewing event
data in GA. The error tracking is described, [below](#error-tracking).

## App Tracking

23 changes: 23 additions & 0 deletions src/utils/tracking/conv-raven-to-ga.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// 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.

import convRavenToGa from './conv-raven-to-ga';
import { RAVEN_PAYLOAD, RAVEN_TO_GA } from './fixtures';

describe('convRavenToGa()', () => {
it('converts the raven-js payload to { category, action, label, value }', () => {
const data = convRavenToGa(RAVEN_PAYLOAD);
expect(data).toEqual(RAVEN_TO_GA);
});
});
221 changes: 221 additions & 0 deletions src/utils/tracking/fixtures.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
// 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.

import deepFreeze from 'deep-freeze';

const poeExcerpt = `
Excerpt of Alone, by Edgar Allen Poe:
"Then—in my childhood—in the dawn
Of a most stormy life—was drawn
From the red cliff of the mountain—
From the sun that ’round me roll’d
In its autumn tint of gold—
From the lightning in the sky
As it pass’d me flying by—
From the thunder, and the storm
And the cloud that took the form
(When the rest of Heaven was blue)
Of a demon in my view—"
3/17/1829`;

module.exports.RAVEN_PAYLOAD = deepFreeze({
data: {
request: {
url: 'http://localhost/trace/565c1f00385ebd0b',
},
exception: {
values: [
{
type: 'Error',
value: 'test-sentry',
stacktrace: {
frames: [
{
filename: 'http://localhost/static/js/ultra-long-func.js',
function: poeExcerpt,
},
{
filename: 'http://localhost/static/js/b.js',
function: 'fnBb',
},
{
filename: 'http://localhost/static/js/b.js',
function: 'fnBa',
},
{
filename: 'http://localhost/static/js/a.js',
function: 'fnAb',
},
{
filename: 'http://localhost/static/js/a.js',
function: 'fnAa',
},
{
filename: 'http://localhost/static/js/a.js',
function: 'HTMLBodyElement.wrapped',
},
],
},
},
],
},
tags: {
git: 'SHA shortstat',
},
extra: {
'session:duration': 10952,
},
breadcrumbs: {
values: [
{
category: 'sentry',
message: '6 Breadcrumbs should be truncated from the top (oldest)',
},
{
category: 'sentry',
message: '5 Breadcrumbs should be truncated from the top (oldest)',
},
{
category: 'sentry',
message: '4 Breadcrumbs should be truncated from the top (oldest)',
},
{
category: 'sentry',
message: '3 Breadcrumbs should be truncated from the top (oldest)',
},
{
category: 'sentry',
message: '2 Breadcrumbs should be truncated from the top (oldest)',
},
{
category: 'sentry',
message: '1 Breadcrumbs should be truncated from the top (oldest)',
},
{
category: 'sentry',
message: '0 Breadcrumbs should be truncated from the top (oldest)',
},
{
type: 'http',
category: 'fetch',
data: {
url: '/api/traces/565c1f00385ebd0b',
status_code: 200,
},
},
{
type: 'http',
category: 'fetch',
data: {
url: '/api/traces/565c1f00385ebd0b',
status_code: 404,
},
},
{
type: 'http',
category: 'fetch',
data: {
url: '/unknown/url/1',
status_code: 200,
},
},
{
category: 'navigation',
data: {
to: '/trace/cde2457775afa8d2',
},
},
{
category: 'navigation',
data: {
to: '/uknonwn/url',
},
},
{
category: 'sentry',
message: 'Error: test-sentry',
},
{
category: 'sentry',
message:
"TypeError: A very long message that will be truncated and reduced to a faint flicker of it's former glory",
},
{
category: 'ui.click',
},
{
category: 'ui.input',
},
{
category: 'ui.click',
},
{
category: 'ui.click',
},
{
category: 'ui.input',
},
{
category: 'ui.input',
},
{
category: 'ui.input',
message: 'header > ul.LabeledList.TracePageHeader--overviewItems',
},
],
},
},
});

const action = `! test-sentry
SHA shortstat
/trace/565c1f00385ebd0b
> a.js
HTMLBodyElement.wrapped
fnAa
fnAb
> b.js
fnBa
fnBb
> ultra-long-func.js
Excerpt of Alone, by Edgar Allen Poe:|"Then—in my childhood—in the dawn|Of a most stormy life—was drawn|From the red cliff of the mountain—|From the sun that ’round me roll’d|In its autumn tint of gold—|From the lightning in the sky|As it pass’d me flying by—|From the thunder, and the storm|And the cloud that took the form|(When the rest of Heaven was blue)|Of a d~`;

const label = `! test-sentry
trace
11
SHA shortstat
~om the top (oldest)
! 4 Breadcrumbs should be truncated from the top (oldest)
! 3 Breadcrumbs should be truncated from the top (oldest)
! 2 Breadcrumbs should be truncated from the top (oldest)
! 1 Breadcrumbs should be truncated from the top (oldest)
! 0 Breadcrumbs should be truncated from the top (oldest)
[tr][tr|404][??]
tr
??
! test-sentry
! Type! A very long message that will be truncated and re~
cic2i2i{.LabeledList.TracePageHeader--overviewItems}`;

module.exports.RAVEN_TO_GA = deepFreeze({
action,
label,
message: '! test-sentry',
category: 'jaeger/trace/error',
value: 11,
});
82 changes: 42 additions & 40 deletions src/utils/tracking/index.js
Original file line number Diff line number Diff line change
@@ -40,38 +40,18 @@ const isTruish = value => Boolean(value) && value !== '0' && value !== 'false';

const isProd = process.env.NODE_ENV === 'production';
const isDev = process.env.NODE_ENV === 'development';
const isTest = process.env.NODE_ENV === 'test';

// In test mode if development and envvar REACT_APP_GA_DEBUG is true-ish
const isDebugMode =
(isDev && isTruish(process.env.REACT_APP_GA_DEBUG)) ||
isTruish(queryString.parse(_get(window, 'location.search'))['ga-debug']);

const config = getConfig();
// enable for debug or if in prod with a GA ID
const isGaEnabled = isDebugMode || (isProd && Boolean(config.gaTrackingID));

let appVersion;
if (process.env.REACT_APP_VSN_STATE) {
try {
appVersion = JSON.parse(process.env.REACT_APP_VSN_STATE);
const joiner = [appVersion.objName];
if (appVersion.changed.hasChanged) {
joiner.push(appVersion.changed.pretty);
}
appVersion.shortPretty = joiner.join(' ');
} catch (_) {
appVersion = {
pretty: process.env.REACT_APP_VSN_STATE,
shortPretty: process.env.REACT_APP_VSN_STATE,
};
}
} else {
appVersion = {
pretty: 'unknown',
shortPretty: 'unknown',
};
}
// enable for tests, debug or if in prod with a GA ID
const isGaEnabled = isTest || isDebugMode || (isProd && Boolean(config.gaTrackingID));

/* istanbul ignore next */
function logTrackingCalls() {
const calls = ReactGA.testModeAPI.calls;
for (let i = 0; i < calls.length; i++) {
@@ -95,8 +75,9 @@ export function trackError(description: string) {
if (isGaEnabled) {
let msg = description;
if (!/^jaeger/i.test(msg)) {
msg = `jaeger/${msg}`.slice(0, 149);
msg = `jaeger/${msg}`;
}
msg = msg.slice(0, 149);
ReactGA.exception({ description: msg, fatal: false });
if (isDebugMode) {
logTrackingCalls();
@@ -116,9 +97,7 @@ export function trackEvent(data: EventData) {
category = category.slice(0, EVENT_LENGTHS.category);
}
event.category = category;
if (data.action) {
event.action = data.action.slice(0, EVENT_LENGTHS.action);
}
event.action = data.action ? data.action.slice(0, EVENT_LENGTHS.action) : 'jaeger/action';
if (data.label) {
event.label = data.label.slice(0, EVENT_LENGTHS.label);
}
@@ -133,35 +112,58 @@ export function trackEvent(data: EventData) {
}

function trackRavenError(ravenData: RavenTransportOptions) {
const { message, ...gaData } = convRavenToGa(ravenData);
const data = convRavenToGa(ravenData);
if (isDebugMode) {
Object.keys(gaData).forEach(key => {
/* istanbul ignore next */
Object.keys(data).forEach(key => {
if (key === 'message') {
return;
}
let valueLen = '';
if (typeof gaData[key] === 'string') {
valueLen = `- value length: ${gaData[key].length}`;
if (typeof data[key] === 'string') {
valueLen = `- value length: ${data[key].length}`;
}
// eslint-disable-next-line no-console
console.log(key, valueLen);
// eslint-disable-next-line no-console
console.log(gaData[key]);
console.log(data[key]);
});
}
trackError(message);
trackEvent(gaData);
trackError(data.message);
trackEvent(data);
}

// Tracking needs to be initialized when this file is imported, e.g. early in
// the process of initializing the app, so Raven can wrap various resources,
// like `fetch()`, and generate breadcrumbs from them.

if (isGaEnabled) {
const abbr = appVersion.pretty.length > 99 ? `${appVersion.pretty.slice(0, 96)}...` : appVersion.pretty;
const gaConfig = { testMode: isDebugMode, titleCase: false };
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: isTest || isDebugMode, titleCase: false };
ReactGA.initialize(config.gaTrackingID || 'debug-mode', gaConfig);
ReactGA.set({
appId: 'github.com/jaegertracing/jaeger-ui',
appName: 'Jaeger UI',
appVersion: abbr,
appVersion: versionLong,
});
const ravenConfig = {
autoBreadcrumbs: {
@@ -174,8 +176,8 @@ if (isGaEnabled) {
transport: trackRavenError,
tags: {},
};
if (appVersion.shortPretty && appVersion.shortPretty !== 'unknown') {
ravenConfig.tags.git = appVersion.shortPretty;
if (versionShort && versionShort !== 'unknown') {
ravenConfig.tags.git = versionShort;
}
Raven.config('https://fakedsn@omg.com/1', ravenConfig).install();
window.onunhandledrejection = function trackRejectedPromise(evt) {
124 changes: 124 additions & 0 deletions src/utils/tracking/index.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// 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', () => () => ({ message: 'jaeger/a' }));

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

import ReactGA from 'react-ga';

import * as tracking from './index';

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

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

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

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: jasmine.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', () => {
tracking.trackEvent({ value: 10 });
expect(calls).toEqual([
[
'send',
{
hitType: 'event',
eventCategory: jasmine.any(String),
eventAction: jasmine.any(String),
eventValue: 10,
},
],
]);
});

it('prepends "jaeger/" to the category, if needed', () => {
tracking.trackEvent({ category: 'a' });
expect(calls).toEqual([
['send', { hitType: 'event', eventCategory: 'jaeger/a', eventAction: jasmine.any(String) }],
]);
});

it('truncates values, if needed', () => {
const str = `jaeger/${getStr(600)}`;
tracking.trackEvent({ category: str, action: str, label: 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: jasmine.any(String), exFatal: false }],
['send', { hitType: 'event', eventCategory: jasmine.any(String), eventAction: jasmine.any(String) }],
]);
});
});