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
102 changes: 71 additions & 31 deletions src/default.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@
var offsetHeight = "offset" + Height;
var clientHeight = "client" + Height;
var clientWidth = "client" + Width;
var pagehide = "pagehide";
var platformText = "platform";
var platformVersionText = "platformVersion";
var docsUrl = "https://docs.simpleanalytics.com";
Expand Down Expand Up @@ -432,10 +431,6 @@
// INITIALIZE VALUES
//

/** if duration **/
var start = now();
/** endif **/

/** if scroll **/
var scrolled = 0;
/** endif **/
Expand Down Expand Up @@ -646,11 +641,46 @@
// TIME ON PAGE AND SCROLLED LOGIC
//

// We don't put msHidden in if duration block, because it's used outside of that functionality
var msHidden = 0;
/** if (or duration scroll) **/
/** if duration **/
// Only visible time counts as time on page
var isVisible = function () {
return !doc.hidden;
};
var visibleMs = 0;
var visibleSince = isVisible() ? now() : 0;

var updateVisibleTime = function () {
var time = now();
if (visibleSince) visibleMs += time - visibleSince;
visibleSince = isVisible() ? time : 0;
};

// Optionally send the visible time when it passes thresholds (seconds),
// e.g. data-pings="5,60", so we keep a lower bound when no leave event fires
var pings = convertCommaSeparatedToArray(
settings.pings || attr(scriptElement, "pings")
).sort(function (a, b) {
return a - b;
});
var pingTimer;
var schedulePing = function () {
clearTimeout(pingTimer);
updateVisibleTime();
for (var index = 0; pages && isVisible() && index < pings.length; index++) {
var ms = pings[index] * thousand - visibleMs;
// Skip invalid values and values too large for setTimeout
if (ms > 0 && ms < 2e9)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
return (pingTimer = setTimeout(function () {
sendOnLeave();
schedulePing();
}, ms));
}
};
/** endif **/

var sendOnLeave = function (id, push) {
if (!collectDataOnLeave) return;
if (!collectDataOnLeave || !pages) return;

var append = assign(basePayload, {
type: "append",
Expand All @@ -660,10 +690,9 @@
/** if duration **/
// t = timeonpage
if (collectMetricByString("t")) {
append.duration = Math.round((now() - start - msHidden) / thousand);
updateVisibleTime();
append.duration = Math.floor(visibleMs / thousand);
}
msHidden = 0;
start = now();
/** endif **/

/** if scroll **/
Expand All @@ -673,34 +702,29 @@
}
/** endif **/

if (push || !nav.sendBeacon) {
// sendData will assign payload to request
sendData(append, undefinedVar, trueVar);
} else {
try {
nav.sendBeacon.bind(nav)(fullApiUrl + "/append", stringify(append));
} catch (e) {
// Fallback for browsers throwing "Illegal invocation" when the URL is invalid
sendData(append, undefinedVar, trueVar);
}
}
// The queue drops appends without values
if (!append.duration && !append.scrolled) return;

// sendData will assign payload to request
if (push || !nav.sendBeacon) sendData(append, undefinedVar, trueVar);
else nav.sendBeacon(fullApiUrl + "/append", stringify(append));
Comment thread
coderabbitai[bot] marked this conversation as resolved.
};

/** if duration **/
var hiddenStart;
addEventListenerFunc(
// Getting hidden is the last event that fires reliably, also when mobile
// browsers are killed later, so send the values every time it happens.
// Leaving a page fires it too, so we don't listen to pagehide.
doc.addEventListener(
"visibilitychange",
function () {
if (doc.hidden) {
if (!("on" + pagehide in window)) sendOnLeave();
hiddenStart = now();
} else msHidden += now() - hiddenStart;
updateVisibleTime();
if (!isVisible()) sendOnLeave();
schedulePing();
},
falseVar
);
/** endif **/

addEventListenerFunc(pagehide, sendOnLeave, falseVar);
/** endif **/

/** if scroll **/
var body = doc.body || {};
Expand Down Expand Up @@ -793,7 +817,19 @@
metadata,
callback
) {
if (isPushState) sendOnLeave("" + payload.page_id, trueVar);
/** if (or duration scroll) **/
// Finish the previous page view (SPA navigation or manual pageview)
if (pages) sendOnLeave("" + payload.page_id, trueVar);

// Every page view starts with its own counters
/** if duration **/
updateVisibleTime();
visibleMs = 0;
/** endif **/
/** if scroll **/
scrolled = 0;
/** endif **/
/** endif **/
if (collectDataOnLeave) payload.page_id = uuid();

var currentPage = definedHostname + getPath();
Expand All @@ -820,6 +856,10 @@
referrer = currentPage;

pages++;

/** if duration **/
schedulePing();
/** endif **/
};

var sameSite, userNavigated;
Expand Down
81 changes: 41 additions & 40 deletions test/unit/pageview.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,19 +114,50 @@ describe("pageview", function () {
dom.window.close();
});

// Lets tests move the clock the script uses for time on page
const withClock = (options = {}) => {
const clock = { time: 1e12 };
const dom = createDOM({
...options,
beforeRun(context) {
context.Date.now = () => clock.time;
},
});
dom.clock = clock;
return dom;
};

const setVisibility = (dom, state) => {
Object.defineProperty(dom.window.document, "hidden", {
configurable: true,
value: state == "hidden",
});
dom.window.document.dispatchEvent(new dom.window.Event("visibilitychange"));
};

const appends = (dom) =>
dom.sent
.filter((r) => r.type === "beacon")
.map((r) => JSON.parse(r.data).duration);

it("sends visible time when hidden and the total after returning", function () {
const dom = withClock();
dom.clock.time += 3000;
setVisibility(dom, "hidden");
dom.clock.time += 60000;
setVisibility(dom, "visible");
dom.clock.time += 2000;
setVisibility(dom, "hidden");
expect(appends(dom)).to.deep.equal([3, 5]);
dom.window.close();
});

it("sends pageview, event and beacon requests", function (done) {
const dom = createDOM({ navigationType: "reload" });
const dom = withClock({ navigationType: "reload" });
dom.clock.time += 2000;

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"));
}
setVisibility(dom, "hidden");

setTimeout(() => {
const gif = dom.sent.find(
Expand All @@ -146,34 +177,4 @@ describe("pageview", function () {
done();
}, 10);
});

it("falls back to pixel when sendBeacon fails", function (done) {
const dom = createDOM({ navigationType: "reload" });

dom.window.navigator.sendBeacon = function () {
throw new TypeError("Illegal invocation");
};

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 beacon = dom.sent.find((r) => r.type === "beacon");
const appendGif = dom.sent.find(
(r) => r.type === "image" && /type=append/.test(r.url)
);

expect(beacon, "append beacon request").to.not.exist;
expect(appendGif, "append gif request").to.exist;
done();
}, 10);
});
});
Loading