Real-time analytics show what is happening on your site right now. This tutorial uses Server-Sent Events (SSE) for live updates without WebSocket complexity.
Analytics Event Tracking
// lib/analytics.ts
import { db } from "@/db";
import { events } from "@/db/schema";
interface TrackEvent {
name: string;
path: string;
referrer?: string;
userAgent?: string;
sessionId: string;
properties?: Record<string, string>;
}
export async function trackEvent(event: TrackEvent) {
await db.insert(events).values({
name: event.name,
path: event.path,
referrer: event.referrer ?? null,
userAgent: event.userAgent ?? null,
sessionId: event.sessionId,
properties: event.properties ?? {},
createdAt: new Date(),
});
}
export async function getRealtimeStats() {
const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
// Active visitors (unique sessions in last 5 min)
const activeResult = await db.execute(
`SELECT COUNT(DISTINCT session_id) as count FROM events WHERE created_at > $1`,
[fiveMinutesAgo]
);
// Page views in last 24h
const viewsResult = await db.execute(
`SELECT COUNT(*) as count FROM events WHERE name = 'pageview' AND created_at > $1`,
[twentyFourHoursAgo]
);
// Top pages (last 24h)
const topPages = await db.execute(
`SELECT path, COUNT(*) as views FROM events WHERE name = 'pageview' AND created_at > $1 GROUP BY path ORDER BY views DESC LIMIT 10`,
[twentyFourHoursAgo]
);
// Views per hour (last 24h)
const viewsPerHour = await db.execute(
`SELECT date_trunc('hour', created_at) as hour, COUNT(*) as views FROM events WHERE name = 'pageview' AND created_at > $1 GROUP BY hour ORDER BY hour`,
[twentyFourHoursAgo]
);
// Top referrers (last 24h)
const referrers = await db.execute(
`SELECT referrer, COUNT(*) as count FROM events WHERE referrer IS NOT NULL AND referrer != '' AND created_at > $1 GROUP BY referrer ORDER BY count DESC LIMIT 10`,
[twentyFourHoursAgo]
);
return {
activeVisitors: Number(activeResult.rows[0]?.count ?? 0),
pageViews24h: Number(viewsResult.rows[0]?.count ?? 0),
topPages: topPages.rows as { path: string; views: number }[],
viewsPerHour: viewsPerHour.rows as { hour: string; views: number }[],
referrers: referrers.rows as { referrer: string; count: number }[],
updatedAt: new Date().toISOString(),
};
}
SSE Endpoint for Live Updates
// app/api/analytics/stream/route.ts
import { getRealtimeStats } from "@/lib/analytics";
export const dynamic = "force-dynamic";
export async function GET() {
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
async function sendUpdate() {
try {
const stats = await getRealtimeStats();
const data = `data: ${JSON.stringify(stats)}\n\n`;
controller.enqueue(encoder.encode(data));
} catch (error) {
console.error("Analytics stream error:", error);
}
}
// Send initial data
await sendUpdate();
// Update every 10 seconds
const interval = setInterval(sendUpdate, 10000);
// Clean up on close
const cleanup = () => {
clearInterval(interval);
try {
controller.close();
} catch {
// Already closed
}
};
// AbortController not available in all environments
// The interval will be cleared when the stream ends
setTimeout(cleanup, 5 * 60 * 1000); // 5 min max connection
},
});
return new Response(stream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
});
}
Hook for SSE Connection
// hooks/use-event-source.ts
"use client";
import { useEffect, useState, useCallback, useRef } from "react";
export function useEventSource<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [connected, setConnected] = useState(false);
const eventSourceRef = useRef<EventSource | null>(null);
const connect = useCallback(() => {
if (eventSourceRef.current) {
eventSourceRef.current.close();
}
const es = new EventSource(url);
eventSourceRef.current = es;
es.onopen = () => setConnected(true);
es.onmessage = (event) => {
try {
const parsed = JSON.parse(event.data) as T;
setData(parsed);
} catch {
// Invalid JSON
}
};
es.onerror = () => {
setConnected(false);
es.close();
// Reconnect after 5 seconds
setTimeout(connect, 5000);
};
}, [url]);
useEffect(() => {
connect();
return () => {
eventSourceRef.current?.close();
};
}, [connect]);
return { data, connected };
}
Dashboard Component
"use client";
import { useEventSource } from "@/hooks/use-event-source";
interface AnalyticsData {
activeVisitors: number;
pageViews24h: number;
topPages: { path: string; views: number }[];
viewsPerHour: { hour: string; views: number }[];
referrers: { referrer: string; count: number }[];
updatedAt: string;
}
export function RealtimeDashboard() {
const { data, connected } = useEventSource<AnalyticsData>(
"/api/analytics/stream"
);
if (!data) {
return (
<div className="animate-pulse space-y-6">
<div className="grid grid-cols-2 gap-4">
{[1, 2].map((i) => (
<div key={i} className="h-24 bg-muted rounded-lg" />
))}
</div>
<div className="h-64 bg-muted rounded-lg" />
</div>
);
}
return (
<div className="space-y-6">
{/* Connection Status */}
<div className="flex items-center gap-2 text-sm">
<div
className={`h-2 w-2 rounded-full ${
connected ? "bg-green-500" : "bg-red-500"
}`}
/>
<span className="text-muted-foreground">
{connected ? "Live" : "Reconnecting..."}
</span>
<span className="text-xs text-muted-foreground ml-auto">
Updated {new Date(data.updatedAt).toLocaleTimeString()}
</span>
</div>
{/* Metric Cards */}
<div className="grid grid-cols-2 gap-4">
<MetricCard
label="Active Visitors"
value={data.activeVisitors}
live
/>
<MetricCard
label="Page Views (24h)"
value={data.pageViews24h}
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Top Pages */}
<div className="border rounded-lg p-4">
<h3 className="font-semibold mb-3">Top Pages</h3>
<div className="space-y-2">
{data.topPages.map((page) => (
<div key={page.path} className="flex items-center justify-between text-sm">
<span className="truncate max-w-[200px]">{page.path}</span>
<span className="text-muted-foreground font-mono">
{page.views.toLocaleString()}
</span>
</div>
))}
</div>
</div>
{/* Top Referrers */}
<div className="border rounded-lg p-4">
<h3 className="font-semibold mb-3">Top Referrers</h3>
<div className="space-y-2">
{data.referrers.map((ref) => (
<div key={ref.referrer} className="flex items-center justify-between text-sm">
<span className="truncate max-w-[200px]">{ref.referrer}</span>
<span className="text-muted-foreground font-mono">
{ref.count.toLocaleString()}
</span>
</div>
))}
</div>
</div>
</div>
{/* Views Over Time */}
<div className="border rounded-lg p-4">
<h3 className="font-semibold mb-3">Page Views (Last 24 Hours)</h3>
<MiniBarChart data={data.viewsPerHour} />
</div>
</div>
);
}
function MetricCard({
label,
value,
live,
}: {
label: string;
value: number;
live?: boolean;
}) {
return (
<div className="border rounded-lg p-4">
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">{label}</span>
{live && (
<span className="relative flex h-2 w-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75" />
<span className="relative inline-flex rounded-full h-2 w-2 bg-green-500" />
</span>
)}
</div>
<div className="text-3xl font-bold mt-1">{value.toLocaleString()}</div>
</div>
);
}
function MiniBarChart({ data }: { data: { hour: string; views: number }[] }) {
const maxViews = Math.max(...data.map((d) => d.views), 1);
return (
<div className="flex items-end gap-1 h-32">
{data.map((d) => (
<div
key={d.hour}
className="flex-1 bg-primary/80 rounded-t min-h-[2px] hover:bg-primary transition-colors"
style={{ height: `${(d.views / maxViews) * 100}%` }}
title={`${new Date(d.hour).toLocaleTimeString()}: ${d.views} views`}
/>
))}
</div>
);
}
Track Page Views
// components/PageViewTracker.tsx
"use client";
import { useEffect } from "react";
import { usePathname } from "next/navigation";
export function PageViewTracker() {
const pathname = usePathname();
useEffect(() => {
const sessionId = getOrCreateSessionId();
fetch("/api/analytics/track", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: "pageview",
path: pathname,
referrer: document.referrer || undefined,
sessionId,
}),
}).catch(() => {});
}, [pathname]);
return null;
}
function getOrCreateSessionId(): string {
const key = "analytics_session";
let id = sessionStorage.getItem(key);
if (!id) {
id = crypto.randomUUID();
sessionStorage.setItem(key, id);
}
return id;
}
Need Custom Analytics?
We build privacy-friendly analytics dashboards with real-time tracking and custom events. Contact us to own your analytics data.