import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "~/components/ui/select"; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from "~/components/ui/card"; import { Button } from "~/components/ui/button"; import type { LoaderFunctionArgs, MetaFunction } from "react-router"; import { isRouteErrorResponse, redirect, useLoaderData, useNavigation, useRouteError, useSearchParams, } from "react-router"; import { ReferrerCard } from "./resources.referrer"; import { PathsCard } from "./resources.paths"; import { BrowserCard } from "./resources.browser"; import { BrowserVersionCard } from "./resources.browserversion"; import { CountryCard } from "./resources.country"; import { DeviceCard } from "./resources.device"; import { UtmSourceCard } from "./resources.utm-source"; import { UtmMediumCard } from "./resources.utm-medium"; import { UtmCampaignCard } from "./resources.utm-campaign"; import { UtmTermCard } from "./resources.utm-term"; import { UtmContentCard } from "./resources.utm-content"; import { getFiltersFromSearchParams, getIntervalType, getUserTimezone, } from "~/lib/utils"; import { SearchFilters } from "~/lib/types"; import SearchFilterBadges from "~/components/SearchFilterBadges"; import { TimeSeriesCard } from "./resources.timeseries"; import { StatsCard } from "./resources.stats"; import { requireAuth } from "~/lib/auth"; export const meta: MetaFunction = () => { return [ { title: "Counterscale: Web Analytics" }, { name: "description", content: "Counterscale: Web Analytics" }, ]; }; const MAX_RETENTION_DAYS = 90; export const loader = async ({ context, request }: LoaderFunctionArgs) => { await requireAuth(request, context.cloudflare.env); // NOTE: probably duped from getLoadContext / need to de-duplicate if (!context.cloudflare?.env?.CF_ACCOUNT_ID) { throw new Response("Missing credentials: CF_ACCOUNT_ID is not set.", { status: 501, }); } if (!context.cloudflare?.env?.CF_BEARER_TOKEN) { throw new Response("Missing credentials: CF_BEARER_TOKEN is not set.", { status: 501, }); } const { analyticsEngine } = context; const url = new URL(request.url); let interval; try { interval = url.searchParams.get("interval") || "7d"; } catch { interval = "7d"; } // if no siteId is set, redirect to the site with the most hits // during the default interval (e.g. 7d) if (url.searchParams.has("site") === false) { const sitesByHits = await analyticsEngine.getSitesOrderedByHits(interval); // if at least one result const redirectSite = sitesByHits[0]?.[0] || ""; const redirectUrl = new URL(request.url); redirectUrl.searchParams.set("site", redirectSite); throw redirect(redirectUrl.toString()); } const siteId = url.searchParams.get("site") || ""; const actualSiteId = siteId === "@unknown" ? "" : siteId; const filters = getFiltersFromSearchParams(url.searchParams); // initiate requests to AE in parallel // sites by hits: This is to populate the "sites" dropdown. We query the full retention // period (90 days) so that any site that has been active in the past 90 days // will show up in the dropdown. const sitesByHits = analyticsEngine.getSitesOrderedByHits( `${MAX_RETENTION_DAYS}d`, ); const intervalType = getIntervalType(interval); // await all requests to AE then return the results let out; try { out = { siteId: actualSiteId, sites: (await sitesByHits).map( ([site, _]: [string, number]) => site, ), intervalType, interval, filters, }; } catch (err) { console.error(err); throw new Error("Failed to fetch data from Analytics Engine"); } return out; }; export default function Dashboard() { const [, setSearchParams] = useSearchParams(); const data = useLoaderData(); const navigation = useNavigation(); const loading = navigation.state === "loading"; function changeSite(site: string) { // intentionally not updating prev params; don't want search // filters (e.g. referrer, path) to persist // TODO: might revisit if this is considered unexpected behavior setSearchParams({ site, interval: data.interval, }); } function changeInterval(interval: string) { setSearchParams((prev) => { prev.set("interval", interval); return prev; }); } const handleFilterChange = (filters: SearchFilters) => { setSearchParams((prev) => { for (const key in filters) { if (Object.hasOwnProperty.call(filters, key)) { prev.set( key, filters[key as keyof SearchFilters] as string, ); } } return prev; }); }; const handleFilterDelete = (key: string) => { setSearchParams((prev) => { prev.delete(key); return prev; }); }; const userTimezone = getUserTimezone(); return (
{data.filters && data.filters.browserName ? ( ) : ( )}
); } export function ErrorBoundary() { const error = useRouteError(); const [searchParams] = useSearchParams(); const siteId = searchParams.get("site"); const interval = searchParams.get("interval") || "7d"; let errorInfo = { title: "Dashboard Error", message: "An unexpected error occurred while loading the dashboard.", suggestion: "Please try refreshing the page or contact support if the issue persists.", actionable: true, showRetry: true, showContext: true, }; if (isRouteErrorResponse(error)) { switch (error.status) { case 501: if (error.data?.includes("CF_ACCOUNT_ID")) { errorInfo = { title: "Configuration Error", message: "Missing Cloudflare Account ID configuration.", suggestion: "Please ensure CF_ACCOUNT_ID is properly configured in your environment variables.", actionable: false, showRetry: false, showContext: false, }; } else if (error.data?.includes("CF_BEARER_TOKEN")) { errorInfo = { title: "Configuration Error", message: "Missing Cloudflare Bearer Token configuration.", suggestion: "Please ensure CF_BEARER_TOKEN is properly configured in your environment variables.", actionable: false, showRetry: false, showContext: false, }; } else { errorInfo = { title: `Configuration Error (${error.status})`, message: error.data || "Server configuration is incomplete.", suggestion: "Please check your Cloudflare Analytics Engine configuration.", actionable: false, showRetry: false, showContext: false, }; } break; case 500: errorInfo = { title: "Server Error", message: "The server encountered an internal error.", suggestion: "This is likely a temporary issue. Please try again in a few moments.", actionable: true, showRetry: true, showContext: true, }; break; default: errorInfo = { title: `Error ${error.status}`, message: error.data || error.statusText || "An HTTP error occurred.", suggestion: "Please try refreshing the page or contact support if the issue persists.", actionable: true, showRetry: true, showContext: true, }; } } else if (error instanceof Error) { if (error.message?.includes("Analytics Engine")) { errorInfo = { title: "Analytics Engine Error", message: "Failed to connect to Cloudflare Analytics Engine.", suggestion: "This could be due to network issues or Analytics Engine being temporarily unavailable. Please try again in a few moments.", actionable: true, showRetry: true, showContext: true, }; } else if (error.message?.includes("Authentication")) { errorInfo = { title: "Authentication Error", message: error.message, suggestion: "Please check your credentials and try logging in again.", actionable: true, showRetry: false, showContext: false, }; } else if (error.message?.includes("Invalid interval")) { errorInfo = { title: "Invalid Time Range", message: "The selected time interval is not supported.", suggestion: "Please select a different time range from the dropdown.", actionable: true, showRetry: false, showContext: true, }; } else { errorInfo = { title: "Application Error", message: error.message || "An unexpected application error occurred.", suggestion: "Please try refreshing the page or contact support if the issue persists.", actionable: true, showRetry: true, showContext: true, }; } } const handleRetry = () => { window.location.reload(); }; const handleGoHome = () => { window.location.href = "/dashboard"; }; console.error("Dashboard Error:", error); return (
⚠️ {errorInfo.title} {errorInfo.message}

Suggestion: {errorInfo.suggestion}

{errorInfo.showContext && (siteId || interval !== "7d") && (

Context when error occurred:

    {siteId && (
  • • Site:{" "} {siteId}
  • )}
  • • Time Range:{" "} {interval}
)} {errorInfo.actionable && ( {errorInfo.showRetry && ( )} )}
); }