Skip to content

Debug System Architecture

The Rule Engine debug system provides real-time visibility into rule processing through PostMessage broadcasting. This document explains the architecture, activation mechanisms, and message types.

Design Philosophy

Core principle: Zero performance impact when disabled.

Key requirements:

  1. No coupling to specific debug tools
  2. No performance overhead in production
  3. Complete visibility into rule processing
  4. Easy activation for debugging

Solution: PostMessage-based broadcasting with optional activation.

RuleEngineDebugBroadcaster Class

Location: /lib/ruleEngine/debug/debugBroadcaster.ts

Lines: 432 total

Pattern: Singleton

Class Structure

typescript
export class RuleEngineDebugBroadcaster {
  private static readonly DEBUG_NAMESPACE = "onetrack:ruleEngine:debug";
  private static readonly DEBUG_ENABLED_KEY = "onetrack.ruleEngine.debugMode";
  private static readonly DEBUG_URL_PARAM = "onetrack_debug";

  private static instance: RuleEngineDebugBroadcaster;
  private debugEnabled: boolean = false;

  // Singleton pattern
  public static getInstance(): RuleEngineDebugBroadcaster

  // Activation methods
  public enableDebugMode(): void
  public disableDebugMode(): void
  public isEnabled(): boolean

  // Broadcasting methods
  public broadcastError(error: RuleEngineError): void
  public broadcastRuleExecution(execution: RuleExecutionDebugInfo): void
  public broadcastConfiguration(config: RuleConfigurationDebugInfo): void
  public broadcastRuleEvaluation(evaluation: RuleEvaluationDebugInfo): void
  public broadcastTriggerEvaluation(trigger: TriggerEvaluationDebugInfo): void
  public broadcastConditionEvaluation(condition: ConditionEvaluationDebugInfo): void
  public broadcastInitialization(info: PixelInitializationDebugInfo): void
  public broadcastSuppression(info: ProviderSuppressionDebugInfo): void
  public broadcastDeduplication(info: PixelDeduplicationDebugInfo): void
  public broadcastDispatch(info: EventDispatchDebugInfo): void
}

PostMessage-Based Broadcasting

Why PostMessage?

Alternatives considered:

  1. Console logging: Clutters console, hard to filter, no programmatic access
  2. Custom events: Limited payload size, requires DOM
  3. Callback registration: Couples code to specific debug tools
  4. Global state object: Memory leak risk, no time-based filtering

PostMessage benefits:

  1. Decoupled: Any tool can listen (DevTools extension, iframe, parent window)
  2. Filtered: Namespace prevents collision
  3. Timestamped: Each message has timestamp
  4. Structured: JSON-serializable data
  5. Non-blocking: Fire-and-forget, no impact on rule processing

Broadcast Mechanism

typescript
// Lines 119-144
public broadcastError(error: RuleEngineError): void {
  if (!this.debugEnabled || typeof window === "undefined") return;

  try {
    const message: RuleEngineDebugMessage = {
      namespace: RuleEngineDebugBroadcaster.DEBUG_NAMESPACE,
      type: "error",
      timestamp: Date.now(),
      data: {
        ...error,
        // Ensure error object is serializable
        error: {
          message: error.error?.message || "Unknown error",
          stack: error.error?.stack || "",
          name: error.error?.name || "Error",
          code: error.error?.code
        }
      }
    };

    // Broadcast to all origins for maximum debug flexibility
    window.postMessage(message, "*");
  } catch (e) {
    // Silently fail to avoid debug errors affecting production
  }
}

Safety features:

  1. Early return if disabled: No overhead when not debugging
  2. Try-catch wrapper: Debug errors never break production
  3. Silent failure: Catch block is intentionally empty
  4. Serialization safety: Errors converted to plain objects

Message Structure

typescript
interface RuleEngineDebugMessage {
  namespace: "onetrack:ruleEngine:debug";  // Constant namespace
  type: MessageType;                        // Message category
  timestamp: number;                        // When message was sent
  data: any;                                // Message payload
}

type MessageType =
  | "error"           // Error occurred
  | "execution"       // Rule executed
  | "configuration"   // Rules registered
  | "evaluation"      // Rule evaluated (may not fire)
  | "trigger"         // Trigger matched/not matched
  | "condition"       // Condition passed/failed
  | "debugStatus"     // Debug mode enabled/disabled
  | "initialization"  // Pixel initialized
  | "suppression"     // Provider suppressed
  | "deduplication"   // Event deduplicated
  | "dispatch";       // Event dispatched

Debug Mode Activation

Three Activation Mechanisms

1. URL Parameter:

https://example.com/?onetrack_debug=true

2. localStorage:

javascript
localStorage.setItem("onetrack.ruleEngine.debugMode", "true")

3. Programmatic:

javascript
RuleEngineDebugBroadcaster.getInstance().enableDebugMode()

Activation Flow

typescript
// Lines 49-67
private initialize(): void {
  // Check if debug mode is enabled via localStorage or URL parameter
  this.debugEnabled = this.checkDebugEnabled();

  // If debug mode is enabled via URL, persist it asynchronously
  if (this.debugEnabled && this.isDebugInUrl()) {
    this.persistDebugParameter();
  }

  // Broadcast initial status
  if (this.debugEnabled) {
    this.broadcastDebugStatus(true);
    // Defer configuration broadcast to avoid circular dependency
    setTimeout(() => {
      this.broadcastAllConfigurations();
    }, 0);
  }
}

private checkDebugEnabled(): boolean {
  // Check localStorage
  if (typeof localStorage !== "undefined") {
    const stored = localStorage.getItem(DEBUG_ENABLED_KEY);
    if (stored === "true") return true;
  }

  // Check URL parameter
  if (this.isDebugInUrl()) {
    return true;
  }

  return false;
}

Initialization timing: Happens synchronously during RuleEngine construction

Configuration broadcast: Deferred to next tick to avoid circular dependency with RuleEngine

URL Parameter Persistence

Problem: ?onetrack_debug=true parameter lost on navigation

Solution: Persist parameter across page changes

typescript
// Lines 102-114
private persistDebugParameter(): void {
  // Fire and forget - we don't need to wait for this
  navigateWithParam({
    key: DEBUG_URL_PARAM,
    value: "true",
    reload: false,         // Don't reload page
    replace: true,         // Replace history state
    preserveHash: true,    // Keep URL hash
    skipIfExists: true     // Don't duplicate parameter
  }).catch(error => {
    console.error("[RuleEngineDebug] Failed to persist debug parameter:", error);
  });
}

Behavior:

  • Adds ?onetrack_debug=true to URL if not present
  • Uses history.replaceState() (no page reload)
  • Preserves existing query parameters and hash
  • Persists across client-side navigation

Broadcast Message Types

1. Error Messages

Type: "error"

When: Any error during rule processing

typescript
interface RuleEngineError {
  phase: "trigger" | "condition" | "action" | "variable" | "initialization";
  error: {
    message: string;
    stack: string;
    name: string;
    code?: string;
  };
  rule?: Rule;
  event?: ClientEvent;
  workspaceId?: string;
  executionContext?: any;
  executionTime?: number;
}

Built by: ErrorContextBuilder.buildErrorContext() (lines in errorContextBuilder.ts)

Example:

json
{
  "namespace": "onetrack:ruleEngine:debug",
  "type": "error",
  "timestamp": 1699564800000,
  "data": {
    "phase": "condition",
    "error": {
      "message": "Cannot read property 'price' of undefined",
      "stack": "...",
      "name": "TypeError"
    },
    "rule": { "id": "rule-123", "name": "Price Check" },
    "event": { "event": "pageView", "data": {} },
    "workspaceId": "ws-1"
  }
}

2. Rule Execution Messages

Type: "execution"

When: Rule completes execution (actions run)

