From 891737e89ce9ccf251b6397d766687a94678e80c Mon Sep 17 00:00:00 2001 From: bkellam Date: Sat, 31 Jan 2026 20:55:29 -0800 Subject: [PATCH 1/4] wip --- packages/mcp/src/client.ts | 28 +++++++++ packages/mcp/src/index.ts | 1 - packages/mcp/src/schemas.ts | 1 - .../searchBar/useSuggestionsData.ts | 2 - .../app/[domain]/search/useStreamedSearch.ts | 2 +- packages/web/src/app/api/(client)/client.ts | 18 ++++++ .../web/src/app/api/(server)/chat/route.ts | 6 +- .../web/src/app/api/(server)/commits/route.ts | 5 +- .../src/app/api/(server)/ee/audit/route.ts | 5 +- .../web/src/app/api/(server)/ee/user/route.ts | 15 ++--- .../src/app/api/(server)/ee/users/route.ts | 11 ++-- .../web/src/app/api/(server)/files/route.ts | 5 +- .../api/(server)/find_definitions/route.ts | 5 +- .../app/api/(server)/find_references/route.ts | 5 +- .../web/src/app/api/(server)/health/route.ts | 5 +- .../(server)/repo-status/[repoId]/route.ts | 12 ++-- .../web/src/app/api/(server)/repos/route.ts | 10 +++- .../web/src/app/api/(server)/search/route.ts | 15 ++--- .../web/src/app/api/(server)/source/route.ts | 5 +- .../app/api/(server)/stream_search/route.ts | 9 +-- .../web/src/app/api/(server)/tree/route.ts | 5 +- .../web/src/app/api/(server)/version/route.ts | 5 +- .../[domain]/repos/[repoId]/image/route.ts | 12 ++-- .../components/chatBox/useSuggestionsData.ts | 1 - packages/web/src/features/search/types.ts | 1 - packages/web/src/lib/apiHandler.ts | 59 +++++++++++++++++++ packages/web/src/lib/posthogEvents.ts | 4 ++ 27 files changed, 188 insertions(+), 64 deletions(-) create mode 100644 packages/web/src/lib/apiHandler.ts diff --git a/packages/mcp/src/client.ts b/packages/mcp/src/client.ts index 036f251b6..a4be765bf 100644 --- a/packages/mcp/src/client.ts +++ b/packages/mcp/src/client.ts @@ -35,6 +35,7 @@ export const search = async (request: SearchRequest) => { method: 'POST', headers: { 'Content-Type': 'application/json', + 'X-Sourcebot-Client-Source': 'mcp', ...(env.SOURCEBOT_API_KEY ? { 'X-Sourcebot-Api-Key': env.SOURCEBOT_API_KEY } : {}) }, body: JSON.stringify(request) @@ -56,6 +57,7 @@ export const listRepos = async (queryParams: ListReposQueryParams = {}) => { method: 'GET', headers: { 'Content-Type': 'application/json', + 'X-Sourcebot-Client-Source': 'mcp', ...(env.SOURCEBOT_API_KEY ? { 'X-Sourcebot-Api-Key': env.SOURCEBOT_API_KEY } : {}) }, }); @@ -76,6 +78,7 @@ export const getFileSource = async (request: FileSourceRequest) => { const response = await fetch(url, { method: 'GET', headers: { + 'X-Sourcebot-Client-Source': 'mcp', ...(env.SOURCEBOT_API_KEY ? { 'X-Sourcebot-Api-Key': env.SOURCEBOT_API_KEY } : {}) }, }); @@ -95,6 +98,7 @@ export const listCommits = async (queryParams: ListCommitsQueryParamsSchema) => method: 'GET', headers: { 'X-Org-Domain': '~', + 'X-Sourcebot-Client-Source': 'mcp', ...(env.SOURCEBOT_API_KEY ? { 'X-Sourcebot-Api-Key': env.SOURCEBOT_API_KEY } : {}) }, }); @@ -103,3 +107,27 @@ export const listCommits = async (queryParams: ListCommitsQueryParamsSchema) => const totalCount = parseInt(response.headers.get('X-Total-Count') ?? '0', 10); return { commits, totalCount }; } +<<<<<<< Updated upstream +======= + +/** + * Asks a natural language question about the codebase using the Sourcebot AI agent. + * This is a blocking call that runs the full agent loop and returns when complete. + * + * @param request - The question and optional repo filters + * @returns The agent's answer, chat URL, sources, and metadata + */ +export const askCodebase = async (request: AskCodebaseRequest): Promise => { + const response = await fetch(`${env.SOURCEBOT_HOST}/api/chat/blocking`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Sourcebot-Client-Source': 'mcp', + ...(env.SOURCEBOT_API_KEY ? { 'X-Sourcebot-Api-Key': env.SOURCEBOT_API_KEY } : {}) + }, + body: JSON.stringify(request), + }); + + return parseResponse(response, askCodebaseResponseSchema); +} +>>>>>>> Stashed changes diff --git a/packages/mcp/src/index.ts b/packages/mcp/src/index.ts index e306d8670..2ab02fd67 100644 --- a/packages/mcp/src/index.ts +++ b/packages/mcp/src/index.ts @@ -101,7 +101,6 @@ server.tool( contextLines: env.DEFAULT_CONTEXT_LINES, isRegexEnabled: useRegex, isCaseSensitivityEnabled: caseSensitive, - source: 'mcp', }); if (response.files.length === 0) { diff --git a/packages/mcp/src/schemas.ts b/packages/mcp/src/schemas.ts index edd877741..a70e18fa5 100644 --- a/packages/mcp/src/schemas.ts +++ b/packages/mcp/src/schemas.ts @@ -31,7 +31,6 @@ export const searchOptionsSchema = z.object({ export const searchRequestSchema = z.object({ query: z.string(), // The zoekt query to execute. - source: z.string().optional(), // The source of the search request. ...searchOptionsSchema.shape, }); diff --git a/packages/web/src/app/[domain]/components/searchBar/useSuggestionsData.ts b/packages/web/src/app/[domain]/components/searchBar/useSuggestionsData.ts index b2a372462..92c9bb401 100644 --- a/packages/web/src/app/[domain]/components/searchBar/useSuggestionsData.ts +++ b/packages/web/src/app/[domain]/components/searchBar/useSuggestionsData.ts @@ -61,7 +61,6 @@ export const useSuggestionsData = ({ query: `file:${suggestionQuery}`, matches: 15, contextLines: 1, - source: 'search-bar-file-suggestions' }), select: (data): Suggestion[] => { if (isServiceError(data)) { @@ -82,7 +81,6 @@ export const useSuggestionsData = ({ query: `sym:${suggestionQuery.length > 0 ? suggestionQuery : ".*"}`, matches: 15, contextLines: 1, - source: 'search-bar-symbol-suggestions' }), select: (data): Suggestion[] => { if (isServiceError(data)) { diff --git a/packages/web/src/app/[domain]/search/useStreamedSearch.ts b/packages/web/src/app/[domain]/search/useStreamedSearch.ts index 181b8a62c..1d7c0999f 100644 --- a/packages/web/src/app/[domain]/search/useStreamedSearch.ts +++ b/packages/web/src/app/[domain]/search/useStreamedSearch.ts @@ -121,6 +121,7 @@ export const useStreamedSearch = ({ query, matches, contextLines, whole, isRegex method: 'POST', headers: { 'Content-Type': 'application/json', + 'X-Sourcebot-Client-Source': 'sourcebot-web-client', }, body: JSON.stringify({ query, @@ -129,7 +130,6 @@ export const useStreamedSearch = ({ query, matches, contextLines, whole, isRegex whole, isRegexEnabled, isCaseSensitivityEnabled, - source: 'sourcebot-web-client' } satisfies SearchRequest), signal: abortControllerRef.current.signal, }); diff --git a/packages/web/src/app/api/(client)/client.ts b/packages/web/src/app/api/(client)/client.ts index 1fa998d22..7cb2f95ce 100644 --- a/packages/web/src/app/api/(client)/client.ts +++ b/packages/web/src/app/api/(client)/client.ts @@ -25,6 +25,7 @@ export const search = async (body: SearchRequest): Promise response.json()); @@ -44,6 +45,9 @@ export const getFileSource = async (queryParams: FileSourceRequest): Promise response.json()); return result as FileSourceResponse | ServiceError; @@ -59,6 +63,7 @@ export const listRepos = async (queryParams: ListReposQueryParams): Promise response.json()); @@ -70,6 +75,7 @@ export const getVersion = async (): Promise => { method: "GET", headers: { "Content-Type": "application/json", + "X-Sourcebot-Client-Source": "sourcebot-web-client", }, }).then(response => response.json()); return result as GetVersionResponse; @@ -78,6 +84,9 @@ export const getVersion = async (): Promise => { export const findSearchBasedSymbolReferences = async (body: FindRelatedSymbolsRequest): Promise => { const result = await fetch("/api/find_references", { method: "POST", + headers: { + "X-Sourcebot-Client-Source": "sourcebot-web-client", + }, body: JSON.stringify(body), }).then(response => response.json()); return result as FindRelatedSymbolsResponse | ServiceError; @@ -86,6 +95,9 @@ export const findSearchBasedSymbolReferences = async (body: FindRelatedSymbolsRe export const findSearchBasedSymbolDefinitions = async (body: FindRelatedSymbolsRequest): Promise => { const result = await fetch("/api/find_definitions", { method: "POST", + headers: { + "X-Sourcebot-Client-Source": "sourcebot-web-client", + }, body: JSON.stringify(body), }).then(response => response.json()); return result as FindRelatedSymbolsResponse | ServiceError; @@ -94,6 +106,9 @@ export const findSearchBasedSymbolDefinitions = async (body: FindRelatedSymbolsR export const getTree = async (body: GetTreeRequest): Promise => { const result = await fetch("/api/tree", { method: "POST", + headers: { + "X-Sourcebot-Client-Source": "sourcebot-web-client", + }, body: JSON.stringify(body), }).then(response => response.json()); return result as GetTreeResponse | ServiceError; @@ -102,6 +117,9 @@ export const getTree = async (body: GetTreeRequest): Promise => { const result = await fetch("/api/files", { method: "POST", + headers: { + "X-Sourcebot-Client-Source": "sourcebot-web-client", + }, body: JSON.stringify(body), }).then(response => response.json()); return result as GetFilesResponse | ServiceError; diff --git a/packages/web/src/app/api/(server)/chat/route.ts b/packages/web/src/app/api/(server)/chat/route.ts index 2874c48f2..db16ea387 100644 --- a/packages/web/src/app/api/(server)/chat/route.ts +++ b/packages/web/src/app/api/(server)/chat/route.ts @@ -3,6 +3,7 @@ import { _getConfiguredLanguageModelsFull, _getAISDKLanguageModelAndOptions, upd import { createAgentStream } from "@/features/chat/agent"; import { additionalChatRequestParamsSchema, LanguageModelInfo, SBChatMessage, SearchScope } from "@/features/chat/types"; import { getAnswerPartFromAssistantMessage, getLanguageModelKey } from "@/features/chat/utils"; +import { apiHandler } from "@/lib/apiHandler"; import { ErrorCode } from "@/lib/errorCodes"; import { notFound, requestBodySchemaValidationError, serviceErrorResponse } from "@/lib/serviceError"; import { isServiceError } from "@/lib/utils"; @@ -22,6 +23,7 @@ import { } from "ai"; import { randomUUID } from "crypto"; import { StatusCodes } from "http-status-codes"; +import { NextRequest } from "next/server"; import { z } from "zod"; const logger = createLogger('chat-api'); @@ -33,7 +35,7 @@ const chatRequestSchema = z.object({ ...additionalChatRequestParamsSchema.shape, }) -export async function POST(req: Request) { +export const POST = apiHandler(async (req: NextRequest) => { const requestBody = await req.json(); const parsed = await chatRequestSchema.safeParseAsync(requestBody); if (!parsed.success) { @@ -102,7 +104,7 @@ export async function POST(req: Request) { } return response; -} +}); // eslint-disable-next-line @typescript-eslint/no-explicit-any const mergeStreamAsync = async (stream: StreamTextResult, writer: UIMessageStreamWriter, options: UIMessageStreamOptions = {}) => { diff --git a/packages/web/src/app/api/(server)/commits/route.ts b/packages/web/src/app/api/(server)/commits/route.ts index 9e3a8173c..18b4afb93 100644 --- a/packages/web/src/app/api/(server)/commits/route.ts +++ b/packages/web/src/app/api/(server)/commits/route.ts @@ -1,4 +1,5 @@ import { listCommits } from "@/features/git"; +import { apiHandler } from "@/lib/apiHandler"; import { buildLinkHeader } from "@/lib/pagination"; import { serviceErrorResponse, queryParamsSchemaValidationError } from "@/lib/serviceError"; import { isServiceError } from "@/lib/utils"; @@ -16,7 +17,7 @@ const listCommitsQueryParamsSchema = z.object({ perPage: z.coerce.number().int().positive().max(100).default(50), }); -export const GET = async (request: NextRequest): Promise => { +export const GET = apiHandler(async (request: NextRequest): Promise => { const rawParams = Object.fromEntries( Object.keys(listCommitsQueryParamsSchema.shape).map(key => [ key, @@ -61,4 +62,4 @@ export const GET = async (request: NextRequest): Promise => { if (linkHeader) headers.set('Link', linkHeader); return new Response(JSON.stringify(commits), { status: 200, headers }); -} +}); diff --git a/packages/web/src/app/api/(server)/ee/audit/route.ts b/packages/web/src/app/api/(server)/ee/audit/route.ts index 84d89f267..d5dec2e0b 100644 --- a/packages/web/src/app/api/(server)/ee/audit/route.ts +++ b/packages/web/src/app/api/(server)/ee/audit/route.ts @@ -1,13 +1,14 @@ 'use server'; import { fetchAuditRecords } from "@/ee/features/audit/actions"; +import { apiHandler } from "@/lib/apiHandler"; import { ErrorCode } from "@/lib/errorCodes"; import { serviceErrorResponse } from "@/lib/serviceError"; import { isServiceError } from "@/lib/utils"; import { getEntitlements } from "@sourcebot/shared"; import { StatusCodes } from "http-status-codes"; -export const GET = async () => { +export const GET = apiHandler(async () => { const entitlements = getEntitlements(); if (!entitlements.includes('audit')) { return serviceErrorResponse({ @@ -22,4 +23,4 @@ export const GET = async () => { return serviceErrorResponse(result); } return Response.json(result); -}; \ No newline at end of file +}); \ No newline at end of file diff --git a/packages/web/src/app/api/(server)/ee/user/route.ts b/packages/web/src/app/api/(server)/ee/user/route.ts index 539f4e7c2..8ca6ee807 100644 --- a/packages/web/src/app/api/(server)/ee/user/route.ts +++ b/packages/web/src/app/api/(server)/ee/user/route.ts @@ -1,19 +1,20 @@ 'use server'; +import { getAuditService } from "@/ee/features/audit/factory"; +import { apiHandler } from "@/lib/apiHandler"; +import { ErrorCode } from "@/lib/errorCodes"; +import { serviceErrorResponse, missingQueryParam, notFound } from "@/lib/serviceError"; +import { isServiceError } from "@/lib/utils"; import { withAuthV2, withMinimumOrgRole } from "@/withAuthV2"; import { OrgRole } from "@sourcebot/db"; -import { isServiceError } from "@/lib/utils"; -import { serviceErrorResponse, missingQueryParam, notFound } from "@/lib/serviceError"; import { createLogger } from "@sourcebot/shared"; -import { NextRequest } from "next/server"; import { StatusCodes } from "http-status-codes"; -import { ErrorCode } from "@/lib/errorCodes"; -import { getAuditService } from "@/ee/features/audit/factory"; +import { NextRequest } from "next/server"; const logger = createLogger('ee-user-api'); const auditService = getAuditService(); -export const DELETE = async (request: NextRequest) => { +export const DELETE = apiHandler(async (request: NextRequest) => { const url = new URL(request.url); const userId = url.searchParams.get('userId'); @@ -89,5 +90,5 @@ export const DELETE = async (request: NextRequest) => { } return Response.json(result, { status: StatusCodes.OK }); -}; +}); diff --git a/packages/web/src/app/api/(server)/ee/users/route.ts b/packages/web/src/app/api/(server)/ee/users/route.ts index e77edb18b..02c74d968 100644 --- a/packages/web/src/app/api/(server)/ee/users/route.ts +++ b/packages/web/src/app/api/(server)/ee/users/route.ts @@ -1,16 +1,17 @@ 'use server'; +import { getAuditService } from "@/ee/features/audit/factory"; +import { apiHandler } from "@/lib/apiHandler"; +import { serviceErrorResponse } from "@/lib/serviceError"; +import { isServiceError } from "@/lib/utils"; import { withAuthV2, withMinimumOrgRole } from "@/withAuthV2"; import { OrgRole } from "@sourcebot/db"; -import { isServiceError } from "@/lib/utils"; -import { serviceErrorResponse } from "@/lib/serviceError"; import { createLogger } from "@sourcebot/shared"; -import { getAuditService } from "@/ee/features/audit/factory"; const logger = createLogger('ee-users-api'); const auditService = getAuditService(); -export const GET = async () => { +export const GET = apiHandler(async () => { const result = await withAuthV2(async ({ prisma, org, role, user }) => { return withMinimumOrgRole(role, OrgRole.OWNER, async () => { try { @@ -77,5 +78,5 @@ export const GET = async () => { } return Response.json(result); -}; +}); diff --git a/packages/web/src/app/api/(server)/files/route.ts b/packages/web/src/app/api/(server)/files/route.ts index afc93cdd0..d054e300f 100644 --- a/packages/web/src/app/api/(server)/files/route.ts +++ b/packages/web/src/app/api/(server)/files/route.ts @@ -1,11 +1,12 @@ 'use server'; import { getFiles, getFilesRequestSchema } from "@/features/git/getFilesApi"; +import { apiHandler } from "@/lib/apiHandler"; import { requestBodySchemaValidationError, serviceErrorResponse } from "@/lib/serviceError"; import { isServiceError } from "@/lib/utils"; import { NextRequest } from "next/server"; -export const POST = async (request: NextRequest) => { +export const POST = apiHandler(async (request: NextRequest) => { const body = await request.json(); const parsed = await getFilesRequestSchema.safeParseAsync(body); if (!parsed.success) { @@ -18,5 +19,5 @@ export const POST = async (request: NextRequest) => { } return Response.json(response); -} +}); diff --git a/packages/web/src/app/api/(server)/find_definitions/route.ts b/packages/web/src/app/api/(server)/find_definitions/route.ts index 1ad524957..4a3d718a3 100644 --- a/packages/web/src/app/api/(server)/find_definitions/route.ts +++ b/packages/web/src/app/api/(server)/find_definitions/route.ts @@ -2,11 +2,12 @@ import { findSearchBasedSymbolDefinitions } from "@/features/codeNav/api"; import { findRelatedSymbolsRequestSchema } from "@/features/codeNav/types"; +import { apiHandler } from "@/lib/apiHandler"; import { requestBodySchemaValidationError, serviceErrorResponse } from "@/lib/serviceError"; import { isServiceError } from "@/lib/utils"; import { NextRequest } from "next/server"; -export const POST = async (request: NextRequest) => { +export const POST = apiHandler(async (request: NextRequest) => { const body = await request.json(); const parsed = await findRelatedSymbolsRequestSchema.safeParseAsync(body); if (!parsed.success) { @@ -19,4 +20,4 @@ export const POST = async (request: NextRequest) => { } return Response.json(response); -} \ No newline at end of file +}); \ No newline at end of file diff --git a/packages/web/src/app/api/(server)/find_references/route.ts b/packages/web/src/app/api/(server)/find_references/route.ts index a37ae8005..3fd6a1188 100644 --- a/packages/web/src/app/api/(server)/find_references/route.ts +++ b/packages/web/src/app/api/(server)/find_references/route.ts @@ -1,10 +1,11 @@ import { findSearchBasedSymbolReferences } from "@/features/codeNav/api"; import { findRelatedSymbolsRequestSchema } from "@/features/codeNav/types"; +import { apiHandler } from "@/lib/apiHandler"; import { requestBodySchemaValidationError, serviceErrorResponse } from "@/lib/serviceError"; import { isServiceError } from "@/lib/utils"; import { NextRequest } from "next/server"; -export const POST = async (request: NextRequest) => { +export const POST = apiHandler(async (request: NextRequest) => { const body = await request.json(); const parsed = await findRelatedSymbolsRequestSchema.safeParseAsync(body); if (!parsed.success) { @@ -17,4 +18,4 @@ export const POST = async (request: NextRequest) => { } return Response.json(response); -} \ No newline at end of file +}); \ No newline at end of file diff --git a/packages/web/src/app/api/(server)/health/route.ts b/packages/web/src/app/api/(server)/health/route.ts index 24b698764..37803c114 100644 --- a/packages/web/src/app/api/(server)/health/route.ts +++ b/packages/web/src/app/api/(server)/health/route.ts @@ -1,11 +1,12 @@ 'use server'; +import { apiHandler } from "@/lib/apiHandler"; import { createLogger } from "@sourcebot/shared"; const logger = createLogger('health-check'); -export async function GET() { +export const GET = apiHandler(async () => { logger.info('health check'); return Response.json({ status: 'ok' }); -} +}, { track: false }); diff --git a/packages/web/src/app/api/(server)/repo-status/[repoId]/route.ts b/packages/web/src/app/api/(server)/repo-status/[repoId]/route.ts index ecae9e3d5..3bd54bf9b 100644 --- a/packages/web/src/app/api/(server)/repo-status/[repoId]/route.ts +++ b/packages/web/src/app/api/(server)/repo-status/[repoId]/route.ts @@ -1,14 +1,14 @@ import { getRepoInfo } from "@/app/[domain]/askgh/[owner]/[repo]/api"; +import { apiHandler } from "@/lib/apiHandler"; import { serviceErrorResponse } from "@/lib/serviceError"; import { isServiceError } from "@/lib/utils"; import { NextRequest } from "next/server"; -export async function GET( +export const GET = apiHandler(async ( _request: NextRequest, - props: { params: Promise<{ repoId: string }> } -) { - const params = await props.params; - const { repoId } = params; + { params }: { params: Promise<{ repoId: string }> } +) => { + const { repoId } = await params; const repoIdNum = parseInt(repoId); if (isNaN(repoIdNum)) { @@ -22,4 +22,4 @@ export async function GET( } return Response.json(result); -} +}); diff --git a/packages/web/src/app/api/(server)/repos/route.ts b/packages/web/src/app/api/(server)/repos/route.ts index bf862a6f9..26ce3b09a 100644 --- a/packages/web/src/app/api/(server)/repos/route.ts +++ b/packages/web/src/app/api/(server)/repos/route.ts @@ -1,13 +1,19 @@ +<<<<<<< Updated upstream import { NextRequest } from "next/server"; import { sew } from "@/actions"; import { withOptionalAuthV2 } from "@/withAuthV2"; +======= +import { apiHandler } from "@/lib/apiHandler"; +import { buildLinkHeader } from "@/lib/pagination"; +import { listReposQueryParamsSchema } from "@/lib/schemas"; +>>>>>>> Stashed changes import { queryParamsSchemaValidationError, serviceErrorResponse } from "@/lib/serviceError"; import { isServiceError } from "@/lib/utils"; import { listReposQueryParamsSchema, repositoryQuerySchema } from "@/lib/schemas"; import { buildLinkHeader } from "@/lib/pagination"; import { getBrowsePath } from "@/app/[domain]/browse/hooks/utils"; -export const GET = async (request: NextRequest) => { +export const GET = apiHandler(async (request: NextRequest) => { const rawParams = Object.fromEntries( Object.keys(listReposQueryParamsSchema.shape).map(key => [ key, @@ -87,4 +93,4 @@ export const GET = async (request: NextRequest) => { if (linkHeader) headers.set('Link', linkHeader); return new Response(JSON.stringify(data), { status: 200, headers }); -}; +}); diff --git a/packages/web/src/app/api/(server)/search/route.ts b/packages/web/src/app/api/(server)/search/route.ts index a3fb565f6..d13aae3ba 100644 --- a/packages/web/src/app/api/(server)/search/route.ts +++ b/packages/web/src/app/api/(server)/search/route.ts @@ -1,12 +1,13 @@ 'use server'; import { search, searchRequestSchema } from "@/features/search"; +import { apiHandler } from "@/lib/apiHandler"; +import { captureEvent } from "@/lib/posthog"; +import { requestBodySchemaValidationError, serviceErrorResponse } from "@/lib/serviceError"; import { isServiceError } from "@/lib/utils"; import { NextRequest } from "next/server"; -import { requestBodySchemaValidationError, serviceErrorResponse } from "@/lib/serviceError"; -import { captureEvent } from "@/lib/posthog"; -export const POST = async (request: NextRequest) => { +export const POST = apiHandler(async (request: NextRequest) => { const body = await request.json(); const parsed = await searchRequestSchema.safeParseAsync(body); if (!parsed.success) { @@ -17,15 +18,15 @@ export const POST = async (request: NextRequest) => { const { query, - source, ...options } = parsed.data; + const source = request.headers.get('X-Sourcebot-Client-Source') ?? 'unknown'; await captureEvent('api_code_search_request', { - source: source ?? 'unknown', + source, type: 'blocking', }); - + const response = await search({ queryType: 'string', query, @@ -37,4 +38,4 @@ export const POST = async (request: NextRequest) => { } return Response.json(response); -} \ No newline at end of file +}); \ No newline at end of file diff --git a/packages/web/src/app/api/(server)/source/route.ts b/packages/web/src/app/api/(server)/source/route.ts index 095c3dc46..4e629aa09 100644 --- a/packages/web/src/app/api/(server)/source/route.ts +++ b/packages/web/src/app/api/(server)/source/route.ts @@ -1,6 +1,7 @@ 'use server'; import { getFileSource } from '@/features/git'; +import { apiHandler } from "@/lib/apiHandler"; import { queryParamsSchemaValidationError, serviceErrorResponse } from "@/lib/serviceError"; import { isServiceError } from "@/lib/utils"; import { NextRequest } from "next/server"; @@ -12,7 +13,7 @@ const querySchema = z.object({ ref: z.string().optional(), }); -export const GET = async (request: NextRequest) => { +export const GET = apiHandler(async (request: NextRequest) => { const rawParams = Object.fromEntries( Object.keys(querySchema.shape).map(key => [ key, @@ -39,4 +40,4 @@ export const GET = async (request: NextRequest) => { } return Response.json(response); -} +}); diff --git a/packages/web/src/app/api/(server)/stream_search/route.ts b/packages/web/src/app/api/(server)/stream_search/route.ts index d698517cb..4caeba5b4 100644 --- a/packages/web/src/app/api/(server)/stream_search/route.ts +++ b/packages/web/src/app/api/(server)/stream_search/route.ts @@ -1,12 +1,13 @@ 'use server'; import { streamSearch, searchRequestSchema } from '@/features/search'; +import { apiHandler } from '@/lib/apiHandler'; import { captureEvent } from '@/lib/posthog'; import { requestBodySchemaValidationError, serviceErrorResponse } from '@/lib/serviceError'; import { isServiceError } from '@/lib/utils'; import { NextRequest } from 'next/server'; -export const POST = async (request: NextRequest) => { +export const POST = apiHandler(async (request: NextRequest) => { const body = await request.json(); const parsed = await searchRequestSchema.safeParseAsync(body); @@ -16,12 +17,12 @@ export const POST = async (request: NextRequest) => { const { query, - source, ...options } = parsed.data; + const source = request.headers.get('X-Sourcebot-Client-Source') ?? 'unknown'; await captureEvent('api_code_search_request', { - source: source ?? 'unknown', + source, type: 'streamed', }); @@ -43,4 +44,4 @@ export const POST = async (request: NextRequest) => { 'X-Accel-Buffering': 'no', // Disable nginx buffering if applicable }, }); -}; +}); diff --git a/packages/web/src/app/api/(server)/tree/route.ts b/packages/web/src/app/api/(server)/tree/route.ts index decf72acb..79ffc5203 100644 --- a/packages/web/src/app/api/(server)/tree/route.ts +++ b/packages/web/src/app/api/(server)/tree/route.ts @@ -2,11 +2,12 @@ import { getTree } from "@/features/git/getTreeApi"; import { getTreeRequestSchema } from "@/features/git"; +import { apiHandler } from "@/lib/apiHandler"; import { requestBodySchemaValidationError, serviceErrorResponse } from "@/lib/serviceError"; import { isServiceError } from "@/lib/utils"; import { NextRequest } from "next/server"; -export const POST = async (request: NextRequest) => { +export const POST = apiHandler(async (request: NextRequest) => { const body = await request.json(); const parsed = await getTreeRequestSchema.safeParseAsync(body); if (!parsed.success) { @@ -19,5 +20,5 @@ export const POST = async (request: NextRequest) => { } return Response.json(response); -} +}); diff --git a/packages/web/src/app/api/(server)/version/route.ts b/packages/web/src/app/api/(server)/version/route.ts index 284ca160c..2d87ff3c2 100644 --- a/packages/web/src/app/api/(server)/version/route.ts +++ b/packages/web/src/app/api/(server)/version/route.ts @@ -1,4 +1,5 @@ import { SOURCEBOT_VERSION } from "@sourcebot/shared"; +import { apiHandler } from "@/lib/apiHandler"; import { GetVersionResponse } from "@/lib/types"; // Note: In Next.JS 14, GET methods with no params are cached by default at build time. @@ -8,8 +9,8 @@ import { GetVersionResponse } from "@/lib/types"; // @see: https://nextjs.org/docs/14/app/building-your-application/routing/route-handlers#caching export const dynamic = "force-dynamic"; -export const GET = async () => { +export const GET = apiHandler(async () => { return Response.json({ version: SOURCEBOT_VERSION, } satisfies GetVersionResponse); -} \ No newline at end of file +}); \ No newline at end of file diff --git a/packages/web/src/app/api/[domain]/repos/[repoId]/image/route.ts b/packages/web/src/app/api/[domain]/repos/[repoId]/image/route.ts index b235d5723..93afdf6ba 100644 --- a/packages/web/src/app/api/[domain]/repos/[repoId]/image/route.ts +++ b/packages/web/src/app/api/[domain]/repos/[repoId]/image/route.ts @@ -1,13 +1,13 @@ import { getRepoImage } from "@/actions"; +import { apiHandler } from "@/lib/apiHandler"; import { isServiceError } from "@/lib/utils"; import { NextRequest } from "next/server"; -export async function GET( +export const GET = apiHandler(async ( _request: NextRequest, - props: { params: Promise<{ domain: string; repoId: string }> } -) { - const params = await props.params; - const { repoId } = params; + { params }: { params: Promise<{ domain: string; repoId: string }> } +) => { + const { repoId } = await params; const repoIdNum = parseInt(repoId); if (isNaN(repoIdNum)) { @@ -25,4 +25,4 @@ export async function GET( 'Cache-Control': 'public, max-age=3600', }, }); -} \ No newline at end of file +}); \ No newline at end of file diff --git a/packages/web/src/features/chat/components/chatBox/useSuggestionsData.ts b/packages/web/src/features/chat/components/chatBox/useSuggestionsData.ts index d41367d5b..c1aeff8bf 100644 --- a/packages/web/src/features/chat/components/chatBox/useSuggestionsData.ts +++ b/packages/web/src/features/chat/components/chatBox/useSuggestionsData.ts @@ -47,7 +47,6 @@ export const useSuggestionsData = ({ query: query.join(' '), matches: 10, contextLines: 1, - source: 'chat-file-suggestions' })) }, select: (data): FileSuggestion[] => { diff --git a/packages/web/src/features/search/types.ts b/packages/web/src/features/search/types.ts index 9e9b9aaa4..e053c8e20 100644 --- a/packages/web/src/features/search/types.ts +++ b/packages/web/src/features/search/types.ts @@ -95,7 +95,6 @@ export type SearchOptions = z.infer; export const searchRequestSchema = z.object({ query: z.string(), // The zoekt query to execute. - source: z.string().optional(), // The source of the search request. ...searchOptionsSchema.shape, }); export type SearchRequest = z.infer; diff --git a/packages/web/src/lib/apiHandler.ts b/packages/web/src/lib/apiHandler.ts new file mode 100644 index 000000000..e41a3ae21 --- /dev/null +++ b/packages/web/src/lib/apiHandler.ts @@ -0,0 +1,59 @@ +import { NextRequest } from 'next/server'; +import { captureEvent } from './posthog'; + +interface ApiHandlerConfig { + /** + * Whether to track this API request in PostHog. + * @default true + */ + track?: boolean; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type AnyHandler = (...args: any[]) => Promise | Response; + +/** + * Creates an API route handler with automatic request tracking. + * + * @example + * // Simple handler + * export const GET = apiHandler(async (request) => { + * return Response.json({ data: 'hello' }); + * }); + * + * @example + * // Handler with route params + * export const GET = apiHandler(async (request, { params }) => { + * const { id } = await params; + * return Response.json({ id }); + * }); + * + * @example + * // Disable tracking (for health checks, etc.) + * export const GET = apiHandler(async () => { + * return Response.json({ status: 'ok' }); + * }, { track: false }); + */ +export function apiHandler( + handler: H, + config: ApiHandlerConfig = {} +): H { + const { track = true } = config; + + const wrappedHandler = async (request: NextRequest, ...rest: unknown[]) => { + if (track) { + const path = request.nextUrl.pathname; + const source = request.headers.get('X-Sourcebot-Client-Source') ?? 'unknown'; + + // Fire and forget - don't await to avoid blocking the request + captureEvent('api_request', { path, source }).catch(() => { + // Silently ignore tracking errors + }); + } + + // Call the original handler with all arguments + return handler(request, ...rest); + }; + + return wrappedHandler as H; +} diff --git a/packages/web/src/lib/posthogEvents.ts b/packages/web/src/lib/posthogEvents.ts index ea64f88ee..74e0d6473 100644 --- a/packages/web/src/lib/posthogEvents.ts +++ b/packages/web/src/lib/posthogEvents.ts @@ -192,5 +192,9 @@ export type PosthogEventMap = { source: string; type: 'streamed' | 'blocking'; }, + api_request: { + path: string; + source: string; + }, } export type PosthogEvent = keyof PosthogEventMap; \ No newline at end of file From 2faf278219ba4dc2929c9d2061966aaba5dfcc65 Mon Sep 17 00:00:00 2001 From: bkellam Date: Sat, 31 Jan 2026 21:00:55 -0800 Subject: [PATCH 2/4] fix conflicts --- packages/mcp/src/client.ts | 26 +------------------ .../web/src/app/api/(server)/repos/route.ts | 13 +++------- 2 files changed, 5 insertions(+), 34 deletions(-) diff --git a/packages/mcp/src/client.ts b/packages/mcp/src/client.ts index a4be765bf..a98ab1363 100644 --- a/packages/mcp/src/client.ts +++ b/packages/mcp/src/client.ts @@ -106,28 +106,4 @@ export const listCommits = async (queryParams: ListCommitsQueryParamsSchema) => const commits = await parseResponse(response, listCommitsResponseSchema); const totalCount = parseInt(response.headers.get('X-Total-Count') ?? '0', 10); return { commits, totalCount }; -} -<<<<<<< Updated upstream -======= - -/** - * Asks a natural language question about the codebase using the Sourcebot AI agent. - * This is a blocking call that runs the full agent loop and returns when complete. - * - * @param request - The question and optional repo filters - * @returns The agent's answer, chat URL, sources, and metadata - */ -export const askCodebase = async (request: AskCodebaseRequest): Promise => { - const response = await fetch(`${env.SOURCEBOT_HOST}/api/chat/blocking`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-Sourcebot-Client-Source': 'mcp', - ...(env.SOURCEBOT_API_KEY ? { 'X-Sourcebot-Api-Key': env.SOURCEBOT_API_KEY } : {}) - }, - body: JSON.stringify(request), - }); - - return parseResponse(response, askCodebaseResponseSchema); -} ->>>>>>> Stashed changes +} \ No newline at end of file diff --git a/packages/web/src/app/api/(server)/repos/route.ts b/packages/web/src/app/api/(server)/repos/route.ts index 26ce3b09a..5e93d8bcf 100644 --- a/packages/web/src/app/api/(server)/repos/route.ts +++ b/packages/web/src/app/api/(server)/repos/route.ts @@ -1,17 +1,12 @@ -<<<<<<< Updated upstream -import { NextRequest } from "next/server"; import { sew } from "@/actions"; -import { withOptionalAuthV2 } from "@/withAuthV2"; -======= +import { getBrowsePath } from "@/app/[domain]/browse/hooks/utils"; import { apiHandler } from "@/lib/apiHandler"; import { buildLinkHeader } from "@/lib/pagination"; -import { listReposQueryParamsSchema } from "@/lib/schemas"; ->>>>>>> Stashed changes +import { listReposQueryParamsSchema, repositoryQuerySchema } from "@/lib/schemas"; import { queryParamsSchemaValidationError, serviceErrorResponse } from "@/lib/serviceError"; import { isServiceError } from "@/lib/utils"; -import { listReposQueryParamsSchema, repositoryQuerySchema } from "@/lib/schemas"; -import { buildLinkHeader } from "@/lib/pagination"; -import { getBrowsePath } from "@/app/[domain]/browse/hooks/utils"; +import { withOptionalAuthV2 } from "@/withAuthV2"; +import { NextRequest } from "next/server"; export const GET = apiHandler(async (request: NextRequest) => { const rawParams = Object.fromEntries( From d13a25378df4f9b4c18581cb414c83c6bf7f075e Mon Sep 17 00:00:00 2001 From: bkellam Date: Sat, 31 Jan 2026 21:09:11 -0800 Subject: [PATCH 3/4] feedback --- packages/web/src/app/api/(server)/repos/route.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/web/src/app/api/(server)/repos/route.ts b/packages/web/src/app/api/(server)/repos/route.ts index 5e93d8bcf..c3bb47d3e 100644 --- a/packages/web/src/app/api/(server)/repos/route.ts +++ b/packages/web/src/app/api/(server)/repos/route.ts @@ -40,7 +40,12 @@ export const GET = apiHandler(async (request: NextRequest) => { orderBy: { [orderByField]: direction }, }), prisma.repo.count({ - where: { orgId: org.id }, + where: { + orgId: org.id, + ...(query ? { + name: { contains: query, mode: 'insensitive' }, + } : {}), + }, }), ]); From b93a65255cb24e7675a037b44e7ea5f3fe49df11 Mon Sep 17 00:00:00 2001 From: bkellam Date: Sat, 31 Jan 2026 21:17:46 -0800 Subject: [PATCH 4/4] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 78ec7003d..d7c5b6eed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Changed `/api/source` api to support fetching source code for any revision, not just revisions that are indexed by zoekt. [#829](https://github.com/sourcebot-dev/sourcebot/pull/829) +- Added additional telemetry for api requests. [#835](https://github.com/sourcebot-dev/sourcebot/pull/835) ## [4.10.20] - 2026-01-28