orchard
mirrorEvery 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
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})();