@avsbhq/node 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,268 @@
1
+ # @avsbhq/node
2
+
3
+ Node.js server-side SDK for the [A vs B](https://app.avsb.cloud) platform.
4
+
5
+ Evaluate feature flags, bucket users consistently, and track conversion events on the server. Includes optional Express and Fastify middleware, real-time datafile streaming over SSE, pluggable sticky bucketing (in-memory and Redis), and decision logs for debugging.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @avsbhq/node
11
+ ```
12
+
13
+ ## Authentication
14
+
15
+ You'll need a server SDK key from your A vs B project's environments page.
16
+
17
+ 1. Log in to the [A vs B platform](https://app.avsb.cloud)
18
+ 2. Open your project, go to **Environments**
19
+ 3. Copy the **server** SDK key for the environment you want to use
20
+
21
+ Server SDK keys grant full flag-read access. Store them in environment variables, never check them into source control.
22
+
23
+ ## Quick start
24
+
25
+ ```ts
26
+ import { AvsBServer } from '@avsbhq/node'
27
+
28
+ const server = new AvsBServer({
29
+ sdkKey: process.env.AVSB_SDK_KEY!,
30
+ })
31
+
32
+ await server.onReady()
33
+
34
+ const showNewCheckout = server.evaluateFlag('new-checkout-flow', {
35
+ userId: 'user_123',
36
+ attributes: { plan: 'pro', country: 'US' },
37
+ })
38
+
39
+ await server.track('checkout_completed', {
40
+ userId: 'user_123',
41
+ revenue: 49.99,
42
+ })
43
+ ```
44
+
45
+ Initialize once at app boot. The server holds an in-memory datafile and refreshes it on a polling interval (or via SSE if streaming is enabled).
46
+
47
+ ## API reference
48
+
49
+ ### `new AvsBServer(options)`
50
+
51
+ | Option | Type | Default | Description |
52
+ |---|---|---|---|
53
+ | `sdkKey` | `string` | — | **Required.** Server SDK key for the environment. |
54
+ | `pollingInterval` | `number` | `60_000` | How often (ms) to refresh the datafile. Min `5_000`. |
55
+ | `cdnHost` | `string` | `'https://cdn.avsb.app'` | CDN host for datafile fetches. |
56
+ | `initTimeout` | `number` | `10_000` | Max ms to wait for initial datafile fetch. |
57
+ | `bootstrap` | `FlagDatafile` | — | Pre-fetched datafile to skip the initial network request. |
58
+ | `streaming` | `boolean` | `false` | Enable SSE streaming for real-time datafile updates. |
59
+ | `streamingEndpoint` | `string` | derived | Custom SSE endpoint URL. |
60
+ | `events` | `EventOptions` | defaults | Event tracking config (`flushInterval`, `batchSize`, `maxQueueSize`, `retryAttempts`). |
61
+ | `stickyBucketService` | `StickyBucketService` | none | Pluggable sticky bucketing backend. |
62
+ | `logger` | `Logger` | `noopLogger` | Custom logger (`debug` / `info` / `warn` / `error`). |
63
+
64
+ ### `server.onReady(): Promise<InitResult>`
65
+
66
+ Resolves when the SDK has loaded its datafile.
67
+
68
+ ```ts
69
+ const result = await server.onReady()
70
+ if (!result.success) {
71
+ console.error('SDK failed to initialize:', result.error)
72
+ }
73
+ ```
74
+
75
+ `InitResult.source` is one of `'network'`, `'bootstrap'`, `'cache'`, `'timeout'`, `'error'`.
76
+
77
+ ### `server.evaluateFlag(key, userContext): unknown`
78
+
79
+ Evaluate a flag for a specific user. The user context must include a `userId`.
80
+
81
+ ```ts
82
+ const value = server.evaluateFlag('new-checkout-flow', {
83
+ userId: 'user_123',
84
+ attributes: { plan: 'pro' },
85
+ })
86
+ ```
87
+
88
+ `UserContext` fields:
89
+ | Field | Type | Description |
90
+ |---|---|---|
91
+ | `userId` | `string` | **Required.** Bucketing identifier. |
92
+ | `attributes` | `Record<string, unknown>` | Targeting attributes. |
93
+ | `suppressExposure` | `boolean` | When `true`, don't fire an exposure event. |
94
+ | `decisionLog` | `DecisionLog` | Collect detailed evaluation reasons into a log. |
95
+ | `privateAttributes` | `string[]` | Attribute keys to strip from event payloads. |
96
+
97
+ ### `server.track(eventKey, trackContext): Promise<void>`
98
+
99
+ Send a conversion event.
100
+
101
+ ```ts
102
+ await server.track('purchase_completed', {
103
+ userId: 'user_123',
104
+ revenue: 99.99,
105
+ properties: { plan: 'annual' },
106
+ })
107
+ ```
108
+
109
+ ### `SDK_VERSION`
110
+
111
+ The version of this SDK. Useful for telemetry and bug reports.
112
+
113
+ ```ts
114
+ import { SDK_VERSION } from '@avsbhq/node'
115
+ console.log(SDK_VERSION)
116
+ ```
117
+
118
+ ## Framework integrations
119
+
120
+ ### Express
121
+
122
+ ```ts
123
+ import express from 'express'
124
+ import { AvsBServer, expressMiddleware } from '@avsbhq/node'
125
+
126
+ const server = new AvsBServer({ sdkKey: process.env.AVSB_SDK_KEY! })
127
+ await server.onReady()
128
+
129
+ const app = express()
130
+ app.use(expressMiddleware(server, {
131
+ userIdFrom: (req) => req.user?.id,
132
+ attributesFrom: (req) => ({ plan: req.user?.plan }),
133
+ }))
134
+
135
+ app.get('/checkout', (req, res) => {
136
+ const showNew = req.avsb.getFlag('new-checkout-flow', false)
137
+ res.render(showNew ? 'checkout-v2' : 'checkout-v1')
138
+ })
139
+ ```
140
+
141
+ The middleware decorates each request with `req.avsb`, which exposes per-request `getFlag`, `getFlagDetails`, `getAllFlags`, and `track` methods scoped to the current user.
142
+
143
+ ### Fastify
144
+
145
+ ```ts
146
+ import Fastify from 'fastify'
147
+ import { AvsBServer, fastifyPlugin } from '@avsbhq/node'
148
+
149
+ const server = new AvsBServer({ sdkKey: process.env.AVSB_SDK_KEY! })
150
+ await server.onReady()
151
+
152
+ const app = Fastify()
153
+ await app.register(fastifyPlugin, {
154
+ server,
155
+ userIdFrom: (req) => req.headers['x-user-id'] as string,
156
+ })
157
+ ```
158
+
159
+ ## Sticky bucketing
160
+
161
+ Sticky bucketing keeps a user in the same variation across evaluations, even if rules or rollout percentages change.
162
+
163
+ ### In-memory (single process)
164
+
165
+ ```ts
166
+ import { AvsBServer, InMemoryStickyBucketService } from '@avsbhq/node'
167
+
168
+ const server = new AvsBServer({
169
+ sdkKey: process.env.AVSB_SDK_KEY!,
170
+ stickyBucketService: new InMemoryStickyBucketService(),
171
+ })
172
+ ```
173
+
174
+ ### Redis (multi-process / multi-region)
175
+
176
+ ```ts
177
+ import { createClient } from 'redis'
178
+ import { AvsBServer, RedisStickyBucketService } from '@avsbhq/node'
179
+
180
+ const redis = createClient({ url: process.env.REDIS_URL })
181
+ await redis.connect()
182
+
183
+ const server = new AvsBServer({
184
+ sdkKey: process.env.AVSB_SDK_KEY!,
185
+ stickyBucketService: new RedisStickyBucketService({ redis, keyPrefix: 'avsb' }),
186
+ })
187
+ ```
188
+
189
+ `RedisStickyBucketService` accepts any Redis client matching the `RedisLike` interface (just needs `get` and `set`).
190
+
191
+ ## Real-time updates
192
+
193
+ Enable SSE streaming to receive datafile updates the instant a flag changes on the platform — no polling delay.
194
+
195
+ ```ts
196
+ const server = new AvsBServer({
197
+ sdkKey: process.env.AVSB_SDK_KEY!,
198
+ streaming: true,
199
+ })
200
+ ```
201
+
202
+ Or use the streaming client directly:
203
+
204
+ ```ts
205
+ import { createStreamingClient } from '@avsbhq/node'
206
+
207
+ const stream = createStreamingClient({
208
+ sdkKey: process.env.AVSB_SDK_KEY!,
209
+ onUpdate: (datafile) => { /* refresh in-memory datafile */ },
210
+ })
211
+ ```
212
+
213
+ ## Decision logs
214
+
215
+ Capture full evaluation reasons for debugging or audit trails.
216
+
217
+ ```ts
218
+ import { createDecisionLog } from '@avsbhq/node'
219
+
220
+ const log = createDecisionLog()
221
+ const value = server.evaluateFlag('new-checkout-flow', {
222
+ userId: 'user_123',
223
+ decisionLog: log,
224
+ })
225
+
226
+ console.log(log.entries)
227
+ // [{ flagKey, value, variationKey, source, ruleId, ruleType, reasons, evaluatedAt, durationMs }]
228
+ ```
229
+
230
+ ## Common patterns
231
+
232
+ ### Bootstrap once, evaluate per-request
233
+
234
+ ```ts
235
+ // startup.ts
236
+ import { AvsBServer } from '@avsbhq/node'
237
+
238
+ export const avsb = new AvsBServer({ sdkKey: process.env.AVSB_SDK_KEY! })
239
+ await avsb.onReady()
240
+ ```
241
+
242
+ ```ts
243
+ // route handler
244
+ import { avsb } from './startup'
245
+
246
+ const value = avsb.evaluateFlag('flag-key', { userId: req.user.id })
247
+ ```
248
+
249
+ ### Suppress exposure for backend-only checks
250
+
251
+ ```ts
252
+ const value = server.evaluateFlag('feature-flag', {
253
+ userId: 'user_123',
254
+ suppressExposure: true,
255
+ })
256
+ ```
257
+
258
+ ## Requirements
259
+
260
+ - Node.js >= 18
261
+ - No required peer dependencies. Express and Fastify are optional — only needed if you use the corresponding middleware.
262
+
263
+ ## Links
264
+
265
+ - [Platform docs](https://app.avsb.cloud/docs)
266
+ - [Client SDK (`@avsbhq/js`)](https://www.npmjs.com/package/@avsbhq/js)
267
+ - [React SDK (`@avsbhq/react`)](https://www.npmjs.com/package/@avsbhq/react)
268
+ - [CLI (`@avsbhq/cli`)](https://www.npmjs.com/package/@avsbhq/cli)