-
Notifications
You must be signed in to change notification settings - Fork 2.8k
PartnerStack importer #2642
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
PartnerStack importer #2642
Conversation
…e related schemas
…ema, and update types to include new commission type.
… route to handle commission imports, and remove unused update-stripe-customers case.
|
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
WalkthroughThis update introduces full PartnerStack integration, enabling importing of partners, links, customers, and commissions via a new API route, backend modules, and UI modal. It adds PartnerStack as a supported import source, implements credential management, and provides end-to-end workflows for importing and updating data, including Stripe customer ID matching. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant UI (ImportPartnerStackModal)
participant Server (startPartnerStackImportAction)
participant PartnerStackImporter
participant PartnerStackApi
participant DB/Queue
User->>UI (ImportPartnerStackModal): Open modal, enter API keys
UI (ImportPartnerStackModal)->>Server (startPartnerStackImportAction): Submit credentials
Server (startPartnerStackImportAction)->>PartnerStackApi: Test connection
PartnerStackApi-->>Server (startPartnerStackImportAction): Connection result
Server (startPartnerStackImportAction)->>PartnerStackImporter: Store credentials
Server (startPartnerStackImportAction)->>PartnerStackImporter: Queue import (import-partners)
PartnerStackImporter->>DB/Queue: Enqueue import job
DB/Queue-->>PartnerStackImporter: Job enqueued
PartnerStackImporter-->>Server (startPartnerStackImportAction): Ack
Server (startPartnerStackImportAction)-->>UI (ImportPartnerStackModal): Success/failure
UI (ImportPartnerStackModal)-->>User: Show result
sequenceDiagram
participant Queue (Qstash)
participant API Route
participant Import Modules
participant PartnerStackApi
participant DB
Queue (Qstash)->>API Route: POST with import payload
API Route->>Import Modules: Dispatch to importPartners/importLinks/importCustomers/importCommissions/updateStripeCustomers
Import Modules->>PartnerStackApi: Fetch data (partners/links/customers/commissions)
PartnerStackApi-->>Import Modules: Return data batch
Import Modules->>DB: Upsert data (partners/links/customers/commissions)
Import Modules->>PartnerStackImporter: Queue next step if needed
Import Modules-->>API Route: Complete or error
API Route-->>Queue (Qstash): Respond OK or error
Possibly related PRs
Poem
📜 Recent review detailsConfiguration used: CodeRabbit UI 📒 Files selected for processing (2)
🧰 Additional context used🧠 Learnings (1)apps/web/lib/partnerstack/import-commissions.ts (2)🪛 Biome (1.9.4)apps/web/lib/partnerstack/import-commissions.ts[error] 73-73: Avoid the delete operator which can impact performance. (lint/performance/noDelete) ⏰ 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)
🔇 Additional comments (3)
✨ Finishing Touches
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. Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. 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: 2
♻️ Duplicate comments (1)
apps/web/lib/partnerstack/import-customers.ts (1)
246-249: Error handling needs improvement for production resilience.The current error handling only logs errors without proper error tracking or recovery mechanisms. This could lead to silent failures during imports.
Based on the existing review comment, this issue has already been identified. The current implementation risks silent data loss during customer imports.
🧹 Nitpick comments (5)
apps/web/lib/partnerstack/import-customers.ts (5)
173-173: Consider the implications of using only the first link.The logic always uses
links[0]without considering that a partner might have multiple relevant links. This could lead to inconsistent click attribution if customers should be associated with different links based on specific criteria.Consider adding logic to select the most appropriate link:
- const link = links[0]; + // Select the most appropriate link (e.g., primary, most recent, or based on other criteria) + const link = links.find(l => l.primary) || links[0];Or document why using the first link is the intended behavior if this is correct.
175-183: Avoid hardcoded dummy request headers.The hardcoded request headers could lead to inconsistent geolocation data and user-agent tracking. Consider making these configurable or using more realistic defaults.
+const DEFAULT_IMPORT_HEADERS = { + "user-agent": "PartnerStack-Importer/1.0", + "x-forwarded-for": "127.0.0.1", + "x-vercel-ip-country": "US", + "x-vercel-ip-country-region": "CA", + "x-vercel-ip-continent": "NA", +}; const dummyRequest = new Request(link.url, { - headers: new Headers({ - "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)", - "x-forwarded-for": "127.0.0.1", - "x-vercel-ip-country": "US", - "x-vercel-ip-country-region": "CA", - "x-vercel-ip-continent": "NA", - }), + headers: new Headers(DEFAULT_IMPORT_HEADERS), });
209-213: Improve name fallback logic clarity.The name fallback logic is complex and could be simplified for better readability.
- name: - // if name is null/undefined or starts with cus_, use email as name - !customer.name || customer.name.startsWith("cus_") - ? customer.email - : customer.name, + name: getCustomerDisplayName(customer),Add a helper function:
+function getCustomerDisplayName(customer: PartnerStackCustomer): string { + // If name is null/undefined or starts with cus_, use email as name + if (!customer.name || customer.name.startsWith("cus_")) { + return customer.email; + } + return customer.name; +}
107-118: Consider adding batch processing feedback.The batch processing uses
Promise.allSettledbut doesn't provide feedback on success/failure rates, which could be valuable for monitoring import health.- await Promise.allSettled( + const results = await Promise.allSettled( customers.map((customer) => { const partnerId = partnerKeysToId[customer.partnership_key]; const links = partnerId ? partnerIdToLinks.get(partnerId) ?? [] : []; return createCustomer({ workspace: program.workspace, links, customer, }); }), ); + + const successful = results.filter(r => r.status === 'fulfilled').length; + const failed = results.filter(r => r.status === 'rejected').length; + + if (failed > 0) { + console.warn(`Batch ${processedBatches + 1}: ${successful} succeeded, ${failed} failed`); + }
121-121: Make rate limiting configurable.The hardcoded 2-second delay might not be optimal for all scenarios and could impact import performance.
+const BATCH_DELAY_MS = 2000; // Make this configurable via environment variable - await new Promise((resolve) => setTimeout(resolve, 2000)); + await new Promise((resolve) => setTimeout(resolve, BATCH_DELAY_MS));
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
apps/web/lib/partnerstack/import-customers.ts(1 hunks)
🧰 Additional context used
🧠 Learnings (1)
apps/web/lib/partnerstack/import-customers.ts (1)
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.
🪛 Biome (1.9.4)
apps/web/lib/partnerstack/import-customers.ts
[error] 127-127: Avoid the delete operator which can impact performance.
(lint/performance/noDelete)
⏰ 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
|
@CodeRabbit final review please |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chainI'll conduct a comprehensive final review of this PartnerStack integration PR. Let me examine the key areas systematically. @steven-tey, I've conducted a comprehensive final review of the PartnerStack integration. Here's my analysis: 🔒 Security Assessment - EXCELLENTStrengths:
🏗️ Architecture & Code Quality - VERY GOODStrengths:
Areas for consideration:
⚡ Performance Considerations - GOODStrengths:
Potential improvements:
🛠️ Error Handling & Resilience - VERY GOODStrengths:
🧪 Testing ConsiderationsRecommendations for follow-up:
🎯 Final Verdict: APPROVED WITH CONFIDENCEThis is a well-architected, secure, and robust integration that follows best practices. The code quality is high, security considerations are properly addressed, and the error handling is comprehensive. The modular design makes it maintainable and extensible. Minor suggestions for future iterations:
Excellent work on this integration! 🚀 🏁 Script executed: Length of output: 5134 🏁 Script executed: Length of output: 2056 🏁 Script executed: Length of output: 25624 🏁 Script executed: Length of output: 17339 🏁 Script executed: Length of output: 1706 🏁 Script executed: Length of output: 3315 @steven-tey, I've conducted a comprehensive final review of the PartnerStack integration. Here's my analysis: 🔒 Security Assessment - EXCELLENTStrengths:
🏗️ Architecture & Code Quality - VERY GOODStrengths:
Areas for consideration:
⚡ Performance Considerations - GOODStrengths:
🛠️ Error Handling & Resilience - VERY GOODStrengths:
🎯 Configuration & Constants - GOODConstants are well-defined:
🧪 Testing ConsiderationsRecommendations for follow-up:
🎯 Final Verdict: APPROVED WITH CONFIDENCE ✅This is a well-architected, secure, and robust integration that follows best practices. The code quality is high, security considerations are properly addressed, and the error handling is comprehensive. The modular design makes it maintainable and extensible. Minor suggestions for future iterations:
Key highlights:
Excellent work on this integration! The implementation demonstrates strong engineering practices and attention to detail. 🚀 |
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 (6)
apps/web/lib/partnerstack/import-partners.ts (1)
75-75: Replacedeleteoperator with more performant approach.Same performance issue as in the import-links file.
Apply this diff:
- delete payload?.startingAfter; + const { startingAfter: _, ...updatedPayload } = payload;Then update the queue call:
await partnerStackImporter.queue({ - ...payload, + ...updatedPayload, ...(hasMore && { startingAfter: currentStartingAfter }), action: hasMore ? "import-partners" : "import-links", });apps/web/lib/partnerstack/import-commissions.ts (2)
105-117: Clarify the use of commission.key for deduplication.Using
commission.keyasinvoiceIdis confusing and could lead to maintenance issues. The comment acknowledges this isn't the actual invoice ID, which suggests a better approach might be needed.Consider:
- Adding a dedicated
externalIdfield to store the PartnerStack commission key- Creating a composite unique constraint that better represents the deduplication logic
- Documenting why this approach was chosen over alternatives
const commissionFound = await prisma.commission.findUnique({ where: { - invoiceId_programId: { - invoiceId: commission.key, // This is not the actual invoice ID, but we use this to deduplicate the commissions - programId, - }, + externalId_programId: { + externalId: commission.key, + programId, + }, }, });
204-265: Ensure data consistency with database transactions.The commission creation involves multiple related database operations that should be atomic. If any operation fails, the data could be left in an inconsistent state.
Wrap the operations in a database transaction:
- await Promise.all([ - prisma.commission.create({ + await prisma.$transaction(async (tx) => { + await Promise.all([ + tx.commission.create({ data: { // ... commission data }, }), - - recordSaleWithTimestamp({ - // ... sale event data - }), - - // update link stats - prisma.link.update({ - where: { - id: customer.linkId, - }, - data: { - // ... update data - }, - }), - - // update customer stats - prisma.customer.update({ - where: { - id: customer.id, - }, - data: { - // ... update data - }, - }), - ]); + + // update link stats + tx.link.update({ + where: { + id: customer.linkId, + }, + data: { + // ... update data + }, + }), + + // update customer stats + tx.customer.update({ + where: { + id: customer.id, + }, + data: { + // ... update data + }, + }), + ]); + + // Record sale event outside transaction if it's an external service + await recordSaleWithTimestamp({ + // ... sale event data + }); + });Note: If
recordSaleWithTimestampis an external service call, consider whether it should be inside or outside the transaction based on your consistency requirements.apps/web/lib/partnerstack/import-customers.ts (3)
128-128: Replace delete operator with object destructuring for better performance.The
deleteoperator can impact performance as it changes the object's hidden class in JavaScript engines.- delete payload?.startingAfter; + const { startingAfter: _, ...payloadWithoutStartingAfter } = payload;Then update the queue call:
await partnerStackImporter.queue({ - ...payload, + ...payloadWithoutStartingAfter, ...(hasMore && { startingAfter: currentStartingAfter }), action: hasMore ? "import-customers" : "import-commissions", });
204-204: Add validation for customer.created_at timestamp.The code assumes
customer.created_atexists and is a valid date, but this should be validated to prevent runtime errors.+ const createdAt = customer.created_at ? new Date(customer.created_at) : new Date(); + + if (customer.created_at && isNaN(createdAt.getTime())) { + console.warn(`Invalid created_at timestamp for customer ${customer.email}: ${customer.created_at}`); + createdAt = new Date(); + } const clickData = await recordClick({ req: dummyRequest, linkId: link.id, clickId: nanoid(16), url: link.url, domain: link.domain, key: link.key, workspaceId: workspace.id, skipRatelimit: true, - timestamp: new Date(customer.created_at).toISOString(), + timestamp: createdAt.toISOString(), });
256-259: Improve error handling for customer creation failures.The current error handling only logs errors to console without propagating them or tracking failures. This could lead to silent data loss during imports.
Consider:
- Collecting failed customer imports and returning them to the caller
- Implementing retry logic for transient failures
- Recording import failures in a database table for later analysis
- Using structured logging instead of console.error
- } catch (error) { - console.error("Error creating customer", customer, error); - } + } catch (error) { + // Log with structured data for better monitoring + console.error("Failed to create customer", { + customerEmail: customer.email, + customerKey: customer.customer_key, + partnershipKey: customer.partnership_key, + error: error instanceof Error ? error.message : String(error), + }); + + // Re-throw to let Promise.allSettled handle it + throw error; + }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
apps/web/lib/partnerstack/import-commissions.ts(1 hunks)apps/web/lib/partnerstack/import-customers.ts(1 hunks)apps/web/lib/partnerstack/import-partners.ts(1 hunks)
🧰 Additional context used
🧠 Learnings (3)
apps/web/lib/partnerstack/import-partners.ts (1)
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.
apps/web/lib/partnerstack/import-commissions.ts (2)
Learnt from: devkiran
PR: dubinc/dub#2635
File: packages/prisma/schema/payout.prisma:24-25
Timestamp: 2025-07-11T16:28:55.693Z
Learning: In the Dub codebase, multiple payout records can now share the same stripeTransferId because payouts are grouped by partner and processed as single Stripe transfers. This is why the @unique constraint was removed from the stripeTransferId field in the Payout model - a single transfer can include multiple payouts for the same partner.
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.
apps/web/lib/partnerstack/import-customers.ts (1)
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.
🪛 Biome (1.9.4)
apps/web/lib/partnerstack/import-partners.ts
[error] 75-75: Avoid the delete operator which can impact performance.
(lint/performance/noDelete)
apps/web/lib/partnerstack/import-commissions.ts
[error] 73-73: Avoid the delete operator which can impact performance.
(lint/performance/noDelete)
apps/web/lib/partnerstack/import-customers.ts
[error] 128-128: Avoid the delete operator which can impact performance.
(lint/performance/noDelete)
⏰ 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 (1)
apps/web/lib/partnerstack/import-partners.ts (1)
84-149: Well-structured partner creation with proper validation.The implementation demonstrates good practices:
- Proper validation to skip partners with no customers
- Idempotent upsert operations for both partner and program enrollment
- Sound country code resolution logic
- Appropriate Redis caching for partner key mappings
- Good logging for debugging
Summary by CodeRabbit
New Features
Improvements
Bug Fixes
Chores