chore: update CHANGELOG.md to include enhancements for dashboard performance, including smarter updates, real-time visitor tracking, and faster event processing

This commit is contained in:
Usman Baig
2026-02-27 09:10:08 +01:00
parent 3aa0d7ae7c
commit 3efd23b386
5 changed files with 314 additions and 8 deletions

View File

@@ -8,6 +8,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
### Added
- **Smarter dashboard updates.** Your dashboard now knows when you're actively viewing it versus when it's in the background. When you switch to another tab, we intelligently slow down data refreshes to save resources, then instantly catch up when you return. This keeps your analytics current without putting unnecessary load on the system.
- **Instant real-time visitor counts.** Your dashboard's "current visitors" counter now updates lightning-fast using an optimized tracking system. Instead of scanning your entire database, we maintain a live session index that shows active visitors in milliseconds—even when thousands of people are browsing your sites simultaneously.
- **More accurate visitor tracking.** We've upgraded how we identify unique visitors to ensure your analytics are always precise, even during the busiest traffic spikes. Every visitor now gets a truly unique identifier that never overlaps with others, eliminating rare edge cases where visitor counts could be slightly off.
- **Faster event tracking.** Your analytics data is now captured instantly without slowing down your website. We've switched to asynchronous processing that collects events in batches of 100, so your visitors' page views and interactions are recorded with zero impact on their browsing experience, even during traffic spikes.
- **Faster dashboard loading.** Your site analytics now load almost instantly, even during busy periods. Behind the scenes, we've added intelligent caching that remembers your dashboard data for 30 seconds and refreshes it automatically in the background. Real-time visitor counts are updated every 5 seconds so you always see current activity without waiting.
- **Better data management for long-term performance.** We've restructured how your analytics data is stored so the app stays fast even as you collect months of data. Old data is now automatically organized by month and cleaned up efficiently based on your retention settings, keeping everything running smoothly no matter how much traffic you get.
- **Smarter database indexing.** We've optimized how your analytics data is indexed, making common queries—like loading your dashboard or filtering by date—significantly faster. This also reduces storage overhead, keeping the app lean as your data grows.

View File

