Skip to content

Commit

Permalink
feat: adding new stats (#244)
Browse files Browse the repository at this point in the history
* adding analytics with kv

* refactor

* fix

* improve charts UI

* better

* tweaks

* tweak

---------

Co-authored-by: Asher Gomez <[email protected]>
  • Loading branch information
brunocorrea23 and iuioiua authored Jun 4, 2023
1 parent 6e75b10 commit 4eabda6
Show file tree
Hide file tree
Showing 3 changed files with 98 additions and 35 deletions.
4 changes: 2 additions & 2 deletions routes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
getAllItemsInPastWeek,
getAreVotedBySessionId,
getManyUsers,
incrementVisitsPerDay,
incrementAnalyticsMetricPerDay,
type Item,
type User,
} from "@/utils/db.ts";
Expand All @@ -34,7 +34,7 @@ function calcLastPage(total = 0, pageLength = PAGE_LENGTH): number {

export const handler: Handlers<HomePageData, State> = {
async GET(req, ctx) {
await incrementVisitsPerDay(new Date());
await incrementAnalyticsMetricPerDay("visits_count", new Date());

const pageNum = calcPageNum(new URL(req.url));
const allItems = await getAllItemsInPastWeek();
Expand Down
56 changes: 37 additions & 19 deletions routes/stats.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,35 +4,51 @@ import { SITE_WIDTH_STYLES } from "@/utils/constants.ts";
import Layout from "@/components/Layout.tsx";
import Head from "@/components/Head.tsx";
import type { State } from "./_middleware.ts";
import { getAllVisitsPerDay } from "@/utils/db.ts";
import { getManyAnalyticsMetricsPerDay } from "@/utils/db.ts";
import { Chart } from "fresh_charts/mod.ts";
import { ChartColors } from "fresh_charts/utils.ts";

interface AnalyticsByDay {
metricsValue: number[];
dates: string[];
}

interface StatsPageData extends State {
visits?: number[];
dates?: string[];
metricsByDay: AnalyticsByDay[];
metricsTitles: string[];
}

export const handler: Handlers<StatsPageData, State> = {
async GET(_, ctx) {
const daysBefore = 30;
const { visits, dates } = await getAllVisitsPerDay({

const metricsKeys = [
"visits_count",
"users_count",
"items_count",
"votes_count",
];
const metricsTitles = ["Visits", "New Users", "New Items", "New Votes"];
const metricsByDay = await getManyAnalyticsMetricsPerDay(metricsKeys, {
limit: daysBefore,
});

return ctx.render({ ...ctx.state, visits, dates });
return ctx.render({ ...ctx.state, metricsByDay, metricsTitles });
},
};

function LineChart(
props: { title: string; x: string[]; y: number[] },
) {
return (
<>
<div class="py-4">
<h3 class="py-4 text-2xl font-bold">{props.title}</h3>
<Chart
width={550}
height={300}
type="line"
options={{
maintainAspectRatio: false,
plugins: {
legend: { display: false },
},
Expand All @@ -54,7 +70,7 @@ function LineChart(
}],
}}
/>
</>
</div>
);
}

Expand All @@ -64,18 +80,20 @@ export default function StatsPage(props: PageProps<StatsPageData>) {
<Head title="Stats" href={props.url.href} />
<Layout session={props.data.sessionId}>
<div class={`${SITE_WIDTH_STYLES} flex-1 px-4`}>
<div class="p-4 mx-auto max-w-screen-md">
<LineChart
title="Visits"
x={props.data.dates!.map((date) =>
new Date(date).toLocaleDateString("en-us", {
year: "numeric",
month: "short",
day: "numeric",
})
)}
y={props.data.visits!}
/>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
{props.data.metricsByDay.map((metric, index) => (
<LineChart
title={props.data.metricsTitles[index]}
x={metric.dates!.map((date) =>
new Date(date).toLocaleDateString("en-us", {
year: "numeric",
month: "short",
day: "numeric",
})
)}
y={metric.metricsValue!}
/>
))}
</div>
</div>
</Layout>
Expand Down
73 changes: 59 additions & 14 deletions utils/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ export async function createItem(initItem: InitItem) {

if (!res.ok) throw new Error(`Failed to set item: ${item}`);

await incrementAnalyticsMetricPerDay("items_count", new Date());

return item;
}

Expand Down Expand Up @@ -165,6 +167,8 @@ export async function createVote(vote: Vote) {

if (!res.ok) throw new Error(`Failed to set vote: ${vote}`);

await incrementAnalyticsMetricPerDay("votes_count", new Date());

return vote;
}

Expand Down Expand Up @@ -251,6 +255,8 @@ export async function createUser(initUser: InitUser) {

if (!res.ok) throw new Error(`Failed to create user: ${user}`);

await incrementAnalyticsMetricPerDay("users_count", new Date());

return user;
}

Expand Down Expand Up @@ -317,7 +323,37 @@ export async function getManyUsers(ids: string[]) {
return res.map((entry) => entry.value!);
}

// Visit
export async function getAreVotedBySessionId(
items: Item[],
sessionId?: string,
) {
if (!sessionId) return [];
const sessionUser = await getUserBySessionId(sessionId);
if (!sessionUser) return [];
const votedItems = await getVotedItemsByUser(sessionUser.id);
const votedItemIds = votedItems.map((item) => item.id);
return items.map((item) => votedItemIds.includes(item.id));
}

export function compareScore(a: Item, b: Item) {
return Number(b.score) - Number(a.score);
}

// Analytics
export async function incrementAnalyticsMetricPerDay(
metric: string,
date: Date,
) {
// convert to ISO format that is zero UTC offset
const metricKey = [
metric,
`${date.toISOString().split("T")[0]}`,
];
await kv.atomic()
.sum(metricKey, 1n)
.commit();
}

export async function incrementVisitsPerDay(date: Date) {
// convert to ISO format that is zero UTC offset
const visitsKey = [
Expand All @@ -336,16 +372,29 @@ export async function getVisitsPerDay(date: Date) {
]);
}

export async function getAreVotedBySessionId(
items: Item[],
sessionId?: string,
export async function getAnalyticsMetricsPerDay(
metric: string,
options?: Deno.KvListOptions,
) {
if (!sessionId) return [];
const sessionUser = await getUserBySessionId(sessionId);
if (!sessionUser) return [];
const votedItems = await getVotedItemsByUser(sessionUser.id);
const votedItemIds = votedItems.map((item) => item.id);
return items.map((item) => votedItemIds.includes(item.id));
const iter = await kv.list<bigint>({ prefix: [metric] }, options);
const metricsValue = [];
const dates = [];
for await (const res of iter) {
metricsValue.push(Number(res.value));
dates.push(String(res.key[1]));
}
return { metricsValue, dates };
}

export async function getManyAnalyticsMetricsPerDay(
metrics: string[],
options?: Deno.KvListOptions,
) {
const analyticsByDay = await Promise.all(
metrics.map((metric) => getAnalyticsMetricsPerDay(metric, options)),
);

return analyticsByDay;
}

export async function getAllVisitsPerDay(options?: Deno.KvListOptions) {
Expand All @@ -358,7 +407,3 @@ export async function getAllVisitsPerDay(options?: Deno.KvListOptions) {
}
return { visits, dates };
}

export function compareScore(a: Item, b: Item) {
return Number(b.score) - Number(a.score);
}

0 comments on commit 4eabda6

Please sign in to comment.