Skip to content

VariableResolver Internals

The VariableResolver provides dynamic values from 12 different source types including DOM elements, cookies, localStorage, event properties, and custom JavaScript. This document explains the resolution flow and type-specific implementations.

VariableResolver Class

Location: /lib/ruleEngine/variables/variableResolver.ts

Lines: 399 total

Purpose: Resolve variables from multiple sources and provide template variable interpolation for dynamic content generation.

Class Structure

typescript
export class VariableResolver {
  // Main resolution method
  public resolve(variable: Variable, context: ExecutionContext): any

  // Template processing
  public processTemplate(template: any, context: ExecutionContext): any

  // Type-specific resolvers (12 types)
  private resolveConstant(config: ConstantVariableConfig): any
  private resolveDataLayer(config: DataLayerVariableConfig): any
  private resolveLookupTable(config: LookupTableVariableConfig, context: ExecutionContext): any
  private resolveCustomJavaScript(config: CustomJavaScriptVariableConfig, context: ExecutionContext): any
  private resolveEventProperty(config: EventPropertyVariableConfig, context: ExecutionContext): any
  private resolveDomElement(config: DomElementVariableConfig): any
  private resolveCookie(config: CookieVariableConfig): any
  private resolveLocalStorage(config: LocalStorageVariableConfig): any
  private resolveUrlComponent(config: UrlComponentVariableConfig): any
  private resolveTimestamp(config: TimestampVariableConfig): any
  private resolveTimeOnPage(config: TimeOnPageVariableConfig): number
  private resolveScrollDepth(config: ScrollDepthVariableConfig): number
}

Variable Resolution Flow

Main Resolve Method

typescript
// Lines 36-92
public resolve(variable: Variable, context: ExecutionContext): any {
  try {
    switch (variable.type) {
      case "constant":
        return this.resolveConstant(variable.config as ConstantVariableConfig);

      case "dataLayer":
        return this.resolveDataLayer(variable.config as DataLayerVariableConfig);

      case "lookupTable":
        return this.resolveLookupTable(
          variable.config as LookupTableVariableConfig,
          context
        );

      case "customJavaScript":
        return this.resolveCustomJavaScript(
          variable.config as CustomJavaScriptVariableConfig,
          context
        );

      case "eventProperty":
        return this.resolveEventProperty(
          variable.config as EventPropertyVariableConfig,
          context
        );

      case "domElement":
        return this.resolveDomElement(variable.config as DomElementVariableConfig);

      case "cookie":
        return this.resolveCookie(variable.config as CookieVariableConfig);

      case "localStorage":
        return this.resolveLocalStorage(variable.config as LocalStorageVariableConfig);

      case "urlComponent":
        return this.resolveUrlComponent(variable.config as UrlComponentVariableConfig);

      case "timestamp":
        return this.resolveTimestamp(variable.config as TimestampVariableConfig);

      case "timeOnPage":
        return this.resolveTimeOnPage(variable.config as TimeOnPageVariableConfig);

      case "scrollDepth":
        return this.resolveScrollDepth(variable.config as ScrollDepthVariableConfig);

      default:
        console.warn(`Unknown variable type: ${variable.type}`);
        return undefined;
    }
  } catch (error) {
    console.error(`Error resolving variable ${variable.name}:`, error);
    return undefined;
  }
}

Error handling strategy: Catch errors and return undefined (graceful degradation)

Type safety: Union type VariableConfig ensures config matches variable type

Type-Specific Resolvers

1. Constant Variable

Simplest type: Just returns configured value

typescript
// Lines 156-158
private resolveConstant(config: ConstantVariableConfig): any {
  return config.value;
}

Config:

typescript
interface ConstantVariableConfig {
  value: any;
}

Example:

typescript
{
  name: "currency",
  type: "constant",
  config: { value: "USD" }
}
// Resolves to: "USD"

2. Data Layer Variable

Purpose: Access global variables on window object

