Only send bounce event every 20 emails
This commit is contained in:
@@ -276,6 +276,7 @@ export class NtfyService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Notify about email bounce - LOW priority (high volume)
|
* 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(
|
public static async notifyEmailBounce(
|
||||||
projectName: string,
|
projectName: string,
|
||||||
@@ -283,13 +284,72 @@ export class NtfyService {
|
|||||||
recipientEmail: string,
|
recipientEmail: string,
|
||||||
bounceType?: string,
|
bounceType?: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const bounceInfo = bounceType ? ` (${bounceType})` : '';
|
// Import redis at runtime to avoid circular dependencies
|
||||||
await this.send({
|
const {redis} = await import('../database/redis.js');
|
||||||
title: 'Email Bounced',
|
|
||||||
message: `Email to ${recipientEmail} bounced${bounceInfo} in project "${projectName}" (${projectId})`,
|
// Use Redis counters to track bounce count globally and per-project
|
||||||
priority: NtfyPriority.LOW,
|
const globalCountKey = `ntfy:bounce:count`;
|
||||||
tags: [NtfyTag.WARNING],
|
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],
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user