@coframe/sdk 1.1.29 → 1.1.31

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.
@@ -96,6 +96,206 @@ const setUserTokenCookie = (uuid, logger) => {
96
96
  setCookie(USER_TOKEN_COOKIE_KEY, uuid);
97
97
  };
98
98
 
99
+ const FEATURE_FLAG_EVENT_TYPE = 'feature_flag_called';
100
+ const FEATURE_FLAG_BATCH_BYTES = 48 * 1024;
101
+ function resolveFeatureFlagIngestBase(customUrl) {
102
+ const normalized = customUrl === null || customUrl === void 0 ? void 0 : customUrl.trim().replace(/\/+$/, '');
103
+ return normalized || "https://ingest.app.coframe.com/ingest/v2";
104
+ }
105
+ function featureFlagEventsEndpoint(ingestBase, projectId) {
106
+ return `${ingestBase}/batched_events/${projectId}/`;
107
+ }
108
+ function featureFlagValue(result) {
109
+ var _a;
110
+ return (_a = result.variant) !== null && _a !== void 0 ? _a : result.enabled;
111
+ }
112
+ function snapshotPayload(payload) {
113
+ return payload === null
114
+ ? null
115
+ : JSON.parse(JSON.stringify(payload));
116
+ }
117
+ function newEventId() {
118
+ const cryptoObject = globalThis.crypto;
119
+ if ((cryptoObject === null || cryptoObject === void 0 ? void 0 : cryptoObject.randomUUID) || (cryptoObject === null || cryptoObject === void 0 ? void 0 : cryptoObject.getRandomValues)) {
120
+ return generateUUID();
121
+ }
122
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (char) => {
123
+ const random = Math.floor(Math.random() * 16);
124
+ return (char === 'x' ? random : (random & 0x3) | 0x8).toString(16);
125
+ });
126
+ }
127
+ function createFeatureFlagTrackingEvent(access) {
128
+ var _a;
129
+ const { result, response } = access;
130
+ const errors = [];
131
+ if (response.errorsWhileComputingFlags) {
132
+ errors.push('errors_while_computing_flags');
133
+ }
134
+ if (!result)
135
+ errors.push('flag_missing');
136
+ return {
137
+ event_id: newEventId(),
138
+ project_id: access.projectId,
139
+ session_id: access.sessionId,
140
+ type: FEATURE_FLAG_EVENT_TYPE,
141
+ time: new Date().toISOString(),
142
+ url: access.url,
143
+ user_token: access.userToken,
144
+ content: {
145
+ metadata: {
146
+ key: access.key,
147
+ ...(result
148
+ ? {
149
+ value: featureFlagValue(result),
150
+ payload: snapshotPayload('payload' in result ? ((_a = result.payload) !== null && _a !== void 0 ? _a : null) : null),
151
+ version: result.metadata.version,
152
+ reason: result.reason,
153
+ }
154
+ : {}),
155
+ request_id: response.requestId,
156
+ snapshot_version: response.snapshotVersion,
157
+ ...(errors.length > 0 ? { error: errors.join(',') } : {}),
158
+ },
159
+ },
160
+ };
161
+ }
162
+ function jsonBytes(value) {
163
+ return new TextEncoder().encode(JSON.stringify(value)).byteLength;
164
+ }
165
+ function chunkFeatureFlagEvents(events) {
166
+ const chunks = [];
167
+ let current = [];
168
+ for (const event of events) {
169
+ if (current.length > 0 &&
170
+ jsonBytes([...current, event]) > FEATURE_FLAG_BATCH_BYTES) {
171
+ chunks.push(current);
172
+ current = [];
173
+ }
174
+ current.push(event);
175
+ }
176
+ if (current.length > 0)
177
+ chunks.push(current);
178
+ return chunks;
179
+ }
180
+
181
+ const FLUSH_INTERVAL_MS = 3000;
182
+ const BATCH_SIZE = 32;
183
+ const MAX_QUEUE_SIZE = 1000;
184
+ let config$1;
185
+ let ingestBase = '';
186
+ let sessionId = '';
187
+ let consentAllowed = true;
188
+ let queue = [];
189
+ let flushTimer;
190
+ let warnedQueueFull = false;
191
+ let warnedTransport = false;
192
+ function installLifecycle() {
193
+ if (typeof window === 'undefined')
194
+ return;
195
+ flushTimer = window.setInterval(flushFeatureFlagBrowserEvents, FLUSH_INTERVAL_MS);
196
+ window.addEventListener('pagehide', flushFeatureFlagBrowserEvents);
197
+ if (typeof document !== 'undefined') {
198
+ document.addEventListener('visibilitychange', flushWhenHidden);
199
+ }
200
+ }
201
+ function removeLifecycle() {
202
+ if (typeof window === 'undefined')
203
+ return;
204
+ if (flushTimer !== undefined)
205
+ window.clearInterval(flushTimer);
206
+ flushTimer = undefined;
207
+ window.removeEventListener('pagehide', flushFeatureFlagBrowserEvents);
208
+ if (typeof document !== 'undefined') {
209
+ document.removeEventListener('visibilitychange', flushWhenHidden);
210
+ }
211
+ }
212
+ function flushWhenHidden() {
213
+ if (typeof document !== 'undefined' &&
214
+ document.visibilityState === 'hidden') {
215
+ flushFeatureFlagBrowserEvents();
216
+ }
217
+ }
218
+ function configureFeatureFlagBrowserTracking(nextConfig) {
219
+ removeLifecycle();
220
+ const nextIngestBase = resolveFeatureFlagIngestBase(nextConfig.ingestUrl);
221
+ const sameTarget = (config$1 === null || config$1 === void 0 ? void 0 : config$1.projectId) === nextConfig.projectId && ingestBase === nextIngestBase;
222
+ if (!sameTarget) {
223
+ flushFeatureFlagBrowserEvents();
224
+ queue = [];
225
+ }
226
+ config$1 = nextConfig;
227
+ ingestBase = nextIngestBase;
228
+ consentAllowed = nextConfig.consentAllowed;
229
+ warnedQueueFull = false;
230
+ warnedTransport = false;
231
+ sessionId || (sessionId = generateUUID());
232
+ installLifecycle();
233
+ }
234
+ function updateFeatureFlagTrackingUserToken(nextUserToken) {
235
+ if (config$1)
236
+ config$1.userToken = nextUserToken;
237
+ }
238
+ function setFeatureFlagTrackingConsent(enabled) {
239
+ consentAllowed = enabled;
240
+ if (enabled) {
241
+ flushFeatureFlagBrowserEvents();
242
+ }
243
+ }
244
+ function trackFeatureFlagBrowserAccess(key, result, response) {
245
+ if (!config$1)
246
+ return;
247
+ const access = {
248
+ projectId: config$1.projectId,
249
+ sessionId,
250
+ userToken: config$1.userToken,
251
+ url: typeof window === 'undefined' ? '' : window.location.href,
252
+ key,
253
+ result,
254
+ response,
255
+ };
256
+ queue.push(createFeatureFlagTrackingEvent(access));
257
+ if (queue.length > MAX_QUEUE_SIZE) {
258
+ queue.shift();
259
+ if (!warnedQueueFull) {
260
+ warnedQueueFull = true;
261
+ console.warn('[Coframe] Feature Flag tracking queue is full');
262
+ }
263
+ }
264
+ if (consentAllowed && queue.length >= BATCH_SIZE) {
265
+ flushFeatureFlagBrowserEvents();
266
+ }
267
+ }
268
+ function flushFeatureFlagBrowserEvents() {
269
+ if (!config$1 || !consentAllowed || queue.length === 0)
270
+ return;
271
+ const events = queue;
272
+ queue = [];
273
+ const endpoint = featureFlagEventsEndpoint(ingestBase, config$1.projectId);
274
+ if (typeof navigator === 'undefined' || !navigator.sendBeacon) {
275
+ if (!warnedTransport) {
276
+ warnedTransport = true;
277
+ console.warn('[Coframe] Feature Flag tracking requires navigator.sendBeacon');
278
+ }
279
+ return;
280
+ }
281
+ try {
282
+ for (const chunk of chunkFeatureFlagEvents(events)) {
283
+ const blob = new Blob([JSON.stringify(chunk)], { type: 'text/plain' });
284
+ if (!navigator.sendBeacon(endpoint, blob))
285
+ warnTransport();
286
+ }
287
+ }
288
+ catch (_a) {
289
+ warnTransport();
290
+ }
291
+ }
292
+ function warnTransport() {
293
+ if (warnedTransport)
294
+ return;
295
+ warnedTransport = true;
296
+ console.warn('[Coframe] Failed to send Feature Flag tracking beacon');
297
+ }
298
+
99
299
  const DEFAULT_TIMEOUT_MS = 3000;
