Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@steven-tey
Copy link
Collaborator

@steven-tey steven-tey commented Nov 11, 2025

Summary by CodeRabbit

  • New Features

    • Batch-based partner notification processing with controllable batch progression and scheduling.
    • Emails now set sender using verified partner domains and include improved templating.
  • Refactor

    • Optimized queuing and idempotency to reliably enqueue subsequent batches with adaptive delays.
  • Bug Fixes

    • More reliable ordering and cursor-based batch retrieval to avoid duplicate sends.
  • Tests

    • Added coverage for an additional nullable partner date field.

@vercel
Copy link
Contributor

vercel bot commented Nov 11, 2025

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

Project Deployment Preview Updated (UTC)
dub Ready Ready Preview Nov 11, 2025 8:25pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 11, 2025

Warning

Rate limit exceeded

@steven-tey has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 4 minutes and 10 seconds before requesting another review.

βŒ› How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between d2495b0 and defe409.

πŸ“’ Files selected for processing (2)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/transactional-campaign-logic.tsx (1 hunks)
  • apps/web/ui/partners/bounties/bounty-logic.tsx (1 hunks)

Walkthrough

The bounty partner notification cron moved from page-based pagination to cursor-driven batch processing using startingAfter and batchNumber, added batch size and adaptive delay constants, altered enrollment querying/order, updated email headers/template handling, and enqueues subsequent batches with computed delays.

Changes

Cohort / File(s) Summary
Batch processing / notify route
apps/web/app/(ee)/api/cron/bounties/notify-partners/route.ts
Replaced page-based pagination with batch-based cursor (startingAfter) and batchNumber; added constants EMAIL_BATCH_SIZE, BATCH_DELAY_SECONDS, EXTENDED_DELAY_SECONDS, EXTENDED_DELAY_INTERVAL; switched ordering to id; fetches enrollments with take/skip or cursor; computes delays and enqueues next batch with updated idempotency key and logs; sets From header based on verified email domain and includes template/react content when sending.
Test resource schema
apps/web/tests/partners/resource.ts
Added trustedAt: z.string().nullish() to normalizedPartnerDateFields schema (nullable date field).

Sequence Diagram(s)

sequenceDiagram
    participant Cron as Cron Trigger
    participant API as notify-partners Route
    participant DB as Database
    participant Mail as Mailer/Resend
    participant QStash as QStash (enqueue)

    Cron->>API: POST { bountyId, startingAfter?, batchNumber? }
    API->>DB: Query bounty (include program.emailDomains) and enrollments
    DB-->>API: Return up to EMAIL_BATCH_SIZE enrollments (ordered by id)

    rect rgb(220,245,220)
    Note over API,Mail: For each enrollment: build email (From set by verified domain) and send via Mailer
    API->>Mail: sendEmail(batch payload)
    Mail-->>API: send result
    end

    API->>API: if results.length == EMAIL_BATCH_SIZE\n  compute nextStartingAfter = lastEnrollment.id\n  compute delay = (batchNumber % EXTENDED_DELAY_INTERVAL == 0) ? EXTENDED_DELAY_SECONDS : BATCH_DELAY_SECONDS

    alt More batches needed
        API->>QStash: Enqueue next batch { bountyId, startingAfter: nextStartingAfter, batchNumber+1 } with delay and idempotencyKey
        QStash-->>API: ack
    else Final/short batch
        Note over API: Do not enqueue further batches
    end

    API-->>Cron: 200 OK (batch queued / processed)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Check cursor logic and boundary conditions around startingAfter to avoid skips/duplicates.
  • Verify ordering change to id is safe for determinism and comparable to previous createdAt.
  • Validate delay calculation (especially EXTENDED_DELAY_INTERVAL modulo logic) and QStash enqueue payload/idempotencyKey format.
  • Inspect email From-selection logic against verified domain list and template/react payload correctness.

Possibly related PRs

Poem

🐰
Hops of batches, one by one,
Cursors lead where pages run.
Delays keep rhythm, gentle, sure,
Partners hear β€” the mail’s secure.
I nibble logs and hum β€” job done! πŸ₯•

Pre-merge checks and finishing touches

βœ… Passed checks (3 passed)
Check name Status Explanation
Description Check βœ… Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check βœ… Passed The title 'Improve bounty notification batching mechanism' directly reflects the main changes in the PR, which refactor the notification system from page-based to batch-based processing with new batch controls and delays.
Docstring Coverage βœ… Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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.

@steven-tey
Copy link
Collaborator Author

@coderabbitai full review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 11, 2025

βœ… Actions performed

Full review triggered.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
apps/web/app/(ee)/api/cron/bounties/notify-partners/route.ts (2)

