Skip to content

Targeting

phil/feature-flags-bundle

An active flag with rules is on for a context when any of its rules matches. The rules are tried in their declared order and the first match gives the reason of the evaluation.

Attributes

A rule reads one attribute of the EvaluationContext (TargetingAttribute):

AttributeTypical value
tenantThe account or organisation the request acts for.
projectThe project inside a tenant.
subjectThe person or client making the request.

The rule, not the context, says which attribute it means: a rollout by tenant keeps every member of a tenant on the same side of the flag; a rollout by subject splits them.

A rule that cannot read its attribute does not match. A context without a tenant never matches a tenant rule, so a flag fails closed for a context it cannot place. With AnonymousContextResolver, every rule fails.

Membership: MemberOf

rules:
    - { attribute: tenant, members: [tenant-a, tenant-b] }

Matches when the attribute's value is one of the listed identifiers (exact, case-sensitive string comparison). The set needs at least one member and no empty member: an empty set would be an inactive flag. Reason when it matches: TARGETING_MATCH.

Percentage rollout: PercentageRollout

rules:
    - { attribute: tenant, percentage: 20 }

Matches a stable share of the values of the attribute. The percentage is a whole number from 0 to 100 (Percentage): 0 admits nobody, 100 admits every context that has the attribute. Reason when it matches: SPLIT.

A rule has either members or percentage, never both.

The stable bucket

Each value falls into a bucket from 0 to 99 (Bucket::of()):

bucket = murmur3a("<flag>:<value>") mod 100

murmur3a is the 32-bit MurmurHash3 that PHP ships in ext/hash, read as an unsigned integer; the input has the group:identifier shape of the Unleash SDKs' gradual rollout. A value is inside the rollout when its bucket is strictly below the percentage (Percentage::admits()).

What follows from it:

  • Stable. The same value gets the same bucket on every request and on every server, so a rollout does not flicker.
  • Monotonic. Raising a percentage only ever adds values: a tenant inside a 20 % rollout stays inside at 50 %.
  • Salted by the flag name. Two flags place the same tenants independently, so the tenants first in one rollout are not the first in all of them. Renaming a flag reshuffles its rollout.
  • Nearly uniform. 2^32 mod 100 = 96, so buckets 0 to 95 are one part in about 43 million more likely than 96 to 99, far below anything a rollout can observe.
  • An empty value is refused (every empty key would share one bucket); the context already refuses empty attributes.

A one-way door

Changing the bucket function reshuffles every running rollout at once. The unit tests pin known values (BucketTest), for instance:

InputBucket
new-checkout:tenant-181
new-checkout:tenant-262
other-flag:tenant-149
new-checkout:4273

A change that moves any of them is deliberate or a bug, never an accident.

Combining rules

flags:
    new-checkout:
        active: true
        rules:
            - { attribute: tenant, members: [tenant-a] }      # the pilot tenant, always
            - { attribute: tenant, percentage: 20 }           # plus a fifth of the others

tenant-a is on with reason TARGETING_MATCH; another tenant is on with reason SPLIT if it falls in the 20 %, otherwise off with reason DEFAULT. A context without a tenant is off (DEFAULT).

To turn the rollout off for everyone, deactivate the flag (a toggle); the rules stay, and reactivation brings back the same 20 % (toggles-and-edits.md).

Custom rules

Rule is an interface (matches(FlagName, EvaluationContext): bool and matchReason(): EvaluationReason), and FlagDefinition accepts any implementation. A provider of the application can build definitions with its own rules. They cannot be written to the configuration or the database store, though: FlagDefinitionFormat reads and writes only MemberOf and PercentageRollout, and refuses to write any other rule (extension-points.md).