Skip to content

Commit

Permalink
Render views + visitors in table card by path (#48)
Browse files Browse the repository at this point in the history
* Render views + visitors in table card by path

* Sort results by visitors

* Views, not visits

* Put common count accumulation code into a helper fn
  • Loading branch information
benvinegar authored Feb 18, 2024
1 parent 52748ab commit b1ed6ae
Show file tree
Hide file tree
Showing 4 changed files with 127 additions and 18 deletions.
129 changes: 114 additions & 15 deletions app/analytics/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,27 @@ interface AnalyticsCountResult {
visitors: number;
}

/** Given an AnalyticsCountResult object, and an object representing a row returned from
* CF Analytics Engine w/ counts grouped by isVisitor and isVisit, accumulate view,
* visit, and visitor counts.
*/
function accumulateCountsFromRowResult(
counts: AnalyticsCountResult,
row: {
count: number;
isVisitor: number;
isVisit: number;
},
) {
if (row.isVisit == 1) {
counts.visits += Number(row.count);
}
if (row.isVisitor == 1) {
counts.visitors += Number(row.count);
}
counts.views += Number(row.count);
}

/**
* Convert a Date object to YY-MM-DD HH:MM:SS
*/
Expand Down Expand Up @@ -239,8 +260,8 @@ export class AnalyticsEngineAPI {
const siteIdColumn = ColumnMappings["siteId"];

const query = `
SELECT SUM(_sample_interval) as count,
${ColumnMappings.newVisitor} as isVisitor,
SELECT SUM(_sample_interval) as count,
${ColumnMappings.newVisitor} as isVisitor,
${ColumnMappings.newSession} as isVisit
FROM metricsDataset
WHERE timestamp > NOW() - INTERVAL '${interval}' DAY
Expand Down Expand Up @@ -276,13 +297,7 @@ export class AnalyticsEngineAPI {
// NOTE: note it's possible to get no results, or half results (i.e. a row where isVisit=1 but
// no row where isVisit=0), so this code makes no assumption on number of results
responseData.data.forEach((row) => {
if (row.isVisit == 1) {
counts.visits += Number(row.count);
}
if (row.isVisitor == 1) {
counts.visitors += Number(row.count);
}
counts.views += Number(row.count);
accumulateCountsFromRowResult(counts, row);
});
resolve(counts);
})(),
Expand Down Expand Up @@ -344,6 +359,95 @@ export class AnalyticsEngineAPI {
return returnPromise;
}

async getAllCountsByColumn<T extends keyof typeof ColumnMappings>(
siteId: string,
column: T,
sinceDays: number,
limit?: number,
) {
// defaults to 1 day if not specified
const interval = sinceDays || 1;
limit = limit || 10;

const _column = ColumnMappings[column];
const query = `
SELECT ${_column},
${ColumnMappings.newVisitor} as isVisitor,
${ColumnMappings.newSession} as isVisit,
SUM(_sample_interval) as count
FROM metricsDataset
WHERE timestamp > NOW() - INTERVAL '${interval}' DAY
AND ${ColumnMappings.siteId} = '${siteId}'
GROUP BY ${_column}, ${ColumnMappings.newVisitor}, ${ColumnMappings.newSession}
ORDER BY count DESC
LIMIT ${limit}`;

type SelectionSet = {
readonly count: number;
readonly isVisitor: number;
readonly isVisit: number;
} & Record<
(typeof ColumnMappings)[T],
ColumnMappingToType<(typeof ColumnMappings)[T]>
>;

const queryResult = this.query(query);
const returnPromise = new Promise<Record<string, AnalyticsCountResult>>(
(resolve, reject) =>
(async () => {
const response = await queryResult;

if (!response.ok) {
reject(response.statusText);
}

const responseData =
(await response.json()) as AnalyticsQueryResult<SelectionSet>;

const result = responseData.data.reduce(
(acc, row) => {
console.log(row);
const key =
row[_column] === ""
? "(none)"
: (row[_column] as string);
if (!Object.hasOwn(acc, key)) {
acc[key] = {
views: 0,
visitors: 0,
visits: 0,
} as AnalyticsCountResult;
}

accumulateCountsFromRowResult(acc[key], row);
return acc;
},
{} as Record<string, AnalyticsCountResult>,
);
resolve(result);
})(),
);
return returnPromise;
}

async getCountByPath(siteId: string, sinceDays: number) {
const allCountsResultPromise = this.getAllCountsByColumn(
siteId,
"path",
sinceDays,
);

return allCountsResultPromise.then((allCountsResult) => {
const result: [string, number, number][] = [];
for (const [key] of Object.entries(allCountsResult)) {
const record = allCountsResult[key];
result.push([key, record.visitors, record.views]);
}
// sort by visitors
return result.sort((a, b) => b[1] - a[1]);
});
}

async getCountByUserAgent(siteId: string, sinceDays: number) {
return this.getVisitorCountByColumn(siteId, "userAgent", sinceDays);
}
Expand All @@ -355,11 +459,6 @@ export class AnalyticsEngineAPI {
async getCountByReferrer(siteId: string, sinceDays: number) {
return this.getVisitorCountByColumn(siteId, "referrer", sinceDays);
}

async getCountByPath(siteId: string, sinceDays: number) {
return this.getVisitorCountByColumn(siteId, "path", sinceDays);
}

async getCountByBrowser(siteId: string, sinceDays: number) {
return this.getVisitorCountByColumn(siteId, "browserName", sinceDays);
}
Expand All @@ -374,7 +473,7 @@ export class AnalyticsEngineAPI {
limit = limit || 10;

const query = `
SELECT SUM(_sample_interval) as count,
SELECT SUM(_sample_interval) as count,
${ColumnMappings.siteId} as siteId
FROM metricsDataset
WHERE timestamp > NOW() - INTERVAL '${interval}' DAY
Expand Down
7 changes: 7 additions & 0 deletions app/components/TableCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,16 @@ export default function TableCard({
>
{item[0]}
</TableCell>

<TableCell className="text-right w-1/5">
{item[1]}
</TableCell>

{item.length > 2 && (
<TableCell className="text-right w-1/5">
{item[2]}
</TableCell>
)}
</TableRow>
))}
</TableBody>
Expand Down
7 changes: 5 additions & 2 deletions app/routes/dashboard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,10 @@ describe("Dashboard route", () => {
// response for getCountByPath
fetch.mockResolvedValueOnce(
createFetchResponse({
data: [{ blob3: "/", count: 1 }],
data: [
{ blob3: "/", count: 1, isVisitor: 1, isVisit: 1 },
{ blob3: "/", count: 3, isVisitor: 0, isVisit: 0 },
],
}),
);

Expand Down Expand Up @@ -203,7 +206,7 @@ describe("Dashboard route", () => {
views: 6,
visits: 3,
visitors: 1,
countByPath: [["/", 1]],
countByPath: [["/", 1, 4]],
countByCountry: [["US", 1]],
countByReferrer: [["google.com", 1]],
countByBrowser: [["Chrome", 2]],
Expand Down
2 changes: 1 addition & 1 deletion app/routes/dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ export default function Dashboard() {
<div className="grid md:grid-cols-2 gap-4 mb-4">
<TableCard
countByProperty={data.countByPath}
columnHeaders={["Page", "Visitors"]}
columnHeaders={["Page", "Visitors", "Views"]}
/>

<TableCard
Expand Down

0 comments on commit b1ed6ae

Please sign in to comment.