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:
- Page URL and pathname
- HTTP Referer (traffic source)
- Browser and device information (anonymized)
- Viewport dimensions
- Country (from IP address, which is not stored)
- UTM parameters (campaign tracking)
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:
- Navigate to your site's settings in Plausible
- Click "Goals" in the left sidebar
- 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:
- Open browser DevTools (F12)
- Go to Network tab
- Filter for
api/event - Trigger an event
- 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
- Open Plausible dashboard
- Click "Realtime" to see live events
- Trigger events on your site
- Verify events appear within 1-2 seconds
- 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
- Privacy First: Never include PII (email, name, phone, IP address) in event data
- Consistent Naming: Use snake_case for property names (e.g.,
product_id, notproductId) - Pre-Configure Goals: Set up all goals in Plausible dashboard before deploying tracking code
- Property Limits: Plausible limits custom properties to 30 per site - plan accordingly
- Validation Before Production: Test all events in staging environment with real data flows
- Documentation: Maintain a central registry of all events and their properties
- Version Control: Keep tracking code in version control with clear commit messages
- Monitoring: Set up alerts for tracking failures or unexpected event volumes
- Regular Audits: Review tracked data monthly for schema drift or anomalies
- Error Handling: Wrap plausible calls in try-catch blocks to prevent JavaScript errors from breaking your site