Skip to content

Commit

Permalink
Merge pull request #46 from Prodeko/analytics-timeseries
Browse files Browse the repository at this point in the history
Analytics timeseries
  • Loading branch information
nlinnanen authored May 6, 2024
2 parents f22d7c5 + c25ad8d commit 7c7f585
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 50 deletions.
36 changes: 36 additions & 0 deletions src/analytics/timeseries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import _ from "lodash";
import { prisma } from "../../config";
import { GUILDS } from "../common/constants";
import type { TimeSeriesData } from "../common/types";

export const getTimeSeriesData = async (): Promise<TimeSeriesData> => {
const data = await prisma.$queryRaw`
WITH "PointsByDate" AS (
SELECT
DATE_TRUNC('day', "Entry"."createdAt") as "date",
"guild",
SUM("earnedPoints") as "totalPoints"
FROM
"Entry" JOIN "User" ON "Entry"."userId" = "User"."telegramUserId"
GROUP BY
"date",
"guild"
)
SELECT
"date",
"guild",
SUM("totalPoints") OVER (ORDER BY "date") as "totalPoints"
FROM
"PointsByDate"
ORDER BY
"date" ASC
` as {
date: Date;
guild: string;
totalPoints: number;
}[];

return data;
}
8 changes: 8 additions & 0 deletions src/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,11 @@ export type Statistics = Map<Guild, TeamStatistics>;
export type pointsPerGuild = Map<Guild, number>;

export type PrivacyState = "accepted" | "rejected";

export type TimeSeriesData = [
{
date: Date;
guild: Guild;
totalPoints: number;
}
]
124 changes: 74 additions & 50 deletions src/server/analytics.ts
Original file line number Diff line number Diff line change
@@ -1,66 +1,90 @@
import express from 'express';
import { validatePeriod } from './middleware';
import { calculateGuildStatistics } from '../analytics/statistics';
import topUsersByGuild from '../analytics/rankings';
import { arrayToCSV } from '../common/utils';
import express from "express";
import { validatePeriod } from "./middleware";
import { calculateGuildStatistics } from "../analytics/statistics";
import topUsersByGuild from "../analytics/rankings";
import { arrayToCSV } from "../common/utils";
import { getTimeSeriesData } from "../analytics/timeseries";
import entry from "../commands/entry";
import { GUILDS } from "../common/constants";

const router = express.Router({mergeParams: true});
const router = express.Router({ mergeParams: true });

export interface StatisticsResponse extends express.Response {
locals: {
guild: string;
periodStart: Date;
periodEnd: Date;
};
locals: {
guild: string;
periodStart: Date;
periodEnd: Date;
};
}

router.get('/ranking/:guild', async (req, res: StatisticsResponse) => {
if (req.query.pass !== process.env.ADMIN_PASSWORD) {
console.log("Wrong password");
return res.status(401).send("Wrong password!");
}

const guild = req.params.guild;
if (!req.query.limit || ! ((typeof req.query.limit) === 'string')) {
return res.status(400).send('Limit query parameter is required as a string');
}
const limit = Number.parseInt(req.query.limit as string ?? '10');
try {
const topUsers = await topUsersByGuild(guild, limit) as Record<string, unknown>[];
const csv = arrayToCSV([
"userId",
"telegramUsername",
"firstName",
"lastName",
"totalPoints",
"totalEntries"
], topUsers);
res.header("Content-Type", "text/csv");
res.status(200).send(csv);
} catch (error) {
console.error(error);
res.status(500).send('An error occurred while calculating rankings');
}
router.get("/ranking/:guild", async (req, res: StatisticsResponse) => {
if (req.query.pass !== process.env.ADMIN_PASSWORD) {
console.log("Wrong password");
return res.status(401).send("Wrong password!");
}

const guild = req.params.guild;
if (!req.query.limit || !(typeof req.query.limit === "string")) {
return res
.status(400)
.send("Limit query parameter is required as a string");
}
const limit = Number.parseInt((req.query.limit as string) ?? "10");
try {
const topUsers = (await topUsersByGuild(guild, limit)) as Record<
string,
unknown
>[];
const csv = arrayToCSV(
[
"userId",
"telegramUsername",
"firstName",
"lastName",
"totalPoints",
"totalEntries",
],
topUsers,
);
res.header("Content-Type", "text/csv");
res.status(200).send(csv);
} catch (error) {
console.error(error);
res.status(500).send("An error occurred while calculating rankings");
}
});

router.get("/time-series", async (req, res) => {
if (req.query.pass !== process.env.ADMIN_PASSWORD) {
console.log("Wrong password");
return res.status(401).send("Wrong password!");
}

const timeSeries = await getTimeSeriesData();

const csv = arrayToCSV(["date", "guild", "totalPoints"], timeSeries);

res.header("Content-Type", "text/csv");
res.status(200).send(csv);
});

// Middleware to validate the period start and end query parameters
router.use(validatePeriod);

router.get('/statistics', async (req, res: StatisticsResponse) => {
if (req.query.pass !== process.env.ADMIN_PASSWORD) {
router.get("/statistics", async (req, res: StatisticsResponse) => {
if (req.query.pass !== process.env.ADMIN_PASSWORD) {
console.log("Wrong password");
return res.status(401).send("Wrong password!");
}

const { periodStart, periodEnd } = res.locals;
try {
const statistics = await calculateGuildStatistics(periodStart, periodEnd);
res.json(Object.fromEntries(statistics));
} catch (error) {
console.error(error);
res.status(500).send('An error occurred while calculating statistics');
}
});

const { periodStart, periodEnd } = res.locals;
try {
const statistics = await calculateGuildStatistics(periodStart, periodEnd);
res.json(Object.fromEntries(statistics));
} catch (error) {
console.error(error);
res.status(500).send("An error occurred while calculating statistics");
}
});

export default router;

0 comments on commit 7c7f585

Please sign in to comment.