Skip to content

perf fix sciweave#3

Merged
hubsmoke merged 6 commits into
developfrom
perf-sciweave
Mar 13, 2026
Merged

perf fix sciweave#3
hubsmoke merged 6 commits into
developfrom
perf-sciweave

Conversation

@hubsmoke

@hubsmoke hubsmoke commented Sep 11, 2025

Copy link
Copy Markdown
Member

Description of the Problem / Feature

Explanation of the solution

Instructions on making this work

Describe environment variable changes

Preview of changes

Summary by CodeRabbit

  • New Features

    • Added POST support to analytics endpoints with replica-friendly gating.
  • Bug Fixes

    • Improved error handling and runtime validation across analytics and UI views.
    • Analytics CSV endpoints now return downloadable CSVs with clearer error responses and a max-duration guard.
    • Tightened login form enable/disable logic and adjusted submit button label flow.
  • Documentation

    • README switched to Yarn-based setup instructions.
  • Chores

    • Added .nvmrc (Node 22.15) and updated example environment declarations (removed one variable and reordered keys).

@coderabbitai

coderabbitai Bot commented Sep 11, 2025

Copy link
Copy Markdown

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

Removed DESCI_RESEARCH_CONNECTION_DATABASE_URL from .env.example; added .nvmrc; switched README setup to Yarn; refactored sciweave analytics routes to use internal handleRequest and add replica-friendly POST(?get=true) paths; rewrote devices aggregation; added defensive JSON checks and UI tweaks; improved CSV download handling with maxDuration.

Changes

Cohort / File(s) Summary
Env & Tooling
/.env.example, /.nvmrc, /README.md
Removed DESCI_RESEARCH_CONNECTION_DATABASE_URL and reordered SCIWEAVE_SUPABASE_SERVICE_KEY in .env.example; added .nvmrc pinning Node 22.15; replaced npm instructions with Yarn in README.
API: Sciweave Analytics Routes
src/app/api/sciweave-analytics/chats/route.ts, src/app/api/sciweave-analytics/devices/route.ts, src/app/api/sciweave-analytics/sessions/route.ts, src/app/api/sciweave-analytics/users/route.ts
Extracted core logic into internal handleRequest; GET delegates to it; added POST that requires ?get=true or returns 400 for replica compatibility. devices aggregation rewritten to use SUM over boolean expressions with clarified OS/device predicates.
Frontend Analytics
src/app/(auth)/metrics/sciweave/page.tsx, src/components/molecules/DesciResearchAnalytics.tsx
Added defensive fetch/response handling and runtime guards to ensure parsed JSON is an array and userSessions is an array; log and return empty arrays on invalid payloads to avoid runtime errors.
Downloads / Reports
src/app/api/download/route.ts, src/app/api/downloadAnalyticsReport/route.ts
Added export const maxDuration = 300; replaced returning raw fetch responses with explicit error checks and return of a Response setting Content-Type: text/csv and Content-Disposition: attachment; filename="report.csv" or JSON error on failure.
Auth / Login UI
src/app/(unauth)/login/page.tsx, src/components/molecules/LoginForm.tsx
Tightened login form disabled logic to use truthy check (!!state?.user); changed submit button label from "Send code" to "Verify code" when email present and no pending status.
Misc small changes
src/app/actions.tsx, src/app/(auth)/metrics/sciweave/page.tsx (getUserSessionsAnalytics)
Switched cookie domain gating to use NEXT_ENV === "production" and added defensive handling for fetch responses (check res.ok, ensure parsed JSON is an array).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Client
  participant Route as API Route (GET/POST)
  participant Handler as handleRequest(...)
  participant DB as Database

  Client->>Route: GET /api/sciweave-analytics/{chats|devices|sessions|users}?start&end
  Route->>Handler: delegate(request)
  Handler->>DB: Query (start/end, aggregations)
  DB-->>Handler: Rows
  Handler-->>Route: JSON
  Route-->>Client: 200 OK

  rect rgba(230,245,255,0.5)
  note over Client,Route: Replica-compatible POST path
  Client->>Route: POST ...?get=true
  Route->>Handler: delegate(request)
  Handler->>DB: Same query
  DB-->>Handler: Rows
  Handler-->>Route: JSON
  Route-->>Client: 200 OK
  end

  Client->>Route: POST ... (no ?get=true)
  Route-->>Client: 400 Bad Request (requires ?get=true)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

I hop through envs and queries bright,
I pin Node, tidy handlers right.
POSTs now mirror GETs with care,
CSVs wrapped up for download fare.
A rabbit cheers — analytics light! 🐇

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'perf fix sciweave' is vague and overly generic, failing to clearly summarize the main changes in the changeset. Replace with a more specific title that captures the core change, such as 'Fix auth cookie domain handling for Vercel preview deployments' or 'Use NEXT_ENV instead of NODE_ENV for cookie domain selection'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ 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 perf-sciweave
📝 Coding Plan
  • Generate coding plan for human review comments

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (8)
src/components/molecules/DesciResearchAnalytics.tsx (3)

124-142: Fix 0% trend handling and negative sign display.

0 currently shows “No trend…”, and negatives render as “-3% down”. Treat 0 as valid and show absolute value for “down”.

