Skip to content

fix(dashboard-api): optimize getGlobalStats database connections concurrency#324

Open
Kirtan-pc wants to merge 1 commit into
geturbackend:mainfrom
Kirtan-pc:fix/global-stats-parallel-connections
Open

fix(dashboard-api): optimize getGlobalStats database connections concurrency#324
Kirtan-pc wants to merge 1 commit into
geturbackend:mainfrom
Kirtan-pc:fix/global-stats-parallel-connections

Conversation

@Kirtan-pc

@Kirtan-pc Kirtan-pc commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Description

Resolves #285 .

Previously, getGlobalStats in analytics.controller.js iterated over every developer project in a sequential for...of loop, awaiting getConnection and countDocuments for each project. Under real load with many projects, this caused a linear slowdown (O(N) latency) before returning statistics.

This change parallelizes the connection and user count calls using Promise.all(projects.map(...)), reducing the latency to O(1) matching the slowest single connection. It retains project-level error trapping so that a failure in one project's database connection does not break the entire endpoint.

🛠️ Type of Change

  • 🐛 Bug fix (non-breaking change which fixes an issue)
  • ✨ New feature (non-breaking change which adds functionality)
  • 💥 Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • 📝 Documentation update
  • 🎨 UI/UX improvement (Frontend only)
  • ⚙️ Refactor / Chore

🧪 Testing & Validation

Backend Verification:

  • I have run tests in apps/dashboard-api and all tests passed.
  • New unit tests have been added (if applicable) under apps/dashboard-api/src/__tests__/analytics.controller.test.js.

Frontend Verification:

  • I have run npm run lint in the frontend/ directory.
  • Verified the UI changes on different screen sizes (Responsive).
  • Checked for any console errors in the browser dev tools.

📸 Screenshots / Recordings (Optional)

✅ Checklist

  • My code follows the code style of this project.
  • I have performed a self-review of my code.
  • I have commented my code, particularly in hard-to-understand areas.
  • My changes generate no new warnings or errors.
  • I have updated the documentation (README/Docs) accordingly.

Built with ❤️ for urBackend.

Summary by CodeRabbit

  • Tests

    • Added comprehensive test coverage for analytics features including global stats, activity, funnel, retention, engagement, and North Star metrics.
  • Refactor

    • Improved performance of global statistics retrieval with parallel processing for user count calculations.

…urrency

Fetch per-project user counts in parallel using Promise.all instead of sequentially to prevent linear slowdown with project counts.
@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

getGlobalStats in analytics.controller.js is refactored to replace a sequential for...of user-count loop with Promise.all, running all per-project DB connection and count operations concurrently with per-project error fallback to 0. A new Jest test file adds full coverage for all six analytics controller methods.

Analytics Controller: Parallel User Counting and Test Coverage

Layer / File(s) Summary
Parallel user counting in getGlobalStats
apps/dashboard-api/src/controllers/analytics.controller.js
Replaces sequential for...of with Promise.all over per-project async tasks; each task catches errors and returns 0, and the resolved array is reduced to totalUsers.
Test scaffolding and all controller test cases
apps/dashboard-api/src/__tests__/analytics.controller.test.js
Introduces Jest mock scaffolding for @urbackend/common models/helpers and covers getGlobalStats (including parallel error fallback), getRecentActivity, getActivationFunnel, getRetention (with and without signup event), getEngagement, and getNorthStar (including zero-project guard).

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant getGlobalStats
  participant Promise.all
  participant getConnection
  participant MongoDB

  Client->>getGlobalStats: GET /api/analytics/global
  getGlobalStats->>Promise.all: projects.map(async project => ...)
  par Project A
    Promise.all->>getConnection: getConnection(projectA._id)
    getConnection-->>Promise.all: connA
    Promise.all->>MongoDB: connA.collection('users').countDocuments()
    MongoDB-->>Promise.all: countA
  and Project B (fails)
    Promise.all->>getConnection: getConnection(projectB._id)
    getConnection-->>Promise.all: Error
    Promise.all-->>Promise.all: catch → return 0
  end
  Promise.all-->>getGlobalStats: [countA, 0, ...]
  getGlobalStats->>getGlobalStats: reduce → totalUsers
  getGlobalStats-->>Client: res.json({ totalUsers, ... })
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~15 minutes

Possibly related issues

  • #259: The Promise.all refactor in getGlobalStats directly implements the concurrent execution pattern described as the expected fix in this issue, eliminating the sequential O(N×latency) DB connection loop.

Poem

🐇 No more waiting in a row,
Each DB call can steal the show!
Promise.all — we leap in sync,
No sequential bottleneck to sink.
The dashboard flies, the latency shrinks,
This bunny hops faster than you'd think! 🚀

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the primary change: optimizing database connection concurrency in getGlobalStats by replacing sequential with parallel operations.
Linked Issues check ✅ Passed The PR successfully implements the fix for issue #285: getGlobalStats now uses Promise.all(projects.map(...)) to parallelize database connections and user counting, reducing latency from O(N×latency) to O(1×max_latency), with per-project error handling to prevent cascading failures.
Out of Scope Changes check ✅ Passed All changes are directly scoped to resolving issue #285: the analytics.controller.js modification optimizes getGlobalStats concurrency, and the test file additions verify the fix with comprehensive test coverage for the controller's analytics methods.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with 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.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
apps/dashboard-api/src/__tests__/analytics.controller.test.js (1)

