From 870ff16e7d390bd95cc20929baea0bcbdac44ccb Mon Sep 17 00:00:00 2001 From: James Grugett Date: Tue, 19 May 2026 11:45:58 -0700 Subject: [PATCH 1/2] Add freebuff access tier analytics --- .../completions/__tests__/completions.test.ts | 80 +++++++++++++++++++ web/src/app/api/v1/chat/completions/_post.ts | 32 ++++---- 2 files changed, 98 insertions(+), 14 deletions(-) diff --git a/web/src/app/api/v1/chat/completions/__tests__/completions.test.ts b/web/src/app/api/v1/chat/completions/__tests__/completions.test.ts index 80ca4f02d1..b64f440ee4 100644 --- a/web/src/app/api/v1/chat/completions/__tests__/completions.test.ts +++ b/web/src/app/api/v1/chat/completions/__tests__/completions.test.ts @@ -1,6 +1,7 @@ import { afterEach, beforeEach, describe, expect, mock, it } from 'bun:test' import { NextRequest } from 'next/server' +import { AnalyticsEvent } from '@codebuff/common/constants/analytics-events' import { TEST_USER_ID } from '@codebuff/common/constants/paths' import { FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID, @@ -626,6 +627,72 @@ describe('/api/v1/chat/completions POST endpoint', () => { FETCH_PATH_TEST_TIMEOUT_MS, ) + it( + 'includes full freebuff access tier on successful usage analytics', + async () => { + const originalRandom = Math.random + Math.random = () => 0 + try { + const req = new NextRequest( + 'http://localhost:3000/api/v1/chat/completions', + { + method: 'POST', + headers: allowedFreeModeHeaders('test-api-key-new-free'), + body: JSON.stringify({ + model: 'minimax/minimax-m2.7', + stream: false, + codebuff_metadata: { + run_id: 'run-free', + client_id: 'test-client-id-123', + cost_mode: 'free', + }, + }), + }, + ) + + const response = await postChatCompletionsForTest({ + req, + getUserInfoFromApiKey: mockGetUserInfoFromApiKey, + logger: mockLogger, + trackEvent: mockTrackEvent, + getUserUsageData: mockGetUserUsageData, + getAgentRunFromId: mockGetAgentRunFromId, + fetch: mockFetch, + insertMessageBigquery: mockInsertMessageBigquery, + loggerWithContext: mockLoggerWithContext, + checkSessionAdmissible: mockCheckSessionAdmissibleAllow, + }) + + expect(response.status).toBe(200) + + const trackedEvents = ( + mockTrackEvent as ReturnType + ).mock.calls.map( + ([params]) => params as Parameters[0], + ) + const requestEvent = trackedEvents.find( + ({ event }) => event === AnalyticsEvent.CHAT_COMPLETIONS_REQUEST, + ) + const generationEvent = trackedEvents.find( + ({ event }) => + event === AnalyticsEvent.CHAT_COMPLETIONS_GENERATION_STARTED, + ) + + expect(requestEvent?.properties).toMatchObject({ + freebuff: true, + accessTier: 'full', + }) + expect(generationEvent?.properties).toMatchObject({ + freebuff: true, + accessTier: 'full', + }) + } finally { + Math.random = originalRandom + } + }, + FETCH_PATH_TEST_TIMEOUT_MS, + ) + it( 'lets a BYOK free-tier new account through the paid-plan gate', async () => { @@ -750,6 +817,19 @@ describe('/api/v1/chat/completions POST endpoint', () => { const body = await response.json() expect(body.error).toBe('session_model_mismatch') expect(checkSessionAdmissible).toHaveBeenCalledTimes(0) + const validationEvent = ( + mockTrackEvent as ReturnType + ).mock.calls + .map(([params]) => params as Parameters[0]) + .find( + ({ event, properties }) => + event === AnalyticsEvent.CHAT_COMPLETIONS_VALIDATION_ERROR && + properties?.error === 'session_model_mismatch', + ) + expect(validationEvent?.properties).toMatchObject({ + freebuff: true, + accessTier: 'limited', + }) }) it('classifies anonymized Cloudflare country codes as limited access', async () => { diff --git a/web/src/app/api/v1/chat/completions/_post.ts b/web/src/app/api/v1/chat/completions/_post.ts index 8fb66930be..0a48fce0bc 100644 --- a/web/src/app/api/v1/chat/completions/_post.ts +++ b/web/src/app/api/v1/chat/completions/_post.ts @@ -1,6 +1,7 @@ import { AnalyticsEvent } from '@codebuff/common/constants/analytics-events' import { BYOK_OPENROUTER_HEADER } from '@codebuff/common/constants/byok' import { + type FreebuffAccessTier, FREEBUFF_GEMINI_PRO_MODEL_ID, isFreebuffModelAllowedForAccessTier, isSupportedFreebuffModelId, @@ -293,7 +294,7 @@ export async function postChatCompletions(params: { const userId = userInfo.id const stripeCustomerId = userInfo.stripe_customer_id ?? null - let freebuffAccessTier: 'full' | 'limited' = 'full' + let freebuffAccessTier: FreebuffAccessTier = 'full' // Check if user is banned. // We use a clear, helpful message rather than a cryptic error because: @@ -311,19 +312,6 @@ export async function postChatCompletions(params: { ) } - // Track API request. Freebuff success-path analytics are sampled to keep - // high-volume free traffic from dominating PostHog and log forwarding. - trackSuccessEvent({ - event: AnalyticsEvent.CHAT_COMPLETIONS_REQUEST, - userId, - properties: { - hasStream: !!bodyStream, - hasRunId: !!runId, - userInfo, - }, - logger, - }) - // For free mode requests, classify the request into full or limited // access. Disallowed countries and anonymized networks are no longer // blocked outright; they are limited to the cheap DeepSeek Flash path. @@ -338,6 +326,9 @@ export async function postChatCompletions(params: { env.FREEBUFF_DEV_FORCE_LIMITED, }) freebuffAccessTier = getFreeModeAccessTier(countryAccess) + trackEvent = withDefaultProperties(trackEvent, { + accessTier: freebuffAccessTier, + }) if (!countryAccess.allowed || sampleFreebuffSuccess) { logger.info( @@ -369,6 +360,19 @@ export async function postChatCompletions(params: { } } + // Track API request. Freebuff success-path analytics are sampled to keep + // high-volume free traffic from dominating PostHog and log forwarding. + trackSuccessEvent({ + event: AnalyticsEvent.CHAT_COMPLETIONS_REQUEST, + userId, + properties: { + hasStream: !!bodyStream, + hasRunId: !!runId, + userInfo, + }, + logger, + }) + // Extract and validate agent run ID const runIdFromBody = typedBody.codebuff_metadata?.run_id if (!runIdFromBody || typeof runIdFromBody !== 'string') { From d4e53f973cfa628dc00264c78bce0dc09d0f476f Mon Sep 17 00:00:00 2001 From: James Grugett Date: Tue, 19 May 2026 11:49:31 -0700 Subject: [PATCH 2/2] Use env helper for proxy lookup --- cli/src/utils/codebuff-api.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cli/src/utils/codebuff-api.ts b/cli/src/utils/codebuff-api.ts index 329f60f8f4..8300688c3a 100644 --- a/cli/src/utils/codebuff-api.ts +++ b/cli/src/utils/codebuff-api.ts @@ -1,4 +1,5 @@ import { WEBSITE_URL } from '@codebuff/sdk' +import { getSystemProcessEnv } from './env' import type { PublishAgentsResponse, @@ -208,7 +209,7 @@ export interface CodebuffApiClient { * Returns undefined when no proxy is configured. */ export function resolveProxyUrl( - env: Record = process.env, + env: Record = getSystemProcessEnv(), ): string | undefined { return ( env['HTTPS_PROXY'] ||