-                {props.trend ? (
+                {props.trend !== undefined && props.trend !== null ? (
                   <>
-                    {props.trend > 0 ? (
+                    {props.trend > 0 ? (
                       <>
                         Trending up by{" "}
                         <span className="text-green-500">{props.trend}%</span>{" "}
                         <TrendingUp className="h-4 w-4" />
                       </>
                     ) : (
                       <>
                         Trending down by{" "}
-                        <span className="text-red-500">{props.trend}%</span>{" "}
+                        <span className="text-red-500">{Math.abs(props.trend)}%</span>{" "}
                         <TrendingDown className="h-4 w-4" />
                       </>
                     )}
                   </>
                 ) : (
                   "No trend data available"
                 )}

195-205: Fix date parsing bug in devices chart (Invalid Date).

You pre-format date to "dd MMM", then try new Date(value) in tick/tooltip formatters → “Invalid Date”. Keep Date objects in data and format only in formatters.

-  const chartData = useMemo(() => {
-    return props.data.map((item) => ({
-      date: formatDate(item.date, "dd MMM"),
-      mobile: item.mobile,
-      tablet: item.tablet,
-      mac: item.mac,
-      windows: item.windows,
-      otherDesktops: item.otherDesktops,
-      unknown: item.unknown,
-    }));
-  }, [props.data]);
+  const chartData = useMemo(() => {
+    return props.data.map((item) => ({
+      date: item.date, // keep as Date
+      mobile: item.mobile,
+      tablet: item.tablet,
+      mac: item.mac,
+      windows: item.windows,
+      otherDesktops: item.otherDesktops,
+      unknown: item.unknown,
+    }));
+  }, [props.data]);
-            <XAxis
+            <XAxis
               dataKey="date"
               tickLine={false}
               axisLine={false}
               tickMargin={8}
               minTickGap={32}
-              tickFormatter={(value) => {
-                const date = new Date(value);
-                return date.toLocaleDateString("en-US", {
-                  month: "short",
-                  day: "numeric",
-                });
-              }}
+              tickFormatter={(value: Date) =>
+                new Date(value).toLocaleDateString("en-US", {
+                  month: "short",
+                  day: "numeric",
+                })
+              }
             />
-                  labelFormatter={(value) => {
-                    return new Date(value).toLocaleDateString("en-US", {
-                      month: "short",
-                      day: "numeric",
-                    });
-                  }}
+                  labelFormatter={(value: Date) =>
+                    new Date(value).toLocaleDateString("en-US", {
+                      month: "short",
+                      day: "numeric",
+                    })
+                  }

Also applies to: 308-315, 320-326


418-444: Guard trend calculations against empty arrays and zero baselines.

Current code will throw on empty data and can divide by zero.

-  const trends = useMemo(() => {
-    const chatsTrend =
-      (Number(chats[chats.length - 1].value) - Number(chats[0].value)) /
-      Number(chats[0].value);
-    const uniqueUsersTrend =
-      (Number(uniqueUsers[uniqueUsers.length - 1].value) -
-        Number(uniqueUsers[0].value)) /
-      Number(uniqueUsers[0].value);
-    const userSessionsTrend =
-      (Number(userSessions[userSessions.length - 1].sessionCount) -
-        Number(userSessions[0].sessionCount)) /
-      Number(userSessions[0].sessionCount);
-    const userSessionsDurationTrend =
-      (Number(
-        userSessionsDurationData[userSessionsDurationData.length - 1].value
-      ) -
-        Number(userSessionsDurationData[0].value)) /
-      Number(userSessionsDurationData[0].value);
-
-    return {
-      chats: Math.round(chatsTrend * 100),
-      uniqueUsers: Math.round(uniqueUsersTrend * 100),
-      userSessions: Math.round(userSessionsTrend * 100),
-      userSessionsDuration: Math.round(userSessionsDurationTrend * 100),
-    };
-  }, [chats, uniqueUsers, userSessions, userSessionsDurationData]);
+  const trends = useMemo(() => {
+    const pct = (end: number, start: number) =>
+      start === 0 ? 0 : Math.round(((end - start) / start) * 100);
+
+    const safeFirst = <T>(arr: T[]) => (arr.length ? arr[0] : undefined);
+    const safeLast = <T>(arr: T[]) => (arr.length ? arr[arr.length - 1] : undefined);
+
+    const c0 = safeFirst(chats)?.value;
+    const c1 = safeLast(chats)?.value;
+    const u0 = safeFirst(uniqueUsers)?.value;
+    const u1 = safeLast(uniqueUsers)?.value;
+    const s0 = safeFirst(userSessions)?.sessionCount;
+    const s1 = safeLast(userSessions)?.sessionCount;
+    const d0 = safeFirst(userSessionsDurationData)?.value;
+    const d1 = safeLast(userSessionsDurationData)?.value;
+
+    return {
+      chats: c0 !== undefined && c1 !== undefined ? pct(Number(c1), Number(c0)) : 0,
+      uniqueUsers: u0 !== undefined && u1 !== undefined ? pct(Number(u1), Number(u0)) : 0,
+      userSessions: s0 !== undefined && s1 !== undefined ? pct(Number(s1), Number(s0)) : 0,
+      userSessionsDuration: d0 !== undefined && d1 !== undefined ? pct(Number(d1), Number(d0)) : 0,
+    };
+  }, [chats, uniqueUsers, userSessions, userSessionsDurationData]);
src/app/(auth)/metrics/sciweave/page.tsx (1)

133-135: Align client-side date range with server’s exclusive end boundary.

Server changed to created_at < $2. Use startOfDay(next day) instead of endOfDay(current), to include the selected “to” day completely and avoid microsecond edge cases.

-  const normalizedFrom = startOfDay(fromDate, { in: tz("UTC") }).toISOString();
-  const normalizedTo = endOfDay(toDate, { in: tz("UTC") }).toISOString();
+  const normalizedFrom = startOfDay(fromDate, { in: tz("UTC") }).toISOString();
+  const normalizedTo = startOfDay(addDays(toDate, 1), { in: tz("UTC") }).toISOString();

Add missing import:

// at top
import { startOfDay, subDays, addDays } from "date-fns";
src/app/api/sciweave-analytics/chats/route.ts (1)

19-41: Prevent connection leaks; use pool.query and cast counts.

client.release() is skipped on thrown queries → leaked connections. Also COUNT(*) returns text in pg; cast to integer.

-    const client = await pool.connect();
-    const result = await client.query(
+    const result = await pool.query(
       `
       SELECT
-        DATE_TRUNC($3, created_at) AS DATE,
-        COUNT(*) AS value
+        DATE_TRUNC($3, created_at) AS date,
+        COUNT(*)::integer AS value
     FROM
         public.search_logs
     WHERE
         thread_id IS NULL
         AND created_at >= $1
         AND created_at < $2
         ${IS_PROD ? "AND username NOT LIKE '%@desci.com'" : ""}
     GROUP BY
-        DATE
+        date
     ORDER BY
-        DATE
+        date
       `,
       [from, to, interval]
     );
-    client.release();
-    const data = result.rows as { day: string; value: number }[];
+    const data = result.rows as { date: string; value: number }[];
src/app/api/sciweave-analytics/users/route.ts (1)

19-41: Prevent connection leaks; use pool.query and cast counts.

Same leak risk and text-return issue as chats.

-    const client = await pool.connect();
-    const result = await client.query(
+    const result = await pool.query(
       `
-     SELECT
-        DATE_TRUNC($3, created_at) AS DATE,
-        COUNT(DISTINCT username) AS value
+     SELECT
+        DATE_TRUNC($3, created_at) AS date,
+        COUNT(DISTINCT username)::integer AS value
     FROM
         public.search_logs
     WHERE
         thread_id IS NULL
         AND created_at >= $1
         AND created_at < $2
         ${IS_PROD ? "AND username NOT LIKE '%@desci.com'" : ""}
     GROUP BY
-        DATE
+        date
     ORDER BY
-        DATE
+        date
       `,
       [from, to, interval]
     );
-    client.release();
-    const data = result.rows as { day: string; value: number }[];
+    const data = result.rows as { date: string; value: number }[];
src/app/api/sciweave-analytics/sessions/route.ts (1)

19-28: Prevent connection leaks; use pool.query.

Swap to pool.query to avoid leaking clients on errors.

-    const client = await pool.connect();
-    const result = await client.query(
+    const result = await pool.query(
       `
@@
-      SELECT
-           session_value as DATE,
-          COUNT(*)::integer AS "sessionCount",
-          ROUND(AVG(session_duration_sec), 2)::integer AS "durationInSeconds"
+      SELECT
+          session_value AS date,
+          COUNT(*)::integer AS "sessionCount",
+          ROUND(AVG(session_duration_sec))::integer AS "durationInSeconds"
@@
-    client.release();
-    const data = result.rows as {
-      date: Date;
+    const data = result.rows as {
+      date: Date;
       sessionCount: number;
       durationInSeconds: number;
     }[];

Also applies to: 106-118, 122-128

src/app/api/sciweave-analytics/devices/route.ts (1)

19-21: Always release the PG client in finally (prevent connection leaks).

If anything throws between connect and release, the client is never returned to the pool.

-    const client = await pool.connect();
-    const result = await client.query(
+    const client = await pool.connect();
+    let result;
+    try {
+      result = await client.query(
         `
         ...
-      [from, to, interval]
-    );
-    client.release();
+      [from, to, interval]
+      );
+    } finally {
+      client.release();
+    }

Also applies to: 39-39

🧹 Nitpick comments (12)
README.md (1)

6-8: Mention .nvmrc usage to avoid version drift.

Since you pinned Node, tell devs to run nvm use before yarn to ensure matching runtime.

 ```bash
-yarn
-yarn dev
+nvm use
+yarn
+yarn dev

</blockquote></details>
<details>
<summary>src/components/molecules/DesciResearchAnalytics.tsx (1)</summary><blockquote>

`554-555`: **Grammar nit: “each sessions” → “each session”.**


```diff
-        description="Analysis of devices used in each sessions for the selected period"
+        description="Analysis of devices used in each session for the selected period"
src/app/(auth)/metrics/sciweave/page.tsx (2)

34-45: Make fetches consistent and resilient (match sessions handler).

Mirror the robust checks you added to getUserSessionsAnalytics: gate on res.ok and verify array payloads.

-  const res = await fetch(
+  const res = await fetch(
     `${process.env.NEXT_PUBLIC_BASE_URL}/api/sciweave-analytics/chats?${params.toString()}`,
     { next: { revalidate: 3600 } }
   );
-  return res.json() as Promise<DataItem[]>;
+  if (!res.ok) {
+    console.error("Failed to fetch chats:", res.status, res.statusText);
+    return [];
+  }
+  const data = await res.json();
+  return Array.isArray(data) ? (data as DataItem[]) : [];
-  const res = await fetch(
+  const res = await fetch(
     `${process.env.NEXT_PUBLIC_BASE_URL}/api/sciweave-analytics/users?${params.toString()}`,
     { next: { revalidate: 3600 } }
   );
-  return res.json() as Promise<DataItem[]>;
+  if (!res.ok) {
+    console.error("Failed to fetch unique users:", res.status, res.statusText);
+    return [];
+  }
+  const data = await res.json();
+  return Array.isArray(data) ? (data as DataItem[]) : [];
-  const res = await fetch(
+  const res = await fetch(
     `${process.env.NEXT_PUBLIC_BASE_URL}/api/sciweave-analytics/devices?${params.toString()}`,
     { next: { revalidate: 0 } }
   );
-  return res.json() as Promise<DevicesDataItem[]>;
+  if (!res.ok) {
+    console.error("Failed to fetch devices:", res.status, res.statusText);
+    return [];
+  }
+  const data = await res.json();
+  return Array.isArray(data) ? (data as DevicesDataItem[]) : [];

Also applies to: 56-67, 110-121


123-131: Type of searchParams should not be Promise.

Next.js App Router passes searchParams as a plain object; the Promise type is misleading and can mask type issues.

-export default async function DesciResearch({
+export default async function DesciResearch({
   searchParams,
 }: {
-  searchParams: Promise<{ from: string; to: string; interval: string }>;
+  searchParams: { from?: string; to?: string; interval?: string };
 }) {
-  const { from, to, interval } = await searchParams;
+  const { from, to, interval } = searchParams;
src/app/api/sciweave-analytics/chats/route.ts (1)

42-48: Clarify error message.

“chats over time” is fine here, but mirror this precise phrasing across routes for consistency.

src/app/api/sciweave-analytics/users/route.ts (1)

42-48: Correct copy-paste error in log/response text.

These should say “unique users over time”.

-    console.error("Error fetching chats over time:", error);
+    console.error("Error fetching unique users over time:", error);
@@
-      { error: "Failed to fetch chats over time" },
+      { error: "Failed to fetch unique users over time" },
src/app/api/sciweave-analytics/sessions/route.ts (2)

129-135: Clarify error message.

Say “sessions over time” to match the route.

-    console.error("Error fetching chats over time:", error);
+    console.error("Error fetching sessions over time:", error);
@@
-      { error: "Failed to fetch chats over time" },
+      { error: "Failed to fetch sessions over time" },

47-49: Confirm internal email filter logic and excluded list governance.

The static excluded array plus IS_PROD filter can drift. Consider sourcing from config or DB.

src/app/api/sciweave-analytics/devices/route.ts (4)

33-33: Don’t drop NULL usernames when excluding internal traffic.

username NOT LIKE ... is NULL when username is NULL, removing those rows. Coalesce to keep them.

-        ${IS_PROD ? "AND username NOT LIKE '%@desci.com'" : ""}
+        ${IS_PROD ? "AND COALESCE(username, '') NOT LIKE '%@desci.com'" : ""}

15-17: Add quick input sanity checks (from < to, small max window).

Pre-empt bad queries and protect the DB.

     const { from, to, interval } = querySchema.parse(
       Object.fromEntries(searchParams)
     );
+    if (from >= to) {
+      return NextResponse.json({ error: "'from' must be before 'to'." }, { status: 400 });
+    }
+    // Optional: cap window to e.g. 366 days
+    if (to.getTime() - from.getTime() > 366 * 24 * 60 * 60 * 1000) {
+      return NextResponse.json({ error: "Date range too large." }, { status: 400 });
+    }

52-55: Fix copy/paste: “chats” → “devices”.

Aligns logs and response with this route.

-    console.error("Error fetching chats over time:", error);
+    console.error("Error fetching device analytics over time:", error);
@@
-      { error: "Failed to fetch chats over time" },
+      { error: "Failed to fetch device analytics over time" },

64-76: Consider 405 Method Not Allowed for POST without ?get=true.

HTTP 405 is more semantically correct; optionally include Allow header.

-  return NextResponse.json(
-    { error: "POST method requires ?get=true parameter for replica compatibility" },
-    { status: 400 }
-  );
+  return NextResponse.json(
+    { error: "POST method requires ?get=true parameter for replica compatibility" },
+    { status: 405 /*, headers: { 'Allow': 'GET, POST' } */ }
+  );
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0ac699c and 262f6a4.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (9)
  • .env.example (1 hunks)
  • .nvmrc (1 hunks)
  • README.md (1 hunks)
  • src/app/(auth)/metrics/sciweave/page.tsx (1 hunks)
  • src/app/api/sciweave-analytics/chats/route.ts (3 hunks)
  • src/app/api/sciweave-analytics/devices/route.ts (3 hunks)
  • src/app/api/sciweave-analytics/sessions/route.ts (3 hunks)
  • src/app/api/sciweave-analytics/users/route.ts (3 hunks)
  • src/components/molecules/DesciResearchAnalytics.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
src/app/api/sciweave-analytics/chats/route.ts (3)
src/app/api/sciweave-analytics/devices/route.ts (2)
  • GET (60-62)
  • POST (64-76)
src/app/api/sciweave-analytics/sessions/route.ts (2)
  • GET (138-140)
  • POST (142-154)
src/app/api/sciweave-analytics/users/route.ts (2)
  • GET (51-53)
  • POST (55-67)
src/app/api/sciweave-analytics/users/route.ts (3)
src/app/api/sciweave-analytics/chats/route.ts (2)
  • GET (51-53)
  • POST (55-67)
src/app/api/sciweave-analytics/devices/route.ts (2)
  • GET (60-62)
  • POST (64-76)
src/app/api/sciweave-analytics/sessions/route.ts (2)
  • GET (138-140)
  • POST (142-154)
src/app/api/sciweave-analytics/sessions/route.ts (3)
src/app/api/sciweave-analytics/chats/route.ts (2)
  • GET (51-53)
  • POST (55-67)
src/app/api/sciweave-analytics/devices/route.ts (2)
  • GET (60-62)
  • POST (64-76)
src/app/api/sciweave-analytics/users/route.ts (2)
  • GET (51-53)
  • POST (55-67)
src/app/api/sciweave-analytics/devices/route.ts (3)
src/app/api/sciweave-analytics/chats/route.ts (2)
  • GET (51-53)
  • POST (55-67)
src/app/api/sciweave-analytics/sessions/route.ts (2)
  • GET (138-140)
  • POST (142-154)
src/app/api/sciweave-analytics/users/route.ts (2)
  • GET (51-53)
  • POST (55-67)
🪛 dotenv-linter (3.3.0)
.env.example

[warning] 23-23: [EndingBlankLine] No blank line at the end of the file

(EndingBlankLine)

🔇 Additional comments (10)
.nvmrc (1)

1-1: Pin a full Node semver and add a trailing newline in .nvmrc

22.15.0 was released Apr 23, 2025, but the latest 22.x as of Sep 11, 2025 is v22.19.0 (Aug 28, 2025). Choose one:

  • To pin the intended minor: replace 22.1522.15.0 and add a trailing newline.
  • To use the current latest 22.x: replace with 22.19.0 and add a trailing newline.
src/app/(auth)/metrics/sciweave/page.tsx (1)

115-118: Revalidate policy check.

Devices endpoint uses revalidate: 0 while others use 3600. If unintentional, align them; if intentional, consider documenting the rationale.

src/app/api/sciweave-analytics/chats/route.ts (2)

51-67: POST get=true gate LGTM.


31-31: Confirm internal email filter logic.

Ensure excluding “%@desci.com” in IS_PROD matches product intent; otherwise move to a configurable allowlist/regex.

src/app/api/sciweave-analytics/users/route.ts (2)

51-67: POST get=true gate LGTM.


31-31: Confirm internal email filter logic.

Same note as chats route.

src/app/api/sciweave-analytics/sessions/route.ts (2)

146-154: POST get=true gate LGTM.


48-50: End-time exclusive change confirmed.

created_at < $2 aligns with UI suggestion to use next-day start. Good.

src/app/api/sciweave-analytics/devices/route.ts (2)

60-62: GET delegator — LGTM.

Simple and consistent with sibling routes.


40-48: ```shell
#!/bin/bash
set -euo pipefail

echo "=== show src/lib/postgresClient.ts ==="
if [ -f src/lib/postgresClient.ts ]; then
sed -n '1,240p' src/lib/postgresClient.ts || true
else
echo "file not found: src/lib/postgresClient.ts"
fi

echo
echo "=== search repo for pg type parser config / pg.types usage ==="
rg -n 'setTypeParser' || true
rg -n 'types.setTypeParser' || true
rg -n 'pg.types' || true

echo
echo "=== search repo for created_at / timestamptz / timestamp usages (migrations/schema) ==="
rg -n -S 'created_at' || true
rg -n -S 'timestamptz' || true
rg -n -S 'timestamp without time zone' || true
rg -n -S 'CREATE TABLE' || true


</blockquote></details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

Comment thread .env.example Outdated
Comment on lines +18 to +23
DESCI_RESEARCH_CONNECTION_DATABASE_URL=""

NEXT_PUBLIC_BASE_URL=http://localhost:3000 No newline at end of file
NEXT_PUBLIC_BASE_URL=http://localhost:3000

# supabase for search_logs sciweave
DESCI_RESEARCH_DATABASE_URL= No newline at end of file

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Resolve env var duplication and add EOF newline.

You have both DESCI_RESEARCH_CONNECTION_DATABASE_URL and DESCI_RESEARCH_DATABASE_URL. This invites misconfiguration.

  • Pick one canonical name (prefer the new DESCI_RESEARCH_DATABASE_URL) and remove/alias the other across the codebase.
  • Add a blank line at file end (dotenv-linter EndingBlankLine).

Proposed change:

-DESCI_RESEARCH_CONNECTION_DATABASE_URL=""
+## (deprecated) DESCI_RESEARCH_CONNECTION_DATABASE_URL=""  # consider removing
@@
-# supabase for search_logs sciweave
-DESCI_RESEARCH_DATABASE_URL=
+# supabase for search_logs sciweave
+DESCI_RESEARCH_DATABASE_URL=
+

To verify usage across the repo:


🏁 Script executed:

#!/bin/bash
# Find all references to both env names
rg -nP 'DESCI_RESEARCH_(CONNECTION_)?DATABASE_URL' -C2

Length of output: 256


Remove unused DESCI_RESEARCH_CONNECTION_DATABASE_URL from .env.example and add trailing newline

Repo search: only DESCI_RESEARCH_DATABASE_URL is referenced (src/lib/postgresClient.ts). Remove or mark DESCI_RESEARCH_CONNECTION_DATABASE_URL in .env.example and add a single blank line at EOF.

Location: .env.example (lines 18–23)

Proposed change:

-DESCI_RESEARCH_CONNECTION_DATABASE_URL=""
+## (deprecated) DESCI_RESEARCH_CONNECTION_DATABASE_URL=""  # consider removing
@@
-# supabase for search_logs sciweave
-DESCI_RESEARCH_DATABASE_URL=
+# supabase for search_logs sciweave
+DESCI_RESEARCH_DATABASE_URL=
+
📝 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
DESCI_RESEARCH_CONNECTION_DATABASE_URL=""
NEXT_PUBLIC_BASE_URL=http://localhost:3000
\ No newline at end of file
NEXT_PUBLIC_BASE_URL=http://localhost:3000
# supabase for search_logs sciweave
DESCI_RESEARCH_DATABASE_URL=
## (deprecated) DESCI_RESEARCH_CONNECTION_DATABASE_URL="" # consider removing
NEXT_PUBLIC_BASE_URL=http://localhost:3000
# supabase for search_logs sciweave
DESCI_RESEARCH_DATABASE_URL=
🧰 Tools
🪛 dotenv-linter (3.3.0)

[warning] 18-18: [QuoteCharacter] The value has quote characters (', ")

(QuoteCharacter)


[warning] 23-23: [EndingBlankLine] No blank line at the end of the file

(EndingBlankLine)

🤖 Prompt for AI Agents
In .env.example around lines 18 to 23, remove the unused
DESCI_RESEARCH_CONNECTION_DATABASE_URL entry (it is not referenced anywhere;
only DESCI_RESEARCH_DATABASE_URL is used) and ensure the file ends with a single
trailing newline (add one blank line at EOF if missing); simply delete or
comment out the DESCI_RESEARCH_CONNECTION_DATABASE_URL line and save the file
with a final newline.

Comment thread src/app/api/sciweave-analytics/devices/route.ts
Fixes FUNCTION_INVOCATION_TIMEOUT on Vercel by adding maxDuration=300
and streaming the backend response body instead of buffering.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 (1)
src/app/api/download/route.ts (1)

7-35: Consider extracting shared logic to reduce duplication.

Both download/route.ts and downloadAnalyticsReport/route.ts share nearly identical fetch-and-stream logic. A shared utility would improve maintainability.

Example shared utility
// lib/streamCsvFromBackend.ts
export async function streamCsvFromBackend(
  endpoint: string,
  cookieHeader: string
): Promise<Response> {
  const response = await fetch(endpoint, {
    credentials: "include",
    headers: {
      cookie: cookieHeader,
      "Content-Type": "application/json",
    },
  });

  if (!response.ok || !response.body) {
    return NextResponse.json(
      { error: "Failed to fetch report" },
      { status: response.ok ? 502 : response.status }
    );
  }

  return new Response(response.body, {
    headers: {
      "Content-Type": "text/csv",
      "Content-Disposition": 'attachment; filename="report.csv"',
    },
  });
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/api/download/route.ts` around lines 7 - 35, Extract the duplicated
fetch-and-stream logic into a shared utility (e.g., streamCsvFromBackend) and
have the GET handler in this file call that utility instead of duplicating code;
specifically, create streamCsvFromBackend(endpoint: string, cookieHeader:
string) that performs the fetch with credentials and cookie header, checks
response.ok and response.body, returns a NextResponse.json({ error: "Failed to
fetch report" }, { status: response.ok ? 502 : response.status }) on failure, or
returns new Response(response.body, { headers: { "Content-Type": "text/csv",
"Content-Disposition": 'attachment; filename="report.csv"' } }) on success, then
update this file's GET to call
streamCsvFromBackend(`${NODES_API_URL}/v1/admin/analytics/csv`,
cookies().toString()) and forward the returned Response; also update
downloadAnalyticsReport/route.ts to use the same utility to remove duplication
and ensure the catch block returns a JSON error message string (not the raw
error object).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/app/api/download/route.ts`:
- Around line 33-35: The catch block in the download route returns the raw
caught error object which serializes to {} (see the catch in download/route.ts);
update the catch to extract a serializable message instead—use the caught
variable (err) and pass err instanceof Error ? err.message : String(err) (or
similar) into NextResponse.json({ error: ... }, { status: 500 }) so the response
includes the actual error text; locate the catch surrounding NextResponse.json
in download/route.ts and replace the returned error payload accordingly.
- Around line 20-25: The current check returns response.status even when
response.ok is true but response.body is missing, which can be misleading;
update the block that checks response.ok and response.body (the lines using
response.ok, response.body and NextResponse.json) to specifically handle the
"body missing" case by returning a clear error and a 502 (or other appropriate
server-error) status instead of using response.status—ensure the
NextResponse.json call communicates "Missing response body" (or similar) and
uses a fixed 502 status when response.ok is true but response.body is falsy.

In `@src/app/api/downloadAnalyticsReport/route.ts`:
- Around line 34-36: The catch block in the downloadAnalyticsReport route
currently returns the raw Error instance (err) which serializes to an empty
object; change the error payload to a serializable object (e.g., { name:
err.name, message: err.message, stack: err.stack }) and return that via
NextResponse.json instead of err so consumers receive useful error details;
update the catch in route.ts (the handler that uses NextResponse.json) to build
and return this serializable error object.
- Around line 21-26: The current check returns response.status even when
response.ok is true but response.body is missing, which can misleadingly return
200; update the handling in route.ts around the fetch response so that you
distinguish the two cases: if !response.ok return NextResponse.json({...}, {
status: response.status }); else if response.ok but !response.body return
NextResponse.json({ error: "Failed to fetch report" }, { status: 502 });
reference the same response and NextResponse.json calls in this file so the
missing-body case uses a 502 (or other non-200) status instead of returning
response.status.

---

Nitpick comments:
In `@src/app/api/download/route.ts`:
- Around line 7-35: Extract the duplicated fetch-and-stream logic into a shared
utility (e.g., streamCsvFromBackend) and have the GET handler in this file call
that utility instead of duplicating code; specifically, create
streamCsvFromBackend(endpoint: string, cookieHeader: string) that performs the
fetch with credentials and cookie header, checks response.ok and response.body,
returns a NextResponse.json({ error: "Failed to fetch report" }, { status:
response.ok ? 502 : response.status }) on failure, or returns new
Response(response.body, { headers: { "Content-Type": "text/csv",
"Content-Disposition": 'attachment; filename="report.csv"' } }) on success, then
update this file's GET to call
streamCsvFromBackend(`${NODES_API_URL}/v1/admin/analytics/csv`,
cookies().toString()) and forward the returned Response; also update
downloadAnalyticsReport/route.ts to use the same utility to remove duplication
and ensure the catch block returns a JSON error message string (not the raw
error object).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 87af05f3-a825-4d23-8a9a-1bb1061b1e62

📥 Commits

Reviewing files that changed from the base of the PR and between 262f6a4 and e13a699.

📒 Files selected for processing (2)
  • src/app/api/download/route.ts
  • src/app/api/downloadAnalyticsReport/route.ts

Comment thread src/app/api/download/route.ts Outdated
Comment on lines 33 to 35
} catch (err) {
return NextResponse.json({ error: err }, { status: 500 });
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Same error serialization issue as in downloadAnalyticsReport/route.ts.

Error objects serialize to {}. Extract err.message instead (see comment in the other file for proposed fix).

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

In `@src/app/api/download/route.ts` around lines 33 - 35, The catch block in the
download route returns the raw caught error object which serializes to {} (see
the catch in download/route.ts); update the catch to extract a serializable
message instead—use the caught variable (err) and pass err instanceof Error ?
err.message : String(err) (or similar) into NextResponse.json({ error: ... }, {
status: 500 }) so the response includes the actual error text; locate the catch
surrounding NextResponse.json in download/route.ts and replace the returned
error payload accordingly.

Comment thread src/app/api/downloadAnalyticsReport/route.ts Outdated
Comment on lines 34 to 36
} catch (err) {
return NextResponse.json({ error: err }, { status: 500 });
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Error objects don't serialize to JSON properly.

Error instances serialize to {} when passed to JSON.stringify. This will produce an unhelpful response like { "error": {} }.

Proposed fix
   } catch (err) {
-    return NextResponse.json({ error: err }, { status: 500 });
+    return NextResponse.json(
+      { error: err instanceof Error ? err.message : "Internal server error" },
+      { status: 500 }
+    );
   }
📝 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
} catch (err) {
return NextResponse.json({ error: err }, { status: 500 });
}
} catch (err) {
return NextResponse.json(
{ error: err instanceof Error ? err.message : "Internal server error" },
{ status: 500 }
);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/api/downloadAnalyticsReport/route.ts` around lines 34 - 36, The catch
block in the downloadAnalyticsReport route currently returns the raw Error
instance (err) which serializes to an empty object; change the error payload to
a serializable object (e.g., { name: err.name, message: err.message, stack:
err.stack }) and return that via NextResponse.json instead of err so consumers
receive useful error details; update the catch in route.ts (the handler that
uses NextResponse.json) to build and return this serializable error object.

Take develop's PROD_FILTER constants, supabase env vars, and yarn.lock.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
src/app/api/sciweave-analytics/devices/route.ts (2)

46-49: ⚠️ Potential issue | 🟡 Minor

Correct error context in logs/response

This devices endpoint currently reports "chats over time" errors, which is misleading during incident/debug workflows.

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

In `@src/app/api/sciweave-analytics/devices/route.ts` around lines 46 - 49, The
error handling in the devices route logs and returns a message referencing
"chats over time", which is misleading; update the console.error and the
NextResponse.json error message inside the GET handler (or exported route
function) to reflect the correct context such as "devices over time" or "devices
metrics" (update the console.error("Error fetching chats over time:", error) and
the corresponding NextResponse.json({ error: "Failed to fetch chats over time"
}, { status: 500 }) to use the correct wording), ensuring the error text matches
the endpoint purpose.

13-33: ⚠️ Potential issue | 🟠 Major

Release DB connection in a finally block

The database connection is not released if query execution fails. When client.query() throws an error (lines 14–32), the exception is caught by the outer try-catch block, but client.release() on line 33 is skipped, leaving the connection unreleased in the pool and risking pool exhaustion under error load.

Suggested fix
-    const client = await pool.connect();
-    const result = await client.query(
+    const client = await pool.connect();
+    try {
+      const result = await client.query(
       `
@@
-      [from, to, intervalToDateTrunc(interval)]
-    );
-    client.release();
-    const data = result.rows as {
+      [from, to, intervalToDateTrunc(interval)]
+      );
+      const data = result.rows as {
       date: Date;
       mobile: number;
       tablet: number;
       mac: number;
       windows: number;
       otherDesktops: number;
       unknown: number;
-    }[];
-
-    return NextResponse.json(data);
+      }[];
+
+      return NextResponse.json(data);
+    } finally {
+      client.release();
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/api/sciweave-analytics/devices/route.ts` around lines 13 - 33, The DB
client acquired via pool.connect() in the route handler must be released in a
finally block to ensure it is returned to the pool even if client.query(...)
throws; refactor the code around pool.connect(), client.query(...) and
client.release() so you call client.release() from a finally (or guard with if
(client) client.release()) after the try that runs the query (e.g., the block
surrounding the SELECT using intervalToDateTrunc), leaving error handling in the
existing catch but guaranteeing release.
src/app/api/sciweave-analytics/users/route.ts (2)

37-40: ⚠️ Potential issue | 🟡 Minor

Fix route-specific error message

This users endpoint logs/returns "chats over time" errors, which makes triage misleading.

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

In `@src/app/api/sciweave-analytics/users/route.ts` around lines 37 - 40, The
error handling in the users API route incorrectly logs and returns a generic
"chats over time" message; update the console.error call and the
NextResponse.json error message to reflect this users endpoint (e.g., "Failed to
fetch users" or "Error fetching users") so logs and responses are accurate;
locate the error block in src/app/api/sciweave-analytics/users/route.ts where
console.error("Error fetching chats over time:", error) and NextResponse.json({
error: "Failed to fetch chats over time" }, { status: 500 }) are used and
replace both strings with a users-specific message while keeping the error
object included in the log and preserving the 500 response.

13-34: ⚠️ Potential issue | 🟠 Major

Ensure DB client is always released

client.release() only runs on the success path. If client.query(...) throws, the connection is leaked.

Suggested fix
-    const client = await pool.connect();
-    const result = await client.query(
+    const client = await pool.connect();
+    try {
+      const result = await client.query(
       `
      SELECT
         DATE_TRUNC($3, created_at) AS DATE,
         COUNT(DISTINCT username) AS value
@@
-      [from, to, intervalToDateTrunc(interval)]
-    );
-    client.release();
-    const data = result.rows as { day: string; value: number }[];
-    return NextResponse.json(data);
+      [from, to, intervalToDateTrunc(interval)]
+      );
+      const data = result.rows as { day: string; value: number }[];
+      return NextResponse.json(data);
+    } finally {
+      client.release();
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/api/sciweave-analytics/users/route.ts` around lines 13 - 34, The DB
client obtained via pool.connect() is only released on the success path, so wrap
the usage of client (the result of pool.connect()) in a try/finally (or use a
helper that auto-releases) to guarantee client.release() runs even if
client.query(...) throws; update the block around pool.connect(),
client.query(..., [from, to, intervalToDateTrunc(interval)]) and
client.release() to call client.release() from finally and keep the same
query/parameters and PROD_FILTER_AND logic.
♻️ Duplicate comments (1)
src/app/api/sciweave-analytics/devices/route.ts (1)

17-23: ⚠️ Potential issue | 🟠 Major

Use NULL-safe aggregate predicates for device counters

Current SUM(bool::int) expressions can produce NULL-driven undercount/empty totals and are less robust when fields are missing. Prefer COUNT(*) FILTER (WHERE ...) with COALESCE(...).

Suggested fix
-        date_trunc($3, created_at) AS DATE,
-        SUM(((device_info->>'isMobile')::boolean)::int) AS "mobile",
-        SUM(((device_info->>'isTablet')::boolean)::int) AS "tablet",
-        SUM(((device_info->>'isDesktop')::boolean AND device_info->>'osName' ILIKE 'mac%')::int) AS "mac",
-        SUM(((device_info->>'isDesktop')::boolean AND device_info->>'osName' ILIKE 'windows%')::int) AS "windows",
-        SUM(((device_info->>'isDesktop')::boolean AND device_info->>'osName' NOT ILIKE 'mac%' AND device_info->>'osName' NOT ILIKE 'windows%')::int) AS "otherDesktops",
-        SUM((NOT((device_info->>'isMobile')::boolean OR (device_info->>'isTablet')::boolean OR (device_info->>'isDesktop')::boolean))::int) AS "unknown"
+        date_trunc($3, created_at) AS "date",
+        COUNT(*) FILTER (WHERE (device_info->>'isMobile')::boolean IS TRUE) AS "mobile",
+        COUNT(*) FILTER (WHERE (device_info->>'isTablet')::boolean IS TRUE) AS "tablet",
+        COUNT(*) FILTER (
+          WHERE COALESCE((device_info->>'isDesktop')::boolean, FALSE) IS TRUE
+            AND COALESCE(device_info->>'osName', '') ILIKE 'mac%'
+        ) AS "mac",
+        COUNT(*) FILTER (
+          WHERE COALESCE((device_info->>'isDesktop')::boolean, FALSE) IS TRUE
+            AND COALESCE(device_info->>'osName', '') ILIKE 'windows%'
+        ) AS "windows",
+        COUNT(*) FILTER (
+          WHERE COALESCE((device_info->>'isDesktop')::boolean, FALSE) IS TRUE
+            AND COALESCE(device_info->>'osName', '') NOT ILIKE 'mac%'
+            AND COALESCE(device_info->>'osName', '') NOT ILIKE 'windows%'
+        ) AS "otherDesktops",
+        COUNT(*) FILTER (
+          WHERE COALESCE((device_info->>'isMobile')::boolean, FALSE) IS NOT TRUE
+            AND COALESCE((device_info->>'isTablet')::boolean, FALSE) IS NOT TRUE
+            AND COALESCE((device_info->>'isDesktop')::boolean, FALSE) IS NOT TRUE
+        ) AS "unknown"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/api/sciweave-analytics/devices/route.ts` around lines 17 - 23,
Replace the SUM(bool::int) aggregates with NULL-safe COUNT(*) FILTER expressions
wrapped in COALESCE to avoid NULLs when device_info keys are missing: for each
alias ("mobile","tablet","mac","windows","otherDesktops","unknown") change e.g.
SUM(((device_info->>'isMobile')::boolean)::int) AS "mobile" to COALESCE(COUNT(*)
FILTER (WHERE (device_info->>'isMobile')::boolean = TRUE), 0) AS "mobile", and
similarly use FILTER conditions for tablet, for mac/windows use
(device_info->>'isDesktop')::boolean = TRUE AND device_info->>'osName' ILIKE
'mac%' (or 'windows%'), for otherDesktops use desktop TRUE AND osName NOT ILIKE
'mac%' AND NOT ILIKE 'windows%', and for unknown use FILTER (WHERE NOT
((device_info->>'isMobile')::boolean = TRUE OR
(device_info->>'isTablet')::boolean = TRUE OR
(device_info->>'isDesktop')::boolean = TRUE)) with COALESCE(..., 0); update the
SELECT expressions that currently define the
"mobile","tablet","mac","windows","otherDesktops","unknown" columns accordingly.
🧹 Nitpick comments (1)
.env.example (1)

20-21: Consider alphabetical ordering of environment variables.

The static analysis suggests ordering SCIWEAVE_SUPABASE_SERVICE_KEY before SCIWEAVE_SUPABASE_URL alphabetically. While not critical, consistent ordering improves readability and reduces merge conflicts.

📝 Proposed reordering
-SCIWEAVE_SUPABASE_URL=
 SCIWEAVE_SUPABASE_SERVICE_KEY=
+SCIWEAVE_SUPABASE_URL=

As per static analysis: "The SCIWEAVE_SUPABASE_SERVICE_KEY key should go before the SCIWEAVE_SUPABASE_URL key (UnorderedKey)."

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

In @.env.example around lines 20 - 21, The environment variables in .env.example
are out of alphabetical order; move SCIWEAVE_SUPABASE_SERVICE_KEY so it appears
before SCIWEAVE_SUPABASE_URL to maintain consistent alphabetical ordering. Edit
the .env.example file and reorder the two entries
(SCIWEAVE_SUPABASE_SERVICE_KEY, then SCIWEAVE_SUPABASE_URL), and ensure any
related example env blocks follow the same ordering for consistency.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.env.example:
- Line 21: The file `.env.example` ends with the variable declaration
SCIWEAVE_SUPABASE_SERVICE_KEY= but is missing a trailing newline; update the
file to ensure there is a single blank line (newline character) after that line
so the file ends with a newline.

In `@src/app/`(auth)/metrics/sciweave/page.tsx:
- Around line 93-105: Wrap the entire fetch + parsing flow in a try/catch so
network errors and JSON parsing exceptions are handled and return the fallback
[]; specifically surround the call that produces res and the subsequent
res.json() check (the block that checks res.ok, res.statusText,
Array.isArray(data), and returns data as UserSessionsDataItem[]) with try/catch,
log the caught error (including error details) and return [] from the catch
block to ensure callers always receive a safe UserSessionsDataItem[].

---

Outside diff comments:
In `@src/app/api/sciweave-analytics/devices/route.ts`:
- Around line 46-49: The error handling in the devices route logs and returns a
message referencing "chats over time", which is misleading; update the
console.error and the NextResponse.json error message inside the GET handler (or
exported route function) to reflect the correct context such as "devices over
time" or "devices metrics" (update the console.error("Error fetching chats over
time:", error) and the corresponding NextResponse.json({ error: "Failed to fetch
chats over time" }, { status: 500 }) to use the correct wording), ensuring the
error text matches the endpoint purpose.
- Around line 13-33: The DB client acquired via pool.connect() in the route
handler must be released in a finally block to ensure it is returned to the pool
even if client.query(...) throws; refactor the code around pool.connect(),
client.query(...) and client.release() so you call client.release() from a
finally (or guard with if (client) client.release()) after the try that runs the
query (e.g., the block surrounding the SELECT using intervalToDateTrunc),
leaving error handling in the existing catch but guaranteeing release.

In `@src/app/api/sciweave-analytics/users/route.ts`:
- Around line 37-40: The error handling in the users API route incorrectly logs
and returns a generic "chats over time" message; update the console.error call
and the NextResponse.json error message to reflect this users endpoint (e.g.,
"Failed to fetch users" or "Error fetching users") so logs and responses are
accurate; locate the error block in
src/app/api/sciweave-analytics/users/route.ts where console.error("Error
fetching chats over time:", error) and NextResponse.json({ error: "Failed to
fetch chats over time" }, { status: 500 }) are used and replace both strings
with a users-specific message while keeping the error object included in the log
and preserving the 500 response.
- Around line 13-34: The DB client obtained via pool.connect() is only released
on the success path, so wrap the usage of client (the result of pool.connect())
in a try/finally (or use a helper that auto-releases) to guarantee
client.release() runs even if client.query(...) throws; update the block around
pool.connect(), client.query(..., [from, to, intervalToDateTrunc(interval)]) and
client.release() to call client.release() from finally and keep the same
query/parameters and PROD_FILTER_AND logic.

---

Duplicate comments:
In `@src/app/api/sciweave-analytics/devices/route.ts`:
- Around line 17-23: Replace the SUM(bool::int) aggregates with NULL-safe
COUNT(*) FILTER expressions wrapped in COALESCE to avoid NULLs when device_info
keys are missing: for each alias
("mobile","tablet","mac","windows","otherDesktops","unknown") change e.g.
SUM(((device_info->>'isMobile')::boolean)::int) AS "mobile" to COALESCE(COUNT(*)
FILTER (WHERE (device_info->>'isMobile')::boolean = TRUE), 0) AS "mobile", and
similarly use FILTER conditions for tablet, for mac/windows use
(device_info->>'isDesktop')::boolean = TRUE AND device_info->>'osName' ILIKE
'mac%' (or 'windows%'), for otherDesktops use desktop TRUE AND osName NOT ILIKE
'mac%' AND NOT ILIKE 'windows%', and for unknown use FILTER (WHERE NOT
((device_info->>'isMobile')::boolean = TRUE OR
(device_info->>'isTablet')::boolean = TRUE OR
(device_info->>'isDesktop')::boolean = TRUE)) with COALESCE(..., 0); update the
SELECT expressions that currently define the
"mobile","tablet","mac","windows","otherDesktops","unknown" columns accordingly.

---

Nitpick comments:
In @.env.example:
- Around line 20-21: The environment variables in .env.example are out of
alphabetical order; move SCIWEAVE_SUPABASE_SERVICE_KEY so it appears before
SCIWEAVE_SUPABASE_URL to maintain consistent alphabetical ordering. Edit the
.env.example file and reorder the two entries (SCIWEAVE_SUPABASE_SERVICE_KEY,
then SCIWEAVE_SUPABASE_URL), and ensure any related example env blocks follow
the same ordering for consistency.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: edcb2f5e-d948-4a6b-88cc-6668eeba56c7

📥 Commits

Reviewing files that changed from the base of the PR and between e13a699 and 360534c.

📒 Files selected for processing (7)
  • .env.example
  • src/app/(auth)/metrics/sciweave/page.tsx
  • src/app/api/sciweave-analytics/chats/route.ts
  • src/app/api/sciweave-analytics/devices/route.ts
  • src/app/api/sciweave-analytics/sessions/route.ts
  • src/app/api/sciweave-analytics/users/route.ts
  • src/components/molecules/DesciResearchAnalytics.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/components/molecules/DesciResearchAnalytics.tsx
  • src/app/api/sciweave-analytics/chats/route.ts
  • src/app/api/sciweave-analytics/sessions/route.ts

Comment thread .env.example

SCIWEAVE_SUPABASE_URL=
SCIWEAVE_SUPABASE_SERVICE_KEY= No newline at end of file
SCIWEAVE_SUPABASE_SERVICE_KEY=

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if .env.example ends with a newline
if [ -s .env.example ]; then
  tail -c 1 .env.example | od -An -tc | grep -q '\n' && echo "✓ File ends with newline" || echo "✗ Missing trailing newline"
fi

Repository: desci-labs/desci-admin

Length of output: 150


🏁 Script executed:

cat -n .env.example | tail -5

Repository: desci-labs/desci-admin

Length of output: 199


Add a trailing newline at the end of the file.

The file still lacks a newline after line 21. Add a single blank line to the end of .env.example.

🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 21-21: [UnorderedKey] The SCIWEAVE_SUPABASE_SERVICE_KEY key should go before the SCIWEAVE_SUPABASE_URL key

(UnorderedKey)

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

In @.env.example at line 21, The file `.env.example` ends with the variable
declaration SCIWEAVE_SUPABASE_SERVICE_KEY= but is missing a trailing newline;
update the file to ensure there is a single blank line (newline character) after
that line so the file ends with a newline.

Comment thread src/app/(auth)/metrics/sciweave/page.tsx Outdated
Change disabled check from `state?.user !== undefined` to `!!state?.user`
so that null/undefined both correctly result in enabled button.
Also fix button label from "Send code" to "Verify code" on step 2.
NODE_ENV is always "production" on Vercel, which sets the cookie domain
to .desci.com even on vercel.app preview deployments. The browser
ignores this cross-domain cookie, so login succeeds server-side but
the middleware never sees the auth cookie.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/app/actions.tsx (1)

46-46: ⚠️ Potential issue | 🟡 Minor

Comment/code mismatch: Comment says "2 hours" but code calculates 30 days.

The expression 1000 * 60 * 60 * 24 * 30 * 1 equals 30 days, not 2 hours as stated in the comment. Please verify whether this extended expiry is intentional for the dev cookie.

📝 Proposed fix (if 30 days is intentional)
-        expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30 * 1), // 2 hours
+        expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30), // 30 days
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/actions.tsx` at line 46, The expires value for the dev cookie (the
expires property expression `1000 * 60 * 60 * 24 * 30 * 1`) does not match the
inline comment "2 hours"; decide which is intended and fix accordingly: if you
want a 2-hour expiry, replace the expression with `1000 * 60 * 60 * 2` for the
expires property used when setting the cookie; if 30 days is intended, update
the comment to reflect "30 days" (or remove the misleading comment) — locate the
expires assignment in the cookie-setting logic in src/app/actions.tsx (the
expires property on the cookie object) and make the change there.
🧹 Nitpick comments (1)
src/app/actions.tsx (1)

31-61: Duplicate cookie setting in production.

When NEXT_ENV === "production", the cookie is set twice with identical values:

  1. Lines 31-37: Sets cookie with domain: ".desci.com" (since the condition evaluates to true)
  2. Lines 53-61: Sets the same cookie again

Consider removing the redundant production block (lines 53-61) since the first block already handles production correctly after this change.

♻️ Proposed fix to remove duplicate cookie setting
     cookies().set(AUTH_COOKIE_FIELDNAME, response.user.token, {
       path: "/",
       expires: new Date(Date.now() + 1000 * 60 * 60 * 2), // 2 hours
       httpOnly: true,
       secure: process.env.NEXT_ENV === "production",
       domain: process.env.NEXT_ENV === "production" ? ".desci.com" : undefined,
     });

     if (
       AUTH_COOKIE_FIELDNAME === "auth-dev" &&
       process.env.NEXT_ENV === "development"
     ) {
       console.log("[cookie]", AUTH_COOKIE_FIELDNAME, process.env.NEXT_ENV);
       cookies().set("auth", response.user.token, {
         path: "/",
         expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30 * 1), // 30 days
         httpOnly: true,
         secure: true,
         // domain: '.desci.com',
       });
     }
-
-    if (process.env.NEXT_ENV === "production") {
-      cookies().set(AUTH_COOKIE_FIELDNAME, response.user.token, {
-        path: "/",
-        expires: new Date(Date.now() + 1000 * 60 * 60 * 2), // 2 hours
-        httpOnly: true,
-        secure: true,
-        domain: ".desci.com",
-      });
-    }

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

In `@src/app/actions.tsx` around lines 31 - 61, Remove the redundant
production-only cookies().set call: the initial
cookies().set(AUTH_COOKIE_FIELDNAME, response.user.token, {..., domain:
process.env.NEXT_ENV === "production" ? ".desci.com" : undefined}) already
covers production, so delete the later if (process.env.NEXT_ENV ===
"production") { cookies().set(AUTH_COOKIE_FIELDNAME, ...) } block; keep the
dev-only branch that sets "auth" when AUTH_COOKIE_FIELDNAME === "auth-dev" and
NEXT_ENV === "development".
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/app/actions.tsx`:
- Line 33: The inline comment for the expires value mismatches the calculation:
the object property expires uses new Date(Date.now() + 1000 * 60 * 60 * 2) which
computes 2 hours but the comment says "3 hours"; update the code so they match
by either changing the multiplier from 2 to 3 to actually set 3 hours, or change
the comment to "2 hours" to reflect the current calculation (locate the expires
property assignment in actions.tsx to apply the fix).

---

Outside diff comments:
In `@src/app/actions.tsx`:
- Line 46: The expires value for the dev cookie (the expires property expression
`1000 * 60 * 60 * 24 * 30 * 1`) does not match the inline comment "2 hours";
decide which is intended and fix accordingly: if you want a 2-hour expiry,
replace the expression with `1000 * 60 * 60 * 2` for the expires property used
when setting the cookie; if 30 days is intended, update the comment to reflect
"30 days" (or remove the misleading comment) — locate the expires assignment in
the cookie-setting logic in src/app/actions.tsx (the expires property on the
cookie object) and make the change there.

---

Nitpick comments:
In `@src/app/actions.tsx`:
- Around line 31-61: Remove the redundant production-only cookies().set call:
the initial cookies().set(AUTH_COOKIE_FIELDNAME, response.user.token, {...,
domain: process.env.NEXT_ENV === "production" ? ".desci.com" : undefined})
already covers production, so delete the later if (process.env.NEXT_ENV ===
"production") { cookies().set(AUTH_COOKIE_FIELDNAME, ...) } block; keep the
dev-only branch that sets "auth" when AUTH_COOKIE_FIELDNAME === "auth-dev" and
NEXT_ENV === "development".

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 97a523f4-f69e-472c-8a69-e1674601e16f

📥 Commits

Reviewing files that changed from the base of the PR and between 5f5b04c and 0eeb76b.

📒 Files selected for processing (1)
  • src/app/actions.tsx

Comment thread src/app/actions.tsx Outdated
- Wrap session analytics fetch in try/catch to handle network/parse errors
- Separate !ok and !body checks in download routes, return 502 for missing body
- Fix mismatched expiry comments in login cookie settings
@hubsmoke hubsmoke merged commit cc7448f into develop Mar 13, 2026
0 of 3 checks passed
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