88-155: ⚡ Quick win

The “parallel” test currently allows a sequential regression.

Line 88-155 only checks call count and arguments; a for...of await implementation would still pass. Use a deferred first getConnection promise and assert the second getConnection is called before the first resolves.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/dashboard-api/src/__tests__/analytics.controller.test.js` around lines
88 - 155, The test for the getGlobalStats method labeled 'should return
aggregated global stats with user counts fetched in parallel' does not actually
verify parallel execution because it only checks call counts and arguments. To
properly test parallelism, create a deferred first promise for the initial
mockGetConnection call that doesn't resolve immediately, setup the second
mockGetConnection to track when it's called relative to the first promise's
resolution, and add an assertion verifying that the second getConnection is
invoked before the first promise resolves. This will ensure the implementation
uses concurrent execution like Promise.all rather than sequential awaits like
for...of await.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/dashboard-api/src/controllers/analytics.controller.js`:
- Around line 43-53: The code at lines 43-53 creates unbounded parallel database
connection tasks by mapping over all projects without concurrency limits, which
can cause connection spikes when there are many projects. Instead of directly
mapping all projects and using Promise.all on userCountPromises, implement a
bounded concurrency pattern such as a queue or semaphore that limits the maximum
number of concurrent getConnection and countDocuments operations. This ensures
only a small fixed number of projects are being processed in parallel at any
given time (e.g., 5-10 concurrent connections) while maintaining correct
aggregation of userCounts into totalUsers.

---

Nitpick comments:
In `@apps/dashboard-api/src/__tests__/analytics.controller.test.js`:
- Around line 88-155: The test for the getGlobalStats method labeled 'should
return aggregated global stats with user counts fetched in parallel' does not
actually verify parallel execution because it only checks call counts and
arguments. To properly test parallelism, create a deferred first promise for the
initial mockGetConnection call that doesn't resolve immediately, setup the
second mockGetConnection to track when it's called relative to the first
promise's resolution, and add an assertion verifying that the second
getConnection is invoked before the first promise resolves. This will ensure the
implementation uses concurrent execution like Promise.all rather than sequential
awaits like for...of await.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d2de2de5-e419-41b1-bd63-09168990e616

📥 Commits

Reviewing files that changed from the base of the PR and between 9ccebb3 and 79e3631.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (2)
  • apps/dashboard-api/src/__tests__/analytics.controller.test.js
  • apps/dashboard-api/src/controllers/analytics.controller.js

Comment on lines +43 to +53
const userCountPromises = projects.map(async (project) => {
try {
const conn = await getConnection(project._id.toString());
const userCount = await conn.collection('users').countDocuments();
totalUsers += userCount;
return await conn.collection('users').countDocuments();
} catch (err) {
console.error(`Failed to count users for project ${project._id}:`, err.message);
return 0;
}
}
});
const userCounts = await Promise.all(userCountPromises);
const totalUsers = userCounts.reduce((sum, count) => sum + count, 0);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Bound per-project DB counting concurrency to avoid connection spikes.

Line 43 fans out one getConnection + countDocuments task per project with no upper bound. With many projects, this can overload DB/socket resources and hurt endpoint stability. A small concurrency cap keeps the latency win while preventing burst pressure.

Proposed fix (bounded parallelism)
-    const userCountPromises = projects.map(async (project) => {
-      try {
-        const conn = await getConnection(project._id.toString());
-        return await conn.collection('users').countDocuments();
-      } catch (err) {
-        console.error(`Failed to count users for project ${project._id}:`, err.message);
-        return 0;
-      }
-    });
-    const userCounts = await Promise.all(userCountPromises);
-    const totalUsers = userCounts.reduce((sum, count) => sum + count, 0);
+    const USER_COUNT_CONCURRENCY = 10;
+    let totalUsers = 0;
+
+    for (let i = 0; i < projects.length; i += USER_COUNT_CONCURRENCY) {
+      const batch = projects.slice(i, i + USER_COUNT_CONCURRENCY);
+      const batchCounts = await Promise.all(
+        batch.map(async (project) => {
+          try {
+            const conn = await getConnection(project._id.toString());
+            return await conn.collection("users").countDocuments();
+          } catch (err) {
+            console.error(`Failed to count users for project ${project._id}:`, err.message);
+            return 0;
+          }
+        })
+      );
+      totalUsers += batchCounts.reduce((sum, count) => sum + count, 0);
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/dashboard-api/src/controllers/analytics.controller.js` around lines 43 -
53, The code at lines 43-53 creates unbounded parallel database connection tasks
by mapping over all projects without concurrency limits, which can cause
connection spikes when there are many projects. Instead of directly mapping all
projects and using Promise.all on userCountPromises, implement a bounded
concurrency pattern such as a queue or semaphore that limits the maximum number
of concurrent getConnection and countDocuments operations. This ensures only a
small fixed number of projects are being processed in parallel at any given time
(e.g., 5-10 concurrent connections) while maintaining correct aggregation of
userCounts into totalUsers.

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.

[Bug] analytics.controller: getGlobalStats opens N sequential DB connections to count users — blocks event loop

1 participant