@coframe/sdk 1.1.47 → 1.1.49
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 +174 -0
- package/dist/pkg/cf.cjs.js +100 -51
- package/dist/pkg/cf.esm.mjs +100 -51
- package/dist/pkg/feature-flags.cjs.js +4 -2
- package/dist/pkg/feature-flags.esm.mjs +4 -2
- package/dist/pkg/server.cjs.js +410 -40
- package/dist/pkg/server.esm.mjs +409 -41
- package/dist/pkg/types/index.d.ts +8 -1
- package/dist/pkg/types/react.d.ts +8 -1
- package/dist/pkg/types/server.d.ts +62 -4
- package/package.json +1 -1
package/dist/pkg/server.esm.mjs
CHANGED
|
@@ -1,3 +1,83 @@
|
|
|
1
|
+
const LAST_VISIT_TIME_KEY = 'coframe.lastVisitTime';
|
|
2
|
+
const VISIT_COUNT_COOKIE_KEY = 'coframe.cvc';
|
|
3
|
+
const LANE_COOKIE_KEY = 'coframe.lanes';
|
|
4
|
+
function readCookieHeader(cookieHeader, name) {
|
|
5
|
+
if (!cookieHeader)
|
|
6
|
+
return undefined;
|
|
7
|
+
let value;
|
|
8
|
+
for (const part of cookieHeader.split(';')) {
|
|
9
|
+
const entry = part.trim();
|
|
10
|
+
const separator = entry.indexOf('=');
|
|
11
|
+
if (separator >= 0 && entry.slice(0, separator) === name) {
|
|
12
|
+
value = entry.slice(separator + 1);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
function getGlobalScope() {
|
|
18
|
+
if (typeof globalThis !== 'undefined') {
|
|
19
|
+
return globalThis;
|
|
20
|
+
}
|
|
21
|
+
if (typeof window !== 'undefined') {
|
|
22
|
+
return window;
|
|
23
|
+
}
|
|
24
|
+
if (typeof self !== 'undefined') {
|
|
25
|
+
return self;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const generateUUID = () => {
|
|
29
|
+
var _a;
|
|
30
|
+
const cryptoObject = (_a = getGlobalScope()) === null || _a === void 0 ? void 0 : _a.crypto;
|
|
31
|
+
if (cryptoObject === null || cryptoObject === void 0 ? void 0 : cryptoObject.randomUUID)
|
|
32
|
+
return cryptoObject.randomUUID();
|
|
33
|
+
const randomByte = (cryptoObject === null || cryptoObject === void 0 ? void 0 : cryptoObject.getRandomValues)
|
|
34
|
+
? () => cryptoObject.getRandomValues(new Uint8Array(1))[0]
|
|
35
|
+
: () => Math.floor(Math.random() * 256);
|
|
36
|
+
return `${1e7}${ -1e3}${ -4e3}${ -8e3}${ -1e11}`.replace(/[018]/g, (c) => {
|
|
37
|
+
const randomValue = randomByte();
|
|
38
|
+
return (c ^
|
|
39
|
+
(randomValue & (15 >> (c / 4)))).toString(16);
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
const VISIT_BOUNDARY_MS = 2 * 60 * 60 * 1000;
|
|
43
|
+
function startsNewVisit(lastVisitTime, now) {
|
|
44
|
+
return !lastVisitTime || now - lastVisitTime > VISIT_BOUNDARY_MS;
|
|
45
|
+
}
|
|
46
|
+
function nextVisitCount(existing, lastVisitTime, now) {
|
|
47
|
+
return startsNewVisit(lastVisitTime, now) ? existing + 1 : existing;
|
|
48
|
+
}
|
|
49
|
+
function parseNonNegativeInt(raw) {
|
|
50
|
+
if (!raw || !/^\d+$/.test(raw))
|
|
51
|
+
return undefined;
|
|
52
|
+
const parsed = Number(raw);
|
|
53
|
+
return parsed <= 9007199254740991 ? parsed : undefined;
|
|
54
|
+
}
|
|
55
|
+
function parseVisitCount(raw) {
|
|
56
|
+
var _a;
|
|
57
|
+
return (_a = parseNonNegativeInt(raw)) !== null && _a !== void 0 ? _a : 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function isRecord(value) {
|
|
61
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function inferBrowserPlatform(userAgent) {
|
|
65
|
+
const ua = userAgent !== null && userAgent !== void 0 ? userAgent : '';
|
|
66
|
+
if (/iphone/i.test(ua))
|
|
67
|
+
return 'iPhone';
|
|
68
|
+
if (/ipad/i.test(ua))
|
|
69
|
+
return 'iPad';
|
|
70
|
+
if (/android/i.test(ua))
|
|
71
|
+
return 'Linux armv8l';
|
|
72
|
+
if (/windows/i.test(ua))
|
|
73
|
+
return 'Win32';
|
|
74
|
+
if (/macintosh|mac os/i.test(ua))
|
|
75
|
+
return 'MacIntel';
|
|
76
|
+
if (/cros|linux/i.test(ua))
|
|
77
|
+
return 'Linux x86_64';
|
|
78
|
+
return '';
|
|
79
|
+
}
|
|
80
|
+
|
|
1
81
|
const MAX_USER_CONTEXT_JSON_BYTES = 4096;
|
|
2
82
|
function userContextJsonByteLength(userContext) {
|
|
3
83
|
try {
|
|
@@ -20,29 +100,152 @@ function sanitizeUserContext(userContext) {
|
|
|
20
100
|
return userContext;
|
|
21
101
|
}
|
|
22
102
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
103
|
+
const ASSIGNMENT_FIELD_LIMITS = {
|
|
104
|
+
projectId: 200,
|
|
105
|
+
experimentId: 512,
|
|
106
|
+
userToken: 512,
|
|
107
|
+
currentUrl: 4096,
|
|
108
|
+
referrer: 4096,
|
|
109
|
+
userAgent: 2048,
|
|
110
|
+
browserPlatform: 128,
|
|
111
|
+
country: 8,
|
|
112
|
+
region: 128,
|
|
113
|
+
city: 256,
|
|
114
|
+
verifiedBotCategory: 128,
|
|
115
|
+
clientIp: 64,
|
|
116
|
+
languages: 20,
|
|
117
|
+
languageLength: 64,
|
|
118
|
+
botScoreMin: 1,
|
|
119
|
+
botScoreMax: 99,
|
|
120
|
+
};
|
|
121
|
+
const ASSIGNMENT_DEVICES = [
|
|
122
|
+
'desktop',
|
|
123
|
+
'mobile',
|
|
124
|
+
'tablet',
|
|
125
|
+
];
|
|
126
|
+
function isInvalidString(value, max) {
|
|
127
|
+
return (value !== undefined && (typeof value !== 'string' || value.length > max));
|
|
128
|
+
}
|
|
129
|
+
function isInvalidLanguages(value) {
|
|
130
|
+
return (value !== undefined &&
|
|
131
|
+
(!Array.isArray(value) ||
|
|
132
|
+
value.length > ASSIGNMENT_FIELD_LIMITS.languages ||
|
|
133
|
+
!value.every((language) => typeof language === 'string' &&
|
|
134
|
+
language.length <= ASSIGNMENT_FIELD_LIMITS.languageLength)));
|
|
135
|
+
}
|
|
136
|
+
function invalidUserContext(value) {
|
|
137
|
+
if (value === undefined)
|
|
138
|
+
return null;
|
|
139
|
+
if (!isRecord(value)) {
|
|
140
|
+
return { field: 'userContext', message: 'must be an object' };
|
|
29
141
|
}
|
|
30
|
-
if (
|
|
31
|
-
return
|
|
142
|
+
if (userContextJsonByteLength(value) > MAX_USER_CONTEXT_JSON_BYTES) {
|
|
143
|
+
return { field: 'userContext', message: 'is too large', tooLarge: true };
|
|
32
144
|
}
|
|
145
|
+
return null;
|
|
33
146
|
}
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
(randomValue & (15 >> (c / 4)))).toString(16);
|
|
44
|
-
});
|
|
147
|
+
const ASSIGNMENT_CONTEXT_HEADERS = {
|
|
148
|
+
userAgent: 'user-agent',
|
|
149
|
+
country: 'cf-ipcountry',
|
|
150
|
+
region: 'cf-region-code',
|
|
151
|
+
city: 'cf-ipcity',
|
|
152
|
+
device: 'cf-device-type',
|
|
153
|
+
botScore: 'cf-bot-score',
|
|
154
|
+
verifiedBotCategory: 'cf-verified-bot-category',
|
|
155
|
+
clientIp: 'cf-connecting-ip',
|
|
45
156
|
};
|
|
157
|
+
const HEADER_STRING_FIELDS = [
|
|
158
|
+
'userAgent',
|
|
159
|
+
'country',
|
|
160
|
+
'region',
|
|
161
|
+
'city',
|
|
162
|
+
'verifiedBotCategory',
|
|
163
|
+
'clientIp',
|
|
164
|
+
];
|
|
165
|
+
const CONTEXT_STRING_FIELDS = [
|
|
166
|
+
'referrer',
|
|
167
|
+
'userAgent',
|
|
168
|
+
'browserPlatform',
|
|
169
|
+
'country',
|
|
170
|
+
'region',
|
|
171
|
+
'city',
|
|
172
|
+
'verifiedBotCategory',
|
|
173
|
+
'clientIp',
|
|
174
|
+
];
|
|
175
|
+
function invalidAssignmentContextField(context) {
|
|
176
|
+
if (!isRecord(context)) {
|
|
177
|
+
return { field: 'context', message: 'must be an object' };
|
|
178
|
+
}
|
|
179
|
+
const { currentUrl } = context;
|
|
180
|
+
if (typeof currentUrl !== 'string' || !currentUrl) {
|
|
181
|
+
return { field: 'currentUrl', message: 'is required' };
|
|
182
|
+
}
|
|
183
|
+
if (currentUrl.length > ASSIGNMENT_FIELD_LIMITS.currentUrl) {
|
|
184
|
+
return {
|
|
185
|
+
field: 'currentUrl',
|
|
186
|
+
message: `must be at most ${ASSIGNMENT_FIELD_LIMITS.currentUrl} characters`,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
try {
|
|
190
|
+
new URL(currentUrl);
|
|
191
|
+
}
|
|
192
|
+
catch (_a) {
|
|
193
|
+
return { field: 'currentUrl', message: 'must be an absolute URL' };
|
|
194
|
+
}
|
|
195
|
+
for (const field of CONTEXT_STRING_FIELDS) {
|
|
196
|
+
const max = ASSIGNMENT_FIELD_LIMITS[field];
|
|
197
|
+
if (isInvalidString(context[field], max)) {
|
|
198
|
+
return {
|
|
199
|
+
field,
|
|
200
|
+
message: `must be a string of at most ${max} characters`,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (context.device !== undefined &&
|
|
205
|
+
!ASSIGNMENT_DEVICES.includes(context.device)) {
|
|
206
|
+
return { field: 'device', message: 'must be desktop, mobile, or tablet' };
|
|
207
|
+
}
|
|
208
|
+
const { botScore, languages, userVisitCount, userContext } = context;
|
|
209
|
+
if (botScore !== undefined &&
|
|
210
|
+
(!Number.isInteger(botScore) ||
|
|
211
|
+
botScore < ASSIGNMENT_FIELD_LIMITS.botScoreMin ||
|
|
212
|
+
botScore > ASSIGNMENT_FIELD_LIMITS.botScoreMax)) {
|
|
213
|
+
return {
|
|
214
|
+
field: 'botScore',
|
|
215
|
+
message: `must be an integer from ${ASSIGNMENT_FIELD_LIMITS.botScoreMin} to ${ASSIGNMENT_FIELD_LIMITS.botScoreMax}`,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
if (isInvalidLanguages(languages)) {
|
|
219
|
+
return {
|
|
220
|
+
field: 'languages',
|
|
221
|
+
message: `must contain at most ${ASSIGNMENT_FIELD_LIMITS.languages} strings of ${ASSIGNMENT_FIELD_LIMITS.languageLength} characters`,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
if (userVisitCount !== undefined &&
|
|
225
|
+
(!Number.isSafeInteger(userVisitCount) || userVisitCount < 0)) {
|
|
226
|
+
return {
|
|
227
|
+
field: 'userVisitCount',
|
|
228
|
+
message: 'must be a non-negative safe integer',
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
return invalidUserContext(userContext);
|
|
232
|
+
}
|
|
233
|
+
const ID_FIELDS = ['projectId', 'experimentId', 'userToken'];
|
|
234
|
+
function invalidAssignmentIdField(body) {
|
|
235
|
+
for (const field of ID_FIELDS) {
|
|
236
|
+
const value = body[field];
|
|
237
|
+
if (typeof value !== 'string' || !value) {
|
|
238
|
+
return { field, message: 'is required' };
|
|
239
|
+
}
|
|
240
|
+
if (value.length > ASSIGNMENT_FIELD_LIMITS[field]) {
|
|
241
|
+
return {
|
|
242
|
+
field,
|
|
243
|
+
message: `must be at most ${ASSIGNMENT_FIELD_LIMITS[field]} characters`,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
46
249
|
|
|
47
250
|
async function fetchProjectFromEdge(projectId, currentUrl, signal, userContext, previewVariantId, headers, referrer) {
|
|
48
251
|
const payload = { projectId, currentUrl };
|
|
@@ -310,12 +513,189 @@ async function flushFeatureFlagEvents() {
|
|
|
310
513
|
}
|
|
311
514
|
}
|
|
312
515
|
|
|
516
|
+
const DEFAULT_ASSIGNMENT_TIMEOUT_MS = 1500;
|
|
517
|
+
function normalizeRequestHeaders(headers) {
|
|
518
|
+
const normalized = new Headers();
|
|
519
|
+
if (typeof (headers === null || headers === void 0 ? void 0 : headers.forEach) === 'function') {
|
|
520
|
+
headers.forEach((value, name) => {
|
|
521
|
+
normalized.set(name, value);
|
|
522
|
+
});
|
|
523
|
+
return normalized;
|
|
524
|
+
}
|
|
525
|
+
for (const [name, value] of Object.entries(headers !== null && headers !== void 0 ? headers : {})) {
|
|
526
|
+
const joined = typeof value === 'string'
|
|
527
|
+
? value
|
|
528
|
+
: Array.isArray(value)
|
|
529
|
+
? value.join(name.toLowerCase() === 'cookie' ? '; ' : ', ')
|
|
530
|
+
: undefined;
|
|
531
|
+
if (joined === undefined)
|
|
532
|
+
continue;
|
|
533
|
+
try {
|
|
534
|
+
normalized.set(name, joined);
|
|
535
|
+
}
|
|
536
|
+
catch (_a) { }
|
|
537
|
+
}
|
|
538
|
+
return normalized;
|
|
539
|
+
}
|
|
540
|
+
function createAssignmentHeaders(incoming) {
|
|
541
|
+
const headers = new Headers({ 'Content-Type': 'application/json' });
|
|
542
|
+
const laneCookie = readCookieHeader(incoming.get('cookie'), LANE_COOKIE_KEY);
|
|
543
|
+
if (laneCookie !== undefined) {
|
|
544
|
+
headers.set('cookie', `${LANE_COOKIE_KEY}=${laneCookie}`);
|
|
545
|
+
}
|
|
546
|
+
return headers;
|
|
547
|
+
}
|
|
548
|
+
function requestLanguages(headers) {
|
|
549
|
+
var _a;
|
|
550
|
+
const languages = ((_a = headers.get('accept-language')) !== null && _a !== void 0 ? _a : '')
|
|
551
|
+
.split(',')
|
|
552
|
+
.map((entry) => { var _a; return (_a = entry.split(';')[0]) === null || _a === void 0 ? void 0 : _a.trim(); })
|
|
553
|
+
.filter((entry) => !!entry && entry.length <= ASSIGNMENT_FIELD_LIMITS.languageLength)
|
|
554
|
+
.slice(0, ASSIGNMENT_FIELD_LIMITS.languages);
|
|
555
|
+
return languages.length > 0 ? languages : undefined;
|
|
556
|
+
}
|
|
557
|
+
function clampHeader(value, maxLength) {
|
|
558
|
+
if (!value)
|
|
559
|
+
return undefined;
|
|
560
|
+
return value.length > maxLength ? value.slice(0, maxLength) : value;
|
|
561
|
+
}
|
|
562
|
+
function headerStrings(options, incoming) {
|
|
563
|
+
var _a;
|
|
564
|
+
const values = {};
|
|
565
|
+
for (const field of HEADER_STRING_FIELDS) {
|
|
566
|
+
const value = (_a = options[field]) !== null && _a !== void 0 ? _a : clampHeader(incoming.get(ASSIGNMENT_CONTEXT_HEADERS[field]), ASSIGNMENT_FIELD_LIMITS[field]);
|
|
567
|
+
if (value)
|
|
568
|
+
values[field] = value;
|
|
569
|
+
}
|
|
570
|
+
return values;
|
|
571
|
+
}
|
|
572
|
+
function requestDevice(headers) {
|
|
573
|
+
var _a;
|
|
574
|
+
const device = (_a = headers.get(ASSIGNMENT_CONTEXT_HEADERS.device)) === null || _a === void 0 ? void 0 : _a.toLowerCase();
|
|
575
|
+
return ASSIGNMENT_DEVICES.includes(device)
|
|
576
|
+
? device
|
|
577
|
+
: undefined;
|
|
578
|
+
}
|
|
579
|
+
function requestBotScore(headers) {
|
|
580
|
+
const score = parseNonNegativeInt(headers.get(ASSIGNMENT_CONTEXT_HEADERS.botScore));
|
|
581
|
+
return score !== undefined &&
|
|
582
|
+
score >= ASSIGNMENT_FIELD_LIMITS.botScoreMin &&
|
|
583
|
+
score <= ASSIGNMENT_FIELD_LIMITS.botScoreMax
|
|
584
|
+
? score
|
|
585
|
+
: undefined;
|
|
586
|
+
}
|
|
587
|
+
function edgeBaseUrl(edgeUrl) {
|
|
588
|
+
return (edgeUrl || "https://edge.cofra.me").replace(/\/+$/, '');
|
|
589
|
+
}
|
|
590
|
+
function validateAssignmentContextOptions(options) {
|
|
591
|
+
const problem = invalidAssignmentContextField(options);
|
|
592
|
+
if (problem)
|
|
593
|
+
throw new Error(`${problem.field} ${problem.message}`);
|
|
594
|
+
}
|
|
595
|
+
function effectiveVisitCount(headers) {
|
|
596
|
+
const cookie = headers.get('cookie');
|
|
597
|
+
const existing = parseVisitCount(readCookieHeader(cookie, VISIT_COUNT_COOKIE_KEY));
|
|
598
|
+
const lastVisitTime = parseVisitCount(readCookieHeader(cookie, LAST_VISIT_TIME_KEY));
|
|
599
|
+
const next = nextVisitCount(existing, lastVisitTime, Date.now());
|
|
600
|
+
return Number.isSafeInteger(next) ? next : existing;
|
|
601
|
+
}
|
|
602
|
+
function isExperimentAssignmentResult(value, experimentId) {
|
|
603
|
+
if (!isRecord(value) || !isRecord(value.projectData))
|
|
604
|
+
return false;
|
|
605
|
+
const { projectData } = value;
|
|
606
|
+
const context = projectData.assignmentContext;
|
|
607
|
+
if (!isRecord(projectData.project) ||
|
|
608
|
+
!Array.isArray(projectData.project.frames) ||
|
|
609
|
+
!isRecord(projectData.personalizationHeaders) ||
|
|
610
|
+
!isRecord(context) ||
|
|
611
|
+
!Array.isArray(context.languages) ||
|
|
612
|
+
!context.languages.every((language) => typeof language === 'string') ||
|
|
613
|
+
typeof context.browserPlatform !== 'string' ||
|
|
614
|
+
!Number.isSafeInteger(context.userVisitCount) ||
|
|
615
|
+
context.userVisitCount < 0) {
|
|
616
|
+
return false;
|
|
617
|
+
}
|
|
618
|
+
if (value.variant === null) {
|
|
619
|
+
return typeof value.reason === 'string';
|
|
620
|
+
}
|
|
621
|
+
if (!isRecord(value.variant))
|
|
622
|
+
return false;
|
|
623
|
+
return (typeof value.variant.id === 'string' &&
|
|
624
|
+
typeof value.variant.slug === 'string' &&
|
|
625
|
+
value.variant.experimentId === experimentId &&
|
|
626
|
+
typeof value.variant.iterationId === 'string');
|
|
627
|
+
}
|
|
628
|
+
function generateUserToken() {
|
|
629
|
+
return generateUUID();
|
|
630
|
+
}
|
|
631
|
+
async function getExperimentAssignment(options) {
|
|
632
|
+
var _a, _b, _c, _d, _e, _f;
|
|
633
|
+
const idProblem = invalidAssignmentIdField(options);
|
|
634
|
+
if (idProblem)
|
|
635
|
+
throw new Error(`${idProblem.field} ${idProblem.message}`);
|
|
636
|
+
if (options.timeoutMs !== undefined &&
|
|
637
|
+
!(Number.isFinite(options.timeoutMs) && options.timeoutMs > 0)) {
|
|
638
|
+
throw new Error('timeoutMs must be a positive finite number');
|
|
639
|
+
}
|
|
640
|
+
validateAssignmentContextOptions(options);
|
|
641
|
+
const currentUrl = new URL(options.currentUrl);
|
|
642
|
+
if (/cf_disable/.test(currentUrl.search))
|
|
643
|
+
return undefined;
|
|
644
|
+
const incoming = normalizeRequestHeaders(options.headers);
|
|
645
|
+
const controller = new AbortController();
|
|
646
|
+
const timeout = setTimeout(() => controller.abort(), (_a = options.timeoutMs) !== null && _a !== void 0 ? _a : DEFAULT_ASSIGNMENT_TIMEOUT_MS);
|
|
647
|
+
try {
|
|
648
|
+
const fromHeaders = headerStrings(options, incoming);
|
|
649
|
+
const languages = (_b = options.languages) !== null && _b !== void 0 ? _b : requestLanguages(incoming);
|
|
650
|
+
const browserPlatform = (_c = options.browserPlatform) !== null && _c !== void 0 ? _c : inferBrowserPlatform(fromHeaders.userAgent);
|
|
651
|
+
const device = (_d = options.device) !== null && _d !== void 0 ? _d : requestDevice(incoming);
|
|
652
|
+
const botScore = (_e = options.botScore) !== null && _e !== void 0 ? _e : requestBotScore(incoming);
|
|
653
|
+
const userVisitCount = (_f = options.userVisitCount) !== null && _f !== void 0 ? _f : effectiveVisitCount(incoming);
|
|
654
|
+
const referrer = options.referrer !== undefined
|
|
655
|
+
? options.referrer
|
|
656
|
+
: clampHeader(incoming.get('referer'), ASSIGNMENT_FIELD_LIMITS.referrer);
|
|
657
|
+
const requestBody = {
|
|
658
|
+
projectId: options.projectId,
|
|
659
|
+
experimentId: options.experimentId,
|
|
660
|
+
userToken: options.userToken,
|
|
661
|
+
context: {
|
|
662
|
+
currentUrl: options.currentUrl,
|
|
663
|
+
...(referrer !== undefined ? { referrer } : {}),
|
|
664
|
+
...(options.userContext ? { userContext: options.userContext } : {}),
|
|
665
|
+
...fromHeaders,
|
|
666
|
+
...(languages !== undefined ? { languages } : {}),
|
|
667
|
+
...(browserPlatform !== undefined ? { browserPlatform } : {}),
|
|
668
|
+
...(device ? { device } : {}),
|
|
669
|
+
...(botScore !== undefined ? { botScore } : {}),
|
|
670
|
+
userVisitCount,
|
|
671
|
+
},
|
|
672
|
+
};
|
|
673
|
+
const response = await fetch(`${edgeBaseUrl(options.edgeUrl)}/api/v1/assignment`, {
|
|
674
|
+
method: 'POST',
|
|
675
|
+
headers: createAssignmentHeaders(incoming),
|
|
676
|
+
signal: controller.signal,
|
|
677
|
+
body: JSON.stringify(requestBody),
|
|
678
|
+
});
|
|
679
|
+
if (!response.ok)
|
|
680
|
+
return undefined;
|
|
681
|
+
const result = await response.json();
|
|
682
|
+
return isExperimentAssignmentResult(result, options.experimentId)
|
|
683
|
+
? result
|
|
684
|
+
: undefined;
|
|
685
|
+
}
|
|
686
|
+
catch (_g) {
|
|
687
|
+
return undefined;
|
|
688
|
+
}
|
|
689
|
+
finally {
|
|
690
|
+
clearTimeout(timeout);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
313
693
|
async function requestFeatureFlags(options) {
|
|
314
|
-
var _a
|
|
694
|
+
var _a;
|
|
315
695
|
const controller = new AbortController();
|
|
316
696
|
const timeout = setTimeout(() => controller.abort(), (_a = options.requestTimeoutMs) !== null && _a !== void 0 ? _a : 3000);
|
|
317
697
|
try {
|
|
318
|
-
const response = await fetch(`${(
|
|
698
|
+
const response = await fetch(`${edgeBaseUrl(options.edgeUrl)}/api/v1/flags?v=1`, {
|
|
319
699
|
method: 'POST',
|
|
320
700
|
headers: {
|
|
321
701
|
'Content-Type': 'application/json',
|
|
@@ -343,7 +723,7 @@ async function requestFeatureFlags(options) {
|
|
|
343
723
|
return undefined;
|
|
344
724
|
return (await response.json());
|
|
345
725
|
}
|
|
346
|
-
catch (
|
|
726
|
+
catch (_b) {
|
|
347
727
|
return undefined;
|
|
348
728
|
}
|
|
349
729
|
finally {
|
|
@@ -387,19 +767,6 @@ async function getFeatureFlagPayload(key, options) {
|
|
|
387
767
|
return 'payload' in result ? ((_a = result.payload) !== null && _a !== void 0 ? _a : null) : null;
|
|
388
768
|
}
|
|
389
769
|
const getRemoteConfig = getFeatureFlagPayload;
|
|
390
|
-
function getRequestReferrer(headers) {
|
|
391
|
-
const get = headers.get;
|
|
392
|
-
if (typeof get === 'function') {
|
|
393
|
-
const value = get.call(headers, 'referer');
|
|
394
|
-
return typeof value === 'string' ? value : undefined;
|
|
395
|
-
}
|
|
396
|
-
for (const [key, value] of Object.entries(headers)) {
|
|
397
|
-
if (key.toLowerCase() === 'referer') {
|
|
398
|
-
return typeof value === 'string' ? value : undefined;
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
return undefined;
|
|
402
|
-
}
|
|
403
770
|
async function fetchAntiflickerFromEdge(projectId, currentUrl, referrer, userContext, antiflickerMode, headers) {
|
|
404
771
|
try {
|
|
405
772
|
const payload = { projectId, currentUrl };
|
|
@@ -427,23 +794,24 @@ async function fetchAntiflickerFromEdge(projectId, currentUrl, referrer, userCon
|
|
|
427
794
|
}
|
|
428
795
|
}
|
|
429
796
|
async function getProjectData(options) {
|
|
430
|
-
var _a;
|
|
797
|
+
var _a, _b;
|
|
431
798
|
try {
|
|
432
799
|
if (/cf_disable/.test(new URL(options.currentUrl).search)) {
|
|
433
800
|
return null;
|
|
434
801
|
}
|
|
435
802
|
}
|
|
436
|
-
catch (
|
|
803
|
+
catch (_c) {
|
|
437
804
|
}
|
|
438
805
|
const includeProject = options.includeProject !== false;
|
|
439
806
|
const includeCSS = options.includeCSS !== false;
|
|
440
|
-
const
|
|
807
|
+
const headers = normalizeRequestHeaders(options.headers);
|
|
808
|
+
const referrer = (_b = (_a = options.referrer) !== null && _a !== void 0 ? _a : headers.get('referer')) !== null && _b !== void 0 ? _b : undefined;
|
|
441
809
|
const [projectResult, antiflickerCSS] = await Promise.all([
|
|
442
810
|
includeProject
|
|
443
|
-
? fetchProjectFromEdge(options.projectId, options.currentUrl, undefined, options.userContext, undefined,
|
|
811
|
+
? fetchProjectFromEdge(options.projectId, options.currentUrl, undefined, options.userContext, undefined, headers, referrer)
|
|
444
812
|
: Promise.resolve(null),
|
|
445
813
|
includeCSS
|
|
446
|
-
? fetchAntiflickerFromEdge(options.projectId, options.currentUrl, referrer, options.userContext, options.antiflickerMode,
|
|
814
|
+
? fetchAntiflickerFromEdge(options.projectId, options.currentUrl, referrer, options.userContext, options.antiflickerMode, headers)
|
|
447
815
|
: Promise.resolve(null),
|
|
448
816
|
]);
|
|
449
817
|
const config = {};
|
|
@@ -463,4 +831,4 @@ async function getProjectData(options) {
|
|
|
463
831
|
return null;
|
|
464
832
|
}
|
|
465
833
|
|
|
466
|
-
export { evaluateFlags, flushFeatureFlagEvents, getFeatureFlag, getFeatureFlagPayload, getFeatureFlagResult, getProjectData, getRemoteConfig, isFeatureEnabled };
|
|
834
|
+
export { evaluateFlags, flushFeatureFlagEvents, generateUserToken, getExperimentAssignment, getFeatureFlag, getFeatureFlagPayload, getFeatureFlagResult, getProjectData, getRemoteConfig, isFeatureEnabled };
|
|
@@ -171,7 +171,7 @@ interface Project {
|
|
|
171
171
|
}
|
|
172
172
|
interface Frame {
|
|
173
173
|
activation_condition: ActivationCondition;
|
|
174
|
-
experiments
|
|
174
|
+
experiments?: Experiment[];
|
|
175
175
|
elements: CoframeElements;
|
|
176
176
|
}
|
|
177
177
|
interface Experiment {
|
|
@@ -298,9 +298,16 @@ type PersonalizationHeaders = {
|
|
|
298
298
|
'x-bot-reason': BotReason;
|
|
299
299
|
};
|
|
300
300
|
|
|
301
|
+
interface AssignmentEvaluationContext {
|
|
302
|
+
languages: string[];
|
|
303
|
+
browserPlatform: string;
|
|
304
|
+
userVisitCount: number;
|
|
305
|
+
}
|
|
306
|
+
|
|
301
307
|
interface ProjectData {
|
|
302
308
|
project: Project;
|
|
303
309
|
personalizationHeaders?: Partial<PersonalizationHeaders>;
|
|
310
|
+
assignmentContext?: AssignmentEvaluationContext;
|
|
304
311
|
}
|
|
305
312
|
interface CoframeInitOptions {
|
|
306
313
|
config?: CoframeConfig;
|
|
@@ -191,7 +191,7 @@ interface Project {
|
|
|
191
191
|
}
|
|
192
192
|
interface Frame {
|
|
193
193
|
activation_condition: ActivationCondition;
|
|
194
|
-
experiments
|
|
194
|
+
experiments?: Experiment[];
|
|
195
195
|
elements: CoframeElements;
|
|
196
196
|
}
|
|
197
197
|
interface Experiment {
|
|
@@ -312,9 +312,16 @@ type PersonalizationHeaders = {
|
|
|
312
312
|
'x-bot-reason': BotReason;
|
|
313
313
|
};
|
|
314
314
|
|
|
315
|
+
interface AssignmentEvaluationContext {
|
|
316
|
+
languages: string[];
|
|
317
|
+
browserPlatform: string;
|
|
318
|
+
userVisitCount: number;
|
|
319
|
+
}
|
|
320
|
+
|
|
315
321
|
interface ProjectData {
|
|
316
322
|
project: Project;
|
|
317
323
|
personalizationHeaders?: Partial<PersonalizationHeaders>;
|
|
324
|
+
assignmentContext?: AssignmentEvaluationContext;
|
|
318
325
|
}
|
|
319
326
|
type LogPayload = {
|
|
320
327
|
kind: 'warn' | 'error' | 'exception';
|
|
@@ -137,7 +137,7 @@ interface Project {
|
|
|
137
137
|
}
|
|
138
138
|
interface Frame {
|
|
139
139
|
activation_condition: ActivationCondition;
|
|
140
|
-
experiments
|
|
140
|
+
experiments?: Experiment[];
|
|
141
141
|
elements: CoframeElements;
|
|
142
142
|
}
|
|
143
143
|
interface Experiment {
|
|
@@ -234,6 +234,50 @@ type PersonalizationHeaders = {
|
|
|
234
234
|
'x-bot-reason': BotReason;
|
|
235
235
|
};
|
|
236
236
|
|
|
237
|
+
type AssignmentDevice = 'desktop' | 'mobile' | 'tablet';
|
|
238
|
+
interface AssignmentContext {
|
|
239
|
+
currentUrl: string;
|
|
240
|
+
referrer?: string;
|
|
241
|
+
userContext?: Record<string, unknown>;
|
|
242
|
+
userAgent?: string;
|
|
243
|
+
languages?: string[];
|
|
244
|
+
browserPlatform?: string;
|
|
245
|
+
country?: string;
|
|
246
|
+
region?: string;
|
|
247
|
+
city?: string;
|
|
248
|
+
device?: AssignmentDevice;
|
|
249
|
+
botScore?: number;
|
|
250
|
+
verifiedBotCategory?: string;
|
|
251
|
+
clientIp?: string;
|
|
252
|
+
userVisitCount?: number;
|
|
253
|
+
}
|
|
254
|
+
interface AssignmentEvaluationContext {
|
|
255
|
+
languages: string[];
|
|
256
|
+
browserPlatform: string;
|
|
257
|
+
userVisitCount: number;
|
|
258
|
+
}
|
|
259
|
+
interface AssignmentProjectData {
|
|
260
|
+
project: Project;
|
|
261
|
+
personalizationHeaders: PersonalizationHeaders;
|
|
262
|
+
assignmentContext: AssignmentEvaluationContext;
|
|
263
|
+
}
|
|
264
|
+
interface AssignedVariant$1 {
|
|
265
|
+
id: string;
|
|
266
|
+
slug: string;
|
|
267
|
+
experimentId: string;
|
|
268
|
+
iterationId: string;
|
|
269
|
+
}
|
|
270
|
+
type AssignmentMissReason$1 = 'experiment_not_found' | 'not_assigned' | 'unsupported_experiment_type' | 'preview_active';
|
|
271
|
+
type AssignmentOutcome<Reason extends string = AssignmentMissReason$1> = {
|
|
272
|
+
variant: AssignedVariant$1;
|
|
273
|
+
} | {
|
|
274
|
+
variant: null;
|
|
275
|
+
reason: Reason;
|
|
276
|
+
};
|
|
277
|
+
type AssignmentResponse<Reason extends string = AssignmentMissReason$1> = AssignmentOutcome<Reason> & {
|
|
278
|
+
projectData: AssignmentProjectData;
|
|
279
|
+
};
|
|
280
|
+
|
|
237
281
|
type JsonValue = string | number | boolean | null | JsonValue[] | {
|
|
238
282
|
[key: string]: JsonValue;
|
|
239
283
|
};
|
|
@@ -262,12 +306,24 @@ interface FeatureFlagSelector {
|
|
|
262
306
|
|
|
263
307
|
declare function flushFeatureFlagEvents(): Promise<void>;
|
|
264
308
|
|
|
309
|
+
type RequestHeaders = Headers | Record<string, string | string[] | undefined>;
|
|
310
|
+
interface GetExperimentAssignmentOptions extends AssignmentContext {
|
|
311
|
+
projectId: string;
|
|
312
|
+
experimentId: string;
|
|
313
|
+
userToken: string;
|
|
314
|
+
headers: RequestHeaders;
|
|
315
|
+
timeoutMs?: number;
|
|
316
|
+
edgeUrl?: string;
|
|
317
|
+
}
|
|
318
|
+
type AssignedVariant = AssignedVariant$1;
|
|
319
|
+
type AssignmentMissReason = AssignmentMissReason$1 | (string & {});
|
|
320
|
+
type ExperimentAssignmentResult = AssignmentResponse<AssignmentMissReason>;
|
|
265
321
|
interface GetProjectDataOptions {
|
|
266
322
|
projectId: string;
|
|
267
323
|
currentUrl: string;
|
|
268
324
|
referrer?: string;
|
|
269
325
|
userContext?: Record<string, unknown>;
|
|
270
|
-
headers:
|
|
326
|
+
headers: RequestHeaders;
|
|
271
327
|
antiflickerMode?: AntiflickerMode;
|
|
272
328
|
includeProject?: boolean;
|
|
273
329
|
includeCSS?: boolean;
|
|
@@ -277,6 +333,8 @@ interface CoframeProjectPayload {
|
|
|
277
333
|
personalizationHeaders?: PersonalizationHeaders;
|
|
278
334
|
antiflickerCSS?: string;
|
|
279
335
|
}
|
|
336
|
+
declare function generateUserToken(): string;
|
|
337
|
+
declare function getExperimentAssignment(options: GetExperimentAssignmentOptions): Promise<ExperimentAssignmentResult | undefined>;
|
|
280
338
|
interface EvaluateFlagsOptions {
|
|
281
339
|
apiKey: string;
|
|
282
340
|
projectId: string;
|
|
@@ -306,5 +364,5 @@ declare const getRemoteConfig: typeof getFeatureFlagPayload;
|
|
|
306
364
|
|
|
307
365
|
declare function getProjectData(options: GetProjectDataOptions): Promise<CoframeProjectPayload | null>;
|
|
308
366
|
|
|
309
|
-
export { evaluateFlags, flushFeatureFlagEvents, getFeatureFlag, getFeatureFlagPayload, getFeatureFlagResult, getProjectData, getRemoteConfig, isFeatureEnabled };
|
|
310
|
-
export type { CoframeProjectPayload, EvaluateFlagsOptions, FeatureFlagServerReadOptions, GetProjectDataOptions };
|
|
367
|
+
export { evaluateFlags, flushFeatureFlagEvents, generateUserToken, getExperimentAssignment, getFeatureFlag, getFeatureFlagPayload, getFeatureFlagResult, getProjectData, getRemoteConfig, isFeatureEnabled };
|
|
368
|
+
export type { AssignedVariant, AssignmentMissReason, CoframeProjectPayload, EvaluateFlagsOptions, ExperimentAssignmentResult, FeatureFlagServerReadOptions, GetExperimentAssignmentOptions, GetProjectDataOptions, RequestHeaders };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coframe/sdk",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.49",
|
|
4
4
|
"description": "Coframe optimizes your website using generative AI to deliver maximum impact to your audience and allow your site to improve itself as you sleep. Learn more at coframe.com.",
|
|
5
5
|
"main": "dist/pkg/cf.cjs.js",
|
|
6
6
|
"module": "dist/pkg/cf.esm.mjs",
|