Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Average Scroll Depth Metric: extracted tracker changes #4826

Merged
merged 17 commits into from
Nov 21, 2024
Merged
Show file tree
Hide file tree
Changes from 6 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
36 changes: 36 additions & 0 deletions tracker/src/plausible.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,47 @@
// flag prevents sending multiple pageleaves in those cases.
var pageLeaveSending = false

function getDocumentHeight() {
return Math.max(
document.body.scrollHeight || 0,
document.body.offsetHeight || 0,
document.body.clientHeight || 0,
document.documentElement.scrollHeight || 0,
document.documentElement.offsetHeight || 0,
document.documentElement.clientHeight || 0
)
}

function getCurrentScrollDepthPx() {
var viewportHeight = window.innerHeight || document.documentElement.clientHeight || 0
var scrollTop = window.scrollY || document.documentElement.scrollTop || document.body.scrollTop || 0

return currentDocumentHeight <= viewportHeight ? currentDocumentHeight : scrollTop + viewportHeight
}

var currentDocumentHeight = getDocumentHeight()
var maxScrollDepthPx = getCurrentScrollDepthPx()

window.addEventListener('load', function () {
currentDocumentHeight = getDocumentHeight()
macobo marked this conversation as resolved.
Show resolved Hide resolved
})

document.addEventListener('scroll', function() {
var currentScrollDepthPx = getCurrentScrollDepthPx()

if (currentScrollDepthPx > maxScrollDepthPx) {
maxScrollDepthPx = currentScrollDepthPx
}
})

function triggerPageLeave() {
if (pageLeaveSending) {return}
pageLeaveSending = true
setTimeout(function () {pageLeaveSending = false}, 500)

var payload = {
n: 'pageleave',
sd: Math.round((maxScrollDepthPx / currentDocumentHeight) * 100),
d: dataDomain,
u: currentPageLeaveURL,
}
Expand Down Expand Up @@ -202,6 +236,8 @@
if (isSPANavigation && listeningPageLeave) {
triggerPageLeave();
currentPageLeaveURL = location.href;
currentDocumentHeight = getDocumentHeight()
maxScrollDepthPx = getCurrentScrollDepthPx()
}
{{/if}}

Expand Down
Binary file added tracker/test/fixtures/img/black3x3000.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
37 changes: 37 additions & 0 deletions tracker/test/fixtures/scroll-depth-hash.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Plausible Playwright tests</title>
<script defer src="/tracker/js/plausible.hash.local.pageleave.js"></script>
</head>

<body>
<nav>
<a id="home-link" href="#home">Home</a>
<a id="about-link" href="#about">About</a>
</nav>

<div id="content"></div>

<script>
const routes = {
'#home': '<h1>Home</h1><p>Welcome to the Home page!</p>',
'#about': '<h1>About</h1><p style="height: 2000px;">Learn more about us here</p>',
};

function loadContent() {
const hash = window.location.hash || '#home';
const content = routes[hash]
document.getElementById('content').innerHTML = content;
}

window.addEventListener('hashchange', loadContent);
window.addEventListener('DOMContentLoaded', loadContent);
</script>
</body>

</html>
16 changes: 16 additions & 0 deletions tracker/test/fixtures/scroll-depth-slow-window-load.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script defer src="/tracker/js/plausible.local.pageleave.js"></script>
</head>
<body>
<a id="navigate-away" href="/manual.html">
Navigate away
</a>
<br>
<img id="slow-image" src="/img/slow-image" alt="slow image">
</body>
</html>
14 changes: 14 additions & 0 deletions tracker/test/fixtures/scroll-depth.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script defer src="/tracker/js/plausible.local.pageleave.js"></script>
<title>Document</title>
</head>
<body>
<div style="height: 5000px; background: repeating-linear-gradient(white, gray 500px);">
<a id="navigate-away" href="/manual.html">Navigate away</a>
</div>
</body>
</html>
2 changes: 1 addition & 1 deletion tracker/test/pageleave.spec.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable playwright/expect-expect */
/* eslint-disable playwright/no-skipped-test */
const { clickPageElementAndExpectEventRequests, mockRequest } = require('./support/test-utils')
const { test, expect } = require('@playwright/test');
const { test } = require('@playwright/test');
const { LOCAL_SERVER_ADDR } = require('./support/server');

test.describe('pageleave extension', () => {
Expand Down
62 changes: 62 additions & 0 deletions tracker/test/scroll-depth.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/* eslint-disable playwright/expect-expect */
/* eslint-disable playwright/no-skipped-test */
const { clickPageElementAndExpectEventRequests, mockRequest } = require('./support/test-utils')
const { test } = require('@playwright/test');
RobertJoonas marked this conversation as resolved.
Show resolved Hide resolved
const { LOCAL_SERVER_ADDR } = require('./support/server');

test.describe('scroll depth', () => {
RobertJoonas marked this conversation as resolved.
Show resolved Hide resolved
test.skip(({browserName}) => browserName === 'webkit', 'Not testable on Webkit');

test('sends scroll_depth in the pageleave payload when navigating to the next page', async ({ page }) => {
const pageviewRequestMock = mockRequest(page, '/api/event')
await page.goto('/scroll-depth.html');
await pageviewRequestMock;

// eslint-disable-next-line no-undef
RobertJoonas marked this conversation as resolved.
Show resolved Hide resolved
await page.evaluate(() => window.scrollBy(0, 300));
// eslint-disable-next-line no-undef
await page.evaluate(() => window.scrollBy(0, 0));

await clickPageElementAndExpectEventRequests(page, '#navigate-away', [
{n: 'pageleave', u: `${LOCAL_SERVER_ADDR}/scroll-depth.html`, sd: 20}
])
});

test('sends scroll depth on hash navigation', async ({ page }) => {
const pageviewRequestMock = mockRequest(page, '/api/event')
await page.goto('/scroll-depth-hash.html');
await pageviewRequestMock;

await clickPageElementAndExpectEventRequests(page, '#about-link', [
{n: 'pageleave', u: `${LOCAL_SERVER_ADDR}/scroll-depth-hash.html`, sd: 100},
{n: 'pageview', u: `${LOCAL_SERVER_ADDR}/scroll-depth-hash.html#about`}
])

// Wait 600ms before navigating again, because
// pageleave events are throttled to 500ms.

// eslint-disable-next-line playwright/no-wait-for-timeout
await page.waitForTimeout(600)

await clickPageElementAndExpectEventRequests(page, '#home-link', [
{n: 'pageleave', u: `${LOCAL_SERVER_ADDR}/scroll-depth-hash.html#about`, sd: 34},
{n: 'pageview', u: `${LOCAL_SERVER_ADDR}/scroll-depth-hash.html#home`}
])
});

test('document height gets reevaluated after window load', async ({ page }) => {
const pageviewRequestMock = mockRequest(page, '/api/event')
await page.goto('/scroll-depth-slow-window-load.html');
await pageviewRequestMock;

// Wait for the image to be loaded
await page.waitForFunction(() => {
// eslint-disable-next-line no-undef
return document.getElementById('slow-image').complete
});

await clickPageElementAndExpectEventRequests(page, '#navigate-away', [
{n: 'pageleave', u: `${LOCAL_SERVER_ADDR}/scroll-depth-slow-window-load.html`, sd: 24}
])
});
});
7 changes: 7 additions & 0 deletions tracker/test/support/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ exports.runLocalFileServer = function () {
app.use(express.static(FIXTURES_PATH));
app.use('/tracker', express.static(TRACKERS_PATH));

// A test utility - serve an image with an artificial delay
app.get('/img/slow-image', (_req, res) => {
setTimeout(() => {
res.sendFile(path.join(FIXTURES_PATH, '/img/black3x3000.png'));
}, 100);
});

app.listen(LOCAL_SERVER_PORT, function () {
console.log(`Local server listening on ${LOCAL_SERVER_ADDR}`)
});
Expand Down
49 changes: 36 additions & 13 deletions tracker/test/support/test-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,33 +54,56 @@ exports.expectCustomEvent = function (request, eventName, eventProps) {
}
}


/**
* A powerful utility function that makes it easy to assert on the event
* requests that should or should not have been made after clicking a page
* element.
*
* This function accepts subsets of request bodies (the JSON payloads) as
* arguments, and compares them with the bodies of the requests that were
* actually made. For a body subset to match a request, all the key-value
* pairs present in the subset should also appear in the request body.
*/
exports.clickPageElementAndExpectEventRequests = async function (page, locatorToClick, expectedBodySubsets, refutedBodySubsets = []) {
const requestsToExpect = expectedBodySubsets.length
const requestsToAwait = requestsToExpect + refutedBodySubsets.length

const plausibleRequestMockList = mockManyRequests(page, '/api/event', requestsToAwait)
await page.click(locatorToClick)
const requests = await plausibleRequestMockList
const requestBodies = (await plausibleRequestMockList).map(r => r.postDataJSON())

expect(requests.length).toBe(requestsToExpect)
const expectedButNotFoundBodySubsets = []

expectedBodySubsets.forEach((bodySubset) => {
expect(requests.some((request) => {
return hasExpectedBodyParams(request, bodySubset)
})).toBe(true)
const wasFound = requestBodies.some((requestBody) => {
return includesSubset(requestBody, bodySubset)
})

if (!wasFound) {expectedButNotFoundBodySubsets.push(bodySubset)}
})

const refutedButFoundRequestBodies = []

refutedBodySubsets.forEach((bodySubset) => {
expect(requests.every((request) => {
return !hasExpectedBodyParams(request, bodySubset)
})).toBe(true)
const found = requestBodies.find((requestBody) => {
return includesSubset(requestBody, bodySubset)
})

if (found) {refutedButFoundRequestBodies.push(found)}
})
}

function hasExpectedBodyParams(request, expectedBodyParams) {
const body = request.postDataJSON()
const expectedBodySubsetsErrorMessage = `The following body subsets were not found from the requests that were made:\n\n${JSON.stringify(expectedButNotFoundBodySubsets, null, 4)}\n\nReceived requests with the following bodies:\n\n${JSON.stringify(requestBodies, null, 4)}`
expect(expectedButNotFoundBodySubsets, expectedBodySubsetsErrorMessage).toHaveLength(0)

const refutedBodySubsetsErrorMessage = `The following requests were made, but were not expected:\n\n${JSON.stringify(refutedButFoundRequestBodies, null, 4)}`
expect(refutedButFoundRequestBodies, refutedBodySubsetsErrorMessage).toHaveLength(0)

expect(requestBodies.length).toBe(requestsToExpect)
}

return Object.keys(expectedBodyParams).every((key) => {
return body[key] === expectedBodyParams[key]
function includesSubset(body, subset) {
return Object.keys(subset).every((key) => {
return body[key] === subset[key]
})
}
Loading