flagmint-js-sdk 1.2.26 โ 2.2.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 +215 -471
- package/dist/flagmint.cjs.js +39410 -10
- package/dist/flagmint.cjs.js.map +1 -0
- package/dist/flagmint.es.js +33166 -14332
- package/dist/flagmint.es.js.map +1 -0
- package/dist/flagmint.umd.js +39422 -18
- package/dist/flagmint.umd.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/sdk/core/client.d.ts +97 -9
- package/dist/sdk/core/config-sync/aslEcdh.d.ts +32 -0
- package/dist/sdk/core/config-sync/evaluateSdkFlag.d.ts +26 -0
- package/dist/sdk/core/config-sync/flagEvaluator.d.ts +373 -0
- package/dist/sdk/core/config-sync/flattenContext.d.ts +28 -0
- package/dist/sdk/core/config-sync/handshake.d.ts +21 -0
- package/dist/sdk/core/config-sync/index.d.ts +13 -0
- package/dist/sdk/core/config-sync/rulesStore.d.ts +56 -0
- package/dist/sdk/core/config-sync/signPayload.d.ts +17 -0
- package/dist/sdk/core/config-sync/stringHash.d.ts +7 -0
- package/dist/sdk/core/config-sync/types.d.ts +72 -0
- package/dist/sdk/core/helpers/applicationEvents.d.ts +27 -0
- package/dist/sdk/core/helpers/connectionShare.d.ts +222 -0
- package/dist/sdk/core/helpers/jsonCloneable.d.ts +2 -0
- package/dist/sdk/core/transports/SSETransport.d.ts +142 -0
- 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
|
-
|
|
70
|
-
|
|
71
|
-
| Option | Type | Default | Description |
|
|
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()`. |
|
|
85
|
-
|
|
86
|
-
## ๐ชต Debug Logging
|
|
87
|
-
|
|
88
|
-
By default the SDK is **silent** โ no console output in any environment. To enable verbose logging during development, call `logger.setup()` before creating your client:
|
|
89
|
-
|
|
90
|
-
```ts
|
|
91
|
-
import { logger } from 'flagmint-js-sdk';
|
|
92
|
-
|
|
93
|
-
logger.setup({ debugLog: true });
|
|
94
|
-
|
|
95
|
-
const client = new FlagClient({ apiKey: '...' });
|
|
96
|
-
```
|
|
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.
|
|
97
76
|
|
|
98
|
-
|
|
77
|
+
## Node.js
|
|
99
78
|
|
|
100
|
-
|
|
101
|
-
// These messages always appear in production:
|
|
102
|
-
// "[WebSocketTransport] Connected"
|
|
103
|
-
// "[WebSocketTransport] Connection closed: 1000"
|
|
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`).
|
|
104
80
|
|
|
105
|
-
|
|
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
103
|
|
|
141
|
-
|
|
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
|
-
|
|
154
|
-
### Custom Endpoints
|
|
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 stream base URL (`/stream` only). Defaults to `stream.flagmint.com` / `staging-streamโฆ` |
|
|
141
|
+
| `flagsEndpoint` | string | from `restEndpoint` | Override the API base for `POST /context`. Needed when stream and API hosts differ |
|
|
142
|
+
| `handshakeEndpoint` | string | env default | Override the ASL handshake URL. Use with `restEndpoint` / `sseEndpoint` for self-hosted gateways |
|
|
143
|
+
| `restEndpoint` | string | env default | Override REST evaluate URL (also drives events + default context host) |
|
|
144
|
+
| `env` | string | `NODE_ENV` | `development` \| `staging` \| `production` |
|
|
145
|
+
| `wrapperInfo` | `{ name, version }` | native-js | Framework wrapper telemetry on the stream URL |
|
|
146
|
+
| `previewMode` / `rawFlags` | | | Local-only evaluation, no network |
|
|
147
|
+
| `deferInitialization` | boolean | `false` | Wait for `ready()` before connecting |
|
|
148
|
+
| `debugLog` | boolean | `false` | Verbose SDK logs |
|
|
149
|
+
| `onError` | `(error) => void` | | Auth, quota, transport, and context errors. `ready()` still resolves. |
|
|
199
150
|
|
|
200
|
-
|
|
201
|
-
* If matched, applies rollout (percentage or variant).
|
|
202
|
-
* Returns flagโs value or `null` if not enabled.
|
|
151
|
+
---
|
|
203
152
|
|
|
204
|
-
|
|
153
|
+
# Transport modes
|
|
205
154
|
|
|
206
155
|
```ts
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
156
|
+
transportMode: 'auto' // SSE, then long-polling if SSE init fails
|
|
157
|
+
transportMode: 'sse' // stream only โ reports failures through onError; uses cached flags if any (no long-polling fallback)
|
|
158
|
+
transportMode: 'long-polling' // POST evaluate on an interval
|
|
210
159
|
```
|
|
211
160
|
|
|
212
|
-
|
|
213
|
-
* Supports **variant** distributions (A/B tests).
|
|
214
|
-
|
|
215
|
-
### `rolloutUtils`
|
|
161
|
+
---
|
|
216
162
|
|
|
217
|
-
|
|
218
|
-
* `pickVariant(value: string, variants: VariantOption[]): string | number | boolean | null`
|
|
163
|
+
# Connection sharing
|
|
219
164
|
|
|
220
|
-
|
|
165
|
+
Used when the same app is embedded twice on one origin (for example two WeSeeDo iframes in a host page).
|
|
221
166
|
|
|
222
|
-
|
|
167
|
+
- **Leader** performs handshake and holds EventSource.
|
|
168
|
+
- **Followers** receive flag snapshots over `BroadcastChannel` (`flagmint-share:<apiKey>`). Leadership is a `localStorage` lock (`flagmint_share_lock:<apiKey>`).
|
|
169
|
+
- 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.
|
|
170
|
+
- 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
171
|
|
|
224
172
|
```ts
|
|
225
|
-
|
|
173
|
+
// Default in browsers with BroadcastChannel โ one stream for this API key
|
|
174
|
+
shareConnection: true
|
|
226
175
|
|
|
227
|
-
|
|
176
|
+
// Two clients, two contexts, two streams โ or no BroadcastChannel
|
|
177
|
+
shareConnection: false
|
|
228
178
|
```
|
|
229
179
|
|
|
230
|
-
|
|
180
|
+
Node cluster workers do not share. Each worker process opens its own stream.
|
|
231
181
|
|
|
232
|
-
|
|
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
|
-
});
|
|
248
|
-
|
|
249
|
-
export function App() {
|
|
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
|
-
```
|
|
182
|
+
---
|
|
278
183
|
|
|
279
|
-
|
|
184
|
+
# Updating context
|
|
280
185
|
|
|
281
186
|
```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 });
|
|
187
|
+
await client.updateContext({
|
|
188
|
+
kind: 'multi',
|
|
189
|
+
user: { kind: 'user', key: '123' },
|
|
190
|
+
organization: { kind: 'organization', key: 'acme' }
|
|
316
191
|
});
|
|
317
192
|
```
|
|
318
193
|
|
|
319
|
-
|
|
194
|
+
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
195
|
|
|
321
|
-
|
|
196
|
+
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.
|
|
322
197
|
|
|
323
|
-
|
|
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
|
-
```
|
|
198
|
+
---
|
|
349
199
|
|
|
350
|
-
|
|
200
|
+
# Tracking application errors
|
|
351
201
|
|
|
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
|
-
```
|
|
202
|
+
`getFlag()` tells you what was served and, when Analytics Tracking is on for that flag, queues a **debounced call-site evaluation** report (dashboard Evaluations). That report is not a billing meter.
|
|
362
203
|
|
|
363
|
-
|
|
204
|
+
`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
205
|
|
|
365
|
-
|
|
206
|
+
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
207
|
|
|
367
208
|
```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
|
-
```
|
|
209
|
+
const checkoutOn = client.getFlag('checkout_redesign', false);
|
|
380
210
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
apiKey: '...',
|
|
388
|
-
onError: (error) => {
|
|
389
|
-
console.error('Flag client error:', error);
|
|
390
|
-
// Report to monitoring service
|
|
391
|
-
sentry.captureException(error);
|
|
392
|
-
}
|
|
393
|
-
});
|
|
211
|
+
try {
|
|
212
|
+
await submitPayment();
|
|
213
|
+
} catch (error) {
|
|
214
|
+
client.trackError('checkout_redesign', error, { step: 'payment' });
|
|
215
|
+
throw error;
|
|
216
|
+
}
|
|
394
217
|
```
|
|
395
218
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
The SDK gracefully handles connection failures using cached flags as a fallback:
|
|
219
|
+
`track(flagKey, eventName)` uses the same pipe for custom metrics (conversions, etc.). Only `kind: 'error'` is counted in app error rate today.
|
|
399
220
|
|
|
400
|
-
**When the server is unreachable:**
|
|
401
221
|
|
|
402
|
-
| Scenario | `ready()` Promise | Behavior |
|
|
403
|
-
|----------|-------------------|----------|
|
|
404
|
-
| โ
Cached flags available | Resolves | Operates in degraded mode with cached flags; `onError` is called |
|
|
405
|
-
| โ No cached flags | Rejects | Initialization fails |
|
|
406
|
-
| โ
Connected successfully | Resolves | Normal operation with live updates |
|
|
407
|
-
|
|
408
|
-
**Degraded Mode:** When `ready()` resolves but the connection failed, the `onError` callback is triggered to notify you:
|
|
409
|
-
|
|
410
|
-
```ts
|
|
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
|
-
});
|
|
420
222
|
|
|
223
|
+
```tsx
|
|
224
|
+
const enabled = useFlag('checkout_redesign', false);
|
|
225
|
+
const { client } = useFlagmint();
|
|
421
226
|
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
|
|
227
|
+
await submitPayment();
|
|
426
228
|
} catch (error) {
|
|
427
|
-
|
|
428
|
-
|
|
229
|
+
client?.trackError?.('checkout_redesign', error);
|
|
230
|
+
throw error;
|
|
429
231
|
}
|
|
430
232
|
```
|
|
431
233
|
|
|
432
|
-
|
|
234
|
+
Full notes: `FF-EU/documentation/APPLICATION_ERROR_TRACKING.md`.
|
|
433
235
|
|
|
434
|
-
|
|
236
|
+
---
|
|
435
237
|
|
|
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
|
-
});
|
|
238
|
+
# Errors and quota
|
|
446
239
|
|
|
447
|
-
|
|
448
|
-
const checkout = client.getFlag('new_checkout', false); // true
|
|
449
|
-
```
|
|
240
|
+
`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
241
|
|
|
451
|
-
|
|
242
|
+
| `error.code` | Meaning |
|
|
243
|
+
|---|---|
|
|
244
|
+
| `ERR_AUTH` | Invalid API key, missing session, inactive subscription |
|
|
245
|
+
| `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. |
|
|
246
|
+
| `ERR_INVALID_CONTEXT` | Stream rejected the context payload |
|
|
247
|
+
| `ERR_INTERNAL` | Named SSE `error` from the server |
|
|
452
248
|
|
|
453
249
|
```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
|
-
});
|
|
250
|
+
onError: (error) => {
|
|
251
|
+
const { code, retryAfter, resetTime, message } = error as {
|
|
252
|
+
code?: string;
|
|
253
|
+
retryAfter?: number;
|
|
254
|
+
resetTime?: string;
|
|
255
|
+
message: string;
|
|
256
|
+
};
|
|
257
|
+
console.error(code, message, retryAfter, resetTime);
|
|
258
|
+
}
|
|
486
259
|
```
|
|
487
260
|
|
|
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
|
|
261
|
+
---
|
|
501
262
|
|
|
502
|
-
|
|
263
|
+
# Subscribing
|
|
503
264
|
|
|
504
265
|
```ts
|
|
505
266
|
const unsubscribe = client.subscribe((flags) => {
|
|
506
|
-
|
|
507
|
-
|
|
267
|
+
const theme = client.getFlag('theme', 'light');
|
|
268
|
+
console.log(theme, flags);
|
|
508
269
|
});
|
|
509
270
|
|
|
510
|
-
// Later, to unsubscribe:
|
|
511
271
|
unsubscribe();
|
|
512
272
|
```
|
|
513
273
|
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
```ts
|
|
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
|
-
```
|
|
274
|
+
The callback runs immediately with the current snapshot, then on every push. `destroy()` clears subscribers.
|
|
524
275
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
### Build
|
|
276
|
+
---
|
|
528
277
|
|
|
529
|
-
|
|
530
|
-
npm run build
|
|
531
|
-
```
|
|
278
|
+
# Offline cache
|
|
532
279
|
|
|
533
|
-
|
|
280
|
+
Enabled by default.
|
|
534
281
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
282
|
+
- **Browser:** loads `flagmint_<apiKey>_flags` from localStorage (24h TTL). With `configSync`, also hydrates/persists `flagmint_<apiKey>_rules` (lease `expiresAt`, not wall-clock TTL).
|
|
283
|
+
- **Node with no adapter:** config-sync rules still work **in memory** for that process (`RulesStore`). Helper persist is a no-op โ restart โ cold `fullConfig`. Plug in `setCacheStorage` / `setAsyncCacheStorage` or a custom `cacheAdapter` to survive restarts.
|
|
284
|
+
- If the stream fails, the last in-memory snapshot stays and `onError` fires.
|
|
538
285
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
โ โโโ transports/ # Transport implementations
|
|
286
|
+
```ts
|
|
287
|
+
cacheAdapter: {
|
|
288
|
+
loadFlags(apiKey, ttl) { /* ... */ },
|
|
289
|
+
saveFlags(apiKey, data) { /* ... */ },
|
|
290
|
+
loadContext(apiKey) { /* ... */ },
|
|
291
|
+
saveContext(apiKey, ctx) { /* ... */ },
|
|
292
|
+
loadRulesSnapshot(apiKey) { /* ... */ }, // configSync
|
|
293
|
+
saveRulesSnapshot(apiKey, snap) { /* ... */ },
|
|
294
|
+
}
|
|
549
295
|
```
|
|
550
296
|
|
|
551
|
-
|
|
297
|
+
Use `syncCache` in the browser and `asyncCache` (or your own) in Node / React Native. See `sdk/core/helpers/Readme.md`.
|
|
552
298
|
|
|
553
|
-
|
|
299
|
+
---
|
|
554
300
|
|
|
555
|
-
|
|
556
|
-
- Check network connectivity
|
|
557
|
-
- Review browser console for errors
|
|
558
|
-
- Ensure CORS is properly configured if using from browser
|
|
301
|
+
# Debug logging
|
|
559
302
|
|
|
560
|
-
|
|
303
|
+
```ts
|
|
304
|
+
const client = new FlagClient({
|
|
305
|
+
apiKey: 'ff_your_api_key',
|
|
306
|
+
enableFlagmint: true,
|
|
307
|
+
debugLog: true
|
|
308
|
+
});
|
|
309
|
+
```
|
|
561
310
|
|
|
562
|
-
|
|
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
|
|
311
|
+
In production, only connection lifecycle messages are emitted.
|
|
566
312
|
|
|
567
|
-
|
|
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
|
+
|