+
Skip to content

Conversation

devkiran
Copy link
Collaborator

@devkiran devkiran commented Aug 7, 2025

Summary by CodeRabbit

  • New Features

    • Added support for querying analytics and events using multiple link IDs, allowing users to retrieve data for several links at once.
    • Enhanced permission checks to ensure users only access analytics and events for links and folders they have permission to view.
  • Refactor

    • Improved efficiency of click event aggregation by processing commissions in batches rather than individually.

Copy link
Contributor

vercel bot commented Aug 7, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Updated (UTC)
dub ✅ Ready (Inspect) Visit Preview Aug 8, 2025 2:34am

Copy link
Contributor

coderabbitai bot commented Aug 7, 2025

Walkthrough

The changes introduce batch processing for click commission aggregation, enhance analytics and events endpoints to support multiple link IDs with folder-based permission checks, and update the analytics query schema to accept a linkIds parameter. The updates focus on more efficient data handling and refined access control within the API endpoints.

Changes

Cohort / File(s) Change Summary
Batch Click Commission Aggregation
apps/web/app/(ee)/api/cron/aggregate-clicks/route.ts
Refactored to batch process click commissions per program, replacing per-link loops with bulk operations, and added mapping, bulk insertion, and async syncing of total commissions. Updated imports and removed obsolete checks.
Multi-Link Analytics & Permission Checks
apps/web/app/api/analytics/route.ts, apps/web/app/(ee)/api/events/route.ts
Enhanced endpoints to accept multiple link IDs (linkIds), fetch and filter links by folder permissions, and adjust folder ID filtering logic. Added permission checks and filtering for multiple links and their folders.
Analytics Query Schema Update
apps/web/lib/zod/schemas/analytics.ts
Added an optional linkIds field to the analytics query schema, supporting both single strings and arrays, with transformation for comma-separated strings.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant API (Analytics/Events)
    participant DB
    participant Permissions

    Client->>API (Analytics/Events): GET request with linkIds
    API (Analytics/Events)->>DB: Fetch links by linkIds
    API (Analytics/Events)->>Permissions: Check folder access for each link
    Permissions-->>API (Analytics/Events): Allowed links/folders
    API (Analytics/Events)->>DB: Query events/analytics for allowed links/folders
    DB-->>API (Analytics/Events): Return results
    API (Analytics/Events)-->>Client: Respond with data
Loading
sequenceDiagram
    participant CronJob
    participant AggregateRoute
    participant DB
    participant SyncService

    CronJob->>AggregateRoute: Trigger aggregation
    AggregateRoute->>DB: Fetch all links for program
    AggregateRoute->>DB: Fetch click analytics in bulk
    AggregateRoute->>DB: Bulk insert commissions
    AggregateRoute->>SyncService: Sync total commissions per partner (async)
    SyncService-->>AggregateRoute: (async) Complete
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~40 minutes

Possibly related PRs

Suggested reviewers

  • devkiran

Poem

A rabbit hops through fields of code,
Where batch and bulk now share the load.
Permissions checked on every link,
Analytics flow in one swift blink.
With schemas new and logic neat,
This patch makes progress—oh, how sweet!
🐇✨

✨ 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-aggregate-clicks-cron

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@steven-tey steven-tey marked this pull request as ready for review August 7, 2025 20:35
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: 5

🧹 Nitpick comments (1)
apps/web/app/(ee)/api/cron/aggregate-clicks/route.ts (1)

102-113: Add safety check for link ID mapping.

While the non-null assertion should be safe given the data flow, consider adding a safety check to handle unexpected analytics results gracefully.

