Skip to content

Commit

Permalink
add per-referrer analytics chart
Browse files Browse the repository at this point in the history
  • Loading branch information
kumpmati committed Apr 17, 2024
1 parent fb273b2 commit 3eb547d
Show file tree
Hide file tree
Showing 11 changed files with 279 additions and 154 deletions.
153 changes: 153 additions & 0 deletions src/lib/server/database/handlers/analytics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { and, asc, desc, eq, gte, sql } from 'drizzle-orm';
import { linkVisit } from '../schema/analytics';
import { db } from '..';
import dayjs from 'dayjs';
import type { AuthUser } from '../schema/auth';
import { links, type Link } from '../schema/link';

export const getLinkVisits = async (linkId: string) => {
return await db
.select()
.from(linkVisit)
.where(eq(linkVisit.linkId, linkId))
.orderBy(desc(linkVisit.timestamp));
};

export type LinkVisitsPerDay = {
day: string;
count: number;
}[];

export const getLinkVisitsPerDay = async (linkId: string): Promise<LinkVisitsPerDay> => {
const data = await db
.select({
day: sql<string>`date_trunc('day', ${linkVisit.timestamp})`,
count: sql<number>`cast(count(*) as int)`
})
.from(linkVisit)
.where(eq(linkVisit.linkId, linkId))
.orderBy(asc(sql`1`))
.groupBy(sql`1`);

if (data.length === 0) {
return [];
}

let current = dayjs(data.at(0)!.day);
const last = dayjs(data.at(-1)!.day);

while (dayjs(current).isBefore(last)) {
const hasVisits = data.find((d) => Math.abs(current.diff(d.day)) < 1);

if (!hasVisits) {
data.push({ day: current.format('YYYY-MM-DD 00:00:00'), count: 0 });
}

current = current.add(1, 'day');
}

return data.toSorted((a, b) => new Date(a.day).getTime() - new Date(b.day).getTime());
};

export type LinkVisitsPerReferrer = {
referrer: string;
count: number;
}[];

export const getLinkVisitsPerReferrer = async (linkId: string) => {
return await db
.select({
count: sql<number>`cast(count(${linkVisit.id}) as int)`,
referrer: linkVisit.referrer
})
.from(linkVisit)
.where(eq(linkVisit.linkId, linkId))
.groupBy(linkVisit.referrer);
};

/**
* Returns overall statistics for all of the user's links
* @param userId
*/
export const getOverallLinkStatistics = async (
userId: AuthUser['id']
): Promise<OverallLinkStatistics> => {
const last24hDate = dayjs().subtract(24, 'hours').toDate();

const [lastDayVisits, alltimeVisits, mostVisited] = await Promise.all([
db
.select({ count: sql<number>`cast(count(${linkVisit.id}) as int)` })
.from(linkVisit)
.where(gte(linkVisit.timestamp, last24hDate))
.innerJoin(links, and(eq(linkVisit.linkId, links.id), eq(links.userId, userId))),

db
.select({ count: sql<number>`cast(count(${linkVisit.id}) as int)` })
.from(linkVisit)
.innerJoin(links, and(eq(linkVisit.linkId, links.id), eq(links.userId, userId))),

db
.select({
linkId: linkVisit.linkId,
count: sql<number>`cast(count(${linkVisit.linkId}) as int)`
})
.from(linkVisit)
.innerJoin(links, and(eq(linkVisit.linkId, links.id), eq(links.userId, userId)))
.groupBy(linkVisit.linkId)
.orderBy(desc(sql`count`))
.limit(1)
]);

return {
lastDay: lastDayVisits[0].count,
total: alltimeVisits[0].count,
mostVisited: mostVisited.at(0) ?? null
};
};

export type OverallLinkStatistics = {
lastDay: number;
total: number;
mostVisited: { linkId: string; count: number } | null;
};

/**
* Returns overall statistics for all of the user's links
* @param userId
*/
export const getLinkStatistics = async (linkId: Link['id']): Promise<PerLinkStatistics> => {
const last24hDate = dayjs().subtract(24, 'hours').toDate();
const lastWeekDate = dayjs().subtract(7, 'days').toDate();

const [lastDayVisits, lastWeekVisits, alltimeVisits] = await Promise.all([
db
.select({ count: sql<number>`cast(count(${linkVisit.id}) as int)` })
.from(linkVisit)
.where(and(gte(linkVisit.timestamp, last24hDate), eq(linkVisit.linkId, linkId)))
.innerJoin(links, eq(linkVisit.linkId, links.id)),

db
.select({ count: sql<number>`cast(count(${linkVisit.id}) as int)` })
.from(linkVisit)
.where(and(gte(linkVisit.timestamp, lastWeekDate), eq(linkVisit.linkId, linkId)))
.innerJoin(links, eq(linkVisit.linkId, links.id)),

db
.select({ count: sql<number>`cast(count(${linkVisit.id}) as int)` })
.from(linkVisit)
.where(eq(linkVisit.linkId, linkId))
.innerJoin(links, eq(linkVisit.linkId, links.id))
]);

return {
lastDay: lastDayVisits[0].count,
lastWeek: lastWeekVisits[0].count,
total: alltimeVisits[0].count
};
};

export type PerLinkStatistics = {
lastDay: number;
lastWeek: number;
total: number;
};
92 changes: 1 addition & 91 deletions src/lib/server/database/handlers/linkVisit.ts
Original file line number Diff line number Diff line change
@@ -1,96 +1,6 @@
import { and, desc, eq, gte, sql } from 'drizzle-orm';
import { db } from '..';
import { linkVisit } from '../schema/analytics';
import type { AuthUser } from '../schema/auth';
import { links, type Link } from '../schema/link';
import dayjs from 'dayjs';

/**
* Returns overall statistics for all of the user's links
* @param userId
*/
export const getOverallLinkStatistics = async (
userId: AuthUser['id']
): Promise<OverallLinkStatistics> => {
const last24hDate = dayjs().subtract(24, 'hours').toDate();

const [lastDayVisits, alltimeVisits, mostVisited] = await Promise.all([
db
.select({ count: sql<number>`cast(count(${linkVisit.id}) as int)` })
.from(linkVisit)
.where(gte(linkVisit.timestamp, last24hDate))
.innerJoin(links, and(eq(linkVisit.linkId, links.id), eq(links.userId, userId))),

db
.select({ count: sql<number>`cast(count(${linkVisit.id}) as int)` })
.from(linkVisit)
.innerJoin(links, and(eq(linkVisit.linkId, links.id), eq(links.userId, userId))),

db
.select({
linkId: linkVisit.linkId,
count: sql<number>`cast(count(${linkVisit.linkId}) as int)`
})
.from(linkVisit)
.innerJoin(links, and(eq(linkVisit.linkId, links.id), eq(links.userId, userId)))
.groupBy(linkVisit.linkId)
.orderBy(desc(sql`count`))
.limit(1)
]);

return {
lastDay: lastDayVisits[0].count,
total: alltimeVisits[0].count,
mostVisited: mostVisited.at(0) ?? null
};
};

export type OverallLinkStatistics = {
lastDay: number;
total: number;
mostVisited: { linkId: string; count: number } | null;
};

/**
* Returns overall statistics for all of the user's links
* @param userId
*/
export const getLinkStatistics = async (linkId: Link['id']): Promise<PerLinkStatistics> => {
const last24hDate = dayjs().subtract(24, 'hours').toDate();
const lastWeekDate = dayjs().subtract(7, 'days').toDate();

const [lastDayVisits, lastWeekVisits, alltimeVisits] = await Promise.all([
db
.select({ count: sql<number>`cast(count(${linkVisit.id}) as int)` })
.from(linkVisit)
.where(and(gte(linkVisit.timestamp, last24hDate), eq(linkVisit.linkId, linkId)))
.innerJoin(links, eq(linkVisit.linkId, links.id)),

db
.select({ count: sql<number>`cast(count(${linkVisit.id}) as int)` })
.from(linkVisit)
.where(and(gte(linkVisit.timestamp, lastWeekDate), eq(linkVisit.linkId, linkId)))
.innerJoin(links, eq(linkVisit.linkId, links.id)),

db
.select({ count: sql<number>`cast(count(${linkVisit.id}) as int)` })
.from(linkVisit)
.where(eq(linkVisit.linkId, linkId))
.innerJoin(links, eq(linkVisit.linkId, links.id))
]);

return {
lastDay: lastDayVisits[0].count,
lastWeek: lastWeekVisits[0].count,
total: alltimeVisits[0].count
};
};

export type PerLinkStatistics = {
lastDay: number;
lastWeek: number;
total: number;
};
import type { Link } from '../schema/link';

