Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 20 additions & 12 deletions dist/latest/latest.dev.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/* Simple Analytics - Privacy friendly analytics (docs.simpleanalytics.com/script; 2023-05-03; ed1a; v11) */
/* Simple Analytics - Privacy-first analytics (docs.simpleanalytics.com/script; 2025-05-29; 36a5; v12) */
/* eslint-env browser */

(function (
Expand Down Expand Up @@ -110,8 +110,8 @@
return Array.isArray(csv)
? csv
: isString(csv) && csv.length
? csv.split(/, ?/)
: [];
? csv.split(/, ?/)
: [];
};

var isObject = function (object) {
Expand Down Expand Up @@ -432,12 +432,13 @@
// PAYLOAD FOR BOTH PAGE VIEWS AND EVENTS
//

var phantom = window.phantom;
var bot =
nav.webdriver ||
window.__nightmare ||
window.callPhantom ||
window._phantom ||
window.phantom ||
(phantom && !phantom.solana) ||
window.__polypane ||
window._bot ||
isBotAgent ||
Expand Down Expand Up @@ -512,8 +513,12 @@
var lastSendPath;

var getReferrer = function () {
// Customers can overwrite their referrer, here we check for that
var overwrittenReferrer =
overwriteOptions.referrer || attr(scriptElement, "referrer");

return (
(doc.referrer || "")
(overwrittenReferrer || doc.referrer || "")
.replace(locationHostname, definedHostname)
.replace(/^https?:\/\/((m|l|w{2,3}([0-9]+)?)\.)?([^?#]+)(.*)$/, "$4")
.replace(/^([^/]+)$/, "$1") || undefinedVar
Expand Down Expand Up @@ -738,7 +743,10 @@
: falseVar;

// We set unique variable based on pushstate or back navigation, if no match we check the referrer
page.unique = isPushState || userNavigated ? falseVar : !sameSite;
page.unique =
/__cf_/.test(getReferrer()) || isPushState || userNavigated
? falseVar
: !sameSite;

metadata = appendMetadata(metadata, {
type: pageviewText,
Expand Down Expand Up @@ -841,11 +849,11 @@
}

if (autoCollect) pageview();
else {
window.sa_pageview = function (path, metadata) {
pageview(0, path, metadata);
};
}

window.sa_pageview = function (path, metadata) {
pageview(0, path, metadata);
};


/////////////////////
// EVENTS
Expand Down Expand Up @@ -938,6 +946,6 @@
{},
"simpleanalyticscdn.com",
"queue.",
"cdn_latest_dev_11",
"cdn_latest_dev_12",
"sa"
);
116 changes: 0 additions & 116 deletions test/unit/default.test.js

This file was deleted.

78 changes: 78 additions & 0 deletions test/unit/helpers/dom.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
const { JSDOM } = require("jsdom");
const { readFileSync } = require("fs");
const vm = require("vm");

const SCRIPT_PATH = "dist/latest/latest.dev.js";

/**
* @typedef {"navigate" | "reload" | "back_forward" | "prerender"} NavigationType
*/

/** @type {Record<NavigationType, {name: NavigationType, code: number}>} */
const NAVIGATION_TYPES = {
navigate: { name: "navigate", code: 0 },
reload: { name: "reload", code: 1 },
back_forward: { name: "back_forward", code: 2 },
prerender: { name: "prerender", code: 255 },
};

function createDOM(options = {}) {
const {
url = "https://example.com/",
navigationType = "navigate",
settings,
beforeRun,
} = options;
const dom = new JSDOM("<!doctype html><html><body></body></html>", {
url,
runScripts: "outside-only",
pretendToBeVisual: true,
});

if (settings) {
vm.runInContext(
`window.sa_settings = ${JSON.stringify(settings)}`,
dom.getInternalVMContext()
);
}

if (typeof beforeRun === "function") beforeRun(dom.getInternalVMContext());

const sent = [];
dom.window.Image = function () {
return {
set src(value) {
sent.push({ type: "image", url: value });
},
};
};
dom.window.navigator.sendBeacon = function (url, data) {
sent.push({ type: "beacon", url, data });
return true;
};

Object.defineProperty(dom.window, "performance", {
writable: true,
value: {
getEntriesByType: function (type) {
if (type === "navigation") {
return [{ type: NAVIGATION_TYPES[navigationType].name }];
}
return [];
},
navigation: { type: NAVIGATION_TYPES[navigationType].code },
},
});

const script = readFileSync(SCRIPT_PATH, "utf8");
vm.runInContext(script, dom.getInternalVMContext());

dom.sent = sent;
return dom;
}

module.exports = {
createDOM,
SCRIPT_PATH,
NAVIGATION_TYPES,
};
1 change: 1 addition & 0 deletions test/unit/helpers/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require("./dom");
26 changes: 26 additions & 0 deletions test/unit/ignore-pages.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const { expect } = require("chai");
const { createDOM } = require("./helpers/dom");

describe("ignore pages", function () {
it("does not send a request for ignored paths", function (done) {
const dom = createDOM({
settings: { autoCollect: false, ignorePages: "/ignore" },
});

dom.window.sa_pageview("/ignore");
dom.window.sa_pageview("/allowed");

setTimeout(() => {
const ignoreReq = dom.sent.find(
(r) => r.type === "image" && /path=%2Fignore/.test(r.url)
);
const allowedReq = dom.sent.find(
(r) => r.type === "image" && /path=%2Fallowed/.test(r.url)
);

expect(ignoreReq, "request for ignored path").to.not.exist;
expect(allowedReq, "request for allowed path").to.exist;
done();
}, 10);
});
});
42 changes: 42 additions & 0 deletions test/unit/metadata.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
const { expect } = require("chai");
const { createDOM } = require("./helpers/dom");

describe("metadata", function () {
it("collects metadata from global object and collector", function (done) {
const dom = createDOM({
settings: { autoCollect: false, metadataCollector: "collector" },
beforeRun(vmContext) {
const { runInContext } = require("vm");
runInContext(
"window.sa_metadata = { fromGlobal: true };" +
"window.collector = function(data){ return { fromCollector: true, path: data.path }; };",
vmContext
);
},
});

const { runInContext } = require("vm");
runInContext(
"window.manualMeta = { manual: true };",
dom.getInternalVMContext()
);
dom.window.sa_pageview("/meta", dom.window.manualMeta);

setTimeout(() => {
const req = dom.sent.find(
(r) => r.type === "image" && /path=%2Fmeta/.test(r.url)
);
expect(req, "pageview request").to.exist;
const url = new URL(req.url);
const meta = JSON.parse(
decodeURIComponent(url.searchParams.get("metadata"))
);
expect(meta).to.include({
manual: true,
fromGlobal: true,
fromCollector: true,
});
done();
}, 10);
});
});
37 changes: 37 additions & 0 deletions test/unit/pageview.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
const { expect } = require("chai");
const { createDOM } = require("./helpers/dom");

describe("pageview", function () {
it("sends pageview, event and beacon requests", function (done) {
const dom = createDOM({ navigationType: "reload" });

dom.window.sa_event("unit_test");

if (!("onpagehide" in dom.window)) {
dom.window.document.hidden = true;
dom.window.document.dispatchEvent(
new dom.window.Event("visibilitychange")
);
} else {
dom.window.dispatchEvent(new dom.window.Event("pagehide"));
}

setTimeout(() => {
const gif = dom.sent.find(
(r) => r.type === "image" && /simple\.gif/.test(r.url)
);
const eventReq = dom.sent.find(
(r) => r.type === "image" && /event=unit_test/.test(r.url)
);
const beacon = dom.sent.find((r) => r.type === "beacon");

expect(gif, "pageview gif request").to.exist;
expect(eventReq, "event gif request").to.exist;
expect(beacon, "append beacon request").to.exist;
expect(beacon.url).to.match(/\/append$/);
expect(beacon.data).to.include('"type":"append"');

done();
}, 10);
});
});
Loading