1
mirror of https://github.com/jakejarvis/jarv.is.git synced 2025-04-28 06:10:30 -04:00
jarv.is/assets/js/simple-analytics.js

373 lines
11 KiB
JavaScript

/*! Simple Analytics - Privacy friendly analytics (docs.simpleanalytics.com/script; 2020-05-03; d98d) */
// https://github.com/simpleanalytics/scripts/blob/915d98d39868cbb578619f64b5e2374a5af60c2b/src/default.js
(function (window, baseUrl) {
if (!window) return;
// Generate the needed variables, this seems like a lot of repetition, but it
// makes our script availble for multple destination which prevents us to
// need multiple scripts. The minified version stays small.
var https = "https:";
var pageviewsText = "pageview";
var errorText = "error";
var protocol = https + "//";
var con = window.console;
var slash = "/";
var nav = window.navigator;
var loc = window.location;
var hostname = loc.hostname;
var doc = window.document;
var notSending = "Not sending requests ";
var encodeURIComponentFunc = encodeURIComponent;
var decodeURIComponentFunc = decodeURIComponent;
var stringify = JSON.stringify;
var thousand = 1000;
var addEventListenerFunc = window.addEventListener;
var fullApiUrl = protocol + baseUrl;
var undefinedVar = undefined;
var functionName = "sa_event";
var payload = {
version: 2,
};
var options = {
hostname: hostname,
functionName: functionName,
};
payload.hostname = options.hostname;
// A simple log function so the user knows why a request is not being send
var warn = function (message) {
if (con && con.warn) con.warn("Simple Analytics:", message);
};
var now = Date.now;
var uuid = function () {
var cryptoObject = window.crypto || window.msCrypto;
var emptyUUID = [1e7] + -1e3 + -4e3 + -8e3 + -1e11;
if (cryptoObject && cryptoObject.getRandomValues)
return emptyUUID.replace(/[018]/g, function (c) {
return (c ^ (cryptoObject.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))).toString(16);
});
return emptyUUID.replace(/[018]/g, function (c) {
var r = (Math.random() * 16) | 0,
v = c < 2 ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
};
var assign = function () {
var to = {};
for (var index = 0; index < arguments.length; index++) {
var nextSource = arguments[index];
if (nextSource) {
for (var nextKey in nextSource) {
if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
to[nextKey] = nextSource[nextKey];
}
}
}
}
return to;
};
// This code could error on not having resolvedOptions in the Android Webview, that's why we use try...catch
var timezone;
try {
timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
} catch (e) {
/* Do nothing */
}
// Send data via image
function sendData(data, callback) {
data = assign(payload, data);
var image = new Image();
if (callback) {
image.onerror = callback;
image.onload = callback;
}
image.src =
fullApiUrl +
"/send.gif?" +
Object.keys(data)
.filter(function (key) {
return data[key] != undefinedVar;
})
.map(function (key) {
return encodeURIComponentFunc(key) + "=" + encodeURIComponentFunc(data[key]);
})
.join("&");
}
// Send errors
function sendError(errorOrMessage) {
errorOrMessage = errorOrMessage.message || errorOrMessage;
warn(errorOrMessage);
sendData({
type: errorText,
error: errorOrMessage,
url: options.hostname + loc.pathname,
});
}
// We listen for the error events and only send errors that are
// from our script (checked by filename) to our server.
addEventListenerFunc(
errorText,
function (event) {
if (event.filename && event.filename.indexOf(baseUrl) > -1) {
sendError(event.message);
}
},
false
);
/** if duration **/
var duration = "duration";
var start = now();
/** endif **/
/** if scroll **/
var scrolled = 0;
/** endif **/
// When a customer overwrites the hostname, we need to know what the original
// hostname was to hide that domain from referrer traffic
if (options.hostname !== hostname) payload.hostname_original = hostname;
// Don't track when localhost
/** unless testing **/
if (hostname.indexOf(".") == -1) return warn(notSending + "from " + hostname);
/** endunless **/
try {
var getParams = function (regex) {
// From the search we grab the utm_source and ref and save only that
var matches = loc.search.match(new RegExp("[?&](" + regex + ")=([^?&]+)", "gi"));
var match = matches
? matches.map(function (m) {
return m.split("=")[1];
})
: [];
if (match && match[0]) return match[0];
};
var page = {};
var lastPageId = uuid();
var lastSendPath;
// We don't want to end up with sensitive data so we clean the referrer URL
var utmRegexPrefix = "(utm_)?";
var source = {
source: getParams(utmRegexPrefix + "source|source|ref"),
medium: getParams(utmRegexPrefix + "medium"),
campaign: getParams(utmRegexPrefix + "campaign"),
term: getParams(utmRegexPrefix + "term"),
content: getParams(utmRegexPrefix + "content"),
referrer:
(doc.referrer || "")
.replace(/^https?:\/\/((m|l|w{2,3}([0-9]+)?)\.)?([^?#]+)(.*)$/, "$4")
.replace(/^([^/]+)\/$/, "$1") || undefinedVar,
};
// We don't put msHidden in if duration block, because it's used outside of that functionality
var msHidden = 0;
/** if duration **/
var hiddenStart;
window.addEventListener(
"visibilitychange",
function () {
if (doc.hidden) hiddenStart = now();
else msHidden += now() - hiddenStart;
},
false
);
/** endif **/
var sendBeaconText = "sendBeacon";
var sendOnLeave = function (id, push) {
var append = { type: "append", original_id: push ? id : lastPageId };
/** if duration **/
append[duration] = Math.round((now() - start + msHidden) / thousand);
msHidden = 0;
start = now();
/** endif **/
/** if scroll **/
append.scrolled = Math.max(0, scrolled, position());
/** endif **/
if (push || !(sendBeaconText in nav)) {
sendData(append);
} else {
nav[sendBeaconText](fullApiUrl + "/append", stringify(assign(payload, append)));
}
};
addEventListenerFunc("unload", sendOnLeave, false);
/** if scroll **/
var scroll = "scroll";
var body = doc.body || {};
var documentElement = doc.documentElement || {};
var position = function () {
try {
var Height = "Height";
var scrollHeight = scroll + Height;
var offsetHeight = "offset" + Height;
var clientHeight = "client" + Height;
var documentClientHeight = documentElement[clientHeight] || 0;
var height = Math.max(
body[scrollHeight] || 0,
body[offsetHeight] || 0,
documentElement[clientHeight] || 0,
documentElement[scrollHeight] || 0,
documentElement[offsetHeight] || 0
);
return Math.min(
100,
Math.round((100 * ((documentElement.scrollTop || 0) + documentClientHeight)) / height / 5) * 5
);
} catch (error) {
return 0;
}
};
addEventListenerFunc("load", function () {
scrolled = position();
addEventListenerFunc(
scroll,
function () {
if (scrolled < position()) scrolled = position();
},
false
);
});
/** endif **/
var sendPageView = function (isPushState, deleteSourceInfo) {
if (isPushState) sendOnLeave("" + lastPageId, true);
lastPageId = uuid();
page.id = lastPageId;
sendData(
assign(page, deleteSourceInfo ? null : source, {
https: loc.protocol == https,
timezone: timezone,
width: window.innerWidth,
type: pageviewsText,
})
);
};
var pageview = function (isPushState) {
// Obfuscate personal data in URL by dropping the search and hash
var path = decodeURIComponentFunc(loc.pathname);
// Don't send the last path again (this could happen when pushState is used to change the path hash or search)
if (lastSendPath == path) return;
lastSendPath = path;
var data = {
path: path,
};
// If a user does refresh we need to delete the referrer because otherwise it count double
var perf = window.performance;
var navigation = "navigation";
// Check if back, forward or reload buttons are being used in modern browsers
var userNavigated =
perf &&
perf.getEntriesByType &&
perf.getEntriesByType(navigation)[0] &&
perf.getEntriesByType(navigation)[0].type
? ["reload", "back_forward"].indexOf(perf.getEntriesByType(navigation)[0].type) > -1
: // Check if back, forward or reload buttons are being use in older browsers
// 1: TYPE_RELOAD, 2: TYPE_BACK_FORWARD
perf && perf[navigation] && [1, 2].indexOf(perf[navigation].type) > -1;
/** if uniques **/
// We set unique variable based on pushstate or back navigation, if no match we check the referrer
data.unique =
isPushState || userNavigated ? false : doc.referrer ? doc.referrer.split(slash)[2] != hostname : true;
/** endif **/
page = data;
sendPageView(isPushState, isPushState || userNavigated);
};
pageview();
/** if events **/
var sessionId = uuid();
var validTypes = ["string", "number"];
var sendEvent = function (event, callbackRaw) {
var isFunction = event instanceof Function;
var callback = callbackRaw instanceof Function ? callbackRaw : function () {};
if (validTypes.indexOf(typeof event) < 0 && !isFunction) {
warn("event is not a string: " + event);
return callback();
}
try {
if (isFunction) {
event = event();
if (validTypes.indexOf(typeof event) < 0) {
warn("event function output is not a string: " + event);
return callback();
}
}
} catch (error) {
warn("in your event function: " + error.message);
return callback();
}
event = ("" + event).replace(/[^a-z0-9]+/gi, "_").replace(/(^_|_$)/g, "");
if (event)
sendData(
assign(source, {
type: "event",
event: event,
session_id: sessionId,
}),
callback
);
};
var defaultEventFunc = function (event, callback) {
sendEvent(event, callback);
};
// Set default function if user didn't define a function
if (!window[options.functionName]) window[options.functionName] = defaultEventFunc;
var eventFunc = window[options.functionName];
// Read queue of the user defined function
var queue = eventFunc && eventFunc.q ? eventFunc.q : [];
// Overwrite user defined function
window[options.functionName] = defaultEventFunc;
// Post events from the queue of the user defined function
for (var event in queue) sendEvent(queue[event]);
/** endif **/
} catch (e) {
sendError(e);
}
})(window, "{{ (urls.Parse .Site.BaseURL).Host }}/sa");