better-auth-feature-flags
Version:
Ship features safely with feature flags, A/B testing, and progressive rollouts - Better Auth plugin for modern release management
281 lines (277 loc) • 7.91 kB
JavaScript
import {
__name
} from "./chunk-SHUYVCID.js";
// src/utils.ts
function generateId() {
return crypto.randomUUID();
}
__name(generateId, "generateId");
function parseJSON(value) {
if (typeof value !== "string") {
return value;
}
try {
return JSON.parse(value);
} catch {
return value;
}
}
__name(parseJSON, "parseJSON");
function calculateRollout(userId, percentage) {
if (percentage <= 0) return false;
if (percentage >= 100) return true;
const hashValue = userId.split("").reduce((acc, char) => {
return (acc << 5) - acc + char.charCodeAt(0) | 0;
}, 0);
return Math.abs(hashValue) % 100 < percentage;
}
__name(calculateRollout, "calculateRollout");
function evaluateCondition(value, operator, target) {
switch (operator) {
case "equals":
return value === target;
case "not_equals":
return value !== target;
case "contains":
return String(value).includes(String(target));
case "not_contains":
return !String(value).includes(String(target));
case "starts_with":
return String(value).startsWith(String(target));
case "ends_with":
return String(value).endsWith(String(target));
case "greater_than":
return Number(value) > Number(target);
case "less_than":
return Number(value) < Number(target);
case "greater_than_or_equal":
return Number(value) >= Number(target);
case "less_than_or_equal":
return Number(value) <= Number(target);
case "in":
return Array.isArray(target) ? target.includes(value) : false;
case "not_in":
return Array.isArray(target) ? !target.includes(value) : true;
case "regex":
try {
return new RegExp(String(target)).test(String(value));
} catch {
return false;
}
default:
return false;
}
}
__name(evaluateCondition, "evaluateCondition");
// src/evaluation.ts
async function evaluateFlags(flag, context, pluginContext) {
if (!flag.enabled) {
return {
value: flag.defaultValue,
reason: "disabled"
};
}
const override = await checkOverride(flag, context, pluginContext);
if (override) {
return override;
}
const ruleResult = await evaluateRules(flag, context, pluginContext);
if (ruleResult) {
return ruleResult;
}
const rolloutResult = checkRollout(flag, context);
if (rolloutResult) {
return rolloutResult;
}
return {
value: flag.defaultValue,
variant: flag.defaultVariant,
reason: "default"
};
}
__name(evaluateFlags, "evaluateFlags");
async function checkOverride(flag, context, pluginContext) {
const { storage } = pluginContext;
try {
const override = await storage.getOverride(flag.id, context.userId);
if (override && override.enabled) {
return {
value: override.value,
variant: override.variant,
reason: "override",
metadata: {
overrideId: override.id
}
};
}
} catch (error) {
console.error(`[feature-flags] Error checking override: ${error}`);
}
return null;
}
__name(checkOverride, "checkOverride");
async function evaluateRules(flag, context, pluginContext) {
const { storage } = pluginContext;
try {
const rules = await storage.getRulesForFlag(flag.id);
for (const rule of rules) {
if (!rule.enabled) continue;
const matches = evaluateRuleConditions(rule, context);
if (matches) {
const variant = selectVariant(rule, flag, context);
return {
value: rule.value !== void 0 ? rule.value : flag.defaultValue,
variant: variant || rule.variant,
reason: "rule_match",
metadata: {
ruleId: rule.id,
ruleName: rule.name,
priority: rule.priority
}
};
}
}
} catch (error) {
console.error(`[feature-flags] Error evaluating rules: ${error}`);
}
return null;
}
__name(evaluateRules, "evaluateRules");
function evaluateRuleConditions(rule, context) {
const { conditions } = rule;
if (!conditions || conditions.conditions.length === 0) {
return true;
}
const results = conditions.conditions.map((condition) => {
const attributeValue = getAttributeValue(context, condition.attribute);
return evaluateCondition(
attributeValue,
condition.operator,
condition.value
);
});
if (conditions.operator === "AND") {
return results.every((r) => r === true);
} else {
return results.some((r) => r === true);
}
}
__name(evaluateRuleConditions, "evaluateRuleConditions");
function getAttributeValue(context, attribute) {
const parts = attribute.split(".");
let value = context;
for (const part of parts) {
if (value && typeof value === "object") {
value = value[part];
} else {
return void 0;
}
}
return value;
}
__name(getAttributeValue, "getAttributeValue");
function checkRollout(flag, context) {
if (flag.rolloutPercentage === void 0 || flag.rolloutPercentage === 100) {
return null;
}
if (flag.rolloutPercentage === 0) {
return {
value: flag.defaultValue,
reason: "percentage_rollout",
metadata: {
percentage: 0,
included: false
}
};
}
const hashInput = `${context.userId}:${flag.key}`;
const included = calculateRollout(hashInput, flag.rolloutPercentage);
if (included) {
const variant = selectVariantByPercentage(flag, context);
return {
value: flag.defaultValue !== false ? flag.defaultValue : true,
variant,
reason: "percentage_rollout",
metadata: {
percentage: flag.rolloutPercentage,
included: true
}
};
}
return {
value: flag.type === "boolean" ? false : flag.defaultValue,
reason: "percentage_rollout",
metadata: {
percentage: flag.rolloutPercentage,
included: false
}
};
}
__name(checkRollout, "checkRollout");
function selectVariant(rule, flag, context) {
if (rule.variant) {
return rule.variant;
}
if (flag.variants && Object.keys(flag.variants).length > 0) {
return selectVariantByPercentage(flag, context);
}
return void 0;
}
__name(selectVariant, "selectVariant");
function selectVariantByPercentage(flag, context) {
if (!flag.variants || Object.keys(flag.variants).length === 0) {
return void 0;
}
const variants = Object.keys(flag.variants);
if (variants.length === 0) return void 0;
const hashInput = `${context.userId}:${flag.key}:variant`;
const hashValue = simpleHash(hashInput);
const variantIndex = hashValue % variants.length;
return variants[variantIndex];
}
__name(selectVariantByPercentage, "selectVariantByPercentage");
function simpleHash(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash = hash & hash;
}
return Math.abs(hash);
}
__name(simpleHash, "simpleHash");
async function evaluateFlagsBatch(keys, context, pluginContext) {
const { storage, config } = pluginContext;
const results = {};
const organizationId = config.multiTenant.enabled ? context.organizationId : void 0;
const flagPromises = keys.map((key) => storage.getFlag(key, organizationId));
const flags = await Promise.all(flagPromises);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
const flag = flags[i];
if (!flag) {
results[key] = {
value: void 0,
reason: "not_found"
};
continue;
}
try {
results[key] = await evaluateFlags(flag, context, pluginContext);
} catch (error) {
console.error(`[feature-flags] Error evaluating flag ${key}:`, error);
results[key] = {
value: flag.defaultValue,
reason: "error"
};
}
}
return results;
}
__name(evaluateFlagsBatch, "evaluateFlagsBatch");
export {
generateId,
parseJSON,
evaluateFlags,
evaluateFlagsBatch
};