@coframe/sdk 1.1.23 → 1.1.25
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 +157 -0
- package/dist/pkg/cf.cjs.js +52 -3
- package/dist/pkg/cf.esm.mjs +52 -3
- package/dist/pkg/feature-flags.cjs.js +623 -0
- package/dist/pkg/feature-flags.esm.mjs +609 -0
- package/dist/pkg/react.cjs.js +36 -1
- package/dist/pkg/react.esm.mjs +32 -2
- package/dist/pkg/server.cjs.js +75 -0
- package/dist/pkg/server.esm.mjs +70 -1
- package/dist/pkg/types/feature-flags.d.ts +75 -0
- package/dist/pkg/types/react.d.ts +22 -1
- package/dist/pkg/types/server.d.ts +51 -2
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -136,6 +136,148 @@ const projectData = serverData?.project
|
|
|
136
136
|
|
|
137
137
|
Pass `projectData` into `<CoframeProvider>` to avoid the extra client-side project fetch and personalize on first paint.
|
|
138
138
|
|
|
139
|
+
## Feature flags and remote config
|
|
140
|
+
|
|
141
|
+
Browser Feature Flags use the existing public `projectId` and receive only
|
|
142
|
+
flags marked **Public**. `selector` is optional; use `names` for
|
|
143
|
+
individual flag keys and `groups` to evaluate every flag assigned to a page or
|
|
144
|
+
business area.
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
import { setUserToken } from '@coframe/sdk'
|
|
148
|
+
import {
|
|
149
|
+
featureFlags,
|
|
150
|
+
getFeatureFlag,
|
|
151
|
+
getFeatureFlagPayload,
|
|
152
|
+
isFeatureEnabled,
|
|
153
|
+
onFeatureFlags,
|
|
154
|
+
} from '@coframe/sdk/feature-flags'
|
|
155
|
+
|
|
156
|
+
setUserToken('customer-123')
|
|
157
|
+
|
|
158
|
+
await featureFlags.init({
|
|
159
|
+
projectId: 'your-project-id',
|
|
160
|
+
selector: {
|
|
161
|
+
names: ['checkout-redesign'],
|
|
162
|
+
groups: ['homepage_campaigns'],
|
|
163
|
+
},
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
onFeatureFlags(() => {
|
|
167
|
+
if (isFeatureEnabled('checkout-redesign')) {
|
|
168
|
+
showNewCheckout()
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const content = getFeatureFlagPayload('homepage-content')
|
|
172
|
+
const checkoutVariant = getFeatureFlag('checkout-layout')
|
|
173
|
+
})
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Feature Flags are a separate browser entry point and do not increase or initialize
|
|
177
|
+
the main SDK bundle. When both clients are used, Feature Flags reuse the latest
|
|
178
|
+
`setUserToken()` command from Coframe's shared command queue and follow later
|
|
179
|
+
identity changes automatically.
|
|
180
|
+
|
|
181
|
+
Loading does not delay Coframe page modification. Synchronous getters return
|
|
182
|
+
`undefined` until a cached or fresh evaluation is available. The browser client
|
|
183
|
+
always keeps the latest successful response in memory. Before consent, no
|
|
184
|
+
Feature Flag result or evaluation context is persisted. After consent, the
|
|
185
|
+
client also caches the response in `localStorage` for five minutes and keys it
|
|
186
|
+
with an opaque SHA-256 fingerprint derived from the user token and evaluation
|
|
187
|
+
context. It can fall back to that persistent cache if Edge is temporarily
|
|
188
|
+
unavailable. Call `reloadFeatureFlags()` to request fresh evaluations
|
|
189
|
+
explicitly.
|
|
190
|
+
|
|
191
|
+
Payloads support any JSON value up to 32 KB. A payload is returned only when its
|
|
192
|
+
flag is enabled. Remote Config uses the same transport and payload API and
|
|
193
|
+
returns the same payload to all users while active:
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
import { getRemoteConfig } from '@coframe/sdk/feature-flags'
|
|
197
|
+
|
|
198
|
+
const navigation = getRemoteConfig('navigation-config')
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Multivariate flags return a stable variant key from `getFeatureFlag()`. Their
|
|
202
|
+
weights divide users into deterministic buckets, and each variant can carry an
|
|
203
|
+
independent optional payload. `isFeatureEnabled()` remains a boolean
|
|
204
|
+
convenience and returns `true` whenever a variant is assigned:
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
import {
|
|
208
|
+
getFeatureFlag,
|
|
209
|
+
getFeatureFlagPayload,
|
|
210
|
+
isFeatureEnabled,
|
|
211
|
+
} from '@coframe/sdk/feature-flags'
|
|
212
|
+
|
|
213
|
+
const variant = getFeatureFlag('checkout-layout')
|
|
214
|
+
const config = getFeatureFlagPayload('checkout-layout')
|
|
215
|
+
const enabled = isFeatureEnabled('checkout-layout')
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
React apps can read evaluated values from the existing provider:
|
|
219
|
+
|
|
220
|
+
```tsx
|
|
221
|
+
import {
|
|
222
|
+
useFeatureFlag,
|
|
223
|
+
useFeatureFlagEnabled,
|
|
224
|
+
useFeatureFlagPayload,
|
|
225
|
+
useFeatureFlagVariantKey,
|
|
226
|
+
} from '@coframe/sdk/react'
|
|
227
|
+
|
|
228
|
+
function Checkout() {
|
|
229
|
+
const enabled = useFeatureFlag('checkout-redesign')
|
|
230
|
+
const payload = useFeatureFlagPayload('checkout-redesign')
|
|
231
|
+
|
|
232
|
+
if (enabled === undefined) return <CheckoutSkeleton />
|
|
233
|
+
return enabled ? <NewCheckout config={payload} /> : <OldCheckout />
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function CheckoutLayout() {
|
|
237
|
+
const variant = useFeatureFlagVariantKey('checkout-layout')
|
|
238
|
+
const enabled = useFeatureFlagEnabled('checkout-layout')
|
|
239
|
+
const payload = useFeatureFlagPayload('checkout-layout')
|
|
240
|
+
return <Checkout variant={variant} enabled={enabled} config={payload} />
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
For server and edge runtimes, use a secret Feature Flags Server API key. It is
|
|
245
|
+
project-scoped, can read **Private** flags, and must never be included in a
|
|
246
|
+
browser bundle or a `NEXT_PUBLIC_*` environment variable. Each single-key
|
|
247
|
+
helper sends a key-filtered request and fails closed unless a `defaultValue` is
|
|
248
|
+
supplied. Browser and server helpers both call `/api/v1/flags`; server helpers
|
|
249
|
+
include the API key as a Bearer credential so the response may include both
|
|
250
|
+
**Public** and **Private** flags:
|
|
251
|
+
|
|
252
|
+
```ts
|
|
253
|
+
import {
|
|
254
|
+
evaluateFlags,
|
|
255
|
+
getFeatureFlag,
|
|
256
|
+
getRemoteConfig,
|
|
257
|
+
isFeatureEnabled,
|
|
258
|
+
} from '@coframe/sdk/server'
|
|
259
|
+
|
|
260
|
+
const options = {
|
|
261
|
+
projectId: 'your-project-id',
|
|
262
|
+
apiKey: process.env.COFRAME_FEATURE_FLAGS_SERVER_KEY!,
|
|
263
|
+
userToken: user.id,
|
|
264
|
+
currentUrl: request.url,
|
|
265
|
+
userContext: { plan: user.plan },
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const checkoutFlags = await evaluateFlags({
|
|
269
|
+
...options,
|
|
270
|
+
selector: { groups: ['cart_checkout'] },
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
const enabled = await isFeatureEnabled('checkout-redesign', {
|
|
274
|
+
...options,
|
|
275
|
+
defaultValue: false,
|
|
276
|
+
})
|
|
277
|
+
const variant = await getFeatureFlag('checkout-layout', options)
|
|
278
|
+
const content = await getRemoteConfig('homepage-content', options)
|
|
279
|
+
```
|
|
280
|
+
|
|
139
281
|
## Common React usage
|
|
140
282
|
|
|
141
283
|
### Read variants in components
|
|
@@ -259,9 +401,21 @@ Modern resolution modes handle package subpath exports more reliably.
|
|
|
259
401
|
- `onTreatmentApplied(...)`
|
|
260
402
|
- `onLog(...)`
|
|
261
403
|
|
|
404
|
+
### `@coframe/sdk/feature-flags`
|
|
405
|
+
|
|
406
|
+
- `featureFlags.init(options)`
|
|
407
|
+
- `featureFlags.setUserToken(userToken)` / `featureFlags.reset()`
|
|
408
|
+
- `featureFlags.getUserToken()`
|
|
409
|
+
- `getFeatureFlag(key)` / `isFeatureEnabled(key)`
|
|
410
|
+
- `getFeatureFlagPayload(key)` / `getRemoteConfig(key)`
|
|
411
|
+
- `onFeatureFlags(callback)` / `reloadFeatureFlags()`
|
|
412
|
+
|
|
262
413
|
### `@coframe/sdk/server`
|
|
263
414
|
|
|
264
415
|
- `getProjectData(options)`
|
|
416
|
+
- `evaluateFlags(options)`
|
|
417
|
+
- `getFeatureFlag(key, options)` / `isFeatureEnabled(key, options)`
|
|
418
|
+
- `getFeatureFlagPayload(key, options)` / `getRemoteConfig(key, options)`
|
|
265
419
|
|
|
266
420
|
Returns a payload that may include:
|
|
267
421
|
|
|
@@ -275,3 +429,6 @@ Returns a payload that may include:
|
|
|
275
429
|
- `useCoframeTreatments()`
|
|
276
430
|
- `useCoframeTreatment(experimentId)`
|
|
277
431
|
- `useCoframeVariant(experimentId)`
|
|
432
|
+
- `useFeatureFlag(key)` / `useFeatureFlagVariantKey(key)`
|
|
433
|
+
- `useFeatureFlagEnabled(key)`
|
|
434
|
+
- `useFeatureFlagResult(key)` / `useFeatureFlagPayload(key)`
|
package/dist/pkg/cf.cjs.js
CHANGED
|
@@ -2277,7 +2277,7 @@ function isElementVisible(el) {
|
|
|
2277
2277
|
return rect.top >= 0 && rect.bottom <= window.innerHeight;
|
|
2278
2278
|
}
|
|
2279
2279
|
const VALUE_UNKNOWN_YET = 'n/a';
|
|
2280
|
-
function checkActivationCondition(condition, personalizationHeaders, currentUrl, languages, browserPlatform, userToken, userVisitCount, runJsFunction, logger) {
|
|
2280
|
+
function checkActivationCondition(condition, personalizationHeaders, currentUrl, languages, browserPlatform, userToken, userVisitCount, runJsFunction, logger, options) {
|
|
2281
2281
|
if (condition.and) {
|
|
2282
2282
|
const results = condition.and.map((c) => checkActivationCondition(c, personalizationHeaders, currentUrl, languages, browserPlatform, userToken, userVisitCount, runJsFunction, logger));
|
|
2283
2283
|
const hasUnknown = results.some((r) => r === VALUE_UNKNOWN_YET);
|
|
@@ -2835,7 +2835,7 @@ function keepRolledOutExperiments(frames) {
|
|
|
2835
2835
|
function buildSessionContext(isInitialApply) {
|
|
2836
2836
|
const context = {};
|
|
2837
2837
|
const viewport = getScreenSize();
|
|
2838
|
-
if (isReportedSize(viewport.width) && isReportedSize(viewport.height)) {
|
|
2838
|
+
if (isReportedSize$1(viewport.width) && isReportedSize$1(viewport.height)) {
|
|
2839
2839
|
context.viewport_width = viewport.width;
|
|
2840
2840
|
context.viewport_height = viewport.height;
|
|
2841
2841
|
}
|
|
@@ -2858,7 +2858,7 @@ function buildSessionContext(isInitialApply) {
|
|
|
2858
2858
|
}
|
|
2859
2859
|
return context;
|
|
2860
2860
|
}
|
|
2861
|
-
function isReportedSize(size) {
|
|
2861
|
+
function isReportedSize$1(size) {
|
|
2862
2862
|
return (typeof size === 'number' && size > 0 && size !== Number.POSITIVE_INFINITY);
|
|
2863
2863
|
}
|
|
2864
2864
|
function getNavigationType(isInitialApply) {
|
|
@@ -3779,6 +3779,39 @@ async function modifyPage(Coframe, configResult, projectResult, analytics) {
|
|
|
3779
3779
|
}
|
|
3780
3780
|
|
|
3781
3781
|
const TIMEOUT_MAX_MS = 5000;
|
|
3782
|
+
const MAX_NOT_RENDERED_REASON_CHARS = 200;
|
|
3783
|
+
function isReportedSize(size) {
|
|
3784
|
+
return (typeof size === 'number' && size > 0 && size !== Number.POSITIVE_INFINITY);
|
|
3785
|
+
}
|
|
3786
|
+
function describeEnvironment(initTime) {
|
|
3787
|
+
const metadata = {};
|
|
3788
|
+
if (typeof window !== 'undefined') {
|
|
3789
|
+
if (isReportedSize(window.innerWidth)) {
|
|
3790
|
+
metadata.viewport_width = window.innerWidth;
|
|
3791
|
+
}
|
|
3792
|
+
if (isReportedSize(window.innerHeight)) {
|
|
3793
|
+
metadata.viewport_height = window.innerHeight;
|
|
3794
|
+
}
|
|
3795
|
+
const screen = window.screen;
|
|
3796
|
+
if (screen && isReportedSize(screen.width)) {
|
|
3797
|
+
metadata.screen_width = screen.width;
|
|
3798
|
+
}
|
|
3799
|
+
if (screen && isReportedSize(screen.height)) {
|
|
3800
|
+
metadata.screen_height = screen.height;
|
|
3801
|
+
}
|
|
3802
|
+
if (isReportedSize(window.devicePixelRatio)) {
|
|
3803
|
+
metadata.device_pixel_ratio = window.devicePixelRatio;
|
|
3804
|
+
}
|
|
3805
|
+
if (typeof navigator.userAgent === 'string') {
|
|
3806
|
+
metadata.user_agent = navigator.userAgent;
|
|
3807
|
+
}
|
|
3808
|
+
if (typeof document.readyState === 'string') {
|
|
3809
|
+
metadata.document_ready_state = document.readyState;
|
|
3810
|
+
}
|
|
3811
|
+
}
|
|
3812
|
+
metadata.ms_since_init = Math.max(0, Date.now() - initTime);
|
|
3813
|
+
return metadata;
|
|
3814
|
+
}
|
|
3782
3815
|
function getFeatureFlagValue(cmd) {
|
|
3783
3816
|
if (cmd.featureFlagSet !== undefined)
|
|
3784
3817
|
return cmd.featureFlagSet;
|
|
@@ -3832,6 +3865,15 @@ async function resolveConfig(Coframe, revealPage) {
|
|
|
3832
3865
|
logger.warn('Failed to wait for variant rendered command', 'missing_variant_rendered');
|
|
3833
3866
|
}
|
|
3834
3867
|
};
|
|
3868
|
+
const warnVariantNotRendered = (reason) => {
|
|
3869
|
+
if (!applyVariantChangesStarted)
|
|
3870
|
+
return;
|
|
3871
|
+
const metadata = describeEnvironment(executionStartTime.getTime());
|
|
3872
|
+
if (typeof reason === 'string') {
|
|
3873
|
+
metadata.reason = reason.slice(0, MAX_NOT_RENDERED_REASON_CHARS);
|
|
3874
|
+
}
|
|
3875
|
+
logger.warn('Variant not rendered', 'variant_not_rendered', metadata);
|
|
3876
|
+
};
|
|
3835
3877
|
waitForCmd((cmd) => cmd.variantRendered === true || cmd.emit === 'variantRendered', Coframe, timeoutMs)
|
|
3836
3878
|
.then((cmd) => {
|
|
3837
3879
|
if (!cmd) {
|
|
@@ -3841,6 +3883,13 @@ async function resolveConfig(Coframe, revealPage) {
|
|
|
3841
3883
|
.finally(() => {
|
|
3842
3884
|
revealPage();
|
|
3843
3885
|
});
|
|
3886
|
+
const loggedReasons = [];
|
|
3887
|
+
listenForCmd((cmd) => cmd.emit === 'variantNotRendered', (cmd) => {
|
|
3888
|
+
if (loggedReasons.indexOf(cmd.reason) !== -1)
|
|
3889
|
+
return;
|
|
3890
|
+
loggedReasons.push(cmd.reason);
|
|
3891
|
+
warnVariantNotRendered(cmd.reason);
|
|
3892
|
+
});
|
|
3844
3893
|
if (!projectId) {
|
|
3845
3894
|
logger.error('No project ID', 'no_project_id', {
|
|
3846
3895
|
description: 'No Coframe Page ID',
|
package/dist/pkg/cf.esm.mjs
CHANGED
|
@@ -2275,7 +2275,7 @@ function isElementVisible(el) {
|
|
|
2275
2275
|
return rect.top >= 0 && rect.bottom <= window.innerHeight;
|
|
2276
2276
|
}
|
|
2277
2277
|
const VALUE_UNKNOWN_YET = 'n/a';
|
|
2278
|
-
function checkActivationCondition(condition, personalizationHeaders, currentUrl, languages, browserPlatform, userToken, userVisitCount, runJsFunction, logger) {
|
|
2278
|
+
function checkActivationCondition(condition, personalizationHeaders, currentUrl, languages, browserPlatform, userToken, userVisitCount, runJsFunction, logger, options) {
|
|
2279
2279
|
if (condition.and) {
|
|
2280
2280
|
const results = condition.and.map((c) => checkActivationCondition(c, personalizationHeaders, currentUrl, languages, browserPlatform, userToken, userVisitCount, runJsFunction, logger));
|
|
2281
2281
|
const hasUnknown = results.some((r) => r === VALUE_UNKNOWN_YET);
|
|
@@ -2833,7 +2833,7 @@ function keepRolledOutExperiments(frames) {
|
|
|
2833
2833
|
function buildSessionContext(isInitialApply) {
|
|
2834
2834
|
const context = {};
|
|
2835
2835
|
const viewport = getScreenSize();
|
|
2836
|
-
if (isReportedSize(viewport.width) && isReportedSize(viewport.height)) {
|
|
2836
|
+
if (isReportedSize$1(viewport.width) && isReportedSize$1(viewport.height)) {
|
|
2837
2837
|
context.viewport_width = viewport.width;
|
|
2838
2838
|
context.viewport_height = viewport.height;
|
|
2839
2839
|
}
|
|
@@ -2856,7 +2856,7 @@ function buildSessionContext(isInitialApply) {
|
|
|
2856
2856
|
}
|
|
2857
2857
|
return context;
|
|
2858
2858
|
}
|
|
2859
|
-
function isReportedSize(size) {
|
|
2859
|
+
function isReportedSize$1(size) {
|
|
2860
2860
|
return (typeof size === 'number' && size > 0 && size !== Number.POSITIVE_INFINITY);
|
|
2861
2861
|
}
|
|
2862
2862
|
function getNavigationType(isInitialApply) {
|
|
@@ -3777,6 +3777,39 @@ async function modifyPage(Coframe, configResult, projectResult, analytics) {
|
|
|
3777
3777
|
}
|
|
3778
3778
|
|
|
3779
3779
|
const TIMEOUT_MAX_MS = 5000;
|
|
3780
|
+
const MAX_NOT_RENDERED_REASON_CHARS = 200;
|
|
3781
|
+
function isReportedSize(size) {
|
|
3782
|
+
return (typeof size === 'number' && size > 0 && size !== Number.POSITIVE_INFINITY);
|
|
3783
|
+
}
|
|
3784
|
+
function describeEnvironment(initTime) {
|
|
3785
|
+
const metadata = {};
|
|
3786
|
+
if (typeof window !== 'undefined') {
|
|
3787
|
+
if (isReportedSize(window.innerWidth)) {
|
|
3788
|
+
metadata.viewport_width = window.innerWidth;
|
|
3789
|
+
}
|
|
3790
|
+
if (isReportedSize(window.innerHeight)) {
|
|
3791
|
+
metadata.viewport_height = window.innerHeight;
|
|
3792
|
+
}
|
|
3793
|
+
const screen = window.screen;
|
|
3794
|
+
if (screen && isReportedSize(screen.width)) {
|
|
3795
|
+
metadata.screen_width = screen.width;
|
|
3796
|
+
}
|
|
3797
|
+
if (screen && isReportedSize(screen.height)) {
|
|
3798
|
+
metadata.screen_height = screen.height;
|
|
3799
|
+
}
|
|
3800
|
+
if (isReportedSize(window.devicePixelRatio)) {
|
|
3801
|
+
metadata.device_pixel_ratio = window.devicePixelRatio;
|
|
3802
|
+
}
|
|
3803
|
+
if (typeof navigator.userAgent === 'string') {
|
|
3804
|
+
metadata.user_agent = navigator.userAgent;
|
|
3805
|
+
}
|
|
3806
|
+
if (typeof document.readyState === 'string') {
|
|
3807
|
+
metadata.document_ready_state = document.readyState;
|
|
3808
|
+
}
|
|
3809
|
+
}
|
|
3810
|
+
metadata.ms_since_init = Math.max(0, Date.now() - initTime);
|
|
3811
|
+
return metadata;
|
|
3812
|
+
}
|
|
3780
3813
|
function getFeatureFlagValue(cmd) {
|
|
3781
3814
|
if (cmd.featureFlagSet !== undefined)
|
|
3782
3815
|
return cmd.featureFlagSet;
|
|
@@ -3830,6 +3863,15 @@ async function resolveConfig(Coframe, revealPage) {
|
|
|
3830
3863
|
logger.warn('Failed to wait for variant rendered command', 'missing_variant_rendered');
|
|
3831
3864
|
}
|
|
3832
3865
|
};
|
|
3866
|
+
const warnVariantNotRendered = (reason) => {
|
|
3867
|
+
if (!applyVariantChangesStarted)
|
|
3868
|
+
return;
|
|
3869
|
+
const metadata = describeEnvironment(executionStartTime.getTime());
|
|
3870
|
+
if (typeof reason === 'string') {
|
|
3871
|
+
metadata.reason = reason.slice(0, MAX_NOT_RENDERED_REASON_CHARS);
|
|
3872
|
+
}
|
|
3873
|
+
logger.warn('Variant not rendered', 'variant_not_rendered', metadata);
|
|
3874
|
+
};
|
|
3833
3875
|
waitForCmd((cmd) => cmd.variantRendered === true || cmd.emit === 'variantRendered', Coframe, timeoutMs)
|
|
3834
3876
|
.then((cmd) => {
|
|
3835
3877
|
if (!cmd) {
|
|
@@ -3839,6 +3881,13 @@ async function resolveConfig(Coframe, revealPage) {
|
|
|
3839
3881
|
.finally(() => {
|
|
3840
3882
|
revealPage();
|
|
3841
3883
|
});
|
|
3884
|
+
const loggedReasons = [];
|
|
3885
|
+
listenForCmd((cmd) => cmd.emit === 'variantNotRendered', (cmd) => {
|
|
3886
|
+
if (loggedReasons.indexOf(cmd.reason) !== -1)
|
|
3887
|
+
return;
|
|
3888
|
+
loggedReasons.push(cmd.reason);
|
|
3889
|
+
warnVariantNotRendered(cmd.reason);
|
|
3890
|
+
});
|
|
3842
3891
|
if (!projectId) {
|
|
3843
3892
|
logger.error('No project ID', 'no_project_id', {
|
|
3844
3893
|
description: 'No Coframe Page ID',
|