@rolloutctrl/js-sdk 0.0.3 → 0.0.5

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/dist/index.d.mts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { FeatureFlagEnvironment, Action, EvaluationContext, EvaluationResult } from '@rolloutctrl/evaluator';
2
2
 
3
3
  interface FlagConfig extends FeatureFlagEnvironment {
4
+ id: string;
4
5
  key: string;
5
6
  }
6
7
  interface Configuration {
@@ -22,12 +23,21 @@ interface StorageAdapter {
22
23
  set(configuration: Configuration): Promise<void>;
23
24
  clear(): Promise<void>;
24
25
  }
26
+ type MetricType = 'FLAG_EVALUATION' | 'FLAG_ENABLED' | 'FLAG_DISABLED' | 'STRATEGY_MATCH' | 'VARIANT_EXPOSURE';
27
+ interface MetricEvent {
28
+ featureFlagEnvironmentId: string;
29
+ featureFlagId: string;
30
+ type: MetricType;
31
+ strategyId?: string;
32
+ variantId?: string;
33
+ }
25
34
  interface RolloutCtrlOptions {
26
35
  sdkKey: string;
27
36
  environment: string;
28
37
  apiUrl?: string;
29
38
  refreshInterval?: number;
30
39
  requestTimeout?: number;
40
+ enableMetrics?: boolean;
31
41
  bootstrap?: Configuration;
32
42
  storage?: StorageAdapter;
33
43
  }
@@ -43,6 +53,7 @@ declare class RolloutCtrlClient {
43
53
  private readonly repository;
44
54
  private readonly evaluatorManager;
45
55
  private readonly updateProvider;
56
+ private readonly metricsQueue;
46
57
  private readonly subscribers;
47
58
  private isInitialized;
48
59
  private readonly readyPromise;
@@ -61,6 +72,7 @@ declare class RolloutCtrlClient {
61
72
  isEnabled(flagKey: string, context?: EvaluationContext): boolean;
62
73
  evaluate(flagKey: string, context?: EvaluationContext): EvaluationResult;
63
74
  getVariant(flagKey: string, context?: EvaluationContext): VariantResult;
75
+ private trackEvaluation;
64
76
  can(actionKey: string, context?: EvaluationContext): boolean;
65
77
  }
66
78
 
@@ -130,4 +142,19 @@ declare class LocalStorageAdapter implements StorageAdapter {
130
142
  clear(): Promise<void>;
131
143
  }
132
144
 
133
- export { type Configuration, type ConfigurationRepository, type Evaluator, EvaluatorManager, type FlagConfig, HttpConfigurationRepository, LocalStorageAdapter, PollingUpdateProvider, RolloutCtrlClient, type RolloutCtrlOptions, SseUpdateProvider, type StorageAdapter, type UpdateProvider, type VariantResult, createEvaluator };
145
+ declare class MetricsQueue {
146
+ private readonly apiUrl;
147
+ private readonly sdkKey;
148
+ private readonly requestTimeout;
149
+ private queue;
150
+ private flushTimer;
151
+ constructor(apiUrl: string, sdkKey: string, requestTimeout: number);
152
+ start(): void;
153
+ stop(): void;
154
+ push(event: MetricEvent): void;
155
+ flush(): Promise<void>;
156
+ private flushWithRetry;
157
+ private sendBatch;
158
+ }
159
+
160
+ export { type Configuration, type ConfigurationRepository, type Evaluator, EvaluatorManager, type FlagConfig, HttpConfigurationRepository, LocalStorageAdapter, type MetricEvent, type MetricType, MetricsQueue, PollingUpdateProvider, RolloutCtrlClient, type RolloutCtrlOptions, SseUpdateProvider, type StorageAdapter, type UpdateProvider, type VariantResult, createEvaluator };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { FeatureFlagEnvironment, Action, EvaluationContext, EvaluationResult } from '@rolloutctrl/evaluator';
2
2
 
3
3
  interface FlagConfig extends FeatureFlagEnvironment {
4
+ id: string;
4
5
  key: string;
5
6
  }
6
7
  interface Configuration {
@@ -22,12 +23,21 @@ interface StorageAdapter {
22
23
  set(configuration: Configuration): Promise<void>;
23
24
  clear(): Promise<void>;
24
25
  }
26
+ type MetricType = 'FLAG_EVALUATION' | 'FLAG_ENABLED' | 'FLAG_DISABLED' | 'STRATEGY_MATCH' | 'VARIANT_EXPOSURE';
27
+ interface MetricEvent {
28
+ featureFlagEnvironmentId: string;
29
+ featureFlagId: string;
30
+ type: MetricType;
31
+ strategyId?: string;
32
+ variantId?: string;
33
+ }
25
34
  interface RolloutCtrlOptions {
26
35
  sdkKey: string;
27
36
  environment: string;
28
37
  apiUrl?: string;
29
38
  refreshInterval?: number;
30
39
  requestTimeout?: number;
40
+ enableMetrics?: boolean;
31
41
  bootstrap?: Configuration;
32
42
  storage?: StorageAdapter;
33
43
  }
@@ -43,6 +53,7 @@ declare class RolloutCtrlClient {
43
53
  private readonly repository;
44
54
  private readonly evaluatorManager;
45
55
  private readonly updateProvider;
56
+ private readonly metricsQueue;
46
57
  private readonly subscribers;
47
58
  private isInitialized;
48
59
  private readonly readyPromise;
@@ -61,6 +72,7 @@ declare class RolloutCtrlClient {
61
72
  isEnabled(flagKey: string, context?: EvaluationContext): boolean;
62
73
  evaluate(flagKey: string, context?: EvaluationContext): EvaluationResult;
63
74
  getVariant(flagKey: string, context?: EvaluationContext): VariantResult;
75
+ private trackEvaluation;
64
76
  can(actionKey: string, context?: EvaluationContext): boolean;
65
77
  }
66
78
 
@@ -130,4 +142,19 @@ declare class LocalStorageAdapter implements StorageAdapter {
130
142
  clear(): Promise<void>;
131
143
  }
132
144
 
133
- export { type Configuration, type ConfigurationRepository, type Evaluator, EvaluatorManager, type FlagConfig, HttpConfigurationRepository, LocalStorageAdapter, PollingUpdateProvider, RolloutCtrlClient, type RolloutCtrlOptions, SseUpdateProvider, type StorageAdapter, type UpdateProvider, type VariantResult, createEvaluator };
145
+ declare class MetricsQueue {
146
+ private readonly apiUrl;
147
+ private readonly sdkKey;
148
+ private readonly requestTimeout;
149
+ private queue;
150
+ private flushTimer;
151
+ constructor(apiUrl: string, sdkKey: string, requestTimeout: number);
152
+ start(): void;
153
+ stop(): void;
154
+ push(event: MetricEvent): void;
155
+ flush(): Promise<void>;
156
+ private flushWithRetry;
157
+ private sendBatch;
158
+ }
159
+
160
+ export { type Configuration, type ConfigurationRepository, type Evaluator, EvaluatorManager, type FlagConfig, HttpConfigurationRepository, LocalStorageAdapter, type MetricEvent, type MetricType, MetricsQueue, PollingUpdateProvider, RolloutCtrlClient, type RolloutCtrlOptions, SseUpdateProvider, type StorageAdapter, type UpdateProvider, type VariantResult, createEvaluator };
package/dist/index.js CHANGED
@@ -23,6 +23,7 @@ __export(index_exports, {
23
23
  EvaluatorManager: () => EvaluatorManager,
24
24
  HttpConfigurationRepository: () => HttpConfigurationRepository,
25
25
  LocalStorageAdapter: () => LocalStorageAdapter,
26
+ MetricsQueue: () => MetricsQueue,
26
27
  PollingUpdateProvider: () => PollingUpdateProvider,
27
28
  RolloutCtrlClient: () => RolloutCtrlClient,
28
29
  SseUpdateProvider: () => SseUpdateProvider,
@@ -182,7 +183,7 @@ var HttpConfigurationRepository = class {
182
183
  };
183
184
  }
184
185
  async fetchChanges() {
185
- const url = `${this.apiUrl}/config/changes?environment=${encodeURIComponent(this.environment)}&since=${this.version}`;
186
+ const url = `${this.apiUrl}/sdk/config/changes?environment=${encodeURIComponent(this.environment)}&since=${this.version}`;
186
187
  return this.request(url);
187
188
  }
188
189
  async request(url) {
@@ -251,6 +252,74 @@ var SseUpdateProvider = class {
251
252
  }
252
253
  };
253
254
 
255
+ // src/metrics.ts
256
+ var FLUSH_INTERVAL_MS = 3e4;
257
+ var FLUSH_BATCH_SIZE = 100;
258
+ var MAX_RETRIES = 3;
259
+ var MetricsQueue = class {
260
+ constructor(apiUrl, sdkKey, requestTimeout) {
261
+ this.apiUrl = apiUrl;
262
+ this.sdkKey = sdkKey;
263
+ this.requestTimeout = requestTimeout;
264
+ this.queue = [];
265
+ this.flushTimer = null;
266
+ }
267
+ start() {
268
+ this.flushTimer = setInterval(() => {
269
+ this.flush().catch(() => {
270
+ });
271
+ }, FLUSH_INTERVAL_MS);
272
+ }
273
+ stop() {
274
+ if (this.flushTimer !== null) {
275
+ clearInterval(this.flushTimer);
276
+ this.flushTimer = null;
277
+ }
278
+ }
279
+ push(event) {
280
+ this.queue.push(event);
281
+ if (this.queue.length >= FLUSH_BATCH_SIZE) {
282
+ this.flush().catch(() => {
283
+ });
284
+ }
285
+ }
286
+ async flush() {
287
+ await this.flushWithRetry(0);
288
+ }
289
+ async flushWithRetry(attempt) {
290
+ if (this.queue.length === 0) return;
291
+ const batch = this.queue.splice(0, this.queue.length);
292
+ try {
293
+ await this.sendBatch(batch);
294
+ } catch {
295
+ if (attempt < MAX_RETRIES) {
296
+ this.queue.unshift(...batch);
297
+ await this.flushWithRetry(attempt + 1);
298
+ }
299
+ }
300
+ }
301
+ async sendBatch(events) {
302
+ const controller = new AbortController();
303
+ const timeout = setTimeout(() => controller.abort(), this.requestTimeout);
304
+ try {
305
+ const response = await fetch(`${this.apiUrl}/sdk/metrics`, {
306
+ method: "POST",
307
+ headers: {
308
+ "x-api-key": this.sdkKey,
309
+ "Content-Type": "application/json"
310
+ },
311
+ body: JSON.stringify({ events }),
312
+ signal: controller.signal
313
+ });
314
+ if (!response.ok) {
315
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
316
+ }
317
+ } finally {
318
+ clearTimeout(timeout);
319
+ }
320
+ }
321
+ };
322
+
254
323
  // src/client.ts
255
324
  var DEFAULT_API_URL = "https://rolloutctrl.io/api";
256
325
  var DEFAULT_REFRESH_INTERVAL = 3e4;
@@ -266,6 +335,7 @@ var RolloutCtrlClient = class {
266
335
  this.repository = new HttpConfigurationRepository(apiUrl, options.sdkKey, options.environment, requestTimeout);
267
336
  this.evaluatorManager = new EvaluatorManager();
268
337
  this.updateProvider = new PollingUpdateProvider(refreshInterval);
338
+ this.metricsQueue = options.enableMetrics ?? true ? new MetricsQueue(apiUrl, options.sdkKey, requestTimeout) : null;
269
339
  this.repository.subscribe((configuration) => {
270
340
  this.evaluatorManager.update(configuration);
271
341
  options.storage?.set(configuration).catch(() => {
@@ -302,6 +372,7 @@ var RolloutCtrlClient = class {
302
372
  this.updateProvider.start(() => {
303
373
  void this.repository.refresh();
304
374
  });
375
+ this.metricsQueue?.start();
305
376
  this.emitter.emit("ready");
306
377
  this.resolveReady();
307
378
  } catch (error) {
@@ -323,6 +394,10 @@ var RolloutCtrlClient = class {
323
394
  }
324
395
  async close() {
325
396
  this.updateProvider.stop();
397
+ if (this.metricsQueue) {
398
+ this.metricsQueue.stop();
399
+ await this.metricsQueue.flush();
400
+ }
326
401
  this.subscribers.clear();
327
402
  this.emitter.emit("shutdown");
328
403
  this.emitter.removeAllListeners();
@@ -343,13 +418,41 @@ var RolloutCtrlClient = class {
343
418
  return this.evaluatorManager.get();
344
419
  }
345
420
  isEnabled(flagKey, context) {
346
- return this.getEvaluator().isEnabled(flagKey, context);
421
+ return this.evaluate(flagKey, context).enabled;
347
422
  }
348
423
  evaluate(flagKey, context) {
349
- return this.getEvaluator().evaluate(flagKey, context);
424
+ const result = this.getEvaluator().evaluate(flagKey, context);
425
+ this.trackEvaluation(flagKey, result);
426
+ return result;
350
427
  }
351
428
  getVariant(flagKey, context) {
352
- return this.getEvaluator().getVariant(flagKey, context);
429
+ return this.evaluate(flagKey, context).variant;
430
+ }
431
+ trackEvaluation(flagKey, result) {
432
+ if (!this.metricsQueue || !this.isInitialized) return;
433
+ try {
434
+ const config = this.repository.getConfiguration();
435
+ const flag = config.flags.find((f) => f.key === flagKey);
436
+ if (!flag) return;
437
+ let type;
438
+ if (result.variant) {
439
+ type = "VARIANT_EXPOSURE";
440
+ } else if (result.strategy) {
441
+ type = "STRATEGY_MATCH";
442
+ } else if (result.enabled) {
443
+ type = "FLAG_ENABLED";
444
+ } else {
445
+ type = "FLAG_DISABLED";
446
+ }
447
+ this.metricsQueue.push({
448
+ featureFlagId: flag.id,
449
+ featureFlagEnvironmentId: config.environmentId,
450
+ type,
451
+ strategyId: result.strategy?.id,
452
+ variantId: result.variant?.id
453
+ });
454
+ } catch {
455
+ }
353
456
  }
354
457
  can(actionKey, context) {
355
458
  return this.getEvaluator().can(actionKey, context);
@@ -389,6 +492,7 @@ var LocalStorageAdapter = class {
389
492
  EvaluatorManager,
390
493
  HttpConfigurationRepository,
391
494
  LocalStorageAdapter,
495
+ MetricsQueue,
392
496
  PollingUpdateProvider,
393
497
  RolloutCtrlClient,
394
498
  SseUpdateProvider,
package/dist/index.mjs CHANGED
@@ -154,7 +154,7 @@ var HttpConfigurationRepository = class {
154
154
  };
155
155
  }
156
156
  async fetchChanges() {
157
- const url = `${this.apiUrl}/config/changes?environment=${encodeURIComponent(this.environment)}&since=${this.version}`;
157
+ const url = `${this.apiUrl}/sdk/config/changes?environment=${encodeURIComponent(this.environment)}&since=${this.version}`;
158
158
  return this.request(url);
159
159
  }
160
160
  async request(url) {
@@ -223,6 +223,74 @@ var SseUpdateProvider = class {
223
223
  }
224
224
  };
225
225
 
226
+ // src/metrics.ts
227
+ var FLUSH_INTERVAL_MS = 3e4;
228
+ var FLUSH_BATCH_SIZE = 100;
229
+ var MAX_RETRIES = 3;
230
+ var MetricsQueue = class {
231
+ constructor(apiUrl, sdkKey, requestTimeout) {
232
+ this.apiUrl = apiUrl;
233
+ this.sdkKey = sdkKey;
234
+ this.requestTimeout = requestTimeout;
235
+ this.queue = [];
236
+ this.flushTimer = null;
237
+ }
238
+ start() {
239
+ this.flushTimer = setInterval(() => {
240
+ this.flush().catch(() => {
241
+ });
242
+ }, FLUSH_INTERVAL_MS);
243
+ }
244
+ stop() {
245
+ if (this.flushTimer !== null) {
246
+ clearInterval(this.flushTimer);
247
+ this.flushTimer = null;
248
+ }
249
+ }
250
+ push(event) {
251
+ this.queue.push(event);
252
+ if (this.queue.length >= FLUSH_BATCH_SIZE) {
253
+ this.flush().catch(() => {
254
+ });
255
+ }
256
+ }
257
+ async flush() {
258
+ await this.flushWithRetry(0);
259
+ }
260
+ async flushWithRetry(attempt) {
261
+ if (this.queue.length === 0) return;
262
+ const batch = this.queue.splice(0, this.queue.length);
263
+ try {
264
+ await this.sendBatch(batch);
265
+ } catch {
266
+ if (attempt < MAX_RETRIES) {
267
+ this.queue.unshift(...batch);
268
+ await this.flushWithRetry(attempt + 1);
269
+ }
270
+ }
271
+ }
272
+ async sendBatch(events) {
273
+ const controller = new AbortController();
274
+ const timeout = setTimeout(() => controller.abort(), this.requestTimeout);
275
+ try {
276
+ const response = await fetch(`${this.apiUrl}/sdk/metrics`, {
277
+ method: "POST",
278
+ headers: {
279
+ "x-api-key": this.sdkKey,
280
+ "Content-Type": "application/json"
281
+ },
282
+ body: JSON.stringify({ events }),
283
+ signal: controller.signal
284
+ });
285
+ if (!response.ok) {
286
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
287
+ }
288
+ } finally {
289
+ clearTimeout(timeout);
290
+ }
291
+ }
292
+ };
293
+
226
294
  // src/client.ts
227
295
  var DEFAULT_API_URL = "https://rolloutctrl.io/api";
228
296
  var DEFAULT_REFRESH_INTERVAL = 3e4;
@@ -238,6 +306,7 @@ var RolloutCtrlClient = class {
238
306
  this.repository = new HttpConfigurationRepository(apiUrl, options.sdkKey, options.environment, requestTimeout);
239
307
  this.evaluatorManager = new EvaluatorManager();
240
308
  this.updateProvider = new PollingUpdateProvider(refreshInterval);
309
+ this.metricsQueue = options.enableMetrics ?? true ? new MetricsQueue(apiUrl, options.sdkKey, requestTimeout) : null;
241
310
  this.repository.subscribe((configuration) => {
242
311
  this.evaluatorManager.update(configuration);
243
312
  options.storage?.set(configuration).catch(() => {
@@ -274,6 +343,7 @@ var RolloutCtrlClient = class {
274
343
  this.updateProvider.start(() => {
275
344
  void this.repository.refresh();
276
345
  });
346
+ this.metricsQueue?.start();
277
347
  this.emitter.emit("ready");
278
348
  this.resolveReady();
279
349
  } catch (error) {
@@ -295,6 +365,10 @@ var RolloutCtrlClient = class {
295
365
  }
296
366
  async close() {
297
367
  this.updateProvider.stop();
368
+ if (this.metricsQueue) {
369
+ this.metricsQueue.stop();
370
+ await this.metricsQueue.flush();
371
+ }
298
372
  this.subscribers.clear();
299
373
  this.emitter.emit("shutdown");
300
374
  this.emitter.removeAllListeners();
@@ -315,13 +389,41 @@ var RolloutCtrlClient = class {
315
389
  return this.evaluatorManager.get();
316
390
  }
317
391
  isEnabled(flagKey, context) {
318
- return this.getEvaluator().isEnabled(flagKey, context);
392
+ return this.evaluate(flagKey, context).enabled;
319
393
  }
320
394
  evaluate(flagKey, context) {
321
- return this.getEvaluator().evaluate(flagKey, context);
395
+ const result = this.getEvaluator().evaluate(flagKey, context);
396
+ this.trackEvaluation(flagKey, result);
397
+ return result;
322
398
  }
323
399
  getVariant(flagKey, context) {
324
- return this.getEvaluator().getVariant(flagKey, context);
400
+ return this.evaluate(flagKey, context).variant;
401
+ }
402
+ trackEvaluation(flagKey, result) {
403
+ if (!this.metricsQueue || !this.isInitialized) return;
404
+ try {
405
+ const config = this.repository.getConfiguration();
406
+ const flag = config.flags.find((f) => f.key === flagKey);
407
+ if (!flag) return;
408
+ let type;
409
+ if (result.variant) {
410
+ type = "VARIANT_EXPOSURE";
411
+ } else if (result.strategy) {
412
+ type = "STRATEGY_MATCH";
413
+ } else if (result.enabled) {
414
+ type = "FLAG_ENABLED";
415
+ } else {
416
+ type = "FLAG_DISABLED";
417
+ }
418
+ this.metricsQueue.push({
419
+ featureFlagId: flag.id,
420
+ featureFlagEnvironmentId: config.environmentId,
421
+ type,
422
+ strategyId: result.strategy?.id,
423
+ variantId: result.variant?.id
424
+ });
425
+ } catch {
426
+ }
325
427
  }
326
428
  can(actionKey, context) {
327
429
  return this.getEvaluator().can(actionKey, context);
@@ -360,6 +462,7 @@ export {
360
462
  EvaluatorManager,
361
463
  HttpConfigurationRepository,
362
464
  LocalStorageAdapter,
465
+ MetricsQueue,
363
466
  PollingUpdateProvider,
364
467
  RolloutCtrlClient,
365
468
  SseUpdateProvider,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rolloutctrl/js-sdk",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "description": "RolloutCtrl JavaScript SDK for browser applications",
5
5
  "license": "MIT",
6
6
  "main": "./dist/index.js",