Feature Flag Service Tutorial · Module 03 of 10

Advanced Targeting & Rules

Build powerful targeting rules: segment membership, user attributes, geographic location, custom conditions. Support rule operators (eq, contains, regex). Add rule priority to control evaluation order. By the end, users can target flags with surgical precision.

~4–5 hrsIntermediateRules focus
← Back to Module 03 overview
What You'll Have at the End

Definition of Done

  • POST /flags/:id/rules — create a targeting rule with priority.
  • PATCH /flags/:id/rules/:ruleId — update rule condition or priority.
  • DELETE /flags/:id/rules/:ruleId — delete a rule.
  • Rule operators: eq, ne, contains, regex, gt, lt.
  • Rule types: segment membership, user attributes, percentage-based.
  • Rule priority: lower number = evaluated first.
  • Evaluation respects rule priority and returns matching rule ID.
  • Test suite: rule evaluation with multiple rules and priorities.
The Steps

Build It

STEP 1

Create rule evaluation engine

Create src/utils/ruleEngine.ts:

import { EvaluationContext } from '../types/flag';

interface Rule {
  rule_type: 'segment' | 'user_attribute' | 'percentage';
  rule_value: Record;
}

export function evaluateRule(rule: Rule, context: EvaluationContext): boolean {
  if (rule.rule_type === 'segment') {
    return context.segment === rule.rule_value.segment;
  }

  if (rule.rule_type === 'user_attribute') {
    const attr = rule.rule_value.attribute;
    const operator = rule.rule_value.operator; // eq, ne, contains, regex, gt, lt
    const expected = rule.rule_value.value;
    const actual = context.attributes?.[attr];

    if (actual === undefined) return false;

    switch (operator) {
      case 'eq':
        return actual === expected;
      case 'ne':
        return actual !== expected;
      case 'contains':
        return String(actual).includes(String(expected));
      case 'regex':
        return new RegExp(expected).test(String(actual));
      case 'gt':
        return Number(actual) > Number(expected);
      case 'lt':
        return Number(actual) < Number(expected);
      default:
        return false;
    }
  }

  if (rule.rule_type === 'percentage') {
    const percentage = rule.rule_value.percentage || 0;
    const userId = context.user_id || context.session_id || '';
    const hash = hashString(userId);
    return hash % 100 < percentage;
  }

  return false;
}

function hashString(str: string): number {
  let hash = 0;
  for (let i = 0; i < str.length; i++) {
    const char = str.charCodeAt(i);
    hash = (hash << 5) - hash + char;
    hash = hash & hash; // Convert to 32-bit integer
  }
  return Math.abs(hash);
}