Compare commits

..

1 Commits

Author SHA1 Message Date
Cursor Agent
1dcaaa87d8 fix: wrap custom scripts in try-catch to prevent ReferenceErrors
Fixes FORMBRICKS-GH

- Wrap inline script content in IIFE with try-catch block to catch runtime errors like ReferenceError
- Add onerror handler for external scripts to catch loading failures
- Log errors to console for debugging while preventing survey breakage
- This prevents undefined global variable references (like 'frappe') from breaking the survey experience
2026-02-05 05:01:21 +00:00
5 changed files with 31 additions and 32 deletions

View File

@@ -2,7 +2,7 @@
import { z } from "zod";
import { ZId } from "@formbricks/types/common";
import { AuthorizationError, OperationNotAllowedError } from "@formbricks/types/errors";
import { 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 AuthorizationError("Membership not found");
throw new Error("Membership not found");
}
return await getProjectsByUserId(ctx.user.id, membership);

View File

@@ -8,12 +8,6 @@ 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
*/
@@ -36,16 +30,10 @@ 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 {
// Only report unexpected errors to Sentry
// Expected errors (auth failures, rate limits) are handled gracefully in the UI
if (!isExpectedError) {
Sentry.captureException(error);
}
Sentry.captureException(error);
}
return (

View File

@@ -22,29 +22,24 @@ 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,
},
});
// 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 =
if (
e instanceof ResourceNotFoundError ||
e instanceof AuthorizationError ||
e instanceof InvalidInputError ||
e instanceof UnknownError ||
e instanceof AuthenticationError ||
e instanceof OperationNotAllowedError ||
e instanceof TooManyRequestsError;
if (isExpectedError) {
e instanceof TooManyRequestsError
) {
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 AuthorizationError(t("common.membership_not_found"));
throw new Error(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 AuthorizationError(t("common.membership_not_found"));
throw new Error(t("common.membership_not_found"));
}
// Fetch remaining data in parallel

View File

@@ -56,9 +56,25 @@ export const CustomScriptsInjector = ({
newScript.setAttribute(attr.name, attr.value);
});
// Copy inline script content
// Copy inline script content with error handling
if (script.textContent) {
newScript.textContent = script.textContent;
// Wrap inline scripts in try-catch to prevent user script errors from breaking the survey
newScript.textContent = `
(function() {
try {
${script.textContent}
} catch (error) {
console.warn('[Formbricks] Error in custom script:', error);
}
})();
`.trim();
}
// Add error handler for external scripts
if (script.src) {
newScript.onerror = (error) => {
console.warn("[Formbricks] Error loading external script:", script.src, error);
};
}
document.head.appendChild(newScript);