flagmint-js-sdk 1.2.25 โ 2.0.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 +213 -469
- package/dist/flagmint.cjs.js +40369 -10
- package/dist/flagmint.cjs.js.map +1 -0
- package/dist/flagmint.es.js +35639 -15848
- package/dist/flagmint.es.js.map +1 -0
- package/dist/flagmint.umd.js +40381 -18
- package/dist/flagmint.umd.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/sdk/core/client.d.ts +148 -8
- package/dist/sdk/core/helpers/applicationEvents.d.ts +20 -0
- package/dist/sdk/core/helpers/connectionShare.d.ts +215 -0
- package/dist/sdk/core/transports/LongPollingTransport.d.ts +5 -1
- package/dist/sdk/core/transports/SSETransport.d.ts +90 -0
- package/dist/sdk/core/transports/Transport.d.ts +5 -1
- package/package.json +10 -3
- package/dist/sdk/core/transports/WebsocketTransport.d.ts +0 -33
package/README.md
CHANGED
|
@@ -1,603 +1,347 @@
|
|
|
1
1
|
# Flagmint JavaScript SDK
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> A framework-agnostic JavaScript SDK for evaluating and streaming feature flags in browsers and Node.js.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
6
8
|
|
|
7
|
-
##
|
|
9
|
+
## Features
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
- ๐ **`initialFlagsReject` on WebSocket transport** โ a post-`onopen` close with a terminal code now correctly rejects the `waitForInitialFlags()` promise (and therefore `init()`), so the error surfaces reliably through `client.ready()` even when the connection opened before the server sent the close frame.
|
|
16
|
-
- ๐ **`onConnectionStateChanged()` exposed** โ consumers can now register a callback to observe WebSocket connection state transitions (`connecting`, `connected`, `disconnected`, `reconnecting`, `failed`).
|
|
11
|
+
- Framework agnostic (React, Vue, Angular, vanilla JS, Node.js)
|
|
12
|
+
- Real-time flag updates over Server-Sent Events (SSE)
|
|
13
|
+
- Automatic reconnection with a fresh single-use session token
|
|
14
|
+
- Same-origin iframe/tab connection sharing (one stream per API key)
|
|
15
|
+
- Built-in localStorage cache, with pluggable adapters for Node
|
|
16
|
+
- `getFlag()` is always local โ no network on the read path
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
- โจ Add `env` parameter to SDK configuration for explicit environment specification
|
|
20
|
-
- Allows users to pass environment as a variable when NODE_ENV or NEXT_PUBLIC_NODE_ENV cannot be reliably detected
|
|
21
|
-
- Improved environment resolution with fallback to auto-detection
|
|
18
|
+
## Release notes
|
|
22
19
|
|
|
23
|
-
|
|
20
|
+
### v2.0.0
|
|
24
21
|
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
29
|
-
-
|
|
30
|
-
-
|
|
22
|
+
- **SSE instead of WebSockets.** Handshake at `POST /auth/asl-handshake`, stream at `GET /evaluator/v2/flags/stream`, context at `POST /evaluator/v2/flags/context`.
|
|
23
|
+
- **Context updates match the API.** The SDK sends `x-api-key`, treats `202` as queued, and waits for the next `flags` event on the open stream (400ms server debounce). Browser and Node use the same path.
|
|
24
|
+
- **Quota and stream errors.** Listens for `quota_exceeded` and named `error` events. Quota surfaces as `ERR_RATE_LIMITED` (`retryAfter`, `resetTime`) through `onError`. Cached flags keep serving.
|
|
25
|
+
- **Connection sharing.** Same-origin documents (tabs, sibling iframes) with the same API key elect one leader that holds EventSource. Followers skip handshake and the stream. Off in Node so cluster workers each keep their own connection.
|
|
26
|
+
- **Node `EventSourceImpl`.** Node has no native EventSource; pass one (for example `eventsource`).
|
|
27
|
+
- **Session refresh on reconnect.** The stream consumes a single-use `sessionId`; reconnect fetches a new one before opening EventSource again.
|
|
28
|
+
- **`ready()` never throws.** Failures go to `onError`. Cached flags, if any, are used in degraded mode.
|
|
31
29
|
|
|
32
|
-
|
|
30
|
+
### v1.2.25 / v1.2.24
|
|
31
|
+
|
|
32
|
+
Historical WebSocket-era releases. See git tags for the full notes. v2.0.0 replaces that transport.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
# Installation
|
|
33
37
|
|
|
34
38
|
```bash
|
|
35
39
|
npm install flagmint-js-sdk
|
|
36
|
-
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
```bash
|
|
37
43
|
yarn add flagmint-js-sdk
|
|
38
44
|
```
|
|
39
45
|
|
|
40
|
-
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
# Quick start
|
|
49
|
+
|
|
50
|
+
## Browser
|
|
41
51
|
|
|
42
52
|
```ts
|
|
43
53
|
import { FlagClient } from 'flagmint-js-sdk';
|
|
44
54
|
|
|
45
|
-
// Create a client instance
|
|
46
55
|
const client = new FlagClient({
|
|
47
|
-
apiKey: '
|
|
56
|
+
apiKey: 'ff_your_api_key',
|
|
57
|
+
enableFlagmint: true,
|
|
48
58
|
context: {
|
|
49
|
-
kind:
|
|
50
|
-
user: {
|
|
51
|
-
|
|
59
|
+
kind: 'user',
|
|
60
|
+
user: {
|
|
61
|
+
kind: 'user',
|
|
62
|
+
key: 'user-123'
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
onError: (error) => {
|
|
66
|
+
console.error(error.message, (error as { code?: string }).code);
|
|
52
67
|
}
|
|
53
68
|
});
|
|
54
69
|
|
|
55
|
-
// Wait for initial connection and flag synchronization
|
|
56
70
|
await client.ready();
|
|
57
71
|
|
|
58
|
-
|
|
59
|
-
const showBanner = client.getFlag('show_banner', false);
|
|
60
|
-
const featureVersion = client.getFlag('feature_version', 'v1');
|
|
61
|
-
|
|
62
|
-
// Update context and re-evaluate
|
|
63
|
-
await client.updateContext({
|
|
64
|
-
kind: "user",
|
|
65
|
-
user: { kind: "user", key: 'user456', email: 'user456@example.com' }
|
|
66
|
-
});
|
|
72
|
+
const enabled = client.getFlag('new_dashboard', false);
|
|
67
73
|
```
|
|
68
74
|
|
|
69
|
-
|
|
75
|
+
Same-origin iframes that use this API key share one SSE connection by default **in browsers that provide `BroadcastChannel`**. Without it (older browsers, Node), each client opens its own stream. Set `shareConnection: false` if two clients on the page must keep different evaluation contexts at the same time, or if the origin hosts untrusted documents that should not share that stream.
|
|
70
76
|
|
|
71
|
-
|
|
72
|
-
| --------------------- | ----------------------------------------- | ------------------ | --------------------------------------------------------------------- |
|
|
73
|
-
| `apiKey` | `string` | **Required** | Your environment API key. |
|
|
74
|
-
| `context` | `Record<string, any>` | `{}` | Initial evaluation context (e.g. user attributes). |
|
|
75
|
-
| `enableOfflineCache` | `boolean` | `true` | Enable caching of flags locally. |
|
|
76
|
-
| `persistContext` | `boolean` | `false` | Persist evaluation context across sessions. |
|
|
77
|
-
| `cacheAdapter` | `CacheAdapter<C>` | Sync `cacheHelper` | Custom cache implementation (see examples). |
|
|
78
|
-
| `transportMode` | `'auto' \| 'websocket' \| 'long-polling'` | `'auto'` | How to fetch flags: prefer WebSocket, or use long-polling transport. |
|
|
79
|
-
| `restEndpoint` | `string` | env-derived | Override the REST endpoint (useful for proxies or self-hosted setups).|
|
|
80
|
-
| `wsEndpoint` | `string` | env-derived | Override the WebSocket endpoint. |
|
|
81
|
-
| `onError` | `(error: Error) => void` | โ | Callback for transport or initialization errors. Called when operating in degraded mode with cached flags. |
|
|
82
|
-
| `previewMode` | `boolean` | `false` | Evaluate using `rawFlags` only, bypassing remote fetch. |
|
|
83
|
-
| `rawFlags` | `Record<string, FlagValue>` | โ | Local-only flag definitions used when `previewMode: true`. |
|
|
84
|
-
| `deferInitialization` | `boolean` | `false` | If `true`, client initialization is deferred until you call `ready()`. |
|
|
77
|
+
## Node.js
|
|
85
78
|
|
|
86
|
-
|
|
79
|
+
Node does not provide `EventSource`. Install a polyfill and inject it. Each cluster worker should construct its own `FlagClient` (sharing is disabled without `window`).
|
|
87
80
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
```ts
|
|
91
|
-
import { logger } from 'flagmint-js-sdk';
|
|
92
|
-
|
|
93
|
-
logger.setup({ debugLog: true });
|
|
94
|
-
|
|
95
|
-
const client = new FlagClient({ apiKey: '...' });
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
In **production** (`NODE_ENV=production`), only connection and disconnection messages are emitted regardless of the `debugLog` setting โ all other internal logs are suppressed.
|
|
99
|
-
|
|
100
|
-
```ts
|
|
101
|
-
// These messages always appear in production:
|
|
102
|
-
// "[WebSocketTransport] Connected"
|
|
103
|
-
// "[WebSocketTransport] Connection closed: 1000"
|
|
104
|
-
|
|
105
|
-
// All other internal logs require debugLog: true in non-production environments
|
|
81
|
+
```bash
|
|
82
|
+
npm install eventsource
|
|
106
83
|
```
|
|
107
84
|
|
|
108
|
-
## ๐ง Cache Adapters
|
|
109
|
-
|
|
110
|
-
### Sync (Browser / In-Memory)
|
|
111
|
-
|
|
112
|
-
By default, the SDK uses a **sync** `localStorage`-based helper in browsers or a `Map` in Node.js:
|
|
113
|
-
|
|
114
85
|
```ts
|
|
115
|
-
import { FlagClient
|
|
116
|
-
|
|
117
|
-
syncCache.setCacheStorage({
|
|
118
|
-
getItem: key => myMap.get(key) ?? null,
|
|
119
|
-
setItem: (key, val) => myMap.set(key, val),
|
|
120
|
-
});
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
### Async (Redis / File System)
|
|
124
|
-
|
|
125
|
-
Use the **async** helper for server-side or React Native:
|
|
86
|
+
import { FlagClient } from 'flagmint-js-sdk';
|
|
87
|
+
import EventSource from 'eventsource';
|
|
126
88
|
|
|
127
|
-
|
|
128
|
-
|
|
89
|
+
const apiKey = process.env.FLAGMINT_API_KEY;
|
|
90
|
+
if (!apiKey) {
|
|
91
|
+
throw new Error('FLAGMINT_API_KEY is required');
|
|
92
|
+
}
|
|
129
93
|
|
|
130
94
|
const client = new FlagClient({
|
|
131
|
-
apiKey
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
95
|
+
apiKey,
|
|
96
|
+
enableFlagmint: true,
|
|
97
|
+
EventSourceImpl: EventSource,
|
|
98
|
+
env: process.env.FLAGMINT_ENVIRONMENT || 'production',
|
|
99
|
+
onError: (error) => {
|
|
100
|
+
console.error('Flagmint error:', error);
|
|
137
101
|
}
|
|
138
102
|
});
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
## ๐ Transport Modes
|
|
142
|
-
|
|
143
|
-
* **`'websocket'`**: Persistent WebSocket connection for live updates.
|
|
144
|
-
* **`'long-polling'`**: HTTP requests held open by server until flags change.
|
|
145
|
-
* **`'auto'`** (default): Try WebSocket, fallback to long-polling.
|
|
146
|
-
|
|
147
|
-
```ts
|
|
148
|
-
const client = new FlagClient({
|
|
149
|
-
apiKey: '...',
|
|
150
|
-
transportMode: 'long-polling'
|
|
151
|
-
});
|
|
152
|
-
```
|
|
153
103
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
Override the default environment-derived endpoints โ useful for proxies, staging overrides, or self-hosted deployments:
|
|
157
|
-
|
|
158
|
-
```ts
|
|
159
|
-
const client = new FlagClient({
|
|
160
|
-
apiKey: '...',
|
|
161
|
-
restEndpoint: 'https://my-proxy.example.com/evaluator/evaluate',
|
|
162
|
-
wsEndpoint: 'wss://my-proxy.example.com/ws/sdk',
|
|
163
|
-
});
|
|
104
|
+
await client.ready();
|
|
164
105
|
```
|
|
165
106
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
Monitor connection state transitions by accessing the underlying WebSocket transport directly:
|
|
169
|
-
|
|
170
|
-
```ts
|
|
171
|
-
import { WebSocketTransport } from 'flagmint-js-sdk';
|
|
172
|
-
|
|
173
|
-
const ws = new WebSocketTransport(wsUrl, apiKey);
|
|
174
|
-
ws.onConnectionStateChanged((state) => {
|
|
175
|
-
// state: 'connecting' | 'connected' | 'disconnected' | 'reconnecting' | 'failed'
|
|
176
|
-
console.log('WS state:', state);
|
|
177
|
-
});
|
|
178
|
-
```
|
|
107
|
+
---
|
|
179
108
|
|
|
180
|
-
|
|
109
|
+
# How it works
|
|
181
110
|
|
|
182
|
-
|
|
183
|
-
|------|---------|-----------|----------|
|
|
184
|
-
| WebSocket | Lowest (real-time) | Efficient | Real-time dashboards, live updates |
|
|
185
|
-
| Long-Polling | Medium | Higher | Simpler setup, less overhead on server |
|
|
186
|
-
| Auto | Lowest to Medium | Varies | Most applications (recommended) |
|
|
111
|
+
After `ready()`:
|
|
187
112
|
|
|
188
|
-
|
|
113
|
+
1. Optionally joins a same-origin share group (browser). Followers stop here and receive flags from the leader.
|
|
114
|
+
2. `POST /auth/asl-handshake` with `X-API-Key` โ single-use `sessionId`.
|
|
115
|
+
3. Opens `GET /evaluator/v2/flags/stream?sessionId&context&sdkVersion&platform&wrapper*`. The server contract requires `sessionId` and a base64 `context` in the query string. Treat those query parameters as sensitive: do not put secrets or PII in `context`, and redact `sessionId` and `context` from access logs, reverse-proxy logs, and telemetry.
|
|
116
|
+
4. Server sends `connected` (connection id) then `flags` (evaluated map). Heartbeats are SSE comments (`: heartbeat`) and are not exposed by EventSource.
|
|
117
|
+
5. Flags are stored in the cache adapter (localStorage in the browser by default).
|
|
118
|
+
6. Later admin publishes are pushed as `flags` events on the same stream.
|
|
189
119
|
|
|
190
|
-
|
|
120
|
+
`getFlag()` / `getFlags()` never hit the network. They read the last evaluated snapshot.
|
|
191
121
|
|
|
192
|
-
|
|
122
|
+
`updateContext()` `POST`s `/evaluator/v2/flags/context` with `x-api-key` and `{ connectionId, context }`. The HTTP response is `202`; new values arrive as a `flags` event after the serverโs 400ms debounce.
|
|
193
123
|
|
|
194
|
-
|
|
195
|
-
import { evaluateFlagValue } from 'flagmint-js-sdk';
|
|
124
|
+
---
|
|
196
125
|
|
|
197
|
-
|
|
198
|
-
|
|
126
|
+
# Configuration
|
|
127
|
+
|
|
128
|
+
| Option | Type | Default | Description |
|
|
129
|
+
|---|---|---|---|
|
|
130
|
+
| `apiKey` | string | required | Environment API key |
|
|
131
|
+
| `enableFlagmint` | boolean | `true` | Set `false` in local/dev to skip connecting (no handshake, no stream) |
|
|
132
|
+
| `context` | object | `{}` | Initial evaluation context. Do not put secrets or PII here โ the stream URL serializes this object in the query string |
|
|
133
|
+
| `transportMode` | `'auto' \| 'sse' \| 'long-polling'` | `'auto'` | `auto` tries SSE, then long-polling |
|
|
134
|
+
| `EventSourceImpl` | EventSource | browser global | Required in Node |
|
|
135
|
+
| `shareConnection` | boolean | `true` in browsers with `BroadcastChannel`, `false` in Node | One stream per API key across same-origin documents |
|
|
136
|
+
| `enableOfflineCache` | boolean | `true` | Load/save flags via the cache adapter |
|
|
137
|
+
| `persistContext` | boolean | `false` | Persist context in the cache adapter |
|
|
138
|
+
| `cacheAdapter` | CacheAdapter | localStorage helpers | Custom cache (Redis, files, โฆ) |
|
|
139
|
+
| `restEndpoint` | string | env default | Override the long-polling / evaluate URL |
|
|
140
|
+
| `sseEndpoint` | string | env default | Override the SSE base URL (`/stream` and `/context`) |
|
|
141
|
+
| `handshakeEndpoint` | string | env default | Override the ASL handshake URL. Use with `sseEndpoint` for self-hosted gateways |
|
|
142
|
+
| `env` | string | `NODE_ENV` | `development` \| `staging` \| `production` |
|
|
143
|
+
| `wrapperInfo` | `{ name, version }` | native-js | Framework wrapper telemetry on the stream URL |
|
|
144
|
+
| `previewMode` / `rawFlags` | | | Local-only evaluation, no network |
|
|
145
|
+
| `deferInitialization` | boolean | `false` | Wait for `ready()` before connecting |
|
|
146
|
+
| `debugLog` | boolean | `false` | Verbose SDK logs |
|
|
147
|
+
| `onError` | `(error) => void` | | Auth, quota, transport, and context errors. `ready()` still resolves. |
|
|
199
148
|
|
|
200
|
-
|
|
201
|
-
* If matched, applies rollout (percentage or variant).
|
|
202
|
-
* Returns flagโs value or `null` if not enabled.
|
|
149
|
+
---
|
|
203
150
|
|
|
204
|
-
|
|
151
|
+
# Transport modes
|
|
205
152
|
|
|
206
153
|
```ts
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
154
|
+
transportMode: 'auto' // SSE, then long-polling if SSE init fails
|
|
155
|
+
transportMode: 'sse' // stream only โ reports failures through onError; uses cached flags if any (no long-polling fallback)
|
|
156
|
+
transportMode: 'long-polling' // POST evaluate on an interval
|
|
210
157
|
```
|
|
211
158
|
|
|
212
|
-
|
|
213
|
-
* Supports **variant** distributions (A/B tests).
|
|
214
|
-
|
|
215
|
-
### `rolloutUtils`
|
|
159
|
+
---
|
|
216
160
|
|
|
217
|
-
|
|
218
|
-
* `pickVariant(value: string, variants: VariantOption[]): string | number | boolean | null`
|
|
161
|
+
# Connection sharing
|
|
219
162
|
|
|
220
|
-
|
|
163
|
+
Used when the same app is embedded twice on one origin (for example two WeSeeDo iframes in a host page).
|
|
221
164
|
|
|
222
|
-
|
|
165
|
+
- **Leader** performs handshake and holds EventSource.
|
|
166
|
+
- **Followers** receive flag snapshots over `BroadcastChannel` (`flagmint-share:<apiKey>`). Leadership is a `localStorage` lock (`flagmint_share_lock:<apiKey>`).
|
|
167
|
+
- Follower `updateContext()` is forwarded to the leader, which runs it on the shared stream. Last write wins. Any same-origin document can send that RPC and see the flags that come back โ turn sharing off if you host untrusted content on this origin.
|
|
168
|
+
- If the leader iframe unloads, it posts `bye` so another member can take over immediately. If the tab is killed without `pagehide`, followers wait for the lock TTL.
|
|
223
169
|
|
|
224
170
|
```ts
|
|
225
|
-
|
|
171
|
+
// Default in browsers with BroadcastChannel โ one stream for this API key
|
|
172
|
+
shareConnection: true
|
|
226
173
|
|
|
227
|
-
|
|
174
|
+
// Two clients, two contexts, two streams โ or no BroadcastChannel
|
|
175
|
+
shareConnection: false
|
|
228
176
|
```
|
|
229
177
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
## ๐ Framework Integration Examples
|
|
233
|
-
|
|
234
|
-
### React
|
|
235
|
-
|
|
236
|
-
```tsx
|
|
237
|
-
import { useEffect, useState } from 'react';
|
|
238
|
-
import { FlagClient } from 'flagmint-js-sdk';
|
|
239
|
-
|
|
240
|
-
const client = new FlagClient({
|
|
241
|
-
apiKey: 'ff_...',
|
|
242
|
-
context: {
|
|
243
|
-
kind: "multi | user | organisation",
|
|
244
|
-
user: { kind: "user", key: '', email: '' },
|
|
245
|
-
organization: { kind: "organization", key: '', plan: '' }
|
|
246
|
-
}
|
|
247
|
-
});
|
|
178
|
+
Node cluster workers do not share. Each worker process opens its own stream.
|
|
248
179
|
|
|
249
|
-
|
|
250
|
-
const [flags, setFlags] = useState({});
|
|
251
|
-
|
|
252
|
-
useEffect(() => {
|
|
253
|
-
client.ready().then(() => {
|
|
254
|
-
setFlags({
|
|
255
|
-
showBeta: client.getFlag('show_beta', false),
|
|
256
|
-
theme: client.getFlag('theme', 'light')
|
|
257
|
-
});
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
// Subscribe to flag updates
|
|
261
|
-
const unsubscribe = client.subscribe((updatedFlags) => {
|
|
262
|
-
setFlags({
|
|
263
|
-
showBeta: client.getFlag('show_beta', false),
|
|
264
|
-
theme: client.getFlag('theme', 'light')
|
|
265
|
-
});
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
return () => unsubscribe();
|
|
269
|
-
}, []);
|
|
270
|
-
|
|
271
|
-
return (
|
|
272
|
-
<div className={`theme-${flags.theme}`}>
|
|
273
|
-
{flags.showBeta && <BetaFeature />}
|
|
274
|
-
</div>
|
|
275
|
-
);
|
|
276
|
-
}
|
|
277
|
-
```
|
|
180
|
+
---
|
|
278
181
|
|
|
279
|
-
|
|
182
|
+
# Updating context
|
|
280
183
|
|
|
281
184
|
```ts
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
cacheAdapter: {
|
|
287
|
-
loadFlags: asyncCache.loadCachedFlags,
|
|
288
|
-
saveFlags: asyncCache.saveCachedFlags,
|
|
289
|
-
loadContext: asyncCache.loadCachedContext,
|
|
290
|
-
saveContext: asyncCache.saveCachedContext
|
|
291
|
-
}
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
// Wait for initialization
|
|
295
|
-
await client.ready();
|
|
296
|
-
|
|
297
|
-
app.get('/api/feature', async (req, res) => {
|
|
298
|
-
// Update context with proper structure
|
|
299
|
-
await client.updateContext({
|
|
300
|
-
kind: "multi",
|
|
301
|
-
user: {
|
|
302
|
-
kind: "user",
|
|
303
|
-
key: req.user.id,
|
|
304
|
-
email: req.user.email
|
|
305
|
-
},
|
|
306
|
-
organization: {
|
|
307
|
-
kind: "organization",
|
|
308
|
-
key: req.user.orgId,
|
|
309
|
-
plan: req.user.plan
|
|
310
|
-
}
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
const isEnabled = client.getFlag('new_api', false);
|
|
314
|
-
|
|
315
|
-
res.json({ enabled: isEnabled });
|
|
185
|
+
await client.updateContext({
|
|
186
|
+
kind: 'multi',
|
|
187
|
+
user: { kind: 'user', key: '123' },
|
|
188
|
+
organization: { kind: 'organization', key: 'acme' }
|
|
316
189
|
});
|
|
317
190
|
```
|
|
318
191
|
|
|
319
|
-
|
|
192
|
+
Await this call. The promise settles when the stream delivers the next `flags` packet (or times out and keeps the previous snapshot). Overlapping calls are serialized.
|
|
320
193
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
```ts
|
|
324
|
-
// Initial context
|
|
325
|
-
const client = new FlagClient({
|
|
326
|
-
apiKey: '...',
|
|
327
|
-
context: {
|
|
328
|
-
kind: "user",
|
|
329
|
-
user: { kind: "user", key: 'user123' }
|
|
330
|
-
}
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
// Update context later (e.g., after user upgrades)
|
|
334
|
-
await client.updateContext({
|
|
335
|
-
kind: "multi",
|
|
336
|
-
user: {
|
|
337
|
-
kind: "user",
|
|
338
|
-
key: 'user123',
|
|
339
|
-
email: 'user@example.com'
|
|
340
|
-
},
|
|
341
|
-
organization: {
|
|
342
|
-
kind: "organization",
|
|
343
|
-
key: 'org456',
|
|
344
|
-
plan: 'premium',
|
|
345
|
-
country: 'CA'
|
|
346
|
-
}
|
|
347
|
-
});
|
|
348
|
-
```
|
|
194
|
+
SSE `/context` changes **that connectionโs** context. It is the right tool for a browser user logging in. It is the wrong tool for many concurrent Node HTTP requests with different targeting on one singleton client โ wait for client-side rule evaluation, or use a dedicated client per distinct context.
|
|
349
195
|
|
|
350
|
-
|
|
196
|
+
---
|
|
351
197
|
|
|
352
|
-
|
|
353
|
-
const client = new FlagClient({
|
|
354
|
-
apiKey: '...',
|
|
355
|
-
persistContext: true, // Saves to localStorage/AsyncStorage
|
|
356
|
-
context: {
|
|
357
|
-
kind: "user",
|
|
358
|
-
user: { kind: "user", key: 'user123', email: 'user@example.com' }
|
|
359
|
-
}
|
|
360
|
-
});
|
|
361
|
-
```
|
|
198
|
+
# Tracking application errors
|
|
362
199
|
|
|
363
|
-
|
|
200
|
+
`getFlag()` tells you what was served. `trackError()` tells Flagmint whether that variation hurt the user. Errors are attributed to the **currently served value** of the flag, batched, and posted to `POST /evaluator/events`. The call never throws.
|
|
364
201
|
|
|
365
|
-
|
|
202
|
+
Analytics must be enabled on the flag in the dashboard. The SDK skips `trackError` / `track` locally once the SSE `flags` packet includes `analytics` for that key. Older servers without the map still accept the POST and drop it at ingest.
|
|
366
203
|
|
|
367
204
|
```ts
|
|
368
|
-
const
|
|
369
|
-
apiKey: '...',
|
|
370
|
-
deferInitialization: true
|
|
371
|
-
});
|
|
372
|
-
|
|
373
|
-
// Later, when context is ready:
|
|
374
|
-
await client.updateContext({
|
|
375
|
-
kind: "user",
|
|
376
|
-
user: { kind: "user", key: 'user123', email: 'user@example.com' }
|
|
377
|
-
});
|
|
378
|
-
await client.ready(); // Initializes and connects
|
|
379
|
-
```
|
|
380
|
-
|
|
381
|
-
## ๐ Advanced Usage
|
|
205
|
+
const checkoutOn = client.getFlag('checkout_redesign', false);
|
|
382
206
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
console.error('Flag client error:', error);
|
|
390
|
-
// Report to monitoring service
|
|
391
|
-
sentry.captureException(error);
|
|
392
|
-
}
|
|
393
|
-
});
|
|
207
|
+
try {
|
|
208
|
+
await submitPayment();
|
|
209
|
+
} catch (error) {
|
|
210
|
+
client.trackError('checkout_redesign', error, { step: 'payment' });
|
|
211
|
+
throw error;
|
|
212
|
+
}
|
|
394
213
|
```
|
|
395
214
|
|
|
396
|
-
|
|
215
|
+
`track(flagKey, eventName)` uses the same pipe for custom metrics (conversions, etc.). Only `kind: 'error'` is counted in app error rate today.
|
|
397
216
|
|
|
398
|
-
|
|
217
|
+
### Manual verification
|
|
399
218
|
|
|
400
|
-
|
|
219
|
+
The Flagmint dashboard evaluates its own flags with `flagmint-react-sdk` (`useFlag('homepage_variant')`). Boom that path, don't curl:
|
|
401
220
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
| โ No cached flags | Rejects | Initialization fails |
|
|
406
|
-
| โ
Connected successfully | Resolves | Normal operation with live updates |
|
|
221
|
+
1. Enable Analytics Tracking on `homepage_variant`.
|
|
222
|
+
2. Run FF-EU on `:3000` and the dashboard (`npm run dev` in `flagmint`, port **5200**).
|
|
223
|
+
3. Open [http://localhost:5200/?flagmint_boom=1](http://localhost:5200/?flagmint_boom=1).
|
|
407
224
|
|
|
408
|
-
|
|
225
|
+
The homepage reads the served variant, then `trackError('homepage_variant', โฆ)` (or POSTs `/evaluator/events` if this app still has js-sdk 1.x). Expect a **Flag error sent** toast. Metrics **App error rate** moves after the 5-minute flush.
|
|
409
226
|
|
|
410
|
-
|
|
411
|
-
const client = new FlagClient({
|
|
412
|
-
apiKey: '...',
|
|
413
|
-
enableOfflineCache: true,
|
|
414
|
-
onError: (error) => {
|
|
415
|
-
// Called when operating in degraded mode with cached flags
|
|
416
|
-
console.warn('โ ๏ธ Degraded mode - using cached flags:', error.message);
|
|
417
|
-
// Optionally report to monitoring service
|
|
418
|
-
}
|
|
419
|
-
});
|
|
227
|
+
Other SDK flags: `?flagmint_boom=show_google_login` on `/login`. Production ignores the query param.
|
|
420
228
|
|
|
229
|
+
```tsx
|
|
230
|
+
const enabled = useFlag('checkout_redesign', false);
|
|
231
|
+
const { client } = useFlagmint();
|
|
421
232
|
try {
|
|
422
|
-
await
|
|
423
|
-
// โ
Resolves successfully even if server is down (when cached flags exist)
|
|
424
|
-
console.log('Client ready');
|
|
425
|
-
const feature = client.getFlag('my_feature', false); // Always works
|
|
233
|
+
await submitPayment();
|
|
426
234
|
} catch (error) {
|
|
427
|
-
|
|
428
|
-
|
|
235
|
+
client?.trackError?.('checkout_redesign', error);
|
|
236
|
+
throw error;
|
|
429
237
|
}
|
|
430
238
|
```
|
|
431
239
|
|
|
432
|
-
|
|
240
|
+
Full notes: `FF-EU/documentation/APPLICATION_ERROR_TRACKING.md`.
|
|
433
241
|
|
|
434
|
-
|
|
242
|
+
---
|
|
435
243
|
|
|
436
|
-
|
|
437
|
-
const client = new FlagClient({
|
|
438
|
-
apiKey: '...',
|
|
439
|
-
previewMode: true,
|
|
440
|
-
rawFlags: {
|
|
441
|
-
new_checkout: true,
|
|
442
|
-
discount_percent: 20,
|
|
443
|
-
ui_variant: 'experimental'
|
|
444
|
-
}
|
|
445
|
-
});
|
|
244
|
+
# Errors and quota
|
|
446
245
|
|
|
447
|
-
|
|
448
|
-
const checkout = client.getFlag('new_checkout', false); // true
|
|
449
|
-
```
|
|
246
|
+
`ready()` always resolves. Failures are reported through `onError`, never thrown from `ready()`. `transportMode: 'sse'` does not fall back to long-polling; it stays on the last cached snapshot (if any). `auto` tries SSE, then long-polling.
|
|
450
247
|
|
|
451
|
-
|
|
248
|
+
| `error.code` | Meaning |
|
|
249
|
+
|---|---|
|
|
250
|
+
| `ERR_AUTH` | Invalid API key, missing session, inactive subscription |
|
|
251
|
+
| `ERR_RATE_LIMITED` | Evaluation quota exceeded. Check `retryAfter` (seconds) and `resetTime`. Free-tier payloads may include a last snapshot in `data`; the SDK applies it and keeps serving cache. |
|
|
252
|
+
| `ERR_INVALID_CONTEXT` | Stream rejected the context payload |
|
|
253
|
+
| `ERR_INTERNAL` | Named SSE `error` from the server |
|
|
452
254
|
|
|
453
255
|
```ts
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
const cached = await redisClient.get(`flags:${apiKey}`);
|
|
464
|
-
return cached ? JSON.parse(cached) : null;
|
|
465
|
-
},
|
|
466
|
-
saveFlags: async (apiKey, flags) => {
|
|
467
|
-
await redisClient.setex(
|
|
468
|
-
`flags:${apiKey}`,
|
|
469
|
-
600, // 10 min TTL
|
|
470
|
-
JSON.stringify(flags)
|
|
471
|
-
);
|
|
472
|
-
},
|
|
473
|
-
loadContext: async (apiKey) => {
|
|
474
|
-
const cached = await redisClient.get(`context:${apiKey}`);
|
|
475
|
-
return cached ? JSON.parse(cached) : null;
|
|
476
|
-
},
|
|
477
|
-
saveContext: async (apiKey, context) => {
|
|
478
|
-
await redisClient.setex(
|
|
479
|
-
`context:${apiKey}`,
|
|
480
|
-
3600, // 1 hour TTL
|
|
481
|
-
JSON.stringify(context)
|
|
482
|
-
);
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
});
|
|
256
|
+
onError: (error) => {
|
|
257
|
+
const { code, retryAfter, resetTime, message } = error as {
|
|
258
|
+
code?: string;
|
|
259
|
+
retryAfter?: number;
|
|
260
|
+
resetTime?: string;
|
|
261
|
+
message: string;
|
|
262
|
+
};
|
|
263
|
+
console.error(code, message, retryAfter, resetTime);
|
|
264
|
+
}
|
|
486
265
|
```
|
|
487
266
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
### `FlagClient`
|
|
491
|
-
|
|
492
|
-
**Methods:**
|
|
493
|
-
- `ready(timeoutMs?: number): Promise<void>` - Wait for initial connection (default timeout: 3000ms)
|
|
494
|
-
- `getFlag<K>(key: K, fallback?: T): T` - Get flag value with optional fallback
|
|
495
|
-
- `getFlags(): FeatureFlags<T>` - Get all flags as an object
|
|
496
|
-
- `updateContext(context: Record<string, any>): Promise<void>` - Update evaluation context (async)
|
|
497
|
-
- `subscribe(callback: (flags) => void): () => void` - Subscribe to flag updates; fires immediately with current flags, returns unsubscribe function
|
|
498
|
-
- `destroy(): void` - Close transport connection, clear all subscribers, and release resources
|
|
499
|
-
|
|
500
|
-
### Subscribing to Flag Updates
|
|
267
|
+
---
|
|
501
268
|
|
|
502
|
-
|
|
269
|
+
# Subscribing
|
|
503
270
|
|
|
504
271
|
```ts
|
|
505
272
|
const unsubscribe = client.subscribe((flags) => {
|
|
506
|
-
|
|
507
|
-
|
|
273
|
+
const theme = client.getFlag('theme', 'light');
|
|
274
|
+
console.log(theme, flags);
|
|
508
275
|
});
|
|
509
276
|
|
|
510
|
-
// Later, to unsubscribe:
|
|
511
277
|
unsubscribe();
|
|
512
278
|
```
|
|
513
279
|
|
|
514
|
-
|
|
280
|
+
The callback runs immediately with the current snapshot, then on every push. `destroy()` clears subscribers.
|
|
515
281
|
|
|
516
|
-
|
|
517
|
-
interface CacheAdapter {
|
|
518
|
-
loadFlags(apiKey: string): Promise<Record<string, any>> | Record<string, any>;
|
|
519
|
-
saveFlags(apiKey: string, flags: Record<string, any>): Promise<void> | void;
|
|
520
|
-
loadContext(apiKey: string): Promise<Record<string, any> | null> | Record<string, any> | null;
|
|
521
|
-
saveContext(apiKey: string, context: Record<string, any>): Promise<void> | void;
|
|
522
|
-
}
|
|
523
|
-
```
|
|
282
|
+
---
|
|
524
283
|
|
|
525
|
-
|
|
284
|
+
# Offline cache
|
|
526
285
|
|
|
527
|
-
|
|
286
|
+
Enabled by default. On boot the SDK loads `flagmint_<apiKey>_flags` from localStorage (24h TTL). If the stream fails, those values stay in memory and `onError` fires.
|
|
528
287
|
|
|
529
|
-
```
|
|
530
|
-
|
|
288
|
+
```ts
|
|
289
|
+
cacheAdapter: {
|
|
290
|
+
loadFlags(apiKey, ttl) { /* ... */ },
|
|
291
|
+
saveFlags(apiKey, data) { /* ... */ },
|
|
292
|
+
loadContext(apiKey) { /* ... */ },
|
|
293
|
+
saveContext(apiKey, ctx) { /* ... */ }
|
|
294
|
+
}
|
|
531
295
|
```
|
|
532
296
|
|
|
533
|
-
|
|
297
|
+
Use `syncCache` in the browser and `asyncCache` (or your own) in Node / React Native.
|
|
534
298
|
|
|
535
|
-
|
|
536
|
-
npm test
|
|
537
|
-
```
|
|
299
|
+
---
|
|
538
300
|
|
|
539
|
-
|
|
301
|
+
# Debug logging
|
|
540
302
|
|
|
541
|
-
```
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
โ โโโ helpers/ # Cache helpers (sync & async)
|
|
548
|
-
โ โโโ transports/ # Transport implementations
|
|
303
|
+
```ts
|
|
304
|
+
const client = new FlagClient({
|
|
305
|
+
apiKey: 'ff_your_api_key',
|
|
306
|
+
enableFlagmint: true,
|
|
307
|
+
debugLog: true
|
|
308
|
+
});
|
|
549
309
|
```
|
|
550
310
|
|
|
551
|
-
|
|
311
|
+
In production, only connection lifecycle messages are emitted.
|
|
552
312
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
- Verify your API key is correct
|
|
556
|
-
- Check network connectivity
|
|
557
|
-
- Review browser console for errors
|
|
558
|
-
- Ensure CORS is properly configured if using from browser
|
|
559
|
-
|
|
560
|
-
### Flags not updating
|
|
561
|
-
|
|
562
|
-
- Confirm `enableOfflineCache` setting matches your needs
|
|
563
|
-
- Check transport mode (WebSocket vs long-polling)
|
|
564
|
-
- Verify context is properly set with `await updateContext()`
|
|
565
|
-
- Use `subscribe()` to listen for flag changes instead of polling
|
|
566
|
-
|
|
567
|
-
### Cache behavior when server is down
|
|
313
|
+
---
|
|
568
314
|
|
|
569
|
-
|
|
570
|
-
- Cached flags are loaded even if the server is unreachable
|
|
571
|
-
- `ready()` **resolves** (does not throw) if cached flags are available โ the `onError` callback is invoked instead to signal degraded mode
|
|
572
|
-
- `ready()` only rejects if **no cached flags exist** and the server is unreachable
|
|
573
|
-
- You can safely call `getFlag()` after `ready()` resolves, regardless of whether the connection succeeded
|
|
315
|
+
# API
|
|
574
316
|
|
|
575
|
-
|
|
317
|
+
```ts
|
|
318
|
+
await client.ready()
|
|
319
|
+
client.getFlag(key, fallback)
|
|
320
|
+
client.getFlags()
|
|
321
|
+
await client.updateContext(context)
|
|
322
|
+
client.subscribe(callback) // returns unsubscribe
|
|
323
|
+
client.destroy()
|
|
324
|
+
```
|
|
576
325
|
|
|
577
|
-
|
|
578
|
-
- Implement proper cache TTLs
|
|
579
|
-
- Monitor transport connection status
|
|
326
|
+
---
|
|
580
327
|
|
|
581
|
-
|
|
328
|
+
# Framework support
|
|
582
329
|
|
|
583
|
-
|
|
584
|
-
- **Issues**: Report on [GitHub](https://github.com/flagmint/js-sdk/issues)
|
|
585
|
-
- **Email**: support@flagmint.io
|
|
330
|
+
React, Vue, Angular, Next.js, Nuxt, Express, Fastify, Node.js, vanilla JavaScript.
|
|
586
331
|
|
|
587
|
-
|
|
332
|
+
Wrappers (`flagmint-vuejs-feature-flags`, `flagmint-react-sdk`) construct `FlagClient` for you. Connection sharing lives in this core SDK, not in the wrappers.
|
|
588
333
|
|
|
589
|
-
|
|
334
|
+
---
|
|
590
335
|
|
|
591
|
-
|
|
336
|
+
# License
|
|
592
337
|
|
|
593
|
-
|
|
594
|
-
* Type definitions: [sdk/core/types.ts](sdk/core/types.ts)
|
|
595
|
-
* Cache helpers: [sdk/core/helpers/cacheHelper.ts](sdk/core/helpers/cacheHelper.ts), [cacheHelper.async.ts](sdk/core/helpers/cacheHelper.async.ts)
|
|
596
|
-
* Transports: [WebsocketTransport](sdk/core/transports/WebsocketTransport.ts), [LongPollingTransport](sdk/core/transports/LongPollingTransport.ts)
|
|
597
|
-
* Evaluation: [evaluateFlagValue](sdk/core/evaluation/evaluateFlagValue.ts), [evaluateRollout](sdk/core/evaluation/evaluateRollout.ts), [rolloutUtils](sdk/core/evaluation/rolloutUtils.ts)
|
|
338
|
+
BSD 3-Clause License ยฉ Flagmint Team
|
|
598
339
|
|
|
599
340
|
---
|
|
600
341
|
|
|
601
|
-
**
|
|
342
|
+
- **Documentation**: See [Flagmint Docs](https://docs.flagmint.com/sdks/nodejs)
|
|
343
|
+
<!-- - **Issues**: Report on [GitHub](https://github.com/flagmint/js-sdk/issues) -->
|
|
344
|
+
- **Email**: support@flagmint.com
|
|
602
345
|
|
|
603
346
|
**Maintained with โค๏ธ by the Flagmint Team**
|
|
347
|
+
|