From fe199f7f92f20d3d20b0f4936137a8470fb705ba Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Fri, 14 Aug 2026 11:59:46 +0100 Subject: [PATCH] perf(webapp): aggregate admin notification interaction counts in the database (#4616) ## Summary The notifications admin list loaded every interaction row for the notifications on the current page just to show three per-notification counters (seen, clicked, dismissed), then counted them in memory. On notifications with many interactions this made the page slow to load and heavy on memory, even though only 20 notifications are shown. ## Fix Compute the counters in a single grouped aggregate in the database instead, returning one row per notification rather than one row per interaction: ```sql SELECT "notificationId", COUNT(*) AS seen, COUNT(*) FILTER (WHERE "webappClickedAt" IS NOT NULL) AS clicked, COUNT(*) FILTER (WHERE "webappDismissedAt" IS NOT NULL OR "cliDismissedAt" IS NOT NULL) AS dismissed FROM "PlatformNotificationInteraction" WHERE "notificationId" IN (...) GROUP BY "notificationId" ``` Behavior is unchanged; notifications with no interactions report zero. --- .../services/platformNotifications.server.ts | 50 ++++++++++++------- 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/apps/webapp/app/services/platformNotifications.server.ts b/apps/webapp/app/services/platformNotifications.server.ts index 24e95740a..0b39fd915 100644 --- a/apps/webapp/app/services/platformNotifications.server.ts +++ b/apps/webapp/app/services/platformNotifications.server.ts @@ -1,6 +1,6 @@ import type { z } from "zod"; import { errAsync, fromPromise, type ResultAsync } from "neverthrow"; -import { prisma } from "~/db.server"; +import { Prisma, prisma, sqlDatabaseSchema } from "~/db.server"; import { type PlatformNotificationScope, type PlatformNotificationSurface, @@ -49,25 +49,41 @@ export async function getAdminNotificationsList({ orderBy: [{ createdAt: "desc" }], skip: (page - 1) * pageSize, take: pageSize, - include: { - _count: { - select: { interactions: true }, - }, - interactions: { - select: { - webappDismissedAt: true, - webappClickedAt: true, - cliDismissedAt: true, - }, - }, - }, }), prisma.platformNotification.count({ where }), ]); + const notificationIds = notifications.map((n) => n.id); + + const interactionStats = + notificationIds.length > 0 + ? await prisma.$queryRaw< + { + notificationId: string; + seen: bigint; + clicked: bigint; + dismissed: bigint; + }[] + >` + SELECT + "notificationId", + COUNT(*) AS seen, + COUNT(*) FILTER (WHERE "webappClickedAt" IS NOT NULL) AS clicked, + COUNT(*) FILTER ( + WHERE "webappDismissedAt" IS NOT NULL OR "cliDismissedAt" IS NOT NULL + ) AS dismissed + FROM ${sqlDatabaseSchema}."PlatformNotificationInteraction" + WHERE "notificationId" IN (${Prisma.join(notificationIds)}) + GROUP BY "notificationId" + ` + : []; + + const statsById = new Map(interactionStats.map((row) => [row.notificationId, row])); + return { notifications: notifications.map((n) => { const parsed = PayloadV1Schema.safeParse(n.payload); + const stats = statsById.get(n.id); return { id: n.id, friendlyId: n.friendlyId, @@ -99,11 +115,9 @@ export async function getAdminNotificationsList({ cliMaxDaysAfterFirstSeen: n.cliMaxDaysAfterFirstSeen, cliShowEvery: n.cliShowEvery, stats: { - seen: n._count.interactions, - clicked: n.interactions.filter((i) => i.webappClickedAt !== null).length, - dismissed: n.interactions.filter( - (i) => i.webappDismissedAt !== null || i.cliDismissedAt !== null - ).length, + seen: stats ? Number(stats.seen) : 0, + clicked: stats ? Number(stats.clicked) : 0, + dismissed: stats ? Number(stats.dismissed) : 0, }, }; }),