Skip to content

Commit b8870c0

Browse files
committed
feat: Improve default search mode robustness and error handling
Address CodeRabbit review feedback to enhance production readiness: - Add runtime validation with Zod schema for mode parameter in server actions - Enhance audit logging with before/after values for better change tracking - Fix homepage logic to clamp org default and cookie values when no language models available - Improve error handling to surface specific ServiceError messages in toast notifications - Hide default search mode card for non-owners instead of just disabling it - Add searchModeSchema export to types for reusable validation Prevents invalid states where "Ask" mode could be set/used without configured models and provides clearer feedback to users when operations fail.
1 parent f330360 commit b8870c0

File tree

5 files changed

+53
-16
lines changed

5 files changed

+53
-16
lines changed

packages/web/src/actions.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import { getAuditService } from "@/ee/features/audit/factory";
4040
import { addUserToOrganization, orgHasAvailability } from "@/lib/authUtils";
4141
import { getOrgMetadata } from "@/lib/utils";
4242
import { getOrgFromDomain } from "./data/org";
43+
import { searchModeSchema } from "@/types";
4344

4445
const ajv = new Ajv({
4546
validateFormats: false,
@@ -2189,10 +2190,21 @@ export const getDefaultSearchMode = async (domain: string): Promise<"precise" |
21892190
});
21902191

21912192
export const setDefaultSearchMode = async (domain: string, mode: "precise" | "agentic"): Promise<{ success: boolean } | ServiceError> => sew(async () => {
2193+
// Runtime validation to guard server action from invalid input
2194+
const parsed = searchModeSchema.safeParse(mode);
2195+
if (!parsed.success) {
2196+
return {
2197+
statusCode: StatusCodes.BAD_REQUEST,
2198+
errorCode: ErrorCode.INVALID_REQUEST_BODY,
2199+
message: "Invalid default search mode",
2200+
} satisfies ServiceError;
2201+
}
2202+
const validatedMode = parsed.data;
2203+
21922204
return await withAuth(async (userId) => {
21932205
return await withOrgMembership(userId, domain, async ({ org }) => {
21942206
// Validate that agentic mode is not being set when no language models are configured
2195-
if (mode === "agentic") {
2207+
if (validatedMode === "agentic") {
21962208
const { getConfiguredLanguageModelsInfo } = await import("@/features/chat/actions");
21972209
const languageModels = await getConfiguredLanguageModelsInfo();
21982210
if (languageModels.length === 0) {
@@ -2205,9 +2217,10 @@ export const setDefaultSearchMode = async (domain: string, mode: "precise" | "ag
22052217
}
22062218

22072219
const currentMetadata = getOrgMetadata(org);
2220+
const previousMode = currentMetadata?.defaultSearchMode ?? "precise";
22082221
const mergedMetadata = {
22092222
...(currentMetadata ?? {}),
2210-
defaultSearchMode: mode,
2223+
defaultSearchMode: validatedMode,
22112224
};
22122225

22132226
await prisma.org.update({
@@ -2231,7 +2244,8 @@ export const setDefaultSearchMode = async (domain: string, mode: "precise" | "ag
22312244
},
22322245
orgId: org.id,
22332246
metadata: {
2234-
defaultSearchMode: mode
2247+
previousDefaultSearchMode: previousMode,
2248+
newDefaultSearchMode: validatedMode
22352249
}
22362250
});
22372251

packages/web/src/app/[domain]/page.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,14 +52,17 @@ export default async function Home(props: { params: Promise<{ domain: string }>
5252
const defaultSearchMode = await getDefaultSearchMode(domain);
5353
// If there was an error or no setting found, default to precise (search)
5454
const orgDefaultMode = isServiceError(defaultSearchMode) ? "precise" : defaultSearchMode;
55+
const effectiveOrgDefaultMode =
56+
orgDefaultMode === "agentic" && models.length === 0 ? "precise" : orgDefaultMode;
5557

5658
// Read search mode from cookie, defaulting to the org's default setting if not set
5759
const cookieStore = await cookies();
5860
const searchModeCookie = cookieStore.get(SEARCH_MODE_COOKIE_NAME);
5961
const initialSearchMode = (
6062
searchModeCookie?.value === "agentic" ||
6163
searchModeCookie?.value === "precise"
62-
) ? searchModeCookie.value : orgDefaultMode;
64+
) ? ((searchModeCookie.value === "agentic" && models.length === 0) ? "precise" : searchModeCookie.value)
65+
: effectiveOrgDefaultMode;
6366

6467
const isAgenticSearchTutorialDismissed = cookieStore.get(AGENTIC_SEARCH_TUTORIAL_DISMISSED_COOKIE_NAME)?.value === "true";
6568

packages/web/src/app/[domain]/settings/(general)/components/defaultSearchModeCard.tsx

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,19 @@ export const DefaultSearchModeCard = ({ initialDefaultMode, currentUserRole, isA
3434
setIsUpdating(true);
3535
try {
3636
const result = await setDefaultSearchMode(domain as string, defaultSearchMode);
37-
if (!result || typeof result !== 'object' || !result.success) {
37+
if (!result || typeof result !== 'object') {
38+
throw new Error('Unexpected response');
39+
}
40+
// If this is a ServiceError, surface its message
41+
if ('statusCode' in result && 'errorCode' in result && 'message' in result) {
42+
toast({
43+
title: "Failed to update",
44+
description: result.message,
45+
variant: "destructive",
46+
});
47+
return;
48+
}
49+
if (!result.success) {
3850
throw new Error('Failed to update default search mode');
3951
}
4052
toast({
@@ -44,11 +56,14 @@ export const DefaultSearchModeCard = ({ initialDefaultMode, currentUserRole, isA
4456
});
4557
} catch (error) {
4658
console.error('Error updating default search mode:', error);
47-
toast({
48-
title: "Failed to update",
49-
description: "An error occurred while updating the default search mode.",
50-
variant: "destructive",
51-
});
59+
// If we already showed a specific error above, do nothing here; otherwise fallback
60+
if (!(error instanceof Error && /Unexpected response/.test(error.message))) {
61+
toast({
62+
title: "Failed to update",
63+
description: "An error occurred while updating the default search mode.",
64+
variant: "destructive",
65+
});
66+
}
5267
} finally {
5368
setIsUpdating(false);
5469
}

packages/web/src/app/[domain]/settings/(general)/page.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { ServiceErrorException } from "@/lib/serviceError";
88
import { ErrorCode } from "@/lib/errorCodes";
99
import { headers } from "next/headers";
1010
import { getConfiguredLanguageModelsInfo } from "@/features/chat/actions";
11+
import { OrgRole } from "@sourcebot/db";
1112

1213
interface GeneralSettingsPageProps {
1314
params: Promise<{
@@ -63,11 +64,13 @@ export default async function GeneralSettingsPage(props: GeneralSettingsPageProp
6364
rootDomain={host}
6465
/>
6566

66-
<DefaultSearchModeCard
67-
initialDefaultMode={initialDefaultMode}
68-
currentUserRole={currentUserRole}
69-
isAskModeAvailable={isAskModeAvailable}
70-
/>
67+
{currentUserRole === OrgRole.OWNER && (
68+
<DefaultSearchModeCard
69+
initialDefaultMode={initialDefaultMode}
70+
currentUserRole={currentUserRole}
71+
isAskModeAvailable={isAskModeAvailable}
72+
/>
73+
)}
7174
</div>
7275
)
7376
}

packages/web/src/types.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { z } from "zod";
22

3+
export const searchModeSchema = z.enum(["precise", "agentic"]);
4+
35
export const orgMetadataSchema = z.object({
46
anonymousAccessEnabled: z.boolean().optional(),
5-
defaultSearchMode: z.enum(["precise", "agentic"]).optional(),
7+
defaultSearchMode: searchModeSchema.optional(),
68
})
79

810
export const demoSearchScopeSchema = z.object({

0 commit comments

Comments
 (0)