Skip to content

Commit 74e43e4

Browse files
committed
Add analytics
1 parent 1150f3b commit 74e43e4

File tree

7 files changed

+364
-1
lines changed

7 files changed

+364
-1
lines changed

components/init.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import React, { Component } from 'react'
2+
3+
import { init as initAnalytics } from '../lib/analytics/autotrack'
4+
5+
export default class Init extends Component {
6+
componentDidMount() {
7+
initAnalytics()
8+
}
9+
10+
render() {
11+
return null
12+
}
13+
}

lib/analytics/autotrack.js

Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
// From:
2+
// https://github.com/philipwalton/analyticsjs-boilerplate/blob/777ef6f2695c510a9e8d75ff0b426ee0a87ed18a/src/analytics/autotrack.js
3+
4+
/* eslint-disable */
5+
6+
// Import the individual autotrack plugins you want to use.
7+
// import 'autotrack/lib/plugins/clean-url-tracker';
8+
// import 'autotrack/lib/plugins/max-scroll-tracker';
9+
// import 'autotrack/lib/plugins/outbound-link-tracker';
10+
// import 'autotrack/lib/plugins/page-visibility-tracker';
11+
// import 'autotrack/lib/plugins/url-change-tracker';
12+
// import 'autotrack'
13+
14+
15+
/* global ga */
16+
17+
18+
/**
19+
* The tracking ID for your Google Analytics property.
20+
* https://support.google.com/analytics/answer/1032385
21+
*/
22+
const TRACKING_ID = 'UA-96115966-1';
23+
24+
25+
/**
26+
* Bump this when making backwards incompatible changes to the tracking
27+
* implementation. This allows you to create a segment or view filter
28+
* that isolates only data captured with the most recent tracking changes.
29+
*/
30+
const TRACKING_VERSION = '1';
31+
32+
33+
/**
34+
* A default value for dimensions so unset values always are reported as
35+
* something. This is needed since Google Analytics will drop empty dimension
36+
* values in reports.
37+
*/
38+
const NULL_VALUE = '(not set)';
39+
40+
41+
/**
42+
* A mapping between custom dimension names and their indexes.
43+
*/
44+
const dimensions = {
45+
TRACKING_VERSION: 'dimension1',
46+
CLIENT_ID: 'dimension2',
47+
WINDOW_ID: 'dimension3',
48+
HIT_ID: 'dimension4',
49+
HIT_TIME: 'dimension5',
50+
HIT_TYPE: 'dimension6',
51+
HIT_SOURCE: 'dimension7',
52+
VISIBILITY_STATE: 'dimension8',
53+
URL_QUERY_PARAMS: 'dimension9',
54+
};
55+
56+
57+
/**
58+
* A mapping between custom metric names and their indexes.
59+
*/
60+
const metrics = {
61+
RESPONSE_END_TIME: 'metric1',
62+
DOM_LOAD_TIME: 'metric2',
63+
WINDOW_LOAD_TIME: 'metric3',
64+
PAGE_VISIBLE: 'metric4',
65+
MAX_SCROLL_PERCENTAGE: 'metric5',
66+
};
67+
68+
69+
/**
70+
* Initializes all the analytics setup. Creates trackers and sets initial
71+
* values on the trackers.
72+
*/
73+
export const init = () => {
74+
// Initialize the command queue in case analytics.js hasn't loaded yet.
75+
window.ga = window.ga || ((...args) => (ga.q = ga.q || []).push(args));
76+
77+
createTracker();
78+
trackErrors();
79+
trackCustomDimensions();
80+
requireAutotrackPlugins();
81+
sendInitialPageview();
82+
sendNavigationTimingMetrics();
83+
};
84+
85+
86+
/**
87+
* Tracks a JavaScript error with optional fields object overrides.
88+
* This function is exported so it can be used in other parts of the codebase.
89+
* E.g.:
90+
*
91+
* `fetch('/api.json').catch(trackError);`
92+
*
93+
* @param {Error|undefined} err
94+
* @param {Object=} fieldsObj
95+
*/
96+
export const trackError = (err, fieldsObj = {}) => {
97+
ga('send', 'event', Object.assign({
98+
eventCategory: 'Error',
99+
eventAction: err.name,
100+
eventLabel: `${err.message}\n${err.stack || '(no stack trace)'}`,
101+
nonInteraction: true,
102+
}, fieldsObj));
103+
};
104+
105+
106+
/**
107+
* Creates the trackers and sets the default transport and tracking
108+
* version fields. In non-production environments it also logs hits.
109+
*/
110+
const createTracker = () => {
111+
ga('create', TRACKING_ID, 'auto');
112+
113+
// Ensures all hits are sent via `navigator.sendBeacon()`.
114+
ga('set', 'transport', 'beacon');
115+
};
116+
117+
118+
/**
119+
* Tracks any errors that may have occured on the page prior to analytics being
120+
* initialized, then adds an event handler to track future errors.
121+
*/
122+
const trackErrors = () => {
123+
// Errors that have occurred prior to this script running are stored on
124+
// `window.__e.q`, as specified in `index.html`.
125+
const loadErrorEvents = window.__e && window.__e.q || [];
126+
127+
// Use a different eventCategory for uncaught errors.
128+
const fieldsObj = {eventCategory: 'Uncaught Error'};
129+
130+
// Replay any stored load error events.
131+
for (let event of loadErrorEvents) {
132+
trackError(event.error, fieldsObj);
133+
}
134+
135+
// Add a new listener to track event immediately.
136+
window.addEventListener('error', (event) => {
137+
trackError(event.error, fieldsObj);
138+
});
139+
};
140+
141+
142+
/**
143+
* Sets a default dimension value for all custom dimensions on all trackers.
144+
*/
145+
const trackCustomDimensions = () => {
146+
// Sets a default dimension value for all custom dimensions to ensure
147+
// that every dimension in every hit has *some* value. This is necessary
148+
// because Google Analytics will drop rows with empty dimension values
149+
// in your reports.
150+
Object.keys(dimensions).forEach((key) => {
151+
ga('set', dimensions[key], NULL_VALUE);
152+
});
153+
154+
// Adds tracking of dimensions known at page load time.
155+
ga((tracker) => {
156+
tracker.set({
157+
[dimensions.TRACKING_VERSION]: TRACKING_VERSION,
158+
[dimensions.CLIENT_ID]: tracker.get('clientId'),
159+
[dimensions.WINDOW_ID]: uuid(),
160+
});
161+
});
162+
163+
// Adds tracking to record each the type, time, uuid, and visibility state
164+
// of each hit immediately before it's sent.
165+
ga((tracker) => {
166+
const originalBuildHitTask = tracker.get('buildHitTask');
167+
tracker.set('buildHitTask', (model) => {
168+
const qt = model.get('queueTime') || 0;
169+
model.set(dimensions.HIT_TIME, String(new Date - qt), true);
170+
model.set(dimensions.HIT_ID, uuid(), true);
171+
model.set(dimensions.HIT_TYPE, model.get('hitType'), true);
172+
model.set(dimensions.VISIBILITY_STATE, document.visibilityState, true);
173+
174+
originalBuildHitTask(model);
175+
});
176+
});
177+
};
178+
179+
180+
/**
181+
* Requires select autotrack plugins and initializes each one with its
182+
* respective configuration options.
183+
*/
184+
const requireAutotrackPlugins = () => {
185+
ga('require', 'cleanUrlTracker', {
186+
stripQuery: true,
187+
queryDimensionIndex: getDefinitionIndex(dimensions.URL_QUERY_PARAMS),
188+
trailingSlash: 'remove',
189+
});
190+
ga('require', 'maxScrollTracker', {
191+
sessionTimeout: 30,
192+
timeZone: 'America/Los_Angeles',
193+
maxScrollMetricIndex: getDefinitionIndex(metrics.MAX_SCROLL_PERCENTAGE),
194+
});
195+
ga('require', 'outboundLinkTracker', {
196+
events: ['click', 'contextmenu'],
197+
});
198+
ga('require', 'pageVisibilityTracker', {
199+
visibleMetricIndex: getDefinitionIndex(metrics.PAGE_VISIBLE),
200+
sessionTimeout: 30,
201+
timeZone: 'America/Los_Angeles',
202+
fieldsObj: {[dimensions.HIT_SOURCE]: 'pageVisibilityTracker'},
203+
});
204+
ga('require', 'urlChangeTracker', {
205+
fieldsObj: {[dimensions.HIT_SOURCE]: 'urlChangeTracker'},
206+
});
207+
};
208+
209+
210+
/**
211+
* Sends the initial pageview to Google Analytics.
212+
*/
213+
const sendInitialPageview = () => {
214+
ga('send', 'pageview', {[dimensions.HIT_SOURCE]: 'pageload'});
215+
};
216+
217+
218+
/**
219+
* Gets the DOM and window load times and sends them as custom metrics to
220+
* Google Analytics via an event hit.
221+
*/
222+
const sendNavigationTimingMetrics = () => {
223+
// Only track performance in supporting browsers.
224+
if (!(window.performance && window.performance.timing)) return;
225+
226+
// If the window hasn't loaded, run this function after the `load` event.
227+
if (document.readyState != 'complete') {
228+
window.addEventListener('load', sendNavigationTimingMetrics);
229+
return;
230+
}
231+
232+
const nt = performance.timing;
233+
const navStart = nt.navigationStart;
234+
235+
const responseEnd = Math.round(nt.responseEnd - navStart);
236+
const domLoaded = Math.round(nt.domContentLoadedEventStart - navStart);
237+
const windowLoaded = Math.round(nt.loadEventStart - navStart);
238+
239+
// In some edge cases browsers return very obviously incorrect NT values,
240+
// e.g. 0, negative, or future times. This validates values before sending.
241+
const allValuesAreValid = (...values) => {
242+
return values.every((value) => value > 0 && value < 6e6);
243+
};
244+
245+
if (allValuesAreValid(responseEnd, domLoaded, windowLoaded)) {
246+
ga('send', 'event', {
247+
eventCategory: 'Navigation Timing',
248+
eventAction: 'track',
249+
nonInteraction: true,
250+
[metrics.RESPONSE_END_TIME]: responseEnd,
251+
[metrics.DOM_LOAD_TIME]: domLoaded,
252+
[metrics.WINDOW_LOAD_TIME]: windowLoaded,
253+
});
254+
}
255+
};
256+
257+
258+
/**
259+
* Accepts a custom dimension or metric and returns it's numerical index.
260+
* @param {string} definition The definition string (e.g. 'dimension1').
261+
* @return {number} The definition index.
262+
*/
263+
const getDefinitionIndex = (definition) => +/\d+$/.exec(definition)[0];
264+
265+
266+
/**
267+
* Generates a UUID.
268+
* https://gist.github.com/jed/982883
269+
* @param {string|undefined=} a
270+
* @return {string}
271+
*/
272+
const uuid = function b(a) {
273+
return a ? (a ^ Math.random() * 16 >> a / 4).toString(16) :
274+
([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, b);
275+
};

next.config.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
module.exports = {
2+
webpack: (config, { dev }) => {
3+
// Perform customizations to config
4+
5+
// Important: return the modified config
6+
return config
7+
},
8+
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"description": "https://graphql.guide",
55
"main": "server.js",
66
"scripts": {
7-
"dev": "mongod & babel-node server.js",
7+
"dev": "babel-node server.js",
88
"build": "next build",
99
"start": "NODE_ENV=production babel-node server.js",
1010
"test": "jest",

pages/_document.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ import Document, { Head, Main, NextScript } from 'next/document'
22

33
import raw from '../css/raw'
44

5+
const gaScriptFile = process.env.NODE_ENV === 'production'
6+
? 'analytics.js'
7+
: 'analytics_debug.js'
8+
59
export default class MyDocument extends Document {
610
static async getInitialProps({ renderPage }) {
711
const page = renderPage()
@@ -38,6 +42,10 @@ export default class MyDocument extends Document {
3842
<meta name="msapplication-TileColor" content="#FFFFFF" />
3943
<meta name="msapplication-TileImage" content="/static/favicon/favicon-144.png" />
4044
<meta name="msapplication-config" content="/static/browserconfig.xml" />
45+
46+
<script dangerouslySetInnerHTML={{ __html: "addEventListener('error', window.__e=function f(e){f.q=f.q||[];f.q.push(e)});" }} />
47+
<script async src={`https://www.google-analytics.com/${gaScriptFile}`} />
48+
<script async src="/static/autotrack.js" />
4149
</Head>
4250
<body>
4351
<Main />

pages/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { TimelineLite, TweenLite, Power0, Power2 } from 'gsap'
1414
import CustomEase from '../vendor/gsap/CustomEase'
1515
// import CustomEase from '../vendor/CustomEase'
1616

17+
import Init from '../components/init'
1718
import Delay from '../components/delay'
1819
import Email from '../components/email'
1920
import SubscribeForm from '../components/SubscribeForm'
@@ -141,6 +142,7 @@ class Index extends Component {
141142
return (
142143
<MuiThemeProvider muiTheme={muiTheme}>
143144
<Ripple>
145+
<Init />
144146
<Head>
145147
<title>
146148
The GraphQL Guide

0 commit comments

Comments
 (0)