功能说明
该示例根据功能开关键与用户 ID 计算稳定分桶,实现按百分比逐步放量。同一个用户在同一开关下始终落入同一桶,不会因刷新而反复切换;不同开关使用不同哈希输入,避免同一批用户总是先看到所有新功能。白名单可以强制开启,百分比为零时直接关闭。适合配置管理、灰度发布与实验分组。
关键代码
evaluateFlag 先校验开关键、用户 ID 与百分比范围,再检查白名单。百分比为零时返回 off,否则把开关键与用户 ID 用冒号拼接后交给 bucketFor。bucketFor 使用 FNV-1a 计算 32 位哈希,对一万取模得到 0 到 9999 的整数,再除以一百得到 0 到 99.99 的桶值。最终用桶值小于百分比决定是否开启。
使用注意
修改开关键会重新洗牌所有用户的分桶,发布后应保持键名稳定。哈希不是加密安全算法,但用于分流足够且计算快。百分比调高时原有开启用户仍保持开启,调低时部分用户会关闭,这是预期行为。白名单应谨慎维护并记录审计。该评估只负责决策,实际配置下发与缓存仍需独立处理。
验证方式
对同一用户重复调用断言桶值稳定、结果一致。白名单用户即使百分比为零也应开启。百分比为零返回 off,百分比为一百时所有用户开启。测试非法百分比、空用户 ID 与空开关键分别抛错,并用大量用户统计开启比例接近设定值。
代码示例
export interface FlagContext {
readonly userId: string;
readonly allowlist?: ReadonlySet<string>;
}
export interface FlagDecision {
readonly enabled: boolean;
readonly bucket: number;
readonly reason: "allowlist" | "percentage" | "off";
}
export function evaluateFlag(flagKey: string, percentage: number, context: FlagContext): FlagDecision {
if (typeof flagKey !== "string" || flagKey.trim() === "") {
throw new TypeError("flagKey must be a non-empty string");
}
if (typeof context.userId !== "string" || context.userId === "") {
throw new TypeError("userId must be a non-empty string");
}
if (!Number.isFinite(percentage) || percentage < 0 || percentage > 100) {
throw new RangeError("percentage must be between 0 and 100");
}
if (context.allowlist?.has(context.userId) === true) {
return { enabled: true, bucket: 0, reason: "allowlist" };
}
if (percentage === 0) return { enabled: false, bucket: 100, reason: "off" };
const bucket = bucketFor(`${flagKey}:${context.userId}`) / 100;
return { enabled: bucket < percentage, bucket, reason: "percentage" };
}
function bucketFor(value: string): number {
let hash = 0x811c9dc5;
for (let index = 0; index < value.length; index += 1) {
hash ^= value.charCodeAt(index);
hash = Math.imul(hash, 0x01000193);
}
return (hash >>> 0) % 10000;
}
const allowlist = new Set(["internal-1"]);
console.log(evaluateFlag("new-checkout", 25, { userId: "user-42", allowlist }));
console.log(evaluateFlag("new-checkout", 25, { userId: "internal-1", allowlist }));
console.log(evaluateFlag("new-checkout", 0, { userId: "user-42" }));