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 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
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
- In **production** (`NODE_ENV=production`), only connection and disconnection messages are emitted regardless of the `debugLog` setting โ€” all other internal logs are suppressed.
77
+ ## Node.js
99
78
 
100
- ```ts
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
- // 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
103
 
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
-
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 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
- * 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.
151
+ ---
203
152
 
204
- ### `evaluateRollout(rollout, context)`
153
+ # Transport modes
205
154
 
206
155
  ```ts
207
- import { evaluateRollout } from 'flagmint-js-sdk';
208
-
209
- const rolloutValue = evaluateRollout(rolloutConfig, userContext);
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
- * Supports **percentage** rollouts using consistent hashing.
213
- * Supports **variant** distributions (A/B tests).
214
-
215
- ### `rolloutUtils`
161
+ ---
216
162
 
217
- * `hashToPercentage(value: string): number`
218
- * `pickVariant(value: string, variants: VariantOption[]): string | number | boolean | null`
163
+ # Connection sharing
219
164
 
220
- These helpers underpin reliable, deterministic assignment of users to flag variants.
165
+ Used when the same app is embedded twice on one origin (for example two WeSeeDo iframes in a host page).
221
166
 
222
- ### `isInSegment(segment, context)`
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
- import { isInSegment } from 'flagmint-js-sdk';
173
+ // Default in browsers with BroadcastChannel โ€” one stream for this API key
174
+ shareConnection: true
226
175
 
227
- const inBetaSegment = isInSegment(betaSegment, userContext);
176
+ // Two clients, two contexts, two streams โ€” or no BroadcastChannel
177
+ shareConnection: false
228
178
  ```
229
179
 
230
- Evaluates whether a user context matches a segment's criteria.
180
+ Node cluster workers do not share. Each worker process opens its own stream.
231
181
 
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
- });
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
- ### Node.js / Express
184
+ # Updating context
280
185
 
281
186
  ```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 });
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
- ## ๐Ÿ”„ Context Management
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
- ### Updating Context
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
- ```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
- ```
198
+ ---
349
199
 
350
- ### Persisting Context
200
+ # Tracking application errors
351
201
 
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
- ```
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
- ## ๐Ÿ”Œ Deferred Initialization
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
- For scenarios where you need to set up the client before context is available:
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 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
- ```
209
+ const checkoutOn = client.getFlag('checkout_redesign', false);
380
210
 
381
- ## ๐ŸŒ Advanced Usage
382
-
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
- });
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
- **Important: Offline Fallback & Cache Behavior**
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 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
227
+ await submitPayment();
426
228
  } catch (error) {
427
- // โŒ Only throws if NO cached flags AND server is unreachable
428
- console.error('Completely offline:', error);
229
+ client?.trackError?.('checkout_redesign', error);
230
+ throw error;
429
231
  }
430
232
  ```
431
233
 
432
- ### Preview Mode
234
+ Full notes: `FF-EU/documentation/APPLICATION_ERROR_TRACKING.md`.
433
235
 
434
- For testing or preview environments where you want to bypass remote flag fetching:
236
+ ---
435
237
 
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
- });
238
+ # Errors and quota
446
239
 
447
- // All getFlag calls will use rawFlags
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
- ### Custom Cache Implementations
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
- 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
- });
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
- ## ๐Ÿ“‹ 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
261
+ ---
501
262
 
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:
263
+ # Subscribing
503
264
 
504
265
  ```ts
505
266
  const unsubscribe = client.subscribe((flags) => {
506
- console.log('Flags updated:', flags);
507
- // Also fires immediately with current flags โ€” no need to call getFlags() separately
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
- ### Cache Adapter Interface
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
- ## ๐Ÿ› ๏ธ Development & Contributing
526
-
527
- ### Build
276
+ ---
528
277
 
529
- ```bash
530
- npm run build
531
- ```
278
+ # Offline cache
532
279
 
533
- ### Test
280
+ Enabled by default.
534
281
 
535
- ```bash
536
- npm test
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
- ### Project Structure
540
-
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
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
- ## ๐Ÿ› Troubleshooting
297
+ Use `syncCache` in the browser and `asyncCache` (or your own) in Node / React Native. See `sdk/core/helpers/Readme.md`.
552
298
 
553
- ### Client not connecting
299
+ ---
554
300
 
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
301
+ # Debug logging
559
302
 
560
- ### Flags not updating
303
+ ```ts
304
+ const client = new FlagClient({
305
+ apiKey: 'ff_your_api_key',
306
+ enableFlagmint: true,
307
+ debugLog: true
308
+ });
309
+ ```
561
310
 
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
311
+ In production, only connection lifecycle messages are emitted.
566
312
 
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
+