service-worker/matomo-offline.jsjavascript
self.matomoAnalytics = {
initialize: function (options) {
if ("object" !== typeof options) {
options = {};
}
var maxLimitQueue = options.queueLimit || 50;
var maxTimeLimit = options.timeLimit || 60 * 60 * 24; // in seconds...
// same as configured in in tracking_requests_require_authentication_when_custom_timestamp_newer_than
function getQueue() {
return new Promise(function (resolve, reject) {
// do a thing, possibly async, then...
if (!indexedDB) {
reject(new Error("No support for IndexedDB"));
return;
}
var request = indexedDB.open("matomo", 1);
request.onerror = function () {
reject(new Error(request.error));
};
request.onupgradeneeded = function (event) {
var db = event.target.result;
if (!db.objectStoreNames.contains("requests")) {
db.createObjectStore("requests", { autoIncrement: true, keyPath: "id" });
}
};
request.onsuccess = function (event) {
var db = event.target.result;
let transaction = db.transaction("requests", "readwrite");
let requests = transaction.objectStore("requests");
resolve(requests);
};
});
}
function syncQueue() {
// check something in indexdb
return getQueue().then(function (queue) {
queue.openCursor().onsuccess = function (event) {
var cursor = event.target.result;
if (cursor && navigator.onLine) {
cursor.continue();
var queueId = cursor.value.id;
var secondsQueuedAgo = (Date.now() - cursor.value.created) / 1000;
secondsQueuedAgo = parseInt(secondsQueuedAgo, 10);
if (secondsQueuedAgo > maxTimeLimit) {
// too old
getQueue().then(function (queue) {
queue.delete(queueId);
});
return;
}
var init = {
headers: cursor.value.headers,
method: cursor.value.method,
};
if (cursor.value.body) {
init.body = cursor.value.body;
}
if (cursor.value.url.includes("?")) {
cursor.value.url += "&cdo=" + secondsQueuedAgo;
} else if (init.body) {
// todo test if this actually works for bulk requests
init.body = init.body.replace("&idsite=", "&cdo=" + secondsQueuedAgo + "&idsite=");
}
fetch(cursor.value.url, init)
.then(function (response) {
if (response.status < 400) {
getQueue().then(function (queue) {
queue.delete(queueId);
});
}
})
.catch(function (error) {
throw error;
});
}
};
});
}
function limitQueueIfNeeded(queue) {
var countRequest = queue.count();
countRequest.onsuccess = function (event) {
if (event.result > maxLimitQueue) {
// we delete only one at a time because of concurrency some other process might delete data too
queue.openCursor().onsuccess = function (event) {
var cursor = event.target.result;
if (cursor) {
queue.delete(cursor.value.id);
limitQueueIfNeeded(queue);
}
};
}
};
}
self.addEventListener("sync", function (event) {
if (event.tag === "matomoSync") {
syncQueue();
}
});
self.addEventListener("fetch", function (event) {
let isOnline = navigator.onLine;
let isTrackingRequest =
event.request.url.includes("/matomo.php") || event.request.url.includes("/piwik.php");
let isTrackerRequest =
event.request.url.endsWith("/matomo.js") || event.request.url.endsWith("/piwik.js");
if (isTrackerRequest) {
if (isOnline) {
syncQueue();
}
caches.open("matomo").then(function (cache) {
return cache.match(event.request).then(function (response) {
return (
response ||
fetch(event.request).then(function (response) {
cache.put(event.request, response.clone());
return response;
})
);
});
});
} else if (isTrackingRequest && isOnline) {
syncQueue();
event.respondWith(fetch(event.request));
} else if (isTrackingRequest && !isOnline) {
var headers = {};
for (const [header, value] of event.request.headers) {
headers[header] = value;
}
let requestInfo = {
url: event.request.url,
referrer: event.request.referrer,
method: event.request.method,
referrerPolicy: event.request.referrerPolicy,
headers: headers,
created: Date.now(),
};
event.request.text().then(function (postData) {
requestInfo.body = postData;
getQueue().then(function (queue) {
queue.add(requestInfo);
limitQueueIfNeeded(queue);
return queue;
});
});
}
});
},
};