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
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
// 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
// Lines 156-158
private resolveConstant(config: ConstantVariableConfig): any {
return config.value;
}Config:
interface ConstantVariableConfig {
value: any;
}Example:
{
name: "currency",
type: "constant",
config: { value: "USD" }
}
// Resolves to: "USD"2. Data Layer Variable
Purpose: Access global variables on window object
// 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:
interface DataLayerVariableConfig {
path: string; // Dot notation path
defaultValue?: any; // Fallback if undefined
}Example:
{
name: "gtmDataLayer",
type: "dataLayer",
config: {
path: "dataLayer[0].page.category",
defaultValue: "unknown"
}
}
// Accesses: window.dataLayer[0].page.categoryPath resolution: Uses internal getValueFromPath() method (lines 174-198)
3. Lookup Table Variable
Purpose: Map input values to output values (like switch statement)
// 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:
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):
{
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:
// 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
// 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:
interface CustomJavaScriptVariableConfig {
code: string; // JavaScript code to execute
}Example:
{
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 objectevent- Current ClientEventcontext.variables- Map of all variables
Limitations:
- Cannot access outer scope
- Cannot use
importorrequire - Runs synchronously
5. Event Property Variable
Purpose: Extract value from event data
// Lines 292-298
private resolveEventProperty(
config: EventPropertyVariableConfig,
context: ExecutionContext
): any {
const value = resolvePath(config.path, context);
return value !== undefined ? value : config.defaultValue;
}Config:
interface EventPropertyVariableConfig {
path: string; // Dot notation path into event
defaultValue?: any; // Fallback if undefined
}Example:
{
name: "orderValue",
type: "eventProperty",
config: {
path: "data.order.total",
defaultValue: 0
}
}
// Accesses: context.event.data.order.total6. DOM Element Variable
Purpose: Extract data from DOM elements
// 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:
interface DomElementVariableConfig {
selector: string; // CSS selector
attribute: string; // "textContent", "innerHTML", or any HTML attribute
defaultValue?: any; // If element not found
}Examples:
// 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: ""
}
}7. Cookie Variable
Purpose: Read browser cookies
// 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:
interface CookieVariableConfig {
name: string; // Cookie name
defaultValue?: any; // If cookie not found
}Example:
{
name: "userConsent",
type: "cookie",
config: {
name: "cookie_consent",
defaultValue: "pending"
}
}URL decoding: Automatically decodes cookie values
8. LocalStorage Variable
Purpose: Read from browser localStorage
// 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:
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:
{
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
// 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:
interface UrlComponentVariableConfig {
component: "href" | "protocol" | "host" | "hostname" | "port" | "pathname" | "search" | "hash";
defaultValue?: any;
}Component descriptions:
| Component | Example | Description |
|---|---|---|
href | https://example.com/path?q=1#hash | Full URL |
protocol | https: | Protocol with colon |
host | example.com:8080 | Hostname + port |
hostname | example.com | Just hostname |
port | 8080 | Port number (or empty string) |
pathname | /path/to/page | Path without query/hash |
search | ?q=1&p=2 | Query string with ? |
hash | #section | Hash with # |
Example:
{
name: "currentPath",
type: "urlComponent",
config: {
component: "pathname",
defaultValue: "/"
}
}10. Timestamp Variable
Purpose: Generate current timestamp
// 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:
interface TimestampVariableConfig {
format?: "milliseconds" | "seconds" | "iso";
}Examples:
// 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
// Lines 390-393
private resolveTimeOnPage(_config: TimeOnPageVariableConfig): number {
// Use the shared utility function
return getTimeOnPage();
}Config:
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:
{
name: "sessionDuration",
type: "timeOnPage",
config: {},
outputType: "number" // Always number (seconds)
}12. Scroll Depth Variable
Purpose: Get current scroll position
// Lines 395-398
private resolveScrollDepth(config: ScrollDepthVariableConfig): number {
// Use the shared utility function with unit preference
return getScrollDepth(config.unit || "percentage");
}Config:
interface ScrollDepthVariableConfig {
unit?: "percentage" | "pixels"; // Default: "percentage"
}Implementation: Uses shared utility from /lib/utils/pageMetrics.ts
Examples:
// 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):
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:
- Variable reference (highest priority)
event.*prefix → Navigate into event objectpage.*prefix → Navigate into page system variable- Direct event path (fallback)
Array Access Support
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
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 Type | Default Output Type | Notes |
|---|---|---|
constant | Inferred from value | "hello" → string, 42 → number |
dataLayer | Requires explicit | Could be any type |
lookupTable | Inferred from outputs | All outputs same type preferred |
customJavaScript | Requires explicit | Return type unknown |
eventProperty | Requires explicit | Depends on event schema |
domElement | string | textContent or attribute always string |
cookie | string | Cookie values always strings |
localStorage | Inferred or explicit | Parsed JSON or string |
urlComponent | string | URL parts always strings |
timestamp | number or string | Depends on format |
timeOnPage | number | Always seconds |
scrollDepth | number | Always numeric |
No automatic coercion: Variable resolver returns actual type, caller must transform if needed
Variable Scoping
Three Scope Levels
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
workspaceVariablesMap - Available to all rules in workspace
- Resolved during context creation
- Medium priority (overrides global)
3. Global variables:
- Defined in
globalVariablesarray - Available to all workspaces
- Resolved during context creation
- Lowest priority
Resolution Order
During context creation (lines 1056-1064 in ruleEngine.ts):
// 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):
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:
// 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:
"User {{userName}} purchased {{productName}} for {{price}}"
// → "User John purchased Widget for 29.99"Implementation
// 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:
- Single variable reference:
""→ Returns actual value (preserves type) - String with interpolation:
"Total: "→ Returns string - Arrays: Processes each element
- Objects: Processes each value recursively
String Template Processing
// 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:
{
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:
{
type: "modifyEvent",
config: {
eventData: {
enrichedData: {
category: "{{pageCategory}}",
userSegment: "{{userSegment}}",
fullMessage: "{{userName}} on {{page.url}}"
}
}
}
}Key Takeaways for Developers
- 12 variable types cover most use cases - DOM, cookies, localStorage, event data, custom JS
- Graceful error handling - Always returns value or undefined, never throws
- Scoping hierarchy - Rule > Workspace > Global
- Template interpolation preserves types - Single
returns original type - Path resolution supports arrays -
data.items[0].nameworks - localStorage auto-parses JSON - Transparent JSON handling
- Custom JS is sandboxed - Uses
new Function(), has access to context - Lookup tables support regex - Flexible matching strategies
- DOM elements return strings - textContent or attributes
- Time/scroll variables use shared utils - Consistent calculation across system