Skip to content

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 RuleEngine

Processing 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()

typescript
// lib/ruleEngine/core/ruleEngine.ts:277-576
public processEvent(event: ClientEvent): ProcessedEvent[]

What it does:

  1. Validates recursion depth (max 10 to prevent infinite loops)
  2. Clones event once per workspace for isolation
  3. Executes matching rules in priority order
  4. Consolidates results with proper routing
  5. Tracks execution for debug broadcasting

Key invariant: The input event parameter is NEVER modified. Each workspace receives a deep clone.

Initialization Entry Point: initializeRuleEngine()

typescript
// lib/ruleEngine/index.ts
export function initializeRuleEngine(
  rules: Rule[],
  workspaceId: string,
  variables?: Variable[]
): void

What it does:

  1. Registers rules for a workspace
  2. Sorts rules by priority (lower = higher priority)
  3. Indexes triggers for fast matching
  4. Starts required background monitors lazily

Background Trigger Entry Point: processTriggeredEvent()

typescript
// lib/ruleEngine/core/ruleEngine.ts:578-633
public processTriggeredEvent(
  trigger: Trigger,
  eventData: Partial<ClientEvent>
): ProcessedEvent[]

What it does:

  1. Creates synthetic event from trigger
  2. Processes through normal pipeline
  3. 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:

typescript
interface ProcessedEvent extends ClientEvent {
  routing?: {
    workspaceId?: string[];           // Explicit workspace targets
    suppressWorkspaces?: string[];    // Workspaces to exclude
    externalProviders?: string[];     // Third-party providers
  }
}

Routing logic:

  • Empty workspaceId array = "all workspaces except those in suppressWorkspaces"
  • Populated workspaceId array = "only these specific workspaces"
  • suppressWorkspaces is 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:

typescript
// 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:

typescript
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:

typescript
// 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

OperationComplexityNotes
Trigger matchingO(1)Map-based indexing by event type
Condition evaluationO(n)n = number of conditions, early exit on first failure
Variable resolutionO(1)Map lookup
Event cloningO(m)m = event size, happens once per workspace
Regex matchingO(1) amortizedCompiled patterns cached

Error Handling Philosophy

Strategy: Graceful degradation with comprehensive debugging

  1. Try-catch boundaries: Each rule execution is isolated
  2. Continue on error: One failing rule doesn't break the pipeline
  3. Debug broadcasting: All errors broadcast via PostMessage in debug mode
  4. 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 any types in public APIs
  • Union types for config objects ensure type safety
  • Type guards for runtime type checking

Example:

typescript
export type TriggerConfig =
  | EventTriggerConfig
  | ScrollDepthTriggerConfig
  | TimeOnPageTriggerConfig
  | PostMessageTriggerConfig
  | DomReadyTriggerConfig
  | WindowLoadTriggerConfig;

Next Steps

For detailed information about specific subsystems, see: