Files
pulse/lib/swr/dashboard.ts
Usman Baig af29bb77cd fix: stop retrying rate-limited and auth-failed requests
SWR was retrying 429/401/403 responses with exponential backoff,
which cascaded into a flood of failed requests when the tab regained
focus. Now skips retries entirely for these status codes.
2026-03-14 17:16:23 +01:00

467 lines
17 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,
getDashboardPerformance,
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 } from '@/lib/api/gsc'
import type { GSCStatus, GSCOverview, GSCQueryResponse, GSCPageResponse } from '@/lib/api/gsc'
import { getSubscription, type SubscriptionDetails } from '@/lib/api/billing'
import type {
Stats,
DailyStat,
CampaignStat,
DashboardData,
DashboardOverviewData,
DashboardPagesData,
DashboardLocationsData,
DashboardDevicesData,
DashboardReferrersData,
DashboardPerformanceData,
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),
dashboardPerformance: (siteId: string, start: string, end: string, filters?: string) => getDashboardPerformance(siteId, start, end, 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),
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 performance data
export function useDashboardPerformance(siteId: string, start: string, end: string, filters?: string) {
return useSWR<DashboardPerformanceData>(
siteId && start && end ? ['dashboardPerformance', siteId, start, end, filters] : null,
() => fetchers.dashboardPerformance(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 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 }