Skip to content

analytics replay filters#1213

Merged
BilalG1 merged 35 commits intodevfrom
analytics-replay-filters
Feb 24, 2026
Merged

analytics replay filters#1213
BilalG1 merged 35 commits intodevfrom
analytics-replay-filters

Conversation

@BilalG1
Copy link
Copy Markdown
Collaborator

@BilalG1 BilalG1 commented Feb 19, 2026

https://www.loom.com/share/5fe96d0d675b455391a6ade1377d2fa5

Summary by CodeRabbit

  • New Features

    • Advanced session replay filtering: user, team, duration range, last-event time window, and minimum click count
    • Dashboard UI: multi-dialog filters, active-filter badges, filter chips, clear actions, and filter-aware no-results messaging
    • New searchable user picker with server-backed pagination
    • Enhanced results: embedded project-user (id, display_name, primary_email), timing fields, chunk/event counts, and composite cursor pagination (next_cursor)
  • Tests

    • Extensive end-to-end coverage for filters, pagination, validation, and edge cases

@vercel
Copy link
Copy Markdown

vercel bot commented Feb 19, 2026

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

Project Deployment Actions Updated (UTC)
stack-backend Ready Ready Preview, Comment Feb 24, 2026 8:38pm
stack-dashboard Ready Ready Preview, Comment Feb 24, 2026 8:38pm
stack-demo Ready Ready Preview, Comment Feb 24, 2026 8:38pm
stack-docs Ready Ready Preview, Comment Feb 24, 2026 8:38pm

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Feb 19, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds server- and client-side session-replay filtering: new query parameters (user/team, duration, last-event window, click_count_min), ClickHouse-backed click-count prefiltering, composite cursor pagination, embedded project_user in rows, dashboard filter UI with a user-search picker, and extensive E2E tests.

Changes

Cohort / File(s) Summary
Backend Session Replays Route
apps/backend/src/app/api/latest/internal/session-replays/route.tsx
Adds query parsing/validation helpers (parseCsvIds, parseCsvUuids, parseNonNegativeInt, parseMillis), new query params (user_ids, team_ids, duration_ms_min/max, last_event_at_from_millis/to_millis, click_count_min), ClickHouse prefetch (loadClickQualifiedReplayIds), single-query join returning embedded project_user, composite cursor pagination, and in-query/in-memory numeric/time filtering.
Dashboard Filtering UI (page-client)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/replays/page-client.tsx
Adds ReplayFilters and EMPTY_FILTERS, draft/applied filter state, filter dialogs (user, team, duration, last-active, clicks), active-filter badge/chips, passes new filter fields to listSessionReplays, updates pagination/loading/selection logic and replayer init tweak.
User Search Picker Component
apps/dashboard/src/components/data-table/user-search-picker.tsx
New UserSearchPicker component: debounced search input, server-backed paginated user table, three columns (display name, primary email, actions), and pagination handling for selecting users in filters.
Shared Interfaces & Types
packages/stack-shared/src/interface/admin-interface.ts, packages/stack-shared/src/interface/crud/session-replays.ts, packages/template/src/lib/stack-app/...
Extends ListSessionReplays option types across layers to include optional filters: user_ids/userIds, team_ids/teamIds, duration_ms_min/durationMsMin, duration_ms_max/durationMsMax, last_event_at_from_millis/lastEventAtFromMillis, last_event_at_to_millis/lastEventAtToMillis, click_count_min/clickCountMin.
E2E Tests
apps/e2e/tests/backend/endpoints/api/v1/session-replays.test.ts
Adds extensive tests covering user/team filtering, duration and last-event windows, click_count_min (with eventual-consistency retries), invalid params, pagination and access restrictions.
Misc / Plumbing & Templates
packages/template/src/lib/stack-app/..., small adapters, test helpers
Forwards new filter fields through admin/client implementations and adds test helpers and wiring used by new UI/tests.

Sequence Diagram

sequenceDiagram
    participant User as Dashboard User
    participant Client as Page Client
    participant API as Backend API Route
    participant DB as PostgreSQL
    participant CH as ClickHouse

    User->>Client: Open filters / Apply filters
    Client->>Client: Build appliedFilters state
    Client->>API: GET /internal/session-replays?user_ids...&click_count_min...&cursor...

    rect rgba(100,150,200,0.5)
        API->>API: Parse & validate query params (durations, dates, non-negatives)
        alt click_count_min supplied
            API->>CH: loadClickQualifiedReplayIds(click_count_min)
            CH-->>API: [qualified_replay_ids]
            API->>API: Short-circuit to empty if none
        end
    end

    rect rgba(150,200,100,0.5)
        API->>DB: Query session_replays with tenancy, user/team filters, lastEventAt window, duration bounds, qualified_replay_ids, join project_user + email
        DB-->>API: [rows with embedded project_user + timestamps + counts]
    end

    rect rgba(200,100,150,0.5)
        API->>API: Compute chunk/event counts, map rows to items, build composite nextCursor
    end

    API-->>Client: { items: [...], nextCursor }
    Client->>Client: Update UI, show filter chips/badge
    Client-->>User: Render filtered replays
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • N2D4
  • Developing-Gamer

Poem

🐰 I hopped through queries, clipped a seed,

Filters sprout where click-counts lead,
Users picked, durations aligned,
Cursors stitch the time I find,
Replays bloom — a rabbit’s deed.

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description contains only a Loom video link and the template comment, lacking substantive details about the changes, rationale, testing, or implementation approach. Provide a detailed written description explaining the filtering features added, why they were implemented, how to test them, and any migration notes or breaking changes.
Docstring Coverage ⚠️ Warning Docstring coverage is 8.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title 'analytics replay filters' is concise and directly reflects the main change: adding comprehensive filtering capabilities to the session replays analytics feature.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch analytics-replay-filters

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.

@BilalG1 BilalG1 marked this pull request as ready for review February 19, 2026 03:24
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Feb 19, 2026

Greptile Summary

Added comprehensive filtering capabilities to session replay listing, enabling filtering by user, team, duration, last active time, and click count. The backend implementation optimizes query performance by pushing most filters to the database while handling duration filtering in memory with overfetch. Click count filtering queries ClickHouse analytics data and passes qualifying IDs to Postgres. The UI provides a clean filter dropdown with modal dialogs for each filter type and displays active filters as removable chips.

Confidence Score: 4/5

  • This PR is safe to merge with minor considerations around edge cases
  • The implementation is thorough with comprehensive validation, proper error handling, and extensive test coverage. The filtering logic correctly handles cursor-based pagination and intelligently optimizes queries. Duration filtering uses an overfetch strategy to handle in-memory filtering correctly. However, there's a potential edge case with pagination when duration filters are active that could cause hasMore to be incorrectly calculated if the overfetch limit is reached.
  • Pay attention to apps/backend/src/app/api/latest/internal/session-replays/route.tsx for the pagination logic with duration filters

