From d4f19557248df4a178138cc0c351e58ca6d67eb0 Mon Sep 17 00:00:00 2001 From: brunocorrea23 Date: Fri, 23 Jun 2023 18:43:17 -0300 Subject: [PATCH 1/5] analytics into atomic operations --- routes/callback.ts | 2 -- routes/submit.tsx | 2 -- utils/db.ts | 17 +++++++++++++---- utils/db_test.ts | 8 ++++++++ 4 files changed, 21 insertions(+), 8 deletions(-) diff --git a/routes/callback.ts b/routes/callback.ts index ed4a87bfb..ae9d49767 100644 --- a/routes/callback.ts +++ b/routes/callback.ts @@ -4,7 +4,6 @@ import { createUser, deleteUserBySession, getUser, - incrementAnalyticsMetricPerDay, newUserProps, updateUser, type User, @@ -63,7 +62,6 @@ export const handler: Handlers = { ...newUserProps(), }; await createUser(user); - await incrementAnalyticsMetricPerDay("users_count", new Date()); } else { await deleteUserBySession(sessionId); await updateUser({ ...user, sessionId }); diff --git a/routes/submit.tsx b/routes/submit.tsx index 00232340e..734df4346 100644 --- a/routes/submit.tsx +++ b/routes/submit.tsx @@ -6,7 +6,6 @@ import type { State } from "@/routes/_middleware.ts"; import { createItem, getUserBySession, - incrementAnalyticsMetricPerDay, type Item, newItemProps, } from "@/utils/db.ts"; @@ -51,7 +50,6 @@ export const handler: Handlers = { ...newItemProps(), }; await createItem(item); - await incrementAnalyticsMetricPerDay("items_count", new Date()); return redirect(`/item/${item!.id}`); }, diff --git a/utils/db.ts b/utils/db.ts index 9d194f90e..98114f931 100644 --- a/utils/db.ts +++ b/utils/db.ts @@ -63,7 +63,6 @@ export function newItemProps(): Pick { * }; * * await createItem(item); - * await incrementAnalyticsMetricPerDay("items_count", item.createdAt); * ``` */ export async function createItem(item: Item) { @@ -78,6 +77,7 @@ export async function createItem(item: Item) { .set(itemsKey, item) .set(itemsByTimeKey, item) .set(itemsByUserKey, item) + .sum(analyticsKey("items_count"), 1n) .commit(); if (!res.ok) throw new Error(`Failed to create item: ${item}`); @@ -196,12 +196,11 @@ export async function createVote(vote: Vote) { .set(itemsByUserKey, vote.item) .set(votedItemsByUserKey, vote.item) .set(votedUsersByItemKey, vote.user) + .sum(analyticsKey("votes_count"), 1n) .commit(); if (!res.ok) throw new Error(`Failed to set vote: ${vote}`); - await incrementAnalyticsMetricPerDay("votes_count", new Date()); - return vote; } @@ -281,7 +280,6 @@ export function newUserProps(): Pick { * ...newUserProps(), * }; * await createUser(user); - * await incrementAnalyticsMetricPerDay("users_count", new Date()); * ``` */ export async function createUser(user: User) { @@ -308,6 +306,7 @@ export async function createUser(user: User) { .set(usersKey, user) .set(usersByLoginKey, user) .set(usersBySessionKey, user) + .sum(analyticsKey("users_count"), 1n) .commit(); if (!res.ok) throw new Error(`Failed to create user: ${user}`); @@ -401,6 +400,16 @@ export async function incrementAnalyticsMetricPerDay( .commit(); } +export function analyticsKey( + metric: string, +) { + // convert to ISO format that is zero UTC offset + return [ + metric, + `${new Date().toISOString().split("T")[0]}`, + ]; +} + export async function incrementVisitsPerDay(date: Date) { // convert to ISO format that is zero UTC offset const visitsKey = [ diff --git a/utils/db_test.ts b/utils/db_test.ts index e4104b3af..44f2abfda 100644 --- a/utils/db_test.ts +++ b/utils/db_test.ts @@ -1,5 +1,6 @@ // Copyright 2023 the Deno authors. All rights reserved. MIT license. import { + analyticsKey, type Comment, createComment, createItem, @@ -205,3 +206,10 @@ Deno.test("[db] createComment() + getCommentsByItem()", async () => { await assertRejects(async () => await createComment(comment2)); assertArrayIncludes(await getCommentsByItem(itemId), [comment1, comment2]); }); + +Deno.test("[db] analyticsKey()", () => { + assertEquals(analyticsKey("metric_test"), [ + "metric_test", + `${new Date().toISOString().split("T")[0]}`, + ]); +}); From ad91d4afe0f7b78f836d2214383b02219640b298 Mon Sep 17 00:00:00 2001 From: brunocorrea23 Date: Sat, 24 Jun 2023 03:10:23 -0300 Subject: [PATCH 2/5] refactor analytics code and add tests --- utils/db.ts | 40 +++++++++------------------------------- utils/db_test.ts | 36 ++++++++++++++++++++++++++---------- 2 files changed, 35 insertions(+), 41 deletions(-) diff --git a/utils/db.ts b/utils/db.ts index 98114f931..604d2d56b 100644 --- a/utils/db.ts +++ b/utils/db.ts @@ -400,6 +400,13 @@ export async function incrementAnalyticsMetricPerDay( .commit(); } +export async function getAnalyticsMetricPerDay(metric: string, date: Date) { + return await getValue([ + metric, + `${date.toISOString().split("T")[0]}`, + ]); +} + export function analyticsKey( metric: string, ) { @@ -410,25 +417,7 @@ export function analyticsKey( ]; } -export async function incrementVisitsPerDay(date: Date) { - // convert to ISO format that is zero UTC offset - const visitsKey = [ - "visits", - `${date.toISOString().split("T")[0]}`, - ]; - await kv.atomic() - .sum(visitsKey, 1n) - .commit(); -} - -export async function getVisitsPerDay(date: Date) { - return await getValue([ - "visits", - `${date.toISOString().split("T")[0]}`, - ]); -} - -export async function getAnalyticsMetricsPerDay( +export async function getAnalyticsMetricListPerDay( metric: string, options?: Deno.KvListOptions, ) { @@ -447,19 +436,8 @@ export async function getManyAnalyticsMetricsPerDay( options?: Deno.KvListOptions, ) { const analyticsByDay = await Promise.all( - metrics.map((metric) => getAnalyticsMetricsPerDay(metric, options)), + metrics.map((metric) => getAnalyticsMetricListPerDay(metric, options)), ); return analyticsByDay; } - -export async function getAllVisitsPerDay(options?: Deno.KvListOptions) { - const iter = await kv.list({ prefix: ["visits"] }, options); - const visits = []; - const dates = []; - for await (const res of iter) { - visits.push(Number(res.value)); - dates.push(String(res.key[1])); - } - return { visits, dates }; -} diff --git a/utils/db_test.ts b/utils/db_test.ts index 44f2abfda..3eec6c5ca 100644 --- a/utils/db_test.ts +++ b/utils/db_test.ts @@ -7,6 +7,7 @@ import { createUser, deleteUserBySession, getAllItems, + getAnalyticsMetricPerDay, getCommentsByItem, getItem, getItemsByUser, @@ -16,8 +17,7 @@ import { getUserByLogin, getUserBySession, getUserByStripeCustomer, - getVisitsPerDay, - incrementVisitsPerDay, + incrementAnalyticsMetricPerDay, type Item, kv, newCommentProps, @@ -72,7 +72,14 @@ Deno.test("[db] (get/create)Item()", async () => { assertEquals(await getItem(item.id), null); + const itemsCount = + (await getAnalyticsMetricPerDay("items_count", new Date()))?.valueOf() ?? + 0n; await createItem(item); + assertEquals( + (await getAnalyticsMetricPerDay("items_count", new Date()))!.valueOf(), + itemsCount + 1n, + ); await assertRejects(async () => await createItem(item)); assertEquals(await getItem(item.id), item); }); @@ -140,8 +147,14 @@ Deno.test("[db] user", async () => { assertEquals(await getUserBySession(user.sessionId), null); assertEquals(await getUserByStripeCustomer(user.stripeCustomerId!), null); + const usersCount = (await getAnalyticsMetricPerDay("users_count", new Date())) + ?.valueOf() ?? 0n; await createUser(user); await assertRejects(async () => await createUser(user)); + assertEquals( + (await getAnalyticsMetricPerDay("users_count", new Date()))!.valueOf(), + usersCount + 1n, + ); assertEquals(await getUser(user.id), user); assertEquals(await getUserByLogin(user.login), user); assertEquals(await getUserBySession(user.sessionId), user); @@ -165,17 +178,20 @@ Deno.test("[db] user", async () => { ); }); -Deno.test("[db] visit", async () => { +Deno.test("[db] analytics increment/get", async () => { const date = new Date("2023-01-01"); - const visitsKey = [ - "visits", + const dummyKey = [ + "example", `${date.toISOString().split("T")[0]}`, ]; - await incrementVisitsPerDay(date); - assertEquals((await kv.get(visitsKey)).key[1], "2023-01-01"); - assertEquals((await getVisitsPerDay(date))!.valueOf(), 1n); - await kv.delete(visitsKey); - assertEquals(await getVisitsPerDay(date), null); + await incrementAnalyticsMetricPerDay("example", date); + assertEquals((await kv.get(dummyKey)).key[1], "2023-01-01"); + assertEquals( + (await getAnalyticsMetricPerDay("example", date))!.valueOf(), + 1n, + ); + await kv.delete(dummyKey); + assertEquals(await getAnalyticsMetricPerDay("example", date), null); }); Deno.test("[db] newCommentProps()", () => { From ff67ac06f943d28f46d9aaa7c6c114e4f50ef9cf Mon Sep 17 00:00:00 2001 From: brunocorrea23 Date: Tue, 27 Jun 2023 02:43:25 -0300 Subject: [PATCH 3/5] refactor metrics in atomic ops --- utils/db.ts | 24 +++++++++++------------- utils/db_test.ts | 10 ++++------ 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/utils/db.ts b/utils/db.ts index 604d2d56b..11636d01a 100644 --- a/utils/db.ts +++ b/utils/db.ts @@ -29,6 +29,11 @@ async function getValues( return values; } +/** Converts `Date` to ISO format that is zero UTC offset */ +export function formatDate(date: Date) { + return date.toISOString().split("T")[0]; +} + // Item export interface Item { userId: string; @@ -69,6 +74,7 @@ export async function createItem(item: Item) { const itemsKey = ["items", item.id]; const itemsByTimeKey = ["items_by_time", item.createdAt.getTime(), item.id]; const itemsByUserKey = ["items_by_user", item.userId, item.id]; + const itemsCountKey = ["items_count", `${formatDate(new Date())}`]; const res = await kv.atomic() .check({ key: itemsKey, versionstamp: null }) @@ -77,7 +83,7 @@ export async function createItem(item: Item) { .set(itemsKey, item) .set(itemsByTimeKey, item) .set(itemsByUserKey, item) - .sum(analyticsKey("items_count"), 1n) + .sum(itemsCountKey, 1n) .commit(); if (!res.ok) throw new Error(`Failed to create item: ${item}`); @@ -179,6 +185,7 @@ export async function createVote(vote: Vote) { vote.item.id, vote.user.id, ]; + const votesCountKey = ["votes_count", `${formatDate(new Date())}`]; const [itemRes, itemsByTimeRes, itemsByUserRes] = await kv.getMany([ itemKey, @@ -196,7 +203,7 @@ export async function createVote(vote: Vote) { .set(itemsByUserKey, vote.item) .set(votedItemsByUserKey, vote.item) .set(votedUsersByItemKey, vote.user) - .sum(analyticsKey("votes_count"), 1n) + .sum(votesCountKey, 1n) .commit(); if (!res.ok) throw new Error(`Failed to set vote: ${vote}`); @@ -286,6 +293,7 @@ export async function createUser(user: User) { const usersKey = ["users", user.id]; const usersByLoginKey = ["users_by_login", user.login]; const usersBySessionKey = ["users_by_session", user.sessionId]; + const usersCountKey = ["users_count", `${formatDate(new Date())}`]; const atomicOp = kv.atomic(); @@ -306,7 +314,7 @@ export async function createUser(user: User) { .set(usersKey, user) .set(usersByLoginKey, user) .set(usersBySessionKey, user) - .sum(analyticsKey("users_count"), 1n) + .sum(usersCountKey, 1n) .commit(); if (!res.ok) throw new Error(`Failed to create user: ${user}`); @@ -407,16 +415,6 @@ export async function getAnalyticsMetricPerDay(metric: string, date: Date) { ]); } -export function analyticsKey( - metric: string, -) { - // convert to ISO format that is zero UTC offset - return [ - metric, - `${new Date().toISOString().split("T")[0]}`, - ]; -} - export async function getAnalyticsMetricListPerDay( metric: string, options?: Deno.KvListOptions, diff --git a/utils/db_test.ts b/utils/db_test.ts index 3eec6c5ca..a25c7215c 100644 --- a/utils/db_test.ts +++ b/utils/db_test.ts @@ -1,11 +1,11 @@ // Copyright 2023 the Deno authors. All rights reserved. MIT license. import { - analyticsKey, type Comment, createComment, createItem, createUser, deleteUserBySession, + formatDate, getAllItems, getAnalyticsMetricPerDay, getCommentsByItem, @@ -223,9 +223,7 @@ Deno.test("[db] createComment() + getCommentsByItem()", async () => { assertArrayIncludes(await getCommentsByItem(itemId), [comment1, comment2]); }); -Deno.test("[db] analyticsKey()", () => { - assertEquals(analyticsKey("metric_test"), [ - "metric_test", - `${new Date().toISOString().split("T")[0]}`, - ]); +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"); }); From 3e2a394cb146a686799e9cf27b07212847c62779 Mon Sep 17 00:00:00 2001 From: brunocorrea23 Date: Tue, 27 Jun 2023 21:21:52 -0300 Subject: [PATCH 4/5] reverting some analytics change plus some refactoring --- routes/index.tsx | 4 +-- utils/db.ts | 80 ++++++++++++++++++++++++++++++++++++++++++------ utils/db_test.ts | 36 ++++++++++------------ 3 files changed, 90 insertions(+), 30 deletions(-) diff --git a/routes/index.tsx b/routes/index.tsx index 185b7d72b..9a4e0d43d 100644 --- a/routes/index.tsx +++ b/routes/index.tsx @@ -12,7 +12,7 @@ import { getAreVotedBySessionId, getItemsSince, getManyUsers, - incrementAnalyticsMetricPerDay, + incrementVisitsCountByDay, type Item, type User, } from "@/utils/db.ts"; @@ -31,7 +31,7 @@ function calcTimeAgoFilter(url: URL) { export const handler: Handlers = { async GET(req, ctx) { - await incrementAnalyticsMetricPerDay("visits_count", new Date()); + await incrementVisitsCountByDay(new Date()); const url = new URL(req.url); const pageNum = calcPageNum(url); diff --git a/utils/db.ts b/utils/db.ts index 11636d01a..53b5f528e 100644 --- a/utils/db.ts +++ b/utils/db.ts @@ -394,27 +394,89 @@ export function compareScore(a: Item, b: Item) { } // Analytics -export async function incrementAnalyticsMetricPerDay( - metric: string, - date: Date, -) { +export async function incrementVisitsCountByDay(date: Date) { // convert to ISO format that is zero UTC offset - const metricKey = [ - metric, + const visitsKey = [ + "visits_count", `${date.toISOString().split("T")[0]}`, ]; await kv.atomic() - .sum(metricKey, 1n) + .sum(visitsKey, 1n) .commit(); } -export async function getAnalyticsMetricPerDay(metric: string, date: Date) { +export async function getVisitsCountByDay(date: Date) { return await getValue([ - metric, + "visits_count", `${date.toISOString().split("T")[0]}`, ]); } +export async function getItemsCountByDay(date: Date) { + return await getValue([ + "items_count", + `${date.toISOString().split("T")[0]}`, + ]); +} + +export async function getVotesCountByDay(date: Date) { + return await getValue([ + "votes_count", + `${date.toISOString().split("T")[0]}`, + ]); +} + +export async function getUsersCountDay(date: Date) { + return await getValue([ + "users_count", + `${date.toISOString().split("T")[0]}`, + ]); +} + +export async function getAllVisitsCountByDay(options?: Deno.KvListOptions) { + const iter = await kv.list({ prefix: ["visits_count"] }, options); + const visits = []; + const dates = []; + for await (const res of iter) { + visits.push(Number(res.value)); + dates.push(String(res.key[1])); + } + return { visits, dates }; +} + +export async function getAllItemsCountByDay(options?: Deno.KvListOptions) { + const iter = await kv.list({ prefix: ["items_count"] }, options); + const visits = []; + const dates = []; + for await (const res of iter) { + visits.push(Number(res.value)); + dates.push(String(res.key[1])); + } + return { visits, dates }; +} + +export async function getAllVotesCountByDay(options?: Deno.KvListOptions) { + const iter = await kv.list({ prefix: ["votes_count"] }, options); + const visits = []; + const dates = []; + for await (const res of iter) { + visits.push(Number(res.value)); + dates.push(String(res.key[1])); + } + return { visits, dates }; +} + +export async function getAllUsersCountByDay(options?: Deno.KvListOptions) { + const iter = await kv.list({ prefix: ["users_count"] }, options); + const visits = []; + const dates = []; + for await (const res of iter) { + visits.push(Number(res.value)); + dates.push(String(res.key[1])); + } + return { visits, dates }; +} + export async function getAnalyticsMetricListPerDay( metric: string, options?: Deno.KvListOptions, diff --git a/utils/db_test.ts b/utils/db_test.ts index a25c7215c..60f17b8a3 100644 --- a/utils/db_test.ts +++ b/utils/db_test.ts @@ -7,17 +7,19 @@ import { deleteUserBySession, formatDate, getAllItems, - getAnalyticsMetricPerDay, getCommentsByItem, getItem, getItemsByUser, + getItemsCountByDay, getItemsSince, getManyUsers, getUser, getUserByLogin, getUserBySession, getUserByStripeCustomer, - incrementAnalyticsMetricPerDay, + getUsersCountDay, + getVisitsCountByDay, + incrementVisitsCountByDay, type Item, kv, newCommentProps, @@ -72,12 +74,11 @@ Deno.test("[db] (get/create)Item()", async () => { assertEquals(await getItem(item.id), null); - const itemsCount = - (await getAnalyticsMetricPerDay("items_count", new Date()))?.valueOf() ?? - 0n; + const itemsCount = (await getItemsCountByDay(new Date()))?.valueOf() ?? + 0n; await createItem(item); assertEquals( - (await getAnalyticsMetricPerDay("items_count", new Date()))!.valueOf(), + (await getItemsCountByDay(new Date()))!.valueOf(), itemsCount + 1n, ); await assertRejects(async () => await createItem(item)); @@ -147,12 +148,12 @@ Deno.test("[db] user", async () => { assertEquals(await getUserBySession(user.sessionId), null); assertEquals(await getUserByStripeCustomer(user.stripeCustomerId!), null); - const usersCount = (await getAnalyticsMetricPerDay("users_count", new Date())) + const usersCount = (await getUsersCountDay(new Date())) ?.valueOf() ?? 0n; await createUser(user); await assertRejects(async () => await createUser(user)); assertEquals( - (await getAnalyticsMetricPerDay("users_count", new Date()))!.valueOf(), + (await getUsersCountDay(new Date()))!.valueOf(), usersCount + 1n, ); assertEquals(await getUser(user.id), user); @@ -178,20 +179,17 @@ Deno.test("[db] user", async () => { ); }); -Deno.test("[db] analytics increment/get", async () => { +Deno.test("[db] visit", async () => { const date = new Date("2023-01-01"); - const dummyKey = [ - "example", + const visitsKey = [ + "visits_count", `${date.toISOString().split("T")[0]}`, ]; - await incrementAnalyticsMetricPerDay("example", date); - assertEquals((await kv.get(dummyKey)).key[1], "2023-01-01"); - assertEquals( - (await getAnalyticsMetricPerDay("example", date))!.valueOf(), - 1n, - ); - await kv.delete(dummyKey); - assertEquals(await getAnalyticsMetricPerDay("example", date), null); + await incrementVisitsCountByDay(date); + assertEquals((await kv.get(visitsKey)).key[1], "2023-01-01"); + assertEquals((await getVisitsCountByDay(date))!.valueOf(), 1n); + await kv.delete(visitsKey); + assertEquals(await getVisitsCountByDay(date), null); }); Deno.test("[db] newCommentProps()", () => { From 19167fbaa35da4a0c102c2a486b4eaea0c673ef1 Mon Sep 17 00:00:00 2001 From: brunocorrea23 Date: Tue, 27 Jun 2023 22:12:25 -0300 Subject: [PATCH 5/5] better --- routes/index.tsx | 4 ++-- utils/db.ts | 16 ++++++++-------- utils/db_test.ts | 4 ++-- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/routes/index.tsx b/routes/index.tsx index 824e75cc4..f91a4c2a3 100644 --- a/routes/index.tsx +++ b/routes/index.tsx @@ -12,7 +12,7 @@ import { getAreVotedBySessionId, getItemsSince, getManyUsers, - incrementVisitsCountByDay, + incrVisitsCountByDay, type Item, type User, } from "@/utils/db.ts"; @@ -31,7 +31,7 @@ function calcTimeAgoFilter(url: URL) { export const handler: Handlers = { async GET(req, ctx) { - await incrementVisitsCountByDay(new Date()); + await incrVisitsCountByDay(new Date()); const url = new URL(req.url); const pageNum = calcPageNum(url); diff --git a/utils/db.ts b/utils/db.ts index 231c1cfde..18b41435d 100644 --- a/utils/db.ts +++ b/utils/db.ts @@ -74,7 +74,7 @@ export async function createItem(item: Item) { const itemsKey = ["items", item.id]; const itemsByTimeKey = ["items_by_time", item.createdAt.getTime(), item.id]; const itemsByUserKey = ["items_by_user", item.userId, item.id]; - const itemsCountKey = ["items_count", `${formatDate(new Date())}`]; + const itemsCountKey = ["items_count", formatDate(new Date())]; const res = await kv.atomic() .check({ key: itemsKey, versionstamp: null }) @@ -209,7 +209,7 @@ export async function createVote(vote: Vote) { vote.item.id, vote.user.id, ]; - const votesCountKey = ["votes_count", `${formatDate(new Date())}`]; + const votesCountKey = ["votes_count", formatDate(new Date())]; const [itemRes, itemsByTimeRes, itemsByUserRes] = await kv.getMany([ itemKey, @@ -317,7 +317,7 @@ export async function createUser(user: User) { const usersKey = ["users", user.id]; const usersByLoginKey = ["users_by_login", user.login]; const usersBySessionKey = ["users_by_session", user.sessionId]; - const usersCountKey = ["users_count", `${formatDate(new Date())}`]; + const usersCountKey = ["users_count", formatDate(new Date())]; const atomicOp = kv.atomic(); @@ -418,7 +418,7 @@ export function compareScore(a: Item, b: Item) { } // Analytics -export async function incrementVisitsCountByDay(date: Date) { +export async function incrVisitsCountByDay(date: Date) { // convert to ISO format that is zero UTC offset const visitsKey = [ "visits_count", @@ -432,28 +432,28 @@ export async function incrementVisitsCountByDay(date: Date) { export async function getVisitsCountByDay(date: Date) { return await getValue([ "visits_count", - `${date.toISOString().split("T")[0]}`, + formatDate(date), ]); } export async function getItemsCountByDay(date: Date) { return await getValue([ "items_count", - `${date.toISOString().split("T")[0]}`, + formatDate(date), ]); } export async function getVotesCountByDay(date: Date) { return await getValue([ "votes_count", - `${date.toISOString().split("T")[0]}`, + formatDate(date), ]); } export async function getUsersCountByDay(date: Date) { return await getValue([ "users_count", - `${date.toISOString().split("T")[0]}`, + formatDate(date), ]); } diff --git a/utils/db_test.ts b/utils/db_test.ts index 48abe0b0c..fca893156 100644 --- a/utils/db_test.ts +++ b/utils/db_test.ts @@ -24,7 +24,7 @@ import { getUsersCountByDay, getVisitsCountByDay, getVotedItemsByUser, - incrementVisitsCountByDay, + incrVisitsCountByDay, type Item, kv, newCommentProps, @@ -179,7 +179,7 @@ Deno.test("[db] visit", async () => { "visits_count", `${date.toISOString().split("T")[0]}`, ]; - await incrementVisitsCountByDay(date); + await incrVisitsCountByDay(date); assertEquals((await kv.get(visitsKey)).key[1], "2023-01-01"); assertEquals((await getVisitsCountByDay(date))!.valueOf(), 1n); await kv.delete(visitsKey);