Skip to content

Commit

Permalink
fix(stats): populate empty chart axis (#289)
Browse files Browse the repository at this point in the history
* fix(stats): set ticks to natural numbers and ensure that dates are sequential

* chore: revert unintended formatting

* chore: revert unintended formatting

* chore: slightly tweak utils/db.ts

* chore: refactor logic to retrieve analytics metrics

* chore: tweak tests

* chore: tweak tests

* chore: tweak tests

* chore: make tests more robust

* chore: format

* chore: rebase

* chore: refactor logic to incorporate batching

* chore: implement review remarks

* perf tweak

* tweak

---------

Co-authored-by: Asher Gomez <ashersaupingomez@gmail.com>
  • Loading branch information
mbhrznr and iuioiua authored Jun 29, 2023
1 parent 440fda7 commit 9c815af
Show file tree
Hide file tree
Showing 3 changed files with 64 additions and 47 deletions.
43 changes: 20 additions & 23 deletions routes/stats.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
// Copyright 2023 the Deno authors. All rights reserved. MIT license.
import type { Handlers, PageProps } from "$fresh/server.ts";
import { DAY } from "std/datetime/constants.ts";
import { SITE_WIDTH_STYLES } from "@/utils/constants.ts";
import Head from "@/components/Head.tsx";
import type { State } from "./_middleware.ts";
import { getDatesSince, getManyMetrics } from "@/utils/db.ts";
import { Chart } from "fresh_charts/mod.ts";
import { ChartColors } from "fresh_charts/utils.ts";
import { DAY } from "std/datetime/constants.ts";

interface StatsPageData extends State {
dates: Date[];
Expand All @@ -18,7 +18,7 @@ interface StatsPageData extends State {

export const handler: Handlers<StatsPageData, State> = {
async GET(_req, ctx) {
const msAgo = 10 * DAY;
const msAgo = 30 * DAY;
const dates = getDatesSince(msAgo).map((date) => new Date(date));

const [
Expand Down Expand Up @@ -60,7 +60,11 @@ function LineChart(
legend: { display: false },
},
scales: {
y: { grid: { display: false }, beginAtZero: true },
y: {
beginAtZero: true,
grid: { display: false },
ticks: { stepSize: 1 },
},
x: { grid: { display: false } },
},
}}
Expand All @@ -82,6 +86,12 @@ function LineChart(
}

export default function StatsPage(props: PageProps<StatsPageData>) {
const charts: [string, bigint[]][] = [
["Visits", props.data.visitsCounts],
["Users", props.data.usersCounts],
["Items", props.data.itemsCounts],
["Votes", props.data.votesCounts],
];
const x = props.data.dates.map((date) =>
new Date(date).toLocaleDateString("en-us", {
year: "numeric",
Expand All @@ -105,26 +115,13 @@ export default function StatsPage(props: PageProps<StatsPageData>) {
</Head>
<div class={`${SITE_WIDTH_STYLES} flex-1 px-4`}>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<LineChart
title="Visits"
x={x}
y={props.data.visitsCounts}
/>
<LineChart
title="Users"
x={x}
y={props.data.usersCounts}
/>
<LineChart
title="Items"
x={x}
y={props.data.itemsCounts}
/>
<LineChart
title="Votes"
x={x}
y={props.data.votesCounts}
/>
{charts.map(([title, values]) => (
<LineChart
title={title}
x={x}
y={values}
/>
))}
</div>
</div>
</>
Expand Down
60 changes: 36 additions & 24 deletions utils/db.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// Copyright 2023 the Deno authors. All rights reserved. MIT license.
import { chunk } from "std/collections/chunk.ts";

const KV_PATH_KEY = "KV_PATH";
let path = undefined;
Expand Down Expand Up @@ -29,6 +30,35 @@ async function getValues<T>(
return values;
}

/**
* Gets many values from KV. Uses batched requests to get values in chunks of 10.
*/
async function getManyValues<T>(
keys: Deno.KvKey[],
): Promise<(T | null)[]> {
const promises = [];
for (const batch of chunk(keys, 10)) {
promises.push(kv.getMany<T[]>(batch));
}
return (await Promise.all(promises))
.flat()
.map((entry) => entry?.value);
}

/** Gets all dates since a given number of milliseconds ago */
export function getDatesSince(msAgo: number) {
const dates = [];
const now = Date.now();
const start = new Date(now - msAgo);

while (+start < now) {
start.setDate(start.getDate() + 1);
dates.push(formatDate(new Date(start)));
}

return dates;
}

/** Converts `Date` to ISO format that is zero UTC offset */
export function formatDate(date: Date) {
return date.toISOString().split("T")[0];
Expand Down Expand Up @@ -58,7 +88,7 @@ export function newItemProps(): Pick<Item, "id" | "score" | "createdAt"> {
*
* @example New item creation
* ```ts
* import { newItemProps, createItem, incrementAnalyticsMetricPerDay } from "@/utils/db.ts";
* import { newItemProps, createItem } from "@/utils/db.ts";
*
* const item: Item = {
* userId: "example-user-id",
Expand Down Expand Up @@ -397,8 +427,8 @@ export async function getUserByStripeCustomer(stripeCustomerId: string) {

export async function getManyUsers(ids: string[]) {
const keys = ids.map((id) => ["users", id]);
const res = await kv.getMany<User[]>(keys);
return res.map((entry) => entry.value!);
const res = await getManyValues<User>(keys);
return res.filter(Boolean) as User[];
}

export async function getAreVotedBySessionId(
Expand All @@ -419,35 +449,17 @@ export function compareScore(a: Item, b: Item) {

// Analytics
export async function incrVisitsCountByDay(date: Date) {
// convert to ISO format that is zero UTC offset
const visitsKey = [
"visits_count",
formatDate(date),
];
const visitsKey = ["visits_count", formatDate(date)];
await kv.atomic()
.sum(visitsKey, 1n)
.commit();
}

/** Gets all dates since a given number of milliseconds ago */
export function getDatesSince(msAgo: number) {
const dates = [];
const now = Date.now();
const start = new Date(now - msAgo);

while (+start < now) {
start.setDate(start.getDate() + 1);
dates.push(formatDate(new Date(start)));
}

return dates;
}

export async function getManyMetrics(
metric: "visits_count" | "items_count" | "votes_count" | "users_count",
dates: Date[],
) {
const keys = dates.map((date) => [metric, formatDate(date)]);
const res = await kv.getMany<bigint[]>(keys);
return res.map(({ value }) => value?.valueOf() ?? 0n);
const res = await getManyValues<bigint>(keys);
return res.map((value) => value?.valueOf() ?? 0n);
}
8 changes: 8 additions & 0 deletions utils/db_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,14 @@ Deno.test("[db] votes", async () => {
assertRejects(async () => await deleteVote({ item, user }));
});

Deno.test("[db] getManyMetrics()", async () => {
const last5Days = getDatesSince(DAY * 5).map((date) => new Date(date));
const last30Days = getDatesSince(DAY * 30).map((date) => new Date(date));

assertEquals((await getManyMetrics("items_count", last5Days)).length, 5);
assertEquals((await getManyMetrics("items_count", last30Days)).length, 30);
});

Deno.test("[db] formatDate()", () => {
assertEquals(formatDate(new Date("2023-01-01")), "2023-01-01");
assertEquals(formatDate(new Date("2023-01-01T13:59:08.740Z")), "2023-01-01");
Expand Down

0 comments on commit 9c815af

Please sign in to comment.