Important Files Changed

Filename Overview
apps/backend/src/app/api/latest/internal/session-replays/route.tsx Added comprehensive filtering (user, team, duration, timestamp, click count) with proper validation, ClickHouse integration, and pagination optimization
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/replays/page-client.tsx Added filter UI with dialogs for user/team/duration/time/click filters, proper state management, and filter chip display
apps/dashboard/src/components/data-table/user-search-picker.tsx New reusable component for user search/selection with debounced search and paginated results
apps/e2e/tests/backend/endpoints/api/v1/session-replays.test.ts Comprehensive test coverage for all new filter parameters with positive and negative test cases

Sequence Diagram

sequenceDiagram
    participant UI as Dashboard UI
    participant API as Backend API
    participant PG as Postgres
    participant CH as ClickHouse

    UI->>UI: User applies filters (user/team/duration/clicks)
    UI->>API: GET /internal/session-replays?user_ids=...&click_count_min=...
    API->>API: Parse & validate filters
    
    alt Click count filter active
        API->>CH: Query events table for replays with N+ clicks
        CH-->>API: Return qualified replay IDs
    end
    
    API->>API: Build WHERE clause (user, team, time filters)
    
    alt Duration filter active
        API->>PG: Overfetch replays (5x limit)
    else No duration filter
        API->>PG: Fetch replays (limit + 1)
    end
    
    PG-->>API: Return session replays with user data
    
    alt Duration filter active
        API->>API: Filter by duration in memory
        API->>API: Slice to requested limit
    end
    
    API->>PG: Fetch chunk aggregates for page
    PG-->>API: Return chunk counts & event counts
    API-->>UI: Return filtered replays with pagination
Loading

Last reviewed commit: d7cd237

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

8 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Base automatically changed from analytics-anon-users to analytics-replays-event-markers February 19, 2026 03:33
Base automatically changed from analytics-replays-event-markers to dev February 19, 2026 03:33
Copy link
Copy Markdown
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

🧹 Nitpick comments (9)
apps/backend/src/app/api/latest/internal/session-replays/route.tsx (2)

13-16: Make CSV parsing use explicit empty-string checks.
Using !raw and filter(Boolean) violates the explicit null/undefined guideline.

🔧 Suggested change
-  if (!raw) return [];
-  return raw.split(",").map(s => s.trim()).filter(Boolean);
+  if (raw == null || raw === "") return [];
+  return raw.split(",").map(s => s.trim()).filter((s) => s.length > 0);

As per coding guidelines: Use explicit null/undefinedness checks instead of boolean checks, e.g., foo == null instead of !foo.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/backend/src/app/api/latest/internal/session-replays/route.tsx` around
lines 13 - 16, The parseCsvIds function uses truthy checks and filter(Boolean);
change to explicit null/undefined checks and explicit empty-string filtering: in
parseCsvIds, replace the if (!raw) with if (raw == null) and replace
.filter(Boolean) with .filter(s => s !== '') (or .filter(s => s.length > 0))
after trimming so only non-empty strings remain; keep the split and trim logic
and return type the same.

126-141: Avoid truthy checks for clickCountMin and clickQualifiedIds.
Use explicit !== null checks to keep intent clear.

🔧 Suggested change
-    const clickQualifiedIds = clickCountMin && clickCountMin > 0
+    const clickQualifiedIds = clickCountMin !== null && clickCountMin > 0
       ? await loadClickQualifiedReplayIds({
         projectId: auth.tenancy.project.id,
         branchId: auth.tenancy.branchId,
         clickCountMin,
       })
       : null;

-    if (clickQualifiedIds && clickQualifiedIds.length === 0) {
+    if (clickQualifiedIds !== null && clickQualifiedIds.length === 0) {

As per coding guidelines: Use explicit null/undefinedness checks instead of boolean checks, e.g., foo == null instead of !foo.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/backend/src/app/api/latest/internal/session-replays/route.tsx` around
lines 126 - 141, The current code uses truthy checks for clickCountMin and
clickQualifiedIds; change them to explicit null/undefined checks: call
loadClickQualifiedReplayIds only when clickCountMin !== null && clickCountMin
!== undefined && clickCountMin > 0 (or equivalently clickCountMin != null &&
clickCountMin > 0), and replace the if that short-circuits on clickQualifiedIds
with an explicit check like clickQualifiedIds !== null &&
clickQualifiedIds.length === 0 (or clickQualifiedIds != null &&
clickQualifiedIds.length === 0); reference variables/functions: clickCountMin,
clickQualifiedIds, loadClickQualifiedReplayIds, and auth.tenancy.project.id when
updating these conditions.
apps/e2e/tests/backend/endpoints/api/v1/session-replays.test.ts (2)

626-633: Avoid manual URL interpolation in listReplays.
Prefer urlString (if available) or a URL/URLSearchParams composition rather than interpolating query strings into the path.

As per coding guidelines: Use urlString``-template literals or encodeURIComponent()` instead of normal string interpolation for URLs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/e2e/tests/backend/endpoints/api/v1/session-replays.test.ts` around lines
626 - 633, The function listReplays currently builds the request URL by manually
interpolating the query string into the path; change it to construct the full
URL safely using a URL or urlString helper to avoid manual interpolation and
ensure proper encoding: use the existing URLSearchParams (params) to build the
query and then create a new URL with pathname "/api/v1/internal/session-replays"
and set search to params.toString(), or use the project's urlString template
helper if available, then pass that URL.toString() (or urlString result) into
niceBackendFetch along with the same options; update references to listReplays
and keep accessType "admin" unchanged.

635-964: Prefer inline snapshots for the new response assertions where feasible.
Inline snapshots would reduce repetitive expectations in the new filter tests.

