Using Umami for privacy friendly analytics
Learn how to implement Umami analytics in your website to track user behavior while respecting privacy. A complete guide with code examples and best practices.

Matias Facello

Introduction
So, you want to understand how people use your website without being invasive about it? Most analytics tools out there track visitors across the web, build profiles, and create privacy headaches. That’s not great for users, and for developers, it means dealing with GDPR, cookie banners, and legal compliance.
Umami is a lightweight, open-source analytics tool that respects user privacy while still giving you the key insights you need. No cookies. No personal data collection. No cross-site tracking. Just clean, honest data about how people interact with your site.
In this post, I’ll share why I picked Umami over other tools, when it might not be the right fit, and how I integrated it into my Next.js app. I’ll also walk through event tracking, deployment options, a peek at the dashboard, and a few lessons learned along the way.
Why Umami instead of the usual stuff?
For me, the biggest selling point is that Umami takes privacy seriously, not as a checkbox, but as a core philosophy. It’s GDPR-compliant without needing cookie banners. It doesn’t track users across sites, store IP addresses, or build personal profiles. All the data is anonymized and aggregated, and nothing gets shared with third parties.
From a developer’s perspective, it’s refreshingly minimal. The script is about 2KB (compared to Google Analytics’ > 100KB), so it barely touches performance.
You can self-host to keep full control over your data or use their cloud offering for zero maintenance. And because it’s open source, you can audit the code or tweak it to your needs.
Even with this privacy-first approach, you still get plenty of valuable insights: page views, unique visitors, referrers, device and browser breakdowns, and custom events for the interactions that matter most. It’s the “just enough” approach to analytics.

