web-analyst
Version:
Web Analyst is a simple back-end tracking system to measure your web app performance.
226 lines (195 loc) • 6.71 kB
JavaScript
(async function ()
{
function lzwEncode(s) {
let dict = {};
let data = (s + "").split("");
let out = [];
let currChar;
let phrase = data[0];
let code = 256;
for (let i = 1; i < data.length; i++) {
currChar = data[i];
if (dict[phrase + currChar] != null) {
phrase += currChar;
} else {
out.push(phrase.length > 1 ? dict[phrase] : phrase.charCodeAt(0));
dict[phrase + currChar] = code;
code++;
phrase = currChar;
}
}
out.push(phrase.length > 1 ? dict[phrase] : phrase.charCodeAt(0));
// Manually convert the LZW codes into a binary string
let encoded = "";
for (let i = 0; i < out.length; i++) {
// Each code is a 12-bit value
encoded += String.fromCharCode(out[i] & 0xFF, out[i] >> 8);
}
// Remove any padding if necessary
encoded = encoded.substr(0, encoded.length - 1);
return btoa(encoded);
}
function lzwDecode(s) {
const data = atob(s);
let dict = {};
let out = [];
let i = 0;
// Read the first code (12 bits)
let oldCode = data.charCodeAt(i) | (data.charCodeAt(i + 1) << 8);
i += 2;
let oldPhrase = String.fromCharCode(oldCode);
out.push(oldPhrase);
let code = 256;
let newCode;
let newPhrase;
let currChar;
while (i < data.length) {
// Read the next code (12 bits)
newCode = data.charCodeAt(i) | (data.charCodeAt(i + 1) << 8);
i += 2;
if (newCode < 256) {
newPhrase = String.fromCharCode(newCode);
} else {
newPhrase = dict[newCode] ? dict[newCode] : (oldPhrase + oldPhrase.charAt(0));
}
out.push(newPhrase);
currChar = newPhrase.charAt(0);
dict[code] = oldPhrase + currChar;
code++;
oldPhrase = newPhrase;
}
return out.join("");
}
function generateId()
{
return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c =>
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
);
}
function getDeviceType()
{
const ua = navigator.userAgent;
if (/CrOS/i.test(ua))
{
return "chromebook";
}
if (/iPad|Tablet/i.test(ua))
{
return "tablet";
}
if (/Mobi|Android/i.test(ua))
{
return "mobile";
}
return "desktop";
}
async function getClientWACookie()
{
const match = document.cookie.match(/(?:^|;\s*)wa-plus=([^;]*)/);
if (!match || !match[1])
{
return {};
}
try
{
const decompressed = lzwDecode(match[1]);
return JSON.parse(decompressed);
}
catch (err)
{
console.warn("Failed to decompress wa-plus cookie:", err);
return {};
}
}
async function setClientWACookie(newData)
{
try
{
const existing = await getClientWACookie();
const merged = {...existing, ...newData};
const str = JSON.stringify(merged);
const compressed = lzwEncode(str);
document.cookie = `wa-plus=${compressed}; path=/; max-age=86400`;
}
catch(e)
{
console.warn("Failed to compress analytics cookie", e);
}
}
async function updateWACookie()
{
const existing = await getClientWACookie();
const baseData = {
tokenClientId: existing.tokenClientId || generateId(),
resolution : `${window.screen.width}x${window.screen.height}`,
viewport : `${window.innerWidth}x${window.innerHeight}`,
deviceType : getDeviceType(),
pixelRatio : window.devicePixelRatio,
language : navigator.language,
timezone : Intl.DateTimeFormat().resolvedOptions().timeZone,
durationMs : existing.durationMs || 0
};
await setClientWACookie({...existing, ...baseData});
}
async function addActiveDuration()
{
let activeStart = null;
let totalDuration = 0;
async function updateDuration()
{
const now = Date.now();
if (activeStart)
{
totalDuration += now - activeStart;
activeStart = null;
const analytics = await getClientWACookie();
const hasOtherFields = Object.keys(analytics).some(k => k !== "durationMs");
analytics.durationMs = (analytics.durationMs || 0) + totalDuration;
if (hasOtherFields)
{
await setClientWACookie(analytics);
}
else
{
console.warn("Skipping cookie update: analytics object is incomplete");
}
totalDuration = 0;
}
}
function handleVisibilityChange()
{
if (document.visibilityState === "visible")
{
activeStart = Date.now();
}
else
{
updateDuration();
}
}
document.addEventListener("visibilitychange", handleVisibilityChange);
window.addEventListener("beforeunload", updateDuration);
if (document.visibilityState === "visible")
{
activeStart = Date.now();
}
}
/**
* Keep full navigational context
* @param maxEntries
*/
async function addVisitedPage(maxEntries = 2)
{
const analytics = await getClientWACookie();
const visited = analytics.visitedPages || [];
const currentPage = window.location.pathname + window.location.search + window.location.hash;
const timestamp = Date.now();
const visitId = generateId();
visited.push({page: currentPage, time: timestamp, visitId});
analytics.visitedPages = visited.slice(-maxEntries);
await setClientWACookie(analytics);
}
await updateWACookie();
await addActiveDuration();
await addVisitedPage();
})();