diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..35e3ed1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist_bundle +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +.TODO diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..ff2677e --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "useTabs": true, + "singleQuote": true, + "trailingComma": "none", + "printWidth": 100 +} diff --git a/index.html b/index.html new file mode 100644 index 0000000..16f8f36 --- /dev/null +++ b/index.html @@ -0,0 +1,59 @@ + + + + + + ALTCHA Analytics Script + + +
+ +
+ Links: + +
+ +
+ PushState: + +
+ +
+ Hash: + +
+ +
+ Outbound: + +
+ +
+ + + + + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..2152568 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2124 @@ +{ + "name": "@altcha/tracker", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@altcha/tracker", + "version": "0.1.0", + "license": "MIT", + "devDependencies": { + "jsdom": "^25.0.1", + "rimraf": "^6.0.1", + "typescript": "^5.5.3", + "vite": "^5.4.1", + "vitest": "^2.1.1" + }, + "optionalDependencies": { + "@rollup/rollup-linux-x64-gnu": "4.18.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz", + "integrity": "sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.0.tgz", + "integrity": "sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.0.tgz", + "integrity": "sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.0.tgz", + "integrity": "sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.0.tgz", + "integrity": "sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.0.tgz", + "integrity": "sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.0.tgz", + "integrity": "sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.0.tgz", + "integrity": "sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.0.tgz", + "integrity": "sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.0.tgz", + "integrity": "sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.0.tgz", + "integrity": "sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.0.tgz", + "integrity": "sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.0.tgz", + "integrity": "sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.0.tgz", + "integrity": "sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.0.tgz", + "integrity": "sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.0.tgz", + "integrity": "sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true + }, + "node_modules/@vitest/expect": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.1.tgz", + "integrity": "sha512-YeueunS0HiHiQxk+KEOnq/QMzlUuOzbU1Go+PgAsHvvv3tUkJPm9xWt+6ITNTlzsMXUjmgm5T+U7KBPK2qQV6w==", + "dev": true, + "dependencies": { + "@vitest/spy": "2.1.1", + "@vitest/utils": "2.1.1", + "chai": "^5.1.1", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.1.tgz", + "integrity": "sha512-LNN5VwOEdJqCmJ/2XJBywB11DLlkbY0ooDJW3uRX5cZyYCrc4PI/ePX0iQhE3BiEGiQmK4GE7Q/PqCkkaiPnrA==", + "dev": true, + "dependencies": { + "@vitest/spy": "^2.1.0-beta.1", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.11" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/spy": "2.1.1", + "msw": "^2.3.5", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.1.tgz", + "integrity": "sha512-SjxPFOtuINDUW8/UkElJYQSFtnWX7tMksSGW0vfjxMneFqxVr8YJ979QpMbDW7g+BIiq88RAGDjf7en6rvLPPQ==", + "dev": true, + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.1.tgz", + "integrity": "sha512-uTPuY6PWOYitIkLPidaY5L3t0JJITdGTSwBtwMjKzo5O6RCOEncz9PUN+0pDidX8kTHYjO0EwUIvhlGpnGpxmA==", + "dev": true, + "dependencies": { + "@vitest/utils": "2.1.1", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.1.tgz", + "integrity": "sha512-BnSku1WFy7r4mm96ha2FzN99AZJgpZOWrAhtQfoxjUU5YMRpq1zmHRq7a5K9/NjqonebO7iVDla+VvZS8BOWMw==", + "dev": true, + "dependencies": { + "@vitest/pretty-format": "2.1.1", + "magic-string": "^0.30.11", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.1.tgz", + "integrity": "sha512-ZM39BnZ9t/xZ/nF4UwRH5il0Sw93QnZXd9NAZGRpIgj0yvVwPpLd702s/Cx955rGaMlyBQkZJ2Ir7qyY48VZ+g==", + "dev": true, + "dependencies": { + "tinyspy": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.1.tgz", + "integrity": "sha512-Y6Q9TsI+qJ2CC0ZKj6VBb+T8UPz593N113nnUykqwANqhgf3QkZeHFlusgKLTqrnVHbj/XDKZcDHol+dxVT+rQ==", + "dev": true, + "dependencies": { + "@vitest/pretty-format": "2.1.1", + "loupe": "^3.1.1", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dev": true, + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.1.tgz", + "integrity": "sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==", + "dev": true, + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "engines": { + "node": ">= 16" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssstyle": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.1.0.tgz", + "integrity": "sha512-h66W1URKpBS5YMI/V8PyXvTMFT8SupJ1IzoIV8IeBC/ji8WVmrO8dGlTi+2dh6whmdk6BiKJLD/ZBkhWbcg6nA==", + "dev": true, + "dependencies": { + "rrweb-cssom": "^0.7.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", + "dev": true + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/glob": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.0.tgz", + "integrity": "sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^4.0.1", + "minimatch": "^10.0.0", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "dev": true, + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "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, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/jackspeak": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.2.tgz", + "integrity": "sha512-bZsjR/iRjl1Nk1UkjGpAzLNfQtzuijhn2g+pbZb98HQ1Gk8vM9hfbxeMBP+M2/UUdwj0RqGG3mlvk2MsAqwvEw==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jsdom": { + "version": "25.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.1.tgz", + "integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==", + "dev": true, + "dependencies": { + "cssstyle": "^4.1.0", + "data-urls": "^5.0.0", + "decimal.js": "^10.4.3", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.12", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.7.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^2.11.2" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/loupe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.1.tgz", + "integrity": "sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "node_modules/lru-cache": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.1.tgz", + "integrity": "sha512-CgeuL5uom6j/ZVrg7G/+1IXqRY8JXX4Hghfy5YE0EhoYQWvndP1kufu58cmZLNIDKnRhZrXfdS9urVWx98AipQ==", + "dev": true, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/magic-string": { + "version": "0.30.11", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", + "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", + "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/nwsapi": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.12.tgz", + "integrity": "sha512-qXDmcVlZV4XRtKFzddidpfVP4oMSGhga+xdMc25mv8kaLUHtgzCDhUxkrN8exkGdTlLNaXj7CV3GtON7zuGZ+w==", + "dev": true + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true + }, + "node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dev": true, + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "dev": true, + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true + }, + "node_modules/pathval": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", + "dev": true, + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", + "dev": true + }, + "node_modules/postcss": { + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/rimraf": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", + "integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==", + "dev": true, + "dependencies": { + "glob": "^11.0.0", + "package-json-from-dist": "^1.0.0" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz", + "integrity": "sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.6" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.24.0", + "@rollup/rollup-android-arm64": "4.24.0", + "@rollup/rollup-darwin-arm64": "4.24.0", + "@rollup/rollup-darwin-x64": "4.24.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.24.0", + "@rollup/rollup-linux-arm-musleabihf": "4.24.0", + "@rollup/rollup-linux-arm64-gnu": "4.24.0", + "@rollup/rollup-linux-arm64-musl": "4.24.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.24.0", + "@rollup/rollup-linux-riscv64-gnu": "4.24.0", + "@rollup/rollup-linux-s390x-gnu": "4.24.0", + "@rollup/rollup-linux-x64-gnu": "4.24.0", + "@rollup/rollup-linux-x64-musl": "4.24.0", + "@rollup/rollup-win32-arm64-msvc": "4.24.0", + "@rollup/rollup-win32-ia32-msvc": "4.24.0", + "@rollup/rollup-win32-x64-msvc": "4.24.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/rollup/node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz", + "integrity": "sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/rrweb-cssom": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", + "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==", + "dev": true + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true + }, + "node_modules/std-env": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz", + "integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==", + "dev": true + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "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, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/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, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/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 + }, + "node_modules/string-width-cjs/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, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "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, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/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, + "engines": { + "node": ">=8" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true + }, + "node_modules/tinyexec": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.0.tgz", + "integrity": "sha512-tVGE0mVJPGb0chKhqmsoosjsS+qUnJVGJpZgsHYQcGoPlG3B51R3PouqTgEGH2Dc9jjFyOqOpix6ZHNMXp1FZg==", + "dev": true + }, + "node_modules/tinypool": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.1.tgz", + "integrity": "sha512-URZYihUbRPcGv95En+sz6MfghfIc2OJ1sv/RmhWZLouPY0/8Vo80viwPvg3dlaS9fuq7fQMEfgRRK7BBZThBEA==", + "dev": true, + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "6.1.47", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.47.tgz", + "integrity": "sha512-R/K2tZ5MiY+mVrnSkNJkwqYT2vUv1lcT6wJvd2emGaMJ7PHUGRY4e3tUsdFCXgqxi2QgbHjL3yJgXCo40v9Hxw==", + "dev": true, + "dependencies": { + "tldts-core": "^6.1.47" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.47", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.47.tgz", + "integrity": "sha512-6SWyFMnlst1fEt7GQVAAu16EGgFK0cLouH/2Mk6Ftlwhv3Ol40L0dlpGMcnnNiiOMyD2EV/aF3S+U2nKvvLvrA==", + "dev": true + }, + "node_modules/tough-cookie": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.0.0.tgz", + "integrity": "sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==", + "dev": true, + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", + "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", + "dev": true, + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/typescript": { + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "5.4.8", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.8.tgz", + "integrity": "sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==", + "dev": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.1.tgz", + "integrity": "sha512-N/mGckI1suG/5wQI35XeR9rsMsPqKXzq1CdUndzVstBj/HvyxxGctwnK6WX43NGt5L3Z5tcRf83g4TITKJhPrA==", + "dev": true, + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.6", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.1.tgz", + "integrity": "sha512-97We7/VC0e9X5zBVkvt7SGQMGrRtn3KtySFQG5fpaMlS+l62eeXRQO633AYhSTC3z7IMebnPPNjGXVGNRFlxBA==", + "dev": true, + "dependencies": { + "@vitest/expect": "2.1.1", + "@vitest/mocker": "2.1.1", + "@vitest/pretty-format": "^2.1.1", + "@vitest/runner": "2.1.1", + "@vitest/snapshot": "2.1.1", + "@vitest/spy": "2.1.1", + "@vitest/utils": "2.1.1", + "chai": "^5.1.1", + "debug": "^4.3.6", + "magic-string": "^0.30.11", + "pathe": "^1.1.2", + "std-env": "^3.7.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.0", + "tinypool": "^1.0.0", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.1", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.1", + "@vitest/ui": "2.1.1", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dev": true, + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.0.0.tgz", + "integrity": "sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==", + "dev": true, + "dependencies": { + "tr46": "^5.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/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, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/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 + }, + "node_modules/wrap-ansi-cjs/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, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/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, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..cbe8810 --- /dev/null +++ b/package.json @@ -0,0 +1,52 @@ +{ + "name": "@altcha/tracker", + "description": "JavaScript client for ALTCHA Analytics.", + "version": "0.1.0", + "license": "MIT", + "author": { + "name": "Daniel Regeci", + "url": "https://altcha.org" + }, + "homepage": "https://altcha.org", + "repository": { + "type": "git", + "url": "https://github.com/altcha-org/tracker" + }, + "type": "module", + "keywords": [ + "altcha", + "analytics", + "statistics", + "tracker", + "svelte" + ], + "files": [ + "dist", + "dist_bundle" + ], + "main": "./dist/tracker.umd.cjs", + "module": "./dist/tracker.js", + "exports": { + ".": { + "import": "./dist/tracker.js", + "require": "./dist/tracker.umd.cjs" + } + }, + "scripts": { + "dev": "vite", + "build": "rimraf dist && tsc && vite build -c vite.config.js", + "build:bundle": "rimraf dist_bundle && tsc && vite build -c vite.bundle.config.js", + "preview": "vite preview", + "test": "vitest run" + }, + "devDependencies": { + "jsdom": "^25.0.1", + "rimraf": "^6.0.1", + "typescript": "^5.5.3", + "vite": "^5.4.1", + "vitest": "^2.1.1" + }, + "optionalDependencies": { + "@rollup/rollup-linux-x64-gnu": "4.18.0" + } +} diff --git a/src/extensions/base.ext.ts b/src/extensions/base.ext.ts new file mode 100644 index 0000000..67b5641 --- /dev/null +++ b/src/extensions/base.ext.ts @@ -0,0 +1,21 @@ +import type { Tracker } from '../tracker'; +import type { IBaseExtensionOptions } from '../types'; + +export class BaseExtension { + protected lastEvent: TEvent | null = null; + + constructor( + readonly tracker: Tracker, + readonly options: IBaseExtensionOptions = { disable: false } + ) {} + + destroy() {} + + getExitReason(_unload: boolean = false): string | undefined { + return undefined; + } + + isLastEventRecent(threshold: number = 10000, ev = this.lastEvent) { + return !!ev && performance.now() - ev.timeStamp < threshold; + } +} diff --git a/src/extensions/click.ext.ts b/src/extensions/click.ext.ts new file mode 100644 index 0000000..63068c9 --- /dev/null +++ b/src/extensions/click.ext.ts @@ -0,0 +1,39 @@ +import { BaseExtension } from './base.ext'; +import type { IBaseExtensionOptions } from '../types'; +import type { Tracker } from '../tracker'; + +export class ClickExtension extends BaseExtension { + readonly #_onClick = this.onClick.bind(this); + + constructor(tracker: Tracker, _options: IBaseExtensionOptions) { + super(tracker); + addEventListener('click', this.#_onClick, { + capture: true, + }); + } + + destroy(): void { + removeEventListener('click', this.#_onClick); + } + + getExitReason() { + if (this.lastEvent && this.isLastEventRecent()) { + const target = this.lastEvent.target as HTMLElement | null; + const href = target?.getAttribute('data-link') || target?.getAttribute('href'); + if (href) { + const url = new URL(href, location.origin); + if (url.hostname && url.hostname !== location.hostname) { + return this.tracker.sanitizeUrl(url); + } + } + } + return undefined; + } + + onClick(ev: MouseEvent) { + const target = ev.target as HTMLElement | null; + if (target && target.tagName === 'A') { + this.lastEvent = ev; + } + } +} diff --git a/src/extensions/cookie.ext.ts b/src/extensions/cookie.ext.ts new file mode 100644 index 0000000..8a14ed6 --- /dev/null +++ b/src/extensions/cookie.ext.ts @@ -0,0 +1,49 @@ +import { BaseExtension } from './base.ext'; +import type { ICookieExtensionOptions } from '../types'; +import type { Tracker } from '../tracker'; + +export class CookieExtension extends BaseExtension { + readonly #cookieName: string; + + readonly #cookieExpireDays: number; + + readonly #cookiePath: string; + + constructor(tracker: Tracker, options: ICookieExtensionOptions) { + super(tracker); + this.#cookieName = options.cookieName || '_altcha_visited'; + this.#cookieExpireDays = options.cookieExpireDays || 30; + this.#cookiePath = options.cookiePath || '/'; + const cookie = this.getCookie(this.#cookieName); + if (cookie === '1') { + this.tracker.returningVisitor = true; + } + this.setCookie( + this.#cookieName, + '1', + new Date(Date.now() + 86_400_000 * this.#cookieExpireDays) + ); + } + + destroy() {} + + getCookie(name: string) { + const cookies = document.cookie.split(/;\s?/); + for (const cookie of cookies) { + if (cookie.startsWith(name + '=')) { + return cookie.slice(name.length + 1); + } + } + return null; + } + + setCookie(name: string, value: string, expires: Date) { + document.cookie = + name + + '=' + + value + + '; expires=' + + expires.toUTCString() + + `; SameSite=Strict; path=${this.#cookiePath || '/'}`; + } +} diff --git a/src/extensions/hash.ext.ts b/src/extensions/hash.ext.ts new file mode 100644 index 0000000..590a225 --- /dev/null +++ b/src/extensions/hash.ext.ts @@ -0,0 +1,20 @@ +import { BaseExtension } from './base.ext'; +import type { IBaseExtensionOptions } from '../types'; +import type { Tracker } from '../tracker'; + +export class HashExtension extends BaseExtension { + readonly #_onHashChange = this.onHashChange.bind(this); + + constructor(tracker: Tracker, _options: IBaseExtensionOptions) { + super(tracker); + addEventListener('hashchange', this.#_onHashChange); + } + + destroy() { + removeEventListener('hashchange', this.#_onHashChange); + } + + onHashChange() { + this.tracker.trackPageview(); + } +} diff --git a/src/extensions/keyboard.ext.ts b/src/extensions/keyboard.ext.ts new file mode 100644 index 0000000..47433df --- /dev/null +++ b/src/extensions/keyboard.ext.ts @@ -0,0 +1,35 @@ +import { BaseExtension } from './base.ext'; +import type { IBaseExtensionOptions } from '../types'; +import type { Tracker } from '../tracker'; + +export class KeyboardExtension extends BaseExtension { + readonly #_onKeyDown = this.onKeyDown.bind(this); + + constructor(tracker: Tracker, _options: IBaseExtensionOptions) { + super(tracker); + addEventListener('keydown', this.#_onKeyDown); + } + + destroy() { + removeEventListener('keydown', this.#_onKeyDown); + } + + isLastKeyboardEventCtrl() { + return ( + !!this.lastEvent && + (this.lastEvent.ctrlKey || this.lastEvent.metaKey) + ); + } + + getExitReason(unload: boolean = false) { + if (unload && this.isLastEventRecent(100) && this.isLastKeyboardEventCtrl()) { + // return empty string to indicate "exit" reason + return ''; + } + return undefined; + } + + onKeyDown(ev: KeyboardEvent) { + this.lastEvent = ev; + } +} diff --git a/src/extensions/mouse.ext.ts b/src/extensions/mouse.ext.ts new file mode 100644 index 0000000..ef4831b --- /dev/null +++ b/src/extensions/mouse.ext.ts @@ -0,0 +1,59 @@ +import { BaseExtension } from './base.ext'; +import { PushstateExtension } from './pushstate.ext'; +import type { IBaseExtensionOptions } from '../types'; +import type { Tracker } from '../tracker'; + +export class MouseExtension extends BaseExtension { + readonly #_onMouseEnter = this.onMouseEnter.bind(this); + + readonly #_onMouseLeave = this.onMouseLeave.bind(this); + + isMouseOut: boolean = false; + + offsetX: number = -1; + + offsetY: number = -1; + + constructor(tracker: Tracker, _options: IBaseExtensionOptions) { + super(tracker); + document.body.addEventListener('mouseleave', this.#_onMouseLeave); + document.body.addEventListener('mouseenter', this.#_onMouseEnter); + } + + destroy(): void { + document.body.removeEventListener('mouseleave', this.#_onMouseLeave); + document.body.removeEventListener('mouseenter', this.#_onMouseEnter); + } + + getExitReason() { + if (this.isMouseOut) { + const pushStateExt = this.tracker.getExtension('pushstate') as PushstateExtension | undefined; + if ( + pushStateExt && + pushStateExt.lastPopStateEvent && + pushStateExt.isLastEventRecent(100, pushStateExt.lastPopStateEvent) + ) { + // popstate event has been called, most likely a back button navigation + return undefined; + } + if (this.offsetX >= 0 && this.offsetX >= 0 && this.offsetX < 150) { + // top left corner of the screen, most likely navigation buttons such as a back button + return undefined; + } + return ''; + } + return undefined; + } + + onMouseEnter() { + this.isMouseOut = false; + this.offsetX = -1; + this.offsetY = -1; + } + + onMouseLeave(ev: MouseEvent) { + this.isMouseOut = true; + this.offsetX = ev.clientX; + this.offsetY = ev.clientY; + } +} diff --git a/src/extensions/pushstate.ext.ts b/src/extensions/pushstate.ext.ts new file mode 100644 index 0000000..d2c3949 --- /dev/null +++ b/src/extensions/pushstate.ext.ts @@ -0,0 +1,39 @@ +import { BaseExtension } from './base.ext'; +import type { IBaseExtensionOptions } from '../types'; +import type { Tracker } from '../tracker'; + +export class PushstateExtension extends BaseExtension { + #originalPushState: typeof history.pushState | null = null; + + readonly #_onPopState = this.onPopState.bind(this); + + readonly #_onPushState = this.onPushState.bind(this); + + lastPopStateEvent: PopStateEvent | null = null; + + constructor(tracker: Tracker, _options: IBaseExtensionOptions) { + super(tracker); + const originalPushState = (this.#originalPushState = history.pushState); + history.pushState = (data, title, url) => { + this.#_onPushState(); + originalPushState?.apply(history, [data, title, url]); + }; + addEventListener('popstate', this.#_onPopState); + } + + destroy() { + if (this.#originalPushState) { + history.pushState = this.#originalPushState; + } + removeEventListener('popstate', this.#_onPopState); + } + + onPopState(ev: PopStateEvent) { + this.lastPopStateEvent = ev; + this.tracker.trackPageview(); + } + + onPushState() { + this.tracker.trackPageview(); + } +} diff --git a/src/extensions/visibility.ext.ts b/src/extensions/visibility.ext.ts new file mode 100644 index 0000000..87463b2 --- /dev/null +++ b/src/extensions/visibility.ext.ts @@ -0,0 +1,61 @@ +import { BaseExtension } from './base.ext'; +import type { IVivibilityExtensionOptions } from '../types'; +import type { Tracker } from '../tracker'; + +export class VisibilityExtension extends BaseExtension { + readonly #_onVisibilityChange = this.onVisibilityChange.bind(this); + + readonly #hiddenTimeout: number; + + #timeout: ReturnType | null = null; + + visibilityState: DocumentVisibilityState = document.visibilityState; + + constructor(tracker: Tracker, options: IVivibilityExtensionOptions) { + super(tracker); + this.#hiddenTimeout = options.hiddenTimeout || 4000; + addEventListener('visibilitychange', this.#_onVisibilityChange); + } + + destroy() { + removeEventListener('visibilitychange', this.#_onVisibilityChange); + } + + getExitReason(unload: boolean = false) { + const minThreshold = 1000; + if ( + unload && + this.visibilityState === 'hidden' && + this.lastEvent && + performance.now() - this.lastEvent.timeStamp >= minThreshold + ) { + // if hidden for a while, unload event indicates closing the tab + // note: visibilitychange can also fire during navigation => ignore very recent events + return ''; + } + return undefined; + } + + onTimeout() { + if (document.visibilityState === 'hidden') { + // track pageview as exit when hidden on timeout + this.tracker.trackPageview({}, true); + } + } + + onVisibilityChange(ev: Event) { + this.lastEvent = ev; + this.visibilityState = document.visibilityState; + if (this.tracker.isMobile) { + // set timeout only on mobile devices + if (this.#timeout) { + clearTimeout(this.#timeout); + } + if (document.visibilityState === 'hidden') { + this.#timeout = setTimeout(() => { + this.onTimeout(); + }, this.#hiddenTimeout); + } + } + } +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..4d48d5f --- /dev/null +++ b/src/main.ts @@ -0,0 +1,28 @@ +import { Tracker } from './tracker'; +import type { ITrackerOptions } from './types'; + +const currentScript = document.currentScript; +const region = currentScript?.getAttribute('src')?.match(/([^\.]+)\.altcha\.org/)?.[1]; +const attrs = currentScript?.getAttributeNames() || []; +const options = attrs.reduce((acc, attr) => { + if (attr.startsWith('data-')) { + const name = attr.slice(5).replace(/\-([a-z])/g, (_, w: string) => { + return w.toUpperCase(); + }); + let value: string | boolean | null | undefined = currentScript?.getAttribute(attr); + if (value) { + if (value === 'true') { + value = true; + } else if (value === 'false') { + value = false; + } + acc[name] = value; + } + } + return acc; +}, {} as Record); + +export const tracker = new Tracker({ + apiUrl: region ? `https://${region}.altcha.org/api/v1/event` : undefined, + ...options +} as ITrackerOptions); diff --git a/src/tests/extensions/click.ext.test.ts b/src/tests/extensions/click.ext.test.ts new file mode 100644 index 0000000..4ae158f --- /dev/null +++ b/src/tests/extensions/click.ext.test.ts @@ -0,0 +1,69 @@ +/** + * @vitest-environment jsdom + */ + +import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest'; +import { ClickExtension } from '../../extensions/click.ext'; +import { Tracker } from '../../tracker'; + +describe('ClickExtension', () => { + const tracker = new Tracker({ + globalName: null, + projectId: '1' + }); + + it('should create a new instance and attache click listener', () => { + const addEventListenerSpy = vi.spyOn(globalThis, 'addEventListener'); + const result = new ClickExtension(tracker, {}); + expect(result).toBeInstanceOf(ClickExtension); + expect(addEventListenerSpy).toHaveBeenCalledWith('click', expect.any(Function), { + capture: true, + }); + }); + + describe('methods', () => { + const ext = new ClickExtension(tracker, {}); + + afterAll(() => { + if (ext) { + try { + ext.destroy(); + } catch {} + } + }); + + describe('.destroy()', () => { + it('should remove click', () => { + const removeEventListenerSpy = vi.spyOn(globalThis, 'removeEventListener'); + ext.destroy(); + expect(removeEventListenerSpy).toHaveBeenCalledWith('click', expect.any(Function)); + }); + }); + + describe('.getExitReason()', () => { + let a: HTMLAnchorElement; + + beforeEach(() => { + a = document.createElement('a'); + }); + + it('should return outbound url if clicked on a A element with external URL', () => { + a.setAttribute('href', 'https://google.com/'); + ext.onClick({ + target: a, + timeStamp: performance.now(), + } as any); + expect(ext.getExitReason()).toEqual('https://google.com/'); + }); + + it('should return undefined if clicked on a A element with internal URL', () => { + a.setAttribute('href', '/test'); + ext.onClick({ + target: a, + timeStamp: performance.now(), + } as any); + expect(ext.getExitReason()).toBeUndefined(); + }); + }); + }); +}); diff --git a/src/tests/extensions/cookie.ext.test.ts b/src/tests/extensions/cookie.ext.test.ts new file mode 100644 index 0000000..973aac3 --- /dev/null +++ b/src/tests/extensions/cookie.ext.test.ts @@ -0,0 +1,23 @@ +/** + * @vitest-environment jsdom + */ + +import { describe, expect, it, vi } from 'vitest' +import { CookieExtension } from '../../extensions/cookie.ext'; +import { Tracker } from '../../tracker'; + +describe('CookieExtenstion', () => { + const tracker = new Tracker({ + globalName: null, + projectId: '1', + }); + + it('should create a new instance and set new cookie', () => { + const getCookie = vi.spyOn(CookieExtension.prototype, 'getCookie'); + const setCookie = vi.spyOn(CookieExtension.prototype, 'setCookie'); + const result = new CookieExtension(tracker, {}); + expect(result).toBeInstanceOf(CookieExtension); + expect(getCookie).toHaveBeenCalledOnce(); + expect(setCookie).toHaveBeenCalledOnce(); + }) +}); diff --git a/src/tests/extensions/hash.ext.test.ts b/src/tests/extensions/hash.ext.test.ts new file mode 100644 index 0000000..21e226e --- /dev/null +++ b/src/tests/extensions/hash.ext.test.ts @@ -0,0 +1,49 @@ +/** + * @vitest-environment jsdom + */ + +import { afterAll, describe, expect, it, vi } from 'vitest'; +import { HashExtension } from '../../extensions/hash.ext'; +import { Tracker } from '../../tracker'; + +describe('HashExtension', () => { + const tracker = new Tracker({ + globalName: null, + projectId: '1' + }); + + it('should create a new instance and attache hashchange listener', () => { + const addEventListenerSpy = vi.spyOn(globalThis, 'addEventListener'); + const result = new HashExtension(tracker, {}); + expect(result).toBeInstanceOf(HashExtension); + expect(addEventListenerSpy).toHaveBeenCalledWith('hashchange', expect.any(Function)); + }); + + describe('methods', () => { + const ext = new HashExtension(tracker, {}); + + afterAll(() => { + if (ext) { + try { + ext.destroy(); + } catch {} + } + }); + + describe('.destroy()', () => { + it('should remove hashchange', () => { + const removeEventListenerSpy = vi.spyOn(globalThis, 'removeEventListener'); + ext.destroy(); + expect(removeEventListenerSpy).toHaveBeenCalledWith('hashchange', expect.any(Function)); + }); + }); + + describe('.onHashChange()', () => { + it('should call tracker.trackPageview()', () => { + const trackedPageviewSpy = vi.spyOn(tracker, 'trackPageview'); + ext.onHashChange(); + expect(trackedPageviewSpy).toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/src/tests/extensions/keyboard.ext.test.ts b/src/tests/extensions/keyboard.ext.test.ts new file mode 100644 index 0000000..668d3f3 --- /dev/null +++ b/src/tests/extensions/keyboard.ext.test.ts @@ -0,0 +1,103 @@ +/** + * @vitest-environment jsdom + */ + +import { afterAll, describe, expect, it, vi } from "vitest"; +import { KeyboardExtension } from "../../extensions/keyboard.ext"; +import { Tracker } from "../../tracker"; + +describe("KeyboardExtenstion", () => { + const tracker = new Tracker({ + globalName: null, + projectId: "1", + }); + + it("should create a new instance and attach keydown listener", () => { + const addEventListenerSpy = vi.spyOn(globalThis, "addEventListener"); + const result = new KeyboardExtension(tracker, {}); + expect(result).toBeInstanceOf(KeyboardExtension); + expect(addEventListenerSpy).toHaveBeenCalledWith( + "keydown", + expect.any(Function) + ); + }); + + describe("methods", () => { + const ext = new KeyboardExtension(tracker, {}); + + afterAll(() => { + if (ext) { + try { + ext.destroy(); + } catch {} + } + }); + + describe(".destroy()", () => { + it("should remove keydown", () => { + const removeEventListenerSpy = vi.spyOn( + globalThis, + "removeEventListener" + ); + ext.destroy(); + expect(removeEventListenerSpy).toHaveBeenCalledWith( + "keydown", + expect.any(Function) + ); + }); + }); + + describe(".isLastKeyboardEventCtrl()", () => { + it("should return true if CTRL", () => { + ext.onKeyDown({ + ctrlKey: true, + timeStamp: performance.now(), + } as KeyboardEvent); + expect(ext.isLastKeyboardEventCtrl()).toBe(true); + }); + + it("should return true if META", () => { + ext.onKeyDown({ + metaKey: true, + timeStamp: performance.now(), + } as KeyboardEvent); + expect(ext.isLastKeyboardEventCtrl()).toBe(true); + }); + }); + + describe(".getExitReason()", () => { + it("should return undefined if CTRL key was pressed but without unload", () => { + ext.onKeyDown({ + ctrlKey: true, + timeStamp: performance.now(), + } as KeyboardEvent); + expect(ext.getExitReason(false)).toBe(undefined); + }); + + it("should return undefined if CTRL key was pressed but a second ago", async () => { + ext.onKeyDown({ + ctrlKey: true, + timeStamp: performance.now(), + } as KeyboardEvent); + await new Promise((r) => setTimeout(r, 1000)); + expect(ext.getExitReason(true)).toBe(undefined); + }); + + it("should return empty string if CTRL key was pressed before unload", () => { + ext.onKeyDown({ + ctrlKey: true, + timeStamp: performance.now(), + } as KeyboardEvent); + expect(ext.getExitReason(true)).toBe(""); + }); + + it("should return empty string if META key was pressed before unload", () => { + ext.onKeyDown({ + metaKey: true, + timeStamp: performance.now(), + } as KeyboardEvent); + expect(ext.getExitReason(true)).toBe(""); + }); + }); + }); +}); diff --git a/src/tests/extensions/mouse.ext.test.ts b/src/tests/extensions/mouse.ext.test.ts new file mode 100644 index 0000000..e5b45d5 --- /dev/null +++ b/src/tests/extensions/mouse.ext.test.ts @@ -0,0 +1,78 @@ +/** + * @vitest-environment jsdom + */ + +import { afterAll, describe, expect, it, vi } from 'vitest' +import { MouseExtension } from '../../extensions/mouse.ext'; +import { Tracker } from '../../tracker'; + +describe('MouseExtenstion', () => { + const tracker = new Tracker({ + globalName: null, + projectId: '1', + }); + + it('should create a new instance and attach mouseleave listener', () => { + const addEventListenerSpy = vi.spyOn(document.body, "addEventListener"); + const result = new MouseExtension(tracker, {}); + expect(result).toBeInstanceOf(MouseExtension); + expect(addEventListenerSpy).toHaveBeenCalledWith( + "mouseleave", + expect.any(Function) + ); + }); + + describe("methods", () => { + const ext = new MouseExtension(tracker, {}); + + afterAll(() => { + if (ext) { + try { + ext.destroy(); + } catch {} + } + }); + + describe(".destroy()", () => { + it("should remove mouseleave", () => { + const removeEventListenerSpy = vi.spyOn( + document.body, + "removeEventListener" + ); + ext.destroy(); + expect(removeEventListenerSpy).toHaveBeenCalledWith( + "mouseleave", + expect.any(Function) + ); + }); + }); + + describe('.getExitReason()', () => { + it('should return empty string if cursor is outside the viewport', () => { + ext.onMouseLeave({ + clientX: 500, + clientY: -1, + } as MouseEvent); + expect(ext.getExitReason()).toBe(''); + }); + + it('should return undefined if cursor is outside the viewport but in the top left corner (navigation buttons)', () => { + ext.onMouseLeave({ + clientX: 100, + clientY: -1, + } as MouseEvent); + ext.onMouseEnter(); + expect(ext.getExitReason()).toBe(undefined); + }); + + it('should return undefined if cursor is inside the viewport', () => { + ext.onMouseLeave({ + clientX: 500, + clientY: -1, + } as MouseEvent); + ext.onMouseEnter(); + expect(ext.getExitReason()).toBe(undefined); + }); + }); + }); +}); diff --git a/src/tests/extensions/pushstate.ext.test.ts b/src/tests/extensions/pushstate.ext.test.ts new file mode 100644 index 0000000..f4beb17 --- /dev/null +++ b/src/tests/extensions/pushstate.ext.test.ts @@ -0,0 +1,56 @@ +/** + * @vitest-environment jsdom + */ + +import { afterAll, describe, expect, it, vi } from 'vitest'; +import { PushstateExtension } from '../../extensions/pushstate.ext'; +import { Tracker } from '../../tracker'; + +describe('PushstateExtension', () => { + const tracker = new Tracker({ + globalName: null, + projectId: '1' + }); + + it('should create a new instance and attache popstate listener', () => { + const addEventListenerSpy = vi.spyOn(globalThis, 'addEventListener'); + const result = new PushstateExtension(tracker, {}); + expect(result).toBeInstanceOf(PushstateExtension); + expect(addEventListenerSpy).toHaveBeenCalledWith('popstate', expect.any(Function)); + }); + + it('should replace history.pushState function', () => { + const originalPushState = history.pushState; + const ext = new PushstateExtension(tracker, {}); + expect(history.pushState).not.toEqual(originalPushState); + ext.destroy(); + }); + + describe('methods', () => { + const ext = new PushstateExtension(tracker, {}); + + afterAll(() => { + if (ext) { + try { + ext.destroy(); + } catch {} + } + }); + + describe('.destroy()', () => { + it('should remove popstate', () => { + const removeEventListenerSpy = vi.spyOn(globalThis, 'removeEventListener'); + ext.destroy(); + expect(removeEventListenerSpy).toHaveBeenCalledWith('popstate', expect.any(Function)); + }); + }); + + describe('.onPushState()', () => { + it('should call tracker.trackPageview()', () => { + const trackedPageviewSpy = vi.spyOn(tracker, 'trackPageview'); + ext.onPushState(); + expect(trackedPageviewSpy).toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/src/tests/extensions/visibility.ext.test.ts b/src/tests/extensions/visibility.ext.test.ts new file mode 100644 index 0000000..93c2997 --- /dev/null +++ b/src/tests/extensions/visibility.ext.test.ts @@ -0,0 +1,41 @@ +/** + * @vitest-environment jsdom + */ + +import { afterAll, describe, expect, it, vi } from 'vitest'; +import { VisibilityExtension } from '../../extensions/visibility.ext'; +import { Tracker } from '../../tracker'; + +describe('VisibilityExtension', () => { + const tracker = new Tracker({ + globalName: null, + projectId: '1' + }); + + it('should create a new instance and attache visibilitychange listener', () => { + const addEventListenerSpy = vi.spyOn(globalThis, 'addEventListener'); + const result = new VisibilityExtension(tracker, {}); + expect(result).toBeInstanceOf(VisibilityExtension); + expect(addEventListenerSpy).toHaveBeenCalledWith('visibilitychange', expect.any(Function)); + }); + + describe('methods', () => { + const ext = new VisibilityExtension(tracker, {}); + + afterAll(() => { + if (ext) { + try { + ext.destroy(); + } catch {} + } + }); + + describe('.destroy()', () => { + it('should remove visibilitychange', () => { + const removeEventListenerSpy = vi.spyOn(globalThis, 'removeEventListener'); + ext.destroy(); + expect(removeEventListenerSpy).toHaveBeenCalledWith('visibilitychange', expect.any(Function)); + }); + }); + }); +}); diff --git a/src/tests/tracker.test.ts b/src/tests/tracker.test.ts new file mode 100644 index 0000000..9761be4 --- /dev/null +++ b/src/tests/tracker.test.ts @@ -0,0 +1,386 @@ +/** + * @vitest-environment jsdom + */ + +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { Tracker } from '../tracker'; +import type { IEvent, ITrackerOptions } from '../types'; + +describe('Tracker', () => { + + describe('constructor', () => { + it('should throw error if projectId is not provided', () => { + expect(() => new Tracker({ + globalName: null, + } as ITrackerOptions)).toThrow(); + }); + + it('should throw error if projectId is an empty string', () => { + expect(() => new Tracker({ + globalName: null, + projectId: '', + })).toThrow(); + }); + + it('should create a new instance and attach pagehide and beforeunload listeners', () => { + const addEventListenerSpy = vi.spyOn(globalThis, "addEventListener"); + expect(new Tracker({ + globalName: null, + projectId: '1' + })).toBeInstanceOf(Tracker); + expect(addEventListenerSpy).toHaveBeenCalledWith( + "pagehide", + expect.any(Function) + ); + expect(addEventListenerSpy).toHaveBeenCalledWith( + "beforeunload", + expect.any(Function) + ); + }); + + it('should register a global reference to the instance', () => { + const t = new Tracker({ + projectId: '1', + }); + // @ts-expect-error + expect(globalThis[Tracker.DEAFAUL_GLOBAL_NAME]).toEqual(t); + t.destroy(); + }); + + it('should throw if another global reference is already registered', () => { + const t = new Tracker({ + projectId: '1', + }); + expect(() => new Tracker({ + projectId: '1', + } as ITrackerOptions)).toThrow(); + t.destroy(); + }); + + it('should register a global reference to the instance with a configured name', () => { + const t = new Tracker({ + globalName: 'test', + projectId: '1', + }); + // @ts-expect-error + expect(globalThis.test).toEqual(t); + t.destroy(); + }); + + it('should load default extensions', () => { + const loadExtensionsSpy = vi.spyOn(Tracker.prototype, "loadExtensions"); + const tracker = new Tracker({ + globalName: null, + projectId: '1' + }); + expect(loadExtensionsSpy).toHaveBeenCalledOnce(); + expect(Tracker.DEFAULT_EXTENSIONS.length).toBeGreaterThan(0); + expect(Object.keys(tracker.extensions)).toEqual(Tracker.DEFAULT_EXTENSIONS); + }); + + it('should disable all extensions', () => { + const tracker = new Tracker({ + globalName: null, + click: false, + keyboard: false, + mouse: false, + pushstate: false, + visibility: false, + projectId: '1' + }); + expect(Object.keys(tracker.extensions).length).toEqual(0); + }); + + it('should enable only pushstate extension', () => { + const tracker = new Tracker({ + globalName: null, + click: false, + keyboard: false, + mouse: false, + pushstate: true, + visibility: false, + projectId: '1' + }); + expect(Object.keys(tracker.extensions)).toEqual(['pushstate']); + }); + }); + + describe('properties', () => { + let tracker: Tracker; + + afterEach(() => { + if (tracker) { + tracker.destroy(); + } + }); + + beforeEach(() => { + tracker = new Tracker({ + globalName: null, + projectId: '1', + }); + vi.restoreAllMocks(); + }); + + describe('.apiUrl', () => { + it('should return default API URL', () => { + expect(tracker.apiUrl).toEqual(Tracker.DEFAULT_API_URL); + }); + + it('should return custom API URL when configured', () => { + const apiUrl = 'http://example.com/event'; + const t = new Tracker({ + apiUrl, + globalName: null, + projectId: '1', + }); + expect(t.apiUrl).toEqual(apiUrl); + t.destroy(); + }); + }); + + describe('.isDNTEnabled', () => { + it('should return true if navigator.doNotTrack is 1', () => { + // @ts-ignore + navigator.doNotTrack = '1'; + expect(tracker.isDNTEnabled).toEqual(true); + // @ts-ignore + navigator.doNotTrack = '0'; + }); + + it('should return true if navigator.globalPrivacyControl is true', () => { + // @ts-ignore + navigator.globalPrivacyControl = true; + expect(tracker.isDNTEnabled).toEqual(true); + // @ts-ignore + navigator.globalPrivacyControl = false; + }); + }); + }); + + describe('methods', () => { + let tracker: Tracker; + + afterEach(() => { + if (tracker) { + tracker.destroy(); + } + }); + + beforeEach(() => { + tracker = new Tracker({ + globalName: null, + projectId: '1', + }); + vi.restoreAllMocks(); + }); + + describe('.destroy()', () => { + it('should destroy the instance and remove pagehide listener', () => { + const removeEventListenerSpy = vi.spyOn( + globalThis, + "removeEventListener" + ); + tracker.destroy(); + expect(removeEventListenerSpy).toHaveBeenCalledWith( + "pagehide", + expect.any(Function) + ); + }); + }); + + describe('.getBeaconPayload()', () => { + it('should return an object with events, time and projectId', () => { + const events: IEvent[] = [{ + timestamp: Date.now(), + }]; + expect(tracker.getBeaconPayload(events)).toEqual({ + events, + projectId: tracker.options.projectId, + time: expect.any(Number), + }); + }); + }); + + describe('.getHostname()', () => { + it('should return current location hostnane (localhost by default in JSDOM)', () => { + expect(tracker.getHostname()).toEqual('localhost'); + }); + }); + + describe('.getPageviewOptions()', () => { + it('should return options for pageview event', () => { + expect(tracker.getPageviewOptions()).toEqual({ + appVersion: undefined, + duration: expect.any(Number), + exit: false, + outbound: undefined, + referrer: expect.any(String), + returning: undefined, + view: expect.any(String), + }); + }); + + it('should return exit reason from .getExitReason()', () => { + const getExitReasonSpy = vi.spyOn(tracker, 'getExitReason'); + expect(tracker.getPageviewOptions().exit).toEqual(false); + expect(getExitReasonSpy).toHaveBeenCalled(); + }); + + it('should return duration', () => { + tracker.lastPageLoadAt = performance.now() - 1100; + expect(tracker.getPageviewOptions().duration).toBeGreaterThan(1000); + }); + + it('should return referrer from .getReferrer()', () => { + const getReferrerSpy = vi.spyOn(tracker, 'getReferrer'); + expect(tracker.getPageviewOptions().referrer).toEqual(tracker.getReferrer()); + expect(getReferrerSpy).toHaveBeenCalled(); + }); + + it('should return view for .getView()', () => { + const getViewSpy = vi.spyOn(tracker, 'getView'); + expect(tracker.getPageviewOptions().view).toEqual(tracker.getView()); + expect(getViewSpy).toHaveBeenCalled(); + }); + }); + + describe('.getReferrer()', () => { + it('should return the document referrer (empty string by default)', () => { + expect(tracker.getReferrer()).toEqual(''); + }); + }); + + describe('.getView()', () => { + it('should return current location href', () => { + expect(tracker.getView()).toEqual(location.href); + }); + + it('should return call .sanitizeUrl()', () => { + const sanitizeUrlSpy = vi.spyOn(tracker, 'sanitizeUrl'); + expect(tracker.getView()).toEqual(location.href); + expect(sanitizeUrlSpy).toHaveBeenCalled(); + }); + }); + + describe('.hasExtension()', () => { + it('should return true if extension is loaded', () => { + expect(tracker.hasExtension('pushstate')).toEqual(true); + }); + it('should return true if extension is not loaded', () => { + expect(tracker.hasExtension('nonexistent' as any)).toEqual(false); + }); + }); + + describe('.log()', () => { + it('should not log anything if debug is disabled', () => { + const logSpy = vi.spyOn(console, 'log'); + tracker.log('test'); + expect(logSpy).not.toHaveBeenCalled(); + }); + + it('should log if debug is enabled', () => { + const logSpy = vi.spyOn(console, 'log').mockImplementation(() => void 0); + tracker.options.debug = true; + tracker.log('test'); + expect(logSpy).toHaveBeenCalledOnce(); + }); + }); + + describe('.sanitizeUrl()', () => { + it('should remove search params and hash', () => { + const url = 'https://localhost:1234/test' + expect(tracker.sanitizeUrl(url + '?test=123#removethis')).toEqual(url); + }); + + it('should remove only non-whitelisted search params', () => { + const url = 'https://localhost:1234/test' + const t = new Tracker({ + allowSearchParams: ['test1', 'test2'], + globalName: null, + projectId: '1' + }); + expect(t.sanitizeUrl(url + '?abc=test&test1=1&test2=2&test3=3')).toEqual(url + '?test1=1&test2=2'); + t.destroy(); + }); + }); + + describe('.trackEvent()', () => { + it('should record an event', () => { + expect(tracker.trackedEvents).toEqual(0); + tracker.trackEvent({ + customEvent: 'test', + props: { + customerPlan: 'free', + }, + }); + expect(tracker.trackedEvents).toEqual(1); + expect(tracker.events.length).toEqual(1); + expect(tracker.events[0].timestamp).toBeGreaterThan(0); + }); + }); + + describe('.trackPageview()', () => { + it('should record a pageview event', () => { + expect(tracker.trackedEvents).toEqual(0); + tracker.trackPageview(); + expect(tracker.trackedEvents).toEqual(1); + expect(tracker.events.length).toEqual(1); + expect(tracker.events[0].timestamp).toBeGreaterThan(0); + }); + }); + + describe('.flushEvents()', () => { + it('should reset the internal events array', () => { + tracker.trackEvent({ + customEvent: 'test', + }); + expect(tracker.events.length).toEqual(1); + tracker.flushEvents(); + expect(tracker.events.length).toEqual(0); + }); + + it('should call sendBeacon() with recoded events', () => { + const sendBeaconSpy = vi.spyOn(tracker, 'sendBeacon'); + tracker.trackEvent({ + customEvent: 'test', + }); + const events = [...tracker.events]; + tracker.flushEvents(); + expect(sendBeaconSpy).toHaveBeenCalledWith(events); + }); + }); + + describe('.sendBeacon()', () => { + it('should call navigator.sendBeacon() with JSON data', () => { + let sendBeaconArgs: any[] = []; + navigator.sendBeacon = (...args: any[]) => { + sendBeaconArgs = args; + return true; + }; + const sendBeaconSpy = vi.spyOn(navigator, 'sendBeacon'); + tracker.trackEvent({ + customEvent: 'test', + }); + const events = [...tracker.events]; + tracker.flushEvents(); + expect(sendBeaconSpy).toHaveBeenCalledWith(tracker.apiUrl, expect.any(String)); + expect(JSON.parse(sendBeaconArgs[1])).toEqual({ + events, + projectId: tracker.options.projectId, + time: expect.any(Number), + }) + }); + }); + + describe('.onPageHide()', () => { + it('should track pageview and flush events', () => { + const trackPageviewSpy = vi.spyOn(tracker, 'trackPageview'); + const flushEventsSpy = vi.spyOn(tracker, 'flushEvents'); + tracker.onPageHide(); + expect(trackPageviewSpy).toHaveBeenCalledWith({}, true); + expect(flushEventsSpy).toHaveBeenCalled(); + }); + }); + }); +}); \ No newline at end of file diff --git a/src/tracker.ts b/src/tracker.ts new file mode 100644 index 0000000..4c7d59b --- /dev/null +++ b/src/tracker.ts @@ -0,0 +1,286 @@ +import { BaseExtension } from './extensions/base.ext'; +import { ClickExtension } from './extensions/click.ext'; +import { CookieExtension } from './extensions/cookie.ext'; +import { HashExtension } from './extensions/hash.ext'; +import { KeyboardExtension } from './extensions/keyboard.ext'; +import { MouseExtension } from './extensions/mouse.ext'; +import { PushstateExtension } from './extensions/pushstate.ext'; +import { VisibilityExtension } from './extensions/visibility.ext'; +import type { IBaseExtensionOptions, IEvent, ITrackerOptions } from './types'; + +type TExtentionName = keyof typeof Tracker.EXTENSIONS; + +export class Tracker { + static readonly EXTENSIONS = { + click: ClickExtension, + cookie: CookieExtension, + hash: HashExtension, + keyboard: KeyboardExtension, + mouse: MouseExtension, + pushstate: PushstateExtension, + visibility: VisibilityExtension, + } as const; + + static readonly DEFAULT_API_URL: string = 'https://eu.altcha.org/api/v1/event'; + + static readonly DEFAULT_EXTENSIONS: TExtentionName[] = [ + 'click', + 'keyboard', + 'mouse', + 'pushstate', + 'visibility', + ]; + + static readonly DEAFAUL_GLOBAL_NAME: string = 'altchaTracker'; + + readonly #_onPageHide = this.onPageHide.bind(this); + + readonly isMobile: boolean = /Mobi|Android|iPhone|iPad|iPod|Opera Mini/i.test(navigator.userAgent || ''); + + events: IEvent[] = []; + + extensions = {} as Record>; + + globalName: string | null = null; + + lastPageLoadAt: number = performance.now(); + + lastPageview: string | null = null; + + returningVisitor: boolean | null = null; + + trackedEvents: number = 0; + + trackedPageviews: number = 0; + + get apiUrl() { + return this.options.apiUrl || Tracker.DEFAULT_API_URL; + } + + get isDNTEnabled() { + return ( + ('doNotTrack' in navigator && navigator.doNotTrack === '1') || + ('globalPrivacyControl' in navigator && navigator.globalPrivacyControl === true) + ); + } + + constructor(readonly options: ITrackerOptions) { + if (!options.projectId) { + throw new Error('Parameter projectId required.'); + } + this.#registerGlobalName(); + if (this.isDNTEnabled && this.options.respectDNT === true) { + this.log('DoNotTrack enabled.'); + } else { + this.loadExtensions(); + // attach both pagehide and beforeunload listeners to handle inconsitent behaviour across browsers + // safari does not fire beforeunload after clicking on an external link + // brave does not fire pagehide after clicking on an external link + addEventListener('pagehide', this.#_onPageHide); + addEventListener('beforeunload', this.#_onPageHide); + } + } + + destroy() { + this.flushEvents(); + for (const ext in this.extensions) { + this.extensions[ext as TExtentionName].destroy(); + } + this.extensions = {} as Record>; + removeEventListener('pagehide', this.#_onPageHide); + removeEventListener('beforeunload', this.#_onPageHide); + if (this.globalName) { + // @ts-expect-error + delete globalThis[this.globalName]; + } + } + + flushEvents() { + const events = this.events.splice(0); + if (events.length) { + this.sendBeacon(events); + } + } + + getBeaconPayload(events: IEvent[]) { + return { + events, + projectId: this.options.projectId, + time: Date.now(), + uniqueId: this.options.uniqueId + }; + } + + getExitReason(unload: boolean = false) { + for (const ext in this.extensions) { + const result = this.extensions[ext as TExtentionName].getExitReason(unload); + if (result !== undefined) { + this.log('exit reason:', { ext, result }); + return result; + } + } + return undefined; + } + + getExtension(name: TExtentionName) { + return this.extensions[name]; + } + + getHostname() { + return location.hostname; + } + + getOrigin() { + return location.origin; + } + + getView() { + return this.sanitizeUrl(location.href); + } + + getPageviewOptions(unload: boolean = false) { + const exitReason = this.getExitReason(unload); + const referrer = this.getReferrer(); + const origin = this.getOrigin(); + return { + appVersion: this.options.appVersion, + exit: exitReason !== undefined, + outbound: exitReason, + duration: Math.max(0, Math.floor(performance.now() - this.lastPageLoadAt)), + referrer: referrer && new URL(referrer, origin).origin === origin ? '' : referrer, + returning: this.returningVisitor === null ? undefined : this.returningVisitor, + view: this.getView() + }; + } + + getReferrer() { + return document.referrer; + } + + hasExtension(name: TExtentionName) { + return !!this.getExtension(name); + } + + loadExtensions() { + for (const ext in Tracker.EXTENSIONS) { + let extOptions: IBaseExtensionOptions | boolean = + this.options?.[ext as TExtentionName] !== undefined + ? this.options[ext as TExtentionName]! + : ({} as IBaseExtensionOptions); + if (typeof extOptions === 'boolean') { + extOptions = { + disable: !extOptions + }; + } else { + extOptions.disable = + extOptions.disable === undefined + ? !Tracker.DEFAULT_EXTENSIONS.includes(ext as TExtentionName) + : extOptions.disable; + } + if (extOptions.disable !== true) { + this.extensions[ext as TExtentionName] = new Tracker.EXTENSIONS[ext as TExtentionName]( + this, + extOptions + ); + } + } + } + + log(...args: any[]) { + if (this.options.debug) { + console.log('[ALTCHA Tracker]', ...args); + } + } + + sanitizeUrl(url: string | URL) { + url = new URL(url); + if (this.options.allowSearchParams?.length && url.hostname === this.getHostname()) { + // remove only non-whitelisted params (only for current hostname) + for (const [param] of url.searchParams) { + if (!this.options.allowSearchParams.includes(param)) { + url.searchParams.delete(param); + } + } + } else { + // remove all search params + url.search = ''; + } + if (!this.hasExtension('hash')) { + url.hash = ''; + } + return url.toString(); + } + + sendBeacon(events: IEvent[]) { + if ('sendBeacon' in navigator) { + return navigator.sendBeacon(this.apiUrl, JSON.stringify(this.getBeaconPayload(events))); + } + } + + trackEvent(options: Partial = {}, unload: boolean = false) { + const event = { + timestamp: Date.now(), + ...options + }; + this.events.push(event); + this.trackedEvents += 1; + this.log('trackEvent', event); + if (unload) { + this.flushEvents(); + } + return true; + } + + trackPageview(options: Partial = {}, unload: boolean = false) { + const pageviewOptions = this.getPageviewOptions(unload); + if ( + this.events.length && + pageviewOptions.duration < 100 && + this.events[this.events.length - 1].view === pageviewOptions.view + ) { + this.log('duplicate pageview', pageviewOptions); + return false; + } + this.log('trackPageview', pageviewOptions); + this.trackEvent( + { + ...pageviewOptions, + ...options + }, + unload + ); + this.trackedPageviews += 1; + this.lastPageLoadAt = performance.now(); + this.lastPageview = pageviewOptions.view; + if (pageviewOptions.exit) { + // reset pageview counter to recognize the new enter event after back button + this.trackedPageviews = 0; + } + return true; + } + + onPageHide() { + if (this.lastPageview !== this.getView()) { + // track only if the path is different from the previously recorder one. + // This is workaround for Safari's Back button from external link which fires pagehide on load + this.trackPageview({}, true); + } + } + + #registerGlobalName() { + this.globalName = + this.options.globalName === undefined + ? Tracker.DEAFAUL_GLOBAL_NAME + : this.options.globalName || null; + if (this.globalName) { + // @ts-expect-error + if (globalThis[this.globalName]) { + throw new Error( + 'Another instance of the Tracker is already present in globalThis. Set globalName:null to disable global reference.' + ); + } + // @ts-expect-error + globalThis[this.globalName] = this; + } + } +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..ea11996 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,41 @@ +export interface IBaseExtensionOptions { + disable?: boolean; +} + +export interface ICookieExtensionOptions extends IBaseExtensionOptions { + cookieExpireDays?: number; + cookieName?: string; + cookiePath?: string; +} + +export interface IVivibilityExtensionOptions extends IBaseExtensionOptions { + hiddenTimeout?: number; +} + +export interface IEvent { + customEvent?: string; + duration?: number; + exit?: boolean; + props?: Record; + returning?: boolean; + timestamp: number; + view?: string; +} + +export interface ITrackerOptions { + allowSearchParams?: string[]; + apiUrl?: string; + appVersion?: string; + click?: IBaseExtensionOptions | boolean; + cookie?: ICookieExtensionOptions | boolean; + debug?: boolean; + globalName?: string | null | false; + hash?: IBaseExtensionOptions | boolean; + keyboard?: IBaseExtensionOptions | boolean; + mouse?: IBaseExtensionOptions | boolean; + projectId: string; + pushstate?: IBaseExtensionOptions | boolean; + respectDNT?: boolean; + uniqueId?: string; + visibility?: IVivibilityExtensionOptions | boolean; +} \ No newline at end of file diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..0511b9f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/vite.bundle.config.js b/vite.bundle.config.js new file mode 100644 index 0000000..d88b5a0 --- /dev/null +++ b/vite.bundle.config.js @@ -0,0 +1,13 @@ +import { defineConfig } from 'vite' + +export default defineConfig({ + build: { + outDir: 'dist_bundle', + lib: { + entry: 'src/main.ts', + name: 'AltchaAnalyticsTracker', + fileName: () => 'tracker.js', + formats: ['umd'] + }, + }, +}) \ No newline at end of file diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..eb991a4 --- /dev/null +++ b/vite.config.js @@ -0,0 +1,11 @@ +import { defineConfig } from 'vite' + +export default defineConfig({ + build: { + lib: { + entry: 'src/tracker.ts', + name: 'AltchaAnalyticsTracker', + formats: ['es', 'umd'] + }, + }, +}) \ No newline at end of file