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:
- No coupling to specific debug tools
- No performance overhead in production
- Complete visibility into rule processing
- 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
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:
- Console logging: Clutters console, hard to filter, no programmatic access
- Custom events: Limited payload size, requires DOM
- Callback registration: Couples code to specific debug tools
- Global state object: Memory leak risk, no time-based filtering
PostMessage benefits:
- Decoupled: Any tool can listen (DevTools extension, iframe, parent window)
- Filtered: Namespace prevents collision
- Timestamped: Each message has timestamp
- Structured: JSON-serializable data
- Non-blocking: Fire-and-forget, no impact on rule processing
Broadcast Mechanism
// 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:
- Early return if disabled: No overhead when not debugging
- Try-catch wrapper: Debug errors never break production
- Silent failure: Catch block is intentionally empty
- Serialization safety: Errors converted to plain objects
Message Structure
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 dispatchedDebug Mode Activation
Three Activation Mechanisms
1. URL Parameter:
https://example.com/?onetrack_debug=true2. localStorage:
localStorage.setItem("onetrack.ruleEngine.debugMode", "true")3. Programmatic:
RuleEngineDebugBroadcaster.getInstance().enableDebugMode()Activation Flow
// 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
// 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=trueto 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
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:
{
"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)
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:
{
"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)
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):
{
"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
interface TriggerEvaluationDebugInfo {
ruleId: string;
ruleName: string;
workspaceId: string;
triggerType: TriggerType;
matched: boolean;
eventType: EventType;
config?: any;
}Example:
{
"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
interface ConditionEvaluationDebugInfo {
ruleId: string;
ruleName: string;
workspaceId: string;
field: string;
operator: ComparisonOperator;
passed: boolean;
actualValue: any;
expectedValue: any;
negated?: boolean;
transform?: ValueTransform | ValueTransform[];
}Example:
{
"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
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:
{
"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
interface DebugStatusInfo {
enabled: boolean;
}Purpose: Debug tools can react to mode changes
Example:
{
"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
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:
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:
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
// 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
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
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
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
// 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
// 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
// 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
- Zero overhead when disabled - Single boolean check per broadcast
- PostMessage decouples debug tool - Any listener can receive messages
- Three activation methods - URL param, localStorage, programmatic
- Silent failure philosophy - Debug errors never break production
- Namespace prevents collision -
onetrack:ruleEngine:debugprefix - Rich message types - 10+ message types for complete visibility
- Error context building - Sanitizes circular refs, provides full context
- URL param persists - Survives navigation via replaceState
- Configuration on enable - Broadcasts existing rules when mode activated
- Singleton pattern - Single broadcaster across all workspaces