Skip to content

Commit cd79dcf

Browse files
committed
fix(billing): address PR review — re-throw errors, guard reserved keys, handle zero-cost counters
1 parent 58dd753 commit cd79dcf

File tree

1 file changed

+40
-42
lines changed

1 file changed

+40
-42
lines changed

apps/sim/lib/billing/core/usage-log.ts

Lines changed: 40 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -86,15 +86,22 @@ export async function recordUsage(params: RecordUsageParams): Promise<void> {
8686

8787
// Filter to entries with positive cost and derive total
8888
const validEntries = entries.filter((e) => e.cost > 0)
89-
if (validEntries.length === 0) {
89+
const totalCost = validEntries.reduce((sum, e) => sum + e.cost, 0)
90+
91+
// Nothing to write: no cost entries and no counter increments
92+
if (validEntries.length === 0 && !additionalStats) {
9093
return
9194
}
9295

93-
const totalCost = validEntries.reduce((sum, e) => sum + e.cost, 0)
96+
// Keys managed by recordUsage — callers must not override these via additionalStats
97+
const RESERVED_KEYS = new Set(['totalCost', 'currentPeriodCost', 'lastActive'])
98+
const safeStats = additionalStats
99+
? Object.fromEntries(Object.entries(additionalStats).filter(([k]) => !RESERVED_KEYS.has(k)))
100+
: undefined
94101

95-
try {
96-
await db.transaction(async (tx) => {
97-
// Step 1: Insert all usage_log entries
102+
await db.transaction(async (tx) => {
103+
// Step 1: Insert usage_log entries (only if there are positive-cost entries)
104+
if (validEntries.length > 0) {
98105
await tx.insert(usageLog).values(
99106
validEntries.map((entry) => ({
100107
id: crypto.randomUUID(),
@@ -109,48 +116,39 @@ export async function recordUsage(params: RecordUsageParams): Promise<void> {
109116
executionId: executionId ?? null,
110117
}))
111118
)
119+
}
112120

113-
// Step 2: Update userStats — core billing fields derived from entries
114-
const updateFields: Record<string, SQL | Date> = {
121+
// Step 2: Update userStats — core billing fields + source-specific counters
122+
const updateFields: Record<string, SQL | Date> = {
123+
lastActive: new Date(),
124+
...(totalCost > 0 && {
115125
totalCost: sql`total_cost + ${totalCost}`,
116126
currentPeriodCost: sql`current_period_cost + ${totalCost}`,
117-
lastActive: new Date(),
118-
// Merge any source-specific counter increments from the caller
119-
...additionalStats,
120-
}
121-
122-
const result = await tx
123-
.update(userStats)
124-
.set(updateFields)
125-
.where(eq(userStats.userId, userId))
126-
.returning({ userId: userStats.userId })
127+
}),
128+
...safeStats,
129+
}
127130

128-
if (result.length === 0) {
129-
logger.warn('recordUsage: userStats row not found, usage_log entries will roll back', {
130-
userId,
131-
totalCost,
132-
})
133-
throw new Error(`userStats row not found for userId: ${userId}`)
134-
}
135-
})
131+
const result = await tx
132+
.update(userStats)
133+
.set(updateFields)
134+
.where(eq(userStats.userId, userId))
135+
.returning({ userId: userStats.userId })
136136

137-
logger.debug('Recorded usage', {
138-
userId,
139-
totalCost,
140-
entryCount: validEntries.length,
141-
sources: [...new Set(validEntries.map((e) => e.source))],
142-
})
143-
} catch (error) {
144-
logger.error('Failed to record usage', {
145-
error: error instanceof Error ? error.message : String(error),
146-
userId,
147-
totalCost,
148-
entryCount: validEntries.length,
149-
})
150-
// Don't throw — the caller (execution logger, wand, copilot) decides whether to retry.
151-
// Critically, the transaction ensures we never have a state where currentPeriodCost
152-
// is incremented without corresponding usage_log entries.
153-
}
137+
if (result.length === 0) {
138+
logger.warn('recordUsage: userStats row not found, transaction will roll back', {
139+
userId,
140+
totalCost,
141+
})
142+
throw new Error(`userStats row not found for userId: ${userId}`)
143+
}
144+
})
145+
146+
logger.debug('Recorded usage', {
147+
userId,
148+
totalCost,
149+
entryCount: validEntries.length,
150+
sources: [...new Set(validEntries.map((e) => e.source))],
151+
})
154152
}
155153

156154
/**

0 commit comments

Comments
 (0)