Apply this diff for safer mapping:

      const commissions = clickStats.map(
        ({ id: linkId, clicks: quantity }) => {
+         const partnerId = linkToPartnerMap.get(linkId);
+         if (!partnerId) {
+           console.warn(`Link ${linkId} not found in linkToPartnerMap`);
+           return null;
+         }
+         return {
          id: createId({ prefix: "cm_" }),
          programId,
-         partnerId: linkToPartnerMap.get(linkId)!,
+         partnerId,
          linkId,
          quantity,
          type: CommissionType.click,
          amount: 0,
          earnings: reward.amount * quantity,
-       }),
-     );
+         };
+       }).filter(Boolean) as NonNullable<typeof commissions[0]>[];
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 056187b and 4fa625e.

📒 Files selected for processing (4)
  • apps/web/app/(ee)/api/cron/aggregate-clicks/route.ts (3 hunks)
  • apps/web/app/(ee)/api/events/route.ts (4 hunks)
  • apps/web/app/api/analytics/route.ts (5 hunks)
  • apps/web/lib/zod/schemas/analytics.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-06-16T19:21:23.506Z
Learnt from: TWilson023
PR: dubinc/dub#2519
File: apps/web/ui/analytics/utils.ts:35-37
Timestamp: 2025-06-16T19:21:23.506Z
Learning: In the `useAnalyticsFilterOption` function in `apps/web/ui/analytics/utils.ts`, the pattern `options?.context ?? useContext(AnalyticsContext)` is intentionally designed as a complete replacement strategy, not a merge. When `options.context` is provided, it should contain all required fields (`baseApiPath`, `queryString`, `selectedTab`, `requiresUpgrade`) and completely replace the React context, not be merged with it. This is used for dependency injection or testing scenarios.

Applied to files:

  • apps/web/lib/zod/schemas/analytics.ts
  • apps/web/app/api/analytics/route.ts
📚 Learning: 2025-06-06T07:59:03.120Z
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/lib/zod/schemas/analytics.ts
  • apps/web/app/(ee)/api/events/route.ts
  • apps/web/app/api/analytics/route.ts
  • apps/web/app/(ee)/api/cron/aggregate-clicks/route.ts
📚 Learning: 2025-07-17T06:41:45.620Z
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.

Applied to files:

  • apps/web/app/(ee)/api/events/route.ts
📚 Learning: 2025-05-29T04:45:18.504Z
Learnt from: devkiran
PR: dubinc/dub#2448
File: packages/email/src/templates/partner-program-summary.tsx:0-0
Timestamp: 2025-05-29T04:45:18.504Z
Learning: In the PartnerProgramSummary email template (packages/email/src/templates/partner-program-summary.tsx), the stat titles are hardcoded constants ("Clicks", "Leads", "Sales", "Earnings") that will always match the ICONS object keys after toLowerCase() conversion, so icon lookup failures are not possible.

Applied to files:

  • apps/web/app/(ee)/api/cron/aggregate-clicks/route.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). (1)
  • GitHub Check: build
🔇 Additional comments (1)
apps/web/lib/zod/schemas/analytics.ts (1)

90-94: LGTM! Well-structured schema addition.

The linkIds field follows the established pattern used for tagIds and correctly handles both single string and array inputs with appropriate transformation.

}),
);

console.log(commissions);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Remove or sanitize commission data logging.

Logging full commission objects may expose sensitive financial data. Consider removing this log or logging only non-sensitive summary information.

Apply this diff to log summary information instead:

-     console.log(commissions);
+     console.log(`Creating ${commissions.length} commissions for program ${programId}`);
📝 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.

Suggested change
console.log(commissions);
console.log(`Creating ${commissions.length} commissions for program ${programId}`);
🤖 Prompt for AI Agents
In apps/web/app/(ee)/api/cron/aggregate-clicks/route.ts at line 115, the current
console.log outputs full commission objects, which may expose sensitive
financial data. Replace this log with a summary that excludes sensitive details,
such as logging only the count of commissions or aggregated totals, or remove
the log entirely to prevent sensitive data exposure.