26-29: Consider making batch configuration environment-based.

The hardcoded constants are reasonable defaults, but making them configurable via environment variables would provide flexibility for production tuning without code changes.

Example:

const EMAIL_BATCH_SIZE = parseInt(process.env.BOUNTY_BATCH_SIZE || "100", 10);
const BATCH_DELAY_SECONDS = parseInt(process.env.BOUNTY_BATCH_DELAY || "2", 10);
const EXTENDED_DELAY_SECONDS = parseInt(process.env.BOUNTY_EXTENDED_DELAY || "30", 10);
const EXTENDED_DELAY_INTERVAL = parseInt(process.env.BOUNTY_EXTENDED_INTERVAL || "25", 10);

174-179: Remove redundant check and add explanatory comment.

The batchNumber > 0 check is redundant since batchNumber defaults to 1 and is always incremented. Additionally, the throttling strategy would benefit from a brief comment.

Apply this diff:

-      // Add BATCH_DELAY_SECONDS pause between each batch, and a longer EXTENDED_DELAY_SECONDS cooldown after every EXTENDED_DELAY_INTERVAL batches.
       let delay = 0;
-      if (batchNumber > 0 && batchNumber % EXTENDED_DELAY_INTERVAL === 0) {
+      // Apply extended delay every 25 batches (at batch 25, 50, 75, etc.) to prevent rate limiting
+      if (batchNumber % EXTENDED_DELAY_INTERVAL === 0) {
         delay = EXTENDED_DELAY_SECONDS;
       } else {
         delay = BATCH_DELAY_SECONDS;
       }
πŸ“œ Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between faebf8f and 6b8a9c4.

πŸ“’ Files selected for processing (1)
  • apps/web/app/(ee)/api/cron/bounties/notify-partners/route.ts (5 hunks)
⏰ 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: api-tests
πŸ”‡ Additional comments (3)
apps/web/app/(ee)/api/cron/bounties/notify-partners/route.ts (3)

16-24: LGTM! Well-structured schema for cursor-based batching.

The addition of startingAfter for cursor pagination and batchNumber for batch tracking is clean and appropriate. The inline description on batchNumber is helpful documentation.


152-152: LGTM! Idempotency key correctly reflects cursor-based batching.

Using startingAfter || "initial" ensures each batch has a unique idempotency key, preventing duplicate email sends if the job is retried.


181-190: QStash delay parameter is correctly specified in seconds.

The web search confirms that QStash's publishJSON delay parameter expects seconds. The code correctly passes delay values as BATCH_DELAY_SECONDS=2 and EXTENDED_DELAY_SECONDS=30, which aligns with the expected unit.

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/app/(ee)/api/cron/bounties/notify-partners/route.ts (1)

168-180: Add error handling for email sending failures.

When sendBatchEmail fails and returns null data, the code silently continues without logging the failure or creating notification records. This could result in partners not being notified without any indication of failure in the logs or database.

Add explicit error handling:

     if (data) {
       await prisma.notificationEmail.createMany({
         data: programEnrollments.map(({ partner }, idx) => ({
           id: createId({ prefix: "em_" }),
           type: NotificationEmailType.Bounty,
           emailId: data.data[idx].id,
           bountyId: bounty.id,
           programId: bounty.programId,
           partnerId: partner.id,
           recipientUserId: partner.users[0].userId,
         })),
       });
+    } else {
+      await log({
+        message: `Failed to send batch emails for bounty ${bountyId}, batch ${batchNumber}. Skipping notification records.`,
+        type: "errors",
+      });
     }
♻️ Duplicate comments (1)
apps/web/app/(ee)/api/cron/bounties/notify-partners/route.ts (1)

113-123: Critical: Change orderBy from id to createdAt for reliable pagination.

This issue was flagged in a previous review and remains unfixed. The id field uses CUID, which is cryptographically random and non-sequential. Ordering by id produces unpredictable, non-chronological results that make cursor-based pagination unreliable. Records may be skipped or processed multiple times across batches.

When changing to createdAt, you'll also need to update:

  1. The orderBy clause (line 121)
  2. The cursor field (line 117)
  3. The startingAfter assignment (line 183)

Apply this diff:

-      take: EMAIL_BATCH_SIZE,
-      skip: startingAfter ? 1 : 0,
-      ...(startingAfter && {
-        cursor: {
-          id: startingAfter,
-        },
-      }),
       orderBy: {
-        id: "asc",
+        createdAt: "asc",
       },
+      take: EMAIL_BATCH_SIZE,
+      skip: startingAfter ? 1 : 0,
+      ...(startingAfter && {
+        cursor: {
+          createdAt: new Date(startingAfter),
+        },
+      }),

And update line 183:

-      startingAfter = programEnrollments[programEnrollments.length - 1].id;
+      startingAfter = programEnrollments[programEnrollments.length - 1].createdAt.toISOString();
🧹 Nitpick comments (1)
apps/web/app/(ee)/api/cron/bounties/notify-partners/route.ts (1)

182-207: Batch continuation logic is mostly correct.

The delay calculation and batch enqueueing logic works properly. Extended delays are applied every 25 batches as intended.

For clarity, consider using a new variable name instead of reassigning the parameter:

     if (programEnrollments.length === EMAIL_BATCH_SIZE) {
-      startingAfter = programEnrollments[programEnrollments.length - 1].id;
+      const nextCursor = programEnrollments[programEnrollments.length - 1].id;

       // Add BATCH_DELAY_SECONDS pause between each batch, and a longer EXTENDED_DELAY_SECONDS cooldown after every EXTENDED_DELAY_INTERVAL batches.
       let delay = 0;
       if (batchNumber > 0 && batchNumber % EXTENDED_DELAY_INTERVAL === 0) {
         delay = EXTENDED_DELAY_SECONDS;
       } else {
         delay = BATCH_DELAY_SECONDS;
       }

       await qstash.publishJSON({
         url: `${APP_DOMAIN_WITH_NGROK}/api/cron/bounties/notify-partners`,
         method: "POST",
         delay,
         body: {
           bountyId,
-          startingAfter,
+          startingAfter: nextCursor,
           batchNumber: batchNumber + 1,
         },
       });

       return logAndRespond(
-        `Enqueued next batch (${startingAfter}) for bounty ${bountyId} to run after ${delay} seconds.`,
+        `Enqueued next batch (${nextCursor}) for bounty ${bountyId} to run after ${delay} seconds.`,
       );
     }
πŸ“œ Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 6b8a9c4 and d2495b0.

πŸ“’ Files selected for processing (2)
  • apps/web/app/(ee)/api/cron/bounties/notify-partners/route.ts (7 hunks)
  • apps/web/tests/partners/resource.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (3)
πŸ“š Learning: 2025-08-16T11:14:00.667Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2754
File: apps/web/lib/partnerstack/schemas.ts:47-52
Timestamp: 2025-08-16T11:14:00.667Z
Learning: The PartnerStack API always includes the `group` field in partner responses, so the schema should use `.nullable()` rather than `.nullish()` since the field is never omitted/undefined.

Applied to files:

  • apps/web/tests/partners/resource.ts
πŸ“š 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/tests/partners/resource.ts
πŸ“š Learning: 2025-08-26T14:20:23.943Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2736
File: apps/web/app/api/workspaces/[idOrSlug]/notification-preferences/route.ts:13-14
Timestamp: 2025-08-26T14:20:23.943Z
Learning: The updateNotificationPreference action in apps/web/lib/actions/update-notification-preference.ts already handles all notification preference types dynamically, including newBountySubmitted, through its schema validation using the notificationTypes enum and Prisma's dynamic field update pattern.

Applied to files:

  • apps/web/app/(ee)/api/cron/bounties/notify-partners/route.ts
🧬 Code graph analysis (1)
apps/web/app/(ee)/api/cron/bounties/notify-partners/route.ts (1)
packages/email/src/index.ts (1)
  • sendBatchEmail (31-70)
⏰ 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: build
πŸ”‡ Additional comments (5)
apps/web/tests/partners/resource.ts (1)

32-32: LGTM: Consistent date field addition.

The trustedAt field follows the same pattern as other date fields in the schema, using .nullish() to allow both null and undefined values.

apps/web/app/(ee)/api/cron/bounties/notify-partners/route.ts (4)

16-24: LGTM: Schema appropriately extended for cursor-based pagination.

The addition of startingAfter and batchNumber fields properly supports the batch processing mechanism with clear typing and documentation.


26-29: LGTM: Well-chosen batch processing constants.

The batch size and delay configuration provides good balance between throughput and rate limiting, with extended cooldowns to prevent service exhaustion.


53-58: LGTM: Appropriate query extension for email domain support.

The nested include for program.emailDomains enables custom sender addresses from verified domains, which enhances email deliverability.


135-166: LGTM: Email domain verification and batch sending implementation.

The verified email domain lookup and conditional from address configuration enhances email deliverability. The idempotency key properly includes startingAfter to ensure each batch is uniquely identified.

@steven-tey steven-tey merged commit 362dad6 into main Nov 11, 2025
7 of 8 checks passed
@steven-tey steven-tey deleted the improve-bounty-notification branch November 11, 2025 20:30
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.

2 participants