Single-binary self-hosted website analytics on Rust axum: collector API, dashboards, world map, and PDF reports.
analyticsaxumdockerrustself-hostedsqliteviteweb-analytics
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})();