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.
This commit is contained in:
Eric Allam
2026-08-14 11:59:46 +01:00
committed by GitHub
parent c4b5e27258
commit fe199f7f92
@@ -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,
},
};
}),