Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 22 additions & 33 deletions apps/sim/lib/logs/execution/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -403,54 +403,43 @@ export class ExecutionLogger implements IExecutionLoggerService {
// Apply cost multiplier only to model costs, not base execution charge
const costToStore = costSummary.baseExecutionCharge + costSummary.modelCost * costMultiplier

// Upsert user stats record - insert if doesn't exist, update if it does
const { getFreeTierLimit } = await import('@/lib/billing/subscriptions/utils')
const defaultLimit = getFreeTierLimit()
const existing = await db.select().from(userStats).where(eq(userStats.userId, userId))
if (existing.length === 0) {
logger.error('User stats record not found - should be created during onboarding', {
userId,
trigger,
})
return
}

const updateFields: any = {
totalTokensUsed: sql`total_tokens_used + ${costSummary.totalTokens}`,
totalCost: sql`total_cost + ${costToStore}`,
currentPeriodCost: sql`current_period_cost + ${costToStore}`,
lastActive: new Date(),
}

const triggerIncrements: any = {}
switch (trigger) {
case 'manual':
triggerIncrements.totalManualExecutions = sql`total_manual_executions + 1`
updateFields.totalManualExecutions = sql`total_manual_executions + 1`
break
case 'api':
triggerIncrements.totalApiCalls = sql`total_api_calls + 1`
updateFields.totalApiCalls = sql`total_api_calls + 1`
break
case 'webhook':
triggerIncrements.totalWebhookTriggers = sql`total_webhook_triggers + 1`
updateFields.totalWebhookTriggers = sql`total_webhook_triggers + 1`
break
case 'schedule':
triggerIncrements.totalScheduledExecutions = sql`total_scheduled_executions + 1`
updateFields.totalScheduledExecutions = sql`total_scheduled_executions + 1`
break
case 'chat':
triggerIncrements.totalChatExecutions = sql`total_chat_executions + 1`
updateFields.totalChatExecutions = sql`total_chat_executions + 1`
break
}

await db
.insert(userStats)
.values({
id: uuidv4(),
userId: userId,
currentUsageLimit: defaultLimit.toString(),
usageLimitUpdatedAt: new Date(),
totalTokensUsed: costSummary.totalTokens,
totalCost: costToStore,
currentPeriodCost: costToStore,
lastActive: new Date(),
...triggerIncrements,
})
.onConflictDoUpdate({
target: userStats.userId,
set: {
totalTokensUsed: sql`total_tokens_used + ${costSummary.totalTokens}`,
totalCost: sql`total_cost + ${costToStore}`,
currentPeriodCost: sql`current_period_cost + ${costToStore}`,
lastActive: new Date(),
...triggerIncrements,
},
})
await db.update(userStats).set(updateFields).where(eq(userStats.userId, userId))

logger.debug('Upserted user stats record with cost data', {
logger.debug('Updated user stats record with cost data', {
userId,
trigger,
addedCost: costToStore,
Expand Down
47 changes: 47 additions & 0 deletions packages/db/migrations/0091_backfill_user_stats.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
-- Backfill user_stats for any users missing a stats row
-- Uses defaults from schema for limits and counters

INSERT INTO "user_stats" (
"id",
"user_id",
"current_usage_limit",
"usage_limit_updated_at",
"total_manual_executions",
"total_api_calls",
"total_webhook_triggers",
"total_scheduled_executions",
"total_chat_executions",
"total_tokens_used",
"total_cost",
"current_period_cost",
"last_period_cost",
"total_copilot_cost",
"total_copilot_tokens",
"total_copilot_calls",
"last_active",
"billing_blocked"
)
SELECT
u."id" AS id,
u."id" AS user_id,
NULL::decimal AS current_usage_limit,
NOW() AS usage_limit_updated_at,
0 AS total_manual_executions,
0 AS total_api_calls,
0 AS total_webhook_triggers,
0 AS total_scheduled_executions,
0 AS total_chat_executions,
0 AS total_tokens_used,
'0'::decimal AS total_cost,
'0'::decimal AS current_period_cost,
'0'::decimal AS last_period_cost,
'0'::decimal AS total_copilot_cost,
Comment on lines +35 to +38
Copy link
Contributor

Choose a reason for hiding this comment

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

style: Inconsistent decimal casting - some use '0'::decimal while current_usage_limit uses NULL::decimal. Consider using consistent casting for all decimal fields.

0 AS total_copilot_tokens,
0 AS total_copilot_calls,
NOW() AS last_active,
FALSE AS billing_blocked
FROM "user" u
LEFT JOIN "user_stats" s ON s."user_id" = u."id"
WHERE s."user_id" IS NULL;