Targeting Rules
Targeting rules let you enable flags for specific users based on attributes like email, user_id, country, or custom properties. This is more powerful than percentage rollouts because you can precisely control who sees a feature.
When to Use Targeting
Use targeting rules when you want to:
- Release features to specific users (beta testers, employees)
- Enable features based on user attributes (enterprise plans, specific regions)
- Gradually roll out to specific groups before general availability
- A/B test with specific user segments
Basic Usage
The evaluate() method evaluates targeting rules locally using cached flag data:
from featsync import Featsync, EvaluationContext
client = Featsync(api_key="fs_your_api_key")
# Evaluate with user contextenabled = client.evaluate( "new-checkout", EvaluationContext( user_id="user_123", email="john@acme.com", country="US", ),)
if enabled: show_new_checkout()Evaluation Context
The evaluation context contains user attributes that are compared against your targeting rules:
from featsync import EvaluationContext
context = EvaluationContext( # Standard attributes user_id="user_123", # User identifier email="john@acme.com", # User's email country="US", # ISO country code
# Custom attributes custom={ "plan": "enterprise", "company_size": 500, "beta_tester": True, "app_version": "2.1.0", })Standard Attributes
| Attribute | Type | Description | Example |
|---|---|---|---|
user_id | string | Unique user identifier | "user_123" |
email | string | User’s email address | "john@acme.com" |
country | string | ISO country code | "US", "GB", "DE" |
Custom Attributes
Pass any custom attributes in the custom dict:
context = EvaluationContext( user_id=current_user.id, email=current_user.email, custom={ "plan": "enterprise", # String "company_size": 500, # Number "beta_tester": True, # Boolean "app_version": "2.1.0", # Semver "roles": "admin,editor", # Comma-separated list },)
enabled = client.evaluate("enterprise-feature", context)In your targeting rules, access custom attributes with the custom. prefix:
custom.planequals"enterprise"custom.company_sizegreater than100custom.beta_testerequalstrue
Detailed Results
Use evaluate_with_details() to get metadata about why a flag was enabled/disabled:
from featsync import EvaluationContextfrom featsync.types import EvaluationReason
result = client.evaluate_with_details( "new-checkout", EvaluationContext( user_id="user_123", email="john@acme.com", ),)
print(result.enabled) # Trueprint(result.reason) # EvaluationReason.RULEprint(result.rule_name) # "Enterprise users"print(result.rule_id) # "rule_abc123"Evaluation Reasons
| Reason | Description |
|---|---|
DEFAULT | No rules matched, using flag’s default state |
RULE | A targeting rule matched |
SEGMENT | A segment rule matched (Pro) |
PERCENTAGE | Percentage rollout applied |
DISABLED | Flag is disabled in this environment |
NOT_FOUND | Flag does not exist |
ERROR | Evaluation error occurred |
Supported Operators
String Operators
| Operator | Description | Example |
|---|---|---|
equals | Exact match | email equals "admin@acme.com" |
not_equals | Not equal | country not_equals "CN" |
contains | Substring match | email contains "@acme.com" |
not_contains | No substring | email not_contains "test" |
starts_with | Prefix match | email starts_with "admin" |
ends_with | Suffix match | email ends_with "@acme.com" |
in_list | Value in list | country in ["US", "CA", "UK"] |
not_in_list | Not in list | country not_in ["CN", "RU"] |
matches_regex | Regex match | email matches ".*@(acme|corp)\\.com" |
Number Operators
| Operator | Description | Example |
|---|---|---|
eq | Equal | custom.age eq 18 |
neq | Not equal | custom.age neq 0 |
gt | Greater than | custom.age gt 18 |
gte | Greater or equal | custom.requests gte 100 |
lt | Less than | custom.age lt 65 |
lte | Less or equal | custom.requests lte 1000 |
Semver Operators
| Operator | Description | Example |
|---|---|---|
semver_eq | Version equal | custom.app_version semver_eq "2.0.0" |
semver_neq | Version not equal | custom.app_version semver_neq "1.0.0" |
semver_gt | Version greater | custom.app_version semver_gt "2.0.0" |
semver_gte | Version greater or equal | custom.app_version semver_gte "1.5.0" |
semver_lt | Version less | custom.app_version semver_lt "3.0.0" |
semver_lte | Version less or equal | custom.app_version semver_lte "2.9.9" |
AND/OR Logic
Rules can be combined with AND (all) or OR (any) logic:
AND (all): All conditions must match
IF email ends_with "@acme.com" AND country equals "US"THEN enableOR (any): At least one condition must match
IF email ends_with "@beta.com" OR custom.beta_tester equals trueTHEN enableEvaluation Priority
Rules are evaluated in priority order (lower number = higher priority):
- Priority 1: Beta testers rule
- Priority 2: Enterprise users rule
- Priority 3: US users rule
- Default: “Enable for everyone else” setting (if no rules match)
The first matching rule wins. If no rules match, the “Enable for everyone else” setting determines if the flag is enabled or disabled for that user.
Migration from is_enabled
If you’re currently using is_enabled(), you can gradually migrate to evaluate():
# Simple check (no targeting)enabled = client.is_enabled("new-checkout")# With targeting contextenabled = client.evaluate( "new-checkout", EvaluationContext( user_id=current_user.id, email=current_user.email, custom={"plan": current_user.plan}, ),)Both methods work for the same flags. Use is_enabled() for simple boolean checks, and evaluate() when you need targeting.
Best Practices
1. Include user_id for Consistency
Always include user_id to ensure users get consistent flag values:
# Good - user gets consistent experienceenabled = client.evaluate( "feature", EvaluationContext( user_id=current_user.id, # ... other attributes ),)2. Use Specific Attributes
Be specific with your attributes to make targeting rules clearer:
# Good - clear attributescustom={ "subscription_plan": "enterprise", "company_size": 500,}
# Avoid - vague attributescustom={ "type": "big", "level": "high",}3. Handle Errors Gracefully
evaluate() returns False on errors, but you can provide a default:
# Custom default valueenabled = client.evaluate("feature", context, default_value=True)4. Debug with evaluate_with_details
Use evaluate_with_details() during development to understand targeting:
import os
if os.environ.get("DEBUG"): result = client.evaluate_with_details("feature", context) print(f"Flag: {result.enabled}, Reason: {result.reason}")How Client-Side Evaluation Works
The SDK uses client-side evaluation for optimal performance:
- Fetch once: When you call
evaluate(), the SDK fetches all flags with their targeting rules - Cache locally: Flags and rules are cached (default: 5 minutes)
- Evaluate locally: Targeting rules are evaluated in your app, not on the server
- No extra calls: Subsequent
evaluate()calls use the cache
First call:App → SDK → API (fetches flags + rules) → Cache ↓ Local evaluation → Result
Subsequent calls:App → SDK → Cache ↓ Local evaluation → Result (no API call!)This approach minimizes API calls while providing instant flag evaluations.
Related
- Segments - Create reusable user groups (Pro)
- Targeting Concepts - Understanding targeting rules