Umami Event Tracking Setup: Custom Events | OpsBlu Docs

Umami Event Tracking Setup: Custom Events

How to implement event tracking in Umami analytics. Covers data-umami-event attributes, JavaScript API, ecommerce tracking, custom properties, and...

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:

  1. JavaScript API: Programmatic tracking with umami.track()
  2. Data Attributes: No-code tracking using HTML attributes
  3. 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
      });
    }
  });
});

Time on Page:

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:

  1. Trigger event in your application
  2. Go to Umami dashboard
  3. Navigate to your website
  4. Click "Events" tab
  5. Look for your event name in the list
  6. 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

  1. Use Descriptive Event Names: newsletter_signup not ns or signup
  2. Consistent Naming Convention: Use snake_case or camelCase consistently
  3. Group Related Events: Use prefixes like checkout_started, checkout_completed
  4. Include Context in Metadata: Add relevant information to understand the event
  5. Don't Over-Track: Focus on business-critical events, not every interaction
  6. Avoid PII: Never include emails, names, or other personal information
  7. Test Before Deploying: Verify events appear in dashboard
  8. Document Events: Maintain a list of tracked events and their meaning
  9. Use Helper Functions: Centralize tracking logic for consistency
  10. 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

  1. Tracking Before Umami Loads: Always check window.umami exists
  2. Typos in Event Names: Use constants to avoid typos
  3. Tracking Too Much: Focus on actionable insights
  4. Inconsistent Data Structure: Keep event metadata consistent
  5. 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.