Guides

Building Web Analytics with open-source tools

This guide outlines the implementation of a privacy-first, cookie-less web analytics strategy. It focuses on bypassing ad-blockers via reverse proxying and instrumenting custom event tracking for product-led growth metrics without requiring user consent banners.

60 minutes5 steps
1

Configure a Reverse Proxy for the Tracking Script

To prevent ad-blockers from stripping your analytics script, proxy the tracking script through your own domain. This ensures the request originates from your first-party domain rather than a known tracking CDN.

vercel.json
{
  "rewrites": [
    {
      "source": "/stats/js/script.js",
      "destination": "https://plausible.io/js/script.js"
    },
    {
      "source": "/stats/api/event",
      "destination": "https://plausible.io/api/event"
    }
  ]
}

⚠ Common Pitfalls

  • Failure to proxy the API endpoint (/api/event) will result in script loading but data failing to send.
  • Ensure your analytics provider supports custom domains or proxying before implementation.
2

Initialize the Tracking Snippet

Inject the script into your application's head. Use data-attributes to define the domain and ensure the script points to your proxied path rather than the provider's default URL.

<script defer data-domain="yourdomain.com" data-api="/stats/api/event" src="/stats/js/script.js"></script>

⚠ Common Pitfalls

  • Setting data-domain incorrectly will lead to a 403 Forbidden error from the analytics collector.
  • Loading the script synchronously will negatively impact Core Web Vitals (LCP).
3

Implement Custom Event Tracking for UI Interactions

Standard pageview tracking is insufficient for modern apps. Use a global helper to trigger custom events for specific user actions like 'Signup Clicked' or 'AI Prompt Submitted'.

lib/analytics.ts
export const trackEvent = (eventName: string, props?: Record<string, string | number | boolean>) => {
  if (typeof window !== 'undefined' && (window as any).plausible) {
    (window as any).plausible(eventName, { props });
  }
};

⚠ Common Pitfalls

  • Do not include Personally Identifiable Information (PII) like email addresses in props to maintain GDPR compliance.
  • Ensure the script has finished loading before calling the tracking function, or use a queueing pattern.
4

Track Server-Side Events for AI Features

Client-side tracking can miss events if a user closes the tab before an AI response finishes. Trigger a server-side POST request to the analytics API when an AI generation completes.

api/generate.ts
await fetch('https://yourdomain.com/stats/api/event', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', 'User-Agent': userAgent },
  body: JSON.stringify({
    name: 'ai_generation_complete',
    url: 'https://yourdomain.com/dashboard',
    domain: 'yourdomain.com',
    props: { model: 'gpt-4', tokens: 450 }
  })
});

⚠ Common Pitfalls

  • Server-side calls require manual User-Agent and X-Forwarded-For headers to accurately record geographic data.
  • Rate limits on the analytics provider's API can drop server-side events if not handled with a retry logic.
5

Verify Data Accuracy and Payload Structure

Open the browser's Network tab and filter by the proxied path. Trigger an event and verify the payload is a valid JSON object with the expected properties. Check that no cookies are being set in the Response Headers.

⚠ Common Pitfalls

  • Testing with an active ad-blocker that uses CNAME cloaking detection might still block the proxy; test in a clean environment.
  • Verify that the 'domain' field in the payload matches exactly what is configured in your analytics dashboard.

What you built

You now have a robust, privacy-compliant analytics setup that avoids common ad-blocking pitfalls. By using a reverse proxy and custom event helpers, you can track deep product engagement metrics while maintaining a zero-cookie footprint.