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 ; }