Skip to content

Conversation

Codebmk
Copy link
Member

@Codebmk Codebmk commented Oct 14, 2025

Summary by CodeRabbit

  • New Features

    • Unified devices list with cross-category pagination.
    • Added "gas" device category and updated device guidance banner.
    • Data table can open with a preselected filter via a provided key.
    • Device-category selectable for downloads.
  • Improvements

    • Cohort-aware devices/sites fetching when a group is present.
    • Clearer, consistent error messages for downloads and listings.
    • Streamlined device/site retrieval and retry flows.
  • Refactor

    • Consolidated multiple device data paths into a single devices flow.
  • Reliability

    • More robust default group handling during login.

Status of maturity (all need to be checked before merging):

  • I've tested this locally
  • I consider this code done
  • This change ready to hit production in its current state
  • The title of the PR states what changed and the related issues number (used for the release note).
  • I've included issue number in the "Closes #ISSUE-NUMBER" part of the "What are the relevant tickets?" section to link the issue.
  • I've updated corresponding documentation for the changes in this PR.
  • I have written unit and/or e2e tests for my change(s).

How should this be manually tested?

  • Please include the steps to be done inorder to setup and test this PR.

What are the relevant tickets?

Screenshots (optional)

image image

@Codebmk Codebmk self-assigned this Oct 14, 2025
Copy link
Contributor

coderabbitai bot commented Oct 14, 2025

📝 Walkthrough

Walkthrough

Consolidates device/site pagination into cohort-based fetching using groupId and device category; adds cohort APIs and URLs, forwards new DataTable prop activeFilterKey, includes gas as a device category, standardizes login fallback via AIRQO_GROUP_IDS/getAirqoGroupId, and normalizes errors with getApiErrorMessage.

Changes

Cohort / File(s) Summary
Data table prop update
src/platform/src/common/features/download-insights-locations/components/DataTable.jsx
Adds public prop activeFilterKey; initializes and synchronizes internal activeFilter from a matching filter key or fallback, and resets column filters and current page when activeFilterKey or filters change.
Data download consolidation
src/platform/src/common/features/download-insights-locations/data-download/DataDownload.jsx
Replaces multiple device-specific paginated hooks with a single usePaginatedDevicesSummary (now called with an options object using group: groupId, category, ...), uses groupId/groupTitle from useGetActiveGroup, centralizes devices into devicesData, updates pagination/metadata handlers, and maps errors via getApiErrorMessage.
Prop forwarding & banner
src/platform/src/common/features/download-insights-locations/data-download/components/DataContent.js
Forwards activeFilterKey into DataTable; device info banner now includes gas category and uses a mapping for device-specific banner text.
Device category validation & options
src/platform/src/common/features/download-insights-locations/data-download/hooks/useDataDownloadLogic.js, src/platform/src/common/features/download-insights-locations/data-download/constants.js
Adds 'gas' to valid device categories and appends { id: 4, name: 'gas' } to DEVICE_CATEGORY_OPTIONS.
Cohort APIs
src/platform/src/core/apis/Account.js, src/platform/src/core/apis/DeviceRegistry.js
Adds getGroupCohortsApi(groupId, signal) and cohort-targeted endpoints: getDevicesForCohortsApi({ cohort_ids, skip, limit, search, category, signal }) and getSitesForCohortsApi({ cohort_ids, skip, limit, search, category, signal }) with JWT auth and abort-aware handling.
Paginated hooks refactor
src/platform/src/core/hooks/usePaginatedData.js, src/platform/src/core/hooks/analyticHooks.js
usePaginatedDevicesSummary now accepts options (including category) and supports cohort-based fetching with SWR keys including category; cohort-based sites fetching added. Removed exports/re-exports of usePaginatedMobileDevices, usePaginatedBAMDevices, and usePaginatedLowCostDevices.
URLs & constants
src/platform/src/core/urls/authentication.js, src/platform/src/core/urls/deviceRegistry.js, src/platform/src/lib/constants.js
Adds getGroupCohortsUrl(groupId), DEVICES_FOR_COHORTS_URL = '/devices/cohorts/devices', SITES_FOR_COHORTS_URL = '/devices/cohorts/sites', and introduces AIRQO_GROUP_IDS with getAirqoGroupId() selector.
Login fallback
src/platform/src/core/utils/loginSetup.js
Replaces prior conditional login fallback with a single default AirQo group constructed via getAirqoGroupId() and fixed slug/title values.
Analytics & downloads
src/platform/src/core/apis/Analytics.js, src/platform/src/common/features/analytics-overview/hooks/useAnalyticsDownload.js, src/platform/src/common/features/download-insights-locations/more-insights/hooks/useDownload.js
getDeviceSummaryApi gains optional category param; download helpers now accept/propagate a deviceCategory parameter (default 'lowcost') into requests.
Error handling utility usage
src/platform/src/common/features/download-insights-locations/data-download/DataDownload.jsx, src/platform/src/core/hooks/usePaginatedData.js
Introduces getApiErrorMessage to normalize error messages and maps abort/timeout to "Request cancelled" where applicable.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant UI as DataDownload UI
  participant Hook as usePaginatedDevicesSummary
  participant Account as Account.getGroupCohortsApi
  participant RegistryDev as DeviceRegistry.getDevicesForCohortsApi
  participant RegistrySites as DeviceRegistry.getSitesForCohortsApi
  participant Cache as SWR Cache

  User->>UI: Open Download Insights
  UI->>Hook: init({ group: groupId, category, search, pagination })
  Hook->>Cache: lookup(['devices-summary-paginated', groupId||'all', category, search||''])
  alt cache miss
    Hook->>Account: getGroupCohortsApi(groupId, signal)
    Account-->>Hook: cohort_ids
    alt cohorts found
      par fetch devices and sites
        Hook->>RegistryDev: getDevicesForCohortsApi({ cohort_ids, limit, skip, category, search, signal })
        RegistryDev-->>Hook: { data: devices, meta }
        Hook->>RegistrySites: getSitesForCohortsApi({ cohort_ids, limit, skip, category, search, signal })
        RegistrySites-->>Hook: { data: sites, meta }
      end
      Hook->>Cache: set(key, { data, meta })
    else no cohorts
      Hook-->>Cache: set(key, { data: [], meta: {} })
    end
  else cache hit
    Cache-->>Hook: cached { data, meta }
  end
  Hook-->>UI: { data, isLoading, isError, error, meta, loadMore, nextPage, prevPage }
  UI-->>User: Render tables, banners (includes gas), pagination, errors (via getApiErrorMessage)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

Cohorts call out names in rows and streams,
Devices assemble under category dreams.
Gas joins the chorus, filters align,
A fallback group steadies the sign.
Hooks hum, URLs map the lines.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title succinctly describes the primary change of migrating site and device data download flows to use Cohorts APIs within the analytics feature without extraneous details, making it clear and focused on the main update.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch cohorts-data-download

Warning

Review ran into problems

🔥 Problems

Errors were encountered while retrieving linked issues.

Errors (1)
  • JIRA integration encountered authorization issues. Please disconnect and reconnect the integration in the CodeRabbit UI.

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

New next-platform changes available for preview here

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 (1)
src/platform/src/common/features/download-insights-locations/data-download/DataDownload.jsx (1)

336-343: Bug: wrong variable passed to getApiErrorMessage

You’re passing siteAndDeviceIds (data) instead of the error object, so the message is unreliable.

   const {
     data: siteAndDeviceIds,
     isLoading: isLoadingSiteIds,
     isError: isSiteIdsError,
+    error: siteIdsErrorMsg,
   } = useSiteAndDeviceIds(selectedGridId);
