|
| 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 | +}; |
0 commit comments