-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Improve ranking algorithm #3085
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.
|
WalkthroughAdds a Multi-Program Success Bonus (exponential, capped) and multiProgramSuccess metric into partner ranking, updates finalScore composition and SQL projections to include the bonus, and introduces a PartnerStarButton component replacing inline star toggles in UI components. Changes
Sequence Diagram(s)sequenceDiagram
participant QB as Query Builder
participant Joins as Metrics Joins
participant BonusCalc as Multi-Program Bonus Calc
participant ScoreAgg as Final Score Aggregator
participant Discover as Discover Projection/Sort
Note over QB,Joins: New joins include multiProgramSuccessJoin
QB->>Joins: join similarity, programMatch, multiProgramSuccess
Joins->>BonusCalc: provide commissions, onlinePresence, program counts
BonusCalc-->>Joins: compute exponential, capped bonus
Joins->>ScoreAgg: similarityScore, programMatchScore, multiProgramSuccessBonus
ScoreAgg->>ScoreAgg: finalScore = multiProgramSuccessBonus + similarity + programMatch
ScoreAgg->>Discover: SELECT projections include multiProgramSuccessBonus and finalScore
rect rgba(63,81,181,0.08)
Note over Discover: Discover sorting:
Note over Discover: - starred: dp.starredAt → finalScore → profile presence
Note over Discover: - non-starred: finalScore prioritized earlier
end
sequenceDiagram
participant UI as PartnerStarButton
participant Cache as SWR Cache (/api/network/partners*)
participant API as updateDiscoveredPartner
participant Toast as Toast/Error
UI->>Cache: optimistic mutate set starredAt (now or null)
UI->>API: call updateDiscoveredPartner(workspaceId, partnerId, starred)
API-->>UI: success (returns server starredAt)
UI->>Cache: apply server starredAt to specific partner cache entry
API-->>UI: failure
UI->>Toast: show error
UI->>Cache: mutatePrefix to revalidate/revert optimistic changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes
Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 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.
Actionable comments posted: 0
🧹 Nitpick comments (2)
apps/web/lib/api/network/calculate-partner-ranking.ts (2)
191-191: Document the rationale for excluding LARGE_PROGRAM_IDS.Line 191 filters out
LARGE_PROGRAM_IDSfrom the multi-program success calculation, but unlike the exclusion ofACME_PROGRAM_ID(which is documented elsewhere), there's no explanation for why large programs are excluded. This could be intentional to prevent bias from programs with many partners, but the reasoning should be documented.Add a comment explaining the exclusion:
WHERE pe_mult.programId != ${ACME_PROGRAM_ID} + -- Exclude large programs to prevent bias in multi-program success scoring AND pe_mult.programId NOT IN (${Prisma.join(LARGE_PROGRAM_IDS, ",")})
172-180: Consider adding a brief comment explaining the MAX aggregation pattern.The use of
MAX(p_filter_mult.website)to check for online presence is correct but may be unintuitive. This pattern is necessary due to theGROUP BY partnerIdclause. A brief inline comment could help future maintainers understand this choice.Example:
CASE WHEN ( + -- Using MAX because of GROUP BY; returns field value if exists, NULL otherwise MAX(p_filter_mult.website) IS NOT NULL OR
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
apps/web/lib/api/network/calculate-partner-ranking.ts(6 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
apps/web/lib/api/network/calculate-partner-ranking.ts (1)
apps/web/lib/constants/program.ts (1)
LARGE_PROGRAM_IDS(2-5)
⏰ 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 (6)
apps/web/lib/api/network/calculate-partner-ranking.ts (6)
24-44: Excellent documentation of the new scoring system!The detailed breakdown with concrete examples (2 programs = 50, 3 = 87, etc.) makes the exponential scaling immediately clear. The 0-265+ point range is well-explained and matches the implementation.
109-110: LGTM: Sorting logic correctly prioritizes the enhanced finalScore.The updated ORDER BY clauses appropriately surface high-performing partners while maintaining logical tiebreakers (starredAt, profile presence, ID).
145-148: LGTM: Filter definition follows established pattern.The new multi-program filter correctly reuses the helper function and maintains consistency with the optimization strategy.
182-182: Verify the use of totalCommissions vs totalConversions is intentional.The multi-program bonus uses
totalCommissions > 0as the success metric (lines 182, 194), while the all-program metrics join usestotalConversions > 0(line 160). This difference may be intentional—commissions represent actual revenue and could be a stronger signal of quality than conversions alone. Please confirm this distinction is deliberate.Also applies to: 194-194
272-279: LGTM: Final score calculation is correct and well-documented.The calculation properly integrates the new multi-program bonus with proper NULL handling. The order (Bonus + Similarity + Match) reflects the intended priority, and the maximum of 265 points is achievable (200 + 50 + 15).
299-300: LGTM: Join integration is correct.The
multiProgramSuccessJoinis properly integrated into the query structure, maintaining consistency with other metric joins.
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: 1
🧹 Nitpick comments (3)
apps/web/ui/partners/partner-star-button.tsx (1)
40-55: Consider narrowing the cache key matching pattern.The current pattern
key.startsWith("/api/network/partners")is broad and will match all partner-related cache entries, including potentially unrelated endpoints like/api/network/partners/statsor/api/network/partners/[id]/details. This could lead to unintended cache updates.If there are specific query parameter patterns you expect, consider a more precise matcher:
mutate( (key) => - typeof key === "string" && key.startsWith("/api/network/partners"), + typeof key === "string" && + key.startsWith("/api/network/partners") && + key.includes("workspaceId="), (data: any) => {Alternatively, if all
/api/network/partners*endpoints return arrays of partners, the current approach is acceptable.apps/web/lib/api/network/calculate-partner-ranking.ts (2)
173-177: Consider JOIN instead of IN subquery for better performance.The subquery pattern (lines 173-177) filters discoverable partners using
WHERE partnerId IN (SELECT ...). While this works, a direct INNER JOIN on the Partner table might perform better and allow MySQL to optimize the query plan more effectively, similar to the pattern used insimilarProgramMetricsJoin(line 224).FROM ProgramEnrollment pe_mult - -- OPTIMIZATION: Only process enrollments for discoverable partners (using subquery to avoid JOIN) - WHERE pe_mult.partnerId IN ( - SELECT p_filter_mult.id - FROM Partner p_filter_mult - WHERE ${discoverablePartnersMultiProgramFilter} - ) + -- OPTIMIZATION: Only process enrollments for discoverable partners + INNER JOIN Partner p_filter_mult ON p_filter_mult.id = pe_mult.partnerId + AND ${discoverablePartnersMultiProgramFilter} + WHERE TRUE AND pe_mult.programId != ${ACME_PROGRAM_ID}This aligns with your existing optimization pattern and may benefit from better index usage.
264-271: Profile check in SELECT clause prevents index optimization.The
hasProfileCheckcondition (line 266) is evaluated in the SELECT clause, which means the multi-program bonus calculation happens for every row before sorting. This prevents MySQL from using the bonus calculation to optimize the query plan or early-exit on sorting.For better performance, consider computing the bonus within the
multiProgramSuccessJoinsubquery wherehasProfileCheckcan be evaluated earlier:const multiProgramSuccessJoin = Prisma.sql`LEFT JOIN ( SELECT pe_mult.partnerId, - -- Count of successful programs (based on commissions) - COUNT(DISTINCT CASE WHEN pe_mult.totalCommissions > 0 THEN pe_mult.programId END) as successfulProgramCount + COUNT(DISTINCT CASE WHEN pe_mult.totalCommissions > 0 THEN pe_mult.programId END) as successfulProgramCount, + -- Pre-compute bonus based on profile check in subquery + CASE + WHEN ${hasProfileCheck} AND COUNT(...) >= 2 THEN + LEAST(200, POWER(COUNT(...) - 1, 1.5) * 20 + 30) + ELSE 0 + END as multiProgramBonus FROM ProgramEnrollment pe_mult + INNER JOIN Partner p_mult_profile ON p_mult_profile.id = pe_mult.partnerIdHowever, this adds another JOIN and might not improve performance. Profile this change before applying.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/network/page-client.tsx(3 hunks)apps/web/lib/api/network/calculate-partner-ranking.ts(6 hunks)apps/web/ui/partners/partner-info-cards.tsx(2 hunks)apps/web/ui/partners/partner-star-button.tsx(1 hunks)
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-09-24T16:10:37.349Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/ui/partners/partner-about.tsx:11-11
Timestamp: 2025-09-24T16:10:37.349Z
Learning: In the Dub codebase, the team prefers to import Icon as a runtime value from "dub/ui" and uses Icon as both a type and variable name in component props, even when this creates shadowing. This is their established pattern and should not be suggested for refactoring.
Applied to files:
apps/web/ui/partners/partner-info-cards.tsxapps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/network/page-client.tsx
📚 Learning: 2025-10-15T01:05:43.266Z
Learnt from: steven-tey
Repo: dubinc/dub PR: 2958
File: apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx:432-457
Timestamp: 2025-10-15T01:05:43.266Z
Learning: In apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx, defer refactoring the custom MenuItem component (lines 432-457) to use the shared dub/ui MenuItem component to a future PR, as requested by steven-tey.
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/network/page-client.tsx
📚 Learning: 2025-09-19T18:46:43.787Z
Learnt from: CR
Repo: dubinc/dub PR: 0
File: packages/hubspot-app/CLAUDE.md:0-0
Timestamp: 2025-09-19T18:46:43.787Z
Learning: Applies to packages/hubspot-app/app/cards/**/*.{js,jsx,ts,tsx} : Only use components exported by hubspot/ui-extensions within card components
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/network/page-client.tsx
📚 Learning: 2025-05-29T04:45:18.504Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2448
File: packages/email/src/templates/partner-program-summary.tsx:0-0
Timestamp: 2025-05-29T04:45:18.504Z
Learning: In the PartnerProgramSummary email template (packages/email/src/templates/partner-program-summary.tsx), the stat titles are hardcoded constants ("Clicks", "Leads", "Sales", "Earnings") that will always match the ICONS object keys after toLowerCase() conversion, so icon lookup failures are not possible.
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/network/page-client.tsx
🧬 Code graph analysis (4)
apps/web/ui/partners/partner-star-button.tsx (3)
apps/web/lib/types.ts (1)
NetworkPartnerProps(455-455)apps/web/lib/swr/use-workspace.ts (1)
useWorkspace(6-46)apps/web/lib/actions/partners/update-discovered-partner.ts (1)
updateDiscoveredPartnerAction(10-46)
apps/web/ui/partners/partner-info-cards.tsx (1)
apps/web/ui/partners/partner-star-button.tsx (1)
PartnerStarButton(19-106)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/network/page-client.tsx (1)
apps/web/ui/partners/partner-star-button.tsx (1)
PartnerStarButton(19-106)
apps/web/lib/api/network/calculate-partner-ranking.ts (1)
apps/web/lib/constants/program.ts (1)
LARGE_PROGRAM_IDS(2-5)
🔇 Additional comments (5)
apps/web/ui/partners/partner-info-cards.tsx (1)
182-184: LGTM! Clean integration of the star button.The conditional rendering and positioning of
PartnerStarButtonis consistent with the existingPartnerStatusBadgeWithTooltippattern for enrolled partners. Thesize-9className appropriately sizes the button for the card layout.apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/network/page-client.tsx (1)
440-445: LGTM! Effective refactoring to use the shared component.The migration from an inline Button to
PartnerStarButtonmaintains all existing behavior through theonToggleStarredcallback. The custom mutation logic (lines 256-293) with optimistic updates and populateCache is preserved, which is appropriate for this page's specific SWR data management needs.apps/web/lib/api/network/calculate-partner-ranking.ts (2)
109-110: Verify order-by behavior with starred partners.The updated order clause for starred discover status now prioritizes
starredAt DESCbeforefinalScore DESC(line 109). This means more recently starred partners appear first, regardless of their score. Is this the intended behavior?If the goal is to show highest-scored starred partners first (ignoring when they were starred), the order should be:
- ? Prisma.sql`dp.starredAt DESC, finalScore DESC, ${hasProfileCheck} DESC, p.id ASC` + ? Prisma.sql`finalScore DESC, ${hasProfileCheck} DESC, p.id ASC`Otherwise, if recency of starring is important for user workflow (e.g., "I just starred this to review later"), the current order is correct.
166-183: Verify multi-program join performance in staging with production-like data.The code structure appears sound with deliberate optimizations (subquery approach to avoid expensive joins, proper filtering), but actual performance impact cannot be determined without database-level verification. The sandbox environment lacks database access for EXPLAIN plan analysis.
Recommend testing the full ranking query in a staging environment with production-like data volumes to confirm performance characteristics, particularly for scenarios with high volumes of ProgramEnrollment records and diverse partner counts.
apps/web/ui/partners/partner-star-button.tsx (1)
58-63: Add null check before API call.The non-null assertion on
workspaceId!(line 60) could cause a runtime error ifuseWorkspacereturns undefined. While the Button is only rendered when a partner exists, the workspace might still be loading.Apply this diff to add a guard:
try { + if (!workspaceId) { + toast.error("Workspace not found"); + mutatePrefix("/api/network/partners"); + return; + } + const result = await updateDiscoveredPartner({ - workspaceId: workspaceId!, + workspaceId, partnerId: partner.id, starred, });⛔ Skipped due to learnings
Learnt from: TWilson023 Repo: dubinc/dub PR: 2538 File: apps/web/ui/partners/overview/blocks/commissions-block.tsx:16-27 Timestamp: 2025-06-18T20:26:25.177Z Learning: In the Dub codebase, components that use workspace data (workspaceId, defaultProgramId) are wrapped in `WorkspaceAuth` which ensures these values are always available, making non-null assertions safe. This is acknowledged as a common pattern in their codebase, though not ideal.Learnt from: TWilson023 Repo: dubinc/dub PR: 2736 File: apps/web/lib/swr/use-bounty.ts:11-16 Timestamp: 2025-08-26T15:05:55.081Z Learning: In the Dub codebase, workspace authentication and route structures prevent endless loading states when workspaceId or similar route parameters are missing, so gating SWR loading states on parameter availability is often unnecessary.
| } catch (error) { | ||
| // Revert optimistic update on error | ||
| mutatePrefix("/api/network/partners"); | ||
| } |
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.
Silent error handling may hide issues.
The catch block (lines 85-88) only reverts the optimistic update without notifying the user. This could leave users unaware that their action failed, especially for network errors or unexpected failures not caught by the !result?.data check.
Apply this diff to add user feedback:
} catch (error) {
+ toast.error("Failed to star partner");
// Revert optimistic update on error
mutatePrefix("/api/network/partners");
}Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In apps/web/ui/partners/partner-star-button.tsx around lines 85 to 88 the catch
block only reverts the optimistic update and swallows the error; update the
catch to (1) call mutatePrefix("/api/network/partners") to revert the optimistic
change, (2) extract a usable message from the caught error (error.message or a
fallback), and (3) surface that message to the user via the app's notification
mechanism (e.g., toast or setError state) so users see the failure and any
useful detail for retrying.
Summary by CodeRabbit
New Features
UX / UI Improvements