ScriptV2: NPM library codebase (#5459)
* Compile NPM modules into commonjs and esm modules Note that since the module uses only vanilla es5 outside the export, it's safe to just append to the minified output * package.json, Typescript definition, generation for commonjs * Scrap commonjs for the moment Running into too big rabbit holes trying to get it off the ground * Shared tests for npm and web * Better way to identify npm module in fixture * Simplify code * Add NPM library specific tests * Fix a flaky test * Standardize a callsite * npm_package standardize on * Make legacy variant code more explicit * Wrap code pre and post * analyze-sizes.js should work with npm_package * Port test changes from PR #5432 * Update outbound link feature test * Await callInit * Smaller npm module - mangle variables * Clean up wrapping code * chore: Bump tracker_script_version to 15 * Add debug messaging to expect * Remove dead code * Refactor, account for initialization delays in tests * Remove needless features from compiler As the compiler currently manages to compile everything in ~3 seconds, the `features`/`compileIds` feature serves no more point. Scrapping it * Re-inline readOutput * Update tracker/test/plausible-npm.spec.js Co-authored-by: Artur Pata <artur.pata@gmail.com> * Remove redundant comment --------- Co-authored-by: Artur Pata <artur.pata@gmail.com>
This commit is contained in:
parent
d242e4ecdd
commit
595e71a399
|
|
@ -47,6 +47,11 @@ npm-debug.log
|
|||
# Temporary file used by analyze-sizes.js
|
||||
/tracker/compiler/.analyze-sizes.json
|
||||
|
||||
# Tracker npm module files that are generated by the compiler for the NPM package
|
||||
/tracker/npm_package/plausible.js*
|
||||
/tracker/npm_package/plausible.cjs*
|
||||
/tracker/npm_package/plausible.d.cts
|
||||
|
||||
# test coverage directory
|
||||
/assets/coverage
|
||||
|
||||
|
|
|
|||
|
|
@ -2,11 +2,8 @@ import { parseArgs } from 'node:util'
|
|||
import { compileAll, compileWebSnippet } from './compiler/index.js'
|
||||
import chokidar from 'chokidar'
|
||||
|
||||
const { values, positionals } = parseArgs({
|
||||
const { values } = parseArgs({
|
||||
options: {
|
||||
'target': {
|
||||
type: 'string',
|
||||
},
|
||||
'watch': {
|
||||
type: 'boolean',
|
||||
short: 'w'
|
||||
|
|
@ -21,14 +18,12 @@ const { values, positionals } = parseArgs({
|
|||
'web-snippet': {
|
||||
type: 'boolean',
|
||||
}
|
||||
},
|
||||
allowPositionals: true
|
||||
}
|
||||
})
|
||||
|
||||
if (values.help) {
|
||||
console.log('Usage: node compile.js [...compile-ids] [flags]')
|
||||
console.log('Usage: node compile.js [flags]')
|
||||
console.log('Options:')
|
||||
console.log(' --target hash,outbound-links,exclusions Only compile variants that contain all specified compile-ids')
|
||||
console.log(' --watch, -w Watch src/ directory for changes and recompile')
|
||||
console.log(' --suffix, -s Suffix to add to the output file name. Used for testing script size changes')
|
||||
console.log(' --help Show this help message')
|
||||
|
|
@ -36,34 +31,21 @@ if (values.help) {
|
|||
process.exit(0);
|
||||
}
|
||||
|
||||
function parse(value) {
|
||||
if (value == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
return value
|
||||
.split(/[.,]/)
|
||||
.filter(feature => !['js', 'plausible'].includes(feature))
|
||||
.sort()
|
||||
}
|
||||
|
||||
const compileOptions = {
|
||||
targets: parse(values.target),
|
||||
only: positionals && positionals.length > 0 ? positionals.map(parse) : null,
|
||||
suffix: values.suffix
|
||||
}
|
||||
|
||||
if (values['web-snippet']) {
|
||||
console.log(compileWebSnippet())
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
const compileOptions = {
|
||||
suffix: values.suffix
|
||||
}
|
||||
|
||||
await compileAll(compileOptions)
|
||||
|
||||
if (values.watch) {
|
||||
console.log('Watching src/ directory for changes...')
|
||||
|
||||
chokidar.watch('./src').on('change', async (event, path) => {
|
||||
chokidar.watch('./src').on('change', async (_event, path) => {
|
||||
if (path) {
|
||||
console.log(`\nFile changed: ${path}`)
|
||||
console.log('Recompiling...')
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ const { values } = parseArgs({
|
|||
const { currentSuffix, baselineSuffix } = values
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
const TRACKER_FILES_DIR = path.join(__dirname, "../../priv/tracker/js/")
|
||||
const NPM_PACKAGE_FILES_DIR = path.join(__dirname, "../npm_package")
|
||||
|
||||
const HEADER = ['', 'Brotli', 'Gzip', 'Uncompressed']
|
||||
|
||||
|
|
@ -229,21 +230,34 @@ function addSign(value) {
|
|||
}
|
||||
|
||||
function readPlausibleScriptSizes() {
|
||||
const files = fs.readdirSync(TRACKER_FILES_DIR).filter((filename) =>
|
||||
!['.gitkeep', 'p.js'].includes(filename) && (filename.includes(currentSuffix) || filename.includes(baselineSuffix))
|
||||
)
|
||||
const trackerFileSizes = fs.readdirSync(TRACKER_FILES_DIR)
|
||||
.filter(isRelevantFile)
|
||||
.map((filename) => readFileSize(filename, TRACKER_FILES_DIR))
|
||||
|
||||
return files.map((filename) => {
|
||||
const filePath = path.join(TRACKER_FILES_DIR, filename)
|
||||
const [_, variant, suffix] = /(.*)[.]js(.*)/.exec(filename)
|
||||
return {
|
||||
variant: `${variant}.js`,
|
||||
suffix,
|
||||
uncompressed: fs.statSync(filePath).size,
|
||||
gzip: execSync(`gzip -c -9 "${filePath}"`).length,
|
||||
brotli: execSync(`brotli -c -q 11 "${filePath}"`).length
|
||||
}
|
||||
})
|
||||
const npmPackageFileSizes = fs.readdirSync(NPM_PACKAGE_FILES_DIR)
|
||||
.filter(isRelevantFile)
|
||||
.map((filename) => readFileSize(filename, NPM_PACKAGE_FILES_DIR))
|
||||
|
||||
return trackerFileSizes.concat(npmPackageFileSizes)
|
||||
}
|
||||
|
||||
function readFileSize(filename, basepath) {
|
||||
const filePath = path.join(basepath, filename)
|
||||
const [_, variant, suffix] = /(.*)[.]js(.*)/.exec(filename)
|
||||
|
||||
return {
|
||||
variant: (basepath === TRACKER_FILES_DIR ? `${variant}.js` : 'npm_package/plausible.js'),
|
||||
suffix,
|
||||
uncompressed: fs.statSync(filePath).size,
|
||||
gzip: execSync(`gzip -c -9 "${filePath}"`).length,
|
||||
brotli: execSync(`brotli -c -q 11 "${filePath}"`).length
|
||||
}
|
||||
}
|
||||
|
||||
function isRelevantFile(filename) {
|
||||
return !['.gitkeep', 'p.js'].includes(filename) &&
|
||||
filename.includes('.js') &&
|
||||
(filename.includes(currentSuffix) || filename.includes(baselineSuffix))
|
||||
}
|
||||
|
||||
function clickhouseLocal(sql, inputLines = null) {
|
||||
|
|
|
|||
|
|
@ -14,8 +14,10 @@ let legacyVariants = [...g.clone.powerSet(LEGACY_VARIANT_NAMES)]
|
|||
.map(a => a.sort())
|
||||
.map((variant) => ({
|
||||
name: variant.length > 0 ? `plausible.${variant.join('.')}.js` : 'plausible.js',
|
||||
compileIds: variant,
|
||||
globals: Object.fromEntries(variant.map(id => [idToGlobal(id), true]))
|
||||
globals: {
|
||||
...Object.fromEntries(variant.map(id => [idToGlobal(id), true])),
|
||||
COMPILE_PLAUSIBLE_LEGACY_VARIANT: true
|
||||
}
|
||||
}))
|
||||
|
||||
const variantsFile = path.join(__dirname, 'variants.json')
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { minifySync as swcMinify } from '@swc/core'
|
||||
import { minifySync } from '@swc/core'
|
||||
import { rollup } from 'rollup'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
|
@ -12,6 +12,10 @@ import { spawn, Worker, Pool } from "threads"
|
|||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
const DEFAULT_GLOBALS = {
|
||||
COMPILE_PLAUSIBLE_WEB: false,
|
||||
COMPILE_PLAUSIBLE_NPM: false,
|
||||
COMPILE_PLAUSIBLE_LEGACY_VARIANT: false,
|
||||
COMPILE_CONFIG: false,
|
||||
COMPILE_HASH: false,
|
||||
COMPILE_OUTBOUND_LINKS: false,
|
||||
COMPILE_COMPAT: false,
|
||||
|
|
@ -24,26 +28,26 @@ const DEFAULT_GLOBALS = {
|
|||
COMPILE_REVENUE: false,
|
||||
COMPILE_EXCLUSIONS: false,
|
||||
COMPILE_TRACKER_SCRIPT_VERSION: packageJson.tracker_script_version,
|
||||
COMPILE_CONFIG: false
|
||||
}
|
||||
|
||||
const ALL_VARIANTS = variantsFile.legacyVariants.concat(variantsFile.manualVariants)
|
||||
|
||||
export async function compileAll(options = {}) {
|
||||
if (process.env.NODE_ENV === 'dev' && canSkipCompile()) {
|
||||
console.info('COMPILATION SKIPPED: No changes detected in tracker dependencies')
|
||||
return
|
||||
}
|
||||
|
||||
const variants = getVariantsToCompile(options)
|
||||
const bundledCode = await bundleCode()
|
||||
|
||||
const startTime = Date.now();
|
||||
console.log(`Starting compilation of ${variants.length} variants...`)
|
||||
console.log(`Starting compilation of ${ALL_VARIANTS.length} variants...`)
|
||||
|
||||
const bar = new progress.SingleBar({ clearOnComplete: true }, progress.Presets.shades_classic)
|
||||
bar.start(variants.length, 0)
|
||||
bar.start(ALL_VARIANTS.length, 0)
|
||||
|
||||
const workerPool = Pool(() => spawn(new Worker('./worker-thread.js')))
|
||||
variants.forEach(variant => {
|
||||
ALL_VARIANTS.forEach(variant => {
|
||||
workerPool.queue(async (worker) => {
|
||||
await worker.compileFile(variant, { ...options, bundledCode })
|
||||
bar.increment()
|
||||
|
|
@ -54,19 +58,27 @@ export async function compileAll(options = {}) {
|
|||
await workerPool.terminate()
|
||||
bar.stop()
|
||||
|
||||
console.log(`Completed compilation of ${variants.length} variants in ${((Date.now() - startTime) / 1000).toFixed(2)}s`);
|
||||
console.log(`Completed compilation of ${ALL_VARIANTS.length} variants in ${((Date.now() - startTime) / 1000).toFixed(2)}s`);
|
||||
}
|
||||
|
||||
export async function compileFile(variant, options) {
|
||||
const wrappedCode = wrapInstantlyEvaluatingFunction(options.bundledCode || await bundleCode())
|
||||
const globals = { ...DEFAULT_GLOBALS, ...variant.globals }
|
||||
let code = options.bundledCode || await bundleCode()
|
||||
|
||||
const code = minify(wrappedCode, globals)
|
||||
if (!variant.npm_package) {
|
||||
code = wrapInstantlyEvaluatingFunction(code)
|
||||
}
|
||||
|
||||
code = minify(code, globals, variant)
|
||||
|
||||
if (variant.npm_package) {
|
||||
code = addExports(code)
|
||||
}
|
||||
|
||||
if (options.returnCode) {
|
||||
return code
|
||||
} else {
|
||||
fs.writeFileSync(relPath(`../../priv/tracker/js/${variant.name}${options.suffix || ""}`), code)
|
||||
fs.writeFileSync(outputPath(variant, options), code)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -74,6 +86,11 @@ function wrapInstantlyEvaluatingFunction(baseCode) {
|
|||
return `(function(){${baseCode}})()`
|
||||
}
|
||||
|
||||
// Works around minification limitation of swc not allowing exports
|
||||
function addExports(code) {
|
||||
return `${code}\nexport { init, track }`
|
||||
}
|
||||
|
||||
export function compileWebSnippet() {
|
||||
const code = fs.readFileSync(relPath('../src/web-snippet.js')).toString()
|
||||
return `
|
||||
|
|
@ -84,41 +101,39 @@ export function compileWebSnippet() {
|
|||
`
|
||||
}
|
||||
|
||||
function getVariantsToCompile(options) {
|
||||
let targetVariants = variantsFile.legacyVariants.concat(variantsFile.manualVariants)
|
||||
if (options.targets !== null) {
|
||||
targetVariants = targetVariants.filter(variant =>
|
||||
options.targets.every(target => variant.compileIds.includes(target))
|
||||
)
|
||||
}
|
||||
if (options.only !== null) {
|
||||
targetVariants = targetVariants.filter(variant =>
|
||||
options.only.some(targetCompileIds => equalLists(variant.compileIds, targetCompileIds))
|
||||
)
|
||||
}
|
||||
|
||||
return targetVariants
|
||||
}
|
||||
|
||||
async function bundleCode() {
|
||||
async function bundleCode(format = 'esm') {
|
||||
const bundle = await rollup({
|
||||
input: 'src/plausible.js',
|
||||
})
|
||||
|
||||
const { output } = await bundle.generate({
|
||||
format: 'esm',
|
||||
})
|
||||
const { output } = await bundle.generate({ format })
|
||||
|
||||
return output[0].code
|
||||
}
|
||||
|
||||
function minify(baseCode, globals) {
|
||||
const result = swcMinify(baseCode, {
|
||||
function outputPath(variant, options) {
|
||||
if (variant.npm_package) {
|
||||
return relPath(`../${variant.name}${options.suffix || ""}`)
|
||||
} else {
|
||||
return relPath(`../../priv/tracker/js/${variant.name}${options.suffix || ""}`)
|
||||
}
|
||||
}
|
||||
|
||||
function minify(code, globals, variant = {}) {
|
||||
const minifyOptions = {
|
||||
compress: {
|
||||
global_defs: globals,
|
||||
passes: 4
|
||||
}
|
||||
})
|
||||
},
|
||||
mangle: {}
|
||||
}
|
||||
|
||||
if (variant.npm_package) {
|
||||
minifyOptions.mangle.reserved = ['init', 'track']
|
||||
minifyOptions.mangle.toplevel = true
|
||||
}
|
||||
|
||||
const result = minifySync(code, minifyOptions)
|
||||
|
||||
if (result.code) {
|
||||
return result.code
|
||||
|
|
@ -127,18 +142,6 @@ function minify(baseCode, globals) {
|
|||
}
|
||||
}
|
||||
|
||||
function equalLists(a, b) {
|
||||
if (a.length != b.length) {
|
||||
return false
|
||||
}
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (a[i] !== b[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function relPath(segment) {
|
||||
return path.join(__dirname, segment)
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"name": "macobo-test-tracker",
|
||||
"version": "0.0.1",
|
||||
"description": "Plausible Analytics official frontend tracking library",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: Testing done in the tracker folder\" && exit 1"
|
||||
},
|
||||
"repository": "https://github.com/plausible/plausible",
|
||||
"keywords": [
|
||||
"web-analytics",
|
||||
"website-analytics",
|
||||
"analytics"
|
||||
],
|
||||
"author": "Plausible Analytics",
|
||||
"license": "AGPLv3",
|
||||
"homepage": "https://plausible.io",
|
||||
"type": "module",
|
||||
"module": "./plausible.js",
|
||||
"types": "./plausible.d.ts"
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
// Sets up the tracking library. Can be called once.
|
||||
export function init(config: PlausibleConfig): void
|
||||
|
||||
// Tracks an event, requires `init` to be called first.
|
||||
export function track(eventName: string, options: PlausibleEventOptions): void
|
||||
|
||||
export interface PlausibleConfig {
|
||||
// Domain of the site to track. Should be registered with Plausible.
|
||||
domain: string,
|
||||
|
||||
// The URL of the Plausible API endpoint. Defaults to https://plausible.io/api/event
|
||||
// See proxying guide at https://plausible.io/docs/proxy/introduction
|
||||
endpoint?: string
|
||||
|
||||
// Whether to automatically capture pageviews. Defaults to true.
|
||||
autoCapturePageviews?: boolean
|
||||
|
||||
// Whether the page uses hash based routing. Defaults to false.
|
||||
// Read more at https://plausible.io/docs/hash-based-routing
|
||||
hashBasedRouting?: boolean
|
||||
|
||||
// Whether to track outbound link clicks. Defaults to false.
|
||||
outboundLinks?: boolean
|
||||
|
||||
// Whether to track file downloads. Defaults to false.
|
||||
fileDownloads?: boolean
|
||||
|
||||
// Whether to track form submissions. Defaults to false.
|
||||
formSubmissions?: boolean
|
||||
|
||||
// Whether to capture events on localhost. Defaults to false.
|
||||
captureOnLocalhost?: 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)
|
||||
}
|
||||
|
||||
export interface PlausibleEventOptions {
|
||||
// Custom properties to add to the event.
|
||||
// Read more at https://plausible.io/docs/custom-props/introduction
|
||||
props?: CustomProperties
|
||||
|
||||
// Whether the tracked event is interactive. Defaults to true.
|
||||
// By marking a custom event as non-interactive, it will not be counted towards bounce rate calculations.
|
||||
interactive?: boolean
|
||||
|
||||
// Revenue data to add to the event.
|
||||
// Read more at https://plausible.io/docs/ecommerce-revenue-tracking
|
||||
revenue?: PlausibleEventRevenue
|
||||
|
||||
// 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
|
||||
|
||||
// Overrides the URL of the page that the event is being tracked on.
|
||||
// If not provided, `location.href` will be used.
|
||||
u?: string
|
||||
}
|
||||
|
||||
export type CustomProperties = Record<string, string>
|
||||
|
||||
export type PlausibleEventRevenue = {
|
||||
// Revenue amount in `currency`
|
||||
amount: number | string,
|
||||
// Currency is an ISO 4217 string representing the currency code, e.g. "USD" or "EUR"
|
||||
currency: string
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"tracker_script_version": 14,
|
||||
"tracker_script_version": 15,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"deploy": "node compile.js",
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ var document = window.document
|
|||
|
||||
if (COMPILE_COMPAT) {
|
||||
var scriptEl = document.getElementById('plausible')
|
||||
} else {
|
||||
} else if (COMPILE_PLAUSIBLE_LEGACY_VARIANT) {
|
||||
var scriptEl = document.currentScript
|
||||
}
|
||||
|
||||
|
|
@ -21,7 +21,7 @@ function defaultEndpoint() {
|
|||
}
|
||||
|
||||
export function init(overrides) {
|
||||
if (COMPILE_CONFIG) {
|
||||
if (COMPILE_PLAUSIBLE_WEB) {
|
||||
// This will be dynamically replaced by a config json object in the script serving endpoint
|
||||
config = "<%= @config_js %>"
|
||||
Object.assign(config, overrides, {
|
||||
|
|
@ -30,7 +30,22 @@ export function init(overrides) {
|
|||
// autoCapturePageviews defaults to `true`
|
||||
autoCapturePageviews: overrides.autoCapturePageviews !== false
|
||||
})
|
||||
} else if (COMPILE_PLAUSIBLE_NPM) {
|
||||
if (config.isInitialized) {
|
||||
throw new Error('plausible.init() can only be called once')
|
||||
}
|
||||
if (!overrides || !overrides.domain) {
|
||||
throw new Error('plausible.init(): domain argument is required')
|
||||
}
|
||||
if (!overrides.endpoint) {
|
||||
overrides.endpoint = 'https://plausible.io/api/event'
|
||||
}
|
||||
Object.assign(config, overrides, {
|
||||
autoCapturePageviews: overrides.autoCapturePageviews !== false
|
||||
})
|
||||
config.isInitialized = true
|
||||
} else {
|
||||
// Legacy variant
|
||||
config.endpoint = scriptEl.getAttribute('data-api') || defaultEndpoint()
|
||||
config.domain = scriptEl.getAttribute('data-domain')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { init as initAutocapture } from './autocapture'
|
|||
import { track } from './track'
|
||||
|
||||
function init(overrides) {
|
||||
if (COMPILE_CONFIG && window.plausible && window.plausible.l) {
|
||||
if (COMPILE_PLAUSIBLE_WEB && window.plausible && window.plausible.l) {
|
||||
console.warn('Plausible analytics script was already initialized, skipping init')
|
||||
return
|
||||
}
|
||||
|
|
@ -17,22 +17,25 @@ function init(overrides) {
|
|||
initAutocapture(track)
|
||||
}
|
||||
|
||||
if (COMPILE_OUTBOUND_LINKS || COMPILE_FILE_DOWNLOADS || COMPILE_TAGGED_EVENTS || COMPILE_CONFIG) {
|
||||
if (COMPILE_PLAUSIBLE_WEB || COMPILE_PLAUSIBLE_NPM || COMPILE_OUTBOUND_LINKS || COMPILE_FILE_DOWNLOADS || COMPILE_TAGGED_EVENTS) {
|
||||
initCustomEvents()
|
||||
}
|
||||
|
||||
// Call `track` for any events that were queued via plausible('event') before `init` was called
|
||||
var queue = (window.plausible && window.plausible.q) || []
|
||||
for (var i = 0; i < queue.length; i++) {
|
||||
track.apply(this, queue[i])
|
||||
}
|
||||
|
||||
window.plausible = track
|
||||
window.plausible.init = init
|
||||
window.plausible.l = true
|
||||
if (COMPILE_PLAUSIBLE_WEB || COMPILE_PLAUSIBLE_LEGACY_VARIANT) {
|
||||
// Call `track` for any events that were queued via plausible('event') before `init` was called
|
||||
var queue = (window.plausible && window.plausible.q) || []
|
||||
for (var i = 0; i < queue.length; i++) {
|
||||
track.apply(this, queue[i])
|
||||
}
|
||||
|
||||
window.plausible = track
|
||||
window.plausible.init = init
|
||||
window.plausible.l = true
|
||||
}
|
||||
}
|
||||
|
||||
if (COMPILE_CONFIG) {
|
||||
if (COMPILE_PLAUSIBLE_WEB) {
|
||||
window.plausible = (window.plausible || {})
|
||||
|
||||
if (plausible.o) {
|
||||
|
|
@ -40,6 +43,10 @@ if (COMPILE_CONFIG) {
|
|||
}
|
||||
|
||||
plausible.init = init
|
||||
} else {
|
||||
} else if (COMPILE_PLAUSIBLE_LEGACY_VARIANT) {
|
||||
// Legacy variants automatically initialize based compile variables
|
||||
init()
|
||||
}
|
||||
|
||||
// In npm module, we export the init and track functions
|
||||
// export { init, track }
|
||||
|
|
|
|||
|
|
@ -3,6 +3,10 @@ import { prePageviewTrack, postPageviewTrack, onPageviewIgnored } from './engage
|
|||
import { config, scriptEl, location, document } from './config'
|
||||
|
||||
export function track(eventName, options) {
|
||||
if (COMPILE_PLAUSIBLE_NPM && !config.isInitialized) {
|
||||
throw new Error('plausible.track() can only be called after plausible.init()')
|
||||
}
|
||||
|
||||
var isPageview = eventName === 'pageview'
|
||||
|
||||
if (isPageview) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
<!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" onclick="window.track('pageview', { u: '/:test-plausible' })">Manual pageview</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>
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
|
||||
<a id="outbound-link" href="https://example.com">Outbound link</a>
|
||||
|
||||
<a id="manual-pageview" onclick="plausible('pageview', { u: '/:test-plausible-web' })">Manual pageview</a>
|
||||
<a id="manual-pageview" onclick="plausible('pageview', { u: '/:test-plausible' })">Manual pageview</a>
|
||||
|
||||
<a id="custom-event" onclick="plausible('Custom event', { props: { author: 'Karl' } })">Custom event</a>
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ import { test, Page } from '@playwright/test'
|
|||
import { LOCAL_SERVER_ADDR } from './support/server'
|
||||
import {
|
||||
expectPlausibleInAction,
|
||||
ignoreEngagementRequests
|
||||
isEngagementEvent,
|
||||
isPageviewEvent
|
||||
} from './support/test-utils'
|
||||
import { initializePageDynamically } from './support/initialize-page-dynamically'
|
||||
import { ScriptConfig } from './support/types'
|
||||
|
|
@ -33,7 +34,7 @@ test('does not track form submissions when the feature is disabled', async ({
|
|||
await page.goto(url)
|
||||
await page.click('input[type="submit"]')
|
||||
},
|
||||
shouldIgnoreRequest: ignoreEngagementRequests,
|
||||
shouldIgnoreRequest: isEngagementEvent,
|
||||
expectedRequests: [{ n: 'pageview' }],
|
||||
refutedRequests: [
|
||||
{
|
||||
|
|
@ -66,7 +67,7 @@ test.describe('form submissions feature is enabled', () => {
|
|||
await page.fill('input[type="text"]', 'Any Name')
|
||||
await page.click('input[type="submit"]')
|
||||
},
|
||||
shouldIgnoreRequest: pageviewOrEngagementEvent,
|
||||
shouldIgnoreRequest: [isPageviewEvent, isEngagementEvent],
|
||||
expectedRequests: [
|
||||
{
|
||||
n: 'Form Submission',
|
||||
|
|
@ -97,7 +98,7 @@ test.describe('form submissions feature is enabled', () => {
|
|||
await ensurePlausibleInitialized(page)
|
||||
await page.click('input[type="submit"]')
|
||||
},
|
||||
shouldIgnoreRequest: pageviewOrEngagementEvent,
|
||||
shouldIgnoreRequest: [isPageviewEvent, isEngagementEvent],
|
||||
expectedRequests: [
|
||||
{
|
||||
n: 'Form Submission',
|
||||
|
|
@ -136,7 +137,7 @@ test.describe('form submissions feature is enabled', () => {
|
|||
await page.click('button#dynamically-insert-form')
|
||||
await page.click('input[type="submit"]')
|
||||
},
|
||||
shouldIgnoreRequest: pageviewOrEngagementEvent,
|
||||
shouldIgnoreRequest: [isPageviewEvent, isEngagementEvent],
|
||||
expectedRequests: [
|
||||
{
|
||||
n: 'Form Submission',
|
||||
|
|
@ -170,7 +171,7 @@ test.describe('form submissions feature is enabled', () => {
|
|||
await page.fill('input[type="email"]', 'invalid email')
|
||||
await page.click('input[type="submit"]')
|
||||
},
|
||||
shouldIgnoreRequest: pageviewOrEngagementEvent,
|
||||
shouldIgnoreRequest: [isPageviewEvent, isEngagementEvent],
|
||||
expectedRequests: [
|
||||
{
|
||||
n: 'Form Submission',
|
||||
|
|
@ -203,7 +204,7 @@ test.describe('form submissions feature is enabled', () => {
|
|||
await page.fill('input[type="email"]', 'invalid email')
|
||||
await page.click('input[type="submit"]')
|
||||
},
|
||||
shouldIgnoreRequest: ignoreEngagementRequests,
|
||||
shouldIgnoreRequest: isEngagementEvent,
|
||||
expectedRequests: [{ n: 'pageview' }],
|
||||
refutedRequests: [
|
||||
{
|
||||
|
|
@ -236,7 +237,7 @@ test.describe('form submissions feature is enabled', () => {
|
|||
|
||||
await page.click('button#trigger-FormElement-submit')
|
||||
},
|
||||
shouldIgnoreRequest: ignoreEngagementRequests,
|
||||
shouldIgnoreRequest: isEngagementEvent,
|
||||
expectedRequests: [{ n: 'pageview' }],
|
||||
refutedRequests: [
|
||||
{
|
||||
|
|
@ -272,7 +273,7 @@ test.describe('form submissions feature is enabled', () => {
|
|||
await ensurePlausibleInitialized(page)
|
||||
await page.click('input[type="submit"]')
|
||||
},
|
||||
shouldIgnoreRequest: pageviewOrEngagementEvent,
|
||||
shouldIgnoreRequest: [isPageviewEvent, isEngagementEvent],
|
||||
expectedRequests: [
|
||||
{
|
||||
n: 'Form Submission',
|
||||
|
|
@ -286,7 +287,7 @@ test.describe('form submissions feature is enabled', () => {
|
|||
await page.fill('input[type="email"]', 'customer@example.com')
|
||||
await page.keyboard.press('Enter')
|
||||
},
|
||||
shouldIgnoreRequest: pageviewOrEngagementEvent,
|
||||
shouldIgnoreRequest: [isPageviewEvent, isEngagementEvent],
|
||||
expectedRequests: [
|
||||
{
|
||||
n: 'Form Submission',
|
||||
|
|
@ -305,9 +306,6 @@ function ensurePlausibleInitialized(page: Page) {
|
|||
return page.waitForFunction(() => (window as any).plausible?.l === true)
|
||||
}
|
||||
|
||||
const pageviewOrEngagementEvent = ({ n }) =>
|
||||
['pageview', 'engagement'].includes(n)
|
||||
|
||||
/**
|
||||
* This is a stub for custom form onsubmit handlers Plausible users may have on their websites.
|
||||
* Overriding onsubmit with a custom handler is common practice in web development for a variety of reasons (mostly UX),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
Tests for plausible-npm.js variant
|
||||
|
||||
Config is set at init(), as we expect consumers to do in production.
|
||||
*/
|
||||
|
||||
import {
|
||||
expectPlausibleInAction,
|
||||
hideAndShowCurrentTab,
|
||||
metaKey,
|
||||
mockRequest,
|
||||
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 = {}) {
|
||||
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")
|
||||
})
|
||||
})
|
||||
|
|
@ -7,14 +7,11 @@ better test the script in isolation of the plausible codebase.
|
|||
|
||||
import {
|
||||
expectPlausibleInAction,
|
||||
hideAndShowCurrentTab,
|
||||
metaKey,
|
||||
mockRequest,
|
||||
e as expecting,
|
||||
ignoreEngagementRequests
|
||||
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',
|
||||
|
|
@ -36,210 +33,11 @@ async function openPage(page, config, options = {}) {
|
|||
}
|
||||
|
||||
test.describe('plausible-web.js', () => {
|
||||
test.beforeEach(({ page }) => {
|
||||
// Mock file download requests
|
||||
mockRequest(page, 'https://awesome.website.com/file.pdf')
|
||||
})
|
||||
|
||||
test('triggers pageview and engagement automatically', async ({ page }) => {
|
||||
await expectPlausibleInAction(page, {
|
||||
action: () => openPage(page, {}),
|
||||
expectedRequests: [{ n: 'pageview', d: 'example.com', u: expecting.stringContaining('plausible-web.html')}]
|
||||
})
|
||||
|
||||
await expectPlausibleInAction(page, {
|
||||
action: () => hideAndShowCurrentTab(page, {delay: 2000}),
|
||||
expectedRequests: [{n: 'engagement', d: 'example.com', u: expecting.stringContaining('plausible-web.html')}],
|
||||
})
|
||||
})
|
||||
|
||||
test('does not trigger any events when `local` config is disabled', async ({ page }) => {
|
||||
await expectPlausibleInAction(page, {
|
||||
action: () => openPage(page, { captureOnLocalhost: false }),
|
||||
expectedRequests: [],
|
||||
refutedRequests: [{ n: 'pageview' }]
|
||||
})
|
||||
})
|
||||
|
||||
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: ignoreEngagementRequests
|
||||
})
|
||||
})
|
||||
|
||||
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: ignoreEngagementRequests
|
||||
})
|
||||
})
|
||||
|
||||
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: [metaKey()] })
|
||||
},
|
||||
expectedRequests: [{ n: 'pageview' } ],
|
||||
refutedRequests: [{ n: 'File Download' }],
|
||||
shouldIgnoreRequest: ignoreEngagementRequests
|
||||
})
|
||||
})
|
||||
|
||||
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('plausible-web.html') },
|
||||
{ n: 'Outbound Link: Click', d: 'example.com', u: expecting.stringContaining('plausible-web.html'), p: { url: 'https://example.com/' } },
|
||||
]
|
||||
})
|
||||
})
|
||||
|
||||
test('tracks file downloads (when feature enabled)', async ({ page }) => {
|
||||
await openPage(page, { fileDownloads: true })
|
||||
|
||||
await expectPlausibleInAction(page, {
|
||||
action: () => page.click('#file-download'),
|
||||
expectedRequests: [{ n: 'File Download', p: { url: 'https://awesome.website.com/file.pdf' } }],
|
||||
shouldIgnoreRequest: ignoreEngagementRequests
|
||||
})
|
||||
})
|
||||
|
||||
test('tracks static custom pageview properties (when feature enabled)', async ({ page }) => {
|
||||
await expectPlausibleInAction(page, {
|
||||
action: async () => {
|
||||
await openPage(page, {}, { skipPlausibleInit: true })
|
||||
await page.evaluate(() => {
|
||||
plausible.init({ 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 page.evaluate(() => {
|
||||
plausible.init({ customProperties: () => ({ "title": document.title }) })
|
||||
})
|
||||
},
|
||||
expectedRequests: [{ n: 'pageview', p: { "title": "plausible-web.js tests" } }]
|
||||
})
|
||||
})
|
||||
|
||||
test('tracks dynamic custom pageview properties with custom events and engagements', async ({ page }) => {
|
||||
await expectPlausibleInAction(page, {
|
||||
action: async () => {
|
||||
await openPage(page, {}, { skipPlausibleInit: true })
|
||||
await page.evaluate(() => {
|
||||
plausible.init({
|
||||
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 page.evaluate(() => {
|
||||
plausible.init({ 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 page.evaluate(() => {
|
||||
plausible.init({ 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 page.evaluate(() => {
|
||||
plausible.init({ 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 page.evaluate(() => {
|
||||
plausible.init({ autoCapturePageviews: false })
|
||||
})
|
||||
await page.click('#manual-pageview')
|
||||
await hideAndShowCurrentTab(page, { delay: 200 })
|
||||
},
|
||||
expectedRequests: [
|
||||
{ n: 'pageview', u: '/:test-plausible-web', d: 'example.com' },
|
||||
{ n: 'engagement', u: '/:test-plausible-web', 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 openPage(page, {})
|
||||
|
||||
await expectPlausibleInAction(page, {
|
||||
action: () => page.click('#tagged-event'),
|
||||
expectedRequests: [{ n: 'Purchase', p: { foo: 'bar' }, $: { currency: 'EUR', amount: '13.32' } }]
|
||||
})
|
||||
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 }) => {
|
||||
|
|
@ -291,28 +89,9 @@ test.describe('plausible-web.js', () => {
|
|||
await expectPlausibleInAction(page, {
|
||||
action: async () => {
|
||||
await openPage(page, {}, { skipPlausibleInit: true })
|
||||
await page.evaluate(() => {
|
||||
plausible.init({
|
||||
domain: 'another-domain.com'
|
||||
})
|
||||
})
|
||||
await callInit(page, { domain: 'another-domain.com' }, 'window.plausible')
|
||||
},
|
||||
expectedRequests: [{ n: 'pageview', d: 'example.com', u: expecting.stringContaining('plausible-web.html') }]
|
||||
})
|
||||
})
|
||||
|
||||
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 page.evaluate(() => {
|
||||
plausible.init({
|
||||
endpoint: 'http://proxy.io/endpoint'
|
||||
})
|
||||
})
|
||||
},
|
||||
expectedRequests: [{ n: 'pageview', d: 'example.com', u: expecting.stringContaining('plausible-web.html')}]
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -0,0 +1,236 @@
|
|||
import {
|
||||
expectPlausibleInAction,
|
||||
hideAndShowCurrentTab,
|
||||
metaKey,
|
||||
mockRequest,
|
||||
e as expecting,
|
||||
isPageviewEvent,
|
||||
isEngagementEvent
|
||||
} from './support/test-utils'
|
||||
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
|
||||
if (config && typeof config.customProperties === 'function') {
|
||||
config.customProperties = { "_wrapFunction": config.customProperties.toString() }
|
||||
}
|
||||
|
||||
await page.evaluate(({ config, parent }) => {
|
||||
if (config && config.customProperties && config.customProperties._wrapFunction) {
|
||||
config.customProperties = new Function(`return (${config.customProperties._wrapFunction})`)();
|
||||
}
|
||||
eval(parent).init(config)
|
||||
}, { config, parent })
|
||||
}
|
||||
|
||||
export function testPlausibleConfiguration({ openPage, initPlausible, fixtureName, fixtureTitle }) {
|
||||
test.describe('shared configuration tests', () => {
|
||||
test.beforeEach(({ page }) => {
|
||||
// Mock file download requests
|
||||
mockRequest(page, 'https://awesome.website.com/file.pdf')
|
||||
})
|
||||
|
||||
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('does not trigger any events when `local` config is disabled', async ({ page }) => {
|
||||
await expectPlausibleInAction(page, {
|
||||
action: () => openPage(page, { captureOnLocalhost: false }),
|
||||
expectedRequests: [],
|
||||
refutedRequests: [{ n: 'pageview' }]
|
||||
})
|
||||
})
|
||||
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: [metaKey()] })
|
||||
},
|
||||
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')
|
||||
await hideAndShowCurrentTab(page, { delay: 200 })
|
||||
},
|
||||
expectedRequests: [
|
||||
{ n: 'pageview', u: '/:test-plausible', d: 'example.com' },
|
||||
{ n: 'engagement', u: '/:test-plausible', 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)}]
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
@ -1,5 +1,8 @@
|
|||
import { Page } from '@playwright/test'
|
||||
|
||||
type RequestData = Record<string, unknown>
|
||||
type ShouldIgnoreRequest = (requestData?: RequestData) => boolean
|
||||
|
||||
export async function mockManyRequests({
|
||||
page,
|
||||
path,
|
||||
|
|
@ -12,13 +15,13 @@ export async function mockManyRequests({
|
|||
path: string
|
||||
numberOfRequests: number
|
||||
responseDelay?: number
|
||||
shouldIgnoreRequest?: (requestData?: Record<string, unknown>) => boolean
|
||||
shouldIgnoreRequest?: ShouldIgnoreRequest | ShouldIgnoreRequest[]
|
||||
mockRequestTimeout?: number
|
||||
}) {
|
||||
const requestList: any[] = []
|
||||
await page.route(path, async (route, request) => {
|
||||
const postData = request.postDataJSON()
|
||||
if (!shouldIgnoreRequest || !shouldIgnoreRequest(postData)) {
|
||||
if (shouldAllow(postData, shouldIgnoreRequest)) {
|
||||
requestList.push(postData)
|
||||
}
|
||||
if (responseDelay) {
|
||||
|
|
@ -51,6 +54,16 @@ export async function mockManyRequests({
|
|||
return getWaitForRequests
|
||||
}
|
||||
|
||||
function shouldAllow(requestData: RequestData, ignores: ShouldIgnoreRequest | ShouldIgnoreRequest[] | undefined) {
|
||||
if (Array.isArray(ignores)) {
|
||||
return !ignores.some((shouldIgnore) => shouldIgnore(requestData))
|
||||
} else if (ignores) {
|
||||
return !ignores(requestData)
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
function delay(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@ export const LOCAL_SERVER_ADDR = `http://localhost:${LOCAL_SERVER_PORT}`
|
|||
export function runLocalFileServer() {
|
||||
app.use(express.static(FIXTURES_PATH));
|
||||
|
||||
app.get('/tracker/js/:name', async (req, res) => {
|
||||
const name = req.params.name
|
||||
app.get('/tracker/js/*', async (req, res) => {
|
||||
const name = req.params[0]
|
||||
const variant = VARIANTS.find((variant) => variant.name === name)
|
||||
|
||||
let code = await compileFile(variant, { returnCode: true })
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ export const metaKey = function() {
|
|||
* is `expectedRequests.length + refutedRequests.length`.
|
||||
* @param {number} [args.expectedRequestCount] - When provided, expects the total amount of
|
||||
* event requests made to match this number.
|
||||
* @param {Function} [args.shouldIgnoreRequest] - When provided, ignores certain requests
|
||||
* @param {Array|Function} [args.shouldIgnoreRequest] - When provided, ignores certain requests
|
||||
* @param {number} [args.responseDelay] - When provided, delays the response from the Plausible
|
||||
* API by the given number of milliseconds.
|
||||
* @param {Function} [args.mockRequestTimeout] - How long to wait for the requests to be made
|
||||
|
|
@ -105,17 +105,18 @@ export const expectPlausibleInAction = async function (page, {
|
|||
const refutedBodySubsetsErrorMessage = `The following requests were made, but were not expected:\n\n${JSON.stringify(refutedButFoundRequestBodies, null, 4)}`
|
||||
expect(refutedButFoundRequestBodies, refutedBodySubsetsErrorMessage).toHaveLength(0)
|
||||
|
||||
expect(requestBodies.length).toBe(requestsToExpect)
|
||||
const unexpectedRequestBodiesErrorMessage = `Expected ${requestsToExpect} requests, but received ${requestBodies.length}:\n\n${JSON.stringify(requestBodies, null, 4)}`
|
||||
expect(requestBodies.length, unexpectedRequestBodiesErrorMessage).toBe(requestsToExpect)
|
||||
|
||||
return requestBodies
|
||||
}
|
||||
|
||||
export const ignoreEngagementRequests = function(requestPostData) {
|
||||
return requestPostData.n === 'engagement'
|
||||
export const isPageviewEvent = function(requestPostData) {
|
||||
return requestPostData.n === 'pageview'
|
||||
}
|
||||
|
||||
export const ignorePageleaveRequests = function(requestPostData) {
|
||||
return requestPostData.n === 'pageleave'
|
||||
export const isEngagementEvent = function(requestPostData) {
|
||||
return requestPostData.n === 'engagement'
|
||||
}
|
||||
|
||||
async function toggleTabVisibility(page, hide) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue