refactor(activity-log): use db aggregation - #924
Conversation
❌ Preview Environment deleted from BunnyshellAvailable commands (reply to this comment):
|
|
Warning Rate limit exceeded@steebchen has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 6 minutes and 7 seconds before requesting another review. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. 📒 Files selected for processing (1)
WalkthroughReplaces per-log in-memory aggregation with database-level daily and model/provider aggregation queries, maps aggregate rows into per-day activity objects (counts, tokens, costs, error/cache metrics, model breakdowns), and returns an Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor Client
participant API as Activity Route
participant DB as Database
Client->>API: GET /activity?projectIds=&days=
API->>DB: Query dailyAggregates (SELECT DATE(...), COUNT(...), SUM(...) GROUP BY DATE)
DB-->>API: dailyAggregates rows
API->>DB: Query modelBreakdowns (SELECT DATE(...), usedModel, usedProvider, COUNT, SUM GROUP BY DATE, model, provider)
DB-->>API: modelBreakdowns rows
rect rgba(200,230,255,0.6)
note right of API: Merge DB rows by date → build per-day objects:\n- counts, tokens, costs\n- error/cache metrics (rates)\n- modelBreakdown array per date
end
API-->>Client: 200 OK { "activity": [ ...per-day aggregated objects... ] }
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested labels
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (3)
apps/api/src/routes/activity.ts (3)
130-176: Use UTC day buckets and ensure numeric sums serialize as numbers
- DATE() without an explicit timezone can shift day buckets by server/session TZ. Prefer normalizing to UTC.
- Postgres NUMERIC sums often come back as strings in pg. Cast to double precision to guarantee JSON numbers.
Apply:
- date: sql<string>`DATE(${tables.log.createdAt})`.as("date"), + date: sql<string>`DATE(${tables.log.createdAt} AT TIME ZONE 'UTC')`.as("date"), @@ - inputTokens: - sql<number>`COALESCE(SUM(CAST(${tables.log.promptTokens} AS NUMERIC)), 0)`.as( + inputTokens: + sql<number>`COALESCE(SUM(${tables.log.promptTokens}::double precision), 0)`.as( "inputTokens", ), @@ - outputTokens: - sql<number>`COALESCE(SUM(CAST(${tables.log.completionTokens} AS NUMERIC)), 0)`.as( + outputTokens: + sql<number>`COALESCE(SUM(${tables.log.completionTokens}::double precision), 0)`.as( "outputTokens", ), @@ - totalTokens: - sql<number>`COALESCE(SUM(CAST(${tables.log.totalTokens} AS NUMERIC)), 0)`.as( + totalTokens: + sql<number>`COALESCE(SUM(${tables.log.totalTokens}::double precision), 0)`.as( "totalTokens", ), - cost: sql<number>`COALESCE(SUM(${tables.log.cost}), 0)`.as("cost"), - inputCost: sql<number>`COALESCE(SUM(${tables.log.inputCost}), 0)`.as( + cost: sql<number>`COALESCE(SUM(${tables.log.cost}::double precision), 0)`.as("cost"), + inputCost: sql<number>`COALESCE(SUM(${tables.log.inputCost}::double precision), 0)`.as( "inputCost", ), - outputCost: sql<number>`COALESCE(SUM(${tables.log.outputCost}), 0)`.as( + outputCost: sql<number>`COALESCE(SUM(${tables.log.outputCost}::double precision), 0)`.as( "outputCost", ), - requestCost: sql<number>`COALESCE(SUM(${tables.log.requestCost}), 0)`.as( + requestCost: sql<number>`COALESCE(SUM(${tables.log.requestCost}::double precision), 0)`.as( "requestCost", ), @@ - .groupBy(sql`DATE(${tables.log.createdAt})`) - .orderBy(sql`DATE(${tables.log.createdAt}) ASC`); + .groupBy(sql`DATE(${tables.log.createdAt} AT TIME ZONE 'UTC')`) + .orderBy(sql`DATE(${tables.log.createdAt} AT TIME ZONE 'UTC') ASC`);Optional (outside this hunk): consider normalizing JS bounds to UTC midnight for exact day inclusion.
177-211: Align date bucketing with UTC and stabilize ordering by provider as well
- Keep model breakdown date bucketing consistent with daily aggregates (UTC).
- Include provider in ORDER BY for deterministic modelBreakdown array ordering.
- Same note on casting sums to double precision as above.
- date: sql<string>`DATE(${tables.log.createdAt})`.as("date"), + date: sql<string>`DATE(${tables.log.createdAt} AT TIME ZONE 'UTC')`.as("date"), @@ - inputTokens: - sql<number>`COALESCE(SUM(CAST(${tables.log.promptTokens} AS NUMERIC)), 0)`.as( + inputTokens: + sql<number>`COALESCE(SUM(${tables.log.promptTokens}::double precision), 0)`.as( "inputTokens", ), @@ - outputTokens: - sql<number>`COALESCE(SUM(CAST(${tables.log.completionTokens} AS NUMERIC)), 0)`.as( + outputTokens: + sql<number>`COALESCE(SUM(${tables.log.completionTokens}::double precision), 0)`.as( "outputTokens", ), @@ - totalTokens: - sql<number>`COALESCE(SUM(CAST(${tables.log.totalTokens} AS NUMERIC)), 0)`.as( + totalTokens: + sql<number>`COALESCE(SUM(${tables.log.totalTokens}::double precision), 0)`.as( "totalTokens", ), - cost: sql<number>`COALESCE(SUM(${tables.log.cost}), 0)`.as("cost"), + cost: sql<number>`COALESCE(SUM(${tables.log.cost}::double precision), 0)`.as("cost"), @@ - .groupBy( - sql`DATE(${tables.log.createdAt}), ${tables.log.usedModel}, ${tables.log.usedProvider}`, - ) + .groupBy( + sql`DATE(${tables.log.createdAt} AT TIME ZONE 'UTC'), ${tables.log.usedModel}, ${tables.log.usedProvider}`, + ) @@ - .orderBy( - sql`DATE(${tables.log.createdAt}) ASC, ${tables.log.usedModel} ASC`, - ); + .orderBy( + sql`DATE(${tables.log.createdAt} AT TIME ZONE 'UTC') ASC, ${tables.log.usedModel} ASC, ${tables.log.usedProvider} ASC`, + );
130-211: Add supporting indexes to keep these aggregations fast at scaleRecommend composite indexes:
- logs(project_id, created_at)
- logs(project_id, created_at, used_model, used_provider)
This will speed up both WHERE and GROUP BY for the new queries.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
apps/api/src/routes/activity.ts(2 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
{apps/api,apps/gateway,apps/ui,apps/docs,packages}/**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Always use top-level import; never use require() or dynamic imports (e.g., import(), next/dynamic)
Files:
apps/api/src/routes/activity.ts
{apps/api,apps/gateway,packages/db}/**/*.ts
📄 CodeRabbit inference engine (AGENTS.md)
.findMany() or db().query.
{apps/api,apps/gateway,packages/db}/**/*.ts: Use Drizzle ORM with the latest object syntax for database access
For reads, use db().query..findFirst() Files:
apps/api/src/routes/activity.ts**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}: Never useanyoras anyin this TypeScript project unless absolutely necessary
Always use top-levelimport; never userequireor dynamic importsFiles:
apps/api/src/routes/activity.ts{apps/api,apps/gateway}/src/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
For reads, use db().query.
.findMany() or db().query.
.findFirst() Files:
apps/api/src/routes/activity.ts🧬 Code graph analysis (1)
apps/api/src/routes/activity.ts (2)
packages/db/src/db.ts (1)
db(13-17)packages/db/src/index.ts (1)
tables(13-15)⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
- GitHub Check: build / run
- GitHub Check: test / run
- GitHub Check: lint / run
- GitHub Check: generate / run
- GitHub Check: autofix
🔇 Additional comments (2)
apps/api/src/routes/activity.ts (2)
5-5: LGTM on importsTop-level imports look correct and align with project guidelines.
233-256: Verify day coverage and rate units
- This returns only days that have logs. If the UI expects a contiguous series with zero-filled days, we need to fill gaps.
- Confirm whether errorRate/cacheRate should be 0–100 (%) as implemented or 0–1 (fraction).
- Replace raw log fetching and in-memory aggregation with SQL-level aggregation - Aggregate daily activity and model breakdown data directly in the database - Simplify data processing by mapping aggregated results to response format - Improve performance and reduce memory usage for activity data queries Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
- Added comprehensive test for `/activity` endpoint to validate token aggregation and daily statistics. - Ensured proper handling of input/output tokens, total cost, and daily data breakdown in the response. - Fixed type inconsistency in `activity.ts` by converting string fields to numbers.
3ebcb4e to
cc6716a
Compare
- Replaced `typeof` with `z.infer` for improved type consistency.
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/api/src/routes/activity.ts (1)
46-50: Stricter validation fordays: don’t accept strings like "7d"
parseInt("7d", 10) -> 7, which passes.int().positive(). Use a digit‑only regex or z.coerce.number with refinement to reject junk.- days: z - .string() - .transform((val) => parseInt(val, 10)) - .pipe(z.number().int().positive()), + days: z.string().regex(/^\d+$/).transform((val) => Number(val)),
🧹 Nitpick comments (5)
apps/api/src/routes/activity.spec.ts (2)
249-392: Make the test deterministic: freeze time to avoid date-window flakinessThe test derives “today/yesterday” dynamically while the API groups by DATE(). Around midnight or with TZ differences this can flake. Freeze the clock.
Apply within this test:
test("GET /activity should correctly aggregate token counts", async () => { - // Clear existing logs and insert test data with known values + // Freeze time for deterministic DATE() bucketing + vi.useFakeTimers(); + vi.setSystemTime(new Date("2024-01-01T12:00:00Z")); + + // Clear existing logs and insert test data with known values await db.delete(tables.log); @@ - }); + vi.useRealTimers(); + });Also ensure vi is imported:
// at top-level import import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
349-368: Avoidanyin TS tests: type daily entriesReplace
anywith a minimal type to comply with repo guidelines and improve safety.-const totalRequests = data.activity.reduce( - (sum: number, day: any) => sum + day.requestCount, +type ActivityDay = { + requestCount: number; + totalTokens: number; + inputTokens: number; + outputTokens: number; + cost: number; +}; + +const totalRequests = data.activity.reduce( + (sum: number, day: ActivityDay) => sum + day.requestCount, 0, ); -const totalTokens = data.activity.reduce( - (sum: number, day: any) => sum + day.totalTokens, +const totalTokens = data.activity.reduce( + (sum: number, day: ActivityDay) => sum + day.totalTokens, 0, ); -const totalInputTokens = data.activity.reduce( - (sum: number, day: any) => sum + day.inputTokens, +const totalInputTokens = data.activity.reduce( + (sum: number, day: ActivityDay) => sum + day.inputTokens, 0, ); -const totalOutputTokens = data.activity.reduce( - (sum: number, day: any) => sum + day.outputTokens, +const totalOutputTokens = data.activity.reduce( + (sum: number, day: ActivityDay) => sum + day.outputTokens, 0, ); -const totalCost = data.activity.reduce( - (sum: number, day: any) => sum + day.cost, +const totalCost = data.activity.reduce( + (sum: number, day: ActivityDay) => sum + day.cost, 0, );apps/api/src/routes/activity.ts (3)
167-173: Normalize the date range to day boundaries to match day-level groupingCurrently
startDateis “now − days” at current time, causing a partial first day. Normalize to [startOfDay, endOfDay] to avoid off‑by‑one surprises.Add right after computing dates:
// Normalize to day boundaries startDate.setHours(0, 0, 0, 0); endDate.setHours(23, 59, 59, 999);
130-176: Project guideline alignment: prefer query API or a DB view for aggregatesGuidelines recommend
db().query.<table>.findMany()for reads. For complex aggregates, consider defining a SQL view inpackages/dband query it viadb.query.<view>.findMany()to keep call sites consistent.
167-176: Add covering indexes to support the new aggregationsTo keep scans fast under load, add:
- logs(project_id, created_at)
- logs(project_id, created_at, used_model, used_provider)
Optionally partial index if used_model/used_provider can be null. Consider concurrent creation and migration notes.
Also applies to: 200-205
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
apps/api/src/routes/activity.spec.ts(1 hunks)apps/api/src/routes/activity.ts(2 hunks)
🧰 Additional context used
📓 Path-based instructions (5)
{apps/api,apps/gateway,apps/ui,apps/docs,packages}/**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Always use top-level import; never use require() or dynamic imports (e.g., import(), next/dynamic)
Files:
apps/api/src/routes/activity.spec.tsapps/api/src/routes/activity.ts
{apps/api,apps/gateway,packages/db}/**/*.ts
📄 CodeRabbit inference engine (AGENTS.md)
{apps/api,apps/gateway,packages/db}/**/*.ts: Use Drizzle ORM with the latest object syntax for database access
For reads, use db().query..findMany() or db().query..findFirst()
Files:
apps/api/src/routes/activity.spec.tsapps/api/src/routes/activity.ts**/*.spec.ts
📄 CodeRabbit inference engine (AGENTS.md)
Place unit tests in files named *.spec.ts
Unit test files must be named with the .spec.ts suffix
Files:
apps/api/src/routes/activity.spec.ts**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}: Never useanyoras anyin this TypeScript project unless absolutely necessary
Always use top-levelimport; never userequireor dynamic importsFiles:
apps/api/src/routes/activity.spec.tsapps/api/src/routes/activity.ts{apps/api,apps/gateway}/src/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
For reads, use db().query.
.findMany() or db().query.
.findFirst() Files:
apps/api/src/routes/activity.spec.tsapps/api/src/routes/activity.ts🧬 Code graph analysis (2)
apps/api/src/routes/activity.spec.ts (3)
packages/db/src/db.ts (1)
db(13-17)packages/db/src/index.ts (1)
tables(13-15)apps/api/src/index.ts (1)
app(36-36)apps/api/src/routes/activity.ts (2)
packages/db/src/db.ts (1)
db(13-17)packages/db/src/index.ts (1)
tables(13-15)⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
- GitHub Check: build / run
- GitHub Check: test / run
- GitHub Check: generate / run
- GitHub Check: lint / run
- GitHub Check: autofix
🔇 Additional comments (2)
apps/api/src/routes/activity.spec.ts (1)
249-392: Nice E2E coverage for aggregationGood assertions on totals and per-day splits. Consider also asserting per‑day cost to fully cover the new cost aggregation.
apps/api/src/routes/activity.ts (1)
213-217: Fix Zod type extraction: use z.infer; schema._type isn’t public APIReplace internal
_typeusage withz.inferfor stability.-const modelBreakdownByDate = new Map< - string, - (typeof modelUsageSchema._type)[] ->(); +const modelBreakdownByDate = new Map<string, z.infer<typeof modelUsageSchema>[]>();
| date: sql<string>`DATE(${tables.log.createdAt})`.as("date"), | ||
| requestCount: sql<number>`COUNT(*)`.as("requestCount"), | ||
| inputTokens: | ||
| sql<number>`COALESCE(SUM(CAST(${tables.log.promptTokens} AS NUMERIC)), 0)`.as( | ||
| "inputTokens", | ||
| ), | ||
| outputTokens: | ||
| sql<number>`COALESCE(SUM(CAST(${tables.log.completionTokens} AS NUMERIC)), 0)`.as( | ||
| "outputTokens", | ||
| ), | ||
| totalTokens: | ||
| sql<number>`COALESCE(SUM(CAST(${tables.log.totalTokens} AS NUMERIC)), 0)`.as( | ||
| "totalTokens", | ||
| ), | ||
| cost: sql<number>`COALESCE(SUM(${tables.log.cost}), 0)`.as("cost"), | ||
| inputCost: sql<number>`COALESCE(SUM(${tables.log.inputCost}), 0)`.as( | ||
| "inputCost", | ||
| ), | ||
| outputCost: sql<number>`COALESCE(SUM(${tables.log.outputCost}), 0)`.as( | ||
| "outputCost", | ||
| ), | ||
| requestCost: sql<number>`COALESCE(SUM(${tables.log.requestCost}), 0)`.as( | ||
| "requestCost", | ||
| ), | ||
| errorCount: | ||
| sql<number>`SUM(CASE WHEN ${tables.log.hasError} = true THEN 1 ELSE 0 END)`.as( | ||
| "errorCount", | ||
| ), | ||
| cacheCount: | ||
| sql<number>`SUM(CASE WHEN ${tables.log.cached} = true THEN 1 ELSE 0 END)`.as( | ||
| "cacheCount", | ||
| ), | ||
| }) | ||
| .from(tables.log) | ||
| .where( | ||
| and( | ||
| inArray(tables.log.projectId, projectIds), | ||
| gte(tables.log.createdAt, startDate), | ||
| lte(tables.log.createdAt, endDate), | ||
| ), | ||
| ) | ||
| .groupBy(sql`DATE(${tables.log.createdAt})`) | ||
| .orderBy(sql`DATE(${tables.log.createdAt}) ASC`); | ||
|
|
There was a problem hiding this comment.
TZ-safe day bucketing: avoid DATE(timestamp) ambiguity
DATE(createdAt) is session-TZ dependent and can mis-bucket around midnight. Use a UTC-normalized day key consistently in SELECT/GROUP BY/ORDER BY.
- date: sql<string>`DATE(${tables.log.createdAt})`.as("date"),
+ date: sql<string>`to_char((${tables.log.createdAt} AT TIME ZONE 'UTC')::date, 'YYYY-MM-DD')`.as("date"),
@@
- .groupBy(sql`DATE(${tables.log.createdAt})`)
- .orderBy(sql`DATE(${tables.log.createdAt}) ASC`);
+ .groupBy(sql`to_char((${tables.log.createdAt} AT TIME ZONE 'UTC')::date, 'YYYY-MM-DD')`)
+ .orderBy(sql`to_char((${tables.log.createdAt} AT TIME ZONE 'UTC')::date, 'YYYY-MM-DD') ASC`);📝 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.
| date: sql<string>`DATE(${tables.log.createdAt})`.as("date"), | |
| requestCount: sql<number>`COUNT(*)`.as("requestCount"), | |
| inputTokens: | |
| sql<number>`COALESCE(SUM(CAST(${tables.log.promptTokens} AS NUMERIC)), 0)`.as( | |
| "inputTokens", | |
| ), | |
| outputTokens: | |
| sql<number>`COALESCE(SUM(CAST(${tables.log.completionTokens} AS NUMERIC)), 0)`.as( | |
| "outputTokens", | |
| ), | |
| totalTokens: | |
| sql<number>`COALESCE(SUM(CAST(${tables.log.totalTokens} AS NUMERIC)), 0)`.as( | |
| "totalTokens", | |
| ), | |
| cost: sql<number>`COALESCE(SUM(${tables.log.cost}), 0)`.as("cost"), | |
| inputCost: sql<number>`COALESCE(SUM(${tables.log.inputCost}), 0)`.as( | |
| "inputCost", | |
| ), | |
| outputCost: sql<number>`COALESCE(SUM(${tables.log.outputCost}), 0)`.as( | |
| "outputCost", | |
| ), | |
| requestCost: sql<number>`COALESCE(SUM(${tables.log.requestCost}), 0)`.as( | |
| "requestCost", | |
| ), | |
| errorCount: | |
| sql<number>`SUM(CASE WHEN ${tables.log.hasError} = true THEN 1 ELSE 0 END)`.as( | |
| "errorCount", | |
| ), | |
| cacheCount: | |
| sql<number>`SUM(CASE WHEN ${tables.log.cached} = true THEN 1 ELSE 0 END)`.as( | |
| "cacheCount", | |
| ), | |
| }) | |
| .from(tables.log) | |
| .where( | |
| and( | |
| inArray(tables.log.projectId, projectIds), | |
| gte(tables.log.createdAt, startDate), | |
| lte(tables.log.createdAt, endDate), | |
| ), | |
| ) | |
| .groupBy(sql`DATE(${tables.log.createdAt})`) | |
| .orderBy(sql`DATE(${tables.log.createdAt}) ASC`); | |
| date: sql<string>`to_char((${tables.log.createdAt} AT TIME ZONE 'UTC')::date, 'YYYY-MM-DD')`.as("date"), | |
| requestCount: sql<number>`COUNT(*)`.as("requestCount"), | |
| inputTokens: | |
| sql<number>`COALESCE(SUM(CAST(${tables.log.promptTokens} AS NUMERIC)), 0)`.as( | |
| "inputTokens", | |
| ), | |
| outputTokens: | |
| sql<number>`COALESCE(SUM(CAST(${tables.log.completionTokens} AS NUMERIC)), 0)`.as( | |
| "outputTokens", | |
| ), | |
| totalTokens: | |
| sql<number>`COALESCE(SUM(CAST(${tables.log.totalTokens} AS NUMERIC)), 0)`.as( | |
| "totalTokens", | |
| ), | |
| cost: sql<number>`COALESCE(SUM(${tables.log.cost}), 0)`.as("cost"), | |
| inputCost: sql<number>`COALESCE(SUM(${tables.log.inputCost}), 0)`.as( | |
| "inputCost", | |
| ), | |
| outputCost: sql<number>`COALESCE(SUM(${tables.log.outputCost}), 0)`.as( | |
| "outputCost", | |
| ), | |
| requestCost: sql<number>`COALESCE(SUM(${tables.log.requestCost}), 0)`.as( | |
| "requestCost", | |
| ), | |
| errorCount: | |
| sql<number>`SUM(CASE WHEN ${tables.log.hasError} = true THEN 1 ELSE 0 END)`.as( | |
| "errorCount", | |
| ), | |
| cacheCount: | |
| sql<number>`SUM(CASE WHEN ${tables.log.cached} = true THEN 1 ELSE 0 END)`.as( | |
| "cacheCount", | |
| ), | |
| }) | |
| .from(tables.log) | |
| .where( | |
| and( | |
| inArray(tables.log.projectId, projectIds), | |
| gte(tables.log.createdAt, startDate), | |
| lte(tables.log.createdAt, endDate), | |
| ), | |
| ) | |
| .groupBy(sql`to_char((${tables.log.createdAt} AT TIME ZONE 'UTC')::date, 'YYYY-MM-DD')`) | |
| .orderBy(sql`to_char((${tables.log.createdAt} AT TIME ZONE 'UTC')::date, 'YYYY-MM-DD') ASC`); |
🤖 Prompt for AI Agents
In apps/api/src/routes/activity.ts around lines 133 to 176, the query uses
DATE(${tables.log.createdAt}) which is session-timezone dependent and can
mis-bucket records around midnight; change the SELECT, GROUP BY and ORDER BY to
use a UTC-normalized day key (e.g., CAST(${tables.log.createdAt} AT TIME ZONE
'UTC' AS DATE) or date_trunc('day', timezone('UTC', ${tables.log.createdAt}))
cast to date) and reference that exact same expression everywhere so the day
bucketing is TZ-safe and consistent across SELECT, GROUP BY, and ORDER BY.
| .select({ | ||
| date: sql<string>`DATE(${tables.log.createdAt})`.as("date"), | ||
| usedModel: tables.log.usedModel, | ||
| usedProvider: tables.log.usedProvider, | ||
| requestCount: sql<number>`COUNT(*)`.as("requestCount"), | ||
| inputTokens: | ||
| sql<number>`COALESCE(SUM(CAST(${tables.log.promptTokens} AS NUMERIC)), 0)`.as( | ||
| "inputTokens", | ||
| ), | ||
| outputTokens: | ||
| sql<number>`COALESCE(SUM(CAST(${tables.log.completionTokens} AS NUMERIC)), 0)`.as( | ||
| "outputTokens", | ||
| ), | ||
| totalTokens: | ||
| sql<number>`COALESCE(SUM(CAST(${tables.log.totalTokens} AS NUMERIC)), 0)`.as( | ||
| "totalTokens", | ||
| ), | ||
| cost: sql<number>`COALESCE(SUM(${tables.log.cost}), 0)`.as("cost"), | ||
| }) | ||
| .from(tables.log) | ||
| .where( | ||
| and( | ||
| inArray(tables.log.projectId, projectIds), | ||
| gte(tables.log.createdAt, startDate), | ||
| lte(tables.log.createdAt, endDate), | ||
| ), | ||
| ) | ||
| .groupBy( | ||
| sql`DATE(${tables.log.createdAt}), ${tables.log.usedModel}, ${tables.log.usedProvider}`, | ||
| ) | ||
| .orderBy( | ||
| sql`DATE(${tables.log.createdAt}) ASC, ${tables.log.usedModel} ASC`, | ||
| ); |
There was a problem hiding this comment.
Apply the same UTC-normalized bucketing to model breakdowns
Keep the grouping key identical to daily aggregates to ensure joins by date don’t drift across TZ boundaries.
- date: sql<string>`DATE(${tables.log.createdAt})`.as("date"),
+ date: sql<string>`to_char((${tables.log.createdAt} AT TIME ZONE 'UTC')::date, 'YYYY-MM-DD')`.as("date"),
@@
- .groupBy(
- sql`DATE(${tables.log.createdAt}), ${tables.log.usedModel}, ${tables.log.usedProvider}`,
- )
- .orderBy(
- sql`DATE(${tables.log.createdAt}) ASC, ${tables.log.usedModel} ASC`,
- );
+ .groupBy(
+ sql`to_char((${tables.log.createdAt} AT TIME ZONE 'UTC')::date, 'YYYY-MM-DD'), ${tables.log.usedModel}, ${tables.log.usedProvider}`,
+ )
+ .orderBy(
+ sql`to_char((${tables.log.createdAt} AT TIME ZONE 'UTC')::date, 'YYYY-MM-DD') ASC, ${tables.log.usedModel} ASC`,
+ );📝 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.
| .select({ | |
| date: sql<string>`DATE(${tables.log.createdAt})`.as("date"), | |
| usedModel: tables.log.usedModel, | |
| usedProvider: tables.log.usedProvider, | |
| requestCount: sql<number>`COUNT(*)`.as("requestCount"), | |
| inputTokens: | |
| sql<number>`COALESCE(SUM(CAST(${tables.log.promptTokens} AS NUMERIC)), 0)`.as( | |
| "inputTokens", | |
| ), | |
| outputTokens: | |
| sql<number>`COALESCE(SUM(CAST(${tables.log.completionTokens} AS NUMERIC)), 0)`.as( | |
| "outputTokens", | |
| ), | |
| totalTokens: | |
| sql<number>`COALESCE(SUM(CAST(${tables.log.totalTokens} AS NUMERIC)), 0)`.as( | |
| "totalTokens", | |
| ), | |
| cost: sql<number>`COALESCE(SUM(${tables.log.cost}), 0)`.as("cost"), | |
| }) | |
| .from(tables.log) | |
| .where( | |
| and( | |
| inArray(tables.log.projectId, projectIds), | |
| gte(tables.log.createdAt, startDate), | |
| lte(tables.log.createdAt, endDate), | |
| ), | |
| ) | |
| .groupBy( | |
| sql`DATE(${tables.log.createdAt}), ${tables.log.usedModel}, ${tables.log.usedProvider}`, | |
| ) | |
| .orderBy( | |
| sql`DATE(${tables.log.createdAt}) ASC, ${tables.log.usedModel} ASC`, | |
| ); | |
| .select({ | |
| date: sql<string>`to_char((${tables.log.createdAt} AT TIME ZONE 'UTC')::date, 'YYYY-MM-DD')`.as("date"), | |
| usedModel: tables.log.usedModel, | |
| usedProvider: tables.log.usedProvider, | |
| requestCount: sql<number>`COUNT(*)`.as("requestCount"), | |
| inputTokens: | |
| sql<number>`COALESCE(SUM(CAST(${tables.log.promptTokens} AS NUMERIC)), 0)`.as( | |
| "inputTokens", | |
| ), | |
| outputTokens: | |
| sql<number>`COALESCE(SUM(CAST(${tables.log.completionTokens} AS NUMERIC)), 0)`.as( | |
| "outputTokens", | |
| ), | |
| totalTokens: | |
| sql<number>`COALESCE(SUM(CAST(${tables.log.totalTokens} AS NUMERIC)), 0)`.as( | |
| "totalTokens", | |
| ), | |
| cost: sql<number>`COALESCE(SUM(${tables.log.cost}), 0)`.as("cost"), | |
| }) | |
| .from(tables.log) | |
| .where( | |
| and( | |
| inArray(tables.log.projectId, projectIds), | |
| gte(tables.log.createdAt, startDate), | |
| lte(tables.log.createdAt, endDate), | |
| ), | |
| ) | |
| .groupBy( | |
| sql`to_char((${tables.log.createdAt} AT TIME ZONE 'UTC')::date, 'YYYY-MM-DD'), ${tables.log.usedModel}, ${tables.log.usedProvider}`, | |
| ) | |
| .orderBy( | |
| sql`to_char((${tables.log.createdAt} AT TIME ZONE 'UTC')::date, 'YYYY-MM-DD') ASC, ${tables.log.usedModel} ASC`, | |
| ); |
🤖 Prompt for AI Agents
In apps/api/src/routes/activity.ts around lines 179 to 211, the model-breakdown
query is grouping by DATE(${tables.log.createdAt}) which can differ from the
daily-aggregates date bucket when time zone normalization is applied; update
both the selected date and the GROUP BY key to use the same UTC-normalized date
expression used by the daily aggregates (e.g. DATE(${tables.log.createdAt} AT
TIME ZONE 'UTC') or the project’s canonical UTC normalization function) so the
grouping key matches exactly and joins by date won’t drift across time zones.
Summary
Changes
Backend API
/activityroute to query daily aggregated data from the logs table using SQL aggregation functionsCode Cleanup
Test plan
🌿 Generated by Terry
ℹ️ Tag @terragon-labs to ask questions and address PR feedback
📎 Task: https://www.terragonlabs.com/task/8cf41406-6317-4286-be78-157eb092c47e
Summary by CodeRabbit
New Features
Refactor
Tests