While the current tracking snippet is good, it can definitely be better. One problem with the async tracking snippet is, by default, it will still create two HTTP requests (one for the analytics.js script and one to send the initial pageview) that will push back the load
event, which will affect other load-based metrics and potentially delay code scheduled to run after the window loads.
There are two ways to solve this problem, the first is to wait until after the load
event fires to run the tracking code, but this is undesirable since it will potentially result in missed pageviews from users who bounce early.
The second option is to use the tracker to use navigator.sendBeacon()
for all subsequent hits:
<script>
window.ga=window.ga||function(){(ga.q=ga.q||[]).push(arguments)};
ga('create', 'UA-XXXXX-Y', 'auto');
ga('set', 'transport', 'beacon');
ga('send', 'pageview');
</script>
<script async src="https://www.google-analytics.com/analytics.js"></script>
The default tracking snippet instructions recommend adding the snippet code to the <head>
of all pages on your site. If you’re just using the snippet as is, that’s probably fine, but as we add more code to track additional user interactions (which we will throughout this post), keeping all that code in the <head>
is a bad idea.
I prefer to put all my analytics-related code in a separate file that I load asynchronously after my other site code has finished loading.
If you use a build tool that supports code splitting (like Webpack), you can lazily initialize your tracking code from your script’s main entry point like this:
/ index.js
const main = () => {
/ Load custom tracking code lazily, so it's non-blocking.
import('./analytics/base.js').then((analytics) => analytics.init());
/ Initiate all other code paths here...
};
/ Start the app through its main entry point.
main();
Your custom tracking code will then live in its own module and can be initialized via its exported init()
function:
/ analytics/base.js
export const init = () => {
/ Initialize the command queue in case analytics.js hasn't loaded yet.
window.ga = window.ga || ((...args) => (ga.q = ga.q || []).push(args));
ga('create', 'UA-XXXXX-Y', 'auto');
ga('set', 'transport', 'beacon');
ga('send', 'pageview');
};
Now your template files only includes the <script async>
tag to load analytics.js as well as your site’s regular JavaScript code:
<!-- Loads the site's main script -->
<script src="path/to/index.js"></script>
<!-- Loads analytics.js asynchronously -->
<script async src="https://www.google-analytics.com/analytics.js"></script>
Note that if you’re not using a build system with code splitting features, you can get the same effect by compiling your tracking code separately and loading it via <script async>
just like you do with analytics.js.[1] Also note that you don’t have to worry about load order; analytics.js is specifically designed to handle cases where it loads first, last, or not at all (e.g. in cases where its blocked by an extension):
<!-- Loads the site's main script -->
<script src="path/to/index.js"></script>
<!-- Loads analytics.js and custom tracking code asynchronously -->
<script async src="https://www.google-analytics.com/analytics.js"></script>
<script async src="/path/to/tracking-code-bundle.js"></script>
A Language, TRACKING_VERSION = '1';
ga('create', 'UA-XXXXX-Y', 'auto');
ga('set', 'transport', 'beacon');
ga('set', dimensions.TRACKING_VERSION, TRACKING_VERSION);
ga('send', 'pageview');
With this new dimension, any time you make a breaking change to your tracking implementation you can increment the version number. Then, at reporting time, you can This code works by passing a function to the this uuid() function because it’s very short, but you can use whatever function you like: Then set the Window ID value on your tracker object before you send any data to Google Analytics: The Google Analytics interface lets you easily report on aggregate data, but if you want to get access to individual user, session, or hit-level data, you’re mostly out of luck. There is a feature called model, thus ensuring they’re applied to every hit: The Hit ID dimension is set to the result of calling our Do you know for sure if your code is running as intended (and without error) for every user who visits your site? Even if you do comprehensive cross-browser/device testing prior to releasing your code, there’s still the possibility that something went wrong or that some browser/device combination you didn’t test will fail. There are paid services like Track:js and Rollbar that do this for you, but you can get a lot of the way there for free with just Google Analytics. I track unhandled errors by adding a global This code stores any unhandled errors in an array on the listener function itself. Then, once the rest of the analytics code has loaded, you can report on these errors with the following functions: Note: the Several years ago Google Analytics introduced Custom metrics (similar to custom dimensions) do not have either of these limitations, so that’s what I use to do all my performance tracking. To see how to track performance via custom metrics, consider three of the more commonly referenced performance metrics from the formatting type to “Integer”), you can use this code to track them: Now you can get the average load time values by dividing the totals for Response End Time, DOM Load Time, and Window Load Time by the metric ga('create', 'UA-XXXXX-Y', 'auto', 'prod');
ga('create', 'UA-XXXXX-Z', 'auto', 'test');
ga('prod.send', 'pageview');
ga('test.send', 'pageview');
Sometimes spammers will send fake data to your Google Analytics account to promote their shady businesses. If you don’t believe me, just search for On mobile the median was 1634 ms when controlled by a service worker vs. 1933 ms when not controlled. There are a lot of features developers would like that Google Analytics doesn’t track by default. However, with only a bit of configuration and extra code, you can do almost everything you want with Google Analytics’ existing extensibility features today. This article provides a brief introduction into my rationale for including a lot of these features in my “boilerplate” analytics.js implementation. If you want to keep up to date with the current state of best practices, you should follow the share it on Twitter.set()
method to assign that value to your newly created custom dimension:const dimensions = {
TRACKING_VERSION: 'dimension1',
CLIENT_ID: 'dimension2',
};
/ ...
ga((tracker) => {
var clientId = tracker.get('clientId');
tracker.set(dimensions.CLIENT_ID, clientId);
});
const uuid = function b(a) {
return a
? (a ^ ((Math.random() * 16) >> (a / 4))).toString(16)
: ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, b);
};
const dimensions = {
TRACKING_VERSION: 'dimension1',
CLIENT_ID: 'dimension2',
WINDOW_ID: 'dimension3',
};
export const init = () => {
/ ...
ga('set', dimensions.WINDOW_ID, uuid());
/ ...
};
Hit ID, time, and type
const dimensions = {
/ ...
HIT_ID: 'dimension4',
HIT_TIME: 'dimension5',
HIT_TYPE: 'dimension6',
};
export const init = () => {
/ ...
ga((tracker) => {
const originalBuildHitTask = tracker.get('buildHitTask');
tracker.set('buildHitTask', (model) => {
model.set(dimensions.HIT_ID, uuid(), true);
model.set(dimensions.HIT_TIME, String(+new Date), true);
model.set(dimensions.HIT_TYPE, model.get('hitType'), true);
originalBuildHitTask(model);
});
});
/ ...
};
uuid()
function (like we did with Window ID), the Hit Time dimension is set to the current timestamp, and the Hit Type dimension is set to the value already stored on the tracker (which, like with client ID, is tracked by GA but not made available in reports):Error tracking
error
event listener as the very first <script>
in the <head>
of the page. It’s important to add this first, so it catches all errors:<script>addEventListener('error', window.__e=function f(e){f.q=f.q||[];f.q.push(e)});</script>
export const init = () => {
/ ...
trackErrors();
/ ...
};
export const trackError = (error, fieldsObj = {}) => {
ga('send', 'event', Object.assign({
eventCategory: 'Script',
eventAction: 'error',
eventLabel: (error && error.stack) || '(not set)',
nonInteraction: true,
}, fieldsObj));
};
const trackErrors = () => {
const loadErrorEvents = window.__e && window.__e.q || [];
const fieldsObj = {eventAction: 'uncaught error'};
/ Replay any stored load error events.
for (let event of loadErrorEvents) {
trackError(event.error, fieldsObj);
}
/ Add a new listener to track event immediately.
window.addEventListener('error', (event) => {
trackError(event.error, fieldsObj);
});
};
nonInteraction
field is set to true to prevent this event from influencing bounce rate calculations. If you’re curious as to why this is important you should read more about Real Time report and event hits do. It’s a shame because, of all the hit types that you’d want to know about in real time, exception hits are clearly at the top of that list.Performance Tracking
Custom performance metrics
const metrics = {
RESPONSE_END_TIME: 'metric1',
DOM_LOAD_TIME: 'metric2',
WINDOW_LOAD_TIME: 'metric3',
};
export const init = () => {
/ …
sendNavigationTimingMetrics();
};
const sendNavigationTimingMetrics = () => {
/ Only track performance in supporting browsers.
if (!(window.performance && window.performance.timing)) return;
/ If the window hasn't loaded, run this function after the `load` event.
if (document.readyState != 'complete') {
window.addEventListener('load', sendNavigationTimingMetrics);
return;
}
const nt = performance.timing;
const navStart = nt.navigationStart;
const responseEnd = Math.round(nt.responseEnd - navStart);
const domLoaded = Math.round(nt.domContentLoadedEventStart - navStart);
const windowLoaded = Math.round(nt.loadEventStart - navStart);
/ In some edge cases browsers return very obviously incorrect NT values,
/ e.g. 0, negative, or future times. This validates values before sending.
const allValuesAreValid = (...values) => {
return values.every((value) => value > 0 && value < 1e6);
};
if (allValuesAreValid(responseEnd, domLoaded, windowLoaded)) {
ga('send', 'event', {
eventCategory: 'Navigation Timing',
eventAction: 'track',
nonInteraction: true,
[metrics.RESPONSE_END_TIME]: responseEnd,
[metrics.DOM_LOAD_TIME]: domLoaded,
[metrics.WINDOW_LOAD_TIME]: windowLoaded,
});
}
};
Filtering out local/spam data
Conclusions