Skip to content

Commit

Permalink
feat: track city and region of site visitors
Browse files Browse the repository at this point in the history
resolves benvinegar#85
  • Loading branch information
nickarnold authored and Nick Arnold committed Aug 8, 2024
1 parent 8f67e80 commit 557d87f
Show file tree
Hide file tree
Showing 8 changed files with 187 additions and 5 deletions.
4 changes: 4 additions & 0 deletions app/analytics/__tests__/collect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ function generateRequestParams(headers: Record<string, string>) {
// Cloudflare-specific request properties
cf: {
country: "US",
region: "Colorado",
city: "Denver",
},
};
}
Expand Down Expand Up @@ -65,6 +67,8 @@ describe("collectRequestHandler", () => {
"Chrome", // browser name
"",
"example", // site id
"Colorado", // region
"Denver", // city
],
doubles: [
1, // new visitor
Expand Down
15 changes: 14 additions & 1 deletion app/analytics/collect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,19 @@ export function collectRequestHandler(request: Request, env: Env) {

// NOTE: location is derived from Cloudflare-specific request properties
// see: https://developers.cloudflare.com/workers/runtime-apis/request/#incomingrequestcfproperties
const country = (request as RequestInit).cf?.country;
const cfIncomingRequestProperties = (request as RequestInit).cf;
const city = cfIncomingRequestProperties?.city;
if (typeof city === "string") {
data.city = city;
}
const country = cfIncomingRequestProperties?.country;
if (typeof country === "string") {
data.country = country;
}
const region = cfIncomingRequestProperties?.region;
if (typeof region === "string") {
data.region = region;
}

writeDataPoint(env.WEB_COUNTER_AE, data);

Expand Down Expand Up @@ -116,7 +125,9 @@ interface DataPoint {
host?: string | undefined;
userAgent?: string;
path?: string;
city?: string;
country?: string;
region?: string;
referrer?: string;
browserName?: string;
deviceModel?: string;
Expand Down Expand Up @@ -144,6 +155,8 @@ export function writeDataPoint(
data.browserName || "", // blob6
data.deviceModel || "", // blob7
data.siteId || "", // blob8
data.region || "", // blob9
data.city || "", // blob10
],
doubles: [data.newVisitor || 0, data.newSession || 0],
};
Expand Down
34 changes: 34 additions & 0 deletions app/analytics/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,23 @@ export class AnalyticsEngineAPI {
});
}

async getCountByCity(
siteId: string,
interval: string,
tz?: string,
filters: SearchFilters = {},
page: number = 1,
) {
return this.getVisitorCountByColumn(
siteId,
"city",
interval,
tz,
filters,
page,
);
}

async getCountByCountry(
siteId: string,
interval: string,
Expand All @@ -518,6 +535,23 @@ export class AnalyticsEngineAPI {
);
}

async getCountByRegion(
siteId: string,
interval: string,
tz?: string,
filters: SearchFilters = {},
page: number = 1,
) {
return this.getVisitorCountByColumn(
siteId,
"region",
interval,
tz,
filters,
page,
);
}

async getCountByReferrer(
siteId: string,
interval: string,
Expand Down
2 changes: 2 additions & 0 deletions app/analytics/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export const ColumnMappings = {
browserName: "blob6",
deviceModel: "blob7",
siteId: "blob8",
region: "blob9",
city: "blob10",

/**
* doubles
Expand Down
40 changes: 40 additions & 0 deletions app/routes/__tests__/dashboard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -301,12 +301,24 @@ describe("Dashboard route", () => {
return json({ countsByProperty: [] });
},
},
{
path: "/resources/city",
loader: () => {
return json({ countsByProperty: [] });
},
},
{
path: "/resources/country",
loader: () => {
return json({ countsByProperty: [] });
},
},
{
path: "/resources/region",
loader: () => {
return json({ countsByProperty: [] });
},
},
{
path: "/resources/device",
loader: () => {
Expand All @@ -323,7 +335,9 @@ describe("Dashboard route", () => {
expect(screen.getByText("Path")).toBeInTheDocument();
expect(screen.getByText("Referrer")).toBeInTheDocument();
expect(screen.getByText("Browser")).toBeInTheDocument();
expect(screen.getByText("City")).toBeInTheDocument();
expect(screen.getByText("Country")).toBeInTheDocument();
expect(screen.getByText("Region")).toBeInTheDocument();
expect(screen.getByText("Device")).toBeInTheDocument();
});

Expand Down Expand Up @@ -396,6 +410,18 @@ describe("Dashboard route", () => {
});
},
},
{
path: "/resources/city",
loader: () => {
return json({
countsByProperty: [
["Chicago", 100],
["Denver", 80],
["San Diego", 60],
],
});
},
},
{
path: "/resources/country",
loader: () => {
Expand All @@ -408,6 +434,18 @@ describe("Dashboard route", () => {
});
},
},
{
path: "/resources/region",
loader: () => {
return json({
countsByProperty: [
["California", 100],
["Colorado", 80],
["Illinois", 60],
],
});
},
},
{
path: "/resources/device",
loader: () => {
Expand Down Expand Up @@ -436,7 +474,9 @@ describe("Dashboard route", () => {
expect(screen.getByText("/about")).toBeInTheDocument();
expect(screen.getByText("Chrome")).toBeInTheDocument();
expect(screen.getByText("google.com")).toBeInTheDocument();
expect(screen.getByText("Denver")).toBeInTheDocument();
expect(screen.getByText("Canada")).toBeInTheDocument(); // assert converted CA -> Canada
expect(screen.getByText("California")).toBeInTheDocument();
expect(screen.getByText("Mobile")).toBeInTheDocument();
});
});
13 changes: 9 additions & 4 deletions app/routes/dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import { ReferrerCard } from "./resources.referrer";
import { PathsCard } from "./resources.paths";
import { BrowserCard } from "./resources.browser";
import { CountryCard } from "./resources.country";
import { RegionCard } from "./resources.region";
import { CityCard } from "./resources.city";
import { DeviceCard } from "./resources.device";

import TimeSeriesChart from "~/components/TimeSeriesChart";
Expand Down Expand Up @@ -319,27 +321,30 @@ export default function Dashboard() {
onFilterChange={handleFilterChange}
/>
</div>
<div className="grid md:grid-cols-3 gap-4 mb-4">
<div className="grid md:grid-cols-2 gap-4 mb-4">
<BrowserCard
siteId={data.siteId}
interval={data.interval}
filters={data.filters}
onFilterChange={handleFilterChange}
/>

<CountryCard
<DeviceCard
siteId={data.siteId}
interval={data.interval}
filters={data.filters}
onFilterChange={handleFilterChange}
/>

<DeviceCard
</div>
<div className="grid md:grid-cols-3 gap-4 mb-4">
<CountryCard
siteId={data.siteId}
interval={data.interval}
filters={data.filters}
onFilterChange={handleFilterChange}
/>
<RegionCard siteId={data.siteId} interval={data.interval} />
<CityCard siteId={data.siteId} interval={data.interval} />
</div>
</div>
</div>
Expand Down
42 changes: 42 additions & 0 deletions app/routes/resources.city.tsx
Original file line number Diff line number Diff line change
@@ -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.getCountByCity(
site,
interval,
tz,
Number(page),
),
page: Number(page),
});
}

export const CityCard = ({
siteId,
interval,
}: {
siteId: string;
interval: string;
}) => {
return (
<PaginatedTableCard
siteId={siteId}
interval={interval}
columnHeaders={["City", "Visitors"]}
dataFetcher={useFetcher<typeof loader>()}
loaderUrl="/resources/city"
/>
);
};
42 changes: 42 additions & 0 deletions app/routes/resources.region.tsx
Original file line number Diff line number Diff line change
@@ -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.getCountByRegion(
site,
interval,
tz,
Number(page),
),
page: Number(page),
});
}

export const RegionCard = ({
siteId,
interval,
}: {
siteId: string;
interval: string;
}) => {
return (
<PaginatedTableCard
siteId={siteId}
interval={interval}
columnHeaders={["Region", "Visitors"]}
dataFetcher={useFetcher<typeof loader>()}
loaderUrl="/resources/region"
/>
);
};

0 comments on commit 557d87f

Please sign in to comment.