{"_id":"@avsbhq/browser","_rev":"9-b98964c43c19a3d17f891892f17c1d91","name":"@avsbhq/browser","dist-tags":{"latest":"1.4.2"},"versions":{"1.0.0":{"name":"@avsbhq/browser","version":"1.0.0","_id":"@avsbhq/browser@1.0.0","maintainers":[{"name":"main1479","email":"m.main2402@gmail.com"}],"homepage":"https://github.com/main1479/a-vs-b/tree/main/packages/avsb-browser#readme","bugs":{"url":"https://github.com/main1479/a-vs-b/issues"},"dist":{"shasum":"9419eaba5417bc6d8b908ffa13358c1f4aec6477","tarball":"https://registry.npmjs.org/@avsbhq/browser/-/browser-1.0.0.tgz","fileCount":10,"integrity":"sha512-4pGe3nfUjgmnqUe4vNKYSREfeHeA06c28k5RIst8IT3wCLSRwApnmcHA5waDSBtXrLLXWGY6nELEmCDrAZGlZQ==","signatures":[{"sig":"MEUCIQCiXOoqXlV3LoqeCx88lf5MfWPy7c9KwAAiZt6fh3K3TwIgSRryjq43l1ijokbrJU/0CiiUPNyC/Xqfj3q8kYQ+kcI=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":97291},"main":"dist/index.cjs","type":"module","types":"dist/index.d.ts","module":"dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./server":{"types":"./dist/server.d.ts","import":"./dist/server.js","require":"./dist/server.cjs"}},"gitHead":"8cd6226e120fe67c198b2f1db0087cd38b4a4060","scripts":{"dev":"tsup --watch","build":"tsup"},"_npmUser":{"name":"main1479","email":"m.main2402@gmail.com"},"repository":{"url":"git+https://github.com/main1479/a-vs-b.git","type":"git","directory":"packages/avsb-browser"},"_npmVersion":"10.9.2","description":"Browser client SDK for the [A vs B](https://app.avsb.cloud) platform.","directories":{},"_nodeVersion":"22.14.0","dependencies":{"@avsbhq/core":"1.0.0","@avsbhq/utils":"1.0.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","typescript":"^5.5.0"},"_npmOperationalInternal":{"tmp":"tmp/browser_1.0.0_1780034343794_0.044925683565254726","host":"s3://npm-registry-packages-npm-production"}},"1.1.0":{"name":"@avsbhq/browser","version":"1.1.0","_id":"@avsbhq/browser@1.1.0","maintainers":[{"name":"main1479","email":"m.main2402@gmail.com"}],"homepage":"https://github.com/main1479/a-vs-b/tree/main/packages/avsb-browser#readme","bugs":{"url":"https://github.com/main1479/a-vs-b/issues"},"dist":{"shasum":"8b21cda81f33a0aba50beff7740d923b17654093","tarball":"https://registry.npmjs.org/@avsbhq/browser/-/browser-1.1.0.tgz","fileCount":6,"integrity":"sha512-LT/0ryn9llWlTkT4M7jLhr7UiOsukz93rIjI+rOj1j2dWQ0BVDVr7XIhU2/M62gYpY/CpAtbaN8UFCOHk6X0Cw==","signatures":[{"sig":"MEUCIGhhw6nt5HYv2uitdMsJZrrZcNzd6WxLUbdHxxkgXmDyAiEAzeu/q4fXbjeRbAUvxSzO7nENSsEPvAWXUpQ+aeYxA0M=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":86339},"main":"dist/index.cjs","type":"module","types":"dist/index.d.ts","module":"dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./server":{"types":"./dist/server.d.ts","import":"./dist/server.js","require":"./dist/server.cjs"}},"gitHead":"123dcb4c9c057ba28db9c98bad7bfa4d7f770457","scripts":{"dev":"tsup --watch","build":"tsup"},"_npmUser":{"name":"main1479","email":"m.main2402@gmail.com"},"deprecated":"Published from a stale build; use 1.1.1 instead.","repository":{"url":"git+https://github.com/main1479/a-vs-b.git","type":"git","directory":"packages/avsb-browser"},"_npmVersion":"10.9.8","description":"Browser client SDK for the [A vs B](https://app.avsb.cloud) platform.","directories":{},"_nodeVersion":"22.22.3","dependencies":{"@avsbhq/core":"1.0.0","@avsbhq/utils":"1.0.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","typescript":"^5.5.0"},"_npmOperationalInternal":{"tmp":"tmp/browser_1.1.0_1781205910236_0.8123442949549724","host":"s3://npm-registry-packages-npm-production"}},"1.1.1":{"name":"@avsbhq/browser","version":"1.1.1","_id":"@avsbhq/browser@1.1.1","maintainers":[{"name":"main1479","email":"m.main2402@gmail.com"}],"homepage":"https://github.com/main1479/a-vs-b/tree/main/packages/avsb-browser#readme","bugs":{"url":"https://github.com/main1479/a-vs-b/issues"},"dist":{"shasum":"d678165ecb07e8cff678744158baba70d5b76cc0","tarball":"https://registry.npmjs.org/@avsbhq/browser/-/browser-1.1.1.tgz","fileCount":10,"integrity":"sha512-aigOv3fRzY4UFNJGIO9Sy9m1XyD4pr5C8KFVFKq74IrtGeHxoAzv0Dd45JkSGOwt4lccY3ikeT2ZRmP1JMs5yA==","signatures":[{"sig":"MEUCIQDvb+vPRcznp+Vy3bX6TDVwHpHvNPECg2eOZCXUGPxodgIgTf7r3aCyfnrE4dpKtF68VAknb0xmYA+ma1VSVQpuOBg=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":104160},"main":"dist/index.cjs","type":"module","types":"dist/index.d.ts","module":"dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./server":{"types":"./dist/server.d.ts","import":"./dist/server.js","require":"./dist/server.cjs"}},"gitHead":"123dcb4c9c057ba28db9c98bad7bfa4d7f770457","scripts":{"dev":"tsup --watch","build":"tsup"},"_npmUser":{"name":"main1479","email":"m.main2402@gmail.com"},"repository":{"url":"git+https://github.com/main1479/a-vs-b.git","type":"git","directory":"packages/avsb-browser"},"_npmVersion":"10.9.8","description":"Browser client SDK for the [A vs B](https://app.avsb.cloud) platform.","directories":{},"_nodeVersion":"22.22.3","dependencies":{"@avsbhq/core":"1.1.0","@avsbhq/utils":"1.0.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","typescript":"^5.5.0"},"_npmOperationalInternal":{"tmp":"tmp/browser_1.1.1_1781206076371_0.9514872554220273","host":"s3://npm-registry-packages-npm-production"}},"1.3.0":{"name":"@avsbhq/browser","version":"1.3.0","_id":"@avsbhq/browser@1.3.0","maintainers":[{"name":"main1479","email":"m.main2402@gmail.com"}],"homepage":"https://github.com/main1479/a-vs-b/tree/main/packages/avsb-browser#readme","bugs":{"url":"https://github.com/main1479/a-vs-b/issues"},"dist":{"shasum":"c370566da89032c5ec00920ec748814614ff616b","tarball":"https://registry.npmjs.org/@avsbhq/browser/-/browser-1.3.0.tgz","fileCount":10,"integrity":"sha512-ZyEQZERf4LRrR5nM2MoL/SKklM7GNosT/xPrJZO4jJ9JJGwuH1GlsgmZN0qaNgjrc3V1lZcw2eRQiP1cstNjyw==","signatures":[{"sig":"MEUCID+R8zaQmJicOxi0ez9iy3N5g83qrqwOAhvGM705mjAYAiEAysgvq7DmAlWM2yLWdd9sJjCsBiPqD+DTOzBgwJqP0+I=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":114952},"main":"dist/index.cjs","type":"module","types":"dist/index.d.ts","module":"dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./server":{"types":"./dist/server.d.ts","import":"./dist/server.js","require":"./dist/server.cjs"}},"scripts":{"dev":"tsup --watch","build":"tsup"},"_npmUser":{"name":"main1479","email":"m.main2402@gmail.com"},"repository":{"url":"git+https://github.com/main1479/a-vs-b.git","type":"git","directory":"packages/avsb-browser"},"_npmVersion":"10.9.8","description":"Browser client SDK for the [A vs B](https://app.avsb.cloud) platform.","directories":{},"_nodeVersion":"22.22.3","dependencies":{"@avsbhq/core":"1.4.0","@avsbhq/utils":"1.0.2"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","typescript":"^5.5.0"},"_npmOperationalInternal":{"tmp":"tmp/browser_1.3.0_1785346453833_0.5819988257334754","host":"s3://npm-registry-packages-npm-production"}},"1.3.1":{"name":"@avsbhq/browser","version":"1.3.1","_id":"@avsbhq/browser@1.3.1","maintainers":[{"name":"main1479","email":"m.main2402@gmail.com"}],"homepage":"https://github.com/main1479/a-vs-b/tree/main/packages/avsb-browser#readme","bugs":{"url":"https://github.com/main1479/a-vs-b/issues"},"dist":{"shasum":"4cafe7674655e99eff3da6c57efa02686801d1f6","tarball":"https://registry.npmjs.org/@avsbhq/browser/-/browser-1.3.1.tgz","fileCount":10,"integrity":"sha512-urKyLroug0IKIzUmZgXLQS2Y27q0+lwrpyHLGe/VVPTOuGlfalvuvdeJIqcaGapTc1aUuyzRgFnRCoL50bJIDA==","signatures":[{"sig":"MEUCIQCwFuzWsRxNtkSzOq0kE4Bhnus6WpMUzl3WliSdktJ/mQIgTFgP7ichv4Kjwe0SUoUs7WxZ3z/U9kEHPERpRvNti6g=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":114952},"main":"dist/index.cjs","type":"module","types":"dist/index.d.ts","module":"dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./server":{"types":"./dist/server.d.ts","import":"./dist/server.js","require":"./dist/server.cjs"}},"scripts":{"dev":"tsup --watch","build":"tsup"},"_npmUser":{"name":"main1479","email":"m.main2402@gmail.com"},"repository":{"url":"git+https://github.com/main1479/a-vs-b.git","type":"git","directory":"packages/avsb-browser"},"_npmVersion":"10.9.8","description":"Browser client SDK for the [A vs B](https://app.avsb.cloud) platform.","directories":{},"_nodeVersion":"22.22.3","dependencies":{"@avsbhq/core":"1.4.0","@avsbhq/utils":"1.0.2"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","typescript":"^5.5.0"},"_npmOperationalInternal":{"tmp":"tmp/browser_1.3.1_1785347969066_0.643755774600929","host":"s3://npm-registry-packages-npm-production"}},"1.4.0":{"name":"@avsbhq/browser","version":"1.4.0","keywords":["avsb","feature-flags","ab-testing","experiments","browser","sdk"],"license":"MIT","_id":"@avsbhq/browser@1.4.0","maintainers":[{"name":"main1479","email":"m.main2402@gmail.com"}],"homepage":"https://github.com/avsbhq/a-vs-b/tree/main/packages/avsb-browser#readme","bugs":{"url":"https://github.com/avsbhq/a-vs-b/issues"},"dist":{"shasum":"74ddacf12aa985f509595cb60287b2956ac31d20","tarball":"https://registry.npmjs.org/@avsbhq/browser/-/browser-1.4.0.tgz","fileCount":12,"integrity":"sha512-GhYPGxSAEE/nh7kzSGUNL4t5WL8EIvkAoNNZpwxodoT9CZkt0xDdLJEaittSSpslTsueeqa4gt6dNY6iRE8D0A==","signatures":[{"sig":"MEYCIQC99wBWyDqQjZQ9sejH7HZ6bqtPnJkA4Sr10FpuKJBBpgIhANdR05qR34r2NllB1lzyqeyULfas0EafJRGD3/VyMcYJ","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":262816},"main":"dist/index.cjs","type":"module","types":"dist/index.d.ts","module":"dist/index.js","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./server":{"import":{"types":"./dist/server.d.ts","default":"./dist/server.js"},"require":{"types":"./dist/server.d.cts","default":"./dist/server.cjs"}},"./package.json":"./package.json"},"gitHead":"5eed46d5944b522ba0aa34e82850c98bb91667fe","scripts":{"dev":"tsup --watch","build":"tsup"},"_npmUser":{"name":"main1479","email":"m.main2402@gmail.com"},"repository":{"url":"git+https://github.com/avsbhq/a-vs-b.git","type":"git","directory":"packages/avsb-browser"},"_npmVersion":"10.9.8","description":"Browser SDK for A vs B feature flags and experiments: typed flag getters, a cached datafile, persistent visitor identity, and event tracking.","directories":{},"sideEffects":false,"_nodeVersion":"22.22.3","dependencies":{"@avsbhq/core":"^1.4.0","@avsbhq/utils":"^1.0.2"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","react":"^19.2.4","react-dom":"^19.2.4","typescript":"^5.5.0","@types/react":"^19.2.14","@testing-library/react":"^16.3.2"},"_npmOperationalInternal":{"tmp":"tmp/browser_1.4.0_1785971464495_0.8266211369947702","host":"s3://npm-registry-packages-npm-production"}},"1.4.1":{"name":"@avsbhq/browser","version":"1.4.1","keywords":["avsb","feature-flags","ab-testing","experiments","browser","sdk"],"license":"MIT","_id":"@avsbhq/browser@1.4.1","maintainers":[{"name":"main1479","email":"m.main2402@gmail.com"}],"homepage":"https://github.com/avsbhq/a-vs-b/tree/main/packages/avsb-browser#readme","bugs":{"url":"https://github.com/avsbhq/a-vs-b/issues"},"dist":{"shasum":"9975c634ba6ceaf13bf29a47b3031e6d63dc95d9","tarball":"https://registry.npmjs.org/@avsbhq/browser/-/browser-1.4.1.tgz","fileCount":12,"integrity":"sha512-e8U4AchbcVVD1oxQsqchL0XkZ7EqbnVNtjlaLpRHd5iyYoL35y+fz5mOLuElobnG5mFp9yjB6w4Lvnxg7YOZYg==","signatures":[{"sig":"MEUCIFzuqYCCMXX8v2zggRzpXYgkOc8L/cjaPJH3QjTm+OAJAiEA9KJt4+2gHr8i5nlYR3MhHTfUx5bOuW0LSZ27CnJAgAQ=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":262874},"main":"dist/index.cjs","type":"module","types":"dist/index.d.ts","module":"dist/index.js","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./server":{"import":{"types":"./dist/server.d.ts","default":"./dist/server.js"},"require":{"types":"./dist/server.d.cts","default":"./dist/server.cjs"}},"./package.json":"./package.json"},"gitHead":"c14be14648ac1463e94a3e6eedfcd567d9d4b45e","scripts":{"dev":"tsup --watch","build":"tsup"},"_npmUser":{"name":"main1479","email":"m.main2402@gmail.com"},"repository":{"url":"git+https://github.com/avsbhq/a-vs-b.git","type":"git","directory":"packages/avsb-browser"},"_npmVersion":"10.9.8","description":"Browser SDK for A vs B feature flags and experiments: typed flag getters, a cached datafile, persistent visitor identity, and event tracking.","directories":{},"sideEffects":false,"_nodeVersion":"22.22.3","dependencies":{"@avsbhq/core":"^1.4.0","@avsbhq/utils":"^1.2.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","react":"^19.2.4","react-dom":"^19.2.4","typescript":"^5.5.0","@types/react":"^19.2.14","@testing-library/react":"^16.3.2"},"_npmOperationalInternal":{"tmp":"tmp/browser_1.4.1_1785996103263_0.5867322443459662","host":"s3://npm-registry-packages-npm-production"}},"1.4.2":{"name":"@avsbhq/browser","version":"1.4.2","description":"Browser SDK for A vs B feature flags and experiments: typed flag getters, a cached datafile, persistent visitor identity, and event tracking.","keywords":["avsb","feature-flags","ab-testing","experiments","browser","sdk"],"license":"MIT","type":"module","sideEffects":false,"main":"dist/index.cjs","module":"dist/index.js","types":"dist/index.d.ts","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./server":{"import":{"types":"./dist/server.d.ts","default":"./dist/server.js"},"require":{"types":"./dist/server.d.cts","default":"./dist/server.cjs"}},"./package.json":"./package.json"},"publishConfig":{"access":"public"},"repository":{"type":"git","url":"git+https://github.com/avsbhq/a-vs-b.git","directory":"packages/avsb-browser"},"homepage":"https://github.com/avsbhq/a-vs-b/tree/main/packages/avsb-browser#readme","bugs":{"url":"https://github.com/avsbhq/a-vs-b/issues"},"scripts":{"build":"tsup","dev":"tsup --watch"},"dependencies":{"@avsbhq/core":"^1.4.0","@avsbhq/utils":"^1.2.0"},"devDependencies":{"@testing-library/react":"^16.3.2","@types/react":"^19.2.14","react":"^19.2.4","react-dom":"^19.2.4","tsup":"^8.0.0","typescript":"^5.5.0"},"_id":"@avsbhq/browser@1.4.2","gitHead":"d31d88fbf202f40041b4cb47f77882f454fa5ef8","_nodeVersion":"22.22.3","_npmVersion":"10.9.8","dist":{"integrity":"sha512-sqQ2YqkaPFHCdxW9TwvaRWAznpOYpBPXTv6qQbNtHvPWpAxaVyaxPgg4JvKbAa3ESFnOiWCvLXlVZE/Wrlu+Qg==","shasum":"6ce509d3054e129d93509d8d1b152861d306131b","tarball":"https://registry.npmjs.org/@avsbhq/browser/-/browser-1.4.2.tgz","fileCount":12,"unpackedSize":262890,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIE1isQYcIhmoNxbcoiT7ZglbztIxWiE6gUDrXhbHqVpuAiEA2ByF7D/YDWMJVgRSdQvvk2m7aa6dzgPK9uJ1t9/YzUQ="}]},"_npmUser":{"name":"main1479","email":"m.main2402@gmail.com"},"directories":{},"maintainers":[{"name":"main1479","email":"m.main2402@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/browser_1.4.2_1786795822270_0.9696741426804081"},"_hasShrinkwrap":false}},"time":{"created":"2026-05-29T05:59:03.673Z","modified":"2026-08-15T12:10:22.587Z","1.0.0":"2026-05-29T05:59:03.926Z","1.1.0":"2026-06-11T19:25:10.388Z","1.1.1":"2026-06-11T19:27:56.512Z","1.3.0":"2026-07-29T17:34:13.967Z","1.3.1":"2026-07-29T17:59:29.193Z","1.4.0":"2026-08-05T23:11:04.665Z","1.4.1":"2026-08-06T06:01:43.410Z","1.4.2":"2026-08-15T12:10:22.437Z"},"bugs":{"url":"https://github.com/avsbhq/a-vs-b/issues"},"license":"MIT","homepage":"https://github.com/avsbhq/a-vs-b/tree/main/packages/avsb-browser#readme","keywords":["avsb","feature-flags","ab-testing","experiments","browser","sdk"],"repository":{"type":"git","url":"git+https://github.com/avsbhq/a-vs-b.git","directory":"packages/avsb-browser"},"description":"Browser SDK for A vs B feature flags and experiments: typed flag getters, a cached datafile, persistent visitor identity, and event tracking.","maintainers":[{"name":"main1479","email":"m.main2402@gmail.com"}],"readme":"# @avsbhq/browser\n\nBrowser client SDK for the [A vs B](https://app.avsb.cloud) platform.\n\nEvaluate feature flags, run A/B experiments, and track conversion events in any browser context. For React apps, use [`@avsbhq/react`](https://www.npmjs.com/package/@avsbhq/react) which wraps this SDK with hooks and a provider.\n\n---\n\n## 1. Install\n\n```bash\nnpm install @avsbhq/browser\n```\n\nRequires a modern browser with ES2020 support. Both ESM and CJS builds are shipped, each with its own type declarations (`.d.ts` for `import`, `.d.cts` for `require`), so TypeScript resolves types under `node16`, `nodenext` and `bundler` alike.\n\nNo peer dependencies to install. `@avsbhq/core` and `@avsbhq/utils` are real dependencies and arrive with this package, so the pino and winston logger adapters, the OpenFeature provider, and the framework middleware are already on disk at `@avsbhq/utils/*` with nothing further to install.\n\n---\n\n## 2. Quickstart\n\n```ts\nimport { AvsbClient } from '@avsbhq/browser';\nimport type { Flag, InitResult } from '@avsbhq/browser';\n\ndeclare function renderNewCheckout(): void;\n\nconst client: AvsbClient = new AvsbClient({\n  sdkKey: 'sdk_production_ttqm0eaj4vth1krcb2xn',\n  context: { kind: 'user', key: 'u_123', plan: 'pro' },\n});\n\nconst result: InitResult = await client.onReady();\nif (!result.success) {\n  // Nothing could be loaded. Every flag returns the default you pass.\n  // result.error is an Error whose message names the status, the URL, and the fix.\n  console.warn(result.error?.message);\n}\n\nconst flag: Flag<boolean> = client.getBoolFlag('new-checkout-flow', false);\nif (flag.isEnabled()) {\n  renderNewCheckout();\n}\n\nclient.track('checkout_started', { revenue: 99.0 });\n\n// Flush and tear down before the app unloads\nwindow.addEventListener('beforeunload', () => {\n  void client.close();\n});\n```\n\nConstruct the client once at application boot, not per page or per component.\n\n**The constructor starts loading immediately.** You do not have to call anything to begin: `new AvsbClient({ sdkKey })` validates the key, resolves a stable visitor id, serves any cached datafile, and starts the network fetch. `onReady()` is how you wait for the result, not how you start it. Pass `autoInit: false` if you want to build the client now and load later (tests, or a server render).\n\n---\n\n## 3. SDK keys\n\nGet the SDK key for the environment you want to target from your A vs B project:\n\n1. Log in to [app.avsb.cloud](https://app.avsb.cloud)\n2. Open your project, then **Settings**, then **Environments**\n3. Copy the SDK key (format `sdk_<environment>_<id>`, for example `sdk_production_ttqm0eaj4vth1krcb2xn`)\n\nSDK keys are scoped to a single environment and grant flag-read access only: they cannot write to your project or read from other environments. They are safe to embed in browser bundles within those bounds.\n\n```\nVITE_AVSB_SDK_KEY=sdk_production_...\nNEXT_PUBLIC_AVSB_SDK_KEY=sdk_production_...\n```\n\nThe client checks the shape of the key at construction. A key that does not look like an SDK key (a pasted dashboard URL, a truncated copy, a personal access token or a service token) logs one actionable error naming what it got and where the real key lives, then carries on: the datafile request is the real test.\n\n---\n\n## 4. The whole type surface\n\nEvery public method, with its real signature. Nothing here is overloaded or\noptional-by-magic: what you see is what the compiler sees.\n\nEvery type named below is exported by the package, so your own declarations can\nimport them from the same place:\n\n```ts\nimport type {\n  AvsbClientOptions,\n  BrowserEventOptions,\n  BrowserLogLevel,\n  ClientEventMap,\n  ClientEventName,\n  DecideOption,\n  EvalContext,\n  EvaluationSource,\n  FlagDatafile,\n  FlagSnapshot,\n  GetAllFlagsOptions,\n  GetFlagOptions,\n  Logger,\n  RuleType,\n  TrackPayload,\n} from '@avsbhq/browser';\n```\n\n```ts\nclass AvsbClient {\n  constructor(options: AvsbClientOptions);\n\n  // Lifecycle\n  onReady(options?: { timeout?: number }): Promise<InitResult>;\n  isReady(): boolean;\n  getInitResult(): InitResult | null;\n  applyDatafileBootstrap(datafile: FlagDatafile): void;\n  applySnapshot(snapshot: FlagSnapshot): void;\n  refresh(): Promise<void>;\n  flush(): Promise<void>;\n  close(): Promise<void>;\n\n  // Identity\n  identify(context: EvalContext): void;\n  updateAttributes(partial: Record<string, unknown>, contextKind?: string): void;\n  alias(previousContext: EvalContext, newContext: EvalContext): void;\n  getContext(): EvalContext;\n  reset(): void;\n\n  // Reading flags\n  getFlag<T>(flagKey: string, defaultValue: T, options?: GetFlagOptions): Flag<T>;\n  getBoolFlag(flagKey: string, defaultValue: boolean, options?: GetFlagOptions): Flag<boolean>;\n  getStringFlag(flagKey: string, defaultValue: string, options?: GetFlagOptions): Flag<string>;\n  getNumberFlag(flagKey: string, defaultValue: number, options?: GetFlagOptions): Flag<number>;\n  getJsonFlag<T>(flagKey: string, defaultValue: T, options?: GetFlagOptions): Flag<T>;\n  getAllFlags(options?: GetAllFlagsOptions): Record<string, Flag>;\n\n  // Store contract for framework adapters\n  subscribe(flagKey: string, listener: () => void): () => void;\n  getSnapshot<T>(flagKey: string, defaultValue: T): Flag<T>;\n\n  // Events\n  track(eventKey: string, payload?: TrackPayload): void;\n  on<E extends ClientEventName>(event: E, cb: ClientEventMap[E]): () => void;\n  onFlagChange(\n    callback: (flagKey: string, newValue: unknown, oldValue: unknown) => void,\n  ): () => void;\n\n  // Runtime overrides (QA and local development)\n  setOverrideForUser(\n    flagKey: string,\n    contextKey: string,\n    variationKey: string,\n    contextKind?: string,\n  ): void;\n  setGlobalOverride(flagKey: string, variationKey: string): void;\n  clearOverrideForUser(flagKey: string, contextKey: string, contextKind?: string): void;\n  clearGlobalOverride(flagKey: string): void;\n  clearAllOverrides(): void;\n\n  // Extras (debounce, retry, snapshots, storage adapters, OpenFeature)\n  readonly utils: AvsbUtils;\n}\n```\n\n`on` and `subscribe` both return their own unsubscribe function: `() => void`.\nEvery method that can fail resolves rather than rejecting, so no call above\nneeds a `try` block except your own code inside a callback.\n\n### `AvsbClientOptions`\n\n```ts\ninterface AvsbClientOptions {\n  sdkKey: string;\n  context?: EvalContext; // THE identity model; see the note below\n  pollingInterval?: number; // default 60_000, clamped to a 5_000 minimum\n  autoRefresh?: boolean; // default true\n  bootstrap?: FlagDatafile;\n  cdnHost?: string; // default 'https://cdn.avsb.cloud'\n  autoInit?: boolean; // default true: start loading from the constructor\n  initTimeout?: number; // default 10_000, per datafile request\n  cache?: boolean; // default true: localStorage datafile cache\n  cacheMaxAgeMs?: number; // default 604_800_000 (7 days)\n  adoptSnippetVisitorId?: boolean; // default true\n  pauseWhenHidden?: boolean; // default true\n  refetchOnFocus?: boolean; // default true\n  maxPendingEvents?: number; // default 100\n  events?: BrowserEventOptions;\n  logger?: Logger;\n  logLevel?: BrowserLogLevel; // 'silent' | 'debug' | 'info' | 'warn' | 'error'\n  onError?: (e: Error, source: 'init' | 'poll' | 'stream' | 'track' | 'eval') => void;\n  streaming?: boolean; // default false\n  streamingEndpoint?: string; // normally published in the datafile\n}\n```\n\n`context` is the ONE identity model. The parallel `attributes` and\n`userIdAttribute` options were deleted (breaking change B5): two models meant\nevery adapter and doc had to teach both, and the second could not express\nmulti-context at all. Migration is mechanical:\n\n```ts\nconst sdkKey = 'sdk_production_ttqm0eaj4vth1krcb2xn';\n\n// Was:  new AvsbClient({ sdkKey, attributes: { userId: 'u_1', plan: 'pro' } })\nnew AvsbClient({ sdkKey, context: { kind: 'user', key: 'u_1', plan: 'pro' } });\n```\n\n```ts\ninterface BrowserEventOptions {\n  flushInterval?: number; // default 2000\n  batchSize?: number; // default 10\n  maxQueueSize?: number; // default 500\n  retryAttempts?: number; // default 3\n}\n\ninterface GetFlagOptions {\n  fireExposure?: boolean; // default true\n  readOnly?: boolean; // default false: render-safe, cached, no side effects\n  decideOptions?: DecideOption[]; // DISABLE_EXPOSURE is honoured here\n}\n\ninterface GetAllFlagsOptions {\n  fireExposures?: boolean; // default false\n  context?: EvalContext; // evaluate for someone else without rebinding\n}\n```\n\n`GetAllFlagsOptions` has no `decideOptions`: a bulk read already suppresses\nexposures by default, so `DISABLE_EXPOSURE` would just be a second name for\n`fireExposures: false`.\n\n### `Flag<T>`\n\nThe return type of every read. Exact shape, from `@avsbhq/core`:\n\n```ts\ninterface Flag<T = unknown> {\n  /** The variation value typed against the default. */\n  readonly value: T;\n  /** Variation key (null if served the default or not found). */\n  readonly variationKey: string | null;\n  /** Why this value was produced. */\n  readonly source: EvaluationSource;\n  /** Rule that matched (null when source is 'default'/'not_found'/etc). */\n  readonly ruleId: string | null;\n  /** Rule type that matched (null when no rule applied). */\n  readonly ruleType: RuleType | null;\n  /** Structured reasons for this decision. */\n  readonly reasons: string[];\n  /** ms-epoch when evaluated. */\n  readonly evaluatedAt: number;\n  /** µs elapsed in the evaluator. */\n  readonly durationMicros: number;\n  /** Convenience: a real decision produced a truthy value. */\n  isEnabled(): boolean;\n  /** Convenience: false for 'not_found' and for 'not_ready'. */\n  exists(): boolean;\n}\n\ntype RuleType = 'targeted_delivery' | 'ab_test' | 'holdout' | 'bandit';\n```\n\nThe object is frozen. `reasons` is never null; it is an empty array only when\nthe evaluator had nothing to say.\n\n### `EvaluationSource`, member by member\n\n```ts\ntype EvaluationSource =\n  | 'datafileOverride'\n  | 'runtimeOverride'\n  | 'sticky'\n  | 'rule'\n  | 'holdout'\n  | 'bandit'\n  | 'default'\n  | 'not_found'\n  | 'disabled'\n  | 'not_ready';\n```\n\n| Member             | Meaning                                                                | `isEnabled()`   | `exists()` |\n| ------------------ | ---------------------------------------------------------------------- | --------------- | ---------- |\n| `datafileOverride` | A per-user override configured in the dashboard matched.               | value-dependent | true       |\n| `runtimeOverride`  | `setOverrideForUser` or `setGlobalOverride` matched.                   | value-dependent | true       |\n| `sticky`           | A previously stored assignment was reused.                             | value-dependent | true       |\n| `rule`             | A targeting rule or A/B rule matched.                                  | value-dependent | true       |\n| `holdout`          | The visitor is in a holdout, so the holdout variation was served.      | value-dependent | true       |\n| `bandit`           | A bandit rule picked the variation.                                    | value-dependent | true       |\n| `default`          | The flag exists, nothing matched, so its default variation was served. | false           | true       |\n| `disabled`         | The flag exists but is switched off in this environment.               | false           | true       |\n| `not_found`        | The datafile loaded and does not contain this key. Check the key name. | false           | false      |\n| `not_ready`        | The SDK has no datafile yet. Await `onReady()`, or pass `bootstrap`.   | false           | false      |\n\n\"value-dependent\" means `isEnabled()` is `Boolean(flag.value)`: a real decision\nwas made, so the answer is whatever that decision produced.\n\n### `InitResult`\n\n```ts\ninterface InitResult {\n  success: boolean;\n  /** 'cache' deliberately removed; never returned by current code. */\n  source: 'network' | 'bootstrap' | 'timeout' | 'error';\n  error?: Error;\n  /**\n   * Serving STALE-BUT-REAL flag values: a bootstrap or a cached datafile is\n   * loaded and readable, and a refresh of it failed. `success` is `true`\n   * because flags are answerable; polling continues in the background.\n   *\n   * It is never set when there is nothing to serve. A failed fetch with no\n   * bootstrap and no cache resolves `{success: false, source: 'error'}` (spec\n   * §1.2, breaking change B1); reading \"degraded\" as \"the SDK is fine on\n   * defaults\" was the bug that let a typo'd SDK key look healthy.\n   */\n  degraded?: boolean;\n}\n```\n\n### Context types\n\n```ts\ninterface ContextMeta {\n  /** Attribute keys redacted from exposure events and decision-log reasons. */\n  privateAttributes?: string[];\n  /** Mark this context as anonymous (suppresses some integrations). */\n  anonymous?: boolean;\n}\n\ninterface SingleContext {\n  /** Context kind. 'user' is conventional but not enforced. */\n  kind: string;\n  /** Unique key within the kind (bucketing identifier for this kind). */\n  key: string;\n  /** Optional human-readable name (UI display only, not used in eval). */\n  name?: string;\n  /** Reserved metadata. */\n  _meta?: ContextMeta;\n  /** Targeting attributes. Reserved keys: 'kind', 'key', 'name', '_meta'. */\n  [attribute: string]: unknown;\n}\n\ninterface MultiContext {\n  kind: 'multi';\n  /** Keyed by context kind. At least one entry required (other than 'kind'). */\n  [contextKind: string]: SingleContext | 'multi';\n}\n\ntype EvalContext = SingleContext | MultiContext;\n```\n\nBecause `SingleContext` carries an index signature, attribute values are typed\n`unknown`. Narrow before use, or build contexts with `defineContextSchema` from\n`@avsbhq/utils` for a typed factory:\n\n```ts\nconst ctx: EvalContext = client.getContext();\nconst plan: unknown = 'plan' in ctx ? ctx['plan'] : undefined;\nif (typeof plan === 'string') {\n  // plan is a string here\n}\n```\n\n### `TrackPayload`\n\n```ts\ninterface TrackPayload {\n  /** Numeric metric value. Generalises 'revenue': any quantity. */\n  value?: number;\n  /** Free-form properties forwarded to analytics. */\n  properties?: Record<string, unknown>;\n  /** Override the context to track against (server SDK only). */\n  context?: EvalContext;\n}\n```\n\n`context` is ignored by this SDK: the browser client always tracks against its\nbound context. It exists on the shared type because the server SDKs honour it.\n\n### Typed flag keys\n\nEvery key parameter above is `string` until you generate your keys. Generate\nthem and it becomes the union of this project's real flag keys, so a typo is a\ncompile error and your editor completes the list:\n\n```bash\nnpx avsb codegen --output src/generated/flags.ts\n```\n\nThe generated file declares your flags twice on purpose: an importable\n`FlagValues` interface for payload types, and a `declare global` block that\nteaches every getter in this package which keys exist. Its important parts:\n\n```text\n// AUTO-GENERATED by @avsbhq/cli codegen. Do not edit by hand.\n\nexport interface FlagValues {\n  'checkout-v2': boolean\n  'hero-copy': 'control' | 'variant-a'\n  'theme': { primary: string }\n}\n\ndeclare global {\n  interface AvsbFlags {\n    'checkout-v2': boolean\n    'hero-copy': 'control' | 'variant-a'\n    'theme': { primary: string }\n  }\n}\n```\n\n```ts\nimport type { FlagValues } from './generated/flags';\n\nconst typedCheckout = client.getBoolFlag('checkout-v2', false);\n\n// A JSON flag types its payload from the generated table:\nconst typedTheme = client.getJsonFlag<FlagValues['theme']>('theme', { primary: '#111' });\n\n// Once the generated file exists, this line stops compiling:\n// 'chekcout-v2' is not a flag key.\nconst typedTypo = client.getBoolFlag('chekcout-v2', false);\n```\n\nNothing changes for a project that never runs `codegen`: with no generated file\nthe table is empty, every key parameter is exactly `string`, and every call you\nhave already written compiles unchanged. For a key computed at runtime, widen\ndeliberately with `key as AvsbFlagKey` (that type is exported from\n`@avsbhq/browser` and from `@avsbhq/core`).\n\n---\n\n## 5. Lifecycle and readiness\n\n```ts\nclient.isReady(); // boolean: are flags readable right now?\nawait client.onReady(); // Promise<InitResult>, never rejects\nclient.getInitResult(); // InitResult | null, the settled outcome so far\n```\n\n`isReady()` and `onReady()` answer different questions, and the difference matters when caching is on:\n\n- **`isReady()`** is true as soon as flags can be read from something real: a `bootstrap` datafile, a cached datafile from a previous visit, or the network response. It is synchronous, so a framework adapter can decide what to render on the first pass.\n- **`onReady()`** resolves when the init attempt settles. With a cached datafile, that is after the background refresh finishes, so `isReady()` can be true while `onReady()` is still pending.\n\n`InitResult` shapes:\n\n| Result                                               | Meaning                                                                                                                   |\n| ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |\n| `{ success: true, source: 'network' }`               | The datafile was fetched (or confirmed unchanged with a 304).                                                             |\n| `{ success: true, source: 'bootstrap' }`             | You supplied the datafile. No fetch was needed.                                                                           |\n| `{ success: true, degraded: true, source: 'error' }` | A cached datafile is being served after a failed refresh. Flags work; they may be stale.                                  |\n| `{ success: false, source: 'error', error }`         | Nothing could be loaded. Every flag returns the default you pass. `error.message` names the status, the URL, and the fix. |\n| `{ success: false, source: 'timeout', error }`       | Your `onReady({ timeout })` elapsed first.                                                                                |\n\n### `onReady({ timeout })`\n\n```ts\nconst result = await client.onReady({ timeout: 2000 });\n```\n\nThe timeout bounds how long **you** wait, not what the SDK does: loading continues in the background, so a client that timed out at 2 seconds can be ready a moment later. Watch `on('ready', ...)` or `isReady()` if you want to know when that happens.\n\n### Events\n\n```ts\nclient.on('ready', (result) => {}); // fires once, when flags become readable\nclient.on('error', (error) => {}); // init or refresh failure, with an actionable message\nclient.on('configUpdate', ({ publishedAt, reason }) => {}); // a new datafile was applied\nclient.on('flagChange', ({ flagKey, previousValue, newValue }) => {});\n```\n\nWhen the first datafile lands, `flagChange` fires once per flag (moving from \"unknown\" to its value), and every per-flag subscriber is notified. After that, only flags whose value actually changed fire.\n\n---\n\n## 6. Identity\n\nAnonymous visitors get a **persisted** id (`localStorage`, with a cookie fallback), so a returning visitor buckets into the same variation instead of being re-randomised on every page load.\n\nIf the A vs B web snippet is installed on the same page, the client adopts its `_avsb_visitor` id so feature flag exposures and web experiment exposures join to one visitor in your results. The SDK only reads that cookie; it never writes it. Pass `adoptSnippetVisitorId: false` to opt out.\n\n### `identify(context: EvalContext): void`\n\nReplace the current evaluation context entirely. All flags are re-evaluated and any changed flags emit a `flagChange` event. Bucketing rehashes immediately.\n\n```ts\ndeclare const user: { id: string; email: string; plan: string };\n\n// After user signs in\nclient.identify({\n  kind: 'user',\n  key: user.id,\n  email: user.email,\n  plan: user.plan,\n});\n```\n\n### `updateAttributes(partial: Record<string, unknown>, contextKind?: string): void`\n\nMerge new attributes into the current context without replacing the entire context. `contextKind` defaults to `'user'`. When the kind is not present yet, it is added as a new sub-context and the context becomes a multi-context.\n\n```ts\nclient.updateAttributes({ plan: 'enterprise' });\nclient.updateAttributes({ tier: 3 }, 'organization');\n```\n\n### `alias(previousContext: EvalContext, newContext: EvalContext): void`\n\nMark the moment a signup or login claimed an anonymous session. Synchronous: it queues one `avsb_alias` event and returns. It used to be declared `async`, which implied awaiting mattered when the promise resolved immediately after a synchronous `track`.\n\nWhat it does NOT do, precisely:\n\n- It does not rewrite variation assignments, sticky bucketing, or past exposures. Nothing already decided for either identity changes.\n- It does not store the PREVIOUS key. The conversion wire (`/v1/collect/metric-batch`) has no properties column, so a retroactive session join is not available yet. The event is attributed to whichever identity the client is bound to, so call `identify(newContext)` first; the SDK warns once if you do not.\n\nTo carry bucketing across a login, evaluate with the SAME context key on both sides: keep using the anonymous visitor id after sign-in, or set the account id before the first evaluation.\n\n```ts\ndeclare const anonymousId: string;\ndeclare const user: { id: string };\n\nclient.identify({ kind: 'user', key: user.id });\nclient.alias({ kind: 'user', key: anonymousId }, { kind: 'user', key: user.id });\n```\n\n### `reset(): void`\n\nReturn to a **new** anonymous identity and clear runtime overrides. Call this on logout: the persisted visitor id is rotated, so the next anonymous session is genuinely a different visitor rather than the person who just signed out.\n\n```ts\nclient.reset();\n```\n\nOne caveat when the A vs B web snippet is installed on the same page: the\nsnippet owns the visitor cookie, and this SDK only reads it. `reset()` therefore\nrotates this SDK's own persisted id and rebinds its context, but the next page\nload adopts the snippet's visitor id again. Pass `adoptSnippetVisitorId: false`\nif you want the SDK's identity to be entirely its own.\n\n### `getContext(): EvalContext`\n\nThe current evaluation context. Treat it as read-only: mutating it does not\nre-evaluate anything. Use `identify` or `updateAttributes` to change identity.\n\n---\n\n## 7. Multi-context\n\nEvaluate flags simultaneously against multiple context kinds, such as a user, their organization, and their device:\n\n```ts\nclient.identify({\n  kind: 'multi',\n  user: { kind: 'user', key: 'u_123', plan: 'pro' },\n  organization: { kind: 'organization', key: 'org_456', tier: 'enterprise' },\n  device: { kind: 'device', key: 'd_abc', os: 'ios' },\n});\n```\n\nWhen a multi-context is active, rules can target any context kind. A rule might bucket on `user.key` while matching an audience condition on `organization.tier`. The `hashAttribute` in each rule's datafile entry controls which context kind drives the bucket.\n\n---\n\n## 8. Reading flags\n\nAll evaluation methods return a `Flag<T>` object rather than a raw value. Access `.value` for the primitive or use the convenience methods on the `Flag`.\n\n### Typed variants\n\n```ts\ngetBoolFlag(flagKey: string, defaultValue: boolean, options?: GetFlagOptions): Flag<boolean>\ngetStringFlag(flagKey: string, defaultValue: string, options?: GetFlagOptions): Flag<string>\ngetNumberFlag(flagKey: string, defaultValue: number, options?: GetFlagOptions): Flag<number>\ngetJsonFlag<T>(flagKey: string, defaultValue: T, options?: GetFlagOptions): Flag<T>\n```\n\nEach typed variant checks the value against the type the platform declared for\nthat flag. **On a mismatch it never throws:** it logs one warning naming the\nflag, the getter used, and the declared type, then returns\n`Flag<T>` carrying your `defaultValue` with `source: 'not_found'`. Asking for a\nstring flag with `getNumberFlag` is therefore safe, and visible.\n\n`getJsonFlag<T>` does not validate the shape of `T` at runtime: it checks only\nthat the platform declared the flag as JSON. Validate the payload yourself (zod\nor a type guard) if it crosses a trust boundary.\n\n```ts\nconst darkMode: Flag<boolean> = client.getBoolFlag('dark-mode', false);\nconst theme: Flag<string> = client.getStringFlag('theme', 'light');\nconst maxItems: Flag<number> = client.getNumberFlag('max-results', 25);\n\ninterface ApiConfig {\n  timeout: number;\n  retries: number;\n}\nconst config: Flag<ApiConfig> = client.getJsonFlag<ApiConfig>('api-config', {\n  timeout: 5000,\n  retries: 3,\n});\nconst timeout: number = config.value.timeout;\n```\n\n### Generic `getFlag<T>`\n\n```ts\ngetFlag<T>(flagKey: string, defaultValue: T, options?: GetFlagOptions): Flag<T>\n```\n\nNo runtime type check runs here: whatever the datafile says is returned, typed\nas `T`. Use it when the type is dynamic, and prefer a typed variant otherwise.\n`T` comes from your explicit type argument when you give one, and from the\ndefault value when you do not:\n\n```ts\nconst inferred: Flag<boolean> = client.getFlag('checkout-v2', false);\nconst explicit: Flag<string | null> = client.getFlag<string | null>('banner-copy', null);\n```\n\n### `getAllFlags(options?: GetAllFlagsOptions): Record<string, Flag>`\n\nEvery flag in the datafile, keyed by flag key, each as a `Flag<unknown>`\n(`Flag`'s default type argument). Exposures are suppressed by default, and the\nreturned object is the same object between changes, so it is safe to read on\nevery render. Before the SDK is ready this returns `{}` and logs one warning\nsaying why, rather than looking like a project with no flags.\n\n```ts\nconst all: Record<string, Flag> = client.getAllFlags();\nconst withExposures: Record<string, Flag> = client.getAllFlags({ fireExposures: true });\n\n// Evaluate for someone else without changing the bound context.\nconst forOtherUser: Record<string, Flag> = client.getAllFlags({\n  context: { kind: 'user', key: 'u_999', plan: 'pro' },\n});\n\nconst value: unknown = all['dark-mode']?.value;\n```\n\n### Render-safe reads\n\nFor code that runs during render (a React hook, a Vue computed, a Svelte store), use read-only mode:\n\n```ts\n// generic\nconst snapshot: Flag<boolean> = client.getSnapshot('checkout-v2', false);\n// typed, with the same runtime type check as a normal typed read\nconst typed: Flag<boolean> = client.getBoolFlag('checkout-v2', false, { readOnly: true });\n```\n\nA read-only read:\n\n- returns the **same object** until that flag's value changes, which is what `useSyncExternalStore` and every equivalent store API require;\n- fires **no exposure** and emits no `evaluation` event, because a render is a read, not a decision.\n\nPair it with `subscribe` to re-read when the value moves:\n\n```ts\nconst unsubscribe: () => void = client.subscribe('checkout-v2', () => {\n  // this flag changed, or the SDK just became ready\n});\n```\n\nFire the exposure once, where the decision is actually shown to the user, with a normal (non read-only) read.\n\n### The store contract, for framework adapters\n\nIf you are writing an integration (or reading `@avsbhq/react`, `@avsbhq/vue`,\n`@avsbhq/svelte`, `@avsbhq/solid`, `@avsbhq/angular`, `@avsbhq/react-native`, or\n`@avsbhq/next`), these methods are the whole contract:\n\n```ts\nsubscribe(flagKey: string, listener: () => void): () => void\ngetSnapshot<T>(flagKey: string, defaultValue: T): Flag<T>\ngetBoolFlag(flagKey: string, defaultValue: boolean, options?: GetFlagOptions): Flag<boolean>\n// and the other typed getters, called with { readOnly: true }\nisReady(): boolean\ngetInitResult(): InitResult | null\n```\n\nGuaranteed:\n\n1. **Identity stability.** Two read-only reads of the same key with the same\n   default return the same object, until that flag's value, variation, source,\n   or rule changes. Different defaults for the same key get their own stable\n   objects.\n2. **Wake-up on ready.** Every subscriber is called once when the first datafile\n   is applied, including subscribers for keys the datafile does not contain\n   (their answer moves from `not_ready` to `not_found`).\n3. **Targeted notification.** After that, only subscribers of flags whose value\n   changed are called. An unrelated flag changing neither wakes you nor changes\n   your snapshot's identity.\n4. **No side effects on read.** Read-only reads fire no exposure and emit no\n   `evaluation` event, so a discarded or replayed render cannot corrupt results.\n5. **Synchronous readiness.** `isReady()` and `getInitResult()` answer without\n   awaiting, so a first render can choose between a value, a loading state, and\n   an error state.\n\nA React hook is then exactly this:\n\n```ts\nimport { useSyncExternalStore } from 'react';\nimport { AvsbClient } from '@avsbhq/browser';\nimport type { Flag } from '@avsbhq/browser';\n\nexport function useBoolFlag(\n  client: AvsbClient,\n  flagKey: string,\n  defaultValue: boolean,\n): Flag<boolean> {\n  return useSyncExternalStore(\n    (onStoreChange: () => void) => client.subscribe(flagKey, onStoreChange),\n    () => client.getBoolFlag(flagKey, defaultValue, { readOnly: true }),\n    () => client.getBoolFlag(flagKey, defaultValue, { readOnly: true }),\n  );\n}\n```\n\n### `DecideOption`: per-call overrides\n\n```ts\nimport { DecideOption } from '@avsbhq/browser';\nimport type { Flag } from '@avsbhq/browser';\n\nconst flag: Flag<boolean> = client.getBoolFlag('my-flag', false, {\n  decideOptions: [DecideOption.DISABLE_EXPOSURE],\n});\n```\n\n- `DISABLE_EXPOSURE`: do not fire an exposure event for this call. Same as `{ fireExposure: false }`.\n- `INCLUDE_REASONS`: accepted and always satisfied. The browser client collects `flag.reasons` on every evaluation.\n\n---\n\n## 9. Caching and offline behaviour\n\nThe client caches the datafile in `localStorage` and serves it on the next visit while a refresh runs behind it (cache-then-network). That makes second loads instant and keeps flags working when the network is unavailable.\n\n```ts\nconst client = new AvsbClient({\n  sdkKey: 'sdk_production_ttqm0eaj4vth1krcb2xn',\n  cache: false, // opt out of the datafile cache entirely\n  cacheMaxAgeMs: 86_400_000, // ignore anything cached more than a day ago (default 7 days)\n});\n```\n\nDetails worth knowing:\n\n- Refreshes are conditional. The cached ETag is sent as `If-None-Match`, so an unchanged datafile costs a 304 with no body.\n- Only network responses are cached. A `bootstrap` datafile you pass in is never written to the cache.\n- A cached datafile that fails validation is discarded rather than trusted.\n- If a refresh fails while a cached datafile is being served, `onReady()` resolves `{ success: true, degraded: true }`. Flags keep working; they may be stale.\n- Polling is clamped to a 5 second minimum, jittered, paused while the tab is hidden, and refreshed when the window regains focus.\n\n```ts\nconst client = new AvsbClient({\n  sdkKey: 'sdk_production_ttqm0eaj4vth1krcb2xn',\n  pollingInterval: 60_000, // default 60s, clamped to 5s minimum\n  pauseWhenHidden: false, // keep polling in background tabs\n  refetchOnFocus: false, // do not refetch when the tab regains focus\n  autoRefresh: false, // no polling at all\n});\n```\n\n---\n\n## 10. Tracking events\n\n```ts\nclient.track('purchase_completed', { revenue: 199.0 }); // money\nclient.track('items_added', { value: 3 }); // a quantity\nclient.track('checkout_started'); // a count\n```\n\n`TrackPayload` fields:\n\n- `revenue`: money in decimal MAJOR units of the project currency (49.99, not 4999). Lands in the storage column `revenue`.\n- `value`: numeric metric value for average-value metrics (items, seats, seconds). Lands in the SEPARATE storage column `value`.\n- `properties`: accepted by the shared type, but NOT stored for conversions. The ingestion contract for `/v1/collect/metric-batch` declares no properties column, so anything sent under that name is dropped at the edge; this SDK does not put it on the wire. Exposure events do carry properties.\n\n`revenue` and `value` are two columns end to end, so one conversion can carry money, a quantity, or both. Earlier releases mapped `value` into the revenue column, which recorded a quantity as money.\n\nDelivery is durable:\n\n- Events are batched and flushed on an interval, and on tab hide via `sendBeacon`.\n- A failed send is retried on an exponential backoff curve with jitter, honouring `Retry-After`.\n- A batch that still fails goes back on the queue and is retried on the next flush, instead of being dropped.\n- Queues are bounded. Past the cap the oldest events are dropped and one warning names the endpoint that is not answering.\n- Events tracked before the client is ready are held (bounded) and flushed as soon as it is.\n\n```ts\nconst client = new AvsbClient({\n  sdkKey: 'sdk_production_ttqm0eaj4vth1krcb2xn',\n  maxPendingEvents: 100, // held before the SDK is ready\n  events: {\n    flushInterval: 2000,\n    batchSize: 10,\n    maxQueueSize: 500,\n    retryAttempts: 3,\n  },\n});\n```\n\n---\n\n## 11. Logging and error handling\n\n### Defaults\n\nThe default logger writes to the console at `warn` level in development and is silent everywhere else. Development is detected in this order:\n\n1. `globalThis.__AVSB_DEV__` when you set it. This is the bundler hook:\n   - Vite: `define: { __AVSB_DEV__: 'import.meta.env.DEV' }`\n   - webpack: `new DefinePlugin({ __AVSB_DEV__: 'process.env.NODE_ENV !== \"production\"' })`\n2. `NODE_ENV` when a real `process` exists (Node, SSR, test runners). `production` and `test` are silent; anything else is development.\n3. Otherwise, the host: `localhost`, `127.0.0.1`, `*.local`, and private LAN addresses count as development.\n\nOverride it explicitly whenever you want:\n\n```ts\nconst client = new AvsbClient({\n  sdkKey: 'sdk_production_ttqm0eaj4vth1krcb2xn',\n  logLevel: 'debug', // or 'silent' to turn SDK logging off entirely\n});\n```\n\n### Custom logger\n\n```ts\nimport { AvsbClient, createLogger, consoleTransport } from '@avsbhq/browser';\nimport type { Logger } from '@avsbhq/browser';\n\nconst logger: Logger = createLogger({\n  level: 'info',\n  transports: [consoleTransport('warn')],\n});\n\nconst client = new AvsbClient({\n  sdkKey: 'sdk_production_ttqm0eaj4vth1krcb2xn',\n  logger,\n});\n```\n\nFor pino or winston adapters:\n\n```ts\nimport { createLogger } from '@avsbhq/browser';\nimport { createPinoTransport } from '@avsbhq/utils/log/pino';\nimport pino from 'pino';\n\nconst logger = createLogger({\n  level: 'info',\n  transports: [createPinoTransport({ logger: pino(), level: 'info' })],\n});\n```\n\n### `onError` callback\n\n```ts\ndeclare const myMonitoring: {\n  captureException: (e: Error, ctx: { tags: Record<string, string> }) => void;\n};\n\nconst client = new AvsbClient({\n  sdkKey: 'sdk_production_ttqm0eaj4vth1krcb2xn',\n  onError: (err: Error, source: 'init' | 'poll' | 'stream' | 'track' | 'eval') => {\n    myMonitoring.captureException(err, { tags: { source } });\n  },\n});\n```\n\n`source` is one of `'init' | 'poll' | 'stream' | 'track' | 'eval'`. Network failures arrive as `AvsbNetworkError` and timeouts as `AvsbTimeoutError`, both exported from this package, so you can branch on the cause instead of matching message strings:\n\n```ts\nimport { AvsbNetworkError, AvsbTimeoutError } from '@avsbhq/browser';\n\nfunction describe(error: unknown): string {\n  if (error instanceof AvsbTimeoutError) return `slow host: ${error.host}`;\n  if (error instanceof AvsbNetworkError) return `unreachable host: ${error.host}`;\n  return error instanceof Error ? error.message : String(error);\n}\n```\n\n---\n\n## 12. Server rendering and bootstrap\n\nPre-fetch the datafile on the server and pass it as `bootstrap` to skip the initial network round-trip on the client:\n\n```ts\n// Server\nimport { fetchDatafile } from '@avsbhq/browser/server';\nimport type { FlagDatafile } from '@avsbhq/browser';\n\ndeclare const sdkKey: string;\n\n// fetchDatafile(sdkKey: string, options?: { cdnHost?: string; timeout?: number;\n//   onError?: (error: Error) => void }): Promise<FlagDatafile | null>\nconst datafile: FlagDatafile | null = await fetchDatafile(sdkKey, { timeout: 2000 });\n// Serialise `datafile` into the page. It is null when the fetch failed, in\n// which case the browser client falls back to fetching it itself.\n```\n\n```ts\n// Client\nimport { AvsbClient } from '@avsbhq/browser';\nimport type { EvalContext, FlagDatafile } from '@avsbhq/browser';\n\ndeclare const userContext: EvalContext;\ndeclare const datafileFromServer: FlagDatafile;\n\nconst client = new AvsbClient({\n  sdkKey: 'sdk_production_ttqm0eaj4vth1krcb2xn',\n  context: userContext,\n  bootstrap: datafileFromServer,\n});\n// isReady() is already true; onReady() resolves with source: 'bootstrap'.\n```\n\nBootstrap guarantees the same variation values on the server render and the initial client render, preventing hydration mismatches.\n\nYou can also apply a datafile after construction, which is what the bootstrap-blob helper in `@avsbhq/utils` does:\n\n```ts\nclient.applyDatafileBootstrap(datafileFromServer);\n```\n\nFor Next.js App Router, use `@avsbhq/next` and its server components instead of wiring this manually.\n\n---\n\n## 13. Streaming updates\n\nStreaming is opt-in in the browser. The endpoint is published by the platform in the datafile, so nothing needs configuring:\n\n```ts\nconst client = new AvsbClient({\n  sdkKey: 'sdk_production_ttqm0eaj4vth1krcb2xn',\n  streaming: true,\n});\n```\n\nIf the datafile carries no stream endpoint and you passed none, the client says so in one log line and stays on polling. It never guesses a host.\n\n---\n\n## 14. Graceful shutdown\n\n```ts\nawait client.flush(); // drain queued events, sendBeacon or fetch({ keepalive: true })\nawait client.close(); // flush, stop polling, disconnect streaming, destroy the tracker\n```\n\n`close()` flushes internally, so calling both is safe but redundant. `track()` after `close()` is dropped with one warning rather than silently.\n\n---\n\n## 15. Testing\n\nInstall `@avsbhq/test` and use `createMockClient` to replace the real SDK in tests:\n\n```ts\nimport { createMockClient } from '@avsbhq/test';\n\nconst client = createMockClient({\n  flags: { 'checkout-v2': true },\n});\n\nexpect(client.getBoolFlag('checkout-v2', false).value).toBe(true);\n```\n\nTo test against the real client without a network, pass a `bootstrap` datafile and turn the extras off:\n\n```ts\nimport { AvsbClient } from '@avsbhq/browser';\nimport type { FlagDatafile } from '@avsbhq/browser';\n\ndeclare const myTestDatafile: FlagDatafile;\n\nconst client = new AvsbClient({\n  sdkKey: 'sdk_test_000000000000',\n  bootstrap: myTestDatafile,\n  autoRefresh: false,\n  cache: false,\n  logLevel: 'silent',\n});\n```\n\n---\n\n## 16. Breaking changes in this release\n\n| Change                                                                                                                                                                               | What to do                                                                                                            |\n| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------- |\n| A failed init with nothing to fall back on now resolves `{ success: false }`. It used to resolve `{ success: true, degraded: true }`, which made a typo'd SDK key look like success. | Branch on `result.success`. `degraded: true` now means only \"serving a stale cached datafile after a failed refresh\". |\n| The default logger writes to the console at `warn` level in development. It used to be silent everywhere.                                                                            | Nothing, unless you want silence: pass `logLevel: 'silent'`.                                                          |\n| A read before the datafile loads reports `source: 'not_ready'`. It used to report `not_found`.                                                                                       | Treat `not_ready` as \"ask again after `onReady()`\".                                                                   |\n| `getAllFlags({ context })` now evaluates against the context you pass. It used to ignore it and answer for the bound context.                                                        | Nothing, unless you were relying on the bug.                                                                          |\n| The constructor starts loading the datafile. It used to wait for `onReady()`.                                                                                                        | Nothing. Pass `autoInit: false` if you need the old timing.                                                           |\n| `DecideOption` members other than `DISABLE_EXPOSURE` and `INCLUDE_REASONS` are gone.                                                                                                 | Remove them. They were never implemented.                                                                             |\n\n---\n\n## 17. Migration\n\n### From LaunchDarkly JS\n\n| LaunchDarkly JS                          | `@avsbhq/browser`                                                          |\n| ---------------------------------------- | -------------------------------------------------------------------------- |\n| `initialize(clientId, context, options)` | `new AvsbClient({ sdkKey, context })`                                      |\n| `client.waitForInitialization()`         | `await client.onReady()`                                                   |\n| `bootstrap: 'localStorage'`              | on by default (`cache: true`)                                              |\n| `client.variation('key', default)`       | `client.getBoolFlag('key', false).value`                                   |\n| `client.variationDetail('key', default)` | `client.getFlag('key', default)`                                           |\n| `client.identify(context)`               | `client.identify(context)`                                                 |\n| `client.track('event', { metricValue })` | `client.track('event', { revenue })` for money, `{ value }` for a quantity |\n| `client.on('change:key', cb)`            | `client.subscribe('key', cb)` or `client.on('flagChange', cb)`             |\n| `client.flush()`                         | `await client.flush()`                                                     |\n\nKey differences:\n\n- `getFlag` returns a `Flag<T>` object, not a raw value. Access `.value` for the primitive.\n- All typed variants (`getBoolFlag`, `getStringFlag`, and so on) require an explicit `defaultValue`.\n- `track` splits the number in two: `revenue` for money, `value` for an average-value quantity. LaunchDarkly's single `metricValue` maps to whichever of the two you mean.\n- Multi-context is a first-class concept with kinded `EvalContext` rather than a separate wrapper type.\n\n### From Statsig JS\n\n| Statsig JS                                           | `@avsbhq/browser`                                                                                                  |\n| ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |\n| `Statsig.initialize(clientKey, user)`                | `new AvsbClient({ sdkKey, context })`                                                                              |\n| `Statsig.checkGate('key')`                           | `client.getBoolFlag('key', false).isEnabled()`                                                                     |\n| `Statsig.getExperiment('key').get('param', default)` | `client.getFlag('key', default).value`                                                                             |\n| `Statsig.logEvent('event', value, metadata)`         | `client.track('event', { value })` for a quantity, `{ revenue }` for money. Metadata is not stored on conversions. |\n| `Statsig.updateUser(user)`                           | `client.identify(context)`                                                                                         |\n","readmeFilename":"README.md"}