@rolloutctrl/js-sdk 0.0.4 → 0.0.6
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 +30 -1
- package/dist/index.d.ts +30 -1
- package/dist/index.js +118 -4
- package/dist/index.mjs +117 -4
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
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;
|
|
6
|
+
featureFlagEnvironmentId: string;
|
|
5
7
|
}
|
|
6
8
|
interface Configuration {
|
|
7
9
|
projectId: string;
|
|
@@ -22,12 +24,21 @@ interface StorageAdapter {
|
|
|
22
24
|
set(configuration: Configuration): Promise<void>;
|
|
23
25
|
clear(): Promise<void>;
|
|
24
26
|
}
|
|
27
|
+
type MetricType = 'FLAG_EVALUATION' | 'FLAG_ENABLED' | 'FLAG_DISABLED' | 'STRATEGY_MATCH' | 'VARIANT_EXPOSURE';
|
|
28
|
+
interface MetricEvent {
|
|
29
|
+
featureFlagEnvironmentId: string;
|
|
30
|
+
featureFlagId: string;
|
|
31
|
+
type: MetricType;
|
|
32
|
+
strategyId?: string;
|
|
33
|
+
variantId?: string;
|
|
34
|
+
}
|
|
25
35
|
interface RolloutCtrlOptions {
|
|
26
36
|
sdkKey: string;
|
|
27
37
|
environment: string;
|
|
28
38
|
apiUrl?: string;
|
|
29
39
|
refreshInterval?: number;
|
|
30
40
|
requestTimeout?: number;
|
|
41
|
+
enableMetrics?: boolean;
|
|
31
42
|
bootstrap?: Configuration;
|
|
32
43
|
storage?: StorageAdapter;
|
|
33
44
|
}
|
|
@@ -43,8 +54,10 @@ declare class RolloutCtrlClient {
|
|
|
43
54
|
private readonly repository;
|
|
44
55
|
private readonly evaluatorManager;
|
|
45
56
|
private readonly updateProvider;
|
|
57
|
+
private readonly metricsQueue;
|
|
46
58
|
private readonly subscribers;
|
|
47
59
|
private isInitialized;
|
|
60
|
+
private readonly trackedEvaluations;
|
|
48
61
|
private readonly readyPromise;
|
|
49
62
|
private resolveReady;
|
|
50
63
|
private rejectReady;
|
|
@@ -61,6 +74,7 @@ declare class RolloutCtrlClient {
|
|
|
61
74
|
isEnabled(flagKey: string, context?: EvaluationContext): boolean;
|
|
62
75
|
evaluate(flagKey: string, context?: EvaluationContext): EvaluationResult;
|
|
63
76
|
getVariant(flagKey: string, context?: EvaluationContext): VariantResult;
|
|
77
|
+
private trackEvaluation;
|
|
64
78
|
can(actionKey: string, context?: EvaluationContext): boolean;
|
|
65
79
|
}
|
|
66
80
|
|
|
@@ -130,4 +144,19 @@ declare class LocalStorageAdapter implements StorageAdapter {
|
|
|
130
144
|
clear(): Promise<void>;
|
|
131
145
|
}
|
|
132
146
|
|
|
133
|
-
|
|
147
|
+
declare class MetricsQueue {
|
|
148
|
+
private readonly apiUrl;
|
|
149
|
+
private readonly sdkKey;
|
|
150
|
+
private readonly requestTimeout;
|
|
151
|
+
private queue;
|
|
152
|
+
private flushTimer;
|
|
153
|
+
constructor(apiUrl: string, sdkKey: string, requestTimeout: number);
|
|
154
|
+
start(): void;
|
|
155
|
+
stop(): void;
|
|
156
|
+
push(event: MetricEvent): void;
|
|
157
|
+
flush(): Promise<void>;
|
|
158
|
+
private flushWithRetry;
|
|
159
|
+
private sendBatch;
|
|
160
|
+
}
|
|
161
|
+
|
|
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 };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
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;
|
|
6
|
+
featureFlagEnvironmentId: string;
|
|
5
7
|
}
|
|
6
8
|
interface Configuration {
|
|
7
9
|
projectId: string;
|
|
@@ -22,12 +24,21 @@ interface StorageAdapter {
|
|
|
22
24
|
set(configuration: Configuration): Promise<void>;
|
|
23
25
|
clear(): Promise<void>;
|
|
24
26
|
}
|
|
27
|
+
type MetricType = 'FLAG_EVALUATION' | 'FLAG_ENABLED' | 'FLAG_DISABLED' | 'STRATEGY_MATCH' | 'VARIANT_EXPOSURE';
|
|
28
|
+
interface MetricEvent {
|
|
29
|
+
featureFlagEnvironmentId: string;
|
|
30
|
+
featureFlagId: string;
|
|
31
|
+
type: MetricType;
|
|
32
|
+
strategyId?: string;
|
|
33
|
+
variantId?: string;
|
|
34
|
+
}
|
|
25
35
|
interface RolloutCtrlOptions {
|
|
26
36
|
sdkKey: string;
|
|
27
37
|
environment: string;
|
|
28
38
|
apiUrl?: string;
|
|
29
39
|
refreshInterval?: number;
|
|
30
40
|
requestTimeout?: number;
|
|
41
|
+
enableMetrics?: boolean;
|
|
31
42
|
bootstrap?: Configuration;
|
|
32
43
|
storage?: StorageAdapter;
|
|
33
44
|
}
|
|
@@ -43,8 +54,10 @@ declare class RolloutCtrlClient {
|
|
|
43
54
|
private readonly repository;
|
|
44
55
|
private readonly evaluatorManager;
|
|
45
56
|
private readonly updateProvider;
|
|
57
|
+
private readonly metricsQueue;
|
|
46
58
|
private readonly subscribers;
|
|
47
59
|
private isInitialized;
|
|
60
|
+
private readonly trackedEvaluations;
|
|
48
61
|
private readonly readyPromise;
|
|
49
62
|
private resolveReady;
|
|
50
63
|
private rejectReady;
|
|
@@ -61,6 +74,7 @@ declare class RolloutCtrlClient {
|
|
|
61
74
|
isEnabled(flagKey: string, context?: EvaluationContext): boolean;
|
|
62
75
|
evaluate(flagKey: string, context?: EvaluationContext): EvaluationResult;
|
|
63
76
|
getVariant(flagKey: string, context?: EvaluationContext): VariantResult;
|
|
77
|
+
private trackEvaluation;
|
|
64
78
|
can(actionKey: string, context?: EvaluationContext): boolean;
|
|
65
79
|
}
|
|
66
80
|
|
|
@@ -130,4 +144,19 @@ declare class LocalStorageAdapter implements StorageAdapter {
|
|
|
130
144
|
clear(): Promise<void>;
|
|
131
145
|
}
|
|
132
146
|
|
|
133
|
-
|
|
147
|
+
declare class MetricsQueue {
|
|
148
|
+
private readonly apiUrl;
|
|
149
|
+
private readonly sdkKey;
|
|
150
|
+
private readonly requestTimeout;
|
|
151
|
+
private queue;
|
|
152
|
+
private flushTimer;
|
|
153
|
+
constructor(apiUrl: string, sdkKey: string, requestTimeout: number);
|
|
154
|
+
start(): void;
|
|
155
|
+
stop(): void;
|
|
156
|
+
push(event: MetricEvent): void;
|
|
157
|
+
flush(): Promise<void>;
|
|
158
|
+
private flushWithRetry;
|
|
159
|
+
private sendBatch;
|
|
160
|
+
}
|
|
161
|
+
|
|
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 };
|
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,
|
|
@@ -171,7 +172,12 @@ var HttpConfigurationRepository = class {
|
|
|
171
172
|
return this.normalizeConfiguration(raw);
|
|
172
173
|
}
|
|
173
174
|
normalizeConfiguration(raw) {
|
|
174
|
-
const flags = Array.isArray(raw.flags) ? raw.flags : Object.entries(raw.flags ?? {}).map(([key, value]) => ({
|
|
175
|
+
const flags = Array.isArray(raw.flags) ? raw.flags : Object.entries(raw.flags ?? {}).map(([key, value]) => ({
|
|
176
|
+
key,
|
|
177
|
+
...value,
|
|
178
|
+
id: value._featureFlagId ?? value.id ?? "",
|
|
179
|
+
featureFlagEnvironmentId: value._featureFlagEnvironmentId ?? ""
|
|
180
|
+
}));
|
|
175
181
|
const actions = Array.isArray(raw.actions) ? raw.actions : Object.values(raw.actions ?? {});
|
|
176
182
|
return {
|
|
177
183
|
projectId: raw.project?.id ?? raw.projectId ?? "",
|
|
@@ -251,6 +257,74 @@ var SseUpdateProvider = class {
|
|
|
251
257
|
}
|
|
252
258
|
};
|
|
253
259
|
|
|
260
|
+
// src/metrics.ts
|
|
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) {
|
|
266
|
+
this.apiUrl = apiUrl;
|
|
267
|
+
this.sdkKey = sdkKey;
|
|
268
|
+
this.requestTimeout = requestTimeout;
|
|
269
|
+
this.queue = [];
|
|
270
|
+
this.flushTimer = null;
|
|
271
|
+
}
|
|
272
|
+
start() {
|
|
273
|
+
this.flushTimer = setInterval(() => {
|
|
274
|
+
this.flush().catch(() => {
|
|
275
|
+
});
|
|
276
|
+
}, FLUSH_INTERVAL_MS);
|
|
277
|
+
}
|
|
278
|
+
stop() {
|
|
279
|
+
if (this.flushTimer !== null) {
|
|
280
|
+
clearInterval(this.flushTimer);
|
|
281
|
+
this.flushTimer = null;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
push(event) {
|
|
285
|
+
this.queue.push(event);
|
|
286
|
+
if (this.queue.length >= FLUSH_BATCH_SIZE) {
|
|
287
|
+
this.flush().catch(() => {
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
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);
|
|
297
|
+
try {
|
|
298
|
+
await this.sendBatch(batch);
|
|
299
|
+
} catch {
|
|
300
|
+
if (attempt < MAX_RETRIES) {
|
|
301
|
+
this.queue.unshift(...batch);
|
|
302
|
+
await this.flushWithRetry(attempt + 1);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
async sendBatch(events) {
|
|
307
|
+
const controller = new AbortController();
|
|
308
|
+
const timeout = setTimeout(() => controller.abort(), this.requestTimeout);
|
|
309
|
+
try {
|
|
310
|
+
const response = await fetch(`${this.apiUrl}/sdk/metrics`, {
|
|
311
|
+
method: "POST",
|
|
312
|
+
headers: {
|
|
313
|
+
"x-api-key": this.sdkKey,
|
|
314
|
+
"Content-Type": "application/json"
|
|
315
|
+
},
|
|
316
|
+
body: JSON.stringify({ events }),
|
|
317
|
+
signal: controller.signal
|
|
318
|
+
});
|
|
319
|
+
if (!response.ok) {
|
|
320
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
321
|
+
}
|
|
322
|
+
} finally {
|
|
323
|
+
clearTimeout(timeout);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
|
|
254
328
|
// src/client.ts
|
|
255
329
|
var DEFAULT_API_URL = "https://rolloutctrl.io/api";
|
|
256
330
|
var DEFAULT_REFRESH_INTERVAL = 3e4;
|
|
@@ -260,14 +334,17 @@ var RolloutCtrlClient = class {
|
|
|
260
334
|
this.emitter = new TinyEmitter();
|
|
261
335
|
this.subscribers = /* @__PURE__ */ new Set();
|
|
262
336
|
this.isInitialized = false;
|
|
337
|
+
this.trackedEvaluations = /* @__PURE__ */ new Map();
|
|
263
338
|
const apiUrl = options.apiUrl ?? DEFAULT_API_URL;
|
|
264
339
|
const refreshInterval = options.refreshInterval ?? DEFAULT_REFRESH_INTERVAL;
|
|
265
340
|
const requestTimeout = options.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT;
|
|
266
341
|
this.repository = new HttpConfigurationRepository(apiUrl, options.sdkKey, options.environment, requestTimeout);
|
|
267
342
|
this.evaluatorManager = new EvaluatorManager();
|
|
268
343
|
this.updateProvider = new PollingUpdateProvider(refreshInterval);
|
|
344
|
+
this.metricsQueue = options.enableMetrics ?? true ? new MetricsQueue(apiUrl, options.sdkKey, requestTimeout) : null;
|
|
269
345
|
this.repository.subscribe((configuration) => {
|
|
270
346
|
this.evaluatorManager.update(configuration);
|
|
347
|
+
this.trackedEvaluations.clear();
|
|
271
348
|
options.storage?.set(configuration).catch(() => {
|
|
272
349
|
});
|
|
273
350
|
if (this.isInitialized) {
|
|
@@ -302,6 +379,7 @@ var RolloutCtrlClient = class {
|
|
|
302
379
|
this.updateProvider.start(() => {
|
|
303
380
|
void this.repository.refresh();
|
|
304
381
|
});
|
|
382
|
+
this.metricsQueue?.start();
|
|
305
383
|
this.emitter.emit("ready");
|
|
306
384
|
this.resolveReady();
|
|
307
385
|
} catch (error) {
|
|
@@ -323,6 +401,10 @@ var RolloutCtrlClient = class {
|
|
|
323
401
|
}
|
|
324
402
|
async close() {
|
|
325
403
|
this.updateProvider.stop();
|
|
404
|
+
if (this.metricsQueue) {
|
|
405
|
+
this.metricsQueue.stop();
|
|
406
|
+
await this.metricsQueue.flush();
|
|
407
|
+
}
|
|
326
408
|
this.subscribers.clear();
|
|
327
409
|
this.emitter.emit("shutdown");
|
|
328
410
|
this.emitter.removeAllListeners();
|
|
@@ -343,13 +425,44 @@ var RolloutCtrlClient = class {
|
|
|
343
425
|
return this.evaluatorManager.get();
|
|
344
426
|
}
|
|
345
427
|
isEnabled(flagKey, context) {
|
|
346
|
-
return this.
|
|
428
|
+
return this.evaluate(flagKey, context).enabled;
|
|
347
429
|
}
|
|
348
430
|
evaluate(flagKey, context) {
|
|
349
|
-
|
|
431
|
+
const result = this.getEvaluator().evaluate(flagKey, context);
|
|
432
|
+
this.trackEvaluation(flagKey, result);
|
|
433
|
+
return result;
|
|
350
434
|
}
|
|
351
435
|
getVariant(flagKey, context) {
|
|
352
|
-
return this.
|
|
436
|
+
return this.evaluate(flagKey, context).variant;
|
|
437
|
+
}
|
|
438
|
+
trackEvaluation(flagKey, result) {
|
|
439
|
+
if (!this.metricsQueue || !this.isInitialized) return;
|
|
440
|
+
try {
|
|
441
|
+
const config = this.repository.getConfiguration();
|
|
442
|
+
const flag = config.flags.find((f) => f.key === flagKey);
|
|
443
|
+
if (!flag) return;
|
|
444
|
+
let type;
|
|
445
|
+
if (result.variant) {
|
|
446
|
+
type = "VARIANT_EXPOSURE";
|
|
447
|
+
} else if (result.strategy) {
|
|
448
|
+
type = "STRATEGY_MATCH";
|
|
449
|
+
} else if (result.enabled) {
|
|
450
|
+
type = "FLAG_ENABLED";
|
|
451
|
+
} else {
|
|
452
|
+
type = "FLAG_DISABLED";
|
|
453
|
+
}
|
|
454
|
+
const fingerprint = `${type}:${result.strategy?.id ?? ""}:${result.variant?.id ?? ""}`;
|
|
455
|
+
if (this.trackedEvaluations.get(flagKey) === fingerprint) return;
|
|
456
|
+
this.trackedEvaluations.set(flagKey, fingerprint);
|
|
457
|
+
this.metricsQueue.push({
|
|
458
|
+
featureFlagId: flag.id,
|
|
459
|
+
featureFlagEnvironmentId: flag.featureFlagEnvironmentId,
|
|
460
|
+
type,
|
|
461
|
+
strategyId: result.strategy?.id,
|
|
462
|
+
variantId: result.variant?.id
|
|
463
|
+
});
|
|
464
|
+
} catch {
|
|
465
|
+
}
|
|
353
466
|
}
|
|
354
467
|
can(actionKey, context) {
|
|
355
468
|
return this.getEvaluator().can(actionKey, context);
|
|
@@ -389,6 +502,7 @@ var LocalStorageAdapter = class {
|
|
|
389
502
|
EvaluatorManager,
|
|
390
503
|
HttpConfigurationRepository,
|
|
391
504
|
LocalStorageAdapter,
|
|
505
|
+
MetricsQueue,
|
|
392
506
|
PollingUpdateProvider,
|
|
393
507
|
RolloutCtrlClient,
|
|
394
508
|
SseUpdateProvider,
|
package/dist/index.mjs
CHANGED
|
@@ -143,7 +143,12 @@ var HttpConfigurationRepository = class {
|
|
|
143
143
|
return this.normalizeConfiguration(raw);
|
|
144
144
|
}
|
|
145
145
|
normalizeConfiguration(raw) {
|
|
146
|
-
const flags = Array.isArray(raw.flags) ? raw.flags : Object.entries(raw.flags ?? {}).map(([key, value]) => ({
|
|
146
|
+
const flags = Array.isArray(raw.flags) ? raw.flags : Object.entries(raw.flags ?? {}).map(([key, value]) => ({
|
|
147
|
+
key,
|
|
148
|
+
...value,
|
|
149
|
+
id: value._featureFlagId ?? value.id ?? "",
|
|
150
|
+
featureFlagEnvironmentId: value._featureFlagEnvironmentId ?? ""
|
|
151
|
+
}));
|
|
147
152
|
const actions = Array.isArray(raw.actions) ? raw.actions : Object.values(raw.actions ?? {});
|
|
148
153
|
return {
|
|
149
154
|
projectId: raw.project?.id ?? raw.projectId ?? "",
|
|
@@ -223,6 +228,74 @@ var SseUpdateProvider = class {
|
|
|
223
228
|
}
|
|
224
229
|
};
|
|
225
230
|
|
|
231
|
+
// src/metrics.ts
|
|
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) {
|
|
237
|
+
this.apiUrl = apiUrl;
|
|
238
|
+
this.sdkKey = sdkKey;
|
|
239
|
+
this.requestTimeout = requestTimeout;
|
|
240
|
+
this.queue = [];
|
|
241
|
+
this.flushTimer = null;
|
|
242
|
+
}
|
|
243
|
+
start() {
|
|
244
|
+
this.flushTimer = setInterval(() => {
|
|
245
|
+
this.flush().catch(() => {
|
|
246
|
+
});
|
|
247
|
+
}, FLUSH_INTERVAL_MS);
|
|
248
|
+
}
|
|
249
|
+
stop() {
|
|
250
|
+
if (this.flushTimer !== null) {
|
|
251
|
+
clearInterval(this.flushTimer);
|
|
252
|
+
this.flushTimer = null;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
push(event) {
|
|
256
|
+
this.queue.push(event);
|
|
257
|
+
if (this.queue.length >= FLUSH_BATCH_SIZE) {
|
|
258
|
+
this.flush().catch(() => {
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
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);
|
|
268
|
+
try {
|
|
269
|
+
await this.sendBatch(batch);
|
|
270
|
+
} catch {
|
|
271
|
+
if (attempt < MAX_RETRIES) {
|
|
272
|
+
this.queue.unshift(...batch);
|
|
273
|
+
await this.flushWithRetry(attempt + 1);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
async sendBatch(events) {
|
|
278
|
+
const controller = new AbortController();
|
|
279
|
+
const timeout = setTimeout(() => controller.abort(), this.requestTimeout);
|
|
280
|
+
try {
|
|
281
|
+
const response = await fetch(`${this.apiUrl}/sdk/metrics`, {
|
|
282
|
+
method: "POST",
|
|
283
|
+
headers: {
|
|
284
|
+
"x-api-key": this.sdkKey,
|
|
285
|
+
"Content-Type": "application/json"
|
|
286
|
+
},
|
|
287
|
+
body: JSON.stringify({ events }),
|
|
288
|
+
signal: controller.signal
|
|
289
|
+
});
|
|
290
|
+
if (!response.ok) {
|
|
291
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
292
|
+
}
|
|
293
|
+
} finally {
|
|
294
|
+
clearTimeout(timeout);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
|
|
226
299
|
// src/client.ts
|
|
227
300
|
var DEFAULT_API_URL = "https://rolloutctrl.io/api";
|
|
228
301
|
var DEFAULT_REFRESH_INTERVAL = 3e4;
|
|
@@ -232,14 +305,17 @@ var RolloutCtrlClient = class {
|
|
|
232
305
|
this.emitter = new TinyEmitter();
|
|
233
306
|
this.subscribers = /* @__PURE__ */ new Set();
|
|
234
307
|
this.isInitialized = false;
|
|
308
|
+
this.trackedEvaluations = /* @__PURE__ */ new Map();
|
|
235
309
|
const apiUrl = options.apiUrl ?? DEFAULT_API_URL;
|
|
236
310
|
const refreshInterval = options.refreshInterval ?? DEFAULT_REFRESH_INTERVAL;
|
|
237
311
|
const requestTimeout = options.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT;
|
|
238
312
|
this.repository = new HttpConfigurationRepository(apiUrl, options.sdkKey, options.environment, requestTimeout);
|
|
239
313
|
this.evaluatorManager = new EvaluatorManager();
|
|
240
314
|
this.updateProvider = new PollingUpdateProvider(refreshInterval);
|
|
315
|
+
this.metricsQueue = options.enableMetrics ?? true ? new MetricsQueue(apiUrl, options.sdkKey, requestTimeout) : null;
|
|
241
316
|
this.repository.subscribe((configuration) => {
|
|
242
317
|
this.evaluatorManager.update(configuration);
|
|
318
|
+
this.trackedEvaluations.clear();
|
|
243
319
|
options.storage?.set(configuration).catch(() => {
|
|
244
320
|
});
|
|
245
321
|
if (this.isInitialized) {
|
|
@@ -274,6 +350,7 @@ var RolloutCtrlClient = class {
|
|
|
274
350
|
this.updateProvider.start(() => {
|
|
275
351
|
void this.repository.refresh();
|
|
276
352
|
});
|
|
353
|
+
this.metricsQueue?.start();
|
|
277
354
|
this.emitter.emit("ready");
|
|
278
355
|
this.resolveReady();
|
|
279
356
|
} catch (error) {
|
|
@@ -295,6 +372,10 @@ var RolloutCtrlClient = class {
|
|
|
295
372
|
}
|
|
296
373
|
async close() {
|
|
297
374
|
this.updateProvider.stop();
|
|
375
|
+
if (this.metricsQueue) {
|
|
376
|
+
this.metricsQueue.stop();
|
|
377
|
+
await this.metricsQueue.flush();
|
|
378
|
+
}
|
|
298
379
|
this.subscribers.clear();
|
|
299
380
|
this.emitter.emit("shutdown");
|
|
300
381
|
this.emitter.removeAllListeners();
|
|
@@ -315,13 +396,44 @@ var RolloutCtrlClient = class {
|
|
|
315
396
|
return this.evaluatorManager.get();
|
|
316
397
|
}
|
|
317
398
|
isEnabled(flagKey, context) {
|
|
318
|
-
return this.
|
|
399
|
+
return this.evaluate(flagKey, context).enabled;
|
|
319
400
|
}
|
|
320
401
|
evaluate(flagKey, context) {
|
|
321
|
-
|
|
402
|
+
const result = this.getEvaluator().evaluate(flagKey, context);
|
|
403
|
+
this.trackEvaluation(flagKey, result);
|
|
404
|
+
return result;
|
|
322
405
|
}
|
|
323
406
|
getVariant(flagKey, context) {
|
|
324
|
-
return this.
|
|
407
|
+
return this.evaluate(flagKey, context).variant;
|
|
408
|
+
}
|
|
409
|
+
trackEvaluation(flagKey, result) {
|
|
410
|
+
if (!this.metricsQueue || !this.isInitialized) return;
|
|
411
|
+
try {
|
|
412
|
+
const config = this.repository.getConfiguration();
|
|
413
|
+
const flag = config.flags.find((f) => f.key === flagKey);
|
|
414
|
+
if (!flag) return;
|
|
415
|
+
let type;
|
|
416
|
+
if (result.variant) {
|
|
417
|
+
type = "VARIANT_EXPOSURE";
|
|
418
|
+
} else if (result.strategy) {
|
|
419
|
+
type = "STRATEGY_MATCH";
|
|
420
|
+
} else if (result.enabled) {
|
|
421
|
+
type = "FLAG_ENABLED";
|
|
422
|
+
} else {
|
|
423
|
+
type = "FLAG_DISABLED";
|
|
424
|
+
}
|
|
425
|
+
const fingerprint = `${type}:${result.strategy?.id ?? ""}:${result.variant?.id ?? ""}`;
|
|
426
|
+
if (this.trackedEvaluations.get(flagKey) === fingerprint) return;
|
|
427
|
+
this.trackedEvaluations.set(flagKey, fingerprint);
|
|
428
|
+
this.metricsQueue.push({
|
|
429
|
+
featureFlagId: flag.id,
|
|
430
|
+
featureFlagEnvironmentId: flag.featureFlagEnvironmentId,
|
|
431
|
+
type,
|
|
432
|
+
strategyId: result.strategy?.id,
|
|
433
|
+
variantId: result.variant?.id
|
|
434
|
+
});
|
|
435
|
+
} catch {
|
|
436
|
+
}
|
|
325
437
|
}
|
|
326
438
|
can(actionKey, context) {
|
|
327
439
|
return this.getEvaluator().can(actionKey, context);
|
|
@@ -360,6 +472,7 @@ export {
|
|
|
360
472
|
EvaluatorManager,
|
|
361
473
|
HttpConfigurationRepository,
|
|
362
474
|
LocalStorageAdapter,
|
|
475
|
+
MetricsQueue,
|
|
363
476
|
PollingUpdateProvider,
|
|
364
477
|
RolloutCtrlClient,
|
|
365
478
|
SseUpdateProvider,
|