unites_files/athena.js

Summary

Maintainability
F
3 days
Test Coverage
// @license  magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3.0
/* eslint-disable no-var, semi, prefer-arrow-callback, prefer-template */

/**
 * Collection of methods for sending analytics events to Archive.org's analytics server.
 *
 * These events are used for internal stats and sent (in anonymized form) to Google Analytics.
 *
 * @see analytics.md
 *
 * @type {Object}
 */
window.archive_analytics = (function defineArchiveAnalytics () {
  // keep orignal Date object so as not to be affected by wayback's
  // hijacking global Date object
  var Date = window.Date
  var ARCHIVE_ANALYTICS_VERSION = 2
  var DEFAULT_SERVICE = "ao_2"
  var NO_SAMPLING_SERVICE = "ao_no_sampling" // sends every event instead of a percentage

  var startTime = new Date()

  /**
   * @return {Boolean}
   */
  function isPerformanceTimingApiSupported () {
    return "performance" in window && "timing" in window.performance
  }

  /**
   * Determines how many milliseconds elapsed between the browser starting to parse the DOM and
   * the current time.
   *
   * Uses the Performance API or a fallback value if it's not available.
   *
   * @see https://developer.mozilla.org/en-US/docs/Web/API/Performance_API
   *
   * @return {Number}
   */
  function getLoadTime () {
    var start

    if (isPerformanceTimingApiSupported())
      start = window.performance.timing.domLoading
    else
      start = startTime.getTime()

    return new Date().getTime() - start
  }

  /**
   * Determines how many milliseconds elapsed between the user navigating to the page and
   * the current time.
   *
   * @see https://developer.mozilla.org/en-US/docs/Web/API/Performance_API
   *
   * @return {Number|null} null if the browser doesn't support the Performance API
   */
  function getNavToDoneTime () {
    if (!isPerformanceTimingApiSupported())
      return null

    return new Date().getTime() - window.performance.timing.navigationStart
  }

  /**
   * Performs an arithmetic calculation on a string with a number and unit, while maintaining
   * the unit.
   *
   * @param {String} original value to modify, with a unit
   * @param {Function} doOperation accepts one Number parameter, returns a Number
   * @returns {String}
   */
  function computeWithUnit (original, doOperation) {
    var number = parseFloat(original, 10)
    var unit = original.replace(/(\d*\.\d+)|\d+/, "")

    return doOperation(number) + unit
  }

  /**
   * Computes the default font size of the browser.
   *
   * @returns {String|null} computed font-size with units (typically pixels), null if it cannot be computed
   */
  function getDefaultFontSize () {
    var fontSizeStr

    if (!("getComputedStyle" in window))
      return null

    var style = window.getComputedStyle(document.documentElement)
    if (!style)
      return null

    fontSizeStr = style.fontSize

    // Don't modify the value if tracking book reader.
    if (document.querySelector("#BookReader"))
      return fontSizeStr

    return computeWithUnit(fontSizeStr, function reverseBootstrapFontSize (number) {
      // Undo the 62.5% size applied in the Bootstrap CSS.
      return number * 1.6
    })
  }

  /**
   * Get the URL parameters for a given Location
   * @param  {Location}
   * @return {Object} The URL parameters
   */
  function getParams (location) {
    if (!location) location = window.location
    var vars
    var i
    var pair
    var params = {}
    var query = location.search
    if (!query) return params
    vars = query.substring(1).split("&")
    for (i = 0; i < vars.length; i++) {
      pair = vars[i].split("=")
      params[pair[0]] = decodeURIComponent(pair[1])
    }
    return params
  }

  function getMetaProp (name) {
    var metaTag = document.querySelector("meta[property=" + name + "]")
    return metaTag ? metaTag.getAttribute("content") || null : null
  }

  var ArchiveAnalytics = {
    /**
     * @type {String|null}
     */
    service: getMetaProp("service"),
    mediaType: getMetaProp("mediatype"),
    primaryCollection: getMetaProp("primary_collection"),

    /**
     * Key-value pairs to send in pageviews (you can read this after a pageview to see what was
     * sent).
     *
     * @type {Object}
     */
    values: {},

    /**
     * Sends an analytics ping, preferably using navigator.sendBeacon()
     * @param {Object}   values
     * @param {Function} [onload_callback]      (deprecated) callback to invoke once ping to analytics server is done
     * @param {Boolean}  [augment_for_ao_site]  (deprecated) if true, add some archive.org site-specific values
     */
    send_ping: function send_ping (values, onload_callback, augment_for_ao_site) {
      if (typeof window.navigator !== "undefined" && typeof window.navigator.sendBeacon !== "undefined")
        this.send_ping_via_beacon(values)
      else
        this.send_ping_via_image(values)
    },

    /**
     * Sends a ping via Beacon API
     * NOTE: Assumes window.navigator.sendBeacon exists
     * @param {Object} values Tracking parameters to pass
     */
    send_ping_via_beacon: function send_ping_via_beacon (values) {
      var url = this.generate_tracking_url(values || {})
      window.navigator.sendBeacon(url)
    },

    /**
     * Sends a ping via Image object
     * @param {Object} values Tracking parameters to pass
     */
    send_ping_via_image: function send_ping_via_image (values) {
      var url = this.generate_tracking_url(values || {})
      var loadtime_img = new Image(1, 1)
      loadtime_img.src = url
      loadtime_img.alt = ""
    },

    /**
     * Construct complete tracking URL containing payload
     * @param {Object} params Tracking parameters to pass
     * @return {String} URL to use for tracking call
     */
    generate_tracking_url: function generate_tracking_url (params) {
      var baseUrl = "//athena.archive.org/0.gif"
      var keys
      var outputParams = params
      var outputParamsArray = []

      outputParams.service = outputParams.service || this.service || DEFAULT_SERVICE

      // Build array of querystring parameters
      keys = Object.keys(outputParams)
      keys.forEach(function keyIteration (key) {
        outputParamsArray.push(encodeURIComponent(key) + "=" + encodeURIComponent(outputParams[key]))
      })
      outputParamsArray.push("version=" + ARCHIVE_ANALYTICS_VERSION)
      outputParamsArray.push("count=" + (keys.length + 2)) // Include `version` and `count` in count

      return baseUrl + "?" + outputParamsArray.join("&")
    },

    /**
     * @param {int} page Page number
     */
    send_scroll_fetch_event: function send_scroll_fetch_event (page) {
      var additionalValues = { ev: page }
      var loadTime = getLoadTime()
      var navToDoneTime = getNavToDoneTime()
      if (loadTime) additionalValues.loadtime = loadTime
      if (navToDoneTime) additionalValues.nav_to_done_ms = navToDoneTime
      this.send_event("page_action", "scroll_fetch", location.pathname, additionalValues)
    },

    send_scroll_fetch_base_event: function send_scroll_fetch_base_event () {
      var additionalValues = {}
      var loadTime = getLoadTime()
      var navToDoneTime = getNavToDoneTime()
      if (loadTime) additionalValues.loadtime = loadTime
      if (navToDoneTime) additionalValues.nav_to_done_ms = navToDoneTime
      this.send_event("page_action", "scroll_fetch_base", location.pathname, additionalValues)
    },

    /**
     * @param {Object} [options]
     * @param {String} [options.mediaType]
     * @param {String} [options.mediaLanguage]
     * @param {String} [options.page] The path portion of the page URL
     */
    send_pageview: function send_pageview (options) {
      var settings = options || {}

      var defaultFontSize
      var loadTime = getLoadTime()
      var mediaType = settings.mediaType
      var primaryCollection = settings.primaryCollection
      var page = settings.page
      var navToDoneTime = getNavToDoneTime()

      /**
       * @return {String}
       */
      function get_locale () {
        if (navigator) {
          if (navigator.language)
            return navigator.language

          else if (navigator.browserLanguage)
            return navigator.browserLanguage

          else if (navigator.systemLanguage)
            return navigator.systemLanguage

          else if (navigator.userLanguage)
            return navigator.userLanguage
        }
        return ""
      }

      defaultFontSize = getDefaultFontSize()

      // Set field values
      this.values.kind = "pageview"
      this.values.timediff = (new Date().getTimezoneOffset() / 60) * (-1) // *timezone* diff from UTC
      this.values.locale = get_locale()
      this.values.referrer = (document.referrer == "" ? "-" : document.referrer)

      if (loadTime)
        this.values.loadtime = loadTime

      if (navToDoneTime)
        this.values.nav_to_done_ms = navToDoneTime

      if (settings.trackingId) {
        this.values.ga_tid = settings.trackingId
      }

      /* START CUSTOM DIMENSIONS */
      if (defaultFontSize)
        this.values.iaprop_fontSize = defaultFontSize

      if ("devicePixelRatio" in window)
        this.values.iaprop_devicePixelRatio = window.devicePixelRatio

      if (mediaType)
        this.values.iaprop_mediaType = mediaType

      if (settings.mediaLanguage) {
        this.values.iaprop_mediaLanguage = settings.mediaLanguage
      }

      if (primaryCollection) {
        this.values.iaprop_primaryCollection = primaryCollection
      }
      /* END CUSTOM DIMENSIONS */

      if (page)
        this.values.page = page

      this.send_ping(this.values)
    },

    /**
     * Sends a tracking "Event".
     * @param {string} category
     * @param {string} action
     * @param {string} label
     * @param {Object} additionalEventParams
     */
    send_event: function send_event (
      category,
      action,
      label,
      additionalEventParams
    ) {
      if (!label) label = window.location.pathname
      if (!additionalEventParams) additionalEventParams = {}
      if (additionalEventParams.mediaLanguage) {
        additionalEventParams.ga_cd4 = additionalEventParams.mediaLanguage
        delete additionalEventParams.mediaLanguage
      }
      var eventParams = Object.assign(
        {
          kind: "event",
          ec: category,
          ea: action,
          el: label,
          cache_bust: Math.random()
        },
        additionalEventParams
      )
      this.send_ping(eventParams)
    },

    /**
     * Sends every event instead of a small percentage.
     *
     * Use this sparingly as it can generate a lot of events.
     *
     * @param {string} category
     * @param {string} action
     * @param {string} label
     * @param {Object} additionalEventParams
     */
    send_event_no_sampling: function send_event_no_sampling (
      category,
      action,
      label,
      additionalEventParams
    ) {
      var extraParams = additionalEventParams || {}
      extraParams.service = NO_SAMPLING_SERVICE
      this.send_event(category, action, label, extraParams)
    },

    /**
     * @param {Object} options see this.send_pageview options
     */
    send_pageview_on_load: function send_pageview_on_load (options) {
      var self = this
      window.addEventListener("load", function send_pageview_with_options () {
        self.send_pageview(options)
      })
    },

    /**
     * Handles tracking events passed in URL.
     * Assumes category and action values are separated by a "|" character.
     * NOTE: Uses the unsampled analytics property. Watch out for future high click links!
     * @param {Location}
     */
    process_url_events: function process_url_events (location) {
      var eventValues
      var actionValue
      var eventValue = getParams(location).iax
      if (!eventValue) return
      eventValues = eventValue.split("|")
      actionValue = eventValues.length >= 1 ? eventValues[1] : ""
      this.send_event_no_sampling(
        eventValues[0],
        actionValue,
        window.location.pathname
      )
    },

    /**
     * Attaches handlers for event tracking.
     *
     * To enable click tracking for a link, add a `data-event-click-tracking`
     * attribute containing the Google Analytics Event Category and Action, separated
     * by a vertical pipe (|).
     * e.g. `<a href="foobar" data-event-click-tracking="TopNav|FooBar">`
     *
     * To enable form submit tracking, add a `data-event-form-tracking` attribute
     * to the `form` tag.
     * e.g. `<form data-event-form-tracking="TopNav|SearchForm" method="GET">`
     *
     * Additional tracking options can be added via a `data-event-tracking-options`
     * parameter. This parameter, if included, should be a JSON string of the parameters.
     * Valid parameters are:
     * - service {string}: Corresponds to the Google Analytics property data values flow into
     */
    set_up_event_tracking: function set_up_event_tracking () {
      var self = this
      var clickTrackingAttributeName = "event-click-tracking"
      var formTrackingAttributeName = "event-form-tracking"
      var trackingOptionsAttributeName = "event-tracking-options"

      function handleAction (event, attributeName) {
        var selector = "[data-" + attributeName + "]"
        var eventTarget = event.target
        if (!eventTarget) return
        var target = eventTarget.closest(selector)
        if (!target) return
        var categoryAction
        var categoryActionParts
        var options
        categoryAction = target.dataset[toCamelCase(attributeName)]
        if (!categoryAction) return
        categoryActionParts = categoryAction.split("|")
        options = target.dataset[toCamelCase(trackingOptionsAttributeName)]
        options = options ? JSON.parse(options) : {}
        self.send_event(
          categoryActionParts[0],
          categoryActionParts[1],
          categoryActionParts[2] || window.location.pathname,
          options.service ? { service: options.service } : {}
        )
      }

      function toCamelCase (str) {
        return str.replace(/\W+(.)/g, function (match, chr) {
          return chr.toUpperCase()
        })
      }

      document.addEventListener("click", function (e) {
        handleAction(e, clickTrackingAttributeName)
      })

      document.addEventListener("submit", function (e) {
        handleAction(e, formTrackingAttributeName)
      })
    },

    /**
     * @returns {Object[]}
     */
    get_data_packets: function get_data_packets () {
      return [this.values]
    },

    /**
     * Creates a tracking image for tracking JS compatibility.
     *
     * @param {string} type The type value for track_js_case in query params for 0.gif
     */
    create_tracking_image: function create_tracking_image (type) {
      this.send_ping_via_image({
        cache_bust: Math.random(),
        kind: "track_js",
        track_js_case: type
      })
    }
  }

  return ArchiveAnalytics
}())
// @license-end