@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 +268 -0
- package/dist/index.cjs +864 -0
- package/dist/index.d.cts +246 -0
- package/dist/index.d.ts +246 -0
- package/dist/index.js +829 -0
- package/package.json +39 -0
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)
|