@avsbhq/browser
v1.1.1
Published
Browser client SDK for the [A vs B](https://app.avsb.cloud) platform.
Readme
@avsbhq/browser
Browser client SDK for the A vs B platform.
Evaluate feature flags, run A/B experiments, and track conversion events in any browser context. For React apps, use @avsbhq/react which wraps this SDK with hooks and a provider.
1. Install
npm install @avsbhq/browserRequires a modern browser with ES2020 support. Both ESM and CJS builds are shipped.
No mandatory peer dependencies. Install @avsbhq/utils if you need pino/winston logger adapters, OpenFeature integration, or middleware helpers.
2. Quickstart
import { AvsbClient } from '@avsbhq/browser'
const client = new AvsbClient({
sdkKey: 'sdk_production_...',
context: { kind: 'user', key: 'u_123', plan: 'pro' },
})
const result = await client.onReady()
if (!result.success) {
// degraded: client still works on defaults; poll continues
}
const flag = client.getBoolFlag('new-checkout-flow', false)
if (flag.isEnabled()) {
renderNewCheckout()
}
client.track('checkout_started', { value: 99.0, properties: { step: 'cart' } })
// Flush and tear down before app unloads
window.addEventListener('beforeunload', () => { void client.close() })Construct the client once at application boot — not per page or per component.
3. SDK keys
Get the SDK key for the environment you want to target from your A vs B project's Environments page:
- Log in to app.avsb.cloud
- Open your project and go to Environments
- Copy the SDK key (format
sdk_<environment>_<id>, e.g.sdk_production_clp1a2b3c4d5e6)
SDK 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.
VITE_AVSB_SDK_KEY=sdk_production_...
NEXT_PUBLIC_AVSB_SDK_KEY=sdk_production_...Rotate keys from the Environments page. After rotation, the old key stops working for new SDK fetches within 5 minutes. In-memory datafiles that are already loaded continue to function until the next poll.
4. Identity
identify(context)
Replace the current evaluation context entirely. All flags are re-evaluated and any changed flags emit a flagChange event. Bucketing rehashes immediately.
// After user signs in
client.identify({
kind: 'user',
key: user.id,
email: user.email,
plan: user.plan,
})updateAttributes(partial, contextKind?)
Merge new attributes into the current context without replacing the entire context. Defaults to the 'user' kind.
// User upgrades their plan mid-session
client.updateAttributes({ plan: 'enterprise' })
// Update organization attributes in a multi-context
client.updateAttributes({ tier: 3 }, 'organization')alias(previousContext, newContext)
Record that an anonymous visitor and an identified user are the same person. Sends an alias event to the analytics pipeline and re-saves sticky bucket assignments from the previous context key under the new key.
// On sign-in: link the anonymous session to the identified user
await client.alias(
{ kind: 'user', key: anonymousId },
{ kind: 'user', key: user.id }
)reset()
Return to an anonymous context and clear local sticky assignments. Call this on logout.
client.reset()getContext()
Read-only snapshot of the current evaluation context.
const ctx = client.getContext()5. Multi-context
Evaluate flags simultaneously against multiple context kinds — a user, their organization, and their device:
client.identify({
kind: 'multi',
user: { kind: 'user', key: 'u_123', plan: 'pro' },
organization: { kind: 'organization', key: 'org_456', tier: 'enterprise' },
device: { kind: 'device', key: 'd_abc', os: 'ios' },
})When 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.
6. Reading flags
All 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.
Typed variants
Use the typed methods to get a runtime type-safety check against the datafile's declared flag type. A type mismatch logs a warning and returns the default.
const darkMode = client.getBoolFlag('dark-mode', false)
const theme = client.getStringFlag('theme', 'light')
const maxItems = client.getNumberFlag('max-results', 25)
const config = client.getJsonFlag<{ timeout: number }>('api-config', { timeout: 5000 })Generic getFlag<T>
Use when the type is dynamic or when you need an escape hatch:
const flag = client.getFlag<boolean>('checkout-v2', false)The Flag<T> object
flag.value // T — the variation value
flag.isEnabled() // true if source === 'rule' && value is truthy
flag.variationKey // 'on' | 'off' | 'control' | null (null = default served)
flag.exists() // false only when source === 'not_found'
flag.source // 'rule' | 'sticky' | 'holdout' | 'runtimeOverride' | 'default' | ...
flag.ruleId // which rule matched (null if none)
flag.reasons // string[] — human-readable evaluation path
flag.evaluatedAt // ms epoch timestampgetAllFlags(options?)
Returns all evaluated flags as a Record<string, Flag>. Exposures are suppressed by default.
const all = client.getAllFlags()
const allWithExposures = client.getAllFlags({ fireExposures: true })DecideOption — per-call overrides
import { DecideOption } from '@avsbhq/browser'
const flag = client.getBoolFlag('my-flag', false, {
decideOptions: [DecideOption.DISABLE_EXPOSURE],
})Available options:
DISABLE_EXPOSURE— do not fire an exposure event for this callINCLUDE_REASONS— populateflag.reasonseven when normally suppressedEXCLUDE_VARIABLES— return the variation key only, no variable payloadIGNORE_STICKY_BUCKET— skip sticky assignment lookupENABLED_FLAGS_ONLY— return default if the flag is not actively enabled
7. Tracking events
client.track('purchase_completed', {
value: 199.0,
properties: { plan: 'annual', sku: 'PRO_ANNUAL' },
})TrackPayload fields:
value— numeric metric quantity (revenue, count, duration, etc.)properties— free-form analytics payload forwarded verbatim- (No
revenuefield — usevalueinstead)
Events are queued and flushed in batches. Events queued before onReady() resolves are held and flushed automatically after the tracker initializes.
8. Error handling
logger option
import { createLogger, consoleTransport } from '@avsbhq/browser'
const client = new AvsbClient({
sdkKey: '...',
context: { kind: 'user', key: 'u_1' },
logger: createLogger({
level: 'info',
transports: [consoleTransport({ level: 'warn' })],
}),
})For pino or winston adapters:
import { createPinoTransport } from '@avsbhq/utils/log/pino'
import pino from 'pino'
const logger = createLogger({
level: 'info',
transports: [createPinoTransport({ logger: pino(), level: 'info' })],
})onError callback
const client = new AvsbClient({
sdkKey: '...',
context: { kind: 'user', key: 'u_1' },
onError: (err, source) => {
myMonitoring.captureException(err, { tags: { source } })
},
})source is one of 'init' | 'poll' | 'stream' | 'track' | 'eval'.
Event bus
const unsubscribe = client.on('error', (err) => {
myMonitoring.captureException(err)
})
client.on('configUpdate', ({ publishedAt, reason }) => {
myLogger.info('datafile updated', { publishedAt, reason })
})
client.on('flagChange', ({ flagKey, previousValue, newValue }) => {
// re-render or trigger side effects
})9. SSR / hydration
Pre-fetch the datafile on the server and pass it as bootstrap to skip the initial network round-trip on the client:
// Server — fetch and serialize
import { AvsbClient } from '@avsbhq/browser'
// or use @avsbhq/next/server for App Router
const serverClient = new AvsbClient({
sdkKey: process.env.AVSB_SDK_KEY!,
context: userContext,
})
await serverClient.onReady()
const bootstrap = await serverClient.utils.serializeBootstrap(userContext)
// Pass `bootstrap` to the client as a serialized prop / window var// Client — hydrate from the bootstrap blob
const client = new AvsbClient({
sdkKey: 'sdk_production_...',
context: userContext,
})
client.utils.hydrateBootstrap(bootstrapBlobFromServer)
// onReady() resolves immediately with source: 'bootstrap'Bootstrap guarantees the same variation values on both the server render and the initial client render, preventing hydration mismatches.
For Next.js App Router, use @avsbhq/next and its AvsbHydrator server component instead of wiring this manually.
10. Graceful shutdown
Tab unload
window.addEventListener('beforeunload', () => {
void client.flush() // sendBeacon or fetch({keepalive:true})
})Full teardown
await client.flush() // drain in-flight events
await client.close() // flush + stop polling + destroy trackerclose() calls flush() internally — calling both is safe but redundant.
Streaming
If streaming: true is configured, close() terminates the SSE connection and drains any pending events before resolving.
11. Testing
Install @avsbhq/test and use createMockClient to replace the real SDK in tests:
import { createMockClient, TestData } from '@avsbhq/test'
const td = TestData.flag('checkout-v2')
.booleanFlag()
.variationForUser('u_1', true)
.fallthroughVariation(false)
const client = createMockClient({ flags: [td.build()] })
// In your component test — inject via AvsbTestProvider or pass directly
expect(client.getBoolFlag('checkout-v2', false).value).toBe(true)For React component tests:
import { AvsbTestProvider } from '@avsbhq/test'
render(
<AvsbTestProvider mockFlags={{ 'checkout-v2': true }}>
<CheckoutButton />
</AvsbTestProvider>
)12. Migration
From LaunchDarkly JS
| LaunchDarkly JS | @avsbhq/browser |
|---|---|
| initialize(clientId, context, options) | new AvsbClient({ sdkKey, context }) |
| client.waitForInitialization() | await client.onReady() |
| client.variation('key', default) | client.getBoolFlag('key', false).value |
| client.variationDetail('key', default) | client.getFlag('key', default) |
| client.identify(context) | client.identify(context) |
| client.track('event', { metricValue }) | client.track('event', { value }) |
| client.on('change:key', cb) | client.on('flagChange', ({ flagKey, newValue }) => ...) |
| client.flush() | await client.flush() |
Key differences:
getFlagreturns aFlag<T>object, not a raw value. Access.valuefor the primitive.- All typed variants (
getBoolFlag,getStringFlag, etc.) require an explicitdefaultValue. trackusesvaluenotmetricValuefor the numeric metric.- Multi-context is a first-class concept with kinded
EvalContextrather than a separateLDMultiKindContextwrapper.
From Statsig JS
| Statsig JS | @avsbhq/browser |
|---|---|
| Statsig.initialize(clientKey, user) | new AvsbClient({ sdkKey, context }) |
| Statsig.checkGate('key') | client.getBoolFlag('key', false).isEnabled() |
| Statsig.getExperiment('key').get('param', default) | client.getFlag('key', default).value |
| Statsig.logEvent('event', value, metadata) | client.track('event', { value, properties: metadata }) |
| Statsig.updateUser(user) | client.identify(context) |
