repos
/ analytics-rust master

analytics-rust

mirror archived upstream

Single-binary self-hosted website analytics on Rust axum: collector API, dashboards, world map, and PDF reports.

analyticsaxumdockerrustself-hostedsqliteviteweb-analytics

6.3 KB · 198 lines · JavaScript Raw History
  1/**
  2 * colector.js
  3 *
  4 * Our basic collector script that is added to all pages we want to collect.
  5 * This sends back the following basic events:
  6 *
  7 * - session_start
  8 * - page_view
  9 * - click
 10 * - scroll
 11 *
 12 * This will also send back custom events that are triggered by pushing data to
 13 * the collectorQueue with an event name and then event data that are key-value
 14 * pairs.
 15 */
 16
 17(function() {
 18  // get the collector cookie as user_id, if it doesn't exist, create it
 19  // set a collector cookie with a random value and set it to expire in a year
 20  function set_cookie(name, value, expires) {
 21    var d = new Date();
 22    d.setTime(d.getTime() + (expires * 24 * 60 * 60 * 1000));
 23    expires = "expires=" + d.toUTCString();
 24    document.cookie = name + "=" + value + "; " + expires + "; path=/";
 25  }
 26
 27  function get_cookie(name) {
 28    name = name + "=";
 29    var ca = document.cookie.split(";");
 30    for (var i = 0; i < ca.length; i++) {
 31      var c = ca[i];
 32      while (c.charAt(0) == " ") {
 33        c = c.substring(1);
 34      }
 35      if (c.indexOf(name) == 0) {
 36        return c.substring(name.length, c.length);
 37      }
 38    }
 39    return "";
 40  }
 41
 42  var collectorUserId = get_cookie("collectoruserid");
 43  if (collectorUserId === "") {
 44    collectorUserId = Math.floor(Math.random() * 1000000000);
 45    set_cookie("collectoruserid", collectorUserId, 365);
 46    window.collectorQueue.push({
 47      collectorId: window.collectorId,
 48      event: "session_start",
 49      data: {
 50        user_id: collectorUserId,
 51        url: window.location.pathname,
 52        title: document.title,
 53        referrer: document.referrer,
 54        screen_width: window.screen.width,
 55        screen_height: window.screen.height,
 56        user_agent: "userAgent" in navigator ? navigator.userAgent : "",
 57        platform: "userAgentData" in navigator ? navigator.userAgentData.platform : "",
 58        device: "userAgentData" in navigator ? navigator.userAgentData.mobile ? "Mobile" : "Desktop" : "",
 59        browser: "userAgentData" in navigator ? navigator.userAgentData.brands[navigator.userAgentData.brands.length - 1].brand : "",
 60      },
 61    });
 62  }
 63
 64  window.collectorQueue = {
 65    data: window.collectorQueue || [],
 66    post: function() {
 67      for (var i = 0; i < this.data.length; i++) {
 68        var data = this.data[i];
 69        if (!data.collectorId) {
 70          data.collectorId = window.collectorId;
 71        }
 72        if (!data.user_id) {
 73          data.user_id = collectorUserId;
 74        }
 75        // keepalive lets the pagehide-triggered page_leave survive the page
 76        // being torn down; without it browsers drop the request mid-unload.
 77        fetch(window.collectorServer + "/collect/", {
 78          method: "POST",
 79          headers: {
 80            "Content-Type": "application/json",
 81          },
 82          body: JSON.stringify(data),
 83          keepalive: true,
 84        });
 85      }
 86      this.data = [];
 87    },
 88    push: function(data) {
 89      this.data.push(data);
 90      this.post();
 91    },
 92  };
 93
 94  // parse querystring into an object
 95  function parse_querystring(querystring) {
 96    var query = {};
 97    var pairs = querystring.split("&");
 98    for (var i = 0; i < pairs.length; i++) {
 99      var pair = pairs[i].split("=");
100      query[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1]);
101    }
102    return query;
103  }
104
105  const query = parse_querystring(window.location.search.substring(1));
106
107  // send a page view event. Screen dimensions are included here too (not just
108  // session_start) so the dashboard's screen-size breakdown populates for
109  // returning visitors whose collectoruserid cookie suppresses session_start.
110  window.collectorQueue.push({
111    collectorId: window.collectorId,
112    event: "page_view",
113    data: {
114      user_id: collectorUserId,
115      url: window.location.pathname,
116      title: document.title,
117      referrer: document.referrer,
118      utm_source: query.utm_source,
119      utm_medium: query.utm_medium,
120      utm_campaign: query.utm_campaign,
121      screen_width: window.screen.width,
122      screen_height: window.screen.height,
123    },
124  });
125
126  // send click and auxclick events
127  document.addEventListener("click", function (event) {
128    window.collectorQueue.push({
129      collectorId: window.collectorId,
130      event: "click",
131      data: {
132        user_id: collectorUserId,
133        url: window.location.pathname,
134        title: document.title,
135        x: event.clientX,
136        y: event.clientY,
137        target: event.target.tagName,
138        // A click on <body> would otherwise ship the whole page's text.
139        text: (event.target.textContent || "").slice(0, 200),
140      },
141    });
142  });
143
144  // send scroll events, but only one per second
145  var last_scroll_event = 0;
146  window.addEventListener("scroll", function () {
147    if (new Date().getTime() - last_scroll_event > 1000) {
148      window.collectorQueue.push({
149        collectorId: window.collectorId,
150        event: "scroll",
151        data: {
152          user_id: collectorUserId,
153          url: window.location.pathname,
154          title: document.title,
155        },
156      });
157      last_scroll_event = new Date().getTime();
158    }
159  });
160
161  // send page_leave events
162  // Track only *visible* time so idle/background tabs don't inflate the metric.
163  // Accumulate elapsed time in chunks bounded by visibilitychange, then flush
164  // on pagehide (more reliable than beforeunload on Safari/mobile).
165  var visible_since = document.visibilityState === "visible" ? new Date().getTime() : null;
166  var visible_accumulated = 0;
167  var page_leave_sent = false;
168
169  document.addEventListener("visibilitychange", function () {
170    var now = new Date().getTime();
171    if (document.visibilityState === "hidden" && visible_since !== null) {
172      visible_accumulated += now - visible_since;
173      visible_since = null;
174    } else if (document.visibilityState === "visible" && visible_since === null) {
175      visible_since = now;
176    }
177  });
178
179  function send_page_leave() {
180    if (page_leave_sent) return;
181    page_leave_sent = true;
182    var now = new Date().getTime();
183    var time_on_page = visible_accumulated + (visible_since !== null ? now - visible_since : 0);
184    window.collectorQueue.push({
185      collectorId: window.collectorId,
186      event: "page_leave",
187      data: {
188        user_id: collectorUserId,
189        url: window.location.pathname,
190        title: document.title,
191        time_on_page: time_on_page,
192      },
193    });
194  }
195
196  window.addEventListener("pagehide", send_page_leave);
197})();