Skip to content

Conversation

@david-roper
Copy link
Collaborator

@david-roper david-roper commented Nov 5, 2025

will close issue #1220

problems with collecting sessions for then use the userids in the subject table export

Summary by CodeRabbit

  • New Features

    • Bulk session retrieval endpoint accepting multiple IDs.
    • Frontend helpers to fetch session and user info; visualization exports now include Username (with fallbacks).
    • Visualization loading updated to asynchronously enrich records with session/user data before export.
  • Tests

    • Export/download tests updated to wait for async enrichment and include user-related headers and values.

@david-roper david-roper self-assigned this Nov 5, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 5, 2025

Warning

Rate limit exceeded

@david-roper has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 6 minutes and 42 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 75c42f0 and 04cc380.

📒 Files selected for processing (3)
  • apps/api/src/sessions/sessions.controller.ts (1 hunks)
  • apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts (6 hunks)
  • apps/web/src/hooks/useInstrumentVisualization.ts (5 hunks)

Walkthrough

Adds a POST /sessions/list endpoint and SessionsService.findSessionList to return multiple sessions by IDs with per-session ability checks. Adds frontend helpers sessionInfo and userInfo, updates useInstrumentVisualization to asynchronously enrich records with session/user data and include Username in long-form exports, and updates tests to mock and await the async flow.

Changes

Cohort / File(s) Summary
Sessions API Controller & Service
apps/api/src/sessions/sessions.controller.ts, apps/api/src/sessions/sessions.service.ts
New POST /sessions/list endpoint accepting ids (string[] or comma-separated string), normalizes input and calls SessionsService.findSessionList(ids, { ability }). Adds findSessionList which concurrently fetches sessions, enforces per-session ability checks, and throws NotFoundException if any id is missing.
Client-side session/user helpers
apps/web/src/hooks/useFindSession.ts, apps/web/src/hooks/useFindUser.ts
New helpers sessionInfo(sessionId) and userInfo(userId) performing GET /v1/sessions/{id} and /v1/users/{id} via axios; return entity or null when no data, log and rethrow on error.
Instrument visualization & tests
apps/web/src/hooks/useInstrumentVisualization.ts, apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts
Converts record processing to an async enrichment flow: per-record sessionInfo and userInfo calls to derive Username for long-form exports (fallback 'N/A'), aggregates enriched records and updates state after completion, adds error logging, and updates tests to mock session/user data and await async updates with waitFor.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant Client
    participant Controller as SessionsController
    participant Service as SessionsService
    participant DB as Database
    participant Ability as AccessControl

    Client->>Controller: POST /sessions/list?ids=id1,id2
    Controller->>Controller: normalize ids -> [id1,id2]
    Controller->>Service: findSessionList([id1,id2], {ability})
    loop per id
        Service->>Ability: authorize access for id
        Ability-->>Service: allowed/denied
        Service->>DB: fetch session by id
        DB-->>Service: session | not found
    end
    Service->>Controller: return Session[]
    Controller-->>Client: 200 OK with Session[]
Loading
sequenceDiagram
    autonumber
    participant Hook as useInstrumentVisualization
    participant Helpers as sessionInfo/userInfo
    participant Backend as API Server

    Hook->>Hook: iterate records
    loop per record
        Hook->>Helpers: sessionInfo(sessionId)
        Helpers->>Backend: GET /v1/sessions/{sessionId}
        Backend-->>Helpers: Session | error
        Helpers-->>Hook: Session | null
        alt session has userId
            Hook->>Helpers: userInfo(userId)
            Helpers->>Backend: GET /v1/users/{userId}
            Backend-->>Helpers: User | error
            Helpers-->>Hook: User | null
            Hook->>Hook: set Username (user.username or 'N/A')
        else
            Hook->>Hook: set Username = 'N/A'
        end
    end
    Hook->>Hook: setState(enrichedRecords)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Review concurrency and NotFound behavior in SessionsService.findSessionList (Promise.all semantics).
  • Verify per-session authorization ordering and correct propagation of ability.
  • Inspect useInstrumentVisualization for potential race conditions, unmount cancellation, and batching of state updates.
  • Confirm tests correctly mock sessionInfo/userInfo and use waitFor to await async state changes.

Possibly related PRs

Suggested reviewers

  • joshunrau

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 'Add username to export' directly matches the main change: enriching export records with username data via session and user information fetches.

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.

@david-roper david-roper force-pushed the add-username-to-export branch from 0635ee4 to fd0a90c Compare November 5, 2025 21:52
@david-roper david-roper force-pushed the add-username-to-export branch from 4e28260 to 5e1c096 Compare November 6, 2025 19:40
@david-roper david-roper marked this pull request as ready for review November 14, 2025 20:10
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: 4

🧹 Nitpick comments (5)
apps/api/src/sessions/sessions.service.ts (1)

106-119: N+1 query pattern impacts performance for bulk lookups.

The current implementation executes N separate database queries via Promise.all, which increases latency and database load for large ID arrays. Consider using a single findMany query and validating the results afterward.

Apply this refactor:

  async findSessionList(ids: string[], { ability }: EntityOperationOptions = {}) {
-    const sessionsArray = await Promise.all(
-      ids.map(async (id) => {
-        const session = await this.sessionModel.findFirst({
-          where: { AND: [accessibleQuery(ability, 'read', 'Session')], id }
-        });
-        if (!session) {
-          throw new NotFoundException(`Failed to find session with ID: ${id}`);
-        }
-        return session;
-      })
-    );
-    return sessionsArray;
+    const sessions = await this.sessionModel.findMany({
+      where: { AND: [accessibleQuery(ability, 'read', 'Session'), { id: { in: ids } }] }
+    });
+    
+    if (sessions.length !== ids.length) {
+      const foundIds = new Set(sessions.map(s => s.id));
+      const missingId = ids.find(id => !foundIds.has(id));
+      throw new NotFoundException(`Failed to find session with ID: ${missingId}`);
+    }
+    
+    return sessions;
  }
apps/web/src/hooks/useFindUser.ts (1)

4-12: Consider validating the response data.

The type assertion response.data as User assumes the API response matches the User schema without validation. If the API returns an unexpected shape, this could cause runtime errors downstream.

Consider adding runtime validation using a Zod schema if available:

 export const userInfo = async (userId: string): Promise<null | User> => {
   try {
     const response = await axios.get(`/v1/users/${userId}`);
-    return response.data ? (response.data as User) : null;
+    if (!response.data) return null;
+    // Assuming $User schema is available from @opendatacapture/schemas/user
+    const parsed = $User.safeParse(response.data);
+    return parsed.success ? parsed.data : null;
   } catch (error) {
     console.error('Error fetching user:', error);
     return null;
   }
 };
apps/web/src/hooks/useFindSession.ts (1)

4-12: Consider validating the response data.

Similar to userInfo, the type assertion response.data as Session assumes the API response matches the Session schema without validation.

Consider adding runtime validation:

 export const sessionInfo = async (sessionId: string): Promise<null | Session> => {
   try {
     const response = await axios.get(`/v1/sessions/${sessionId}`);
-    return response.data ? (response.data as Session) : null;
+    if (!response.data) return null;
+    // Assuming $Session schema is available from @opendatacapture/schemas/session
+    const parsed = $Session.safeParse(response.data);
+    return parsed.success ? parsed.data : null;
   } catch (error) {
-    console.error('Error fetching user:', error);
+    console.error('Error fetching session:', error);
     return null;
   }
 };
apps/web/src/hooks/useInstrumentVisualization.ts (2)

99-109: Initialize the username variable.

The username variable is declared without initialization. While the current flow ensures it gets assigned, this pattern is fragile.

-    let username: string;
+    let username: string = 'N/A';

221-221: Consider renaming the field from userId to username.

The field is named userId but contains the username string value. This naming mismatch could confuse future maintainers.

          records.push({
            __date__: record.date,
            __time__: record.date.getTime(),
-            userId: 'N/A',
+            username: 'N/A',
            ...record.computedMeasures,
            ...props
          });

And similarly on line 233. Also update the field extraction in makeLongRows (line 106).

Also applies to: 233-233

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1388475 and 233e34d.

📒 Files selected for processing (6)
  • apps/api/src/sessions/sessions.controller.ts (2 hunks)
  • apps/api/src/sessions/sessions.service.ts (1 hunks)
  • apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts (6 hunks)
  • apps/web/src/hooks/useFindSession.ts (1 hunks)
  • apps/web/src/hooks/useFindUser.ts (1 hunks)
  • apps/web/src/hooks/useInstrumentVisualization.ts (5 hunks)
🧰 Additional context used
🧬 Code graph analysis (6)
apps/api/src/sessions/sessions.service.ts (1)
apps/api/src/core/types.ts (1)
  • EntityOperationOptions (3-5)
apps/web/src/hooks/useFindSession.ts (1)
packages/schemas/src/session/session.ts (1)
  • Session (9-9)
apps/web/src/hooks/useFindUser.ts (1)
packages/schemas/src/user/user.ts (1)
  • User (10-10)
apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts (1)
apps/web/src/hooks/useInstrumentVisualization.ts (1)
  • useInstrumentVisualization (32-254)
apps/api/src/sessions/sessions.controller.ts (1)
packages/schemas/src/session/session.ts (1)
  • Session (9-9)
apps/web/src/hooks/useInstrumentVisualization.ts (2)
apps/web/src/hooks/useFindSession.ts (1)
  • sessionInfo (4-12)
apps/web/src/hooks/useFindUser.ts (1)
  • userInfo (4-12)
🔇 Additional comments (3)
apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts (3)

29-81: LGTM!

The mock setup correctly provides session and user data to support the async enrichment flow. Mock objects are properly structured with the expected fields.


88-123: LGTM!

Tests correctly use waitFor to handle async record population and verify username is included in CSV/TSV exports.


124-232: LGTM!

All export format tests consistently verify username data is included and properly handle the async record enrichment flow.

