refactor(analyitics): finished analyitics as a base
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import { and, count, countDistinct, gte, lt, sql } from "drizzle-orm";
|
||||
import { db } from "../db/db.controller.js";
|
||||
import { analytics } from "../db/schema/analytics.schema.js";
|
||||
import { analyticsDaily } from "../db/schema/dailyAnalytics.schema.js";
|
||||
|
||||
export const ignoredRoutePrefixes = [
|
||||
"/health",
|
||||
@@ -29,3 +31,111 @@ type CreateRouteHitInput = {
|
||||
export async function createRouteHit(input: CreateRouteHitInput) {
|
||||
await db.insert(analytics).values(input);
|
||||
}
|
||||
|
||||
function getPreviousBusinessDayWindow(date = new Date()) {
|
||||
const end = new Date(date);
|
||||
end.setHours(7, 0, 0, 0);
|
||||
|
||||
const start = new Date(end);
|
||||
start.setDate(start.getDate() - 1);
|
||||
|
||||
const businessDate = start.toISOString().slice(0, 10);
|
||||
|
||||
return {
|
||||
start,
|
||||
end,
|
||||
businessDate,
|
||||
};
|
||||
}
|
||||
|
||||
export async function runRouteHitAnalyticsCron(): Promise<void> {
|
||||
const result = await aggregateRouteHitsForBusinessDay();
|
||||
|
||||
await cleanupOldRouteHits();
|
||||
|
||||
console.log("Route hit analytics aggregated", result);
|
||||
}
|
||||
|
||||
export async function aggregateRouteHitsForBusinessDay() {
|
||||
const { start, end, businessDate } = getPreviousBusinessDayWindow();
|
||||
|
||||
const rows = await db
|
||||
.select({
|
||||
businessDate: sql<string>`${businessDate}`,
|
||||
method: analytics.method,
|
||||
routePattern: analytics.routePattern,
|
||||
module: sql<string>`COALESCE(${analytics.module}, 'unknown')`,
|
||||
|
||||
totalHits: count(),
|
||||
uniqueUsers: countDistinct(analytics.userId),
|
||||
|
||||
successCount: sql<number>`
|
||||
COUNT(*) FILTER (WHERE ${analytics.statusCode} < 400)
|
||||
`,
|
||||
errorCount: sql<number>`
|
||||
COUNT(*) FILTER (WHERE ${analytics.statusCode} >= 400)
|
||||
`,
|
||||
|
||||
avgDurationMs: sql<number>`
|
||||
COALESCE(ROUND(AVG(${analytics.durationMs})), 0)
|
||||
`,
|
||||
maxDurationMs: sql<number>`
|
||||
COALESCE(MAX(${analytics.durationMs}), 0)
|
||||
`,
|
||||
|
||||
firstHitAt: sql<Date>`
|
||||
COALESCE(MIN(${analytics.createdAt}), NOW())
|
||||
`,
|
||||
lastHitAt: sql<Date>`
|
||||
COALESCE(MAX(${analytics.createdAt}), NOW())
|
||||
`,
|
||||
})
|
||||
.from(analytics)
|
||||
.where(and(gte(analytics.createdAt, start), lt(analytics.createdAt, end)))
|
||||
.groupBy(
|
||||
analytics.method,
|
||||
analytics.routePattern,
|
||||
sql`COALESCE(${analytics.module}, 'unknown')`,
|
||||
);
|
||||
|
||||
if (rows.length === 0) {
|
||||
return {
|
||||
businessDate,
|
||||
inserted: 0,
|
||||
};
|
||||
}
|
||||
|
||||
await db
|
||||
.insert(analyticsDaily)
|
||||
.values(rows)
|
||||
.onConflictDoUpdate({
|
||||
target: [
|
||||
analyticsDaily.businessDate,
|
||||
analyticsDaily.method,
|
||||
analyticsDaily.routePattern,
|
||||
analyticsDaily.module,
|
||||
],
|
||||
set: {
|
||||
totalHits: sql`excluded.total_hits`,
|
||||
uniqueUsers: sql`excluded.unique_users`,
|
||||
successCount: sql`excluded.success_count`,
|
||||
errorCount: sql`excluded.error_count`,
|
||||
avgDurationMs: sql`excluded.avg_duration_ms`,
|
||||
maxDurationMs: sql`excluded.max_duration_ms`,
|
||||
firstHitAt: sql`excluded.first_hit_at`,
|
||||
lastHitAt: sql`excluded.last_hit_at`,
|
||||
updatedAt: sql`now()`,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
businessDate,
|
||||
inserted: rows.length,
|
||||
};
|
||||
}
|
||||
|
||||
export async function cleanupOldRouteHits() {
|
||||
await db
|
||||
.delete(analytics)
|
||||
.where(lt(analytics.createdAt, sql`now() - interval '4 days'`));
|
||||
}
|
||||
|
||||
61
backend/utils/umami.utils.ts
Normal file
61
backend/utils/umami.utils.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { isUmamiEnabled, umamiConfig } from "../configs/umami.config.js";
|
||||
|
||||
type TrackLstEventInput = {
|
||||
eventName: string;
|
||||
eventData?: Record<string, unknown>;
|
||||
url?: string;
|
||||
hostname?: string;
|
||||
};
|
||||
|
||||
export async function trackLstEvent({
|
||||
eventName,
|
||||
eventData,
|
||||
url = "/backend",
|
||||
hostname = umamiConfig.server,
|
||||
}: TrackLstEventInput): Promise<void> {
|
||||
if (!isUmamiEnabled()) return;
|
||||
|
||||
try {
|
||||
await fetch(`${umamiConfig.umamiHost}/api/send`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "LST-Backend",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
type: "event",
|
||||
payload: {
|
||||
website: umamiConfig.umamiWebsiteId,
|
||||
name: eventName,
|
||||
url,
|
||||
hostname,
|
||||
language: "en-US",
|
||||
screen: "backend",
|
||||
data: {
|
||||
app: umamiConfig.appName,
|
||||
site: umamiConfig.site,
|
||||
server: umamiConfig.server,
|
||||
appVersion: umamiConfig.appVersion,
|
||||
source: "backend",
|
||||
...eventData,
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Failed to send Umami backend event", err);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
await trackLstEvent({
|
||||
eventName: "label_print_completed",
|
||||
url: "/backend/printers",
|
||||
eventData: {
|
||||
module: "printers",
|
||||
printerName,
|
||||
labelType,
|
||||
},
|
||||
});
|
||||
|
||||
*/
|
||||
Reference in New Issue
Block a user