@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.
- package/README.md +42 -5
- package/dist/pkg/cf.cjs.js +10 -4
- package/dist/pkg/cf.esm.mjs +10 -4
- package/dist/pkg/feature-flags.cjs.js +239 -9
- package/dist/pkg/feature-flags.esm.mjs +239 -9
- package/dist/pkg/react.cjs.js +17 -11
- package/dist/pkg/react.esm.mjs +17 -11
- package/dist/pkg/server.cjs.js +276 -3
- package/dist/pkg/server.esm.mjs +276 -4
- package/dist/pkg/types/feature-flags.d.ts +9 -5
- package/dist/pkg/types/react.d.ts +5 -4
- package/dist/pkg/types/server.d.ts +12 -6
- package/package.json +1 -1
package/dist/pkg/server.cjs.js
CHANGED
|
@@ -22,6 +22,30 @@ function sanitizeUserContext(userContext) {
|
|
|
22
22
|
return userContext;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
function getGlobalScope() {
|
|
26
|
+
if (typeof globalThis !== 'undefined') {
|
|
27
|
+
return globalThis;
|
|
28
|
+
}
|
|
29
|
+
if (typeof window !== 'undefined') {
|
|
30
|
+
return window;
|
|
31
|
+
}
|
|
32
|
+
if (typeof self !== 'undefined') {
|
|
33
|
+
return self;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const generateUUID = () => {
|
|
37
|
+
var _a;
|
|
38
|
+
const cryptoObject = (_a = getGlobalScope()) === null || _a === void 0 ? void 0 : _a.crypto;
|
|
39
|
+
if (cryptoObject === null || cryptoObject === void 0 ? void 0 : cryptoObject.randomUUID)
|
|
40
|
+
return cryptoObject.randomUUID();
|
|
41
|
+
return `${1e7}${ -1e3}${ -4e3}${ -8e3}${ -1e11}`.replace(/[018]/g, (c) => {
|
|
42
|
+
var _a, _b;
|
|
43
|
+
const randomValue = (_b = (_a = cryptoObject === null || cryptoObject === void 0 ? void 0 : cryptoObject.getRandomValues) === null || _a === void 0 ? void 0 : _a.call(cryptoObject, new Uint8Array(1))[0]) !== null && _b !== void 0 ? _b : 0;
|
|
44
|
+
return (c ^
|
|
45
|
+
(randomValue & (15 >> (c / 4)))).toString(16);
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
|
|
25
49
|
async function fetchProjectFromEdge(projectId, currentUrl, signal, userContext, previewVariantId, headers, referrer) {
|
|
26
50
|
const payload = { projectId, currentUrl };
|
|
27
51
|
if (referrer !== undefined) {
|
|
@@ -48,6 +72,246 @@ async function fetchProjectFromEdge(projectId, currentUrl, signal, userContext,
|
|
|
48
72
|
}
|
|
49
73
|
}
|
|
50
74
|
|
|
75
|
+
const FEATURE_FLAG_EVENT_TYPE = 'feature_flag_called';
|
|
76
|
+
const FEATURE_FLAG_BATCH_BYTES = 48 * 1024;
|
|
77
|
+
function resolveFeatureFlagIngestBase(customUrl) {
|
|
78
|
+
const normalized = customUrl === null || customUrl === void 0 ? void 0 : customUrl.trim().replace(/\/+$/, '');
|
|
79
|
+
return normalized || "https://ingest.app.coframe.com/ingest/v2";
|
|
80
|
+
}
|
|
81
|
+
function featureFlagEventsEndpoint(ingestBase, projectId) {
|
|
82
|
+
return `${ingestBase}/batched_events/${projectId}/`;
|
|
83
|
+
}
|
|
84
|
+
function featureFlagValue(result) {
|
|
85
|
+
var _a;
|
|
86
|
+
return (_a = result.variant) !== null && _a !== void 0 ? _a : result.enabled;
|
|
87
|
+
}
|
|
88
|
+
function snapshotPayload(payload) {
|
|
89
|
+
return payload === null
|
|
90
|
+
? null
|
|
91
|
+
: JSON.parse(JSON.stringify(payload));
|
|
92
|
+
}
|
|
93
|
+
function newEventId() {
|
|
94
|
+
const cryptoObject = globalThis.crypto;
|
|
95
|
+
if ((cryptoObject === null || cryptoObject === void 0 ? void 0 : cryptoObject.randomUUID) || (cryptoObject === null || cryptoObject === void 0 ? void 0 : cryptoObject.getRandomValues)) {
|
|
96
|
+
return generateUUID();
|
|
97
|
+
}
|
|
98
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (char) => {
|
|
99
|
+
const random = Math.floor(Math.random() * 16);
|
|
100
|
+
return (char === 'x' ? random : (random & 0x3) | 0x8).toString(16);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
function createFeatureFlagTrackingEvent(access) {
|
|
104
|
+
var _a;
|
|
105
|
+
const { result, response } = access;
|
|
106
|
+
const errors = [];
|
|
107
|
+
if (response.errorsWhileComputingFlags) {
|
|
108
|
+
errors.push('errors_while_computing_flags');
|
|
109
|
+
}
|
|
110
|
+
if (!result)
|
|
111
|
+
errors.push('flag_missing');
|
|
112
|
+
return {
|
|
113
|
+
event_id: newEventId(),
|
|
114
|
+
project_id: access.projectId,
|
|
115
|
+
session_id: access.sessionId,
|
|
116
|
+
type: FEATURE_FLAG_EVENT_TYPE,
|
|
117
|
+
time: new Date().toISOString(),
|
|
118
|
+
url: access.url,
|
|
119
|
+
user_token: access.userToken,
|
|
120
|
+
content: {
|
|
121
|
+
metadata: {
|
|
122
|
+
key: access.key,
|
|
123
|
+
...(result
|
|
124
|
+
? {
|
|
125
|
+
value: featureFlagValue(result),
|
|
126
|
+
payload: snapshotPayload('payload' in result ? ((_a = result.payload) !== null && _a !== void 0 ? _a : null) : null),
|
|
127
|
+
version: result.metadata.version,
|
|
128
|
+
reason: result.reason,
|
|
129
|
+
}
|
|
130
|
+
: {}),
|
|
131
|
+
request_id: response.requestId,
|
|
132
|
+
snapshot_version: response.snapshotVersion,
|
|
133
|
+
...(errors.length > 0 ? { error: errors.join(',') } : {}),
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function jsonBytes(value) {
|
|
139
|
+
return new TextEncoder().encode(JSON.stringify(value)).byteLength;
|
|
140
|
+
}
|
|
141
|
+
function chunkFeatureFlagEvents(events) {
|
|
142
|
+
const chunks = [];
|
|
143
|
+
let current = [];
|
|
144
|
+
for (const event of events) {
|
|
145
|
+
if (current.length > 0 &&
|
|
146
|
+
jsonBytes([...current, event]) > FEATURE_FLAG_BATCH_BYTES) {
|
|
147
|
+
chunks.push(current);
|
|
148
|
+
current = [];
|
|
149
|
+
}
|
|
150
|
+
current.push(event);
|
|
151
|
+
}
|
|
152
|
+
if (current.length > 0)
|
|
153
|
+
chunks.push(current);
|
|
154
|
+
return chunks;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const FLUSH_INTERVAL_MS = 3000;
|
|
158
|
+
const BATCH_SIZE = 32;
|
|
159
|
+
const MAX_QUEUE_SIZE = 1000;
|
|
160
|
+
const MAX_RETRIES = 3;
|
|
161
|
+
const INGEST_TIMEOUT_MS = 3000;
|
|
162
|
+
let queue = [];
|
|
163
|
+
let flushTimer;
|
|
164
|
+
let inFlight;
|
|
165
|
+
let warnedQueueFull = false;
|
|
166
|
+
let warnedTransport = false;
|
|
167
|
+
let warnedTrackingFailure = false;
|
|
168
|
+
function scheduleFlush() {
|
|
169
|
+
var _a;
|
|
170
|
+
if (flushTimer !== undefined)
|
|
171
|
+
return;
|
|
172
|
+
flushTimer = setInterval(() => void flushOnce(), FLUSH_INTERVAL_MS);
|
|
173
|
+
const timer = flushTimer;
|
|
174
|
+
(_a = timer.unref) === null || _a === void 0 ? void 0 : _a.call(timer);
|
|
175
|
+
}
|
|
176
|
+
function enqueue(item) {
|
|
177
|
+
queue.push(item);
|
|
178
|
+
if (queue.length > MAX_QUEUE_SIZE) {
|
|
179
|
+
queue.shift();
|
|
180
|
+
if (!warnedQueueFull) {
|
|
181
|
+
warnedQueueFull = true;
|
|
182
|
+
console.warn('[Coframe] Feature Flag server tracking queue is full');
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
scheduleFlush();
|
|
186
|
+
if (queue.length >= BATCH_SIZE)
|
|
187
|
+
void flushOnce();
|
|
188
|
+
}
|
|
189
|
+
function trackFeatureFlagServerAccess(access) {
|
|
190
|
+
var _a;
|
|
191
|
+
const ingestBase = resolveFeatureFlagIngestBase(access.ingestUrl);
|
|
192
|
+
const commonAccess = {
|
|
193
|
+
projectId: access.projectId,
|
|
194
|
+
sessionId: '',
|
|
195
|
+
userToken: access.userToken,
|
|
196
|
+
url: (_a = access.currentUrl) !== null && _a !== void 0 ? _a : '',
|
|
197
|
+
key: access.key,
|
|
198
|
+
result: access.result,
|
|
199
|
+
response: access.response,
|
|
200
|
+
};
|
|
201
|
+
const endpoint = featureFlagEventsEndpoint(ingestBase, access.projectId);
|
|
202
|
+
enqueue({
|
|
203
|
+
endpoint,
|
|
204
|
+
event: createFeatureFlagTrackingEvent(commonAccess),
|
|
205
|
+
attempts: 0,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
function trackFeatureFlagServerReads(context, response, readResults) {
|
|
209
|
+
var _a;
|
|
210
|
+
try {
|
|
211
|
+
for (const [key, result] of readResults()) {
|
|
212
|
+
trackFeatureFlagServerAccess({
|
|
213
|
+
projectId: context.projectId,
|
|
214
|
+
userToken: (_a = context.userToken) !== null && _a !== void 0 ? _a : '',
|
|
215
|
+
currentUrl: context.currentUrl,
|
|
216
|
+
ingestUrl: context.ingestUrl,
|
|
217
|
+
key,
|
|
218
|
+
result,
|
|
219
|
+
response,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
catch (_b) {
|
|
224
|
+
if (!warnedTrackingFailure) {
|
|
225
|
+
warnedTrackingFailure = true;
|
|
226
|
+
console.warn('[Coframe] Feature Flag server tracking failed');
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function warnTransport() {
|
|
231
|
+
if (warnedTransport)
|
|
232
|
+
return;
|
|
233
|
+
warnedTransport = true;
|
|
234
|
+
console.warn('[Coframe] Failed to send Feature Flag server tracking');
|
|
235
|
+
}
|
|
236
|
+
async function sendGroup(endpoint, items) {
|
|
237
|
+
var _a;
|
|
238
|
+
const failed = [];
|
|
239
|
+
let chunks;
|
|
240
|
+
try {
|
|
241
|
+
chunks = chunkFeatureFlagEvents(items.map((item) => item.event));
|
|
242
|
+
}
|
|
243
|
+
catch (_b) {
|
|
244
|
+
warnTransport();
|
|
245
|
+
return failed;
|
|
246
|
+
}
|
|
247
|
+
let offset = 0;
|
|
248
|
+
for (const chunk of chunks) {
|
|
249
|
+
const chunkItems = items.slice(offset, offset + chunk.length);
|
|
250
|
+
offset += chunk.length;
|
|
251
|
+
const controller = new AbortController();
|
|
252
|
+
const timeout = setTimeout(() => controller.abort(), INGEST_TIMEOUT_MS);
|
|
253
|
+
const timer = timeout;
|
|
254
|
+
(_a = timer.unref) === null || _a === void 0 ? void 0 : _a.call(timer);
|
|
255
|
+
try {
|
|
256
|
+
const response = await fetch(endpoint, {
|
|
257
|
+
method: 'POST',
|
|
258
|
+
headers: { 'Content-Type': 'text/plain' },
|
|
259
|
+
body: JSON.stringify(chunk),
|
|
260
|
+
signal: controller.signal,
|
|
261
|
+
});
|
|
262
|
+
if (!response.ok)
|
|
263
|
+
throw new Error(`Feature Flag ingest failed (${response.status})`);
|
|
264
|
+
}
|
|
265
|
+
catch (_c) {
|
|
266
|
+
for (const item of chunkItems) {
|
|
267
|
+
if (item.attempts < MAX_RETRIES) {
|
|
268
|
+
failed.push({ ...item, attempts: item.attempts + 1 });
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
warnTransport();
|
|
272
|
+
}
|
|
273
|
+
finally {
|
|
274
|
+
clearTimeout(timeout);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return failed;
|
|
278
|
+
}
|
|
279
|
+
function flushOnce() {
|
|
280
|
+
var _a;
|
|
281
|
+
if (inFlight)
|
|
282
|
+
return inFlight;
|
|
283
|
+
if (queue.length === 0)
|
|
284
|
+
return Promise.resolve();
|
|
285
|
+
const pending = queue;
|
|
286
|
+
queue = [];
|
|
287
|
+
const groups = new Map();
|
|
288
|
+
for (const item of pending) {
|
|
289
|
+
const group = (_a = groups.get(item.endpoint)) !== null && _a !== void 0 ? _a : [];
|
|
290
|
+
group.push(item);
|
|
291
|
+
groups.set(item.endpoint, group);
|
|
292
|
+
}
|
|
293
|
+
inFlight = Promise.all(Array.from(groups, ([endpoint, items]) => sendGroup(endpoint, items)))
|
|
294
|
+
.then((failedGroups) => {
|
|
295
|
+
for (const failed of failedGroups) {
|
|
296
|
+
for (const item of failed)
|
|
297
|
+
enqueue(item);
|
|
298
|
+
}
|
|
299
|
+
})
|
|
300
|
+
.finally(() => {
|
|
301
|
+
inFlight = undefined;
|
|
302
|
+
});
|
|
303
|
+
return inFlight;
|
|
304
|
+
}
|
|
305
|
+
async function flushFeatureFlagEvents() {
|
|
306
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt += 1) {
|
|
307
|
+
if (inFlight)
|
|
308
|
+
await inFlight;
|
|
309
|
+
if (queue.length === 0)
|
|
310
|
+
return;
|
|
311
|
+
await flushOnce();
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
51
315
|
async function requestFeatureFlags(options) {
|
|
52
316
|
var _a, _b;
|
|
53
317
|
const controller = new AbortController();
|
|
@@ -88,15 +352,23 @@ async function requestFeatureFlags(options) {
|
|
|
88
352
|
clearTimeout(timeout);
|
|
89
353
|
}
|
|
90
354
|
}
|
|
91
|
-
function evaluateFlags(options) {
|
|
92
|
-
|
|
355
|
+
async function evaluateFlags(options) {
|
|
356
|
+
const response = await requestFeatureFlags(options);
|
|
357
|
+
if (response && options.sendEvent !== false) {
|
|
358
|
+
trackFeatureFlagServerReads(options, response, () => Object.entries(response.flags));
|
|
359
|
+
}
|
|
360
|
+
return response;
|
|
93
361
|
}
|
|
94
362
|
async function getFeatureFlagResult(key, options) {
|
|
95
363
|
const response = await requestFeatureFlags({
|
|
96
364
|
...options,
|
|
97
365
|
selector: { names: [key] },
|
|
98
366
|
});
|
|
99
|
-
|
|
367
|
+
const result = response === null || response === void 0 ? void 0 : response.flags[key];
|
|
368
|
+
if (response && options.sendEvent !== false) {
|
|
369
|
+
trackFeatureFlagServerReads(options, response, () => [[key, result]]);
|
|
370
|
+
}
|
|
371
|
+
return result;
|
|
100
372
|
}
|
|
101
373
|
async function getFeatureFlag(key, options) {
|
|
102
374
|
var _a;
|
|
@@ -194,6 +466,7 @@ async function getProjectData(options) {
|
|
|
194
466
|
}
|
|
195
467
|
|
|
196
468
|
exports.evaluateFlags = evaluateFlags;
|
|
469
|
+
exports.flushFeatureFlagEvents = flushFeatureFlagEvents;
|
|
197
470
|
exports.getFeatureFlag = getFeatureFlag;
|
|
198
471
|
exports.getFeatureFlagPayload = getFeatureFlagPayload;
|
|
199
472
|
exports.getFeatureFlagResult = getFeatureFlagResult;
|
package/dist/pkg/server.esm.mjs
CHANGED
|
@@ -20,6 +20,30 @@ function sanitizeUserContext(userContext) {
|
|
|
20
20
|
return userContext;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
function getGlobalScope() {
|
|
24
|
+
if (typeof globalThis !== 'undefined') {
|
|
25
|
+
return globalThis;
|
|
26
|
+
}
|
|
27
|
+
if (typeof window !== 'undefined') {
|
|
28
|
+
return window;
|
|
29
|
+
}
|
|
30
|
+
if (typeof self !== 'undefined') {
|
|
31
|
+
return self;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const generateUUID = () => {
|
|
35
|
+
var _a;
|
|
36
|
+
const cryptoObject = (_a = getGlobalScope()) === null || _a === void 0 ? void 0 : _a.crypto;
|
|
37
|
+
if (cryptoObject === null || cryptoObject === void 0 ? void 0 : cryptoObject.randomUUID)
|
|
38
|
+
return cryptoObject.randomUUID();
|
|
39
|
+
return `${1e7}${ -1e3}${ -4e3}${ -8e3}${ -1e11}`.replace(/[018]/g, (c) => {
|
|
40
|
+
var _a, _b;
|
|
41
|
+
const randomValue = (_b = (_a = cryptoObject === null || cryptoObject === void 0 ? void 0 : cryptoObject.getRandomValues) === null || _a === void 0 ? void 0 : _a.call(cryptoObject, new Uint8Array(1))[0]) !== null && _b !== void 0 ? _b : 0;
|
|
42
|
+
return (c ^
|
|
43
|
+
(randomValue & (15 >> (c / 4)))).toString(16);
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
|
|
23
47
|
async function fetchProjectFromEdge(projectId, currentUrl, signal, userContext, previewVariantId, headers, referrer) {
|
|
24
48
|
const payload = { projectId, currentUrl };
|
|
25
49
|
if (referrer !== undefined) {
|
|
@@ -46,6 +70,246 @@ async function fetchProjectFromEdge(projectId, currentUrl, signal, userContext,
|
|
|
46
70
|
}
|
|
47
71
|
}
|
|
48
72
|
|
|
73
|
+
const FEATURE_FLAG_EVENT_TYPE = 'feature_flag_called';
|
|
74
|
+
const FEATURE_FLAG_BATCH_BYTES = 48 * 1024;
|
|
75
|
+
function resolveFeatureFlagIngestBase(customUrl) {
|
|
76
|
+
const normalized = customUrl === null || customUrl === void 0 ? void 0 : customUrl.trim().replace(/\/+$/, '');
|
|
77
|
+
return normalized || "https://ingest.app.coframe.com/ingest/v2";
|
|
78
|
+
}
|
|
79
|
+
function featureFlagEventsEndpoint(ingestBase, projectId) {
|
|
80
|
+
return `${ingestBase}/batched_events/${projectId}/`;
|
|
81
|
+
}
|
|
82
|
+
function featureFlagValue(result) {
|
|
83
|
+
var _a;
|
|
84
|
+
return (_a = result.variant) !== null && _a !== void 0 ? _a : result.enabled;
|
|
85
|
+
}
|
|
86
|
+
function snapshotPayload(payload) {
|
|
87
|
+
return payload === null
|
|
88
|
+
? null
|
|
89
|
+
: JSON.parse(JSON.stringify(payload));
|
|
90
|
+
}
|
|
91
|
+
function newEventId() {
|
|
92
|
+
const cryptoObject = globalThis.crypto;
|
|
93
|
+
if ((cryptoObject === null || cryptoObject === void 0 ? void 0 : cryptoObject.randomUUID) || (cryptoObject === null || cryptoObject === void 0 ? void 0 : cryptoObject.getRandomValues)) {
|
|
94
|
+
return generateUUID();
|
|
95
|
+
}
|
|
96
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (char) => {
|
|
97
|
+
const random = Math.floor(Math.random() * 16);
|
|
98
|
+
return (char === 'x' ? random : (random & 0x3) | 0x8).toString(16);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
function createFeatureFlagTrackingEvent(access) {
|
|
102
|
+
var _a;
|
|
103
|
+
const { result, response } = access;
|
|
104
|
+
const errors = [];
|
|
105
|
+
if (response.errorsWhileComputingFlags) {
|
|
106
|
+
errors.push('errors_while_computing_flags');
|
|
107
|
+
}
|
|
108
|
+
if (!result)
|
|
109
|
+
errors.push('flag_missing');
|
|
110
|
+
return {
|
|
111
|
+
event_id: newEventId(),
|
|
112
|
+
project_id: access.projectId,
|
|
113
|
+
session_id: access.sessionId,
|
|
114
|
+
type: FEATURE_FLAG_EVENT_TYPE,
|
|
115
|
+
time: new Date().toISOString(),
|
|
116
|
+
url: access.url,
|
|
117
|
+
user_token: access.userToken,
|
|
118
|
+
content: {
|
|
119
|
+
metadata: {
|
|
120
|
+
key: access.key,
|
|
121
|
+
...(result
|
|
122
|
+
? {
|
|
123
|
+
value: featureFlagValue(result),
|
|
124
|
+
payload: snapshotPayload('payload' in result ? ((_a = result.payload) !== null && _a !== void 0 ? _a : null) : null),
|
|
125
|
+
version: result.metadata.version,
|
|
126
|
+
reason: result.reason,
|
|
127
|
+
}
|
|
128
|
+
: {}),
|
|
129
|
+
request_id: response.requestId,
|
|
130
|
+
snapshot_version: response.snapshotVersion,
|
|
131
|
+
...(errors.length > 0 ? { error: errors.join(',') } : {}),
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function jsonBytes(value) {
|
|
137
|
+
return new TextEncoder().encode(JSON.stringify(value)).byteLength;
|
|
138
|
+
}
|
|
139
|
+
function chunkFeatureFlagEvents(events) {
|
|
140
|
+
const chunks = [];
|
|
141
|
+
let current = [];
|
|
142
|
+
for (const event of events) {
|
|
143
|
+
if (current.length > 0 &&
|
|
144
|
+
jsonBytes([...current, event]) > FEATURE_FLAG_BATCH_BYTES) {
|
|
145
|
+
chunks.push(current);
|
|
146
|
+
current = [];
|
|
147
|
+
}
|
|
148
|
+
current.push(event);
|
|
149
|
+
}
|
|
150
|
+
if (current.length > 0)
|
|
151
|
+
chunks.push(current);
|
|
152
|
+
return chunks;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const FLUSH_INTERVAL_MS = 3000;
|
|
156
|
+
const BATCH_SIZE = 32;
|
|
157
|
+
const MAX_QUEUE_SIZE = 1000;
|
|
158
|
+
const MAX_RETRIES = 3;
|
|
159
|
+
const INGEST_TIMEOUT_MS = 3000;
|
|
160
|
+
let queue = [];
|
|
161
|
+
let flushTimer;
|
|
162
|
+
let inFlight;
|
|
163
|
+
let warnedQueueFull = false;
|
|
164
|
+
let warnedTransport = false;
|
|
165
|
+
let warnedTrackingFailure = false;
|
|
166
|
+
function scheduleFlush() {
|
|
167
|
+
var _a;
|
|
168
|
+
if (flushTimer !== undefined)
|
|
169
|
+
return;
|
|
170
|
+
flushTimer = setInterval(() => void flushOnce(), FLUSH_INTERVAL_MS);
|
|
171
|
+
const timer = flushTimer;
|
|
172
|
+
(_a = timer.unref) === null || _a === void 0 ? void 0 : _a.call(timer);
|
|
173
|
+
}
|
|
174
|
+
function enqueue(item) {
|
|
175
|
+
queue.push(item);
|
|
176
|
+
if (queue.length > MAX_QUEUE_SIZE) {
|
|
177
|
+
queue.shift();
|
|
178
|
+
if (!warnedQueueFull) {
|
|
179
|
+
warnedQueueFull = true;
|
|
180
|
+
console.warn('[Coframe] Feature Flag server tracking queue is full');
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
scheduleFlush();
|
|
184
|
+
if (queue.length >= BATCH_SIZE)
|
|
185
|
+
void flushOnce();
|
|
186
|
+
}
|
|
187
|
+
function trackFeatureFlagServerAccess(access) {
|
|
188
|
+
var _a;
|
|
189
|
+
const ingestBase = resolveFeatureFlagIngestBase(access.ingestUrl);
|
|
190
|
+
const commonAccess = {
|
|
191
|
+
projectId: access.projectId,
|
|
192
|
+
sessionId: '',
|
|
193
|
+
userToken: access.userToken,
|
|
194
|
+
url: (_a = access.currentUrl) !== null && _a !== void 0 ? _a : '',
|
|
195
|
+
key: access.key,
|
|
196
|
+
result: access.result,
|
|
197
|
+
response: access.response,
|
|
198
|
+
};
|
|
199
|
+
const endpoint = featureFlagEventsEndpoint(ingestBase, access.projectId);
|
|
200
|
+
enqueue({
|
|
201
|
+
endpoint,
|
|
202
|
+
event: createFeatureFlagTrackingEvent(commonAccess),
|
|
203
|
+
attempts: 0,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
function trackFeatureFlagServerReads(context, response, readResults) {
|
|
207
|
+
var _a;
|
|
208
|
+
try {
|
|
209
|
+
for (const [key, result] of readResults()) {
|
|
210
|
+
trackFeatureFlagServerAccess({
|
|
211
|
+
projectId: context.projectId,
|
|
212
|
+
userToken: (_a = context.userToken) !== null && _a !== void 0 ? _a : '',
|
|
213
|
+
currentUrl: context.currentUrl,
|
|
214
|
+
ingestUrl: context.ingestUrl,
|
|
215
|
+
key,
|
|
216
|
+
result,
|
|
217
|
+
response,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
catch (_b) {
|
|
222
|
+
if (!warnedTrackingFailure) {
|
|
223
|
+
warnedTrackingFailure = true;
|
|
224
|
+
console.warn('[Coframe] Feature Flag server tracking failed');
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
function warnTransport() {
|
|
229
|
+
if (warnedTransport)
|
|
230
|
+
return;
|
|
231
|
+
warnedTransport = true;
|
|
232
|
+
console.warn('[Coframe] Failed to send Feature Flag server tracking');
|
|
233
|
+
}
|
|
234
|
+
async function sendGroup(endpoint, items) {
|
|
235
|
+
var _a;
|
|
236
|
+
const failed = [];
|
|
237
|
+
let chunks;
|
|
238
|
+
try {
|
|
239
|
+
chunks = chunkFeatureFlagEvents(items.map((item) => item.event));
|
|
240
|
+
}
|
|
241
|
+
catch (_b) {
|
|
242
|
+
warnTransport();
|
|
243
|
+
return failed;
|
|
244
|
+
}
|
|
245
|
+
let offset = 0;
|
|
246
|
+
for (const chunk of chunks) {
|
|
247
|
+
const chunkItems = items.slice(offset, offset + chunk.length);
|
|
248
|
+
offset += chunk.length;
|
|
249
|
+
const controller = new AbortController();
|
|
250
|
+
const timeout = setTimeout(() => controller.abort(), INGEST_TIMEOUT_MS);
|
|
251
|
+
const timer = timeout;
|
|
252
|
+
(_a = timer.unref) === null || _a === void 0 ? void 0 : _a.call(timer);
|
|
253
|
+
try {
|
|
254
|
+
const response = await fetch(endpoint, {
|
|
255
|
+
method: 'POST',
|
|
256
|
+
headers: { 'Content-Type': 'text/plain' },
|
|
257
|
+
body: JSON.stringify(chunk),
|
|
258
|
+
signal: controller.signal,
|
|
259
|
+
});
|
|
260
|
+
if (!response.ok)
|
|
261
|
+
throw new Error(`Feature Flag ingest failed (${response.status})`);
|
|
262
|
+
}
|
|
263
|
+
catch (_c) {
|
|
264
|
+
for (const item of chunkItems) {
|
|
265
|
+
if (item.attempts < MAX_RETRIES) {
|
|
266
|
+
failed.push({ ...item, attempts: item.attempts + 1 });
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
warnTransport();
|
|
270
|
+
}
|
|
271
|
+
finally {
|
|
272
|
+
clearTimeout(timeout);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return failed;
|
|
276
|
+
}
|
|
277
|
+
function flushOnce() {
|
|
278
|
+
var _a;
|
|
279
|
+
if (inFlight)
|
|
280
|
+
return inFlight;
|
|
281
|
+
if (queue.length === 0)
|
|
282
|
+
return Promise.resolve();
|
|
283
|
+
const pending = queue;
|
|
284
|
+
queue = [];
|
|
285
|
+
const groups = new Map();
|
|
286
|
+
for (const item of pending) {
|
|
287
|
+
const group = (_a = groups.get(item.endpoint)) !== null && _a !== void 0 ? _a : [];
|
|
288
|
+
group.push(item);
|
|
289
|
+
groups.set(item.endpoint, group);
|
|
290
|
+
}
|
|
291
|
+
inFlight = Promise.all(Array.from(groups, ([endpoint, items]) => sendGroup(endpoint, items)))
|
|
292
|
+
.then((failedGroups) => {
|
|
293
|
+
for (const failed of failedGroups) {
|
|
294
|
+
for (const item of failed)
|
|
295
|
+
enqueue(item);
|
|
296
|
+
}
|
|
297
|
+
})
|
|
298
|
+
.finally(() => {
|
|
299
|
+
inFlight = undefined;
|
|
300
|
+
});
|
|
301
|
+
return inFlight;
|
|
302
|
+
}
|
|
303
|
+
async function flushFeatureFlagEvents() {
|
|
304
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt += 1) {
|
|
305
|
+
if (inFlight)
|
|
306
|
+
await inFlight;
|
|
307
|
+
if (queue.length === 0)
|
|
308
|
+
return;
|
|
309
|
+
await flushOnce();
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
49
313
|
async function requestFeatureFlags(options) {
|
|
50
314
|
var _a, _b;
|
|
51
315
|
const controller = new AbortController();
|
|
@@ -86,15 +350,23 @@ async function requestFeatureFlags(options) {
|
|
|
86
350
|
clearTimeout(timeout);
|
|
87
351
|
}
|
|
88
352
|
}
|
|
89
|
-
function evaluateFlags(options) {
|
|
90
|
-
|
|
353
|
+
async function evaluateFlags(options) {
|
|
354
|
+
const response = await requestFeatureFlags(options);
|
|
355
|
+
if (response && options.sendEvent !== false) {
|
|
356
|
+
trackFeatureFlagServerReads(options, response, () => Object.entries(response.flags));
|
|
357
|
+
}
|
|
358
|
+
return response;
|
|
91
359
|
}
|
|
92
360
|
async function getFeatureFlagResult(key, options) {
|
|
93
361
|
const response = await requestFeatureFlags({
|
|
94
362
|
...options,
|
|
95
363
|
selector: { names: [key] },
|
|
96
364
|
});
|
|
97
|
-
|
|
365
|
+
const result = response === null || response === void 0 ? void 0 : response.flags[key];
|
|
366
|
+
if (response && options.sendEvent !== false) {
|
|
367
|
+
trackFeatureFlagServerReads(options, response, () => [[key, result]]);
|
|
368
|
+
}
|
|
369
|
+
return result;
|
|
98
370
|
}
|
|
99
371
|
async function getFeatureFlag(key, options) {
|
|
100
372
|
var _a;
|
|
@@ -191,4 +463,4 @@ async function getProjectData(options) {
|
|
|
191
463
|
return null;
|
|
192
464
|
}
|
|
193
465
|
|
|
194
|
-
export { evaluateFlags, getFeatureFlag, getFeatureFlagPayload, getFeatureFlagResult, getProjectData, getRemoteConfig, isFeatureEnabled };
|
|
466
|
+
export { evaluateFlags, flushFeatureFlagEvents, getFeatureFlag, getFeatureFlagPayload, getFeatureFlagResult, getProjectData, getRemoteConfig, isFeatureEnabled };
|
|
@@ -27,20 +27,24 @@ interface FeatureFlagsConfig {
|
|
|
27
27
|
selector?: FeatureFlagSelector;
|
|
28
28
|
requestTimeoutMs?: number;
|
|
29
29
|
cacheTtlMs?: number;
|
|
30
|
+
ingestUrl?: string;
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
type FeatureFlagsCallback = (flags: Record<string, FeatureFlagResult>, metadata: {
|
|
33
34
|
errorsWhileComputingFlags: boolean;
|
|
34
35
|
snapshotVersion: number;
|
|
35
36
|
}) => void;
|
|
37
|
+
interface FeatureFlagReadOptions {
|
|
38
|
+
sendEvent?: boolean;
|
|
39
|
+
}
|
|
36
40
|
declare function reloadFeatureFlags(): Promise<void>;
|
|
37
41
|
declare function onFeatureFlags(callback: FeatureFlagsCallback): () => void;
|
|
38
|
-
declare function getFeatureFlagResult(key: string): FeatureFlagResult | undefined;
|
|
39
|
-
declare function getFeatureFlag(key: string): FeatureFlagValue | undefined;
|
|
40
|
-
declare function isFeatureEnabled(key: string, options?: {
|
|
42
|
+
declare function getFeatureFlagResult(key: string, options?: FeatureFlagReadOptions): FeatureFlagResult | undefined;
|
|
43
|
+
declare function getFeatureFlag(key: string, options?: FeatureFlagReadOptions): FeatureFlagValue | undefined;
|
|
44
|
+
declare function isFeatureEnabled(key: string, options?: FeatureFlagReadOptions & {
|
|
41
45
|
defaultValue?: boolean;
|
|
42
46
|
}): boolean | undefined;
|
|
43
|
-
declare function getFeatureFlagPayload(key: string): JsonValue | null | undefined;
|
|
47
|
+
declare function getFeatureFlagPayload(key: string, options?: FeatureFlagReadOptions): JsonValue | null | undefined;
|
|
44
48
|
declare const getRemoteConfig: typeof getFeatureFlagPayload;
|
|
45
49
|
|
|
46
50
|
interface FeatureFlagsInitOptions extends FeatureFlagsConfig {
|
|
@@ -72,4 +76,4 @@ declare const featureFlags: {
|
|
|
72
76
|
};
|
|
73
77
|
|
|
74
78
|
export { featureFlags, getFeatureFlag, getFeatureFlagPayload, getFeatureFlagResult, getRemoteConfig, getUserToken, initFeatureFlags, isFeatureEnabled, onFeatureFlags, reloadFeatureFlags, reset, setConsent, setUserToken };
|
|
75
|
-
export type { FeatureFlagResult, FeatureFlagSelector, FeatureFlagValue, FeatureFlagsConfig, FeatureFlagsInitOptions, FeatureFlagsResponse, JsonValue };
|
|
79
|
+
export type { FeatureFlagReadOptions, FeatureFlagResult, FeatureFlagSelector, FeatureFlagValue, FeatureFlagsConfig, FeatureFlagsInitOptions, FeatureFlagsResponse, JsonValue };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { FeatureFlagReadOptions } from '@coframe/sdk/feature-flags';
|
|
1
2
|
import React, { ReactNode } from 'react';
|
|
2
3
|
export { init, onLog, onTreatmentApplied, setConsent, setFeatureFlag, setPageHydrated, setUserToken, trackConversion } from '@coframe/sdk';
|
|
3
4
|
|
|
@@ -339,11 +340,11 @@ declare function CoframeProvider({ projectConfig, projectData, onTreatmentApplie
|
|
|
339
340
|
declare function useCoframeTreatments(): TreatmentData[] | null;
|
|
340
341
|
declare function useCoframeTreatment(experimentId: string): TreatmentData | undefined;
|
|
341
342
|
declare function useCoframeVariant(experimentId: string): string | undefined;
|
|
342
|
-
declare function useFeatureFlagResult(key: string): FeatureFlagResult | undefined;
|
|
343
|
-
declare function useFeatureFlagVariantKey(key: string): FeatureFlagValue | undefined;
|
|
343
|
+
declare function useFeatureFlagResult(key: string, options?: FeatureFlagReadOptions): FeatureFlagResult | undefined;
|
|
344
|
+
declare function useFeatureFlagVariantKey(key: string, options?: FeatureFlagReadOptions): FeatureFlagValue | undefined;
|
|
344
345
|
declare const useFeatureFlag: typeof useFeatureFlagVariantKey;
|
|
345
|
-
declare function useFeatureFlagEnabled(key: string): boolean | undefined;
|
|
346
|
-
declare function useFeatureFlagPayload(key: string): JsonValue | null | undefined;
|
|
346
|
+
declare function useFeatureFlagEnabled(key: string, options?: FeatureFlagReadOptions): boolean | undefined;
|
|
347
|
+
declare function useFeatureFlagPayload(key: string, options?: FeatureFlagReadOptions): JsonValue | null | undefined;
|
|
347
348
|
|
|
348
349
|
export { CoframeProvider, useCoframeTreatment, useCoframeTreatments, useCoframeVariant, useFeatureFlag, useFeatureFlagEnabled, useFeatureFlagPayload, useFeatureFlagResult, useFeatureFlagVariantKey };
|
|
349
350
|
export type { CoframeProviderProps };
|