@avsbhq/utils
v1.0.1
Published
Utility library for the [A vs B](https://app.avsb.cloud) SDK ecosystem.
Readme
@avsbhq/utils
Utility library for the A vs B SDK ecosystem.
Provides timing helpers, resilience primitives, structured logging, context builders, snapshot/bootstrap serialization, decision recording, OpenFeature integration, request-scoped middleware for eight frameworks, and unified storage adapters. Most utilities are accessed through client.utils.* on any AvsbClient or AvsbServer instance. Some primitives are also importable directly.
1. Install
npm install @avsbhq/utilsInstall peer dependencies only for the features you use:
# For pino logger adapter
npm install pino
# For winston logger adapter
npm install winston
# For OpenFeature
npm install @openfeature/server-sdk # or @openfeature/web-sdk for browser
# For Express middleware
npm install express
# For Fastify middleware
npm install fastifyNode.js 18 or later.
2. Quickstart
All utilities are available on the SDK client after construction:
import { AvsbClient } from '@avsbhq/browser'
const client = new AvsbClient({ sdkKey: '...', context: { kind: 'user', key: 'u_1' } })
await client.onReady()
// Use utilities through client.utils
const snapshot = client.utils.snapshot()
const debounced = client.utils.debounce(mySearch, 300)
const bootstrap = await client.utils.serializeBootstrap()For server-side:
import { AvsbServer } from '@avsbhq/node'
const server = new AvsbServer({ sdkKey: process.env.AVSB_SDK_KEY! })
await server.onReady()
const recorder = server.utils.createDecisionRecorder({ sinks: [otelSink] })3. SDK keys
@avsbhq/utils contains no networking code and does not use SDK keys directly. SDK keys are managed by @avsbhq/browser and @avsbhq/node.
4. Identity
Not applicable as a standalone utility. Context helpers are available in the context sub-module:
import {
defineContextSchema,
mergeContext,
redactAttributes,
anonymousId,
userContext,
} from '@avsbhq/utils'
// Shorthand context factory
const ctx = userContext('u_123', { plan: 'pro' })
// { kind: 'user', key: 'u_123', plan: 'pro' }
// Merge attributes into an existing context
const updated = mergeContext(ctx, { country: 'US' })
// Generate an anonymous ID (UUID v4)
const anonId = anonymousId()
// Redact private attributes before logging
const safe = redactAttributes(ctx, ['email', 'phone'])5. Multi-context
import { defineContextSchema } from '@avsbhq/utils'
import { z } from 'zod'
const Ctx = defineContextSchema({
user: z.object({ id: z.string(), plan: z.enum(['free', 'pro', 'enterprise']) }),
organization: z.object({ id: z.string(), tier: z.number() }),
})
const ctx = Ctx.builder()
.setUser('u_123', { id: 'u_123', plan: 'pro' })
.setOrganization('org_456', { id: 'org_456', tier: 3 })
.build()
// Validates at build time against the declared schema
const result = Ctx.validate(ctx)
if (!result.success) { /* handle errors */ }defineContextSchema integrates with avsb codegen — the CLI generates the schema file automatically from your platform's declared registeredAttributes.
6. Timing helpers
import { once, debounce, throttle, retry, sleep, withTimeout, withDeadline } from '@avsbhq/utils'
// Or via client:
const { once, debounce, throttle, retry, sleep, withTimeout, withDeadline } = client.utils
// Run a function exactly once (idempotent wrapper)
const initOnce = once(initialize)
initOnce() // runs
initOnce() // no-op
// Debounce user input
const handleSearch = debounce(search, 300)
handleSearch.cancel() // cancel pending call
// Throttle scroll events
const handleScroll = throttle(onScroll, 16)
// Retry with backoff
const data = await retry(() => fetchData(), {
maxAttempts: 3,
baseDelayMs: 100,
backoff: 'exponential',
})
// Add a timeout to any promise
const result = await withTimeout(fetchDatafile(), 5000)
// Deadline (absolute ms timestamp)
const result2 = await withDeadline(fetchDatafile(), Date.now() + 5000)7. Resilience
import { FallbackChain, circuitBreaker } from '@avsbhq/utils'
// FallbackChain: try each layer in order, return the first that succeeds
const chain = new FallbackChain([
{ name: 'redis', fn: () => redis.get(key) },
{ name: 'memory', fn: () => memoryCache.get(key) },
{ name: 'default', fn: () => defaultValue },
])
const value = await chain.get()
// Circuit breaker: open after N failures, half-open after reset timeout
const safeFetch = circuitBreaker(fetchDatafile, {
failureThreshold: 5,
resetTimeoutMs: 30_000,
onOpen: () => logger.warn('circuit open'),
})8. Structured logging
import { createLogger, consoleLogger, noopLogger } from '@avsbhq/utils'
import { createPinoTransport } from '@avsbhq/utils/log/pino'
import { createWinstonTransport } from '@avsbhq/utils/log/winston'
import pino from 'pino'
import winston from 'winston'
// Console logger (default, zero deps)
const logger = consoleLogger('warn')
// Multi-transport pino logger
const pinoLogger = createLogger({
level: 'info',
prefix: 'avsb',
transports: [
createPinoTransport({ logger: pino(), level: 'info' }),
],
})
// Winston adapter
const winstonLogger = createLogger({
level: 'info',
transports: [
createWinstonTransport({ logger: winston.createLogger(), level: 'warn' }),
],
})
// Child logger (inherits parent settings, scoped prefix)
const requestLogger = logger.child('request-handler')
requestLogger.info('flag evaluated', { flagKey: 'checkout-v2' })
// Pass to the SDK
const server = new AvsbServer({ sdkKey, logger: pinoLogger })9. Context builder and visitor data
import { AvsbClient } from '@avsbhq/browser'
const client = new AvsbClient({ sdkKey, context: { kind: 'user', key: 'u_1' } })
// Pre-built context factories for common web signals
const browserCtx = client.utils.context.Browser()
// { kind: 'device', key: fingerprint, os: 'macos', browser: 'chrome', viewport: '...', ... }
const geoCtx = client.utils.context.Geolocation({ country: 'US', region: 'CA' })
// { kind: 'geo', key: 'US:CA', country: 'US', region: 'CA' }
const pageCtx = client.utils.context.PageView({ url: window.location.href })
// { kind: 'pageview', key: pathname, path: '/checkout', referrer: '...' }10. Snapshots and bootstrap
// Synchronous snapshot of all current flag values
const snap = client.utils.snapshot()
// { capturedAt: 1717000000000, datafileVersion: '...', flags: { 'key': Flag<T> }, contextSummary: [...] }
// Force-refresh datafile then snapshot
const fresh = await client.utils.snapshotFresh()
// SSR bootstrap: serialize for handoff to the client
const blob = await client.utils.serializeBootstrap(ctx)
// { format: 'avsb-bootstrap-v1', snapshot: {...}, datafile: {...}, producedAt: ... }
// Client: consume the bootstrap blob (skips initial network fetch)
client.utils.hydrateBootstrap(blob)
// Load a datafile from a URL, KV store, or filesystem
const df = await client.utils.bootstrapFromUrl('https://cdn.avsb.cloud/datafile/prod.json')
const df2 = await client.utils.bootstrapFromKV(kvNamespace, 'my-datafile')
const df3 = await client.utils.bootstrapFromFile('/etc/avsb/datafile.json')11. Change streams
// Async iterator over flagChange events
for await (const change of client.utils.changes()) {
myLogger.info('flag changed', {
flagKey: change.flagKey,
from: change.previousValue,
to: change.newValue,
})
}
// Typed per-flag callback
const unsub = client.utils.onFlagChange('checkout-v2', (flag) => {
analytics.track('flag_updated', { value: flag.value })
})
unsub() // stop listening
// Wait for a flag to reach a specific value (with optional timeout)
const flag = await client.utils.waitForFlag('feature-gate', (f) => f.isEnabled(), { timeout: 10_000 })12. Decision recorder and warehouse sinks
Record every flag evaluation and stream it to one or more sinks for analytics, compliance, or debugging:
import { createOtelSink } from '@avsbhq/utils'
import { createSnowflakeSink } from '@avsbhq/utils'
import { createClickHouseSink } from '@avsbhq/utils'
const recorder = server.utils.createDecisionRecorder({
sample: 0.1, // record 10% of decisions
privateAttributes: ['email', 'ip'], // redact before forwarding
bufferSize: 1000,
flushInterval: 5000,
sinks: [
createOtelSink({ exporter: myOtelExporter }),
createSnowflakeSink({ connection: snowflakeConn, table: 'avsb_decisions' }),
],
})
// Attach to the server's evaluation pipeline
server.on('evaluation', ({ flagKey, flag, contextKeys }) => {
recorder.record({ flagKey, value: flag.value, ...flag })
})
// Flush and close on shutdown
await recorder.close()Available sinks:
createOtelSink— OpenTelemetry exportercreateSentrySink— Sentry clientcreateDatadogSink— Datadog HTTP APIcreateSnowflakeSink— Snowflake Node connectorcreateBigQuerySink—@google-cloud/bigquerycreateRedshiftSink— Redshift Data API (aws-sdk-js-v3)createClickHouseSink—@clickhouse/client
13. Request-scoped middleware
Eight framework adapters. All open an AsyncLocalStorage scope so getRequestClient() works anywhere in the call chain without passing the client explicitly.
Express
import { expressMiddleware } from '@avsbhq/utils/middleware/express'
import { getRequestClient } from '@avsbhq/utils'
app.use(expressMiddleware(server, {
contextFrom: (req) => ({
kind: 'user' as const,
key: req.user?.id ?? 'anon',
}),
}))
// In any nested function — no need to pass client
function myService() {
const client = getRequestClient()
return client.getBoolFlag('my-flag', false)
}Other adapters: fastifyPlugin, honoMiddleware, koaMiddleware, nextAppMiddleware, nextPagesMiddleware, lambdaMiddleware, workersMiddleware.
14. OpenFeature provider
import { AvsbProvider } from '@avsbhq/utils/openfeature'
import { OpenFeature } from '@openfeature/server-sdk'
import { AvsbServer } from '@avsbhq/node'
const server = new AvsbServer({ sdkKey: process.env.AVSB_SDK_KEY! })
await server.onReady()
OpenFeature.setProvider(new AvsbProvider(server))
const client = OpenFeature.getClient()
const enabled = await client.getBooleanValue('checkout-v2', false, { targetingKey: 'u_123' })Browser variant (@openfeature/web-sdk):
import { AvsbWebProvider } from '@avsbhq/utils/openfeature'
import { OpenFeature } from '@openfeature/web-sdk'
import { AvsbClient } from '@avsbhq/browser'
const avsbClient = new AvsbClient({ sdkKey: '...', context: { kind: 'user', key: 'u_1' } })
OpenFeature.setProvider(new AvsbWebProvider(avsbClient))15. Storage adapters
Unified storage interface covering both sticky bucketing and datafile caching:
import {
createMemoryAdapter,
createRedisAdapter,
createLocalStorageAdapter,
createIndexedDBAdapter,
createCookieAdapter,
createDynamoDBAdapter,
createPostgresAdapter,
createDurableObjectAdapter,
} from '@avsbhq/utils'
// Redis (recommended for server-side, multi-process)
const redis = createClient({ url: process.env.REDIS_URL })
const adapter = createRedisAdapter({ client: redis, prefix: 'avsb:', datafileTtlMs: 60_000 })
// DynamoDB
const adapter2 = createDynamoDBAdapter({ client: dynamoClient, tableName: 'avsb-storage' })
// Cloudflare Durable Objects
const adapter3 = createDurableObjectAdapter({ stub: env.AVSB_DO })Big segment stores (separate from sticky bucketing):
import { createRedisBigSegmentStore, createDynamoDBBigSegmentStore } from '@avsbhq/utils'
const bigSegments = createRedisBigSegmentStore({
client: redis,
prefix: 'avsb:segments:',
refreshInterval: 30_000,
})16. Track helpers
import { Transaction, itemOf } from '@avsbhq/utils'
// Batch multiple events
client.utils.trackBatch([
{ eventKey: 'page_viewed', payload: { value: 1 } },
{ eventKey: 'product_clicked', payload: { properties: { sku: 'A' } } },
])
// Deduplicated tracking (event fires at most once per dedupKey per session)
client.utils.trackOnce('onboarding_complete', 'user-onboarding')
// Transaction helper (tracks items + total automatically)
const tx = new Transaction('purchase')
tx.add(itemOf('PRO_ANNUAL', 199.0, { currency: 'USD' }))
tx.add(itemOf('SUPPORT_ADDON', 49.0))
tx.track(client) // fires 'purchase' with value=248.017. Cleanup registry
import { createRegistry } from '@avsbhq/utils'
const registry = createRegistry()
const timer = setInterval(pollMetrics, 5000)
registry.add(() => clearInterval(timer))
const sub = client.on('flagChange', handleChange)
registry.add(sub) // sub() is the unsubscribe function
// On shutdown:
registry.drain() // calls all registered teardown functions in reverse order18. Migration
From LaunchDarkly client utilities
| LaunchDarkly | @avsbhq/utils |
|---|---|
| LDClient.on('change', cb) | client.utils.onFlagChange(key, cb) |
| LDClient.allFlagsState(ctx) | client.utils.snapshot() |
| Custom OpenFeature provider | AvsbProvider from @avsbhq/utils/openfeature |
| LDRedisFeatureStore | createRedisAdapter(...) (storage adapter) |
From Statsig diagnostics / utils
| Statsig | @avsbhq/utils |
|---|---|
| StatsigLocalOverrides | createMockClient from @avsbhq/test |
| EvaluationsDataAdapter | UnifiedStorageAdapter from @avsbhq/utils |
| StatsigUser extension | defineContextSchema(...) with Zod schema |
| Statsig.flush() | client.flush() / recorder.flush() |
