Files
pulse/lib/swr/dashboard.ts
Usman Baig b305b5345b refactor: remove performance insights (Web Vitals) feature entirely
Remove Performance tab, PerformanceStats component, settings toggle,
Web Vitals observers from tracking script, and all related API types
and SWR hooks. Duration tracking is preserved.
2026-03-14 22:47:33 +01:00

513 lines
19 KiB
TypeScript

// * SWR configuration for dashboard data fetching
// * Implements stale-while-revalidate pattern for efficient data updates
import useSWR from 'swr'
import {
getDashboard,
getDashboardOverview,
getDashboardPages,
getDashboardLocations,
getDashboardDevices,
getDashboardReferrers,
getDashboardGoals,
getCampaigns,
getRealtime,
getStats,
getDailyStats,
getBehavior,
} from '@/lib/api/stats'
import {
getJourneyTransitions,
getJourneyTopPaths,
getJourneyEntryPoints,
type TransitionsResponse,
type TopPath as JourneyTopPath,
type EntryPoint,
} from '@/lib/api/journeys'
import { listAnnotations } from '@/lib/api/annotations'
import type { Annotation } from '@/lib/api/annotations'
import { getSite } from '@/lib/api/sites'
import type { Site } from '@/lib/api/sites'
import { listFunnels, type Funnel } from '@/lib/api/funnels'
import { getUptimeStatus, type UptimeStatusResponse } from '@/lib/api/uptime'
import { listGoals, type Goal } from '@/lib/api/goals'
import { listReportSchedules, type ReportSchedule } from '@/lib/api/report-schedules'
import { getGSCStatus, getGSCOverview, getGSCTopQueries, getGSCTopPages, getGSCDailyTotals, getGSCNewQueries } from '@/lib/api/gsc'
import type { GSCStatus, GSCOverview, GSCQueryResponse, GSCPageResponse, GSCDailyTotal, GSCNewQueries } from '@/lib/api/gsc'
import { getBunnyStatus, getBunnyOverview, getBunnyDailyStats, getBunnyTopCountries } from '@/lib/api/bunny'
import type { BunnyStatus, BunnyOverview, BunnyDailyRow, BunnyGeoRow } from '@/lib/api/bunny'
import { getSubscription, type SubscriptionDetails } from '@/lib/api/billing'
import type {
Stats,
DailyStat,
CampaignStat,
DashboardData,
DashboardOverviewData,
DashboardPagesData,
DashboardLocationsData,
DashboardDevicesData,
DashboardReferrersData,
DashboardGoalsData,
BehaviorData,
} from '@/lib/api/stats'
// * SWR fetcher functions
const fetchers = {
site: (siteId: string) => getSite(siteId),
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),
dashboardDevices: (siteId: string, start: string, end: string, filters?: string) => getDashboardDevices(siteId, start, end, undefined, filters),
dashboardReferrers: (siteId: string, start: string, end: string, filters?: string) => getDashboardReferrers(siteId, start, end, undefined, filters),
dashboardGoals: (siteId: string, start: string, end: string, filters?: string) => getDashboardGoals(siteId, start, end, undefined, filters),
stats: (siteId: string, start: string, end: string, filters?: string) => getStats(siteId, start, end, filters),
dailyStats: (siteId: string, start: string, end: string, interval: 'hour' | 'day' | 'minute') =>
getDailyStats(siteId, start, end, interval),
realtime: (siteId: string) => getRealtime(siteId),
campaigns: (siteId: string, start: string, end: string, limit: number) =>
getCampaigns(siteId, start, end, limit),
annotations: (siteId: string, start: string, end: string) => listAnnotations(siteId, start, end),
behavior: (siteId: string, start: string, end: string) => getBehavior(siteId, start, end),
journeyTransitions: (siteId: string, start: string, end: string, depth?: number, minSessions?: number, entryPath?: string) =>
getJourneyTransitions(siteId, start, end, { depth, minSessions, entryPath }),
journeyTopPaths: (siteId: string, start: string, end: string, limit?: number, minSessions?: number, entryPath?: string) =>
getJourneyTopPaths(siteId, start, end, { limit, minSessions, entryPath }),
journeyEntryPoints: (siteId: string, start: string, end: string) =>
getJourneyEntryPoints(siteId, start, end),
funnels: (siteId: string) => listFunnels(siteId),
uptimeStatus: (siteId: string) => getUptimeStatus(siteId),
goals: (siteId: string) => listGoals(siteId),
reportSchedules: (siteId: string) => listReportSchedules(siteId),
gscStatus: (siteId: string) => getGSCStatus(siteId),
gscOverview: (siteId: string, start: string, end: string) => getGSCOverview(siteId, start, end),
gscTopQueries: (siteId: string, start: string, end: string, limit: number, offset: number) => getGSCTopQueries(siteId, start, end, limit, offset),
gscTopPages: (siteId: string, start: string, end: string, limit: number, offset: number) => getGSCTopPages(siteId, start, end, limit, offset),
gscDailyTotals: (siteId: string, start: string, end: string) => getGSCDailyTotals(siteId, start, end),
gscNewQueries: (siteId: string, start: string, end: string) => getGSCNewQueries(siteId, start, end),
bunnyStatus: (siteId: string) => getBunnyStatus(siteId),
bunnyOverview: (siteId: string, start: string, end: string) => getBunnyOverview(siteId, start, end),
bunnyDailyStats: (siteId: string, start: string, end: string) => getBunnyDailyStats(siteId, start, end),
bunnyTopCountries: (siteId: string, start: string, end: string) => getBunnyTopCountries(siteId, start, end),
subscription: () => getSubscription(),
}
// * 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 (but not rate limits or auth errors)
shouldRetryOnError: true,
errorRetryCount: 3,
// * Error retry interval with exponential backoff
errorRetryInterval: 5000,
// * Don't retry on 429 (rate limit) or 401/403 (auth) — retrying makes it worse
onErrorRetry: (error: any, _key: string, _config: any, revalidate: any, { retryCount }: { retryCount: number }) => {
if (error?.status === 429 || error?.status === 401 || error?.status === 403) return
if (retryCount >= 3) return
setTimeout(() => revalidate({ retryCount }), 5000 * Math.pow(2, retryCount))
},
}
// * 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 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<DashboardData>(
siteId && start && end ? ['dashboard', siteId, start, end, interval, filters] : null,
() => fetchers.dashboard(siteId, start, end, interval, filters),
{
...dashboardSWRConfig,
// * Refresh every 60 seconds for dashboard data
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, filters?: string) {
return useSWR<Stats>(
siteId && start && end ? ['stats', siteId, start, end, filters] : null,
() => fetchers.stats(siteId, start, end, filters),
{
...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,
}
)
}
// * Hook for focused dashboard overview data (Fix 4.2: Efficient Data Transfer)
export function useDashboardOverview(siteId: string, start: string, end: string, interval?: string, filters?: string) {
return useSWR<DashboardOverviewData>(
siteId && start && end ? ['dashboardOverview', siteId, start, end, interval, filters] : null,
() => fetchers.dashboardOverview(siteId, start, end, interval, filters),
{
...dashboardSWRConfig,
refreshInterval: 60 * 1000,
dedupingInterval: 10 * 1000,
}
)
}
// * Hook for focused dashboard pages data
export function useDashboardPages(siteId: string, start: string, end: string, filters?: string) {
return useSWR<DashboardPagesData>(
siteId && start && end ? ['dashboardPages', siteId, start, end, filters] : null,
() => fetchers.dashboardPages(siteId, start, end, filters),
{
...dashboardSWRConfig,
refreshInterval: 60 * 1000,
dedupingInterval: 10 * 1000,
}
)
}
// * Hook for focused dashboard locations data
export function useDashboardLocations(siteId: string, start: string, end: string, filters?: string) {
return useSWR<DashboardLocationsData>(
siteId && start && end ? ['dashboardLocations', siteId, start, end, filters] : null,
() => fetchers.dashboardLocations(siteId, start, end, filters),
{
...dashboardSWRConfig,
refreshInterval: 60 * 1000,
dedupingInterval: 10 * 1000,
}
)
}
// * Hook for focused dashboard devices data
export function useDashboardDevices(siteId: string, start: string, end: string, filters?: string) {
return useSWR<DashboardDevicesData>(
siteId && start && end ? ['dashboardDevices', siteId, start, end, filters] : null,
() => fetchers.dashboardDevices(siteId, start, end, filters),
{
...dashboardSWRConfig,
refreshInterval: 60 * 1000,
dedupingInterval: 10 * 1000,
}
)
}
// * Hook for focused dashboard referrers data
export function useDashboardReferrers(siteId: string, start: string, end: string, filters?: string) {
return useSWR<DashboardReferrersData>(
siteId && start && end ? ['dashboardReferrers', siteId, start, end, filters] : null,
() => fetchers.dashboardReferrers(siteId, start, end, filters),
{
...dashboardSWRConfig,
refreshInterval: 60 * 1000,
dedupingInterval: 10 * 1000,
}
)
}
// * Hook for focused dashboard goals data
export function useDashboardGoals(siteId: string, start: string, end: string, filters?: string) {
return useSWR<DashboardGoalsData>(
siteId && start && end ? ['dashboardGoals', siteId, start, end, filters] : null,
() => fetchers.dashboardGoals(siteId, start, end, filters),
{
...dashboardSWRConfig,
refreshInterval: 60 * 1000,
dedupingInterval: 10 * 1000,
}
)
}
// * Hook for campaigns data (used by export modal)
export function useCampaigns(siteId: string, start: string, end: string, limit = 100) {
return useSWR<CampaignStat[]>(
siteId && start && end ? ['campaigns', siteId, start, end, limit] : null,
() => fetchers.campaigns(siteId, start, end, limit),
{
...dashboardSWRConfig,
refreshInterval: 60 * 1000,
dedupingInterval: 10 * 1000,
}
)
}
// * Hook for annotations data
export function useAnnotations(siteId: string, startDate: string, endDate: string) {
return useSWR<Annotation[]>(
siteId && startDate && endDate ? ['annotations', siteId, startDate, endDate] : null,
() => fetchers.annotations(siteId, startDate, endDate),
{
...dashboardSWRConfig,
refreshInterval: 60 * 1000,
dedupingInterval: 10 * 1000,
}
)
}
// * Hook for bundled behavior data (all frustration signals in one request)
export function useBehavior(siteId: string, start: string, end: string) {
return useSWR<BehaviorData>(
siteId && start && end ? ['behavior', siteId, start, end] : null,
() => fetchers.behavior(siteId, start, end),
{
...dashboardSWRConfig,
refreshInterval: 60 * 1000,
dedupingInterval: 10 * 1000,
}
)
}
// * Hook for journey flow transitions (Sankey diagram data)
export function useJourneyTransitions(siteId: string, start: string, end: string, depth?: number, minSessions?: number, entryPath?: string) {
return useSWR<TransitionsResponse>(
siteId && start && end ? ['journeyTransitions', siteId, start, end, depth, minSessions, entryPath] : null,
() => fetchers.journeyTransitions(siteId, start, end, depth, minSessions, entryPath),
{
...dashboardSWRConfig,
refreshInterval: 60 * 1000,
dedupingInterval: 10 * 1000,
}
)
}
// * Hook for top journey paths
export function useJourneyTopPaths(siteId: string, start: string, end: string, limit?: number, minSessions?: number, entryPath?: string) {
return useSWR<JourneyTopPath[]>(
siteId && start && end ? ['journeyTopPaths', siteId, start, end, limit, minSessions, entryPath] : null,
() => fetchers.journeyTopPaths(siteId, start, end, limit, minSessions, entryPath),
{
...dashboardSWRConfig,
refreshInterval: 60 * 1000,
dedupingInterval: 10 * 1000,
}
)
}
// * Hook for journey entry points (refreshes less frequently)
export function useJourneyEntryPoints(siteId: string, start: string, end: string) {
return useSWR<EntryPoint[]>(
siteId && start && end ? ['journeyEntryPoints', siteId, start, end] : null,
() => fetchers.journeyEntryPoints(siteId, start, end),
{
...dashboardSWRConfig,
refreshInterval: 5 * 60 * 1000,
dedupingInterval: 30 * 1000,
}
)
}
// * Hook for funnels list
export function useFunnels(siteId: string) {
return useSWR<Funnel[]>(
siteId ? ['funnels', siteId] : null,
() => fetchers.funnels(siteId),
{
...dashboardSWRConfig,
refreshInterval: 60 * 1000,
dedupingInterval: 10 * 1000,
}
)
}
// * Hook for uptime status (refreshes every 30s to match original polling)
export function useUptimeStatus(siteId: string) {
return useSWR<UptimeStatusResponse>(
siteId ? ['uptimeStatus', siteId] : null,
() => fetchers.uptimeStatus(siteId),
{
...dashboardSWRConfig,
refreshInterval: 30 * 1000,
dedupingInterval: 10 * 1000,
keepPreviousData: true,
}
)
}
// * Hook for goals list
export function useGoals(siteId: string) {
return useSWR<Goal[]>(
siteId ? ['goals', siteId] : null,
() => fetchers.goals(siteId),
{
...dashboardSWRConfig,
refreshInterval: 60 * 1000,
dedupingInterval: 10 * 1000,
}
)
}
// * Hook for report schedules
export function useReportSchedules(siteId: string) {
return useSWR<ReportSchedule[]>(
siteId ? ['reportSchedules', siteId] : null,
() => fetchers.reportSchedules(siteId),
{
...dashboardSWRConfig,
refreshInterval: 60 * 1000,
dedupingInterval: 10 * 1000,
}
)
}
// * Hook for GSC connection status
export function useGSCStatus(siteId: string) {
return useSWR<GSCStatus>(
siteId ? ['gscStatus', siteId] : null,
() => fetchers.gscStatus(siteId),
{
...dashboardSWRConfig,
refreshInterval: 60 * 1000,
dedupingInterval: 30 * 1000,
}
)
}
// * Hook for GSC overview metrics (clicks, impressions, CTR, position)
export function useGSCOverview(siteId: string, start: string, end: string) {
return useSWR<GSCOverview>(
siteId && start && end ? ['gscOverview', siteId, start, end] : null,
() => fetchers.gscOverview(siteId, start, end),
dashboardSWRConfig
)
}
// * Hook for GSC top queries
export function useGSCTopQueries(siteId: string, start: string, end: string, limit = 50, offset = 0) {
return useSWR<GSCQueryResponse>(
siteId && start && end ? ['gscTopQueries', siteId, start, end, limit, offset] : null,
() => fetchers.gscTopQueries(siteId, start, end, limit, offset),
dashboardSWRConfig
)
}
// * Hook for GSC top pages
export function useGSCTopPages(siteId: string, start: string, end: string, limit = 50, offset = 0) {
return useSWR<GSCPageResponse>(
siteId && start && end ? ['gscTopPages', siteId, start, end, limit, offset] : null,
() => fetchers.gscTopPages(siteId, start, end, limit, offset),
dashboardSWRConfig
)
}
// * Hook for GSC daily totals (clicks & impressions per day)
export function useGSCDailyTotals(siteId: string, start: string, end: string) {
return useSWR<{ daily_totals: GSCDailyTotal[] }>(
siteId && start && end ? ['gscDailyTotals', siteId, start, end] : null,
() => fetchers.gscDailyTotals(siteId, start, end),
dashboardSWRConfig
)
}
// * Hook for GSC new queries (queries that appeared in the current period)
export function useGSCNewQueries(siteId: string, start: string, end: string) {
return useSWR<GSCNewQueries>(
siteId && start && end ? ['gscNewQueries', siteId, start, end] : null,
() => fetchers.gscNewQueries(siteId, start, end),
dashboardSWRConfig
)
}
// * Hook for BunnyCDN connection status
export function useBunnyStatus(siteId: string) {
return useSWR<BunnyStatus>(
siteId ? ['bunnyStatus', siteId] : null,
() => fetchers.bunnyStatus(siteId),
{ ...dashboardSWRConfig, refreshInterval: 60 * 1000, dedupingInterval: 30 * 1000 }
)
}
// * Hook for BunnyCDN overview metrics (bandwidth, requests, cache hit rate)
export function useBunnyOverview(siteId: string, startDate: string, endDate: string) {
return useSWR<BunnyOverview>(
siteId && startDate && endDate ? ['bunnyOverview', siteId, startDate, endDate] : null,
() => fetchers.bunnyOverview(siteId, startDate, endDate),
dashboardSWRConfig
)
}
// * Hook for BunnyCDN daily stats (bandwidth & requests per day)
export function useBunnyDailyStats(siteId: string, startDate: string, endDate: string) {
return useSWR<{ daily_stats: BunnyDailyRow[] }>(
siteId && startDate && endDate ? ['bunnyDailyStats', siteId, startDate, endDate] : null,
() => fetchers.bunnyDailyStats(siteId, startDate, endDate),
dashboardSWRConfig
)
}
// * Hook for BunnyCDN top countries by bandwidth
export function useBunnyTopCountries(siteId: string, startDate: string, endDate: string) {
return useSWR<{ countries: BunnyGeoRow[] }>(
siteId && startDate && endDate ? ['bunnyTopCountries', siteId, startDate, endDate] : null,
() => fetchers.bunnyTopCountries(siteId, startDate, endDate),
dashboardSWRConfig
)
}
// * Hook for subscription details (changes rarely)
export function useSubscription() {
return useSWR<SubscriptionDetails>(
'subscription',
() => fetchers.subscription(),
{
...dashboardSWRConfig,
refreshInterval: 5 * 60 * 1000,
dedupingInterval: 30 * 1000,
}
)
}
// * Re-export for convenience
export { fetchers }