ScriptV2: `transformRequest` (#5488)

* Commit as Plausible Bot

This way commits by it can be excluded from protections

* ScriptV2: Add support for `config.transformRequest`

transformRequest allows users to either manipulate or ignore requests to plausible
with a minimal footprint.

Some use-cases we're aware of and are planning for:
1. Ignoring requests similar to previous exclusion rules in WordPress
2. Normalizing urls for requests

* Allow passing `options.url` for overriding url when calling track

Previous naming `u` was unintuitive, but is kept around (untyped) for backwards
compatibility reasons

* chore: Bump tracker_script_version to 17

* Changelog

* Update types

* Docs

* Add test showing interaction with engagement events

* README.md
This commit is contained in:
Karl-Aksel Puulmann 2025-06-12 14:50:27 +03:00 committed by GitHub
parent d215e50982
commit c3dd21431c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 116 additions and 10 deletions

View File

@ -58,7 +58,9 @@ jobs:
uses: EndBug/add-and-commit@a94899bca583c204427a224a7af87c02f9b325d5
with:
message: "Released tracker script version ${{ steps.package.outputs.version }}"
add: "tracker/npm_package"
github_token: ${{ secrets.PLAUSIBLE_BOT_GITHUB_TOKEN }}
add: |
- tracker/npm_package
- name: Notify team on success
if: ${{ success() }}

View File

@ -7,4 +7,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased
Initial release.
- Support for `config.transformRequest`
- Support for passing `url` as option when calling `track`

View File

@ -50,6 +50,7 @@ See also [plausible.d.ts](https://github.com/plausible/analytics/blob/master/tra
| `formSubmissions` | Whether to track form submissions. | `false` |
| `captureOnLocalhost` | Whether to capture events on localhost. | `false` |
| `customProperties` | Object or function that returns custom properties for a given event. | `{}` |
| `transformRequest` | Function that allows transforming or ignoring requests | |
#### Using `customProperties`

View File

@ -1,6 +1,6 @@
{
"name": "macobo-test-tracker",
"version": "0.1.6",
"version": "0.2.0",
"description": "Plausible Analytics official frontend tracking library",
"scripts": {
"test": "echo \"Error: Testing done in the tracker folder\" && exit 1"

View File

@ -34,6 +34,13 @@ export interface PlausibleConfig {
// Custom properties to add to all events tracked.
// If passed as a function, it will be called when `track` is called.
customProperties?: CustomProperties | ((eventName: string) => CustomProperties)
// A function that can be used to transform the payload before it is sent to the API.
// If the function returns null or any other falsy value, the event will be ignored.
//
// This can be used to avoid sending certain types of events, or modifying any event
// parameters, e.g. to clean URLs of values that should not be recorded.
transformRequest?: (payload: PlausibleRequestPayload) => PlausibleRequestPayload | null
}
export interface PlausibleEventOptions {
@ -56,7 +63,7 @@ export interface PlausibleEventOptions {
// Overrides the URL of the page that the event is being tracked on.
// If not provided, `location.href` will be used.
u?: string
url?: string
}
export type CustomProperties = Record<string, string>
@ -67,3 +74,20 @@ export type PlausibleEventRevenue = {
// Currency is an ISO 4217 string representing the currency code, e.g. "USD" or "EUR"
currency: string
}
export type PlausibleRequestPayload = {
// Event name
n: string,
// URL of the event
u: string,
// Domain of the event
d: string,
// Referrer
r?: string | null,
// Custom properties
p?: CustomProperties,
// Revenue information
$?: PlausibleEventRevenue,
// Whether the event is interactive
i?: boolean,
} & Record<string, unknown>

View File

@ -1,5 +1,5 @@
{
"tracker_script_version": 16,
"tracker_script_version": 17,
"type": "module",
"scripts": {
"deploy": "node compile.js",

View File

@ -55,7 +55,7 @@ export function track(eventName, options) {
payload.v = COMPILE_TRACKER_SCRIPT_VERSION
if (COMPILE_MANUAL) {
var customURL = options && options.u
var customURL = options && (options.u || options.url)
payload.u = customURL ? customURL : location.href
} else {
@ -111,6 +111,14 @@ export function track(eventName, options) {
payload.h = 1
}
if ((COMPILE_PLAUSIBLE_WEB || COMPILE_PLAUSIBLE_NPM) && typeof config.transformRequest === 'function') {
payload = config.transformRequest(payload)
if (!payload) {
return onIgnoredEvent(eventName, options, 'transformRequest')
}
}
if (isPageview) {
postPageviewTrack(payload)
}

View File

@ -19,7 +19,8 @@
<a id="outbound-link" href="https://example.com">Outbound link</a>
<a id="manual-pageview" onclick="window.track('pageview', { u: '/:test-plausible' })">Manual pageview</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>

View File

@ -14,7 +14,8 @@
<a id="outbound-link" href="https://example.com">Outbound link</a>
<a id="manual-pageview" onclick="plausible('pageview', { u: '/:test-plausible' })">Manual pageview</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>

View File

@ -11,15 +11,21 @@ import { test } 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 function to work around evaluate not being able to serialize functions
// 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 })
}
@ -187,12 +193,15 @@ export function testPlausibleConfiguration({ openPage, initPlausible, fixtureNam
action: async () => {
await openPage(page, {}, { skipPlausibleInit: true })
await initPlausible(page, { autoCapturePageviews: false })
await page.click('#manual-pageview')
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' },
],
})
})
@ -232,5 +241,64 @@ export function testPlausibleConfiguration({ openPage, initPlausible, fixtureNam
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 } }
]
})
})
})
}