1// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3.0 2/* eslint-disable no-var, semi, prefer-arrow-callback, prefer-template */ 3 4/** 5 * Collection of methods for sending analytics events to Archive.org's analytics server. 6 * 7 * These events are used for internal stats and sent (in anonymized form) to Google Analytics. 8 * 9 * @see analytics.md 10 * 11 * @type {Object} 12 */ 13window.archive_analytics = (function defineArchiveAnalytics() { 14 // keep orignal Date object so as not to be affected by wayback's 15 // hijacking global Date object 16 var Date = window.Date; 17 var ARCHIVE_ANALYTICS_VERSION = 2; 18 var DEFAULT_SERVICE = 'ao_2'; 19 var NO_SAMPLING_SERVICE = 'ao_no_sampling'; // sends every event instead of a percentage 20 21 var startTime = new Date(); 22 23 /** 24 * @return {Boolean} 25 */ 26 function isPerformanceTimingApiSupported() { 27 return 'performance' in window && 'timing' in window.performance; 28 } 29 30 /** 31 * Determines how many milliseconds elapsed between the browser starting to parse the DOM and 32 * the current time. 33 * 34 * Uses the Performance API or a fallback value if it's not available. 35 * 36 * @see https://developer.mozilla.org/en-US/docs/Web/API/Performance_API 37 * 38 * @return {Number} 39 */ 40 function getLoadTime() { 41 var start; 42 43 if (isPerformanceTimingApiSupported()) 44 start = window.performance.timing.domLoading; 45 else 46 start = startTime.getTime(); 47 48 return new Date().getTime() - start; 49 } 50 51 /** 52 * Determines how many milliseconds elapsed between the user navigating to the page and 53 * the current time. 54 * 55 * @see https://developer.mozilla.org/en-US/docs/Web/API/Performance_API 56 * 57 * @return {Number|null} null if the browser doesn't support the Performance API 58 */ 59 function getNavToDoneTime() { 60 if (!isPerformanceTimingApiSupported()) 61 return null; 62 63 return new Date().getTime() - window.performance.timing.navigationStart; 64 } 65 66 /** 67 * Performs an arithmetic calculation on a string with a number and unit, while maintaining 68 * the unit. 69 * 70 * @param {String} original value to modify, with a unit 71 * @param {Function} doOperation accepts one Number parameter, returns a Number 72 * @returns {String} 73 */ 74 function computeWithUnit(original, doOperation) { 75 var number = parseFloat(original, 10); 76 var unit = original.replace(/(\d*\.\d+)|\d+/, ''); 77 78 return doOperation(number) + unit; 79 } 80 81 /** 82 * Computes the default font size of the browser. 83 * 84 * @returns {String|null} computed font-size with units (typically pixels), null if it cannot be computed 85 */ 86 function getDefaultFontSize() { 87 var fontSizeStr; 88 89 if (!('getComputedStyle' in window)) 90 return null; 91 92 var style = window.getComputedStyle(document.documentElement); 93 if (!style) 94 return null; 95 96 fontSizeStr = style.fontSize; 97 98 // Don't modify the value if tracking book reader. 99 if (document.querySelector('#BookReader')) 100 return fontSizeStr; 101 102 return computeWithUnit(fontSizeStr, function reverseBootstrapFontSize(number) { 103 // Undo the 62.5% size applied in the Bootstrap CSS. 104 return number * 1.6; 105 }); 106 } 107 108 /** 109 * Get the URL parameters for a given Location 110 * @param {Location} 111 * @return {Object} The URL parameters 112 */ 113 function getParams(location) { 114 if (!location) location = window.location; 115 var vars; 116 var i; 117 var pair; 118 var params = {}; 119 var query = location.search; 120 if (!query) return params; 121 vars = query.substring(1).split('&'); 122 for (i = 0; i < vars.length; i++) { 123 pair = vars[i].split('='); 124 params[pair[0]] = decodeURIComponent(pair[1]); 125 } 126 return params; 127 } 128 129 function getMetaProp(name) { 130 var metaTag = document.querySelector('meta[property=' + name + ']'); 131 return metaTag ? metaTag.getAttribute('content') || null : null; 132 } 133 134 var ArchiveAnalytics = { 135 /** 136 * @type {String|null} 137 */ 138 service: getMetaProp('service'), 139 mediaType: getMetaProp('mediatype'), 140 primaryCollection: getMetaProp('primary_collection'), 141 142 /** 143 * Key-value pairs to send in pageviews (you can read this after a pageview to see what was 144 * sent). 145 * 146 * @type {Object} 147 */ 148 values: {}, 149 150 /** 151 * Sends an analytics ping, preferably using navigator.sendBeacon() 152 * @param {Object} values 153 * @param {Function} [onload_callback] (deprecated) callback to invoke once ping to analytics server is done 154 * @param {Boolean} [augment_for_ao_site] (deprecated) if true, add some archive.org site-specific values 155 */ 156 send_ping: function send_ping(values, onload_callback, augment_for_ao_site) { 157 if (typeof window.navigator !== 'undefined' && typeof window.navigator.sendBeacon !== 'undefined') 158 this.send_ping_via_beacon(values); 159 else 160 this.send_ping_via_image(values); 161 }, 162 163 /** 164 * Sends a ping via Beacon API 165 * NOTE: Assumes window.navigator.sendBeacon exists 166 * @param {Object} values Tracking parameters to pass 167 */ 168 send_ping_via_beacon: function send_ping_via_beacon(values) { 169 var url = this.generate_tracking_url(values || {}); 170 window.navigator.sendBeacon(url); 171 }, 172 173 /** 174 * Sends a ping via Image object 175 * @param {Object} values Tracking parameters to pass 176 */ 177 send_ping_via_image: function send_ping_via_image(values) { 178 var url = this.generate_tracking_url(values || {}); 179 var loadtime_img = new Image(1, 1); 180 loadtime_img.src = url; 181 loadtime_img.alt = ''; 182 }, 183 184 /** 185 * Construct complete tracking URL containing payload 186 * @param {Object} params Tracking parameters to pass 187 * @return {String} URL to use for tracking call 188 */ 189 generate_tracking_url: function generate_tracking_url(params) { 190 var baseUrl = '//analytics.archive.org/0.gif'; 191 var keys; 192 var outputParams = params; 193 var outputParamsArray = []; 194 195 outputParams.service = outputParams.service || this.service || DEFAULT_SERVICE; 196 197 // Build array of querystring parameters 198 keys = Object.keys(outputParams); 199 keys.forEach(function keyIteration(key) { 200 outputParamsArray.push(encodeURIComponent(key) + '=' + encodeURIComponent(outputParams[key])); 201 }); 202 outputParamsArray.push('version=' + ARCHIVE_ANALYTICS_VERSION); 203 outputParamsArray.push('count=' + (keys.length + 2)); // Include `version` and `count` in count 204 205 return baseUrl + '?' + outputParamsArray.join('&'); 206 }, 207 208 /** 209 * @param {int} page Page number 210 */
211 send_scroll_fetch_event: function send_scroll_fetch_event(page) { 212 var additionalValues = { ev: page }; 213 var loadTime = getLoadTime(); 214 var navToDoneTime = getNavToDoneTime(); 215 if (loadTime) additionalValues.loadtime = loadTime; 216 if (navToDoneTime) additionalValues.nav_to_done_ms = navToDoneTime; 217 this.send_event('page_action', 'scroll_fetch', location.pathname, additionalValues); 218 }, 219 220 send_scroll_fetch_base_event: function send_scroll_fetch_base_event() { 221 var additionalValues = {}; 222 var loadTime = getLoadTime(); 223 var navToDoneTime = getNavToDoneTime(); 224 if (loadTime) additionalValues.loadtime = loadTime; 225 if (navToDoneTime) additionalValues.nav_to_done_ms = navToDoneTime; 226 this.send_event('page_action', 'scroll_fetch_base', location.pathname, additionalValues); 227 }, 228 229 /** 230 * @param {Object} [options] 231 * @param {String} [options.mediaType] 232 * @param {String} [options.mediaLanguage] 233 * @param {String} [options.page] The path portion of the page URL 234 */ 235 send_pageview: function send_pageview(options) { 236 var settings = options || {}; 237 238 var defaultFontSize; 239 var loadTime = getLoadTime(); 240 var mediaType = settings.mediaType; 241 var primaryCollection = settings.primaryCollection; 242 var page = settings.page; 243 var navToDoneTime = getNavToDoneTime(); 244 245 /** 246 * @return {String} 247 */ 248 function get_locale() { 249 if (navigator) { 250 if (navigator.language) 251 return navigator.language; 252 253 else if (navigator.browserLanguage) 254 return navigator.browserLanguage; 255 256 else if (navigator.systemLanguage) 257 return navigator.systemLanguage; 258 259 else if (navigator.userLanguage) 260 return navigator.userLanguage; 261 } 262 return ''; 263 } 264 265 defaultFontSize = getDefaultFontSize(); 266 267 // Set field values 268 this.values.kind = 'pageview'; 269 this.values.timediff = (new Date().getTimezoneOffset()/60)*(-1); // *timezone* diff from UTC 270 this.values.locale = get_locale(); 271 this.values.referrer = (document.referrer == '' ? '-' : document.referrer); 272 273 if (loadTime) 274 this.values.loadtime = loadTime; 275 276 if (navToDoneTime) 277 this.values.nav_to_done_ms = navToDoneTime; 278 279 if (settings.trackingId) { 280 this.values.ga_tid = settings.trackingId; 281 } 282 283 /* START CUSTOM DIMENSIONS */ 284 if (defaultFontSize) 285 this.values.iaprop_fontSize = defaultFontSize; 286 287 if ('devicePixelRatio' in window) 288 this.values.iaprop_devicePixelRatio = window.devicePixelRatio; 289 290 if (mediaType) 291 this.values.iaprop_mediaType = mediaType; 292 293 if (settings.mediaLanguage) { 294 this.values.iaprop_mediaLanguage = settings.mediaLanguage; 295 } 296 297 if (primaryCollection) { 298 this.values.iaprop_primaryCollection = primaryCollection; 299 } 300 /* END CUSTOM DIMENSIONS */ 301 302 if (page) 303 this.values.page = page; 304 305 this.send_ping(this.values); 306 }, 307 308 /** 309 * Sends a tracking "Event". 310 * @param {string} category 311 * @param {string} action 312 * @param {string} label 313 * @param {Object} additionalEventParams 314 */ 315 send_event: function send_event( 316 category, 317 action, 318 label, 319 additionalEventParams 320 ) { 321 if (!label) label = window.location.pathname; 322 if (!additionalEventParams) additionalEventParams = {}; 323 if (additionalEventParams.mediaLanguage) { 324 additionalEventParams.ga_cd4 = additionalEventParams.mediaLanguage; 325 delete additionalEventParams.mediaLanguage; 326 } 327 var eventParams = Object.assign( 328 { 329 kind: 'event', 330 ec: category, 331 ea: action, 332 el: label, 333 cache_bust: Math.random(), 334 }, 335 additionalEventParams 336 ); 337 this.send_ping(eventParams); 338 }, 339 340 /** 341 * Sends every event instead of a small percentage. 342 * 343 * Use this sparingly as it can generate a lot of events. 344 * 345 * @param {string} category 346 * @param {string} action 347 * @param {string} label 348 * @param {Object}
348 additionalEventParams 349 */ 350 send_event_no_sampling: function send_event_no_sampling( 351 category, 352 action, 353 label, 354 additionalEventParams 355 ) { 356 var extraParams = additionalEventParams || {}; 357 extraParams.service = NO_SAMPLING_SERVICE; 358 this.send_event(category, action, label, extraParams); 359 }, 360 361 /** 362 * @param {Object} options see this.send_pageview options 363 */ 364 send_pageview_on_load: function send_pageview_on_load(options) { 365 var self = this; 366 window.addEventListener('load', function send_pageview_with_options() { 367 self.send_pageview(options); 368 }); 369 }, 370 371 /** 372 * Handles tracking events passed in URL. 373 * Assumes category and action values are separated by a "|" character. 374 * NOTE: Uses the unsampled analytics property. Watch out for future high click links! 375 * @param {Location} 376 */ 377 process_url_events: function process_url_events(location) { 378 var eventValues; 379 var actionValue; 380 var eventValue = getParams(location).iax; 381 if (!eventValue) return; 382 eventValues = eventValue.split('|'); 383 actionValue = eventValues.length >= 1 ? eventValues[1] : ''; 384 this.send_event_no_sampling( 385 eventValues[0], 386 actionValue, 387 window.location.pathname 388 ); 389 }, 390 391 /** 392 * Attaches handlers for event tracking. 393 * 394 * To enable click tracking for a link, add a `data-event-click-tracking` 395 * attribute containing the Google Analytics Event Category and Action, separated 396 * by a vertical pipe (|). 397 * e.g. `<a href="foobar" data-event-click-tracking="TopNav|FooBar">` 398 * 399 * To enable form submit tracking, add a `data-event-form-tracking` attribute 400 * to the `form` tag. 401 * e.g. `<form data-event-form-tracking="TopNav|SearchForm" method="GET">` 402 * 403 * Additional tracking options can be added via a `data-event-tracking-options` 404 * parameter. This parameter, if included, should be a JSON string of the parameters. 405 * Valid parameters are: 406 * - service {string}: Corresponds to the Google Analytics property data values flow into 407 */ 408 set_up_event_tracking: function set_up_event_tracking() { 409 var self = this; 410 var clickTrackingAttributeName = 'event-click-tracking'; 411 var formTrackingAttributeName = 'event-form-tracking'; 412 var trackingOptionsAttributeName = 'event-tracking-options'; 413 414 function handleAction(event, attributeName) { 415 var selector = '[data-' + attributeName + ']'; 416 var eventTarget = event.target; 417 if (!eventTarget) return; 418 var target = eventTarget.closest(selector); 419 if (!target) return; 420 var categoryAction; 421 var categoryActionParts; 422 var options; 423 categoryAction = target.dataset[toCamelCase(attributeName)]; 424 if (!categoryAction) return; 425 categoryActionParts = categoryAction.split('|'); 426 options = target.dataset[toCamelCase(trackingOptionsAttributeName)]; 427 options = options ? JSON.parse(options) : {}; 428 self.send_event( 429 categoryActionParts[0], 430 categoryActionParts[1], 431 categoryActionParts[2] || window.location.pathname, 432 options.service ? { service: options.service } : {} 433 ); 434 } 435 436 function toCamelCase(str) { 437 return str.replace(/\W+(.)/g, function (match, chr) { 438 return chr.toUpperCase(); 439 }); 440 }; 441 442 document.addEventListener('click', function(e) { 443 handleAction(e, clickTrackingAttributeName); 444 }); 445 446 document.addEventListener('submit', function(e) { 447 handleAction(e, formTrackingAttributeName); 448 }); 449 }, 450 451 /** 452 * @returns {Object[]} 453 */ 454 get_data_packets: function get_data_packets() { 455 return [this.values]; 456 }, 457 458 /** 459 * Creates a tracking image for tracking JS compatibility. 460 * 461 * @param {string} type The type value for track_js_case in query params for 0.gif 462 */ 463 create_tracking_image: function create_tracking_image(type) { 464 this.send_ping_via_image({ 465 cache_bust: Math.random(), 466 kind: 'track_js', 467 track_js_case: type, 468 }); 469 } 470 }; 471 472 return ArchiveAnalytics; 473}()); 474// @license-end
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.