-
Notifications
You must be signed in to change notification settings - Fork 0
rule evaluation
In Bucketing Algorithm, we learned how the SDK uses a "Sorting Hat" approach (hashing) to consistently assign visitors to different variations based on traffic percentages, after they've already qualified for an experiment.
But how does the SDK decide if a visitor qualifies in the first place? What if you only want to run your headline A/B test for visitors from Canada who are using the Safari browser? How does the SDK check these conditions?
Imagine you're running a special promotion on your website, but only for visitors who meet specific criteria:
- They must be visiting from Canada.
- They must be using a mobile device.
- They must have visited your pricing page before (maybe indicated by a custom attribute).
When a visitor arrives, the SDK needs a way to check if they satisfy all these conditions before even considering showing them the promotion (or bucketing them into an experiment variation). How does it perform this check against potentially complex sets of rules?
Think of the RuleManager as the bouncer at an exclusive club. Before anyone gets in (i.e., qualifies for an experiment or feature), the bouncer checks their credentials against the entry rules.
- The Rules: The bouncer has a list of requirements (the targeting rules you set up in your Convert project, like "country: Canada", "device: mobile", "custom attribute 'has_visited_pricing': true").
-
The Visitor's ID: The bouncer looks at the visitor's details (the visitor attributes provided to the SDK, such as country, device, and custom variable values, plus any default segments set via
context.setDefaultSegments). - The Check: The bouncer compares the visitor's details against the rules one by one. Do they match?
- The Decision: The bouncer decides: "Yes, this visitor meets all the criteria, let them in!" or "No, they don't meet the criteria, they can't enter."
The RuleManager does exactly this: it takes a data map of [String: String] attributes and compares it against a set of predefined rules using various comparison methods (like "equals", "contains", "less than"). It then returns whether the visitor is a "match" or "no match".
RuleManager (Sources/ConvertSwiftSDKCore/Rules/RuleManager.swift) is a stateless internal struct — it owns no mutable state; its only stored property is an injected Logger for fail-closed WARN lines.
You, as the SDK user, never interact with RuleManager directly. It is called internally by ExperienceManager during selectVariation.
Remember the selectVariation flow?
-
ExperienceManagerreads the experience's audience and location rule lists from theProjectConfigsnapshot. -
RuleAdapterflattens the generated 3-level rule graphs (OR → AND → OR_WHEN → leaf) into the flat[RuleGroup]modelRuleManagerconsumes. -
ExperienceManagerasks theRuleManager: "Does this visitor (with these attributes) match these targeting rules?" by callingruleManager.evaluate(rules:against:). - The
RuleManager(the bouncer) performs the checks and reports back:true(match) orfalse(no match). An empty rule set returnsfalse(fail-closed). - If the
RuleManagerreportstruefor both the audience rules AND the location rules, thenExperienceManagerproceeds to ask theBucketingManagerto assign a variation. Iffalse, the process stops, and the visitor doesn't see the experiment.
So, RuleManager is the specialist that ExperienceManager consults to verify visitor eligibility based on targeting rules.
1. The Rule Structure (Flat Model)
The targeting rules you define in Convert arrive in the serving config as a hierarchical object (OR → AND → OR_WHEN → leaf). The RuleAdapter flattens this into the [RuleGroup] model RuleManager consumes:
-
Outer array (
[RuleGroup]): An OR — the overall evaluation passes if ANYRuleGrouppasses. -
RuleGroup.conditions: An AND — ALL conditions inside a group must pass for that group to pass.
A single RuleCondition carries:
-
key: String— the visitor-data key to look up (e.g."country") -
matchType: String— the comparison operator (e.g."equals","contains") -
value: String?— the rule's target value to compare against -
negation: Bool— whether to invert the result
The original source had a 4-level hierarchy (OR → AND → OR_WHEN → RuleElement); the flat model collapses OR_WHEN leaves into the AND-group's conditions list — a single AND-block with two OR_WHEN leaves becomes one RuleGroup with two RuleConditions.
2. The Evaluation Process (evaluate(rules:against:))
When ExperienceManager calls ruleManager.evaluate(rules: audienceRules, against: attributes):
- If
rulesis empty → returnsfalseand logs WARN (fail-closed, AC3). Eligibility is NEVER vacuous-true. - It iterates each
RuleGroupin the outer OR array. The first group that passes causes the whole evaluation to returntrue. - For each
RuleGroup, it iterates theconditionswithallSatisfy. If any condition fails, the group fails and the next group in the OR is tried. - For each
RuleCondition:- It looks up
attributes[condition.key]— this is aString?(nil when the key is absent). The nil flows straight intoComparisons.evaluatefor EVERY operator —RuleManagernever short-circuits on a missing key, becauseexists/doesNotExistrely on nil reaching the comparator to compute presence (AC2). - It dispatches to
Comparisons.evaluate(matchType:value:testAgainst:negated:logger:). - Returns the boolean result.
- It looks up
3. The Comparison Methods (Comparisons)
The actual checking ("equals", "contains", "less than", etc.) happens in Comparisons (Sources/ConvertSwiftSDKCore/Rules/Comparisons.swift), a stateless internal enum with pure static functions. The dispatch is a static [String: Comparator] table (rather than a switch) to avoid SwiftLint cyclomatic_complexity flags.
Every comparator is parity-verified line-by-line against the live JavaScript SDK comparisons.ts. Seven subtle behaviours are pinned by test vectors in ComparisonsTests.swift:
-
equals/equalsNumber/matchesare case-insensitive (lowercase both sides).equalsNumberandmatchesare aliases for string equality — NOT numeric comparison. -
containsis case-insensitive;value(the visitor side) is the HAYSTACK andtestAgainst(the rule side) is the NEEDLE; an empty / whitespace-only needle always returnstrue. -
startsWith/endsWithare case-insensitive — lowercase both sides. -
less/lessEqualare type-guarded: a non-numeric operand on either side yieldsfalse. The numeric gate matches JSisNumeric(accepts comma-grouped thousands; rejects sci-notation, hex,Infinity, leading-plus). -
regexMatchesis case-insensitive (JS'i'flag viaNSRegularExpression.Options.caseInsensitive); an invalid pattern yieldsfalse. -
isInis asymmetric: thevaluecandidates are NOT lowercased; thetestAgainstallow-list IS lowercased. Split usescomponents(separatedBy:)(notsplit) to preserve JS empty-segment semantics. -
exists/not_existstreat the empty string as absent;doesNotExistis an alias fornot_exists.
Available comparison operators (wire matchType strings):
"equals" "equalsNumber" "matches"
"less" "lessEqual"
"contains" "isIn"
"startsWith" "endsWith" "regexMatches"
"exists" "not_exists" "doesNotExist"
Negation inverts the result of every real operator at the end of evaluate (via applyNegation). An unknown operator fails closed: it logs WARN and returns false directly, WITHOUT negation applied (JS parity).
4. The RuleAdapter
Before RuleManager can evaluate, RuleAdapter (Sources/ConvertSwiftSDKCore/Rules/RuleAdapter.swift) flattens the generated rule graphs. The generated types (RuleObjectAudience / RuleObject) are structurally OR → AND → OR_WHEN → leaf. The adapter:
- One
RuleGroupper AND-block. - That block's
OR_WHENleaves are extracted asRuleConditions. - An absent
ORyields an empty[RuleGroup](whichRuleManagertreats as fail-closed).
Leaf families currently handled: GenericTextMatchRule-backed text rules (city, campaign, keyword, medium, region, url, page_tag, etc.) and CountryMatchRule. Every other family degrades to a fail-closed condition (matchType = "", absent from Comparisons.comparators) — evaluates to false without ever matching wrong-positive.
Simplified Sequence Diagram:
sequenceDiagram
participant EM as ExperienceManager
participant RA as RuleAdapter
participant RM as RuleManager
participant Comp as Comparisons
EM->>+RA: flatten(audience.rules)
RA-->>-EM: [RuleGroup] (flat OR-of-AND)
EM->>+RM: evaluate(rules: [RuleGroup], against: attributes)
Note over RM: Start outer OR loop...
RM->>RM: group = rules[0]; allSatisfy(conditions)
RM->>RM: condition: key="country", matchType="equals", value="Canada"
RM->>Comp: evaluate("equals", value: "Canada", testAgainst: "Canada", negated: false)
Comp-->>RM: true
RM->>RM: condition: key="devices", matchType="equals", value="mobile"
RM->>Comp: evaluate("equals", value: "mobile", testAgainst: "mobile", negated: false)
Comp-->>RM: true
Note over RM: All conditions in group passed — outer OR is true.
RM-->>-EM: true (Visitor matches)
The RuleManager Constructor
// internal — not part of the public API
internal struct RuleManager {
private let logger: Logger
init(logger: Logger) { self.logger = logger }
}- The
loggeris the only injected dependency — for the fail-closed WARN lines (empty outer set / empty group). - There is NO
keys_case_sensitiveflag on the iOSRuleManageritself; attribute key lookup is direct dictionary subscripting (attributes[condition.key]), and theruleKeysCaseSensitiveconfiguration option is propagated at theConvertConfigurationlevel for future use. - There is NO pluggable comparator constructor parameter — the iOS SDK uses
Comparisonsdirectly via a static dispatch table, rather than passing the comparison processor as a constructor argument.
The Main Entry Point (evaluate(rules:against:))
func evaluate(rules: [RuleGroup], against attributes: [String: String]) -> Bool- Empty
rules→false+ WARN (fail-closed, AC3). - Iterates
rules; first group whoseevaluate(group:against:)returnstrue→ returnstrue. - If all groups fail →
false.
Processing AND Groups (evaluate(group:against:))
- Empty
group.conditions→false+ WARN (fail-closed, AC3). -
group.conditions.allSatisfy { evaluate(condition:against:) }— short-circuits on the first failing condition.
Processing a Single Condition (evaluate(condition:against:))
- Looks up
attributes[condition.key]asString?. - Calls
Comparisons.evaluate(matchType:value:testAgainst:negated:logger:)with the (possibly nil) value.
The RuleManager is the iOS SDK's engine for evaluating complex targeting rules. Like a diligent bouncer, it checks if a visitor's attributes (location, device, custom properties set via createContext or setDefaultSegments) match the conditions defined for an experiment, audience, or feature flag.
Key takeaways:
- Why rule evaluation is necessary for targeted experiences.
- The "bouncer" analogy for
RuleManager. - How rules are structured — generated 3-level graphs flattened by
RuleAdapterinto a flat[RuleGroup]OR-of-AND model. - How
RuleManagerprocesses these rules throughevaluate(rules:against:)andevaluate(group:against:). - How it uses comparison methods from the
Comparisonsenum (parity-verified against the JS SDK) to perform individual checks. - The fail-closed behaviour on empty rule sets and unknown operators (AC2, AC3).
- That it's primarily used internally by
ExperienceManagerto determine visitor eligibility before bucketing.
Now we understand how the SDK gets configuration data (Architecture), manages individual visitor sessions (Context), handles experiences (Experiences and Variations) and features (Feature Management), stores data and orchestrates decisions (Data Management), allocates traffic fairly (Bucketing Algorithm), and evaluates targeting rules (Rule Evaluation & Targeting).
But how does the SDK actually get the project configuration data (including those rules) from the Convert servers in the first place? And how does it send tracking data back? That's the job of the communicator module.
Let's explore how the SDK interacts with external APIs next: API Management!
Copyrights © 2026 All Rights Reserved by Convert Insights, Inc.
Getting Started
iOS SDK
- Quickstart
- Installation
- Initialization
- Configuration
- Return Types & Models
- Code Examples
- Offline Behavior
- Tracking Control
- App Privacy & Data Collection
- Objective-C Interop
Core Concepts
- Experiences & Variations
- Feature Flags
- Bucketing Algorithm
- Rule Evaluation
- Segments
- Data Management
- Event System
- API Communication
How-To Guides
- Running Experiences
- Running Features
- Tracking Conversions
- Visitor Context
- Persistent Storage
- Troubleshooting
Contributing