typescript
interface RuleExecutionDebugInfo {
  ruleId: string;
  ruleName: string;
  workspaceId: string;
  eventType: EventType;
  eventId: string;
  result: "success" | "error";
  duration: number;  // Milliseconds
  details?: {
    actionsExecuted: Array<{
      type: ActionType;
      result: "success" | "error";
      output?: any;
    }>;
  };
}

Example:

json
{
  "namespace": "onetrack:ruleEngine:debug",
  "type": "execution",
  "timestamp": 1699564800000,
  "data": {
    "ruleId": "rule-123",
    "ruleName": "Fire Lead Event",
    "workspaceId": "ws-1",
    "eventType": "formSubmit",
    "eventId": "evt-456",
    "result": "success",
    "duration": 2.5,
    "details": {
      "actionsExecuted": [
        { "type": "fireEvent", "result": "success", "output": 1 }
      ]
    }
  }
}

3. Rule Evaluation Messages

Type: "evaluation"

When: Rule is evaluated (even if it doesn't fire)

typescript
interface RuleEvaluationDebugInfo {
  ruleId: string;
  ruleName: string;
  workspaceId: string;
  eventType: EventType;
  eventId: string;
  evaluated: boolean;
  fired: boolean;
  reason: "triggered" | "no_trigger_match" | "condition_failed" | "error";
  duration: number;
  triggerResults?: TriggerEvaluationDebugInfo[];
  conditionResults?: ConditionEvaluationDebugInfo[];
}

Purpose: Shows why rules didn't fire (extremely useful for debugging)

Example (rule didn't fire):

json
{
  "namespace": "onetrack:ruleEngine:debug",
  "type": "evaluation",
  "timestamp": 1699564800000,
  "data": {
    "ruleId": "rule-123",
    "ruleName": "High Value Purchase",
    "workspaceId": "ws-1",
    "eventType": "purchase",
    "eventId": "evt-789",
    "evaluated": true,
    "fired": false,
    "reason": "condition_failed",
    "duration": 1.2,
    "conditionResults": [
      {
        "field": "data.total",
        "operator": "greaterThan",
        "passed": false,
        "actualValue": 50,
        "expectedValue": 100
      }
    ]
  }
}

4. Trigger Evaluation Messages

Type: "trigger"

When: Each trigger is checked

typescript
interface TriggerEvaluationDebugInfo {
  ruleId: string;
  ruleName: string;
  workspaceId: string;
  triggerType: TriggerType;
  matched: boolean;
  eventType: EventType;
  config?: any;
}

Example:

json
{
  "namespace": "onetrack:ruleEngine:debug",
  "type": "trigger",
  "timestamp": 1699564800000,
  "data": {
    "ruleId": "rule-123",
    "ruleName": "Checkout Flow",
    "workspaceId": "ws-1",
    "triggerType": "oneTrackEvent",
    "matched": true,
    "eventType": "pageView",
    "config": { "eventType": ["pageView", "lead"] }
  }
}

5. Condition Evaluation Messages

Type: "condition"

When: Each condition is evaluated

typescript
interface ConditionEvaluationDebugInfo {
  ruleId: string;
  ruleName: string;
  workspaceId: string;
  field: string;
  operator: ComparisonOperator;
  passed: boolean;
  actualValue: any;
  expectedValue: any;
  negated?: boolean;
  transform?: ValueTransform | ValueTransform[];
}

Example:

json
{
  "namespace": "onetrack:ruleEngine:debug",
  "type": "condition",
  "timestamp": 1699564800000,
  "data": {
    "ruleId": "rule-123",
    "ruleName": "URL Filter",
    "workspaceId": "ws-1",
    "field": "page.url",
    "operator": "contains",
    "passed": true,
    "actualValue": "https://example.com/checkout",
    "expectedValue": "checkout",
    "transform": ["toLowerCase"]
  }
}

6. Configuration Messages

Type: "configuration"

When: Rules are registered or debug mode enabled

typescript
interface RuleConfigurationDebugInfo {
  workspaceId: string;
  rules: Rule[];           // Full rule objects
  variables?: Variable[];  // Workspace variables
  timestamp: number;
  source: "registerRules" | "registerVariables" | "debugInitialization";
}

Purpose: Debug tool can display all configured rules

Example:

json
{
  "namespace": "onetrack:ruleEngine:debug",
  "type": "configuration",
  "timestamp": 1699564800000,
  "data": {
    "workspaceId": "ws-1",
    "rules": [
      {
        "id": "rule-123",
        "name": "Fire Lead Event",
        "enabled": true,
        "triggers": [...],
        "conditionGroups": [...],
        "actions": [...]
      }
    ],
    "variables": [],
    "source": "registerRules"
  }
}

7. Debug Status Messages

Type: "debugStatus"

When: Debug mode enabled or disabled

typescript
interface DebugStatusInfo {
  enabled: boolean;
}

Purpose: Debug tools can react to mode changes

Example:

json
{
  "namespace": "onetrack:ruleEngine:debug",
  "type": "debugStatus",
  "timestamp": 1699564800000,
  "data": {
    "enabled": true
  }
}

Error Context Building

ErrorContextBuilder

Location: /lib/ruleEngine/debug/errorContextBuilder.ts

Purpose: Enrich errors with context for debugging

typescript
export class ErrorContextBuilder {
  public static buildErrorContext(
    error: Error,
    phase: ErrorPhase,
    context: {
      rule?: Rule;
      event?: ClientEvent;
      workspaceId?: string;
      executionContext?: ExecutionContext;
      trigger?: Trigger;
      executionTime?: number;
    }
  ): RuleEngineError {
    return {
      phase,
      error: {
        message: error.message,
        stack: error.stack || "",
        name: error.name,
        code: (error as any).code
      },
      rule: context.rule,
      event: context.event,
      workspaceId: context.workspaceId,
      executionContext: this.sanitizeContext(context.executionContext),
      trigger: context.trigger,
      executionTime: context.executionTime
    };
  }

  private static sanitizeContext(context?: ExecutionContext): any {
    if (!context) return undefined;

    // Remove circular references and deep objects
    return {
      workspaceId: context.workspaceId,
      variables: this.sanitizeVariables(context.variables),
      metrics: context.metrics
      // Event excluded to avoid duplication
    };
  }

  public static generateEventId(): string {
    return `evt_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
  }
}

Sanitization: Prevents circular references and excessive payloads

No Performance Impact When Disabled

Early Return Pattern

Every broadcast method:

typescript
public broadcastSomething(data: any): void {
  // FIRST LINE: Check if enabled
  if (!this.debugEnabled || typeof window === "undefined") return;

  // Only executes if debug enabled
  // ...
}

Performance:

  • If disabled: Single boolean check (~0.0001ms)
  • No object creation
  • No serialization
  • No PostMessage call

Try-Catch Safety

All broadcast methods wrapped:

typescript
try {
  const message: RuleEngineDebugMessage = { /* ... */ };
  window.postMessage(message, "*");
} catch (e) {
  // Silently fail to avoid debug errors affecting production
}

Guarantees: Debug system never throws errors that break rule processing

Listening to Debug Messages

In Browser Console

javascript
// Listen to all debug messages
window.addEventListener('message', (event) => {
  if (event.data.namespace === 'onetrack:ruleEngine:debug') {
    console.log(event.data.type, event.data.data);
  }
});

In Debug Tool / Extension

javascript
class RuleEngineDebugger {
  constructor() {
    window.addEventListener('message', this.handleMessage.bind(this));
  }

  handleMessage(event) {
    if (event.data.namespace !== 'onetrack:ruleEngine:debug') return;

    switch (event.data.type) {
      case 'configuration':
        this.displayRules(event.data.data.rules);
        break;

      case 'evaluation':
        this.logEvaluation(event.data.data);
        break;

      case 'error':
        this.showError(event.data.data);
        break;

      // ... handle other message types
    }
  }

  displayRules(rules) {
    // Update UI with registered rules
  }

  logEvaluation(evaluation) {
    // Show rule evaluation timeline
  }

  showError(error) {
    // Display error with context
  }
}

// Initialize debugger
const debugger = new RuleEngineDebugger();

Filtering by Workspace

javascript
window.addEventListener('message', (event) => {
  if (event.data.namespace !== 'onetrack:ruleEngine:debug') return;

  const data = event.data.data;

  // Only show messages for specific workspace
  if (data.workspaceId === 'ws-1') {
    console.log(`[WS-1] ${event.data.type}:`, data);
  }
});

Filtering by Time Range

javascript
const debugMessages = [];

window.addEventListener('message', (event) => {
  if (event.data.namespace === 'onetrack:ruleEngine:debug') {
    debugMessages.push(event.data);
  }
});

// Get messages from last 5 seconds
function getRecentMessages(seconds = 5) {
  const cutoff = Date.now() - (seconds * 1000);
  return debugMessages.filter(msg => msg.timestamp >= cutoff);
}

Debug Mode Management

Enable Debug Mode

typescript
// Lines 169-186
public enableDebugMode(): void {
  this.debugEnabled = true;

  // Persist to localStorage
  if (typeof localStorage !== "undefined") {
    localStorage.setItem(DEBUG_ENABLED_KEY, "true");
  }

  // Add to URL if not already present
  if (!this.isDebugInUrl()) {
    this.persistDebugParameter();
  }

  this.broadcastDebugStatus(true);

  // Broadcast all existing configurations
  this.broadcastAllConfigurations();
}

Broadcasts configuration: Existing rules sent to any new listeners

Disable Debug Mode

typescript
// Lines 191-213
public disableDebugMode(): void {
  this.debugEnabled = false;

  // Remove from localStorage
  if (typeof localStorage !== "undefined") {
    localStorage.removeItem(DEBUG_ENABLED_KEY);
  }

  // Remove from URL if present
  if (this.isDebugInUrl()) {
    navigateWithParam({
      key: DEBUG_URL_PARAM,
      value: undefined,  // Removes parameter
      reload: false,
      replace: true,
      preserveHash: true
    });
  }

  this.broadcastDebugStatus(false);
}

Cleanup: Removes from all persistence mechanisms

Broadcast All Configurations

typescript
// Lines 406-431
private broadcastAllConfigurations(): void {
  try {
    // Import RuleEngine here to avoid circular dependency
    const { RuleEngine } = require("../core/ruleEngine");
    const ruleEngine = RuleEngine.getInstance();

    // Get all workspace configurations
    if (ruleEngine && ruleEngine.getAllWorkspaceConfigurations) {
      const configurations = ruleEngine.getAllWorkspaceConfigurations();

      configurations.forEach((config: any) => {
        const configInfo: RuleConfigurationDebugInfo = {
          workspaceId: config.workspaceId,
          rules: config.rules,
          variables: config.variables,
          timestamp: Date.now(),
          source: "debugInitialization"
        };

        this.broadcastConfiguration(configInfo);
      });
    }
  } catch (e) {
    // Silently fail - RuleEngine might not be available in test environments
  }
}

Circular dependency handling: Dynamic require() inside method

Use case: When debug mode enabled after rules registered

Key Takeaways for Developers

  1. Zero overhead when disabled - Single boolean check per broadcast
  2. PostMessage decouples debug tool - Any listener can receive messages
  3. Three activation methods - URL param, localStorage, programmatic
  4. Silent failure philosophy - Debug errors never break production
  5. Namespace prevents collision - onetrack:ruleEngine:debug prefix
  6. Rich message types - 10+ message types for complete visibility
  7. Error context building - Sanitizes circular refs, provides full context
  8. URL param persists - Survives navigation via replaceState
  9. Configuration on enable - Broadcasts existing rules when mode activated
  10. Singleton pattern - Single broadcaster across all workspaces