Skip to Content
FeatureVariants

Variants

@evanion/feature 0.1.0 and its patch releases up to 0.1.1, the version on npm. Values on this page were produced by running the source at e53b2d8, a later commit than the 0.1.0 tag, and are not re-executed.

A feature’s variants split the customers it reaches across named versions, such as two button labels in an A/B test, and resolve() returns the one each customer gets, the same one on every request.

How Variants Work

To split a feature’s audience into different experiences, you use variants. When an @evanion/feature feature is enabled and declares variants, the library assigns one of those variants to the customer from the same bucket Rollouts derives, under a separate seed. A customer who saw two variants would count in both arms of the test, so the assignment never moves between requests.

import { } from '@evanion/feature'; const = ([ { : 'checkout-cta', : true, : [ { : 'control', : 50 }, { : 'blue', : 50, : { : 'Get it' } }, ], }, ]); .({ : 'user-1' })['checkout-cta']; // -> { key: 'checkout-cta', enabled: true, reason: 'default-on', variant: 'control', assignment: { source: 'weighted', by: 'targetingKey', bucket: 0.17582221026532352 } }

Weights, Values and Assignment

The library determines which variant a customer sees based on several configuration properties:

  • Weight: Each variant has a weight. These are relative, not absolute. A split with weights 3 and 1 results in a 75%/25% distribution.
  • Control: The first variant in bucketing order is the control, the one a customer gets when nothing else decides.
  • Value: You can attach a specific value to a variant (e.g., blue: { label: 'Get it' }). This allows your components to remain agnostic of the variant names. A variant that declares no value leaves value off the decision.
  • Assignment Source: The assignment field tells you how the variant was chosen: weighted, pinned, sticky, or fallback.
import { } from '@evanion/feature'; const = ([ { : 'summary-panel', : true, : [ { : 'control', : 3 }, { : 'compact', : 1 }, ], }, ]); .({ : 'user-4' })['summary-panel']; // -> { key: 'summary-panel', enabled: true, reason: 'default-on', variant: 'compact', assignment: { source: 'weighted', by: 'targetingKey', bucket: 0.9914100710302591 } } .({ : 'user-1' })['summary-panel']; // -> { key: 'summary-panel', enabled: true, reason: 'default-on', variant: 'control', assignment: { source: 'weighted', by: 'targetingKey', bucket: 0.4314444069750607 } }

Bucketing Order and Seed

To assign a variant, @evanion/feature walks a feature’s variants in order, and each one takes a band of the bucket range as wide as its share of the weight. The bucket’s band decides the variant.

The Importance of order

By default, the walk follows each variant’s position in the variants array. If you reorder the array in a future deploy, the bands move, and customers move between variants with them.

To prevent this, you can provide an explicit order on each variant to fix its position in the walk, so reordering the array moves nobody. This does not fix the bands: a weight change still moves the boundaries. A configuration document a store reads as JSON is refused unless every variant declares order.

import { } from '@evanion/feature'; const = ([ { : 'summary-panel', : true, : [ { : 'control', : 3 }, { : 'compact', : 1 }, ], }, ]); const = ([ { : 'summary-panel', : true, : [ { : 'compact', : 1 }, { : 'control', : 3 }, ], }, ]); const = ([ { : 'summary-panel', : true, : [ { : 'compact', : 1, : 1 }, { : 'control', : 3, : 0 }, ], }, ]); .({ : 'user-4' })['summary-panel'].; // -> 'compact' .({ : 'user-4' })['summary-panel'].; // -> 'control' .({ : 'user-4' })['summary-panel'].; // -> 'compact'

The Variant Seed

The variantSeed determines how the variant bucket is derived. It defaults to the feature’s seed (or its key) followed by :variant, so the split is bucketed apart from the rollout. Setting it to the rollout’s own seed correlates the two: every member of a 50% rollout sits below the same 0.5 boundary a 50/50 split uses, and all of them get the control.

Advanced covers what changing variantSeed does to a customer already placed.

