Feature flags
A feature flag lets you run an A/B test and roll a change out in stages. Control a feature flag from the dashboard, or define it in SDK code when you need your own decision rules.
Where feature flags come from
You control most feature flags from the dashboard. The dynamic experiences plugin reads the options of your project at runtime and registers every feature flag for you. You declare no feature flag in code.
For a feature flag that you control from the dashboard, the SDK makes the definition:
| Field | Value |
|---|---|
id | The dashboard key of the feature flag |
key | v{version}, for example v3 |
name | The dashboard name of the feature flag |
enabled | The dashboard Enabled toggle |
weight | The dashboard Weight, 0 to 100 |
storage | local, or session in preview mode |
decide | Made by the SDK. See the two routes below. |
The decide function that the SDK makes derives from the visitor ID and from the ID of
the feature flag. A feature flag that you control from the dashboard therefore cannot use
the data of your application. Define the feature flag in SDK code when you need that.
Which route decide takes depends on the type of the feature flag:
| Dashboard type | The bucket | How the SDK decides |
|---|---|---|
| Boolean | true or false | It compares the stable value of the visitor against weight. |
| Text | The value of one variant, or false | It compares the stable value of the visitor against the range that Picked gives to each variant. |
The SDK sorts the variants by their key before it allocates, so the order of the payload has no effect. See More than two groups.
Define a feature flag in SDK code
Define your feature flags when you create the Picked client. Pass a features object. Use
this when a decision needs data that only your application holds. Each feature flag needs a
unique identifier, a version key, a name that a person can read, and a decide
function:
import { createClient } from 'picked'
const client = createClient({
projectId: 'your-project-id',
features: {
'summer-sale': {
key: 'summer-sale-v1', // The version key. Change it to invalidate the cache.
name: 'Summer Sale Experiment', // A name for analytics that a person can read.
decide: async () => {
// Put your decision rules here.
const isEligible = await checkUserEligibility()
return isEligible // Returns a boolean.
},
storage: 'local', // Optional: 'local' is the default, or 'session'.
},
'homepage-variant': {
key: 'homepage-test-v2',
name: 'Homepage A/B Test',
decide: async () => {
// Return a string for a test of more than two variants.
return Math.random() < 0.5 ? 'variant-a' : 'variant-b'
},
},
},
})A feature flag can also be a factory function, which starts the feature flag only when it is first needed:
features: {
'my-feature': () => ({
key: 'my-feature-v1',
name: 'My Feature',
decide: async () => true,
}),
}Overrides in the URL
You can override a feature flag decision from the URL search parameters, for a test:
https://example.com?picked-feature=my-feature:true
You can give more than one override value:
https://example.com?picked-feature=my-feature:true&picked-feature=homepage-variant:variant-a
The format is ?picked-feature=<feature-id>:<bucket-value>. For a Text feature flag,
give the value that the variant serves.
A feature ID contains only lowercase letters, numbers, and hyphens. Therefore the SDK divides the parameter at the first colon. The bucket value can contain more colons:
https://example.com?picked-feature=opening-hours:09:00-17:00
This is useful for QA, because you can test one bucket value and change no code. The SDK keeps the override value in session storage. The browser clears it when you close the tab.
When a URL parameter overrides a feature flag, the SDK registers that feature flag as
enabled. Therefore useFeatureEnabled and getFeatureEnabled give true. A URL
parameter can also override a feature flag that the dashboard Enabled toggle keeps
off.
Use a feature flag in an experience condition
You can use a feature flag in an experience condition through JSON logic:
const myExperience = createExperience({
// The other options go here.
condition: {
and: [
{
'==': [
{
invoke: {
var: 'features.summer-sale', // Point at the feature flag by its ID.
},
},
true, // Check that the bucket equals true.
],
},
],
},
})The ID in features.<id> is the Key of the feature flag from the dashboard. It is the
same value that the detail page of the feature flag shows.
An evaluation of a condition reads the bucket. Picked therefore allocates a visitor to a feature flag when the condition that points at it runs.
Watch for feature flag decisions
Subscribe to the feature_decide event to track when a feature flag resolves:
client.eventBus.watch((event) => {
if (event.type !== 'feature_decide') return
const { feature } = event
console.log(`Flag "${feature.id}" resolved to bucket:`, feature.bucket)
// Example output: the feature flag resolved to the bucket true.
// Send the decision to your analytics.
analytics.track('Feature Decision', {
featureId: feature.id,
bucket: feature.bucket,
})
})A watch on eventBus reports only what happens after you subscribe.
Read the events that already happened
client.events is a store of every event since the client started, in order. Each entry
holds an id, the time at, and the payload that went on the bus.
const alreadyAllocated = client.events
.getState()
.filter((entry) => entry.payload.type === 'feature_decide')Use it when your code starts after Picked does, and a feature_decide that you needed
already happened before you subscribed.
Versions and cache invalidation
The SDK caches a feature flag decision in browser storage. It uses localStorage, or
sessionStorage in a preview. The cache key derives from the feature flag ID and the version,
so a visitor keeps the same decision across page loads.
How versions work
A feature flag that you create in the dashboard starts at version 1. The SDK uses the
version to build the cache key. A feature flag at version 3 caches under v3.
The version affects the cache only. It is not part of the allocation. For a feature flag that you control from the dashboard, the allocation derives from the visitor ID and the weight of the feature flag.
What happens when you increment the version
When you increment the version of a feature flag in the dashboard:
- The cache key changes. The old entry,
v3, no longer matches the new key,v4. The SDK therefore treats the feature flag as undecided. - The
decidefunction runs again. On the next page load, the SDK callsdecide()again and caches the new result. - Picked evaluates each visitor again and moves nobody. For a feature flag that you control from the dashboard the allocation is deterministic. The same visitor against an unchanged weight gives the same bucket.
The SDK key and the dashboard version
For a feature flag that you define in SDK code, use the key field to set the version
yourself. This applies when you do not read the feature flag from the dashboard:
features: {
'summer-sale': {
key: 'summer-sale-v2', // Change this to invalidate the cache.
// The other options go here.
},
}For a feature flag that you control from the dashboard, Picked sets the version for you. The dashboard adds the version number to the end of the cache key that the SDK uses.
Related
- How to read a feature flag — the accessors in use.
- About feature flag allocation — when a visitor enters a test.
- Feature flags — the dashboard controls.
