feat(analytics): added in backend anaylitics

This commit is contained in:
2026-05-07 10:20:50 -05:00
parent e9b0101095
commit 9edafc9d28
21 changed files with 4766 additions and 15 deletions

View File

@@ -1,12 +1,18 @@
import type { Express } from "express";
import { requireAuth } from "../middleware/auth.middleware.js";
import { routeHitMiddleware } from "../middleware/routeHit.middleware.js";
import build from "./admin.build.js";
import update from "./admin.updateServer.js";
export const setupAdminRoutes = (baseUrl: string, app: Express) => {
//stats will be like this as we dont need to change this
app.use(`${baseUrl}/api/admin/build`, requireAuth, build);
app.use(`${baseUrl}/api/admin/build`, requireAuth, update);
app.use(`${baseUrl}/api/admin/build`, requireAuth, routeHitMiddleware, build);
app.use(
`${baseUrl}/api/admin/build`,
requireAuth,
routeHitMiddleware,
update,
);
// all other system should be under /api/system/*
};

View File

@@ -1,9 +1,11 @@
import type { Express } from "express";
import { routeHitMiddleware } from "../middleware/routeHit.middleware.js";
import login from "./login.route.js";
import register from "./register.route.js";
export const setupAuthRoutes = (baseUrl: string, app: Express) => {
//setup all the routes
app.use(routeHitMiddleware);
app.use(`${baseUrl}/api/authentication/login`, login);
app.use(`${baseUrl}/api/authentication/register`, register);
};

View File

@@ -1,5 +1,5 @@
import type { Express } from "express";
import { routeHitMiddleware } from "../middleware/routeHit.middleware.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { datamartData } from "./datamartData.utlis.js";
import runQuery from "./getDatamart.route.js";
@@ -30,7 +30,7 @@ export const setupDatamartRoutes = (baseUrl: string, app: Express) => {
// });
//setup all the routes
app.use(routeHitMiddleware);
app.use(`${baseUrl}/api/datamart`, runQuery);
// just sending a get on datamart will return all the queries that we can call.

View File

@@ -1,5 +1,6 @@
import { type Express, Router } from "express";
import { requireAuth } from "../middleware/auth.middleware.js";
import { routeHitMiddleware } from "../middleware/routeHit.middleware.js";
import restart from "./gpSqlRestart.route.js";
import start from "./gpSqlStart.route.js";
import stop from "./gpSqlStop.route.js";
@@ -8,6 +9,7 @@ export const setupGPSqlRoutes = (baseUrl: string, app: Express) => {
// Apply auth to entire router
const router = Router();
router.use(requireAuth);
app.use(routeHitMiddleware);
router.use(start);
router.use(stop);

View File

@@ -0,0 +1,83 @@
// routeHit.middleware.ts
import type { NextFunction, Request, Response } from "express";
import {
createRouteHit,
shouldIgnoreRoute,
} from "../utils/analyticRouteHits.utils.js";
export function routeHitMiddleware(
req: Request,
res: Response,
next: NextFunction,
) {
const start = performance.now();
res.on("finish", () => {
const actualPath = getActualPath(req);
if (shouldIgnoreRoute(actualPath)) {
return;
}
const durationMs = Math.round(performance.now() - start);
const routePattern = getRoutePattern(req) as string;
const module = getModuleName(req);
void createRouteHit({
method: req.method,
routePattern,
actualPath,
statusCode: res.statusCode,
durationMs,
module,
// adjust these names to your Better Auth/session shape
userId: req.user?.id ?? null,
userEmail: req.user?.email ?? null,
ipAddress: req.ip ?? null,
userAgent: req.get("user-agent") ?? null,
}).catch((err) => {
console.error("Failed to save route hit", err);
});
});
next();
}
function getActualPath(req: Request) {
return req.originalUrl.split("?")[0] ?? req.path ?? "unknown";
}
function getRoutePattern(req: Request) {
const baseUrl = req.baseUrl || "";
const routePath = req.route?.path;
if (typeof routePath === "string") {
return `${baseUrl}${routePath}`;
}
return getActualPath(req);
}
function getModuleName(req: Request) {
const path = req.originalUrl.split("?")[0];
if (path?.includes("/printers")) return "printers";
if (path?.includes("/releases")) return "releases";
if (path?.includes("/quality")) return "quality";
if (path?.includes("/scanner")) return "scanner";
if (path?.includes("/settings")) return "settings";
if (path?.includes("/users")) return "users";
if (path?.includes("/mobile")) return "mobile";
if (path?.includes("/servers")) return "servers";
if (path?.includes("/logistics")) return "servers";
if (path?.includes("/ocp")) return "ocp";
if (path?.includes("/auth")) return "auth";
if (path?.includes("/datamart")) return "datamart";
if (path?.includes("/opendock")) return "opendock";
return "unknown";
}

View File

@@ -1,4 +1,5 @@
import type { Express } from "express";
import { routeHitMiddleware } from "../middleware/routeHit.middleware.js";
import downloads from "./donwloadApps.route.js";
import lanes from "./laneCheck.js";
import authPin from "./mobileAuth.route.js";
@@ -8,6 +9,9 @@ import version from "./version.route.js";
export const setupMobileRoutes = (baseUrl: string, app: Express) => {
//stats will be like this as we dont need to change this
app.use(routeHitMiddleware);
app.use(`${baseUrl}/api/mobile/version`, version);
app.use(`${baseUrl}/api/mobile/apk`, downloads);
app.use(`${baseUrl}/api/mobile/logs`, logs);

View File

@@ -1,7 +1,9 @@
import fs from "node:fs";
import { and, eq } from "drizzle-orm";
import { Router } from "express";
import path from "path";
import { db } from "../db/db.controller.js";
import { settings } from "../db/schema/settings.schema.js";
const router = Router();
@@ -10,6 +12,15 @@ const appJsonPath = path.join(projectRoot, "app.json");
router.get("/", async (req, res) => {
const baseUrl = `${req.protocol}://${req.get("host")}`;
const mobileSettings = await db
.select()
.from(settings)
.where(
and(
eq(settings.moduleName, "mobile"),
eq(settings.settingType, "standard"),
),
);
const raw = fs.readFileSync(appJsonPath, "utf-8");
const config = JSON.parse(raw);
@@ -22,6 +33,7 @@ router.get("/", async (req, res) => {
versionCode: exp.android?.versionCode,
minSupportedVersionCode: exp?.android?.minSupportedVersionCode ?? 0,
downloadUrl: `${baseUrl}/lst/api/mobile/apk/latest`,
settings: mobileSettings,
});
});

View File

@@ -1,5 +1,6 @@
import type { Express } from "express";
import { requireAuth } from "../middleware/auth.middleware.js";
import { routeHitMiddleware } from "../middleware/routeHit.middleware.js";
import manual from "./notification.manualTrigger.js";
import getNotifications from "./notification.route.js";
import updateNote from "./notification.update.route.js";
@@ -10,13 +11,48 @@ import updateSub from "./notificationSub.update.route.js";
export const setupNotificationRoutes = (baseUrl: string, app: Express) => {
//stats will be like this as we dont need to change this
app.use(`${baseUrl}/api/notification`, requireAuth, getNotifications);
app.use(`${baseUrl}/api/notification`, requireAuth, updateNote);
app.use(`${baseUrl}/api/notification/manual`, requireAuth, manual);
app.use(`${baseUrl}/api/notification/sub`, requireAuth, subs);
app.use(`${baseUrl}/api/notification/sub`, requireAuth, newSub);
app.use(`${baseUrl}/api/notification/sub`, requireAuth, updateSub);
app.use(`${baseUrl}/api/notification/sub`, requireAuth, deleteSub);
app.use(
`${baseUrl}/api/notification`,
requireAuth,
routeHitMiddleware,
getNotifications,
);
app.use(
`${baseUrl}/api/notification`,
requireAuth,
routeHitMiddleware,
updateNote,
);
app.use(
`${baseUrl}/api/notification/manual`,
requireAuth,
routeHitMiddleware,
manual,
);
app.use(
`${baseUrl}/api/notification/sub`,
requireAuth,
routeHitMiddleware,
subs,
);
app.use(
`${baseUrl}/api/notification/sub`,
requireAuth,
routeHitMiddleware,
newSub,
);
app.use(
`${baseUrl}/api/notification/sub`,
requireAuth,
routeHitMiddleware,
updateSub,
);
app.use(
`${baseUrl}/api/notification/sub`,
requireAuth,
routeHitMiddleware,
deleteSub,
);
// all other system should be under /api/system/*
};

View File

@@ -1,6 +1,7 @@
import { type Express, Router } from "express";
import { requireAuth } from "../middleware/auth.middleware.js";
import { featureCheck } from "../middleware/featureActive.middleware.js";
import { routeHitMiddleware } from "../middleware/routeHit.middleware.js";
import listener from "./ocp.printer.listener.js";
import update from "./ocp.printer.update.js";
@@ -17,6 +18,8 @@ export const setupOCPRoutes = (baseUrl: string, app: Express) => {
// auth routes below here
router.use(requireAuth);
app.use(routeHitMiddleware);
router.use(update);
//router.use("");

View File

@@ -1,6 +1,7 @@
import { type Express, Router } from "express";
import { requireAuth } from "../middleware/auth.middleware.js";
import { featureCheck } from "../middleware/featureActive.middleware.js";
import { routeHitMiddleware } from "../middleware/routeHit.middleware.js";
import getApt from "./opendockGetRelease.route.js";
export const setupOpendockRoutes = (baseUrl: string, app: Express) => {
@@ -13,6 +14,7 @@ export const setupOpendockRoutes = (baseUrl: string, app: Express) => {
// we need to make sure we are authenticated to see the releases
router.use(requireAuth);
app.use(routeHitMiddleware);
router.use(getApt);
app.use(`${baseUrl}/api/opendock`, router);

View File

@@ -1,5 +1,6 @@
import { type Express, Router } from "express";
import { requireAuth } from "../middleware/auth.middleware.js";
import { routeHitMiddleware } from "../middleware/routeHit.middleware.js";
import restart from "./prodSqlRestart.route.js";
import start from "./prodSqlStart.route.js";
import stop from "./prodSqlStop.route.js";
@@ -8,6 +9,7 @@ export const setupProdSqlRoutes = (baseUrl: string, app: Express) => {
// Apply auth to entire router
const router = Router();
router.use(requireAuth);
app.use(routeHitMiddleware);
router.use(start);
router.use(stop);

View File

@@ -76,6 +76,16 @@ const newSettings: NewSetting[] = [
roles: ["admin"],
seedVersion: 1,
},
{
name: "mobile",
value: "0",
active: false,
description: "LST Android Mobile app",
moduleName: "mobile",
settingType: "feature",
roles: ["admin"],
seedVersion: 1,
},
// standard settings
{
@@ -304,6 +314,38 @@ const newSettings: NewSetting[] = [
roles: ["admin"],
seedVersion: 1,
},
{
name: "laneCheck",
value: "0",
active: false,
description:
"Allows the driver to scan a lane and see what is in the lane and details about each pallet.",
moduleName: "mobile",
settingType: "standard",
roles: ["admin"],
seedVersion: 1,
},
{
name: "dockScan",
value: "0",
active: false,
description:
"Enables dock door scanning, must have a dock scanner setup for this to work.",
moduleName: "mobile",
settingType: "standard",
roles: ["admin"],
seedVersion: 1,
},
{
name: "cycleCounting",
value: "0",
active: false,
description: "Enables a cycle count to be triggered from the scanner.",
moduleName: "mobile",
settingType: "standard",
roles: ["admin"],
seedVersion: 1,
},
];
export const baseSettingValidationCheck = async () => {

View File

@@ -1,5 +1,6 @@
import type { Express } from "express";
import { requireAuth } from "../middleware/auth.middleware.js";
import { routeHitMiddleware } from "../middleware/routeHit.middleware.js";
import getServers from "./serverData.route.js";
import getSettings from "./settings.route.js";
import updSetting from "./settingsUpdate.route.js";
@@ -7,6 +8,7 @@ import stats from "./stats.route.js";
export const setupSystemRoutes = (baseUrl: string, app: Express) => {
//stats will be like this as we dont need to change this
app.use(routeHitMiddleware);
app.use(`${baseUrl}/api/stats`, stats);
app.use(`${baseUrl}/api/settings`, getSettings);
app.use(`${baseUrl}/api/servers`, getServers);

View File

@@ -1,14 +1,21 @@
import type { Express } from "express";
import { requireAuth } from "../middleware/auth.middleware.js";
import { routeHitMiddleware } from "../middleware/routeHit.middleware.js";
import restart from "./tcpRestart.route.js";
import start from "./tcpStart.route.js";
import stop from "./tcpStop.route.js";
export const setupTCPRoutes = (baseUrl: string, app: Express) => {
//stats will be like this as we dont need to change this
app.use(`${baseUrl}/api/tcp/start`, requireAuth, start);
app.use(`${baseUrl}/api/tcp/stop`, requireAuth, stop);
app.use(`${baseUrl}/api/tcp/restart`, requireAuth, restart);
app.use(`${baseUrl}/api/tcp/start`, requireAuth, routeHitMiddleware, start);
app.use(`${baseUrl}/api/tcp/stop`, requireAuth, routeHitMiddleware, stop);
app.use(
`${baseUrl}/api/tcp/restart`,
requireAuth,
routeHitMiddleware,
restart,
);
// all other system should be under /api/system/*
};

View File

@@ -0,0 +1,31 @@
import { db } from "../db/db.controller.js";
import { analytics } from "../db/schema/analytics.schema.js";
export const ignoredRoutePrefixes = [
"/health",
"/favicon.ico",
"/socket.io",
"/lst/api/ws",
"/lst-config.js",
];
export function shouldIgnoreRoute(path: string) {
return ignoredRoutePrefixes.some((prefix) => path.startsWith(prefix));
}
type CreateRouteHitInput = {
method: string;
routePattern: string;
actualPath: string;
statusCode: number;
durationMs: number;
module?: string | null;
userId?: string | null;
userEmail?: string | null;
ipAddress?: string | null;
userAgent?: string | null;
};
export async function createRouteHit(input: CreateRouteHitInput) {
await db.insert(analytics).values(input);
}

View File

@@ -1,7 +1,9 @@
import type { Express } from "express";
import { routeHitMiddleware } from "../middleware/routeHit.middleware.js";
import getActiveJobs from "./cronerActiveJobs.route.js";
import jobStatusChange from "./cronerStatusChange.route.js";
export const setupUtilsRoutes = (baseUrl: string, app: Express) => {
app.use(routeHitMiddleware);
app.use(`${baseUrl}/api/utils/croner`, getActiveJobs);
app.use(`${baseUrl}/api/utils/croner`, jobStatusChange);
};