ScriptV2: `callback`, file downloads and `logging` changes (#5514)

* Make logging in tracker script configurable

* Improved callbacks

* Improved fileDownloads

* Export DEFAULT_FILE_TYPES

* Cross-browser compatibility

* Rename

* Rename a test

* chore: Bump tracker_script_version to 20
This commit is contained in:
Karl-Aksel Puulmann 2025-06-19 11:45:20 +03:00 committed by GitHub
parent 7da74b3031
commit 1f86ae55ef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 192 additions and 29 deletions

View File

@ -88,7 +88,7 @@ function wrapInstantlyEvaluatingFunction(baseCode) {
// Works around minification limitation of swc not allowing exports
function addExports(code) {
return `${code}\nexport { init, track }`
return `${code}\nexport { init, track, DEFAULT_FILE_TYPES }`
}
export function compileWebSnippet() {
@ -129,7 +129,7 @@ function minify(code, globals, variant = {}) {
}
if (variant.npm_package) {
minifyOptions.mangle.reserved = ['init', 'track']
minifyOptions.mangle.reserved = ['init', 'track', 'DEFAULT_FILE_TYPES']
minifyOptions.mangle.toplevel = true
}

View File

@ -7,9 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased
## [0.2.3] - 2025-06-17
- Rename `Form Submission` system event to `Form: Submission`.
- Add `logging` option
- Improve `callback` option
- Improve `fileDownloads` typing, export `DEFAULT_FILE_TYPES`
## [0.2.2] - 2025-06-16

View File

@ -51,6 +51,7 @@ See also [plausible.d.ts](https://github.com/plausible/analytics/blob/master/tra
| `fileDownloads` | Whether to track file downloads. | `false` |
| `formSubmissions` | Whether to track form submissions. | `false` |
| `captureOnLocalhost` | Whether to capture events on localhost. | `false` |
| `logging` | Whether to log on ignored events. | `true` |
| `customProperties` | Object or function that returns custom properties for a given event. | `{}` |
| `transformRequest` | Function that allows transforming or ignoring requests | |
@ -102,6 +103,26 @@ track('Purchase', { revenue: { amount: 15.99, currency: 'USD' } })
More information can be found in [ecommerce revenue tracking docs](https://plausible.io/docs/ecommerce-revenue-tracking)
### Callbacks
When calling `track`, you can pass in a custom callback.
```javascript
import { track } from 'macobo-test-tracker'
track('some-event', {
callback: (result) => {
if (result?.status) {
console.debug("Request to plausible done. Status:", result.status)
} else if (result?.error) {
console.log("Error handling request:", result.error)
} else {
console.log("Request was ignored")
}
}
})
```
### Opt out and exclude yourself from the analytics
Since plausible-tracker is bundled with your application code, using an ad-blocker to exclude your visits isn't an option. Fortunately Plausible has an alternative for this scenario: plausible-tracker will not send events if `localStorage.plausible_ignore` is set to `"true"`.

View File

@ -23,7 +23,7 @@ export interface PlausibleConfig {
outboundLinks?: boolean
// Whether to track file downloads. Defaults to false.
fileDownloads?: boolean
fileDownloads?: boolean | { fileExtensions: string[] }
// Whether to track form submissions. Defaults to false.
formSubmissions?: boolean
@ -31,6 +31,9 @@ export interface PlausibleConfig {
// Whether to capture events on localhost. Defaults to false.
captureOnLocalhost?: boolean
// Whether to log on ignored events. Defaults to true.
logging?: boolean
// 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)
@ -59,7 +62,8 @@ export interface PlausibleEventOptions {
// Called when request to `endpoint` completes or is ignored.
// When request is ignored, the result will be undefined.
// When request was delivered, the result will be an object with the response status code of the request.
callback?: (result?: { status: number } | undefined) => void
// When there was a network error, the result will be an object with the error object.
callback?: (result?: { status: number } | { error: unknown } | undefined) => void
// Overrides the URL of the page that the event is being tracked on.
// If not provided, `location.href` will be used.
@ -91,3 +95,6 @@ export type PlausibleRequestPayload = {
// Whether the event is interactive
i?: boolean,
} & Record<string, unknown>
// Default file types that are tracked when `fileDownloads` is enabled.
export const DEFAULT_FILE_TYPES: string[]

View File

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

View File

@ -27,8 +27,9 @@ export function init(overrides) {
Object.assign(config, overrides, {
// Explicitly set domain before any overrides are applied as `plausible-web` does not support overriding it
domain: config.domain,
// autoCapturePageviews defaults to `true`
autoCapturePageviews: overrides.autoCapturePageviews !== false
// Configuration which defaults to `true`
autoCapturePageviews: overrides.autoCapturePageviews !== false,
logging: overrides.logging !== false
})
} else if (COMPILE_PLAUSIBLE_NPM) {
if (config.isInitialized) {
@ -41,13 +42,15 @@ export function init(overrides) {
overrides.endpoint = 'https://plausible.io/api/event'
}
Object.assign(config, overrides, {
autoCapturePageviews: overrides.autoCapturePageviews !== false
autoCapturePageviews: overrides.autoCapturePageviews !== false,
logging: overrides.logging !== false
})
config.isInitialized = true
} else {
// Legacy variant
config.endpoint = scriptEl.getAttribute('data-api') || defaultEndpoint()
config.domain = scriptEl.getAttribute('data-domain')
config.logging = true
}
}

View File

@ -2,10 +2,11 @@
import { config, scriptEl, location, document } from './config'
import { track } from './track'
export var DEFAULT_FILE_TYPES = ['pdf', 'xlsx', 'docx', 'txt', 'rtf', 'csv', 'exe', 'key', 'pps', 'ppt', 'pptx', '7z', 'pkg', 'rar', 'gz', 'zip', 'avi', 'mov', 'mp4', 'mpeg', 'wmv', 'midi', 'mp3', 'wav', 'wma', 'dmg']
var MIDDLE_MOUSE_BUTTON = 1
var PARENTS_TO_SEARCH_LIMIT = 3
var defaultFileTypes = ['pdf', 'xlsx', 'docx', 'txt', 'rtf', 'csv', 'exe', 'key', 'pps', 'ppt', 'pptx', '7z', 'pkg', 'rar', 'gz', 'zip', 'avi', 'mov', 'mp4', 'mpeg', 'wmv', 'midi', 'mp3', 'wav', 'wma', 'dmg']
var fileTypesToTrack = defaultFileTypes
var fileTypesToTrack = DEFAULT_FILE_TYPES
function getLinkEl(link) {
while (link && (typeof link.tagName === 'undefined' || !isLink(link) || !link.href)) {
@ -156,8 +157,8 @@ export function init() {
if (COMPILE_FILE_DOWNLOADS && (!COMPILE_CONFIG || config.fileDownloads)) {
if (COMPILE_CONFIG) {
if (Array.isArray(config.fileDownloads)) {
fileTypesToTrack = config.fileDownloads
if (typeof config.fileDownloads === 'object' && Array.isArray(config.fileDownloads.fileExtensions)) {
fileTypesToTrack = config.fileDownloads.fileExtensions
}
} else {
var fileTypesAttr = scriptEl.getAttribute('file-types')
@ -167,7 +168,7 @@ export function init() {
fileTypesToTrack = fileTypesAttr.split(",")
}
if (addFileTypesAttr) {
fileTypesToTrack = addFileTypesAttr.split(",").concat(defaultFileTypes)
fileTypesToTrack = addFileTypesAttr.split(",").concat(DEFAULT_FILE_TYPES)
}
}

View File

@ -8,7 +8,11 @@ export function sendRequest(endpoint, payload, options) {
request.onreadystatechange = function () {
if (request.readyState === 4) {
options && options.callback && options.callback({ status: request.status })
if (request.status === 0) {
options && options.callback && options.callback({ error: new Error('Network error') })
} else {
options && options.callback && options.callback({ status: request.status })
}
}
}
} else {
@ -22,7 +26,9 @@ export function sendRequest(endpoint, payload, options) {
body: JSON.stringify(payload)
}).then(function (response) {
options && options.callback && options.callback({ status: response.status })
}).catch(function () { })
}).catch(function (error) {
options && options.callback && options.callback({ error })
})
}
}
}

View File

@ -1,16 +1,19 @@
import { init as initEngagementTracking } from './engagement'
import { init as initConfig, config } from './config'
import { init as initCustomEvents } from './custom-events'
import { init as initCustomEvents, DEFAULT_FILE_TYPES } from './custom-events'
import { init as initAutocapture } from './autocapture'
import { track } from './track'
function init(overrides) {
initConfig(overrides || {})
if (COMPILE_PLAUSIBLE_WEB && window.plausible && window.plausible.l) {
console.warn('Plausible analytics script was already initialized, skipping init')
if (config.logging) {
console.warn('Plausible analytics script was already initialized, skipping init')
}
return
}
initConfig(overrides)
initEngagementTracking()
if (!COMPILE_MANUAL || (COMPILE_CONFIG && config.autoCapturePageviews)) {
@ -49,4 +52,4 @@ if (COMPILE_PLAUSIBLE_WEB) {
}
// In npm module, we export the init and track functions
// export { init, track }
// export { init, track, DEFAULT_FILE_TYPES }

View File

@ -128,7 +128,10 @@ export function track(eventName, options) {
function onIgnoredEvent(eventName, options, reason) {
if (reason) console.warn('Ignoring Event: ' + reason);
if (reason && config.logging) {
console.warn('Ignoring Event: ' + reason);
}
options && options.callback && options.callback()
if (eventName === 'pageview') {

View File

@ -0,0 +1,59 @@
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')
})

View File

@ -94,7 +94,7 @@ test.describe('file downloads', () => {
})
test('tracks iso but not pdf files when config.fileDownloads includes "iso"', async ({ page }) => {
await openPage(page, { fileDownloads: ['iso'] })
await openPage(page, { fileDownloads: { fileExtensions: ['iso'] } })
await expectPlausibleInAction(page, {
action: async () => {
@ -111,7 +111,7 @@ test.describe('file downloads', () => {
})
test('ignores malformed value but enables the feature', async ({ page }) => {
await openPage(page, { fileDownloads: 'iso' })
await openPage(page, { fileDownloads: { fileExtensions: 'iso' } })
await expectPlausibleInAction(page, {
action: async () => {

36
tracker/test/fixtures/callbacks.html vendored Normal file
View File

@ -0,0 +1,36 @@
<!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>

View File

@ -5,9 +5,10 @@ import {
mockRequest,
e as expecting,
isPageviewEvent,
isEngagementEvent
isEngagementEvent,
delay
} from './support/test-utils'
import { test } from '@playwright/test'
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) {
@ -49,13 +50,36 @@ export function testPlausibleConfiguration({ openPage, initPlausible, fixtureNam
})
})
test('does not trigger any events when `local` config is disabled', async ({ page }) => {
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 () => {

View File

@ -208,6 +208,6 @@ function checkEqual(a, b) {
return a === b
}
function delay(ms) {
export function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}