/**
* Creates a new link visit entry, returning true if the visit was successful
Expand Down
2 changes: 1 addition & 1 deletion src/routes/(app)/me/+page.server.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { PageServerLoad } from './$types';
import { error, redirect } from '@sveltejs/kit';
import { deleteLink, getAllUserLinks } from '$lib/server/database/handlers/links';
import { getOverallLinkStatistics } from '$lib/server/database/handlers/linkVisit';
import { getOverallLinkStatistics } from '$lib/server/database/handlers/analytics';
import { deleteUser } from '$lib/server/database/handlers/user';

export const load = (async ({ parent }) => {
Expand Down
2 changes: 1 addition & 1 deletion src/routes/(app)/me/links/[id]/+layout.server.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getLinkStatistics } from '$lib/server/database/handlers/linkVisit';
import { getLinkStatistics } from '$lib/server/database/handlers/analytics';
import { getUserLink } from '$lib/server/database/handlers/links.js';
import { error } from '@sveltejs/kit';

Expand Down
2 changes: 1 addition & 1 deletion src/routes/(app)/me/links/[id]/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import { IconChartHistogram, IconChevronLeft, IconEdit } from '@tabler/icons-svelte';
</script>

<div class="relative mt-14 flex w-full flex-col gap-2 lg:flex-row">
<div class="relative mt-14 flex w-full flex-col gap-2 pb-8 lg:flex-row">
<div class="flex flex-row gap-1 pr-4 lg:absolute lg:-translate-x-full lg:flex-col xl:flex">
<Button href="/me" variant="ghost" class="justify-start gap-2 pl-2 lg:pl-4">
<IconChevronLeft size={16} /> Profile
Expand Down
18 changes: 6 additions & 12 deletions src/routes/(app)/me/links/[id]/analytics/+page.server.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,12 @@
import { linkVisit } from '$lib/server/database/schema/analytics';
import { eq, sql } from 'drizzle-orm';
import { db } from '$lib/server/database';
import {
getLinkVisitsPerDay,
getLinkVisitsPerReferrer
} from '$lib/server/database/handlers/analytics';
import type { PageServerLoad } from './$types';

export const load = (async ({ params }) => {
return {
visitsByDay: db
.select({
day: sql<string>`date_trunc('day', ${linkVisit.timestamp})`,
count: sql<number>`cast(count(*) as int)`
})
.from(linkVisit)
.where(eq(linkVisit.linkId, params.id))
.orderBy(sql`1`)
.groupBy(sql`1`)
visitsPerDay: getLinkVisitsPerDay(params.id),
visitsPerReferrer: getLinkVisitsPerReferrer(params.id)
};
}) satisfies PageServerLoad;
72 changes: 33 additions & 39 deletions src/routes/(app)/me/links/[id]/analytics/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,43 +1,13 @@
<script lang="ts">
import ApexCharts from 'apexcharts';
import type { PageData } from './$types';
import { onMount } from 'svelte';
import PerLinkStatisticsOverview from '../PerLinkStatisticsOverview.svelte';
import { Skeleton } from '$lib/components/ui/skeleton';
import LinkVisitsChart from './LinkVisitsChart.svelte';
import LinkReferrersChart from './LinkReferrersChart.svelte';
export let data: PageData;
let chartElement: HTMLDivElement;
import * as Card from '$lib/components/ui/card';
onMount(() => {
data.visitsByDay.then((days) => {
const chart = new ApexCharts(chartElement, {
chart: {
type: 'line',
foreColor: '#ccc',
toolbar: { show: false }
},
tooltip: { theme: 'dark' },
grid: {
borderColor: '#222',
xaxis: { lines: { show: true } }
},
series: [
{
name: 'Visits',
data: days.map((item) => ({
x: item.day,
y: item.count
}))
}
],
stroke: { curve: 'smooth' },
xaxis: { tickAmount: 10 }
});
chart.render();
});
});
export let data: PageData;
</script>

{#await data.stats}
Expand All @@ -46,8 +16,32 @@
<PerLinkStatisticsOverview {stats} />
{/await}

{#await data.visitsByDay}
<Skeleton class=" h-96 w-full" />
{:then}
<div bind:this={chartElement} />
{/await}
<Card.Root>
<Card.Header>
<Card.Title>Visits per day</Card.Title>
<Card.Description>Distribution of visits for each day</Card.Description>
</Card.Header>

<Card.Content>
{#await data.visitsPerDay}
<Skeleton class=" h-96 w-full" />
{:then data}
<LinkVisitsChart {data} />
{/await}
</Card.Content>
</Card.Root>

<Card.Root>
<Card.Header>
<Card.Title>Visits per referrer</Card.Title>
<Card.Description>Distribution of visits between the unique referrers</Card.Description>
</Card.Header>

<Card.Content>
{#await data.visitsPerReferrer}
<Skeleton class=" h-96 w-full" />
{:then data}
<LinkReferrersChart {data} />
{/await}
</Card.Content>
</Card.Root>
1 change: 0 additions & 1 deletion src/routes/(app)/me/links/[id]/analytics/+page.ts

This file was deleted.

Loading

0 comments on commit 3eb547d

Please sign in to comment.