@rolloutctrl/js-sdk 0.0.8 → 0.0.9

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
@@ -32,6 +32,14 @@ interface MetricEvent {
32
32
  strategyId?: string;
33
33
  variantId?: string;
34
34
  }
35
+ interface AggregatedMetric {
36
+ type: MetricType;
37
+ featureFlagEnvironmentId: string;
38
+ featureFlagId?: string;
39
+ strategyId?: string;
40
+ variantId?: string;
41
+ count: number;
42
+ }
35
43
  interface RolloutCtrlOptions {
36
44
  sdkKey: string;
37
45
  environment: string;
@@ -54,10 +62,9 @@ declare class RolloutCtrlClient {
54
62
  private readonly repository;
55
63
  private readonly evaluatorManager;
56
64
  private readonly updateProvider;
57
- private readonly metricsQueue;
65
+ private readonly metricAggregator;
58
66
  private readonly subscribers;
59
67
  private isInitialized;
60
- private readonly trackedEvaluations;
61
68
  private readonly readyPromise;
62
69
  private resolveReady;
63
70
  private rejectReady;
@@ -144,19 +151,20 @@ declare class LocalStorageAdapter implements StorageAdapter {
144
151
  clear(): Promise<void>;
145
152
  }
146
153
 
147
- declare class MetricsQueue {
154
+ type TrackInput = Omit<AggregatedMetric, 'count'>;
155
+ declare class MetricAggregator {
148
156
  private readonly apiUrl;
149
157
  private readonly sdkKey;
150
158
  private readonly requestTimeout;
151
- private queue;
159
+ private readonly metrics;
152
160
  private flushTimer;
153
- constructor(apiUrl: string, sdkKey: string, requestTimeout: number);
161
+ private readonly maxMetricKeys;
162
+ constructor(apiUrl: string, sdkKey: string, requestTimeout: number, maxMetricKeys?: number);
154
163
  start(): void;
155
164
  stop(): void;
156
- push(event: MetricEvent): void;
165
+ track(metric: TrackInput): void;
157
166
  flush(): Promise<void>;
158
- private flushWithRetry;
159
- private sendBatch;
167
+ private send;
160
168
  }
161
169
 
162
- 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 };
170
+ export { type AggregatedMetric, type Configuration, type ConfigurationRepository, type Evaluator, EvaluatorManager, type FlagConfig, HttpConfigurationRepository, LocalStorageAdapter, MetricAggregator, type MetricEvent, type MetricType, PollingUpdateProvider, RolloutCtrlClient, type RolloutCtrlOptions, SseUpdateProvider, type StorageAdapter, type UpdateProvider, type VariantResult, createEvaluator };
package/dist/index.d.ts CHANGED
@@ -32,6 +32,14 @@ interface MetricEvent {
32
32
  strategyId?: string;
33
33
  variantId?: string;
34
34
  }
35
+ interface AggregatedMetric {
36
+ type: MetricType;
37
+ featureFlagEnvironmentId: string;
38
+ featureFlagId?: string;
39
+ strategyId?: string;
40
+ variantId?: string;
41
+ count: number;
42
+ }
35
43
  interface RolloutCtrlOptions {
36
44
  sdkKey: string;
37
45
  environment: string;
@@ -54,10 +62,9 @@ declare class RolloutCtrlClient {
54
62
  private readonly repository;
55
63
  private readonly evaluatorManager;
56
64
  private readonly updateProvider;
57
- private readonly metricsQueue;
65
+ private readonly metricAggregator;
58
66
  private readonly subscribers;
59
67
  private isInitialized;
60
- private readonly trackedEvaluations;
61
68
  private readonly readyPromise;
62
69
  private resolveReady;
63
70
  private rejectReady;
@@ -144,19 +151,20 @@ declare class LocalStorageAdapter implements StorageAdapter {
144
151
  clear(): Promise<void>;
145
152
  }
146
153
 
147
- declare class MetricsQueue {
154
+ type TrackInput = Omit<AggregatedMetric, 'count'>;
155
+ declare class MetricAggregator {
148
156
  private readonly apiUrl;
149
157
  private readonly sdkKey;
150
158
  private readonly requestTimeout;
151
- private queue;
159
+ private readonly metrics;
152
160
  private flushTimer;
153
- constructor(apiUrl: string, sdkKey: string, requestTimeout: number);
161
+ private readonly maxMetricKeys;
162
+ constructor(apiUrl: string, sdkKey: string, requestTimeout: number, maxMetricKeys?: number);
154
163
  start(): void;
155
164
  stop(): void;
156
- push(event: MetricEvent): void;
165
+ track(metric: TrackInput): void;
157
166
  flush(): Promise<void>;
158
- private flushWithRetry;
159
- private sendBatch;
167
+ private send;
160
168
  }
161
169
 
162
- 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 };
170
+ export { type AggregatedMetric, type Configuration, type ConfigurationRepository, type Evaluator, EvaluatorManager, type FlagConfig, HttpConfigurationRepository, LocalStorageAdapter, MetricAggregator, type MetricEvent, type MetricType, PollingUpdateProvider, RolloutCtrlClient, type RolloutCtrlOptions, SseUpdateProvider, type StorageAdapter, type UpdateProvider, type VariantResult, createEvaluator };
package/dist/index.js CHANGED
@@ -23,7 +23,7 @@ __export(index_exports, {
23
23
  EvaluatorManager: () => EvaluatorManager,
24
24
  HttpConfigurationRepository: () => HttpConfigurationRepository,
25
25
  LocalStorageAdapter: () => LocalStorageAdapter,
26
- MetricsQueue: () => MetricsQueue,
26
+ MetricAggregator: () => MetricAggregator,
27
27
  PollingUpdateProvider: () => PollingUpdateProvider,
28
28
  RolloutCtrlClient: () => RolloutCtrlClient,
29
29
  SseUpdateProvider: () => SseUpdateProvider,
@@ -259,15 +259,15 @@ var SseUpdateProvider = class {
259
259
 
260
260
  // src/metrics.ts
261
261
  var FLUSH_INTERVAL_MS = 3e4;
262
- var FLUSH_BATCH_SIZE = 100;
263
- var MAX_RETRIES = 3;
264
- var MetricsQueue = class {
265
- constructor(apiUrl, sdkKey, requestTimeout) {
262
+ var DEFAULT_MAX_METRIC_KEYS = 1e3;
263
+ var MetricAggregator = class {
264
+ constructor(apiUrl, sdkKey, requestTimeout, maxMetricKeys = DEFAULT_MAX_METRIC_KEYS) {
266
265
  this.apiUrl = apiUrl;
267
266
  this.sdkKey = sdkKey;
268
267
  this.requestTimeout = requestTimeout;
269
- this.queue = [];
268
+ this.metrics = /* @__PURE__ */ new Map();
270
269
  this.flushTimer = null;
270
+ this.maxMetricKeys = maxMetricKeys;
271
271
  }
272
272
  start() {
273
273
  this.flushTimer = setInterval(() => {
@@ -281,29 +281,38 @@ var MetricsQueue = class {
281
281
  this.flushTimer = null;
282
282
  }
283
283
  }
284
- push(event) {
285
- this.queue.push(event);
286
- if (this.queue.length >= FLUSH_BATCH_SIZE) {
284
+ track(metric) {
285
+ const key = `${metric.type}:${metric.featureFlagEnvironmentId}:${metric.featureFlagId ?? ""}:${metric.strategyId ?? ""}:${metric.variantId ?? ""}`;
286
+ const existing = this.metrics.get(key);
287
+ if (existing) {
288
+ existing.count++;
289
+ return;
290
+ }
291
+ if (this.metrics.size >= this.maxMetricKeys) return;
292
+ this.metrics.set(key, { ...metric, count: 1 });
293
+ if (this.metrics.size >= this.maxMetricKeys) {
287
294
  this.flush().catch(() => {
288
295
  });
289
296
  }
290
297
  }
291
298
  async flush() {
292
- await this.flushWithRetry(0);
293
- }
294
- async flushWithRetry(attempt) {
295
- if (this.queue.length === 0) return;
296
- const batch = this.queue.splice(0, this.queue.length);
299
+ if (this.metrics.size === 0) return;
300
+ const snapshot = new Map(this.metrics);
301
+ this.metrics.clear();
297
302
  try {
298
- await this.sendBatch(batch);
303
+ await this.send([...snapshot.values()]);
299
304
  } catch {
300
- if (attempt < MAX_RETRIES) {
301
- this.queue.unshift(...batch);
302
- await this.flushWithRetry(attempt + 1);
305
+ for (const [key, metric] of snapshot) {
306
+ const current = this.metrics.get(key);
307
+ if (current) {
308
+ current.count += metric.count;
309
+ } else {
310
+ this.metrics.set(key, { ...metric });
311
+ }
303
312
  }
304
313
  }
305
314
  }
306
- async sendBatch(events) {
315
+ async send(metrics) {
307
316
  const controller = new AbortController();
308
317
  const timeout = setTimeout(() => controller.abort(), this.requestTimeout);
309
318
  try {
@@ -313,7 +322,7 @@ var MetricsQueue = class {
313
322
  "x-api-key": this.sdkKey,
314
323
  "Content-Type": "application/json"
315
324
  },
316
- body: JSON.stringify({ events }),
325
+ body: JSON.stringify({ metrics }),
317
326
  signal: controller.signal
318
327
  });
319
328
  if (!response.ok) {
@@ -334,17 +343,15 @@ var RolloutCtrlClient = class {
334
343
  this.emitter = new TinyEmitter();
335
344
  this.subscribers = /* @__PURE__ */ new Set();
336
345
  this.isInitialized = false;
337
- this.trackedEvaluations = /* @__PURE__ */ new Map();
338
346
  const apiUrl = options.apiUrl ?? DEFAULT_API_URL;
339
347
  const refreshInterval = options.refreshInterval ?? DEFAULT_REFRESH_INTERVAL;
340
348
  const requestTimeout = options.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT;
341
349
  this.repository = new HttpConfigurationRepository(apiUrl, options.sdkKey, options.environment, requestTimeout);
342
350
  this.evaluatorManager = new EvaluatorManager();
343
351
  this.updateProvider = new PollingUpdateProvider(refreshInterval);
344
- this.metricsQueue = options.enableMetrics ?? true ? new MetricsQueue(apiUrl, options.sdkKey, requestTimeout) : null;
352
+ this.metricAggregator = options.enableMetrics ?? true ? new MetricAggregator(apiUrl, options.sdkKey, requestTimeout) : null;
345
353
  this.repository.subscribe((configuration) => {
346
354
  this.evaluatorManager.update(configuration);
347
- this.trackedEvaluations.clear();
348
355
  options.storage?.set(configuration).catch(() => {
349
356
  });
350
357
  if (this.isInitialized) {
@@ -379,7 +386,7 @@ var RolloutCtrlClient = class {
379
386
  this.updateProvider.start(() => {
380
387
  void this.repository.refresh();
381
388
  });
382
- this.metricsQueue?.start();
389
+ this.metricAggregator?.start();
383
390
  this.emitter.emit("ready");
384
391
  this.resolveReady();
385
392
  } catch (error) {
@@ -401,9 +408,9 @@ var RolloutCtrlClient = class {
401
408
  }
402
409
  async close() {
403
410
  this.updateProvider.stop();
404
- if (this.metricsQueue) {
405
- this.metricsQueue.stop();
406
- await this.metricsQueue.flush();
411
+ if (this.metricAggregator) {
412
+ this.metricAggregator.stop();
413
+ await this.metricAggregator.flush();
407
414
  }
408
415
  this.subscribers.clear();
409
416
  this.emitter.emit("shutdown");
@@ -436,29 +443,26 @@ var RolloutCtrlClient = class {
436
443
  return this.evaluate(flagKey, context).variant;
437
444
  }
438
445
  trackEvaluation(flagKey, result) {
439
- if (!this.metricsQueue || !this.isInitialized) return;
446
+ if (!this.metricAggregator || !this.isInitialized) return;
440
447
  try {
441
448
  const config = this.repository.getConfiguration();
442
449
  const flag = config.flags.find((f) => f.key === flagKey);
443
450
  if (!flag) return;
444
- const fingerprint = `${result.enabled ? 1 : 0}:${result.strategy?.id ?? ""}:${result.variant?.id ?? ""}`;
445
- if (this.trackedEvaluations.get(flagKey) === fingerprint) return;
446
- this.trackedEvaluations.set(flagKey, fingerprint);
447
451
  const base = {
448
452
  featureFlagId: flag.id,
449
453
  featureFlagEnvironmentId: flag.featureFlagEnvironmentId
450
454
  };
451
- this.metricsQueue.push({ ...base, type: "FLAG_EVALUATION" });
455
+ this.metricAggregator.track({ ...base, type: "FLAG_EVALUATION" });
452
456
  if (result.enabled) {
453
- this.metricsQueue.push({ ...base, type: "FLAG_ENABLED" });
457
+ this.metricAggregator.track({ ...base, type: "FLAG_ENABLED" });
454
458
  if (result.strategy?.id) {
455
- this.metricsQueue.push({ ...base, type: "STRATEGY_MATCH", strategyId: result.strategy.id });
459
+ this.metricAggregator.track({ ...base, type: "STRATEGY_MATCH", strategyId: result.strategy.id });
456
460
  }
457
461
  if (result.variant?.id) {
458
- this.metricsQueue.push({ ...base, type: "VARIANT_EXPOSURE", variantId: result.variant.id });
462
+ this.metricAggregator.track({ ...base, type: "VARIANT_EXPOSURE", variantId: result.variant.id });
459
463
  }
460
464
  } else {
461
- this.metricsQueue.push({ ...base, type: "FLAG_DISABLED" });
465
+ this.metricAggregator.track({ ...base, type: "FLAG_DISABLED" });
462
466
  }
463
467
  } catch {
464
468
  }
@@ -501,7 +505,7 @@ var LocalStorageAdapter = class {
501
505
  EvaluatorManager,
502
506
  HttpConfigurationRepository,
503
507
  LocalStorageAdapter,
504
- MetricsQueue,
508
+ MetricAggregator,
505
509
  PollingUpdateProvider,
506
510
  RolloutCtrlClient,
507
511
  SseUpdateProvider,
package/dist/index.mjs CHANGED
@@ -230,15 +230,15 @@ var SseUpdateProvider = class {
230
230
 
231
231
  // src/metrics.ts
232
232
  var FLUSH_INTERVAL_MS = 3e4;
233
- var FLUSH_BATCH_SIZE = 100;
234
- var MAX_RETRIES = 3;
235
- var MetricsQueue = class {
236
- constructor(apiUrl, sdkKey, requestTimeout) {
233
+ var DEFAULT_MAX_METRIC_KEYS = 1e3;
234
+ var MetricAggregator = class {
235
+ constructor(apiUrl, sdkKey, requestTimeout, maxMetricKeys = DEFAULT_MAX_METRIC_KEYS) {
237
236
  this.apiUrl = apiUrl;
238
237
  this.sdkKey = sdkKey;
239
238
  this.requestTimeout = requestTimeout;
240
- this.queue = [];
239
+ this.metrics = /* @__PURE__ */ new Map();
241
240
  this.flushTimer = null;
241
+ this.maxMetricKeys = maxMetricKeys;
242
242
  }
243
243
  start() {
244
244
  this.flushTimer = setInterval(() => {
@@ -252,29 +252,38 @@ var MetricsQueue = class {
252
252
  this.flushTimer = null;
253
253
  }
254
254
  }
255
- push(event) {
256
- this.queue.push(event);
257
- if (this.queue.length >= FLUSH_BATCH_SIZE) {
255
+ track(metric) {
256
+ const key = `${metric.type}:${metric.featureFlagEnvironmentId}:${metric.featureFlagId ?? ""}:${metric.strategyId ?? ""}:${metric.variantId ?? ""}`;
257
+ const existing = this.metrics.get(key);
258
+ if (existing) {
259
+ existing.count++;
260
+ return;
261
+ }
262
+ if (this.metrics.size >= this.maxMetricKeys) return;
263
+ this.metrics.set(key, { ...metric, count: 1 });
264
+ if (this.metrics.size >= this.maxMetricKeys) {
258
265
  this.flush().catch(() => {
259
266
  });
260
267
  }
261
268
  }
262
269
  async flush() {
263
- await this.flushWithRetry(0);
264
- }
265
- async flushWithRetry(attempt) {
266
- if (this.queue.length === 0) return;
267
- const batch = this.queue.splice(0, this.queue.length);
270
+ if (this.metrics.size === 0) return;
271
+ const snapshot = new Map(this.metrics);
272
+ this.metrics.clear();
268
273
  try {
269
- await this.sendBatch(batch);
274
+ await this.send([...snapshot.values()]);
270
275
  } catch {
271
- if (attempt < MAX_RETRIES) {
272
- this.queue.unshift(...batch);
273
- await this.flushWithRetry(attempt + 1);
276
+ for (const [key, metric] of snapshot) {
277
+ const current = this.metrics.get(key);
278
+ if (current) {
279
+ current.count += metric.count;
280
+ } else {
281
+ this.metrics.set(key, { ...metric });
282
+ }
274
283
  }
275
284
  }
276
285
  }
277
- async sendBatch(events) {
286
+ async send(metrics) {
278
287
  const controller = new AbortController();
279
288
  const timeout = setTimeout(() => controller.abort(), this.requestTimeout);
280
289
  try {
@@ -284,7 +293,7 @@ var MetricsQueue = class {
284
293
  "x-api-key": this.sdkKey,
285
294
  "Content-Type": "application/json"
286
295
  },
287
- body: JSON.stringify({ events }),
296
+ body: JSON.stringify({ metrics }),
288
297
  signal: controller.signal
289
298
  });
290
299
  if (!response.ok) {
@@ -305,17 +314,15 @@ var RolloutCtrlClient = class {
305
314
  this.emitter = new TinyEmitter();
306
315
  this.subscribers = /* @__PURE__ */ new Set();
307
316
  this.isInitialized = false;
308
- this.trackedEvaluations = /* @__PURE__ */ new Map();
309
317
  const apiUrl = options.apiUrl ?? DEFAULT_API_URL;
310
318
  const refreshInterval = options.refreshInterval ?? DEFAULT_REFRESH_INTERVAL;
311
319
  const requestTimeout = options.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT;
312
320
  this.repository = new HttpConfigurationRepository(apiUrl, options.sdkKey, options.environment, requestTimeout);
313
321
  this.evaluatorManager = new EvaluatorManager();
314
322
  this.updateProvider = new PollingUpdateProvider(refreshInterval);
315
- this.metricsQueue = options.enableMetrics ?? true ? new MetricsQueue(apiUrl, options.sdkKey, requestTimeout) : null;
323
+ this.metricAggregator = options.enableMetrics ?? true ? new MetricAggregator(apiUrl, options.sdkKey, requestTimeout) : null;
316
324
  this.repository.subscribe((configuration) => {
317
325
  this.evaluatorManager.update(configuration);
318
- this.trackedEvaluations.clear();
319
326
  options.storage?.set(configuration).catch(() => {
320
327
  });
321
328
  if (this.isInitialized) {
@@ -350,7 +357,7 @@ var RolloutCtrlClient = class {
350
357
  this.updateProvider.start(() => {
351
358
  void this.repository.refresh();
352
359
  });
353
- this.metricsQueue?.start();
360
+ this.metricAggregator?.start();
354
361
  this.emitter.emit("ready");
355
362
  this.resolveReady();
356
363
  } catch (error) {
@@ -372,9 +379,9 @@ var RolloutCtrlClient = class {
372
379
  }
373
380
  async close() {
374
381
  this.updateProvider.stop();
375
- if (this.metricsQueue) {
376
- this.metricsQueue.stop();
377
- await this.metricsQueue.flush();
382
+ if (this.metricAggregator) {
383
+ this.metricAggregator.stop();
384
+ await this.metricAggregator.flush();
378
385
  }
379
386
  this.subscribers.clear();
380
387
  this.emitter.emit("shutdown");
@@ -407,29 +414,26 @@ var RolloutCtrlClient = class {
407
414
  return this.evaluate(flagKey, context).variant;
408
415
  }
409
416
  trackEvaluation(flagKey, result) {
410
- if (!this.metricsQueue || !this.isInitialized) return;
417
+ if (!this.metricAggregator || !this.isInitialized) return;
411
418
  try {
412
419
  const config = this.repository.getConfiguration();
413
420
  const flag = config.flags.find((f) => f.key === flagKey);
414
421
  if (!flag) return;
415
- const fingerprint = `${result.enabled ? 1 : 0}:${result.strategy?.id ?? ""}:${result.variant?.id ?? ""}`;
416
- if (this.trackedEvaluations.get(flagKey) === fingerprint) return;
417
- this.trackedEvaluations.set(flagKey, fingerprint);
418
422
  const base = {
419
423
  featureFlagId: flag.id,
420
424
  featureFlagEnvironmentId: flag.featureFlagEnvironmentId
421
425
  };
422
- this.metricsQueue.push({ ...base, type: "FLAG_EVALUATION" });
426
+ this.metricAggregator.track({ ...base, type: "FLAG_EVALUATION" });
423
427
  if (result.enabled) {
424
- this.metricsQueue.push({ ...base, type: "FLAG_ENABLED" });
428
+ this.metricAggregator.track({ ...base, type: "FLAG_ENABLED" });
425
429
  if (result.strategy?.id) {
426
- this.metricsQueue.push({ ...base, type: "STRATEGY_MATCH", strategyId: result.strategy.id });
430
+ this.metricAggregator.track({ ...base, type: "STRATEGY_MATCH", strategyId: result.strategy.id });
427
431
  }
428
432
  if (result.variant?.id) {
429
- this.metricsQueue.push({ ...base, type: "VARIANT_EXPOSURE", variantId: result.variant.id });
433
+ this.metricAggregator.track({ ...base, type: "VARIANT_EXPOSURE", variantId: result.variant.id });
430
434
  }
431
435
  } else {
432
- this.metricsQueue.push({ ...base, type: "FLAG_DISABLED" });
436
+ this.metricAggregator.track({ ...base, type: "FLAG_DISABLED" });
433
437
  }
434
438
  } catch {
435
439
  }
@@ -471,7 +475,7 @@ export {
471
475
  EvaluatorManager,
472
476
  HttpConfigurationRepository,
473
477
  LocalStorageAdapter,
474
- MetricsQueue,
478
+ MetricAggregator,
475
479
  PollingUpdateProvider,
476
480
  RolloutCtrlClient,
477
481
  SseUpdateProvider,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rolloutctrl/js-sdk",
3
- "version": "0.0.8",
3
+ "version": "0.0.9",
4
4
  "description": "RolloutCtrl JavaScript SDK for browser applications",
5
5
  "license": "MIT",
6
6
  "main": "./dist/index.js",