Event Tracking in Umami
Umami provides flexible event tracking capabilities that let you measure user interactions beyond basic page views. With both JavaScript API and HTML data attribute approaches, you can track clicks, form submissions, custom actions, and business-critical events - all while maintaining Umami's privacy-first principles.
Event Tracking Methods
Umami offers two primary methods for tracking events:
- JavaScript API: Programmatic tracking with
umami.track() - Data Attributes: No-code tracking using HTML attributes
- Automatic Tracking: Auto-capture certain interactions
Both methods are privacy-friendly and don't require cookies or persistent user identification.
JavaScript API Tracking
The umami.track() function is the most flexible way to track events:
Basic Syntax:
umami.track(eventName, eventData);
Simple Event (No Data):
// Track newsletter signup
umami.track('newsletter_signup');
// Track button click
umami.track('cta_clicked');
// Track download
umami.track('whitepaper_download');
Event with Metadata:
// Track with additional context
umami.track('product_purchased', {
product: 'Pro Plan',
value: 99,
currency: 'USD',
payment_method: 'stripe'
});
// Track feature usage
umami.track('export_data', {
format: 'CSV',
rows: 1500,
file_size: '2.3MB'
});
Checking if Umami is Loaded:
// Always check if umami exists before tracking
if (window.umami) {
umami.track('event_name', { key: 'value' });
}
// Or use optional chaining
window.umami?.track('event_name', { key: 'value' });
Data Attribute Tracking
For simple click tracking without JavaScript, use data attributes:
Basic Button Tracking:
<!-- Track button click -->
<button data-umami-event="signup_button">Sign Up</button>
<!-- Track link click -->
<a href="/pricing" data-umami-event="view_pricing">View Pricing</a>
<!-- Track form submit -->
<form data-umami-event="contact_form_submit">
<!-- form fields -->
<button type="submit">Submit</button>
</form>
Custom Event Data with Attributes:
<!-- Track with additional data -->
<button
data-umami-event="product_click"
data-umami-event-product="Pro Plan"
data-umami-event-price="99">
Buy Now
</button>
<!-- Multiple data attributes -->
<a
href="/download/guide.pdf"
data-umami-event="download"
data-umami-event-file="product-guide"
data-umami-event-type="pdf">
Download Guide
</a>
When Event Data Attributes Are Processed:
The tracking fires automatically when:
- Buttons: On click
- Links: On click (before navigation)
- Forms: On submit (before submission)
- Any element: On click if data-umami-event is present
Common Event Tracking Patterns
User Interactions
Navigation Tracking:
// Track menu clicks
document.querySelectorAll('.main-nav a').forEach(link => {
link.addEventListener('click', (e) => {
umami.track('nav_click', {
section: e.target.dataset.section,
label: e.target.textContent.trim()
});
});
});
Button Clicks:
// Track CTA button
document.getElementById('cta-button').addEventListener('click', () => {
umami.track('cta_clicked', {
location: 'hero',
button_text: 'Get Started'
});
});
Modal/Dialog Interactions:
// Track modal open
function openModal(modalId) {
umami.track('modal_opened', {
modal_id: modalId
});
// ... modal opening logic
}
// Track modal close
function closeModal(modalId) {
umami.track('modal_closed', {
modal_id: modalId,
time_open: getTimeOpen() // seconds
});
// ... modal closing logic
}
Form Tracking
Form Submission:
document.getElementById('contact-form').addEventListener('submit', (e) => {
umami.track('form_submit', {
form_name: 'contact',
fields_count: e.target.elements.length
});
});
Form Field Interactions:
// Track when users start filling a form
document.getElementById('signup-email').addEventListener('focus', () => {
umami.track('signup_started', {
field: 'email'
});
}, { once: true }); // Only fire once
Form Validation Errors:
function validateForm(formData) {
const errors = [];
if (!formData.email.includes('@')) {
errors.push('email');
umami.track('form_error', {
form: 'signup',
field: 'email',
error: 'invalid_format'
});
}
return errors;
}
E-Commerce Events
Product Views:
// Track product page view
umami.track('product_view', {
product_id: product.id,
category: product.category,
price: product.price
});
Add to Cart:
function addToCart(product) {
// Add to cart logic...
umami.track('add_to_cart', {
product_id: product.id,
quantity: 1,
price: product.price
});
}
Checkout Steps:
// Track checkout flow
umami.track('checkout_started', {
cart_value: getTotalValue(),
items_count: getItemsCount()
});
umami.track('payment_info_entered', {
payment_method: 'credit_card'
});
umami.track('order_completed', {
order_id: orderId,
value: totalValue,
items: itemsCount
});
Content Engagement
Video Tracking:
const video = document.getElementById('product-video');
// Video started
video.addEventListener('play', () => {
umami.track('video_play', {
video_id: 'product-demo',
video_length: video.duration
});
});
// Video progress milestones
let milestones = [25, 50, 75, 100];
video.addEventListener('timeupdate', () => {
const percent = (video.currentTime / video.duration) * 100;
milestones.forEach((milestone, index) => {
if (percent >= milestone && !video.dataset[`milestone${milestone}`]) {
video.dataset[`milestone${milestone}`] = 'true';
umami.track('video_progress', {
video_id: 'product-demo',
percent: milestone
});
}
});
});
// Video completed
video.addEventListener('ended', () => {
umami.track('video_complete', {
video_id: 'product-demo'
});
});
Scroll Depth:
let scrollDepths = [25, 50, 75, 100];
let trackedDepths = new Set();
window.addEventListener('scroll', () => {
const scrollPercent = (window.scrollY / (document.body.scrollHeight - window.innerHeight)) * 100;
scrollDepths.forEach(depth => {
if (scrollPercent >= depth && !trackedDepths.has(depth)) {
trackedDepths.add(depth);
umami.track('scroll_depth', {
depth: depth,
page: window.location.pathname
});
}
});
});
let startTime = Date.now();
window.addEventListener('beforeunload', () => {
const timeOnPage = Math.round((Date.now() - startTime) / 1000);
umami.track('time_on_page', {
page: window.location.pathname,
seconds: timeOnPage
});
});
File Downloads:
document.querySelectorAll('a[download]').forEach(link => {
link.addEventListener('click', (e) => {
const fileName = e.target.getAttribute('download') || e.target.href.split('/').pop();
umami.track('file_download', {
file_name: fileName,
file_type: fileName.split('.').pop(),
link_location: e.target.dataset.location
});
});
});
Search and Filtering
Search Queries:
document.getElementById('search-form').addEventListener('submit', (e) => {
e.preventDefault();
const query = e.target.querySelector('input[name="q"]').value;
const resultsCount = getSearchResults(query).length;
umami.track('search', {
query: query.toLowerCase(),
results_count: resultsCount,
has_results: resultsCount > 0
});
});
Filter Usage:
function applyFilter(filterType, filterValue) {
// Apply filter logic...
umami.track('filter_applied', {
filter_type: filterType,
filter_value: filterValue,
results_count: getFilteredResultsCount()
});
}
Framework-Specific Implementations
React
import { useEffect } from 'react';
function ProductCard({ product }) {
const trackProductClick = () => {
window.umami?.track('product_click', {
product_id: product.id,
category: product.category
});
};
return (
<div
<h3>{product.name}</h3>
<p>${product.price}</p>
</div>
);
}
// Custom hook for tracking
function useTrackEvent(eventName, eventData) {
useEffect(() => {
window.umami?.track(eventName, eventData);
}, [eventName, eventData]);
}
Vue.js
export default {
methods: {
trackEvent(eventName, eventData) {
if (window.umami) {
window.umami.track(eventName, eventData);
}
},
handlePurchase() {
this.trackEvent('purchase', {
product: this.product.name,
value: this.product.price
});
// ... purchase logic
}
}
};
// Global mixin for all components
Vue.mixin({
methods: {
$umami(eventName, eventData) {
window.umami?.track(eventName, eventData);
}
}
});
Next.js
// utils/analytics.js
export const trackEvent = (eventName, eventData) => {
if (typeof window !== 'undefined' && window.umami) {
window.umami.track(eventName, eventData);
}
};
// In a component
import { trackEvent } from '@/utils/analytics';
export default function SignupButton() {
const handleClick = () => {
trackEvent('signup_clicked', {
location: 'header'
});
};
return <button Up</button>;
}
Event Tracking Helper Functions
Wrapper Function for Safer Tracking:
function trackEvent(eventName, eventData = {}) {
try {
if (window.umami && typeof window.umami.track === 'function') {
window.umami.track(eventName, eventData);
} else if (process.env.NODE_ENV === 'development') {
console.log('[Umami Debug]', eventName, eventData);
}
} catch (error) {
console.error('Error tracking event:', error);
}
}
// Usage
trackEvent('button_click', { button: 'signup' });
Debounced Event Tracking:
// Useful for rapid events like scroll or resize
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
const trackScroll = debounce(() => {
umami.track('page_scrolled', {
scroll_position: window.scrollY
});
}, 500);
window.addEventListener('scroll', trackScroll);
Testing Event Tracking
Development Mode Logging:
const isDev = process.env.NODE_ENV === 'development';
function trackEvent(eventName, eventData) {
if (isDev) {
console.group('Umami Event Tracked');
console.log('Event Name:', eventName);
console.log('Event Data:', eventData);
console.groupEnd();
}
window.umami?.track(eventName, eventData);
}
Verify Events in Dashboard:
- Trigger event in your application
- Go to Umami dashboard
- Navigate to your website
- Click "Events" tab
- Look for your event name in the list
- Click event to see metadata
Browser Console Testing:
// Test in browser console
umami.track('test_event', {
test: true,
timestamp: new Date().toISOString()
});
// Check if umami is loaded
console.log(window.umami);
Best Practices
- Use Descriptive Event Names:
newsletter_signupnotnsorsignup - Consistent Naming Convention: Use snake_case or camelCase consistently
- Group Related Events: Use prefixes like
checkout_started,checkout_completed - Include Context in Metadata: Add relevant information to understand the event
- Don't Over-Track: Focus on business-critical events, not every interaction
- Avoid PII: Never include emails, names, or other personal information
- Test Before Deploying: Verify events appear in dashboard
- Document Events: Maintain a list of tracked events and their meaning
- Use Helper Functions: Centralize tracking logic for consistency
- Handle Errors: Wrap tracking in try-catch to prevent app crashes
Privacy Considerations
What NOT to Track:
// BAD: Don't track PII
umami.track('signup', {
email: 'user@example.com', // Don't
name: 'John Doe', // Don't
phone: '555-1234' // Don't
});
// GOOD: Track without PII
umami.track('signup', {
source: 'homepage',
plan: 'free',
referrer_type: 'organic'
});
Respecting User Consent:
// Check for consent before tracking
function trackWithConsent(eventName, eventData) {
const hasConsent = localStorage.getItem('analytics_consent') === 'true';
if (hasConsent && window.umami) {
umami.track(eventName, eventData);
}
}
Common Pitfalls
- Tracking Before Umami Loads: Always check
window.umamiexists - Typos in Event Names: Use constants to avoid typos
- Tracking Too Much: Focus on actionable insights
- Inconsistent Data Structure: Keep event metadata consistent
- Forgetting to Test: Verify events appear in dashboard
Event Naming Constants
// Define event names as constants to avoid typos
const EVENTS = {
SIGNUP: 'user_signup',
LOGIN: 'user_login',
PURCHASE: 'purchase_completed',
CART_ADD: 'item_added_to_cart',
DOWNLOAD: 'file_downloaded',
SEARCH: 'search_performed'
};
// Usage
umami.track(EVENTS.SIGNUP, {
source: 'homepage'
});
This structured approach to event tracking ensures you capture valuable insights while maintaining Umami's commitment to user privacy.