typescript
// Lines 160-168
private resolveDataLayer(config: DataLayerVariableConfig): any {
  if (typeof window === "undefined") {
    return config.defaultValue;
  }

  // Direct path resolution on window object
  const value = this.getValueFromPath(config.path, window);
  return value !== undefined ? value : config.defaultValue;
}

Config:

typescript
interface DataLayerVariableConfig {
  path: string;           // Dot notation path
  defaultValue?: any;     // Fallback if undefined
}

Example:

typescript
{
  name: "gtmDataLayer",
  type: "dataLayer",
  config: {
    path: "dataLayer[0].page.category",
    defaultValue: "unknown"
  }
}
// Accesses: window.dataLayer[0].page.category

Path resolution: Uses internal getValueFromPath() method (lines 174-198)

3. Lookup Table Variable

Purpose: Map input values to output values (like switch statement)

typescript
// Lines 200-248
private resolveLookupTable(
  config: LookupTableVariableConfig,
  context: ExecutionContext
): any {
  console.log(`Resolving lookup table with inputType: ${config.inputType}`);

  let inputValue: any;

  // Resolve input based on inputType
  switch (config.inputType) {
    case "literal":
      inputValue = config.input;
      break;

    case "eventPath":
      inputValue = resolvePath(config.input, context);
      break;

    case "variable":
      inputValue = context.variables.get(config.input);
      if (inputValue === undefined) {
        console.warn(`Variable '${config.input}' not found in context`);
      }
      break;

    default:
      console.error(`Unknown inputType: ${config.inputType}`);
      inputValue = config.input; // Fallback to literal
  }

  // Find matching entry in lookup table
  for (const entry of config.lookupTable) {
    const matches = this.matchesLookupEntry(
      inputValue,
      entry.input,
      config.matchType
    );

    if (matches) {
      console.log(`Match found! Returning: ${entry.output}`);
      return entry.output;
    }
  }

  console.log(`No match found. Returning default: ${config.defaultValue}`);
  return config.defaultValue;
}

Config:

typescript
interface LookupTableVariableConfig {
  input: string;                              // What to match against
  inputType: "literal" | "eventPath" | "variable";  // How to interpret input
  defaultValue?: any;                         // If no match found
  lookupTable: Array<{
    input: any;                               // Value to match
    output: any;                              // Value to return
  }>;
  matchType?: "exact" | "regex" | "contains"; // Match strategy
}

Example (URL to category mapping):

typescript
{
  name: "pageCategory",
  type: "lookupTable",
  config: {
    input: "page.url",
    inputType: "variable",
    matchType: "contains",
    lookupTable: [
      { input: "/products/", output: "Product" },
      { input: "/blog/", output: "Blog" },
      { input: "/about", output: "About" }
    ],
    defaultValue: "Other"
  }
}

Match strategies:

typescript
// Lines 250-276
private matchesLookupEntry(
  value: any,
  pattern: any,
  matchType?: "exact" | "regex" | "contains"
): boolean {
  const strValue = String(value);
  const strPattern = String(pattern);

  switch (matchType || "exact") {
    case "exact":
      return strValue === strPattern;

    case "contains":
      return strValue.includes(strPattern);

    case "regex":
      try {
        const regex = new RegExp(strPattern);
        return regex.test(strValue);
      } catch (e) {
        return false;
      }

    default:
      return false;
  }
}

4. Custom JavaScript Variable

Purpose: Execute arbitrary JavaScript code to compute value

Security: Uses new Function() sandboxing

typescript
// Lines 278-290
private resolveCustomJavaScript(
  config: CustomJavaScriptVariableConfig,
  context: ExecutionContext
): any {
  try {
    // Create a sandboxed function with context
    const func = new Function("context", "event", config.code);
    return func(context, context.event);
  } catch (error) {
    console.error("Error executing custom JavaScript:", error);
    return undefined;
  }
}

Config:

typescript
interface CustomJavaScriptVariableConfig {
  code: string;  // JavaScript code to execute
}

Example:

