Compare commits

...

1 Commits

Author SHA1 Message Date
TheodorTomas
deee967511 fix: prevent expected auth errors from being reported to Sentry
AuthorizationError, AuthenticationError, and TooManyRequestsError are
expected errors that should be handled gracefully in the UI, not reported
to Sentry. This change:

- Filters expected errors in the global error boundary before Sentry capture
- Moves Sentry capture in action client to after expected error check
- Changes "Membership not found" errors to AuthorizationError for consistency

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-05 22:33:15 +08:00
4 changed files with 30 additions and 13 deletions

View File

@@ -2,7 +2,7 @@
import { z } from "zod";
import { ZId } from "@formbricks/types/common";
import { OperationNotAllowedError } from "@formbricks/types/errors";
import { AuthorizationError, OperationNotAllowedError } from "@formbricks/types/errors";
import { ZProjectUpdateInput } from "@formbricks/types/project";
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
import { getOrganization } from "@/lib/organization/service";
@@ -138,7 +138,7 @@ export const getProjectsForSwitcherAction = authenticatedActionClient
// Need membership for getProjectsByUserId (1 DB query)
const membership = await getMembershipByUserIdOrganizationId(ctx.user.id, parsedInput.organizationId);
if (!membership) {
throw new Error("Membership not found");
throw new AuthorizationError("Membership not found");
}
return await getProjectsByUserId(ctx.user.id, membership);

View File

@@ -8,6 +8,12 @@ import { type ClientErrorType, getClientErrorData } from "@formbricks/types/erro
import { Button } from "@/modules/ui/components/button";
import { ErrorComponent } from "@/modules/ui/components/error-component";
/**
* Expected error names that should NOT be reported to Sentry.
* These are handled gracefully in the UI (e.g., show "no access" or redirect).
*/
const EXPECTED_ERROR_NAMES = new Set(["AuthorizationError", "AuthenticationError", "TooManyRequestsError"]);
/**
* Get translated error messages based on error type
*/
@@ -30,10 +36,16 @@ const ErrorBoundary = ({ error, reset }: { error: Error; reset: () => void }) =>
const errorData = getClientErrorData(error);
const { title, description } = getErrorMessages(errorData.type, t);
const isExpectedError = EXPECTED_ERROR_NAMES.has(error.name);
if (process.env.NODE_ENV === "development") {
console.error(error.message);
} else {
Sentry.captureException(error);
// Only report unexpected errors to Sentry
// Expected errors (auth failures, rate limits) are handled gracefully in the UI
if (!isExpectedError) {
Sentry.captureException(error);
}
}
return (

View File

@@ -22,24 +22,29 @@ import { ActionClientCtx } from "./types/context";
export const actionClient = createSafeActionClient({
handleServerError(e, utils) {
const eventId = (utils.ctx as Record<string, any>)?.auditLoggingCtx?.eventId ?? undefined; // keep explicit fallback
Sentry.captureException(e, {
extra: {
eventId,
},
});
if (
// Expected errors that should NOT be reported to Sentry
// These are handled gracefully in the UI (e.g., show "no access", redirect, or retry message)
const isExpectedError =
e instanceof ResourceNotFoundError ||
e instanceof AuthorizationError ||
e instanceof InvalidInputError ||
e instanceof UnknownError ||
e instanceof AuthenticationError ||
e instanceof OperationNotAllowedError ||
e instanceof TooManyRequestsError
) {
e instanceof TooManyRequestsError;
if (isExpectedError) {
return e.message;
}
// Only capture unexpected errors to Sentry
Sentry.captureException(e, {
extra: {
eventId,
},
});
// eslint-disable-next-line no-console -- This error needs to be logged for debugging server-side errors
logger.withContext({ eventId }).error(e, "SERVER ERROR");
return DEFAULT_SERVER_ERROR_MESSAGE;

View File

@@ -61,7 +61,7 @@ export const getEnvironmentAuth = reactCache(async (environmentId: string): Prom
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
if (!currentUserMembership) {
throw new Error(t("common.membership_not_found"));
throw new AuthorizationError(t("common.membership_not_found"));
}
const { isMember, isOwner, isManager, isBilling } = getAccessFlags(currentUserMembership?.role);
@@ -293,7 +293,7 @@ export const getEnvironmentLayoutData = reactCache(
// Validate user's membership was found
if (!membership) {
throw new Error(t("common.membership_not_found"));
throw new AuthorizationError(t("common.membership_not_found"));
}
// Fetch remaining data in parallel