Skip to content
LogoLogo

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:

FieldValue
idThe dashboard key of the feature flag
keyv{version}, for example v3
nameThe dashboard name of the feature flag
enabledThe dashboard Enabled toggle
weightThe dashboard Weight, 0 to 100
storagelocal, or session in preview mode
decideMade 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 typeThe bucketHow the SDK decides
Booleantrue or falseIt compares the stable value of the visitor against weight.
TextThe value of one variant, or falseIt 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:

  1. The cache key changes. The old entry, v3, no longer matches the new key, v4. The SDK therefore treats the feature flag as undecided.
  2. The decide function runs again. On the next page load, the SDK calls decide() again and caches the new result.
  3. 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.