ScriptV2: Improved tracker compile.js (#5363)
* Add CLI arguments to compile.js and logging * Rename folder * Extract compile code, es modules * Add a progress bar * Remove handlebars * Update report-sizes * Remove debug code * inline * More generous split * Allow positional arguments for compiling * Add watch option to compile * Add compileFile logic * Most tests run under playwright * All tests runnable * Update playwright, remove hack Note that upgrading to latest failed due to a new test failure. This might be due to a chrome update. * Compile script on the fly for tests * Minor refactor for compileAll * es module for generate-variants.js * Allow passing suffix to compilation script - this can be used to generate separate files for comparison * Fix positionals * Switch from 2 passes to 1 pass Did some data analysis on this data: - Compared to master, 1 pass increased brotli size by 0.7%, 2 passes 0.4%. Given the change is insignificant enough, we can ignore it for now The increase is likely due to order of operations in compilation and some inlined functions getting lost. * Move customEvents.js to plausible.js * Clean up API * Suffix default * Rework variants.json, globals stored there * Add more variants under test * Distribute work across multiple worker threads Compile time went on my machine from 60s -> 30s * Fixup server * Update canSkipCompile * chore: Bump tracker_script_version to 7 * Update scripts * Update node-version * Experiment with adding a small delay to page * Casing * rename variable * Update help text * features -> compileIds, backport functionality from other branch
This commit is contained in:
parent
34ae68456b
commit
7265d04a8c
|
|
@ -18,7 +18,7 @@ jobs:
|
|||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 16
|
||||
node-version: 23.2.0
|
||||
- name: Install dependencies
|
||||
run: npm --prefix ./tracker ci
|
||||
- name: Install Playwright Browsers
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ npm-debug.log
|
|||
|
||||
# Stored hash of source tracker files used in development environment
|
||||
# to detect changes in /tracker/src and avoid unnecessary compilation.
|
||||
/tracker/dev-compile/last-hash.txt
|
||||
/tracker/compiler/last-hash.txt
|
||||
|
||||
# test coverage directory
|
||||
/assets/coverage
|
||||
|
|
@ -84,7 +84,7 @@ plausible-report.xml
|
|||
/priv/geodb/*.mmdb.gz
|
||||
|
||||
# Auto-generated tracker files
|
||||
/priv/tracker/js/plausible*.js
|
||||
/priv/tracker/js/plausible*.js*
|
||||
|
||||
# Docker volumes
|
||||
.clickhouse_db_vol*
|
||||
|
|
|
|||
|
|
@ -53,6 +53,19 @@ defmodule PlausibleWeb.TrackerTest do
|
|||
assert get_script("script.manual.pageleave.js") == get_script("script.manual.js")
|
||||
end
|
||||
|
||||
for variant <- [
|
||||
"plausible.js",
|
||||
"script.manual.pageview-props.tagged-events.pageleave.js",
|
||||
"script.compat.local.exclusions.js",
|
||||
"script.hash.revenue.file-downloads.pageview-props.js"
|
||||
] do
|
||||
test "variant #{variant} is available" do
|
||||
script = get_script(unquote(variant))
|
||||
assert String.starts_with?(script, "!function(){var")
|
||||
assert String.length(script) > 200
|
||||
end
|
||||
end
|
||||
|
||||
def get_script(filename) do
|
||||
opts = PlausibleWeb.Tracker.init([])
|
||||
|
||||
|
|
|
|||
|
|
@ -1,44 +1,67 @@
|
|||
const uglify = require("uglify-js");
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const Handlebars = require("handlebars");
|
||||
const g = require("generatorics");
|
||||
const { canSkipCompile } = require("./dev-compile/can-skip-compile");
|
||||
const { tracker_script_version } = require("./package.json");
|
||||
import { parseArgs } from 'node:util'
|
||||
import { compileAll } from './compiler/index.js'
|
||||
import chokidar from 'chokidar'
|
||||
|
||||
if (process.env.NODE_ENV === 'dev' && canSkipCompile()) {
|
||||
console.info('COMPILATION SKIPPED: No changes detected in tracker dependencies')
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
Handlebars.registerHelper('any', function (...args) {
|
||||
return args.slice(0, -1).some(Boolean)
|
||||
const { values, positionals } = parseArgs({
|
||||
options: {
|
||||
'target': {
|
||||
type: 'string',
|
||||
},
|
||||
'watch': {
|
||||
type: 'boolean',
|
||||
short: 'w'
|
||||
},
|
||||
'help': {
|
||||
type: 'boolean',
|
||||
},
|
||||
'suffix': {
|
||||
type: 'string',
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
allowPositionals: true
|
||||
})
|
||||
|
||||
Handlebars.registerPartial('customEvents', Handlebars.compile(fs.readFileSync(relPath('src/customEvents.js')).toString()))
|
||||
|
||||
function relPath(segment) {
|
||||
return path.join(__dirname, segment)
|
||||
if (values.help) {
|
||||
console.log('Usage: node compile.js [...compile-ids] [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')
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
function compilefile(input, output, templateVars = {}) {
|
||||
const code = fs.readFileSync(input).toString()
|
||||
const template = Handlebars.compile(code)
|
||||
const rendered = template({ ...templateVars, TRACKER_SCRIPT_VERSION: tracker_script_version })
|
||||
const result = uglify.minify(rendered)
|
||||
if (result.code) {
|
||||
fs.writeFileSync(output, result.code)
|
||||
} else {
|
||||
throw new Error(`Failed to compile ${output.split('/').pop()}.\n${result.error}\n`)
|
||||
function parse(value) {
|
||||
if (value == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
return value
|
||||
.split(/[.,]/)
|
||||
.filter(feature => !['js', 'plausible'].includes(feature))
|
||||
.sort()
|
||||
}
|
||||
|
||||
const base_variants = ["hash", "outbound-links", "exclusions", "compat", "local", "manual", "file-downloads", "pageview-props", "tagged-events", "revenue"]
|
||||
const variants = [...g.clone.powerSet(base_variants)].filter(a => a.length > 0).map(a => a.sort());
|
||||
const compileOptions = {
|
||||
targets: parse(values.target),
|
||||
only: positionals && positionals.length > 0 ? positionals.map(parse) : null,
|
||||
suffix: values.suffix
|
||||
}
|
||||
|
||||
compilefile(relPath('src/plausible.js'), relPath('../priv/tracker/js/plausible.js'))
|
||||
await compileAll(compileOptions)
|
||||
|
||||
variants.map(variant => {
|
||||
const options = variant.map(variant => variant.replace('-', '_')).reduce((acc, curr) => (acc[curr] = true, acc), {})
|
||||
compilefile(relPath('src/plausible.js'), relPath(`../priv/tracker/js/plausible.${variant.join('.')}.js`), options)
|
||||
})
|
||||
if (values.watch) {
|
||||
console.log('Watching src/ directory for changes...')
|
||||
|
||||
chokidar.watch('./src').on('change', async (event, path) => {
|
||||
if (path) {
|
||||
console.log(`\nFile changed: ${path}`)
|
||||
console.log('Recompiling...')
|
||||
|
||||
await compileAll(compileOptions)
|
||||
|
||||
console.log('Done. Watching for changes...')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,17 @@
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import crypto from 'crypto'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
const LAST_HASH_FILEPATH = path.join(__dirname, './last-hash.txt')
|
||||
|
||||
// Re-compilation is only required if any of these files have been changed.
|
||||
// Re-compilation is only required if any of these files have been changed.
|
||||
const COMPILE_DEPENDENCIES = [
|
||||
path.join(__dirname, './index.js'),
|
||||
path.join(__dirname, '../compile.js'),
|
||||
path.join(__dirname, '../src/plausible.js'),
|
||||
path.join(__dirname, '../src/customEvents.js')
|
||||
path.join(__dirname, '../src/plausible.js')
|
||||
]
|
||||
|
||||
function currentHash() {
|
||||
|
|
@ -39,7 +42,7 @@ function lastHash() {
|
|||
* will be updated. Compilation can be skipped if the hash hasn't changed since
|
||||
* the last execution.
|
||||
*/
|
||||
exports.canSkipCompile = function() {
|
||||
export function canSkipCompile() {
|
||||
const current = currentHash()
|
||||
const last = lastHash()
|
||||
|
||||
|
|
@ -49,4 +52,4 @@ exports.canSkipCompile = function() {
|
|||
fs.writeFileSync(LAST_HASH_FILEPATH, current)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
import g from 'generatorics'
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
function idToGlobal(id) {
|
||||
return `COMPILE_${id.replace('-', '_').toUpperCase()}`
|
||||
}
|
||||
|
||||
const LEGACY_VARIANT_NAMES = ["hash", "outbound-links", "exclusions", "compat", "local", "manual", "file-downloads", "pageview-props", "tagged-events", "revenue"]
|
||||
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]))
|
||||
}))
|
||||
|
||||
const variantsFile = path.join(__dirname, 'variants.json')
|
||||
const existingData = JSON.parse(fs.readFileSync(variantsFile, 'utf8'))
|
||||
|
||||
fs.writeFileSync(variantsFile, JSON.stringify({ ...existingData, legacyVariants }, null, 2) + "\n")
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
import uglify from 'uglify-js'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
import variantsFile from './variants.json' with { type: 'json' }
|
||||
import { canSkipCompile } from './can-skip-compile.js'
|
||||
import packageJson from '../package.json' with { type: 'json' }
|
||||
import progress from 'cli-progress'
|
||||
import { spawn, Worker, Pool } from "threads"
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
const DEFAULT_GLOBALS = {
|
||||
COMPILE_HASH: false,
|
||||
COMPILE_OUTBOUND_LINKS: false,
|
||||
COMPILE_EXCLUSIONS: false,
|
||||
COMPILE_COMPAT: false,
|
||||
COMPILE_LOCAL: false,
|
||||
COMPILE_MANUAL: false,
|
||||
COMPILE_FILE_DOWNLOADS: false,
|
||||
COMPILE_PAGEVIEW_PROPS: false,
|
||||
COMPILE_TAGGED_EVENTS: false,
|
||||
COMPILE_REVENUE: false,
|
||||
COMPILE_TRACKER_SCRIPT_VERSION: packageJson.tracker_script_version
|
||||
}
|
||||
|
||||
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 baseCode = getCode()
|
||||
|
||||
const startTime = Date.now();
|
||||
console.log(`Starting compilation of ${variants.length} variants...`)
|
||||
|
||||
const bar = new progress.SingleBar({ clearOnComplete: true }, progress.Presets.shades_classic)
|
||||
bar.start(variants.length, 0)
|
||||
|
||||
const workerPool = Pool(() => spawn(new Worker('./worker-thread.js')))
|
||||
variants.forEach(variant => {
|
||||
workerPool.queue(async (worker) => {
|
||||
await worker.compileFile(variant, { ...options, baseCode })
|
||||
bar.increment()
|
||||
})
|
||||
})
|
||||
|
||||
await workerPool.completed()
|
||||
await workerPool.terminate()
|
||||
bar.stop()
|
||||
|
||||
console.log(`Completed compilation of ${variants.length} variants in ${((Date.now() - startTime) / 1000).toFixed(2)}s`);
|
||||
}
|
||||
|
||||
export function compileFile(variant, options) {
|
||||
const baseCode = options.baseCode || getCode()
|
||||
const globals = { ...DEFAULT_GLOBALS, ...variant.globals }
|
||||
|
||||
const code = minify(baseCode, globals)
|
||||
|
||||
if (options.returnCode) {
|
||||
return code
|
||||
} else {
|
||||
fs.writeFileSync(relPath(`../../priv/tracker/js/${variant.name}${options.suffix || ""}`), code)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
function getCode() {
|
||||
// Wrap the code in an instantly evaluating function
|
||||
return `(function(){${fs.readFileSync(relPath('../src/plausible.js')).toString()}})()`
|
||||
}
|
||||
|
||||
function minify(baseCode, globals) {
|
||||
const result = uglify.minify(baseCode, {
|
||||
compress: {
|
||||
global_defs: globals
|
||||
}
|
||||
})
|
||||
|
||||
if (result.code) {
|
||||
return result.code
|
||||
} else {
|
||||
throw result.error
|
||||
}
|
||||
}
|
||||
|
||||
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,4 @@
|
|||
import { compileFile } from './index.js'
|
||||
import { expose } from "threads/worker"
|
||||
|
||||
expose({ compileFile })
|
||||
|
|
@ -6,16 +6,18 @@
|
|||
"": {
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chokidar": "^4.0.3",
|
||||
"generatorics": "^1.1.0",
|
||||
"handlebars": "^4.7.8",
|
||||
"uglify-js": "^3.19.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.48.1",
|
||||
"@playwright/test": "^1.49.1",
|
||||
"@types/node": "^22.13.4",
|
||||
"cli-progress": "^3.12.0",
|
||||
"eslint": "^9.20.1",
|
||||
"eslint-plugin-playwright": "^2.2.0",
|
||||
"express": "^4.21.2"
|
||||
"express": "^4.21.2",
|
||||
"threads": "^1.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aashutoshrathi/word-wrap": {
|
||||
|
|
@ -205,12 +207,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.48.1",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.48.1.tgz",
|
||||
"integrity": "sha512-s9RtWoxkOLmRJdw3oFvhFbs9OJS0BzrLUc8Hf6l2UdCNd1rqeEyD4BhCJkvzeEoD1FsK4mirsWwGerhVmYKtZg==",
|
||||
"version": "1.49.1",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.1.tgz",
|
||||
"integrity": "sha512-Ky+BVzPz8pL6PQxHqNRW1k3mIyv933LML7HktS8uik0bUXNCdPhoS/kLihiO1tMf/egaJb4IutXd7UywvXEW+g==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright": "1.48.1"
|
||||
"playwright": "1.49.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
|
|
@ -290,6 +293,16 @@
|
|||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
|
|
@ -435,6 +448,34 @@
|
|||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"readdirp": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.16.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/cli-progress": {
|
||||
"version": "3.12.0",
|
||||
"resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.12.0.tgz",
|
||||
"integrity": "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
|
|
@ -571,6 +612,13 @@
|
|||
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/encodeurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||
|
|
@ -745,6 +793,17 @@
|
|||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/esm": {
|
||||
"version": "3.2.25",
|
||||
"resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz",
|
||||
"integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/espree": {
|
||||
"version": "10.3.0",
|
||||
"resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz",
|
||||
|
|
@ -1008,6 +1067,7 @@
|
|||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
|
|
@ -1109,26 +1169,6 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/handlebars": {
|
||||
"version": "4.7.8",
|
||||
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz",
|
||||
"integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==",
|
||||
"dependencies": {
|
||||
"minimist": "^1.2.5",
|
||||
"neo-async": "^2.6.2",
|
||||
"source-map": "^0.6.1",
|
||||
"wordwrap": "^1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"handlebars": "bin/handlebars"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.7"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"uglify-js": "^3.1.4"
|
||||
}
|
||||
},
|
||||
"node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
|
|
@ -1248,6 +1288,16 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-glob": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||
|
|
@ -1260,6 +1310,19 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-observable": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-observable/-/is-observable-2.1.0.tgz",
|
||||
"integrity": "sha512-DailKdLb0WU+xX8K5w7VsJhapwHLZ9jjmazqCJq4X12CTgqq73TKnbRcnSLuXYPOoLQgV5IrD7ePiX/h1vnkBw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
|
|
@ -1420,14 +1483,6 @@
|
|||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
|
|
@ -1449,11 +1504,6 @@
|
|||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/neo-async": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
|
||||
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="
|
||||
},
|
||||
"node_modules/object-inspect": {
|
||||
"version": "1.13.4",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||
|
|
@ -1466,6 +1516,13 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/observable-fns": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/observable-fns/-/observable-fns-0.6.1.tgz",
|
||||
"integrity": "sha512-9gRK4+sRWzeN6AOewNBTLXir7Zl/i3GB6Yl26gK4flxz8BXVpD3kt8amREmWNb0mxYOGDotvE5a4N+PtGGKdkg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/on-finished": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||
|
|
@ -1495,11 +1552,27 @@
|
|||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/p-limit": {
|
||||
"node_modules/p-locate": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
|
||||
"integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"p-limit": "^3.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/p-locate/node_modules/p-limit": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
|
||||
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"yocto-queue": "^0.1.0"
|
||||
},
|
||||
|
|
@ -1510,14 +1583,12 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/p-locate": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
|
||||
"integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
|
||||
"node_modules/p-locate/node_modules/yocto-queue": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"p-limit": "^3.0.2"
|
||||
},
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
|
|
@ -1571,12 +1642,13 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.48.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.48.1.tgz",
|
||||
"integrity": "sha512-j8CiHW/V6HxmbntOfyB4+T/uk08tBy6ph0MpBXwuoofkSnLmlfdYNNkFTYD6ofzzlSqLA1fwH4vwvVFvJgLN0w==",
|
||||
"version": "1.49.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.1.tgz",
|
||||
"integrity": "sha512-VYL8zLoNTBxVOrJBbDuRgDWa3i+mfQgDTrL8Ah9QXZ7ax4Dsj0MSq5bYgytRnDVVe+njoKnfsYkH3HzqVj5UZA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.48.1"
|
||||
"playwright-core": "1.49.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
|
|
@ -1589,10 +1661,11 @@
|
|||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.48.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.48.1.tgz",
|
||||
"integrity": "sha512-Yw/t4VAFX/bBr1OzwCuOMZkY1Cnb4z/doAFSwf4huqAGWmf9eMNjmK7NiOljCdLmxeRYcGPPmcDgU0zOlzP0YA==",
|
||||
"version": "1.49.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.1.tgz",
|
||||
"integrity": "sha512-BzmpVcs4kE2CH15rWfzpjzVGhWERJfmnXmniSyKeRZUs9Ws65m+RGIi7mjJK/euCegfn3i7jvqWeWyHe9y3Vgg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
|
|
@ -1670,6 +1743,19 @@
|
|||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14.18.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "individual",
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-from": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
||||
|
|
@ -1867,14 +1953,6 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
||||
|
|
@ -1884,6 +1962,34 @@
|
|||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-json-comments": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
|
||||
|
|
@ -1908,6 +2014,36 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/threads": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/threads/-/threads-1.7.0.tgz",
|
||||
"integrity": "sha512-Mx5NBSHX3sQYR6iI9VYbgHKBLisyB+xROCBGjjWm1O9wb9vfLxdaGtmT/KCjUqMsSNW6nERzCW3T6H43LqjDZQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"callsites": "^3.1.0",
|
||||
"debug": "^4.2.0",
|
||||
"is-observable": "^2.1.0",
|
||||
"observable-fns": "^0.6.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/andywer/threads.js?sponsor=1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"tiny-worker": ">= 2"
|
||||
}
|
||||
},
|
||||
"node_modules/tiny-worker": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/tiny-worker/-/tiny-worker-2.3.0.tgz",
|
||||
"integrity": "sha512-pJ70wq5EAqTAEl9IkGzA+fN0836rycEuz2Cn6yeZ6FRzlVS5IDOkFHpIoEsksPRQV34GDqXm65+OlnZqUSyK2g==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"esm": "^3.2.25"
|
||||
}
|
||||
},
|
||||
"node_modules/toidentifier": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||
|
|
@ -2021,23 +2157,6 @@
|
|||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/wordwrap": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
|
||||
"integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q=="
|
||||
},
|
||||
"node_modules/yocto-queue": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,23 +1,26 @@
|
|||
{
|
||||
"tracker_script_version": 6,
|
||||
"tracker_script_version": 7,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"deploy": "node compile.js",
|
||||
"test": "npm run deploy && npx playwright test",
|
||||
"test:local": "NODE_ENV=dev npm run deploy && npx playwright test",
|
||||
"test": "npx playwright test",
|
||||
"test:local": "npx playwright test",
|
||||
"report-sizes": "node report-sizes.js",
|
||||
"start": "node test/support/server.js"
|
||||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chokidar": "^4.0.3",
|
||||
"generatorics": "^1.1.0",
|
||||
"handlebars": "^4.7.8",
|
||||
"uglify-js": "^3.19.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"express": "^4.21.2",
|
||||
"@playwright/test": "^1.48.1",
|
||||
"@playwright/test": "^1.49.1",
|
||||
"@types/node": "^22.13.4",
|
||||
"cli-progress": "^3.12.0",
|
||||
"eslint": "^9.20.1",
|
||||
"eslint-plugin-playwright": "^2.2.0"
|
||||
"eslint-plugin-playwright": "^2.2.0",
|
||||
"express": "^4.21.2",
|
||||
"threads": "^1.7.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
// @ts-check
|
||||
const { defineConfig, devices } = require('@playwright/test');
|
||||
import { defineConfig, devices } from '@playwright/test'
|
||||
|
||||
/**
|
||||
* @see https://playwright.dev/docs/test-configuration
|
||||
*/
|
||||
module.exports = defineConfig({
|
||||
export default defineConfig({
|
||||
testDir: './test',
|
||||
timeout: 60 * 1000,
|
||||
fullyParallel: true,
|
||||
|
|
@ -37,4 +37,3 @@ module.exports = defineConfig({
|
|||
reuseExistingServer: !process.env.CI
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { execSync } from 'child_process'
|
||||
|
||||
const PrivTrackerDir = '../priv/tracker/js/';
|
||||
const PrivTrackerDir = '../priv/tracker/js/'
|
||||
|
||||
const toReport = [
|
||||
'plausible.js',
|
||||
|
|
|
|||
|
|
@ -1,230 +0,0 @@
|
|||
function getLinkEl(link) {
|
||||
while (link && (typeof link.tagName === 'undefined' || !isLink(link) || !link.href)) {
|
||||
link = link.parentNode
|
||||
}
|
||||
return link
|
||||
}
|
||||
|
||||
function isLink(element) {
|
||||
return element && element.tagName && element.tagName.toLowerCase() === 'a'
|
||||
}
|
||||
|
||||
function shouldFollowLink(event, link) {
|
||||
// If default has been prevented by an external script, Plausible should not intercept navigation.
|
||||
if (event.defaultPrevented) { return false }
|
||||
|
||||
var targetsCurrentWindow = !link.target || link.target.match(/^_(self|parent|top)$/i)
|
||||
var isRegularClick = !(event.ctrlKey || event.metaKey || event.shiftKey) && event.type === 'click'
|
||||
return targetsCurrentWindow && isRegularClick
|
||||
}
|
||||
|
||||
var MIDDLE_MOUSE_BUTTON = 1
|
||||
|
||||
function handleLinkClickEvent(event) {
|
||||
if (event.type === 'auxclick' && event.button !== MIDDLE_MOUSE_BUTTON) { return }
|
||||
|
||||
var link = getLinkEl(event.target)
|
||||
var hrefWithoutQuery = link && link.href && link.href.split('?')[0]
|
||||
|
||||
{{#if tagged_events}}
|
||||
if (isElementOrParentTagged(link, 0)) {
|
||||
// Return to prevent sending multiple events with the same action.
|
||||
// Clicks on tagged links are handled by another function.
|
||||
return
|
||||
}
|
||||
{{/if}}
|
||||
|
||||
{{#if outbound_links}}
|
||||
if (isOutboundLink(link)) {
|
||||
return sendLinkClickEvent(event, link, { name: 'Outbound Link: Click', props: { url: link.href } })
|
||||
}
|
||||
{{/if}}
|
||||
|
||||
{{#if file_downloads}}
|
||||
if (isDownloadToTrack(hrefWithoutQuery)) {
|
||||
return sendLinkClickEvent(event, link, { name: 'File Download', props: { url: hrefWithoutQuery } })
|
||||
}
|
||||
{{/if}}
|
||||
}
|
||||
|
||||
function sendLinkClickEvent(event, link, eventAttrs) {
|
||||
var followedLink = false
|
||||
|
||||
function followLink() {
|
||||
if (!followedLink) {
|
||||
followedLink = true
|
||||
window.location = link.href
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldFollowLink(event, link)) {
|
||||
var attrs = { props: eventAttrs.props, callback: followLink }
|
||||
{{#if revenue}}
|
||||
attrs.revenue = eventAttrs.revenue
|
||||
{{/if}}
|
||||
plausible(eventAttrs.name, attrs)
|
||||
setTimeout(followLink, 5000)
|
||||
event.preventDefault()
|
||||
} else {
|
||||
var attrs = { props: eventAttrs.props }
|
||||
{{#if revenue}}
|
||||
attrs.revenue = eventAttrs.revenue
|
||||
{{/if}}
|
||||
plausible(eventAttrs.name, attrs)
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('click', handleLinkClickEvent)
|
||||
document.addEventListener('auxclick', handleLinkClickEvent)
|
||||
|
||||
{{#if outbound_links}}
|
||||
function isOutboundLink(link) {
|
||||
return link && link.href && link.host && link.host !== location.host
|
||||
}
|
||||
{{/if}}
|
||||
|
||||
{{#if file_downloads}}
|
||||
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 fileTypesAttr = scriptEl.getAttribute('file-types')
|
||||
var addFileTypesAttr = scriptEl.getAttribute('add-file-types')
|
||||
var fileTypesToTrack = (fileTypesAttr && fileTypesAttr.split(",")) || (addFileTypesAttr && addFileTypesAttr.split(",").concat(defaultFileTypes)) || defaultFileTypes;
|
||||
|
||||
function isDownloadToTrack(url) {
|
||||
if (!url) { return false }
|
||||
|
||||
var fileType = url.split('.').pop();
|
||||
return fileTypesToTrack.some(function (fileTypeToTrack) {
|
||||
return fileTypeToTrack === fileType
|
||||
})
|
||||
}
|
||||
{{/if}}
|
||||
|
||||
{{#if tagged_events}}
|
||||
// Finds event attributes by iterating over the given element's (or its
|
||||
// parent's) classList. Returns an object with `name` and `props` keys.
|
||||
function getTaggedEventAttributes(htmlElement) {
|
||||
var taggedElement = isTagged(htmlElement) ? htmlElement : htmlElement && htmlElement.parentNode
|
||||
var eventAttrs = { name: null, props: {} }
|
||||
{{#if revenue}}
|
||||
eventAttrs.revenue = {}
|
||||
{{/if}}
|
||||
|
||||
var classList = taggedElement && taggedElement.classList
|
||||
if (!classList) { return eventAttrs }
|
||||
|
||||
for (var i = 0; i < classList.length; i++) {
|
||||
var className = classList.item(i)
|
||||
|
||||
var matchList = className.match(/plausible-event-(.+)(=|--)(.+)/)
|
||||
if (matchList) {
|
||||
var key = matchList[1]
|
||||
var value = matchList[3].replace(/\+/g, ' ')
|
||||
|
||||
if (key.toLowerCase() == 'name') {
|
||||
eventAttrs.name = value
|
||||
} else {
|
||||
eventAttrs.props[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
{{#if revenue}}
|
||||
var revenueMatchList = className.match(/plausible-revenue-(.+)(=|--)(.+)/)
|
||||
if (revenueMatchList) {
|
||||
var key = revenueMatchList[1]
|
||||
var value = revenueMatchList[3]
|
||||
eventAttrs.revenue[key] = value
|
||||
}
|
||||
{{/if}}
|
||||
}
|
||||
|
||||
return eventAttrs
|
||||
}
|
||||
|
||||
function handleTaggedFormSubmitEvent(event) {
|
||||
var form = event.target
|
||||
var eventAttrs = getTaggedEventAttributes(form)
|
||||
if (!eventAttrs.name) { return }
|
||||
|
||||
event.preventDefault()
|
||||
var formSubmitted = false
|
||||
|
||||
function submitForm() {
|
||||
if (!formSubmitted) {
|
||||
formSubmitted = true
|
||||
form.submit()
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(submitForm, 5000)
|
||||
|
||||
var attrs = { props: eventAttrs.props, callback: submitForm }
|
||||
{{#if revenue}}
|
||||
attrs.revenue = eventAttrs.revenue
|
||||
{{/if}}
|
||||
plausible(eventAttrs.name, attrs)
|
||||
}
|
||||
|
||||
function isForm(element) {
|
||||
return element && element.tagName && element.tagName.toLowerCase() === 'form'
|
||||
}
|
||||
|
||||
var PARENTS_TO_SEARCH_LIMIT = 3
|
||||
|
||||
function handleTaggedElementClickEvent(event) {
|
||||
if (event.type === 'auxclick' && event.button !== MIDDLE_MOUSE_BUTTON) { return }
|
||||
|
||||
var clicked = event.target
|
||||
|
||||
var clickedLink
|
||||
var taggedElement
|
||||
// Iterate over parents to find the tagged element. Also search for
|
||||
// a link element to call for different tracking behavior if found.
|
||||
for (var i = 0; i <= PARENTS_TO_SEARCH_LIMIT; i++) {
|
||||
if (!clicked) { break }
|
||||
|
||||
// Clicks inside forms are not tracked. Only form submits are.
|
||||
if (isForm(clicked)) { return }
|
||||
if (isLink(clicked)) { clickedLink = clicked }
|
||||
if (isTagged(clicked)) { taggedElement = clicked }
|
||||
clicked = clicked.parentNode
|
||||
}
|
||||
|
||||
if (taggedElement) {
|
||||
var eventAttrs = getTaggedEventAttributes(taggedElement)
|
||||
|
||||
if (clickedLink) {
|
||||
// if the clicked tagged element is a link, we attach the `url` property
|
||||
// automatically for user convenience
|
||||
eventAttrs.props.url = clickedLink.href
|
||||
sendLinkClickEvent(event, clickedLink, eventAttrs)
|
||||
} else {
|
||||
var attrs = {}
|
||||
attrs.props = eventAttrs.props
|
||||
{{#if revenue}}
|
||||
attrs.revenue = eventAttrs.revenue
|
||||
{{/if}}
|
||||
plausible(eventAttrs.name, attrs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isTagged(element) {
|
||||
var classList = element && element.classList
|
||||
if (classList) {
|
||||
for (var i = 0; i < classList.length; i++) {
|
||||
if (classList.item(i).match(/plausible-event-name(=|--)(.+)/)) { return true }
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function isElementOrParentTagged(element, parentsChecked) {
|
||||
if (!element || parentsChecked > PARENTS_TO_SEARCH_LIMIT) { return false }
|
||||
if (isTagged(element)) { return true }
|
||||
return isElementOrParentTagged(element.parentNode, parentsChecked + 1)
|
||||
}
|
||||
|
||||
document.addEventListener('submit', handleTaggedFormSubmitEvent)
|
||||
document.addEventListener('click', handleTaggedElementClickEvent)
|
||||
document.addEventListener('auxclick', handleTaggedElementClickEvent)
|
||||
{{/if}}
|
||||
|
|
@ -1,15 +1,14 @@
|
|||
(function(){
|
||||
'use strict';
|
||||
|
||||
var location = window.location
|
||||
var document = window.document
|
||||
|
||||
{{#if compat}}
|
||||
if (COMPILE_COMPAT) {
|
||||
var scriptEl = document.getElementById('plausible');
|
||||
{{else}}
|
||||
} else {
|
||||
var scriptEl = document.currentScript;
|
||||
{{/if}}
|
||||
var endpoint = scriptEl.getAttribute('data-api') || defaultEndpoint(scriptEl)
|
||||
}
|
||||
var endpoint = scriptEl.getAttribute('data-api') || defaultEndpoint()
|
||||
var dataDomain = scriptEl.getAttribute('data-domain')
|
||||
|
||||
function onIgnoredEvent(eventName, reason, options) {
|
||||
|
|
@ -21,15 +20,15 @@
|
|||
}
|
||||
}
|
||||
|
||||
function defaultEndpoint(el) {
|
||||
{{#if compat}}
|
||||
var pathArray = el.src.split( '/' );
|
||||
function defaultEndpoint() {
|
||||
if (COMPILE_COMPAT) {
|
||||
var pathArray = scriptEl.src.split( '/' );
|
||||
var protocol = pathArray[0];
|
||||
var host = pathArray[2];
|
||||
return protocol + '//' + host + '/api/event';
|
||||
{{else}}
|
||||
return new URL(el.src).origin + '/api/event'
|
||||
{{/if}}
|
||||
} else {
|
||||
return new URL(scriptEl.src).origin + '/api/event'
|
||||
}
|
||||
}
|
||||
|
||||
var currentEngagementIgnored
|
||||
|
|
@ -127,16 +126,16 @@
|
|||
u: currentEngagementURL,
|
||||
p: currentEngagementProps,
|
||||
e: engagementTime,
|
||||
v: {{TRACKER_SCRIPT_VERSION}}
|
||||
v: COMPILE_TRACKER_SCRIPT_VERSION
|
||||
}
|
||||
|
||||
// Reset current engagement time metrics. They will restart upon when page becomes visible or the next SPA pageview
|
||||
runningEngagementStart = null
|
||||
currentEngagementTime = 0
|
||||
|
||||
{{#if hash}}
|
||||
if (COMPILE_HASH) {
|
||||
payload.h = 1
|
||||
{{/if}}
|
||||
}
|
||||
|
||||
sendRequest(endpoint, payload)
|
||||
}
|
||||
|
|
@ -176,14 +175,14 @@
|
|||
maxScrollDepthPx = getCurrentScrollDepthPx()
|
||||
}
|
||||
|
||||
{{#unless local}}
|
||||
if (!COMPILE_LOCAL) {
|
||||
if (/^localhost$|^127(\.[0-9]+){0,2}\.[0-9]+$|^\[::1?\]$/.test(location.hostname) || location.protocol === 'file:') {
|
||||
return onIgnoredEvent(eventName, 'localhost', options)
|
||||
}
|
||||
if ((window._phantom || window.__nightmare || window.navigator.webdriver || window.Cypress) && !window.__plausible) {
|
||||
return onIgnoredEvent(eventName, null, options)
|
||||
}
|
||||
{{/unless}}
|
||||
}
|
||||
try {
|
||||
if (window.localStorage.plausible_ignore === 'true') {
|
||||
return onIgnoredEvent(eventName, 'localStorage flag', options)
|
||||
|
|
@ -191,7 +190,7 @@
|
|||
} catch (e) {
|
||||
|
||||
}
|
||||
{{#if exclusions}}
|
||||
if (COMPILE_EXCLUSIONS) {
|
||||
var dataIncludeAttr = scriptEl && scriptEl.getAttribute('data-include')
|
||||
var dataExcludeAttr = scriptEl && scriptEl.getAttribute('data-exclude')
|
||||
|
||||
|
|
@ -205,25 +204,25 @@
|
|||
function pathMatches(wildcardPath) {
|
||||
var actualPath = location.pathname
|
||||
|
||||
{{#if hash}}
|
||||
if (COMPILE_HASH) {
|
||||
actualPath += location.hash
|
||||
{{/if}}
|
||||
}
|
||||
|
||||
return actualPath.match(new RegExp('^' + wildcardPath.trim().replace(/\*\*/g, '.*').replace(/([^\.])\*/g, '$1[^\\s\/]*') + '\/?$'))
|
||||
}
|
||||
{{/if}}
|
||||
}
|
||||
|
||||
var payload = {}
|
||||
payload.n = eventName
|
||||
payload.v = {{TRACKER_SCRIPT_VERSION}}
|
||||
payload.v = COMPILE_TRACKER_SCRIPT_VERSION
|
||||
|
||||
{{#if manual}}
|
||||
if (COMPILE_MANUAL) {
|
||||
var customURL = options && options.u
|
||||
|
||||
payload.u = customURL ? customURL : location.href
|
||||
{{else}}
|
||||
} else {
|
||||
payload.u = location.href
|
||||
{{/if}}
|
||||
}
|
||||
|
||||
payload.d = dataDomain
|
||||
payload.r = document.referrer || null
|
||||
|
|
@ -236,13 +235,13 @@
|
|||
if (options && options.interactive === false) {
|
||||
payload.i = false
|
||||
}
|
||||
{{#if revenue}}
|
||||
if (COMPILE_REVENUE) {
|
||||
if (options && options.revenue) {
|
||||
payload.$ = options.revenue
|
||||
}
|
||||
{{/if}}
|
||||
}
|
||||
|
||||
{{#if pageview_props}}
|
||||
if (COMPILE_PAGEVIEW_PROPS) {
|
||||
var propAttributes = scriptEl.getAttributeNames().filter(function (name) {
|
||||
return name.substring(0, 6) === 'event-'
|
||||
})
|
||||
|
|
@ -256,11 +255,11 @@
|
|||
})
|
||||
|
||||
payload.p = props
|
||||
{{/if}}
|
||||
}
|
||||
|
||||
{{#if hash}}
|
||||
if (COMPILE_HASH) {
|
||||
payload.h = 1
|
||||
{{/if}}
|
||||
}
|
||||
|
||||
if (isPageview) {
|
||||
currentEngagementIgnored = false
|
||||
|
|
@ -276,7 +275,7 @@
|
|||
}
|
||||
|
||||
function sendRequest(endpoint, payload, options) {
|
||||
{{#if compat}}
|
||||
if (COMPILE_COMPAT) {
|
||||
var request = new XMLHttpRequest();
|
||||
request.open('POST', endpoint, true);
|
||||
request.setRequestHeader('Content-Type', 'text/plain');
|
||||
|
|
@ -288,7 +287,7 @@
|
|||
options && options.callback && options.callback({status: request.status})
|
||||
}
|
||||
}
|
||||
{{else}}
|
||||
} else {
|
||||
if (window.fetch) {
|
||||
fetch(endpoint, {
|
||||
method: 'POST',
|
||||
|
|
@ -301,7 +300,7 @@
|
|||
options && options.callback && options.callback({status: response.status})
|
||||
}).catch(function() {})
|
||||
}
|
||||
{{/if}}
|
||||
}
|
||||
}
|
||||
|
||||
var queue = (window.plausible && window.plausible.q) || []
|
||||
|
|
@ -310,13 +309,13 @@
|
|||
trigger.apply(this, queue[i])
|
||||
}
|
||||
|
||||
{{#unless manual}}
|
||||
if (!COMPILE_MANUAL) {
|
||||
var lastPage;
|
||||
|
||||
function page(isSPANavigation) {
|
||||
{{#unless hash}}
|
||||
if (!COMPILE_HASH) {
|
||||
if (isSPANavigation && lastPage === location.pathname) return;
|
||||
{{/unless}}
|
||||
}
|
||||
|
||||
lastPage = location.pathname
|
||||
trigger('pageview')
|
||||
|
|
@ -324,9 +323,9 @@
|
|||
|
||||
var onSPANavigation = function() {page(true)}
|
||||
|
||||
{{#if hash}}
|
||||
if (COMPILE_HASH) {
|
||||
window.addEventListener('hashchange', onSPANavigation)
|
||||
{{else}}
|
||||
} else {
|
||||
var his = window.history
|
||||
if (his.pushState) {
|
||||
var originalPushState = his['pushState']
|
||||
|
|
@ -336,7 +335,7 @@
|
|||
}
|
||||
window.addEventListener('popstate', onSPANavigation)
|
||||
}
|
||||
{{/if}}
|
||||
}
|
||||
|
||||
function handleVisibilityChange() {
|
||||
if (!lastPage && document.visibilityState === 'visible') {
|
||||
|
|
@ -356,9 +355,237 @@
|
|||
page();
|
||||
}
|
||||
})
|
||||
{{/unless}}
|
||||
}
|
||||
|
||||
{{#if (any outbound_links file_downloads tagged_events)}}
|
||||
{{> customEvents}}
|
||||
{{/if}}
|
||||
})();
|
||||
if (COMPILE_OUTBOUND_LINKS || COMPILE_FILE_DOWNLOADS || COMPILE_TAGGED_EVENTS) {
|
||||
function getLinkEl(link) {
|
||||
while (link && (typeof link.tagName === 'undefined' || !isLink(link) || !link.href)) {
|
||||
link = link.parentNode
|
||||
}
|
||||
return link
|
||||
}
|
||||
|
||||
function isLink(element) {
|
||||
return element && element.tagName && element.tagName.toLowerCase() === 'a'
|
||||
}
|
||||
|
||||
function shouldFollowLink(event, link) {
|
||||
// If default has been prevented by an external script, Plausible should not intercept navigation.
|
||||
if (event.defaultPrevented) { return false }
|
||||
|
||||
var targetsCurrentWindow = !link.target || link.target.match(/^_(self|parent|top)$/i)
|
||||
var isRegularClick = !(event.ctrlKey || event.metaKey || event.shiftKey) && event.type === 'click'
|
||||
return targetsCurrentWindow && isRegularClick
|
||||
}
|
||||
|
||||
var MIDDLE_MOUSE_BUTTON = 1
|
||||
|
||||
function handleLinkClickEvent(event) {
|
||||
if (event.type === 'auxclick' && event.button !== MIDDLE_MOUSE_BUTTON) { return }
|
||||
|
||||
var link = getLinkEl(event.target)
|
||||
var hrefWithoutQuery = link && link.href && link.href.split('?')[0]
|
||||
|
||||
if (COMPILE_TAGGED_EVENTS) {
|
||||
if (isElementOrParentTagged(link, 0)) {
|
||||
// Return to prevent sending multiple events with the same action.
|
||||
// Clicks on tagged links are handled by another function.
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (COMPILE_OUTBOUND_LINKS) {
|
||||
if (isOutboundLink(link)) {
|
||||
return sendLinkClickEvent(event, link, { name: 'Outbound Link: Click', props: { url: link.href } })
|
||||
}
|
||||
}
|
||||
|
||||
if (COMPILE_FILE_DOWNLOADS) {
|
||||
if (isDownloadToTrack(hrefWithoutQuery)) {
|
||||
return sendLinkClickEvent(event, link, { name: 'File Download', props: { url: hrefWithoutQuery } })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function sendLinkClickEvent(event, link, eventAttrs) {
|
||||
var followedLink = false
|
||||
|
||||
function followLink() {
|
||||
if (!followedLink) {
|
||||
followedLink = true
|
||||
window.location = link.href
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldFollowLink(event, link)) {
|
||||
var attrs = { props: eventAttrs.props, callback: followLink }
|
||||
if (COMPILE_REVENUE) {
|
||||
attrs.revenue = eventAttrs.revenue
|
||||
}
|
||||
plausible(eventAttrs.name, attrs)
|
||||
setTimeout(followLink, 5000)
|
||||
event.preventDefault()
|
||||
} else {
|
||||
var attrs = { props: eventAttrs.props }
|
||||
if (COMPILE_REVENUE) {
|
||||
attrs.revenue = eventAttrs.revenue
|
||||
}
|
||||
plausible(eventAttrs.name, attrs)
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('click', handleLinkClickEvent)
|
||||
document.addEventListener('auxclick', handleLinkClickEvent)
|
||||
|
||||
if (COMPILE_OUTBOUND_LINKS) {
|
||||
function isOutboundLink(link) {
|
||||
return link && link.href && link.host && link.host !== location.host
|
||||
}
|
||||
}
|
||||
|
||||
if (COMPILE_FILE_DOWNLOADS) {
|
||||
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 fileTypesAttr = scriptEl.getAttribute('file-types')
|
||||
var addFileTypesAttr = scriptEl.getAttribute('add-file-types')
|
||||
var fileTypesToTrack = (fileTypesAttr && fileTypesAttr.split(",")) || (addFileTypesAttr && addFileTypesAttr.split(",").concat(defaultFileTypes)) || defaultFileTypes;
|
||||
|
||||
function isDownloadToTrack(url) {
|
||||
if (!url) { return false }
|
||||
|
||||
var fileType = url.split('.').pop();
|
||||
return fileTypesToTrack.some(function (fileTypeToTrack) {
|
||||
return fileTypeToTrack === fileType
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (COMPILE_TAGGED_EVENTS) {
|
||||
// Finds event attributes by iterating over the given element's (or its
|
||||
// parent's) classList. Returns an object with `name` and `props` keys.
|
||||
function getTaggedEventAttributes(htmlElement) {
|
||||
var taggedElement = isTagged(htmlElement) ? htmlElement : htmlElement && htmlElement.parentNode
|
||||
var eventAttrs = { name: null, props: {} }
|
||||
if (COMPILE_REVENUE) {
|
||||
eventAttrs.revenue = {}
|
||||
}
|
||||
|
||||
var classList = taggedElement && taggedElement.classList
|
||||
if (!classList) { return eventAttrs }
|
||||
|
||||
for (var i = 0; i < classList.length; i++) {
|
||||
var className = classList.item(i)
|
||||
|
||||
var matchList = className.match(/plausible-event-(.+)(=|--)(.+)/)
|
||||
if (matchList) {
|
||||
var key = matchList[1]
|
||||
var value = matchList[3].replace(/\+/g, ' ')
|
||||
|
||||
if (key.toLowerCase() == 'name') {
|
||||
eventAttrs.name = value
|
||||
} else {
|
||||
eventAttrs.props[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
if (COMPILE_REVENUE) {
|
||||
var revenueMatchList = className.match(/plausible-revenue-(.+)(=|--)(.+)/)
|
||||
if (revenueMatchList) {
|
||||
var key = revenueMatchList[1]
|
||||
var value = revenueMatchList[3]
|
||||
eventAttrs.revenue[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return eventAttrs
|
||||
}
|
||||
|
||||
function handleTaggedFormSubmitEvent(event) {
|
||||
var form = event.target
|
||||
var eventAttrs = getTaggedEventAttributes(form)
|
||||
if (!eventAttrs.name) { return }
|
||||
|
||||
event.preventDefault()
|
||||
var formSubmitted = false
|
||||
|
||||
function submitForm() {
|
||||
if (!formSubmitted) {
|
||||
formSubmitted = true
|
||||
form.submit()
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(submitForm, 5000)
|
||||
|
||||
var attrs = { props: eventAttrs.props, callback: submitForm }
|
||||
if (COMPILE_REVENUE) {
|
||||
attrs.revenue = eventAttrs.revenue
|
||||
}
|
||||
plausible(eventAttrs.name, attrs)
|
||||
}
|
||||
|
||||
function isForm(element) {
|
||||
return element && element.tagName && element.tagName.toLowerCase() === 'form'
|
||||
}
|
||||
|
||||
var PARENTS_TO_SEARCH_LIMIT = 3
|
||||
|
||||
function handleTaggedElementClickEvent(event) {
|
||||
if (event.type === 'auxclick' && event.button !== MIDDLE_MOUSE_BUTTON) { return }
|
||||
|
||||
var clicked = event.target
|
||||
|
||||
var clickedLink
|
||||
var taggedElement
|
||||
// Iterate over parents to find the tagged element. Also search for
|
||||
// a link element to call for different tracking behavior if found.
|
||||
for (var i = 0; i <= PARENTS_TO_SEARCH_LIMIT; i++) {
|
||||
if (!clicked) { break }
|
||||
|
||||
// Clicks inside forms are not tracked. Only form submits are.
|
||||
if (isForm(clicked)) { return }
|
||||
if (isLink(clicked)) { clickedLink = clicked }
|
||||
if (isTagged(clicked)) { taggedElement = clicked }
|
||||
clicked = clicked.parentNode
|
||||
}
|
||||
|
||||
if (taggedElement) {
|
||||
var eventAttrs = getTaggedEventAttributes(taggedElement)
|
||||
|
||||
if (clickedLink) {
|
||||
// if the clicked tagged element is a link, we attach the `url` property
|
||||
// automatically for user convenience
|
||||
eventAttrs.props.url = clickedLink.href
|
||||
sendLinkClickEvent(event, clickedLink, eventAttrs)
|
||||
} else {
|
||||
var attrs = {}
|
||||
attrs.props = eventAttrs.props
|
||||
if (COMPILE_REVENUE) {
|
||||
attrs.revenue = eventAttrs.revenue
|
||||
}
|
||||
plausible(eventAttrs.name, attrs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isTagged(element) {
|
||||
var classList = element && element.classList
|
||||
if (classList) {
|
||||
for (var i = 0; i < classList.length; i++) {
|
||||
if (classList.item(i).match(/plausible-event-name(=|--)(.+)/)) { return true }
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function isElementOrParentTagged(element, parentsChecked) {
|
||||
if (!element || parentsChecked > PARENTS_TO_SEARCH_LIMIT) { return false }
|
||||
if (isTagged(element)) { return true }
|
||||
return isElementOrParentTagged(element.parentNode, parentsChecked + 1)
|
||||
}
|
||||
|
||||
document.addEventListener('submit', handleTaggedFormSubmitEvent)
|
||||
document.addEventListener('click', handleTaggedElementClickEvent)
|
||||
document.addEventListener('auxclick', handleTaggedElementClickEvent)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
const { expectPlausibleInAction } = require('./support/test-utils')
|
||||
const { test } = require('@playwright/test')
|
||||
const { LOCAL_SERVER_ADDR } = require('./support/server')
|
||||
import { expectPlausibleInAction } from './support/test-utils'
|
||||
import { test } from '@playwright/test'
|
||||
import { LOCAL_SERVER_ADDR } from './support/server'
|
||||
|
||||
test.describe('script.file-downloads.outbound-links.tagged-events.js', () => {
|
||||
test('sends only outbound link event when clicked link is both download and outbound', async ({ page }) => {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
const { expect } = require("@playwright/test")
|
||||
const { expectPlausibleInAction, hideAndShowCurrentTab, focus, blur, blurAndFocusPage } = require('./support/test-utils')
|
||||
const { test } = require('@playwright/test')
|
||||
const { LOCAL_SERVER_ADDR } = require('./support/server')
|
||||
const { tracker_script_version } = require('../package.json')
|
||||
import { expect } from "@playwright/test"
|
||||
import { expectPlausibleInAction, hideAndShowCurrentTab, focus, blur, blurAndFocusPage, tracker_script_version } from './support/test-utils'
|
||||
import { test } from '@playwright/test'
|
||||
import { LOCAL_SERVER_ADDR } from './support/server'
|
||||
|
||||
test.describe('engagement events', () => {
|
||||
test('sends an engagement event with time measurement when navigating to the next page', async ({ page }) => {
|
||||
|
|
@ -205,6 +204,7 @@ test.describe('engagement events', () => {
|
|||
await expectPlausibleInAction(page, {
|
||||
action: async () => {
|
||||
await page.click('#to-pageleave-pageview-props')
|
||||
await page.waitForTimeout(500)
|
||||
await page.click('#back-button-trigger')
|
||||
},
|
||||
expectedRequests: [
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
const { mockRequest, mockManyRequests, metaKey, expectPlausibleInAction } = require('./support/test-utils')
|
||||
const { expect, test } = require('@playwright/test')
|
||||
const { LOCAL_SERVER_ADDR } = require('./support/server')
|
||||
import { mockRequest, mockManyRequests, metaKey, expectPlausibleInAction } from './support/test-utils'
|
||||
import { expect, test } from '@playwright/test'
|
||||
import { LOCAL_SERVER_ADDR } from './support/server'
|
||||
|
||||
test.describe('file-downloads extension', () => {
|
||||
test('sends event and does not start download when link opens in new tab', async ({ page }) => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
const { mockRequest } = require('./support/test-utils')
|
||||
const { expect, test } = require('@playwright/test')
|
||||
import { mockRequest } from './support/test-utils'
|
||||
import { expect, test } from '@playwright/test'
|
||||
|
||||
test.describe('combination of hash and exclusions script extensions', () => {
|
||||
test('excludes by hash part of the URL', async ({ page }) => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
const { expectPlausibleInAction } = require('./support/test-utils')
|
||||
const { test } = require('@playwright/test')
|
||||
const { LOCAL_SERVER_ADDR } = require('./support/server')
|
||||
import { expectPlausibleInAction } from './support/test-utils'
|
||||
import { test } from '@playwright/test'
|
||||
import { LOCAL_SERVER_ADDR } from './support/server'
|
||||
|
||||
test.describe('manual extension', () => {
|
||||
test('can trigger custom events with and without a custom URL if pageview was sent with the default URL', async ({ page }) => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
const { mockRequest, metaKey, expectPlausibleInAction } = require('./support/test-utils')
|
||||
const { expect, test } = require('@playwright/test')
|
||||
import { mockRequest, metaKey, expectPlausibleInAction } from './support/test-utils'
|
||||
import { expect, test } from '@playwright/test'
|
||||
|
||||
test.describe('outbound-links extension', () => {
|
||||
test('sends event and does not navigate when link opens in new tab', async ({ page }) => {
|
||||
|
|
@ -7,7 +7,7 @@ test.describe('outbound-links extension', () => {
|
|||
const outboundURL = await page.locator('#link').getAttribute('href')
|
||||
|
||||
const navigationRequestMock = mockRequest(page, outboundURL)
|
||||
|
||||
|
||||
await expectPlausibleInAction(page, {
|
||||
action: () => page.click('#link', { modifiers: [metaKey()] }),
|
||||
expectedRequests: [{n: 'Outbound Link: Click', p: { url: outboundURL }}]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
const { mockRequest } = require('./support/test-utils')
|
||||
const { expect, test } = require('@playwright/test')
|
||||
const { tracker_script_version } = require('../package.json')
|
||||
import { mockRequest, tracker_script_version } from './support/test-utils'
|
||||
import { expect, test } from '@playwright/test'
|
||||
|
||||
test.describe('Basic installation', () => {
|
||||
test('Sends pageview automatically', async ({ page }) => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
const { expectPlausibleInAction } = require('./support/test-utils')
|
||||
const { test } = require('@playwright/test')
|
||||
import { expectPlausibleInAction } from './support/test-utils'
|
||||
import { test } from '@playwright/test'
|
||||
|
||||
test.describe('with revenue script extension', () => {
|
||||
test('sends revenue currency and amount in manual mode', async ({ page }) => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
const { expectPlausibleInAction, hideCurrentTab, hideAndShowCurrentTab } = require('./support/test-utils')
|
||||
const { test } = require('@playwright/test')
|
||||
const { LOCAL_SERVER_ADDR } = require('./support/server')
|
||||
import { expectPlausibleInAction, hideCurrentTab, hideAndShowCurrentTab } from './support/test-utils'
|
||||
import { test } from '@playwright/test'
|
||||
import { LOCAL_SERVER_ADDR } from './support/server'
|
||||
|
||||
test.describe('scroll depth (engagement events)', () => {
|
||||
test('sends scroll_depth in the pageleave payload when navigating to the next page', async ({ page }) => {
|
||||
|
|
|
|||
|
|
@ -1,15 +1,30 @@
|
|||
const express = require('express');
|
||||
import express from 'express'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'url'
|
||||
import { compileFile } from '../../compiler/index.js'
|
||||
import variantsFile from '../../compiler/variants.json' with { type: 'json' }
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
const isMainModule = fileURLToPath(import.meta.url) === process.argv[1];
|
||||
|
||||
const app = express();
|
||||
const path = require('node:path');
|
||||
|
||||
const LOCAL_SERVER_PORT = 3000
|
||||
const LOCAL_SERVER_ADDR = `http://localhost:${LOCAL_SERVER_PORT}`
|
||||
const FIXTURES_PATH = path.join(__dirname, '/../fixtures')
|
||||
const TRACKERS_PATH = path.join(__dirname, '/../../../priv/tracker')
|
||||
const VARIANTS = variantsFile.legacyVariants.concat(variantsFile.manualVariants)
|
||||
|
||||
exports.runLocalFileServer = function () {
|
||||
export const LOCAL_SERVER_ADDR = `http://localhost:${LOCAL_SERVER_PORT}`
|
||||
|
||||
export function runLocalFileServer() {
|
||||
app.use(express.static(FIXTURES_PATH));
|
||||
app.use('/tracker', express.static(TRACKERS_PATH));
|
||||
|
||||
app.get('/tracker/js/:name', (req, res) => {
|
||||
const name = req.params.name
|
||||
const variant = VARIANTS.find((variant) => variant.name === name)
|
||||
|
||||
const code = compileFile(variant, { returnCode: true })
|
||||
|
||||
res.send(code)
|
||||
});
|
||||
|
||||
// A test utility - serve an image with an artificial delay
|
||||
app.get('/img/slow-image', (_req, res) => {
|
||||
|
|
@ -23,8 +38,6 @@ exports.runLocalFileServer = function () {
|
|||
});
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
exports.runLocalFileServer()
|
||||
if (isMainModule) {
|
||||
runLocalFileServer()
|
||||
}
|
||||
|
||||
exports.LOCAL_SERVER_ADDR = LOCAL_SERVER_ADDR
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
const { expect, Page } = require("@playwright/test");
|
||||
import { expect } from "@playwright/test"
|
||||
import packageJson from '../../package.json' with { type: 'json' }
|
||||
|
||||
export const tracker_script_version = packageJson.tracker_script_version
|
||||
|
||||
// Mocks an HTTP request call with the given path. Returns a Promise that resolves to the request
|
||||
// data. If the request is not made, resolves to null after 3 seconds.
|
||||
const mockRequest = function (page, path) {
|
||||
export const mockRequest = function (page, path) {
|
||||
return new Promise((resolve, _reject) => {
|
||||
const requestTimeoutTimer = setTimeout(() => resolve(null), 3000)
|
||||
|
||||
|
|
@ -14,9 +17,7 @@ const mockRequest = function (page, path) {
|
|||
})
|
||||
}
|
||||
|
||||
exports.mockRequest = mockRequest
|
||||
|
||||
exports.metaKey = function() {
|
||||
export const metaKey = function() {
|
||||
if (process.platform === 'darwin') {
|
||||
return 'Meta'
|
||||
} else {
|
||||
|
|
@ -26,7 +27,7 @@ exports.metaKey = function() {
|
|||
|
||||
// Mocks a specified number of HTTP requests with given path. Returns a promise that resolves to a
|
||||
// list of requests as soon as the specified number of requests is made, or 3 seconds has passed.
|
||||
const mockManyRequests = function({ page, path, numberOfRequests, responseDelay, shouldIgnoreRequest, mockRequestTimeout = 3000 }) {
|
||||
export const mockManyRequests = function({ page, path, numberOfRequests, responseDelay, shouldIgnoreRequest, mockRequestTimeout = 3000 }) {
|
||||
return new Promise((resolve, _reject) => {
|
||||
let requestList = []
|
||||
const requestTimeoutTimer = setTimeout(() => resolve(requestList), mockRequestTimeout)
|
||||
|
|
@ -48,8 +49,6 @@ const mockManyRequests = function({ page, path, numberOfRequests, responseDelay,
|
|||
})
|
||||
}
|
||||
|
||||
exports.mockManyRequests = mockManyRequests
|
||||
|
||||
/**
|
||||
* A powerful utility function that makes it easy to assert on the event
|
||||
* requests that should or should not have been made after doing a page
|
||||
|
|
@ -76,7 +75,7 @@ exports.mockManyRequests = mockManyRequests
|
|||
* @param {number} [args.responseDelay] - When provided, delays the response from the Plausible
|
||||
* API by the given number of milliseconds.
|
||||
*/
|
||||
exports.expectPlausibleInAction = async function (page, {
|
||||
export const expectPlausibleInAction = async function (page, {
|
||||
action,
|
||||
expectedRequests = [],
|
||||
refutedRequests = [],
|
||||
|
|
@ -131,11 +130,11 @@ exports.expectPlausibleInAction = async function (page, {
|
|||
return requestBodies
|
||||
}
|
||||
|
||||
exports.ignoreEngagementRequests = function(requestPostData) {
|
||||
export const ignoreEngagementRequests = function(requestPostData) {
|
||||
return requestPostData.n === 'engagement'
|
||||
}
|
||||
|
||||
exports.ignorePageleaveRequests = function(requestPostData) {
|
||||
export const ignorePageleaveRequests = function(requestPostData) {
|
||||
return requestPostData.n === 'pageleave'
|
||||
}
|
||||
|
||||
|
|
@ -147,11 +146,11 @@ async function toggleTabVisibility(page, hide) {
|
|||
}, hide)
|
||||
}
|
||||
|
||||
exports.hideCurrentTab = async function(page) {
|
||||
export const hideCurrentTab = async function(page) {
|
||||
return toggleTabVisibility(page, true)
|
||||
}
|
||||
|
||||
exports.showCurrentTab = async function(page) {
|
||||
export const showCurrentTab = async function(page) {
|
||||
return toggleTabVisibility(page, false)
|
||||
}
|
||||
|
||||
|
|
@ -164,28 +163,28 @@ async function setFocus(page, focus) {
|
|||
}, focus)
|
||||
}
|
||||
|
||||
exports.focus = async function(page) {
|
||||
export const focus = async function(page) {
|
||||
return setFocus(page, true)
|
||||
}
|
||||
|
||||
exports.blur = async function(page) {
|
||||
export const blur = async function(page) {
|
||||
return setFocus(page, false)
|
||||
}
|
||||
|
||||
exports.hideAndShowCurrentTab = async function(page, options = {}) {
|
||||
await exports.hideCurrentTab(page)
|
||||
export const hideAndShowCurrentTab = async function(page, options = {}) {
|
||||
await hideCurrentTab(page)
|
||||
if (options.delay > 0) {
|
||||
await delay(options.delay)
|
||||
}
|
||||
await exports.showCurrentTab(page)
|
||||
await showCurrentTab(page)
|
||||
}
|
||||
|
||||
exports.blurAndFocusPage = async function(page, options = {}) {
|
||||
await exports.blur(page)
|
||||
export const blurAndFocusPage = async function(page, options = {}) {
|
||||
await blur(page)
|
||||
if (options.delay > 0) {
|
||||
await delay(options.delay)
|
||||
}
|
||||
await exports.focus(page)
|
||||
await focus(page)
|
||||
}
|
||||
|
||||
function includesSubset(body, subset) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
const { mockRequest, metaKey, expectPlausibleInAction } = require('./support/test-utils')
|
||||
const { expect, test } = require('@playwright/test')
|
||||
import { mockRequest, metaKey, expectPlausibleInAction } from './support/test-utils'
|
||||
import { expect, test } from '@playwright/test'
|
||||
|
||||
test.describe('tagged-events extension', () => {
|
||||
test('tracks a tagged link click with custom props + url prop', async ({ page }) => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue