Overview
Event tracking in Plausible Analytics allows you to measure specific user interactions beyond simple pageviews. Unlike traditional analytics platforms with complex event taxonomies, Plausible uses a streamlined approach where events are called "goals" and can be triggered either automatically (pageview goals) or manually via JavaScript (custom event goals).
Plausible's event model emphasizes simplicity and privacy. Events capture user actions without personal identifiers, maintaining GDPR and CCPA compliance. Each event can include custom properties to provide context, and revenue tracking can be attached to conversion events for e-commerce analytics.
Key Concepts
Goals: In Plausible terminology, trackable events are called "goals." Goals must be configured in your Plausible dashboard before data will appear in reports.
Custom Events: JavaScript-triggered events using the plausible() function, allowing you to track button clicks, form submissions, downloads, and other interactions.
Pageview Goals: Automatically triggered when users visit specific URL patterns, useful for tracking thank-you pages, specific sections, or conversion endpoints.
Event Properties: Additional key-value data attached to events to provide context (limited to 30 properties per site).
Event Model Architecture
Plausible events follow a flat, simple structure:
plausible(eventName, {
props: { key: 'value' }, // Optional properties
revenue: { currency: 'USD', amount: 10.00 } // Optional revenue
});
This simplicity contrasts with complex event taxonomies in Google Analytics 4 or Adobe Analytics, making implementation and maintenance significantly easier.
Configuration
Setting Up Goals in Dashboard
Before sending custom events, configure goals in your Plausible dashboard:
- Log into your Plausible account
- Select your website
- Navigate to Settings → Goals
- Click "+ Add Goal"
- Choose goal type:
- Custom Event: For JavaScript-triggered events
- Pageview: For URL-based triggers
Custom Event Goal Configuration
For custom events, simply enter the event name exactly as it will appear in your JavaScript code:
Dashboard Configuration:
- Goal Type: Custom Event
- Event Name:
Signup(case-sensitive)
JavaScript Implementation:
plausible('Signup');
The event name must match exactly, including capitalization.
Pageview Goal Configuration
Pageview goals trigger when users visit URLs matching a pattern:
Dashboard Configuration:
- Goal Type: Pageview
- Page Path:
/thank-you(exact match) - Or:
/products/*(wildcard match) - Or:
/blog/**(recursive wildcard)
Pageview goals don't require JavaScript implementation - they fire automatically when the URL matches.
Enabling Custom Properties
To use custom properties in your events:
- Navigate to Settings → General
- Scroll to "Custom Properties"
- Enable the feature (available on paid plans)
- Properties are automatically available once enabled
Revenue Tracking Configuration
Revenue tracking is available on paid plans:
- Navigate to Settings → General
- Enable "Revenue Goals"
- Select which goals should track revenue
- Implement revenue tracking in your code
Core Events to Ship
1. Pageview (Automatic)
Automatically tracked on every page load:
// Automatic - no code required
// Plausible script handles this automatically
For Single-Page Applications (SPAs), manually trigger pageviews:
// After route change in SPA
plausible('pageview');
2. Signup Events
Track user registration and account creation:
// Basic signup event
plausible('Signup');
// With properties
plausible('Signup', {
props: {
method: 'email', // 'email', 'google', 'github'
plan: 'free' // 'free', 'starter', 'pro'
}
});
3. Purchase Events
Track e-commerce transactions with revenue:
plausible('Purchase', {
revenue: {
currency: 'USD',
amount: 99.99
},
props: {
product_category: 'software',
payment_method: 'credit_card',
subscription_type: 'monthly'
}
});
4. Newsletter Subscribe
Track newsletter signups and email subscriptions:
plausible('Newsletter Subscribe', {
props: {
location: 'footer', // 'header', 'footer', 'popup'
list_type: 'weekly' // 'weekly', 'daily', 'announcements'
}
});
5. Download Events
Track file downloads:
// Automatic file download tracking
// Add data-attribute to download links
<a href="/guide.pdf" data-plausible-event-name="Download">Download Guide</a>
// Or manual tracking
document.querySelector('.download-link').addEventListener('click', function(e) {
plausible('Download', {
props: {
file_name: 'user-guide.pdf',
file_type: 'pdf',
file_size: '2.5MB'
}
});
});
6. Outbound Link Tracking
Track clicks to external websites:
<!-- Automatic outbound link tracking -->
<!-- Use script extension -->
<script defer data-domain="example.com" src="https://plausible.io/js/script.outbound-links.js"></script>
<!-- Links to external sites are automatically tracked -->
<a href="https://external-site.com">External Link</a>
<!-- Or manual tracking -->
<a href="https://external-site.com" Link', {props: {url: 'external-site.com'}})">
External Link
</a>
7. Video Engagement
Track video plays and engagement:
// Track video play
document.querySelector('#video').addEventListener('play', function() {
plausible('Video Play', {
props: {
video_id: 'intro-2025',
video_title: 'Product Introduction',
video_duration: '180' // seconds
}
});
});
// Track completion milestones
document.querySelector('#video').addEventListener('timeupdate', function(e) {
const video = e.target;
const percentComplete = (video.currentTime / video.duration) * 100;
if (percentComplete >= 25 && !video.milestone25) {
plausible('Video 25% Complete', {props: {video_id: 'intro-2025'}});
video.milestone25 = true;
}
// Repeat for 50%, 75%, 100%
});
8. Form Submission
Track form submissions and conversions:
document.querySelector('#contact-form').addEventListener('submit', function(e) {
plausible('Contact Form Submit', {
props: {
form_type: 'contact',
page_location: window.location.pathname
}
});
});
9. Search Events
Track site search usage:
document.querySelector('#search-form').addEventListener('submit', function(e) {
const query = document.querySelector('#search-input').value;
plausible('Search', {
props: {
query_length: query.length.toString(),
has_results: 'true' // Update based on results
}
});
});
10. Add to Cart
Track e-commerce cart additions:
document.querySelector('.add-to-cart-btn').addEventListener('click', function() {
plausible('Add to Cart', {
props: {
product_id: 'prod-123',
product_category: 'electronics',
quantity: '1'
}
});
});
Code Examples
Basic Event Tracking
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Event Tracking Example</title>
<!-- Plausible script -->
<script defer data-domain="example.com" src="https://plausible.io/js/script.js"></script>
</head>
<body>
<button id="cta-button">Sign Up</button>
<script>
document.getElementById('cta-button').addEventListener('click', function() {
plausible('CTA Click', {
props: {
button_text: 'Sign Up',
button_location: 'hero'
}
});
});
</script>
</body>
</html>
E-commerce Full Funnel Tracking
// Product view
function trackProductView(product) {
plausible('Product View', {
props: {
product_id: product.id,
product_name: product.name,
product_category: product.category,
price: product.price.toString()
}
});
}
// Add to cart
function trackAddToCart(product, quantity) {
plausible('Add to Cart', {
props: {
product_id: product.id,
product_category: product.category,
quantity: quantity.toString(),
price: product.price.toString()
}
});
}
// Begin checkout
function trackBeginCheckout(cart) {
plausible('Begin Checkout', {
props: {
cart_value: cart.total.toString(),
item_count: cart.items.length.toString()
}
});
}
// Purchase completion
function trackPurchase(order) {
plausible('Purchase', {
revenue: {
currency: order.currency,
amount: order.total
},
props: {
order_id: order.id,
payment_method: order.paymentMethod,
shipping_method: order.shippingMethod,
item_count: order.items.length.toString(),
discount_applied: order.discountCode ? 'true' : 'false'
}
});
}
SPA (Single-Page Application) Tracking
// React example with React Router
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
function usePageViewTracking() {
const location = useLocation();
useEffect(() => {
// Track pageview on route change
plausible('pageview');
}, [location]);
}
// Vue.js example with Vue Router
router.afterEach((to, from) => {
// Track pageview on route change
plausible('pageview');
});
// Vanilla JavaScript with History API
const originalPushState = history.pushState;
history.pushState = function() {
originalPushState.apply(this, arguments);
plausible('pageview');
};
const originalReplaceState = history.replaceState;
history.replaceState = function() {
originalReplaceState.apply(this, arguments);
plausible('pageview');
};
window.addEventListener('popstate', function() {
plausible('pageview');
});
Advanced Event Tracking with Callbacks
// Track event with callback
function trackWithCallback(eventName, props, callback) {
plausible(eventName, {
props: props,
callback: callback
});
}
// Usage: Track signup and redirect after event is sent
document.querySelector('#signup-btn').addEventListener('click', function(e) {
e.preventDefault();
trackWithCallback('Signup',
{ method: 'email' },
function() {
// Redirect after event is tracked
window.location.href = '/welcome';
}
);
});
Scroll Depth Tracking
let scrollDepthTracked = {
'25': false,
'50': false,
'75': false,
'100': false
};
function trackScrollDepth() {
const windowHeight = window.innerHeight;
const documentHeight = document.documentElement.scrollHeight;
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
const scrollPercentage = (scrollTop / (documentHeight - windowHeight)) * 100;
Object.keys(scrollDepthTracked).forEach(depth => {
if (scrollPercentage >= parseInt(depth) && !scrollDepthTracked[depth]) {
plausible('Scroll Depth', {
props: {
depth: depth + '%',
page: window.location.pathname
}
});
scrollDepthTracked[depth] = true;
}
});
}
// Throttle scroll events
let scrollTimeout;
window.addEventListener('scroll', function() {
if (scrollTimeout) {
clearTimeout(scrollTimeout);
}
scrollTimeout = setTimeout(trackScrollDepth, 100);
});
Click Tracking with Data Attributes
<!-- Declarative event tracking using data attributes -->
<button data-plausible-event-name="CTA Click"
data-plausible-event-location="hero">
Get Started
</button>
<a href="/pricing"
data-plausible-event-name="Pricing Link Click"
data-plausible-event-source="navigation">
View Pricing
</a>
<script>
// Automatically track clicks with data attributes
document.addEventListener('click', function(e) {
const element = e.target.closest('[data-plausible-event-name]');
if (element) {
const eventName = element.getAttribute('data-plausible-event-name');
const props = {};
// Collect all data-plausible-event-* attributes
Array.from(element.attributes).forEach(attr => {
if (attr.name.startsWith('data-plausible-event-') && attr.name !== 'data-plausible-event-name') {
const propName = attr.name.replace('data-plausible-event-', '');
props[propName] = attr.value;
}
});
plausible(eventName, { props: Object.keys(props).length > 0 ? props : undefined });
}
});
</script>
Error Tracking
// Track JavaScript errors
window.addEventListener('error', function(event) {
plausible('JavaScript Error', {
props: {
message: event.message.substring(0, 100),
filename: event.filename.split('/').pop(),
line: event.lineno.toString(),
column: event.colno.toString()
}
});
});
// Track unhandled promise rejections
window.addEventListener('unhandledrejection', function(event) {
plausible('Unhandled Promise Rejection', {
props: {
reason: String(event.reason).substring(0, 100)
}
});
});
// Track API errors
async function apiCall(url) {
try {
const response = await fetch(url);
if (!response.ok) {
plausible('API Error', {
props: {
endpoint: url.split('?')[0],
status_code: response.status.toString(),
status_text: response.statusText
}
});
}
return response;
} catch (error) {
plausible('Network Error', {
props: {
endpoint: url.split('?')[0],
error_type: error.name
}
});
throw error;
}
}
Payload Rules
Event Name Requirements
- Format: Use Title Case or lowercase-with-hyphens
- Length: Keep under 50 characters
- Characters: Letters, numbers, spaces, hyphens, underscores
- Consistency: Match dashboard goal configuration exactly (case-sensitive)
Valid examples:
SignupPurchasenewsletter-subscribeVideo Playadd_to_cart
Invalid examples:
signup!(special characters)This Is An Extremely Long Event Name That Will Be Hard To Read In Reports(too long)user-email@example.com(contains PII)
Property Requirements
Properties must be:
- String values only: All property values must be strings
- No PII: Never include email, name, phone, IP, user ID
- Descriptive keys: Use clear, lowercase_with_underscores format
- Reasonable length: Keep values under 500 characters
- Limited count: Maximum 30 unique properties per site
Valid property examples:
{
product_category: 'electronics',
button_location: 'header',
price_range: '50-100',
user_type: 'premium' // Status, not identifier
}
Invalid property examples:
{
email: 'user@example.com', // PII!
userId: '12345', // Personal identifier
ipAddress: '192.168.1.1' // PII!
}
Revenue Requirements
Revenue tracking requires specific format:
{
revenue: {
currency: 'USD', // Required: ISO 4217 code
amount: 49.99 // Required: Number (not string)
}
}
Supported currencies: USD, EUR, GBP, CAD, AUD, JPY, and all ISO 4217 codes.
Naming & Conventions
Recommended Naming Standards
Option 1: Title Case (Recommended)
plausible('Signup');
plausible('Add to Cart');
plausible('Newsletter Subscribe');
plausible('Video Play');
Option 2: lowercase-with-hyphens
plausible('signup');
plausible('add-to-cart');
plausible('newsletter-subscribe');
plausible('video-play');
Option 3: snake_case
plausible('signup');
plausible('add_to_cart');
plausible('newsletter_subscribe');
plausible('video_play');
Choose one convention and stick to it across your entire site.
Event Naming Categories
Organize events by category for easier reporting:
// User Actions
plausible('Signup');
plausible('Login');
plausible('Logout');
// E-commerce
plausible('Product View');
plausible('Add to Cart');
plausible('Purchase');
// Content Engagement
plausible('Article Read');
plausible('Video Play');
plausible('Download');
// Forms
plausible('Contact Form Submit');
plausible('Newsletter Subscribe');
// Navigation
plausible('Search');
plausible('Filter Applied');
Property Naming Standards
Use consistent snake_case for all properties:
plausible('Purchase', {
props: {
product_id: 'abc123',
product_category: 'electronics',
payment_method: 'credit_card',
shipping_method: 'express',
order_value: '99.99'
}
});
Validation Steps
1. Pre-Deployment Checklist
Before deploying event tracking:
- All goals configured in Plausible dashboard
- Event names match dashboard configuration exactly
- No PII included in event data
- Property names follow naming convention
- Revenue format validated
- Code tested in staging environment
2. Real-Time Verification
Test events using Plausible's real-time dashboard:
// Test script to verify all events
const testEvents = [
{ name: 'Signup', props: { method: 'test' } },
{ name: 'Purchase', revenue: { currency: 'USD', amount: 0.01 }, props: { test: 'true' } },
{ name: 'Download', props: { file_type: 'test' } }
];
testEvents.forEach(event => {
console.log('Testing event:', event.name);
plausible(event.name, {
props: event.props,
revenue: event.revenue
});
});
console.log('Check Plausible dashboard for events appearing in real-time');
3. Browser Console Verification
Check events in browser DevTools:
// Enable Plausible debugging
localStorage.plausible_debug = true;
// Trigger event
plausible('Test Event', { props: { test: 'true' } });
// Check console for debug output
// Check Network tab for /api/event requests
4. Network Request Inspection
Inspect the event payload being sent:
- Open DevTools (F12)
- Go to Network tab
- Filter for
event - Trigger an event
- Click the request to see payload
Expected payload:
{
"n": "Signup",
"u": "https://example.com/signup",
"d": "example.com",
"p": "{\"method\":\"email\",\"plan\":\"free\"}"
}
5. Goal Funnel Testing
Test complete user journeys:
// Simulate complete e-commerce funnel
async function testEcommerceFunnel() {
console.log('Starting funnel test...');
// Step 1: Product View
plausible('Product View', { props: { product_id: 'test-001' } });
await sleep(1000);
// Step 2: Add to Cart
plausible('Add to Cart', { props: { product_id: 'test-001' } });
await sleep(1000);
// Step 3: Begin Checkout
plausible('Begin Checkout', { props: { cart_value: '99.99' } });
await sleep(1000);
// Step 4: Purchase
plausible('Purchase', {
revenue: { currency: 'USD', amount: 0.01 },
props: { order_id: 'test-' + Date.now() }
});
console.log('Funnel test complete. Check dashboard.');
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
6. SPA Pageview Validation
For single-page applications, verify pageviews fire on route changes:
// Test SPA pageview tracking
let pageviewCount = 0;
const originalPlausible = window.plausible;
window.plausible = function(eventName, options) {
if (eventName === 'pageview') {
pageviewCount++;
console.log(`Pageview ${pageviewCount} tracked:`, window.location.pathname);
}
return originalPlausible.apply(this, arguments);
};
// Navigate through SPA and verify pageviews are tracked
QA Notes
Pre-Launch Testing
- Goal Configuration: Verify all events are configured as goals in dashboard
- Staging Tests: Trigger each goal at least once in staging environment
- Real-Time Verification: Confirm events appear in real-time dashboard within 2 seconds
- Domain Verification: Ensure correct domain label appears with each event
- Property Verification: Check that all properties appear correctly in dashboard
- Revenue Testing: Verify revenue amounts display correctly in revenue reports
SPA-Specific Testing
For single-page applications:
- Pageviews trigger on route changes
- No duplicate pageviews on initial load
- Browser back/forward buttons trigger pageviews
- Direct URL access triggers pageview
- Hash-based routing handled correctly (if applicable)
Common Testing Scenarios
// Test 1: Event fires on button click
document.querySelector('#test-button').addEventListener('click', function() {
console.log('Button clicked - event should fire');
plausible('Button Click');
});
// Test 2: Event fires only once (no duplicates)
let clickCount = 0;
document.querySelector('#test-button').addEventListener('click', function() {
clickCount++;
console.log(`Button clicked ${clickCount} times`);
plausible('Button Click');
});
// Test 3: Properties are correctly formatted
plausible('Test Event', {
props: {
test_property: 'test_value',
numeric_value: '123', // Must be string
boolean_value: 'true' // Must be string
}
});
// Test 4: Revenue tracking works
plausible('Test Purchase', {
revenue: {
currency: 'USD',
amount: 0.01
},
props: {
test: 'true'
}
});
Performance Testing
Monitor the impact of event tracking on page performance:
// Measure event tracking performance
console.time('event-tracking');
plausible('Performance Test');
console.timeEnd('event-tracking');
// Typically should be < 10ms
// Test under load
async function loadTest() {
for (let i = 0; i < 100; i++) {
plausible('Load Test', { props: { iteration: i.toString() } });
await sleep(10);
}
}
Troubleshooting
| Issue | Possible Cause | Solution |
|---|---|---|
| Events not appearing in dashboard | Goal not configured in dashboard | Add goal in Plausible settings before sending events |
| Event name mismatch | Case sensitivity issue | Ensure event name matches dashboard configuration exactly |
| Properties not showing | Properties not enabled | Enable custom properties in Plausible site settings (paid plans) |
| Revenue data missing | Incorrect format | Verify revenue object has currency (string) and amount (number) |
| Duplicate events | Multiple event listeners | Remove duplicate listeners; use event delegation |
| SPA pageviews not tracking | Manual pageview not called | Call plausible('pageview') after route changes |
| Events delayed | Network latency | Events may take 1-2 seconds to appear; this is normal |
| Ad blocker preventing events | Plausible domain blocked | Implement custom proxy with first-party domain |
| Property values truncated | Value too long | Limit property values to 500 characters |
| Events fire but props empty | Props object incorrect format | Ensure props are nested: {props: {key: 'value'}} |
| Revenue not in correct currency | Currency code invalid | Use ISO 4217 codes (USD, EUR, GBP, etc.) |
| Events tracked on wrong domain | data-domain attribute wrong | Verify data-domain in script tag matches your domain |
| Callback not firing | Callback function error | Check browser console for JavaScript errors |
| Pageview goal not triggering | URL pattern mismatch | Check URL pattern in goal configuration; test with wildcards |
| Outbound links not tracked | Script extension not loaded | Use script.outbound-links.js version |
| File downloads not tracked | Missing data attribute | Add data-plausible-event-name to download links |
| Historical data incomplete | Events sent before goal configured | Goals must be configured before sending events |
| Cross-domain events lost | Session tracking broken | Verify cross-domain tracking configuration |
| Mobile events not tracking | Script blocked on mobile | Check mobile browser console; verify script loads |
| Event fired too early | Script not loaded yet | Ensure plausible function exists before calling |
Best Practices
- Configure Goals First: Always set up goals in dashboard before implementing tracking code
- Keep Names Simple: Use clear, concise event names that are easy to understand in reports
- Consistent Naming: Choose a naming convention (Title Case recommended) and stick to it
- Avoid PII: Never include personally identifiable information in events or properties
- Test Thoroughly: Test all events in staging before deploying to production
- Use Properties Wisely: Limit to essential context; remember the 30-property limit per site
- Document Events: Maintain a registry of all events and their purposes
- Monitor Performance: Track the impact of event code on page load times
- Version Control: Keep event tracking code in version control
- Regular Audits: Review events quarterly to remove unused goals and update documentation