As per coding guidelines: When writing tests, prefer .toMatchInlineSnapshot over other selectors, if possible.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/e2e/tests/backend/endpoints/api/v1/session-replays.test.ts` around lines
635 - 964, Replace repetitive field-by-field assertions in the new filter tests
(the it blocks titled "admin list session replays filters by user_ids",
"team_ids", "duration range", "last_event_at time range", "click_count_min", and
invalid filter params") with inline snapshots using Jest's
toMatchInlineSnapshot; for each call-site that currently does
expect(res.status).toBe(200) and multiple expect(res.body... ) checks (e.g.
after listReplays, uploadBatch, uploadEventBatch and the retry loop using
listReplays), replace the body assertions with a single
expect(res.body).toMatchInlineSnapshot(...) or a snapshot that uses Jest
asymmetric matchers (e.g. expect.any(String)/expect.any(Number)) to handle
dynamic ids/timestamps (references: listReplays, uploadBatch, uploadEventBatch,
the variables replayIdA/shortId/longId/userA/userB/teamId/segmentIdA), while
keeping the status code asserts; capture snapshots by running the tests to
generate the inline snapshots and ensure any unstable fields are normalized with
asymmetric matchers in the snapshot.
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/replays/page-client.tsx (3)

1385-1386: Add a dashboard deep link for the updated page.
Please include a deep link (e.g., http://localhost:<NEXT_PUBLIC_STACK_PORT_PREFIX>01/projects/-selector-/analytics/replays) in the PR description so reviewers can jump straight to the updated view.

Based on learnings: When making changes in the dashboard, provide users with a deep link to the dashboard page that was changed, typically in the form of http://localhost:<NEXT_PUBLIC_STACK_PORT_PREFIX>01/projects/-selector-/....

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/analytics/replays/page-client.tsx
around lines 1385 - 1386, Add a dashboard deep link to the PR description that
points reviewers to the updated view for the Session Replays page; include a URL
like
http://localhost:<NEXT_PUBLIC_STACK_PORT_PREFIX>01/projects/-selector-/analytics/replays
so reviewers can jump directly to the component rendered in
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/replays/page-client.tsx
(look for PageLayout title="Session Replays" and the surrounding PanelGroup) and
ensure the link uses the correct NEXT_PUBLIC_STACK_PORT_PREFIX placeholder for
local testing.

501-515: Prefer a Map for presets and make filter checks explicit.
This keeps the preset table aligned with the Map guideline and avoids truthy checks for filter values.

🔧 Suggested change
-      const presetMs: Record<string, number> = { "24h": 86_400_000, "7d": 604_800_000, "30d": 2_592_000_000 };
-      const lastActiveFromMillis = appliedFilters.lastActivePreset && presetMs[appliedFilters.lastActivePreset]
-        ? Date.now() - presetMs[appliedFilters.lastActivePreset]
-        : undefined;
+      const presetMs = new Map<Exclude<ReplayFilters["lastActivePreset"], "">, number>([
+        ["24h", 86_400_000],
+        ["7d", 604_800_000],
+        ["30d", 2_592_000_000],
+      ]);
+      const presetValue = appliedFilters.lastActivePreset === ""
+        ? undefined
+        : presetMs.get(appliedFilters.lastActivePreset);
+      const lastActiveFromMillis = presetValue == null ? undefined : Date.now() - presetValue;
...
-        userIds: appliedFilters.userId ? [appliedFilters.userId] : undefined,
-        teamIds: appliedFilters.teamId ? [appliedFilters.teamId] : undefined,
-        durationMsMin: appliedFilters.durationMinSeconds ? Number(appliedFilters.durationMinSeconds) * 1000 : undefined,
-        durationMsMax: appliedFilters.durationMaxSeconds ? Number(appliedFilters.durationMaxSeconds) * 1000 : undefined,
-        clickCountMin: appliedFilters.clickCountMin ? Number(appliedFilters.clickCountMin) : undefined,
+        userIds: appliedFilters.userId === "" ? undefined : [appliedFilters.userId],
+        teamIds: appliedFilters.teamId === "" ? undefined : [appliedFilters.teamId],
+        durationMsMin: appliedFilters.durationMinSeconds === "" ? undefined : Number(appliedFilters.durationMinSeconds) * 1000,
+        durationMsMax: appliedFilters.durationMaxSeconds === "" ? undefined : Number(appliedFilters.durationMaxSeconds) * 1000,
+        clickCountMin: appliedFilters.clickCountMin === "" ? undefined : Number(appliedFilters.clickCountMin),

As per coding guidelines: Use ES6 Maps instead of records/objects wherever possible; use explicit null/undefinedness checks instead of boolean checks, e.g., foo == null instead of !foo.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/analytics/replays/page-client.tsx
around lines 501 - 515, Replace the preset Record with an ES6 Map (e.g.,
presetMs Map) and compute lastActiveFromMillis by explicitly checking
appliedFilters.lastActivePreset against null/undefined and using
presetMs.get(appliedFilters.lastActivePreset) instead of truthy lookup; also
change the filter boolean checks passed to adminApp.listSessionReplays to
explicit null/undefined checks (e.g., appliedFilters.userId != null,
appliedFilters.teamId != null, appliedFilters.durationMinSeconds != null,
appliedFilters.durationMaxSeconds != null, appliedFilters.clickCountMin != null)
so you only convert values to arrays/numbers when they are not null/undefined
before calling listSessionReplays.

151-158: Use explicit empty-string checks in filtersActiveCount.
Make the empty-string handling explicit instead of relying on truthiness.

🔧 Suggested change
-  if (filters.userId) count += 1;
-  if (filters.teamId) count += 1;
-  if (filters.durationMinSeconds || filters.durationMaxSeconds) count += 1;
-  if (filters.lastActivePreset) count += 1;
-  if (filters.clickCountMin) count += 1;
+  if (filters.userId !== "") count += 1;
+  if (filters.teamId !== "") count += 1;
+  if (filters.durationMinSeconds !== "" || filters.durationMaxSeconds !== "") count += 1;
+  if (filters.lastActivePreset !== "") count += 1;
+  if (filters.clickCountMin !== "") count += 1;

As per coding guidelines: Use explicit null/undefinedness checks instead of boolean checks, e.g., foo == null instead of !foo.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/analytics/replays/page-client.tsx
around lines 151 - 158, The function filtersActiveCount relies on truthiness
which misclassifies empty strings and zero; update it to use explicit
null/undefined and empty-string checks: replace if (filters.userId) and if
(filters.teamId) and if (filters.lastActivePreset) with checks like
filters.userId != null && filters.userId !== '' (same for
teamId/lastActivePreset), replace the duration check with
(filters.durationMinSeconds != null || filters.durationMaxSeconds != null), and
replace if (filters.clickCountMin) with filters.clickCountMin != null so zero
values are counted correctly (refer to function filtersActiveCount and the
properties filters.userId, filters.teamId, filters.durationMinSeconds,
filters.durationMaxSeconds, filters.lastActivePreset, filters.clickCountMin).
packages/stack-shared/src/interface/admin-interface.ts (1)

790-792: Use explicit null/undefined checks for optional ID arrays.
Boolean checks can blur intent; make the null/undefined handling explicit.

🔧 Suggested change
-    if (params?.user_ids && params.user_ids.length > 0) qs.set("user_ids", params.user_ids.join(","));
-    if (params?.team_ids && params.team_ids.length > 0) qs.set("team_ids", params.team_ids.join(","));
+    if (params?.user_ids != null && params.user_ids.length > 0) qs.set("user_ids", params.user_ids.join(","));
+    if (params?.team_ids != null && params.team_ids.length > 0) qs.set("team_ids", params.team_ids.join(","));

As per coding guidelines: Use explicit null/undefinedness checks instead of boolean checks, e.g., foo == null instead of !foo.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/stack-shared/src/interface/admin-interface.ts` around lines 790 -
792, Replace the boolean presence checks for the optional ID arrays with
explicit null/undefined checks: change the conditionals that reference
params.user_ids and params.team_ids to use explicit checks like params.user_ids
!= null && params.user_ids.length > 0 (or params.user_ids == null for negation)
before calling qs.set, leaving the duration_ms_min typeof check as-is; update
the conditionals surrounding qs.set("user_ids", ...) and qs.set("team_ids", ...)
accordingly.
apps/dashboard/src/components/data-table/user-search-picker.tsx (1)

