Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@steven-tey
Copy link
Collaborator

@steven-tey steven-tey commented Sep 11, 2025

Summary by CodeRabbit

  • Bug Fixes

    • Duplicate sale tracking (same invoice ID) now returns an idempotent cached success on repeat submissions, avoiding re-processing.
  • New Features

    • Lead tracking responses now include link details alongside click and customer data, making link info available to consumers.
  • Tests

    • Updated tests to validate idempotent sale responses and the new link field in lead responses.

@vercel
Copy link
Contributor

vercel bot commented Sep 11, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Updated (UTC)
dub Ready Ready Preview Sep 11, 2025 3:58pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 11, 2025

Walkthrough

Adds workspace-scoped idempotency and 7-day cached responses for trackSale when invoiceId is provided; fetches, persists, and returns the full Link object in trackLead responses; updates schemas and tests to include link and idempotent/cached sale behavior.

Changes

Cohort / File(s) Summary
Track sale idempotency & caching
apps/web/lib/api/conversions/track-sale.ts
Replace single Redis NX-set gate with MGET of legacy global and workspace-scoped keys; if scoped key present, return its cached response; if only legacy key present, return minimal { eventName, customer: null, sale: null }; otherwise process sale and SETEX full response under trackSale:${workspace.id}:invoiceId:${invoiceId} with 7-day TTL. Refactor to assign/return a trackSaleResponse constant.
Track sale tests
apps/web/tests/tracks/track-sale.test.ts
Update test name/comment to reflect idempotent behavior; replace explicit status/data assertions on repeated invoiceId calls with expectValidSaleResponse(response, sale) to assert the cached/full sale response.
Track lead: Link retrieval & return
apps/web/lib/api/conversions/track-lead.ts
Import Link type; hoist `let link: Link
Zod schema for leads
apps/web/lib/zod/schemas/leads.ts
Add link to trackLeadResponseSchema as nullable LinkSchema.pick(...) with selected fields (id, domain, key, shortLink, url, partnerId, programId, tenantId, externalId).
Track lead tests (client & server)
apps/web/tests/tracks/track-lead-client.test.ts, apps/web/tests/tracks/track-lead.test.ts
Add link to request payloads and response expectations; update test names and fixtures to reflect link inclusion and client vs click origins.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant C as Client
  participant API as /trackSale
  participant R as Redis
  participant S as SaleProcessor

  C->>API: POST /trackSale (invoiceId, workspaceId, payload)
  API->>R: MGET legacyKey, scopedKey
  alt scopedKey exists
    R-->>API: scopedResponse
    API-->>C: return scopedResponse
  else legacyKey exists
    R-->>API: legacyFlag
    API-->>C: return { eventName, customer: null, sale: null }
  else no keys
    API->>S: process sale
    S-->>API: trackSaleResponse
    API->>R: SETEX scopedKey 7d trackSaleResponse
    API-->>C: return trackSaleResponse
  end
Loading
sequenceDiagram
  autonumber
  participant C as Client
  participant API as /track/lead
  participant DB as Prisma
  participant W as Workspace

  C->>API: POST /track/lead (clickId, linkId?, payload)
  API->>DB: findUnique Link (full object)
  alt Link found
    API->>DB: Promise.all([update Link, update Workspace])
    DB-->>API: [updatedLink, updatedWorkspace]
    API->>API: assign link = updatedLink
    API->>W: run workflows / create partner commissions (uses updatedLink)
    API-->>C: return { click, customer, link }
  else no Link
    DB-->>API: null
    API-->>C: return { click, customer, link: null }
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • devkiran

Pre-merge checks (3 passed)

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "Improve /track/ endpoints" is concise and correctly identifies the PR's focus on the /track endpoints (changes to track-sale and track-lead such as idempotency/caching and exposing link data), so it relates to the main changeset; while somewhat broad, it is clear enough for a teammate scanning history.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.

Poem

I nibble keys in Redis shade,
Two gates check what past calls made.
A cached crumb for seven days,
Links hop back into the praise.
Hooray — idempotent trails for all my plays! 🐇✨


📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dc4248e and 5dc8394.

📒 Files selected for processing (1)
  • apps/web/lib/zod/schemas/leads.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/lib/zod/schemas/leads.ts
⏰ 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). (2)
  • GitHub Check: Vade Review
  • GitHub Check: build
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch improve-tracks

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
apps/web/lib/api/conversions/track-sale.ts (3)

60-76: Idempotency race: no lock → duplicate sales possible under concurrency.

Two requests with the same invoiceId can pass the checks before the cache is written and both record a sale. Add a per-workspace+invoice lock using SET NX with a short TTL and fall back to cached/minimal response when the lock isn’t acquired. Also parse cached values via the response schema.

Suggested patch:

   // Return idempotent response if invoiceId is already processed
   if (invoiceId) {
-    const [oldKeyValue, newKeyValue] = await redis.mget(legacyKey, resultKey);
+    const legacyKey = `dub_sale_events:invoiceId:${invoiceId}`;
+    const resultKey = `trackSale:${workspace.id}:invoiceId:${invoiceId}`;
+    const lockKey = `${resultKey}:lock`;
+    const [oldKeyValue, newKeyValue] = await redis.mget(legacyKey, resultKey);
 
-    if (newKeyValue) {
-      return newKeyValue;
+    if (newKeyValue) {
+      return trackSaleResponseSchema.parse(newKeyValue);
     } else if (oldKeyValue) {
       return {
         eventName,
         customer: null,
         sale: null,
       };
     }
+
+    // Acquire lock to prevent double-processing (expires automatically)
+    const acquired = await redis.set(lockKey, 1, { nx: true, ex: 60 });
+    if (!acquired) {
+      // Another worker is handling this invoice; best-effort read
+      const cached = await redis.get(resultKey);
+      if (cached) return trackSaleResponseSchema.parse(cached);
+      return { eventName, customer: null, sale: null };
+    }
   }

Reference for SET options (nx, ex): Upstash TS docs. (upstash.com)


548-558: metadata can be undefined → Zod parse failure.

trackSaleResponseSchema.sale.metadata is nullable, not optional; passing undefined will fail validation and 500 the request. Default to null.

   const trackSaleResponse = trackSaleResponseSchema.parse({
     eventName,
     customer,
     sale: {
       amount,
       currency,
       invoiceId,
       paymentProcessor,
-      metadata,
+      metadata: metadata ?? null,
     },
   });

67-75: Validate cached response before returning; avoid returning any.

When returning newKeyValue, parse/cast to the response schema to ensure shape consistency and type-safety.

-    if (newKeyValue) {
-      return newKeyValue;
+    if (newKeyValue) {
+      return trackSaleResponseSchema.parse(newKeyValue);
     }
🧹 Nitpick comments (2)
apps/web/lib/api/conversions/track-sale.ts (1)

560-568: Consider PII minimization in cache.

You cache the full customer (email, name, avatar) for 7 days in Redis. If privacy constraints apply, consider caching only a stable sale fingerprint and reloading fresh customer fields, or encrypting/shortening TTL.

apps/web/tests/tracks/track-sale.test.ts (1)

97-144: Optional: add a concurrency/idempotency test.

Spawn two parallel POSTs with the same invoiceId and assert only one commission/sale is recorded, the other returns the cached response. Helps prevent regressions.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e15843d and f9ad68b.

📒 Files selected for processing (2)
  • apps/web/lib/api/conversions/track-sale.ts (3 hunks)
  • apps/web/tests/tracks/track-sale.test.ts (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
apps/web/lib/api/conversions/track-sale.ts (2)
apps/web/lib/upstash/redis.ts (1)
  • redis (4-7)
apps/web/lib/zod/schemas/sales.ts (1)
  • trackSaleResponseSchema (98-118)
⏰ 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). (2)
  • GitHub Check: Vade Review
  • GitHub Check: build
🔇 Additional comments (1)
apps/web/tests/tracks/track-sale.test.ts (1)

84-95: LGTM: assertion updated to expect full idempotent response.

Matches the new behavior of returning the cached sale when invoiceId repeats.

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/lib/api/conversions/track-lead.ts (1)

274-301: eventQuantity=0 bug: payload vs. counters diverge.

createLeadEventPayload uses a truthy check (records once when 0), while counters use eventQuantity ?? 1 (increments by 0). This records one event but increments zero — inconsistent.

Apply a single sanitized quantity and use it everywhere (payload, counters, commissions):

@@
-  let isDuplicateEvent = false;
+  let isDuplicateEvent = false;
+  // Normalize quantity early; 0/negative -> 1 (paired with schema tightening)
+  const qty = typeof eventQuantity === "number" && eventQuantity > 0
+    ? Math.floor(eventQuantity)
+    : 1;
@@
-    const createLeadEventPayload = (customerId: string) => {
+    const createLeadEventPayload = (customerId: string) => {
       const basePayload = {
@@
-      return eventQuantity
-        ? Array(eventQuantity)
+      return qty > 1
+        ? Array(qty)
             .fill(null)
             .map(() => ({
               ...basePayload,
               event_id: nanoid(16),
             }))
         : basePayload;
     };
@@
-              data: {
-                leads: {
-                  increment: eventQuantity ?? 1,
-                },
-              },
+              data: { leads: { increment: qty } },
@@
-              data: {
-                usage: {
-                  increment: eventQuantity ?? 1,
-                },
-              },
+              data: { usage: { increment: qty } },
@@
-              quantity: eventQuantity ?? 1,
+              quantity: qty,

Also consider tightening the schema as suggested in leads.ts to forbid non-positive values.

🧹 Nitpick comments (4)
apps/web/tests/tracks/track-lead-client.test.ts (1)

46-53: Strengthen link assertions; avoid self-referential expectation.

Using link: leadResponse.link makes the check tautological. Assert the shape/values you know (domain/key) so we actually validate the new response field.

-    expect(leadResponse).toStrictEqual({
-      click: {
-        id: clickId,
-      },
-      link: leadResponse.link,
-      customer: customer,
-    });
+    expect(leadResponse).toMatchObject({
+      click: { id: clickId },
+      customer,
+    });
+    expect(leadResponse.link).toMatchObject({
+      id: expect.any(String),
+      domain: "getacme.link",
+      key: "derek",
+    });
apps/web/tests/tracks/track-lead.test.ts (1)

22-24: Make link check meaningful and resilient to duplicates.

The current link: response.data.link is self-referential. Validate known fields when present, but allow null for idempotent duplicates.

-  expect(response.data).toStrictEqual({
-    click: {
-      id: clickId,
-    },
-    link: response.data.link,
-    customer,
-  });
+  expect(response.data).toMatchObject({
+    click: { id: clickId },
+    customer,
+  });
+  if (response.data.link) {
+    expect(response.data.link).toMatchObject({
+      id: expect.any(String),
+      domain: "getacme.link",
+      key: "derek",
+    });
+  } else {
+    expect(response.data.link).toBeNull();
+  }
apps/web/lib/api/conversions/track-lead.ts (2)

236-337: Add error guard inside waitUntil to avoid unhandled rejections.

Failures in recordLead, uploads, or webhooks currently bubble silently. Wrap the async body in try/catch and log.

-    waitUntil(
-      (async () => {
+    waitUntil(
+      (async () => {
+        try {
@@
-          await sendWorkspaceWebhook({
+          await sendWorkspaceWebhook({
             trigger: "lead.created",
             data: transformLeadEventData({
               ...clickData,
               eventName,
               link,
               customer,
             }),
             workspace,
           });
-        }
-      })(),
+        }
+        } catch (err) {
+          console.error("trackLead waitUntil error", {
+            err,
+            clickId,
+            workspaceId: workspace.id,
+          });
+        }
+      })(),
     );

340-351: Response may contain pre-update link — acceptable here.

Because only counters change in updatedLink and those fields aren’t in the response pick, returning the pre-update link is fine.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f9ad68b and 256ecf8.

📒 Files selected for processing (6)
  • apps/web/lib/api/conversions/track-lead.ts (6 hunks)
  • apps/web/lib/api/conversions/track-sale.ts (3 hunks)
  • apps/web/lib/zod/schemas/leads.ts (2 hunks)
  • apps/web/tests/tracks/track-lead-client.test.ts (2 hunks)
  • apps/web/tests/tracks/track-lead.test.ts (2 hunks)
  • apps/web/tests/tracks/track-sale.test.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/web/tests/tracks/track-sale.test.ts
  • apps/web/lib/api/conversions/track-sale.ts
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-25T17:33:45.072Z
Learnt from: devkiran
PR: dubinc/dub#2736
File: apps/web/app/(ee)/api/stripe/integration/webhook/invoice-paid.ts:12-12
Timestamp: 2025-08-25T17:33:45.072Z
Learning: The WorkflowTrigger enum in packages/prisma/schema/workflow.prisma contains three values: leadRecorded, saleRecorded, and commissionEarned. All three are properly used throughout the codebase.

Applied to files:

  • apps/web/lib/api/conversions/track-lead.ts
🧬 Code graph analysis (1)
apps/web/tests/tracks/track-lead.test.ts (2)
apps/web/tests/utils/helpers.ts (1)
  • randomCustomer (7-17)
apps/web/lib/types.ts (1)
  • TrackLeadResponse (388-388)
⏰ 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). (2)
  • GitHub Check: Vade Review
  • GitHub Check: build
🔇 Additional comments (5)
apps/web/tests/tracks/track-lead-client.test.ts (1)

26-26: Test name clarifies precondition — good change.

apps/web/tests/tracks/track-lead.test.ts (1)

178-188: Renames for backward-compat tests look good.

Variable renames (customer5/customer6) and aligned expectations are consistent and improve clarity.

Also applies to: 191-195, 199-209, 212-216

apps/web/lib/zod/schemas/leads.ts (1)

5-5: Importing LinkSchema is correct.

apps/web/lib/api/conversions/track-lead.ts (2)

18-18: Type import for Link/WorkflowTrigger — good.


139-145: Fetching full Link is fine given response stripping.

You're returning the link through a schema that strips extra fields, so fetching full Link here is acceptable.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants