From e57826d6e020f9bf26fde4a7f3a7203a318f4bdf Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 26 Aug 2026 10:50:04 -0400 Subject: [PATCH] Session logs and usage logs should use seconds not ms --- server/db/pg/schema/schema.ts | 2 +- server/db/sqlite/schema/schema.ts | 2 +- server/lib/aiBudgetEnforcement.ts | 4 +++- server/private/routers/aiGateway/logAiSession.ts | 11 +++++------ server/routers/auditLogs/aiUsageAnalyticsShared.ts | 12 ++++++------ server/routers/auditLogs/queryAiSessionLog.ts | 4 ++-- .../routers/auditLogs/queryAiUsageFilterOptions.ts | 4 ++-- 7 files changed, 20 insertions(+), 19 deletions(-) diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts index 1a4013f0f..fdb3f2112 100644 --- a/server/db/pg/schema/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -1984,7 +1984,7 @@ export const aiSessionLog = pgTable( // were cut short at AI_SESSION_LOG_MAX_BODY_CHARS before storage. truncated: boolean("truncated").notNull().default(false), statusCode: integer("statusCode"), - createdAt: bigint("createdAt", { mode: "number" }).notNull() // epoch ms + createdAt: bigint("createdAt", { mode: "number" }).notNull() // epoch seconds }, (t) => [ index("idx_ai_session_log_org_created").on(t.orgId, t.createdAt), diff --git a/server/db/sqlite/schema/schema.ts b/server/db/sqlite/schema/schema.ts index 4d3a7482f..47853432e 100644 --- a/server/db/sqlite/schema/schema.ts +++ b/server/db/sqlite/schema/schema.ts @@ -1980,7 +1980,7 @@ export const aiSessionLog = sqliteTable( .notNull() .default(false), statusCode: integer("statusCode"), - createdAt: integer("createdAt").notNull() // epoch ms + createdAt: integer("createdAt").notNull() // epoch seconds }, (t) => [ index("idx_ai_session_log_org_created").on(t.orgId, t.createdAt), diff --git a/server/lib/aiBudgetEnforcement.ts b/server/lib/aiBudgetEnforcement.ts index 312d41297..104e11d21 100644 --- a/server/lib/aiBudgetEnforcement.ts +++ b/server/lib/aiBudgetEnforcement.ts @@ -580,6 +580,8 @@ export async function recordUsage(input: UsageRecordInput): Promise { ); } + const timestamp = Math.floor(Date.now() / 1000); + usageRecordBuffer.push({ orgId: input.orgId, providerId: input.providerId, @@ -597,7 +599,7 @@ export async function recordUsage(input: UsageRecordInput): Promise { totalTokens, costUsd: input.costUsd, estimated: usage.estimated, - createdAt: input.createdAt ?? Date.now() + createdAt: input.createdAt ?? timestamp }); // Flush immediately if buffer is full, otherwise schedule a flush diff --git a/server/private/routers/aiGateway/logAiSession.ts b/server/private/routers/aiGateway/logAiSession.ts index 47b0f039e..1532aa5c5 100644 --- a/server/private/routers/aiGateway/logAiSession.ts +++ b/server/private/routers/aiGateway/logAiSession.ts @@ -151,17 +151,14 @@ async function getRetentionDays(orgId: string): Promise { } export async function cleanUpOldLogs(orgId: string, retentionDays: number) { - // calculateCutoffTimestamp returns a seconds-epoch cutoff (built for - // requestAuditLog.timestamp), but aiSessionLog.createdAt is ms-epoch to - // match aiUsageRecords - convert before comparing. - const cutoffTimestampMs = calculateCutoffTimestamp(retentionDays) * 1000; + const cutoffTimestamp = calculateCutoffTimestamp(retentionDays) * 1000; try { await logsDb .delete(aiSessionLog) .where( and( - lt(aiSessionLog.createdAt, cutoffTimestampMs), + lt(aiSessionLog.createdAt, cutoffTimestamp), eq(aiSessionLog.orgId, orgId) ) ); @@ -243,6 +240,8 @@ export function logAiSession(data: { ); } + const timestamp = Math.floor(Date.now() / 1000); + sessionLogBuffer.push({ sessionId: data.sessionId, orgId: sanitizeString(data.orgId), @@ -270,7 +269,7 @@ export function logAiSession(data: { (normalizedRequestText?.truncated ?? false) || (normalizedResponseText?.truncated ?? false), statusCode: data.statusCode, - createdAt: Date.now() + createdAt: timestamp }); // Flush immediately if buffer is full, otherwise schedule a flush diff --git a/server/routers/auditLogs/aiUsageAnalyticsShared.ts b/server/routers/auditLogs/aiUsageAnalyticsShared.ts index ada94a9b9..af2b7b20e 100644 --- a/server/routers/auditLogs/aiUsageAnalyticsShared.ts +++ b/server/routers/auditLogs/aiUsageAnalyticsShared.ts @@ -18,7 +18,7 @@ export const aiUsageAnalyticsFiltersQuery = z.object({ .refine((val) => !isNaN(Date.parse(val)), { error: "timeStart must be a valid ISO date string" }) - .transform((val) => new Date(val).getTime()) + .transform((val) => Math.floor(new Date(val).getTime() / 1000)) .prefault(() => getSevenDaysAgo().toISOString()) .openapi({ type: "string", @@ -31,7 +31,7 @@ export const aiUsageAnalyticsFiltersQuery = z.object({ .refine((val) => !isNaN(Date.parse(val)), { error: "timeEnd must be a valid ISO date string" }) - .transform((val) => new Date(val).getTime()) + .transform((val) => Math.floor(new Date(val).getTime() / 1000)) .prefault(() => new Date().toISOString()) .openapi({ type: "string", @@ -122,12 +122,12 @@ export function buildAiUsageWhere( ); } -// Buckets createdAt (epoch ms) down to a per-day string, dialect-aware, same -// approach as the DATE_TRUNC/DATE branch in queryRequestAnalytics.ts. +// Buckets createdAt (epoch seconds) down to a per-day string, dialect-aware, +// same approach as the DATE_TRUNC/DATE branch in queryRequestAnalytics.ts. export function dayBucketExpr() { return driver === "pg" - ? sql`DATE_TRUNC('day', TO_TIMESTAMP(${aiUsageRecords.createdAt} / 1000.0))` - : sql`DATE(${aiUsageRecords.createdAt} / 1000, 'unixepoch')`; + ? sql`DATE_TRUNC('day', TO_TIMESTAMP(${aiUsageRecords.createdAt}))` + : sql`DATE(${aiUsageRecords.createdAt}, 'unixepoch')`; } export type DailyMetricRow = { diff --git a/server/routers/auditLogs/queryAiSessionLog.ts b/server/routers/auditLogs/queryAiSessionLog.ts index 24f0065f5..3d0309905 100644 --- a/server/routers/auditLogs/queryAiSessionLog.ts +++ b/server/routers/auditLogs/queryAiSessionLog.ts @@ -32,7 +32,7 @@ export const queryAiSessionLogsQuery = z.strictObject({ .refine((val) => !isNaN(Date.parse(val)), { error: "timeStart must be a valid ISO date string" }) - .transform((val) => new Date(val).getTime()) + .transform((val) => Math.floor(new Date(val).getTime() / 1000)) .prefault(() => getSevenDaysAgo().toISOString()) .openapi({ type: "string", @@ -45,7 +45,7 @@ export const queryAiSessionLogsQuery = z.strictObject({ .refine((val) => !isNaN(Date.parse(val)), { error: "timeEnd must be a valid ISO date string" }) - .transform((val) => new Date(val).getTime()) + .transform((val) => Math.floor(new Date(val).getTime() / 1000)) .optional() .prefault(() => new Date().toISOString()) .openapi({ diff --git a/server/routers/auditLogs/queryAiUsageFilterOptions.ts b/server/routers/auditLogs/queryAiUsageFilterOptions.ts index cd0f42cb6..bab6ed842 100644 --- a/server/routers/auditLogs/queryAiUsageFilterOptions.ts +++ b/server/routers/auditLogs/queryAiUsageFilterOptions.ts @@ -30,14 +30,14 @@ const queryAiUsageFilterOptionsQuery = z.object({ .refine((val) => !isNaN(Date.parse(val)), { error: "timeStart must be a valid ISO date string" }) - .transform((val) => new Date(val).getTime()) + .transform((val) => Math.floor(new Date(val).getTime() / 1000)) .prefault(() => getSevenDaysAgo().toISOString()), timeEnd: z .string() .refine((val) => !isNaN(Date.parse(val)), { error: "timeEnd must be a valid ISO date string" }) - .transform((val) => new Date(val).getTime()) + .transform((val) => Math.floor(new Date(val).getTime() / 1000)) .prefault(() => new Date().toISOString()) });