-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Schedule bounty email for newly-joined partners #2834
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.
|
|
Warning Rate limit exceeded@devkiran has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 14 minutes and 35 seconds before requesting another review. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the 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. 📒 Files selected for processing (4)
WalkthroughAdds partner-scoped bounty notifications: cron endpoint accepts optional Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor Admin
participant App as Web App
participant Helper as getBountiesByGroup
participant Q as QStash
participant Cron as /api/cron/bounties/notify-partners
participant DB as Prisma
rect rgba(200,230,255,0.25)
note over Admin,App: Partner approval (bulk or single)
Admin->>App: Approve partner(s)
App->>Helper: getBountiesByGroup(programId, groupIds)
Helper->>DB: Query active bounties by group/time
DB-->>Helper: Bounties by group
Helper-->>App: Map groupId -> [bounty]
loop For each bounty/group/partner set
App->>Q: publishJSON({ bountyId, partnerIds }, notBefore=+1h, url=APP_DOMAIN_WITH_NGROK/.../notify-partners)
end
end
rect rgba(220,255,220,0.25)
note over Q,Cron: Deferred notification processing
Q-->>Cron: POST { bountyId, partnerIds?, page }
Cron->>DB: Fetch bounty
alt partnerIds provided
Cron->>DB: programEnrollment where partnerId IN partnerIds
else no partnerIds
Cron->>DB: programEnrollment for bounty/group
end
Cron-->>Q: Enqueue next page if MAX_PAGE_SIZE reached (page+1)
Cron->>App: Send emails to selected partners
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Pre-merge checks (2 passed, 1 warning)❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
Poem
✨ Finishing touches
🧪 Generate unit tests
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.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
apps/web/app/(ee)/api/cron/bounties/notify-partners/route.ts (2)
142-149: Fix pagination filter leak: propagate partnerIds to next pageWhen
partnerIdsis provided, the next page omits it and will email all eligible partners. This is a severe privacy/notification bug.const res = await qstash.publishJSON({ url: `${APP_DOMAIN_WITH_NGROK}/api/cron/bounties/notify-partners`, body: { bountyId, - page: page + 1, + page: page + 1, + ...(partnerIds?.length ? { partnerIds } : {}), }, });
56-63: Start-time gating can drop notifications; re-enqueue until startsAtIf the bounty hasn’t started, the handler returns early without re-enqueuing, so notifications may never send. Requeue for
startsAt(plus a small buffer).- let diffMinutes = differenceInMinutes(bounty.startsAt, new Date()); - - if (diffMinutes >= 10) { - return logAndRespond( - `Bounty ${bountyId} not started yet, it will start at ${bounty.startsAt.toISOString()}`, - ); - } + const diffMinutes = differenceInMinutes(bounty.startsAt, new Date()); + if (diffMinutes > 0) { + await qstash.publishJSON({ + url: `${APP_DOMAIN_WITH_NGROK}/api/cron/bounties/notify-partners`, + body: { bountyId, page: 0, ...(partnerIds?.length ? { partnerIds } : {}) }, + notBefore: Math.floor(bounty.startsAt.getTime() / 1000) + 60, // run ~1 min after start + }); + return logAndRespond( + `Bounty ${bountyId} not started yet; re-enqueued for ${bounty.startsAt.toISOString()}.`, + ); + }
🧹 Nitpick comments (5)
apps/web/lib/api/bounties/get-bounties-by-group.ts (1)
16-54: Tighten Prisma filter for readability and index-friendlinessThe nested
AND: [{ OR: [...] }]is redundant. Flattening improves readability and can help query planners. Functionality is unchanged.- // Only fetch bounties that are relevant to the specified groups - AND: [ - { - OR: [ - { - groups: { - none: {}, - }, - }, - { - groups: { - some: { - groupId: { - in: targetGroupIds, - }, - }, - }, - }, - ], - }, - ], + // Only bounties with no groups or intersecting target groups + OR: [ + { groups: { none: {} } }, + { groups: { some: { groupId: { in: targetGroupIds } } } }, + ],apps/web/app/(ee)/api/cron/bounties/notify-partners/route.ts (2)
118-140: Strengthen idempotency key to avoid collisions across subsetsThe key ignores
partnerIds, so runs with different subsets can collide. Include a stable subset hash.Minimal change (non-cryptographic, acceptable if subsets are rare):
headers: { - "Idempotency-Key": `${bountyId}-page-${page}`, + "Idempotency-Key": `${bountyId}-page-${page}${partnerIds?.length ? `-subset` : `-all`}`, },Prefer hashing the subset for uniqueness:
// Near parse line, compute once: const subsetKey = partnerIds?.length ? `-${partnerIds.slice().sort().join(",")}` : "-all";and then:
- "Idempotency-Key": `${bountyId}-page-${page}`, + "Idempotency-Key": `${bountyId}-page-${page}${subsetKey}`,
160-166: Safely log unknown errors
errorisunknown. Narrow toErrorbefore accessing.message.- await log({ - message: "New bounties published cron failed. Error: " + error.message, + const msg = + error instanceof Error ? error.message : JSON.stringify(error); + await log({ + message: "New bounties published cron failed. Error: " + msg, type: "errors", });apps/web/lib/partners/approve-partner-enrollment.ts (1)
177-182: Optionally include startsAt in helper to schedule closer to go-liveIf you want to avoid the cron handler re-queues, extend
getBountiesByGroupto selectstartsAtand schedulenotBefore = max(now+1h, startsAt+60s).apps/web/lib/actions/partners/bulk-approve-partners.ts (1)
195-206: Chunk partnerIds to keep QStash payloads small and resilientLarge groups can produce big payloads. Chunking reduces risk of hitting request size/time limits and eases retries.
- for (const bounty of bounties) { - bountyNotificationPromises.push( - qstash.publishJSON({ - url: `${APP_DOMAIN_WITH_NGROK}/api/cron/bounties/notify-partners`, - body: { - bountyId: bounty.id, - partnerIds: partners.map((p) => p.id), - }, - notBefore: - Math.floor(new Date().getTime() / 1000) + 1 * 60 * 60, // 1 hour from now - }), - ); - } + for (const bounty of bounties) { + for (const partnerIdChunk of chunk( + partners.map((p) => p.id), + 500, + )) { + bountyNotificationPromises.push( + qstash.publishJSON({ + url: `${APP_DOMAIN_WITH_NGROK}/api/cron/bounties/notify-partners`, + body: { + bountyId: bounty.id, + partnerIds: partnerIdChunk, + }, + notBefore: + Math.floor(Date.now() / 1000) + 60 * 60, // 1 hour from now + }), + ); + } + }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
apps/web/app/(ee)/api/cron/bounties/notify-partners/route.ts(4 hunks)apps/web/lib/actions/partners/bulk-approve-partners.ts(3 hunks)apps/web/lib/api/bounties/get-bounties-by-group.ts(1 hunks)apps/web/lib/partners/approve-partner-enrollment.ts(3 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-08-26T15:38:48.173Z
Learnt from: TWilson023
PR: dubinc/dub#2736
File: apps/web/lib/api/bounties/get-bounty-or-throw.ts:53-63
Timestamp: 2025-08-26T15:38:48.173Z
Learning: In bounty-related code, getBountyOrThrow returns group objects with { id } field (transformed from BountyGroup.groupId), while other routes working directly with BountyGroup Prisma records use the actual groupId field. This is intentional - getBountyOrThrow abstracts the join table details.
Applied to files:
apps/web/lib/api/bounties/get-bounties-by-group.tsapps/web/lib/partners/approve-partner-enrollment.ts
📚 Learning: 2025-08-14T05:17:51.825Z
Learnt from: devkiran
PR: dubinc/dub#2735
File: apps/web/lib/actions/partners/delete-reward.ts:33-41
Timestamp: 2025-08-14T05:17:51.825Z
Learning: In the partner groups system, a rewardId can only belong to one group, establishing a one-to-one relationship between rewards and groups. This means using Prisma's `update` method (rather than `updateMany`) is appropriate when updating groups by rewardId.
Applied to files:
apps/web/app/(ee)/api/cron/bounties/notify-partners/route.ts
🧬 Code graph analysis (2)
apps/web/lib/partners/approve-partner-enrollment.ts (2)
apps/web/lib/api/bounties/get-bounties-by-group.ts (1)
getBountiesByGroup(9-90)packages/utils/src/constants/main.ts (1)
APP_DOMAIN_WITH_NGROK(20-25)
apps/web/lib/actions/partners/bulk-approve-partners.ts (2)
apps/web/lib/api/bounties/get-bounties-by-group.ts (1)
getBountiesByGroup(9-90)packages/utils/src/constants/main.ts (1)
APP_DOMAIN_WITH_NGROK(20-25)
⏰ 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: Vade Review
🔇 Additional comments (1)
apps/web/lib/actions/partners/bulk-approve-partners.ts (1)
164-171: LGTM on helper usageFetching once and reusing the map across groups is clean and efficient.
|
@coderabbitai full review |
✅ Actions performedFull review triggered. |
Summary by CodeRabbit
New Features
Bug Fixes / Notes