wip on better error handling

This commit is contained in:
bkellam 2025-11-20 16:47:54 -08:00
parent 1d1205f471
commit 5c624cda4f
6 changed files with 61 additions and 30 deletions

View file

@ -48,6 +48,10 @@ export const sew = async <T>(fn: () => Promise<T>): Promise<T | ServiceError> =>
Sentry.captureException(e); Sentry.captureException(e);
logger.error(e); logger.error(e);
if (e instanceof ServiceErrorException) {
return e.serviceError;
}
if (e instanceof Error) { if (e instanceof Error) {
return unexpectedError(e.message); return unexpectedError(e.message);
} }

View file

@ -287,7 +287,7 @@ export const SearchBar = ({
indentWithTab={false} indentWithTab={false}
autoFocus={autoFocus ?? false} autoFocus={autoFocus ?? false}
/> />
<div className="flex flex-row items-center gap-1"> <div className="flex flex-row items-center gap-1 ml-1">
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<span> <span>

View file

@ -33,6 +33,7 @@ import { CodePreviewPanel } from "./codePreviewPanel";
import { FilterPanel } from "./filterPanel"; import { FilterPanel } from "./filterPanel";
import { useFilteredMatches } from "./filterPanel/useFilterMatches"; import { useFilteredMatches } from "./filterPanel/useFilterMatches";
import { SearchResultsPanel, SearchResultsPanelHandle } from "./searchResultsPanel"; import { SearchResultsPanel, SearchResultsPanelHandle } from "./searchResultsPanel";
import { ServiceErrorException } from "@/lib/serviceError";
interface SearchResultsPageProps { interface SearchResultsPageProps {
searchQuery: string; searchQuery: string;
@ -79,7 +80,7 @@ export const SearchResultsPage = ({
useEffect(() => { useEffect(() => {
if (error) { if (error) {
toast({ toast({
description: `❌ Search failed. Reason: ${error.message}`, description: `❌ Search failed. Reason: ${error instanceof ServiceErrorException ? error.serviceError.message : error.message}`,
}); });
} }
}, [error, toast]); }, [error, toast]);
@ -184,7 +185,7 @@ export const SearchResultsPage = ({
<div className="flex flex-col items-center justify-center h-full gap-2"> <div className="flex flex-col items-center justify-center h-full gap-2">
<AlertTriangleIcon className="h-6 w-6" /> <AlertTriangleIcon className="h-6 w-6" />
<p className="font-semibold text-center">Failed to search</p> <p className="font-semibold text-center">Failed to search</p>
<p className="text-sm text-center">{error.message}</p> <p className="text-sm text-center">{error instanceof ServiceErrorException ? error.serviceError.message : error.message}</p>
</div> </div>
) : ( ) : (
<PanelGroup <PanelGroup

View file

@ -1,8 +1,10 @@
'use client'; 'use client';
import { RepositoryInfo, SearchRequest, SearchResultFile, SearchStats, StreamedSearchResponse } from '@/features/search'; import { RepositoryInfo, SearchRequest, SearchResultFile, SearchStats, StreamedSearchResponse } from '@/features/search';
import { useState, useCallback, useRef, useEffect } from 'react'; import { ServiceErrorException } from '@/lib/serviceError';
import { isServiceError } from '@/lib/utils';
import * as Sentry from '@sentry/nextjs'; import * as Sentry from '@sentry/nextjs';
import { useCallback, useEffect, useRef, useState } from 'react';
interface CacheEntry { interface CacheEntry {
files: SearchResultFile[]; files: SearchResultFile[];
@ -132,6 +134,14 @@ export const useStreamedSearch = ({ query, matches, contextLines, whole, isRegex
}); });
if (!response.ok) { if (!response.ok) {
// Check if this is a service error response
const contentType = response.headers.get('content-type');
if (contentType?.includes('application/json')) {
const errorData = await response.json();
if (isServiceError(errorData)) {
throw new ServiceErrorException(errorData);
}
}
throw new Error(`HTTP error! status: ${response.status}`); throw new Error(`HTTP error! status: ${response.status}`);
} }
@ -249,7 +259,7 @@ export const useStreamedSearch = ({ query, matches, contextLines, whole, isRegex
...prev, ...prev,
isStreaming: false, isStreaming: false,
timeToSearchCompletionMs, timeToSearchCompletionMs,
error: error as Error, error: error instanceof Error ? error : null,
})); }));
} }
} }

