Custom Event Tracking & Analytics Setup -- From Page Views to User Actions
In this tutorial, you'll learn about Custom Event Tracking & Analytics Setup. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
Custom event tracking captures specific user interactions like button clicks, form submissions, file downloads, and feature usage beyond standard page views, enabling data-driven product decisions and funnel optimization.
What You'll Learn
In this tutorial, you will learn how to design, implement, and validate custom event tracking across client-side JavaScript, Google Analytics 4, Plausible, and server-side Google Tag Manager, with data quality checks to prevent garbage-in-garbage-out analytics.
Why It Matters
Page views tell you nothing about user behavior. Do users click the signup button? Do they complete the checkout flow? Do they use the search feature? Custom events answer these questions. Without them, you make product decisions based on guesses rather than data.
Real-World Use
Durga Antivirus Pro tracks 47 custom events per session, including scan initiation, threat detection, quarantine actions, and update checks. This data revealed that 68% of users who run a full scan never complete it, leading to an optimized scan flow that increased completion rates by 34%.
Event Tracking Architecture
flowchart LR
A[User Action] --> B[JavaScript Listener]
B --> C[Client-Side Queue]
C --> D{Validation}
D -->|Valid| E[Send to Server]
D -->|Invalid| F[Log Warning]
E --> G[Server-Side Validation]
G --> H[(Event Store)]
H --> I[Analytics Dashboard]
H --> J[Data Warehouse]
Designing an Event Schema
Before writing code, define your event schema with consistent naming:
const EventSchema = {
event_name: "string", // snake_case, e.g. "button_click"
timestamp: "ISO8601", // auto-generated
user_id: "string|null", // hashed, never raw
session_id: "string", // generated per visit
properties: {
element_id: "string",
page_URL: "string",
referrer: "string|null",
value: "number|null", // for numeric events
},
};
Expected behavior: Every event follows this schema. Invalid events are rejected at the validation layer before reaching storage.
Client-Side Event Tracking
Track a form submission with custom properties:
class AnalyticsTracker {
constructor(endpoint) {
this.endpoint = endpoint;
this.queue = [];
this.flushInterval = setInterval(() => this.flush(), 5000);
}
track(eventName, properties = {}) {
const event = {
event_name: eventName,
timestamp: new Date().toISOString(),
session_id: this.getSessionId(),
properties,
};
this.queue.push(event);
if (this.queue.length >= 10) this.flush();
}
async flush() {
if (this.queue.length === 0) return;
const batch = this.queue.splice(0);
try {
const res = await fetch(this.endpoint, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ events: batch }),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
} catch (err) {
console.error("Analytics flush failed:", err);
this.queue.unshift(...batch);
}
}
getSessionId() {
let id = sessionStorage.getItem("session_id");
if (!id) {
id = crypto.randomUUID();
sessionStorage.setItem("session_id", id);
}
return id;
}
}
const tracker = new AnalyticsTracker("/api/analytics/events");
// Track button click
document.getElementById("signup-btn").addEventListener("click", () => {
tracker.track("button_click", {
element_id: "signup-btn",
page_url: window.location.pathname,
});
});
Expected behavior: Each button click queues an event. Events flush in batches of 10 or every 5 seconds. Failed requests retry from the Queue. The session ID persists across page reloads via sessionStorage.
Server-Side Event Validation
Validate events before storing them:
from datetime import datetime
import re
REQUIRED_FIELDS = {"event_name", "timestamp", "session_id"}
ALLOWED_EVENTS = {"button_click", "form_submit", "file_download", "search", "page_scroll"}
def validate_event(event):
errors = []
missing = REQUIRED_FIELDS - set(event.keys())
if missing:
errors.append(f"Missing fields: {missing}")
if event.get("event_name") not in ALLOWED_EVENTS:
errors.append(f"Unknown event: {event.get('event_name')}")
try:
datetime.fromisoformat(event.get("timestamp", ""))
except (ValueError, TypeError):
errors.append("Invalid timestamp format")
if not re.match(r"^[a-f0-9-]{36}$", event.get("session_id", "")):
errors.append("Invalid session_id format")
prop_len = len(json.dumps(event.get("properties", {})))
if prop_len > 10000:
errors.append(f"Properties too large: {prop_len} bytes")
return errors
Expected output: Valid events return an empty error list. Invalid events return specific error messages for each violation.
Google Analytics 4 Custom Events
Send custom events to GA4 using the gtag API:
// In Google Tag Manager or gtag snippet
gtag("event", "file_download", {
file_name: "dodabrowser-setup.exe",
file_extension: "exe",
file_size_mb: 42.5,
download_method: "direct",
});
Expected behavior: Events appear in GA4 under Reports > Engagement > Events within 24 hours. Custom dimensions and metrics must be registered in GA4 admin settings first.
Tool Comparison
| Feature | Plausible | GA4 | PostHog | Mixpanel |
|---|---|---|---|---|
| Custom events | Yes (Goals) | Yes | Yes | Yes |
| Event properties | Yes | Yes | Yes | Yes |
| Data retention | Unlimited | 2-14 months | Unlimited | 5 years (pay) |
| Server-side SDK | No (API) | Yes | Yes | Yes |
| Free tier | 10K/mo | Unlimited | 1M events/mo | 20M events/mo |
Common Errors
1. Inconsistent Event Naming
Using button_click, ButtonClick, and btn-click in different places creates data chaos. Enforce a single naming convention (snake_case) across all events.
2. Forgetting to Validate Client Data
Client-side events can be forged. Always validate event names, property types, and value ranges on the server before storing.
3. Queue Overflow Without Batching
Sending events one-by-one as HTTP requests creates network overhead. Always batch events and flush them on an interval or at a Queue size threshold.
4. Missing Session Context
Events without session IDs cannot be grouped into user journeys. Always attach a session identifier generated on first page load.
5. Silent Failures
Failed events that are silently dropped create data holes. Implement retry logic with exponential backoff and log all failures for monitoring.
Practice Questions
1. What fields are essential in every custom event? event_name, timestamp, session_id, and a properties object. Without these, events cannot be validated, grouped, or analyzed meaningfully.
2. Why batch events instead of sending individually? Batching reduces HTTP overhead, improves network efficiency, and lowers server load. Most analytics SDKs batch events at intervals of 5-30 seconds.
3. How does server-side validation improve data quality? Server-side validation catches malformed, forged, or unexpected events before they pollute the analytics data store, ensuring dashboards reflect real user behavior.
4. What is the purpose of the session ID? The session ID groups events from a single visit, enabling funnel analysis, session duration calculation, and user journey mapping across page views.
5. Challenge: Design and implement a custom event tracking system with client-side batching, server-side validation, retry logic with exponential backoff, and a dead-letter Queue for persistently failing events. Test with a simulated form submission flow.
Mini Project
Build a feature adoption tracker for a signup flow with four steps: email entry, password creation, profile setup, and confirmation. Track step completions, abandonment, time per step, and error rates. Create a funnel visualization that shows where users drop off and trigger an alert if any step has a completion rate below 40%.
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro