chore: merge main, resolve OpenTelemetry version conflict

Kept full OTel suite at 0.217.0/2.7.1 (our security fix) — main had
partially updated exporter-prometheus to 0.217.0 but left the rest at
0.213.0/2.6.1. Full suite update required for consistency and to fix
the Prometheus exporter CVE.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Matti Nannt
2026-05-13 09:33:48 +02:00
46 changed files with 1325 additions and 1067 deletions
+8 -8
View File
@@ -12,18 +12,18 @@
},
"devDependencies": {
"@chromatic-com/storybook": "5.0.2",
"@storybook/addon-a11y": "10.3.5",
"@storybook/addon-docs": "10.3.5",
"@storybook/addon-links": "10.3.5",
"@storybook/addon-onboarding": "10.3.5",
"@storybook/react-vite": "10.3.5",
"@storybook/addon-a11y": "10.3.6",
"@storybook/addon-docs": "10.3.6",
"@storybook/addon-links": "10.3.6",
"@storybook/addon-onboarding": "10.3.6",
"@storybook/react-vite": "10.3.6",
"@tailwindcss/vite": "4.2.4",
"@typescript-eslint/eslint-plugin": "8.57.2",
"@typescript-eslint/parser": "8.57.2",
"@vitejs/plugin-react": "5.1.4",
"eslint-plugin-react-refresh": "0.4.26",
"eslint-plugin-storybook": "10.3.5",
"storybook": "10.3.5",
"vite": "7.3.2"
"eslint-plugin-storybook": "10.3.6",
"storybook": "10.3.6",
"vite": "7.3.3"
}
}
@@ -1,7 +1,12 @@
import { AuthenticationError } from "@formbricks/types/errors";
import { AccountSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(account)/components/AccountSettingsNavbar";
import { AccountSecurity } from "@/app/(app)/environments/[environmentId]/settings/(account)/profile/components/AccountSecurity";
import { EMAIL_VERIFICATION_DISABLED, IS_FORMBRICKS_CLOUD, PASSWORD_RESET_DISABLED } from "@/lib/constants";
import {
EMAIL_VERIFICATION_DISABLED,
ENTERPRISE_LICENSE_REQUEST_FORM_URL,
IS_FORMBRICKS_CLOUD,
PASSWORD_RESET_DISABLED,
} from "@/lib/constants";
import { getOrganizationsWhereUserIsSingleOwner } from "@/lib/organization/service";
import { getUser } from "@/lib/user/service";
import { getTranslate } from "@/lingodotdev/server";
@@ -71,7 +76,7 @@ const Page = async (props: {
: t("common.request_trial_license"),
href: IS_FORMBRICKS_CLOUD
? `/environments/${params.environmentId}/settings/billing`
: "https://formbricks.com/upgrade-self-hosting-license",
: ENTERPRISE_LICENSE_REQUEST_FORM_URL,
},
{
text: t("common.learn_more"),
@@ -3,7 +3,7 @@ import Link from "next/link";
import { notFound } from "next/navigation";
import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar";
import { EnterpriseLicenseStatus } from "@/app/(app)/environments/[environmentId]/settings/(organization)/enterprise/components/EnterpriseLicenseStatus";
import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
import { ENTERPRISE_LICENSE_REQUEST_FORM_URL, IS_FORMBRICKS_CLOUD } from "@/lib/constants";
import { getTranslate } from "@/lingodotdev/server";
import { GRACE_PERIOD_MS, getEnterpriseLicense } from "@/modules/ee/license-check/lib/license";
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
@@ -173,7 +173,7 @@ const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
</p>
<Button asChild>
<Link
href="https://app.formbricks.com/s/clvupq3y205i5yrm3sm9v1xt5"
href={ENTERPRISE_LICENSE_REQUEST_FORM_URL}
target="_blank"
rel="noopener noreferrer nofollow"
referrerPolicy="no-referrer">
@@ -1,6 +1,11 @@
import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar";
import { isInstanceAIConfigured } from "@/lib/ai/service";
import { FB_LOGO_URL, IS_FORMBRICKS_CLOUD, IS_STORAGE_CONFIGURED } from "@/lib/constants";
import {
ENTERPRISE_LICENSE_REQUEST_FORM_URL,
FB_LOGO_URL,
IS_FORMBRICKS_CLOUD,
IS_STORAGE_CONFIGURED,
} from "@/lib/constants";
import { getUser } from "@/lib/user/service";
import { getTranslate } from "@/lingodotdev/server";
import { getIsMultiOrgEnabled, getWhiteLabelPermission } from "@/modules/ee/license-check/lib/utils";
@@ -80,6 +85,7 @@ const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
fbLogoUrl={FB_LOGO_URL}
user={user}
isStorageConfigured={IS_STORAGE_CONFIGURED}
enterpriseLicenseRequestFormUrl={ENTERPRISE_LICENSE_REQUEST_FORM_URL}
/>
{isMultiOrgEnabled && (
<SettingsCard
@@ -2,7 +2,12 @@ import { AuthenticationError, ResourceNotFoundError } from "@formbricks/types/er
import { SurveyAnalysisNavigation } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/SurveyAnalysisNavigation";
import { ResponsePage } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponsePage";
import { SurveyAnalysisCTA } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SurveyAnalysisCTA";
import { IS_FORMBRICKS_CLOUD, IS_STORAGE_CONFIGURED, RESPONSES_PER_PAGE } from "@/lib/constants";
import {
ENTERPRISE_LICENSE_REQUEST_FORM_URL,
IS_FORMBRICKS_CLOUD,
IS_STORAGE_CONFIGURED,
RESPONSES_PER_PAGE,
} from "@/lib/constants";
import { getPublicDomain } from "@/lib/getPublicUrl";
import { getResponseCountBySurveyId, getResponses } from "@/lib/response/service";
import { getSurvey } from "@/lib/survey/service";
@@ -72,6 +77,7 @@ const Page = async (props: { params: Promise<{ environmentId: string; surveyId:
isContactsEnabled={isContactsEnabled}
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
isStorageConfigured={IS_STORAGE_CONFIGURED}
enterpriseLicenseRequestFormUrl={ENTERPRISE_LICENSE_REQUEST_FORM_URL}
/>
}>
<SurveyAnalysisNavigation activeId="responses" />
@@ -31,6 +31,7 @@ interface SurveyAnalysisCTAProps {
isContactsEnabled: boolean;
isFormbricksCloud: boolean;
isStorageConfigured: boolean;
enterpriseLicenseRequestFormUrl: string;
}
interface ModalState {
@@ -47,6 +48,7 @@ export const SurveyAnalysisCTA = ({
isContactsEnabled,
isFormbricksCloud,
isStorageConfigured,
enterpriseLicenseRequestFormUrl,
}: SurveyAnalysisCTAProps) => {
const { t } = useTranslation();
const router = useRouter();
@@ -231,6 +233,7 @@ export const SurveyAnalysisCTA = ({
isReadOnly={isReadOnly}
isStorageConfigured={isStorageConfigured}
projectCustomScripts={project.customHeadScripts}
enterpriseLicenseRequestFormUrl={enterpriseLicenseRequestFormUrl}
/>
)}
<SuccessMessage />
@@ -54,6 +54,7 @@ interface ShareSurveyModalProps {
isReadOnly: boolean;
isStorageConfigured: boolean;
projectCustomScripts?: string | null;
enterpriseLicenseRequestFormUrl: string;
}
export const ShareSurveyModal = ({
@@ -69,6 +70,7 @@ export const ShareSurveyModal = ({
isReadOnly,
isStorageConfigured,
projectCustomScripts,
enterpriseLicenseRequestFormUrl,
}: ShareSurveyModalProps) => {
const environmentId = survey.environmentId;
const [surveyUrl, setSurveyUrl] = useState<string>(getSurveyUrl(survey, publicDomain, "default"));
@@ -108,6 +110,7 @@ export const ShareSurveyModal = ({
segments,
isContactsEnabled,
isFormbricksCloud,
enterpriseLicenseRequestFormUrl,
},
disabled: survey.singleUse?.enabled,
},
@@ -34,6 +34,7 @@ interface PersonalLinksTabProps {
segments: TSegment[];
isContactsEnabled: boolean;
isFormbricksCloud: boolean;
enterpriseLicenseRequestFormUrl: string;
}
interface PersonalLinksFormData {
@@ -74,6 +75,7 @@ export const PersonalLinksTab = ({
surveyId,
isContactsEnabled,
isFormbricksCloud,
enterpriseLicenseRequestFormUrl,
}: PersonalLinksTabProps) => {
const { t } = useTranslation();
@@ -169,7 +171,7 @@ export const PersonalLinksTab = ({
text: isFormbricksCloud ? t("common.upgrade_plan") : t("common.request_trial_license"),
href: isFormbricksCloud
? `/environments/${environmentId}/settings/billing`
: "https://formbricks.com/upgrade-self-hosting-license",
: enterpriseLicenseRequestFormUrl,
},
{
text: t("common.learn_more"),
@@ -1106,6 +1106,21 @@ describe("getSurveySummary", () => {
expect.objectContaining({ responseIds: expect.any(Array) })
);
});
test("does not pass responseIds for date-only filterCriteria", async () => {
const filterCriteria: TResponseFilterCriteria = {
createdAt: {
min: new Date("2024-01-01T00:00:00.000Z"),
max: new Date("2024-01-31T23:59:59.999Z"),
},
};
await getSurveySummary(mockSurveyId, filterCriteria);
expect(getDisplayCountBySurveyId).toHaveBeenCalledWith(mockSurveyId, {
createdAt: filterCriteria.createdAt,
});
});
});
describe("getResponsesForSummary", () => {
@@ -979,7 +979,7 @@ export const getSurveySummary = reactCache(
const elements = getElementsFromBlocks(survey.blocks);
const batchSize = 5000;
const hasFilter = Object.keys(filterCriteria ?? {}).length > 0;
const hasFilter = Object.keys(filterCriteria ?? {}).some((filterKey) => filterKey !== "createdAt");
// Use cursor-based pagination instead of count + offset to avoid expensive queries
const responses: TSurveySummaryResponse[] = [];
@@ -4,7 +4,12 @@ import { SurveyAnalysisNavigation } from "@/app/(app)/environments/[environmentI
import { SummaryPage } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryPage";
import { SurveyAnalysisCTA } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SurveyAnalysisCTA";
import { getSurveySummary } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/surveySummary";
import { DEFAULT_LOCALE, IS_FORMBRICKS_CLOUD, IS_STORAGE_CONFIGURED } from "@/lib/constants";
import {
DEFAULT_LOCALE,
ENTERPRISE_LICENSE_REQUEST_FORM_URL,
IS_FORMBRICKS_CLOUD,
IS_STORAGE_CONFIGURED,
} from "@/lib/constants";
import { getPublicDomain } from "@/lib/getPublicUrl";
import { getSurvey } from "@/lib/survey/service";
import { getUser } from "@/lib/user/service";
@@ -74,6 +79,7 @@ const SurveyPage = async (props: { params: Promise<{ environmentId: string; surv
isContactsEnabled={isContactsEnabled}
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
isStorageConfigured={IS_STORAGE_CONFIGURED}
enterpriseLicenseRequestFormUrl={ENTERPRISE_LICENSE_REQUEST_FORM_URL}
/>
}>
<SurveyAnalysisNavigation activeId="summary" />
+3
View File
@@ -155,6 +155,9 @@ export const DEBUG = env.DEBUG === "1";
// Enterprise License constant
export const ENTERPRISE_LICENSE_KEY = env.ENTERPRISE_LICENSE_KEY;
export const ENTERPRISE_LICENSE_REQUEST_FORM_URL =
"https://app.formbricks.com/s/trvp8tzy5uvsps9rc9qi9l9w?delivery=onpremise&source=ce";
export const REDIS_URL = env.REDIS_URL;
export const RATE_LIMITING_DISABLED = env.RATE_LIMITING_DISABLED === "1";
export const TELEMETRY_DISABLED = env.TELEMETRY_DISABLED === "1";
+16 -3
View File
@@ -4,7 +4,7 @@ import { cache as reactCache } from "react";
import { z } from "zod";
import { prisma } from "@formbricks/database";
import { ZId } from "@formbricks/types/common";
import { TDisplay, TDisplayFilters, TDisplayWithContact } from "@formbricks/types/displays";
import { TDisplay, TDisplayFilters, TDisplayWithContact, ZDisplayFilters } from "@formbricks/types/displays";
import { DatabaseError } from "@formbricks/types/errors";
import { validateInputs } from "../utils/validate";
@@ -18,18 +18,31 @@ export const selectDisplay = {
export const getDisplayCountBySurveyId = reactCache(
async (surveyId: string, filters?: TDisplayFilters): Promise<number> => {
validateInputs([surveyId, ZId]);
validateInputs([surveyId, ZId], [filters, ZDisplayFilters.optional()]);
if (filters?.responseIds?.length === 0) {
return 0;
}
try {
const displayCount = await prisma.display.count({
where: {
surveyId: surveyId,
surveyId,
...(filters?.createdAt && {
createdAt: {
gte: filters.createdAt.min,
lte: filters.createdAt.max,
},
}),
...(filters?.responseIds && {
response: {
is: {
id: {
in: filters.responseIds,
},
},
},
}),
},
});
return displayCount;
@@ -4,9 +4,14 @@ import { Prisma } from "@prisma/client";
import { describe, expect, test, vi } from "vitest";
import { PrismaErrorType } from "@formbricks/database/types/error";
import { DatabaseError, ValidationError } from "@formbricks/types/errors";
import { getDisplaysByContactId, getDisplaysBySurveyIdWithContact } from "../service";
import {
getDisplayCountBySurveyId,
getDisplaysByContactId,
getDisplaysBySurveyIdWithContact,
} from "../service";
const mockContactId = "clqnj99r9000008lebgf8734j";
const mockResponseIds = ["clqnfg59i000208i426pb4wcv", "clqnfg59i000208i426pb4wcw"];
const mockDisplaysForContact = [
{
@@ -45,6 +50,74 @@ const mockDisplaysWithContact = [
},
];
describe("getDisplayCountBySurveyId", () => {
describe("Happy Path", () => {
test("counts displays by surveyId", async () => {
vi.mocked(prisma.display.count).mockResolvedValue(5);
const result = await getDisplayCountBySurveyId(mockSurveyId);
expect(result).toBe(5);
expect(prisma.display.count).toHaveBeenCalledWith({
where: {
surveyId: mockSurveyId,
},
});
});
test("combines createdAt and responseIds filters", async () => {
const createdAt = {
min: new Date("2024-01-01T00:00:00.000Z"),
max: new Date("2024-01-31T23:59:59.999Z"),
};
vi.mocked(prisma.display.count).mockResolvedValue(2);
const result = await getDisplayCountBySurveyId(mockSurveyId, {
createdAt,
responseIds: mockResponseIds,
});
expect(result).toBe(2);
expect(prisma.display.count).toHaveBeenCalledWith({
where: {
surveyId: mockSurveyId,
createdAt: {
gte: createdAt.min,
lte: createdAt.max,
},
response: {
is: {
id: {
in: mockResponseIds,
},
},
},
},
});
});
test("returns 0 without querying when responseIds filter is empty", async () => {
const result = await getDisplayCountBySurveyId(mockSurveyId, { responseIds: [] });
expect(result).toBe(0);
expect(prisma.display.count).not.toHaveBeenCalled();
});
});
describe("Sad Path", () => {
test("throws DatabaseError on PrismaClientKnownRequestError", async () => {
const errToThrow = new Prisma.PrismaClientKnownRequestError("Mock error", {
code: PrismaErrorType.UniqueConstraintViolation,
clientVersion: "0.0.1",
});
vi.mocked(prisma.display.count).mockRejectedValue(errToThrow);
await expect(getDisplayCountBySurveyId(mockSurveyId)).rejects.toThrow(DatabaseError);
});
});
});
describe("getDisplaysByContactId", () => {
describe("Happy Path", () => {
test("returns displays for a contact ordered by createdAt desc", async () => {
@@ -1,5 +1,5 @@
import { ReactNode } from "react";
import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
import { ENTERPRISE_LICENSE_REQUEST_FORM_URL, IS_FORMBRICKS_CLOUD } from "@/lib/constants";
import { getTranslate } from "@/lingodotdev/server";
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
import { PageHeader } from "@/modules/ui/components/page-header";
@@ -52,7 +52,7 @@ export const ContactsPageLayout = async ({
text: IS_FORMBRICKS_CLOUD ? t("common.upgrade_plan") : t("common.request_trial_license"),
href: IS_FORMBRICKS_CLOUD
? `/environments/${environmentId}/settings/billing`
: "https://formbricks.com/upgrade-self-hosting-license",
: ENTERPRISE_LICENSE_REQUEST_FORM_URL,
},
{
text: t("common.learn_more"),
@@ -29,6 +29,7 @@ interface QuotasCardProps {
isFormbricksCloud?: boolean;
quotas: TSurveyQuota[];
hasResponses: boolean;
enterpriseLicenseRequestFormUrl: string;
}
const AddQuotaButton = ({
@@ -67,6 +68,7 @@ export const QuotasCard = ({
isFormbricksCloud,
quotas,
hasResponses,
enterpriseLicenseRequestFormUrl,
}: QuotasCardProps) => {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
@@ -177,7 +179,7 @@ export const QuotasCard = ({
text: isFormbricksCloud ? t("common.upgrade_plan") : t("common.request_trial_license"),
href: isFormbricksCloud
? `/environments/${environmentId}/settings/billing`
: "https://formbricks.com/upgrade-self-hosting-license",
: enterpriseLicenseRequestFormUrl,
},
{
text: t("common.learn_more"),
@@ -1,7 +1,7 @@
import { ResourceNotFoundError } from "@formbricks/types/errors";
import { TOrganizationRole } from "@formbricks/types/memberships";
import { SettingsCard } from "@/app/(app)/environments/[environmentId]/settings/components/SettingsCard";
import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
import { ENTERPRISE_LICENSE_REQUEST_FORM_URL, IS_FORMBRICKS_CLOUD } from "@/lib/constants";
import { getTranslate } from "@/lingodotdev/server";
import { TeamsTable } from "@/modules/ee/teams/team-list/components/teams-table";
import { getProjectsByOrganizationId } from "@/modules/ee/teams/team-list/lib/project";
@@ -41,7 +41,7 @@ export const TeamsView = async ({
text: IS_FORMBRICKS_CLOUD ? t("common.upgrade_plan") : t("common.request_trial_license"),
href: IS_FORMBRICKS_CLOUD
? `/environments/${environmentId}/settings/billing`
: "https://formbricks.com/docs/self-hosting/license#30-day-trial-license-request",
: ENTERPRISE_LICENSE_REQUEST_FORM_URL,
},
{
text: t("common.learn_more"),
@@ -36,6 +36,7 @@ interface EmailCustomizationSettingsProps {
user: TUser | null;
fbLogoUrl: string;
isStorageConfigured: boolean;
enterpriseLicenseRequestFormUrl: string;
}
export const EmailCustomizationSettings = ({
@@ -47,6 +48,7 @@ export const EmailCustomizationSettings = ({
user,
fbLogoUrl,
isStorageConfigured,
enterpriseLicenseRequestFormUrl,
}: EmailCustomizationSettingsProps) => {
const { t } = useTranslation();
@@ -184,7 +186,7 @@ export const EmailCustomizationSettings = ({
text: isFormbricksCloud ? t("common.upgrade_plan") : t("common.request_trial_license"),
href: isFormbricksCloud
? `/environments/${environmentId}/settings/billing`
: "https://formbricks.com/upgrade-self-hosting-license",
: enterpriseLicenseRequestFormUrl,
},
{
text: t("common.learn_more"),
@@ -1,6 +1,6 @@
import { Project } from "@prisma/client";
import { SettingsCard } from "@/app/(app)/environments/[environmentId]/settings/components/SettingsCard";
import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
import { ENTERPRISE_LICENSE_REQUEST_FORM_URL, IS_FORMBRICKS_CLOUD } from "@/lib/constants";
import { getTranslate } from "@/lingodotdev/server";
import { EditBranding } from "@/modules/ee/whitelabel/remove-branding/components/edit-branding";
import { Alert, AlertDescription } from "@/modules/ui/components/alert";
@@ -26,7 +26,7 @@ export const BrandingSettingsCard = async ({
text: IS_FORMBRICKS_CLOUD ? t("common.upgrade_plan") : t("common.request_trial_license"),
href: IS_FORMBRICKS_CLOUD
? `/environments/${environmentId}/settings/billing`
: "https://formbricks.com/upgrade-self-hosting-license",
: ENTERPRISE_LICENSE_REQUEST_FORM_URL,
},
{
text: t("common.learn_more"),
@@ -39,6 +39,7 @@ interface OrganizationActionsProps {
isStorageConfigured: boolean;
isTeamAdmin: boolean;
userAdminTeamIds?: string[];
enterpriseLicenseRequestFormUrl: string;
}
export const OrganizationActions = ({
@@ -56,6 +57,7 @@ export const OrganizationActions = ({
isStorageConfigured,
isTeamAdmin,
userAdminTeamIds,
enterpriseLicenseRequestFormUrl,
}: OrganizationActionsProps) => {
const router = useRouter();
const { t } = useTranslation();
@@ -174,6 +176,7 @@ export const OrganizationActions = ({
isOwnerOrManager={isOwnerOrManager}
isTeamAdmin={isTeamAdmin}
userAdminTeamIds={userAdminTeamIds}
enterpriseLicenseRequestFormUrl={enterpriseLicenseRequestFormUrl}
/>
<Dialog open={isLeaveOrganizationModalOpen} onOpenChange={setIsLeaveOrganizationModalOpen}>
@@ -29,6 +29,7 @@ interface IndividualInviteTabProps {
environmentId: string;
membershipRole?: TOrganizationRole;
showTeamAdminRestrictions: boolean;
enterpriseLicenseRequestFormUrl: string;
}
export const IndividualInviteTab = ({
@@ -40,6 +41,7 @@ export const IndividualInviteTab = ({
environmentId,
membershipRole,
showTeamAdminRestrictions,
enterpriseLicenseRequestFormUrl,
}: IndividualInviteTabProps) => {
const ZFormSchema = z.object({
name: ZUserName,
@@ -191,7 +193,7 @@ export const IndividualInviteTab = ({
href={
isFormbricksCloud
? `/environments/${environmentId}/settings/billing`
: "https://formbricks.com/upgrade-self-hosting-license"
: enterpriseLicenseRequestFormUrl
}>
{t("common.upgrade_plan")}
</Link>
@@ -30,6 +30,7 @@ interface InviteMemberModalProps {
isOwnerOrManager: boolean;
isTeamAdmin: boolean;
userAdminTeamIds?: string[];
enterpriseLicenseRequestFormUrl: string;
}
export const InviteMemberModal = ({
@@ -45,6 +46,7 @@ export const InviteMemberModal = ({
isOwnerOrManager,
isTeamAdmin,
userAdminTeamIds,
enterpriseLicenseRequestFormUrl,
}: InviteMemberModalProps) => {
const [type, setType] = useState<"individual" | "bulk">("individual");
@@ -68,6 +70,7 @@ export const InviteMemberModal = ({
teams={filteredTeams}
membershipRole={membershipRole}
showTeamAdminRestrictions={showTeamAdminRestrictions}
enterpriseLicenseRequestFormUrl={enterpriseLicenseRequestFormUrl}
/>
),
bulk: (
@@ -2,7 +2,12 @@ import { Suspense } from "react";
import { TOrganizationRole } from "@formbricks/types/memberships";
import { TOrganization } from "@formbricks/types/organizations";
import { SettingsCard } from "@/app/(app)/environments/[environmentId]/settings/components/SettingsCard";
import { INVITE_DISABLED, IS_FORMBRICKS_CLOUD, IS_STORAGE_CONFIGURED } from "@/lib/constants";
import {
ENTERPRISE_LICENSE_REQUEST_FORM_URL,
INVITE_DISABLED,
IS_FORMBRICKS_CLOUD,
IS_STORAGE_CONFIGURED,
} from "@/lib/constants";
import { getTranslate } from "@/lingodotdev/server";
import { getIsMultiOrgEnabled } from "@/modules/ee/license-check/lib/utils";
import { getTeamsWhereUserIsAdmin } from "@/modules/ee/teams/lib/roles";
@@ -70,6 +75,7 @@ export const MembersView = async ({
isAccessControlAllowed={isAccessControlAllowed}
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
isStorageConfigured={IS_STORAGE_CONFIGURED}
enterpriseLicenseRequestFormUrl={ENTERPRISE_LICENSE_REQUEST_FORM_URL}
environmentId={environmentId}
isMultiOrgEnabled={isMultiOrgEnabled}
teams={teams}
@@ -29,6 +29,7 @@ interface SettingsViewProps {
isFormbricksCloud: boolean;
isQuotasAllowed: boolean;
quotas: TSurveyQuota[];
enterpriseLicenseRequestFormUrl: string;
}
export const SettingsView = ({
@@ -46,6 +47,7 @@ export const SettingsView = ({
projectPermission,
isFormbricksCloud,
quotas,
enterpriseLicenseRequestFormUrl,
}: SettingsViewProps) => {
const isAppSurvey = localSurvey.type === "app";
@@ -70,7 +72,11 @@ export const SettingsView = ({
</div>
</div>
) : (
<TargetingLockedCard isFormbricksCloud={isFormbricksCloud} environmentId={environment.id} />
<TargetingLockedCard
isFormbricksCloud={isFormbricksCloud}
environmentId={environment.id}
enterpriseLicenseRequestFormUrl={enterpriseLicenseRequestFormUrl}
/>
)}
</div>
) : null}
@@ -89,6 +95,7 @@ export const SettingsView = ({
isFormbricksCloud={isFormbricksCloud}
quotas={quotas}
hasResponses={responseCount > 0}
enterpriseLicenseRequestFormUrl={enterpriseLicenseRequestFormUrl}
/>
<ResponseOptionsCard
@@ -51,6 +51,7 @@ interface SurveyEditorProps {
quotas: TSurveyQuota[];
isExternalUrlsAllowed: boolean;
publicDomain: string;
enterpriseLicenseRequestFormUrl: string;
}
export const SurveyEditor = ({
@@ -80,6 +81,7 @@ export const SurveyEditor = ({
quotas,
isExternalUrlsAllowed,
publicDomain,
enterpriseLicenseRequestFormUrl,
}: SurveyEditorProps) => {
const [activeView, setActiveView] = useState<TSurveyEditorTabs>("elements");
const [activeElementId, setActiveElementId] = useState<string | null>(null);
@@ -266,6 +268,7 @@ export const SurveyEditor = ({
isFormbricksCloud={isFormbricksCloud}
isQuotasAllowed={isQuotasAllowed}
quotas={quotas}
enterpriseLicenseRequestFormUrl={enterpriseLicenseRequestFormUrl}
/>
)}
@@ -280,6 +283,7 @@ export const SurveyEditor = ({
userEmail={userEmail}
teamMemberDetails={teamMemberDetails}
locale={locale}
enterpriseLicenseRequestFormUrl={enterpriseLicenseRequestFormUrl}
/>
)}
</main>
@@ -9,9 +9,14 @@ import { UpgradePrompt } from "@/modules/ui/components/upgrade-prompt";
interface TargetingLockedCardProps {
isFormbricksCloud: boolean;
environmentId: string;
enterpriseLicenseRequestFormUrl: string;
}
export const TargetingLockedCard = ({ isFormbricksCloud, environmentId }: TargetingLockedCardProps) => {
export const TargetingLockedCard = ({
isFormbricksCloud,
environmentId,
enterpriseLicenseRequestFormUrl,
}: TargetingLockedCardProps) => {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
@@ -47,7 +52,7 @@ export const TargetingLockedCard = ({ isFormbricksCloud, environmentId }: Target
text: isFormbricksCloud ? t("common.upgrade_plan") : t("common.request_trial_license"),
href: isFormbricksCloud
? `/environments/${environmentId}/settings/billing`
: "https://formbricks.com/upgrade-self-hosting-license",
: enterpriseLicenseRequestFormUrl,
},
{
text: t("common.learn_more"),
+2
View File
@@ -1,6 +1,7 @@
import { ResourceNotFoundError } from "@formbricks/types/errors";
import {
DEFAULT_LOCALE,
ENTERPRISE_LICENSE_REQUEST_FORM_URL,
IS_FORMBRICKS_CLOUD,
IS_STORAGE_CONFIGURED,
MAIL_FROM,
@@ -138,6 +139,7 @@ export const SurveyEditorPage = async (props: {
quotas={quotas}
isExternalUrlsAllowed={isExternalUrlsAllowed}
publicDomain={publicDomain}
enterpriseLicenseRequestFormUrl={ENTERPRISE_LICENSE_REQUEST_FORM_URL}
/>
);
};
@@ -22,6 +22,7 @@ interface FollowUpsViewProps {
userEmail: string;
teamMemberDetails: TFollowUpEmailToUser[];
locale: TUserLocale;
enterpriseLicenseRequestFormUrl: string;
}
export const FollowUpsView = ({
@@ -34,6 +35,7 @@ export const FollowUpsView = ({
userEmail,
teamMemberDetails,
locale,
enterpriseLicenseRequestFormUrl,
}: FollowUpsViewProps) => {
const { t } = useTranslation();
const [addFollowUpModalOpen, setAddFollowUpModalOpen] = useState(false);
@@ -54,7 +56,7 @@ export const FollowUpsView = ({
: t("common.request_trial_license"),
href: isFormbricksCloud
? `/environments/${localSurvey.environmentId}/settings/billing`
: "https://formbricks.com/docs/self-hosting/license",
: enterpriseLicenseRequestFormUrl,
},
{
text: t("common.learn_more"),
+6 -6
View File
@@ -107,12 +107,12 @@
"prismjs": "1.30.0",
"qr-code-styling": "1.9.2",
"qrcode": "1.5.4",
"react": "19.2.5",
"react": "19.2.6",
"react-calendar": "6.0.1",
"react-colorful": "5.6.1",
"react-colorful": "5.6.2",
"react-confetti": "6.4.0",
"react-day-picker": "9.14.0",
"react-dom": "19.2.5",
"react-dom": "19.2.6",
"react-hook-form": "7.71.2",
"react-hot-toast": "2.6.0",
"react-i18next": "16.5.8",
@@ -142,15 +142,15 @@
"@types/papaparse": "5.5.2",
"@types/qrcode": "1.5.6",
"@types/sanitize-html": "2.16.1",
"@vitest/coverage-v8": "4.1.5",
"@vitest/coverage-v8": "4.1.6",
"autoprefixer": "10.4.27",
"cross-env": "10.1.0",
"dotenv": "17.3.1",
"postcss": "8.5.14",
"resize-observer-polyfill": "1.5.1",
"vite": "7.3.2",
"vite": "7.3.3",
"vite-tsconfig-paths": "6.1.1",
"vitest": "4.1.5",
"vitest": "4.1.6",
"vitest-mock-extended": "3.1.1"
}
}
+1
View File
@@ -96,6 +96,7 @@
"xm-and-surveys/surveys/link-surveys/data-prefilling",
"xm-and-surveys/surveys/link-surveys/embed-surveys",
"xm-and-surveys/surveys/link-surveys/link-settings",
"xm-and-surveys/surveys/link-surveys/pretty-url",
"xm-and-surveys/surveys/link-surveys/personal-links",
"xm-and-surveys/surveys/link-surveys/single-use-links",
"xm-and-surveys/surveys/link-surveys/source-tracking",
+3 -3
View File
@@ -8,7 +8,7 @@ The Formbricks core source code is licensed under AGPLv3 and available on GitHub
<Note>
Want to get your hands on the Enterprise Edition? [Request a free Enterprise Edition
Trial](https://formbricks.com/enterprise-license?source=docs) License to build a fully functioning Proof of
Trial](https://app.formbricks.com/s/trvp8tzy5uvsps9rc9qi9l9w?delivery=onpremise&source=docs) License to build a fully functioning Proof of
Concept.
</Note>
@@ -38,11 +38,11 @@ The Formbricks core application is licensed under the [AGPLv3 Open Source Licens
### The Enterprise Edition
Additional to the AGPL licensed Formbricks core, this repository contains code licensed under an Enterprise license. The [code](https://github.com/formbricks/formbricks/tree/main/apps/web/modules/ee) and [license](https://github.com/formbricks/formbricks/blob/main/apps/web/modules/ee/LICENSE) for the enterprise functionality can be found in the `/apps/web/modules/ee` folder of this repository. This additional functionality is not part of the AGPLv3 licensed Formbricks core and is designed to meet the needs of larger teams and enterprises. This advanced functionality is already included in the Docker images, but you need an [Enterprise License Key](https://formbricks.com/enterprise-license?source=docs) to unlock it.
Additional to the AGPL licensed Formbricks core, this repository contains code licensed under an Enterprise license. The [code](https://github.com/formbricks/formbricks/tree/main/apps/web/modules/ee) and [license](https://github.com/formbricks/formbricks/blob/main/apps/web/modules/ee/LICENSE) for the enterprise functionality can be found in the `/apps/web/modules/ee` folder of this repository. This additional functionality is not part of the AGPLv3 licensed Formbricks core and is designed to meet the needs of larger teams and enterprises. This advanced functionality is already included in the Docker images, but you need an [Enterprise License Key](https://app.formbricks.com/s/trvp8tzy5uvsps9rc9qi9l9w?delivery=onpremise&source=docs) to unlock it.
<Note>
Want to get your hands on the Enterprise Edition? [Request a free Enterprise Edition
Trial](https://formbricks.com/enterprise-license?source=docs) License to build a fully functioning Proof of
Trial](https://app.formbricks.com/s/trvp8tzy5uvsps9rc9qi9l9w?delivery=onpremise&source=docs) License to build a fully functioning Proof of
Concept.
</Note>
@@ -0,0 +1,81 @@
---
title: "Pretty URL"
description: "Create a custom, memorable URL for your survey instead of sharing a long auto-generated link."
icon: "link"
---
<Note>
**Self-Hosted Only**: Pretty URLs are available exclusively on self-hosted Formbricks instances. This feature is not available on Formbricks Cloud.
</Note>
## What is a Pretty URL?
By default, every survey is accessible at a URL containing its auto-generated ID, e.g. `yourdomain.com/s/cm1abc123xyz`. A Pretty URL lets you replace that with a short, human-readable slug of your choice:
```
yourdomain.com/p/customer-feedback
```
When someone visits the pretty URL, they are automatically redirected to the actual survey. Query parameters such as `suId` and `lang` are forwarded as well.
## Setting Up a Pretty URL
<Steps>
<Step title="Open the Share Modal">
Navigate to your survey's **Summary** page and click the **Share survey** button in the top toolbar.
</Step>
<Step title="Go to the Pretty URL tab">
In the Share Modal, select the **Pretty URL** tab.
</Step>
<Step title="Enter a slug">
Type your desired slug in the input field. Slugs may only contain **lowercase letters, numbers, and hyphens** (e.g. `customer-feedback`, `q4-nps-2024`).
The full URL is shown in real time below the input so you can confirm how it will look.
</Step>
<Step title="Save">
Click **Save**. The slug is now live. Anyone visiting the pretty URL is immediately redirected to your survey.
</Step>
</Steps>
## Managing Pretty URLs
Once a slug is saved, the Pretty URL tab shows the active link with two actions:
- **Copy**: copies the full pretty URL to your clipboard.
- **Remove**: deletes the slug (after a confirmation prompt). The survey remains accessible via its original `/s/[surveyId]` URL.
## Viewing All Pretty URLs in Your Organization
All surveys that have a pretty URL assigned are listed in one place:
1. Go to **Organization Settings → Domain**.
2. Open the **Pretty URLs** section.
The table shows each survey's name, workspace, slug, and environment type (production / development).
## Slug Rules
| Rule | Detail |
|------|--------|
| Characters | Lowercase letters (a-z), digits (0-9), and hyphens (-) |
| Uniqueness | Must be unique across your entire Formbricks instance |
| Format example | `customer-feedback`, `onboarding-survey`, `q4-nps` |
## Query Parameter Forwarding
Pretty URLs forward all query parameters to the destination survey URL. For example:
```
/p/customer-feedback?suId=contact123&lang=de
```
redirects to:
```
/s/[surveyId]?suId=contact123&lang=de
```
This means features like [single-use links](/xm-and-surveys/surveys/link-surveys/single-use-links), [data prefilling](/xm-and-surveys/surveys/link-surveys/data-prefilling), and [multi-language surveys](/xm-and-surveys/surveys/general-features/multi-language-surveys) all work with pretty URLs.
+2 -2
View File
@@ -46,8 +46,8 @@
"dev:setup": "bash scripts/setup-dev-env.sh"
},
"dependencies": {
"react": "19.2.5",
"react-dom": "19.2.5"
"react": "19.2.6",
"react-dom": "19.2.6"
},
"devDependencies": {
"@azure/playwright": "1.1.5",
+7 -7
View File
@@ -36,19 +36,19 @@
},
"author": "Formbricks <hola@formbricks.com>",
"dependencies": {
"@ai-sdk/amazon-bedrock": "4.0.96",
"@ai-sdk/azure": "3.0.54",
"@ai-sdk/google-vertex": "4.0.112",
"@ai-sdk/amazon-bedrock": "4.0.104",
"@ai-sdk/azure": "3.0.64",
"@ai-sdk/google-vertex": "4.0.126",
"@aws-sdk/credential-providers": "3.1017.0",
"@formbricks/logger": "workspace:*",
"@formbricks/types": "workspace:*",
"ai": "6.0.168"
"ai": "6.0.177"
},
"devDependencies": {
"@formbricks/config-typescript": "workspace:*",
"@formbricks/eslint-config": "workspace:*",
"@vitest/coverage-v8": "4.1.5",
"vite": "8.0.10",
"vitest": "4.1.5"
"@vitest/coverage-v8": "4.1.6",
"vite": "8.0.12",
"vitest": "4.1.6"
}
}
+3 -3
View File
@@ -44,9 +44,9 @@
"devDependencies": {
"@formbricks/config-typescript": "workspace:*",
"@formbricks/eslint-config": "workspace:*",
"@vitest/coverage-v8": "4.1.5",
"vite": "7.3.2",
"@vitest/coverage-v8": "4.1.6",
"vite": "7.3.3",
"vite-plugin-dts": "4.5.4",
"vitest": "4.1.5"
"vitest": "4.1.6"
}
}
+3 -3
View File
@@ -3,16 +3,16 @@
"version": "0.0.0",
"private": true,
"devDependencies": {
"@next/eslint-plugin-next": "15.5.15",
"@next/eslint-plugin-next": "15.5.18",
"@typescript-eslint/eslint-plugin": "8.57.2",
"@typescript-eslint/parser": "8.57.2",
"@vercel/style-guide": "6.0.0",
"eslint-config-next": "15.5.15",
"eslint-config-next": "15.5.18",
"eslint-config-prettier": "10.1.8",
"eslint-config-turbo": "2.8.21",
"eslint-plugin-react": "7.37.5",
"eslint-plugin-react-hooks": "5.2.0",
"eslint-plugin-react-refresh": "0.5.2",
"@vitest/eslint-plugin": "1.6.16"
"@vitest/eslint-plugin": "1.6.17"
}
}
+1 -1
View File
@@ -66,7 +66,7 @@
"prisma": "6.19.3",
"prisma-json-types-generator": "3.6.2",
"tsx": "4.21.0",
"vite": "7.3.2",
"vite": "7.3.3",
"vite-plugin-dts": "4.5.4"
}
}
+1 -1
View File
@@ -22,7 +22,7 @@
"@formbricks/types": "workspace:*",
"autoprefixer": "10.4.27",
"clsx": "2.1.1",
"postcss": "8.5.12",
"postcss": "8.5.14",
"tailwind-merge": "3.5.0",
"tailwindcss": "3.4.19"
}
+2 -2
View File
@@ -42,7 +42,7 @@
"@formbricks/eslint-config": "workspace:*",
"@types/node": "^25.4.0",
"tsx": "^4.21.0",
"vite": "7.3.2",
"vitest": "4.1.5"
"vite": "7.3.3",
"vitest": "4.1.6"
}
}
+3 -3
View File
@@ -45,9 +45,9 @@
"devDependencies": {
"@formbricks/config-typescript": "workspace:*",
"@formbricks/eslint-config": "workspace:*",
"@vitest/coverage-v8": "4.1.5",
"vite": "7.3.2",
"@vitest/coverage-v8": "4.1.6",
"vite": "7.3.3",
"vite-plugin-dts": "4.5.4",
"vitest": "4.1.5"
"vitest": "4.1.6"
}
}
+2 -2
View File
@@ -43,7 +43,7 @@
"devDependencies": {
"@formbricks/config-typescript": "workspace:*",
"@formbricks/eslint-config": "workspace:*",
"vite": "7.3.2",
"vitest": "4.1.5"
"vite": "7.3.3",
"vitest": "4.1.6"
}
}
+3 -3
View File
@@ -44,8 +44,8 @@
"devDependencies": {
"@formbricks/config-typescript": "workspace:*",
"@formbricks/eslint-config": "workspace:*",
"@vitest/coverage-v8": "4.1.5",
"vite": "7.3.2",
"vitest": "4.1.5"
"@vitest/coverage-v8": "4.1.6",
"vite": "7.3.3",
"vitest": "4.1.6"
}
}
+7 -7
View File
@@ -85,21 +85,21 @@
"devDependencies": {
"@formbricks/config-typescript": "workspace:*",
"@formbricks/eslint-config": "workspace:*",
"@storybook/react": "10.3.5",
"@storybook/react-vite": "10.3.5",
"@storybook/react": "10.3.6",
"@storybook/react-vite": "10.3.6",
"@tailwindcss/postcss": "4.2.4",
"@tailwindcss/vite": "4.2.4",
"@types/react": "19.2.14",
"@types/react-dom": "19.2.3",
"@vitejs/plugin-react": "5.1.4",
"@vitest/coverage-v8": "4.1.5",
"react": "19.2.5",
"react-dom": "19.2.5",
"@vitest/coverage-v8": "4.1.6",
"react": "19.2.6",
"react-dom": "19.2.6",
"rimraf": "6.1.3",
"tailwindcss": "4.2.4",
"vite": "7.3.2",
"vite": "7.3.3",
"vite-plugin-dts": "4.5.4",
"vite-tsconfig-paths": "6.1.1",
"vitest": "4.1.5"
"vitest": "4.1.6"
}
}
+2 -2
View File
@@ -65,10 +65,10 @@
"concurrently": "9.2.1",
"fake-indexeddb": "6.2.5",
"happy-dom": "20.8.9",
"postcss": "8.5.12",
"postcss": "8.5.14",
"rollup-plugin-visualizer": "7.0.1",
"tailwindcss": "4.2.4",
"vite": "7.3.2",
"vite": "7.3.3",
"vite-tsconfig-paths": "6.1.1"
}
}
+1 -1
View File
@@ -17,6 +17,6 @@
"devDependencies": {
"@formbricks/config-typescript": "workspace:*",
"@formbricks/eslint-config": "workspace:*",
"vite": "7.3.2"
"vite": "7.3.3"
}
}
+989 -986
View File
File diff suppressed because it is too large Load Diff