feat: add gradual campaign delivery#436
Conversation
|
@Pankaj3112 is attempting to deploy a commit to the kmkoushik's projects Team on Vercel. A member of the Team first needs to authorize it. |
WalkthroughAdds gradual or all-at-once campaign delivery across the API, database, service layer, SDKs, scheduler, and dashboard. Campaigns store delivery and recipient-processing state, claim recipients in waves, validate claim-bound email processing, and update progress and scheduling timestamps. New analytics procedures expose audience and delivery progress. Scheduling and campaign details interfaces display gradual-delivery settings, estimates, wave progress, and completion state. Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
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.
Actionable comments posted: 2
🧹 Nitpick comments (4)
apps/web/src/server/public-api/api/campaigns/create-campaign.ts (1)
58-66: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExtract the repeated delivery-strategy mapping into a shared helper. The lowercase-request → uppercase-service
deliverymapping is duplicated byte-for-byte in three call sites; a single helper (e.g.toServiceDelivery(body.delivery)) removes divergence risk as the union evolves.
apps/web/src/server/public-api/api/campaigns/create-campaign.ts#L58-L66: replace inline mapping with the shared helper.apps/web/src/server/public-api/api/campaigns/create-campaign.ts#L79-L87: replace the identical inline mapping (schedule-on-create path) with the same helper.apps/web/src/server/public-api/api/campaigns/schedule-campaign.ts#L59-L67: replace the identical inline mapping with the same helper.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/src/server/public-api/api/campaigns/create-campaign.ts` around lines 58 - 66, The delivery strategy mapping is duplicated across three campaign API call sites and should be centralized. Add a shared helper such as toServiceDelivery(body.delivery) that preserves the current undefined, GRADUAL, and ALL_AT_ONCE mappings, then replace the inline mappings at apps/web/src/server/public-api/api/campaigns/create-campaign.ts lines 58-66 and 79-87 and apps/web/src/server/public-api/api/campaigns/schedule-campaign.ts lines 59-67 with the helper.apps/web/src/server/jobs/campaign-scheduler-job.ts (1)
43-82: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winAdd coverage for the enqueue-failure path once the swallowed-rejection bug is fixed. The scheduler's failure-reporting logic (flagged separately) is currently untested, which is why the regression went unnoticed.
apps/web/src/server/jobs/campaign-scheduler-job.ts#L43-L82: fix the.catch()-before-allSettledbug sorejected/fulfilledcounts reflect actual outcomes (see proposed diff on this file).apps/web/src/server/jobs/campaign-scheduler-job.unit.test.ts#L49-L106: add a test wheremocks.queueBatchrejects for one campaign and assert the resultingrejected/fulfilledcounts (or the correspondinglogger.errorcall) reflect that failure.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/src/server/jobs/campaign-scheduler-job.ts` around lines 43 - 82, In apps/web/src/server/jobs/campaign-scheduler-job.ts lines 43-82, update the enqueuePromises flow so CampaignBatchService.queueBatch rejections remain rejected until Promise.allSettled, while preserving failure logging without swallowing the rejection; ensure rejected and fulfilled counts reflect actual outcomes. In apps/web/src/server/jobs/campaign-scheduler-job.unit.test.ts lines 49-106, add coverage for one mocks.queueBatch rejection and assert the resulting rejected/fulfilled summary or corresponding logger.error call records the failure.apps/web/prisma/migrations/20260721120000_add_gradual_campaign_sending/migration.sql (1)
28-32: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winConsider building the
CampaignEmail/Campaignindexes concurrently.
CREATE INDEX(non-concurrent) takes a write lock for the duration of the build.CampaignEmailholds one row per recipient per campaign and can be large, so this can stall inserts/updates during deploy. Prisma wraps a migration in a transaction (blockingCONCURRENTLY), but you already have the "single-statement migration outside a transaction" pattern in20260721190000_add_campaign_email_status_index; splitting these index creations into their own migrations lets you useCREATE INDEX CONCURRENTLYand avoid the write stall.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/prisma/migrations/20260721120000_add_gradual_campaign_sending/migration.sql` around lines 28 - 32, Split the CampaignEmail and Campaign index creation from this migration into separate single-statement migrations, following the established pattern in 20260721190000_add_campaign_email_status_index. Define both indexes with CREATE INDEX CONCURRENTLY so each build runs outside Prisma’s transaction and avoids blocking writes.Source: Linters/SAST tools
apps/web/src/server/service/campaign-service.ts (1)
2020-2039: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueReuse
getGradualDeliveryWaveSizeinstead of re-deriving wave size here.This block recomputes
previouslyReleased/currentWaveSizewith the exact formula ingetGradualDeliveryWaveSize(Line 1568-1574). Two copies of the wave-sizing math can silently diverge if the batching rule ever changes. The select already returns the needed fields, so you can construct the minimal shape and delegate.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/src/server/service/campaign-service.ts` around lines 2020 - 2039, Replace the duplicated previouslyReleased/currentWaveSize calculation in the current-wave completion check with a call to getGradualDeliveryWaveSize, passing the minimal campaign fields already available from the select. Preserve the existing currentWaveComplete and nextDeliveryAt early-return behavior while centralizing wave-sizing logic in getGradualDeliveryWaveSize.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/web/src/server/jobs/campaign-scheduler-job.ts`:
- Around line 43-82: Update the enqueue flow around
CampaignBatchService.queueBatch and the enqueuePromises summary so individual
enqueue failures remain rejected for Promise.allSettled to observe. Preserve
logging of each failure with campaignId, but rethrow or otherwise propagate the
caught error after logging, allowing the existing rejected count in “Scheduler
enqueue summary” to reflect actual failures.
In `@apps/web/src/server/public-api/schemas/campaign-schema.unit.test.ts`:
- Around line 40-45: Extend the campaignScheduleSchema tests around the “accepts
the all-at-once delivery strategy” case to cover all_at_once with
batchPercentage and interval, and update the all_at_once schema branch to reject
those gradual-only fields rather than stripping them. Use strict object
validation or an equivalent refinement while preserving acceptance of valid
all_at_once payloads.
---
Nitpick comments:
In
`@apps/web/prisma/migrations/20260721120000_add_gradual_campaign_sending/migration.sql`:
- Around line 28-32: Split the CampaignEmail and Campaign index creation from
this migration into separate single-statement migrations, following the
established pattern in 20260721190000_add_campaign_email_status_index. Define
both indexes with CREATE INDEX CONCURRENTLY so each build runs outside Prisma’s
transaction and avoids blocking writes.
In `@apps/web/src/server/jobs/campaign-scheduler-job.ts`:
- Around line 43-82: In apps/web/src/server/jobs/campaign-scheduler-job.ts lines
43-82, update the enqueuePromises flow so CampaignBatchService.queueBatch
rejections remain rejected until Promise.allSettled, while preserving failure
logging without swallowing the rejection; ensure rejected and fulfilled counts
reflect actual outcomes. In
apps/web/src/server/jobs/campaign-scheduler-job.unit.test.ts lines 49-106, add
coverage for one mocks.queueBatch rejection and assert the resulting
rejected/fulfilled summary or corresponding logger.error call records the
failure.
In `@apps/web/src/server/public-api/api/campaigns/create-campaign.ts`:
- Around line 58-66: The delivery strategy mapping is duplicated across three
campaign API call sites and should be centralized. Add a shared helper such as
toServiceDelivery(body.delivery) that preserves the current undefined, GRADUAL,
and ALL_AT_ONCE mappings, then replace the inline mappings at
apps/web/src/server/public-api/api/campaigns/create-campaign.ts lines 58-66 and
79-87 and apps/web/src/server/public-api/api/campaigns/schedule-campaign.ts
lines 59-67 with the helper.
In `@apps/web/src/server/service/campaign-service.ts`:
- Around line 2020-2039: Replace the duplicated
previouslyReleased/currentWaveSize calculation in the current-wave completion
check with a call to getGradualDeliveryWaveSize, passing the minimal campaign
fields already available from the select. Preserve the existing
currentWaveComplete and nextDeliveryAt early-return behavior while centralizing
wave-sizing logic in getGradualDeliveryWaveSize.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: f4f1c24e-feaa-4e02-a067-610605050e91
📒 Files selected for processing (23)
apps/docs/api-reference/openapi.jsonapps/web/prisma/migrations/20260721120000_add_gradual_campaign_sending/migration.sqlapps/web/prisma/migrations/20260721190000_add_campaign_email_status_index/migration.sqlapps/web/prisma/schema.prismaapps/web/src/app/(dashboard)/campaigns/[campaignId]/page.tsxapps/web/src/app/(dashboard)/campaigns/schedule-campaign.tsxapps/web/src/app/(dashboard)/campaigns/toggle-pause-campaign.tsxapps/web/src/lib/campaign-delivery.tsapps/web/src/lib/campaign-delivery.unit.test.tsapps/web/src/server/api/routers/campaign-delivery.trpc.test.tsapps/web/src/server/api/routers/campaign.tsapps/web/src/server/jobs/campaign-scheduler-job.tsapps/web/src/server/jobs/campaign-scheduler-job.unit.test.tsapps/web/src/server/public-api/api/campaigns/create-campaign.tsapps/web/src/server/public-api/api/campaigns/schedule-campaign.tsapps/web/src/server/public-api/schemas/campaign-schema.tsapps/web/src/server/public-api/schemas/campaign-schema.unit.test.tsapps/web/src/server/service/campaign-service.tsapps/web/src/server/service/campaign-service.unit.test.tspackages/python-sdk/README.mdpackages/python-sdk/tests/test_resources.pypackages/python-sdk/usesend/types.pypackages/sdk/types/schema.d.ts
| it("accepts the all-at-once delivery strategy", () => { | ||
| const result = campaignScheduleSchema.safeParse({ | ||
| delivery: { strategy: "all_at_once" }, | ||
| }); | ||
|
|
||
| expect(result.success).toBe(true); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate the schema and its tests
git ls-files 'apps/web/src/server/public-api/**' | sed -n '1,200p'
# Show the relevant test file with line numbers
wc -l apps/web/src/server/public-api/schemas/campaign-schema.unit.test.ts
cat -n apps/web/src/server/public-api/schemas/campaign-schema.unit.test.ts | sed -n '1,220p'
# Find the schema implementation and any related delivery strategy definitions
rg -n "campaignScheduleSchema|all_at_once|batchPercentage|interval|delivery" apps/web/src/server/public-api -g '!**/*.map'
# If the schema is in a nearby file, show it with line numbers
for f in $(rg -l "campaignScheduleSchema|all_at_once|batchPercentage|interval|delivery" apps/web/src/server/public-api -g '!**/*.map' | head -n 5); do
echo "===== $f ====="
wc -l "$f"
cat -n "$f" | sed -n '1,260p'
doneRepository: usesend/useSend
Length of output: 21265
Reject gradual-only fields for all_at_once. Add a case with strategy: "all_at_once" plus batchPercentage/interval; the all_at_once branch is a non-strict z.object, so those keys are stripped and the payload still parses. If that input should be invalid, make the branch strict or add a refinement.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/web/src/server/public-api/schemas/campaign-schema.unit.test.ts` around
lines 40 - 45, Extend the campaignScheduleSchema tests around the “accepts the
all-at-once delivery strategy” case to cover all_at_once with batchPercentage
and interval, and update the all_at_once schema branch to reject those
gradual-only fields rather than stripping them. Use strict object validation or
an equivalent refinement while preserving acceptance of valid all_at_once
payloads.
Closes #389
Summary
Impact
Campaign senders can throttle large sends into predictable waves instead of queueing the entire audience at once. Existing campaigns retain all-at-once behavior by default.
This implements fixed-percentage interval throttling and the underlying resumable delivery path. Adaptive ramp-up strategies and daily-volume caps are outside this initial scope.
Screenshots
Migrations
ALL_AT_ONCEand treat existing campaign recipients as already queuedEmail(campaignId, latestStatus)index in a separate one-statement migration usingCREATE INDEX CONCURRENTLYBoth feature migrations were deployed successfully from an empty PostgreSQL database as part of the integration test run.
Verification
pnpm test:web— 181 tests passedpnpm test:web:integration:full— all 43 migrations applied from an empty database and 11 integration tests passedpnpm --filter=web typecheckpnpm build— all workspace builds passedgit diff --checkManual QA
Summary by cubic
Adds gradual campaign delivery with fixed-percentage batches on minute/hour intervals, plus pause/resume, scheduling previews, and live progress. Keeps existing campaigns all at once by default and exposes settings in the dashboard and Public API.
New Features
Migration
ALL_AT_ONCE.Written for commit db0a28b. Summary will update on new commits.
Summary by CodeRabbit