-
Notifications
You must be signed in to change notification settings - Fork 2
RuleManager
Welcome back! In Chapter 6: BucketingManager, 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 Chrome 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 desktop device.
- They must have visited your pricing page before (maybe indicated by a cookie).
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 "Location: Canada", "Device: Desktop", "Cookie 'visited_pricing': exists").
-
The Visitor's ID: The bouncer looks at the visitor's details (the visitor attributes and location properties provided to the SDK, like
{ country: 'Canada', device: 'desktop', cookies: { visited_pricing: 'true' } }
). - 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 visitor data and compares it against a set of predefined rules (often called Audience conditions or Segment rules) using various comparison methods (like "equals", "contains", "less than"). It then returns whether the visitor is a "match" or "no match".
Just like the bouncer doesn't decide the club's overall admission policy but just enforces the rules given, the RuleManager
doesn't act alone. You, as the SDK user, rarely interact with it directly.
Instead, the RuleManager
is called by the DataManager. Remember the DataManager.getBucketing
flow?
- DataManager checks its cache/store for a prior decision.
- If none exists, it retrieves the experiment/feature details, including the targeting rules.
-
Crucially, it asks the
RuleManager
: "Hey, does this visitor (with these properties) match these targeting rules?" by callingruleManager.isRuleMatched(visitorData, targetingRules)
. - The
RuleManager
(the bouncer) performs the checks and reports back:true
(match) orfalse
(no match). - If the
RuleManager
reportstrue
, then the DataManager proceeds to ask the BucketingManager to assign a variation. Iffalse
, the process stops, and the visitor doesn't see the experiment.
So, RuleManager
is the specialist that DataManager
consults to verify visitor eligibility based on targeting rules.
How does the "bouncer" actually read the rules and check the visitor's ID?
1. The Rule Structure (JSON)
The targeting rules you define in Convert are represented inside the SDK as a JSON object. This object has a specific structure using OR
and AND
logic, allowing for complex conditions.
Imagine rules like: "(Country is Canada AND Device is Desktop) OR (User is logged in)".
A simplified JSON representation might look like this (from packages/types/src/Rule.ts
):
{
"OR": [ // The top level is usually OR
{ // First main condition block (Country=Canada AND Device=Desktop)
"AND": [
{ // Conditions inside this AND must ALL be true
"OR_WHEN": [ // Conditions inside OR_WHEN: ANY can be true
{
"rule_type": "location", // Check visitor location data
"key": "country", // Specifically the 'country' property
"matching": {
"match_type": "equals", // Must be exactly equal to
"negated": false // (Not negated)
},
"value": "Canada" // The value to compare against
}
]
},
{
"OR_WHEN": [
{
"rule_type": "visitor", // Check visitor properties data
"key": "device",
"matching": { "match_type": "equals", "negated": false },
"value": "desktop"
}
]
}
]
},
{ // Second main condition block (User is logged in)
"AND": [
{
"OR_WHEN": [
{
"rule_type": "visitor",
"key": "isLoggedIn",
"matching": { "match_type": "equals", "negated": false },
"value": true
}
]
}
]
}
]
}
-
OR
: An array of conditions where at least oneAND
block must be true. -
AND
: An array of conditions where allOR_WHEN
blocks must be true. -
OR_WHEN
: An array of individual rule checks where at least one must be true. (Often contains just one rule). -
RuleElement
(the innermost object): Defines a single check (rule_type
,key
,matching
method,value
).
2. The Evaluation Process (isRuleMatched
)
When DataManager
calls ruleManager.isRuleMatched(visitorData, rules)
, the RuleManager
recursively navigates this JSON structure:
- It starts at the top
OR
level. It processes eachAND
block within theOR
array. - For an
AND
block, it processes eachOR_WHEN
block inside. All of these must evaluate totrue
for theAND
block to betrue
. If anyOR_WHEN
block isfalse
, theRuleManager
stops processing thisAND
block and moves to the next one in the parentOR
array. - For an
OR_WHEN
block, it processes each individualRuleElement
inside. At least one of these must evaluate totrue
for theOR_WHEN
block to betrue
. If it finds onetrue
rule, it stops processing thisOR_WHEN
block and considers ittrue
. - For a single
RuleElement
:- It identifies the
rule_type
(e.g., 'location', 'visitor', 'cookie'). - It looks up the corresponding value from the
visitorData
provided (e.g.,visitorData.locationProperties.country
orvisitorData.visitorProperties.device
). - It finds the comparison method specified in
matching.match_type
(e.g., 'equals', 'contains', 'less'). - It calls that comparison method, passing the visitor's value and the rule's
value
. - It considers
matching.negated
(e.g., "does not equal"). - It returns
true
orfalse
for this single rule check.
- It identifies the
If the RuleManager
finds any top-level AND
block that evaluates to true
(meaning all its conditions passed), the entire isRuleMatched
call returns true
. If it goes through all the AND
blocks and none evaluate to true
, it returns false
.
3. The Comparison Methods (Comparisons
utility)
The actual checking ("equals", "contains", "less than", etc.) happens using helper functions, typically found in a utility class like Comparisons
(from packages/utils/src/comparisons.ts
).
These methods take the visitor's data value and the rule's target value and perform the check:
// Simplified example from packages/utils/src/comparisons.ts
class Comparisons {
// Checks if the value contains the testAgainst string (case-insensitive)
static contains(
value: string | number,
testAgainst: string | number,
negation?: boolean // Is this a "does NOT contain" check?
): boolean {
let strValue = String(value).toLowerCase();
let strTestAgainst = String(testAgainst).toLowerCase();
let result = strValue.indexOf(strTestAgainst) !== -1; // Does it contain?
// Apply negation if needed
return negation ? !result : result;
}
// Checks if the value is exactly equal to testAgainst (case-insensitive)
static equals(
value: any, // Can be string, number, boolean, array...
testAgainst: string | number | boolean,
negation?: boolean
): boolean {
// ... logic to handle different types and compare ...
let result = /* comparison logic */;
return negation ? !result : result;
}
// Other methods like less, lessEqual, startsWith, endsWith, regexMatches...
}
The RuleManager
uses these comparison methods when processing each individual RuleElement
.
Simplified Sequence Diagram:
sequenceDiagram
participant DM as DataManager
participant RM as RuleManager
participant Comp as Comparisons Util
DM->>+RM: isRuleMatched(visitorData, rules)
Note over RM: Start processing rules JSON...
RM->>RM: Process OR[0].AND[0].OR_WHEN[0]... (Check Country)
RM->>Comp: equals(visitorData.country, 'Canada', false)
Comp-->>RM: true
RM->>RM: Process OR[0].AND[1].OR_WHEN[0]... (Check Device)
RM->>Comp: equals(visitorData.device, 'desktop', false)
Comp-->>RM: true
Note over RM: AND block is true. OR block is true.
RM-->>-DM: true (Visitor matches)
Let's look at simplified snippets from packages/rules/src/rule-manager.ts
.
1. The Constructor
Sets up the comparison methods to use and configuration options.
// File: packages/rules/src/rule-manager.ts (Simplified)
import { Comparisons as DEFAULT_COMPARISON_PROCESSOR } from '@convertcom/js-sdk-utils';
import { Config, RuleObject, RuleElement, RuleAnd, RuleOrWhen } from '@convertcom/js-sdk-types';
import { LogManagerInterface } from '@convertcom/js-sdk-logger';
import { ERROR_MESSAGES, MESSAGES, RuleError } from '@convertcom/js-sdk-enums';
const DEFAULT_KEYS_CASE_SENSITIVE = true;
export class RuleManager implements RuleManagerInterface {
// Holds the comparison functions (equals, contains, etc.)
private _comparisonProcessor: Record<string, any> = DEFAULT_COMPARISON_PROCESSOR;
private _loggerManager: LogManagerInterface | null;
private _keys_case_sensitive: boolean;
constructor(
config?: Config,
{ loggerManager }: { loggerManager?: LogManagerInterface } = {}
) {
this._loggerManager = loggerManager;
// Use comparison processor from config, or the default one
this._comparisonProcessor = config?.rules?.comparisonProcessor || DEFAULT_COMPARISON_PROCESSOR;
// Should rule keys ('country', 'device') be matched case-sensitively?
this._keys_case_sensitive = config?.rules?.keys_case_sensitive || DEFAULT_KEYS_CASE_SENSITIVE;
// ... logging ...
}
// ... methods ...
}
- It stores a reference to the
comparisonProcessor
(which contains methods likeequals
,contains
). This allows customizing the comparison logic if needed. - It also stores configuration like
keys_case_sensitive
.
2. The Main Entry Point (isRuleMatched
)
This method starts the recursive processing of the rule JSON.
// File: packages/rules/src/rule-manager.ts (Simplified)
// Inside RuleManager class:
isRuleMatched(
data: Record<string, any>, // Visitor data (properties, location, cookies)
ruleSet: RuleObject, // The rule JSON object
logEntry?: string // Optional label for logging
): boolean | RuleError { // Returns true, false, or an error
// ... logging ...
// Top OR level check
if (ruleSet?.OR && Array.isArray(ruleSet.OR)) {
// Loop through each AND block inside the top OR
for (const andBlock of ruleSet.OR) {
// === Process the AND block ===
const match = this._processAND(data, andBlock as RuleAnd);
// If this AND block is true, the whole rule set is true!
if (match === true) {
// ... logging ...
return true; // Short-circuit: Found a match!
}
// Handle potential errors during processing
if (Object.values(RuleError).includes(match as RuleError)) {
// ... logging ...
}
// ... logging for no match in this AND block ...
}
// If we finished the loop and didn't return true, no AND block matched
} else {
this._loggerManager?.warn?.(/* Rule structure invalid */);
}
// If no matching OR condition was found
return false;
}
- It expects the top level of the
ruleSet
to be anOR
array. - It iterates through the items in the
OR
array (which should beAND
blocks). - It calls
_processAND
for eachAND
block. - If any call to
_processAND
returnstrue
, this method immediately returnstrue
. - If the loop finishes without finding a match, it returns
false
.
3. Processing AND Blocks (_processAND
)
This helper checks if all conditions within an AND block are met.
// File: packages/rules/src/rule-manager.ts (Simplified)
// Inside RuleManager class:
private _processAND(
data: Record<string, any>,
rulesSubset: RuleAnd // An object like { "AND": [...] }
): boolean | RuleError {
if (rulesSubset?.AND && Array.isArray(rulesSubset.AND)) {
// Loop through each OR_WHEN block inside this AND
for (const orWhenBlock of rulesSubset.AND) {
// === Process the OR_WHEN block ===
const match = this._processORWHEN(data, orWhenBlock);
// If ANY OR_WHEN block is false, this AND block is false
if (match === false) {
return false; // Short-circuit: AND condition failed
}
// Propagate errors up
if (match !== true) return match;
}
// If loop finished, all OR_WHEN blocks were true
// ... logging ...
return true;
} else {
this._loggerManager?.warn?.(/* Rule structure invalid */);
}
return false;
}
- It expects an
AND
array containingOR_WHEN
blocks. - It iterates through the
OR_WHEN
blocks, calling_processORWHEN
for each. - If any call to
_processORWHEN
returnsfalse
, this method immediately returnsfalse
. - If the loop completes (meaning all
OR_WHEN
blocks weretrue
), it returnstrue
.
4. Processing OR_WHEN Blocks (_processORWHEN
)
This helper checks if at least one condition within an OR_WHEN block is met.
// File: packages/rules/src/rule-manager.ts (Simplified)
// Inside RuleManager class:
private _processORWHEN(
data: Record<string, any>,
rulesSubset: RuleOrWhen // An object like { "OR_WHEN": [...] }
): boolean | RuleError {
if (rulesSubset?.OR_WHEN && Array.isArray(rulesSubset.OR_WHEN)) {
// Loop through each individual RuleElement inside this OR_WHEN
for (const ruleItem of rulesSubset.OR_WHEN) {
// === Process the single rule ===
const match = this._processRuleItem(data, ruleItem);
// If ANY rule item is true, this OR_WHEN block is true
if (match === true) {
return true; // Short-circuit: OR condition met!
}
// Propagate errors up
if (match !== false) return match;
}
// If loop finished, no rule items were true
} else {
this._loggerManager?.warn?.(/* Rule structure invalid */);
}
return false;
}
- It expects an
OR_WHEN
array containing individualRuleElement
objects. - It iterates through the
RuleElement
s, calling_processRuleItem
for each. - If any call to
_processRuleItem
returnstrue
, this method immediately returnstrue
. - If the loop completes (meaning all rules were
false
), it returnsfalse
.
5. Processing a Single Rule (_processRuleItem
)
This is where the actual comparison happens using the visitor data.
// File: packages/rules/src/rule-manager.ts (Simplified)
// Inside RuleManager class:
private _processRuleItem(
data: Record<string, any>, // Visitor data
rule: RuleElement // Single rule {rule_type, key, matching, value}
): boolean | RuleError {
if (this.isValidRule(rule)) { // Basic check if rule object looks okay
try {
const matching = rule.matching.match_type; // e.g., 'equals', 'contains'
const negation = rule.matching.negated || false; // e.g., false
const ruleKey = rule.key; // e.g., 'country', 'device'
const ruleValue = rule.value; // e.g., 'Canada', 'desktop'
// Check if the required comparison method exists
if (this._comparisonProcessor[matching]) {
// === Get the Visitor's Value ===
// (Simplified logic - actual code determines which part of 'data'
// to use based on rule.rule_type: visitorProperties, locationProperties, etc.)
const visitorValue = data?.[ruleKey]; // e.g., data['country'] -> 'Canada'
if (visitorValue !== undefined) {
// === Perform the Comparison ===
return this._comparisonProcessor[matching](
visitorValue, // e.g., 'Canada'
ruleValue, // e.g., 'Canada'
negation // e.g., false
); // Returns true or false
} else {
// Visitor data did not contain the required key
return false;
}
} else {
this._loggerManager?.warn?.(/* Match type not supported */);
}
} catch (error) { /* ... logging ... */ }
} else { /* Rule not valid log */ }
// Default to false if anything goes wrong
return false;
}
- It extracts the details from the
rule
object (match_type
,key
,value
,negated
). - It retrieves the corresponding value from the visitor
data
based on therule.key
. - It calls the appropriate method from the
_comparisonProcessor
(e.g.,_comparisonProcessor['equals'](...)
). - It returns the boolean result of the comparison.
The RuleManager
is the SDK's engine for evaluating complex targeting rules. Like a diligent bouncer, it checks if a visitor's attributes (location, device, cookies, custom properties) match the conditions defined for an experiment, audience, or feature flag.
You've learned:
- Why rule evaluation is necessary for targeted experiences.
- The "bouncer" analogy for
RuleManager
. - How rules are structured using
OR
/AND
/OR_WHEN
logic in JSON. - How
RuleManager
processes these rules recursively usingisRuleMatched
. - How it uses comparison methods (like
equals
,contains
) from a utility class to perform individual checks. - That it's primarily used internally by the DataManager to determine visitor eligibility before bucketing.
Now we understand how the SDK gets configuration data (Chapter 1: ConvertSDK / Core), manages individual visitor sessions (Chapter 2: Context), handles experiences (Chapter 3: ExperienceManager) and features (Chapter 4: FeatureManager), stores data and orchestrates decisions (Chapter 5: DataManager), allocates traffic fairly (Chapter 6: BucketingManager), and evaluates targeting rules (Chapter 7: RuleManager).
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 in the next chapter: Chapter 8: ApiManager!
Copyrights © 2025 All Rights Reserved by Convert Insights, Inc.
Core Modules
- ConvertSDK / Core
- Context
- ExperienceManager
- FeatureManager
- DataManager
- BucketingManager
- RuleManager
- ApiManager
- EventManager
- Config / Types
Integration Guides