Refine suppression list to include only affected recipients - #339

Merged
KMKoushik merged 1 commit into
usesend:mainfrom
infordoc:main
Jan 17, 2026
Merged

Refine suppression list to include only affected recipients#339
KMKoushik merged 1 commit into
usesend:mainfrom
infordoc:main

Conversation

@tpraxedes

@tpraxedestpraxedes commented Jan 12, 2026

Copy link
Copy Markdown
Contributor

Refactor suppression list logic to only include recipients that actually bounced or complained, improving accuracy in handling email suppression.


Summary by cubic

Limit suppression list updates to only recipients that actually bounced or complained, based on SES event data. Prevents suppressing unaffected recipients and improves accuracy.

  • Bug Fixes
    • Use bounce.bouncedRecipients and complaint.complainedRecipients instead of all message recipients.
    • Apply suppression only for permanent (hard) bounces and complaints.
    • Skip when no affected recipients are found and log a warning; improve error logging without failing webhook processing.

Written for commit d21f7ca. Summary will update on new commits.

Summary by CodeRabbit

  • Bug Fixes
    • Improved email suppression handling to only suppress actually affected recipients instead of all email recipients.
    • Enhanced error handling in suppression logic to prevent failures from interrupting the process.
    • Added more targeted logging for bounce and complaint events.

✏️ Tip: You can customize this high-level summary in your review settings.

Refactor suppression list logic to only include recipients that actually bounced or complained, improving accuracy in handling email suppression.
@vercel

vercelBot commented Jan 12, 2026

Copy link
Copy Markdown

@tpraxedes is attempting to deploy a commit to the kmkoushik's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitaiBot commented Jan 12, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

Modified the SES webhook parser to populate suppression lists using only the actually affected recipients from SES event data instead of all recipients from email headers. For hard bounces, the parser now extracts recipients from the bounce data; for complaints, from the complaint data. Added a guard condition to proceed only when affected recipients exist, wrapped suppression calls in error handling, and refined logging to distinguish between bounce and complaint scenarios while maintaining the existing suppression reason selection logic.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 0.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title accurately describes the main change: refining suppression list logic to include only affected recipients from SES event data instead of all email recipients.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
apps/web/src/server/service/ses-hook-parser.ts (1)

139-172: Consider using Promise.allSettled for better partial failure handling.

With Promise.all, if one suppression fails, the entire batch rejects and you lose visibility into which recipients succeeded vs failed. Using Promise.allSettled would allow all suppressions to be attempted and provide per-recipient success/failure status in logs.

Given that addSuppression uses upsert and webhooks can be retried, the current approach is acceptable, but Promise.allSettled would improve observability.

♻️ Optional improvement with Promise.allSettled
- try {- await Promise.all(- recipientEmails.map((recipientEmail) =>- SuppressionService.addSuppression({- email: recipientEmail,- teamId: email.teamId,- reason: isHardBounced- ? SuppressionReason.HARD_BOUNCE- : SuppressionReason.COMPLAINT,- source: email.id,- }),- ),- );-- logger.info(- {- emailId: email.id,- recipients: recipientEmails,- reason: isHardBounced ? "HARD_BOUNCE" : "COMPLAINT",- },- "Added emails to suppression list due to bounce/complaint",- );- } catch (error) {- logger.error(- {- emailId: email.id,- recipients: recipientEmails,- error: error instanceof Error ? error.message : "Unknown error",- },- "Failed to add emails to suppression list",- );- // Don't throw error - continue processing the webhook- }+ const results = await Promise.allSettled(+ recipientEmails.map((recipientEmail) =>+ SuppressionService.addSuppression({+ email: recipientEmail,+ teamId: email.teamId,+ reason: isHardBounced+ ? SuppressionReason.HARD_BOUNCE+ : SuppressionReason.COMPLAINT,+ source: email.id,+ }),+ ),+ );++ const succeeded = recipientEmails.filter((_, i) => results[i].status === "fulfilled");+ const failed = recipientEmails.filter((_, i) => results[i].status === "rejected");++ if (succeeded.length > 0) {+ logger.info(+ {+ emailId: email.id,+ recipients: succeeded,+ reason: isHardBounced ? "HARD_BOUNCE" : "COMPLAINT",+ },+ "Added emails to suppression list due to bounce/complaint",+ );+ }++ if (failed.length > 0) {+ logger.error(+ {+ emailId: email.id,+ failedRecipients: failed,+ },+ "Failed to add some emails to suppression list",+ );+ }
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 68d951c and d21f7ca.

📒 Files selected for processing (1)
  • apps/web/src/server/service/ses-hook-parser.ts
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{tsx,ts,jsx,js}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

Include all required imports and ensure proper naming of key components in React/NextJS code

Files:

  • apps/web/src/server/service/ses-hook-parser.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Use TypeScript-first approach with 2-space indent and semicolons enabled by Prettier in apps/web (Next.js), apps/marketing, apps/smtp-server, and all packages
Never use dynamic imports; always import on the top level
Run ESLint via @usesend/eslint-config and ensure no warnings remain before submitting PRs

Files:

  • apps/web/src/server/service/ses-hook-parser.ts
apps/web/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use alias ~/ for src imports in apps/web (e.g., import { x } from "~/utils/x")

Files:

  • apps/web/src/server/service/ses-hook-parser.ts
apps/web/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/**/*.{ts,tsx}: Prefer to use TRPC for client-server communication unless explicitly asked otherwise in apps/web
Use Prisma for database access in apps/web

Files:

  • apps/web/src/server/service/ses-hook-parser.ts
**/*.{ts,tsx,md}

📄 CodeRabbit inference engine (AGENTS.md)

Run Prettier 3 for code formatting on TypeScript, TSX, and Markdown files

Files:

  • apps/web/src/server/service/ses-hook-parser.ts
🧬 Code graph analysis (1)
apps/web/src/server/service/ses-hook-parser.ts (2)
apps/web/src/server/logger/log.ts (1)
  • logger (31-63)
apps/web/src/server/service/suppression-service.ts (1)
  • SuppressionService (28-393)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: cubic · AI code reviewer
🔇 Additional comments (2)
apps/web/src/server/service/ses-hook-parser.ts (2)

123-136: LGTM - Recipient extraction logic is well-structured.

The conditional extraction using optional chaining ensures null safety, and the logic correctly distinguishes between bounce recipients and complaint recipients.


173-181: Good defensive logging for edge cases.

This warning appropriately catches scenarios where a bounce or complaint event arrives without the expected recipient data, which could indicate malformed SES payloads or unexpected event structures.

@cubic-dev-aicubic-dev-aiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 1 file

@KMKoushikKMKoushik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great catch

@KMKoushik
KMKoushik merged commit 83119f9 into usesend:mainJan 17, 2026
2 of 3 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@tpraxedes@KMKoushik
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Refine suppression list to include only affected recipients - #339

Merged
KMKoushik merged 1 commit into
usesend:mainfrom
infordoc:main
Jan 17, 2026
Merged

Refine suppression list to include only affected recipients#339
KMKoushik merged 1 commit into
usesend:mainfrom
infordoc:main

Conversation

@tpraxedes

@tpraxedestpraxedes commented Jan 12, 2026

Copy link
Copy Markdown
Contributor

Refactor suppression list logic to only include recipients that actually bounced or complained, improving accuracy in handling email suppression.


Summary by cubic

Limit suppression list updates to only recipients that actually bounced or complained, based on SES event data. Prevents suppressing unaffected recipients and improves accuracy.

  • Bug Fixes
    • Use bounce.bouncedRecipients and complaint.complainedRecipients instead of all message recipients.
    • Apply suppression only for permanent (hard) bounces and complaints.
    • Skip when no affected recipients are found and log a warning; improve error logging without failing webhook processing.

Written for commit d21f7ca. Summary will update on new commits.

Summary by CodeRabbit

  • Bug Fixes
    • Improved email suppression handling to only suppress actually affected recipients instead of all email recipients.
    • Enhanced error handling in suppression logic to prevent failures from interrupting the process.
    • Added more targeted logging for bounce and complaint events.

✏️ Tip: You can customize this high-level summary in your review settings.

Refactor suppression list logic to only include recipients that actually bounced or complained, improving accuracy in handling email suppression.
@vercel

vercelBot commented Jan 12, 2026

Copy link
Copy Markdown

@tpraxedes is attempting to deploy a commit to the kmkoushik's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitaiBot commented Jan 12, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

Modified the SES webhook parser to populate suppression lists using only the actually affected recipients from SES event data instead of all recipients from email headers. For hard bounces, the parser now extracts recipients from the bounce data; for complaints, from the complaint data. Added a guard condition to proceed only when affected recipients exist, wrapped suppression calls in error handling, and refined logging to distinguish between bounce and complaint scenarios while maintaining the existing suppression reason selection logic.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 0.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title accurately describes the main change: refining suppression list logic to include only affected recipients from SES event data instead of all email recipients.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
apps/web/src/server/service/ses-hook-parser.ts (1)

139-172: Consider using Promise.allSettled for better partial failure handling.

With Promise.all, if one suppression fails, the entire batch rejects and you lose visibility into which recipients succeeded vs failed. Using Promise.allSettled would allow all suppressions to be attempted and provide per-recipient success/failure status in logs.

Given that addSuppression uses upsert and webhooks can be retried, the current approach is acceptable, but Promise.allSettled would improve observability.

♻️ Optional improvement with Promise.allSettled
- try {- await Promise.all(- recipientEmails.map((recipientEmail) =>- SuppressionService.addSuppression({- email: recipientEmail,- teamId: email.teamId,- reason: isHardBounced- ? SuppressionReason.HARD_BOUNCE- : SuppressionReason.COMPLAINT,- source: email.id,- }),- ),- );-- logger.info(- {- emailId: email.id,- recipients: recipientEmails,- reason: isHardBounced ? "HARD_BOUNCE" : "COMPLAINT",- },- "Added emails to suppression list due to bounce/complaint",- );- } catch (error) {- logger.error(- {- emailId: email.id,- recipients: recipientEmails,- error: error instanceof Error ? error.message : "Unknown error",- },- "Failed to add emails to suppression list",- );- // Don't throw error - continue processing the webhook- }+ const results = await Promise.allSettled(+ recipientEmails.map((recipientEmail) =>+ SuppressionService.addSuppression({+ email: recipientEmail,+ teamId: email.teamId,+ reason: isHardBounced+ ? SuppressionReason.HARD_BOUNCE+ : SuppressionReason.COMPLAINT,+ source: email.id,+ }),+ ),+ );++ const succeeded = recipientEmails.filter((_, i) => results[i].status === "fulfilled");+ const failed = recipientEmails.filter((_, i) => results[i].status === "rejected");++ if (succeeded.length > 0) {+ logger.info(+ {+ emailId: email.id,+ recipients: succeeded,+ reason: isHardBounced ? "HARD_BOUNCE" : "COMPLAINT",+ },+ "Added emails to suppression list due to bounce/complaint",+ );+ }++ if (failed.length > 0) {+ logger.error(+ {+ emailId: email.id,+ failedRecipients: failed,+ },+ "Failed to add some emails to suppression list",+ );+ }
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 68d951c and d21f7ca.

📒 Files selected for processing (1)
  • apps/web/src/server/service/ses-hook-parser.ts
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{tsx,ts,jsx,js}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

Include all required imports and ensure proper naming of key components in React/NextJS code

Files:

  • apps/web/src/server/service/ses-hook-parser.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Use TypeScript-first approach with 2-space indent and semicolons enabled by Prettier in apps/web (Next.js), apps/marketing, apps/smtp-server, and all packages
Never use dynamic imports; always import on the top level
Run ESLint via @usesend/eslint-config and ensure no warnings remain before submitting PRs

Files:

  • apps/web/src/server/service/ses-hook-parser.ts
apps/web/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use alias ~/ for src imports in apps/web (e.g., import { x } from "~/utils/x")

Files:

  • apps/web/src/server/service/ses-hook-parser.ts
apps/web/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/**/*.{ts,tsx}: Prefer to use TRPC for client-server communication unless explicitly asked otherwise in apps/web
Use Prisma for database access in apps/web

Files:

  • apps/web/src/server/service/ses-hook-parser.ts
**/*.{ts,tsx,md}

📄 CodeRabbit inference engine (AGENTS.md)

Run Prettier 3 for code formatting on TypeScript, TSX, and Markdown files

Files:

  • apps/web/src/server/service/ses-hook-parser.ts
🧬 Code graph analysis (1)
apps/web/src/server/service/ses-hook-parser.ts (2)
apps/web/src/server/logger/log.ts (1)
  • logger (31-63)
apps/web/src/server/service/suppression-service.ts (1)
  • SuppressionService (28-393)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: cubic · AI code reviewer
🔇 Additional comments (2)
apps/web/src/server/service/ses-hook-parser.ts (2)

123-136: LGTM - Recipient extraction logic is well-structured.

The conditional extraction using optional chaining ensures null safety, and the logic correctly distinguishes between bounce recipients and complaint recipients.


173-181: Good defensive logging for edge cases.

This warning appropriately catches scenarios where a bounce or complaint event arrives without the expected recipient data, which could indicate malformed SES payloads or unexpected event structures.

@cubic-dev-aicubic-dev-aiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 1 file

@KMKoushikKMKoushik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great catch

@KMKoushik
KMKoushik merged commit 83119f9 into usesend:mainJan 17, 2026
2 of 3 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@tpraxedes@KMKoushik
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Refine suppression list to include only affected recipients - #339

Merged
KMKoushik merged 1 commit into
usesend:mainfrom
infordoc:main
Jan 17, 2026
Merged

Refine suppression list to include only affected recipients#339
KMKoushik merged 1 commit into
usesend:mainfrom
infordoc:main

Conversation

@tpraxedes

@tpraxedestpraxedes commented Jan 12, 2026

Copy link
Copy Markdown
Contributor

Refactor suppression list logic to only include recipients that actually bounced or complained, improving accuracy in handling email suppression.


Summary by cubic

Limit suppression list updates to only recipients that actually bounced or complained, based on SES event data. Prevents suppressing unaffected recipients and improves accuracy.

  • Bug Fixes
    • Use bounce.bouncedRecipients and complaint.complainedRecipients instead of all message recipients.
    • Apply suppression only for permanent (hard) bounces and complaints.
    • Skip when no affected recipients are found and log a warning; improve error logging without failing webhook processing.

Written for commit d21f7ca. Summary will update on new commits.

Summary by CodeRabbit

  • Bug Fixes
    • Improved email suppression handling to only suppress actually affected recipients instead of all email recipients.
    • Enhanced error handling in suppression logic to prevent failures from interrupting the process.
    • Added more targeted logging for bounce and complaint events.

✏️ Tip: You can customize this high-level summary in your review settings.

Refactor suppression list logic to only include recipients that actually bounced or complained, improving accuracy in handling email suppression.
@vercel

vercelBot commented Jan 12, 2026

Copy link
Copy Markdown

@tpraxedes is attempting to deploy a commit to the kmkoushik's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitaiBot commented Jan 12, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

Modified the SES webhook parser to populate suppression lists using only the actually affected recipients from SES event data instead of all recipients from email headers. For hard bounces, the parser now extracts recipients from the bounce data; for complaints, from the complaint data. Added a guard condition to proceed only when affected recipients exist, wrapped suppression calls in error handling, and refined logging to distinguish between bounce and complaint scenarios while maintaining the existing suppression reason selection logic.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 0.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title accurately describes the main change: refining suppression list logic to include only affected recipients from SES event data instead of all email recipients.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
apps/web/src/server/service/ses-hook-parser.ts (1)

139-172: Consider using Promise.allSettled for better partial failure handling.

With Promise.all, if one suppression fails, the entire batch rejects and you lose visibility into which recipients succeeded vs failed. Using Promise.allSettled would allow all suppressions to be attempted and provide per-recipient success/failure status in logs.

Given that addSuppression uses upsert and webhooks can be retried, the current approach is acceptable, but Promise.allSettled would improve observability.

♻️ Optional improvement with Promise.allSettled
- try {- await Promise.all(- recipientEmails.map((recipientEmail) =>- SuppressionService.addSuppression({- email: recipientEmail,- teamId: email.teamId,- reason: isHardBounced- ? SuppressionReason.HARD_BOUNCE- : SuppressionReason.COMPLAINT,- source: email.id,- }),- ),- );-- logger.info(- {- emailId: email.id,- recipients: recipientEmails,- reason: isHardBounced ? "HARD_BOUNCE" : "COMPLAINT",- },- "Added emails to suppression list due to bounce/complaint",- );- } catch (error) {- logger.error(- {- emailId: email.id,- recipients: recipientEmails,- error: error instanceof Error ? error.message : "Unknown error",- },- "Failed to add emails to suppression list",- );- // Don't throw error - continue processing the webhook- }+ const results = await Promise.allSettled(+ recipientEmails.map((recipientEmail) =>+ SuppressionService.addSuppression({+ email: recipientEmail,+ teamId: email.teamId,+ reason: isHardBounced+ ? SuppressionReason.HARD_BOUNCE+ : SuppressionReason.COMPLAINT,+ source: email.id,+ }),+ ),+ );++ const succeeded = recipientEmails.filter((_, i) => results[i].status === "fulfilled");+ const failed = recipientEmails.filter((_, i) => results[i].status === "rejected");++ if (succeeded.length > 0) {+ logger.info(+ {+ emailId: email.id,+ recipients: succeeded,+ reason: isHardBounced ? "HARD_BOUNCE" : "COMPLAINT",+ },+ "Added emails to suppression list due to bounce/complaint",+ );+ }++ if (failed.length > 0) {+ logger.error(+ {+ emailId: email.id,+ failedRecipients: failed,+ },+ "Failed to add some emails to suppression list",+ );+ }
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 68d951c and d21f7ca.

📒 Files selected for processing (1)
  • apps/web/src/server/service/ses-hook-parser.ts
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{tsx,ts,jsx,js}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

Include all required imports and ensure proper naming of key components in React/NextJS code

Files:

  • apps/web/src/server/service/ses-hook-parser.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Use TypeScript-first approach with 2-space indent and semicolons enabled by Prettier in apps/web (Next.js), apps/marketing, apps/smtp-server, and all packages
Never use dynamic imports; always import on the top level
Run ESLint via @usesend/eslint-config and ensure no warnings remain before submitting PRs

Files:

  • apps/web/src/server/service/ses-hook-parser.ts
apps/web/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use alias ~/ for src imports in apps/web (e.g., import { x } from "~/utils/x")

Files:

  • apps/web/src/server/service/ses-hook-parser.ts
apps/web/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/**/*.{ts,tsx}: Prefer to use TRPC for client-server communication unless explicitly asked otherwise in apps/web
Use Prisma for database access in apps/web

Files:

  • apps/web/src/server/service/ses-hook-parser.ts
**/*.{ts,tsx,md}

📄 CodeRabbit inference engine (AGENTS.md)

Run Prettier 3 for code formatting on TypeScript, TSX, and Markdown files

Files:

  • apps/web/src/server/service/ses-hook-parser.ts
🧬 Code graph analysis (1)
apps/web/src/server/service/ses-hook-parser.ts (2)
apps/web/src/server/logger/log.ts (1)
  • logger (31-63)
apps/web/src/server/service/suppression-service.ts (1)
  • SuppressionService (28-393)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: cubic · AI code reviewer
🔇 Additional comments (2)
apps/web/src/server/service/ses-hook-parser.ts (2)

123-136: LGTM - Recipient extraction logic is well-structured.

The conditional extraction using optional chaining ensures null safety, and the logic correctly distinguishes between bounce recipients and complaint recipients.


173-181: Good defensive logging for edge cases.

This warning appropriately catches scenarios where a bounce or complaint event arrives without the expected recipient data, which could indicate malformed SES payloads or unexpected event structures.

@cubic-dev-aicubic-dev-aiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 1 file

@KMKoushikKMKoushik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great catch

@KMKoushik
KMKoushik merged commit 83119f9 into usesend:mainJan 17, 2026
2 of 3 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@tpraxedes@KMKoushik
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Refine suppression list to include only affected recipients - #339

Merged
KMKoushik merged 1 commit into
usesend:mainfrom
infordoc:main
Jan 17, 2026
Merged

Refine suppression list to include only affected recipients#339
KMKoushik merged 1 commit into
usesend:mainfrom
infordoc:main

Conversation

@tpraxedes

@tpraxedestpraxedes commented Jan 12, 2026

Copy link
Copy Markdown
Contributor

Refactor suppression list logic to only include recipients that actually bounced or complained, improving accuracy in handling email suppression.


Summary by cubic

Limit suppression list updates to only recipients that actually bounced or complained, based on SES event data. Prevents suppressing unaffected recipients and improves accuracy.

  • Bug Fixes
    • Use bounce.bouncedRecipients and complaint.complainedRecipients instead of all message recipients.
    • Apply suppression only for permanent (hard) bounces and complaints.
    • Skip when no affected recipients are found and log a warning; improve error logging without failing webhook processing.

Written for commit d21f7ca. Summary will update on new commits.

Summary by CodeRabbit

  • Bug Fixes
    • Improved email suppression handling to only suppress actually affected recipients instead of all email recipients.
    • Enhanced error handling in suppression logic to prevent failures from interrupting the process.
    • Added more targeted logging for bounce and complaint events.

✏️ Tip: You can customize this high-level summary in your review settings.

Refactor suppression list logic to only include recipients that actually bounced or complained, improving accuracy in handling email suppression.
@vercel

vercelBot commented Jan 12, 2026

Copy link
Copy Markdown

@tpraxedes is attempting to deploy a commit to the kmkoushik's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitaiBot commented Jan 12, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

Modified the SES webhook parser to populate suppression lists using only the actually affected recipients from SES event data instead of all recipients from email headers. For hard bounces, the parser now extracts recipients from the bounce data; for complaints, from the complaint data. Added a guard condition to proceed only when affected recipients exist, wrapped suppression calls in error handling, and refined logging to distinguish between bounce and complaint scenarios while maintaining the existing suppression reason selection logic.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 0.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title accurately describes the main change: refining suppression list logic to include only affected recipients from SES event data instead of all email recipients.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
apps/web/src/server/service/ses-hook-parser.ts (1)

139-172: Consider using Promise.allSettled for better partial failure handling.

With Promise.all, if one suppression fails, the entire batch rejects and you lose visibility into which recipients succeeded vs failed. Using Promise.allSettled would allow all suppressions to be attempted and provide per-recipient success/failure status in logs.

Given that addSuppression uses upsert and webhooks can be retried, the current approach is acceptable, but Promise.allSettled would improve observability.

♻️ Optional improvement with Promise.allSettled
- try {- await Promise.all(- recipientEmails.map((recipientEmail) =>- SuppressionService.addSuppression({- email: recipientEmail,- teamId: email.teamId,- reason: isHardBounced- ? SuppressionReason.HARD_BOUNCE- : SuppressionReason.COMPLAINT,- source: email.id,- }),- ),- );-- logger.info(- {- emailId: email.id,- recipients: recipientEmails,- reason: isHardBounced ? "HARD_BOUNCE" : "COMPLAINT",- },- "Added emails to suppression list due to bounce/complaint",- );- } catch (error) {- logger.error(- {- emailId: email.id,- recipients: recipientEmails,- error: error instanceof Error ? error.message : "Unknown error",- },- "Failed to add emails to suppression list",- );- // Don't throw error - continue processing the webhook- }+ const results = await Promise.allSettled(+ recipientEmails.map((recipientEmail) =>+ SuppressionService.addSuppression({+ email: recipientEmail,+ teamId: email.teamId,+ reason: isHardBounced+ ? SuppressionReason.HARD_BOUNCE+ : SuppressionReason.COMPLAINT,+ source: email.id,+ }),+ ),+ );++ const succeeded = recipientEmails.filter((_, i) => results[i].status === "fulfilled");+ const failed = recipientEmails.filter((_, i) => results[i].status === "rejected");++ if (succeeded.length > 0) {+ logger.info(+ {+ emailId: email.id,+ recipients: succeeded,+ reason: isHardBounced ? "HARD_BOUNCE" : "COMPLAINT",+ },+ "Added emails to suppression list due to bounce/complaint",+ );+ }++ if (failed.length > 0) {+ logger.error(+ {+ emailId: email.id,+ failedRecipients: failed,+ },+ "Failed to add some emails to suppression list",+ );+ }
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 68d951c and d21f7ca.

📒 Files selected for processing (1)
  • apps/web/src/server/service/ses-hook-parser.ts
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{tsx,ts,jsx,js}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

Include all required imports and ensure proper naming of key components in React/NextJS code

Files:

  • apps/web/src/server/service/ses-hook-parser.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Use TypeScript-first approach with 2-space indent and semicolons enabled by Prettier in apps/web (Next.js), apps/marketing, apps/smtp-server, and all packages
Never use dynamic imports; always import on the top level
Run ESLint via @usesend/eslint-config and ensure no warnings remain before submitting PRs

Files:

  • apps/web/src/server/service/ses-hook-parser.ts
apps/web/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use alias ~/ for src imports in apps/web (e.g., import { x } from "~/utils/x")

Files:

  • apps/web/src/server/service/ses-hook-parser.ts
apps/web/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/**/*.{ts,tsx}: Prefer to use TRPC for client-server communication unless explicitly asked otherwise in apps/web
Use Prisma for database access in apps/web

Files:

  • apps/web/src/server/service/ses-hook-parser.ts
**/*.{ts,tsx,md}

📄 CodeRabbit inference engine (AGENTS.md)

Run Prettier 3 for code formatting on TypeScript, TSX, and Markdown files

Files:

  • apps/web/src/server/service/ses-hook-parser.ts
🧬 Code graph analysis (1)
apps/web/src/server/service/ses-hook-parser.ts (2)
apps/web/src/server/logger/log.ts (1)
  • logger (31-63)
apps/web/src/server/service/suppression-service.ts (1)
  • SuppressionService (28-393)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: cubic · AI code reviewer
🔇 Additional comments (2)
apps/web/src/server/service/ses-hook-parser.ts (2)

123-136: LGTM - Recipient extraction logic is well-structured.

The conditional extraction using optional chaining ensures null safety, and the logic correctly distinguishes between bounce recipients and complaint recipients.


173-181: Good defensive logging for edge cases.

This warning appropriately catches scenarios where a bounce or complaint event arrives without the expected recipient data, which could indicate malformed SES payloads or unexpected event structures.

@cubic-dev-aicubic-dev-aiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 1 file

@KMKoushikKMKoushik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great catch

@KMKoushik
KMKoushik merged commit 83119f9 into usesend:mainJan 17, 2026
2 of 3 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@tpraxedes@KMKoushik
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Refine suppression list to include only affected recipients - #339

Merged
KMKoushik merged 1 commit into
usesend:mainfrom
infordoc:main
Jan 17, 2026
Merged

Refine suppression list to include only affected recipients#339
KMKoushik merged 1 commit into
usesend:mainfrom
infordoc:main

Conversation

@tpraxedes

@tpraxedestpraxedes commented Jan 12, 2026

Copy link
Copy Markdown
Contributor

Refactor suppression list logic to only include recipients that actually bounced or complained, improving accuracy in handling email suppression.


Summary by cubic

Limit suppression list updates to only recipients that actually bounced or complained, based on SES event data. Prevents suppressing unaffected recipients and improves accuracy.

  • Bug Fixes
    • Use bounce.bouncedRecipients and complaint.complainedRecipients instead of all message recipients.
    • Apply suppression only for permanent (hard) bounces and complaints.
    • Skip when no affected recipients are found and log a warning; improve error logging without failing webhook processing.

Written for commit d21f7ca. Summary will update on new commits.

Summary by CodeRabbit

  • Bug Fixes
    • Improved email suppression handling to only suppress actually affected recipients instead of all email recipients.
    • Enhanced error handling in suppression logic to prevent failures from interrupting the process.
    • Added more targeted logging for bounce and complaint events.

✏️ Tip: You can customize this high-level summary in your review settings.

Refactor suppression list logic to only include recipients that actually bounced or complained, improving accuracy in handling email suppression.
@vercel

vercelBot commented Jan 12, 2026

Copy link
Copy Markdown

@tpraxedes is attempting to deploy a commit to the kmkoushik's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitaiBot commented Jan 12, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

Modified the SES webhook parser to populate suppression lists using only the actually affected recipients from SES event data instead of all recipients from email headers. For hard bounces, the parser now extracts recipients from the bounce data; for complaints, from the complaint data. Added a guard condition to proceed only when affected recipients exist, wrapped suppression calls in error handling, and refined logging to distinguish between bounce and complaint scenarios while maintaining the existing suppression reason selection logic.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 0.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title accurately describes the main change: refining suppression list logic to include only affected recipients from SES event data instead of all email recipients.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
apps/web/src/server/service/ses-hook-parser.ts (1)

139-172: Consider using Promise.allSettled for better partial failure handling.

With Promise.all, if one suppression fails, the entire batch rejects and you lose visibility into which recipients succeeded vs failed. Using Promise.allSettled would allow all suppressions to be attempted and provide per-recipient success/failure status in logs.

Given that addSuppression uses upsert and webhooks can be retried, the current approach is acceptable, but Promise.allSettled would improve observability.

♻️ Optional improvement with Promise.allSettled
- try {- await Promise.all(- recipientEmails.map((recipientEmail) =>- SuppressionService.addSuppression({- email: recipientEmail,- teamId: email.teamId,- reason: isHardBounced- ? SuppressionReason.HARD_BOUNCE- : SuppressionReason.COMPLAINT,- source: email.id,- }),- ),- );-- logger.info(- {- emailId: email.id,- recipients: recipientEmails,- reason: isHardBounced ? "HARD_BOUNCE" : "COMPLAINT",- },- "Added emails to suppression list due to bounce/complaint",- );- } catch (error) {- logger.error(- {- emailId: email.id,- recipients: recipientEmails,- error: error instanceof Error ? error.message : "Unknown error",- },- "Failed to add emails to suppression list",- );- // Don't throw error - continue processing the webhook- }+ const results = await Promise.allSettled(+ recipientEmails.map((recipientEmail) =>+ SuppressionService.addSuppression({+ email: recipientEmail,+ teamId: email.teamId,+ reason: isHardBounced+ ? SuppressionReason.HARD_BOUNCE+ : SuppressionReason.COMPLAINT,+ source: email.id,+ }),+ ),+ );++ const succeeded = recipientEmails.filter((_, i) => results[i].status === "fulfilled");+ const failed = recipientEmails.filter((_, i) => results[i].status === "rejected");++ if (succeeded.length > 0) {+ logger.info(+ {+ emailId: email.id,+ recipients: succeeded,+ reason: isHardBounced ? "HARD_BOUNCE" : "COMPLAINT",+ },+ "Added emails to suppression list due to bounce/complaint",+ );+ }++ if (failed.length > 0) {+ logger.error(+ {+ emailId: email.id,+ failedRecipients: failed,+ },+ "Failed to add some emails to suppression list",+ );+ }
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 68d951c and d21f7ca.

📒 Files selected for processing (1)
  • apps/web/src/server/service/ses-hook-parser.ts
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{tsx,ts,jsx,js}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

Include all required imports and ensure proper naming of key components in React/NextJS code

Files:

  • apps/web/src/server/service/ses-hook-parser.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Use TypeScript-first approach with 2-space indent and semicolons enabled by Prettier in apps/web (Next.js), apps/marketing, apps/smtp-server, and all packages
Never use dynamic imports; always import on the top level
Run ESLint via @usesend/eslint-config and ensure no warnings remain before submitting PRs

Files:

  • apps/web/src/server/service/ses-hook-parser.ts
apps/web/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use alias ~/ for src imports in apps/web (e.g., import { x } from "~/utils/x")

Files:

  • apps/web/src/server/service/ses-hook-parser.ts
apps/web/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/**/*.{ts,tsx}: Prefer to use TRPC for client-server communication unless explicitly asked otherwise in apps/web
Use Prisma for database access in apps/web

Files:

  • apps/web/src/server/service/ses-hook-parser.ts
**/*.{ts,tsx,md}

📄 CodeRabbit inference engine (AGENTS.md)

Run Prettier 3 for code formatting on TypeScript, TSX, and Markdown files

Files:

  • apps/web/src/server/service/ses-hook-parser.ts
🧬 Code graph analysis (1)
apps/web/src/server/service/ses-hook-parser.ts (2)
apps/web/src/server/logger/log.ts (1)
  • logger (31-63)
apps/web/src/server/service/suppression-service.ts (1)
  • SuppressionService (28-393)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: cubic · AI code reviewer
🔇 Additional comments (2)
apps/web/src/server/service/ses-hook-parser.ts (2)

123-136: LGTM - Recipient extraction logic is well-structured.

The conditional extraction using optional chaining ensures null safety, and the logic correctly distinguishes between bounce recipients and complaint recipients.


173-181: Good defensive logging for edge cases.

This warning appropriately catches scenarios where a bounce or complaint event arrives without the expected recipient data, which could indicate malformed SES payloads or unexpected event structures.

@cubic-dev-aicubic-dev-aiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 1 file

@KMKoushikKMKoushik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great catch

@KMKoushik
KMKoushik merged commit 83119f9 into usesend:mainJan 17, 2026
2 of 3 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@tpraxedes@KMKoushik
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Refine suppression list to include only affected recipients - #339

Merged
KMKoushik merged 1 commit into
usesend:mainfrom
infordoc:main
Jan 17, 2026
Merged

Refine suppression list to include only affected recipients#339
KMKoushik merged 1 commit into
usesend:mainfrom
infordoc:main

Conversation

@tpraxedes

@tpraxedestpraxedes commented Jan 12, 2026

Copy link
Copy Markdown
Contributor

Refactor suppression list logic to only include recipients that actually bounced or complained, improving accuracy in handling email suppression.


Summary by cubic

Limit suppression list updates to only recipients that actually bounced or complained, based on SES event data. Prevents suppressing unaffected recipients and improves accuracy.

  • Bug Fixes
    • Use bounce.bouncedRecipients and complaint.complainedRecipients instead of all message recipients.
    • Apply suppression only for permanent (hard) bounces and complaints.
    • Skip when no affected recipients are found and log a warning; improve error logging without failing webhook processing.

Written for commit d21f7ca. Summary will update on new commits.

Summary by CodeRabbit

  • Bug Fixes
    • Improved email suppression handling to only suppress actually affected recipients instead of all email recipients.
    • Enhanced error handling in suppression logic to prevent failures from interrupting the process.
    • Added more targeted logging for bounce and complaint events.

✏️ Tip: You can customize this high-level summary in your review settings.

Refactor suppression list logic to only include recipients that actually bounced or complained, improving accuracy in handling email suppression.
@vercel

vercelBot commented Jan 12, 2026

Copy link
Copy Markdown

@tpraxedes is attempting to deploy a commit to the kmkoushik's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitaiBot commented Jan 12, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

Modified the SES webhook parser to populate suppression lists using only the actually affected recipients from SES event data instead of all recipients from email headers. For hard bounces, the parser now extracts recipients from the bounce data; for complaints, from the complaint data. Added a guard condition to proceed only when affected recipients exist, wrapped suppression calls in error handling, and refined logging to distinguish between bounce and complaint scenarios while maintaining the existing suppression reason selection logic.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 0.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title accurately describes the main change: refining suppression list logic to include only affected recipients from SES event data instead of all email recipients.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
apps/web/src/server/service/ses-hook-parser.ts (1)

139-172: Consider using Promise.allSettled for better partial failure handling.

With Promise.all, if one suppression fails, the entire batch rejects and you lose visibility into which recipients succeeded vs failed. Using Promise.allSettled would allow all suppressions to be attempted and provide per-recipient success/failure status in logs.

Given that addSuppression uses upsert and webhooks can be retried, the current approach is acceptable, but Promise.allSettled would improve observability.

♻️ Optional improvement with Promise.allSettled
- try {- await Promise.all(- recipientEmails.map((recipientEmail) =>- SuppressionService.addSuppression({- email: recipientEmail,- teamId: email.teamId,- reason: isHardBounced- ? SuppressionReason.HARD_BOUNCE- : SuppressionReason.COMPLAINT,- source: email.id,- }),- ),- );-- logger.info(- {- emailId: email.id,- recipients: recipientEmails,- reason: isHardBounced ? "HARD_BOUNCE" : "COMPLAINT",- },- "Added emails to suppression list due to bounce/complaint",- );- } catch (error) {- logger.error(- {- emailId: email.id,- recipients: recipientEmails,- error: error instanceof Error ? error.message : "Unknown error",- },- "Failed to add emails to suppression list",- );- // Don't throw error - continue processing the webhook- }+ const results = await Promise.allSettled(+ recipientEmails.map((recipientEmail) =>+ SuppressionService.addSuppression({+ email: recipientEmail,+ teamId: email.teamId,+ reason: isHardBounced+ ? SuppressionReason.HARD_BOUNCE+ : SuppressionReason.COMPLAINT,+ source: email.id,+ }),+ ),+ );++ const succeeded = recipientEmails.filter((_, i) => results[i].status === "fulfilled");+ const failed = recipientEmails.filter((_, i) => results[i].status === "rejected");++ if (succeeded.length > 0) {+ logger.info(+ {+ emailId: email.id,+ recipients: succeeded,+ reason: isHardBounced ? "HARD_BOUNCE" : "COMPLAINT",+ },+ "Added emails to suppression list due to bounce/complaint",+ );+ }++ if (failed.length > 0) {+ logger.error(+ {+ emailId: email.id,+ failedRecipients: failed,+ },+ "Failed to add some emails to suppression list",+ );+ }
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 68d951c and d21f7ca.

📒 Files selected for processing (1)
  • apps/web/src/server/service/ses-hook-parser.ts
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{tsx,ts,jsx,js}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

Include all required imports and ensure proper naming of key components in React/NextJS code

Files:

  • apps/web/src/server/service/ses-hook-parser.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Use TypeScript-first approach with 2-space indent and semicolons enabled by Prettier in apps/web (Next.js), apps/marketing, apps/smtp-server, and all packages
Never use dynamic imports; always import on the top level
Run ESLint via @usesend/eslint-config and ensure no warnings remain before submitting PRs

Files:

  • apps/web/src/server/service/ses-hook-parser.ts
apps/web/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use alias ~/ for src imports in apps/web (e.g., import { x } from "~/utils/x")

Files:

  • apps/web/src/server/service/ses-hook-parser.ts
apps/web/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/**/*.{ts,tsx}: Prefer to use TRPC for client-server communication unless explicitly asked otherwise in apps/web
Use Prisma for database access in apps/web

Files:

  • apps/web/src/server/service/ses-hook-parser.ts
**/*.{ts,tsx,md}

📄 CodeRabbit inference engine (AGENTS.md)

Run Prettier 3 for code formatting on TypeScript, TSX, and Markdown files

Files:

  • apps/web/src/server/service/ses-hook-parser.ts
🧬 Code graph analysis (1)
apps/web/src/server/service/ses-hook-parser.ts (2)
apps/web/src/server/logger/log.ts (1)
  • logger (31-63)
apps/web/src/server/service/suppression-service.ts (1)
  • SuppressionService (28-393)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: cubic · AI code reviewer
🔇 Additional comments (2)
apps/web/src/server/service/ses-hook-parser.ts (2)

123-136: LGTM - Recipient extraction logic is well-structured.

The conditional extraction using optional chaining ensures null safety, and the logic correctly distinguishes between bounce recipients and complaint recipients.


173-181: Good defensive logging for edge cases.

This warning appropriately catches scenarios where a bounce or complaint event arrives without the expected recipient data, which could indicate malformed SES payloads or unexpected event structures.

@cubic-dev-aicubic-dev-aiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 1 file

@KMKoushikKMKoushik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great catch

@KMKoushik
KMKoushik merged commit 83119f9 into usesend:mainJan 17, 2026
2 of 3 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@tpraxedes@KMKoushik
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Refine suppression list to include only affected recipients - #339

Merged
KMKoushik merged 1 commit into
usesend:mainfrom
infordoc:main
Jan 17, 2026
Merged

Refine suppression list to include only affected recipients#339
KMKoushik merged 1 commit into
usesend:mainfrom
infordoc:main

Conversation

@tpraxedes

@tpraxedestpraxedes commented Jan 12, 2026

Copy link
Copy Markdown
Contributor

Refactor suppression list logic to only include recipients that actually bounced or complained, improving accuracy in handling email suppression.


Summary by cubic

Limit suppression list updates to only recipients that actually bounced or complained, based on SES event data. Prevents suppressing unaffected recipients and improves accuracy.

  • Bug Fixes
    • Use bounce.bouncedRecipients and complaint.complainedRecipients instead of all message recipients.
    • Apply suppression only for permanent (hard) bounces and complaints.
    • Skip when no affected recipients are found and log a warning; improve error logging without failing webhook processing.

Written for commit d21f7ca. Summary will update on new commits.

Summary by CodeRabbit

  • Bug Fixes
    • Improved email suppression handling to only suppress actually affected recipients instead of all email recipients.
    • Enhanced error handling in suppression logic to prevent failures from interrupting the process.
    • Added more targeted logging for bounce and complaint events.

✏️ Tip: You can customize this high-level summary in your review settings.

Refactor suppression list logic to only include recipients that actually bounced or complained, improving accuracy in handling email suppression.
@vercel

vercelBot commented Jan 12, 2026

Copy link
Copy Markdown

@tpraxedes is attempting to deploy a commit to the kmkoushik's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitaiBot commented Jan 12, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

Modified the SES webhook parser to populate suppression lists using only the actually affected recipients from SES event data instead of all recipients from email headers. For hard bounces, the parser now extracts recipients from the bounce data; for complaints, from the complaint data. Added a guard condition to proceed only when affected recipients exist, wrapped suppression calls in error handling, and refined logging to distinguish between bounce and complaint scenarios while maintaining the existing suppression reason selection logic.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 0.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title accurately describes the main change: refining suppression list logic to include only affected recipients from SES event data instead of all email recipients.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
apps/web/src/server/service/ses-hook-parser.ts (1)

139-172: Consider using Promise.allSettled for better partial failure handling.

With Promise.all, if one suppression fails, the entire batch rejects and you lose visibility into which recipients succeeded vs failed. Using Promise.allSettled would allow all suppressions to be attempted and provide per-recipient success/failure status in logs.

Given that addSuppression uses upsert and webhooks can be retried, the current approach is acceptable, but Promise.allSettled would improve observability.

♻️ Optional improvement with Promise.allSettled
- try {- await Promise.all(- recipientEmails.map((recipientEmail) =>- SuppressionService.addSuppression({- email: recipientEmail,- teamId: email.teamId,- reason: isHardBounced- ? SuppressionReason.HARD_BOUNCE- : SuppressionReason.COMPLAINT,- source: email.id,- }),- ),- );-- logger.info(- {- emailId: email.id,- recipients: recipientEmails,- reason: isHardBounced ? "HARD_BOUNCE" : "COMPLAINT",- },- "Added emails to suppression list due to bounce/complaint",- );- } catch (error) {- logger.error(- {- emailId: email.id,- recipients: recipientEmails,- error: error instanceof Error ? error.message : "Unknown error",- },- "Failed to add emails to suppression list",- );- // Don't throw error - continue processing the webhook- }+ const results = await Promise.allSettled(+ recipientEmails.map((recipientEmail) =>+ SuppressionService.addSuppression({+ email: recipientEmail,+ teamId: email.teamId,+ reason: isHardBounced+ ? SuppressionReason.HARD_BOUNCE+ : SuppressionReason.COMPLAINT,+ source: email.id,+ }),+ ),+ );++ const succeeded = recipientEmails.filter((_, i) => results[i].status === "fulfilled");+ const failed = recipientEmails.filter((_, i) => results[i].status === "rejected");++ if (succeeded.length > 0) {+ logger.info(+ {+ emailId: email.id,+ recipients: succeeded,+ reason: isHardBounced ? "HARD_BOUNCE" : "COMPLAINT",+ },+ "Added emails to suppression list due to bounce/complaint",+ );+ }++ if (failed.length > 0) {+ logger.error(+ {+ emailId: email.id,+ failedRecipients: failed,+ },+ "Failed to add some emails to suppression list",+ );+ }
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 68d951c and d21f7ca.

📒 Files selected for processing (1)
  • apps/web/src/server/service/ses-hook-parser.ts
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{tsx,ts,jsx,js}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

Include all required imports and ensure proper naming of key components in React/NextJS code

Files:

  • apps/web/src/server/service/ses-hook-parser.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Use TypeScript-first approach with 2-space indent and semicolons enabled by Prettier in apps/web (Next.js), apps/marketing, apps/smtp-server, and all packages
Never use dynamic imports; always import on the top level
Run ESLint via @usesend/eslint-config and ensure no warnings remain before submitting PRs

Files:

  • apps/web/src/server/service/ses-hook-parser.ts
apps/web/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use alias ~/ for src imports in apps/web (e.g., import { x } from "~/utils/x")

Files:

  • apps/web/src/server/service/ses-hook-parser.ts
apps/web/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/**/*.{ts,tsx}: Prefer to use TRPC for client-server communication unless explicitly asked otherwise in apps/web
Use Prisma for database access in apps/web

Files:

  • apps/web/src/server/service/ses-hook-parser.ts
**/*.{ts,tsx,md}

📄 CodeRabbit inference engine (AGENTS.md)

Run Prettier 3 for code formatting on TypeScript, TSX, and Markdown files

Files:

  • apps/web/src/server/service/ses-hook-parser.ts
🧬 Code graph analysis (1)
apps/web/src/server/service/ses-hook-parser.ts (2)
apps/web/src/server/logger/log.ts (1)
  • logger (31-63)
apps/web/src/server/service/suppression-service.ts (1)
  • SuppressionService (28-393)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: cubic · AI code reviewer
🔇 Additional comments (2)
apps/web/src/server/service/ses-hook-parser.ts (2)

123-136: LGTM - Recipient extraction logic is well-structured.

The conditional extraction using optional chaining ensures null safety, and the logic correctly distinguishes between bounce recipients and complaint recipients.


173-181: Good defensive logging for edge cases.

This warning appropriately catches scenarios where a bounce or complaint event arrives without the expected recipient data, which could indicate malformed SES payloads or unexpected event structures.

@cubic-dev-aicubic-dev-aiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 1 file

@KMKoushikKMKoushik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great catch

@KMKoushik
KMKoushik merged commit 83119f9 into usesend:mainJan 17, 2026
2 of 3 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@tpraxedes@KMKoushik
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Refine suppression list to include only affected recipients - #339

Merged
KMKoushik merged 1 commit into
usesend:mainfrom
infordoc:main
Jan 17, 2026
Merged

Refine suppression list to include only affected recipients#339
KMKoushik merged 1 commit into
usesend:mainfrom
infordoc:main

Conversation

@tpraxedes

@tpraxedestpraxedes commented Jan 12, 2026

Copy link
Copy Markdown
Contributor

Refactor suppression list logic to only include recipients that actually bounced or complained, improving accuracy in handling email suppression.


Summary by cubic

Limit suppression list updates to only recipients that actually bounced or complained, based on SES event data. Prevents suppressing unaffected recipients and improves accuracy.

  • Bug Fixes
    • Use bounce.bouncedRecipients and complaint.complainedRecipients instead of all message recipients.
    • Apply suppression only for permanent (hard) bounces and complaints.
    • Skip when no affected recipients are found and log a warning; improve error logging without failing webhook processing.

Written for commit d21f7ca. Summary will update on new commits.

Summary by CodeRabbit

  • Bug Fixes
    • Improved email suppression handling to only suppress actually affected recipients instead of all email recipients.
    • Enhanced error handling in suppression logic to prevent failures from interrupting the process.
    • Added more targeted logging for bounce and complaint events.

✏️ Tip: You can customize this high-level summary in your review settings.

Refactor suppression list logic to only include recipients that actually bounced or complained, improving accuracy in handling email suppression.
@vercel

vercelBot commented Jan 12, 2026

Copy link
Copy Markdown

@tpraxedes is attempting to deploy a commit to the kmkoushik's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitaiBot commented Jan 12, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

Modified the SES webhook parser to populate suppression lists using only the actually affected recipients from SES event data instead of all recipients from email headers. For hard bounces, the parser now extracts recipients from the bounce data; for complaints, from the complaint data. Added a guard condition to proceed only when affected recipients exist, wrapped suppression calls in error handling, and refined logging to distinguish between bounce and complaint scenarios while maintaining the existing suppression reason selection logic.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 0.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title accurately describes the main change: refining suppression list logic to include only affected recipients from SES event data instead of all email recipients.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
apps/web/src/server/service/ses-hook-parser.ts (1)

139-172: Consider using Promise.allSettled for better partial failure handling.

With Promise.all, if one suppression fails, the entire batch rejects and you lose visibility into which recipients succeeded vs failed. Using Promise.allSettled would allow all suppressions to be attempted and provide per-recipient success/failure status in logs.

Given that addSuppression uses upsert and webhooks can be retried, the current approach is acceptable, but Promise.allSettled would improve observability.

♻️ Optional improvement with Promise.allSettled
- try {- await Promise.all(- recipientEmails.map((recipientEmail) =>- SuppressionService.addSuppression({- email: recipientEmail,- teamId: email.teamId,- reason: isHardBounced- ? SuppressionReason.HARD_BOUNCE- : SuppressionReason.COMPLAINT,- source: email.id,- }),- ),- );-- logger.info(- {- emailId: email.id,- recipients: recipientEmails,- reason: isHardBounced ? "HARD_BOUNCE" : "COMPLAINT",- },- "Added emails to suppression list due to bounce/complaint",- );- } catch (error) {- logger.error(- {- emailId: email.id,- recipients: recipientEmails,- error: error instanceof Error ? error.message : "Unknown error",- },- "Failed to add emails to suppression list",- );- // Don't throw error - continue processing the webhook- }+ const results = await Promise.allSettled(+ recipientEmails.map((recipientEmail) =>+ SuppressionService.addSuppression({+ email: recipientEmail,+ teamId: email.teamId,+ reason: isHardBounced+ ? SuppressionReason.HARD_BOUNCE+ : SuppressionReason.COMPLAINT,+ source: email.id,+ }),+ ),+ );++ const succeeded = recipientEmails.filter((_, i) => results[i].status === "fulfilled");+ const failed = recipientEmails.filter((_, i) => results[i].status === "rejected");++ if (succeeded.length > 0) {+ logger.info(+ {+ emailId: email.id,+ recipients: succeeded,+ reason: isHardBounced ? "HARD_BOUNCE" : "COMPLAINT",+ },+ "Added emails to suppression list due to bounce/complaint",+ );+ }++ if (failed.length > 0) {+ logger.error(+ {+ emailId: email.id,+ failedRecipients: failed,+ },+ "Failed to add some emails to suppression list",+ );+ }
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 68d951c and d21f7ca.

📒 Files selected for processing (1)
  • apps/web/src/server/service/ses-hook-parser.ts
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{tsx,ts,jsx,js}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

Include all required imports and ensure proper naming of key components in React/NextJS code

Files:

  • apps/web/src/server/service/ses-hook-parser.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Use TypeScript-first approach with 2-space indent and semicolons enabled by Prettier in apps/web (Next.js), apps/marketing, apps/smtp-server, and all packages
Never use dynamic imports; always import on the top level
Run ESLint via @usesend/eslint-config and ensure no warnings remain before submitting PRs

Files:

  • apps/web/src/server/service/ses-hook-parser.ts
apps/web/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use alias ~/ for src imports in apps/web (e.g., import { x } from "~/utils/x")

Files:

  • apps/web/src/server/service/ses-hook-parser.ts
apps/web/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/**/*.{ts,tsx}: Prefer to use TRPC for client-server communication unless explicitly asked otherwise in apps/web
Use Prisma for database access in apps/web

Files:

  • apps/web/src/server/service/ses-hook-parser.ts
**/*.{ts,tsx,md}

📄 CodeRabbit inference engine (AGENTS.md)

Run Prettier 3 for code formatting on TypeScript, TSX, and Markdown files

Files:

  • apps/web/src/server/service/ses-hook-parser.ts
🧬 Code graph analysis (1)
apps/web/src/server/service/ses-hook-parser.ts (2)
apps/web/src/server/logger/log.ts (1)
  • logger (31-63)
apps/web/src/server/service/suppression-service.ts (1)
  • SuppressionService (28-393)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: cubic · AI code reviewer
🔇 Additional comments (2)
apps/web/src/server/service/ses-hook-parser.ts (2)

123-136: LGTM - Recipient extraction logic is well-structured.

The conditional extraction using optional chaining ensures null safety, and the logic correctly distinguishes between bounce recipients and complaint recipients.


173-181: Good defensive logging for edge cases.

This warning appropriately catches scenarios where a bounce or complaint event arrives without the expected recipient data, which could indicate malformed SES payloads or unexpected event structures.

@cubic-dev-aicubic-dev-aiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 1 file

@KMKoushikKMKoushik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great catch

@KMKoushik
KMKoushik merged commit 83119f9 into usesend:mainJan 17, 2026
2 of 3 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@tpraxedes@KMKoushik