diff --git a/node_modules/.package-lock.json b/node_modules/.package-lock.json
index 625961a9..7f6b4150 100644
--- a/node_modules/.package-lock.json
+++ b/node_modules/.package-lock.json
@@ -306,78 +306,6 @@
"node": ">=6.9.0"
}
},
- "node_modules/@esbuild/aix-ppc64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
- "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "ideallyInert": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "aix"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/android-arm": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
- "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "ideallyInert": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/android-arm64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
- "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "ideallyInert": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/android-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
- "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "ideallyInert": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=12"
- }
- },
"node_modules/@esbuild/darwin-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
@@ -395,204 +323,6 @@
"node": ">=12"
}
},
- "node_modules/@esbuild/darwin-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
- "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "ideallyInert": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/freebsd-arm64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
- "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "ideallyInert": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/freebsd-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
- "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "ideallyInert": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-arm": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
- "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "ideallyInert": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-arm64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
- "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "ideallyInert": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-ia32": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
- "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "ideallyInert": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-loong64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
- "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
- "cpu": [
- "loong64"
- ],
- "dev": true,
- "ideallyInert": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-mips64el": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
- "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
- "cpu": [
- "mips64el"
- ],
- "dev": true,
- "ideallyInert": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-ppc64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
- "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "ideallyInert": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-riscv64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
- "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "ideallyInert": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-s390x": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
- "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "ideallyInert": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
"node_modules/@esbuild/linux-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
@@ -601,7 +331,6 @@
"x64"
],
"dev": true,
- "ideallyInert": true,
"license": "MIT",
"optional": true,
"os": [
@@ -611,114 +340,6 @@
"node": ">=12"
}
},
- "node_modules/@esbuild/netbsd-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
- "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "ideallyInert": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/openbsd-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
- "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "ideallyInert": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/sunos-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
- "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "ideallyInert": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "sunos"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/win32-arm64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
- "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "ideallyInert": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/win32-ia32": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
- "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "ideallyInert": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/win32-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
- "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "ideallyInert": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=12"
- }
- },
"node_modules/@eslint-community/eslint-utils": {
"version": "4.9.1",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
@@ -956,6 +577,15 @@
"node": ">= 8"
}
},
+ "node_modules/@probat/react": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/@probat/react/-/react-0.3.2.tgz",
+ "integrity": "sha512-QVdlSiJMMmNOJtRBgrlwOUywTMi+BV0fh1ei5ZSICik2FhDKKCAPZeMXtktGQbdeHWOHudMrWKZTTXm09ASbCg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": ">=18.0.0"
+ }
+ },
"node_modules/@rolldown/pluginutils": {
"version": "1.0.0-beta.27",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
@@ -963,36 +593,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/@rollup/rollup-android-arm-eabi": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz",
- "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "ideallyInert": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ]
- },
- "node_modules/@rollup/rollup-android-arm64": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz",
- "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "ideallyInert": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ]
- },
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz",
@@ -1007,216 +607,6 @@
"darwin"
]
},
- "node_modules/@rollup/rollup-darwin-x64": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz",
- "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "ideallyInert": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ]
- },
- "node_modules/@rollup/rollup-freebsd-arm64": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz",
- "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "ideallyInert": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ]
- },
- "node_modules/@rollup/rollup-freebsd-x64": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz",
- "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "ideallyInert": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ]
- },
- "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz",
- "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "ideallyInert": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-arm-musleabihf": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz",
- "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "ideallyInert": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-arm64-gnu": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz",
- "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "ideallyInert": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-arm64-musl": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz",
- "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "ideallyInert": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-loong64-gnu": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz",
- "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==",
- "cpu": [
- "loong64"
- ],
- "dev": true,
- "ideallyInert": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-loong64-musl": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz",
- "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==",
- "cpu": [
- "loong64"
- ],
- "dev": true,
- "ideallyInert": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-ppc64-gnu": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz",
- "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "ideallyInert": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-ppc64-musl": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz",
- "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "ideallyInert": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-riscv64-gnu": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz",
- "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "ideallyInert": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-riscv64-musl": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz",
- "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "ideallyInert": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-s390x-gnu": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz",
- "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "ideallyInert": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz",
@@ -1225,7 +615,6 @@
"x64"
],
"dev": true,
- "ideallyInert": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1240,103 +629,12 @@
"x64"
],
"dev": true,
- "ideallyInert": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
- "node_modules/@rollup/rollup-openbsd-x64": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz",
- "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "ideallyInert": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ]
- },
- "node_modules/@rollup/rollup-openharmony-arm64": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz",
- "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "ideallyInert": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openharmony"
- ]
- },
- "node_modules/@rollup/rollup-win32-arm64-msvc": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz",
- "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "ideallyInert": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ]
- },
- "node_modules/@rollup/rollup-win32-ia32-msvc": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz",
- "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "ideallyInert": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ]
- },
- "node_modules/@rollup/rollup-win32-x64-gnu": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz",
- "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "ideallyInert": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ]
- },
- "node_modules/@rollup/rollup-win32-x64-msvc": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz",
- "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "ideallyInert": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ]
- },
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@@ -2369,22 +1667,6 @@
"dev": true,
"license": "ISC"
},
- "node_modules/fsevents": {
- "version": "2.3.3",
- "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
- "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
- "dev": true,
- "hasInstallScript": true,
- "ideallyInert": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
- }
- },
"node_modules/gensync": {
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
diff --git a/node_modules/@esbuild/linux-x64/README.md b/node_modules/@esbuild/linux-x64/README.md
new file mode 100644
index 00000000..b2f19300
--- /dev/null
+++ b/node_modules/@esbuild/linux-x64/README.md
@@ -0,0 +1,3 @@
+# esbuild
+
+This is the Linux 64-bit binary for esbuild, a JavaScript bundler and minifier. See https://github.com/evanw/esbuild for details.
diff --git a/node_modules/@esbuild/linux-x64/bin/esbuild b/node_modules/@esbuild/linux-x64/bin/esbuild
new file mode 100755
index 00000000..288f7689
Binary files /dev/null and b/node_modules/@esbuild/linux-x64/bin/esbuild differ
diff --git a/node_modules/@esbuild/linux-x64/package.json b/node_modules/@esbuild/linux-x64/package.json
new file mode 100644
index 00000000..b70b09ec
--- /dev/null
+++ b/node_modules/@esbuild/linux-x64/package.json
@@ -0,0 +1,20 @@
+{
+ "name": "@esbuild/linux-x64",
+ "version": "0.21.5",
+ "description": "The Linux 64-bit binary for esbuild, a JavaScript bundler.",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/evanw/esbuild.git"
+ },
+ "license": "MIT",
+ "preferUnplugged": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "os": [
+ "linux"
+ ],
+ "cpu": [
+ "x64"
+ ]
+}
diff --git a/node_modules/@probat/react/README.md b/node_modules/@probat/react/README.md
new file mode 100644
index 00000000..2f48d7f2
--- /dev/null
+++ b/node_modules/@probat/react/README.md
@@ -0,0 +1,68 @@
+# @probat/react
+
+React SDK for Probat A/B testing. Automatic impression and click tracking with zero boilerplate.
+
+## Install
+
+```bash
+npm install @probat/react
+```
+
+## Quick Start
+
+Wrap your app root with the provider (Next.js App Router example):
+
+```tsx
+// app/providers.tsx
+"use client";
+import { ProbatProviderClient } from "@probat/react";
+
+export function Providers({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ );
+}
+```
+
+Create a thin experiment wrapper — one per experiment:
+
+```tsx
+// components/CTAButton/CTAButton.experiment.tsx
+"use client";
+import { Experiment } from "@probat/react";
+import { CTAButton } from "./CTAButton";
+import { CTAButtonAI } from "./CTAButton.ai-v1";
+
+export function CTAButtonExperiment() {
+ return (
+ }
+ variants={{ ai_v1: }}
+ />
+ );
+}
+```
+
+That's it. Impressions (50% visible for 250ms) and clicks are tracked automatically.
+
+## Custom Metrics
+
+```tsx
+const { capture } = useProbatMetrics();
+capture("purchase", { revenue: 42 });
+```
+
+## Run the Example
+
+```bash
+cd packages/probat-react/example
+node mocks/server.ts & # mock API on :3001
+npx next dev # app on :3000
+```
+
+## Docs
+
+See [SPEC.md](./SPEC.md) for the full event schemas, assignment rules, dedupe logic, and edge cases.
diff --git a/node_modules/@probat/react/dist/index.d.mts b/node_modules/@probat/react/dist/index.d.mts
new file mode 100644
index 00000000..f4bdb8b4
--- /dev/null
+++ b/node_modules/@probat/react/dist/index.d.mts
@@ -0,0 +1,104 @@
+import React from 'react';
+
+interface ProbatProviderProps {
+ /** Gushi user ID (UUID) — used to scope SDK requests to a customer */
+ userId: string;
+ /** Base URL for the Probat API. Defaults to https://gushi.onrender.com */
+ host?: string;
+ /**
+ * Bootstrap assignments to avoid flash on first render.
+ * Map of experiment id → variant key.
+ * e.g. { "cta-copy-test": "ai_v1" }
+ */
+ bootstrap?: Record;
+ children: React.ReactNode;
+}
+
+/**
+ * Client-only provider for Next.js App Router.
+ * Import this in your layout/providers file.
+ *
+ * @example
+ * ```tsx
+ * // app/providers.tsx
+ * "use client";
+ * import { ProbatProviderClient } from "@probat/react";
+ *
+ * export function Providers({ children }) {
+ * return (
+ *
+ * {children}
+ *
+ * );
+ * }
+ * ```
+ */
+declare function ProbatProviderClient(props: ProbatProviderProps): React.FunctionComponentElement;
+
+interface ExperimentTrackOptions {
+ /** Auto-track impressions (default true) */
+ impression?: boolean;
+ /** Auto-track clicks (default true) */
+ primaryClick?: boolean;
+ /** Custom impression event name (default "$experiment_exposure") */
+ impressionEventName?: string;
+ /** Custom click event name (default "$experiment_click") */
+ clickEventName?: string;
+}
+interface ExperimentProps {
+ /** Experiment key / identifier */
+ id: string;
+ /** Control variant ReactNode */
+ control: React.ReactNode;
+ /** Named variant ReactNodes, keyed by variant key (e.g. { ai_v1: }) */
+ variants: Record;
+ /** Tracking configuration */
+ track?: ExperimentTrackOptions;
+ /** Stable instance id when multiple instances of the same experiment exist on a page */
+ componentInstanceId?: string;
+ /** Behavior when assignment fetch fails: "control" (default) renders control, "suspend" throws */
+ fallback?: "control" | "suspend";
+ /** Log decisions + events to console */
+ debug?: boolean;
+}
+declare function Experiment({ id, control, variants, track, componentInstanceId, fallback, debug, }: ExperimentProps): React.JSX.Element;
+
+interface UseProbatMetricsReturn {
+ /**
+ * Send a custom event with arbitrary properties.
+ * Never throws — failures are silently dropped.
+ *
+ * @example
+ * ```tsx
+ * const { capture } = useProbatMetrics();
+ * capture("purchase", { revenue: 42, currency: "USD" });
+ * ```
+ */
+ capture: (event: string, properties?: Record) => void;
+}
+/**
+ * Minimal metrics hook. Provides a single `capture(event, props)` function
+ * that sends events to the Probat backend using the provider's host config.
+ */
+declare function useProbatMetrics(): UseProbatMetricsReturn;
+
+interface DecisionResponse {
+ variant_key: string;
+}
+interface MetricPayload {
+ event: string;
+ environment: "dev" | "prod";
+ properties: Record;
+}
+/**
+ * Fetch the variant assignment for an experiment.
+ * Returns the variant key string (e.g. "control", "ai_v1").
+ * Deduplicates concurrent calls for the same experiment.
+ */
+declare function fetchDecision(host: string, experimentId: string, distinctId: string): Promise;
+/**
+ * Fire-and-forget metric send. Never throws.
+ */
+declare function sendMetric(host: string, event: string, properties: Record): void;
+
+export { type DecisionResponse, Experiment, type ExperimentProps, type ExperimentTrackOptions, type MetricPayload, ProbatProviderClient, type ProbatProviderProps, type UseProbatMetricsReturn, fetchDecision, sendMetric, useProbatMetrics };
diff --git a/node_modules/@probat/react/dist/index.d.ts b/node_modules/@probat/react/dist/index.d.ts
new file mode 100644
index 00000000..f4bdb8b4
--- /dev/null
+++ b/node_modules/@probat/react/dist/index.d.ts
@@ -0,0 +1,104 @@
+import React from 'react';
+
+interface ProbatProviderProps {
+ /** Gushi user ID (UUID) — used to scope SDK requests to a customer */
+ userId: string;
+ /** Base URL for the Probat API. Defaults to https://gushi.onrender.com */
+ host?: string;
+ /**
+ * Bootstrap assignments to avoid flash on first render.
+ * Map of experiment id → variant key.
+ * e.g. { "cta-copy-test": "ai_v1" }
+ */
+ bootstrap?: Record;
+ children: React.ReactNode;
+}
+
+/**
+ * Client-only provider for Next.js App Router.
+ * Import this in your layout/providers file.
+ *
+ * @example
+ * ```tsx
+ * // app/providers.tsx
+ * "use client";
+ * import { ProbatProviderClient } from "@probat/react";
+ *
+ * export function Providers({ children }) {
+ * return (
+ *
+ * {children}
+ *
+ * );
+ * }
+ * ```
+ */
+declare function ProbatProviderClient(props: ProbatProviderProps): React.FunctionComponentElement;
+
+interface ExperimentTrackOptions {
+ /** Auto-track impressions (default true) */
+ impression?: boolean;
+ /** Auto-track clicks (default true) */
+ primaryClick?: boolean;
+ /** Custom impression event name (default "$experiment_exposure") */
+ impressionEventName?: string;
+ /** Custom click event name (default "$experiment_click") */
+ clickEventName?: string;
+}
+interface ExperimentProps {
+ /** Experiment key / identifier */
+ id: string;
+ /** Control variant ReactNode */
+ control: React.ReactNode;
+ /** Named variant ReactNodes, keyed by variant key (e.g. { ai_v1: }) */
+ variants: Record;
+ /** Tracking configuration */
+ track?: ExperimentTrackOptions;
+ /** Stable instance id when multiple instances of the same experiment exist on a page */
+ componentInstanceId?: string;
+ /** Behavior when assignment fetch fails: "control" (default) renders control, "suspend" throws */
+ fallback?: "control" | "suspend";
+ /** Log decisions + events to console */
+ debug?: boolean;
+}
+declare function Experiment({ id, control, variants, track, componentInstanceId, fallback, debug, }: ExperimentProps): React.JSX.Element;
+
+interface UseProbatMetricsReturn {
+ /**
+ * Send a custom event with arbitrary properties.
+ * Never throws — failures are silently dropped.
+ *
+ * @example
+ * ```tsx
+ * const { capture } = useProbatMetrics();
+ * capture("purchase", { revenue: 42, currency: "USD" });
+ * ```
+ */
+ capture: (event: string, properties?: Record) => void;
+}
+/**
+ * Minimal metrics hook. Provides a single `capture(event, props)` function
+ * that sends events to the Probat backend using the provider's host config.
+ */
+declare function useProbatMetrics(): UseProbatMetricsReturn;
+
+interface DecisionResponse {
+ variant_key: string;
+}
+interface MetricPayload {
+ event: string;
+ environment: "dev" | "prod";
+ properties: Record;
+}
+/**
+ * Fetch the variant assignment for an experiment.
+ * Returns the variant key string (e.g. "control", "ai_v1").
+ * Deduplicates concurrent calls for the same experiment.
+ */
+declare function fetchDecision(host: string, experimentId: string, distinctId: string): Promise;
+/**
+ * Fire-and-forget metric send. Never throws.
+ */
+declare function sendMetric(host: string, event: string, properties: Record): void;
+
+export { type DecisionResponse, Experiment, type ExperimentProps, type ExperimentTrackOptions, type MetricPayload, ProbatProviderClient, type ProbatProviderProps, type UseProbatMetricsReturn, fetchDecision, sendMetric, useProbatMetrics };
diff --git a/node_modules/@probat/react/dist/index.js b/node_modules/@probat/react/dist/index.js
new file mode 100644
index 00000000..ba78b7db
--- /dev/null
+++ b/node_modules/@probat/react/dist/index.js
@@ -0,0 +1,504 @@
+"use client";
+"use client";
+'use strict';
+
+var React3 = require('react');
+
+function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
+
+var React3__default = /*#__PURE__*/_interopDefault(React3);
+
+var ProbatContext = React3.createContext(null);
+var DEFAULT_HOST = "https://gushi.onrender.com";
+function ProbatProvider({
+ userId,
+ host = DEFAULT_HOST,
+ bootstrap,
+ children
+}) {
+ const value = React3.useMemo(
+ () => ({
+ host: host.replace(/\/$/, ""),
+ userId,
+ bootstrap: bootstrap ?? {}
+ }),
+ [userId, host, bootstrap]
+ );
+ return /* @__PURE__ */ React3__default.default.createElement(ProbatContext.Provider, { value }, children);
+}
+function useProbatContext() {
+ const ctx = React3.useContext(ProbatContext);
+ if (!ctx) {
+ throw new Error(
+ "useProbatContext must be used within . Wrap your app with ."
+ );
+ }
+ return ctx;
+}
+
+// src/components/ProbatProviderClient.tsx
+function ProbatProviderClient(props) {
+ return React3__default.default.createElement(ProbatProvider, props);
+}
+
+// src/utils/environment.ts
+function detectEnvironment() {
+ if (typeof window === "undefined") {
+ return "prod";
+ }
+ const hostname = window.location.hostname;
+ if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "0.0.0.0" || hostname.startsWith("192.168.") || hostname.startsWith("10.") || hostname.startsWith("172.16.") || hostname.startsWith("172.17.") || hostname.startsWith("172.18.") || hostname.startsWith("172.19.") || hostname.startsWith("172.20.") || hostname.startsWith("172.21.") || hostname.startsWith("172.22.") || hostname.startsWith("172.23.") || hostname.startsWith("172.24.") || hostname.startsWith("172.25.") || hostname.startsWith("172.26.") || hostname.startsWith("172.27.") || hostname.startsWith("172.28.") || hostname.startsWith("172.29.") || hostname.startsWith("172.30.") || hostname.startsWith("172.31.")) {
+ return "dev";
+ }
+ return "prod";
+}
+
+// src/utils/eventContext.ts
+var DISTINCT_ID_KEY = "probat:distinct_id";
+var SESSION_ID_KEY = "probat:session_id";
+var cachedDistinctId = null;
+var cachedSessionId = null;
+function generateId() {
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
+ return crypto.randomUUID();
+ }
+ const bytes = new Uint8Array(16);
+ if (typeof crypto !== "undefined" && crypto.getRandomValues) {
+ crypto.getRandomValues(bytes);
+ } else {
+ for (let i = 0; i < 16; i++) bytes[i] = Math.floor(Math.random() * 256);
+ }
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
+}
+function getDistinctId() {
+ if (cachedDistinctId) return cachedDistinctId;
+ if (typeof window === "undefined") return "server";
+ try {
+ const stored = localStorage.getItem(DISTINCT_ID_KEY);
+ if (stored) {
+ cachedDistinctId = stored;
+ return stored;
+ }
+ } catch {
+ }
+ const id = `anon_${generateId()}`;
+ cachedDistinctId = id;
+ try {
+ localStorage.setItem(DISTINCT_ID_KEY, id);
+ } catch {
+ }
+ return id;
+}
+function getSessionId() {
+ if (cachedSessionId) return cachedSessionId;
+ if (typeof window === "undefined") return "server";
+ try {
+ const stored = sessionStorage.getItem(SESSION_ID_KEY);
+ if (stored) {
+ cachedSessionId = stored;
+ return stored;
+ }
+ } catch {
+ }
+ const id = `sess_${generateId()}`;
+ cachedSessionId = id;
+ try {
+ sessionStorage.setItem(SESSION_ID_KEY, id);
+ } catch {
+ }
+ return id;
+}
+function getPageKey() {
+ if (typeof window === "undefined") return "";
+ return window.location.pathname + window.location.search;
+}
+function getPageUrl() {
+ if (typeof window === "undefined") return "";
+ return window.location.href;
+}
+function getReferrer() {
+ if (typeof document === "undefined") return "";
+ return document.referrer;
+}
+function buildEventContext() {
+ return {
+ distinct_id: getDistinctId(),
+ session_id: getSessionId(),
+ $page_url: getPageUrl(),
+ $pathname: typeof window !== "undefined" ? window.location.pathname : "",
+ $referrer: getReferrer()
+ };
+}
+
+// src/utils/api.ts
+var pendingDecisions = /* @__PURE__ */ new Map();
+async function fetchDecision(host, experimentId, distinctId) {
+ const existing = pendingDecisions.get(experimentId);
+ if (existing) return existing;
+ const promise = (async () => {
+ try {
+ const url = `${host.replace(/\/$/, "")}/experiment/decide`;
+ const res = await fetch(url, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ Accept: "application/json"
+ },
+ credentials: "include",
+ body: JSON.stringify({
+ experiment_id: experimentId,
+ distinct_id: distinctId
+ })
+ });
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
+ const data = await res.json();
+ return data.variant_key || "control";
+ } finally {
+ pendingDecisions.delete(experimentId);
+ }
+ })();
+ pendingDecisions.set(experimentId, promise);
+ return promise;
+}
+function sendMetric(host, event, properties) {
+ if (typeof window === "undefined") return;
+ const ctx = buildEventContext();
+ const payload = {
+ event,
+ environment: detectEnvironment(),
+ properties: {
+ ...ctx,
+ source: "react-sdk",
+ captured_at: (/* @__PURE__ */ new Date()).toISOString(),
+ ...properties
+ }
+ };
+ try {
+ const url = `${host.replace(/\/$/, "")}/experiment/metrics`;
+ fetch(url, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ credentials: "include",
+ body: JSON.stringify(payload)
+ }).catch(() => {
+ });
+ } catch {
+ }
+}
+function extractClickMeta(target) {
+ if (!target || !(target instanceof HTMLElement)) return null;
+ const primary = target.closest('[data-probat-click="primary"]');
+ if (primary) return buildMeta(primary, true);
+ const interactive = target.closest('button, a, [role="button"]');
+ if (interactive) return buildMeta(interactive, false);
+ return null;
+}
+function buildMeta(el, isPrimary) {
+ const meta = {
+ click_target_tag: el.tagName,
+ click_is_primary: isPrimary
+ };
+ if (el.id) meta.click_target_id = el.id;
+ const text = el.textContent?.trim();
+ if (text) meta.click_target_text = text.slice(0, 120);
+ return meta;
+}
+
+// src/utils/dedupeStorage.ts
+var PREFIX = "probat:seen:";
+var memorySet = /* @__PURE__ */ new Set();
+function makeDedupeKey(experimentId, variantKey, instanceId, pageKey) {
+ return `${PREFIX}${experimentId}:${variantKey}:${instanceId}:${pageKey}`;
+}
+function hasSeen(key) {
+ if (memorySet.has(key)) return true;
+ if (typeof window === "undefined") return false;
+ try {
+ return sessionStorage.getItem(key) === "1";
+ } catch {
+ return false;
+ }
+}
+function markSeen(key) {
+ memorySet.add(key);
+ if (typeof window === "undefined") return;
+ try {
+ sessionStorage.setItem(key, "1");
+ } catch {
+ }
+}
+var INSTANCE_PREFIX = "probat:instance:";
+function shortId() {
+ const bytes = new Uint8Array(4);
+ if (typeof crypto !== "undefined" && crypto.getRandomValues) {
+ crypto.getRandomValues(bytes);
+ } else {
+ for (let i = 0; i < 4; i++) bytes[i] = Math.floor(Math.random() * 256);
+ }
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
+}
+function resolveStableId(storageKey) {
+ if (typeof window !== "undefined") {
+ try {
+ const stored = sessionStorage.getItem(storageKey);
+ if (stored) return stored;
+ } catch {
+ }
+ }
+ const id = `inst_${shortId()}`;
+ if (typeof window !== "undefined") {
+ try {
+ sessionStorage.setItem(storageKey, id);
+ } catch {
+ }
+ }
+ return id;
+}
+var slotCounters = /* @__PURE__ */ new Map();
+var resetScheduled = false;
+function claimSlot(groupKey) {
+ const idx = slotCounters.get(groupKey) ?? 0;
+ slotCounters.set(groupKey, idx + 1);
+ if (!resetScheduled) {
+ resetScheduled = true;
+ Promise.resolve().then(() => {
+ slotCounters.clear();
+ resetScheduled = false;
+ });
+ }
+ return idx;
+}
+function useStableInstanceIdV18(experimentId) {
+ const reactId = React3__default.default.useId();
+ const ref = React3.useRef("");
+ if (!ref.current) {
+ const key = `${INSTANCE_PREFIX}${experimentId}:${getPageKey()}:${reactId}`;
+ ref.current = resolveStableId(key);
+ }
+ return ref.current;
+}
+function useStableInstanceIdFallback(experimentId) {
+ const slotRef = React3.useRef(-1);
+ const ref = React3.useRef("");
+ if (slotRef.current === -1) {
+ slotRef.current = claimSlot(`${experimentId}:${getPageKey()}`);
+ }
+ if (!ref.current) {
+ const key = `${INSTANCE_PREFIX}${experimentId}:${getPageKey()}:${slotRef.current}`;
+ ref.current = resolveStableId(key);
+ }
+ return ref.current;
+}
+var useStableInstanceId = typeof React3__default.default.useId === "function" ? useStableInstanceIdV18 : useStableInstanceIdFallback;
+
+// src/components/Experiment.tsx
+var ASSIGNMENT_PREFIX = "probat:assignment:";
+function readAssignment(id) {
+ if (typeof window === "undefined") return null;
+ try {
+ const raw = localStorage.getItem(ASSIGNMENT_PREFIX + id);
+ if (!raw) return null;
+ const parsed = JSON.parse(raw);
+ return parsed.variantKey ?? null;
+ } catch {
+ return null;
+ }
+}
+function writeAssignment(id, variantKey) {
+ if (typeof window === "undefined") return;
+ try {
+ const entry = { variantKey, ts: Date.now() };
+ localStorage.setItem(ASSIGNMENT_PREFIX + id, JSON.stringify(entry));
+ } catch {
+ }
+}
+function Experiment({
+ id,
+ control,
+ variants,
+ track,
+ componentInstanceId,
+ fallback = "control",
+ debug = false
+}) {
+ const { host, bootstrap } = useProbatContext();
+ const autoInstanceId = useStableInstanceId(id);
+ const instanceId = componentInstanceId ?? autoInstanceId;
+ const trackImpression = track?.impression !== false;
+ const trackClick = track?.primaryClick !== false;
+ const impressionEvent = track?.impressionEventName ?? "$experiment_exposure";
+ const clickEvent = track?.clickEventName ?? "$experiment_click";
+ const [variantKey, setVariantKey] = React3.useState(() => {
+ if (bootstrap[id]) return bootstrap[id];
+ const cached = readAssignment(id);
+ if (cached) return cached;
+ return "control";
+ });
+ const [resolved, setResolved] = React3.useState(() => {
+ return !!(bootstrap[id] || readAssignment(id));
+ });
+ React3.useEffect(() => {
+ if (bootstrap[id] || readAssignment(id)) {
+ const key = bootstrap[id] ?? readAssignment(id) ?? "control";
+ setVariantKey(key);
+ setResolved(true);
+ return;
+ }
+ let cancelled = false;
+ (async () => {
+ try {
+ const distinctId = getDistinctId();
+ const key = await fetchDecision(host, id, distinctId);
+ if (cancelled) return;
+ if (key !== "control" && !(key in variants)) {
+ if (debug) {
+ console.warn(
+ `[probat] Unknown variant "${key}" for experiment "${id}", falling back to control`
+ );
+ }
+ setVariantKey("control");
+ } else {
+ setVariantKey(key);
+ writeAssignment(id, key);
+ }
+ } catch (err) {
+ if (cancelled) return;
+ if (debug) {
+ console.error(`[probat] fetchDecision failed for "${id}":`, err);
+ }
+ if (fallback === "suspend") throw err;
+ setVariantKey("control");
+ } finally {
+ if (!cancelled) setResolved(true);
+ }
+ })();
+ return () => {
+ cancelled = true;
+ };
+ }, [id, host]);
+ React3.useEffect(() => {
+ if (debug && resolved) {
+ console.log(`[probat] Experiment "${id}" \u2192 variant "${variantKey}"`, {
+ instanceId,
+ pageKey: getPageKey()
+ });
+ }
+ }, [debug, id, variantKey, resolved, instanceId]);
+ const eventProps = React3.useMemo(
+ () => ({
+ experiment_id: id,
+ variant_key: variantKey,
+ component_instance_id: instanceId
+ }),
+ [id, variantKey, instanceId]
+ );
+ const containerRef = React3.useRef(null);
+ const impressionSent = React3.useRef(false);
+ React3.useEffect(() => {
+ if (!trackImpression || !resolved) return;
+ impressionSent.current = false;
+ const pageKey = getPageKey();
+ const dedupeKey = makeDedupeKey(id, variantKey, instanceId, pageKey);
+ if (hasSeen(dedupeKey)) {
+ impressionSent.current = true;
+ return;
+ }
+ const el = containerRef.current;
+ if (!el) return;
+ if (typeof IntersectionObserver === "undefined") {
+ if (!impressionSent.current) {
+ impressionSent.current = true;
+ markSeen(dedupeKey);
+ sendMetric(host, impressionEvent, eventProps);
+ if (debug) console.log(`[probat] Impression sent (no IO) for "${id}"`);
+ }
+ return;
+ }
+ let timer = null;
+ const observer = new IntersectionObserver(
+ ([entry]) => {
+ if (!entry || impressionSent.current) return;
+ if (entry.isIntersecting) {
+ timer = setTimeout(() => {
+ if (impressionSent.current) return;
+ impressionSent.current = true;
+ markSeen(dedupeKey);
+ sendMetric(host, impressionEvent, eventProps);
+ if (debug) console.log(`[probat] Impression sent for "${id}"`);
+ observer.disconnect();
+ }, 250);
+ } else if (timer) {
+ clearTimeout(timer);
+ timer = null;
+ }
+ },
+ { threshold: 0.5 }
+ );
+ observer.observe(el);
+ return () => {
+ observer.disconnect();
+ if (timer) clearTimeout(timer);
+ };
+ }, [
+ trackImpression,
+ resolved,
+ id,
+ variantKey,
+ instanceId,
+ host,
+ impressionEvent,
+ eventProps,
+ debug
+ ]);
+ const handleClick = React3.useCallback(
+ (e) => {
+ if (!trackClick) return;
+ const meta = extractClickMeta(e.target);
+ if (!meta) return;
+ sendMetric(host, clickEvent, {
+ ...eventProps,
+ ...meta
+ });
+ if (debug) {
+ console.log(`[probat] Click tracked for "${id}"`, meta);
+ }
+ },
+ [trackClick, host, clickEvent, eventProps, id, debug]
+ );
+ const content = variantKey === "control" || !(variantKey in variants) ? control : variants[variantKey];
+ return /* @__PURE__ */ React3__default.default.createElement(
+ "div",
+ {
+ ref: containerRef,
+ onClick: handleClick,
+ "data-probat-experiment": id,
+ "data-probat-variant": variantKey,
+ style: {
+ display: "block",
+ margin: 0,
+ padding: 0,
+ opacity: resolved ? 1 : 0,
+ transition: resolved ? "opacity 0.15s ease-in" : "none"
+ }
+ },
+ content
+ );
+}
+function useProbatMetrics() {
+ const { host } = useProbatContext();
+ const capture = React3.useCallback(
+ (event, properties = {}) => {
+ sendMetric(host, event, properties);
+ },
+ [host]
+ );
+ return { capture };
+}
+
+exports.Experiment = Experiment;
+exports.ProbatProviderClient = ProbatProviderClient;
+exports.fetchDecision = fetchDecision;
+exports.sendMetric = sendMetric;
+exports.useProbatMetrics = useProbatMetrics;
+//# sourceMappingURL=index.js.map
+//# sourceMappingURL=index.js.map
\ No newline at end of file
diff --git a/node_modules/@probat/react/dist/index.js.map b/node_modules/@probat/react/dist/index.js.map
new file mode 100644
index 00000000..b1279e20
--- /dev/null
+++ b/node_modules/@probat/react/dist/index.js.map
@@ -0,0 +1 @@
+{"version":3,"sources":["../src/context/ProbatContext.tsx","../src/components/ProbatProviderClient.tsx","../src/utils/environment.ts","../src/utils/eventContext.ts","../src/utils/api.ts","../src/utils/dedupeStorage.ts","../src/utils/stableInstanceId.ts","../src/components/Experiment.tsx","../src/hooks/useProbatMetrics.ts"],"names":["createContext","useMemo","React","useContext","useRef","useState","useEffect","useCallback"],"mappings":"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","file":"index.js","sourcesContent":["\"use client\";\n\nimport React, { createContext, useContext, useMemo } from \"react\";\n\nexport interface ProbatContextValue {\n host: string;\n userId: string;\n bootstrap: Record;\n}\n\nconst ProbatContext = createContext(null);\n\nconst DEFAULT_HOST = \"https://gushi.onrender.com\";\n\nexport interface ProbatProviderProps {\n /** Gushi user ID (UUID) — used to scope SDK requests to a customer */\n userId: string;\n /** Base URL for the Probat API. Defaults to https://gushi.onrender.com */\n host?: string;\n /**\n * Bootstrap assignments to avoid flash on first render.\n * Map of experiment id → variant key.\n * e.g. { \"cta-copy-test\": \"ai_v1\" }\n */\n bootstrap?: Record;\n children: React.ReactNode;\n}\n\nexport function ProbatProvider({\n userId,\n host = DEFAULT_HOST,\n bootstrap,\n children,\n}: ProbatProviderProps) {\n const value = useMemo(\n () => ({\n host: host.replace(/\\/$/, \"\"),\n userId,\n bootstrap: bootstrap ?? {},\n }),\n [userId, host, bootstrap]\n );\n\n return (\n \n {children}\n \n );\n}\n\nexport function useProbatContext(): ProbatContextValue {\n const ctx = useContext(ProbatContext);\n if (!ctx) {\n throw new Error(\n \"useProbatContext must be used within . Wrap your app with .\"\n );\n }\n return ctx;\n}\n","\"use client\";\n\nimport React from \"react\";\nimport { ProbatProvider } from \"../context/ProbatContext\";\nimport type { ProbatProviderProps } from \"../context/ProbatContext\";\n\n/**\n * Client-only provider for Next.js App Router.\n * Import this in your layout/providers file.\n *\n * @example\n * ```tsx\n * // app/providers.tsx\n * \"use client\";\n * import { ProbatProviderClient } from \"@probat/react\";\n *\n * export function Providers({ children }) {\n * return (\n * \n * {children}\n * \n * );\n * }\n * ```\n */\nexport function ProbatProviderClient(props: ProbatProviderProps) {\n return React.createElement(ProbatProvider, props);\n}\n\nexport type { ProbatProviderProps };\n","/**\n * Detect if the code is running on localhost (development environment).\n * Returns \"dev\" for localhost, \"prod\" for production.\n */\nexport function detectEnvironment(): \"dev\" | \"prod\" {\n if (typeof window === \"undefined\") {\n return \"prod\"; // Server-side, default to prod\n }\n\n const hostname = window.location.hostname;\n\n // Check for localhost, 127.0.0.1, or local IP addresses\n if (\n hostname === \"localhost\" ||\n hostname === \"127.0.0.1\" ||\n hostname === \"0.0.0.0\" ||\n hostname.startsWith(\"192.168.\") ||\n hostname.startsWith(\"10.\") ||\n hostname.startsWith(\"172.16.\") ||\n hostname.startsWith(\"172.17.\") ||\n hostname.startsWith(\"172.18.\") ||\n hostname.startsWith(\"172.19.\") ||\n hostname.startsWith(\"172.20.\") ||\n hostname.startsWith(\"172.21.\") ||\n hostname.startsWith(\"172.22.\") ||\n hostname.startsWith(\"172.23.\") ||\n hostname.startsWith(\"172.24.\") ||\n hostname.startsWith(\"172.25.\") ||\n hostname.startsWith(\"172.26.\") ||\n hostname.startsWith(\"172.27.\") ||\n hostname.startsWith(\"172.28.\") ||\n hostname.startsWith(\"172.29.\") ||\n hostname.startsWith(\"172.30.\") ||\n hostname.startsWith(\"172.31.\")\n ) {\n return \"dev\";\n }\n\n return \"prod\";\n}\n\n","/**\n * Event context helpers: distinct_id, session_id, page info.\n * All browser-safe — no-ops when window is unavailable.\n */\n\nconst DISTINCT_ID_KEY = \"probat:distinct_id\";\nconst SESSION_ID_KEY = \"probat:session_id\";\n\nlet cachedDistinctId: string | null = null;\nlet cachedSessionId: string | null = null;\n\nfunction generateId(): string {\n // crypto.randomUUID where available, else fallback\n if (typeof crypto !== \"undefined\" && crypto.randomUUID) {\n return crypto.randomUUID();\n }\n // fallback: random hex\n const bytes = new Uint8Array(16);\n if (typeof crypto !== \"undefined\" && crypto.getRandomValues) {\n crypto.getRandomValues(bytes);\n } else {\n for (let i = 0; i < 16; i++) bytes[i] = Math.floor(Math.random() * 256);\n }\n return Array.from(bytes, (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n}\n\nexport function getDistinctId(): string {\n if (cachedDistinctId) return cachedDistinctId;\n if (typeof window === \"undefined\") return \"server\";\n try {\n const stored = localStorage.getItem(DISTINCT_ID_KEY);\n if (stored) {\n cachedDistinctId = stored;\n return stored;\n }\n } catch {}\n const id = `anon_${generateId()}`;\n cachedDistinctId = id;\n try {\n localStorage.setItem(DISTINCT_ID_KEY, id);\n } catch {}\n return id;\n}\n\nexport function getSessionId(): string {\n if (cachedSessionId) return cachedSessionId;\n if (typeof window === \"undefined\") return \"server\";\n try {\n const stored = sessionStorage.getItem(SESSION_ID_KEY);\n if (stored) {\n cachedSessionId = stored;\n return stored;\n }\n } catch {}\n const id = `sess_${generateId()}`;\n cachedSessionId = id;\n try {\n sessionStorage.setItem(SESSION_ID_KEY, id);\n } catch {}\n return id;\n}\n\nexport function getPageKey(): string {\n if (typeof window === \"undefined\") return \"\";\n return window.location.pathname + window.location.search;\n}\n\nexport function getPageUrl(): string {\n if (typeof window === \"undefined\") return \"\";\n return window.location.href;\n}\n\nexport function getReferrer(): string {\n if (typeof document === \"undefined\") return \"\";\n return document.referrer;\n}\n\nexport interface EventContext {\n distinct_id: string;\n session_id: string;\n $page_url: string;\n $pathname: string;\n $referrer: string;\n}\n\nexport function buildEventContext(): EventContext {\n return {\n distinct_id: getDistinctId(),\n session_id: getSessionId(),\n $page_url: getPageUrl(),\n $pathname: typeof window !== \"undefined\" ? window.location.pathname : \"\",\n $referrer: getReferrer(),\n };\n}\n","import { detectEnvironment } from \"./environment\";\nimport { buildEventContext } from \"./eventContext\";\n\n// ── Types ──────────────────────────────────────────────────────────────────\n\nexport interface DecisionResponse {\n variant_key: string;\n}\n\nexport interface MetricPayload {\n event: string;\n environment: \"dev\" | \"prod\";\n properties: Record;\n}\n\n// ── Assignment fetching ────────────────────────────────────────────────────\n\nconst pendingDecisions = new Map>();\n\n/**\n * Fetch the variant assignment for an experiment.\n * Returns the variant key string (e.g. \"control\", \"ai_v1\").\n * Deduplicates concurrent calls for the same experiment.\n */\nexport async function fetchDecision(\n host: string,\n experimentId: string,\n distinctId: string\n): Promise {\n const existing = pendingDecisions.get(experimentId);\n if (existing) return existing;\n\n const promise = (async () => {\n try {\n const url = `${host.replace(/\\/$/, \"\")}/experiment/decide`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n },\n credentials: \"include\",\n body: JSON.stringify({\n experiment_id: experimentId,\n distinct_id: distinctId,\n }),\n });\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n const data = (await res.json()) as DecisionResponse;\n return data.variant_key || \"control\";\n } finally {\n pendingDecisions.delete(experimentId);\n }\n })();\n\n pendingDecisions.set(experimentId, promise);\n return promise;\n}\n\n// ── Metric sending ─────────────────────────────────────────────────────────\n\n/**\n * Fire-and-forget metric send. Never throws.\n */\nexport function sendMetric(\n host: string,\n event: string,\n properties: Record\n): void {\n if (typeof window === \"undefined\") return;\n\n const ctx = buildEventContext();\n const payload: MetricPayload = {\n event,\n environment: detectEnvironment(),\n properties: {\n ...ctx,\n source: \"react-sdk\",\n captured_at: new Date().toISOString(),\n ...properties,\n },\n };\n\n try {\n const url = `${host.replace(/\\/$/, \"\")}/experiment/metrics`;\n fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n credentials: \"include\",\n body: JSON.stringify(payload),\n }).catch(() => {});\n } catch {\n // silently drop\n }\n}\n\n// ── Click metadata extraction ──────────────────────────────────────────────\n\nexport interface ClickMeta {\n click_target_tag: string;\n click_target_text?: string;\n click_target_id?: string;\n click_is_primary: boolean;\n}\n\n/**\n * Given a click event inside an experiment boundary, extract metadata.\n * Prioritizes elements with data-probat-click=\"primary\",\n * then falls back to button/a/role=button.\n */\nexport function extractClickMeta(target: EventTarget | null): ClickMeta | null {\n if (!target || !(target instanceof HTMLElement)) return null;\n\n // Priority 1: explicit primary marker\n const primary = target.closest('[data-probat-click=\"primary\"]');\n if (primary) return buildMeta(primary as HTMLElement, true);\n\n // Priority 2: interactive elements\n const interactive = target.closest('button, a, [role=\"button\"]');\n if (interactive) return buildMeta(interactive as HTMLElement, false);\n\n return null;\n}\n\nfunction buildMeta(el: HTMLElement, isPrimary: boolean): ClickMeta {\n const meta: ClickMeta = {\n click_target_tag: el.tagName,\n click_is_primary: isPrimary,\n };\n if (el.id) meta.click_target_id = el.id;\n const text = el.textContent?.trim();\n if (text) meta.click_target_text = text.slice(0, 120);\n return meta;\n}\n","/**\n * Dedupe storage for experiment exposures.\n * Uses sessionStorage + in-memory Set fallback.\n * Key format: probat:seen:{id}:{variantKey}:{instanceId}:{pageKey}\n */\n\nconst PREFIX = \"probat:seen:\";\nconst memorySet = new Set();\n\nexport function makeDedupeKey(\n experimentId: string,\n variantKey: string,\n instanceId: string,\n pageKey: string\n): string {\n return `${PREFIX}${experimentId}:${variantKey}:${instanceId}:${pageKey}`;\n}\n\nexport function hasSeen(key: string): boolean {\n if (memorySet.has(key)) return true;\n if (typeof window === \"undefined\") return false;\n try {\n return sessionStorage.getItem(key) === \"1\";\n } catch {\n return false;\n }\n}\n\nexport function markSeen(key: string): void {\n memorySet.add(key);\n if (typeof window === \"undefined\") return;\n try {\n sessionStorage.setItem(key, \"1\");\n } catch {}\n}\n\n/** Reset all dedupe state — useful for testing. */\nexport function resetDedupe(): void {\n memorySet.clear();\n}\n","/**\n * Stable auto-generated instance IDs for .\n *\n * Problem: a naïve useRef + module counter gives a different ID on every mount,\n * so StrictMode double-mount or unmount/remount changes the dedupe key.\n *\n * Solution:\n * 1. React 18+ → useId() is stable per fiber position.\n * 2. Fallback → sessionStorage-backed slot counter per (experimentId, pageKey).\n * 3. Both paths persist a mapping in sessionStorage:\n * probat:instance:{experimentId}:{pageKey}:{positionKey} → stableId\n * so the same position resolves to the same ID across mounts.\n */\n\nimport React, { useRef } from \"react\";\nimport { getPageKey } from \"./eventContext\";\n\nconst INSTANCE_PREFIX = \"probat:instance:\";\n\n// ── Helpers ────────────────────────────────────────────────────────────────\n\nfunction shortId(): string {\n const bytes = new Uint8Array(4);\n if (typeof crypto !== \"undefined\" && crypto.getRandomValues) {\n crypto.getRandomValues(bytes);\n } else {\n for (let i = 0; i < 4; i++) bytes[i] = Math.floor(Math.random() * 256);\n }\n return Array.from(bytes, (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n}\n\n/**\n * Look up or create a stable instance ID in sessionStorage.\n */\nfunction resolveStableId(storageKey: string): string {\n if (typeof window !== \"undefined\") {\n try {\n const stored = sessionStorage.getItem(storageKey);\n if (stored) return stored;\n } catch {}\n }\n const id = `inst_${shortId()}`;\n if (typeof window !== \"undefined\") {\n try {\n sessionStorage.setItem(storageKey, id);\n } catch {}\n }\n return id;\n}\n\n// ── Fallback: render-wave slot counter ─────────────────────────────────────\n// Each synchronous render batch claims sequential slots per (experimentId,\n// pageKey). A microtask resets the counters so the next batch starts at 0,\n// giving the same component position the same slot across mounts.\n\nconst slotCounters = new Map();\nlet resetScheduled = false;\n\nfunction claimSlot(groupKey: string): number {\n const idx = slotCounters.get(groupKey) ?? 0;\n slotCounters.set(groupKey, idx + 1);\n if (!resetScheduled) {\n resetScheduled = true;\n Promise.resolve().then(() => {\n slotCounters.clear();\n resetScheduled = false;\n });\n }\n return idx;\n}\n\n// ── Hook: React 18+ path (useId available) ─────────────────────────────────\n\nfunction useStableInstanceIdV18(experimentId: string): string {\n const reactId = (React as any).useId() as string;\n const ref = useRef(\"\");\n if (!ref.current) {\n const key = `${INSTANCE_PREFIX}${experimentId}:${getPageKey()}:${reactId}`;\n ref.current = resolveStableId(key);\n }\n return ref.current;\n}\n\n// ── Hook: fallback path (no useId) ─────────────────────────────────────────\n\nfunction useStableInstanceIdFallback(experimentId: string): string {\n const slotRef = useRef(-1);\n const ref = useRef(\"\");\n if (slotRef.current === -1) {\n slotRef.current = claimSlot(`${experimentId}:${getPageKey()}`);\n }\n if (!ref.current) {\n const key = `${INSTANCE_PREFIX}${experimentId}:${getPageKey()}:${slotRef.current}`;\n ref.current = resolveStableId(key);\n }\n return ref.current;\n}\n\n// ── Exported hook ──────────────────────────────────────────────────────────\n// Selection is a module-level constant so the hook-call count never changes\n// between renders — safe for the rules of hooks.\n\nexport const useStableInstanceId: (experimentId: string) => string =\n typeof (React as any).useId === \"function\"\n ? useStableInstanceIdV18\n : useStableInstanceIdFallback;\n\n// ── Test utility ───────────────────────────────────────────────────────────\n\nexport function resetInstanceIdState(): void {\n slotCounters.clear();\n resetScheduled = false;\n}\n","\"use client\";\n\nimport React, {\n useEffect,\n useRef,\n useState,\n useCallback,\n useMemo,\n} from \"react\";\nimport { useProbatContext } from \"../context/ProbatContext\";\nimport { fetchDecision, sendMetric, extractClickMeta } from \"../utils/api\";\nimport { getDistinctId, getPageKey } from \"../utils/eventContext\";\nimport { makeDedupeKey, hasSeen, markSeen } from \"../utils/dedupeStorage\";\nimport { useStableInstanceId } from \"../utils/stableInstanceId\";\n\n// ── localStorage assignment cache ──────────────────────────────────────────\n\nconst ASSIGNMENT_PREFIX = \"probat:assignment:\";\n\ninterface StoredAssignment {\n variantKey: string;\n ts: number;\n}\n\nfunction readAssignment(id: string): string | null {\n if (typeof window === \"undefined\") return null;\n try {\n const raw = localStorage.getItem(ASSIGNMENT_PREFIX + id);\n if (!raw) return null;\n const parsed: StoredAssignment = JSON.parse(raw);\n return parsed.variantKey ?? null;\n } catch {\n return null;\n }\n}\n\nfunction writeAssignment(id: string, variantKey: string): void {\n if (typeof window === \"undefined\") return;\n try {\n const entry: StoredAssignment = { variantKey, ts: Date.now() };\n localStorage.setItem(ASSIGNMENT_PREFIX + id, JSON.stringify(entry));\n } catch {}\n}\n\n// ── Types ──────────────────────────────────────────────────────────────────\n\nexport interface ExperimentTrackOptions {\n /** Auto-track impressions (default true) */\n impression?: boolean;\n /** Auto-track clicks (default true) */\n primaryClick?: boolean;\n /** Custom impression event name (default \"$experiment_exposure\") */\n impressionEventName?: string;\n /** Custom click event name (default \"$experiment_click\") */\n clickEventName?: string;\n}\n\nexport interface ExperimentProps {\n /** Experiment key / identifier */\n id: string;\n /** Control variant ReactNode */\n control: React.ReactNode;\n /** Named variant ReactNodes, keyed by variant key (e.g. { ai_v1: }) */\n variants: Record;\n /** Tracking configuration */\n track?: ExperimentTrackOptions;\n /** Stable instance id when multiple instances of the same experiment exist on a page */\n componentInstanceId?: string;\n /** Behavior when assignment fetch fails: \"control\" (default) renders control, \"suspend\" throws */\n fallback?: \"control\" | \"suspend\";\n /** Log decisions + events to console */\n debug?: boolean;\n}\n\nexport function Experiment({\n id,\n control,\n variants,\n track,\n componentInstanceId,\n fallback = \"control\",\n debug = false,\n}: ExperimentProps) {\n const { host, bootstrap } = useProbatContext();\n\n // Stable instance id (useId + sessionStorage for cross-mount stability)\n const autoInstanceId = useStableInstanceId(id);\n const instanceId = componentInstanceId ?? autoInstanceId;\n\n // Track options with defaults\n const trackImpression = track?.impression !== false;\n const trackClick = track?.primaryClick !== false;\n const impressionEvent = track?.impressionEventName ?? \"$experiment_exposure\";\n const clickEvent = track?.clickEventName ?? \"$experiment_click\";\n\n // ── Assignment resolution ──────────────────────────────────────────────\n\n const [variantKey, setVariantKey] = useState(() => {\n // Sync resolution order: bootstrap → localStorage → \"control\"\n if (bootstrap[id]) return bootstrap[id];\n const cached = readAssignment(id);\n if (cached) return cached;\n return \"control\";\n });\n const [resolved, setResolved] = useState(() => {\n return !!(bootstrap[id] || readAssignment(id));\n });\n\n useEffect(() => {\n // Already resolved from bootstrap or cache\n if (bootstrap[id] || readAssignment(id)) {\n // Ensure state is synced (StrictMode may re-mount)\n const key = bootstrap[id] ?? readAssignment(id) ?? \"control\";\n setVariantKey(key);\n setResolved(true);\n return;\n }\n\n let cancelled = false;\n\n (async () => {\n try {\n const distinctId = getDistinctId();\n const key = await fetchDecision(host, id, distinctId);\n if (cancelled) return;\n\n // Validate variant key\n if (key !== \"control\" && !(key in variants)) {\n if (debug) {\n console.warn(\n `[probat] Unknown variant \"${key}\" for experiment \"${id}\", falling back to control`\n );\n }\n setVariantKey(\"control\");\n } else {\n setVariantKey(key);\n writeAssignment(id, key);\n }\n } catch (err) {\n if (cancelled) return;\n if (debug) {\n console.error(`[probat] fetchDecision failed for \"${id}\":`, err);\n }\n if (fallback === \"suspend\") throw err;\n setVariantKey(\"control\");\n } finally {\n if (!cancelled) setResolved(true);\n }\n })();\n\n return () => {\n cancelled = true;\n };\n }, [id, host]); // eslint-disable-line react-hooks/exhaustive-deps\n\n // ── Debug logging ──────────────────────────────────────────────────────\n\n useEffect(() => {\n if (debug && resolved) {\n console.log(`[probat] Experiment \"${id}\" → variant \"${variantKey}\"`, {\n instanceId,\n pageKey: getPageKey(),\n });\n }\n }, [debug, id, variantKey, resolved, instanceId]);\n\n // ── Shared event properties ────────────────────────────────────────────\n\n const eventProps = useMemo(\n () => ({\n experiment_id: id,\n variant_key: variantKey,\n component_instance_id: instanceId,\n }),\n [id, variantKey, instanceId]\n );\n\n // ── Impression tracking via IntersectionObserver ────────────────────────\n\n const containerRef = useRef(null);\n const impressionSent = useRef(false);\n\n useEffect(() => {\n if (!trackImpression || !resolved) return;\n\n // Reset on re-mount (StrictMode safety)\n impressionSent.current = false;\n\n const pageKey = getPageKey();\n const dedupeKey = makeDedupeKey(id, variantKey, instanceId, pageKey);\n\n // Already seen this session\n if (hasSeen(dedupeKey)) {\n impressionSent.current = true;\n return;\n }\n\n const el = containerRef.current;\n if (!el) return;\n\n // Fallback: no IntersectionObserver (SSR, old browser)\n if (typeof IntersectionObserver === \"undefined\") {\n if (!impressionSent.current) {\n impressionSent.current = true;\n markSeen(dedupeKey);\n sendMetric(host, impressionEvent, eventProps);\n if (debug) console.log(`[probat] Impression sent (no IO) for \"${id}\"`);\n }\n return;\n }\n\n let timer: ReturnType | null = null;\n\n const observer = new IntersectionObserver(\n ([entry]) => {\n if (!entry || impressionSent.current) return;\n\n if (entry.isIntersecting) {\n timer = setTimeout(() => {\n if (impressionSent.current) return;\n impressionSent.current = true;\n markSeen(dedupeKey);\n sendMetric(host, impressionEvent, eventProps);\n if (debug) console.log(`[probat] Impression sent for \"${id}\"`);\n observer.disconnect();\n }, 250);\n } else if (timer) {\n clearTimeout(timer);\n timer = null;\n }\n },\n { threshold: 0.5 }\n );\n\n observer.observe(el);\n\n return () => {\n observer.disconnect();\n if (timer) clearTimeout(timer);\n };\n }, [\n trackImpression,\n resolved,\n id,\n variantKey,\n instanceId,\n host,\n impressionEvent,\n eventProps,\n debug,\n ]);\n\n // ── Click tracking ─────────────────────────────────────────────────────\n\n const handleClick = useCallback(\n (e: React.MouseEvent) => {\n if (!trackClick) return;\n\n const meta = extractClickMeta(e.target as EventTarget);\n if (!meta) return;\n\n sendMetric(host, clickEvent, {\n ...eventProps,\n ...meta,\n });\n if (debug) {\n console.log(`[probat] Click tracked for \"${id}\"`, meta);\n }\n },\n [trackClick, host, clickEvent, eventProps, id, debug]\n );\n\n // ── Render ─────────────────────────────────────────────────────────────\n\n const content =\n variantKey === \"control\" || !(variantKey in variants)\n ? control\n : variants[variantKey];\n\n return (\n \n {content}\n
\n );\n}\n","\"use client\";\n\nimport { useCallback } from \"react\";\nimport { useProbatContext } from \"../context/ProbatContext\";\nimport { sendMetric } from \"../utils/api\";\n\nexport interface UseProbatMetricsReturn {\n /**\n * Send a custom event with arbitrary properties.\n * Never throws — failures are silently dropped.\n *\n * @example\n * ```tsx\n * const { capture } = useProbatMetrics();\n * capture(\"purchase\", { revenue: 42, currency: \"USD\" });\n * ```\n */\n capture: (event: string, properties?: Record) => void;\n}\n\n/**\n * Minimal metrics hook. Provides a single `capture(event, props)` function\n * that sends events to the Probat backend using the provider's host config.\n */\nexport function useProbatMetrics(): UseProbatMetricsReturn {\n const { host } = useProbatContext();\n\n const capture = useCallback(\n (event: string, properties: Record = {}) => {\n sendMetric(host, event, properties);\n },\n [host]\n );\n\n return { capture };\n}\n"]}
\ No newline at end of file
diff --git a/node_modules/@probat/react/dist/index.mjs b/node_modules/@probat/react/dist/index.mjs
new file mode 100644
index 00000000..7e3d816a
--- /dev/null
+++ b/node_modules/@probat/react/dist/index.mjs
@@ -0,0 +1,494 @@
+"use client";
+"use client";
+import React3, { createContext, useRef, useState, useEffect, useMemo, useCallback, useContext } from 'react';
+
+var ProbatContext = createContext(null);
+var DEFAULT_HOST = "https://gushi.onrender.com";
+function ProbatProvider({
+ userId,
+ host = DEFAULT_HOST,
+ bootstrap,
+ children
+}) {
+ const value = useMemo(
+ () => ({
+ host: host.replace(/\/$/, ""),
+ userId,
+ bootstrap: bootstrap ?? {}
+ }),
+ [userId, host, bootstrap]
+ );
+ return /* @__PURE__ */ React3.createElement(ProbatContext.Provider, { value }, children);
+}
+function useProbatContext() {
+ const ctx = useContext(ProbatContext);
+ if (!ctx) {
+ throw new Error(
+ "useProbatContext must be used within . Wrap your app with ."
+ );
+ }
+ return ctx;
+}
+
+// src/components/ProbatProviderClient.tsx
+function ProbatProviderClient(props) {
+ return React3.createElement(ProbatProvider, props);
+}
+
+// src/utils/environment.ts
+function detectEnvironment() {
+ if (typeof window === "undefined") {
+ return "prod";
+ }
+ const hostname = window.location.hostname;
+ if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "0.0.0.0" || hostname.startsWith("192.168.") || hostname.startsWith("10.") || hostname.startsWith("172.16.") || hostname.startsWith("172.17.") || hostname.startsWith("172.18.") || hostname.startsWith("172.19.") || hostname.startsWith("172.20.") || hostname.startsWith("172.21.") || hostname.startsWith("172.22.") || hostname.startsWith("172.23.") || hostname.startsWith("172.24.") || hostname.startsWith("172.25.") || hostname.startsWith("172.26.") || hostname.startsWith("172.27.") || hostname.startsWith("172.28.") || hostname.startsWith("172.29.") || hostname.startsWith("172.30.") || hostname.startsWith("172.31.")) {
+ return "dev";
+ }
+ return "prod";
+}
+
+// src/utils/eventContext.ts
+var DISTINCT_ID_KEY = "probat:distinct_id";
+var SESSION_ID_KEY = "probat:session_id";
+var cachedDistinctId = null;
+var cachedSessionId = null;
+function generateId() {
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
+ return crypto.randomUUID();
+ }
+ const bytes = new Uint8Array(16);
+ if (typeof crypto !== "undefined" && crypto.getRandomValues) {
+ crypto.getRandomValues(bytes);
+ } else {
+ for (let i = 0; i < 16; i++) bytes[i] = Math.floor(Math.random() * 256);
+ }
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
+}
+function getDistinctId() {
+ if (cachedDistinctId) return cachedDistinctId;
+ if (typeof window === "undefined") return "server";
+ try {
+ const stored = localStorage.getItem(DISTINCT_ID_KEY);
+ if (stored) {
+ cachedDistinctId = stored;
+ return stored;
+ }
+ } catch {
+ }
+ const id = `anon_${generateId()}`;
+ cachedDistinctId = id;
+ try {
+ localStorage.setItem(DISTINCT_ID_KEY, id);
+ } catch {
+ }
+ return id;
+}
+function getSessionId() {
+ if (cachedSessionId) return cachedSessionId;
+ if (typeof window === "undefined") return "server";
+ try {
+ const stored = sessionStorage.getItem(SESSION_ID_KEY);
+ if (stored) {
+ cachedSessionId = stored;
+ return stored;
+ }
+ } catch {
+ }
+ const id = `sess_${generateId()}`;
+ cachedSessionId = id;
+ try {
+ sessionStorage.setItem(SESSION_ID_KEY, id);
+ } catch {
+ }
+ return id;
+}
+function getPageKey() {
+ if (typeof window === "undefined") return "";
+ return window.location.pathname + window.location.search;
+}
+function getPageUrl() {
+ if (typeof window === "undefined") return "";
+ return window.location.href;
+}
+function getReferrer() {
+ if (typeof document === "undefined") return "";
+ return document.referrer;
+}
+function buildEventContext() {
+ return {
+ distinct_id: getDistinctId(),
+ session_id: getSessionId(),
+ $page_url: getPageUrl(),
+ $pathname: typeof window !== "undefined" ? window.location.pathname : "",
+ $referrer: getReferrer()
+ };
+}
+
+// src/utils/api.ts
+var pendingDecisions = /* @__PURE__ */ new Map();
+async function fetchDecision(host, experimentId, distinctId) {
+ const existing = pendingDecisions.get(experimentId);
+ if (existing) return existing;
+ const promise = (async () => {
+ try {
+ const url = `${host.replace(/\/$/, "")}/experiment/decide`;
+ const res = await fetch(url, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ Accept: "application/json"
+ },
+ credentials: "include",
+ body: JSON.stringify({
+ experiment_id: experimentId,
+ distinct_id: distinctId
+ })
+ });
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
+ const data = await res.json();
+ return data.variant_key || "control";
+ } finally {
+ pendingDecisions.delete(experimentId);
+ }
+ })();
+ pendingDecisions.set(experimentId, promise);
+ return promise;
+}
+function sendMetric(host, event, properties) {
+ if (typeof window === "undefined") return;
+ const ctx = buildEventContext();
+ const payload = {
+ event,
+ environment: detectEnvironment(),
+ properties: {
+ ...ctx,
+ source: "react-sdk",
+ captured_at: (/* @__PURE__ */ new Date()).toISOString(),
+ ...properties
+ }
+ };
+ try {
+ const url = `${host.replace(/\/$/, "")}/experiment/metrics`;
+ fetch(url, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ credentials: "include",
+ body: JSON.stringify(payload)
+ }).catch(() => {
+ });
+ } catch {
+ }
+}
+function extractClickMeta(target) {
+ if (!target || !(target instanceof HTMLElement)) return null;
+ const primary = target.closest('[data-probat-click="primary"]');
+ if (primary) return buildMeta(primary, true);
+ const interactive = target.closest('button, a, [role="button"]');
+ if (interactive) return buildMeta(interactive, false);
+ return null;
+}
+function buildMeta(el, isPrimary) {
+ const meta = {
+ click_target_tag: el.tagName,
+ click_is_primary: isPrimary
+ };
+ if (el.id) meta.click_target_id = el.id;
+ const text = el.textContent?.trim();
+ if (text) meta.click_target_text = text.slice(0, 120);
+ return meta;
+}
+
+// src/utils/dedupeStorage.ts
+var PREFIX = "probat:seen:";
+var memorySet = /* @__PURE__ */ new Set();
+function makeDedupeKey(experimentId, variantKey, instanceId, pageKey) {
+ return `${PREFIX}${experimentId}:${variantKey}:${instanceId}:${pageKey}`;
+}
+function hasSeen(key) {
+ if (memorySet.has(key)) return true;
+ if (typeof window === "undefined") return false;
+ try {
+ return sessionStorage.getItem(key) === "1";
+ } catch {
+ return false;
+ }
+}
+function markSeen(key) {
+ memorySet.add(key);
+ if (typeof window === "undefined") return;
+ try {
+ sessionStorage.setItem(key, "1");
+ } catch {
+ }
+}
+var INSTANCE_PREFIX = "probat:instance:";
+function shortId() {
+ const bytes = new Uint8Array(4);
+ if (typeof crypto !== "undefined" && crypto.getRandomValues) {
+ crypto.getRandomValues(bytes);
+ } else {
+ for (let i = 0; i < 4; i++) bytes[i] = Math.floor(Math.random() * 256);
+ }
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
+}
+function resolveStableId(storageKey) {
+ if (typeof window !== "undefined") {
+ try {
+ const stored = sessionStorage.getItem(storageKey);
+ if (stored) return stored;
+ } catch {
+ }
+ }
+ const id = `inst_${shortId()}`;
+ if (typeof window !== "undefined") {
+ try {
+ sessionStorage.setItem(storageKey, id);
+ } catch {
+ }
+ }
+ return id;
+}
+var slotCounters = /* @__PURE__ */ new Map();
+var resetScheduled = false;
+function claimSlot(groupKey) {
+ const idx = slotCounters.get(groupKey) ?? 0;
+ slotCounters.set(groupKey, idx + 1);
+ if (!resetScheduled) {
+ resetScheduled = true;
+ Promise.resolve().then(() => {
+ slotCounters.clear();
+ resetScheduled = false;
+ });
+ }
+ return idx;
+}
+function useStableInstanceIdV18(experimentId) {
+ const reactId = React3.useId();
+ const ref = useRef("");
+ if (!ref.current) {
+ const key = `${INSTANCE_PREFIX}${experimentId}:${getPageKey()}:${reactId}`;
+ ref.current = resolveStableId(key);
+ }
+ return ref.current;
+}
+function useStableInstanceIdFallback(experimentId) {
+ const slotRef = useRef(-1);
+ const ref = useRef("");
+ if (slotRef.current === -1) {
+ slotRef.current = claimSlot(`${experimentId}:${getPageKey()}`);
+ }
+ if (!ref.current) {
+ const key = `${INSTANCE_PREFIX}${experimentId}:${getPageKey()}:${slotRef.current}`;
+ ref.current = resolveStableId(key);
+ }
+ return ref.current;
+}
+var useStableInstanceId = typeof React3.useId === "function" ? useStableInstanceIdV18 : useStableInstanceIdFallback;
+
+// src/components/Experiment.tsx
+var ASSIGNMENT_PREFIX = "probat:assignment:";
+function readAssignment(id) {
+ if (typeof window === "undefined") return null;
+ try {
+ const raw = localStorage.getItem(ASSIGNMENT_PREFIX + id);
+ if (!raw) return null;
+ const parsed = JSON.parse(raw);
+ return parsed.variantKey ?? null;
+ } catch {
+ return null;
+ }
+}
+function writeAssignment(id, variantKey) {
+ if (typeof window === "undefined") return;
+ try {
+ const entry = { variantKey, ts: Date.now() };
+ localStorage.setItem(ASSIGNMENT_PREFIX + id, JSON.stringify(entry));
+ } catch {
+ }
+}
+function Experiment({
+ id,
+ control,
+ variants,
+ track,
+ componentInstanceId,
+ fallback = "control",
+ debug = false
+}) {
+ const { host, bootstrap } = useProbatContext();
+ const autoInstanceId = useStableInstanceId(id);
+ const instanceId = componentInstanceId ?? autoInstanceId;
+ const trackImpression = track?.impression !== false;
+ const trackClick = track?.primaryClick !== false;
+ const impressionEvent = track?.impressionEventName ?? "$experiment_exposure";
+ const clickEvent = track?.clickEventName ?? "$experiment_click";
+ const [variantKey, setVariantKey] = useState(() => {
+ if (bootstrap[id]) return bootstrap[id];
+ const cached = readAssignment(id);
+ if (cached) return cached;
+ return "control";
+ });
+ const [resolved, setResolved] = useState(() => {
+ return !!(bootstrap[id] || readAssignment(id));
+ });
+ useEffect(() => {
+ if (bootstrap[id] || readAssignment(id)) {
+ const key = bootstrap[id] ?? readAssignment(id) ?? "control";
+ setVariantKey(key);
+ setResolved(true);
+ return;
+ }
+ let cancelled = false;
+ (async () => {
+ try {
+ const distinctId = getDistinctId();
+ const key = await fetchDecision(host, id, distinctId);
+ if (cancelled) return;
+ if (key !== "control" && !(key in variants)) {
+ if (debug) {
+ console.warn(
+ `[probat] Unknown variant "${key}" for experiment "${id}", falling back to control`
+ );
+ }
+ setVariantKey("control");
+ } else {
+ setVariantKey(key);
+ writeAssignment(id, key);
+ }
+ } catch (err) {
+ if (cancelled) return;
+ if (debug) {
+ console.error(`[probat] fetchDecision failed for "${id}":`, err);
+ }
+ if (fallback === "suspend") throw err;
+ setVariantKey("control");
+ } finally {
+ if (!cancelled) setResolved(true);
+ }
+ })();
+ return () => {
+ cancelled = true;
+ };
+ }, [id, host]);
+ useEffect(() => {
+ if (debug && resolved) {
+ console.log(`[probat] Experiment "${id}" \u2192 variant "${variantKey}"`, {
+ instanceId,
+ pageKey: getPageKey()
+ });
+ }
+ }, [debug, id, variantKey, resolved, instanceId]);
+ const eventProps = useMemo(
+ () => ({
+ experiment_id: id,
+ variant_key: variantKey,
+ component_instance_id: instanceId
+ }),
+ [id, variantKey, instanceId]
+ );
+ const containerRef = useRef(null);
+ const impressionSent = useRef(false);
+ useEffect(() => {
+ if (!trackImpression || !resolved) return;
+ impressionSent.current = false;
+ const pageKey = getPageKey();
+ const dedupeKey = makeDedupeKey(id, variantKey, instanceId, pageKey);
+ if (hasSeen(dedupeKey)) {
+ impressionSent.current = true;
+ return;
+ }
+ const el = containerRef.current;
+ if (!el) return;
+ if (typeof IntersectionObserver === "undefined") {
+ if (!impressionSent.current) {
+ impressionSent.current = true;
+ markSeen(dedupeKey);
+ sendMetric(host, impressionEvent, eventProps);
+ if (debug) console.log(`[probat] Impression sent (no IO) for "${id}"`);
+ }
+ return;
+ }
+ let timer = null;
+ const observer = new IntersectionObserver(
+ ([entry]) => {
+ if (!entry || impressionSent.current) return;
+ if (entry.isIntersecting) {
+ timer = setTimeout(() => {
+ if (impressionSent.current) return;
+ impressionSent.current = true;
+ markSeen(dedupeKey);
+ sendMetric(host, impressionEvent, eventProps);
+ if (debug) console.log(`[probat] Impression sent for "${id}"`);
+ observer.disconnect();
+ }, 250);
+ } else if (timer) {
+ clearTimeout(timer);
+ timer = null;
+ }
+ },
+ { threshold: 0.5 }
+ );
+ observer.observe(el);
+ return () => {
+ observer.disconnect();
+ if (timer) clearTimeout(timer);
+ };
+ }, [
+ trackImpression,
+ resolved,
+ id,
+ variantKey,
+ instanceId,
+ host,
+ impressionEvent,
+ eventProps,
+ debug
+ ]);
+ const handleClick = useCallback(
+ (e) => {
+ if (!trackClick) return;
+ const meta = extractClickMeta(e.target);
+ if (!meta) return;
+ sendMetric(host, clickEvent, {
+ ...eventProps,
+ ...meta
+ });
+ if (debug) {
+ console.log(`[probat] Click tracked for "${id}"`, meta);
+ }
+ },
+ [trackClick, host, clickEvent, eventProps, id, debug]
+ );
+ const content = variantKey === "control" || !(variantKey in variants) ? control : variants[variantKey];
+ return /* @__PURE__ */ React3.createElement(
+ "div",
+ {
+ ref: containerRef,
+ onClick: handleClick,
+ "data-probat-experiment": id,
+ "data-probat-variant": variantKey,
+ style: {
+ display: "block",
+ margin: 0,
+ padding: 0,
+ opacity: resolved ? 1 : 0,
+ transition: resolved ? "opacity 0.15s ease-in" : "none"
+ }
+ },
+ content
+ );
+}
+function useProbatMetrics() {
+ const { host } = useProbatContext();
+ const capture = useCallback(
+ (event, properties = {}) => {
+ sendMetric(host, event, properties);
+ },
+ [host]
+ );
+ return { capture };
+}
+
+export { Experiment, ProbatProviderClient, fetchDecision, sendMetric, useProbatMetrics };
+//# sourceMappingURL=index.mjs.map
+//# sourceMappingURL=index.mjs.map
\ No newline at end of file
diff --git a/node_modules/@probat/react/dist/index.mjs.map b/node_modules/@probat/react/dist/index.mjs.map
new file mode 100644
index 00000000..b1a84f46
--- /dev/null
+++ b/node_modules/@probat/react/dist/index.mjs.map
@@ -0,0 +1 @@
+{"version":3,"sources":["../src/context/ProbatContext.tsx","../src/components/ProbatProviderClient.tsx","../src/utils/environment.ts","../src/utils/eventContext.ts","../src/utils/api.ts","../src/utils/dedupeStorage.ts","../src/utils/stableInstanceId.ts","../src/components/Experiment.tsx","../src/hooks/useProbatMetrics.ts"],"names":["React","useMemo","useRef","useCallback"],"mappings":"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","file":"index.mjs","sourcesContent":["\"use client\";\n\nimport React, { createContext, useContext, useMemo } from \"react\";\n\nexport interface ProbatContextValue {\n host: string;\n userId: string;\n bootstrap: Record;\n}\n\nconst ProbatContext = createContext(null);\n\nconst DEFAULT_HOST = \"https://gushi.onrender.com\";\n\nexport interface ProbatProviderProps {\n /** Gushi user ID (UUID) — used to scope SDK requests to a customer */\n userId: string;\n /** Base URL for the Probat API. Defaults to https://gushi.onrender.com */\n host?: string;\n /**\n * Bootstrap assignments to avoid flash on first render.\n * Map of experiment id → variant key.\n * e.g. { \"cta-copy-test\": \"ai_v1\" }\n */\n bootstrap?: Record;\n children: React.ReactNode;\n}\n\nexport function ProbatProvider({\n userId,\n host = DEFAULT_HOST,\n bootstrap,\n children,\n}: ProbatProviderProps) {\n const value = useMemo(\n () => ({\n host: host.replace(/\\/$/, \"\"),\n userId,\n bootstrap: bootstrap ?? {},\n }),\n [userId, host, bootstrap]\n );\n\n return (\n \n {children}\n \n );\n}\n\nexport function useProbatContext(): ProbatContextValue {\n const ctx = useContext(ProbatContext);\n if (!ctx) {\n throw new Error(\n \"useProbatContext must be used within . Wrap your app with .\"\n );\n }\n return ctx;\n}\n","\"use client\";\n\nimport React from \"react\";\nimport { ProbatProvider } from \"../context/ProbatContext\";\nimport type { ProbatProviderProps } from \"../context/ProbatContext\";\n\n/**\n * Client-only provider for Next.js App Router.\n * Import this in your layout/providers file.\n *\n * @example\n * ```tsx\n * // app/providers.tsx\n * \"use client\";\n * import { ProbatProviderClient } from \"@probat/react\";\n *\n * export function Providers({ children }) {\n * return (\n * \n * {children}\n * \n * );\n * }\n * ```\n */\nexport function ProbatProviderClient(props: ProbatProviderProps) {\n return React.createElement(ProbatProvider, props);\n}\n\nexport type { ProbatProviderProps };\n","/**\n * Detect if the code is running on localhost (development environment).\n * Returns \"dev\" for localhost, \"prod\" for production.\n */\nexport function detectEnvironment(): \"dev\" | \"prod\" {\n if (typeof window === \"undefined\") {\n return \"prod\"; // Server-side, default to prod\n }\n\n const hostname = window.location.hostname;\n\n // Check for localhost, 127.0.0.1, or local IP addresses\n if (\n hostname === \"localhost\" ||\n hostname === \"127.0.0.1\" ||\n hostname === \"0.0.0.0\" ||\n hostname.startsWith(\"192.168.\") ||\n hostname.startsWith(\"10.\") ||\n hostname.startsWith(\"172.16.\") ||\n hostname.startsWith(\"172.17.\") ||\n hostname.startsWith(\"172.18.\") ||\n hostname.startsWith(\"172.19.\") ||\n hostname.startsWith(\"172.20.\") ||\n hostname.startsWith(\"172.21.\") ||\n hostname.startsWith(\"172.22.\") ||\n hostname.startsWith(\"172.23.\") ||\n hostname.startsWith(\"172.24.\") ||\n hostname.startsWith(\"172.25.\") ||\n hostname.startsWith(\"172.26.\") ||\n hostname.startsWith(\"172.27.\") ||\n hostname.startsWith(\"172.28.\") ||\n hostname.startsWith(\"172.29.\") ||\n hostname.startsWith(\"172.30.\") ||\n hostname.startsWith(\"172.31.\")\n ) {\n return \"dev\";\n }\n\n return \"prod\";\n}\n\n","/**\n * Event context helpers: distinct_id, session_id, page info.\n * All browser-safe — no-ops when window is unavailable.\n */\n\nconst DISTINCT_ID_KEY = \"probat:distinct_id\";\nconst SESSION_ID_KEY = \"probat:session_id\";\n\nlet cachedDistinctId: string | null = null;\nlet cachedSessionId: string | null = null;\n\nfunction generateId(): string {\n // crypto.randomUUID where available, else fallback\n if (typeof crypto !== \"undefined\" && crypto.randomUUID) {\n return crypto.randomUUID();\n }\n // fallback: random hex\n const bytes = new Uint8Array(16);\n if (typeof crypto !== \"undefined\" && crypto.getRandomValues) {\n crypto.getRandomValues(bytes);\n } else {\n for (let i = 0; i < 16; i++) bytes[i] = Math.floor(Math.random() * 256);\n }\n return Array.from(bytes, (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n}\n\nexport function getDistinctId(): string {\n if (cachedDistinctId) return cachedDistinctId;\n if (typeof window === \"undefined\") return \"server\";\n try {\n const stored = localStorage.getItem(DISTINCT_ID_KEY);\n if (stored) {\n cachedDistinctId = stored;\n return stored;\n }\n } catch {}\n const id = `anon_${generateId()}`;\n cachedDistinctId = id;\n try {\n localStorage.setItem(DISTINCT_ID_KEY, id);\n } catch {}\n return id;\n}\n\nexport function getSessionId(): string {\n if (cachedSessionId) return cachedSessionId;\n if (typeof window === \"undefined\") return \"server\";\n try {\n const stored = sessionStorage.getItem(SESSION_ID_KEY);\n if (stored) {\n cachedSessionId = stored;\n return stored;\n }\n } catch {}\n const id = `sess_${generateId()}`;\n cachedSessionId = id;\n try {\n sessionStorage.setItem(SESSION_ID_KEY, id);\n } catch {}\n return id;\n}\n\nexport function getPageKey(): string {\n if (typeof window === \"undefined\") return \"\";\n return window.location.pathname + window.location.search;\n}\n\nexport function getPageUrl(): string {\n if (typeof window === \"undefined\") return \"\";\n return window.location.href;\n}\n\nexport function getReferrer(): string {\n if (typeof document === \"undefined\") return \"\";\n return document.referrer;\n}\n\nexport interface EventContext {\n distinct_id: string;\n session_id: string;\n $page_url: string;\n $pathname: string;\n $referrer: string;\n}\n\nexport function buildEventContext(): EventContext {\n return {\n distinct_id: getDistinctId(),\n session_id: getSessionId(),\n $page_url: getPageUrl(),\n $pathname: typeof window !== \"undefined\" ? window.location.pathname : \"\",\n $referrer: getReferrer(),\n };\n}\n","import { detectEnvironment } from \"./environment\";\nimport { buildEventContext } from \"./eventContext\";\n\n// ── Types ──────────────────────────────────────────────────────────────────\n\nexport interface DecisionResponse {\n variant_key: string;\n}\n\nexport interface MetricPayload {\n event: string;\n environment: \"dev\" | \"prod\";\n properties: Record;\n}\n\n// ── Assignment fetching ────────────────────────────────────────────────────\n\nconst pendingDecisions = new Map>();\n\n/**\n * Fetch the variant assignment for an experiment.\n * Returns the variant key string (e.g. \"control\", \"ai_v1\").\n * Deduplicates concurrent calls for the same experiment.\n */\nexport async function fetchDecision(\n host: string,\n experimentId: string,\n distinctId: string\n): Promise {\n const existing = pendingDecisions.get(experimentId);\n if (existing) return existing;\n\n const promise = (async () => {\n try {\n const url = `${host.replace(/\\/$/, \"\")}/experiment/decide`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n },\n credentials: \"include\",\n body: JSON.stringify({\n experiment_id: experimentId,\n distinct_id: distinctId,\n }),\n });\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n const data = (await res.json()) as DecisionResponse;\n return data.variant_key || \"control\";\n } finally {\n pendingDecisions.delete(experimentId);\n }\n })();\n\n pendingDecisions.set(experimentId, promise);\n return promise;\n}\n\n// ── Metric sending ─────────────────────────────────────────────────────────\n\n/**\n * Fire-and-forget metric send. Never throws.\n */\nexport function sendMetric(\n host: string,\n event: string,\n properties: Record\n): void {\n if (typeof window === \"undefined\") return;\n\n const ctx = buildEventContext();\n const payload: MetricPayload = {\n event,\n environment: detectEnvironment(),\n properties: {\n ...ctx,\n source: \"react-sdk\",\n captured_at: new Date().toISOString(),\n ...properties,\n },\n };\n\n try {\n const url = `${host.replace(/\\/$/, \"\")}/experiment/metrics`;\n fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n credentials: \"include\",\n body: JSON.stringify(payload),\n }).catch(() => {});\n } catch {\n // silently drop\n }\n}\n\n// ── Click metadata extraction ──────────────────────────────────────────────\n\nexport interface ClickMeta {\n click_target_tag: string;\n click_target_text?: string;\n click_target_id?: string;\n click_is_primary: boolean;\n}\n\n/**\n * Given a click event inside an experiment boundary, extract metadata.\n * Prioritizes elements with data-probat-click=\"primary\",\n * then falls back to button/a/role=button.\n */\nexport function extractClickMeta(target: EventTarget | null): ClickMeta | null {\n if (!target || !(target instanceof HTMLElement)) return null;\n\n // Priority 1: explicit primary marker\n const primary = target.closest('[data-probat-click=\"primary\"]');\n if (primary) return buildMeta(primary as HTMLElement, true);\n\n // Priority 2: interactive elements\n const interactive = target.closest('button, a, [role=\"button\"]');\n if (interactive) return buildMeta(interactive as HTMLElement, false);\n\n return null;\n}\n\nfunction buildMeta(el: HTMLElement, isPrimary: boolean): ClickMeta {\n const meta: ClickMeta = {\n click_target_tag: el.tagName,\n click_is_primary: isPrimary,\n };\n if (el.id) meta.click_target_id = el.id;\n const text = el.textContent?.trim();\n if (text) meta.click_target_text = text.slice(0, 120);\n return meta;\n}\n","/**\n * Dedupe storage for experiment exposures.\n * Uses sessionStorage + in-memory Set fallback.\n * Key format: probat:seen:{id}:{variantKey}:{instanceId}:{pageKey}\n */\n\nconst PREFIX = \"probat:seen:\";\nconst memorySet = new Set();\n\nexport function makeDedupeKey(\n experimentId: string,\n variantKey: string,\n instanceId: string,\n pageKey: string\n): string {\n return `${PREFIX}${experimentId}:${variantKey}:${instanceId}:${pageKey}`;\n}\n\nexport function hasSeen(key: string): boolean {\n if (memorySet.has(key)) return true;\n if (typeof window === \"undefined\") return false;\n try {\n return sessionStorage.getItem(key) === \"1\";\n } catch {\n return false;\n }\n}\n\nexport function markSeen(key: string): void {\n memorySet.add(key);\n if (typeof window === \"undefined\") return;\n try {\n sessionStorage.setItem(key, \"1\");\n } catch {}\n}\n\n/** Reset all dedupe state — useful for testing. */\nexport function resetDedupe(): void {\n memorySet.clear();\n}\n","/**\n * Stable auto-generated instance IDs for .\n *\n * Problem: a naïve useRef + module counter gives a different ID on every mount,\n * so StrictMode double-mount or unmount/remount changes the dedupe key.\n *\n * Solution:\n * 1. React 18+ → useId() is stable per fiber position.\n * 2. Fallback → sessionStorage-backed slot counter per (experimentId, pageKey).\n * 3. Both paths persist a mapping in sessionStorage:\n * probat:instance:{experimentId}:{pageKey}:{positionKey} → stableId\n * so the same position resolves to the same ID across mounts.\n */\n\nimport React, { useRef } from \"react\";\nimport { getPageKey } from \"./eventContext\";\n\nconst INSTANCE_PREFIX = \"probat:instance:\";\n\n// ── Helpers ────────────────────────────────────────────────────────────────\n\nfunction shortId(): string {\n const bytes = new Uint8Array(4);\n if (typeof crypto !== \"undefined\" && crypto.getRandomValues) {\n crypto.getRandomValues(bytes);\n } else {\n for (let i = 0; i < 4; i++) bytes[i] = Math.floor(Math.random() * 256);\n }\n return Array.from(bytes, (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n}\n\n/**\n * Look up or create a stable instance ID in sessionStorage.\n */\nfunction resolveStableId(storageKey: string): string {\n if (typeof window !== \"undefined\") {\n try {\n const stored = sessionStorage.getItem(storageKey);\n if (stored) return stored;\n } catch {}\n }\n const id = `inst_${shortId()}`;\n if (typeof window !== \"undefined\") {\n try {\n sessionStorage.setItem(storageKey, id);\n } catch {}\n }\n return id;\n}\n\n// ── Fallback: render-wave slot counter ─────────────────────────────────────\n// Each synchronous render batch claims sequential slots per (experimentId,\n// pageKey). A microtask resets the counters so the next batch starts at 0,\n// giving the same component position the same slot across mounts.\n\nconst slotCounters = new Map();\nlet resetScheduled = false;\n\nfunction claimSlot(groupKey: string): number {\n const idx = slotCounters.get(groupKey) ?? 0;\n slotCounters.set(groupKey, idx + 1);\n if (!resetScheduled) {\n resetScheduled = true;\n Promise.resolve().then(() => {\n slotCounters.clear();\n resetScheduled = false;\n });\n }\n return idx;\n}\n\n// ── Hook: React 18+ path (useId available) ─────────────────────────────────\n\nfunction useStableInstanceIdV18(experimentId: string): string {\n const reactId = (React as any).useId() as string;\n const ref = useRef(\"\");\n if (!ref.current) {\n const key = `${INSTANCE_PREFIX}${experimentId}:${getPageKey()}:${reactId}`;\n ref.current = resolveStableId(key);\n }\n return ref.current;\n}\n\n// ── Hook: fallback path (no useId) ─────────────────────────────────────────\n\nfunction useStableInstanceIdFallback(experimentId: string): string {\n const slotRef = useRef(-1);\n const ref = useRef(\"\");\n if (slotRef.current === -1) {\n slotRef.current = claimSlot(`${experimentId}:${getPageKey()}`);\n }\n if (!ref.current) {\n const key = `${INSTANCE_PREFIX}${experimentId}:${getPageKey()}:${slotRef.current}`;\n ref.current = resolveStableId(key);\n }\n return ref.current;\n}\n\n// ── Exported hook ──────────────────────────────────────────────────────────\n// Selection is a module-level constant so the hook-call count never changes\n// between renders — safe for the rules of hooks.\n\nexport const useStableInstanceId: (experimentId: string) => string =\n typeof (React as any).useId === \"function\"\n ? useStableInstanceIdV18\n : useStableInstanceIdFallback;\n\n// ── Test utility ───────────────────────────────────────────────────────────\n\nexport function resetInstanceIdState(): void {\n slotCounters.clear();\n resetScheduled = false;\n}\n","\"use client\";\n\nimport React, {\n useEffect,\n useRef,\n useState,\n useCallback,\n useMemo,\n} from \"react\";\nimport { useProbatContext } from \"../context/ProbatContext\";\nimport { fetchDecision, sendMetric, extractClickMeta } from \"../utils/api\";\nimport { getDistinctId, getPageKey } from \"../utils/eventContext\";\nimport { makeDedupeKey, hasSeen, markSeen } from \"../utils/dedupeStorage\";\nimport { useStableInstanceId } from \"../utils/stableInstanceId\";\n\n// ── localStorage assignment cache ──────────────────────────────────────────\n\nconst ASSIGNMENT_PREFIX = \"probat:assignment:\";\n\ninterface StoredAssignment {\n variantKey: string;\n ts: number;\n}\n\nfunction readAssignment(id: string): string | null {\n if (typeof window === \"undefined\") return null;\n try {\n const raw = localStorage.getItem(ASSIGNMENT_PREFIX + id);\n if (!raw) return null;\n const parsed: StoredAssignment = JSON.parse(raw);\n return parsed.variantKey ?? null;\n } catch {\n return null;\n }\n}\n\nfunction writeAssignment(id: string, variantKey: string): void {\n if (typeof window === \"undefined\") return;\n try {\n const entry: StoredAssignment = { variantKey, ts: Date.now() };\n localStorage.setItem(ASSIGNMENT_PREFIX + id, JSON.stringify(entry));\n } catch {}\n}\n\n// ── Types ──────────────────────────────────────────────────────────────────\n\nexport interface ExperimentTrackOptions {\n /** Auto-track impressions (default true) */\n impression?: boolean;\n /** Auto-track clicks (default true) */\n primaryClick?: boolean;\n /** Custom impression event name (default \"$experiment_exposure\") */\n impressionEventName?: string;\n /** Custom click event name (default \"$experiment_click\") */\n clickEventName?: string;\n}\n\nexport interface ExperimentProps {\n /** Experiment key / identifier */\n id: string;\n /** Control variant ReactNode */\n control: React.ReactNode;\n /** Named variant ReactNodes, keyed by variant key (e.g. { ai_v1: }) */\n variants: Record;\n /** Tracking configuration */\n track?: ExperimentTrackOptions;\n /** Stable instance id when multiple instances of the same experiment exist on a page */\n componentInstanceId?: string;\n /** Behavior when assignment fetch fails: \"control\" (default) renders control, \"suspend\" throws */\n fallback?: \"control\" | \"suspend\";\n /** Log decisions + events to console */\n debug?: boolean;\n}\n\nexport function Experiment({\n id,\n control,\n variants,\n track,\n componentInstanceId,\n fallback = \"control\",\n debug = false,\n}: ExperimentProps) {\n const { host, bootstrap } = useProbatContext();\n\n // Stable instance id (useId + sessionStorage for cross-mount stability)\n const autoInstanceId = useStableInstanceId(id);\n const instanceId = componentInstanceId ?? autoInstanceId;\n\n // Track options with defaults\n const trackImpression = track?.impression !== false;\n const trackClick = track?.primaryClick !== false;\n const impressionEvent = track?.impressionEventName ?? \"$experiment_exposure\";\n const clickEvent = track?.clickEventName ?? \"$experiment_click\";\n\n // ── Assignment resolution ──────────────────────────────────────────────\n\n const [variantKey, setVariantKey] = useState(() => {\n // Sync resolution order: bootstrap → localStorage → \"control\"\n if (bootstrap[id]) return bootstrap[id];\n const cached = readAssignment(id);\n if (cached) return cached;\n return \"control\";\n });\n const [resolved, setResolved] = useState(() => {\n return !!(bootstrap[id] || readAssignment(id));\n });\n\n useEffect(() => {\n // Already resolved from bootstrap or cache\n if (bootstrap[id] || readAssignment(id)) {\n // Ensure state is synced (StrictMode may re-mount)\n const key = bootstrap[id] ?? readAssignment(id) ?? \"control\";\n setVariantKey(key);\n setResolved(true);\n return;\n }\n\n let cancelled = false;\n\n (async () => {\n try {\n const distinctId = getDistinctId();\n const key = await fetchDecision(host, id, distinctId);\n if (cancelled) return;\n\n // Validate variant key\n if (key !== \"control\" && !(key in variants)) {\n if (debug) {\n console.warn(\n `[probat] Unknown variant \"${key}\" for experiment \"${id}\", falling back to control`\n );\n }\n setVariantKey(\"control\");\n } else {\n setVariantKey(key);\n writeAssignment(id, key);\n }\n } catch (err) {\n if (cancelled) return;\n if (debug) {\n console.error(`[probat] fetchDecision failed for \"${id}\":`, err);\n }\n if (fallback === \"suspend\") throw err;\n setVariantKey(\"control\");\n } finally {\n if (!cancelled) setResolved(true);\n }\n })();\n\n return () => {\n cancelled = true;\n };\n }, [id, host]); // eslint-disable-line react-hooks/exhaustive-deps\n\n // ── Debug logging ──────────────────────────────────────────────────────\n\n useEffect(() => {\n if (debug && resolved) {\n console.log(`[probat] Experiment \"${id}\" → variant \"${variantKey}\"`, {\n instanceId,\n pageKey: getPageKey(),\n });\n }\n }, [debug, id, variantKey, resolved, instanceId]);\n\n // ── Shared event properties ────────────────────────────────────────────\n\n const eventProps = useMemo(\n () => ({\n experiment_id: id,\n variant_key: variantKey,\n component_instance_id: instanceId,\n }),\n [id, variantKey, instanceId]\n );\n\n // ── Impression tracking via IntersectionObserver ────────────────────────\n\n const containerRef = useRef(null);\n const impressionSent = useRef(false);\n\n useEffect(() => {\n if (!trackImpression || !resolved) return;\n\n // Reset on re-mount (StrictMode safety)\n impressionSent.current = false;\n\n const pageKey = getPageKey();\n const dedupeKey = makeDedupeKey(id, variantKey, instanceId, pageKey);\n\n // Already seen this session\n if (hasSeen(dedupeKey)) {\n impressionSent.current = true;\n return;\n }\n\n const el = containerRef.current;\n if (!el) return;\n\n // Fallback: no IntersectionObserver (SSR, old browser)\n if (typeof IntersectionObserver === \"undefined\") {\n if (!impressionSent.current) {\n impressionSent.current = true;\n markSeen(dedupeKey);\n sendMetric(host, impressionEvent, eventProps);\n if (debug) console.log(`[probat] Impression sent (no IO) for \"${id}\"`);\n }\n return;\n }\n\n let timer: ReturnType | null = null;\n\n const observer = new IntersectionObserver(\n ([entry]) => {\n if (!entry || impressionSent.current) return;\n\n if (entry.isIntersecting) {\n timer = setTimeout(() => {\n if (impressionSent.current) return;\n impressionSent.current = true;\n markSeen(dedupeKey);\n sendMetric(host, impressionEvent, eventProps);\n if (debug) console.log(`[probat] Impression sent for \"${id}\"`);\n observer.disconnect();\n }, 250);\n } else if (timer) {\n clearTimeout(timer);\n timer = null;\n }\n },\n { threshold: 0.5 }\n );\n\n observer.observe(el);\n\n return () => {\n observer.disconnect();\n if (timer) clearTimeout(timer);\n };\n }, [\n trackImpression,\n resolved,\n id,\n variantKey,\n instanceId,\n host,\n impressionEvent,\n eventProps,\n debug,\n ]);\n\n // ── Click tracking ─────────────────────────────────────────────────────\n\n const handleClick = useCallback(\n (e: React.MouseEvent) => {\n if (!trackClick) return;\n\n const meta = extractClickMeta(e.target as EventTarget);\n if (!meta) return;\n\n sendMetric(host, clickEvent, {\n ...eventProps,\n ...meta,\n });\n if (debug) {\n console.log(`[probat] Click tracked for \"${id}\"`, meta);\n }\n },\n [trackClick, host, clickEvent, eventProps, id, debug]\n );\n\n // ── Render ─────────────────────────────────────────────────────────────\n\n const content =\n variantKey === \"control\" || !(variantKey in variants)\n ? control\n : variants[variantKey];\n\n return (\n \n {content}\n
\n );\n}\n","\"use client\";\n\nimport { useCallback } from \"react\";\nimport { useProbatContext } from \"../context/ProbatContext\";\nimport { sendMetric } from \"../utils/api\";\n\nexport interface UseProbatMetricsReturn {\n /**\n * Send a custom event with arbitrary properties.\n * Never throws — failures are silently dropped.\n *\n * @example\n * ```tsx\n * const { capture } = useProbatMetrics();\n * capture(\"purchase\", { revenue: 42, currency: \"USD\" });\n * ```\n */\n capture: (event: string, properties?: Record) => void;\n}\n\n/**\n * Minimal metrics hook. Provides a single `capture(event, props)` function\n * that sends events to the Probat backend using the provider's host config.\n */\nexport function useProbatMetrics(): UseProbatMetricsReturn {\n const { host } = useProbatContext();\n\n const capture = useCallback(\n (event: string, properties: Record = {}) => {\n sendMetric(host, event, properties);\n },\n [host]\n );\n\n return { capture };\n}\n"]}
\ No newline at end of file
diff --git a/node_modules/@probat/react/package.json b/node_modules/@probat/react/package.json
new file mode 100644
index 00000000..e2e40916
--- /dev/null
+++ b/node_modules/@probat/react/package.json
@@ -0,0 +1,56 @@
+{
+ "name": "@probat/react",
+ "version": "0.3.2",
+ "description": "React SDK for Probat experimentation — CTR tracking, variant assignment, and more",
+ "main": "./dist/index.js",
+ "module": "./dist/index.mjs",
+ "types": "./dist/index.d.ts",
+ "exports": {
+ ".": {
+ "types": "./dist/index.d.ts",
+ "react-server": "./dist/index.mjs",
+ "import": "./dist/index.mjs",
+ "require": "./dist/index.js",
+ "default": "./dist/index.mjs"
+ }
+ },
+ "files": [
+ "dist",
+ "src",
+ "README.md"
+ ],
+ "scripts": {
+ "build": "tsup && npm run postbuild",
+ "postbuild": "echo '\"use client\";' | cat - dist/index.mjs > temp && mv temp dist/index.mjs && echo '\"use client\";' | cat - dist/index.js > temp && mv temp dist/index.js",
+ "dev": "tsup --watch",
+ "test": "vitest run",
+ "test:watch": "vitest",
+ "typecheck": "tsc --noEmit",
+ "prepublishOnly": "npm run build"
+ },
+ "keywords": [
+ "probat",
+ "ab-testing",
+ "experimentation",
+ "react",
+ "ctr"
+ ],
+ "sideEffects": false,
+ "author": "Probat AI",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": ">=18.0.0"
+ },
+ "devDependencies": {
+ "@testing-library/jest-dom": "^6.6.3",
+ "@testing-library/react": "^16.1.0",
+ "@types/react": "^18.2.0",
+ "@types/react-dom": "^18.2.0",
+ "jsdom": "^25.0.1",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "tsup": "^8.0.0",
+ "typescript": "^5.0.0",
+ "vitest": "^2.1.8"
+ }
+}
diff --git a/node_modules/@probat/react/src/__tests__/Experiment.test.tsx b/node_modules/@probat/react/src/__tests__/Experiment.test.tsx
new file mode 100644
index 00000000..d7ef447e
--- /dev/null
+++ b/node_modules/@probat/react/src/__tests__/Experiment.test.tsx
@@ -0,0 +1,764 @@
+import React, { StrictMode } from "react";
+import { render, screen, fireEvent, act } from "@testing-library/react";
+import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
+import { ProbatProviderClient } from "../components/ProbatProviderClient";
+import { Experiment } from "../components/Experiment";
+import { resetDedupe } from "../utils/dedupeStorage";
+import { resetInstanceIdState } from "../utils/stableInstanceId";
+import { MockIntersectionObserver } from "./setup";
+
+// ── Helpers ────────────────────────────────────────────────────────────────
+
+function wrapper({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ );
+}
+
+function wrapperWithBootstrap(bootstrap: Record) {
+ return function BootstrapWrapper({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ );
+ };
+}
+
+// ── Setup / teardown ───────────────────────────────────────────────────────
+
+beforeEach(() => {
+ vi.useFakeTimers();
+ localStorage.clear();
+ sessionStorage.clear();
+ resetDedupe();
+ resetInstanceIdState();
+ MockIntersectionObserver._reset();
+ vi.restoreAllMocks();
+ global.fetch = vi.fn();
+});
+
+afterEach(() => {
+ vi.useRealTimers();
+});
+
+// ── Assignment caching ─────────────────────────────────────────────────────
+
+describe("assignment caching", () => {
+ it("caches assignment in localStorage after fetch", async () => {
+ (global.fetch as ReturnType).mockResolvedValueOnce({
+ ok: true,
+ json: () => Promise.resolve({ variant_key: "ai_v1" }),
+ });
+
+ render(
+ Control}
+ variants={{ ai_v1: Variant AI
}}
+ />,
+ { wrapper }
+ );
+
+ // Initially renders control (sync)
+ expect(screen.getByText("Control")).toBeInTheDocument();
+
+ // Flush fetch
+ await act(async () => {
+ await vi.runAllTimersAsync();
+ });
+
+ expect(screen.getByText("Variant AI")).toBeInTheDocument();
+
+ // Verify localStorage
+ const stored = localStorage.getItem("probat:assignment:test-exp");
+ expect(stored).toBeTruthy();
+ const parsed = JSON.parse(stored!);
+ expect(parsed.variantKey).toBe("ai_v1");
+ });
+
+ it("reads cached assignment without fetching", async () => {
+ localStorage.setItem(
+ "probat:assignment:cached-exp",
+ JSON.stringify({ variantKey: "ai_v1", ts: Date.now() })
+ );
+
+ render(
+ Control}
+ variants={{ ai_v1: Cached Variant
}}
+ />,
+ { wrapper }
+ );
+
+ // Should render variant immediately from cache
+ expect(screen.getByText("Cached Variant")).toBeInTheDocument();
+ expect(global.fetch).not.toHaveBeenCalled();
+ });
+
+ it("uses bootstrap over localStorage", () => {
+ localStorage.setItem(
+ "probat:assignment:boot-exp",
+ JSON.stringify({ variantKey: "old_variant", ts: Date.now() })
+ );
+
+ render(
+ Control}
+ variants={{ bootstrapped: Bootstrapped
, old_variant: Old
}}
+ />,
+ { wrapper: wrapperWithBootstrap({ "boot-exp": "bootstrapped" }) }
+ );
+
+ expect(screen.getByText("Bootstrapped")).toBeInTheDocument();
+ });
+
+ it("falls back to control on fetch failure", async () => {
+ (global.fetch as ReturnType).mockRejectedValueOnce(
+ new Error("Network error")
+ );
+
+ render(
+ Fallback Control}
+ variants={{ ai_v1: Variant
}}
+ />,
+ { wrapper }
+ );
+
+ await act(async () => {
+ await vi.runAllTimersAsync();
+ });
+
+ expect(screen.getByText("Fallback Control")).toBeInTheDocument();
+ // Should NOT persist fallback
+ expect(localStorage.getItem("probat:assignment:fail-exp")).toBeNull();
+ });
+
+ it("falls back to control for unknown variant key", async () => {
+ (global.fetch as ReturnType).mockResolvedValueOnce({
+ ok: true,
+ json: () => Promise.resolve({ variant_key: "nonexistent_variant" }),
+ });
+
+ render(
+ Control}
+ variants={{ ai_v1: Variant
}}
+ debug
+ />,
+ { wrapper }
+ );
+
+ await act(async () => {
+ await vi.runAllTimersAsync();
+ });
+
+ expect(screen.getByText("Control")).toBeInTheDocument();
+ });
+});
+
+// ── Dedupe key behavior ────────────────────────────────────────────────────
+
+describe("dedupe", () => {
+ it("does not double-send impressions for same experiment on same page", async () => {
+ const fetchMock = global.fetch as ReturnType;
+ // First call: decide endpoint, second+ calls: metrics
+ fetchMock.mockResolvedValue({
+ ok: true,
+ json: () => Promise.resolve({ variant_key: "control" }),
+ });
+
+ // Use explicit componentInstanceId so dedupe key is stable across mounts
+ const { unmount } = render(
+ Control}
+ variants={{}}
+ componentInstanceId="stable-inst"
+ />,
+ { wrapper }
+ );
+
+ await act(async () => {
+ await vi.runAllTimersAsync();
+ });
+
+ // Trigger intersection
+ const observer = MockIntersectionObserver._instances[0];
+ if (observer) {
+ act(() => observer._trigger(true));
+ await act(async () => {
+ await vi.advanceTimersByTimeAsync(300);
+ });
+ }
+
+ const metricCalls1 = fetchMock.mock.calls.filter(
+ (c: any[]) => typeof c[0] === "string" && c[0].includes("/experiment/metrics")
+ );
+
+ // Unmount and remount with same instanceId
+ unmount();
+
+ render(
+ Control}
+ variants={{}}
+ componentInstanceId="stable-inst"
+ />,
+ { wrapper }
+ );
+
+ await act(async () => {
+ await vi.runAllTimersAsync();
+ });
+
+ const observer2 = MockIntersectionObserver._instances[MockIntersectionObserver._instances.length - 1];
+ if (observer2) {
+ act(() => observer2._trigger(true));
+ await act(async () => {
+ await vi.advanceTimersByTimeAsync(300);
+ });
+ }
+
+ const metricCalls2 = fetchMock.mock.calls.filter(
+ (c: any[]) => typeof c[0] === "string" && c[0].includes("/experiment/metrics")
+ );
+
+ // Should only have 1 impression total (deduped on second mount)
+ expect(metricCalls2.length).toBe(metricCalls1.length);
+ });
+});
+
+// ── Click capture rules ────────────────────────────────────────────────────
+
+describe("click tracking", () => {
+ beforeEach(() => {
+ localStorage.setItem(
+ "probat:assignment:click-exp",
+ JSON.stringify({ variantKey: "control", ts: Date.now() })
+ );
+ });
+
+ it("captures clicks on data-probat-click='primary'", async () => {
+ const fetchMock = global.fetch as ReturnType;
+ fetchMock.mockResolvedValue({ ok: true });
+
+ render(
+
+
+ Primary CTA
+
+
+ }
+ variants={{}}
+ />,
+ { wrapper }
+ );
+
+ await act(async () => {
+ await vi.runAllTimersAsync();
+ });
+
+ fireEvent.click(screen.getByTestId("primary-el"));
+
+ await act(async () => {
+ await vi.runAllTimersAsync();
+ });
+
+ const clickCalls = fetchMock.mock.calls.filter((c: any[]) => {
+ if (typeof c[0] !== "string" || !c[0].includes("/experiment/metrics")) return false;
+ const body = JSON.parse(c[1]?.body || "{}");
+ return body.event === "$experiment_click";
+ });
+
+ expect(clickCalls.length).toBe(1);
+ const payload = JSON.parse(clickCalls[0][1].body);
+ expect(payload.properties.click_is_primary).toBe(true);
+ expect(payload.properties.click_target_text).toBe("Primary CTA");
+ });
+
+ it("captures clicks on }
+ variants={{}}
+ />,
+ { wrapper }
+ );
+
+ await act(async () => {
+ await vi.runAllTimersAsync();
+ });
+
+ fireEvent.click(screen.getByTestId("btn"));
+
+ await act(async () => {
+ await vi.runAllTimersAsync();
+ });
+
+ const clickCalls = fetchMock.mock.calls.filter((c: any[]) => {
+ if (typeof c[0] !== "string" || !c[0].includes("/experiment/metrics")) return false;
+ const body = JSON.parse(c[1]?.body || "{}");
+ return body.event === "$experiment_click";
+ });
+
+ expect(clickCalls.length).toBe(1);
+ const payload = JSON.parse(clickCalls[0][1].body);
+ expect(payload.properties.click_target_tag).toBe("BUTTON");
+ expect(payload.properties.click_is_primary).toBe(false);
+ });
+
+ it("captures clicks on ", async () => {
+ const fetchMock = global.fetch as ReturnType;
+ fetchMock.mockResolvedValue({ ok: true });
+
+ render(
+ Link}
+ variants={{}}
+ />,
+ { wrapper }
+ );
+
+ await act(async () => {
+ await vi.runAllTimersAsync();
+ });
+
+ fireEvent.click(screen.getByTestId("link"));
+
+ await act(async () => {
+ await vi.runAllTimersAsync();
+ });
+
+ const clickCalls = fetchMock.mock.calls.filter((c: any[]) => {
+ if (typeof c[0] !== "string" || !c[0].includes("/experiment/metrics")) return false;
+ const body = JSON.parse(c[1]?.body || "{}");
+ return body.event === "$experiment_click";
+ });
+
+ expect(clickCalls.length).toBe(1);
+ });
+
+ it("captures clicks on role='button'", async () => {
+ const fetchMock = global.fetch as ReturnType;
+ fetchMock.mockResolvedValue({ ok: true });
+
+ render(
+ Role Btn}
+ variants={{}}
+ />,
+ { wrapper }
+ );
+
+ await act(async () => {
+ await vi.runAllTimersAsync();
+ });
+
+ fireEvent.click(screen.getByTestId("role-btn"));
+
+ await act(async () => {
+ await vi.runAllTimersAsync();
+ });
+
+ const clickCalls = fetchMock.mock.calls.filter((c: any[]) => {
+ if (typeof c[0] !== "string" || !c[0].includes("/experiment/metrics")) return false;
+ const body = JSON.parse(c[1]?.body || "{}");
+ return body.event === "$experiment_click";
+ });
+
+ expect(clickCalls.length).toBe(1);
+ });
+
+ it("ignores clicks on non-interactive elements", async () => {
+ const fetchMock = global.fetch as ReturnType;
+ fetchMock.mockResolvedValue({ ok: true });
+
+ render(
+ Just text
}
+ variants={{}}
+ />,
+ { wrapper }
+ );
+
+ await act(async () => {
+ await vi.runAllTimersAsync();
+ });
+
+ fireEvent.click(screen.getByTestId("paragraph"));
+
+ await act(async () => {
+ await vi.runAllTimersAsync();
+ });
+
+ const clickCalls = fetchMock.mock.calls.filter((c: any[]) => {
+ if (typeof c[0] !== "string" || !c[0].includes("/experiment/metrics")) return false;
+ const body = JSON.parse(c[1]?.body || "{}");
+ return body.event === "$experiment_click";
+ });
+
+ expect(clickCalls.length).toBe(0);
+ });
+});
+
+// ── Impression visibility (IntersectionObserver + 250ms) ───────────────────
+
+describe("impression tracking", () => {
+ beforeEach(() => {
+ localStorage.setItem(
+ "probat:assignment:vis-exp",
+ JSON.stringify({ variantKey: "control", ts: Date.now() })
+ );
+ });
+
+ it("sends impression after >=250ms of 50% visibility", async () => {
+ const fetchMock = global.fetch as ReturnType;
+ fetchMock.mockResolvedValue({ ok: true });
+
+ render(
+ Visible}
+ variants={{}}
+ />,
+ { wrapper }
+ );
+
+ await act(async () => {
+ await vi.runAllTimersAsync();
+ });
+
+ const observer = MockIntersectionObserver._instances[MockIntersectionObserver._instances.length - 1];
+ expect(observer).toBeDefined();
+
+ // Trigger visible
+ act(() => observer._trigger(true));
+
+ // Before 250ms — should NOT have sent
+ await act(async () => {
+ await vi.advanceTimersByTimeAsync(200);
+ });
+
+ let impressionCalls = fetchMock.mock.calls.filter((c: any[]) => {
+ if (typeof c[0] !== "string" || !c[0].includes("/experiment/metrics")) return false;
+ const body = JSON.parse(c[1]?.body || "{}");
+ return body.event === "$experiment_exposure";
+ });
+ expect(impressionCalls.length).toBe(0);
+
+ // After 250ms total — should have sent
+ await act(async () => {
+ await vi.advanceTimersByTimeAsync(100);
+ });
+
+ impressionCalls = fetchMock.mock.calls.filter((c: any[]) => {
+ if (typeof c[0] !== "string" || !c[0].includes("/experiment/metrics")) return false;
+ const body = JSON.parse(c[1]?.body || "{}");
+ return body.event === "$experiment_exposure";
+ });
+ expect(impressionCalls.length).toBe(1);
+
+ const payload = JSON.parse(impressionCalls[0][1].body);
+ expect(payload.properties.experiment_id).toBe("vis-exp");
+ expect(payload.properties.variant_key).toBe("control");
+ });
+
+ it("cancels impression if element leaves viewport before 250ms", async () => {
+ const fetchMock = global.fetch as ReturnType;
+ fetchMock.mockResolvedValue({ ok: true });
+
+ render(
+ Short Visit}
+ variants={{}}
+ />,
+ { wrapper }
+ );
+
+ await act(async () => {
+ await vi.runAllTimersAsync();
+ });
+
+ const observer = MockIntersectionObserver._instances[MockIntersectionObserver._instances.length - 1];
+
+ // Enter viewport
+ act(() => observer._trigger(true));
+ await act(async () => {
+ await vi.advanceTimersByTimeAsync(100);
+ });
+
+ // Leave viewport before 250ms
+ act(() => observer._trigger(false));
+ await act(async () => {
+ await vi.advanceTimersByTimeAsync(300);
+ });
+
+ const impressionCalls = fetchMock.mock.calls.filter((c: any[]) => {
+ if (typeof c[0] !== "string" || !c[0].includes("/experiment/metrics")) return false;
+ const body = JSON.parse(c[1]?.body || "{}");
+ return body.event === "$experiment_exposure";
+ });
+ expect(impressionCalls.length).toBe(0);
+ });
+});
+
+// ── StrictMode double-mount safety ─────────────────────────────────────────
+
+describe("StrictMode safety", () => {
+ it("does not double-count impressions in StrictMode (no componentInstanceId)", async () => {
+ const fetchMock = global.fetch as ReturnType;
+ fetchMock.mockResolvedValue({ ok: true });
+
+ localStorage.setItem(
+ "probat:assignment:strict-exp",
+ JSON.stringify({ variantKey: "control", ts: Date.now() })
+ );
+
+ render(
+
+
+ Strict Control}
+ variants={{}}
+ />
+
+
+ );
+
+ await act(async () => {
+ await vi.runAllTimersAsync();
+ });
+
+ // Trigger ALL observers — the dedupe key must be identical across the
+ // StrictMode double-mount so that even if both observers fire only one
+ // impression is sent. Earlier observers have empty element sets (cleanup
+ // called disconnect()) so _trigger produces an empty entries array which
+ // the component guards with `if (!entry)`.
+ for (const obs of MockIntersectionObserver._instances) {
+ act(() => obs._trigger(true));
+ }
+
+ await act(async () => {
+ await vi.advanceTimersByTimeAsync(300);
+ });
+
+ const impressionCalls = fetchMock.mock.calls.filter((c: any[]) => {
+ if (typeof c[0] !== "string" || !c[0].includes("/experiment/metrics")) return false;
+ const body = JSON.parse(c[1]?.body || "{}");
+ return body.event === "$experiment_exposure";
+ });
+
+ // Should be exactly 1, not 2
+ expect(impressionCalls.length).toBe(1);
+ });
+
+ it("auto instanceId is stable across StrictMode double-mount", async () => {
+ const fetchMock = global.fetch as ReturnType;
+ fetchMock.mockResolvedValue({ ok: true });
+
+ localStorage.setItem(
+ "probat:assignment:stable-id-exp",
+ JSON.stringify({ variantKey: "control", ts: Date.now() })
+ );
+
+ render(
+
+
+ Buy}
+ variants={{}}
+ />
+
+
+ );
+
+ await act(async () => {
+ await vi.runAllTimersAsync();
+ });
+
+ // Fire a click so we get a metric payload with the auto-generated instanceId
+ fireEvent.click(screen.getByText("Buy"));
+
+ await act(async () => {
+ await vi.runAllTimersAsync();
+ });
+
+ // Now trigger impression via the active observer
+ const lastObs = MockIntersectionObserver._instances[MockIntersectionObserver._instances.length - 1];
+ act(() => lastObs._trigger(true));
+ await act(async () => {
+ await vi.advanceTimersByTimeAsync(300);
+ });
+
+ // Collect all metric payloads
+ const metricPayloads = fetchMock.mock.calls
+ .filter((c: any[]) => typeof c[0] === "string" && c[0].includes("/experiment/metrics"))
+ .map((c: any[]) => JSON.parse(c[1].body));
+
+ // Should have at least click + impression
+ expect(metricPayloads.length).toBeGreaterThanOrEqual(2);
+
+ // Every payload must share the same component_instance_id
+ const instanceIds = new Set(
+ metricPayloads.map((p: any) => p.properties.component_instance_id)
+ );
+ expect(instanceIds.size).toBe(1);
+
+ // The id should have the inst_ prefix (auto-generated, not undefined)
+ const theId = [...instanceIds][0] as string;
+ expect(theId).toMatch(/^inst_/);
+ });
+
+ it("persists auto instanceId mapping in sessionStorage", () => {
+ (global.fetch as ReturnType).mockResolvedValue({ ok: true });
+
+ localStorage.setItem(
+ "probat:assignment:persist-exp",
+ JSON.stringify({ variantKey: "control", ts: Date.now() })
+ );
+
+ render(
+ Control}
+ variants={{}}
+ />,
+ { wrapper }
+ );
+
+ // Check that a sessionStorage key was written for the instance mapping
+ const keys = Object.keys(sessionStorage);
+ const instanceKey = keys.find((k) => k.startsWith("probat:instance:persist-exp:"));
+ expect(instanceKey).toBeDefined();
+
+ // The stored value should be the stable inst_ id
+ const storedId = sessionStorage.getItem(instanceKey!);
+ expect(storedId).toMatch(/^inst_/);
+ });
+});
+
+// ── Event payload fields ───────────────────────────────────────────────────
+
+describe("event payload", () => {
+ it("includes all required fields in exposure event", async () => {
+ const fetchMock = global.fetch as ReturnType;
+ fetchMock.mockResolvedValue({ ok: true });
+
+ localStorage.setItem(
+ "probat:assignment:payload-exp",
+ JSON.stringify({ variantKey: "ai_v1", ts: Date.now() })
+ );
+
+ render(
+ Control}
+ variants={{ ai_v1: AI Variant
}}
+ componentInstanceId="hero-cta"
+ />,
+ { wrapper }
+ );
+
+ await act(async () => {
+ await vi.runAllTimersAsync();
+ });
+
+ const observer = MockIntersectionObserver._instances[MockIntersectionObserver._instances.length - 1];
+ act(() => observer._trigger(true));
+ await act(async () => {
+ await vi.advanceTimersByTimeAsync(300);
+ });
+
+ const impressionCalls = fetchMock.mock.calls.filter((c: any[]) => {
+ if (typeof c[0] !== "string" || !c[0].includes("/experiment/metrics")) return false;
+ const body = JSON.parse(c[1]?.body || "{}");
+ return body.event === "$experiment_exposure";
+ });
+
+ expect(impressionCalls.length).toBe(1);
+ const payload = JSON.parse(impressionCalls[0][1].body);
+
+ expect(payload.event).toBe("$experiment_exposure");
+ expect(payload.properties.experiment_id).toBe("payload-exp");
+ expect(payload.properties.variant_key).toBe("ai_v1");
+ expect(payload.properties.component_instance_id).toBe("hero-cta");
+ expect(payload.properties.distinct_id).toBeDefined();
+ expect(payload.properties.session_id).toBeDefined();
+ expect(payload.properties.source).toBe("react-sdk");
+ expect(payload.properties.captured_at).toBeDefined();
+ });
+
+ it("includes all required fields in click event", async () => {
+ const fetchMock = global.fetch as ReturnType;
+ fetchMock.mockResolvedValue({ ok: true });
+
+ localStorage.setItem(
+ "probat:assignment:click-payload-exp",
+ JSON.stringify({ variantKey: "control", ts: Date.now() })
+ );
+
+ render(
+ Buy Now}
+ variants={{}}
+ componentInstanceId="footer-cta"
+ />,
+ { wrapper }
+ );
+
+ await act(async () => {
+ await vi.runAllTimersAsync();
+ });
+
+ fireEvent.click(screen.getByText("Buy Now"));
+
+ await act(async () => {
+ await vi.runAllTimersAsync();
+ });
+
+ const clickCalls = fetchMock.mock.calls.filter((c: any[]) => {
+ if (typeof c[0] !== "string" || !c[0].includes("/experiment/metrics")) return false;
+ const body = JSON.parse(c[1]?.body || "{}");
+ return body.event === "$experiment_click";
+ });
+
+ expect(clickCalls.length).toBe(1);
+ const payload = JSON.parse(clickCalls[0][1].body);
+
+ expect(payload.event).toBe("$experiment_click");
+ expect(payload.properties.experiment_id).toBe("click-payload-exp");
+ expect(payload.properties.variant_key).toBe("control");
+ expect(payload.properties.component_instance_id).toBe("footer-cta");
+ expect(payload.properties.click_target_tag).toBe("BUTTON");
+ expect(payload.properties.click_target_text).toBe("Buy Now");
+ expect(payload.properties.click_target_id).toBe("cta-btn");
+ expect(payload.properties.click_is_primary).toBe(false);
+ });
+});
diff --git a/node_modules/@probat/react/src/__tests__/setup.ts b/node_modules/@probat/react/src/__tests__/setup.ts
new file mode 100644
index 00000000..0e417c0c
--- /dev/null
+++ b/node_modules/@probat/react/src/__tests__/setup.ts
@@ -0,0 +1,63 @@
+import "@testing-library/jest-dom/vitest";
+
+// Mock IntersectionObserver
+class MockIntersectionObserver implements IntersectionObserver {
+ readonly root: Element | null = null;
+ readonly rootMargin: string = "";
+ readonly thresholds: ReadonlyArray = [];
+
+ private callback: IntersectionObserverCallback;
+ private elements: Set = new Set();
+
+ constructor(callback: IntersectionObserverCallback, _options?: IntersectionObserverInit) {
+ this.callback = callback;
+ MockIntersectionObserver._instances.push(this);
+ }
+
+ observe(el: Element) {
+ this.elements.add(el);
+ }
+
+ unobserve(el: Element) {
+ this.elements.delete(el);
+ }
+
+ disconnect() {
+ this.elements.clear();
+ }
+
+ takeRecords(): IntersectionObserverEntry[] {
+ return [];
+ }
+
+ // Test helpers
+ static _instances: MockIntersectionObserver[] = [];
+
+ static _reset() {
+ MockIntersectionObserver._instances = [];
+ }
+
+ _trigger(isIntersecting: boolean) {
+ const entries: IntersectionObserverEntry[] = Array.from(this.elements).map(
+ (el) =>
+ ({
+ target: el,
+ isIntersecting,
+ intersectionRatio: isIntersecting ? 0.6 : 0,
+ boundingClientRect: {} as DOMRectReadOnly,
+ intersectionRect: {} as DOMRectReadOnly,
+ rootBounds: null,
+ time: Date.now(),
+ }) as IntersectionObserverEntry
+ );
+ this.callback(entries, this);
+ }
+}
+
+Object.defineProperty(globalThis, "IntersectionObserver", {
+ value: MockIntersectionObserver,
+ writable: true,
+});
+
+// Export for test use
+export { MockIntersectionObserver };
diff --git a/node_modules/@probat/react/src/__tests__/utils.test.ts b/node_modules/@probat/react/src/__tests__/utils.test.ts
new file mode 100644
index 00000000..ed4d93ab
--- /dev/null
+++ b/node_modules/@probat/react/src/__tests__/utils.test.ts
@@ -0,0 +1,79 @@
+import { describe, it, expect, beforeEach } from "vitest";
+import { makeDedupeKey, hasSeen, markSeen, resetDedupe } from "../utils/dedupeStorage";
+import { getDistinctId, getSessionId, buildEventContext } from "../utils/eventContext";
+
+// ── dedupeStorage ──────────────────────────────────────────────────────────
+
+describe("dedupeStorage", () => {
+ beforeEach(() => {
+ sessionStorage.clear();
+ resetDedupe();
+ });
+
+ it("makeDedupeKey produces correct format", () => {
+ const key = makeDedupeKey("exp1", "ai_v1", "inst_1", "/pricing?plan=pro");
+ expect(key).toBe("probat:seen:exp1:ai_v1:inst_1:/pricing?plan=pro");
+ });
+
+ it("hasSeen returns false for unseen keys", () => {
+ expect(hasSeen("probat:seen:new:key:a:b")).toBe(false);
+ });
+
+ it("markSeen + hasSeen round-trips", () => {
+ const key = makeDedupeKey("exp1", "control", "inst_1", "/");
+ expect(hasSeen(key)).toBe(false);
+ markSeen(key);
+ expect(hasSeen(key)).toBe(true);
+ });
+
+ it("resetDedupe clears in-memory set", () => {
+ const key = makeDedupeKey("exp1", "control", "inst_1", "/");
+ markSeen(key);
+ expect(hasSeen(key)).toBe(true);
+ resetDedupe();
+ // sessionStorage still has it, so it should still be seen
+ expect(hasSeen(key)).toBe(true);
+ });
+
+ it("resetDedupe + clear sessionStorage fully resets", () => {
+ const key = makeDedupeKey("exp1", "control", "inst_1", "/");
+ markSeen(key);
+ resetDedupe();
+ sessionStorage.clear();
+ expect(hasSeen(key)).toBe(false);
+ });
+});
+
+// ── eventContext ────────────────────────────────────────────────────────────
+
+describe("eventContext", () => {
+ beforeEach(() => {
+ localStorage.clear();
+ sessionStorage.clear();
+ });
+
+ it("getDistinctId returns a stable id", () => {
+ // Clear the module-level cache by removing from storage and re-importing
+ // Since the module caches in a variable, we just check consistency
+ const id1 = getDistinctId();
+ const id2 = getDistinctId();
+ expect(id1).toBe(id2);
+ expect(id1).toMatch(/^anon_|^server$/);
+ });
+
+ it("getSessionId returns a stable id within session", () => {
+ const id1 = getSessionId();
+ const id2 = getSessionId();
+ expect(id1).toBe(id2);
+ expect(id1).toMatch(/^sess_|^server$/);
+ });
+
+ it("buildEventContext includes all required fields", () => {
+ const ctx = buildEventContext();
+ expect(ctx).toHaveProperty("distinct_id");
+ expect(ctx).toHaveProperty("session_id");
+ expect(ctx).toHaveProperty("$page_url");
+ expect(ctx).toHaveProperty("$pathname");
+ expect(ctx).toHaveProperty("$referrer");
+ });
+});
diff --git a/node_modules/@probat/react/src/components/Experiment.tsx b/node_modules/@probat/react/src/components/Experiment.tsx
new file mode 100644
index 00000000..b7b16d2b
--- /dev/null
+++ b/node_modules/@probat/react/src/components/Experiment.tsx
@@ -0,0 +1,297 @@
+"use client";
+
+import React, {
+ useEffect,
+ useRef,
+ useState,
+ useCallback,
+ useMemo,
+} from "react";
+import { useProbatContext } from "../context/ProbatContext";
+import { fetchDecision, sendMetric, extractClickMeta } from "../utils/api";
+import { getDistinctId, getPageKey } from "../utils/eventContext";
+import { makeDedupeKey, hasSeen, markSeen } from "../utils/dedupeStorage";
+import { useStableInstanceId } from "../utils/stableInstanceId";
+
+// ── localStorage assignment cache ──────────────────────────────────────────
+
+const ASSIGNMENT_PREFIX = "probat:assignment:";
+
+interface StoredAssignment {
+ variantKey: string;
+ ts: number;
+}
+
+function readAssignment(id: string): string | null {
+ if (typeof window === "undefined") return null;
+ try {
+ const raw = localStorage.getItem(ASSIGNMENT_PREFIX + id);
+ if (!raw) return null;
+ const parsed: StoredAssignment = JSON.parse(raw);
+ return parsed.variantKey ?? null;
+ } catch {
+ return null;
+ }
+}
+
+function writeAssignment(id: string, variantKey: string): void {
+ if (typeof window === "undefined") return;
+ try {
+ const entry: StoredAssignment = { variantKey, ts: Date.now() };
+ localStorage.setItem(ASSIGNMENT_PREFIX + id, JSON.stringify(entry));
+ } catch {}
+}
+
+// ── Types ──────────────────────────────────────────────────────────────────
+
+export interface ExperimentTrackOptions {
+ /** Auto-track impressions (default true) */
+ impression?: boolean;
+ /** Auto-track clicks (default true) */
+ primaryClick?: boolean;
+ /** Custom impression event name (default "$experiment_exposure") */
+ impressionEventName?: string;
+ /** Custom click event name (default "$experiment_click") */
+ clickEventName?: string;
+}
+
+export interface ExperimentProps {
+ /** Experiment key / identifier */
+ id: string;
+ /** Control variant ReactNode */
+ control: React.ReactNode;
+ /** Named variant ReactNodes, keyed by variant key (e.g. { ai_v1: }) */
+ variants: Record;
+ /** Tracking configuration */
+ track?: ExperimentTrackOptions;
+ /** Stable instance id when multiple instances of the same experiment exist on a page */
+ componentInstanceId?: string;
+ /** Behavior when assignment fetch fails: "control" (default) renders control, "suspend" throws */
+ fallback?: "control" | "suspend";
+ /** Log decisions + events to console */
+ debug?: boolean;
+}
+
+export function Experiment({
+ id,
+ control,
+ variants,
+ track,
+ componentInstanceId,
+ fallback = "control",
+ debug = false,
+}: ExperimentProps) {
+ const { host, bootstrap } = useProbatContext();
+
+ // Stable instance id (useId + sessionStorage for cross-mount stability)
+ const autoInstanceId = useStableInstanceId(id);
+ const instanceId = componentInstanceId ?? autoInstanceId;
+
+ // Track options with defaults
+ const trackImpression = track?.impression !== false;
+ const trackClick = track?.primaryClick !== false;
+ const impressionEvent = track?.impressionEventName ?? "$experiment_exposure";
+ const clickEvent = track?.clickEventName ?? "$experiment_click";
+
+ // ── Assignment resolution ──────────────────────────────────────────────
+
+ const [variantKey, setVariantKey] = useState(() => {
+ // Sync resolution order: bootstrap → localStorage → "control"
+ if (bootstrap[id]) return bootstrap[id];
+ const cached = readAssignment(id);
+ if (cached) return cached;
+ return "control";
+ });
+ const [resolved, setResolved] = useState(() => {
+ return !!(bootstrap[id] || readAssignment(id));
+ });
+
+ useEffect(() => {
+ // Already resolved from bootstrap or cache
+ if (bootstrap[id] || readAssignment(id)) {
+ // Ensure state is synced (StrictMode may re-mount)
+ const key = bootstrap[id] ?? readAssignment(id) ?? "control";
+ setVariantKey(key);
+ setResolved(true);
+ return;
+ }
+
+ let cancelled = false;
+
+ (async () => {
+ try {
+ const distinctId = getDistinctId();
+ const key = await fetchDecision(host, id, distinctId);
+ if (cancelled) return;
+
+ // Validate variant key
+ if (key !== "control" && !(key in variants)) {
+ if (debug) {
+ console.warn(
+ `[probat] Unknown variant "${key}" for experiment "${id}", falling back to control`
+ );
+ }
+ setVariantKey("control");
+ } else {
+ setVariantKey(key);
+ writeAssignment(id, key);
+ }
+ } catch (err) {
+ if (cancelled) return;
+ if (debug) {
+ console.error(`[probat] fetchDecision failed for "${id}":`, err);
+ }
+ if (fallback === "suspend") throw err;
+ setVariantKey("control");
+ } finally {
+ if (!cancelled) setResolved(true);
+ }
+ })();
+
+ return () => {
+ cancelled = true;
+ };
+ }, [id, host]); // eslint-disable-line react-hooks/exhaustive-deps
+
+ // ── Debug logging ──────────────────────────────────────────────────────
+
+ useEffect(() => {
+ if (debug && resolved) {
+ console.log(`[probat] Experiment "${id}" → variant "${variantKey}"`, {
+ instanceId,
+ pageKey: getPageKey(),
+ });
+ }
+ }, [debug, id, variantKey, resolved, instanceId]);
+
+ // ── Shared event properties ────────────────────────────────────────────
+
+ const eventProps = useMemo(
+ () => ({
+ experiment_id: id,
+ variant_key: variantKey,
+ component_instance_id: instanceId,
+ }),
+ [id, variantKey, instanceId]
+ );
+
+ // ── Impression tracking via IntersectionObserver ────────────────────────
+
+ const containerRef = useRef(null);
+ const impressionSent = useRef(false);
+
+ useEffect(() => {
+ if (!trackImpression || !resolved) return;
+
+ // Reset on re-mount (StrictMode safety)
+ impressionSent.current = false;
+
+ const pageKey = getPageKey();
+ const dedupeKey = makeDedupeKey(id, variantKey, instanceId, pageKey);
+
+ // Already seen this session
+ if (hasSeen(dedupeKey)) {
+ impressionSent.current = true;
+ return;
+ }
+
+ const el = containerRef.current;
+ if (!el) return;
+
+ // Fallback: no IntersectionObserver (SSR, old browser)
+ if (typeof IntersectionObserver === "undefined") {
+ if (!impressionSent.current) {
+ impressionSent.current = true;
+ markSeen(dedupeKey);
+ sendMetric(host, impressionEvent, eventProps);
+ if (debug) console.log(`[probat] Impression sent (no IO) for "${id}"`);
+ }
+ return;
+ }
+
+ let timer: ReturnType | null = null;
+
+ const observer = new IntersectionObserver(
+ ([entry]) => {
+ if (!entry || impressionSent.current) return;
+
+ if (entry.isIntersecting) {
+ timer = setTimeout(() => {
+ if (impressionSent.current) return;
+ impressionSent.current = true;
+ markSeen(dedupeKey);
+ sendMetric(host, impressionEvent, eventProps);
+ if (debug) console.log(`[probat] Impression sent for "${id}"`);
+ observer.disconnect();
+ }, 250);
+ } else if (timer) {
+ clearTimeout(timer);
+ timer = null;
+ }
+ },
+ { threshold: 0.5 }
+ );
+
+ observer.observe(el);
+
+ return () => {
+ observer.disconnect();
+ if (timer) clearTimeout(timer);
+ };
+ }, [
+ trackImpression,
+ resolved,
+ id,
+ variantKey,
+ instanceId,
+ host,
+ impressionEvent,
+ eventProps,
+ debug,
+ ]);
+
+ // ── Click tracking ─────────────────────────────────────────────────────
+
+ const handleClick = useCallback(
+ (e: React.MouseEvent) => {
+ if (!trackClick) return;
+
+ const meta = extractClickMeta(e.target as EventTarget);
+ if (!meta) return;
+
+ sendMetric(host, clickEvent, {
+ ...eventProps,
+ ...meta,
+ });
+ if (debug) {
+ console.log(`[probat] Click tracked for "${id}"`, meta);
+ }
+ },
+ [trackClick, host, clickEvent, eventProps, id, debug]
+ );
+
+ // ── Render ─────────────────────────────────────────────────────────────
+
+ const content =
+ variantKey === "control" || !(variantKey in variants)
+ ? control
+ : variants[variantKey];
+
+ return (
+
+ {content}
+
+ );
+}
diff --git a/node_modules/@probat/react/src/components/ProbatProviderClient.tsx b/node_modules/@probat/react/src/components/ProbatProviderClient.tsx
new file mode 100644
index 00000000..ddae22ad
--- /dev/null
+++ b/node_modules/@probat/react/src/components/ProbatProviderClient.tsx
@@ -0,0 +1,30 @@
+"use client";
+
+import React from "react";
+import { ProbatProvider } from "../context/ProbatContext";
+import type { ProbatProviderProps } from "../context/ProbatContext";
+
+/**
+ * Client-only provider for Next.js App Router.
+ * Import this in your layout/providers file.
+ *
+ * @example
+ * ```tsx
+ * // app/providers.tsx
+ * "use client";
+ * import { ProbatProviderClient } from "@probat/react";
+ *
+ * export function Providers({ children }) {
+ * return (
+ *
+ * {children}
+ *
+ * );
+ * }
+ * ```
+ */
+export function ProbatProviderClient(props: ProbatProviderProps) {
+ return React.createElement(ProbatProvider, props);
+}
+
+export type { ProbatProviderProps };
diff --git a/node_modules/@probat/react/src/context/ProbatContext.tsx b/node_modules/@probat/react/src/context/ProbatContext.tsx
new file mode 100644
index 00000000..97fa4931
--- /dev/null
+++ b/node_modules/@probat/react/src/context/ProbatContext.tsx
@@ -0,0 +1,59 @@
+"use client";
+
+import React, { createContext, useContext, useMemo } from "react";
+
+export interface ProbatContextValue {
+ host: string;
+ userId: string;
+ bootstrap: Record;
+}
+
+const ProbatContext = createContext(null);
+
+const DEFAULT_HOST = "https://gushi.onrender.com";
+
+export interface ProbatProviderProps {
+ /** Gushi user ID (UUID) — used to scope SDK requests to a customer */
+ userId: string;
+ /** Base URL for the Probat API. Defaults to https://gushi.onrender.com */
+ host?: string;
+ /**
+ * Bootstrap assignments to avoid flash on first render.
+ * Map of experiment id → variant key.
+ * e.g. { "cta-copy-test": "ai_v1" }
+ */
+ bootstrap?: Record;
+ children: React.ReactNode;
+}
+
+export function ProbatProvider({
+ userId,
+ host = DEFAULT_HOST,
+ bootstrap,
+ children,
+}: ProbatProviderProps) {
+ const value = useMemo(
+ () => ({
+ host: host.replace(/\/$/, ""),
+ userId,
+ bootstrap: bootstrap ?? {},
+ }),
+ [userId, host, bootstrap]
+ );
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function useProbatContext(): ProbatContextValue {
+ const ctx = useContext(ProbatContext);
+ if (!ctx) {
+ throw new Error(
+ "useProbatContext must be used within . Wrap your app with ."
+ );
+ }
+ return ctx;
+}
diff --git a/node_modules/@probat/react/src/hooks/useProbatMetrics.ts b/node_modules/@probat/react/src/hooks/useProbatMetrics.ts
new file mode 100644
index 00000000..6973aadc
--- /dev/null
+++ b/node_modules/@probat/react/src/hooks/useProbatMetrics.ts
@@ -0,0 +1,36 @@
+"use client";
+
+import { useCallback } from "react";
+import { useProbatContext } from "../context/ProbatContext";
+import { sendMetric } from "../utils/api";
+
+export interface UseProbatMetricsReturn {
+ /**
+ * Send a custom event with arbitrary properties.
+ * Never throws — failures are silently dropped.
+ *
+ * @example
+ * ```tsx
+ * const { capture } = useProbatMetrics();
+ * capture("purchase", { revenue: 42, currency: "USD" });
+ * ```
+ */
+ capture: (event: string, properties?: Record) => void;
+}
+
+/**
+ * Minimal metrics hook. Provides a single `capture(event, props)` function
+ * that sends events to the Probat backend using the provider's host config.
+ */
+export function useProbatMetrics(): UseProbatMetricsReturn {
+ const { host } = useProbatContext();
+
+ const capture = useCallback(
+ (event: string, properties: Record = {}) => {
+ sendMetric(host, event, properties);
+ },
+ [host]
+ );
+
+ return { capture };
+}
diff --git a/node_modules/@probat/react/src/index.ts b/node_modules/@probat/react/src/index.ts
new file mode 100644
index 00000000..25fddfe3
--- /dev/null
+++ b/node_modules/@probat/react/src/index.ts
@@ -0,0 +1,17 @@
+"use client";
+
+// ── Provider ───────────────────────────────────────────────────────────────
+export { ProbatProviderClient } from "./components/ProbatProviderClient";
+export type { ProbatProviderProps } from "./context/ProbatContext";
+
+// ── Experiment component ───────────────────────────────────────────────────
+export { Experiment } from "./components/Experiment";
+export type { ExperimentProps, ExperimentTrackOptions } from "./components/Experiment";
+
+// ── Hooks ──────────────────────────────────────────────────────────────────
+export { useProbatMetrics } from "./hooks/useProbatMetrics";
+export type { UseProbatMetricsReturn } from "./hooks/useProbatMetrics";
+
+// ── Utilities (advanced) ───────────────────────────────────────────────────
+export { sendMetric, fetchDecision } from "./utils/api";
+export type { MetricPayload, DecisionResponse } from "./utils/api";
diff --git a/node_modules/@probat/react/src/utils/api.ts b/node_modules/@probat/react/src/utils/api.ts
new file mode 100644
index 00000000..0f62fade
--- /dev/null
+++ b/node_modules/@probat/react/src/utils/api.ts
@@ -0,0 +1,134 @@
+import { detectEnvironment } from "./environment";
+import { buildEventContext } from "./eventContext";
+
+// ── Types ──────────────────────────────────────────────────────────────────
+
+export interface DecisionResponse {
+ variant_key: string;
+}
+
+export interface MetricPayload {
+ event: string;
+ environment: "dev" | "prod";
+ properties: Record;
+}
+
+// ── Assignment fetching ────────────────────────────────────────────────────
+
+const pendingDecisions = new Map>();
+
+/**
+ * Fetch the variant assignment for an experiment.
+ * Returns the variant key string (e.g. "control", "ai_v1").
+ * Deduplicates concurrent calls for the same experiment.
+ */
+export async function fetchDecision(
+ host: string,
+ experimentId: string,
+ distinctId: string
+): Promise {
+ const existing = pendingDecisions.get(experimentId);
+ if (existing) return existing;
+
+ const promise = (async () => {
+ try {
+ const url = `${host.replace(/\/$/, "")}/experiment/decide`;
+ const res = await fetch(url, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ Accept: "application/json",
+ },
+ credentials: "include",
+ body: JSON.stringify({
+ experiment_id: experimentId,
+ distinct_id: distinctId,
+ }),
+ });
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
+ const data = (await res.json()) as DecisionResponse;
+ return data.variant_key || "control";
+ } finally {
+ pendingDecisions.delete(experimentId);
+ }
+ })();
+
+ pendingDecisions.set(experimentId, promise);
+ return promise;
+}
+
+// ── Metric sending ─────────────────────────────────────────────────────────
+
+/**
+ * Fire-and-forget metric send. Never throws.
+ */
+export function sendMetric(
+ host: string,
+ event: string,
+ properties: Record
+): void {
+ if (typeof window === "undefined") return;
+
+ const ctx = buildEventContext();
+ const payload: MetricPayload = {
+ event,
+ environment: detectEnvironment(),
+ properties: {
+ ...ctx,
+ source: "react-sdk",
+ captured_at: new Date().toISOString(),
+ ...properties,
+ },
+ };
+
+ try {
+ const url = `${host.replace(/\/$/, "")}/experiment/metrics`;
+ fetch(url, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ credentials: "include",
+ body: JSON.stringify(payload),
+ }).catch(() => {});
+ } catch {
+ // silently drop
+ }
+}
+
+// ── Click metadata extraction ──────────────────────────────────────────────
+
+export interface ClickMeta {
+ click_target_tag: string;
+ click_target_text?: string;
+ click_target_id?: string;
+ click_is_primary: boolean;
+}
+
+/**
+ * Given a click event inside an experiment boundary, extract metadata.
+ * Prioritizes elements with data-probat-click="primary",
+ * then falls back to button/a/role=button.
+ */
+export function extractClickMeta(target: EventTarget | null): ClickMeta | null {
+ if (!target || !(target instanceof HTMLElement)) return null;
+
+ // Priority 1: explicit primary marker
+ const primary = target.closest('[data-probat-click="primary"]');
+ if (primary) return buildMeta(primary as HTMLElement, true);
+
+ // Priority 2: interactive elements
+ const interactive = target.closest('button, a, [role="button"]');
+ if (interactive) return buildMeta(interactive as HTMLElement, false);
+
+ return null;
+}
+
+function buildMeta(el: HTMLElement, isPrimary: boolean): ClickMeta {
+ const meta: ClickMeta = {
+ click_target_tag: el.tagName,
+ click_is_primary: isPrimary,
+ };
+ if (el.id) meta.click_target_id = el.id;
+ const text = el.textContent?.trim();
+ if (text) meta.click_target_text = text.slice(0, 120);
+ return meta;
+}
diff --git a/node_modules/@probat/react/src/utils/dedupeStorage.ts b/node_modules/@probat/react/src/utils/dedupeStorage.ts
new file mode 100644
index 00000000..b51fc462
--- /dev/null
+++ b/node_modules/@probat/react/src/utils/dedupeStorage.ts
@@ -0,0 +1,40 @@
+/**
+ * Dedupe storage for experiment exposures.
+ * Uses sessionStorage + in-memory Set fallback.
+ * Key format: probat:seen:{id}:{variantKey}:{instanceId}:{pageKey}
+ */
+
+const PREFIX = "probat:seen:";
+const memorySet = new Set();
+
+export function makeDedupeKey(
+ experimentId: string,
+ variantKey: string,
+ instanceId: string,
+ pageKey: string
+): string {
+ return `${PREFIX}${experimentId}:${variantKey}:${instanceId}:${pageKey}`;
+}
+
+export function hasSeen(key: string): boolean {
+ if (memorySet.has(key)) return true;
+ if (typeof window === "undefined") return false;
+ try {
+ return sessionStorage.getItem(key) === "1";
+ } catch {
+ return false;
+ }
+}
+
+export function markSeen(key: string): void {
+ memorySet.add(key);
+ if (typeof window === "undefined") return;
+ try {
+ sessionStorage.setItem(key, "1");
+ } catch {}
+}
+
+/** Reset all dedupe state — useful for testing. */
+export function resetDedupe(): void {
+ memorySet.clear();
+}
diff --git a/node_modules/@probat/react/src/utils/environment.ts b/node_modules/@probat/react/src/utils/environment.ts
new file mode 100644
index 00000000..390aa1c8
--- /dev/null
+++ b/node_modules/@probat/react/src/utils/environment.ts
@@ -0,0 +1,41 @@
+/**
+ * Detect if the code is running on localhost (development environment).
+ * Returns "dev" for localhost, "prod" for production.
+ */
+export function detectEnvironment(): "dev" | "prod" {
+ if (typeof window === "undefined") {
+ return "prod"; // Server-side, default to prod
+ }
+
+ const hostname = window.location.hostname;
+
+ // Check for localhost, 127.0.0.1, or local IP addresses
+ if (
+ hostname === "localhost" ||
+ hostname === "127.0.0.1" ||
+ hostname === "0.0.0.0" ||
+ hostname.startsWith("192.168.") ||
+ hostname.startsWith("10.") ||
+ hostname.startsWith("172.16.") ||
+ hostname.startsWith("172.17.") ||
+ hostname.startsWith("172.18.") ||
+ hostname.startsWith("172.19.") ||
+ hostname.startsWith("172.20.") ||
+ hostname.startsWith("172.21.") ||
+ hostname.startsWith("172.22.") ||
+ hostname.startsWith("172.23.") ||
+ hostname.startsWith("172.24.") ||
+ hostname.startsWith("172.25.") ||
+ hostname.startsWith("172.26.") ||
+ hostname.startsWith("172.27.") ||
+ hostname.startsWith("172.28.") ||
+ hostname.startsWith("172.29.") ||
+ hostname.startsWith("172.30.") ||
+ hostname.startsWith("172.31.")
+ ) {
+ return "dev";
+ }
+
+ return "prod";
+}
+
diff --git a/node_modules/@probat/react/src/utils/eventContext.ts b/node_modules/@probat/react/src/utils/eventContext.ts
new file mode 100644
index 00000000..6055d73b
--- /dev/null
+++ b/node_modules/@probat/react/src/utils/eventContext.ts
@@ -0,0 +1,94 @@
+/**
+ * Event context helpers: distinct_id, session_id, page info.
+ * All browser-safe — no-ops when window is unavailable.
+ */
+
+const DISTINCT_ID_KEY = "probat:distinct_id";
+const SESSION_ID_KEY = "probat:session_id";
+
+let cachedDistinctId: string | null = null;
+let cachedSessionId: string | null = null;
+
+function generateId(): string {
+ // crypto.randomUUID where available, else fallback
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
+ return crypto.randomUUID();
+ }
+ // fallback: random hex
+ const bytes = new Uint8Array(16);
+ if (typeof crypto !== "undefined" && crypto.getRandomValues) {
+ crypto.getRandomValues(bytes);
+ } else {
+ for (let i = 0; i < 16; i++) bytes[i] = Math.floor(Math.random() * 256);
+ }
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
+}
+
+export function getDistinctId(): string {
+ if (cachedDistinctId) return cachedDistinctId;
+ if (typeof window === "undefined") return "server";
+ try {
+ const stored = localStorage.getItem(DISTINCT_ID_KEY);
+ if (stored) {
+ cachedDistinctId = stored;
+ return stored;
+ }
+ } catch {}
+ const id = `anon_${generateId()}`;
+ cachedDistinctId = id;
+ try {
+ localStorage.setItem(DISTINCT_ID_KEY, id);
+ } catch {}
+ return id;
+}
+
+export function getSessionId(): string {
+ if (cachedSessionId) return cachedSessionId;
+ if (typeof window === "undefined") return "server";
+ try {
+ const stored = sessionStorage.getItem(SESSION_ID_KEY);
+ if (stored) {
+ cachedSessionId = stored;
+ return stored;
+ }
+ } catch {}
+ const id = `sess_${generateId()}`;
+ cachedSessionId = id;
+ try {
+ sessionStorage.setItem(SESSION_ID_KEY, id);
+ } catch {}
+ return id;
+}
+
+export function getPageKey(): string {
+ if (typeof window === "undefined") return "";
+ return window.location.pathname + window.location.search;
+}
+
+export function getPageUrl(): string {
+ if (typeof window === "undefined") return "";
+ return window.location.href;
+}
+
+export function getReferrer(): string {
+ if (typeof document === "undefined") return "";
+ return document.referrer;
+}
+
+export interface EventContext {
+ distinct_id: string;
+ session_id: string;
+ $page_url: string;
+ $pathname: string;
+ $referrer: string;
+}
+
+export function buildEventContext(): EventContext {
+ return {
+ distinct_id: getDistinctId(),
+ session_id: getSessionId(),
+ $page_url: getPageUrl(),
+ $pathname: typeof window !== "undefined" ? window.location.pathname : "",
+ $referrer: getReferrer(),
+ };
+}
diff --git a/node_modules/@probat/react/src/utils/stableInstanceId.ts b/node_modules/@probat/react/src/utils/stableInstanceId.ts
new file mode 100644
index 00000000..8cc2d37b
--- /dev/null
+++ b/node_modules/@probat/react/src/utils/stableInstanceId.ts
@@ -0,0 +1,113 @@
+/**
+ * Stable auto-generated instance IDs for .
+ *
+ * Problem: a naïve useRef + module counter gives a different ID on every mount,
+ * so StrictMode double-mount or unmount/remount changes the dedupe key.
+ *
+ * Solution:
+ * 1. React 18+ → useId() is stable per fiber position.
+ * 2. Fallback → sessionStorage-backed slot counter per (experimentId, pageKey).
+ * 3. Both paths persist a mapping in sessionStorage:
+ * probat:instance:{experimentId}:{pageKey}:{positionKey} → stableId
+ * so the same position resolves to the same ID across mounts.
+ */
+
+import React, { useRef } from "react";
+import { getPageKey } from "./eventContext";
+
+const INSTANCE_PREFIX = "probat:instance:";
+
+// ── Helpers ────────────────────────────────────────────────────────────────
+
+function shortId(): string {
+ const bytes = new Uint8Array(4);
+ if (typeof crypto !== "undefined" && crypto.getRandomValues) {
+ crypto.getRandomValues(bytes);
+ } else {
+ for (let i = 0; i < 4; i++) bytes[i] = Math.floor(Math.random() * 256);
+ }
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
+}
+
+/**
+ * Look up or create a stable instance ID in sessionStorage.
+ */
+function resolveStableId(storageKey: string): string {
+ if (typeof window !== "undefined") {
+ try {
+ const stored = sessionStorage.getItem(storageKey);
+ if (stored) return stored;
+ } catch {}
+ }
+ const id = `inst_${shortId()}`;
+ if (typeof window !== "undefined") {
+ try {
+ sessionStorage.setItem(storageKey, id);
+ } catch {}
+ }
+ return id;
+}
+
+// ── Fallback: render-wave slot counter ─────────────────────────────────────
+// Each synchronous render batch claims sequential slots per (experimentId,
+// pageKey). A microtask resets the counters so the next batch starts at 0,
+// giving the same component position the same slot across mounts.
+
+const slotCounters = new Map();
+let resetScheduled = false;
+
+function claimSlot(groupKey: string): number {
+ const idx = slotCounters.get(groupKey) ?? 0;
+ slotCounters.set(groupKey, idx + 1);
+ if (!resetScheduled) {
+ resetScheduled = true;
+ Promise.resolve().then(() => {
+ slotCounters.clear();
+ resetScheduled = false;
+ });
+ }
+ return idx;
+}
+
+// ── Hook: React 18+ path (useId available) ─────────────────────────────────
+
+function useStableInstanceIdV18(experimentId: string): string {
+ const reactId = (React as any).useId() as string;
+ const ref = useRef("");
+ if (!ref.current) {
+ const key = `${INSTANCE_PREFIX}${experimentId}:${getPageKey()}:${reactId}`;
+ ref.current = resolveStableId(key);
+ }
+ return ref.current;
+}
+
+// ── Hook: fallback path (no useId) ─────────────────────────────────────────
+
+function useStableInstanceIdFallback(experimentId: string): string {
+ const slotRef = useRef(-1);
+ const ref = useRef("");
+ if (slotRef.current === -1) {
+ slotRef.current = claimSlot(`${experimentId}:${getPageKey()}`);
+ }
+ if (!ref.current) {
+ const key = `${INSTANCE_PREFIX}${experimentId}:${getPageKey()}:${slotRef.current}`;
+ ref.current = resolveStableId(key);
+ }
+ return ref.current;
+}
+
+// ── Exported hook ──────────────────────────────────────────────────────────
+// Selection is a module-level constant so the hook-call count never changes
+// between renders — safe for the rules of hooks.
+
+export const useStableInstanceId: (experimentId: string) => string =
+ typeof (React as any).useId === "function"
+ ? useStableInstanceIdV18
+ : useStableInstanceIdFallback;
+
+// ── Test utility ───────────────────────────────────────────────────────────
+
+export function resetInstanceIdState(): void {
+ slotCounters.clear();
+ resetScheduled = false;
+}
diff --git a/node_modules/@probat/react/src/utils/storage.ts b/node_modules/@probat/react/src/utils/storage.ts
new file mode 100644
index 00000000..69b26b4d
--- /dev/null
+++ b/node_modules/@probat/react/src/utils/storage.ts
@@ -0,0 +1,35 @@
+/**
+ * Safe localStorage/sessionStorage helpers.
+ */
+
+export function safeGetLocal(key: string): string | null {
+ if (typeof window === "undefined") return null;
+ try {
+ return localStorage.getItem(key);
+ } catch {
+ return null;
+ }
+}
+
+export function safeSetLocal(key: string, value: string): void {
+ if (typeof window === "undefined") return;
+ try {
+ localStorage.setItem(key, value);
+ } catch {}
+}
+
+export function safeGetSession(key: string): string | null {
+ if (typeof window === "undefined") return null;
+ try {
+ return sessionStorage.getItem(key);
+ } catch {
+ return null;
+ }
+}
+
+export function safeSetSession(key: string, value: string): void {
+ if (typeof window === "undefined") return;
+ try {
+ sessionStorage.setItem(key, value);
+ } catch {}
+}
diff --git a/node_modules/@rollup/rollup-linux-x64-gnu/README.md b/node_modules/@rollup/rollup-linux-x64-gnu/README.md
new file mode 100644
index 00000000..cabe280f
--- /dev/null
+++ b/node_modules/@rollup/rollup-linux-x64-gnu/README.md
@@ -0,0 +1,3 @@
+# `@rollup/rollup-linux-x64-gnu`
+
+This is the **x86_64-unknown-linux-gnu** binary for `rollup`
diff --git a/node_modules/@rollup/rollup-linux-x64-gnu/package.json b/node_modules/@rollup/rollup-linux-x64-gnu/package.json
new file mode 100644
index 00000000..ca092c89
--- /dev/null
+++ b/node_modules/@rollup/rollup-linux-x64-gnu/package.json
@@ -0,0 +1,25 @@
+{
+ "name": "@rollup/rollup-linux-x64-gnu",
+ "version": "4.57.1",
+ "os": [
+ "linux"
+ ],
+ "cpu": [
+ "x64"
+ ],
+ "files": [
+ "rollup.linux-x64-gnu.node"
+ ],
+ "description": "Native bindings for Rollup",
+ "author": "Lukas Taegert-Atkinson",
+ "homepage": "https://rollupjs.org/",
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/rollup/rollup.git"
+ },
+ "libc": [
+ "glibc"
+ ],
+ "main": "./rollup.linux-x64-gnu.node"
+}
\ No newline at end of file
diff --git a/node_modules/@rollup/rollup-linux-x64-gnu/rollup.linux-x64-gnu.node b/node_modules/@rollup/rollup-linux-x64-gnu/rollup.linux-x64-gnu.node
new file mode 100644
index 00000000..49aced3b
Binary files /dev/null and b/node_modules/@rollup/rollup-linux-x64-gnu/rollup.linux-x64-gnu.node differ
diff --git a/node_modules/@rollup/rollup-linux-x64-musl/README.md b/node_modules/@rollup/rollup-linux-x64-musl/README.md
new file mode 100644
index 00000000..5848a6c6
--- /dev/null
+++ b/node_modules/@rollup/rollup-linux-x64-musl/README.md
@@ -0,0 +1,3 @@
+# `@rollup/rollup-linux-x64-musl`
+
+This is the **x86_64-unknown-linux-musl** binary for `rollup`
diff --git a/node_modules/@rollup/rollup-linux-x64-musl/package.json b/node_modules/@rollup/rollup-linux-x64-musl/package.json
new file mode 100644
index 00000000..8a557721
--- /dev/null
+++ b/node_modules/@rollup/rollup-linux-x64-musl/package.json
@@ -0,0 +1,25 @@
+{
+ "name": "@rollup/rollup-linux-x64-musl",
+ "version": "4.57.1",
+ "os": [
+ "linux"
+ ],
+ "cpu": [
+ "x64"
+ ],
+ "files": [
+ "rollup.linux-x64-musl.node"
+ ],
+ "description": "Native bindings for Rollup",
+ "author": "Lukas Taegert-Atkinson",
+ "homepage": "https://rollupjs.org/",
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/rollup/rollup.git"
+ },
+ "libc": [
+ "musl"
+ ],
+ "main": "./rollup.linux-x64-musl.node"
+}
\ No newline at end of file
diff --git a/node_modules/@rollup/rollup-linux-x64-musl/rollup.linux-x64-musl.node b/node_modules/@rollup/rollup-linux-x64-musl/rollup.linux-x64-musl.node
new file mode 100644
index 00000000..337ac66a
Binary files /dev/null and b/node_modules/@rollup/rollup-linux-x64-musl/rollup.linux-x64-musl.node differ
diff --git a/package-lock.json b/package-lock.json
index 86ffc834..d27ba751 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,6 +8,7 @@
"name": "landing-page",
"version": "1.0.0",
"dependencies": {
+ "@probat/react": "^0.3.2",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
@@ -954,6 +955,15 @@
"node": ">= 8"
}
},
+ "node_modules/@probat/react": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/@probat/react/-/react-0.3.2.tgz",
+ "integrity": "sha512-QVdlSiJMMmNOJtRBgrlwOUywTMi+BV0fh1ei5ZSICik2FhDKKCAPZeMXtktGQbdeHWOHudMrWKZTTXm09ASbCg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": ">=18.0.0"
+ }
+ },
"node_modules/@rolldown/pluginutils": {
"version": "1.0.0-beta.27",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
diff --git a/src/components/GetStartedButton.original.tsx b/src/components/GetStartedButton.original.tsx
deleted file mode 100644
index 1a1173af..00000000
--- a/src/components/GetStartedButton.original.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import React from 'react'
-
-interface GetStartedButtonProps {
- label?: string
- large?: boolean
-}
-
-const GetStartedButton: React.FC = ({ label = 'Get Started Free', large = false }) => {
- return (
-
- )
-}
-
-export default GetStartedButton
diff --git a/src/components/GetStartedButton.tsx b/src/components/GetStartedButton.tsx
index 9165b3cf..e3a0b858 100644
--- a/src/components/GetStartedButton.tsx
+++ b/src/components/GetStartedButton.tsx
@@ -1,16 +1,5 @@
-"use client";
-import { ProbatProviderClient, Experiment } from "@probat/react";
-import OriginalComponent from "./GetStartedButton.original";
import ExperimentVariant from "./GetStartedButton.experiment";
export default function GetStartedButton(props: any) {
- return (
-
- }
- variants={{ experiment: }}
- />
-
- );
+ return ;
}