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;
          });
        });
      }
    });
  },
};