Plausible Data Layer Setup | OpsBlu Docs

Plausible Data Layer Setup

How to configure data layer variables and user properties for Plausible Analytics.

Overview

Plausible Analytics takes a minimalist approach to data collection, focusing on privacy-first analytics without the complexity of traditional data layers. Unlike Google Analytics or Adobe Analytics, Plausible doesn't require a formal data layer object, but establishing a consistent data structure ensures accurate tracking and easier maintenance.

The data layer in Plausible context refers to the structured way you prepare and organize information before sending it to Plausible. This includes page metadata, custom event properties, goal definitions, and revenue tracking parameters.

Key Principles

Privacy First: Plausible is designed to comply with GDPR, CCPA, and other privacy regulations without requiring cookie consent banners. Never include personally identifiable information (PII) in your data layer.

Lightweight Structure: Unlike complex data layer objects, Plausible uses simple key-value pairs for event properties and automatic collection of essential page metadata.

Goal-Oriented Tracking: Events in Plausible are called "goals" and must be pre-configured in the dashboard or sent as custom events with the plausible() function.

What Plausible Collects Automatically

Plausible automatically captures:

Configuration

Basic Data Layer Structure

While Plausible doesn't require a formal data layer object, establishing a consistent pattern helps maintain data quality:

// Optional: Create a simple data layer structure for consistency
window.analyticsData = {
  pageInfo: {
    type: 'product',
    category: 'electronics',
    title: document.title
  },
  user: {
    // Never include PII
    loginStatus: 'logged_in',
    accountType: 'premium'
  },
  content: {
    contentId: 'prod-123', // Non-PII identifier
    contentType: 'product-page'
  }
};

Goal Configuration in Dashboard

Before sending custom events, configure goals in your Plausible dashboard:

  1. Navigate to your site's settings in Plausible
  2. Click "Goals" in the left sidebar
  3. Choose goal type:
    • Pageview goals: Trigger when specific pages are visited
    • Custom event goals: Trigger when custom events are sent via JavaScript

Example goal configurations:

  • Pageview goal: /thank-you (purchase confirmation page)
  • Custom event goal: Signup (user registration)
  • Custom event goal: Download (file downloads)

Event Properties Schema

Plausible supports custom properties (props) for events, limited to 30 properties per site:

// Event with properties
plausible('Button Click', {
  props: {
    location: 'hero_section',
    button_text: 'Get Started',
    color: 'blue'
  }
});

Properties must be:

  • String key-value pairs
  • No PII
  • Consistent naming convention
  • Pre-configured in dashboard settings (for prop filtering)

Revenue Tracking Configuration

For e-commerce tracking, configure revenue data:

plausible('Purchase', {
  revenue: {
    currency: 'USD',
    amount: 49.99
  },
  props: {
    product_category: 'software',
    payment_method: 'credit_card'
  }
});

Revenue fields:

  • currency: ISO 4217 code (USD, EUR, GBP, etc.)
  • amount: Decimal number representing revenue

Code Examples

Basic Page Data Layer

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Product Page - Electronics</title>

  <!-- Plausible script -->
  <script defer data-domain="example.com" src="https://plausible.io/js/script.js"></script>

  <!-- Optional: Initialize data layer before Plausible loads -->
  <script>
    window.siteData = {
      pageType: 'product',
      category: 'electronics',
      environment: 'production'
    };
  </script>
</head>
<body>
  <!-- Page content -->
</body>
</html>

E-commerce Data Layer

// Product page data layer
const productData = {
  id: 'prod-abc-123',
  name: 'Wireless Headphones', // Non-PII
  category: 'audio',
  price: 79.99,
  currency: 'USD',
  inStock: true
};

// Track product view
plausible('Product View', {
  props: {
    product_id: productData.id,
    product_category: productData.category,
    price_range: getPriceRange(productData.price) // '50-100'
  }
});

// Track add to cart
document.querySelector('#add-to-cart').addEventListener('click', function() {
  plausible('Add to Cart', {
    props: {
      product_id: productData.id,
      product_category: productData.category,
      quantity: document.querySelector('#quantity').value
    }
  });
});

// Track purchase completion
function trackPurchase(orderData) {
  plausible('Purchase', {
    revenue: {
      currency: orderData.currency,
      amount: orderData.total
    },
    props: {
      order_id: orderData.id,
      item_count: orderData.items.length,
      shipping_method: orderData.shippingMethod,
      payment_method: orderData.paymentMethod
    }
  });
}

// Helper function to categorize prices
function getPriceRange(price) {
  if (price < 25) return '0-25';
  if (price < 50) return '25-50';
  if (price < 100) return '50-100';
  if (price < 200) return '100-200';
  return '200+';
}

User Journey Data Layer

// Track user journey stages
const journeyData = {
  currentStage: 'checkout',
  previousStage: 'cart',
  stepsCompleted: ['browse', 'cart', 'checkout']
};

// Track stage progression
function trackJourneyStage(stage) {
  plausible('Journey Stage', {
    props: {
      stage: stage,
      previous_stage: journeyData.previousStage,
      steps_count: journeyData.stepsCompleted.length
    }
  });

  // Update journey data
  journeyData.previousStage = journeyData.currentStage;
  journeyData.currentStage = stage;
  journeyData.stepsCompleted.push(stage);
}

// Usage
trackJourneyStage('payment');

Campaign Tracking Data Layer

// Extract and structure campaign data from URL
function getCampaignData() {
  const urlParams = new URLSearchParams(window.location.search);

  return {
    source: urlParams.get('utm_source') || 'direct',
    medium: urlParams.get('utm_medium') || 'none',
    campaign: urlParams.get('utm_campaign') || 'none',
    term: urlParams.get('utm_term') || '',
    content: urlParams.get('utm_content') || ''
  };
}

// Plausible automatically captures UTM parameters
// Optional: Store campaign data for later use
const campaignData = getCampaignData();

// Use campaign context in custom events
document.querySelector('#signup-button').addEventListener('click', function() {
  plausible('Signup', {
    props: {
      signup_source: campaignData.source,
      signup_campaign: campaignData.campaign
    }
  });
});

Content Engagement Data Layer

// Track content engagement metrics
const contentData = {
  articleId: 'article-456',
  category: 'tutorials',
  author: 'tech_team', // Non-PII
  publishDate: '2025-01-15',
  wordCount: 1500,
  readingTime: 7 // minutes
};

// Track scroll depth
let maxScrollDepth = 0;
let scrollMilestones = [25, 50, 75, 100];

window.addEventListener('scroll', function() {
  const scrollPercentage = Math.round(
    (window.scrollY / (document.documentElement.scrollHeight - window.innerHeight)) * 100
  );

  if (scrollPercentage > maxScrollDepth) {
    maxScrollDepth = scrollPercentage;

    // Track milestone achievements
    scrollMilestones.forEach((milestone, index) => {
      if (scrollPercentage >= milestone && !window[`milestone${milestone}Tracked`]) {
        plausible('Scroll Depth', {
          props: {
            depth: `${milestone}%`,
            article_id: contentData.articleId,
            article_category: contentData.category
          }
        });
        window[`milestone${milestone}Tracked`] = true;
      }
    });
  }
});

// Track time on page
let startTime = Date.now();

window.addEventListener('beforeunload', function() {
  const timeOnPage = Math.round((Date.now() - startTime) / 1000); // seconds

  if (timeOnPage > 10) { // Only track if user spent more than 10 seconds
    plausible('Time on Page', {
      props: {
        duration: getTimeRange(timeOnPage),
        article_id: contentData.articleId
      }
    });
  }
});

function getTimeRange(seconds) {
  if (seconds < 30) return '0-30s';
  if (seconds < 60) return '30-60s';
  if (seconds < 180) return '1-3min';
  if (seconds < 300) return '3-5min';
  return '5min+';
}

Form Interaction Data Layer

// Track form interactions
const formData = {
  formId: 'contact-form',
  formType: 'contact',
  fieldsStarted: [],
  fieldsCompleted: []
};

// Track form start
document.querySelectorAll('form input, form textarea').forEach(field => {
  field.addEventListener('focus', function() {
    if (!formData.fieldsStarted.includes(field.name)) {
      formData.fieldsStarted.push(field.name);

      if (formData.fieldsStarted.length === 1) {
        // Track first interaction with form
        plausible('Form Started', {
          props: {
            form_id: formData.formId,
            form_type: formData.formType
          }
        });
      }
    }
  });

  field.addEventListener('blur', function() {
    if (field.value && !formData.fieldsCompleted.includes(field.name)) {
      formData.fieldsCompleted.push(field.name);
    }
  });
});

// Track form submission
document.querySelector('form').addEventListener('submit', function(e) {
  plausible('Form Submitted', {
    props: {
      form_id: formData.formId,
      form_type: formData.formType,
      fields_completed: formData.fieldsCompleted.length,
      completion_rate: Math.round((formData.fieldsCompleted.length / formData.fieldsStarted.length) * 100)
    }
  });
});

Error Tracking Data Layer

// Track JavaScript errors (without PII)
window.addEventListener('error', function(event) {
  plausible('JavaScript Error', {
    props: {
      error_message: event.message.substring(0, 100), // Truncate to avoid PII
      error_source: event.filename.split('/').pop(), // Just filename
      error_line: event.lineno.toString(),
      page_path: window.location.pathname
    }
  });
});

// Track API errors
async function fetchWithTracking(url, options) {
  try {
    const response = await fetch(url, options);

    if (!response.ok) {
      plausible('API Error', {
        props: {
          status_code: response.status.toString(),
          endpoint: url.split('?')[0], // Remove query parameters
          method: options.method || 'GET'
        }
      });
    }

    return response;
  } catch (error) {
    plausible('Network Error', {
      props: {
        error_type: error.name,
        endpoint: url.split('?')[0]
      }
    });
    throw error;
  }
}

Validation Steps

1. Verify Data Layer Initialization

Check that your data layer is properly initialized before Plausible script loads:

// In browser console
console.log(window.siteData); // Should show your data layer object
console.log(typeof window.plausible); // Should be 'function'

2. Inspect Network Requests

Monitor event requests to Plausible:

  1. Open browser DevTools (F12)
  2. Go to Network tab
  3. Filter for api/event
  4. Trigger an event
  5. Inspect the request payload

Expected payload structure:

{
  "n": "Purchase",
  "u": "https://example.com/checkout/complete",
  "d": "example.com",
  "r": "https://example.com/cart",
  "p": "{\"product_category\":\"electronics\",\"payment_method\":\"credit_card\"}",
  "$": "{\"currency\":\"USD\",\"amount\":49.99}"
}

3. Real-Time Dashboard Verification

  1. Open Plausible dashboard
  2. Click "Realtime" to see live events
  3. Trigger events on your site
  4. Verify events appear within 1-2 seconds
  5. Check that event properties are correctly displayed

4. Goal Configuration Check

Verify all goals are properly configured:

// Test each goal
const testGoals = ['Signup', 'Purchase', 'Download', 'Newsletter Subscribe'];

testGoals.forEach(goal => {
  console.log(`Testing goal: ${goal}`);
  plausible(goal, {
    props: { test: 'validation' }
  });
});

// Check Plausible dashboard to confirm all goals were received

5. Revenue Tracking Validation

Test revenue tracking with sample data:

// Test purchase with revenue
plausible('Purchase', {
  revenue: {
    currency: 'USD',
    amount: 0.01 // Use small test amount
  },
  props: {
    test: 'revenue_validation',
    order_id: 'test-' + Date.now()
  }
});

// Verify in Plausible dashboard under Revenue report

6. Property Naming Consistency

Audit event properties for consistency:

// List all unique properties being sent
const propertyAudit = new Set();

// Override plausible function to intercept calls
const originalPlausible = window.plausible;
window.plausible = function(eventName, options) {
  if (options && options.props) {
    Object.keys(options.props).forEach(key => propertyAudit.add(key));
  }
  return originalPlausible.apply(this, arguments);
};

// After triggering various events, check for consistency
console.log('Properties used:', Array.from(propertyAudit));
// Look for inconsistencies like: 'product_id' vs 'productId' vs 'product-id'

Governance Notes

Goal Name Registry

Maintain a centralized registry of all goal names and their purposes:

// goals-registry.js
export const GOALS = {
  // User actions
  SIGNUP: 'Signup',
  LOGIN: 'Login',
  LOGOUT: 'Logout',

  // E-commerce
  PRODUCT_VIEW: 'Product View',
  ADD_TO_CART: 'Add to Cart',
  PURCHASE: 'Purchase',

  // Content
  ARTICLE_READ: 'Article Read',
  VIDEO_PLAY: 'Video Play',
  DOWNLOAD: 'Download',

  // Forms
  FORM_STARTED: 'Form Started',
  FORM_SUBMITTED: 'Form Submitted',

  // Engagement
  NEWSLETTER_SUBSCRIBE: 'Newsletter Subscribe',
  SHARE: 'Share',
  COMMENT: 'Comment'
};

// Usage
plausible(GOALS.PURCHASE, { props: { ... } });

Owner Documentation

Document ownership for each tracking implementation:

/**
 * E-commerce Tracking
 * Owner: E-commerce Team
 * Contact: ecommerce-team@example.com
 * Last Updated: 2025-01-15
 *
 * Goals:
 * - Product View
 * - Add to Cart
 * - Purchase
 *
 * Properties:
 * - product_id: Internal product identifier (string)
 * - product_category: Product category (string)
 * - price_range: Price bucket (string: '0-25', '25-50', etc.)
 */

Schema Drift Prevention

Establish validation to prevent schema drift:

// Property validation schema
const PROPERTY_SCHEMAS = {
  'Purchase': {
    required: ['order_id', 'payment_method'],
    optional: ['product_category', 'shipping_method'],
    types: {
      order_id: 'string',
      payment_method: 'string',
      product_category: 'string'
    }
  },
  'Signup': {
    required: ['signup_source'],
    optional: ['signup_campaign'],
    types: {
      signup_source: 'string',
      signup_campaign: 'string'
    }
  }
};

// Validation function
function validateEventProps(eventName, props) {
  const schema = PROPERTY_SCHEMAS[eventName];

  if (!schema) {
    console.warn(`No schema defined for event: ${eventName}`);
    return true;
  }

  // Check required properties
  for (const required of schema.required) {
    if (!(required in props)) {
      console.error(`Missing required property '${required}' for event '${eventName}'`);
      return false;
    }
  }

  // Check property types
  for (const [key, value] of Object.entries(props)) {
    const expectedType = schema.types[key];
    if (expectedType && typeof value !== expectedType) {
      console.error(`Property '${key}' should be ${expectedType}, got ${typeof value}`);
      return false;
    }
  }

  // Warn about unexpected properties
  const allowedProps = [...schema.required, ...schema.optional];
  for (const key of Object.keys(props)) {
    if (!allowedProps.includes(key)) {
      console.warn(`Unexpected property '${key}' for event '${eventName}'`);
    }
  }

  return true;
}

// Wrap plausible function with validation
const originalPlausible = window.plausible;
window.plausible = function(eventName, options) {
  if (options && options.props) {
    validateEventProps(eventName, options.props);
  }
  return originalPlausible.apply(this, arguments);
};

Version Management

Keep track of Plausible script versions:

// Check current Plausible script version
// Add to site footer or monitoring script
console.log('Plausible script version:', document.querySelector('script[src*="plausible"]')?.src);

// For self-hosted instances, document version
const PLAUSIBLE_VERSION = '2.0.0'; // Update after each upgrade

PII Protection Checklist

Implement automated checks to prevent PII in data layer:

// PII detection patterns
const PII_PATTERNS = [
  /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/, // Email
  /\b\d{3}-\d{2}-\d{4}\b/, // SSN
  /\b\d{3}[-.]?\d{3}[-.]?\d{4}\b/, // Phone
  /\b(?:\d{4}[-\s]?){3}\d{4}\b/ // Credit card
];

function containsPII(value) {
  const strValue = String(value);
  return PII_PATTERNS.some(pattern => pattern.test(strValue));
}

function checkPropsForPII(props) {
  for (const [key, value] of Object.entries(props)) {
    if (containsPII(value)) {
      console.error(`PII detected in property '${key}': ${value}`);
      console.trace('Stack trace:');
      return true;
    }
  }
  return false;
}

// Add to validation wrapper
const originalPlausible = window.plausible;
window.plausible = function(eventName, options) {
  if (options && options.props && checkPropsForPII(options.props)) {
    console.error('Event blocked due to PII detection');
    return;
  }
  return originalPlausible.apply(this, arguments);
};

Troubleshooting

Issue Possible Cause Solution
Events not appearing in dashboard Goal not configured in Plausible settings Add goal in Plausible dashboard before sending events
Properties not showing up Properties not enabled in site settings Enable custom properties in Plausible site settings
Revenue data missing Incorrect revenue object structure Ensure revenue object has both currency and amount fields
Duplicate events firing Event listeners attached multiple times Use event delegation or ensure listeners are only attached once
Data layer undefined Script loading order issue Initialize data layer before Plausible script loads
Events tracked but properties empty Props not passed correctly Verify props object is nested correctly in options parameter
Network request fails Ad blocker or CORS issue Implement custom proxy with first-party domain
UTM parameters not captured URL format incorrect Verify UTM parameters use standard names (utm_source, utm_medium, etc.)
Events tracked on wrong domain data-domain attribute incorrect Verify data-domain matches your site's domain exactly
Properties truncated Property value too long Limit property values to 500 characters
Currency not recognized Invalid ISO code Use standard ISO 4217 currency codes (USD, EUR, GBP, etc.)
Events delayed in dashboard Network latency or batching Events may take 1-2 seconds to appear; check network connection
Historical data inconsistent Schema changed over time Maintain schema versions; document all changes with dates
PII accidentally tracked No validation in place Implement PII detection and blocking (see Governance section)
Property names inconsistent No naming convention enforced Use PROPERTY_SCHEMAS constant for consistency

Best Practices

  1. Privacy First: Never include PII (email, name, phone, IP address) in event data
  2. Consistent Naming: Use snake_case for property names (e.g., product_id, not productId)
  3. Pre-Configure Goals: Set up all goals in Plausible dashboard before deploying tracking code
  4. Property Limits: Plausible limits custom properties to 30 per site - plan accordingly
  5. Validation Before Production: Test all events in staging environment with real data flows
  6. Documentation: Maintain a central registry of all events and their properties
  7. Version Control: Keep tracking code in version control with clear commit messages
  8. Monitoring: Set up alerts for tracking failures or unexpected event volumes
  9. Regular Audits: Review tracked data monthly for schema drift or anomalies
  10. Error Handling: Wrap plausible calls in try-catch blocks to prevent JavaScript errors from breaking your site