Why not choose Umami
Even though I like it a lot, Umami isn’t perfect for every scenario.
If you need granular, user-level tracking for personalization, or if your marketing team runs ad campaigns that depend on building detailed user profiles, Umami isn’t the right tool. It doesn’t offer heatmaps, session recordings, or advanced segmentation based on personal data.
If you need those, you might be better off with tools like Plausible (still privacy-focused but with more analytics depth) or traditional tools like GA4 (at the cost of user privacy).
Setting Up Umami
First things first, you'll need to add some environment variables.
# The website ID and the Script URL are the two basic things needed NEXT_PUBLIC_UMAMI_WEBSITE_ID=your-website-id NEXT_PUBLIC_UMAMI_SCRIPT_URL=https://your-umami-instance.com/umami.js # We can also add a domain to tell the script to track only certain domains NEXT_PUBLIC_UMAMI_DOMAINS=yourdomain.com
Umami is pretty simple to integrate. In most common cases, only adding the script already guarantees basic tracking of page views.
<script defer src="https://www.umami.com/script.js" data-website-id="0287c722-686e-488f-bf39-418daa197960" ></script>
In my case, I created a file that handles all the Umami setup.
import Script from "next/script"; export function UmamiScript() { const scriptUrl = process.env.NEXT_PUBLIC_UMAMI_SCRIPT_URL; const websiteId = process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID; const domains = process.env.NEXT_PUBLIC_UMAMI_DOMAINS; if (!scriptUrl || !websiteId) return null; return ( <Script src={scriptUrl} data-website-id={websiteId} {...(domains && { "data-domains": domains })} strategy="afterInteractive" /> ); }
I prefer this over a raw <script>
tag because it’s cleaner and the recommended thing for Next.
Just add <UmamiScript />
to your root layout and you’re done with the base tracking setup. With this, we already have a basic tracking of page views.
Tracking events (the interesting part)
Umami has two simple ways of tracking, one using data attributes and the other using a JavaScript function.
Using data attributes is an easy way to track links and buttons. We can also add a "custom" field using the data-umami-event-{key}={value}
way to add more information if we need it.
<button id="signup-button" data-umami-event="Signup button">Sign up</button> <a src="" data-umami-event="Navbar Link" data-umami-event-label="/blog"> /blog </a>
In my case, I moved this to a JavaScript function so I can use it throughout the app:
export const trackEventData = (eventName: string, eventData?: EventData) => { return { "data-umami-event": eventName, ...(eventData && { [`data-umami-event-${eventData.label}`]: eventData.value, }), }; }; type EventData = { label: string; value: string };
For example:
// Usage <Link href="/blog" {...trackEventData(eventNames.heroButtonClick)} > Read My Thoughts <svg /> </Link> // Results in <a href="/blog" data-umami-event="Hero Button - Click" > Read My Thoughts <svg /> </a>
I like this approach because it’s declarative. You can see at a glance what’s being tracked.
In the case of the JavaScript function, it is as simple as calling the Umami function added to the browser window.
button.onclick = () => umami.track("Signup button"); // Tracks the current page umami.track(); // Custom event with data umami.track(event_name: string, data: object);
I prefer the first way, but this way is useful for code that doesn't involve HTML, for example, I use it to track the read progress on the blog or the contact form. But since we need the window value from the browser for this, we can use it only on Client components in the React/Next environment.
export const trackEvent = (eventName: string, eventData?: EventData): void => { // Validate if window exists if (typeof window === "undefined" || !window.umami) { return; } // Try catch to call the event track try { window.umami.track(eventName, eventData); } catch (error) { console.error(`[Umami] Failed to track event ${eventName}:`, error); } };
This is a simplified version of my scroll tracking event.
// State to validate the article is completed only once per page refresh const [isCompleted, setIsCompleted] = useState(false); const scrollTop = window.scrollY; const docHeight = document.documentElement.scrollHeight - window.innerHeight; const scrollPercent = scrollTop / docHeight; const progress = Math.min(scrollPercent * 100, 100); if (progress >= 90 && !isCompleted) { setIsCompleted(true); trackEvent(eventNames.blogPostRead); }
This gives me a sense of whether people are actually reading my posts or just checking them. It's surprisingly useful for understanding content engagement.
Whichever you choose, it’s worth keeping a shared list of event names to stay consistent:
export const eventNames = { heroButtonClick: "Hero Button - Click", navbarClick: "Navbar - Click", blogPostRead: "Blog Post - Read", // Form events formSubmit: "Form - Submit", formError: "Form - Error", formSuccess: "Form - Success", };
I learned pretty quickly that having a consistent naming convention makes life much easier. If we use conventions, tracking will be more consistent, and we can understand user behavior better.
Deployment Options
Umami has two options for deployment. One is to self-host on your own server, and the other is to use their Cloud.
If you are starting, their free tier is pretty good, so you can use it unless you have a website with a lot of traffic.
Option 1: Umami Cloud
If you don't want to deal with hosting and maintenance. It's free tier, it's good (at the moment of writing), it includes 100K events per month, up to 3 websites, and 6 months of data retention.
And the most important things when not doing self-hosting: zero maintenance, reliable hosting, and automatic updates.
The downside is that your data lives on their servers, but since Umami doesn't collect personal information anyway, this doesn't bother me much.
Option 2: Self-Hosting with Docker
If you want complete control, you can self-host.
They have a small tutorial on how to do it: Installation - Docs - Umami
Since I worked a bit with a company that hosts most of its websites, I have grown used to it, so I use this option.
It still adds another layer of complexity, but I like it.
What I Learned Along the Way
The biggest lesson? Start small. Don’t try to track everything from day one; it only creates noise. Begin with page views, then add events as you see patterns you want to measure.
Good naming matters more than you think. “Click” tells you nothing; “Blog Post - Tag - Click” gives context you can act on.
And even though Umami makes it easy to stay privacy-friendly, it’s worth double-checking that your event tracking doesn’t sneak in anything personal.
Wrapping Up
Umami has been a great fit for my projects: lightweight, easy to integrate, and most importantly, respectful of users. It’s proof that you can get useful analytics without following people around the internet.
Remember: Good analytics should help you serve your users better, not exploit them.
If you’re curious, give it a try; whether you self-host or go cloud, you can be up and running in minutes.
And if you’re working on something privacy-first (or thinking about it), I’d love to hear how you approach analytics without sacrificing trust.
Happy tracking! 📊
Matias
Table of Contents