Skip to content

Commit 8c29d8e

Browse files
committed
Include freebuff prop in chat completions events
1 parent eeebd1f commit 8c29d8e

File tree

3 files changed

+54
-35
lines changed

3 files changed

+54
-35
lines changed

cli/src/hooks/use-auth-state.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import type { User } from '../utils/auth'
1515
const setAuthLoggerContext = (params: { userId: string; email: string }) => {
1616
loggerContext.userId = params.userId
1717
loggerContext.userEmail = params.email
18-
identifyUser(params.userId, { email: params.email, is_freebuff: IS_FREEBUFF })
18+
identifyUser(params.userId, { email: params.email, freebuff: IS_FREEBUFF })
1919
}
2020

2121
const clearAuthLoggerContext = () => {

common/src/analytics.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { env, DEBUG_ANALYTICS } from '@codebuff/common/env'
33
import { createPostHogClient, type AnalyticsClient } from './analytics-core'
44
import { AnalyticsEvent } from './constants/analytics-events'
55

6+
import type { TrackEventFn } from '@codebuff/common/types/contracts/analytics'
67
import type { Logger } from '@codebuff/common/types/contracts/logger'
78

89
let client: AnalyticsClient | undefined
@@ -32,6 +33,18 @@ export async function flushAnalytics(logger?: Logger) {
3233
}
3334
}
3435

36+
export function withDefaultProperties(
37+
trackEventFn: TrackEventFn,
38+
defaultProperties: Record<string, unknown>,
39+
): TrackEventFn {
40+
return (params) => {
41+
trackEventFn({
42+
...params,
43+
properties: { ...defaultProperties, ...params.properties },
44+
})
45+
}
46+
}
47+
3548
export function trackEvent({
3649
event,
3750
userId,

web/src/app/api/v1/chat/completions/_post.ts

Lines changed: 40 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ import {
6565
OpenRouterError,
6666
} from '@/llm-api/openrouter'
6767
import { extractApiKeyFromHeader } from '@/util/auth'
68+
import { withDefaultProperties } from '@codebuff/common/analytics'
6869
import { checkFreeModeRateLimit } from './free-mode-rate-limiter'
6970

7071
const FREE_MODE_ALLOWED_COUNTRIES = new Set([
@@ -148,7 +149,6 @@ export async function postChatCompletions(params: {
148149
req,
149150
getUserInfoFromApiKey,
150151
loggerWithContext,
151-
trackEvent,
152152
getUserUsageData,
153153
getAgentRunFromId,
154154
fetch,
@@ -157,6 +157,7 @@ export async function postChatCompletions(params: {
157157
getUserPreferences,
158158
} = params
159159
let { logger } = params
160+
let { trackEvent } = params
160161

161162
try {
162163
// Parse request body
@@ -182,6 +183,12 @@ export async function postChatCompletions(params: {
182183
const bodyStream = typedBody.stream ?? false
183184
const runId = typedBody.codebuff_metadata?.run_id
184185

186+
// Check if the request is in FREE mode (costs 0 credits for allowed agent+model combos)
187+
const costMode = typedBody.codebuff_metadata?.cost_mode
188+
const isFreeModeRequest = isFreeMode(costMode)
189+
190+
trackEvent = withDefaultProperties(trackEvent, { freebuff: isFreeModeRequest })
191+
185192
// Extract and validate API key
186193
const apiKey = extractApiKeyFromHeader(req)
187194
if (!apiKey) {
@@ -249,10 +256,6 @@ export async function postChatCompletions(params: {
249256
logger,
250257
})
251258

252-
// Check if the request is in FREE mode (costs 0 credits for allowed agent+model combos)
253-
const costMode = typedBody.codebuff_metadata?.cost_mode
254-
const isFreeModeRequest = isFreeMode(costMode)
255-
256259
// For free mode requests, check if user is in US or Canada
257260
if (isFreeModeRequest) {
258261
const countryCode = getCountryCode(req)
@@ -288,35 +291,6 @@ export async function postChatCompletions(params: {
288291
)
289292
}
290293

291-
// Rate limit free mode requests
292-
const rateLimitResult = checkFreeModeRateLimit(userId)
293-
if (rateLimitResult.limited) {
294-
const retryAfterSeconds = Math.ceil(rateLimitResult.retryAfterMs / 1000)
295-
const resetTime = new Date(Date.now() + rateLimitResult.retryAfterMs).toISOString()
296-
const resetCountdown = formatQuotaResetCountdown(resetTime)
297-
298-
trackEvent({
299-
event: AnalyticsEvent.CHAT_COMPLETIONS_VALIDATION_ERROR,
300-
userId,
301-
properties: {
302-
error: 'free_mode_rate_limited',
303-
windowName: rateLimitResult.windowName,
304-
retryAfterSeconds,
305-
},
306-
logger,
307-
})
308-
309-
return NextResponse.json(
310-
{
311-
error: 'free_mode_rate_limited',
312-
message: `Free mode rate limit exceeded (${rateLimitResult.windowName} limit). Try again ${resetCountdown}.`,
313-
},
314-
{
315-
status: 429,
316-
headers: { 'Retry-After': String(retryAfterSeconds) },
317-
},
318-
)
319-
}
320294
}
321295

322296
// Extract and validate agent run ID
@@ -377,6 +351,38 @@ export async function postChatCompletions(params: {
377351
)
378352
}
379353

354+
// Rate limit free mode requests (after validation so invalid requests don't consume quota)
355+
if (isFreeModeRequest) {
356+
const rateLimitResult = checkFreeModeRateLimit(userId)
357+
if (rateLimitResult.limited) {
358+
const retryAfterSeconds = Math.ceil(rateLimitResult.retryAfterMs / 1000)
359+
const resetTime = new Date(Date.now() + rateLimitResult.retryAfterMs).toISOString()
360+
const resetCountdown = formatQuotaResetCountdown(resetTime)
361+
362+
trackEvent({
363+
event: AnalyticsEvent.CHAT_COMPLETIONS_VALIDATION_ERROR,
364+
userId,
365+
properties: {
366+
error: 'free_mode_rate_limited',
367+
windowName: rateLimitResult.windowName,
368+
retryAfterSeconds,
369+
},
370+
logger,
371+
})
372+
373+
return NextResponse.json(
374+
{
375+
error: 'free_mode_rate_limited',
376+
message: `Free mode rate limit exceeded (${rateLimitResult.windowName} limit). Try again ${resetCountdown}.`,
377+
},
378+
{
379+
status: 429,
380+
headers: { 'Retry-After': String(retryAfterSeconds) },
381+
},
382+
)
383+
}
384+
}
385+
380386
// For subscribers, ensure a block grant exists before processing the request.
381387
// This is done AFTER validation so malformed requests don't start a new 5-hour block.
382388
// When the function is provided, always include subscription credits in the balance:

0 commit comments

Comments
 (0)