-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Add attributeViaPromoCode to cases where dubCustomerId is provided
#2928
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.
|
WalkthroughStripe checkout-session-completed webhook now attempts promotion-code-based attribution when a Dub customer is not found. If a promotionCodeId exists and attribution succeeds, it updates linkId, customer, clickEvent, and leadEvent, and disables the lead webhook. If attribution fails, it returns a failure and skips. This branch is added in two not-found paths. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant Stripe as Stripe
participant Webhook as CheckoutSessionCompleted Handler
participant DB as Database
participant Promo as Promotion Attribution
participant Lead as Lead Webhook
Stripe->>Webhook: POST /webhook (checkout.session.completed)
Webhook->>DB: Find Dub customer by externalId / Stripe customerId
alt Customer found
Webhook->>DB: Proceed with standard updates
Webhook-->>Stripe: 200 OK
else Customer not found
alt promotionCodeId present
Webhook->>Promo: Attribute via promotion code
alt Attribution success
Webhook->>DB: Update linkId, customer, clickEvent, leadEvent
Webhook-x Lead: Disable lead webhook
Webhook-->>Stripe: 200 OK (attributed)
else Attribution failure
Webhook-->>Stripe: 200 OK (skip: attribution failed)
end
else No promotion code
Webhook-->>Stripe: 200 OK (skip: customer not found)
end
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 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 |
dubCustomerId is provided
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
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/stripe/integration/webhook/checkout-session-completed.ts (2)
592-607: Add idempotency checks for customer creation.The
attributeViaPromoCodefunction creates a new customer without checking if a customer with the samestripeCustomerIdalready exists. If the Stripe webhook is retried (due to network issues or timeout), this could create duplicate customers.Consider checking for an existing customer before creation:
+ // Check if customer already exists + const existingCustomer = await prisma.customer.findUnique({ + where: { + stripeCustomerId: charge.customer as string, + }, + }); + + if (existingCustomer) { + return { + linkId, + customer: existingCustomer, + clickEvent, + leadEvent: await getLeadEvent({ customerId: existingCustomer.id }).then( + (res) => res.data[0], + ), + }; + } + const customer = await prisma.customer.create({
581-620: Add error handling for recordFakeClick and recordLead.The
recordFakeClick(line 581) andrecordLead(line 620) calls are not wrapped in try-catch blocks. If either operation fails, the function will throw an exception, and the webhook handler will return a 500 error to Stripe, causing unnecessary retries.Consider adding error handling:
+ let clickEvent; + try { - const clickEvent = await recordFakeClick({ + clickEvent = await recordFakeClick({ link, customer: { continent: customerAddress?.country ? COUNTRIES_TO_CONTINENTS[customerAddress.country] : "Unknown", country: customerAddress?.country ?? "Unknown", region: customerAddress?.state ?? "Unknown", }, }); + } catch (error) { + console.error("Failed to record fake click", error); + return null; + } // ... customer creation ... + try { await recordLead(leadEvent); + } catch (error) { + console.error("Failed to record lead", error); + // Consider whether to return null or continue + }
🧹 Nitpick comments (4)
apps/web/app/(ee)/api/stripe/integration/webhook/checkout-session-completed.ts (4)
609-620: Consider idempotency for lead recording.The function records a lead without checking if one already exists for this customer. If the Stripe webhook is retried, this could result in duplicate lead events in Tinybird.
If Tinybird does not deduplicate based on
event_id, consider checking for an existing lead before recording:// Check if lead already exists const existingLead = await getLeadEvent({ customerId: customer.id }).then( (res) => res.data[0], ); if (!existingLead) { await recordLead(leadEvent); }Alternatively, ensure the Redis idempotency check (lines 267-293) covers lead creation as well, or leverage Tinybird's deduplication if available.
522-572: Consider structured error logging.The function uses
console.logfor error messages before returningnull. In production, these errors might be difficult to track without structured logging or error monitoring.Consider using a structured logger or error tracking service (e.g., Sentry) to capture these failure scenarios with context:
logger.warn("Promotion code attribution failed", { reason: "promotion_code_not_found", promotionCodeId, stripeAccountId, });This will improve observability and make debugging easier.
581-590: Document the "Unknown" fallback for missing address data.When customer address data is missing from the Stripe checkout session, the code falls back to "Unknown" for continent, country, and region. This is reasonable for handling incomplete data, but it might affect analytics and reporting.
Consider documenting this behavior in a comment to clarify the expected impact on analytics:
// Note: If customer address is missing, we use "Unknown" for geo fields. // This ensures attribution succeeds but may affect geo-based analytics. const clickEvent = await recordFakeClick({ link, customer: { continent: customerAddress?.country ? COUNTRIES_TO_CONTINENTS[customerAddress.country] : "Unknown", country: customerAddress?.country ?? "Unknown", region: customerAddress?.state ?? "Unknown", }, });
599-599: Use the stripeCustomerId variable instead of unsafe casting.Line 599 casts
charge.customer as string, but according to Stripe types,charge.customercan be astring | Stripe.Customer | Stripe.DeletedCustomer | null. While the main handler verifiesstripeCustomerIdis present (line 145), directly casting here is fragile.Pass
stripeCustomerIdas a parameter toattributeViaPromoCodeand use it directly:async function attributeViaPromoCode({ promotionCodeId, stripeAccountId, livemode, charge, + stripeCustomerId, }: { promotionCodeId: string; stripeAccountId: string; livemode: boolean; charge: Stripe.Checkout.Session; + stripeCustomerId: string; }) { // ... const customer = await prisma.customer.create({ data: { // ... - stripeCustomerId: charge.customer as string, + stripeCustomerId, // ... }, });Then update the callers at lines 180 and 224 to pass
stripeCustomerId.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
apps/web/app/(ee)/api/stripe/integration/webhook/checkout-session-completed.ts(2 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (2)
apps/web/app/(ee)/api/stripe/integration/webhook/checkout-session-completed.ts (2)
149-169: LGTM: Clear documentation of promotion code flow.The updated comments clearly describe the new promotion code attribution logic and the two scenarios where it applies. This improves code readability and maintainability.
179-194: Promo code attribution fallback is sound
shouldSendLeadWebhook correctly prevents duplicate lead.created webhooks after promotion code attribution, and defined leadEvent skips the Tinybird fetch as intended.
Summary by CodeRabbit
New Features
Bug Fixes