Only send bounce event every 20 emails

This commit is contained in:
Dries Augustyns
2025-12-04 19:44:20 +01:00
parent af54ab4c4e
commit 2b65940e73
+67 -7
View File
@@ -276,6 +276,7 @@ export class NtfyService {
/**
* Notify about email bounce - LOW priority (high volume)
* Rate-limited to only send notification every 20 bounces with per-project breakdown
*/
public static async notifyEmailBounce(
projectName: string,
@@ -283,13 +284,72 @@ export class NtfyService {
recipientEmail: string,
bounceType?: string,
): Promise<void> {
const bounceInfo = bounceType ? ` (${bounceType})` : '';
await this.send({
title: 'Email Bounced',
message: `Email to ${recipientEmail} bounced${bounceInfo} in project "${projectName}" (${projectId})`,
priority: NtfyPriority.LOW,
tags: [NtfyTag.WARNING],
});
// Import redis at runtime to avoid circular dependencies
const {redis} = await import('../database/redis.js');
// Use Redis counters to track bounce count globally and per-project
const globalCountKey = `ntfy:bounce:count`;
const projectCountKey = `ntfy:bounce:count:${projectId}`;
const projectListKey = `ntfy:bounce:projects`;
// Increment global counter
const globalCount = await redis.incr(globalCountKey);
// Increment project-specific counter
await redis.incr(projectCountKey);
// Add project to the set of projects with bounces (for tracking)
await redis.sadd(projectListKey, projectId);
// Set expiry on first increment (24 hour rolling window)
if (globalCount === 1) {
await redis.expire(globalCountKey, 86400);
await redis.expire(projectListKey, 86400);
}
// Always refresh project counter expiry to match global window
await redis.expire(projectCountKey, 86400);
// Only send notification every 20 bounces
if (globalCount % 20 === 0) {
// Get all projects with bounces
const projectIds = await redis.smembers(projectListKey);
// Get bounce counts for each project
const projectCounts: Array<{projectId: string; count: number; name: string}> = [];
for (const pid of projectIds) {
const count = await redis.get(`ntfy:bounce:count:${pid}`);
if (count) {
// Fetch project name from database
const {prisma} = await import('../database/prisma.js');
const project = await prisma.project.findUnique({
where: {id: pid},
select: {name: true},
});
projectCounts.push({
projectId: pid,
count: parseInt(count, 10),
name: project?.name || 'Unknown',
});
}
}
// Sort by count descending
projectCounts.sort((a, b) => b.count - a.count);
// Build breakdown message
const breakdown = projectCounts
.map((p) => `${p.name} (${p.projectId}): ${p.count}`)
.join('\n');
const bounceInfo = bounceType ? ` (${bounceType})` : '';
await this.send({
title: 'Email Bounces',
message: `20 email bounces detected (total: ${globalCount})\n\nBreakdown by project:\n${breakdown}\n\nLatest: ${recipientEmail}${bounceInfo} in "${projectName}"`,
priority: NtfyPriority.LOW,
tags: [NtfyTag.WARNING],
});
}
}
/**