flagmint-js-sdk 1.2.26 โ†’ 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 CHANGED
@@ -1,603 +1,347 @@
1
1
  # Flagmint JavaScript SDK
2
2
 
3
- **Version: v1.2.25**
3
+ > A framework-agnostic JavaScript SDK for evaluating and streaming feature flags in browsers and Node.js.
4
4
 
5
- This SDK provides a javascript framework-agnostic client for evaluating feature flags, with pluggable caching (sync or async) and transport strategies (WebSocket or long-polling). It's designed for both browser and server-side Node.js environments.
5
+ ![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue)
6
+ ![Node](https://img.shields.io/badge/Node.js-18%2B-green)
7
+ ![License](https://img.shields.io/badge/license-BSD--3--Clause-lightgrey)
6
8
 
7
- ## ๐Ÿ“‹ Release Notes
9
+ ## Features
8
10
 
9
- ### v1.2.25
10
- - ๐Ÿ”‡ **Controllable debug logging** โ€” SDK logs are now silent by default; opt in with `logger.setup({ debugLog: true })`. In production, only connection/disconnection messages are ever emitted regardless of the setting.
11
- - ๐Ÿ”Œ **Configurable endpoints** โ€” `restEndpoint` and `wsEndpoint` can now be passed directly in `FlagClientOptions`, overriding the environment-derived defaults. Useful for proxies, staging overrides, or self-hosted deployments.
12
- - ๐Ÿ“ก **`subscribe()` delivers current flags immediately** โ€” the callback is now called synchronously with the current flag state at subscription time, so you never miss the initial value.
13
- - ๐Ÿงน **`destroy()` clears all subscribers** โ€” calling `destroy()` now also clears the subscriber set, preventing stale callbacks from holding references after teardown.
14
- - ๐Ÿ›ก๏ธ **WebSocket auth vs rate-limit error discrimination** โ€” close code `1008`/`4001` now inspects the close reason to distinguish `ERR_AUTH` (invalid API key) from `ERR_RATE_LIMITED` errors. Rate-limit errors include a `resetTime` field parsed from the close reason.
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
- ### v1.2.24
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
- ## โœจ Key Features
20
+ ### v2.0.0
24
21
 
25
- - ๐ŸŽฏ **Framework-Agnostic**: Works with React, Vue, vanilla JS, Node.js, and more
26
- - ๐Ÿ”„ **Flexible Transport**: WebSocket for real-time updates or long-polling fallback
27
- - ๐Ÿ’พ **Pluggable Caching**: Use built-in sync cache, async cache (Redis/filesystem), or custom implementations
28
- - ๐Ÿš€ **Server & Browser Support**: Compatible with browser environments, Node.js, React Native
29
- - ๐Ÿ”’ **Type-Safe**: Full TypeScript support with comprehensive type definitions
30
- - โšก **Zero-Config Defaults**: Works out of the box with sensible defaults
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
- ## ๐Ÿ“ฆ Installation
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
- # or
40
+ ```
41
+
42
+ ```bash
37
43
  yarn add flagmint-js-sdk
38
44
  ```
39
45
 
40
- ## Quick Start
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: 'ff_your_api_key_here',
56
+ apiKey: 'ff_your_api_key',
57
+ enableFlagmint: true,
48
58
  context: {
49
- kind: "multi | user | organisation",
50
- user: { kind: "user", key: '', email: '' },
51
- organization: { kind: "organization", key: '', plan: '' }
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
- // Get flag values
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
- ## FlagClientOptions
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
- | 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()`. |
77
+ ## Node.js
85
78
 
86
- ## ๐Ÿชต Debug Logging
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
- 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
- ```
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, syncCache } from 'flagmint-js-sdk';
116
- // No setup needed for browser; for Node you can override:
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
- ```ts
128
- import { FlagClient, asyncCache } from 'flagmint-js-sdk';
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
- cacheAdapter: {
133
- loadFlags: asyncCache.loadCachedFlags,
134
- saveFlags: asyncCache.saveCachedFlags,
135
- loadContext: asyncCache.loadCachedContext,
136
- saveContext: asyncCache.saveCachedContext
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
- ### 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
- ### WebSocket Connection State
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
- ### Transport Performance
109
+ # How it works
181
110
 
182
- | Mode | Latency | Bandwidth | Best For |
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
- ## โš™๏ธ Evaluation Helpers
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
- The SDK includes utilities to evaluate flag targeting rules and rollouts:
120
+ `getFlag()` / `getFlags()` never hit the network. They read the last evaluated snapshot.
191
121
 
192
- ### `evaluateFlagValue(flag, context)`
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
- ```ts
195
- import { evaluateFlagValue } from 'flagmint-js-sdk';
124
+ ---
196
125
 
197
- const result = evaluateFlagValue(flagValue, userContext);
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
- * Applies targeting rules (`segment` or attribute rules).
201
- * If matched, applies rollout (percentage or variant).
202
- * Returns flagโ€™s value or `null` if not enabled.
149
+ ---
203
150
 
204
- ### `evaluateRollout(rollout, context)`
151
+ # Transport modes
205
152
 
206
153
  ```ts
207
- import { evaluateRollout } from 'flagmint-js-sdk';
208
-
209
- const rolloutValue = evaluateRollout(rolloutConfig, userContext);
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
- * Supports **percentage** rollouts using consistent hashing.
213
- * Supports **variant** distributions (A/B tests).
214
-
215
- ### `rolloutUtils`
159
+ ---
216
160
 
217
- * `hashToPercentage(value: string): number`
218
- * `pickVariant(value: string, variants: VariantOption[]): string | number | boolean | null`
161
+ # Connection sharing
219
162
 
220
- These helpers underpin reliable, deterministic assignment of users to flag variants.
163
+ Used when the same app is embedded twice on one origin (for example two WeSeeDo iframes in a host page).
221
164
 
222
- ### `isInSegment(segment, context)`
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
- import { isInSegment } from 'flagmint-js-sdk';
171
+ // Default in browsers with BroadcastChannel โ€” one stream for this API key
172
+ shareConnection: true
226
173
 
227
- const inBetaSegment = isInSegment(betaSegment, userContext);
174
+ // Two clients, two contexts, two streams โ€” or no BroadcastChannel
175
+ shareConnection: false
228
176
  ```
229
177
 
230
- Evaluates whether a user context matches a segment's criteria.
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
- 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
- ```
180
+ ---
278
181
 
279
- ### Node.js / Express
182
+ # Updating context
280
183
 
281
184
  ```ts
282
- import { FlagClient, asyncCache } from 'flagmint-js-sdk';
283
-
284
- const client = new FlagClient({
285
- apiKey: 'ff_...',
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
- ## ๐Ÿ”„ Context Management
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
- ### Updating Context
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
- ### Persisting Context
196
+ ---
351
197
 
352
- ```ts
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
- ## ๐Ÿ”Œ Deferred Initialization
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
- For scenarios where you need to set up the client before context is available:
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 client = new FlagClient({
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
- ### Error Handling
384
-
385
- ```ts
386
- const client = new FlagClient({
387
- apiKey: '...',
388
- onError: (error) => {
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
- **Important: Offline Fallback & Cache Behavior**
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
- The SDK gracefully handles connection failures using cached flags as a fallback:
217
+ ### Manual verification
399
218
 
400
- **When the server is unreachable:**
219
+ The Flagmint dashboard evaluates its own flags with `flagmint-react-sdk` (`useFlag('homepage_variant')`). Boom that path, don't curl:
401
220
 
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 |
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
- **Degraded Mode:** When `ready()` resolves but the connection failed, the `onError` callback is triggered to notify you:
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
- ```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
- });
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 client.ready();
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
- // โŒ Only throws if NO cached flags AND server is unreachable
428
- console.error('Completely offline:', error);
235
+ client?.trackError?.('checkout_redesign', error);
236
+ throw error;
429
237
  }
430
238
  ```
431
239
 
432
- ### Preview Mode
240
+ Full notes: `FF-EU/documentation/APPLICATION_ERROR_TRACKING.md`.
433
241
 
434
- For testing or preview environments where you want to bypass remote flag fetching:
242
+ ---
435
243
 
436
- ```ts
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
- // All getFlag calls will use rawFlags
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
- ### Custom Cache Implementations
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
- import { FlagClient } from 'flagmint-js-sdk';
455
- import redis from 'redis';
456
-
457
- const redisClient = redis.createClient();
458
-
459
- const client = new FlagClient({
460
- apiKey: '...',
461
- cacheAdapter: {
462
- loadFlags: async (apiKey) => {
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
- ## ๐Ÿ“‹ API Reference
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
- Instead of event listeners, use the `subscribe()` method. The callback is called **immediately** with the current flag state at subscription time, then again on every subsequent update:
269
+ # Subscribing
503
270
 
504
271
  ```ts
505
272
  const unsubscribe = client.subscribe((flags) => {
506
- console.log('Flags updated:', flags);
507
- // Also fires immediately with current flags โ€” no need to call getFlags() separately
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
- ### Cache Adapter Interface
280
+ The callback runs immediately with the current snapshot, then on every push. `destroy()` clears subscribers.
515
281
 
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
- ```
282
+ ---
524
283
 
525
- ## ๐Ÿ› ๏ธ Development & Contributing
284
+ # Offline cache
526
285
 
527
- ### Build
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
- ```bash
530
- npm run build
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
- ### Test
297
+ Use `syncCache` in the browser and `asyncCache` (or your own) in Node / React Native.
534
298
 
535
- ```bash
536
- npm test
537
- ```
299
+ ---
538
300
 
539
- ### Project Structure
301
+ # Debug logging
540
302
 
541
- ```
542
- sdk/
543
- โ”œโ”€โ”€ core/
544
- โ”‚ โ”œโ”€โ”€ client.ts # Main FlagClient
545
- โ”‚ โ”œโ”€โ”€ types.ts # Type definitions
546
- โ”‚ โ”œโ”€โ”€ evaluation/ # Flag evaluation logic
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
- ## ๐Ÿ› Troubleshooting
311
+ In production, only connection lifecycle messages are emitted.
552
312
 
553
- ### Client not connecting
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
- **Important**: When `enableOfflineCache: true` (default):
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
- ### Performance issues
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
- - Consider using long-polling instead of WebSocket for high-traffic scenarios
578
- - Implement proper cache TTLs
579
- - Monitor transport connection status
326
+ ---
580
327
 
581
- ## ๐Ÿ“ž Support & Resources
328
+ # Framework support
582
329
 
583
- - **Documentation**: See [Flagmint Docs](https://docs.flagmint.io)
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
- ## ๐Ÿš€ Releases & Changelog
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
- See [GitHub Releases](https://github.com/flagmint/js-sdk/releases) for the complete version history, changelog, and upgrade guides.
334
+ ---
590
335
 
591
- ## ๐Ÿ“– References
336
+ # License
592
337
 
593
- * Core client: [sdk/core/client.ts](sdk/core/client.ts)
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
- **License**: BSD 3-Clause License ยฉ Flagmint Team
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
+