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);
logger.error(e);
if (e instanceof ServiceErrorException) {
return e.serviceError;
}
if (e instanceof Error) {
return unexpectedError(e.message);
}

View file

@ -287,7 +287,7 @@ export const SearchBar = ({
indentWithTab={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>
<TooltipTrigger asChild>
<span>

View file

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

View file

@ -1,8 +1,10 @@
'use client';
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 { useCallback, useEffect, useRef, useState } from 'react';
interface CacheEntry {
files: SearchResultFile[];
@ -132,6 +134,14 @@ export const useStreamedSearch = ({ query, matches, contextLines, whole, isRegex
});
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}`);
}
@ -249,7 +259,7 @@ export const useStreamedSearch = ({ query, matches, contextLines, whole, isRegex
...prev,
isStreaming: false,
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 { PrismaClient } from '@sourcebot/db';
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.
const parser = _parser.configure({
@ -61,35 +64,47 @@ export const parseQuerySyntaxIntoIR = async ({
},
prisma: PrismaClient,
}): Promise<QueryIR> => {
// First parse the query into a Lezer tree.
const tree = parser.parse(query);
// Then transform the tree into the intermediate representation.
return transformTreeToIR({
tree,
input: query,
isCaseSensitivityEnabled: options.isCaseSensitivityEnabled ?? false,
isRegexEnabled: options.isRegexEnabled ?? false,
onExpandSearchContext: async (contextName: string) => {
const context = await prisma.searchContext.findUnique({
where: {
name_orgId: {
name: contextName,
orgId: SINGLE_TENANT_ORG_ID,
try {
// First parse the query into a Lezer tree.
const tree = parser.parse(query);
// Then transform the tree into the intermediate representation.
return transformTreeToIR({
tree,
input: query,
isCaseSensitivityEnabled: options.isCaseSensitivityEnabled ?? false,
isRegexEnabled: options.isRegexEnabled ?? false,
onExpandSearchContext: async (contextName: string) => {
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 new Error(`Search context "${contextName}" not found`);
}
return context.repos.map((repo) => repo.name);
},
});
}
throw error;
}
}
/**

View file

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