Merge branch 'main' of https://github.com/formbricks/formbricks into feature-formal-wording

This commit is contained in:
Dhruwang
2026-01-09 14:04:10 +05:30
55 changed files with 1111 additions and 59 deletions

View File

@@ -168,6 +168,9 @@ SLACK_CLIENT_SECRET=
# Enterprise License Key
ENTERPRISE_LICENSE_KEY=
# Internal Environment (production, staging) - used for internal staging environment
# ENVIRONMENT=production
# Automatically assign new users to a specific organization and role within that organization
# Insert an existing organization id or generate a valid CUID for a new one at https://www.getuniqueid.com/cuid (e.g. cjld2cjxh0000qzrmn831i7rn)
# (Role Management is an Enterprise feature)

View File

@@ -213,6 +213,7 @@ export const SurveyAnalysisCTA = ({
isFormbricksCloud={isFormbricksCloud}
isReadOnly={isReadOnly}
isStorageConfigured={isStorageConfigured}
projectCustomScripts={project.customHeadScripts}
/>
)}
<SuccessMessage environment={environment} survey={survey} />

View File

@@ -3,6 +3,7 @@
import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
import {
Code2Icon,
CodeIcon,
Link2Icon,
MailIcon,
QrCodeIcon,
@@ -18,6 +19,7 @@ import { TSurvey } from "@formbricks/types/surveys/types";
import { TUser } from "@formbricks/types/user";
import { AnonymousLinksTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/anonymous-links-tab";
import { AppTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/app-tab";
import { CustomHtmlTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/custom-html-tab";
import { DynamicPopupTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/dynamic-popup-tab";
import { EmailTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/email-tab";
import { LinkSettingsTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/link-settings-tab";
@@ -51,6 +53,7 @@ interface ShareSurveyModalProps {
isFormbricksCloud: boolean;
isReadOnly: boolean;
isStorageConfigured: boolean;
projectCustomScripts?: string | null;
}
export const ShareSurveyModal = ({
@@ -65,6 +68,7 @@ export const ShareSurveyModal = ({
isFormbricksCloud,
isReadOnly,
isStorageConfigured,
projectCustomScripts,
}: ShareSurveyModalProps) => {
const environmentId = survey.environmentId;
const [surveyUrl, setSurveyUrl] = useState<string>(getSurveyUrl(survey, publicDomain, "default"));
@@ -191,9 +195,24 @@ export const ShareSurveyModal = ({
componentType: PrettyUrlTab,
componentProps: { publicDomain, isReadOnly },
},
{
id: ShareSettingsType.CUSTOM_HTML,
type: LinkTabsType.SHARE_SETTING,
label: t("environments.surveys.share.custom_html.nav_title"),
icon: CodeIcon,
title: t("environments.surveys.share.custom_html.nav_title"),
description: t("environments.surveys.share.custom_html.description"),
componentType: CustomHtmlTab,
componentProps: { projectCustomScripts, isReadOnly },
},
];
return isFormbricksCloud ? tabs.filter((tab) => tab.id !== ShareSettingsType.PRETTY_URL) : tabs;
// Filter out tabs that should not be shown on Formbricks Cloud
return isFormbricksCloud
? tabs.filter(
(tab) => tab.id !== ShareSettingsType.PRETTY_URL && tab.id !== ShareSettingsType.CUSTOM_HTML
)
: tabs;
}, [
t,
survey,
@@ -207,6 +226,7 @@ export const ShareSurveyModal = ({
isFormbricksCloud,
email,
isStorageConfigured,
projectCustomScripts,
]);
const getDefaultActiveId = useCallback(() => {

View File

@@ -0,0 +1,163 @@
"use client";
import { AlertTriangleIcon } from "lucide-react";
import { useState } from "react";
import { useForm } from "react-hook-form";
import toast from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { TSurvey } from "@formbricks/types/surveys/types";
import { useSurvey } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/context/survey-context";
import { cn } from "@/lib/cn";
import { updateSurveyAction } from "@/modules/survey/editor/actions";
import { Alert, AlertDescription } from "@/modules/ui/components/alert";
import { Button } from "@/modules/ui/components/button";
import {
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormProvider,
} from "@/modules/ui/components/form";
import { TabToggle } from "@/modules/ui/components/tab-toggle";
interface CustomHtmlTabProps {
projectCustomScripts: string | null | undefined;
isReadOnly: boolean;
}
interface CustomHtmlFormData {
customHeadScripts: string;
customHeadScriptsMode: TSurvey["customHeadScriptsMode"];
}
export const CustomHtmlTab = ({ projectCustomScripts, isReadOnly }: CustomHtmlTabProps) => {
const { t } = useTranslation();
const { survey } = useSurvey();
const [isSaving, setIsSaving] = useState(false);
const form = useForm<CustomHtmlFormData>({
defaultValues: {
customHeadScripts: survey.customHeadScripts ?? "",
customHeadScriptsMode: survey.customHeadScriptsMode ?? "add",
},
});
const {
handleSubmit,
watch,
setValue,
reset,
formState: { isDirty },
} = form;
const scriptsMode = watch("customHeadScriptsMode");
const onSubmit = async (data: CustomHtmlFormData) => {
if (isSaving || isReadOnly) return;
setIsSaving(true);
const updatedSurvey: TSurvey = {
...survey,
customHeadScripts: data.customHeadScripts || null,
customHeadScriptsMode: data.customHeadScriptsMode,
};
const result = await updateSurveyAction(updatedSurvey);
if (result?.data) {
toast.success(t("environments.surveys.share.custom_html.saved_successfully"));
reset(data);
} else {
toast.error(t("common.something_went_wrong_please_try_again"));
}
setIsSaving(false);
};
return (
<div className="px-1">
<FormProvider {...form}>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
{/* Mode Toggle */}
<div className="space-y-2">
<FormLabel>{t("environments.surveys.share.custom_html.script_mode")}</FormLabel>
<TabToggle
id="custom-scripts-mode"
options={[
{ value: "add", label: t("environments.surveys.share.custom_html.add_to_workspace") },
{ value: "replace", label: t("environments.surveys.share.custom_html.replace_workspace") },
]}
defaultSelected={scriptsMode ?? "add"}
onChange={(value) => setValue("customHeadScriptsMode", value, { shouldDirty: true })}
disabled={isReadOnly}
/>
<p className="text-sm text-slate-500">
{scriptsMode === "add"
? t("environments.surveys.share.custom_html.add_mode_description")
: t("environments.surveys.share.custom_html.replace_mode_description")}
</p>
</div>
{/* Workspace Scripts Preview */}
{projectCustomScripts && (
<div className={scriptsMode === "replace" ? "opacity-50" : ""}>
<FormLabel>{t("environments.surveys.share.custom_html.workspace_scripts_label")}</FormLabel>
<div className="mt-2 max-h-32 overflow-auto rounded-md border border-slate-200 bg-slate-50 p-3">
<pre className="font-mono text-xs whitespace-pre-wrap text-slate-600">
{projectCustomScripts}
</pre>
</div>
</div>
)}
{!projectCustomScripts && (
<div className="rounded-md border border-slate-200 bg-slate-50 p-3">
<p className="text-sm text-slate-500">
{t("environments.surveys.share.custom_html.no_workspace_scripts")}
</p>
</div>
)}
{/* Survey Scripts */}
<FormField
control={form.control}
name="customHeadScripts"
render={({ field }) => (
<FormItem>
<FormLabel>{t("environments.surveys.share.custom_html.survey_scripts_label")}</FormLabel>
<FormDescription>
{t("environments.surveys.share.custom_html.survey_scripts_description")}
</FormDescription>
<FormControl>
<textarea
rows={8}
placeholder={t("environments.surveys.share.custom_html.placeholder")}
className={cn(
"focus:border-brand-dark flex w-full rounded-md border border-slate-300 bg-white px-3 py-2 font-mono text-xs text-slate-800 placeholder:text-slate-400 focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
)}
{...field}
disabled={isReadOnly}
/>
</FormControl>
</FormItem>
)}
/>
{/* Save Button */}
<Button type="submit" disabled={isSaving || isReadOnly || !isDirty}>
{isSaving ? t("common.saving") : t("common.save")}
</Button>
{/* Security Warning */}
<Alert variant="warning" className="flex items-start gap-2">
<AlertTriangleIcon className="mt-0.5 h-4 w-4 shrink-0" />
<AlertDescription>
{t("environments.surveys.share.custom_html.security_warning")}
</AlertDescription>
</Alert>
</form>
</FormProvider>
</div>
);
};

View File

@@ -13,6 +13,7 @@ export enum ShareViaType {
export enum ShareSettingsType {
LINK_SETTINGS = "link-settings",
PRETTY_URL = "pretty-url",
CUSTOM_HTML = "custom-html",
}
export enum LinkTabsType {

View File

@@ -1609,6 +1609,20 @@ checksums:
environments/surveys/share/anonymous_links/source_tracking: dcf85834f1ba490347a301ab55d32402
environments/surveys/share/anonymous_links/url_encryption_description: 1509056fdae7b42fc85f1ee3c49de4c3
environments/surveys/share/anonymous_links/url_encryption_label: 9c70fd3f64cf8cc5039b198d3af79d14
environments/surveys/share/custom_html/add_mode_description: f48dcf53bce27cc40c3546547e8395cb
environments/surveys/share/custom_html/add_to_workspace: af9cd24872f25cfc4231b926acc76d7c
environments/surveys/share/custom_html/description: 0634048655de8b4b17b41d496e1ea457
environments/surveys/share/custom_html/nav_title: 01f993f027ab277058eacb8a48ea7c01
environments/surveys/share/custom_html/no_workspace_scripts: 7fc57f576c98e96ee73e7b489345d51a
environments/surveys/share/custom_html/placeholder: 229eb1676a69311ff1dcc19c1a52c080
environments/surveys/share/custom_html/replace_mode_description: 6eaf17275c02b0d5ac21255747f36271
environments/surveys/share/custom_html/replace_workspace: b80e698cc8790246fea42453bfa4b09d
environments/surveys/share/custom_html/saved_successfully: 14e7d2d646803ac1dd24cfa45c22606c
environments/surveys/share/custom_html/script_mode: 60ed1102dd42ad14e272df5f6921b423
environments/surveys/share/custom_html/security_warning: 5faa0f284d48110918a5e8a467e2bcb8
environments/surveys/share/custom_html/survey_scripts_description: 948746d51db23b348164105c175391b3
environments/surveys/share/custom_html/survey_scripts_label: 095d9fe768abe2bb32428184ee1c9b5a
environments/surveys/share/custom_html/workspace_scripts_label: 3d9b6c09eae10a2bacb3ac96b4db4a19
environments/surveys/share/dynamic_popup/alert_button: 8932096e3eee837beeb21dd4afd8b662
environments/surveys/share/dynamic_popup/alert_description: 53d2ba39984a059a5eca4cb6cf9ba00d
environments/surveys/share/dynamic_popup/alert_title: 813a9160940894da26ec2a09bbb1a7bf
@@ -1820,6 +1834,13 @@ checksums:
environments/workspace/app-connection/setup_alert_title: 9561cca2b391e0df81e8a982921ff2bb
environments/workspace/app-connection/webapp_url: d64d8cc3c4c4ecce780d94755f7e4de9
environments/workspace/general/cannot_delete_only_workspace: 853f32a75d92b06eaccc0d43d767c183
environments/workspace/general/custom_scripts: a6a06a2e20764d76d3e22e5e17d98dbb
environments/workspace/general/custom_scripts_card_description: 1585c47126e4b68f9f79f232631c67a1
environments/workspace/general/custom_scripts_description: 1c477e711fc08850b2ab70d98ffe18d6
environments/workspace/general/custom_scripts_label: 3b189dd62ae0cc35d616e04af90f0b38
environments/workspace/general/custom_scripts_placeholder: 229eb1676a69311ff1dcc19c1a52c080
environments/workspace/general/custom_scripts_updated_successfully: eabe8e6ededa86342d59093fe308c681
environments/workspace/general/custom_scripts_warning: 5faa0f284d48110918a5e8a467e2bcb8
environments/workspace/general/delete_workspace: 3badbc0f4b49644986fc19d8b2d8f317
environments/workspace/general/delete_workspace_confirmation: 54a4ee78867537e0244c7170453cdb3f
environments/workspace/general/delete_workspace_name_includes_surveys_responses_people_and_more: 11e9ac5a799fbec22495f92f42c40d98

View File

@@ -23,6 +23,7 @@ export const env = createEnv({
EMAIL_VERIFICATION_DISABLED: z.enum(["1", "0"]).optional(),
ENCRYPTION_KEY: z.string(),
ENTERPRISE_LICENSE_KEY: z.string().optional(),
ENVIRONMENT: z.enum(["production", "staging"]).default("production"),
GITHUB_ID: z.string().optional(),
GITHUB_SECRET: z.string().optional(),
GOOGLE_CLIENT_ID: z.string().optional(),
@@ -151,6 +152,7 @@ export const env = createEnv({
EMAIL_VERIFICATION_DISABLED: process.env.EMAIL_VERIFICATION_DISABLED,
ENCRYPTION_KEY: process.env.ENCRYPTION_KEY,
ENTERPRISE_LICENSE_KEY: process.env.ENTERPRISE_LICENSE_KEY,
ENVIRONMENT: process.env.ENVIRONMENT,
GITHUB_ID: process.env.GITHUB_ID,
GITHUB_SECRET: process.env.GITHUB_SECRET,
GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID,

View File

@@ -21,7 +21,7 @@ export type TInstanceInfo = {
export const getInstanceInfo = reactCache(async (): Promise<TInstanceInfo | null> => {
try {
const oldestOrg = await prisma.organization.findFirst({
orderBy: { createdAt: "asc" },
orderBy: [{ createdAt: "asc" }, { id: "asc" }],
select: { id: true, createdAt: true },
});

View File

@@ -26,6 +26,7 @@ const selectProject = {
environments: true,
styling: true,
logo: true,
customHeadScripts: true,
};
export const getUserProjects = reactCache(

View File

@@ -268,6 +268,8 @@ export const mockSyncSurveyOutput: SurveyMock = {
showLanguageSwitch: null,
metadata: {},
slug: null,
customHeadScripts: null,
customHeadScriptsMode: null,
};
export const mockSurveyOutput: SurveyMock = {
@@ -292,6 +294,8 @@ export const mockSurveyOutput: SurveyMock = {
showLanguageSwitch: null,
...baseSurveyProperties,
slug: null,
customHeadScripts: null,
customHeadScriptsMode: null,
};
export const createSurveyInput: TSurveyCreateInput = {
@@ -322,6 +326,8 @@ export const updateSurveyInput: TSurvey = {
...baseSurveyProperties,
...commonMockProperties,
slug: null,
customHeadScripts: null,
customHeadScriptsMode: null,
};
export const mockTransformedSurveyOutput = {
@@ -574,4 +580,6 @@ export const mockSurveyWithLogic: TSurvey = {
{ id: "siog1dabtpo3l0a3xoxw2922", type: "text", name: "var1", value: "lmao" },
{ id: "km1srr55owtn2r7lkoh5ny1u", type: "number", name: "var2", value: 32 },
],
customHeadScripts: null,
customHeadScriptsMode: null,
};

View File

@@ -65,6 +65,8 @@ export const selectSurvey = {
showLanguageSwitch: true,
recaptcha: true,
metadata: true,
customHeadScripts: true,
customHeadScriptsMode: true,
languages: {
select: {
default: true,
@@ -563,6 +565,7 @@ export const updateSurveyInternal = async (
...prismaSurvey, // Properties from prismaSurvey
displayPercentage: Number(prismaSurvey.displayPercentage) || null,
segment: surveySegment,
customHeadScriptsMode: prismaSurvey.customHeadScriptsMode,
};
return modifiedSurvey;
@@ -783,6 +786,7 @@ export const loadNewSegmentInSurvey = async (surveyId: string, newSegmentId: str
const modifiedSurvey: TSurvey = {
...prismaSurvey, // Properties from prismaSurvey
segment: surveySegment,
customHeadScriptsMode: prismaSurvey.customHeadScriptsMode,
};
return modifiedSurvey;

View File

@@ -29,6 +29,7 @@ export const transformPrismaSurvey = <T extends TSurvey | TJsEnvironmentStateSur
...surveyPrisma,
displayPercentage: Number(surveyPrisma.displayPercentage) || null,
segment,
customHeadScriptsMode: surveyPrisma.customHeadScriptsMode,
} as T;
return transformedSurvey;

View File

@@ -1690,6 +1690,22 @@
"url_encryption_description": "Nur deaktivieren, wenn Sie eine benutzerdefinierte Einmal-ID setzen müssen.",
"url_encryption_label": "Verschlüsselung der URL für einmalige Nutzung ID"
},
"custom_html": {
"add_mode_description": "Umfrage-Skripte werden zusätzlich zu den Workspace-Skripten ausgeführt.",
"add_to_workspace": "Zu Workspace-Skripten hinzufügen",
"description": "Tracking-Skripte und Pixel zu dieser Umfrage hinzufügen",
"nav_title": "Benutzerdefiniertes HTML",
"no_workspace_scripts": "Keine Workspace-Skripte konfiguriert. Sie können diese in Workspace-Einstellungen → Allgemein hinzufügen.",
"placeholder": "<!-- Fügen Sie hier Ihre Tracking-Skripte ein -->\n<script>\n // Google Tag Manager, Analytics, etc.\n</script>",
"replace_mode_description": "Nur Umfrage-Skripte werden ausgeführt. Workspace-Skripte werden ignoriert. Leer lassen, um keine Skripte zu laden.",
"replace_workspace": "Workspace-Skripte ersetzen",
"saved_successfully": "Benutzerdefinierte Skripte erfolgreich gespeichert",
"script_mode": "Skript-Modus",
"security_warning": "Skripte werden mit vollem Browser-Zugriff ausgeführt. Fügen Sie nur Skripte aus vertrauenswürdigen Quellen hinzu.",
"survey_scripts_description": "Benutzerdefiniertes HTML hinzufügen, das in den <head> dieser Umfrageseite eingefügt wird.",
"survey_scripts_label": "Umfragespezifische Skripte",
"workspace_scripts_label": "Workspace-Skripte (vererbt)"
},
"dynamic_popup": {
"alert_button": "Umfrage bearbeiten",
"alert_description": "Diese Umfrage ist derzeit als Link-Umfrage konfiguriert, die dynamische Pop-ups nicht unterstützt. Sie können dies im Tab Einstellungen im Umfrage-Editor ändern.",
@@ -1929,6 +1945,13 @@
},
"general": {
"cannot_delete_only_workspace": "Dies ist Ihr einziges Projekt, es kann nicht gelöscht werden. Erstellen Sie zuerst ein neues Projekt.",
"custom_scripts": "Benutzerdefinierte Skripte",
"custom_scripts_card_description": "Tracking-Skripte und Pixel zu allen Link-Umfragen in diesem Workspace hinzufügen.",
"custom_scripts_description": "Skripte werden in den <head> aller Link-Umfrageseiten eingefügt.",
"custom_scripts_label": "HTML-Skripte",
"custom_scripts_placeholder": "<!-- Fügen Sie hier Ihre Tracking-Skripte ein -->\n<script>\n // Google Tag Manager, Analytics, etc.\n</script>",
"custom_scripts_updated_successfully": "Benutzerdefinierte Skripte erfolgreich aktualisiert",
"custom_scripts_warning": "Skripte werden mit vollem Browser-Zugriff ausgeführt. Fügen Sie nur Skripte aus vertrauenswürdigen Quellen hinzu.",
"delete_workspace": "Projekt löschen",
"delete_workspace_confirmation": "Sind Sie sicher, dass Sie {projectName} löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.",
"delete_workspace_name_includes_surveys_responses_people_and_more": "{projectName} inkl. aller Umfragen, Antworten, Personen, Aktionen und Attribute löschen.",

View File

@@ -1690,6 +1690,22 @@
"url_encryption_description": "Only disable if you need to set a custom single-use ID.",
"url_encryption_label": "URL encryption of single-use ID"
},
"custom_html": {
"add_mode_description": "Survey scripts will run in addition to workspace-level scripts.",
"add_to_workspace": "Add to Workspace scripts",
"description": "Add tracking scripts and pixels to this survey",
"nav_title": "Custom HTML",
"no_workspace_scripts": "No workspace-level scripts configured. You can add them in Workspace Settings → General.",
"placeholder": "<!-- Paste your tracking scripts here -->\n<script>\n // Google Tag Manager, Analytics, etc.\n</script>",
"replace_mode_description": "Only survey scripts will run. Workspace scripts will be ignored. Keep empty to not load any scripts.",
"replace_workspace": "Replace Workspace scripts",
"saved_successfully": "Custom scripts saved successfully",
"script_mode": "Script Mode",
"security_warning": "Scripts execute with full browser access. Only add scripts from trusted sources.",
"survey_scripts_description": "Add custom HTML to inject into the <head> of this survey page.",
"survey_scripts_label": "Survey-specific scripts",
"workspace_scripts_label": "Workspace scripts (inherited)"
},
"dynamic_popup": {
"alert_button": "Edit survey",
"alert_description": "This survey is currently configured as a link survey, which does not support dynamic pop-ups. You can change this in the settings tab of the survey editor.",
@@ -1929,6 +1945,13 @@
},
"general": {
"cannot_delete_only_workspace": "This is your only workspace, it cannot be deleted. Create a new workspace first.",
"custom_scripts": "Custom Scripts",
"custom_scripts_card_description": "Add tracking scripts and pixels to all link surveys in this workspace.",
"custom_scripts_description": "Scripts will be injected into the <head> of all link survey pages.",
"custom_scripts_label": "HTML Scripts",
"custom_scripts_placeholder": "<!-- Paste your tracking scripts here -->\n<script>\n // Google Tag Manager, Analytics, etc.\n</script>",
"custom_scripts_updated_successfully": "Custom scripts updated successfully",
"custom_scripts_warning": "Scripts execute with full browser access. Only add scripts from trusted sources.",
"delete_workspace": "Delete Workspace",
"delete_workspace_confirmation": "Are you sure you want to delete {projectName}? This action cannot be undone.",
"delete_workspace_name_includes_surveys_responses_people_and_more": "Delete {projectName} including all surveys, responses, people, actions and attributes.",

View File

@@ -1690,6 +1690,22 @@
"url_encryption_description": "Desactiva solo si necesitas establecer un ID de uso único personalizado.",
"url_encryption_label": "Cifrado URL del ID de uso único"
},
"custom_html": {
"add_mode_description": "Los scripts de la encuesta se ejecutarán además de los scripts a nivel de espacio de trabajo.",
"add_to_workspace": "Añadir a los scripts del espacio de trabajo",
"description": "Añade scripts de seguimiento y píxeles a esta encuesta",
"nav_title": "HTML personalizado",
"no_workspace_scripts": "No hay scripts configurados a nivel de espacio de trabajo. Puedes añadirlos en Configuración del espacio de trabajo → General.",
"placeholder": "<!-- Pega tus scripts de seguimiento aquí -->\n<script>\n // Google Tag Manager, Analytics, etc.\n</script>",
"replace_mode_description": "Solo se ejecutarán los scripts de la encuesta. Los scripts del espacio de trabajo serán ignorados. Déjalo vacío para no cargar ningún script.",
"replace_workspace": "Reemplazar scripts del espacio de trabajo",
"saved_successfully": "Scripts personalizados guardados correctamente",
"script_mode": "Modo de script",
"security_warning": "Los scripts se ejecutan con acceso completo al navegador. Solo añade scripts de fuentes confiables.",
"survey_scripts_description": "Añade HTML personalizado para inyectar en el <head> de esta página de encuesta.",
"survey_scripts_label": "Scripts específicos de la encuesta",
"workspace_scripts_label": "Scripts del espacio de trabajo (heredados)"
},
"dynamic_popup": {
"alert_button": "Editar encuesta",
"alert_description": "Esta encuesta está actualmente configurada como una encuesta de enlace, que no admite ventanas emergentes dinámicas. Puedes cambiar esto en la pestaña de ajustes del editor de encuestas.",
@@ -1929,6 +1945,13 @@
},
"general": {
"cannot_delete_only_workspace": "Este es tu único proyecto, no se puede eliminar. Crea primero un proyecto nuevo.",
"custom_scripts": "Scripts personalizados",
"custom_scripts_card_description": "Añade scripts de seguimiento y píxeles a todas las encuestas con enlace en este espacio de trabajo.",
"custom_scripts_description": "Los scripts se inyectarán en el <head> de todas las páginas de encuestas con enlace.",
"custom_scripts_label": "Scripts HTML",
"custom_scripts_placeholder": "<!-- Pega tus scripts de seguimiento aquí -->\n<script>\n // Google Tag Manager, Analytics, etc.\n</script>",
"custom_scripts_updated_successfully": "Scripts personalizados actualizados correctamente",
"custom_scripts_warning": "Los scripts se ejecutan con acceso completo al navegador. Solo añade scripts de fuentes confiables.",
"delete_workspace": "Eliminar proyecto",
"delete_workspace_confirmation": "¿Estás seguro de que quieres eliminar {projectName}? Esta acción no se puede deshacer.",
"delete_workspace_name_includes_surveys_responses_people_and_more": "Eliminar {projectName} incluyendo todas las encuestas, respuestas, personas, acciones y atributos.",

View File

@@ -1690,6 +1690,22 @@
"url_encryption_description": "Désactiver seulement si vous devez définir un identifiant unique personnalisé",
"url_encryption_label": "Cryptage de l'identifiant à usage unique dans l'URL"
},
"custom_html": {
"add_mode_description": "Les scripts de l'enquête s'exécuteront en plus des scripts au niveau de l'espace de travail.",
"add_to_workspace": "Ajouter aux scripts de l'espace de travail",
"description": "Ajouter des scripts de suivi et des pixels à cette enquête",
"nav_title": "HTML personnalisé",
"no_workspace_scripts": "Aucun script au niveau de l'espace de travail configuré. Vous pouvez les ajouter dans Paramètres de l'espace de travail → Général.",
"placeholder": "<!-- Collez vos scripts de suivi ici -->\n<script>\n // Google Tag Manager, Analytics, etc.\n</script>",
"replace_mode_description": "Seuls les scripts de l'enquête s'exécuteront. Les scripts de l'espace de travail seront ignorés. Laissez vide pour ne charger aucun script.",
"replace_workspace": "Remplacer les scripts de l'espace de travail",
"saved_successfully": "Scripts personnalisés enregistrés avec succès",
"script_mode": "Mode de script",
"security_warning": "Les scripts s'exécutent avec un accès complet au navigateur. Ajoutez uniquement des scripts provenant de sources fiables.",
"survey_scripts_description": "Ajouter du HTML personnalisé à injecter dans le <head> de cette page d'enquête.",
"survey_scripts_label": "Scripts spécifiques à l'enquête",
"workspace_scripts_label": "Scripts de l'espace de travail (hérités)"
},
"dynamic_popup": {
"alert_button": "Modifier enquête",
"alert_description": "Ce sondage est actuellement configuré comme un sondage de lien, qui ne prend pas en charge les pop-ups dynamiques. Vous pouvez le modifier dans l'onglet des paramètres de l'éditeur de sondage.",
@@ -1929,6 +1945,13 @@
},
"general": {
"cannot_delete_only_workspace": "Il s'agit de votre seul projet, il ne peut pas être supprimé. Créez d'abord un nouveau projet.",
"custom_scripts": "Scripts personnalisés",
"custom_scripts_card_description": "Ajouter des scripts de suivi et des pixels à toutes les enquêtes par lien dans cet espace de travail.",
"custom_scripts_description": "Les scripts seront injectés dans le <head> de toutes les pages d'enquête par lien.",
"custom_scripts_label": "Scripts HTML",
"custom_scripts_placeholder": "<!-- Collez vos scripts de suivi ici -->\n<script>\n // Google Tag Manager, Analytics, etc.\n</script>",
"custom_scripts_updated_successfully": "Scripts personnalisés mis à jour avec succès",
"custom_scripts_warning": "Les scripts s'exécutent avec un accès complet au navigateur. Ajoutez uniquement des scripts provenant de sources fiables.",
"delete_workspace": "Supprimer le projet",
"delete_workspace_confirmation": "Êtes-vous sûr de vouloir supprimer {projectName}? Cette action ne peut pas être annulée.",
"delete_workspace_name_includes_surveys_responses_people_and_more": "Supprimer {projectName} y compris toutes les enquêtes, réponses, personnes, actions et attributs.",

View File

@@ -1690,6 +1690,22 @@
"url_encryption_description": "カスタムの単一使用IDを設定する必要がある場合にのみ無効にしてください。",
"url_encryption_label": "単一使用IDのURL暗号化"
},
"custom_html": {
"add_mode_description": "アンケートスクリプトは、ワークスペースレベルのスクリプトに加えて実行されます。",
"add_to_workspace": "ワークスペーススクリプトに追加",
"description": "このアンケートにトラッキングスクリプトとピクセルを追加",
"nav_title": "カスタムHTML",
"no_workspace_scripts": "ワークスペースレベルのスクリプトが設定されていません。ワークスペース設定→一般から追加できます。",
"placeholder": "<!-- トラッキングスクリプトをここに貼り付けてください -->\n<script>\n // Google Tag Manager、Analyticsなど\n</script>",
"replace_mode_description": "アンケートスクリプトのみが実行されます。ワークスペーススクリプトは無視されます。スクリプトを読み込まない場合は空のままにしてください。",
"replace_workspace": "ワークスペーススクリプトを置き換え",
"saved_successfully": "カスタムスクリプトを正常に保存しました",
"script_mode": "スクリプトモード",
"security_warning": "スクリプトはブラウザへの完全なアクセス権で実行されます。信頼できるソースからのスクリプトのみを追加してください。",
"survey_scripts_description": "このアンケートページの<head>に挿入するカスタムHTMLを追加します。",
"survey_scripts_label": "アンケート固有のスクリプト",
"workspace_scripts_label": "ワークスペーススクリプト(継承)"
},
"dynamic_popup": {
"alert_button": "フォームを編集",
"alert_description": "このフォームは現在、動的なポップアップをサポートしていないリンクフォームとして設定されています。フォームエディターの設定タブでこれを変更できます。",
@@ -1929,6 +1945,13 @@
},
"general": {
"cannot_delete_only_workspace": "これは唯一のワークスペースのため、削除できません。まず新しいワークスペースを作成してください。",
"custom_scripts": "カスタムスクリプト",
"custom_scripts_card_description": "このワークスペース内のすべてのリンクアンケートにトラッキングスクリプトとピクセルを追加します。",
"custom_scripts_description": "すべてのリンクアンケートページの<head>にスクリプトが挿入されます。",
"custom_scripts_label": "HTMLスクリプト",
"custom_scripts_placeholder": "<!-- トラッキングスクリプトをここに貼り付けてください -->\n<script>\n // Google Tag Manager、Analyticsなど\n</script>",
"custom_scripts_updated_successfully": "カスタムスクリプトを正常に更新しました",
"custom_scripts_warning": "スクリプトはブラウザへの完全なアクセス権で実行されます。信頼できるソースからのスクリプトのみを追加してください。",
"delete_workspace": "ワークスペースを削除",
"delete_workspace_confirmation": "{projectName}を削除してもよろしいですか?このアクションは元に戻せません。",
"delete_workspace_name_includes_surveys_responses_people_and_more": "{projectName}をすべてのフォーム、回答、人物、アクション、属性を含めて削除します。",

View File

@@ -1690,6 +1690,22 @@
"url_encryption_description": "Schakel dit alleen uit als u een aangepaste ID voor eenmalig gebruik moet instellen.",
"url_encryption_label": "URL-codering van ID voor eenmalig gebruik"
},
"custom_html": {
"add_mode_description": "Enquêtescripts worden uitgevoerd naast scripts op werkruimteniveau.",
"add_to_workspace": "Toevoegen aan werkruimtescripts",
"description": "Voeg trackingscripts en pixels toe aan deze enquête",
"nav_title": "Aangepaste HTML",
"no_workspace_scripts": "Geen scripts op werkruimteniveau geconfigureerd. Je kunt ze toevoegen in Werkruimte-instellingen → Algemeen.",
"placeholder": "<!-- Plak hier je trackingscripts -->\n<script>\n // Google Tag Manager, Analytics, etc.\n</script>",
"replace_mode_description": "Alleen enquêtescripts worden uitgevoerd. Werkruimtescripts worden genegeerd. Laat leeg om geen scripts te laden.",
"replace_workspace": "Werkruimtescripts vervangen",
"saved_successfully": "Aangepaste scripts succesvol opgeslagen",
"script_mode": "Scriptmodus",
"security_warning": "Scripts worden uitgevoerd met volledige browsertoegang. Voeg alleen scripts toe van vertrouwde bronnen.",
"survey_scripts_description": "Voeg aangepaste HTML toe om te injecteren in de <head> van deze enquêtepagina.",
"survey_scripts_label": "Enquêtespecifieke scripts",
"workspace_scripts_label": "Werkruimtescripts (overgenomen)"
},
"dynamic_popup": {
"alert_button": "Enquête bewerken",
"alert_description": "Deze enquête is momenteel geconfigureerd als een linkenquête, die geen dynamische pop-ups ondersteunt. U kunt dit wijzigen op het tabblad Instellingen van de enquête-editor.",
@@ -1929,6 +1945,13 @@
},
"general": {
"cannot_delete_only_workspace": "Dit is uw enige project, het kan niet worden verwijderd. Maak eerst een nieuw project aan.",
"custom_scripts": "Aangepaste scripts",
"custom_scripts_card_description": "Voeg trackingscripts en pixels toe aan alle linkenquêtes in deze werkruimte.",
"custom_scripts_description": "Scripts worden geïnjecteerd in de <head> van alle linkenquêtepagina's.",
"custom_scripts_label": "HTML-scripts",
"custom_scripts_placeholder": "<!-- Plak hier je trackingscripts -->\n<script>\n // Google Tag Manager, Analytics, etc.\n</script>",
"custom_scripts_updated_successfully": "Aangepaste scripts succesvol bijgewerkt",
"custom_scripts_warning": "Scripts worden uitgevoerd met volledige browsertoegang. Voeg alleen scripts toe van vertrouwde bronnen.",
"delete_workspace": "Project verwijderen",
"delete_workspace_confirmation": "Weet u zeker dat u {projectName} wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt.",
"delete_workspace_name_includes_surveys_responses_people_and_more": "Verwijder {projectName} incl. alle enquêtes, reacties, mensen, acties en attributen.",

View File

@@ -1690,6 +1690,22 @@
"url_encryption_description": "Desative apenas se precisar definir um ID de uso único personalizado",
"url_encryption_label": "Criptografia de URL de ID de uso único"
},
"custom_html": {
"add_mode_description": "Os scripts da pesquisa serão executados além dos scripts do nível do workspace.",
"add_to_workspace": "Adicionar aos scripts do workspace",
"description": "Adicione scripts de rastreamento e pixels a esta pesquisa",
"nav_title": "HTML personalizado",
"no_workspace_scripts": "Nenhum script de nível de workspace configurado. Você pode adicioná-los em Configurações do Workspace → Geral.",
"placeholder": "<!-- Cole seus scripts de rastreamento aqui -->\n<script>\n // Google Tag Manager, Analytics, etc.\n</script>",
"replace_mode_description": "Apenas os scripts da pesquisa serão executados. Os scripts do workspace serão ignorados. Deixe vazio para não carregar nenhum script.",
"replace_workspace": "Substituir scripts do workspace",
"saved_successfully": "Scripts personalizados salvos com sucesso",
"script_mode": "Modo de script",
"security_warning": "Os scripts são executados com acesso total ao navegador. Adicione apenas scripts de fontes confiáveis.",
"survey_scripts_description": "Adicione HTML personalizado para injetar no <head> desta página de pesquisa.",
"survey_scripts_label": "Scripts específicos da pesquisa",
"workspace_scripts_label": "Scripts do workspace (herdados)"
},
"dynamic_popup": {
"alert_button": "Editar pesquisa",
"alert_description": "Esta pesquisa está atualmente configurada como uma pesquisa de link, o que não suporta pop-ups dinâmicos. Você pode alterar isso na aba de configurações do editor de pesquisas.",
@@ -1929,6 +1945,13 @@
},
"general": {
"cannot_delete_only_workspace": "Este é seu único projeto, ele não pode ser excluído. Crie um novo projeto primeiro.",
"custom_scripts": "Scripts personalizados",
"custom_scripts_card_description": "Adicione scripts de rastreamento e pixels a todas as pesquisas de link neste workspace.",
"custom_scripts_description": "Os scripts serão injetados no <head> de todas as páginas de pesquisa de link.",
"custom_scripts_label": "Scripts HTML",
"custom_scripts_placeholder": "<!-- Cole seus scripts de rastreamento aqui -->\n<script>\n // Google Tag Manager, Analytics, etc.\n</script>",
"custom_scripts_updated_successfully": "Scripts personalizados atualizados com sucesso",
"custom_scripts_warning": "Os scripts são executados com acesso total ao navegador. Adicione apenas scripts de fontes confiáveis.",
"delete_workspace": "Excluir projeto",
"delete_workspace_confirmation": "Tem certeza de que deseja excluir {projectName}? Essa ação não pode ser desfeita.",
"delete_workspace_name_includes_surveys_responses_people_and_more": "Excluir {projectName} incluindo todas as pesquisas, respostas, pessoas, ações e atributos.",

View File

@@ -1690,6 +1690,22 @@
"url_encryption_description": "Desative apenas se precisar definir um ID de uso único personalizado.",
"url_encryption_label": "Encriptação do URL de ID de uso único"
},
"custom_html": {
"add_mode_description": "Os scripts do inquérito serão executados para além dos scripts ao nível da área de trabalho.",
"add_to_workspace": "Adicionar aos scripts da área de trabalho",
"description": "Adicionar scripts de rastreamento e pixels a este inquérito",
"nav_title": "HTML personalizado",
"no_workspace_scripts": "Nenhum script ao nível da área de trabalho configurado. Pode adicioná-los em Definições da Área de Trabalho → Geral.",
"placeholder": "<!-- Cole os seus scripts de rastreamento aqui -->\n<script>\n // Google Tag Manager, Analytics, etc.\n</script>",
"replace_mode_description": "Apenas os scripts do inquérito serão executados. Os scripts da área de trabalho serão ignorados. Deixe vazio para não carregar nenhum script.",
"replace_workspace": "Substituir scripts da área de trabalho",
"saved_successfully": "Scripts personalizados guardados com sucesso",
"script_mode": "Modo de script",
"security_warning": "Os scripts são executados com acesso total ao navegador. Adicione apenas scripts de fontes fidedignas.",
"survey_scripts_description": "Adicionar HTML personalizado para injetar no <head> desta página de inquérito.",
"survey_scripts_label": "Scripts específicos do inquérito",
"workspace_scripts_label": "Scripts da área de trabalho (herdados)"
},
"dynamic_popup": {
"alert_button": "Editar inquérito",
"alert_description": "Este questionário está atualmente configurado como um questionário de link, que não suporta pop-ups dinâmicos. Você pode alterar isso na aba de configurações do editor de questionários.",
@@ -1929,6 +1945,13 @@
},
"general": {
"cannot_delete_only_workspace": "Este é o seu único projeto, não pode ser eliminado. Crie primeiro um novo projeto.",
"custom_scripts": "Scripts personalizados",
"custom_scripts_card_description": "Adicionar scripts de rastreamento e pixels a todos os inquéritos de link nesta área de trabalho.",
"custom_scripts_description": "Os scripts serão injetados no <head> de todas as páginas de inquéritos de link.",
"custom_scripts_label": "Scripts HTML",
"custom_scripts_placeholder": "<!-- Cole os seus scripts de rastreamento aqui -->\n<script>\n // Google Tag Manager, Analytics, etc.\n</script>",
"custom_scripts_updated_successfully": "Scripts personalizados atualizados com sucesso",
"custom_scripts_warning": "Os scripts são executados com acesso total ao navegador. Adicione apenas scripts de fontes fidedignas.",
"delete_workspace": "Eliminar projeto",
"delete_workspace_confirmation": "Tem a certeza de que pretende eliminar {projectName}? Esta ação não pode ser desfeita.",
"delete_workspace_name_includes_surveys_responses_people_and_more": "Eliminar {projectName} incluindo todos os inquéritos, respostas, pessoas, ações e atributos.",

View File

@@ -1690,6 +1690,22 @@
"url_encryption_description": "Dezactivați doar dacă trebuie să setați un ID unic personalizat.",
"url_encryption_label": "Criptarea URL pentru ID unic de utilizare"
},
"custom_html": {
"add_mode_description": "Scripturile sondajului vor rula în plus față de scripturile la nivel de spațiu de lucru.",
"add_to_workspace": "Adaugă la scripturile spațiului de lucru",
"description": "Adaugă scripturi de tracking și pixeli acestui sondaj",
"nav_title": "HTML personalizat",
"no_workspace_scripts": "Nu există scripturi la nivel de spațiu de lucru configurate. Le poți adăuga în Setări spațiu de lucru → General.",
"placeholder": "<!-- Lipește aici scripturile tale de tracking -->\n<script>\n // Google Tag Manager, Analytics, etc.\n</script>",
"replace_mode_description": "Vor rula doar scripturile sondajului. Scripturile spațiului de lucru vor fi ignorate. Lasă gol pentru a nu încărca niciun script.",
"replace_workspace": "Înlocuiește scripturile spațiului de lucru",
"saved_successfully": "Scripturile personalizate au fost salvate cu succes",
"script_mode": "Modul script",
"security_warning": "Scripturile se execută cu acces complet la browser. Adaugă doar scripturi din surse de încredere.",
"survey_scripts_description": "Adaugă HTML personalizat pentru a fi injectat în <head> pe această pagină de sondaj.",
"survey_scripts_label": "Scripturi specifice sondajului",
"workspace_scripts_label": "Scripturi spațiu de lucru (moștenite)"
},
"dynamic_popup": {
"alert_button": "Editează chestionar",
"alert_description": "Acest sondaj este configurat în prezent ca un sondaj cu link, care nu suportă pop-up-uri dinamice. Puteți schimba acest lucru în fila de setări a editorului de sondaje.",
@@ -1929,6 +1945,13 @@
},
"general": {
"cannot_delete_only_workspace": "Acesta este singurul tău proiect, nu poate fi șters. Creează mai întâi un proiect nou.",
"custom_scripts": "Scripturi personalizate",
"custom_scripts_card_description": "Adaugă scripturi de tracking și pixeli tuturor sondajelor cu link din acest spațiu de lucru.",
"custom_scripts_description": "Scripturile vor fi injectate în <head> pe toate paginile sondajelor cu link.",
"custom_scripts_label": "Scripturi HTML",
"custom_scripts_placeholder": "<!-- Lipește aici scripturile tale de tracking -->\n<script>\n // Google Tag Manager, Analytics, etc.\n</script>",
"custom_scripts_updated_successfully": "Scripturile personalizate au fost actualizate cu succes",
"custom_scripts_warning": "Scripturile se execută cu acces complet la browser. Adaugă doar scripturi din surse de încredere.",
"delete_workspace": "Șterge proiectul",
"delete_workspace_confirmation": "Sigur vrei să ștergi {projectName}? Această acțiune nu poate fi anulată.",
"delete_workspace_name_includes_surveys_responses_people_and_more": "Șterge {projectName} incl. toate sondajele, răspunsurile, persoanele, acțiunile și atributele.",

View File

@@ -1690,6 +1690,22 @@
"url_encryption_description": "Отключайте только если нужно задать собственный одноразовый ID.",
"url_encryption_label": "Шифрование URL для одноразового ID"
},
"custom_html": {
"add_mode_description": "Скрипты опроса будут выполняться дополнительно к скриптам на уровне рабочего пространства.",
"add_to_workspace": "Добавить к скриптам рабочего пространства",
"description": "Добавьте трекинговые скрипты и пиксели в этот опрос",
"nav_title": "Пользовательский HTML",
"no_workspace_scripts": "Скрипты на уровне рабочего пространства не настроены. Вы можете добавить их в настройках рабочего пространства → Общие.",
"placeholder": "<!-- Вставьте сюда ваши трекинговые скрипты -->\n<script>\n // Google Tag Manager, Analytics и др.\n</script>",
"replace_mode_description": "Будут выполняться только скрипты опроса. Скрипты рабочего пространства будут проигнорированы. Оставьте пустым, чтобы не загружать скрипты.",
"replace_workspace": "Заменить скрипты рабочего пространства",
"saved_successfully": "Пользовательские скрипты успешно сохранены",
"script_mode": "Режим скриптов",
"security_warning": "Скрипты выполняются с полным доступом к браузеру. Добавляйте только скрипты из доверенных источников.",
"survey_scripts_description": "Добавьте пользовательский HTML для внедрения в <head> этой страницы опроса.",
"survey_scripts_label": "Скрипты, специфичные для опроса",
"workspace_scripts_label": "Скрипты рабочего пространства (унаследованные)"
},
"dynamic_popup": {
"alert_button": "Редактировать опрос",
"alert_description": "Этот опрос сейчас настроен как опрос по ссылке, что не поддерживает динамические pop-up окна. Вы можете изменить это на вкладке настроек редактора опроса.",
@@ -1929,6 +1945,13 @@
},
"general": {
"cannot_delete_only_workspace": "Это ваш единственный рабочий проект, его нельзя удалить. Сначала создайте новый проект.",
"custom_scripts": "Пользовательские скрипты",
"custom_scripts_card_description": "Добавьте трекинговые скрипты и пиксели ко всем опросам по ссылке в этом рабочем пространстве.",
"custom_scripts_description": "Скрипты будут внедряться в <head> всех страниц опросов по ссылке.",
"custom_scripts_label": "HTML-скрипты",
"custom_scripts_placeholder": "<!-- Вставьте сюда ваши трекинговые скрипты -->\n<script>\n // Google Tag Manager, Analytics и др.\n</script>",
"custom_scripts_updated_successfully": "Пользовательские скрипты успешно обновлены",
"custom_scripts_warning": "Скрипты выполняются с полным доступом к браузеру. Добавляйте только скрипты из доверенных источников.",
"delete_workspace": "Удалить рабочий проект",
"delete_workspace_confirmation": "Вы уверены, что хотите удалить {projectName}? Это действие необратимо.",
"delete_workspace_name_includes_surveys_responses_people_and_more": "Удалить {projectName} вместе со всеми опросами, ответами, пользователями, действиями и атрибутами.",

View File

@@ -1690,6 +1690,22 @@
"url_encryption_description": "Inaktivera endast om du behöver ange ett anpassat engångs-ID.",
"url_encryption_label": "URL-kryptering av engångs-ID"
},
"custom_html": {
"add_mode_description": "Undersökningsskript kommer att köras utöver arbetsytans skript.",
"add_to_workspace": "Lägg till i arbetsytans skript",
"description": "Lägg till spårningsskript och pixlar i denna undersökning",
"nav_title": "Anpassad HTML",
"no_workspace_scripts": "Inga arbetsytans skript har konfigurerats. Du kan lägga till dem i Arbetsytans inställningar → Allmänt.",
"placeholder": "<!-- Klistra in dina spårningsskript här -->\n<script>\n // Google Tag Manager, Analytics, etc.\n</script>",
"replace_mode_description": "Endast undersökningsskript kommer att köras. Arbetsytans skript ignoreras. Lämna tomt för att inte ladda några skript.",
"replace_workspace": "Ersätt arbetsytans skript",
"saved_successfully": "Anpassade skript har sparats",
"script_mode": "Skriptläge",
"security_warning": "Skript körs med full åtkomst till webbläsaren. Lägg endast till skript från betrodda källor.",
"survey_scripts_description": "Lägg till anpassad HTML för att injicera i <head> på denna undersökningssida.",
"survey_scripts_label": "Undersökningsspecifika skript",
"workspace_scripts_label": "Arbetsytans skript (ärvda)"
},
"dynamic_popup": {
"alert_button": "Redigera enkät",
"alert_description": "Denna enkät är för närvarande konfigurerad som en länkenkät, vilket inte stöder dynamiska popup-fönster. Du kan ändra detta i inställningsfliken i enkätredigeraren.",
@@ -1929,6 +1945,13 @@
},
"general": {
"cannot_delete_only_workspace": "Detta är din enda arbetsyta, den kan inte tas bort. Skapa först en ny arbetsyta.",
"custom_scripts": "Anpassade skript",
"custom_scripts_card_description": "Lägg till spårningsskript och pixlar i alla länkundersökningar i denna arbetsyta.",
"custom_scripts_description": "Skript kommer att injiceras i <head> på alla länkundersökningssidor.",
"custom_scripts_label": "HTML-skript",
"custom_scripts_placeholder": "<!-- Klistra in dina spårningsskript här -->\n<script>\n // Google Tag Manager, Analytics, etc.\n</script>",
"custom_scripts_updated_successfully": "Anpassade skript har uppdaterats",
"custom_scripts_warning": "Skript körs med full åtkomst till webbläsaren. Lägg endast till skript från betrodda källor.",
"delete_workspace": "Ta bort arbetsyta",
"delete_workspace_confirmation": "Är du säker på att du vill ta bort {projectName}? Denna åtgärd kan inte ångras.",
"delete_workspace_name_includes_surveys_responses_people_and_more": "Ta bort {projectName} inkl. alla enkäter, svar, personer, åtgärder och attribut.",

View File

@@ -1690,6 +1690,22 @@
"url_encryption_description": "仅在 需要 设置 自定义 单次使用 ID 时 才 禁用。",
"url_encryption_label": "单次 使用 ID 的 URL 加密"
},
"custom_html": {
"add_mode_description": "调查脚本将在工作区级脚本的基础上运行。",
"add_to_workspace": "添加到工作区脚本",
"description": "为此调查添加跟踪脚本和像素代码",
"nav_title": "自定义 HTML",
"no_workspace_scripts": "尚未配置工作区级脚本。你可以在工作区设置 → 常规中添加。",
"placeholder": "<!-- 在此粘贴你的跟踪脚本 -->\n<script>\n // Google Tag Manager、Analytics 等\n</script>",
"replace_mode_description": "仅运行调查脚本,工作区脚本将被忽略。保持为空则不加载任何脚本。",
"replace_workspace": "替换工作区脚本",
"saved_successfully": "自定义脚本保存成功",
"script_mode": "脚本模式",
"security_warning": "脚本将以完整浏览器权限执行。请仅添加来自可信来源的脚本。",
"survey_scripts_description": "添加自定义 HTML 注入到此调查页面的<head>中。",
"survey_scripts_label": "调查专用脚本",
"workspace_scripts_label": "工作区脚本(继承)"
},
"dynamic_popup": {
"alert_button": "编辑 survey",
"alert_description": "此 问卷 当前 配置 为 链接 问卷, 不 支持 动态 弹出 窗。 您 可以 在 问卷 编辑器 的 设置 选项 中 进行 修改。",
@@ -1929,6 +1945,13 @@
},
"general": {
"cannot_delete_only_workspace": "这是您唯一的工作区,无法删除。请先创建一个新工作区。",
"custom_scripts": "自定义脚本",
"custom_scripts_card_description": "为此工作区内所有链接调查添加跟踪脚本和像素代码。",
"custom_scripts_description": "脚本将被注入到所有链接调查页面的<head>中。",
"custom_scripts_label": "HTML 脚本",
"custom_scripts_placeholder": "<!-- 在此粘贴你的跟踪脚本 -->\n<script>\n // Google Tag Manager、Analytics 等\n</script>",
"custom_scripts_updated_successfully": "自定义脚本更新成功",
"custom_scripts_warning": "脚本将以完整浏览器权限执行。请仅添加来自可信来源的脚本。",
"delete_workspace": "删除工作区",
"delete_workspace_confirmation": "您确定要删除 {projectName} 吗?此操作无法撤销。",
"delete_workspace_name_includes_surveys_responses_people_and_more": "删除 {projectName},包括所有调查、回应、人员、动作和属性。",

View File

@@ -1690,6 +1690,22 @@
"url_encryption_description": "僅在需要設定自訂一次性 ID 時停用",
"url_encryption_label": "單次使用 ID 的 URL 加密"
},
"custom_html": {
"add_mode_description": "調查問卷腳本將會與工作區層級的腳本一同執行。",
"add_to_workspace": "加入至工作區腳本",
"description": "將追蹤腳本與像素碼加入此調查問卷",
"nav_title": "自訂 HTML",
"no_workspace_scripts": "尚未設定工作區層級腳本。您可以在「工作區設定」→「一般」中新增。",
"placeholder": "<!-- 請在此貼上您的追蹤腳本 -->\n<script>\n // Google Tag Manager、Analytics 等\n</script>",
"replace_mode_description": "僅執行調查問卷腳本,將忽略工作區腳本。若不需載入任何腳本,請保持空白。",
"replace_workspace": "取代工作區腳本",
"saved_successfully": "自訂腳本已成功儲存",
"script_mode": "腳本模式",
"security_warning": "腳本將以完整瀏覽器權限執行。請僅加入來自可信來源的腳本。",
"survey_scripts_description": "新增自訂 HTML 以注入至此調查問卷頁面的 <head>。",
"survey_scripts_label": "調查問卷專屬腳本",
"workspace_scripts_label": "工作區腳本(繼承)"
},
"dynamic_popup": {
"alert_button": "編輯 問卷",
"alert_description": "此 問卷 目前 被 設定 為 連結 問卷,不 支援 動態 彈出窗口。您 可 在 問卷 編輯器 的 設定 標籤 中 進行 更改。",
@@ -1929,6 +1945,13 @@
},
"general": {
"cannot_delete_only_workspace": "這是您唯一的工作區,無法刪除。請先建立新的工作區。",
"custom_scripts": "自訂腳本",
"custom_scripts_card_description": "將追蹤腳本與像素碼加入此工作區內所有連結調查問卷。",
"custom_scripts_description": "腳本將注入至所有連結調查問卷頁面的 <head>。",
"custom_scripts_label": "HTML 腳本",
"custom_scripts_placeholder": "<!-- 請在此貼上您的追蹤腳本 -->\n<script>\n // Google Tag Manager、Analytics 等\n</script>",
"custom_scripts_updated_successfully": "自訂腳本已成功更新",
"custom_scripts_warning": "腳本將以完整瀏覽器權限執行。請僅加入來自可信來源的腳本。",
"delete_workspace": "刪除工作區",
"delete_workspace_confirmation": "您確定要刪除 {projectName} 嗎?此操作無法復原。",
"delete_workspace_name_includes_surveys_responses_people_and_more": "刪除 {projectName}(包含所有問卷、回應、人員、操作和屬性)。",

View File

@@ -11,6 +11,7 @@ import {
vi.mock("@/lib/env", () => ({
env: {
ENTERPRISE_LICENSE_KEY: "test-license-key",
ENVIRONMENT: "production",
VERCEL_URL: "some.vercel.url",
FORMBRICKS_COM_URL: "https://app.formbricks.com",
HTTPS_PROXY: undefined,
@@ -690,4 +691,61 @@ describe("License Core Logic", () => {
);
});
});
describe("Environment-based endpoint selection", () => {
test("should use staging endpoint when ENVIRONMENT is staging", async () => {
vi.resetModules();
vi.doMock("@/lib/env", () => ({
env: {
ENTERPRISE_LICENSE_KEY: "test-license-key",
ENVIRONMENT: "staging",
HTTPS_PROXY: undefined,
HTTP_PROXY: undefined,
},
}));
const fetch = (await import("node-fetch")).default as Mock;
// Mock cache.withCache to execute the function (simulating cache miss)
mockCache.withCache.mockImplementation(async (fn) => await fn());
fetch.mockResolvedValueOnce({
ok: true,
json: async () => ({
data: {
status: "active",
features: {
isMultiOrgEnabled: true,
projects: 5,
twoFactorAuth: true,
sso: true,
whitelabel: true,
removeBranding: true,
contacts: true,
ai: true,
saml: true,
spamProtection: true,
auditLogs: true,
multiLanguageSurveys: true,
accessControl: true,
quotas: true,
},
},
}),
} as any);
// Re-import the module to apply the new mock
const { fetchLicense } = await import("./license");
await fetchLicense();
// Verify the staging endpoint was called
expect(fetch).toHaveBeenCalledWith(
"https://staging.ee.formbricks.com/api/licenses/check",
expect.objectContaining({
method: "POST",
headers: { "Content-Type": "application/json" },
})
);
});
});
});

View File

@@ -26,7 +26,10 @@ const CONFIG = {
RETRY_DELAY_MS: 1000,
},
API: {
ENDPOINT: "https://ee.formbricks.com/api/licenses/check",
ENDPOINT:
env.ENVIRONMENT === "staging"
? "https://staging.ee.formbricks.com/api/licenses/check"
: "https://ee.formbricks.com/api/licenses/check",
TIMEOUT_MS: 5000,
},
} as const;

View File

@@ -153,6 +153,7 @@ export const getEnvironmentWithRelations = reactCache(async (environmentId: stri
darkOverlay: true,
styling: true,
logo: true,
customHeadScripts: true,
// All project environments
environments: {
select: {
@@ -222,6 +223,7 @@ export const getEnvironmentWithRelations = reactCache(async (environmentId: stri
darkOverlay: data.project.darkOverlay,
styling: data.project.styling,
logo: data.project.logo,
customHeadScripts: data.project.customHeadScripts,
environments: data.project.environments,
},
organization: {

View File

@@ -0,0 +1,124 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangleIcon } from "lucide-react";
import { SubmitHandler, useForm } from "react-hook-form";
import toast from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { z } from "zod";
import { TProject } from "@formbricks/types/project";
import { cn } from "@/lib/cn";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { Alert, AlertDescription } from "@/modules/ui/components/alert";
import { Button } from "@/modules/ui/components/button";
import {
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormProvider,
} from "@/modules/ui/components/form";
import { updateProjectAction } from "../../actions";
interface CustomScriptsFormProps {
project: TProject;
isReadOnly: boolean;
}
const ZCustomScriptsInput = z.object({
customHeadScripts: z.string().nullish(),
});
type TCustomScriptsFormValues = z.infer<typeof ZCustomScriptsInput>;
export const CustomScriptsForm: React.FC<CustomScriptsFormProps> = ({ project, isReadOnly }) => {
const { t } = useTranslation();
const form = useForm<TCustomScriptsFormValues>({
defaultValues: {
customHeadScripts: project.customHeadScripts ?? "",
},
resolver: zodResolver(ZCustomScriptsInput),
mode: "onChange",
});
const { isDirty, isSubmitting } = form.formState;
const updateCustomScripts: SubmitHandler<TCustomScriptsFormValues> = async (data) => {
try {
const updatedProjectResponse = await updateProjectAction({
projectId: project.id,
data: {
customHeadScripts: data.customHeadScripts || null,
},
});
if (updatedProjectResponse?.data) {
toast.success(t("environments.workspace.general.custom_scripts_updated_successfully"));
form.reset({ customHeadScripts: updatedProjectResponse.data.customHeadScripts ?? "" });
} else {
const errorMessage = getFormattedErrorMessage(updatedProjectResponse);
toast.error(errorMessage);
}
} catch {
toast.error(t("common.something_went_wrong_please_try_again"));
}
};
return (
<>
<FormProvider {...form}>
<form className="flex w-full flex-col space-y-4" onSubmit={form.handleSubmit(updateCustomScripts)}>
<Alert variant="warning" className="flex items-start gap-2">
<AlertTriangleIcon className="mt-0.5 h-4 w-4 shrink-0" />
<AlertDescription>{t("environments.workspace.general.custom_scripts_warning")}</AlertDescription>
</Alert>
<FormField
control={form.control}
name="customHeadScripts"
render={({ field }) => (
<FormItem>
<FormLabel htmlFor="customHeadScripts">
{t("environments.workspace.general.custom_scripts_label")}
</FormLabel>
<FormDescription>
{t("environments.workspace.general.custom_scripts_description")}
</FormDescription>
<FormControl>
<textarea
id="customHeadScripts"
rows={8}
placeholder={t("environments.workspace.general.custom_scripts_placeholder")}
className={cn(
"focus:border-brand-dark flex w-full rounded-md border border-slate-300 bg-transparent px-3 py-2 font-mono text-xs text-slate-800 placeholder:text-slate-400 focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50",
isReadOnly && "bg-slate-50"
)}
{...field}
value={field.value ?? ""}
disabled={isReadOnly}
/>
</FormControl>
</FormItem>
)}
/>
<Button
type="submit"
size="sm"
className="w-fit"
loading={isSubmitting}
disabled={isSubmitting || !isDirty || isReadOnly}>
{t("common.save")}
</Button>
</form>
</FormProvider>
{isReadOnly && (
<Alert variant="warning" className="mt-4">
<AlertDescription>
{t("common.only_owners_managers_and_manage_access_members_can_perform_this_action")}
</AlertDescription>
</Alert>
)}
</>
);
};

View File

@@ -8,6 +8,7 @@ import { IdBadge } from "@/modules/ui/components/id-badge";
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
import { PageHeader } from "@/modules/ui/components/page-header";
import packageJson from "@/package.json";
import { CustomScriptsForm } from "./components/custom-scripts-form";
import { DeleteProject } from "./components/delete-project";
import { EditProjectNameForm } from "./components/edit-project-name-form";
import { EditWaitingTimeForm } from "./components/edit-waiting-time-form";
@@ -39,6 +40,13 @@ export const GeneralSettingsPage = async (props: { params: Promise<{ environment
description={t("environments.workspace.general.recontact_waiting_time_settings_description")}>
<EditWaitingTimeForm project={project} isReadOnly={isReadOnly} />
</SettingsCard>
{!IS_FORMBRICKS_CLOUD && (
<SettingsCard
title={t("environments.workspace.general.custom_scripts")}
description={t("environments.workspace.general.custom_scripts_card_description")}>
<CustomScriptsForm project={project} isReadOnly={!isOwnerOrManager} />
</SettingsCard>
)}
<SettingsCard
title={t("environments.workspace.general.delete_workspace")}
description={t("environments.workspace.general.delete_workspace_settings_description")}>

View File

@@ -28,6 +28,7 @@ const selectProject = {
environments: true,
styling: true,
logo: true,
customHeadScripts: true,
};
export const updateProject = async (

View File

@@ -112,7 +112,7 @@ export const InviteMembers = ({ IS_SMTP_CONFIGURED, organizationId }: InviteMemb
<div className="relative">
<Input
{...field}
placeholder={`Full Name (optional)`}
placeholder={t("common.full_name")}
className="w-80"
isInvalid={Boolean(error?.message)}
/>

View File

@@ -111,7 +111,7 @@ export const CTAElementForm = ({
description={t("environments.surveys.edit.button_external_description")}
childBorder
customContainerClass="p-0 mt-4">
<div className="flex flex-1 flex-col gap-2 px-4 pb-4 pt-1">
<div className="flex flex-1 flex-col gap-2 px-4 pt-1 pb-4">
<ElementFormInput
id="ctaButtonLabel"
value={element.ctaButtonLabel}
@@ -133,6 +133,7 @@ export const CTAElementForm = ({
<Input
id="buttonUrl"
name="buttonUrl"
className="mt-1 bg-white"
value={element.buttonUrl}
placeholder="https://website.com"
onChange={(e) => updateElement(elementIdx, { buttonUrl: e.target.value })}

View File

@@ -271,6 +271,7 @@ export const updateSurvey = async (updatedSurvey: TSurvey): Promise<TSurvey> =>
...prismaSurvey, // Properties from prismaSurvey
displayPercentage: Number(prismaSurvey.displayPercentage) || null,
segment: surveySegment,
customHeadScriptsMode: prismaSurvey.customHeadScriptsMode,
};
return modifiedSurvey;

View File

@@ -40,6 +40,8 @@ export const selectSurvey = {
isBackButtonHidden: true,
metadata: true,
slug: true,
customHeadScripts: true,
customHeadScriptsMode: true,
languages: {
select: {
default: true,

View File

@@ -20,6 +20,7 @@ export const transformPrismaSurvey = <T extends TSurvey | TJsEnvironmentStateSur
...surveyPrisma,
displayPercentage: Number(surveyPrisma.displayPercentage) || null,
segment,
customHeadScriptsMode: surveyPrisma.customHeadScriptsMode,
} as T;
return transformedSurvey;

View File

@@ -0,0 +1,82 @@
"use client";
import { useEffect, useRef } from "react";
import { TSurvey } from "@formbricks/types/surveys/types";
interface CustomScriptsInjectorProps {
projectScripts?: string | null;
surveyScripts?: string | null;
scriptsMode?: TSurvey["customHeadScriptsMode"];
}
/**
* Injects custom HTML scripts into the document head for link surveys.
* Supports merging project and survey scripts or replacing project scripts with survey scripts.
*
* @param projectScripts - Scripts configured at the workspace/project level
* @param surveyScripts - Scripts configured at the survey level
* @param scriptsMode - "add" merges both, "replace" uses only survey scripts
*/
export const CustomScriptsInjector = ({
projectScripts,
surveyScripts,
scriptsMode,
}: CustomScriptsInjectorProps) => {
const injectedRef = useRef(false);
useEffect(() => {
// Prevent double injection in React strict mode
if (injectedRef.current) return;
// Determine which scripts to inject based on mode
let scriptsToInject: string;
if (scriptsMode === "replace" && surveyScripts) {
// Replace mode: only use survey scripts
scriptsToInject = surveyScripts;
} else {
// Add mode (default): merge project and survey scripts
scriptsToInject = [projectScripts, surveyScripts].filter(Boolean).join("\n");
}
if (!scriptsToInject.trim()) return;
try {
// Create a temporary container to parse the HTML
const container = document.createElement("div");
container.innerHTML = scriptsToInject;
// Process and inject script elements
const scripts = container.querySelectorAll("script");
scripts.forEach((script) => {
const newScript = document.createElement("script");
// Copy all attributes (src, async, defer, type, etc.)
Array.from(script.attributes).forEach((attr) => {
newScript.setAttribute(attr.name, attr.value);
});
// Copy inline script content
if (script.textContent) {
newScript.textContent = script.textContent;
}
document.head.appendChild(newScript);
});
// Process and inject non-script elements (noscript, meta, link, style, etc.)
const nonScripts = container.querySelectorAll(":not(script)");
nonScripts.forEach((el) => {
const clonedEl = el.cloneNode(true) as Element;
document.head.appendChild(clonedEl);
});
injectedRef.current = true;
} catch (error) {
// Log error but don't break the survey - self-hosted admins can check console
console.warn("[Formbricks] Error injecting custom scripts:", error);
}
}, [projectScripts, surveyScripts, scriptsMode]);
return null;
};

View File

@@ -13,7 +13,7 @@ import { OTPInput } from "@/modules/ui/components/otp-input";
interface PinScreenProps {
surveyId: string;
project: Pick<Project, "styling" | "logo" | "linkSurveyBranding">;
project: Pick<Project, "styling" | "logo" | "linkSurveyBranding" | "customHeadScripts">;
singleUseId?: string;
singleUseResponse?: Pick<Response, "id" | "finished">;
publicDomain: string;

View File

@@ -7,13 +7,14 @@ import { TProjectStyling } from "@formbricks/types/project";
import { TResponseData } from "@formbricks/types/responses";
import { TSurvey, TSurveyStyling } from "@formbricks/types/surveys/types";
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
import { CustomScriptsInjector } from "@/modules/survey/link/components/custom-scripts-injector";
import { LinkSurveyWrapper } from "@/modules/survey/link/components/link-survey-wrapper";
import { getPrefillValue } from "@/modules/survey/link/lib/prefill";
import { SurveyInline } from "@/modules/ui/components/survey";
interface SurveyClientWrapperProps {
survey: TSurvey;
project: Pick<Project, "styling" | "logo" | "linkSurveyBranding">;
project: Pick<Project, "styling" | "logo" | "linkSurveyBranding" | "customHeadScripts">;
styling: TProjectStyling | TSurveyStyling;
publicDomain: string;
responseCount?: number;
@@ -117,52 +118,62 @@ export const SurveyClientWrapper = ({
};
return (
<LinkSurveyWrapper
project={project}
surveyId={survey.id}
isWelcomeCardEnabled={survey.welcomeCard.enabled}
isPreview={isPreview}
surveyType={survey.type}
determineStyling={() => styling}
handleResetSurvey={handleResetSurvey}
isEmbed={isEmbed}
publicDomain={publicDomain}
IS_FORMBRICKS_CLOUD={IS_FORMBRICKS_CLOUD}
IMPRINT_URL={IMPRINT_URL}
PRIVACY_URL={PRIVACY_URL}
isBrandingEnabled={project.linkSurveyBranding}>
<SurveyInline
appUrl={publicDomain}
environmentId={survey.environmentId}
isPreviewMode={isPreview}
survey={survey}
styling={styling}
languageCode={languageCode}
isBrandingEnabled={project.linkSurveyBranding}
shouldResetQuestionId={false}
autoFocus={autoFocus}
prefillResponseData={prefillValue}
skipPrefilled={skipPrefilled}
responseCount={responseCount}
getSetBlockId={(f: (value: string) => void) => {
setBlockId = f;
}}
getSetResponseData={(f: (value: TResponseData) => void) => {
setResponseData = f;
}}
startAtQuestionId={startAt && isStartAtValid ? startAt : undefined}
fullSizeCards={isEmbed}
hiddenFieldsRecord={{
...hiddenFieldsRecord,
...getVerifiedEmail,
}}
singleUseId={singleUseId}
singleUseResponseId={singleUseResponseId}
getSetIsResponseSendingFinished={(_f: (value: boolean) => void) => {}}
contactId={contactId}
recaptchaSiteKey={recaptchaSiteKey}
isSpamProtectionEnabled={isSpamProtectionEnabled}
/>
</LinkSurveyWrapper>
<>
{/* Inject custom scripts for tracking/analytics (self-hosted only) */}
{!IS_FORMBRICKS_CLOUD && !isPreview && (
<CustomScriptsInjector
projectScripts={project.customHeadScripts}
surveyScripts={survey.customHeadScripts}
scriptsMode={survey.customHeadScriptsMode}
/>
)}
<LinkSurveyWrapper
project={project}
surveyId={survey.id}
isWelcomeCardEnabled={survey.welcomeCard.enabled}
isPreview={isPreview}
surveyType={survey.type}
determineStyling={() => styling}
handleResetSurvey={handleResetSurvey}
isEmbed={isEmbed}
publicDomain={publicDomain}
IS_FORMBRICKS_CLOUD={IS_FORMBRICKS_CLOUD}
IMPRINT_URL={IMPRINT_URL}
PRIVACY_URL={PRIVACY_URL}
isBrandingEnabled={project.linkSurveyBranding}>
<SurveyInline
appUrl={publicDomain}
environmentId={survey.environmentId}
isPreviewMode={isPreview}
survey={survey}
styling={styling}
languageCode={languageCode}
isBrandingEnabled={project.linkSurveyBranding}
shouldResetQuestionId={false}
autoFocus={autoFocus}
prefillResponseData={prefillValue}
skipPrefilled={skipPrefilled}
responseCount={responseCount}
getSetBlockId={(f: (value: string) => void) => {
setBlockId = f;
}}
getSetResponseData={(f: (value: TResponseData) => void) => {
setResponseData = f;
}}
startAtQuestionId={startAt && isStartAtValid ? startAt : undefined}
fullSizeCards={isEmbed}
hiddenFieldsRecord={{
...hiddenFieldsRecord,
...getVerifiedEmail,
}}
singleUseId={singleUseId}
singleUseResponseId={singleUseResponseId}
getSetIsResponseSendingFinished={(_f: (value: boolean) => void) => {}}
contactId={contactId}
recaptchaSiteKey={recaptchaSiteKey}
isSpamProtectionEnabled={isSpamProtectionEnabled}
/>
</LinkSurveyWrapper>
</>
);
};

View File

@@ -60,6 +60,10 @@ export const getSurveyWithMetadata = reactCache(async (surveyId: string) => {
recaptcha: true,
metadata: true,
// Custom scripts (self-hosted only)
customHeadScripts: true,
customHeadScriptsMode: true,
// Related data
languages: {
select: {

View File

@@ -85,6 +85,7 @@ describe("getEnvironmentContextForLinkSurvey", () => {
styling: true,
logo: true,
linkSurveyBranding: true,
customHeadScripts: true,
organizationId: true,
organization: {
select: {

View File

@@ -16,7 +16,10 @@ import { validateInputs } from "@/lib/utils/validate";
* deduplication within the same render cycle.
*/
type TProjectForLinkSurvey = Pick<Project, "id" | "name" | "styling" | "logo" | "linkSurveyBranding">;
type TProjectForLinkSurvey = Pick<
Project,
"id" | "name" | "styling" | "logo" | "linkSurveyBranding" | "customHeadScripts"
>;
export interface TEnvironmentContextForLinkSurvey {
project: TProjectForLinkSurvey;
@@ -61,6 +64,7 @@ export const getEnvironmentContextForLinkSurvey = reactCache(
styling: true,
logo: true,
linkSurveyBranding: true,
customHeadScripts: true,
organizationId: true,
organization: {
select: {
@@ -91,6 +95,7 @@ export const getEnvironmentContextForLinkSurvey = reactCache(
styling: environment.project.styling,
logo: environment.project.logo,
linkSurveyBranding: environment.project.linkSurveyBranding,
customHeadScripts: environment.project.customHeadScripts,
},
organizationId: environment.project.organizationId,
organizationBilling: environment.project.organization.billing,

View File

@@ -61,6 +61,7 @@ describe("getProjectByEnvironmentId", () => {
},
},
select: {
customHeadScripts: true,
linkSurveyBranding: true,
logo: true,
styling: true,

View File

@@ -10,7 +10,10 @@ import { validateInputs } from "@/lib/utils/validate";
export const getProjectByEnvironmentId = reactCache(
async (
environmentId: string
): Promise<Pick<Project, "styling" | "logo" | "linkSurveyBranding" | "name"> | null> => {
): Promise<Pick<
Project,
"styling" | "logo" | "linkSurveyBranding" | "name" | "customHeadScripts"
> | null> => {
validateInputs([environmentId, ZId]);
let projectPrisma;
@@ -29,6 +32,7 @@ export const getProjectByEnvironmentId = reactCache(
logo: true,
linkSurveyBranding: true,
name: true,
customHeadScripts: true,
},
});

View File

@@ -16,7 +16,7 @@
"generate-api-specs": "./scripts/openapi/generate.sh",
"merge-client-endpoints": "tsx ./scripts/openapi/merge-client-endpoints.ts",
"generate-and-merge-api-specs": "pnpm run generate-api-specs && pnpm run merge-client-endpoints",
"i18n:generate": "npx lingo.dev@latest i18n"
"i18n:generate": "npx lingo.dev@latest run && npx lingo.dev@latest lockfile --force"
},
"dependencies": {
"@aws-sdk/client-s3": "3.879.0",

View File

@@ -101,7 +101,8 @@
"xm-and-surveys/surveys/link-surveys/start-at-question",
"xm-and-surveys/surveys/link-surveys/verify-email-before-survey",
"xm-and-surveys/surveys/link-surveys/market-research-panel",
"xm-and-surveys/surveys/link-surveys/pin-protected-surveys"
"xm-and-surveys/surveys/link-surveys/pin-protected-surveys",
"xm-and-surveys/surveys/link-surveys/custom-head-scripts"
]
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View File

@@ -0,0 +1,193 @@
---
title: "Custom Head Scripts"
description: "Add tracking pixels, analytics, or custom code to your link surveys for self-hosted instances."
icon: "code"
---
Custom Head Scripts allow you to inject custom HTML code into the `<head>` section of your **Link Surveys**. This is useful for adding tracking pixels, analytics scripts, chatbots, or any other third-party code.
<Note>
Custom Head Scripts is only available for **Link Surveys on self-hosted instances**. This feature is not available for Website & App Surveys or on Formbricks Cloud.
</Note>
## When to Use Custom Head Scripts
Use Custom Head Scripts when you need to:
- Add analytics tools (Google Analytics, Plausible, Mixpanel, etc.)
- Integrate tracking pixels (Facebook Pixel, LinkedIn Insight Tag, etc.)
- Include custom JavaScript for advanced survey behavior
- Add third-party widgets or chatbots
- Inject custom meta tags or stylesheets
## Configuration Guide
### Workspace-Level Scripts
These scripts apply to **all link surveys** in your workspace.
<Frame>
<img src="/images/xm-and-surveys/surveys/link-surveys/custom-head-scripts/workspace-setting.webp" alt="Custom Scripts in Workspace Settings" />
</Frame>
<Steps>
<Step title="Navigate to Workspace Settings">
Go to your Workspace Settings from the main navigation menu.
</Step>
<Step title="Locate Custom Scripts Section">
Scroll down to the **Custom Scripts** card in the General settings.
</Step>
<Step title="Add Your Scripts">
Paste your HTML code into the text area. You can include:
- `<script>` tags (inline or with `src` attribute)
- `<meta>` tags
- `<link>` tags for stylesheets
- `<style>` tags
- `<noscript>` tags
**Example:**
```html
<!-- Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=GA_MEASUREMENT_ID"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'GA_MEASUREMENT_ID');
</script>
```
</Step>
<Step title="Save Your Changes">
Click the **Save** button to apply your custom scripts to all link surveys.
</Step>
</Steps>
### Survey-Level Scripts
Override or extend workspace scripts for **specific surveys**.
<Frame>
<img src="/images/xm-and-surveys/surveys/link-surveys/custom-head-scripts/share-survey-modal-setting.webp" alt="Custom HTML tab in Share Survey Modal" />
</Frame>
<Steps>
<Step title="Open Share Survey Modal">
Navigate to your survey and click the **Share** button to open the share modal.
</Step>
<Step title="Go to Custom HTML Tab">
In the share modal, click on the **Custom HTML** tab under the "Share Settings" section.
</Step>
<Step title="Choose Script Mode">
Select how you want survey scripts to interact with workspace scripts:
- **Add to Workspace Scripts** - Survey scripts will be added **after** workspace scripts (both run)
- **Replace Workspace Scripts** - Survey scripts will **replace** workspace scripts entirely (only survey scripts run)
</Step>
<Step title="Add Survey-Specific Scripts">
Paste your HTML code into the "Survey Scripts" text area.
**Example** (Facebook Pixel for a specific campaign):
```html
<!-- Facebook Pixel for Campaign X -->
<script>
fbq('track', 'ViewContent', {
content_name: 'Customer Satisfaction Survey',
campaign: 'Q1-2024'
});
</script>
```
</Step>
<Step title="Save Changes">
Click **Save** to apply the survey-specific scripts.
</Step>
</Steps>
## To keep in mind
<Warning>
Custom scripts execute in the context of your survey pages. Only add scripts from trusted sources. Malicious scripts could compromise your survey data or user privacy.
</Warning>
- **Scripts Don't Load in Preview Mode** — Custom Head Scripts are not loaded in preview mode (editor preview or `?preview=true`). This prevents analytics tracking and pixel triggers during testing. To test your scripts, publish your survey and view it through the actual link survey URL without the preview parameter.
- **Link Surveys Only** — Custom Head Scripts only work with link surveys. They are not available for app/website surveys, as those are embedded in your application and should use your application's existing script management.
- **Self-Hosted Only** — This feature is only available on self-hosted instances. Formbricks Cloud does not support Custom Head Scripts for security and performance reasons.
- **Permissions** — Only Owners, Managers, and members with Manage access can configure Custom Head Scripts.
## Common Use Cases
### Global Analytics (Workspace Level)
Set up Google Analytics for all surveys in your workspace:
```html
<script async src="https://www.googletagmanager.com/gtag/js?id=GA_MEASUREMENT_ID"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'GA_MEASUREMENT_ID');
</script>
```
### Campaign-Specific Tracking (Survey Level with Add Mode)
Add Facebook Pixel tracking for a specific marketing campaign:
**Workspace Scripts:** Google Analytics (as above)
**Survey Scripts:**
```html
<script>
!function(f,b,e,v,n,t,s)
{if(f.fbq)return;n=f.fbq=function(){n.callMethod?
n.callMethod.apply(n,arguments):n.queue.push(arguments)};
if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
n.queue=[];t=b.createElement(e);t.async=!0;
t.src=v;s=b.getElementsByTagName(e)[0];
s.parentNode.insertBefore(t,s)}(window, document,'script',
'https://connect.facebook.net/en_US/fbevents.js');
fbq('init', 'YOUR_PIXEL_ID');
fbq('track', 'PageView');
</script>
```
**Result:** Both Google Analytics and Facebook Pixel run on this survey.
### Custom Fonts (Workspace Level)
Load custom fonts for all your surveys:
```html
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Inter', sans-serif;
}
</style>
```
## Troubleshooting
If your scripts aren't loading, check:
1. **Is it a self-hosted instance?** Custom scripts only work on self-hosted Formbricks.
2. **Are you in preview mode?** Scripts don't load in preview—test with the actual survey link.
3. **Check the browser console** for JavaScript errors that might prevent script execution.
4. **Verify your HTML syntax** is correct (properly closed tags, valid attributes).

View File

@@ -0,0 +1,9 @@
-- CreateEnum
CREATE TYPE "public"."SurveyScriptMode" AS ENUM ('add', 'replace');
-- AlterTable
ALTER TABLE "public"."Project" ADD COLUMN "customHeadScripts" TEXT;
-- AlterTable
ALTER TABLE "public"."Survey" ADD COLUMN "customHeadScripts" TEXT,
ADD COLUMN "customHeadScriptsMode" "public"."SurveyScriptMode" DEFAULT 'add';

View File

@@ -312,6 +312,11 @@ enum displayOptions {
respondMultiple
}
enum SurveyScriptMode {
add
replace
}
/// Represents a complete survey configuration including questions, styling, and display rules.
/// Core model for the survey functionality in Formbricks.
///
@@ -324,6 +329,8 @@ enum displayOptions {
/// @property displayOption - Rules for how often the survey can be shown
/// @property triggers - Actions that can trigger this survey
/// @property attributeFilters - Rules for targeting specific contacts
/// @property customHeadScripts - Survey-specific custom HTML scripts (self-hosted only)
/// @property customHeadScriptsMode - "add" (merge with project) or "replace" (override project)
model Survey {
id String @id @default(cuid())
createdAt DateTime @default(now()) @map(name: "created_at")
@@ -390,6 +397,9 @@ model Survey {
slug String? @unique
customHeadScripts String?
customHeadScriptsMode SurveyScriptMode? @default(add)
@@index([environmentId, updatedAt])
@@index([segmentId])
}
@@ -620,6 +630,7 @@ model Project {
/// [Logo]
logo Json?
projectTeams ProjectTeam[]
customHeadScripts String? // Custom HTML scripts for link surveys (self-hosted only)
@@unique([organizationId, name])
@@index([organizationId])

View File

@@ -35,7 +35,7 @@
"clean": "rimraf .turbo node_modules dist",
"test": "vitest run",
"test:coverage": "vitest run --coverage",
"i18n:generate": "npx lingo.dev@latest i18n"
"i18n:generate": "npx lingo.dev@latest run && npx lingo.dev@latest lockfile --force"
},
"dependencies": {
"@calcom/embed-snippet": "1.3.3",

View File

@@ -69,6 +69,7 @@ export const ZProject = z.object({
environments: z.array(ZEnvironment),
languages: z.array(ZLanguage),
logo: ZLogo.nullish(),
customHeadScripts: z.string().nullish(),
});
export type TProject = z.infer<typeof ZProject>;
@@ -88,6 +89,7 @@ export const ZProjectUpdateInput = z.object({
styling: ZProjectStyling.optional(),
logo: ZLogo.optional(),
teamIds: z.array(z.string()).optional(),
customHeadScripts: z.string().nullish(),
});
export type TProjectUpdateInput = z.infer<typeof ZProjectUpdateInput>;

View File

@@ -900,6 +900,8 @@ export const ZSurvey = z
languages: z.array(ZSurveyLanguage),
metadata: ZSurveyMetadata,
slug: ZSurveySlug.nullable(),
customHeadScripts: z.string().nullish(),
customHeadScriptsMode: z.enum(["add", "replace"]).nullish(),
})
.superRefine((survey, ctx) => {
const { questions, blocks, languages, welcomeCard, endings, isBackButtonHidden } = survey;

View File

@@ -163,6 +163,7 @@
"EMAIL_VERIFICATION_DISABLED",
"ENCRYPTION_KEY",
"ENTERPRISE_LICENSE_KEY",
"ENVIRONMENT",
"GITHUB_ID",
"GITHUB_SECRET",
"GOOGLE_CLIENT_ID",