typescript
{
  name: "totalPrice",
  type: "customJavaScript",
  config: {
    code: `
      const items = event.data.items || [];
      return items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
    `
  }
}

Available in code:

  • context - Full ExecutionContext object
  • event - Current ClientEvent
  • context.variables - Map of all variables

Limitations:

  • Cannot access outer scope
  • Cannot use import or require
  • Runs synchronously

5. Event Property Variable

Purpose: Extract value from event data

typescript
// Lines 292-298
private resolveEventProperty(
  config: EventPropertyVariableConfig,
  context: ExecutionContext
): any {
  const value = resolvePath(config.path, context);
  return value !== undefined ? value : config.defaultValue;
}

Config:

typescript
interface EventPropertyVariableConfig {
  path: string;         // Dot notation path into event
  defaultValue?: any;   // Fallback if undefined
}

Example:

typescript
{
  name: "orderValue",
  type: "eventProperty",
  config: {
    path: "data.order.total",
    defaultValue: 0
  }
}
// Accesses: context.event.data.order.total

6. DOM Element Variable

Purpose: Extract data from DOM elements

typescript
// Lines 300-315
private resolveDomElement(config: DomElementVariableConfig): any {
  if (typeof document === "undefined") {
    return config.defaultValue;
  }

  const element = document.querySelector(config.selector);
  if (!element) {
    return config.defaultValue;
  }

  if (config.attribute === "textContent") {
    return element.textContent;
  }

  return element.getAttribute(config.attribute) || config.defaultValue;
}

Config:

typescript
interface DomElementVariableConfig {
  selector: string;       // CSS selector
  attribute: string;      // "textContent", "innerHTML", or any HTML attribute
  defaultValue?: any;     // If element not found
}

Examples:

typescript
// Extract page title
{
  name: "pageTitle",
  type: "domElement",
  config: {
    selector: "h1.page-title",
    attribute: "textContent",
    defaultValue: "Untitled"
  }
}

// Extract meta description
{
  name: "metaDescription",
  type: "domElement",
  config: {
    selector: 'meta[name="description"]',
    attribute: "content",
    defaultValue: ""
  }
}

Purpose: Read browser cookies

typescript
// Lines 317-331
private resolveCookie(config: CookieVariableConfig): any {
  if (typeof document === "undefined") {
    return config.defaultValue;
  }

  const cookies = document.cookie.split(';');
  for (const cookie of cookies) {
    const [name, value] = cookie.trim().split('=');
    if (name === config.name) {
      return decodeURIComponent(value);
    }
  }

  return config.defaultValue;
}

Config:

typescript
interface CookieVariableConfig {
  name: string;          // Cookie name
  defaultValue?: any;    // If cookie not found
}

Example:

typescript
{
  name: "userConsent",
  type: "cookie",
  config: {
    name: "cookie_consent",
    defaultValue: "pending"
  }
}

URL decoding: Automatically decodes cookie values

8. LocalStorage Variable

Purpose: Read from browser localStorage

typescript
// Lines 333-353
private resolveLocalStorage(config: LocalStorageVariableConfig): any {
  if (typeof window === "undefined" || !window.localStorage) {
    return config.defaultValue;
  }

  try {
    const value = localStorage.getItem(config.key);
    if (value === null) {
      return config.defaultValue;
    }

    // Try to parse as JSON
    try {
      return JSON.parse(value);
    } catch {
      return value;  // Return as string if not JSON
    }
  } catch (error) {
    return config.defaultValue;
  }
}

Config:

typescript
interface LocalStorageVariableConfig {
  key: string;           // localStorage key
  defaultValue?: any;    // If key not found
}

Features:

  • Automatically parses JSON values
  • Returns raw string if not valid JSON
  • Handles localStorage exceptions (quota exceeded, etc.)

Example:

typescript
{
  name: "userPreferences",
  type: "localStorage",
  config: {
    key: "user_prefs",
    defaultValue: { theme: "light" }
  }
}
// If stored as: '{"theme":"dark","lang":"en"}'
// Resolves to: { theme: "dark", lang: "en" }

