Skip to content

Commit

Permalink
adding browser version tracking
Browse files Browse the repository at this point in the history
  • Loading branch information
S-Makrod committed Dec 26, 2024
1 parent 7aa1b99 commit f428c14
Show file tree
Hide file tree
Showing 11 changed files with 175 additions and 9 deletions.
1 change: 1 addition & 0 deletions app/analytics/__tests__/collect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ describe("collectRequestHandler", () => {
"Chrome", // browser name
"",
"example", // site id
"51.0.2704.103", // browser version
],
doubles: [
1, // new visitor
Expand Down
3 changes: 3 additions & 0 deletions app/analytics/collect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ export function collectRequestHandler(request: Request, env: Env) {
// user agent stuff
userAgent: userAgent,
browserName: parsedUserAgent.getBrowser().name,
browserVersion: parsedUserAgent.getBrowser().version,
deviceModel: parsedUserAgent.getDevice().model,
};

Expand Down Expand Up @@ -157,6 +158,7 @@ interface DataPoint {
country?: string;
referrer?: string;
browserName?: string;
browserVersion?: string;
deviceModel?: string;

// doubles
Expand All @@ -183,6 +185,7 @@ export function writeDataPoint(
data.browserName || "", // blob6
data.deviceModel || "", // blob7
data.siteId || "", // blob8
data.browserVersion || "", // blob9
],
doubles: [data.newVisitor || 0, data.newSession || 0, data.bounce],
};
Expand Down
20 changes: 19 additions & 1 deletion app/analytics/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ function filtersToSql(filters: SearchFilters) {
"path",
"referrer",
"browserName",
"browserVersion",
"country",
"deviceModel",
];
Expand Down Expand Up @@ -654,6 +655,23 @@ export class AnalyticsEngineAPI {
);
}

async getCountByBrowserVersion(
siteId: string,
interval: string,
tz?: string,
filters: SearchFilters = {},
page: number = 1,
): Promise<[browser: string, visitors: number][]> {
return this.getVisitorCountByColumn(
siteId,
"browserVersion",
interval,
tz,
filters,
page,
);
}

async getCountByDevice(
siteId: string,
interval: string,
Expand Down Expand Up @@ -725,7 +743,7 @@ export class AnalyticsEngineAPI {
earliestBounce: Date | null;
}> {
const query = `
SELECT
SELECT
MIN(timestamp) as earliestEvent,
${ColumnMappings.bounce} as isBounce
FROM metricsDataset
Expand Down
1 change: 1 addition & 0 deletions app/analytics/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const ColumnMappings = {
browserName: "blob6",
deviceModel: "blob7",
siteId: "blob8",
browserVersion: "blob9",

/**
* doubles
Expand Down
3 changes: 2 additions & 1 deletion app/lib/__tests__/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,15 @@ dayjs.extend(timezone);
describe("getFiltersFromSearchParams", () => {
test("it should return an object with the correct keys", () => {
const searchParams = new URLSearchParams(
"?path=/about&referrer=google.com&deviceModel=iphone&country=us&browserName=chrome",
"?path=/about&referrer=google.com&deviceModel=iphone&country=us&browserName=chrome&browserVersion=118",
);
expect(getFiltersFromSearchParams(searchParams)).toEqual({
path: "/about",
referrer: "google.com",
deviceModel: "iphone",
country: "us",
browserName: "chrome",
browserVersion: "118",
});
});

Expand Down
1 change: 1 addition & 0 deletions app/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ export interface SearchFilters {
deviceModel?: string;
country?: string;
browserName?: string;
browserVersion?: string;
}
4 changes: 4 additions & 0 deletions app/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ interface SearchFilters {
deviceModel?: string;
country?: string;
browserName?: string;
browserVersion?: string;
}

export function getFiltersFromSearchParams(searchParams: URLSearchParams) {
Expand All @@ -46,6 +47,9 @@ export function getFiltersFromSearchParams(searchParams: URLSearchParams) {
if (searchParams.has("browserName")) {
filters.browserName = searchParams.get("browserName") || "";
}
if (searchParams.has("browserVersion")) {
filters.browserVersion = searchParams.get("browserVersion") || "";
}

return filters;
}
Expand Down
12 changes: 12 additions & 0 deletions app/routes/__tests__/dashboard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,12 @@ describe("Dashboard route", () => {
return { countsByProperty: [] };
},
},
{
path: "/resources/browserversion",
loader: () => {
return { countsByProperty: [] };
},
},
],
},
]);
Expand Down Expand Up @@ -382,6 +388,12 @@ describe("Dashboard route", () => {
};
},
},
{
path: "/resources/browserversion",
loader: () => {
return { countsByProperty: [] };
},
},
],
},
]);
Expand Down
58 changes: 58 additions & 0 deletions app/routes/__tests__/resources.browserversion.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// @vitest-environment jsdom
import {
vi,
test,
describe,
beforeEach,
afterEach,
expect,
Mock,
} from "vitest";
import "vitest-dom/extend-expect";

import { loader } from "../resources.browserversion";
import { createFetchResponse, getDefaultContext } from "./testutils";

describe("Resources/Browserversion route", () => {
let fetch: Mock;

beforeEach(() => {
fetch = global.fetch = vi.fn();
});

afterEach(() => {
vi.restoreAllMocks();
});

describe("loader", () => {
test("returns valid json", async () => {
fetch.mockResolvedValueOnce(
createFetchResponse({
data: [
{ blob6: "Chrome", blob9: "118", count: "5" },
{ blob6: "Chrome", blob9: "117", count: "15" },
{ blob6: "Chrome", blob9: "116", count: "1" },
],
}),
);

const response = await loader({
...getDefaultContext(),
// @ts-expect-error we don't need to provide all the properties of the request object
request: {
url: "http://localhost:3000/resources/browserversion?browserName=Chrome", // need browserName query param
},
});

const json = await response;
expect(json).toEqual({
countsByProperty: [
["118", 5],
["117", 15],
["116", 1],
],
page: 1,
});
});
});
});
25 changes: 18 additions & 7 deletions app/routes/dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
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";

Expand Down Expand Up @@ -250,13 +251,23 @@ export default function Dashboard() {
/>
</div>
<div className="grid md:grid-cols-3 gap-4 mb-4">
<BrowserCard
siteId={data.siteId}
interval={data.interval}
filters={data.filters}
onFilterChange={handleFilterChange}
timezone={userTimezone}
/>
{data.filters && data.filters.browserName ? (
<BrowserVersionCard
siteId={data.siteId}
interval={data.interval}
filters={data.filters}
onFilterChange={handleFilterChange}
timezone={userTimezone}
/>
) : (
<BrowserCard
siteId={data.siteId}
interval={data.interval}
filters={data.filters}
onFilterChange={handleFilterChange}
timezone={userTimezone}
/>
)}

<CountryCard
siteId={data.siteId}
Expand Down
56 changes: 56 additions & 0 deletions app/routes/resources.browserversion.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { useFetcher } from "@remix-run/react";

import type { LoaderFunctionArgs } from "@remix-run/cloudflare";

import { getFiltersFromSearchParams, paramsFromUrl } from "~/lib/utils";
import PaginatedTableCard from "~/components/PaginatedTableCard";
import { SearchFilters } from "~/lib/types";

export async function loader({ context, request }: LoaderFunctionArgs) {
const { analyticsEngine } = context;

const { interval, site, page = 1 } = paramsFromUrl(request.url);
const url = new URL(request.url);
const tz = url.searchParams.get("timezone") || "UTC";
const filters = getFiltersFromSearchParams(url.searchParams);

return {
countsByProperty: await analyticsEngine.getCountByBrowserVersion(
site,
interval,
tz,
filters,
Number(page),
),
page: Number(page),
};
}

export const BrowserVersionCard = ({
siteId,
interval,
filters,
onFilterChange,
timezone,
}: {
siteId: string;
interval: string;
filters: SearchFilters;
onFilterChange: (filters: SearchFilters) => void;
timezone: string;
}) => {
return (
<PaginatedTableCard
siteId={siteId}
interval={interval}
columnHeaders={["Browser Versions", "Visitors"]}
dataFetcher={useFetcher<typeof loader>()}
loaderUrl="/resources/browserversion"
onClick={(browserVersion) =>
onFilterChange({ ...filters, browserVersion })
}
filters={filters}
timezone={timezone}
/>
);
};

0 comments on commit f428c14

Please sign in to comment.