-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Fix Resend batch Idempotency key #3043
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. WalkthroughReplaces per-call deduplicationId/Idempotency-Key header usage with a propagated top-level idempotencyKey across batch-email APIs and callers. Function signatures and calls were updated to accept and forward idempotencyKey (queue, package, and cron/workflow routes); Resend integration and QStash deduplication now use computed idempotencyKey. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant Cron as Cron/Workflow Route
participant Queue as queueBatchEmail
participant QStash as QStash (dequeue)
participant SendRoute as send-batch-email Route
participant Email as sendBatchEmail
participant Resend as Resend API
Cron->>Queue: queueBatchEmail(emails[], { idempotencyKey })
Queue->>Queue: compute per-batch idempotencyKey\n(append suffix if multiple batches)
Queue->>QStash: enqueue payload + deduplicationId (from idempotencyKey)
QStash->>SendRoute: deliver queued payload (includes Idempotency-Key header)
SendRoute->>Email: sendBatchEmail(payload, { idempotencyKey })
Email->>Resend: sendBatchEmailViaResend(payload, { idempotencyKey })
Resend-->>Email: response (uses Idempotency-Key for dedupe)
Email-->>SendRoute: result
SendRoute-->>Cron: 200 / status
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes
Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR adds idempotency key support to batch email sending functionality to prevent duplicate emails. The change introduces a consistent pattern where idempotency keys are used for both QStash deduplication (for queuing) and Resend idempotency (for sending), replacing the previous inconsistent approach where headers were sometimes passed directly to queueBatchEmail.
- Added
idempotencyKeyparameter tosendBatchEmailandsendBatchEmailViaResendfunctions - Updated
queueBatchEmailto useidempotencyKeyinstead ofdeduplicationIdfor both QStash and Resend - Standardized all call sites to use the new
idempotencyKeyoption pattern
Reviewed Changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| packages/email/src/send-via-resend.ts | Added optional idempotencyKey parameter to sendBatchEmailViaResend and passed it to Resend's batch.send API |
| packages/email/src/index.ts | Added optional idempotencyKey parameter to sendBatchEmail wrapper function |
| apps/web/lib/email/queue-batch-email.ts | Renamed deduplicationId to idempotencyKey and added logic to pass it as both QStash deduplication ID and Resend idempotency header |
| apps/web/app/(ee)/api/workflows/partner-approved/route.ts | Migrated from inline headers to using the new idempotencyKey option pattern |
| apps/web/app/(ee)/api/cron/send-batch-email/route.ts | Added extraction of Idempotency-Key header and passed it to sendBatchEmail |
| apps/web/app/(ee)/api/cron/payouts/process/process-payouts.ts | Renamed deduplicationId to idempotencyKey in option parameter |
| apps/web/app/(ee)/api/cron/partner-program-summary/route.ts | Added idempotencyKey option to queueBatchEmail call |
| apps/web/app/(ee)/api/cron/merge-partner-accounts/route.ts | Added idempotencyKey option to sendBatchEmail call |
| apps/web/app/(ee)/api/cron/bounties/notify-partners/route.ts | Migrated from inline headers to using the new idempotency Key option pattern |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
@coderabbitai full review. Are there any breaking changes? |
🧩 Analysis chain🏁 Script executed: Length of output: 3783 🏁 Script executed: Length of output: 2304 🏁 Script executed: Length of output: 839 🏁 Script executed: Length of output: 1156 🏁 Script executed: Length of output: 586 🏁 Script executed: Length of output: 12573 🏁 Script executed: Length of output: 3475 🏁 Script executed: Length of output: 5601 @devkiran: I've analyzed the PR for breaking changes. Here's what I found: Breaking Changes AnalysisGood news: There are NO external breaking changes ✅ Internal API Changes (All Updated in this PR):
Why No Breaking Changes:
Files Updated in this PR:
The PR is safe to merge without concerns about breaking external dependencies. ✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (1)
packages/email/src/send-via-resend.ts (1)
74-74: Redundant expression with|| undefined.The expression
options?.idempotencyKey || undefinedis redundant. Whenoptions?.idempotencyKeyis falsy (includingundefined), it already evaluates toundefined, so the|| undefineddoesn't change the behavior.Apply this diff:
- const idempotencyKey = options?.idempotencyKey || undefined; + const idempotencyKey = options?.idempotencyKey;
🧹 Nitpick comments (1)
apps/web/lib/email/queue-batch-email.ts (1)
42-47: Idempotency key computation logic is sound.The logic correctly handles:
- Single batch → uses the key as-is (no suffix needed)
- Multiple batches → appends
-batch-${i}to create unique per-batch keys- No key provided → undefined (no deduplication)
This ensures proper deduplication for retries while avoiding collisions across different batch configurations.
Optional: The nested ternary could be flattened for readability:
- const idempotencyKey = options?.idempotencyKey - ? batches.length > 1 - ? `${options.idempotencyKey}-batch-${i}` - : options.idempotencyKey - : undefined; + const idempotencyKey = + !options?.idempotencyKey ? undefined + : batches.length > 1 ? `${options.idempotencyKey}-batch-${i}` + : options.idempotencyKey;
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (9)
apps/web/app/(ee)/api/cron/bounties/notify-partners/route.ts(1 hunks)apps/web/app/(ee)/api/cron/merge-partner-accounts/route.ts(1 hunks)apps/web/app/(ee)/api/cron/partner-program-summary/route.ts(1 hunks)apps/web/app/(ee)/api/cron/payouts/process/process-payouts.ts(1 hunks)apps/web/app/(ee)/api/cron/send-batch-email/route.ts(2 hunks)apps/web/app/(ee)/api/workflows/partner-approved/route.ts(1 hunks)apps/web/lib/email/queue-batch-email.ts(2 hunks)packages/email/src/index.ts(1 hunks)packages/email/src/send-via-resend.ts(2 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-09-17T17:44:03.965Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2857
File: apps/web/lib/actions/partners/update-program.ts:96-0
Timestamp: 2025-09-17T17:44:03.965Z
Learning: In apps/web/lib/actions/partners/update-program.ts, the team prefers to keep the messagingEnabledAt update logic simple by allowing client-provided timestamps rather than implementing server-controlled timestamp logic to avoid added complexity.
Applied to files:
apps/web/app/(ee)/api/workflows/partner-approved/route.tsapps/web/app/(ee)/api/cron/partner-program-summary/route.ts
📚 Learning: 2025-07-11T16:28:55.693Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2635
File: packages/prisma/schema/payout.prisma:24-25
Timestamp: 2025-07-11T16:28:55.693Z
Learning: In the Dub codebase, multiple payout records can now share the same stripeTransferId because payouts are grouped by partner and processed as single Stripe transfers. This is why the unique constraint was removed from the stripeTransferId field in the Payout model - a single transfer can include multiple payouts for the same partner.
Applied to files:
apps/web/app/(ee)/api/cron/payouts/process/process-payouts.ts
🔇 Additional comments (14)
apps/web/app/(ee)/api/cron/partner-program-summary/route.ts (1)
340-342: LGTM! Idempotency key format is appropriate.The idempotency key format
partner-summary/${program.id}/${format(currentMonth, "yyyy-MM")}correctly ensures monthly deduplication per program, which aligns with the cron schedule and prevents duplicate summary emails.apps/web/app/(ee)/api/cron/payouts/process/process-payouts.ts (1)
308-308: LGTM! Idempotency key format is correct.The idempotency key
payout-confirmed/${invoice.id}correctly ensures that payout confirmation emails for the same invoice are deduplicated.apps/web/app/(ee)/api/workflows/partner-approved/route.ts (1)
247-249: LGTM! Idempotency format aligned with other routes.The idempotency key now uses the slash separator format
application-approved/${programEnrollment.id}, consistent with the pattern established across other routes in this PR.apps/web/app/(ee)/api/cron/send-batch-email/route.ts (2)
49-49: LGTM! Header extraction is correct.The idempotency key is properly extracted from the
Idempotency-Keyheader and normalized to undefined when absent.
145-147: LGTM! Idempotency key propagation is correct.The idempotency key is properly forwarded to the
sendBatchEmailfunction through the options object.apps/web/app/(ee)/api/cron/bounties/notify-partners/route.ts (1)
136-138: LGTM! Idempotency key format supports pagination.The idempotency key
bounty-notify/${bountyId}-page-${page}correctly ensures deduplication per bounty and page, supporting the paginated email sending flow.apps/web/app/(ee)/api/cron/merge-partner-accounts/route.ts (1)
225-251: LGTM! Batch structure and idempotency key are correct.The restructured call properly batches both emails (sourceEmail and targetEmail) in a single
sendBatchEmailcall with an appropriate idempotency keymerge-partner-accounts/${userId}.packages/email/src/index.ts (1)
31-36: LGTM! Backward-compatible API extension.The optional
optionsparameter is properly added and forwarded to the Resend implementation. The SMTP fallback path correctly ignores idempotency since it doesn't support it.packages/email/src/send-via-resend.ts (3)
57-60: LGTM! Signature extension is backward compatible.The optional
optionsparameter properly extends the API without breaking existing callers.
66-69: LGTM! Consistent error return shape.The normalized return value
{ data: null, error: null }ensures consistency with the Resend path, improving error handling predictability.
76-79: LGTM! Conditional idempotency key handling is correct.The conditional second argument ensures the idempotency key is only passed to Resend when provided, maintaining clean API usage.
apps/web/lib/email/queue-batch-email.ts (3)
18-23: LGTM – Parameter rename improves clarity.The rename from
deduplicationIdtoidempotencyKeybetter reflects the dual purpose (QStash deduplication + Resend idempotency), and the inline comment clearly documents this behavior.Per the PR objectives, all internal callers have been updated and no external consumers exist.
49-61: Correct application of idempotency key to both systems.The computed
idempotencyKeyis correctly applied to:
- QStash deduplication (line 54) – prevents duplicate queue messages
- Resend idempotency (line 58) – prevents duplicate email sends
The conditional spread operators ensure both fields are only added when an idempotency key is provided, maintaining backward compatibility with callers that don't specify the key.
65-71: Improved logging for better observability.The structured log format with conditional
idempotencyKeyinclusion makes it easier to trace and debug deduplication behavior across batch operations.
Summary by CodeRabbit