import { } from '@evanion/feature'; const = ([ { : 'checkout-cta', : true, : [{ : { : 50 } }], : [ { : 'control', : 50 }, { : 'blue', : 50 }, ], : 'checkout-cta', }, ]); .({ : 'user-1' })['checkout-cta'].; // -> 'control' .({ : 'user-2' })['checkout-cta'].; // -> 'control'

Advanced Variant Control

Pinning via Rules

You can override the weighted distribution of an @evanion/feature variant for specific customers using rules. For example, you might want every bookseller to see the blue variant regardless of their bucket.

import { } from '@evanion/feature'; const = ([ { : 'checkout-cta', : true, : [ { : 'control', : 50 }, { : 'blue', : 50, : { : 'Get it' } }, ], : [ { : [{ : 'role', : 'eq', : 'bookseller' }], : 'blue', }, { : { : 20 } }, ], }, ]); const = { : 'user-1', : 'bookseller' }; .({ : 'user-1' })['checkout-cta'].; // -> 'control' .()['checkout-cta']; // -> { key: 'checkout-cta', enabled: true, reason: 'rule-match', rule: 'rule-d69c7f2b', variant: 'blue', assignment: { source: 'pinned', by: 'targetingKey', rule: 'rule-d69c7f2b' }, value: { label: 'Get it' } }

When a rule matches, the variant is “pinned.” The assignment.rule field identifies which rule caused the override, and the bucket is absent because no weighted assignment took place.

Sticky Variants

If you change the weights of a variant, some customers move to a different variant. To prevent this, your application hands back a customer’s prior assignment in the context’s stickyVariants field, keyed by feature, from wherever it keeps session state. The library reads it and writes no storage of its own.

import { } from '@evanion/feature'; const = ([ { : 'checkout-cta', : true, : [ { : 'control', : 50 }, { : 'blue', : 50, : { : 'Get it' } }, ], }, ]); .({ : { 'checkout-cta': 'blue' } })['checkout-cta']; // -> { key: 'checkout-cta', enabled: true, reason: 'default-on', variant: 'blue', assignment: { source: 'sticky', by: 'targetingKey' }, value: { label: 'Get it' } }

A sticky entry wins over the weights, ensuring that a user who has already seen a variant continues to see it even after the weights change. A rule pin still wins over a sticky entry, and an entry naming a variant the feature no longer declares is ignored.

Advanced covers the order in which assignVariant reads a pin, a sticky entry and the weights.

Edge Cases

No Bucketing Value

If a customer’s context is missing the field used for bucketing (e.g., they are not logged in), @evanion/feature assigns the control, and assignment.source reads fallback. This ensures the app still renders a valid version of the feature.

import { } from '@evanion/feature'; const = ([ { : 'checkout-cta', : true, : [ { : 'control', : 50 }, { : 'blue', : 50, : { : 'Get it' } }, ], }, ]); .({})['checkout-cta']; // -> { key: 'checkout-cta', enabled: true, reason: 'default-on', variant: 'control', assignment: { source: 'fallback', by: 'targetingKey' } }

Features that resolve “Off”

If an @evanion/feature feature resolves off for any reason, it carries no variant, no value, and no assignment. Here no rule matched. A variant is assigned only for a feature that has resolved on.

import { } from '@evanion/feature'; const = ([ { : 'checkout-cta', : true, : [ { : 'control', : 50 }, { : 'blue', : 50, : { : 'Get it' } }, ], : [ { : [{ : 'role', : 'eq', : 'bookseller' }], : 'blue', }, { : { : 20 } }, ], }, ]); .({ : 'user-2' })['checkout-cta']; // -> { key: 'checkout-cta', enabled: false, reason: 'no-rule-matched', rules: [{ rule: 'rule-d69c7f2b', matched: false, failed: { field: 'role', op: 'eq', value: 'bookseller' } }, { rule: 'rule-3deffef5', matched: false, rollout: { percent: 20, by: 'targetingKey', member: false } }] }

@evanion/react-feature hands these decisions, variants included, to a component tree.

Last updated on