View file

@ -24,6 +24,9 @@ import {
import { parser as _parser } from '@sourcebot/query-language'; import { parser as _parser } from '@sourcebot/query-language';
import { PrismaClient } from '@sourcebot/db'; import { PrismaClient } from '@sourcebot/db';
import { SINGLE_TENANT_ORG_ID } from '@/lib/constants'; import { SINGLE_TENANT_ORG_ID } from '@/lib/constants';
import { ServiceErrorException } from '@/lib/serviceError';
import { StatusCodes } from 'http-status-codes';
import { ErrorCode } from '@/lib/errorCodes';
// Configure the parser to throw errors when encountering invalid syntax. // Configure the parser to throw errors when encountering invalid syntax.
const parser = _parser.configure({ const parser = _parser.configure({
@ -61,35 +64,47 @@ export const parseQuerySyntaxIntoIR = async ({
}, },
prisma: PrismaClient, prisma: PrismaClient,
}): Promise<QueryIR> => { }): Promise<QueryIR> => {
// First parse the query into a Lezer tree.
const tree = parser.parse(query);
// Then transform the tree into the intermediate representation. try {
return transformTreeToIR({ // First parse the query into a Lezer tree.
tree, const tree = parser.parse(query);
input: query,
isCaseSensitivityEnabled: options.isCaseSensitivityEnabled ?? false, // Then transform the tree into the intermediate representation.
isRegexEnabled: options.isRegexEnabled ?? false, return transformTreeToIR({
onExpandSearchContext: async (contextName: string) => { tree,
const context = await prisma.searchContext.findUnique({ input: query,
where: { isCaseSensitivityEnabled: options.isCaseSensitivityEnabled ?? false,
name_orgId: { isRegexEnabled: options.isRegexEnabled ?? false,
name: contextName, onExpandSearchContext: async (contextName: string) => {
orgId: SINGLE_TENANT_ORG_ID, const context = await prisma.searchContext.findUnique({
where: {
name_orgId: {
name: contextName,
orgId: SINGLE_TENANT_ORG_ID,
}
},
include: {
repos: true,
} }
}, });
include: {
repos: true, if (!context) {
throw new Error(`Search context "${contextName}" not found`);
} }
return context.repos.map((repo) => repo.name);
},
});
} catch (error) {
if (error instanceof SyntaxError) {
throw new ServiceErrorException({
statusCode: StatusCodes.BAD_REQUEST,
errorCode: ErrorCode.FAILED_TO_PARSE_QUERY,
message: `Failed to parse query "${query}" with message: ${error.message}`,
}); });
}
if (!context) { throw error;
throw new Error(`Search context "${contextName}" not found`); }
}
return context.repos.map((repo) => repo.name);
},
});
} }
/** /**

View file

@ -34,4 +34,5 @@ export enum ErrorCode {
API_KEY_NOT_FOUND = 'API_KEY_NOT_FOUND', API_KEY_NOT_FOUND = 'API_KEY_NOT_FOUND',
INVALID_API_KEY = 'INVALID_API_KEY', INVALID_API_KEY = 'INVALID_API_KEY',
CHAT_IS_READONLY = 'CHAT_IS_READONLY', CHAT_IS_READONLY = 'CHAT_IS_READONLY',
FAILED_TO_PARSE_QUERY = 'FAILED_TO_PARSE_QUERY',
} }