Pendo Data Layer | OpsBlu Docs

Pendo Data Layer

Set up the data layer for Pendo — structure events, define variables, and ensure consistent tracking data.

Data Layer Setup

Pendo's data layer consists of visitor (user) and account (organization) metadata that enriches your analytics and enables powerful segmentation, targeting, and reporting capabilities.

Visitor Data Structure

Visitor data represents individual users in your application. The visitor.id is the only required field, but additional metadata unlocks Pendo's full potential.

Required Fields

pendo.initialize({
  visitor: {
    id: 'USER_ID'  // Required: Unique identifier for the user
  }
});
pendo.initialize({
  visitor: {
    id: 'user-12345',              // Required: Unique user ID
    email: 'user@example.com',     // Recommended: User email
    full_name: 'John Doe',         // Recommended: Display name
    role: 'admin'                  // Recommended: User role/type
  }
});

Comprehensive Visitor Data

pendo.initialize({
  visitor: {
    // Identity
    id: 'user-12345',
    email: 'john.doe@example.com',
    username: 'johndoe',
    full_name: 'John Doe',
    first_name: 'John',
    last_name: 'Doe',

    // Profile
    role: 'admin',
    title: 'Product Manager',
    department: 'Product',
    team: 'Growth',

    // Account relationship
    account_id: 'acct-789',
    is_account_owner: true,
    permissions: 'admin,billing,analytics',

    // Subscription
    plan_type: 'enterprise',
    subscription_status: 'active',
    trial_user: false,

    // Activity
    signup_date: '2024-01-15',
    last_login: '2024-12-26',
    login_count: 150,

    // Engagement
    feature_flags: 'new_dashboard,beta_reports',
    onboarding_completed: true,
    onboarding_step: 5,

    // Custom fields
    company_size: '100-500',
    industry: 'SaaS',
    use_case: 'Product Analytics'
  }
});

Account Data Structure

Account data represents organizations, companies, or tenants in multi-tenant applications.

Basic Account Data

pendo.initialize({
  visitor: {
    id: 'user-12345'
  },
  account: {
    id: 'acct-789',           // Required: Unique account ID
    name: 'Acme Corporation'  // Recommended: Account name
  }
});

Comprehensive Account Data

pendo.initialize({
  visitor: {
    id: 'user-12345'
  },
  account: {
    // Identity
    id: 'acct-789',
    name: 'Acme Corporation',
    domain: 'acme.com',

    // Subscription
    plan: 'enterprise',
    plan_tier: 'premium',
    subscription_status: 'active',
    billing_cycle: 'annual',

    // Revenue
    mrr: 2500,                    // Monthly recurring revenue
    arr: 30000,                   // Annual recurring revenue
    ltv: 75000,                   // Lifetime value
    total_spend: 45000,

    // Company details
    industry: 'Technology',
    company_size: '100-500',
    number_of_employees: 250,
    founded_year: 2015,

    // Location
    country: 'United States',
    state: 'California',
    city: 'San Francisco',
    timezone: 'America/Los_Angeles',

    // Engagement
    signup_date: '2023-06-01',
    contract_start: '2024-01-01',
    contract_end: '2025-01-01',
    renewal_date: '2025-01-01',

    // Usage
    total_users: 50,
    active_users: 35,
    seats_purchased: 75,
    storage_used_gb: 1500,

    // Features
    enabled_features: 'api,sso,custom_reports',
    integration_count: 12,

    // Support
    support_tier: 'premium',
    account_manager: 'Jane Smith',
    health_score: 85,

    // Custom fields
    lead_source: 'Referral',
    customer_segment: 'Enterprise',
    product_interest: 'Analytics'
  }
});

Data Types and Formatting

Supported Data Types

pendo.initialize({
  visitor: {
    id: 'user-123',

    // Strings
    email: 'user@example.com',
    role: 'admin',

    // Numbers
    age: 32,
    login_count: 150,
    score: 95.5,

    // Booleans
    is_active: true,
    trial_user: false,

    // Dates (ISO 8601 strings recommended)
    signup_date: '2024-01-15',
    last_login: '2024-12-26T10:30:00Z',

    // Arrays (as comma-separated strings)
    permissions: 'admin,billing,analytics',

    // Objects (as JSON strings)
    preferences: JSON.stringify({
      theme: 'dark',
      notifications: true
    })
  }
});

Naming Conventions

Use snake_case for consistency with Pendo's data model:

// Good: snake_case
pendo.initialize({
  visitor: {
    id: 'user-123',
    full_name: 'John Doe',
    signup_date: '2024-01-15',
    is_trial_user: false
  }
});

// Avoid: camelCase or PascalCase
pendo.initialize({
  visitor: {
    id: 'user-123',
    fullName: 'John Doe',        // Avoid
    signupDate: '2024-01-15',    // Avoid
    IsTrialUser: false           // Avoid
  }
});

Updating Visitor and Account Data

Update During Session

Update visitor or account metadata after initialization:

// Update visitor role after permission change
pendo.updateOptions({
  visitor: {
    role: 'super_admin',
    permissions: 'admin,billing,analytics,settings'
  }
});

// Update account plan after upgrade
pendo.updateOptions({
  account: {
    plan: 'enterprise',
    mrr: 5000,
    seats_purchased: 150
  }
});

Incremental Updates

You can update individual fields without resending all data:

// User completes onboarding
pendo.updateOptions({
  visitor: {
    onboarding_completed: true,
    onboarding_completion_date: new Date().toISOString()
  }
});

// User logs in
pendo.updateOptions({
  visitor: {
    last_login: new Date().toISOString(),
    login_count: user.loginCount + 1
  }
});

Dynamic Data Loading

From API

async function initializePendoFromAPI() {
  try {
    const response = await fetch('/api/user/pendo-data');
    const data = await response.json();

    pendo.initialize({
      visitor: data.visitor,
      account: data.account
    });
  } catch (error) {
    console.error('Failed to initialize Pendo:', error);
  }
}

// Call after authentication
initializePendoFromAPI();

From Data Layer

// Wait for data layer to be ready
function waitForDataLayer() {
  if (window.dataLayer && window.dataLayer.user) {
    const user = window.dataLayer.user;
    const account = window.dataLayer.account;

    pendo.initialize({
      visitor: {
        id: user.id,
        email: user.email,
        full_name: user.name,
        role: user.role
      },
      account: {
        id: account.id,
        name: account.name,
        plan: account.plan
      }
    });
  } else {
    setTimeout(waitForDataLayer, 100);
  }
}

waitForDataLayer();

From Local Storage

// Initialize from stored user data
const userData = JSON.parse(localStorage.getItem('user'));
const accountData = JSON.parse(localStorage.getItem('account'));

if (userData && accountData) {
  pendo.initialize({
    visitor: {
      id: userData.id,
      email: userData.email,
      full_name: userData.fullName,
      role: userData.role
    },
    account: {
      id: accountData.id,
      name: accountData.name,
      plan: accountData.plan
    }
  });
}

Framework-Specific Implementations

React with State

import { useEffect } from 'react';
import { useAuth } from './hooks/useAuth';

function App() {
  const { user, account } = useAuth();

  useEffect(() => {
    if (user && account && window.pendo) {
      window.pendo.initialize({
        visitor: {
          id: user.id,
          email: user.email,
          full_name: user.fullName,
          role: user.role,
          signup_date: user.createdAt,
          plan_type: account.plan
        },
        account: {
          id: account.id,
          name: account.name,
          plan: account.plan,
          mrr: account.monthlyRecurringRevenue,
          total_users: account.userCount
        }
      });
    }
  }, [user, account]);

  return <YourApp />;
}

Angular with RxJS

import { Component, OnInit } from '@angular/core';
import { combineLatest } from 'rxjs';
import { AuthService } from './services/auth.service';
import { AccountService } from './services/account.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html'
})
export class AppComponent implements OnInit {
  constructor(
    private authService: AuthService,
    private accountService: AccountService
  ) {}

  ngOnInit() {
    combineLatest([
      this.authService.user$,
      this.accountService.account$
    ]).subscribe(([user, account]) => {
      if (user && account && window.pendo) {
        window.pendo.initialize({
          visitor: {
            id: user.id,
            email: user.email,
            full_name: user.fullName,
            role: user.role
          },
          account: {
            id: account.id,
            name: account.name,
            plan: account.plan,
            mrr: account.mrr
          }
        });
      }
    });
  }
}

Vue with Composables

// composables/usePendo.js
import { watch } from 'vue';
import { useAuthStore } from '@/stores/auth';
import { useAccountStore } from '@/stores/account';

export function usePendo() {
  const authStore = useAuthStore();
  const accountStore = useAccountStore();

  watch(
    () => [authStore.user, accountStore.account],
    ([user, account]) => {
      if (user && account && window.pendo) {
        window.pendo.initialize({
          visitor: {
            id: user.id,
            email: user.email,
            full_name: user.fullName,
            role: user.role
          },
          account: {
            id: account.id,
            name: account.name,
            plan: account.plan
          }
        });
      }
    },
    { immediate: true }
  );
}

Data Privacy and Security

Excluding Sensitive Data

Never include sensitive information in visitor or account metadata:

// BAD: Contains sensitive data
pendo.initialize({
  visitor: {
    id: 'user-123',
    ssn: '123-45-6789',          // Never include
    credit_card: '4111...',       // Never include
    password: 'secret123'         // Never include
  }
});

// GOOD: Only safe metadata
pendo.initialize({
  visitor: {
    id: 'user-123',
    email: 'user@example.com',
    role: 'admin',
    plan_type: 'enterprise'
  }
});

Anonymizing Email Addresses

For privacy compliance, consider hashing emails:

async function hashEmail(email) {
  const encoder = new TextEncoder();
  const data = encoder.encode(email);
  const hash = await crypto.subtle.digest('SHA-256', data);
  return Array.from(new Uint8Array(hash))
    .map(b => b.toString(16).padStart(2, '0'))
    .join('');
}

const hashedEmail = await hashEmail(user.email);

pendo.initialize({
  visitor: {
    id: user.id,
    email_hash: hashedEmail,  // Use hash instead of plain email
    role: user.role
  }
});

GDPR Compliance

Respect user consent:

// Only initialize Pendo if user consented
if (userConsentedToAnalytics) {
  pendo.initialize({
    visitor: {
      id: user.id,
      email: user.email,
      consent_given: true,
      consent_date: new Date().toISOString()
    }
  });
}

Data Validation

Validation Helper

function validatePendoData(visitor, account) {
  const errors = [];

  // Visitor validation
  if (!visitor.id) {
    errors.push('Visitor ID is required');
  }

  if (visitor.email && !isValidEmail(visitor.email)) {
    errors.push('Invalid email format');
  }

  // Account validation
  if (account && !account.id) {
    errors.push('Account ID is required when account is provided');
  }

  if (account && account.mrr && typeof account.mrr !== 'number') {
    errors.push('MRR must be a number');
  }

  return errors;
}

// Use validation before initialization
const errors = validatePendoData(visitorData, accountData);
if (errors.length > 0) {
  console.error('Pendo data validation failed:', errors);
} else {
  pendo.initialize({
    visitor: visitorData,
    account: accountData
  });
}

Common Data Patterns

SaaS Applications

pendo.initialize({
  visitor: {
    id: user.id,
    email: user.email,
    full_name: user.fullName,
    role: user.role,
    signup_date: user.createdAt,
    trial_end_date: user.trialEndsAt,
    is_paying_customer: user.subscriptionStatus === 'active'
  },
  account: {
    id: account.id,
    name: account.name,
    plan: account.plan,
    mrr: account.monthlyRevenue,
    seats_purchased: account.licenses,
    seats_used: account.activeUsers,
    contract_end_date: account.contractEndsAt
  }
});

E-commerce Platforms

pendo.initialize({
  visitor: {
    id: user.id,
    email: user.email,
    full_name: user.fullName,
    customer_since: user.firstOrderDate,
    total_orders: user.orderCount,
    total_spent: user.lifetimeValue,
    vip_status: user.isVIP,
    preferred_category: user.favoriteCategory
  },
  account: {
    id: user.id,  // For B2C, account ID often equals visitor ID
    customer_tier: user.tier,
    loyalty_points: user.points
  }
});

Enterprise Applications

pendo.initialize({
  visitor: {
    id: user.employeeId,
    email: user.email,
    full_name: user.fullName,
    department: user.department,
    job_title: user.title,
    manager_id: user.managerId,
    office_location: user.office,
    access_level: user.securityClearance
  },
  account: {
    id: company.id,
    name: company.name,
    division: company.division,
    region: company.region,
    employee_count: company.employeeCount
  }
});

Debugging Data Layer

Enable Debug Mode

pendo.setDebugMode(true);

// Check visitor data
console.log('Visitor ID:', pendo.getVisitorId());
console.log('Account ID:', pendo.getAccountId());

// Inspect full configuration
console.log('Pendo Config:', window.pendo.getConfig());

Verify Data in Pendo

  1. Log into Pendo dashboard
  2. Navigate to Settings > Data Mappings
  3. Check "Visitor Metadata" and "Account Metadata"
  4. Verify your custom fields appear
  5. Confirm data types are correct

Additional Resources