-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Fraud detection #2688
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
base: main
Are you sure you want to change the base?
Fraud detection #2688
Conversation
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
WalkthroughThis update introduces a comprehensive fraud detection and risk management system across the application. It adds new Prisma schema models and fields for tracking fraud events, implements detection logic for self-referrals, Google Ads clicks, and disposable emails, and integrates asynchronous fraud detection and recording into partner-related workflows. The frontend gains new APIs, React components, hooks, and UI elements for displaying, filtering, and managing fraud events, including status badges, modals for marking events as safe or banned, and a detailed risk review sheet. Changes
Sequence Diagram(s)Fraud Detection and Recording FlowsequenceDiagram
participant Workflow (Lead/Sale/Integration)
participant detectAndRecordFraud
participant DB (Prisma)
participant detectFraudEvents
participant isSelfReferral/isGoogleAdsClick/isDisposableEmail
Workflow->>detectAndRecordFraud: Trigger with partner, customer, click, commission info
detectAndRecordFraud->>DB: Fetch program enrollment & partner user IP
alt Enrollment is trusted
detectAndRecordFraud-->>Workflow: Skip fraud detection
else Not trusted
detectAndRecordFraud->>detectFraudEvents: Run fraud checks
detectFraudEvents->>isSelfReferral: Check self-referral
detectFraudEvents->>isGoogleAdsClick: Check Google Ads click
detectFraudEvents->>isDisposableEmail: Check disposable email
detectFraudEvents-->>detectAndRecordFraud: Return fraud events
alt Fraud events found
detectAndRecordFraud->>DB: Upsert FraudEvent, link commission, set status "held"
else No fraud events
detectAndRecordFraud-->>Workflow: Log and exit
end
end
Fraud Event Review and Resolution (Frontend)sequenceDiagram
participant User
participant FraudEventTable
participant RiskReviewSheet
participant markFraudEventSafe/BannedModal
participant API (mark-fraud-event-safe/banned)
participant DB
User->>FraudEventTable: Click "Review risk"
FraudEventTable->>RiskReviewSheet: Open with fraud event details
User->>RiskReviewSheet: Click "Mark as Safe" or "Ban Partner"
RiskReviewSheet->>markFraudEventSafe/BannedModal: Open modal
User->>markFraudEventSafe/BannedModal: Confirm action
markFraudEventSafe/BannedModal->>API: Submit resolution
API->>DB: Update FraudEvent status, commissions, enrollment
API-->>markFraudEventSafe/BannedModal: Success/Failure
markFraudEventSafe/BannedModal-->>RiskReviewSheet: Close modal, refresh data
Estimated code review effort🎯 5 (Critical) | ⏱️ ~90 minutes Poem
📜 Recent review detailsConfiguration used: CodeRabbit UI ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
⏰ 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)
✨ 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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
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: 6
🧹 Nitpick comments (3)
apps/web/lib/analytics/fraud/is-google-ads-click.ts (1)
3-15
: Simplify the boolean return logic.The function logic is correct, but the explicit if-else structure can be simplified for better readability.
export const isGoogleAdsClick = (url: string) => { const urlObj = getUrlObjFromString(url); if (!urlObj) { return false; } - if (urlObj.searchParams.has("gclid")) { - return true; - } - - return false; + return urlObj.searchParams.has("gclid"); };apps/web/scripts/get-top-email-domains.ts (1)
13-18
: Optimize database query by removing unused fields.The script only uses the
name
andcreatedAt
which are not used, potentially impacting performance.const users = await prisma.user.findMany({ select: { email: true, - name: true, - createdAt: true, }, skip, take: CHUNK_SIZE, });apps/web/lib/analytics/fraud/is-self-referral.ts (1)
203-213
: Consider implementing or removing disposable email check.There's commented-out code for checking disposable email domains. Either implement this feature completely or remove the code to avoid confusion:
- // Check for disposable email domains - // const { domain: customerDomain } = destructureEmail(customer.email); - // const isCustomerDisposable = await redis.sismember( - // "disposableEmailDomains", - // customerDomain, - // ); - - // if (isCustomerDisposable) { - // confidence += 0.3; - // reasons.push("Customer email uses disposable domain"); - // } + // Check for disposable email domains + const { domain: customerDomain } = destructureEmail(customer.email); + const isCustomerDisposable = await redis.sismember( + "disposableEmailDomains", + customerDomain, + ); + + if (isCustomerDisposable) { + confidence += 0.3; + reasons.push("Customer email uses disposable domain"); + }Note: This would require importing
redis
and ensuring the disposable domains set is populated.Would you like help implementing the disposable email detection feature?
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (10)
apps/web/lib/analytics/fraud/is-google-ads-click.ts
(1 hunks)apps/web/lib/analytics/fraud/is-self-referral.ts
(1 hunks)apps/web/lib/api/conversions/track-lead.ts
(2 hunks)apps/web/lib/emails.ts
(1 hunks)apps/web/scripts/get-top-email-domains.ts
(1 hunks)packages/prisma/schema/customer.prisma
(1 hunks)packages/prisma/schema/fraud.prisma
(1 hunks)packages/prisma/schema/link.prisma
(1 hunks)packages/prisma/schema/partner.prisma
(1 hunks)packages/prisma/schema/program.prisma
(1 hunks)
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: devkiran
PR: dubinc/dub#2637
File: apps/web/app/(ee)/api/singular/webhook/route.ts:0-0
Timestamp: 2025-07-17T06:41:45.620Z
Learning: In the Singular integration (apps/web/app/(ee)/api/singular/webhook/route.ts), the event names in the singularToDubEvent object have intentionally different casing: "Copy GAID" and "copy IDFA". This casing difference is valid and should not be changed, as these are the correct event names expected from Singular.
packages/prisma/schema/link.prisma (1)
Learnt from: devkiran
PR: #2177
File: apps/web/lib/api/links/bulk-create-links.ts:66-84
Timestamp: 2025-06-06T07:59:03.120Z
Learning: In apps/web/lib/api/links/bulk-create-links.ts, the team accepts the risk of potential undefined results from links.find() operations when building invalidLinks arrays, because existing links are fetched from the database based on the input links, so matches are expected to always exist.
apps/web/lib/api/conversions/track-lead.ts (2)
Learnt from: devkiran
PR: #2637
File: apps/web/app/(ee)/api/singular/webhook/route.ts:0-0
Timestamp: 2025-07-17T06:41:45.620Z
Learning: In the Singular integration (apps/web/app/(ee)/api/singular/webhook/route.ts), the event names in the singularToDubEvent object have intentionally different casing: "Copy GAID" and "copy IDFA". This casing difference is valid and should not be changed, as these are the correct event names expected from Singular.
Learnt from: devkiran
PR: #2177
File: apps/web/lib/api/links/bulk-create-links.ts:66-84
Timestamp: 2025-06-06T07:59:03.120Z
Learning: In apps/web/lib/api/links/bulk-create-links.ts, the team accepts the risk of potential undefined results from links.find() operations when building invalidLinks arrays, because existing links are fetched from the database based on the input links, so matches are expected to always exist.
⏰ 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 (8)
apps/web/lib/emails.ts (1)
1-24
: Excellent refactoring that improves maintainability and performance.The refactoring from multiple hardcoded
endsWith
checks to an array-based lookup provides several benefits:
- Maintainability: Centralized domain list is easier to update
- Performance: Single
some()
call is more efficient than multipleendsWith
checks- Reusability: Exported
genericEmailDomains
array can be used elsewhere- Coverage: Expanded domain list improves fraud detection accuracy
The implementation correctly handles the domain matching logic.
packages/prisma/schema/partner.prisma (1)
58-58
: Schema change approved: FraudEvent model and relations verifiedThe
Partner
model’s newfraudEvents FraudEvent[]
field correctly pairs with thepartner Partner @relation(fields: [partnerId], references: [id])
in theFraudEvent
model. All relation fields and indexes inpackages/prisma/schema/fraud.prisma
are properly defined.
- No further changes needed.
- The one-to-many relationship is fully established.
packages/prisma/schema/program.prisma (1)
64-64
: Schema relation correctly implemented.The
fraudEvents
relation field is properly added to the Program model, maintaining consistency with the Partner model changes and supporting comprehensive fraud event tracking across the system.packages/prisma/schema/customer.prisma (1)
25-26
: LGTM! Schema relation properly configured.The
fraudEvents
relation field correctly establishes a one-to-many relationship from Customer to FraudEvent, consistent with the fraud detection infrastructure being introduced.packages/prisma/schema/link.prisma (1)
83-83
: LGTM! Relation field properly added.The
fraudEvents
relation correctly establishes a one-to-many relationship from Link to FraudEvent, consistent with the fraud tracking requirements.apps/web/lib/api/conversions/track-lead.ts (1)
1-2
: Fraud detection utilities properly imported.The imports for
isGoogleAdsClick
andisSelfReferral
utilities are correctly added to support fraud detection functionality.packages/prisma/schema/fraud.prisma (1)
1-5
: Fraud event types properly defined.The
FraudEventType
enum correctly captures the three types of fraud being detected in the system.apps/web/lib/analytics/fraud/is-self-referral.ts (1)
82-88
: Verify domain comparison logic for generic email providers.The condition appears to give higher confidence when domains match for non-generic providers. However, matching domains for company emails (e.g., @company.com) could be legitimate referrals between colleagues:
if ( customerDomain === partnerDomain && - !genericEmailDomains.includes(customerDomain) + genericEmailDomains.includes(customerDomain) ) { - score += 0.3; - reasons.push("Identical email domains"); + score += 0.1; + reasons.push("Same generic email provider"); }Alternatively, if the current logic is intentional, consider adding a comment explaining why non-generic matching domains indicate fraud.
…information in API response, updating UI components to display fraud events with new attributes, and implementing a script for bulk fraud event creation.
…, enhancing UI components for better display of fraud event details and integrating new properties for improved user experience.
Introduces a modal UI for banning a partner in response to a fraud event, allowing selection of a ban reason and notification option. Updates the backend action to accept and store the ban reason, and integrates the modal into the risk review sheet for pending fraud events.
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
♻️ Duplicate comments (2)
apps/web/ui/partners/risk-review-sheet.tsx (2)
46-48
: Handle error state when fetching partner dataThe
usePartner
hook's error field is not destructured or handled, causing the component to render nothing on fetch failure and confuse users.
349-354
: Handle undefined commission amountsThe
helAmount
calculation doesn't explicitly handle the case wherefraudEvent.commissions
is undefined or null, which could lead to runtime errors.
🧹 Nitpick comments (3)
apps/web/scripts/fraud-events.ts (1)
6-6
: Remove or explain the unused variable.The variable
i = 3
appears to be used only for email generation but its purpose and specific value aren't clear.Consider making this more explicit:
- const i = 3; + const testCaseNumber = 3; // or remove if not needed for multiple test casesapps/web/ui/partners/risk-review-sheet.tsx (2)
342-523
: Consider extracting FraudEventCard to a separate file.The
FraudEventCard
component is quite substantial (180+ lines) and is used in multiple contexts. Extracting it to a separate file would improve maintainability and reusability.Create a new file
apps/web/ui/partners/fraud-event-card.tsx
:export function FraudEventCard({ fraudEvent }: { fraudEvent: FraudEvent }) { // Move the entire FraudEventCard implementation here }Then import it in this file:
+import { FraudEventCard } from "./fraud-event-card";
487-499
: Improve conditional rendering logic.The condition
helAmount && helAmount > 0
could be simplified and made more explicit.- {helAmount && helAmount > 0 ? ( + {(helAmount ?? 0) > 0 && ( <div> <h3 className="text-sm font-medium text-neutral-900"> Commission hold </h3> <span className="text-sm font-medium text-neutral-500"> {currencyFormatter(helAmount / 100, { minimumFractionDigits: 2, maximumFractionDigits: 2, })} </span> </div> - ) : null} + )}
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (13)
apps/web/app/(ee)/api/fraud-events/route.ts
(1 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/fraud/fraud-event-table.tsx
(1 hunks)apps/web/lib/actions/partners/ban-partner.ts
(2 hunks)apps/web/lib/actions/partners/mark-fraud-event-banned.ts
(1 hunks)apps/web/lib/actions/partners/mark-fraud-event-safe.ts
(1 hunks)apps/web/lib/actions/partners/unban-partner.ts
(2 hunks)apps/web/lib/api/partners/ban-partner.ts
(1 hunks)apps/web/lib/api/partners/unban-partner.ts
(1 hunks)apps/web/lib/partners/constants.ts
(0 hunks)apps/web/lib/zod/schemas/fraud-events.ts
(1 hunks)apps/web/scripts/fraud-events.ts
(1 hunks)apps/web/ui/partners/risk-review-sheet.tsx
(1 hunks)packages/prisma/schema/fraud.prisma
(1 hunks)
💤 Files with no reviewable changes (1)
- apps/web/lib/partners/constants.ts
🚧 Files skipped from review as they are similar to previous changes (8)
- apps/web/app/(ee)/api/fraud-events/route.ts
- apps/web/lib/actions/partners/ban-partner.ts
- apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/fraud/fraud-event-table.tsx
- apps/web/lib/actions/partners/mark-fraud-event-banned.ts
- apps/web/lib/actions/partners/mark-fraud-event-safe.ts
- apps/web/lib/api/partners/ban-partner.ts
- apps/web/lib/zod/schemas/fraud-events.ts
- packages/prisma/schema/fraud.prisma
🧰 Additional context used
🧬 Code Graph Analysis (2)
apps/web/scripts/fraud-events.ts (1)
apps/web/lib/api/create-id.ts (1)
createId
(57-66)
apps/web/lib/actions/partners/unban-partner.ts (1)
apps/web/lib/api/partners/unban-partner.ts (1)
unbanPartner
(10-99)
🔇 Additional comments (3)
apps/web/lib/actions/partners/unban-partner.ts (1)
32-37
: LGTM! Clean separation of concerns.The refactoring successfully delegates the unbanning logic to a dedicated API function, making the action handler cleaner and more maintainable. The validation logic remains intact while the business logic is properly encapsulated.
apps/web/lib/api/partners/unban-partner.ts (2)
26-67
: Excellent transaction design and error handling.The implementation demonstrates several best practices:
- Uses Prisma transaction for atomicity across multiple entity updates
- Properly resets all related data (links, enrollment, commissions, payouts)
- Good use of spread operator for common where conditions
69-98
: Proper async operation handling.The use of
waitUntil
withPromise.allSettled
is appropriate for non-critical operations like cache clearing and audit logging. This ensures the main operation doesn't fail if these secondary operations encounter issues.
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
🔭 Outside diff range comments (1)
apps/web/scripts/fraud-events.ts (1)
30-31
: Add error handling and logging for better operational clarity.The script lacks error handling and feedback mechanisms, which could make debugging difficult if database operations fail.
Consider this improvement for better error handling and logging:
-main(); +main() + .then(() => { + console.log("✅ Fraud event script completed successfully"); + process.exit(0); + }) + .catch((error) => { + console.error("❌ Fraud event script failed:", error); + process.exit(1); + });Additionally, consider wrapping the database operations in a transaction to ensure atomicity:
async function main() { + return await prisma.$transaction(async (tx) => { const customer = await tx.customer.create({ // ... existing data }); await tx.fraudEvent.create({ // ... existing data }); + }); }
🧹 Nitpick comments (3)
apps/web/lib/api/partners/unban-partner.ts (1)
69-98
: Consider adding error logging for failed async operations.While using
Promise.allSettled
is good for preventing cascading failures, consider logging errors when cache invalidation or audit logging fails to improve observability.- await Promise.allSettled([ + const results = await Promise.allSettled([ linkCache.deleteMany(links), recordAuditLog({ workspaceId: workspace.id, programId: program.id, action: "partner.unbanned", description: `Partner ${partner.id} unbanned`, actor: user, targets: [ { type: "partner", id: partner.id, metadata: partner, }, ], }), ]); + + results.forEach((result, index) => { + if (result.status === 'rejected') { + const operation = index === 0 ? 'cache invalidation' : 'audit logging'; + console.error(`Failed ${operation} during partner unban:`, result.reason); + } + });apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/fraud/fraud-event-table.tsx (2)
298-301
: Enhance email subject with fraud event context.Consider including more context in the email subject to help partners identify which fraud event is being discussed.
window.open( - `mailto:${fraudEvent.partner.email}?subject=Question about fraud event`, + `mailto:${fraudEvent.partner.email}?subject=Question about fraud event - ${formatDate(fraudEvent.createdAt, { month: "short" })}`, "_blank", );
351-363
: Consider refactoring fraud reason collection for better maintainability.The current approach manually checks each fraud type. Consider iterating over fraud types to make it easier to add new types in the future.
function FraudEventReasons({ fraudEvent }: { fraudEvent: FraudEvent }) { - const reasons: string[] = []; - - if (fraudEvent.selfReferral) { - reasons.push(FRAUD_EVENT_TYPES.selfReferral.label); - } - - if (fraudEvent.googleAdsClick) { - reasons.push(FRAUD_EVENT_TYPES.googleAdsClick.label); - } - - if (fraudEvent.disposableEmail) { - reasons.push(FRAUD_EVENT_TYPES.disposableEmail.label); - } + const reasons = Object.entries(FRAUD_EVENT_TYPES) + .filter(([key]) => fraudEvent[key as keyof FraudEvent]) + .map(([_, type]) => type.label);This approach automatically includes all fraud types defined in
FRAUD_EVENT_TYPES
without manual updates.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (13)
apps/web/app/(ee)/api/fraud-events/route.ts
(1 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/fraud/fraud-event-table.tsx
(1 hunks)apps/web/lib/actions/partners/ban-partner.ts
(2 hunks)apps/web/lib/actions/partners/mark-fraud-event-banned.ts
(1 hunks)apps/web/lib/actions/partners/mark-fraud-event-safe.ts
(1 hunks)apps/web/lib/actions/partners/unban-partner.ts
(2 hunks)apps/web/lib/api/partners/ban-partner.ts
(1 hunks)apps/web/lib/api/partners/unban-partner.ts
(1 hunks)apps/web/lib/partners/constants.ts
(0 hunks)apps/web/lib/zod/schemas/fraud-events.ts
(1 hunks)apps/web/scripts/fraud-events.ts
(1 hunks)apps/web/ui/partners/risk-review-sheet.tsx
(1 hunks)packages/prisma/schema/fraud.prisma
(1 hunks)
💤 Files with no reviewable changes (1)
- apps/web/lib/partners/constants.ts
🚧 Files skipped from review as they are similar to previous changes (8)
- apps/web/lib/actions/partners/ban-partner.ts
- apps/web/app/(ee)/api/fraud-events/route.ts
- apps/web/lib/actions/partners/mark-fraud-event-banned.ts
- apps/web/lib/api/partners/ban-partner.ts
- apps/web/lib/zod/schemas/fraud-events.ts
- apps/web/lib/actions/partners/mark-fraud-event-safe.ts
- apps/web/ui/partners/risk-review-sheet.tsx
- packages/prisma/schema/fraud.prisma
🔇 Additional comments (4)
apps/web/lib/actions/partners/unban-partner.ts (1)
3-3
: Good refactoring to centralize unban logic.The extraction of the unban logic into a dedicated function improves code organization and maintainability. This follows the DRY principle and aligns with the similar pattern used for banning partners.
Also applies to: 32-37
apps/web/lib/api/partners/unban-partner.ts (1)
26-67
: Well-structured atomic transaction for unban operations.The transaction correctly handles all necessary updates: clearing link expirations, updating enrollment status, and reverting commission/payout statuses. This ensures data consistency across all affected entities.
apps/web/scripts/fraud-events.ts (2)
1-3
: LGTM!The imports are well-structured and appropriate for a database script. Using
dotenv-flow/config
ensures environment variables are loaded correctly.
16-27
: Verify hardcoded foreign key references in fraud event creationThe script didn’t find any occurrences of these IDs elsewhere in the codebase, so please confirm they exist in the target database and won’t cause foreign-key constraint violations.
• File: apps/web/scripts/fraud-events.ts (lines 16–27)
– programId:prog_1K1QC1VPXKT6CP80KTDV6SMBR
– partnerId:pn_1K1QE594RDF9MGE3YYXPEVP4H
– linkId:link_1K1QE5BG9FWQHN2C4BVX33J13
Suggested actions:
- Manually verify that each referenced record exists in your database before running this script.
- Consider making these IDs configurable (e.g., via environment variables or a lookup table) and adding error handling around the
prisma.fraudEvent.create
call to guard against missing records.
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
♻️ Duplicate comments (3)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/fraud/fraud-event-table.tsx (1)
133-137
: Hold amount calculation properly handles null/undefined commissionsThe implementation correctly uses optional chaining and nullish coalescing to handle cases where
commissions
might be null or undefined, returning 0 as the default value.apps/web/ui/partners/risk-review-sheet.tsx (2)
46-48
: Add error handling for partner data fetchingThe
usePartner
hook returns an error field that should be handled to provide better user experience when the fetch fails.- const { partner, loading: isLoadingPartner } = usePartner({ - partnerId: fraudEvent.partner.id, - }); + const { partner, loading: isLoadingPartner, error: partnerError } = usePartner({ + partnerId: fraudEvent.partner.id, + });Then add error handling after the loading check:
{isLoadingPartner ? ( // ... loading skeleton ) : ( + partnerError ? ( + <div className="text-sm text-red-600">Failed to load partner data</div> + ) : ( partner && <PartnerInfoSection partner={partner} /> + ) )}
349-354
: Add null safety to commission amount calculationThe
reduce
operation could fail iffraudEvent.commissions
is null or undefined.const helAmount = useMemo(() => { - return fraudEvent.commissions?.reduce( + return (fraudEvent.commissions ?? []).reduce( (acc, commission) => acc + commission.earnings, 0, ); }, [fraudEvent.commissions]);
🧹 Nitpick comments (5)
apps/web/scripts/fraud-events.ts (3)
6-6
: Remove or clarify the hardcoded variable.The variable
i = 3
appears to be a remnant from a loop or batch operation. If this script is intended for one-time execution, consider removing this variable and using a more descriptive identifier in the email.- const i = 3; + // Using a descriptive identifier for the test email
8-14
: Consider parameterizing the hardcoded project ID.The customer creation uses a hardcoded
projectId
. For a reusable script, consider making this configurable via environment variables or command-line arguments.const customer = await prisma.customer.create({ data: { id: createId({ prefix: "cus_" }), - email: `thomas+${i}@example.com`, - projectId: "clrei1gld0002vs9mzn93p8ik", + email: `[email protected]`, + projectId: process.env.PROJECT_ID || "clrei1gld0002vs9mzn93p8ik", }, });
16-27
: Add error handling and consider more realistic test data.The fraud event creation has several areas for improvement:
- All fraud flags are set to
true
, which may not represent realistic test scenarios- Hardcoded IDs make the script less flexible
- No error handling if the creation fails
- await prisma.fraudEvent.create({ + const fraudEvent = await prisma.fraudEvent.create({ data: { id: createId({ prefix: "fraud_" }), - programId: "prog_1K1QC1VPXKT6CP80KTDV6SMBR", - partnerId: "pn_1K1QE594RDF9MGE3YYXPEVP4H", + programId: process.env.PROGRAM_ID || "prog_1K1QC1VPXKT6CP80KTDV6SMBR", + partnerId: process.env.PARTNER_ID || "pn_1K1QE594RDF9MGE3YYXPEVP4H", customerId: customer.id, - linkId: "link_1K1QE5BG9FWQHN2C4BVX33J13", - selfReferral: true, - googleAdsClick: true, - disposableEmail: true, + linkId: process.env.LINK_ID || "link_1K1QE5BG9FWQHN2C4BVX33J13", + selfReferral: false, // More realistic default + googleAdsClick: false, + disposableEmail: true, // Keep one flag true for testing }, }); + + console.log(`Created fraud event: ${fraudEvent.id}`);apps/web/lib/api/partners/unban-partner.ts (2)
69-98
: Good use of waitUntil for non-blocking side effectsThe implementation properly handles cache invalidation and audit logging without blocking the response. Using
Promise.allSettled
ensures both operations run independently.Consider adding error logging for failed operations since
allSettled
will silently continue even if cache invalidation or audit logging fails.
10-99
: Consider adding explicit error handlingWhile Prisma handles transaction rollback automatically, consider adding explicit error handling for better observability:
export const unbanPartner = async ({ workspace, program, partner, user, }: { workspace: Pick<Project, "id">; program: Pick<Program, "id" | "name" | "supportEmail">; partner: Pick<Partner, "id" | "name" | "email">; user: Session["user"]; }) => { const where = { programId: program.id, partnerId: partner.id, }; + try { await prisma.$transaction([ // ... existing transaction operations ]); + } catch (error) { + console.error(`Failed to unban partner ${partner.id}:`, error); + throw error; + } waitUntil( (async () => { const links = await prisma.link.findMany({ where, select: { domain: true, key: true, }, }); - await Promise.allSettled([ + const results = await Promise.allSettled([ linkCache.deleteMany(links), recordAuditLog({ // ... existing audit log }), ]); + + results.forEach((result, index) => { + if (result.status === 'rejected') { + const operation = index === 0 ? 'cache invalidation' : 'audit logging'; + console.error(`Failed ${operation} for partner ${partner.id}:`, result.reason); + } + }); })(), ); };
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (13)
apps/web/app/(ee)/api/fraud-events/route.ts
(1 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/fraud/fraud-event-table.tsx
(1 hunks)apps/web/lib/actions/partners/ban-partner.ts
(2 hunks)apps/web/lib/actions/partners/mark-fraud-event-banned.ts
(1 hunks)apps/web/lib/actions/partners/mark-fraud-event-safe.ts
(1 hunks)apps/web/lib/actions/partners/unban-partner.ts
(2 hunks)apps/web/lib/api/partners/ban-partner.ts
(1 hunks)apps/web/lib/api/partners/unban-partner.ts
(1 hunks)apps/web/lib/partners/constants.ts
(0 hunks)apps/web/lib/zod/schemas/fraud-events.ts
(1 hunks)apps/web/scripts/fraud-events.ts
(1 hunks)apps/web/ui/partners/risk-review-sheet.tsx
(1 hunks)packages/prisma/schema/fraud.prisma
(1 hunks)
💤 Files with no reviewable changes (1)
- apps/web/lib/partners/constants.ts
🚧 Files skipped from review as they are similar to previous changes (7)
- apps/web/lib/actions/partners/ban-partner.ts
- apps/web/app/(ee)/api/fraud-events/route.ts
- apps/web/lib/api/partners/ban-partner.ts
- apps/web/lib/actions/partners/mark-fraud-event-banned.ts
- apps/web/lib/zod/schemas/fraud-events.ts
- apps/web/lib/actions/partners/mark-fraud-event-safe.ts
- packages/prisma/schema/fraud.prisma
🧰 Additional context used
🧬 Code Graph Analysis (2)
apps/web/scripts/fraud-events.ts (1)
apps/web/lib/api/create-id.ts (1)
createId
(57-66)
apps/web/lib/actions/partners/unban-partner.ts (1)
apps/web/lib/api/partners/unban-partner.ts (1)
unbanPartner
(10-99)
🔇 Additional comments (8)
apps/web/scripts/fraud-events.ts (1)
1-3
: LGTM!The imports are correctly structured and include the necessary dependencies for the script functionality.
apps/web/lib/actions/partners/unban-partner.ts (2)
3-3
: Good refactoring to centralize unbanning logicMoving the unbanning logic to a dedicated helper function improves code organization and reusability.
32-37
: Error handling is intact—no further changes neededThe
unbanPartner
helper neither swallows nor alters errors from the main transaction (it will reject on failure just as the inline code did), and usesPromise.allSettled
for background cache/logging without blocking. Delegation is clean and preserves the previous behavior.apps/web/lib/api/partners/unban-partner.ts (2)
10-20
: Well-typed function signature with appropriate use of Pick typesGood use of TypeScript's Pick utility type to ensure type safety while only requiring the necessary fields from each entity.
26-67
: Comprehensive transaction ensures data consistencyThe transaction properly handles all aspects of unbanning:
- Removes link expiration
- Updates enrollment status from "banned" to "approved"
- Clears ban metadata
- Reverts canceled commissions and payouts to "pending"
All operations are atomic, ensuring data consistency.
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/fraud/fraud-event-table.tsx (2)
224-241
: Excellent empty state handling with context-aware messagingThe component provides clear feedback to users with different messages for filtered vs unfiltered empty states, improving the user experience.
307-333
: Well-structured conditional action renderingThe menu intelligently shows/hides actions based on the fraud event status:
- Ban action only for "pending" or "safe" events
- Mark safe action only for "pending" or "banned" events
This prevents invalid state transitions.
apps/web/ui/partners/risk-review-sheet.tsx (1)
564-586
: Clean sheet implementation with proper URL state managementThe component properly cleans up the
fraudEventId
query parameter when the sheet closes, maintaining clean URLs and preventing stale state.
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
♻️ Duplicate comments (3)
apps/web/lib/analytics/fraud/is-google-ads-click.ts (1)
40-48
: Refine the referer check to reduce false positivesThe current implementation checks if "google" appears anywhere in the referer string, which could lead to false positives. For example, referers like
https://example.com/google-analytics
orhttps://mygoogleproject.com
would incorrectly trigger this condition.apps/web/ui/partners/risk-review-sheet.tsx (2)
47-49
: Handle error state when fetching partner dataThe
usePartner
hook returns anerror
field, but you only destructurepartner
andloading
. If the fetch fails, the component will render nothing and confuse users.
350-355
: Handle undefined commission amountsThe
helAmount
calculation doesn't explicitly handle the case wherefraudEvent.commissions
is undefined or null, which could lead to runtime errors.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (7)
apps/web/lib/analytics/fraud/detect-fraud-events.ts
(1 hunks)apps/web/lib/analytics/fraud/is-google-ads-click.ts
(1 hunks)apps/web/lib/analytics/fraud/record-fraud-if-detected.ts
(1 hunks)apps/web/lib/middleware/utils/get-final-url.ts
(1 hunks)apps/web/lib/types.ts
(2 hunks)apps/web/lib/zod/schemas/fraud-events.ts
(1 hunks)apps/web/ui/partners/risk-review-sheet.tsx
(1 hunks)
✅ Files skipped from review due to trivial changes (1)
- apps/web/lib/middleware/utils/get-final-url.ts
🚧 Files skipped from review as they are similar to previous changes (4)
- apps/web/lib/analytics/fraud/detect-fraud-events.ts
- apps/web/lib/types.ts
- apps/web/lib/analytics/fraud/record-fraud-if-detected.ts
- apps/web/lib/zod/schemas/fraud-events.ts
🧰 Additional context used
🧠 Learnings (5)
📚 Learning: in apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx, the form schema uses partial condition ob...
Learnt from: TWilson023
PR: dubinc/dub#2673
File: apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx:56-66
Timestamp: 2025-07-30T15:25:13.936Z
Learning: In apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx, the form schema uses partial condition objects to allow users to add empty/unconfigured condition fields without type errors, while submission validation uses strict schemas to ensure data integrity. This two-stage validation pattern improves UX by allowing progressive completion of complex forms.
Applied to files:
apps/web/ui/partners/risk-review-sheet.tsx
📚 Learning: in apps/web/ui/partners/rewards/rewards-logic.tsx, when setting the entity field in a reward conditi...
Learnt from: TWilson023
PR: dubinc/dub#2673
File: apps/web/ui/partners/rewards/rewards-logic.tsx:268-275
Timestamp: 2025-07-30T15:29:54.131Z
Learning: In apps/web/ui/partners/rewards/rewards-logic.tsx, when setting the entity field in a reward condition, dependent fields (attribute, operator, value) should be reset rather than preserved because different entities (customer vs sale) have different available attributes. Maintaining existing fields when the entity changes would create invalid state combinations and confusing UX.
Applied to files:
apps/web/ui/partners/risk-review-sheet.tsx
📚 Learning: in apps/web/lib/api/links/bulk-create-links.ts, the team accepts the risk of potential undefined res...
Learnt from: devkiran
PR: dubinc/dub#2177
File: apps/web/lib/api/links/bulk-create-links.ts:66-84
Timestamp: 2025-06-06T07:59:03.120Z
Learning: In apps/web/lib/api/links/bulk-create-links.ts, the team accepts the risk of potential undefined results from links.find() operations when building invalidLinks arrays, because existing links are fetched from the database based on the input links, so matches are expected to always exist.
Applied to files:
apps/web/ui/partners/risk-review-sheet.tsx
📚 Learning: in react components where arrays are defined with usememo using static array literals (not filtered ...
Learnt from: TWilson023
PR: dubinc/dub#2538
File: apps/web/ui/partners/overview/blocks/conversion-block.tsx:85-85
Timestamp: 2025-06-18T20:31:51.779Z
Learning: In React components where arrays are defined with useMemo using static array literals (not filtered or dynamically populated), the array will always have the same number of elements regardless of the data values. Using .at(-1)! on such arrays is safe because the array structure is constant, even if individual property values within the array elements may be 0 or falsy.
Applied to files:
apps/web/ui/partners/risk-review-sheet.tsx
📚 Learning: in apps/web/ui/partners/design/previews/lander-preview.tsx, the ellipsis wave animation delay calcul...
Learnt from: TWilson023
PR: dubinc/dub#2614
File: apps/web/ui/partners/design/previews/lander-preview.tsx:181-181
Timestamp: 2025-07-09T20:52:56.592Z
Learning: In apps/web/ui/partners/design/previews/lander-preview.tsx, the ellipsis wave animation delay calculation `3 - i * -0.15` is intentionally designed to create negative delays that offset each dot's animation cycle. This pattern works correctly for the intended ellipsis effect and should not be changed to positive incremental delays.
Applied to files:
apps/web/ui/partners/risk-review-sheet.tsx
🪛 Biome (2.1.2)
apps/web/ui/partners/risk-review-sheet.tsx
[error] 470-470: Missing key property for this element in iterable.
The order of the items may change, and having a key can help React identify which item was moved.
Check the React documentation.
(lint/correctness/useJsxKeyInIterable)
🔇 Additional comments (6)
apps/web/lib/analytics/fraud/is-google-ads-click.ts (3)
1-12
: LGTM - Clean function signature and typesThe function signature is well-structured with proper TypeScript types and clear parameter definitions.
13-38
: LGTM - Robust Google Ads parameter detectionThe URL parameter checking logic correctly identifies Google Ads parameters (
gclid
,gad_source
,gad_campaignid
) and safely handles URL parsing with proper null checks.
50-54
: LGTM - Appropriate default returnThe default return correctly indicates no Google Ads click detected with proper null parameters.
apps/web/ui/partners/risk-review-sheet.tsx (3)
1-42
: LGTM - Well-organized imports and type definitionsThe component imports are comprehensive and the TypeScript interfaces are properly defined.
119-177
: LGTM - Comprehensive partner metrics displayThe partner statistics display logic properly handles null values and formats currency amounts correctly, converting from cents to dollars.
282-609
: LGTM - Well-structured component hierarchyThe component architecture is well-organized with proper separation of concerns, loading states, error handling, and clean composition patterns.
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: 2
🧹 Nitpick comments (2)
apps/web/lib/analytics/fraud/detect-and-record-fraud.ts (2)
19-47
: Consider graceful handling of missing program enrollments.The use of
findUniqueOrThrow
will cause the function to throw an error if the program enrollment doesn't exist. In a fraud detection context, you might want to handle this scenario more gracefully to avoid breaking downstream processes.Consider this approach for more resilient error handling:
- const { partner: partnerData, ...enrollment } = - await prisma.programEnrollment.findUniqueOrThrow({ + const enrollmentData = await prisma.programEnrollment.findUnique({ where: { partnerId_programId: { partnerId: partner.id, programId: partner.programId, }, }, // ... rest of select - }); + }); + + if (!enrollmentData) { + console.log(`Program enrollment not found for partner ${partner.id}`); + return; + } + + const { partner: partnerData, ...enrollment } = enrollmentData;
89-115
: Consider using Prisma's upsert for cleaner code.The manual upsert pattern works but could be simplified using Prisma's built-in
upsert
operation.Consider this approach:
- let fraudEvent = await tx.fraudEvent.findUnique({ - where: { - partnerId_customerId: { - partnerId: partner.id, - customerId: customer.id, - }, - }, - }); - - if (!fraudEvent) { - fraudEvent = await tx.fraudEvent.create({ - data: { + const fraudEvent = await tx.fraudEvent.upsert({ + where: { + partnerId_customerId: { + partnerId: partner.id, + customerId: customer.id, + }, + }, + create: { id: createId({ prefix: "fraud_" }), selfReferral: fraudTypesFound.has("selfReferral"), googleAdsClick: fraudTypesFound.has("googleAdsClick"), disposableEmail: fraudTypesFound.has("disposableEmail"), programId: partner.programId, partnerId: partner.id, customerId: customer.id, linkId: partner.linkId, ...(eventsWithDetails.length > 0 && { details: eventsWithDetails as unknown as Prisma.InputJsonValue, }), - }, - }); - } + }, + update: { + // Update existing fraud event if needed + selfReferral: fraudTypesFound.has("selfReferral"), + googleAdsClick: fraudTypesFound.has("googleAdsClick"), + disposableEmail: fraudTypesFound.has("disposableEmail"), + ...(eventsWithDetails.length > 0 && { + details: eventsWithDetails as unknown as Prisma.InputJsonValue, + }), + }, + });
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (9)
apps/web/app/(ee)/api/stripe/integration/webhook/checkout-session-completed.ts
(2 hunks)apps/web/app/(ee)/api/stripe/integration/webhook/invoice-paid.ts
(2 hunks)apps/web/app/(ee)/api/stripe/integration/webhook/utils.ts
(3 hunks)apps/web/lib/analytics/fraud/detect-and-record-fraud.ts
(1 hunks)apps/web/lib/api/conversions/track-lead.ts
(3 hunks)apps/web/lib/api/conversions/track-sale.ts
(2 hunks)apps/web/lib/integrations/shopify/create-sale.ts
(2 hunks)apps/web/ui/layout/sidebar/app-sidebar-nav.tsx
(6 hunks)packages/prisma/schema/link.prisma
(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (8)
- packages/prisma/schema/link.prisma
- apps/web/app/(ee)/api/stripe/integration/webhook/utils.ts
- apps/web/lib/api/conversions/track-lead.ts
- apps/web/app/(ee)/api/stripe/integration/webhook/checkout-session-completed.ts
- apps/web/lib/integrations/shopify/create-sale.ts
- apps/web/app/(ee)/api/stripe/integration/webhook/invoice-paid.ts
- apps/web/ui/layout/sidebar/app-sidebar-nav.tsx
- apps/web/lib/api/conversions/track-sale.ts
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: TWilson023
PR: dubinc/dub#2673
File: apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx:56-66
Timestamp: 2025-07-30T15:25:13.936Z
Learning: In apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx, the form schema uses partial condition objects to allow users to add empty/unconfigured condition fields without type errors, while submission validation uses strict schemas to ensure data integrity. This two-stage validation pattern improves UX by allowing progressive completion of complex forms.
⏰ 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/lib/analytics/fraud/detect-and-record-fraud.ts (5)
1-18
: LGTM! Well-structured imports and function signature.The imports are clean and the function signature uses appropriate TypeScript patterns with
Pick
utility types to enforce minimal required fields. The parameter structure clearly defines the expected data shape.
49-54
: LGTM! Appropriate trusted partner bypass.The early return for trusted partners is correct - fraud detection should be skipped for verified partners. The logging provides good visibility into the decision.
67-80
: LGTM! Clean fraud detection logic.The fraud detection call and empty result handling is well-implemented. The logging provides good visibility and the early return avoids unnecessary processing.
82-86
: LGTM! Efficient fraud event processing.Good use of
Set
for fraud type lookup and logical filtering for events with detailed information. The functional approach is clean and readable.
129-144
: LGTM! Comprehensive error handling.Excellent error handling that:
- Gracefully handles expected unique constraint violations from concurrent operations
- Provides proper logging for debugging and monitoring
- Uses external logging with alerting for critical errors
- Follows good practices for transaction error recovery
// Get partner's IP address from their associated user | ||
let partnerIpAddress: string | null = null; | ||
|
||
if (partnerData.users.length > 0) { | ||
const partnerUser = partnerData.users[0]?.user; | ||
|
||
if (partnerUser?.ipAddress) { | ||
partnerIpAddress = partnerUser.ipAddress.toString("utf8"); | ||
} | ||
} |
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.
🛠️ Refactor suggestion
Review IP address extraction logic.
Several considerations for the IP address handling:
-
Buffer conversion: Using
toString("utf8")
for IP addresses may not be correct. IP addresses are typically stored as binary data and should be converted differently. -
Multiple users: The logic only uses the first user's IP address. Consider if this is the intended behavior when a partner has multiple associated users.
Consider this improved approach:
if (partnerData.users.length > 0) {
- const partnerUser = partnerData.users[0]?.user;
+ const partnerUser = partnerData.users[0].user;
if (partnerUser?.ipAddress) {
- partnerIpAddress = partnerUser.ipAddress.toString("utf8");
+ // Convert Buffer to IP address string properly
+ partnerIpAddress = partnerUser.ipAddress.toString();
}
}
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
// Get partner's IP address from their associated user | |
let partnerIpAddress: string | null = null; | |
if (partnerData.users.length > 0) { | |
const partnerUser = partnerData.users[0]?.user; | |
if (partnerUser?.ipAddress) { | |
partnerIpAddress = partnerUser.ipAddress.toString("utf8"); | |
} | |
} | |
// Get partner's IP address from their associated user | |
let partnerIpAddress: string | null = null; | |
if (partnerData.users.length > 0) { | |
const partnerUser = partnerData.users[0].user; | |
if (partnerUser?.ipAddress) { | |
// Convert Buffer to IP address string properly | |
partnerIpAddress = partnerUser.ipAddress.toString(); | |
} | |
} |
🤖 Prompt for AI Agents
In apps/web/lib/analytics/fraud/detect-and-record-fraud.ts around lines 56 to
65, the IP address extraction uses toString("utf8") which is not appropriate for
binary IP data; instead, convert the buffer to a standard IP string format
(e.g., using a library or appropriate method for IPv4/IPv6). Also, review
whether using only the first user's IP is correct; if multiple users' IPs are
relevant, update the logic to handle all or select the correct one accordingly.
ea17ddf
to
f7d87c3
Compare
Summary by CodeRabbit
New Features
Bug Fixes
Refactor
Chores