@coframe/sdk 1.1.30 → 1.1.32

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/README.md CHANGED
@@ -161,6 +161,8 @@ setUserToken('customer-123')
161
161
 
162
162
  await featureFlags.init({
163
163
  projectId: 'your-project-id',
164
+ // Optional: replaces the complete default ingest base, including /ingest/v2.
165
+ ingestUrl: 'https://events.example.com/coframe',
164
166
  selector: {
165
167
  names: ['checkout-redesign'],
166
168
  groups: ['homepage_campaigns'],
@@ -192,6 +194,24 @@ context. It can fall back to that persistent cache if Edge is temporarily
192
194
  unavailable. Call `reloadFeatureFlags()` to request fresh evaluations
193
195
  explicitly.
194
196
 
197
+ Every single-flag getter automatically records a `feature_flag_called` event
198
+ after a result has loaded. This applies to boolean, multivariate, and Remote
199
+ Config flags. The event includes the key, evaluated value, payload, version,
200
+ reason, and evaluation request metadata. The project, key, and version identify
201
+ the corresponding flag definition without changing the Edge response. Every
202
+ getter or hook access records its own event, including repeated reads of the
203
+ same result. To read without recording an event, pass `{ sendEvent: false }`:
204
+
205
+ ```ts
206
+ const variant = getFeatureFlag('checkout-layout', { sendEvent: false })
207
+ ```
208
+
209
+ Tracking uses an independent Feature Flags queue and does not initialize the
210
+ main SDK. It follows the shared consent state: while consent is explicitly
211
+ disabled, events remain in memory and are sent after consent is enabled. The
212
+ optional Feature Flags `ingestUrl` has the same base-replacement behavior as
213
+ the main SDK; the client appends `/batched_events/<projectId>/`.
214
+
195
215
  Payloads support any JSON value up to 32 KB. A payload is returned only when its
196
216
  flag is enabled. Remote Config uses the same transport and payload API and
197
217
  returns the same payload to all users while active:
@@ -247,15 +267,20 @@ function CheckoutLayout() {
247
267
 
248
268
  For server and edge runtimes, use a secret Feature Flags Server API key. It is
249
269
  project-scoped, can read **Private** flags, and must never be included in a
250
- browser bundle or a `NEXT_PUBLIC_*` environment variable. Each single-key
251
- helper sends a key-filtered request and fails closed unless a `defaultValue` is
252
- supplied. Browser and server helpers both call `/api/v1/flags`; server helpers
253
- include the API key as a Bearer credential so the response may include both
254
- **Public** and **Private** flags:
270
+ browser bundle or a `NEXT_PUBLIC_*` environment variable. `evaluateFlags()`
271
+ records every flag returned by its selector, while each single-key helper
272
+ records only that key. Repeated evaluations record new events even when the
273
+ result is unchanged. Both paths enqueue tracking without delaying their result
274
+ and accept `sendEvent: false` to evaluate without recording an event.
275
+ Single-key helpers send a key-filtered request and fail closed unless a
276
+ `defaultValue` is supplied. Browser and server helpers both call
277
+ `/api/v1/flags`; server helpers include the API key as a Bearer credential so
278
+ the response may include both **Public** and **Private** flags:
255
279
 
256
280
  ```ts
257
281
  import {
258
282
  evaluateFlags,
283
+ flushFeatureFlagEvents,
259
284
  getFeatureFlag,
260
285
  getRemoteConfig,
261
286
  isFeatureEnabled,
@@ -272,8 +297,19 @@ const options = {
272
297
  const checkoutFlags = await evaluateFlags({
273
298
  ...options,
274
299
  selector: { groups: ['cart_checkout'] },
300
+ ingestUrl: 'https://events.example.com/coframe',
275
301
  })
276
302
 
303
+ // evaluateFlags tracks every returned flag without another Edge request.
304
+ // Single-key helpers track their requested key automatically.
305
+ const checkoutVariant = await getFeatureFlag('checkout-layout', {
306
+ ...options,
307
+ ingestUrl: 'https://events.example.com/coframe',
308
+ })
309
+
310
+ // Useful before a short-lived process exits. Tracking failures are swallowed.
311
+ await flushFeatureFlagEvents()
312
+
277
313
  const enabled = await isFeatureEnabled('checkout-redesign', {
278
314
  ...options,
279
315
  defaultValue: false,
@@ -420,6 +456,7 @@ Modern resolution modes handle package subpath exports more reliably.
420
456
  - `evaluateFlags(options)`
421
457
  - `getFeatureFlag(key, options)` / `isFeatureEnabled(key, options)`
422
458
  - `getFeatureFlagPayload(key, options)` / `getRemoteConfig(key, options)`
459
+ - `flushFeatureFlagEvents()`
423
460
 
424
461
  Returns a payload that may include:
425
462
 
@@ -991,7 +991,13 @@ function isDebugToolsSessionEnabled(allowed) {
991
991
  clearDebugToolsSession();
992
992
  return false;
993
993
  }
994
- const enabledByUrl = resolveDebugToolsEntry().kind !== 'none';
994
+ const entry = resolveDebugToolsEntry();
995
+ const enabledByUrl = entry.kind !== 'none';
996
+ const annotationUrlEntry = entry.kind === 'tool' && entry.toolId === 'annotations';
997
+ if (annotationUrlEntry) {
998
+ clearDebugToolsSession();
999
+ return true;
1000
+ }
995
1001
  if (enabledByUrl) {
996
1002
  enableDebugToolsSession();
997
1003
  return true;
@@ -999,6 +1005,8 @@ function isDebugToolsSessionEnabled(allowed) {
999
1005
  try {
1000
1006
  const enabled = window.sessionStorage.getItem(COFRAME_DEBUG_TOOLS_SESSION_STORAGE_KEY) ===
1001
1007
  ENABLED_VALUE;
1008
+ if (!enabled)
1009
+ clearDebugToolsSession();
1002
1010
  return enabled;
1003
1011
  }
1004
1012
  catch (_a) {
@@ -989,7 +989,13 @@ function isDebugToolsSessionEnabled(allowed) {
989
989
  clearDebugToolsSession();
990
990
  return false;
991
991
  }
992
- const enabledByUrl = resolveDebugToolsEntry().kind !== 'none';
992
+ const entry = resolveDebugToolsEntry();
993
+ const enabledByUrl = entry.kind !== 'none';
994
+ const annotationUrlEntry = entry.kind === 'tool' && entry.toolId === 'annotations';
995
+ if (annotationUrlEntry) {
996
+ clearDebugToolsSession();
997
+ return true;
998
+ }
993
999
  if (enabledByUrl) {
994
1000
  enableDebugToolsSession();
995
1001
  return true;
@@ -997,6 +1003,8 @@ function isDebugToolsSessionEnabled(allowed) {
997
1003
  try {
998
1004
  const enabled = window.sessionStorage.getItem(COFRAME_DEBUG_TOOLS_SESSION_STORAGE_KEY) ===
999
1005
  ENABLED_VALUE;
1006
+ if (!enabled)
1007
+ clearDebugToolsSession();
1000
1008
  return enabled;
1001
1009
  }
1002
1010
  catch (_a) {
@@ -98,6 +98,206 @@ const setUserTokenCookie = (uuid, logger) => {
98
98
  setCookie(USER_TOKEN_COOKIE_KEY, uuid);
99
99
  };
100
100
 
101
+ const FEATURE_FLAG_EVENT_TYPE = 'feature_flag_called';
102
+ const FEATURE_FLAG_BATCH_BYTES = 48 * 1024;
103
+ function resolveFeatureFlagIngestBase(customUrl) {
104
+ const normalized = customUrl === null || customUrl === void 0 ? void 0 : customUrl.trim().replace(/\/+$/, '');
105
+ return normalized || "https://ingest.app.coframe.com/ingest/v2";
106
+ }
107
+ function featureFlagEventsEndpoint(ingestBase, projectId) {
108
+ return `${ingestBase}/batched_events/${projectId}/`;
109
+ }
110
+ function featureFlagValue(result) {
111
+ var _a;
112
+ return (_a = result.variant) !== null && _a !== void 0 ? _a : result.enabled;
113
+ }
114
+ function snapshotPayload(payload) {
115
+ return payload === null
116
+ ? null
117
+ : JSON.parse(JSON.stringify(payload));
118
+ }
119
+ function newEventId() {
120
+ const cryptoObject = globalThis.crypto;
121
+ if ((cryptoObject === null || cryptoObject === void 0 ? void 0 : cryptoObject.randomUUID) || (cryptoObject === null || cryptoObject === void 0 ? void 0 : cryptoObject.getRandomValues)) {
122
+ return generateUUID();
123
+ }
124
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (char) => {
125
+ const random = Math.floor(Math.random() * 16);
126
+ return (char === 'x' ? random : (random & 0x3) | 0x8).toString(16);
127
+ });
128
+ }
129
+ function createFeatureFlagTrackingEvent(access) {
130
+ var _a;
131
+ const { result, response } = access;
132
+ const errors = [];
133
+ if (response.errorsWhileComputingFlags) {
134
+ errors.push('errors_while_computing_flags');
135
+ }
136
+ if (!result)
137
+ errors.push('flag_missing');
138
+ return {
139
+ event_id: newEventId(),
140
+ project_id: access.projectId,
141
+ session_id: access.sessionId,
142
+ type: FEATURE_FLAG_EVENT_TYPE,
143
+ time: new Date().toISOString(),
144
+ url: access.url,
145
+ user_token: access.userToken,
146
+ content: {
147
+ metadata: {
148
+ key: access.key,
149
+ ...(result
150
+ ? {
151
+ value: featureFlagValue(result),
152
+ payload: snapshotPayload('payload' in result ? ((_a = result.payload) !== null && _a !== void 0 ? _a : null) : null),
153
+ version: result.metadata.version,
154
+ reason: result.reason,
155
+ }
156
+ : {}),
157
+ request_id: response.requestId,
158
+ snapshot_version: response.snapshotVersion,
159
+ ...(errors.length > 0 ? { error: errors.join(',') } : {}),
160
+ },
161
+ },
162
+ };
163
+ }
164
+ function jsonBytes(value) {
165
+ return new TextEncoder().encode(JSON.stringify(value)).byteLength;
166
+ }
167
+ function chunkFeatureFlagEvents(events) {
168
+ const chunks = [];
169
+ let current = [];
170
+ for (const event of events) {
171
+ if (current.length > 0 &&
172
+ jsonBytes([...current, event]) > FEATURE_FLAG_BATCH_BYTES) {
173
+ chunks.push(current);
174
+ current = [];
175
+ }
176
+ current.push(event);
177
+ }
178
+ if (current.length > 0)
179
+ chunks.push(current);
180
+ return chunks;
181
+ }
182
+
183
+ const FLUSH_INTERVAL_MS = 3000;
184
+ const BATCH_SIZE = 32;
185
+ const MAX_QUEUE_SIZE = 1000;
186
+ let config$1;
187
+ let ingestBase = '';
188
+ let sessionId = '';
189
+ let consentAllowed = true;
190
+ let queue = [];
191
+ let flushTimer;
192
+ let warnedQueueFull = false;
193
+ let warnedTransport = false;
194
+ function installLifecycle() {
195
+ if (typeof window === 'undefined')
196
+ return;
197
+ flushTimer = window.setInterval(flushFeatureFlagBrowserEvents, FLUSH_INTERVAL_MS);
198
+ window.addEventListener('pagehide', flushFeatureFlagBrowserEvents);
199
+ if (typeof document !== 'undefined') {
200
+ document.addEventListener('visibilitychange', flushWhenHidden);
201
+ }
202
+ }
203
+ function removeLifecycle() {
204
+ if (typeof window === 'undefined')
205
+ return;
206
+ if (flushTimer !== undefined)
207
+ window.clearInterval(flushTimer);
208
+ flushTimer = undefined;
209
+ window.removeEventListener('pagehide', flushFeatureFlagBrowserEvents);
210
+ if (typeof document !== 'undefined') {
211
+ document.removeEventListener('visibilitychange', flushWhenHidden);
212
+ }
213
+ }
214
+ function flushWhenHidden() {
215
+ if (typeof document !== 'undefined' &&
216
+ document.visibilityState === 'hidden') {
217
+ flushFeatureFlagBrowserEvents();
218
+ }
219
+ }
220
+ function configureFeatureFlagBrowserTracking(nextConfig) {
221
+ removeLifecycle();
222
+ const nextIngestBase = resolveFeatureFlagIngestBase(nextConfig.ingestUrl);
223
+ const sameTarget = (config$1 === null || config$1 === void 0 ? void 0 : config$1.projectId) === nextConfig.projectId && ingestBase === nextIngestBase;
224
+ if (!sameTarget) {
225
+ flushFeatureFlagBrowserEvents();
226
+ queue = [];
227
+ }
228
+ config$1 = nextConfig;
229
+ ingestBase = nextIngestBase;
230
+ consentAllowed = nextConfig.consentAllowed;
231
+ warnedQueueFull = false;
232
+ warnedTransport = false;
233
+ sessionId || (sessionId = generateUUID());
234
+ installLifecycle();
235
+ }
236
+ function updateFeatureFlagTrackingUserToken(nextUserToken) {
237
+ if (config$1)
238
+ config$1.userToken = nextUserToken;
239
+ }
240
+ function setFeatureFlagTrackingConsent(enabled) {
241
+ consentAllowed = enabled;
242
+ if (enabled) {
243
+ flushFeatureFlagBrowserEvents();
244
+ }
245
+ }
246
+ function trackFeatureFlagBrowserAccess(key, result, response) {
247
+ if (!config$1)
248
+ return;
249
+ const access = {
250
+ projectId: config$1.projectId,
251
+ sessionId,
252
+ userToken: config$1.userToken,
253
+ url: typeof window === 'undefined' ? '' : window.location.href,
254
+ key,
255
+ result,
256
+ response,
257
+ };
258
+ queue.push(createFeatureFlagTrackingEvent(access));
259
+ if (queue.length > MAX_QUEUE_SIZE) {
260
+ queue.shift();
261
+ if (!warnedQueueFull) {
262
+ warnedQueueFull = true;
263
+ console.warn('[Coframe] Feature Flag tracking queue is full');
264
+ }
265
+ }
266
+ if (consentAllowed && queue.length >= BATCH_SIZE) {
267
+ flushFeatureFlagBrowserEvents();
268
+ }
269
+ }
270
+ function flushFeatureFlagBrowserEvents() {
271
+ if (!config$1 || !consentAllowed || queue.length === 0)
272
+ return;
273
+ const events = queue;
274
+ queue = [];
275
+ const endpoint = featureFlagEventsEndpoint(ingestBase, config$1.projectId);
276
+ if (typeof navigator === 'undefined' || !navigator.sendBeacon) {
277
+ if (!warnedTransport) {
278
+ warnedTransport = true;
279
+ console.warn('[Coframe] Feature Flag tracking requires navigator.sendBeacon');
280
+ }
281
+ return;
282
+ }
283
+ try {
284
+ for (const chunk of chunkFeatureFlagEvents(events)) {
285
+ const blob = new Blob([JSON.stringify(chunk)], { type: 'text/plain' });
286
+ if (!navigator.sendBeacon(endpoint, blob))
287
+ warnTransport();
288
+ }
289
+ }
290
+ catch (_a) {
291
+ warnTransport();
292
+ }
293
+ }
294
+ function warnTransport() {
295
+ if (warnedTransport)
296
+ return;
297
+ warnedTransport = true;
298
+ console.warn('[Coframe] Failed to send Feature Flag tracking beacon');
299
+ }
300
+
101
301
  const DEFAULT_TIMEOUT_MS = 3000;
102
302
  const DEFAULT_CACHE_TTL_MS = 5 * 60 * 1000;
103
303
  const CACHE_PREFIX = 'coframe.featureFlags.';
@@ -241,7 +441,7 @@ async function fetchFeatureFlags(context) {
241
441
  window.clearTimeout(timeout);
242
442
  }
243
443
  }
244
- async function initializeFeatureFlags(nextConfig, nextProjectId, nextUserToken, nextUserContext) {
444
+ async function initializeFeatureFlags(nextConfig, nextProjectId, nextUserToken, nextUserContext, trackingConsentAllowed = true) {
245
445
  var _a;
246
446
  if (!nextConfig)
247
447
  return;
@@ -249,6 +449,12 @@ async function initializeFeatureFlags(nextConfig, nextProjectId, nextUserToken,
249
449
  projectId = nextProjectId;
250
450
  userToken = nextUserToken;
251
451
  userContext = nextUserContext;
452
+ configureFeatureFlagBrowserTracking({
453
+ projectId: nextProjectId,
454
+ userToken: nextUserToken,
455
+ ingestUrl: nextConfig.ingestUrl,
456
+ consentAllowed: trackingConsentAllowed,
457
+ });
252
458
  requestGeneration += 1;
253
459
  inFlight = undefined;
254
460
  inFlightContextKey = undefined;
@@ -336,6 +542,7 @@ function updateFeatureFlagUserToken(nextUserToken) {
336
542
  if (nextUserToken === userToken)
337
543
  return;
338
544
  userToken = nextUserToken;
545
+ updateFeatureFlagTrackingUserToken(nextUserToken);
339
546
  requestGeneration += 1;
340
547
  inFlight = undefined;
341
548
  inFlightContextKey = undefined;
@@ -362,23 +569,43 @@ function onFeatureFlags(callback) {
362
569
  listeners.splice(index, 1);
363
570
  };
364
571
  }
365
- function getFeatureFlagResult(key) {
572
+ function peekFeatureFlagResult(key) {
366
573
  return response === null || response === void 0 ? void 0 : response.flags[key];
367
574
  }
368
- function getFeatureFlag(key) {
575
+ let warnedTrackingFailure = false;
576
+ function trackFeatureFlagAccess(key) {
577
+ if (!response)
578
+ return;
579
+ try {
580
+ trackFeatureFlagBrowserAccess(key, response.flags[key], response);
581
+ }
582
+ catch (_a) {
583
+ if (!warnedTrackingFailure) {
584
+ warnedTrackingFailure = true;
585
+ console.warn('[Coframe] Feature Flag tracking failed');
586
+ }
587
+ }
588
+ }
589
+ function getFeatureFlagResult(key, options = {}) {
590
+ const result = peekFeatureFlagResult(key);
591
+ if (options.sendEvent !== false)
592
+ trackFeatureFlagAccess(key);
593
+ return result;
594
+ }
595
+ function getFeatureFlag(key, options) {
369
596
  var _a;
370
- const result = getFeatureFlagResult(key);
597
+ const result = getFeatureFlagResult(key, options);
371
598
  if (!result)
372
599
  return undefined;
373
600
  return (_a = result.variant) !== null && _a !== void 0 ? _a : result.enabled;
374
601
  }
375
602
  function isFeatureEnabled(key, options) {
376
603
  var _a, _b;
377
- 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;
604
+ 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;
378
605
  }
379
- function getFeatureFlagPayload(key) {
606
+ function getFeatureFlagPayload(key, options) {
380
607
  var _a;
381
- const result = getFeatureFlagResult(key);
608
+ const result = getFeatureFlagResult(key, options);
382
609
  if (!result)
383
610
  return undefined;
384
611
  return 'payload' in result ? ((_a = result.payload) !== null && _a !== void 0 ? _a : null) : null;
@@ -511,6 +738,7 @@ function persistIdentity() {
511
738
  }
512
739
  function applyConsent(enabled) {
513
740
  setCookiesEnabled(enabled);
741
+ setFeatureFlagTrackingConsent(enabled);
514
742
  if (enabled)
515
743
  persistIdentity();
516
744
  }
@@ -552,6 +780,7 @@ function installConsentSync() {
552
780
  subscribeToCmd((cmd) => typeof cmd.cookiesEnabled === 'boolean', (cmd) => applyConsent(!!cmd.cookiesEnabled));
553
781
  }
554
782
  function initFeatureFlags(options) {
783
+ var _a, _b;
555
784
  const queuedConsent = getQueuedConsent();
556
785
  installConsentSync();
557
786
  const queuedUserToken = options.userToken ? undefined : getQueuedUserToken();
@@ -573,8 +802,9 @@ function initFeatureFlags(options) {
573
802
  if (options.reloadOnSpaNavigation !== false) {
574
803
  installSpaNavigationReload();
575
804
  }
576
- const { selector, requestTimeoutMs, cacheTtlMs } = options;
577
- return initializeFeatureFlags({ selector, requestTimeoutMs, cacheTtlMs }, options.projectId, currentUserToken, options.userContext);
805
+ const { selector, requestTimeoutMs, cacheTtlMs, ingestUrl } = options;
806
+ const trackingConsentAllowed = (_b = (_a = options.consent) !== null && _a !== void 0 ? _a : queuedConsent) !== null && _b !== void 0 ? _b : true;
807
+ return initializeFeatureFlags({ selector, requestTimeoutMs, cacheTtlMs, ingestUrl }, options.projectId, currentUserToken, options.userContext, trackingConsentAllowed);
578
808
  }
579
809
  function setUserToken(userToken) {
580
810
  currentUserToken = requireIdentity(userToken);