@@
   } else if (isSiteIdsError) {
-    setStatusMessage(getApiErrorMessage(siteAndDeviceIds) || 'Error loading data. Please try again.');
+    setStatusMessage(
+      getApiErrorMessage(siteIdsErrorMsg) || 'Error loading data. Please try again.',
+    );
     setMessageType(MESSAGE_TYPES.ERROR);
🧹 Nitpick comments (6)
src/platform/src/common/features/download-insights-locations/data-download/components/DataContent.js (1)

187-196: Forwarding activeFilterKey — good; drop unused pagination props to reduce noise

DataTable doesn’t consume enableInfiniteScroll, canLoadMore, or hasNextPage (it destructures underscored variants). Passing these can confuse readers.

Apply this diff:

           defaultSortDirection="asc"
           activeFilterKey={activeFilterKey}
           // Pagination props
-          enableInfiniteScroll={enableInfiniteScroll}
           paginationMeta={paginationMeta}
           onLoadMore={onLoadMore}
           onNextPage={onNextPage}
           onPrevPage={onPrevPage}
-          canLoadMore={canLoadMore}
-          hasNextPage={hasNextPage}
src/platform/src/lib/constants.js (1)

21-30: Freeze IDs and consider env-backed config

IDs aren’t secrets, but freezing avoids accidental mutation. Longer-term, allow override via env to avoid code changes when IDs change.

Apply this diff:

-export const AIRQO_GROUP_IDS = {
+export const AIRQO_GROUP_IDS = Object.freeze({
   production: '64f54e4621d9b90013925a08',
   staging: '653b00efb657380014328b54',
-};
+});

Optional (future): read from env with sane fallbacks, e.g., process.env.NEXT_PUBLIC_AIRQO_GROUP_ID_PROD/STAGING.

src/platform/src/core/apis/Account.js (1)

252-270: Cohorts API helper — add timeout and normalized error message

Function is solid. Consider a timeout and consistent error surface for better UX.

Apply this diff:

 export const getGroupCohortsApi = (groupId, signal) => {
   if (!groupId) {
     return Promise.reject(new Error('Group ID is required'));
   }
   return secureApiProxy
-    .get(getGroupCohortsUrl(groupId), { authType: AUTH_TYPES.JWT, signal })
-    .then((response) => response.data)
-    .catch((error) => {
-      throw error;
-    });
+    .get(getGroupCohortsUrl(groupId), {
+      authType: AUTH_TYPES.JWT,
+      signal,
+      timeout: 15000,
+    })
+    .then((response) => response.data)
+    .catch((error) => {
+      const msg = error?.response?.data?.message || error.message || 'Failed to fetch cohorts';
+      throw new Error(msg);
+    });
 };
src/platform/src/core/utils/loginSetup.js (1)

261-268: Augment fallback group with grp_slug for broader compatibility

Several places check organization_slug || grp_slug. Adding grp_slug improves resilience.

Apply this diff:

         userGroups = [
           {
             _id: getAirqoGroupId(),
             grp_title: 'airqo',
             grp_name: 'airqo',
             organization_slug: 'airqo',
+            grp_slug: 'airqo',
           },
         ];
src/platform/src/core/hooks/usePaginatedData.js (1)

347-392: Cohort-driven fetcher looks good; consider caching cohort IDs per group

Current flow fetches cohorts on every page/search change. Cache cohort IDs per group (e.g., a separate SWR call keyed by group) and reuse them to cut round-trips and latency while paginating.

Also, until DeviceRegistry forwards search, devices search will be a no-op. See suggested fix in DeviceRegistry.

src/platform/src/common/features/download-insights-locations/data-download/DataDownload.jsx (1)

660-666: Nit: redundant checks in next/prev handlers

The inner if (formData.deviceCategory) branches return the same value.

 const currentNextPage = useMemo(() => {
   if (activeFilterKey === FILTER_TYPES.DEVICES) {
-    if (formData.deviceCategory) {
-      return devicesNextPage;
-    }
-    return devicesNextPage;
+    return devicesNextPage;
   }
   const nextMap = {
     [FILTER_TYPES.COUNTRIES]: countriesNextPage,
     [FILTER_TYPES.CITIES]: citiesNextPage,
     [FILTER_TYPES.SITES]: sitesNextPage,
   };
   return nextMap[activeFilterKey];
 }, [
   activeFilterKey,
-  formData.deviceCategory,
   countriesNextPage,
   citiesNextPage,
   devicesNextPage,
   sitesNextPage,
 ]);
 
 const currentPrevPage = useMemo(() => {
   if (activeFilterKey === FILTER_TYPES.DEVICES) {
-    if (formData.deviceCategory) {
-      return devicesPrevPage;
-    }
-    return devicesPrevPage;
+    return devicesPrevPage;
   }
   const prevMap = {
     [FILTER_TYPES.COUNTRIES]: countriesPrevPage,
     [FILTER_TYPES.CITIES]: citiesPrevPage,
     [FILTER_TYPES.SITES]: sitesPrevPage,
   };
   return prevMap[activeFilterKey];
 }, [
   activeFilterKey,
-  formData.deviceCategory,
   countriesPrevPage,
   citiesPrevPage,
   devicesPrevPage,
   sitesPrevPage,
 ]);

Also applies to: 684-689

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7447ebe and 7765211.

📒 Files selected for processing (12)
  • src/platform/src/common/features/download-insights-locations/components/DataTable.jsx (2 hunks)
  • src/platform/src/common/features/download-insights-locations/data-download/DataDownload.jsx (17 hunks)
  • src/platform/src/common/features/download-insights-locations/data-download/components/DataContent.js (1 hunks)
  • src/platform/src/common/features/download-insights-locations/data-download/hooks/useDataDownloadLogic.js (1 hunks)
  • src/platform/src/core/apis/Account.js (2 hunks)
  • src/platform/src/core/apis/DeviceRegistry.js (2 hunks)
  • src/platform/src/core/hooks/analyticHooks.js (0 hunks)
  • src/platform/src/core/hooks/usePaginatedData.js (1 hunks)
  • src/platform/src/core/urls/authentication.js (1 hunks)
  • src/platform/src/core/urls/deviceRegistry.js (1 hunks)
  • src/platform/src/core/utils/loginSetup.js (2 hunks)
  • src/platform/src/lib/constants.js (1 hunks)
💤 Files with no reviewable changes (1)
  • src/platform/src/core/hooks/analyticHooks.js
🧰 Additional context used
🧬 Code graph analysis (7)
src/platform/src/core/utils/loginSetup.js (3)
src/platform/src/app/providers/UnifiedGroupProvider.jsx (1)
  • userGroups (153-171)
src/platform/src/core/utils/domainUpdateVerification.js (1)
  • userGroups (14-15)
src/platform/src/lib/constants.js (2)
  • getAirqoGroupId (26-30)
  • getAirqoGroupId (26-30)
src/platform/src/common/features/download-insights-locations/components/DataTable.jsx (2)
src/platform/src/common/features/download-insights-locations/data-download/DataDownload.jsx (1)
  • filters (554-569)
src/platform/src/common/features/download-insights-locations/data-download/hooks/useDataDownloadLogic.js (1)
  • activeFilterKey (109-109)
src/platform/src/core/apis/Account.js (2)
src/platform/src/core/utils/secureApiProxyClient.js (4)
  • secureApiProxy (344-344)
  • secureApiProxy (344-344)
  • AUTH_TYPES (93-98)
  • AUTH_TYPES (93-98)
src/platform/src/core/urls/authentication.js (2)
  • getGroupCohortsUrl (28-28)
  • getGroupCohortsUrl (28-28)
src/platform/src/core/apis/DeviceRegistry.js (3)
src/platform/src/core/hooks/usePaginatedData.js (2)
  • skip (43-43)
  • limit (35-35)
src/platform/src/core/utils/secureApiProxyClient.js (4)
  • AUTH_TYPES (93-98)
  • AUTH_TYPES (93-98)
  • secureApiProxy (344-344)
  • secureApiProxy (344-344)
src/platform/src/core/urls/deviceRegistry.js (2)
  • DEVICES_FOR_COHORTS_URL (5-5)
  • DEVICES_FOR_COHORTS_URL (5-5)
src/platform/src/core/hooks/usePaginatedData.js (2)
src/platform/src/core/apis/Account.js (3)
  • params (325-325)
  • getGroupCohortsApi (259-269)
  • getGroupCohortsApi (259-269)
src/platform/src/core/apis/DeviceRegistry.js (7)
  • params (47-47)
  • params (97-102)
  • params (190-190)
  • params (215-215)
  • params (239-239)
  • getDevicesForCohortsApi (85-130)
  • getDevicesForCohortsApi (85-130)
src/platform/src/common/features/download-insights-locations/data-download/components/DataContent.js (1)
src/platform/src/common/features/download-insights-locations/data-download/hooks/useDataDownloadLogic.js (1)
  • activeFilterKey (109-109)
src/platform/src/common/features/download-insights-locations/data-download/DataDownload.jsx (2)
src/platform/src/app/providers/UnifiedGroupProvider.jsx (2)
  • useGetActiveGroup (850-879)
  • useGetActiveGroup (850-879)
src/platform/src/core/hooks/usePaginatedData.js (2)
  • usePaginatedDevicesSummary (347-393)
  • usePaginatedDevicesSummary (347-393)
⏰ 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-push-deploy-analytics-platform-preview
🔇 Additional comments (5)
src/platform/src/core/urls/deviceRegistry.js (1)

5-5: Cohort devices URL added — looks good

Constant name and path are clear and consistent with the file’s style.

src/platform/src/core/urls/authentication.js (1)

27-29: New group cohorts URL helper — LGTM

Straightforward helper aligned with existing patterns.

src/platform/src/core/utils/loginSetup.js (1)

28-28: Import of getAirqoGroupId — good centralization

Nice consolidation of environment-specific group IDs.

src/platform/src/common/features/download-insights-locations/data-download/DataDownload.jsx (1)

272-290: LGTM: unified devices pagination via groupId + category

Good consolidation to usePaginatedDevicesSummary with groupId and category. Aligns with the cohorts-first flow and simplifies pagination state.

src/platform/src/core/apis/DeviceRegistry.js (1)

85-131: Forward search and surface axios cancellations in getDevicesForCohortsApi

  • In src/platform/src/core/apis/DeviceRegistry.js, extend getDevicesForCohortsApi to forward a non-empty search param into params.
  • Remove the .catch so that axios’s CanceledError/ERR_CANCELED propagate to SWR’s abort logic.
  • Confirm the backend supports search on DEVICES_FOR_COHORTS_URL.


// Ensure deviceCategoryValue is valid
const validCategories = ['lowcost', 'bam', 'mobile'];
const validCategories = ['lowcost', 'bam', 'mobile', 'gas'];
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 | 🟠 Major

🧩 Analysis chain

LGTM! Verify backend API support for 'gas' category.

The addition of 'gas' to the valid device categories is correct and aligns with the PR's goal of supporting cohorts APIs. This prevents 'gas' devices from being incorrectly coerced to 'lowcost'.

Please verify that the backend API accepts 'gas' as a valid device_category parameter. Run the following script to check for other locations in the codebase that might also need to support the 'gas' category:


🏁 Script executed:

#!/bin/bash
# Description: Find other references to device categories that may need updating

# Search for device category validation or constants
rg -nP --type=js --type=jsx --type=ts --type=tsx -C3 '\b(lowcost|bam|mobile)\b.*\b(lowcost|bam|mobile)\b' \
  -g '!node_modules' -g '!dist' -g '!build'

# Search for device_category or deviceCategory field usage
rg -nP --type=js --type=jsx --type=ts --type=tsx -C3 'device[_-]?category' \
  -g '!node_modules' -g '!dist' -g '!build'

Length of output: 55


🏁 Script executed:

#!/bin/bash
# Description: Find other references to device categories that may need updating

# Search for device category validation or constants
rg -nP --type js --type ts -C3 '\b(lowcost|bam|mobile)\b.*\b(lowcost|bam|mobile)\b' \
  -g '!node_modules' -g '!dist' -g '!build'

# Search for device_category or deviceCategory field usage
rg -nP --type js --type ts -C3 'device[_-]?category' \
  -g '!node_modules' -g '!dist' -g '!build'

Length of output: 10619


Extend 'gas' support across device-category logic

  • DataDownload.jsx: update raw/calibrated and frequency filters to include ‘gas’
  • useAnalyticsDownload.js & useDownload.js: replace hardcoded 'lowcost' with dynamic deviceCategoryValue
  • DataContent.js: add messaging for ‘gas’ devices
  • useDataPreview.js: ensure preview payload handles ‘gas’
  • Verify backend API accepts ‘gas’ as a valid device_category
🤖 Prompt for AI Agents
In
src/platform/src/common/features/download-insights-locations/data-download/hooks/useDataDownloadLogic.js
around line 494, the device-category logic must fully support the 'gas' device
type: ensure the validCategories array explicitly includes 'gas' (if not
already), replace any hardcoded 'lowcost' references in this file with the
dynamic deviceCategoryValue variable, and extend the raw/calibrated and
frequency filter checks to include 'gas' alongside other categories so filters
and payloads are generated correctly for gas devices.

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

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7765211 and a8b3cd0.

📒 Files selected for processing (4)
  • src/platform/src/common/features/download-insights-locations/components/DataTable.jsx (3 hunks)
  • src/platform/src/common/features/download-insights-locations/data-download/DataDownload.jsx (17 hunks)
  • src/platform/src/common/features/download-insights-locations/data-download/constants.js (1 hunks)
  • src/platform/src/core/hooks/usePaginatedData.js (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
src/platform/src/core/hooks/usePaginatedData.js (2)
src/platform/src/core/apis/Account.js (3)
  • params (325-325)
  • getGroupCohortsApi (259-269)
  • getGroupCohortsApi (259-269)
src/platform/src/core/apis/DeviceRegistry.js (7)
  • params (47-47)
  • params (97-102)
  • params (190-190)
  • params (215-215)
  • params (239-239)
  • getDevicesForCohortsApi (85-130)
  • getDevicesForCohortsApi (85-130)
src/platform/src/common/features/download-insights-locations/components/DataTable.jsx (3)
src/platform/src/common/features/download-insights-locations/data-download/DataDownload.jsx (1)
  • filters (553-568)
src/platform/src/common/features/download-insights-locations/add-favorites/components/MainContent.jsx (1)
  • filters (42-45)
src/platform/src/common/features/download-insights-locations/data-download/hooks/useDataDownloadLogic.js (1)
  • activeFilterKey (109-109)
src/platform/src/common/features/download-insights-locations/data-download/DataDownload.jsx (2)
src/platform/src/app/providers/UnifiedGroupProvider.jsx (2)
  • useGetActiveGroup (850-879)
  • useGetActiveGroup (850-879)
src/platform/src/core/hooks/usePaginatedData.js (2)
  • usePaginatedDevicesSummary (347-392)
  • usePaginatedDevicesSummary (347-392)
⏰ 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-push-deploy-analytics-platform-preview
🔇 Additional comments (3)
src/platform/src/common/features/download-insights-locations/components/DataTable.jsx (1)

313-313: Previous feedback successfully addressed.

The activeFilterKey prop and synchronization logic have been implemented as suggested in the previous review. The state now properly updates when the prop or filters change, and stale column filters and pagination are reset.

Also applies to: 326-330, 354-362

src/platform/src/common/features/download-insights-locations/data-download/constants.js (1)

32-32: LGTM.

The new gas device category option is consistent with the existing entries and properly integrated.

src/platform/src/common/features/download-insights-locations/data-download/DataDownload.jsx (1)

54-54: Improved error handling with centralized formatting.

The introduction of getApiErrorMessage provides consistent, user-friendly error messages across all data loading scenarios. This is a solid improvement over the previous raw error handling.

Also applies to: 306-326, 1293-1309

Comment on lines 272 to 289
const {
data: mobileDevicesData,
isLoading: mobileDevicesLoading,
isError: mobileDevicesError,
error: mobileDevicesErrorMsg,
refresh: refreshMobileDevices,
meta: mobileDevicesMeta,
loadMore: loadMoreMobileDevices,
canLoadMore: canLoadMoreMobileDevices,
hasNextPage: mobileDevicesHasNextPage,
nextPage: mobileDevicesNextPage,
prevPage: mobileDevicesPrevPage,
} = usePaginatedMobileDevices({
enableInfiniteScroll: false,
initialLimit: 6,
search: searchQuery,
group: groupTitle,
});

const {
data: bamDevicesData,
isLoading: bamDevicesLoading,
isError: bamDevicesError,
error: bamDevicesErrorMsg,
refresh: refreshBAMDevices,
meta: bamDevicesMeta,
loadMore: loadMoreBAMDevices,
canLoadMore: canLoadMoreBAMDevices,
hasNextPage: bamDevicesHasNextPage,
nextPage: bamDevicesNextPage,
prevPage: bamDevicesPrevPage,
} = usePaginatedBAMDevices({
enableInfiniteScroll: false,
initialLimit: 6,
search: searchQuery,
group: groupTitle,
});

const {
data: lowCostDevicesData,
isLoading: lowCostDevicesLoading,
isError: lowCostDevicesError,
error: lowCostDevicesErrorMsg,
refresh: refreshLowCostDevices,
meta: lowCostDevicesMeta,
loadMore: loadMoreLowCostDevices,
canLoadMore: canLoadMoreLowCostDevices,
hasNextPage: lowCostDevicesHasNextPage,
nextPage: lowCostDevicesNextPage,
prevPage: lowCostDevicesPrevPage,
} = usePaginatedLowCostDevices({
data: devicesData,
isLoading: devicesLoading,
isError: devicesError,
error: devicesErrorMsg,
refresh: refreshDevices,
meta: devicesMeta,
loadMore: loadMoreDevices,
canLoadMore: canLoadMoreDevices,
hasNextPage: devicesHasNextPage,
nextPage: devicesNextPage,
prevPage: devicesPrevPage,
} = usePaginatedDevicesSummary(groupId, {
enableInfiniteScroll: false,
initialLimit: 6,
search: searchQuery,
group: groupTitle,
category: formData.deviceCategory?.name?.toLowerCase() || 'lowcost',
});
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 | 🔴 Critical

Critical: Function signature mismatch.

The call to usePaginatedDevicesSummary on line 284 passes groupId as the first argument, but the hook's new signature expects a single options object. This mismatch will cause groupId to be destructured as options, leading to incorrect behavior.

Apply this fix:

   const {
     data: devicesData,
     isLoading: devicesLoading,
     isError: devicesError,
     error: devicesErrorMsg,
     refresh: refreshDevices,
     meta: devicesMeta,
     loadMore: loadMoreDevices,
     canLoadMore: canLoadMoreDevices,
     hasNextPage: devicesHasNextPage,
     nextPage: devicesNextPage,
     prevPage: devicesPrevPage,
-  } = usePaginatedDevicesSummary(groupId, {
+  } = usePaginatedDevicesSummary({
+    group: groupId,
     enableInfiniteScroll: false,
     initialLimit: 6,
     search: searchQuery,
     category: formData.deviceCategory?.name?.toLowerCase() || 'lowcost',
   });
📝 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
const {
data: mobileDevicesData,
isLoading: mobileDevicesLoading,
isError: mobileDevicesError,
error: mobileDevicesErrorMsg,
refresh: refreshMobileDevices,
meta: mobileDevicesMeta,
loadMore: loadMoreMobileDevices,
canLoadMore: canLoadMoreMobileDevices,
hasNextPage: mobileDevicesHasNextPage,
nextPage: mobileDevicesNextPage,
prevPage: mobileDevicesPrevPage,
} = usePaginatedMobileDevices({
enableInfiniteScroll: false,
initialLimit: 6,
search: searchQuery,
group: groupTitle,
});
const {
data: bamDevicesData,
isLoading: bamDevicesLoading,
isError: bamDevicesError,
error: bamDevicesErrorMsg,
refresh: refreshBAMDevices,
meta: bamDevicesMeta,
loadMore: loadMoreBAMDevices,
canLoadMore: canLoadMoreBAMDevices,
hasNextPage: bamDevicesHasNextPage,
nextPage: bamDevicesNextPage,
prevPage: bamDevicesPrevPage,
} = usePaginatedBAMDevices({
enableInfiniteScroll: false,
initialLimit: 6,
search: searchQuery,
group: groupTitle,
});
const {
data: lowCostDevicesData,
isLoading: lowCostDevicesLoading,
isError: lowCostDevicesError,
error: lowCostDevicesErrorMsg,
refresh: refreshLowCostDevices,
meta: lowCostDevicesMeta,
loadMore: loadMoreLowCostDevices,
canLoadMore: canLoadMoreLowCostDevices,
hasNextPage: lowCostDevicesHasNextPage,
nextPage: lowCostDevicesNextPage,
prevPage: lowCostDevicesPrevPage,
} = usePaginatedLowCostDevices({
data: devicesData,
isLoading: devicesLoading,
isError: devicesError,
error: devicesErrorMsg,
refresh: refreshDevices,
meta: devicesMeta,
loadMore: loadMoreDevices,
canLoadMore: canLoadMoreDevices,
hasNextPage: devicesHasNextPage,
nextPage: devicesNextPage,
prevPage: devicesPrevPage,
} = usePaginatedDevicesSummary(groupId, {
enableInfiniteScroll: false,
initialLimit: 6,
search: searchQuery,
group: groupTitle,
category: formData.deviceCategory?.name?.toLowerCase() || 'lowcost',
});
const {
data: devicesData,
isLoading: devicesLoading,
isError: devicesError,
error: devicesErrorMsg,
refresh: refreshDevices,
meta: devicesMeta,
loadMore: loadMoreDevices,
canLoadMore: canLoadMoreDevices,
hasNextPage: devicesHasNextPage,
nextPage: devicesNextPage,
prevPage: devicesPrevPage,
} = usePaginatedDevicesSummary({
group: groupId,
enableInfiniteScroll: false,
initialLimit: 6,
search: searchQuery,
category: formData.deviceCategory?.name?.toLowerCase() || 'lowcost',
});
🤖 Prompt for AI Agents
In
src/platform/src/common/features/download-insights-locations/data-download/DataDownload.jsx
around lines 272 to 289, the hook is being called with groupId as a positional
first argument but the new usePaginatedDevicesSummary signature expects a single
options object; fix this by replacing the positional call with a single object
containing groupId and the existing options (e.g. { groupId,
enableInfiniteScroll: false, initialLimit: 6, search: searchQuery, category:
formData.deviceCategory?.name?.toLowerCase() || 'lowcost' }) so the hook
receives the correct properties.

Comment on lines 347 to 391
export const usePaginatedDevicesSummary = (options = {}) => {
const { search = '', group = '', category = 'lowcost', ...rest } = options;
const normalizedGroup =
typeof group === 'string' && group.trim().length > 0 ? group.trim() : '';

const fetcher = useCallback(
async (params, signal) => {
const { getDeviceSummaryApi } = await import('../apis/Analytics');
const groupParam = normalizedGroup || undefined;
return getDeviceSummaryApi({
group: groupParam,
status: 'deployed',
skip: params.skip,
limit: params.limit,
search: params.search,
if (!normalizedGroup) return { devices: [], meta: {} };

const { getGroupCohortsApi } = await import('../apis/Account');
const { getDevicesForCohortsApi } = await import('../apis/DeviceRegistry');

// Step 1: Fetch cohorts for the group
const cohortsResponse = await getGroupCohortsApi(normalizedGroup, signal);
const cohortIds = cohortsResponse?.data || [];

if (cohortIds.length === 0) {
return { devices: [], meta: {} };
}

// Step 2: Fetch devices for the retrieved cohorts
return getDevicesForCohortsApi(
{
cohort_ids: cohortIds,
skip: params.skip,
limit: params.limit,
category: category,
...(params.search && { search: params.search }),
},
signal,
});
);
},
[normalizedGroup],
[normalizedGroup, category],
);

return usePaginatedData(
['devices-summary-paginated', normalizedGroup || 'all', search || ''],
['devices-summary-paginated', normalizedGroup || 'all', category, search || ''],
fetcher,
{
initialLimit: 20,
maxLimit: 80,
...restOptions,
...rest,
search,
},
);
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 | 🔴 Critical

🧩 Analysis chain

Verify cohort API error handling.

The refactored hook now uses a two-step flow (fetch cohorts, then devices), which is a significant architectural change. However, if the cohort API fails, the error will propagate through SWR without explicit handling. Consider whether partial failures need special treatment.

Also, when normalizedGroup is empty, the hook returns early with empty data. Ensure this behavior aligns with expectations for unauthenticated or group-less scenarios.

Verify that the cohort API failure scenarios are properly handled by SWR's error handling mechanism:


🏁 Script executed:

#!/bin/bash
# Check how cohort API errors are handled in consuming components
rg -n -C3 'usePaginatedDevicesSummary' src/platform/src/common/features/download-insights-locations/

Length of output: 1717


Update hook usage signature and verify error handling

  • DataDownload.jsx (line 284) and any other consumers must call
    usePaginatedDevicesSummary({ group: groupId, enableInfiniteScroll: false, initialLimit: 6, search: searchQuery }) instead of positional args.
  • Ensure the fetcher’s two-step flow handles cohort API failures appropriately (errors currently bubble through SWR).
  • Confirm that early return with empty group aligns with expected behavior for group-less scenarios.

Copy link
Contributor

New next-platform changes available for preview here

Copy link
Contributor

New next-platform changes available for preview here

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

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a8b3cd0 and ba767e7.

📒 Files selected for processing (4)
  • src/platform/src/common/features/download-insights-locations/data-download/DataDownload.jsx (18 hunks)
  • src/platform/src/core/apis/DeviceRegistry.js (2 hunks)
  • src/platform/src/core/hooks/usePaginatedData.js (2 hunks)
  • src/platform/src/core/urls/deviceRegistry.js (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/platform/src/core/apis/DeviceRegistry.js
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-09-22T07:30:46.957Z
Learnt from: Codebmk
PR: airqo-platform/AirQo-frontend#2944
File: src/vertex/components/features/dashboard/stats-cards.tsx:108-114
Timestamp: 2025-09-22T07:30:46.957Z
Learning: In the useDeviceCount hook and getDeviceCountApi, the groupId parameter expects the group title (grp_title) not the group's _id field. The API maps groupId to the group_id query parameter which uses the group name/title string value.

Applied to files:

  • src/platform/src/common/features/download-insights-locations/data-download/DataDownload.jsx
📚 Learning: 2025-09-22T07:30:46.957Z
Learnt from: Codebmk
PR: airqo-platform/AirQo-frontend#2944
File: src/vertex/components/features/dashboard/stats-cards.tsx:108-114
Timestamp: 2025-09-22T07:30:46.957Z
Learning: In the useDeviceCount hook and related device APIs, the groupId parameter expects the group title (grp_title) not the group's _id field. The API maps groupId to the group_id query parameter which uses the group name/title.

Applied to files:

  • src/platform/src/common/features/download-insights-locations/data-download/DataDownload.jsx
🧬 Code graph analysis (2)
src/platform/src/core/hooks/usePaginatedData.js (3)
src/platform/src/core/apis/DeviceRegistry.js (10)
  • params (48-48)
  • params (99-104)
  • params (147-152)
  • params (232-232)
  • params (257-257)
  • params (281-281)
  • getSitesForCohortsApi (134-172)
  • getSitesForCohortsApi (134-172)
  • getDevicesForCohortsApi (86-132)
  • getDevicesForCohortsApi (86-132)
src/platform/src/core/apis/Account.js (3)
  • params (325-325)
  • getGroupCohortsApi (259-269)
  • getGroupCohortsApi (259-269)
src/platform/src/core/apis/Analytics.js (5)
  • params (38-45)
  • params (69-77)
  • params (100-107)
  • getSitesSummaryApi (31-59)
  • getSitesSummaryApi (31-59)
src/platform/src/common/features/download-insights-locations/data-download/DataDownload.jsx (2)
src/platform/src/app/providers/UnifiedGroupProvider.jsx (2)
  • useGetActiveGroup (850-879)
  • useGetActiveGroup (850-879)
src/platform/src/core/hooks/usePaginatedData.js (4)
  • usePaginatedSitesSummary (312-367)
  • usePaginatedSitesSummary (312-367)
  • usePaginatedDevicesSummary (372-417)
  • usePaginatedDevicesSummary (372-417)
⏰ 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-push-deploy-analytics-platform-preview
🔇 Additional comments (6)
src/platform/src/core/urls/deviceRegistry.js (1)

5-7: LGTM!

The new URL constants follow the established naming conventions and fit naturally into the existing URL structure.

src/platform/src/core/hooks/usePaginatedData.js (3)

317-355: Verify UX for empty cohorts.

When a group has no cohorts (line 333-335), the hook returns empty data without an error. This could confuse users who expect to see sites for their group but get nothing. Consider whether this scenario warrants a specific error message or warning.

Additionally, if getGroupCohortsApi fails, the error propagates through SWR without explicit handling. Confirm this aligns with your error-handling strategy.


407-416: Good: SWR key includes category.

The SWR key (line 408) correctly includes the category parameter, ensuring data is refetched when the device category changes. This prevents serving stale data across different device types.


377-405: Breaking change: devices hook lacks fallback when group is empty.
Unlike usePaginatedSitesSummary, usePaginatedDevicesSummary immediately returns { devices: [], meta: {} } when group is empty. Confirm this is intentional and ensure consumers handle the empty state; if devices should be available without a group, add a fallback fetch path similar to the sites implementation.

src/platform/src/common/features/download-insights-locations/data-download/DataDownload.jsx (2)

80-80: Good: using group _id instead of title.

Extracting id as groupId aligns with the learnings that state API parameters expect the group's _id field, not the title. This corrects potential issues from using the group title as an identifier.

Based on learnings.


272-289: Hook call signature corrected.

The call to usePaginatedDevicesSummary now correctly passes groupId as the first positional argument with options as the second parameter, matching the hook's signature (group, options = {}). This addresses the critical issue flagged in previous reviews.

Copy link
Contributor

New next-platform changes available for preview here

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: 0

♻️ Duplicate comments (2)
src/platform/src/common/features/download-insights-locations/data-download/DataDownload.jsx (2)

208-209: Good fix: pass the hook error to getApiErrorMessage.

Destructuring error as siteAndDeviceIdsError and using it in the status message resolves the earlier issue of passing data instead of the error.

Also applies to: 340-341, 349-357


274-290: Hook call signature is correct; prior bot warning is outdated.

usePaginatedDevicesSummary currently takes (group, options). Your call with groupId as first arg and options object is correct per core/hooks/usePaginatedData.

🧹 Nitpick comments (4)
src/platform/src/core/hooks/usePaginatedData.js (1)

373-373: Consider documenting expected category values.

The category parameter defaults to 'lowcost' but lacks validation or documentation. With the addition of new categories like 'gas' (per the AI summary), consider documenting the expected values or adding runtime validation to catch invalid categories early.

src/platform/src/common/features/download-insights-locations/data-download/DataDownload.jsx (3)

305-327: Optional: prune unused dependency to avoid extra effect runs.

backToDownload isn’t used inside this effect. Remove it from the deps array.

   useEffect(() => {
     setFilterErrors({
       sites: sitesError ? getApiErrorMessage(sitesErrorMsg) || 'Error loading sites' : '',
       devices: devicesError ? getApiErrorMessage(devicesErrorMsg) || 'Error loading devices' : '',
       countries: countriesError
         ? getApiErrorMessage(countriesErrorMsg) || 'Error loading countries'
         : '',
       cities: citiesError
         ? getApiErrorMessage(citiesErrorMsg) || 'Error loading cities'
         : '',
     });
   }, [
     sitesError,
     sitesErrorMsg,
     devicesError,
     devicesErrorMsg,
     countriesError,
     countriesErrorMsg,
     citiesError,
     citiesErrorMsg,
     setFilterErrors,
-    backToDownload,
   ]);

662-667: Simplify redundant conditionals for devices next/prev.

The inner if/else returns the same value; remove the dead branch.

  const currentNextPage = useMemo(() => {
    if (activeFilterKey === FILTER_TYPES.DEVICES) {
-     if (formData.deviceCategory) {
-       return devicesNextPage;
-     }
-     return devicesNextPage;
+     return devicesNextPage;
    }
    ...
  }, [...]);

  const currentPrevPage = useMemo(() => {
    if (activeFilterKey === FILTER_TYPES.DEVICES) {
-     if (formData.deviceCategory) {
-       return devicesPrevPage;
-     }
-     return devicesPrevPage;
+     return devicesPrevPage;
    }
    ...
  }, [...]);

Also applies to: 684-690


144-162: Handle the new ‘gas’ category in data type/frequency filters.

If ‘gas’ devices are supported, reflect their constraints here (raw-only, etc.). Right now gas will fall through to defaults.

  const filteredDataTypeOptions = useMemo(() => {
    const deviceCategory =
      formData.deviceCategory?.name?.toLowerCase() || 'lowcost';

    if (deviceCategory === 'lowcost') {
      return DATA_TYPE_OPTIONS;
    }

-   if (deviceCategory === 'bam' || deviceCategory === 'mobile') {
+   if (deviceCategory === 'bam' || deviceCategory === 'mobile' || deviceCategory === 'gas') {
      return DATA_TYPE_OPTIONS.filter(
        (option) => option.name.toLowerCase() === 'raw data',
      );
    }

    return DATA_TYPE_OPTIONS;
  }, [formData.deviceCategory]);

  const filteredFrequencyOptions = useMemo(() => {
    const deviceCategory =
      formData.deviceCategory?.name?.toLowerCase() || 'lowcost';
    if (
      activeFilterKey === FILTER_TYPES.DEVICES &&
-     deviceCategory === 'mobile'
+     (deviceCategory === 'mobile' || deviceCategory === 'gas')
    ) {
      return FREQUENCY_OPTIONS.filter(
        (option) => option.name.toLowerCase() === 'raw',
      );
    }

    if (activeFilterKey === FILTER_TYPES.DEVICES && deviceCategory === 'bam') {
      return FREQUENCY_OPTIONS;
    }

    return FREQUENCY_OPTIONS.filter(
      (option) => option.name.toLowerCase() !== 'raw',
    );
  }, [formData.deviceCategory, activeFilterKey]);

Also applies to: 165-187

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ba767e7 and 8bca60d.

📒 Files selected for processing (2)
  • src/platform/src/common/features/download-insights-locations/data-download/DataDownload.jsx (20 hunks)
  • src/platform/src/core/hooks/usePaginatedData.js (2 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-09-22T07:30:46.957Z
Learnt from: Codebmk
PR: airqo-platform/AirQo-frontend#2944
File: src/vertex/components/features/dashboard/stats-cards.tsx:108-114
Timestamp: 2025-09-22T07:30:46.957Z
Learning: In the useDeviceCount hook and getDeviceCountApi, the groupId parameter expects the group title (grp_title) not the group's _id field. The API maps groupId to the group_id query parameter which uses the group name/title string value.

Applied to files:

  • src/platform/src/common/features/download-insights-locations/data-download/DataDownload.jsx
📚 Learning: 2025-09-22T07:30:46.957Z
Learnt from: Codebmk
PR: airqo-platform/AirQo-frontend#2944
File: src/vertex/components/features/dashboard/stats-cards.tsx:108-114
Timestamp: 2025-09-22T07:30:46.957Z
Learning: In the useDeviceCount hook and related device APIs, the groupId parameter expects the group title (grp_title) not the group's _id field. The API maps groupId to the group_id query parameter which uses the group name/title.

Applied to files:

  • src/platform/src/common/features/download-insights-locations/data-download/DataDownload.jsx
🧬 Code graph analysis (2)
src/platform/src/common/features/download-insights-locations/data-download/DataDownload.jsx (2)
src/platform/src/app/providers/UnifiedGroupProvider.jsx (2)
  • useGetActiveGroup (850-879)
  • useGetActiveGroup (850-879)
src/platform/src/core/hooks/usePaginatedData.js (4)
  • usePaginatedSitesSummary (312-367)
  • usePaginatedSitesSummary (312-367)
  • usePaginatedDevicesSummary (372-417)
  • usePaginatedDevicesSummary (372-417)
src/platform/src/core/hooks/usePaginatedData.js (3)
src/platform/src/core/apis/DeviceRegistry.js (10)
  • params (48-48)
  • params (99-104)
  • params (147-152)
  • params (232-232)
  • params (257-257)
  • params (281-281)
  • getSitesForCohortsApi (134-172)
  • getSitesForCohortsApi (134-172)
  • getDevicesForCohortsApi (86-132)
  • getDevicesForCohortsApi (86-132)
src/platform/src/core/apis/Account.js (3)
  • params (325-325)
  • getGroupCohortsApi (259-269)
  • getGroupCohortsApi (259-269)
src/platform/src/core/apis/Analytics.js (5)
  • params (38-45)
  • params (69-77)
  • params (100-107)
  • getSitesSummaryApi (31-59)
  • getSitesSummaryApi (31-59)
⏰ 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-push-deploy-analytics-platform-preview
🔇 Additional comments (7)
src/platform/src/core/hooks/usePaginatedData.js (4)

317-355: Cohort-based sites fetching implemented correctly.

The two-step flow (fetch cohorts → fetch sites) is well-structured with proper signal handling in both API calls. The fallback to Analytics API when no group is provided maintains backward compatibility.


377-405: Critical signal placement issue resolved.

The signal parameter is now correctly placed inside the options object (line 400), resolving the critical issue flagged in the previous review. The two-step cohort flow is well-implemented with proper signal handling and parameter passing.


408-408: SWR cache key correctly updated for category.

Adding category to the cache key ensures proper cache segregation, preventing data from different categories from being mixed. This is essential for the cohort-based fetching approach.


379-379: Confirm early-return behavior in hook consumers. The new if (!normalizedGroup) return (empty devices/meta) is a breaking change—ensure DataDownload.jsx (line 285) and any other usePaginatedDevicesSummary consumers handle the no-group scenario correctly.

src/platform/src/common/features/download-insights-locations/data-download/DataDownload.jsx (3)

230-235: Sites hook call LGTM.

Passing '' when grid-selection is active cleanly triggers the “all sites” fallback; revalidateOnMount is appropriate.


1295-1311: Error messaging path looks solid.

Mapping API errors through getApiErrorMessage for the active filter with a sane fallback reads well.


80-81: Remove group identifier warning—passing id is correct for cohorts APIs. getGroupCohortsApi/getGroupCohortsUrl expect the group’s _id, so no change needed.

Likely an incorrect or invalid review comment.

Copy link
Contributor

New next-platform changes available for preview here

1 similar comment
Copy link
Contributor

New next-platform changes available for preview here

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

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8bca60d and 31865c8.

📒 Files selected for processing (1)
  • src/platform/src/core/hooks/usePaginatedData.js (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/platform/src/core/hooks/usePaginatedData.js (3)
src/platform/src/core/apis/Account.js (3)
  • getGroupCohortsApi (259-269)
  • getGroupCohortsApi (259-269)
  • params (325-325)
src/platform/src/core/apis/DeviceRegistry.js (10)
  • getSitesForCohortsApi (134-172)
  • getSitesForCohortsApi (134-172)
  • params (48-48)
  • params (99-104)
  • params (147-152)
  • params (232-232)
  • params (257-257)
  • params (281-281)
  • getDevicesForCohortsApi (86-132)
  • getDevicesForCohortsApi (86-132)
src/platform/src/core/apis/Analytics.js (5)
  • params (38-45)
  • params (69-77)
  • params (100-107)
  • getSitesSummaryApi (31-59)
  • getSitesSummaryApi (31-59)
⏰ 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-push-deploy-analytics-platform-preview
🔇 Additional comments (1)
src/platform/src/core/hooks/usePaginatedData.js (1)

5-5: Consistent error surfacing via getApiErrorMessage.

Good addition; keeps API failure messages uniform across hooks.

Comment on lines 385 to 436
const { search = '', category = 'lowcost', ...rest } = options;
const normalizedGroup =
typeof group === 'string' && group.trim().length > 0 ? group.trim() : '';

const fetcher = useCallback(
async (params, signal) => {
const { getDeviceSummaryApi } = await import('../apis/Analytics');
const groupParam = normalizedGroup || undefined;
return getDeviceSummaryApi({
group: groupParam,
status: 'deployed',
skip: params.skip,
limit: params.limit,
search: params.search,
signal,
if (normalizedGroup) {

const { getGroupCohortsApi } = await import('../apis/Account');
const { getDevicesForCohortsApi } = await import('../apis/DeviceRegistry');

// Step 1: Fetch cohorts for the group
try {
const cohortsResponse = await getGroupCohortsApi(normalizedGroup, signal);
const cohortIds = cohortsResponse?.data || [];

if (cohortIds.length === 0) {
return { devices: [], meta: {} };
}

// Step 2: Fetch devices for the retrieved cohorts
return getDevicesForCohortsApi(
{
cohort_ids: cohortIds,
skip: params.skip,
limit: params.limit,
category: category,
...(search && { search }),
signal
},
);
} catch (error) {
if (error.name === 'AbortError' || error.code === 'ECONNABORTED') {
throw new Error('Request cancelled');
}
// Handle cohort fetch errors
throw new Error(
`Unable to fetch devices: ${getApiErrorMessage(error)}`
);
}
}

const { getDevicesSummaryApi } = await import('../apis/Analytics'); // Or wherever your all-devices API is
return getDevicesSummaryApi({
...params,
category,
...(search && { search }),
signal
});

},
[normalizedGroup],
[normalizedGroup, category, search],
Copy link
Contributor

@coderabbitai coderabbitai bot Oct 15, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Same fixes for devices: preserve aborts, robust cohort IDs, zeroed meta; plus verify Analytics API supports category.

  • Don’t wrap aborts; rethrow so the outer fetcher swallows them.
  • Extract cohort IDs defensively.
  • Return explicit zero meta when empty.
  • Signal is correctly passed in options to getDevicesForCohortsApi now. Good.

Apply this diff:

 export const usePaginatedDevicesSummary = (group, options = {}) => {
   const { search = '', category = 'lowcost', ...rest } = options;
   const normalizedGroup =
     typeof group === 'string' && group.trim().length > 0 ? group.trim() : '';

   const fetcher = useCallback(
     async (params, signal) => {
       if (normalizedGroup) {

         const { getGroupCohortsApi } = await import('../apis/Account');
         const { getDevicesForCohortsApi } = await import('../apis/DeviceRegistry');

         // Step 1: Fetch cohorts for the group
         try {
-          const cohortsResponse = await getGroupCohortsApi(normalizedGroup, signal);
-          const cohortIds = cohortsResponse?.data || [];
+          const cohortsResponse = await getGroupCohortsApi(normalizedGroup, signal);
+          const raw = cohortsResponse;
+          const source =
+            Array.isArray(raw)
+              ? raw
+              : Array.isArray(raw?.data)
+              ? raw.data
+              : raw?.cohorts ?? raw?.results ?? [];
+          const cohortIds = (Array.isArray(source) ? source : [])
+            .map((c) => (typeof c === 'string' ? c : c?._id ?? c?.id))
+            .filter(Boolean);

           if (cohortIds.length === 0) {
-            return { devices: [], meta: {} };
+            return {
+              devices: [],
+              meta: { total: 0, totalPages: 0, nextPage: null, previousPage: null },
+            };
           }

           // Step 2: Fetch devices for the retrieved cohorts
           return getDevicesForCohortsApi(
             {
               cohort_ids: cohortIds,
               skip: params.skip,
               limit: params.limit,
-              category: category,
+              category,
               ...(search && { search }),
-              signal
+              signal
             },
           );
         } catch (error) {
-          if (error.name === 'AbortError' || error.code === 'ECONNABORTED') {
-            throw new Error('Request cancelled');
-          }
+          if (
+            error?.name === 'AbortError' ||
+            error?.name === 'CanceledError' ||
+            error?.code === 'ERR_CANCELED' ||
+            error?.code === 'ECONNABORTED'
+          ) {
+            throw error;
+          }
           // Handle cohort fetch errors
           throw new Error(
             `Unable to fetch devices: ${getApiErrorMessage(error)}`
           );
         }
       }

-      const { getDevicesSummaryApi } = await import('../apis/Analytics'); // Or wherever your all-devices API is
+      const { getDevicesSummaryApi } = await import('../apis/Analytics');
       return getDevicesSummaryApi({
         ...params,
         category,
         ...(search && { search }),
         signal
       });

     },
     [normalizedGroup, category, search],
   );

Additionally, please verify that getDevicesSummaryApi exists and accepts a category param in the Analytics API.

Also applies to: 440-446


🏁 Script executed:

#!/bin/bash
# Verify the Analytics devices summary API exists and supports `category`

echo "Locate getDevicesSummaryApi definition:"
rg -n -C3 --type=js --type=ts 'export\s+const\s+getDevicesSummaryApi' src

echo
echo "Inspect parameter handling (look for category in params):"
rg -n -C3 --type=js --type=ts 'getDevicesSummaryApi\s*=\s*async|\{.*category.*\}|DEVICES_SUMMARY_URL' src/platform/src/core/apis

echo
echo "Search for other references to confirm expected shape:"
rg -n -C3 --type=js --type=ts '\bgetDevicesSummaryApi\s*\(' src

Length of output: 3521


🏁 Script executed:

#!/bin/bash
echo "Searching for getDevicesSummaryApi definitions in Analytics API:"
rg -n -C3 'getDevicesSummaryApi' src/platform/src/core/apis/Analytics.js || echo "No matches found"

echo
echo "Looking for any export of getDevicesSummaryApi:"
rg -n -C3 'export\s+function\s+getDevicesSummaryApi' src/platform/src/core/apis/Analytics.js || echo "No explicit function export"

echo
echo "Searching for default export patterns referencing getDevicesSummaryApi:"
rg -n -C3 'export\s+default\s+' src/platform/src/core/apis/Analytics.js || echo "No default export"

Length of output: 249


Fix missing Analytics API export and verify signature

  • getDevicesSummaryApi is not exported from src/platform/src/core/apis/Analytics.js; update the export or correct the import path.
  • Ensure its implementation accepts a category parameter in its options.
🤖 Prompt for AI Agents
In src/platform/src/core/hooks/usePaginatedData.js around lines 385 to 436, the
code imports getDevicesSummaryApi from ../apis/Analytics but that export is
missing or has a different name/signature; update the Analytics module to export
getDevicesSummaryApi (or change this import to the correct exported name/path)
and ensure the exported function accepts an options object that includes
category, search, skip, limit and signal so the call here works as expected.

Copy link
Member Author

Choose a reason for hiding this comment

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

It is the correct path; found in Analytics.js. Check that file to confirm

Copy link
Contributor

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check Analytics.js file for getDevicesSummaryApi export and signature
echo "=== Examining Analytics.js file ==="
cat src/platform/src/core/apis/Analytics.js | head -100

echo -e "\n=== Searching for getDevicesSummaryApi in Analytics.js ==="
rg -n -A10 'getDevicesSummaryApi' src/platform/src/core/apis/Analytics.js

echo -e "\n=== Checking all exports in Analytics.js ==="
rg -n 'export' src/platform/src/core/apis/Analytics.js | head -50

Length of output: 2891


@Codebmk, you're right that there's a devices function in Analytics.js, but I found two issues:

1. Import name mismatch:

  • Line 428 imports: getDevicesSummaryApi (plural "Devices")
  • Analytics.js exports: getDeviceSummaryApi (singular "Device")

2. Missing category parameter:
The actual getDeviceSummaryApi function in Analytics.js (line 62) has this signature:

export const getDeviceSummaryApi = async ({
  group = null,
  skip = 0,
  limit = 80,
  search = undefined,
  signal,
} = {})

It doesn't support the category parameter that's being passed on line 429.

Questions:

  1. Should the import be corrected to getDeviceSummaryApi (without the 's')?
  2. Does getDeviceSummaryApi need to be updated to accept a category parameter?
  3. Or is there a different API endpoint that should be used for category-filtered devices?

🧬 Code Graph Analysis Results

[src/platform/src/core/apis/Analytics.js] // lines 31-59

export const getSitesSummaryApi = async ({
  group = undefined,
  skip = 0,
  limit = 80,
  search = undefined,
  signal,
} = {}) => {
  const params = {
    skip,
    limit,
    ...(group !== undefined && group !== null && group !== '' ? { group } : {}),
    ...(search !== undefined && search !== null && search.trim() !== ''
      ? { search: search.trim() }
      : {}),
  };

  return secureApiProxy
    .get(SITES_SUMMARY_URL, {
      params,
      authType: AUTH_TYPES.JWT,
      signal,
    })
    .then((response) => response.data)
    .catch((error) => {
      logger.error('getSitesSummaryApi error:', error);
      // Re-throw the error so it can be handled by SWR and components
      throw error;
    });
};

[src/platform/src/core/apis/Analytics.js] // lines 93-120

const getGridSummaryApi = async ({
  admin_level = null,
  skip = 0,
  limit = 80,
  search = undefined,
  signal,
} = {}) => {
  const params = {
    skip,
    limit,
    ...(admin_level ? { admin_level } : {}),
    ...(search !== undefined && search !== null && search.trim() !== ''
      ? { search: search.trim() }
      : {}),
  };

  return secureApiProxy
    .get(GRID_SUMMARY_URL, {
      params,
      authType: AUTH_TYPES.JWT,
      signal,
    })
    .then((response) => response.data)
    .catch((error) => {
      logger.error('getGridSummaryApi error:', error);
      throw error;
    });
};

Copy link
Member Author

Choose a reason for hiding this comment

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

fixed

Copy link
Contributor

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== Checking import statement in usePaginatedData.js ==="
rg -n -C2 "getDevice.*SummaryApi.*import" src/platform/src/core/hooks/usePaginatedData.js

echo -e "\n=== Checking getDeviceSummaryApi export and signature in Analytics.js ==="
rg -n -A15 "export const getDeviceSummaryApi" src/platform/src/core/apis/Analytics.js

echo -e "\n=== Verifying category parameter usage ==="
rg -n -C3 "category" src/platform/src/core/apis/Analytics.js | grep -A3 -B3 "getDeviceSummaryApi"

Length of output: 776


@Codebmk, perfect! I've confirmed both issues are resolved:

Import name corrected: Line 427 now correctly imports getDeviceSummaryApi (singular "Device") matching the export in Analytics.js

Category parameter added: Line 68 in Analytics.js shows the function signature now includes category = 'lowcost' as a parameter, and it's properly used in the params object on line 72

The implementation looks solid now. Great work! 🎉


🧬 Code Graph Analysis Results
  • Filename: src/platform/src/core/apis/DeviceRegistry.js
    • Snippet (lines 86-132)
    • Content:
const getDevicesForCohortsApi = ({
  cohort_ids = [],
  skip = 0,
  limit = 30,
  search='',
  category,
  signal,
} = {}) => {
  // Sanitize pagination parameters
  const sanitizedSkip = Math.max(0, parseInt(skip, 10) || 0);
  const sanitizedLimit = Math.max(1, Math.min(100, parseInt(limit, 10) || 30));

  // Put pagination parameters in query params
  const params = {
    skip: sanitizedSkip,
    limit: sanitizedLimit,
    ...(category && { category }),
    ...(search && { search }),
  };

  // Put cohort_ids in request body
  const body = {
    cohort_ids: Array.isArray(cohort_ids) ? cohort_ids : [],
  };

  const config = {
    params,
    authType: AUTH_TYPES.JWT,
  };

  // Add abort signal if provided and still valid
  if (signal && !signal.aborted) {
    config.signal = signal;
  }

  return secureApiProxy
    .post(DEVICES_FOR_COHORTS_URL, body, config)
    .then((response) => response.data)
    .catch((error) => {
      // Handle abort errors gracefully
      if (error.name === 'AbortError' || error.code === 'ECONNABORTED') {
        throw new Error('Request cancelled');
      }
      // Re-throw other errors
      throw error;
    });
};
  • Filename: src/platform/src/core/apis/DeviceRegistry.js
    • Snippet (lines 86-132)
    • Content:
const getDevicesForCohortsApi = ({
  cohort_ids = [],
  skip = 0,
  limit = 30,
  search='',
  category,
  signal,
} = {}) => {
  // Sanitize pagination parameters
  const sanitizedSkip = Math.max(0, parseInt(skip, 10) || 0);
  const sanitizedLimit = Math.max(1, Math.min(100, parseInt(limit, 10) || 30));

  // Put pagination parameters in query params
  const params = {
    skip: sanitizedSkip,
    limit: sanitizedLimit,
    ...(category && { category }),
    ...(search && { search }),
  };

  // Put cohort_ids in request body
  const body = {
    cohort_ids: Array.isArray(cohort_ids) ? cohort_ids : [],
  };

  const config = {
    params,
    authType: AUTH_TYPES.JWT,
  };

  // Add abort signal if provided and still valid
  if (signal && !signal.aborted) {
    config.signal = signal;
  }

  return secureApiProxy
    .post(DEVICES_FOR_COHORTS_URL, body, config)
    .then((response) => response.data)
    .catch((error) => {
      // Handle abort errors gracefully
      if (error.name === 'AbortError' || error.code === 'ECONNABORTED') {
        throw new Error('Request cancelled');
      }
      // Re-throw other errors
      throw error;
    });
};
  • Filename: src/platform/src/core/apis/Analytics.js
    • Snippet (lines 31-59)
    • Content:
export const getSitesSummaryApi = async ({
  group = undefined,
  skip = 0,
  limit = 80,
  search = undefined,
  signal,
} = {}) => {
  const params = {
    skip,
    limit,
    ...(group !== undefined && group !== null && group !== '' ? { group } : {}),
    ...(search !== undefined && search !== null && search.trim() !== ''
      ? { search: search.trim() }
      : {}),
  };

  return secureApiProxy
    .get(SITES_SUMMARY_URL, {
      params,
      authType: AUTH_TYPES.JWT,
      signal,
    })
    .then((response) => response.data)
    .catch((error) => {
      logger.error('getSitesSummaryApi error:', error);
      // Re-throw the error so it can be handled by SWR and components
      throw error;
    });
};
  • Filename: src/platform/src/core/apis/Analytics.js
    • Snippet (lines 62-92)
    • Content:
export const getDeviceSummaryApi = async ({
  group = null,
  skip = 0,
  limit = 80,
  search = undefined,
  signal,
  category = 'lowcost',
} = {}) => {
  const params = {
    status: 'deployed',
    ...(category ? { category } : {}),
    skip,
    limit,
    ...(group ? { group } : {}),
    ...(search !== undefined && search !== null && search.trim() !== ''
      ? { search: search.trim() }
      : {}),
  };

  return secureApiProxy
    .get(DEVICE_SUMMARY_URL, {
      params,
      authType: AUTH_TYPES.JWT,
      signal,
    })
    .then((response) => response.data)
    .catch((error) => {
      logger.error('getDeviceSummaryApi error:', error);
      throw error;
    });
};
  • Filename: src/platform/src/core/apis/Analytics.js
    • Snippet (lines 95-122)
    • Content:
export const getGridSummaryApi = async ({
  admin_level = null,
  skip = 0,
  limit = 80,
  search = undefined,
  signal,
} = {}) => {
  const params = {
    skip,
    limit,
    ...(admin_level ? { admin_level } : {}),
    ...(search !== undefined && search !== null && search.trim() !== ''
      ? { search: search.trim() }
      : {}),
  };

  return secureApiProxy
    .get(GRID_SUMMARY_URL, {
      params,
      authType: AUTH_TYPES.JWT,
      signal,
    })
    .then((response) => response.data)
    .catch((error) => {
      logger.error('getGridSummaryApi error:', error);
      throw error;
    });
};

If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

Copy link
Contributor

New next-platform changes available for preview here

1 similar comment
Copy link
Contributor

New next-platform changes available for preview here

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

♻️ Duplicate comments (1)
src/platform/src/core/hooks/usePaginatedData.js (1)

391-425: Critical: Apply same fixes for abort handling, cohort extraction, and meta structure.

The devices fetcher has identical issues to the sites fetcher:

  1. Lines 417-418: Wrapping aborts breaks SWR's abort detection.
  2. Line 399: Cohort ID extraction assumes cohortsResponse?.data is an array of strings.
  3. Line 402: Returning { devices: [], meta: {} } lacks explicit pagination fields.

Apply this diff:

   const fetcher = useCallback(
     async (params, signal) => {
       if (normalizedGroup) {
-
         const { getGroupCohortsApi } = await import('../apis/Account');
         const { getDevicesForCohortsApi } = await import('../apis/DeviceRegistry');
 
-        // Step 1: Fetch cohorts for the group
         try {
           const cohortsResponse = await getGroupCohortsApi(normalizedGroup, signal);
-          const cohortIds = cohortsResponse?.data || [];
+          const raw = cohortsResponse;
+          const source =
+            Array.isArray(raw)
+              ? raw
+              : Array.isArray(raw?.data)
+              ? raw.data
+              : raw?.cohorts ?? raw?.results ?? [];
+          const cohortIds = (Array.isArray(source) ? source : [])
+            .map((c) => (typeof c === 'string' ? c : c?._id ?? c?.id))
+            .filter(Boolean);
 
           if (cohortIds.length === 0) {
-            return { devices: [], meta: {} };
+            return {
+              devices: [],
+              meta: { total: 0, totalPages: 0, nextPage: null, previousPage: null },
+            };
           }
 
-          // Step 2: Fetch devices for the retrieved cohorts
           return getDevicesForCohortsApi(
             {
               cohort_ids: cohortIds,
               skip: params.skip,
               limit: params.limit,
-              category: category,
+              category,
               ...(search && { search }),
               signal
             },
           );
         } catch (error) {
-          if (error.name === 'AbortError' || error.code === 'ECONNABORTED') {
-            throw new Error('Request cancelled');
+          if (
+            error?.name === 'AbortError' ||
+            error?.name === 'CanceledError' ||
+            error?.code === 'ERR_CANCELED' ||
+            error?.code === 'ECONNABORTED'
+          ) {
+            throw error;
           }
-          // Handle cohort fetch errors
           throw new Error(
             `Unable to fetch devices: ${getApiErrorMessage(error)}`
           );
         }
       }
 
       const { getDeviceSummaryApi } = await import('../apis/Analytics');
       return getDeviceSummaryApi({
         ...params,
         category,
         ...(search && { search }),
         signal
       });
-
     },
     [normalizedGroup, category, search],
   );
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 691d965 and 6a0da44.

📒 Files selected for processing (2)
  • src/platform/src/core/apis/Analytics.js (1 hunks)
  • src/platform/src/core/hooks/usePaginatedData.js (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/platform/src/core/hooks/usePaginatedData.js (3)
src/platform/src/core/apis/Account.js (3)
  • getGroupCohortsApi (259-269)
  • getGroupCohortsApi (259-269)
  • params (325-325)
src/platform/src/core/apis/DeviceRegistry.js (10)
  • getSitesForCohortsApi (134-172)
  • getSitesForCohortsApi (134-172)
  • params (48-48)
  • params (99-104)
  • params (147-152)
  • params (232-232)
  • params (257-257)
  • params (281-281)
  • getDevicesForCohortsApi (86-132)
  • getDevicesForCohortsApi (86-132)
src/platform/src/core/apis/Analytics.js (7)
  • params (38-45)
  • params (70-79)
  • params (102-109)
  • getSitesSummaryApi (31-59)
  • getSitesSummaryApi (31-59)
  • getDeviceSummaryApi (62-92)
  • getDeviceSummaryApi (62-92)
src/platform/src/core/apis/Analytics.js (1)
src/platform/src/core/apis/DeviceRegistry.js (6)
  • params (48-48)
  • params (99-104)
  • params (147-152)
  • params (232-232)
  • params (257-257)
  • params (281-281)
⏰ 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-push-deploy-analytics-platform-preview
🔇 Additional comments (1)
src/platform/src/core/apis/Analytics.js (1)

68-72: LGTM! Category parameter properly integrated.

The addition of the category parameter with a sensible default ('lowcost') and conditional inclusion in params is well-implemented. This aligns with the broader cohort-based filtering introduced across the PR.

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)
src/platform/src/common/features/analytics-overview/hooks/useAnalyticsDownload.js (1)

187-187: Add deviceCategory to the dependency array.

The deviceCategory parameter is used inside the callback (line 139) but is not included in the dependency array. This could lead to stale closures where the callback uses an outdated deviceCategory value.

Apply this diff:

     },
-    [chartData, fetchData],
+    [chartData, fetchData, deviceCategory],
   );
♻️ Duplicate comments (2)
src/platform/src/core/hooks/usePaginatedData.js (2)

320-364: Critical: Fix abort handling, cohort extraction, meta structure, and parameter leakage.

Four issues need to be addressed in the sites fetcher:

  1. Lines 347-348: Wrapping abort errors in new Error('Request cancelled') breaks SWR's abort detection. The outer paginatedFetcher (lines 76-84) expects native abort errors to return undefined without updating the cache. This causes spurious errors during rapid navigation or typing.

  2. Line 333: Extracting cohort IDs as cohortsResponse?.data || [] assumes a specific response shape. If the API returns { cohorts: [...] }, { results: [...] }, or an array of objects with _id/id fields, extraction fails silently.

  3. Line 336: Returning { sites: [], meta: {} } when no cohorts are found provides an incomplete pagination structure. Components expecting meta.total, meta.totalPages, etc., may encounter undefined errors.

  4. Line 342: The params object may contain a page property that should not be passed to DeviceRegistry APIs.

Apply this diff:

   const fetcher = useCallback(
     async (params, signal) => {
       if (normalizedGroup) {
         const { getGroupCohortsApi } = await import('../apis/Account');
         const { getSitesForCohortsApi } = await import(
           '../apis/DeviceRegistry'
         );
 
         try {
           const cohortsResponse = await getGroupCohortsApi(
             normalizedGroup,
             signal,
           );
-          const cohortIds = cohortsResponse?.data || [];
+          // Extract cohort IDs robustly from various response shapes
+          const raw = cohortsResponse;
+          const source =
+            Array.isArray(raw)
+              ? raw
+              : Array.isArray(raw?.data)
+              ? raw.data
+              : raw?.cohorts ?? raw?.results ?? [];
+          const cohortIds = (Array.isArray(source) ? source : [])
+            .map((c) => (typeof c === 'string' ? c : c?._id ?? c?.id))
+            .filter(Boolean);
 
           if (cohortIds.length === 0) {
-            return { sites: [], meta: {} };
+            return {
+              sites: [],
+              meta: { total: 0, totalPages: 0, nextPage: null, previousPage: null },
+            };
           }
 
+          const { page: _page, ...restParams } = params;
           return getSitesForCohortsApi({
             cohort_ids: cohortIds,
-            ...params,
+            ...restParams,
             ...(search && { search }),
             signal,
           });
         } catch (error) {
-          if (error.name === 'AbortError' || error.code === 'ECONNABORTED') {
-            throw new Error('Request cancelled');
+          // Preserve abort identity so SWR can handle gracefully
+          if (
+            error?.name === 'AbortError' ||
+            error?.name === 'CanceledError' ||
+            error?.code === 'ERR_CANCELED' ||
+            error?.code === 'ECONNABORTED'
+          ) {
+            throw error;
           }
           throw new Error(
             `Unable to fetch sites: ${getApiErrorMessage(error)}`
           );
         }
       }
 
       const { getSitesSummaryApi } = await import('../apis/Analytics');
       return getSitesSummaryApi({
         ...params,
         ...(search && { search }),
         signal
       });
     },
     [normalizedGroup, search],
   );

389-437: Critical: Apply the same fixes to the devices fetcher.

The devices fetcher has the same issues as the sites fetcher:

  1. Lines 417-418: Wrapping abort errors breaks SWR's abort detection.
  2. Line 399: Fragile cohort ID extraction.
  3. Line 402: Incomplete meta structure.

Additionally, verify that params doesn't leak unwanted properties to the API calls.

Apply this diff:

   const fetcher = useCallback(
     async (params, signal) => {
       if (normalizedGroup) {
         const { getGroupCohortsApi } = await import('../apis/Account');
         const { getDevicesForCohortsApi } = await import('../apis/DeviceRegistry');
 
         try {
           const cohortsResponse = await getGroupCohortsApi(normalizedGroup, signal);
-          const cohortIds = cohortsResponse?.data || [];
+          // Extract cohort IDs robustly from various response shapes
+          const raw = cohortsResponse;
+          const source =
+            Array.isArray(raw)
+              ? raw
+              : Array.isArray(raw?.data)
+              ? raw.data
+              : raw?.cohorts ?? raw?.results ?? [];
+          const cohortIds = (Array.isArray(source) ? source : [])
+            .map((c) => (typeof c === 'string' ? c : c?._id ?? c?.id))
+            .filter(Boolean);
 
           if (cohortIds.length === 0) {
-            return { devices: [], meta: {} };
+            return {
+              devices: [],
+              meta: { total: 0, totalPages: 0, nextPage: null, previousPage: null },
+            };
           }
 
           return getDevicesForCohortsApi(
             {
               cohort_ids: cohortIds,
               skip: params.skip,
               limit: params.limit,
               ...(category && { category }),
               ...(search && { search }),
               signal
             },
           );
         } catch (error) {
-          if (error.name === 'AbortError' || error.code === 'ECONNABORTED') {
-            throw new Error('Request cancelled');
+          if (
+            error?.name === 'AbortError' ||
+            error?.name === 'CanceledError' ||
+            error?.code === 'ERR_CANCELED' ||
+            error?.code === 'ECONNABORTED'
+          ) {
+            throw error;
           }
           throw new Error(
             `Unable to fetch devices: ${getApiErrorMessage(error)}`
           );
         }
       }
 
       const { getDeviceSummaryApi } = await import('../apis/Analytics');
       return getDeviceSummaryApi({
         ...params,
         ...(category && { category }),
         ...(search && { search }),
         signal
       });
     },
     [normalizedGroup, category, search],
   );
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6a0da44 and 82d96a7.

📒 Files selected for processing (4)
  • src/platform/src/common/features/analytics-overview/hooks/useAnalyticsDownload.js (2 hunks)
  • src/platform/src/common/features/download-insights-locations/data-download/components/DataContent.js (3 hunks)
  • src/platform/src/common/features/download-insights-locations/more-insights/hooks/useDownload.js (3 hunks)
  • src/platform/src/core/hooks/usePaginatedData.js (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/platform/src/common/features/download-insights-locations/data-download/components/DataContent.js
🧰 Additional context used
🧬 Code graph analysis (2)
src/platform/src/core/hooks/usePaginatedData.js (3)
src/platform/src/core/apis/Account.js (3)
  • getGroupCohortsApi (259-269)
  • getGroupCohortsApi (259-269)
  • params (325-325)
src/platform/src/core/apis/DeviceRegistry.js (10)
  • getSitesForCohortsApi (134-172)
  • getSitesForCohortsApi (134-172)
  • params (48-48)
  • params (99-104)
  • params (147-152)
  • params (232-232)
  • params (257-257)
  • params (281-281)
  • getDevicesForCohortsApi (86-132)
  • getDevicesForCohortsApi (86-132)
src/platform/src/core/apis/Analytics.js (7)
  • params (38-45)
  • params (70-79)
  • params (102-109)
  • getSitesSummaryApi (31-59)
  • getSitesSummaryApi (31-59)
  • getDeviceSummaryApi (62-92)
  • getDeviceSummaryApi (62-92)
src/platform/src/common/features/download-insights-locations/more-insights/hooks/useDownload.js (2)
src/platform/src/common/features/download-insights-locations/more-insights/hooks/useMoreInsights.js (4)
  • visibleSites (93-93)
  • allSites (22-28)
  • dateRange (90-90)
  • frequency (74-74)
src/platform/src/common/features/airQuality-map/hooks/useMapPageState.js (1)
  • pollutant (34-34)
⏰ 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-push-deploy-analytics-platform-preview
🔇 Additional comments (4)
src/platform/src/common/features/download-insights-locations/more-insights/hooks/useDownload.js (1)

84-92: LGTM: Dependency array correctly includes deviceCategory.

The addition of deviceCategory to the dependency array is correct and ensures the callback is properly re-created when the category changes.

src/platform/src/common/features/analytics-overview/hooks/useAnalyticsDownload.js (1)

79-83: LGTM: Clean function signature update.

The addition of deviceCategory parameter with a default value is well-implemented and follows a cleaner pattern compared to the arguments[0] approach used elsewhere.

src/platform/src/core/hooks/usePaginatedData.js (2)

440-440: LGTM: SWR cache key properly includes category.

The cache key correctly includes the category parameter, ensuring that data is properly cached and invalidated when the category changes.


5-5: LGTM: Good addition for consistent error handling.

The getApiErrorMessage utility import supports consistent error messaging across the cohort-based fetching flows.

…eta; avoid leaking page into DeviceRegistry calls.
Copy link
Contributor

New next-platform changes available for preview here

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

🧹 Nitpick comments (2)
src/platform/src/core/hooks/usePaginatedData.js (2)

366-371: Consider stripping page from params.

The ...params spread at line 368 may include a page property, which getSitesForCohortsApi doesn't use (it expects skip/limit). While not harmful, this was flagged in earlier reviews. For cleaner API calls, consider destructuring to exclude page:

           // Step 2: Fetch sites for the retrieved cohorts
+          const { page: _page, ...restParams } = params;
           return getSitesForCohortsApi({
             cohort_ids: cohortIds,
-            ...params,
+            ...restParams,
             ...(search && { search }),
             signal,
           });

373-373: Standardize abort error checks.

Lines 373 and 441 check only error.name === 'AbortError' and error.code === 'ECONNABORTED'. However, the main paginatedFetcher (lines 76-80) also checks for 'CanceledError' and 'ERR_CANCELED', which Axios throws when using axios.CancelToken or signal. For consistency and completeness, consider aligning these checks.

Apply this diff to both locations:

         } catch (error) {
-          if (error.name === 'AbortError' || error.code === 'ECONNABORTED') throw error;
+          if (
+            error?.name === 'AbortError' ||
+            error?.name === 'CanceledError' ||
+            error?.code === 'ERR_CANCELED' ||
+            error?.code === 'ECONNABORTED'
+          ) {
+            throw error;
+          }
           // Handle cohort fetch errors
           throw new Error(

Also applies to: 441-441

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 82d96a7 and 2bac229.

📒 Files selected for processing (1)
  • src/platform/src/core/hooks/usePaginatedData.js (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/platform/src/core/hooks/usePaginatedData.js (3)
src/platform/src/core/apis/Analytics.js (9)
  • response (16-18)
  • response (24-26)
  • params (38-45)
  • params (70-79)
  • params (102-109)
  • getSitesSummaryApi (31-59)
  • getSitesSummaryApi (31-59)
  • getDeviceSummaryApi (62-92)
  • getDeviceSummaryApi (62-92)
src/platform/src/core/apis/Account.js (3)
  • getGroupCohortsApi (259-269)
  • getGroupCohortsApi (259-269)
  • params (325-325)
src/platform/src/core/apis/DeviceRegistry.js (10)
  • params (48-48)
  • params (99-104)
  • params (147-152)
  • params (232-232)
  • params (257-257)
  • params (281-281)
  • getSitesForCohortsApi (134-172)
  • getSitesForCohortsApi (134-172)
  • getDevicesForCohortsApi (86-132)
  • getDevicesForCohortsApi (86-132)
⏰ 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: build-push-deploy-analytics-platform-preview
  • GitHub Check: test-analytics-platform-changes
🔇 Additional comments (2)
src/platform/src/core/hooks/usePaginatedData.js (2)

310-335: Solid defensive extraction of cohort IDs.

The extractCohortIds helper robustly handles multiple API response formats (direct array, data/cohorts/results wrappers, object vs. string IDs) and gracefully returns an empty array for malformed responses. This addresses the earlier concern about fragile cohort ID extraction.


430-439: Good explicit parameter passing.

Unlike the sites fetcher, here you explicitly pass skip, limit, category, and search instead of spreading params. This avoids the potential issue of passing unnecessary properties like page to the API.

Copy link
Contributor

New next-platform changes available for preview here

Copy link
Contributor

New next-platform changes available for preview here

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: 0

🧹 Nitpick comments (1)
src/platform/src/core/hooks/usePaginatedData.js (1)

344-391: Solid cohort-based implementation.

The refactored sites fetcher correctly:

  • Uses extractCohortIds for defensive cohort extraction
  • Preserves abort error identity (line 373) without wrapping
  • Returns properly structured meta with limit fallback when empty (line 362)
  • Applies getApiErrorMessage for consistent error formatting

Minor observation: The sites hook spreads all params (line 368) while the devices hook explicitly passes only skip, limit, etc. (lines 433-434). Both work correctly, but explicit field selection is slightly cleaner and more self-documenting.

Optionally consider passing only necessary fields for consistency:

-          return getSitesForCohortsApi({
-            cohort_ids: cohortIds,
-            ...params,
-            ...(search && { search }),
-            signal,
-          });
+          return getSitesForCohortsApi({
+            cohort_ids: cohortIds,
+            skip: params.skip,
+            limit: params.limit,
+            ...(search && { search }),
+            signal,
+          });
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2bac229 and f00ff6d.

📒 Files selected for processing (2)
  • src/platform/src/common/features/download-insights-locations/more-insights/hooks/useDownload.js (3 hunks)
  • src/platform/src/core/hooks/usePaginatedData.js (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/platform/src/common/features/download-insights-locations/more-insights/hooks/useDownload.js
🧰 Additional context used
🧬 Code graph analysis (1)
src/platform/src/core/hooks/usePaginatedData.js (3)
src/platform/src/core/apis/Analytics.js (9)
  • response (16-18)
  • response (24-26)
  • params (38-45)
  • params (70-79)
  • params (102-109)
  • getSitesSummaryApi (31-59)
  • getSitesSummaryApi (31-59)
  • getDeviceSummaryApi (62-92)
  • getDeviceSummaryApi (62-92)
src/platform/src/core/apis/Account.js (3)
  • getGroupCohortsApi (259-269)
  • getGroupCohortsApi (259-269)
  • params (325-325)
src/platform/src/core/apis/DeviceRegistry.js (10)
  • params (48-48)
  • params (99-104)
  • params (147-152)
  • params (232-232)
  • params (257-257)
  • params (281-281)
  • getSitesForCohortsApi (134-172)
  • getSitesForCohortsApi (134-172)
  • getDevicesForCohortsApi (86-132)
  • getDevicesForCohortsApi (86-132)
⏰ 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-push-deploy-analytics-platform-preview
🔇 Additional comments (3)
src/platform/src/core/hooks/usePaginatedData.js (3)

5-5: Good addition for consistent error messaging.

The getApiErrorMessage import supports centralized error formatting in the new cohort-based flows.


310-335: Well-implemented defensive extraction.

The extractCohortIds helper elegantly handles multiple API response shapes (nested arrays, direct arrays, objects with _id/id, string arrays) and provides a safe empty array fallback. Good defensive programming.


408-471: Excellent devices hook implementation.

The devices fetcher correctly:

  • Handles category parameter throughout cohort and non-cohort flows
  • Explicitly passes only necessary fields to getDevicesForCohortsApi (lines 433-434) for clarity
  • Preserves abort error identity (line 441)
  • Includes comprehensive useCallback dependencies (line 458) and SWR key (line 462)

The implementation is clean, consistent, and well-structured.

@Baalmart Baalmart merged commit 159feb4 into staging Oct 16, 2025
19 of 20 checks passed
@Baalmart Baalmart deleted the cohorts-data-download branch October 16, 2025 06:28
@Baalmart Baalmart mentioned this pull request Oct 16, 2025
2 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants