FA-74311 / Feature flag rollout bucketing / Open access
Seeded weighted rollout: Empty secondary keys are dropped · case 01
Users with an empty secondary attribute bucket as if they had none.
ROOT CAUSE
The secondary is appended only when truthy.
VERIFIED REPAIR
Append "." + secondary whenever secondary is not None.
Unsuccessful approach: Appending without the dot separator hashes a different string.
Case contract
Hash text is (str(seed) if seed is not None else flag_key + "." + salt) + "." + ident, plus "." + secondary when secondary is not None (an empty string still counts). bucket = int(first 15 hex digits of sha1(text), 16) / 0xFFFFFFFFFFFFFFF. weights are in 1/100000 units; return the first index whose cumulative weight / 100000 exceeds the bucket, else the last index, as [index, round(bucket, 6)].
Why this case matters
Seeds let several flags share one bucketing so a cohort sees consistent variations.
1 / The failure
Exit 1"""Failure Map reference implementation. Python standard library only."""
import json
import hashlib
N = 1
observations = []
def solve(flag_key, salt, ident, secondary, seed, weights):
prefix = str(seed) if seed is not None else flag_key + '.' + salt
text = prefix + '.' + ident
if secondary:
text += '.' + secondary
bucket = int(hashlib.sha1(text.encode()).hexdigest()[:15], 16) / float(0xFFFFFFFFFFFFFFF)
acc = 0
for index, w in enumerate(weights):
acc += w
if bucket < acc / 100000.0:
return [index, round(bucket, 6)]
return [len(weights) - 1, round(bucket, 6)]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[('seed zero is a real seed', ['flag-a', 's', 'id1', None, 0, [50000, 50000]], [0, 0.331393]),
('seed replaces flag key and salt', ['flag-b', 's', 'id2', None, 7, [30000, 70000]], [1, 0.855827]),
('empty secondary is still appended', ['flag-a', 's', 'id3', '', None, [50000, 50000]], [1, 0.891663]),
('non-empty secondary', ['flag-a', 's', 'id4', 'eu', None, [10000, 90000]], [1, 0.975219]),
('weights short of total go to last index',
['flag-b', 'salt2', 'id5', None, None, [10000, 10000]],
[1, 0.233147]),
('rollout sample 1', ['flag-b', 'salt2', 'id120', '', 0, [0, 60000, 40000]], [2, 0.88242]),
('rollout sample 2', ['flag-b', 's', 'id212', 'x', 61, [25000, 25000, 25000]], [1, 0.37423]),
('rollout sample 3', ['flag-b', 'salt2', 'id106', None, 7, [50000, 50000]], [0, 0.322784])],
[('seed replaces flag key and salt', ['flag-b', 's', 'id2', None, 7, [30000, 70000]], [1, 0.855827]),
('empty secondary is still appended', ['flag-a', 's', 'id3', '', None, [50000, 50000]], [1, 0.891663]),
('non-empty secondary', ['flag-a', 's', 'id4', 'eu', None, [10000, 90000]], [1, 0.975219]),
('weights short of total go to last index',
['flag-b', 'salt2', 'id5', None, None, [10000, 10000]],
[1, 0.233147]),
('zero-weight first variation', ['flag-b', 's', 'id6', None, None, [0, 100000]], [1, 0.255084]),
('rollout sample 6', ['flag-b', 'salt2', 'id765', None, 7, [10000, 20000, 70000]], [2, 0.464498]),
('rollout sample 11', ['flag-a', 'salt2', 'id773', '', 7, [0, 60000, 40000]], [1, 0.477906]),
('rollout sample 25', ['flag-a', 'salt2', 'id671', '', 0, [30000, 30000]], [0, 0.182748])],
[('seed zero is a real seed', ['flag-a', 's', 'id1', None, 0, [50000, 50000]], [0, 0.331393]),
('empty secondary is still appended', ['flag-a', 's', 'id3', '', None, [50000, 50000]], [1, 0.891663]),
('non-empty secondary', ['flag-a', 's', 'id4', 'eu', None, [10000, 90000]], [1, 0.975219]),
('weights short of total go to last index',
['flag-b', 'salt2', 'id5', None, None, [10000, 10000]],
[1, 0.233147]),
('zero-weight first variation', ['flag-b', 's', 'id6', None, None, [0, 100000]], [1, 0.255084]),
('rollout sample 1', ['flag-b', 'salt2', 'id120', '', 0, [0, 60000, 40000]], [2, 0.88242]),
('rollout sample 11', ['flag-a', 'salt2', 'id773', '', 7, [0, 60000, 40000]], [1, 0.477906]),
('rollout sample 24', ['flag-b', 's', 'id723', 'x', None, [0, 60000, 40000]], [2, 0.892779])],
[('seed zero is a real seed', ['flag-a', 's', 'id1', None, 0, [50000, 50000]], [0, 0.331393]),
('seed replaces flag key and salt', ['flag-b', 's', 'id2', None, 7, [30000, 70000]], [1, 0.855827]),
('non-empty secondary', ['flag-a', 's', 'id4', 'eu', None, [10000, 90000]], [1, 0.975219]),
('weights short of total go to last index',
['flag-b', 'salt2', 'id5', None, None, [10000, 10000]],
[1, 0.233147]),
('zero-weight first variation', ['flag-b', 's', 'id6', None, None, [0, 100000]], [1, 0.255084]),
('rollout sample 16', ['flag-b', 'salt2', 'id229', 'eu', 61, [10000, 20000, 70000]], [2, 0.315757]),
('rollout sample 27', ['flag-a', 'salt2', 'id732', '', 61, [50000, 50000]], [1, 0.80431]),
('rollout sample 32', ['flag-b', 's', 'id350', 'eu', None, [30000, 30000]], [0, 0.003521])],
[('seed zero is a real seed', ['flag-a', 's', 'id1', None, 0, [50000, 50000]], [0, 0.331393]),
('seed replaces flag key and salt', ['flag-b', 's', 'id2', None, 7, [30000, 70000]], [1, 0.855827]),
('empty secondary is still appended', ['flag-a', 's', 'id3', '', None, [50000, 50000]], [1, 0.891663]),
('weights short of total go to last index',
['flag-b', 'salt2', 'id5', None, None, [10000, 10000]],
[1, 0.233147]),
('zero-weight first variation', ['flag-b', 's', 'id6', None, None, [0, 100000]], [1, 0.255084]),
('rollout sample 11', ['flag-a', 'salt2', 'id773', '', 7, [0, 60000, 40000]], [1, 0.477906]),
('rollout sample 21', ['flag-a', 'salt2', 'id352', '', None, [99999]], [0, 0.94531]),
('rollout sample 45', ['flag-b', 'salt2', 'id995', 'eu', 0, [99999]], [0, 0.585568])]]
for label, args, expected in fixtures[N - 1]:
check(label, solve(*args), expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
| Boundary fixture | Actual | Expected | Outcome |
|---|---|---|---|
| seed zero is a real seed | [0, 0.331393] | [0, 0.331393] | Passed |
| seed replaces flag key and salt | [1, 0.855827] | [1, 0.855827] | Passed |
| empty secondary is still appended | [0, 0.0619] | [1, 0.891663] | Failed |
| non-empty secondary | [1, 0.975219] | [1, 0.975219] | Passed |
| weights short of total go to last index | [1, 0.233147] | [1, 0.233147] | Passed |
| rollout sample 1 | [2, 0.801288] | [2, 0.88242] | Failed |
| rollout sample 2 | [1, 0.37423] | [1, 0.37423] | Passed |
| rollout sample 3 | [0, 0.322784] | [0, 0.322784] | Passed |
SHA-256 / 6ae5da1e59c52532f84de86e75114f41a61f948e8947746311dfd438e10c7324
2 / The unsuccessful fix
Exit 1"""Failure Map reference implementation. Python standard library only."""
import json
import hashlib
N = 1
observations = []
def solve(flag_key, salt, ident, secondary, seed, weights):
prefix = str(seed) if seed is not None else flag_key + '.' + salt
text = prefix + '.' + ident
if secondary is not None:
text += secondary
bucket = int(hashlib.sha1(text.encode()).hexdigest()[:15], 16) / float(0xFFFFFFFFFFFFFFF)
acc = 0
for index, w in enumerate(weights):
acc += w
if bucket < acc / 100000.0:
return [index, round(bucket, 6)]
return [len(weights) - 1, round(bucket, 6)]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[('seed zero is a real seed', ['flag-a', 's', 'id1', None, 0, [50000, 50000]], [0, 0.331393]),
('seed replaces flag key and salt', ['flag-b', 's', 'id2', None, 7, [30000, 70000]], [1, 0.855827]),
('empty secondary is still appended', ['flag-a', 's', 'id3', '', None, [50000, 50000]], [1, 0.891663]),
('non-empty secondary', ['flag-a', 's', 'id4', 'eu', None, [10000, 90000]], [1, 0.975219]),
('weights short of total go to last index',
['flag-b', 'salt2', 'id5', None, None, [10000, 10000]],
[1, 0.233147]),
('rollout sample 1', ['flag-b', 'salt2', 'id120', '', 0, [0, 60000, 40000]], [2, 0.88242]),
('rollout sample 2', ['flag-b', 's', 'id212', 'x', 61, [25000, 25000, 25000]], [1, 0.37423]),
('rollout sample 3', ['flag-b', 'salt2', 'id106', None, 7, [50000, 50000]], [0, 0.322784])],
[('seed replaces flag key and salt', ['flag-b', 's', 'id2', None, 7, [30000, 70000]], [1, 0.855827]),
('empty secondary is still appended', ['flag-a', 's', 'id3', '', None, [50000, 50000]], [1, 0.891663]),
('non-empty secondary', ['flag-a', 's', 'id4', 'eu', None, [10000, 90000]], [1, 0.975219]),
('weights short of total go to last index',
['flag-b', 'salt2', 'id5', None, None, [10000, 10000]],
[1, 0.233147]),
('zero-weight first variation', ['flag-b', 's', 'id6', None, None, [0, 100000]], [1, 0.255084]),
('rollout sample 6', ['flag-b', 'salt2', 'id765', None, 7, [10000, 20000, 70000]], [2, 0.464498]),
('rollout sample 11', ['flag-a', 'salt2', 'id773', '', 7, [0, 60000, 40000]], [1, 0.477906]),
('rollout sample 25', ['flag-a', 'salt2', 'id671', '', 0, [30000, 30000]], [0, 0.182748])],
[('seed zero is a real seed', ['flag-a', 's', 'id1', None, 0, [50000, 50000]], [0, 0.331393]),
('empty secondary is still appended', ['flag-a', 's', 'id3', '', None, [50000, 50000]], [1, 0.891663]),
('non-empty secondary', ['flag-a', 's', 'id4', 'eu', None, [10000, 90000]], [1, 0.975219]),
('weights short of total go to last index',
['flag-b', 'salt2', 'id5', None, None, [10000, 10000]],
[1, 0.233147]),
('zero-weight first variation', ['flag-b', 's', 'id6', None, None, [0, 100000]], [1, 0.255084]),
('rollout sample 1', ['flag-b', 'salt2', 'id120', '', 0, [0, 60000, 40000]], [2, 0.88242]),
('rollout sample 11', ['flag-a', 'salt2', 'id773', '', 7, [0, 60000, 40000]], [1, 0.477906]),
('rollout sample 24', ['flag-b', 's', 'id723', 'x', None, [0, 60000, 40000]], [2, 0.892779])],
[('seed zero is a real seed', ['flag-a', 's', 'id1', None, 0, [50000, 50000]], [0, 0.331393]),
('seed replaces flag key and salt', ['flag-b', 's', 'id2', None, 7, [30000, 70000]], [1, 0.855827]),
('non-empty secondary', ['flag-a', 's', 'id4', 'eu', None, [10000, 90000]], [1, 0.975219]),
('weights short of total go to last index',
['flag-b', 'salt2', 'id5', None, None, [10000, 10000]],
[1, 0.233147]),
('zero-weight first variation', ['flag-b', 's', 'id6', None, None, [0, 100000]], [1, 0.255084]),
('rollout sample 16', ['flag-b', 'salt2', 'id229', 'eu', 61, [10000, 20000, 70000]], [2, 0.315757]),
('rollout sample 27', ['flag-a', 'salt2', 'id732', '', 61, [50000, 50000]], [1, 0.80431]),
('rollout sample 32', ['flag-b', 's', 'id350', 'eu', None, [30000, 30000]], [0, 0.003521])],
[('seed zero is a real seed', ['flag-a', 's', 'id1', None, 0, [50000, 50000]], [0, 0.331393]),
('seed replaces flag key and salt', ['flag-b', 's', 'id2', None, 7, [30000, 70000]], [1, 0.855827]),
('empty secondary is still appended', ['flag-a', 's', 'id3', '', None, [50000, 50000]], [1, 0.891663]),
('weights short of total go to last index',
['flag-b', 'salt2', 'id5', None, None, [10000, 10000]],
[1, 0.233147]),
('zero-weight first variation', ['flag-b', 's', 'id6', None, None, [0, 100000]], [1, 0.255084]),
('rollout sample 11', ['flag-a', 'salt2', 'id773', '', 7, [0, 60000, 40000]], [1, 0.477906]),
('rollout sample 21', ['flag-a', 'salt2', 'id352', '', None, [99999]], [0, 0.94531]),
('rollout sample 45', ['flag-b', 'salt2', 'id995', 'eu', 0, [99999]], [0, 0.585568])]]
for label, args, expected in fixtures[N - 1]:
check(label, solve(*args), expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
| Boundary fixture | Actual | Expected | Outcome |
|---|---|---|---|
| seed zero is a real seed | [0, 0.331393] | [0, 0.331393] | Passed |
| seed replaces flag key and salt | [1, 0.855827] | [1, 0.855827] | Passed |
| empty secondary is still appended | [0, 0.0619] | [1, 0.891663] | Failed |
| non-empty secondary | [1, 0.647006] | [1, 0.975219] | Failed |
| weights short of total go to last index | [1, 0.233147] | [1, 0.233147] | Passed |
| rollout sample 1 | [2, 0.801288] | [2, 0.88242] | Failed |
| rollout sample 2 | [2, 0.959143] | [1, 0.37423] | Failed |
| rollout sample 3 | [0, 0.322784] | [0, 0.322784] | Passed |
SHA-256 / 2c91d391112db9e4b68855bb407bdb1f283e5e19acae9df4a4b14bfe66efd2d2
3 / The verified repair
Exit 0"""Failure Map reference implementation. Python standard library only."""
import json
import hashlib
N = 1
observations = []
def solve(flag_key, salt, ident, secondary, seed, weights):
prefix = str(seed) if seed is not None else flag_key + '.' + salt
text = prefix + '.' + ident
if secondary is not None:
text += '.' + secondary
bucket = int(hashlib.sha1(text.encode()).hexdigest()[:15], 16) / float(0xFFFFFFFFFFFFFFF)
acc = 0
for index, w in enumerate(weights):
acc += w
if bucket < acc / 100000.0:
return [index, round(bucket, 6)]
return [len(weights) - 1, round(bucket, 6)]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[('seed zero is a real seed', ['flag-a', 's', 'id1', None, 0, [50000, 50000]], [0, 0.331393]),
('seed replaces flag key and salt', ['flag-b', 's', 'id2', None, 7, [30000, 70000]], [1, 0.855827]),
('empty secondary is still appended', ['flag-a', 's', 'id3', '', None, [50000, 50000]], [1, 0.891663]),
('non-empty secondary', ['flag-a', 's', 'id4', 'eu', None, [10000, 90000]], [1, 0.975219]),
('weights short of total go to last index',
['flag-b', 'salt2', 'id5', None, None, [10000, 10000]],
[1, 0.233147]),
('rollout sample 1', ['flag-b', 'salt2', 'id120', '', 0, [0, 60000, 40000]], [2, 0.88242]),
('rollout sample 2', ['flag-b', 's', 'id212', 'x', 61, [25000, 25000, 25000]], [1, 0.37423]),
('rollout sample 3', ['flag-b', 'salt2', 'id106', None, 7, [50000, 50000]], [0, 0.322784])],
[('seed replaces flag key and salt', ['flag-b', 's', 'id2', None, 7, [30000, 70000]], [1, 0.855827]),
('empty secondary is still appended', ['flag-a', 's', 'id3', '', None, [50000, 50000]], [1, 0.891663]),
('non-empty secondary', ['flag-a', 's', 'id4', 'eu', None, [10000, 90000]], [1, 0.975219]),
('weights short of total go to last index',
['flag-b', 'salt2', 'id5', None, None, [10000, 10000]],
[1, 0.233147]),
('zero-weight first variation', ['flag-b', 's', 'id6', None, None, [0, 100000]], [1, 0.255084]),
('rollout sample 6', ['flag-b', 'salt2', 'id765', None, 7, [10000, 20000, 70000]], [2, 0.464498]),
('rollout sample 11', ['flag-a', 'salt2', 'id773', '', 7, [0, 60000, 40000]], [1, 0.477906]),
('rollout sample 25', ['flag-a', 'salt2', 'id671', '', 0, [30000, 30000]], [0, 0.182748])],
[('seed zero is a real seed', ['flag-a', 's', 'id1', None, 0, [50000, 50000]], [0, 0.331393]),
('empty secondary is still appended', ['flag-a', 's', 'id3', '', None, [50000, 50000]], [1, 0.891663]),
('non-empty secondary', ['flag-a', 's', 'id4', 'eu', None, [10000, 90000]], [1, 0.975219]),
('weights short of total go to last index',
['flag-b', 'salt2', 'id5', None, None, [10000, 10000]],
[1, 0.233147]),
('zero-weight first variation', ['flag-b', 's', 'id6', None, None, [0, 100000]], [1, 0.255084]),
('rollout sample 1', ['flag-b', 'salt2', 'id120', '', 0, [0, 60000, 40000]], [2, 0.88242]),
('rollout sample 11', ['flag-a', 'salt2', 'id773', '', 7, [0, 60000, 40000]], [1, 0.477906]),
('rollout sample 24', ['flag-b', 's', 'id723', 'x', None, [0, 60000, 40000]], [2, 0.892779])],
[('seed zero is a real seed', ['flag-a', 's', 'id1', None, 0, [50000, 50000]], [0, 0.331393]),
('seed replaces flag key and salt', ['flag-b', 's', 'id2', None, 7, [30000, 70000]], [1, 0.855827]),
('non-empty secondary', ['flag-a', 's', 'id4', 'eu', None, [10000, 90000]], [1, 0.975219]),
('weights short of total go to last index',
['flag-b', 'salt2', 'id5', None, None, [10000, 10000]],
[1, 0.233147]),
('zero-weight first variation', ['flag-b', 's', 'id6', None, None, [0, 100000]], [1, 0.255084]),
('rollout sample 16', ['flag-b', 'salt2', 'id229', 'eu', 61, [10000, 20000, 70000]], [2, 0.315757]),
('rollout sample 27', ['flag-a', 'salt2', 'id732', '', 61, [50000, 50000]], [1, 0.80431]),
('rollout sample 32', ['flag-b', 's', 'id350', 'eu', None, [30000, 30000]], [0, 0.003521])],
[('seed zero is a real seed', ['flag-a', 's', 'id1', None, 0, [50000, 50000]], [0, 0.331393]),
('seed replaces flag key and salt', ['flag-b', 's', 'id2', None, 7, [30000, 70000]], [1, 0.855827]),
('empty secondary is still appended', ['flag-a', 's', 'id3', '', None, [50000, 50000]], [1, 0.891663]),
('weights short of total go to last index',
['flag-b', 'salt2', 'id5', None, None, [10000, 10000]],
[1, 0.233147]),
('zero-weight first variation', ['flag-b', 's', 'id6', None, None, [0, 100000]], [1, 0.255084]),
('rollout sample 11', ['flag-a', 'salt2', 'id773', '', 7, [0, 60000, 40000]], [1, 0.477906]),
('rollout sample 21', ['flag-a', 'salt2', 'id352', '', None, [99999]], [0, 0.94531]),
('rollout sample 45', ['flag-b', 'salt2', 'id995', 'eu', 0, [99999]], [0, 0.585568])]]
for label, args, expected in fixtures[N - 1]:
check(label, solve(*args), expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
| Boundary fixture | Actual | Expected | Outcome |
|---|---|---|---|
| seed zero is a real seed | [0, 0.331393] | [0, 0.331393] | Passed |
| seed replaces flag key and salt | [1, 0.855827] | [1, 0.855827] | Passed |
| empty secondary is still appended | [1, 0.891663] | [1, 0.891663] | Passed |
| non-empty secondary | [1, 0.975219] | [1, 0.975219] | Passed |
| weights short of total go to last index | [1, 0.233147] | [1, 0.233147] | Passed |
| rollout sample 1 | [2, 0.88242] | [2, 0.88242] | Passed |
| rollout sample 2 | [1, 0.37423] | [1, 0.37423] | Passed |
| rollout sample 3 | [0, 0.322784] | [0, 0.322784] | Passed |
SHA-256 / e843b399279c70138d7f1a4231a2bd012af09b0df78f578258154faf27588348
Verification & scope
A deterministic toy flag-evaluation model with a stipulated contract; it does not reproduce any vendor SDK byte for byte. This reproducer isolates one failure mechanism. Results cover the supplied fixtures. Variants within a family share a test contract and should remain grouped when constructing evaluation splits. Related mechanisms with a shared evaluation_group must also remain together; these controlled models are not independent production incidents.
Observations recorded using Python 3.12.14 at 2026-09-29T14:48:55.514590+00:00.
Case digest / 67d2fadcb2c8e71f6700bea8c31005d5c5c725b6a1bba2c04ae8961ba38e4b38