From 848569eec83eeb72c652670a3bdfff714acc3cbd Mon Sep 17 00:00:00 2001 From: Ben Vinegar <2153+benvinegar@users.noreply.github.com> Date: Tue, 18 Jun 2024 23:59:21 -0400 Subject: [PATCH] Table cards now paginate independently (#80) --- app/analytics/query.ts | 103 ++++++++++--- app/components/PaginatedTableCard.tsx | 65 ++++++++ app/components/PaginationButtons.tsx | 47 ++++++ app/components/TableCard.tsx | 78 +++++----- app/lib/utils.ts | 9 ++ app/routes/dashboard.test.tsx | 205 ++++++++++++++----------- app/routes/dashboard.tsx | 98 +++--------- app/routes/resources.browser.tsx | 42 ++++++ app/routes/resources.country.tsx | 69 +++++++++ app/routes/resources.device.tsx | 42 ++++++ app/routes/resources.paths.tsx | 42 ++++++ app/routes/resources.referrer.tsx | 42 ++++++ package-lock.json | 210 +++++++++++++------------- server.ts | 9 ++ 14 files changed, 735 insertions(+), 326 deletions(-) create mode 100644 app/components/PaginatedTableCard.tsx create mode 100644 app/components/PaginationButtons.tsx create mode 100644 app/routes/resources.browser.tsx create mode 100644 app/routes/resources.country.tsx create mode 100644 app/routes/resources.device.tsx create mode 100644 app/routes/resources.paths.tsx create mode 100644 app/routes/resources.referrer.tsx diff --git a/app/analytics/query.ts b/app/analytics/query.ts index 6cb82ca..fce0e48 100644 --- a/app/analytics/query.ts +++ b/app/analytics/query.ts @@ -305,10 +305,9 @@ export class AnalyticsEngineAPI { column: T, interval: string, tz?: string, - limit?: number, + page: number = 1, + limit: number = 10, ) { - limit = limit || 10; - const intervalSql = intervalToSql(interval, tz); const _column = ColumnMappings[column]; @@ -320,7 +319,7 @@ export class AnalyticsEngineAPI { AND ${ColumnMappings.siteId} = '${siteId}' GROUP BY ${_column} ORDER BY count DESC - LIMIT ${limit}`; + LIMIT ${limit * page}`; type SelectionSet = { count: number; @@ -342,8 +341,16 @@ export class AnalyticsEngineAPI { const responseData = (await response.json()) as AnalyticsQueryResult; + + // since CF AE doesn't support OFFSET clauses, we select up to LIMIT and + // then slice that into the individual requested page + const pageData = responseData.data.slice( + limit * (page - 1), + limit * page, + ); + resolve( - responseData.data.map((row) => { + pageData.map((row) => { const key = row[_column] === "" ? "(none)" : row[_column]; return [key, row["count"]] as const; @@ -359,11 +366,9 @@ export class AnalyticsEngineAPI { column: T, interval: string, tz?: string, - limit?: number, + page: number = 1, + limit: number = 10, ) { - // defaults to 1 day if not specified - limit = limit || 10; - const intervalSql = intervalToSql(interval, tz); const _column = ColumnMappings[column]; @@ -377,7 +382,7 @@ export class AnalyticsEngineAPI { AND ${ColumnMappings.siteId} = '${siteId}' GROUP BY ${_column}, ${ColumnMappings.newVisitor}, ${ColumnMappings.newSession} ORDER BY count DESC - LIMIT ${limit}`; + LIMIT ${limit * page}`; type SelectionSet = { readonly count: number; @@ -401,7 +406,14 @@ export class AnalyticsEngineAPI { const responseData = (await response.json()) as AnalyticsQueryResult; - const result = responseData.data.reduce( + // since CF AE doesn't support OFFSET clauses, we select up to LIMIT and + // then slice that into the individual requested page + const pageData = responseData.data.slice( + limit * (page - 1), + limit * page, + ); + + const result = pageData.reduce( (acc, row) => { const key = row[_column] === "" @@ -426,12 +438,18 @@ export class AnalyticsEngineAPI { return returnPromise; } - async getCountByPath(siteId: string, interval: string, tz?: string) { + async getCountByPath( + siteId: string, + interval: string, + tz?: string, + page: number = 1, + ) { const allCountsResultPromise = this.getAllCountsByColumn( siteId, "path", interval, tz, + page, ); return allCountsResultPromise.then((allCountsResult) => { @@ -445,32 +463,77 @@ export class AnalyticsEngineAPI { }); } - async getCountByUserAgent(siteId: string, interval: string, tz?: string) { - return this.getVisitorCountByColumn(siteId, "userAgent", interval, tz); + async getCountByUserAgent( + siteId: string, + interval: string, + tz?: string, + page: number = 1, + ) { + return this.getVisitorCountByColumn( + siteId, + "userAgent", + interval, + tz, + page, + ); } - async getCountByCountry(siteId: string, interval: string, tz?: string) { - return this.getVisitorCountByColumn(siteId, "country", interval, tz); + async getCountByCountry( + siteId: string, + interval: string, + tz?: string, + page: number = 1, + ) { + return this.getVisitorCountByColumn( + siteId, + "country", + interval, + tz, + page, + ); } - async getCountByReferrer(siteId: string, interval: string, tz?: string) { - return this.getVisitorCountByColumn(siteId, "referrer", interval, tz); + async getCountByReferrer( + siteId: string, + interval: string, + tz?: string, + page: number = 1, + ) { + return this.getVisitorCountByColumn( + siteId, + "referrer", + interval, + tz, + page, + ); } - async getCountByBrowser(siteId: string, interval: string, tz?: string) { + async getCountByBrowser( + siteId: string, + interval: string, + tz?: string, + page: number = 1, + ) { return this.getVisitorCountByColumn( siteId, "browserName", interval, tz, + page, ); } - async getCountByDevice(siteId: string, interval: string, tz?: string) { + async getCountByDevice( + siteId: string, + interval: string, + tz?: string, + page: number = 1, + ) { return this.getVisitorCountByColumn( siteId, "deviceModel", interval, tz, + page, ); } diff --git a/app/components/PaginatedTableCard.tsx b/app/components/PaginatedTableCard.tsx new file mode 100644 index 0000000..9c028b8 --- /dev/null +++ b/app/components/PaginatedTableCard.tsx @@ -0,0 +1,65 @@ +import { useEffect } from "react"; +import TableCard from "~/components/TableCard"; + +import { Card } from "./ui/card"; +import PaginationButtons from "./PaginationButtons"; + +const ReferrerCard = ({ + siteId, + interval, + dataFetcher, + columnHeaders, + loaderUrl, +}: { + siteId: string; + interval: string; + dataFetcher: any; // ignore type for now + columnHeaders: string[]; + loaderUrl: string; +}) => { + const countsByProperty = dataFetcher.data?.countsByProperty || []; + const page = dataFetcher.data?.page || 1; + + useEffect(() => { + if (dataFetcher.state === "idle") { + dataFetcher.load( + `${loaderUrl}?site=${siteId}&interval=${interval}`, + ); + } + }, []); + + useEffect(() => { + if (dataFetcher.state === "idle") { + dataFetcher.load( + `${loaderUrl}?site=${siteId}&interval=${interval}`, + ); + } + }, [siteId, interval]); + + function handlePagination(page: number) { + dataFetcher.load( + `${loaderUrl}?site=${siteId}&interval=${interval}&page=${page}`, + ); + } + + const hasMore = countsByProperty.length === 10; + return ( + + {countsByProperty ? ( +
+ + +
+ ) : null} +
+ ); +}; + +export default ReferrerCard; diff --git a/app/components/PaginationButtons.tsx b/app/components/PaginationButtons.tsx new file mode 100644 index 0000000..7639b7b --- /dev/null +++ b/app/components/PaginationButtons.tsx @@ -0,0 +1,47 @@ +import React from "react"; + +import { ArrowLeft, ArrowRight } from "lucide-react"; + +interface PaginationButtonsProps { + page: number; + hasMore: boolean; + handlePagination: (page: number) => void; +} + +const PaginationButtons: React.FC = ({ + page, + hasMore, + handlePagination, +}) => { + return ( +
+
+ + +
+ ); +}; + +export default PaginationButtons; diff --git a/app/components/TableCard.tsx b/app/components/TableCard.tsx index d4f5760..6f09624 100644 --- a/app/components/TableCard.tsx +++ b/app/components/TableCard.tsx @@ -9,8 +9,6 @@ import { TableRow, } from "~/components/ui/table"; -import { Card } from "~/components/ui/card"; - type CountByProperty = [string, string][]; function calculateCountPercentages(countByProperty: CountByProperty) { @@ -40,49 +38,47 @@ export default function TableCard({ ? "grid-cols-[minmax(0,1fr),minmax(0,8ch),minmax(0,8ch)]" : "grid-cols-[minmax(0,1fr),minmax(0,8ch)]"; return ( - - - - - {(columnHeaders || []).map((header: string, index) => ( - - {header} - - ))} - - - - {(countByProperty || []).map((item, key) => ( - + + + {(columnHeaders || []).map((header: string, index) => ( + - - {item[0]} - + {header} + + ))} + + + + {(countByProperty || []).map((item, key) => ( + + + {item[0]} + + + {countFormatter.format(item[1] as number)} + + + {item.length > 2 && ( - {countFormatter.format(item[1] as number)} + {countFormatter.format(item[2] as number)} - - {item.length > 2 && ( - - {countFormatter.format(item[2] as number)} - - )} - - ))} - -
-
+ )} + + ))} + + ); } diff --git a/app/lib/utils.ts b/app/lib/utils.ts index e644794..6d6d1fe 100644 --- a/app/lib/utils.ts +++ b/app/lib/utils.ts @@ -4,3 +4,12 @@ import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } + +export function paramsFromUrl(url: string) { + const searchParams = new URL(url).searchParams; + const params: Record = {}; + searchParams.forEach((value, key) => { + params[key] = value; + }); + return params; +} diff --git a/app/routes/dashboard.test.tsx b/app/routes/dashboard.test.tsx index c5897cb..2a30191 100644 --- a/app/routes/dashboard.test.tsx +++ b/app/routes/dashboard.test.tsx @@ -16,6 +16,7 @@ import { createRemixStub } from "@remix-run/testing"; import { render, screen, waitFor } from "@testing-library/react"; import Dashboard, { loader } from "./dashboard"; +import { AnalyticsEngineAPI } from "~/analytics/query"; function createFetchResponse(data: T) { return { @@ -46,6 +47,10 @@ describe("Dashboard route", () => { await expect( loader({ context: { + analyticsEngine: new AnalyticsEngineAPI( + "testAccountId", + "testApiToken", + ), env: { VERSION: "", CF_BEARER_TOKEN: "", @@ -70,6 +75,10 @@ describe("Dashboard route", () => { const response = await loader({ context: { + analyticsEngine: new AnalyticsEngineAPI( + "testAccountId", + "testApiToken", + ), env: { VERSION: "", CF_BEARER_TOKEN: "fake", @@ -99,6 +108,10 @@ describe("Dashboard route", () => { const response = await loader({ context: { + analyticsEngine: new AnalyticsEngineAPI( + "testAccountId", + "testApiToken", + ), env: { VERSION: "", CF_BEARER_TOKEN: "fake", @@ -137,44 +150,6 @@ describe("Dashboard route", () => { }), ); - // response for getCountByPath - fetch.mockResolvedValueOnce( - createFetchResponse({ - data: [ - { blob3: "/", count: 1, isVisitor: 1, isVisit: 1 }, - { blob3: "/", count: 3, isVisitor: 0, isVisit: 0 }, - ], - }), - ); - - // response for getCountByCountry - fetch.mockResolvedValueOnce( - createFetchResponse({ - data: [{ blob4: "US", count: 1 }], - }), - ); - - // response for getCountByReferrer - fetch.mockResolvedValueOnce( - createFetchResponse({ - data: [{ blob5: "google.com", count: 1 }], - }), - ); - - // response for getCountByBrowser - fetch.mockResolvedValueOnce( - createFetchResponse({ - data: [{ blob6: "Chrome", count: 2 }], - }), - ); - - // response for getCountByDevice - fetch.mockResolvedValueOnce( - createFetchResponse({ - data: [{ blob7: "Desktop", count: 3 }], - }), - ); - // response for getViewsGroupedByInterval fetch.mockResolvedValueOnce( createFetchResponse({ @@ -186,6 +161,10 @@ describe("Dashboard route", () => { const response = await loader({ context: { + analyticsEngine: new AnalyticsEngineAPI( + "testAccountId", + "testApiToken", + ), env: { VERSION: "", CF_BEARER_TOKEN: "fake", @@ -206,11 +185,6 @@ describe("Dashboard route", () => { views: 6, visits: 3, visitors: 1, - countByPath: [["/", 1, 4]], - countByCountry: [["United States", 1]], - countByReferrer: [["google.com", 1]], - countByBrowser: [["Chrome", 2]], - countByDevice: [["Desktop", 3]], viewsGroupedByInterval: [ ["2024-01-11 05:00:00", 4], ["2024-01-12 05:00:00", 0], @@ -231,15 +205,14 @@ describe("Dashboard route", () => { fetch.mockResolvedValueOnce(createFetchResponse({ data: [] })); // getSitesOrderedByHits fetch.mockResolvedValueOnce(createFetchResponse({ data: [] })); // get counts - fetch.mockResolvedValueOnce(createFetchResponse({ data: [] })); // getCountByPath - fetch.mockResolvedValueOnce(createFetchResponse({ data: [] })); // getCountByCountry - fetch.mockResolvedValueOnce(createFetchResponse({ data: [] })); // getCountByReferrer - fetch.mockResolvedValueOnce(createFetchResponse({ data: [] })); // getCountByBrowser - fetch.mockResolvedValueOnce(createFetchResponse({ data: [] })); // getCountByDevice fetch.mockResolvedValueOnce(createFetchResponse({ data: [] })); // getViewsGroupedByInterval const response = await loader({ context: { + analyticsEngine: new AnalyticsEngineAPI( + "testAccountId", + "testApiToken", + ), env: { VERSION: "", CF_BEARER_TOKEN: "fake", @@ -260,11 +233,6 @@ describe("Dashboard route", () => { views: 0, visits: 0, visitors: 0, - countByPath: [], - countByCountry: [], - countByReferrer: [], - countByBrowser: [], - countByDevice: [], viewsGroupedByInterval: [ ["2024-01-11 05:00:00", 0], ["2024-01-12 05:00:00", 0], @@ -289,11 +257,6 @@ describe("Dashboard route", () => { views: [], visits: [], visitors: [], - countByPath: [], - countByBrowser: [], - countByCountry: [], - countByReferrer: [], - countByDevice: [], viewsGroupedByInterval: [], intervalType: "day", }); @@ -304,14 +267,49 @@ describe("Dashboard route", () => { path: "/", Component: Dashboard, loader, + children: [ + { + path: "/resources/paths", + loader: () => { + return json({ countsByProperty: [] }); + }, + }, + { + path: "/resources/referrer", + loader: () => { + return json({ countsByProperty: [] }); + }, + }, + { + path: "/resources/browser", + loader: () => { + return json({ countsByProperty: [] }); + }, + }, + { + path: "/resources/country", + loader: () => { + return json({ countsByProperty: [] }); + }, + }, + { + path: "/resources/device", + loader: () => { + return json({ countsByProperty: [] }); + }, + }, + ], }, ]); render(); - // wait until the rows render in the document - await waitFor(() => screen.findByText("Country")); + await waitFor(() => screen.findByText("Path")); + expect(screen.getByText("Path")).toBeInTheDocument(); + expect(screen.getByText("Referrer")).toBeInTheDocument(); + expect(screen.getByText("Browser")).toBeInTheDocument(); expect(screen.getByText("Country")).toBeInTheDocument(); + expect(screen.getByText("Device")).toBeInTheDocument(); }); const defaultMockedLoaderJson = { @@ -320,31 +318,6 @@ describe("Dashboard route", () => { views: 2133, visits: 80, visitors: 33, - countByPath: [ - ["/", 100], - ["/about", 80], - ["/contact", 60], - ], - countByBrowser: [ - ["Chrome", 100], - ["Safari", 80], - ["Firefox", 60], - ], - countByCountry: [ - ["United States", 100], - ["Canada", 80], - ["United Kingdom", 60], - ], - countByReferrer: [ - ["google.com", 100], - ["facebook.com", 80], - ["twitter.com", 60], - ], - countByDevice: [ - ["Desktop", 100], - ["Mobile", 80], - ["Tablet", 60], - ], viewsGroupedByInterval: [ ["2024-01-11 05:00:00", 0], ["2024-01-12 05:00:00", 0], @@ -368,6 +341,68 @@ describe("Dashboard route", () => { path: "/", Component: Dashboard, loader, + children: [ + { + path: "/resources/paths", + loader: () => { + return json({ + countsByProperty: [ + ["/", 100], + ["/about", 80], + ["/contact", 60], + ], + }); + }, + }, + { + path: "/resources/referrer", + loader: () => { + return json({ + countsByProperty: [ + ["google.com", 100], + ["facebook.com", 80], + ["twitter.com", 60], + ], + }); + }, + }, + { + path: "/resources/browser", + loader: () => { + return json({ + countsByProperty: [ + ["Chrome", 100], + ["Safari", 80], + ["Firefox", 60], + ], + }); + }, + }, + { + path: "/resources/country", + loader: () => { + return json({ + countsByProperty: [ + ["United States", 100], + ["Canada", 80], + ["United Kingdom", 60], + ], + }); + }, + }, + { + path: "/resources/device", + loader: () => { + return json({ + countsByProperty: [ + ["Desktop", 100], + ["Mobile", 80], + ["Tablet", 60], + ], + }); + }, + }, + ], }, ]); diff --git a/app/routes/dashboard.tsx b/app/routes/dashboard.tsx index 9845008..ae5888f 100644 --- a/app/routes/dashboard.tsx +++ b/app/routes/dashboard.tsx @@ -17,7 +17,12 @@ import { import { AnalyticsEngineAPI } from "../analytics/query"; -import TableCard from "~/components/TableCard"; +import { ReferrerCard } from "./resources.referrer"; +import { PathsCard } from "./resources.paths"; +import { BrowserCard } from "./resources.browser"; +import { CountryCard } from "./resources.country"; +import { DeviceCard } from "./resources.device"; + import TimeSeriesChart from "~/components/TimeSeriesChart"; import dayjs from "dayjs"; @@ -32,6 +37,7 @@ const MAX_RETENTION_DAYS = 90; declare module "@remix-run/server-runtime" { export interface AppLoadContext { + analyticsEngine: AnalyticsEngineAPI; env: { VERSION: string; CF_BEARER_TOKEN: string; @@ -71,8 +77,8 @@ export const loader = async ({ context, request }: LoaderFunctionArgs) => { redirectUrl.searchParams.set("site", redirectSite); return redirect(redirectUrl.toString()); } - const siteId = url.searchParams.get("site") || ""; + const siteId = url.searchParams.get("site") || ""; const actualSiteId = siteId == "@unknown" ? "" : siteId; const tz = context.requestTimezone as string; @@ -88,32 +94,6 @@ export const loader = async ({ context, request }: LoaderFunctionArgs) => { ); const counts = analyticsEngine.getCounts(actualSiteId, interval, tz); - const countByPath = analyticsEngine.getCountByPath( - actualSiteId, - interval, - tz, - ); - const countByCountry = analyticsEngine.getCountByCountry( - actualSiteId, - interval, - tz, - ); - const countByReferrer = analyticsEngine.getCountByReferrer( - actualSiteId, - interval, - tz, - ); - const countByBrowser = analyticsEngine.getCountByBrowser( - actualSiteId, - interval, - tz, - ); - const countByDevice = analyticsEngine.getCountByDevice( - actualSiteId, - interval, - tz, - ); - let intervalType: "DAY" | "HOUR" = "DAY"; switch (interval) { case "today": @@ -165,48 +145,20 @@ export const loader = async ({ context, request }: LoaderFunctionArgs) => { views: (await counts).views, visits: (await counts).visits, visitors: (await counts).visitors, - countByPath: await countByPath, - countByBrowser: await countByBrowser, - countByCountry: await countByCountry, - countByReferrer: await countByReferrer, - countByDevice: await countByDevice, + // countByReferrer: await countByReferrer, viewsGroupedByInterval: await viewsGroupedByInterval, intervalType, interval, + tz, }; } catch (err) { console.error(err); throw new Error("Failed to fetch data from Analytics Engine"); } - // normalize country codes to country names - // NOTE: this must be done ONLY on server otherwise hydration mismatches - // can occur because Intl.DisplayNames produces different results - // in different browsers (see ) - out.countByCountry = convertCountryCodesToNames(out.countByCountry); - return json(out); }; -function convertCountryCodesToNames( - countByCountry: [string, number][], -): [string, number][] { - const regionNames = new Intl.DisplayNames(["en"], { type: "region" }); - return countByCountry.map((countByBrowserRow) => { - let countryName; - try { - // throws an exception if country code isn't valid - // use try/catch to be defensive and not explode if an invalid - // country code gets insrted into Analytics Engine - countryName = regionNames.of(countByBrowserRow[0])!; // "United States" - } catch (err) { - countryName = "(unknown)"; - } - const count = countByBrowserRow[1]; - return [countryName, count]; - }); -} - export default function Dashboard() { const [, setSearchParams] = useSearchParams(); @@ -328,28 +280,24 @@ export default function Dashboard() {
- - +
- - - + +
diff --git a/app/routes/resources.browser.tsx b/app/routes/resources.browser.tsx new file mode 100644 index 0000000..de01c35 --- /dev/null +++ b/app/routes/resources.browser.tsx @@ -0,0 +1,42 @@ +import { useFetcher } from "@remix-run/react"; + +import type { LoaderFunctionArgs } from "@remix-run/cloudflare"; +import { json } from "@remix-run/cloudflare"; + +import { paramsFromUrl } from "~/lib/utils"; +import PaginatedTableCard from "~/components/PaginatedTableCard"; + +export async function loader({ context, request }: LoaderFunctionArgs) { + const { analyticsEngine } = context; + + const { interval, site, page = 1 } = paramsFromUrl(request.url); + const tz = context.requestTimezone as string; + + return json({ + countsByProperty: await analyticsEngine.getCountByBrowser( + site, + interval, + tz, + Number(page), + ), + page: Number(page), + }); +} + +export const BrowserCard = ({ + siteId, + interval, +}: { + siteId: string; + interval: string; +}) => { + return ( + ()} + loaderUrl="/resources/browser" + /> + ); +}; diff --git a/app/routes/resources.country.tsx b/app/routes/resources.country.tsx new file mode 100644 index 0000000..beebda0 --- /dev/null +++ b/app/routes/resources.country.tsx @@ -0,0 +1,69 @@ +import { useFetcher } from "@remix-run/react"; + +import type { LoaderFunctionArgs } from "@remix-run/cloudflare"; +import { json } from "@remix-run/cloudflare"; + +import { paramsFromUrl } from "~/lib/utils"; +import PaginatedTableCard from "~/components/PaginatedTableCard"; + +function convertCountryCodesToNames( + countByCountry: [string, number][], +): [string, number][] { + const regionNames = new Intl.DisplayNames(["en"], { type: "region" }); + return countByCountry.map((countByBrowserRow) => { + let countryName; + try { + // throws an exception if country code isn't valid + // use try/catch to be defensive and not explode if an invalid + // country code gets insrted into Analytics Engine + countryName = regionNames.of(countByBrowserRow[0])!; // "United States" + } catch (err) { + countryName = "(unknown)"; + } + const count = countByBrowserRow[1]; + return [countryName, count]; + }); +} + +export async function loader({ context, request }: LoaderFunctionArgs) { + const { analyticsEngine } = context; + + const { interval, site, page = 1 } = paramsFromUrl(request.url); + const tz = context.requestTimezone as string; + + const countByCountry = await analyticsEngine.getCountByCountry( + site, + interval, + tz, + Number(page), + ); + + // normalize country codes to country names + // NOTE: this must be done ONLY on server otherwise hydration mismatches + // can occur because Intl.DisplayNames produces different results + // in different browsers (see ) + const normalizedCountByCountry = convertCountryCodesToNames(countByCountry); + + return json({ + countsByProperty: normalizedCountByCountry, + page: Number(page), + }); +} + +export const CountryCard = ({ + siteId, + interval, +}: { + siteId: string; + interval: string; +}) => { + return ( + ()} + loaderUrl="/resources/country" + /> + ); +}; diff --git a/app/routes/resources.device.tsx b/app/routes/resources.device.tsx new file mode 100644 index 0000000..9cb2c41 --- /dev/null +++ b/app/routes/resources.device.tsx @@ -0,0 +1,42 @@ +import { useFetcher } from "@remix-run/react"; + +import type { LoaderFunctionArgs } from "@remix-run/cloudflare"; +import { json } from "@remix-run/cloudflare"; + +import { paramsFromUrl } from "~/lib/utils"; +import PaginatedTableCard from "~/components/PaginatedTableCard"; + +export async function loader({ context, request }: LoaderFunctionArgs) { + const { analyticsEngine } = context; + + const { interval, site, page = 1 } = paramsFromUrl(request.url); + const tz = context.requestTimezone as string; + + return json({ + countsByProperty: await analyticsEngine.getCountByDevice( + site, + interval, + tz, + Number(page), + ), + page: Number(page), + }); +} + +export const DeviceCard = ({ + siteId, + interval, +}: { + siteId: string; + interval: string; +}) => { + return ( + ()} + loaderUrl="/resources/device" + /> + ); +}; diff --git a/app/routes/resources.paths.tsx b/app/routes/resources.paths.tsx new file mode 100644 index 0000000..ee8a67b --- /dev/null +++ b/app/routes/resources.paths.tsx @@ -0,0 +1,42 @@ +import { useFetcher } from "@remix-run/react"; + +import type { LoaderFunctionArgs } from "@remix-run/cloudflare"; +import { json } from "@remix-run/cloudflare"; + +import { paramsFromUrl } from "~/lib/utils"; +import PaginatedTableCard from "~/components/PaginatedTableCard"; + +export async function loader({ context, request }: LoaderFunctionArgs) { + const { analyticsEngine } = context; + + const { interval, site, page = 1 } = paramsFromUrl(request.url); + const tz = context.requestTimezone as string; + + return json({ + countsByProperty: await analyticsEngine.getCountByPath( + site, + interval, + tz, + Number(page), + ), + page: Number(page), + }); +} + +export const PathsCard = ({ + siteId, + interval, +}: { + siteId: string; + interval: string; +}) => { + return ( + ()} + loaderUrl="/resources/paths" + /> + ); +}; diff --git a/app/routes/resources.referrer.tsx b/app/routes/resources.referrer.tsx new file mode 100644 index 0000000..5d4f091 --- /dev/null +++ b/app/routes/resources.referrer.tsx @@ -0,0 +1,42 @@ +import { useFetcher } from "@remix-run/react"; + +import type { LoaderFunctionArgs } from "@remix-run/cloudflare"; +import { json } from "@remix-run/cloudflare"; + +import { paramsFromUrl } from "~/lib/utils"; +import PaginatedTableCard from "~/components/PaginatedTableCard"; + +export async function loader({ context, request }: LoaderFunctionArgs) { + const { analyticsEngine } = context; + + const { interval, site, page = 1 } = paramsFromUrl(request.url); + const tz = context.requestTimezone as string; + + return json({ + countsByProperty: await analyticsEngine.getCountByReferrer( + site, + interval, + tz, + Number(page), + ), + page: Number(page), + }); +} + +export const ReferrerCard = ({ + siteId, + interval, +}: { + siteId: string; + interval: string; +}) => { + return ( + ()} + loaderUrl="/resources/referrer" + /> + ); +}; diff --git a/package-lock.json b/package-lock.json index 6cd5220..9b274a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -931,9 +931,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", + "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", "cpu": [ "ppc64" ], @@ -3696,9 +3696,9 @@ } }, "node_modules/@vanilla-extract/integration/node_modules/vite": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz", - "integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==", + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.2.tgz", + "integrity": "sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==", "dev": true, "dependencies": { "esbuild": "^0.18.10", @@ -13539,12 +13539,12 @@ } }, "node_modules/vite": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.1.tgz", - "integrity": "sha512-XBmSKRLXLxiaPYamLv3/hnP/KXDai1NDexN0FpkTaZXTfycHvkRHoenpgl/fvuK/kPbB6xAgoyiryAhQNxYmAQ==", + "version": "5.2.7", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.7.tgz", + "integrity": "sha512-k14PWOKLI6pMaSzAuGtT+Cf0YmIx12z9YGon39onaJNy8DLBfBJrzg9FQEmkAM5lpHBZs9wksWAsyF/HkpEwJA==", "dev": true, "dependencies": { - "esbuild": "^0.21.3", + "esbuild": "^0.20.1", "postcss": "^8.4.38", "rollup": "^4.13.0" }, @@ -14033,9 +14033,9 @@ } }, "node_modules/vite-node/node_modules/vite": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz", - "integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==", + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.2.tgz", + "integrity": "sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==", "dev": true, "dependencies": { "esbuild": "^0.18.10", @@ -14107,9 +14107,9 @@ } }, "node_modules/vite/node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", + "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", "cpu": [ "arm" ], @@ -14123,9 +14123,9 @@ } }, "node_modules/vite/node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", + "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", "cpu": [ "arm64" ], @@ -14139,9 +14139,9 @@ } }, "node_modules/vite/node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", + "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", "cpu": [ "x64" ], @@ -14155,9 +14155,9 @@ } }, "node_modules/vite/node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", + "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", "cpu": [ "arm64" ], @@ -14171,9 +14171,9 @@ } }, "node_modules/vite/node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", + "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", "cpu": [ "x64" ], @@ -14187,9 +14187,9 @@ } }, "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", + "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", "cpu": [ "arm64" ], @@ -14203,9 +14203,9 @@ } }, "node_modules/vite/node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", + "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", "cpu": [ "x64" ], @@ -14219,9 +14219,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", + "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", "cpu": [ "arm" ], @@ -14235,9 +14235,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", + "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", "cpu": [ "arm64" ], @@ -14251,9 +14251,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", + "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", "cpu": [ "ia32" ], @@ -14267,9 +14267,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", + "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", "cpu": [ "loong64" ], @@ -14283,9 +14283,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", + "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", "cpu": [ "mips64el" ], @@ -14299,9 +14299,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", + "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", "cpu": [ "ppc64" ], @@ -14315,9 +14315,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", + "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", "cpu": [ "riscv64" ], @@ -14331,9 +14331,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", + "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", "cpu": [ "s390x" ], @@ -14347,9 +14347,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", + "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", "cpu": [ "x64" ], @@ -14363,9 +14363,9 @@ } }, "node_modules/vite/node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", + "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", "cpu": [ "x64" ], @@ -14379,9 +14379,9 @@ } }, "node_modules/vite/node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", + "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", "cpu": [ "x64" ], @@ -14395,9 +14395,9 @@ } }, "node_modules/vite/node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", + "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", "cpu": [ "x64" ], @@ -14411,9 +14411,9 @@ } }, "node_modules/vite/node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", + "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", "cpu": [ "arm64" ], @@ -14427,9 +14427,9 @@ } }, "node_modules/vite/node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", + "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", "cpu": [ "ia32" ], @@ -14443,9 +14443,9 @@ } }, "node_modules/vite/node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", + "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", "cpu": [ "x64" ], @@ -14459,9 +14459,9 @@ } }, "node_modules/vite/node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", + "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", "dev": true, "hasInstallScript": true, "bin": { @@ -14471,29 +14471,29 @@ "node": ">=12" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" + "@esbuild/aix-ppc64": "0.20.2", + "@esbuild/android-arm": "0.20.2", + "@esbuild/android-arm64": "0.20.2", + "@esbuild/android-x64": "0.20.2", + "@esbuild/darwin-arm64": "0.20.2", + "@esbuild/darwin-x64": "0.20.2", + "@esbuild/freebsd-arm64": "0.20.2", + "@esbuild/freebsd-x64": "0.20.2", + "@esbuild/linux-arm": "0.20.2", + "@esbuild/linux-arm64": "0.20.2", + "@esbuild/linux-ia32": "0.20.2", + "@esbuild/linux-loong64": "0.20.2", + "@esbuild/linux-mips64el": "0.20.2", + "@esbuild/linux-ppc64": "0.20.2", + "@esbuild/linux-riscv64": "0.20.2", + "@esbuild/linux-s390x": "0.20.2", + "@esbuild/linux-x64": "0.20.2", + "@esbuild/netbsd-x64": "0.20.2", + "@esbuild/openbsd-x64": "0.20.2", + "@esbuild/sunos-x64": "0.20.2", + "@esbuild/win32-arm64": "0.20.2", + "@esbuild/win32-ia32": "0.20.2", + "@esbuild/win32-x64": "0.20.2" } }, "node_modules/vitest": { diff --git a/server.ts b/server.ts index 68e875c..5663bf7 100644 --- a/server.ts +++ b/server.ts @@ -4,6 +4,7 @@ import { getAssetFromKV } from "@cloudflare/kv-asset-handler"; import type { AppLoadContext } from "@remix-run/cloudflare"; import { createRequestHandler, logDevReady } from "@remix-run/cloudflare"; +import { AnalyticsEngineAPI } from "./app/analytics/query"; import { collectRequestHandler } from "./app/analytics/collect"; import * as build from "@remix-run/dev/server-build"; @@ -52,8 +53,16 @@ export default { // No-op } + if (!env.CF_BEARER_TOKEN || !env.CF_ACCOUNT_ID) { + throw new Error("Missing Cloudflare credentials"); + } try { + const analyticsEngine = new AnalyticsEngineAPI( + env.CF_ACCOUNT_ID, + env.CF_BEARER_TOKEN, + ); const loadContext: AppLoadContext = { + analyticsEngine, env, requestTimezone: (request as RequestInit).cf ?.timezone as string,