9. URL Component Variable

Purpose: Extract parts of current URL

typescript
// Lines 355-373
private resolveUrlComponent(config: UrlComponentVariableConfig): any {
  if (typeof window === "undefined" || !window.location) {
    return config.defaultValue;
  }

  const location = window.location;
  const componentMap: Record<string, any> = {
    href: location.href,
    protocol: location.protocol,
    host: location.host,
    hostname: location.hostname,
    port: location.port || "",
    pathname: location.pathname,
    search: location.search,
    hash: location.hash
  };

  return componentMap[config.component] || config.defaultValue;
}

Config:

typescript
interface UrlComponentVariableConfig {
  component: "href" | "protocol" | "host" | "hostname" | "port" | "pathname" | "search" | "hash";
  defaultValue?: any;
}

Component descriptions:

ComponentExampleDescription
hrefhttps://example.com/path?q=1#hashFull URL
protocolhttps:Protocol with colon
hostexample.com:8080Hostname + port
hostnameexample.comJust hostname
port8080Port number (or empty string)
pathname/path/to/pagePath without query/hash
search?q=1&p=2Query string with ?
hash#sectionHash with #

Example:

typescript
{
  name: "currentPath",
  type: "urlComponent",
  config: {
    component: "pathname",
    defaultValue: "/"
  }
}

10. Timestamp Variable

Purpose: Generate current timestamp

typescript
// Lines 375-388
private resolveTimestamp(config: TimestampVariableConfig): any {
  const now = Date.now();

  switch (config.format || "milliseconds") {
    case "milliseconds":
      return now;
    case "seconds":
      return Math.floor(now / 1000);
    case "iso":
      return new Date(now).toISOString();
    default:
      return now;
  }
}

Config:

typescript
interface TimestampVariableConfig {
  format?: "milliseconds" | "seconds" | "iso";
}

Examples:

typescript
// Milliseconds (default)
{ type: "timestamp", config: {} }
// → 1699564800000

// Seconds
{ type: "timestamp", config: { format: "seconds" } }
// → 1699564800

// ISO string
{ type: "timestamp", config: { format: "iso" } }
// → "2024-11-09T16:00:00.000Z"

11. Time On Page Variable

Purpose: Get time user has spent on page

typescript
// Lines 390-393
private resolveTimeOnPage(_config: TimeOnPageVariableConfig): number {
  // Use the shared utility function
  return getTimeOnPage();
}

Config:

typescript
interface TimeOnPageVariableConfig {
  // No config needed - always returns seconds
}

Implementation: Uses shared utility from /lib/utils/pageMetrics.ts

Returns: Number of seconds since page load (always in seconds)

Example:

typescript
{
  name: "sessionDuration",
  type: "timeOnPage",
  config: {},
  outputType: "number"  // Always number (seconds)
}

12. Scroll Depth Variable

Purpose: Get current scroll position

typescript
// Lines 395-398
private resolveScrollDepth(config: ScrollDepthVariableConfig): number {
  // Use the shared utility function with unit preference
  return getScrollDepth(config.unit || "percentage");
}

Config:

typescript
interface ScrollDepthVariableConfig {
  unit?: "percentage" | "pixels";  // Default: "percentage"
}

Implementation: Uses shared utility from /lib/utils/pageMetrics.ts

Examples:

typescript
// Percentage (default)
{
  name: "scrollPercent",
  type: "scrollDepth",
  config: { unit: "percentage" }
}
// → 75 (75% scrolled)

// Pixels
{
  name: "scrollPixels",
  type: "scrollDepth",
  config: { unit: "pixels" }
}
// → 1200 (1200px from top)

Path Resolution

Centralized Path Resolver

Location: /lib/ruleEngine/utils/pathResolver.ts

Used by multiple variable types (eventProperty, dataLayer, lookupTable):