Comment on lines +59 to +68
if (linkIds && linkIds.length) {
links = await prisma.link.findMany({
where: {
id: {
in: linkIds,
},
programId: workspace.id,
},
});

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix incorrect field name in database query.

The query uses programId but should use workspaceId to match the workspace's links.

Apply this diff to fix the field name:

      links = await prisma.link.findMany({
        where: {
          id: {
            in: linkIds,
          },
-         programId: workspace.id,
+         workspaceId: workspace.id,
        },
      });
📝 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.

Suggested change
if (linkIds && linkIds.length) {
links = await prisma.link.findMany({
where: {
id: {
in: linkIds,
},
programId: workspace.id,
},
});
if (linkIds && linkIds.length) {
links = await prisma.link.findMany({
where: {
id: {
in: linkIds,
},
workspaceId: workspace.id,
},
});
}
🤖 Prompt for AI Agents
In apps/web/app/(ee)/api/events/route.ts between lines 59 and 68, the Prisma
query incorrectly uses the field name 'programId' when filtering links by
workspace. Change the field name from 'programId' to 'workspaceId' in the where
clause to correctly filter links associated with the workspace.

Comment on lines +69 to +100
if (checkIfLinksHaveFolders(links)) {
const linkFolderIds = Array.from(
new Set(
links.map((link) => link.folderId).filter(Boolean) as string[],
),
);

const folderPermissions = await checkFolderPermissions({
workspaceId: workspace.id,
userId: session.user.id,
folderIds: linkFolderIds,
requiredPermission: "folders.read",
});

links = links.filter((link) => {
if (!link.folderId) {
return true;
}

const validFolder = folderPermissions.find(
(folder) => folder.folderId === link.folderId,
);

if (!validFolder?.hasPermission) {
return false;
}

folderIds?.push(link.folderId);

return true;
});
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Initialize folderIds array before usage.

The folderIds array is undefined when trying to push folder IDs, causing the collection to fail silently.

Apply this diff to initialize the array:

      if (checkIfLinksHaveFolders(links)) {
+       folderIds = [];
        const linkFolderIds = Array.from(
          new Set(
            links.map((link) => link.folderId).filter(Boolean) as string[],
          ),
        );
📝 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.

Suggested change
if (checkIfLinksHaveFolders(links)) {
const linkFolderIds = Array.from(
new Set(
links.map((link) => link.folderId).filter(Boolean) as string[],
),
);
const folderPermissions = await checkFolderPermissions({
workspaceId: workspace.id,
userId: session.user.id,
folderIds: linkFolderIds,
requiredPermission: "folders.read",
});
links = links.filter((link) => {
if (!link.folderId) {
return true;
}
const validFolder = folderPermissions.find(
(folder) => folder.folderId === link.folderId,
);
if (!validFolder?.hasPermission) {
return false;
}
folderIds?.push(link.folderId);
return true;
});
}
if (checkIfLinksHaveFolders(links)) {
// Initialize the folderIds array before collecting folder IDs
folderIds = [];
const linkFolderIds = Array.from(
new Set(
links.map((link) => link.folderId).filter(Boolean) as string[],
),
);
const folderPermissions = await checkFolderPermissions({
workspaceId: workspace.id,
userId: session.user.id,
folderIds: linkFolderIds,
requiredPermission: "folders.read",
});
links = links.filter((link) => {
if (!link.folderId) {
return true;
}
const validFolder = folderPermissions.find(
(folder) => folder.folderId === link.folderId,
);
if (!validFolder?.hasPermission) {
return false;
}
folderIds?.push(link.folderId);
return true;
});
}
🤖 Prompt for AI Agents
In apps/web/app/(ee)/api/events/route.ts around lines 69 to 100, the folderIds
array is used without initialization, causing errors when pushing folder IDs.
Initialize folderIds as an empty array before the if block that checks for links
with folders to ensure it is defined before pushing values into it.

Comment on lines +90 to +98
if (linkIds && linkIds.length) {
links = await prisma.link.findMany({
where: {
id: {
in: linkIds,
},
programId: workspace.id,
},
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix incorrect field name in database query.

The query uses programId but should use workspaceId to match the workspace's links.

Apply this diff to fix the field name:

      links = await prisma.link.findMany({
        where: {
          id: {
            in: linkIds,
          },
-         programId: workspace.id,
+         workspaceId: workspace.id,
        },
      });
📝 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.

Suggested change
if (linkIds && linkIds.length) {
links = await prisma.link.findMany({
where: {
id: {
in: linkIds,
},
programId: workspace.id,
},
});
if (linkIds && linkIds.length) {
links = await prisma.link.findMany({
where: {
id: {
in: linkIds,
},
workspaceId: workspace.id,
},
});
🤖 Prompt for AI Agents
In apps/web/app/api/analytics/route.ts between lines 90 and 98, the database
query incorrectly uses the field name programId when filtering links. Change the
field name from programId to workspaceId in the where clause to correctly match
links associated with the workspace.

Comment on lines +100 to +131
if (checkIfLinksHaveFolders(links)) {
const linkFolderIds = Array.from(
new Set(
links.map((link) => link.folderId).filter(Boolean) as string[],
),
);

const folderPermissions = await checkFolderPermissions({
workspaceId: workspace.id,
userId: session.user.id,
folderIds: linkFolderIds,
requiredPermission: "folders.read",
});

links = links.filter((link) => {
if (!link.folderId) {
return true;
}

const validFolder = folderPermissions.find(
(folder) => folder.folderId === link.folderId,
);

if (!validFolder?.hasPermission) {
return false;
}

folderIds?.push(link.folderId);

return true;
});
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Initialize folderIds array before usage.

The folderIds array is undefined when trying to push folder IDs, causing the collection to fail silently.

Apply this diff to initialize the array:

      if (checkIfLinksHaveFolders(links)) {
+       folderIds = [];
        const linkFolderIds = Array.from(
          new Set(
            links.map((link) => link.folderId).filter(Boolean) as string[],
          ),
        );
📝 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.

Suggested change
if (checkIfLinksHaveFolders(links)) {
const linkFolderIds = Array.from(
new Set(
links.map((link) => link.folderId).filter(Boolean) as string[],
),
);
const folderPermissions = await checkFolderPermissions({
workspaceId: workspace.id,
userId: session.user.id,
folderIds: linkFolderIds,
requiredPermission: "folders.read",
});
links = links.filter((link) => {
if (!link.folderId) {
return true;
}
const validFolder = folderPermissions.find(
(folder) => folder.folderId === link.folderId,
);
if (!validFolder?.hasPermission) {
return false;
}
folderIds?.push(link.folderId);
return true;
});
}
if (checkIfLinksHaveFolders(links)) {
folderIds = [];
const linkFolderIds = Array.from(
new Set(
links.map((link) => link.folderId).filter(Boolean) as string[],
),
);
const folderPermissions = await checkFolderPermissions({
workspaceId: workspace.id,
userId: session.user.id,
folderIds: linkFolderIds,
requiredPermission: "folders.read",
});
links = links.filter((link) => {
if (!link.folderId) {
return true;
}
const validFolder = folderPermissions.find(
(folder) => folder.folderId === link.folderId,
);
if (!validFolder?.hasPermission) {
return false;
}
folderIds?.push(link.folderId);
return true;
});
}
🤖 Prompt for AI Agents
In apps/web/app/api/analytics/route.ts around lines 100 to 131, the folderIds
array is used without initialization, causing errors when pushing folder IDs.
Initialize folderIds as an empty array before the if block or before it is first
used to ensure it is defined and can collect folder IDs properly.

@steven-tey steven-tey merged commit 86c240f into main Aug 8, 2025
8 checks passed
@steven-tey steven-tey deleted the improve-aggregate-clicks-cron branch August 8, 2025 05:10
@devkiran devkiran restored the improve-aggregate-clicks-cron branch August 8, 2025 05:43
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.

2 participants

点击 这是indexloc提供的php浏览器服务,不要输入任何密码和下载