Skip to content

Commit

Permalink
Tracker: Track only engagements (pageleave extension) (#5039)
Browse files Browse the repository at this point in the history
* Tracker: Remove tracking pageleaves

* chore(tracker): rename html files

* Update comment
  • Loading branch information
macobo authored Feb 6, 2025
1 parent 3f6c64c commit 63b732e
Show file tree
Hide file tree
Showing 10 changed files with 138 additions and 185 deletions.
82 changes: 34 additions & 48 deletions tracker/src/plausible.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

{{#if pageleave}}
if (eventName === 'pageview') {
currentPageLeaveIgnored = true
currentEngagementIgnored = true
}
{{/if}}
}
Expand All @@ -35,24 +35,24 @@
}

{{#if pageleave}}
// :NOTE: Tracking pageleave events is currently experimental.
// :NOTE: Tracking engagement events is currently experimental.

var currentPageLeaveIgnored
var currentPageLeaveURL = location.href
var currentPageLeaveProps = {}
var currentPageMaxEngagementScrollDepth = -1
var currentEngagementIgnored
var currentEngagementURL = location.href
var currentEngagementProps = {}
var currentEngagementMaxScrollDepth = -1

// Multiple pageviews might be sent by the same script when the page
// uses client-side routing (e.g. hash or history-based). This flag
// prevents registering multiple listeners in those cases.
var listeningPageLeave = false
var listeningOnEngagement = false

// In SPA-s, multiple listeners that trigger the pageleave event
// might fire nearly at the same time. E.g. when navigating back
// in browser history while using hash-based routing - a popstate
// and hashchange will be fired in a very quick succession. This
// flag prevents sending multiple pageleaves in those cases.
var pageLeaveSending = false
// flag prevents sending multiple engagement events in those cases.
var engagementCooldown = false

function getDocumentHeight() {
var body = document.body || {}
Expand Down Expand Up @@ -100,52 +100,39 @@
}
})

function triggerPageLeave() {
if (!pageLeaveSending && !currentPageLeaveIgnored) {
pageLeaveSending = true
setTimeout(function () {pageLeaveSending = false}, 500)
triggerEngagementEvent('pageleave')
}
}

function triggerEngagement() {
// Avoid sending redundant engagement events if user has not scrolled the page
// Note that `currentPageMaxEngagementScrollDepth` default of -1 ensures that at least one
// Note that `currentEngagementMaxScrollDepth` default of -1 ensures that at least one
// engagement event is sent
if (!currentPageLeaveIgnored && currentPageMaxEngagementScrollDepth < maxScrollDepthPx) {
currentPageMaxEngagementScrollDepth = maxScrollDepthPx
triggerEngagementEvent('engagement')
}
if (!engagementCooldown && !currentEngagementIgnored && currentEngagementMaxScrollDepth < maxScrollDepthPx) {
currentEngagementMaxScrollDepth = maxScrollDepthPx
setTimeout(function () {engagementCooldown = false}, 300)

var payload = {
n: 'engagement',
sd: Math.round((maxScrollDepthPx / currentDocumentHeight) * 100),
d: dataDomain,
u: currentEngagementURL,
p: currentEngagementProps
}

}
{{#if hash}}
payload.h = 1
{{/if}}

function triggerEngagementEvent(name) {
var payload = {
n: name,
sd: Math.round((maxScrollDepthPx / currentDocumentHeight) * 100),
d: dataDomain,
u: currentPageLeaveURL,
p: currentPageLeaveProps
sendRequest(endpoint, payload)
}

{{#if hash}}
payload.h = 1
{{/if}}

sendRequest(endpoint, payload)
}

function registerPageLeaveListener() {
if (!listeningPageLeave) {
window.addEventListener('pagehide', triggerPageLeave)

function registerEngagementListener() {
if (!listeningOnEngagement) {
// Only register visibilitychange listener only after initial page load and pageview
document.addEventListener('visibilitychange', function() {
if (document.visibilityState === 'hidden') {
triggerEngagement()
}
})
listeningPageLeave = true
listeningOnEngagement = true
}
}
{{/if}}
Expand Down Expand Up @@ -237,11 +224,11 @@

{{#if pageleave}}
if (isPageview) {
currentPageLeaveIgnored = false
currentPageLeaveURL = payload.u
currentPageLeaveProps = payload.p
currentPageMaxEngagementScrollDepth = -1
registerPageLeaveListener()
currentEngagementIgnored = false
currentEngagementURL = payload.u
currentEngagementProps = payload.p
currentEngagementMaxScrollDepth = -1
registerEngagementListener()
}
{{/if}}

Expand Down Expand Up @@ -292,8 +279,7 @@
{{/unless}}

{{#if pageleave}}
if (isSPANavigation && listeningPageLeave) {
triggerPageLeave()
if (isSPANavigation && listeningOnEngagement) {
triggerEngagement()
currentDocumentHeight = getDocumentHeight()
maxScrollDepthPx = getCurrentScrollDepthPx()
Expand Down
111 changes: 45 additions & 66 deletions tracker/test/pageleave.spec.js → tracker/test/engagement.spec.js
Original file line number Diff line number Diff line change
@@ -1,63 +1,52 @@
const { expectPlausibleInAction, pageleaveCooldown, ignoreEngagementRequests, ignorePageleaveRequests } = require('./support/test-utils')
const { expectPlausibleInAction, engagementCooldown } = require('./support/test-utils')
const { test } = require('@playwright/test')
const { LOCAL_SERVER_ADDR } = require('./support/server')

test.describe('pageleave extension (pageleave events)', () => {
sharedTests('pageleave', ignoreEngagementRequests)
})

test.describe('pageleave extension (engagement events)', () => {
sharedTests('engagement', ignorePageleaveRequests)
})

function sharedTests(expectedEvent, ignoreRequests) {
test.describe('engagement events', () => {
test('sends a pageleave when navigating to the next page', async ({ page }) => {
await expectPlausibleInAction(page, {
action: () => page.goto('/pageleave.html'),
action: () => page.goto('/engagement.html'),
expectedRequests: [{n: 'pageview'}],
})

await expectPlausibleInAction(page, {
action: () => page.click('#navigate-away'),
expectedRequests: [{n: expectedEvent, u: `${LOCAL_SERVER_ADDR}/pageleave.html`}],
shouldIgnoreRequest: ignoreRequests
expectedRequests: [{n: 'engagement', u: `${LOCAL_SERVER_ADDR}/engagement.html`}]
})
})

test('sends an event and a pageview on hash-based SPA navigation', async ({ page }) => {
await expectPlausibleInAction(page, {
action: () => page.goto('/pageleave-hash.html'),
action: () => page.goto('/engagement-hash.html'),
expectedRequests: [{n: 'pageview'}],
})

await expectPlausibleInAction(page, {
action: () => page.click('#hash-nav'),
expectedRequests: [
{n: expectedEvent, u: `${LOCAL_SERVER_ADDR}/pageleave-hash.html`},
{n: 'pageview', u: `${LOCAL_SERVER_ADDR}/pageleave-hash.html#some-hash`}
],
shouldIgnoreRequest: ignoreRequests
{n: 'engagement', u: `${LOCAL_SERVER_ADDR}/engagement-hash.html`},
{n: 'pageview', u: `${LOCAL_SERVER_ADDR}/engagement-hash.html#some-hash`}
]
})
})

test('sends an event and a pageview on history-based SPA navigation', async ({ page }) => {
await expectPlausibleInAction(page, {
action: () => page.goto('/pageleave.html'),
action: () => page.goto('/engagement.html'),
expectedRequests: [{n: 'pageview'}],
})

await expectPlausibleInAction(page, {
action: () => page.click('#history-nav'),
expectedRequests: [
{n: expectedEvent, u: `${LOCAL_SERVER_ADDR}/pageleave.html`},
{n: 'engagement', u: `${LOCAL_SERVER_ADDR}/engagement.html`},
{n: 'pageview', u: `${LOCAL_SERVER_ADDR}/another-page`}
],
shouldIgnoreRequest: ignoreRequests
]
})
})

test('sends an event with the manually overridden URL', async ({ page }) => {
await page.goto('/pageleave-manual.html')
await page.goto('/engagement-manual.html')

await expectPlausibleInAction(page, {
action: () => page.click('#pageview-trigger-custom-url'),
Expand All @@ -66,64 +55,59 @@ function sharedTests(expectedEvent, ignoreRequests) {

await expectPlausibleInAction(page, {
action: () => page.click('#navigate-away'),
expectedRequests: [{n: expectedEvent, u: 'https://example.com/custom/location'}],
shouldIgnoreRequest: ignoreRequests
expectedRequests: [{n: 'engagement', u: 'https://example.com/custom/location'}]
})
})

test('does not send an event when pageview was not sent in manual mode', async ({ page }) => {
await page.goto('/pageleave-manual.html')
await page.goto('/engagement-manual.html')

await expectPlausibleInAction(page, {
action: () => page.click('#navigate-away'),
refutedRequests: [{n: expectedEvent}],
shouldIgnoreRequest: ignoreRequests
refutedRequests: [{n: 'engagement'}]
})
})

test('script.exclusions.hash.pageleave.js sends an event only from URLs where a pageview was sent', async ({ page }) => {
const pageBaseURL = `${LOCAL_SERVER_ADDR}/pageleave-hash-exclusions.html`
const pageBaseURL = `${LOCAL_SERVER_ADDR}/engagement-hash-exclusions.html`

await expectPlausibleInAction(page, {
action: () => page.goto('/pageleave-hash-exclusions.html'),
action: () => page.goto('/engagement-hash-exclusions.html'),
expectedRequests: [{n: 'pageview'}],
})

// After the initial pageview is sent, navigate to ignored page ->
// pageleave event is sent from the initial page URL
await expectPlausibleInAction(page, {
action: () => page.click('#ignored-hash-link'),
expectedRequests: [{n: expectedEvent, u: pageBaseURL, h: 1}],
shouldIgnoreRequest: ignoreRequests
expectedRequests: [{n: 'engagement', u: pageBaseURL, h: 1}]
})

await pageleaveCooldown(page)
await engagementCooldown(page)

// Navigate from ignored page to a tracked page ->
// no pageleave from the current page, pageview on the next page
await expectPlausibleInAction(page, {
action: () => page.click('#hash-link-1'),
expectedRequests: [{n: 'pageview', u: `${pageBaseURL}#hash1`, h: 1}],
refutedRequests: [{n: expectedEvent}],
shouldIgnoreRequest: ignoreRequests
refutedRequests: [{n: 'engagement'}]
})

await pageleaveCooldown(page)
await engagementCooldown(page)

// Navigate from a tracked page to another tracked page ->
// pageleave with the last page URL, pageview with the new URL
await expectPlausibleInAction(page, {
action: () => page.click('#hash-link-2'),
expectedRequests: [
{n: expectedEvent, u: `${pageBaseURL}#hash1`, h: 1},
{n: 'engagement', u: `${pageBaseURL}#hash1`, h: 1},
{n: 'pageview', u: `${pageBaseURL}#hash2`, h: 1}
],
shouldIgnoreRequest: ignoreRequests
]
})
})

test('sends an event with the same props as pageview (manual extension)', async ({ page }) => {
await page.goto('/pageleave-manual.html')
await page.goto('/engagement-manual.html')

await expectPlausibleInAction(page, {
action: () => page.click('#pageview-trigger-custom-props'),
Expand All @@ -132,66 +116,61 @@ function sharedTests(expectedEvent, ignoreRequests) {

await expectPlausibleInAction(page, {
action: () => page.click('#navigate-away'),
expectedRequests: [{n: expectedEvent, p: {author: 'John'}}],
shouldIgnoreRequest: ignoreRequests
expectedRequests: [{n: 'engagement', p: {author: 'John'}}]
})
})

test('sends an event with the same props as pageview (pageview-props extension)', async ({ page }) => {
await expectPlausibleInAction(page, {
action: () => page.goto('/pageleave-pageview-props.html'),
action: () => page.goto('/engagement-pageview-props.html'),
expectedRequests: [{n: 'pageview', p: {author: 'John'}}],
})

await expectPlausibleInAction(page, {
action: () => page.click('#navigate-away'),
expectedRequests: [{n: expectedEvent, p: {author: 'John'}}],
shouldIgnoreRequest: ignoreRequests
expectedRequests: [{n: 'engagement', p: {author: 'John'}}]
})
})

test('sends an event with the same props as pageview (hash navigation / pageview-props extension)', async ({ page }) => {
await expectPlausibleInAction(page, {
action: () => page.goto('/pageleave-hash-pageview-props.html'),
action: () => page.goto('/engagement-hash-pageview-props.html'),
expectedRequests: [{n: 'pageview', p: {}}],
})

await expectPlausibleInAction(page, {
action: () => page.click('#john-post'),
expectedRequests: [
{n: expectedEvent, p: {}},
{n: 'engagement', p: {}},
{n: 'pageview', p: {author: 'john'}}
],
shouldIgnoreRequest: ignoreRequests
]
})

await pageleaveCooldown(page)
await engagementCooldown(page)

await expectPlausibleInAction(page, {
action: () => page.click('#jane-post'),
expectedRequests: [
{n: expectedEvent, p: {author: 'john'}},
{n: 'engagement', p: {author: 'john'}},
{n: 'pageview', p: {author: 'jane'}}
],
shouldIgnoreRequest: ignoreRequests
]
})

await pageleaveCooldown(page)
await engagementCooldown(page)

await expectPlausibleInAction(page, {
action: () => page.click('#home'),
expectedRequests: [
{n: expectedEvent, p: {author: 'jane'}},
{n: 'engagement', p: {author: 'jane'}},
{n: 'pageview', p: {}}
],
shouldIgnoreRequest: ignoreRequests
]
})
})

test('sends an event when plausible API is slow and user navigates away before response is received', async ({ page }) => {
await expectPlausibleInAction(page, {
action: () => page.goto('/pageleave.html'),
expectedRequests: [{n: 'pageview', u: `${LOCAL_SERVER_ADDR}/pageleave.html`}],
action: () => page.goto('/engagement.html'),
expectedRequests: [{n: 'pageview', u: `${LOCAL_SERVER_ADDR}/engagement.html`}],
})

await expectPlausibleInAction(page, {
Expand All @@ -200,13 +179,13 @@ function sharedTests(expectedEvent, ignoreRequests) {
await page.click('#back-button-trigger')
},
expectedRequests: [
{n: expectedEvent, u: `${LOCAL_SERVER_ADDR}/pageleave.html`},
{n: 'pageview', u: `${LOCAL_SERVER_ADDR}/pageleave-pageview-props.html`, p: {author: 'John'}},
{n: expectedEvent, u: `${LOCAL_SERVER_ADDR}/pageleave-pageview-props.html`, p: {author: 'John'}},
{n: 'pageview', u: `${LOCAL_SERVER_ADDR}/pageleave.html`}
{n: 'engagement', u: `${LOCAL_SERVER_ADDR}/engagement.html`},
{n: 'pageview', u: `${LOCAL_SERVER_ADDR}/engagement-pageview-props.html`, p: {author: 'John'}},
{n: 'engagement', u: `${LOCAL_SERVER_ADDR}/engagement-pageview-props.html`, p: {author: 'John'}},
{n: 'pageview', u: `${LOCAL_SERVER_ADDR}/engagement.html`}
],
responseDelay: 1000,
shouldIgnoreRequest: ignoreRequests
responseDelay: 1000
})
})
}

})
File renamed without changes.
File renamed without changes.
Loading

0 comments on commit 63b732e

Please sign in to comment.