typescript
export function resolvePath(path: string, context: ExecutionContext): any {
  // 1. Check variables first
  if (context.variables.has(path)) {
    return context.variables.get(path);
  }

  // 2. Handle special prefixes
  if (path.startsWith("event.")) {
    return getValueFromPath(path.substring(6), context.event);
  }

  if (path.startsWith("page.")) {
    const pageVar = context.variables.get("page");
    if (pageVar) {
      return getValueFromPath(path.substring(5), pageVar);
    }
  }

  // 3. Try direct path on event
  return getValueFromPath(path, context.event);
}

Resolution order:

  1. Variable reference (highest priority)
  2. event.* prefix → Navigate into event object
  3. page.* prefix → Navigate into page system variable
  4. Direct event path (fallback)

Array Access Support

typescript
function getValueFromPath(path: string, obj: any): any {
  const parts = path.split(".");
  let current = obj;

  for (const part of parts) {
    if (current === null || current === undefined) {
      return undefined;
    }

    // Handle array access like items[0]
    const arrayMatch = part.match(/^(.+)\[(\d+)\]$/);
    if (arrayMatch) {
      current = current[arrayMatch[1]];
      if (Array.isArray(current)) {
        current = current[parseInt(arrayMatch[2], 10)];
      }
    } else {
      current = current[part];
    }
  }

  return current;
}

Supported patterns:

  • Simple path: data.order.total
  • Array index: data.items[0]
  • Nested arrays: data.users[1].orders[0].items[2].name
  • Mixed: data.cart.items[0].price

Output Type Coercion

Variable Output Types

typescript
export type ValueType = "string" | "number" | "boolean" | "array" | "object";

interface Variable {
  name: string;
  type: VariableType;
  config: VariableConfig;
  outputType?: ValueType;  // Expected output type
}

Purpose: UI can show only compatible operators for variable's output type

Default output types by variable type:

Variable TypeDefault Output TypeNotes
constantInferred from value"hello" → string, 42 → number
dataLayerRequires explicitCould be any type
lookupTableInferred from outputsAll outputs same type preferred
customJavaScriptRequires explicitReturn type unknown
eventPropertyRequires explicitDepends on event schema
domElementstringtextContent or attribute always string
cookiestringCookie values always strings
localStorageInferred or explicitParsed JSON or string
urlComponentstringURL parts always strings
timestampnumber or stringDepends on format
timeOnPagenumberAlways seconds
scrollDepthnumberAlways numeric

No automatic coercion: Variable resolver returns actual type, caller must transform if needed

Variable Scoping

Three Scope Levels

typescript
interface Variable {
  scope?: "rule" | "workspace" | "global";
}

1. Rule-scoped variables:

  • Defined in rule.variables[]
  • Only available within that rule
  • Resolved during rule execution
  • Highest priority (overrides workspace/global)

2. Workspace-scoped variables:

  • Defined in workspaceVariables Map
  • Available to all rules in workspace
  • Resolved during context creation
  • Medium priority (overrides global)

3. Global variables:

  • Defined in globalVariables array
  • Available to all workspaces
  • Resolved during context creation
  • Lowest priority

Resolution Order

During context creation (lines 1056-1064 in ruleEngine.ts):

typescript
// Load workspace and global variables
const workspaceVars = this.workspaceVariables.get(workspaceId) || [];
const allVariables = [...this.globalVariables, ...workspaceVars];

// Resolve all variables into context
for (const variable of allVariables) {
  const value = this.variableResolver.resolve(variable, context);
  context.variables.set(variable.name, value);
}

During rule execution (lines 1103-1128 in ruleEngine.ts):

typescript
private resolveRuleVariables(variables: Variable[], context: ExecutionContext): void {
  for (const variable of variables) {
    if (variable.scope === "rule" || !variable.scope) {
      const value = this.variableResolver.resolve(variable, context);
      context.variables.set(variable.name, value);  // Overwrites if exists
    }
  }
}

Precedence example:

