diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a8c3fa..217b39c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ## [Unreleased] +### Improved + +- **Even faster dashboard loading.** Your dashboard now fetches all its data โ€” pages, locations, devices, referrers, performance, and goals โ€” in a single request instead of seven separate ones. This means the entire dashboard appears at once rather than sections loading one by one, and puts much less strain on the server when many people are viewing their analytics at the same time. + ### Added - **Interactive 3D Globe.** The Locations panel now has a "Globe" tab showing your visitor locations on a beautiful, interactive 3D globe. Drag to rotate, and orange markers highlight where your visitors are โ€” sized by how much traffic each country sends. The globe slowly auto-rotates and adapts to light and dark mode. diff --git a/app/sites/[id]/page.tsx b/app/sites/[id]/page.tsx index 0488ecb..a220bc3 100644 --- a/app/sites/[id]/page.tsx +++ b/app/sites/[id]/page.tsx @@ -40,13 +40,7 @@ const EventProperties = dynamic(() => import('@/components/dashboard/EventProper const ExportModal = dynamic(() => import('@/components/dashboard/ExportModal')) import { type DimensionFilter, serializeFilters, parseFiltersFromURL } from '@/lib/filters' import { - useDashboardOverview, - useDashboardPages, - useDashboardLocations, - useDashboardDevices, - useDashboardReferrers, - useDashboardPerformance, - useDashboardGoals, + useDashboard, useRealtime, useStats, useDailyStats, @@ -220,16 +214,10 @@ export default function SiteDashboardPage() { return { start: prevStart.toISOString().split('T')[0], end: prevEnd.toISOString().split('T')[0] } }, [dateRange]) - // SWR hooks - replace manual useState + useEffect + setInterval polling - // Each hook handles its own refresh interval, deduplication, and error retry - // Filters are included in cache keys so changing filters auto-refetches - const { data: overview, isLoading: overviewLoading, error: overviewError } = useDashboardOverview(siteId, dateRange.start, dateRange.end, interval, filtersParam || undefined) - const { data: pages } = useDashboardPages(siteId, dateRange.start, dateRange.end, filtersParam || undefined) - const { data: locations } = useDashboardLocations(siteId, dateRange.start, dateRange.end, filtersParam || undefined) - const { data: devicesData } = useDashboardDevices(siteId, dateRange.start, dateRange.end, filtersParam || undefined) - const { data: referrers } = useDashboardReferrers(siteId, dateRange.start, dateRange.end, filtersParam || undefined) - const { data: performanceData } = useDashboardPerformance(siteId, dateRange.start, dateRange.end, filtersParam || undefined) - const { data: goalsData } = useDashboardGoals(siteId, dateRange.start, dateRange.end, filtersParam || undefined) + // Single dashboard request replaces 7 focused hooks (overview, pages, locations, + // devices, referrers, performance, goals). The backend runs all queries in parallel + // and caches the result in Redis, reducing requests from 12 to 6 per refresh cycle. + const { data: dashboard, isLoading: dashboardLoading, error: dashboardError } = useDashboard(siteId, dateRange.start, dateRange.end, interval, filtersParam || undefined) const { data: realtimeData } = useRealtime(siteId) const { data: prevStats } = useStats(siteId, prevRange.start, prevRange.end) const { data: prevDailyStats } = useDailyStats(siteId, prevRange.start, prevRange.end, interval) @@ -255,24 +243,24 @@ export default function SiteDashboardPage() { toast.success('Annotation deleted') } - // Derive typed values from SWR data - const site = overview?.site ?? null - const stats: Stats = overview?.stats ?? { pageviews: 0, visitors: 0, bounce_rate: 0, avg_duration: 0 } - const realtime = realtimeData?.visitors ?? overview?.realtime_visitors ?? 0 - const dailyStats: DailyStat[] = overview?.daily_stats ?? [] + // Derive typed values from single dashboard response + const site = dashboard?.site ?? null + const stats: Stats = dashboard?.stats ?? { pageviews: 0, visitors: 0, bounce_rate: 0, avg_duration: 0 } + const realtime = realtimeData?.visitors ?? dashboard?.realtime_visitors ?? 0 + const dailyStats: DailyStat[] = dashboard?.daily_stats ?? [] // Build filter suggestions from current dashboard data const filterSuggestions = useMemo(() => { const s: FilterSuggestions = {} // Pages - const topPages = pages?.top_pages ?? [] + const topPages = dashboard?.top_pages ?? [] if (topPages.length > 0) { s.page = topPages.map(p => ({ value: p.path, label: p.path, count: p.pageviews })) } // Referrers - const refs = referrers?.top_referrers ?? [] + const refs = dashboard?.top_referrers ?? [] if (refs.length > 0) { s.referrer = refs.filter(r => r.referrer && r.referrer !== '').map(r => ({ value: r.referrer, @@ -282,7 +270,7 @@ export default function SiteDashboardPage() { } // Countries - const ctrs = locations?.countries ?? [] + const ctrs = dashboard?.countries ?? [] if (ctrs.length > 0) { const regionNames = (() => { try { return new Intl.DisplayNames(['en'], { type: 'region' }) } catch { return null } })() s.country = ctrs.filter(c => c.country && c.country !== 'Unknown').map(c => ({ @@ -293,7 +281,7 @@ export default function SiteDashboardPage() { } // Regions - const regs = locations?.regions ?? [] + const regs = dashboard?.regions ?? [] if (regs.length > 0) { s.region = regs.filter(r => r.region && r.region !== 'Unknown').map(r => ({ value: r.region, @@ -303,7 +291,7 @@ export default function SiteDashboardPage() { } // Cities - const cts = locations?.cities ?? [] + const cts = dashboard?.cities ?? [] if (cts.length > 0) { s.city = cts.filter(c => c.city && c.city !== 'Unknown').map(c => ({ value: c.city, @@ -313,7 +301,7 @@ export default function SiteDashboardPage() { } // Browsers - const brs = devicesData?.browsers ?? [] + const brs = dashboard?.browsers ?? [] if (brs.length > 0) { s.browser = brs.filter(b => b.browser && b.browser !== 'Unknown').map(b => ({ value: b.browser, @@ -323,7 +311,7 @@ export default function SiteDashboardPage() { } // OS - const oses = devicesData?.os ?? [] + const oses = dashboard?.os ?? [] if (oses.length > 0) { s.os = oses.filter(o => o.os && o.os !== 'Unknown').map(o => ({ value: o.os, @@ -333,7 +321,7 @@ export default function SiteDashboardPage() { } // Devices - const devs = devicesData?.devices ?? [] + const devs = dashboard?.devices ?? [] if (devs.length > 0) { s.device = devs.filter(d => d.device && d.device !== 'Unknown').map(d => ({ value: d.device, @@ -359,19 +347,19 @@ export default function SiteDashboardPage() { } return s - }, [pages, referrers, locations, devicesData, campaigns]) + }, [dashboard, campaigns]) // Show error toast on fetch failure useEffect(() => { - if (overviewError) { + if (dashboardError) { toast.error('Failed to load dashboard analytics') } - }, [overviewError]) + }, [dashboardError]) // Track when data was last updated (for "Live ยท Xs ago" display) useEffect(() => { - if (overview) lastUpdatedAtRef.current = Date.now() - }, [overview]) + if (dashboard) lastUpdatedAtRef.current = Date.now() + }, [dashboard]) // Save settings to localStorage const saveSettings = (type: string, newDateRange?: { start: string; end: string }) => { @@ -413,7 +401,7 @@ export default function SiteDashboardPage() { // Skip the minimum-loading skeleton when SWR already has cached data // (prevents the 300ms flash when navigating back to the dashboard) - const showSkeleton = useMinimumLoading(overviewLoading && !overview) + const showSkeleton = useMinimumLoading(dashboardLoading && !dashboard) if (showSkeleton) { return @@ -543,8 +531,8 @@ export default function SiteDashboardPage() { {site.enable_performance_insights && (
!/^scroll_\d+$/.test(g.event_name))} + goalCounts={(dashboard?.goal_counts ?? []).filter(g => !/^scroll_\d+$/.test(g.event_name))} onSelectEvent={setSelectedEvent} />
- +
{/* Event Properties Breakdown */} @@ -636,8 +624,8 @@ export default function SiteDashboardPage() { onClose={() => setIsExportModalOpen(false)} data={dailyStats} stats={stats} - topPages={pages?.top_pages} - topReferrers={referrers?.top_referrers} + topPages={dashboard?.top_pages} + topReferrers={dashboard?.top_referrers} campaigns={campaigns} /> diff --git a/lib/api/stats.ts b/lib/api/stats.ts index 5e66122..883e87f 100644 --- a/lib/api/stats.ts +++ b/lib/api/stats.ts @@ -245,8 +245,8 @@ export interface DashboardData { goal_counts?: GoalCountStat[] } -export function getDashboard(siteId: string, startDate?: string, endDate?: string, limit = 10, interval?: string): Promise { - return apiRequest(`/sites/${siteId}/dashboard${buildQuery({ startDate, endDate, limit, interval })}`) +export function getDashboard(siteId: string, startDate?: string, endDate?: string, limit = 10, interval?: string, filters?: string): Promise { + return apiRequest(`/sites/${siteId}/dashboard${buildQuery({ startDate, endDate, limit, interval, filters })}`) } export function getPublicDashboard( diff --git a/lib/swr/dashboard.ts b/lib/swr/dashboard.ts index 1911bb8..e6e9813 100644 --- a/lib/swr/dashboard.ts +++ b/lib/swr/dashboard.ts @@ -24,6 +24,7 @@ import type { Stats, DailyStat, CampaignStat, + DashboardData, DashboardOverviewData, DashboardPagesData, DashboardLocationsData, @@ -36,7 +37,7 @@ import type { // * SWR fetcher functions const fetchers = { site: (siteId: string) => getSite(siteId), - dashboard: (siteId: string, start: string, end: string) => getDashboard(siteId, start, end), + dashboard: (siteId: string, start: string, end: string, interval?: string, filters?: string) => getDashboard(siteId, start, end, 10, interval, filters), dashboardOverview: (siteId: string, start: string, end: string, interval?: string, filters?: string) => getDashboardOverview(siteId, start, end, interval, filters), dashboardPages: (siteId: string, start: string, end: string, filters?: string) => getDashboardPages(siteId, start, end, undefined, filters), dashboardLocations: (siteId: string, start: string, end: string, filters?: string) => getDashboardLocations(siteId, start, end, undefined, undefined, filters), @@ -81,14 +82,15 @@ export function useSite(siteId: string) { ) } -// * 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), +// * Hook for full dashboard data (single request replaces 7 focused hooks) +// * The backend runs all queries in parallel and caches the result in Redis (30s TTL) +export function useDashboard(siteId: string, start: string, end: string, interval?: string, filters?: string) { + return useSWR( + siteId && start && end ? ['dashboard', siteId, start, end, interval, filters] : null, + () => fetchers.dashboard(siteId, start, end, interval, filters), { ...dashboardSWRConfig, - // * Refresh every 60 seconds for dashboard summary + // * Refresh every 60 seconds for dashboard data refreshInterval: 60 * 1000, // * Deduping interval to prevent duplicate requests dedupingInterval: 10 * 1000,