100
300
  const DEFAULT_CACHE_TTL_MS = 5 * 60 * 1000;
101
301
  const CACHE_PREFIX = 'coframe.featureFlags.';
@@ -239,7 +439,7 @@ async function fetchFeatureFlags(context) {
239
439
  window.clearTimeout(timeout);
240
440
  }
241
441
  }
242
- async function initializeFeatureFlags(nextConfig, nextProjectId, nextUserToken, nextUserContext) {
442
+ async function initializeFeatureFlags(nextConfig, nextProjectId, nextUserToken, nextUserContext, trackingConsentAllowed = true) {
243
443
  var _a;
244
444
  if (!nextConfig)
245
445
  return;
@@ -247,6 +447,12 @@ async function initializeFeatureFlags(nextConfig, nextProjectId, nextUserToken,
247
447
  projectId = nextProjectId;
248
448
  userToken = nextUserToken;
249
449
  userContext = nextUserContext;
450
+ configureFeatureFlagBrowserTracking({
451
+ projectId: nextProjectId,
452
+ userToken: nextUserToken,
453
+ ingestUrl: nextConfig.ingestUrl,
454
+ consentAllowed: trackingConsentAllowed,
455
+ });
250
456
  requestGeneration += 1;
251
457
  inFlight = undefined;
252
458
  inFlightContextKey = undefined;
@@ -334,6 +540,7 @@ function updateFeatureFlagUserToken(nextUserToken) {
334
540
  if (nextUserToken === userToken)
335
541
  return;
336
542
  userToken = nextUserToken;
543
+ updateFeatureFlagTrackingUserToken(nextUserToken);
337
544
  requestGeneration += 1;
338
545
  inFlight = undefined;
339
546
  inFlightContextKey = undefined;
@@ -360,23 +567,43 @@ function onFeatureFlags(callback) {
360
567
  listeners.splice(index, 1);
361
568
  };
362
569
  }
363
- function getFeatureFlagResult(key) {
570
+ function peekFeatureFlagResult(key) {
364
571
  return response === null || response === void 0 ? void 0 : response.flags[key];
365
572
  }
366
- function getFeatureFlag(key) {
573
+ let warnedTrackingFailure = false;
574
+ function trackFeatureFlagAccess(key) {
575
+ if (!response)
576
+ return;
577
+ try {
578
+ trackFeatureFlagBrowserAccess(key, response.flags[key], response);
579
+ }
580
+ catch (_a) {
581
+ if (!warnedTrackingFailure) {
582
+ warnedTrackingFailure = true;
583
+ console.warn('[Coframe] Feature Flag tracking failed');
584
+ }
585
+ }
586
+ }
587
+ function getFeatureFlagResult(key, options = {}) {
588
+ const result = peekFeatureFlagResult(key);
589
+ if (options.sendEvent !== false)
590
+ trackFeatureFlagAccess(key);
591
+ return result;
592
+ }
593
+ function getFeatureFlag(key, options) {
367
594
  var _a;
368
- const result = getFeatureFlagResult(key);
595
+ const result = getFeatureFlagResult(key, options);
369
596
  if (!result)
370
597
  return undefined;
371
598
  return (_a = result.variant) !== null && _a !== void 0 ? _a : result.enabled;
372
599
  }
373
600
  function isFeatureEnabled(key, options) {
374
601
  var _a, _b;
375
- return (_b = (_a = getFeatureFlagResult(key)) === null || _a === void 0 ? void 0 : _a.enabled) !== null && _b !== void 0 ? _b : options === null || options === void 0 ? void 0 : options.defaultValue;
602
+ return (_b = (_a = getFeatureFlagResult(key, options)) === null || _a === void 0 ? void 0 : _a.enabled) !== null && _b !== void 0 ? _b : options === null || options === void 0 ? void 0 : options.defaultValue;
376
603
  }
377
- function getFeatureFlagPayload(key) {
604
+ function getFeatureFlagPayload(key, options) {
378
605
  var _a;
379
- const result = getFeatureFlagResult(key);
606
+ const result = getFeatureFlagResult(key, options);
380
607
  if (!result)
381
608
  return undefined;
382
609
  return 'payload' in result ? ((_a = result.payload) !== null && _a !== void 0 ? _a : null) : null;
@@ -509,6 +736,7 @@ function persistIdentity() {
509
736
  }
510
737
  function applyConsent(enabled) {
511
738
  setCookiesEnabled(enabled);
739
+ setFeatureFlagTrackingConsent(enabled);
512
740
  if (enabled)
513
741
  persistIdentity();
514
742
  }
@@ -550,6 +778,7 @@ function installConsentSync() {
550
778
  subscribeToCmd((cmd) => typeof cmd.cookiesEnabled === 'boolean', (cmd) => applyConsent(!!cmd.cookiesEnabled));
551
779
  }
552
780
  function initFeatureFlags(options) {
781
+ var _a, _b;
553
782
  const queuedConsent = getQueuedConsent();
554
783
  installConsentSync();
555
784
  const queuedUserToken = options.userToken ? undefined : getQueuedUserToken();
@@ -571,8 +800,9 @@ function initFeatureFlags(options) {
571
800
  if (options.reloadOnSpaNavigation !== false) {
572
801
  installSpaNavigationReload();
573
802
  }
574
- const { selector, requestTimeoutMs, cacheTtlMs } = options;
575
- return initializeFeatureFlags({ selector, requestTimeoutMs, cacheTtlMs }, options.projectId, currentUserToken, options.userContext);
803
+ const { selector, requestTimeoutMs, cacheTtlMs, ingestUrl } = options;
804
+ const trackingConsentAllowed = (_b = (_a = options.consent) !== null && _a !== void 0 ? _a : queuedConsent) !== null && _b !== void 0 ? _b : true;
805
+ return initializeFeatureFlags({ selector, requestTimeoutMs, cacheTtlMs, ingestUrl }, options.projectId, currentUserToken, options.userContext, trackingConsentAllowed);
576
806
  }
577
807
  function setUserToken(userToken) {
578
808
  currentUserToken = requireIdentity(userToken);
@@ -62,29 +62,35 @@ function useCoframeVariant(experimentId) {
62
62
  var _a;
63
63
  return (_a = useCoframeTreatment(experimentId)) === null || _a === void 0 ? void 0 : _a.variantId;
64
64
  }
65
- function useFeatureFlagResult(key) {
66
- const [result, setResult] = React.useState(() => featureFlags.getFeatureFlagResult(key));
65
+ function useFeatureFlagResult(key, options) {
66
+ const [result, setResult] = React.useState(() => featureFlags.getFeatureFlagResult(key, { sendEvent: false }));
67
+ const sendEvent = options === null || options === void 0 ? void 0 : options.sendEvent;
67
68
  React.useEffect(() => {
68
- setResult(featureFlags.getFeatureFlagResult(key));
69
- return featureFlags.onFeatureFlags(() => setResult(featureFlags.getFeatureFlagResult(key)));
70
- }, [key]);
69
+ let subscribing = true;
70
+ const unsubscribe = featureFlags.onFeatureFlags(() => setResult(featureFlags.getFeatureFlagResult(key, {
71
+ sendEvent: subscribing ? false : sendEvent,
72
+ })));
73
+ subscribing = false;
74
+ setResult(featureFlags.getFeatureFlagResult(key, { sendEvent }));
75
+ return unsubscribe;
76
+ }, [key, sendEvent]);
71
77
  return result;
72
78
  }
73
- function useFeatureFlagVariantKey(key) {
79
+ function useFeatureFlagVariantKey(key, options) {
74
80
  var _a;
75
- const result = useFeatureFlagResult(key);
81
+ const result = useFeatureFlagResult(key, options);
76
82
  if (!result)
77
83
  return undefined;
78
84
  return (_a = result.variant) !== null && _a !== void 0 ? _a : result.enabled;
79
85
  }
80
86
  const useFeatureFlag = useFeatureFlagVariantKey;
81
- function useFeatureFlagEnabled(key) {
87
+ function useFeatureFlagEnabled(key, options) {
82
88
  var _a;
83
- return (_a = useFeatureFlagResult(key)) === null || _a === void 0 ? void 0 : _a.enabled;
89
+ return (_a = useFeatureFlagResult(key, options)) === null || _a === void 0 ? void 0 : _a.enabled;
84
90
  }
85
- function useFeatureFlagPayload(key) {
91
+ function useFeatureFlagPayload(key, options) {
86
92
  var _a;
87
- const result = useFeatureFlagResult(key);
93
+ const result = useFeatureFlagResult(key, options);
88
94
  if (!result)
89
95
  return undefined;
90
96
  return 'payload' in result ? ((_a = result.payload) !== null && _a !== void 0 ? _a : null) : null;
@@ -61,29 +61,35 @@ function useCoframeVariant(experimentId) {
61
61
  var _a;
62
62
  return (_a = useCoframeTreatment(experimentId)) === null || _a === void 0 ? void 0 : _a.variantId;
63
63
  }
64
- function useFeatureFlagResult(key) {
65
- const [result, setResult] = useState(() => getFeatureFlagResult(key));
64
+ function useFeatureFlagResult(key, options) {
65
+ const [result, setResult] = useState(() => getFeatureFlagResult(key, { sendEvent: false }));
66
+ const sendEvent = options === null || options === void 0 ? void 0 : options.sendEvent;
66
67
  useEffect(() => {
67
- setResult(getFeatureFlagResult(key));
68
- return onFeatureFlags(() => setResult(getFeatureFlagResult(key)));
69
- }, [key]);
68
+ let subscribing = true;
69
+ const unsubscribe = onFeatureFlags(() => setResult(getFeatureFlagResult(key, {
70
+ sendEvent: subscribing ? false : sendEvent,
71
+ })));
72
+ subscribing = false;
73
+ setResult(getFeatureFlagResult(key, { sendEvent }));
74
+ return unsubscribe;
75
+ }, [key, sendEvent]);
70
76
  return result;
71
77
  }
72
- function useFeatureFlagVariantKey(key) {
78
+ function useFeatureFlagVariantKey(key, options) {
73
79
  var _a;
74
- const result = useFeatureFlagResult(key);
80
+ const result = useFeatureFlagResult(key, options);
75
81
  if (!result)
76
82
  return undefined;
77
83
  return (_a = result.variant) !== null && _a !== void 0 ? _a : result.enabled;
78
84
  }
79
85
  const useFeatureFlag = useFeatureFlagVariantKey;
80
- function useFeatureFlagEnabled(key) {
86
+ function useFeatureFlagEnabled(key, options) {
81
87
  var _a;
82
- return (_a = useFeatureFlagResult(key)) === null || _a === void 0 ? void 0 : _a.enabled;
88
+ return (_a = useFeatureFlagResult(key, options)) === null || _a === void 0 ? void 0 : _a.enabled;
83
89
  }
84
- function useFeatureFlagPayload(key) {
90
+ function useFeatureFlagPayload(key, options) {
85
91
  var _a;
86
- const result = useFeatureFlagResult(key);
92
+ const result = useFeatureFlagResult(key, options);
87
93
  if (!result)
88
94
  return undefined;
89
95
  return 'payload' in result ? ((_a = result.payload) !== null && _a !== void 0 ? _a : null) : null;