Script v2: Handle exceptions on clicks on links within svgs (#5538)
* Access href attribute more defensively to avoid uncaught errors WIP chore: Bump tracker_script_version to 22 Try accept limitation Clarify issue with tagged link clicks within svgs Fix potential error chain accessing object Revert fixes about tagged events props * Refactor script tests
This commit is contained in:
parent
51647b323d
commit
9c35a0f47c
|
|
@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
- Fix issue with link tracking features (tagged events, file downloads, outbound links) causing errors on the page when clicking on `a` tags within `svg` tags.
|
||||||
|
|
||||||
## [0.3.4] - 2025-07-23
|
## [0.3.4] - 2025-07-23
|
||||||
|
|
||||||
- Plausible loaded indicator `window.plausible.l = true` is set last in initialisation functions
|
- Plausible loaded indicator `window.plausible.l = true` is set last in initialisation functions
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"tracker_script_version": 25,
|
"tracker_script_version": 26,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"deploy": "node compile.js",
|
"deploy": "node compile.js",
|
||||||
|
|
|
||||||
|
|
@ -19,20 +19,23 @@ function isLink(element) {
|
||||||
return element && element.tagName && element.tagName.toLowerCase() === 'a'
|
return element && element.tagName && element.tagName.toLowerCase() === 'a'
|
||||||
}
|
}
|
||||||
|
|
||||||
function shouldFollowLink(event, link) {
|
function shouldInterceptNavigation(event, link) {
|
||||||
// If default has been prevented by an external script, Plausible should not intercept navigation.
|
// If default has been prevented by an external script, Plausible should not intercept navigation.
|
||||||
if (event.defaultPrevented) { return false }
|
if (event.defaultPrevented) return false;
|
||||||
|
var target = link.target;
|
||||||
|
// If the link directs to open the link in a different context, or we're not sure, do not intercept navigation
|
||||||
|
if (target && (typeof target !== 'string' || !target.match(/^_(self|parent|top)$/i))) return false;
|
||||||
|
// If the click is not a regular click (e.g. ctrl, meta, shift, or not a click event), do not intercept navigation
|
||||||
|
if (event.ctrlKey || event.metaKey || event.shiftKey || event.type !== 'click') return false;
|
||||||
|
|
||||||
var targetsCurrentWindow = !link.target || link.target.match(/^_(self|parent|top)$/i)
|
return true;
|
||||||
var isRegularClick = !(event.ctrlKey || event.metaKey || event.shiftKey) && event.type === 'click'
|
|
||||||
return targetsCurrentWindow && isRegularClick
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleLinkClickEvent(event) {
|
function handleLinkClickEvent(event) {
|
||||||
if (event.type === 'auxclick' && event.button !== MIDDLE_MOUSE_BUTTON) { return }
|
if (event.type === 'auxclick' && event.button !== MIDDLE_MOUSE_BUTTON) { return }
|
||||||
|
|
||||||
var link = getLinkEl(event.target)
|
var link = getLinkEl(event.target)
|
||||||
var hrefWithoutQuery = link && link.href && link.href.split('?')[0]
|
var hrefWithoutQuery = link && typeof link.href === 'string' && link.href.split('?')[0]
|
||||||
|
|
||||||
if (COMPILE_TAGGED_EVENTS) {
|
if (COMPILE_TAGGED_EVENTS) {
|
||||||
if (isElementOrParentTagged(link, 0)) {
|
if (isElementOrParentTagged(link, 0)) {
|
||||||
|
|
@ -57,7 +60,7 @@ function handleLinkClickEvent(event) {
|
||||||
|
|
||||||
function sendLinkClickEvent(event, link, eventAttrs) {
|
function sendLinkClickEvent(event, link, eventAttrs) {
|
||||||
// In some legacy variants, this block delays opening the link up to 5 seconds,
|
// In some legacy variants, this block delays opening the link up to 5 seconds,
|
||||||
// or until analytics request finishes, otherwise navigation prevents the analytics event from being sent.
|
// or until analytics request finishes, otherwise navigation could prevent the analytics event from being sent.
|
||||||
if (COMPILE_COMPAT) {
|
if (COMPILE_COMPAT) {
|
||||||
var followedLink = false
|
var followedLink = false
|
||||||
|
|
||||||
|
|
@ -68,7 +71,7 @@ function sendLinkClickEvent(event, link, eventAttrs) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldFollowLink(event, link)) {
|
if (shouldInterceptNavigation(event, link)) {
|
||||||
var attrs = { props: eventAttrs.props, callback: followLink }
|
var attrs = { props: eventAttrs.props, callback: followLink }
|
||||||
if (COMPILE_REVENUE) {
|
if (COMPILE_REVENUE) {
|
||||||
attrs.revenue = eventAttrs.revenue
|
attrs.revenue = eventAttrs.revenue
|
||||||
|
|
@ -93,7 +96,7 @@ function sendLinkClickEvent(event, link, eventAttrs) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function isOutboundLink(link) {
|
function isOutboundLink(link) {
|
||||||
return link && link.href && link.host && link.host !== location.host
|
return link && typeof link.href === 'string' && link.host && link.host !== location.host
|
||||||
}
|
}
|
||||||
|
|
||||||
function isDownloadToTrack(url) {
|
function isDownloadToTrack(url) {
|
||||||
|
|
@ -205,7 +208,7 @@ export function init() {
|
||||||
if (!eventAttrs.name) { return }
|
if (!eventAttrs.name) { return }
|
||||||
|
|
||||||
// In some legacy variants, this block delays submitting the form for up to 5 seconds,
|
// In some legacy variants, this block delays submitting the form for up to 5 seconds,
|
||||||
// or until analytics request finishes, otherwise form-related navigation can prevent the analytics event from being sent.
|
// or until analytics request finishes, otherwise form-related navigation could prevent the analytics event from being sent.
|
||||||
if (COMPILE_COMPAT) {
|
if (COMPILE_COMPAT) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
var formSubmitted = false
|
var formSubmitted = false
|
||||||
|
|
|
||||||
|
|
@ -1,59 +0,0 @@
|
||||||
import { test, expect } from '@playwright/test'
|
|
||||||
import { LOCAL_SERVER_ADDR } from './support/server'
|
|
||||||
|
|
||||||
async function openPage(page, src, endpoint = `${LOCAL_SERVER_ADDR}/api/event`) {
|
|
||||||
await page.goto(`/callbacks.html?src=${src}&endpoint=${endpoint}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
function trackWithCallback(page) {
|
|
||||||
return page.evaluate(() => window.callPlausible())
|
|
||||||
}
|
|
||||||
|
|
||||||
function testCallbacks(trackerScriptSrc) {
|
|
||||||
test("on successful request", async ({ page }) => {
|
|
||||||
await openPage(page, trackerScriptSrc)
|
|
||||||
|
|
||||||
const callbackResult = await trackWithCallback(page)
|
|
||||||
expect(callbackResult).toEqual({ status: 202 })
|
|
||||||
})
|
|
||||||
|
|
||||||
test('on ignored request', async ({ page }) => {
|
|
||||||
const trackerScriptSrcwithoutLocal = trackerScriptSrc.replace('.local', '')
|
|
||||||
await openPage(page, trackerScriptSrcwithoutLocal)
|
|
||||||
|
|
||||||
const callbackResult = await trackWithCallback(page)
|
|
||||||
expect(callbackResult).toEqual(undefined)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('on 404', async ({ page }) => {
|
|
||||||
await openPage(page, trackerScriptSrc, `${LOCAL_SERVER_ADDR}/api/404`)
|
|
||||||
|
|
||||||
const callbackResult = await trackWithCallback(page)
|
|
||||||
expect(callbackResult).toEqual({ status: 404 })
|
|
||||||
})
|
|
||||||
|
|
||||||
test('on network error', async ({ page }) => {
|
|
||||||
await openPage(page, trackerScriptSrc, `h://bad.url////`)
|
|
||||||
|
|
||||||
const callbackResult = await trackWithCallback(page)
|
|
||||||
expect(callbackResult.error).toBeInstanceOf(Error)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
|
||||||
await page.route(`${LOCAL_SERVER_ADDR}/api/event`, (route) => {
|
|
||||||
return route.fulfill({ status: 202, contentType: 'text/plain', body: 'ok' })
|
|
||||||
})
|
|
||||||
await page.route(`${LOCAL_SERVER_ADDR}/api/404`, (route) => {
|
|
||||||
return route.fulfill({ status: 404, contentType: 'text/plain', body: 'ok' })
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
test.describe("callbacks behavior (with fetch)", () => {
|
|
||||||
testCallbacks('/tracker/js/plausible.local.manual.js')
|
|
||||||
})
|
|
||||||
|
|
||||||
test.describe("callbacks behavior (with xhr/compat)", () => {
|
|
||||||
testCallbacks('/tracker/js/plausible.compat.local.manual.js')
|
|
||||||
})
|
|
||||||
|
|
@ -0,0 +1,94 @@
|
||||||
|
import { test, expect } from '@playwright/test'
|
||||||
|
import { LOCAL_SERVER_ADDR } from './support/server'
|
||||||
|
import { initializePageDynamically } from './support/initialize-page-dynamically'
|
||||||
|
import { mockManyRequests } from './support/mock-many-requests'
|
||||||
|
import { switchByMode } from './support/test-utils'
|
||||||
|
|
||||||
|
const DOMAIN = 'example.com'
|
||||||
|
|
||||||
|
for (const mode of ['web', 'esm', 'legacy']) {
|
||||||
|
test.describe(`callback results (${mode})`, () => {
|
||||||
|
for (const {
|
||||||
|
name,
|
||||||
|
captureOnLocalhost,
|
||||||
|
apiPath,
|
||||||
|
mockPath,
|
||||||
|
fulfill,
|
||||||
|
expectedResult
|
||||||
|
} of [
|
||||||
|
{
|
||||||
|
name: 'on successful request',
|
||||||
|
captureOnLocalhost: true,
|
||||||
|
apiPath: '/api/event',
|
||||||
|
mockPath: `/api/event`,
|
||||||
|
fulfill: { status: 202 },
|
||||||
|
expectedResult: { status: 202 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'on 404',
|
||||||
|
captureOnLocalhost: true,
|
||||||
|
apiPath: '/api/event',
|
||||||
|
mockPath: `/api/event`,
|
||||||
|
fulfill: { status: 404 },
|
||||||
|
expectedResult: { status: 404 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'on network error',
|
||||||
|
captureOnLocalhost: true,
|
||||||
|
apiPath: 'h://no-exist',
|
||||||
|
mockPath: `/api/event`,
|
||||||
|
fulfill: { status: 202 },
|
||||||
|
expectedResult: { error: expect.any(Error)}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'on ignored request (because of not having capturing events on localhost)',
|
||||||
|
captureOnLocalhost: false,
|
||||||
|
apiPath: '/api/event',
|
||||||
|
mockPath: `/api/event`,
|
||||||
|
fulfill: { status: 202 },
|
||||||
|
expectedResult: undefined
|
||||||
|
}
|
||||||
|
]) {
|
||||||
|
test(name, async ({ page }, { testId }) => {
|
||||||
|
const config = { domain: DOMAIN, endpoint: apiPath, captureOnLocalhost }
|
||||||
|
const { url } = await initializePageDynamically(page, {
|
||||||
|
testId,
|
||||||
|
scriptConfig: switchByMode(
|
||||||
|
{
|
||||||
|
legacy: `<script data-api="${apiPath}" defer id="plausible" data-domain="${DOMAIN}" src="${
|
||||||
|
captureOnLocalhost
|
||||||
|
? '/tracker/js/plausible.compat.local.manual.js'
|
||||||
|
: '/tracker/js/plausible.compat.manual.js'
|
||||||
|
}"></script>`,
|
||||||
|
web: { domain: DOMAIN, endpoint: apiPath, captureOnLocalhost },
|
||||||
|
esm: `<script type="module">import { init, track } from "/tracker/js/npm_package/plausible.js"; init(${JSON.stringify(
|
||||||
|
config
|
||||||
|
)})</script>`
|
||||||
|
},
|
||||||
|
mode
|
||||||
|
),
|
||||||
|
bodyContent: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
await mockManyRequests({
|
||||||
|
page,
|
||||||
|
path: mockPath,
|
||||||
|
fulfill,
|
||||||
|
awaitedRequestCount: 1,
|
||||||
|
mockRequestTimeout: 2000
|
||||||
|
})
|
||||||
|
await page.goto(url)
|
||||||
|
await page.waitForFunction(() => (window as any).plausible?.l)
|
||||||
|
const callbackResult = await page.evaluate(
|
||||||
|
() =>
|
||||||
|
new Promise((resolve) =>
|
||||||
|
(window as any).plausible('Purchase', {
|
||||||
|
callback: (result) => resolve(result)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
expect(callbackResult).toEqual(expectedResult)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,281 @@
|
||||||
|
import {
|
||||||
|
initializePageDynamically,
|
||||||
|
serializeWithFunctions
|
||||||
|
} from './support/initialize-page-dynamically'
|
||||||
|
import {
|
||||||
|
e,
|
||||||
|
expectPlausibleInAction,
|
||||||
|
hideAndShowCurrentTab,
|
||||||
|
isEngagementEvent,
|
||||||
|
isPageviewEvent,
|
||||||
|
switchByMode
|
||||||
|
} from './support/test-utils'
|
||||||
|
import { test } from '@playwright/test'
|
||||||
|
import { ScriptConfig } from './support/types'
|
||||||
|
import { LOCAL_SERVER_ADDR } from './support/server'
|
||||||
|
const DEFAULT_CONFIG: ScriptConfig = {
|
||||||
|
domain: 'example.com',
|
||||||
|
endpoint: `${LOCAL_SERVER_ADDR}/api/event`,
|
||||||
|
captureOnLocalhost: true
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const mode of ['web', 'esm']) {
|
||||||
|
test.describe(`respects "customProperties" config option (${mode})`, () => {
|
||||||
|
test('if "customProperties" is not set, pageviews and engagement events are sent without "p" parameter', async ({
|
||||||
|
page
|
||||||
|
}, { testId }) => {
|
||||||
|
const { url } = await initializePageDynamically(page, {
|
||||||
|
testId,
|
||||||
|
scriptConfig: switchByMode(
|
||||||
|
{
|
||||||
|
web: DEFAULT_CONFIG,
|
||||||
|
esm: `<script type="module">import { init, track } from '/tracker/js/npm_package/plausible.js'; init(${serializeWithFunctions(
|
||||||
|
DEFAULT_CONFIG
|
||||||
|
)})</script>`
|
||||||
|
},
|
||||||
|
mode
|
||||||
|
),
|
||||||
|
bodyContent: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
await expectPlausibleInAction(page, {
|
||||||
|
action: async () => {
|
||||||
|
await page.goto(url)
|
||||||
|
await hideAndShowCurrentTab(page, { delay: 200 })
|
||||||
|
},
|
||||||
|
expectedRequests: [
|
||||||
|
{
|
||||||
|
n: 'pageview',
|
||||||
|
p: e.toBeUndefined()
|
||||||
|
},
|
||||||
|
{ n: 'engagement', p: e.toBeUndefined() }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('if "customProperties" is set to a fixed value, pageviews and engagement events are sent with "p" parameter', async ({
|
||||||
|
page
|
||||||
|
}, { testId }) => {
|
||||||
|
const config = {
|
||||||
|
...DEFAULT_CONFIG,
|
||||||
|
customProperties: { author: 'John Smith' }
|
||||||
|
}
|
||||||
|
const { url } = await initializePageDynamically(page, {
|
||||||
|
testId,
|
||||||
|
scriptConfig: switchByMode(
|
||||||
|
{
|
||||||
|
web: config,
|
||||||
|
esm: `<script type="module">import { init, track } from '/tracker/js/npm_package/plausible.js'; init(${serializeWithFunctions(
|
||||||
|
config
|
||||||
|
)})</script>`
|
||||||
|
},
|
||||||
|
mode
|
||||||
|
),
|
||||||
|
bodyContent: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
await expectPlausibleInAction(page, {
|
||||||
|
action: async () => {
|
||||||
|
await page.goto(url)
|
||||||
|
await hideAndShowCurrentTab(page, { delay: 200 })
|
||||||
|
},
|
||||||
|
expectedRequests: [
|
||||||
|
{
|
||||||
|
n: 'pageview',
|
||||||
|
p: config.customProperties
|
||||||
|
},
|
||||||
|
{ n: 'engagement', p: config.customProperties }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('if "customProperties" is set to be a function, pageviews and engagement events are sent with "p" parameter', async ({
|
||||||
|
page
|
||||||
|
}, { testId }) => {
|
||||||
|
const config = {
|
||||||
|
...DEFAULT_CONFIG,
|
||||||
|
customProperties: (eventName) => ({
|
||||||
|
author: 'John Smith',
|
||||||
|
eventName: eventName,
|
||||||
|
documentTitle: document.title
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const { url } = await initializePageDynamically(page, {
|
||||||
|
testId,
|
||||||
|
scriptConfig: switchByMode(
|
||||||
|
{
|
||||||
|
web: config,
|
||||||
|
esm: `<script type="module">import { init, track } from '/tracker/js/npm_package/plausible.js'; init(${serializeWithFunctions(
|
||||||
|
config
|
||||||
|
)})</script>`
|
||||||
|
},
|
||||||
|
mode
|
||||||
|
),
|
||||||
|
bodyContent: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
await expectPlausibleInAction(page, {
|
||||||
|
action: async () => {
|
||||||
|
await page.goto(url)
|
||||||
|
await hideAndShowCurrentTab(page, { delay: 200 })
|
||||||
|
},
|
||||||
|
expectedRequests: [
|
||||||
|
{
|
||||||
|
n: 'pageview',
|
||||||
|
p: {
|
||||||
|
author: 'John Smith',
|
||||||
|
eventName: 'pageview',
|
||||||
|
documentTitle: 'Plausible Playwright tests'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
n: 'engagement',
|
||||||
|
// Engagement event is sent with the same custom properties as the pageview event, customProperties function does not get called for these!
|
||||||
|
p: {
|
||||||
|
author: 'John Smith',
|
||||||
|
eventName: 'pageview',
|
||||||
|
documentTitle: 'Plausible Playwright tests'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('specificity: props given in "track" call override any custom properties set in "customProperties"', async ({
|
||||||
|
page
|
||||||
|
}, { testId }) => {
|
||||||
|
const config = {
|
||||||
|
...DEFAULT_CONFIG,
|
||||||
|
customProperties: () => ({
|
||||||
|
author: 'John Smith',
|
||||||
|
title: document.title
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const { url } = await initializePageDynamically(page, {
|
||||||
|
testId,
|
||||||
|
scriptConfig: switchByMode(
|
||||||
|
{
|
||||||
|
web: config,
|
||||||
|
esm: `<script type="module">import { init, track } from '/tracker/js/npm_package/plausible.js'; init(${serializeWithFunctions(
|
||||||
|
config
|
||||||
|
)})</script>`
|
||||||
|
},
|
||||||
|
mode
|
||||||
|
),
|
||||||
|
bodyContent: `<button onclick="window.plausible('subscribed from blog', { props: { title: 'A blog post title' } })">Subscribe</button>`
|
||||||
|
})
|
||||||
|
|
||||||
|
await expectPlausibleInAction(page, {
|
||||||
|
action: async () => {
|
||||||
|
await page.goto(url)
|
||||||
|
await page.click('button')
|
||||||
|
},
|
||||||
|
shouldIgnoreRequest: [isEngagementEvent],
|
||||||
|
expectedRequests: [
|
||||||
|
{
|
||||||
|
n: 'pageview',
|
||||||
|
p: {
|
||||||
|
author: 'John Smith',
|
||||||
|
title: 'Plausible Playwright tests'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
n: 'subscribed from blog',
|
||||||
|
p: {
|
||||||
|
author: 'John Smith',
|
||||||
|
title: 'A blog post title'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('if "customProperties" is defined as not an object or function, it is ignored', async ({
|
||||||
|
page
|
||||||
|
}, { testId }) => {
|
||||||
|
const config = {
|
||||||
|
...DEFAULT_CONFIG,
|
||||||
|
customProperties: 123
|
||||||
|
}
|
||||||
|
|
||||||
|
const { url } = await initializePageDynamically(page, {
|
||||||
|
testId,
|
||||||
|
scriptConfig: switchByMode(
|
||||||
|
{
|
||||||
|
web: config,
|
||||||
|
esm: `<script type="module">import { init, track } from '/tracker/js/npm_package/plausible.js'; init(${serializeWithFunctions(
|
||||||
|
config
|
||||||
|
)})</script>`
|
||||||
|
},
|
||||||
|
mode
|
||||||
|
),
|
||||||
|
bodyContent: `<button onclick="window.plausible('subscribed from blog', { props: { title: 'A blog post title' } })">Subscribe</button>`
|
||||||
|
})
|
||||||
|
|
||||||
|
await expectPlausibleInAction(page, {
|
||||||
|
action: async () => {
|
||||||
|
await page.goto(url)
|
||||||
|
await page.click('button')
|
||||||
|
},
|
||||||
|
shouldIgnoreRequest: [isEngagementEvent],
|
||||||
|
expectedRequests: [
|
||||||
|
{
|
||||||
|
n: 'pageview',
|
||||||
|
p: e.toBeUndefined()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
n: 'subscribed from blog',
|
||||||
|
p: {
|
||||||
|
title: 'A blog post title'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('if "customProperties" is defined to be a function that does not return an object, its output is ignored', async ({
|
||||||
|
page
|
||||||
|
}, { testId }) => {
|
||||||
|
const config = {
|
||||||
|
...DEFAULT_CONFIG,
|
||||||
|
customProperties: () => 'not an object'
|
||||||
|
}
|
||||||
|
|
||||||
|
const { url } = await initializePageDynamically(page, {
|
||||||
|
testId,
|
||||||
|
scriptConfig: switchByMode(
|
||||||
|
{
|
||||||
|
web: config,
|
||||||
|
esm: `<script type="module">import { init, track } from '/tracker/js/npm_package/plausible.js'; init(${serializeWithFunctions(
|
||||||
|
config
|
||||||
|
)})</script>`
|
||||||
|
},
|
||||||
|
mode
|
||||||
|
),
|
||||||
|
bodyContent: `<button onclick="window.plausible('subscribed from blog', { props: { title: 'A blog post title' } })">Subscribe</button>`
|
||||||
|
})
|
||||||
|
|
||||||
|
await expectPlausibleInAction(page, {
|
||||||
|
action: async () => {
|
||||||
|
await page.goto(url)
|
||||||
|
await page.click('button')
|
||||||
|
},
|
||||||
|
shouldIgnoreRequest: [isEngagementEvent],
|
||||||
|
expectedRequests: [
|
||||||
|
{
|
||||||
|
n: 'pageview',
|
||||||
|
p: e.toBeUndefined()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
n: 'subscribed from blog',
|
||||||
|
p: {
|
||||||
|
title: 'A blog post title'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -19,6 +19,156 @@ const DEFAULT_CONFIG: ScriptConfig = {
|
||||||
captureOnLocalhost: true
|
captureOnLocalhost: true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const mode of ['web', 'esm']) {
|
||||||
|
test.describe(`respects "fileDownloads" v2 config option (${mode})`, () => {
|
||||||
|
test('does not track file downloads when `fileDownloads: false`', async ({
|
||||||
|
page
|
||||||
|
}, { testId }) => {
|
||||||
|
const filePath = '/file.csv'
|
||||||
|
const downloadMock = await mockManyRequests({
|
||||||
|
page,
|
||||||
|
path: `${LOCAL_SERVER_ADDR}${filePath}`,
|
||||||
|
fulfill: {
|
||||||
|
contentType: 'text/csv'
|
||||||
|
},
|
||||||
|
awaitedRequestCount: 1
|
||||||
|
})
|
||||||
|
const config = { ...DEFAULT_CONFIG, fileDownloads: false }
|
||||||
|
const { url } = await initializePageDynamically(page, {
|
||||||
|
testId,
|
||||||
|
scriptConfig: switchByMode(
|
||||||
|
{
|
||||||
|
web: config,
|
||||||
|
esm: `<script type="module">import { init, track } from '/tracker/js/npm_package/plausible.js'; init(${JSON.stringify(
|
||||||
|
config
|
||||||
|
)})</script>`
|
||||||
|
},
|
||||||
|
mode
|
||||||
|
),
|
||||||
|
bodyContent: `<a href="${filePath}">📥</a>`
|
||||||
|
})
|
||||||
|
|
||||||
|
await expectPlausibleInAction(page, {
|
||||||
|
action: async () => {
|
||||||
|
await page.goto(url)
|
||||||
|
await page.click('a')
|
||||||
|
},
|
||||||
|
expectedRequests: [
|
||||||
|
{
|
||||||
|
n: 'pageview',
|
||||||
|
d: DEFAULT_CONFIG.domain,
|
||||||
|
u: `${LOCAL_SERVER_ADDR}${url}`
|
||||||
|
}
|
||||||
|
],
|
||||||
|
refutedRequests: [{ n: 'File Download' }],
|
||||||
|
shouldIgnoreRequest: isEngagementEvent
|
||||||
|
})
|
||||||
|
await expect(downloadMock.getRequestList()).resolves.toHaveLength(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('tracks file downloads when `fileDownloads: true`', async ({ page }, {
|
||||||
|
testId
|
||||||
|
}) => {
|
||||||
|
const filePath = '/file.csv'
|
||||||
|
const downloadMock = await mockManyRequests({
|
||||||
|
page,
|
||||||
|
path: `${LOCAL_SERVER_ADDR}${filePath}`,
|
||||||
|
fulfill: {
|
||||||
|
contentType: 'text/csv'
|
||||||
|
},
|
||||||
|
awaitedRequestCount: 1
|
||||||
|
})
|
||||||
|
const config = { ...DEFAULT_CONFIG, fileDownloads: true }
|
||||||
|
const { url } = await initializePageDynamically(page, {
|
||||||
|
testId,
|
||||||
|
scriptConfig: switchByMode(
|
||||||
|
{
|
||||||
|
web: config,
|
||||||
|
esm: `<script type="module">import { init, track } from '/tracker/js/npm_package/plausible.js'; init(${JSON.stringify(
|
||||||
|
config
|
||||||
|
)})</script>`
|
||||||
|
},
|
||||||
|
mode
|
||||||
|
),
|
||||||
|
bodyContent: `<a href="${filePath}">📥</a>`
|
||||||
|
})
|
||||||
|
|
||||||
|
await expectPlausibleInAction(page, {
|
||||||
|
action: async () => {
|
||||||
|
await page.goto(url)
|
||||||
|
await page.click('a')
|
||||||
|
},
|
||||||
|
expectedRequests: [
|
||||||
|
{
|
||||||
|
n: 'pageview',
|
||||||
|
d: DEFAULT_CONFIG.domain,
|
||||||
|
u: `${LOCAL_SERVER_ADDR}${url}`
|
||||||
|
},
|
||||||
|
{ n: 'File Download', p: { url: `${LOCAL_SERVER_ADDR}${filePath}` } }
|
||||||
|
],
|
||||||
|
shouldIgnoreRequest: isEngagementEvent
|
||||||
|
})
|
||||||
|
await expect(downloadMock.getRequestList()).resolves.toHaveLength(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('malformed `fileDownloads: "iso"` option enables the feature with default file types', async ({
|
||||||
|
page
|
||||||
|
}, { testId }) => {
|
||||||
|
const csvFileURL = `https://example.com/file.csv`
|
||||||
|
const isoFileURL = `https://example.com/file.iso`
|
||||||
|
|
||||||
|
const csvMock = await mockManyRequests({
|
||||||
|
page,
|
||||||
|
path: csvFileURL,
|
||||||
|
fulfill: {
|
||||||
|
contentType: 'text/csv'
|
||||||
|
},
|
||||||
|
awaitedRequestCount: 1
|
||||||
|
})
|
||||||
|
const isoMock = await mockManyRequests({
|
||||||
|
page,
|
||||||
|
path: isoFileURL,
|
||||||
|
fulfill: {
|
||||||
|
contentType: 'application/octet-stream'
|
||||||
|
},
|
||||||
|
awaitedRequestCount: 1
|
||||||
|
})
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
...DEFAULT_CONFIG,
|
||||||
|
fileDownloads: 'iso'
|
||||||
|
}
|
||||||
|
const { url } = await initializePageDynamically(page, {
|
||||||
|
testId,
|
||||||
|
scriptConfig: switchByMode(
|
||||||
|
{
|
||||||
|
web: config,
|
||||||
|
esm: `<script type="module">import { init, track } from '/tracker/js/npm_package/plausible.js'; init(${JSON.stringify(
|
||||||
|
config
|
||||||
|
)})</script>`
|
||||||
|
},
|
||||||
|
mode
|
||||||
|
),
|
||||||
|
bodyContent: `<a href="${isoFileURL}" target="_blank">📥</a><a href="${csvFileURL}" target="_blank">📥</a>`
|
||||||
|
})
|
||||||
|
await page.goto(url)
|
||||||
|
await expectPlausibleInAction(page, {
|
||||||
|
action: () => page.click(`a[href="${csvFileURL}"]`),
|
||||||
|
expectedRequests: [{ n: 'File Download', p: { url: csvFileURL } }],
|
||||||
|
shouldIgnoreRequest: [isPageviewEvent, isEngagementEvent]
|
||||||
|
})
|
||||||
|
await expect(csvMock.getRequestList()).resolves.toHaveLength(1)
|
||||||
|
|
||||||
|
await expectPlausibleInAction(page, {
|
||||||
|
action: () => page.click(`a[href="${isoFileURL}"]`),
|
||||||
|
refutedRequests: [{ n: 'File Download' }],
|
||||||
|
shouldIgnoreRequest: [isPageviewEvent, isEngagementEvent]
|
||||||
|
})
|
||||||
|
await expect(isoMock.getRequestList()).resolves.toHaveLength(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
for (const mode of ['legacy', 'web']) {
|
for (const mode of ['legacy', 'web']) {
|
||||||
test.describe(`file downloads feature legacy/v2 parity (${mode})`, () => {
|
test.describe(`file downloads feature legacy/v2 parity (${mode})`, () => {
|
||||||
test('tracks download when link opens in same tab', async ({ page }, {
|
test('tracks download when link opens in same tab', async ({ page }, {
|
||||||
|
|
@ -343,17 +493,11 @@ for (const mode of ['legacy', 'web']) {
|
||||||
})
|
})
|
||||||
await expect(isoMock.getRequestList()).resolves.toHaveLength(1)
|
await expect(isoMock.getRequestList()).resolves.toHaveLength(1)
|
||||||
})
|
})
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const mode of ['web', 'esm']) {
|
test('limitation: does track downloads of links within svg elements', async ({
|
||||||
test.describe(`file downloads feature v2-specific (${mode})`, () => {
|
|
||||||
test('malformed `fileDownloads: "iso"` option enables the feature with default file types', async ({
|
|
||||||
page
|
page
|
||||||
}, { testId }) => {
|
}, { testId }) => {
|
||||||
const csvFileURL = `https://example.com/file.csv`
|
const csvFileURL = `https://example.com/file.csv`
|
||||||
const isoFileURL = `https://example.com/file.iso`
|
|
||||||
|
|
||||||
const csvMock = await mockManyRequests({
|
const csvMock = await mockManyRequests({
|
||||||
page,
|
page,
|
||||||
path: csvFileURL,
|
path: csvFileURL,
|
||||||
|
|
@ -362,46 +506,35 @@ for (const mode of ['web', 'esm']) {
|
||||||
},
|
},
|
||||||
awaitedRequestCount: 1
|
awaitedRequestCount: 1
|
||||||
})
|
})
|
||||||
const isoMock = await mockManyRequests({
|
|
||||||
page,
|
|
||||||
path: isoFileURL,
|
|
||||||
fulfill: {
|
|
||||||
contentType: 'application/octet-stream'
|
|
||||||
},
|
|
||||||
awaitedRequestCount: 1
|
|
||||||
})
|
|
||||||
|
|
||||||
const config = {
|
|
||||||
...DEFAULT_CONFIG,
|
|
||||||
fileDownloads: 'iso'
|
|
||||||
}
|
|
||||||
const { url } = await initializePageDynamically(page, {
|
const { url } = await initializePageDynamically(page, {
|
||||||
testId,
|
testId,
|
||||||
scriptConfig: switchByMode(
|
scriptConfig: switchByMode(
|
||||||
{
|
{
|
||||||
web: config,
|
web: { ...DEFAULT_CONFIG, fileDownloads: true },
|
||||||
esm: `<script type="module">import { init, track } from '/tracker/js/npm_package/plausible.js'; window.init = init; window.track = track; init(${JSON.stringify(
|
legacy:
|
||||||
config
|
'<script defer src="/tracker/js/plausible.file-downloads.local.js"></script>'
|
||||||
)})</script>`
|
|
||||||
},
|
},
|
||||||
mode
|
mode
|
||||||
),
|
),
|
||||||
bodyContent: `<a href="${isoFileURL}" target="_blank">📥</a><a href="${csvFileURL}" target="_blank">📥</a>`
|
bodyContent: `
|
||||||
|
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><a href="${csvFileURL}"><circle cx="50" cy="50" r="50" /></a></svg>
|
||||||
|
`
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const pageErrors: Error[] = []
|
||||||
|
page.on('pageerror', (err) => pageErrors.push(err))
|
||||||
|
|
||||||
await page.goto(url)
|
await page.goto(url)
|
||||||
await expectPlausibleInAction(page, {
|
|
||||||
action: () => page.click(`a[href="${csvFileURL}"]`),
|
|
||||||
expectedRequests: [{ n: 'File Download', p: { url: csvFileURL } }],
|
|
||||||
shouldIgnoreRequest: [isPageviewEvent, isEngagementEvent]
|
|
||||||
})
|
|
||||||
await expect(csvMock.getRequestList()).resolves.toHaveLength(1)
|
|
||||||
|
|
||||||
await expectPlausibleInAction(page, {
|
await expectPlausibleInAction(page, {
|
||||||
action: () => page.click(`a[href="${isoFileURL}"]`),
|
action: () => page.click('a'),
|
||||||
refutedRequests: [{ n: 'File Download' }],
|
refutedRequests: [{ n: 'File Download' }],
|
||||||
shouldIgnoreRequest: [isPageviewEvent, isEngagementEvent]
|
shouldIgnoreRequest: [isPageviewEvent, isEngagementEvent]
|
||||||
})
|
})
|
||||||
await expect(isoMock.getRequestList()).resolves.toHaveLength(1)
|
|
||||||
|
expect(pageErrors).toHaveLength(0)
|
||||||
|
await expect(csvMock.getRequestList()).resolves.toHaveLength(1)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
<!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>callbacks</title>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<script>
|
|
||||||
const params = new URLSearchParams(window.location.search)
|
|
||||||
const src = params.get('src')
|
|
||||||
const endpoint = params.get('endpoint')
|
|
||||||
|
|
||||||
const script = document.createElement('script')
|
|
||||||
|
|
||||||
script.src = src
|
|
||||||
script.id = 'plausible'
|
|
||||||
script.setAttribute('data-domain', 'test.com')
|
|
||||||
script.setAttribute('data-api', endpoint)
|
|
||||||
|
|
||||||
var r = document.getElementsByTagName("script")[0]
|
|
||||||
r.parentNode.insertBefore(script, r)
|
|
||||||
|
|
||||||
window.callPlausible = function () {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
plausible('event', {
|
|
||||||
callback: (result) => { resolve(result) }
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
<!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 NPM package tests</title>
|
|
||||||
<script type="module">
|
|
||||||
import { init, track } from './tracker/js/npm_package/plausible.js'
|
|
||||||
|
|
||||||
window.init = init
|
|
||||||
window.track = track
|
|
||||||
</script>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<a id="file-download" href="https://awesome.website.com/file.pdf">Download</a>
|
|
||||||
|
|
||||||
<a id="outbound-link" href="https://example.com">Outbound link</a>
|
|
||||||
|
|
||||||
<a id="manual-pageview-1" onclick="window.track('pageview', { u: '/:test-plausible' })">Manual pageview</a>
|
|
||||||
<a id="manual-pageview-2" onclick="window.track('pageview', { url: '/:test-plausible-2' })">Manual pageview 2</a>
|
|
||||||
|
|
||||||
<a id="custom-event" onclick="window.track('Custom event', { props: { author: 'Karl' } })">Custom event</a>
|
|
||||||
|
|
||||||
<button id="tagged-event" class="plausible-event-name=Purchase plausible-event-foo=bar plausible-revenue-currency=EUR plausible-revenue-amount=13.32">
|
|
||||||
Tagged event
|
|
||||||
</button>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -1,58 +0,0 @@
|
||||||
<!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-web.js tests</title>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<a id="file-download" href="https://awesome.website.com/file.pdf">Download</a>
|
|
||||||
<a id="file-download-iso" href="https://awesome.website.com/file.iso">Download ISO</a>
|
|
||||||
|
|
||||||
<a id="outbound-link" href="https://example.com">Outbound link</a>
|
|
||||||
|
|
||||||
<a id="manual-pageview-1" onclick="plausible('pageview', { u: '/:test-plausible' })">Manual pageview</a>
|
|
||||||
<a id="manual-pageview-2" onclick="plausible('pageview', { url: '/:test-plausible-2' })">Manual pageview 2</a>
|
|
||||||
|
|
||||||
<a id="custom-event" onclick="plausible('Custom event', { props: { author: 'Karl' } })">Custom event</a>
|
|
||||||
|
|
||||||
<button id="tagged-event" class="plausible-event-name=Purchase plausible-event-foo=bar plausible-revenue-currency=EUR plausible-revenue-amount=13.32">
|
|
||||||
Tagged event
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const params = new URLSearchParams(window.location.search)
|
|
||||||
|
|
||||||
|
|
||||||
window.pageviewProps = { someProp: 123 }
|
|
||||||
|
|
||||||
window.includePlausibleScript = function(src) {
|
|
||||||
// Base snippet (minus script inclusion)
|
|
||||||
window.plausible=window.plausible||function(){(window.plausible.q = window.plausible.q || []).push(arguments)}
|
|
||||||
window.plausible.init = function(overrides) { window.plausible.o = overrides || {} }
|
|
||||||
|
|
||||||
if (params.has('beforeScriptLoaded')) {
|
|
||||||
const beforeScriptLoaded = params.get('beforeScriptLoaded')
|
|
||||||
eval(beforeScriptLoaded)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!params.has('skipPlausibleInit')) {
|
|
||||||
plausible.init()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load the script with the passed config
|
|
||||||
const config = params.get('script_config')
|
|
||||||
const script = document.createElement('script')
|
|
||||||
script.src = `/tracker/js/plausible-web.js?script_config=${encodeURIComponent(config)}`
|
|
||||||
|
|
||||||
var r = document.getElementsByTagName("script")[0]
|
|
||||||
r.parentNode.insertBefore(script, r);
|
|
||||||
}
|
|
||||||
|
|
||||||
includePlausibleScript()
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -0,0 +1,123 @@
|
||||||
|
import { initializePageDynamically } from './support/initialize-page-dynamically'
|
||||||
|
import {
|
||||||
|
e,
|
||||||
|
expectPlausibleInAction,
|
||||||
|
hideAndShowCurrentTab,
|
||||||
|
isEngagementEvent,
|
||||||
|
switchByMode,
|
||||||
|
tracker_script_version
|
||||||
|
} from './support/test-utils'
|
||||||
|
import { test } from '@playwright/test'
|
||||||
|
import { ScriptConfig } from './support/types'
|
||||||
|
import { LOCAL_SERVER_ADDR } from './support/server'
|
||||||
|
|
||||||
|
const DEFAULT_CONFIG: ScriptConfig = {
|
||||||
|
domain: 'example.com',
|
||||||
|
endpoint: `${LOCAL_SERVER_ADDR}/api/event`,
|
||||||
|
captureOnLocalhost: true
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const mode of ['web', 'esm']) {
|
||||||
|
test.describe(`respects hashBasedRouting config option (${mode})`, () => {
|
||||||
|
test('pageviews and engagement events are sent without "h" parameter if hashBasedRouting is not set', async ({
|
||||||
|
page
|
||||||
|
}, { testId }) => {
|
||||||
|
const { url } = await initializePageDynamically(page, {
|
||||||
|
testId,
|
||||||
|
scriptConfig: switchByMode(
|
||||||
|
{
|
||||||
|
web: DEFAULT_CONFIG,
|
||||||
|
esm: `<script type="module">import { init, track } from '/tracker/js/npm_package/plausible.js'; init(${JSON.stringify(
|
||||||
|
DEFAULT_CONFIG
|
||||||
|
)})</script>`
|
||||||
|
},
|
||||||
|
mode
|
||||||
|
),
|
||||||
|
bodyContent: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
await expectPlausibleInAction(page, {
|
||||||
|
action: async () => {
|
||||||
|
await page.goto(url)
|
||||||
|
await hideAndShowCurrentTab(page, { delay: 200 })
|
||||||
|
},
|
||||||
|
expectedRequests: [
|
||||||
|
{
|
||||||
|
n: 'pageview',
|
||||||
|
h: e.toBeUndefined()
|
||||||
|
},
|
||||||
|
{ n: 'engagement', h: e.toBeUndefined() }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('pageviews and engagement events are sent with "h:1" parameter if hashBasedRouting is set to true', async ({
|
||||||
|
page
|
||||||
|
}, { testId }) => {
|
||||||
|
const config = { ...DEFAULT_CONFIG, hashBasedRouting: true }
|
||||||
|
const { url } = await initializePageDynamically(page, {
|
||||||
|
testId,
|
||||||
|
scriptConfig: switchByMode(
|
||||||
|
{
|
||||||
|
web: config,
|
||||||
|
esm: `<script type="module">import { init, track } from '/tracker/js/npm_package/plausible.js'; init(${JSON.stringify(
|
||||||
|
config
|
||||||
|
)})</script>`
|
||||||
|
},
|
||||||
|
mode
|
||||||
|
),
|
||||||
|
bodyContent: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
await expectPlausibleInAction(page, {
|
||||||
|
action: async () => {
|
||||||
|
await page.goto(`${url}#page1`)
|
||||||
|
await hideAndShowCurrentTab(page, { delay: 200 })
|
||||||
|
},
|
||||||
|
expectedRequests: [
|
||||||
|
{
|
||||||
|
n: 'pageview',
|
||||||
|
h: 1,
|
||||||
|
u: `${LOCAL_SERVER_ADDR}${url}#page1`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
n: 'engagement',
|
||||||
|
h: 1,
|
||||||
|
u: `${LOCAL_SERVER_ADDR}${url}#page1`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
test.describe('hash-based routing (legacy)', () => {
|
||||||
|
test('pageviews and engagement events are sent with "h:1" parameter if using the hash extension', async ({
|
||||||
|
page
|
||||||
|
}, { testId }) => {
|
||||||
|
const { url } = await initializePageDynamically(page, {
|
||||||
|
testId,
|
||||||
|
scriptConfig: `<script data-domain="${DEFAULT_CONFIG.domain}" defer src="/tracker/js/plausible.hash.local.js"></script>`,
|
||||||
|
bodyContent: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
await expectPlausibleInAction(page, {
|
||||||
|
action: async () => {
|
||||||
|
await page.goto(`${url}#page1`)
|
||||||
|
await hideAndShowCurrentTab(page, { delay: 200 })
|
||||||
|
},
|
||||||
|
expectedRequests: [
|
||||||
|
{
|
||||||
|
n: 'pageview',
|
||||||
|
h: 1,
|
||||||
|
u: `${LOCAL_SERVER_ADDR}${url}#page1`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
n: 'engagement',
|
||||||
|
h: 1,
|
||||||
|
u: `${LOCAL_SERVER_ADDR}${url}#page1`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
import { initializePageDynamically } from './support/initialize-page-dynamically'
|
||||||
|
import {
|
||||||
|
expectPlausibleInAction,
|
||||||
|
isEngagementEvent,
|
||||||
|
switchByMode,
|
||||||
|
tracker_script_version
|
||||||
|
} from './support/test-utils'
|
||||||
|
import { expect, test } from '@playwright/test'
|
||||||
|
import { ScriptConfig } from './support/types'
|
||||||
|
import { LOCAL_SERVER_ADDR } from './support/server'
|
||||||
|
|
||||||
|
const DEFAULT_CONFIG: ScriptConfig = {
|
||||||
|
domain: 'example.com',
|
||||||
|
endpoint: `${LOCAL_SERVER_ADDR}/api/event`,
|
||||||
|
captureOnLocalhost: false
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const mode of ['web', 'esm']) {
|
||||||
|
test.describe(`respects "logging" v2 config option (${mode})`, () => {
|
||||||
|
test('if logging is not explicitly set, it is treated as true and logs are emitted on ingored events', async ({
|
||||||
|
page
|
||||||
|
}, { testId }) => {
|
||||||
|
const config = { ...DEFAULT_CONFIG }
|
||||||
|
const consoleMessages: [string, string][] = []
|
||||||
|
page.on('console', (message) => {
|
||||||
|
consoleMessages.push([message.type(), message.text()])
|
||||||
|
})
|
||||||
|
const { url } = await initializePageDynamically(page, {
|
||||||
|
testId,
|
||||||
|
scriptConfig: switchByMode(
|
||||||
|
{
|
||||||
|
web: config,
|
||||||
|
esm: `<script type="module">import { init, track } from '/tracker/js/npm_package/plausible.js'; init(${JSON.stringify(
|
||||||
|
config
|
||||||
|
)})</script>`
|
||||||
|
},
|
||||||
|
mode
|
||||||
|
),
|
||||||
|
bodyContent: 'hello world'
|
||||||
|
})
|
||||||
|
|
||||||
|
await expectPlausibleInAction(page, {
|
||||||
|
action: () => page.goto(url),
|
||||||
|
expectedRequests: [],
|
||||||
|
refutedRequests: [{ n: 'pageview' }]
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(consoleMessages).toEqual([
|
||||||
|
['warning', 'Ignoring Event: localhost']
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('if logging is explicitly set to false, logs are not emitted on ingored events', async ({
|
||||||
|
page
|
||||||
|
}, { testId }) => {
|
||||||
|
const config = { ...DEFAULT_CONFIG, logging: false }
|
||||||
|
const consoleMessages: [string, string][] = []
|
||||||
|
page.on('console', (message) => {
|
||||||
|
consoleMessages.push([message.type(), message.text()])
|
||||||
|
})
|
||||||
|
const { url } = await initializePageDynamically(page, {
|
||||||
|
testId,
|
||||||
|
scriptConfig: switchByMode(
|
||||||
|
{
|
||||||
|
web: config,
|
||||||
|
esm: `<script type="module">import { init, track } from '/tracker/js/npm_package/plausible.js'; init(${JSON.stringify(
|
||||||
|
config
|
||||||
|
)})</script>`
|
||||||
|
},
|
||||||
|
mode
|
||||||
|
),
|
||||||
|
bodyContent: 'hello world'
|
||||||
|
})
|
||||||
|
|
||||||
|
await expectPlausibleInAction(page, {
|
||||||
|
action: () => page.goto(url),
|
||||||
|
expectedRequests: [],
|
||||||
|
refutedRequests: [{ n: 'pageview' }]
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(consoleMessages).toEqual([])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -3,7 +3,12 @@ import {
|
||||||
mockManyRequests,
|
mockManyRequests,
|
||||||
resolveWithTimestamps
|
resolveWithTimestamps
|
||||||
} from './support/mock-many-requests'
|
} from './support/mock-many-requests'
|
||||||
import { expectPlausibleInAction, switchByMode } from './support/test-utils'
|
import {
|
||||||
|
expectPlausibleInAction,
|
||||||
|
isEngagementEvent,
|
||||||
|
isPageviewEvent,
|
||||||
|
switchByMode
|
||||||
|
} from './support/test-utils'
|
||||||
import { expect, test } from '@playwright/test'
|
import { expect, test } from '@playwright/test'
|
||||||
import { ScriptConfig } from './support/types'
|
import { ScriptConfig } from './support/types'
|
||||||
import { LOCAL_SERVER_ADDR } from './support/server'
|
import { LOCAL_SERVER_ADDR } from './support/server'
|
||||||
|
|
@ -14,6 +19,104 @@ const DEFAULT_CONFIG: ScriptConfig = {
|
||||||
captureOnLocalhost: true
|
captureOnLocalhost: true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const mode of ['web', 'esm']) {
|
||||||
|
test.describe(`respects "outboundLinks" v2 config option (${mode})`, () => {
|
||||||
|
test('does not track outbound links when "outboundLinks: false"', async ({
|
||||||
|
page
|
||||||
|
}, { testId }) => {
|
||||||
|
const outboundUrl = 'https://other.example.com/target'
|
||||||
|
const outboundMock = await mockManyRequests({
|
||||||
|
page,
|
||||||
|
path: outboundUrl,
|
||||||
|
fulfill: {
|
||||||
|
status: 200,
|
||||||
|
contentType: 'text/html',
|
||||||
|
body: '<!DOCTYPE html><html><head><title>other page</title></head><body>other page</body></html>'
|
||||||
|
},
|
||||||
|
awaitedRequestCount: 1
|
||||||
|
})
|
||||||
|
const config = { ...DEFAULT_CONFIG, outboundLinks: false }
|
||||||
|
const { url } = await initializePageDynamically(page, {
|
||||||
|
testId,
|
||||||
|
scriptConfig: switchByMode(
|
||||||
|
{
|
||||||
|
web: config,
|
||||||
|
esm: `<script type="module">import { init, track } from '/tracker/js/npm_package/plausible.js'; init(${JSON.stringify(
|
||||||
|
config
|
||||||
|
)})</script>`
|
||||||
|
},
|
||||||
|
mode
|
||||||
|
),
|
||||||
|
bodyContent: `<a href="${outboundUrl}">➡️</a>`
|
||||||
|
})
|
||||||
|
|
||||||
|
await expectPlausibleInAction(page, {
|
||||||
|
action: async () => {
|
||||||
|
await page.goto(url)
|
||||||
|
await page.click('a')
|
||||||
|
},
|
||||||
|
expectedRequests: [
|
||||||
|
{
|
||||||
|
n: 'pageview',
|
||||||
|
d: DEFAULT_CONFIG.domain,
|
||||||
|
u: `${LOCAL_SERVER_ADDR}${url}`
|
||||||
|
}
|
||||||
|
],
|
||||||
|
refutedRequests: [{ n: 'Outbound Link: Click' }],
|
||||||
|
shouldIgnoreRequest: isEngagementEvent
|
||||||
|
})
|
||||||
|
await expect(outboundMock.getRequestList()).resolves.toHaveLength(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('tracks outbound links when "outboundLinks: true"', async ({ page }, {
|
||||||
|
testId
|
||||||
|
}) => {
|
||||||
|
const outboundUrl = 'https://other.example.com/target'
|
||||||
|
const outboundMock = await mockManyRequests({
|
||||||
|
page,
|
||||||
|
path: outboundUrl,
|
||||||
|
fulfill: {
|
||||||
|
status: 200,
|
||||||
|
contentType: 'text/html',
|
||||||
|
body: '<!DOCTYPE html><html><head><title>other page</title></head><body>other page</body></html>'
|
||||||
|
},
|
||||||
|
awaitedRequestCount: 1
|
||||||
|
})
|
||||||
|
const config = { ...DEFAULT_CONFIG, outboundLinks: true }
|
||||||
|
const { url } = await initializePageDynamically(page, {
|
||||||
|
testId,
|
||||||
|
scriptConfig: switchByMode(
|
||||||
|
{
|
||||||
|
web: config,
|
||||||
|
esm: `<script type="module">import { init, track } from '/tracker/js/npm_package/plausible.js'; init(${JSON.stringify(
|
||||||
|
config
|
||||||
|
)})</script>`
|
||||||
|
},
|
||||||
|
mode
|
||||||
|
),
|
||||||
|
bodyContent: `<a href="${outboundUrl}">➡️</a>`
|
||||||
|
})
|
||||||
|
|
||||||
|
await expectPlausibleInAction(page, {
|
||||||
|
action: async () => {
|
||||||
|
await page.goto(url)
|
||||||
|
await page.click('a')
|
||||||
|
},
|
||||||
|
expectedRequests: [
|
||||||
|
{
|
||||||
|
n: 'pageview',
|
||||||
|
d: DEFAULT_CONFIG.domain,
|
||||||
|
u: `${LOCAL_SERVER_ADDR}${url}`
|
||||||
|
},
|
||||||
|
{ n: 'Outbound Link: Click', p: { url: outboundUrl } }
|
||||||
|
],
|
||||||
|
shouldIgnoreRequest: isEngagementEvent
|
||||||
|
})
|
||||||
|
await expect(outboundMock.getRequestList()).resolves.toHaveLength(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
for (const mode of ['legacy', 'web'])
|
for (const mode of ['legacy', 'web'])
|
||||||
test.describe(`outbound links feature legacy/v2 parity (${mode})`, () => {
|
test.describe(`outbound links feature legacy/v2 parity (${mode})`, () => {
|
||||||
for (const { clickName, click, skip } of [
|
for (const { clickName, click, skip } of [
|
||||||
|
|
@ -139,6 +242,55 @@ for (const mode of ['legacy', 'web'])
|
||||||
})
|
})
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('limitation: does not track outbound links within svg elements', async ({
|
||||||
|
page
|
||||||
|
}, { testId }) => {
|
||||||
|
const outboundUrl = 'https://other.example.com/target'
|
||||||
|
const outboundMock = await mockManyRequests({
|
||||||
|
page,
|
||||||
|
path: outboundUrl,
|
||||||
|
fulfill: {
|
||||||
|
status: 200,
|
||||||
|
contentType: 'text/html',
|
||||||
|
body: '<!DOCTYPE html><html><head><title>other page</title></head><body>other page</body></html>'
|
||||||
|
},
|
||||||
|
awaitedRequestCount: 1
|
||||||
|
})
|
||||||
|
|
||||||
|
const { url } = await initializePageDynamically(page, {
|
||||||
|
testId,
|
||||||
|
scriptConfig: switchByMode(
|
||||||
|
{
|
||||||
|
web: { ...DEFAULT_CONFIG, outboundLinks: true },
|
||||||
|
legacy:
|
||||||
|
'<script defer src="/tracker/js/plausible.local.outbound-links.js"></script>'
|
||||||
|
},
|
||||||
|
mode
|
||||||
|
),
|
||||||
|
bodyContent: `
|
||||||
|
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<a href="${outboundUrl}">
|
||||||
|
<circle cx="50" cy="50" r="50" />
|
||||||
|
</a>
|
||||||
|
</svg>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
|
||||||
|
const pageErrors: Error[] = []
|
||||||
|
page.on('pageerror', (err) => pageErrors.push(err))
|
||||||
|
|
||||||
|
await page.goto(url)
|
||||||
|
|
||||||
|
await expectPlausibleInAction(page, {
|
||||||
|
action: () => page.click('a'),
|
||||||
|
refutedRequests: [{ n: 'Outbound Link: Click' }],
|
||||||
|
shouldIgnoreRequest: [isPageviewEvent, isEngagementEvent]
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(pageErrors).toHaveLength(0)
|
||||||
|
await expect(outboundMock.getRequestList()).resolves.toHaveLength(1)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test.describe('outbound links feature when using legacy .compat extension', () => {
|
test.describe('outbound links feature when using legacy .compat extension', () => {
|
||||||
|
|
@ -313,4 +465,58 @@ test.describe('outbound links feature when using legacy .compat extension', () =
|
||||||
await expect(page.getByText('other page')).toBeVisible()
|
await expect(page.getByText('other page')).toBeVisible()
|
||||||
await expect(outboundMock.getRequestList()).resolves.toHaveLength(1)
|
await expect(outboundMock.getRequestList()).resolves.toHaveLength(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test(`limitation: does not track outbound links within svg elements, but follows link properly`, async ({
|
||||||
|
page
|
||||||
|
}, { testId }) => {
|
||||||
|
const outboundUrl = 'https://other.example.com/target'
|
||||||
|
const outboundMockOptions = {
|
||||||
|
page,
|
||||||
|
path: outboundUrl,
|
||||||
|
fulfill: {
|
||||||
|
status: 200,
|
||||||
|
contentType: 'text/html',
|
||||||
|
body: '<!DOCTYPE html><html><head><title>other page</title></head><body>other page</body></html>'
|
||||||
|
},
|
||||||
|
awaitedRequestCount: 2,
|
||||||
|
mockRequestTimeout: 2000
|
||||||
|
}
|
||||||
|
|
||||||
|
const outboundMockForOtherPages = await mockManyRequests({
|
||||||
|
...outboundMockOptions,
|
||||||
|
scopeMockToPage: false
|
||||||
|
})
|
||||||
|
const outboundMockForSamePage = await mockManyRequests({
|
||||||
|
...outboundMockOptions,
|
||||||
|
scopeMockToPage: true
|
||||||
|
})
|
||||||
|
|
||||||
|
const { url } = await initializePageDynamically(page, {
|
||||||
|
testId,
|
||||||
|
scriptConfig:
|
||||||
|
'<script id="plausible" defer src="/tracker/js/plausible.compat.local.manual.outbound-links.js"></script>',
|
||||||
|
bodyContent: `
|
||||||
|
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<a href="${outboundUrl}">
|
||||||
|
<circle cx="50" cy="50" r="50" />
|
||||||
|
</a>
|
||||||
|
</svg>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
await page.goto(url)
|
||||||
|
|
||||||
|
await expectPlausibleInAction(page, {
|
||||||
|
action: () => page.click('a'),
|
||||||
|
refutedRequests: [{ n: 'Outbound Link: Click' }]
|
||||||
|
})
|
||||||
|
|
||||||
|
const [requestsOnOtherPages, requestsOnSamePage] = await Promise.all([
|
||||||
|
outboundMockForOtherPages.getRequestList().then((d) => d.length),
|
||||||
|
outboundMockForSamePage.getRequestList().then((d) => d.length)
|
||||||
|
])
|
||||||
|
expect({ requestsOnOtherPages, requestsOnSamePage }).toEqual({
|
||||||
|
requestsOnOtherPages: 0,
|
||||||
|
requestsOnSamePage: 1
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import { initializePageDynamically } from './support/initialize-page-dynamically'
|
import { initializePageDynamically } from './support/initialize-page-dynamically'
|
||||||
import {
|
import {
|
||||||
|
e,
|
||||||
expectPlausibleInAction,
|
expectPlausibleInAction,
|
||||||
|
hideAndShowCurrentTab,
|
||||||
isEngagementEvent,
|
isEngagementEvent,
|
||||||
switchByMode,
|
switchByMode,
|
||||||
tracker_script_version
|
tracker_script_version
|
||||||
|
|
@ -15,6 +17,93 @@ const DEFAULT_CONFIG: ScriptConfig = {
|
||||||
captureOnLocalhost: true
|
captureOnLocalhost: true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const mode of ['web', 'esm']) {
|
||||||
|
test.describe(`"autoCapturePageviews" v2 config option (${mode})`, () => {
|
||||||
|
test('if autoCapturePageviews is not explicitly set, it is treated as true and a pageview is sent on navigating to page, engagement tracking is triggered', async ({
|
||||||
|
page
|
||||||
|
}, { testId }) => {
|
||||||
|
const config = { ...DEFAULT_CONFIG }
|
||||||
|
const { url } = await initializePageDynamically(page, {
|
||||||
|
testId,
|
||||||
|
scriptConfig: switchByMode(
|
||||||
|
{
|
||||||
|
web: config,
|
||||||
|
esm: `<script type="module">import { init, track } from '/tracker/js/npm_package/plausible.js'; init(${JSON.stringify(
|
||||||
|
config
|
||||||
|
)})</script>`
|
||||||
|
},
|
||||||
|
mode
|
||||||
|
),
|
||||||
|
bodyContent: 'hello world'
|
||||||
|
})
|
||||||
|
|
||||||
|
await expectPlausibleInAction(page, {
|
||||||
|
action: async () => {
|
||||||
|
await page.goto(url)
|
||||||
|
await hideAndShowCurrentTab(page, { delay: 200 })
|
||||||
|
},
|
||||||
|
expectedRequests: [
|
||||||
|
{
|
||||||
|
n: 'pageview',
|
||||||
|
d: config.domain,
|
||||||
|
u: `${LOCAL_SERVER_ADDR}${url}`,
|
||||||
|
v: tracker_script_version,
|
||||||
|
p: e.toBeUndefined()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
n: 'engagement',
|
||||||
|
d: config.domain,
|
||||||
|
u: `${LOCAL_SERVER_ADDR}${url}`,
|
||||||
|
v: tracker_script_version,
|
||||||
|
p: e.toBeUndefined()
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('if autoCapturePageviews is explicitly set to false, a pageview is not sent on navigating to page, but sending pageviews manually works and it starts engagement tracking logic', async ({
|
||||||
|
page
|
||||||
|
}, { testId }) => {
|
||||||
|
const config = { ...DEFAULT_CONFIG, autoCapturePageviews: false }
|
||||||
|
const { url } = await initializePageDynamically(page, {
|
||||||
|
testId,
|
||||||
|
scriptConfig: switchByMode(
|
||||||
|
{
|
||||||
|
web: config,
|
||||||
|
esm: `<script type="module">import { init, track } from '/tracker/js/npm_package/plausible.js'; init(${JSON.stringify(
|
||||||
|
config
|
||||||
|
)});</script>`
|
||||||
|
},
|
||||||
|
mode
|
||||||
|
),
|
||||||
|
bodyContent: `
|
||||||
|
<a id="alfa" onclick="window.plausible('pageview', { u: '/:masked/alfa' })" href="#">A</a>
|
||||||
|
<a id="beta" onclick="window.plausible('pageview', { url: '/:masked/beta' })" href="#">B</a>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
|
||||||
|
await expectPlausibleInAction(page, {
|
||||||
|
action: () => page.goto(url),
|
||||||
|
refutedRequests: [{ n: 'pageview' }, { n: 'engagement' }]
|
||||||
|
})
|
||||||
|
|
||||||
|
await expectPlausibleInAction(page, {
|
||||||
|
action: async () => {
|
||||||
|
await page.click('#alfa')
|
||||||
|
await page.click('#beta')
|
||||||
|
await hideAndShowCurrentTab(page, { delay: 200 })
|
||||||
|
},
|
||||||
|
expectedRequests: [
|
||||||
|
{ n: 'pageview', u: '/:masked/alfa', d: config.domain },
|
||||||
|
{ n: 'engagement', u: '/:masked/alfa', d: config.domain },
|
||||||
|
{ n: 'pageview', u: '/:masked/beta', d: config.domain },
|
||||||
|
{ n: 'engagement', u: '/:masked/beta', d: config.domain }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
for (const mode of ['legacy', 'web', 'esm']) {
|
for (const mode of ['legacy', 'web', 'esm']) {
|
||||||
test.describe(`pageviews parity legacy/v2 (${mode})`, () => {
|
test.describe(`pageviews parity legacy/v2 (${mode})`, () => {
|
||||||
test('sends pageview on navigating to page', async ({ page }, {
|
test('sends pageview on navigating to page', async ({ page }, {
|
||||||
|
|
@ -28,8 +117,7 @@ for (const mode of ['legacy', 'web', 'esm']) {
|
||||||
esm: `<script type="module">import { init, track } from '/tracker/js/npm_package/plausible.js'; init(${JSON.stringify(
|
esm: `<script type="module">import { init, track } from '/tracker/js/npm_package/plausible.js'; init(${JSON.stringify(
|
||||||
DEFAULT_CONFIG
|
DEFAULT_CONFIG
|
||||||
)})</script>`,
|
)})</script>`,
|
||||||
legacy:
|
legacy: `<script data-domain="${DEFAULT_CONFIG.domain}" defer src="/tracker/js/plausible.local.js"></script>`
|
||||||
'<script defer src="/tracker/js/plausible.local.js"></script>'
|
|
||||||
},
|
},
|
||||||
mode
|
mode
|
||||||
),
|
),
|
||||||
|
|
@ -38,7 +126,15 @@ for (const mode of ['legacy', 'web', 'esm']) {
|
||||||
|
|
||||||
await expectPlausibleInAction(page, {
|
await expectPlausibleInAction(page, {
|
||||||
action: () => page.goto(url),
|
action: () => page.goto(url),
|
||||||
expectedRequests: [{ n: 'pageview', v: tracker_script_version }],
|
expectedRequests: [
|
||||||
|
{
|
||||||
|
n: 'pageview',
|
||||||
|
d: DEFAULT_CONFIG.domain,
|
||||||
|
u: `${LOCAL_SERVER_ADDR}${url}`,
|
||||||
|
v: tracker_script_version,
|
||||||
|
h: e.toBeUndefined(),
|
||||||
|
}
|
||||||
|
],
|
||||||
shouldIgnoreRequest: [isEngagementEvent]
|
shouldIgnoreRequest: [isEngagementEvent]
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,179 @@
|
||||||
|
import { test, expect } from '@playwright/test'
|
||||||
|
import { LOCAL_SERVER_ADDR } from './support/server'
|
||||||
|
import {
|
||||||
|
isEngagementEvent,
|
||||||
|
expectPlausibleInAction,
|
||||||
|
tracker_script_version,
|
||||||
|
hideAndShowCurrentTab
|
||||||
|
} from './support/test-utils'
|
||||||
|
import { initializePageDynamically } from './support/initialize-page-dynamically'
|
||||||
|
|
||||||
|
const DEFAULT_CONFIG = {
|
||||||
|
domain: 'example.com',
|
||||||
|
endpoint: `${LOCAL_SERVER_ADDR}/api/event`,
|
||||||
|
captureOnLocalhost: true
|
||||||
|
}
|
||||||
|
|
||||||
|
test('if `init` is called without domain, it throws', async ({ page }, {
|
||||||
|
testId
|
||||||
|
}) => {
|
||||||
|
const { url } = await initializePageDynamically(page, {
|
||||||
|
testId,
|
||||||
|
scriptConfig: `<script type="module">import { init, track } from "/tracker/js/npm_package/plausible.js"; window.init = init; window.track = track;</script>`,
|
||||||
|
bodyContent: 'body'
|
||||||
|
})
|
||||||
|
await page.goto(url)
|
||||||
|
const config = { ...DEFAULT_CONFIG, domain: undefined }
|
||||||
|
await expect(
|
||||||
|
page.evaluate((config) => (window as any).init(config), { config })
|
||||||
|
).rejects.toThrow('plausible.init(): domain argument is required')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('if `init` is called with no configuration, it throws', async ({ page }, {
|
||||||
|
testId
|
||||||
|
}) => {
|
||||||
|
const { url } = await initializePageDynamically(page, {
|
||||||
|
testId,
|
||||||
|
scriptConfig: `<script type="module">import { init, track } from "/tracker/js/npm_package/plausible.js"; window.init = init; window.track = track;</script>`,
|
||||||
|
bodyContent: 'body'
|
||||||
|
})
|
||||||
|
await page.goto(url)
|
||||||
|
await expect(page.evaluate(() => (window as any).init())).rejects.toThrow(
|
||||||
|
'plausible.init(): domain argument is required'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('if `track` is called before `init`, it throws', async ({ page }, {
|
||||||
|
testId
|
||||||
|
}) => {
|
||||||
|
const { url } = await initializePageDynamically(page, {
|
||||||
|
testId,
|
||||||
|
scriptConfig: `<script type="module">import { init, track } from "/tracker/js/npm_package/plausible.js"; window.init = init; window.track = track;</script>`,
|
||||||
|
bodyContent: 'body'
|
||||||
|
})
|
||||||
|
await page.goto(url)
|
||||||
|
await expect(
|
||||||
|
page.evaluate(() => (window as any).track('purchase'))
|
||||||
|
).rejects.toThrow(
|
||||||
|
'plausible.track() can only be called after plausible.init()'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('if `init` is called twice, it throws, but tracking still works', async ({
|
||||||
|
page
|
||||||
|
}, { testId }) => {
|
||||||
|
const config = { ...DEFAULT_CONFIG }
|
||||||
|
const { url } = await initializePageDynamically(page, {
|
||||||
|
testId,
|
||||||
|
scriptConfig: `<script type="module">import { init, track } from "/tracker/js/npm_package/plausible.js"; window.init = init; init(${JSON.stringify(
|
||||||
|
config
|
||||||
|
)})</script>`,
|
||||||
|
bodyContent: 'body'
|
||||||
|
})
|
||||||
|
|
||||||
|
await expectPlausibleInAction(page, {
|
||||||
|
action: async () => {
|
||||||
|
await page.goto(url)
|
||||||
|
},
|
||||||
|
expectedRequests: [{ n: 'pageview' }],
|
||||||
|
shouldIgnoreRequest: isEngagementEvent
|
||||||
|
})
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.evaluate((config) => (window as any).init(config), config)
|
||||||
|
).rejects.toThrow('plausible.init() can only be called once')
|
||||||
|
|
||||||
|
await expectPlausibleInAction(page, {
|
||||||
|
action: async () => {
|
||||||
|
await hideAndShowCurrentTab(page, { delay: 200 })
|
||||||
|
},
|
||||||
|
expectedRequests: [{ n: 'engagement' }]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('`bindToWindow` is true by default, and plausible is attached to window', async ({
|
||||||
|
page
|
||||||
|
}, { testId }) => {
|
||||||
|
const config = { ...DEFAULT_CONFIG }
|
||||||
|
const { url } = await initializePageDynamically(page, {
|
||||||
|
testId,
|
||||||
|
scriptConfig: `<script type="module">import { init, track } from "/tracker/js/npm_package/plausible.js"; init(${JSON.stringify(
|
||||||
|
config
|
||||||
|
)})</script>`,
|
||||||
|
bodyContent: 'body'
|
||||||
|
})
|
||||||
|
|
||||||
|
await expectPlausibleInAction(page, {
|
||||||
|
action: async () => {
|
||||||
|
await page.goto(url)
|
||||||
|
},
|
||||||
|
expectedRequests: [{ n: 'pageview' }]
|
||||||
|
})
|
||||||
|
await expect(
|
||||||
|
page.waitForFunction(() => (window as any).plausible?.l !== undefined)
|
||||||
|
).resolves.toBeTruthy()
|
||||||
|
await expect(
|
||||||
|
page.evaluate(() => {
|
||||||
|
if ((window as any).plausible?.l) {
|
||||||
|
return {
|
||||||
|
l: (window as any).plausible.l,
|
||||||
|
v: (window as any).plausible.v,
|
||||||
|
s: (window as any).plausible.s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
).resolves.toEqual({ l: true, v: tracker_script_version, s: 'npm' })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('if `bindToWindow` is false, `plausible` is not attached to window', async ({
|
||||||
|
page
|
||||||
|
}, { testId }) => {
|
||||||
|
const config = { ...DEFAULT_CONFIG, bindToWindow: false }
|
||||||
|
const { url } = await initializePageDynamically(page, {
|
||||||
|
testId,
|
||||||
|
scriptConfig: `<script type="module">import { init, track } from "/tracker/js/npm_package/plausible.js"; init(${JSON.stringify(
|
||||||
|
config
|
||||||
|
)})</script>`,
|
||||||
|
bodyContent: 'body'
|
||||||
|
})
|
||||||
|
|
||||||
|
await expectPlausibleInAction(page, {
|
||||||
|
action: async () => {
|
||||||
|
await page.goto(url)
|
||||||
|
},
|
||||||
|
expectedRequests: [{ n: 'pageview' }],
|
||||||
|
shouldIgnoreRequest: isEngagementEvent
|
||||||
|
})
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.waitForFunction(
|
||||||
|
() => (window as any).plausible !== undefined,
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
timeout: 1000
|
||||||
|
}
|
||||||
|
)
|
||||||
|
).rejects.toThrow('page.waitForFunction: Timeout 1000ms exceeded.')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('allows overriding `endpoint` with a custom URL via `init`', async ({
|
||||||
|
page
|
||||||
|
}, { testId }) => {
|
||||||
|
const config = { ...DEFAULT_CONFIG, endpoint: 'http://example.com/event' }
|
||||||
|
const { url } = await initializePageDynamically(page, {
|
||||||
|
testId,
|
||||||
|
scriptConfig: `<script type="module">import { init, track } from "/tracker/js/npm_package/plausible.js"; init(${JSON.stringify(
|
||||||
|
config
|
||||||
|
)})</script>`,
|
||||||
|
bodyContent: 'body'
|
||||||
|
})
|
||||||
|
await expectPlausibleInAction(page, {
|
||||||
|
pathToMock: config.endpoint,
|
||||||
|
action: () => page.goto(url),
|
||||||
|
expectedRequests: [
|
||||||
|
{ n: 'pageview', d: config.domain, u: `${LOCAL_SERVER_ADDR}${url}` }
|
||||||
|
],
|
||||||
|
shouldIgnoreRequest: isEngagementEvent
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -1,77 +0,0 @@
|
||||||
/*
|
|
||||||
Tests for plausible-npm.js variant
|
|
||||||
|
|
||||||
Config is set at init(), as we expect consumers to do in production.
|
|
||||||
*/
|
|
||||||
import { test, expect } from '@playwright/test'
|
|
||||||
import { LOCAL_SERVER_ADDR } from './support/server'
|
|
||||||
import { testPlausibleConfiguration, callInit } from './shared-configuration-tests'
|
|
||||||
|
|
||||||
const DEFAULT_CONFIG = {
|
|
||||||
domain: 'example.com',
|
|
||||||
endpoint: `${LOCAL_SERVER_ADDR}/api/event`,
|
|
||||||
captureOnLocalhost: true
|
|
||||||
}
|
|
||||||
|
|
||||||
async function openPage(page, config, options = {}) {
|
|
||||||
await page.goto("/plausible-npm.html")
|
|
||||||
await page.waitForFunction('window.init !== undefined')
|
|
||||||
|
|
||||||
if (!options.skipPlausibleInit) {
|
|
||||||
await callInit(page, { ...DEFAULT_CONFIG, ...config }, 'window')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
test.describe('NPM package', () => {
|
|
||||||
testPlausibleConfiguration({
|
|
||||||
openPage,
|
|
||||||
initPlausible: (page, config) => callInit(page, { ...DEFAULT_CONFIG, ...config }, 'window'),
|
|
||||||
fixtureName: 'plausible-npm.html',
|
|
||||||
fixtureTitle: 'Plausible NPM package tests'
|
|
||||||
})
|
|
||||||
|
|
||||||
test('does not support calling `init` without `domain`', async ({ page }) => {
|
|
||||||
await openPage(page, {}, { skipPlausibleInit: true })
|
|
||||||
await expect(async () => {
|
|
||||||
await callInit(page, { hashBasedRouting: true }, 'window')
|
|
||||||
}).rejects.toThrow("plausible.init(): domain argument is required")
|
|
||||||
})
|
|
||||||
|
|
||||||
test('does not support calling `init` with no configuration', async ({ page }) => {
|
|
||||||
await openPage(page, {}, { skipPlausibleInit: true })
|
|
||||||
await expect(async () => {
|
|
||||||
await callInit(page, undefined, 'window')
|
|
||||||
}).rejects.toThrow("plausible.init(): domain argument is required")
|
|
||||||
})
|
|
||||||
|
|
||||||
test('track throws if called before init', async ({ page }) => {
|
|
||||||
await openPage(page, {}, { skipPlausibleInit: true })
|
|
||||||
await expect(async () => {
|
|
||||||
await page.evaluate(() => {
|
|
||||||
window.track('pageview')
|
|
||||||
})
|
|
||||||
}).rejects.toThrow("plausible.track() can only be called after plausible.init()")
|
|
||||||
})
|
|
||||||
|
|
||||||
test('does not support calling `init` twice', async ({ page }) => {
|
|
||||||
await openPage(page, {}, { skipPlausibleInit: true })
|
|
||||||
await callInit(page, DEFAULT_CONFIG, 'window')
|
|
||||||
await expect(async () => {
|
|
||||||
await callInit(page, DEFAULT_CONFIG, 'window')
|
|
||||||
}).rejects.toThrow("plausible.init() can only be called once")
|
|
||||||
})
|
|
||||||
|
|
||||||
test('binds to window by default', async ({ page }) => {
|
|
||||||
await openPage(page, {}, { skipPlausibleInit: true })
|
|
||||||
await callInit(page, DEFAULT_CONFIG, 'window')
|
|
||||||
await page.waitForFunction('window.plausible !== undefined')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('does not bind to window if bindToWindow is false', async ({ page }) => {
|
|
||||||
await openPage(page, {}, { skipPlausibleInit: true })
|
|
||||||
await callInit(page, { ...DEFAULT_CONFIG, bindToWindow: false }, 'window')
|
|
||||||
await expect(
|
|
||||||
page.waitForFunction(() => window.plausible !== undefined, undefined, { timeout: 1000 }),
|
|
||||||
).rejects.toThrow('page.waitForFunction: Timeout 1000ms exceeded.')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
@ -0,0 +1,154 @@
|
||||||
|
/*
|
||||||
|
Tests for plausible-web.js script variant
|
||||||
|
|
||||||
|
Unlike in production, we're manually interpolating the script config in this file to
|
||||||
|
better test the script in isolation of the plausible codebase.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
expectPlausibleInAction,
|
||||||
|
isEngagementEvent
|
||||||
|
} from './support/test-utils'
|
||||||
|
import { test, expect } from '@playwright/test'
|
||||||
|
import { LOCAL_SERVER_ADDR } from './support/server'
|
||||||
|
import {
|
||||||
|
getConfiguredPlausibleWebSnippet,
|
||||||
|
initializePageDynamically
|
||||||
|
} from './support/initialize-page-dynamically'
|
||||||
|
|
||||||
|
const DEFAULT_CONFIG = {
|
||||||
|
domain: 'example.com',
|
||||||
|
endpoint: `${LOCAL_SERVER_ADDR}/api/event`,
|
||||||
|
captureOnLocalhost: true
|
||||||
|
}
|
||||||
|
|
||||||
|
test('with queue code from the web snippet, tracks `plausible` calls made before the script is loaded', async ({
|
||||||
|
page
|
||||||
|
}, { testId }) => {
|
||||||
|
const config = { ...DEFAULT_CONFIG }
|
||||||
|
const { url } = await initializePageDynamically(page, {
|
||||||
|
testId,
|
||||||
|
scriptConfig: config,
|
||||||
|
bodyContent:
|
||||||
|
'<script>window.plausible("loaded", { props: { plausibleLoadedAtEventTime: window.plausible.l ? true : false }, interactive: false })</script>'
|
||||||
|
})
|
||||||
|
await expectPlausibleInAction(page, {
|
||||||
|
action: () => page.goto(url),
|
||||||
|
expectedRequests: [
|
||||||
|
{
|
||||||
|
n: 'loaded',
|
||||||
|
p: { plausibleLoadedAtEventTime: false },
|
||||||
|
i: false,
|
||||||
|
d: config.domain,
|
||||||
|
u: `${LOCAL_SERVER_ADDR}${url}`
|
||||||
|
},
|
||||||
|
{ n: 'pageview', d: config.domain, u: `${LOCAL_SERVER_ADDR}${url}` }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('handles double-initialization of the script with a console.warn', async ({
|
||||||
|
page
|
||||||
|
}, { testId }) => {
|
||||||
|
const config = { ...DEFAULT_CONFIG, customProperties: { init: 1 } }
|
||||||
|
const { url } = await initializePageDynamically(page, {
|
||||||
|
testId,
|
||||||
|
scriptConfig: config,
|
||||||
|
bodyContent: `<button onclick="window.plausible('Purchase')">Purchase</button>`
|
||||||
|
})
|
||||||
|
const messages: [string, string][] = []
|
||||||
|
page.on('console', (message) => {
|
||||||
|
messages.push([message.type(), message.text()])
|
||||||
|
})
|
||||||
|
|
||||||
|
await expectPlausibleInAction(page, {
|
||||||
|
action: () => page.goto(url),
|
||||||
|
expectedRequests: [{ n: 'pageview', p: { init: 1 } }],
|
||||||
|
shouldIgnoreRequest: isEngagementEvent
|
||||||
|
})
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.evaluate(() =>
|
||||||
|
(window as any).plausible.init({
|
||||||
|
captureOnLocalhost: true,
|
||||||
|
customProperties: { init: 2 }
|
||||||
|
})
|
||||||
|
)
|
||||||
|
).resolves.toBeUndefined()
|
||||||
|
|
||||||
|
expect(messages).toEqual([
|
||||||
|
[
|
||||||
|
'warning',
|
||||||
|
'Plausible analytics script was already initialized, skipping init'
|
||||||
|
]
|
||||||
|
])
|
||||||
|
|
||||||
|
await expectPlausibleInAction(page, {
|
||||||
|
action: () => page.click('button'),
|
||||||
|
expectedRequests: [{ n: 'Purchase', p: { init: 2 } }] // bug or maybe feature: expected to be { init: 1 }
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('if there are two snippets on the page, the second one that loads interacts with the first one, no warning is emitted', async ({ page }, {
|
||||||
|
testId
|
||||||
|
}) => {
|
||||||
|
const config = { ...DEFAULT_CONFIG }
|
||||||
|
const snippetAlfa = getConfiguredPlausibleWebSnippet({...config, customProperties: { alfa: true }})
|
||||||
|
const initCallAlfa = 'plausible.init({"captureOnLocalhost":true,"customProperties":{"alfa":true}})'
|
||||||
|
expect(snippetAlfa).toEqual(expect.stringContaining(initCallAlfa))
|
||||||
|
const snippetBeta = getConfiguredPlausibleWebSnippet({...config, customProperties: { beta: true }})
|
||||||
|
const initCallBeta = `plausible.init({"captureOnLocalhost":true,"customProperties":{"beta":true}})`
|
||||||
|
expect(snippetBeta).toEqual(expect.stringContaining(initCallBeta))
|
||||||
|
|
||||||
|
const messages: [string, string][] = []
|
||||||
|
page.on('console', (message) => {
|
||||||
|
messages.push([message.type(), message.text()])
|
||||||
|
})
|
||||||
|
|
||||||
|
const { url } = await initializePageDynamically(page, {
|
||||||
|
testId,
|
||||||
|
scriptConfig: `${snippetAlfa}${snippetBeta}`,
|
||||||
|
bodyContent: ''
|
||||||
|
})
|
||||||
|
await expectPlausibleInAction(page, {
|
||||||
|
action: () => page.goto(url),
|
||||||
|
expectedRequests: [
|
||||||
|
{ n: 'pageview', d: config.domain, u: `${LOCAL_SERVER_ADDR}${url}`, p: { beta: true } },
|
||||||
|
],
|
||||||
|
shouldIgnoreRequest: isEngagementEvent
|
||||||
|
})
|
||||||
|
expect(messages).toEqual([])
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
test('if domain is provided in `init`, it is ignored', async ({ page }, {
|
||||||
|
testId
|
||||||
|
}) => {
|
||||||
|
const config = { ...DEFAULT_CONFIG }
|
||||||
|
const scriptConfig = getConfiguredPlausibleWebSnippet(config)
|
||||||
|
const originalInitCall = 'plausible.init({"captureOnLocalhost":true})'
|
||||||
|
// verify that the original snippet is what we expect it to be
|
||||||
|
expect(scriptConfig).toEqual(expect.stringContaining(originalInitCall))
|
||||||
|
const initCallWithDomainOverride = `plausible.init({"captureOnLocalhost":true,"domain":"sub.${config.domain}"})`
|
||||||
|
const updatedScriptConfig = scriptConfig.replace(
|
||||||
|
originalInitCall,
|
||||||
|
initCallWithDomainOverride
|
||||||
|
)
|
||||||
|
// verify that the updated snippet has the domain override
|
||||||
|
expect(updatedScriptConfig).toEqual(
|
||||||
|
expect.stringContaining(initCallWithDomainOverride)
|
||||||
|
)
|
||||||
|
|
||||||
|
const { url } = await initializePageDynamically(page, {
|
||||||
|
testId,
|
||||||
|
scriptConfig: updatedScriptConfig,
|
||||||
|
bodyContent: ''
|
||||||
|
})
|
||||||
|
await expectPlausibleInAction(page, {
|
||||||
|
action: () => page.goto(url),
|
||||||
|
expectedRequests: [
|
||||||
|
{ n: 'pageview', d: config.domain, u: `${LOCAL_SERVER_ADDR}${url}` }
|
||||||
|
],
|
||||||
|
shouldIgnoreRequest: isEngagementEvent
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -1,97 +0,0 @@
|
||||||
/*
|
|
||||||
Tests for plausible-web.js script variant
|
|
||||||
|
|
||||||
Unlike in production, we're manually interpolating the script config in this file to
|
|
||||||
better test the script in isolation of the plausible codebase.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {
|
|
||||||
expectPlausibleInAction,
|
|
||||||
e as expecting
|
|
||||||
} from './support/test-utils'
|
|
||||||
import { test, expect } from '@playwright/test'
|
|
||||||
import { LOCAL_SERVER_ADDR } from './support/server'
|
|
||||||
import { testPlausibleConfiguration, callInit } from './shared-configuration-tests'
|
|
||||||
|
|
||||||
const DEFAULT_CONFIG = {
|
|
||||||
domain: 'example.com',
|
|
||||||
endpoint: `${LOCAL_SERVER_ADDR}/api/event`,
|
|
||||||
captureOnLocalhost: true
|
|
||||||
}
|
|
||||||
|
|
||||||
async function openPage(page, config, options = {}) {
|
|
||||||
const configJson = JSON.stringify({ ...DEFAULT_CONFIG, ...config })
|
|
||||||
let path = `/plausible-web.html?script_config=${configJson}`
|
|
||||||
if (options.beforeScriptLoaded) {
|
|
||||||
path += `&beforeScriptLoaded=${options.beforeScriptLoaded}`
|
|
||||||
}
|
|
||||||
if (options.skipPlausibleInit) {
|
|
||||||
path += `&skipPlausibleInit=1`
|
|
||||||
}
|
|
||||||
await page.goto(path)
|
|
||||||
await page.waitForFunction('window.plausible !== undefined')
|
|
||||||
}
|
|
||||||
|
|
||||||
test.describe('plausible-web.js', () => {
|
|
||||||
testPlausibleConfiguration({
|
|
||||||
openPage,
|
|
||||||
initPlausible: (page, config) => callInit(page, config, 'window.plausible'),
|
|
||||||
fixtureName: 'plausible-web.html',
|
|
||||||
fixtureTitle: 'plausible-web.js tests'
|
|
||||||
})
|
|
||||||
|
|
||||||
test('with queue code included, respects `plausible` calls made before the script is loaded', async ({ page }) => {
|
|
||||||
await expectPlausibleInAction(page, {
|
|
||||||
action: () => openPage(page, {}, { beforeScriptLoaded: 'window.plausible("custom-event", { props: { foo: "bar" }, interactive: false })' }),
|
|
||||||
expectedRequests: [{ n: 'custom-event', p: { foo: 'bar' }, i: false }, { n: 'pageview' }]
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test('handles double-initialization of the script with a console.warn', async ({ page }) => {
|
|
||||||
const consolePromise = page.waitForEvent('console')
|
|
||||||
|
|
||||||
await expectPlausibleInAction(page, {
|
|
||||||
action: async () => {
|
|
||||||
await openPage(page, {})
|
|
||||||
await page.evaluate(() => {
|
|
||||||
window.plausible.init()
|
|
||||||
})
|
|
||||||
await consolePromise
|
|
||||||
},
|
|
||||||
expectedRequests: [{ n: 'pageview' }]
|
|
||||||
})
|
|
||||||
|
|
||||||
const warning = await consolePromise
|
|
||||||
expect(warning.type()).toBe("warning")
|
|
||||||
expect(warning.text()).toContain('Plausible analytics script was already initialized, skipping init')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('handles the script being loaded and initialized multiple times', async ({ page }) => {
|
|
||||||
const consolePromise = page.waitForEvent('console')
|
|
||||||
|
|
||||||
await expectPlausibleInAction(page, {
|
|
||||||
action: async () => {
|
|
||||||
await openPage(page, {})
|
|
||||||
await page.evaluate(() => {
|
|
||||||
window.includePlausibleScript()
|
|
||||||
})
|
|
||||||
await consolePromise
|
|
||||||
},
|
|
||||||
expectedRequests: [{ n: 'pageview' }]
|
|
||||||
})
|
|
||||||
|
|
||||||
const warning = await consolePromise
|
|
||||||
expect(warning.type()).toBe("warning")
|
|
||||||
expect(warning.text()).toContain('Plausible analytics script was already initialized, skipping init')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('does not support overriding domain via `init`', async ({ page }) => {
|
|
||||||
await expectPlausibleInAction(page, {
|
|
||||||
action: async () => {
|
|
||||||
await openPage(page, {}, { skipPlausibleInit: true })
|
|
||||||
await callInit(page, { domain: 'another-domain.com' }, 'window.plausible')
|
|
||||||
},
|
|
||||||
expectedRequests: [{ n: 'pageview', d: 'example.com', u: expecting.stringContaining('plausible-web.html') }]
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
@ -1,328 +0,0 @@
|
||||||
import {
|
|
||||||
expectPlausibleInAction,
|
|
||||||
hideAndShowCurrentTab,
|
|
||||||
e as expecting,
|
|
||||||
isPageviewEvent,
|
|
||||||
isEngagementEvent,
|
|
||||||
delay
|
|
||||||
} from './support/test-utils'
|
|
||||||
import { test, expect } from '@playwright/test'
|
|
||||||
|
|
||||||
// Wrapper around calling `plausible.init` in the page context for users of `testPlausibleConfiguration`
|
|
||||||
export async function callInit(page, config, parent) {
|
|
||||||
// Stringify the customProperties and transformRequest functions to work around evaluate not being able to serialize functions
|
|
||||||
if (config && typeof config.customProperties === 'function') {
|
|
||||||
config.customProperties = { "_wrapFunction": config.customProperties.toString() }
|
|
||||||
}
|
|
||||||
if (config && typeof config.transformRequest === 'function') {
|
|
||||||
config.transformRequest = { "_wrapFunction": config.transformRequest.toString() }
|
|
||||||
}
|
|
||||||
|
|
||||||
await page.evaluate(({ config, parent }) => {
|
|
||||||
if (config && config.customProperties && config.customProperties._wrapFunction) {
|
|
||||||
config.customProperties = new Function(`return (${config.customProperties._wrapFunction})`)();
|
|
||||||
}
|
|
||||||
if (config && config.transformRequest && config.transformRequest._wrapFunction) {
|
|
||||||
config.transformRequest = new Function(`return (${config.transformRequest._wrapFunction})`)();
|
|
||||||
}
|
|
||||||
eval(parent).init(config)
|
|
||||||
}, { config, parent })
|
|
||||||
}
|
|
||||||
|
|
||||||
export function testPlausibleConfiguration({ openPage, initPlausible, fixtureName, fixtureTitle }) {
|
|
||||||
test.describe('shared configuration tests', () => {
|
|
||||||
test.beforeEach(async ({ page }) => {
|
|
||||||
// Mock file download requests
|
|
||||||
await page.context().route('https://awesome.website.com/file.pdf', async (request) => {
|
|
||||||
await request.fulfill({status: 200, contentType: 'application/pdf', body: 'mocked pdf content'})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test('triggers pageview and engagement automatically', async ({ page }) => {
|
|
||||||
await expectPlausibleInAction(page, {
|
|
||||||
action: () => openPage(page, {}),
|
|
||||||
expectedRequests: [{ n: 'pageview', d: 'example.com', u: expecting.stringContaining(fixtureName)}],
|
|
||||||
})
|
|
||||||
|
|
||||||
await expectPlausibleInAction(page, {
|
|
||||||
action: () => hideAndShowCurrentTab(page, {delay: 2000}),
|
|
||||||
expectedRequests: [{n: 'engagement', d: 'example.com', u: expecting.stringContaining(fixtureName)}],
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test('logs, does not trigger any events when `local` config is disabled', async ({ page }) => {
|
|
||||||
const consolePromise = page.waitForEvent('console')
|
|
||||||
|
|
||||||
await expectPlausibleInAction(page, {
|
|
||||||
action: () => openPage(page, { captureOnLocalhost: false }),
|
|
||||||
expectedRequests: [],
|
|
||||||
refutedRequests: [{ n: 'pageview' }]
|
|
||||||
})
|
|
||||||
|
|
||||||
const warning = await consolePromise
|
|
||||||
expect(warning.type()).toBe("warning")
|
|
||||||
expect(warning.text()).toEqual('Ignoring Event: localhost')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('does not log when `local` and `logging` config is disabled', async ({ page }) => {
|
|
||||||
const consolePromise = page.waitForEvent('console')
|
|
||||||
|
|
||||||
await expectPlausibleInAction(page, {
|
|
||||||
action: async () => {
|
|
||||||
await openPage(page, {}, { skipPlausibleInit: true })
|
|
||||||
await initPlausible(page, { captureOnLocalhost: false, logging: false })
|
|
||||||
},
|
|
||||||
expectedRequests: [],
|
|
||||||
refutedRequests: [{ n: 'pageview' }]
|
|
||||||
})
|
|
||||||
|
|
||||||
const result = await Promise.race([consolePromise, delay(1000)])
|
|
||||||
expect(result).toBeUndefined()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('does not track pageview props without the feature being enabled', async ({ page }) => {
|
|
||||||
await expectPlausibleInAction(page, {
|
|
||||||
action: async () => {
|
|
||||||
await openPage(page, {})
|
|
||||||
},
|
|
||||||
expectedRequests: [{ n: 'pageview', p: expecting.toBeUndefined() }],
|
|
||||||
shouldIgnoreRequest: isEngagementEvent
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test('does not track outbound links without the feature being enabled', async ({ page, browserName }) => {
|
|
||||||
await expectPlausibleInAction(page, {
|
|
||||||
action: async () => {
|
|
||||||
await openPage(page, {})
|
|
||||||
await page.click('#outbound-link')
|
|
||||||
},
|
|
||||||
expectedRequests: [{ n: 'pageview', p: expecting.toBeUndefined() }],
|
|
||||||
refutedRequests: [{ n: 'Outbound Link: Click' }],
|
|
||||||
shouldIgnoreRequest: isEngagementEvent
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test('does not track file downloads without feature being enabled', async ({ page }) => {
|
|
||||||
await expectPlausibleInAction(page, {
|
|
||||||
action: async () => {
|
|
||||||
await openPage(page, {})
|
|
||||||
await page.click('#file-download', { modifiers: ['ControlOrMeta'] })
|
|
||||||
},
|
|
||||||
expectedRequests: [{ n: 'pageview' } ],
|
|
||||||
refutedRequests: [{ n: 'File Download' }],
|
|
||||||
shouldIgnoreRequest: isEngagementEvent
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test('tracks outbound links (when feature enabled)', async ({ page }) => {
|
|
||||||
await expectPlausibleInAction(page, {
|
|
||||||
action: async () => {
|
|
||||||
await openPage(page, { outboundLinks: true })
|
|
||||||
await page.click('#outbound-link')
|
|
||||||
},
|
|
||||||
expectedRequests: [
|
|
||||||
{ n: 'pageview', d: 'example.com', u: expecting.stringContaining(fixtureName) },
|
|
||||||
{ n: 'Outbound Link: Click', d: 'example.com', u: expecting.stringContaining(fixtureName), p: { url: 'https://example.com/' } }
|
|
||||||
],
|
|
||||||
shouldIgnoreRequest: isEngagementEvent
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test('tracks file downloads (when feature enabled)', async ({ page }) => {
|
|
||||||
await expectPlausibleInAction(page, {
|
|
||||||
action: async () => {
|
|
||||||
await openPage(page, { fileDownloads: true })
|
|
||||||
await page.click('#file-download')
|
|
||||||
},
|
|
||||||
expectedRequests: [{ n: 'File Download', p: { url: 'https://awesome.website.com/file.pdf' } }],
|
|
||||||
shouldIgnoreRequest: [isPageviewEvent, isEngagementEvent]
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test('tracks static custom pageview properties (when feature enabled)', async ({ page }) => {
|
|
||||||
await expectPlausibleInAction(page, {
|
|
||||||
action: async () => {
|
|
||||||
await openPage(page, {}, { skipPlausibleInit: true })
|
|
||||||
await initPlausible(page, { customProperties: { "some-prop": "456" } })
|
|
||||||
},
|
|
||||||
expectedRequests: [{ n: 'pageview', p: { "some-prop": "456" } }]
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test('tracks dynamic custom pageview properties (when feature enabled)', async ({ page }) => {
|
|
||||||
await expectPlausibleInAction(page, {
|
|
||||||
action: async () => {
|
|
||||||
await openPage(page, {}, { skipPlausibleInit: true })
|
|
||||||
await initPlausible(page, { customProperties: () => ({ "title": document.title }) })
|
|
||||||
},
|
|
||||||
expectedRequests: [{ n: 'pageview', p: { "title": fixtureTitle } }]
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test('tracks dynamic custom pageview properties with custom events and engagements', async ({ page }) => {
|
|
||||||
await expectPlausibleInAction(page, {
|
|
||||||
action: async () => {
|
|
||||||
await openPage(page, {}, { skipPlausibleInit: true })
|
|
||||||
await initPlausible(page, {
|
|
||||||
customProperties: (eventName) => ({ "author": "Uku", "some-prop": "456", [eventName]: "1" })
|
|
||||||
})
|
|
||||||
|
|
||||||
await page.click('#custom-event')
|
|
||||||
await hideAndShowCurrentTab(page, { delay: 200 })
|
|
||||||
},
|
|
||||||
expectedRequests: [
|
|
||||||
{ n: 'pageview', p: { "author": "Uku", "some-prop": "456", "pageview": "1"} },
|
|
||||||
// Passed property to `plausible` call overrides the default from `config.customProperties`
|
|
||||||
{ n: 'Custom event', p: { "author": "Karl", "some-prop": "456", "Custom event": "1" } },
|
|
||||||
// Engagement event inherits props from the pageview event
|
|
||||||
{ n: 'engagement', p: { "author": "Uku", "some-prop": "456", "pageview": "1"} },
|
|
||||||
]
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test('invalid function customProperties are ignored', async ({ page }) => {
|
|
||||||
await expectPlausibleInAction(page, {
|
|
||||||
action: async () => {
|
|
||||||
await openPage(page, {}, { skipPlausibleInit: true })
|
|
||||||
await initPlausible(page, { customProperties: () => document.title })
|
|
||||||
},
|
|
||||||
expectedRequests: [{ n: 'pageview', p: expecting.toBeUndefined() }]
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test('invalid customProperties are ignored', async ({ page }) => {
|
|
||||||
await expectPlausibleInAction(page, {
|
|
||||||
action: async () => {
|
|
||||||
await openPage(page, {}, { skipPlausibleInit: true })
|
|
||||||
await initPlausible(page, { customProperties: "abcdef" })
|
|
||||||
},
|
|
||||||
expectedRequests: [{ n: 'pageview', p: expecting.toBeUndefined() }]
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test('autoCapturePageviews=false mode does not track pageviews', async ({ page }) => {
|
|
||||||
await expectPlausibleInAction(page, {
|
|
||||||
action: async () => {
|
|
||||||
await openPage(page, {}, { skipPlausibleInit: true })
|
|
||||||
await initPlausible(page, { autoCapturePageviews: false })
|
|
||||||
await hideAndShowCurrentTab(page, { delay: 200 })
|
|
||||||
},
|
|
||||||
expectedRequests: [],
|
|
||||||
refutedRequests: [{ n: 'pageview' }, { n: 'engagement' }]
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test('autoCapturePageviews=false mode after manual pageview continues tracking', async ({ page }) => {
|
|
||||||
await expectPlausibleInAction(page, {
|
|
||||||
action: async () => {
|
|
||||||
await openPage(page, {}, { skipPlausibleInit: true })
|
|
||||||
await initPlausible(page, { autoCapturePageviews: false })
|
|
||||||
await page.click('#manual-pageview-1')
|
|
||||||
await page.click('#manual-pageview-2')
|
|
||||||
await hideAndShowCurrentTab(page, { delay: 200 })
|
|
||||||
},
|
|
||||||
expectedRequests: [
|
|
||||||
{ n: 'pageview', u: '/:test-plausible', d: 'example.com' },
|
|
||||||
{ n: 'engagement', u: '/:test-plausible', d: 'example.com' },
|
|
||||||
{ n: 'pageview', u: '/:test-plausible-2', d: 'example.com' },
|
|
||||||
{ n: 'engagement', u: '/:test-plausible-2', d: 'example.com' },
|
|
||||||
],
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test('does not send `h` parameter when `hashBasedRouting` config is disabled', async ({ page }) => {
|
|
||||||
await expectPlausibleInAction(page, {
|
|
||||||
action: () => openPage(page, {}),
|
|
||||||
expectedRequests: [{ n: 'pageview', h: expecting.toBeUndefined() }]
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test('sends `h` parameter when `hash` config is enabled', async ({ page }) => {
|
|
||||||
await expectPlausibleInAction(page, {
|
|
||||||
action: () => openPage(page, { hashBasedRouting: true }),
|
|
||||||
expectedRequests: [{ n: 'pageview', h: 1 }]
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test('tracking tagged events with revenue', async ({ page }) => {
|
|
||||||
await expectPlausibleInAction(page, {
|
|
||||||
action: async () => {
|
|
||||||
await openPage(page, {})
|
|
||||||
await page.click('#tagged-event')
|
|
||||||
},
|
|
||||||
expectedRequests: [{ n: 'Purchase', p: { foo: 'bar' }, $: { currency: 'EUR', amount: '13.32' } }],
|
|
||||||
shouldIgnoreRequest: [isPageviewEvent, isEngagementEvent]
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test('supports overriding the endpoint with a custom proxy via `init`', async ({ page }) => {
|
|
||||||
await expectPlausibleInAction(page, {
|
|
||||||
pathToMock: 'http://proxy.io/endpoint',
|
|
||||||
action: async () => {
|
|
||||||
await openPage(page, {}, { skipPlausibleInit: true })
|
|
||||||
await initPlausible(page, { endpoint: 'http://proxy.io/endpoint' })
|
|
||||||
},
|
|
||||||
expectedRequests: [{ n: 'pageview', d: 'example.com', u: expecting.stringContaining(fixtureName)}]
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test('supports ignoring requests with `transformRequest`', async ({ page }) => {
|
|
||||||
await expectPlausibleInAction(page, {
|
|
||||||
action: async () => {
|
|
||||||
await openPage(page, {}, { skipPlausibleInit: true })
|
|
||||||
await initPlausible(page, { transformRequest: () => null })
|
|
||||||
await page.click('#custom-event')
|
|
||||||
},
|
|
||||||
expectedRequests: [],
|
|
||||||
refutedRequests: [{ n: 'Custom event' }, { n: 'pageview' }]
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test('supports modifying the request payload with `transformRequest`', async ({ page }) => {
|
|
||||||
const transformRequest = (payload) => {
|
|
||||||
payload.p = payload.p || {}
|
|
||||||
payload.p.eventName = payload.n
|
|
||||||
payload.i = false
|
|
||||||
|
|
||||||
return payload
|
|
||||||
}
|
|
||||||
|
|
||||||
await expectPlausibleInAction(page, {
|
|
||||||
action: async () => {
|
|
||||||
await openPage(page, {}, { skipPlausibleInit: true })
|
|
||||||
await initPlausible(page, { transformRequest })
|
|
||||||
await page.click('#custom-event')
|
|
||||||
},
|
|
||||||
expectedRequests: [
|
|
||||||
{ n: 'pageview', p: { eventName: 'pageview' }, i: false },
|
|
||||||
{ n: 'Custom event', p: { eventName: 'Custom event', author: 'Karl' }, i: false }
|
|
||||||
]
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test('transformRequest props are sent with engagement events', async ({ page }) => {
|
|
||||||
const transformRequest = (payload) => {
|
|
||||||
payload.p = payload.p || {}
|
|
||||||
|
|
||||||
window.requestCount = (window.requestCount || 0) + 1
|
|
||||||
payload.p.requestCount = window.requestCount
|
|
||||||
|
|
||||||
return payload
|
|
||||||
}
|
|
||||||
|
|
||||||
await expectPlausibleInAction(page, {
|
|
||||||
action: async () => {
|
|
||||||
await openPage(page, {}, { skipPlausibleInit: true })
|
|
||||||
await initPlausible(page, { transformRequest })
|
|
||||||
await page.click('#custom-event')
|
|
||||||
await hideAndShowCurrentTab(page, { delay: 200 })
|
|
||||||
},
|
|
||||||
expectedRequests: [
|
|
||||||
{ n: 'pageview', p: { requestCount: 1 } },
|
|
||||||
{ n: 'Custom event', p: { author: 'Karl', requestCount: 2 } },
|
|
||||||
{ n: 'engagement', p: { requestCount: 1 } }
|
|
||||||
]
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
@ -42,21 +42,62 @@ const RESPONSE_BODY_TEMPLATE = `
|
||||||
|
|
||||||
const PLAUSIBLE_WEB_SNIPPET = compileWebSnippet()
|
const PLAUSIBLE_WEB_SNIPPET = compileWebSnippet()
|
||||||
|
|
||||||
function getConfiguredPlausibleWebSnippet({
|
export function serializeWithFunctions(obj: Record<string, any>): string {
|
||||||
autoCapturePageviews,
|
const functions: Record<string, string> = {}
|
||||||
...injectedScriptConfig
|
let counter = 0
|
||||||
|
|
||||||
|
const jsonString = JSON.stringify(obj, (_key, value) => {
|
||||||
|
if (typeof value === 'function') {
|
||||||
|
const placeholder = `__FUNCTION_${counter++}__`
|
||||||
|
functions[placeholder] = value.toString()
|
||||||
|
return placeholder
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
})
|
||||||
|
|
||||||
|
// Replace placeholders with actual function strings
|
||||||
|
let result = jsonString
|
||||||
|
for (const [placeholder, funcString] of Object.entries(functions)) {
|
||||||
|
result = result.replace(`"${placeholder}"`, funcString)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getConfiguredPlausibleWebSnippet({
|
||||||
|
hashBasedRouting,
|
||||||
|
outboundLinks,
|
||||||
|
fileDownloads,
|
||||||
|
formSubmissions,
|
||||||
|
domain,
|
||||||
|
endpoint,
|
||||||
|
...initOverrideOptions
|
||||||
}: ScriptConfig): string {
|
}: ScriptConfig): string {
|
||||||
|
const injectedScriptConfig = {
|
||||||
|
domain,
|
||||||
|
endpoint,
|
||||||
|
hashBasedRouting,
|
||||||
|
outboundLinks,
|
||||||
|
fileDownloads,
|
||||||
|
formSubmissions
|
||||||
|
}
|
||||||
const snippet = PLAUSIBLE_WEB_SNIPPET.replace(
|
const snippet = PLAUSIBLE_WEB_SNIPPET.replace(
|
||||||
'<%= plausible_script_url %>',
|
'<%= plausible_script_url %>',
|
||||||
`/tracker/js/plausible-web.js?script_config=${encodeURIComponent(
|
`/tracker/js/plausible-web.js?script_config=${encodeURIComponent(
|
||||||
JSON.stringify(injectedScriptConfig)
|
JSON.stringify(injectedScriptConfig)
|
||||||
)}`
|
)}`
|
||||||
)
|
)
|
||||||
// This option, if provided, must be lifted to script init(overrides) overrides, otherwise it is ignored. It was not meant to be injected.
|
|
||||||
if (autoCapturePageviews !== undefined) {
|
if (
|
||||||
|
Object.entries(initOverrideOptions).some(
|
||||||
|
([_key, value]) => value !== undefined
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
const serializedOptions = serializeWithFunctions(initOverrideOptions)
|
||||||
|
|
||||||
return snippet.replace(
|
return snippet.replace(
|
||||||
'plausible.init()',
|
'plausible.init()',
|
||||||
`plausible.init({"autoCapturePageviews":${JSON.stringify(autoCapturePageviews)}})`
|
`plausible.init(${serializedOptions})`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return snippet
|
return snippet
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ export async function mockManyRequests({
|
||||||
awaitedRequestCount,
|
awaitedRequestCount,
|
||||||
responseDelay,
|
responseDelay,
|
||||||
shouldIgnoreRequest,
|
shouldIgnoreRequest,
|
||||||
mockRequestTimeout = 3000
|
mockRequestTimeout = 1000
|
||||||
}: MockManyRequestsOptions) {
|
}: MockManyRequestsOptions) {
|
||||||
const requestList: unknown[] = []
|
const requestList: unknown[] = []
|
||||||
const scope = scopeMockToPage ? page : page.context()
|
const scope = scopeMockToPage ? page : page.context()
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ export const tracker_script_version = packageJson.tracker_script_version
|
||||||
* @param {Function} args.action - A function that returns a promise. The function is called
|
* @param {Function} args.action - A function that returns a promise. The function is called
|
||||||
* without arguments, and is `await`ed. This is the action that should or should not trigger
|
* without arguments, and is `await`ed. This is the action that should or should not trigger
|
||||||
* Plausible requests on the page.
|
* Plausible requests on the page.
|
||||||
|
* @param {string} [args.pathToMock] - The path to create the route at and mock the response for.
|
||||||
* @param {Array} [args.expectedRequests] - A list of partial JSON payloads that get matched
|
* @param {Array} [args.expectedRequests] - A list of partial JSON payloads that get matched
|
||||||
* against the bodies of event requests made. An `expectedRequest` is considered as having
|
* against the bodies of event requests made. An `expectedRequest` is considered as having
|
||||||
* occurred if all of its key-value pairs are found from the JSON body of an event request
|
* occurred if all of its key-value pairs are found from the JSON body of an event request
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,9 @@ export type Options = {
|
||||||
formSubmissions: boolean
|
formSubmissions: boolean
|
||||||
captureOnLocalhost: boolean
|
captureOnLocalhost: boolean
|
||||||
autoCapturePageviews: boolean
|
autoCapturePageviews: boolean
|
||||||
|
customProperties: Record<string, any> | ((eventName: string) => Record<string, any>)
|
||||||
|
transformRequest: (payload: unknown) => unknown,
|
||||||
|
logging: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ScriptConfig = {
|
export type ScriptConfig = {
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,42 @@ test.beforeEach(async ({ page }) => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
for (const mode of ['web', 'esm']) {
|
||||||
|
test(`tagged events tracking is always on (${mode})`, async ({ page }, {
|
||||||
|
testId
|
||||||
|
}) => {
|
||||||
|
const config = { ...DEFAULT_CONFIG }
|
||||||
|
const { url } = await initializePageDynamically(page, {
|
||||||
|
testId,
|
||||||
|
scriptConfig: switchByMode(
|
||||||
|
{
|
||||||
|
web: { ...DEFAULT_CONFIG },
|
||||||
|
esm: `<script type="module">import { init, track } from '/tracker/js/npm_package/plausible.js'; init(${JSON.stringify(
|
||||||
|
config
|
||||||
|
)})</script>`
|
||||||
|
},
|
||||||
|
mode
|
||||||
|
),
|
||||||
|
bodyContent: `<a class="plausible-event-name=Purchase plausible-event-discounted=true plausible-revenue-currency=EUR plausible-revenue-amount=13.32" href="https://example.com/target">Purchase</a>`
|
||||||
|
})
|
||||||
|
await expectPlausibleInAction(page, {
|
||||||
|
action: async () => {
|
||||||
|
await page.goto(url)
|
||||||
|
await page.click('a')
|
||||||
|
},
|
||||||
|
expectedRequests: [
|
||||||
|
{
|
||||||
|
n: 'Purchase',
|
||||||
|
p: { discounted: 'true', url: 'https://example.com/target' },
|
||||||
|
$: { currency: 'EUR', amount: '13.32' }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
shouldIgnoreRequest: [isPageviewEvent, isEngagementEvent]
|
||||||
|
})
|
||||||
|
await expect(page.getByText('mocked page')).toBeVisible()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
for (const mode of ['legacy', 'web']) {
|
for (const mode of ['legacy', 'web']) {
|
||||||
test.describe(`tagged events feature legacy/v2 parity (${mode})`, () => {
|
test.describe(`tagged events feature legacy/v2 parity (${mode})`, () => {
|
||||||
test('tracks link click and child of link click when link is tagged (using plausible-event-... double dash syntax)', async ({
|
test('tracks link click and child of link click when link is tagged (using plausible-event-... double dash syntax)', async ({
|
||||||
|
|
@ -361,6 +397,54 @@ for (const mode of ['legacy', 'web']) {
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('tracks tagged link clicks even if the link is within an svg tag, but fails to include href properly', async ({
|
||||||
|
page
|
||||||
|
}, { testId }) => {
|
||||||
|
const targetPage = await initializePageDynamically(page, {
|
||||||
|
testId,
|
||||||
|
scriptConfig: '',
|
||||||
|
bodyContent: `<h1>Navigation successful</h1>`,
|
||||||
|
path: '/target'
|
||||||
|
})
|
||||||
|
const { url } = await initializePageDynamically(page, {
|
||||||
|
testId,
|
||||||
|
scriptConfig: switchByMode(
|
||||||
|
{
|
||||||
|
web: { ...DEFAULT_CONFIG },
|
||||||
|
legacy:
|
||||||
|
'<script defer src="/tracker/js/plausible.local.tagged-events.js"></script>'
|
||||||
|
},
|
||||||
|
mode
|
||||||
|
),
|
||||||
|
bodyContent: `
|
||||||
|
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<a class="plausible-event-name=link+click" href="${targetPage.url}">
|
||||||
|
<circle cx="50" cy="50" r="50" />
|
||||||
|
</a>
|
||||||
|
</svg>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
|
||||||
|
const pageErrors: Error[] = []
|
||||||
|
page.on('pageerror', (err) => pageErrors.push(err))
|
||||||
|
|
||||||
|
await page.goto(url)
|
||||||
|
|
||||||
|
await expectPlausibleInAction(page, {
|
||||||
|
action: () => page.click('circle'),
|
||||||
|
expectedRequests: [
|
||||||
|
{
|
||||||
|
n: 'link click'
|
||||||
|
// bug with p.url, can't assert
|
||||||
|
}
|
||||||
|
],
|
||||||
|
shouldIgnoreRequest: [isPageviewEvent, isEngagementEvent]
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(pageErrors).toHaveLength(0)
|
||||||
|
await expect(page.getByText('Navigation successful')).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
test('does not track button without plausible-event-name class', async ({
|
test('does not track button without plausible-event-name class', async ({
|
||||||
page
|
page
|
||||||
}, { testId }) => {
|
}, { testId }) => {
|
||||||
|
|
@ -380,7 +464,7 @@ for (const mode of ['legacy', 'web']) {
|
||||||
|
|
||||||
await expectPlausibleInAction(page, {
|
await expectPlausibleInAction(page, {
|
||||||
action: () => page.click('button'),
|
action: () => page.click('button'),
|
||||||
refutedRequests: [{ n: expect.any(String) }],
|
refutedRequests: [{}],
|
||||||
shouldIgnoreRequest: [isPageviewEvent, isEngagementEvent]
|
shouldIgnoreRequest: [isPageviewEvent, isEngagementEvent]
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
@ -404,7 +488,7 @@ for (const mode of ['legacy', 'web']) {
|
||||||
|
|
||||||
await expectPlausibleInAction(page, {
|
await expectPlausibleInAction(page, {
|
||||||
action: () => page.click('span'),
|
action: () => page.click('span'),
|
||||||
refutedRequests: [{ n: expect.any(String) }],
|
refutedRequests: [{}],
|
||||||
shouldIgnoreRequest: [isPageviewEvent, isEngagementEvent]
|
shouldIgnoreRequest: [isPageviewEvent, isEngagementEvent]
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
@ -588,10 +672,45 @@ test.describe('tagged events feature when using legacy .compat extension', () =>
|
||||||
|
|
||||||
await expectPlausibleInAction(page, {
|
await expectPlausibleInAction(page, {
|
||||||
action: () => page.click('a'),
|
action: () => page.click('a'),
|
||||||
refutedRequests: [{ n: expect.any(String) }],
|
refutedRequests: [{}]
|
||||||
shouldIgnoreRequest: [isPageviewEvent, isEngagementEvent]
|
|
||||||
})
|
})
|
||||||
|
|
||||||
await expect(page.getByText('Subscription successful')).toBeVisible()
|
await expect(page.getByText('Subscription successful')).toBeVisible()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('does not intercept navigation for links within svgs, causing slow track requests to fail', async ({
|
||||||
|
page
|
||||||
|
}, { testId }) => {
|
||||||
|
const targetPage = await initializePageDynamically(page, {
|
||||||
|
testId,
|
||||||
|
scriptConfig: '',
|
||||||
|
bodyContent: `<h1>Subscription successful</h1>`,
|
||||||
|
path: '/target'
|
||||||
|
})
|
||||||
|
const { url } = await initializePageDynamically(page, {
|
||||||
|
testId,
|
||||||
|
scriptConfig:
|
||||||
|
'<script id="plausible" defer src="/tracker/js/plausible.compat.local.manual.tagged-events.js"></script>',
|
||||||
|
bodyContent: `
|
||||||
|
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<a class="plausible-event-name=link+click" href="${targetPage.url}">
|
||||||
|
<circle cx="50" cy="50" r="50" />
|
||||||
|
</a>
|
||||||
|
</svg>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
const pageErrors: Error[] = []
|
||||||
|
page.on('pageerror', (err) => pageErrors.push(err))
|
||||||
|
await page.goto(url)
|
||||||
|
|
||||||
|
await expectPlausibleInAction(page, {
|
||||||
|
action: () => page.click('a'),
|
||||||
|
refutedRequests: [{}],
|
||||||
|
responseDelay: 500,
|
||||||
|
mockRequestTimeout: 250
|
||||||
|
})
|
||||||
|
expect(pageErrors).toHaveLength(0)
|
||||||
|
|
||||||
|
await expect(page.getByText('Subscription successful')).toBeVisible()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,255 @@
|
||||||
|
import {
|
||||||
|
initializePageDynamically,
|
||||||
|
serializeWithFunctions
|
||||||
|
} from './support/initialize-page-dynamically'
|
||||||
|
import {
|
||||||
|
e,
|
||||||
|
expectPlausibleInAction,
|
||||||
|
hideAndShowCurrentTab,
|
||||||
|
isEngagementEvent,
|
||||||
|
isPageviewEvent,
|
||||||
|
switchByMode
|
||||||
|
} from './support/test-utils'
|
||||||
|
import { test } from '@playwright/test'
|
||||||
|
import { ScriptConfig } from './support/types'
|
||||||
|
import { LOCAL_SERVER_ADDR } from './support/server'
|
||||||
|
const DEFAULT_CONFIG: ScriptConfig = {
|
||||||
|
domain: 'example.com',
|
||||||
|
endpoint: `${LOCAL_SERVER_ADDR}/api/event`,
|
||||||
|
captureOnLocalhost: true
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const mode of ['web', 'esm']) {
|
||||||
|
test.describe(`respects "transformRequest" config option (${mode})`, () => {
|
||||||
|
test('if "transformRequest" is not a function, nothing happens', async ({
|
||||||
|
page
|
||||||
|
}, { testId }) => {
|
||||||
|
const config = {
|
||||||
|
...DEFAULT_CONFIG,
|
||||||
|
transformRequest: 123
|
||||||
|
}
|
||||||
|
const { url } = await initializePageDynamically(page, {
|
||||||
|
testId,
|
||||||
|
scriptConfig: switchByMode(
|
||||||
|
{
|
||||||
|
web: config,
|
||||||
|
esm: `<script type="module">import { init, track } from '/tracker/js/npm_package/plausible.js'; init(${serializeWithFunctions(
|
||||||
|
config
|
||||||
|
)})</script>`
|
||||||
|
},
|
||||||
|
mode
|
||||||
|
),
|
||||||
|
bodyContent: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
await expectPlausibleInAction(page, {
|
||||||
|
action: async () => {
|
||||||
|
await page.goto(url)
|
||||||
|
await hideAndShowCurrentTab(page, { delay: 200 })
|
||||||
|
},
|
||||||
|
expectedRequests: [
|
||||||
|
{
|
||||||
|
n: 'pageview',
|
||||||
|
p: e.toBeUndefined()
|
||||||
|
},
|
||||||
|
{ n: 'engagement', p: e.toBeUndefined() }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('if "transformRequest" is set to be a function that returns null conditionally, those events are not sent', async ({
|
||||||
|
page
|
||||||
|
}, { testId }) => {
|
||||||
|
const config = {
|
||||||
|
...DEFAULT_CONFIG,
|
||||||
|
transformRequest: (payload) => {
|
||||||
|
if (payload.n === 'Purchase') {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { url } = await initializePageDynamically(page, {
|
||||||
|
testId,
|
||||||
|
scriptConfig: switchByMode(
|
||||||
|
{
|
||||||
|
web: config,
|
||||||
|
esm: `<script type="module">import { init, track } from '/tracker/js/npm_package/plausible.js'; init(${serializeWithFunctions(
|
||||||
|
config
|
||||||
|
)})</script>`
|
||||||
|
},
|
||||||
|
mode
|
||||||
|
),
|
||||||
|
bodyContent: `<button onclick="window.plausible('Purchase')">Purchase</button>`
|
||||||
|
})
|
||||||
|
|
||||||
|
await expectPlausibleInAction(page, {
|
||||||
|
action: async () => {
|
||||||
|
await page.goto(url)
|
||||||
|
await page.click('button')
|
||||||
|
},
|
||||||
|
expectedRequests: [{ n: 'pageview' }],
|
||||||
|
refutedRequests: [
|
||||||
|
{
|
||||||
|
n: 'Purchase'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
shouldIgnoreRequest: [isEngagementEvent]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('if "transformRequest" is set to be a function, it will be called for all events', async ({
|
||||||
|
page
|
||||||
|
}, { testId }) => {
|
||||||
|
const config = {
|
||||||
|
...DEFAULT_CONFIG,
|
||||||
|
transformRequest: (payload) => {
|
||||||
|
return { ...payload, u: '/:masked/path' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { url } = await initializePageDynamically(page, {
|
||||||
|
testId,
|
||||||
|
scriptConfig: switchByMode(
|
||||||
|
{
|
||||||
|
web: config,
|
||||||
|
esm: `<script type="module">import { init, track } from '/tracker/js/npm_package/plausible.js'; init(${serializeWithFunctions(
|
||||||
|
config
|
||||||
|
)})</script>`
|
||||||
|
},
|
||||||
|
mode
|
||||||
|
),
|
||||||
|
bodyContent: `<button onclick="window.plausible('Purchase')">Purchase</button>`
|
||||||
|
})
|
||||||
|
|
||||||
|
await expectPlausibleInAction(page, {
|
||||||
|
action: async () => {
|
||||||
|
await page.goto(url)
|
||||||
|
await page.click('button')
|
||||||
|
await hideAndShowCurrentTab(page, { delay: 200 })
|
||||||
|
},
|
||||||
|
expectedRequests: [
|
||||||
|
{ n: 'pageview', u: '/:masked/path' },
|
||||||
|
{ n: 'Purchase', u: '/:masked/path' },
|
||||||
|
{ n: 'engagement', u: '/:masked/path' }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('"transformRequest" does not allow making engagement event props different from pageview event props', async ({
|
||||||
|
page
|
||||||
|
}, { testId }) => {
|
||||||
|
const config = {
|
||||||
|
...DEFAULT_CONFIG,
|
||||||
|
transformRequest: (payload) => {
|
||||||
|
const w = window as any
|
||||||
|
w.requestCount = (w.requestCount ?? 0) + 1
|
||||||
|
return { ...payload, p: { requestCount: w.requestCount } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { url } = await initializePageDynamically(page, {
|
||||||
|
testId,
|
||||||
|
scriptConfig: switchByMode(
|
||||||
|
{
|
||||||
|
web: config,
|
||||||
|
esm: `<script type="module">import { init, track } from '/tracker/js/npm_package/plausible.js'; init(${serializeWithFunctions(
|
||||||
|
config
|
||||||
|
)})</script>`
|
||||||
|
},
|
||||||
|
mode
|
||||||
|
),
|
||||||
|
bodyContent: `<button onclick="window.plausible('Purchase')">Purchase</button>`
|
||||||
|
})
|
||||||
|
|
||||||
|
await expectPlausibleInAction(page, {
|
||||||
|
action: async () => {
|
||||||
|
await page.goto(url)
|
||||||
|
await page.click('button')
|
||||||
|
await hideAndShowCurrentTab(page, { delay: 200 })
|
||||||
|
},
|
||||||
|
expectedRequests: [
|
||||||
|
{ n: 'pageview', p: { requestCount: 1 } },
|
||||||
|
{ n: 'Purchase', p: { requestCount: 2 } },
|
||||||
|
{ n: 'engagement', p: { requestCount: 1 } }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('specificity: "transformRequest" runs after custom properties are determined', async ({
|
||||||
|
page
|
||||||
|
}, { testId }) => {
|
||||||
|
const config = {
|
||||||
|
...DEFAULT_CONFIG,
|
||||||
|
customProperties: () => ({
|
||||||
|
author: 'John Smith'
|
||||||
|
}),
|
||||||
|
transformRequest: (payload) => {
|
||||||
|
return { ...payload, p: { author: 'Jane Doe' } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { url } = await initializePageDynamically(page, {
|
||||||
|
testId,
|
||||||
|
scriptConfig: switchByMode(
|
||||||
|
{
|
||||||
|
web: config,
|
||||||
|
esm: `<script type="module">import { init, track } from '/tracker/js/npm_package/plausible.js'; init(${serializeWithFunctions(
|
||||||
|
config
|
||||||
|
)})</script>`
|
||||||
|
},
|
||||||
|
mode
|
||||||
|
),
|
||||||
|
bodyContent: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
await expectPlausibleInAction(page, {
|
||||||
|
action: async () => {
|
||||||
|
await page.goto(url)
|
||||||
|
},
|
||||||
|
shouldIgnoreRequest: [isEngagementEvent],
|
||||||
|
expectedRequests: [
|
||||||
|
{
|
||||||
|
n: 'pageview',
|
||||||
|
p: {
|
||||||
|
author: 'Jane Doe'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('if "transformRequest" is defined to be a function that does not return an object, the request is still attempted', async ({
|
||||||
|
page
|
||||||
|
}, { testId }) => {
|
||||||
|
const config = {
|
||||||
|
...DEFAULT_CONFIG,
|
||||||
|
transformRequest: () => 'not an object'
|
||||||
|
}
|
||||||
|
|
||||||
|
const { url } = await initializePageDynamically(page, {
|
||||||
|
testId,
|
||||||
|
scriptConfig: switchByMode(
|
||||||
|
{
|
||||||
|
web: config,
|
||||||
|
esm: `<script type="module">import { init, track } from '/tracker/js/npm_package/plausible.js'; init(${serializeWithFunctions(
|
||||||
|
config
|
||||||
|
)})</script>`
|
||||||
|
},
|
||||||
|
mode
|
||||||
|
),
|
||||||
|
bodyContent: `<button onclick="window.plausible('subscribed from blog', { props: { title: 'A blog post title' } })">Subscribe</button>`
|
||||||
|
})
|
||||||
|
|
||||||
|
await expectPlausibleInAction(page, {
|
||||||
|
action: async () => {
|
||||||
|
await page.goto(url)
|
||||||
|
await page.click('button')
|
||||||
|
},
|
||||||
|
shouldIgnoreRequest: [isEngagementEvent],
|
||||||
|
expectedRequests: ['not an object', 'not an object']
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue