@reflag/node-sdk 1.0.0-alpha.1

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.
Files changed (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +835 -0
  3. package/dist/package.json +49 -0
  4. package/dist/src/batch-buffer.js +85 -0
  5. package/dist/src/batch-buffer.js.map +1 -0
  6. package/dist/src/cache.js +71 -0
  7. package/dist/src/cache.js.map +1 -0
  8. package/dist/src/client.js +1141 -0
  9. package/dist/src/client.js.map +1 -0
  10. package/dist/src/config.js +98 -0
  11. package/dist/src/config.js.map +1 -0
  12. package/dist/src/edgeClient.js +32 -0
  13. package/dist/src/edgeClient.js.map +1 -0
  14. package/dist/src/fetch-http-client.js +85 -0
  15. package/dist/src/fetch-http-client.js.map +1 -0
  16. package/dist/src/flusher.js +56 -0
  17. package/dist/src/flusher.js.map +1 -0
  18. package/dist/src/inRequestCache.js +72 -0
  19. package/dist/src/inRequestCache.js.map +1 -0
  20. package/dist/src/index.js +9 -0
  21. package/dist/src/index.js.map +1 -0
  22. package/dist/src/periodicallyUpdatingCache.js +78 -0
  23. package/dist/src/periodicallyUpdatingCache.js.map +1 -0
  24. package/dist/src/rate-limiter.js +52 -0
  25. package/dist/src/rate-limiter.js.map +1 -0
  26. package/dist/src/types.js +6 -0
  27. package/dist/src/types.js.map +1 -0
  28. package/dist/src/utils.js +207 -0
  29. package/dist/src/utils.js.map +1 -0
  30. package/dist/types/src/batch-buffer.d.ts +27 -0
  31. package/dist/types/src/batch-buffer.d.ts.map +1 -0
  32. package/dist/types/src/cache.d.ts +16 -0
  33. package/dist/types/src/cache.d.ts.map +1 -0
  34. package/dist/types/src/client.d.ts +410 -0
  35. package/dist/types/src/client.d.ts.map +1 -0
  36. package/dist/types/src/config.d.ts +20 -0
  37. package/dist/types/src/config.d.ts.map +1 -0
  38. package/dist/types/src/edgeClient.d.ts +25 -0
  39. package/dist/types/src/edgeClient.d.ts.map +1 -0
  40. package/dist/types/src/fetch-http-client.d.ts +20 -0
  41. package/dist/types/src/fetch-http-client.d.ts.map +1 -0
  42. package/dist/types/src/flusher.d.ts +4 -0
  43. package/dist/types/src/flusher.d.ts.map +1 -0
  44. package/dist/types/src/inRequestCache.d.ts +7 -0
  45. package/dist/types/src/inRequestCache.d.ts.map +1 -0
  46. package/dist/types/src/index.d.ts +4 -0
  47. package/dist/types/src/index.d.ts.map +1 -0
  48. package/dist/types/src/periodicallyUpdatingCache.d.ts +16 -0
  49. package/dist/types/src/periodicallyUpdatingCache.d.ts.map +1 -0
  50. package/dist/types/src/rate-limiter.d.ts +14 -0
  51. package/dist/types/src/rate-limiter.d.ts.map +1 -0
  52. package/dist/types/src/types.d.ts +639 -0
  53. package/dist/types/src/types.d.ts.map +1 -0
  54. package/dist/types/src/utils.d.ts +65 -0
  55. package/dist/types/src/utils.d.ts.map +1 -0
  56. package/package.json +50 -0
package/README.md ADDED
@@ -0,0 +1,835 @@
1
+ # Reflag Node.js SDK
2
+
3
+ Node.js, JavaScript/TypeScript client for [Reflag.com](https://reflag.com).
4
+
5
+ Reflag supports feature toggling, tracking feature usage, collecting feedback on features, and [remotely configuring features](#remote-config).
6
+
7
+ ## Installation
8
+
9
+ Install using your favorite package manager:
10
+
11
+ {% tabs %}
12
+ {% tab title="npm" %}
13
+
14
+ ```sh
15
+ npm i @reflag/node-sdk
16
+ ```
17
+
18
+ {% endtab %}
19
+
20
+ {% tab title="yarn" %}
21
+
22
+ ```sh
23
+ yarn add @reflag/node-sdk
24
+ ```
25
+
26
+ {% endtab %}
27
+
28
+ {% tab title="bun" %}
29
+
30
+ ```sh
31
+ bun add @reflag/node-sdk
32
+ ```
33
+
34
+ {% endtab %}
35
+
36
+ {% tab title="pnpm" %}
37
+
38
+ ```sh
39
+ pnpm add @reflag/node-sdk
40
+ ```
41
+
42
+ {% endtab %}
43
+
44
+ {% tab title="deno" %}
45
+
46
+ ```sh
47
+ deno add npm:@reflag/node-sdk
48
+ ```
49
+
50
+ {% endtab %}
51
+ {% endtabs %}
52
+
53
+ Other supported languages/frameworks are in the [Supported languages](https://docs.reflag.com/quickstart/supported-languages) documentation pages.
54
+
55
+ You can also [use the HTTP API directly](https://docs.reflag.com/api/http-api)
56
+
57
+ ## Basic usage
58
+
59
+ To get started you need to obtain your secret key from the [environment settings](https://app.reflag.com/env-current/settings/app-environments)
60
+ in Reflag.
61
+
62
+ > [!CAUTION]
63
+ > Secret keys are meant for use in server side SDKs only. Secret keys offer the users the ability to obtain
64
+ > information that is often sensitive and thus should not be used in client-side applications.
65
+
66
+ Reflag will load settings through the various environment variables automatically (see [Configuring](#configuring) below).
67
+
68
+ 1. Find the Reflag secret key for your development environment under [environment settings](https://app.reflag.com/env-current/settings/app-environments) in Reflag.
69
+ 2. Set `REFLAG_SECRET_KEY` in your `.env` file
70
+ 3. Create a `reflag.ts` file containing the following:
71
+
72
+ ```typescript
73
+ import { ReflagClient } from "@reflag/node-sdk";
74
+
75
+ // Create a new instance of the client with the secret key. Additional options
76
+ // are available, such as supplying a logger and other custom properties.
77
+ //
78
+ // We recommend that only one global instance of `client` should be created
79
+ // to avoid multiple round-trips to our servers.
80
+ export const reflagClient = new ReflagClient();
81
+
82
+ // Initialize the client and begin fetching feature targeting definitions.
83
+ // You must call this method prior to any calls to `getFlags()`,
84
+ // otherwise an empty object will be returned.
85
+ reflagClient.initialize().then({
86
+ console.log("Reflag initialized!")
87
+ })
88
+ ```
89
+
90
+ Once the client is initialized, you can obtain features along with the `isEnabled`
91
+ status to indicate whether the feature is targeted for this user/company:
92
+
93
+ > [!IMPORTANT]
94
+ > If `user.id` or `company.id` is not given, the whole `user` or `company` object is ignored.
95
+
96
+ ```typescript
97
+ // configure the client
98
+ const boundClient = reflagClient.bindClient({
99
+ user: {
100
+ id: "john_doe",
101
+ name: "John Doe",
102
+ email: "john@acme.com",
103
+ avatar: "https://example.com/users/jdoe",
104
+ },
105
+ company: {
106
+ id: "acme_inc",
107
+ name: "Acme, Inc.",
108
+ avatar: "https://example.com/companies/acme",
109
+ },
110
+ });
111
+
112
+ // get the huddle feature using company, user and custom context to
113
+ // evaluate the targeting.
114
+ const { isEnabled, track, config } = boundClient.getFlag("huddle");
115
+
116
+ if (isEnabled) {
117
+ // this is your feature gated code ...
118
+ // send an event when the feature is used:
119
+ track();
120
+
121
+ if (config?.key === "zoom") {
122
+ // this code will run if a given remote configuration
123
+ // is set up.
124
+ }
125
+
126
+ // CAUTION: if you plan to use the event for automated feedback surveys
127
+ // call `flush` immediately after `track`. It can optionally be awaited
128
+ // to guarantee the sent happened.
129
+ boundClient.flush();
130
+ }
131
+ ```
132
+
133
+ You can also use the `getFlags()` method which returns a map of all features:
134
+
135
+ ```typescript
136
+ // get the current features (uses company, user and custom context to
137
+ // evaluate the features).
138
+ const features = boundClient.getFlags();
139
+ const bothEnabled =
140
+ features.huddle?.isEnabled && features.voiceHuddle?.isEnabled;
141
+ ```
142
+
143
+ ## High performance feature targeting
144
+
145
+ The SDK contacts the Reflag servers when you call `initialize()`
146
+ and downloads the features with their targeting rules.
147
+ These rules are then matched against the user/company information you provide
148
+ to `getFlags()` (or through `bindClient(..).getFlags()`). That means the
149
+ `getFlags()` call does not need to contact the Reflag servers once
150
+ `initialize()` has completed. `ReflagClient` will continue to periodically
151
+ download the targeting rules from the Reflag servers in the background.
152
+
153
+ ### Batch Operations
154
+
155
+ The SDK automatically batches operations like user/company updates and feature tracking events to minimize API calls.
156
+ The batch buffer is configurable through the client options:
157
+
158
+ ```typescript
159
+ const client = new ReflagClient({
160
+ batchOptions: {
161
+ maxSize: 100, // Maximum number of events to batch
162
+ intervalMs: 1000, // Flush interval in milliseconds
163
+ },
164
+ });
165
+ ```
166
+
167
+ You can manually flush the batch buffer at any time:
168
+
169
+ ```typescript
170
+ await client.flush();
171
+ ```
172
+
173
+ > [!TIP]
174
+ > It's recommended to call `flush()` before your application shuts down to ensure all events are sent.
175
+
176
+ ### Rate Limiting
177
+
178
+ The SDK includes automatic rate limiting for feature events to prevent overwhelming the API.
179
+ Rate limiting is applied per unique combination of feature key and context. The rate limiter window size is configurable:
180
+
181
+ ```typescript
182
+ const client = new ReflagClient({
183
+ rateLimiterOptions: {
184
+ windowSizeMs: 60000, // Rate limiting window size in milliseconds
185
+ },
186
+ });
187
+ ```
188
+
189
+ ### Flag definitions
190
+
191
+ Flag definitions include the rules needed to determine which features should be enabled and which config values should be applied to any given user/company.
192
+ Flag definitions are automatically fetched when calling `initialize()`.
193
+ They are then cached and refreshed in the background.
194
+ It's also possible to get the currently in use feature definitions:
195
+
196
+ ```typescript
197
+ import fs from "fs";
198
+
199
+ const client = new ReflagClient();
200
+
201
+ const featureDefs = await client.getFlagDefinitions();
202
+ // [{
203
+ // key: "huddle",
204
+ // description: "Live voice conversations with colleagues."
205
+ // flag: { ... }
206
+ // config: { ... }
207
+ // }]
208
+ ```
209
+
210
+ ## Edge-runtimes like Cloudflare Workers
211
+
212
+ To use the Reflag NodeSDK with Cloudflare workers, set the `node_compat` flag [in your wrangler file](https://developers.cloudflare.com/workers/runtime-apis/nodejs/#get-started).
213
+
214
+ Instead of using `ReflagClient`, use `EdgeClient` and make sure you call `ctx.waitUntil(reflag.flush());` before returning from your worker function.
215
+
216
+ ```typescript
217
+ import { EdgeClient } from "@reflag/node-sdk";
218
+
219
+ // set the REFLAG_SECRET_KEY environment variable or pass the secret key in the constructor
220
+ const reflag = new EdgeClient();
221
+
222
+ export default {
223
+ async fetch(request, _env, ctx): Promise<Response> {
224
+ // initialize the client and wait for it to complete
225
+ // if the client was initialized on a previous invocation, this is a no-op.
226
+ await reflag.initialize();
227
+ const features = reflag.getFlags({
228
+ user: { id: "userId" },
229
+ company: { id: "companyId" },
230
+ });
231
+
232
+ // ensure all events are flushed and any requests to refresh the feature cache
233
+ // have completed after the response is sent
234
+ ctx.waitUntil(reflag.flush());
235
+
236
+ return new Response(
237
+ `Flags for user ${userId} and company ${companyId}: ${JSON.stringify(features, null, 2)}`,
238
+ );
239
+ },
240
+ };
241
+ ```
242
+
243
+ See [examples/cloudflare-worker](examples/cloudflare-worker/src/index.ts) for a deployable example.
244
+
245
+ Reflag maintains a cached set of feature definitions in the memory of your worker which it uses to decide which features to turn on for which users/companies.
246
+
247
+ The SDK caches feature definitions in memory for fast performance. The first request to a new worker instance fetches definitions from Reflag's servers, while subsequent requests use the cache. When the cache expires, it's updated in the background. `ctx.waitUntil(reflag.flush())` ensures completion of the background work, so response times are not affected. This background work may increase wall-clock time for your worker, but it will not measurably increase billable CPU time on platforms like Cloudflare.
248
+
249
+ ## Error Handling
250
+
251
+ The SDK is designed to fail gracefully and never throw exceptions to the caller. Instead, it logs errors and provides
252
+ fallback behavior:
253
+
254
+ 1. **Flag Evaluation Failures**:
255
+
256
+ ```typescript
257
+ const { isEnabled } = client.getFlag("my-feature");
258
+ // If feature evaluation fails, isEnabled will be false
259
+ ```
260
+
261
+ 2. **Network Errors**:
262
+
263
+ ```typescript
264
+ // Network errors during tracking are logged but don't affect your application
265
+ const { track } = client.getFlag("my-feature");
266
+ if (isEnabled) {
267
+ try {
268
+ await track();
269
+ } catch (error) {
270
+ // The SDK already logged this error
271
+ // Your application can continue normally
272
+ }
273
+ }
274
+ ```
275
+
276
+ 3. **Missing Context**:
277
+
278
+ ```typescript
279
+ // The SDK tracks missing context fields but continues operation
280
+ const features = client.getFlags({
281
+ user: { id: "user123" },
282
+ // Missing company context will be logged but won't cause errors
283
+ });
284
+ ```
285
+
286
+ 4. **Offline Mode**:
287
+
288
+ ```typescript
289
+ // In offline mode, the SDK uses feature overrides
290
+ const client = new ReflagClient({
291
+ offline: true,
292
+ featureOverrides: () => ({
293
+ "my-feature": true,
294
+ }),
295
+ });
296
+ ```
297
+
298
+ The SDK logs all errors with appropriate severity levels. You can customize logging by providing your own logger:
299
+
300
+ ```typescript
301
+ const client = new ReflagClient({
302
+ logger: {
303
+ debug: (msg) => console.debug(msg),
304
+ info: (msg) => console.info(msg),
305
+ warn: (msg) => console.warn(msg),
306
+ error: (msg, error) => {
307
+ console.error(msg, error);
308
+ // Send to your error tracking service
309
+ errorTracker.capture(error);
310
+ },
311
+ },
312
+ });
313
+ ```
314
+
315
+ ## Remote config
316
+
317
+ Remote config is a dynamic and flexible approach to configuring feature behavior outside of your app – without needing to re-deploy it.
318
+
319
+ Similar to `isEnabled`, each feature has a `config` property. This configuration is managed from within Reflag.
320
+ It is managed similar to the way access to features is managed, but instead of the binary `isEnabled` you can have
321
+ multiple configuration values which are given to different user/companies.
322
+
323
+ ```ts
324
+ const features = reflagClient.getFlags();
325
+ // {
326
+ // huddle: {
327
+ // isEnabled: true,
328
+ // targetingVersion: 42,
329
+ // config: {
330
+ // key: "gpt-3.5",
331
+ // payload: { maxTokens: 10000, model: "gpt-3.5-beta1" }
332
+ // }
333
+ // }
334
+ // }
335
+ ```
336
+
337
+ `key` is mandatory for a config, but if a feature has no config or no config value was matched against the context, the `key` will be `undefined`. Make sure to check against this case when trying to use the configuration in your application. `payload` is an optional JSON value for arbitrary configuration needs.
338
+
339
+ Just as `isEnabled`, accessing `config` on the object returned by `getFlags` does not automatically
340
+ generate a `check` event, contrary to the `config` property on the object returned by `getFlag`.
341
+
342
+ ## Configuring
343
+
344
+ The Reflag `Node.js` SDK can be configured through environment variables,
345
+ a configuration file on disk or by passing options to the `ReflagClient`
346
+ constructor. By default, the SDK searches for `reflag.config.json` in the
347
+ current working directory.
348
+
349
+ | Option | Type | Description | Env Var |
350
+ | ------------------ | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- |
351
+ | `secretKey` | string | The secret key used for authentication with Reflag's servers. | REFLAG_SECRET_KEY |
352
+ | `logLevel` | string | The log level for the SDK (e.g., `"DEBUG"`, `"INFO"`, `"WARN"`, `"ERROR"`). Default: `INFO` | REFLAG_LOG_LEVEL |
353
+ | `offline` | boolean | Operate in offline mode. Default: `false`, except in tests it will default to `true` based off of the `TEST` env. var. | REFLAG_OFFLINE |
354
+ | `apiBaseUrl` | string | The base API URL for the Reflag servers. | REFLAG_API_BASE_URL |
355
+ | `featureOverrides` | Record<string, boolean> | An object specifying feature overrides for testing or local development. See [examples/express/app.test.ts](https://github.com/reflagcom/javascript/tree/main/packages/node-sdk/examples/express/app.test.ts) for how to use `featureOverrides` in tests. | REFLAG_FLAGS_ENABLED, REFLAG_FLAGS_DISABLED |
356
+ | `configFile` | string | Load this config file from disk. Default: `reflag.config.json` | REFLAG_CONFIG_FILE |
357
+
358
+ > [!NOTE] > `REFLAG_FLAGS_ENABLED` and `REFLAG_FLAGS_DISABLED` are comma separated lists of features which will be enabled or disabled respectively.
359
+
360
+ `reflag.config.json` example:
361
+
362
+ ```json
363
+ {
364
+ "secretKey": "...",
365
+ "logLevel": "warn",
366
+ "offline": true,
367
+ "apiBaseUrl": "https://proxy.slick-demo.com",
368
+ "flagOverrides": {
369
+ "huddles": true,
370
+ "voiceChat": { "isEnabled": false },
371
+ "aiAssist": {
372
+ "isEnabled": true,
373
+ "config": {
374
+ "key": "gpt-4.0",
375
+ "payload": {
376
+ "maxTokens": 50000
377
+ }
378
+ }
379
+ }
380
+ }
381
+ }
382
+ ```
383
+
384
+ When using a `reflag.config.json` for local development, make sure you add it to your
385
+ `.gitignore` file. You can also set these options directly in the `ReflagClient`
386
+ constructor. The precedence for configuration options is as follows, listed in the
387
+ order of importance:
388
+
389
+ 1. Options passed along to the constructor directly,
390
+ 2. Environment variable,
391
+ 3. The config file.
392
+
393
+ ## Type safe flags
394
+
395
+ To get type checked flags, install the Reflag CLI:
396
+
397
+ ```
398
+ npm i --save-dev @reflag/cli
399
+ ```
400
+
401
+ then generate the types:
402
+
403
+ ```
404
+ npx reflag features types
405
+ ```
406
+
407
+ This will generate a `reflag.d.ts` containing all your features.
408
+ Any feature look ups will now be checked against the features that exist in Reflag.
409
+
410
+ Here's an example of a failed type check:
411
+
412
+ ```typescript
413
+ import { ReflagClient } from "@reflag/node-sdk";
414
+
415
+ export const reflagClient = new ReflagClient();
416
+
417
+ reflagClient.initialize().then(() => {
418
+ console.log("Reflag initialized!");
419
+
420
+ // TypeScript will catch this error: "invalid-feature" doesn't exist
421
+ reflagClient.getFlag("invalid-feature");
422
+
423
+ const {
424
+ isEnabled,
425
+ config: { payload },
426
+ } = reflagClient.getFlag("create-todos");
427
+ });
428
+ ```
429
+
430
+ ![Type check failed](docs/type-check-failed.png "Type check failed")
431
+
432
+ This is an example of a failed config payload check:
433
+
434
+ ```typescript
435
+ reflagClient.initialize().then(() => {
436
+ // TypeScript will catch this error as well: "minLength" is not part of the payload.
437
+ if (isEnabled && todo.length > config.payload.minLength) {
438
+ // ...
439
+ }
440
+ });
441
+ ```
442
+
443
+ ![Config type check failed](docs/type-check-payload-failed.png "Remote config type check failed")
444
+
445
+ ## Testing
446
+
447
+ When writing tests that cover code with flags, you can toggle features on/off programmatically to test the different behavior.
448
+
449
+ `reflag.ts`:
450
+
451
+ ```typescript
452
+ import { ReflagClient } from "@reflag/node-sdk";
453
+
454
+ export const reflag = new ReflagClient();
455
+ ```
456
+
457
+ `app.test.ts`:
458
+
459
+ ```typescript
460
+ import { reflag } from "./reflag.ts";
461
+
462
+ beforeAll(async () => await reflag.initialize());
463
+ afterEach(() => {
464
+ reflag.clearFlagOverrides();
465
+ });
466
+
467
+ describe("API Tests", () => {
468
+ it("should return 200 for the root endpoint", async () => {
469
+ reflag.featureOverrides = {
470
+ "show-todo": true,
471
+ };
472
+
473
+ const response = await request(app).get("/");
474
+ expect(response.status).toBe(200);
475
+ expect(response.body).toEqual({ message: "Ready to manage some TODOs!" });
476
+ });
477
+ });
478
+ ```
479
+
480
+ See more on feature overrides in the section below.
481
+
482
+ ## Flag Overrides
483
+
484
+ Flag overrides allow you to override flags and their configurations locally. This is particularly useful for development and testing. You can specify overrides in three ways:
485
+
486
+ 1. Through environment variables:
487
+
488
+ ```bash
489
+ REFLAG_FLAGS_ENABLED=feature1,feature2
490
+ REFLAG_FLAGS_DISABLED=feature3,feature4
491
+ ```
492
+
493
+ 2. Through `reflag.config.json`:
494
+
495
+ ```json
496
+ {
497
+ "flagOverrides": {
498
+ "delete-todos": {
499
+ "isEnabled": true,
500
+ "config": {
501
+ "key": "dev-config",
502
+ "payload": {
503
+ "requireConfirmation": true,
504
+ "maxDeletionsPerDay": 5
505
+ }
506
+ }
507
+ }
508
+ }
509
+ }
510
+ ```
511
+
512
+ 3. Programmatically through the client options:
513
+
514
+ You can use a simple `Record<string, boolean>` and pass it either in the constructor or by setting `client.featureOverrides`:
515
+
516
+ ```typescript
517
+ // pass directly in the constructor
518
+ const client = new ReflagClient({ featureOverrides: { myFlag: true } });
519
+ // or set on the client at a later time
520
+ client.featureOverrides = { myFlag: false };
521
+
522
+ // clear feature overrides. Same as setting to {}.
523
+ client.clearFlagOverrides();
524
+ ```
525
+
526
+ To get dynamic overrides, use a function which takes a context and returns a boolean or an object with the shape of `{isEnabled, config}`:
527
+
528
+ ```typescript
529
+ import { ReflagClient, Context } from "@reflag/node-sdk";
530
+
531
+ const featureOverrides = (context: Context) => ({
532
+ "delete-todos": {
533
+ isEnabled: true,
534
+ config: {
535
+ key: "dev-config",
536
+ payload: {
537
+ requireConfirmation: true,
538
+ maxDeletionsPerDay: 5,
539
+ },
540
+ },
541
+ },
542
+ });
543
+
544
+ const client = new ReflagClient({
545
+ featureOverrides,
546
+ });
547
+ ```
548
+
549
+ ## Remote Flag Evaluation
550
+
551
+ In addition to local feature evaluation, Reflag supports remote evaluation using stored context. This is useful when you want to evaluate features using user/company attributes that were previously sent to Reflag:
552
+
553
+ ```typescript
554
+ // First, update user and company attributes
555
+ await client.updateUser("user123", {
556
+ attributes: {
557
+ role: "admin",
558
+ subscription: "premium",
559
+ },
560
+ });
561
+
562
+ await client.updateCompany("company456", {
563
+ attributes: {
564
+ tier: "enterprise",
565
+ employees: 1000,
566
+ },
567
+ });
568
+
569
+ // Later, evaluate features remotely using stored context
570
+ const features = await client.getFlagsRemote("company456", "user123");
571
+ // Or evaluate a single feature
572
+ const feature = await client.getFlagRemote(
573
+ "create-todos",
574
+ "company456",
575
+ "user123",
576
+ );
577
+
578
+ // You can also provide additional context
579
+ const featuresWithContext = await client.getFlagsRemote(
580
+ "company456",
581
+ "user123",
582
+ {
583
+ other: {
584
+ location: "US",
585
+ platform: "mobile",
586
+ },
587
+ },
588
+ );
589
+ ```
590
+
591
+ Remote evaluation is particularly useful when:
592
+
593
+ - You want to use the most up-to-date user/company attributes stored in Reflag
594
+ - You don't want to pass all context attributes with every evaluation
595
+ - You need to ensure consistent feature evaluation across different services
596
+
597
+ ## Using with Express
598
+
599
+ A popular way to integrate the Reflag Node.js SDK is through an express middleware.
600
+
601
+ ```typescript
602
+ import reflag from "./reflag";
603
+ import express from "express";
604
+ import { BoundReflagClient } from "@reflag/node-sdk";
605
+
606
+ // Augment the Express types to include a `boundReflagClient` property on the
607
+ // `res.locals` object.
608
+ // This will allow us to access the ReflagClient instance in our route handlers
609
+ // without having to pass it around manually
610
+ declare global {
611
+ namespace Express {
612
+ interface Locals {
613
+ boundReflagClient: BoundReflagClient;
614
+ }
615
+ }
616
+ }
617
+
618
+ // Add express middleware
619
+ app.use((req, res, next) => {
620
+ // Extract the user and company IDs from the request
621
+ // You'll want to use a proper authentication and identification
622
+ // mechanism in a real-world application
623
+ const user = {
624
+ id: req.user?.id,
625
+ name: req.user?.name
626
+ email: req.user?.email
627
+ }
628
+
629
+ const company = {
630
+ id: req.user?.companyId
631
+ name: req.user?.companyName
632
+ }
633
+
634
+ // Create a new BoundReflagClient instance by calling the `bindClient`
635
+ // method on a `ReflagClient` instance
636
+ // This will create a new instance that is bound to the user/company given.
637
+ const boundReflagClient = reflag.bindClient({ user, company });
638
+
639
+ // Store the BoundReflagClient instance in the `res.locals` object so we
640
+ // can access it in our route handlers
641
+ res.locals.boundReflagClient = boundReflagClient;
642
+ next();
643
+ });
644
+
645
+ // Now use res.locals.boundReflagClient in your handlers
646
+ app.get("/todos", async (_req, res) => {
647
+ const { track, isEnabled } = res.locals.reflagUser.getFlag("show-todos");
648
+
649
+ if (!isEnabled) {
650
+ res.status(403).send({"error": "flag inaccessible"})
651
+ return
652
+ }
653
+
654
+ ...
655
+ }
656
+ ```
657
+
658
+ See [examples/express/app.ts](https://github.com/reflagcom/javascript/tree/main/packages/node-sdk/example/express/app.ts) for a full example.
659
+
660
+ ## Remote flag evaluation with stored context
661
+
662
+ If you don't want to provide context each time when evaluating flags but
663
+ rather you would like to utilize the attributes you sent to Reflag previously
664
+ (by calling `updateCompany` and `updateUser`) you can do so by calling `getFlagsRemote`
665
+ (or `getFlagRemote` for a specific feature) with providing just `userId` and `companyId`.
666
+ These methods will call Reflag's servers and flags will be evaluated remotely
667
+ using the stored attributes.
668
+
669
+ ```typescript
670
+ // Update user and company attributes
671
+ client.updateUser("john_doe", {
672
+ attributes: {
673
+ name: "John O.",
674
+ role: "admin",
675
+ },
676
+ });
677
+
678
+ client.updateCompany("acme_inc", {
679
+ attributes: {
680
+ name: "Acme, Inc",
681
+ tier: "premium"
682
+ },
683
+ });
684
+ ...
685
+
686
+ // This will evaluate flags with respecting the attributes sent previously
687
+ const features = await client.getFlagsRemote("acme_inc", "john_doe");
688
+ ```
689
+
690
+ > [!IMPORTANT]
691
+ > User and company attribute updates are processed asynchronously, so there might
692
+ > be a small delay between when attributes are updated and when they are available
693
+ > for evaluation.
694
+
695
+ ## Opting out of tracking
696
+
697
+ There are use cases in which you not want to be sending `user`, `company` and
698
+ `track` events to [Reflag.com](https://reflag.com). These are usually cases where you could be impersonating
699
+ another user in the system and do not want to interfere with the data being
700
+ collected by Reflag.
701
+
702
+ To disable tracking, bind the client using `bindClient()` as follows:
703
+
704
+ ```typescript
705
+ // binds the client to a given user and company and set `enableTracking` to `false`.
706
+ const boundClient = client.bindClient({ user, company, enableTracking: false });
707
+
708
+ boundClient.track("some event"); // this will not actually send the event to Reflag.
709
+
710
+ // the following code will not update the `user` nor `company` in Reflag and will
711
+ // not send `track` events either.
712
+ const { isEnabled, track } = boundClient.getFlag("user-menu");
713
+ if (isEnabled) {
714
+ track();
715
+ }
716
+ ```
717
+
718
+ Another way way to disable tracking without employing a bound client is to call `getFlag()`
719
+ or `getFlags()` by supplying `enableTracking: false` in the arguments passed to
720
+ these functions.
721
+
722
+ > [!IMPORTANT]
723
+ > Note, however, that calling `track()`, `updateCompany()` or `updateUser()` in the `ReflagClient`
724
+ > will still send tracking data. As such, it is always recommended to use `bindClient()`
725
+ > when using this SDK.
726
+
727
+ ## Flushing
728
+
729
+ ReflagClient employs a batching technique to minimize the number of calls that are sent to
730
+ Reflag's servers.
731
+
732
+ By default, the SDK automatically subscribes to process exit signals and attempts to flush
733
+ any pending events. This behavior is controlled by the `flushOnExit` option in the client configuration:
734
+
735
+ ```typescript
736
+ const client = new ReflagClient({
737
+ batchOptions: {
738
+ flushOnExit: false, // disable automatic flushing on exit
739
+ },
740
+ });
741
+ ```
742
+
743
+ ## Tracking custom events and setting custom attributes
744
+
745
+ Tracking allows events and updating user/company attributes in Reflag.
746
+ For example, if a customer changes their plan, you'll want Reflag to know about it,
747
+ in order to continue to provide up-do-date targeting information in the Reflag interface.
748
+
749
+ The following example shows how to register a new user, associate it with a company
750
+ and finally update the plan they are on.
751
+
752
+ ```typescript
753
+ // registers the user with Reflag using the provided unique ID, and
754
+ // providing a set of custom attributes (can be anything)
755
+ client.updateUser("user_id", {
756
+ attributes: { longTimeUser: true, payingCustomer: false },
757
+ });
758
+ client.updateCompany("company_id", { userId: "user_id" });
759
+
760
+ // the user started a voice huddle
761
+ client.track("user_id", "huddle", { attributes: { voice: true } });
762
+ ```
763
+
764
+ It's also possible to achieve the same through a bound client in the following manner:
765
+
766
+ ```typescript
767
+ const boundClient = client.bindClient({
768
+ user: { id: "user_id", longTimeUser: true, payingCustomer: false },
769
+ company: { id: "company_id" },
770
+ });
771
+
772
+ boundClient.track("huddle", { attributes: { voice: true } });
773
+ ```
774
+
775
+ Some attributes are used by Reflag to improve the UI, and are recommended
776
+ to provide for easier navigation:
777
+
778
+ - `name` -- display name for `user`/`company`,
779
+ - `email` -- the email of the user,
780
+ - `avatar` -- the URL for `user`/`company` avatar image.
781
+
782
+ Attributes cannot be nested (multiple levels) and must be either strings,
783
+ integers or booleans.
784
+
785
+ ## Managing `Last seen`
786
+
787
+ By default `updateUser`/`updateCompany` calls automatically update the given
788
+ user/company `Last seen` property on Reflag servers.
789
+
790
+ You can control if `Last seen` should be updated when the events are sent by setting
791
+ `meta.active = false`. This is often useful if you
792
+ have a background job that goes through a set of companies just to update their
793
+ attributes but not their activity.
794
+
795
+ Example:
796
+
797
+ ```typescript
798
+ client.updateUser("john_doe", {
799
+ attributes: { name: "John O." },
800
+ meta: { active: true },
801
+ });
802
+
803
+ client.updateCompany("acme_inc", {
804
+ attributes: { name: "Acme, Inc" },
805
+ meta: { active: false },
806
+ });
807
+ ```
808
+
809
+ `bindClient()` updates attributes on the Reflag servers but does not automatically
810
+ update `Last seen`.
811
+
812
+ ## Zero PII
813
+
814
+ The Reflag SDK doesn't collect any metadata and HTTP IP addresses are _not_ being
815
+ stored. For tracking individual users, we recommend using something like database
816
+ ID as userId, as it's unique and doesn't include any PII (personal identifiable
817
+ information). If, however, you're using e.g. email address as userId, but prefer
818
+ not to send any PII to Reflag, you can hash the sensitive data before sending
819
+ it to Reflag:
820
+
821
+ ```typescript
822
+ import { sha256 } from 'crypto-hash';
823
+
824
+ client.updateUser({ userId: await sha256("john_doe"), ... });
825
+ ```
826
+
827
+ ## Typescript
828
+
829
+ Types are bundled together with the library and exposed automatically when importing
830
+ through a package manager.
831
+
832
+ ## License
833
+
834
+ > MIT License
835
+ > Copyright (c) 2025 Bucket ApS