{"_id":"@3halves-labs/score-tracker","_rev":"21-d65e87a593555fe30e232ffc03d355ed","name":"@3halves-labs/score-tracker","dist-tags":{"latest":"1.21.2"},"versions":{"1.0.13":{"name":"@3halves-labs/score-tracker","version":"1.0.13","license":"MIT","_id":"@3halves-labs/score-tracker@1.0.13","maintainers":[{"name":"3halves-labs","email":"info@3halves-labs.com"}],"dist":{"shasum":"d436e6023f515e9ebee36a83ad1c44570659005a","tarball":"https://registry.npmjs.org/@3halves-labs/score-tracker/-/score-tracker-1.0.13.tgz","fileCount":15,"integrity":"sha512-lMy9T0Rj37/oygXZLwF09mtACOKGhNNCio67OWkBoVSGHWzk4kdNRsphYk7zwz07Ik7zceaHUSsq61DP9Xgkng==","signatures":[{"sig":"MEUCIQDh6K+N3rJPn8/VMLA2xT8/ix8QX/H5Eos92kVr2V+lRQIgKWoDB+r+6c8q5fQpfaAtJcSi+9ftMVLDkaFPiUCyS6Y=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":71371},"main":"dist/index.cjs.js","types":"dist/index.d.ts","module":"dist/index.esm.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.esm.js","require":"./dist/index.cjs.js"},"./react":{"types":"./dist/react/index.d.ts","import":"./dist/react/index.esm.js","require":"./dist/react/index.cjs.js"}},"gitHead":"911b89d33bb270f64fba3a22a6af846a4b8b74be","scripts":{"dev":"tsup --watch","build":"tsup","clean":"rimraf dist","typecheck":"tsc --noEmit","prepublishOnly":"npm run clean && npm run build"},"_npmUser":{"name":"3halves-labs","email":"info@3halves-labs.com"},"repository":{"url":"https://sourcecode.3halves-labs.com/3hl/score-tracker-react-sdk-with-ci","type":"git"},"_npmVersion":"10.8.2","description":"Lightweight React-friendly website tracking SDK for Score platform. Batching, session/UTM, SPA route, scroll, video, and custom events.","directories":{},"sideEffects":false,"_nodeVersion":"20.19.4","publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.1","rimraf":"^6.0.1","typescript":"^5.5.4","@types/react":"^18.3.3"},"peerDependencies":{"react":">=17 || >=18"},"_npmOperationalInternal":{"tmp":"tmp/score-tracker_1.0.13_1755008493589_0.7168493164517673","host":"s3://npm-registry-packages-npm-production"}},"1.2.2":{"name":"@3halves-labs/score-tracker","version":"1.2.2","keywords":["tracking","analytics","loyalty","score","gamification","react"],"author":{"name":"3 Halves Labs"},"license":"MIT","_id":"@3halves-labs/score-tracker@1.2.2","maintainers":[{"name":"3halves-labs","email":"info@3halves-labs.com"}],"homepage":"https://github.com/3halveslabs/score-tracker#readme","dist":{"shasum":"3cc0a2b2e60f648ed7834c18280dfc425aa8b5f9","tarball":"https://registry.npmjs.org/@3halves-labs/score-tracker/-/score-tracker-1.2.2.tgz","fileCount":15,"integrity":"sha512-0Y4iXnvX/ApwfdjjkpnJBppyR3laFt0YjoDmHAEJs26pABcbSMSbYtd7quPO7JcTUbHrNxvkRNhPpgfc+7qvAQ==","signatures":[{"sig":"MEQCICozfwhiSO8TPwfQpcbHKFlPAgwMAcqg1w6qACuoNq+hAiAZVX4PxR6wQMsTFLTlopOfkgEwnz/BkNNHfWKooQt30Q==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":125352},"main":"dist/index.cjs.js","types":"dist/index.d.ts","module":"dist/index.esm.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.esm.js","require":"./dist/index.cjs.js"},"./react":{"types":"./dist/react/index.d.ts","import":"./dist/react/index.esm.js","require":"./dist/react/index.cjs.js"}},"gitHead":"07a8b885f83f4b006f6f6a39765acef22ada5995","scripts":{"dev":"tsup --watch","test":"vitest run","build":"tsup","clean":"rimraf dist","release":"../scripts/release-sdk.sh sdk sdk","typecheck":"tsc --noEmit","test:watch":"vitest","release:major":"../scripts/release-sdk.sh sdk sdk major","release:minor":"../scripts/release-sdk.sh sdk sdk minor","release:patch":"../scripts/release-sdk.sh sdk sdk patch","prepublishOnly":"npm run clean && npm run build"},"_npmUser":{"name":"3halves-labs","email":"info@3halves-labs.com"},"repository":{"url":"https://gitea.3halves-labs.com/3halveslabs/score-anything-loyalty.git","type":"git","directory":"sdk"},"_npmVersion":"10.7.0","description":"Lightweight React-friendly website tracking SDK for Score Anything Loyalty. Batching, session/UTM, SPA route, scroll, video, and custom events.","directories":{},"sideEffects":false,"_nodeVersion":"18.20.4","publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.1","jsdom":"^25.0.1","rimraf":"^6.0.1","vitest":"^2.1.9","typescript":"^5.5.4","@types/react":"^18.3.3"},"peerDependencies":{"react":">=17 || >=18"},"_npmOperationalInternal":{"tmp":"tmp/score-tracker_1.2.2_1779982222552_0.6306710378311922","host":"s3://npm-registry-packages-npm-production"}},"1.3.0":{"name":"@3halves-labs/score-tracker","version":"1.3.0","keywords":["tracking","analytics","loyalty","score","gamification","react"],"author":{"name":"3 Halves Labs"},"license":"MIT","_id":"@3halves-labs/score-tracker@1.3.0","maintainers":[{"name":"3halves-labs","email":"info@3halves-labs.com"}],"homepage":"https://github.com/3halveslabs/score-tracker#readme","dist":{"shasum":"f621b1ebb0497b1ff546d5b6fe6d2119ad46618b","tarball":"https://registry.npmjs.org/@3halves-labs/score-tracker/-/score-tracker-1.3.0.tgz","fileCount":15,"integrity":"sha512-0yCTbDJDAvEAtUO8Y/LeT6ZnUESro5Fk8rheXtRb79tM4GMcN51t6/BQ/pBT1u4SX8Hp1ZKCQ8jm7Obs1n+NIg==","signatures":[{"sig":"MEUCIQDVJ88YjLaMoRicHxt25qRNRUGMhLC+vDfDt8tkmI8oYgIgQFMMFX65S9y0A3HxN0d3lTj6OKGM2QV1WrL642xlDD4=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":179782},"main":"dist/index.cjs.js","types":"dist/index.d.ts","module":"dist/index.esm.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.esm.js","require":"./dist/index.cjs.js"},"./react":{"types":"./dist/react/index.d.ts","import":"./dist/react/index.esm.js","require":"./dist/react/index.cjs.js"}},"gitHead":"7c1282cbbf4dcbe5346a34b7bfa97bb8cfc450ae","scripts":{"dev":"tsup --watch","test":"vitest run","build":"tsup","clean":"rimraf dist","release":"../scripts/release-sdk.sh sdk sdk","typecheck":"tsc --noEmit","test:watch":"vitest","release:major":"../scripts/release-sdk.sh sdk sdk major","release:minor":"../scripts/release-sdk.sh sdk sdk minor","release:patch":"../scripts/release-sdk.sh sdk sdk patch","prepublishOnly":"npm run clean && npm run build"},"_npmUser":{"name":"3halves-labs","email":"info@3halves-labs.com"},"repository":{"url":"https://gitea.3halves-labs.com/3halveslabs/score-anything-loyalty.git","type":"git","directory":"sdk"},"_npmVersion":"10.7.0","description":"Lightweight React-friendly website tracking SDK for Score Anything Loyalty. Batching, session/UTM, SPA route, scroll, video, and custom events.","directories":{},"sideEffects":false,"_nodeVersion":"18.20.4","publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.1","jsdom":"^25.0.1","rimraf":"^6.0.1","vitest":"^2.1.9","typescript":"^5.5.4","@types/react":"^18.3.3","fake-indexeddb":"6.2.5"},"peerDependencies":{"react":">=17 || >=18"},"_npmOperationalInternal":{"tmp":"tmp/score-tracker_1.3.0_1779984505573_0.849341057122706","host":"s3://npm-registry-packages-npm-production"}},"1.4.0":{"name":"@3halves-labs/score-tracker","version":"1.4.0","keywords":["tracking","analytics","loyalty","score","gamification","react"],"author":{"name":"3 Halves Labs"},"license":"MIT","_id":"@3halves-labs/score-tracker@1.4.0","maintainers":[{"name":"3halves-labs","email":"info@3halves-labs.com"}],"homepage":"https://github.com/3halveslabs/score-tracker#readme","dist":{"shasum":"e9287612dd2ed457fb286ff748e954b288af1bcf","tarball":"https://registry.npmjs.org/@3halves-labs/score-tracker/-/score-tracker-1.4.0.tgz","fileCount":15,"integrity":"sha512-AVLDx3aME/xU9NKY5CFXlfk0Ti3NMG0pPEbvi1gJ8T6Au3/ywMZ/YiDZYxlgx3xlHGBMm88MdpHUxeYHJ8zygA==","signatures":[{"sig":"MEUCIH46b/eB5f1boZxq/H2F8rkn6nd0wXNBSNhQJ5nNhvYFAiEAmY6tKHnE9UpPcMYpGMBSBb5a5JW0VmI3+nB204fbQmA=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":195261},"main":"dist/index.cjs.js","types":"dist/index.d.ts","module":"dist/index.esm.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.esm.js","require":"./dist/index.cjs.js"},"./react":{"types":"./dist/react/index.d.ts","import":"./dist/react/index.esm.js","require":"./dist/react/index.cjs.js"}},"gitHead":"41e327dcb8ac3b7306a4bdf9e49005444b3083bf","scripts":{"dev":"tsup --watch","test":"vitest run","build":"tsup","clean":"rimraf dist","release":"../scripts/release-sdk.sh sdk sdk","typecheck":"tsc --noEmit","test:watch":"vitest","release:major":"../scripts/release-sdk.sh sdk sdk major","release:minor":"../scripts/release-sdk.sh sdk sdk minor","release:patch":"../scripts/release-sdk.sh sdk sdk patch","prepublishOnly":"npm run clean && npm run build"},"_npmUser":{"name":"3halves-labs","email":"info@3halves-labs.com"},"repository":{"url":"https://gitea.3halves-labs.com/3halveslabs/score-anything-loyalty.git","type":"git","directory":"sdk"},"_npmVersion":"10.7.0","description":"Lightweight React-friendly website tracking SDK for Score Anything Loyalty. Batching, session/UTM, SPA route, scroll, video, and custom events.","directories":{},"sideEffects":false,"_nodeVersion":"18.20.4","publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.1","jsdom":"^25.0.1","rimraf":"^6.0.1","vitest":"^2.1.9","typescript":"^5.5.4","@types/react":"^18.3.3","fake-indexeddb":"6.2.5"},"peerDependencies":{"react":">=17 || >=18"},"_npmOperationalInternal":{"tmp":"tmp/score-tracker_1.4.0_1779986713159_0.5105921954243646","host":"s3://npm-registry-packages-npm-production"}},"1.5.0":{"name":"@3halves-labs/score-tracker","version":"1.5.0","keywords":["tracking","analytics","loyalty","score","gamification","react"],"author":{"name":"3 Halves Labs"},"license":"MIT","_id":"@3halves-labs/score-tracker@1.5.0","maintainers":[{"name":"3halves-labs","email":"info@3halves-labs.com"}],"homepage":"https://github.com/3halveslabs/score-tracker#readme","dist":{"shasum":"c65870b79fd83e747efebcc24c31fc854ee37118","tarball":"https://registry.npmjs.org/@3halves-labs/score-tracker/-/score-tracker-1.5.0.tgz","fileCount":15,"integrity":"sha512-rVoKW+HMFt1C0zLHaI++ixFDGPC3Vy5BsXBk2+vrasvQ1Qj4rwWbZQed4bZpgH3FIuQipPY/CgMcNTUC61Grgw==","signatures":[{"sig":"MEQCIC6hyVg8rFrQiKIBUFiUjLE2kAocWXYy1c6pzsn8nnTBAiAciCoBjx6attNjjBxsvJnpO+4XOA23Oxt8qd2i9Cx69g==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":228728},"main":"dist/index.cjs.js","types":"dist/index.d.ts","module":"dist/index.esm.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.esm.js","require":"./dist/index.cjs.js"},"./react":{"types":"./dist/react/index.d.ts","import":"./dist/react/index.esm.js","require":"./dist/react/index.cjs.js"}},"gitHead":"23ce703d17bae749830c82530d5f35ffb0e6a413","scripts":{"dev":"tsup --watch","test":"vitest run","build":"tsup","clean":"rimraf dist","release":"../scripts/release-sdk.sh sdk sdk","typecheck":"tsc --noEmit","test:watch":"vitest","release:major":"../scripts/release-sdk.sh sdk sdk major","release:minor":"../scripts/release-sdk.sh sdk sdk minor","release:patch":"../scripts/release-sdk.sh sdk sdk patch","prepublishOnly":"npm run clean && npm run build"},"_npmUser":{"name":"3halves-labs","email":"info@3halves-labs.com"},"repository":{"url":"https://gitea.3halves-labs.com/3halveslabs/score-anything-loyalty.git","type":"git","directory":"sdk"},"_npmVersion":"10.7.0","description":"Lightweight React-friendly website tracking SDK for Score Anything Loyalty. Batching, session/UTM, SPA route, scroll, video, and custom events.","directories":{},"sideEffects":false,"_nodeVersion":"18.20.4","publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.1","jsdom":"^25.0.1","rimraf":"^6.0.1","vitest":"^2.1.9","typescript":"^5.5.4","@types/react":"^18.3.3","fake-indexeddb":"6.2.5"},"peerDependencies":{"react":">=17 || >=18"},"_npmOperationalInternal":{"tmp":"tmp/score-tracker_1.5.0_1779987478057_0.34781420384754735","host":"s3://npm-registry-packages-npm-production"}},"1.6.0":{"name":"@3halves-labs/score-tracker","version":"1.6.0","keywords":["tracking","analytics","loyalty","score","gamification","react"],"author":{"name":"3 Halves Labs"},"license":"MIT","_id":"@3halves-labs/score-tracker@1.6.0","maintainers":[{"name":"3halves-labs","email":"info@3halves-labs.com"}],"homepage":"https://github.com/3halveslabs/score-tracker#readme","dist":{"shasum":"bb2a9e6a215afc42a841e620aaede75d58eacf79","tarball":"https://registry.npmjs.org/@3halves-labs/score-tracker/-/score-tracker-1.6.0.tgz","fileCount":15,"integrity":"sha512-vcqWCKdCpm3JijwERHpgsSEfrG+ZImJCZGiE7zvlTv2MCu55Sg7ZP7cuWZ8/23xSaHcZC2lvXZ0hcllPPJ6Esg==","signatures":[{"sig":"MEQCIEZUKmrZ10qz9IPweEA80y2PBRH6nLKt/wDJW2wS8x/BAiATYtYPpSTcBjQmvOSqO6xoI+IxyYV7qf7x9fOHNvjFug==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":275604},"main":"dist/index.cjs.js","types":"dist/index.d.ts","module":"dist/index.esm.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.esm.js","require":"./dist/index.cjs.js"},"./react":{"types":"./dist/react/index.d.ts","import":"./dist/react/index.esm.js","require":"./dist/react/index.cjs.js"}},"gitHead":"44b0ab0661aa281d40e6a11cd9858857870e5faa","scripts":{"dev":"tsup --watch","test":"vitest run","build":"tsup","clean":"rimraf dist","release":"../scripts/release-sdk.sh sdk sdk","typecheck":"tsc --noEmit","test:watch":"vitest","release:major":"../scripts/release-sdk.sh sdk sdk major","release:minor":"../scripts/release-sdk.sh sdk sdk minor","release:patch":"../scripts/release-sdk.sh sdk sdk patch","prepublishOnly":"npm run clean && npm run build"},"_npmUser":{"name":"3halves-labs","email":"info@3halves-labs.com"},"repository":{"url":"https://gitea.3halves-labs.com/3halveslabs/score-anything-loyalty.git","type":"git","directory":"sdk"},"_npmVersion":"10.7.0","description":"Lightweight React-friendly website tracking SDK for Score Anything Loyalty. Batching, session/UTM, SPA route, scroll, video, and custom events.","directories":{},"sideEffects":false,"_nodeVersion":"18.20.4","publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.1","jsdom":"^25.0.1","rimraf":"^6.0.1","vitest":"^2.1.9","typescript":"^5.5.4","@types/react":"^18.3.3","fake-indexeddb":"6.2.5"},"peerDependencies":{"react":">=17 || >=18"},"_npmOperationalInternal":{"tmp":"tmp/score-tracker_1.6.0_1779988814345_0.16149453016961623","host":"s3://npm-registry-packages-npm-production"}},"1.7.0":{"name":"@3halves-labs/score-tracker","version":"1.7.0","keywords":["tracking","analytics","loyalty","score","gamification","react"],"author":{"name":"3 Halves Labs"},"license":"MIT","_id":"@3halves-labs/score-tracker@1.7.0","maintainers":[{"name":"3halves-labs","email":"info@3halves-labs.com"}],"homepage":"https://github.com/3halveslabs/score-tracker#readme","dist":{"shasum":"c775409b5959a6c129e2cac0dc77e4df561bf8ee","tarball":"https://registry.npmjs.org/@3halves-labs/score-tracker/-/score-tracker-1.7.0.tgz","fileCount":15,"integrity":"sha512-3S/VyAbEvWF4s7kiyFGhdh31eySp25D7oAtgeZkaKrsHDkECad7mMIHClAL8VT5k4ENfU+Tmhm6dNUlfSjvyjg==","signatures":[{"sig":"MEYCIQCmHq1jbIRkdaj7c52tJdCVdJDZcmzPs/EI7c/NF4CRCQIhAJkScAkt9LjIXqGfBjVigGlUTPQjPpC4tPKeI2kjmrio","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":295486},"main":"dist/index.cjs.js","types":"dist/index.d.ts","module":"dist/index.esm.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.esm.js","require":"./dist/index.cjs.js"},"./react":{"types":"./dist/react/index.d.ts","import":"./dist/react/index.esm.js","require":"./dist/react/index.cjs.js"}},"gitHead":"0ee17cc78958f0523ea20c91f6deccd1c188268e","scripts":{"dev":"tsup --watch","test":"vitest run","build":"tsup","clean":"rimraf dist","release":"../scripts/release-sdk.sh sdk sdk","typecheck":"tsc --noEmit","test:watch":"vitest","release:major":"../scripts/release-sdk.sh sdk sdk major","release:minor":"../scripts/release-sdk.sh sdk sdk minor","release:patch":"../scripts/release-sdk.sh sdk sdk patch","prepublishOnly":"npm run clean && npm run build"},"_npmUser":{"name":"3halves-labs","email":"info@3halves-labs.com"},"repository":{"url":"https://gitea.3halves-labs.com/3halveslabs/score-anything-loyalty.git","type":"git","directory":"sdk"},"_npmVersion":"10.7.0","description":"Lightweight React-friendly website tracking SDK for Score Anything Loyalty. Batching, session/UTM, SPA route, scroll, video, and custom events.","directories":{},"sideEffects":false,"_nodeVersion":"18.20.4","publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.1","jsdom":"^25.0.1","rimraf":"^6.0.1","vitest":"^2.1.9","typescript":"^5.5.4","@types/react":"^18.3.3","fake-indexeddb":"6.2.5"},"peerDependencies":{"react":">=17 || >=18"},"_npmOperationalInternal":{"tmp":"tmp/score-tracker_1.7.0_1779991666688_0.9855521450620688","host":"s3://npm-registry-packages-npm-production"}},"1.8.0":{"name":"@3halves-labs/score-tracker","version":"1.8.0","keywords":["tracking","analytics","loyalty","score","gamification","react"],"author":{"name":"3 Halves Labs"},"license":"MIT","_id":"@3halves-labs/score-tracker@1.8.0","maintainers":[{"name":"3halves-labs","email":"info@3halves-labs.com"}],"homepage":"https://github.com/3halveslabs/score-tracker#readme","dist":{"shasum":"58f520c7bbb30573e2d7ac62808674471f77437a","tarball":"https://registry.npmjs.org/@3halves-labs/score-tracker/-/score-tracker-1.8.0.tgz","fileCount":15,"integrity":"sha512-IEhgaVVD2hsSjrCRWubl6z8kPtjxa8u3NJAAqB/1zj3MbabWtgMB7n5pTWmBZXfETdInIuQLmnmsUBg4gsHbsw==","signatures":[{"sig":"MEYCIQDnkUyzBqnffCfLrFtryJDLzk99rTYz/PbOrnb/fO8yxwIhALv5m8K8klTegS4IjILA6PyECOOgUGA+ocIR0QHq8aaM","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":307235},"main":"dist/index.cjs.js","types":"dist/index.d.ts","module":"dist/index.esm.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.esm.js","require":"./dist/index.cjs.js"},"./react":{"types":"./dist/react/index.d.ts","import":"./dist/react/index.esm.js","require":"./dist/react/index.cjs.js"}},"gitHead":"dd4f71b1e1671b159822b66ea543e0a3d37eca82","scripts":{"dev":"tsup --watch","test":"vitest run","build":"tsup","clean":"rimraf dist","release":"../scripts/release-sdk.sh sdk sdk","typecheck":"tsc --noEmit","test:watch":"vitest","release:major":"../scripts/release-sdk.sh sdk sdk major","release:minor":"../scripts/release-sdk.sh sdk sdk minor","release:patch":"../scripts/release-sdk.sh sdk sdk patch","prepublishOnly":"npm run clean && npm run build"},"_npmUser":{"name":"3halves-labs","email":"info@3halves-labs.com"},"repository":{"url":"https://gitea.3halves-labs.com/3halveslabs/score-anything-loyalty.git","type":"git","directory":"sdk"},"_npmVersion":"10.7.0","description":"Lightweight React-friendly website tracking SDK for Score Anything Loyalty. Batching, session/UTM, SPA route, scroll, video, and custom events.","directories":{},"sideEffects":false,"_nodeVersion":"18.20.4","publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.1","jsdom":"^25.0.1","rimraf":"^6.0.1","vitest":"^2.1.9","typescript":"^5.5.4","@types/react":"^18.3.3","fake-indexeddb":"6.2.5"},"peerDependencies":{"react":">=17 || >=18"},"_npmOperationalInternal":{"tmp":"tmp/score-tracker_1.8.0_1779992255723_0.8142298679593838","host":"s3://npm-registry-packages-npm-production"}},"1.9.0":{"name":"@3halves-labs/score-tracker","version":"1.9.0","keywords":["tracking","analytics","loyalty","score","gamification","react"],"author":{"name":"3 Halves Labs"},"license":"MIT","_id":"@3halves-labs/score-tracker@1.9.0","maintainers":[{"name":"3halves-labs","email":"info@3halves-labs.com"}],"homepage":"https://github.com/3halveslabs/score-tracker#readme","dist":{"shasum":"e380fc76868811d742ede2a7d5c9156fadbe39ca","tarball":"https://registry.npmjs.org/@3halves-labs/score-tracker/-/score-tracker-1.9.0.tgz","fileCount":15,"integrity":"sha512-miYvs2Jp/5bd236jzjL7QR58xDsVtEz7jwEozYgrIXrthPUEmMy7xi6nl5OEe7qw1Ua9Xtmx9aAl/I14StVnhA==","signatures":[{"sig":"MEUCIGlz9ppbEQpLJoTLHarhG1XP5DaqvIQynr99XZlly9arAiEA7Wt2B2baLRC57dCS8vSnVmC9EMjZvnHxp+kiBzJ6moI=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":321420},"main":"dist/index.cjs.js","types":"dist/index.d.ts","module":"dist/index.esm.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.esm.js","require":"./dist/index.cjs.js"},"./react":{"types":"./dist/react/index.d.ts","import":"./dist/react/index.esm.js","require":"./dist/react/index.cjs.js"}},"gitHead":"26bb5e55d683302c25cc9909d0703d588658b425","scripts":{"dev":"tsup --watch","test":"vitest run","build":"tsup","clean":"rimraf dist","release":"../scripts/release-sdk.sh sdk sdk","typecheck":"tsc --noEmit","test:watch":"vitest","release:major":"../scripts/release-sdk.sh sdk sdk major","release:minor":"../scripts/release-sdk.sh sdk sdk minor","release:patch":"../scripts/release-sdk.sh sdk sdk patch","prepublishOnly":"npm run clean && npm run build"},"_npmUser":{"name":"3halves-labs","email":"info@3halves-labs.com"},"repository":{"url":"https://gitea.3halves-labs.com/3halveslabs/score-anything-loyalty.git","type":"git","directory":"sdk"},"_npmVersion":"10.7.0","description":"Lightweight React-friendly website tracking SDK for Score Anything Loyalty. Batching, session/UTM, SPA route, scroll, video, and custom events.","directories":{},"sideEffects":false,"_nodeVersion":"18.20.4","publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.1","jsdom":"^25.0.1","rimraf":"^6.0.1","vitest":"^2.1.9","typescript":"^5.5.4","web-vitals":"5.2.0","@types/react":"^18.3.3","fake-indexeddb":"6.2.5"},"peerDependencies":{"react":">=17 || >=18","web-vitals":">=4"},"peerDependenciesMeta":{"react":{"optional":true},"web-vitals":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/score-tracker_1.9.0_1779992955632_0.6201826304536056","host":"s3://npm-registry-packages-npm-production"}},"1.10.0":{"name":"@3halves-labs/score-tracker","version":"1.10.0","keywords":["tracking","analytics","loyalty","score","gamification","react"],"author":{"name":"3 Halves Labs"},"license":"MIT","_id":"@3halves-labs/score-tracker@1.10.0","maintainers":[{"name":"3halves-labs","email":"info@3halves-labs.com"}],"homepage":"https://github.com/3halveslabs/score-tracker#readme","dist":{"shasum":"e9aefd77e2298b4c121b06606e4d45a559a916e2","tarball":"https://registry.npmjs.org/@3halves-labs/score-tracker/-/score-tracker-1.10.0.tgz","fileCount":15,"integrity":"sha512-0rJ28nGNkBqr43/9asFCksvtDxfNqPOKXXUJ7jcqZMvpbq1YdZ2vdAX/5Fkg7ZKnI0DXB8Asuvip91fqVl9voQ==","signatures":[{"sig":"MEUCIQCzGXtOsJIwppiuYf/vow/SE99OvBdHuWry0JyAA4oubwIgdzEWOkWaufTfzBTQ/VpYeXLWwqNv5tXIGUZJKz9ognw=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":362931},"main":"dist/index.cjs.js","types":"dist/index.d.ts","module":"dist/index.esm.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.esm.js","require":"./dist/index.cjs.js"},"./react":{"types":"./dist/react/index.d.ts","import":"./dist/react/index.esm.js","require":"./dist/react/index.cjs.js"}},"gitHead":"68c501ecfd08e245308a6d0a3362fd266fa43f3b","scripts":{"dev":"tsup --watch","test":"vitest run","build":"tsup","clean":"rimraf dist","release":"../scripts/release-sdk.sh sdk sdk","typecheck":"tsc --noEmit","test:watch":"vitest","release:major":"../scripts/release-sdk.sh sdk sdk major","release:minor":"../scripts/release-sdk.sh sdk sdk minor","release:patch":"../scripts/release-sdk.sh sdk sdk patch","prepublishOnly":"npm run clean && npm run build"},"_npmUser":{"name":"3halves-labs","email":"info@3halves-labs.com"},"repository":{"url":"https://gitea.3halves-labs.com/3halveslabs/score-anything-loyalty.git","type":"git","directory":"sdk"},"_npmVersion":"10.7.0","description":"Lightweight React-friendly website tracking SDK for Score Anything Loyalty. Batching, session/UTM, SPA route, scroll, video, and custom events.","directories":{},"sideEffects":false,"_nodeVersion":"18.20.4","publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.1","jsdom":"^25.0.1","rimraf":"^6.0.1","vitest":"^2.1.9","typescript":"^5.5.4","web-vitals":"5.2.0","@types/react":"^18.3.3","fake-indexeddb":"6.2.5"},"peerDependencies":{"react":">=17 || >=18","web-vitals":">=4"},"peerDependenciesMeta":{"react":{"optional":true},"web-vitals":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/score-tracker_1.10.0_1779999410963_0.5894692230940564","host":"s3://npm-registry-packages-npm-production"}},"1.11.0":{"name":"@3halves-labs/score-tracker","version":"1.11.0","keywords":["tracking","analytics","loyalty","score","gamification","react"],"author":{"name":"3 Halves Labs"},"license":"MIT","_id":"@3halves-labs/score-tracker@1.11.0","maintainers":[{"name":"3halves-labs","email":"info@3halves-labs.com"}],"homepage":"https://github.com/3halveslabs/score-tracker#readme","dist":{"shasum":"c4999a9ac6db11a45a7ec9c6b95bcc09fc4b39da","tarball":"https://registry.npmjs.org/@3halves-labs/score-tracker/-/score-tracker-1.11.0.tgz","fileCount":15,"integrity":"sha512-tKUzt0cSTRPO09b1FSxN8nekrrPPXeTg8eY5TEXdzJl3qvj+puWoCU9Cofjh7wIBfdkLbCXYMu+KwT7JwpSg9A==","signatures":[{"sig":"MEUCIQCssvfBqTZiaeSV9Axy4u+qILXN5X8MIWyZOqL4dcaRigIgDpkYIio1u3sJTdzXoZyIPf+1vBvvVwwUU5pei/d6fTo=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":419069},"main":"dist/index.cjs.js","types":"dist/index.d.ts","module":"dist/index.esm.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.esm.js","require":"./dist/index.cjs.js"},"./react":{"types":"./dist/react/index.d.ts","import":"./dist/react/index.esm.js","require":"./dist/react/index.cjs.js"}},"gitHead":"9cf86fac1bad7636a462b61d00ce645a60907add","scripts":{"dev":"tsup --watch","test":"vitest run","build":"tsup","clean":"rimraf dist","release":"../scripts/release-sdk.sh sdk sdk","typecheck":"tsc --noEmit","test:watch":"vitest","release:major":"../scripts/release-sdk.sh sdk sdk major","release:minor":"../scripts/release-sdk.sh sdk sdk minor","release:patch":"../scripts/release-sdk.sh sdk sdk patch","prepublishOnly":"npm run clean && npm run build"},"_npmUser":{"name":"3halves-labs","email":"info@3halves-labs.com"},"repository":{"url":"https://gitea.3halves-labs.com/3halveslabs/score-anything-loyalty.git","type":"git","directory":"sdk"},"_npmVersion":"10.7.0","description":"Lightweight React-friendly website tracking SDK for Score Anything Loyalty. Batching, session/UTM, SPA route, scroll, video, and custom events.","directories":{},"sideEffects":false,"_nodeVersion":"18.20.4","publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.1","jsdom":"^25.0.1","rimraf":"^6.0.1","vitest":"^2.1.9","typescript":"^5.5.4","web-vitals":"5.2.0","@types/react":"^18.3.3","fake-indexeddb":"6.2.5"},"peerDependencies":{"react":">=17 || >=18","web-vitals":">=4"},"peerDependenciesMeta":{"react":{"optional":true},"web-vitals":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/score-tracker_1.11.0_1779999967560_0.34343003861118215","host":"s3://npm-registry-packages-npm-production"}},"1.12.0":{"name":"@3halves-labs/score-tracker","version":"1.12.0","keywords":["tracking","analytics","loyalty","score","gamification","react"],"author":{"name":"3 Halves Labs"},"license":"MIT","_id":"@3halves-labs/score-tracker@1.12.0","maintainers":[{"name":"3halves-labs","email":"info@3halves-labs.com"}],"homepage":"https://github.com/3halveslabs/score-tracker#readme","dist":{"shasum":"e2ca93f6a073a626c7278b21a4b6a498d6cddb92","tarball":"https://registry.npmjs.org/@3halves-labs/score-tracker/-/score-tracker-1.12.0.tgz","fileCount":15,"integrity":"sha512-3Ezcfoyi4LbZG/hfluPWhlU9wcWFGtWd4TOqRT2MaEXLA/sdEbhpp5NCcxy8Ada7bkQatZo+RCMtY/ULT6DMKQ==","signatures":[{"sig":"MEUCIQCVF2o1cynd6isM3kcnAZkBXldGKa+akLjuO+MfEOHDPgIgXfRrpSJ3OqlQ4DLWfoU5zrL+YGQUI+48sItV9kPTfJM=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":448060},"main":"dist/index.cjs.js","types":"dist/index.d.ts","module":"dist/index.esm.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.esm.js","require":"./dist/index.cjs.js"},"./react":{"types":"./dist/react/index.d.ts","import":"./dist/react/index.esm.js","require":"./dist/react/index.cjs.js"}},"gitHead":"fbe62d9ac3c4027bb9fbd00a24a539b8094c921f","scripts":{"dev":"tsup --watch","test":"vitest run","build":"tsup","clean":"rimraf dist","release":"../scripts/release-sdk.sh sdk sdk","typecheck":"tsc --noEmit","test:watch":"vitest","release:major":"../scripts/release-sdk.sh sdk sdk major","release:minor":"../scripts/release-sdk.sh sdk sdk minor","release:patch":"../scripts/release-sdk.sh sdk sdk patch","prepublishOnly":"npm run clean && npm run build"},"_npmUser":{"name":"3halves-labs","email":"info@3halves-labs.com"},"repository":{"url":"https://gitea.3halves-labs.com/3halveslabs/score-anything-loyalty.git","type":"git","directory":"sdk"},"_npmVersion":"10.7.0","description":"Lightweight React-friendly website tracking SDK for Score Anything Loyalty. Batching, session/UTM, SPA route, scroll, video, and custom events.","directories":{},"sideEffects":false,"_nodeVersion":"18.20.4","publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.1","jsdom":"^25.0.1","rimraf":"^6.0.1","vitest":"^2.1.9","typescript":"^5.5.4","web-vitals":"5.2.0","@types/react":"^18.3.3","fake-indexeddb":"6.2.5"},"peerDependencies":{"react":">=17 || >=18","web-vitals":">=4"},"peerDependenciesMeta":{"react":{"optional":true},"web-vitals":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/score-tracker_1.12.0_1780000829911_0.889325979506802","host":"s3://npm-registry-packages-npm-production"}},"1.13.0":{"name":"@3halves-labs/score-tracker","version":"1.13.0","keywords":["tracking","analytics","loyalty","score","gamification","react"],"author":{"name":"3 Halves Labs"},"license":"MIT","_id":"@3halves-labs/score-tracker@1.13.0","maintainers":[{"name":"3halves-labs","email":"info@3halves-labs.com"}],"homepage":"https://github.com/3halveslabs/score-tracker#readme","dist":{"shasum":"1d609ec72c561aa518d41d1dc84bad2823d54ae9","tarball":"https://registry.npmjs.org/@3halves-labs/score-tracker/-/score-tracker-1.13.0.tgz","fileCount":15,"integrity":"sha512-sFYjjeRaw8OtRvceNv+66Lpu6o54jQLToxD9Cz0PWajTAY43CJU7VYrh2hNgzhPNHjJDLtO4xTfMq+p0M0a7qA==","signatures":[{"sig":"MEUCIQCmaFBwhyEEsByp/K6mR8TcfvknMLWpZJSjgseknTh78wIgVSSuQmTqoVFrk5c8ZSZgPzTP+y/IORr3nhvdx0TkXLY=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":471029},"main":"dist/index.cjs.js","types":"dist/index.d.ts","module":"dist/index.esm.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.esm.js","require":"./dist/index.cjs.js"},"./react":{"types":"./dist/react/index.d.ts","import":"./dist/react/index.esm.js","require":"./dist/react/index.cjs.js"}},"gitHead":"617a6c3858e848f6303ad0ce3e4545fe3421b92e","scripts":{"dev":"tsup --watch","test":"vitest run","build":"tsup","clean":"rimraf dist","release":"../scripts/release-sdk.sh sdk sdk","typecheck":"tsc --noEmit","test:watch":"vitest","release:major":"../scripts/release-sdk.sh sdk sdk major","release:minor":"../scripts/release-sdk.sh sdk sdk minor","release:patch":"../scripts/release-sdk.sh sdk sdk patch","prepublishOnly":"npm run clean && npm run build"},"_npmUser":{"name":"3halves-labs","email":"info@3halves-labs.com"},"repository":{"url":"https://gitea.3halves-labs.com/3halveslabs/score-anything-loyalty.git","type":"git","directory":"sdk"},"_npmVersion":"10.7.0","description":"Lightweight React-friendly website tracking SDK for Score Anything Loyalty. Batching, session/UTM, SPA route, scroll, video, and custom events.","directories":{},"sideEffects":false,"_nodeVersion":"18.20.4","publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.1","jsdom":"^25.0.1","rimraf":"^6.0.1","vitest":"^2.1.9","typescript":"^5.5.4","web-vitals":"5.2.0","@types/react":"^18.3.3","fake-indexeddb":"6.2.5"},"peerDependencies":{"react":">=17 || >=18","web-vitals":">=4"},"peerDependenciesMeta":{"react":{"optional":true},"web-vitals":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/score-tracker_1.13.0_1780001457236_0.4077137969418714","host":"s3://npm-registry-packages-npm-production"}},"1.14.0":{"name":"@3halves-labs/score-tracker","version":"1.14.0","keywords":["tracking","analytics","loyalty","score","gamification","react"],"author":{"name":"3 Halves Labs"},"license":"MIT","_id":"@3halves-labs/score-tracker@1.14.0","maintainers":[{"name":"3halves-labs","email":"info@3halves-labs.com"}],"homepage":"https://github.com/3halveslabs/score-tracker#readme","dist":{"shasum":"3999a0b6f02231011373c2224d0ebf58df1cc240","tarball":"https://registry.npmjs.org/@3halves-labs/score-tracker/-/score-tracker-1.14.0.tgz","fileCount":15,"integrity":"sha512-bJtsvLBmojjGOIygcKEIo4AvO9e4UsZUTT1cT2/x44oF/QwmF50EBlYX3q8lII8UZR9uXRYwCtPvPzT65xnOpA==","signatures":[{"sig":"MEYCIQCaLZNLW/cIhcMgS90tfpcRPJHz1G0aJnt/ec3aS9yM+AIhAL04P+AZKVM8Mhn6N+O/38lNs6Xsq+iUVWr5oSN4ByWW","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":491323},"main":"dist/index.cjs.js","types":"dist/index.d.ts","module":"dist/index.esm.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.esm.js","require":"./dist/index.cjs.js"},"./react":{"types":"./dist/react/index.d.ts","import":"./dist/react/index.esm.js","require":"./dist/react/index.cjs.js"}},"gitHead":"3c5b7eb83521228fc454224c881ae310de6863a0","scripts":{"dev":"tsup --watch","test":"vitest run","build":"tsup","clean":"rimraf dist","release":"../scripts/release-sdk.sh sdk sdk","typecheck":"tsc --noEmit","test:watch":"vitest","release:major":"../scripts/release-sdk.sh sdk sdk major","release:minor":"../scripts/release-sdk.sh sdk sdk minor","release:patch":"../scripts/release-sdk.sh sdk sdk patch","prepublishOnly":"npm run clean && npm run build"},"_npmUser":{"name":"3halves-labs","email":"info@3halves-labs.com"},"repository":{"url":"https://gitea.3halves-labs.com/3halveslabs/score-anything-loyalty.git","type":"git","directory":"sdk"},"_npmVersion":"10.7.0","description":"Lightweight React-friendly website tracking SDK for Score Anything Loyalty. Batching, session/UTM, SPA route, scroll, video, and custom events.","directories":{},"sideEffects":false,"_nodeVersion":"18.20.4","publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.1","jsdom":"^25.0.1","rimraf":"^6.0.1","vitest":"^2.1.9","typescript":"^5.5.4","web-vitals":"5.2.0","@types/react":"^18.3.3","fake-indexeddb":"6.2.5"},"peerDependencies":{"react":">=17 || >=18","web-vitals":">=4"},"peerDependenciesMeta":{"react":{"optional":true},"web-vitals":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/score-tracker_1.14.0_1780022900185_0.14763189494911733","host":"s3://npm-registry-packages-npm-production"}},"1.15.0":{"name":"@3halves-labs/score-tracker","version":"1.15.0","keywords":["tracking","analytics","loyalty","score","gamification","react"],"author":{"name":"3 Halves Labs"},"license":"MIT","_id":"@3halves-labs/score-tracker@1.15.0","maintainers":[{"name":"3halves-labs","email":"info@3halves-labs.com"}],"homepage":"https://github.com/3halveslabs/score-tracker#readme","dist":{"shasum":"fcce226b8536bf59a8bd717f905dce9dd14baf49","tarball":"https://registry.npmjs.org/@3halves-labs/score-tracker/-/score-tracker-1.15.0.tgz","fileCount":15,"integrity":"sha512-qcJVEvd3dViLCo4rirAcD0Vye3/TZxw+r+6AT06HUwdTbxNioj56rBbaCmO9Uf6phl9XCU++FUL5mB1pSK2Ytg==","signatures":[{"sig":"MEQCIHk3RdfQaf0n9kzqIatnL9TK0rdP429HFNfdydbn7hzmAiAYEeo1xNdEN+UhTR1KcsvsHmbiODq3JFH82GItwk7dXA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":514337},"main":"dist/index.cjs.js","types":"dist/index.d.ts","module":"dist/index.esm.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.esm.js","require":"./dist/index.cjs.js"},"./react":{"types":"./dist/react/index.d.ts","import":"./dist/react/index.esm.js","require":"./dist/react/index.cjs.js"}},"gitHead":"bc0bfa0ac8b0a5d927ea23e90f1f700538b29086","scripts":{"dev":"tsup --watch","test":"vitest run","build":"tsup","clean":"rimraf dist","release":"../scripts/release-sdk.sh sdk sdk","typecheck":"tsc --noEmit","test:watch":"vitest","release:major":"../scripts/release-sdk.sh sdk sdk major","release:minor":"../scripts/release-sdk.sh sdk sdk minor","release:patch":"../scripts/release-sdk.sh sdk sdk patch","prepublishOnly":"npm run clean && npm run build"},"_npmUser":{"name":"3halves-labs","email":"info@3halves-labs.com"},"repository":{"url":"https://gitea.3halves-labs.com/3halveslabs/score-anything-loyalty.git","type":"git","directory":"sdk"},"_npmVersion":"10.7.0","description":"Lightweight React-friendly website tracking SDK for Score Anything Loyalty. Batching, session/UTM, SPA route, scroll, video, and custom events.","directories":{},"sideEffects":false,"_nodeVersion":"18.20.4","publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.1","jsdom":"^25.0.1","rimraf":"^6.0.1","vitest":"^2.1.9","typescript":"^5.5.4","web-vitals":"5.2.0","@types/react":"^18.3.3","fake-indexeddb":"6.2.5"},"peerDependencies":{"react":">=17 || >=18","web-vitals":">=4"},"peerDependenciesMeta":{"react":{"optional":true},"web-vitals":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/score-tracker_1.15.0_1780023771024_0.39188192546510536","host":"s3://npm-registry-packages-npm-production"}},"1.17.0":{"name":"@3halves-labs/score-tracker","version":"1.17.0","keywords":["tracking","analytics","loyalty","score","gamification","react"],"author":{"name":"3 Halves Labs"},"license":"MIT","_id":"@3halves-labs/score-tracker@1.17.0","maintainers":[{"name":"3halves-labs","email":"info@3halves-labs.com"}],"homepage":"https://github.com/3halveslabs/score-tracker#readme","dist":{"shasum":"7622d4e89ce0da48dfe41f393d4a5c16d57c9ad8","tarball":"https://registry.npmjs.org/@3halves-labs/score-tracker/-/score-tracker-1.17.0.tgz","fileCount":15,"integrity":"sha512-mOOWxzdFQVWcE4E7iiZgOqj4bp46XU0Vpm+7KS0+ctS8FM9hjkPlCRPioHF1xnmHsWiNW0czIPwvcjU9syw6CA==","signatures":[{"sig":"MEQCIFKFARzpOEcsiwbEvRCUexSatciY9UNwAoSqzodPXhttAiBRXqq5Pw9q3+Psu/3oL5fkd58bNCbPFeHnBwdBm1cqUg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":573634},"main":"dist/index.cjs.js","types":"dist/index.d.ts","module":"dist/index.esm.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.esm.js","require":"./dist/index.cjs.js"},"./react":{"types":"./dist/react/index.d.ts","import":"./dist/react/index.esm.js","require":"./dist/react/index.cjs.js"}},"gitHead":"3a9c3f240148d7cf1d00f7fb885b2d8c71eb5323","scripts":{"dev":"tsup --watch","test":"vitest run","build":"tsup","clean":"rimraf dist","release":"../scripts/release-sdk.sh sdk sdk","typecheck":"tsc --noEmit","test:watch":"vitest","release:major":"../scripts/release-sdk.sh sdk sdk major","release:minor":"../scripts/release-sdk.sh sdk sdk minor","release:patch":"../scripts/release-sdk.sh sdk sdk patch","prepublishOnly":"npm run clean && npm run build"},"_npmUser":{"name":"3halves-labs","email":"info@3halves-labs.com"},"repository":{"url":"https://gitea.3halves-labs.com/3halveslabs/score-anything-loyalty.git","type":"git","directory":"sdk"},"_npmVersion":"10.7.0","description":"Lightweight React-friendly website tracking SDK for Score Anything Loyalty. Batching, session/UTM, SPA route, scroll, video, and custom events.","directories":{},"sideEffects":false,"_nodeVersion":"18.20.4","publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.1","jsdom":"^25.0.1","rimraf":"^6.0.1","vitest":"^2.1.9","typescript":"^5.5.4","web-vitals":"5.2.0","@types/react":"^18.3.3","fake-indexeddb":"6.2.5"},"peerDependencies":{"react":">=17 || >=18","web-vitals":">=4"},"peerDependenciesMeta":{"react":{"optional":true},"web-vitals":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/score-tracker_1.17.0_1780029800306_0.5740874520612056","host":"s3://npm-registry-packages-npm-production"}},"1.19.1":{"name":"@3halves-labs/score-tracker","version":"1.19.1","keywords":["tracking","analytics","loyalty","score","gamification","react"],"author":{"name":"3 Halves Labs"},"license":"MIT","_id":"@3halves-labs/score-tracker@1.19.1","maintainers":[{"name":"3halves-labs","email":"info@3halves-labs.com"}],"homepage":"https://github.com/3halveslabs/score-tracker#readme","dist":{"shasum":"d450b534a4573fa95fb8b76926a3876ad3f568cb","tarball":"https://registry.npmjs.org/@3halves-labs/score-tracker/-/score-tracker-1.19.1.tgz","fileCount":15,"integrity":"sha512-zie/YMut8vk8eREp5glpVxGeUpqcGn25/SaAC9uahiVB2rxDXFNQ3VYX46zsFwfIV1mlWIUHFYpIAzzajhzQmg==","signatures":[{"sig":"MEYCIQCHtZhYbOCPv2xFOzxg4BY1ltMTKKdh92H6NrIwWg49LQIhAK7+7WvAPJ+mfvjZBvF1efxpdy9Ov/x6/G6ND7XNVyDp","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":654124},"main":"dist/index.cjs.js","types":"dist/index.d.ts","module":"dist/index.esm.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.esm.js","require":"./dist/index.cjs.js"},"./react":{"types":"./dist/react/index.d.ts","import":"./dist/react/index.esm.js","require":"./dist/react/index.cjs.js"}},"gitHead":"25598c8c2b2ff1a8fc556ad45364ddda3c309d42","scripts":{"dev":"tsup --watch","test":"vitest run","build":"tsup","clean":"rimraf dist","release":"../scripts/release-sdk.sh sdk sdk","typecheck":"tsc --noEmit","test:watch":"vitest","release:major":"../scripts/release-sdk.sh sdk sdk major","release:minor":"../scripts/release-sdk.sh sdk sdk minor","release:patch":"../scripts/release-sdk.sh sdk sdk patch","prepublishOnly":"npm run clean && npm run build"},"_npmUser":{"name":"3halves-labs","email":"info@3halves-labs.com"},"repository":{"url":"https://gitea.3halves-labs.com/3halveslabs/score-anything-loyalty.git","type":"git","directory":"sdk"},"_npmVersion":"10.7.0","description":"Lightweight React-friendly website tracking SDK for Score Anything Loyalty. Batching, session/UTM, SPA route, scroll, video, and custom events.","directories":{},"sideEffects":false,"_nodeVersion":"18.20.4","publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.1","jsdom":"^25.0.1","react":"19.2.6","rimraf":"^6.0.1","vitest":"^2.1.9","react-dom":"19.2.6","typescript":"^5.5.4","web-vitals":"5.2.0","@types/react":"^18.3.3","fake-indexeddb":"6.2.5","@testing-library/react":"16.3.2"},"peerDependencies":{"react":">=17 || >=18","web-vitals":">=4"},"peerDependenciesMeta":{"react":{"optional":true},"web-vitals":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/score-tracker_1.19.1_1780414957495_0.9802525410386158","host":"s3://npm-registry-packages-npm-production"}},"1.19.2":{"name":"@3halves-labs/score-tracker","version":"1.19.2","keywords":["tracking","analytics","loyalty","score","gamification","react"],"author":{"name":"3 Halves Labs"},"license":"MIT","_id":"@3halves-labs/score-tracker@1.19.2","maintainers":[{"name":"3halves-labs","email":"info@3halves-labs.com"}],"homepage":"https://github.com/3halveslabs/score-tracker#readme","dist":{"shasum":"b4d8245bf166b8039f3ef5393cb96a4f12154277","tarball":"https://registry.npmjs.org/@3halves-labs/score-tracker/-/score-tracker-1.19.2.tgz","fileCount":16,"integrity":"sha512-/ccPQUU4Uav3X2fgay7B6M5oPYtJjKXt3oiEu7L4q5bJEVHQA2SVWQ+taG7vlv7UNSQBBaQGn456mKJoweI9iw==","signatures":[{"sig":"MEQCIA+p2HUsJ5YiiDtBcZOwp4tlTLScTYL/ATiqiLpGLDNVAiBD0wtREqfObbOmYGHQ7/re3OFSurWUqh6uFlLLWdJ2qA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":709907},"main":"dist/index.cjs.js","types":"dist/index.d.ts","module":"dist/index.esm.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.esm.js","require":"./dist/index.cjs.js"},"./react":{"types":"./dist/react/index.d.ts","import":"./dist/react/index.esm.js","require":"./dist/react/index.cjs.js"}},"gitHead":"ed2138d5fbce9585262cd8e0d34fe7ecb2a90cba","scripts":{"dev":"tsup --watch","test":"vitest run","build":"tsup","clean":"rimraf dist","release":"../scripts/release-sdk.sh sdk sdk","typecheck":"tsc --noEmit","test:watch":"vitest","release:major":"../scripts/release-sdk.sh sdk sdk major","release:minor":"../scripts/release-sdk.sh sdk sdk minor","release:patch":"../scripts/release-sdk.sh sdk sdk patch","prepublishOnly":"npm run clean && npm run build"},"_npmUser":{"name":"3halves-labs","email":"info@3halves-labs.com"},"repository":{"url":"https://gitea.3halves-labs.com/3halveslabs/score-anything-loyalty.git","type":"git","directory":"sdk"},"_npmVersion":"10.7.0","description":"Lightweight React-friendly website tracking SDK for Score Anything Loyalty. Batching, session/UTM, SPA route, scroll, video, and custom events.","directories":{},"sideEffects":false,"_nodeVersion":"18.20.4","publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.1","jsdom":"^25.0.1","react":"19.2.6","rimraf":"^6.0.1","vitest":"^2.1.9","react-dom":"19.2.6","typescript":"^5.5.4","web-vitals":"5.2.0","@types/react":"^18.3.3","fake-indexeddb":"6.2.5","@testing-library/react":"16.3.2"},"peerDependencies":{"react":">=17 || >=18","web-vitals":">=4"},"peerDependenciesMeta":{"react":{"optional":true},"web-vitals":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/score-tracker_1.19.2_1780415252947_0.522389380598439","host":"s3://npm-registry-packages-npm-production"}},"1.21.0":{"name":"@3halves-labs/score-tracker","version":"1.21.0","keywords":["tracking","analytics","loyalty","score","gamification","react"],"author":{"name":"3 Halves Labs"},"license":"MIT","_id":"@3halves-labs/score-tracker@1.21.0","maintainers":[{"name":"3halves-labs","email":"info@3halves-labs.com"}],"homepage":"https://github.com/3halveslabs/score-tracker#readme","dist":{"shasum":"fc582474691b81afedfc639772e7d84b54e0ecaa","tarball":"https://registry.npmjs.org/@3halves-labs/score-tracker/-/score-tracker-1.21.0.tgz","fileCount":31,"integrity":"sha512-KUf6lhjtENNsOeSXv+izBz3+E7sVkVnJW2lsKFQO+zCO51jO4TMHSCtuodJhOCM21EYPGA+QLICOcUwx/Z2glA==","signatures":[{"sig":"MEUCIDubCFfpZdTKrASjtkO9DYKX4NDrW/sDYtLRqsnwga/gAiEAhefR3trzSuQP9uKpS26DEcuzmZqimuQT6FAzBNddDYk=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":808539},"main":"dist/index.cjs.js","types":"dist/index.d.ts","module":"dist/index.esm.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.esm.js","require":"./dist/index.cjs.js"},"./react":{"types":"./dist/react/index.d.ts","import":"./dist/react/index.esm.js","require":"./dist/react/index.cjs.js"},"./contract/envelope.schema.json":"./contract/envelope.schema.json"},"gitHead":"81acbab09ecf9b83573cb729fc99ac31f777131c","scripts":{"dev":"tsup --watch","test":"vitest run","build":"tsup","clean":"rimraf dist","release":"../scripts/release-sdk.sh sdk sdk","typecheck":"tsc --noEmit","gen:events":"node scripts/gen-events.mjs","test:watch":"vitest","release:major":"../scripts/release-sdk.sh sdk sdk major","release:minor":"../scripts/release-sdk.sh sdk sdk minor","release:patch":"../scripts/release-sdk.sh sdk sdk patch","prepublishOnly":"npm run clean && npm run build"},"_npmUser":{"name":"3halves-labs","email":"info@3halves-labs.com"},"repository":{"url":"https://gitea.3halves-labs.com/3halveslabs/score-anything-loyalty.git","type":"git","directory":"sdk"},"_npmVersion":"10.7.0","description":"Lightweight React-friendly website tracking SDK for Score Anything Loyalty. Batching, session/UTM, SPA route, scroll, video, and custom events.","directories":{},"sideEffects":false,"_nodeVersion":"18.20.4","publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.1","jsdom":"^25.0.1","react":"19.2.6","rimraf":"^6.0.1","vitest":"^2.1.9","react-dom":"19.2.6","typescript":"^5.5.4","web-vitals":"5.2.0","@types/node":"20.19.41","@types/react":"^18.3.3","fake-indexeddb":"6.2.5","@testing-library/react":"16.3.2"},"peerDependencies":{"react":">=17 || >=18","web-vitals":">=4"},"peerDependenciesMeta":{"react":{"optional":true},"web-vitals":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/score-tracker_1.21.0_1784818651501_0.36187467978863563","host":"s3://npm-registry-packages-npm-production"}},"1.21.1":{"name":"@3halves-labs/score-tracker","version":"1.21.1","keywords":["tracking","analytics","loyalty","score","gamification","react"],"author":{"name":"3 Halves Labs"},"license":"MIT","_id":"@3halves-labs/score-tracker@1.21.1","maintainers":[{"name":"3halves-labs","email":"info@3halves-labs.com"}],"homepage":"https://github.com/3halveslabs/score-tracker#readme","dist":{"shasum":"41fd04a22c35434731ec8286c5a984bad565ab19","tarball":"https://registry.npmjs.org/@3halves-labs/score-tracker/-/score-tracker-1.21.1.tgz","fileCount":31,"integrity":"sha512-TfImPUawtPlK65qL9gzcG4szLa/kkhyUuMAerjfmibr67PMVKdC0WgU1MAeBAr7KEsr6Ng/jUGU0sI2gk665YQ==","signatures":[{"sig":"MEYCIQC+m3ylxXlHPqzRs19Nux5wKCtk8R7QBYW190OEEIiuLgIhAP94tXy/NqaG5mtsSTg+nO4i/Ze6KlP2c8LF1BOE44Eu","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEUCIQDgWUVIcLE+sByoz9VCjHGoMuZZ3l3CedFr/6sqXxyhZgIgWBGONMmCP3rHReLDUluRq/Ol4L1rUV+RnYDNblCetz8=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":837048},"main":"dist/index.cjs.js","types":"dist/index.d.ts","module":"dist/index.esm.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.esm.js","require":"./dist/index.cjs.js"},"./react":{"types":"./dist/react/index.d.ts","import":"./dist/react/index.esm.js","require":"./dist/react/index.cjs.js"},"./contract/envelope.schema.json":"./contract/envelope.schema.json"},"gitHead":"e885de475f29f87e09b813f8e15ea7f681e54137","scripts":{"dev":"tsup --watch","test":"vitest run","build":"tsup","clean":"rimraf dist","release":"../scripts/release-sdk.sh sdk sdk","typecheck":"tsc --noEmit","gen:events":"node scripts/gen-events.mjs","test:watch":"vitest","release:major":"../scripts/release-sdk.sh sdk sdk major","release:minor":"../scripts/release-sdk.sh sdk sdk minor","release:patch":"../scripts/release-sdk.sh sdk sdk patch","prepublishOnly":"npm run clean && npm run build"},"_npmUser":{"name":"3halves-labs","email":"info@3halves-labs.com"},"repository":{"url":"https://gitea.3halves-labs.com/3halveslabs/score-anything-loyalty.git","type":"git","directory":"sdk"},"_npmVersion":"10.9.8","description":"Lightweight React-friendly website tracking SDK for Score Anything Loyalty. Batching, session/UTM, SPA route, scroll, video, and custom events.","directories":{},"sideEffects":false,"_nodeVersion":"22.23.1","publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.1","jsdom":"^25.0.1","react":"19.2.6","rimraf":"^6.0.1","vitest":"^2.1.9","react-dom":"19.2.6","typescript":"^5.5.4","web-vitals":"5.2.0","@types/node":"20.19.41","@types/react":"^18.3.3","fake-indexeddb":"6.2.5","@testing-library/react":"16.3.2"},"peerDependencies":{"react":">=17 || >=18","web-vitals":">=4"},"peerDependenciesMeta":{"react":{"optional":true},"web-vitals":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/score-tracker_1.21.1_1789146800598_0.7998961639769158","host":"s3://npm-registry-packages-npm-production"}},"1.21.2":{"_id":"@3halves-labs/score-tracker@1.21.2","dist":{"shasum":"9e71caca574cfee645b91343560e01cad05a8207","tarball":"https://registry.npmjs.org/@3halves-labs/score-tracker/-/score-tracker-1.21.2.tgz","fileCount":32,"integrity":"sha512-0/HOlU5SWjk1DcIrShk6B+KWDezG7bTYPce16GtGbbriLBIAGeOb9kR2iocq5gjsMwzmr73xiiD4XTD2bqdK3A==","signatures":[{"sig":"MEQCICPjfEKm0OeLzSM9bGqaDLSxiIadlbGBjg4IDjbbjBGfAiAydgs3cJ1O9ZDFGqdJO04/uR7S+nwR5XzOxlZxl+aang==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQDyC4Qa8PSkhkMnONOkGZb0NU5szFBZx4JTijwDR8FoggIhAN6zuCYXj/CwBLHJWoBDW0GRRpXXFn2pxtuMLaI4VHpr"}],"unpackedSize":842233},"main":"dist/index.cjs.js","name":"@3halves-labs/score-tracker","types":"dist/index.d.ts","author":{"name":"3 Halves Labs"},"module":"dist/index.esm.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.esm.js","require":"./dist/index.cjs.js"},"./react":{"types":"./dist/react/index.d.ts","import":"./dist/react/index.esm.js","require":"./dist/react/index.cjs.js"},"./contract/envelope.schema.json":"./contract/envelope.schema.json"},"gitHead":"196ac641deba785d3d16a6e23cf21ad346370065","license":"MIT","scripts":{"dev":"tsup --watch","test":"vitest run","build":"tsup","clean":"rimraf dist","release":"../scripts/release-sdk.sh sdk sdk","typecheck":"tsc --noEmit","gen:events":"node scripts/gen-events.mjs","test:watch":"vitest","release:major":"../scripts/release-sdk.sh sdk sdk major","release:minor":"../scripts/release-sdk.sh sdk sdk minor","release:patch":"../scripts/release-sdk.sh sdk sdk patch","prepublishOnly":"npm run clean && npm run build"},"version":"1.21.2","_npmUser":{"name":"3halves-labs","email":"info@3halves-labs.com"},"homepage":"https://github.com/3halveslabs/score-tracker#readme","keywords":["tracking","analytics","loyalty","score","gamification","react"],"repository":{"url":"https://gitea.3halves-labs.com/3halveslabs/score-anything-loyalty.git","type":"git","directory":"sdk"},"_npmVersion":"10.9.8","description":"Lightweight React-friendly website tracking SDK for Score Anything Loyalty. Batching, session/UTM, SPA route, scroll, video, and custom events.","directories":{},"maintainers":[{"name":"3halves-labs","email":"info@3halves-labs.com"}],"sideEffects":false,"_nodeVersion":"22.23.1","publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.1","jsdom":"^25.0.1","react":"19.2.6","rimraf":"^6.0.1","vitest":"^2.1.9","react-dom":"19.2.6","typescript":"^5.5.4","web-vitals":"5.2.0","@types/node":"20.19.41","@types/react":"^18.3.3","fake-indexeddb":"6.2.5","@testing-library/react":"16.3.2"},"peerDependencies":{"react":">=17 || >=18","web-vitals":">=4"},"peerDependenciesMeta":{"react":{"optional":true},"web-vitals":{"optional":true}},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/score-tracker_1.21.2_1789647789891_0.08073365703983137"}}},"time":{"created":"2025-08-12T14:21:33.486Z","modified":"2026-09-17T12:23:10.155Z","1.0.13":"2025-08-12T14:21:33.823Z","1.2.2":"2026-05-28T15:30:22.756Z","1.3.0":"2026-05-28T16:08:25.715Z","1.4.0":"2026-05-28T16:45:13.309Z","1.5.0":"2026-05-28T16:57:58.212Z","1.6.0":"2026-05-28T17:20:14.500Z","1.7.0":"2026-05-28T18:07:46.830Z","1.8.0":"2026-05-28T18:17:35.972Z","1.9.0":"2026-05-28T18:29:15.772Z","1.10.0":"2026-05-28T20:16:51.156Z","1.11.0":"2026-05-28T20:26:07.778Z","1.12.0":"2026-05-28T20:40:30.064Z","1.13.0":"2026-05-28T20:50:57.384Z","1.14.0":"2026-05-29T02:48:20.384Z","1.15.0":"2026-05-29T03:02:51.264Z","1.17.0":"2026-05-29T04:43:20.505Z","1.19.1":"2026-06-02T15:42:37.726Z","1.19.2":"2026-06-02T15:47:33.128Z","1.21.0":"2026-07-23T14:57:31.660Z","1.21.1":"2026-09-11T17:13:20.718Z","1.21.2":"2026-09-17T12:23:10.013Z"},"author":{"name":"3 Halves Labs"},"license":"MIT","homepage":"https://github.com/3halveslabs/score-tracker#readme","keywords":["tracking","analytics","loyalty","score","gamification","react"],"repository":{"url":"https://gitea.3halves-labs.com/3halveslabs/score-anything-loyalty.git","type":"git","directory":"sdk"},"description":"Lightweight React-friendly website tracking SDK for Score Anything Loyalty. Batching, session/UTM, SPA route, scroll, video, and custom events.","maintainers":[{"name":"3halves-labs","email":"info@3halves-labs.com"}],"readme":"# @3halves-labs/score-tracker\n\nReact-friendly tracking SDK for the Score platform.\n\n- ✅ Batching & retry\n- ✅ Session/anonymous IDs\n- ✅ Identity graph — `alias` / `group` / `ungroup`; group memberships persist + stamp every event with at-event-time context\n- ✅ Deferred identify replay — opt-in re-emit of pre-identify events with `_backfill` + `_originalEventId` for warehouse joining\n- ✅ Delivery guarantees — `critical` events get flush priority, bypass visibility-pause, and emit `delivery_failed` on age-eviction\n- ✅ Cross-domain identity stitching — opt-in linker param carries `anonymousId` / `sessionId` between cooperating domains\n- ✅ Middleware pipeline — `tracker.use((evt, next) => …)` for enrichment / transformation / custom drops\n- ✅ Declarative React components — `<Track>`, `<TrackVisibility>`, `<Sponsor>` for CMS / marketing-friendly wiring\n- ✅ UTM & referrer capture\n- ✅ SPA route + pageviews\n- ✅ Scroll thresholds\n- ✅ Video events (play/pause/end)\n- ✅ Field-level PII handling — denylist or per-field actions (drop / redact / tokenize / hash)\n- ✅ React provider + hook\n- ✅ Consent gating (`init({ getConsent })` + `shutdown()`) — boolean or category-aware (`{ analytics, personalization, advertising }`)\n- ✅ Regional privacy policy engine — `privacy: 'auto' | 'gdpr' | 'cpra' | 'lgpd' | 'quebec' | 'off'`; auto-adjusts consent defaults, retention, and fingerprint signals per jurisdiction\n- ✅ Session-replay correlation — opt-in `replayId` stamped on every event to join analytics to an external recording (rrweb/FullStory); respects the privacy policy\n- ✅ Multi-destination fan-out — mirror events to extra sinks (built-in `ga4Destination`; bring your own) alongside Score; independent + per-destination consent-gated\n- ✅ Typed events (`defineEvent`) — compile-time prop checking, no runtime cost, composes with consent categories\n- ✅ Web vitals capture — opt-in LCP / CLS / INP / TTFB / FCP via `web-vitals` (optional peer dep)\n- ✅ Adaptive batching — opt-in backpressure based on flush latency, page visibility, and Save-Data\n- ✅ Dev debug overlay — opt-in floating panel showing live `inspect()` state (hotkey-toggleable)\n- ✅ Auth0-friendly JWT plumbing (`init({ getToken })` — refreshed each flush)\n- ✅ Optional durable queue (IndexedDB) — survives reload, tab crash, stadium WiFi\n- ✅ Match-state context enrichment — stamp live `gameMinute` / `scoreState` / etc. per event\n- ✅ Engagement scoring (`tracker.getEngagementScore()`) — passive 0–100 score from dwell, scroll, interactions, video, return frequency\n- ✅ Sponsor surface tracking — viewability impression / dwell / click via `data-score-sponsor` tagging + IntersectionObserver\n- ✅ `tracker.inspect()` debug snapshot — queue/consent/identity/persistence/engagement/sponsorship\n\n## Install\n```bash\nnpm i @3halves-labs/score-tracker\n# or\npnpm add @3halves-labs/score-tracker\n```\n## Quick start\n```tsx\nimport { ScoreTracker } from '@3halves-labs/score-tracker';\nimport { ScoreProvider, useScore } from '@3halves-labs/score-tracker/react';\n\nconst tracker = new ScoreTracker({\n  publishableKey: 'pub_abc123',\n  endpoint: 'https://middleware.example.com/api/tracking/website',\n  siteId: 'mysite',\n  batch: { max: 20, intervalMs: 2500 },\n  respectDNT: true,\n  autoPageviews: true,\n  autoRouteChange: true,\n  autoScroll: [25,50,75,100],\n  autoVideo: true,\n  debug: false\n});\n\nfunction App() {\n  return (\n    <ScoreProvider tracker={tracker}>\n      {/* your app */}\n    </ScoreProvider>\n  );\n}\n```\n\n## Identity linking (anonymous → signed-in)\n\nEvery event the tracker sends carries a `context.anonymousId` generated on first\nvisit. The backend resolves a fan from that id through a link table — so until\nthe link exists, **events fired before sign-in cannot be attributed to anyone**,\nno tracking rule can award, and no goal registers progress.\n\n`identify()` creates that link for you:\n\n```ts\n// After your auth flow completes:\ntracker.identify(user.id);\n```\n\nThat POSTs once to `/api/identity/link-anonymous` with the `anonymousId` this\nbrowser has been sending all along, authenticated with the fan token from\n`getToken` (or `setUserToken`). Nothing else changes — the id is not regenerated\nand no extra event is emitted.\n\n**Ordering is forgiving.** If the token arrives after `identify()` — a popup\nlogin, a silent refresh, a pasted JWT — `setUserToken()` retries the link. You\ndo not need to sequence them.\n\n**It is consent-gated.** The link only happens with `analytics` consent, for the\nsame reason events are gated: associating a pre-auth browsing identifier with a\nnamed person is exactly the processing consent governs. This is not lossy — with\nconsent denied, those pre-auth events were never sent either, so there is\nnothing to back-fill. Note that granting consent *later* in the session does not\nretroactively link.\n\n**Check it worked** via `inspect()`:\n\n```ts\ntracker.inspect().identity.link;\n// { linked: 1, skipped: 0, lastSkipReason: null }\n```\n\n`lastSkipReason` is the field to read when attribution is not happening:\n`'consent'`, `'no_token'`, `'already_linked'` or `'error'`.\n\n| Option | Default | Notes |\n| --- | --- | --- |\n| `identityEndpoint` | derived | Defaults to `/api/identity/link-anonymous` resolved against **`endpoint`'s origin** — the whole path is replaced, not just the last segment. If your ingest is proxied under a prefix (`/proxy/api/tracking/website`), set this explicitly. |\n\nThe call is idempotent server-side (`201` created, `200` duplicate), and the SDK\nadditionally remembers the `(anonymousId, userId)` pair so a reload does not\nre-post. The pair is deliberate: a shared device where a second fan signs in\nproduces a second link.\n\n## Consent gating\n\nThe tracker does **not** make consent decisions for you. It accepts a `getConsent` predicate that you wire to your CMP (OneTrust, Cookiebot, etc.) — events only fire while that predicate resolves truthy. If consent is revoked mid-session, call `shutdown()` to flush the in-flight buffer (via `sendBeacon` when available), unbind listeners, restore monkey-patched history methods, and lock the gate.\n\n```ts\nconst tracker = new ScoreTracker({\n  publishableKey: 'pub_abc123',\n  endpoint: 'https://middleware.example.com/api/tracking/website',\n  siteId: 'mysite',\n  // Sync or async predicate. Throwing or rejecting is treated as DENIED.\n  getConsent: () => window.OnetrustActiveGroups?.includes('C0002') ?? false,\n});\n\n// Recommended entry point — awaits any async predicate before starting.\nawait tracker.init();\n```\n\n`init()` is the recommended entry point. `start()` is preserved for backward compat and honors a *synchronous* `getConsent`; async predicates require `init()`.\n\n### OneTrust recipe\n\nOneTrust dispatches a `OneTrustGroupsUpdated` event whenever the user's choice changes. The simplest pattern is to (re-)`init()` on every event and let the SDK consult the predicate fresh:\n\n```ts\nconst tracker = new ScoreTracker({\n  publishableKey: 'pub_abc123',\n  endpoint: 'https://middleware.example.com/api/tracking/website',\n  siteId: 'mysite',\n  // Adjust the group ID to whichever OneTrust group your legal team has\n  // designated for product analytics / loyalty tracking.\n  getConsent: () => (window.OnetrustActiveGroups ?? '').split(',').includes('C0002'),\n});\n\nawait tracker.init(); // initial run on page load\n\nwindow.addEventListener('OneTrustGroupsUpdated', async () => {\n  // Revocation: tear everything down and stop attribution.\n  tracker.shutdown();\n  // Re-grant: init() consults getConsent and only starts if it's now truthy.\n  await tracker.init();\n});\n```\n\nNotes:\n\n- **Events that arrive before consent is granted are dropped, not buffered.** The SDK does not try to retroactively attribute pre-consent events — by design.\n- **`shutdown()` is final until the next `init()`.** It clears the timer, unbinds all listeners, restores `history.pushState` / `history.replaceState`, best-effort flushes the queue with `navigator.sendBeacon`, and locks the consent gate.\n- **Server-side rendering**: the constructor is SSR-safe. `init()` / `start()` must be called in a browser context.\n- **Failures fail closed**: if your `getConsent` throws or rejects, the SDK treats it as denied and stays idle.\n\n### Category-aware consent\n\n`getConsent` can return either a boolean (legacy) or a category object — the SDK normalizes both shapes internally:\n\n```ts\ngetConsent: () => ({\n  analytics: true,        // page views, scroll, video, custom track()\n  personalization: false, // recommendation / segmentation signals\n  advertising: false,     // sponsor impression, ad_*, etc.\n})\n```\n\nCategories: **`analytics`** / **`personalization`** / **`advertising`**. The tracker is considered active if *any* category is true; a fully-false object behaves like `getConsent: () => false`.\n\nPer-event declarations gate which events can fire under which categories:\n\n```ts\ntracker.track('ad_impression', { sponsor: 'Acme' }, {\n  requires: ['advertising'],\n});\n\n// Multiple categories — all must be true:\ntracker.track('personalized_ad', props, {\n  requires: ['advertising', 'personalization'],\n});\n```\n\nEvents whose `requires` lists a category currently false are **dropped and counted** — visible via `inspect().consent.dropped`. Built-in events declare their own categories:\n\n| Event | Declares |\n|---|---|\n| `pageview`, `route_change`, `scroll_depth`, `video_*`, `form_submit`, `cta_click` | `analytics` |\n| `sponsor_impression`, `sponsor_dwell`, `sponsor_click` | `advertising` |\n| Custom `track(name, props)` without `options.requires` | (nothing — fires whenever the global gate is open) |\n\n### OneTrust category recipe\n\nMap OneTrust groups to the consent categories — these IDs follow OneTrust's defaults; check your tenant config and adjust:\n\n```ts\nfunction readOneTrustCategories() {\n  const groups = (window.OnetrustActiveGroups ?? '').split(',');\n  return {\n    analytics:       groups.includes('C0002'), // Performance\n    personalization: groups.includes('C0003'), // Functional\n    advertising:     groups.includes('C0004'), // Targeting / Advertising\n  };\n}\n\nconst tracker = new ScoreTracker({\n  publishableKey: 'pub_abc123',\n  endpoint: 'https://middleware.example.com/api/tracking/website',\n  siteId: 'mysite',\n  getConsent: readOneTrustCategories,\n});\n\nawait tracker.init();\n\nwindow.addEventListener('OneTrustGroupsUpdated', async () => {\n  // Re-evaluate categories on every CMP update. The SDK will adopt the new\n  // category map and start/stop accordingly.\n  tracker.shutdown();\n  await tracker.init();\n});\n```\n\nReturning a `boolean` from `getConsent` remains fully supported and is equivalent to all-three-equal — existing integrations need no changes.\n\n## Regional privacy policy engine\n\nSet a single `privacy` option and the tracker adopts the right posture for a jurisdiction — consent defaults, retention cap, and fingerprint-signal suppression — without hand-wiring each knob. **Opt-in: when `privacy` is unset, behaviour is identical to before (equivalent to `'off'`).**\n\n```ts\nconst tracker = new ScoreTracker({\n  publishableKey: 'pk_…',\n  endpoint: 'https://mid.example.com/api/tracking/website',\n  siteId: 'site_…',\n  privacy: 'auto', // or 'gdpr' | 'cpra' | 'lgpd' | 'quebec' | 'off'\n});\n```\n\nObject form for `auto` resolution and tuning:\n\n```ts\nprivacy: {\n  mode: 'auto',\n  // A browser can't read CDN request headers, so surface the edge-resolved\n  // country to the client and return it here (ISO 3166-1 alpha-2, optional\n  // region subtag e.g. 'US-CA', 'CA-QC'). Async is fine.\n  getRegion: () => document.documentElement.dataset.country ?? null,\n  // Per-knob overrides for your legal team — merged onto the resolved policy.\n  overrides: { retentionMaxHours: 12 },\n}\n```\n\n### Region resolution (`mode: 'auto'`)\n\nAt `init()`, the region is resolved in order: explicit `region` → `getRegion()` → a built-in client hint (a `__SCORE_REGION__` global or `<meta name=\"score-region\" content=\"US-CA\">`, both populatable by a CDN / edge function) → **unknown falls back to the strictest policy (`gdpr`)**, fail-safe. The resolved region maps to a policy: EU/EEA/UK/CH → `gdpr`; `CA-QC` → `quebec` (rest of Canada → `gdpr`); any `US` → `cpra`; `BR` → `lgpd`; anything else → `gdpr`.\n\n### Policy table\n\n> These are **conservative engineering defaults pending legal review (#47 DPIA / legal sign-off)** — not legal advice. Override any value via `privacy.overrides`.\n\n| Policy | Consent default (no `getConsent`) | Retention cap | Fingerprinting | Session replay¹ | Geo enrichment¹ |\n|---|---|---|---|---|---|\n| `gdpr` | opt-in (all categories off) | 24 h | disabled | disabled | disabled |\n| `quebec` | opt-in (all categories off) | 24 h | disabled | disabled | disabled |\n| `lgpd` | opt-in (all categories off) | 48 h | disabled | disabled | allowed |\n| `cpra` | analytics + personalization on, **advertising off** (do-not-sell) | 7 days | allowed | allowed | allowed |\n| `off` | all on (legacy) | none | allowed | allowed | allowed |\n\n¹ **Declared-only.** The SDK ships no session-replay integration (that's #146) and adds no geo/IP enrichment today, so these flags are surfaced via `inspect()` for those features to consult when they land — they are **no-ops now**. The other three columns are **enforced**.\n\n### What each knob does\n\n- **Consent default** — applied only when no `getConsent` predicate is supplied. A predicate is always authoritative. An opt-in default (all-off) leaves the tracker idle until you wire consent — the correct GDPR/Law 25/LGPD behaviour. Composes with [category-aware consent](#category-aware-consent): `cpra` defaults `advertising:false`, so events declaring `requires: ['advertising']` are dropped until the user opts in.\n- **Retention cap** — clamps `queueStorage.maxAgeHours` **down** (never up). With `privacy: 'gdpr'` a configured 72 h durable queue is capped to 24 h.\n- **Fingerprinting** — strips the fingerprint-adjacent envelope signals `screen` and `locale`. (`ua` is retained — it rides in the HTTP request regardless and is needed for baseline analytics.)\n\n### Inspecting the active policy\n\nRegion resolution + the full active policy are surfaced via [`inspect()`](#live-state-via-inspect):\n\n```ts\ntracker.inspect().privacy\n// {\n//   mode: 'auto',\n//   region: 'US-CA',\n//   resolved: true,          // false only while an `auto` mode awaits init()\n//   policy: {\n//     name: 'cpra',\n//     consentDefault: { analytics: true, personalization: true, advertising: false },\n//     retentionMaxHours: 168,\n//     disableFingerprinting: false,\n//     disableSessionReplay: false,\n//     disableGeoEnrichment: false,\n//   },\n// }\n```\n\nThe default policy table is also exported as `PRIVACY_POLICIES` for inspection in your own code.\n\n## Session-replay correlation\n\nJoin your analytics events to an external session recording (rrweb, FullStory, …)\nwithout the SDK capturing or shipping any replay payload — it stamps a\n**`replayId`** on every event's envelope `context`, so the warehouse can correlate\nthe two. Off by default.\n\n```ts\n// 1) Let the SDK mint a stable per-session id:\nnew ScoreTracker({ /* … */, replay: true });\n\n// 2) Or sync to your recorder's session id (read on every flush):\nnew ScoreTracker({ /* … */, replay: { getReplayId: () => rrwebSession.id } });\n\n// 3) Or set it imperatively when your recorder starts a session:\ntracker.setReplayId(rrwebSession.id);   // tracker.getReplayId() reads it back\n```\n\n**Privacy-aware:** suppressed automatically when the active [privacy\npolicy](#regional-privacy-policy-engine) sets `disableSessionReplay` (GDPR /\nQuebec / LGPD by default) — `inspect().replay.active` goes `false` while\n`enabled` stays `true`. Correlation-only by design; bundling full rrweb is out of\nscope (the host owns the recorder).\n\n## Multi-destination fan-out\n\nMirror your events to additional analytics sinks alongside the Score middleware.\nScore stays the **primary, durable** destination (queue / retry / critical-tier);\n`destinations` are **best-effort** sinks that receive a PII-scrubbed copy of each\nbatch *after* it's acknowledged by Score.\n\n```ts\nimport { ScoreTracker, ga4Destination } from '@3halves-labs/score-tracker';\n\nnew ScoreTracker({\n  // …\n  destinations: [\n    ga4Destination({ measurementId: 'G-XXXX', apiSecret: '…' }), // built-in\n    myWarehouseDestination,                                       // bring your own\n  ],\n});\n```\n\n- **Independent:** one destination throwing (or hanging) never blocks another or\n  the primary Score delivery — failures are isolated and counted\n  (`inspect().destinations.dropped`).\n- **Consent-gated per destination:** set `requires` on a destination and it's\n  skipped unless that category is granted (`ga4Destination` defaults to\n  `'analytics'`). Composes with [category-aware consent](#category-aware-consent).\n- **Custom adapters** implement the `Destination` shape:\n\n  ```ts\n  const myWarehouseDestination: Destination = {\n    name: 'warehouse',\n    requires: 'analytics',\n    async send(events, context) { await fetch('/sink', { method: 'POST', body: JSON.stringify({ events, context }) }); },\n  };\n  ```\n\nOnly runs on a successful Score ack, so a retried batch isn't mirrored twice.\nVendors beyond GA4 (Segment, Snowflake, …) ship as separate adapters with the\nsame shape.\n\n## Typed events\n\n`tracker.track()` accepts either a plain string (legacy) or an `EventDefinition` produced by `defineEvent`. The typed form gives you compile-time prop checking with **zero runtime cost** — there's no schema validator, no extra bundle weight, just TypeScript inference.\n\n```ts\nimport { ScoreTracker, defineEvent } from '@3halves-labs/score-tracker';\n\nexport const VideoPlayed = defineEvent<{\n  videoId: string;\n  duration: number;\n  source: 'live' | 'replay';\n}>('video_played');\n\nconst tracker = new ScoreTracker({ /* … */ });\nawait tracker.init();\n\ntracker.track(VideoPlayed, { videoId: 'abc', duration: 120, source: 'live' });\n// ✓ ok\n\ntracker.track(VideoPlayed, { videoId: 'abc', duration: 120 });\n// ✗ TS error: Property 'source' is missing\n\ntracker.track(VideoPlayed, { videoId: 'abc', duration: 'two minutes' as any, source: 'live' });\n// ✗ TS error: Type 'string' is not assignable to type 'number'.\n```\n\n### Encoding the consent contract with the event\n\nA definition can carry its own `requires` so the consent contract lives next to the event, not at every call site. This composes with [category-aware consent](#category-aware-consent):\n\n```ts\nexport const AdImpression = defineEvent<{ sponsor: string; placement: string }>(\n  'ad_impression',\n  { requires: ['advertising'] },\n);\n\ntracker.track(AdImpression, { sponsor: 'Acme', placement: 'hero' });\n// Automatically gated on advertising consent — no need to repeat at the call site.\n```\n\nA per-call `options.requires` overrides the definition default if you need it:\n\n```ts\ntracker.track(AdImpression, props, { requires: ['advertising', 'personalization'] });\n```\n\n### Where to put definitions\n\nCo-locate them with the feature that emits them, or centralise in a single `events.ts` — the SDK doesn't care:\n\n```ts\n// app/events.ts\nimport { defineEvent } from '@3halves-labs/score-tracker';\n\nexport const Checkout      = defineEvent<{ orderId: string; amount: number; currency: string }>('checkout');\nexport const ShareClicked  = defineEvent<{ network: 'x' | 'whatsapp' | 'native' }>('share_clicked');\nexport const AdImpression  = defineEvent<{ sponsor: string }>('ad_impression', { requires: ['advertising'] });\n```\n\n### Generating definitions from JSON Schema\n\nFor teams that keep their event catalogue as schemas (analytics consistency, auto-docs, warehouse reliability), the SDK ships a generator that turns JSON Schema archetypes into the `defineEvent` definitions above — no hand-writing the TypeScript, and still **zero runtime cost** (the schema is a build-time input, not a bundled validator).\n\nDescribe each event once under `schemas/events/*.schema.json`:\n\n```json\n{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"title\": \"VideoPlayed\",\n  \"x-score-event\": \"video_played\",\n  \"x-score-requires\": [\"analytics\"],\n  \"type\": \"object\",\n  \"properties\": {\n    \"videoId\": { \"type\": \"string\" },\n    \"duration\": { \"type\": \"number\" },\n    \"source\": { \"type\": \"string\", \"enum\": [\"live\", \"vod\", \"clip\"] }\n  },\n  \"required\": [\"videoId\", \"duration\"],\n  \"additionalProperties\": false\n}\n```\n\n- `title` → the exported const name (e.g. `VideoPlayed`).\n- `x-score-event` → the runtime event name passed to `track()`.\n- `x-score-requires` → consent categories baked into the definition.\n- `required` decides which props are non-optional; `enum`s become string-literal unions.\n\nRun the generator:\n\n```bash\nnpm run gen:events   # writes src/generated/events.generated.ts\n```\n\nThen import the generated definitions — re-exported under the `Events` namespace:\n\n```ts\nimport { ScoreTracker, Events } from '@3halves-labs/score-tracker';\n\ntracker.track(Events.VideoPlayed, { videoId: 'abc', duration: 120, source: 'live' });\n// ✓ typed; IDE flags a missing videoId or a string duration\n\ntracker.track(Events.AdImpression, { sponsor: 'Acme', placement: 'hero' });\n// gated on advertising consent via the schema's x-score-requires\n```\n\nThe committed `events.generated.ts` is kept in sync with the schemas by a drift-guard test (`src/generated/events.codegen.test.ts`) — edit a schema without re-running `gen:events` and CI fails.\n\n### What about runtime validation?\n\nNot in this release — by design. The typed form catches mismatches at the call site (where the developer can fix them) and the build (where CI catches them). Adding a runtime validator would balloon the bundle for a benefit most consumers don't need. If a future use case needs it (e.g. accepting events from an untrusted source), it'll land as a plugin via the middleware-pipeline ticket (#137).\n\n### Back-compat\n\nThe string form is unchanged. `tracker.track('any_name', { props: 'as before' })` works exactly as it did pre-1.8.0 — no migration required.\n\n## Middleware pipeline\n\nFor enrichment, transformation, or custom filtering that doesn't fit any of the dedicated knobs (`matchContext`, `pii`, consent, etc.), register a synchronous middleware:\n\n```ts\nconst removeDeviceEnricher = tracker.use((evt, next) => {\n  evt.context = { ...evt.context, deviceClass: detectDevice() };\n  next(evt);                                         // pass through (mutated)\n});\n\ntracker.use((evt, next) => {\n  if (evt.name === 'internal_debug') return next(null); // drop\n  next(evt);\n});\n\nremoveDeviceEnricher();                              // detach later\n```\n\n### Where in the pipeline\n\nMiddlewares run **inside `enqueue()`**, after every built-in event-processing step but **before** the in-memory queue push. So your middleware sees:\n\n- The consent gate already passed (otherwise the event would have been dropped earlier).\n- `context.match` already stamped (from `matchContext` / `setMatchContext`).\n- `context.groups` already stamped (from current memberships).\n- `delivery` already resolved (from per-call options or `defaultDelivery`).\n- The `requires` field already stripped.\n\nPII scrubbing (`pii.rules`) runs **later**, at envelope-build time, so middlewares still see the raw values. This is intentional: it lets you make routing/filtering decisions on the real data before scrubbing erases it.\n\n### Chain semantics\n\nMultiple middlewares chain in registration order:\n\n```ts\ntracker.use((evt, next) => { evt.props = { ...evt.props, step1: true }; next(evt); });\ntracker.use((evt, next) => { console.log('saw step1:', !!evt.props.step1); next(evt); });\n//                                                  ^ logs `true`\n```\n\n`next(evt)` advances to the next middleware (or pushes to the queue if it's the last). `next(null)` short-circuits the rest of the chain and **drops** the event.\n\n### Failure modes\n\nThe pipeline is defensive against badly-written middlewares:\n\n- **Throwing**: the error is swallowed (debug-logged when `debug: true`), the event passes through unchanged. A broken middleware can't break tracking.\n- **Forgetting to call `next`**: the event is treated as dropped (and counted toward `middleware.dropped`). This matches the \"if you don't say pass through, you said drop\" mental model.\n\n### Live state\n\n```ts\ntracker.inspect().middleware\n// → { count: 2, dropped: 7 }\n```\n\n`dropped` is cumulative across every middleware's `next(null)` (or didn't-call-next). It's separate from the queue-cap drop counter and the consent-category drop counter — each tier has its own line.\n\n### Out of scope in v1\n\n**Async middlewares** (`async (evt, next) => { … }`). Supporting `await` inside a middleware would require restructuring `track()` / `enqueue()` to be async, which is a breaking-change risk for every existing call site. The sync v1 covers enrichment, transformation, and filtering — the high-leverage use cases. Async paths (network lookups, awaiting a feature-flag client) can land in a follow-up after the sync API settles.\n\n## Declarative React components\n\nFor CMS-driven layouts, marketing pages, and any React tree where you'd rather attach tracking via JSX than via imperative `tracker.track(...)` calls in handlers, the `react` subpath exports three components:\n\n```tsx\nimport { ScoreProvider, Track, TrackVisibility, Sponsor } from '@3halves-labs/score-tracker/react';\n\n<ScoreProvider tracker={tracker}>\n  <Track event=\"cta_clicked\" props={{ location: 'hero' }}>\n    <Button>Buy tickets</Button>\n  </Track>\n\n  <TrackVisibility event=\"hero_seen\" props={{ section: 'home' }} threshold={0.5}>\n    <Hero />\n  </TrackVisibility>\n\n  <Sponsor sponsor=\"Acme\" slot=\"hero\">\n    <img src=\"/acme.png\" alt=\"Acme\" />\n  </Sponsor>\n</ScoreProvider>\n```\n\nAll three wrap their child in a `<span style=\"display: contents\">` so they add no layout box — the wrapped element renders exactly as it would on its own. (`<Sponsor>` accepts an `as` prop if you want the surface itself to be the sponsor element — e.g. an `<a>`.)\n\n### `<Track>` — fire on a DOM event\n\n| Prop | Default | Notes |\n|---|---|---|\n| `event` | required | String event name OR a `defineEvent(...)` definition (composes with [typed events](#typed-events) — props are type-checked against the definition's `Props`). |\n| `props` | `{}` | Forwarded to `tracker.track()` as the second argument. |\n| `on` | `'click'` | DOM event that triggers the call. Accepts `'click' \\| 'mouseenter' \\| 'mouseleave' \\| 'focus' \\| 'blur' \\| 'submit'`. |\n| `requires` | — | Forwarded as `options.requires` (composes with [category-aware consent](#category-aware-consent)). |\n| `delivery` | — | Forwarded as `options.delivery` (composes with [delivery guarantees](#delivery-guarantees)). |\n\nThe listener attaches with `{ capture: true, passive: true }` so it never interferes with the child element's own handlers.\n\n```tsx\nconst CtaClicked = defineEvent<{ location: 'hero' | 'footer' }>('cta_clicked');\n\n<Track event={CtaClicked} props={{ location: 'hero' }} delivery=\"critical\">\n  <Button>Buy</Button>\n</Track>;\n```\n\n### `<TrackVisibility>` — fire when in viewport\n\n| Prop | Default | Notes |\n|---|---|---|\n| `event` / `props` | required / `{}` | As above. |\n| `threshold` | `0.5` | IntersectionObserver ratio (0–1). |\n| `once` | `true` | Fire only the first time the element crosses the threshold. Set `false` to fire on every entry. |\n| `requires` / `delivery` | — | Same as `<Track>`. |\n\n```tsx\n<TrackVisibility event=\"sponsor_seen\" props={{ sponsor: 'Acme' }} once>\n  <SponsorBanner />\n</TrackVisibility>\n```\n\nFalls back to a no-op on environments without IntersectionObserver (very old browsers / SSR). No polyfill is bundled.\n\n### `<Sponsor>` — declarative form of `data-score-sponsor`\n\n| Prop | Default | Notes |\n|---|---|---|\n| `sponsor` | required | Becomes the `data-score-sponsor` attribute. |\n| `slot` | — | Optional placement; becomes `data-score-sponsor-slot`. |\n| `as` | `'span'` (with `display: contents`) | Element type. Pass `'a'` etc. when the surface itself is the sponsor element — no extra wrapper. |\n| `className` | — | Passed through to the rendered element. |\n\nThe component **emits no events itself** — it just applies the data attributes that the existing [sponsorship observer](#sponsor-surface-tracking) is already watching. So `<Sponsor sponsor=\"Acme\">` and `<span data-score-sponsor=\"Acme\">` are equivalent; pick whichever fits your style.\n\n```tsx\n<Sponsor sponsor=\"Acme\" slot=\"hero\" as=\"a\" className=\"cta\">\n  <img src=\"/acme.png\" alt=\"Acme\" />\n</Sponsor>\n```\n\n### React version compatibility\n\nDeclared as a `peerDependency` (already documented at the top of this README). Tested against React 19; works on 17 and 18 since the components use only stable hooks (`useEffect`, `useRef`, `useContext`).\n\n## Web vitals\n\nOpt in via `performance.webVitals: true` to capture **LCP / CLS / INP / TTFB / FCP** for every page load and emit them as `web_vital` events:\n\n```ts\nconst tracker = new ScoreTracker({\n  /* … */\n  performance: { webVitals: true },\n});\n```\n\nThe SDK dynamically imports the [`web-vitals`](https://www.npmjs.com/package/web-vitals) library — it's declared as an **optional peer dependency** so you only install it if you use the feature:\n\n```bash\nnpm i web-vitals\n```\n\nIf the feature is enabled but `web-vitals` isn't installed (or fails to load), the SDK logs a debug warning, stays silent, and `inspect().performance.webVitals` reports `false`. Zero crashes, zero events.\n\n### Event shape\n\nEach metric fires **once per page load** (the `web-vitals` library handles dedup). Every event is named `web_vital`; the specific metric is in `props.metric`:\n\n```jsonc\n{\n  \"type\": \"track\",\n  \"name\": \"web_vital\",\n  \"occurredAt\": \"...\",\n  \"props\": {\n    \"metric\": \"LCP\",                 // 'LCP' | 'CLS' | 'INP' | 'TTFB' | 'FCP'\n    \"value\": 2341.5,                 // metric value (ms for time-based; unitless ratio for CLS)\n    \"rating\": \"good\",                // 'good' | 'needs-improvement' | 'poor' (per Google thresholds)\n    \"id\": \"v3-...\",                  // unique id from web-vitals (for de-dup downstream)\n    \"navigationType\": \"navigate\",    // 'navigate' | 'reload' | 'back-forward' | 'prerender' | ...\n    \"delta\": 2341.5\n  }\n}\n```\n\nFilter by `props.metric` in the warehouse to chart any individual signal.\n\n### Composes with category-aware consent\n\nWeb vitals events declare `requires: ['analytics']`. If the user denies analytics, they're dropped and counted toward `inspect().consent.dropped` like any other analytics event — no special-casing for performance.\n\n### Zero overhead when disabled\n\nIf `performance.webVitals` is omitted or `false`:\n\n- The dynamic `import('web-vitals')` is never reached at runtime.\n- The bundler keeps it as a real dynamic import (the SDK marks it external), so it lands as a separate chunk that the consumer's app never fetches unless they enable the feature.\n- No PerformanceObserver is registered, no events fire, no peer dep needs to be installed.\n\n`inspect().performance.webVitals` reports the wiring state.\n\n## Adaptive batching\n\nFor sites where flush latency varies wildly — stadium WiFi, mobile carriers, off-peak vs match-night traffic — the static `batch.max` / `batch.intervalMs` aren't ideal. Opt in to **adaptive batching** to let the SDK react to network conditions, tab visibility, and the user's Save-Data preference:\n\n```ts\nconst tracker = new ScoreTracker({\n  /* … */\n  batch: { max: 20, intervalMs: 2500, adaptive: true },\n});\n```\n\nSetting `adaptive: true` (or passing an object for tuned thresholds — see below) turns on three coupled behaviours.\n\n### 1. Latency-aware batch sizing (EWMA backpressure)\n\nThe SDK measures end-to-end flush latency for every batch and feeds it into an exponentially-weighted moving average (α=0.3). The next batch is sized based on where the EWMA sits:\n\n- **Latency > `highLatencyMs` (default 2000)** → halve the current batch size (floor `minBatchSize`, default 1). The backend is struggling; ease up.\n- **Latency < `lowLatencyMs` (default 500)** → double the current batch size (cap at `batch.max`). Healthy connection; pack more per round-trip.\n- **In between** → leave the current size unchanged.\n\nThis converges naturally on whatever batch size the backend can handle without piling up.\n\n### 2. Visibility pause\n\nWhen `document.visibilityState !== 'visible'`, the periodic flush timer is cleared and `flush()` calls become no-ops. The queue keeps accumulating in memory (and on disk if the [durable queue](#durable-queue-offline-reload-stadium-wifi) is on). On `visibilitychange` back to `visible`, the timer is re-armed and the queue drains immediately.\n\n`pagehide` still triggers the existing best-effort beacon flush, so events queued during a hidden tab don't all die when the user navigates away.\n\n### 3. Save-Data awareness\n\nWhen `navigator.connection.saveData` is true (Chrome / Android Chrome / some mobile browsers, set by the user via Data Saver), the SDK halves the effective max batch size and doubles the flush interval — fewer, smaller requests for users who've explicitly asked for it.\n\nThe `change` event on `navigator.connection` is watched, so toggling Data Saver mid-session updates the behaviour live.\n\n### Tuning\n\nPass an object instead of `true` to override defaults:\n\n```ts\nbatch: {\n  max: 50,\n  intervalMs: 2500,\n  adaptive: {\n    highLatencyMs: 1500,\n    lowLatencyMs: 300,\n    minBatchSize: 5,\n  },\n}\n```\n\n### Live state via `inspect()`\n\n```ts\ntracker.inspect().queue.adaptive\n// → {\n//     enabled: true,\n//     currentBatchSize: 8,           // halved from max because latency was high\n//     latencyEwmaMs: 2147.3,         // rolling EWMA of post-flush latency\n//     paused: null | 'hidden' | 'saveData',\n//   }\n```\n\nUseful for debugging \"why aren't my events shipping?\" — `paused: 'hidden'` is the most common answer.\n\n### Default off — back-compat\n\n`batch.adaptive` is `false` by default. Sites that want every event flushed at the configured cadence regardless of conditions keep their current behaviour with zero changes.\n\n## Delivery guarantees\n\nPer-event delivery tier. Default `'best-effort'` for everything; mark commerce / ticketing / wagering / entitlement events `'critical'` to get the asymmetric tier:\n\n```ts\ntracker.track('checkout_complete', { orderId, amount }, { delivery: 'critical' });\ntracker.track('purchase_authorized', props, { delivery: 'critical' });\n```\n\nOr set the default for a whole tracker instance dedicated to a critical flow:\n\n```ts\nconst checkoutTracker = new ScoreTracker({ /* … */, defaultDelivery: 'critical' });\ncheckoutTracker.track('amount_changed', { amount }); // critical by default\ncheckoutTracker.track('promo_applied', { code }, { delivery: 'best-effort' }); // opt out per call\n```\n\n### What `'critical'` actually does\n\n| Behaviour | best-effort | critical |\n|---|---|---|\n| Position in batch | preserved enqueue order | **sent first** in the batch |\n| When [`adaptive batching`](#adaptive-batching) has paused the timer because the tab is hidden | flush is a no-op | **flushes anyway** |\n| When age-evicted from the [durable queue](#durable-queue-offline-reload-stadium-wifi) before ack | silently dropped | **emits a `delivery_failed` event** with `originalEventId`, `originalEventName`, `originalOccurredAt`, `reason: 'age_evicted'`, `ageHours` |\n\n### `delivery_failed` event shape\n\nWhen a critical event sits in IndexedDB longer than `queueStorage.maxAgeHours` without being acked (because the device was offline, the backend was down, etc.), the **next** `init()` evicts it and emits a follow-up event:\n\n```jsonc\n{\n  \"name\": \"delivery_failed\",\n  \"props\": {\n    \"originalEventId\": \"uuid-of-the-dropped-event\",\n    \"originalEventName\": \"checkout_complete\",\n    \"originalOccurredAt\": \"2026-…\",\n    \"reason\": \"age_evicted\",\n    \"ageHours\": 72\n  }\n}\n```\n\nThe follow-up itself is **best-effort** — it's a signal, not the original event. Treat it server-side as \"the user did this thing but we never saw the original event; reconcile through your authenticated channel\" (e.g. check the order id against your order system).\n\n### Composition with prior tickets\n\n- **#129 (durable queue)** — critical events benefit most from IndexedDB persistence. They survive reloads + crashes; their final failure mode is the age-evict / `delivery_failed` path.\n- **#142 (adaptive batching)** — critical events override the visibility-pause. The batch-size shrink under high latency still applies (you don't want to bombard a struggling backend); critical events just get the head of the queue.\n- **#133 (consent categories)** — `delivery_failed` itself declares `requires: ['analytics']`.\n\n### Live state\n\n```ts\ntracker.inspect().delivery\n// → {\n//     defaultMode: 'best-effort',\n//     criticalQueued: 2,\n//     criticalDelivered: 17,\n//     criticalFailed: 0,\n//   }\n```\n\n`criticalFailed` is cumulative across the tracker's lifetime. A non-zero value is your \"investigate this\" signal.\n\n### Out of scope in v1\n\nDocumented explicitly because the ticket called these out:\n\n- **Aggressive retry policies beyond the existing fail-and-requeue.** Critical events use the same retry path as best-effort; they get a head-of-queue slot but no different backoff. If the backend is durably down, both tiers stay queued until the durable queue's `maxAgeHours` expires.\n- **Critical-only persistence when global `queueStorage` is in-memory.** Durability still requires `queueStorage: { type: 'indexeddb' }`. A future ticket can add a per-event persistence override; this one focuses on the priority + signal semantics.\n\n## Dev debug overlay\n\nOpt in with `debugOverlay: true` to mount a small floating panel in the bottom-right of the page that reads `tracker.inspect()` on a timer and renders the live state. Useful during development to answer \"why didn't that event ship?\" without opening DevTools and writing `tracker.inspect()` by hand.\n\n```ts\nconst tracker = new ScoreTracker({\n  /* … */\n  debugOverlay: true,                           // mounts collapsed, default hotkey Ctrl+Shift+E\n  // or:\n  debugOverlay: { startOpen: true, hotkey: 'Alt+D', refreshMs: 250 },\n});\n```\n\n### What it shows\n\nThe panel groups the inspect snapshot by section. Each row updates on the configured refresh tick (default 500ms):\n\n- **SDK** — `version`, `started`\n- **Queue** — `size`, `dropped`, `maxSize`, `persistence` (`memory` / `indexeddb` / `unavailable`), and if [adaptive batching](#adaptive-batching) is on: `adaptive.current`, `adaptive.latencyMs`, `adaptive.paused`\n- **Consent** — `granted`, `hasPredicate`, per-category flags (`analytics` / `personalization` / `advertising`), `dropped` (events blocked by `requires`)\n- **Identity** — `anonymousId`, `sessionId`, `userId`, `hasJwt` (never the JWT itself)\n- **Engagement** — composite `score`, `dwellSec`, `interactions`, `scrollMax`, `sessionCount`\n- **Match** (only when configured) — `hasProvider`, `hasManualValue`\n- **Sponsorship** (only when enabled) — `tracked`, `visible`\n- **Performance** (only when enabled and wired) — `webVitals`\n\nSections for unused features don't render at all, so the panel stays compact.\n\n### Options\n\n| Option | Default | Notes |\n|---|---|---|\n| `refreshMs` | `500` | How often to re-read `inspect()` and re-render. |\n| `hotkey` | `'Ctrl+Shift+E'` | Modifier+key spec. Accepts `ctrl`, `alt`, `shift`, `meta` as modifiers. Case-insensitive. |\n| `startOpen` | `false` | Whether to render the body expanded on first mount. The user's most recent toggle wins via `sessionStorage`. |\n\n### Persistence\n\nWhen the user toggles the panel open/closed, the choice is persisted in `sessionStorage` under `score.debugOverlay.open`. The next page load in the same session honors that choice, regardless of `startOpen` — so you don't flash the panel on every reload during development.\n\n### Security / production\n\n- The JWT is **never** in the panel. Only `hasJwt: true/false` is shown.\n- The panel uses inline styles + a `<style>` block scoped to its own `#score-debug-overlay` id — no global CSS pollution.\n- Treat it as a development surface. Ship `debugOverlay: process.env.NODE_ENV !== 'production'` or equivalent so it doesn't render in prod builds.\n\n### Zero overhead when disabled\n\nIf `debugOverlay` is omitted or `false`:\n\n- No DOM element is created, no `setInterval` is registered, no `keydown` listener is attached.\n- The `DebugOverlay` class is never constructed — tree-shaking strips the module from bundles that don't reference it.\n\n`inspect().debugOverlay` reports the current state:\n\n```ts\ntracker.inspect().debugOverlay\n// → { mounted: true,  open: false }   panel mounted but collapsed\n// → { mounted: true,  open: true  }   panel mounted and expanded\n// → { mounted: false, open: false }   feature off\n```\n\n## Diagnosing ingest failures (auth-edge vs app)\n\nThe tracking host may sit behind an auth **edge** (e.g. score-auth's ForwardAuth). A failed send can be rejected at **two layers**, both surfacing as 401/403 but meaning different things:\n\n- **Edge** — the request never reached the tracking service (an infra gate). Carries an `x-auth-request-redirect` header and/or `{ error: 'Authentication required', redirectUrl: 'https://login.…' }`. Usually means `endpoint` is pointed at a non-exempt path (the ingest path `/api/tracking/website` should bypass the edge).\n- **App** — the ingest itself refused the batch: bad/inactive site key, origin mismatch, or rate-limit (`{ success: false, error }`).\n\n`inspect().transport` classifies the most recent failure so you don't have to guess:\n\n```ts\ntracker.inspect().transport\n// → { edgeAuthFailures: 0, lastError: { status: 403, kind: 'app-4xx' } }   // e.g. invalid site key\n// → { edgeAuthFailures: 2, lastError: { status: 401, kind: 'edge-auth' } } // blocked at the ForwardAuth edge\n// kind: 'edge-auth' | 'app-4xx' | 'http' | 'network'\n```\n\nWith `debug: true`, an edge rejection logs a distinct, actionable warning (pointing you at the edge-exempt ingest path) rather than a generic \"send failed\".\n\n## PII handling\n\nThe SDK supports two strategies for handling personally-identifiable fields. Both apply **recursively** to nested objects and arrays.\n\n### Denylist (legacy + shorthand)\n\nThe simplest form — list the property keys you don't want shipping. Matching keys are removed from props and context before the envelope is built.\n\n```ts\nnew ScoreTracker({\n  /* … */\n  piiDenylist: ['email', 'phone', 'fullName'],\n});\n```\n\nThis is equivalent to passing `pii: { strategy: 'denylist', fields: [...] }` — use whichever form fits your call site.\n\n### Classify (per-field actions)\n\nFor more granular control, use the classify strategy. Each rule pairs a field name with an action; the **first matching rule** wins for a given key.\n\n```ts\nnew ScoreTracker({\n  /* … */\n  pii: {\n    strategy: 'classify',\n    rules: [\n      { field: 'email',     action: 'hash'     }, // → 'h:<8 hex chars>'\n      { field: 'phone',     action: 'drop'     }, // removed\n      { field: 'fullName',  action: 'redact'   }, // → '<redacted>'\n      { field: 'household', action: 'tokenize' }, // → 't:<n>' (stable)\n    ],\n  },\n});\n```\n\n### Actions\n\n| Action | Output | Use when |\n|---|---|---|\n| `drop` | (key removed) | The value adds zero analytical signal — don't even ship it. |\n| `redact` | `'<redacted>'` | You want to know the *field was present* but not its value. |\n| `tokenize` | `'t:1'`, `'t:2'`, … | You need a stable opaque value for warehouse joins, scoped per-tracker-instance. Same input → same token for the lifetime of this tracker. |\n| `hash` | `'h:<8 hex chars>'` | You need a deterministic opaque value joinable *across* tracker instances. **Caveat below.** |\n\n### About `hash`\n\nThe hash is **FNV-1a 32-bit**, hex-encoded with an `h:` prefix. It's deterministic and fast (sync inside `enqueue()` so it composes with the [durable queue](#durable-queue-offline-reload-stadium-wifi) and every other downstream feature). It is **not cryptographic** — short inputs over a small space (a list of email addresses, a list of phone numbers) are brute-forceable.\n\nUse `hash` for analytics de-identification where the downstream join key needs to be stable across users / sessions / sites; do **not** use it as a security primitive. If you need cryptographic strength, redact the field instead and surface its real value only over your authenticated server-to-server channel.\n\n### Recursive behaviour\n\nRules apply at every nesting level — there's no path syntax, just a flat list of field names. A rule on `email` scrubs `email` everywhere it appears:\n\n```ts\ntracker.track('order', {\n  orderId: 'o-1',\n  customer: {\n    email: 'x@y.com',                          // → 'h:…'\n    phone: '+5678',                            // → removed\n    addresses: [\n      { city: 'Dublin', email: 'home@y.com' }, // → 'h:…'  (different hash than above)\n      { city: 'Cork',   phone: '+9999' },      // → phone removed\n    ],\n  },\n});\n```\n\n### Precedence + back-compat\n\n- The legacy `piiDenylist: string[]` option still works unchanged.\n- When **both** `pii` and `piiDenylist` are set, `pii` wins entirely — the denylist is ignored. This makes it safe to migrate one option at a time.\n- When **neither** is set, no scrubbing happens.\n\n### Live state\n\n```ts\ntracker.inspect().pii\n// → { strategy: 'classify',  ruleCount: 4, tokenizedValues: 1 }\n// → { strategy: 'denylist',  ruleCount: 3, tokenizedValues: 0 }\n// → { strategy: 'none',      ruleCount: 0, tokenizedValues: 0 }\n```\n\n`tokenizedValues` is the number of distinct inputs the tokenizer has seen — useful for detecting when a tokenize-marked field is being shipped at higher cardinality than you expected.\n\n## Identity graph (`alias` / `group` / `ungroup`)\n\nBeyond the basic `identify(userId, traits)` flow, the SDK exposes three Segment-style methods for modelling identity graphs — useful for household stitching, team / organisation memberships, sponsor cohorts, B2B account attribution, and so on.\n\n### `tracker.alias(previousId, userId)`\n\nBind a legacy or anonymous id to a real userId. Emits a single `$alias` event with both ids and rebinds the tracker's `userId` for subsequent events — no separate `identify()` call needed.\n\n```ts\ntracker.alias('anon-' + tracker.inspect().identity.anonymousId, 'user-42');\n// envelope events from this point onward carry context.userId = 'user-42'\n```\n\n### `tracker.group(groupId, traits?)`\n\nAdd or update a group membership. Multiple `group()` calls accumulate — a user can belong to many groups at once (team + organisation + sponsor cohort + …). Calling `group()` again with an existing id replaces that group's traits.\n\n```ts\ntracker.group('team-munster', { name: 'Munster', tier: 'pro' });\ntracker.group('sponsor-acme', { tier: 'gold' });\n```\n\nEach `group()` call emits a `$group` event with the id + traits. From that point on, **every enqueued event is stamped with `context.groups`** containing the full membership list:\n\n```jsonc\n{\n  \"type\": \"track\",\n  \"name\": \"cheer\",\n  \"context\": {\n    \"groups\": [\n      { \"id\": \"team-munster\", \"traits\": { \"name\": \"Munster\", \"tier\": \"pro\" } },\n      { \"id\": \"sponsor-acme\",  \"traits\": { \"tier\": \"gold\" } }\n    ]\n  }\n}\n```\n\n### `tracker.ungroup(groupId)`\n\nRemove a membership. No-op if the user wasn't a member. Emits a `$ungroup` event when there was an actual removal, so downstream consumers can mirror the change.\n\n```ts\ntracker.ungroup('team-munster'); // emits $ungroup and stops stamping team-munster\ntracker.ungroup('nobody');       // silent no-op\n```\n\n### At-event-time guarantee + durable-queue composition\n\nGroup context is stamped at **enqueue time**, not at flush. An event fired while the user was in `team-munster` carries that membership even if the user has since called `ungroup('team-munster')` before the flush actually leaves. Because the membership is part of the `TrackEvent` itself, it also **survives [durable queue](#durable-queue-offline-reload-stadium-wifi) rehydration** — a queued event reloaded from IndexedDB on the next `init()` keeps its original at-event-time group state, regardless of what the user's current memberships are.\n\n### Persistence\n\nMemberships are persisted in localStorage under `score.groups` alongside `anonymousId`. They survive:\n\n- Page reloads in the same browser\n- Closing and reopening tabs\n\nThey do **not** survive clearing localStorage or moving to a different browser / device — both of which are the standard \"logged out\" signal for downstream systems.\n\n### PII + consent + inspect\n\n- Group traits flow through the same [PII handling](#pii-handling) you've configured. A rule on `email` scrubs `email` inside `traits` exactly like anywhere else.\n- `$alias`, `$group`, and `$ungroup` events declare `requires: ['analytics']` — they respect category-aware consent like any other built-in event.\n- `inspect().groups` exposes `{ count, ids }` only. The `traits` map is deliberately **not** exposed via `inspect()` so debugging surfaces (the dev overlay, DevTools screenshots) don't leak group-level PII.\n\n```ts\ntracker.inspect().groups\n// → { count: 2, ids: ['team-munster', 'sponsor-acme'] }\n```\n\n## Cross-domain identity stitching\n\nMany sports properties span more than one domain — a main marketing site, a stats subdomain, a ticketing partner, a federated auth portal. Each domain runs its own `ScoreTracker` instance (the [two domains, two instances](#two-domains-two-instances) section explains why), but for some flows you still want the `anonymousId` (and current `sessionId`) to follow the user across the hop. The cross-domain linker is the bridge.\n\n```ts\nconst tracker = new ScoreTracker({\n  /* … */\n  crossDomain: {\n    enabled: true,\n    allowedDomains: ['rugby.com', '*.rugby.com'],\n    // linkerParam: '_score',      // default\n    // freshnessMs: 30_000,        // default 30s\n  },\n});\n\nawait tracker.init();\n```\n\n### On the source side: append to outbound URLs\n\n`tracker.appendLinker(url)` accepts a URL string and returns it with the linker query param appended **if** the URL's hostname matches `allowedDomains`. URLs to other hosts (and unparseable strings) pass through unchanged — safe to wrap unconditionally:\n\n```tsx\n// React example — wrap the href of any cross-domain link.\n<a href={tracker.appendLinker('https://stats.rugby.com/leaderboard')}>\n  Leaderboard\n</a>\n```\n\nFor SPAs that build URLs in JS (Next.js `router.push`, Vue Router, …), prefer `tracker.getLinkerValue()` which returns the raw encoded value — append it under your own linker key:\n\n```ts\nconst value = tracker.getLinkerValue();\nif (value) router.push(`https://stats.rugby.com/leaderboard?_score=${value}`);\n```\n\n### On the destination side: adopt on `init()`\n\nWhen the destination's `ScoreTracker` runs `init()`, it inspects `window.location.search` for the linker param. If **all three** of the following hold, the carried `anonymousId` and `sessionId` are adopted in place of the locally-stored / freshly-generated values:\n\n1. The linker payload decodes cleanly to `{ aid, sid, ts }`.\n2. `document.referrer`'s hostname matches `allowedDomains` (the `*.rugby.com` wildcard form matches any subdomain but not the apex).\n3. The encoded `ts` is within `freshnessMs` of now (default 30s — roughly the time it takes for a click to land).\n\nIf any check fails, the tracker silently falls back to its normal id-or-generate flow — adoption is opt-in and bad input never breaks `init()`.\n\n### Trust boundary (read this)\n\nThe linker payload is `base64url(JSON({ aid, sid, ts }))` with **no cryptographic signature**. Anyone who can construct a URL pointing at a cooperating domain can mint a payload claiming any `anonymousId`. That's by design: the use case is preserving identity continuity for analytics correlation across domains you control, **not** for asserting identity claims to a backend.\n\nThe `document.referrer` allowlist + freshness window raise the bar against drive-by adoption from arbitrary external sites; they are not a security boundary. If you need cryptographic guarantees (e.g. preventing a user from adopting another user's `anonymousId`), pair this with your own signed auth flow on each domain — the linker carries the analytics id, your auth flow carries the trust.\n\n### Wildcard semantics\n\n`allowedDomains` accepts two entry shapes:\n\n- **Exact hostname**: `'stats.rugby.com'` — matches only that exact host.\n- **Subdomain wildcard**: `'*.rugby.com'` — matches `a.rugby.com`, `b.c.rugby.com`, etc., but **not** the apex `rugby.com` itself. To match both, list the apex separately: `['rugby.com', '*.rugby.com']`.\n\n### Live state\n\n```ts\ntracker.inspect().crossDomain\n// → { enabled: true, adopted: true,  allowedDomainCount: 2 }   linker consumed\n// → { enabled: true, adopted: false, allowedDomainCount: 2 }   no linker / rejected\n// → { enabled: false, adopted: false, allowedDomainCount: 0 }  feature off\n```\n\n`adopted: true` is set once at construction and stays true for the lifetime of the tracker. The `allowedDomainCount` is a sanity check (\"did my config actually load?\").\n\n### Zero overhead when disabled\n\nIf `crossDomain` is omitted or `enabled: false`:\n\n- No URL parsing on `init()`.\n- `appendLinker(url)` returns its input unchanged.\n- `getLinkerValue()` returns `null`.\n\n## Deferred identify replay\n\nFor pre-login attribution flows (\"what content caused signup?\", \"what did this user do before they registered?\"), `identify()` accepts an opt-in third argument that **re-emits buffered anonymous events** with the new userId attached:\n\n```ts\ntracker.identify('user-42', { plan: 'pro' }, { replayAnonymousEvents: true });\n```\n\n### How the buffer works\n\n- Every event enqueued **before** `identify()` is mirrored into a separate replay buffer (in addition to being normally enqueued and shipped through the regular queue).\n- The buffer is capped via `identifyReplayBufferSize` (default 100). FIFO eviction; drops counted in `inspect().identity.replayBuffer.dropped`.\n- Identity events themselves (`$alias`, `$group`, `$ungroup`, `identify`) are **never** buffered, so replay can't recursively re-fire them.\n- Calling `identify()` **without** the replay option clears the buffer silently — pre-identify events past that line are no longer relevant.\n\n### What replay actually does\n\nWhen `replayAnonymousEvents: true`:\n\n1. The buffer is drained.\n2. Each buffered event is re-emitted as a **NEW envelope event** (fresh id), preserving the original `occurredAt`, props, and `context` (so at-event-time match state and group memberships are kept).\n3. Two extra props are stamped: `_backfill: true` and `_originalEventId: <orig.id>`.\n4. The envelope `context.userId` reflects the just-bound userId.\n\nThe original anonymous events are unchanged — they were already shipped via the normal queue at their original enqueue. Backfills are additive.\n\n### Warehouse de-dup recipe\n\nEach backfill carries a join key (`_originalEventId`) pointing at its anonymous source. Downstream you have two natural choices:\n\n**Keep both rows.** Common for funnel analysis where the anonymous attribution still matters and the identified backfill is just supplementary. Query for `_backfill = false` when you want raw anonymous counts; query for `_backfill = true` when you want identified attribution.\n\n**Replace anonymous with identified.** Drop the original anonymous row when its `event_id` matches some `_originalEventId` in the backfill set. The backfill carries the same `occurredAt` + props + context as the original plus the userId.\n\n```sql\n-- \"Replace\" pattern:\nDELETE FROM events\nWHERE event_id IN (\n  SELECT props._originalEventId\n  FROM events\n  WHERE props._backfill = true\n);\n```\n\nThe SDK ships the data + the join key; the policy is downstream's.\n\n### At-event-time guarantee through replay\n\nBackfilled events preserve their original `context` — including [match context](#match-state-context-enrichment) and [group memberships](#identity-graph-alias--group--ungroup). A backfill of an event captured at minute 23 still reads `context.match.gameMinute: 23` even if `identify()` was called at minute 67. This is why backfilled events are stamped at original enqueue time and not re-stamped on the replay path.\n\n### Live state\n\n```ts\ntracker.inspect().identity.replayBuffer\n// → { size: 12, maxSize: 100, dropped: 0, replayed: 0 }   pre-identify\n// → { size: 0,  maxSize: 100, dropped: 0, replayed: 12 }  post-replay\n// → { size: 0,  maxSize: 100, dropped: 4, replayed: 0  }  buffer overflowed, no replay yet\n```\n\n`dropped` is FIFO eviction (pages with massive pre-login activity that overflowed the cap); `replayed` is cumulative across all `identify()` calls in this tracker's lifetime.\n\n### Back-compat\n\n`identify(userId, traits)` (two args) is unchanged. The new third arg is optional and defaults to no replay — existing call sites need no migration.\n\n## JWT / Auth0\n\nPass a `getToken` provider at init time and the tracker refreshes the JWT before each batch flush — no need to call `setUserToken()` manually on every Auth0 refresh.\n\n```ts\nimport { Auth0Client } from '@auth0/auth0-spa-js';\n\nconst auth0 = new Auth0Client({ domain: '...', clientId: '...' });\n\nconst tracker = new ScoreTracker({\n  publishableKey: 'pub_abc123',\n  endpoint: 'https://middleware.example.com/api/tracking/website',\n  siteId: 'mysite',\n  // Async provider — called once at init() and again before every flush.\n  // The Auth0 SDK handles silent refresh internally, so successful calls\n  // always return a fresh, valid access token.\n  getToken: async () => {\n    try {\n      return await auth0.getTokenSilently();\n    } catch (e: any) {\n      // login_required / consent_required / etc. mean the user is signed out\n      // or hasn't authenticated yet. Returning undefined tells the tracker to\n      // continue with anonymous attribution by anonymousId.\n      if (e?.error === 'login_required' || e?.error === 'consent_required') return undefined;\n      throw e; // surfaces in debug mode; tracker still continues anonymously\n    }\n  },\n});\n\nawait tracker.init();\n```\n\n### Failure semantics\n\n| `getToken()` returns | Behavior |\n| --- | --- |\n| A string | Stamp the envelope with this JWT. |\n| `null` / `undefined` | Continue with anonymous tracking (no JWT in envelope). |\n| Throws / rejects | Swallow, clear the cached JWT, continue anonymously. Debug-logged when `debug: true`. |\n\nThe tracker **never blocks tracking on a token failure**. Anonymous events still attribute via `anonymousId`, and when a valid token returns on the next refresh the middleware can reconcile via the shadow user. To stop tracking entirely on logout, call `tracker.shutdown()` from your logout handler.\n\n### Concurrency\n\nIf two flushes overlap, `getToken` is only called once — the second flush awaits the first refresh. Safe to call `flush()` rapidly without thrashing your auth provider.\n\n### Identity from a cookie (no browser JWT) — `getUserId`\n\nSome hosts authenticate the user **server-side** (e.g. a WordPress/Auth0 code flow) and never expose a JWT to the page — they only set a JS-readable identity cookie. In that case use `getUserId` instead of (or alongside) `getToken`: it's a synchronous provider called at `init()`, and if it returns a value the tracker `identify()`s with it so every event carries `context.userId`.\n\n```ts\nconst tracker = new ScoreTracker({\n  /* … */\n  getUserId: () => {\n    // The host's login cookie (not HttpOnly, so JS can read it).\n    const m = document.cookie.match(/(?:^|;\\s*)auth0_user_id=([^;]+)/);\n    return m ? decodeURIComponent(m[1]) : undefined; // e.g. \"auth0|6a39…\"\n  },\n});\nawait tracker.init();\n```\n\n- Returns a value → `identify(userId)` (events stamp `context.userId`).\n- Returns `undefined` → stays anonymous.\n- **Trust model:** a cookie id is *client-asserted*, not cryptographically verified like a JWT. Prefer `getToken` (a verified JWT) when the host can expose one; treat `getUserId` as the cookie-only fallback. The backend decides how much to trust an unverified `context.userId`.\n- Re-evaluate on login/logout by calling `init()` again (or `identify()` / `shutdown()` from your auth handlers).\n\n## Two domains, two instances\n\nThe Score platform treats each domain as a separate tracked surface. If your\nproperty spans more than one domain — e.g. a main site and a stats/Match-Centre\nsubdomain — run **one `ScoreTracker` instance per domain**, each with its own\n`publishableKey` and `siteId`. Don't share a single instance (or a single key)\nacross domains: the keys are what keep each domain's events attributed to the\nright site, and the SDK stamps every envelope with the instance's own key.\n\nEach domain's `publishableKey` and `siteId` come from provisioning that domain\nin the admin app (`WebsiteTagCreator` → `POST /api/tracking/sites/credentials`),\nwhich returns a `siteKey` and a `siteCode`. The mapping into SDK options is:\n\n| Provisioning field | SDK option |\n| --- | --- |\n| `siteKey` (`pk_…`) | `publishableKey` |\n| `siteCode` | `siteId` |\n| `apiEndpoint` | `endpoint` |\n\n### Worked example: `rugby.com` + `stats.rugby.com`\n\nDefine each instance in its own module so the right one ships with each site's\nbundle. The two instances are fully independent — separate keys, separate\nbatching, separate consent/token wiring — so data from the two domains stays\nseparate.\n\n```ts\n// score.ts on rugby.com\nimport { ScoreTracker } from '@3halves-labs/score-tracker';\n\nexport const tracker = new ScoreTracker({\n  publishableKey: 'pk_xxxx…',          // the siteKey provisioned for rugby.com\n  siteId: 'rugby.com-xxxxxx',    // the siteCode provisioned for rugby.com\n  endpoint: 'https://<tracking-host>/api/tracking/website',\n  getConsent: () => (window.OnetrustActiveGroups ?? '').split(',').includes('C0002'),\n  getToken: async () => auth0.getTokenSilently().catch(() => undefined),\n});\n```\n\n```ts\n// score.ts on stats.rugby.com (Match Centre)\nimport { ScoreTracker } from '@3halves-labs/score-tracker';\n\nexport const tracker = new ScoreTracker({\n  publishableKey: 'pk_yyyy…',                 // the siteKey provisioned for stats.rugby.com\n  siteId: 'stats.rugby.com-yyyyyy',     // the siteCode provisioned for stats.rugby.com\n  endpoint: 'https://<tracking-host>/api/tracking/website',\n  getConsent: () => (window.OnetrustActiveGroups ?? '').split(',').includes('C0002'),\n  getToken: async () => auth0.getTokenSilently().catch(() => undefined),\n});\n```\n\nWire each instance exactly as a single-domain integration would — see\n[Consent gating](#consent-gating) for the OneTrust `getConsent` recipe and\n[JWT / Auth0](#jwt--auth0) for the `getToken` provider. Both must be set\n**per instance**; there is no shared/global state between instances, so each one\nresolves its own consent and token.\n\nNotes:\n\n- **One key per domain.** The `publishableKey` / `siteId` are tied to the domain\n  they were provisioned for; the middleware validates the pair and rejects a\n  mismatch. Reusing one domain's key on another domain will be rejected.\n- **The `endpoint` is the same** for both instances (the shared tracking\n  ingestion host) — only the keys differ.\n- **Consent and tokens are independent.** Each instance resolves its own\n  `getConsent` / `getToken`; calling `shutdown()` on one does not affect the\n  other.\n\n## Lifecycle methods\n\n| Method | Purpose |\n| --- | --- |\n| `new ScoreTracker(opts)` | Construct (SSR-safe; no side effects). |\n| `await tracker.init({ getConsent?, getToken? })` | Recommended entry. Awaits consent + initial token, starts if consent granted. |\n| `tracker.start()` | Sync legacy entry. Honors a sync `getConsent`; warns and skips on async. Does not pre-flight `getToken` — first flush will. |\n| `tracker.shutdown()` | Tear down: clear timer, unbind listeners, restore history methods, beacon-flush queue, lock gate. Idempotent. |\n| `tracker.track(name, props?)` | Custom event. Dropped silently if consent isn't granted. |\n| `tracker.page(props?)` | Pageview. Dropped silently if consent isn't granted. |\n| `tracker.identify(userId, traits?, options?)` | Bind events to a user identity. `options.replayAnonymousEvents: true` re-emits buffered pre-identify events with `_backfill` props. See [Deferred identify replay](#deferred-identify-replay). |\n| `tracker.alias(previousId, userId)` | Bind a previous/anonymous id to a real userId. Emits `$alias` + rebinds `userId`. See [Identity graph](#identity-graph-alias--group--ungroup). |\n| `tracker.group(groupId, traits?)` | Add/update a group membership; emits `$group` and stamps every subsequent event with `context.groups`. Multiple memberships allowed. |\n| `tracker.ungroup(groupId)` | Remove a group membership; emits `$ungroup`. No-op if not a member. |\n| `tracker.appendLinker(url)` | Append the cross-domain linker param to `url` if its hostname matches `crossDomain.allowedDomains`. Returns the URL unchanged otherwise. No-op when `crossDomain` is disabled. See [Cross-domain identity stitching](#cross-domain-identity-stitching). |\n| `tracker.use((evt, next) => …)` | Register a synchronous middleware that runs after built-in event processing and before queue push. Returns a `remove()` handle. See [Middleware pipeline](#middleware-pipeline). |\n| `tracker.getLinkerValue()` | Return the raw linker param value (base64url-encoded JSON of `{aid, sid, ts}`). Returns `null` when `crossDomain` is disabled. |\n| `tracker.setUserToken(jwt?)` | Set the JWT manually. Note: if `getToken` is also set, the next flush will overwrite this. Use either `setUserToken` *or* `getToken`, not both. |\n| `tracker.flush()` | Force-flush the queue. Calls `getToken` (if set) before posting. |\n| `tracker.setMatchContext(ctx \\| null)` | Imperative fallback for the `matchContext` provider. Useful for websocket / SSE driven match state. The constructor provider wins when both are set. Pass `null` to clear (e.g. full-time). |\n| `tracker.getEngagementScore()` | Comp","readmeFilename":"README.md"}