Skip to content

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 context
enabled = 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

AttributeTypeDescriptionExample
user_idstringUnique user identifier"user_123"
emailstringUser’s email address"john@acme.com"
countrystringISO 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.plan equals "enterprise"
  • custom.company_size greater than 100
  • custom.beta_tester equals true

Detailed Results

Use evaluate_with_details() to get metadata about why a flag was enabled/disabled:

from featsync import EvaluationContext
from featsync.types import EvaluationReason
result = client.evaluate_with_details(
"new-checkout",
EvaluationContext(
user_id="user_123",
email="john@acme.com",
),
)
print(result.enabled) # True
print(result.reason) # EvaluationReason.RULE
print(result.rule_name) # "Enterprise users"
print(result.rule_id) # "rule_abc123"

Evaluation Reasons

ReasonDescription
DEFAULTNo rules matched, using flag’s default state
RULEA targeting rule matched
SEGMENTA segment rule matched (Pro)
PERCENTAGEPercentage rollout applied
DISABLEDFlag is disabled in this environment
NOT_FOUNDFlag does not exist
ERROREvaluation error occurred

Supported Operators

String Operators

OperatorDescriptionExample
equalsExact matchemail equals "admin@acme.com"
not_equalsNot equalcountry not_equals "CN"
containsSubstring matchemail contains "@acme.com"
not_containsNo substringemail not_contains "test"
starts_withPrefix matchemail starts_with "admin"
ends_withSuffix matchemail ends_with "@acme.com"
in_listValue in listcountry in ["US", "CA", "UK"]
not_in_listNot in listcountry not_in ["CN", "RU"]
matches_regexRegex matchemail matches ".*@(acme|corp)\\.com"

Number Operators

OperatorDescriptionExample
eqEqualcustom.age eq 18
neqNot equalcustom.age neq 0
gtGreater thancustom.age gt 18
gteGreater or equalcustom.requests gte 100
ltLess thancustom.age lt 65
lteLess or equalcustom.requests lte 1000

Semver Operators

OperatorDescriptionExample
semver_eqVersion equalcustom.app_version semver_eq "2.0.0"
semver_neqVersion not equalcustom.app_version semver_neq "1.0.0"
semver_gtVersion greatercustom.app_version semver_gt "2.0.0"
semver_gteVersion greater or equalcustom.app_version semver_gte "1.5.0"
semver_ltVersion lesscustom.app_version semver_lt "3.0.0"
semver_lteVersion less or equalcustom.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 enable

OR (any): At least one condition must match

IF email ends_with "@beta.com" OR custom.beta_tester equals true
THEN enable

Evaluation Priority

Rules are evaluated in priority order (lower number = higher priority):

  1. Priority 1: Beta testers rule
  2. Priority 2: Enterprise users rule
  3. Priority 3: US users rule
  4. 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")

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 experience
enabled = 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 attributes
custom={
"subscription_plan": "enterprise",
"company_size": 500,
}
# Avoid - vague attributes
custom={
"type": "big",
"level": "high",
}

3. Handle Errors Gracefully

evaluate() returns False on errors, but you can provide a default:

# Custom default value
enabled = 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:

  1. Fetch once: When you call evaluate(), the SDK fetches all flags with their targeting rules
  2. Cache locally: Flags and rules are cached (default: 5 minutes)
  3. Evaluate locally: Targeting rules are evaluated in your app, not on the server
  4. 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.