Skip to content

Commit 870ff16

Browse files
committed
Add freebuff access tier analytics
1 parent c7aa0e6 commit 870ff16

2 files changed

Lines changed: 98 additions & 14 deletions

File tree

web/src/app/api/v1/chat/completions/__tests__/completions.test.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { afterEach, beforeEach, describe, expect, mock, it } from 'bun:test'
22
import { NextRequest } from 'next/server'
33

4+
import { AnalyticsEvent } from '@codebuff/common/constants/analytics-events'
45
import { TEST_USER_ID } from '@codebuff/common/constants/paths'
56
import {
67
FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID,
@@ -626,6 +627,72 @@ describe('/api/v1/chat/completions POST endpoint', () => {
626627
FETCH_PATH_TEST_TIMEOUT_MS,
627628
)
628629

630+
it(
631+
'includes full freebuff access tier on successful usage analytics',
632+
async () => {
633+
const originalRandom = Math.random
634+
Math.random = () => 0
635+
try {
636+
const req = new NextRequest(
637+
'http://localhost:3000/api/v1/chat/completions',
638+
{
639+
method: 'POST',
640+
headers: allowedFreeModeHeaders('test-api-key-new-free'),
641+
body: JSON.stringify({
642+
model: 'minimax/minimax-m2.7',
643+
stream: false,
644+
codebuff_metadata: {
645+
run_id: 'run-free',
646+
client_id: 'test-client-id-123',
647+
cost_mode: 'free',
648+
},
649+
}),
650+
},
651+
)
652+
653+
const response = await postChatCompletionsForTest({
654+
req,
655+
getUserInfoFromApiKey: mockGetUserInfoFromApiKey,
656+
logger: mockLogger,
657+
trackEvent: mockTrackEvent,
658+
getUserUsageData: mockGetUserUsageData,
659+
getAgentRunFromId: mockGetAgentRunFromId,
660+
fetch: mockFetch,
661+
insertMessageBigquery: mockInsertMessageBigquery,
662+
loggerWithContext: mockLoggerWithContext,
663+
checkSessionAdmissible: mockCheckSessionAdmissibleAllow,
664+
})
665+
666+
expect(response.status).toBe(200)
667+
668+
const trackedEvents = (
669+
mockTrackEvent as ReturnType<typeof mock>
670+
).mock.calls.map(
671+
([params]) => params as Parameters<TrackEventFn>[0],
672+
)
673+
const requestEvent = trackedEvents.find(
674+
({ event }) => event === AnalyticsEvent.CHAT_COMPLETIONS_REQUEST,
675+
)
676+
const generationEvent = trackedEvents.find(
677+
({ event }) =>
678+
event === AnalyticsEvent.CHAT_COMPLETIONS_GENERATION_STARTED,
679+
)
680+
681+
expect(requestEvent?.properties).toMatchObject({
682+
freebuff: true,
683+
accessTier: 'full',
684+
})
685+
expect(generationEvent?.properties).toMatchObject({
686+
freebuff: true,
687+
accessTier: 'full',
688+
})
689+
} finally {
690+
Math.random = originalRandom
691+
}
692+
},
693+
FETCH_PATH_TEST_TIMEOUT_MS,
694+
)
695+
629696
it(
630697
'lets a BYOK free-tier new account through the paid-plan gate',
631698
async () => {
@@ -750,6 +817,19 @@ describe('/api/v1/chat/completions POST endpoint', () => {
750817
const body = await response.json()
751818
expect(body.error).toBe('session_model_mismatch')
752819
expect(checkSessionAdmissible).toHaveBeenCalledTimes(0)
820+
const validationEvent = (
821+
mockTrackEvent as ReturnType<typeof mock>
822+
).mock.calls
823+
.map(([params]) => params as Parameters<TrackEventFn>[0])
824+
.find(
825+
({ event, properties }) =>
826+
event === AnalyticsEvent.CHAT_COMPLETIONS_VALIDATION_ERROR &&
827+
properties?.error === 'session_model_mismatch',
828+
)
829+
expect(validationEvent?.properties).toMatchObject({
830+
freebuff: true,
831+
accessTier: 'limited',
832+
})
753833
})
754834

755835
it('classifies anonymized Cloudflare country codes as limited access', async () => {

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

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { AnalyticsEvent } from '@codebuff/common/constants/analytics-events'
22
import { BYOK_OPENROUTER_HEADER } from '@codebuff/common/constants/byok'
33
import {
4+
type FreebuffAccessTier,
45
FREEBUFF_GEMINI_PRO_MODEL_ID,
56
isFreebuffModelAllowedForAccessTier,
67
isSupportedFreebuffModelId,
@@ -293,7 +294,7 @@ export async function postChatCompletions(params: {
293294

294295
const userId = userInfo.id
295296
const stripeCustomerId = userInfo.stripe_customer_id ?? null
296-
let freebuffAccessTier: 'full' | 'limited' = 'full'
297+
let freebuffAccessTier: FreebuffAccessTier = 'full'
297298

298299
// Check if user is banned.
299300
// We use a clear, helpful message rather than a cryptic error because:
@@ -311,19 +312,6 @@ export async function postChatCompletions(params: {
311312
)
312313
}
313314

314-
// Track API request. Freebuff success-path analytics are sampled to keep
315-
// high-volume free traffic from dominating PostHog and log forwarding.
316-
trackSuccessEvent({
317-
event: AnalyticsEvent.CHAT_COMPLETIONS_REQUEST,
318-
userId,
319-
properties: {
320-
hasStream: !!bodyStream,
321-
hasRunId: !!runId,
322-
userInfo,
323-
},
324-
logger,
325-
})
326-
327315
// For free mode requests, classify the request into full or limited
328316
// access. Disallowed countries and anonymized networks are no longer
329317
// blocked outright; they are limited to the cheap DeepSeek Flash path.
@@ -338,6 +326,9 @@ export async function postChatCompletions(params: {
338326
env.FREEBUFF_DEV_FORCE_LIMITED,
339327
})
340328
freebuffAccessTier = getFreeModeAccessTier(countryAccess)
329+
trackEvent = withDefaultProperties(trackEvent, {
330+
accessTier: freebuffAccessTier,
331+
})
341332

342333
if (!countryAccess.allowed || sampleFreebuffSuccess) {
343334
logger.info(
@@ -369,6 +360,19 @@ export async function postChatCompletions(params: {
369360
}
370361
}
371362

363+
// Track API request. Freebuff success-path analytics are sampled to keep
364+
// high-volume free traffic from dominating PostHog and log forwarding.
365+
trackSuccessEvent({
366+
event: AnalyticsEvent.CHAT_COMPLETIONS_REQUEST,
367+
userId,
368+
properties: {
369+
hasStream: !!bodyStream,
370+
hasRunId: !!runId,
371+
userInfo,
372+
},
373+
logger,
374+
})
375+
372376
// Extract and validate agent run ID
373377
const runIdFromBody = typedBody.codebuff_metadata?.run_id
374378
if (!runIdFromBody || typeof runIdFromBody !== 'string') {

0 commit comments

Comments
 (0)