Comment on lines 207 to 243
useEffect(() => {
if (recordsQuery.data) {
const records: InstrumentVisualizationRecord[] = [];
for (const record of recordsQuery.data) {
const props = record.data && typeof record.data === 'object' ? record.data : {};
records.push({
__date__: record.date,
__time__: record.date.getTime(),
...record.computedMeasures,
...props
});
const fetchRecords = async () => {
if (recordsQuery.data) {
const records: InstrumentVisualizationRecord[] = [];

for (const record of recordsQuery.data) {
const props = record.data && typeof record.data === 'object' ? record.data : {};

const sessionData = await sessionInfo(record.sessionId);

if (!sessionData?.userId) {
records.push({
__date__: record.date,
__time__: record.date.getTime(),
userId: 'N/A',
...record.computedMeasures,
...props
});
continue;
}

const userData = await userInfo(sessionData.userId);
// safely check since userData can be null
records.push({
__date__: record.date,
__time__: record.date.getTime(),
userId: userData?.username ?? 'N/A',
...record.computedMeasures,
...props
});
}

setRecords(records);
}
setRecords(records);
}
};
void fetchRecords();
}, [recordsQuery.data]);
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 | 🟠 Major

Parallelize API calls to avoid performance bottleneck.

The current implementation fetches session and user data sequentially for each record. With N records, this performs 2N sequential API calls, which could take significant time for large datasets.

Refactor to fetch all sessions and users in parallel:

  useEffect(() => {
    const fetchRecords = async () => {
      if (recordsQuery.data) {
-        const records: InstrumentVisualizationRecord[] = [];
-
-        for (const record of recordsQuery.data) {
-          const props = record.data && typeof record.data === 'object' ? record.data : {};
-
-          const sessionData = await sessionInfo(record.sessionId);
-
-          if (!sessionData?.userId) {
-            records.push({
-              __date__: record.date,
-              __time__: record.date.getTime(),
-              userId: 'N/A',
-              ...record.computedMeasures,
-              ...props
-            });
-            continue;
-          }
-
-          const userData = await userInfo(sessionData.userId);
-          // safely check since userData can be null
-          records.push({
-            __date__: record.date,
-            __time__: record.date.getTime(),
-            userId: userData?.username ?? 'N/A',
-            ...record.computedMeasures,
-            ...props
-          });
-        }
+        // Fetch all sessions in parallel
+        const sessionPromises = recordsQuery.data.map(record => 
+          sessionInfo(record.sessionId)
+        );
+        const sessions = await Promise.all(sessionPromises);
+        
+        // Extract unique userIds and fetch users in parallel
+        const userIds = [...new Set(sessions.filter(s => s?.userId).map(s => s!.userId))];
+        const userPromises = userIds.map(userId => userInfo(userId));
+        const users = await Promise.all(userPromises);
+        const userMap = new Map(users.filter(u => u).map(u => [u!.id, u!.username]));
+        
+        // Build records with looked-up data
+        const records: InstrumentVisualizationRecord[] = recordsQuery.data.map((record, i) => {
+          const props = record.data && typeof record.data === 'object' ? record.data : {};
+          const session = sessions[i];
+          const username = session?.userId ? (userMap.get(session.userId) ?? 'N/A') : 'N/A';
+          
+          return {
+            __date__: record.date,
+            __time__: record.date.getTime(),
+            userId: username,
+            ...record.computedMeasures,
+            ...props
+          };
+        });

        setRecords(records);
      }
    };
    void fetchRecords();
  }, [recordsQuery.data]);

Note: This assumes User has an id field. Adjust the mapping logic if the field name differs.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apps/web/src/hooks/useInstrumentVisualization.ts around lines 207 to 243, the
code currently awaits sessionInfo and userInfo sequentially for each record
causing 2N serial API calls; change to parallelize: first collect all
record.sessionId values and call sessionInfo for all of them with Promise.all
(or a batched API if available), map results by sessionId, then collect unique
userIds from those session results and call userInfo in parallel for those
unique IDs with Promise.all (or batched API), map users by id, and finally
iterate records to build the records array using the session and user maps
(preserving null checks and using 'N/A' fallbacks for missing user or username)
before calling setRecords.

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 660a6f8 and 603930a.

📒 Files selected for processing (1)
  • apps/api/src/sessions/sessions.controller.ts (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
apps/api/src/sessions/sessions.controller.ts (2)
apps/web/src/store/types.ts (1)
  • CurrentUser (8-10)
packages/schemas/src/session/session.ts (1)
  • Session (9-9)
⏰ 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: lint-and-test

Comment on lines 28 to 34
@ApiOperation({ description: 'Find Sessions by ID' })
@Post('list')
@RouteAccess({ action: 'read', subject: 'Session' })
findSessionList(@Query('ids') ids: string[], @CurrentUser('ability') ability: AppAbility): Promise<Session[]> {
const idArray = Array.isArray(ids) ? ids : (ids as string).split(',');
return this.sessionsService.findSessionList(idArray, { ability });
}
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 | 🟠 Major

Use request body instead of query parameters for POST endpoint.

POST endpoints should use @Body rather than @Query. Query parameters are subject to URL length limits whether using GET or POST, so using POST with query parameters doesn't solve the problem of handling large ID lists. Either switch to GET with query parameters, or use POST with the IDs in the request body.

If POST with body is preferred (recommended for potentially large ID lists):

+import type { string[] } from '@nestjs/common';

- findSessionList(@Query('ids') ids: string[], @CurrentUser('ability') ability: AppAbility): Promise<Session[]> {
+ findSessionList(@Body() body: { ids: string[] }, @CurrentUser('ability') ability: AppAbility): Promise<Session[]> {
-   const idArray = Array.isArray(ids) ? ids : (ids as string).split(',');
+   const idArray = body.ids;
    return this.sessionsService.findSessionList(idArray, { ability });
  }

Alternatively, if the ID list is expected to be small, use GET:

+ @Get('list')
- @Post('list')

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apps/api/src/sessions/sessions.controller.ts around lines 28-34, the POST
'list' endpoint currently reads ids from @Query which is inappropriate for
potentially large ID lists; either change the route to GET if ids will remain
small or (recommended) switch to POST with the request body: replace the
@Query('ids') parameter with a @Body('ids') or, better, a DTO that defines ids:
string[] (and add class-validator decorators), remove the Array.isArray/ split
parsing (expect an array), update the method signature and any
callers/tests/Swagger docs accordingly, and keep calling
this.sessionsService.findSessionList(idArray, { ability }) with the parsed
array.

Comment on lines 31 to 33
findSessionList(@Query('ids') ids: string[], @CurrentUser('ability') ability: AppAbility): Promise<Session[]> {
const idArray = Array.isArray(ids) ? ids : (ids as string).split(',');
return this.sessionsService.findSessionList(idArray, { ability });
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 | 🟠 Major

Add validation for the number of IDs.

No limit is enforced on the number of IDs, which could cause performance degradation or database overload if a large list is provided.

Consider adding validation:

@ApiOperation({ description: 'Find Sessions by ID' })
@Post('list')
@RouteAccess({ action: 'read', subject: 'Session' })
findSessionList(@Query('ids') ids: string[], @CurrentUser('ability') ability: AppAbility): Promise<Session[]> {
  const idArray = Array.isArray(ids) ? ids : (ids as string).split(',');
  
  if (idArray.length === 0) {
    throw new BadRequestException('At least one ID must be provided');
  }
  if (idArray.length > 100) { // adjust limit based on requirements
    throw new BadRequestException('Too many IDs provided. Maximum 100 allowed.');
  }
  
  return this.sessionsService.findSessionList(idArray, { ability });
}

Note: You'll need to import BadRequestException from @nestjs/common.

🤖 Prompt for AI Agents
In apps/api/src/sessions/sessions.controller.ts around lines 31 to 33, the
controller accepts an unbounded list of IDs which can cause performance or DB
overload; add validation after parsing ids to ensure at least one ID is provided
and enforce an upper limit (e.g., 100) throwing BadRequestException for empty or
too-large lists, and import BadRequestException from @nestjs/common; preserve
existing ability handling and pass the validated idArray to
sessionsService.findSessionList.

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

🧹 Nitpick comments (1)
apps/web/src/hooks/useFindSession.ts (1)

1-12: Filename doesn't match the export.

The file is named useFindSession.ts (React hook convention), but exports sessionInfo, which is a plain async function. Consider renaming the file to sessionInfo.ts or similar for consistency.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 603930a and 0e0f079.

📒 Files selected for processing (1)
  • apps/web/src/hooks/useFindSession.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
apps/web/src/hooks/useFindSession.ts (1)
packages/schemas/src/session/session.ts (1)
  • Session (9-9)
⏰ 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: lint-and-test


export const sessionInfo = async (sessionId: string): Promise<null | Session> => {
try {
const response = await axios.get(`/v1/sessions/${sessionId}`);
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 | 🟡 Minor

Apply URL encoding to sessionId.

The sessionId parameter is interpolated directly into the URL without encoding. If it contains special characters, the request could fail or behave unexpectedly.

Apply this diff:

-    const response = await axios.get(`/v1/sessions/${sessionId}`);
+    const response = await axios.get(`/v1/sessions/${encodeURIComponent(sessionId)}`);
📝 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 response = await axios.get(`/v1/sessions/${sessionId}`);
const response = await axios.get(`/v1/sessions/${encodeURIComponent(sessionId)}`);
🤖 Prompt for AI Agents
In apps/web/src/hooks/useFindSession.ts around line 6, the sessionId is
interpolated directly into the URL which can break for special characters;
replace the raw interpolation with an encoded value using
encodeURIComponent(sessionId) when building the request URL (e.g.,
axios.get(`/v1/sessions/${encodeURIComponent(sessionId)}`)) so the sessionId is
properly URL-encoded before sending the request.

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

♻️ Duplicate comments (1)
apps/web/src/hooks/useInstrumentVisualization.ts (1)

207-247: Sequential API calls create performance bottleneck.

This implementation makes 2N sequential API calls (one sessionInfo + one userInfo per record). For large datasets, this will be slow. Parallelize using Promise.all to fetch all sessions first, then all unique users.

Refactor to batch API calls:

  useEffect(() => {
    const fetchRecords = async () => {
      try {
        if (recordsQuery.data) {
-          const records: InstrumentVisualizationRecord[] = [];
-
-          for (const record of recordsQuery.data) {
-            const props = record.data && typeof record.data === 'object' ? record.data : {};
-
-            const sessionData = await sessionInfo(record.sessionId);
-
-            if (!sessionData?.userId) {
-              records.push({
-                __date__: record.date,
-                __time__: record.date.getTime(),
-                userId: 'N/A',
-                ...record.computedMeasures,
-                ...props
-              });
-              continue;
-            }
-
-            const userData = await userInfo(sessionData.userId);
-            // safely check since userData can be null
-            records.push({
-              __date__: record.date,
-              __time__: record.date.getTime(),
-              userId: userData?.username ?? 'N/A',
-              ...record.computedMeasures,
-              ...props
-            });
-          }
+          // Fetch all sessions in parallel
+          const sessionPromises = recordsQuery.data.map(record => sessionInfo(record.sessionId));
+          const sessions = await Promise.all(sessionPromises);
+          
+          // Extract unique userIds and fetch users in parallel
+          const userIds = [...new Set(sessions.filter(s => s?.userId).map(s => s!.userId))];
+          const userPromises = userIds.map(userId => userInfo(userId));
+          const users = await Promise.all(userPromises);
+          const userMap = new Map(users.filter(u => u).map(u => [u!.id, u!.username]));
+          
+          // Build records with looked-up data
+          const records: InstrumentVisualizationRecord[] = recordsQuery.data.map((record, i) => {
+            const props = record.data && typeof record.data === 'object' ? record.data : {};
+            const session = sessions[i];
+            const username = session?.userId ? (userMap.get(session.userId) ?? 'N/A') : 'N/A';
+            
+            return {
+              __date__: record.date,
+              __time__: record.date.getTime(),
+              userId: username,
+              ...record.computedMeasures,
+              ...props
+            };
+          });

          setRecords(records);
        }
      } catch (error) {
        console.error('Error occurred: ', error);
      }
    };
    void fetchRecords();
  }, [recordsQuery.data]);

Note: Adjust userMap key if User type doesn't have an id field.

🧹 Nitpick comments (1)
apps/web/src/hooks/useInstrumentVisualization.ts (1)

242-244: Surface errors to the user.

The error is logged but not surfaced to the user via notifications. Users won't know if data enrichment failed.

Add notification on error:

      } catch (error) {
        console.error('Error occurred: ', error);
+        notifications.addNotification({ 
+          message: t('errors.failedToEnrichRecords'), 
+          type: 'error' 
+        });
      }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0e0f079 and 5db15bd.

📒 Files selected for processing (3)
  • apps/web/src/hooks/useFindSession.ts (1 hunks)
  • apps/web/src/hooks/useFindUser.ts (1 hunks)
  • apps/web/src/hooks/useInstrumentVisualization.ts (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/web/src/hooks/useFindUser.ts
  • apps/web/src/hooks/useFindSession.ts
🧰 Additional context used
🧬 Code graph analysis (1)
apps/web/src/hooks/useInstrumentVisualization.ts (2)
apps/web/src/hooks/useFindSession.ts (1)
  • sessionInfo (4-12)
apps/web/src/hooks/useFindUser.ts (1)
  • userInfo (4-12)
⏰ 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: lint-and-test
🔇 Additional comments (1)
apps/web/src/hooks/useInstrumentVisualization.ts (1)

16-17: LGTM!

Imports are correct and necessary for the username enrichment functionality.

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

♻️ Duplicate comments (1)
apps/web/src/hooks/useFindSession.ts (1)

6-6: Apply URL encoding to sessionId.

The sessionId is interpolated directly without encoding. Special characters will break the request.

-    const response = await axios.get(`/v1/sessions/${sessionId}`);
+    const response = await axios.get(`/v1/sessions/${encodeURIComponent(sessionId)}`);
🧹 Nitpick comments (2)
apps/web/src/hooks/useFindUser.ts (1)

7-8: Log parse failures.

When safeParse fails, returning null without logging makes debugging difficult. Consider logging the parse error.

     const parsedResult = $User.safeParse(response.data);
+    if (!parsedResult.success) {
+      console.error('Failed to parse user data:', parsedResult.error);
+    }
     return parsedResult.success ? parsedResult.data : null;
apps/web/src/hooks/useFindSession.ts (1)

7-8: Log parse failures.

When safeParse fails, returning null without logging makes debugging difficult. Consider logging the parse error.

     const parsedResult = $Session.safeParse(response.data);
+    if (!parsedResult.success) {
+      console.error('Failed to parse session data:', parsedResult.error);
+    }
     return parsedResult.success ? parsedResult.data : null;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5db15bd and a9d8bd9.

📒 Files selected for processing (2)
  • apps/web/src/hooks/useFindSession.ts (1 hunks)
  • apps/web/src/hooks/useFindUser.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
apps/web/src/hooks/useFindSession.ts (1)
packages/schemas/src/session/session.ts (2)
  • Session (9-9)
  • $Session (10-17)
apps/web/src/hooks/useFindUser.ts (1)
packages/schemas/src/user/user.ts (2)
  • User (10-10)
  • $User (11-20)
🪛 ESLint
apps/web/src/hooks/useFindSession.ts

[error] 1-1: Prefer using a top-level type-only import instead of inline type specifiers.

(import/consistent-type-specifier-style)


[error] 1-1: Expected "$Session" to come before "Session".

(perfectionist/sort-named-imports)

apps/web/src/hooks/useFindUser.ts

[error] 1-1: Prefer using a top-level type-only import instead of inline type specifiers.

(import/consistent-type-specifier-style)


[error] 1-1: Expected "$User" to come before "User".

(perfectionist/sort-named-imports)

⏰ 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: lint-and-test

@@ -0,0 +1,13 @@
import { type User, $User } from '@opendatacapture/schemas/user';
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 | 🟡 Minor

Fix import style.

Use top-level type-only imports and sort named imports alphabetically.

-import { type User, $User } from '@opendatacapture/schemas/user';
+import type { User } from '@opendatacapture/schemas/user';
+import { $User } from '@opendatacapture/schemas/user';

As per coding guidelines

📝 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
import { type User, $User } from '@opendatacapture/schemas/user';
import type { User } from '@opendatacapture/schemas/user';
import { $User } from '@opendatacapture/schemas/user';
🧰 Tools
🪛 ESLint

[error] 1-1: Prefer using a top-level type-only import instead of inline type specifiers.

(import/consistent-type-specifier-style)


[error] 1-1: Expected "$User" to come before "User".

(perfectionist/sort-named-imports)

🤖 Prompt for AI Agents
In apps/web/src/hooks/useFindUser.ts around line 1, the import should use a
top-level type-only import and have named/value imports sorted alphabetically;
split the current combined import into an "import type { User } from
'@opendatacapture/schemas/user';" followed by a separate value import "import {
$User } from '@opendatacapture/schemas/user';" ensuring the type-only import
appears first and any multiple value imports are alphabetized.


export const userInfo = async (userId: string): Promise<null | User> => {
try {
const response = await axios.get(`/v1/users/${userId}`);
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 | 🟡 Minor

Apply URL encoding to userId.

The userId is interpolated directly without encoding. Special characters will break the request.

-    const response = await axios.get(`/v1/users/${userId}`);
+    const response = await axios.get(`/v1/users/${encodeURIComponent(userId)}`);
📝 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 response = await axios.get(`/v1/users/${userId}`);
const response = await axios.get(`/v1/users/${encodeURIComponent(userId)}`);
🤖 Prompt for AI Agents
In apps/web/src/hooks/useFindUser.ts around line 6, the code interpolates userId
directly into the request URL which can break for special characters; URL-encode
userId before building the path (e.g., use encodeURIComponent(userId)) so the
GET request is safe, and update the axios.get call to pass the encoded id.

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 (1)
apps/web/src/hooks/useInstrumentVisualization.ts (1)

207-247: Performance: Sequential API calls already flagged.

This implementation fetches session and user data sequentially for each record (2N serial API calls). A past review comment already provided a detailed refactor to parallelize these calls using Promise.all for better performance with large datasets.

🧹 Nitpick comments (2)
apps/web/src/hooks/useInstrumentVisualization.ts (2)

99-99: Optional: Remove inferrable type annotation.

ESLint flags this line because TypeScript can infer string from the literal value.

Apply this diff:

-        let username: string = 'N/A';
+        let username = 'N/A';

242-244: Optional: Add user notification on enrichment failure.

When session/user data fetching fails, users see no records without explanation. Consider notifying users of the error.

Example approach:

} catch (error) {
  console.error('Error occurred: ', error);
  notifications.addNotification({ 
    message: t('errors.failedToEnrichRecords'), 
    type: 'error' 
  });
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a9d8bd9 and 75c42f0.

📒 Files selected for processing (4)
  • apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts (6 hunks)
  • apps/web/src/hooks/useFindSession.ts (1 hunks)
  • apps/web/src/hooks/useFindUser.ts (1 hunks)
  • apps/web/src/hooks/useInstrumentVisualization.ts (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/web/src/hooks/useFindSession.ts
  • apps/web/src/hooks/useFindUser.ts
🧰 Additional context used
🧬 Code graph analysis (2)
apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts (1)
apps/web/src/hooks/useInstrumentVisualization.ts (1)
  • useInstrumentVisualization (32-258)
apps/web/src/hooks/useInstrumentVisualization.ts (2)
apps/web/src/hooks/useFindSession.ts (1)
  • sessionInfo (5-14)
apps/web/src/hooks/useFindUser.ts (1)
  • userInfo (5-14)
🪛 ESLint
apps/web/src/hooks/useInstrumentVisualization.ts

[error] 99-99: Type string trivially inferred from a string literal, remove type annotation.

(@typescript-eslint/no-inferrable-types)

⏰ 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: lint-and-test
🔇 Additional comments (7)
apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts (4)

2-2: LGTM: Import for async testing.

The waitFor import is correctly added to handle the asynchronous record enrichment flow.


35-47: LGTM: Mock data properly structured.

The addition of sessionId to mock records and the new mockSession/mockUser objects correctly support the enrichment flow tested throughout the suite.


75-81: LGTM: Session and user mocks configured.

The mocks for sessionInfo and userInfo are correctly set up to return the test fixtures.


89-230: LGTM: All tests updated for async enrichment.

All test cases properly:

  • Use waitFor to await the asynchronous record enrichment
  • Include username/Username in expected outputs across all export formats
  • Match the implementation's behavior consistently
apps/web/src/hooks/useInstrumentVisualization.ts (3)

16-17: LGTM: Required imports added.

The sessionInfo and userInfo helpers are correctly imported for record enrichment.


106-109: LGTM: Username extraction in long-form export.

The username field is correctly extracted from enriched records and skipped from variable iteration.


118-118: LGTM: Username included in long-form rows.

Username is consistently injected into both array-item and single-value long-form rows.

Also applies to: 131-131

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.

1 participant