Skip to content

fix: keep suppressed addresses out of broadcasts and out of bounce counts - #239

Merged
stewartjarod merged 1 commit into
mainfrom
fix-suppression-list-sync
Oct 31, 2026
Merged

stewartjarod merged 1 commit into
mainfrom
fix-suppression-list-sync

Conversation

@stewartjarod

Copy link
Copy Markdown
Contributor

Summary

A manual suppression only reached the SES list. Broadcasts read contact.email_status, so the Oct 30 broadcast still sent to 5 suppressed addresses. SES refused them as Permanent / OnAccountSuppressionList, and the webhook counted each one as a hard bounce (bounced=6, suppressed=0; really 1 and 5).

 suppressOne (dashboard Suppress / Add address)
   putSuppressedAddress            # SES list
+  suppressContactByEmail          # contact → suppressed (only if active / soft_bounced / NULL)
   audit

 removeSuppression
   deleteSuppressedAddress
+  unsuppressContactByEmail        # suppressed → active
   audit
 processBounce / sdkDiagnosticFor / applyLifecycleToContact
-  if subType in (Suppressed, EmailValidationSuppressed)
+  if isSuppressionBounceSubType(subType)   # @wraps/core: + OnAccountSuppressionList, OnTenantSuppressionList
     → suppression, not bounce

The bounce-reason metrics in packages/db exclude the same subtypes. That package can't import core, so it keeps a copy of the list, with a comment saying to change both together. The event-processor Lambda already treated OnAccountSuppressionList as a suppression.

Evidence

  • Before: the new tests fail with the fix reverted. Webhook: 10 failed (an OnAccountSuppressionList bounce → status: "bounced", batch.bounced + 1, contact bounced). Suppression actions: 10 failed (the contact stays active after Suppress).
    After: all green: API 2360, web 3812, db 342, core 240. Typecheck is clean.
  • Production: broadcast e158d3d0 was corrected by hand to bounced=1, suppressed=5, with the 5 contacts set to suppressed.

Merge Danger

Door: two-way

Code only, no migration. Reverting restores the old labelling.

Blast Radius: small

  • Contact status now follows manual suppress and unsuppress, scoped to the org. Unsuppress also re-activates a contact suppressed by an SES global-list event; SES still blocks that send.
  • An SDK send whose first event is one of these bounces still gets message_send.status = 'bounced' (unchanged). Only its contact and diagnostics change.
  • Existing mislabelled transactional rows in other orgs are not backfilled.

🤖 Generated with Claude Code

https://claude.ai/code/session_01GxJAQPQtEUd9AV6C37CnJM

…unts

A manual suppression wrote only to the SES account suppression list, but
broadcasts pick recipients from contact.email_status. The next broadcast
still sent to those addresses, SES refused them with a Permanent bounce
of subtype OnAccountSuppressionList, and the webhook recorded each one as
a hard bounce.

- Suppressing an address in the dashboard now marks the org's sendable
  contact suppressed (also when SES already had it); removing it returns
  a suppressed contact to active.
- OnAccountSuppressionList and OnTenantSuppressionList bounces are
  suppressions, via one shared isSuppressionBounceSubType predicate used
  by the webhook, the SDK diagnostic path and the contact lifecycle. The
  bounce-reason metrics exclude them too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GxJAQPQtEUd9AV6C37CnJM
@vercel
vercel Bot temporarily deployed to Preview – wraps-website October 31, 2026 15:49 Inactive
@vercel

vercel Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
wraps-web Ready Ready Preview Oct 31, 2026 3:51pm UTC
1 Skipped Deployment
Project Deployment Actions Updated
wraps-website Skipped Skipped Oct 31, 2026 3:51pm UTC

Request Review

@stewartjarod
stewartjarod merged commit 3348027 into main Oct 31, 2026
21 checks passed
@stewartjarod
stewartjarod deleted the fix-suppression-list-sync branch October 31, 2026 16:11

This branch was successfully deployed

1 active and 1 inactive deployments
Preview – wraps-web — 431ac730 Deployed Oct 31, 2026 by vercel[bot]
Preview – wraps-website — 431ac730 Deployed Oct 31, 2026 by vercel[bot]
Sign up for free to 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.

1 participant