typescript
// Global variable
globalVariables = [
  { name: "currency", type: "constant", config: { value: "USD" } }
]

// Workspace variable (overrides global)
workspaceVariables.set("workspace-1", [
  { name: "currency", type: "constant", config: { value: "EUR" } }
])

// Rule variable (overrides workspace)
rule.variables = [
  { name: "currency", type: "constant", config: { value: "GBP" } }
]

// Final value in context: "GBP"

Template Variable Interpolation

Purpose

Generate dynamic strings using variable values:

typescript
"User {{userName}} purchased {{productName}} for {{price}}"
// → "User John purchased Widget for 29.99"

Implementation

typescript
// Lines 97-152
public processTemplate(template: any, context: ExecutionContext): any {
  if (typeof template === "string") {
    // Check if entire string is single variable reference
    const singleVarMatch = template.match(/^\{\{([^}]+)\}\}$/);
    if (singleVarMatch) {
      const varPath = singleVarMatch[1].trim();

      // Check if it's a variable
      if (context.variables.has(varPath)) {
        return context.variables.get(varPath);
      }

      // Try to resolve as a path
      const value = resolvePath(varPath, context);
      if (value !== undefined) {
        return value;
      }
    }

    // Otherwise process as string template
    return this.processStringTemplate(template, context);
  } else if (Array.isArray(template)) {
    return template.map(item => this.processTemplate(item, context));
  } else if (template && typeof template === "object") {
    const result: any = {};
    for (const [key, value] of Object.entries(template)) {
      result[key] = this.processTemplate(value, context);
    }
    return result;
  }
  return template;
}

Handles:

  1. Single variable reference: "" → Returns actual value (preserves type)
  2. String with interpolation: "Total: " → Returns string
  3. Arrays: Processes each element
  4. Objects: Processes each value recursively

String Template Processing

typescript
// Lines 133-152
private processStringTemplate(template: string, context: ExecutionContext): string {
  return template.replace(/\{\{([^}]+)\}\}/g, (match, varPath) => {
    const trimmedPath = varPath.trim();

    // Check if it's a variable
    if (context.variables.has(trimmedPath)) {
      const value = context.variables.get(trimmedPath);
      // Don't convert objects to "[object Object]"
      return typeof value === 'object' ? JSON.stringify(value) : String(value || "");
    }

    // Try to resolve as a path
    const value = resolvePath(trimmedPath, context);
    if (value !== undefined) {
      return typeof value === 'object' ? JSON.stringify(value) : String(value);
    }

    return match; // Return original if not found
  });
}

Object handling: Converts objects to JSON strings (avoids [object Object])

Fallback: If variable not found, keeps original

Template Usage Examples

In fireEvent action:

typescript
{
  type: "fireEvent",
  config: {
    eventType: "customEvent",
    eventData: {
      message: "User {{userName}} completed {{action}}",
      value: "{{totalPrice}}",  // Preserves number type
      metadata: {
        timestamp: "{{timestamp}}",
        page: "{{page.url}}"
      }
    }
  }
}

In modifyEvent action:

typescript
{
  type: "modifyEvent",
  config: {
    eventData: {
      enrichedData: {
        category: "{{pageCategory}}",
        userSegment: "{{userSegment}}",
        fullMessage: "{{userName}} on {{page.url}}"
      }
    }
  }
}

Key Takeaways for Developers

  1. 12 variable types cover most use cases - DOM, cookies, localStorage, event data, custom JS
  2. Graceful error handling - Always returns value or undefined, never throws
  3. Scoping hierarchy - Rule > Workspace > Global
  4. Template interpolation preserves types - Single returns original type
  5. Path resolution supports arrays - data.items[0].name works
  6. localStorage auto-parses JSON - Transparent JSON handling
  7. Custom JS is sandboxed - Uses new Function(), has access to context
  8. Lookup tables support regex - Flexible matching strategies
  9. DOM elements return strings - textContent or attributes
  10. Time/scroll variables use shared utils - Consistent calculation across system