17-25: Use explicit empty-string checks for query fallback.
Relying on || undefined uses a boolean check; make the empty-string handling explicit.

🔧 Suggested change
-  const [filters, setFilters] = useState<Parameters<typeof stackAdminApp.listUsers>[0]>({
-    limit: PAGE_SIZE,
-    query: props.query || undefined,
-  });
+  const [filters, setFilters] = useState<Parameters<typeof stackAdminApp.listUsers>[0]>({
+    limit: PAGE_SIZE,
+    query: props.query === "" ? undefined : props.query,
+  });
...
-    setFilters({ limit: PAGE_SIZE, query: props.query || undefined });
+    setFilters({ limit: PAGE_SIZE, query: props.query === "" ? undefined : props.query });
...
-      query: props.query || undefined,
+      query: props.query === "" ? undefined : props.query,

As per coding guidelines: Use explicit null/undefinedness checks instead of boolean checks, e.g., foo == null instead of !foo.

Also applies to: 56-60

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/dashboard/src/components/data-table/user-search-picker.tsx` around lines
17 - 25, The current initialization and reset of filters uses the boolean
fallback "props.query || undefined" which treats empty string like undefined;
change these to explicit empty-string checks so empty query strings are
preserved or converted deliberately—for example, when creating the initial state
passed to useState (the filters variable typed from stackAdminApp.listUsers) and
in the useEffect that calls setFilters, replace "props.query || undefined" with
an explicit check such as "props.query === '' ? undefined : props.query" (apply
the same change to the second occurrence around lines 56–60) so the logic is
clear and doesn't rely on truthiness.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/dashboard/src/components/data-table/user-search-picker.tsx`:
- Around line 49-55: The onUpdate callback's parameter type currently uses `any`
for `globalFilters`; change its type to `unknown` to preserve type safety while
remaining pass-through. Update the signature in the `onUpdate` function (inside
the `useCallback`) so the options object declares `globalFilters: unknown`
instead of `any`, leaving `cursor`, `limit`, `sorting: SortingState`, and
`columnFilters: ColumnFiltersState` unchanged.

---

Nitpick comments:
In `@apps/backend/src/app/api/latest/internal/session-replays/route.tsx`:
- Around line 13-16: The parseCsvIds function uses truthy checks and
filter(Boolean); change to explicit null/undefined checks and explicit
empty-string filtering: in parseCsvIds, replace the if (!raw) with if (raw ==
null) and replace .filter(Boolean) with .filter(s => s !== '') (or .filter(s =>
s.length > 0)) after trimming so only non-empty strings remain; keep the split
and trim logic and return type the same.
- Around line 126-141: The current code uses truthy checks for clickCountMin and
clickQualifiedIds; change them to explicit null/undefined checks: call
loadClickQualifiedReplayIds only when clickCountMin !== null && clickCountMin
!== undefined && clickCountMin > 0 (or equivalently clickCountMin != null &&
clickCountMin > 0), and replace the if that short-circuits on clickQualifiedIds
with an explicit check like clickQualifiedIds !== null &&
clickQualifiedIds.length === 0 (or clickQualifiedIds != null &&
clickQualifiedIds.length === 0); reference variables/functions: clickCountMin,
clickQualifiedIds, loadClickQualifiedReplayIds, and auth.tenancy.project.id when
updating these conditions.

In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/analytics/replays/page-client.tsx:
- Around line 1385-1386: Add a dashboard deep link to the PR description that
points reviewers to the updated view for the Session Replays page; include a URL
like
http://localhost:<NEXT_PUBLIC_STACK_PORT_PREFIX>01/projects/-selector-/analytics/replays
so reviewers can jump directly to the component rendered in
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/replays/page-client.tsx
(look for PageLayout title="Session Replays" and the surrounding PanelGroup) and
ensure the link uses the correct NEXT_PUBLIC_STACK_PORT_PREFIX placeholder for
local testing.
- Around line 501-515: Replace the preset Record with an ES6 Map (e.g., presetMs
Map) and compute lastActiveFromMillis by explicitly checking
appliedFilters.lastActivePreset against null/undefined and using
presetMs.get(appliedFilters.lastActivePreset) instead of truthy lookup; also
change the filter boolean checks passed to adminApp.listSessionReplays to
explicit null/undefined checks (e.g., appliedFilters.userId != null,
appliedFilters.teamId != null, appliedFilters.durationMinSeconds != null,
appliedFilters.durationMaxSeconds != null, appliedFilters.clickCountMin != null)
so you only convert values to arrays/numbers when they are not null/undefined
before calling listSessionReplays.
- Around line 151-158: The function filtersActiveCount relies on truthiness
which misclassifies empty strings and zero; update it to use explicit
null/undefined and empty-string checks: replace if (filters.userId) and if
(filters.teamId) and if (filters.lastActivePreset) with checks like
filters.userId != null && filters.userId !== '' (same for
teamId/lastActivePreset), replace the duration check with
(filters.durationMinSeconds != null || filters.durationMaxSeconds != null), and
replace if (filters.clickCountMin) with filters.clickCountMin != null so zero
values are counted correctly (refer to function filtersActiveCount and the
properties filters.userId, filters.teamId, filters.durationMinSeconds,
filters.durationMaxSeconds, filters.lastActivePreset, filters.clickCountMin).

In `@apps/dashboard/src/components/data-table/user-search-picker.tsx`:
- Around line 17-25: The current initialization and reset of filters uses the
boolean fallback "props.query || undefined" which treats empty string like
undefined; change these to explicit empty-string checks so empty query strings
are preserved or converted deliberately—for example, when creating the initial
state passed to useState (the filters variable typed from
stackAdminApp.listUsers) and in the useEffect that calls setFilters, replace
"props.query || undefined" with an explicit check such as "props.query === '' ?
undefined : props.query" (apply the same change to the second occurrence around
lines 56–60) so the logic is clear and doesn't rely on truthiness.

