repos
/ orchard main

orchard

mirror

Every site I host, in one repo, along with the Cloudflare Tunnel and Caddy that front them. It's all Go, Vite, and SQLite, and it runs on a desktop at home with nothing listening on an inbound port.

blogbuncaddycloudflare-tunneldockergogolanghomelabhtml-templatemonorepoself-hostedseosqlitestatic-sitetypstuptime-monitoringviteweb-analytics

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