@@ -2,7 +2,7 @@
import { useAuth } from '@/lib/auth/context'
import { logger } from '@/lib/utils/logger'
import { useCallback, useEffect, useState } from 'react'
import { useCallback, useEffect, useState, useRef } from 'react'
import { useParams, useRouter } from 'next/navigation'
import { motion } from 'framer-motion'
import { getSite, type Site } from '@/lib/api/sites'
@@ -148,6 +148,23 @@ export default function SiteDashboardPage() {
return { start: prevStart.toISOString().split('T')[0], end: prevEnd.toISOString().split('T')[0] }
}, [])
// * Visibility-aware polling intervals
// * Historical data: 60s when visible, paused when hidden
// * Real-time data: 5s when visible, 30s when hidden
const [isVisible, setIsVisible] = useState(true)
const dashboardIntervalRef = useRef<NodeJS.Timeout | null>(null)
const realtimeIntervalRef = useRef<NodeJS.Timeout | null>(null)
// * Track visibility state
useEffect(() => {
const handleVisibilityChange = () => {
const visible = document.visibilityState === 'visible'
setIsVisible(visible)
}
document.addEventListener('visibilitychange', handleVisibilityChange)
return () => document.removeEventListener('visibilitychange', handleVisibilityChange)
}, [])
const loadData = useCallback(async (silent = false) => {
try {
if (!silent) setLoading(true)
@@ -204,18 +221,60 @@ export default function SiteDashboardPage() {
const data = await getRealtime(siteId)
setRealtime(data.visitors)
} catch (error) {
// Silently fail for realtime updates
// * Silently fail for realtime updates
}
}, [siteId])
// * Visibility-aware polling for dashboard data (historical)
// * Refreshes every 60 seconds when tab is visible, pauses when hidden
useEffect(() => {
if (isSettingsLoaded) loadData()
const interval = setInterval(() => {
if (!isSettingsLoaded) return
// * Initial load
loadData()
// * Clear existing interval
if (dashboardIntervalRef.current) {
clearInterval(dashboardIntervalRef.current)
}
// * Only poll when visible (saves server resources when tab is backgrounded)
if (isVisible) {
dashboardIntervalRef.current = setInterval(() => {
loadData(true)
}, 60000) // * 60 seconds for historical data
}
return () => {
if (dashboardIntervalRef.current) {
clearInterval(dashboardIntervalRef.current)
}
}
}, [siteId, dateRange, todayInterval, multiDayInterval, isSettingsLoaded, loadData, isVisible])
// * Visibility-aware polling for realtime data
// * Refreshes every 5 seconds when visible, every 30 seconds when hidden
useEffect(() => {
if (!isSettingsLoaded) return
// * Clear existing interval
if (realtimeIntervalRef.current) {
clearInterval(realtimeIntervalRef.current)
}
// * Different intervals based on visibility
const interval = isVisible ? 5000 : 30000 // * 5s visible, 30s hidden
realtimeIntervalRef.current = setInterval(() => {
loadRealtime()
}, 30000)
return () => clearInterval(interval)
}, [siteId, dateRange, todayInterval, multiDayInterval, isSettingsLoaded, loadData, loadRealtime])
}, interval)
return () => {
if (realtimeIntervalRef.current) {
clearInterval(realtimeIntervalRef.current)
}
}
}, [siteId, isSettingsLoaded, loadRealtime, isVisible])
useEffect(() => {
if (site?.domain) document.title = `${site.domain} | Pulse`

View File

@@ -0,0 +1,128 @@
// * Custom hook for visibility-aware polling
// * Pauses polling when tab is not visible, resumes when visible
// * Reduces server load when users aren't actively viewing the dashboard
import { useEffect, useRef, useState, useCallback } from 'react'
interface UseVisibilityPollingOptions {
// * Polling interval when tab is visible (in milliseconds)
visibleInterval: number
// * Polling interval when tab is hidden (in milliseconds, or null to pause)
hiddenInterval: number | null
}
interface UseVisibilityPollingReturn {
// * Whether polling is currently active
isPolling: boolean
// * Time since last poll
lastPollTime: number | null
// * Force a poll immediately
triggerPoll: () => void
}
export function useVisibilityPolling(
callback: () => void | Promise<void>,
options: UseVisibilityPollingOptions
): UseVisibilityPollingReturn {
const { visibleInterval, hiddenInterval } = options
const [isPolling, setIsPolling] = useState(false)
const [lastPollTime, setLastPollTime] = useState<number | null>(null)
const intervalRef = useRef<NodeJS.Timeout | null>(null)
const callbackRef = useRef(callback)
// * Keep callback reference up to date
useEffect(() => {
callbackRef.current = callback
}, [callback])
// * Get current polling interval based on visibility
const getInterval = useCallback((): number | null => {
if (typeof document === 'undefined') return null
const isVisible = document.visibilityState === 'visible'
if (isVisible) {
return visibleInterval
}
return hiddenInterval
}, [visibleInterval, hiddenInterval])
// * Start polling with current interval
const startPolling = useCallback(() => {
const interval = getInterval()
if (interval === null) {
setIsPolling(false)
return
}
setIsPolling(true)
// * Clear any existing interval
if (intervalRef.current) {
clearInterval(intervalRef.current)
}
// * Set up new interval
intervalRef.current = setInterval(() => {
callbackRef.current()
setLastPollTime(Date.now())
}, interval)
}, [getInterval])
// * Stop polling
const stopPolling = useCallback(() => {
if (intervalRef.current) {
clearInterval(intervalRef.current)
intervalRef.current = null
}
setIsPolling(false)
}, [])
// * Trigger immediate poll
const triggerPoll = useCallback(() => {
callbackRef.current()
setLastPollTime(Date.now())
// * Restart polling timer
startPolling()
}, [startPolling])
// * Handle visibility changes
useEffect(() => {
if (typeof document === 'undefined') return
const handleVisibilityChange = () => {
if (document.visibilityState === 'visible') {
// * Tab became visible - resume polling with visible interval
startPolling()
// * Trigger immediate poll to get fresh data
triggerPoll()
} else {
// * Tab hidden - switch to hidden interval or pause
const interval = getInterval()
if (interval === null) {
stopPolling()
} else {
// * Restart with hidden interval
startPolling()
}
}
}
// * Listen for visibility changes
document.addEventListener('visibilitychange', handleVisibilityChange)
// * Start polling initially
startPolling()
return () => {
document.removeEventListener('visibilitychange', handleVisibilityChange)
stopPolling()
}
}, [startPolling, stopPolling, triggerPoll, getInterval])
return {
isPolling,
lastPollTime,
triggerPoll,
}
}

114
lib/swr/dashboard.ts Normal file
View File

@@ -0,0 +1,114 @@
// * SWR configuration for dashboard data fetching
// * Implements stale-while-revalidate pattern for efficient data updates
import useSWR from 'swr'
import { getDashboard, getRealtime, getStats, getDailyStats } from '@/lib/api/stats'
import { getSite } from '@/lib/api/sites'
import type { Site } from '@/lib/api/sites'
import type { Stats, DailyStat } from '@/lib/api/stats'
// * SWR fetcher functions
const fetchers = {
site: (siteId: string) => getSite(siteId),
dashboard: (siteId: string, start: string, end: string) => getDashboard(siteId, start, end),
stats: (siteId: string, start: string, end: string) => getStats(siteId, start, end),
dailyStats: (siteId: string, start: string, end: string, interval: 'hour' | 'day' | 'minute') =>
getDailyStats(siteId, start, end, interval),
realtime: (siteId: string) => getRealtime(siteId),
}
// * Standard SWR config for dashboard data
const dashboardSWRConfig = {
// * Keep stale data visible while revalidating (better UX)
revalidateOnFocus: false,
// * Revalidate when reconnecting (fresh data after offline)
revalidateOnReconnect: true,
// * Retry failed requests
shouldRetryOnError: true,
errorRetryCount: 3,
// * Error retry interval with exponential backoff
errorRetryInterval: 5000,
}
// * Hook for site data (loads once, refreshes rarely)
export function useSite(siteId: string) {
return useSWR<Site>(
siteId ? ['site', siteId] : null,
() => fetchers.site(siteId),
{
...dashboardSWRConfig,
// * Site data changes rarely, refresh every 5 minutes
refreshInterval: 5 * 60 * 1000,
// * Deduping interval to prevent duplicate requests
dedupingInterval: 30 * 1000,
}
)
}
// * Hook for dashboard summary data (refreshed less frequently)
export function useDashboard(siteId: string, start: string, end: string) {
return useSWR(
siteId && start && end ? ['dashboard', siteId, start, end] : null,
() => fetchers.dashboard(siteId, start, end),
{
...dashboardSWRConfig,
// * Refresh every 60 seconds for dashboard summary
refreshInterval: 60 * 1000,
// * Deduping interval to prevent duplicate requests
dedupingInterval: 10 * 1000,
}
)
}
// * Hook for stats (refreshed less frequently)
export function useStats(siteId: string, start: string, end: string) {
return useSWR<Stats>(
siteId && start && end ? ['stats', siteId, start, end] : null,
() => fetchers.stats(siteId, start, end),
{
...dashboardSWRConfig,
// * Refresh every 60 seconds for stats
refreshInterval: 60 * 1000,
dedupingInterval: 10 * 1000,
}
)
}
// * Hook for daily stats (refreshed less frequently)
export function useDailyStats(
siteId: string,
start: string,
end: string,
interval: 'hour' | 'day' | 'minute'
) {
return useSWR<DailyStat[]>(
siteId && start && end ? ['dailyStats', siteId, start, end, interval] : null,
() => fetchers.dailyStats(siteId, start, end, interval),
{
...dashboardSWRConfig,
// * Refresh every 60 seconds for chart data
refreshInterval: 60 * 1000,
dedupingInterval: 10 * 1000,
}
)
}
// * Hook for realtime visitor count (refreshed frequently)
export function useRealtime(siteId: string, refreshInterval: number = 5000) {
return useSWR<{ visitors: number }>(
siteId ? ['realtime', siteId] : null,
() => fetchers.realtime(siteId),
{
...dashboardSWRConfig,
// * Refresh frequently for real-time data (default 5 seconds)
refreshInterval,
// * Short deduping for real-time
dedupingInterval: 2000,
// * Keep previous data while loading new data
keepPreviousData: true,
}
)
}
// * Re-export for convenience
export { fetchers }

View File

@@ -33,6 +33,7 @@
"react-simple-maps": "^3.0.0",
"recharts": "^2.15.0",
"sonner": "^2.0.7",
"swr": "^2.3.3",
"xlsx": "^0.18.5"
},
"overrides": {