In `@apps/e2e/tests/backend/endpoints/api/v1/session-replays.test.ts`:
- Around line 626-633: The function listReplays currently builds the request URL
by manually interpolating the query string into the path; change it to construct
the full URL safely using a URL or urlString helper to avoid manual
interpolation and ensure proper encoding: use the existing URLSearchParams
(params) to build the query and then create a new URL with pathname
"/api/v1/internal/session-replays" and set search to params.toString(), or use
the project's urlString template helper if available, then pass that
URL.toString() (or urlString result) into niceBackendFetch along with the same
options; update references to listReplays and keep accessType "admin" unchanged.
- Around line 635-964: Replace repetitive field-by-field assertions in the new
filter tests (the it blocks titled "admin list session replays filters by
user_ids", "team_ids", "duration range", "last_event_at time range",
"click_count_min", and invalid filter params") with inline snapshots using
Jest's toMatchInlineSnapshot; for each call-site that currently does
expect(res.status).toBe(200) and multiple expect(res.body... ) checks (e.g.
after listReplays, uploadBatch, uploadEventBatch and the retry loop using
listReplays), replace the body assertions with a single
expect(res.body).toMatchInlineSnapshot(...) or a snapshot that uses Jest
asymmetric matchers (e.g. expect.any(String)/expect.any(Number)) to handle
dynamic ids/timestamps (references: listReplays, uploadBatch, uploadEventBatch,
the variables replayIdA/shortId/longId/userA/userB/teamId/segmentIdA), while
keeping the status code asserts; capture snapshots by running the tests to
generate the inline snapshots and ensure any unstable fields are normalized with
asymmetric matchers in the snapshot.

In `@packages/stack-shared/src/interface/admin-interface.ts`:
- Around line 790-792: Replace the boolean presence checks for the optional ID
arrays with explicit null/undefined checks: change the conditionals that
reference params.user_ids and params.team_ids to use explicit checks like
params.user_ids != null && params.user_ids.length > 0 (or params.user_ids ==
null for negation) before calling qs.set, leaving the duration_ms_min typeof
check as-is; update the conditionals surrounding qs.set("user_ids", ...) and
qs.set("team_ids", ...) accordingly.

Copy link
Copy Markdown
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/backend/src/app/api/latest/internal/session-replays/route.tsx (1)

13-34: Several boolean checks on nullable values violate the explicit-null-check guideline

The three helper functions each use !raw to guard an undefined parameter:

// parseCsvIds, parseNonNegativeInt, parseMillis
if (!raw) return [];   // and null variants

raw is typed string | undefined, so the guard should be raw == null. The !raw form would also suppress an empty string "", but for parseNonNegativeInt that actually matters: Number("") === 0, which is a valid non-negative integer — silently returning null for ?duration_ms_min= hides what is arguably a client error. Use the explicit form:

-  if (!raw) return [];
+  if (raw == null) return [];
-  if (!raw) return null;
+  if (raw == null) return null;

The same pattern recurs for Date | null and number | null values inside the handler at lines 126, 131, 139, and 151–155 (lastEventAtFrom && lastEventAtTo, clickCountMin && clickCountMin > 0, clickQualifiedIds && ..., lastEventAtFrom || lastEventAtTo). All should use != null / !== null comparisons. As per coding guidelines: "Use explicit null/undefinedness checks instead of boolean checks, e.g., foo == null instead of !foo."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/backend/src/app/api/latest/internal/session-replays/route.tsx` around
lines 13 - 34, Change all boolean falsy guards on possibly empty
strings/nullable values to explicit null/undefined checks: in parseCsvIds,
parseNonNegativeInt, and parseMillis replace the if (!raw) guards with if (raw
== null) (or raw === undefined/null) so empty string "" is not treated as
absent; similarly, in the request handler replace all truthy checks like
lastEventAtFrom && lastEventAtTo, clickCountMin && clickCountMin > 0,
clickQualifiedIds && ..., and lastEventAtFrom || lastEventAtTo with explicit
null/undefined comparisons (e.g., lastEventAtFrom != null && lastEventAtTo !=
null, clickCountMin != null && clickCountMin > 0, clickQualifiedIds != null,
lastEventAtFrom != null || lastEventAtTo != null) to preserve empty-string/zero
semantics and follow the explicit-null-check guideline.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/backend/src/app/api/latest/internal/session-replays/route.tsx`:
- Around line 36-66: The ClickHouse query in loadClickQualifiedReplayIds is
unbounded and can return very large ID lists which yield memory spikes and
oversized Prisma IN(...) clauses; add a pragmatic cap to the ClickHouse query
(e.g., LIMIT 10000) and detect when the cap is hit (compare rows.length to the
cap) so the calling API can return a 400/validation error or a descriptive "too
many qualifying replays — refine your filter" response instead of passing the
full list to Prisma (see where clickQualifiedIds is used as id: { in:
clickQualifiedIds }). Optionally document the cap or make it configurable via a
parameter to loadClickQualifiedReplayIds so callers can adjust behavior.
- Around line 230-232: The pagination logic incorrectly returns next_cursor null
when page is empty but the DB fetch was exhausted; update the nextCursor
computation in the route so that if hasDurationFilter is true and page.length
=== 0 and sessions.length === fetchSize, nextCursor falls back to the last DB
row id (sessions[sessions.length - 1].id) instead of null; keep existing hasMore
calculation but change the ternary for nextCursor to check for page.length > 0
first, otherwise if hasDurationFilter && sessions.length === fetchSize return
the last session id, else null.
- Line 64: Replace the prohibited TypeScript "as" cast on the JSON result:
instead of using "result.json() as Array<{ session_replay_id: string }>", call
the ClickHouse ResultSet generic form result.json<Array<{ session_replay_id:
string }>>() to get typed rows; update the variable assignment for rows
accordingly (and optionally add zod validation if you need runtime schema
checks).

---

Nitpick comments:
In `@apps/backend/src/app/api/latest/internal/session-replays/route.tsx`:
- Around line 13-34: Change all boolean falsy guards on possibly empty
strings/nullable values to explicit null/undefined checks: in parseCsvIds,
parseNonNegativeInt, and parseMillis replace the if (!raw) guards with if (raw
== null) (or raw === undefined/null) so empty string "" is not treated as
absent; similarly, in the request handler replace all truthy checks like
lastEventAtFrom && lastEventAtTo, clickCountMin && clickCountMin > 0,
clickQualifiedIds && ..., and lastEventAtFrom || lastEventAtTo with explicit
null/undefined comparisons (e.g., lastEventAtFrom != null && lastEventAtTo !=
null, clickCountMin != null && clickCountMin > 0, clickQualifiedIds != null,
lastEventAtFrom != null || lastEventAtTo != null) to preserve empty-string/zero
semantics and follow the explicit-null-check guideline.

Copy link
Copy Markdown
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.

🧹 Nitpick comments (2)
apps/backend/src/app/api/latest/internal/session-replays/route.tsx (2)

215-215: Replace non-null assertion with ?? throwErr(...)

limit is clamped to ≥ 1 and page is only used here when hasMore is true (meaning rows.length > limit ≥ 1), so the assertion is logically safe — but the project guideline explicitly prefers ?? throwErr(...) with a descriptive message over !.

-    const nextCursor = hasMore ? page[page.length - 1]!.id : null;
+    const nextCursor = hasMore
+      ? (page[page.length - 1] ?? throwErr("Expected page to be non-empty when hasMore is true")).id
+      : null;

As per coding guidelines: "Prefer ?? throwErr(...) over non-null assertions with good error messages explicitly stating the assumption."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/backend/src/app/api/latest/internal/session-replays/route.tsx` at line
215, The line computing nextCursor uses a non-null assertion on page[page.length
- 1] (const nextCursor = hasMore ? page[page.length - 1]!.id : null;); replace
the `!` with a null-coalescing guard that calls throwErr(...) so we fail with a
clear message if the assumption is broken (e.g., use page[page.length - 1]?.id
?? throwErr("expected last page item when hasMore is true in nextCursor")).
Update the expression that sets nextCursor (referencing nextCursor, page and
hasMore) to use this pattern and include a descriptive error message about the
invariant.

129-129: Prefer explicit !== null checks over boolean coercion on nullable values

Three separate spots use implicit boolean truthiness on Date | null or number | null values, against the project's explicit-null-check guideline:

-    if (lastEventAtFrom && lastEventAtTo && lastEventAtFrom.getTime() > lastEventAtTo.getTime()) {
+    if (lastEventAtFrom !== null && lastEventAtTo !== null && lastEventAtFrom.getTime() > lastEventAtTo.getTime()) {
-    const clickQualifiedIds = clickCountMin && clickCountMin > 0
+    const clickQualifiedIds = clickCountMin !== null && clickCountMin > 0
-    if (clickQualifiedIds && clickQualifiedIds.length === 0) {
+    if (clickQualifiedIds !== null && clickQualifiedIds.length === 0) {

As per coding guidelines: "Unless very clearly equivalent from types, prefer explicit null/undefinedness checks over boolean checks, e.g., foo == null instead of !foo."

Also applies to: 134-134, 142-142

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/backend/src/app/api/latest/internal/session-replays/route.tsx` at line
129, The conditional checks currently rely on boolean coercion for nullable
values (e.g., lastEventAtFrom and lastEventAtTo); update those conditions to use
explicit null checks instead of truthiness. Replace expressions like "if
(lastEventAtFrom && lastEventAtTo && ...)" with explicit checks such as
"lastEventAtFrom !== null" and "lastEventAtTo !== null" (and similarly for the
other two occurrences flagged around lines 134 and 142) so every nullable Date
or number is compared explicitly to null (and to undefined where applicable)
before using .getTime() or other operations.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@apps/backend/src/app/api/latest/internal/session-replays/route.tsx`:
- Line 66: Replace the post-fetch type assertion with the fetch response
generic: instead of casting the result of result.json() to Array<{
session_replay_id: string }>, call result.json<Array<{ session_replay_id: string
}>>() so the type is inferred by the json<T>() overload; update the declaration
of rows (currently "const rows = await result.json() as Array<{
session_replay_id: string }>;") to await result.json<Array<{ session_replay_id:
string }>>() to remove the `as` cast and keep typesafe typing for the rows
variable.
- Around line 50-57: The query applies CLICK_FILTER_ID_CAP but doesn't indicate
when results were truncated; in the route handler that executes the query and
prepares the response (look for CLICK_FILTER_ID_CAP, rows, and the exported
GET/route function in route.tsx) detect if rows.length === CLICK_FILTER_ID_CAP
and surface that to the caller rather than silently truncating: either return a
400 with a clear message referencing the cap, or (preferred) add a field to the
JSON response such as capExceeded: true (and optionally include cap:
CLICK_FILTER_ID_CAP) so callers can detect overflow; update any response
Type/interface accordingly.

---

Nitpick comments:
In `@apps/backend/src/app/api/latest/internal/session-replays/route.tsx`:
- Line 215: The line computing nextCursor uses a non-null assertion on
page[page.length - 1] (const nextCursor = hasMore ? page[page.length - 1]!.id :
null;); replace the `!` with a null-coalescing guard that calls throwErr(...) so
we fail with a clear message if the assumption is broken (e.g., use
page[page.length - 1]?.id ?? throwErr("expected last page item when hasMore is
true in nextCursor")). Update the expression that sets nextCursor (referencing
nextCursor, page and hasMore) to use this pattern and include a descriptive
error message about the invariant.
- Line 129: The conditional checks currently rely on boolean coercion for
nullable values (e.g., lastEventAtFrom and lastEventAtTo); update those
conditions to use explicit null checks instead of truthiness. Replace
expressions like "if (lastEventAtFrom && lastEventAtTo && ...)" with explicit
checks such as "lastEventAtFrom !== null" and "lastEventAtTo !== null" (and
similarly for the other two occurrences flagged around lines 134 and 142) so
every nullable Date or number is compared explicitly to null (and to undefined
where applicable) before using .getTime() or other operations.

@BilalG1 BilalG1 requested a review from N2D4 February 19, 2026 19:43
@BilalG1 BilalG1 assigned N2D4 and unassigned BilalG1 Feb 19, 2026
Copy link
Copy Markdown
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 (3)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/replays/page-client.tsx (1)

1473-1512: User filter dialog bypasses draft state and applies immediately — inconsistent UX pattern

The team filter dialog (lines 1514–1553) follows the same pattern, but both differ from the Duration and Clicks dialogs that use draftFilters + an explicit "Apply" button. Selecting a user/team immediately commits appliedFilters, with no way to cancel before the filter is applied.

This is not a correctness bug, but the inconsistency may surprise users. If immediate-apply is intentional (a picker selection is final), consider at least adding a "Cancel" escape that also clears the selection (similar to the "Clear" button already present), or document the design intent with a comment.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/analytics/replays/page-client.tsx
around lines 1473 - 1512, The User/team picker dialogs currently call
setAppliedFilters directly on selection (via UserSearchPicker and the onClick
that calls setAppliedFilters and setActiveFilterDialog), causing immediate
commits; change this to mirror the Duration/Clicks pattern by writing selections
to draftFilters instead and only commit to appliedFilters when the dialog's
explicit "Apply" button is pressed (keep the existing Clear behavior to reset
draftFilters), or if immediate apply is intended make that explicit with a
comment and add a "Cancel" action that closes the dialog without changing
appliedFilters; update references to setAppliedFilters, setActiveFilterDialog,
UserSearchPicker, appliedFilters and draftFilters accordingly.
apps/backend/src/app/api/latest/internal/session-replays/route.tsx (1)

137-151: Prefer explicit != null checks over boolean truthiness on Date and number | null

Lines 140 and 145 use truthy/falsy checks on values where explicit null guards are clearer per coding guidelines.

  • new Date(0) is truthy, so the Date check works, but it's non-obvious.
  • clickCountMin is number | null; clickCountMin && clickCountMin > 0 conflates null and 0 via falsy collapse.
♻️ Proposed fix
- if (lastEventAtFrom && lastEventAtTo && lastEventAtFrom.getTime() > lastEventAtTo.getTime()) {
+ if (lastEventAtFrom != null && lastEventAtTo != null && lastEventAtFrom.getTime() > lastEventAtTo.getTime()) {

- const clickQualifiedIds = clickCountMin && clickCountMin > 0
+ const clickQualifiedIds = clickCountMin != null && clickCountMin > 0

As per coding guidelines: "Unless very clearly equivalent from types, prefer explicit null/undefinedness checks over boolean checks, e.g., foo == null instead of !foo."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/backend/src/app/api/latest/internal/session-replays/route.tsx` around
lines 137 - 151, Replace truthy/falsy checks with explicit null checks: for the
date comparison use explicit guards (check lastEventAtFrom != null &&
lastEventAtTo != null before calling getTime()), and for duration use
durationMsMin != null && durationMsMax != null; for the click filter compute
clickQualifiedIds only when clickCountMin != null && clickCountMin > 0 (instead
of clickCountMin && clickCountMin > 0). Update the conditions around
durationMsMin/durationMsMax, lastEventAtFrom/lastEventAtTo, and the
clickQualifiedIds ternary that calls loadClickQualifiedReplayIds so they all use
explicit != null checks and an explicit numeric > 0 check for clickCountMin.
apps/e2e/tests/backend/endpoints/api/v1/session-replays.test.ts (1)

839-1384: Good coverage of all new filter parameters and error paths

The new filter tests are well-structured: they cover all seven new query parameters, validate AND semantics, test boundary/invalid inputs with inline snapshots, and use Project.createAndSwitch for isolation. The ClickHouse retry loop (lines 1116–1124) correctly accounts for eventual consistency.

One minor coverage gap: no test exercises cursor pagination while a filter is simultaneously active (e.g., user_ids + cursor). The SQL composes both correctly, but a combined test would catch future regressions. Consider adding a short test that uploads two sessions for the same user, paginates with limit=1 and user_ids=<that user>, and verifies both pages contain only that user's sessions.

Would you like me to draft that pagination+filter combination test?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/e2e/tests/backend/endpoints/api/v1/session-replays.test.ts` around lines
839 - 1384, Add a test that verifies cursor pagination works when a filter is
active: sign in a user via Auth.Otp.signIn, upload two sessions for that user
with uploadBatch (use randomUUID for ids and bumpEmailAddress between users if
needed), call listReplays with { user_ids: <userId>, limit: "1" } to get the
first page, capture first.body.pagination.next_cursor, then call listReplays
again with { user_ids: <userId>, limit: "1", cursor: next_cursor } and assert
both pages return items with project_user.id equal to the signed-in user's id
and the two replay ids are distinct; also assert pagination behaves (next_cursor
string for first page, eventually null when no more pages).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/analytics/replays/page-client.tsx:
- Around line 152-160: filtersActiveCount currently uses truthy checks and
counts string "0" as active; align it with loadPage by treating zero as a no-op.
Update filtersActiveCount (function filtersActiveCount) to only increment for
numeric filters when Number(value) > 0 (e.g., for clickCountMin,
durationMinSeconds, durationMaxSeconds) and keep existing checks for
userId/teamId/lastActivePreset; also update loadPage to mirror this behavior by
passing clickCountMin/durationMinSeconds/durationMaxSeconds as Number(value) > 0
? Number(value) : undefined so zero is not sent to the API.
- Around line 517-524: Remove the side-effect inside the setRecordings
functional updater: do not call setSelectedRecordingId or mutate
hasAutoSelectedRef from within the setRecordings((prev) => { ... }) callback;
instead just return the new items. Also delete the redundant hasAutoSelectedRef
mutation in the filter-reset effect and then remove the hasAutoSelectedRef ref
entirely if it has no other usages, since the dedicated useEffect that handles
auto-selection already covers initial load, filter-reset, and load-more cases.

---

Duplicate comments:
In `@apps/backend/src/app/api/latest/internal/session-replays/route.tsx`:
- Line 77: Replace the prohibited TypeScript cast on result.json() by using the
generic form: change the line assigning rows to call result.json with a type
parameter (e.g., const rows = await result.json<Array<{ session_replay_id:
string }>>(); or alternatively annotate rows: const rows: Array<{
session_replay_id: string }> = await result.json();), so the JSON parse uses the
generic type instead of an "as" cast in route.tsx where rows and result.json()
are used.
- Around line 47-79: In loadClickQualifiedReplayIds detect when the ClickHouse
result was truncated by checking if rows.length === CLICK_FILTER_ID_CAP
(referencing CLICK_FILTER_ID_CAP and loadClickQualifiedReplayIds) and surface
that to the caller instead of returning a silently truncated list: when cap is
hit throw a descriptive error (or a typed error the route handler maps to a 400)
indicating the query hit the ID cap so the client can return a 400 user-facing
response; ensure the calling code that currently short-circuits on zero matches
(lines around the caller) handles this thrown error and maps it to a 400
response.

---

Nitpick comments:
In `@apps/backend/src/app/api/latest/internal/session-replays/route.tsx`:
- Around line 137-151: Replace truthy/falsy checks with explicit null checks:
for the date comparison use explicit guards (check lastEventAtFrom != null &&
lastEventAtTo != null before calling getTime()), and for duration use
durationMsMin != null && durationMsMax != null; for the click filter compute
clickQualifiedIds only when clickCountMin != null && clickCountMin > 0 (instead
of clickCountMin && clickCountMin > 0). Update the conditions around
durationMsMin/durationMsMax, lastEventAtFrom/lastEventAtTo, and the
clickQualifiedIds ternary that calls loadClickQualifiedReplayIds so they all use
explicit != null checks and an explicit numeric > 0 check for clickCountMin.

In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/analytics/replays/page-client.tsx:
- Around line 1473-1512: The User/team picker dialogs currently call
setAppliedFilters directly on selection (via UserSearchPicker and the onClick
that calls setAppliedFilters and setActiveFilterDialog), causing immediate
commits; change this to mirror the Duration/Clicks pattern by writing selections
to draftFilters instead and only commit to appliedFilters when the dialog's
explicit "Apply" button is pressed (keep the existing Clear behavior to reset
draftFilters), or if immediate apply is intended make that explicit with a
comment and add a "Cancel" action that closes the dialog without changing
appliedFilters; update references to setAppliedFilters, setActiveFilterDialog,
UserSearchPicker, appliedFilters and draftFilters accordingly.

In `@apps/e2e/tests/backend/endpoints/api/v1/session-replays.test.ts`:
- Around line 839-1384: Add a test that verifies cursor pagination works when a
filter is active: sign in a user via Auth.Otp.signIn, upload two sessions for
that user with uploadBatch (use randomUUID for ids and bumpEmailAddress between
users if needed), call listReplays with { user_ids: <userId>, limit: "1" } to
get the first page, capture first.body.pagination.next_cursor, then call
listReplays again with { user_ids: <userId>, limit: "1", cursor: next_cursor }
and assert both pages return items with project_user.id equal to the signed-in
user's id and the two replay ids are distinct; also assert pagination behaves
(next_cursor string for first page, eventually null when no more pages).

Copy link
Copy Markdown
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 (1)
apps/backend/src/app/api/latest/internal/session-replays/route.tsx (1)

140-165: Prefer explicit != null / == null guards over boolean coercion on nullable types

Four places use truthiness checks on Date | null, number | null, or string[] | null values, which violates the codebase guideline:

  • Line 140: if (lastEventAtFrom && lastEventAtTo && ...) — both are Date | null
  • Line 145: clickCountMin && clickCountMin > 0number | null; clickCountMin = 0 is legitimately falsy but the intent is a null-check, not a zero-check
  • Line 153: if (clickQualifiedIds && ...)string[] | null
  • Line 164: if (cursorId)string | undefined
♻️ Suggested changes
-    if (lastEventAtFrom && lastEventAtTo && lastEventAtFrom.getTime() > lastEventAtTo.getTime()) {
+    if (lastEventAtFrom != null && lastEventAtTo != null && lastEventAtFrom.getTime() > lastEventAtTo.getTime()) {

-    const clickQualifiedIds = clickCountMin && clickCountMin > 0
+    const clickQualifiedIds = clickCountMin != null && clickCountMin > 0

-    if (clickQualifiedIds && clickQualifiedIds.length === 0) {
+    if (clickQualifiedIds != null && clickQualifiedIds.length === 0) {

-    if (cursorId) {
+    if (cursorId != null) {

As per coding guidelines: "Unless very clearly equivalent from types, prefer explicit null/undefinedness checks over boolean checks, e.g., foo == null instead of !foo."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/backend/src/app/api/latest/internal/session-replays/route.tsx` around
lines 140 - 165, Summary: Replace truthy/falsy checks with explicit
null/undefined guards for nullable variables to follow codebase guidelines. Fix:
change the condition at the start to use explicit null checks (if
(lastEventAtFrom != null && lastEventAtTo != null && lastEventAtFrom.getTime() >
lastEventAtTo.getTime()) ...), change the click filter check to (clickCountMin
!= null && clickCountMin > 0) before calling loadClickQualifiedReplayIds, change
the empty-result guard to (clickQualifiedIds != null && clickQualifiedIds.length
=== 0) and change the cursor existence check to (cursorId != null) before using
prisma.sessionReplay.findUnique to populate cursorPivot; keep references to
loadClickQualifiedReplayIds, clickQualifiedIds, clickCountMin, lastEventAtFrom,
lastEventAtTo, cursorId, cursorPivot and prisma.sessionReplay.findUnique so the
edits are easy to locate.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/backend/src/app/api/latest/internal/session-replays/route.tsx`:
- Around line 216-219: The Prisma SQL fragment uses the separate variable
cursorId (type string | undefined) which is not type-safe when cursorPivot is
truthy; change the interpolation to use cursorPivot.id (which is string) inside
the Prisma.sql block so the comparison sr."id" < ${cursorPivot.id} is type-safe.
Update the conditional branch that builds the Prisma.sql snippet (the block
referencing cursorPivot and sr."lastEventAt") to replace cursorId with
cursorPivot.id and ensure cursorPivot is non-null before accessing .id.
- Line 226: The current use of a non-null assertion in nextCursor
(page[page.length - 1]!.id) should be replaced with a null-safe access combined
with the codebase's throwErr pattern; when hasMore is true, compute nextCursor
using a safe lookup (e.g., use page.at(-1) or page[page.length - 1] with
optional chaining) and append "?? throwErr(...)" with a clear message like
"Expected non-empty page when hasMore is true" so that nextCursor, hasMore, page
and throwErr are referenced and the assumption is enforced without using "!".

---

Duplicate comments:
In `@apps/backend/src/app/api/latest/internal/session-replays/route.tsx`:
- Around line 47-79: The query now uses CLICK_FILTER_ID_CAP but doesn't indicate
when results were truncated; update loadClickQualifiedReplayIds to detect a
cap-hit by checking whether rows.length === CLICK_FILTER_ID_CAP after parsing
result.json() and propagate that to callers (e.g., change the return shape from
Promise<string[]> to Promise<{ ids: string[]; capped: boolean }>, or throw a
specific error) so callers of loadClickQualifiedReplayIds can detect incomplete
results; reference the function loadClickQualifiedReplayIds, the
CLICK_FILTER_ID_CAP constant, the result.json() parsing and rows.map(...) when
implementing this change.
- Line 77: Replace the prohibited "as" cast on result.json() by using the
generic form of the method: call result.json with the type parameter for Array<{
session_replay_id: string }> so the returned value assigned to rows is properly
typed (replace the usage of result.json() as Array<{ session_replay_id: string
}> and update the declaration of rows accordingly); locate this in the
session-replays route where the variable rows is assigned from result.json() and
use the ResultSet's generic json<T>() method instead.

---

Nitpick comments:
In `@apps/backend/src/app/api/latest/internal/session-replays/route.tsx`:
- Around line 140-165: Summary: Replace truthy/falsy checks with explicit
null/undefined guards for nullable variables to follow codebase guidelines. Fix:
change the condition at the start to use explicit null checks (if
(lastEventAtFrom != null && lastEventAtTo != null && lastEventAtFrom.getTime() >
lastEventAtTo.getTime()) ...), change the click filter check to (clickCountMin
!= null && clickCountMin > 0) before calling loadClickQualifiedReplayIds, change
the empty-result guard to (clickQualifiedIds != null && clickQualifiedIds.length
=== 0) and change the cursor existence check to (cursorId != null) before using
prisma.sessionReplay.findUnique to populate cursorPivot; keep references to
loadClickQualifiedReplayIds, clickQualifiedIds, clickCountMin, lastEventAtFrom,
lastEventAtTo, cursorId, cursorPivot and prisma.sessionReplay.findUnique so the
edits are easy to locate.

@github-actions github-actions bot assigned BilalG1 and unassigned N2D4 Feb 24, 2026
@BilalG1 BilalG1 merged commit 09aa757 into dev Feb 24, 2026
26 of 28 checks passed
@BilalG1 BilalG1 deleted the analytics-replay-filters branch February 24, 2026 21:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants