Skip to content

Commit 653d594

Browse files
committed
add analytics events to chat/completions
1 parent 0a080c3 commit 653d594

File tree

8 files changed

+833
-242
lines changed

8 files changed

+833
-242
lines changed

common/src/constants/analytics-events.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,13 +84,19 @@ export enum AnalyticsEvent {
8484
REFERRAL_BANNER_CLICKED = 'referral_banner.clicked',
8585

8686
// Web - API
87-
SSE_ENDPOINT_REQUEST = 'api.sse_endpoint_request',
8887
AGENT_RUN_API_REQUEST = 'api.agent_run_request',
8988
AGENT_RUN_CREATED = 'api.agent_run_created',
9089
AGENT_RUN_VALIDATION_ERROR = 'api.agent_run_validation_error',
9190
AGENT_RUN_CREATION_ERROR = 'api.agent_run_creation_error',
9291
ME_API_REQUEST = 'api.me_request',
9392
ME_VALIDATION_ERROR = 'api.me_validation_error',
93+
CHAT_COMPLETIONS_REQUEST = 'api.chat_completions_request',
94+
CHAT_COMPLETIONS_AUTH_ERROR = 'api.chat_completions_auth_error',
95+
CHAT_COMPLETIONS_VALIDATION_ERROR = 'api.chat_completions_validation_error',
96+
CHAT_COMPLETIONS_INSUFFICIENT_CREDITS = 'api.chat_completions_insufficient_credits',
97+
CHAT_COMPLETIONS_STREAM_STARTED = 'api.chat_completions_stream_started',
98+
CHAT_COMPLETIONS_STREAM_ERROR = 'api.chat_completions_stream_error',
99+
CHAT_COMPLETIONS_ERROR = 'api.chat_completions_error',
94100

95101
// Common
96102
FLUSH_FAILED = 'common.flush_failed',
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import type { Logger } from './logger'
2+
3+
export type GetUserUsageDataFn = (params: {
4+
userId: string
5+
logger: Logger
6+
}) => Promise<{
7+
balance: { totalRemaining: number }
8+
nextQuotaReset: string
9+
}>

common/src/types/contracts/database.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,26 @@ export type GetUserInfoFromApiKeyFn = <T extends UserColumn>(
2121
params: GetUserInfoFromApiKeyInput<T>,
2222
) => GetUserInfoFromApiKeyOutput<T>
2323

24+
type AgentRun = {
25+
agent_id: string
26+
status: 'running' | 'completed' | 'failed' | 'cancelled'
27+
}
28+
export type AgentRunColumn = keyof AgentRun
29+
export type GetAgentRunFromIdInput<T extends AgentRunColumn> = {
30+
agentRunId: string
31+
userId: string
32+
fields: readonly T[]
33+
}
34+
export type GetAgentRunFromIdOutput<T extends AgentRunColumn> = Promise<
35+
| {
36+
[K in T]: AgentRun[K]
37+
}
38+
| undefined
39+
>
40+
export type GetAgentRunFromIdFn = <T extends AgentRunColumn>(
41+
params: GetAgentRunFromIdInput<T>,
42+
) => GetAgentRunFromIdOutput<T>
43+
2444
/**
2545
* Fetch and validate an agent from the database by `publisher/agent-id[@version]` format
2646
*/

common/src/types/contracts/llm.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,9 @@ export type PromptAiSdkStructuredOutput<T> = Promise<T>
7878
export type PromptAiSdkStructuredFn = <T>(
7979
params: PromptAiSdkStructuredInput<T>,
8080
) => PromptAiSdkStructuredOutput<T>
81+
82+
export type HandleOpenRouterStreamFn = (params: {
83+
body: any
84+
userId: string
85+
agentId: string
86+
}) => Promise<ReadableStream>

knowledge.md

Lines changed: 116 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,15 @@ Codebuff is a tool for editing codebases via natural language instruction to Buf
3535

3636
## Tool Handling System
3737

38-
- Tools are defined in `backend/src/tools.ts` and implemented in `npm-app/src/tool-handlers.ts`
38+
- Tools are defined in `backend/src/tools/definitions/list.ts` and implemented in `npm-app/src/tool-handlers.ts`
3939
- Available tools: read_files, write_file, str_replace, run_terminal_command, code_search, browser_logs, spawn_agents, web_search, read_docs, run_file_change_hooks, and others
4040
- Backend uses tool calls to request additional information or perform actions
4141
- Client-side handles tool calls and sends results back to server
4242

4343
## Agent System
4444

45-
- **LLM-based Agents**: Traditional agents defined in `backend/src/templates/` using prompts and LLM models
46-
- **Programmatic Agents**: Custom agents using JavaScript/TypeScript generator functions in `.agents/templates/`
45+
- **LLM-based Agents**: Traditional agents defined in `.agents/` subdirectories using prompts and LLM models
46+
- **Programmatic Agents**: Custom agents using JavaScript/TypeScript generator functions in `.agents/`
4747
- **Dynamic Agent Templates**: User-defined agents in TypeScript files with `handleSteps` generator functions
4848
- Agent templates define available tools, spawnable sub-agents, and execution behavior
4949
- Programmatic agents allow complex orchestration logic, conditional flows, and iterative refinement
@@ -90,7 +90,6 @@ base-lite "fix this bug" # Works right away!
9090

9191
## Error Handling and Debugging
9292

93-
- The `debug.ts` file provides logging functionality for debugging
9493
- Error messages are logged to console and debug log files
9594
- WebSocket errors are caught and logged in server and client code
9695

@@ -101,166 +100,157 @@ base-lite "fix this bug" # Works right away!
101100
- User input is validated and sanitized before processing
102101
- File operations are restricted to project directory
103102

104-
## Testing Guidelines
103+
## API Endpoint Architecture
105104

106-
- Prefer specific imports over import \* to make dependencies explicit
107-
- Exception: When mocking modules with many internal dependencies (like isomorphic-git), use import \* to avoid listing every internal function
105+
### Dependency Injection Pattern
108106

109-
### Bun Testing Best Practices
107+
All API endpoints in `web/src/app/api/v1/` follow a consistent dependency injection pattern for improved testability and maintainability.
110108

111-
**Always use `spyOn()` instead of `mock.module()` for function and method mocking.**
109+
**Structure:**
112110

113-
- When mocking modules is required (for the purposes of overriding constants instead of functions), use the wrapper functions found in `@codebuff/common/testing/mock-modules.ts`.
114-
- `mockModule` is a drop-in replacement for `mock.module`, but the module should be the absolute module path (e.g., `@codebuff/common/db` instead of `../db`).
115-
- Make sure to call `clearMockedModules()` in `afterAll` to restore the original module implementations.
111+
1. **Implementation file** (`web/src/api/v1/<endpoint>.ts`) - Contains business logic with injected dependencies
112+
2. **Route handler** (`web/src/app/api/v1/<endpoint>/route.ts`) - Minimal wrapper that injects dependencies
113+
3. **Contract types** (`common/src/types/contracts/<domain>.ts`) - Type definitions for injected functions
114+
4. **Unit tests** (`web/src/api/v1/__tests__/<endpoint>.test.ts`) - Comprehensive tests with mocked dependencies
116115

117-
**Preferred approach:**
116+
**Example:**
118117

119118
```typescript
120-
// ✅ Good: Use spyOn for clear, explicit mocking
121-
import { spyOn, beforeEach, afterEach } from 'bun:test'
122-
import * as analytics from '../analytics'
123-
124-
beforeEach(() => {
125-
// Spy on module functions
126-
spyOn(analytics, 'trackEvent').mockImplementation(() => {})
127-
spyOn(analytics, 'initAnalytics').mockImplementation(() => {})
128-
129-
// Spy on global functions like Date.now and setTimeout
130-
spyOn(Date, 'now').mockImplementation(() => 1234567890)
131-
spyOn(global, 'setTimeout').mockImplementation((callback, delay) => {
132-
// Custom timeout logic for tests
133-
return 123 as any
134-
})
135-
})
119+
// Implementation file - Contains business logic
120+
export async function myEndpoint(params: {
121+
req: NextRequest
122+
getDependency: GetDependencyFn
123+
logger: Logger
124+
anotherDep: AnotherDepFn
125+
}) {
126+
// Business logic here
127+
}
136128

137-
afterEach(() => {
138-
// Restore all mocks
139-
mock.restore()
140-
})
129+
// Route handler - Minimal wrapper
130+
export async function GET(req: NextRequest) {
131+
return myEndpointGet({ req, getDependency, logger, anotherDep })
132+
}
133+
134+
// Contract type (in common/src/types/contracts/)
135+
export type GetDependencyFn = (params: SomeParams) => Promise<SomeResult>
141136
```
142137
143-
**Real examples from our codebase:**
138+
**Benefits:**
139+
140+
- Easy to mock dependencies in unit tests
141+
- Type-safe function contracts shared across the codebase
142+
- Clear separation between routing and business logic
143+
- Consistent pattern across all endpoints
144+
145+
**Contract Types Location:**
146+
All contract types live in `common/src/types/contracts/`.
147+
148+
**Contract Type Pattern:**
149+
For generic function types, use separate Input/Output types:
144150
145151
```typescript
146-
// From main-prompt.test.ts - Mocking LLM APIs
147-
agentRuntimeImpl.promptAiSdk = async function () {
148-
return 'Test response'
152+
// Define input type
153+
export type MyFunctionInput<T> = {
154+
param1: string
155+
param2: T
149156
}
150-
agentRuntimeImpl.promptAiSdkStream = async function* () {
151-
yield { type: 'text' as const, text: 'Test response' }
152-
return 'mock-message-id'
157+
158+
// Define output type
159+
export type MyFunctionOutput<T> = Promise<SomeResult<T>>
160+
161+
// Define function type using Input/Output
162+
export type MyFunctionFn = <T>(
163+
params: MyFunctionInput<T>,
164+
) => MyFunctionOutput<T>
165+
```
166+
167+
## Testing Guidelines
168+
169+
### Dependency Injection (Primary Approach)
170+
171+
**Prefer dependency injection over mocking.** Design functions to accept dependencies as parameters with contract types defined in `common/src/types/contracts/`.
172+
173+
```typescript
174+
// ✅ Good: Dependency injection with contract types
175+
import type { TrackEventFn } from '@codebuff/common/types/contracts/analytics'
176+
import type { Logger } from '@codebuff/common/types/contracts/logger'
177+
178+
export async function myFunction(params: {
179+
trackEvent: TrackEventFn
180+
logger: Logger
181+
getData: GetDataFn
182+
}) {
183+
const { trackEvent, logger, getData } = params
184+
// Use injected dependencies
153185
}
154186

155-
// From rage-detector.test.ts - Mocking Date
156-
spyOn(Date, 'now').mockImplementation(() => currentTime)
157-
158-
// From run-agent-step-tools.test.ts - Mocking imported modules
159-
spyOn(websocketAction, 'requestFiles').mockImplementation(
160-
async (ws: any, paths: string[]) => {
161-
const results: Record<string, string | null> = {}
162-
paths.forEach((p) => {
163-
if (p === 'src/auth.ts') {
164-
results[p] = 'export function authenticate() { return true; }'
165-
} else {
166-
results[p] = null
167-
}
168-
})
169-
return results
170-
},
171-
)
187+
// Test with simple mock implementations
188+
const mockTrackEvent: TrackEventFn = mock(() => {})
189+
const mockLogger: Logger = {
190+
error: mock(() => {}),
191+
// ... other methods
192+
}
172193
```
173194

174-
**Use `mock.module()` only for entire module replacement:**
195+
**Benefits:**
196+
197+
- No need for `spyOn()` or `mock.module()`
198+
- Clear, type-safe dependencies
199+
- Easy to test with simple mock objects
200+
- Better code architecture and maintainability
201+
202+
### When to Use spyOn (Secondary Approach)
203+
204+
Use `spyOn()` only when dependency injection is impractical:
205+
206+
- Mocking global functions (Date.now, setTimeout)
207+
- Testing legacy code without DI
208+
- Overriding internal module behavior temporarily
175209

176210
```typescript
177-
// ✅ Good: Use mock.module for replacing entire modules
178-
mock.module('../util/logger', () => ({
179-
logger: {
180-
debug: () => {},
181-
error: () => {},
182-
info: () => {},
183-
warn: () => {},
184-
},
185-
withLoggerContext: async (context: any, fn: () => Promise<any>) => fn(),
186-
}))
187-
188-
// ✅ Good: Mock entire module with multiple exports using anonymous function
189-
mock.module('../services/api-client', () => ({
190-
fetchUserData: jest.fn().mockResolvedValue({ id: 1, name: 'Test User' }),
191-
updateUserProfile: jest.fn().mockResolvedValue({ success: true }),
192-
deleteUser: jest.fn().mockResolvedValue(true),
193-
ApiError: class MockApiError extends Error {
194-
constructor(
195-
message: string,
196-
public status: number,
197-
) {
198-
super(message)
199-
}
200-
},
201-
API_ENDPOINTS: {
202-
USERS: '/api/users',
203-
PROFILES: '/api/profiles',
204-
},
205-
}))
211+
// Use spyOn for globals
212+
spyOn(Date, 'now').mockImplementation(() => 1234567890)
206213
```
207214

208-
**Benefits of spyOn:**
215+
### Avoid mock.module()
216+
217+
**Never use `mock.module()` for functions.** It pollutes global state and carries over between test files.
209218

210-
- Easier to restore original functionality with `mock.restore()`
211-
- Clearer test isolation
212-
- Doesn't interfere with global state (mock.module carries over from test file to test file, which is super bad and unintuitive.)
213-
- Simpler debugging when mocks fail
219+
Only use for overriding module constants when absolutely necessary:
220+
221+
- Use wrapper functions in `@codebuff/common/testing/mock-modules.ts`
222+
- Call `clearMockedModules()` in `afterAll`
214223

215224
### Test Setup Patterns
216225

217-
**Extract duplicative mock state to `beforeEach` for cleaner tests:**
226+
Extract duplicative mock state to `beforeEach`:
218227

219228
```typescript
220-
// ✅ Good: Extract common mock objects to beforeEach
221229
describe('My Tests', () => {
222-
let mockFileContext: ProjectFileContext
223-
let mockAgentTemplate: DynamicAgentTemplate
230+
let mockLogger: Logger
231+
let mockTrackEvent: TrackEventFn
224232

225233
beforeEach(() => {
226-
// Setup common mock data
227-
mockFileContext = {
228-
projectRoot: '/test',
229-
cwd: '/test',
230-
// ... other properties
231-
}
232-
233-
mockAgentTemplate = {
234-
id: 'test-agent',
235-
version: '1.0.0',
236-
// ... other properties
234+
mockLogger = {
235+
error: mock(() => {}),
236+
warn: mock(() => {}),
237+
info: mock(() => {}),
238+
debug: mock(() => {}),
237239
}
240+
mockTrackEvent = mock(() => {})
238241
})
239242

240-
test('should work with mock data', () => {
241-
const agentTemplate = {
242-
'test-agent': {
243-
...mockAgentTemplate,
244-
handleSteps: 'custom function',
245-
} as any, // Use type assertion when needed
246-
}
243+
afterEach(() => {
244+
mock.restore()
245+
})
247246

248-
const fileContext = {
249-
...mockFileContext,
250-
agentTemplates: agentTemplate,
251-
}
252-
// ... test logic
247+
test('works with injected dependencies', async () => {
248+
await myFunction({ logger: mockLogger, trackEvent: mockTrackEvent })
249+
expect(mockTrackEvent).toHaveBeenCalled()
253250
})
254251
})
255252
```
256253

257-
**Benefits:**
258-
259-
- Reduces code duplication across tests
260-
- Makes tests more maintainable
261-
- Ensures consistent mock data structure
262-
- Easier to update mock data in one place
263-
264254
## Constants and Configuration
265255

266256
Important constants are centralized in `common/src/constants.ts`:

0 commit comments

Comments
 (0)