Rule Engine Architecture Overview
The OneTrack Rule Engine is a sophisticated event processing system that enables dynamic modification of tracking behavior without code changes. This document provides a technical overview of the architecture for internal development team members.
Purpose
The Rule Engine transforms incoming events through a configurable pipeline:
- 0 to N events output: A single input event can be suppressed (0 events), passed through unchanged (1 event), modified (1 transformed event), or spawn multiple new events (N events)
- Workspace isolation: Each workspace processes events independently with its own rule set
- Runtime configuration: Rules can be loaded remotely or embedded in
window.OneTrackRules
Directory Structure
The Rule Engine codebase is organized into clear functional boundaries across 70+ files:
lib/ruleEngine/
├── core/
│ └── ruleEngine.ts # Main RuleEngine singleton (1349 lines)
│
├── triggers/
│ ├── TriggerMatcher.ts # O(1) event-to-rule matching
│ ├── backgroundTriggerManager.ts # Lazy monitor initialization
│ └── monitors/ # Background trigger monitors
│ ├── scrollDepthMonitor.ts
│ ├── timeOnPageMonitor.ts
│ ├── postMessageMonitor.ts
│ └── domEventMonitor.ts
│
├── conditions/
│ └── conditionEvaluator.ts # AND/OR condition logic
│
├── actions/
│ ├── actionExecutor.ts # Action pipeline coordinator
│ └── handlers/ # Individual action handlers
│ ├── fireEventHandler.ts
│ ├── modifyEventHandler.ts
│ ├── suppressEventHandler.ts
│ └── suppressProvidersHandler.ts
│
├── variables/
│ └── variableResolver.ts # Multi-source variable resolution
│
├── debug/
│ ├── debugBroadcaster.ts # PostMessage-based debug system
│ ├── errorContextBuilder.ts # Error context creation
│ └── interfaces.ts # Debug message types
│
├── utils/
│ ├── pathResolver.ts # Dot notation + array access
│ ├── regexCache.ts # Compiled regex cache
│ └── objectUtils.ts # Deep clone utilities
│
├── interface.ts # TypeScript type definitions
├── constants.ts # Centralized constants
├── index.ts # Public API entry point
└── processEvent.ts # Wrapper function for RuleEngineProcessing Pipeline
The Rule Engine follows a strict processing order:
Key Design Patterns
Singleton Pattern
RuleEngine.getInstance() ensures a single instance manages all rule processing across the application.
Why? Centralizes execution tracking, variable state, and debug broadcasting.
Location: /lib/ruleEngine/core/ruleEngine.ts:146-151
Strategy Pattern
Different action types (fireEvent, suppressEvent, modifyEvent) are handled by dedicated handler classes.
Why? Separates concerns and makes adding new action types straightforward.
Location: /lib/ruleEngine/actions/handlers/
Observer Pattern
Debug system broadcasts execution details via PostMessage without coupling to specific debug tools.
Why? Zero performance impact when debug mode is disabled; any tool can listen to debug events.
Location: /lib/ruleEngine/debug/debugBroadcaster.ts
Indexing Pattern
TriggerMatcher builds Map-based indices for O(1) event type lookups instead of iterating all rules.
Why? Performance scales with number of matching rules, not total rules.
Location: /lib/ruleEngine/triggers/TriggerMatcher.ts:26-27
Entry Points
Primary Entry Point: processEvent()
// lib/ruleEngine/core/ruleEngine.ts:277-576
public processEvent(event: ClientEvent): ProcessedEvent[]What it does:
- Validates recursion depth (max 10 to prevent infinite loops)
- Clones event once per workspace for isolation
- Executes matching rules in priority order
- Consolidates results with proper routing
- Tracks execution for debug broadcasting
Key invariant: The input event parameter is NEVER modified. Each workspace receives a deep clone.
Initialization Entry Point: initializeRuleEngine()
// lib/ruleEngine/index.ts
export function initializeRuleEngine(
rules: Rule[],
workspaceId: string,
variables?: Variable[]
): voidWhat it does:
- Registers rules for a workspace
- Sorts rules by priority (lower = higher priority)
- Indexes triggers for fast matching
- Starts required background monitors lazily
Background Trigger Entry Point: processTriggeredEvent()
// lib/ruleEngine/core/ruleEngine.ts:578-633
public processTriggeredEvent(
trigger: Trigger,
eventData: Partial<ClientEvent>
): ProcessedEvent[]What it does:
- Creates synthetic event from trigger
- Processes through normal pipeline
- Filters out trigger event itself (only returns rule-generated events)
Workspace Isolation Design
Critical Design Decision: Events are deep-cloned once per workspace.
Why? Each workspace must process events independently:
- Workspace A might modify
event.data.price - Workspace B must see the original
event.data.price - Rule execution in one workspace cannot affect another
Performance trade-off: Deep cloning has cost, but ensures correctness. Optimization: Clone happens only once per workspace, not per rule.
Location: /lib/ruleEngine/core/ruleEngine.ts:329
Event Routing with suppressWorkspaces
The Rule Engine uses a sophisticated routing system:
interface ProcessedEvent extends ClientEvent {
routing?: {
workspaceId?: string[]; // Explicit workspace targets
suppressWorkspaces?: string[]; // Workspaces to exclude
externalProviders?: string[]; // Third-party providers
}
}Routing logic:
- Empty
workspaceIdarray = "all workspaces except those in suppressWorkspaces" - Populated
workspaceIdarray = "only these specific workspaces" suppressWorkspacesis accumulated across all rules that suppress/modify the event
Location: /lib/ruleEngine/core/ruleEngine.ts:515-536
Rule Registration and Priority
Rules are filtered and sorted during registration:
// lib/ruleEngine/core/ruleEngine.ts:156-175
const enabledRules = rules
.filter(rule => rule.enabled)
.sort((a, b) => (a.priority || 999) - (b.priority || 999));Priority semantics:
- Lower number = higher priority = executes first
- Default priority: 999
- Rules without priority execute last
Why it matters: For sequential event modification, execution order determines final state.
Execution Limits and Tracking
Rules can specify execution limits to prevent over-firing:
interface Rule {
executionLimit?: {
maxExecutions?: number; // Max times rule can fire
suppressOnLimitReached?: boolean; // Default: true
}
}Tracking scope: Page-level (resets on reload)
Behavior when limit reached:
- If
suppressOnLimitReached !== false: Suppress the triggering event - Otherwise: Event passes through unchanged
Location: /lib/ruleEngine/core/ruleEngine.ts:1227-1275
Infinite Loop Prevention
Problem: Rules can fire events that trigger other rules, creating potential infinite loops.
Solution: Recursion depth tracking with hard limit:
// lib/ruleEngine/core/ruleEngine.ts:127-128
private eventProcessingDepth: number = 0;
private readonly MAX_RECURSION_DEPTH = 10;Behavior: After 10 levels of nesting, returns original event unchanged and logs error.
Location: /lib/ruleEngine/core/ruleEngine.ts:283-293
Performance Characteristics
| Operation | Complexity | Notes |
|---|---|---|
| Trigger matching | O(1) | Map-based indexing by event type |
| Condition evaluation | O(n) | n = number of conditions, early exit on first failure |
| Variable resolution | O(1) | Map lookup |
| Event cloning | O(m) | m = event size, happens once per workspace |
| Regex matching | O(1) amortized | Compiled patterns cached |
Error Handling Philosophy
Strategy: Graceful degradation with comprehensive debugging
- Try-catch boundaries: Each rule execution is isolated
- Continue on error: One failing rule doesn't break the pipeline
- Debug broadcasting: All errors broadcast via PostMessage in debug mode
- Fallback behavior: On error, return original event unchanged
Location: /lib/ruleEngine/core/ruleEngine.ts:471-485 (action execution), 556-575 (top-level)
Type Safety
The Rule Engine is fully typed with TypeScript:
interface.ts: 506 lines of type definitions- No
anytypes in public APIs - Union types for config objects ensure type safety
- Type guards for runtime type checking
Example:
export type TriggerConfig =
| EventTriggerConfig
| ScrollDepthTriggerConfig
| TimeOnPageTriggerConfig
| PostMessageTriggerConfig
| DomReadyTriggerConfig
| WindowLoadTriggerConfig;Next Steps
For detailed information about specific subsystems, see:
- Core Processing Deep Dive - RuleEngine class internals
- TriggerMatcher Internals - O(1) lookup and background monitors
- ConditionEvaluator Internals - AND/OR logic and operators
- VariableResolver Internals - Multi-source variable resolution
- Debug System - PostMessage broadcasting architecture
- Performance Optimizations - Caching, indexing, and early exits